Пример #1
0
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}
Пример #2
0
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()
Пример #3
0
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
        }
Пример #4
0
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
Пример #5
0
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
        }
Пример #6
0
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)
Пример #7
0
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()
Пример #8
0
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
Пример #9
0
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)
Пример #10
0
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