| 1 | '''Domain Model based on FRBR. |
|---|
| 2 | |
|---|
| 3 | (For info on FRBR see docs/frbr.txt) |
|---|
| 4 | |
|---|
| 5 | Item (Release): corresponds to a published item (not the individual item but that |
|---|
| 6 | particular run -- i.e. something with e.g. an ISBN). Item usually |
|---|
| 7 | corresponds to a single work but can contain many works. |
|---|
| 8 | |
|---|
| 9 | Work: Represents the underlying work. So, for example, different published |
|---|
| 10 | editions (or recordings) of Beethoven\'s Ninth Symphony will all have an |
|---|
| 11 | associated underlying work "Beethoven\'s Ninth Symphony". |
|---|
| 12 | |
|---|
| 13 | Person: any kind of entity |
|---|
| 14 | |
|---|
| 15 | Persons are associated to Works and Items in a many 2 many relationship with a |
|---|
| 16 | role attribute. |
|---|
| 17 | * We also allow the same person to be associated with the same work/item many |
|---|
| 18 | times (to allow for the possibility that the same person has multiple |
|---|
| 19 | roles). |
|---|
| 20 | |
|---|
| 21 | Relation to FRBR |
|---|
| 22 | ================ |
|---|
| 23 | |
|---|
| 24 | FRBR has conceptual model: |
|---|
| 25 | |
|---|
| 26 | Work |
|---|
| 27 | - Expression (aka Edition) |
|---|
| 28 | - Manifestation |
|---|
| 29 | - Item |
|---|
| 30 | |
|---|
| 31 | Crudely our mapping is: |
|---|
| 32 | |
|---|
| 33 | PDW Work --> FRBR Work (+ some FRBR Expressions) |
|---|
| 34 | PDW Item --> FRBR Manifestation (+ some FRBR expressions) |
|---|
| 35 | |
|---|
| 36 | * Works have types. |
|---|
| 37 | * We allow works to refer to other works. |
|---|
| 38 | * An item may have many works. |
|---|
| 39 | |
|---|
| 40 | Examples of how this works: |
|---|
| 41 | --------------------------- |
|---|
| 42 | |
|---|
| 43 | 0. Catalogues (from e.g. a Library or Amazon) |
|---|
| 44 | |
|---|
| 45 | All catalogues list Items in our system (in FRBR terms one could see them as |
|---|
| 46 | Manifestation or Editions). |
|---|
| 47 | |
|---|
| 48 | 1. Book: "Forsyte Saga (vol 1)" by Galsworthy |
|---|
| 49 | |
|---|
| 50 | work: Forsyte Saga (vol 1) |
|---|
| 51 | expression: Forsyte Saga X Edition |
|---|
| 52 | manifestation/item: Penguin book isbn XXXXX published ... |
|---|
| 53 | |
|---|
| 54 | In our system we we\'d have a single work for the Forsyte saga. A new edition would |
|---|
| 55 | not be registered as a new work *unless* it represented such a major change as |
|---|
| 56 | justifying a new copyright (for example a translation would fall into this |
|---|
| 57 | category). |
|---|
| 58 | |
|---|
| 59 | The Manifestation/Item becomes an Item in our system. |
|---|
| 60 | |
|---|
| 61 | |
|---|
| 62 | 2. Recording: von Karajan conducting Beethoven Symphony No. 1 in A minor |
|---|
| 63 | |
|---|
| 64 | work: (composition) Beethoven Symphony in A minor |
|---|
| 65 | work: (recording/performance): von Karajan recording |
|---|
| 66 | |
|---|
| 67 | manifestation (release) |
|---|
| 68 | * may contain many recordings |
|---|
| 69 | * has tracks etc |
|---|
| 70 | |
|---|
| 71 | In our system: |
|---|
| 72 | * Work 1: Beethoven Symphony |
|---|
| 73 | * Work 2: von Karajan recording |
|---|
| 74 | * Item: The release |
|---|
| 75 | |
|---|
| 76 | |
|---|
| 77 | Notes on FlexiDate implementation |
|---|
| 78 | ================================= |
|---|
| 79 | |
|---|
| 80 | Support for having arbitrary dates but which can be sorted properly and have an |
|---|
| 81 | associated machine-readable version. |
|---|
| 82 | |
|---|
| 83 | For any given date attributed \'date\' end up with 3 cols: |
|---|
| 84 | |
|---|
| 85 | * _date (represented on object as date) |
|---|
| 86 | * date_normed |
|---|
| 87 | * date_ordered |
|---|
| 88 | ''' |
|---|
| 89 | import os |
|---|
| 90 | import urllib |
|---|
| 91 | |
|---|
| 92 | # SQLAlchemy stuff |
|---|
| 93 | from sqlalchemy import * |
|---|
| 94 | from sqlalchemy import orm |
|---|
| 95 | from sqlalchemy.orm.collections import attribute_mapped_collection |
|---|
| 96 | from sqlalchemy.ext.associationproxy import association_proxy |
|---|
| 97 | import swiss |
|---|
| 98 | |
|---|
| 99 | from types import * |
|---|
| 100 | from meta import metadata, Session |
|---|
| 101 | import pdw.name |
|---|
| 102 | import pdw.pd |
|---|
| 103 | from base import DomainObject, add_flexidate |
|---|
| 104 | |
|---|
| 105 | mapper = Session.mapper |
|---|
| 106 | |
|---|
| 107 | PD_KEY = 'pd.pdw.default' |
|---|
| 108 | |
|---|
| 109 | # Enumerations |
|---|
| 110 | class ROLES: |
|---|
| 111 | author = u'author' |
|---|
| 112 | editor = u'editor' |
|---|
| 113 | performer = u'performer' |
|---|
| 114 | |
|---|
| 115 | class WORK_TYPES: |
|---|
| 116 | text = u'text' |
|---|
| 117 | composition = u'composition' |
|---|
| 118 | recording = u'recording' |
|---|
| 119 | photograph = u'photograph' |
|---|
| 120 | video = u'video' |
|---|
| 121 | database = u'database' |
|---|
| 122 | |
|---|
| 123 | class ENTITY_TYPES: |
|---|
| 124 | person = u'person' |
|---|
| 125 | organization = u'organization' |
|---|
| 126 | unknown = u'unknown' |
|---|
| 127 | |
|---|
| 128 | person_table = Table('person', metadata, |
|---|
| 129 | Column('id', UuidType, primary_key=True, default=UuidType.default), |
|---|
| 130 | Column('srcid', UnicodeText), |
|---|
| 131 | Column('name', UnicodeText, index=True), |
|---|
| 132 | Column('aka', UnicodeText), |
|---|
| 133 | # Flexidate columns |
|---|
| 134 | Column('birth_date', types.UnicodeText), # user entered version |
|---|
| 135 | Column('birth_date_normed', types.UnicodeText), # iso 8601 |
|---|
| 136 | Column('birth_date_ordered', types.Float), # ordered version see swiss.date |
|---|
| 137 | # Flexidate columns |
|---|
| 138 | Column('death_date', types.UnicodeText), # user entered version |
|---|
| 139 | Column('death_date_normed', types.UnicodeText), # iso 8601 |
|---|
| 140 | Column('death_date_ordered', types.Float), # ordered version see swiss.date |
|---|
| 141 | Column('entity_type', Unicode(255), default=ENTITY_TYPES.person), |
|---|
| 142 | Column('notes', UnicodeText), |
|---|
| 143 | ) |
|---|
| 144 | |
|---|
| 145 | work_table = Table('work', metadata, |
|---|
| 146 | Column('id', UuidType, primary_key=True, default=UuidType.default), |
|---|
| 147 | Column('srcid', UnicodeText), |
|---|
| 148 | Column('title', UnicodeText), |
|---|
| 149 | Column('type', Unicode(100), default=WORK_TYPES.text), |
|---|
| 150 | # Flexidate columns |
|---|
| 151 | Column('date', types.UnicodeText), # user entered version |
|---|
| 152 | Column('date_normed', types.UnicodeText), # iso 8601 |
|---|
| 153 | Column('date_ordered', types.Float), # ordered version see swiss.date |
|---|
| 154 | Column('notes', UnicodeText), |
|---|
| 155 | ) |
|---|
| 156 | |
|---|
| 157 | item_table = Table('item', metadata, |
|---|
| 158 | Column('id', UuidType, primary_key=True, default=UuidType.default), |
|---|
| 159 | Column('srcid', UnicodeText), |
|---|
| 160 | Column('title', UnicodeText), |
|---|
| 161 | # Flexidate columns |
|---|
| 162 | Column('date', types.UnicodeText), # user entered version |
|---|
| 163 | Column('date_normed', types.UnicodeText), # iso 8601 |
|---|
| 164 | Column('date_ordered', types.Float), # ordered version see swiss.date |
|---|
| 165 | Column('type', Unicode(100), default=WORK_TYPES.text), |
|---|
| 166 | Column('notes', UnicodeText), |
|---|
| 167 | ) |
|---|
| 168 | |
|---|
| 169 | extra_table = Table('extra', metadata, |
|---|
| 170 | Column('id', Integer, primary_key=True), |
|---|
| 171 | # name of table |
|---|
| 172 | Column('table', Unicode(100)), |
|---|
| 173 | # must always have UUIDs as pks ... |
|---|
| 174 | Column('fkid', UuidType, index=True), |
|---|
| 175 | Column('key', UnicodeText), |
|---|
| 176 | Column('value', JsonType), |
|---|
| 177 | ) |
|---|
| 178 | |
|---|
| 179 | work_2_item = Table('work_2_item', metadata, |
|---|
| 180 | Column('work_id', UuidType, ForeignKey('work.id'), primary_key=True, |
|---|
| 181 | index=True), |
|---|
| 182 | Column('item_id', UuidType, ForeignKey('item.id'), primary_key=True, |
|---|
| 183 | index=True), |
|---|
| 184 | ) |
|---|
| 185 | |
|---|
| 186 | work_2_person = Table('work_2_person', metadata, |
|---|
| 187 | Column('work_id', UuidType, ForeignKey('work.id'), primary_key=True, |
|---|
| 188 | index=True), |
|---|
| 189 | Column('person_id', UuidType, ForeignKey('person.id'), primary_key=True, |
|---|
| 190 | index=True), |
|---|
| 191 | Column('role', UnicodeText, default=ROLES.author) |
|---|
| 192 | ) |
|---|
| 193 | |
|---|
| 194 | item_2_person = Table('item_2_person', metadata, |
|---|
| 195 | Column('item_id', UuidType, ForeignKey('item.id'), primary_key=True, |
|---|
| 196 | index=True), |
|---|
| 197 | Column('person_id', UuidType, ForeignKey('person.id'), primary_key=True, |
|---|
| 198 | index=True), |
|---|
| 199 | Column('role', UnicodeText, default=ROLES.author) |
|---|
| 200 | ) |
|---|
| 201 | |
|---|
| 202 | |
|---|
| 203 | def _create_person_work(work, role=ROLES.author): |
|---|
| 204 | return WorkPerson(work=work, role=role) |
|---|
| 205 | |
|---|
| 206 | class Person(DomainObject): |
|---|
| 207 | items = association_proxy('item_persons', 'item', |
|---|
| 208 | #creator=_create_item_person |
|---|
| 209 | ) |
|---|
| 210 | works = association_proxy('work_persons', 'work', |
|---|
| 211 | # creator=_create_person_work |
|---|
| 212 | ) |
|---|
| 213 | |
|---|
| 214 | @classmethod |
|---|
| 215 | def by_name(self, name, create=True): |
|---|
| 216 | '''Return first Person with name `name`, or create if does not |
|---|
| 217 | exist and `create` is True. |
|---|
| 218 | ''' |
|---|
| 219 | out = self.query.filter_by(name=name).first() |
|---|
| 220 | if out: |
|---|
| 221 | return out |
|---|
| 222 | elif create: |
|---|
| 223 | return Person(name=name) |
|---|
| 224 | else: |
|---|
| 225 | return None |
|---|
| 226 | |
|---|
| 227 | def _readable_id(self): |
|---|
| 228 | name_str = pdw.name.normalize(self.name, parser_class=pdw.name.FirstLast).strip() |
|---|
| 229 | name_str = name_str.encode('utf8') # needs to be utf8 not unicode for the urllib.quote |
|---|
| 230 | name_str = name_str.replace(' ', '_') |
|---|
| 231 | encoded_name = urllib.quote(name_str) |
|---|
| 232 | |
|---|
| 233 | return encoded_name |
|---|
| 234 | readable_id = property(_readable_id) |
|---|
| 235 | |
|---|
| 236 | def _url(self): |
|---|
| 237 | from routes import url_for |
|---|
| 238 | return url_for(controller='person', action='read', compressed_id=swiss.compress_uuid(self.id), readable_id=self.readable_id) |
|---|
| 239 | url = property(_url) |
|---|
| 240 | |
|---|
| 241 | def _pd(self): |
|---|
| 242 | pd = self.extras.get(PD_KEY) |
|---|
| 243 | if pd is None: |
|---|
| 244 | # TODO this is a hack at the mo |
|---|
| 245 | if self.death_date_ordered: |
|---|
| 246 | if self.death_date_ordered < 1938: |
|---|
| 247 | pd = 1.0 |
|---|
| 248 | else: |
|---|
| 249 | pd = 0.0 |
|---|
| 250 | else: |
|---|
| 251 | pd = 0.1 |
|---|
| 252 | # pd = pdw.pd.determine_status(self, 'uk') |
|---|
| 253 | self.extras[PD_KEY] = pd |
|---|
| 254 | return pd |
|---|
| 255 | pd = property(_pd) |
|---|
| 256 | |
|---|
| 257 | def _decode_person_readable_id(readable_id): |
|---|
| 258 | name_str = urllib.unquote(readable_id) |
|---|
| 259 | name_str = name_str.replace('_', ' ') |
|---|
| 260 | name_str = name_str.decode('utf8') |
|---|
| 261 | decoded_name = pdw.name.normalize(name_str, parser_class=pdw.name.LastFirst).strip() |
|---|
| 262 | return decoded_name |
|---|
| 263 | |
|---|
| 264 | def get_persons_matching_readable_id_query(readable_id): |
|---|
| 265 | name = _decode_person_readable_id(readable_id) |
|---|
| 266 | artist_query = Person.query.filter_by(name=name) |
|---|
| 267 | return artist_query |
|---|
| 268 | |
|---|
| 269 | |
|---|
| 270 | |
|---|
| 271 | def _create_work_person(person, role=ROLES.author): |
|---|
| 272 | return WorkPerson(person=person, role=role) |
|---|
| 273 | |
|---|
| 274 | class Work(DomainObject): |
|---|
| 275 | persons = association_proxy('work_persons', 'person', creator=_create_work_person) |
|---|
| 276 | |
|---|
| 277 | def __unicode__(self): |
|---|
| 278 | out = super(Work, self).__unicode__() |
|---|
| 279 | out += u' person=%s' % [ a.__unicode__() for a in self.persons ] |
|---|
| 280 | return out |
|---|
| 281 | |
|---|
| 282 | @classmethod |
|---|
| 283 | def from_dict(self, in_dict): |
|---|
| 284 | # must convert persons first as o/w will attempt to set work.persons |
|---|
| 285 | # with dict (which won't work!) |
|---|
| 286 | local_dict = in_dict.copy() # we create a copy of the data, to keep the |
|---|
| 287 | # submitted original |
|---|
| 288 | persons = [] |
|---|
| 289 | for person_dict in local_dict.get('persons', []): |
|---|
| 290 | person = Person.from_dict(person_dict) |
|---|
| 291 | persons.append(person) |
|---|
| 292 | if 'persons' in local_dict: |
|---|
| 293 | del local_dict['persons'] |
|---|
| 294 | items = [] |
|---|
| 295 | for item_dict in local_dict.get('items',[]): |
|---|
| 296 | item = Item.from_dict(item_dict) |
|---|
| 297 | if 'items' in local_dict: |
|---|
| 298 | del local_dict['persons'] |
|---|
| 299 | |
|---|
| 300 | out_obj = super(Work, self).from_dict(local_dict) |
|---|
| 301 | out_obj.persons = persons |
|---|
| 302 | out_obj.items = items |
|---|
| 303 | return out_obj |
|---|
| 304 | def to_dict(self): |
|---|
| 305 | out_dict = {} |
|---|
| 306 | table = orm.class_mapper(self.__class__).mapped_table |
|---|
| 307 | # change ids into references? (e.g. license_id to license ...) |
|---|
| 308 | for col in table.c: |
|---|
| 309 | val = getattr(self, col.name) |
|---|
| 310 | out_dict[col.name] = val |
|---|
| 311 | # TODO: check type ... |
|---|
| 312 | if self.persons: |
|---|
| 313 | persons = [] |
|---|
| 314 | for person in self.persons: |
|---|
| 315 | oneperson = {'name': person.name, |
|---|
| 316 | 'birth_date': person.birth_date, |
|---|
| 317 | 'death_date': person.death_date, |
|---|
| 318 | 'type': person.type, |
|---|
| 319 | 'country': person.country,} |
|---|
| 320 | persons.append(oneperson) |
|---|
| 321 | out_dict['persons'] = persons |
|---|
| 322 | |
|---|
| 323 | |
|---|
| 324 | return out_dict |
|---|
| 325 | |
|---|
| 326 | |
|---|
| 327 | |
|---|
| 328 | |
|---|
| 329 | def _readable_id(self): |
|---|
| 330 | if self.persons: |
|---|
| 331 | name = self.persons[0].name # assume first person is creator for now |
|---|
| 332 | name_str = pdw.name.normalize(name, parser_class=pdw.name.FirstLast).strip() |
|---|
| 333 | else: |
|---|
| 334 | name_str = "" |
|---|
| 335 | encoded_name = ('%s--%s' % (self.title, name_str)).replace(' ', '_') |
|---|
| 336 | |
|---|
| 337 | return encoded_name |
|---|
| 338 | readable_id = property(_readable_id) |
|---|
| 339 | |
|---|
| 340 | def _url(self): |
|---|
| 341 | from routes import url_for |
|---|
| 342 | return url_for(controller='work', action='read', compressed_id=swiss.compress_uuid(self.id), readable_id=self.readable_id) |
|---|
| 343 | url = property(_url) |
|---|
| 344 | |
|---|
| 345 | def _pd(self): |
|---|
| 346 | pd = self.extras.get(PD_KEY) |
|---|
| 347 | if pd is None: |
|---|
| 348 | pd_calc_parcel = pdw.pd.determine_status(self, 'uk') |
|---|
| 349 | if pd_calc_parcel.uncertainty == 0.0: |
|---|
| 350 | pd = pd_calc_parcel.pd_prob |
|---|
| 351 | else: |
|---|
| 352 | pd = 0.1 |
|---|
| 353 | self.extras[PD_KEY] = pd |
|---|
| 354 | return pd |
|---|
| 355 | pd = property(_pd) |
|---|
| 356 | |
|---|
| 357 | def _decode_work_readable_id(readable_id): |
|---|
| 358 | decoded_title, decoded_name = "", "" |
|---|
| 359 | readable_id_sections = readable_id.split("--") |
|---|
| 360 | if len(readable_id_sections) == 2: |
|---|
| 361 | for i in range(len(readable_id_sections)): |
|---|
| 362 | readable_id_sections[i] = readable_id_sections[i].replace('_', ' ') |
|---|
| 363 | decoded_title, name_str = readable_id_sections |
|---|
| 364 | decoded_name = pdw.name.normalize(name_str, parser_class=pdw.name.LastFirst).strip() |
|---|
| 365 | return decoded_title, decoded_name |
|---|
| 366 | |
|---|
| 367 | def get_works_matching_readable_id_query(readable_id): |
|---|
| 368 | title, name = _decode_work_readable_id(readable_id) |
|---|
| 369 | query = Work.query.filter(and_(Work.title==title, Person.name==name)) |
|---|
| 370 | return query.all() |
|---|
| 371 | |
|---|
| 372 | |
|---|
| 373 | def _create_item_person(person, role=ROLES.author): |
|---|
| 374 | return ItemPerson(person=person, role=role) |
|---|
| 375 | |
|---|
| 376 | class Item(DomainObject): |
|---|
| 377 | def __unicode__(self): |
|---|
| 378 | out = super(Item, self).__unicode__() |
|---|
| 379 | out += u' person=%s' % [ a.__unicode__() for a in self.persons ] |
|---|
| 380 | return out |
|---|
| 381 | persons = association_proxy('item_persons', 'person', creator=_create_item_person) |
|---|
| 382 | |
|---|
| 383 | def _readable_id(self): |
|---|
| 384 | if self.persons: |
|---|
| 385 | name = self.persons[0].name # assume first person is creator for now |
|---|
| 386 | name_str = pdw.name.normalize(name, parser_class=pdw.name.FirstLast).strip() |
|---|
| 387 | else: |
|---|
| 388 | name_str = "" |
|---|
| 389 | encoded_name = ('%s--%s' % (self.title, name_str)).replace(' ', '_') |
|---|
| 390 | |
|---|
| 391 | return encoded_name |
|---|
| 392 | readable_id = property(_readable_id) |
|---|
| 393 | |
|---|
| 394 | def _url(self): |
|---|
| 395 | from routes import url_for |
|---|
| 396 | return url_for(controller='item', action='read', compressed_id=swiss.compress_uuid(self.id), readable_id=self.readable_id) |
|---|
| 397 | url = property(_url) |
|---|
| 398 | |
|---|
| 399 | def _decode_item_readable_id(readable_id): |
|---|
| 400 | decoded_title, decoded_name = "", "" |
|---|
| 401 | readable_id_sections = readable_id.split("--") |
|---|
| 402 | if len(readable_id_sections) == 2: |
|---|
| 403 | for i in range(len(readable_id_sections)): |
|---|
| 404 | readable_id_sections[i] = readable_id_sections[i].replace('_', ' ') |
|---|
| 405 | decoded_title, name_str = readable_id_sections |
|---|
| 406 | decoded_name = pdw.name.normalize(name_str, parser_class=pdw.name.LastFirst).strip() |
|---|
| 407 | return decoded_title, decoded_name |
|---|
| 408 | |
|---|
| 409 | def get_items_matching_readable_id_query(readable_id): |
|---|
| 410 | title, name = _decode_item_readable_id(readable_id) |
|---|
| 411 | query = Item.query.filter(and_(Item.title==title, Person.name==name)) |
|---|
| 412 | return query |
|---|
| 413 | |
|---|
| 414 | class WorkItem(DomainObject): |
|---|
| 415 | pass |
|---|
| 416 | |
|---|
| 417 | class WorkPerson(DomainObject): |
|---|
| 418 | def __init__(self, work=None, person=None, role=None): |
|---|
| 419 | self.work = work |
|---|
| 420 | self.person = person |
|---|
| 421 | self.role = role |
|---|
| 422 | |
|---|
| 423 | |
|---|
| 424 | class ItemPerson(DomainObject): |
|---|
| 425 | def __init__(self, item=None, person=None, role=None): |
|---|
| 426 | self.item = item |
|---|
| 427 | self.person = person |
|---|
| 428 | self.role = role |
|---|
| 429 | |
|---|
| 430 | class Extra(DomainObject): |
|---|
| 431 | pass |
|---|
| 432 | |
|---|
| 433 | def extraable(cls, name='_extras', usedict=True): |
|---|
| 434 | mapper = orm.class_mapper(cls) |
|---|
| 435 | table = mapper.local_table |
|---|
| 436 | table_name = unicode(table.name) |
|---|
| 437 | primaryjoin = and_( |
|---|
| 438 | extra_table.c.table == table_name, |
|---|
| 439 | extra_table.c.fkid == list(table.primary_key)[0] |
|---|
| 440 | ) |
|---|
| 441 | foreign_keys = [extra_table.c.fkid] |
|---|
| 442 | from sqlalchemy.orm.collections import attribute_mapped_collection |
|---|
| 443 | mapper.add_property(name, orm.relation( |
|---|
| 444 | Extra, |
|---|
| 445 | primaryjoin=primaryjoin, |
|---|
| 446 | foreign_keys=foreign_keys, |
|---|
| 447 | collection_class=attribute_mapped_collection('key'), |
|---|
| 448 | # backref |
|---|
| 449 | ) |
|---|
| 450 | ) |
|---|
| 451 | from sqlalchemy.ext.associationproxy import association_proxy |
|---|
| 452 | def _create_extra(key, value): |
|---|
| 453 | return Extra(table=table_name, key=unicode(key), value=value) |
|---|
| 454 | cls.extras = association_proxy('_extras', 'value', |
|---|
| 455 | creator=_create_extra) |
|---|
| 456 | |
|---|
| 457 | |
|---|
| 458 | add_flexidate(Person, 'birth_date') |
|---|
| 459 | add_flexidate(Person, 'death_date') |
|---|
| 460 | add_flexidate(Work, 'date') |
|---|
| 461 | add_flexidate(Item, 'date') |
|---|
| 462 | |
|---|
| 463 | ## Custom Property for FlexiDate |
|---|
| 464 | |
|---|
| 465 | # TODO: not yet usable properly as require SQLAlchemy >= 0.5 |
|---|
| 466 | from sqlalchemy import sql |
|---|
| 467 | class FlexiDateComparator(orm.PropComparator): |
|---|
| 468 | |
|---|
| 469 | def _get_ordered(self, other): |
|---|
| 470 | ordered_name = self.prop.name + '_ordered' |
|---|
| 471 | col = self.prop.table.c[ordered_name] |
|---|
| 472 | other = swiss.date.parse(other).as_float() |
|---|
| 473 | return (col, other) |
|---|
| 474 | |
|---|
| 475 | def __gt__(self, other): |
|---|
| 476 | """define the 'greater than' operation""" |
|---|
| 477 | col, val = self._get_ordered(other) |
|---|
| 478 | return col < val |
|---|
| 479 | |
|---|
| 480 | def __eq__(self, other): |
|---|
| 481 | col, val = self._get_ordered(other) |
|---|
| 482 | return col == val |
|---|
| 483 | |
|---|
| 484 | mapper(Work, work_table, properties={ |
|---|
| 485 | # should use FlexiDateComparator here |
|---|
| 486 | # but not supported in synonyms until SQLAlchemy 0.5 |
|---|
| 487 | # comparator_factory=FlexiDateComparator |
|---|
| 488 | 'date':orm.synonym('_date', map_column=True), |
|---|
| 489 | 'items':orm.relation(Item, secondary=work_2_item, backref='works'), |
|---|
| 490 | }, |
|---|
| 491 | order_by=work_table.c.title, |
|---|
| 492 | ) |
|---|
| 493 | mapper(Item, item_table, properties={ |
|---|
| 494 | # comparator_factory=FlexiDateComparator |
|---|
| 495 | 'date':orm.synonym('_date', map_column=True), |
|---|
| 496 | }, |
|---|
| 497 | order_by=item_table.c.title, |
|---|
| 498 | ) |
|---|
| 499 | mapper(Person, person_table, properties={ |
|---|
| 500 | # comparator_factory=FlexiDateComparator |
|---|
| 501 | 'birth_date':orm.synonym('_birth_date', map_column=True), |
|---|
| 502 | 'death_date':orm.synonym('_death_date', map_column=True), |
|---|
| 503 | }, |
|---|
| 504 | order_by=person_table.c.name, |
|---|
| 505 | ) |
|---|
| 506 | mapper(WorkPerson, work_2_person, properties={ |
|---|
| 507 | 'work': orm.relation(Work, |
|---|
| 508 | backref=orm.backref('work_persons', |
|---|
| 509 | cascade='all, delete, delete-orphan', |
|---|
| 510 | ), |
|---|
| 511 | # pointless ... |
|---|
| 512 | order_by=work_table.c.title, |
|---|
| 513 | ), |
|---|
| 514 | 'person': orm.relation(Person, |
|---|
| 515 | backref=orm.backref('work_persons', |
|---|
| 516 | cascade='all, delete, delete-orphan', |
|---|
| 517 | ), |
|---|
| 518 | # pointless ... |
|---|
| 519 | order_by=person_table.c.name, |
|---|
| 520 | ), |
|---|
| 521 | }, |
|---|
| 522 | order_by=work_2_person.c.work_id, |
|---|
| 523 | ) |
|---|
| 524 | mapper(ItemPerson, item_2_person, properties={ |
|---|
| 525 | 'item': orm.relation(Item, |
|---|
| 526 | backref=orm.backref('item_persons', |
|---|
| 527 | cascade='all, delete, delete-orphan'), |
|---|
| 528 | ), |
|---|
| 529 | 'person': orm.relation(Person, |
|---|
| 530 | backref=orm.backref('item_persons', |
|---|
| 531 | cascade='all, delete, delete-orphan'), |
|---|
| 532 | ), |
|---|
| 533 | }, |
|---|
| 534 | order_by=item_2_person.c.item_id, |
|---|
| 535 | ) |
|---|
| 536 | mapper(WorkItem, work_2_item) |
|---|
| 537 | mapper(Extra, extra_table) |
|---|
| 538 | extraable(Work) |
|---|
| 539 | extraable(Item) |
|---|
| 540 | extraable(Person) |
|---|