def test_deserialize_empty(self): from c2cgeoform.ext.deform_ext import RelationSelect2Widget widget = RelationSelect2Widget(Tag, 'id', 'name', multiple=True) renderer = DummyRenderer() field = _get_field('tags', renderer) widget.populate(DBSession, None) result = widget.deserialize(field, null) self.assertEqual(result, [])
def test_serialize_empty(self): from c2cgeoform.ext.deform_ext import RelationSelect2Widget widget = RelationSelect2Widget(Tag, 'id', 'name', multiple=True) renderer = DummyRenderer() field = _get_field('tags', renderer) widget.populate(DBSession, None) widget.serialize(field, null) self.assertEqual(renderer.kw['values'], _convert_values(widget.values)) self.assertEqual(renderer.kw['cstruct'], []) first_value = renderer.kw['values'][0] self.assertEqual('0', first_value[0]) self.assertEqual('Tag A', first_value[1])
def test_serialize_wrong_mapping(self): from c2cgeoform.ext.deform_ext import RelationSelect2Widget widget = RelationSelect2Widget(EmploymentStatus, 'id', 'name', multiple=True) renderer = DummyRenderer() field = _get_field('tags', renderer) widget.populate(DBSession, None) objs = [{'bad_column': '101'}, {'bad_column': '102'}] with self.assertRaises(KeyError): widget.serialize(field, objs)
def test_serialize_wrong_mapping(self): from c2cgeoform.ext.deform_ext import RelationSelect2Widget widget = RelationSelect2Widget( EmploymentStatus, 'id', 'name', multiple=True) renderer = DummyRenderer() field = _get_field('tags', renderer) widget.populate(DBSession, None) objs = [ {'tag_id': '0', 'personId': '1', 'id': '101'}, {'tag_id': '2', 'personId': '1', 'id': '102'}] self.assertRaises( RuntimeError, widget.serialize, field, objs)
def test_serialize(self): from c2cgeoform.ext.deform_ext import RelationSelect2Widget widget = RelationSelect2Widget(Tag, 'id', 'name', multiple=True) renderer = DummyRenderer() field = _get_field('tags', renderer) widget.populate(DBSession, None) objs = [ {'tag_id': '0', 'personId': '1', 'id': '101'}, {'tag_id': '2', 'personId': '1', 'id': '102'}] widget.serialize(field, objs) self.assertEqual(renderer.kw['values'], _convert_values(widget.values)) self.assertEqual(renderer.kw['cstruct'], ['0', '2']) first_value = renderer.kw['values'][0] self.assertEqual('0', first_value[0]) self.assertEqual('Tag A', first_value[1])
def test_serialize_default_value(self): from c2cgeoform.ext.deform_ext import RelationSelect2Widget widget = RelationSelect2Widget( EmploymentStatus, 'id', 'name', ('', '- Select -')) renderer = DummyRenderer() field = DummyField(None, renderer=renderer) widget.populate(DBSession, None) widget.serialize(field, null) self.assertEqual(renderer.kw['values'], _convert_values(widget.values)) # first the default value first_value = renderer.kw['values'][0] self.assertEqual('', first_value[0]) self.assertEqual('- Select -', first_value[1]) # then the values loaded from the db snd_value = renderer.kw['values'][1] self.assertEqual('0', snd_value[0]) self.assertEqual('Worker', snd_value[1])
class Demande(Base): __tablename__ = 'demande' __table_args__ = ({"schema": schema}) __colanderalchemy_config__ = { 'title': _('Demande d\'abattage d\'arbres'), 'plural': _('Demande d\'abattage d\'arbres') } id = Column(Integer, primary_key=True, info={'colanderalchemy': { 'widget': HiddenWidget() }}) hash = Column(Text, unique=True, default=lambda: str(uuid4()), info={ 'colanderalchemy': { 'widget': HiddenWidget() }, 'c2cgeoform': { 'duplicate': False } }) proprietaire = Column(Text, nullable=False) adresse = Column(Text, nullable=False) type_travaux_id = Column(Integer, ForeignKey('{}.type_travaux.id'.format(schema)), nullable=False, info={ 'colanderalchemy': { 'title': _('Type de travaux'), 'description': _('Type de travaux à entreprendre'), 'widget': deform_ext.RelationRadioChoiceWidget( Type_travaux, 'id', 'name', order_by='id', inline=True) } }) type_arborisation_id = Column( Integer, ForeignKey('{}.type_arborisation.id'.format(schema)), nullable=False, info={ 'colanderalchemy': { 'title': _('Type d\'arborisation'), 'widget': RelationSelect2Widget(Type_arborisation, 'id', 'name', order_by='id', default_value=('', _('- Select -'))) } }) essence = Column(Text, nullable=False) diametre = Column(Numeric(5, 2), nullable=False) hauteur = Column(Numeric(5, 2), nullable=False) motif = Column(Text, nullable=False, info={ 'colanderalchemy': { 'title': _('Motif'), 'widget': deform.widget.TextAreaWidget(rows=3), } }) email = Column(Text, nullable=False, info={ 'colanderalchemy': { 'title': _('E-mail'), 'validator': colander.Email() } }) date_demande = Column(Date, info={'colanderalchemy': { 'widget': HiddenWidget() }}, default=lambda: date.today()) location_position = Column(geoalchemy2.Geometry('POINT', 4326, management=True), info={ 'colanderalchemy': { 'title': _('Position'), 'typ': colander_ext.Geometry('POINT', srid=4326, map_srid=3857), 'widget': deform_ext.MapWidget( center=[741934, 5863213], zoom=14, fit_max_zoom=18) } }, nullable=False) type_travaux = relationship('Type_travaux', info={'colanderalchemy': { 'exclude': True }}) type_arborisation = relationship( 'Type_arborisation', info={'colanderalchemy': { 'exclude': True }})
class Person(Base): __tablename__ = 'tests_persons' __colanderalchemy_config__ = { 'title': 'A Person', 'description': 'Tell us about you.' } id = Column(Integer, primary_key=True, info={ 'colanderalchemy': { 'title': 'ID', 'widget': deform.widget.HiddenWidget(), 'admin_list': True } }) hash = Column(Text, unique=True) name = Column( Text, nullable=False, info={'colanderalchemy': { 'title': 'Your name', 'admin_list': True }}) first_name = Column(Text, nullable=False, info={'colanderalchemy': { 'title': 'Your first name' }}) age = Column(Integer, info={ 'colanderalchemy': { 'title': 'Your age', 'validator': colander.Range(18, ) } }) phones = relationship( Phone, cascade="all, delete-orphan", info={'colanderalchemy': { 'title': 'Phone numbers', }}) tags = relationship(TagsForPerson, cascade="all, delete-orphan", info={ 'colanderalchemy': { 'title': 'Tags', 'widget': RelationSelect2Widget(Tag, 'id', 'name', order_by='name', multiple=True) } }) validated = Column(Boolean, info={ 'colanderalchemy': { 'title': 'Validation', 'label': 'Validated' } })
class User(Base): __tablename__ = "user" __table_args__ = {"schema": _schema} __colanderalchemy_config__ = {"title": _("User"), "plural": _("Users")} __c2cgeoform_config__ = {"duplicate": True} item_type = Column("type", String(10), nullable=False, info={"colanderalchemy": { "widget": HiddenWidget() }}) __mapper_args__ = { "polymorphic_on": item_type, "polymorphic_identity": "user" } id = Column(Integer, primary_key=True, info={"colanderalchemy": { "widget": HiddenWidget() }}) username = Column(Unicode, unique=True, nullable=False, info={"colanderalchemy": { "title": _("Username") }}) _password = Column("password", Unicode, nullable=False, info={"colanderalchemy": { "exclude": True }}) temp_password = Column("temp_password", Unicode, nullable=True, info={"colanderalchemy": { "exclude": True }}) tech_data = Column(MutableDict.as_mutable(HSTORE), info={"colanderalchemy": { "exclude": True }}) email = Column( Unicode, nullable=False, info={"colanderalchemy": { "title": _("Email"), "validator": Email() }}) is_password_changed = Column(Boolean, default=False, info={ "colanderalchemy": { "title": _("The user changed his password") } }) settings_role_id = Column( Integer, info={ "colanderalchemy": { "title": _("Settings from role"), "description": "Only used for settings not for permissions", "widget": RelationSelect2Widget(Role, "id", "name", order_by="name", default_value=("", _("- Select -"))), } }, ) settings_role = relationship( Role, foreign_keys="User.settings_role_id", primaryjoin="Role.id==User.settings_role_id", info={ "colanderalchemy": { "title": _("Settings role"), "exclude": True } }, ) roles = relationship( Role, secondary=user_role, secondaryjoin=Role.id == user_role.c.role_id, backref=backref("users", order_by="User.username", info={"colanderalchemy": { "exclude": True }}), info={"colanderalchemy": { "title": _("Roles"), "exclude": True }}, ) last_login = Column( DateTime(timezone=True), info={ "colanderalchemy": { "title": _("Last login"), "missing": drop, "widget": DateTimeInputWidget(readonly=True), } }, ) expire_on = Column( DateTime(timezone=True), info={"colanderalchemy": { "title": _("Expiration date") }}) deactivated = Column(Boolean, default=False, info={"colanderalchemy": { "title": _("Deactivated") }}) def __init__( self, username: str = "", password: str = "", email: str = "", is_password_changed: bool = False, settings_role: Role = None, roles: List[Role] = None, expire_on: datetime = None, deactivated: bool = False, ) -> None: self.username = username self.password = password self.tech_data = {} self.email = email self.is_password_changed = is_password_changed if settings_role: self.settings_role = settings_role self.roles = roles or [] self.expire_on = expire_on self.deactivated = deactivated @property def password(self) -> str: """returns password""" return self._password # type: ignore @password.setter def password(self, password: str) -> None: """encrypts password on the fly.""" self._password = self.__encrypt_password(password) def set_temp_password(self, password: str) -> None: """encrypts password on the fly.""" self.temp_password = self.__encrypt_password(password) @staticmethod def __encrypt_password_legacy(password: str) -> str: """Hash the given password with SHA1.""" return sha1(password.encode("utf8")).hexdigest() # nosec @staticmethod def __encrypt_password(password: str) -> str: return crypt.crypt(password, crypt.METHOD_SHA512) def validate_password(self, passwd: str) -> bool: """Check the password against existing credentials. this method _MUST_ return a boolean. @param passwd: the password that was provided by the user to try and authenticate. This is the clear text version that we will need to match against the (possibly) encrypted one in the database. """ if self._password.startswith("$"): # new encryption method if compare_hash(self._password, crypt.crypt(passwd, self._password)): return True else: # legacy encryption method if compare_hash(self._password, self.__encrypt_password_legacy(passwd)): # convert to the new encryption method self._password = self.__encrypt_password(passwd) return True if (self.temp_password is not None and self.temp_password != "" and compare_hash(self.temp_password, crypt.crypt(passwd, self.temp_password))): self._password = self.temp_password self.temp_password = None self.is_password_changed = False return True return False def expired(self) -> bool: return self.expire_on is not None and self.expire_on < datetime.now( pytz.utc) def update_last_login(self) -> None: self.last_login = datetime.now(pytz.utc) def __str__(self) -> str: return self.username or ""
class LayerWMS(DimensionLayer): __tablename__ = "layer_wms" __table_args__ = {"schema": _schema} __colanderalchemy_config__ = { "title": _("WMS Layer"), "plural": _("WMS Layers") } __c2cgeoform_config__ = {"duplicate": True} __mapper_args__ = {"polymorphic_identity": "l_wms"} id = Column( Integer, ForeignKey(_schema + ".layer.id", ondelete="CASCADE"), primary_key=True, info={"colanderalchemy": { "missing": None, "widget": HiddenWidget() }}, ) ogc_server_id = Column( Integer, ForeignKey(_schema + ".ogc_server.id"), nullable=False, info={ "colanderalchemy": { "title": _("OGC server"), "column": 2, "widget": RelationSelect2Widget(OGCServer, "id", "name", order_by="name", default_value=("", _("- Select -"))), } }, ) layer = Column( Unicode, nullable=False, info={"colanderalchemy": { "title": _("WMS layer name"), "column": 2 }}) style = Column( Unicode, info={"colanderalchemy": { "title": _("Style"), "column": 2 }}) valid = Column( Boolean, info={ "colanderalchemy": { "title": _("Valid"), "column": 2, "widget": CheckboxWidget(readonly=True) } }, ) invalid_reason = Column( Unicode, info={ "colanderalchemy": { "title": _("Reason why I am not valid"), "column": 2, "widget": TextInputWidget(readonly=True), } }, ) time_mode = Column( Enum("disabled", "value", "range", native_enum=False), default="disabled", nullable=False, info={ "colanderalchemy": { "title": _("Time mode"), "column": 2, "widget": SelectWidget(values=(("disabled", _("Disabled")), ("value", _("Value")), ("range", _("Range")))), } }, ) time_widget = Column( Enum("slider", "datepicker", native_enum=False), default="slider", nullable=False, info={ "colanderalchemy": { "title": _("Time widget"), "column": 2, "widget": SelectWidget(values=(("slider", _("Slider")), ("datepicker", _("Datepicker")))), } }, ) # relationship with OGCServer ogc_server = relationship( "OGCServer", info={"colanderalchemy": { "title": _("OGC server"), "exclude": True }}) def __init__( self, name: str = "", layer: str = "", public: bool = True, time_mode: str = "disabled", time_widget: str = "slider", ) -> None: DimensionLayer.__init__(self, name=name, public=public) self.layer = layer self.time_mode = time_mode self.time_widget = time_widget @staticmethod def get_default(dbsession: Session) -> Optional[DimensionLayer]: return cast( Optional[DimensionLayer], dbsession.query(LayerWMS).filter( LayerWMS.name == "wms-defaults").one_or_none(), )
class User(Base): __tablename__ = 'user' __table_args__ = {'schema': _schema} __colanderalchemy_config__ = {'title': _('User'), 'plural': _('Users')} __c2cgeoform_config__ = {'duplicate': True} item_type = Column('type', String(10), nullable=False, info={'colanderalchemy': { 'widget': HiddenWidget() }}) __mapper_args__ = { 'polymorphic_on': item_type, 'polymorphic_identity': 'user', } id = Column(Integer, primary_key=True, info={'colanderalchemy': { 'widget': HiddenWidget() }}) username = Column(Unicode, unique=True, nullable=False, info={'colanderalchemy': { 'title': _('Username') }}) _password = Column('password', Unicode, nullable=False, info={'colanderalchemy': { 'exclude': True }}) temp_password = Column('temp_password', Unicode, nullable=True, info={'colanderalchemy': { 'exclude': True }}) tech_data = Column(MutableDict.as_mutable(HSTORE), info={'colanderalchemy': { 'exclude': True }}) email = Column( Unicode, nullable=False, info={'colanderalchemy': { 'title': _('Email'), 'validator': Email() }}) is_password_changed = Column(Boolean, default=False, info={ 'colanderalchemy': { 'title': _('The user changed his password') } }) settings_role_id = Column( Integer, info={ 'colanderalchemy': { 'title': _('Settings from role'), 'description': 'Only used for settings not for permissions', 'widget': RelationSelect2Widget(Role, 'id', 'name', order_by='name', default_value=('', _('- Select -'))) } }) settings_role = relationship(Role, foreign_keys='User.settings_role_id', primaryjoin='Role.id==User.settings_role_id', info={ 'colanderalchemy': { 'title': _('Settings role'), 'exclude': True } }) roles = relationship( Role, secondary=user_role, secondaryjoin=Role.id == user_role.c.role_id, backref=backref('users', info={'colanderalchemy': { 'exclude': True }}), info={'colanderalchemy': { 'title': _('Roles'), 'exclude': True }}) last_login = Column(DateTime(timezone=True), info={ 'colanderalchemy': { 'title': _('Last login'), 'missing': drop, 'widget': DateTimeInputWidget(readonly=True) } }) expire_on = Column( DateTime(timezone=True), info={'colanderalchemy': { 'title': _('Expiration date') }}) deactivated = Column(Boolean, default=False, info={'colanderalchemy': { 'title': _('Deactivated') }}) def __init__(self, username: str = '', password: str = '', email: str = '', is_password_changed: bool = False, settings_role: Role = None, roles: List[Role] = [], expire_on: datetime = None, deactivated: bool = False) -> None: self.username = username self.password = password self.tech_data = {} self.email = email self.is_password_changed = is_password_changed if settings_role: self.settings_role = settings_role self.roles = roles self.expire_on = expire_on self.deactivated = deactivated @property def password(self) -> str: """returns password""" return self._password # pragma: no cover @password.setter def password(self, password: str) -> None: """encrypts password on the fly.""" self._password = self.__encrypt_password(password) def set_temp_password(self, password: str) -> None: """encrypts password on the fly.""" self.temp_password = self.__encrypt_password(password) @staticmethod def __encrypt_password_legacy(password: str) -> str: """Hash the given password with SHA1.""" return sha1(password.encode('utf8')).hexdigest() # nosec @staticmethod def __encrypt_password(password: str) -> str: # TODO: remove pylint disable when https://github.com/PyCQA/pylint/issues/3047 is fixed return crypt.crypt(password, crypt.METHOD_SHA512) # pylint: disable=E1101 def validate_password(self, passwd: str) -> bool: """Check the password against existing credentials. this method _MUST_ return a boolean. @param passwd: the password that was provided by the user to try and authenticate. This is the clear text version that we will need to match against the (possibly) encrypted one in the database. """ if self._password.startswith('$'): # new encryption method if compare_hash(self._password, crypt.crypt(passwd, self._password)): return True else: # legacy encryption method if compare_hash(self._password, self.__encrypt_password_legacy(passwd)): # convert to the new encryption method self._password = self.__encrypt_password(passwd) return True if \ self.temp_password is not None and \ self.temp_password != '' and \ compare_hash(self.temp_password, crypt.crypt(passwd, self.temp_password)): self._password = self.temp_password self.temp_password = None self.is_password_changed = False return True return False def expired(self) -> bool: return self.expire_on is not None and self.expire_on < datetime.now( pytz.utc) def update_last_login(self) -> None: self.last_login = datetime.now(pytz.utc) def __unicode__(self) -> str: return self.username or '' # pragma: no cover
class Person(Base): __tablename__ = 'tests_persons' __colanderalchemy_config__ = { 'title': 'A Person', 'description': 'Tell us about you.' } __c2cgeoform_config__ = {'duplicate': True} id = Column(Integer, primary_key=True, info={ 'colanderalchemy': { 'title': 'ID', 'widget': deform.widget.HiddenWidget() } }) hash = Column(Text, unique=True) name = Column(Text, nullable=False, info={ 'colanderalchemy': { 'title': 'Your name', }, 'c2cgeoform': { 'duplicate': True } }) first_name = Column(Text, nullable=False, info={ 'colanderalchemy': { 'title': 'Your first name' }, 'c2cgeoform': { 'duplicate': True } }) age = Column(Integer, info={ 'colanderalchemy': { 'title': 'Your age', 'validator': colander.Range(18, ) } }) phones = relationship( Phone, cascade="all, delete-orphan", info={'colanderalchemy': { 'title': 'Phone numbers', }}) tags = relationship("Tag", secondary=person_tag, cascade="save-update,merge,refresh-expire", info={ 'colanderalchemy': { 'title': 'Tags', 'widget': RelationSelect2Widget(Tag, 'id', 'name', order_by='name', multiple=True), 'includes': ['id'] } }) validated = Column(Boolean, info={ 'colanderalchemy': { 'title': 'Validation', 'label': 'Validated' } })
class LayerWMS(DimensionLayer): __tablename__ = 'layer_wms' __table_args__ = {'schema': _schema} __colanderalchemy_config__ = { 'title': _('WMS Layer'), 'plural': _('WMS Layers') } __c2cgeoform_config__ = {'duplicate': True} __mapper_args__ = {'polymorphic_identity': 'l_wms'} id = Column( Integer, ForeignKey(_schema + '.layer.id', ondelete='CASCADE'), primary_key=True, info={'colanderalchemy': { 'missing': None, 'widget': HiddenWidget() }}) ogc_server_id = Column(Integer, ForeignKey(_schema + '.ogc_server.id'), nullable=False, info={ 'colanderalchemy': { 'title': _('OGC server'), 'column': 2, 'widget': RelationSelect2Widget( OGCServer, 'id', 'name', order_by='name', default_value=('', _('- Select -'))) } }) layer = Column( Unicode, nullable=False, info={'colanderalchemy': { 'title': _('WMS layer name'), 'column': 2 }}) style = Column( Unicode, info={'colanderalchemy': { 'title': _('Style'), 'column': 2 }}) time_mode = Column(Enum('disabled', 'value', 'range', native_enum=False), default='disabled', nullable=False, info={ 'colanderalchemy': { 'title': _('Time mode'), 'column': 2, 'widget': SelectWidget(values=(('disabled', _('Disabled')), ('value', _('Value')), ('range', _('Range')))) } }) time_widget = Column(Enum('slider', 'datepicker', native_enum=False), default='slider', nullable=False, info={ 'colanderalchemy': { 'title': _('Time widget'), 'column': 2, 'widget': SelectWidget(values=(('slider', _('Slider')), ('datepicker', _('Datepicker')))) } }) # relationship with OGCServer ogc_server = relationship( 'OGCServer', info={'colanderalchemy': { 'title': _('OGC server'), 'exclude': True }}) def __init__(self, name: str = '', layer: str = '', public: bool = True, time_mode: str = 'disabled', time_widget: str = 'slider') -> None: DimensionLayer.__init__(self, name=name, public=public) self.layer = layer self.time_mode = time_mode self.time_widget = time_widget @staticmethod def get_default(dbsession: Session) -> DimensionLayer: return dbsession.query(LayerWMS).filter( LayerWMS.name == 'wms-defaults').one_or_none()
class Excavation(Base): __tablename__ = 'excavation' __table_args__ = ( {"schema": schema} ) __colanderalchemy_config__ = { 'title': _('Application form for permission to carry out excavation work'), 'plural': _('Excavation forms') } __c2cgeoform_config__ = { 'duplicate': True } id = Column(Integer, primary_key=True, info={ # the `colanderalchemy` property allows to set a custom title for the # column or to use a specific widget. 'colanderalchemy': { 'title': _('Permission Number'), 'widget': HiddenWidget() }}) hash = Column(Text, unique=True, default=lambda: str(uuid4()), info={ 'colanderalchemy': { 'widget': HiddenWidget() }, 'c2cgeoform': { 'duplicate': False }}) reference_number = Column(Text, nullable=True, info={ 'colanderalchemy': { 'title': _('Reference Number') }, 'c2cgeoform': { 'duplicate': False }}) request_date = Column(Date, nullable=True, info={ 'colanderalchemy': { 'title': _('Request Date') }}) description = Column(Text, nullable=True, info={ 'colanderalchemy': { 'title': _('Description of the Work'), 'widget': deform.widget.TextAreaWidget(rows=3), }}) motif = Column(Text, nullable=True, info={ 'colanderalchemy': { 'title': _('Motive for the Work'), 'widget': deform.widget.TextAreaWidget(rows=3), }}) situations = relationship( "Situation", secondary=situation_for_permission, order_by=Situation.name, cascade="save-update,merge,refresh-expire", info={'colanderalchemy': {'exclude': True}}) # by default a Deform sequence widget is used for relationship columns, # which, for example, allows to create new contact persons in a sub-form. contact_persons = relationship( ContactPerson, # make sure persons are deleted when removed from the relation cascade="all, delete-orphan", info={'colanderalchemy': { 'title': _('Contact Persons') }}) location_district_id = Column( Integer, ForeignKey('c2cgeoform_demo.district.id'), info={ 'colanderalchemy': { 'title': _('District'), 'widget': RelationSelect2Widget( District, 'id', 'name', order_by='name', default_value=('', _('- Select -')) ) } } ) # if the name for the options should be internationalized, one # can create columns like 'name_fr' and 'name_de' in the table # 'District'. then in the translation files, the column name # can be "translated" (e.g. the French "translation" for the # column name would be 'name_fr'). to apply the translation use # the label `_('name'))` instead of `name`. location_street = Column(Text, nullable=False, info={ 'colanderalchemy': { 'title': _('Street') }}) location_postal_code = Column(Text, nullable=False, info={ 'colanderalchemy': { 'title': _('Postal Code') }}) location_town = Column(Text, nullable=False, info={ 'colanderalchemy': { 'title': _('Town') }}) # this is a search field to search for an address address_id = Column( Integer, ForeignKey('c2cgeoform_demo.address.id'), info={ 'colanderalchemy': { 'title': _('Address'), 'widget': RelationSearchWidget( url=lambda request: request.route_url('addresses'), model=Address, min_length=1, id_field='id', label_field='label' ) } } ) # to show a map for a geometry column, the column has to be defined as # follows. location_position = Column( geoalchemy2.Geometry('POINT', 4326, management=True), info={ 'colanderalchemy': { 'title': _('Position'), 'typ': colander_ext.Geometry('POINT', srid=4326, map_srid=3857), 'widget': deform_ext.MapWidget() }}) # Person in Charge for the Work responsible_title = Column(Text, nullable=True, info={ 'colanderalchemy': { 'title': _('Title'), 'validator': colander.OneOf(['mr', 'mrs']), 'widget': deform.widget.SelectWidget(values=( ('', _('- Select -')), ('mr', _('Mr.')), ('mrs', _('Mrs.')) )) }}) responsible_name = Column(Text, nullable=True, info={ 'colanderalchemy': { 'title': _('Name') }}) responsible_first_name = Column(Text, nullable=True, info={ 'colanderalchemy': { 'title': _('First Name') }}) responsible_mobile = Column(Text, nullable=True, info={ 'colanderalchemy': { 'title': _('Mobile Phone') }}) responsible_mail = Column(Text, nullable=True, info={ 'colanderalchemy': { 'title': _('Mail'), 'validator': colander.Email() }}) responsible_company = Column(Text, nullable=True, info={ 'colanderalchemy': { 'title': _('Company') }}) validated = Column(Boolean, info={ 'colanderalchemy': { 'title': _('Validation'), 'label': _('Validated') }, 'c2cgeoform': { 'duplicate': False }}) # Work footprint work_footprint = Column( geoalchemy2.Geometry('MULTIPOLYGON', 4326, management=True), info={ 'colanderalchemy': { 'title': _('Footprint for the Work'), 'typ': colander_ext.Geometry( 'MULTIPOLYGON', srid=4326, map_srid=3857), 'widget': deform_ext.MapWidget() }}) # Photo photos = relationship( Photo, cascade="all, delete-orphan", info={ 'colanderalchemy': { 'title': _('Photo') }})
class User(Base): # type: ignore """The user table representation.""" __tablename__ = "user" __table_args__ = {"schema": _schema} __colanderalchemy_config__ = { "title": _("User"), "plural": _("Users"), "description": Literal( _(""" <div class="help-block"> <p>Each user may have from 1 to n roles, but each user has a default role from which are taken some settings. The default role (defined through the "Settings from role" selection) has an influence on the role extent and on some functionalities regarding their configuration.</p> <p>Role extents for users can only be set in one role, because the application is currently not able to check multiple extents for one user, thus it is the default role which defines this unique extent.</p> <p>Any functionality specified as <b>single</b> can be defined only once per user. Hence, these functionalities have to be defined in the default role.</p> <p>By default, functionalities are not specified as <b>single</b>. Currently, the following functionalities are of <b>single</b> type:</p> <ul> <li><code>default_basemap</code></li> <li><code>default_theme</code></li> <li><code>preset_layer_filter</code></li> <li><code>open_panel</code></li> </ul> <p>Any other functionality (with <b>single</b> not set or set to <code>false</code>) can be defined in any role linked to the user.</p> <hr> </div> """)), } __c2cgeoform_config__ = {"duplicate": True} item_type = Column("type", String(10), nullable=False, info={"colanderalchemy": { "widget": HiddenWidget() }}) __mapper_args__ = { "polymorphic_on": item_type, "polymorphic_identity": "user" } id = Column(Integer, primary_key=True, info={"colanderalchemy": { "widget": HiddenWidget() }}) username = Column( Unicode, unique=True, nullable=False, info={ "colanderalchemy": { "title": _("Username"), "description": _("Name used for authentication (must be unique)."), } }, ) _password = Column("password", Unicode, nullable=False, info={"colanderalchemy": { "exclude": True }}) temp_password = Column("temp_password", Unicode, nullable=True, info={"colanderalchemy": { "exclude": True }}) tech_data = Column(MutableDict.as_mutable(HSTORE), info={"colanderalchemy": { "exclude": True }}) email = Column( Unicode, nullable=False, info={ "colanderalchemy": { "title": _("Email"), "description": _("Used to send emails to the user, for example in case of password recovery." ), "validator": Email(), } }, ) is_password_changed = Column( Boolean, default=False, info={ "colanderalchemy": { "title": _("The user changed his password"), "description": _("Indicates if user has changed his password."), } }, ) settings_role_id = Column( Integer, info={ "colanderalchemy": { "title": _("Settings from role"), "description": _("Used to get some settings for the user (not for permissions)." ), "widget": RelationSelect2Widget(Role, "id", "name", order_by="name", default_value=("", _("- Select -"))), } }, ) settings_role = relationship( Role, foreign_keys="User.settings_role_id", primaryjoin="Role.id==User.settings_role_id", info={ "colanderalchemy": { "title": _("Settings role"), "exclude": True } }, ) roles = relationship( Role, secondary=user_role, secondaryjoin=Role.id == user_role.c.role_id, backref=backref( "users", order_by="User.username", info={ "colanderalchemy": { "title": _("Users"), "description": _("Users granted with this role."), "exclude": True, } }, ), info={ "colanderalchemy": { "title": _("Roles"), "description": _("Roles granted to the user."), "exclude": True, } }, ) last_login = Column( DateTime(timezone=True), info={ "colanderalchemy": { "title": _("Last login"), "description": _("Date of the user's last login."), "missing": drop, "widget": DateTimeInputWidget(readonly=True), } }, ) expire_on = Column( DateTime(timezone=True), info={ "colanderalchemy": { "title": _("Expiration date"), "description": _("After this date the user will not be able to login anymore." ), } }, ) deactivated = Column( Boolean, default=False, info={ "colanderalchemy": { "title": _("Deactivated"), "description": _("Deactivate a user without removing it completely."), } }, ) def __init__( # nosec self, username: str = "", password: str = "", email: str = "", is_password_changed: bool = False, settings_role: Optional[Role] = None, roles: Optional[List[Role]] = None, expire_on: Optional[datetime] = None, deactivated: bool = False, ) -> None: self.username = username self.password = password self.tech_data = {} self.email = email self.is_password_changed = is_password_changed if settings_role: self.settings_role = settings_role self.roles = roles or [] self.expire_on = expire_on self.deactivated = deactivated @property def password(self) -> str: """Get the password.""" return self._password # type: ignore @password.setter def password(self, password: str) -> None: """Encrypt password on the fly.""" self._password = self.__encrypt_password(password) def set_temp_password(self, password: str) -> None: """Encrypt password on the fly.""" self.temp_password = self.__encrypt_password(password) @staticmethod def __encrypt_password_legacy(password: str) -> str: """Hash the given password with SHA1.""" return sha1(password.encode("utf8")).hexdigest() # nosec @staticmethod def __encrypt_password(password: str) -> str: return crypt.crypt(password, crypt.METHOD_SHA512) def validate_password(self, passwd: str) -> bool: """ Check the password against existing credentials. this method _MUST_ return a boolean. @param passwd: the password that was provided by the user to try and authenticate. This is the clear text version that we will need to match against the (possibly) encrypted one in the database. """ if self._password.startswith("$"): # new encryption method if compare_hash(self._password, crypt.crypt(passwd, self._password)): return True else: # legacy encryption method if compare_hash(self._password, self.__encrypt_password_legacy(passwd)): # convert to the new encryption method self._password = self.__encrypt_password(passwd) return True if (self.temp_password is not None and self.temp_password != "" # nosec and compare_hash(self.temp_password, crypt.crypt(passwd, self.temp_password))): self._password = self.temp_password self.temp_password = None self.is_password_changed = False return True return False def expired(self) -> bool: return self.expire_on is not None and self.expire_on < datetime.now( pytz.utc) def update_last_login(self) -> None: self.last_login = datetime.now(pytz.utc) def __str__(self) -> str: return self.username or ""