class ProxyableModel(Model): proxy = db.ForeignKeyField(Proxy, unique=True) municipality = db.ForeignKeyField(Municipality, related_name='{classname}s') attributes = db.HStoreField(null=True) class Meta: auto_increment = False @property def housenumbers(self): qs = (self.proxy.housenumbers | self.proxy.housenumber_set) return qs.order_by(peewee.SQL('number'), peewee.SQL('ordinal')) def save(self, *args, **kwargs): if not self.id: proxy = Proxy(kind=self.resource) proxy.save() self.proxy = proxy.id self.id = proxy.id kwargs['force_insert'] = True return super().save(*args, **kwargs) def delete_instance(self, recursive=False, delete_nullable=False): with self._meta.database.atomic(): super().delete_instance(recursive, delete_nullable) self.proxy.delete_instance()
class HouseNumber(Model): identifiers = ['cia', 'laposte', 'ign'] resource_fields = [ 'number', 'ordinal', 'parent', 'cia', 'laposte', 'ancestors', 'positions', 'ign', 'postcode' ] readonly_fields = Model.readonly_fields + ['cia'] number = db.CharField(max_length=16, null=True) ordinal = db.CharField(max_length=16, null=True) parent = db.ForeignKeyField(Group) cia = db.CharField(max_length=100, null=True, unique=True) laposte = db.CharField(max_length=10, null=True, unique=True) ign = db.CharField(max_length=24, null=True, unique=True) ancestors = db.ManyToManyField(Group, related_name='_housenumbers') postcode = db.ForeignKeyField(PostCode, null=True) class Meta: indexes = ((('parent', 'number', 'ordinal'), True), ) def __str__(self): return ' '.join([self.number or '', self.ordinal or '']) def save(self, *args, **kwargs): self.cia = self.compute_cia() super().save(*args, **kwargs) self._clean_called = False def compute_cia(self): return compute_cia(str(self.parent.municipality.insee), self.parent.get_fantoir(), self.number, self.ordinal)
class Flag(db.Model): __openapi__ = """ properties: at: type: string format: date-time description: when the flag has been created by: type: string description: identifier of the client who flagged the version """ version = db.ForeignKeyField(Version, related_name='flags') client = db.ForeignKeyField(Client) session = db.ForeignKeyField(Session) created_at = db.DateTimeField() def save(self, *args, **kwargs): if not self.created_at: self.created_at = utcnow() super().save(*args, **kwargs) def serialize(self, *args): return {'at': self.created_at, 'by': self.client.flag_id}
class Diff(db.Model): __openapi__ = """ properties: resource: type: string description: name of the resource the diff is applied to resource_id: type: string description: id of the resource the diff is applied to created_at: type: string format: date-time description: the date and time the diff has been created at old: type: object description: the resource before the change new: type: object description: the resource after the change diff: type: object description: detail of changed properties """ # Allow to skip diff at very first data import. ACTIVE = True # old is empty at creation. old = db.ForeignKeyField(Version, null=True) # new is empty after delete. new = db.ForeignKeyField(Version, null=True) diff = db.BinaryJSONField() created_at = db.DateTimeField() class Meta: validate_backrefs = False order_by = ('pk', ) def save(self, *args, **kwargs): if not self.diff: old = self.old.data if self.old else {} new = self.new.data if self.new else {} self.diff = make_diff(old, new) super().save(*args, **kwargs) Redirect.from_diff(self) def serialize(self, *args): version = self.new or self.old return { 'increment': self.pk, 'old': self.old.data if self.old else None, 'new': self.new.data if self.new else None, 'diff': self.diff, 'resource': version.model_name.lower(), 'resource_id': version.data['id'], 'created_at': self.created_at }
class Session(db.Model): """Stores the minimum data to trace the changes. We have two scenarios: - one registered user (a developer?) create its own token, and then gets a nominative session - a client sends us IP and/or email from a remote user we don't know of """ user = db.ForeignKeyField(User, null=True) client = db.ForeignKeyField(Client, null=True) ip = db.CharField(null=True) # TODO IPField email = db.CharField(null=True) # TODO EmailField
class Grant(db.Model): user = db.ForeignKeyField(User) client = db.ForeignKeyField(Client) code = db.CharField(max_length=255, index=True, null=False) redirect_uri = db.CharField() scope = db.CharField(null=True) expires = db.DateTimeField() @property def scopes(self): return self.scope.split() if self.scope else None
class Diff(db.Model): # Allow to skip diff at very first data import. ACTIVE = True # old is empty at creation. old = db.ForeignKeyField(Version, null=True) # new is empty after delete. new = db.ForeignKeyField(Version, null=True) diff = db.BinaryJSONField() created_at = db.DateTimeField() class Meta: validate_backrefs = False manager = SelectQuery order_by = ('id', ) def save(self, *args, **kwargs): if not self.diff: meta = set([ 'id', 'created_by', 'modified_by', 'created_at', 'modified_at', 'version' ]) old = self.old.as_resource if self.old else {} new = self.new.as_resource if self.new else {} keys = set(list(old.keys()) + list(new.keys())) - meta self.diff = {} for key in keys: old_value = old.get(key) new_value = new.get(key) if new_value != old_value: self.diff[key] = { 'old': str(old_value), 'new': str(new_value) } super().save(*args, **kwargs) IdentifierRedirect.from_diff(self) @property def as_resource(self): version = self.new or self.old return { 'increment': self.id, 'old': self.old.as_resource if self.old else None, 'new': self.new.as_resource if self.new else None, 'diff': self.diff, 'resource': version.model_name.lower(), 'resource_id': version.model_id, 'created_at': self.created_at }
class Client(ResourceModel): identifiers = ['client_id'] GRANT_AUTHORIZATION_CODE = 'authorization_code' GRANT_IMPLICIT = 'implicit' GRANT_PASSWORD = '******' GRANT_CLIENT_CREDENTIALS = 'client_credentials' GRANT_TYPES = ( # (GRANT_AUTHORIZATION_CODE, _('Authorization code')), # (GRANT_IMPLICIT, _('Implicit')), (GRANT_PASSWORD, _('Resource owner password-based')), (GRANT_CLIENT_CREDENTIALS, _('Client credentials')), ) default_scopes = ['contrib'] client_id = db.UUIDField(unique=True, default=uuid.uuid4) name = db.CharField(max_length=100) user = db.ForeignKeyField(User) client_secret = db.CharField(unique=True, max_length=55, index=True) redirect_uris = db.ArrayField(db.CharField) grant_type = db.CharField(choices=GRANT_TYPES) is_confidential = db.BooleanField(default=False) @property def default_redirect_uri(self): return self.redirect_uris[0] if self.redirect_uris else None @property def allowed_grant_types(self): return [id for id, name in self.GRANT_TYPES]
class HouseNumber(Model): # INSEE + set of OCR-friendly characters (dropped confusing ones # (like 0/O, 1/I…)) from La Poste. CEA_FORMAT = Municipality.INSEE_FORMAT + '[234679ABCEGHILMNPRSTUVXYZ]{5}' identifiers = ['cia', 'laposte', 'ign'] resource_fields = [ 'number', 'ordinal', 'parent', 'cia', 'laposte', 'ancestors', 'positions', 'ign', 'postcode' ] readonly_fields = Model.readonly_fields + ['cia'] number = db.CharField(max_length=16, null=True) ordinal = db.CharField(max_length=16, null=True) parent = db.ForeignKeyField(Group) cia = db.CharField(max_length=100, null=True, unique=True) laposte = db.CharField(length=10, null=True, unique=True, format=CEA_FORMAT) ign = db.CharField(max_length=24, null=True, unique=True) ancestors = db.ManyToManyField(Group, related_name='_housenumbers') postcode = db.ForeignKeyField(PostCode, null=True) class Meta: indexes = ((('parent', 'number', 'ordinal'), True), ) def __str__(self): return ' '.join([self.number or '', self.ordinal or '']) def save(self, *args, **kwargs): self.cia = self.compute_cia() super().save(*args, **kwargs) self._clean_called = False def compute_cia(self): return compute_cia(str(self.parent.municipality.insee), self.parent.get_fantoir(), self.number, self.ordinal) @cached_property def municipality(self): return Municipality.select().join( Group, on=Municipality.pk == self.parent.municipality.pk).first()
class Client(ResourceModel): identifiers = ['client_id'] resource_fields = ['name', 'user', 'scopes', 'contributor_types'] GRANT_CLIENT_CREDENTIALS = 'client_credentials' GRANT_TYPES = ((GRANT_CLIENT_CREDENTIALS, _('Client credentials')), ) TYPE_IGN = 'ign' TYPE_LAPOSTE = 'laposte' TYPE_DGFIP = 'dgfip' TYPE_ETALAB = 'etalab' TYPE_OSM = 'osm' TYPE_SDIS = 'sdis' TYPE_MUNICIPAL = 'municipal_administration' TYPE_ADMIN = 'admin' TYPE_DEV = 'develop' TYPE_INSEE = 'insee' TYPE_VIEWER = 'viewer' CONTRIBUTOR_TYPE = (TYPE_SDIS, TYPE_OSM, TYPE_LAPOSTE, TYPE_IGN, TYPE_DGFIP, TYPE_ETALAB, TYPE_MUNICIPAL, TYPE_ADMIN, TYPE_INSEE, TYPE_DEV, TYPE_VIEWER) client_id = db.UUIDField(unique=True, default=uuid.uuid4) name = db.CharField(max_length=100) user = db.ForeignKeyField(User) client_secret = db.CharField(unique=True, max_length=55) redirect_uris = db.ArrayField(db.CharField) grant_type = db.CharField(choices=GRANT_TYPES) is_confidential = db.BooleanField(default=False) contributor_types = db.ArrayField(db.CharField, default=[TYPE_VIEWER], null=True) scopes = db.ArrayField(db.CharField, default=[], null=True) @property def default_redirect_uri(self): return self.redirect_uris[0] if self.redirect_uris else None @property def allowed_grant_types(self): return [id for id, name in self.GRANT_TYPES] #Necessaire pour OAuth @property def default_scopes(self): return self.scopes def save(self, *args, **kwargs): if not self.client_secret: self.client_secret = generate_secret() self.redirect_uris = ['http://localhost/authorize'] # FIXME self.grant_type = self.GRANT_CLIENT_CREDENTIALS if not self.contributor_types: self.contributor_types = ['viewer'] super().save(*args, **kwargs)
class Position(Model): POSTAL = 'postal' ENTRANCE = 'entrance' BUILDING = 'building' STAIRCASE = 'staircase' UNIT = 'unit' PARCEL = 'parcel' SEGMENT = 'segment' UTILITY = 'utility' KIND = ( (POSTAL, _('postal delivery')), (ENTRANCE, _('entrance')), (BUILDING, _('building')), (STAIRCASE, _('staircase identifier')), (UNIT, _('unit identifier')), (PARCEL, _('parcel')), (SEGMENT, _('road segment')), (UTILITY, _('utility service')), ) resource_fields = ['center', 'source', 'housenumber', 'attributes', 'kind', 'comment', 'parent'] center = db.PointField(verbose_name=_("center")) housenumber = db.ForeignKeyField(HouseNumber) parent = db.ForeignKeyField('self', related_name='children', null=True) source = db.CharField(max_length=64, null=True) kind = db.CharField(max_length=64, choices=KIND) attributes = db.HStoreField(null=True) comment = peewee.TextField(null=True) class Meta: unique_together = ('housenumber', 'source') @property def center_resource(self): if not isinstance(self.center, Point): self.center = Point(*self.center) return self.center.geojson
class Session(db.Model): """Stores the minimum data to trace the changes. We have two scenarios: - one registered user (a developer?) create its own token, and then gets a nominative session - a client sends us IP and/or email from a remote user we don't know of """ __openapi__ = """ properties: id: type: integer description: primary key of the session client: type: string description: client name user: type: string description: user name """ user = db.ForeignKeyField(User, null=True) client = db.ForeignKeyField(Client, null=True) ip = db.CharField(null=True) # TODO IPField email = db.CharField(null=True) # TODO EmailField def serialize(self, *args): # Pretend to be a resource for created_by/modified_by values in # resources serialization. # Should we also expose the email/ip? CNIL question to be solved. return { 'id': self.pk, 'client': self.client.name if self.client else None, 'user': self.user.username if self.user else None } def save(self, **kwargs): if not self.user and not self.client: raise ValueError('Session must have either a client or a user') super().save(**kwargs)
class HouseNumber(Model): # INSEE + set of OCR-friendly characters (dropped confusing ones # (like 0/O, 1/I…)) from La Poste. CEA_FORMAT = Municipality.INSEE_FORMAT + '[234679ABCEGHILMNPRSTUVXYZ]{5}' identifiers = ['cia', 'laposte', 'ign'] resource_fields = [ 'number', 'ordinal', 'parent', 'cia', 'laposte', 'ancestors', 'positions', 'ign', 'postcode' ] exclude_for_collection = ['ancestors'] number = db.CharField(max_length=16, null=True) ordinal = db.CharField(max_length=16, null=True) parent = db.ForeignKeyField(Group) cia = db.CharField(max_length=100, null=True, unique=True) laposte = db.CharField(length=10, null=True, unique=True, format=CEA_FORMAT) ign = db.CharField(max_length=24, null=True, unique=True) ancestors = db.ManyToManyField(Group, related_name='_housenumbers') postcode = db.CachedForeignKeyField(PostCode, null=True) class Meta: indexes = ((('parent', 'number', 'ordinal'), True), ) case_ignoring = ('ordinal', ) def __str__(self): return ' '.join([self.number or '', self.ordinal or '']) def save(self, *args, **kwargs): self.cia = self.compute_cia() super().save(*args, **kwargs) self._clean_called = False def compute_cia(self): return compute_cia(self.parent.fantoir[:5], self.parent.fantoir[5:], self.number, self.ordinal) if self.parent.fantoir else None @cached_property def municipality(self): return Municipality.select().where( Municipality.pk == self.parent.municipality.pk).first() @property def as_export(self): """Resources plus relation references without metadata.""" mask = {f: {} for f in self.resource_fields} return self.serialize(mask)
class PostCode(NamedModel): resource_fields = ['code', 'name', 'alias', 'municipality'] code = db.PostCodeField(index=True) municipality = db.ForeignKeyField(Municipality, related_name='postcodes') class Meta: indexes = ((('code', 'municipality'), True), ) @property def housenumbers(self): return self.housenumber_set.order_by( peewee.SQL('number ASC NULLS FIRST'), peewee.SQL('ordinal ASC NULLS FIRST'))
class PostCode(NamedModel): resource_fields = ['code', 'name', 'alias', 'complement', 'municipality'] complement = db.CharField(max_length=38, null=True) code = db.CharField(index=True, format='\d*', length=5) municipality = db.ForeignKeyField(Municipality, related_name='postcodes') class Meta: indexes = ((('code', 'complement', 'municipality'), True), ) @property def housenumbers(self): return self.housenumber_set.order_by( peewee.SQL('number ASC NULLS FIRST'), peewee.SQL('ordinal ASC NULLS FIRST'))
class Group(NamedModel): AREA = 'area' WAY = 'way' KIND = ( (WAY, 'way'), (AREA, 'area'), ) CLASSICAL = 'classical' METRIC = 'metric' LINEAR = 'linear' MIXED = 'mixed' ANARCHICAL = 'anarchical' ADDRESSING = ( (CLASSICAL, 'classical'), (METRIC, 'metric'), (LINEAR, 'linear'), (MIXED, 'mixed types'), (ANARCHICAL, 'anarchical'), ) identifiers = ['fantoir', 'laposte', 'ign'] resource_fields = [ 'name', 'alias', 'fantoir', 'municipality', 'kind', 'laposte', 'ign', 'addressing' ] kind = db.CharField(max_length=64, choices=KIND) addressing = db.CharField(max_length=16, choices=ADDRESSING, null=True) fantoir = db.FantoirField(null=True, unique=True) laposte = db.CharField(max_length=10, null=True, unique=True) ign = db.CharField(max_length=24, null=True, unique=True) municipality = db.ForeignKeyField(Municipality, related_name='groups') @property def tmp_fantoir(self): return '#' + re.sub(r'[\W]', '', unidecode(self.name)).upper() def get_fantoir(self): # Without INSEE code. return self.fantoir[5:] if self.fantoir else self.tmp_fantoir @property def housenumbers(self): qs = (self._housenumbers | self.housenumber_set) return qs.order_by(peewee.SQL('number ASC NULLS FIRST'), peewee.SQL('ordinal ASC NULLS FIRST'))
class Client(ResourceModel): identifiers = ['client_id'] resource_fields = ['name', 'user'] GRANT_AUTHORIZATION_CODE = 'authorization_code' GRANT_IMPLICIT = 'implicit' GRANT_PASSWORD = '******' GRANT_CLIENT_CREDENTIALS = 'client_credentials' GRANT_TYPES = ( # (GRANT_AUTHORIZATION_CODE, _('Authorization code')), # (GRANT_IMPLICIT, _('Implicit')), (GRANT_PASSWORD, _('Resource owner password-based')), (GRANT_CLIENT_CREDENTIALS, _('Client credentials')), ) default_scopes = ['contrib'] FLAGS = ['ign', 'laposte', 'local_authority'] FLAG_IDS = tuple((i, i) for i in FLAGS) + (None, 'None') client_id = db.UUIDField(unique=True, default=uuid.uuid4) name = db.CharField(max_length=100) user = db.ForeignKeyField(User) client_secret = db.CharField(unique=True, max_length=55) redirect_uris = db.ArrayField(db.CharField) grant_type = db.CharField(choices=GRANT_TYPES) is_confidential = db.BooleanField(default=False) flag_id = db.CharField(choices=FLAG_IDS, default=None, null=True) @property def default_redirect_uri(self): return self.redirect_uris[0] if self.redirect_uris else None @property def allowed_grant_types(self): return [id for id, name in self.GRANT_TYPES] def save(self, *args, **kwargs): if not self.client_secret: self.client_secret = generate_secret() self.redirect_uris = ['http://localhost/authorize'] # FIXME self.grant_type = self.GRANT_CLIENT_CREDENTIALS super().save(*args, **kwargs)
class Token(db.Model): session = db.ForeignKeyField(Session) token_type = db.CharField(max_length=40) access_token = db.CharField(max_length=255) refresh_token = db.CharField(max_length=255, null=True) scope = db.CharField(max_length=255) expires = db.DateTimeField() def __init__(self, **kwargs): expires_in = kwargs.pop('expires_in', 60 * 60) kwargs['expires'] = datetime.now() + timedelta(seconds=expires_in) super().__init__(**kwargs) @property def scopes(self): # TODO: custom charfield return self.scope.split() if self.scope else None def is_valid(self, scopes=None): """ Checks if the access token is valid. :param scopes: An iterable containing the scopes to check or None """ return not self.is_expired() and self.allow_scopes(scopes) def is_expired(self): """ Check token expiration with timezone awareness """ return datetime.now() >= self.expires def allow_scopes(self, scopes): """ Check if the token allows the provided scopes :param scopes: An iterable containing the scopes to check """ if not scopes: return True provided_scopes = set(self.scope.split()) resource_scopes = set(scopes) return resource_scopes.issubset(provided_scopes) @property def user(self): return self.session.user @classmethod def create_with_session(cls, **data): if not data.get('ip') and not data.get('email'): return None if not data.get('client_id'): return None session_data = { "email": data.get('email'), "ip": data.get('ip'), "client": Client.first(Client.client_id == data['client_id']) } session = Session.create(**session_data) # get or create? data['session'] = session.id return Token.create(**data)
class Token(db.Model): session = db.ForeignKeyField(Session) token_type = db.CharField(max_length=40) access_token = db.CharField(max_length=255) refresh_token = db.CharField(max_length=255, null=True) scopes = db.ArrayField(db.CharField, default=[], null=True) expires = db.DateTimeField() contributor_type = db.CharField(choices=Client.CONTRIBUTOR_TYPE, null=True) def __init__(self, **kwargs): expires_in = kwargs.pop('expires_in', 60 * 60) kwargs['expires'] = utcnow() + timedelta(seconds=expires_in) super().__init__(**kwargs) def is_valid(self, scopes=None): """ Checks if the access token is valid. :param scopes: An iterable containing the scopes to check or None """ return not self.is_expired() and self.allow_scopes(scopes) def is_expired(self): """ Check token expiration with timezone awareness """ return utcnow() >= self.expires def allow_scopes(self, scopes): """ Check if the token allows the provided scopes :param scopes: An iterable containing the scopes to check """ if not scopes: return True provided_scopes = set(self.scope.split()) resource_scopes = set(scopes) return resource_scopes.issubset(provided_scopes) @property def user(self): return self.session.user @classmethod def create_with_session(cls, **data): if not data.get('ip') and not data.get('email'): return None, None if not data.get('client_id'): return None, 'Client id missing' client = Client.first(Client.client_id == data['client_id']) if len(client.contributor_types) == 0: return None, 'Client has none contributor types' contributor_type = client.contributor_types[0] if data.get('contributor_type'): if data.get('contributor_type') not in client.contributor_types: return None, 'wrong contributor type : must be in the list {}'.format( client.contributor_types) if len(client.contributor_types) > 1: if not data.get('contributor_type'): return None, 'Contributor type missing' contributor_type = data.get('contributor_type') session_data = { "email": data.get('email'), "ip": data.get('ip'), "contributor_type": contributor_type, "client": client } session = Session.create(**session_data) # get or create? data['session'] = session.pk data['scopes'] = client.scopes data['contributor_type'] = session.contributor_type if session.contributor_type == Client.TYPE_VIEWER: data['scopes'] = None return Token.create(**data), None
class Versioned(db.Model, metaclass=BaseVersioned): ForcedVersionError = ForcedVersionError version = db.IntegerField(default=1) created_at = db.DateTimeField() created_by = db.ForeignKeyField(Session) modified_at = db.DateTimeField() modified_by = db.ForeignKeyField(Session) class Meta: validate_backrefs = False unique_together = ('pk', 'version') def prepared(self): self.lock_version() super().prepared() def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.prepared() def store_version(self): new = Version.create(model_name=self.resource, model_pk=self.pk, sequential=self.version, data=self.as_version, period=[self.modified_at, None]) old = None if self.version > 1: old = self.load_version(self.version - 1) old.close_period(new.period.lower) if Diff.ACTIVE: Diff.create(old=old, new=new, created_at=self.modified_at, insee=self.municipality.insee) @property def versions(self): return Version.select().where(Version.model_name == self.resource, Version.model_pk == self.pk).order_by( Version.sequential) def load_version(self, ref=None): qs = self.versions if ref is None: ref = self.version if isinstance(ref, datetime): qs = qs.where(Version.period.contains(ref)) else: qs = qs.where(Version.sequential == ref) return qs.first() @property def locked_version(self): return getattr(self, '_locked_version', None) @locked_version.setter def locked_version(self, value): # Should be set only once, and never updated. assert not hasattr( self, '_locked_version'), 'locked_version is read only' # noqa self._locked_version = value def lock_version(self): if not self.pk: self.version = 1 self._locked_version = self.version if self.pk else 0 def increment_version(self): self.version = self.version + 1 def check_version(self): if self.version != self.locked_version + 1: raise ForcedVersionError('wrong version number: {}'.format( self.version)) # noqa def update_meta(self): session = context.get('session') if session: # TODO remove this if, session should be mandatory. try: getattr(self, 'created_by', None) except Session.DoesNotExist: # Field is not nullable, we can't access it when it's not yet # defined. self.created_by = session self.modified_by = session now = utcnow() if not self.created_at: self.created_at = now self.modified_at = now def save(self, *args, **kwargs): with self._meta.database.atomic(): self.check_version() self.update_meta() super().save(*args, **kwargs) self.store_version() self.lock_version() def delete_instance(self, *args, **kwargs): with self._meta.database.atomic(): Redirect.clear(self) return super().delete_instance(*args, **kwargs)
class Versioned(db.Model, metaclass=BaseVersioned): ForcedVersionError = ForcedVersionError version = db.IntegerField(default=1) created_at = db.DateTimeField() created_by = db.ForeignKeyField(Session) modified_at = db.DateTimeField() modified_by = db.ForeignKeyField(Session) class Meta: abstract = True validate_backrefs = False unique_together = ('id', 'version') @classmethod def get(cls, *query, **kwargs): instance = super().get(*query, **kwargs) instance.lock_version() return instance def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.lock_version() def _serialize(self, fields): data = {} for name, field in fields.items(): value = getattr(self, field.name) value = field.db_value(value) data[field.name] = value return data def serialize(self, fields=None): return dumps(self._serialize(self._meta.fields)) def store_version(self): old = None if self.version > 1: old = self.load_version(self.version - 1) new = Version.create(model_name=self.__class__.__name__, model_id=self.id, sequential=self.version, data=self.serialize()) if Diff.ACTIVE: Diff.create(old=old, new=new, created_at=self.modified_at) @property def versions(self): return Version.select().where( Version.model_name == self.__class__.__name__, Version.model_id == self.id) def load_version(self, id): return self.versions.where(Version.sequential == id).first() @property def locked_version(self): return getattr(self, '_locked_version', None) @locked_version.setter def locked_version(self, value): # Should be set only once, and never updated. assert not hasattr( self, '_locked_version'), 'locked_version is read only' # noqa self._locked_version = value def lock_version(self): if not self.id: self.version = 1 self._locked_version = self.version if self.id else 0 def increment_version(self): self.version = self.version + 1 def check_version(self): if self.version != self.locked_version + 1: raise ForcedVersionError('wrong version number: {}'.format( self.version)) # noqa def update_meta(self): session = context.get('session') if session: try: getattr(self, 'created_by', None) except Session.DoesNotExist: # Field is not nullable, we can't access it when it's not yet # defined. self.created_by = session self.modified_by = session now = datetime.now() if not self.created_at: self.created_at = now self.modified_at = now def save(self, *args, **kwargs): self.check_version() self.update_meta() super().save(*args, **kwargs) self.store_version() self.lock_version()
class Position(Model): POSTAL = 'postal' ENTRANCE = 'entrance' BUILDING = 'building' STAIRCASE = 'staircase' UNIT = 'unit' PARCEL = 'parcel' SEGMENT = 'segment' UTILITY = 'utility' UNKNOWN = 'unknown' AREA = 'area' KIND = ( (POSTAL, _('postal delivery')), (ENTRANCE, _('entrance')), (BUILDING, _('building')), (STAIRCASE, _('staircase identifier')), (UNIT, _('unit identifier')), (PARCEL, _('parcel')), (SEGMENT, _('road segment')), (UTILITY, _('utility service')), (AREA, _('area')), (UNKNOWN, _('unknown')), ) DGPS = 'dgps' GPS = 'gps' IMAGERY = 'imagery' PROJECTION = 'projection' INTERPOLATION = 'interpolation' OTHER = 'other' POSITIONING = ( (DGPS, _('via differencial GPS')), (GPS, _('via GPS')), (IMAGERY, _('via imagery')), (PROJECTION, _('computed via projection')), (INTERPOLATION, _('computed via interpolation')), (OTHER, _('other')), ) identifiers = ['laposte', 'ign'] resource_fields = [ 'center', 'source', 'housenumber', 'kind', 'comment', 'parent', 'positioning', 'name', 'ign', 'laposte' ] name = db.CharField(max_length=200, null=True) center = db.PointField(verbose_name=_("center"), null=True, index=True) housenumber = db.ForeignKeyField(HouseNumber, related_name='positions') parent = db.ForeignKeyField('self', related_name='children', null=True) source = db.CharField(max_length=64, null=True) kind = db.CharField(max_length=64, choices=KIND) positioning = db.CharField(max_length=32, choices=POSITIONING) ign = db.CharField(max_length=24, null=True, unique=True) laposte = db.CharField(max_length=10, null=True, unique=True) comment = db.TextField(null=True) class Meta: indexes = ((('housenumber', 'source'), True), ) @classmethod def validate(cls, validator, document, instance): errors = {} default = instance and validator.update and instance.name name = document.get('name', default) default = instance and validator.update and instance.center center = document.get('center', default) if not name and not center: msg = 'A position must have either a center or a name.' errors['center'] = msg errors['name'] = msg return errors