예제 #1
0
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
예제 #2
0
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)
예제 #3
0
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
예제 #4
0
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)