| 1 | """Determine copyright status of works given relevant information such as |
|---|
| 2 | creator death date. |
|---|
| 3 | |
|---|
| 4 | Remarks |
|---|
| 5 | ======= |
|---|
| 6 | |
|---|
| 7 | Often cannot calculate PD with certainty (at least in an automated process). Different kinds of uncertainty: |
|---|
| 8 | |
|---|
| 9 | * Do not know persons associated with work (with certainty or at all) |
|---|
| 10 | * Do not know their death dates (may or may not know birth dates) |
|---|
| 11 | * Do not know when work was published |
|---|
| 12 | * etc |
|---|
| 13 | |
|---|
| 14 | And this is even assuming we have identified the work (and/or whether it is a |
|---|
| 15 | translation, critical edition etc etc). |
|---|
| 16 | |
|---|
| 17 | This uncertainty must therefore be presented. Core features |
|---|
| 18 | |
|---|
| 19 | * PD/Copyright status |
|---|
| 20 | * Level of uncertainty ... |
|---|
| 21 | * Comments |
|---|
| 22 | * Compressed comments/Standard flags |
|---|
| 23 | |
|---|
| 24 | Suggestions for flags: |
|---|
| 25 | * normalized title |
|---|
| 26 | * notes |
|---|
| 27 | * translation |
|---|
| 28 | |
|---|
| 29 | def determine_status(work, jurisdiction, when): |
|---|
| 30 | # now dispatch on jurisdiction (+ work type?) |
|---|
| 31 | |
|---|
| 32 | class CopyrightStatus: |
|---|
| 33 | # Flags etc |
|---|
| 34 | |
|---|
| 35 | class CalculatorBase: |
|---|
| 36 | def get_work_status(self, work): |
|---|
| 37 | |
|---|
| 38 | class CalculatorUK(CalculatorBase): |
|---|
| 39 | pass |
|---|
| 40 | """ |
|---|
| 41 | import logging |
|---|
| 42 | import datetime |
|---|
| 43 | |
|---|
| 44 | import swiss |
|---|
| 45 | try: |
|---|
| 46 | import json |
|---|
| 47 | except ImportError: |
|---|
| 48 | import simplejson as json |
|---|
| 49 | |
|---|
| 50 | #from sqlalchemy import orm |
|---|
| 51 | import pdw |
|---|
| 52 | |
|---|
| 53 | |
|---|
| 54 | logger = logging.getLogger('pdw.pd') |
|---|
| 55 | |
|---|
| 56 | |
|---|
| 57 | OLDEST_PERSON = 100 |
|---|
| 58 | |
|---|
| 59 | now = datetime.datetime.now() |
|---|
| 60 | |
|---|
| 61 | def float_date(year, month=0, day=0): |
|---|
| 62 | '''to convert a date in a float type''' |
|---|
| 63 | return swiss.date.FlexiDate(year, month, day).as_float() |
|---|
| 64 | |
|---|
| 65 | def determine_status(work, jurisdiction): |
|---|
| 66 | # now dispatch on jurisdiction (+ work type?) |
|---|
| 67 | # note two letter country codes based on ISO 3166 |
|---|
| 68 | # need to add 'when' parameter here and deeper |
|---|
| 69 | if jurisdiction == 'us': |
|---|
| 70 | pd_calculator = CalculatorUnitedStates |
|---|
| 71 | elif jurisdiction == 'ca': |
|---|
| 72 | pd_calculator = CalculatorCanada |
|---|
| 73 | elif jurisdiction in ('uk', 'gb') : |
|---|
| 74 | pd_calculator = CalculatorUk |
|---|
| 75 | else: |
|---|
| 76 | logger.error('Jurisdiction "%s" not currently supported. Try us,ca,uk' % jurisdiction) |
|---|
| 77 | assert 0 |
|---|
| 78 | return pd_calculator().get_work_status(work) |
|---|
| 79 | |
|---|
| 80 | def determine_status_from_raw(json_data): |
|---|
| 81 | ''' |
|---|
| 82 | To create a Work object and analize its pd status from a |
|---|
| 83 | python or json dict |
|---|
| 84 | @param params: json formatted string |
|---|
| 85 | @type params: string |
|---|
| 86 | ''' |
|---|
| 87 | params = json.loads(json_data) |
|---|
| 88 | jur = params['jurisdiction'] |
|---|
| 89 | |
|---|
| 90 | work = pdw.model.Work.from_dict(params['work']) |
|---|
| 91 | |
|---|
| 92 | calculation = determine_status(work, jur) |
|---|
| 93 | result = json.dumps({'pd_probability': calculation.pd_prob, |
|---|
| 94 | 'confidence': 1-calculation.uncertainty, |
|---|
| 95 | 'log': calculation.log, |
|---|
| 96 | 'input': params},indent=2) |
|---|
| 97 | return result |
|---|
| 98 | |
|---|
| 99 | |
|---|
| 100 | class CalcResult(object): |
|---|
| 101 | '''A CalcResult object is returned by the calculator |
|---|
| 102 | ''' |
|---|
| 103 | def __init__(self): |
|---|
| 104 | self.calc_finished = False |
|---|
| 105 | self.date_pd = None |
|---|
| 106 | self.pd_prob = 0.0 # P(is PD) |
|---|
| 107 | self.uncertainty = 0.0 |
|---|
| 108 | self.log = [] # strings |
|---|
| 109 | |
|---|
| 110 | def __str__(self): |
|---|
| 111 | return "date_pd=%s pd_prob=%s log=%s" % (self.date_pd, self.pd_prob, self.log) |
|---|
| 112 | |
|---|
| 113 | @classmethod |
|---|
| 114 | def to_dict(cls, self): |
|---|
| 115 | '''Creates a dict result to give back as json in the api |
|---|
| 116 | ''' |
|---|
| 117 | out_dict = { 'confidence': 1-self.uncertainty, |
|---|
| 118 | 'pd probability': self.pd_prob, |
|---|
| 119 | 'date pd': self.date_pd, |
|---|
| 120 | 'log': self.log, |
|---|
| 121 | } |
|---|
| 122 | return out_dict |
|---|
| 123 | #TODO |
|---|
| 124 | |
|---|
| 125 | class CalculatorBase(object): |
|---|
| 126 | """A Public Domain Calculator |
|---|
| 127 | when=None means today's date |
|---|
| 128 | """ |
|---|
| 129 | def __init__(self, when): |
|---|
| 130 | self.author_list = None |
|---|
| 131 | self.death_dates = [] |
|---|
| 132 | self.names = [] |
|---|
| 133 | if when: |
|---|
| 134 | self._now = when |
|---|
| 135 | else: |
|---|
| 136 | self._now = float_date(datetime.date.today().year) |
|---|
| 137 | |
|---|
| 138 | def get_work_status(self, work): |
|---|
| 139 | self.calc_result = CalcResult() |
|---|
| 140 | self.work = work |
|---|
| 141 | |
|---|
| 142 | def get_author_list(self, parcel): |
|---|
| 143 | if self.author_list == None: |
|---|
| 144 | self.author_list = [] |
|---|
| 145 | for person in self.work.persons: |
|---|
| 146 | self.author_list.append(person.name) |
|---|
| 147 | return self.author_list |
|---|
| 148 | |
|---|
| 149 | def calc_anon(self): |
|---|
| 150 | self.calc_result.is_anon = False |
|---|
| 151 | for person in self.work.persons: |
|---|
| 152 | if person.name.lower() in ('anon', 'anon.', 'anonymous') : |
|---|
| 153 | self.calc_result.is_anon = True |
|---|
| 154 | |
|---|
| 155 | def calc_death_dates(self): |
|---|
| 156 | death_dates = [] # list of: (name, float date) |
|---|
| 157 | names = [] |
|---|
| 158 | most_recent_death_date = 0.0 |
|---|
| 159 | for person in self.work.persons: |
|---|
| 160 | death_date = person.death_date_ordered |
|---|
| 161 | # if we have no deathdate but do have a birthdate, assume |
|---|
| 162 | # death OLDEST_PERSON years after birth |
|---|
| 163 | if not death_date and person.birth_date_ordered: |
|---|
| 164 | death_date = person.birth_date_ordered + OLDEST_PERSON |
|---|
| 165 | self.calc_result.log.append('Author "%s" death date not given - assuming died %s years after birth (%s + %s = %s)' % (person.name, OLDEST_PERSON, person.birth_date_ordered, OLDEST_PERSON, death_date)) |
|---|
| 166 | death_dates.append((person.name, death_date)) |
|---|
| 167 | if death_date and death_date > most_recent_death_date: |
|---|
| 168 | most_recent_death_date = death_date |
|---|
| 169 | an_author_lives = False |
|---|
| 170 | for name, death_date in death_dates: |
|---|
| 171 | if not death_date: |
|---|
| 172 | # no birthday |
|---|
| 173 | # alive: big assumption! |
|---|
| 174 | self.calc_result.log.append('Author "%s" death date not given - assuming alive (!)' % name) |
|---|
| 175 | an_author_lives = True |
|---|
| 176 | |
|---|
| 177 | self.calc_result.death_dates = death_dates |
|---|
| 178 | self.calc_result.most_recent_death_date = most_recent_death_date |
|---|
| 179 | self.calc_result.an_author_lives = an_author_lives |
|---|
| 180 | |
|---|
| 181 | from uk import * |
|---|
| 182 | from us import * |
|---|
| 183 | from ca import * |
|---|
| 184 | from fast import * |
|---|