| |
@@ -8,5583 +8,3 @@
|
| |
Farhaan Bukhsh <farhaan.bukhsh@gmail.com>
|
| |
|
| |
"""
|
| |
-
|
| |
- from __future__ import unicode_literals
|
| |
-
|
| |
- # pylint: disable=too-many-branches
|
| |
- # pylint: disable=too-many-arguments
|
| |
- # pylint: disable=too-many-locals
|
| |
- # pylint: disable=too-many-statements
|
| |
- # pylint: disable=too-many-lines
|
| |
-
|
| |
-
|
| |
- try:
|
| |
- import simplejson as json
|
| |
- except ImportError: # pragma: no cover
|
| |
- import json
|
| |
-
|
| |
- import datetime
|
| |
- import fnmatch
|
| |
- import functools
|
| |
- import hashlib
|
| |
- import logging
|
| |
- import os
|
| |
- import tempfile
|
| |
- import subprocess
|
| |
- import uuid
|
| |
- import markdown
|
| |
- import werkzeug
|
| |
- from collections import Counter
|
| |
- from math import ceil
|
| |
- import copy
|
| |
-
|
| |
- import bleach
|
| |
- import redis
|
| |
- import six
|
| |
- import sqlalchemy
|
| |
- import sqlalchemy.schema
|
| |
-
|
| |
- from six.moves.urllib_parse import urlparse, urlencode, parse_qsl
|
| |
- from sqlalchemy import func
|
| |
- from sqlalchemy import asc, desc
|
| |
- from sqlalchemy.orm import aliased
|
| |
- from sqlalchemy.orm import sessionmaker
|
| |
- from sqlalchemy.orm import scoped_session
|
| |
- from flask import url_for
|
| |
-
|
| |
- import pagure.exceptions
|
| |
- import pagure.lib.git
|
| |
- import pagure.lib.git_auth
|
| |
- import pagure.lib.login
|
| |
- import pagure.lib.notify
|
| |
- import pagure.lib.plugins
|
| |
- import pagure.pfmarkdown
|
| |
- import pagure.utils
|
| |
- from pagure.config import config as pagure_config
|
| |
- from pagure.lib import model
|
| |
- from pagure.lib import tasks
|
| |
- from pagure.lib import tasks_services
|
| |
-
|
| |
-
|
| |
- REDIS = None
|
| |
- PAGURE_CI = None
|
| |
- REPOTYPES = ("main", "docs", "tickets", "requests")
|
| |
- _log = logging.getLogger(__name__)
|
| |
- # The target for hooks migrated to the Runner system, to be able to detect
|
| |
- # whether a hook was migrated without having to open and read the file
|
| |
- HOOK_DNE_TARGET = "/does/not/exist"
|
| |
-
|
| |
-
|
| |
- class Unspecified(object):
|
| |
- """ Custom None object used to indicate that the caller has not made
|
| |
- a choice for a particular argument.
|
| |
- """
|
| |
-
|
| |
- pass
|
| |
-
|
| |
-
|
| |
- def set_redis(host, port, dbname):
|
| |
- """ Set the redis connection with the specified information. """
|
| |
- global REDIS
|
| |
- pool = redis.ConnectionPool(host=host, port=port, db=dbname)
|
| |
- REDIS = redis.StrictRedis(connection_pool=pool)
|
| |
-
|
| |
-
|
| |
- def set_pagure_ci(services):
|
| |
- """ Set the list of CI services supported by this pagure instance. """
|
| |
- global PAGURE_CI
|
| |
- PAGURE_CI = services
|
| |
-
|
| |
-
|
| |
- def get_user(session, key):
|
| |
- """ Searches for a user in the database for a given username or email.
|
| |
- """
|
| |
- user_obj = search_user(session, username=key)
|
| |
- if not user_obj:
|
| |
- user_obj = search_user(session, email=key)
|
| |
-
|
| |
- if not user_obj:
|
| |
- raise pagure.exceptions.PagureException('No user "%s" found' % key)
|
| |
-
|
| |
- return user_obj
|
| |
-
|
| |
-
|
| |
- def get_user_by_id(session, userid):
|
| |
- """ Searches for a user in the database for a given username or email.
|
| |
- """
|
| |
- query = session.query(model.User).filter(model.User.id == userid)
|
| |
-
|
| |
- return query.first()
|
| |
-
|
| |
-
|
| |
- SESSIONMAKER = None
|
| |
-
|
| |
-
|
| |
- def create_session(db_url=None, debug=False, pool_recycle=3600):
|
| |
- """ Create the Session object to use to query the database.
|
| |
-
|
| |
- :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: <engine>://<user>:<password>@<host>/<dbname>
|
| |
- :kwarg debug: a boolean specifying whether we should have the verbose
|
| |
- output of sqlalchemy or not.
|
| |
- :return a Session that can be used to query the database.
|
| |
-
|
| |
- """
|
| |
- global SESSIONMAKER
|
| |
-
|
| |
- if SESSIONMAKER is None or (
|
| |
- db_url and db_url != ("%s" % SESSIONMAKER.kw["bind"].engine.url)
|
| |
- ):
|
| |
- if db_url is None:
|
| |
- raise ValueError("First call to create_session needs db_url")
|
| |
- if db_url.startswith("postgres"): # pragma: no cover
|
| |
- engine = sqlalchemy.create_engine(
|
| |
- db_url,
|
| |
- echo=debug,
|
| |
- pool_recycle=pool_recycle,
|
| |
- client_encoding="utf8",
|
| |
- )
|
| |
- else: # pragma: no cover
|
| |
- engine = sqlalchemy.create_engine(
|
| |
- db_url, echo=debug, pool_recycle=pool_recycle
|
| |
- )
|
| |
-
|
| |
- 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")
|
| |
-
|
| |
- sqlalchemy.event.listen(engine, "connect", _fk_pragma_on_connect)
|
| |
- SESSIONMAKER = sessionmaker(bind=engine)
|
| |
-
|
| |
- scopedsession = scoped_session(SESSIONMAKER)
|
| |
- model.BASE.metadata.bind = scopedsession
|
| |
- return scopedsession
|
| |
-
|
| |
-
|
| |
- def get_next_id(session, projectid):
|
| |
- """ Returns the next identifier of a project ticket or pull-request
|
| |
- based on the identifier already in the database.
|
| |
- """
|
| |
- query1 = session.query(func.max(model.Issue.id)).filter(
|
| |
- model.Issue.project_id == projectid
|
| |
- )
|
| |
-
|
| |
- query2 = session.query(func.max(model.PullRequest.id)).filter(
|
| |
- model.PullRequest.project_id == projectid
|
| |
- )
|
| |
-
|
| |
- ids = [el[0] for el in query1.union(query2).all() if el[0] is not None]
|
| |
- nid = 0
|
| |
- if ids:
|
| |
- nid = max(ids)
|
| |
-
|
| |
- return nid + 1
|
| |
-
|
| |
-
|
| |
- def search_user(session, username=None, email=None, token=None, pattern=None):
|
| |
- """ Searches the database for the user or users matching the given
|
| |
- criterias.
|
| |
-
|
| |
- :arg session: the session to use to connect to the database.
|
| |
- :kwarg username: the username of the user to look for.
|
| |
- :type username: string or None
|
| |
- :kwarg email: the email or one of the email of the user to look for
|
| |
- :type email: string or None
|
| |
- :kwarg token: the token of the user to look for
|
| |
- :type token: string or None
|
| |
- :kwarg pattern: a pattern to search the users with.
|
| |
- :type pattern: string or None
|
| |
- :return: A single User object if any of username, email or token is
|
| |
- specified, a list of User objects otherwise.
|
| |
- :rtype: User or [User]
|
| |
-
|
| |
- """
|
| |
- query = session.query(model.User).order_by(model.User.user)
|
| |
-
|
| |
- if username is not None:
|
| |
- query = query.filter(model.User.user == username)
|
| |
-
|
| |
- if email is not None:
|
| |
- query = query.filter(model.UserEmail.user_id == model.User.id).filter(
|
| |
- model.UserEmail.email == email
|
| |
- )
|
| |
-
|
| |
- if token is not None:
|
| |
- query = query.filter(model.User.token == token)
|
| |
-
|
| |
- if pattern:
|
| |
- pattern = pattern.replace("*", "%")
|
| |
- query = query.filter(model.User.user.like(pattern))
|
| |
-
|
| |
- if any([username, email, token]):
|
| |
- output = query.first()
|
| |
- else:
|
| |
- output = query.all()
|
| |
-
|
| |
- return output
|
| |
-
|
| |
-
|
| |
- def is_valid_ssh_key(key, fp_hash="SHA256"):
|
| |
- """ Validates the ssh key using ssh-keygen. """
|
| |
- key = key.strip()
|
| |
- if not key:
|
| |
- return None
|
| |
- with tempfile.TemporaryFile() as f:
|
| |
- f.write(key.encode("utf-8"))
|
| |
- f.seek(0)
|
| |
- cmd = ["/usr/bin/ssh-keygen", "-l", "-f", "/dev/stdin", "-E", fp_hash]
|
| |
- proc = subprocess.Popen(
|
| |
- cmd, stdin=f, stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
| |
- )
|
| |
- stdout, stderr = proc.communicate()
|
| |
- if proc.returncode != 0:
|
| |
- return False
|
| |
- stdout = stdout.decode("utf-8")
|
| |
-
|
| |
- return stdout
|
| |
-
|
| |
-
|
| |
- def are_valid_ssh_keys(keys):
|
| |
- """ Checks if all the ssh keys are valid or not. """
|
| |
- return all(
|
| |
- [is_valid_ssh_key(key) is not False for key in keys.split("\n")]
|
| |
- )
|
| |
-
|
| |
-
|
| |
- def find_ssh_key(session, search_key, username):
|
| |
- """ Finds and returns SSHKey matching the requested search_key.
|
| |
-
|
| |
- Args:
|
| |
- session: database session
|
| |
- search_key (string): The SSH fingerprint we are requested to look up
|
| |
- username (string or None): If this is provided, the key is looked up
|
| |
- to belong to the requested user.
|
| |
- """
|
| |
- query = session.query(model.SSHKey).filter(
|
| |
- model.SSHKey.ssh_search_key == search_key
|
| |
- )
|
| |
-
|
| |
- if username:
|
| |
- userowner = (
|
| |
- session.query(model.User.id)
|
| |
- .filter(model.User.user == username)
|
| |
- .subquery()
|
| |
- )
|
| |
- query = query.filter(model.SSHKey.user_id == userowner)
|
| |
-
|
| |
- try:
|
| |
- return query.one()
|
| |
- except sqlalchemy.orm.exc.NoResultFound:
|
| |
- return None
|
| |
-
|
| |
-
|
| |
- def create_deploykeys_ssh_keys_on_disk(project, gitolite_keydir):
|
| |
- """ Create the ssh keys for the projects' deploy keys on the key dir.
|
| |
-
|
| |
- This method does NOT support multiple ssh keys per deploy key.
|
| |
- """
|
| |
- if not gitolite_keydir:
|
| |
- # Nothing to do here, move right along
|
| |
- return
|
| |
-
|
| |
- # First remove deploykeys that no longer exist
|
| |
- keyfiles = [
|
| |
- "deploykey_%s_%s.pub"
|
| |
- % (werkzeug.secure_filename(project.fullname), key.id)
|
| |
- for key in project.deploykeys
|
| |
- ]
|
| |
-
|
| |
- project_key_dir = os.path.join(
|
| |
- gitolite_keydir, "deploykeys", project.fullname
|
| |
- )
|
| |
- if not os.path.exists(project_key_dir):
|
| |
- os.makedirs(project_key_dir)
|
| |
-
|
| |
- for keyfile in os.listdir(project_key_dir):
|
| |
- if keyfile not in keyfiles:
|
| |
- # This key is no longer in the project. Remove it.
|
| |
- os.remove(os.path.join(project_key_dir, keyfile))
|
| |
-
|
| |
- for deploykey in project.deploykeys:
|
| |
- # See the comment in lib/git.py:write_gitolite_acls about why this
|
| |
- # name for a file is sane and does not inject a new security risk.
|
| |
- keyfile = "deploykey_%s_%s.pub" % (
|
| |
- werkzeug.secure_filename(project.fullname),
|
| |
- deploykey.id,
|
| |
- )
|
| |
- if not os.path.exists(os.path.join(project_key_dir, keyfile)):
|
| |
- # We only take the very first key - deploykeys must be single keys
|
| |
- key = deploykey.public_ssh_key.split("\n")[0]
|
| |
- if not key:
|
| |
- continue
|
| |
- if not is_valid_ssh_key(key):
|
| |
- continue
|
| |
- with open(os.path.join(project_key_dir, keyfile), "w") as f:
|
| |
- f.write(deploykey.public_ssh_key)
|
| |
-
|
| |
-
|
| |
- def create_user_ssh_keys_on_disk(user, gitolite_keydir):
|
| |
- """ Create the ssh keys for the user on the specific folder.
|
| |
-
|
| |
- This is the method allowing to have multiple ssh keys per user.
|
| |
- """
|
| |
- if gitolite_keydir:
|
| |
- # First remove any old keyfiles for the user
|
| |
- # Assumption: we populated the keydir. This means that files
|
| |
- # will be in 0/<username>.pub, ..., and not in any deeper
|
| |
- # directory structures. Also, this means that if a user
|
| |
- # had 5 lines, they will be up to at most keys_4/<username>.pub,
|
| |
- # meaning that if a user is not in keys_<i>/<username>.pub, with
|
| |
- # i being any integer, the user is most certainly not in
|
| |
- # keys_<i+1>/<username>.pub.
|
| |
- i = 0
|
| |
- keyline_file = os.path.join(
|
| |
- gitolite_keydir, "keys_%i" % i, "%s.pub" % user.user
|
| |
- )
|
| |
- while os.path.exists(keyline_file):
|
| |
- os.unlink(keyline_file)
|
| |
- i += 1
|
| |
- keyline_file = os.path.join(
|
| |
- gitolite_keydir, "keys_%i" % i, "%s.pub" % user.user
|
| |
- )
|
| |
-
|
| |
- if not user.sshkeys:
|
| |
- return
|
| |
-
|
| |
- # Now let's create new keyfiles for the user
|
| |
- i = 0
|
| |
- for key in user.sshkeys:
|
| |
- if not is_valid_ssh_key(key.public_ssh_key):
|
| |
- continue
|
| |
- keyline_dir = os.path.join(gitolite_keydir, "keys_%i" % i)
|
| |
- if not os.path.exists(keyline_dir):
|
| |
- os.mkdir(keyline_dir)
|
| |
- keyfile = os.path.join(keyline_dir, "%s.pub" % user.user)
|
| |
- with open(keyfile, "w") as stream:
|
| |
- stream.write(key.public_ssh_key.strip())
|
| |
- i += 1
|
| |
-
|
| |
-
|
| |
- def add_issue_comment(
|
| |
- session,
|
| |
- issue,
|
| |
- comment,
|
| |
- user,
|
| |
- notify=True,
|
| |
- date_created=None,
|
| |
- notification=False,
|
| |
- ):
|
| |
- """ Add a comment to an issue. """
|
| |
- user_obj = get_user(session, user)
|
| |
-
|
| |
- issue_comment = model.IssueComment(
|
| |
- issue_uid=issue.uid,
|
| |
- comment=comment,
|
| |
- user_id=user_obj.id,
|
| |
- date_created=date_created,
|
| |
- notification=notification,
|
| |
- )
|
| |
- issue.last_updated = datetime.datetime.utcnow()
|
| |
- session.add(issue)
|
| |
- session.add(issue_comment)
|
| |
- # Make sure we won't have SQLAlchemy error before we continue
|
| |
- session.commit()
|
| |
-
|
| |
- pagure.lib.git.update_git(issue, repo=issue.project)
|
| |
-
|
| |
- if not notification:
|
| |
- log_action(session, "commented", issue, user_obj)
|
| |
-
|
| |
- if notify:
|
| |
- pagure.lib.notify.notify_new_comment(issue_comment, user=user_obj)
|
| |
-
|
| |
- if not issue.private:
|
| |
- pagure.lib.notify.log(
|
| |
- issue.project,
|
| |
- topic="issue.comment.added",
|
| |
- msg=dict(
|
| |
- issue=issue.to_json(public=True),
|
| |
- project=issue.project.to_json(public=True),
|
| |
- agent=user_obj.username,
|
| |
- ),
|
| |
- redis=REDIS,
|
| |
- )
|
| |
-
|
| |
- # TODO: we should notify the SSE server even on update of the ticket
|
| |
- # via git push to the ticket repo (the only case where notify=False
|
| |
- # basically), but this causes problem with some of our markdown extension
|
| |
- # so until we figure this out, we won't do live-refresh
|
| |
- if REDIS and notify:
|
| |
- if issue.private:
|
| |
- REDIS.publish(
|
| |
- "pagure.%s" % issue.uid,
|
| |
- json.dumps(
|
| |
- {"issue": "private", "comment_id": issue_comment.id}
|
| |
- ),
|
| |
- )
|
| |
- else:
|
| |
- REDIS.publish(
|
| |
- "pagure.%s" % issue.uid,
|
| |
- json.dumps(
|
| |
- {
|
| |
- "comment_id": issue_comment.id,
|
| |
- "issue_id": issue.id,
|
| |
- "project": issue.project.fullname,
|
| |
- "comment_added": text2markdown(issue_comment.comment),
|
| |
- "comment_user": issue_comment.user.user,
|
| |
- "avatar_url": avatar_url_from_email(
|
| |
- issue_comment.user.default_email, size=16
|
| |
- ),
|
| |
- "comment_date": issue_comment.date_created.strftime(
|
| |
- "%Y-%m-%d %H:%M:%S"
|
| |
- ),
|
| |
- "notification": notification,
|
| |
- }
|
| |
- ),
|
| |
- )
|
| |
-
|
| |
- return "Comment added"
|
| |
-
|
| |
-
|
| |
- def add_tag_obj(session, obj, tags, user):
|
| |
- """ Add a tag to an object (either an issue or a project). """
|
| |
- user_obj = get_user(session, user)
|
| |
-
|
| |
- if isinstance(tags, six.string_types):
|
| |
- tags = [tags]
|
| |
-
|
| |
- added_tags = []
|
| |
- added_tags_color = []
|
| |
- for objtag in tags:
|
| |
- objtag = objtag.strip()
|
| |
- known = False
|
| |
- for tagobj in obj.tags:
|
| |
- if tagobj.tag == objtag:
|
| |
- known = True
|
| |
-
|
| |
- if known:
|
| |
- continue
|
| |
-
|
| |
- if obj.isa == "project":
|
| |
- tagobj = get_tag(session, objtag)
|
| |
- if not tagobj:
|
| |
- tagobj = model.Tag(tag=objtag)
|
| |
-
|
| |
- session.add(tagobj)
|
| |
- session.flush()
|
| |
-
|
| |
- dbobjtag = model.TagProject(project_id=obj.id, tag=tagobj.tag)
|
| |
-
|
| |
- else:
|
| |
- tagobj = get_colored_tag(session, objtag, obj.project.id)
|
| |
- if not tagobj:
|
| |
- tagobj = model.TagColored(
|
| |
- tag=objtag, project_id=obj.project.id
|
| |
- )
|
| |
- session.add(tagobj)
|
| |
- session.flush()
|
| |
-
|
| |
- if obj.isa == "issue":
|
| |
- dbobjtag = model.TagIssueColored(
|
| |
- issue_uid=obj.uid, tag_id=tagobj.id
|
| |
- )
|
| |
- else:
|
| |
- dbobjtag = model.TagPullRequest(
|
| |
- request_uid=obj.uid, tag_id=tagobj.id
|
| |
- )
|
| |
-
|
| |
- added_tags_color.append(tagobj.tag_color)
|
| |
-
|
| |
- session.add(dbobjtag)
|
| |
- # Make sure we won't have SQLAlchemy error before we continue
|
| |
- session.flush()
|
| |
- added_tags.append(tagobj.tag)
|
| |
-
|
| |
- if isinstance(obj, model.Issue):
|
| |
- pagure.lib.git.update_git(obj, repo=obj.project)
|
| |
-
|
| |
- if not obj.private:
|
| |
- pagure.lib.notify.log(
|
| |
- obj.project,
|
| |
- topic="issue.tag.added",
|
| |
- msg=dict(
|
| |
- issue=obj.to_json(public=True),
|
| |
- project=obj.project.to_json(public=True),
|
| |
- tags=added_tags,
|
| |
- agent=user_obj.username,
|
| |
- ),
|
| |
- redis=REDIS,
|
| |
- )
|
| |
-
|
| |
- # Send notification for the event-source server
|
| |
- if REDIS and not obj.project.private:
|
| |
- REDIS.publish(
|
| |
- "pagure.%s" % obj.uid,
|
| |
- json.dumps(
|
| |
- {
|
| |
- "added_tags": added_tags,
|
| |
- "added_tags_color": added_tags_color,
|
| |
- }
|
| |
- ),
|
| |
- )
|
| |
- elif isinstance(obj, model.PullRequest):
|
| |
- pagure.lib.git.update_git(obj, repo=obj.project)
|
| |
-
|
| |
- if not obj.private:
|
| |
- pagure.lib.notify.log(
|
| |
- obj.project,
|
| |
- topic="pull-request.tag.added",
|
| |
- msg=dict(
|
| |
- pull_request=obj.to_json(public=True),
|
| |
- project=obj.project.to_json(public=True),
|
| |
- tags=added_tags,
|
| |
- agent=user_obj.username,
|
| |
- ),
|
| |
- redis=REDIS,
|
| |
- )
|
| |
-
|
| |
- # Send notification for the event-source server
|
| |
- if REDIS and not obj.project.private:
|
| |
- REDIS.publish(
|
| |
- "pagure.%s" % obj.uid,
|
| |
- json.dumps(
|
| |
- {
|
| |
- "added_tags": added_tags,
|
| |
- "added_tags_color": added_tags_color,
|
| |
- }
|
| |
- ),
|
| |
- )
|
| |
-
|
| |
- if added_tags:
|
| |
- return "%s tagged with: %s" % (
|
| |
- obj.isa.capitalize(),
|
| |
- ", ".join(added_tags),
|
| |
- )
|
| |
- else:
|
| |
- return "Nothing to add"
|
| |
-
|
| |
-
|
| |
- def add_issue_assignee(session, issue, assignee, user, notify=True):
|
| |
- """ Add an assignee to an issue, in other words, assigned an issue. """
|
| |
- user_obj = get_user(session, user)
|
| |
-
|
| |
- old_assignee = issue.assignee
|
| |
-
|
| |
- if not assignee and issue.assignee is not None:
|
| |
- issue.assignee_id = None
|
| |
- issue.last_updated = datetime.datetime.utcnow()
|
| |
- session.add(issue)
|
| |
- session.commit()
|
| |
- pagure.lib.git.update_git(issue, repo=issue.project)
|
| |
-
|
| |
- if notify:
|
| |
- pagure.lib.notify.notify_assigned_issue(issue, None, user_obj)
|
| |
-
|
| |
- if not issue.private:
|
| |
- pagure.lib.notify.log(
|
| |
- issue.project,
|
| |
- topic="issue.assigned.reset",
|
| |
- msg=dict(
|
| |
- issue=issue.to_json(public=True),
|
| |
- project=issue.project.to_json(public=True),
|
| |
- agent=user_obj.username,
|
| |
- ),
|
| |
- redis=REDIS,
|
| |
- )
|
| |
-
|
| |
- # Send notification for the event-source server
|
| |
- if REDIS and not issue.project.private:
|
| |
- REDIS.publish(
|
| |
- "pagure.%s" % issue.uid, json.dumps({"unassigned": "-"})
|
| |
- )
|
| |
-
|
| |
- return "Assignee reset"
|
| |
- elif not assignee and issue.assignee is None:
|
| |
- return
|
| |
-
|
| |
- old_assignee = issue.assignee
|
| |
- # Validate the assignee
|
| |
- assignee_obj = get_user(session, assignee)
|
| |
-
|
| |
- if issue.assignee_id != assignee_obj.id:
|
| |
- issue.assignee_id = assignee_obj.id
|
| |
- session.add(issue)
|
| |
- session.commit()
|
| |
- pagure.lib.git.update_git(issue, repo=issue.project)
|
| |
-
|
| |
- if notify:
|
| |
- pagure.lib.notify.notify_assigned_issue(
|
| |
- issue, assignee_obj, user_obj
|
| |
- )
|
| |
-
|
| |
- if not issue.private:
|
| |
- pagure.lib.notify.log(
|
| |
- issue.project,
|
| |
- topic="issue.assigned.added",
|
| |
- msg=dict(
|
| |
- issue=issue.to_json(public=True),
|
| |
- project=issue.project.to_json(public=True),
|
| |
- agent=user_obj.username,
|
| |
- ),
|
| |
- redis=REDIS,
|
| |
- )
|
| |
- issue.last_updated = datetime.datetime.utcnow()
|
| |
-
|
| |
- # Send notification for the event-source server
|
| |
- if REDIS and not issue.project.private:
|
| |
- REDIS.publish(
|
| |
- "pagure.%s" % issue.uid,
|
| |
- json.dumps({"assigned": assignee_obj.to_json(public=True)}),
|
| |
- )
|
| |
-
|
| |
- output = "Issue assigned to %s" % assignee
|
| |
- if old_assignee:
|
| |
- output += " (was: %s)" % old_assignee.username
|
| |
- return output
|
| |
-
|
| |
-
|
| |
- def add_pull_request_assignee(session, request, assignee, user):
|
| |
- """ Add an assignee to a request, in other words, assigned an issue. """
|
| |
- get_user(session, assignee)
|
| |
- user_obj = get_user(session, user)
|
| |
-
|
| |
- if assignee is None and request.assignee is not None:
|
| |
- request.assignee_id = None
|
| |
- request.last_updated = datetime.datetime.utcnow()
|
| |
- session.add(request)
|
| |
- session.commit()
|
| |
- pagure.lib.git.update_git(request, repo=request.project)
|
| |
-
|
| |
- pagure.lib.notify.notify_assigned_request(request, None, user_obj)
|
| |
-
|
| |
- pagure.lib.notify.log(
|
| |
- request.project,
|
| |
- topic="request.assigned.reset",
|
| |
- msg=dict(
|
| |
- request=request.to_json(public=True),
|
| |
- project=request.project.to_json(public=True),
|
| |
- agent=user_obj.username,
|
| |
- ),
|
| |
- redis=REDIS,
|
| |
- )
|
| |
-
|
| |
- return "Request reset"
|
| |
- elif assignee is None and request.assignee is None:
|
| |
- return
|
| |
-
|
| |
- # Validate the assignee
|
| |
- assignee_obj = get_user(session, assignee)
|
| |
-
|
| |
- if request.assignee_id != assignee_obj.id:
|
| |
- request.assignee_id = assignee_obj.id
|
| |
- request.last_updated = datetime.datetime.utcnow()
|
| |
- session.add(request)
|
| |
- session.flush()
|
| |
- pagure.lib.git.update_git(request, repo=request.project)
|
| |
-
|
| |
- pagure.lib.notify.notify_assigned_request(
|
| |
- request, assignee_obj, user_obj
|
| |
- )
|
| |
-
|
| |
- pagure.lib.notify.log(
|
| |
- request.project,
|
| |
- topic="request.assigned.added",
|
| |
- msg=dict(
|
| |
- request=request.to_json(public=True),
|
| |
- project=request.project.to_json(public=True),
|
| |
- agent=user_obj.username,
|
| |
- ),
|
| |
- redis=REDIS,
|
| |
- )
|
| |
-
|
| |
- return "Request assigned"
|
| |
-
|
| |
-
|
| |
- def add_issue_dependency(session, issue, issue_blocked, user):
|
| |
- """ Add a dependency between two issues. """
|
| |
- user_obj = get_user(session, user)
|
| |
-
|
| |
- if issue.uid == issue_blocked.uid:
|
| |
- raise pagure.exceptions.PagureException(
|
| |
- "An issue cannot depend on itself"
|
| |
- )
|
| |
-
|
| |
- if issue_blocked not in issue.children:
|
| |
- i2i = model.IssueToIssue(
|
| |
- parent_issue_id=issue.uid, child_issue_id=issue_blocked.uid
|
| |
- )
|
| |
- session.add(i2i)
|
| |
- # Make sure we won't have SQLAlchemy error before we continue
|
| |
- session.flush()
|
| |
- pagure.lib.git.update_git(issue, repo=issue.project)
|
| |
- pagure.lib.git.update_git(issue_blocked, repo=issue_blocked.project)
|
| |
-
|
| |
- if not issue.private:
|
| |
- pagure.lib.notify.log(
|
| |
- issue.project,
|
| |
- topic="issue.dependency.added",
|
| |
- msg=dict(
|
| |
- issue=issue.to_json(public=True),
|
| |
- project=issue.project.to_json(public=True),
|
| |
- added_dependency=issue_blocked.id,
|
| |
- agent=user_obj.username,
|
| |
- ),
|
| |
- redis=REDIS,
|
| |
- )
|
| |
-
|
| |
- # Send notification for the event-source server
|
| |
- if REDIS and not issue.project.private:
|
| |
- REDIS.publish(
|
| |
- "pagure.%s" % issue.uid,
|
| |
- json.dumps(
|
| |
- {
|
| |
- "added_dependency": issue_blocked.id,
|
| |
- "issue_uid": issue.uid,
|
| |
- "type": "children",
|
| |
- }
|
| |
- ),
|
| |
- )
|
| |
- REDIS.publish(
|
| |
- "pagure.%s" % issue_blocked.uid,
|
| |
- json.dumps(
|
| |
- {
|
| |
- "added_dependency": issue.id,
|
| |
- "issue_uid": issue_blocked.uid,
|
| |
- "type": "parent",
|
| |
- }
|
| |
- ),
|
| |
- )
|
| |
-
|
| |
- return "Issue marked as depending on: #%s" % issue_blocked.id
|
| |
-
|
| |
-
|
| |
- def remove_issue_dependency(session, issue, issue_blocked, user):
|
| |
- """ Remove a dependency between two issues. """
|
| |
- user_obj = get_user(session, user)
|
| |
-
|
| |
- if issue.uid == issue_blocked.uid:
|
| |
- raise pagure.exceptions.PagureException(
|
| |
- "An issue cannot depend on itself"
|
| |
- )
|
| |
-
|
| |
- if issue_blocked in issue.parents:
|
| |
- parent_del = []
|
| |
- for parent in issue.parents:
|
| |
- if parent.uid == issue_blocked.uid:
|
| |
- parent_del.append(parent.id)
|
| |
- issue.parents.remove(parent)
|
| |
-
|
| |
- # Make sure we won't have SQLAlchemy error before we continue
|
| |
- session.flush()
|
| |
- pagure.lib.git.update_git(issue, repo=issue.project)
|
| |
- pagure.lib.git.update_git(issue_blocked, repo=issue_blocked.project)
|
| |
-
|
| |
- if not issue.private:
|
| |
- pagure.lib.notify.log(
|
| |
- issue.project,
|
| |
- topic="issue.dependency.removed",
|
| |
- msg=dict(
|
| |
- issue=issue.to_json(public=True),
|
| |
- project=issue.project.to_json(public=True),
|
| |
- removed_dependency=parent_del,
|
| |
- agent=user_obj.username,
|
| |
- ),
|
| |
- redis=REDIS,
|
| |
- )
|
| |
-
|
| |
- # Send notification for the event-source server
|
| |
- if REDIS and not issue.project.private:
|
| |
- REDIS.publish(
|
| |
- "pagure.%s" % issue.uid,
|
| |
- json.dumps(
|
| |
- {
|
| |
- "removed_dependency": parent_del,
|
| |
- "issue_uid": issue.uid,
|
| |
- "type": "children",
|
| |
- }
|
| |
- ),
|
| |
- )
|
| |
- REDIS.publish(
|
| |
- "pagure.%s" % issue_blocked.uid,
|
| |
- json.dumps(
|
| |
- {
|
| |
- "removed_dependency": issue.id,
|
| |
- "issue_uid": issue_blocked.uid,
|
| |
- "type": "parent",
|
| |
- }
|
| |
- ),
|
| |
- )
|
| |
-
|
| |
- return "Issue **un**marked as depending on: #%s" % " #".join(
|
| |
- [("%s" % id) for id in parent_del]
|
| |
- )
|
| |
-
|
| |
-
|
| |
- def remove_tags(session, project, tags, user):
|
| |
- """ Removes the specified tag of a project. """
|
| |
- user_obj = get_user(session, user)
|
| |
-
|
| |
- if not isinstance(tags, list):
|
| |
- tags = [tags]
|
| |
-
|
| |
- issues = search_issues(session, project, closed=False, tags=tags)
|
| |
- issues.extend(search_issues(session, project, closed=True, tags=tags))
|
| |
-
|
| |
- msgs = []
|
| |
- removed_tags = []
|
| |
- tag_found = False
|
| |
- for tag in tags:
|
| |
- tagobj = get_colored_tag(session, tag, project.id)
|
| |
- if tagobj:
|
| |
- tag_found = True
|
| |
- removed_tags.append(tag)
|
| |
- msgs.append("Tag: %s has been deleted" % tag)
|
| |
- session.delete(tagobj)
|
| |
-
|
| |
- if not tag_found:
|
| |
- raise pagure.exceptions.PagureException(
|
| |
- "Tags not found: %s" % ", ".join(tags)
|
| |
- )
|
| |
-
|
| |
- for issue in issues:
|
| |
- for issue_tag in issue.tags:
|
| |
- if issue_tag.tag in tags:
|
| |
- tag = issue_tag.tag
|
| |
- session.delete(issue_tag)
|
| |
- pagure.lib.git.update_git(issue, repo=issue.project)
|
| |
-
|
| |
- pagure.lib.notify.log(
|
| |
- project,
|
| |
- topic="project.tag.removed",
|
| |
- msg=dict(
|
| |
- project=project.to_json(public=True),
|
| |
- tags=removed_tags,
|
| |
- agent=user_obj.username,
|
| |
- ),
|
| |
- redis=REDIS,
|
| |
- )
|
| |
-
|
| |
- return msgs
|
| |
-
|
| |
-
|
| |
- def remove_tags_obj(session, obj, tags, user):
|
| |
- """ Removes the specified tag(s) of a given object. """
|
| |
- user_obj = get_user(session, user)
|
| |
-
|
| |
- if isinstance(tags, six.string_types):
|
| |
- tags = [tags]
|
| |
-
|
| |
- removed_tags = []
|
| |
- if obj.isa == "project":
|
| |
- for objtag in obj.tags:
|
| |
- if objtag.tag in tags:
|
| |
- tag = objtag.tag
|
| |
- removed_tags.append(tag)
|
| |
- session.delete(objtag)
|
| |
- elif obj.isa == "issue":
|
| |
- for objtag in obj.tags_issues_colored:
|
| |
- if objtag.tag.tag in tags:
|
| |
- tag = objtag.tag.tag
|
| |
- removed_tags.append(tag)
|
| |
- session.delete(objtag)
|
| |
- elif obj.isa == "pull-request":
|
| |
- for objtag in obj.tags_pr_colored:
|
| |
- if objtag.tag.tag in tags:
|
| |
- tag = objtag.tag.tag
|
| |
- removed_tags.append(tag)
|
| |
- session.delete(objtag)
|
| |
-
|
| |
- if isinstance(obj, model.Issue):
|
| |
- pagure.lib.git.update_git(obj, repo=obj.project)
|
| |
-
|
| |
- pagure.lib.notify.log(
|
| |
- obj.project,
|
| |
- topic="issue.tag.removed",
|
| |
- msg=dict(
|
| |
- issue=obj.to_json(public=True),
|
| |
- project=obj.project.to_json(public=True),
|
| |
- tags=removed_tags,
|
| |
- agent=user_obj.username,
|
| |
- ),
|
| |
- redis=REDIS,
|
| |
- )
|
| |
-
|
| |
- # Send notification for the event-source server
|
| |
- if REDIS and not obj.project.private:
|
| |
- REDIS.publish(
|
| |
- "pagure.%s" % obj.uid,
|
| |
- json.dumps({"removed_tags": removed_tags}),
|
| |
- )
|
| |
- elif isinstance(obj, model.PullRequest):
|
| |
- pagure.lib.git.update_git(obj, repo=obj.project)
|
| |
-
|
| |
- pagure.lib.notify.log(
|
| |
- obj.project,
|
| |
- topic="pull-request.tag.removed",
|
| |
- msg=dict(
|
| |
- pull_request=obj.to_json(public=True),
|
| |
- project=obj.project.to_json(public=True),
|
| |
- tags=removed_tags,
|
| |
- agent=user_obj.username,
|
| |
- ),
|
| |
- redis=REDIS,
|
| |
- )
|
| |
-
|
| |
- # Send notification for the event-source server
|
| |
- if REDIS and not obj.project.private:
|
| |
- REDIS.publish(
|
| |
- "pagure.%s" % obj.uid,
|
| |
- json.dumps({"removed_tags": removed_tags}),
|
| |
- )
|
| |
-
|
| |
- return "%s **un**tagged with: %s" % (
|
| |
- obj.isa.capitalize(),
|
| |
- ", ".join(removed_tags),
|
| |
- )
|
| |
-
|
| |
-
|
| |
- def edit_issue_tags(
|
| |
- session,
|
| |
- project,
|
| |
- old_tag,
|
| |
- new_tag,
|
| |
- new_tag_description,
|
| |
- new_tag_color,
|
| |
- user,
|
| |
- ):
|
| |
- """ Removes the specified tag of a project. """
|
| |
- user_obj = get_user(session, user)
|
| |
- old_tag_name = old_tag
|
| |
-
|
| |
- if not isinstance(old_tag, model.TagColored):
|
| |
- old_tag = get_colored_tag(session, old_tag_name, project.id)
|
| |
-
|
| |
- if not old_tag:
|
| |
- raise pagure.exceptions.PagureException(
|
| |
- 'No tag "%s" found related to this project' % (old_tag_name)
|
| |
- )
|
| |
-
|
| |
- old_tag_name = old_tag.tag
|
| |
- old_tag_description = old_tag.tag_description
|
| |
- old_tag_color = old_tag.tag_color
|
| |
-
|
| |
- # check for change
|
| |
- no_change_in_tag = (
|
| |
- old_tag.tag == new_tag
|
| |
- and old_tag_description == new_tag_description
|
| |
- and old_tag_color == new_tag_color
|
| |
- )
|
| |
- if no_change_in_tag:
|
| |
- raise pagure.exceptions.PagureException(
|
| |
- 'No change. Old tag "%s(%s)[%s]" is the same as '
|
| |
- 'new tag "%s(%s)[%s]"'
|
| |
- % (
|
| |
- old_tag,
|
| |
- old_tag_description,
|
| |
- old_tag_color,
|
| |
- new_tag,
|
| |
- new_tag_description,
|
| |
- new_tag_color,
|
| |
- )
|
| |
- )
|
| |
- elif old_tag.tag != new_tag:
|
| |
- # Check if new tag already exists
|
| |
- existing_tag = get_colored_tag(session, new_tag, project.id)
|
| |
- if existing_tag:
|
| |
- raise pagure.exceptions.PagureException(
|
| |
- "Can not rename a tag to an existing tag name: %s" % new_tag
|
| |
- )
|
| |
-
|
| |
- session.query(model.TagColored).filter(
|
| |
- model.TagColored.tag == old_tag.tag
|
| |
- ).filter(model.TagColored.project_id == project.id).update(
|
| |
- {
|
| |
- model.TagColored.tag: new_tag,
|
| |
- model.TagColored.tag_description: new_tag_description,
|
| |
- model.TagColored.tag_color: new_tag_color,
|
| |
- }
|
| |
- )
|
| |
-
|
| |
- issues = (
|
| |
- session.query(model.Issue)
|
| |
- .filter(model.TagIssueColored.tag_id == old_tag.id)
|
| |
- .filter(model.TagIssueColored.issue_uid == model.Issue.uid)
|
| |
- .all()
|
| |
- )
|
| |
- for issue in issues:
|
| |
- # Update the git version
|
| |
- pagure.lib.git.update_git(issue, repo=issue.project)
|
| |
-
|
| |
- msgs = []
|
| |
- msgs.append(
|
| |
- "Edited tag: %s(%s)[%s] to %s(%s)[%s]"
|
| |
- % (
|
| |
- old_tag_name,
|
| |
- old_tag_description,
|
| |
- old_tag_color,
|
| |
- new_tag,
|
| |
- new_tag_description,
|
| |
- new_tag_color,
|
| |
- )
|
| |
- )
|
| |
-
|
| |
- pagure.lib.notify.log(
|
| |
- project,
|
| |
- topic="project.tag.edited",
|
| |
- msg=dict(
|
| |
- project=project.to_json(public=True),
|
| |
- old_tag=old_tag.tag,
|
| |
- old_tag_description=old_tag_description,
|
| |
- old_tag_color=old_tag_color,
|
| |
- new_tag=new_tag,
|
| |
- new_tag_description=new_tag_description,
|
| |
- new_tag_color=new_tag_color,
|
| |
- agent=user_obj.username,
|
| |
- ),
|
| |
- redis=REDIS,
|
| |
- )
|
| |
-
|
| |
- return msgs
|
| |
-
|
| |
-
|
| |
- def add_sshkey_to_project_or_user(
|
| |
- session, ssh_key, pushaccess, creator, project=None, user=None
|
| |
- ):
|
| |
- """ Add a deploy key to a specified project. """
|
| |
- if project is None and user is None:
|
| |
- raise ValueError(
|
| |
- "SSH Keys need to be added to either a project or a user"
|
| |
- )
|
| |
- if project is not None and user is not None:
|
| |
- raise ValueError("SSH Keys need to be assigned to at least one object")
|
| |
-
|
| |
- ssh_key = ssh_key.strip()
|
| |
-
|
| |
- if "\n" in ssh_key:
|
| |
- raise pagure.exceptions.PagureException("Please add single SSH keys.")
|
| |
-
|
| |
- ssh_short_key = is_valid_ssh_key(ssh_key)
|
| |
- if ssh_short_key in [None, False]:
|
| |
- raise pagure.exceptions.PagureException("SSH key invalid.")
|
| |
-
|
| |
- # We are sure that this only contains a single key, but ssh-keygen still
|
| |
- # returns a \n at the end
|
| |
- ssh_short_key = ssh_short_key.strip()
|
| |
- if "\n" in ssh_key:
|
| |
- raise pagure.exceptions.PagureException(
|
| |
- "SSH has misbehaved when analyzing the SSH key"
|
| |
- )
|
| |
-
|
| |
- # Make sure that this key is not an SSH key for another project or user.
|
| |
- # If we dupe keys, we can't really know who this is for.
|
| |
- ssh_search_key = ssh_short_key.split(" ")[1]
|
| |
- if (
|
| |
- session.query(model.SSHKey)
|
| |
- .filter(model.SSHKey.ssh_search_key == ssh_search_key)
|
| |
- .count()
|
| |
- != 0
|
| |
- ):
|
| |
- raise pagure.exceptions.PagureException("SSH key already exists.")
|
| |
-
|
| |
- new_key_obj = model.SSHKey(
|
| |
- pushaccess=pushaccess,
|
| |
- public_ssh_key=ssh_key,
|
| |
- ssh_short_key=ssh_short_key,
|
| |
- ssh_search_key=ssh_search_key,
|
| |
- creator_user_id=creator.id,
|
| |
- )
|
| |
-
|
| |
- if project:
|
| |
- new_key_obj.project = project
|
| |
- if user:
|
| |
- new_key_obj.user_id = user.id
|
| |
-
|
| |
- session.add(new_key_obj)
|
| |
- # Make sure we won't have SQLAlchemy error before we continue
|
| |
- session.flush()
|
| |
-
|
| |
- # We do not send any notifications on purpose
|
| |
-
|
| |
- return "SSH key added"
|
| |
-
|
| |
-
|
| |
- def add_user_to_project(
|
| |
- session, project, new_user, user, access="admin", required_groups=None
|
| |
- ):
|
| |
- """ Add a specified user to a specified project with a specified access
|
| |
- """
|
| |
-
|
| |
- new_user_obj = get_user(session, new_user)
|
| |
-
|
| |
- if required_groups and access != "ticket":
|
| |
- for key in required_groups:
|
| |
- if fnmatch.fnmatch(project.fullname, key):
|
| |
- user_grps = set(new_user_obj.groups)
|
| |
- req_grps = set(required_groups[key])
|
| |
- if not user_grps.intersection(req_grps):
|
| |
- raise pagure.exceptions.PagureException(
|
| |
- "This user must be in one of the following groups "
|
| |
- "to be allowed to be added to this project: %s"
|
| |
- % ", ".join(req_grps)
|
| |
- )
|
| |
-
|
| |
- user_obj = get_user(session, user)
|
| |
-
|
| |
- users = set(
|
| |
- [
|
| |
- user_.user
|
| |
- for user_ in project.get_project_users(access, combine=False)
|
| |
- ]
|
| |
- )
|
| |
- users.add(project.user.user)
|
| |
-
|
| |
- if new_user in users:
|
| |
- raise pagure.exceptions.PagureException(
|
| |
- "This user is already listed on this project with the same access"
|
| |
- )
|
| |
-
|
| |
- # user has some access on project, so update to new access
|
| |
- if new_user_obj in project.users:
|
| |
- access_obj = get_obj_access(session, project, new_user_obj)
|
| |
- access_obj.access = access
|
| |
- project.date_modified = datetime.datetime.utcnow()
|
| |
- update_read_only_mode(session, project, read_only=True)
|
| |
- session.add(access_obj)
|
| |
- session.add(project)
|
| |
- session.flush()
|
| |
-
|
| |
- pagure.lib.notify.log(
|
| |
- project,
|
| |
- topic="project.user.access.updated",
|
| |
- msg=dict(
|
| |
- project=project.to_json(public=True),
|
| |
- new_user=new_user_obj.username,
|
| |
- new_access=access,
|
| |
- agent=user_obj.username,
|
| |
- ),
|
| |
- redis=REDIS,
|
| |
- )
|
| |
-
|
| |
- return "User access updated"
|
| |
-
|
| |
- project_user = model.ProjectUser(
|
| |
- project_id=project.id, user_id=new_user_obj.id, access=access
|
| |
- )
|
| |
- project.date_modified = datetime.datetime.utcnow()
|
| |
- session.add(project_user)
|
| |
- # Mark the project as read only, celery will then unmark it
|
| |
- update_read_only_mode(session, project, read_only=True)
|
| |
- session.add(project)
|
| |
- # Make sure we won't have SQLAlchemy error before we continue
|
| |
- session.flush()
|
| |
-
|
| |
- pagure.lib.notify.log(
|
| |
- project,
|
| |
- topic="project.user.added",
|
| |
- msg=dict(
|
| |
- project=project.to_json(public=True),
|
| |
- new_user=new_user_obj.username,
|
| |
- access=access,
|
| |
- agent=user_obj.username,
|
| |
- ),
|
| |
- redis=REDIS,
|
| |
- )
|
| |
-
|
| |
- return "User added"
|
| |
-
|
| |
-
|
| |
- def add_group_to_project(
|
| |
- session,
|
| |
- project,
|
| |
- new_group,
|
| |
- user,
|
| |
- access="admin",
|
| |
- create=False,
|
| |
- is_admin=False,
|
| |
- ):
|
| |
- """ Add a specified group to a specified project with some access """
|
| |
-
|
| |
- user_obj = search_user(session, username=user)
|
| |
- if not user_obj:
|
| |
- raise pagure.exceptions.PagureException("No user %s found." % user)
|
| |
-
|
| |
- group_obj = search_groups(session, group_name=new_group)
|
| |
-
|
| |
- if not group_obj:
|
| |
- if create:
|
| |
- group_obj = pagure.lib.model.PagureGroup(
|
| |
- group_name=new_group,
|
| |
- display_name=new_group,
|
| |
- group_type="user",
|
| |
- user_id=user_obj.id,
|
| |
- )
|
| |
- session.add(group_obj)
|
| |
- session.flush()
|
| |
- else:
|
| |
- raise pagure.exceptions.PagureException(
|
| |
- "No group %s found." % new_group
|
| |
- )
|
| |
-
|
| |
- if (
|
| |
- user_obj not in project.users
|
| |
- and user_obj != project.user
|
| |
- and not is_admin
|
| |
- ):
|
| |
- raise pagure.exceptions.PagureException(
|
| |
- "You are not allowed to add a group of users to this project"
|
| |
- )
|
| |
-
|
| |
- groups = set(
|
| |
- [
|
| |
- group.group_name
|
| |
- for group in project.get_project_groups(access, combine=False)
|
| |
- ]
|
| |
- )
|
| |
-
|
| |
- if new_group in groups:
|
| |
- raise pagure.exceptions.PagureException(
|
| |
- "This group already has this access on this project"
|
| |
- )
|
| |
-
|
| |
- # the group already has some access, update to new access
|
| |
- if group_obj in project.groups:
|
| |
- access_obj = get_obj_access(session, project, group_obj)
|
| |
- access_obj.access = access
|
| |
- session.add(access_obj)
|
| |
- project.date_modified = datetime.datetime.utcnow()
|
| |
- update_read_only_mode(session, project, read_only=True)
|
| |
- session.add(project)
|
| |
- session.flush()
|
| |
-
|
| |
- pagure.lib.notify.log(
|
| |
- project,
|
| |
- topic="project.group.access.updated",
|
| |
- msg=dict(
|
| |
- project=project.to_json(public=True),
|
| |
- new_group=group_obj.group_name,
|
| |
- new_access=access,
|
| |
- agent=user,
|
| |
- ),
|
| |
- redis=REDIS,
|
| |
- )
|
| |
-
|
| |
- return "Group access updated"
|
| |
-
|
| |
- project_group = model.ProjectGroup(
|
| |
- project_id=project.id, group_id=group_obj.id, access=access
|
| |
- )
|
| |
- session.add(project_group)
|
| |
- # Make sure we won't have SQLAlchemy error before we continue
|
| |
- project.date_modified = datetime.datetime.utcnow()
|
| |
- # Mark the project read_only, celery will then unmark it
|
| |
- update_read_only_mode(session, project, read_only=True)
|
| |
- session.add(project)
|
| |
- session.flush()
|
| |
-
|
| |
- pagure.lib.notify.log(
|
| |
- project,
|
| |
- topic="project.group.added",
|
| |
- msg=dict(
|
| |
- project=project.to_json(public=True),
|
| |
- new_group=group_obj.group_name,
|
| |
- access=access,
|
| |
- agent=user,
|
| |
- ),
|
| |
- redis=REDIS,
|
| |
- )
|
| |
-
|
| |
- return "Group added"
|
| |
-
|
| |
-
|
| |
- def add_pull_request_comment(
|
| |
- session,
|
| |
- request,
|
| |
- commit,
|
| |
- tree_id,
|
| |
- filename,
|
| |
- row,
|
| |
- comment,
|
| |
- user,
|
| |
- notify=True,
|
| |
- notification=False,
|
| |
- trigger_ci=None,
|
| |
- ):
|
| |
- """ Add a comment to a pull-request. """
|
| |
- user_obj = get_user(session, user)
|
| |
-
|
| |
- pr_comment = model.PullRequestComment(
|
| |
- pull_request_uid=request.uid,
|
| |
- commit_id=commit,
|
| |
- tree_id=tree_id,
|
| |
- filename=filename,
|
| |
- line=row,
|
| |
- comment=comment,
|
| |
- user_id=user_obj.id,
|
| |
- notification=notification,
|
| |
- )
|
| |
- session.add(pr_comment)
|
| |
- # Make sure we won't have SQLAlchemy error before we continue
|
| |
- session.flush()
|
| |
-
|
| |
- request.last_updated = datetime.datetime.utcnow()
|
| |
-
|
| |
- pagure.lib.git.update_git(request, repo=request.project)
|
| |
-
|
| |
- log_action(session, "commented", request, user_obj)
|
| |
-
|
| |
- if notify:
|
| |
- pagure.lib.notify.notify_pull_request_comment(pr_comment, user_obj)
|
| |
-
|
| |
- # Send notification for the event-source server
|
| |
- if REDIS and not request.project.private:
|
| |
- comment_text = text2markdown(pr_comment.comment)
|
| |
-
|
| |
- REDIS.publish(
|
| |
- "pagure.%s" % request.uid,
|
| |
- json.dumps(
|
| |
- {
|
| |
- "request_id": request.id,
|
| |
- "comment_added": comment_text,
|
| |
- "comment_user": pr_comment.user.user,
|
| |
- "comment_id": pr_comment.id,
|
| |
- "project": request.project.fullname,
|
| |
- "avatar_url": avatar_url_from_email(
|
| |
- pr_comment.user.default_email, size=16
|
| |
- ),
|
| |
- "comment_date": pr_comment.date_created.strftime(
|
| |
- "%Y-%m-%d %H:%M:%S"
|
| |
- ),
|
| |
- "commit_id": commit,
|
| |
- "filename": filename,
|
| |
- "line": row,
|
| |
- "notification": notification,
|
| |
- }
|
| |
- ),
|
| |
- )
|
| |
-
|
| |
- # Send notification to the CI server, if the comment added was a
|
| |
- # notification and the PR is still open and project is not private
|
| |
- if (
|
| |
- notification
|
| |
- and request.status == "Open"
|
| |
- and pagure_config.get("PAGURE_CI_SERVICES")
|
| |
- and request.project.ci_hook
|
| |
- and request.project.ci_hook.active_pr
|
| |
- and not request.project.private
|
| |
- ):
|
| |
- tasks_services.trigger_ci_build.delay(
|
| |
- pr_uid=request.uid,
|
| |
- cause=request.id,
|
| |
- branch=request.branch_from,
|
| |
- ci_type=request.project.ci_hook.ci_type,
|
| |
- )
|
| |
-
|
| |
- pagure.lib.notify.log(
|
| |
- request.project,
|
| |
- topic="pull-request.comment.added",
|
| |
- msg=dict(
|
| |
- pullrequest=request.to_json(public=True), agent=user_obj.username
|
| |
- ),
|
| |
- redis=REDIS,
|
| |
- )
|
| |
-
|
| |
- if (
|
| |
- trigger_ci
|
| |
- and comment.strip().lower() in trigger_ci
|
| |
- and pagure_config.get("PAGURE_CI_SERVICES")
|
| |
- and request.project.ci_hook
|
| |
- and request.project.ci_hook.active_pr
|
| |
- ):
|
| |
- tasks_services.trigger_ci_build.delay(
|
| |
- pr_uid=request.uid,
|
| |
- cause=request.id,
|
| |
- branch=request.branch_from,
|
| |
- ci_type=request.project.ci_hook.ci_type,
|
| |
- )
|
| |
-
|
| |
- return "Comment added"
|
| |
-
|
| |
-
|
| |
- def edit_comment(session, parent, comment, user, updated_comment):
|
| |
- """ Edit a comment. """
|
| |
- user_obj = get_user(session, user)
|
| |
- comment.comment = updated_comment
|
| |
- comment.edited_on = datetime.datetime.utcnow()
|
| |
- comment.editor = user_obj
|
| |
- parent.last_updated = comment.edited_on
|
| |
-
|
| |
- session.add(parent)
|
| |
- session.add(comment)
|
| |
- # Make sure we won't have SQLAlchemy error before we continue
|
| |
- session.flush()
|
| |
-
|
| |
- pagure.lib.git.update_git(parent, repo=parent.project)
|
| |
-
|
| |
- topic = "unknown"
|
| |
- key = "unknown"
|
| |
- id_ = "unknown"
|
| |
- private = False
|
| |
- if parent.isa == "pull-request":
|
| |
- topic = "pull-request.comment.edited"
|
| |
- key = "pullrequest"
|
| |
- id_ = "request_id"
|
| |
- elif parent.isa == "issue":
|
| |
- topic = "issue.comment.edited"
|
| |
- key = "issue"
|
| |
- id_ = "issue_id"
|
| |
- private = parent.private
|
| |
-
|
| |
- if not private:
|
| |
- pagure.lib.notify.log(
|
| |
- parent.project,
|
| |
- topic=topic,
|
| |
- msg={
|
| |
- key: parent.to_json(public=True, with_comments=False),
|
| |
- "project": parent.project.to_json(public=True),
|
| |
- "comment": comment.to_json(public=True),
|
| |
- "agent": user_obj.username,
|
| |
- },
|
| |
- redis=REDIS,
|
| |
- )
|
| |
-
|
| |
- if REDIS and not parent.project.private:
|
| |
- if private:
|
| |
- REDIS.publish(
|
| |
- "pagure.%s" % comment.parent.uid,
|
| |
- json.dumps(
|
| |
- {"comment_updated": "private", "comment_id": comment.id}
|
| |
- ),
|
| |
- )
|
| |
- else:
|
| |
- REDIS.publish(
|
| |
- "pagure.%s" % parent.uid,
|
| |
- json.dumps(
|
| |
- {
|
| |
- id_: len(parent.comments),
|
| |
- "comment_updated": text2markdown(comment.comment),
|
| |
- "comment_id": comment.id,
|
| |
- "parent_id": comment.parent.id,
|
| |
- "comment_editor": user_obj.user,
|
| |
- "avatar_url": avatar_url_from_email(
|
| |
- comment.user.default_email, size=16
|
| |
- ),
|
| |
- "comment_date": comment.edited_on.strftime(
|
| |
- "%Y-%m-%d %H:%M:%S"
|
| |
- ),
|
| |
- }
|
| |
- ),
|
| |
- )
|
| |
-
|
| |
- return "Comment updated"
|
| |
-
|
| |
-
|
| |
- def add_pull_request_flag(
|
| |
- session, request, username, percent, comment, url, status, uid, user, token
|
| |
- ):
|
| |
- """ Add a flag to a pull-request. """
|
| |
- user_obj = get_user(session, user)
|
| |
-
|
| |
- action = "added"
|
| |
- pr_flag = None
|
| |
- if uid:
|
| |
- pr_flag = get_pull_request_flag_by_uid(session, request, uid)
|
| |
- if pr_flag:
|
| |
- action = "updated"
|
| |
- pr_flag.comment = comment
|
| |
- pr_flag.status = status
|
| |
- pr_flag.percent = percent
|
| |
- pr_flag.url = url
|
| |
- else:
|
| |
- pr_flag = model.PullRequestFlag(
|
| |
- pull_request_uid=request.uid,
|
| |
- uid=uid or uuid.uuid4().hex,
|
| |
- username=username,
|
| |
- percent=percent,
|
| |
- comment=comment,
|
| |
- status=status,
|
| |
- url=url,
|
| |
- user_id=user_obj.id,
|
| |
- token_id=token,
|
| |
- )
|
| |
- session.add(pr_flag)
|
| |
- # Make sure we won't have SQLAlchemy error before we continue
|
| |
- session.flush()
|
| |
-
|
| |
- if request.project.settings.get("notify_on_pull-request_flag"):
|
| |
- pagure.lib.notify.notify_pull_request_flag(pr_flag, username)
|
| |
-
|
| |
- pagure.lib.git.update_git(request, repo=request.project)
|
| |
-
|
| |
- pagure.lib.notify.log(
|
| |
- request.project,
|
| |
- topic="pull-request.flag.%s" % action,
|
| |
- msg=dict(
|
| |
- pullrequest=request.to_json(public=True),
|
| |
- flag=pr_flag.to_json(public=True),
|
| |
- agent=user_obj.username,
|
| |
- ),
|
| |
- redis=REDIS,
|
| |
- )
|
| |
-
|
| |
- return ("Flag %s" % action, pr_flag.uid)
|
| |
-
|
| |
-
|
| |
- def add_commit_flag(
|
| |
- session,
|
| |
- repo,
|
| |
- commit_hash,
|
| |
- username,
|
| |
- status,
|
| |
- percent,
|
| |
- comment,
|
| |
- url,
|
| |
- uid,
|
| |
- user,
|
| |
- token,
|
| |
- ):
|
| |
- """ Add a flag to a add_commit_flag. """
|
| |
- user_obj = get_user(session, user)
|
| |
-
|
| |
- action = "added"
|
| |
- c_flag = get_commit_flag_by_uid(session, commit_hash, uid)
|
| |
- if c_flag:
|
| |
- action = "updated"
|
| |
- c_flag.comment = comment
|
| |
- c_flag.percent = percent
|
| |
- c_flag.status = status
|
| |
- c_flag.url = url
|
| |
- else:
|
| |
- c_flag = model.CommitFlag(
|
| |
- uid=uid or uuid.uuid4().hex,
|
| |
- project_id=repo.id,
|
| |
- commit_hash=commit_hash,
|
| |
- username=username,
|
| |
- status=status,
|
| |
- percent=percent,
|
| |
- comment=comment,
|
| |
- url=url,
|
| |
- user_id=user_obj.id,
|
| |
- token_id=token,
|
| |
- )
|
| |
- session.add(c_flag)
|
| |
- # Make sure we won't have SQLAlchemy error before we continue
|
| |
- session.flush()
|
| |
-
|
| |
- if repo.settings.get("notify_on_commit_flag"):
|
| |
- pagure.lib.notify.notify_commit_flag(c_flag, username)
|
| |
-
|
| |
- pagure.lib.notify.log(
|
| |
- repo,
|
| |
- topic="commit.flag.%s" % action,
|
| |
- msg=dict(
|
| |
- repo=repo.to_json(public=True),
|
| |
- flag=c_flag.to_json(public=True),
|
| |
- agent=user_obj.username,
|
| |
- ),
|
| |
- redis=REDIS,
|
| |
- )
|
| |
-
|
| |
- return ("Flag %s" % action, c_flag.uid)
|
| |
-
|
| |
-
|
| |
- def get_commit_flag(session, project, commit_hash):
|
| |
- """ Return the commit flags corresponding to the specified git hash
|
| |
- (commitid) in the specified repository.
|
| |
-
|
| |
- :arg session: the session with which to connect to the database
|
| |
- :arg repo: the pagure.lib.model.Project object corresponding to the
|
| |
- project whose commit has been flagged
|
| |
- :arg commit_hash: the hash of the commit who has been flagged
|
| |
- :return: list of pagure.lib.model.CommitFlag objects or an empty list
|
| |
-
|
| |
- """
|
| |
- query = (
|
| |
- session.query(model.CommitFlag)
|
| |
- .filter(model.CommitFlag.project_id == project.id)
|
| |
- .filter(model.CommitFlag.commit_hash == commit_hash)
|
| |
- )
|
| |
-
|
| |
- return query.all()
|
| |
-
|
| |
-
|
| |
- def new_project(
|
| |
- session,
|
| |
- user,
|
| |
- name,
|
| |
- blacklist,
|
| |
- allowed_prefix,
|
| |
- repospanner_region,
|
| |
- description=None,
|
| |
- url=None,
|
| |
- avatar_email=None,
|
| |
- parent_id=None,
|
| |
- add_readme=False,
|
| |
- userobj=None,
|
| |
- prevent_40_chars=False,
|
| |
- namespace=None,
|
| |
- user_ns=False,
|
| |
- ignore_existing_repo=False,
|
| |
- private=False,
|
| |
- ):
|
| |
- """ Create a new project based on the information provided.
|
| |
-
|
| |
- Is an async operation, and returns task ID.
|
| |
- """
|
| |
- ns_name = name if not namespace else "%s/%s" % (namespace, name)
|
| |
- matched = any(map(functools.partial(fnmatch.fnmatch, ns_name), blacklist))
|
| |
- if matched:
|
| |
- raise pagure.exceptions.ProjectBlackListedException(
|
| |
- 'No project "%s" are allowed to be created due to potential '
|
| |
- "conflicts in URLs with pagure itself" % ns_name
|
| |
- )
|
| |
-
|
| |
- user_obj = get_user(session, user)
|
| |
- allowed_prefix = allowed_prefix + [grp for grp in user_obj.groups]
|
| |
- if user_ns:
|
| |
- allowed_prefix.append(user)
|
| |
- if not namespace:
|
| |
- namespace = user
|
| |
- if private:
|
| |
- allowed_prefix.append(user)
|
| |
- namespace = user
|
| |
-
|
| |
- if namespace and namespace not in allowed_prefix:
|
| |
- raise pagure.exceptions.PagureException(
|
| |
- "The namespace of your project must be in the list of allowed "
|
| |
- "namespaces set by the admins of this pagure instance, or the "
|
| |
- "name of a group of which you are a member."
|
| |
- )
|
| |
-
|
| |
- if len(name) == 40 and prevent_40_chars:
|
| |
- # We must block project with a name <foo>/<bar> where the length
|
| |
- # of <bar> is exactly 40 characters long as this would otherwise
|
| |
- # conflict with the old URL schema used for commit that was
|
| |
- # <foo>/<commit hash>. To keep backward compatibility, we have an
|
| |
- # endpoint redirecting <foo>/<commit hash> to <foo>/c/<commit hash>
|
| |
- # available as an option.
|
| |
- raise pagure.exceptions.PagureException(
|
| |
- "Your project name cannot have exactly 40 characters after "
|
| |
- "the `/`"
|
| |
- )
|
| |
-
|
| |
- path = name
|
| |
- if namespace:
|
| |
- path = "%s/%s" % (namespace, name)
|
| |
-
|
| |
- # Repo exists in the DB
|
| |
- repo = _get_project(session, name, namespace=namespace)
|
| |
- # this is leaking private repos but we're leaking them anyway if we fail
|
| |
- # to add the requested repo later. Let's be less clear about why :)
|
| |
- if repo:
|
| |
- raise pagure.exceptions.RepoExistsException(
|
| |
- 'It is not possible to create the repo "%s"' % (path)
|
| |
- )
|
| |
-
|
| |
- if repospanner_region == "none":
|
| |
- repospanner_region = None
|
| |
- elif repospanner_region is None:
|
| |
- repospanner_region = pagure_config["REPOSPANNER_NEW_REPO"]
|
| |
-
|
| |
- if (
|
| |
- repospanner_region
|
| |
- and repospanner_region not in pagure_config["REPOSPANNER_REGIONS"]
|
| |
- ):
|
| |
- raise Exception("repoSpanner region %s invalid" % repospanner_region)
|
| |
-
|
| |
- project = model.Project(
|
| |
- name=name,
|
| |
- namespace=namespace,
|
| |
- repospanner_region=repospanner_region,
|
| |
- description=description if description else None,
|
| |
- url=url if url else None,
|
| |
- avatar_email=avatar_email if avatar_email else None,
|
| |
- user_id=user_obj.id,
|
| |
- parent_id=parent_id,
|
| |
- private=private,
|
| |
- hook_token=pagure.lib.login.id_generator(40),
|
| |
- )
|
| |
- session.add(project)
|
| |
- # Flush so that a project ID is generated
|
| |
- session.flush()
|
| |
- for ltype in model.ProjectLock.lock_type.type.enums:
|
| |
- lock = model.ProjectLock(project_id=project.id, lock_type=ltype)
|
| |
- session.add(lock)
|
| |
- session.commit()
|
| |
-
|
| |
- # Register creation et al
|
| |
- log_action(session, "created", project, user_obj)
|
| |
-
|
| |
- pagure.lib.notify.log(
|
| |
- project,
|
| |
- topic="project.new",
|
| |
- msg=dict(
|
| |
- project=project.to_json(public=True), agent=user_obj.username
|
| |
- ),
|
| |
- )
|
| |
-
|
| |
- return tasks.create_project.delay(
|
| |
- user_obj.username, namespace, name, add_readme, ignore_existing_repo
|
| |
- )
|
| |
-
|
| |
-
|
| |
- def new_issue(
|
| |
- session,
|
| |
- repo,
|
| |
- title,
|
| |
- content,
|
| |
- user,
|
| |
- issue_id=None,
|
| |
- issue_uid=None,
|
| |
- private=False,
|
| |
- status=None,
|
| |
- close_status=None,
|
| |
- notify=True,
|
| |
- date_created=None,
|
| |
- milestone=None,
|
| |
- priority=None,
|
| |
- assignee=None,
|
| |
- tags=None,
|
| |
- ):
|
| |
- """ Create a new issue for the specified repo. """
|
| |
- user_obj = get_user(session, user)
|
| |
-
|
| |
- # Only store the priority if there is one in the project
|
| |
- priorities = repo.priorities or []
|
| |
- try:
|
| |
- priority = int(priority)
|
| |
- except (ValueError, TypeError):
|
| |
- priority = None
|
| |
- if (
|
| |
- priorities
|
| |
- and priority is not None
|
| |
- and ("%s" % priority) not in priorities
|
| |
- ):
|
| |
- raise pagure.exceptions.PagureException(
|
| |
- "You are trying to create an issue with a priority that does "
|
| |
- "not exist in the project."
|
| |
- )
|
| |
-
|
| |
- assignee_id = None
|
| |
- if assignee is not None:
|
| |
- assignee_id = get_user(session, assignee).id
|
| |
-
|
| |
- issue = model.Issue(
|
| |
- id=issue_id or get_next_id(session, repo.id),
|
| |
- project_id=repo.id,
|
| |
- title=title,
|
| |
- content=content,
|
| |
- priority=priority,
|
| |
- milestone=milestone,
|
| |
- assignee_id=assignee_id,
|
| |
- user_id=user_obj.id,
|
| |
- uid=issue_uid or uuid.uuid4().hex,
|
| |
- private=private,
|
| |
- date_created=date_created,
|
| |
- )
|
| |
-
|
| |
- if status is not None:
|
| |
- issue.status = status
|
| |
- if close_status is not None:
|
| |
- issue.close_status = close_status
|
| |
- issue.last_updated = datetime.datetime.utcnow()
|
| |
-
|
| |
- session.add(issue)
|
| |
- # Make sure we won't have SQLAlchemy error before we create the issue
|
| |
- session.flush()
|
| |
-
|
| |
- # Add the tags if any are specified
|
| |
- if tags is not None:
|
| |
- for lbl in tags:
|
| |
- tagobj = get_colored_tag(session, lbl, repo.id)
|
| |
- if not tagobj:
|
| |
- tagobj = model.TagColored(tag=lbl, project_id=repo.id)
|
| |
- session.add(tagobj)
|
| |
- session.flush()
|
| |
-
|
| |
- dbobjtag = model.TagIssueColored(
|
| |
- issue_uid=issue.uid, tag_id=tagobj.id
|
| |
- )
|
| |
- session.add(dbobjtag)
|
| |
-
|
| |
- session.commit()
|
| |
-
|
| |
- pagure.lib.git.update_git(issue, repo=repo)
|
| |
-
|
| |
- log_action(session, "created", issue, user_obj)
|
| |
-
|
| |
- if notify:
|
| |
- pagure.lib.notify.notify_new_issue(issue, user=user_obj)
|
| |
-
|
| |
- if not private:
|
| |
- pagure.lib.notify.log(
|
| |
- issue.project,
|
| |
- topic="issue.new",
|
| |
- msg=dict(
|
| |
- issue=issue.to_json(public=True),
|
| |
- project=issue.project.to_json(public=True),
|
| |
- agent=user_obj.username,
|
| |
- ),
|
| |
- redis=REDIS,
|
| |
- )
|
| |
-
|
| |
- return issue
|
| |
-
|
| |
-
|
| |
- def drop_issue(session, issue, user):
|
| |
- """ Delete a specified issue. """
|
| |
- user_obj = get_user(session, user)
|
| |
-
|
| |
- repotype = issue.repotype
|
| |
- uid = issue.uid
|
| |
-
|
| |
- private = issue.private
|
| |
- session.delete(issue)
|
| |
-
|
| |
- # Make sure we won't have SQLAlchemy error before we create the issue
|
| |
- session.flush()
|
| |
-
|
| |
- if not private:
|
| |
- pagure.lib.notify.log(
|
| |
- issue.project,
|
| |
- topic="issue.drop",
|
| |
- msg=dict(
|
| |
- issue=issue.to_json(public=True),
|
| |
- project=issue.project.to_json(public=True),
|
| |
- agent=user_obj.username,
|
| |
- ),
|
| |
- redis=REDIS,
|
| |
- )
|
| |
-
|
| |
- session.commit()
|
| |
-
|
| |
- pagure.lib.git.clean_git(issue.project, repotype, uid)
|
| |
-
|
| |
- return issue
|
| |
-
|
| |
-
|
| |
- def new_pull_request(
|
| |
- session,
|
| |
- branch_from,
|
| |
- repo_to,
|
| |
- branch_to,
|
| |
- title,
|
| |
- user,
|
| |
- initial_comment=None,
|
| |
- repo_from=None,
|
| |
- remote_git=None,
|
| |
- requestuid=None,
|
| |
- requestid=None,
|
| |
- status="Open",
|
| |
- notify=True,
|
| |
- commit_start=None,
|
| |
- commit_stop=None,
|
| |
- ):
|
| |
- """ Create a new pull request on the specified repo. """
|
| |
- if not repo_from and not remote_git:
|
| |
- raise pagure.exceptions.PagureException(
|
| |
- "Invalid input, you must specify either a local repo or a "
|
| |
- "remote one"
|
| |
- )
|
| |
-
|
| |
- user_obj = get_user(session, user)
|
| |
-
|
| |
- request = model.PullRequest(
|
| |
- id=requestid or get_next_id(session, repo_to.id),
|
| |
- uid=requestuid or uuid.uuid4().hex,
|
| |
- project_id=repo_to.id,
|
| |
- project_id_from=repo_from.id if repo_from else None,
|
| |
- remote_git=remote_git if remote_git else None,
|
| |
- branch=branch_to,
|
| |
- branch_from=branch_from,
|
| |
- title=title,
|
| |
- initial_comment=initial_comment or None,
|
| |
- user_id=user_obj.id,
|
| |
- status=status,
|
| |
- commit_start=commit_start,
|
| |
- commit_stop=commit_stop,
|
| |
- )
|
| |
- request.last_updated = datetime.datetime.utcnow()
|
| |
-
|
| |
- session.add(request)
|
| |
- # Make sure we won't have SQLAlchemy error before we create the request
|
| |
- session.flush()
|
| |
-
|
| |
- pagure.lib.git.update_git(request, repo=request.project)
|
| |
-
|
| |
- pagure.lib.tasks.link_pr_to_ticket.delay(request.uid)
|
| |
-
|
| |
- log_action(session, "created", request, user_obj)
|
| |
-
|
| |
- if notify:
|
| |
- pagure.lib.notify.notify_new_pull_request(request)
|
| |
-
|
| |
- pagure.lib.notify.log(
|
| |
- request.project,
|
| |
- topic="pull-request.new",
|
| |
- msg=dict(
|
| |
- pullrequest=request.to_json(public=True), agent=user_obj.username
|
| |
- ),
|
| |
- redis=REDIS,
|
| |
- )
|
| |
-
|
| |
- # Send notification to the CI server
|
| |
- if (
|
| |
- pagure_config.get("PAGURE_CI_SERVICES")
|
| |
- and request.project.ci_hook
|
| |
- and request.project.ci_hook.active_pr
|
| |
- and not request.project.private
|
| |
- ):
|
| |
- tasks_services.trigger_ci_build.delay(
|
| |
- pr_uid=request.uid,
|
| |
- cause=request.id,
|
| |
- branch=request.branch_from,
|
| |
- ci_type=request.project.ci_hook.ci_type,
|
| |
- )
|
| |
-
|
| |
- # Create the ref from the start
|
| |
- tasks.sync_pull_ref.delay(
|
| |
- request.project.name,
|
| |
- request.project.namespace,
|
| |
- request.project.user.username if request.project.is_fork else None,
|
| |
- request.id,
|
| |
- )
|
| |
-
|
| |
- return request
|
| |
-
|
| |
-
|
| |
- def new_tag(session, tag_name, tag_description, tag_color, project_id):
|
| |
- """ Return a new tag object """
|
| |
- tagobj = model.TagColored(
|
| |
- tag=tag_name,
|
| |
- tag_description=tag_description,
|
| |
- tag_color=tag_color,
|
| |
- project_id=project_id,
|
| |
- )
|
| |
- session.add(tagobj)
|
| |
- session.flush()
|
| |
-
|
| |
- return tagobj
|
| |
-
|
| |
-
|
| |
- def edit_issue(
|
| |
- session,
|
| |
- issue,
|
| |
- user,
|
| |
- repo=None,
|
| |
- title=None,
|
| |
- content=None,
|
| |
- status=None,
|
| |
- close_status=Unspecified,
|
| |
- priority=Unspecified,
|
| |
- milestone=Unspecified,
|
| |
- private=None,
|
| |
- ):
|
| |
- """ Edit the specified issue.
|
| |
-
|
| |
- :arg session: the session to use to connect to the database.
|
| |
- :arg issue: the pagure.lib.model.Issue object to edit.
|
| |
- :arg user: the username of the user editing the issue,
|
| |
- :kwarg repo: somehow this isn't used anywhere here...
|
| |
- :kwarg title: the new title of the issue if it's being changed
|
| |
- :kwarg content: the new content of the issue if it's being changed
|
| |
- :kwarg status: the new status of the issue if it's being changed
|
| |
- :kwarg close_status: the new close_status of the issue if it's being
|
| |
- changed
|
| |
- :kwarg priority: the new priority of the issue if it's being changed
|
| |
- :kwarg milestone: the new milestone of the issue if it's being changed
|
| |
- :kwarg private: the new private of the issue if it's being changed
|
| |
-
|
| |
- """
|
| |
- user_obj = get_user(session, user)
|
| |
- if status and status != "Open" and issue.parents:
|
| |
- for parent in issue.parents:
|
| |
- if parent.status == "Open":
|
| |
- raise pagure.exceptions.PagureException(
|
| |
- "You cannot close a ticket that has ticket "
|
| |
- "depending that are still open."
|
| |
- )
|
| |
-
|
| |
- edit = []
|
| |
- messages = []
|
| |
- if title and title != issue.title:
|
| |
- issue.title = title
|
| |
- edit.append("title")
|
| |
- if content and content != issue.content:
|
| |
- issue.content = content
|
| |
- edit.append("content")
|
| |
- if status and status != issue.status:
|
| |
- old_status = issue.status
|
| |
- issue.status = status
|
| |
- if status.lower() != "open":
|
| |
- issue.closed_at = datetime.datetime.utcnow()
|
| |
- elif issue.close_status:
|
| |
- issue.close_status = None
|
| |
- close_status = Unspecified
|
| |
- edit.append("close_status")
|
| |
- edit.append("status")
|
| |
- messages.append(
|
| |
- "Issue status updated to: %s (was: %s)" % (status, old_status)
|
| |
- )
|
| |
- if close_status != Unspecified and close_status != issue.close_status:
|
| |
- old_status = issue.close_status
|
| |
- issue.close_status = close_status
|
| |
- edit.append("close_status")
|
| |
- msg = "Issue close_status updated to: %s" % close_status
|
| |
- if old_status:
|
| |
- msg += " (was: %s)" % old_status
|
| |
- if issue.status.lower() == "open" and close_status:
|
| |
- issue.status = "Closed"
|
| |
- issue.closed_at = datetime.datetime.utcnow()
|
| |
- edit.append("status")
|
| |
- messages.append(msg)
|
| |
- if priority != Unspecified:
|
| |
- priorities = issue.project.priorities
|
| |
- try:
|
| |
- priority = int(priority)
|
| |
- except (ValueError, TypeError):
|
| |
- priority = None
|
| |
-
|
| |
- priority_string = "%s" % priority
|
| |
- if priority_string not in priorities:
|
| |
- priority = None
|
| |
-
|
| |
- if priority != issue.priority:
|
| |
- old_priority = issue.priority
|
| |
- issue.priority = priority
|
| |
- edit.append("priority")
|
| |
- msg = "Issue priority set to: %s" % (
|
| |
- priorities[priority_string] if priority else None
|
| |
- )
|
| |
- if old_priority:
|
| |
- msg += " (was: %s)" % priorities.get(
|
| |
- "%s" % old_priority, old_priority
|
| |
- )
|
| |
- messages.append(msg)
|
| |
- if private in [True, False] and private != issue.private:
|
| |
- old_private = issue.private
|
| |
- issue.private = private
|
| |
- edit.append("private")
|
| |
- msg = "Issue private status set to: %s" % private
|
| |
- if old_private:
|
| |
- msg += " (was: %s)" % old_private
|
| |
- messages.append(msg)
|
| |
- if milestone != Unspecified and milestone != issue.milestone:
|
| |
- old_milestone = issue.milestone
|
| |
- issue.milestone = milestone
|
| |
- edit.append("milestone")
|
| |
- msg = "Issue set to the milestone: %s" % milestone
|
| |
- if old_milestone:
|
| |
- msg += " (was: %s)" % old_milestone
|
| |
- messages.append(msg)
|
| |
- issue.last_updated = datetime.datetime.utcnow()
|
| |
- # uniquify the list of edited fields
|
| |
- edit = list(set(edit))
|
| |
-
|
| |
- pagure.lib.git.update_git(issue, repo=issue.project)
|
| |
-
|
| |
- if "status" in edit:
|
| |
- log_action(session, issue.status.lower(), issue, user_obj)
|
| |
- pagure.lib.notify.notify_status_change_issue(issue, user_obj)
|
| |
-
|
| |
- if not issue.private and edit:
|
| |
- pagure.lib.notify.log(
|
| |
- issue.project,
|
| |
- topic="issue.edit",
|
| |
- msg=dict(
|
| |
- issue=issue.to_json(public=True),
|
| |
- project=issue.project.to_json(public=True),
|
| |
- fields=list(set(edit)),
|
| |
- agent=user_obj.username,
|
| |
- ),
|
| |
- redis=REDIS,
|
| |
- )
|
| |
-
|
| |
- if REDIS and edit and not issue.project.private:
|
| |
- if issue.private:
|
| |
- REDIS.publish(
|
| |
- "pagure.%s" % issue.uid,
|
| |
- json.dumps({"issue": "private", "fields": edit}),
|
| |
- )
|
| |
- else:
|
| |
- REDIS.publish(
|
| |
- "pagure.%s" % issue.uid,
|
| |
- json.dumps(
|
| |
- {
|
| |
- "fields": edit,
|
| |
- "issue": issue.to_json(
|
| |
- public=True, with_comments=False
|
| |
- ),
|
| |
- "priorities": issue.project.priorities,
|
| |
- }
|
| |
- ),
|
| |
- )
|
| |
-
|
| |
- if edit:
|
| |
- session.add(issue)
|
| |
- session.flush()
|
| |
- return messages
|
| |
-
|
| |
-
|
| |
- def update_project_settings(session, repo, settings, user):
|
| |
- """ Update the settings of a project. """
|
| |
- user_obj = get_user(session, user)
|
| |
-
|
| |
- update = []
|
| |
- new_settings = repo.settings
|
| |
- for key in new_settings:
|
| |
- if key in settings:
|
| |
- if key == "Minimum_score_to_merge_pull-request":
|
| |
- try:
|
| |
- settings[key] = int(settings[key]) if settings[key] else -1
|
| |
- except (ValueError, TypeError):
|
| |
- raise pagure.exceptions.PagureException(
|
| |
- "Please enter a numeric value for the 'minimum "
|
| |
- "score to merge pull request' field."
|
| |
- )
|
| |
- elif key == "Web-hooks":
|
| |
- settings[key] = settings[key] or None
|
| |
- else:
|
| |
- # All the remaining keys are boolean, so True is provided
|
| |
- # as 'y' by the html, let's convert it back
|
| |
- settings[key] = settings[key] in ["y", True]
|
| |
-
|
| |
- if new_settings[key] != settings[key]:
|
| |
- update.append(key)
|
| |
- new_settings[key] = settings[key]
|
| |
- else:
|
| |
- val = False
|
| |
- if key == "Web-hooks":
|
| |
- val = None
|
| |
-
|
| |
- # Ensure the default value is different from what is stored.
|
| |
- if new_settings[key] != val:
|
| |
- update.append(key)
|
| |
- new_settings[key] = val
|
| |
-
|
| |
- if not update:
|
| |
- return "No settings to change"
|
| |
- else:
|
| |
- repo.settings = new_settings
|
| |
- repo.date_modified = datetime.datetime.utcnow()
|
| |
- session.add(repo)
|
| |
- session.flush()
|
| |
-
|
| |
- pagure.lib.notify.log(
|
| |
- repo,
|
| |
- topic="project.edit",
|
| |
- msg=dict(
|
| |
- project=repo.to_json(public=True),
|
| |
- fields=update,
|
| |
- agent=user_obj.username,
|
| |
- ),
|
| |
- redis=REDIS,
|
| |
- )
|
| |
-
|
| |
- if "pull_request_access_only" in update:
|
| |
- update_read_only_mode(session, repo, read_only=True)
|
| |
- session.add(repo)
|
| |
- session.flush()
|
| |
- pagure.lib.git.generate_gitolite_acls(project=repo)
|
| |
-
|
| |
- return "Edited successfully settings of repo: %s" % repo.fullname
|
| |
-
|
| |
-
|
| |
- def update_user_settings(session, settings, user):
|
| |
- """ Update the settings of a project. """
|
| |
- user_obj = get_user(session, user)
|
| |
-
|
| |
- update = []
|
| |
- new_settings = user_obj.settings
|
| |
- for key in new_settings:
|
| |
- if key in settings:
|
| |
- if new_settings[key] != settings[key]:
|
| |
- update.append(key)
|
| |
- new_settings[key] = settings[key]
|
| |
- else:
|
| |
- if new_settings[key] is not False:
|
| |
- update.append(key)
|
| |
- new_settings[key] = False
|
| |
-
|
| |
- if not update:
|
| |
- return "No settings to change"
|
| |
- else:
|
| |
- user_obj.settings = new_settings
|
| |
- session.add(user_obj)
|
| |
- session.flush()
|
| |
-
|
| |
- return "Successfully edited your settings"
|
| |
-
|
| |
-
|
| |
- def fork_project(session, user, repo, editbranch=None, editfile=None):
|
| |
- """ Fork a given project into the user's forks. """
|
| |
- if _get_project(session, repo.name, user, repo.namespace) is not None:
|
| |
- raise pagure.exceptions.RepoExistsException(
|
| |
- 'Repo "forks/%s/%s" already exists' % (user, repo.name)
|
| |
- )
|
| |
-
|
| |
- user_obj = get_user(session, user)
|
| |
-
|
| |
- fork_repospanner_setting = pagure_config["REPOSPANNER_NEW_FORK"]
|
| |
- if fork_repospanner_setting is None:
|
| |
- repospanner_region = None
|
| |
- elif fork_repospanner_setting is True:
|
| |
- repospanner_region = repo.repospanner_region
|
| |
- else:
|
| |
- repospanner_region = fork_repospanner_setting
|
| |
-
|
| |
- project = model.Project(
|
| |
- name=repo.name,
|
| |
- namespace=repo.namespace,
|
| |
- description=repo.description,
|
| |
- repospanner_region=repospanner_region,
|
| |
- private=repo.private,
|
| |
- user_id=user_obj.id,
|
| |
- parent_id=repo.id,
|
| |
- is_fork=True,
|
| |
- hook_token=pagure.lib.login.id_generator(40),
|
| |
- )
|
| |
-
|
| |
- # disable issues, PRs in the fork by default
|
| |
- default_repo_settings = project.settings
|
| |
- default_repo_settings["issue_tracker"] = False
|
| |
- default_repo_settings["pull_requests"] = False
|
| |
- project.settings = default_repo_settings
|
| |
-
|
| |
- session.add(project)
|
| |
- # Make sure we won't have SQLAlchemy error before we create the repo
|
| |
- session.flush()
|
| |
- session.commit()
|
| |
-
|
| |
- task = tasks.fork.delay(
|
| |
- repo.name,
|
| |
- repo.namespace,
|
| |
- repo.user.username if repo.is_fork else None,
|
| |
- user,
|
| |
- editbranch,
|
| |
- editfile,
|
| |
- )
|
| |
- return task
|
| |
-
|
| |
-
|
| |
- def search_projects(
|
| |
- session,
|
| |
- username=None,
|
| |
- fork=None,
|
| |
- tags=None,
|
| |
- namespace=None,
|
| |
- pattern=None,
|
| |
- start=None,
|
| |
- limit=None,
|
| |
- count=False,
|
| |
- sort=None,
|
| |
- exclude_groups=None,
|
| |
- private=None,
|
| |
- owner=None,
|
| |
- ):
|
| |
- """List existing projects
|
| |
- """
|
| |
- projects = session.query(sqlalchemy.distinct(model.Project.id))
|
| |
-
|
| |
- if owner is not None and username is not None:
|
| |
- raise RuntimeError(
|
| |
- "You cannot supply both a username and an owner "
|
| |
- "as parameters in the `search_projects` function"
|
| |
- )
|
| |
- elif owner is not None:
|
| |
- projects = projects.join(model.User).filter(model.User.user == owner)
|
| |
- elif username is not None:
|
| |
- projects = projects.filter(
|
| |
- # User created the project
|
| |
- sqlalchemy.and_(
|
| |
- model.User.user == username,
|
| |
- model.User.id == model.Project.user_id,
|
| |
- )
|
| |
- )
|
| |
- sub_q2 = session.query(model.Project.id).filter(
|
| |
- # User got admin or commit right
|
| |
- sqlalchemy.and_(
|
| |
- model.User.user == username,
|
| |
- model.User.id == model.ProjectUser.user_id,
|
| |
- model.ProjectUser.project_id == model.Project.id,
|
| |
- sqlalchemy.or_(
|
| |
- model.ProjectUser.access == "admin",
|
| |
- model.ProjectUser.access == "commit",
|
| |
- ),
|
| |
- )
|
| |
- )
|
| |
- sub_q3 = session.query(model.Project.id).filter(
|
| |
- # User created a group that has admin or commit right
|
| |
- sqlalchemy.and_(
|
| |
- model.User.user == username,
|
| |
- model.PagureGroup.user_id == model.User.id,
|
| |
- model.PagureGroup.group_type == "user",
|
| |
- model.PagureGroup.id == model.ProjectGroup.group_id,
|
| |
- model.Project.id == model.ProjectGroup.project_id,
|
| |
- sqlalchemy.or_(
|
| |
- model.ProjectGroup.access == "admin",
|
| |
- model.ProjectGroup.access == "commit",
|
| |
- ),
|
| |
- )
|
| |
- )
|
| |
- sub_q4 = session.query(model.Project.id).filter(
|
| |
- # User is part of a group that has admin or commit right
|
| |
- sqlalchemy.and_(
|
| |
- model.User.user == username,
|
| |
- model.PagureUserGroup.user_id == model.User.id,
|
| |
- model.PagureUserGroup.group_id == model.PagureGroup.id,
|
| |
- model.PagureGroup.group_type == "user",
|
| |
- model.PagureGroup.id == model.ProjectGroup.group_id,
|
| |
- model.Project.id == model.ProjectGroup.project_id,
|
| |
- sqlalchemy.or_(
|
| |
- model.ProjectGroup.access == "admin",
|
| |
- model.ProjectGroup.access == "commit",
|
| |
- ),
|
| |
- )
|
| |
- )
|
| |
-
|
| |
- # Exclude projects that the user has accessed via a group that we
|
| |
- # do not want to include
|
| |
- if exclude_groups:
|
| |
- sub_q3 = sub_q3.filter(
|
| |
- model.PagureGroup.group_name.notin_(exclude_groups)
|
| |
- )
|
| |
- sub_q4 = sub_q4.filter(
|
| |
- model.PagureGroup.group_name.notin_(exclude_groups)
|
| |
- )
|
| |
-
|
| |
- projects = projects.union(sub_q2).union(sub_q3).union(sub_q4)
|
| |
-
|
| |
- if not private:
|
| |
- projects = projects.filter(
|
| |
- model.Project.private == False # noqa: E712
|
| |
- )
|
| |
- # No filtering is done if private == username i.e if the owner of the
|
| |
- # project is viewing the project
|
| |
- elif isinstance(private, six.string_types) and private != username:
|
| |
- # All the public repo
|
| |
- subquery0 = session.query(
|
| |
- sqlalchemy.distinct(model.Project.id)
|
| |
- ).filter(
|
| |
- model.Project.private == False # noqa: E712
|
| |
- )
|
| |
- sub_q1 = session.query(sqlalchemy.distinct(model.Project.id)).filter(
|
| |
- sqlalchemy.and_(
|
| |
- model.Project.private == True, # noqa: E712
|
| |
- model.User.id == model.Project.user_id,
|
| |
- model.User.user == private,
|
| |
- )
|
| |
- )
|
| |
- sub_q2 = session.query(model.Project.id).filter(
|
| |
- # User got admin or commit right
|
| |
- sqlalchemy.and_(
|
| |
- model.Project.private == True, # noqa: E712
|
| |
- model.User.user == private,
|
| |
- model.User.id == model.ProjectUser.user_id,
|
| |
- model.ProjectUser.project_id == model.Project.id,
|
| |
- sqlalchemy.or_(
|
| |
- model.ProjectUser.access == "admin",
|
| |
- model.ProjectUser.access == "commit",
|
| |
- ),
|
| |
- )
|
| |
- )
|
| |
- sub_q3 = session.query(model.Project.id).filter(
|
| |
- # User created a group that has admin or commit right
|
| |
- sqlalchemy.and_(
|
| |
- model.Project.private == True, # noqa: E712
|
| |
- model.User.user == private,
|
| |
- model.PagureGroup.user_id == model.User.id,
|
| |
- model.PagureGroup.group_type == "user",
|
| |
- model.PagureGroup.id == model.ProjectGroup.group_id,
|
| |
- model.Project.id == model.ProjectGroup.project_id,
|
| |
- sqlalchemy.or_(
|
| |
- model.ProjectGroup.access == "admin",
|
| |
- model.ProjectGroup.access == "commit",
|
| |
- ),
|
| |
- )
|
| |
- )
|
| |
- sub_q4 = session.query(model.Project.id).filter(
|
| |
- # User is part of a group that has admin or commit right
|
| |
- sqlalchemy.and_(
|
| |
- model.Project.private == True, # noqa: E712
|
| |
- model.User.user == private,
|
| |
- model.PagureUserGroup.user_id == model.User.id,
|
| |
- model.PagureUserGroup.group_id == model.PagureGroup.id,
|
| |
- model.PagureGroup.group_type == "user",
|
| |
- model.PagureGroup.id == model.ProjectGroup.group_id,
|
| |
- model.Project.id == model.ProjectGroup.project_id,
|
| |
- sqlalchemy.or_(
|
| |
- model.ProjectGroup.access == "admin",
|
| |
- model.ProjectGroup.access == "commit",
|
| |
- ),
|
| |
- )
|
| |
- )
|
| |
-
|
| |
- # Exclude projects that the user has accessed via a group that we
|
| |
- # do not want to include
|
| |
- if exclude_groups:
|
| |
- sub_q3 = sub_q3.filter(
|
| |
- model.PagureGroup.group_name.notin_(exclude_groups)
|
| |
- )
|
| |
- sub_q4 = sub_q4.filter(
|
| |
- model.PagureGroup.group_name.notin_(exclude_groups)
|
| |
- )
|
| |
-
|
| |
- projects = projects.filter(
|
| |
- model.Project.id.in_(
|
| |
- subquery0.union(sub_q1)
|
| |
- .union(sub_q2)
|
| |
- .union(sub_q3)
|
| |
- .union(sub_q4)
|
| |
- )
|
| |
- )
|
| |
-
|
| |
- if fork is not None:
|
| |
- if fork is True:
|
| |
- projects = projects.filter(
|
| |
- model.Project.is_fork == True # noqa: E712
|
| |
- )
|
| |
- elif fork is False:
|
| |
- projects = projects.filter(
|
| |
- model.Project.is_fork == False # noqa: E712
|
| |
- )
|
| |
-
|
| |
- if tags:
|
| |
- if not isinstance(tags, (list, tuple)):
|
| |
- tags = [tags]
|
| |
-
|
| |
- projects = projects.filter(
|
| |
- model.Project.id == model.TagProject.project_id
|
| |
- ).filter(model.TagProject.tag.in_(tags))
|
| |
-
|
| |
- if pattern:
|
| |
- pattern = pattern.replace("*", "%")
|
| |
- if "%" in pattern:
|
| |
- projects = projects.filter(model.Project.name.ilike(pattern))
|
| |
- else:
|
| |
- projects = projects.filter(model.Project.name == pattern)
|
| |
-
|
| |
- if namespace:
|
| |
- projects = projects.filter(model.Project.namespace == namespace)
|
| |
-
|
| |
- query = session.query(model.Project).filter(
|
| |
- model.Project.id.in_(projects.subquery())
|
| |
- )
|
| |
-
|
| |
- if sort == "latest":
|
| |
- query = query.order_by(model.Project.date_created.desc())
|
| |
- elif sort == "oldest":
|
| |
- query = query.order_by(model.Project.date_created.asc())
|
| |
- else:
|
| |
- query = query.order_by(asc(func.lower(model.Project.name)))
|
| |
-
|
| |
- if start is not None:
|
| |
- query = query.offset(start)
|
| |
-
|
| |
- if limit is not None:
|
| |
- query = query.limit(limit)
|
| |
-
|
| |
- if count:
|
| |
- return query.count()
|
| |
- else:
|
| |
- return query.all()
|
| |
-
|
| |
-
|
| |
- def list_users_projects(
|
| |
- session,
|
| |
- username,
|
| |
- fork=None,
|
| |
- tags=None,
|
| |
- namespace=None,
|
| |
- pattern=None,
|
| |
- start=None,
|
| |
- limit=None,
|
| |
- count=False,
|
| |
- sort=None,
|
| |
- exclude_groups=None,
|
| |
- private=None,
|
| |
- acls=None,
|
| |
- ):
|
| |
- """List a users projects
|
| |
- """
|
| |
- projects = session.query(sqlalchemy.distinct(model.Project.id))
|
| |
-
|
| |
- if acls is None:
|
| |
- acls = ["main admin", "admin", "commit", "ticket"]
|
| |
-
|
| |
- if username is not None:
|
| |
-
|
| |
- projects = projects.filter(
|
| |
- # User created the project
|
| |
- sqlalchemy.and_(
|
| |
- model.User.user == username,
|
| |
- model.User.id == model.Project.user_id,
|
| |
- )
|
| |
- )
|
| |
- if "main admin" not in acls:
|
| |
- projects = projects.filter(model.User.id != model.Project.user_id)
|
| |
-
|
| |
- sub_q2 = session.query(model.Project.id).filter(
|
| |
- # User got admin or commit right
|
| |
- sqlalchemy.and_(
|
| |
- model.User.user == username,
|
| |
- model.User.id == model.ProjectUser.user_id,
|
| |
- model.ProjectUser.project_id == model.Project.id,
|
| |
- model.ProjectUser.access.in_(acls),
|
| |
- )
|
| |
- )
|
| |
- sub_q3 = session.query(model.Project.id).filter(
|
| |
- # User created a group that has admin or commit right
|
| |
- sqlalchemy.and_(
|
| |
- model.User.user == username,
|
| |
- model.PagureGroup.user_id == model.User.id,
|
| |
- model.PagureGroup.group_type == "user",
|
| |
- model.PagureGroup.id == model.ProjectGroup.group_id,
|
| |
- model.Project.id == model.ProjectGroup.project_id,
|
| |
- model.ProjectGroup.access.in_(acls),
|
| |
- )
|
| |
- )
|
| |
- sub_q4 = session.query(model.Project.id).filter(
|
| |
- # User is part of a group that has admin or commit right
|
| |
- sqlalchemy.and_(
|
| |
- model.User.user == username,
|
| |
- model.PagureUserGroup.user_id == model.User.id,
|
| |
- model.PagureUserGroup.group_id == model.PagureGroup.id,
|
| |
- model.PagureGroup.group_type == "user",
|
| |
- model.PagureGroup.id == model.ProjectGroup.group_id,
|
| |
- model.Project.id == model.ProjectGroup.project_id,
|
| |
- model.ProjectGroup.access.in_(acls),
|
| |
- )
|
| |
- )
|
| |
-
|
| |
- # Exclude projects that the user has accessed via a group that we
|
| |
- # do not want to include
|
| |
- if exclude_groups:
|
| |
- sub_q3 = sub_q3.filter(
|
| |
- model.PagureGroup.group_name.notin_(exclude_groups)
|
| |
- )
|
| |
- sub_q4 = sub_q4.filter(
|
| |
- model.PagureGroup.group_name.notin_(exclude_groups)
|
| |
- )
|
| |
-
|
| |
- projects = projects.union(sub_q2).union(sub_q3).union(sub_q4)
|
| |
-
|
| |
- if not private:
|
| |
- projects = projects.filter(
|
| |
- model.Project.private == False # noqa: E712
|
| |
- )
|
| |
- # No filtering is done if private == username i.e if the owner of the
|
| |
- # project is viewing the project
|
| |
- elif isinstance(private, six.string_types) and private != username:
|
| |
- # All the public repo
|
| |
- subquery0 = session.query(
|
| |
- sqlalchemy.distinct(model.Project.id)
|
| |
- ).filter(
|
| |
- model.Project.private == False # noqa: E712
|
| |
- )
|
| |
- sub_q1 = session.query(sqlalchemy.distinct(model.Project.id)).filter(
|
| |
- sqlalchemy.and_(
|
| |
- model.Project.private == True, # noqa: E712
|
| |
- model.User.id == model.Project.user_id,
|
| |
- model.User.user == private,
|
| |
- )
|
| |
- )
|
| |
- sub_q2 = session.query(model.Project.id).filter(
|
| |
- # User got admin or commit right
|
| |
- sqlalchemy.and_(
|
| |
- model.Project.private == True, # noqa: E712
|
| |
- model.User.user == private,
|
| |
- model.User.id == model.ProjectUser.user_id,
|
| |
- model.ProjectUser.project_id == model.Project.id,
|
| |
- model.ProjectUser.access.in_(acls),
|
| |
- )
|
| |
- )
|
| |
- sub_q3 = session.query(model.Project.id).filter(
|
| |
- # User created a group that has admin or commit right
|
| |
- sqlalchemy.and_(
|
| |
- model.Project.private == True, # noqa: E712
|
| |
- model.User.user == private,
|
| |
- model.PagureGroup.user_id == model.User.id,
|
| |
- model.PagureGroup.group_type == "user",
|
| |
- model.PagureGroup.id == model.ProjectGroup.group_id,
|
| |
- model.Project.id == model.ProjectGroup.project_id,
|
| |
- model.ProjectGroup.access.in_(acls),
|
| |
- )
|
| |
- )
|
| |
- sub_q4 = session.query(model.Project.id).filter(
|
| |
- # User is part of a group that has admin or commit right
|
| |
- sqlalchemy.and_(
|
| |
- model.Project.private == True, # noqa: E712
|
| |
- model.User.user == private,
|
| |
- model.PagureUserGroup.user_id == model.User.id,
|
| |
- model.PagureUserGroup.group_id == model.PagureGroup.id,
|
| |
- model.PagureGroup.group_type == "user",
|
| |
- model.PagureGroup.id == model.ProjectGroup.group_id,
|
| |
- model.Project.id == model.ProjectGroup.project_id,
|
| |
- model.ProjectGroup.access.in_(acls),
|
| |
- )
|
| |
- )
|
| |
-
|
| |
- # Exclude projects that the user has accessed via a group that we
|
| |
- # do not want to include
|
| |
- if exclude_groups:
|
| |
- sub_q3 = sub_q3.filter(
|
| |
- model.PagureGroup.group_name.notin_(exclude_groups)
|
| |
- )
|
| |
- sub_q4 = sub_q4.filter(
|
| |
- model.PagureGroup.group_name.notin_(exclude_groups)
|
| |
- )
|
| |
-
|
| |
- projects = projects.filter(
|
| |
- model.Project.id.in_(
|
| |
- subquery0.union(sub_q1)
|
| |
- .union(sub_q2)
|
| |
- .union(sub_q3)
|
| |
- .union(sub_q4)
|
| |
- )
|
| |
- )
|
| |
-
|
| |
- if fork is not None:
|
| |
- if fork is True:
|
| |
- projects = projects.filter(
|
| |
- model.Project.is_fork == True # noqa: E712
|
| |
- )
|
| |
- elif fork is False:
|
| |
- projects = projects.filter(
|
| |
- model.Project.is_fork == False # noqa: E712
|
| |
- )
|
| |
-
|
| |
- if tags:
|
| |
- if not isinstance(tags, (list, tuple)):
|
| |
- tags = [tags]
|
| |
-
|
| |
- projects = projects.filter(
|
| |
- model.Project.id == model.TagProject.project_id
|
| |
- ).filter(model.TagProject.tag.in_(tags))
|
| |
-
|
| |
- if pattern:
|
| |
- pattern = pattern.replace("*", "%")
|
| |
- if "%" in pattern:
|
| |
- projects = projects.filter(model.Project.name.ilike(pattern))
|
| |
- else:
|
| |
- projects = projects.filter(model.Project.name == pattern)
|
| |
-
|
| |
- if namespace:
|
| |
- projects = projects.filter(model.Project.namespace == namespace)
|
| |
-
|
| |
- query = session.query(model.Project).filter(
|
| |
- model.Project.id.in_(projects.subquery())
|
| |
- )
|
| |
-
|
| |
- if sort == "latest":
|
| |
- query = query.order_by(model.Project.date_created.desc())
|
| |
- elif sort == "oldest":
|
| |
- query = query.order_by(model.Project.date_created.asc())
|
| |
- else:
|
| |
- query = query.order_by(asc(func.lower(model.Project.name)))
|
| |
-
|
| |
- if start is not None:
|
| |
- query = query.offset(start)
|
| |
-
|
| |
- if limit is not None:
|
| |
- query = query.limit(limit)
|
| |
-
|
| |
- if count:
|
| |
- return query.count()
|
| |
- else:
|
| |
- return query.all()
|
| |
-
|
| |
-
|
| |
- def _get_project(session, name, user=None, namespace=None):
|
| |
- """Get a project from the database
|
| |
- """
|
| |
- case = pagure_config.get("CASE_SENSITIVE", False)
|
| |
-
|
| |
- query = session.query(model.Project)
|
| |
-
|
| |
- if not case:
|
| |
- query = query.filter(func.lower(model.Project.name) == name.lower())
|
| |
- else:
|
| |
- query = query.filter(model.Project.name == name)
|
| |
-
|
| |
- if namespace:
|
| |
- if not case:
|
| |
- query = query.filter(
|
| |
- func.lower(model.Project.namespace) == namespace.lower()
|
| |
- )
|
| |
- else:
|
| |
- query = query.filter(model.Project.namespace == namespace)
|
| |
- else:
|
| |
- query = query.filter(model.Project.namespace == namespace)
|
| |
-
|
| |
- if user is not None:
|
| |
- query = (
|
| |
- query.filter(model.User.user == user)
|
| |
- .filter(model.User.id == model.Project.user_id)
|
| |
- .filter(model.Project.is_fork == True) # noqa: E712
|
| |
- )
|
| |
- else:
|
| |
- query = query.filter(model.Project.is_fork == False) # noqa: E712
|
| |
-
|
| |
- try:
|
| |
- return query.one()
|
| |
- except sqlalchemy.orm.exc.NoResultFound:
|
| |
- return None
|
| |
-
|
| |
-
|
| |
- def search_issues(
|
| |
- session,
|
| |
- repo=None,
|
| |
- issueid=None,
|
| |
- issueuid=None,
|
| |
- status=None,
|
| |
- closed=False,
|
| |
- tags=None,
|
| |
- assignee=None,
|
| |
- author=None,
|
| |
- private=None,
|
| |
- priority=None,
|
| |
- milestones=None,
|
| |
- count=False,
|
| |
- offset=None,
|
| |
- limit=None,
|
| |
- search_pattern=None,
|
| |
- custom_search=None,
|
| |
- updated_after=None,
|
| |
- no_milestones=None,
|
| |
- order="desc",
|
| |
- order_key=None,
|
| |
- ):
|
| |
- """ Retrieve one or more issues associated to a project with the given
|
| |
- criterias.
|
| |
-
|
| |
- Watch out that the closed argument is incompatible with the status
|
| |
- argument. The closed argument will return all the issues whose status
|
| |
- is not 'Open', otherwise it will return the issues having the specified
|
| |
- status.
|
| |
- The `tags` argument can be used to filter the issues returned based on
|
| |
- a certain tag.
|
| |
- If the `issueid` argument is specified a single Issue object (or None)
|
| |
- will be returned instead of a list of Issue objects.
|
| |
-
|
| |
- :arg session: the session to use to connect to the database.
|
| |
- :arg repo: a Project object to which the issues should be associated
|
| |
- :type repo: pagure.lib.model.Project
|
| |
- :kwarg issueid: the identifier of the issue to look for
|
| |
- :type issueid: int or None
|
| |
- :kwarg issueuid: the unique identifier of the issue to look for
|
| |
- :type issueuid: str or None
|
| |
- :kwarg status: the status of the issue to look for (incompatible with
|
| |
- the `closed` argument).
|
| |
- :type status: str or None
|
| |
- :kwarg closed: a boolean indicating whether the issue to retrieve are
|
| |
- closed or open (incompatible with the `status` argument).
|
| |
- :type closed: bool or None
|
| |
- :kwarg tags: a tag the issue(s) returned should be associated with
|
| |
- :type tags: str or list(str) or None
|
| |
- :kwarg assignee: the name of the user assigned to the issues to search
|
| |
- :type assignee: str or None
|
| |
- :kwarg author: the name of the user who created the issues to search
|
| |
- :type author: str or None
|
| |
- :kwarg private: boolean or string to use to include or exclude private
|
| |
- tickets. Defaults to False.
|
| |
- If False: private tickets are excluded
|
| |
- If None: private tickets are included
|
| |
- If user name is specified: private tickets reported by that user
|
| |
- are included.
|
| |
- :type private: False, None or str
|
| |
- :kwarg priority: the priority of the issues to search
|
| |
- :type priority: int or None
|
| |
- :kwarg milestones: a milestone the issue(s) returned should be
|
| |
- associated with.
|
| |
- :type milestones: str or list(str) or None
|
| |
- :kwarg count: a boolean to specify if the method should return the list
|
| |
- of Issues or just do a COUNT query.
|
| |
- :type count: boolean
|
| |
- :kwarg search_pattern: a string to search in issues title
|
| |
- :type search_pattern: str or None
|
| |
- :kwarg custom_search: a dictionary of key/values to be used when
|
| |
- searching issues with a custom key constraint
|
| |
- :type custom_search: dict or None
|
| |
- :kwarg updated_after: datetime's date format (e.g. 2016-11-15) used to
|
| |
- filter issues updated after that date
|
| |
- :type updated_after: str or None
|
| |
- :kwarg no_milestones: Request issues that do not have a milestone set yet
|
| |
- :type None, True, or False
|
| |
- :kwarg order: Order issues in 'asc' or 'desc' order.
|
| |
- :type order: None, str
|
| |
- :kwarg order_key: Order issues by database column
|
| |
- :type order_key: None, str
|
| |
-
|
| |
- :return: A single Issue object if issueid is specified, a list of Project
|
| |
- objects otherwise.
|
| |
- :rtype: Project or [Project]
|
| |
-
|
| |
- """
|
| |
- query = session.query(sqlalchemy.distinct(model.Issue.uid))
|
| |
-
|
| |
- if repo is not None:
|
| |
- query = query.filter(model.Issue.project_id == repo.id)
|
| |
-
|
| |
- if updated_after:
|
| |
- query = query.filter(model.Issue.last_updated >= updated_after)
|
| |
-
|
| |
- if issueid is not None:
|
| |
- query = query.filter(model.Issue.id == issueid)
|
| |
-
|
| |
- if issueuid is not None:
|
| |
- query = query.filter(model.Issue.uid == issueuid)
|
| |
-
|
| |
- if status is not None:
|
| |
- if status in ["Open", "Closed"]:
|
| |
- query = query.filter(model.Issue.status == status)
|
| |
- else:
|
| |
- query = query.filter(model.Issue.close_status == status)
|
| |
- if closed:
|
| |
- query = query.filter(model.Issue.status != "Open")
|
| |
- if priority:
|
| |
- query = query.filter(model.Issue.priority == priority)
|
| |
- if tags is not None and tags != []:
|
| |
- if isinstance(tags, six.string_types):
|
| |
- tags = [tags]
|
| |
- notags = []
|
| |
- ytags = []
|
| |
- for tag in tags:
|
| |
- if tag.startswith("!"):
|
| |
- notags.append(tag[1:])
|
| |
- else:
|
| |
- ytags.append(tag)
|
| |
-
|
| |
- if ytags:
|
| |
- sub_q2 = session.query(sqlalchemy.distinct(model.Issue.uid))
|
| |
- if repo is not None:
|
| |
- sub_q2 = sub_q2.filter(model.Issue.project_id == repo.id)
|
| |
- sub_q2 = (
|
| |
- sub_q2.filter(
|
| |
- model.Issue.uid == model.TagIssueColored.issue_uid
|
| |
- )
|
| |
- .filter(model.TagIssueColored.tag_id == model.TagColored.id)
|
| |
- .filter(model.TagColored.tag.in_(ytags))
|
| |
- )
|
| |
- if notags:
|
| |
- sub_q3 = session.query(sqlalchemy.distinct(model.Issue.uid))
|
| |
- if repo is not None:
|
| |
- sub_q3 = sub_q3.filter(model.Issue.project_id == repo.id)
|
| |
- sub_q3 = (
|
| |
- sub_q3.filter(
|
| |
- model.Issue.uid == model.TagIssueColored.issue_uid
|
| |
- )
|
| |
- .filter(model.TagIssueColored.tag_id == model.TagColored.id)
|
| |
- .filter(model.TagColored.tag.in_(notags))
|
| |
- )
|
| |
- # Adjust the main query based on the parameters specified
|
| |
- if ytags and not notags:
|
| |
- query = query.filter(model.Issue.uid.in_(sub_q2))
|
| |
- elif not ytags and notags:
|
| |
- query = query.filter(~model.Issue.uid.in_(sub_q3))
|
| |
- elif ytags and notags:
|
| |
- final_set = set([i[0] for i in sub_q2.all()]) - set(
|
| |
- [i[0] for i in sub_q3.all()]
|
| |
- )
|
| |
- if final_set:
|
| |
- query = query.filter(model.Issue.uid.in_(list(final_set)))
|
| |
-
|
| |
- if assignee is not None:
|
| |
- assignee = "%s" % assignee
|
| |
- if not pagure.utils.is_true(assignee, ["false", "0", "true", "1"]):
|
| |
- reverseassignee = False
|
| |
- if assignee.startswith("!"):
|
| |
- reverseassignee = True
|
| |
- assignee = assignee[1:]
|
| |
-
|
| |
- userassignee = (
|
| |
- session.query(model.User.id)
|
| |
- .filter(model.User.user == assignee)
|
| |
- .subquery()
|
| |
- )
|
| |
-
|
| |
- if reverseassignee:
|
| |
- sub = session.query(model.Issue.uid).filter(
|
| |
- model.Issue.assignee_id == userassignee
|
| |
- )
|
| |
-
|
| |
- query = query.filter(~model.Issue.uid.in_(sub))
|
| |
- else:
|
| |
- query = query.filter(model.Issue.assignee_id == userassignee)
|
| |
- elif pagure.utils.is_true(assignee):
|
| |
- query = query.filter(model.Issue.assignee_id.isnot(None))
|
| |
- else:
|
| |
- query = query.filter(model.Issue.assignee_id.is_(None))
|
| |
- if author is not None:
|
| |
- userauthor = (
|
| |
- session.query(model.User.id)
|
| |
- .filter(model.User.user == author)
|
| |
- .subquery()
|
| |
- )
|
| |
- query = query.filter(model.Issue.user_id == userauthor)
|
| |
-
|
| |
- if private is False:
|
| |
- query = query.filter(model.Issue.private == False) # noqa: E712
|
| |
- elif isinstance(private, six.string_types):
|
| |
- userprivate = (
|
| |
- session.query(model.User.id)
|
| |
- .filter(model.User.user == private)
|
| |
- .subquery()
|
| |
- )
|
| |
- query = query.filter(
|
| |
- sqlalchemy.or_(
|
| |
- model.Issue.private == False, # noqa: E712
|
| |
- sqlalchemy.and_(
|
| |
- model.Issue.private == True, # noqa: E712
|
| |
- model.Issue.user_id == userprivate,
|
| |
- ),
|
| |
- sqlalchemy.and_(
|
| |
- model.Issue.private == True, # noqa: E712
|
| |
- model.Issue.assignee_id == userprivate,
|
| |
- ),
|
| |
- )
|
| |
- )
|
| |
-
|
| |
- if no_milestones and milestones is not None and milestones != []:
|
| |
- # Asking for issues with no milestone or a specific milestone
|
| |
- if isinstance(milestones, six.string_types):
|
| |
- milestones = [milestones]
|
| |
- query = query.filter(
|
| |
- (model.Issue.milestone.is_(None))
|
| |
- | (model.Issue.milestone.in_(milestones))
|
| |
- )
|
| |
- elif no_milestones:
|
| |
- # Asking for issues without a milestone
|
| |
- query = query.filter(model.Issue.milestone.is_(None))
|
| |
- elif milestones is not None and milestones != []:
|
| |
- # Asking for a single specific milestone
|
| |
- if isinstance(milestones, six.string_types):
|
| |
- milestones = [milestones]
|
| |
- query = query.filter(model.Issue.milestone.in_(milestones))
|
| |
- elif no_milestones is False:
|
| |
- # Asking for all ticket with a milestone
|
| |
- query = query.filter(model.Issue.milestone.isnot(None))
|
| |
-
|
| |
- if custom_search:
|
| |
- constraints = []
|
| |
- for key in custom_search:
|
| |
- value = custom_search[key]
|
| |
- if "*" in value:
|
| |
- value = value.replace("*", "%")
|
| |
- constraints.append(
|
| |
- sqlalchemy.and_(
|
| |
- model.IssueKeys.name == key,
|
| |
- model.IssueValues.value.ilike(value),
|
| |
- )
|
| |
- )
|
| |
- else:
|
| |
- constraints.append(
|
| |
- sqlalchemy.and_(
|
| |
- model.IssueKeys.name == key,
|
| |
- model.IssueValues.value == value,
|
| |
- )
|
| |
- )
|
| |
- if constraints:
|
| |
- query = query.filter(
|
| |
- model.Issue.uid == model.IssueValues.issue_uid
|
| |
- ).filter(model.IssueValues.key_id == model.IssueKeys.id)
|
| |
- query = query.filter(
|
| |
- sqlalchemy.or_((const for const in constraints))
|
| |
- )
|
| |
-
|
| |
- query = session.query(model.Issue).filter(
|
| |
- model.Issue.uid.in_(query.subquery())
|
| |
- )
|
| |
-
|
| |
- if repo is not None:
|
| |
- query = query.filter(model.Issue.project_id == repo.id)
|
| |
-
|
| |
- if search_pattern is not None:
|
| |
- query = query.filter(
|
| |
- model.Issue.title.ilike("%%%s%%" % search_pattern)
|
| |
- )
|
| |
-
|
| |
- column = model.Issue.date_created
|
| |
- if order_key:
|
| |
- # If we are ordering by assignee, then order by the assignees'
|
| |
- # usernames
|
| |
- if order_key == "assignee":
|
| |
- # We must do a LEFT JOIN on model.Issue.assignee because there are
|
| |
- # two foreign keys on model.Issue tied to model.User. This tells
|
| |
- # SQLAlchemy which foreign key on model.User to order on.
|
| |
- query = query.outerjoin(
|
| |
- model.User, model.Issue.assignee_id == model.User.id
|
| |
- )
|
| |
- column = model.User.user
|
| |
- # If we are ordering by user, then order by reporters' usernames
|
| |
- elif order_key == "user":
|
| |
- # We must do a LEFT JOIN on model.Issue.user because there are
|
| |
- # two foreign keys on model.Issue tied to model.User. This tells
|
| |
- # SQLAlchemy which foreign key on model.User to order on.
|
| |
- query = query.outerjoin(
|
| |
- model.User, model.Issue.user_id == model.User.id
|
| |
- )
|
| |
- column = model.User.user
|
| |
- elif order_key in model.Issue.__table__.columns.keys():
|
| |
- column = getattr(model.Issue, order_key)
|
| |
-
|
| |
- if ("%s" % column.type) == "TEXT":
|
| |
- column = func.lower(column)
|
| |
-
|
| |
- # The priority is sorted differently because it is by weight and the lower
|
| |
- # the number, the higher the priority
|
| |
- if (order_key != "priority" and order == "asc") or (
|
| |
- order_key == "priority" and order == "desc"
|
| |
- ):
|
| |
- query = query.order_by(asc(column))
|
| |
- else:
|
| |
- query = query.order_by(desc(column))
|
| |
-
|
| |
- if issueid is not None or issueuid is not None:
|
| |
- output = query.first()
|
| |
- elif count:
|
| |
- output = query.count()
|
| |
- else:
|
| |
- if offset is not None:
|
| |
- query = query.offset(offset)
|
| |
- if limit:
|
| |
- query = query.limit(limit)
|
| |
- output = query.all()
|
| |
-
|
| |
- return output
|
| |
-
|
| |
-
|
| |
- def get_tags_of_project(session, project, pattern=None):
|
| |
- """ Returns the list of tags associated with the issues of a project.
|
| |
- """
|
| |
- query = (
|
| |
- session.query(model.TagColored)
|
| |
- .filter(model.TagColored.tag != "")
|
| |
- .filter(model.TagColored.project_id == project.id)
|
| |
- .order_by(model.TagColored.tag)
|
| |
- )
|
| |
-
|
| |
- if pattern:
|
| |
- query = query.filter(
|
| |
- model.TagColored.tag.ilike(pattern.replace("*", "%"))
|
| |
- )
|
| |
-
|
| |
- return query.all()
|
| |
-
|
| |
-
|
| |
- def get_tag(session, tag):
|
| |
- """ Returns a Tag object for the given tag text.
|
| |
- """
|
| |
- query = session.query(model.Tag).filter(model.Tag.tag == tag)
|
| |
-
|
| |
- return query.first()
|
| |
-
|
| |
-
|
| |
- def get_colored_tag(session, tag, project_id):
|
| |
- """ Returns a TagColored object for the given tag text.
|
| |
- """
|
| |
- query = (
|
| |
- session.query(model.TagColored)
|
| |
- .filter(model.TagColored.tag == tag)
|
| |
- .filter(model.TagColored.project_id == project_id)
|
| |
- )
|
| |
-
|
| |
- return query.first()
|
| |
-
|
| |
-
|
| |
- def search_pull_requests(
|
| |
- session,
|
| |
- requestid=None,
|
| |
- project_id=None,
|
| |
- project_id_from=None,
|
| |
- status=None,
|
| |
- author=None,
|
| |
- assignee=None,
|
| |
- count=False,
|
| |
- offset=None,
|
| |
- limit=None,
|
| |
- updated_after=None,
|
| |
- branch_from=None,
|
| |
- order="desc",
|
| |
- order_key=None,
|
| |
- search_pattern=None,
|
| |
- ):
|
| |
- """ Retrieve the specified pull-requests.
|
| |
- """
|
| |
-
|
| |
- query = session.query(model.PullRequest)
|
| |
-
|
| |
- # by default sort request by date_created.
|
| |
- column = model.PullRequest.date_created
|
| |
-
|
| |
- if order_key == "last_updated":
|
| |
- column = model.PullRequest.last_updated
|
| |
-
|
| |
- if requestid:
|
| |
- query = query.filter(model.PullRequest.id == requestid)
|
| |
-
|
| |
- if updated_after:
|
| |
- query = query.filter(model.PullRequest.last_updated >= updated_after)
|
| |
-
|
| |
- if project_id:
|
| |
- query = query.filter(model.PullRequest.project_id == project_id)
|
| |
-
|
| |
- if project_id_from:
|
| |
- query = query.filter(
|
| |
- model.PullRequest.project_id_from == project_id_from
|
| |
- )
|
| |
-
|
| |
- if status is not None:
|
| |
- if isinstance(status, bool):
|
| |
- if status:
|
| |
- query = query.filter(model.PullRequest.status == "Open")
|
| |
- else:
|
| |
- query = query.filter(model.PullRequest.status != "Open")
|
| |
- else:
|
| |
- query = query.filter(model.PullRequest.status == status)
|
| |
-
|
| |
- if assignee is not None:
|
| |
- assignee = "%s" % assignee
|
| |
- if not pagure.utils.is_true(assignee, ["false", "0", "true", "1"]):
|
| |
- user2 = aliased(model.User)
|
| |
- if assignee.startswith("!"):
|
| |
- sub = (
|
| |
- session.query(model.PullRequest.uid)
|
| |
- .filter(model.PullRequest.assignee_id == user2.id)
|
| |
- .filter(user2.user == assignee[1:])
|
| |
- )
|
| |
-
|
| |
- query = query.filter(~model.PullRequest.uid.in_(sub))
|
| |
- else:
|
| |
- query = query.filter(
|
| |
- model.PullRequest.assignee_id == user2.id
|
| |
- ).filter(user2.user == assignee)
|
| |
- elif pagure.utils.is_true(assignee):
|
| |
- query = query.filter(model.PullRequest.assignee_id.isnot(None))
|
| |
- else:
|
| |
- query = query.filter(model.PullRequest.assignee_id.is_(None))
|
| |
-
|
| |
- if author is not None:
|
| |
- user3 = aliased(model.User)
|
| |
- query = query.filter(model.PullRequest.user_id == user3.id).filter(
|
| |
- user3.user == author
|
| |
- )
|
| |
-
|
| |
- if branch_from is not None:
|
| |
- query = query.filter(model.PullRequest.branch_from == branch_from)
|
| |
-
|
| |
- if search_pattern is not None:
|
| |
- if "*" in search_pattern:
|
| |
- search_pattern = search_pattern.replace("*", "%")
|
| |
- else:
|
| |
- search_pattern = "%%%s%%" % search_pattern
|
| |
- query = query.filter(model.PullRequest.title.ilike(search_pattern))
|
| |
-
|
| |
- # Depending on the order, the query is sorted(default is desc)
|
| |
- if order == "asc":
|
| |
- query = query.order_by(asc(column))
|
| |
- else:
|
| |
- query = query.order_by(desc(column))
|
| |
-
|
| |
- if requestid:
|
| |
- output = query.first()
|
| |
- elif count:
|
| |
- output = query.count()
|
| |
- else:
|
| |
- if offset:
|
| |
- query = query.offset(offset)
|
| |
- if limit:
|
| |
- query = query.limit(limit)
|
| |
- output = query.all()
|
| |
-
|
| |
- return output
|
| |
-
|
| |
-
|
| |
- def reopen_pull_request(session, request, user):
|
| |
- """ Re-Open the provided pull request
|
| |
- """
|
| |
- if request.status != "Closed":
|
| |
- raise pagure.exceptions.PagureException(
|
| |
- "Trying to reopen a pull request that is not closed"
|
| |
- )
|
| |
- user_obj = get_user(session, user)
|
| |
- request.status = "Open"
|
| |
- session.add(request)
|
| |
- session.flush()
|
| |
- log_action(session, request.status.lower(), request, user_obj)
|
| |
- pagure.lib.notify.notify_reopen_pull_request(request, user_obj)
|
| |
- pagure.lib.git.update_git(request, repo=request.project)
|
| |
- pagure.lib.add_pull_request_comment(
|
| |
- session,
|
| |
- request,
|
| |
- commit=None,
|
| |
- tree_id=None,
|
| |
- filename=None,
|
| |
- row=None,
|
| |
- comment="Pull-Request has been reopened by %s" % (user),
|
| |
- user=user,
|
| |
- notify=False,
|
| |
- notification=True,
|
| |
- )
|
| |
- pagure.lib.notify.log(
|
| |
- request.project,
|
| |
- topic="pull-request.reopened",
|
| |
- msg=dict(
|
| |
- pullrequest=request.to_json(public=True), agent=user_obj.username
|
| |
- ),
|
| |
- redis=REDIS,
|
| |
- )
|
| |
-
|
| |
-
|
| |
- def close_pull_request(session, request, user, merged=True):
|
| |
- """ Close the provided pull-request.
|
| |
- """
|
| |
- user_obj = get_user(session, user)
|
| |
-
|
| |
- if merged is True:
|
| |
- request.status = "Merged"
|
| |
- else:
|
| |
- request.status = "Closed"
|
| |
- request.closed_by_id = user_obj.id
|
| |
- request.closed_at = datetime.datetime.utcnow()
|
| |
- session.add(request)
|
| |
- session.flush()
|
| |
-
|
| |
- log_action(session, request.status.lower(), request, user_obj)
|
| |
-
|
| |
- if merged is True:
|
| |
- pagure.lib.notify.notify_merge_pull_request(request, user_obj)
|
| |
- else:
|
| |
- pagure.lib.notify.notify_cancelled_pull_request(request, user_obj)
|
| |
-
|
| |
- pagure.lib.git.update_git(request, repo=request.project)
|
| |
-
|
| |
- pagure.lib.add_pull_request_comment(
|
| |
- session,
|
| |
- request,
|
| |
- commit=None,
|
| |
- tree_id=None,
|
| |
- filename=None,
|
| |
- row=None,
|
| |
- comment="Pull-Request has been %s by %s"
|
| |
- % (request.status.lower(), user),
|
| |
- user=user,
|
| |
- notify=False,
|
| |
- notification=True,
|
| |
- )
|
| |
-
|
| |
- pagure.lib.notify.log(
|
| |
- request.project,
|
| |
- topic="pull-request.closed",
|
| |
- msg=dict(
|
| |
- pullrequest=request.to_json(public=True),
|
| |
- merged=merged,
|
| |
- agent=user_obj.username,
|
| |
- ),
|
| |
- redis=REDIS,
|
| |
- )
|
| |
-
|
| |
-
|
| |
- def reset_status_pull_request(session, project):
|
| |
- """ Reset the status of all opened Pull-Requests of a project.
|
| |
- """
|
| |
- session.query(model.PullRequest).filter(
|
| |
- model.PullRequest.project_id == project.id
|
| |
- ).filter(model.PullRequest.status == "Open").update(
|
| |
- {model.PullRequest.merge_status: None}
|
| |
- )
|
| |
-
|
| |
- session.commit()
|
| |
-
|
| |
-
|
| |
- def add_attachment(repo, issue, attachmentfolder, user, filename, filestream):
|
| |
- """ Add a file to the attachments folder of repo and update git. """
|
| |
- _log.info(
|
| |
- "Adding file: %s to the git repo: %s",
|
| |
- repo.path,
|
| |
- werkzeug.secure_filename(filename),
|
| |
- )
|
| |
-
|
| |
- # Prefix the filename with a timestamp:
|
| |
- filename = "%s-%s" % (
|
| |
- hashlib.sha256(filestream.read()).hexdigest(),
|
| |
- werkzeug.secure_filename(filename),
|
| |
- )
|
| |
- filedir = os.path.join(attachmentfolder, repo.fullname, "files")
|
| |
- filepath = os.path.join(filedir, filename)
|
| |
-
|
| |
- if os.path.exists(filepath):
|
| |
- return filename
|
| |
-
|
| |
- if not os.path.exists(filedir):
|
| |
- os.makedirs(filedir)
|
| |
-
|
| |
- # Write file
|
| |
- filestream.seek(0)
|
| |
- with open(filepath, "wb") as stream:
|
| |
- stream.write(filestream.read())
|
| |
-
|
| |
- tasks.add_file_to_git.delay(
|
| |
- repo.name,
|
| |
- repo.namespace,
|
| |
- repo.user.username if repo.is_fork else None,
|
| |
- user.username,
|
| |
- issue.uid,
|
| |
- filename,
|
| |
- )
|
| |
-
|
| |
- return filename
|
| |
-
|
| |
-
|
| |
- def get_issue_statuses(session):
|
| |
- """ Return the complete list of status an issue can have.
|
| |
- """
|
| |
- output = []
|
| |
- statuses = session.query(model.StatusIssue).all()
|
| |
- for status in statuses:
|
| |
- output.append(status.status)
|
| |
- return output
|
| |
-
|
| |
-
|
| |
- def get_issue_comment(session, issue_uid, comment_id):
|
| |
- """ Return a specific comment of a specified issue.
|
| |
- """
|
| |
- query = (
|
| |
- session.query(model.IssueComment)
|
| |
- .filter(model.IssueComment.issue_uid == issue_uid)
|
| |
- .filter(model.IssueComment.id == comment_id)
|
| |
- )
|
| |
-
|
| |
- return query.first()
|
| |
-
|
| |
-
|
| |
- def get_issue_comment_by_user_and_comment(
|
| |
- session, issue_uid, user_id, content
|
| |
- ):
|
| |
- """ Return a specific comment of a specified issue.
|
| |
- """
|
| |
- query = (
|
| |
- session.query(model.IssueComment)
|
| |
- .filter(model.IssueComment.issue_uid == issue_uid)
|
| |
- .filter(model.IssueComment.user_id == user_id)
|
| |
- .filter(model.IssueComment.comment == content)
|
| |
- )
|
| |
-
|
| |
- return query.first()
|
| |
-
|
| |
-
|
| |
- def get_request_comment(session, request_uid, comment_id):
|
| |
- """ Return a specific comment of a specified request.
|
| |
- """
|
| |
- query = (
|
| |
- session.query(model.PullRequestComment)
|
| |
- .filter(model.PullRequestComment.pull_request_uid == request_uid)
|
| |
- .filter(model.PullRequestComment.id == comment_id)
|
| |
- )
|
| |
-
|
| |
- return query.first()
|
| |
-
|
| |
-
|
| |
- def get_issue_by_uid(session, issue_uid):
|
| |
- """ Return the issue corresponding to the specified unique identifier.
|
| |
-
|
| |
- :arg session: the session to use to connect to the database.
|
| |
- :arg issue_uid: the unique identifier of an issue. This identifier is
|
| |
- unique accross all projects on this pagure instance and should be
|
| |
- unique accross multiple pagure instances as well
|
| |
- :type issue_uid: str or None
|
| |
-
|
| |
- :return: A single Issue object.
|
| |
- :rtype: pagure.lib.model.Issue
|
| |
-
|
| |
- """
|
| |
- query = session.query(model.Issue).filter(model.Issue.uid == issue_uid)
|
| |
- return query.first()
|
| |
-
|
| |
-
|
| |
- def get_request_by_uid(session, request_uid):
|
| |
- """ Return the request corresponding to the specified unique identifier.
|
| |
-
|
| |
- :arg session: the session to use to connect to the database.
|
| |
- :arg request_uid: the unique identifier of a request. This identifier is
|
| |
- unique accross all projects on this pagure instance and should be
|
| |
- unique accross multiple pagure instances as well
|
| |
- :type request_uid: str or None
|
| |
-
|
| |
- :return: A single Issue object.
|
| |
- :rtype: pagure.lib.model.PullRequest
|
| |
-
|
| |
- """
|
| |
- query = session.query(model.PullRequest).filter(
|
| |
- model.PullRequest.uid == request_uid
|
| |
- )
|
| |
- return query.first()
|
| |
-
|
| |
-
|
| |
- def get_pull_request_flag_by_uid(session, request, flag_uid):
|
| |
- """ Return the flag corresponding to the specified unique identifier.
|
| |
-
|
| |
- :arg session: the session to use to connect to the database.
|
| |
- :arg request: the pull-request that was flagged
|
| |
- :arg flag_uid: the unique identifier of a request. This identifier is
|
| |
- unique accross all flags on this pagure instance and should be
|
| |
- unique accross multiple pagure instances as well
|
| |
- :type request_uid: str or None
|
| |
-
|
| |
- :return: A single Issue object.
|
| |
- :rtype: pagure.lib.model.PullRequestFlag
|
| |
-
|
| |
- """
|
| |
- query = (
|
| |
- session.query(model.PullRequestFlag)
|
| |
- .filter(model.PullRequestFlag.pull_request_uid == request.uid)
|
| |
- .filter(model.PullRequestFlag.uid == flag_uid.strip())
|
| |
- )
|
| |
- return query.first()
|
| |
-
|
| |
-
|
| |
- def get_commit_flag_by_uid(session, commit_hash, flag_uid):
|
| |
- """ Return the flag corresponding to the specified unique identifier.
|
| |
-
|
| |
- :arg session: the session to use to connect to the database.
|
| |
- :arg commit_hash: the hash of the commit that got flagged
|
| |
- :arg flag_uid: the unique identifier of a request. This identifier is
|
| |
- unique accross all flags on this pagure instance and should be
|
| |
- unique accross multiple pagure instances as well
|
| |
- :type request_uid: str or None
|
| |
-
|
| |
- :return: A single Issue object.
|
| |
- :rtype: pagure.lib.model.PullRequestFlag
|
| |
-
|
| |
- """
|
| |
- query = (
|
| |
- session.query(model.CommitFlag)
|
| |
- .filter(model.CommitFlag.commit_hash == commit_hash)
|
| |
- .filter(model.CommitFlag.uid == flag_uid.strip() if flag_uid else None)
|
| |
- )
|
| |
- return query.first()
|
| |
-
|
| |
-
|
| |
- def set_up_user(
|
| |
- session,
|
| |
- username,
|
| |
- fullname,
|
| |
- default_email,
|
| |
- emails=None,
|
| |
- ssh_key=None,
|
| |
- keydir=None,
|
| |
- ):
|
| |
- """ Set up a new user into the database or update its information. """
|
| |
- user = search_user(session, username=username)
|
| |
- if not user:
|
| |
- user = model.User(
|
| |
- user=username, fullname=fullname, default_email=default_email
|
| |
- )
|
| |
- session.add(user)
|
| |
- session.flush()
|
| |
-
|
| |
- if user.fullname != fullname:
|
| |
- user.fullname = fullname
|
| |
- session.add(user)
|
| |
- session.flush()
|
| |
-
|
| |
- if emails:
|
| |
- emails = set(emails)
|
| |
- else:
|
| |
- emails = set()
|
| |
- emails.add(default_email)
|
| |
- for email in emails:
|
| |
- try:
|
| |
- add_email_to_user(session, user, email)
|
| |
- except pagure.exceptions.PagureException as err:
|
| |
- _log.exception(err)
|
| |
-
|
| |
- if ssh_key and not user.sshkeys:
|
| |
- update_user_ssh(session, user, ssh_key, keydir)
|
| |
-
|
| |
- return user
|
| |
-
|
| |
-
|
| |
- def allowed_emailaddress(email):
|
| |
- """ check if email domains are restricted and if a given email address
|
| |
- is allowed. """
|
| |
- allowed_email_domains = pagure_config.get("ALLOWED_EMAIL_DOMAINS", None)
|
| |
- if allowed_email_domains:
|
| |
- for domain in allowed_email_domains:
|
| |
- if email.endswith(domain):
|
| |
- return
|
| |
- raise pagure.exceptions.PagureException(
|
| |
- "The email address "
|
| |
- + email
|
| |
- + " "
|
| |
- + "is not in the list of allowed email domains:\n"
|
| |
- + "\n".join(allowed_email_domains)
|
| |
- )
|
| |
-
|
| |
-
|
| |
- def add_email_to_user(session, user, user_email):
|
| |
- """ Add the provided email to the specified user. """
|
| |
- try:
|
| |
- allowed_emailaddress(user_email)
|
| |
- except pagure.exceptions.PagureException:
|
| |
- raise
|
| |
- emails = [email.email for email in user.emails]
|
| |
- if user_email not in emails:
|
| |
- useremail = model.UserEmail(user_id=user.id, email=user_email)
|
| |
- session.add(useremail)
|
| |
- session.flush()
|
| |
- if email_logs_count(session, user_email):
|
| |
- update_log_email_user(session, user_email, user)
|
| |
-
|
| |
-
|
| |
- def update_user_ssh(session, user, ssh_key, keydir, update_only=False):
|
| |
- """ Set up a new user into the database or update its information. """
|
| |
- if isinstance(user, six.string_types):
|
| |
- user = get_user(session, user)
|
| |
-
|
| |
- if ssh_key:
|
| |
- for key in user.sshkeys:
|
| |
- session.delete(key)
|
| |
- for key in ssh_key.strip().split("\n"):
|
| |
- key = key.strip()
|
| |
- pagure.lib.add_sshkey_to_project_or_user(
|
| |
- session=session,
|
| |
- ssh_key=key,
|
| |
- user=user,
|
| |
- pushaccess=True,
|
| |
- creator=user,
|
| |
- )
|
| |
- session.commit()
|
| |
-
|
| |
- if keydir:
|
| |
- create_user_ssh_keys_on_disk(user, keydir)
|
| |
- if update_only:
|
| |
- pagure.lib.tasks.gitolite_post_compile_only.delay()
|
| |
- else:
|
| |
- pagure.lib.git.generate_gitolite_acls(project=None)
|
| |
- session.add(user)
|
| |
- session.flush()
|
| |
-
|
| |
-
|
| |
- def avatar_url_from_email(email, size=64, default="retro", dns=False):
|
| |
- """
|
| |
- Our own implementation since fas doesn't support this nicely yet.
|
| |
- """
|
| |
- if dns: # pragma: no cover
|
| |
- # This makes an extra DNS SRV query, which can slow down our webapps.
|
| |
- # It is necessary for libravatar federation, though.
|
| |
- import libravatar
|
| |
-
|
| |
- return libravatar.libravatar_url(
|
| |
- openid=email, size=size, default=default
|
| |
- )
|
| |
- else:
|
| |
- query = urlencode({"s": size, "d": default})
|
| |
- email = email.encode("utf-8")
|
| |
- hashhex = hashlib.sha256(email).hexdigest()
|
| |
- return "https://seccdn.libravatar.org/avatar/%s?%s" % (hashhex, query)
|
| |
-
|
| |
-
|
| |
- def update_tags(session, obj, tags, username):
|
| |
- """ Update the tags of a specified object (adding or removing them).
|
| |
- This object can be either an issue or a project.
|
| |
-
|
| |
- """
|
| |
- if isinstance(tags, six.string_types):
|
| |
- tags = [tags]
|
| |
-
|
| |
- toadd = set(tags) - set(obj.tags_text)
|
| |
- torm = set(obj.tags_text) - set(tags)
|
| |
- messages = []
|
| |
- if toadd:
|
| |
- add_tag_obj(session, obj=obj, tags=toadd, user=username)
|
| |
- messages.append(
|
| |
- "%s tagged with: %s"
|
| |
- % (obj.isa.capitalize(), ", ".join(sorted(toadd)))
|
| |
- )
|
| |
-
|
| |
- if torm:
|
| |
- remove_tags_obj(session, obj=obj, tags=torm, user=username)
|
| |
- messages.append(
|
| |
- "%s **un**tagged with: %s"
|
| |
- % (obj.isa.capitalize(), ", ".join(sorted(torm)))
|
| |
- )
|
| |
-
|
| |
- session.commit()
|
| |
-
|
| |
- return messages
|
| |
-
|
| |
-
|
| |
- def update_dependency_issue(session, repo, issue, depends, username):
|
| |
- """ Update the dependency of a specified issue (adding or removing them)
|
| |
-
|
| |
- """
|
| |
- if isinstance(depends, six.string_types):
|
| |
- depends = [depends]
|
| |
-
|
| |
- toadd = set(depends) - set(issue.depending_text)
|
| |
- torm = set(issue.depending_text) - set(depends)
|
| |
- messages = []
|
| |
-
|
| |
- # Add issue depending
|
| |
- for depend in sorted([int(i) for i in toadd]):
|
| |
- messages.append("Issue marked as depending on: #%s" % depend)
|
| |
- issue_depend = search_issues(session, repo, issueid=depend)
|
| |
- if issue_depend is None:
|
| |
- continue
|
| |
- if issue_depend.id in issue.depending_text: # pragma: no cover
|
| |
- # we should never be in this case but better safe than sorry...
|
| |
- continue
|
| |
-
|
| |
- add_issue_dependency(
|
| |
- session, issue=issue_depend, issue_blocked=issue, user=username
|
| |
- )
|
| |
-
|
| |
- # Remove issue depending
|
| |
- for depend in sorted([int(i) for i in torm]):
|
| |
- messages.append("Issue **un**marked as depending on: #%s" % depend)
|
| |
- issue_depend = search_issues(session, repo, issueid=depend)
|
| |
- if issue_depend is None: # pragma: no cover
|
| |
- # We cannot test this as it would mean we managed to put in an
|
| |
- # invalid ticket as dependency earlier
|
| |
- continue
|
| |
- if issue_depend.id not in issue.depending_text: # pragma: no cover
|
| |
- # we should never be in this case but better safe than sorry...
|
| |
- continue
|
| |
-
|
| |
- remove_issue_dependency(
|
| |
- session, issue=issue, issue_blocked=issue_depend, user=username
|
| |
- )
|
| |
-
|
| |
- session.commit()
|
| |
- return messages
|
| |
-
|
| |
-
|
| |
- def update_blocked_issue(session, repo, issue, blocks, username):
|
| |
- """ Update the upstream dependency of a specified issue (adding or
|
| |
- removing them)
|
| |
-
|
| |
- """
|
| |
- if isinstance(blocks, six.string_types):
|
| |
- blocks = [blocks]
|
| |
-
|
| |
- toadd = set(blocks) - set(issue.blocking_text)
|
| |
- torm = set(issue.blocking_text) - set(blocks)
|
| |
- messages = []
|
| |
-
|
| |
- # Add issue blocked
|
| |
- for block in sorted([int(i) for i in toadd]):
|
| |
- messages.append("Issue marked as blocking: #%s" % block)
|
| |
- issue_block = search_issues(session, repo, issueid=block)
|
| |
- if issue_block is None:
|
| |
- continue
|
| |
- if issue_block.id in issue.blocking_text: # pragma: no cover
|
| |
- # we should never be in this case but better safe than sorry...
|
| |
- continue
|
| |
-
|
| |
- add_issue_dependency(
|
| |
- session, issue=issue, issue_blocked=issue_block, user=username
|
| |
- )
|
| |
- session.commit()
|
| |
-
|
| |
- # Remove issue blocked
|
| |
- for block in sorted([int(i) for i in torm]):
|
| |
- messages.append("Issue **un**marked as blocking: #%s" % block)
|
| |
- issue_block = search_issues(session, repo, issueid=block)
|
| |
- if issue_block is None: # pragma: no cover
|
| |
- # We cannot test this as it would mean we managed to put in an
|
| |
- # invalid ticket as dependency earlier
|
| |
- continue
|
| |
-
|
| |
- if issue_block.id not in issue.blocking_text: # pragma: no cover
|
| |
- # we should never be in this case but better safe than sorry...
|
| |
- continue
|
| |
-
|
| |
- remove_issue_dependency(
|
| |
- session, issue=issue_block, issue_blocked=issue, user=username
|
| |
- )
|
| |
-
|
| |
- session.commit()
|
| |
- return messages
|
| |
-
|
| |
-
|
| |
- def add_user_pending_email(session, userobj, email):
|
| |
- """ Add the provided email to the specified user.
|
| |
- """
|
| |
- try:
|
| |
- allowed_emailaddress(email)
|
| |
- except pagure.exceptions.PagureException:
|
| |
- raise
|
| |
- other_user = search_user(session, email=email)
|
| |
- if other_user and other_user != userobj:
|
| |
- raise pagure.exceptions.PagureException(
|
| |
- "Someone else has already registered this email"
|
| |
- )
|
| |
-
|
| |
- pending_email = search_pending_email(session, email=email)
|
| |
- if pending_email:
|
| |
- raise pagure.exceptions.PagureException(
|
| |
- "This email is already pending confirmation"
|
| |
- )
|
| |
-
|
| |
- tmpemail = pagure.lib.model.UserEmailPending(
|
| |
- user_id=userobj.id,
|
| |
- token=pagure.lib.login.id_generator(40),
|
| |
- email=email,
|
| |
- )
|
| |
- session.add(tmpemail)
|
| |
- session.flush()
|
| |
-
|
| |
- pagure.lib.notify.notify_new_email(tmpemail, user=userobj)
|
| |
-
|
| |
-
|
| |
- def resend_pending_email(session, userobj, email):
|
| |
- """ Resend to the user the confirmation email for the provided email
|
| |
- address.
|
| |
- """
|
| |
- other_user = search_user(session, email=email)
|
| |
- if other_user and other_user != userobj:
|
| |
- raise pagure.exceptions.PagureException(
|
| |
- "Someone else has already registered this email address"
|
| |
- )
|
| |
-
|
| |
- pending_email = search_pending_email(session, email=email)
|
| |
- if not pending_email:
|
| |
- raise pagure.exceptions.PagureException(
|
| |
- "This email address has already been confirmed"
|
| |
- )
|
| |
-
|
| |
- pending_email.token = pagure.lib.login.id_generator(40)
|
| |
- session.add(pending_email)
|
| |
- session.flush()
|
| |
-
|
| |
- pagure.lib.notify.notify_new_email(pending_email, user=userobj)
|
| |
-
|
| |
-
|
| |
- def search_pending_email(session, email=None, token=None):
|
| |
- """ Searches the database for the pending email matching the given
|
| |
- criterias.
|
| |
-
|
| |
- :arg session: the session to use to connect to the database.
|
| |
- :kwarg email: the email to look for
|
| |
- :type email: string or None
|
| |
- :kwarg token: the token of the pending email to look for
|
| |
- :type token: string or None
|
| |
- :return: A single UserEmailPending object
|
| |
- :rtype: UserEmailPending
|
| |
-
|
| |
- """
|
| |
- query = session.query(model.UserEmailPending)
|
| |
-
|
| |
- if email is not None:
|
| |
- query = query.filter(model.UserEmailPending.email == email)
|
| |
-
|
| |
- if token is not None:
|
| |
- query = query.filter(model.UserEmailPending.token == token)
|
| |
-
|
| |
- output = query.first()
|
| |
-
|
| |
- return output
|
| |
-
|
| |
-
|
| |
- def generate_hook_token(session):
|
| |
- """ For each project in the database, re-generate a unique hook_token.
|
| |
-
|
| |
- """
|
| |
-
|
| |
- for project in search_projects(session):
|
| |
- project.hook_token = pagure.lib.login.id_generator(40)
|
| |
- session.add(project)
|
| |
- session.commit()
|
| |
-
|
| |
-
|
| |
- def get_group_types(session, group_type=None):
|
| |
- """ Return the list of type a group can have.
|
| |
-
|
| |
- """
|
| |
- query = session.query(model.PagureGroupType).order_by(
|
| |
- model.PagureGroupType.group_type
|
| |
- )
|
| |
-
|
| |
- if group_type:
|
| |
- query = query.filter(model.PagureGroupType.group_type == group_type)
|
| |
-
|
| |
- return query.all()
|
| |
-
|
| |
-
|
| |
- def search_groups(
|
| |
- session,
|
| |
- pattern=None,
|
| |
- group_name=None,
|
| |
- group_type=None,
|
| |
- display_name=None,
|
| |
- offset=None,
|
| |
- limit=None,
|
| |
- count=False,
|
| |
- ):
|
| |
- """ Return the groups based on the criteria specified.
|
| |
-
|
| |
- """
|
| |
- query = session.query(model.PagureGroup).order_by(
|
| |
- model.PagureGroup.group_type
|
| |
- )
|
| |
-
|
| |
- if pattern:
|
| |
- pattern = pattern.replace("*", "%")
|
| |
- query = query.filter(
|
| |
- sqlalchemy.or_(
|
| |
- model.PagureGroup.group_name.ilike(pattern),
|
| |
- model.PagureGroup.display_name.ilike(pattern),
|
| |
- )
|
| |
- )
|
| |
-
|
| |
- if group_name:
|
| |
- query = query.filter(model.PagureGroup.group_name == group_name)
|
| |
-
|
| |
- if display_name:
|
| |
- query = query.filter(model.PagureGroup.display_name == display_name)
|
| |
-
|
| |
- if group_type:
|
| |
- query = query.filter(model.PagureGroup.group_type == group_type)
|
| |
-
|
| |
- if offset:
|
| |
- query = query.offset(offset)
|
| |
- if limit:
|
| |
- query = query.limit(limit)
|
| |
-
|
| |
- if group_name:
|
| |
- return query.first()
|
| |
- elif count:
|
| |
- return query.count()
|
| |
- else:
|
| |
- return query.all()
|
| |
-
|
| |
-
|
| |
- def add_user_to_group(
|
| |
- session, username, group, user, is_admin, from_external=False
|
| |
- ):
|
| |
- """ Add the specified user to the given group.
|
| |
-
|
| |
- from_external indicates whether this is a remotely synced group.
|
| |
- """
|
| |
- new_user = search_user(session, username=username)
|
| |
- if not new_user:
|
| |
- raise pagure.exceptions.PagureException(
|
| |
- "No user `%s` found" % username
|
| |
- )
|
| |
-
|
| |
- action_user = user
|
| |
- user = search_user(session, username=user)
|
| |
- if not user:
|
| |
- raise pagure.exceptions.PagureException(
|
| |
- "No user `%s` found" % action_user
|
| |
- )
|
| |
-
|
| |
- if (
|
| |
- not from_external
|
| |
- and group.group_name not in user.groups
|
| |
- and not is_admin
|
| |
- and user.username != group.creator.username
|
| |
- ):
|
| |
- raise pagure.exceptions.PagureException(
|
| |
- "You are not allowed to add user to this group"
|
| |
- )
|
| |
-
|
| |
- for guser in group.users:
|
| |
- if guser.username == new_user.username:
|
| |
- return "User `%s` already in the group, nothing to change." % (
|
| |
- new_user.username
|
| |
- )
|
| |
-
|
| |
- grp = model.PagureUserGroup(group_id=group.id, user_id=new_user.id)
|
| |
- session.add(grp)
|
| |
- session.flush()
|
| |
- return "User `%s` added to the group `%s`." % (
|
| |
- new_user.username,
|
| |
- group.group_name,
|
| |
- )
|
| |
-
|
| |
-
|
| |
- def edit_group_info(session, group, display_name, description, user, is_admin):
|
| |
- """ Edit the information regarding a given group.
|
| |
- """
|
| |
- action_user = user
|
| |
- user = search_user(session, username=user)
|
| |
- if not user:
|
| |
- raise pagure.exceptions.PagureException(
|
| |
- "No user `%s` found" % action_user
|
| |
- )
|
| |
-
|
| |
- if (
|
| |
- group.group_name not in user.groups
|
| |
- and not is_admin
|
| |
- and user.username != group.creator.username
|
| |
- ):
|
| |
- raise pagure.exceptions.PagureException(
|
| |
- "You are not allowed to edit this group"
|
| |
- )
|
| |
-
|
| |
- edits = []
|
| |
- if display_name and display_name != group.display_name:
|
| |
- group.display_name = display_name
|
| |
- edits.append("display_name")
|
| |
- if description and description != group.description:
|
| |
- group.description = description
|
| |
- edits.append("description")
|
| |
-
|
| |
- session.add(group)
|
| |
- session.flush()
|
| |
-
|
| |
- msg = "Nothing changed"
|
| |
- if edits:
|
| |
- pagure.lib.notify.log(
|
| |
- None,
|
| |
- topic="group.edit",
|
| |
- msg=dict(
|
| |
- group=group.to_json(public=True),
|
| |
- fields=edits,
|
| |
- agent=user.username,
|
| |
- ),
|
| |
- redis=REDIS,
|
| |
- )
|
| |
- msg = 'Group "%s" (%s) edited' % (group.display_name, group.group_name)
|
| |
-
|
| |
- return msg
|
| |
-
|
| |
-
|
| |
- def delete_user_of_group(
|
| |
- session,
|
| |
- username,
|
| |
- groupname,
|
| |
- user,
|
| |
- is_admin,
|
| |
- force=False,
|
| |
- from_external=False,
|
| |
- ):
|
| |
- """ Removes the specified user from the given group.
|
| |
- """
|
| |
- group_obj = search_groups(session, group_name=groupname)
|
| |
-
|
| |
- if not group_obj:
|
| |
- raise pagure.exceptions.PagureException(
|
| |
- "No group `%s` found" % groupname
|
| |
- )
|
| |
-
|
| |
- drop_user = search_user(session, username=username)
|
| |
- if not drop_user:
|
| |
- raise pagure.exceptions.PagureException(
|
| |
- "No user `%s` found" % username
|
| |
- )
|
| |
-
|
| |
- action_user = user
|
| |
- user = search_user(session, username=user)
|
| |
- if not user:
|
| |
- raise pagure.exceptions.PagureException(
|
| |
- "Could not find user %s" % action_user
|
| |
- )
|
| |
-
|
| |
- if (
|
| |
- not from_external
|
| |
- and group_obj.group_name not in user.groups
|
| |
- and not is_admin
|
| |
- ):
|
| |
- raise pagure.exceptions.PagureException(
|
| |
- "You are not allowed to remove user from this group"
|
| |
- )
|
| |
-
|
| |
- if drop_user.username == group_obj.creator.username and not force:
|
| |
- raise pagure.exceptions.PagureException(
|
| |
- "The creator of a group cannot be removed"
|
| |
- )
|
| |
-
|
| |
- user_grp = get_user_group(session, drop_user.id, group_obj.id)
|
| |
- if not user_grp:
|
| |
- raise pagure.exceptions.PagureException(
|
| |
- "User `%s` could not be found in the group `%s`"
|
| |
- % (username, groupname)
|
| |
- )
|
| |
-
|
| |
- session.delete(user_grp)
|
| |
- session.flush()
|
| |
-
|
| |
-
|
| |
- def add_group(
|
| |
- session,
|
| |
- group_name,
|
| |
- display_name,
|
| |
- description,
|
| |
- group_type,
|
| |
- user,
|
| |
- is_admin,
|
| |
- blacklist,
|
| |
- ):
|
| |
- """ Creates a new group with the given information.
|
| |
- """
|
| |
- if " " in group_name:
|
| |
- raise pagure.exceptions.PagureException(
|
| |
- "Spaces are not allowed in group names: %s" % group_name
|
| |
- )
|
| |
-
|
| |
- if group_name in blacklist:
|
| |
- raise pagure.exceptions.PagureException(
|
| |
- "This group name has been blacklisted, "
|
| |
- "please choose another one"
|
| |
- )
|
| |
-
|
| |
- group_types = ["user"]
|
| |
- if is_admin:
|
| |
- group_types = [grp.group_type for grp in get_group_types(session)]
|
| |
-
|
| |
- if not is_admin:
|
| |
- group_type = "user"
|
| |
-
|
| |
- if group_type not in group_types:
|
| |
- raise pagure.exceptions.PagureException("Invalide type for this group")
|
| |
-
|
| |
- username = user
|
| |
- user = search_user(session, username=user)
|
| |
- if not user:
|
| |
- raise pagure.exceptions.PagureException(
|
| |
- "Could not find user %s" % username
|
| |
- )
|
| |
-
|
| |
- group = search_groups(session, group_name=group_name)
|
| |
- if group:
|
| |
- raise pagure.exceptions.PagureException(
|
| |
- "There is already a group named %s" % group_name
|
| |
- )
|
| |
-
|
| |
- display = search_groups(session, display_name=display_name)
|
| |
- if display:
|
| |
- raise pagure.exceptions.PagureException(
|
| |
- "There is already a group with display name `%s` created."
|
| |
- % display_name
|
| |
- )
|
| |
-
|
| |
- grp = pagure.lib.model.PagureGroup(
|
| |
- group_name=group_name,
|
| |
- display_name=display_name,
|
| |
- description=description,
|
| |
- group_type=group_type,
|
| |
- user_id=user.id,
|
| |
- )
|
| |
- session.add(grp)
|
| |
- session.flush()
|
| |
-
|
| |
- return add_user_to_group(
|
| |
- session, user.username, grp, user.username, is_admin
|
| |
- )
|
| |
-
|
| |
-
|
| |
- def get_user_group(session, userid, groupid):
|
| |
- """ Return a specific user_group for the specified group and user
|
| |
- identifiers.
|
| |
-
|
| |
- :arg session: the session with which to connect to the database.
|
| |
-
|
| |
- """
|
| |
- query = (
|
| |
- session.query(model.PagureUserGroup)
|
| |
- .filter(model.PagureUserGroup.user_id == userid)
|
| |
- .filter(model.PagureUserGroup.group_id == groupid)
|
| |
- )
|
| |
-
|
| |
- return query.first()
|
| |
-
|
| |
-
|
| |
- def is_group_member(session, user, groupname):
|
| |
- """ Return whether the user is a member of the specified group. """
|
| |
- if not user:
|
| |
- return False
|
| |
-
|
| |
- user = search_user(session, username=user)
|
| |
- if not user:
|
| |
- return False
|
| |
-
|
| |
- return groupname in user.groups
|
| |
-
|
| |
-
|
| |
- def get_api_token(session, token_str):
|
| |
- """ Return the Token object corresponding to the provided token string
|
| |
- if there is any, returns None otherwise.
|
| |
- """
|
| |
- query = session.query(model.Token).filter(model.Token.id == token_str)
|
| |
-
|
| |
- return query.first()
|
| |
-
|
| |
-
|
| |
- def get_acls(session, restrict=None):
|
| |
- """ Returns all the possible ACLs a token can have according to the
|
| |
- database.
|
| |
- """
|
| |
- query = session.query(model.ACL).order_by(model.ACL.name)
|
| |
- if restrict:
|
| |
- if isinstance(restrict, list):
|
| |
- query = query.filter(model.ACL.name.in_(restrict))
|
| |
- else:
|
| |
- query = query.filter(model.ACL.name == restrict)
|
| |
-
|
| |
- return query.all()
|
| |
-
|
| |
-
|
| |
- def add_token_to_user(session, project, acls, username, description=None):
|
| |
- """ Create a new token for the specified user on the specified project
|
| |
- with the given ACLs.
|
| |
- """
|
| |
- acls_obj = session.query(model.ACL).filter(model.ACL.name.in_(acls)).all()
|
| |
-
|
| |
- user = search_user(session, username=username)
|
| |
-
|
| |
- token = pagure.lib.model.Token(
|
| |
- id=pagure.lib.login.id_generator(64),
|
| |
- user_id=user.id,
|
| |
- project_id=project.id if project else None,
|
| |
- description=description,
|
| |
- expiration=datetime.datetime.utcnow() + datetime.timedelta(days=60),
|
| |
- )
|
| |
- session.add(token)
|
| |
- session.flush()
|
| |
-
|
| |
- for acl in acls_obj:
|
| |
- item = pagure.lib.model.TokenAcl(token_id=token.id, acl_id=acl.id)
|
| |
- session.add(item)
|
| |
-
|
| |
- session.commit()
|
| |
-
|
| |
- return "Token created"
|
| |
-
|
| |
-
|
| |
- def _convert_markdown(md_processor, text):
|
| |
- """ Small function converting the text to html using the given markdown
|
| |
- processor.
|
| |
-
|
| |
- This was done in order to make testing it easier.
|
| |
- """
|
| |
- return md_processor.convert(text)
|
| |
-
|
| |
-
|
| |
- def text2markdown(text, extended=True, readme=False):
|
| |
- """ Simple text to html converter using the markdown library.
|
| |
- """
|
| |
- extensions = [
|
| |
- "markdown.extensions.def_list",
|
| |
- "markdown.extensions.fenced_code",
|
| |
- "markdown.extensions.tables",
|
| |
- "markdown.extensions.smart_strong",
|
| |
- # All of the above are the .extra extensions
|
| |
- # w/o the "attribute lists" one
|
| |
- "markdown.extensions.admonition",
|
| |
- "markdown.extensions.codehilite",
|
| |
- "markdown.extensions.sane_lists",
|
| |
- "markdown.extensions.toc",
|
| |
- ]
|
| |
- # Some extensions are enabled for READMEs and disabled otherwise
|
| |
- if readme:
|
| |
- extensions.extend(
|
| |
- ["markdown.extensions.abbr", "markdown.extensions.footnotes"]
|
| |
- )
|
| |
- else:
|
| |
- extensions.append("markdown.extensions.nl2br")
|
| |
- if extended:
|
| |
- # Install our markdown modifications
|
| |
- extensions.append("pagure.pfmarkdown")
|
| |
-
|
| |
- md_processor = markdown.Markdown(
|
| |
- extensions=extensions,
|
| |
- extension_configs={
|
| |
- "markdown.extensions.codehilite": {"guess_lang": False}
|
| |
- },
|
| |
- output_format="xhtml5",
|
| |
- )
|
| |
-
|
| |
- if text:
|
| |
- try:
|
| |
- text = _convert_markdown(md_processor, text)
|
| |
- except Exception:
|
| |
- _log.debug(
|
| |
- "A markdown error occured while processing: ``%s``", text
|
| |
- )
|
| |
- return clean_input(text)
|
| |
-
|
| |
- return ""
|
| |
-
|
| |
-
|
| |
- def filter_img_src(name, value):
|
| |
- """ Filter in img html tags images coming from a different domain. """
|
| |
- if name in ("alt", "height", "width", "class", "data-src"):
|
| |
- return True
|
| |
- if name == "src":
|
| |
- parsed = urlparse(value)
|
| |
- return (not parsed.netloc) or parsed.netloc == urlparse(
|
| |
- pagure_config["APP_URL"]
|
| |
- ).netloc
|
| |
- return False
|
| |
-
|
| |
-
|
| |
- def clean_input(text, ignore=None):
|
| |
- """ For a given html text, escape everything we do not want to support
|
| |
- to avoid potential security breach.
|
| |
- """
|
| |
- if ignore and not isinstance(ignore, (tuple, set, list)):
|
| |
- ignore = [ignore]
|
| |
-
|
| |
- bleach_v = bleach.__version__.split(".")
|
| |
- for idx, val in enumerate(bleach_v):
|
| |
- try:
|
| |
- val = int(val)
|
| |
- except ValueError: # pragma: no cover
|
| |
- pass
|
| |
- bleach_v[idx] = val
|
| |
-
|
| |
- attrs = bleach.ALLOWED_ATTRIBUTES.copy()
|
| |
- attrs["table"] = ["class"]
|
| |
- attrs["span"] = ["class", "id"]
|
| |
- attrs["div"] = ["class"]
|
| |
- attrs["td"] = ["align"]
|
| |
- attrs["th"] = ["align"]
|
| |
- if not ignore or "img" not in ignore:
|
| |
- # newer bleach need three args for attribute callable
|
| |
- if tuple(bleach_v) >= (2, 0, 0): # pragma: no cover
|
| |
- attrs["img"] = lambda tag, name, val: filter_img_src(name, val)
|
| |
- else:
|
| |
- attrs["img"] = filter_img_src
|
| |
-
|
| |
- tags = bleach.ALLOWED_TAGS + [
|
| |
- "p",
|
| |
- "br",
|
| |
- "div",
|
| |
- "h1",
|
| |
- "h2",
|
| |
- "h3",
|
| |
- "h4",
|
| |
- "h5",
|
| |
- "h6",
|
| |
- "table",
|
| |
- "td",
|
| |
- "tr",
|
| |
- "th",
|
| |
- "thead",
|
| |
- "tbody",
|
| |
- "col",
|
| |
- "pre",
|
| |
- "img",
|
| |
- "hr",
|
| |
- "dl",
|
| |
- "dt",
|
| |
- "dd",
|
| |
- "span",
|
| |
- "kbd",
|
| |
- "var",
|
| |
- "del",
|
| |
- "cite",
|
| |
- "noscript",
|
| |
- ]
|
| |
- if ignore:
|
| |
- for tag in ignore:
|
| |
- if tag in tags:
|
| |
- tags.remove(tag)
|
| |
-
|
| |
- kwargs = {"tags": tags, "attributes": attrs}
|
| |
-
|
| |
- # newer bleach allow to customize the protocol supported
|
| |
- if tuple(bleach_v) >= (1, 5, 0): # pragma: no cover
|
| |
- protocols = bleach.ALLOWED_PROTOCOLS + ["irc", "ircs"]
|
| |
- kwargs["protocols"] = protocols
|
| |
-
|
| |
- return bleach.clean(text, **kwargs)
|
| |
-
|
| |
-
|
| |
- def could_be_text(text):
|
| |
- """ Returns whether we think this chain of character could be text or not
|
| |
- """
|
| |
- try:
|
| |
- text.decode("utf-8")
|
| |
- return True
|
| |
- except (UnicodeDecodeError, UnicodeEncodeError):
|
| |
- return False
|
| |
-
|
| |
-
|
| |
- def get_pull_request_of_user(
|
| |
- session,
|
| |
- username,
|
| |
- status=None,
|
| |
- filed=None,
|
| |
- actionable=None,
|
| |
- offset=None,
|
| |
- limit=None,
|
| |
- count=False,
|
| |
- ):
|
| |
- """List the opened pull-requests of an user.
|
| |
- These pull-requests have either been opened by that user or against
|
| |
- projects that user has commit on.
|
| |
-
|
| |
- If filed: only the PRs opened/filed by the specified username will be
|
| |
- returned.
|
| |
- If actionable: only the PRs not opened/filed by the specified username
|
| |
- will be returned.
|
| |
- """
|
| |
- projects = session.query(sqlalchemy.distinct(model.Project.id))
|
| |
-
|
| |
- projects = projects.filter(
|
| |
- # User created the project
|
| |
- sqlalchemy.and_(
|
| |
- model.User.user == username, model.User.id == model.Project.user_id
|
| |
- )
|
| |
- )
|
| |
- sub_q2 = session.query(sqlalchemy.distinct(model.Project.id)).filter(
|
| |
- # User got commit right
|
| |
- sqlalchemy.and_(
|
| |
- model.User.user == username,
|
| |
- model.User.id == model.ProjectUser.user_id,
|
| |
- model.ProjectUser.project_id == model.Project.id,
|
| |
- sqlalchemy.or_(
|
| |
- model.ProjectUser.access == "admin",
|
| |
- model.ProjectUser.access == "commit",
|
| |
- ),
|
| |
- )
|
| |
- )
|
| |
- sub_q3 = session.query(sqlalchemy.distinct(model.Project.id)).filter(
|
| |
- # User created a group that has commit right
|
| |
- sqlalchemy.and_(
|
| |
- model.User.user == username,
|
| |
- model.PagureGroup.user_id == model.User.id,
|
| |
- model.PagureGroup.group_type == "user",
|
| |
- model.PagureGroup.id == model.ProjectGroup.group_id,
|
| |
- model.Project.id == model.ProjectGroup.project_id,
|
| |
- sqlalchemy.or_(
|
| |
- model.ProjectGroup.access == "admin",
|
| |
- model.ProjectGroup.access == "commit",
|
| |
- ),
|
| |
- )
|
| |
- )
|
| |
- sub_q4 = session.query(sqlalchemy.distinct(model.Project.id)).filter(
|
| |
- # User is part of a group that has commit right
|
| |
- sqlalchemy.and_(
|
| |
- model.User.user == username,
|
| |
- model.PagureUserGroup.user_id == model.User.id,
|
| |
- model.PagureUserGroup.group_id == model.PagureGroup.id,
|
| |
- model.PagureGroup.group_type == "user",
|
| |
- model.PagureGroup.id == model.ProjectGroup.group_id,
|
| |
- model.Project.id == model.ProjectGroup.project_id,
|
| |
- sqlalchemy.or_(
|
| |
- model.ProjectGroup.access == "admin",
|
| |
- model.ProjectGroup.access == "commit",
|
| |
- ),
|
| |
- )
|
| |
- )
|
| |
-
|
| |
- projects = projects.union(sub_q2).union(sub_q3).union(sub_q4)
|
| |
-
|
| |
- query = session.query(sqlalchemy.distinct(model.PullRequest.uid)).filter(
|
| |
- model.PullRequest.project_id.in_(projects.subquery())
|
| |
- )
|
| |
-
|
| |
- query_2 = session.query(sqlalchemy.distinct(model.PullRequest.uid)).filter(
|
| |
- # User open the PR
|
| |
- sqlalchemy.and_(
|
| |
- model.PullRequest.user_id == model.User.id,
|
| |
- model.User.user == username,
|
| |
- )
|
| |
- )
|
| |
-
|
| |
- final_sub = query.union(query_2)
|
| |
-
|
| |
- query = (
|
| |
- session.query(model.PullRequest)
|
| |
- .filter(model.PullRequest.uid.in_(final_sub.subquery()))
|
| |
- .order_by(model.PullRequest.date_created.desc())
|
| |
- )
|
| |
-
|
| |
- if status:
|
| |
- query = query.filter(model.PullRequest.status == status)
|
| |
-
|
| |
- if filed:
|
| |
- query = query.filter(
|
| |
- model.PullRequest.user_id == model.User.id,
|
| |
- model.User.user == filed,
|
| |
- )
|
| |
- elif actionable:
|
| |
- query = query.filter(
|
| |
- model.PullRequest.user_id == model.User.id,
|
| |
- model.User.user != actionable,
|
| |
- )
|
| |
-
|
| |
- if offset:
|
| |
- query = query.offset(offset)
|
| |
- if limit:
|
| |
- query = query.limit(limit)
|
| |
-
|
| |
- if count:
|
| |
- return query.count()
|
| |
- else:
|
| |
- return query.all()
|
| |
-
|
| |
-
|
| |
- def update_watch_status(session, project, user, watch):
|
| |
- """ Update the user status for watching a project.
|
| |
-
|
| |
- The watch status can be:
|
| |
- -1: reset the watch status to default
|
| |
- 0: unwatch, don't notify the user of anything
|
| |
- 1: watch issues and PRs
|
| |
- 2: watch commits
|
| |
- 3: watch issues, PRs and commits
|
| |
-
|
| |
- """
|
| |
- if watch not in ["-1", "0", "1", "2", "3"]:
|
| |
- raise pagure.exceptions.PagureException(
|
| |
- 'The watch value of "%s" is invalid' % watch
|
| |
- )
|
| |
-
|
| |
- user_obj = get_user(session, user)
|
| |
-
|
| |
- watcher = (
|
| |
- session.query(model.Watcher)
|
| |
- .filter(
|
| |
- sqlalchemy.and_(
|
| |
- model.Watcher.project_id == project.id,
|
| |
- model.Watcher.user_id == user_obj.id,
|
| |
- )
|
| |
- )
|
| |
- .first()
|
| |
- )
|
| |
-
|
| |
- if watch == "-1":
|
| |
- if not watcher:
|
| |
- return "Watch status is already reset"
|
| |
-
|
| |
- session.delete(watcher)
|
| |
- session.flush()
|
| |
- return "Watch status reset"
|
| |
-
|
| |
- should_watch_issues = False
|
| |
- should_watch_commits = False
|
| |
- if watch == "1":
|
| |
- should_watch_issues = True
|
| |
- elif watch == "2":
|
| |
- should_watch_commits = True
|
| |
- elif watch == "3":
|
| |
- should_watch_issues = True
|
| |
- should_watch_commits = True
|
| |
-
|
| |
- if not watcher:
|
| |
- watcher = model.Watcher(
|
| |
- project_id=project.id,
|
| |
- user_id=user_obj.id,
|
| |
- watch_issues=should_watch_issues,
|
| |
- watch_commits=should_watch_commits,
|
| |
- )
|
| |
- else:
|
| |
- watcher.watch_issues = should_watch_issues
|
| |
- watcher.watch_commits = should_watch_commits
|
| |
-
|
| |
- session.add(watcher)
|
| |
- session.flush()
|
| |
-
|
| |
- if should_watch_issues and should_watch_commits:
|
| |
- return "You are now watching issues, PRs, and commits on this project"
|
| |
- elif should_watch_issues:
|
| |
- return "You are now watching issues and PRs on this project"
|
| |
- elif should_watch_commits:
|
| |
- return "You are now watching commits on this project"
|
| |
- else:
|
| |
- return "You are no longer watching this project"
|
| |
-
|
| |
-
|
| |
- def get_watch_level_on_repo(
|
| |
- session, user, repo, repouser=None, namespace=None
|
| |
- ):
|
| |
- """ Get a list representing the watch level of the user on the project.
|
| |
- """
|
| |
- # If a user wasn't passed in, we can't determine their watch level
|
| |
- if user is None:
|
| |
- return []
|
| |
- elif isinstance(user, six.string_types):
|
| |
- user_obj = search_user(session, username=user)
|
| |
- else:
|
| |
- user_obj = search_user(session, username=user.username)
|
| |
- # If we can't find the user in the database, we can't determine their
|
| |
- # watch level
|
| |
- if not user_obj:
|
| |
- return []
|
| |
-
|
| |
- # If the project passed in a Project for the repo parameter, then we
|
| |
- # don't need to query for it
|
| |
- if isinstance(repo, model.Project):
|
| |
- project = repo
|
| |
- # If the project passed in a string, then assume it is a project name
|
| |
- elif isinstance(repo, six.string_types):
|
| |
- project = _get_project(
|
| |
- session, repo, user=repouser, namespace=namespace
|
| |
- )
|
| |
- else:
|
| |
- raise RuntimeError(
|
| |
- 'The passed in repo is an invalid type of "{0}"'.format(
|
| |
- type(repo).__name__
|
| |
- )
|
| |
- )
|
| |
-
|
| |
- # If the project is not found, we can't determine the involvement of the
|
| |
- # user in the project
|
| |
- if not project:
|
| |
- return []
|
| |
-
|
| |
- query = (
|
| |
- session.query(model.Watcher)
|
| |
- .filter(model.Watcher.user_id == user_obj.id)
|
| |
- .filter(model.Watcher.project_id == project.id)
|
| |
- )
|
| |
-
|
| |
- watcher = query.first()
|
| |
- # If there is a watcher issue, that means the user explicitly set a watch
|
| |
- # level on the project
|
| |
- if watcher:
|
| |
- if watcher.watch_issues and watcher.watch_commits:
|
| |
- return ["issues", "commits"]
|
| |
- elif watcher.watch_issues:
|
| |
- return ["issues"]
|
| |
- elif watcher.watch_commits:
|
| |
- return ["commits"]
|
| |
- else:
|
| |
- # If a watcher entry is set and both are set to False, that
|
| |
- # means the user explicitly asked to not be notified
|
| |
- return []
|
| |
-
|
| |
- # If the user is the project owner, by default they will be watching
|
| |
- # issues and PRs
|
| |
- if user_obj.username == project.user.username:
|
| |
- return ["issues"]
|
| |
- # If the user is a contributor, by default they will be watching issues
|
| |
- # and PRs
|
| |
- for contributor in project.users:
|
| |
- if user_obj.username == contributor.username:
|
| |
- return ["issues"]
|
| |
- # If the user is in a project group, by default they will be watching
|
| |
- # issues and PRs
|
| |
- for group in project.groups:
|
| |
- for guser in group.users:
|
| |
- if user_obj.username == guser.username:
|
| |
- return ["issues"]
|
| |
- # If no other condition is true, then they are not explicitly watching
|
| |
- # the project or are not involved in the project to the point that
|
| |
- # comes with aq default watch level
|
| |
- return []
|
| |
-
|
| |
-
|
| |
- def user_watch_list(session, user, exclude_groups=None):
|
| |
- """ Returns list of all the projects which the user is watching """
|
| |
-
|
| |
- user_obj = search_user(session, username=user)
|
| |
- if not user_obj:
|
| |
- return []
|
| |
-
|
| |
- unwatched = (
|
| |
- session.query(model.Watcher)
|
| |
- .filter(model.Watcher.user_id == user_obj.id)
|
| |
- .filter(model.Watcher.watch_issues == False) # noqa: E712
|
| |
- .filter(model.Watcher.watch_commits == False) # noqa: E712
|
| |
- )
|
| |
-
|
| |
- unwatched_list = []
|
| |
- if unwatched:
|
| |
- unwatched_list = [unwatch.project for unwatch in unwatched.all()]
|
| |
-
|
| |
- watched = (
|
| |
- session.query(model.Watcher)
|
| |
- .filter(model.Watcher.user_id == user_obj.id)
|
| |
- .filter(model.Watcher.watch_issues == True) # noqa: E712
|
| |
- .filter(model.Watcher.watch_commits == True) # noqa: E712
|
| |
- )
|
| |
-
|
| |
- watched_list = []
|
| |
- if watched:
|
| |
- watched_list = [watch.project for watch in watched.all()]
|
| |
-
|
| |
- user_projects = search_projects(
|
| |
- session, username=user_obj.user, exclude_groups=exclude_groups
|
| |
- )
|
| |
- watch = set(watched_list + user_projects)
|
| |
-
|
| |
- for project in user_projects:
|
| |
- if project in unwatched_list:
|
| |
- watch.remove(project)
|
| |
-
|
| |
- return sorted(list(watch), key=lambda proj: proj.name)
|
| |
-
|
| |
-
|
| |
- def set_watch_obj(session, user, obj, watch_status):
|
| |
- """ Set the watch status of the user on the specified object.
|
| |
-
|
| |
- Objects can be either an issue or a pull-request
|
| |
- """
|
| |
-
|
| |
- user_obj = get_user(session, user)
|
| |
-
|
| |
- if obj.isa == "issue":
|
| |
- query = (
|
| |
- session.query(model.IssueWatcher)
|
| |
- .filter(model.IssueWatcher.user_id == user_obj.id)
|
| |
- .filter(model.IssueWatcher.issue_uid == obj.uid)
|
| |
- )
|
| |
- elif obj.isa == "pull-request":
|
| |
- query = (
|
| |
- session.query(model.PullRequestWatcher)
|
| |
- .filter(model.PullRequestWatcher.user_id == user_obj.id)
|
| |
- .filter(model.PullRequestWatcher.pull_request_uid == obj.uid)
|
| |
- )
|
| |
- else:
|
| |
- raise pagure.exceptions.InvalidObjectException(
|
| |
- 'Unsupported object found: "%s"' % obj
|
| |
- )
|
| |
-
|
| |
- dbobj = query.first()
|
| |
-
|
| |
- if not dbobj:
|
| |
- if obj.isa == "issue":
|
| |
- dbobj = model.IssueWatcher(
|
| |
- user_id=user_obj.id, issue_uid=obj.uid, watch=watch_status
|
| |
- )
|
| |
- elif obj.isa == "pull-request":
|
| |
- dbobj = model.PullRequestWatcher(
|
| |
- user_id=user_obj.id,
|
| |
- pull_request_uid=obj.uid,
|
| |
- watch=watch_status,
|
| |
- )
|
| |
- else:
|
| |
- dbobj.watch = watch_status
|
| |
-
|
| |
- session.add(dbobj)
|
| |
-
|
| |
- output = "You are no longer watching this %s" % obj.isa
|
| |
- if watch_status:
|
| |
- output = "You are now watching this %s" % obj.isa
|
| |
- return output
|
| |
-
|
| |
-
|
| |
- def get_watch_list(session, obj):
|
| |
- """ Return a list of all the users that are watching the "object"
|
| |
- """
|
| |
- private = False
|
| |
- if obj.isa == "issue":
|
| |
- private = obj.private
|
| |
- obj_watchers_query = session.query(model.IssueWatcher).filter(
|
| |
- model.IssueWatcher.issue_uid == obj.uid
|
| |
- )
|
| |
- elif obj.isa == "pull-request":
|
| |
- obj_watchers_query = session.query(model.PullRequestWatcher).filter(
|
| |
- model.PullRequestWatcher.pull_request_uid == obj.uid
|
| |
- )
|
| |
- else:
|
| |
- raise pagure.exceptions.InvalidObjectException(
|
| |
- 'Unsupported object found: "%s"' % obj
|
| |
- )
|
| |
-
|
| |
- project_watchers_query = session.query(model.Watcher).filter(
|
| |
- model.Watcher.project_id == obj.project.id
|
| |
- )
|
| |
-
|
| |
- users = set()
|
| |
-
|
| |
- # Add the person who opened the object
|
| |
- users.add(obj.user.username)
|
| |
-
|
| |
- # Add all the people who commented on that object
|
| |
- for comment in obj.comments:
|
| |
- users.add(comment.user.username)
|
| |
-
|
| |
- # Add the user of the project
|
| |
- users.add(obj.project.user.username)
|
| |
-
|
| |
- # Add the regular contributors
|
| |
- for contributor in obj.project.users:
|
| |
- users.add(contributor.username)
|
| |
-
|
| |
- # Add people in groups with commit access
|
| |
- for group in obj.project.groups:
|
| |
- for member in group.users:
|
| |
- users.add(member.username)
|
| |
-
|
| |
- # If the issue isn't private:
|
| |
- if not private:
|
| |
- # Add all the people watching the repo, remove those who opted-out
|
| |
- for watcher in project_watchers_query.all():
|
| |
- if watcher.watch_issues:
|
| |
- users.add(watcher.user.username)
|
| |
- else:
|
| |
- if watcher.user.username in users:
|
| |
- users.remove(watcher.user.username)
|
| |
-
|
| |
- # Add all the people watching this object, remove those who opted-out
|
| |
- for watcher in obj_watchers_query.all():
|
| |
- if watcher.watch:
|
| |
- users.add(watcher.user.username)
|
| |
- else:
|
| |
- if watcher.user.username in users:
|
| |
- users.remove(watcher.user.username)
|
| |
-
|
| |
- return users
|
| |
-
|
| |
-
|
| |
- def save_report(session, repo, name, url, username):
|
| |
- """ Save the report of issues based on the given URL of the project.
|
| |
- """
|
| |
- url_obj = urlparse(url)
|
| |
- url = url_obj.geturl().replace(url_obj.query, "")
|
| |
- query = {}
|
| |
- for k, v in parse_qsl(url_obj.query):
|
| |
- if k in query:
|
| |
- if isinstance(query[k], list):
|
| |
- query[k].append(v)
|
| |
- else:
|
| |
- query[k] = [query[k], v]
|
| |
- else:
|
| |
- query[k] = v
|
| |
- reports = repo.reports
|
| |
- reports[name] = query
|
| |
- repo.reports = reports
|
| |
- session.add(repo)
|
| |
-
|
| |
-
|
| |
- def set_custom_key_fields(session, project, fields, types, data, notify=None):
|
| |
- """ Set or update the custom key fields of a project with the values
|
| |
- provided. "data" is currently only used for lists
|
| |
- """
|
| |
-
|
| |
- current_keys = {}
|
| |
- for key in project.issue_keys:
|
| |
- current_keys[key.name] = key
|
| |
-
|
| |
- for idx, key in enumerate(fields):
|
| |
- if types[idx] != "list":
|
| |
- # Only Lists use data, strip it otherwise
|
| |
- data[idx] = None
|
| |
- else:
|
| |
- if data[idx]:
|
| |
- data[idx] = [item.strip() for item in data[idx].split(",")]
|
| |
-
|
| |
- if notify and notify[idx] == "on":
|
| |
- notify_flag = True
|
| |
- else:
|
| |
- notify_flag = False
|
| |
-
|
| |
- if key in current_keys:
|
| |
- issuekey = current_keys[key]
|
| |
- issuekey.key_type = types[idx]
|
| |
- issuekey.data = data[idx]
|
| |
- issuekey.key_notify = notify_flag
|
| |
- else:
|
| |
- issuekey = model.IssueKeys(
|
| |
- project_id=project.id,
|
| |
- name=key,
|
| |
- key_type=types[idx],
|
| |
- data=data[idx],
|
| |
- key_notify=notify_flag,
|
| |
- )
|
| |
- session.add(issuekey)
|
| |
-
|
| |
- # Delete keys
|
| |
- for key in current_keys:
|
| |
- if key not in fields:
|
| |
- session.delete(current_keys[key])
|
| |
-
|
| |
- return "List of custom fields updated"
|
| |
-
|
| |
-
|
| |
- def set_custom_key_value(session, issue, key, value):
|
| |
- """ Set or update the value of the specified custom key.
|
| |
- """
|
| |
-
|
| |
- query = (
|
| |
- session.query(model.IssueValues)
|
| |
- .filter(model.IssueValues.key_id == key.id)
|
| |
- .filter(model.IssueValues.issue_uid == issue.uid)
|
| |
- )
|
| |
-
|
| |
- current_field = query.first()
|
| |
- updated = False
|
| |
- delete = False
|
| |
- old_value = None
|
| |
- if current_field:
|
| |
- old_value = current_field.value
|
| |
- if current_field.key.key_type == "boolean":
|
| |
- value = value or False
|
| |
- if value is None or value == "":
|
| |
- session.delete(current_field)
|
| |
- updated = True
|
| |
- delete = True
|
| |
- elif current_field.value != value:
|
| |
- current_field.value = value
|
| |
- updated = True
|
| |
- else:
|
| |
- if value is None or value == "":
|
| |
- delete = True
|
| |
- else:
|
| |
- current_field = model.IssueValues(
|
| |
- issue_uid=issue.uid, key_id=key.id, value=value
|
| |
- )
|
| |
- updated = True
|
| |
-
|
| |
- if not delete:
|
| |
- session.add(current_field)
|
| |
-
|
| |
- if REDIS and updated:
|
| |
- if issue.private:
|
| |
- REDIS.publish(
|
| |
- "pagure.%s" % issue.uid,
|
| |
- json.dumps({"issue": "private", "custom_fields": [key.name]}),
|
| |
- )
|
| |
- else:
|
| |
- REDIS.publish(
|
| |
- "pagure.%s" % issue.uid,
|
| |
- json.dumps(
|
| |
- {
|
| |
- "custom_fields": [key.name],
|
| |
- "issue": issue.to_json(
|
| |
- public=True, with_comments=False
|
| |
- ),
|
| |
- }
|
| |
- ),
|
| |
- )
|
| |
-
|
| |
- if updated and value:
|
| |
- output = "Custom field %s adjusted to %s" % (key.name, value)
|
| |
- if old_value:
|
| |
- output += " (was: %s)" % old_value
|
| |
- return output
|
| |
- elif updated and old_value:
|
| |
- return "Custom field %s reset (from %s)" % (key.name, old_value)
|
| |
-
|
| |
-
|
| |
- def get_yearly_stats_user(session, user, date, tz="UTC"):
|
| |
- """ Return the activity of the specified user in the year preceding the
|
| |
- specified date. 'offset' is intended to be a timezone offset from UTC,
|
| |
- in minutes: you can discover the offset for a timezone and pass that
|
| |
- in order for the results to be relative to that timezone. Note, offset
|
| |
- should be the amount of minutes that should be added to the UTC time to
|
| |
- produce the local time - so for timezones behind UTC the number should
|
| |
- be negative, and for timezones ahead of UTC the number should be
|
| |
- positive. This is the opposite of what Javascript getTimezoneOffset()
|
| |
- does, so you have to invert any value you get from that.
|
| |
- """
|
| |
- start_date = datetime.datetime(date.year - 1, date.month, date.day)
|
| |
-
|
| |
- events = (
|
| |
- session.query(model.PagureLog)
|
| |
- .filter(model.PagureLog.date_created.between(start_date, date))
|
| |
- .filter(model.PagureLog.user_id == user.id)
|
| |
- .all()
|
| |
- )
|
| |
- # Counter very handily does exactly what we want here: it gives
|
| |
- # us a dict with the dates as keys and the number of times each
|
| |
- # date occurs in the data as the values, we return its items as
|
| |
- # a list of tuples
|
| |
- return list(Counter([event.date_tz(tz) for event in events]).items())
|
| |
-
|
| |
-
|
| |
- def get_user_activity_day(session, user, date, tz="UTC"):
|
| |
- """ Return the activity of the specified user on the specified date.
|
| |
- 'offset' is intended to be a timezone offset from UTC, in minutes:
|
| |
- you can discover the offset for a timezone and pass that, so this
|
| |
- will return activity that occurred on the specified date in the
|
| |
- desired timezone. Note, offset should be the amount of minutes
|
| |
- that should be added to the UTC time to produce the local time -
|
| |
- so for timezones behind UTC the number should be negative, and
|
| |
- for timezones ahead of UTC the number should be positive. This is
|
| |
- the opposite of what Javascript getTimezoneOffset() does, so you
|
| |
- have to invert any value you get from that.
|
| |
- """
|
| |
- dt = datetime.datetime.strptime(date, "%Y-%m-%d")
|
| |
- # if the offset is *negative* some of the events we want may be
|
| |
- # on the next day in UTC terms. if the offset is *positive* some
|
| |
- # of the events we want may be on the previous day in UTC terms.
|
| |
- # 'dt' will be at 00:00, so we subtract 1 day for prevday but add
|
| |
- # 2 days for nextday. e.g. 2018-02-15 00:00 - prevday will be
|
| |
- # 2018-02-14 00:00, nextday will be 2018-02-17 00:00. We'll get
|
| |
- # all events that occurred on 2018-02-14, 2018-02-15 or 2018-02-16
|
| |
- # in UTC time.
|
| |
- prevday = dt - datetime.timedelta(days=1)
|
| |
- nextday = dt + datetime.timedelta(days=2)
|
| |
- query = (
|
| |
- session.query(model.PagureLog)
|
| |
- .filter(model.PagureLog.date_created.between(prevday, nextday))
|
| |
- .filter(model.PagureLog.user_id == user.id)
|
| |
- .order_by(model.PagureLog.id.asc())
|
| |
- )
|
| |
- events = query.all()
|
| |
- # Now we filter down to the events that *really* occurred on the
|
| |
- # date we were asked for with the offset applied, and return
|
| |
- return [ev for ev in events if ev.date_tz(tz) == dt.date()]
|
| |
-
|
| |
-
|
| |
- def get_watchlist_messages(session, user, limit=None):
|
| |
-
|
| |
- watched = user_watch_list(session, user.username)
|
| |
-
|
| |
- watched_list = [watch.id for watch in watched]
|
| |
-
|
| |
- events = (
|
| |
- session.query(model.PagureLog)
|
| |
- .filter(model.PagureLog.project_id.in_(watched_list))
|
| |
- .order_by(model.PagureLog.id.desc())
|
| |
- )
|
| |
-
|
| |
- if limit is not None:
|
| |
- events = events.limit(limit)
|
| |
-
|
| |
- events = events.all()
|
| |
-
|
| |
- return events
|
| |
-
|
| |
-
|
| |
- def log_action(session, action, obj, user_obj):
|
| |
- """ Log an user action on a project/issue/PR. """
|
| |
- project_id = None
|
| |
- if obj.isa in ["issue", "pull-request"]:
|
| |
- project_id = obj.project_id
|
| |
- if obj.project.private:
|
| |
- return
|
| |
- elif obj.isa == "project":
|
| |
- project_id = obj.id
|
| |
- if obj.private:
|
| |
- return
|
| |
- else:
|
| |
- raise pagure.exceptions.InvalidObjectException(
|
| |
- 'Unsupported object found: "%s"' % obj
|
| |
- )
|
| |
-
|
| |
- if obj.private:
|
| |
- return
|
| |
-
|
| |
- log = model.PagureLog(
|
| |
- user_id=user_obj.id,
|
| |
- project_id=project_id,
|
| |
- log_type=action,
|
| |
- ref_id=obj.id,
|
| |
- )
|
| |
- if obj.isa == "issue":
|
| |
- setattr(log, "issue_uid", obj.uid)
|
| |
- elif obj.isa == "pull-request":
|
| |
- setattr(log, "pull_request_uid", obj.uid)
|
| |
-
|
| |
- session.add(log)
|
| |
- session.commit()
|
| |
-
|
| |
-
|
| |
- def email_logs_count(session, email):
|
| |
- """ Returns the number of logs associated with a given email."""
|
| |
- query = session.query(model.PagureLog).filter(
|
| |
- model.PagureLog.user_email == email
|
| |
- )
|
| |
-
|
| |
- return query.count()
|
| |
-
|
| |
-
|
| |
- def update_log_email_user(session, email, user):
|
| |
- """ Update the logs with the provided email to point to the specified
|
| |
- user.
|
| |
- """
|
| |
- session.query(model.PagureLog).filter(
|
| |
- model.PagureLog.user_email == email
|
| |
- ).update({model.PagureLog.user_id: user.id}, synchronize_session=False)
|
| |
-
|
| |
-
|
| |
- def get_custom_key(session, project, keyname):
|
| |
- """ Returns custom key object given it's name and the project """
|
| |
-
|
| |
- query = (
|
| |
- session.query(model.IssueKeys)
|
| |
- .filter(model.IssueKeys.project_id == project.id)
|
| |
- .filter(model.IssueKeys.name == keyname)
|
| |
- )
|
| |
-
|
| |
- return query.first()
|
| |
-
|
| |
-
|
| |
- def get_active_milestones(session, project):
|
| |
- """ Returns the list of all the active milestones for a given project.
|
| |
- """
|
| |
-
|
| |
- query = (
|
| |
- session.query(model.Issue.milestone)
|
| |
- .filter(model.Issue.project_id == project.id)
|
| |
- .filter(model.Issue.status == "Open")
|
| |
- .filter(model.Issue.milestone.isnot(None))
|
| |
- )
|
| |
-
|
| |
- return sorted([item[0] for item in query.distinct()])
|
| |
-
|
| |
-
|
| |
- def add_metadata_update_notif(session, obj, messages, user):
|
| |
- """ Add a notification to the specified issue with the given messages
|
| |
- which should reflect changes made to the meta-data of the issue.
|
| |
- """
|
| |
- if not messages:
|
| |
- return
|
| |
-
|
| |
- if not isinstance(messages, (list, set)):
|
| |
- messages = [messages]
|
| |
-
|
| |
- user_id = None
|
| |
- if user:
|
| |
- user_obj = get_user(session, user)
|
| |
- user_id = user_obj.id
|
| |
-
|
| |
- if obj.isa == "issue":
|
| |
- obj_comment = model.IssueComment(
|
| |
- issue_uid=obj.uid,
|
| |
- comment="**Metadata Update from @%s**:\n- %s"
|
| |
- % (user, "\n- ".join(sorted(messages))),
|
| |
- user_id=user_id,
|
| |
- notification=True,
|
| |
- )
|
| |
- elif obj.isa == "pull-request":
|
| |
- obj_comment = model.PullRequestComment(
|
| |
- pull_request_uid=obj.uid,
|
| |
- comment="**Metadata Update from @%s**:\n- %s"
|
| |
- % (user, "\n- ".join(sorted(messages))),
|
| |
- user_id=user_id,
|
| |
- notification=True,
|
| |
- )
|
| |
- obj.last_updated = datetime.datetime.utcnow()
|
| |
- session.add(obj)
|
| |
- session.add(obj_comment)
|
| |
- # Make sure we won't have SQLAlchemy error before we continue
|
| |
- session.commit()
|
| |
-
|
| |
- if REDIS:
|
| |
- REDIS.publish(
|
| |
- "pagure.%s" % obj.uid,
|
| |
- json.dumps(
|
| |
- {
|
| |
- "comment_id": obj_comment.id,
|
| |
- "%s_id" % obj.isa: obj.id,
|
| |
- "project": obj.project.fullname,
|
| |
- "comment_added": text2markdown(obj_comment.comment),
|
| |
- "comment_user": obj_comment.user.user,
|
| |
- "avatar_url": avatar_url_from_email(
|
| |
- obj_comment.user.default_email, size=16
|
| |
- ),
|
| |
- "comment_date": obj_comment.date_created.strftime(
|
| |
- "%Y-%m-%d %H:%M:%S"
|
| |
- ),
|
| |
- "notification": True,
|
| |
- }
|
| |
- ),
|
| |
- )
|
| |
-
|
| |
- pagure.lib.git.update_git(obj, repo=obj.project)
|
| |
-
|
| |
-
|
| |
- def tokenize_search_string(pattern):
|
| |
- """This function tokenizes search patterns into key:value and rest.
|
| |
-
|
| |
- It will also correctly parse key values between quotes.
|
| |
- """
|
| |
- if pattern is None:
|
| |
- return {}, None
|
| |
-
|
| |
- def finalize_token(token, custom_search):
|
| |
- if ":" in token:
|
| |
- # This was a "key:value" parameter
|
| |
- key, value = token.split(":", 1)
|
| |
- custom_search[key] = value
|
| |
- return ""
|
| |
- else:
|
| |
- # This was a token without colon, thus a search pattern
|
| |
- return "%s " % token
|
| |
-
|
| |
- custom_search = {}
|
| |
- # Remaining is the remaining real search_pattern (aka, non-key:values)
|
| |
- remaining = ""
|
| |
- # Token is the current "search token" we are processing
|
| |
- token = ""
|
| |
- in_quotes = False
|
| |
- for char in pattern:
|
| |
- if char == " " and not in_quotes:
|
| |
- remaining += finalize_token(token, custom_search)
|
| |
- token = ""
|
| |
- elif char == '"':
|
| |
- in_quotes = not in_quotes
|
| |
- else:
|
| |
- token += char
|
| |
-
|
| |
- # Parse the final token
|
| |
- remaining += finalize_token(token, custom_search)
|
| |
-
|
| |
- return custom_search, remaining.strip()
|
| |
-
|
| |
-
|
| |
- def get_access_levels(session):
|
| |
- """ Returns all the access levels a user/group can have for a project """
|
| |
-
|
| |
- access_level_objs = session.query(model.AccessLevels).all()
|
| |
- return [access_level.access for access_level in access_level_objs]
|
| |
-
|
| |
-
|
| |
- def get_obj_access(session, project_obj, obj):
|
| |
- """ Returns the level of access the user/group has on the project.
|
| |
-
|
| |
- :arg session: the session to use to connect to the database.
|
| |
- :arg project_obj: SQLAlchemy object of Project class
|
| |
- :arg obj: SQLAlchemy object of either User or PagureGroup class
|
| |
- """
|
| |
-
|
| |
- if isinstance(obj, model.User):
|
| |
- query = (
|
| |
- session.query(model.ProjectUser)
|
| |
- .filter(model.ProjectUser.project_id == project_obj.id)
|
| |
- .filter(model.ProjectUser.user_id == obj.id)
|
| |
- )
|
| |
- else:
|
| |
- query = (
|
| |
- session.query(model.ProjectGroup)
|
| |
- .filter(model.ProjectGroup.project_id == project_obj.id)
|
| |
- .filter(model.ProjectGroup.group_id == obj.id)
|
| |
- )
|
| |
-
|
| |
- return query.first()
|
| |
-
|
| |
-
|
| |
- def search_token(
|
| |
- session, acls, user=None, token=None, active=False, expired=False
|
| |
- ):
|
| |
- """ Searches the API tokens corresponding to the criterias specified.
|
| |
-
|
| |
- :arg session: the session to use to connect to the database.
|
| |
- :arg acls: List of the ACL associated with these API tokens
|
| |
- :arg user: restrict the API tokens to this given user
|
| |
- :arg token: restrict the API tokens to this specified token (if it
|
| |
- exists)
|
| |
- """
|
| |
- query = (
|
| |
- session.query(model.Token)
|
| |
- .filter(model.Token.id == model.TokenAcl.token_id)
|
| |
- .filter(model.TokenAcl.acl_id == model.ACL.id)
|
| |
- )
|
| |
-
|
| |
- if acls:
|
| |
- if isinstance(acls, list):
|
| |
- query = query.filter(model.ACL.name.in_(acls))
|
| |
- else:
|
| |
- query = query.filter(model.ACL.name == acls)
|
| |
-
|
| |
- if user:
|
| |
- query = query.filter(model.Token.user_id == model.User.id).filter(
|
| |
- model.User.user == user
|
| |
- )
|
| |
-
|
| |
- if active:
|
| |
- query = query.filter(
|
| |
- model.Token.expiration > datetime.datetime.utcnow()
|
| |
- )
|
| |
- elif expired:
|
| |
- query = query.filter(
|
| |
- model.Token.expiration <= datetime.datetime.utcnow()
|
| |
- )
|
| |
-
|
| |
- if token:
|
| |
- query = query.filter(model.Token.id == token)
|
| |
- return query.first()
|
| |
- else:
|
| |
- return query.all()
|
| |
-
|
| |
-
|
| |
- def set_project_owner(session, project, user, required_groups=None):
|
| |
- """ Set the ownership of a project
|
| |
- :arg session: the session to use to connect to the database.
|
| |
- :arg project: a Project object representing the project's ownership to
|
| |
- change.
|
| |
- :arg user: a User object representing the new owner of the project.
|
| |
- :arg required_groups: a dict of {pattern: [list of groups]} the new user
|
| |
- should be in to become owner if one of the pattern matches the
|
| |
- project fullname.
|
| |
- :return: None
|
| |
- """
|
| |
-
|
| |
- if required_groups:
|
| |
- for key in required_groups:
|
| |
- if fnmatch.fnmatch(project.fullname, key):
|
| |
- user_grps = set(user.groups)
|
| |
- req_grps = set(required_groups[key])
|
| |
- if not user_grps.intersection(req_grps):
|
| |
- raise pagure.exceptions.PagureException(
|
| |
- "This user must be in one of the following groups "
|
| |
- "to be allowed to be added to this project: %s"
|
| |
- % ", ".join(req_grps)
|
| |
- )
|
| |
-
|
| |
- for contributor in project.users:
|
| |
- if user.id == contributor.id:
|
| |
- project.users.remove(contributor)
|
| |
- break
|
| |
- project.user = user
|
| |
- session.add(project)
|
| |
-
|
| |
-
|
| |
- def get_pagination_metadata(
|
| |
- flask_request, page, per_page, total, key_page="page"
|
| |
- ):
|
| |
- """
|
| |
- Returns pagination metadata for an API. The code was inspired by
|
| |
- Flask-SQLAlchemy.
|
| |
- :param flask_request: flask.request object
|
| |
- :param page: int of the current page
|
| |
- :param per_page: int of results per page
|
| |
- :param total: int of total results
|
| |
- :param key_page: the name of the argument corresponding to the page
|
| |
- :return: dictionary of pagination metadata
|
| |
- """
|
| |
- pages = int(ceil(total / float(per_page)))
|
| |
- request_args_wo_page = dict(copy.deepcopy(flask_request.args))
|
| |
- # Remove pagination related args because those are handled elsewhere
|
| |
- # Also, remove any args that url_for accepts in case the user entered
|
| |
- # those in
|
| |
- for key in [key_page, "per_page", "endpoint"]:
|
| |
- if key in request_args_wo_page:
|
| |
- request_args_wo_page.pop(key)
|
| |
- for key in flask_request.args:
|
| |
- if key.startswith("_"):
|
| |
- request_args_wo_page.pop(key)
|
| |
-
|
| |
- request_args_wo_page.update(flask_request.view_args)
|
| |
-
|
| |
- next_page = None
|
| |
- if page < pages:
|
| |
- request_args_wo_page.update({key_page: page + 1})
|
| |
- next_page = url_for(
|
| |
- flask_request.endpoint,
|
| |
- per_page=per_page,
|
| |
- _external=True,
|
| |
- **request_args_wo_page
|
| |
- )
|
| |
-
|
| |
- prev_page = None
|
| |
- if page > 1:
|
| |
- request_args_wo_page.update({key_page: page - 1})
|
| |
- prev_page = url_for(
|
| |
- flask_request.endpoint,
|
| |
- per_page=per_page,
|
| |
- _external=True,
|
| |
- **request_args_wo_page
|
| |
- )
|
| |
-
|
| |
- request_args_wo_page.update({key_page: 1})
|
| |
- first_page = url_for(
|
| |
- flask_request.endpoint,
|
| |
- per_page=per_page,
|
| |
- _external=True,
|
| |
- **request_args_wo_page
|
| |
- )
|
| |
-
|
| |
- request_args_wo_page.update({key_page: pages})
|
| |
- last_page = url_for(
|
| |
- flask_request.endpoint,
|
| |
- per_page=per_page,
|
| |
- _external=True,
|
| |
- **request_args_wo_page
|
| |
- )
|
| |
-
|
| |
- return {
|
| |
- key_page: page,
|
| |
- "pages": pages,
|
| |
- "per_page": per_page,
|
| |
- "prev": prev_page,
|
| |
- "next": next_page,
|
| |
- "first": first_page,
|
| |
- "last": last_page,
|
| |
- }
|
| |
-
|
| |
-
|
| |
- def update_star_project(session, repo, star, user):
|
| |
- """ Unset or set the star status depending on the star value.
|
| |
-
|
| |
- :arg session: the session to use to connect to the database.
|
| |
- :arg repo: a model.Project object representing the project to star/unstar
|
| |
- :arg star: '1' for starring and '0' for unstarring
|
| |
- :arg user: string representing the user
|
| |
- :return: None or string containing 'You starred this project' or
|
| |
- 'You unstarred this project'
|
| |
- """
|
| |
-
|
| |
- if not all([repo, user, star]):
|
| |
- return
|
| |
- user_obj = get_user(session, user)
|
| |
- msg = None
|
| |
- if star == "1":
|
| |
- msg = _star_project(session, repo=repo, user=user_obj)
|
| |
- elif star == "0":
|
| |
- msg = _unstar_project(session, repo=repo, user=user_obj)
|
| |
- return msg
|
| |
-
|
| |
-
|
| |
- def _star_project(session, repo, user):
|
| |
- """ Star a project
|
| |
-
|
| |
- :arg session: Session object to connect to db with
|
| |
- :arg repo: model.Project object representing the repo to star
|
| |
- :arg user: model.User object who is starring this repo
|
| |
- :return: None or string containing 'You starred this project'
|
| |
- """
|
| |
-
|
| |
- if not all([repo, user]):
|
| |
- return
|
| |
- stargazer_obj = model.Star(project_id=repo.id, user_id=user.id)
|
| |
- session.add(stargazer_obj)
|
| |
- return "You starred this project"
|
| |
-
|
| |
-
|
| |
- def _unstar_project(session, repo, user):
|
| |
- """ Unstar a project
|
| |
- :arg session: Session object to connect to db with
|
| |
- :arg repo: model.Project object representing the repo to unstar
|
| |
- :arg user: model.User object who is unstarring this repo
|
| |
- :return: None or string containing 'You unstarred this project'
|
| |
- or 'You never starred the project'
|
| |
- """
|
| |
-
|
| |
- if not all([repo, user]):
|
| |
- return
|
| |
- # First find the stargazer_obj object
|
| |
- stargazer_obj = _get_stargazer_obj(session, repo, user)
|
| |
- if isinstance(stargazer_obj, model.Star):
|
| |
- session.delete(stargazer_obj)
|
| |
- msg = "You unstarred this project"
|
| |
- else:
|
| |
- msg = "You never starred the project"
|
| |
- return msg
|
| |
-
|
| |
-
|
| |
- def _get_stargazer_obj(session, repo, user):
|
| |
- """ Query the db to find stargazer object with given repo and user
|
| |
- :arg session: Session object to connect to db with
|
| |
- :arg repo: model.Project object
|
| |
- :arg user: model.User object
|
| |
- :return: None or model.Star object
|
| |
- """
|
| |
-
|
| |
- if not all([repo, user]):
|
| |
- return
|
| |
- stargazer_obj = (
|
| |
- session.query(model.Star)
|
| |
- .filter(model.Star.project_id == repo.id)
|
| |
- .filter(model.Star.user_id == user.id)
|
| |
- )
|
| |
-
|
| |
- return stargazer_obj.first()
|
| |
-
|
| |
-
|
| |
- def has_starred(session, repo, user):
|
| |
- """ Check if a given user has starred a particular project
|
| |
-
|
| |
- :arg session: The session object to query the db with
|
| |
- :arg repo: model.Project object for which the star is checked
|
| |
- :arg user: The username of the user in question
|
| |
- :return: True if user has starred the project, False otherwise
|
| |
- """
|
| |
-
|
| |
- if not all([repo, user]):
|
| |
- return
|
| |
- user_obj = search_user(session, username=user)
|
| |
- stargazer_obj = _get_stargazer_obj(session, repo, user_obj)
|
| |
- if isinstance(stargazer_obj, model.Star):
|
| |
- return True
|
| |
- return False
|
| |
-
|
| |
-
|
| |
- def update_read_only_mode(session, repo, read_only=True):
|
| |
- """ Remove the read only mode from the project
|
| |
-
|
| |
- :arg session: The session object to query the db with
|
| |
- :arg repo: model.Project object to mark/unmark read only
|
| |
- :arg read_only: True if project is to be made read only,
|
| |
- False otherwise
|
| |
- """
|
| |
-
|
| |
- if (
|
| |
- not repo
|
| |
- or not isinstance(repo, model.Project)
|
| |
- or read_only not in [True, False]
|
| |
- ):
|
| |
- return
|
| |
- helper = pagure.lib.git_auth.get_git_auth_helper()
|
| |
- if helper.is_dynamic and read_only:
|
| |
- # No need to set a readonly flag if a dynamic auth backend is in use
|
| |
- return
|
| |
- if repo.read_only != read_only:
|
| |
- repo.read_only = read_only
|
| |
- session.add(repo)
|
| |
-
|
| |
-
|
| |
- def issues_history_stats(session, project):
|
| |
- """ Returns the number of opened issues on the specified project over
|
| |
- the last 365 days
|
| |
-
|
| |
- :arg session: The session object to query the db with
|
| |
- :arg repo: model.Project object to get the issues stats about
|
| |
-
|
| |
- """
|
| |
-
|
| |
- # Some ticket got imported as closed but without a closed_at date, so
|
| |
- # let's ignore them all
|
| |
- to_ignore = (
|
| |
- session.query(model.Issue)
|
| |
- .filter(model.Issue.project_id == project.id)
|
| |
- .filter(model.Issue.closed_at == None) # noqa
|
| |
- .filter(model.Issue.status == "Closed")
|
| |
- .count()
|
| |
- )
|
| |
-
|
| |
- # For each week from tomorrow, get the number of open tickets
|
| |
- tomorrow = datetime.datetime.utcnow() + datetime.timedelta(days=1)
|
| |
- output = {}
|
| |
- for week in range(53):
|
| |
- start = tomorrow - datetime.timedelta(days=(week * 7))
|
| |
- closed_ticket = (
|
| |
- session.query(model.Issue)
|
| |
- .filter(model.Issue.project_id == project.id)
|
| |
- .filter(model.Issue.closed_at >= start)
|
| |
- .filter(model.Issue.date_created <= start)
|
| |
- )
|
| |
- open_ticket = (
|
| |
- session.query(model.Issue)
|
| |
- .filter(model.Issue.project_id == project.id)
|
| |
- .filter(model.Issue.status == "Open")
|
| |
- .filter(model.Issue.date_created <= start)
|
| |
- )
|
| |
- cnt = open_ticket.count() + closed_ticket.count() - to_ignore
|
| |
- if cnt < 0:
|
| |
- cnt = 0
|
| |
- output[start.isoformat()] = cnt
|
| |
-
|
| |
- return output
|
| |
-
|
| |
-
|
| |
- def get_authorized_project(
|
| |
- session, project_name, user=None, namespace=None, asuser=None
|
| |
- ):
|
| |
- """ Retrieving the project with user permission constraint
|
| |
-
|
| |
- :arg session: The SQLAlchemy session to use
|
| |
- :type session: sqlalchemy.orm.session.Session
|
| |
- :arg project_name: Name of the project on pagure
|
| |
- :type project_name: String
|
| |
- :arg user: Pagure username
|
| |
- :type user: String
|
| |
- :arg namespace: Pagure namespace
|
| |
- :type namespace: String
|
| |
- :arg asuser: Username to check for access
|
| |
- :type asuser: String
|
| |
- :return: The project object if project is public or user has
|
| |
- permissions for the project else it returns None
|
| |
- :rtype: Project
|
| |
-
|
| |
- """
|
| |
- repo = pagure.lib._get_project(session, project_name, user, namespace)
|
| |
-
|
| |
- if repo and repo.private and not pagure.utils.is_repo_user(repo, asuser):
|
| |
- return None
|
| |
-
|
| |
- return repo
|
| |
-
|
| |
-
|
| |
- def get_project_family(session, project):
|
| |
- """ Retrieve the family of the specified project, ie: all the forks
|
| |
- of the main project.
|
| |
- If the specified project is a fork, let's work our way up the chain
|
| |
- until we find the main project so we can go down and get all the forks
|
| |
- and the forks of the forks (but not one level more).
|
| |
-
|
| |
- :arg session: The SQLAlchemy session to use
|
| |
- :type session: sqlalchemy.orm.session.Session
|
| |
- :arg project: The project whose family is searched
|
| |
- :type project: pagure.lib.model.Project
|
| |
-
|
| |
- """
|
| |
- parent = project
|
| |
- while parent.is_fork:
|
| |
- parent = parent.parent
|
| |
-
|
| |
- sub = session.query(sqlalchemy.distinct(model.Project.id)).filter(
|
| |
- model.Project.parent_id == parent.id
|
| |
- )
|
| |
- query = (
|
| |
- session.query(model.Project)
|
| |
- .filter(
|
| |
- sqlalchemy.or_(
|
| |
- model.Project.parent_id.in_(sub.subquery()),
|
| |
- model.Project.parent_id == parent.id,
|
| |
- )
|
| |
- )
|
| |
- .filter(model.Project.user_id == model.User.id)
|
| |
- .order_by(model.User.user)
|
| |
- )
|
| |
-
|
| |
- return [parent] + query.all()
|
| |
-
|
| |
-
|
| |
- def link_pr_issue(session, issue, request):
|
| |
- """ Associate the specified issue with the specified pull-requets.
|
| |
-
|
| |
- :arg session: The SQLAlchemy session to use
|
| |
- :type session: sqlalchemy.orm.session.Session
|
| |
- :arg issue: The issue mentioned in the commits of the pull-requests to
|
| |
- be associated with
|
| |
- :type issue: pagure.lib.model.Issue
|
| |
- :arg request: A pull-request to associate the specified issue with
|
| |
- :type request: pagure.lib.model.PullRequest
|
| |
-
|
| |
- """
|
| |
-
|
| |
- associated_issues = [iss.uid for iss in request.related_issues]
|
| |
- if issue.uid not in associated_issues:
|
| |
- obj = model.PrToIssue(
|
| |
- pull_request_uid=request.uid, issue_uid=issue.uid
|
| |
- )
|
| |
- session.add(obj)
|
| |
- session.flush()
|
| |
-
|
| |
-
|
| |
- def remove_user_of_project(session, user, project, agent):
|
| |
- """ Remove the specified user from the given project.
|
| |
-
|
| |
- :arg session: the session with which to connect to the database.
|
| |
- :arg user: an pagure.lib.model.User object to remove from the project.
|
| |
- :arg project: an pagure.lib.model.Project object from which to remove
|
| |
- the specified user.
|
| |
- :arg agent: the username of the user performing the action.
|
| |
-
|
| |
- """
|
| |
-
|
| |
- userids = [u.id for u in project.users]
|
| |
-
|
| |
- if user.id not in userids:
|
| |
- raise pagure.exceptions.PagureException(
|
| |
- "User does not have any access on the repo"
|
| |
- )
|
| |
-
|
| |
- for u in project.users:
|
| |
- if u.id == user.id:
|
| |
- user = u
|
| |
- project.users.remove(u)
|
| |
- break
|
| |
-
|
| |
- # Mark the project as read_only, celery will unmark it
|
| |
- update_read_only_mode(session, project, read_only=True)
|
| |
- session.commit()
|
| |
-
|
| |
- pagure.lib.git.generate_gitolite_acls(project=project)
|
| |
- pagure.lib.notify.log(
|
| |
- project,
|
| |
- topic="project.user.removed",
|
| |
- msg=dict(
|
| |
- project=project.to_json(public=True),
|
| |
- removed_user=user.username,
|
| |
- agent=agent,
|
| |
- ),
|
| |
- )
|
| |
-
|
| |
- return "User removed"
|
| |