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 Anomaly(resource.ResourceModel): __openapi__ = """ properties: identifier: type: string description: key/value pair for identifier. . key = identifier name. e.g., 'id'. . value = identifier value. . key and value are separated by a ':' """ resource_fields = ['versions', 'kind', 'insee', 'created_at'] readonly_fields = (resource.ResourceModel.readonly_fields + ['created_at']) versions = db.ManyToManyField(Version, related_name='_anomalies') kind = db.CharField() insee = db.CharField(length=5) created_at = db.DateTimeField() legitimate = db.BooleanField(default=False) def save(self, *args, **kwargs): if not self.created_at: self.created_at = utcnow() return super().save(*args, **kwargs) def mark_deleted(self): self.delete_instance()
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 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 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 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 ResourceModel(db.Model, metaclass=BaseResource): resource_fields = ['id', 'status'] identifiers = [] readonly_fields = ['id', 'pk', 'status', 'deleted_at'] exclude_for_collection = ['status'] exclude_for_version = [] id = db.CharField(max_length=50, unique=True, null=False) deleted_at = db.DateTimeField(null=True, index=True) class Meta: validator = ResourceValidator @classmethod def make_id(cls): return 'ban-{}-{}'.format(cls.__name__.lower(), uuid.uuid4().hex) def save(self, *args, **kwargs): if not self.id: self.id = self.make_id() return super().save(*args, **kwargs) @classmethod def validator(cls, instance=None, update=False, **data): validator = cls._meta.validator(cls, update=update) validator.validate(data, instance=instance) return validator @property def resource(self): return self.__class__.__name__.lower() @property def serialized(self): return self.id def serialize(self, mask=None): if not mask: return self.serialized dest = {} for name, subfields in mask.items(): if name == '*': return self.serialize( {k: subfields for k in self.resource_fields}) field = getattr(self.__class__, name, None) if not field: raise ValueError('Unknown field {}'.format(name)) value = getattr(self, name) if value is not None: if isinstance( field, (db.ManyToManyField, peewee.ReverseRelationDescriptor)): value = [v.serialize(subfields) for v in value] elif isinstance(field, db.ForeignKeyField): value = value.serialize(subfields) elif isinstance(value, datetime): value = value.isoformat() elif isinstance(value, Point): value = value.geojson dest[name] = value return dest @property def as_resource(self): """Resource plus relations.""" # All fields and all first level relations fields. return self.serialize({'*': {}}) @property def as_version(self): """Resources plus relations references and metadata.""" return self.serialize({f: {} for f in self.versioned_fields}) @property def as_export(self): """Flat resources plus references. May be filtered or overrided.""" return self.serialize({'*': {}}) @property def status(self): return 'deleted' if self.deleted_at else 'active' @classmethod def select(cls, *selection): return super().select(*selection) @classmethod def raw_select(cls, *selection): return super().select(*selection) def mark_deleted(self): if self.deleted_at: raise ValueError('Resource already marked as deleted') self.ensure_no_reverse_relation() self.deleted_at = utcnow() self.increment_version() self.save() def ensure_no_reverse_relation(self): for name, field in self._meta.reverse_rel.items(): select = getattr(self, name) if getattr(select.model_class, 'deleted_at', None): select = select.where(select.model_class.deleted_at.is_null()) if select.count(): raise ResourceLinkedError( 'Resource still linked by `{}`'.format(name)) @classmethod def coerce(cls, id, identifier=None, level1=0): if isinstance(id, db.Model): instance = id else: if not identifier: identifier = 'id' # BAN id by default. if isinstance(id, str): *extra, id = id.split(':') if extra: identifier = extra[0] if identifier not in cls.identifiers + ['id', 'pk']: raise cls.DoesNotExist( "Invalid identifier {}".format(identifier)) elif isinstance(id, int): identifier = 'pk' try: if not hasattr(cls, 'auth') and level1 != 1: instance = cls.raw_select(cls._meta.model_class.pk).where( getattr(cls, identifier) == id).get() else: instance = cls.raw_select().where( getattr(cls, identifier) == id).get() except cls.DoesNotExist: # Is it an old identifier? from .versioning import Redirect redirects = Redirect.follow(cls.__name__, identifier, id) if redirects: if len(redirects) > 1: raise MultipleRedirectsError(identifier, id, redirects) raise RedirectError(identifier, id, redirects[0]) raise return instance
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 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