예제 #1
0
class Article(CardinalBase):
    scopes = ('public', 'private')
    progress_states = ('draft', 'wip', 'ready', 'accepted', 'scheduled',
                       'published', 'rejected', 'archived')

    id = PrimaryKeyField()
    publication = ForeignKeyField(model=Publication,
                                  column_name='publication_id',
                                  field='id',
                                  backref='articles',
                                  null=False,
                                  index=True)
    code = CharField(max_length=128, null=False, unique=True, index=True)
    slug = CharField(max_length=255, null=False, unique=True, index=True)
    title = CharField(max_length=255, null=False)
    license = ForeignKeyField(model=License,
                              column_name='license_id',
                              field='id',
                              backref='articles',
                              null=True,
                              index=True)
    copyright = CharField(max_length=64, null=False)
    scope = EnumField(choices=scopes, null=False, index=True, default='public')
    progress_state = EnumField(choices=progress_states,
                               null=False,
                               index=True,
                               default='draft')

    published_at = DateTimeField(null=True)
    created_at = DateTimeField(null=False, default=datetime.utcnow)
    updated_at = DateTimeField(null=False, default=datetime.utcnow)
    deleted_at = DateTimeField(null=True)

    class Meta:
        table_name = 'articles'
예제 #2
0
class UserEmail(CardinalBase):
    activation_states = ('pending', 'active')
    types = ('primary', 'normal')

    id = PrimaryKeyField()
    user = ForeignKeyField(model=User,
                           column_name='user_id',
                           field='id',
                           backref='emails',
                           null=False,
                           index=True)
    email = CharField(max_length=64, null=False, unique=True, index=True)
    type = EnumField(choices=types, null=False, index=True, default='normal')

    activation_state = EnumField(choices=activation_states,
                                 null=False,
                                 index=True,
                                 default='pending')
    activation_token = CharField(max_length=255, null=True, index=True)
    activation_token_expires_at = DateTimeField(null=True, default=None)

    created_at = DateTimeField(null=False, default=datetime.utcnow)
    updated_at = DateTimeField(null=False, default=datetime.utcnow)

    class Meta:
        table_name = 'user_emails'
예제 #3
0
class Contribution(CardinalBase, TimestampMixin):
    """Relationship between user and article."""

    roles = ('primary_author', 'author', 'proofreader', 'cooperator',
             'translator', 'translator_supervisor', 'compiler', 'supervisor')

    id = PrimaryKeyField()
    user = ForeignKeyField(model=User,
                           column_name='user_id',
                           field='id',
                           backref='contributions',
                           null=False)
    article = ForeignKeyField(model=Article,
                              column_name='article_id',
                              field='id',
                              backref='contributions',
                              null=False)
    role = EnumField(choices=roles, null=False, default='primary_author')

    class Meta:
        table_name = 'contributions'

    def __repr__(self):
        return (
            '<Contribution id:{} article_id:{} user_id:{} role:{}>').format(
                self.id, self.article_id, self.user_id, self.role)
예제 #4
0
class Site(CardinalBase, TimestampMixin, DeletedAtMixin, KeyMixin):
    """Site model (website) belongs to a project.

    Site has its type as application (external site) or publication.
    """

    calculation_states = ('off', 'on')
    instance_types = ('Application', 'Publication')

    id = PrimaryKeyField()
    slug = CharField(max_length=255, null=True)

    instance_id = IntegerField(null=True)
    instance_type = CharField(max_length=32, null=True)
    domain = CharField(max_length=64, null=True)
    calculation_state = EnumField(choices=calculation_states,
                                  null=False,
                                  default='off')
    read_key = CharField(max_length=128, null=False)
    write_key = CharField(max_length=128, null=False)
    is_pinned = BooleanField(default=False)

    project = ForeignKeyField(model=Project,
                              column_name='project_id',
                              field='id',
                              backref='sites',
                              null=False,
                              index=True)

    class Meta:
        table_name = 'sites'

    def __repr__(self):
        return '<Site id:{} project_id:{} domain:{} slug:{}>'.format(
            self.id, self.project_id, self.domain, self.slug)

    @reify
    def type(self):
        """Lower case alias to instance_type attribute."""
        return str(self.instance_type).lower()

    @reify
    def instance(self):
        return getattr(self, self.type)

    def instantiate(self, *args, **kwargs):
        return globals()[self.instance_type](*args, **kwargs)

    @reify
    def application(self):
        if self.type != 'application' or not self.instance_id:
            return None
        return Application.get(Application.id == self.instance_id)

    @reify
    def publication(self):
        if self.type != 'publication' or not self.instance_id:
            return None
        return Publication.get(Publication.id == self.instance_id)
class User(CardinalBase):
    activation_states = ('pending', 'active')

    id = PrimaryKeyField()
    name = CharField(max_length=64, null=True)
    username = CharField(max_length=32, null=True, index=True)
    email = CharField(max_length=64, null=False, unique=True, index=True)
    password = CharField(max_length=255)

    activation_state = EnumField(
        choices=activation_states,
        null=True, index=True, default='pending')

    created_at = DateTimeField(null=False, default=datetime.utcnow)
    updated_at = DateTimeField(null=False, default=datetime.utcnow)

    class Meta:
        table_name = 'users'
예제 #6
0
class Page(CardinalBase):
    scopes = ('public', 'private')

    id = PrimaryKeyField()
    application = ForeignKeyField(model=Application,
                                  column_name='application_id',
                                  field='id',
                                  backref='pages',
                                  null=False,
                                  index=True)
    code = CharField(max_length=128, null=False, unique=True, index=True)
    path = CharField(max_length=255, null=False, unique=True, index=True)
    title = CharField(max_length=255, null=False)
    scope = EnumField(choices=scopes, null=False, index=True, default='public')

    created_at = DateTimeField(null=False, default=datetime.utcnow)
    updated_at = DateTimeField(null=False, default=datetime.utcnow)
    deleted_at = DateTimeField(null=True)

    class Meta:
        table_name = 'pages'
class Project(CardinalBase):
    billing_states = ('none', 'pending', 'processing', 'valid')

    id = PrimaryKeyField()
    access_key_id = CharField(
        max_length=128, null=False, unique=True, index=True)
    plan = ForeignKeyField(
        model=Plan, column_name='plan_id', field='id',
        backref='projects', null=False, index=True)
    subscription_id = CharField(max_length=64, null=True, index=True)
    namespace = CharField(max_length=32, null=False)
    name = CharField(max_length=128, null=False)
    description = CharField(max_length=255, null=True)
    billing_state = EnumField(
        choices=billing_states, null=False, default='none')

    created_at = DateTimeField(null=False, default=datetime.utcnow)
    updated_at = DateTimeField(null=False, default=datetime.utcnow)
    deleted_at = DateTimeField(null=True)

    class Meta:
        table_name = 'projects'
예제 #8
0
class Page(CardinalBase, TimestampMixin, DeletedAtMixin, CodeMixin):
    """Web page belong to user's application, which is recorded via script."""

    scopes = ('public', 'private')

    id = PrimaryKeyField()
    path = CharField(max_length=255, null=False)

    application = ForeignKeyField(model=Application,
                                  column_name='application_id',
                                  field='id',
                                  backref='pages',
                                  null=False)
    code = CharField(max_length=128, null=True)
    title = CharField(max_length=128, null=True)
    scope = EnumField(choices=scopes, null=False, default='public')

    class Meta:
        table_name = 'pages'

    def __repr__(self):
        return '<Page id:{} application_id:{} title:{}>'.format(
            self.id, self.application_id, self.title)
예제 #9
0
class Membership(CardinalBase):
    roles = ('primary_owner', 'owner', 'member')

    id = PrimaryKeyField()
    user = ForeignKeyField(model=User,
                           column_name='user_id',
                           field='id',
                           backref='memberships',
                           null=False,
                           index=True)
    project = ForeignKeyField(model=Project,
                              column_name='project_id',
                              field='id',
                              backref='memberships',
                              null=True,
                              index=True)
    role = EnumField(choices=roles, null=False, default='member')

    created_at = DateTimeField(null=False, default=datetime.utcnow)
    updated_at = DateTimeField(null=False, default=datetime.utcnow)

    class Meta:
        table_name = 'memberships'
class Contribution(CardinalBase):
    roles = ('primary_author', 'author', 'proofreader', 'cooperator',
             'translator', 'translation_supervisor', 'compiler', 'supervisor')

    id = PrimaryKeyField()
    user = ForeignKeyField(model=User,
                           column_name='user_id',
                           field='id',
                           backref='contributions',
                           null=False,
                           index=True)
    article = ForeignKeyField(model=Article,
                              column_name='article_id',
                              field='id',
                              backref='contributions',
                              null=False,
                              index=True)
    role = EnumField(choices=roles, null=False, default='primary_author')

    created_at = DateTimeField(null=False, default=datetime.utcnow)
    updated_at = DateTimeField(null=False, default=datetime.utcnow)

    class Meta:
        table_name = 'contributions'
class Site(CardinalBase):
    calculation_states = ('off', 'on')

    id = PrimaryKeyField()
    project = ForeignKeyField(
        model=Project, column_name='project_id', field='id',
        backref='projects', null=False, index=True)
    hosting_id = IntegerField(null=True)
    hosting_type = CharField(max_length=32, null=True)
    domain = CharField(max_length=32, null=False)
    calculation_state = EnumField(
        choices=calculation_states,
        null=False, index=True, default='off')
    read_key = CharField(max_length=128, null=False, unique=True, index=True)
    write_key = CharField(max_length=128, null=False, unique=True, index=True)
    is_pinned = BooleanField(null=False, default=False)

    created_at = DateTimeField(null=False, default=datetime.utcnow)
    updated_at = DateTimeField(null=False, default=datetime.utcnow)

    deleted_at = DateTimeField(null=True)

    class Meta:
        table_name = 'sites'
예제 #12
0
class Membership(CardinalBase, TimestampMixin):
    """Relationship between user and project."""

    roles = ('primary_owner', 'owner', 'member')

    id = PrimaryKeyField()
    user = ForeignKeyField(model=User,
                           column_name='user_id',
                           field='id',
                           backref='memberships',
                           null=False)
    project = ForeignKeyField(model=Project,
                              column_name='project_id',
                              field='id',
                              backref='memberships',
                              null=True)
    role = EnumField(choices=roles, null=False, default='member')

    class Meta:
        table_name = 'memberships'

    def __repr__(self):
        return ('<Membership id:{} user_id:{} project_id:{} role:{}>').format(
            self.id, self.user_id, self.project_id, self.role)
예제 #13
0
class Project(CardinalBase, TimestampMixin, DeletedAtMixin, KeyMixin):
    """Publishing project as workspace."""

    billing_states = ('none', 'pending', 'processing', 'valid')

    id = PrimaryKeyField()
    access_key_id = CharField(max_length=128, null=False)
    plan = ForeignKeyField(model=Plan,
                           column_name='plan_id',
                           field='id',
                           backref='publications',
                           null=False)
    subscription_id = CharField(max_length=64, null=True)
    namespace = CharField(max_length=32, null=False)
    name = CharField(max_length=128, null=False)
    description = CharField(max_length=255, null=True)
    billing_state = EnumField(choices=billing_states,
                              null=False,
                              default='none')

    class Meta:
        table_name = 'projects'

    def __init__(self, *args, **kwargs):
        from peewee import ManyToManyField
        # avoid circular dependencies
        from .membership import Membership
        from .user import User

        users = ManyToManyField(model=User,
                                backref='projects',
                                through_model=Membership)
        self._meta.add_field('users', users)

        super().__init__(*args, **kwargs)

    def __repr__(self):
        return '<Project id:{} namespace:{} name:{} >'.format(
            self.id, self.namespace, self.name)

    @classmethod
    def get_by_access_key_id(cls, access_key_id):
        """Fetches a project by unique access_key_id string."""
        # pylint: disable=no-member
        return cls.select().where(
            cls.access_key_id_key == access_key_id, cls.billing_state == 'none'
            or cls.billing_state == 'valid').get()

    @property
    def applications(self):
        from .site import Site
        # pylint: disable=no-member
        return Site.select().join(
            Application,
            on=((Site.instance_type == 'Application') &
                (Site.instance_id == Application.id))).where(
                    Site.project_id == self.id)

    @property
    def publications(self):
        from .site import Site
        # pylint: disable=no-member
        return Site.select().join(
            Publication,
            on=((Site.instance_type == 'Publication') &
                (Site.instance_id == Publication.id))).where(
                    Site.project_id == self.id)

    @property
    def primary_owner(self):
        """Returns user as primary owner of this project.

        This may raise MembeshipDoesNotExist Exception.
        """
        from .membership import Membership
        from .user import User

        # pylint: disable=no-member
        m = (Membership.select(
            Membership, self.__class__,
            User).join(User).switch(Membership).join(self.__class__).where(
                (Membership.role == 'primary_owner')
                & (Membership.project_id == self.id)).get())
        return m.user
예제 #14
0
class Article(CardinalBase, TimestampMixin, DeletedAtMixin, CodeMixin):
    scopes = ('public', 'private')
    progress_states = (
        'draft', 'wip', 'ready', 'scheduled', 'published',
        'rejected', 'archived')

    id = PrimaryKeyField()
    path = CharField(max_length=255, null=False)

    publication = ForeignKeyField(
        model=Publication, column_name='publication_id', field='id',
        backref='articles', null=False)
    chapter = ForeignKeyField(
        model=Chapter, column_name='chapter_id', field='id',
        backref='articles', null=False)
    code = CharField(max_length=128, null=False)
    title = CharField(max_length=255, null=True)
    scope = EnumField(choices=scopes, null=False, default='public')

    content = TextField(null=True)
    content_html = TextField(null=True)
    content_updated_at = DateTimeField(null=True)

    license = ForeignKeyField(
        model=License, column_name='license_id', field='id',
        backref='articles', null=True)
    copyright = CharField(max_length=64, null=False)
    progress_state = EnumField(
        choices=progress_states, null=False, default='draft')
    published_at = DateTimeField(null=True)

    users = None

    class Meta:
        table_name = 'articles'

    def __init__(self, *args, **kwargs):
        from peewee import ManyToManyField
        # pylint: disable=cyclic-import
        from .contribution import Contribution
        from .user import User
        # pylint: enable=cyclic-import

        users = ManyToManyField(
            model=User, backref='articles', through_model=Contribution)
        self._meta.add_field('users', users)

        super().__init__(*args, **kwargs)

    def __repr__(self):
        return (
            '<Article id:{} publication_id:{} progress_state:{} path:{}>'
        ).format(
            self.id, self.publication_id, self.progress_state, self.path)

    @classproperty
    def progress_state_as_choices(cls):
        """Returns choice pair as list for progress_state.

        See classproperty implementation.
        """
        # pylint: disable=no-self-argument
        return [(str(i), v) for (i, v) in enumerate(cls.progress_states)]

    @classmethod
    def published_on(cls, publication):
        """Provides scope by publication."""
        # pylint: disable=no-member
        return Article.select().join(Publication).where(
            Publication.id == publication.id)

    @property
    def available_progress_states_as_choices(self) -> list:
        """Returns a list contains tulples hold indices and values."""
        # TODO: state machine?
        next_states = self.next_progress_states()
        return [  # pylint: disable=not-an-iterable
            (i, v) for (i, v) in self.__class__.progress_state_as_choices
            if self.progress_state == v or v in next_states
        ]

    def next_progress_states(self) -> tuple:
        """Returns state names for next based on current progress_state."""
        # pylint: disable=too-many-return-statements
        if self.progress_state == 'draft':
            return ('wip', 'ready', 'archived')
        if self.progress_state == 'wip':
            return ('draft', 'ready', 'archived')
        if self.progress_state == 'ready':
            return ('wip', 'scheduled', 'published', 'rejected', 'archived')
        if self.progress_state == 'scheduled':
            return ('ready', 'published', 'archived')
        if self.progress_state == 'published':
            return ('archived',)
        if self.progress_state == 'rejected':
            return ('wip', 'archived')
        if self.progress_state == 'archived':
            return ('draft', 'wip')
        return self.__class__.progress_states
예제 #15
0
class UserEmail(CardinalBase, TokenizerMixin, TimestampMixin):
    """Emails belong to a user.

    A user has multiple emails. User object has primary email.
    """

    activation_states = ('pending', 'active')
    types = ('primary', 'normal')

    id = PrimaryKeyField()
    user = ForeignKeyField(
        model=User, column_name='user_id', field='id',
        backref='emails', null=False)
    email = CharField(max_length=64, null=False, unique=True)
    type = EnumField(
        choices=types, null=False, default='normal')

    activation_state = EnumField(
        choices=activation_states, null=False, default='pending')
    activation_token = CharField(max_length=255, null=True)
    activation_token_expires_at = DateTimeField(
        null=True, default=None)

    def generate_activation_token(self, expiration=3600):
        token = self.generate_token(key='user_email',
                                    salt='user_email_activation',
                                    expiration=expiration)
        # TODO: Move email activation service
        self.activation_token = token
        self.activation_state = 'pending'
        self.activation_token_expires_at = datetime.utcnow() + \
            timedelta(seconds=expiration)
        return token

    def activate(self, token):
        data = self.decode_token(token, salt='user_email_activation')
        if data.get('user_email') != self.id:
            return False
        self.activation_state = 'active'
        self.activation_token = None
        self.activation_token_expires_at = None
        return self.save()

    def make_as_primary(self):
        klass = self.__class__
        with self._meta.database.atomic():
            try:
                current_primary = klass.select().where(
                    klass.user_id == self.user_id,
                    klass.activation_state == 'active',
                    klass.type == 'primary').get()
                current_primary.type = 'normal'
            except klass.DoesNotExist:
                self._meta.database.rollback()
                return False
            self.type = 'primary'
            self.user.email = self.email
            current_primary.save()
            return self.save()

    class Meta:
        table_name = 'user_emails'

    def __repr__(self):
        return '<UserEmail id:{}, user_id:{}, email:{}, activation_state:{}>' \
            .format(self.id, self.user_id, self.email, self.activation_state)
예제 #16
0
class User(CardinalBase, TokenizerMixin, TimestampMixin):
    """User account has a primary email for authentication."""

    activation_states = ('pending', 'active')

    id = PrimaryKeyField()
    name = CharField(max_length=64, null=True)
    username = CharField(max_length=32, null=True)
    email = CharField(max_length=64, null=False, unique=True)
    password = CharField(max_length=255, null=False)
    activation_state = EnumField(choices=activation_states,
                                 null=True,
                                 default='pending')

    reset_password_token = CharField(max_length=255, null=True)
    reset_password_token_expires_at = DateTimeField(null=True, default=None)
    reset_password_token_sent_at = DateTimeField(null=True, default=None)

    class Meta:
        table_name = 'users'

    @classmethod
    def encrypt_password(cls, pw):
        pwhash = bcrypt.hashpw(pw.encode('utf8'), bcrypt.gensalt())
        return pwhash.decode('utf8')

    def __init__(self, *args, **kwargs):
        from peewee import ManyToManyField
        # pylint: disable=cyclic-import
        from .article import Article
        from .contribution import Contribution
        from .project import Project
        from .membership import Membership
        # pylint: enable=cyclic-import

        # collaborator
        projects = ManyToManyField(model=Project,
                                   backref='users',
                                   through_model=Membership)
        self._meta.add_field('projects', projects)

        # contributor
        articles = ManyToManyField(Article,
                                   backref='users',
                                   through_model=Contribution)
        self._meta.add_field('articles', articles)

        super().__init__(*args, **kwargs)

    def __repr__(self):
        return '<User id:{}, email:{}>'.format(self.id, self.email)

    # TODO: Create custom field:
    # See: https://github.com/coleifer/peewee/blob/\
    #   dc0ac68f3a596e27e117698393b4ab64d2f92617/playhouse/fields.py#L54
    def set_password(self, pw):
        self.password = self.__class__.encrypt_password(pw)

    def verify_password(self, pw):
        if self.password is not None:
            expected_hash = self.password.encode('utf8')
            return bcrypt.checkpw(pw.encode('utf8'), expected_hash)
        return False

    def generate_reset_password_token(self, expiration=3600):
        token = self.generate_token(key='user',
                                    salt='reset_password',
                                    expiration=expiration)
        # TODO: Move reset password service
        self.reset_password_token = token
        self.reset_password_token_expires_at = datetime.utcnow() + \
            timedelta(seconds=expiration)
        return token

    def reset_password(self, token, password):
        data = self.decode_token(token, salt='reset_password')
        if data.get('user') != self.id:
            return False
        self.set_password(password)
        self.reset_password_token = None
        self.reset_password_token_expires_at = None
        self.reset_password_token_sent_at = None
        return self.save()