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'
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'
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)
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'
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'
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)
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'
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)
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
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
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)
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()