# -*- coding: utf-8 -*- """ (c) 2014-2016 - Copyright Red Hat Inc Authors: Pierre-Yves Chibon """ __requires__ = ['SQLAlchemy >= 0.8', 'jinja2 >= 2.4'] import pkg_resources import datetime import logging import json import operator import sqlalchemy as sa from sqlalchemy import create_engine, MetaData from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import backref from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import scoped_session from sqlalchemy.orm import relation CONVENTION = { "ix": 'ix_%(table_name)s_%(column_0_label)s', # Checks are currently buggy and prevent us from naming them correctly #"ck": "ck_%(table_name)s_%(constraint_name)s", "fk": "%(table_name)s_%(column_0_name)s_fkey", "pk": "%(table_name)s_pkey", "uq": "%(table_name)s_%(column_0_name)s_key", } BASE = declarative_base(metadata=MetaData(naming_convention=CONVENTION)) ERROR_LOG = logging.getLogger('pagure.model') # hit w/ all the id field we use # pylint: disable=invalid-name # pylint: disable=too-few-public-methods # pylint: disable=no-init # pylint: disable=no-member # pylint: disable=too-many-lines def create_tables(db_url, alembic_ini=None, acls=None, debug=False): """ Create the tables in the database using the information from the url obtained. :arg db_url, URL used to connect to the database. The URL contains information with regards to the database engine, the host to connect to, the user and password and the database name. ie: ://:@/ :kwarg alembic_ini, path to the alembic ini file. This is necessary to be able to use alembic correctly, but not for the unit-tests. :kwarg debug, a boolean specifying wether we should have the verbose output of sqlalchemy or not. :return a session that can be used to query the database. """ if db_url.startswith('sqlite'): engine = create_engine(db_url, echo=debug) else: engine = create_engine(db_url, echo=debug, client_encoding='utf8') from pagure.ui.plugins import get_plugin_tables get_plugin_tables() BASE.metadata.create_all(engine) # engine.execute(collection_package_create_view(driver=engine.driver)) if db_url.startswith('sqlite:'): # Ignore the warning about con_record # pylint: disable=unused-argument def _fk_pragma_on_connect(dbapi_con, _): # pragma: no cover ''' Tries to enforce referential constraints on sqlite. ''' dbapi_con.execute('pragma foreign_keys=ON') sa.event.listen(engine, 'connect', _fk_pragma_on_connect) if alembic_ini is not None: # pragma: no cover # then, load the Alembic configuration and generate the # version table, "stamping" it with the most recent rev: # Ignore the warning missing alembic # pylint: disable=import-error from alembic.config import Config from alembic import command alembic_cfg = Config(alembic_ini) command.stamp(alembic_cfg, "head") scopedsession = scoped_session(sessionmaker(bind=engine)) BASE.metadata.bind = scopedsession # Insert the default data into the db create_default_status(scopedsession, acls=acls) return scopedsession def create_default_status(session, acls=None): """ Insert the defaults status in the status tables. """ statuses = ['Open', 'Invalid', 'Insufficient data', 'Fixed', 'Duplicate'] for status in statuses: ticket_stat = StatusIssue(status=status) session.add(ticket_stat) try: session.commit() except SQLAlchemyError: # pragma: no cover session.rollback() ERROR_LOG.debug('Status %s could not be added', ticket_stat) for status in ['Open', 'Closed', 'Merged']: pr_stat = StatusPullRequest(status=status) session.add(pr_stat) try: session.commit() except SQLAlchemyError: # pragma: no cover session.rollback() ERROR_LOG.debug('Status %s could not be added', pr_stat) for grptype in ['user', 'admin']: grp_type = PagureGroupType(group_type=grptype) session.add(grp_type) try: session.commit() except SQLAlchemyError: # pragma: no cover session.rollback() ERROR_LOG.debug('Type %s could not be added', grptype) for acl in sorted(acls) or {}: item = ACL( name=acl, description=acls[acl] ) session.add(item) try: session.commit() except SQLAlchemyError: # pragma: no cover session.rollback() ERROR_LOG.debug('ACL %s could not be added', acl) class StatusIssue(BASE): """ Stores the status a ticket can have. Table -- status_issue """ __tablename__ = 'status_issue' id = sa.Column(sa.Integer, primary_key=True) status = sa.Column(sa.String(255), nullable=False, unique=True) class StatusPullRequest(BASE): """ Stores the status a pull-request can have. Table -- status_issue """ __tablename__ = 'status_pull_requests' id = sa.Column(sa.Integer, primary_key=True) status = sa.Column(sa.String(255), nullable=False, unique=True) class User(BASE): """ Stores information about users. Table -- users """ __tablename__ = 'users' id = sa.Column(sa.Integer, primary_key=True) user = sa.Column(sa.String(255), nullable=False, unique=True, index=True) fullname = sa.Column(sa.String(255), nullable=False, index=True) public_ssh_key = sa.Column(sa.Text, nullable=True) default_email = sa.Column(sa.Text, nullable=False) password = sa.Column(sa.Text, nullable=True) token = sa.Column(sa.String(50), nullable=True) created = sa.Column( sa.DateTime, nullable=False, default=sa.func.now()) updated_on = sa.Column( sa.DateTime, nullable=False, default=sa.func.now(), onupdate=sa.func.now()) # Relations group_objs = relation( "PagureGroup", secondary="pagure_user_group", primaryjoin="users.c.id==pagure_user_group.c.user_id", secondaryjoin="pagure_group.c.id==pagure_user_group.c.group_id", backref="users", ) session = relation("PagureUserVisit", backref="user") @property def username(self): ''' Return the username. ''' return self.user @property def groups(self): ''' Return the list of Group.group_name in which the user is. ''' return [group.group_name for group in self.group_objs] def __repr__(self): ''' Return a string representation of this object. ''' return 'User: %s - name %s' % (self.id, self.user) def to_json(self, public=False): ''' Return a representation of the User in a dictionnary. ''' output = { 'name': self.user, 'fullname': self.fullname, } if not public: output['default_email'] = self.default_email output['emails'] = [email.email for email in self.emails] return output class UserEmail(BASE): """ Stores email information about the users. Table -- user_emails """ __tablename__ = 'user_emails' id = sa.Column(sa.Integer, primary_key=True) user_id = sa.Column( sa.Integer, sa.ForeignKey( 'users.id', onupdate='CASCADE', ), nullable=False, index=True) email = sa.Column(sa.String(255), nullable=False, unique=True) user = relation( 'User', foreign_keys=[user_id], remote_side=[User.id], backref=backref( 'emails', cascade="delete, delete-orphan", single_parent=True ) ) class UserEmailPending(BASE): """ Stores email information about the users. Table -- user_emails_pending """ __tablename__ = 'user_emails_pending' id = sa.Column(sa.Integer, primary_key=True) user_id = sa.Column( sa.Integer, sa.ForeignKey( 'users.id', onupdate='CASCADE', ), nullable=False, index=True) email = sa.Column(sa.String(255), nullable=False, unique=True) token = sa.Column(sa.String(50), nullable=True) created = sa.Column( sa.DateTime, nullable=False, default=sa.func.now()) user = relation( 'User', foreign_keys=[user_id], remote_side=[User.id], backref=backref( 'emails_pending', cascade="delete, delete-orphan", single_parent=True ) ) class Project(BASE): """ Stores the projects. Table -- projects """ __tablename__ = 'projects' id = sa.Column(sa.Integer, primary_key=True) user_id = sa.Column( sa.Integer, sa.ForeignKey( 'users.id', onupdate='CASCADE', ), nullable=False, index=True) namespace = sa.Column(sa.String(255), nullable=True, index=True) name = sa.Column(sa.String(255), nullable=False, index=True) description = sa.Column(sa.Text, nullable=True) url = sa.Column(sa.Text, nullable=True) _settings = sa.Column(sa.Text, nullable=True) # The hook_token is used to sign the notification sent via web-hook hook_token = sa.Column(sa.String(40), nullable=False, unique=True) avatar_email = sa.Column(sa.Text, nullable=True) is_fork = sa.Column(sa.Boolean, default=False, nullable=False) parent_id = sa.Column( sa.Integer, sa.ForeignKey( 'projects.id', onupdate='CASCADE', ), nullable=True) _priorities = sa.Column(sa.Text, nullable=True) _milestones = sa.Column(sa.Text, nullable=True) _reports = sa.Column(sa.Text, nullable=True) _notifications = sa.Column(sa.Text, nullable=True) date_created = sa.Column(sa.DateTime, nullable=False, default=datetime.datetime.utcnow) parent = relation('Project', remote_side=[id], backref='forks') user = relation('User', foreign_keys=[user_id], remote_side=[User.id], backref='projects') users = relation( 'User', secondary="user_projects", primaryjoin="projects.c.id==user_projects.c.project_id", secondaryjoin="users.c.id==user_projects.c.user_id", backref='co_projects' ) groups = relation( "PagureGroup", secondary="projects_groups", primaryjoin="projects.c.id==projects_groups.c.project_id", secondaryjoin="pagure_group.c.id==projects_groups.c.group_id", backref="projects", ) unwatchers = relation( "Watcher", primaryjoin="and_(Project.id==Watcher.project_id, " "Watcher.watch=='0')" ) @property def path(self): ''' Return the name of the git repo on the filesystem. ''' return '%s.git' % self.fullname @property def fullname(self): ''' Return the name of the git repo as user/project if it is a project forked, otherwise it returns the project name. ''' str_name = self.name if self.namespace: str_name = '%s/%s' % (self.namespace, str_name) if self.is_fork: str_name = "forks/%s/%s" % (self.user.user, str_name) return str_name @property def tags_text(self): ''' Return the list of tags in a simple text form. ''' return [tag.tag for tag in self.tags] @property def settings(self): """ Return the dict stored as string in the database as an actual dict object. """ default = { 'issue_tracker': True, 'project_documentation': False, 'pull_requests': True, 'Only_assignee_can_merge_pull-request': False, 'Minimum_score_to_merge_pull-request': -1, 'Web-hooks': None, 'Enforce_signed-off_commits_in_pull-request': False, 'always_merge': False, 'issues_default_to_private': False, } if self._settings: current = json.loads(self._settings) # Update the current dict with the new keys for key in default: if key not in current: current[key] = default[key] elif key == 'Minimum_score_to_merge_pull-request': current[key] = int(current[key]) elif str(current[key]).lower() in ['true', 'y']: current[key] = True return current else: return default @settings.setter def settings(self, settings): ''' Ensures the settings are properly saved. ''' self._settings = json.dumps(settings) @property def milestones(self): """ Return the dict stored as string in the database as an actual dict object. """ milestones = {} if self._milestones: milestones = json.loads(self._milestones) return milestones @milestones.setter def milestones(self, milestones): ''' Ensures the milestones are properly saved. ''' self._milestones = json.dumps(milestones) @property def priorities(self): """ Return the dict stored as string in the database as an actual dict object. """ priorities = {} if self._priorities: priorities = json.loads(self._priorities) return priorities @priorities.setter def priorities(self, priorities): ''' Ensures the priorities are properly saved. ''' self._priorities = json.dumps(priorities) @property def notifications(self): """ Return the dict stored as string in the database as an actual dict object. """ notifications = {} if self._notifications: notifications = json.loads(self._notifications) return notifications @notifications.setter def notifications(self, notifications): ''' Ensures the notifications are properly saved. ''' self._notifications = json.dumps(notifications) @property def reports(self): """ Return the dict stored as string in the database as an actual dict object. """ reports = {} if self._reports: reports = json.loads(self._reports) return reports @reports.setter def reports(self, reports): ''' Ensures the reports are properly saved. ''' self._reports = json.dumps(reports) @property def open_requests(self): ''' Returns the number of open pull-requests for this project. ''' return BASE.metadata.bind.query( PullRequest ).filter( self.id == PullRequest.project_id ).filter( PullRequest.status == 'Open' ).count() @property def open_tickets(self): ''' Returns the number of open tickets for this project. ''' return BASE.metadata.bind.query( Issue ).filter( self.id == Issue.project_id ).filter( Issue.status == 'Open' ).count() @property def open_tickets_public(self): ''' Returns the number of open tickets for this project. ''' return BASE.metadata.bind.query( Issue ).filter( self.id == Issue.project_id ).filter( Issue.status == 'Open' ).filter( Issue.private == False ).count() def to_json(self, public=False, api=False): ''' Return a representation of the project as JSON. ''' output = { 'id': self.id, 'name': self.name, 'description': self.description, 'namespace': self.namespace, 'parent': self.parent.to_json( public=public, api=api) if self.parent else None, 'date_created': self.date_created.strftime('%s'), 'user': self.user.to_json(public=public), 'tags': self.tags_text, 'priorities': self.priorities, } if not api: output['settings'] = self.settings return output class ProjectUser(BASE): """ Stores the user of a projects. Table -- user_projects """ __tablename__ = 'user_projects' __table_args__ = ( sa.UniqueConstraint('project_id', 'user_id'), ) id = sa.Column(sa.Integer, primary_key=True) project_id = sa.Column( sa.Integer, sa.ForeignKey( 'projects.id', onupdate='CASCADE', ), nullable=False) user_id = sa.Column( sa.Integer, sa.ForeignKey( 'users.id', onupdate='CASCADE', ), nullable=False, index=True) class Issue(BASE): """ Stores the issues reported on a project. Table -- issues """ __tablename__ = 'issues' id = sa.Column(sa.Integer, primary_key=True) uid = sa.Column(sa.String(32), unique=True, nullable=False) project_id = sa.Column( sa.Integer, sa.ForeignKey( 'projects.id', onupdate='CASCADE', ), primary_key=True) title = sa.Column( sa.Text, nullable=False) content = sa.Column( sa.Text(), nullable=False) user_id = sa.Column( sa.Integer, sa.ForeignKey( 'users.id', onupdate='CASCADE', ), nullable=False, index=True) assignee_id = sa.Column( sa.Integer, sa.ForeignKey( 'users.id', onupdate='CASCADE', ), nullable=True, index=True) status = sa.Column( sa.String(255), sa.ForeignKey( 'status_issue.status', onupdate='CASCADE', ), default='Open', nullable=False) private = sa.Column(sa.Boolean, nullable=False, default=False) priority = sa.Column(sa.Integer, nullable=True, default=None) milestone = sa.Column(sa.String(255), nullable=True, default=None) date_created = sa.Column(sa.DateTime, nullable=False, default=datetime.datetime.utcnow) closed_at = sa.Column(sa.DateTime, nullable=True) project = relation( 'Project', foreign_keys=[project_id], remote_side=[Project.id], backref=backref( 'issues', cascade="delete, delete-orphan", single_parent=True) ) user = relation('User', foreign_keys=[user_id], remote_side=[User.id], backref='issues') assignee = relation('User', foreign_keys=[assignee_id], remote_side=[User.id], backref='assigned_issues') parents = relation( "Issue", secondary="issue_to_issue", primaryjoin="issues.c.uid==issue_to_issue.c.child_issue_id", secondaryjoin="issue_to_issue.c.parent_issue_id==issues.c.uid", backref="children", ) def __repr__(self): return 'Issue(%s, project:%s, user:%s, title:%s)' % ( self.id, self.project.name, self.user.user, self.title ) @property def isa(self): ''' A string to allow finding out that this is an issue. ''' return 'issue' @property def mail_id(self): ''' Return a unique reprensetation of the issue as string that can be used when sending emails. ''' return '%s-ticket-%s@pagure' % (self.project.name, self.uid) @property def tags_text(self): ''' Return the list of tags in a simple text form. ''' return [tag.tag for tag in self.tags] @property def depends_text(self): ''' Return the list of issue this issue depends on in simple text. ''' return [issue.id for issue in self.children] @property def blocks_text(self): ''' Return the list of issue this issue blocks on in simple text. ''' return [issue.id for issue in self.parents] @property def user_comments(self): ''' Return user comments only, filter it from notifications ''' return [ comment for comment in self.comments if not comment.notification] def to_json(self, public=False, with_comments=True): ''' Returns a dictionary representation of the issue. ''' output = { 'id': self.id, 'title': self.title, 'content': self.content, 'status': self.status, 'date_created': self.date_created.strftime('%s'), 'closed_at': self.closed_at.strftime( '%s') if self.closed_at else None, 'user': self.user.to_json(public=public), 'private': self.private, 'tags': self.tags_text, 'depends': [str(item) for item in self.depends_text], 'blocks': [str(item) for item in self.blocks_text], 'assignee': self.assignee.to_json( public=public) if self.assignee else None, 'priority': self.priority, 'milestone': self.milestone, } comments = [] if with_comments: for comment in self.comments: comments.append(comment.to_json(public=public)) output['comments'] = comments return output class IssueToIssue(BASE): """ Stores the parent/child relationship between two issues. Table -- issue_to_issue """ __tablename__ = 'issue_to_issue' parent_issue_id = sa.Column( sa.String(32), sa.ForeignKey( 'issues.uid', ondelete='CASCADE', onupdate='CASCADE', ), primary_key=True) child_issue_id = sa.Column( sa.String(32), sa.ForeignKey( 'issues.uid', ondelete='CASCADE', onupdate='CASCADE', ), primary_key=True) class IssueComment(BASE): """ Stores the comments made on a commit/file. Table -- issue_comments """ __tablename__ = 'issue_comments' id = sa.Column(sa.Integer, primary_key=True) issue_uid = sa.Column( sa.String(32), sa.ForeignKey( 'issues.uid', ondelete='CASCADE', onupdate='CASCADE', ), index=True) comment = sa.Column( sa.Text(), nullable=False) parent_id = sa.Column( sa.Integer, sa.ForeignKey( 'issue_comments.id', onupdate='CASCADE', ), nullable=True) user_id = sa.Column( sa.Integer, sa.ForeignKey( 'users.id', onupdate='CASCADE', ), nullable=False, index=True) notification = sa.Column(sa.Boolean, default=False, nullable=False) edited_on = sa.Column(sa.DateTime, nullable=True) editor_id = sa.Column( sa.Integer, sa.ForeignKey( 'users.id', onupdate='CASCADE', ), nullable=True) date_created = sa.Column(sa.DateTime, nullable=False, default=datetime.datetime.utcnow) issue = relation( 'Issue', foreign_keys=[issue_uid], remote_side=[Issue.uid], backref=backref( 'comments', cascade="delete, delete-orphan", order_by="IssueComment.date_created" ), ) user = relation( 'User', foreign_keys=[user_id], remote_side=[User.id], backref='comment_issues') editor = relation( 'User', foreign_keys=[editor_id], remote_side=[User.id]) @property def mail_id(self): ''' Return a unique reprensetation of the issue as string that can be used when sending emails. ''' return '%s-ticket-%s-%s@pagure' % ( self.issue.project.name, self.issue.uid, self.id) @property def parent(self): ''' Return the parent, in this case the issue object. ''' return self.issue def to_json(self, public=False): ''' Returns a dictionary representation of the issue. ''' output = { 'id': self.id, 'comment': self.comment, 'parent': self.parent_id, 'date_created': self.date_created.strftime('%s'), 'user': self.user.to_json(public=public), 'edited_on': self.edited_on.strftime('%s') if self.edited_on else None, 'editor': self.editor.to_json(public=public) if self.editor_id else None, 'notification': self.notification, } return output class Tag(BASE): """ Stores the tags. Table -- tags """ __tablename__ = 'tags' tag = sa.Column(sa.String(255), primary_key=True) date_created = sa.Column(sa.DateTime, nullable=False, default=datetime.datetime.utcnow) class TagIssue(BASE): """ Stores the tag associated with an issue. Table -- tags_issues """ __tablename__ = 'tags_issues' tag = sa.Column( sa.String(255), sa.ForeignKey( 'tags.tag', ondelete='CASCADE', onupdate='CASCADE', ), primary_key=True) issue_uid = sa.Column( sa.String(32), sa.ForeignKey( 'issues.uid', ondelete='CASCADE', onupdate='CASCADE', ), primary_key=True) date_created = sa.Column(sa.DateTime, nullable=False, default=datetime.datetime.utcnow) issue = relation( 'Issue', foreign_keys=[issue_uid], remote_side=[Issue.uid], backref=backref( 'tags', cascade="delete, delete-orphan", single_parent=True) ) def __repr__(self): return 'TagIssue(issue:%s, tag:%s)' % (self.issue.id, self.tag) class TagProject(BASE): """ Stores the tag associated with a project. Table -- tags_projects """ __tablename__ = 'tags_projects' tag = sa.Column( sa.String(255), sa.ForeignKey( 'tags.tag', ondelete='CASCADE', onupdate='CASCADE', ), primary_key=True) project_id = sa.Column( sa.Integer, sa.ForeignKey( 'projects.id', ondelete='CASCADE', onupdate='CASCADE', ), primary_key=True) date_created = sa.Column(sa.DateTime, nullable=False, default=datetime.datetime.utcnow) project = relation( 'Project', foreign_keys=[project_id], remote_side=[Project.id], backref=backref( 'tags', cascade="delete, delete-orphan", single_parent=True) ) def __repr__(self): return 'TagProject(project:%s, tag:%s)' % ( self.project.fullname, self.tag) class PullRequest(BASE): """ Stores the pull requests created on a project. Table -- pull_requests """ __tablename__ = 'pull_requests' id = sa.Column(sa.Integer, primary_key=True) uid = sa.Column(sa.String(32), unique=True, nullable=False) title = sa.Column( sa.Text, nullable=False) project_id = sa.Column( sa.Integer, sa.ForeignKey( 'projects.id', ondelete='CASCADE', onupdate='CASCADE', ), primary_key=True) branch = sa.Column( sa.Text(), nullable=False) project_id_from = sa.Column( sa.Integer, sa.ForeignKey( 'projects.id', ondelete='CASCADE', onupdate='CASCADE', ), nullable=True) remote_git = sa.Column( sa.Text(), nullable=True) branch_from = sa.Column( sa.Text(), nullable=False) commit_start = sa.Column( sa.Text(), nullable=True) commit_stop = sa.Column( sa.Text(), nullable=True) initial_comment = sa.Column( sa.Text(), nullable=True) user_id = sa.Column( sa.Integer, sa.ForeignKey( 'users.id', onupdate='CASCADE', ), nullable=False, index=True) assignee_id = sa.Column( sa.Integer, sa.ForeignKey( 'users.id', onupdate='CASCADE', ), nullable=True, index=True) merge_status = sa.Column( sa.Enum( 'NO_CHANGE', 'FFORWARD', 'CONFLICTS', 'MERGE', name='merge_status_enum', ), nullable=True) status = sa.Column( sa.String(255), sa.ForeignKey( 'status_pull_requests.status', onupdate='CASCADE', ), default='Open', nullable=False) closed_by_id = sa.Column( sa.Integer, sa.ForeignKey( 'users.id', onupdate='CASCADE', ), nullable=True) closed_at = sa.Column( sa.DateTime, nullable=True) date_created = sa.Column(sa.DateTime, nullable=False, default=datetime.datetime.utcnow) updated_on = sa.Column( sa.DateTime, nullable=False, default=sa.func.now(), onupdate=sa.func.now()) __table_args__ = ( sa.CheckConstraint( 'NOT(project_id_from IS NULL AND remote_git IS NULL)', ), ) project = relation( 'Project', foreign_keys=[project_id], remote_side=[Project.id], backref=backref( 'requests', cascade="delete, delete-orphan", ), single_parent=True) project_from = relation( 'Project', foreign_keys=[project_id_from], remote_side=[Project.id]) user = relation('User', foreign_keys=[user_id], remote_side=[User.id], backref='pull_requests') assignee = relation('User', foreign_keys=[assignee_id], remote_side=[User.id], backref='assigned_requests') closed_by = relation('User', foreign_keys=[closed_by_id], remote_side=[User.id], backref='closed_requests') def __repr__(self): return 'PullRequest(%s, project:%s, user:%s, title:%s)' % ( self.id, self.project.name, self.user.user, self.title ) @property def isa(self): ''' A string to allow finding out that this is an pull-request. ''' return 'pull-request' @property def mail_id(self): ''' Return a unique reprensetation of the issue as string that can be used when sending emails. ''' return '%s-pull-request-%s@pagure' % (self.project.name, self.uid) @property def discussion(self): ''' Return the list of comments related to the pull-request itself, ie: not related to a specific commit. ''' return [ comment for comment in self.comments if not comment.commit_id ] @property def score(self): ''' Return the review score of the pull-request by checking the number of +1, -1, :thumbup: and :thumbdown: in the comment of the pull-request. This includes only the main comments not the inline ones. An user can only give one +1 and one -1. ''' positive = set() negative = set() for comment in self.discussion: for word in ['+1', ':thumbsup:']: if word in comment.comment: positive.add(comment.user_id) break for word in ['-1', ':thumbsdown:']: if word in comment.comment: negative.add(comment.user_id) break return len(positive) - len(negative) @property def remote(self): ''' Return whether the current PullRequest is a remote pull-request or not. ''' return self.remote_git is not None @property def user_comments(self): ''' Return user comments only, filter it from notifications ''' return [ comment for comment in self.comments if not comment.notification] def to_json(self, public=False, api=False, with_comments=True): ''' Returns a dictionnary representation of the pull-request. ''' output = { 'id': self.id, 'uid': self.uid, 'title': self.title, 'branch': self.branch, 'project': self.project.to_json(public=public, api=api), 'branch_from': self.branch_from, 'repo_from': self.project_from.to_json( public=public, api=api) if self.project_from else None, 'remote_git': self.remote_git, 'date_created': self.date_created.strftime('%s'), 'updated_on': self.updated_on.strftime('%s'), 'closed_at': self.closed_at.strftime( '%s') if self.closed_at else None, 'user': self.user.to_json(public=public), 'assignee': self.assignee.to_json( public=public) if self.assignee else None, 'status': self.status, 'commit_start': self.commit_start, 'commit_stop': self.commit_stop, 'closed_by': self.closed_by.to_json( public=public) if self.closed_by else None, 'initial_comment': self.initial_comment, } comments = [] if with_comments: for comment in self.comments: comments.append(comment.to_json(public=public)) output['comments'] = comments return output class PullRequestComment(BASE): """ Stores the comments made on a pull-request. Table -- pull_request_comments """ __tablename__ = 'pull_request_comments' id = sa.Column(sa.Integer, primary_key=True) pull_request_uid = sa.Column( sa.String(32), sa.ForeignKey( 'pull_requests.uid', ondelete='CASCADE', onupdate='CASCADE', ), nullable=False) commit_id = sa.Column( sa.String(40), nullable=True, index=True) user_id = sa.Column( sa.Integer, sa.ForeignKey( 'users.id', onupdate='CASCADE', ), nullable=False, index=True) filename = sa.Column( sa.Text, nullable=True) line = sa.Column( sa.Integer, nullable=True) tree_id = sa.Column( sa.String(40), nullable=True) comment = sa.Column( sa.Text(), nullable=False) parent_id = sa.Column( sa.Integer, sa.ForeignKey( 'pull_request_comments.id', onupdate='CASCADE', ), nullable=True) notification = sa.Column(sa.Boolean, default=False, nullable=False) edited_on = sa.Column(sa.DateTime, nullable=True) editor_id = sa.Column( sa.Integer, sa.ForeignKey('users.id', onupdate='CASCADE'), nullable=True) date_created = sa.Column(sa.DateTime, nullable=False, default=datetime.datetime.utcnow) user = relation('User', foreign_keys=[user_id], remote_side=[User.id], backref=backref( 'pull_request_comments', order_by="PullRequestComment.date_created")) pull_request = relation( 'PullRequest', backref=backref( 'comments', cascade="delete, delete-orphan", order_by="PullRequestComment.date_created" ), foreign_keys=[pull_request_uid], remote_side=[PullRequest.uid]) editor = relation( 'User', foreign_keys=[editor_id], remote_side=[User.id]) @property def mail_id(self): ''' Return a unique reprensetation of the issue as string that can be used when sending emails. ''' return '%s-pull-request-%s-%s@pagure' % ( self.pull_request.project.name, self.pull_request.uid, self.id) @property def parent(self): ''' Return the parent, in this case the pull_request object. ''' return self.pull_request def to_json(self, public=False): ''' Return a dict representation of the pull-request comment. ''' return { 'id': self.id, 'commit': self.commit_id, 'tree': self.tree_id, 'filename': self.filename, 'line': self.line, 'comment': self.comment, 'parent': self.parent_id, 'date_created': self.date_created.strftime('%s'), 'user': self.user.to_json(public=public), 'edited_on': self.edited_on.strftime('%s') if self.edited_on else None, 'editor': self.editor.to_json(public=public) if self.editor_id else None, 'notification': self.notification, } class PullRequestFlag(BASE): """ Stores the flags attached to a pull-request. Table -- pull_request_flags """ __tablename__ = 'pull_request_flags' id = sa.Column(sa.Integer, primary_key=True) uid = sa.Column(sa.String(32), unique=True, nullable=False) pull_request_uid = sa.Column( sa.String(32), sa.ForeignKey( 'pull_requests.uid', ondelete='CASCADE', onupdate='CASCADE', ), nullable=False) user_id = sa.Column( sa.Integer, sa.ForeignKey( 'users.id', onupdate='CASCADE', ), nullable=False, index=True) username = sa.Column( sa.Text(), nullable=False) percent = sa.Column( sa.Integer(), nullable=False) comment = sa.Column( sa.Text(), nullable=False) url = sa.Column( sa.Text(), nullable=False) date_created = sa.Column(sa.DateTime, nullable=False, default=datetime.datetime.utcnow) user = relation('User', foreign_keys=[user_id], remote_side=[User.id], backref=backref( 'pull_request_flags', order_by="PullRequestFlag.date_created")) pull_request = relation( 'PullRequest', backref=backref( 'flags', cascade="delete, delete-orphan", ), foreign_keys=[pull_request_uid], remote_side=[PullRequest.uid]) def to_json(self, public=False): ''' Returns a dictionnary representation of the pull-request. ''' output = { 'uid': self.uid, 'pull_request_uid': self.pull_request_uid, 'username': self.username, 'percent': self.percent, 'comment': self.comment, 'url': self.url, 'date_created': self.date_created.strftime('%s'), 'user': self.user.to_json(public=public), } return output class PagureGroupType(BASE): """ A list of the type a group can have definition. """ # names like "Group", "Order" and "User" are reserved words in SQL # so we set the name to something safe for SQL __tablename__ = 'pagure_group_type' group_type = sa.Column(sa.String(16), primary_key=True) created = sa.Column( sa.DateTime, nullable=False, default=datetime.datetime.utcnow) def __repr__(self): ''' Return a string representation of this object. ''' return 'GroupType: %s' % (self.group_type) class PagureGroup(BASE): """ An ultra-simple group definition. """ # names like "Group", "Order" and "User" are reserved words in SQL # so we set the name to something safe for SQL __tablename__ = 'pagure_group' id = sa.Column(sa.Integer, primary_key=True) group_name = sa.Column(sa.String(16), nullable=False, unique=True) display_name = sa.Column(sa.String(255), nullable=False, unique=True) description = sa.Column(sa.String(255), nullable=True) group_type = sa.Column( sa.String(16), sa.ForeignKey( 'pagure_group_type.group_type', ), default='user', nullable=False) user_id = sa.Column( sa.Integer, sa.ForeignKey( 'users.id', onupdate='CASCADE', ), nullable=False, index=True) created = sa.Column( sa.DateTime, nullable=False, default=datetime.datetime.utcnow) creator = relation( 'User', foreign_keys=[user_id], remote_side=[User.id], backref=backref('groups_created') ) def __repr__(self): ''' Return a string representation of this object. ''' return 'Group: %s - name %s' % (self.id, self.group_name) def to_json(self, public=False): ''' Returns a dictionnary representation of the pull-request. ''' output = { 'name': self.group_name, 'display_name': self.display_name, 'description': self.description, 'group_type': self.group_type, 'creator': self.creator.to_json(public=public), 'date_created': self.created.strftime('%s'), } return output class ProjectGroup(BASE): """ Association table linking the projects table to the pagure_group table. This allow linking projects to groups. """ __tablename__ = 'projects_groups' project_id = sa.Column( sa.Integer, sa.ForeignKey( 'projects.id', onupdate='CASCADE', ondelete='CASCADE', ), primary_key=True) group_id = sa.Column( sa.Integer, sa.ForeignKey( 'pagure_group.id', ), primary_key=True) # Constraints __table_args__ = (sa.UniqueConstraint('project_id', 'group_id'),) class Watcher(BASE): """ Stores the user of a projects. Table -- watchers """ __tablename__ = 'watchers' __table_args__ = ( sa.UniqueConstraint('project_id', 'user_id'), ) id = sa.Column(sa.Integer, primary_key=True) project_id = sa.Column( sa.Integer, sa.ForeignKey('projects.id', onupdate='CASCADE'), nullable=False) user_id = sa.Column( sa.Integer, sa.ForeignKey('users.id', onupdate='CASCADE'), nullable=False, index=True) watch = sa.Column( sa.Boolean, nullable=False) user = relation( 'User', foreign_keys=[user_id], remote_side=[User.id], backref=backref( 'watchers', cascade="delete, delete-orphan" ), ) project = relation( 'Project', foreign_keys=[project_id], remote_side=[Project.id], backref=backref( 'watchers', cascade="delete, delete-orphan", ), ) # # Class and tables specific for the API/token access # class ACL(BASE): """ Table listing all the rights a token can be given """ __tablename__ = 'acls' id = sa.Column(sa.Integer, primary_key=True) name = sa.Column(sa.String(32), unique=True, nullable=False) description = sa.Column(sa.Text(), nullable=False) created = sa.Column( sa.DateTime, nullable=False, default=datetime.datetime.utcnow) def __repr__(self): ''' Return a string representation of this object. ''' return 'ACL: %s - name %s' % (self.id, self.name) class Token(BASE): """ Table listing all the tokens per user and per project """ __tablename__ = 'tokens' id = sa.Column(sa.String(64), primary_key=True) user_id = sa.Column( sa.Integer, sa.ForeignKey( 'users.id', onupdate='CASCADE', ), nullable=False, index=True) project_id = sa.Column( sa.Integer, sa.ForeignKey( 'projects.id', onupdate='CASCADE', ), nullable=False, index=True) expiration = sa.Column( sa.DateTime, nullable=False, default=datetime.datetime.utcnow) created = sa.Column( sa.DateTime, nullable=False, default=datetime.datetime.utcnow) acls = relation( "ACL", secondary="tokens_acls", primaryjoin="tokens.c.id==tokens_acls.c.token_id", secondaryjoin="acls.c.id==tokens_acls.c.acl_id", ) user = relation( 'User', backref=backref( 'tokens', cascade="delete, delete-orphan", order_by="Token.created" ), foreign_keys=[user_id], remote_side=[User.id]) project = relation( 'Project', backref=backref( 'tokens', cascade="delete, delete-orphan", ), foreign_keys=[project_id], remote_side=[Project.id]) def __repr__(self): ''' Return a string representation of this object. ''' return 'Token: %s - name %s' % (self.id, self.expiration) @property def expired(self): ''' Returns wether a token has expired or not. ''' if datetime.datetime.utcnow().date() >= self.expiration.date(): return True else: return False @property def acls_list(self): ''' Return a list containing the name of each ACLs this token has. ''' return sorted([str(acl.name) for acl in self.acls]) @property def acls_list_pretty(self): ''' Return a list containing the description of each ACLs this token has. ''' return [acl.description for acl in sorted( self.acls, key=operator.attrgetter('name'))] class TokenAcl(BASE): """ Association table linking the tokens table to the acls table. This allow linking token to acl. """ __tablename__ = 'tokens_acls' token_id = sa.Column( sa.String(64), sa.ForeignKey( 'tokens.id', ), primary_key=True) acl_id = sa.Column( sa.Integer, sa.ForeignKey( 'acls.id', ), primary_key=True) # Constraints __table_args__ = ( sa.UniqueConstraint( 'token_id', 'acl_id'), ) # ########################################################## # These classes are only used if you're using the `local` # authentication method # ########################################################## class PagureUserVisit(BASE): """ Table storing the visits of the user. """ __tablename__ = 'pagure_user_visit' id = sa.Column(sa.Integer, primary_key=True) user_id = sa.Column( sa.Integer, sa.ForeignKey( 'users.id', ), nullable=False) visit_key = sa.Column( sa.String(40), nullable=False, unique=True, index=True) user_ip = sa.Column(sa.String(50), nullable=False) created = sa.Column( sa.DateTime, nullable=False, default=datetime.datetime.utcnow) expiry = sa.Column(sa.DateTime) class PagureUserGroup(BASE): """ Association table linking the mm_user table to the mm_group table. This allow linking users to groups. """ __tablename__ = 'pagure_user_group' user_id = sa.Column( sa.Integer, sa.ForeignKey( 'users.id', ), primary_key=True) group_id = sa.Column( sa.Integer, sa.ForeignKey( 'pagure_group.id', ), primary_key=True) # Constraints __table_args__ = ( sa.UniqueConstraint( 'user_id', 'group_id'), )