예제 #1
0
class Municipality(NamedModel):
    identifiers = ['siren', 'insee']
    resource_fields = ['name', 'alias', 'insee', 'siren', 'postcodes']
    exclude_for_version = ['postcodes']

    insee = db.CharField(length=5, unique=True)
    siren = db.CharField(max_length=9, unique=True, null=True)
예제 #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
파일: models.py 프로젝트: ludovicf01/ban
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]
예제 #4
0
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)
예제 #5
0
파일: models.py 프로젝트: ludovicf01/ban
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
예제 #6
0
class Municipality(NamedModel):
    identifiers = ['siren', 'insee']
    resource_fields = ['name', 'alias', 'insee', 'siren', 'postcodes']

    insee = db.CharField(max_length=5, unique=True)
    siren = db.CharField(max_length=9, unique=True)

    @property
    def postcodes_resource(self):
        return [p.code for p in self.postcodes]
예제 #7
0
파일: models.py 프로젝트: ludovicf01/ban
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
예제 #8
0
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)
예제 #9
0
파일: models.py 프로젝트: pjegouic/ban
class Municipality(NamedModel):
    INSEE_FORMAT = '(2[AB]|\d{2})\d{3}'
    identifiers = ['siren', 'insee']
    resource_fields = ['name', 'alias', 'insee', 'siren', 'postcodes']
    exclude_for_version = ['postcodes']

    insee = db.CharField(length=5, unique=True, format=INSEE_FORMAT)
    siren = db.CharField(length=9, format='\d*', unique=True, null=True)

    @property
    def municipality(self):
        return self
예제 #10
0
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)
예제 #11
0
파일: models.py 프로젝트: pjegouic/ban
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'))
예제 #12
0
class Version(db.Model):
    model_name = db.CharField(max_length=64)
    model_id = db.IntegerField()
    sequential = db.IntegerField()
    data = db.BinaryJSONField()

    class Meta:
        manager = SelectQuery

    def __repr__(self):
        return '<Version {} of {}({})>'.format(self.sequential,
                                               self.model_name, self.model_id)

    @property
    def as_resource(self):
        return json.loads(self.data)

    @property
    def model(self):
        return BaseVersioned.registry[self.model_name]

    def load(self):
        return self.model(**self.as_resource)

    @property
    def diff(self):
        return Diff.first(Diff.new == self.id)
예제 #13
0
class User(ResourceModel):
    identifiers = ['email']
    resource_fields = ['username', 'email', 'company']

    username = db.CharField(max_length=100, index=True)
    email = db.CharField(max_length=100, unique=True)
    company = db.CharField(max_length=100, null=True)
    is_staff = db.BooleanField(default=False, index=True)

    auth = 1

    class Meta:
        database = db.database

    def __str__(self):
        return self.username
예제 #14
0
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)
예제 #15
0
class Version(db.Model):

    __openapi__ = """
        properties:
            data:
                type: object
                description: serialized resource
            flag:
                type: array
                items:
                    $ref: '#/definitions/Flag'
        """

    model_name = db.CharField(max_length=64)
    model_pk = db.IntegerField()
    sequential = db.IntegerField()
    data = db.BinaryJSONField()
    period = db.DateRangeField()

    class Meta:
        indexes = ((('model_name', 'model_pk', 'sequential'), True), )

    def __repr__(self):
        return '<Version {} of {}({})>'.format(self.sequential,
                                               self.model_name, self.model_pk)

    def serialize(self, *args):
        return {'data': self.data, 'flags': list(self.flags.serialize())}

    @property
    def model(self):
        return BaseVersioned.registry[self.model_name]

    def load(self):
        validator = self.model.validator(**self.data)
        return self.model(**validator.data)

    @property
    def diff(self):
        return Diff.first(Diff.new == self.pk)

    @flag_id_required
    def flag(self, session=None):
        """Flag current version with current client."""
        if not Flag.where(Flag.version == self, Flag.client
                          == session.client).exists():
            Flag.create(version=self, session=session, client=session.client)

    @flag_id_required
    def unflag(self, session=None):
        """Delete current version's flags made by current session client."""
        Flag.delete().where(Flag.version == self,
                            Flag.client == session.client).execute()

    def close_period(self, bound):
        # DateTimeRange is immutable, so create new one.
        self.period = [self.period.lower, bound]
        self.save()
예제 #16
0
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'))
예제 #17
0
class NamedModel(Model):
    name = db.CharField(max_length=200)
    alias = db.ArrayField(db.CharField, null=True)

    def __str__(self):
        return self.name

    class Meta:
        abstract = True
        ordering = ('name', )
예제 #18
0
파일: models.py 프로젝트: pjegouic/ban
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()
예제 #19
0
class Proxy(db.Model):
    kind = db.CharField(max_length=50, null=False)

    @property
    def real(self):
        # Make dynamic.
        mapping = {class_.__name__.lower(): class_
                   for class_ in [Street, Locality, PostCode, District]}
        class_ = mapping[self.kind]
        return class_.get(class_.proxy == self)
예제 #20
0
파일: models.py 프로젝트: gschittek/ban
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)
예제 #21
0
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
예제 #22
0
class BaseFantoirModel(ProxyableModel, NamedModel):
    identifiers = ['fantoir']
    resource_fields = ['name', 'alias', 'fantoir', 'municipality']

    fantoir = db.CharField(max_length=9, null=True, index=True)

    class Meta:
        abstract = True

    @property
    def tmp_fantoir(self):
        return '#' + re.sub(r'[\W]', '', unidecode(self.name)).upper()

    def get_fantoir(self):
        return self.fantoir or self.tmp_fantoir
예제 #23
0
파일: models.py 프로젝트: ludovicf01/ban
class User(ResourceModel):
    identifiers = ['email']
    resource_fields = ['username', 'email', 'company']

    username = db.CharField(max_length=100)
    email = db.CharField(max_length=100, unique=True)
    company = db.CharField(max_length=100, null=True)
    # Allow null, because password is not a resource field, and thus cannot be
    # passed to validators.
    password = db.PasswordField(null=True)
    is_staff = db.BooleanField(default=False)

    class Meta:
        database = db.default

    def __str__(self):
        return self.username

    def set_password(self, password):
        self.password = password
        self.save()

    def check_password(self, password):
        return self.password.check_password(password)
예제 #24
0
파일: models.py 프로젝트: ludovicf01/ban
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)
예제 #25
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
예제 #26
0
class Diff(db.Model):

    __openapi__ = """
        properties:
            increment:
                type: integer
                description: incremental id of the diff
            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
            insee:
                type: string
                description: INSEE code of the Municipality the resource
                             is attached
            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)
    insee = db.CharField(length=5)
    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,
            'insee': self.insee,
            '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
        }
예제 #27
0
class Redirect(db.Model):

    __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 ':'
        """

    model_name = db.CharField(max_length=64)
    model_id = db.CharField(max_length=255)
    identifier = db.CharField(max_length=64)
    value = db.CharField(max_length=255)

    class Meta:
        primary_key = peewee.CompositeKey('model_name', 'identifier', 'value',
                                          'model_id')

    @classmethod
    def add(cls, instance, identifier, value):
        if isinstance(instance, tuple):
            # Optim so we don't need to request db when creating a redirect
            # from a diff.
            model_name, model_id = instance
        else:
            model_name = instance.resource
            model_id = instance.id
            if identifier not in instance.__class__.identifiers + ['id', 'pk']:
                raise ValueError('Invalid identifier: {}'.format(identifier))
            if getattr(instance, identifier) == value:
                raise ValueError('Redirect cannot point to itself')
        cls.get_or_create(model_name=model_name,
                          identifier=identifier,
                          value=str(value),
                          model_id=model_id)
        cls.propagate(model_name, identifier, value, model_id)

    @classmethod
    def remove(cls, instance, identifier, value):
        cls.delete().where(cls.model_name == instance.resource,
                           cls.identifier == identifier,
                           cls.value == str(value),
                           cls.model_id == instance.id).execute()

    @classmethod
    def clear(cls, instance):
        cls.delete().where(cls.model_name == instance.resource,
                           cls.model_id == instance.id).execute()

    @classmethod
    def from_diff(cls, diff):
        if not diff.new or not diff.old:
            # Only update makes sense for us, not creation nor deletion.
            return
        model = diff.new.model
        identifiers = [i for i in model.identifiers if i in diff.diff]
        for identifier in identifiers:
            old = diff.diff[identifier]['old']
            new = diff.diff[identifier]['new']
            if not old or not new:
                continue
            cls.add((model.__name__.lower(), diff.new.data['id']), identifier,
                    old)

    @classmethod
    def follow(cls, model_name, identifier, value):
        rows = cls.select(cls.model_id).where(
            cls.model_name == model_name.lower(), cls.identifier == identifier,
            cls.value == str(value))
        return [row.model_id for row in rows]

    @classmethod
    def propagate(cls, model_name, identifier, value, model_id):
        """An identifier was a target and it becomes itself a redirect."""
        model = BaseVersioned.registry.get(model_name)
        if model:
            old = model.first(getattr(model, identifier) == value)
            if old:
                cls.update(model_id=model_id).where(
                    cls.model_id == old.id,
                    cls.model_name == model_name).execute()

    def serialize(self, *args):
        return '{}:{}'.format(self.identifier, self.value)
예제 #28
0
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
예제 #29
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
예제 #30
0
class NamedModel(Model):
    name = db.CharField(max_length=200)
    alias = db.ArrayField(db.CharField, null=True)

    def __str__(self):
        return self.name