class Choice(Base): #: The table in the database __tablename__ = "choice" #: Database primary key for the row (running counter) id = Column(Integer, autoincrement=True, primary_key=True) #: Publicly exposed non-guessable id uuid = Column(UUID(as_uuid=True), default=uuid4) #: What the user sees for this choice choice_text = Column(String(256), default=None) #: How many times this choice has been voted votes = Column(Integer, default=0) #: Which question this choice is part of question_id = Column(Integer, ForeignKey('question.id')) question = relationship("Question", back_populates="choices", uselist=False) def __repr__(self): """Shell and debugger presentation.""" return "#{}: {}".format(self.id, self.choice_text) def __str__(self): """Python default and admin UI string presentation.""" return self.choice_text
class GroupMixin: """Basic fields for Websauna default group model.""" #: Assign the first user initially to this group DEFAULT_ADMIN_GROUP_NAME = 'admin' #: Running counter id of the group id = Column(Integer, autoincrement=True, primary_key=True) #: Publicly exposable ID of the group uuid = Column(UUID(as_uuid=True), default=uuid4) #: Human readable / machine referrable name of the group name = Column(String(64), nullable=False, unique=True) #: Human readable description of the group description = Column(String(256)) #: When this group was created. created_at = Column(UTCDateTime, default=now) #: When the group was updated last time. Please note that this does not concern group membership, only desription updates. updated_at = Column(UTCDateTime, onupdate=now) #: Extra JSON data to be stored with this group group_data = Column(NestedMutationDict.as_mutable(JSONB), default=dict)
class Question(Base): #: The table in the database __tablename__ = "question" #: Database primary key for the row (running counter) id = Column(Integer, autoincrement=True, primary_key=True) #: Publicly exposed non-guessable uuid = Column(UUID(as_uuid=True), default=uuid4) #: Question text question_text = Column(String(256), default=None) #: When this question was published published_at = Column(UTCDateTime, default=None) #: Relationship mapping between question and choice. #: Each choice can have only question. #: Deleteing question deletes its choices. choices = relationship("Choice", back_populates="question", lazy="dynamic", cascade="all, delete-orphan", single_parent=True) def is_recent(self): return self.published_at >= now() - datetime.timedelta(days=1) def __repr__(self): return "#{}: {}".format(self.id, self.question_text) def __str__(self): """Python default and admin UI string presentation.""" return self.question_text
class UserMixin: """A user who signs up with email or with email from social media. This mixin provides the default required columns for user model in Websauna. The user contains normal columns and then ``user_data`` JSON field where properties and non-structured data can be easily added without migrations. This is especially handy to store incoming OAuth fields from social networks. Think Facebook login data and user details. """ #: Running counter id of the user id = Column(Integer, autoincrement=True, primary_key=True) #: Publicly exposable ID of the user uuid = Column(UUID(as_uuid=True), default=uuid4) #: Though not displayed on the site, the concept of "username" is still preversed. If the site needs to have username (think Instragram, Twitter) the user is free to choose this username after the sign up. Username is null until the initial user activation is completed after db.flush() in create_activation(). username = Column(String(256), nullable=True, unique=True) email = Column(String(256), nullable=True, unique=True) #: Stores the password + hash + cycles as password hasher internal format.. By default uses Argon 2 format. See :py:meth:`websauna.system.Initializer.configure_password` hashed_password = Column('password', String(256), nullable=True) #: When this account was created created_at = Column(UTCDateTime, default=now) #: When the account data was updated last time updated_at = Column(UTCDateTime, onupdate=now) #: When this user was activated: email confirmed or first social login activated_at = Column(UTCDateTime, nullable=True) #: Is this user account enabled. The support can disable the user account in the case of suspected malicious activity. enabled = Column(Boolean(name="user_enabled_binary"), default=True) #: When this user accessed the system last time. None if the user has never logged in (only activation email sent). Information stored for the security audits. last_login_at = Column(UTCDateTime, nullable=True) #: From which IP address did this user log in from. If this IP is null the user has never logged in (only activation email sent). Information stored for the security audits. It is also useful for identifying the source country of users e.g. for localized versions. last_login_ip = Column(INET, nullable=True) #: Misc. user data as a bag of JSON. Do not access directly, but use JSONBProperties below user_data = Column(NestedMutationDict.as_mutable(JSONB), default=DEFAULT_USER_DATA) #: Store when this user changed the password or authentication details. Updating this value causes the system to drop all sessions which were created before this moment. E.g. you will kick out all old sessions on a password or email change. last_auth_sensitive_operation_at = Column(UTCDateTime, nullable=True, default=now) #: Full name of the user (if given) full_name = index_property("user_data", "full_name") #: How this user signed up to the site. May include string like "email", "facebook" or "dummy". Up to the application to use this field. Default social media logins and email sign up set this. registration_source = index_property("user_data", "registration_source") #: Social media data of the user as a dict keyed by user media social = index_property("user_data", "social") #: Is this the first login the user manages to do to our system. If this flag is set the user has not logged in to the system before and you can give warm welcoming experience. first_login = index_property("user_data", "first_login") @property def friendly_name(self) -> str: """How we present the user's name to the user itself. Picks one of 1) full name if set 2) username if set 3) email. """ full_name = self.full_name username = self.username friendly_name = self.email if full_name: # Return full_name if available friendly_name = full_name elif username and not username.startswith('user-'): # Get the username if it looks like non-automatic form friendly_name = username return friendly_name def generate_username(self) -> str: """The default username we give for the user. In the format user-{id}. """ assert self.id return 'user-{id}'.format(id=self.id) def is_activated(self) -> bool: """Has the user completed the email activation.""" return self.activated_at is not None def can_login(self) -> bool: """Is this user allowed to login.""" return self.enabled and self.is_activated() def is_in_group(self, name) -> bool: # TODO: groups - defined in Horus for g in self.groups: if g.name == name: return True return False def is_admin(self) -> bool: """Does this user the see the main admin interface link. TODO: This is very suboptimal, wasted database cycles, etc. Change this. """ return self.is_in_group(GroupMixin.DEFAULT_ADMIN_GROUP_NAME) def is_valid_session(self, session_created_at: datetime.datetime) -> bool: """Check if the current session is still valid for this user.""" return self.last_auth_sensitive_operation_at <= session_created_at def __str__(self): return self.friendly_name def __repr__(self): return "#{id}: {friendly_name}".format( id=self.id, friendly_name=self.friendly_name)