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.CachedForeignKeyField(User, null=True) client = db.CachedForeignKeyField(Client, null=True) ip = db.CharField(null=True) # TODO IPField email = db.CharField(null=True) # TODO EmailField contributor_type = db.CharField(null=True) 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, 'contributor_type': self.contributor_type if self.contributor_type 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') if not self.contributor_type: raise ValueError('Session must have a contributor type') 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', 'complement', 'municipality'] complement = db.CharField(max_length=38, null=True) code = db.CharField(index=True, format='\d*', length=5) municipality = db.CachedForeignKeyField(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=8, null=True, unique=True, format='\d*') ign = db.CharField(max_length=24, null=True, unique=True) municipality = db.CachedForeignKeyField(Municipality, related_name='groups') @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 Versioned(db.Model, metaclass=BaseVersioned): ForcedVersionError = ForcedVersionError version = db.IntegerField(default=1) created_at = db.DateTimeField() created_by = db.CachedForeignKeyField(Session) modified_at = db.DateTimeField() modified_by = db.CachedForeignKeyField(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: if not self.created_by: 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() try: self.source_kind = self.created_by.contributor_type except Exception: pass 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)