Diff
42 commits, 56 files changed
+1290 -648

Do not serve svg inline
Michael Scherer • 5 years ago  
Escape html in author name
Michael Scherer • 5 years ago  
file modified
+53
@@ -1,6 +1,59 @@

  Upgrading Pagure

  ================

  

+ From 4.0 or 4.0.1 to 4.0.2

+ --------------------------

+ 

+ This is an important security release fixing CVE-2018-1002151.

+ This CVE would let anyone with an API token with the modify_project ACL create

+ any git branches in any project.

+ Create git branches via the API now has a dedicated ACL, so if you are using the

+ API to create git branches, you will have to get a new API token with this ACL.

+ 

+ 

+ From 3.13.1 to 4.0

+ ------------------

+ 

+ The release 4.0 brings in some major changes (thus the bump in major version).

+ The API and user facing code should not have changed but many of the internal

+ pieces have. So if you have a script that used some parts of the internal APIs,

+ it will likely break.

+ 

+ This release also includes some changes to the database schema:

+ 

+ * Update the data schema using alembic: ``alembic upgrade head``

+ 

+ And some new configuration keys:

+ * SESSION_TYPE

+ * GITOLITE_HAS_COMPILE_1

+ * FAST_CELERY_QUEUE

+ * MEDIUM_CELERY_QUEUE

+ * SLOW_CELERY_QUEUE

+ * STOMP_NOTIFICATIONS

+ * STOMP_BROKERS

+ * STOMP_HIERARCHY

+ * STOMP_SSL

+ * STOMP_KEY_FILE

+ * STOMP_CERT_FILE

+ * STOMP_CREDS_PASSWORD

+ * PROJECT_TEMPLATE_PATH

+ * FORK_TEMPLATE_PATH

+ * ENABLE_DOCS

+ * FEDMSG_NOTIFICATIONS

+ * ALWAYS_FEDMSG_ON_COMMITS

+ * FLAG_STATUSES_LABELS

+ * FLAG_SUCCESS

+ * FLAG_FAILURE

+ * FLAG_PENDING

+ 

+ The following configuration keys have been removed/deprecated:

+ * DOCS_FOLDER

+ * REQUESTS_FOLDER

+ * TICKETS_FOLDER

+ 

+ All of these are documentated at: https://docs.pagure.org/pagure/configuration.html

+ 

+ 

  From 3.13 to 3.13.1

  -------------------

  

@@ -5,7 +5,6 @@

    with_items:

      - python-redis

      - python-trollius

-     - python-trollius-redis

      - redis

  

  

file modified
+1 -1
@@ -12,7 +12,7 @@

                     python-openid-teams python-straight-plugin python-wtforms python-munch \

                     python-enum34 python-redis python-sqlalchemy systemd gitolite3 python-filelock \

                     python-fedora-flask python2-pillow python2-psycopg2 python-trollius \

-                    python-trollius-redis python-celery

+                    python-celery

  

  WORKDIR /code

  ENTRYPOINT ["/usr/bin/python", "/code/pagure-ev/pagure_stream_server.py"]

file modified
+54
@@ -3,6 +3,60 @@

  

  This document records all notable changes to `Pagure <https://pagure.io>`_.

  

+ 4.0.4 (2018-07-19)

+ ------------------

+ 

+ .. note:: This release fixes CVE-2018-1002155, CVE-2018-1002156,

+         CVE-2018-1002157, CVE-2018-1002153

+ 

+ - Ensure the project's description does not contain any javascript (Michael

+   Scherer)

+ - Prevent the project's URL to be anything other than an URL

+ - Escape any html people may have injected in their author name in commits

+   (Michael Scherer)

+ - Do not serve SVG inline (Michael Scherer)

+ 

+   - The four items above constitute CVE-2018-1002155

+ 

+ - Catch exception raised by pagure-ci when it fails to find a build on jenkins

+ - Fix RELATES and FIXES regex to cover projects with a dash in their name

+ - Support calls from jenkins indicating the build is started

+ - Ensure we check the required group membership when giving a project away

+ - Add missing titles to the milestones table in the settings

+ - Properly inform the user if they are introducing a duplicated tag

+ - Only select the default template when creating a new ticket

+ - Fix the subscribe button on the PR page

+ - Fix updating a remote PR

+ - Fix showing the 'more' button on the overview page

+ - Multiple fixes to the pagure-milter

+ - Fix triggering CI checks on new comments added to a PR

+ - Fix logging and the SMTPHandler

+ - Do not notify everyone about private tickets (CVE-2018-1002157)

+ - Make the settings of a project private (CVE-2018-1002156)

+ - Ensure the git repo of private projects aren't exposed via https

+   (CVE-2018-1002153)

+ - Do not log activity on private projects

+ - Drop trollius-redis requirement (Neal Gompa)

+ 

+ 4.0.3 (2018-05-14)

+ ------------------

+ 

+ - Backport utility method from the 4.1 code to fix the 4.0.2 release

+ 

+ 4.0.2 (2018-05-14)

+ ------------------

+ 

+ .. note:: This release fixes CVE-2018-1002151

+ 

+ - Fix showing the list of issues in a timely fashion (Patrick Uiterwijk)

+ - Fix stats for commits without author (Lubomír Sedlář)

+ - Explain how to fetch a pull request locally and some grammar fixes

+   (Todd Zullinger)

+ - Drop the constraint on the requirement on straight.plugin but document it

+ - Fix the requirement on bcrypt, it's optional

+ - Make API endpoint for creating new git branch have its own ACL

+   fixes CVE-2018-1002151

+ 

  4.0.1 (2018-04-26)

  ------------------

  

file modified
+89
@@ -646,6 +646,95 @@

  Where `<foo>` and `<bar>` must be replaced by your values.

  

  

+ LOGGING

+ ~~~~~~~

+ 

+ This configuration key allows you to set up the logging of the application.

+ It relies on the standard `python logging module

+ <https://docs.python.org/2/library/logging.html>`_.

+ 

+ The default value is:

+ 

+ ::

+ 

+     LOGGING = {

+         'version': 1,

+         'disable_existing_loggers': False,

+         'formatters': {

+             'standard': {

+                 'format': '%(asctime)s [%(levelname)s] %(name)s: %(message)s'

+             },

+             'email_format': {

+                 'format': MSG_FORMAT

+             }

+         },

+         'filters': {

+             'myfilter': {

+                 '()': ContextInjector,

+             }

+         },

+         'handlers': {

+             'console': {

+                 'level': 'INFO',

+                 'formatter': 'standard',

+                 'class': 'logging.StreamHandler',

+                 'stream': 'ext://sys.stdout',

+             },

+             'email': {

+                 'level': 'ERROR',

+                 'formatter': 'email_format',

+                 'class': 'logging.handlers.SMTPHandler',

+                 'mailhost': 'localhost',

+                 'fromaddr': 'pagure@localhost',

+                 'toaddrs': 'root@localhost',

+                 'subject': 'ERROR on pagure',

+                 'filters': ['myfilter'],

+             },

+         },

+         # The root logger configuration; this is a catch-all configuration

+         # that applies to all log messages not handled by a different logger

+         'root': {

+             'level': 'INFO',

+             'handlers': ['console'],

+         },

+         'loggers': {

+             'pagure': {

+                 'handlers': ['console'],

+                 'level': 'DEBUG',

+                 'propagate': True

+             },

+             'flask': {

+                 'handlers': ['console'],

+                 'level': 'INFO',

+                 'propagate': False

+             },

+             'sqlalchemy': {

+                 'handlers': ['console'],

+                 'level': 'WARN',

+                 'propagate': False

+             },

+             'binaryornot': {

+                 'handlers': ['console'],

+                 'level': 'WARN',

+                 'propagate': True

+             },

+             'pagure.lib.encoding_utils': {

+                 'handlers': ['console'],

+                 'level': 'WARN',

+                 'propagate': False

+             },

+         }

+     }

+ 

+ .. note:: as you can see there is an ``email`` handler defined. It's not used

+     anywhere by default but you can use it to get report of errors by email

+     and thus monitor your pagure instance.

+     To do this the easiest is to set, on the ``root`` logger:

+     ::

+ 

+         'handlers': ['console', 'email'],

+ 

+ 

  ITEM_PER_PAGE

  ~~~~~~~~~~~~~

  

file modified
+7 -4
@@ -3,23 +3,23 @@

  

  Pagure would be nothing without its contributors.

  

- On April 26, 2018 (release 4.0.1) the list looks as follow:

+ On July 19, 2018 (release 4.0.4) the list looks as follow:

  

  =================  ===========

  Number of commits  Contributor

  =================  ===========

-   5831              Pierre-Yves Chibon <pingou@pingoured.fr>

+   5867              Pierre-Yves Chibon <pingou@pingoured.fr>

     193              Ryan Lerch <rlerch@redhat.com>

     172              Vivek Anand <vivekanand1101@gmail.com>

     139              farhaanbukhsh <farhaan.bukhsh@gmail.com>

-    130              Patrick Uiterwijk <puiterwijk@redhat.com>

+    131              Patrick Uiterwijk <puiterwijk@redhat.com>

     125              Clement Verna <cverna@tutanota.com>

      85              Farhaan Bukhsh <farhaan.bukhsh@gmail.com>

      59              Johan Cwiklinski <johan@x-tnd.be>

      47              Mark Reynolds <mreynolds@redhat.com>

      32              Matt Prahl <mprahl@redhat.com>

      32              Pradeep CE (cep) <breathingcode@gmail.com>

-     30              Lubomír Sedlář <lsedlar@redhat.com>

+     31              Lubomír Sedlář <lsedlar@redhat.com>

      26              Slavek Kabrda <bkabrda@redhat.com>

      23              rahul Bajaj <you@example.com>

      20              Jeremy Cline <jeremy@jcline.org>
@@ -74,6 +74,7 @@

       2              Carlos Mogas da Silva <r3pek@r3pek.org>

       2              Daniel Mach <dmach@redhat.com>

       2              Kamil Páral <kparal@redhat.com>

+      2              Michael Scherer <misc@redhat.com>

       2              Nuno Maltez <nuno@cognitiva.com>

       2              Ompragash <om.apsara@gmail.com>

       2              Peter Oliver <git@mavit.org.uk>
@@ -82,6 +83,7 @@

       2              Richard Marko <rmarko@fedoraproject.org>

       2              Simo Sorce <simo@redhat.com>

       2              Tim Flink <tflink@fedoraproject.org>

+      2              Todd Zullinger <tmz@pobox.com>

       2              William Moreno Reyes <williamjmorenor@gmail.com>

       2              bruno <bruno@wolff.to>

       2              dhrish20 <dhrish20@gmail.com>
@@ -104,6 +106,7 @@

       1              Kunaal Jain <kunaalus@gmail.com>

       1              Mathew Robinson <mathew.robinson3114@gmail.com>

       1              Mohan Boddu <mboddu@redhat.com>

+      1              Neal Gompa <ngompa13@gmail.com>

       1              Neha Kandpal <iec2015048@iiita.ac.in>

       1              Peter Kolínek <fedora@pessoft.com>

       1              Robert Bost <rbost@redhat.com>

@@ -22,7 +22,6 @@

  

      python-jenkins

      python-redis

-     python-trollius-redis

      python-trollius

  

  .. note:: We ship a systemd unit file for pagure_ci but we welcome patches

@@ -17,7 +17,6 @@

  

      python-redis

      python-trollius

-     python-trollius-redis

  

  .. note:: We ship a systemd unit file for pagure_milter but we welcome patches

          for scripts for other init systems.

@@ -14,7 +14,6 @@

  ::

  

      python-redis

-     python-trollius-redis

      python-trollius

  

  .. note:: We ship a systemd unit file for pagure_loadjson but we welcome patches

@@ -14,7 +14,6 @@

  ::

  

      python-redis

-     python-trollius-redis

      python-trollius

  

  .. note:: We ship a systemd unit file for pagure_logcom but we welcome patches

@@ -18,7 +18,6 @@

  

      python-redis

      python-trollius

-     python-trollius-redis

  

  .. note:: We ship a systemd unit file for pagure_webhook but we welcome patches

          for scripts for other init systems.

@@ -56,7 +56,7 @@

  

      FORMAT: JSON

      PROTOCOL: HTTP

-     EVENT: Job Finalized

+     EVENT: All Events

      URL: <The URL provided in the Pagure CI hook on pagure>

      TIMEOUT: 3000

      LOG: 1

file modified
+20 -1
@@ -107,7 +107,26 @@

  Working with Pull Requests

  --------------------------

  It's quite common to work with a pull request locally, either to build on top of

- it or to test it. You can do this by editing your git configuration as follow.

+ it or to test it. You can do this easily using ``git fetch`` to download the

+ pull request followed by ``git checkout`` to work with it as you would any

+ local branch.  The syntax for ``git fetch`` is: ::

+ 

+     git fetch $REMOTE pull/$PR_NUMBER/head:$BRANCHNAME

+ 

+ For example, if you have PR#1 which "adds support for foo" you might run: ::

+ 

+     git fetch origin pull/1/head:add-foo-support

+ 

+ Then you can work with the ``add-foo-support`` normally: ::

+ 

+     git checkout add-foo-support

+ 

+ .. note:: You may use ``/`` characters in your branch name if you want to group

+           your pull requests by the submitter name, bug number, etc.  For

+           example, you could name your local branch ``user/add-foo-support``.

+ 

+ If you want to allow working with all of your pull requests locally, you can do

+ so by editing your git configuration as follows.

  Locate your remote in the ``.git/config`` file, for example::

  

      [remote "origin"]

file modified
+10 -11
@@ -1,5 +1,5 @@

  Name:               pagure

- Version:            4.0.1

+ Version:            4.0.4

  Release:            1%{?dist}

  Summary:            A git-centered forge

  
@@ -175,11 +175,9 @@

  %if (0%{?fedora} && 0%{?fedora} <= 27) || (0%{?rhel} && 0%{?rhel} <= 7)

  Requires:           python-redis

  Requires:           python-trollius

- Requires:           python-trollius-redis

  %else

  Requires:           python2-redis

  Requires:           python2-trollius

- Requires:           python2-trollius-redis

  %endif

  %{?systemd_requires}

  %description        ev
@@ -195,11 +193,9 @@

  %if (0%{?fedora} && 0%{?fedora} <= 27) || (0%{?rhel} && 0%{?rhel} <= 7)

  Requires:           python-redis

  Requires:           python-trollius

- Requires:           python-trollius-redis

  %else

  Requires:           python2-redis

  Requires:           python2-trollius

- Requires:           python2-trollius-redis

  %endif

  %{?systemd_requires}

  %description        webhook
@@ -215,12 +211,10 @@

  %if (0%{?fedora} && 0%{?fedora} <= 27) || (0%{?rhel} && 0%{?rhel} <= 7)

  Requires:           python-redis

  Requires:           python-trollius

- Requires:           python-trollius-redis

  Requires:           python-jenkins

  %else

  Requires:           python2-redis

  Requires:           python2-trollius

- Requires:           python2-trollius-redis

  Requires:           python2-jenkins

  %endif

  %{?systemd_requires}
@@ -239,11 +233,9 @@

  %if (0%{?fedora} && 0%{?fedora} <= 27) || (0%{?rhel} && 0%{?rhel} <= 7)

  Requires:           python-redis

  Requires:           python-trollius

- Requires:           python-trollius-redis

  %else

  Requires:           python2-redis

  Requires:           python2-trollius

- Requires:           python2-trollius-redis

  %endif

  %{?systemd_requires}

  %description        logcom
@@ -259,11 +251,9 @@

  %if (0%{?fedora} && 0%{?fedora} <= 27) || (0%{?rhel} && 0%{?rhel} <= 7)

  Requires:           python-redis

  Requires:           python-trollius

- Requires:           python-trollius-redis

  %else

  Requires:           python2-redis

  Requires:           python2-trollius

- Requires:           python2-trollius-redis

  %endif

  %{?systemd_requires}

  %description        loadjson
@@ -453,6 +443,15 @@

  

  

  %changelog

+ * Thu Jul 19 2018 Pierre-Yves Chibon <pingou@pingoured.fr> - 4.0.4-1

+ - Update to 4.0.4

+ 

+ * Mon May 14 2018 Pierre-Yves Chibon <pingou@pingoured.fr> - 4.0.3-1

+ - Update to 4.0.3

+ 

+ * Mon May 14 2018 Pierre-Yves Chibon <pingou@pingoured.fr> - 4.0.2-1

+ - Update to 4.0.2

+ 

  * Thu Apr 26 2018 Pierre-Yves Chibon <pingou@pingoured.fr> - 4.0.1-1

  - Update to 4.0.1

  

@@ -20,7 +20,8 @@

  

  from Milter.utils import parse_addr

  

- import pagure

+ import pagure.config

+ import pagure.lib

  

  

  if 'PAGURE_CONFIG' not in os.environ \
@@ -92,20 +93,22 @@

          # on the MTA.

          self.fp = StringIO.StringIO()

          self.canon_from = '@'.join(parse_addr(mailfrom))

-         self.fp.write('From %s %s\n' % (self.canon_from, time.ctime()))

+         from_txt = 'From %s %s\n' % (self.canon_from, time.ctime())

+         self.fp.write(from_txt.encode('utf-8'))

          return Milter.CONTINUE

  

      @Milter.noreply

      def header(self, name, hval):

          ''' Headers '''

          # add header to buffer

-         self.fp.write("%s: %s\n" % (name, hval))

+         header_txt = "%s: %s\n" % (name, hval)

+         self.fp.write(header_txt.encode('utf-8'))

          return Milter.CONTINUE

  

      @Milter.noreply

      def eoh(self):

          ''' End of Headers '''

-         self.fp.write("\n")

+         self.fp.write(b"\n")

          return Milter.CONTINUE

  

      @Milter.noreply
@@ -153,12 +156,14 @@

          # they are trying to forge their ID into someone else's

          salt = _config.get('SALT_EMAIL')

          from_email = clean_item(msg['From'])

+         session = pagure.lib.create_session(_config['DB_URL'])

          try:

-             user = pagure.lib.get_user(pagure.SESSION, from_email)

+             user = pagure.lib.get_user(session, from_email)

          except:

              self.log(

                  "Could not find an user in the DB associated with %s" %

                  from_email)

+             session.remove()

              return Milter.CONTINUE

  

          hashes = []
@@ -171,22 +176,27 @@

              self.log('hash list: %s' % hashes)

              self.log('tohash:    %s' % tohash)

              self.log('Hash does not correspond to the destination')

+             session.remove()

              return Milter.CONTINUE

  

          if msg['From'] and msg['From'] == _config.get('FROM_EMAIL'):

              self.log("Let's not process the email we send")

+             session.remove()

              return Milter.CONTINUE

  

          msg_id = clean_item(msg_id)

  

          if msg_id and '-ticket-' in msg_id:

              self.log('Processing issue')

+             session.remove()

              return self.handle_ticket_email(msg, msg_id)

          elif msg_id and '-pull-request-' in msg_id:

              self.log('Processing pull-request')

+             session.remove()

              return self.handle_request_email(msg, msg_id)

          else:

              self.log('Not a pagure ticket or pull-request email, let it go')

+             session.remove()

              return Milter.CONTINUE

  

      def handle_ticket_email(self, emailobj, msg_id):

file modified
+1 -1
@@ -9,5 +9,5 @@

  """

  

  

- __version__ = '4.0.1'

+ __version__ = '4.0.4'

  __api_version__ = '0.23'

file modified
+10
@@ -71,6 +71,16 @@

          _log.debug("Bad Request: No build ID retrieved")

          raise pagure.exceptions.APIError(400, error_code=APIERROR.EINVALIDREQ)

  

+     build_phase = data.get('build', {}).get('phase')

+     if not build_phase:

+         _log.debug("Bad Request: No build phase retrieved")

+         raise pagure.exceptions.APIError(400, error_code=APIERROR.EINVALIDREQ)

+     if build_phase not in ["STARTED", "FINALIZED"]:

+         _log.debug(

+             "Ignoring phase: %s - not in the list: STARTED, FINALIZED",

+             build_phase)

+         raise pagure.exceptions.APIError(400, error_code=APIERROR.EINVALIDREQ)

+ 

      try:

          lib_ci.process_jenkins_build(

              flask.g.session,

file modified
+9 -5
@@ -22,7 +22,7 @@

  from pagure.api import (API, api_method, api_login_required, APIERROR,

                          get_authorized_api_project)

  from pagure.config import config as pagure_config

- from pagure.utils import is_repo_committer, api_authenticated

+ from pagure.utils import is_repo_committer

  

  

  _log = logging.getLogger(__name__)
@@ -849,10 +849,10 @@

          raise pagure.exceptions.APIError(

              404, error_code=APIERROR.EPULLREQUESTSDISABLED)

  

-     if api_authenticated():

-         if flask.g.token.project and repo != flask.g.token.project:

-             raise pagure.exceptions.APIError(

-                 401, error_code=APIERROR.EINVALIDTOK)

+     if flask.g.token and flask.g.token.project \

+             and repo != flask.g.token.project:

+         raise pagure.exceptions.APIError(

+             401, error_code=APIERROR.EINVALIDTOK)

  

      request = pagure.lib.search_pull_requests(

          flask.g.session, project_id=repo.id, requestid=requestid)
@@ -991,6 +991,10 @@

      if repo is None:

          raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOPROJECT)

  

+     if flask.g.token.project and repo != flask.g.token.project:

+         raise pagure.exceptions.APIError(

+             401, error_code=APIERROR.EINVALIDTOK)

+ 

      form = pagure.forms.RequestPullForm(csrf_enabled=False)

      if not form.validate_on_submit():

          raise pagure.exceptions.APIError(

file modified
+14 -2
@@ -222,7 +222,7 @@

      git_urls = {}

  

      git_url_ssh = pagure_config.get('GIT_URL_SSH')

-     if pagure.utils.authenticated() and git_url_ssh:

+     if pagure.utils.api_authenticated() and git_url_ssh:

          try:

              git_url_ssh = git_url_ssh.format(

                  username=flask.g.fas_user.username)
@@ -965,6 +965,10 @@

          raise pagure.exceptions.APIError(

              404, error_code=APIERROR.ENOPROJECT)

  

+     if flask.g.token.project and project != flask.g.token.project:

+         raise pagure.exceptions.APIError(

+             401, error_code=APIERROR.EINVALIDTOK)

+ 

      is_site_admin = pagure.utils.is_admin()

      admins = [u.username for u in project.get_project_users('admin')]

      # Only allow the main admin, the admins of the project, and Pagure site
@@ -1193,6 +1197,10 @@

      if not project:

          raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOPROJECT)

  

+     if flask.g.token.project and project != flask.g.token.project:

+         raise pagure.exceptions.APIError(

+             401, error_code=APIERROR.EINVALIDTOK)

+ 

      # Check if it's JSON or form data

      if flask.request.headers.get('Content-Type') == 'application/json':

          # Set force to True to ignore the mimetype. Set silent so that None is
@@ -1226,7 +1234,7 @@

  @API.route('/fork/<username>/<repo>/git/branch', methods=['POST'])

  @API.route('/fork/<username>/<namespace>/<repo>/git/branch',

             methods=['POST'])

- @api_login_required(acls=['modify_project'])

+ @api_login_required(acls=['create_branch'])

  @api_method

  def api_new_branch(repo, username=None, namespace=None):

      """
@@ -1274,6 +1282,10 @@

      if not project:

          raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOPROJECT)

  

+     if flask.g.token.project and project != flask.g.token.project:

+         raise pagure.exceptions.APIError(

+             401, error_code=APIERROR.EINVALIDTOK)

+ 

      # Check if it's JSON or form data

      if flask.request.headers.get('Content-Type') == 'application/json':

          # Set force to True to ignore the mimetype. Set silent so that None is

file modified
-12
@@ -58,18 +58,6 @@

                "custom_keys": [],

                "description": "",

                "parent": null,

-               "settings": {

-                 "issues_default_to_private": false,

-                 "Minimum_score_to_merge_pull-request": -1,

-                 "Web-hooks": None,

-                 "fedmsg_notifications": true,

-                 "always_merge": false,

-                 "project_documentation": true,

-                 "Enforce_signed-off_commits_in_pull-request": false,

-                 "pull_requests": true,

-                 "Only_assignee_can_merge_pull-request": false,

-                 "issue_tracker": true

-               },

                "tags": [],

                "namespace": None,

                "priorities": {},

file modified
+31 -9
@@ -11,6 +11,8 @@

  import os

  from datetime import timedelta

  

+ from pagure.mail_logging import ContextInjector, MSG_FORMAT

+ 

  

  # Set the time after which the admin session expires

  ADMIN_SESSION_LIFETIME = timedelta(minutes=20)
@@ -263,26 +265,27 @@

  

  

  ACLS = {

+     'create_branch': 'Create a git branch on a project',

      'create_project': 'Create a new project',

+     'commit_flag': 'Flag a commit',

      'fork_project': 'Fork a project',

+     'generate_acls_project': 'Generate the Gitolite ACLs on a project',

      'issue_assign': 'Assign issue to someone',

-     'issue_create': 'Create a new ticket',

      'issue_change_status': 'Change the status of a ticket',

      'issue_comment': 'Comment on a ticket',

+     'issue_create': 'Create a new ticket',

+     'issue_subscribe': 'Subscribe the user with this token to an issue',

+     'issue_update': 'Update an issue, status, comments, custom fields...',

+     'issue_update_custom_fields': 'Update the custom fields of an issue',

+     'issue_update_milestone': 'Update the milestone of an issue',

+     'modify_project': 'Modify an existing project',

+     'pull_request_create': 'Open a new pull-request',

      'pull_request_close': 'Close a pull-request',

      'pull_request_comment': 'Comment on a pull-request',

-     'pull_request_create': 'Open a new pull-request',

      'pull_request_flag': 'Flag a pull-request',

      'pull_request_merge': 'Merge a pull-request',

      'pull_request_subscribe':

          'Subscribe the user with this token to a pull-request',

-     'issue_subscribe': 'Subscribe the user with this token to an issue',

-     'issue_update': 'Update an issue, status, comments, custom fields...',

-     'issue_update_custom_fields': 'Update the custom fields of an issue',

-     'issue_update_milestone': 'Update the milestone of an issue',

-     'modify_project': 'Modify an existing project',

-     'generate_acls_project': 'Generate the Gitolite ACLs on a project',

-     'commit_flag': 'Flag a commit',

  }

  

  # List of ACLs which a regular user is allowed to associate to an API token
@@ -307,6 +310,7 @@

      'pull_request_merge',

      'generate_acls_project',

      'commit_flag',

+     'create_branch',

  ]

  

  # Bootstrap URLS
@@ -358,6 +362,14 @@

          'standard': {

              'format': '%(asctime)s [%(levelname)s] %(name)s: %(message)s'

          },

+         'email_format': {

+             'format': MSG_FORMAT

+         }

+     },

+     'filters': {

+         'myfilter': {

+             '()': ContextInjector,

+         }

      },

      'handlers': {

          'console': {
@@ -366,6 +378,16 @@

              'class': 'logging.StreamHandler',

              'stream': 'ext://sys.stdout',

          },

+         'email': {

+             'level': 'ERROR',

+             'formatter': 'email_format',

+             'class': 'logging.handlers.SMTPHandler',

+             'mailhost': 'localhost',

+             'fromaddr': 'pagure@localhost',

+             'toaddrs': 'root@localhost',

+             'subject': 'ERROR on pagure',

+             'filters': ['myfilter'],

+         },

      },

      # The root logger configuration; this is a catch-all configuration

      # that applies to all log messages not handled by a different logger

file modified
+1 -15
@@ -11,7 +11,6 @@

  import datetime

  import gc

  import logging

- import logging.config

  import time

  import os

  
@@ -38,8 +37,6 @@

      perfrepo = None

  

  

- logging.basicConfig()

- logging.config.dictConfig(pagure_config.get('LOGGING') or {'version': 1})

  logger = logging.getLogger(__name__)

  

  REDIS = None
@@ -69,8 +66,7 @@

          import flask_session

          flask_session.Session(app)

  

-     logging.basicConfig()

-     logging.config.dictConfig(app.config.get('LOGGING') or {'version': 1})

+     pagure.utils.set_up_logging(app=app)

  

      app.jinja_env.trim_blocks = True

      app.jinja_env.lstrip_blocks = True
@@ -120,16 +116,6 @@

          oidc.init_app(app)

          app.before_request(fas_user_from_oidc)

  

-     # Report error by email

-     if not app.debug and not pagure_config.get('DEBUG', False):

-         app.logger.addHandler(pagure.mail_logging.get_mail_handler(

-             smtp_server=pagure_config.get('SMTP_SERVER', '127.0.0.1'),

-             mail_admin=pagure_config.get(

-                 'MAIL_ADMIN', pagure_config['EMAIL_ERROR']),

-             from_email=pagure_config.get(

-                 'FROM_EMAIL', 'pagure@fedoraproject.org')

-         ))

- 

      # Support proxy

      app.wsgi_app = pagure.proxy.ReverseProxied(app.wsgi_app)

  

file modified
+9 -2
@@ -27,6 +27,7 @@

  

  import pagure.lib

  from pagure.config import config as pagure_config

+ from pagure.utils import urlpattern

  

  

  STRICT_REGEX = '^[a-zA-Z0-9-_]+$'
@@ -124,7 +125,10 @@

      )

      url = wtforms.TextField(

          'URL',

-         [wtforms.validators.optional()]

+         [

+             wtforms.validators.optional(),

+             wtforms.validators.Regexp(urlpattern, flags=re.IGNORECASE),

+         ]

      )

      avatar_email = wtforms.TextField(

          'Avatar email',
@@ -481,7 +485,10 @@

      comment = wtforms.TextAreaField(

          'Comment', [wtforms.validators.Required()])

      url = wtforms.TextField(

-         'URL', [wtforms.validators.Required()])

+         'URL', [

+             wtforms.validators.Required(),

+             wtforms.validators.Regexp(urlpattern, flags=re.IGNORECASE),

+         ])

      uid = wtforms.TextField(

          'UID', [wtforms.validators.optional()])

  

file modified
+64 -34
@@ -1283,20 +1283,20 @@

              '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:

-             pagure.lib.tasks_services.trigger_ci_build.delay(

-                 project_name=request.project_from.fullname,

-                 cause=request.id,

-                 branch=request.branch_from,

-                 ci_type=request.project.ci_hook.ci_type

-             )

+     # 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:

+         pagure.lib.tasks_services.trigger_ci_build.delay(

+             project_name=request.project_from.fullname,

+             cause=request.id,

+             branch=request.branch_from,

+             ci_type=request.project.ci_hook.ci_type

+         )

  

      pagure.lib.notify.log(

          request.project,
@@ -2501,15 +2501,24 @@

                  query = query.filter(model.Issue.uid.in_(list(final_set)))

  

      if assignee is not None:

-         if str(assignee).lower() not in ['false', '0', 'true', '1']:

-             user2 = aliased(model.User)

+         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 == user2.id

-                 ).filter(

-                     user2.user == assignee[1:]

+                     model.Issue.assignee_id == userassignee

                  )

  

                  query = query.filter(
@@ -2517,9 +2526,7 @@

                  )

              else:

                  query = query.filter(

-                     model.Issue.assignee_id == user2.id

-                 ).filter(

-                     user2.user == assignee

+                     model.Issue.assignee_id == userassignee

                  )

          elif str(assignee).lower() in ['true', '1']:

              query = query.filter(
@@ -2530,32 +2537,35 @@

                  model.Issue.assignee_id.is_(None)

              )

      if author is not None:

-         user3 = aliased(model.User)

-         query = query.filter(

-             model.Issue.user_id == user3.id

+         userauthor = session.query(

+             model.User.id

          ).filter(

-             user3.user == author

+             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, basestring):

-         user2 = aliased(model.User)

-         user4 = aliased(model.User)

+     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 == user2.id,

-                     user2.user == private,

+                     model.Issue.user_id == userprivate

                  ),

                  sqlalchemy.and_(

                      model.Issue.private == True,  # noqa: E712

-                     model.Issue.assignee_id == user4.id,

-                     user4.user == private,

+                     model.Issue.assignee_id == userprivate

                  )

              )

          )
@@ -4491,8 +4501,12 @@

      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
@@ -4757,14 +4771,30 @@

          return query.all()

  

  

- def set_project_owner(session, project, user):

+ 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)

file modified
+38 -20
@@ -24,8 +24,10 @@

  _log = logging.getLogger(__name__)

  

  BUILD_STATS = {

-     'SUCCESS': ('Build successful', 100),

-     'FAILURE': ('Build failed', 0),

+     'SUCCESS': ('Build successful', pagure_config['FLAG_SUCCESS'], 100),

+     'FAILURE': ('Build failed', pagure_config['FLAG_FAILURE'], 0),

+     'ABORTED': ('Build aborted', 'error', 0),

+     'BUILDING': ('Build in progress', pagure_config['FLAG_PENDING'], 0),

  }

  

  
@@ -42,20 +44,28 @@

      _log.info(

          'Querying jenkins for project: %s, build: %s',

          jenkins_name, build_id)

-     build_info = jenk.get_build_info(jenkins_name, build_id)

+     try:

+         build_info = jenk.get_build_info(jenkins_name, build_id)

+     except jenkins.NotFoundException:

+         _log.debug('Could not find build %s at: %s', build_id, jenkins_name)

+         raise pagure.exceptions.PagureException(

+             'Could not find build %s at: %s' % (build_id, jenkins_name))

  

      if build_info.get('building') is True:

-         _log('Build is still going, let\'s wait a sec and try again')

-         if iteration == 10:

-             raise pagure.exceptions.NoCorrespondingPR(

-                 "We've been waiting for 10 seconds and the build is still "

-                 "not finished.")

-         time.sleep(1)

-         return process_jenkins_build(

-             session, project, build_id, requestfolder,

-             iteration=iteration + 1)

+         if iteration < 5:

+             _log.info('Build is still going, let\'s wait a sec and try again')

+             time.sleep(1)

+             return process_jenkins_build(

+                 session, project, build_id, requestfolder,

+                 iteration=iteration + 1)

+         _log.info(

+             "We've been waiting for 5 seconds and the build is still "

+             "not finished, so let's keep going.")

  

      result = build_info.get('result')

+     if not result and build_info.get('building') is True:

+         result = 'BUILDING'

+ 

      _log.info('Result from jenkins: %s', result)

      url = build_info['url']

      _log.info('URL from jenkins: %s', url)
@@ -73,21 +83,29 @@

              'No corresponding PR found')

  

      if not result or result not in BUILD_STATS:

-         pagure.exceptions.PagureException(

+         raise pagure.exceptions.PagureException(

              'Unknown build status: %s' % result)

  

-     status = result.lower()

- 

      request = pagure.lib.search_pull_requests(

          session, project_id=project.id, requestid=pr_id)

  

      if not request:

          raise pagure.exceptions.PagureException('Request not found')

  

-     comment, percent = BUILD_STATS[result]

+     comment, state, percent = BUILD_STATS[result]

      # Adding build ID to the CI type

-     username = project.ci_hook.ci_type + " #" + str(build_id)

- 

+     username = "%s #%s" % (project.ci_hook.ci_type, build_id)

+     if request.commit_stop:

+         comment += ' (commit: %s)' % (request.commit_stop[:8])

+ 

+     uid = None

+     for flag in request.flags:

+         if flag.status == pagure_config['FLAG_PENDING'] \

+                 and flag.username == username:

+             uid = flag.uid

+             break

+ 

+     _log.info("Flag's UID: %s", uid)

      pagure.lib.add_pull_request_flag(

          session,

          request=request,
@@ -95,8 +113,8 @@

          percent=percent,

          comment=comment,

          url=url,

-         status=status,

-         uid=None,

+         status=state,

+         uid=uid,

          user=project.user.username,

          token=None,

          requestfolder=requestfolder,

file modified
+4 -4
@@ -19,15 +19,15 @@

  FIXES = [

      re.compile(r'(?:.*\s+)?fixe?[sd]?:?\s*?#(\d+)', re.I),

      re.compile(

-         r'(?:.*\s+)?fixe?[sd]?:?\s*?https?://.*/(\w+)'

+         r'(?:.*\s+)?fixe?[sd]?:?\s*?https?://.*/([a-zA-z0-9_][a-zA-Z0-9-_]*)'

          '/(?:issue|pull-request)/(\d+)', re.I),

      re.compile(r'(?:.*\s+)?merge?[sd]?:?\s*?#(\d+)', re.I),

      re.compile(

-         r'(?:.*\s+)?merge?[sd]?:?\s*?https?://.*/(\w+)'

+         r'(?:.*\s+)?merge?[sd]?:?\s*?https?://.*/([a-zA-z0-9_][a-zA-Z0-9-_]*)'

          '/(?:issue|pull-request)/(\d+)', re.I),

      re.compile(r'(?:.*\s+)?close?[sd]?:?\s*?#(\d+)', re.I),

      re.compile(

-         r'(?:.*\s+)?close?[sd]?:?\s*?https?://.*/(\w+)'

+         r'(?:.*\s+)?close?[sd]?:?\s*?https?://.*/([a-zA-z0-9_][a-zA-Z0-9-_]*)'

          '/(?:issue|pull-request)/(\d+)', re.I),

  ]

  
@@ -36,7 +36,7 @@

      re.compile(r'(?:.*\s+)?relate[sd]?:?\s?#(\d+)', re.I),

      re.compile(

          r'(?:.*\s+)?relate[sd]?:?\s*?(?:to)?\s*?'

-         'https?://.*/(\w+)/issue/(\d+)', re.I),

+         'https?://.*/([a-zA-z0-9_][a-zA-Z0-9-_]*)/issue/(\d+)', re.I),

  ]

  

  

file modified
+1 -1
@@ -54,7 +54,7 @@

      if not mimetype:

          return None

      headers = {'X-Content-Type-Options': 'nosniff'}

-     if 'html' in mimetype or 'javascript' in mimetype:

+     if 'html' in mimetype or 'javascript' in mimetype or 'svg' in mimetype:

          mimetype = 'application/octet-stream'

          headers['Content-Disposition'] = 'attachment'

      if encoding:

file modified
+1 -1
@@ -899,7 +899,7 @@

              'close_status': self.close_status,

              'milestones': self.milestones,

          }

-         if not api:

+         if not api and not public:

              output['settings'] = self.settings

  

          return output

file modified
+9 -4
@@ -156,10 +156,15 @@

      if obj.project.user.default_email:

          emails.add(obj.project.user.default_email)

  

-     # Add project maintainers

-     for user in obj.project.users:

-         if user.default_email:

-             emails.add(user.default_email)

+     # Add committers is object is private, otherwise all contributors

+     if obj.isa in ['issue', 'pull-request'] and obj.private:

+         for user in obj.project.committers:

+             if user.default_email:

+                 emails.add(user.default_email)

+     else:

+         for user in obj.project.users:

+             if user.default_email:

+                 emails.add(user.default_email)

  

      # Add people in groups with any access to the project:

      for group in obj.project.groups:

file modified
+18 -8
@@ -12,7 +12,6 @@

  import datetime

  import gc

  import hashlib

- import logging

  import os

  import os.path

  import shutil
@@ -27,6 +26,8 @@

  

  from celery import Celery

  from celery.result import AsyncResult

+ from celery.signals import after_setup_task_logger

+ from celery.utils.log import get_task_logger

  from sqlalchemy.exc import SQLAlchemyError

  

  import pagure.lib
@@ -39,7 +40,7 @@

  from pagure.utils import get_parent_repo_path

  

  # logging.config.dictConfig(pagure_config.get('LOGGING') or {'version': 1})

- _log = logging.getLogger(__name__)

+ _log = get_task_logger(__name__)

  

  

  if os.environ.get('PAGURE_BROKER_URL'):
@@ -53,6 +54,11 @@

  conn.conf.update(pagure_config['CELERY_CONFIG'])

  

  

+ @after_setup_task_logger.connect

+ def augment_celery_log(**kwargs):

+     pagure.utils.set_up_logging(force=True)

+ 

+ 

  def pagure_task(function):

      """ Simple decorator that is responsible for:

      * Adjusting the status of the task when it starts
@@ -319,11 +325,12 @@

  

              shutil.rmtree(temp_gitrepo_path)

  

-         # Make the repo exportable via apache

-         http_clone_file = os.path.join(gitrepo, 'git-daemon-export-ok')

-         if not os.path.exists(http_clone_file):

-             with open(http_clone_file, 'w') as stream:

-                 pass

+         if not project.private:

+             # Make the repo exportable via apache

+             http_clone_file = os.path.join(gitrepo, 'git-daemon-export-ok')

+             if not os.path.exists(http_clone_file):

+                 with open(http_clone_file, 'w') as stream:

+                     pass

  

          docrepo = None

          if pagure_config.get('DOCS_FOLDER'):
@@ -648,7 +655,7 @@

          'refreshing remote pull-request: %s/#%s', request.project.fullname,

          request.id)

  

-     clonepath = pagure.utils.utils.get_remote_repo_path(

+     clonepath = pagure.utils.get_remote_repo_path(

          request.remote_git, request.branch_from)

  

      repo = pagure.lib.repo.PagureRepo(clonepath)
@@ -851,6 +858,9 @@

          stats[(author, email)] += 1

  

      for (name, email), val in stats.items():

+         if not email:

+             # Author email is missing in the git commit.

+             continue

          # For each recorded user info, check if we know the e-mail address of

          # the user.

          user = pagure.lib.search_user(session, email=email)

file modified
+9 -3
@@ -12,7 +12,6 @@

  import hashlib

  import hmac

  import json

- import logging

  import os

  import os.path

  import time
@@ -22,6 +21,8 @@

  import six

  

  from celery import Celery

+ from celery.signals import after_setup_task_logger

+ from celery.utils.log import get_task_logger

  from kitchen.text.converters import to_bytes

  from sqlalchemy.exc import SQLAlchemyError

  
@@ -30,10 +31,10 @@

  from pagure.lib.tasks import pagure_task

  from pagure.mail_logging import format_callstack

  from pagure.lib.lib_ci import trigger_jenkins_build

- from pagure.utils import split_project_fullname

+ from pagure.utils import split_project_fullname, set_up_logging

  

  # logging.config.dictConfig(pagure_config.get('LOGGING') or {'version': 1})

- _log = logging.getLogger(__name__)

+ _log = get_task_logger(__name__)

  _i = 0

  

  
@@ -48,6 +49,11 @@

  conn.conf.update(pagure_config['CELERY_CONFIG'])

  

  

+ @after_setup_task_logger.connect

+ def augment_celery_log(**kwargs):

+     set_up_logging(force=True)

+ 

+ 

  def call_web_hooks(project, topic, msg, urls):

      ''' Sends the web-hook notification. '''

      _log.info(

@@ -53,7 +53,11 @@

            <strong><label for="status">Type</label></strong>

            <select class="form-control c-select" id="type" name="type">

              {% for type in types %}

-             <option selected value="{{ type }}">{{ type }}</option>

+               {% if type == 'default' %}

+                 <option selected value="{{ type }}">{{ type }}</option>

+               {% else %}

+                 <option value="{{ type }}">{{ type }}</option>

+               {% endif %}

              {% endfor %}

            </select>

            <div>

@@ -660,7 +660,7 @@

        {% if pull_request %}

        <div class="small">

          <p>Pull this pull-request locally <a href="#" id="local_pull_info_btn">v</a></p>

-         <pre id="local_pull_info" style="display:none">git pull {{ config.get('GIT_URL_GIT') }}{{ repo.fullname }}.git refs/pull/{{ pull_request.id }}/head</pre>

+         <pre id="local_pull_info" style="display:none">git fetch {{ config.get('GIT_URL_GIT') }}{{ repo.fullname }}.git refs/pull/{{ pull_request.id }}/head:pr{{ pull_request.id }}</pre>

        </div>

        {% endif %}

      {% endif %}
@@ -1486,7 +1486,7 @@

  }

  

  $(document).ready(function () {

-   {% if authenticated and pull_request %}

+   {% if g.authenticated and pull_request %}

    function set_up_subcribed() {

      $("#subcribe-btn").click(function(){

        var _url = "{{ url_for(

@@ -52,7 +52,7 @@

          <div class="card">

            <div class="card-block">

                <h5><strong>Source GIT URLs</strong>{% if

-                 (authenticated and g.repo_committer) or

+                 (g.authenticated and g.repo_committer) or

                  (config['DOC_APP_URL'] and repo and

                          repo.settings.get('project_documentation', True))

                  %}

@@ -169,7 +169,7 @@

        </div>

        {% endif %}

        <div class="projectinfo m-t-1 m-b-1">

-       {% if repo.description %}{{ repo.description | safe }}{% else %}-{% endif -%}

+       {% if repo.description %}{{ repo.description | noJS | safe }}{% else %}-{% endif -%}

        {%- if repo.url %} &nbsp;| &nbsp;<a class="inline" href="{{ repo.url }}">{{ repo.url }}</a>{% endif %}

        </div>

      </header>

@@ -25,7 +25,11 @@

                <div class="col-sm-4">

                  <strong>Date (optional)</strong>

                </div>

-               <div class="col-sm-4">

+               <div class="col-sm-2">

+                 <strong>Reorder</strong>

+               </div>

+               <div class="col-sm-2">

+                 <strong>Active</strong>

                </div>

              </div>

              <div id="milestones">

file modified
+3 -2
@@ -25,6 +25,7 @@

  from pygments.lexers.text import DiffLexer

  from pygments.formatters import HtmlFormatter

  from pygments.filters import VisibleWhitespaceFilter

+ from jinja2 import escape

  

  import pagure.exceptions

  import pagure.lib
@@ -454,7 +455,7 @@

      """ Template filter transforming a pygit2 Author object into a text

      either with just the username or linking to the user in pagure.

      """

-     output = author.name

+     output = escape(author.name)

      if not author.email:

          return output

      user = pagure.lib.search_user(flask.g.session, email=author.email)
@@ -472,7 +473,7 @@

                      'ui_ns.view_user', username=user.username),

                  'cssclass': ('class="%s"' % cssclass) if cssclass else '',

                  'username': user.username,

-                 'name': author.name,

+                 'name': escape(author.name),

              }

          )

  

file modified
+4
@@ -511,7 +511,11 @@

              flask.flash(error_message, 'error')

  

          if not error:

+             known_tags = [tag.tag for tag in repo.tags_colored]

              for idx, tag in enumerate(tags):

+                 if tag in known_tags:

+                     flask.flash('Duplicated tag: %s' % tag, 'error')

+                     break

                  try:

                      pagure.lib.new_tag(

                          flask.g.session,

file modified
+13 -1
@@ -1200,6 +1200,11 @@

          except SQLAlchemyError as err:  # pragma: no cover

              flask.g.session.rollback()

              flask.flash(str(err), 'error')

+     else:

+         for field in form.errors:

+             flask.flash(

+                 'Field "%s" errored with errors: %s' % (

+                     field, ', '.join(form.errors[field])), 'error')

  

      return flask.redirect(flask.url_for(

          'ui_ns.view_settings', username=username, repo=repo.name,
@@ -2570,7 +2575,10 @@

                  'No such user %s found' % new_username)

          try:

              old_main_admin = repo.user.user

-             pagure.lib.set_project_owner(flask.g.session, repo, new_owner)

+             pagure.lib.set_project_owner(

+                 flask.g.session, repo, new_owner,

+                 required_groups=pagure_config.get('REQUIRED_GROUPS')

+             )

              # If the person doing the action is the former main admin, keep

              # them as admins

              if flask.g.fas_user.username == old_main_admin:
@@ -2581,6 +2589,10 @@

              pagure.lib.git.generate_gitolite_acls(project=repo)

              flask.flash(

                  'The project has been transferred to %s' % new_username)

+         except pagure.exceptions.PagureException as msg:

+             flask.g.session.rollback()

+             _log.debug(msg)

+             flask.flash(str(msg), 'error')

          except SQLAlchemyError:  # pragma: no cover

              flask.g.session.rollback()

              flask.flash(

file modified
+30
@@ -8,6 +8,8 @@

  

  """

  

+ import logging

+ import logging.config

  import os

  import re

  import urlparse
@@ -15,11 +17,28 @@

  

  import flask

  import pygit2

+ import six

  import werkzeug

  

  from pagure.config import config as pagure_config

  

  

+ _log = logging.getLogger(__name__)

+ LOGGER_SETUP = False

+ 

+ 

+ def set_up_logging(app=None, force=False):

+     global LOGGER_SETUP

+     if LOGGER_SETUP and not force:

+         _log.info('logging already setup')

+         return

+ 

+     logging.basicConfig()

+     logging.config.dictConfig(pagure_config.get('LOGGING') or {'version': 1})

+ 

+     LOGGER_SETUP = True

+ 

+ 

  def authenticated():

      ''' Utility function checking if the current user is logged in or not.

      '''
@@ -397,3 +416,14 @@

      rv = t.stream(context)

      rv.enable_buffering(5)

      return rv

+ 

+ 

+ def is_true(value, trueish=('1', 'true', 't', 'y')):

+     if isinstance(value, bool):

+         return value

+     if isinstance(value, six.binary_type):

+         # In Py3, str(b'true') == "b'true'", not b'true' as in Py2.

+         value = value.decode()

+     else:

+         value = str(value)

+     return value.strip().lower() in trueish

file modified
+1 -1
@@ -1,3 +1,3 @@

  cryptography

  python-jenkins

- trollius-redis

+ trollius

file modified
+1 -1
@@ -1,1 +1,1 @@

- trollius-redis

+ trollius

file modified
+1 -1
@@ -1,1 +1,1 @@

- trollius-redis

+ trollius

file modified
+4 -3
@@ -2,7 +2,6 @@

  # Use this file by running "$ pip install -r requirements.txt"

  alembic

  arrow

- bcrypt

  binaryornot

  bleach

  blinker
@@ -27,7 +26,9 @@

  requests

  six

  sqlalchemy >= 0.8

- straight.plugin >= 1.4.0-post-1

+ # 1.4.0 is broken, 1.4.0-post-1 works but gives odd results on newer setuptools

+ # the latest version 1.5.0 is also known to work

+ straight.plugin

  wtforms

  

  # Needed only for local authentication and/or Pagure CI
@@ -43,5 +44,5 @@

  # python-fedora

  

  # Required only for the `local` authentication backend

- # py-bcrypt

+ # bcrypt

  

@@ -1128,6 +1128,77 @@

              pagure.lib.get_watch_list(self.session, request),

              set(['pingou']))

  

+     @patch('pagure.lib.git.update_git')

+     @patch('pagure.lib.notify.send_email')

+     def test_api_subscribe_pull_request_logged_in(self, p_send_email, p_ugt):

+         """ Test the api_subscribe_pull_request method of the flask api

+         when the user is logged in via the UI. """

+         p_send_email.return_value = True

+         p_ugt.return_value = True

+ 

+         item = pagure.lib.model.User(

+             user='bar',

+             fullname='bar foo',

+             password='foo',

+             default_email='bar@bar.com',

+         )

+         self.session.add(item)

+         item = pagure.lib.model.UserEmail(

+             user_id=3,

+             email='bar@bar.com')

+         self.session.add(item)

+ 

+         self.session.commit()

+ 

+         tests.create_projects(self.session)

+         tests.create_tokens(self.session, user_id=3)

+         tests.create_tokens_acl(self.session)

+ 

+         # Create pull-request

+         repo = pagure.lib.get_authorized_project(self.session, 'test')

+         req = pagure.lib.new_pull_request(

+             session=self.session,

+             repo_from=repo,

+             branch_from='feature',

+             repo_to=repo,

+             branch_to='master',

+             title='test pull-request',

+             user='pingou',

+             requestfolder=None,

+         )

+         self.session.commit()

+         self.assertEqual(req.id, 1)

+         self.assertEqual(req.title, 'test pull-request')

+ 

+         # Check subscribtion before

+         repo = pagure.lib.get_authorized_project(self.session, 'test')

+         request = pagure.lib.search_pull_requests(

+             self.session, project_id=1, requestid=1)

+         self.assertEqual(

+             pagure.lib.get_watch_list(self.session, request),

+             set(['pingou']))

+ 

+         # Subscribe

+         user = tests.FakeUser(username='foo')

+         with tests.user_set(self.app.application, user):

+             data = {'status': True}

+             output = self.app.post(

+                 '/api/0/test/pull-request/1/subscribe', data=data)

+             self.assertEqual(output.status_code, 200)

+             data = json.loads(output.get_data(as_text=True))

+             self.assertDictEqual(

+                 data,

+                 {'message': 'You are now watching this pull-request'}

+             )

+ 

+         # Check subscribtions after

+         repo = pagure.lib.get_authorized_project(self.session, 'test')

+         request = pagure.lib.search_pull_requests(

+             self.session, project_id=1, requestid=1)

+         self.assertEqual(

+             pagure.lib.get_watch_list(self.session, request),

+             set(['pingou', 'foo']))

+ 

      @patch('pagure.lib.notify.send_email', MagicMock(return_value=True))

      def test_api_pull_request_open_invalid_project(self):

          """ Test the api_pull_request_create method of the flask api when

@@ -311,23 +311,6 @@

                      "namespace": None,

                      "parent": None,

                      "priorities": {},

-                     "settings": {

-                         "Enforce_signed-off_commits_in_pull-request": False,

-                         "Minimum_score_to_merge_pull-request": -1,

-                         "Only_assignee_can_merge_pull-request": False,

-                         "Web-hooks": None,

-                         "always_merge": False,

-                         "fedmsg_notifications": True,

-                         "issue_tracker": True,

-                         "issues_default_to_private": False,

-                         "notify_on_commit_flag": False,

-                         "notify_on_pull-request_flag": False,

-                         "project_documentation": False,

-                         "pull_request_access_only": False,

-                         "pull_requests": True,

-                         "roadmap_on_issues_page": False,

-                         "stomp_notifications": True,

-                     },

                      "tags": [],

                      "url_path": "test2",

                      "user": {
@@ -405,23 +388,6 @@

                      "namespace": None,

                      "parent": None,

                      "priorities": {},

-                     "settings": {

-                         "Enforce_signed-off_commits_in_pull-request": False,

-                         "Minimum_score_to_merge_pull-request": -1,

-                         "Only_assignee_can_merge_pull-request": False,

-                         "Web-hooks": None,

-                         "always_merge": False,

-                         "fedmsg_notifications": True,

-                         "issue_tracker": True,

-                         "issues_default_to_private": False,

-                         "notify_on_commit_flag": False,

-                         "notify_on_pull-request_flag": False,

-                         "project_documentation": False,

-                         "pull_request_access_only": False,

-                         "pull_requests": True,

-                         "roadmap_on_issues_page": False,

-                         "stomp_notifications": True,

-                     },

                      "tags": [],

                      "url_path": "test2",

                      "user": {
@@ -495,23 +461,6 @@

                      "namespace": None,

                      "parent": None,

                      "priorities": {},

-                     "settings": {

-                         "Enforce_signed-off_commits_in_pull-request": False,

-                         "Minimum_score_to_merge_pull-request": -1,

-                         "Only_assignee_can_merge_pull-request": False,

-                         "Web-hooks": None,

-                         "always_merge": False,

-                         "fedmsg_notifications": True,

-                         "issue_tracker": True,

-                         "issues_default_to_private": False,

-                         "notify_on_commit_flag": False,

-                         "notify_on_pull-request_flag": False,

-                         "project_documentation": False,

-                         "pull_request_access_only": False,

-                         "pull_requests": True,

-                         "roadmap_on_issues_page": False,

-                         "stomp_notifications": True,

-                     },

                      "tags": [],

                      "url_path": "test2",

                      "user": {

@@ -2534,7 +2534,7 @@

          # is required

          item = pagure.lib.model.TokenAcl(

              token_id='pingou_foo',

-             acl_id=6,

+             acl_id=7,

          )

          self.session.add(item)

          self.session.commit()

@@ -1377,59 +1377,56 @@

          tests.create_tokens_acl(self.session, 'aaabbbcccddd', 'modify_project')

          headers = {'Authorization': 'token aaabbbcccddd'}

  

-         user = pagure.lib.get_user(self.session, 'pingou')

-         user.cla_done = True

-         with tests.user_set(self.app.application, user):

-             output = self.app.patch(

-                 '/api/0/test', headers=headers,

-                 data={'main_admin': 'foo'})

-             self.assertEqual(output.status_code, 200)

-             data = json.loads(output.data)

-             data['date_created'] = '1496338274'

-             data['date_modified'] = '1496338274'

-             expected_output = {

-                 "access_groups": {

-                     "admin": [],

-                     "commit": [],

-                     "ticket": []

-                 },

-                 "access_users": {

-                     "admin": [],

-                     "commit": [],

-                     "owner": [

-                       "foo"

-                     ],

-                     "ticket": []

-                 },

-                 "close_status": [

-                     "Invalid",

-                     "Insufficient data",

-                     "Fixed",

-                     "Duplicate"

+         output = self.app.patch(

+             '/api/0/test', headers=headers,

+             data={'main_admin': 'foo'})

+         self.assertEqual(output.status_code, 200)

+         data = json.loads(output.get_data(as_text=True))

+         data['date_created'] = '1496338274'

+         data['date_modified'] = '1496338274'

+         expected_output = {

+             "access_groups": {

+                 "admin": [],

+                 "commit": [],

+                 "ticket": []

+             },

+             "access_users": {

+                 "admin": [],

+                 "commit": [],

+                 "owner": [

+                   "foo"

                  ],

-                 "custom_keys": [],

-                 "date_created": "1496338274",

-                 "date_modified": "1496338274",

-                 "description": "test project #1",

-                 "fullname": "test",

-                 "url_path": "test",

-                 "id": 1,

-                 "milestones": {},

-                 "name": "test",

-                 "namespace": None,

-                 "parent": None,

-                 "priorities": {},

-                 "tags": [],

-                 "user": {

-                     "default_email": "foo@bar.com",

-                     "emails": [

-                         "foo@bar.com"

-                     ],

-                     "fullname": "foo bar",

-                     "name": "foo"

-                 }

+                 "ticket": []

+             },

+             "close_status": [

+                 "Invalid",

+                 "Insufficient data",

+                 "Fixed",

+                 "Duplicate"

+             ],

+             "custom_keys": [],

+             "date_created": "1496338274",

+             "date_modified": "1496338274",

+             "description": "test project #1",

+             "fullname": "test",

+             "url_path": "test",

+             "id": 1,

+             "milestones": {},

+             "name": "test",

+             "namespace": None,

+             "parent": None,

+             "priorities": {},

+             "tags": [],

+             "user": {

+                 "default_email": "foo@bar.com",

+                 "emails": [

+                     "foo@bar.com"

+                 ],

+                 "fullname": "foo bar",

+                 "name": "foo"

              }

-             self.assertEqual(data, expected_output)

+         }

+         self.assertEqual(data, expected_output)

  

      def test_api_modify_project_main_admin_retain_access(self):

          """ Test the api_modify_project method of the flask api when the
@@ -1440,61 +1437,58 @@

          tests.create_tokens_acl(self.session, 'aaabbbcccddd', 'modify_project')

          headers = {'Authorization': 'token aaabbbcccddd'}

  

-         user = pagure.lib.get_user(self.session, 'pingou')

-         user.cla_done = True

-         with tests.user_set(self.app.application, user):

-             output = self.app.patch(

-                 '/api/0/test', headers=headers,

-                 data={'main_admin': 'foo', 'retain_access': True})

-             self.assertEqual(output.status_code, 200)

-             data = json.loads(output.data)

-             data['date_created'] = '1496338274'

-             data['date_modified'] = '1496338274'

-             expected_output = {

-                 "access_groups": {

-                     "admin": [],

-                     "commit": [],

-                     "ticket": []

-                 },

-                 "access_users": {

-                     "admin": [

-                         "pingou"

-                     ],

-                     "commit": [],

-                     "owner": [

-                         "foo"

-                     ],

-                     "ticket": []

-                 },

-                 "close_status": [

-                     "Invalid",

-                     "Insufficient data",

-                     "Fixed",

-                     "Duplicate"

+         output = self.app.patch(

+             '/api/0/test', headers=headers,

+             data={'main_admin': 'foo', 'retain_access': True})

+         self.assertEqual(output.status_code, 200)

+         data = json.loads(output.get_data(as_text=True))

+         data['date_created'] = '1496338274'

+         data['date_modified'] = '1496338274'

+         expected_output = {

+             "access_groups": {

+                 "admin": [],

+                 "commit": [],

+                 "ticket": []

+             },

+             "access_users": {

+                 "admin": [

+                     "pingou"

                  ],

-                 "custom_keys": [],

-                 "date_created": "1496338274",

-                 "date_modified": "1496338274",

-                 "description": "test project #1",

-                 "fullname": "test",

-                 "url_path": "test",

-                 "id": 1,

-                 "milestones": {},

-                 "name": "test",

-                 "namespace": None,

-                 "parent": None,

-                 "priorities": {},

-                 "tags": [],

-                 "user": {

-                     "default_email": "foo@bar.com",

-                     "emails": [

-                         "foo@bar.com"

-                     ],

-                     "fullname": "foo bar",

-                     "name": "foo"

-                 }

+                 "commit": [],

+                 "owner": [

+                     "foo"

+                 ],

+                 "ticket": []

+             },

+             "close_status": [

+                 "Invalid",

+                 "Insufficient data",

+                 "Fixed",

+                 "Duplicate"

+             ],

+             "custom_keys": [],

+             "date_created": "1496338274",

+             "date_modified": "1496338274",

+             "description": "test project #1",

+             "fullname": "test",

+             "url_path": "test",

+             "id": 1,

+             "milestones": {},

+             "name": "test",

+             "namespace": None,

+             "parent": None,

+             "priorities": {},

+             "tags": [],

+             "user": {

+                 "default_email": "foo@bar.com",

+                 "emails": [

+                     "foo@bar.com"

+                 ],

+                 "fullname": "foo bar",

+                 "name": "foo"

              }

-             self.assertEqual(data, expected_output)

+         }

+         self.assertEqual(data, expected_output)

  

      def test_api_modify_project_main_admin_retain_access_already_user(self):

          """ Test the api_modify_project method of the flask api when the
@@ -1514,61 +1508,58 @@

          )

          self.session.commit()

  

-         user = pagure.lib.get_user(self.session, 'pingou')

-         user.cla_done = True

-         with tests.user_set(self.app.application, user):

-             output = self.app.patch(

-                 '/api/0/test', headers=headers,

-                 data={'main_admin': 'foo', 'retain_access': True})

-             self.assertEqual(output.status_code, 200)

-             data = json.loads(output.data)

-             data['date_created'] = '1496338274'

-             data['date_modified'] = '1496338274'

-             expected_output = {

-                 "access_groups": {

-                     "admin": [],

-                     "commit": [],

-                     "ticket": []

-                 },

-                 "access_users": {

-                     "admin": [

-                         "pingou"

-                     ],

-                     "commit": [],

-                     "owner": [

-                         "foo"

-                     ],

-                     "ticket": []

-                 },

-                 "close_status": [

-                     "Invalid",

-                     "Insufficient data",

-                     "Fixed",

-                     "Duplicate"

+         output = self.app.patch(

+             '/api/0/test', headers=headers,

+             data={'main_admin': 'foo', 'retain_access': True})

+         self.assertEqual(output.status_code, 200)

+         data = json.loads(output.get_data(as_text=True))

+         data['date_created'] = '1496338274'

+         data['date_modified'] = '1496338274'

+         expected_output = {

+             "access_groups": {

+                 "admin": [],

+                 "commit": [],

+                 "ticket": []

+             },

+             "access_users": {

+                 "admin": [

+                     "pingou"

                  ],

-                 "custom_keys": [],

-                 "date_created": "1496338274",

-                 "date_modified": "1496338274",

-                 "description": "test project #1",

-                 "fullname": "test",

-                 "url_path": "test",

-                 "id": 1,

-                 "milestones": {},

-                 "name": "test",

-                 "namespace": None,

-                 "parent": None,

-                 "priorities": {},

-                 "tags": [],

-                 "user": {

-                     "default_email": "foo@bar.com",

-                     "emails": [

-                         "foo@bar.com"

-                     ],

-                     "fullname": "foo bar",

-                     "name": "foo"

-                 }

+                 "commit": [],

+                 "owner": [

+                     "foo"

+                 ],

+                 "ticket": []

+             },

+             "close_status": [

+                 "Invalid",

+                 "Insufficient data",

+                 "Fixed",

+                 "Duplicate"

+             ],

+             "custom_keys": [],

+             "date_created": "1496338274",

+             "date_modified": "1496338274",

+             "description": "test project #1",

+             "fullname": "test",

+             "url_path": "test",

+             "id": 1,

+             "milestones": {},

+             "name": "test",

+             "namespace": None,

+             "parent": None,

+             "priorities": {},

+             "tags": [],

+             "user": {

+                 "default_email": "foo@bar.com",

+                 "emails": [

+                     "foo@bar.com"

+                 ],

+                 "fullname": "foo bar",

+                 "name": "foo"

              }

-             self.assertEqual(data, expected_output)

+         }

+         self.assertEqual(data, expected_output)

  

      def test_api_modify_project_main_admin_json(self):

          """ Test the api_modify_project method of the flask api when the
@@ -1579,59 +1570,56 @@

          headers = {'Authorization': 'token aaabbbcccddd',

                     'Content-Type': 'application/json'}

  

-         user = pagure.lib.get_user(self.session, 'pingou')

-         user.cla_done = True

-         with tests.user_set(self.app.application, user):

-             output = self.app.patch(

-                 '/api/0/test', headers=headers,

-                 data=json.dumps({'main_admin': 'foo'}))

-             self.assertEqual(output.status_code, 200)

-             data = json.loads(output.data)

-             data['date_created'] = '1496338274'

-             data['date_modified'] = '1496338274'

-             expected_output = {

-                 "access_groups": {

-                     "admin": [],

-                     "commit": [],

-                     "ticket": []

-                 },

-                 "access_users": {

-                     "admin": [],

-                     "commit": [],

-                     "owner": [

-                       "foo"

-                     ],

-                     "ticket": []

-                 },

-                 "close_status": [

-                     "Invalid",

-                     "Insufficient data",

-                     "Fixed",

-                     "Duplicate"

+         output = self.app.patch(

+             '/api/0/test', headers=headers,

+             data=json.dumps({'main_admin': 'foo'}))

+         self.assertEqual(output.status_code, 200)

+         data = json.loads(output.get_data(as_text=True))

+         data['date_created'] = '1496338274'

+         data['date_modified'] = '1496338274'

+         expected_output = {

+             "access_groups": {

+                 "admin": [],

+                 "commit": [],

+                 "ticket": []

+             },

+             "access_users": {

+                 "admin": [],

+                 "commit": [],

+                 "owner": [

+                   "foo"

                  ],

-                 "custom_keys": [],

-                 "date_created": "1496338274",

-                 "date_modified": "1496338274",

-                 "description": "test project #1",

-                 "fullname": "test",

-                 "url_path": "test",

-                 "id": 1,

-                 "milestones": {},

-                 "name": "test",

-                 "namespace": None,

-                 "parent": None,

-                 "priorities": {},

-                 "tags": [],

-                 "user": {

-                     "default_email": "foo@bar.com",

-                     "emails": [

-                         "foo@bar.com"

-                     ],

-                     "fullname": "foo bar",

-                     "name": "foo"

-                 }

+                 "ticket": []

+             },

+             "close_status": [

+                 "Invalid",

+                 "Insufficient data",

+                 "Fixed",

+                 "Duplicate"

+             ],

+             "custom_keys": [],

+             "date_created": "1496338274",

+             "date_modified": "1496338274",

+             "description": "test project #1",

+             "fullname": "test",

+             "url_path": "test",

+             "id": 1,

+             "milestones": {},

+             "name": "test",

+             "namespace": None,

+             "parent": None,

+             "priorities": {},

+             "tags": [],

+             "user": {

+                 "default_email": "foo@bar.com",

+                 "emails": [

+                     "foo@bar.com"

+                 ],

+                 "fullname": "foo bar",

+                 "name": "foo"

              }

-             self.assertEqual(data, expected_output)

+         }

+         self.assertEqual(data, expected_output)

  

      @patch.dict('pagure.config.config', {'PAGURE_ADMIN_USERS': 'foo'})

      def test_api_modify_project_main_admin_as_site_admin(self):
@@ -1643,59 +1631,56 @@

          tests.create_tokens_acl(self.session, 'aaabbbcccddd', 'modify_project')

          headers = {'Authorization': 'token aaabbbcccddd'}

  

-         user = pagure.lib.get_user(self.session, 'foo')

-         user.cla_done = True

-         with tests.user_set(self.app.application, user):

-             output = self.app.patch(

-                 '/api/0/test', headers=headers,

-                 data={'main_admin': 'foo'})

-             self.assertEqual(output.status_code, 200)

-             data = json.loads(output.data)

-             data['date_created'] = '1496338274'

-             data['date_modified'] = '1496338274'

-             expected_output = {

-                 "access_groups": {

-                     "admin": [],

-                     "commit": [],

-                     "ticket": []

-                 },

-                 "access_users": {

-                     "admin": [],

-                     "commit": [],

-                     "owner": [

-                       "foo"

-                     ],

-                     "ticket": []

-                 },

-                 "close_status": [

-                     "Invalid",

-                     "Insufficient data",

-                     "Fixed",

-                     "Duplicate"

+         output = self.app.patch(

+             '/api/0/test', headers=headers,

+             data={'main_admin': 'foo'})

+         self.assertEqual(output.status_code, 200)

+         data = json.loads(output.get_data(as_text=True))

+         data['date_created'] = '1496338274'

+         data['date_modified'] = '1496338274'

+         expected_output = {

+             "access_groups": {

+                 "admin": [],

+                 "commit": [],

+                 "ticket": []

+             },

+             "access_users": {

+                 "admin": [],

+                 "commit": [],

+                 "owner": [

+                   "foo"

                  ],

-                 "custom_keys": [],

-                 "date_created": "1496338274",

-                 "date_modified": "1496338274",

-                 "description": "test project #1",

-                 "fullname": "test",

-                 "url_path": "test",

-                 "id": 1,

-                 "milestones": {},

-                 "name": "test",

-                 "namespace": None,

-                 "parent": None,

-                 "priorities": {},

-                 "tags": [],

-                 "user": {

-                     "default_email": "foo@bar.com",

-                     "emails": [

-                         "foo@bar.com"

-                     ],

-                     "fullname": "foo bar",

-                     "name": "foo"

-                 }

+                 "ticket": []

+             },

+             "close_status": [

+                 "Invalid",

+                 "Insufficient data",

+                 "Fixed",

+                 "Duplicate"

+             ],

+             "custom_keys": [],

+             "date_created": "1496338274",

+             "date_modified": "1496338274",

+             "description": "test project #1",

+             "fullname": "test",

+             "url_path": "test",

+             "id": 1,

+             "milestones": {},

+             "name": "test",

+             "namespace": None,

+             "parent": None,

+             "priorities": {},

+             "tags": [],

+             "user": {

+                 "default_email": "foo@bar.com",

+                 "emails": [

+                     "foo@bar.com"

+                 ],

+                 "fullname": "foo bar",

+                 "name": "foo"

              }

-             self.assertEqual(data, expected_output)

+         }

+         self.assertEqual(data, expected_output)

  

      def test_api_modify_project_main_admin_not_main_admin(self):

          """ Test the api_modify_project method of the flask api when the
@@ -1714,19 +1699,17 @@

          tests.create_tokens_acl(self.session, 'aaabbbcccddd', 'modify_project')

          headers = {'Authorization': 'token aaabbbcccddd'}

  

-         user = pagure.lib.get_user(self.session, 'foo')

-         user.cla_done = True

-         with tests.user_set(self.app.application, user):

-             output = self.app.patch(

-                 '/api/0/test', headers=headers,

-                 data={'main_admin': 'foo'})

-             self.assertEqual(output.status_code, 401)

-             expected_error = {

-                 'error': ('Only the main admin can set the main admin of a '

-                           'project'),

-                 'error_code': 'ENOTMAINADMIN'

-             }

-             self.assertEqual(json.loads(output.data), expected_error)

+         output = self.app.patch(

+             '/api/0/test', headers=headers,

+             data={'main_admin': 'foo'})

+         self.assertEqual(output.status_code, 401)

+         expected_error = {

+             'error': ('Only the main admin can set the main admin of a '

+                       'project'),

+             'error_code': 'ENOTMAINADMIN'

+         }

+         self.assertEqual(

+             json.loads(output.get_data(as_text=True)), expected_error)

  

      def test_api_modify_project_not_admin(self):

          """ Test the api_modify_project method of the flask api when the
@@ -1737,18 +1720,16 @@

          tests.create_tokens_acl(self.session, 'aaabbbcccddd', 'modify_project')

          headers = {'Authorization': 'token aaabbbcccddd'}

  

-         user = pagure.lib.get_user(self.session, 'foo')

-         user.cla_done = True

-         with tests.user_set(self.app.application, user):

-             output = self.app.patch(

-                 '/api/0/test', headers=headers,

-                 data={'main_admin': 'foo'})

-             self.assertEqual(output.status_code, 401)

-             expected_error = {

-                 'error': 'You are not allowed to modify this project',

-                 'error_code': 'EMODIFYPROJECTNOTALLOWED'

-             }

-             self.assertEqual(json.loads(output.data), expected_error)

+         output = self.app.patch(

+             '/api/0/test', headers=headers,

+             data={'main_admin': 'foo'})

+         self.assertEqual(output.status_code, 401)

+         expected_error = {

+             'error': 'You are not allowed to modify this project',

+             'error_code': 'EMODIFYPROJECTNOTALLOWED'

+         }

+         self.assertEqual(

+             json.loads(output.get_data(as_text=True)), expected_error)

  

      def test_api_modify_project_invalid_request(self):

          """ Test the api_modify_project method of the flask api when the
@@ -1759,18 +1740,16 @@

          tests.create_tokens_acl(self.session, 'aaabbbcccddd', 'modify_project')

          headers = {'Authorization': 'token aaabbbcccddd'}

  

-         user = pagure.lib.get_user(self.session, 'pingou')

-         user.cla_done = True

-         with tests.user_set(self.app.application, user):

-             output = self.app.patch(

-                 '/api/0/test', headers=headers,

-                 data='invalid')

-             self.assertEqual(output.status_code, 400)

-             expected_error = {

-                 'error': 'Invalid or incomplete input submitted',

-                 'error_code': 'EINVALIDREQ'

-             }

-             self.assertEqual(json.loads(output.data), expected_error)

+         output = self.app.patch(

+             '/api/0/test', headers=headers,

+             data='invalid')

+         self.assertEqual(output.status_code, 400)

+         expected_error = {

+             'error': 'Invalid or incomplete input submitted',

+             'error_code': 'EINVALIDREQ'

+         }

+         self.assertEqual(

+             json.loads(output.get_data(as_text=True)), expected_error)

  

      def test_api_modify_project_invalid_keys(self):

          """ Test the api_modify_project method of the flask api when the
@@ -1781,18 +1760,16 @@

          tests.create_tokens_acl(self.session, 'aaabbbcccddd', 'modify_project')

          headers = {'Authorization': 'token aaabbbcccddd'}

  

-         user = pagure.lib.get_user(self.session, 'pingou')

-         user.cla_done = True

-         with tests.user_set(self.app.application, user):

-             output = self.app.patch(

-                 '/api/0/test', headers=headers,

-                 data={'invalid': 'invalid'})

-             self.assertEqual(output.status_code, 400)

-             expected_error = {

-                 'error': 'Invalid or incomplete input submitted',

-                 'error_code': 'EINVALIDREQ'

-             }

-             self.assertEqual(json.loads(output.data), expected_error)

+         output = self.app.patch(

+             '/api/0/test', headers=headers,

+             data={'invalid': 'invalid'})

+         self.assertEqual(output.status_code, 400)

+         expected_error = {

+             'error': 'Invalid or incomplete input submitted',

+             'error_code': 'EINVALIDREQ'

+         }

+         self.assertEqual(

+             json.loads(output.get_data(as_text=True)), expected_error)

  

      def test_api_modify_project_invalid_new_main_admin(self):

          """ Test the api_modify_project method of the flask api when the
@@ -1804,18 +1781,16 @@

          tests.create_tokens_acl(self.session, 'aaabbbcccddd', 'modify_project')

          headers = {'Authorization': 'token aaabbbcccddd'}

  

-         user = pagure.lib.get_user(self.session, 'pingou')

-         user.cla_done = True

-         with tests.user_set(self.app.application, user):

-             output = self.app.patch(

-                 '/api/0/test', headers=headers,

-                 data={'main_admin': 'tbrady'})

-             self.assertEqual(output.status_code, 400)

-             expected_error = {

-                 'error': 'No such user found',

-                 'error_code': 'ENOUSER'

-             }

-             self.assertEqual(json.loads(output.data), expected_error)

+         output = self.app.patch(

+             '/api/0/test', headers=headers,

+             data={'main_admin': 'tbrady'})

+         self.assertEqual(output.status_code, 400)

+         expected_error = {

+             'error': 'No such user found',

+             'error_code': 'ENOUSER'

+         }

+         self.assertEqual(

+             json.loads(output.get_data(as_text=True)), expected_error)

  

      def test_api_project_watchers(self):

          """ Test the api_project_watchers method of the flask api. """
@@ -2563,19 +2538,18 @@

          headers = {'Authorization': 'token aaabbbcccddd'}

  

          user = pagure.lib.get_user(self.session, 'pingou')

-         with tests.user_set(self.app.application, user):

-             output = self.app.post(

-                 '/api/0/test/git/generateacls', headers=headers,

-                 data={'wait': False})

-             self.assertEqual(output.status_code, 200)

-             data = json.loads(output.data)

-             expected_output = {

-                 'message': 'Project ACL generation queued',

-                 'taskid': 'abc-1234'

-             }

-             self.assertEqual(data, expected_output)

-             self.mock_gen_acls.assert_called_once_with(

-                 name='test', namespace=None, user=None, group=None)

+         output = self.app.post(

+             '/api/0/test/git/generateacls', headers=headers,

+             data={'wait': False})

+         self.assertEqual(output.status_code, 200)

+         data = json.loads(output.get_data(as_text=True))

+         expected_output = {

+             'message': 'Project ACL generation queued',

+             'taskid': 'abc-1234'

+         }

+         self.assertEqual(data, expected_output)

+         self.mock_gen_acls.assert_called_once_with(

+             name='test', namespace=None, user=None, group=None)

  

      def test_api_generate_acls_json(self):

          """ Test the api_generate_acls method of the flask api using JSON """
@@ -2587,19 +2561,18 @@

                     'Content-Type': 'application/json'}

  

          user = pagure.lib.get_user(self.session, 'pingou')

-         with tests.user_set(self.app.application, user):

-             output = self.app.post(

-                 '/api/0/test/git/generateacls', headers=headers,

-                 data=json.dumps({'wait': False}))

-             self.assertEqual(output.status_code, 200)

-             data = json.loads(output.data)

-             expected_output = {

-                 'message': 'Project ACL generation queued',

-                 'taskid': 'abc-1234'

-             }

-             self.assertEqual(data, expected_output)

-             self.mock_gen_acls.assert_called_once_with(

-                 name='test', namespace=None, user=None, group=None)

+         output = self.app.post(

+             '/api/0/test/git/generateacls', headers=headers,

+             data=json.dumps({'wait': False}))

+         self.assertEqual(output.status_code, 200)

+         data = json.loads(output.get_data(as_text=True))

+         expected_output = {

+             'message': 'Project ACL generation queued',

+             'taskid': 'abc-1234'

+         }

+         self.assertEqual(data, expected_output)

+         self.mock_gen_acls.assert_called_once_with(

+             name='test', namespace=None, user=None, group=None)

  

      def test_api_generate_acls_wait_true(self):

          """ Test the api_generate_acls method of the flask api when wait is
@@ -2615,19 +2588,18 @@

          self.mock_gen_acls.return_value = task_result

  

          user = pagure.lib.get_user(self.session, 'pingou')

-         with tests.user_set(self.app.application, user):

-             output = self.app.post(

-                 '/api/0/test/git/generateacls', headers=headers,

-                 data={'wait': True})

-             self.assertEqual(output.status_code, 200)

-             data = json.loads(output.data)

-             expected_output = {

-                 'message': 'Project ACLs generated',

-             }

-             self.assertEqual(data, expected_output)

-             self.mock_gen_acls.assert_called_once_with(

-                 name='test', namespace=None, user=None, group=None)

-             self.assertTrue(task_result.get.called)

+         output = self.app.post(

+             '/api/0/test/git/generateacls', headers=headers,

+             data={'wait': True})

+         self.assertEqual(output.status_code, 200)

+         data = json.loads(output.get_data(as_text=True))

+         expected_output = {

+             'message': 'Project ACLs generated',

+         }

+         self.assertEqual(data, expected_output)

+         self.mock_gen_acls.assert_called_once_with(

+             name='test', namespace=None, user=None, group=None)

+         self.assertTrue(task_result.get.called)

  

      def test_api_generate_acls_no_project(self):

          """ Test the api_generate_acls method of the flask api when the project
@@ -2639,17 +2611,16 @@

          headers = {'Authorization': 'token aaabbbcccddd'}

  

          user = pagure.lib.get_user(self.session, 'pingou')

-         with tests.user_set(self.app.application, user):

-             output = self.app.post(

-                 '/api/0/test12345123/git/generateacls', headers=headers,

-                 data={'wait': False})

-             self.assertEqual(output.status_code, 404)

-             data = json.loads(output.data)

-             expected_output = {

-                 'error_code': 'ENOPROJECT',

-                 'error': 'Project not found'

-             }

-             self.assertEqual(data, expected_output)

+         output = self.app.post(

+             '/api/0/test12345123/git/generateacls', headers=headers,

+             data={'wait': False})

+         self.assertEqual(output.status_code, 404)

+         data = json.loads(output.get_data(as_text=True))

+         expected_output = {

+             'error_code': 'ENOPROJECT',

+             'error': 'Project not found'

+         }

+         self.assertEqual(data, expected_output)

  

      def test_api_new_git_branch(self):

          """ Test the api_new_branch method of the flask api """
@@ -2659,7 +2630,7 @@

          tests.add_content_git_repo(os.path.join(repo_path, 'test.git'))

          tests.create_tokens(self.session, project_id=None)

          tests.create_tokens_acl(

-             self.session, 'aaabbbcccddd', 'modify_project')

+             self.session, 'aaabbbcccddd', 'create_branch')

          headers = {'Authorization': 'token aaabbbcccddd'}

          args = {'branch': 'test123'}

          output = self.app.post('/api/0/test/git/branch', headers=headers,
@@ -2683,7 +2654,7 @@

          tests.add_content_git_repo(os.path.join(repo_path, 'test.git'))

          tests.create_tokens(self.session, project_id=None)

          tests.create_tokens_acl(

-             self.session, 'aaabbbcccddd', 'modify_project')

+             self.session, 'aaabbbcccddd', 'create_branch')

          headers = {'Authorization': 'token aaabbbcccddd',

                     'Content-Type': 'application/json'}

          args = {'branch': 'test123'}
@@ -2707,7 +2678,7 @@

          tests.add_content_git_repo(os.path.join(repo_path, 'test.git'))

          tests.create_tokens(self.session, project_id=None)

          tests.create_tokens_acl(

-             self.session, 'aaabbbcccddd', 'modify_project')

+             self.session, 'aaabbbcccddd', 'create_branch')

          git_path = os.path.join(self.path, 'repos', 'test.git')

          repo_obj = pygit2.Repository(git_path)

          parent = pagure.lib.git.get_branch_ref(repo_obj, 'master').get_object()
@@ -2733,7 +2704,7 @@

          tests.add_content_git_repo(os.path.join(repo_path, 'test.git'))

          tests.create_tokens(self.session, project_id=None)

          tests.create_tokens_acl(

-             self.session, 'aaabbbcccddd', 'modify_project')

+             self.session, 'aaabbbcccddd', 'create_branch')

          headers = {'Authorization': 'token aaabbbcccddd'}

          args = {'branch': 'master'}

          output = self.app.post('/api/0/test/git/branch', headers=headers,
@@ -2755,7 +2726,7 @@

          tests.add_content_git_repo(git_path)

          tests.create_tokens(self.session, project_id=None)

          tests.create_tokens_acl(

-             self.session, 'aaabbbcccddd', 'modify_project')

+             self.session, 'aaabbbcccddd', 'create_branch')

          repo_obj = pygit2.Repository(git_path)

          from_commit = repo_obj.revparse_single('HEAD').oid.hex

          headers = {'Authorization': 'token aaabbbcccddd'}

@@ -122,23 +122,6 @@

                      "namespace": None,

                      "parent": None,

                      "priorities": {},

-                     "settings": {

-                         "Enforce_signed-off_commits_in_pull-request": False,

-                         "Minimum_score_to_merge_pull-request": -1,

-                         "Only_assignee_can_merge_pull-request": False,

-                         "Web-hooks": None,

-                         "always_merge": False,

-                         "fedmsg_notifications": True,

-                         "issue_tracker": True,

-                         "issues_default_to_private": False,

-                         "notify_on_commit_flag": False,

-                         "notify_on_pull-request_flag": False,

-                         "pull_request_access_only": False,

-                         "project_documentation": False,

-                         "pull_requests": True,

-                         "roadmap_on_issues_page": False,

-                         "stomp_notifications": True,

-                     },

                      "tags": [],

                      "user": {

                          "fullname": "PY C",
@@ -175,23 +158,6 @@

                      "namespace": None,

                      "parent": None,

                      "priorities": {},

-                     "settings": {

-                         "Enforce_signed-off_commits_in_pull-request": False,

-                         "Minimum_score_to_merge_pull-request": -1,

-                         "Only_assignee_can_merge_pull-request": False,

-                         "Web-hooks": None,

-                         "always_merge": False,

-                         "fedmsg_notifications": True,

-                         "issue_tracker": True,

-                         "issues_default_to_private": False,

-                         "notify_on_commit_flag": False,

-                         "notify_on_pull-request_flag": False,

-                         "pull_request_access_only": False,

-                         "project_documentation": False,

-                         "pull_requests": True,

-                         "roadmap_on_issues_page": False,

-                         "stomp_notifications": True,

-                     },

                      "tags": [],

                      "user": {

                          "fullname": "PY C",
@@ -227,23 +193,6 @@

                      "namespace": "somenamespace",

                      "parent": None,

                      "priorities": {},

-                     "settings": {

-                         "Enforce_signed-off_commits_in_pull-request": False,

-                         "Minimum_score_to_merge_pull-request": -1,

-                         "Only_assignee_can_merge_pull-request": False,

-                         "Web-hooks": None,

-                         "always_merge": False,

-                         "fedmsg_notifications": True,

-                         "issue_tracker": True,

-                         "issues_default_to_private": False,

-                         "notify_on_commit_flag": False,

-                         "notify_on_pull-request_flag": False,

-                         "project_documentation": False,

-                         "pull_request_access_only": False,

-                         "pull_requests": True,

-                         "roadmap_on_issues_page": False,

-                         "stomp_notifications": True,

-                     },

                      "tags": [],

                      "user": {

                          "fullname": "PY C",

@@ -41,6 +41,7 @@

  

          tests.create_projects(self.session)

          tests.create_projects_git(os.path.join(self.path, 'repos'), bare=True)

+         self._check_user(user='pingou')

  

      def _check_user(self, user='pingou'):

          self.session.commit()
@@ -320,6 +321,99 @@

              self.assertEqual(len(project.users), 1)

              self.assertEqual(project.users[0].user, 'pingou')

  

+     @patch.dict('pagure.config.config', {'REQUIRED_GROUPS': {'*': ['packager']}})

+     @patch.dict('pagure.config.config', {'PAGURE_ADMIN_USERS': 'foo'})

+     @patch('pagure.lib.git.generate_gitolite_acls', MagicMock())

+     def test_give_project_not_in_required_group(self):

+         """ Test the give_project endpoint. """

+ 

+         user = tests.FakeUser()

+         user.username = 'pingou'

+         with tests.user_set(self.app.application, user):

+             csrf_token = self.get_csrf()

+ 

+             self._check_user()

+ 

+             # User not a packager

+             data = {

+                 'user': 'foo',

+                 'csrf_token': csrf_token,

+             }

+ 

+             output = self.app.post(

+                 '/test/give', data=data, follow_redirects=True)

+             self.assertEqual(output.status_code, 200)

+             self.assertIn(

+                 '</button>\n                      '

+                 'This user must be in one of the following groups to be '

+                 'allowed to be added to this project: packager'

+                 '\n                    </div>\n',

+                 output.get_data(as_text=True))

+ 

+             self._check_user(user='pingou')

+ 

+     @patch.dict('pagure.config.config', {'REQUIRED_GROUPS': {'*': ['packager']}})

+     @patch.dict('pagure.config.config', {'PAGURE_ADMIN_USERS': 'foo'})

+     @patch('pagure.lib.git.generate_gitolite_acls', MagicMock())

+     def test_give_project_in_required_group(self):

+         """ Test the give_project endpoint. """

+ 

+         # Create the packager group

+         msg = pagure.lib.add_group(

+             self.session,

+             group_name='packager',

+             display_name='packager group',

+             description=None,

+             group_type='user',

+             user='pingou',

+             is_admin=False,

+             blacklist=[],

+         )

+         self.session.commit()

+         self.assertEqual(msg, 'User `pingou` added to the group `packager`.')

+ 

+         # Add foo to the packager group

+         group = pagure.lib.search_groups(self.session, group_name='packager')

+         msg = pagure.lib.add_user_to_group(

+             self.session,

+             username='foo',

+             group=group,

+             user='pingou',

+             is_admin=False,

+         )

+         self.session.commit()

+         self.assertEqual(msg, 'User `foo` added to the group `packager`.')

+ 

+         # pingou transferts test to foo

+         user = tests.FakeUser()

+         user.username = 'pingou'

+         with tests.user_set(self.app.application, user):

+             csrf_token = self.get_csrf()

+ 

+             self._check_user()

+ 

+             # User not a packager

+             data = {

+                 'user': 'foo',

+                 'csrf_token': csrf_token,

+             }

+ 

+             output = self.app.post(

+                 '/test/give', data=data, follow_redirects=True)

+             self.assertEqual(output.status_code, 200)

+             self.assertIn(

+                 '</button>\n                      '

+                 'The project has been transferred to foo'

+                 '\n                    </div>\n',

+                 output.get_data(as_text=True))

+ 

+             self._check_user('foo')

+             # Make sure that the user giving the project is still an admin

+             project = pagure.lib.get_authorized_project(

+                 self.session, project_name='test')

+             self.assertEqual(len(project.users), 1)

+             self.assertEqual(project.users[0].user, 'pingou')

+ 

  

  if __name__ == '__main__':

      unittest.main(verbosity=2)

@@ -3400,7 +3400,7 @@

  

              # Valid query

              data = {

-                 'tag': ['red', 'green'],

+                 'tag': ['red1', 'green'],

                  'tag_description': ['lorem ipsum', 'sample description'],

                  'tag_color': ['#ff0000', '#00ff00'],

                  'csrf_token': csrf_token,
@@ -3423,6 +3423,10 @@

                  '#ff0000">red</span>\n'

                  '            &nbsp;<span class="text-muted">lorem ipsum'

                  '</span>', output.data)

+ #                '<span class="badge badge-info" '

+ #                'style="background-color:#ff0000">red1</span>\n'

+ #                '                              &nbsp;'

+ #                '<span class="text-muted">lorem ipsum</span>', output.data)

              self.assertIn(

                  '<input type="hidden" value="red" name="tag" />',

                  output.data)
@@ -3456,11 +3460,52 @@

                  '<input type="hidden" value="red" name="tag" />',

                  output.data)

  

+             # Invalid query - Tag already known

+             data = {

+                 'tag': ['red2'],

+                 'tag_color': ['#000'],

+                 'tag_description': [''],

+                 'csrf_token': csrf_token,

+             }

+             output = self.app.post(

+                 '/test/update/tags', data=data, follow_redirects=True)

+             self.assertEqual(output.status_code, 200)

+             output_text = output.get_data(as_text=True)

+             self.assertIn(

+                 '<title>Settings - test - Pagure</title>', output_text)

+             self.assertIn(

+                 '<span class="label label-info" style="background-color:'

+                 '#ff0000">red2</span>\n'

+                 '            &nbsp;<span class="text-muted"></span>',

+                 output.data)

+ #                '<span class="badge badge-info" '

+ #                'style="background-color:#ff0000">red2</span>\n'

+ #                '                              &nbsp;'

+ #                '<span class="text-muted"></span>', output_text)

+             self.assertIn(

+                 '<input type="hidden" value="green" name="tag" />',

+                 output_text)

+             self.assertIn(

+                 '<span class="label label-info" style="background-color:'

+                 '#ff0000">red3</span>\n'

+                 '            &nbsp;<span class="text-muted"></span>',

+                 output.data)

+ #                '<span class="badge badge-info" '

+ #                'style="background-color:#ff0000">red3</span>\n'

+ #                '                              &nbsp;'

+ #                '<span class="text-muted"></span>', output_text)

+             self.assertIn(

+                 '<input type="hidden" value="red" name="tag" />',

+                 output_text)

+             self.assertIn(

+                 '</button>\n                      Duplicated tag: red2',

+                 output_text)

+ 

          # After update, list tags

          tags = pagure.lib.get_tags_of_project(self.session, repo)

          self.assertEqual(

              sorted([tag.tag for tag in tags]),

-             ['blue', 'green', 'red', 'red2', 'red3'])

+             ['blue', 'green', 'red', 'red1', 'red2', 'red3'])

  

      @patch('pagure.lib.git.update_git')

      @patch('pagure.lib.notify.send_email')

@@ -70,7 +70,7 @@

          'Alice Author', 'alice@authors.tld')

      committer = pygit2.Signature(

          'Cecil Committer', 'cecil@committers.tld')

-     clone_repo.create_commit(

+     commit = clone_repo.create_commit(

          'refs/heads/master',  # the name of the reference to update

          author,

          committer,
@@ -81,6 +81,30 @@

          [commit.hex]

      )

  

+     # Create the default.md template

+     template = os.path.join(repopath, 'templates', 'default.md')

+     with open(template, 'w') as stream:

+         stream.write('Report your issue')

+     clone_repo.index.add(os.path.join('templates', 'default.md'))

+     clone_repo.index.write()

+ 

+     # Commit

+     tree = clone_repo.index.write_tree()

+     author = pygit2.Signature(

+         'Alice Author', 'alice@authors.tld')

+     committer = pygit2.Signature(

+         'Cecil Committer', 'cecil@committers.tld')

+     clone_repo.create_commit(

+         'refs/heads/master',  # the name of the reference to update

+         author,

+         committer,

+         'Add a default template',

+         # binary string representing the tree object ID

+         tree,

+         # list of binary strings representing parents of the new commit

+         [commit.hex]

+     )

+ 

  

  class PagureFlaskIssuestests(tests.Modeltests):

      """ Tests for flask issues controller of pagure """
@@ -144,10 +168,13 @@

                  '<select class="form-control c-select" id="type" name="type">',

                  output.data)

              self.assertIn(

-                 '<option selected value="RFE">RFE</option>',

+                 '<option value="RFE">RFE</option>',

+                 output.data)

+             self.assertIn(

+                 '<option value="2018-bid">2018-bid</option>',

                  output.data)

              self.assertIn(

-                 '<option selected value="2018-bid">2018-bid</option>',

+                 '<option selected value="default">default</option>',

                  output.data)

  

      def test_get_ticket_template_no_csrf(self):

@@ -1444,6 +1444,79 @@

          output = self.app.get('/TEST')

          self.assertEqual(output.status_code, 404)

  

+     def test_view_repo_more_button_absent_no_auth(self):

+         """ Test the view_repo endpoint and check if the "more" button is

+         absent when not logged in. """

+ 

+         tests.create_projects(self.session)

+         tests.create_projects_git(os.path.join(self.path, 'repos'), bare=True)

+ 

+         output = self.app.get('/test')

+         self.assertEqual(output.status_code, 200)

+         output_text = output.get_data(as_text=True)

+         self.assertNotIn(

+             '<span class="pull-xs-right"><a data-toggle="collapse" '

+             'href="#moregiturls"', output_text)

+         self.assertIn('<p>This repo is brand new!</p>', output_text)

+         self.assertIn(

+             '<div class="projectinfo m-t-1 m-b-1">\n'

+             'test project #1      </div>', output_text)

+         self.assertIn(

+             '<span class="hidden-sm-down">Stats&nbsp;</span>',

+             output_text)

+         self.perfMaxWalks(0, 0)

+         self.perfReset()

+ 

+     def test_view_repo_more_button_present(self):

+         """ Test the view_repo endpoint and check if the "more" button is

+         present when it should be. """

+ 

+         tests.create_projects(self.session)

+         tests.create_projects_git(os.path.join(self.path, 'repos'), bare=True)

+ 

+         user = tests.FakeUser(username='pingou')

+         with tests.user_set(self.app.application, user):

+             output = self.app.get('/test')

+             self.assertEqual(output.status_code, 200)

+             output_text = output.get_data(as_text=True)

+             self.assertIn(

+                 '<span class="pull-xs-right"><a data-toggle="collapse" '

+                 'href="#moregiturls"', output_text)

+             self.assertIn('<p>This repo is brand new!</p>', output_text)

+             self.assertIn(

+                 '<div class="projectinfo m-t-1 m-b-1">\n'

+                 'test project #1      </div>', output_text)

+             self.assertIn(

+                 '<span class="hidden-sm-down">Stats&nbsp;</span>',

+                 output_text)

+             self.perfMaxWalks(0, 0)

+             self.perfReset()

+ 

+     def test_view_repo_more_button_absent_no_access(self):

+         """ Test the view_repo endpoint and check if the "more" button is

+         absent if the user doesn't have access to the project. """

+ 

+         tests.create_projects(self.session)

+         tests.create_projects_git(os.path.join(self.path, 'repos'), bare=True)

+ 

+         user = tests.FakeUser(username='foo')

+         with tests.user_set(self.app.application, user):

+             output = self.app.get('/test')

+             self.assertEqual(output.status_code, 200)

+             output_text = output.get_data(as_text=True)

+             self.assertNotIn(

+                 '<span class="pull-xs-right"><a data-toggle="collapse" '

+                 'href="#moregiturls"', output_text)

+             self.assertIn('<p>This repo is brand new!</p>', output_text)

+             self.assertIn(

+                 '<div class="projectinfo m-t-1 m-b-1">\n'

+                 'test project #1      </div>', output_text)

+             self.assertIn(

+                 '<span class="hidden-sm-down">Stats&nbsp;</span>',

+                 output_text)

+             self.perfMaxWalks(0, 0)

+             self.perfReset()

+ 

      def test_view_repo(self):

          """ Test the view_repo endpoint. """

  

@@ -5593,6 +5593,7 @@

              sorted([a.name for a in acls]),

              [

                  'commit_flag',

+                 'create_branch',

                  'create_project',

                  'fork_project',

                  'generate_acls_project',

@@ -221,6 +221,13 @@

              else:

                  self.assertEqual(regex.match(text), None)

  

+         text = 'relates https://localhost/SSSD/ding-libs/issue/31'

+         for index, regex in enumerate(pagure.lib.link.RELATES):

+             if index == 2:

+                 self.assertNotEqual(regex.match(text), None)

+             else:

+                 self.assertEqual(regex.match(text), None)

+ 

      def test_fixes_regex(self):

          ''' Test the fixes regex present in pagure.lib.link. '''

  
@@ -258,6 +265,8 @@

              ('test', '123'))

          project_match('Merge: http://localhost/fork/pingou/test/issue/1234#foo',

              ('test', '1234'))

+         project_match('Merges: https://localhost/SSSD/ding-libs/pull-request/3188',

+             ('ding-libs', '3188'))

  

          # issue matches

          def issue_match(text, issue):

@@ -332,6 +332,84 @@

          out = pagure.lib.notify._get_emails_for_obj(req)

          self.assertEqual(out, exp)

  

+     def test_get_emails_for_obj_private_issue(self):

+         """ Test the _get_emails_for_obj method from pagure.lib.notify. """

+ 

+         # Create the project ns/test

+         item = pagure.lib.model.Project(

+             user_id=1,  # pingou

+             name='test3',

+             namespace='ns',

+             description='test project #1',

+             hook_token='aaabbbcccdd',

+         )

+         item.close_status = ['Invalid', 'Insufficient data', 'Fixed']

+         self.session.add(item)

+         self.session.commit()

+ 

+         # Create the private ticket

+         iss = pagure.lib.new_issue(

+             issue_id=4,

+             session=self.session,

+             repo=item,

+             title='test issue',

+             content='content test issue',

+             user='pingou',

+             private=True,

+             ticketfolder=None,

+         )

+         self.session.commit()

+         self.assertEqual(iss.id, 4)

+         self.assertEqual(iss.title, 'test issue')

+ 

+         exp = set(['bar@pingou.com'])

+         out = pagure.lib.notify._get_emails_for_obj(iss)

+         self.assertEqual(out, exp)

+ 

+         # Comment on the ticket

+         out = pagure.lib.add_issue_comment(

+             self.session,

+             issue=iss,

+             comment='This is a comment',

+             user='foo',

+             ticketfolder=None,

+             notify=False)

+         self.assertEqual(out, 'Comment added')

+ 

+         exp = set(['bar@pingou.com', 'foo@bar.com'])

+         out = pagure.lib.notify._get_emails_for_obj(iss)

+         self.assertEqual(out, exp)

+ 

+         # Create user `bar`

+         item = pagure.lib.model.User(

+             user='bar',

+             fullname='bar name',

+             password='bar',

+             default_email='bar@bar.com',

+         )

+         self.session.add(item)

+         item = pagure.lib.model.UserEmail(

+             user_id=3,

+             email='bar@bar.com')

+         self.session.add(item)

+         self.session.commit()

+ 

+         # Add bar on the project with ticket acl

+         project = pagure.lib._get_project(self.session, 'test3', namespace='ns')

+         msg = pagure.lib.add_user_to_project(

+             session=self.session,

+             project=project,

+             new_user='bar',

+             user='pingou',

+             access='ticket',

+         )

+         self.session.commit()

+         self.assertEqual(msg, 'User added')

+ 

+         exp = set(['bar@pingou.com', 'foo@bar.com'])

+         out = pagure.lib.notify._get_emails_for_obj(iss)

+         self.assertEqual(out, exp)

+ 

      @patch('pagure.lib.notify.smtplib.SMTP')

      def test_send_email(self, mock_smtp):

          """ Test the notify_new_comment method from pagure.lib.notify. """