From c95e281431a9c7157093e9410c20cf83a60e64c6 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Aug 11 2016 08:44:49 +0000 Subject: [PATCH 1/635] Move the pagure_ci doc into the usage section --- diff --git a/doc/pagure_ci.rst b/doc/pagure_ci.rst deleted file mode 100644 index 6f76c31..0000000 --- a/doc/pagure_ci.rst +++ /dev/null @@ -1,135 +0,0 @@ -========= -Pagure CI -========= - -Pagure CI is a continuous integration tool using which the PR on the projects -can be tested and flaged with the status of the build. - -How to enable Pagure CI -======================= - -* Enable the Fedmsg plugin in pagure project setting . This will emit the message - to for consumer to consume it. - -* Fill in the Pagure CI form with the required details. - -:: - - Pagure Project Name - Jenkins Project Name - Jenkins Token - Jenkins Url - - All of which are required field. - -* The jenkins token is any string that you give here. The only thing that should - be kept in mind that this token should be same through out. - -* This will give a POST URL which will be used for Job Notification in Jenkins - -* The POST url will only appear only after you successfully submitted the form. - - -Configuring Jenkins -=================== - -Jenkins configuration is the most important part of how the Pagure CI works, -after you login to your Jenkins Instance. - -* Go to Manage Jenkins -> Configuire Global Security and under that select - `Project-based Matrix Authorization Strategy` - -* Add your username here and make sure to give that username all the permissions. - You should give all the permissions possible so that you save your self from - getting locked in Jenkins. - -* Download the following plugins: - -:: - - Build Authorization Root Plugin - Git Plugins - Notification Plugin - - -* Click on the New Item - -* Select Freestyle Project - -* Click OK and enter the name of the project, make sure the project name - you filled in the Pagure CI form should match the name you entered here. - -* Under 'Job Notification' click 'Add Endpoint' - -* Fields in Endpoint will be : - -:: - - FORMAT: JSON - PROTOCOL: HTTP - EVENT: Job Finalized - URL: - TIMEOUT: 3000 - LOG: 1 - -* Tick the build is parameterized - -* From the Add Parameter drop down select String Parameter - -* Two string parameters need to be created REPO and BRANCH - -* Source Code Management select Git and give the URL of the pagure project - -* Under Build Trigger click on Trigger build remotely and give the same token - that you gave in the Pagure CI form. - -* Under Build -> Add build step -> Execute Shell - -* In the box given enter the shell steps you want for testing your project. - - -Example Script - -:: - - if [ -n "$REPO" -a -n "$BRANCH" ]; then - git remote rm proposed || true - git remote add proposed "$REPO" - git fetch proposed - git checkout origin/master - git config --global user.email "you@example.com" - git config --global user.name "Your Name" - git merge --no-ff "proposed/$BRANCH" -m "Merge PR" - fi - -How to install Pagure CI -======================== - -Pagure CI requires `fedmsg` to run since it uses a consumer to get messages -and take appropriate actions. The dependency that is required is `fedmdg-hubs`. -For that the steps are given. - -To install the dependencies required: - - `dnf install fedmsg-hub` - -`fedmsg` apart from the consumer require a file that tells to which cosumer -it should listen to. This file basically enable the consumer in PagureCI/. -For doing that, we need to place this file in appropriate directory. - - `sudo cp pagure/fedmsg.d/pagure_ci.py /etc/fedmsg.d/` - -Since the deployment is done using rpm, the next step is covered using `setup.py` -which binds the consumer with the environment, this is done while building the rpm -so if rpm is already built this is not explicitly required. - - `python setup.py install` - -Run the service: - - `sudo systemctl enable fedmsg-hub.service` - - `sudo systemctl start fedmsg-hub.service` - - - diff --git a/doc/usage/pagure_ci.rst b/doc/usage/pagure_ci.rst new file mode 100644 index 0000000..3d7780d --- /dev/null +++ b/doc/usage/pagure_ci.rst @@ -0,0 +1,132 @@ +========= +Pagure CI +========= + +Pagure CI is a continuous integration tool using which the PR on the projects +can be tested and flaged with the status of the build. + +How to enable Pagure CI +======================= + +* Enable the Fedmsg plugin in pagure project setting . This will emit the message + to for consumer to consume it. + +* Fill in the Pagure CI form with the required details. + +:: + + Pagure Project Name + Jenkins Project Name + Jenkins Token + Jenkins Url + + All of which are required field. + +* The jenkins token is any string that you give here. The only thing that should + be kept in mind that this token should be same through out. + +* This will give a POST URL which will be used for Job Notification in Jenkins + +* The POST url will only appear only after you successfully submitted the form. + + +Configuring Jenkins +=================== + +Jenkins configuration is the most important part of how the Pagure CI works, +after you login to your Jenkins Instance. + +* Go to Manage Jenkins -> Configuire Global Security and under that select + `Project-based Matrix Authorization Strategy` + +* Add your username here and make sure to give that username all the permissions. + You should give all the permissions possible so that you save your self from + getting locked in Jenkins. + +* Download the following plugins: + +:: + + Build Authorization Root Plugin + Git Plugins + Notification Plugin + + +* Click on the New Item + +* Select Freestyle Project + +* Click OK and enter the name of the project, make sure the project name + you filled in the Pagure CI form should match the name you entered here. + +* Under 'Job Notification' click 'Add Endpoint' + +* Fields in Endpoint will be : + +:: + + FORMAT: JSON + PROTOCOL: HTTP + EVENT: Job Finalized + URL: + TIMEOUT: 3000 + LOG: 1 + +* Tick the build is parameterized + +* From the Add Parameter drop down select String Parameter + +* Two string parameters need to be created REPO and BRANCH + +* Source Code Management select Git and give the URL of the pagure project + +* Under Build Trigger click on Trigger build remotely and give the same token + that you gave in the Pagure CI form. + +* Under Build -> Add build step -> Execute Shell + +* In the box given enter the shell steps you want for testing your project. + + +Example Script + +:: + + if [ -n "$REPO" -a -n "$BRANCH" ]; then + git remote rm proposed || true + git remote add proposed "$REPO" + git fetch proposed + git checkout origin/master + git config --global user.email "you@example.com" + git config --global user.name "Your Name" + git merge --no-ff "proposed/$BRANCH" -m "Merge PR" + fi + +How to install Pagure CI +======================== + +Pagure CI requires `fedmsg` to run since it uses a consumer to get messages +and take appropriate actions. The dependency that is required is `fedmdg-hubs`. +For that the steps are given. + +To install the dependencies required: + + `dnf install fedmsg-hub` + +`fedmsg` apart from the consumer require a file that tells to which cosumer +it should listen to. This file basically enable the consumer in PagureCI/. +For doing that, we need to place this file in appropriate directory. + + `sudo cp pagure/fedmsg.d/pagure_ci.py /etc/fedmsg.d/` + +Since the deployment is done using rpm, the next step is covered using `setup.py` +which binds the consumer with the environment, this is done while building the rpm +so if rpm is already built this is not explicitly required. + + `python setup.py install` + +Run the service: + + `sudo systemctl enable fedmsg-hub.service` + + `sudo systemctl start fedmsg-hub.service` From 29fc18a1fdbc9f7c33a2189387c2154a9f028501 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Aug 11 2016 08:44:49 +0000 Subject: [PATCH 2/635] Make the pagure_ci home page something generic --- diff --git a/doc/usage/pagure_ci.rst b/doc/usage/pagure_ci.rst index 3d7780d..96004ff 100644 --- a/doc/usage/pagure_ci.rst +++ b/doc/usage/pagure_ci.rst @@ -1,132 +1,19 @@ -========= Pagure CI ========= -Pagure CI is a continuous integration tool using which the PR on the projects -can be tested and flaged with the status of the build. - -How to enable Pagure CI -======================= - -* Enable the Fedmsg plugin in pagure project setting . This will emit the message - to for consumer to consume it. - -* Fill in the Pagure CI form with the required details. - -:: - - Pagure Project Name - Jenkins Project Name - Jenkins Token - Jenkins Url - - All of which are required field. - -* The jenkins token is any string that you give here. The only thing that should - be kept in mind that this token should be same through out. - -* This will give a POST URL which will be used for Job Notification in Jenkins - -* The POST url will only appear only after you successfully submitted the form. - - -Configuring Jenkins -=================== - -Jenkins configuration is the most important part of how the Pagure CI works, -after you login to your Jenkins Instance. - -* Go to Manage Jenkins -> Configuire Global Security and under that select - `Project-based Matrix Authorization Strategy` - -* Add your username here and make sure to give that username all the permissions. - You should give all the permissions possible so that you save your self from - getting locked in Jenkins. - -* Download the following plugins: - -:: - - Build Authorization Root Plugin - Git Plugins - Notification Plugin - - -* Click on the New Item - -* Select Freestyle Project - -* Click OK and enter the name of the project, make sure the project name - you filled in the Pagure CI form should match the name you entered here. - -* Under 'Job Notification' click 'Add Endpoint' - -* Fields in Endpoint will be : - -:: - - FORMAT: JSON - PROTOCOL: HTTP - EVENT: Job Finalized - URL: - TIMEOUT: 3000 - LOG: 1 - -* Tick the build is parameterized - -* From the Add Parameter drop down select String Parameter - -* Two string parameters need to be created REPO and BRANCH - -* Source Code Management select Git and give the URL of the pagure project - -* Under Build Trigger click on Trigger build remotely and give the same token - that you gave in the Pagure CI form. - -* Under Build -> Add build step -> Execute Shell - -* In the box given enter the shell steps you want for testing your project. - - -Example Script - -:: - - if [ -n "$REPO" -a -n "$BRANCH" ]; then - git remote rm proposed || true - git remote add proposed "$REPO" - git fetch proposed - git checkout origin/master - git config --global user.email "you@example.com" - git config --global user.name "Your Name" - git merge --no-ff "proposed/$BRANCH" -m "Merge PR" - fi - -How to install Pagure CI -======================== - -Pagure CI requires `fedmsg` to run since it uses a consumer to get messages -and take appropriate actions. The dependency that is required is `fedmdg-hubs`. -For that the steps are given. - -To install the dependencies required: - - `dnf install fedmsg-hub` - -`fedmsg` apart from the consumer require a file that tells to which cosumer -it should listen to. This file basically enable the consumer in PagureCI/. -For doing that, we need to place this file in appropriate directory. +Pagure CI is a service integrating the results of Continuous Integration (CI) +services, such as jenkins or travis-ci, into pull-requests opened against +your project on pagure. - `sudo cp pagure/fedmsg.d/pagure_ci.py /etc/fedmsg.d/` -Since the deployment is done using rpm, the next step is covered using `setup.py` -which binds the consumer with the environment, this is done while building the rpm -so if rpm is already built this is not explicitly required. +.. note: By default pagure-ci is off, an admin of your pagure instance will + need to configure it to support one or more CI services. Check the + configuration section on how to do that. - `python setup.py install` -Run the service: +Contents: - `sudo systemctl enable fedmsg-hub.service` +.. toctree:: + :maxdepth: 2 - `sudo systemctl start fedmsg-hub.service` + usage/pagure_ci_jenkins From 5854255dd4e3ca6d387b695591dcd9f4552b76fb Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Aug 11 2016 08:44:49 +0000 Subject: [PATCH 3/635] Introduce the pagure_ci_jenkins doc, specific for jenkings/pagure integration --- diff --git a/doc/usage/pagure_ci_jenkins.rst b/doc/usage/pagure_ci_jenkins.rst new file mode 100644 index 0000000..ec54ebf --- /dev/null +++ b/doc/usage/pagure_ci_jenkins.rst @@ -0,0 +1,92 @@ +Jenkins with Pagure-ci +====================== + +Jenkins is a Continuous Integration service that can be configured to be +integrated with pagure. + +This document describe the steps needed to make it work. + + +How to enable Pagure CI +======================= + +* Visit the settings page of your project + +* Scroll down to the `Hooks` section and click on `Pagure CI` + +* Select the type of CI service you want + +* Enter the URL to the project on the CI service. For example, if your + project is running at `http://jenkins.fedoraproject.org` you will need to + enter the url: `http://jenkins.fedoraproject.org/job/` + +* Tick the checkbox activating the hook. + + +These steps will activate the hook, after reloading the page or the tab, you +will be given access to two important values: the token used to trigger the +build on jenkins and the URL used by jenkins to report the status of the +build. +Keep these two available when configuring jenkins for your project. + + +Configure Jenkins +================= + +These steps can only be made by the admins of your jenkins instance, but +they only need to be made once. + +* Download the following plugins: + + * `Git Plugin `_ + * `Notification Plugin `_ + + +Configure your project on Jenkins +================================= + +* Go to the `Configure` page of your project + +* Under `Job Notification` click `Add Endpoint` + +* Fields in Endpoint will be : + +:: + + FORMAT: JSON + PROTOCOL: HTTP + EVENT: Job Finalized + URL: + TIMEOUT: 3000 + LOG: 1 + +* Tick the checkbox `This build is parameterized` + +* Add two `String Parameters` named REPO and BRANCH + +* Source Code Management select Git and give the URL of the pagure project + +* Under Build Trigger click on Trigger build remotely and specify the token + given by pagure. + +* Under Build -> Add build step -> Execute Shell + +* In the box given enter the shell steps you want for testing your project. + + +Example Script + +:: + + # Script specific for Pull-Request build + if [ -n "$REPO" -a -n "$BRANCH" ]; then + git remote rm proposed || true + git remote add proposed "$REPO" + git fetch proposed + git checkout origin/master + git config --global user.email "you@example.com" + git config --global user.name "Your Name" + git merge --no-ff "proposed/$BRANCH" -m "Merge PR" + fi + + # Part of the script specific to how you run the tests on your project From c1b01d554ac784da47859bf373a720b8877c6d38 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Aug 11 2016 08:44:49 +0000 Subject: [PATCH 4/635] Include pagure_ci in the usage doc --- diff --git a/doc/usage.rst b/doc/usage.rst index aee0095..3996a8a 100644 --- a/doc/usage.rst +++ b/doc/usage.rst @@ -41,3 +41,4 @@ Contents: usage/pr_custom_page usage/theming usage/upgrade_db + usage/pagure_ci From 219d3119c02789ee7592a1bafe83e66fb1e05ad4 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Aug 11 2016 08:44:49 +0000 Subject: [PATCH 5/635] Do not repeat information from the doc, rather point to it --- diff --git a/pagure-ci/README.rst b/pagure-ci/README.rst index 0ab6362..9dac42e 100644 --- a/pagure-ci/README.rst +++ b/pagure-ci/README.rst @@ -9,76 +9,5 @@ dependencies are resolved. PAGURE_CONFIG=/path/to/config PYTHONPATH=. python pagure-ci/pagure_ci_server.py - -Configure Jenkins -================= - -Jenkins configuration is the most important part of how the Pagure CI works, -after you login to your Jenkins Instance. - - -* Go to Manage Jenkins -> Configuire Global Security and under that select - 'Project-based Matrix Authorization Strategy' - -* Add a user and give all the permission to that user. - -* Download the following plugins: - - * Build Authorization Root Plugin - * `Git Plugin `_ - * `Notification Plugin `_ - - -Configure your project on Jenkins -================================= - -* Start by enabling the `Pagure CI` hook in the settings of your project on - pagure. This will provide you two values needed to configure your project - on jenkins: a token and an URL that jenkins calls to return the results - of a build. - -* Go to the `Configure` page of your project - -* Under `Job Notification` click `Add Endpoint` - -* Fields in Endpoint will be : - -:: - - FORMAT: JSON - PROTOCOL: HTTP - EVENT: Job Finalized - URL: - TIMEOUT: 3000 - LOG: 1 - -* Tick the checkbox `This build is parameterized` - -* Add two `String Parameters` named REPO and BRANCH - -* Source Code Management select Git and give the URL of the pagure project - -* Under Build Trigger click on Trigger build remotely and specify the token - given by pagure. - -* Under Build -> Add build step -> Execute Shell - -* In the box given enter the shell steps you want for testing your project. - - -Example Script - -:: - - # Script specific for Pull-Request build - if [ -n "$REPO" -a -n "$BRANCH" ]; then - git remote rm proposed || true - git remote add proposed "$REPO" - git fetch proposed - git checkout origin/master - git config --global user.email "you@example.com" - git config --global user.name "Your Name" - git merge --no-ff "proposed/$BRANCH" -m "Merge PR" - fi - - # Part of the script specific to how you run the tests on your project +Check `doc/usage/pagure_ci.rst` for further information on how to set up +and configure your project on both pagure and the CI services From 5aef08047002e6a875dddf964447aab37f0e148b Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Aug 11 2016 08:44:49 +0000 Subject: [PATCH 6/635] Fix including the pagure_ci_jenkins page --- diff --git a/doc/usage/pagure_ci.rst b/doc/usage/pagure_ci.rst index 96004ff..e0d337f 100644 --- a/doc/usage/pagure_ci.rst +++ b/doc/usage/pagure_ci.rst @@ -16,4 +16,4 @@ Contents: .. toctree:: :maxdepth: 2 - usage/pagure_ci_jenkins + pagure_ci_jenkins From 2323afda018f49bf595797888703b0c64d5e01f1 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Aug 11 2016 08:44:49 +0000 Subject: [PATCH 7/635] Adjust the title levels in the pagure_ci_jenkins doc --- diff --git a/doc/usage/pagure_ci_jenkins.rst b/doc/usage/pagure_ci_jenkins.rst index ec54ebf..10a9a98 100644 --- a/doc/usage/pagure_ci_jenkins.rst +++ b/doc/usage/pagure_ci_jenkins.rst @@ -8,7 +8,7 @@ This document describe the steps needed to make it work. How to enable Pagure CI -======================= +----------------------- * Visit the settings page of your project @@ -31,7 +31,7 @@ Keep these two available when configuring jenkins for your project. Configure Jenkins -================= +----------------- These steps can only be made by the admins of your jenkins instance, but they only need to be made once. @@ -43,7 +43,7 @@ they only need to be made once. Configure your project on Jenkins -================================= +--------------------------------- * Go to the `Configure` page of your project From 1d815a409bcfd8d76b62f52274953c3ecadbb9a1 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Aug 11 2016 08:44:49 +0000 Subject: [PATCH 8/635] Document PAGURE_CI_SERVICES in the documentation --- diff --git a/doc/configuration.rst b/doc/configuration.rst index 42a36c0..ca83c75 100644 --- a/doc/configuration.rst +++ b/doc/configuration.rst @@ -515,3 +515,18 @@ which is useful for pagure instances running since before 1.3 but is not for newer instances. Defaults to: ``False``. + + +PAGURE_CI_SERVICES +~~~~~~~~~~~~~~~~~~ + +Pagure can be configure to integrate results of a Continuous Integration (CI) +service to pull-requests open against a project. + +To enable this integration, follow the documentation on how to install +pagure-ci and set this configuration key to ``['jenkins']`` (Jenkins being +the only CI service supported at the moment). + +Defaults to: ``None``. + +.. warning:: Requires `Redis` to be configured and running. From 499dad90eec78b29a4e3420485e9dcb64c796325 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Aug 11 2016 08:44:49 +0000 Subject: [PATCH 9/635] Add a documentation on how to install pagure-ci --- diff --git a/doc/index.rst b/doc/index.rst index 268c3e3..3daea4c 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -34,6 +34,7 @@ Contents: install_milter install_evs install_webhooks + install_pagure_ci configuration development usage diff --git a/doc/install_pagure_ci.rst b/doc/install_pagure_ci.rst new file mode 100644 index 0000000..a1158e5 --- /dev/null +++ b/doc/install_pagure_ci.rst @@ -0,0 +1,61 @@ +Installing pagure-ci +==================== + +A CI stands for `Continuous Integration +`_. Pagure can be +configured to integrate results coming from CI services, such as `Jenkins +`_ on pull-request opened +against the project. + + +.. note: Currently, pagure only supports `Jenkins` but we welcome help to + integrate pagure with other services such as `travis-ci + `_. + + +Configure your system +--------------------- + +* Install the required dependencies + +:: + + python-jenkins + python-redis + python-trollius-redis + python-trollius + +.. note:: We ship a systemd unit file for pagure_ci but we welcome patches + for scripts for other init systems. + + +* Install the files of pagure-ci as follow: + ++--------------------------------------+---------------------------------------------------+ +| Source | Destination | ++======================================+===================================================+ +| ``pagure-ci/pagure_ci_server.py`` | ``/usr/libexec/pagure-ci/pagure_ci_server.py`` | ++--------------------------------------+---------------------------------------------------+ +| ``pagure-ci/pagure_ci.service`` | ``/etc/systemd/system/pagure_ci.service`` | ++--------------------------------------+---------------------------------------------------+ + +The first file is the pagure-ci service itself, triggering the build on the +CI service when there is a new pull-request or a change to an existing one. + +The second file is the systemd service file. + +* Configure your pagure instance to support CI, add the following to your + configuration file + +:: + + PAGURE_CI_SERVICES = ['jenkins'] + +* Activate the service and ensure it's started upon boot: + +:: + + systemctl enable redis + systemctl start redis + systemctl enable pagure_ci + systemctl start pagure_ci From 4e920083a654a11877df17cb461e2e1152ab0aed Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Aug 11 2016 08:44:50 +0000 Subject: [PATCH 10/635] Adjust syntax for a note --- diff --git a/doc/usage/pagure_ci.rst b/doc/usage/pagure_ci.rst index e0d337f..8176c5b 100644 --- a/doc/usage/pagure_ci.rst +++ b/doc/usage/pagure_ci.rst @@ -6,7 +6,7 @@ services, such as jenkins or travis-ci, into pull-requests opened against your project on pagure. -.. note: By default pagure-ci is off, an admin of your pagure instance will +.. note:: By default pagure-ci is off, an admin of your pagure instance will need to configure it to support one or more CI services. Check the configuration section on how to do that. From c68e929ed03ba760bc3606012d444ba85fe3eb36 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Aug 11 2016 09:14:51 +0000 Subject: [PATCH 11/635] Fix unit-tests We were retrieving the commit hash of the wrong repo then trying to look for it on the right page. This should fix that. --- diff --git a/tests/test_pagure_flask_ui_repo_slash_name.py b/tests/test_pagure_flask_ui_repo_slash_name.py index 4ecd2a3..349d7f8 100644 --- a/tests/test_pagure_flask_ui_repo_slash_name.py +++ b/tests/test_pagure_flask_ui_repo_slash_name.py @@ -239,11 +239,18 @@ class PagureFlaskSlashInNametests(tests.Modeltests): output.data) # Try accessing the commit - gitrepo = os.path.join(tests.HERE, 'repos', 'test.git') + gitrepo = os.path.join(tests.HERE, 'repos', 'forks/test.git') repo = pygit2.Repository(gitrepo) master_branch = repo.lookup_branch('master') first_commit = master_branch.get_object().hex + output = self.app.get('/forks/test/commits') + self.assertEqual(output.status_code, 200) + self.assertIn(first_commit, output.data) + self.assertIn( + 'Commit - forks/test ', output.data) From bb29e1232c6bac32553a0fb43236893f962e14dd Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Aug 11 2016 13:30:44 +0000 Subject: [PATCH 12/635] Add a display_name and a description to groups --- diff --git a/pagure/lib/model.py b/pagure/lib/model.py index 68085e6..abf16d9 100644 --- a/pagure/lib/model.py +++ b/pagure/lib/model.py @@ -1261,6 +1261,8 @@ class PagureGroup(BASE): id = sa.Column(sa.Integer, primary_key=True) group_name = sa.Column(sa.String(16), nullable=False, unique=True) + display_name = sa.Column(sa.String(255), nullable=False, unique=True) + description = sa.Column(sa.String(255), nullable=True) group_type = sa.Column( sa.String(16), sa.ForeignKey( From 7702ddf182c1a129f3d7b160e8df84545f9481ce Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Aug 11 2016 13:30:54 +0000 Subject: [PATCH 13/635] Add a to_json() method to the PagureGroup objects --- diff --git a/pagure/lib/model.py b/pagure/lib/model.py index abf16d9..93a9f03 100644 --- a/pagure/lib/model.py +++ b/pagure/lib/model.py @@ -1292,6 +1292,21 @@ class PagureGroup(BASE): return 'Group: %s - name %s' % (self.id, self.group_name) + def to_json(self, public=False): + ''' Returns a dictionnary representation of the pull-request. + + ''' + output = { + 'name': self.group_name, + 'display_name': self.display_name, + 'description': self.description, + 'group_type': self.group_type, + 'creator': self.creator.to_json(public=public), + 'date_created': self.created.strftime('%s'), + } + + return output + class ProjectGroup(BASE): """ From 4451bc7c01475ca3a44ca4d37f25520b1faf24ff Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Aug 11 2016 13:33:49 +0000 Subject: [PATCH 14/635] Specify display name and description when creating a group --- diff --git a/pagure/lib/__init__.py b/pagure/lib/__init__.py index 8b1be61..73a0ad5 100644 --- a/pagure/lib/__init__.py +++ b/pagure/lib/__init__.py @@ -2537,7 +2537,9 @@ def delete_user_of_group(session, username, groupname, user, is_admin, session.flush() -def add_group(session, group_name, group_type, user, is_admin, blacklist): +def add_group( + session, group_name, display_name, description, + group_type, user, is_admin, blacklist): ''' Creates a new group with the given information. ''' if ' ' in group_name: @@ -2576,6 +2578,8 @@ def add_group(session, group_name, group_type, user, is_admin, blacklist): grp = pagure.lib.model.PagureGroup( group_name=group_name, + display_name=display_name, + description=description, group_type=group_type, user_id=user.id, ) diff --git a/pagure/templates/add_group.html b/pagure/templates/add_group.html index 11fe7c1..73bd161 100644 --- a/pagure/templates/add_group.html +++ b/pagure/templates/add_group.html @@ -14,6 +14,8 @@ {{ render_field_in_row(form.group_name) }} + {{ render_field_in_row(form.display_name) }} + {{ render_field_in_row(form.description) }} {%- if admin %} {{ render_field_in_row(form.group_type) }} {%- endif %} diff --git a/pagure/ui/groups.py b/pagure/ui/groups.py index e98e844..a1183ca 100644 --- a/pagure/ui/groups.py +++ b/pagure/ui/groups.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """ - (c) 2015 - Copyright Red Hat Inc + (c) 2015-2016 - Copyright Red Hat Inc Authors: Pierre-Yves Chibon @@ -234,10 +234,15 @@ def add_group(): if form.validate_on_submit(): try: - group_name = form.group_name.data + group_name = form.group_name.data.strip() + display_name=form.display_name.data.strip() + description=form.description.data.strip() + msg = pagure.lib.add_group( session=pagure.SESSION, group_name=group_name, + display_name=display_name, + description=description, group_type=form.group_type.data, user=flask.g.fas_user.username, is_admin=pagure.is_admin(), From 42452cb42835df4d264f65a0db725081a14cbb6a Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Aug 11 2016 13:39:44 +0000 Subject: [PATCH 15/635] Add the possibility to edit group info Group info including the display name and the description --- diff --git a/pagure/forms.py b/pagure/forms.py index d6d0e24..8802c98 100644 --- a/pagure/forms.py +++ b/pagure/forms.py @@ -359,7 +359,23 @@ class CommentForm(wtf.Form): [wtforms.validators.Required(), file_virus_validator]) -class NewGroupForm(wtf.Form): +class EditGroupForm(wtf.Form): + """ Form to ask for a password change. """ + display_name = wtforms.TextField( + 'Group name to display *', + [ + wtforms.validators.Required(), + ] + ) + description = wtforms.TextField( + 'Description *', + [ + wtforms.validators.Required(), + ] + ) + + +class NewGroupForm(EditGroupForm): """ Form to ask for a password change. """ group_name = wtforms.TextField( 'Group name *', diff --git a/pagure/lib/__init__.py b/pagure/lib/__init__.py index 73a0ad5..03efeca 100644 --- a/pagure/lib/__init__.py +++ b/pagure/lib/__init__.py @@ -2498,6 +2498,51 @@ def add_user_to_group(session, username, group, user, is_admin): new_user.username, group.group_name) +def edit_group_info( + session, group, display_name, description, user, is_admin): + ''' Edit the information regarding a given group. + ''' + action_user = user + user = search_user(session, username=user) + if not user: + raise pagure.exceptions.PagureException( + 'No user `%s` found' % action_user) + + if group.group_name not in user.groups \ + and not is_admin \ + and user.username != group.creator.username: + raise pagure.exceptions.PagureException( + 'You are not allowed to edit this group') + + edits = [] + if display_name and display_name != group.display_name: + group.display_name = display_name + edits.append('display_name') + if description and description != group.description: + group.description = description + edits.append('description') + + session.add(group) + session.flush() + + msg = 'Nothing changed' + if edits: + pagure.lib.notify.log( + None, + topic='group.edit', + msg=dict( + group=group.to_json(public=True), + fields=edits, + agent=user.username, + ), + redis=REDIS, + ) + msg = 'Group "%s" (%s) edited' % ( + group.display_name, group.group_name) + + return msg + + def delete_user_of_group(session, username, groupname, user, is_admin, force=False): ''' Removes the specified user from the given group. diff --git a/pagure/templates/edit_group.html b/pagure/templates/edit_group.html new file mode 100644 index 0000000..8013b60 --- /dev/null +++ b/pagure/templates/edit_group.html @@ -0,0 +1,32 @@ +{% extends "master.html" %} +{% from "_formhelper.html" import render_field_in_row %} + +{% set tag = "groups" %} +{% block title %}Edit group: {{ group.group_name }}{% endblock %} + + +{% block content %} + +

Edit group: {{ group.group_name }}

+ +
+
+ +
+ {{ render_field_in_row(form.display_name) }} + {{ render_field_in_row(form.description) }} + {%- if admin %} + {{ render_field_in_row(form.group_type) }} + {%- endif %} +
+

+ + + {{ form.csrf_token }} +

+ + + +{% endblock %} diff --git a/pagure/ui/groups.py b/pagure/ui/groups.py index a1183ca..877333d 100644 --- a/pagure/ui/groups.py +++ b/pagure/ui/groups.py @@ -114,6 +114,63 @@ def view_group(group): ) +@pagure.APP.route('/group//edit/', methods=['GET', 'POST']) +@pagure.APP.route('/group//edit', methods=['GET', 'POST']) +@pagure.login_required +def edit_group(group): + ''' Allows editing the information about this group. ''' + if not pagure.APP.config.get('ENABLE_USER_MNGT', True): + flask.abort(404) + + group_type = 'user' + is_admin = pagure.is_admin() + if is_admin: + group_type = None + group = pagure.lib.search_groups( + pagure.SESSION, group_name=group, group_type=group_type) + + if not group: + flask.abort(404, 'Group not found') + + # Edit group info + form = pagure.forms.EditGroupForm() + if form.validate_on_submit(): + + try: + msg = pagure.lib.edit_group_info( + pagure.SESSION, + group=group, + display_name=form.display_name.data, + description=form.description.data, + user=flask.g.fas_user.username, + is_admin=is_admin, + ) + pagure.SESSION.commit() + flask.flash(msg) + return flask.redirect( + flask.url_for('.view_group', group=group.group_name)) + except pagure.exceptions.PagureException as err: + pagure.SESSION.rollback() + flask.flash(err.message, 'error') + return flask.redirect( + flask.url_for('.view_group', group=group.group_name)) + except SQLAlchemyError as err: # pragma: no cover + pagure.SESSION.rollback() + flask.flash( + 'Could not edit group `%s`.' % (group.group_name), + 'error') + pagure.APP.logger.debug( + 'Could not edit group `%s`.' % (group.group_name)) + pagure.APP.logger.exception(err) + + + return flask.render_template( + 'edit_group.html', + group=group, + form=form, + ) + + @pagure.APP.route('/group///delete', methods=['POST']) @pagure.login_required def group_user_delete(user, group): From ce666617c2d06c5e24b0431627669babdab0a3a2 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Aug 11 2016 13:44:02 +0000 Subject: [PATCH 16/635] Adjust the unit-tests for the change in the group model --- diff --git a/tests/test_pagure_flask_api.py b/tests/test_pagure_flask_api.py index f3e627d..e22a2e4 100644 --- a/tests/test_pagure_flask_api.py +++ b/tests/test_pagure_flask_api.py @@ -133,6 +133,7 @@ class PagureFlaskApitests(tests.Modeltests): item = pagure.lib.model.PagureGroup( group_name='group1', group_type='user', + display_name='User group', user_id=1, # pingou ) self.session.add(item) @@ -140,6 +141,7 @@ class PagureFlaskApitests(tests.Modeltests): item = pagure.lib.model.PagureGroup( group_name='rel-eng', group_type='user', + display_name='Release engineering group', user_id=1, # pingou ) self.session.add(item) diff --git a/tests/test_pagure_flask_ui_groups.py b/tests/test_pagure_flask_ui_groups.py index 26e392d..efb2a9e 100644 --- a/tests/test_pagure_flask_ui_groups.py +++ b/tests/test_pagure_flask_ui_groups.py @@ -88,10 +88,12 @@ class PagureFlaskGroupstests(tests.Modeltests): self.assertEqual(output.status_code, 200) self.assertIn('

Create group

', output.data) self.assertEqual(output.data.count( - 'This field is required.'), 1) + 'This field is required.'), 3) data = { 'group_name': 'test_group', + 'display_name': 'Test Group', + 'description': 'This is a group for the tests', } # Missing CSRF @@ -130,6 +132,8 @@ class PagureFlaskGroupstests(tests.Modeltests): data = { 'group_name': 'test_admin_group', 'group_type': 'admin', + 'display_name': 'Test Admin Group', + 'description': 'This is another group for the tests', 'csrf_token': csrf_token, } diff --git a/tests/test_pagure_flask_ui_repo.py b/tests/test_pagure_flask_ui_repo.py index 8d20941..fb23dc9 100644 --- a/tests/test_pagure_flask_ui_repo.py +++ b/tests/test_pagure_flask_ui_repo.py @@ -190,6 +190,8 @@ class PagureFlaskRepotests(tests.Modeltests): self.session, group_name='foo', group_type='bar', + display_name='foo group', + description=None, user='pingou', is_admin=False, blacklist=pagure.APP.config['BLACKLISTED_GROUPS'], @@ -269,6 +271,8 @@ class PagureFlaskRepotests(tests.Modeltests): msg = pagure.lib.add_group( self.session, group_name='foo', + display_name='foo group', + description=None, group_type='bar', user='pingou', is_admin=False, @@ -313,7 +317,6 @@ class PagureFlaskRepotests(tests.Modeltests): self.assertIn( '\n Group added', output.data) - @patch('pagure.ui.repo.admin_session_timedout') def test_remove_user_when_user_mngt_off(self, ast): """ Test the remove_user endpoint when user management is turned @@ -469,6 +472,8 @@ class PagureFlaskRepotests(tests.Modeltests): session=self.session, group_name='testgrp', group_type='user', + display_name='testgrp group', + description=None, user='pingou', is_admin=False, blacklist=[], @@ -548,6 +553,8 @@ class PagureFlaskRepotests(tests.Modeltests): session=self.session, group_name='testgrp', group_type='user', + display_name='testgrp group', + description=None, user='pingou', is_admin=False, blacklist=[], diff --git a/tests/test_pagure_lib.py b/tests/test_pagure_lib.py index f233651..f566487 100644 --- a/tests/test_pagure_lib.py +++ b/tests/test_pagure_lib.py @@ -1895,6 +1895,8 @@ class PagureLibtests(tests.Modeltests): pagure.lib.add_group, self.session, group_name='foo', + display_name='foo group', + description=None, group_type='bar', user='pingou', is_admin=True, @@ -1910,6 +1912,8 @@ class PagureLibtests(tests.Modeltests): pagure.lib.add_group, self.session, group_name='foo', + display_name='foo group', + description=None, group_type='user', user='test', is_admin=False, @@ -1922,6 +1926,8 @@ class PagureLibtests(tests.Modeltests): msg = pagure.lib.add_group( self.session, group_name='foo', + display_name='foo group', + description=None, group_type='bar', user='pingou', is_admin=False, @@ -1940,6 +1946,8 @@ class PagureLibtests(tests.Modeltests): pagure.lib.add_group, self.session, group_name='foo', + display_name='foo group', + description=None, group_type='bar', user='pingou', is_admin=False, @@ -1952,6 +1960,8 @@ class PagureLibtests(tests.Modeltests): pagure.lib.add_group, self.session, group_name='forks', + display_name='foo group', + description=None, group_type='bar', user='pingou', is_admin=False, @@ -2072,6 +2082,8 @@ class PagureLibtests(tests.Modeltests): msg = pagure.lib.add_group( self.session, group_name='foo', + display_name='foo group', + description=None, group_type='bar', user='pingou', is_admin=False, @@ -2087,6 +2099,8 @@ class PagureLibtests(tests.Modeltests): msg = pagure.lib.add_group( self.session, group_name='bar', + display_name='bar group', + description=None, group_type='admin', user='pingou', is_admin=True, diff --git a/tests/test_pagure_lib_git.py b/tests/test_pagure_lib_git.py index fff7ac4..730e512 100644 --- a/tests/test_pagure_lib_git.py +++ b/tests/test_pagure_lib_git.py @@ -148,6 +148,8 @@ repo requests/forks/pingou/test3 msg = pagure.lib.add_group( self.session, group_name='sysadmin', + display_name='sysadmin group', + description=None, group_type='user', user='pingou', is_admin=False, @@ -158,6 +160,8 @@ repo requests/forks/pingou/test3 msg = pagure.lib.add_group( self.session, group_name='devs', + display_name='devs group', + description=None, group_type='user', user='pingou', is_admin=False, diff --git a/tests/test_pagure_lib_model.py b/tests/test_pagure_lib_model.py index ff08eb6..9e21ca4 100644 --- a/tests/test_pagure_lib_model.py +++ b/tests/test_pagure_lib_model.py @@ -115,6 +115,8 @@ class PagureLibModeltests(tests.Modeltests): """ Test the PagureGroup.__repr__ function of pagure.lib.model. """ item = pagure.lib.model.PagureGroup( group_name='admin', + display_name='admin group', + description='the local admin group', user_id=1, ) self.session.add(item) From 0148d9466baaefc94b98090164c6a7f305b11518 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Aug 11 2016 13:48:15 +0000 Subject: [PATCH 17/635] Add unit-tests around the edit_group feature --- diff --git a/tests/test_pagure_flask_ui_groups.py b/tests/test_pagure_flask_ui_groups.py index efb2a9e..6505cbc 100644 --- a/tests/test_pagure_flask_ui_groups.py +++ b/tests/test_pagure_flask_ui_groups.py @@ -152,6 +152,97 @@ class PagureFlaskGroupstests(tests.Modeltests): ' Groups 2', output.data) + def test_edit_group(self): + """ Test the edit_group endpoint. """ + + output = self.app.get('/group/test_group/edit') + self.assertEqual(output.status_code, 302) + + user = tests.FakeUser() + with tests.user_set(pagure.APP, user): + output = self.app.get('/group/test_group/edit') + self.assertEqual(output.status_code, 404) + self.assertIn('

Group not found

', output.data) + + self.test_add_group() + + user.username = 'foo' + with tests.user_set(pagure.APP, user): + output = self.app.get('/group/foo/edit') + self.assertEqual(output.status_code, 404) + self.assertIn('

Group not found

', output.data) + + output = self.app.get('/group/test_group/edit') + self.assertEqual(output.status_code, 200) + self.assertIn( + 'Edit group: test_group - Pagure', + output.data) + self.assertIn( + '
', + output.data) + self.assertIn( + '', output.data) + + csrf_token = output.data.split( + 'name="csrf_token" type="hidden" value="')[1].split('">')[0] + + # Missing CSRF + data = { + 'group_name': 'test_group', + 'display_name': 'Test Group edited', + 'description': 'This is a group for the tests edited', + } + + output = self.app.post( + '/group/test_group/edit', data=data, follow_redirects=True) + #print output.data + self.assertEqual(output.status_code, 200) + self.assertIn( + 'Edit group: test_group - Pagure', + output.data) + self.assertIn( + '', + output.data) + self.assertIn( + '', output.data) + + # User not allowed + data['csrf_token'] = csrf_token + + output = self.app.post( + '/group/test_group/edit', data=data, follow_redirects=True) + self.assertEqual(output.status_code, 200) + self.assertIn( + 'Group - Pagure', + output.data) + self.assertIn( + '\n You are not ' + 'allowed to edit this group', output.data) + self.assertIn( + ' ' + ' test_group', output.data) + + user.username = 'pingou' + with tests.user_set(pagure.APP, user): + # Invalid repo + output = self.app.post( + '/group/bar/edit', data=data, follow_redirects=True) + self.assertEqual(output.status_code, 404) + self.assertIn('

Group not found

', output.data) + + output = self.app.post( + '/group/test_group/edit', data=data, follow_redirects=True) + self.assertEqual(output.status_code, 200) + self.assertIn('Group - Pagure', output.data) + self.assertIn( + '' + '  test_group', output.data) + self.assertIn( + 'Group "Test Group edited" (test_group) edited', + output.data) + def test_group_delete(self): """ Test the group_delete endpoint. """ output = self.app.post('/group/foo/delete') diff --git a/tests/test_pagure_lib.py b/tests/test_pagure_lib.py index f566487..6952975 100644 --- a/tests/test_pagure_lib.py +++ b/tests/test_pagure_lib.py @@ -2228,6 +2228,71 @@ class PagureLibtests(tests.Modeltests): group = pagure.lib.search_groups(self.session, group_name='foo') self.assertEqual(len(group.users), 1) + def test_edit_group_info(self): + """ Test the edit_group_info method of pagure.lib. """ + self.test_add_group() + group = pagure.lib.search_groups(self.session, group_name='foo') + self.assertNotEqual(group, None) + self.assertEqual(group.group_name, 'foo') + + # Invalid new user + self.assertRaises( + pagure.exceptions.PagureException, + pagure.lib.edit_group_info, + self.session, + group=group, + display_name='edited name', + description=None, + user='foo', + is_admin=False, + ) + + # Invalid user + self.assertRaises( + pagure.exceptions.PagureException, + pagure.lib.edit_group_info, + self.session, + group=group, + display_name='edited name', + description=None, + user='foobar', + is_admin=False, + ) + + # User not allowed + self.assertRaises( + pagure.exceptions.PagureException, + pagure.lib.edit_group_info, + self.session, + group=group, + display_name='edited name', + description=None, + user='bar', + is_admin=False, + ) + + msg = pagure.lib.edit_group_info( + self.session, + group=group, + display_name='edited name', + description=None, + user='pingou', + is_admin=False, + ) + self.session.commit() + self.assertEqual(msg, 'Group "edited name" (foo) edited') + + msg = pagure.lib.edit_group_info( + self.session, + group=group, + display_name='edited name', + description=None, + user='pingou', + is_admin=False, + ) + self.session.commit() + self.assertEqual(msg, 'Nothing changed') + def test_add_group_to_project(self): """ Test the add_group_to_project method of pagure.lib. """ tests.create_projects(self.session) From 20d4054cfa7d8f3320481ca028ee0c8a795ff5d4 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Aug 11 2016 16:47:08 +0000 Subject: [PATCH 18/635] Drop the small red stars, they aren't used anywhere anymore --- diff --git a/pagure/forms.py b/pagure/forms.py index 8802c98..523d49f 100644 --- a/pagure/forms.py +++ b/pagure/forms.py @@ -362,13 +362,13 @@ class CommentForm(wtf.Form): class EditGroupForm(wtf.Form): """ Form to ask for a password change. """ display_name = wtforms.TextField( - 'Group name to display *', + 'Group name to display', [ wtforms.validators.Required(), ] ) description = wtforms.TextField( - 'Description *', + 'Description', [ wtforms.validators.Required(), ] From 305fe244db21080c80bba88d037304dcbbc3017c Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Aug 11 2016 16:47:25 +0000 Subject: [PATCH 19/635] Only notify if there is a project to be notified about --- diff --git a/pagure/lib/notify.py b/pagure/lib/notify.py index 885492d..890f6cd 100644 --- a/pagure/lib/notify.py +++ b/pagure/lib/notify.py @@ -52,7 +52,7 @@ def log(project, topic, msg, redis=None): # Send fedmsg notification (if fedmsg is there and set-up) fedmsg_publish(topic, msg) - if redis: + if redis and project: redis.publish( 'pagure.hook', json.dumps({ From 3b08c551cfa1d172ef51159fc85f11324aeea514 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Aug 11 2016 16:48:38 +0000 Subject: [PATCH 20/635] Make the edit_group template pretty! --- diff --git a/pagure/templates/edit_group.html b/pagure/templates/edit_group.html index 8013b60..a3bc517 100644 --- a/pagure/templates/edit_group.html +++ b/pagure/templates/edit_group.html @@ -1,32 +1,38 @@ {% extends "master.html" %} -{% from "_formhelper.html" import render_field_in_row %} +{% from "_formhelper.html" import render_bootstrap_field %} {% set tag = "groups" %} {% block title %}Edit group: {{ group.group_name }}{% endblock %} - {% block content %} - -

Edit group: {{ group.group_name }}

- -
- - - - {{ render_field_in_row(form.display_name) }} - {{ render_field_in_row(form.description) }} - {%- if admin %} - {{ render_field_in_row(form.group_type) }} - {%- endif %} -
-

- - - {{ form.csrf_token }} -

- -
+
+
+
+
+
+ Edit group: {{ group.group_name }} +
+
+
+ + {{ render_bootstrap_field( + form.display_name, + field_description="Name of the group that will be displayed in the UI") }} + {{ render_bootstrap_field( + form.description, + field_description="Small description of the group") }} +
+

+ + + {{ form.csrf_token }} +

+
+
+
+
+
+
{% endblock %} From 73e7fdf4855c3adb145d100a43eecf747e5c1664 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Aug 11 2016 16:48:49 +0000 Subject: [PATCH 21/635] Pre-fill the content of the form if the request was a GET This makes editing group's info so much easier --- diff --git a/pagure/ui/groups.py b/pagure/ui/groups.py index 877333d..66c67ab 100644 --- a/pagure/ui/groups.py +++ b/pagure/ui/groups.py @@ -162,7 +162,9 @@ def edit_group(group): pagure.APP.logger.debug( 'Could not edit group `%s`.' % (group.group_name)) pagure.APP.logger.exception(err) - + elif flask.request.method == 'GET': + form.display_name.data = group.display_name + form.description.data = group.description return flask.render_template( 'edit_group.html', From fa31fa963ae4da34f647eff4194188b7429e5f79 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Aug 11 2016 16:49:20 +0000 Subject: [PATCH 22/635] Fix the title of the page and add a button to edit the group's info --- diff --git a/pagure/templates/group_info.html b/pagure/templates/group_info.html index 5632e87..3dd82a8 100644 --- a/pagure/templates/group_info.html +++ b/pagure/templates/group_info.html @@ -1,7 +1,7 @@ {% extends "master.html" %} {% from "_render_repo.html" import render_repos_as_card %} -{% block title %}Group {{ group.name }}{% endblock %} +{% block title %}Group {{ group.group_name }}{% endblock %} {% set tag = "groups" %} {% from "_browseheader.html" import browse_header %} @@ -20,23 +20,33 @@

-  {{ group.group_name }} +  {{ + group.display_name }} {% if authenticated and (member or admin) and config.get('ENABLE_GROUP_MNGT') %} -
- {{ form.csrf_token }} - -
+
+ + + +
+ {% if admin %} + {% endif %} + {{ form.csrf_token }} + +
+
{% endif %}

+ {% if group.description %}

{{ group.description }}

{% endif %} created {{ group.created |humanize }} by {{ group.creator.fullname }} ({{ group.creator.user }})
From 29b32d5c5bd9acb8aaea7c9862883ed0762b9d4d Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Aug 11 2016 16:49:34 +0000 Subject: [PATCH 23/635] Show the display name when listing the groups, put the group name as title --- diff --git a/pagure/templates/group_list.html b/pagure/templates/group_list.html index 2747332..b75eed8 100644 --- a/pagure/templates/group_list.html +++ b/pagure/templates/group_list.html @@ -87,7 +87,9 @@
-
{{ group.group_name }}
+
+ {{ group.display_name }} +
Formed {{ group.created |humanize }} From 615c238a74bff869a64e2323f6c6f90c6f74f721 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Aug 11 2016 16:56:41 +0000 Subject: [PATCH 24/635] Adjust the unit-tests for the change in behavior --- diff --git a/tests/test_pagure_flask_ui_groups.py b/tests/test_pagure_flask_ui_groups.py index 6505cbc..8f11da5 100644 --- a/tests/test_pagure_flask_ui_groups.py +++ b/tests/test_pagure_flask_ui_groups.py @@ -181,8 +181,8 @@ class PagureFlaskGroupstests(tests.Modeltests): '
', output.data) self.assertIn( - '', output.data) + '', output.data) csrf_token = output.data.split( 'name="csrf_token" type="hidden" value="')[1].split('">')[0] @@ -205,8 +205,8 @@ class PagureFlaskGroupstests(tests.Modeltests): '', output.data) self.assertIn( - '', output.data) + '', output.data) # User not allowed data['csrf_token'] = csrf_token @@ -215,14 +215,14 @@ class PagureFlaskGroupstests(tests.Modeltests): '/group/test_group/edit', data=data, follow_redirects=True) self.assertEqual(output.status_code, 200) self.assertIn( - 'Group - Pagure', + 'Group test_group - Pagure', output.data) self.assertIn( '\n You are not ' 'allowed to edit this group', output.data) self.assertIn( ' ' - ' test_group', output.data) + ' Test Group', output.data) user.username = 'pingou' with tests.user_set(pagure.APP, user): @@ -235,10 +235,11 @@ class PagureFlaskGroupstests(tests.Modeltests): output = self.app.post( '/group/test_group/edit', data=data, follow_redirects=True) self.assertEqual(output.status_code, 200) - self.assertIn('Group - Pagure', output.data) self.assertIn( - '' - '  test_group', output.data) + 'Group test_group - Pagure', output.data) + self.assertIn( + ' ' + ' Test Group', output.data) self.assertIn( 'Group "Test Group edited" (test_group) edited', output.data) @@ -335,7 +336,7 @@ class PagureFlaskGroupstests(tests.Modeltests): self.assertEqual(output.status_code, 200) self.assertIn( '  ' - 'test_group', output.data) + 'Test Group', output.data) output = self.app.get('/group/test_admin_group') self.assertEqual(output.status_code, 404) @@ -349,7 +350,7 @@ class PagureFlaskGroupstests(tests.Modeltests): self.assertEqual(output.status_code, 200) self.assertIn( '  ' - 'test_admin_group', output.data) + 'Test Admin Group', output.data) self.assertEqual(output.data.count('  ' - 'test_admin_group', output.data) + 'Test Admin Group', output.data) self.assertEqual(output.data.count('  ' - 'test_admin_group', output.data) + 'Test Admin Group', output.data) self.assertEqual(output.data.count('  ' - 'test_admin_group', output.data) + 'Test Admin Group', output.data) self.assertEqual(output.data.count('  ' - 'test_group', output.data) + 'Test Group', output.data) self.assertEqual(output.data.count('  ' - 'test_group', output.data) + 'Test Group', output.data) self.assertEqual(output.data.count('  ' - 'test_group', output.data) + 'Test Group', output.data) self.assertEqual(output.data.count('  ' - 'test_group', output.data) + 'Test Group', output.data) self.assertEqual(output.data.count('  ' - 'test_group', output.data) + 'Test Group', output.data) self.assertEqual(output.data.count('  ' - 'test_group', output.data) + 'Test Group', output.data) self.assertEqual(output.data.count('  ' - 'test_group', output.data) + 'Test Group', output.data) self.assertEqual(output.data.count('

From 6a511596d1e567a399a031370499d550d60215c9 Mon Sep 17 00:00:00 2001 From: Vivek Anand Date: Aug 12 2016 21:38:11 +0000 Subject: [PATCH 28/635] alembic: display name and description columns in pagure_group --- diff --git a/alembic/versions/32d636cb5e00_display_name_in_groups.py b/alembic/versions/32d636cb5e00_display_name_in_groups.py new file mode 100644 index 0000000..b7a3b93 --- /dev/null +++ b/alembic/versions/32d636cb5e00_display_name_in_groups.py @@ -0,0 +1,53 @@ +"""display_name_in_groups + +Revision ID: 32d636cb5e00 +Revises: 43df5e588a87 +Create Date: 2016-08-13 02:54:27.199948 + +""" + +# revision identifiers, used by Alembic. +revision = '32d636cb5e00' +down_revision = '43df5e588a87' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + ''' Add attributes display name and description in PagureGroup ''' + op.add_column( + 'pagure_group', + sa.Column( + 'display_name', + sa.String(255), + nullable=True, + unique=True, + ) + ) + + op.execute('''UPDATE "pagure_group" SET display_name=group_name; ''') + + op.alter_column( + 'pagure_group', + column_name='display_name', + nullable=False, + existing_nullable=True + ) + + op.add_column( + 'pagure_group', + sa.Column( + 'description', + sa.String(255), + nullable=True, + ) + ) + + +def downgrade(): + ''' Remove attributes display name and description in PagureGroup ''' + + op.drop_column('pagure_group', 'display_name') + op.drop_column('pagure_group', 'description') + From 03a6d0b724bdabb83c9e984a5dca3147b24c5f44 Mon Sep 17 00:00:00 2001 From: Vivek Anand Date: Aug 12 2016 21:59:15 +0000 Subject: [PATCH 29/635] show groups when user mngt is turned off --- diff --git a/pagure/ui/groups.py b/pagure/ui/groups.py index 66c67ab..2671072 100644 --- a/pagure/ui/groups.py +++ b/pagure/ui/groups.py @@ -24,8 +24,6 @@ import pagure.lib.git @pagure.APP.route('/groups') def group_lists(): ''' List all the groups associated with all the projects. ''' - if not pagure.APP.config.get('ENABLE_USER_MNGT', True): - flask.abort(404) group_type = 'user' if pagure.is_admin(): @@ -55,7 +53,8 @@ def group_lists(): @pagure.APP.route('/group/', methods=['GET', 'POST']) def view_group(group): ''' Displays information about this group. ''' - if not pagure.APP.config.get('ENABLE_USER_MNGT', True): + if flask.request.method == 'POST' and \ + not pagure.APP.config.get('ENABLE_USER_MNGT', True): flask.abort(404) group_type = 'user' From 686fbd8983ce04cbf2a61bdca3933d25d05d0bbd Mon Sep 17 00:00:00 2001 From: Farhaan Bukhsh Date: Aug 16 2016 03:29:06 +0000 Subject: [PATCH 30/635] Fix description for separaters in project tags --- diff --git a/pagure/templates/settings.html b/pagure/templates/settings.html index 8e8b0c4..4f55331 100644 --- a/pagure/templates/settings.html +++ b/pagure/templates/settings.html @@ -36,7 +36,7 @@

- tags for project + tags for project (separated by commas)
+
+
+ My Watch List {{ watch_list | count }} +
+ {% for repo in watch_list %} +
+ {% if repo.is_fork %} + +
+  {{ repo.user.username }}/{{ repo.name }} +
+
+ {% else %} + +
+  {{ repo.name }} +
+
+ {% endif %} +
+ {% else %} +
+

No project in watch list

+
+ {% endfor %} +
diff --git a/pagure/ui/app.py b/pagure/ui/app.py index b8568bb..17475fb 100644 --- a/pagure/ui/app.py +++ b/pagure/ui/app.py @@ -116,12 +116,17 @@ def index_auth(): fork=True, count=True) + watch_list = pagure.lib.user_watch_list( + SESSION, + user=flask.g.fas_user.username) + return flask.render_template( 'index_auth.html', username=flask.g.fas_user.username, user=user, forks=forks, repos=repos, + watch_list=watch_list, repopage=repopage, forkpage=forkpage, repos_length=repos_length, diff --git a/tests/test_pagure_flask_ui_app.py b/tests/test_pagure_flask_ui_app.py index 1236f14..eb08845 100644 --- a/tests/test_pagure_flask_ui_app.py +++ b/tests/test_pagure_flask_ui_app.py @@ -91,7 +91,36 @@ class PagureFlaskApptests(tests.Modeltests): self.assertEqual( output.data.count('

No group found

'), 1) self.assertEqual( - output.data.count('
'), 3) + output.data.count('
'), 4) + + def test_watch_list(self): + ''' Test for watch list of a user ''' + + user = tests.FakeUser(username='pingou') + with tests.user_set(pagure.APP, user): + output = self.app.get('/') + self.assertIn( + '
You have no projects
', + output.data) + self.assertIn( + '

You have no forks

', + output.data) + self.assertIn( + '

No project in watch list

', + output.data) + + tests.create_projects(self.session) + + output = self.app.get('/') + self.assertIn( + 'My Projects 2', + output.data) + self.assertIn( + 'My Forks 0', + output.data) + self.assertIn( + 'My Watch List 2', + output.data) def test_view_users(self): """ Test the view_users endpoint. """ diff --git a/tests/test_pagure_lib.py b/tests/test_pagure_lib.py index 6952975..85fc630 100644 --- a/tests/test_pagure_lib.py +++ b/tests/test_pagure_lib.py @@ -2483,6 +2483,40 @@ class PagureLibtests(tests.Modeltests): self.assertFalse(watch) + def test_user_watch_list(self): + ''' test user watch list method of pagure.lib ''' + + tests.create_projects(self.session) + + # He should be watching + user = tests.FakeUser() + user.username = 'pingou' + watch_list_objs = pagure.lib.user_watch_list( + session=self.session, + user='pingou', + ) + watch_list = [obj.name for obj in watch_list_objs] + self.assertEqual(watch_list, ['test', 'test2']) + + # He isn't in the db, thus not watching anything + user.username = 'vivek' + watch_list_objs = pagure.lib.user_watch_list( + session=self.session, + user='vivek', + ) + watch_list = [obj.name for obj in watch_list_objs] + self.assertEqual(watch_list, []) + + # He shouldn't be watching anything + user.username = 'foo' + watch_list_objs = pagure.lib.user_watch_list( + session=self.session, + user='foo', + ) + watch_list = [obj.name for obj in watch_list_objs] + self.assertEqual(watch_list, []) + + if __name__ == '__main__': SUITE = unittest.TestLoader().loadTestsFromTestCase(PagureLibtests) unittest.TextTestRunner(verbosity=2).run(SUITE) From c99ccdea71a1817e82ddc948a33ebec17d3edc10 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Aug 29 2016 09:25:52 +0000 Subject: [PATCH 43/635] Pep8 fixes to the pagure.hooks --- diff --git a/pagure/hooks/files/pagure_block_unsigned.py b/pagure/hooks/files/pagure_block_unsigned.py index c9bee63..111b813 100755 --- a/pagure/hooks/files/pagure_block_unsigned.py +++ b/pagure/hooks/files/pagure_block_unsigned.py @@ -54,7 +54,7 @@ def run_as_pre_receive_hook(): print 'Processing commit: %s' % commit signed = False for line in pagure.lib.git.read_git_lines( - ['log', '--no-walk', commit], abspath): + ['log', '--no-walk', commit], abspath): if line.lower().strip().startswith('signed-off-by'): signed = True break diff --git a/pagure/hooks/mail.py b/pagure/hooks/mail.py index cf67419..941e54c 100644 --- a/pagure/hooks/mail.py +++ b/pagure/hooks/mail.py @@ -66,8 +66,9 @@ class Mail(BaseHook): ''' Mail hooks. ''' name = 'Mail' - description = 'Generate notification emails for pushes to a git repository. '\ - 'This hook sends emails describing changes introduced by pushes to a git repository.' + description = 'Generate notification emails for pushes to a git '\ + 'repository. This hook sends emails describing changes introduced '\ + 'by pushes to a git repository.' form = MailForm db_object = MailTable backref = 'mail_hook' diff --git a/pagure/hooks/pagure_ci.py b/pagure/hooks/pagure_ci.py index 12c2576..3998477 100644 --- a/pagure/hooks/pagure_ci.py +++ b/pagure/hooks/pagure_ci.py @@ -60,7 +60,8 @@ class PagureCITable(BASE): tmpl = """ -{% if repo | hasattr('ci_hook') and repo.ci_hook and repo.ci_hook[0].pagure_ci_token %} +{% if repo | hasattr('ci_hook') and repo.ci_hook and + repo.ci_hook[0].pagure_ci_token %} The token to be used by jenkins to trigger the build is:
diff --git a/pagure/hooks/pagure_request_hook.py b/pagure/hooks/pagure_request_hook.py
index 7182531..b65f060 100644
--- a/pagure/hooks/pagure_request_hook.py
+++ b/pagure/hooks/pagure_request_hook.py
@@ -64,7 +64,8 @@ class PagureRequestHook(BaseHook):
 
     name = 'Pagure requests'
     description = 'Pagure specific hook to update pull-requests stored '\
-        'in the database based on the information pushed in the requests git repository.'
+        'in the database based on the information pushed in the requests '\
+        'git repository.'
     form = PagureRequestsForm
     db_object = PagureRequestsTable
     backref = 'pagure_hook_requests'

From 2c3a3fa373df1fae09097b85fe3741025616fb0c Mon Sep 17 00:00:00 2001
From: Pierre-Yves Chibon 
Date: Aug 29 2016 09:25:52 +0000
Subject: [PATCH 44/635] Drop un-used imports (thanks pylint)


---

diff --git a/pagure/hooks/fedmsg.py b/pagure/hooks/fedmsg.py
index 69cf3f5..e7ed0bb 100644
--- a/pagure/hooks/fedmsg.py
+++ b/pagure/hooks/fedmsg.py
@@ -1,18 +1,16 @@
 # -*- coding: utf-8 -*-
 
 """
- (c) 2015 - Copyright Red Hat Inc
+ (c) 2015-2016 - Copyright Red Hat Inc
 
  Authors:
    Pierre-Yves Chibon 
 
 """
 
-import os
-
+import flask_wtf as wtf
 import sqlalchemy as sa
 import wtforms
-from flask.ext import wtf
 from sqlalchemy.orm import relation
 from sqlalchemy.orm import backref
 
diff --git a/pagure/hooks/irc.py b/pagure/hooks/irc.py
index c9ff494..f1d26a0 100644
--- a/pagure/hooks/irc.py
+++ b/pagure/hooks/irc.py
@@ -1,7 +1,7 @@
 # -*- coding: utf-8 -*-
 
 """
- (c) 2014 - Copyright Red Hat Inc
+ (c) 2014-2016 - Copyright Red Hat Inc
 
  Authors:
    Pierre-Yves Chibon 
@@ -10,10 +10,10 @@
 
 import os
 
+import flask_wtf as wtf
 import sqlalchemy as sa
 import pygit2
 import wtforms
-from flask.ext import wtf
 from sqlalchemy.orm import relation
 from sqlalchemy.orm import backref
 
diff --git a/pagure/hooks/mail.py b/pagure/hooks/mail.py
index 941e54c..667538d 100644
--- a/pagure/hooks/mail.py
+++ b/pagure/hooks/mail.py
@@ -8,12 +8,10 @@
 
 """
 
-import os
-
+import flask_wtf as wtf
 import sqlalchemy as sa
 import pygit2
-import wtforms
-from flask.ext import wtf
+import wtforms\
 from sqlalchemy.orm import relation
 from sqlalchemy.orm import backref
 
diff --git a/pagure/hooks/pagure_ci.py b/pagure/hooks/pagure_ci.py
index 3998477..fa33272 100644
--- a/pagure/hooks/pagure_ci.py
+++ b/pagure/hooks/pagure_ci.py
@@ -8,18 +8,16 @@
 
 """
 
-import os
-
+import flask_wtf as wtf
 import sqlalchemy as sa
 import wtforms
-from flask.ext import wtf
 from sqlalchemy.orm import relation
 from sqlalchemy.orm import backref
 
 import pagure.lib
 from pagure.hooks import BaseHook, RequiredIf
 from pagure.lib.model import BASE, Project
-from pagure import get_repo_path, SESSION, APP
+from pagure import SESSION, APP
 
 
 class PagureCITable(BASE):
diff --git a/pagure/hooks/pagure_force_commit.py b/pagure/hooks/pagure_force_commit.py
index b90f047..2ef2d3b 100644
--- a/pagure/hooks/pagure_force_commit.py
+++ b/pagure/hooks/pagure_force_commit.py
@@ -8,18 +8,16 @@
 
 """
 
-import os
-
+import flask_wtf as wtf
 import sqlalchemy as sa
 import pygit2
 import wtforms
-from flask.ext import wtf
 from sqlalchemy.orm import relation
 from sqlalchemy.orm import backref
 
 from pagure.hooks import BaseHook, RequiredIf
 from pagure.lib.model import BASE, Project
-from pagure import APP, get_repo_path
+from pagure import get_repo_path
 
 
 class PagureForceCommitTable(BASE):
diff --git a/pagure/hooks/pagure_hook.py b/pagure/hooks/pagure_hook.py
index 3f35f99..541cae5 100644
--- a/pagure/hooks/pagure_hook.py
+++ b/pagure/hooks/pagure_hook.py
@@ -10,10 +10,9 @@
 
 import os
 
+import flask_wtf as wtf
 import sqlalchemy as sa
-import pygit2
 import wtforms
-from flask.ext import wtf
 from sqlalchemy.orm import relation
 from sqlalchemy.orm import backref
 
diff --git a/pagure/hooks/pagure_request_hook.py b/pagure/hooks/pagure_request_hook.py
index b65f060..ee0529c 100644
--- a/pagure/hooks/pagure_request_hook.py
+++ b/pagure/hooks/pagure_request_hook.py
@@ -11,10 +11,9 @@
 import os
 
 import flask
+import flask_wtf as wtf
 import sqlalchemy as sa
-import pygit2
 import wtforms
-from flask.ext import wtf
 from sqlalchemy.orm import relation
 from sqlalchemy.orm import backref
 
diff --git a/pagure/hooks/pagure_ticket_hook.py b/pagure/hooks/pagure_ticket_hook.py
index 83b1a8b..d2379c1 100644
--- a/pagure/hooks/pagure_ticket_hook.py
+++ b/pagure/hooks/pagure_ticket_hook.py
@@ -1,7 +1,7 @@
 # -*- coding: utf-8 -*-
 
 """
- (c) 2014 - Copyright Red Hat Inc
+ (c) 2014-2016 - Copyright Red Hat Inc
 
  Authors:
    Pierre-Yves Chibon 
@@ -11,10 +11,9 @@
 import os
 
 import flask
+import flask_wtf as wtf
 import sqlalchemy as sa
-import pygit2
 import wtforms
-from flask.ext import wtf
 from sqlalchemy.orm import relation
 from sqlalchemy.orm import backref
 
diff --git a/pagure/hooks/pagure_unsigned_commits.py b/pagure/hooks/pagure_unsigned_commits.py
index 745abb7..117139f 100644
--- a/pagure/hooks/pagure_unsigned_commits.py
+++ b/pagure/hooks/pagure_unsigned_commits.py
@@ -8,18 +8,15 @@
 
 """
 
-import os
-
+import flask_wtf as wtf
 import sqlalchemy as sa
-import pygit2
 import wtforms
-from flask.ext import wtf
 from sqlalchemy.orm import relation
 from sqlalchemy.orm import backref
 
-from pagure.hooks import BaseHook, RequiredIf
+from pagure.hooks import BaseHook
 from pagure.lib.model import BASE, Project
-from pagure import APP, get_repo_path
+from pagure import get_repo_path
 
 
 class PagureUnsignedCommitTable(BASE):
diff --git a/pagure/hooks/rtd.py b/pagure/hooks/rtd.py
index 0a0e586..5f072b2 100644
--- a/pagure/hooks/rtd.py
+++ b/pagure/hooks/rtd.py
@@ -8,18 +8,15 @@
 
 """
 
-import os
-
+import flask_wtf as wtf
 import sqlalchemy as sa
-import pygit2
 import wtforms
-from flask.ext import wtf
 from sqlalchemy.orm import relation
 from sqlalchemy.orm import backref
 
 from pagure.hooks import BaseHook, RequiredIf
 from pagure.lib.model import BASE, Project
-from pagure import APP, get_repo_path
+from pagure import get_repo_path
 
 
 class RtdTable(BASE):

From 362b15ce56df4b641a84fab0f2afe3dc29bdd07d Mon Sep 17 00:00:00 2001
From: Pierre-Yves Chibon 
Date: Aug 29 2016 09:25:52 +0000
Subject: [PATCH 45/635] Fix last imports on flask-wtf


---

diff --git a/pagure/forms.py b/pagure/forms.py
index f3a2652..e23598b 100644
--- a/pagure/forms.py
+++ b/pagure/forms.py
@@ -1,7 +1,7 @@
 # -*- coding: utf-8 -*-
 
 """
- (c) 2014 - Copyright Red Hat Inc
+ (c) 2014-2016 - Copyright Red Hat Inc
 
  Authors:
    Pierre-Yves Chibon 
@@ -9,8 +9,9 @@
 """
 
 import re
-from flask.ext import wtf
+
 import flask
+import flask_wtf as wtf
 import wtforms
 import tempfile
 # pylint: disable=R0903,W0232,E1002
diff --git a/pagure/hooks/mail.py b/pagure/hooks/mail.py
index 667538d..f6e70c4 100644
--- a/pagure/hooks/mail.py
+++ b/pagure/hooks/mail.py
@@ -11,7 +11,7 @@
 import flask_wtf as wtf
 import sqlalchemy as sa
 import pygit2
-import wtforms\
+import wtforms
 from sqlalchemy.orm import relation
 from sqlalchemy.orm import backref
 
diff --git a/pagure/login_forms.py b/pagure/login_forms.py
index 0819762..dd0d631 100644
--- a/pagure/login_forms.py
+++ b/pagure/login_forms.py
@@ -1,7 +1,7 @@
 # -*- coding: utf-8 -*-
 
 """
- (c) 2014 - Copyright Red Hat Inc
+ (c) 2014-2016 - Copyright Red Hat Inc
 
  Authors:
    Pierre-Yves Chibon 
@@ -19,7 +19,7 @@
 # pylint: disable=W0232
 
 
-from flask.ext import wtf
+import flask_wtf as wtf
 import wtforms
 
 from pagure.forms import ConfirmationForm

From 0074a54ba5e79f9f539ff2bd8dbf799ae2505f53 Mon Sep 17 00:00:00 2001
From: Pierre-Yves Chibon 
Date: Aug 29 2016 09:25:52 +0000
Subject: [PATCH 46/635] Small style fixes in the model


---

diff --git a/pagure/lib/model.py b/pagure/lib/model.py
index 6b8b1ea..75622ea 100644
--- a/pagure/lib/model.py
+++ b/pagure/lib/model.py
@@ -70,7 +70,7 @@ def create_tables(db_url, alembic_ini=None, acls=None, debug=False):
     if db_url.startswith('sqlite:'):
         # Ignore the warning about con_record
         # pylint: disable=W0613
-        def _fk_pragma_on_connect(dbapi_con, con_record):  # pragma: no cover
+        def _fk_pragma_on_connect(dbapi_con, _):  # pragma: no cover
             ''' Tries to enforce referential constraints on sqlite. '''
             dbapi_con.execute('pragma foreign_keys=ON')
         sa.event.listen(engine, 'connect', _fk_pragma_on_connect)
@@ -1144,6 +1144,8 @@ class PullRequestComment(BASE):
         return self.pull_request
 
     def to_json(self, public=False):
+        ''' Return a dict representation of the pull-request comment. '''
+
         return {
             'id': self.id,
             'commit': self.commit_id,

From d1e05379c38564cb821ec395843f1a5a32e8b780 Mon Sep 17 00:00:00 2001
From: Pierre-Yves Chibon 
Date: Aug 29 2016 09:25:52 +0000
Subject: [PATCH 47/635] Some more code and style fixes in pagure.lib


---

diff --git a/pagure/lib/__init__.py b/pagure/lib/__init__.py
index 0bd36f6..03d674d 100644
--- a/pagure/lib/__init__.py
+++ b/pagure/lib/__init__.py
@@ -50,10 +50,10 @@ from pagure.lib import model
 REDIS = None
 PAGURE_CI = None
 
-def set_redis(host, port, db):
+def set_redis(host, port, dbname):
     """ Set the redis connection with the specified information. """
     global REDIS
-    pool = redis.ConnectionPool(host=host, port=port, db=db)
+    pool = redis.ConnectionPool(host=host, port=port, db=dbname)
     REDIS = redis.StrictRedis(connection_pool=pool)
 
 def set_pagure_ci(services):
@@ -178,6 +178,10 @@ def search_user(session, username=None, email=None, token=None, pattern=None):
 
 
 def create_user_ssh_keys_on_disk(user, gitolite_keydir):
+    ''' Create the ssh keys for the user on the specific folder.
+
+    This is the method allowing to have multiple ssh keys per user.
+    '''
     if gitolite_keydir:
         # First remove any old keyfiles for the user
         # Assumption: we populated the keydir. This means that files
@@ -1055,13 +1059,14 @@ def new_project(session, user, name, blacklist, allowed_prefix,
             userobj.default_email.encode('utf-8') if six.PY2 else userobj.fullname
         )
         content = u"# %s\n\n%s" % (name, description)
-        f = open(os.path.join(temp_gitrepo.workdir,"README.md"), 'wb')
-        f.write(content.encode('utf-8'))
-        f.close()
+        with open(os.path.join(temp_gitrepo.workdir, "README.md"), 'wb') \
+                as stream:
+            stream.write(content.encode('utf-8'))
         temp_gitrepo.index.add_all()
         temp_gitrepo.index.write()
         tree = temp_gitrepo.index.write_tree()
-        temp_gitrepo.create_commit('HEAD', author,author, 'Added the README', tree, [])
+        temp_gitrepo.create_commit(
+            'HEAD', author,author, 'Added the README', tree, [])
         pygit2.clone_repository(temp_gitrepo_path, gitrepo, bare=True)
         shutil.rmtree(temp_gitrepo_path)
 
@@ -1421,7 +1426,7 @@ def fork_project(session, user, repo, gitfolder,
     # Create the git-daemin-export-ok file on the clone
     http_clone_file = os.path.join(forkreponame, 'git-daemon-export-ok')
     if not os.path.exists(http_clone_file):
-        with open(http_clone_file, 'w') as stream:
+        with open(http_clone_file, 'w'):
             pass
 
     docrepo = os.path.join(docfolder, project.path)
@@ -2365,7 +2370,7 @@ def resend_pending_email(session, userobj, email):
             'This email address has already been confirmed'
         )
 
-    pending_email.token=pagure.lib.login.id_generator(40)
+    pending_email.token = pagure.lib.login.id_generator(40)
     session.add(pending_email)
     session.flush()
 

From f9f1323c1de28e4556039c759dc577d47f7a5463 Mon Sep 17 00:00:00 2001
From: Pierre-Yves Chibon 
Date: Aug 29 2016 09:25:52 +0000
Subject: [PATCH 48/635] Some more code style fixes in the pagure.lib module


---

diff --git a/pagure/lib/__init__.py b/pagure/lib/__init__.py
index 03d674d..5908de3 100644
--- a/pagure/lib/__init__.py
+++ b/pagure/lib/__init__.py
@@ -44,7 +44,14 @@ import pagure.lib.notify
 import pagure.pfmarkdown
 from pagure.lib import model
 
+# too-many-branches
+# pylint: disable=R0912
+# too-many-arguments
 # pylint: disable=R0913
+# too-many-locals
+# pylint: disable=R0914
+# too-many-statements
+# pylint: disable=R0915
 
 
 REDIS = None
@@ -1059,14 +1066,14 @@ def new_project(session, user, name, blacklist, allowed_prefix,
             userobj.default_email.encode('utf-8') if six.PY2 else userobj.fullname
         )
         content = u"# %s\n\n%s" % (name, description)
-        with open(os.path.join(temp_gitrepo.workdir, "README.md"), 'wb') \
-                as stream:
+        readme_file = os.path.join(temp_gitrepo.workdir, "README.md")
+        with open(readme_file, 'wb') as stream:
             stream.write(content.encode('utf-8'))
         temp_gitrepo.index.add_all()
         temp_gitrepo.index.write()
         tree = temp_gitrepo.index.write_tree()
         temp_gitrepo.create_commit(
-            'HEAD', author,author, 'Added the README', tree, [])
+            'HEAD', author, author, 'Added the README', tree, [])
         pygit2.clone_repository(temp_gitrepo_path, gitrepo, bare=True)
         shutil.rmtree(temp_gitrepo_path)
 
@@ -1417,11 +1424,12 @@ def fork_project(session, user, repo, gitfolder,
     frepo = pygit2.clone_repository(reponame, forkreponame, bare=True)
     # Clone all the branches as well
     for branch in frepo.listall_branches(pygit2.GIT_BRANCH_REMOTE):
-        br = frepo.lookup_branch(branch, pygit2.GIT_BRANCH_REMOTE)
-        name = br.branch_name.replace(br.remote_name, '')[1:]
+        branch_obj = frepo.lookup_branch(branch, pygit2.GIT_BRANCH_REMOTE)
+        name = branch_obj.branch_name.replace(
+            branch_obj.remote_name, '')[1:]
         if name in frepo.listall_branches(pygit2.GIT_BRANCH_LOCAL):
             continue
-        frepo.create_branch(name, frepo.get(br.target.hex))
+        frepo.create_branch(name, frepo.get(branch_obj.target.hex))
 
     # Create the git-daemin-export-ok file on the clone
     http_clone_file = os.path.join(forkreponame, 'git-daemon-export-ok')
@@ -1489,7 +1497,7 @@ def search_projects(
                 model.User.id == model.Project.user_id,
             )
         )
-        q2 = session.query(
+        sub_q2 = session.query(
             model.Project.id
         ).filter(
             # User got commit right
@@ -1499,7 +1507,7 @@ def search_projects(
                 model.ProjectUser.project_id == model.Project.id
             )
         )
-        q3 = session.query(
+        sub_q3 = session.query(
             model.Project.id
         ).filter(
             # User created a group that has commit right
@@ -1511,7 +1519,7 @@ def search_projects(
                 model.Project.id == model.ProjectGroup.project_id,
             )
         )
-        q4 = session.query(
+        sub_q4 = session.query(
             model.Project.id
         ).filter(
             # User is part of a group that has commit right
@@ -1525,7 +1533,7 @@ def search_projects(
             )
         )
 
-        projects = projects.union(q2).union(q3).union(q4)
+        projects = projects.union(sub_q2).union(sub_q3).union(sub_q4)
 
     if fork is not None:
         if fork is True:
@@ -1706,7 +1714,7 @@ def search_issues(
                 ytags.append(tag)
 
         if ytags:
-            q2 = session.query(
+            sub_q2 = session.query(
                 sqlalchemy.distinct(model.Issue.uid)
             ).filter(
                 model.Issue.project_id == repo.id
@@ -1716,7 +1724,7 @@ def search_issues(
                 model.TagIssue.tag.in_(ytags)
             )
         if notags:
-            q3 = session.query(
+            sub_q3 = session.query(
                 sqlalchemy.distinct(model.Issue.uid)
             ).filter(
                 model.Issue.project_id == repo.id
@@ -1727,11 +1735,11 @@ def search_issues(
             )
         # Adjust the main query based on the parameters specified
         if ytags and not notags:
-            query = query.filter(model.Issue.uid.in_(q2))
+            query = query.filter(model.Issue.uid.in_(sub_q2))
         elif not ytags and notags:
-            query = query.filter(~model.Issue.uid.in_(q3))
+            query = query.filter(~model.Issue.uid.in_(sub_q3))
         elif ytags and notags:
-            final_set = set(q2.all()) - set(q3.all())
+            final_set = set(sub_q2.all()) - set(sub_q3.all())
             if final_set:
                 query = query.filter(model.Issue.uid.in_(list(final_set)))
 
@@ -2733,10 +2741,10 @@ def add_token_to_user(session, project, acls, username):
 def text2markdown(text, extended=True):
     """ Simple text to html converter using the markdown library.
     """
-    md = markdown.Markdown(safe_mode="escape")
+    md_processor = markdown.Markdown(safe_mode="escape")
     if extended:
         # Install our markdown modifications
-        md = markdown.Markdown(extensions=['pagure.pfmarkdown'])
+        md_processor = markdown.Markdown(extensions=['pagure.pfmarkdown'])
 
     if text:
         # Hack to allow blockquotes to be marked by ~~~
@@ -2749,7 +2757,7 @@ def text2markdown(text, extended=True):
             if indent:
                 line = '    %s' % line
             ntext.append(line)
-        return clean_input(md.convert('\n'.join(ntext)))
+        return clean_input(md_processor.convert('\n'.join(ntext)))
 
     return ''
 
@@ -2759,8 +2767,8 @@ def filter_img_src(name, value):
     if name in ('alt', 'height', 'width', 'class'):
         return True
     if name == 'src':
-        p = urlparse.urlparse(value)
-        return (not p.netloc) or p.netloc == urlparse.urlparse(
+        parsed = urlparse.urlparse(value)
+        return (not parsed.netloc) or parsed.netloc == urlparse.urlparse(
             pagure.APP.config['APP_URL']).netloc
     return False
 
@@ -2816,7 +2824,7 @@ def get_pull_request_of_user(session, username):
             model.User.id == model.Project.user_id,
         )
     )
-    q2 = session.query(
+    sub_q2 = session.query(
         model.Project.id
     ).filter(
         # User got commit right
@@ -2826,7 +2834,7 @@ def get_pull_request_of_user(session, username):
             model.ProjectUser.project_id == model.Project.id
         )
     )
-    q3 = session.query(
+    sub_q3 = session.query(
         model.Project.id
     ).filter(
         # User created a group that has commit right
@@ -2838,7 +2846,7 @@ def get_pull_request_of_user(session, username):
             model.Project.id == model.ProjectGroup.project_id,
         )
     )
-    q4 = session.query(
+    sub_q4 = session.query(
         model.Project.id
     ).filter(
         # User is part of a group that has commit right
@@ -2852,7 +2860,7 @@ def get_pull_request_of_user(session, username):
         )
     )
 
-    projects = projects.union(q2).union(q3).union(q4)
+    projects = projects.union(sub_q2).union(sub_q3).union(sub_q4)
 
     query = session.query(
         model.PullRequest

From 341785d61b48784a7bcd2979f24e1986a32c1be0 Mon Sep 17 00:00:00 2001
From: Pierre-Yves Chibon 
Date: Aug 29 2016 09:25:52 +0000
Subject: [PATCH 49/635] Fix copyright year in pagure.lib and style fix pagure.lib.link


---

diff --git a/pagure/lib/__init__.py b/pagure/lib/__init__.py
index 5908de3..177d054 100644
--- a/pagure/lib/__init__.py
+++ b/pagure/lib/__init__.py
@@ -1,7 +1,7 @@
 # -*- coding: utf-8 -*-
 
 """
- (c) 2014-2015 - Copyright Red Hat Inc
+ (c) 2014-2016 - Copyright Red Hat Inc
 
  Authors:
    Pierre-Yves Chibon 
diff --git a/pagure/lib/link.py b/pagure/lib/link.py
index 6cdf46d..8749439 100644
--- a/pagure/lib/link.py
+++ b/pagure/lib/link.py
@@ -1,13 +1,16 @@
 # -*- coding: utf-8 -*-
 
 """
- (c) 2015 - Copyright Red Hat Inc
+ (c) 2015-2016 - Copyright Red Hat Inc
 
  Authors:
    Pierre-Yves Chibon 
 
 """
 
+# too-many-arguments
+# pylint: disable=R0913
+
 
 import re
 

From 5b20c87638a8bb46f98faf336bee62ca9e729cd9 Mon Sep 17 00:00:00 2001
From: Pierre-Yves Chibon 
Date: Aug 29 2016 09:25:52 +0000
Subject: [PATCH 50/635] Style fix to pagure.lib.lib_ci


---

diff --git a/pagure/lib/lib_ci.py b/pagure/lib/lib_ci.py
index d2ea609..5d77e70 100644
--- a/pagure/lib/lib_ci.py
+++ b/pagure/lib/lib_ci.py
@@ -10,14 +10,8 @@
 
 """
 
-import json
-import logging
-
-import requests
-
-from sqlalchemy.orm import scoped_session, sessionmaker
-from sqlalchemy.exc import SQLAlchemyError
-from sqlalchemy import create_engine
+# too-many-locals
+# pylint: disable=R0914
 
 import pagure.exceptions
 import pagure.lib
@@ -80,7 +74,7 @@ def process_jenkins_build(session, project, build_id, requestfolder):
 
     comment, percent = BUILD_STATS[result]
 
-    message = pagure.lib.add_pull_request_flag(
+    pagure.lib.add_pull_request_flag(
         session,
         request=request,
         username=project.ci_hook[0].ci_type,

From c70df4d232940f49d228284f348a0d6712203882 Mon Sep 17 00:00:00 2001
From: Pierre-Yves Chibon 
Date: Aug 29 2016 09:25:52 +0000
Subject: [PATCH 51/635] Style fixes to pagure.lib.git


---

diff --git a/pagure/lib/git.py b/pagure/lib/git.py
index ceba03a..1b720d4 100644
--- a/pagure/lib/git.py
+++ b/pagure/lib/git.py
@@ -1,7 +1,7 @@
 # -*- coding: utf-8 -*-
 
 """
- (c) 2015 - Copyright Red Hat Inc
+ (c) 2015-2016 - Copyright Red Hat Inc
 
  Authors:
    Pierre-Yves Chibon 
@@ -16,7 +16,6 @@ import os
 import shutil
 import subprocess
 import tempfile
-import re
 
 import pygit2
 import werkzeug
@@ -30,7 +29,19 @@ import pagure.lib.notify
 from pagure.lib import model
 from pagure.lib.repo import PagureRepo
 
-# pylint: disable=R0913,E1101,R0914
+
+# too-many-branches
+# pylint: disable=R0912
+# too-many-arguments
+# pylint: disable=R0913
+# too-many-locals
+# pylint: disable=R0914
+# too-many-statements
+# pylint: disable=R0915
+# no-member
+# pylint: disable=E1101
+# C0302
+# pylint: disable=C0302
 
 
 def commit_to_patch(repo_obj, commits):
@@ -189,11 +200,11 @@ def update_git(obj, repo, repofolder):
     # Retrieve the list of files that changed
     diff = new_repo.diff()
     files = []
-    for p in diff:
-        if hasattr(p, 'new_file_path'):
-            files.append(p.new_file_path)
-        elif hasattr(p, 'delta'):
-            files.append(p.delta.new_file.path)
+    for patch in diff:
+        if hasattr(patch, 'new_file_path'):
+            files.append(patch.new_file_path)
+        elif hasattr(patch, 'delta'):
+            files.append(patch.delta.new_file.path)
 
     # Add the changes to the index
     if added:
@@ -380,6 +391,8 @@ def get_project_from_json(
                 user=user.username)
 
         else:
+            gitfolder = os.path.join(
+                gitfolder, 'forks', user.username) if parent else gitfolder
             pagure.lib.new_project(
                 session,
                 user=user.username,
@@ -387,8 +400,8 @@ def get_project_from_json(
                 description=jsondata.get('description'),
                 parent_id=parent.id if parent else None,
                 blacklist=pagure.APP.config.get('BLACKLISTED_PROJECTS', []),
-                gitfolder=os.path.join(gitfolder, 'forks', user.username)
-                    if parent else gitfolder,
+                allowed_prefix=pagure.APP.config.get('ALLOWED_PREFIX', []),
+                gitfolder=gitfolder,
                 docfolder=docfolder,
                 ticketfolder=ticketfolder,
                 requestfolder=requestfolder,
@@ -738,11 +751,11 @@ def update_file_in_git(
     # Retrieve the list of files that changed
     diff = new_repo.diff()
     files = []
-    for p in diff:
-        if hasattr(p, 'new_file_path'):
-            files.append(p.new_file_path)
-        elif hasattr(p, 'delta'):
-            files.append(p.delta.new_file.path)
+    for patch in diff:
+        if hasattr(patch, 'new_file_path'):
+            files.append(patch.new_file_path)
+        elif hasattr(patch, 'delta'):
+            files.append(patch.delta.new_file.path)
 
     # Add the changes to the index
     added = False
@@ -1176,11 +1189,14 @@ def diff_pull_request(
                         if i.oid.hex == request.commit_stop:
                             break
                         new_commits_count = new_commits_count + 1
-                        commenttext = '%s * %s\n' % (commenttext, i.message.strip().split('\n')[0])
+                        commenttext = '%s * %s\n' % (
+                            commenttext, i.message.strip().split('\n')[0])
                     if new_commits_count == 1:
-                        commenttext = "**%d new commit added**\n\n%s" % (new_commits_count, commenttext)
+                        commenttext = "**%d new commit added**\n\n%s" % (
+                            new_commits_count, commenttext)
                     else:
-                        commenttext = "**%d new commits added**\n\n%s" % (new_commits_count, commenttext)
+                        commenttext = "**%d new commits added**\n\n%s" % (
+                            new_commits_count, commenttext)
                 if request.commit_start and \
                         request.commit_start != first_commit.oid.hex:
                     commenttext = 'rebased'
@@ -1272,7 +1288,7 @@ def get_git_tags_objects(project):
 
             tags[commit_time] = {
                 "object": repo_obj[repo_obj.lookup_reference(tag).target],
-                "tagname": tag.replace("refs/tags/",""),
+                "tagname": tag.replace("refs/tags/", ""),
                 "date": commit_time,
                 "objecttype": objecttype,
                 "head_msg": None,

From c3b072a5411af30634aad47aca420f5826f606a7 Mon Sep 17 00:00:00 2001
From: Pierre-Yves Chibon 
Date: Aug 29 2016 09:25:52 +0000
Subject: [PATCH 52/635] Be consistent in where we're placing the pylint instructions


---

diff --git a/pagure/lib/__init__.py b/pagure/lib/__init__.py
index 177d054..e8d7ddd 100644
--- a/pagure/lib/__init__.py
+++ b/pagure/lib/__init__.py
@@ -8,6 +8,16 @@
 
 """
 
+# too-many-branches
+# pylint: disable=R0912
+# too-many-arguments
+# pylint: disable=R0913
+# too-many-locals
+# pylint: disable=R0914
+# too-many-statements
+# pylint: disable=R0915
+
+
 try:
     import simplejson as json
 except ImportError:
@@ -44,15 +54,6 @@ import pagure.lib.notify
 import pagure.pfmarkdown
 from pagure.lib import model
 
-# too-many-branches
-# pylint: disable=R0912
-# too-many-arguments
-# pylint: disable=R0913
-# too-many-locals
-# pylint: disable=R0914
-# too-many-statements
-# pylint: disable=R0915
-
 
 REDIS = None
 PAGURE_CI = None

From 6123e5cec3937ffce8606bb995e201e9d1647b8c Mon Sep 17 00:00:00 2001
From: Pierre-Yves Chibon 
Date: Aug 29 2016 09:25:52 +0000
Subject: [PATCH 53/635] Style fixes on pagure.lib.model


---

diff --git a/pagure/lib/model.py b/pagure/lib/model.py
index 75622ea..25d7752 100644
--- a/pagure/lib/model.py
+++ b/pagure/lib/model.py
@@ -1,7 +1,7 @@
 # -*- coding: utf-8 -*-
 
 """
- (c) 2014-2015 - Copyright Red Hat Inc
+ (c) 2014-2016 - Copyright Red Hat Inc
 
  Authors:
    Pierre-Yves Chibon 
@@ -40,7 +40,16 @@ BASE = declarative_base(metadata=MetaData(naming_convention=CONVENTION))
 
 ERROR_LOG = logging.getLogger('pagure.model')
 
-# pylint: disable=C0103,R0903,W0232,E1101
+# invalid-name  - hit w/ all the id field we use
+# pylint: disable=C0103
+# too-few-public-methods
+# pylint: disable=R0903
+# no-init
+# pylint: disable=W0232
+# no-member
+# pylint: disable=E1101
+# too-many-lines
+# pylint: disable=C0302
 
 
 def create_tables(db_url, alembic_ini=None, acls=None, debug=False):
@@ -731,10 +740,15 @@ class IssueComment(BASE):
             order_by="IssueComment.date_created"
         ),
     )
-    user = relation('User', foreign_keys=[user_id],
-                    remote_side=[User.id], backref='comment_issues')
-    editor = relation('User', foreign_keys=[editor_id],
-                         remote_side=[User.id])
+    user = relation(
+        'User',
+        foreign_keys=[user_id],
+        remote_side=[User.id],
+        backref='comment_issues')
+    editor = relation(
+        'User',
+        foreign_keys=[editor_id],
+        remote_side=[User.id])
 
     @property
     def mail_id(self):
@@ -1122,13 +1136,16 @@ class PullRequestComment(BASE):
     pull_request = relation(
         'PullRequest',
         backref=backref(
-            'comments', cascade="delete, delete-orphan",
+            'comments',
+            cascade="delete, delete-orphan",
             order_by="PullRequestComment.date_created"
         ),
         foreign_keys=[pull_request_uid],
         remote_side=[PullRequest.uid])
-    editor = relation('User', foreign_keys=[editor_id],
-                         remote_side=[User.id])
+    editor = relation(
+        'User',
+        foreign_keys=[editor_id],
+        remote_side=[User.id])
 
     @property
     def mail_id(self):

From f5708bdd46546520245c60a9d11bcbd18df2cba6 Mon Sep 17 00:00:00 2001
From: Pierre-Yves Chibon 
Date: Aug 29 2016 09:25:52 +0000
Subject: [PATCH 54/635] Final style fixes to pagure.lib


---

diff --git a/pagure/lib/__init__.py b/pagure/lib/__init__.py
index e8d7ddd..2bd4ca6 100644
--- a/pagure/lib/__init__.py
+++ b/pagure/lib/__init__.py
@@ -16,6 +16,8 @@
 # pylint: disable=R0914
 # too-many-statements
 # pylint: disable=R0915
+# too-many-lines
+# pylint: disable=C0302
 
 
 try:
diff --git a/pagure/lib/notify.py b/pagure/lib/notify.py
index 890f6cd..6c789ce 100644
--- a/pagure/lib/notify.py
+++ b/pagure/lib/notify.py
@@ -1,7 +1,7 @@
 # -*- coding: utf-8 -*-
 
 """
- (c) 2014-2015 - Copyright Red Hat Inc
+ (c) 2014-2016 - Copyright Red Hat Inc
 
  Authors:
    Pierre-Yves Chibon 
@@ -9,6 +9,12 @@
 pagure notifications.
 """
 
+# too-many-branches
+# pylint: disable=R0912
+# too-many-arguments
+# pylint: disable=R0913
+
+
 import datetime
 import hashlib
 import json
@@ -19,8 +25,6 @@ import time
 import warnings
 
 import flask
-import requests
-import six
 import pagure
 
 from email.mime.text import MIMEText
@@ -66,8 +70,8 @@ def _add_mentioned_users(emails, comment):
     ''' Check the comment to see if an user is mentioned in it and if
     so add this user to the list of people to notify.
     '''
-    MENTION_RE = r'@(\w+)'
-    for username in re.findall(MENTION_RE, comment):
+    mentio_re = r'@(\w+)'
+    for username in re.findall(mentio_re, comment):
         user = pagure.lib.search_user(pagure.SESSION, username=username)
         if user:
             emails.add(user.default_email)
diff --git a/pagure/lib/repo.py b/pagure/lib/repo.py
index 1b77330..ef3c11b 100644
--- a/pagure/lib/repo.py
+++ b/pagure/lib/repo.py
@@ -76,7 +76,7 @@ class PagureRepo(pygit2.Repository):
                 else:
                     pagure.LOG.debug(
                         'Un-expected merge result: %s' % (
-                        pygit2.GIT_MERGE_ANALYSIS_NORMAL))
+                            pygit2.GIT_MERGE_ANALYSIS_NORMAL))
                     raise AssertionError('Unknown merge analysis result')
 
     def run_hook(self, old, new, ref, username):

From 602511951c6eb287204fee160157eaf4960dc828 Mon Sep 17 00:00:00 2001
From: Pierre-Yves Chibon 
Date: Aug 29 2016 09:25:52 +0000
Subject: [PATCH 55/635] pep8 and pylint fixes to pagure.api


---

diff --git a/pagure/api/__init__.py b/pagure/api/__init__.py
index 1e1d55e..cfadddd 100644
--- a/pagure/api/__init__.py
+++ b/pagure/api/__init__.py
@@ -1,7 +1,7 @@
 # -*- coding: utf-8 -*-
 
 """
- (c) 2015 - Copyright Red Hat Inc
+ (c) 2015-2016 - Copyright Red Hat Inc
 
  Authors:
    Pierre-Yves Chibon 
@@ -10,6 +10,15 @@ API namespace version 0.
 
 """
 
+# invalid-name
+# pylint: disable=C0103
+# too-few-public-methods
+# pylint: disable=R0903
+# no-member
+# pylint: disable=E1101
+# too-many-locals
+# pylint: disable=R0914
+
 import codecs
 import functools
 import os
@@ -34,8 +43,8 @@ def preload_docs(endpoint):
 
     here = os.path.dirname(os.path.abspath(__file__))
     fname = os.path.join(here, '..', 'doc', endpoint + '.rst')
-    with codecs.open(fname, 'r', 'utf-8') as f:
-        rst = f.read()
+    with codecs.open(fname, 'r', 'utf-8') as stream:
+        rst = stream.read()
 
     rst = modify_rst(rst)
     api_docs = docutils.examples.html_body(rst)
@@ -123,17 +132,17 @@ def api_login_required(acls=None):
     API endpoint.
     '''
 
-    def decorator(fn):
+    def decorator(function):
         ''' The decorator of the function '''
 
-        @functools.wraps(fn)
+        @functools.wraps(function)
         def decorated_function(*args, **kwargs):
             ''' Actually does the job with the arguments provided. '''
 
             response = check_api_acls(acls)
             if response:
                 return response
-            return fn(*args, **kwargs)
+            return function(*args, **kwargs)
 
         return decorated_function
 
@@ -145,17 +154,17 @@ def api_login_optional(acls=None):
     API endpoint.
     '''
 
-    def decorator(fn):
+    def decorator(function):
         ''' The decorator of the function '''
 
-        @functools.wraps(fn)
+        @functools.wraps(function)
         def decorated_function(*args, **kwargs):
             ''' Actually does the job with the arguments provided. '''
 
             response = check_api_acls(acls, optional=True)
             if response:
                 return response
-            return fn(*args, **kwargs)
+            return function(*args, **kwargs)
 
         return decorated_function
 
@@ -167,27 +176,28 @@ def api_method(function):
 
     @functools.wraps(function)
     def wrapper(*args, **kwargs):
+        ''' Actually does the job with the arguments provided. '''
         try:
             result = function(*args, **kwargs)
-        except APIError as e:
-            if e.error_code in [APIERROR.EDBERROR]:
-                APP.logger.exception(e)
+        except APIError as err:
+            if err.error_code in [APIERROR.EDBERROR]:
+                APP.logger.exception(err)
 
-            if e.error_code in [APIERROR.ENOCODE]:
+            if err.error_code in [APIERROR.ENOCODE]:
                 response = flask.jsonify(
                     {
-                        'error': e.error,
-                        'error_code': e.error_code.name
+                        'error': err.error,
+                        'error_code': err.error_code.name
                     }
                 )
             else:
                 response = flask.jsonify(
                     {
-                        'error': e.error_code.value,
-                        'error_code': e.error_code.name,
+                        'error': err.error_code.value,
+                        'error_code': err.error_code.name,
                     }
                 )
-            response.status_code = e.status_code
+            response.status_code = err.status_code
         else:
             response = result
 
@@ -274,13 +284,13 @@ def api_users():
     return flask.jsonify(
         {
             'total_users': len(users),
-            'users': [user.username for user in users],
+            'users': [usr.username for usr in users],
             'mention': [{
-                'username': user.username,
-                'name': user.fullname,
-                'image': pagure.lib.avatar_url_from_openid(user.default_email,
+                'username': usr.username,
+                'name': usr.fullname,
+                'image': pagure.lib.avatar_url_from_openid(usr.default_email,
                                                            size=16)
-            } for user in users]
+            } for usr in users]
         }
     )
 
@@ -437,10 +447,10 @@ def api():
         issues.append(load_doc(issue.api_view_issue_comment))
         issues.append(load_doc(issue.api_comment_issue))
 
-    ci = []
+    ci_doc = []
     if pagure.APP.config.get('PAGURE_CI_SERVICES', True):
         if 'jenkins' in pagure.APP.config['PAGURE_CI_SERVICES']:
-            ci.append(load_doc(jenkins.jenkins_ci_notification))
+            ci_doc.append(load_doc(jenkins.jenkins_ci_notification))
 
     api_pull_request_views_doc = load_doc(fork.api_pull_request_views)
     api_pull_request_view_doc = load_doc(fork.api_pull_request_view)
@@ -491,7 +501,7 @@ def api():
             api_view_user_doc,
             api_groups_doc,
         ],
-        ci=ci,
+        ci=ci_doc,
         extras=extras,
     )
 
diff --git a/pagure/api/ci/jenkins.py b/pagure/api/ci/jenkins.py
index 74673a4..3b30872 100644
--- a/pagure/api/ci/jenkins.py
+++ b/pagure/api/ci/jenkins.py
@@ -12,7 +12,6 @@ import flask
 
 from cryptography.hazmat.primitives import constant_time
 from kitchen.text.converters import to_bytes
-from sqlalchemy.exc import SQLAlchemyError
 
 import pagure
 import pagure.exceptions
@@ -22,9 +21,10 @@ from pagure import APP, SESSION
 from pagure.api import API, APIERROR
 
 
-
-@API.route('/ci/jenkins///build-finished', methods=['POST'])
-@API.route('/ci/jenkins/forks////build-finished', methods=['POST'])
+@API.route('/ci/jenkins///build-finished',
+           methods=['POST'])
+@API.route('/ci/jenkins/forks///'
+           '/build-finished', methods=['POST'])
 def jenkins_ci_notification(repo, pagure_ci_token, username=None):
     """
     Jenkins Build Notification
@@ -40,20 +40,22 @@ def jenkins_ci_notification(repo, pagure_ci_token, username=None):
 
     project = pagure.lib.get_project(SESSION, repo, user=username)
     if repo is None:
-        flask.abort(404, 'Project not found')
+        raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOPROJECT)
 
     if not constant_time.bytes_eq(
-          to_bytes(pagure_ci_token),
-          to_bytes(project.ci_hook[0].pagure_ci_token)):
-        return ('Token mismatch', 401)
+            to_bytes(pagure_ci_token),
+            to_bytes(project.ci_hook[0].pagure_ci_token)):
+        raise pagure.exceptions.APIError(401, error_code=APIERROR.EINVALIDTOK)
 
     data = flask.request.get_json()
     if not data:
-        flask.abort(400, "Bad Request: No JSON retrived")
+        APP.logger.debug("Bad Request: No JSON retrived")
+        raise pagure.exceptions.APIError(400, error_code=APIERROR.EINVALIDREQ)
 
     build_id = data.get('build', {}).get('number')
     if not build_id:
-        flask.abort(400, "Bad Request: No build ID retrived")
+        APP.logger.debug("Bad Request: No build ID retrived")
+        raise pagure.exceptions.APIError(400, error_code=APIERROR.EINVALIDREQ)
 
     try:
         lib_ci.process_jenkins_build(
@@ -64,7 +66,8 @@ def jenkins_ci_notification(repo, pagure_ci_token, username=None):
         )
     except pagure.exceptions.PagureException as err:
         APP.logger.error('Error processing jenkins notification', exc_info=err)
-        flask.abort(400, "Bad Request: %s" % err)
+        raise pagure.exceptions.APIError(
+            400, error_code=APIERROR.ENOCODE, error=str(err))
 
     APP.logger.info('Successfully proccessed jenkins notification')
     return ('', 204)
diff --git a/pagure/api/issue.py b/pagure/api/issue.py
index 8738a59..706f222 100644
--- a/pagure/api/issue.py
+++ b/pagure/api/issue.py
@@ -460,7 +460,8 @@ def api_view_issue_comment(repo, issueid, commentid, username=None):
 
 
 @API.route('//issue//status', methods=['POST'])
-@API.route('/fork///issue//status', methods=['POST'])
+@API.route(
+    '/fork///issue//status', methods=['POST'])
 @api_login_required(acls=['issue_change_status'])
 @api_method
 def api_change_status_issue(repo, issueid, username=None):
@@ -555,7 +556,8 @@ def api_change_status_issue(repo, issueid, username=None):
 
 
 @API.route('//issue//comment', methods=['POST'])
-@API.route('/fork///issue//comment', methods=['POST'])
+@API.route(
+    '/fork///issue//comment', methods=['POST'])
 @api_login_required(acls=['issue_comment'])
 @api_method
 def api_comment_issue(repo, issueid, username=None):
@@ -645,7 +647,8 @@ def api_comment_issue(repo, issueid, username=None):
 
 
 @API.route('//issue//assign', methods=['POST'])
-@API.route('/fork///issue//assign', methods=['POST'])
+@API.route(
+    '/fork///issue//assign', methods=['POST'])
 @api_login_required(acls=['issue_assign'])
 @api_method
 def api_assign_issue(repo, issueid, username=None):
diff --git a/pagure/api/user.py b/pagure/api/user.py
index 65c8664..9280052 100644
--- a/pagure/api/user.py
+++ b/pagure/api/user.py
@@ -1,7 +1,7 @@
 # -*- coding: utf-8 -*-
 
 """
- (c) 2015 - Copyright Red Hat Inc
+ (c) 2015-2016 - Copyright Red Hat Inc
 
  Authors:
    Pierre-Yves Chibon 
@@ -13,7 +13,7 @@ import flask
 import pagure
 import pagure.exceptions
 import pagure.lib
-from pagure import APP, SESSION
+from pagure import SESSION
 from pagure.api import API, api_method, APIERROR
 
 
@@ -87,10 +87,6 @@ def api_view_user(username):
     except ValueError:
         forkpage = 1
 
-    limit = APP.config['ITEM_PER_PAGE']
-    repo_start = limit * (repopage - 1)
-    fork_start = limit * (forkpage - 1)
-
     repos = pagure.lib.search_projects(
         SESSION,
         username=username,

From fa85ee2605dfdd58dc3467bc0340c3b3dbf2355c Mon Sep 17 00:00:00 2001
From: Pierre-Yves Chibon 
Date: Aug 29 2016 09:25:52 +0000
Subject: [PATCH 56/635] pep8 and pylint fixes to the pagure top module


---

diff --git a/pagure/__init__.py b/pagure/__init__.py
index ceb0a06..f9873b5 100644
--- a/pagure/__init__.py
+++ b/pagure/__init__.py
@@ -56,7 +56,7 @@ if APP.config.get('THEME_TEMPLATE_FOLDER', False):
     # That's what we do here
     template_folder = APP.config['THEME_TEMPLATE_FOLDER']
     if template_folder[0] != '/':
-        template_folder= os.path.join(
+        template_folder = os.path.join(
             APP.root_path, APP.template_folder, template_folder)
     import jinja2
     # Jinja looks for the template in the order of the folders specified
@@ -70,7 +70,7 @@ if APP.config.get('THEME_TEMPLATE_FOLDER', False):
 if APP.config.get('THEME_STATIC_FOLDER', False):
     static_folder = APP.config['THEME_STATIC_FOLDER']
     if static_folder[0] != '/':
-        static_folder= os.path.join(
+        static_folder = os.path.join(
             APP.root_path, 'static', static_folder)
     # Unlike templates, to serve static files from multiples folders we
     # need flask-multistatic
@@ -87,7 +87,7 @@ class RepoConverter(BaseConverter):
     :param map: the :class:`Map`.
     """
     regex = r'[^/]*(/[^/]+)?'
-    #weight = 200
+    # weight = 200
 
 
 APP.url_map.converters['repo'] = RepoConverter
@@ -418,7 +418,8 @@ def auth_login():  # pragma: no cover
         if not APP.config.get('ENABLE_GROUP_MNGT', False):
             groups = [
                 group.group_name
-                for group in pagure.lib.search_groups(SESSION, group_type='user')
+                for group in pagure.lib.search_groups(
+                    SESSION, group_type='user')
             ]
         groups = set(groups).union(admins)
         return FAS.login(return_url=return_point, groups=groups)
diff --git a/pagure/default_config.py b/pagure/default_config.py
index d9a497c..246d8df 100644
--- a/pagure/default_config.py
+++ b/pagure/default_config.py
@@ -221,8 +221,10 @@ ACLS = {
 }
 
 # Bootstrap URLS
-BOOTSTRAP_URLS_CSS = 'https://apps.fedoraproject.org/global/fedora-bootstrap-1.0.1/fedora-bootstrap.css'
-BOOTSTRAP_URLS_JS = 'https://apps.fedoraproject.org/global/fedora-bootstrap-1.0.1/fedora-bootstrap.js'
+BOOTSTRAP_URLS_CSS = 'https://apps.fedoraproject.org/global/' \
+    'fedora-bootstrap-1.0.1/fedora-bootstrap.css'
+BOOTSTRAP_URLS_JS = 'https://apps.fedoraproject.org/global/' \
+    'fedora-bootstrap-1.0.1/fedora-bootstrap.js'
 
 # List of the type of CI service supported by this pagure instance
 PAGURE_CI_SERVICES = []
diff --git a/pagure/docs_server.py b/pagure/docs_server.py
index dad385e..d2b5263 100644
--- a/pagure/docs_server.py
+++ b/pagure/docs_server.py
@@ -135,7 +135,6 @@ def view_docs(repo, username=None, filename=None):
 
     repo_obj = pygit2.Repository(reponame)
 
-
     if not repo_obj.is_empty:
         commit = repo_obj[repo_obj.head.target]
     else:
diff --git a/pagure/forms.py b/pagure/forms.py
index e23598b..cff4e58 100644
--- a/pagure/forms.py
+++ b/pagure/forms.py
@@ -30,14 +30,15 @@ def file_virus_validator(form, field):
         return
     from pyclamd import ClamdUnixSocket
 
-    if not field.name in flask.request.files or \
+    if field.name not in flask.request.files or \
             flask.request.files[field.name].filename == '':
         # If no file was uploaded, this field is correct
         return
     uploaded = flask.request.files[field.name]
     clam = ClamdUnixSocket()
     if not clam.ping():
-        raise wtforms.ValidationError('Unable to communicate with virus scanner')
+        raise wtforms.ValidationError(
+            'Unable to communicate with virus scanner')
     results = clam.scan_stream(uploaded.stream.read())
     if results is None:
         uploaded.stream.seek(0)
@@ -457,6 +458,7 @@ class DefaultBranchForm(wtf.Form):
                 (branch, branch) for branch in kwargs['branches']
             ]
 
+
 class EditCommentForm(wtf.Form):
     """ Form to verify that comment is not empty
     """

From 0653e692061c1b6d64c470b61d42eb6236b0c720 Mon Sep 17 00:00:00 2001
From: Pierre-Yves Chibon 
Date: Aug 29 2016 09:25:52 +0000
Subject: [PATCH 57/635] pep8 fixes to pagure.login_forms


---

diff --git a/pagure/login_forms.py b/pagure/login_forms.py
index dd0d631..c5e5a38 100644
--- a/pagure/login_forms.py
+++ b/pagure/login_forms.py
@@ -29,7 +29,8 @@ def same_password(form, field):
     ''' Check if the data in the field is the same as in the password field.
     '''
     if field.data != form.password.data:
-        raise wtforms.validators.ValidationError('Both password fields should be equal')
+        raise wtforms.validators.ValidationError(
+            'Both password fields should be equal')
 
 
 class LostPasswordForm(wtf.Form):
@@ -87,6 +88,7 @@ class NewUserForm(wtf.Form):
         [wtforms.validators.Required(), same_password]
     )
 
+
 class ChangePasswordForm(wtf.Form):
     """ Form to reset one's password in the local database. """
     old_password = wtforms.PasswordField(

From 2b091df9f78b8fd80873e290d4c90d0ab0a90c77 Mon Sep 17 00:00:00 2001
From: Pierre-Yves Chibon 
Date: Aug 29 2016 09:25:52 +0000
Subject: [PATCH 58/635] pep8 fixes to pagure.internal


---

diff --git a/pagure/internal/__init__.py b/pagure/internal/__init__.py
index 6185d77..46e8506 100644
--- a/pagure/internal/__init__.py
+++ b/pagure/internal/__init__.py
@@ -332,8 +332,7 @@ def get_pull_request_ready_branch():
 
 
 @PV.route('//issue/template', methods=['POST'])
-@PV.route('/fork///issue/template',
-           methods=['POST'])
+@PV.route('/fork///issue/template', methods=['POST'])
 def get_ticket_template(repo, username=None):
     """ Return the template asked for the specified project
     """
@@ -387,7 +386,7 @@ def get_ticket_template(repo, username=None):
                 bail_on_tree=True)
             if content_file:
                 content, _ = pagure.doc_utils.convert_readme(
-                        content_file.data, 'md')
+                    content_file.data, 'md')
     if content:
         response = flask.jsonify({
             'code': 'OK',

From b13b92d17d57666ec6f8d6e9cfcc4ae74d533002 Mon Sep 17 00:00:00 2001
From: Pierre-Yves Chibon 
Date: Aug 29 2016 09:25:52 +0000
Subject: [PATCH 59/635] Start pep8 fixes to pagure.lib


---

diff --git a/pagure/lib/__init__.py b/pagure/lib/__init__.py
index 2bd4ca6..522281d 100644
--- a/pagure/lib/__init__.py
+++ b/pagure/lib/__init__.py
@@ -60,12 +60,14 @@ from pagure.lib import model
 REDIS = None
 PAGURE_CI = None
 
+
 def set_redis(host, port, dbname):
     """ Set the redis connection with the specified information. """
     global REDIS
     pool = redis.ConnectionPool(host=host, port=port, db=dbname)
     REDIS = redis.StrictRedis(connection_pool=pool)
 
+
 def set_pagure_ci(services):
     """ Set the list of CI services supported by this pagure instance. """
     global PAGURE_CI
@@ -872,7 +874,8 @@ def add_pull_request_comment(session, request, commit, tree_id, filename,
             'comment_id': pr_comment.id,
             'avatar_url': avatar_url_from_openid(
                 pr_comment.user.default_email, size=16),
-            'comment_date': pr_comment.date_created.strftime('%Y-%m-%d %H:%M:%S'),
+            'comment_date': pr_comment.date_created.strftime(
+                '%Y-%m-%d %H:%M:%S'),
             'commit_id': commit,
             'filename': filename,
             'line': row,
@@ -955,7 +958,8 @@ def edit_comment(session, parent, comment, user,
                 'comment_editor': user_obj.user,
                 'avatar_url': avatar_url_from_openid(
                     comment.user.default_email, size=16),
-                'comment_date': comment.edited_on.strftime('%Y-%m-%d %H:%M:%S'),
+                'comment_date': comment.edited_on.strftime(
+                    '%Y-%m-%d %H:%M:%S'),
             }))
 
     return "Comment updated"
@@ -1065,8 +1069,10 @@ def new_project(session, user, name, blacklist, allowed_prefix,
         temp_gitrepo_path = tempfile.mkdtemp(prefix='pagure-')
         temp_gitrepo = pygit2.init_repository(temp_gitrepo_path, bare=False)
         author = pygit2.Signature(
-            userobj.fullname.encode('utf-8') if six.PY2 else userobj.fullname,
-            userobj.default_email.encode('utf-8') if six.PY2 else userobj.fullname
+            userobj.fullname.encode('utf-8')
+            if six.PY2 else userobj.fullname,
+            userobj.default_email.encode('utf-8')
+            if six.PY2 else userobj.fullname
         )
         content = u"# %s\n\n%s" % (name, description)
         readme_file = os.path.join(temp_gitrepo.workdir, "README.md")
@@ -1414,7 +1420,7 @@ def fork_project(session, user, repo, gitfolder,
         hook_token=pagure.lib.login.id_generator(40)
     )
 
-    #disable issues, PRs in the fork by default
+    # disable issues, PRs in the fork by default
     default_repo_settings = project.settings
     default_repo_settings['issue_tracker'] = False
     default_repo_settings['pull_requests'] = False
@@ -2875,6 +2881,7 @@ def get_pull_request_of_user(session, username):
 
     return query.all()
 
+
 def update_watch_status(session, project, user, watch):
     ''' Update the user status for watching a project.
     '''
diff --git a/pagure/lib/git.py b/pagure/lib/git.py
index 1b720d4..90ef234 100644
--- a/pagure/lib/git.py
+++ b/pagure/lib/git.py
@@ -1127,7 +1127,6 @@ def merge_pull_request(
             shutil.rmtree(newpath)
             return 'MERGE'
 
-
     # Update status
     pagure.lib.close_pull_request(
         session, request, username,
diff --git a/pagure/lib/lib_ci.py b/pagure/lib/lib_ci.py
index 5d77e70..f976737 100644
--- a/pagure/lib/lib_ci.py
+++ b/pagure/lib/lib_ci.py
@@ -45,7 +45,8 @@ def process_jenkins_build(session, project, build_id, requestfolder):
     import jenkins
     # Jenkins Base URL
     jenk = jenkins.Jenkins(project.ci_hook[0].ci_url.split('/job/')[0])
-    jenkins_name = project.ci_hook[0].ci_url.split('/job/', 1)[1].split('/', 1)[0]
+    jenkins_name = project.ci_hook[0].ci_url.split(
+        '/job/', 1)[1].split('/', 1)[0]
     build_info = jenk.get_build_info(jenkins_name, build_id)
     result = build_info['result']
     url = build_info['url']

From f610bfc5e374319ac37ebc4b783f6730d696d1c0 Mon Sep 17 00:00:00 2001
From: Pierre-Yves Chibon 
Date: Aug 29 2016 09:25:52 +0000
Subject: [PATCH 60/635] Finish pep8 and pylint fixes to pagure.lib


---

diff --git a/pagure/lib/link.py b/pagure/lib/link.py
index 8749439..5236478 100644
--- a/pagure/lib/link.py
+++ b/pagure/lib/link.py
@@ -19,19 +19,25 @@ import pagure.exceptions
 
 FIXES = [
     re.compile(r'(?:.*\s+)?fixe?[sd]?:?\s*?#(\d+)', re.I),
-    re.compile(r'(?:.*\s+)?fixe?[sd]?:?\s*?https?://.*/(\w+)/(?:issue|pull-request)/(\d+)', re.I),
+    re.compile(
+        r'(?:.*\s+)?fixe?[sd]?:?\s*?https?://.*/(\w+)'
+        '/(?: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+)/(?:issue|pull-request)/(\d+)', re.I),
+    re.compile(
+        r'(?:.*\s+)?merge?[sd]?:?\s*?https?://.*/(\w+)'
+        '/(?: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+)/(?:issue|pull-request)/(\d+)', re.I),
+    re.compile(
+        r'(?:.*\s+)?close?[sd]?:?\s*?https?://.*/(\w+)'
+        '/(?:issue|pull-request)/(\d+)', re.I),
 ]
 
 RELATES = [
     re.compile(r'(?:.*\s+)?relate[sd]?:?\s*?(?:to)?\s*?#(\d+)', re.I),
     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),
+        r'(?:.*\s+)?relate[sd]?:?\s*?(?:to)?\s*?'
+        'https?://.*/(\w+)/issue/(\d+)', re.I),
 ]
 
 
diff --git a/pagure/lib/model.py b/pagure/lib/model.py
index 25d7752..ba2b0da 100644
--- a/pagure/lib/model.py
+++ b/pagure/lib/model.py
@@ -106,7 +106,8 @@ def create_default_status(session, acls=None):
     """ Insert the defaults status in the status tables.
     """
 
-    for status in ['Open', 'Invalid', 'Insufficient data', 'Fixed', 'Duplicate']:
+    statuses = ['Open', 'Invalid', 'Insufficient data', 'Fixed', 'Duplicate']
+    for status in statuses:
         ticket_stat = StatusIssue(status=status)
         session.add(ticket_stat)
         try:
@@ -478,7 +479,6 @@ class Project(BASE):
             Issue.private == False
         ).count()
 
-
     def to_json(self, public=False, api=False):
         ''' Return a representation of the project as JSON.
         '''
@@ -633,7 +633,10 @@ class Issue(BASE):
     def user_comments(self):
         ''' Return user comments only, filter it from notifications
         '''
-        return [comment for comment in self.comments if not comment.notification]
+        return [
+            comment
+            for comment in self.comments
+            if not comment.notification]
 
     def to_json(self, public=False, with_comments=True):
         ''' Returns a dictionary representation of the issue.
@@ -773,8 +776,10 @@ class IssueComment(BASE):
             'parent': self.parent_id,
             'date_created': self.date_created.strftime('%s'),
             'user': self.user.to_json(public=public),
-            'edited_on': self.edited_on.strftime('%s') if self.edited_on else None,
-            'editor': self.editor.to_json(public=public) if self.editor_id else None,
+            'edited_on': self.edited_on.strftime('%s')
+            if self.edited_on else None,
+            'editor': self.editor.to_json(public=public)
+            if self.editor_id else None,
             'notification': self.notification,
         }
         return output
@@ -1033,7 +1038,10 @@ class PullRequest(BASE):
     def user_comments(self):
         ''' Return user comments only, filter it from notifications
         '''
-        return [comment for comment in self.comments if not comment.notification]
+        return [
+            comment
+            for comment in self.comments
+            if not comment.notification]
 
     def to_json(self, public=False, api=False, with_comments=True):
         ''' Returns a dictionnary representation of the pull-request.
@@ -1173,8 +1181,10 @@ class PullRequestComment(BASE):
             'parent': self.parent_id,
             'date_created': self.date_created.strftime('%s'),
             'user': self.user.to_json(public=public),
-            'edited_on': self.edited_on.strftime('%s') if self.edited_on else None,
-            'editor': self.editor.to_json(public=public) if self.editor_id else None,
+            'edited_on': self.edited_on.strftime('%s')
+            if self.edited_on else None,
+            'editor': self.editor.to_json(public=public)
+            if self.editor_id else None,
             'notification': self.notification,
         }
 
@@ -1349,10 +1359,8 @@ class ProjectGroup(BASE):
         primary_key=True)
 
     # Constraints
-    __table_args__ = (
-        sa.UniqueConstraint(
-            'project_id', 'group_id'),
-    )
+    __table_args__ = (sa.UniqueConstraint('project_id', 'group_id'),)
+
 
 class Watcher(BASE):
     """ Stores the user of a projects.
diff --git a/pagure/lib/notify.py b/pagure/lib/notify.py
index 6c789ce..3b85667 100644
--- a/pagure/lib/notify.py
+++ b/pagure/lib/notify.py
@@ -194,14 +194,17 @@ def send_email(text, subject, to_mail,
         subject_tag = 'Pagure'
 
     if pagure.APP.config['SMTP_SSL']:
-        smtp = smtplib.SMTP_SSL(pagure.APP.config['SMTP_SERVER'], pagure.APP.config['SMTP_PORT'])
+        smtp = smtplib.SMTP_SSL(
+            pagure.APP.config['SMTP_SERVER'], pagure.APP.config['SMTP_PORT'])
     else:
-        smtp = smtplib.SMTP(pagure.APP.config['SMTP_SERVER'], pagure.APP.config['SMTP_PORT'])
+        smtp = smtplib.SMTP(
+            pagure.APP.config['SMTP_SERVER'], pagure.APP.config['SMTP_PORT'])
 
     for mailto in to_mail.split(','):
         msg = MIMEText(text.encode('utf-8'), 'plain', 'utf-8')
         msg['Subject'] = '[%s] %s' % (subject_tag, subject)
-        from_email = pagure.APP.config.get('FROM_EMAIL', 'pagure@fedoraproject.org')
+        from_email = pagure.APP.config.get(
+            'FROM_EMAIL', 'pagure@fedoraproject.org')
         msg['From'] = from_email
 
         if mail_id:
@@ -224,8 +227,12 @@ def send_email(text, subject, to_mail,
             mhash.hexdigest(),
             pagure.APP.config['DOMAIN_EMAIL_NOTIFICATIONS'])
         try:
-            if pagure.APP.config['SMTP_USERNAME'] and pagure.APP.config['SMTP_PASSWORD']:
-                smtp.login(pagure.APP.config['SMTP_USERNAME'], pagure.APP.config['SMTP_PASSWORD'])
+            if pagure.APP.config['SMTP_USERNAME'] \
+                    and pagure.APP.config['SMTP_PASSWORD']:
+                smtp.login(
+                    pagure.APP.config['SMTP_USERNAME'],
+                    pagure.APP.config['SMTP_PASSWORD']
+                )
 
             smtp.sendmail(
                 from_email,

From 11d3da763df240121b6605e3292e085b3c0a95aa Mon Sep 17 00:00:00 2001
From: Pierre-Yves Chibon 
Date: Aug 29 2016 09:25:52 +0000
Subject: [PATCH 61/635] Start pep8 and pylint fixes to pagure.ui


---

diff --git a/pagure/ui/admin.py b/pagure/ui/admin.py
index 776f299..e458051 100644
--- a/pagure/ui/admin.py
+++ b/pagure/ui/admin.py
@@ -1,17 +1,20 @@
 # -*- coding: utf-8 -*-
 
 """
- (c) 2014-2015 - Copyright Red Hat Inc
+ (c) 2014-2016 - Copyright Red Hat Inc
 
  Authors:
    Pierre-Yves Chibon 
 
 """
 
+# no-member
+# pylint: disable=E1101
+
+
 from functools import wraps
 
 import flask
-from sqlalchemy.exc import SQLAlchemyError
 
 import pagure.exceptions
 import pagure.forms
@@ -20,8 +23,6 @@ import pagure.lib.git
 from pagure import (APP, SESSION, generate_user_key_files,
                     is_admin, admin_session_timedout)
 
-# pylint: disable=E1101
-
 
 def admin_required(function):
     """ Flask decorator to retrict access to admins of pagure.
diff --git a/pagure/ui/app.py b/pagure/ui/app.py
index 17475fb..58133fa 100644
--- a/pagure/ui/app.py
+++ b/pagure/ui/app.py
@@ -84,7 +84,7 @@ def index_auth():
     try:
         repopage = int(repopage)
         if repopage < 1:
-            page = 1
+            repopage = 1
     except ValueError:
         repopage = 1
 
@@ -92,7 +92,7 @@ def index_auth():
     try:
         forkpage = int(forkpage)
         if forkpage < 1:
-            page = 1
+            forkpage = 1
     except ValueError:
         forkpage = 1
 
@@ -381,7 +381,7 @@ def new_project():
         create_readme = form.create_readme.data
 
         try:
-            message = pagure.lib.new_project(
+            pagure.lib.new_project(
                 SESSION,
                 name=name,
                 description=description,
diff --git a/pagure/ui/filters.py b/pagure/ui/filters.py
index 825c87f..a32c774 100644
--- a/pagure/ui/filters.py
+++ b/pagure/ui/filters.py
@@ -1,13 +1,21 @@
 # -*- coding: utf-8 -*-
 
 """
- (c) 2014 - Copyright Red Hat Inc
+ (c) 2014-2016 - Copyright Red Hat Inc
 
  Authors:
    Pierre-Yves Chibon 
 
 """
 
+# too-many-branches
+# pylint: disable=R0912
+# too-many-arguments
+# pylint: disable=R0913
+# too-many-locals
+# pylint: disable=R0914
+
+
 import datetime
 import textwrap
 
@@ -94,18 +102,19 @@ def format_loc(loc, commit=None, filename=None, tree_id=None, prequest=None,
         if filename and commit:
             output.append(
                 ''
-                ''
+                ''
                 ''
                 '

' - '' + '' '

' '' % ( { 'cnt': '%s_%s' % (index, cnt), 'cnt_lbl': cnt, - 'img': flask.url_for('static', filename='users.png'), 'filename': filename.decode('UTF-8'), 'commit': commit, 'tree_id': tree_id, @@ -115,7 +124,8 @@ def format_loc(loc, commit=None, filename=None, tree_id=None, prequest=None, else: output.append( '' - '' + '' % ( { 'cnt': '%s_%s' % (index, cnt), @@ -154,27 +164,30 @@ def format_loc(loc, commit=None, filename=None, tree_id=None, prequest=None, templ_delete = '' templ_edit = '' templ_edited = '' + status = str(comment.parent.status).lower() if authenticated() and ( - (str(comment.parent.status).lower() in ['true', 'open'] - and comment.user.user == flask.g.fas_user.username) - or is_repo_admin(comment.parent.project)): + ( + status in ['true', 'open'] + and comment.user.user == flask.g.fas_user.username + ) + or is_repo_admin(comment.parent.project)): templ_delete = tpl_delete % ({'commentid': comment.id}) - templ_edit = tpl_edit %({ + templ_edit = tpl_edit % ({ 'edit_url': flask.url_for( 'pull_request_edit_comment', repo=comment.parent.project.name, requestid=comment.parent.id, commentid=comment.id, - username=comment.parent.user.user \ - if comment.parent.project.is_fork else None + username=comment.parent.user.user + if comment.parent.project.is_fork else None ), 'requestid': comment.parent.id, 'commentid': comment.id, }) if comment.edited_on: - templ_edited = tpl_edited %({ - 'edit_date':comment.edited_on.strftime( + templ_edited = tpl_edited % ({ + 'edit_date': comment.edited_on.strftime( '%b %d %Y %H:%M:%S'), 'human_edit_date': humanize_date(comment.edited_on), 'user': comment.editor.user, @@ -199,7 +212,8 @@ def format_loc(loc, commit=None, filename=None, tree_id=None, prequest=None, '' '
' '%(templ_edited)s' - '
From d094aa01db94c6614f053eaa39de192d4b66d2e4 Mon Sep 17 00:00:00 2001 From: Clement Verna Date: Sep 06 2016 08:24:23 +0000 Subject: [PATCH 91/635] Avoid issues attachment containing json to be considered as an issue to be created/updated --- diff --git a/pagure/hooks/files/pagure_hook_tickets.py b/pagure/hooks/files/pagure_hook_tickets.py index b07292e..dbe9bff 100755 --- a/pagure/hooks/files/pagure_hook_tickets.py +++ b/pagure/hooks/files/pagure_hook_tickets.py @@ -80,7 +80,7 @@ def run_as_post_receive_hook(): data = ''.join( pagure.lib.git.read_git_lines( ['show', 'HEAD:%s' % filename], abspath)) - if data: + if data and 'files' not in filename: try: json_data = json.loads(data) except: From 506ac6b6b0226407ec1211de797ac1ecd517512e Mon Sep 17 00:00:00 2001 From: Clement Verna Date: Sep 06 2016 08:25:57 +0000 Subject: [PATCH 92/635] Add the html tag to allow striketrough in the markdown rendering Fixes https://pagure.io/pagure/issue/1261 --- diff --git a/pagure/lib/__init__.py b/pagure/lib/__init__.py index 1f22ace..0822242 100644 --- a/pagure/lib/__init__.py +++ b/pagure/lib/__init__.py @@ -2793,7 +2793,7 @@ def clean_input(text, ignore=None): 'p', 'br', 'div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'table', 'td', 'tr', 'th', 'col', 'tbody', 'pre', 'img', 'hr', 'dl', 'dt', 'dd', 'span', - 'kbd', 'var', + 'kbd', 'var', 'del', ] if ignore: for tag in ignore: From 0873f4bb38efa8224cc8841c12c64068d637ba75 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Sep 06 2016 08:36:08 +0000 Subject: [PATCH 93/635] Specify rel="noopener noreferrer" to link including target='_blank' This avoids potential security risk: https://dev.to/ben/the-targetblank-vulnerability-by-example https://mathiasbynens.github.io/rel-noopener/ --- diff --git a/pagure/templates/_render_repo.html b/pagure/templates/_render_repo.html index 231a2fb..38df7e8 100644 --- a/pagure/templates/_render_repo.html +++ b/pagure/templates/_render_repo.html @@ -255,8 +255,10 @@ Contributions View List
diff --git a/pagure/templates/issue.html b/pagure/templates/issue.html index 997a59c..ff46a70 100644 --- a/pagure/templates/issue.html +++ b/pagure/templates/issue.html @@ -89,7 +89,7 @@ Markdown Syntax + target="_blank" rel="noopener noreferrer">Markdown Syntax
diff --git a/pagure/templates/new_issue.html b/pagure/templates/new_issue.html index 642019b..32a0d09 100644 --- a/pagure/templates/new_issue.html +++ b/pagure/templates/new_issue.html @@ -91,7 +91,7 @@ {{ form.csrf_token }} Markdown Syntax + target="_blank" rel="noopener noreferrer">Markdown Syntax

diff --git a/pagure/templates/pull_request.html b/pagure/templates/pull_request.html index 531cb8a..f11cb8f 100644 --- a/pagure/templates/pull_request.html +++ b/pagure/templates/pull_request.html @@ -541,7 +541,7 @@ Markdown Syntax + target="_blank" rel="noopener noreferrer">Markdown Syntax
diff --git a/pagure/templates/repo_master.html b/pagure/templates/repo_master.html index 1fa75be..de13543 100644 --- a/pagure/templates/repo_master.html +++ b/pagure/templates/repo_master.html @@ -212,7 +212,7 @@ From c64736cc38bb9c32a20ec7f689e3c9acc3a739f3 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Sep 06 2016 08:54:57 +0000 Subject: [PATCH 94/635] Return the branches already concerned by a PR as well as those not This way we can both offer to open a new PR for that branch or to see the existing PR concerning the branch. --- diff --git a/pagure/internal/__init__.py b/pagure/internal/__init__.py index 828e50e..514a1af 100644 --- a/pagure/internal/__init__.py +++ b/pagure/internal/__init__.py @@ -321,14 +321,19 @@ def get_pull_request_ready_branch(): project_id_from=repo.id, status='Open' ) + branches_pr = {} for pr in prs: if pr.branch_from in branches: + branches_pr[pr.branch_from] = pr.id del(branches[pr.branch_from]) return flask.jsonify( { 'code': 'OK', - 'message': branches, + 'message': { + 'new_branch': branches, + 'branch_w_pr': branches_pr, + }, } ) From fc52ba91b2ccd7b80f88b3fabe0162f81c5a8d1a Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Sep 06 2016 08:56:05 +0000 Subject: [PATCH 95/635] Adjust the JS for the data change to point to the opened PR when there is one --- diff --git a/pagure/templates/repo_info.html b/pagure/templates/repo_info.html index f65c3fc..44d1552 100644 --- a/pagure/templates/repo_info.html +++ b/pagure/templates/repo_info.html @@ -278,7 +278,7 @@ $(function() { dataType: 'json', success: function(res) { if (res.code == 'OK'){ - for (branch in res.message){ + for (branch in res.message.new_branch){ var url = "{{ url_for( 'new_request_pull', repo=repo.name, @@ -286,7 +286,8 @@ $(function() { branch_to=head or 'master', branch_from='') }}"; html = ''; {% else %} html2 = ' \ New PR \
'; {%endif%} $('#branch-'+branch+' .branch_del').prepend(html2); $('[data-toggle="tooltip"]').tooltip({placement : 'bottom'}); } + for (branch in res.message.branch_w_pr){ + var url = "{{ url_for( + 'request_pull', + repo=repo.name, + username=repo.user.user if repo.is_fork else None, + requestid=-100) }}"; + url = url.replace(-100, res.message.branch_w_pr[branch]); + var html = ' \ + ' + + 'See PR \ + '; + console.log(html); + $('#branch-' + branch + ' .branch_del').prepend(html); + $('[data-toggle="tooltip"]').tooltip({placement : 'bottom'}); + } } } }); From d746d149c593bffcf2fa8851c5ba456a257a06c7 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Sep 06 2016 10:14:18 +0000 Subject: [PATCH 96/635] Adjust the label to be ``PR#`` rather than ``See PR`` --- diff --git a/pagure/templates/repo_info.html b/pagure/templates/repo_info.html index 44d1552..9ab7e36 100644 --- a/pagure/templates/repo_info.html +++ b/pagure/templates/repo_info.html @@ -327,7 +327,7 @@ $(function() { ' - + 'See PR \ + + 'PR#' + res.message.branch_w_pr[branch] + ' \ '; console.log(html); $('#branch-' + branch + ' .branch_del').prepend(html); From a83847de0886d3fd6606d5d54bf372161db67916 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Sep 06 2016 14:38:38 +0000 Subject: [PATCH 97/635] If the identified provided isn't a commit but a blob, bail out This should fix a number of error received by emails about Blob not having a .tree attribute --- diff --git a/pagure/ui/repo.py b/pagure/ui/repo.py index d287b21..3695b27 100644 --- a/pagure/ui/repo.py +++ b/pagure/ui/repo.py @@ -820,7 +820,7 @@ def view_tree(repo, identifier=None, username=None): if isinstance(commit, pygit2.Tag): commit = commit.get_object() - if commit: + if commit and not isinstance(commit, pygit2.Blob): content = sorted(commit.tree, key=lambda x: x.filemode) for i in commit.tree: name, ext = os.path.splitext(i.name) From 78837b8fd2030b19ad5649e4710cf8bc823a5c40 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Sep 07 2016 10:08:22 +0000 Subject: [PATCH 98/635] Make all the hooks use uselist=False This means that when doing project.ci_hook for example, only 1 item will be returned instead of a list (of 1 item) --- diff --git a/pagure/hooks/fedmsg.py b/pagure/hooks/fedmsg.py index e7ed0bb..d671bcf 100644 --- a/pagure/hooks/fedmsg.py +++ b/pagure/hooks/fedmsg.py @@ -42,7 +42,7 @@ class FedmsgTable(BASE): 'Project', remote_side=[Project.id], backref=backref( 'fedmsg_hook', cascade="delete, delete-orphan", - single_parent=True) + single_parent=True, uselist=False) ) diff --git a/pagure/hooks/irc.py b/pagure/hooks/irc.py index f1d26a0..28ca148 100644 --- a/pagure/hooks/irc.py +++ b/pagure/hooks/irc.py @@ -52,7 +52,7 @@ class IrcTable(BASE): 'Project', remote_side=[Project.id], backref=backref( 'irc_hook', cascade="delete, delete-orphan", - single_parent=True) + single_parent=True, uselist=False) ) diff --git a/pagure/hooks/mail.py b/pagure/hooks/mail.py index f6e70c4..a1c3792 100644 --- a/pagure/hooks/mail.py +++ b/pagure/hooks/mail.py @@ -44,7 +44,7 @@ class MailTable(BASE): 'Project', remote_side=[Project.id], backref=backref( 'mail_hook', cascade="delete, delete-orphan", - single_parent=True) + single_parent=True, uselist=False) ) diff --git a/pagure/hooks/pagure_ci.py b/pagure/hooks/pagure_ci.py index fa33272..7a671a3 100644 --- a/pagure/hooks/pagure_ci.py +++ b/pagure/hooks/pagure_ci.py @@ -53,26 +53,26 @@ class PagureCITable(BASE): 'Project', remote_side=[Project.id], backref=backref( 'ci_hook', cascade="delete, delete-orphan", - single_parent=True) + single_parent=True, uselist=False) ) tmpl = """ {% if repo | hasattr('ci_hook') and repo.ci_hook and - repo.ci_hook[0].pagure_ci_token %} + repo.ci_hook.pagure_ci_token %} The token to be used by jenkins to trigger the build is:
-{{ repo.ci_hook[0].pagure_ci_token}}
+{{ repo.ci_hook.pagure_ci_token}}
 
The URL to be used to POST the results of your build is:
 {{ (config['APP_URL'][:-1] if config['APP_URL'].endswith('/')
   else config['APP_URL'])
-  + url_for('api_ns.%s_ci_notification' % repo.ci_hook[0].ci_type,
+  + url_for('api_ns.%s_ci_notification' % repo.ci_hook.ci_type,
     repo=repo.name, username=username,
-    pagure_ci_token=repo.ci_hook[0].pagure_ci_token) }}
+    pagure_ci_token=repo.ci_hook.pagure_ci_token) }}
 
{% else %} diff --git a/pagure/hooks/pagure_force_commit.py b/pagure/hooks/pagure_force_commit.py index 2ef2d3b..e0ab0d0 100644 --- a/pagure/hooks/pagure_force_commit.py +++ b/pagure/hooks/pagure_force_commit.py @@ -45,7 +45,7 @@ class PagureForceCommitTable(BASE): 'Project', foreign_keys=[project_id], remote_side=[Project.id], backref=backref( 'pagure_force_commit_hook', cascade="delete, delete-orphan", - single_parent=True) + single_parent=True, uselist=False) ) diff --git a/pagure/hooks/pagure_hook.py b/pagure/hooks/pagure_hook.py index 541cae5..a207f66 100644 --- a/pagure/hooks/pagure_hook.py +++ b/pagure/hooks/pagure_hook.py @@ -44,7 +44,7 @@ class PagureTable(BASE): 'Project', remote_side=[Project.id], backref=backref( 'pagure_hook', cascade="delete, delete-orphan", - single_parent=True) + single_parent=True, uselist=False) ) diff --git a/pagure/hooks/pagure_request_hook.py b/pagure/hooks/pagure_request_hook.py index ee0529c..32f6123 100644 --- a/pagure/hooks/pagure_request_hook.py +++ b/pagure/hooks/pagure_request_hook.py @@ -46,7 +46,7 @@ class PagureRequestsTable(BASE): 'Project', remote_side=[Project.id], backref=backref( 'pagure_hook_requests', cascade="delete, delete-orphan", - single_parent=True) + single_parent=True, uselist=False) ) diff --git a/pagure/hooks/pagure_ticket_hook.py b/pagure/hooks/pagure_ticket_hook.py index d2379c1..06e6c33 100644 --- a/pagure/hooks/pagure_ticket_hook.py +++ b/pagure/hooks/pagure_ticket_hook.py @@ -45,7 +45,7 @@ class PagureTicketsTable(BASE): 'Project', remote_side=[Project.id], backref=backref( 'pagure_hook_tickets', cascade="delete, delete-orphan", - single_parent=True) + single_parent=True, uselist=False) ) diff --git a/pagure/hooks/pagure_unsigned_commits.py b/pagure/hooks/pagure_unsigned_commits.py index 117139f..39117bd 100644 --- a/pagure/hooks/pagure_unsigned_commits.py +++ b/pagure/hooks/pagure_unsigned_commits.py @@ -42,7 +42,7 @@ class PagureUnsignedCommitTable(BASE): 'Project', foreign_keys=[project_id], remote_side=[Project.id], backref=backref( 'pagure_unsigned_commit_hook', cascade="delete, delete-orphan", - single_parent=True) + single_parent=True, uselist=False) ) diff --git a/pagure/hooks/rtd.py b/pagure/hooks/rtd.py index 5f072b2..66ee001 100644 --- a/pagure/hooks/rtd.py +++ b/pagure/hooks/rtd.py @@ -45,7 +45,7 @@ class RtdTable(BASE): 'Project', remote_side=[Project.id], backref=backref( 'rtd_hook', cascade="delete, delete-orphan", - single_parent=True) + single_parent=True, uselist=False) ) From dcbd998670700c54362737ed8e9955501d4da8fd Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Sep 07 2016 10:08:22 +0000 Subject: [PATCH 99/635] Adjust all the place that were dong *_hook[0] to do *_hook directly --- diff --git a/pagure-ci/pagure_ci_server.py b/pagure-ci/pagure_ci_server.py index fb43952..a3470d9 100644 --- a/pagure-ci/pagure_ci_server.py +++ b/pagure-ci/pagure_ci_server.py @@ -96,7 +96,7 @@ def handle_messages(): log.info("Trigger on %s PR #%s from %s: %s", project.fullname, pr_id, repo, branch) - url = project.ci_hook[0].ci_url + url = project.ci_hook.ci_url if data['ci_type'] == 'jenkins': url = urlparse.urljoin(url, '/buildWithParameters') @@ -104,7 +104,7 @@ def handle_messages(): requests.post( url, data={ - 'token': project.ci_hook[0].pagure_ci_token, + 'token': project.ci_hook.pagure_ci_token, 'cause': pr_id, 'REPO': project.fullname, 'BRANCH': branch diff --git a/pagure/api/ci/jenkins.py b/pagure/api/ci/jenkins.py index 981055e..e0d45ac 100644 --- a/pagure/api/ci/jenkins.py +++ b/pagure/api/ci/jenkins.py @@ -44,7 +44,7 @@ def jenkins_ci_notification(repo, pagure_ci_token, username=None): if not constant_time.bytes_eq( to_bytes(pagure_ci_token), - to_bytes(project.ci_hook[0].pagure_ci_token)): + to_bytes(project.ci_hook.pagure_ci_token)): raise pagure.exceptions.APIError(401, error_code=APIERROR.EINVALIDTOK) data = flask.request.get_json() diff --git a/pagure/hooks/files/pagure_force_commit_hook.py b/pagure/hooks/files/pagure_force_commit_hook.py index a1bacd4..3b07189 100755 --- a/pagure/hooks/files/pagure_force_commit_hook.py +++ b/pagure/hooks/files/pagure_force_commit_hook.py @@ -42,7 +42,7 @@ def run_as_pre_receive_hook(): # Get the list of branches branches = [ branch.strip() - for branch in repo.pagure_force_commit_hook[0].branches.split(',') + for branch in repo.pagure_force_commit_hook.branches.split(',') if repo.pagure_force_commit_hook] # Remove empty branches diff --git a/pagure/hooks/files/rtd_hook.py b/pagure/hooks/files/rtd_hook.py index 98525c6..3803fb1 100755 --- a/pagure/hooks/files/rtd_hook.py +++ b/pagure/hooks/files/rtd_hook.py @@ -43,7 +43,7 @@ def run_as_post_receive_hook(): # Get the list of branches branches = [ branch.strip() - for branch in repo.rtd_hook[0].branches.split(',') + for branch in repo.rtd_hook.branches.split(',') if repo.rtd_hook] # Remove empty branches @@ -53,7 +53,7 @@ def run_as_post_receive_hook(): if branch] url = 'http://readthedocs.org/build/%s' % ( - repo.rtd_hook[0].project_name.strip() + repo.rtd_hook.project_name.strip() ) for line in sys.stdin: @@ -65,11 +65,11 @@ def run_as_post_receive_hook(): if branches: if refname in branches: print 'Starting RTD build for %s' % ( - repo.rtd_hook[0].project_name.strip()) + repo.rtd_hook.project_name.strip()) requests.post(url) else: print 'Starting RTD build for %s' % ( - repo.rtd_hook[0].project_name.strip()) + repo.rtd_hook.project_name.strip()) requests.post(url) diff --git a/pagure/lib/__init__.py b/pagure/lib/__init__.py index 0822242..0781098 100644 --- a/pagure/lib/__init__.py +++ b/pagure/lib/__init__.py @@ -882,7 +882,7 @@ def add_pull_request_comment(session, request, commit, tree_id, filename, if notification and request.status == 'Open' \ and request.project.ci_hook and PAGURE_CI: REDIS.publish('pagure.ci', json.dumps({ - 'ci_type': request.project.ci_hook[0].ci_type, + 'ci_type': request.project.ci_hook.ci_type, 'pr': request.to_json(public=True, with_comments=False) })) @@ -1255,7 +1255,7 @@ def new_pull_request(session, branch_from, # Send notification to the CI server if REDIS and request.project.ci_hook and PAGURE_CI: REDIS.publish('pagure.ci', json.dumps({ - 'ci_type': request.project.ci_hook[0].ci_type, + 'ci_type': request.project.ci_hook.ci_type, 'pr': request.to_json(public=True, with_comments=False) })) diff --git a/pagure/lib/lib_ci.py b/pagure/lib/lib_ci.py index 0e390e6..ecb3913 100644 --- a/pagure/lib/lib_ci.py +++ b/pagure/lib/lib_ci.py @@ -43,8 +43,8 @@ def process_jenkins_build(session, project, build_id, requestfolder): """ import jenkins # Jenkins Base URL - jenk = jenkins.Jenkins(project.ci_hook[0].ci_url.split('/job/')[0]) - jenkins_name = project.ci_hook[0].ci_url.split( + jenk = jenkins.Jenkins(project.ci_hook.ci_url.split('/job/')[0]) + jenkins_name = project.ci_hook.ci_url.split( '/job/', 1)[1].split('/', 1)[0] build_info = jenk.get_build_info(jenkins_name, build_id) result = build_info['result'] @@ -77,7 +77,7 @@ def process_jenkins_build(session, project, build_id, requestfolder): pagure.lib.add_pull_request_flag( session, request=request, - username=project.ci_hook[0].ci_type, + username=project.ci_hook.ci_type, percent=percent, comment=comment, url=url, From 66dd261fa217784fe263aaa898ba6a3ce9aecae3 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Sep 07 2016 10:08:22 +0000 Subject: [PATCH 100/635] Drop the check if the dbobj retrieved is of some length or not --- diff --git a/pagure/ui/plugins.py b/pagure/ui/plugins.py index 2c41394..a8997ba 100644 --- a/pagure/ui/plugins.py +++ b/pagure/ui/plugins.py @@ -101,8 +101,7 @@ def view_plugin(repo, plugin, username=None, full=True): dbobj = getattr(repo, plugin.backref) # There should always be only one, but let's double check - if dbobj and len(dbobj) > 0: - dbobj = dbobj[0] + if dbobj: new = False else: dbobj = plugin.db_object() From bf690318a75299c28047f4212131a174f648eb15 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Sep 07 2016 10:15:01 +0000 Subject: [PATCH 101/635] Small style fix in pagure_ci_server --- diff --git a/pagure-ci/pagure_ci_server.py b/pagure-ci/pagure_ci_server.py index a3470d9..aedeec3 100644 --- a/pagure-ci/pagure_ci_server.py +++ b/pagure-ci/pagure_ci_server.py @@ -93,8 +93,9 @@ def handle_messages(): base[:-1] base += '/%s' % project.path - log.info("Trigger on %s PR #%s from %s: %s", - project.fullname, pr_id, repo, branch) + log.info( + "Trigger on %s PR #%s from %s: %s", + project.fullname, pr_id, repo, branch) url = project.ci_hook.ci_url From 9ef674317b25ea6628a583a939c6f2e200855ce1 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Sep 07 2016 10:15:31 +0000 Subject: [PATCH 102/635] Fix building the jenkins URL to call to trigger the build in pagure-ci --- diff --git a/pagure-ci/pagure_ci_server.py b/pagure-ci/pagure_ci_server.py index aedeec3..99ff2fb 100644 --- a/pagure-ci/pagure_ci_server.py +++ b/pagure-ci/pagure_ci_server.py @@ -97,10 +97,10 @@ def handle_messages(): "Trigger on %s PR #%s from %s: %s", project.fullname, pr_id, repo, branch) - url = project.ci_hook.ci_url + url = project.ci_hook.ci_url.rstrip('/') if data['ci_type'] == 'jenkins': - url = urlparse.urljoin(url, '/buildWithParameters') + url = url + '/buildWithParameters' log.info('Triggering the build at: %s', url) requests.post( url, From 1ed4176981d871ae17f6f757663731956679427f Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Sep 07 2016 10:15:31 +0000 Subject: [PATCH 103/635] Drop un-used import --- diff --git a/pagure-ci/pagure_ci_server.py b/pagure-ci/pagure_ci_server.py index 99ff2fb..72ba82a 100644 --- a/pagure-ci/pagure_ci_server.py +++ b/pagure-ci/pagure_ci_server.py @@ -16,22 +16,14 @@ receiving end is offline or so. """ -import datetime -import hashlib -import hmac import json import logging import os import requests -import time -import urlparse -import uuid -import six import trollius import trollius_redis -from kitchen.text.converters import to_bytes log = logging.getLogger(__name__) From d31d7e2fc24333f5d91c710205f3a61d2630245f Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Sep 07 2016 10:15:31 +0000 Subject: [PATCH 104/635] pylint fixes to pagure-ci --- diff --git a/pagure-ci/pagure_ci_server.py b/pagure-ci/pagure_ci_server.py index 72ba82a..9e4f13e 100644 --- a/pagure-ci/pagure_ci_server.py +++ b/pagure-ci/pagure_ci_server.py @@ -25,8 +25,7 @@ import trollius import trollius_redis - -log = logging.getLogger(__name__) +LOG = logging.getLogger(__name__) if 'PAGURE_CONFIG' not in os.environ \ and os.path.exists('/etc/pagure/pagure.cfg'): @@ -36,16 +35,20 @@ if 'PAGURE_CONFIG' not in os.environ \ import pagure import pagure.lib -from pagure.exceptions import PagureEvException @trollius.coroutine def handle_messages(): + ''' Handles connecting to redis and acting upon messages received. + In this case, it means triggering a build on jenkins based on the + information provided. + ''' + host = pagure.APP.config.get('REDIS_HOST', '0.0.0.0') port = pagure.APP.config.get('REDIS_PORT', 6379) - db = pagure.APP.config.get('REDIS_DB', 0) + dbname = pagure.APP.config.get('REDIS_DB', 0) connection = yield trollius.From(trollius_redis.Connection.create( - host=host, port=port, db=db)) + host=host, port=port, db=dbname)) # Create subscriber. subscriber = yield trollius.From(connection.start_subscribe()) @@ -56,7 +59,7 @@ def handle_messages(): # Inside a while loop, wait for incoming events. while True: reply = yield trollius.From(subscriber.next_published()) - log.info( + LOG.info( 'Received: %s on channel: %s', repr(reply.value), reply.channel) data = json.loads(reply.value) @@ -74,26 +77,19 @@ def handle_messages(): session=pagure.SESSION, name=projectname, user=username) if not project: - log.warning( - 'No project could be found from the message %s' % data) + LOG.warning( + 'No project could be found from the message %s', data) continue - repo = data['pr'].get('remote_git') - if not repo: - base = pagure.APP.config['APP_URL'] - if base.endswith('/'): - base[:-1] - base += '/%s' % project.path - - log.info( + LOG.info( "Trigger on %s PR #%s from %s: %s", - project.fullname, pr_id, repo, branch) + project.fullname, pr_id, project.fullname, branch) url = project.ci_hook.ci_url.rstrip('/') if data['ci_type'] == 'jenkins': url = url + '/buildWithParameters' - log.info('Triggering the build at: %s', url) + LOG.info('Triggering the build at: %s', url) requests.post( url, data={ @@ -104,13 +100,14 @@ def handle_messages(): } ) else: - log.warning('Un-supported CI type') + LOG.warning('Un-supported CI type') - log.info('Ready for another') + LOG.info('Ready for another') def main(): - server = None + ''' Start the main async loop. ''' + try: loop = trollius.get_event_loop() tasks = [ @@ -123,24 +120,23 @@ def main(): except trollius.ConnectionResetError: pass - log.info("End Connection") + LOG.info("End Connection") loop.close() - log.info("End") + LOG.info("End") if __name__ == '__main__': - log = logging.getLogger("") formatter = logging.Formatter( "%(asctime)s %(levelname)s [%(module)s:%(lineno)d] %(message)s") # setup console logging - log.setLevel(logging.DEBUG) - ch = logging.StreamHandler() - ch.setLevel(logging.DEBUG) + LOG.setLevel(logging.DEBUG) + shellhandler = logging.StreamHandler() + shellhandler.setLevel(logging.DEBUG) aslog = logging.getLogger("asyncio") aslog.setLevel(logging.DEBUG) - ch.setFormatter(formatter) - log.addHandler(ch) + shellhandler.setFormatter(formatter) + LOG.addHandler(shellhandler) main() From b1b1f1db7dcbeb7a0bdad3f873cc9bc026e5783e Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Sep 07 2016 10:15:31 +0000 Subject: [PATCH 105/635] Make the jenkins_ci_notifications API endpoint marked as such This will allow catching all the APIError exception raised and turn them into valid JSON messages for the client --- diff --git a/pagure/api/ci/jenkins.py b/pagure/api/ci/jenkins.py index e0d45ac..22799fc 100644 --- a/pagure/api/ci/jenkins.py +++ b/pagure/api/ci/jenkins.py @@ -18,13 +18,14 @@ import pagure.exceptions import pagure.lib import pagure.lib.lib_ci as lib_ci from pagure import APP, SESSION -from pagure.api import API, APIERROR +from pagure.api import API, APIERROR, api_method @API.route('/ci/jenkins///build-finished', methods=['POST']) @API.route('/ci/jenkins/forks///' '/build-finished', methods=['POST']) +@api_method def jenkins_ci_notification(repo, pagure_ci_token, username=None): """ Jenkins Build Notification From abe18c084524e22e00f57f64861e6d053113b38b Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Sep 07 2016 10:15:32 +0000 Subject: [PATCH 106/635] Let process_jenkins_build be a little more flexible about the data retrieve Apparently, not all the builds have a 'result' key, so let's not assume there is always one. --- diff --git a/pagure/lib/lib_ci.py b/pagure/lib/lib_ci.py index ecb3913..db719e9 100644 --- a/pagure/lib/lib_ci.py +++ b/pagure/lib/lib_ci.py @@ -47,7 +47,7 @@ def process_jenkins_build(session, project, build_id, requestfolder): jenkins_name = project.ci_hook.ci_url.split( '/job/', 1)[1].split('/', 1)[0] build_info = jenk.get_build_info(jenkins_name, build_id) - result = build_info['result'] + result = build_info.get('result') url = build_info['url'] pr_id = None From 3e1101ec0e114c16b0f74eaf027a0a5d91baf6d7 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Sep 07 2016 10:15:32 +0000 Subject: [PATCH 107/635] Make the REPO variable be the public url where it can be cloned --- diff --git a/pagure-ci/pagure_ci_server.py b/pagure-ci/pagure_ci_server.py index 9e4f13e..69cc2a5 100644 --- a/pagure-ci/pagure_ci_server.py +++ b/pagure-ci/pagure_ci_server.py @@ -89,13 +89,16 @@ def handle_messages(): if data['ci_type'] == 'jenkins': url = url + '/buildWithParameters' - LOG.info('Triggering the build at: %s', url) + repo = '%s/%s' % ( + pagure.APP.config['GIT_URL_GIT'].rstrip('/'), project.path) + LOG.info( + 'Triggering the build at: %s, for repo: %s', url, repo) requests.post( url, data={ 'token': project.ci_hook.pagure_ci_token, 'cause': pr_id, - 'REPO': project.fullname, + 'REPO': repo, 'BRANCH': branch } ) From feb4b60541e4b8cbfc86e397a7a75fc57b5b3f57 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Sep 07 2016 10:15:32 +0000 Subject: [PATCH 108/635] Fix retrieving the username from the data when the repo is a fork Thanks @vivekanand1101 --- diff --git a/pagure-ci/pagure_ci_server.py b/pagure-ci/pagure_ci_server.py index 69cc2a5..96d8c74 100644 --- a/pagure-ci/pagure_ci_server.py +++ b/pagure-ci/pagure_ci_server.py @@ -71,7 +71,7 @@ def handle_messages(): username = None projectname = data['pr']['project']['name'] if data['pr'].get('parent'): - username, data['pr']['project']['user']['user'] + username = data['pr']['project']['user']['user'] project = pagure.lib.get_project( session=pagure.SESSION, name=projectname, user=username) From 2ae1ff01dbb2af55fbc4fd2ae38599266bcc310f Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Sep 07 2016 10:15:32 +0000 Subject: [PATCH 109/635] Drop un-used variable --- diff --git a/pagure-ci/pagure_ci_server.py b/pagure-ci/pagure_ci_server.py index 96d8c74..b9d87e8 100644 --- a/pagure-ci/pagure_ci_server.py +++ b/pagure-ci/pagure_ci_server.py @@ -65,11 +65,10 @@ def handle_messages(): data = json.loads(reply.value) pr_id = data['pr']['id'] - project = data['pr']['project']['name'] branch = data['pr']['branch_from'] + projectname = data['pr']['project']['name'] username = None - projectname = data['pr']['project']['name'] if data['pr'].get('parent'): username = data['pr']['project']['user']['user'] From 52ed110a52259f30b57370c39bb4ce27b4bcec65 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Sep 07 2016 10:22:24 +0000 Subject: [PATCH 110/635] Drop get_project_by_ci_token from pagure.lib_ci since it not used anywhere --- diff --git a/pagure/lib/lib_ci.py b/pagure/lib/lib_ci.py index db719e9..33ddd98 100644 --- a/pagure/lib/lib_ci.py +++ b/pagure/lib/lib_ci.py @@ -24,19 +24,6 @@ BUILD_STATS = { } -def get_project_by_ci_token(session, ci_token): - """ Return the project corresponding to the provided ci_token. """ - query = session.query( - model.Project - ).filter( - model.Project.id == pagure_ci.PagureCITable.project_id - ).filter( - pagure_ci.PagureCITable.pagure_ci_token == ci_token - ) - - return query.first() - - def process_jenkins_build(session, project, build_id, requestfolder): """ Gets the build info from jenkins and flags that particular pull-request. From 78e5507315aa176f7dcdf602ffd1ebff01130755 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Sep 07 2016 10:50:18 +0000 Subject: [PATCH 111/635] Add a decorator: repo_method saving some of the most used object This way, the most used variables don't need to be called everytime --- diff --git a/pagure/__init__.py b/pagure/__init__.py index 11692be..0d6a904 100644 --- a/pagure/__init__.py +++ b/pagure/__init__.py @@ -17,6 +17,7 @@ __api_version__ = '0.7' import datetime +import functools import logging import os import subprocess @@ -198,6 +199,32 @@ LOG.setLevel(APP.config.get('LOG_LEVEL', 'INFO')) APP.wsgi_app = pagure.proxy.ReverseProxied(APP.wsgi_app) +def repo_method(function): + ''' Check the info provided in the URL and return the arguments needed + for all the endpoints. + ''' + + @functools.wraps(function) + def wrapper(*args, **kwargs): + ''' Actually does the job with the arguments provided. ''' + + repo = flask.request.view_args.get('repo') + username = flask.request.view_args.get('username') + if repo: + flask.g.repo = pagure.lib.get_project( + SESSION, repo, user=username) + if flask.g.repo is None: + flask.abort(404, 'Project not found') + + flask.g.reponame = pagure.get_repo_path(flask.g.repo) + flask.g.repo_obj = pygit2.Repository(flask.g.reponame) + flask.g.repo_admin = is_repo_admin(flask.g.repo) + + return function(*args, **kwargs) + + return wrapper + + def authenticated(): ''' Utility function checking if the current user is logged in or not. ''' From 16572e734b8e14cd990eda6a9bf68d9f7c73c8cb Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Sep 07 2016 10:50:18 +0000 Subject: [PATCH 112/635] Make use of the repo_method decorator in the repo endpoint --- diff --git a/pagure/ui/repo.py b/pagure/ui/repo.py index 3695b27..cdd76d5 100644 --- a/pagure/ui/repo.py +++ b/pagure/ui/repo.py @@ -48,7 +48,7 @@ import pagure.forms import pagure import pagure.ui.plugins from pagure import (APP, SESSION, LOG, __get_file_in_tree, login_required, - is_repo_admin, admin_session_timedout) + is_repo_admin, admin_session_timedout, repo_method) @APP.route('/.git') @@ -65,17 +65,13 @@ def view_repo_git(repo, username=None): @APP.route('/') @APP.route('/fork///') @APP.route('/fork//') +@repo_method def view_repo(repo, username=None): """ Front page of a specific repo. """ - repo = pagure.lib.get_project(SESSION, repo, user=username) - - if repo is None: - flask.abort(404, 'Project not found') - - reponame = pagure.get_repo_path(repo) - - repo_obj = pygit2.Repository(reponame) + repo = flask.g.repo + reponame = flask.g.reponame + repo_obj = flask.g.repo_obj if not repo_obj.is_empty and not repo_obj.head_is_unborn: head = repo_obj.head.shorthand @@ -168,17 +164,13 @@ def view_repo(repo, username=None): @APP.route('//branch/') @APP.route('/fork///branch/') +@repo_method def view_repo_branch(repo, branchname, username=None): ''' Returns the list of branches in the repo. ''' - repo = pagure.lib.get_project(SESSION, repo, user=username) - - if not repo: - flask.abort(404, 'Project not found') - - reponame = pagure.get_repo_path(repo) - - repo_obj = pygit2.Repository(reponame) + repo = flask.g.repo + reponame = flask.g.reponame + repo_obj = flask.g.repo_obj if branchname not in repo_obj.listall_branches(): flask.abort(404, 'Branch no found') @@ -272,17 +264,13 @@ def view_repo_branch(repo, branchname, username=None): @APP.route('/fork///commits/') @APP.route('/fork///commits') @APP.route('/fork///commits/') +@repo_method def view_commits(repo, branchname=None, username=None): """ Displays the commits of the specified repo. """ - repo = pagure.lib.get_project(SESSION, repo, user=username) - - if not repo: - flask.abort(404, 'Project not found') - - reponame = pagure.get_repo_path(repo) - - repo_obj = pygit2.Repository(reponame) + repo = flask.g.repo + reponame = flask.g.reponame + repo_obj = flask.g.repo_obj if branchname and branchname not in repo_obj.listall_branches(): flask.abort(404, 'Branch no found') @@ -384,17 +372,13 @@ def view_commits(repo, branchname=None, username=None): @APP.route('//c/..') @APP.route('/fork///c/../') @APP.route('/fork///c/..') +@repo_method def compare_commits(repo, commit1, commit2, username=None): """ Compares two commits for specified repo """ - repo = pagure.lib.get_project(SESSION, repo, user=username) - - if not repo: - flask.abort(404, 'Project not found') - - reponame = pagure.get_repo_path(repo) - - repo_obj = pygit2.Repository(reponame) + repo = flask.g.repo + reponame = flask.g.reponame + repo_obj = flask.g.repo_obj if not repo_obj.is_empty and not repo_obj.head_is_unborn: head = repo_obj.head.shorthand @@ -456,17 +440,13 @@ def compare_commits(repo, commit1, commit2, username=None): @APP.route('//blob//f/') @APP.route( '/fork///blob//f/') +@repo_method def view_file(repo, identifier, filename, username=None): """ Displays the content of a file or a tree for the specified repo. """ - repo = pagure.lib.get_project(SESSION, repo, user=username) - - if not repo: - flask.abort(404, 'Project not found') - - reponame = pagure.get_repo_path(repo) - - repo_obj = pygit2.Repository(reponame) + repo = flask.g.repo + reponame = flask.g.reponame + repo_obj = flask.g.repo_obj if repo_obj.is_empty: flask.abort(404, 'Empty repo cannot have a file') @@ -591,17 +571,13 @@ def view_file(repo, identifier, filename, username=None): defaults={'filename': None}) @APP.route( '/fork///raw//f/') +@repo_method def view_raw_file(repo, identifier, filename=None, username=None): """ Displays the raw content of a file of a commit for the specified repo. """ - repo = pagure.lib.get_project(SESSION, repo, user=username) - - if not repo: - flask.abort(404, 'Project not found') - - reponame = pagure.get_repo_path(repo) - - repo_obj = pygit2.Repository(reponame) + repo = flask.g.repo + reponame = flask.g.reponame + repo_obj = flask.g.repo_obj if repo_obj.is_empty: flask.abort(404, 'Empty repo cannot have a file') @@ -701,17 +677,23 @@ if APP.config.get('OLD_VIEW_COMMIT_ENABLED', False): @APP.route('//c/') @APP.route('/fork///c//') @APP.route('/fork///c/') +@repo_method def view_commit(repo, commitid, username=None): """ Render a commit in a repo """ - repo = pagure.lib.get_project(SESSION, repo, user=username) - - if not repo: - flask.abort(404, 'Project not found') - - reponame = pagure.get_repo_path(repo) - - repo_obj = pygit2.Repository(reponame) + print flask.request + print dir(flask.request) + print flask.request.args + print flask.request.query_string + print flask.request.values + print flask.request.endpoint + print flask.request.json + print flask.request.url_rule + print flask.request.view_args + + repo = flask.g.repo + reponame = flask.g.reponame + repo_obj = flask.g.repo_obj branchname = flask.request.args.get('branch', None) @@ -751,17 +733,13 @@ def view_commit(repo, commitid, username=None): @APP.route('//c/.patch') @APP.route('/fork///c/.patch') +@repo_method def view_commit_patch(repo, commitid, username=None): """ Render a commit in a repo as patch """ - repo = pagure.lib.get_project(SESSION, repo, user=username) - - if not repo: - flask.abort(404, 'Project not found') - - reponame = pagure.get_repo_path(repo) - - repo_obj = pygit2.Repository(reponame) + repo = flask.g.repo + reponame = flask.g.reponame + repo_obj = flask.g.repo_obj try: commit = repo_obj.get(commitid) @@ -782,17 +760,13 @@ def view_commit_patch(repo, commitid, username=None): @APP.route('/fork///tree/') @APP.route('/fork///tree') @APP.route('/fork///tree/') +@repo_method def view_tree(repo, identifier=None, username=None): """ Render the tree of the repo """ - repo = pagure.lib.get_project(SESSION, repo, user=username) - - if repo is None: - flask.abort(404, 'Project not found') - - reponame = pagure.get_repo_path(repo) - - repo_obj = pygit2.Repository(reponame) + repo = flask.g.repo + reponame = flask.g.reponame + repo_obj = flask.g.repo_obj branchname = None content = None @@ -857,13 +831,13 @@ def view_tree(repo, identifier=None, username=None): @APP.route('//forks') @APP.route('/fork///forks/') @APP.route('/fork///forks') +@repo_method def view_forks(repo, username=None): """ Presents all the forks of the project. """ - repo = pagure.lib.get_project(SESSION, repo, user=username) - - if not repo: - flask.abort(404, 'Project not found') + repo = flask.g.repo + reponame = flask.g.reponame + repo_obj = flask.g.repo_obj return flask.render_template( 'forks.html', @@ -878,16 +852,13 @@ def view_forks(repo, username=None): @APP.route('//releases') @APP.route('/fork///releases/') @APP.route('/fork///releases') +@repo_method def view_tags(repo, username=None): """ Presents all the tags of the project. """ - repo = pagure.lib.get_project(SESSION, repo, user=username) - - if not repo: - flask.abort(404, 'Project not found') - - reponame = pagure.get_repo_path(repo) - repo_obj = pygit2.Repository(reponame) + repo = flask.g.repo + reponame = flask.g.reponame + repo_obj = flask.g.repo_obj tags = pagure.lib.git.get_git_tags_objects(repo) @@ -957,6 +928,7 @@ def new_release(repo, username=None): @APP.route('/fork///settings/', methods=('GET', 'POST')) @APP.route('/fork///settings', methods=('GET', 'POST')) @login_required +@repo_method def view_settings(repo, username=None): """ Presents the settings of the project. """ @@ -966,10 +938,9 @@ def view_settings(repo, username=None): return flask.redirect( flask.url_for('auth_login', next=flask.request.url)) - repo = pagure.lib.get_project(SESSION, repo, user=username) - - if not repo: - flask.abort(404, 'Project not found') + repo = flask.g.repo + reponame = flask.g.reponame + repo_obj = flask.g.repo_obj repo_admin = is_repo_admin(repo) if not repo_admin: @@ -977,9 +948,6 @@ def view_settings(repo, username=None): 403, 'You are not allowed to change the settings for this project') - reponame = pagure.get_repo_path(repo) - repo_obj = pygit2.Repository(reponame) - plugins = pagure.ui.plugins.get_plugin_names( APP.config.get('DISABLED_PLUGINS')) tags = pagure.lib.get_tags_of_project(SESSION, repo) @@ -1261,6 +1229,7 @@ def update_milestones(repo, username=None): @APP.route('//default/branch/', methods=['POST']) @APP.route('/fork///default/branch/', methods=['POST']) @login_required +@repo_method def change_ref_head(repo, username=None): """ Change HEAD reference """ @@ -1272,16 +1241,15 @@ def change_ref_head(repo, username=None): return flask.redirect( flask.url_for('auth_login', next=url)) - repo = pagure.lib.get_project(SESSION, repo, user=username) - if not repo: - flask.abort(404, 'Project not found') + repo = flask.g.repo + reponame = flask.g.reponame + repo_obj = flask.g.repo_obj if not is_repo_admin(repo): flask.abort( 403, 'You are not allowed to change the settings for this project') - repopath = pagure.get_repo_path(repo) - repo_obj = pygit2.Repository(repopath) + branches = repo_obj.listall_branches() form = pagure.forms.DefaultBranchForm(branches=branches) @@ -1695,6 +1663,7 @@ def regenerate_git(repo, username=None): @APP.route('/fork///token/new/', methods=('GET', 'POST')) @APP.route('/fork///token/new', methods=('GET', 'POST')) @login_required +@repo_method def add_token(repo, username=None): """ Add a token to a specified project. """ @@ -1704,13 +1673,9 @@ def add_token(repo, username=None): return flask.redirect( flask.url_for('auth_login', next=flask.request.url)) - repo = pagure.lib.get_project(SESSION, repo, user=username) - - if not repo: - flask.abort(404, 'Project not found') + repo = flask.g.repo - repo_admin = is_repo_admin(repo) - if not repo_admin: + if not flask.g.repo_admin: flask.abort( 403, 'You are not allowed to change the settings for this project') @@ -1742,7 +1707,6 @@ def add_token(repo, username=None): select='settings', form=form, acls=acls, - repo_admin=repo_admin, username=username, repo=repo, ) @@ -1752,6 +1716,7 @@ def add_token(repo, username=None): @APP.route('/fork///token/revoke/', methods=['POST']) @login_required +@repo_method def revoke_api_token(repo, token_id, username=None): """ Revokie a token to a specified project. """ @@ -1762,12 +1727,9 @@ def revoke_api_token(repo, token_id, username=None): return flask.redirect( flask.url_for('auth_login', next=url)) - repo = pagure.lib.get_project(SESSION, repo, user=username) - - if not repo: - flask.abort(404, 'Project not found') + repo = flask.g.repo - if not is_repo_admin(repo): + if not flask.g.repo_admin: flask.abort( 403, 'You are not allowed to change the settings for this project') @@ -1806,15 +1768,15 @@ def revoke_api_token(repo, token_id, username=None): '/fork///edit//f/', methods=('GET', 'POST')) @login_required +@repo_method def edit_file(repo, branchname, filename, username=None): """ Edit a file online. """ - repo = pagure.lib.get_project(SESSION, repo, user=username) - - if not repo: - flask.abort(404, 'Project not found') + repo = flask.g.repo + reponame = flask.g.reponame + repo_obj = flask.g.repo_obj - if not is_repo_admin(repo): + if not flask.g.repo_admin: flask.abort( 403, 'You are not allowed to change the settings for this project') @@ -1822,10 +1784,6 @@ def edit_file(repo, branchname, filename, username=None): user = pagure.lib.search_user( SESSION, username=flask.g.fas_user.username) - reponame = pagure.get_repo_path(repo) - - repo_obj = pygit2.Repository(reponame) - if repo_obj.is_empty: flask.abort(404, 'Empty repo cannot have a file') @@ -1894,15 +1852,15 @@ def edit_file(repo, branchname, filename, username=None): @APP.route('/fork///b//delete', methods=['POST']) @login_required +@repo_method def delete_branch(repo, branchname, username=None): """ Delete the branch of a project. """ - repo_obj = pagure.lib.get_project(SESSION, repo, user=username) - - if not repo_obj: - flask.abort(404, 'Project not found') + repo = flask.g.repo + reponame = flask.g.reponame + repo_obj = flask.g.repo_obj - if not is_repo_admin(repo_obj): + if not flask.g.repo_admin: flask.abort( 403, 'You are not allowed to delete branch for this project') @@ -1910,14 +1868,11 @@ def delete_branch(repo, branchname, username=None): if branchname == 'master': flask.abort(403, 'You are not allowed to delete the master branch') - reponame = pagure.get_repo_path(repo_obj) - repo_git = pygit2.Repository(reponame) - - if branchname not in repo_git.listall_branches(): + if branchname not in repo_obj.listall_branches(): flask.abort(404, 'Branch no found') try: - branch = repo_git.lookup_branch(branchname) + branch = repo_obj.lookup_branch(branchname) branch.delete() flask.flash('Branch `%s` deleted' % branchname) except pygit2.GitError as err: @@ -1925,7 +1880,7 @@ def delete_branch(repo, branchname, username=None): flask.flash('Could not delete `%s`' % branchname, 'error') return flask.redirect( - flask.url_for('view_repo', repo=repo, username=username)) + flask.url_for('view_repo', repo=repo.name, username=username)) @APP.route('/docs//') @@ -1974,9 +1929,9 @@ def view_project_activity(repo): @APP.route('/watch//settings/', methods=['POST']) -@APP.route('/watch/fork///settings/', methods=['POST']) +@APP.route('/watch/fork///settings/', methods=['POST']) @login_required -def watch_repo(repo, watch, user=None): +def watch_repo(repo, watch, username=None): """ Marked for watching or Unwatching """ return_point = flask.url_for('index') @@ -1990,19 +1945,17 @@ def watch_repo(repo, watch, user=None): if str(watch) not in ['0', '1']: flask.abort(400) - username = flask.g.fas_user.username - repo_obj = pagure.lib.get_project(SESSION, repo) - if user is not None: - repo_obj = pagure.lib.get_project(SESSION, repo, user) + repo_obj = pagure.lib.get_project(SESSION, repo, user=username) if not repo_obj: flask.abort(404, 'Project not found') try: msg = pagure.lib.update_watch_status( - SESSION, repo_obj, - username, watch - ) + SESSION, + repo_obj, + flask.g.fas_user.username, + watch) SESSION.commit() flask.flash(msg) except pagure.exceptions.PagureException as msg: From ae3cda3343a78767e6079304998eca64fbb6571a Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Sep 07 2016 10:50:18 +0000 Subject: [PATCH 113/635] Adjust the tests for the use of the repo_method decorator --- diff --git a/tests/test_pagure_flask_ui_repo.py b/tests/test_pagure_flask_ui_repo.py index 0efa5d7..0c1bda9 100644 --- a/tests/test_pagure_flask_ui_repo.py +++ b/tests/test_pagure_flask_ui_repo.py @@ -2560,6 +2560,7 @@ index 0000000..fb7093d self.assertEqual(output.status_code, 404) tests.create_projects(self.session) + tests.create_projects_git(tests.HERE, bare=True) # No a repo admin output = self.app.get('/test/edit/foo/f/sources') @@ -2568,12 +2569,7 @@ index 0000000..fb7093d user.username = 'pingou' with tests.user_set(pagure.APP, user): - # No associated git repo - output = self.app.get('/test/edit/foo/f/sources') - self.assertEqual(output.status_code, 404) - - tests.create_projects_git(tests.HERE, bare=True) - + # No such file output = self.app.get('/test/edit/foo/f/sources') self.assertEqual(output.status_code, 404) @@ -2704,7 +2700,7 @@ index 0000000..fb7093d user = tests.FakeUser() with tests.user_set(pagure.APP, user): output = self.app.post('/foo/default/branch/') - self.assertEqual(output.status_code, 302) + self.assertEqual(output.status_code, 404) ast.return_value = False @@ -2712,13 +2708,13 @@ index 0000000..fb7093d self.assertEqual(output.status_code, 404) tests.create_projects(self.session) + repos = tests.create_projects_git(tests.HERE) output = self.app.post('/test/default/branch/') self.assertEqual(output.status_code, 403) user.username = 'pingou' with tests.user_set(pagure.APP, user): - repo = tests.create_projects_git(tests.HERE) output = self.app.post('/test/default/branch/', follow_redirects=True) # without git branch self.assertEqual(output.status_code, 200) @@ -2731,7 +2727,7 @@ index 0000000..fb7093d csrf_token = output.data.split( 'name="csrf_token" type="hidden" value="')[1].split('">')[0] - repo_obj = pygit2.Repository(repo[0]) + repo_obj = pygit2.Repository(repos[0]) tree = repo_obj.index.write_tree() author = pygit2.Signature( 'Alice Author', 'alice@authors.tld') @@ -2858,6 +2854,7 @@ index 0000000..fb7093d self.assertEqual(output.status_code, 404) tests.create_projects(self.session) + tests.create_projects_git(tests.HERE, bare=True) output = self.app.get('/test/token/new/') self.assertEqual(output.status_code, 403) @@ -2889,15 +2886,6 @@ index 0000000..fb7093d self.assertIn('Create a new token', output.data) data = {'csrf_token': csrf_token, 'acls': ['issue_create']} - output = self.app.post( - '/test/token/new/', data=data, follow_redirects=True) - self.assertEqual(output.status_code, 404) - self.assertIn( - '\n Token created', - output.data) - self.assertIn('

No git repo found

', output.data) - - repo = tests.create_projects_git(tests.HERE) # Upload successful data = {'csrf_token': csrf_token, 'acls': ['issue_create']} @@ -2927,6 +2915,7 @@ index 0000000..fb7093d self.assertEqual(output.status_code, 404) tests.create_projects(self.session) + tests.create_projects_git(tests.HERE, bare=True) output = self.app.post('/test/token/revoke/123') self.assertEqual(output.status_code, 403) @@ -2957,7 +2946,6 @@ index 0000000..fb7093d self.assertIn('

Token not found

', output.data) # Create a token to revoke - repo = tests.create_projects_git(tests.HERE) data = {'csrf_token': csrf_token, 'acls': ['issue_create']} output = self.app.post( '/test/token/new/', data=data, follow_redirects=True) @@ -2999,6 +2987,7 @@ index 0000000..fb7093d self.assertEqual(output.status_code, 302) tests.create_projects(self.session) + tests.create_projects_git(tests.HERE, bare=True) user = tests.FakeUser() with tests.user_set(pagure.APP, user): @@ -3017,8 +3006,6 @@ index 0000000..fb7093d '

You are not allowed to delete the master branch

', output.data) - tests.create_projects_git(tests.HERE, bare=True) - output = self.app.post('/test/b/bar/delete') self.assertEqual(output.status_code, 404) self.assertIn('

Branch no found

', output.data) From 3119c6b72657c09c917342e676327e1f161ed477 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Sep 07 2016 10:50:18 +0000 Subject: [PATCH 114/635] Drop setting repo_admin in most endpoints --- diff --git a/pagure/ui/repo.py b/pagure/ui/repo.py index cdd76d5..9b6d249 100644 --- a/pagure/ui/repo.py +++ b/pagure/ui/repo.py @@ -157,7 +157,6 @@ def view_repo(repo, username=None): last_commits=last_commits, tree=tree, diff_commits=diff_commits, - repo_admin=is_repo_admin(repo), form=pagure.forms.ConfirmationForm(), ) @@ -253,7 +252,6 @@ def view_repo_branch(repo, branchname, username=None): safe=safe, readme=readme, diff_commits=diff_commits, - repo_admin=is_repo_admin(repo), form=pagure.forms.ConfirmationForm(), ) @@ -363,7 +361,6 @@ def view_commits(repo, branchname=None, username=None): number_of_commits=n_commits, page=page, total_page=total_page, - repo_admin=is_repo_admin(repo), form=pagure.forms.ConfirmationForm(), ) @@ -433,7 +430,6 @@ def compare_commits(repo, commit1, commit2, username=None): diff=diff, diff_commits=diff_commits, branches=sorted(repo_obj.listall_branches()), - repo_admin=is_repo_admin(repo), ) @@ -554,7 +550,6 @@ def view_file(repo, identifier, filename, username=None): filename=filename, content=content, output_type=output_type, - repo_admin=is_repo_admin(repo), readme=readme, readme_ext=readme_ext, safe=safe, @@ -723,7 +718,6 @@ def view_commit(repo, commitid, username=None): repo=repo, branchname=branchname, username=username, - repo_admin=is_repo_admin(repo), commitid=commitid, commit=commit, diff=diff, @@ -820,7 +814,6 @@ def view_tree(repo, identifier=None, username=None): filename='', content=content, output_type=output_type, - repo_admin=is_repo_admin(repo), readme=readme, readme_ext=readme_ext, safe=safe, @@ -844,7 +837,6 @@ def view_forks(repo, username=None): select='forks', username=username, repo=repo, - repo_admin=is_repo_admin(repo), ) @@ -868,7 +860,6 @@ def view_tags(repo, username=None): username=username, repo=repo, tags=tags, - repo_admin=is_repo_admin(repo), repo_obj=repo_obj, ) @@ -942,8 +933,7 @@ def view_settings(repo, username=None): reponame = flask.g.reponame repo_obj = flask.g.repo_obj - repo_admin = is_repo_admin(repo) - if not repo_admin: + if not flask.g.repo_admin: flask.abort( 403, 'You are not allowed to change the settings for this project') @@ -1001,7 +991,6 @@ def view_settings(repo, username=None): branches_form=branches_form, tags=tags, plugins=plugins, - repo_admin=repo_admin, branchname=branchname, ) @@ -1245,7 +1234,7 @@ def change_ref_head(repo, username=None): reponame = flask.g.reponame repo_obj = flask.g.repo_obj - if not is_repo_admin(repo): + if not flask.g.repo_admin: flask.abort( 403, 'You are not allowed to change the settings for this project') @@ -1270,6 +1259,7 @@ def change_ref_head(repo, username=None): @APP.route('//delete', methods=['POST']) @APP.route('/fork///delete', methods=['POST']) @login_required +@repo_method def delete_repo(repo, username=None): """ Delete the present project. """ @@ -1283,12 +1273,9 @@ def delete_repo(repo, username=None): return flask.redirect( flask.url_for('auth_login', next=url)) - repo = pagure.lib.get_project(SESSION, repo, user=username) - - if not repo: - flask.abort(404, 'Project not found') + repo = flask.g.repo - if not is_repo_admin(repo): + if not flask.g.repo_admin: flask.abort( 403, 'You are not allowed to change the settings for this project') @@ -1329,6 +1316,7 @@ def delete_repo(repo, username=None): @APP.route('//hook_token', methods=['POST']) @APP.route('/fork///hook_token', methods=['POST']) @login_required +@repo_method def new_repo_hook_token(repo, username=None): """ Re-generate a hook token for the present project. """ @@ -1342,12 +1330,9 @@ def new_repo_hook_token(repo, username=None): return flask.redirect( flask.url_for('auth_login', next=url)) - repo = pagure.lib.get_project(SESSION, repo, user=username) - - if not repo: - flask.abort(404, 'Project not found') + repo = flask.g.repo - if not is_repo_admin(repo): + if not flask.g.repo_admin: flask.abort( 403, 'You are not allowed to change the settings for this project') @@ -1373,6 +1358,7 @@ def new_repo_hook_token(repo, username=None): @APP.route('/fork///dropuser/', methods=['POST']) @login_required +@repo_method def remove_user(repo, userid, username=None): """ Remove the specified user from the project. """ @@ -1387,12 +1373,9 @@ def remove_user(repo, userid, username=None): return flask.redirect( flask.url_for('auth_login', next=url)) - repo = pagure.lib.get_project(SESSION, repo, user=username) - - if not repo: - flask.abort(404, 'Project not found') + repo = flask.g.repo - if not is_repo_admin(repo): + if not flask.g.repo_admin: flask.abort( 403, 'You are not allowed to change the users for this project') @@ -1432,6 +1415,7 @@ def remove_user(repo, userid, username=None): @APP.route('/fork///adduser/', methods=('GET', 'POST')) @APP.route('/fork///adduser', methods=('GET', 'POST')) @login_required +@repo_method def add_user(repo, username=None): """ Add the specified user from the project. """ @@ -1446,12 +1430,9 @@ def add_user(repo, username=None): return flask.redirect( flask.url_for('auth_login', next=flask.request.url)) - repo = pagure.lib.get_project(SESSION, repo, user=username) - - if not repo: - flask.abort(404, 'Project not found') + repo = flask.g.repo - if not is_repo_admin(repo): + if not flask.g.repo_admin: flask.abort( 403, 'You are not allowed to add users to this project') @@ -1492,6 +1473,7 @@ def add_user(repo, username=None): @APP.route( '/fork///dropgroup/', methods=['POST']) @login_required +@repo_method def remove_group_project(repo, groupid, username=None): """ Remove the specified group from the project. """ @@ -1507,12 +1489,9 @@ def remove_group_project(repo, groupid, username=None): return flask.redirect( flask.url_for('auth_login', next=url)) - repo = pagure.lib.get_project(SESSION, repo, user=username) - - if not repo: - flask.abort(404, 'Project not found') + repo = flask.g.repo - if not is_repo_admin(repo): + if not flask.g.repo_admin: flask.abort( 403, 'You are not allowed to change the users for this project') @@ -1552,6 +1531,7 @@ def remove_group_project(repo, groupid, username=None): @APP.route('/fork///addgroup/', methods=('GET', 'POST')) @APP.route('/fork///addgroup', methods=('GET', 'POST')) @login_required +@repo_method def add_group_project(repo, username=None): """ Add the specified group from the project. """ @@ -1566,12 +1546,9 @@ def add_group_project(repo, username=None): return flask.redirect( flask.url_for('auth_login', next=flask.request.url)) - repo = pagure.lib.get_project(SESSION, repo, user=username) - - if not repo: - flask.abort(404, 'Project not found') + repo = flask.g.repo - if not is_repo_admin(repo): + if not flask.g.repo_admin: flask.abort( 403, 'You are not allowed to add groups to this project') From 319056e6bc95fbaf8d45a47bea072ff0326f7b04 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Sep 07 2016 10:50:18 +0000 Subject: [PATCH 115/635] Start using @repo_method in the fork controller --- diff --git a/pagure/ui/fork.py b/pagure/ui/fork.py index 6845a86..4023fdc 100644 --- a/pagure/ui/fork.py +++ b/pagure/ui/fork.py @@ -30,7 +30,7 @@ import pagure.lib import pagure.lib.git import pagure.forms from pagure import (APP, SESSION, LOG, login_required, is_repo_admin, - __get_file_in_tree) + __get_file_in_tree, repo_method) @@ -133,6 +133,7 @@ def _get_pr_info(repo_obj, orig_repo, branch_from, branch_to): @APP.route('//pull-requests') @APP.route('/fork///pull-requests/') @APP.route('/fork///pull-requests') +@repo_method def request_pulls(repo, username=None): """ Request pulling the changes from the fork into the project. """ @@ -140,10 +141,7 @@ def request_pulls(repo, username=None): assignee = flask.request.args.get('assignee', None) author = flask.request.args.get('author', None) - repo = pagure.lib.get_project(SESSION, repo, user=username) - - if not repo: - flask.abort(404, 'Project not found') + repo = flask.g.repo if not repo.settings.get('pull_requests', True): flask.abort(404, 'No pull-requests found for this project') @@ -198,7 +196,6 @@ def request_pulls(repo, username=None): status=status, assignee=assignee, author=author, - repo_admin=is_repo_admin(repo), form=pagure.forms.ConfirmationForm(), head=head, ) @@ -208,14 +205,12 @@ def request_pulls(repo, username=None): @APP.route('//pull-request/') @APP.route('/fork///pull-request//') @APP.route('/fork///pull-request/') +@repo_method def request_pull(repo, requestid, username=None): """ Request pulling the changes from the fork into the project. """ - repo = pagure.lib.get_project(SESSION, repo, user=username) - - if not repo: - flask.abort(404, 'Project not found') + repo = flask.g.repo if not repo.settings.get('pull_requests', True): flask.abort(404, 'No pull-requests found for this project') @@ -286,7 +281,6 @@ def request_pull(repo, requestid, username=None): username=username, repo_obj=repo_obj, pull_request=request, - repo_admin=is_repo_admin(request.project), diff_commits=diff_commits, diff=diff, mergeform=form, @@ -295,13 +289,11 @@ def request_pull(repo, requestid, username=None): @APP.route('//pull-request/.patch') @APP.route('/fork///pull-request/.patch') +@repo_method def request_pull_patch(repo, requestid, username=None): """ Returns the commits from the specified pull-request as patches. """ - repo = pagure.lib.get_project(SESSION, repo, user=username) - - if not repo: - flask.abort(404, 'Project not found') + repo = flask.g.repo if not repo.settings.get('pull_requests', True): flask.abort(404, 'No pull-requests found for this project') @@ -372,14 +364,12 @@ def request_pull_patch(repo, requestid, username=None): @APP.route('/fork///pull-request//edit', methods=('GET', 'POST')) @login_required +@repo_method def request_pull_edit(repo, requestid, username=None): """ Edit the title of a pull-request. """ - repo = pagure.lib.get_project(SESSION, repo, user=username) - - if not repo: - flask.abort(404, 'Project not found') + repo = flask.g.repo if not repo.settings.get('pull_requests', True): flask.abort(404, 'No pull-requests found for this project') @@ -437,15 +427,13 @@ def request_pull_edit(repo, requestid, username=None): @APP.route('/fork///pull-request//comment/' '//', methods=('GET', 'POST')) @login_required +@repo_method def pull_request_add_comment( repo, requestid, commit=None, filename=None, row=None, username=None): """ Add a comment to a commit in a pull-request. """ - repo = pagure.lib.get_project(SESSION, repo, user=username) - - if not repo: - flask.abort(404, 'Project not found') + repo = flask.g.repo if not repo.settings.get('pull_requests', True): flask.abort(404, 'No pull-requests found for this project') @@ -520,13 +508,11 @@ def pull_request_add_comment( '/fork///pull-request//comment/drop', methods=['POST']) @login_required +@repo_method def pull_request_drop_comment(repo, requestid, username=None): """ Delete a comment of a pull-request. """ - repo = pagure.lib.get_project(SESSION, repo, user=username) - - if not repo: - flask.abort(404, 'Project not found') + repo = flask.g.repo if not repo.settings.get('pull_requests', True): flask.abort(404, 'No pull-requests found for this project') @@ -585,15 +571,13 @@ def pull_request_drop_comment(repo, requestid, username=None): '/fork///pull-request//comment' '//edit', methods=('GET', 'POST')) @login_required +@repo_method def pull_request_edit_comment(repo, requestid, commentid, username=None): """Edit comment of a pull request """ is_js = flask.request.args.get('js', False) - project = pagure.lib.get_project(SESSION, repo, user=username) - - if not project: - flask.abort(404, 'Project not found') + project = flask.g.repo if not project.settings.get('pull_requests', True): flask.abort(404, 'No pull-requests found for this project') @@ -666,6 +650,7 @@ def pull_request_edit_comment(repo, requestid, commentid, username=None): @APP.route('/fork///pull-request//merge', methods=['POST']) @login_required +@repo_method def merge_request_pull(repo, requestid, username=None): """ Request pulling the changes from the fork into the project. """ @@ -676,10 +661,7 @@ def merge_request_pull(repo, requestid, username=None): return flask.redirect(flask.url_for( 'request_pull', repo=repo, requestid=requestid, username=username)) - repo = pagure.lib.get_project(SESSION, repo, user=username) - - if not repo: - flask.abort(404, 'Project not found') + repo = flask.g.repo if not repo.settings.get('pull_requests', True): flask.abort(404, 'No pull-requests found for this project') @@ -741,28 +723,27 @@ def merge_request_pull(repo, requestid, username=None): @APP.route('/fork///pull-request/cancel/', methods=['POST']) @login_required +@repo_method def cancel_request_pull(repo, requestid, username=None): """ Cancel request pulling request. """ + reponame=repo form = pagure.forms.ConfirmationForm() if form.validate_on_submit(): - repo_obj = pagure.lib.get_project(SESSION, repo, user=username) - - if not repo_obj: - flask.abort(404, 'Project not found') + repo = flask.g.repo - if not repo_obj.settings.get('pull_requests', True): + if not repo.settings.get('pull_requests', True): flask.abort(404, 'No pull-requests found for this project') request = pagure.lib.search_pull_requests( - SESSION, project_id=repo_obj.id, requestid=requestid) + SESSION, project_id=repo.id, requestid=requestid) if not request: flask.abort(404, 'Pull-request not found') - if not is_repo_admin(repo_obj) \ + if not is_repo_admin(repo) \ and not flask.g.fas_user.username == request.user.username: flask.abort( 403, @@ -784,7 +765,7 @@ def cancel_request_pull(repo, requestid, username=None): else: flask.flash('Invalid input submitted', 'error') - return flask.redirect(flask.url_for('view_repo', repo=repo)) + return flask.redirect(flask.url_for('view_repo', repo=reponame)) @APP.route( @@ -793,12 +774,10 @@ def cancel_request_pull(repo, requestid, username=None): '/fork///pull-request//assign', methods=['POST']) @login_required +@repo_method def set_assignee_requests(repo, requestid, username=None): ''' Assign a pull-request. ''' - repo = pagure.lib.get_project(SESSION, repo, user=username) - - if repo is None: - flask.abort(404, 'Project not found') + repo = flask.g.repo if not repo.settings.get('pull_requests', True): flask.abort(404, 'No pull-request allowed on this project') @@ -847,18 +826,16 @@ def set_assignee_requests(repo, requestid, username=None): @APP.route('/do_fork/', methods=['POST']) @APP.route('/do_fork/fork//', methods=['POST']) @login_required +@repo_method def fork_project(repo, username=None): """ Fork the project specified into the user's namespace """ - repo = pagure.lib.get_project(SESSION, repo, user=username) + repo = flask.g.repo form = pagure.forms.ConfirmationForm() if not form.validate_on_submit(): flask.abort(400) - if repo is None: - flask.abort(404) - if pagure.lib.get_project( SESSION, repo.name, user=flask.g.fas_user.username): flask.flash('You had already forked this project') @@ -904,14 +881,13 @@ def fork_project(repo, username=None): @APP.route( '/fork///diff/..', methods=('GET', 'POST')) +@repo_method def new_request_pull(repo, branch_to, branch_from, username=None): """ Request pulling the changes from the fork into the project. """ - repo = pagure.lib.get_project(SESSION, repo, user=username) branch_to = flask.request.values.get('branch_to', branch_to) - if not repo: - flask.abort(404) + repo = flask.g.repo parent = repo if repo.parent: @@ -1040,14 +1016,13 @@ def new_request_pull(repo, branch_to, branch_from, username=None): @APP.route( '/fork///diff/remote', methods=('GET', 'POST')) @login_required +@repo_method def new_remote_request_pull(repo, username=None): """ Request pulling the changes from a remote fork into the project. """ - repo = pagure.lib.get_project(SESSION, repo, user=username) confirm = flask.request.values.get('confirm', False) - if not repo: - flask.abort(404) + repo = flask.g.repo if not repo.settings.get('pull_requests', True): flask.abort(404, 'No pull-request allowed on this project') @@ -1182,18 +1157,16 @@ def new_remote_request_pull(repo, username=None): '/fork_edit/fork///edit//' 'f/', methods=['POST']) @login_required +@repo_method def fork_edit_file(repo, branchname, filename, username=None): """ Fork the project specified and open the specific file to edit """ - repo = pagure.lib.get_project(SESSION, repo, user=username) + repo = flask.g.repo form = pagure.forms.ConfirmationForm() if not form.validate_on_submit(): flask.abort(400) - if repo is None: - flask.abort(404) - if pagure.lib.get_project( SESSION, repo.name, user=flask.g.fas_user.username): flask.flash('You had already forked this project') From 068e0f0e5eccdb2fbcd27deca8c2a05344b5810f Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Sep 07 2016 10:50:18 +0000 Subject: [PATCH 116/635] Rely on flask.g.repo_admin in the templates since we set it in @repo_method --- diff --git a/pagure/templates/repo_info.html b/pagure/templates/repo_info.html index 9ab7e36..35863c9 100644 --- a/pagure/templates/repo_info.html +++ b/pagure/templates/repo_info.html @@ -9,7 +9,7 @@
- {% if repo_admin and branch != head %} + {% if g.repo_admin and branch != head %}
SSH
@@ -177,7 +177,7 @@ git push -u origin master
{% endif %} - {% if authenticated and repo_admin %} + {% if authenticated and g.repo_admin %} {% if config.get('ENABLE_TICKETS', True) and repo.settings.get('issue_tracker', True) %}
Issues GIT URLs
@@ -266,7 +266,7 @@ $(function() { } }); - {% if authenticated and repo_admin %} + {% if authenticated and g.repo_admin %} $.ajax({ url: '{{ url_for("internal_ns.get_pull_request_ready_branch") }}' , type: 'POST', diff --git a/pagure/templates/repo_master.html b/pagure/templates/repo_master.html index de13543..2007267 100644 --- a/pagure/templates/repo_master.html +++ b/pagure/templates/repo_master.html @@ -156,7 +156,7 @@ Issues  - {{ repo.open_tickets if repo_admin else repo.open_tickets_public }} + {{ repo.open_tickets if g.repo_admin else repo.open_tickets_public }} @@ -195,7 +195,7 @@ {% endif %} {% if authenticated %} - {% if repo_admin %} + {% if g.repo_admin %} ");return f},_renderHeader:function(b){if(this.header){this._$header||(this._$header=a('
  • ').prependTo(this.$el));var c=a.isFunction(this.header)?this.header(b):this.header;this._$header.html(c)}},_renderFooter:function(b){if(this.footer){this._$footer||(this._$footer=a('').appendTo(this.$el));var c=a.isFunction(this.footer)?this.footer(b):this.footer;this._$footer.html(c)}},_renderNoResultsMessage:function(b){if(this.noResultsMessage){this._$noResultsMessage||(this._$noResultsMessage=a('
  • ').appendTo(this.$el));var c=a.isFunction(this.noResultsMessage)?this.noResultsMessage(b):this.noResultsMessage;this._$noResultsMessage.html(c)}},_renderContents:function(a){this._$footer?this._$footer.before(a):this.$el.append(a)},_fitToBottom:function(){var a=c.scrollTop()+c.height(),b=this.$el.height();this.$el.position().top+b>a&&(this.completer.$iframe||this.$el.offset({top:a-b}))},_fitToRight:function(){for(var a,b=this.option.rightEdgeOffset,d=this.$el.offset().left,e=this.$el.width(),f=c.width()-b;d+e>f&&(this.$el.offset({left:d-b}),a=this.$el.offset().left,!(a>=d));)d=a},_applyPlacement:function(a){return-1!==this.placement.indexOf("top")?a={top:"auto",bottom:this.$el.parent().height()-a.top+a.lineHeight,left:a.left}:(a.bottom="auto",delete a.lineHeight),-1!==this.placement.indexOf("absleft")?a.left=0:-1!==this.placement.indexOf("absright")&&(a.right=0,a.left="auto"),a}}),a.fn.textcomplete.Dropdown=b,a.extend(a.fn.textcomplete,f)}(a),+function(a){"use strict";function b(b){a.extend(this,b),this.cache&&(this.search=c(this.search))}var c=function(a){var b={};return function(c,d){b[c]?d(b[c]):a.call(this,c,function(a){b[c]=(b[c]||[]).concat(a),d.apply(null,arguments)})}};b.parse=function(c,d){return a.map(c,function(a){var c=new b(a);return c.el=d.el,c.$el=d.$el,c})},a.extend(b.prototype,{match:null,replace:null,search:null,id:null,cache:!1,context:function(){return!0},index:2,template:function(a){return a},idProperty:null}),a.fn.textcomplete.Strategy=b}(a),+function(a){"use strict";function b(){}var c=Date.now||function(){return(new Date).getTime()},d=function(a,b){var d,e,f,g,h,i=function(){var j=c()-g;b>j?d=setTimeout(i,b-j):(d=null,h=a.apply(f,e),f=e=null)};return function(){return f=this,e=arguments,g=c(),d||(d=setTimeout(i,b)),h}};a.extend(b.prototype,{id:null,completer:null,el:null,$el:null,option:null,initialize:function(b,c,e){this.el=b,this.$el=a(b),this.id=c.id+this.constructor.name,this.completer=c,this.option=e,this.option.debounce&&(this._onKeyup=d(this._onKeyup,this.option.debounce)),this._bindEvents()},destroy:function(){this.$el.off("."+this.id),this.$el=this.el=this.completer=null},select:function(){throw new Error("Not implemented")},getCaretPosition:function(){var b=this._getCaretRelativePosition(),c=this.$el.offset(),d=this.option.appendTo;if(d){d instanceof a||(d=a(d));var e=d.offsetParent().offset();c.top-=e.top,c.left-=e.left}return b.top+=c.top,b.left+=c.left,b},focus:function(){this.$el.focus()},_bindEvents:function(){this.$el.on("keyup."+this.id,a.proxy(this._onKeyup,this))},_onKeyup:function(a){this._skipSearch(a)||this.completer.trigger(this.getTextFromHeadToCaret(),!0)},_skipSearch:function(a){switch(a.keyCode){case 9:case 13:case 40:case 38:case 27:return!0}if(a.ctrlKey)switch(a.keyCode){case 78:case 80:return!0}}}),a.fn.textcomplete.Adapter=b}(a),+function(a){"use strict";function b(a,b,c){this.initialize(a,b,c)}a.extend(b.prototype,a.fn.textcomplete.Adapter.prototype,{select:function(b,c,d){var e,f=this.getTextFromHeadToCaret(),g=this.el.value.substring(this.el.selectionEnd),h=c.replace(b,d);"undefined"!=typeof h&&(a.isArray(h)&&(g=h[1]+g,h=h[0]),e=a.isFunction(c.match)?c.match(f):c.match,f=f.replace(e,h),this.$el.val(f+g),this.el.selectionStart=this.el.selectionEnd=f.length)},getTextFromHeadToCaret:function(){return this.el.value.substring(0,this.el.selectionEnd)},_getCaretRelativePosition:function(){var b=a.fn.textcomplete.getCaretCoordinates(this.el,this.el.selectionStart);return{top:b.top+this._calculateLineHeight()-this.$el.scrollTop(),left:b.left-this.$el.scrollLeft(),lineHeight:this._calculateLineHeight()}},_calculateLineHeight:function(){var a=parseInt(this.$el.css("line-height"),10);if(isNaN(a)){var b=this.el.parentNode,c=document.createElement(this.el.nodeName),d=this.el.style;c.setAttribute("style","margin:0px;padding:0px;font-family:"+d.fontFamily+";font-size:"+d.fontSize),c.innerHTML="test",b.appendChild(c),a=c.clientHeight,b.removeChild(c)}return a}}),a.fn.textcomplete.Textarea=b}(a),+function(a){"use strict";function b(b,d,e){this.initialize(b,d,e),a(""+c+"").css({position:"absolute",top:-9999,left:-9999}).insertBefore(b)}var c="吶";a.extend(b.prototype,a.fn.textcomplete.Textarea.prototype,{select:function(b,c,d){var e,f=this.getTextFromHeadToCaret(),g=this.el.value.substring(f.length),h=c.replace(b,d);if("undefined"!=typeof h){a.isArray(h)&&(g=h[1]+g,h=h[0]),e=a.isFunction(c.match)?c.match(f):c.match,f=f.replace(e,h),this.$el.val(f+g),this.el.focus();var i=this.el.createTextRange();i.collapse(!0),i.moveEnd("character",f.length),i.moveStart("character",f.length),i.select()}},getTextFromHeadToCaret:function(){this.el.focus();var a=document.selection.createRange();a.moveStart("character",-this.el.value.length);var b=a.text.split(c);return 1===b.length?b[0]:b[1]}}),a.fn.textcomplete.IETextarea=b}(a),+function(a){"use strict";function b(a,b,c){this.initialize(a,b,c)}a.extend(b.prototype,a.fn.textcomplete.Adapter.prototype,{select:function(b,c,d){var e=this.getTextFromHeadToCaret(),f=this.el.ownerDocument.getSelection(),g=f.getRangeAt(0),h=g.cloneRange();h.selectNodeContents(g.startContainer);var i,j=h.toString(),k=j.substring(g.startOffset),l=c.replace(b,d);if("undefined"!=typeof l){a.isArray(l)&&(k=l[1]+k,l=l[0]),i=a.isFunction(c.match)?c.match(e):c.match,e=e.replace(i,l).replace(/ $/," "),g.selectNodeContents(g.startContainer),g.deleteContents();var m=this.el.ownerDocument.createElement("div");m.innerHTML=e;var n=this.el.ownerDocument.createElement("div");n.innerHTML=k;for(var o,p,q=this.el.ownerDocument.createDocumentFragment();o=m.firstChild;)p=q.appendChild(o);for(;o=n.firstChild;)q.appendChild(o);g.insertNode(q),g.setStartAfter(p),g.collapse(!0),f.removeAllRanges(),f.addRange(g)}},_getCaretRelativePosition:function(){var b=this.el.ownerDocument.getSelection().getRangeAt(0).cloneRange(),c=this.el.ownerDocument.createElement("span");b.insertNode(c),b.selectNodeContents(c),b.deleteContents();var d=a(c),e=d.offset();if(e.left-=this.$el.offset().left,e.top+=d.height()-this.$el.offset().top,e.lineHeight=d.height(),this.completer.$iframe){var f=this.completer.$iframe.offset();e.top+=f.top,e.left+=f.left,e.top-=this.$el.scrollTop()}return d.remove(),e},getTextFromHeadToCaret:function(){var a=this.el.ownerDocument.getSelection().getRangeAt(0),b=a.cloneRange();return b.selectNodeContents(a.startContainer),b.toString().substring(0,a.startOffset)}}),a.fn.textcomplete.ContentEditable=b}(a),+function(a){"use strict";function b(a,b,c){this.initialize(a,b,c)}a.extend(b.prototype,a.fn.textcomplete.ContentEditable.prototype,{_bindEvents:function(){var b=this;CKEDITOR.instances.issue_notes.on("key",function(a){var c=a.data;return b._onKeyup(c),b.completer.dropdown.shown&&b._skipSearch(c)?!1:void 0},null,null,1),this.$el.on("keyup."+this.id,a.proxy(this._onKeyup,this))}}),a.fn.textcomplete.CKEditor=b}(a),function(a){function b(a,b,f){if(!d)throw new Error("textarea-caret-position#getCaretCoordinates should only be called in a browser");var g=f&&f.debug||!1;if(g){var h=document.querySelector("#input-textarea-caret-position-mirror-div");h&&h.parentNode.removeChild(h)}var i=document.createElement("div");i.id="input-textarea-caret-position-mirror-div",document.body.appendChild(i);var j=i.style,k=window.getComputedStyle?getComputedStyle(a):a.currentStyle;j.whiteSpace="pre-wrap","INPUT"!==a.nodeName&&(j.wordWrap="break-word"),j.position="absolute",g||(j.visibility="hidden"),c.forEach(function(a){j[a]=k[a]}),e?a.scrollHeight>parseInt(k.height)&&(j.overflowY="scroll"):j.overflow="hidden",i.textContent=a.value.substring(0,b),"INPUT"===a.nodeName&&(i.textContent=i.textContent.replace(/\s/g," "));var l=document.createElement("span");l.textContent=a.value.substring(b)||".",i.appendChild(l);var m={top:l.offsetTop+parseInt(k.borderTopWidth),left:l.offsetLeft+parseInt(k.borderLeftWidth)};return g?l.style.backgroundColor="#aaa":document.body.removeChild(i),m}var c=["direction","boxSizing","width","height","overflowX","overflowY","borderTopWidth","borderRightWidth","borderBottomWidth","borderLeftWidth","borderStyle","paddingTop","paddingRight","paddingBottom","paddingLeft","fontStyle","fontVariant","fontWeight","fontStretch","fontSize","fontSizeAdjust","lineHeight","fontFamily","textAlign","textTransform","textIndent","textDecoration","letterSpacing","wordSpacing","tabSize","MozTabSize"],d="undefined"!=typeof window,e=d&&null!=window.mozInnerScreenX;a.fn.textcomplete.getCaretCoordinates=b}(a),a}); +//# sourceMappingURL=dist/jquery.textcomplete.min.map \ No newline at end of file diff --git a/pagure/static/emoji/jquery.textcomplete.js b/pagure/static/emoji/jquery.textcomplete.js new file mode 120000 index 0000000..a2bee68 --- /dev/null +++ b/pagure/static/emoji/jquery.textcomplete.js @@ -0,0 +1 @@ +jquery.textcomplete-1.7.1.js \ No newline at end of file diff --git a/pagure/static/emoji/jquery.textcomplete.min.js b/pagure/static/emoji/jquery.textcomplete.min.js deleted file mode 100644 index d7aed69..0000000 --- a/pagure/static/emoji/jquery.textcomplete.min.js +++ /dev/null @@ -1,4 +0,0 @@ -/*! jquery-textcomplete - v0.3.2 - 2014-09-16 */if("undefined"==typeof jQuery)throw new Error("jQuery.textcomplete requires jQuery");+function(a){"use strict";var b=function(a){console.warn&&console.warn(a)};a.fn.textcomplete=function(c,d){var e=Array.prototype.slice.call(arguments);return this.each(function(){var f=a(this),g=f.data("textComplete");g||(g=new a.fn.textcomplete.Completer(this,d||{}),f.data("textComplete",g)),"string"==typeof c?(e.shift(),g[c].apply(g,e)):(a.each(c,function(c){a.each(["header","footer","placement","maxCount"],function(a){c[a]&&(g.option[a]=c[a],b(a+"as a strategy param is deplicated. Use option."),delete c[a])})}),g.register(a.fn.textcomplete.Strategy.parse(c)))})}}(jQuery),+function(a){"use strict";function b(c,d){if(this.$el=a(c),this.id="textcomplete"+e++,this.strategies=[],this.views=[],this.option=a.extend({},b.DEFAULTS,d),!this.$el.is("textarea")&&!c.isContentEditable)throw new Error("textcomplete must be called to a Textarea or a ContentEditable.");if(c===document.activeElement)this.initialize();else{var f=this;this.$el.one("focus."+this.id,function(){f.initialize()})}}var c=function(a){var b,c;return function(){var d=Array.prototype.slice.call(arguments);if(b)return c=d,void 0;b=!0;var e=this;d.unshift(function f(){if(c){var d=c;c=void 0,d.unshift(f),a.apply(e,d)}else b=!1}),a.apply(this,d)}},d=function(a){return"[object String]"===Object.prototype.toString.call(a)},e=0;b.DEFAULTS={appendTo:a("body"),zIndex:"100"},a.extend(b.prototype,{id:null,option:null,strategies:null,adapter:null,dropdown:null,$el:null,initialize:function(){var b=this.$el.get(0);this.dropdown=new a.fn.textcomplete.Dropdown(b,this,this.option);var c,d;this.option.adapter?c=this.option.adapter:(d=this.$el.is("textarea")?"number"==typeof b.selectionEnd?"Textarea":"IETextarea":"ContentEditable",c=a.fn.textcomplete[d]),this.adapter=new c(b,this,this.option)},destroy:function(){this.$el.off("."+this.id),this.adapter.destroy(),this.dropdown.destroy(),this.$el=this.adapter=this.dropdown=null},trigger:function(a,b){this.dropdown||this.initialize();var c=this._extractSearchQuery(a);if(c.length){var d=c[1];if(b&&this._term===d)return;this._term=d,this._search.apply(this,c)}else this._term=null,this.dropdown.deactivate()},fire:function(a){return this.$el.trigger(a),this},register:function(a){Array.prototype.push.apply(this.strategies,a)},select:function(a,b){this.adapter.select(a,b),this.fire("change").fire("textComplete:select",a,b),this.adapter.focus()},_clearAtNext:!0,_term:null,_extractSearchQuery:function(a){for(var b=0;b').css({display:"none",left:0,position:"absolute",zIndex:b.zIndex}).appendTo(c)),d}}),a.extend(b.prototype,{$el:null,$inputEl:null,completer:null,footer:null,header:null,id:null,maxCount:10,placement:"",shown:!1,data:[],destroy:function(){this.$el.off("."+this.id),this.$inputEl.off("."+this.id),this.clear(),this.$el=this.$inputEl=this.completer=null,delete d[this.id]},render:function(b){var c=this._buildContents(b),d=a.map(this.data,function(a){return a.value});this.data.length?(this._renderHeader(d),this._renderFooter(d),c&&(this._renderContents(c),this._activateIndexedItem()),this._setScroll()):this.shown&&this.deactivate()},setPosition:function(a){return this.$el.css(this._applyPlacement(a)),this},clear:function(){this.$el.html(""),this.data=[],this._index=0,this._$header=this._$footer=null},activate:function(){return this.shown||(this.clear(),this.$el.show(),this.completer.fire("textComplete:show"),this.shown=!0),this},deactivate:function(){return this.shown&&(this.$el.hide(),this.completer.fire("textComplete:hide"),this.shown=!1),this},isUp:function(a){return 38===a.keyCode||a.ctrlKey&&80===a.keyCode},isDown:function(a){return 40===a.keyCode||a.ctrlKey&&78===a.keyCode},isEnter:function(a){var b=a.ctrlKey||a.altKey||a.metaKey||a.shiftKey;return!b&&(13===a.keyCode||9===a.keyCode)},isPageup:function(a){return 33===a.keyCode},isPagedown:function(a){return 34===a.keyCode},_data:null,_index:null,_$header:null,_$footer:null,_bindEvents:function(){this.$el.on("mousedown."+this.id,".textcomplete-item",a.proxy(this._onClick,this)),this.$el.on("mouseover."+this.id,".textcomplete-item",a.proxy(this._onMouseover,this)),this.$inputEl.on("keydown."+this.id,a.proxy(this._onKeydown,this))},_onClick:function(b){var c=a(b.target);b.preventDefault(),b.originalEvent.keepTextCompleteDropdown=this.id,c.hasClass("textcomplete-item")||(c=c.closest(".textcomplete-item"));var d=this.data[parseInt(c.data("index"),10)];this.completer.select(d.value,d.strategy);var e=this;setTimeout(function(){e.deactivate()},0)},_onMouseover:function(b){var c=a(b.target);b.preventDefault(),c.hasClass("textcomplete-item")||(c=c.closest(".textcomplete-item")),this._index=parseInt(c.data("index"),10),this._activateIndexedItem()},_onKeydown:function(a){this.shown&&(this.isUp(a)?(a.preventDefault(),this._up()):this.isDown(a)?(a.preventDefault(),this._down()):this.isEnter(a)?(a.preventDefault(),this._enter()):this.isPageup(a)?(a.preventDefault(),this._pageup()):this.isPagedown(a)&&(a.preventDefault(),this._pagedown()))},_up:function(){0===this._index?this._index=this.data.length-1:this._index-=1,this._activateIndexedItem(),this._setScroll()},_down:function(){this._index===this.data.length-1?this._index=0:this._index+=1,this._activateIndexedItem(),this._setScroll()},_enter:function(){var a=this.data[parseInt(this._getActiveElement().data("index"),10)];this.completer.select(a.value,a.strategy),this._setScroll()},_pageup:function(){var b=0,c=this._getActiveElement().position().top-this.$el.innerHeight();this.$el.children().each(function(d){return a(this).position().top+a(this).outerHeight()>c?(b=d,!1):void 0}),this._index=b,this._activateIndexedItem(),this._setScroll()},_pagedown:function(){var b=this.data.length-1,c=this._getActiveElement().position().top+this.$el.innerHeight();this.$el.children().each(function(d){return a(this).position().top>c?(b=d,!1):void 0}),this._index=b,this._activateIndexedItem(),this._setScroll()},_activateIndexedItem:function(){this.$el.find(".textcomplete-item.active").removeClass("active"),this._getActiveElement().addClass("active")},_getActiveElement:function(){return this.$el.children(".textcomplete-item:nth("+this._index+")")},_setScroll:function(){var a=this._getActiveElement(),b=a.position().top,c=a.outerHeight(),d=this.$el.innerHeight(),e=this.$el.scrollTop();0===this._index||this._index==this.data.length-1||0>b?this.$el.scrollTop(b+e):b+c>d&&this.$el.scrollTop(b+c+e-d)},_buildContents:function(a){var b,d,e,f="";for(d=0;d',f+=b.strategy.template(b.value),f+="");return f},_renderHeader:function(b){if(this.header){this._$header||(this._$header=a('
  • ').prependTo(this.$el));var c=a.isFunction(this.header)?this.header(b):this.header;this._$header.html(c)}},_renderFooter:function(b){if(this.footer){this._$footer||(this._$footer=a('').appendTo(this.$el));var c=a.isFunction(this.footer)?this.footer(b):this.footer;this._$footer.html(c)}},_renderContents:function(a){this._$footer?this._$footer.before(a):this.$el.append(a)},_applyPlacement:function(a){return-1!==this.placement.indexOf("top")?a={top:"auto",bottom:this.$el.parent().height()-a.top+a.lineHeight,left:a.left}:(a.bottom="auto",delete a.lineHeight),-1!==this.placement.indexOf("absleft")?a.left=0:-1!==this.placement.indexOf("absright")&&(a.right=0,a.left="auto"),a}}),a.fn.textcomplete.Dropdown=b}(jQuery),+function(a){"use strict";function b(b){a.extend(this,b),this.cache&&(this.search=c(this.search))}var c=function(a){var b={};return function(c,d){b[c]?d(b[c]):a.call(this,c,function(a){b[c]=(b[c]||[]).concat(a),d.apply(null,arguments)})}};b.parse=function(c){return a.map(c,function(a){return new b(a)})},a.extend(b.prototype,{match:null,replace:null,search:null,cache:!1,context:function(){return!0},index:2,template:function(a){return a},idProperty:null}),a.fn.textcomplete.Strategy=b}(jQuery),+function(a){"use strict";function b(){}var c=Date.now||function(){return(new Date).getTime()},d=function(a,b){var d,e,f,g,h,i=function(){var j=c()-g;b>j?d=setTimeout(i,b-j):(d=null,h=a.apply(f,e),f=e=null)};return function(){return f=this,e=arguments,g=c(),d||(d=setTimeout(i,b)),h}};a.extend(b.prototype,{id:null,completer:null,el:null,$el:null,option:null,initialize:function(b,c,e){this.el=b,this.$el=a(b),this.id=c.id+this.constructor.name,this.completer=c,this.option=e,this.option.debounce&&(this._onKeyup=d(this._onKeyup,this.option.debounce)),this._bindEvents()},destroy:function(){this.$el.off("."+this.id),this.$el=this.el=this.completer=null},select:function(){throw new Error("Not implemented")},getCaretPosition:function(){var a=this._getCaretRelativePosition(),b=this.$el.offset();return a.top+=b.top,a.left+=b.left,a},focus:function(){this.$el.focus()},_bindEvents:function(){this.$el.on("keyup."+this.id,a.proxy(this._onKeyup,this))},_onKeyup:function(a){this._skipSearch(a)||this.completer.trigger(this._getTextFromHeadToCaret(),!0)},_skipSearch:function(a){switch(a.keyCode){case 40:case 38:return!0}if(a.ctrlKey)switch(a.keyCode){case 78:case 80:return!0}}}),a.fn.textcomplete.Adapter=b}(jQuery),+function(a){"use strict";function b(a,b,c){this.initialize(a,b,c)}b.DIV_PROPERTIES={left:-9999,position:"absolute",top:0,whiteSpace:"pre-wrap"},b.COPY_PROPERTIES=["border-width","font-family","font-size","font-style","font-variant","font-weight","height","letter-spacing","word-spacing","line-height","text-decoration","text-align","width","padding-top","padding-right","padding-bottom","padding-left","margin-top","margin-right","margin-bottom","margin-left","border-style","box-sizing","tab-size"],a.extend(b.prototype,a.fn.textcomplete.Adapter.prototype,{select:function(b,c){var d=this._getTextFromHeadToCaret(),e=this.el.value.substring(this.el.selectionEnd),f=c.replace(b);a.isArray(f)&&(e=f[1]+e,f=f[0]),d=d.replace(c.match,f),this.$el.val(d+e),this.el.selectionStart=this.el.selectionEnd=d.length},_getCaretRelativePosition:function(){var b=a("
    ").css(this._copyCss()).text(this._getTextFromHeadToCaret()),c=a("").text(".").appendTo(b);this.$el.before(b);var d=c.position();return d.top+=c.height()-this.$el.scrollTop(),d.lineHeight=c.height(),b.remove(),d},_copyCss:function(){return a.extend({overflow:this.el.scrollHeight>this.el.offsetHeight?"scroll":"auto"},b.DIV_PROPERTIES,this._getStyles())},_getStyles:function(a){var c=a("
    ").css(["color"]).color;return"undefined"!=typeof c?function(){return this.$el.css(b.COPY_PROPERTIES)}:function(){var c=this.$el,d={};return a.each(b.COPY_PROPERTIES,function(a,b){d[b]=c.css(b)}),d}}(a),_getTextFromHeadToCaret:function(){return this.el.value.substring(0,this.el.selectionEnd)}}),a.fn.textcomplete.Textarea=b}(jQuery),+function(a){"use strict";function b(b,d,e){this.initialize(b,d,e),a(""+c+"").css({position:"absolute",top:-9999,left:-9999}).insertBefore(b)}var c="吶";a.extend(b.prototype,a.fn.textcomplete.Textarea.prototype,{select:function(b,c){var d=this._getTextFromHeadToCaret(),e=this.el.value.substring(d.length),f=c.replace(b);a.isArray(f)&&(e=f[1]+e,f=f[0]),d=d.replace(c.match,f),this.$el.val(d+e),this.el.focus();var g=this.el.createTextRange();g.collapse(!0),g.moveEnd("character",d.length),g.moveStart("character",d.length),g.select()},_getTextFromHeadToCaret:function(){this.el.focus();var a=document.selection.createRange();a.moveStart("character",-this.el.value.length);var b=a.text.split(c);return 1===b.length?b[0]:b[1]}}),a.fn.textcomplete.IETextarea=b}(jQuery),+function(a){"use strict";function b(a,b,c){this.initialize(a,b,c)}a.extend(b.prototype,a.fn.textcomplete.Adapter.prototype,{select:function(b,c){var d=this._getTextFromHeadToCaret(),e=window.getSelection(),f=e.getRangeAt(0),g=f.cloneRange();g.selectNodeContents(f.startContainer);var h=g.toString(),i=h.substring(f.startOffset),j=c.replace(b);a.isArray(j)&&(i=j[1]+i,j=j[0]),d=d.replace(c.match,j),f.selectNodeContents(f.startContainer),f.deleteContents();var k=document.createTextNode(d+i);f.insertNode(k),f.setStart(k,d.length),f.collapse(!0),e.removeAllRanges(),e.addRange(f)},_getCaretRelativePosition:function(){var b=window.getSelection().getRangeAt(0).cloneRange(),c=document.createElement("span");b.insertNode(c),b.selectNodeContents(c),b.deleteContents();var d=a(c),e=d.offset();e.left-=this.$el.offset().left,e.top+=d.height()-this.$el.offset().top,e.lineHeight=d.height();var f=this.$el.attr("dir")||this.$el.css("direction");return"rtl"===f&&(e.left-=this.listView.$el.width()),e},_getTextFromHeadToCaret:function(){var a=window.getSelection().getRangeAt(0),b=a.cloneRange();return b.selectNodeContents(a.startContainer),b.toString().substring(0,a.startOffset)}}),a.fn.textcomplete.ContentEditable=b}(jQuery); -/* - //@ sourceMappingURL=dist/jquery.textcomplete.min.map - */ \ No newline at end of file diff --git a/pagure/static/emoji/jquery.textcomplete.min.js b/pagure/static/emoji/jquery.textcomplete.min.js new file mode 120000 index 0000000..8406123 --- /dev/null +++ b/pagure/static/emoji/jquery.textcomplete.min.js @@ -0,0 +1 @@ +jquery.textcomplete-1.7.1.min.js \ No newline at end of file diff --git a/pagure/static/hack_fonts/css/hack-extended-2.020.css b/pagure/static/hack_fonts/css/hack-extended-2.020.css new file mode 100644 index 0000000..0f98461 --- /dev/null +++ b/pagure/static/hack_fonts/css/hack-extended-2.020.css @@ -0,0 +1,38 @@ +/*! + * Hack v2.020 - https://sourcefoundry.org/hack/ + * Licenses - Fonts: Hack Open Font License + Bitstream Vera license, CSS: MIT License + */ +/* FONT PATHS + * -------------------------- */ +@font-face { + font-family: 'Hack'; + src: url('../fonts/eot/hack-regular-webfont.eot?v=2.020'); + src: url('../fonts/eot/hack-regular-webfont.eot?#iefix&v=2.020') format('embedded-opentype'), url('../fonts/woff2/hack-regular-webfont.woff2?v=2.020') format('woff2'), url('../fonts/woff/hack-regular-webfont.woff?v=2.020') format('woff'), url('../fonts/web-ttf/hack-regular-webfont.ttf?v=2.020') format('truetype'); + font-weight: 400; + font-style: normal; +} + +@font-face { + font-family: 'Hack'; + src: url('../fonts/eot/hack-bold-webfont.eot?v=2.020'); + src: url('../fonts/eot/hack-bold-webfont.eot?#iefix&v=2.020') format('embedded-opentype'), url('../fonts/woff2/hack-bold-webfont.woff2?v=2.020') format('woff2'), url('../fonts/woff/hack-bold-webfont.woff?v=2.020') format('woff'), url('../fonts/web-ttf/hack-bold-webfont.ttf?v=2.020') format('truetype'); + font-weight: 700; + font-style: normal; +} + +@font-face { + font-family: 'Hack'; + src: url('../fonts/eot/hack-italic-webfont.eot?v=2.020'); + src: url('../fonts/eot/hack-italic-webfont.eot?#iefix&v=2.020') format('embedded-opentype'), url('../fonts/woff2/hack-italic-webfont.woff2?v=2.020') format('woff2'), url('../fonts/woff/hack-italic-webfont.woff?v=2.020') format('woff'), url('../fonts/web-ttf/hack-italic-webfont.ttf?v=2.020') format('truetype'); + font-weight: 400; + font-style: italic; +} + +@font-face { + font-family: 'Hack'; + src: url('../fonts/eot/hack-bolditalic-webfont.eot?v=2.020'); + src: url('../fonts/eot/hack-bolditalic-webfont.eot?#iefix&v=2.020') format('embedded-opentype'), url('../fonts/woff2/hack-bolditalic-webfont.woff2?v=2.020') format('woff2'), url('../fonts/woff/hack-bolditalic-webfont.woff?v=2.020') format('woff'), url('../fonts/web-ttf/hack-bolditalic-webfont.ttf?v=2.020') format('truetype'); + font-weight: 700; + font-style: italic; +} + diff --git a/pagure/static/hack_fonts/css/hack-extended-2.020.min.css b/pagure/static/hack_fonts/css/hack-extended-2.020.min.css new file mode 100644 index 0000000..11f9859 --- /dev/null +++ b/pagure/static/hack_fonts/css/hack-extended-2.020.min.css @@ -0,0 +1,4 @@ +/*! + * Hack v2.020 - https://sourcefoundry.org/hack/ + * Licenses - Fonts: Hack Open Font License + Bitstream Vera license, CSS: MIT License + */@font-face{font-family:'Hack';src:url('../fonts/eot/hack-regular-webfont.eot?v=2.020');src:url('../fonts/eot/hack-regular-webfont.eot?#iefix&v=2.020') format('embedded-opentype'),url('../fonts/woff2/hack-regular-webfont.woff2?v=2.020') format('woff2'),url('../fonts/woff/hack-regular-webfont.woff?v=2.020') format('woff'),url('../fonts/web-ttf/hack-regular-webfont.ttf?v=2.020') format('truetype');font-weight:400;font-style:normal}@font-face{font-family:'Hack';src:url('../fonts/eot/hack-bold-webfont.eot?v=2.020');src:url('../fonts/eot/hack-bold-webfont.eot?#iefix&v=2.020') format('embedded-opentype'),url('../fonts/woff2/hack-bold-webfont.woff2?v=2.020') format('woff2'),url('../fonts/woff/hack-bold-webfont.woff?v=2.020') format('woff'),url('../fonts/web-ttf/hack-bold-webfont.ttf?v=2.020') format('truetype');font-weight:700;font-style:normal}@font-face{font-family:'Hack';src:url('../fonts/eot/hack-italic-webfont.eot?v=2.020');src:url('../fonts/eot/hack-italic-webfont.eot?#iefix&v=2.020') format('embedded-opentype'),url('../fonts/woff2/hack-italic-webfont.woff2?v=2.020') format('woff2'),url('../fonts/woff/hack-italic-webfont.woff?v=2.020') format('woff'),url('../fonts/web-ttf/hack-italic-webfont.ttf?v=2.020') format('truetype');font-weight:400;font-style:italic}@font-face{font-family:'Hack';src:url('../fonts/eot/hack-bolditalic-webfont.eot?v=2.020');src:url('../fonts/eot/hack-bolditalic-webfont.eot?#iefix&v=2.020') format('embedded-opentype'),url('../fonts/woff2/hack-bolditalic-webfont.woff2?v=2.020') format('woff2'),url('../fonts/woff/hack-bolditalic-webfont.woff?v=2.020') format('woff'),url('../fonts/web-ttf/hack-bolditalic-webfont.ttf?v=2.020') format('truetype');font-weight:700;font-style:italic} \ No newline at end of file diff --git a/pagure/static/hack_fonts/css/hack-extended.css b/pagure/static/hack_fonts/css/hack-extended.css new file mode 120000 index 0000000..f08cb16 --- /dev/null +++ b/pagure/static/hack_fonts/css/hack-extended.css @@ -0,0 +1 @@ +hack-extended-2.020.css \ No newline at end of file diff --git a/pagure/static/hack_fonts/css/hack-extended.min.css b/pagure/static/hack_fonts/css/hack-extended.min.css deleted file mode 100644 index d8abefd..0000000 --- a/pagure/static/hack_fonts/css/hack-extended.min.css +++ /dev/null @@ -1,4 +0,0 @@ -/*! - * Hack v2.018 - https://sourcefoundry.org/hack/ - * Licenses - Fonts: Hack Open Font License + Bitstream Vera license, CSS: MIT License - */@font-face{font-family:'Hack';src:url('../fonts/eot/hack-regular-webfont.eot?v=2.018');src:url('../fonts/eot/hack-regular-webfont.eot?#iefix&v=2.018') format('embedded-opentype'),url('../fonts/woff2/hack-regular-webfont.woff2?v=2.018') format('woff2'),url('../fonts/woff/hack-regular-webfont.woff?v=2.018') format('woff'),url('../fonts/web-ttf/hack-regular-webfont.ttf?v=2.018') format('truetype'),url('../fonts/svg/hack-regular-webfont.svg?v=2.018#hackregular') format('svg');font-weight:400;font-style:normal}@font-face{font-family:'Hack';src:url('../fonts/eot/hack-bold-webfont.eot?v=2.018');src:url('../fonts/eot/hack-bold-webfont.eot?#iefix&v=2.018') format('embedded-opentype'),url('../fonts/woff2/hack-bold-webfont.woff2?v=2.018') format('woff2'),url('../fonts/woff/hack-bold-webfont.woff?v=2.018') format('woff'),url('../fonts/web-ttf/hack-bold-webfont.ttf?v=2.018') format('truetype'),url('../fonts/svg/hack-bold-webfont.svg?v=2.018#hackbold') format('svg');font-weight:700;font-style:normal}@font-face{font-family:'Hack';src:url('../fonts/eot/hack-italic-webfont.eot?v=2.018');src:url('../fonts/eot/hack-italic-webfont.eot?#iefix&v=2.018') format('embedded-opentype'),url('../fonts/woff2/hack-italic-webfont.woff2?v=2.018') format('woff2'),url('../fonts/woff/hack-italic-webfont.woff?v=2.018') format('woff'),url('../fonts/web-ttf/hack-italic-webfont.ttf?v=2.018') format('truetype'),url('../fonts/svg/hack-italic-webfont.svg?v=2.018#hackitalic') format('svg');font-weight:400;font-style:italic}@font-face{font-family:'Hack';src:url('../fonts/eot/hack-bolditalic-webfont.eot?v=2.018');src:url('../fonts/eot/hack-bolditalic-webfont.eot?#iefix&v=2.018') format('embedded-opentype'),url('../fonts/woff2/hack-bolditalic-webfont.woff2?v=2.018') format('woff2'),url('../fonts/woff/hack-bolditalic-webfont.woff?v=2.018') format('woff'),url('../fonts/web-ttf/hack-bolditalic-webfont.ttf?v=2.018') format('truetype'),url('../fonts/svg/hack-bolditalic-webfont.svg?v=2.010#hackbolditalic') format('svg');font-weight:700;font-style:italic} \ No newline at end of file diff --git a/pagure/static/hack_fonts/css/hack-extended.min.css b/pagure/static/hack_fonts/css/hack-extended.min.css new file mode 120000 index 0000000..ceb0a59 --- /dev/null +++ b/pagure/static/hack_fonts/css/hack-extended.min.css @@ -0,0 +1 @@ +hack-extended-2.020.min.css \ No newline at end of file diff --git a/pagure/static/jdenticon-1.3.2.js b/pagure/static/jdenticon-1.3.2.js new file mode 100644 index 0000000..c56ee80 --- /dev/null +++ b/pagure/static/jdenticon-1.3.2.js @@ -0,0 +1,801 @@ +/** + * Jdenticon 1.3.2 + * http://jdenticon.com + * + * Built: 2015-10-10T11:55:57.451Z + * + * Copyright (c) 2014-2015 Daniel Mester Pirttijärvi + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgment in the product documentation would be + * appreciated but is not required. + * + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * + * 3. This notice may not be removed or altered from any source distribution. + * + */ + +/*jslint bitwise: true */ + +(function (global, name, factory) { + var jQuery = global["jQuery"], + jdenticon = factory(global, jQuery); + + // Node.js + if (typeof module !== "undefined" && "exports" in module) { + module["exports"] = jdenticon; + } + // RequireJS + else if (typeof define === "function" && define["amd"]) { + define([], function () { return jdenticon; }); + } + // No module loader + else { + global[name] = jdenticon; + } +})(this, "jdenticon", function (global, jQuery) { + "use strict"; + + + + + /** + * Represents a point. + * @private + * @constructor + */ + function Point(x, y) { + this.x = x; + this.y = y; + }; + + + /** + * Translates and rotates a point before being passed on to the canvas context. This was previously done by the canvas context itself, + * but this caused a rendering issue in Chrome on sizes > 256 where the rotation transformation of inverted paths was not done properly. + * @param {number} x The x-coordinate of the upper left corner of the transformed rectangle. + * @param {number} y The y-coordinate of the upper left corner of the transformed rectangle. + * @param {number} size The size of the transformed rectangle. + * @param {number} rotation Rotation specified as 0 = 0 rad, 1 = 0.5π rad, 2 = π rad, 3 = 1.5π rad + * @private + * @constructor + */ + function Transform(x, y, size, rotation) { + this._x = x; + this._y = y; + this._size = size; + this._rotation = rotation; + } + Transform.prototype = { + /** + * Transforms the specified point based on the translation and rotation specification for this Transform. + * @param {number} x x-coordinate + * @param {number} y y-coordinate + * @param {number=} w The width of the transformed rectangle. If greater than 0, this will ensure the returned point is of the upper left corner of the transformed rectangle. + * @param {number=} h The height of the transformed rectangle. If greater than 0, this will ensure the returned point is of the upper left corner of the transformed rectangle. + */ + transformPoint: function (x, y, w, h) { + var right = this._x + this._size, + bottom = this._y + this._size; + return this._rotation === 1 ? new Point(right - y - (h || 0), this._y + x) : + this._rotation === 2 ? new Point(right - x - (w || 0), bottom - y - (h || 0)) : + this._rotation === 3 ? new Point(this._x + y, bottom - x - (w || 0)) : + new Point(this._x + x, this._y + y); + } + }; + Transform.noTransform = new Transform(0, 0, 0, 0); + + + + /** + * Provides helper functions for rendering common basic shapes. + * @private + * @constructor + */ + function Graphics(renderer) { + this._renderer = renderer; + this._transform = Transform.noTransform; + } + Graphics.prototype = { + /** + * Adds a polygon to the underlying renderer. + * @param {Array} points The points of the polygon clockwise on the format [ x0, y0, x1, y1, ..., xn, yn ] + * @param {boolean=} invert Specifies if the polygon will be inverted. + */ + addPolygon: function (points, invert) { + var di = invert ? -2 : 2, + transform = this._transform, + transformedPoints = [], + i; + + for (i = invert ? points.length - 2 : 0; i < points.length && i >= 0; i += di) { + transformedPoints.push(transform.transformPoint(points[i], points[i + 1])); + } + + this._renderer.addPolygon(transformedPoints); + }, + + /** + * Adds a polygon to the underlying renderer. + * Source: http://stackoverflow.com/a/2173084 + * @param {number} x The x-coordinate of the upper left corner of the rectangle holding the entire ellipse. + * @param {number} y The y-coordinate of the upper left corner of the rectangle holding the entire ellipse. + * @param {number} size The size of the ellipse. + * @param {boolean=} invert Specifies if the ellipse will be inverted. + */ + addCircle: function (x, y, size, invert) { + var p = this._transform.transformPoint(x, y, size, size); + this._renderer.addCircle(p, size, invert); + }, + + /** + * Adds a rectangle to the underlying renderer. + * @param {number} x The x-coordinate of the upper left corner of the rectangle. + * @param {number} y The y-coordinate of the upper left corner of the rectangle. + * @param {number} w The width of the rectangle. + * @param {number} h The height of the rectangle. + * @param {boolean=} invert Specifies if the rectangle will be inverted. + */ + addRectangle: function (x, y, w, h, invert) { + this.addPolygon([ + x, y, + x + w, y, + x + w, y + h, + x, y + h + ], invert); + }, + + /** + * Adds a right triangle to the underlying renderer. + * @param {number} x The x-coordinate of the upper left corner of the rectangle holding the triangle. + * @param {number} y The y-coordinate of the upper left corner of the rectangle holding the triangle. + * @param {number} w The width of the triangle. + * @param {number} h The height of the triangle. + * @param {number} r The rotation of the triangle (clockwise). 0 = right corner of the triangle in the lower left corner of the bounding rectangle. + * @param {boolean=} invert Specifies if the triangle will be inverted. + */ + addTriangle: function (x, y, w, h, r, invert) { + var points = [ + x + w, y, + x + w, y + h, + x, y + h, + x, y + ]; + points.splice(((r || 0) % 4) * 2, 2); + this.addPolygon(points, invert); + }, + + /** + * Adds a rhombus to the underlying renderer. + * @param {number} x The x-coordinate of the upper left corner of the rectangle holding the rhombus. + * @param {number} y The y-coordinate of the upper left corner of the rectangle holding the rhombus. + * @param {number} w The width of the rhombus. + * @param {number} h The height of the rhombus. + * @param {boolean=} invert Specifies if the rhombus will be inverted. + */ + addRhombus: function (x, y, w, h, invert) { + this.addPolygon([ + x + w / 2, y, + x + w, y + h / 2, + x + w / 2, y + h, + x, y + h / 2 + ], invert); + } + }; + + + + + var shapes = { + center: [ + /** @param {Graphics} g */ + function (g, cell, index) { + var k = cell * 0.42; + g.addPolygon([ + 0, 0, + cell, 0, + cell, cell - k * 2, + cell - k, cell, + 0, cell + ]); + }, + /** @param {Graphics} g */ + function (g, cell, index) { + var w = 0 | (cell * 0.5), + h = 0 | (cell * 0.8); + g.addTriangle(cell - w, 0, w, h, 2); + }, + /** @param {Graphics} g */ + function (g, cell, index) { + var s = 0 | (cell / 3); + g.addRectangle(s, s, cell - s, cell - s); + }, + /** @param {Graphics} g */ + function (g, cell, index) { + var inner = 0 | (cell * 0.1), + outer = 0 | (cell * 0.25); + g.addRectangle(outer, outer, cell - inner - outer, cell - inner - outer); + }, + /** @param {Graphics} g */ + function (g, cell, index) { + var m = 0 | (cell * 0.15), + s = 0 | (cell * 0.5); + g.addCircle(cell - s - m, cell - s - m, s); + }, + /** @param {Graphics} g */ + function (g, cell, index) { + var inner = cell * 0.1, + outer = inner * 4; + + g.addRectangle(0, 0, cell, cell); + g.addPolygon([ + outer, outer, + cell - inner, outer, + outer + (cell - outer - inner) / 2, cell - inner + ], true); + }, + /** @param {Graphics} g */ + function (g, cell, index) { + g.addPolygon([ + 0, 0, + cell, 0, + cell, cell * 0.7, + cell * 0.4, cell * 0.4, + cell * 0.7, cell, + 0, cell + ]); + }, + /** @param {Graphics} g */ + function (g, cell, index) { + g.addTriangle(cell / 2, cell / 2, cell / 2, cell / 2, 3); + }, + /** @param {Graphics} g */ + function (g, cell, index) { + g.addRectangle(0, 0, cell, cell / 2); + g.addRectangle(0, cell / 2, cell / 2, cell / 2); + g.addTriangle(cell / 2, cell / 2, cell / 2, cell / 2, 1); + }, + /** @param {Graphics} g */ + function (g, cell, index) { + var inner = 0 | (cell * 0.14), + outer = 0 | (cell * 0.35); + g.addRectangle(0, 0, cell, cell); + g.addRectangle(outer, outer, cell - outer - inner, cell - outer - inner, true); + }, + /** @param {Graphics} g */ + function (g, cell, index) { + var inner = cell * 0.12, + outer = inner * 3; + + g.addRectangle(0, 0, cell, cell); + g.addCircle(outer, outer, cell - inner - outer, true); + }, + /** @param {Graphics} g */ + function (g, cell, index) { + g.addTriangle(cell / 2, cell / 2, cell / 2, cell / 2, 3); + }, + /** @param {Graphics} g */ + function (g, cell, index) { + var m = cell * 0.25; + g.addRectangle(0, 0, cell, cell); + g.addRhombus(m, m, cell - m, cell - m, true); + }, + /** @param {Graphics} g */ + function (g, cell, index) { + var m = cell * 0.4, s = cell * 1.2; + if (!index) { + g.addCircle(m, m, s); + } + } + ], + + outer: [ + /** @param {Graphics} g */ + function (g, cell, index) { + g.addTriangle(0, 0, cell, cell, 0); + }, + /** @param {Graphics} g */ + function (g, cell, index) { + g.addTriangle(0, cell / 2, cell, cell / 2, 0); + }, + /** @param {Graphics} g */ + function (g, cell, index) { + g.addRhombus(0, 0, cell, cell); + }, + /** @param {Graphics} g */ + function (g, cell, index) { + var m = cell / 6; + g.addCircle(m, m, cell - 2 * m); + } + ] + }; + + + + + function decToHex(v) { + v |= 0; // Ensure integer value + return v < 0 ? "00" : + v < 16 ? "0" + v.toString(16) : + v < 256 ? v.toString(16) : + "ff"; + } + + function hueToRgb(m1, m2, h) { + h = h < 0 ? h + 6 : h > 6 ? h - 6 : h; + return decToHex(255 * ( + h < 1 ? m1 + (m2 - m1) * h : + h < 3 ? m2 : + h < 4 ? m1 + (m2 - m1) * (4 - h) : + m1)); + } + + /** + * Functions for converting colors to hex-rgb representations. + * @private + */ + var color = { + /** + * @param {number} r Red channel [0, 255] + * @param {number} g Green channel [0, 255] + * @param {number} b Blue channel [0, 255] + */ + rgb: function (r, g, b) { + return "#" + decToHex(r) + decToHex(g) + decToHex(b); + }, + /** + * @param h Hue [0, 1] + * @param s Saturation [0, 1] + * @param l Lightness [0, 1] + */ + hsl: function (h, s, l) { + // Based on http://www.w3.org/TR/2011/REC-css3-color-20110607/#hsl-color + if (s == 0) { + var partialHex = decToHex(l * 255); + return "#" + partialHex + partialHex + partialHex; + } + else { + var m2 = l <= 0.5 ? l * (s + 1) : l + s - l * s, + m1 = l * 2 - m2; + return "#" + + hueToRgb(m1, m2, h * 6 + 2) + + hueToRgb(m1, m2, h * 6) + + hueToRgb(m1, m2, h * 6 - 2); + } + }, + // This function will correct the lightness for the "dark" hues + correctedHsl: function (h, s, l) { + // The corrector specifies the perceived middle lightnesses for each hue + var correctors = [ 0.55, 0.5, 0.5, 0.46, 0.6, 0.55, 0.55 ], + corrector = correctors[(h * 6 + 0.5) | 0]; + + // Adjust the input lightness relative to the corrector + l = l < 0.5 ? l * corrector * 2 : corrector + (l - 0.5) * (1 - corrector) * 2; + + return color.hsl(h, s, l); + } + }; + + + + + /** + * Gets a set of identicon color candidates for a specified hue and config. + */ + function colorTheme(hue, config) { + return [ + // Dark gray + color.hsl(0, 0, config.grayscaleLightness(0)), + // Mid color + color.correctedHsl(hue, config.saturation, config.colorLightness(0.5)), + // Light gray + color.hsl(0, 0, config.grayscaleLightness(1)), + // Light color + color.correctedHsl(hue, config.saturation, config.colorLightness(1)), + // Dark color + color.correctedHsl(hue, config.saturation, config.colorLightness(0)) + ]; + } + + + + + /** + * Draws an identicon to a specified renderer. + */ + function iconGenerator(renderer, hash, x, y, size, padding, config) { + var undefined; + + // Calculate padding + padding = (size * (padding === undefined ? 0.08 : padding)) | 0; + size -= padding * 2; + + // Sizes smaller than 30 px are not supported. If really needed, apply a scaling transformation + // to the context before passing it to this function. + if (size < 30) { + throw new Error("Jdenticon cannot render identicons smaller than 30 pixels."); + } + if (!/^[0-9a-f]{11,}$/i.test(hash)) { + throw new Error("Invalid hash passed to Jdenticon."); + } + + var graphics = new Graphics(renderer); + + // Calculate cell size and ensure it is an integer + var cell = 0 | (size / 4); + + // Since the cell size is integer based, the actual icon will be slightly smaller than specified => center icon + x += 0 | (padding + size / 2 - cell * 2); + y += 0 | (padding + size / 2 - cell * 2); + + function renderShape(colorIndex, shapes, index, rotationIndex, positions) { + var r = rotationIndex ? parseInt(hash.charAt(rotationIndex), 16) : 0, + shape = shapes[parseInt(hash.charAt(index), 16) % shapes.length], + i; + + renderer.beginShape(availableColors[selectedColorIndexes[colorIndex]]); + + for (i = 0; i < positions.length; i++) { + graphics._transform = new Transform(x + positions[i][0] * cell, y + positions[i][1] * cell, cell, r++ % 4); + shape(graphics, cell, i); + } + + renderer.endShape(); + } + + // AVAILABLE COLORS + var hue = parseInt(hash.substr(-7), 16) / 0xfffffff, + + // Available colors for this icon + availableColors = colorTheme(hue, config), + + // The index of the selected colors + selectedColorIndexes = [], + index; + + function isDuplicate(values) { + if (values.indexOf(index) >= 0) { + for (var i = 0; i < values.length; i++) { + if (selectedColorIndexes.indexOf(values[i]) >= 0) { + return true; + } + } + } + } + + for (var i = 0; i < 3; i++) { + index = parseInt(hash.charAt(8 + i), 16) % availableColors.length; + if (isDuplicate([0, 4]) || // Disallow dark gray and dark color combo + isDuplicate([2, 3])) { // Disallow light gray and light color combo + index = 1; + } + selectedColorIndexes.push(index); + } + + // ACTUAL RENDERING + // Sides + renderShape(0, shapes.outer, 2, 3, [[1, 0], [2, 0], [2, 3], [1, 3], [0, 1], [3, 1], [3, 2], [0, 2]]); + // Corners + renderShape(1, shapes.outer, 4, 5, [[0, 0], [3, 0], [3, 3], [0, 3]]); + // Center + renderShape(2, shapes.center, 1, null, [[1, 1], [2, 1], [2, 2], [1, 2]]); + }; + + + + /** + * Represents an SVG path element. + * @private + * @constructor + */ + function SvgPath() { + /** + * This property holds the data string (path.d) of the SVG path. + */ + this.dataString = ""; + } + SvgPath.prototype = { + /** + * Adds a polygon with the current fill color to the SVG path. + * @param points An array of Point objects. + */ + addPolygon: function (points) { + var dataString = "M" + points[0].x + " " + points[0].y; + for (var i = 1; i < points.length; i++) { + dataString += "L" + points[i].x + " " + points[i].y; + } + this.dataString += dataString + "Z"; + }, + /** + * Adds a circle with the current fill color to the SVG path. + * @param {Point} point The upper left corner of the circle bounding box. + * @param {number} diameter The diameter of the circle. + * @param {boolean} counterClockwise True if the circle is drawn counter-clockwise (will result in a hole if rendered on a clockwise path). + */ + addCircle: function (point, diameter, counterClockwise) { + var sweepFlag = counterClockwise ? 0 : 1, + radius = diameter / 2; + this.dataString += + "M" + (point.x) + " " + (point.y + radius) + + "a" + radius + "," + radius + " 0 1," + sweepFlag + " " + diameter + ",0" + + "a" + radius + "," + radius + " 0 1," + sweepFlag + " " + (-diameter) + ",0"; + } + }; + + + + /** + * Renderer producing SVG output. + * @private + * @constructor + */ + function SvgRenderer(width, height) { + this._pathsByColor = { }; + this._size = { w: width, h: height }; + } + SvgRenderer.prototype = { + /** + * Marks the beginning of a new shape of the specified color. Should be ended with a call to endShape. + * @param {string} color Fill color on format #xxxxxx. + */ + beginShape: function (color) { + this._path = this._pathsByColor[color] || (this._pathsByColor[color] = new SvgPath()); + }, + /** + * Marks the end of the currently drawn shape. + */ + endShape: function () { }, + /** + * Adds a polygon with the current fill color to the SVG. + * @param points An array of Point objects. + */ + addPolygon: function (points) { + this._path.addPolygon(points); + }, + /** + * Adds a circle with the current fill color to the SVG. + * @param {Point} point The upper left corner of the circle bounding box. + * @param {number} diameter The diameter of the circle. + * @param {boolean} counterClockwise True if the circle is drawn counter-clockwise (will result in a hole if rendered on a clockwise path). + */ + addCircle: function (point, diameter, counterClockwise) { + this._path.addCircle(point, diameter, counterClockwise); + }, + /** + * Gets the rendered image as an SVG string. + * @param {boolean=} fragment If true, the container svg element is not included in the result. + */ + toSvg: function (fragment) { + var svg = fragment ? '' : + ''; + + for (var color in this._pathsByColor) { + svg += ''; + } + + return fragment ? svg : + svg + ''; + } + }; + + + + /** + * Renderer redirecting drawing commands to a canvas context. + * @private + * @constructor + */ + function CanvasRenderer(ctx, width, height) { + this._ctx = ctx; + ctx.clearRect(0, 0, width, height); + } + CanvasRenderer.prototype = { + /** + * Marks the beginning of a new shape of the specified color. Should be ended with a call to endShape. + * @param {string} color Fill color on format #xxxxxx. + */ + beginShape: function (color) { + this._ctx.fillStyle = color; + this._ctx.beginPath(); + }, + /** + * Marks the end of the currently drawn shape. This causes the queued paths to be rendered on the canvas. + */ + endShape: function () { + this._ctx.fill(); + }, + /** + * Adds a polygon to the rendering queue. + * @param points An array of Point objects. + */ + addPolygon: function (points) { + var ctx = this._ctx, i; + ctx.moveTo(points[0].x, points[0].y); + for (i = 1; i < points.length; i++) { + ctx.lineTo(points[i].x, points[i].y); + } + ctx.closePath(); + }, + /** + * Adds a circle to the rendering queue. + * @param {Point} point The upper left corner of the circle bounding box. + * @param {number} diameter The diameter of the circle. + * @param {boolean} counterClockwise True if the circle is drawn counter-clockwise (will result in a hole if rendered on a clockwise path). + */ + addCircle: function (point, diameter, counterClockwise) { + var ctx = this._ctx, + radius = diameter / 2; + ctx.arc(point.x + radius, point.y + radius, radius, 0, Math.PI * 2, counterClockwise); + ctx.closePath(); + } + }; + + + + + + + var /** @const */ + HASH_ATTRIBUTE = "data-jdenticon-hash", + supportsQuerySelectorAll = "document" in global && "querySelectorAll" in document; + + /** + * Gets the normalized current Jdenticon color configuration. Missing fields have default values. + */ + function getCurrentConfig() { + var configObject = jdenticon["config"] || global["jdenticon_config"] || { }, + lightnessConfig = configObject["lightness"] || { }, + saturation = configObject["saturation"]; + + /** + * Creates a lightness range. + */ + function lightness(configName, defaultMin, defaultMax) { + var range = lightnessConfig[configName] instanceof Array ? lightnessConfig[configName] : [defaultMin, defaultMax]; + + /** + * Gets a lightness relative the specified value in the specified lightness range. + */ + return function (value) { + value = range[0] + value * (range[1] - range[0]); + return value < 0 ? 0 : value > 1 ? 1 : value; + }; + } + + return { + saturation: typeof saturation == "number" ? saturation : 0.5, + colorLightness: lightness("color", 0.4, 0.8), + grayscaleLightness: lightness("grayscale", 0.3, 0.9) + } + } + + /** + * Updates the identicon in the specified canvas or svg elements. + * @param {string=} hash Optional hash to be rendered. If not specified, the hash specified by the data-jdenticon-hash is used. + * @param {number=} padding Optional padding in percents. Extra padding might be added to center the rendered identicon. + */ + function update(el, hash, padding) { + if (typeof(el) === "string") { + if (supportsQuerySelectorAll) { + var elements = document.querySelectorAll(el); + for (var i = 0; i < elements.length; i++) { + update(elements[i], hash, padding); + } + } + return; + } + if (!el || !el["tagName"]) { + // No element found + return; + } + hash = hash || el.getAttribute(HASH_ATTRIBUTE); + if (!hash) { + // No hash specified + return; + } + + var isSvg = el["tagName"].toLowerCase() == "svg", + isCanvas = el["tagName"].toLowerCase() == "canvas"; + + // Ensure we have a supported element + if (!isSvg && !(isCanvas && "getContext" in el)) { + return; + } + + var width = Number(el.getAttribute("width")) || el.clientWidth || 0, + height = Number(el.getAttribute("height")) || el.clientHeight || 0, + renderer = isSvg ? new SvgRenderer(width, height) : new CanvasRenderer(el.getContext("2d"), width, height), + size = Math.min(width, height); + + // Draw icon + iconGenerator(renderer, hash, 0, 0, size, padding, getCurrentConfig()); + + // SVG needs postprocessing + if (isSvg) { + // Parse svg to a temporary span element. + // Simply using innerHTML does unfortunately not work on IE. + var wrapper = document.createElement("span"); + wrapper.innerHTML = renderer.toSvg(false); + + // Then replace the content of the target element with the parsed svg. + while (el.firstChild) { + el.removeChild(el.firstChild); + } + var newNodes = wrapper.firstChild.childNodes; + while (newNodes.length) { + el.appendChild(newNodes[0]); + } + + // Set viewBox attribute to ensure the svg scales nicely. + el.setAttribute("viewBox", "0 0 " + width + " " + height); + } + } + + /** + * Draws an identicon to a context. + */ + function drawIcon(ctx, hash, size) { + if (!ctx) { + throw new Error("No canvas specified."); + } + + var renderer = new CanvasRenderer(ctx, size, size); + iconGenerator(renderer, hash, 0, 0, size, 0, getCurrentConfig()); + } + + /** + * Draws an identicon to a context. + * @param {number=} padding Optional padding in percents. Extra padding might be added to center the rendered identicon. + */ + function toSvg(hash, size, padding) { + var renderer = new SvgRenderer(size, size); + iconGenerator(renderer, hash, 0, 0, size, padding, getCurrentConfig()); + return renderer.toSvg(); + } + + /** + * Updates all canvas elements with the data-jdenticon-hash attribute. + */ + function jdenticon() { + if (supportsQuerySelectorAll) { + update("svg[" + HASH_ATTRIBUTE + "],canvas[" + HASH_ATTRIBUTE + "]"); + } + } + + // Public API + jdenticon["drawIcon"] = drawIcon; + jdenticon["toSvg"] = toSvg; + jdenticon["update"] = update; + jdenticon["version"] = "1.3.2"; + + // Basic jQuery plugin + if (jQuery) { + jQuery["fn"]["jdenticon"] = function (hash, padding) { + this["each"](function (index, el) { + update(el, hash, padding); + }); + return this; + }; + } + + // Schedule to render all identicons on the page once it has been loaded. + if (typeof setTimeout === "function") { + setTimeout(jdenticon, 0); + } + + return jdenticon; + +}); \ No newline at end of file diff --git a/pagure/static/jdenticon-1.3.2.min.js b/pagure/static/jdenticon-1.3.2.min.js new file mode 100644 index 0000000..610d18e --- /dev/null +++ b/pagure/static/jdenticon-1.3.2.min.js @@ -0,0 +1,13 @@ +// Jdenticon 1.3.2 | jdenticon.com | zlib licensed | (c) 2014-2015 Daniel Mester Pirttijärvi +(function(k,g,h){var l=h(k,k.jQuery);"undefined"!==typeof module&&"exports"in module?module.exports=l:"function"===typeof define&&define.amd?define([],function(){return l}):k[g]=l})(this,"jdenticon",function(k,g){function h(b,a){this.x=b;this.y=a}function l(b,a,c,d){this.o=b;this.s=a;this.f=c;this.l=d}function A(b){this.C=b;this.m=l.O}function m(b){b|=0;return 0>b?"00":16>b?"0"+b.toString(16):256>b?b.toString(16):"ff"}function q(b,a,c){c=0>c?c+6:6c?b+(a-b)*c:3>c?a:4>c?b+(a- +b)*(4-c):b))}function D(b,a){return[n.w(0,0,a.H(0)),n.v(b,a.A,a.u(.5)),n.w(0,0,a.H(1)),n.v(b,a.A,a.u(1)),n.v(b,a.A,a.u(0))]}function r(b,a,c,d,t){var f=0,u=0;function v(c,d,t,e,g){e=e?parseInt(a.charAt(e),16):0;d=d[parseInt(a.charAt(t),16)%d.length];b.F(n[m[c]]);for(c=0;cc)throw Error("Jdenticon cannot render identicons smaller than 30 pixels."); +if(!/^[0-9a-f]{11,}$/i.test(a))throw Error("Invalid hash passed to Jdenticon.");var k=new A(b),h=0|c/4,f=f+(0|d+c/2-2*h),u=u+(0|d+c/2-2*h),n=D(parseInt(a.substr(-7),16)/268435455,t),m=[],g;for(c=0;3>c;c++){g=parseInt(a.charAt(8+c),16)%n.length;if(e([0,4])||e([2,3]))g=1;m.push(g)}v(0,w.J,2,3,[[1,0],[2,0],[2,3],[1,3],[0,1],[3,1],[3,2],[0,2]]);v(1,w.J,4,5,[[0,0],[3,0],[3,3],[0,3]]);v(2,w.N,1,null,[[1,1],[2,1],[2,2],[1,2]])}function B(){this.i=""}function x(b,a){this.j={};this.f={M:b,I:a}}function y(b, +a,c){this.h=b;b.clearRect(0,0,a,c)}function z(){function b(a,b,f){var e=c[a]instanceof Array?c[a]:[b,f];return function(a){a=e[0]+a*(e[1]-e[0]);return 0>a?0:1=c?c*(a+1):c+a-c*a;c=2*c-a;return"#"+q(c,a,6*b+2)+q(c,a,6*b)+q(c,a,6*b-2)},v:function(b,a,c){var d=[.55,.5,.5,.46,.6,.55,.55][6*b+.5|0];return n.w(b,a,.5>c?c*d*2:d+(c-.5)*(1-d)*2)}};B.prototype={a:function(b){for(var a="M"+b[0].x+" "+b[0].y,c=1;c< +b.length;c++)a+="L"+b[c].x+" "+b[c].y;this.i+=a+"Z"},b:function(b,a,c){c=c?0:1;var d=a/2;this.i+="M"+b.x+" "+(b.y+d)+"a"+d+","+d+" 0 1,"+c+" "+a+",0a"+d+","+d+" 0 1,"+c+" "+-a+",0"}};x.prototype={F:function(b){this.B=this.j[b]||(this.j[b]=new B)},G:function(){},a:function(b){this.B.a(b)},b:function(b,a,c){this.B.b(b,a,c)},K:function(b){var a=b?"":'', +c;for(c in this.j)a+='';return b?a:a+""}};y.prototype={F:function(b){this.h.fillStyle=b;this.h.beginPath()},G:function(){this.h.fill()},a:function(b){var a=this.h,c;a.moveTo(b[0].x,b[0].y);for(c=1;c)[^>]*|#([\w-]*))$/,k=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,E=/^[\],:{}\s]*$/,S=/(?:^|:|,)(?:\s*\[)+/g,A=/\\(?:["\\\/bfnrt]|u[\da-fA-F]{4})/g,j=/"[^"\\\r\n]*"|true|false|null|-?(?:\d+\.|)\d+(?:[eE][+-]?\d+|)/g,D=/^-ms-/,L=/-([\da-z])/gi,H=function(e,t){return t.toUpperCase()},q=function(e){(a.addEventListener||"load"===e.type||"complete"===a.readyState)&&(_(),x.ready())},_=function(){a.addEventListener?(a.removeEventListener("DOMContentLoaded",q,!1),e.removeEventListener("load",q,!1)):(a.detachEvent("onreadystatechange",q),e.detachEvent("onload",q))};x.fn=x.prototype={jquery:f,constructor:x,init:function(e,n,r){var i,o;if(!e)return this;if("string"==typeof e){if(i="<"===e.charAt(0)&&">"===e.charAt(e.length-1)&&e.length>=3?[null,e,null]:N.exec(e),!i||!i[1]&&n)return!n||n.jquery?(n||r).find(e):this.constructor(n).find(e);if(i[1]){if(n=n instanceof x?n[0]:n,x.merge(this,x.parseHTML(i[1],n&&n.nodeType?n.ownerDocument||n:a,!0)),k.test(i[1])&&x.isPlainObject(n))for(i in n)x.isFunction(this[i])?this[i](n[i]):this.attr(i,n[i]);return this}if(o=a.getElementById(i[2]),o&&o.parentNode){if(o.id!==i[2])return r.find(e);this.length=1,this[0]=o}return this.context=a,this.selector=e,this}return e.nodeType?(this.context=this[0]=e,this.length=1,this):x.isFunction(e)?r.ready(e):(e.selector!==t&&(this.selector=e.selector,this.context=e.context),x.makeArray(e,this))},selector:"",length:0,toArray:function(){return g.call(this)},get:function(e){return null==e?this.toArray():0>e?this[this.length+e]:this[e]},pushStack:function(e){var t=x.merge(this.constructor(),e);return t.prevObject=this,t.context=this.context,t},each:function(e,t){return x.each(this,e,t)},ready:function(e){return x.ready.promise().done(e),this},slice:function(){return this.pushStack(g.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(e){var t=this.length,n=+e+(0>e?t:0);return this.pushStack(n>=0&&t>n?[this[n]]:[])},map:function(e){return this.pushStack(x.map(this,function(t,n){return e.call(t,n,t)}))},end:function(){return this.prevObject||this.constructor(null)},push:h,sort:[].sort,splice:[].splice},x.fn.init.prototype=x.fn,x.extend=x.fn.extend=function(){var e,n,r,i,o,a,s=arguments[0]||{},l=1,u=arguments.length,c=!1;for("boolean"==typeof s&&(c=s,s=arguments[1]||{},l=2),"object"==typeof s||x.isFunction(s)||(s={}),u===l&&(s=this,--l);u>l;l++)if(null!=(o=arguments[l]))for(i in o)e=s[i],r=o[i],s!==r&&(c&&r&&(x.isPlainObject(r)||(n=x.isArray(r)))?(n?(n=!1,a=e&&x.isArray(e)?e:[]):a=e&&x.isPlainObject(e)?e:{},s[i]=x.extend(c,a,r)):r!==t&&(s[i]=r));return s},x.extend({expando:"jQuery"+(f+Math.random()).replace(/\D/g,""),noConflict:function(t){return e.$===x&&(e.$=u),t&&e.jQuery===x&&(e.jQuery=l),x},isReady:!1,readyWait:1,holdReady:function(e){e?x.readyWait++:x.ready(!0)},ready:function(e){if(e===!0?!--x.readyWait:!x.isReady){if(!a.body)return setTimeout(x.ready);x.isReady=!0,e!==!0&&--x.readyWait>0||(n.resolveWith(a,[x]),x.fn.trigger&&x(a).trigger("ready").off("ready"))}},isFunction:function(e){return"function"===x.type(e)},isArray:Array.isArray||function(e){return"array"===x.type(e)},isWindow:function(e){return null!=e&&e==e.window},isNumeric:function(e){return!isNaN(parseFloat(e))&&isFinite(e)},type:function(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?c[y.call(e)]||"object":typeof e},isPlainObject:function(e){var n;if(!e||"object"!==x.type(e)||e.nodeType||x.isWindow(e))return!1;try{if(e.constructor&&!v.call(e,"constructor")&&!v.call(e.constructor.prototype,"isPrototypeOf"))return!1}catch(r){return!1}if(x.support.ownLast)for(n in e)return v.call(e,n);for(n in e);return n===t||v.call(e,n)},isEmptyObject:function(e){var t;for(t in e)return!1;return!0},error:function(e){throw Error(e)},parseHTML:function(e,t,n){if(!e||"string"!=typeof e)return null;"boolean"==typeof t&&(n=t,t=!1),t=t||a;var r=k.exec(e),i=!n&&[];return r?[t.createElement(r[1])]:(r=x.buildFragment([e],t,i),i&&x(i).remove(),x.merge([],r.childNodes))},parseJSON:function(n){return e.JSON&&e.JSON.parse?e.JSON.parse(n):null===n?n:"string"==typeof n&&(n=x.trim(n),n&&E.test(n.replace(A,"@").replace(j,"]").replace(S,"")))?Function("return "+n)():(x.error("Invalid JSON: "+n),t)},parseXML:function(n){var r,i;if(!n||"string"!=typeof n)return null;try{e.DOMParser?(i=new DOMParser,r=i.parseFromString(n,"text/xml")):(r=new ActiveXObject("Microsoft.XMLDOM"),r.async="false",r.loadXML(n))}catch(o){r=t}return r&&r.documentElement&&!r.getElementsByTagName("parsererror").length||x.error("Invalid XML: "+n),r},noop:function(){},globalEval:function(t){t&&x.trim(t)&&(e.execScript||function(t){e.eval.call(e,t)})(t)},camelCase:function(e){return e.replace(D,"ms-").replace(L,H)},nodeName:function(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()},each:function(e,t,n){var r,i=0,o=e.length,a=M(e);if(n){if(a){for(;o>i;i++)if(r=t.apply(e[i],n),r===!1)break}else for(i in e)if(r=t.apply(e[i],n),r===!1)break}else if(a){for(;o>i;i++)if(r=t.call(e[i],i,e[i]),r===!1)break}else for(i in e)if(r=t.call(e[i],i,e[i]),r===!1)break;return e},trim:b&&!b.call("\ufeff\u00a0")?function(e){return null==e?"":b.call(e)}:function(e){return null==e?"":(e+"").replace(C,"")},makeArray:function(e,t){var n=t||[];return null!=e&&(M(Object(e))?x.merge(n,"string"==typeof e?[e]:e):h.call(n,e)),n},inArray:function(e,t,n){var r;if(t){if(m)return m.call(t,e,n);for(r=t.length,n=n?0>n?Math.max(0,r+n):n:0;r>n;n++)if(n in t&&t[n]===e)return n}return-1},merge:function(e,n){var r=n.length,i=e.length,o=0;if("number"==typeof r)for(;r>o;o++)e[i++]=n[o];else while(n[o]!==t)e[i++]=n[o++];return e.length=i,e},grep:function(e,t,n){var r,i=[],o=0,a=e.length;for(n=!!n;a>o;o++)r=!!t(e[o],o),n!==r&&i.push(e[o]);return i},map:function(e,t,n){var r,i=0,o=e.length,a=M(e),s=[];if(a)for(;o>i;i++)r=t(e[i],i,n),null!=r&&(s[s.length]=r);else for(i in e)r=t(e[i],i,n),null!=r&&(s[s.length]=r);return d.apply([],s)},guid:1,proxy:function(e,n){var r,i,o;return"string"==typeof n&&(o=e[n],n=e,e=o),x.isFunction(e)?(r=g.call(arguments,2),i=function(){return e.apply(n||this,r.concat(g.call(arguments)))},i.guid=e.guid=e.guid||x.guid++,i):t},access:function(e,n,r,i,o,a,s){var l=0,u=e.length,c=null==r;if("object"===x.type(r)){o=!0;for(l in r)x.access(e,n,l,r[l],!0,a,s)}else if(i!==t&&(o=!0,x.isFunction(i)||(s=!0),c&&(s?(n.call(e,i),n=null):(c=n,n=function(e,t,n){return c.call(x(e),n)})),n))for(;u>l;l++)n(e[l],r,s?i:i.call(e[l],l,n(e[l],r)));return o?e:c?n.call(e):u?n(e[0],r):a},now:function(){return(new Date).getTime()},swap:function(e,t,n,r){var i,o,a={};for(o in t)a[o]=e.style[o],e.style[o]=t[o];i=n.apply(e,r||[]);for(o in t)e.style[o]=a[o];return i}}),x.ready.promise=function(t){if(!n)if(n=x.Deferred(),"complete"===a.readyState)setTimeout(x.ready);else if(a.addEventListener)a.addEventListener("DOMContentLoaded",q,!1),e.addEventListener("load",q,!1);else{a.attachEvent("onreadystatechange",q),e.attachEvent("onload",q);var r=!1;try{r=null==e.frameElement&&a.documentElement}catch(i){}r&&r.doScroll&&function o(){if(!x.isReady){try{r.doScroll("left")}catch(e){return setTimeout(o,50)}_(),x.ready()}}()}return n.promise(t)},x.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(e,t){c["[object "+t+"]"]=t.toLowerCase()});function M(e){var t=e.length,n=x.type(e);return x.isWindow(e)?!1:1===e.nodeType&&t?!0:"array"===n||"function"!==n&&(0===t||"number"==typeof t&&t>0&&t-1 in e)}r=x(a),function(e,t){var n,r,i,o,a,s,l,u,c,p,f,d,h,g,m,y,v,b="sizzle"+-new Date,w=e.document,T=0,C=0,N=st(),k=st(),E=st(),S=!1,A=function(e,t){return e===t?(S=!0,0):0},j=typeof t,D=1<<31,L={}.hasOwnProperty,H=[],q=H.pop,_=H.push,M=H.push,O=H.slice,F=H.indexOf||function(e){var t=0,n=this.length;for(;n>t;t++)if(this[t]===e)return t;return-1},B="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",P="[\\x20\\t\\r\\n\\f]",R="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",W=R.replace("w","w#"),$="\\["+P+"*("+R+")"+P+"*(?:([*^$|!~]?=)"+P+"*(?:(['\"])((?:\\\\.|[^\\\\])*?)\\3|("+W+")|)|)"+P+"*\\]",I=":("+R+")(?:\\(((['\"])((?:\\\\.|[^\\\\])*?)\\3|((?:\\\\.|[^\\\\()[\\]]|"+$.replace(3,8)+")*)|.*)\\)|)",z=RegExp("^"+P+"+|((?:^|[^\\\\])(?:\\\\.)*)"+P+"+$","g"),X=RegExp("^"+P+"*,"+P+"*"),U=RegExp("^"+P+"*([>+~]|"+P+")"+P+"*"),V=RegExp(P+"*[+~]"),Y=RegExp("="+P+"*([^\\]'\"]*)"+P+"*\\]","g"),J=RegExp(I),G=RegExp("^"+W+"$"),Q={ID:RegExp("^#("+R+")"),CLASS:RegExp("^\\.("+R+")"),TAG:RegExp("^("+R.replace("w","w*")+")"),ATTR:RegExp("^"+$),PSEUDO:RegExp("^"+I),CHILD:RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+P+"*(even|odd|(([+-]|)(\\d*)n|)"+P+"*(?:([+-]|)"+P+"*(\\d+)|))"+P+"*\\)|)","i"),bool:RegExp("^(?:"+B+")$","i"),needsContext:RegExp("^"+P+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+P+"*((?:-\\d)?\\d*)"+P+"*\\)|)(?=[^-]|$)","i")},K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,et=/^(?:input|select|textarea|button)$/i,tt=/^h\d$/i,nt=/'|\\/g,rt=RegExp("\\\\([\\da-f]{1,6}"+P+"?|("+P+")|.)","ig"),it=function(e,t,n){var r="0x"+t-65536;return r!==r||n?t:0>r?String.fromCharCode(r+65536):String.fromCharCode(55296|r>>10,56320|1023&r)};try{M.apply(H=O.call(w.childNodes),w.childNodes),H[w.childNodes.length].nodeType}catch(ot){M={apply:H.length?function(e,t){_.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function at(e,t,n,i){var o,a,s,l,u,c,d,m,y,x;if((t?t.ownerDocument||t:w)!==f&&p(t),t=t||f,n=n||[],!e||"string"!=typeof e)return n;if(1!==(l=t.nodeType)&&9!==l)return[];if(h&&!i){if(o=Z.exec(e))if(s=o[1]){if(9===l){if(a=t.getElementById(s),!a||!a.parentNode)return n;if(a.id===s)return n.push(a),n}else if(t.ownerDocument&&(a=t.ownerDocument.getElementById(s))&&v(t,a)&&a.id===s)return n.push(a),n}else{if(o[2])return M.apply(n,t.getElementsByTagName(e)),n;if((s=o[3])&&r.getElementsByClassName&&t.getElementsByClassName)return M.apply(n,t.getElementsByClassName(s)),n}if(r.qsa&&(!g||!g.test(e))){if(m=d=b,y=t,x=9===l&&e,1===l&&"object"!==t.nodeName.toLowerCase()){c=mt(e),(d=t.getAttribute("id"))?m=d.replace(nt,"\\$&"):t.setAttribute("id",m),m="[id='"+m+"'] ",u=c.length;while(u--)c[u]=m+yt(c[u]);y=V.test(e)&&t.parentNode||t,x=c.join(",")}if(x)try{return M.apply(n,y.querySelectorAll(x)),n}catch(T){}finally{d||t.removeAttribute("id")}}}return kt(e.replace(z,"$1"),t,n,i)}function st(){var e=[];function t(n,r){return e.push(n+=" ")>o.cacheLength&&delete t[e.shift()],t[n]=r}return t}function lt(e){return e[b]=!0,e}function ut(e){var t=f.createElement("div");try{return!!e(t)}catch(n){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function ct(e,t){var n=e.split("|"),r=e.length;while(r--)o.attrHandle[n[r]]=t}function pt(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&(~t.sourceIndex||D)-(~e.sourceIndex||D);if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function ft(e){return function(t){var n=t.nodeName.toLowerCase();return"input"===n&&t.type===e}}function dt(e){return function(t){var n=t.nodeName.toLowerCase();return("input"===n||"button"===n)&&t.type===e}}function ht(e){return lt(function(t){return t=+t,lt(function(n,r){var i,o=e([],n.length,t),a=o.length;while(a--)n[i=o[a]]&&(n[i]=!(r[i]=n[i]))})})}s=at.isXML=function(e){var t=e&&(e.ownerDocument||e).documentElement;return t?"HTML"!==t.nodeName:!1},r=at.support={},p=at.setDocument=function(e){var n=e?e.ownerDocument||e:w,i=n.defaultView;return n!==f&&9===n.nodeType&&n.documentElement?(f=n,d=n.documentElement,h=!s(n),i&&i.attachEvent&&i!==i.top&&i.attachEvent("onbeforeunload",function(){p()}),r.attributes=ut(function(e){return e.className="i",!e.getAttribute("className")}),r.getElementsByTagName=ut(function(e){return e.appendChild(n.createComment("")),!e.getElementsByTagName("*").length}),r.getElementsByClassName=ut(function(e){return e.innerHTML="
    ",e.firstChild.className="i",2===e.getElementsByClassName("i").length}),r.getById=ut(function(e){return d.appendChild(e).id=b,!n.getElementsByName||!n.getElementsByName(b).length}),r.getById?(o.find.ID=function(e,t){if(typeof t.getElementById!==j&&h){var n=t.getElementById(e);return n&&n.parentNode?[n]:[]}},o.filter.ID=function(e){var t=e.replace(rt,it);return function(e){return e.getAttribute("id")===t}}):(delete o.find.ID,o.filter.ID=function(e){var t=e.replace(rt,it);return function(e){var n=typeof e.getAttributeNode!==j&&e.getAttributeNode("id");return n&&n.value===t}}),o.find.TAG=r.getElementsByTagName?function(e,n){return typeof n.getElementsByTagName!==j?n.getElementsByTagName(e):t}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},o.find.CLASS=r.getElementsByClassName&&function(e,n){return typeof n.getElementsByClassName!==j&&h?n.getElementsByClassName(e):t},m=[],g=[],(r.qsa=K.test(n.querySelectorAll))&&(ut(function(e){e.innerHTML="",e.querySelectorAll("[selected]").length||g.push("\\["+P+"*(?:value|"+B+")"),e.querySelectorAll(":checked").length||g.push(":checked")}),ut(function(e){var t=n.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("t",""),e.querySelectorAll("[t^='']").length&&g.push("[*^$]="+P+"*(?:''|\"\")"),e.querySelectorAll(":enabled").length||g.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),g.push(",.*:")})),(r.matchesSelector=K.test(y=d.webkitMatchesSelector||d.mozMatchesSelector||d.oMatchesSelector||d.msMatchesSelector))&&ut(function(e){r.disconnectedMatch=y.call(e,"div"),y.call(e,"[s!='']:x"),m.push("!=",I)}),g=g.length&&RegExp(g.join("|")),m=m.length&&RegExp(m.join("|")),v=K.test(d.contains)||d.compareDocumentPosition?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},A=d.compareDocumentPosition?function(e,t){if(e===t)return S=!0,0;var i=t.compareDocumentPosition&&e.compareDocumentPosition&&e.compareDocumentPosition(t);return i?1&i||!r.sortDetached&&t.compareDocumentPosition(e)===i?e===n||v(w,e)?-1:t===n||v(w,t)?1:c?F.call(c,e)-F.call(c,t):0:4&i?-1:1:e.compareDocumentPosition?-1:1}:function(e,t){var r,i=0,o=e.parentNode,a=t.parentNode,s=[e],l=[t];if(e===t)return S=!0,0;if(!o||!a)return e===n?-1:t===n?1:o?-1:a?1:c?F.call(c,e)-F.call(c,t):0;if(o===a)return pt(e,t);r=e;while(r=r.parentNode)s.unshift(r);r=t;while(r=r.parentNode)l.unshift(r);while(s[i]===l[i])i++;return i?pt(s[i],l[i]):s[i]===w?-1:l[i]===w?1:0},n):f},at.matches=function(e,t){return at(e,null,null,t)},at.matchesSelector=function(e,t){if((e.ownerDocument||e)!==f&&p(e),t=t.replace(Y,"='$1']"),!(!r.matchesSelector||!h||m&&m.test(t)||g&&g.test(t)))try{var n=y.call(e,t);if(n||r.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(i){}return at(t,f,null,[e]).length>0},at.contains=function(e,t){return(e.ownerDocument||e)!==f&&p(e),v(e,t)},at.attr=function(e,n){(e.ownerDocument||e)!==f&&p(e);var i=o.attrHandle[n.toLowerCase()],a=i&&L.call(o.attrHandle,n.toLowerCase())?i(e,n,!h):t;return a===t?r.attributes||!h?e.getAttribute(n):(a=e.getAttributeNode(n))&&a.specified?a.value:null:a},at.error=function(e){throw Error("Syntax error, unrecognized expression: "+e)},at.uniqueSort=function(e){var t,n=[],i=0,o=0;if(S=!r.detectDuplicates,c=!r.sortStable&&e.slice(0),e.sort(A),S){while(t=e[o++])t===e[o]&&(i=n.push(o));while(i--)e.splice(n[i],1)}return e},a=at.getText=function(e){var t,n="",r=0,i=e.nodeType;if(i){if(1===i||9===i||11===i){if("string"==typeof e.textContent)return e.textContent;for(e=e.firstChild;e;e=e.nextSibling)n+=a(e)}else if(3===i||4===i)return e.nodeValue}else for(;t=e[r];r++)n+=a(t);return n},o=at.selectors={cacheLength:50,createPseudo:lt,match:Q,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(rt,it),e[3]=(e[4]||e[5]||"").replace(rt,it),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||at.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&at.error(e[0]),e},PSEUDO:function(e){var n,r=!e[5]&&e[2];return Q.CHILD.test(e[0])?null:(e[3]&&e[4]!==t?e[2]=e[4]:r&&J.test(r)&&(n=mt(r,!0))&&(n=r.indexOf(")",r.length-n)-r.length)&&(e[0]=e[0].slice(0,n),e[2]=r.slice(0,n)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(rt,it).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=N[e+" "];return t||(t=RegExp("(^|"+P+")"+e+"("+P+"|$)"))&&N(e,function(e){return t.test("string"==typeof e.className&&e.className||typeof e.getAttribute!==j&&e.getAttribute("class")||"")})},ATTR:function(e,t,n){return function(r){var i=at.attr(r,e);return null==i?"!="===t:t?(i+="","="===t?i===n:"!="===t?i!==n:"^="===t?n&&0===i.indexOf(n):"*="===t?n&&i.indexOf(n)>-1:"$="===t?n&&i.slice(-n.length)===n:"~="===t?(" "+i+" ").indexOf(n)>-1:"|="===t?i===n||i.slice(0,n.length+1)===n+"-":!1):!0}},CHILD:function(e,t,n,r,i){var o="nth"!==e.slice(0,3),a="last"!==e.slice(-4),s="of-type"===t;return 1===r&&0===i?function(e){return!!e.parentNode}:function(t,n,l){var u,c,p,f,d,h,g=o!==a?"nextSibling":"previousSibling",m=t.parentNode,y=s&&t.nodeName.toLowerCase(),v=!l&&!s;if(m){if(o){while(g){p=t;while(p=p[g])if(s?p.nodeName.toLowerCase()===y:1===p.nodeType)return!1;h=g="only"===e&&!h&&"nextSibling"}return!0}if(h=[a?m.firstChild:m.lastChild],a&&v){c=m[b]||(m[b]={}),u=c[e]||[],d=u[0]===T&&u[1],f=u[0]===T&&u[2],p=d&&m.childNodes[d];while(p=++d&&p&&p[g]||(f=d=0)||h.pop())if(1===p.nodeType&&++f&&p===t){c[e]=[T,d,f];break}}else if(v&&(u=(t[b]||(t[b]={}))[e])&&u[0]===T)f=u[1];else while(p=++d&&p&&p[g]||(f=d=0)||h.pop())if((s?p.nodeName.toLowerCase()===y:1===p.nodeType)&&++f&&(v&&((p[b]||(p[b]={}))[e]=[T,f]),p===t))break;return f-=i,f===r||0===f%r&&f/r>=0}}},PSEUDO:function(e,t){var n,r=o.pseudos[e]||o.setFilters[e.toLowerCase()]||at.error("unsupported pseudo: "+e);return r[b]?r(t):r.length>1?(n=[e,e,"",t],o.setFilters.hasOwnProperty(e.toLowerCase())?lt(function(e,n){var i,o=r(e,t),a=o.length;while(a--)i=F.call(e,o[a]),e[i]=!(n[i]=o[a])}):function(e){return r(e,0,n)}):r}},pseudos:{not:lt(function(e){var t=[],n=[],r=l(e.replace(z,"$1"));return r[b]?lt(function(e,t,n,i){var o,a=r(e,null,i,[]),s=e.length;while(s--)(o=a[s])&&(e[s]=!(t[s]=o))}):function(e,i,o){return t[0]=e,r(t,null,o,n),!n.pop()}}),has:lt(function(e){return function(t){return at(e,t).length>0}}),contains:lt(function(e){return function(t){return(t.textContent||t.innerText||a(t)).indexOf(e)>-1}}),lang:lt(function(e){return G.test(e||"")||at.error("unsupported lang: "+e),e=e.replace(rt,it).toLowerCase(),function(t){var n;do if(n=h?t.lang:t.getAttribute("xml:lang")||t.getAttribute("lang"))return n=n.toLowerCase(),n===e||0===n.indexOf(e+"-");while((t=t.parentNode)&&1===t.nodeType);return!1}}),target:function(t){var n=e.location&&e.location.hash;return n&&n.slice(1)===t.id},root:function(e){return e===d},focus:function(e){return e===f.activeElement&&(!f.hasFocus||f.hasFocus())&&!!(e.type||e.href||~e.tabIndex)},enabled:function(e){return e.disabled===!1},disabled:function(e){return e.disabled===!0},checked:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&!!e.checked||"option"===t&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,e.selected===!0},empty:function(e){for(e=e.firstChild;e;e=e.nextSibling)if(e.nodeName>"@"||3===e.nodeType||4===e.nodeType)return!1;return!0},parent:function(e){return!o.pseudos.empty(e)},header:function(e){return tt.test(e.nodeName)},input:function(e){return et.test(e.nodeName)},button:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&"button"===e.type||"button"===t},text:function(e){var t;return"input"===e.nodeName.toLowerCase()&&"text"===e.type&&(null==(t=e.getAttribute("type"))||t.toLowerCase()===e.type)},first:ht(function(){return[0]}),last:ht(function(e,t){return[t-1]}),eq:ht(function(e,t,n){return[0>n?n+t:n]}),even:ht(function(e,t){var n=0;for(;t>n;n+=2)e.push(n);return e}),odd:ht(function(e,t){var n=1;for(;t>n;n+=2)e.push(n);return e}),lt:ht(function(e,t,n){var r=0>n?n+t:n;for(;--r>=0;)e.push(r);return e}),gt:ht(function(e,t,n){var r=0>n?n+t:n;for(;t>++r;)e.push(r);return e})}},o.pseudos.nth=o.pseudos.eq;for(n in{radio:!0,checkbox:!0,file:!0,password:!0,image:!0})o.pseudos[n]=ft(n);for(n in{submit:!0,reset:!0})o.pseudos[n]=dt(n);function gt(){}gt.prototype=o.filters=o.pseudos,o.setFilters=new gt;function mt(e,t){var n,r,i,a,s,l,u,c=k[e+" "];if(c)return t?0:c.slice(0);s=e,l=[],u=o.preFilter;while(s){(!n||(r=X.exec(s)))&&(r&&(s=s.slice(r[0].length)||s),l.push(i=[])),n=!1,(r=U.exec(s))&&(n=r.shift(),i.push({value:n,type:r[0].replace(z," ")}),s=s.slice(n.length));for(a in o.filter)!(r=Q[a].exec(s))||u[a]&&!(r=u[a](r))||(n=r.shift(),i.push({value:n,type:a,matches:r}),s=s.slice(n.length));if(!n)break}return t?s.length:s?at.error(e):k(e,l).slice(0)}function yt(e){var t=0,n=e.length,r="";for(;n>t;t++)r+=e[t].value;return r}function vt(e,t,n){var r=t.dir,o=n&&"parentNode"===r,a=C++;return t.first?function(t,n,i){while(t=t[r])if(1===t.nodeType||o)return e(t,n,i)}:function(t,n,s){var l,u,c,p=T+" "+a;if(s){while(t=t[r])if((1===t.nodeType||o)&&e(t,n,s))return!0}else while(t=t[r])if(1===t.nodeType||o)if(c=t[b]||(t[b]={}),(u=c[r])&&u[0]===p){if((l=u[1])===!0||l===i)return l===!0}else if(u=c[r]=[p],u[1]=e(t,n,s)||i,u[1]===!0)return!0}}function bt(e){return e.length>1?function(t,n,r){var i=e.length;while(i--)if(!e[i](t,n,r))return!1;return!0}:e[0]}function xt(e,t,n,r,i){var o,a=[],s=0,l=e.length,u=null!=t;for(;l>s;s++)(o=e[s])&&(!n||n(o,r,i))&&(a.push(o),u&&t.push(s));return a}function wt(e,t,n,r,i,o){return r&&!r[b]&&(r=wt(r)),i&&!i[b]&&(i=wt(i,o)),lt(function(o,a,s,l){var u,c,p,f=[],d=[],h=a.length,g=o||Nt(t||"*",s.nodeType?[s]:s,[]),m=!e||!o&&t?g:xt(g,f,e,s,l),y=n?i||(o?e:h||r)?[]:a:m;if(n&&n(m,y,s,l),r){u=xt(y,d),r(u,[],s,l),c=u.length;while(c--)(p=u[c])&&(y[d[c]]=!(m[d[c]]=p))}if(o){if(i||e){if(i){u=[],c=y.length;while(c--)(p=y[c])&&u.push(m[c]=p);i(null,y=[],u,l)}c=y.length;while(c--)(p=y[c])&&(u=i?F.call(o,p):f[c])>-1&&(o[u]=!(a[u]=p))}}else y=xt(y===a?y.splice(h,y.length):y),i?i(null,a,y,l):M.apply(a,y)})}function Tt(e){var t,n,r,i=e.length,a=o.relative[e[0].type],s=a||o.relative[" "],l=a?1:0,c=vt(function(e){return e===t},s,!0),p=vt(function(e){return F.call(t,e)>-1},s,!0),f=[function(e,n,r){return!a&&(r||n!==u)||((t=n).nodeType?c(e,n,r):p(e,n,r))}];for(;i>l;l++)if(n=o.relative[e[l].type])f=[vt(bt(f),n)];else{if(n=o.filter[e[l].type].apply(null,e[l].matches),n[b]){for(r=++l;i>r;r++)if(o.relative[e[r].type])break;return wt(l>1&&bt(f),l>1&&yt(e.slice(0,l-1).concat({value:" "===e[l-2].type?"*":""})).replace(z,"$1"),n,r>l&&Tt(e.slice(l,r)),i>r&&Tt(e=e.slice(r)),i>r&&yt(e))}f.push(n)}return bt(f)}function Ct(e,t){var n=0,r=t.length>0,a=e.length>0,s=function(s,l,c,p,d){var h,g,m,y=[],v=0,b="0",x=s&&[],w=null!=d,C=u,N=s||a&&o.find.TAG("*",d&&l.parentNode||l),k=T+=null==C?1:Math.random()||.1;for(w&&(u=l!==f&&l,i=n);null!=(h=N[b]);b++){if(a&&h){g=0;while(m=e[g++])if(m(h,l,c)){p.push(h);break}w&&(T=k,i=++n)}r&&((h=!m&&h)&&v--,s&&x.push(h))}if(v+=b,r&&b!==v){g=0;while(m=t[g++])m(x,y,l,c);if(s){if(v>0)while(b--)x[b]||y[b]||(y[b]=q.call(p));y=xt(y)}M.apply(p,y),w&&!s&&y.length>0&&v+t.length>1&&at.uniqueSort(p)}return w&&(T=k,u=C),x};return r?lt(s):s}l=at.compile=function(e,t){var n,r=[],i=[],o=E[e+" "];if(!o){t||(t=mt(e)),n=t.length;while(n--)o=Tt(t[n]),o[b]?r.push(o):i.push(o);o=E(e,Ct(i,r))}return o};function Nt(e,t,n){var r=0,i=t.length;for(;i>r;r++)at(e,t[r],n);return n}function kt(e,t,n,i){var a,s,u,c,p,f=mt(e);if(!i&&1===f.length){if(s=f[0]=f[0].slice(0),s.length>2&&"ID"===(u=s[0]).type&&r.getById&&9===t.nodeType&&h&&o.relative[s[1].type]){if(t=(o.find.ID(u.matches[0].replace(rt,it),t)||[])[0],!t)return n;e=e.slice(s.shift().value.length)}a=Q.needsContext.test(e)?0:s.length;while(a--){if(u=s[a],o.relative[c=u.type])break;if((p=o.find[c])&&(i=p(u.matches[0].replace(rt,it),V.test(s[0].type)&&t.parentNode||t))){if(s.splice(a,1),e=i.length&&yt(s),!e)return M.apply(n,i),n;break}}}return l(e,f)(i,t,!h,n,V.test(e)),n}r.sortStable=b.split("").sort(A).join("")===b,r.detectDuplicates=S,p(),r.sortDetached=ut(function(e){return 1&e.compareDocumentPosition(f.createElement("div"))}),ut(function(e){return e.innerHTML="","#"===e.firstChild.getAttribute("href")})||ct("type|href|height|width",function(e,n,r){return r?t:e.getAttribute(n,"type"===n.toLowerCase()?1:2)}),r.attributes&&ut(function(e){return e.innerHTML="",e.firstChild.setAttribute("value",""),""===e.firstChild.getAttribute("value")})||ct("value",function(e,n,r){return r||"input"!==e.nodeName.toLowerCase()?t:e.defaultValue}),ut(function(e){return null==e.getAttribute("disabled")})||ct(B,function(e,n,r){var i;return r?t:(i=e.getAttributeNode(n))&&i.specified?i.value:e[n]===!0?n.toLowerCase():null}),x.find=at,x.expr=at.selectors,x.expr[":"]=x.expr.pseudos,x.unique=at.uniqueSort,x.text=at.getText,x.isXMLDoc=at.isXML,x.contains=at.contains}(e);var O={};function F(e){var t=O[e]={};return x.each(e.match(T)||[],function(e,n){t[n]=!0}),t}x.Callbacks=function(e){e="string"==typeof e?O[e]||F(e):x.extend({},e);var n,r,i,o,a,s,l=[],u=!e.once&&[],c=function(t){for(r=e.memory&&t,i=!0,a=s||0,s=0,o=l.length,n=!0;l&&o>a;a++)if(l[a].apply(t[0],t[1])===!1&&e.stopOnFalse){r=!1;break}n=!1,l&&(u?u.length&&c(u.shift()):r?l=[]:p.disable())},p={add:function(){if(l){var t=l.length;(function i(t){x.each(t,function(t,n){var r=x.type(n);"function"===r?e.unique&&p.has(n)||l.push(n):n&&n.length&&"string"!==r&&i(n)})})(arguments),n?o=l.length:r&&(s=t,c(r))}return this},remove:function(){return l&&x.each(arguments,function(e,t){var r;while((r=x.inArray(t,l,r))>-1)l.splice(r,1),n&&(o>=r&&o--,a>=r&&a--)}),this},has:function(e){return e?x.inArray(e,l)>-1:!(!l||!l.length)},empty:function(){return l=[],o=0,this},disable:function(){return l=u=r=t,this},disabled:function(){return!l},lock:function(){return u=t,r||p.disable(),this},locked:function(){return!u},fireWith:function(e,t){return!l||i&&!u||(t=t||[],t=[e,t.slice?t.slice():t],n?u.push(t):c(t)),this},fire:function(){return p.fireWith(this,arguments),this},fired:function(){return!!i}};return p},x.extend({Deferred:function(e){var t=[["resolve","done",x.Callbacks("once memory"),"resolved"],["reject","fail",x.Callbacks("once memory"),"rejected"],["notify","progress",x.Callbacks("memory")]],n="pending",r={state:function(){return n},always:function(){return i.done(arguments).fail(arguments),this},then:function(){var e=arguments;return x.Deferred(function(n){x.each(t,function(t,o){var a=o[0],s=x.isFunction(e[t])&&e[t];i[o[1]](function(){var e=s&&s.apply(this,arguments);e&&x.isFunction(e.promise)?e.promise().done(n.resolve).fail(n.reject).progress(n.notify):n[a+"With"](this===r?n.promise():this,s?[e]:arguments)})}),e=null}).promise()},promise:function(e){return null!=e?x.extend(e,r):r}},i={};return r.pipe=r.then,x.each(t,function(e,o){var a=o[2],s=o[3];r[o[1]]=a.add,s&&a.add(function(){n=s},t[1^e][2].disable,t[2][2].lock),i[o[0]]=function(){return i[o[0]+"With"](this===i?r:this,arguments),this},i[o[0]+"With"]=a.fireWith}),r.promise(i),e&&e.call(i,i),i},when:function(e){var t=0,n=g.call(arguments),r=n.length,i=1!==r||e&&x.isFunction(e.promise)?r:0,o=1===i?e:x.Deferred(),a=function(e,t,n){return function(r){t[e]=this,n[e]=arguments.length>1?g.call(arguments):r,n===s?o.notifyWith(t,n):--i||o.resolveWith(t,n)}},s,l,u;if(r>1)for(s=Array(r),l=Array(r),u=Array(r);r>t;t++)n[t]&&x.isFunction(n[t].promise)?n[t].promise().done(a(t,u,n)).fail(o.reject).progress(a(t,l,s)):--i;return i||o.resolveWith(u,n),o.promise()}}),x.support=function(t){var n,r,o,s,l,u,c,p,f,d=a.createElement("div");if(d.setAttribute("className","t"),d.innerHTML="
    a",n=d.getElementsByTagName("*")||[],r=d.getElementsByTagName("a")[0],!r||!r.style||!n.length)return t;s=a.createElement("select"),u=s.appendChild(a.createElement("option")),o=d.getElementsByTagName("input")[0],r.style.cssText="top:1px;float:left;opacity:.5",t.getSetAttribute="t"!==d.className,t.leadingWhitespace=3===d.firstChild.nodeType,t.tbody=!d.getElementsByTagName("tbody").length,t.htmlSerialize=!!d.getElementsByTagName("link").length,t.style=/top/.test(r.getAttribute("style")),t.hrefNormalized="/a"===r.getAttribute("href"),t.opacity=/^0.5/.test(r.style.opacity),t.cssFloat=!!r.style.cssFloat,t.checkOn=!!o.value,t.optSelected=u.selected,t.enctype=!!a.createElement("form").enctype,t.html5Clone="<:nav>"!==a.createElement("nav").cloneNode(!0).outerHTML,t.inlineBlockNeedsLayout=!1,t.shrinkWrapBlocks=!1,t.pixelPosition=!1,t.deleteExpando=!0,t.noCloneEvent=!0,t.reliableMarginRight=!0,t.boxSizingReliable=!0,o.checked=!0,t.noCloneChecked=o.cloneNode(!0).checked,s.disabled=!0,t.optDisabled=!u.disabled;try{delete d.test}catch(h){t.deleteExpando=!1}o=a.createElement("input"),o.setAttribute("value",""),t.input=""===o.getAttribute("value"),o.value="t",o.setAttribute("type","radio"),t.radioValue="t"===o.value,o.setAttribute("checked","t"),o.setAttribute("name","t"),l=a.createDocumentFragment(),l.appendChild(o),t.appendChecked=o.checked,t.checkClone=l.cloneNode(!0).cloneNode(!0).lastChild.checked,d.attachEvent&&(d.attachEvent("onclick",function(){t.noCloneEvent=!1}),d.cloneNode(!0).click());for(f in{submit:!0,change:!0,focusin:!0})d.setAttribute(c="on"+f,"t"),t[f+"Bubbles"]=c in e||d.attributes[c].expando===!1;d.style.backgroundClip="content-box",d.cloneNode(!0).style.backgroundClip="",t.clearCloneStyle="content-box"===d.style.backgroundClip;for(f in x(t))break;return t.ownLast="0"!==f,x(function(){var n,r,o,s="padding:0;margin:0;border:0;display:block;box-sizing:content-box;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;",l=a.getElementsByTagName("body")[0];l&&(n=a.createElement("div"),n.style.cssText="border:0;width:0;height:0;position:absolute;top:0;left:-9999px;margin-top:1px",l.appendChild(n).appendChild(d),d.innerHTML="
    t
    ",o=d.getElementsByTagName("td"),o[0].style.cssText="padding:0;margin:0;border:0;display:none",p=0===o[0].offsetHeight,o[0].style.display="",o[1].style.display="none",t.reliableHiddenOffsets=p&&0===o[0].offsetHeight,d.innerHTML="",d.style.cssText="box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;padding:1px;border:1px;display:block;width:4px;margin-top:1%;position:absolute;top:1%;",x.swap(l,null!=l.style.zoom?{zoom:1}:{},function(){t.boxSizing=4===d.offsetWidth}),e.getComputedStyle&&(t.pixelPosition="1%"!==(e.getComputedStyle(d,null)||{}).top,t.boxSizingReliable="4px"===(e.getComputedStyle(d,null)||{width:"4px"}).width,r=d.appendChild(a.createElement("div")),r.style.cssText=d.style.cssText=s,r.style.marginRight=r.style.width="0",d.style.width="1px",t.reliableMarginRight=!parseFloat((e.getComputedStyle(r,null)||{}).marginRight)),typeof d.style.zoom!==i&&(d.innerHTML="",d.style.cssText=s+"width:1px;padding:1px;display:inline;zoom:1",t.inlineBlockNeedsLayout=3===d.offsetWidth,d.style.display="block",d.innerHTML="
    ",d.firstChild.style.width="5px",t.shrinkWrapBlocks=3!==d.offsetWidth,t.inlineBlockNeedsLayout&&(l.style.zoom=1)),l.removeChild(n),n=d=o=r=null)}),n=s=l=u=r=o=null,t +}({});var B=/(?:\{[\s\S]*\}|\[[\s\S]*\])$/,P=/([A-Z])/g;function R(e,n,r,i){if(x.acceptData(e)){var o,a,s=x.expando,l=e.nodeType,u=l?x.cache:e,c=l?e[s]:e[s]&&s;if(c&&u[c]&&(i||u[c].data)||r!==t||"string"!=typeof n)return c||(c=l?e[s]=p.pop()||x.guid++:s),u[c]||(u[c]=l?{}:{toJSON:x.noop}),("object"==typeof n||"function"==typeof n)&&(i?u[c]=x.extend(u[c],n):u[c].data=x.extend(u[c].data,n)),a=u[c],i||(a.data||(a.data={}),a=a.data),r!==t&&(a[x.camelCase(n)]=r),"string"==typeof n?(o=a[n],null==o&&(o=a[x.camelCase(n)])):o=a,o}}function W(e,t,n){if(x.acceptData(e)){var r,i,o=e.nodeType,a=o?x.cache:e,s=o?e[x.expando]:x.expando;if(a[s]){if(t&&(r=n?a[s]:a[s].data)){x.isArray(t)?t=t.concat(x.map(t,x.camelCase)):t in r?t=[t]:(t=x.camelCase(t),t=t in r?[t]:t.split(" ")),i=t.length;while(i--)delete r[t[i]];if(n?!I(r):!x.isEmptyObject(r))return}(n||(delete a[s].data,I(a[s])))&&(o?x.cleanData([e],!0):x.support.deleteExpando||a!=a.window?delete a[s]:a[s]=null)}}}x.extend({cache:{},noData:{applet:!0,embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"},hasData:function(e){return e=e.nodeType?x.cache[e[x.expando]]:e[x.expando],!!e&&!I(e)},data:function(e,t,n){return R(e,t,n)},removeData:function(e,t){return W(e,t)},_data:function(e,t,n){return R(e,t,n,!0)},_removeData:function(e,t){return W(e,t,!0)},acceptData:function(e){if(e.nodeType&&1!==e.nodeType&&9!==e.nodeType)return!1;var t=e.nodeName&&x.noData[e.nodeName.toLowerCase()];return!t||t!==!0&&e.getAttribute("classid")===t}}),x.fn.extend({data:function(e,n){var r,i,o=null,a=0,s=this[0];if(e===t){if(this.length&&(o=x.data(s),1===s.nodeType&&!x._data(s,"parsedAttrs"))){for(r=s.attributes;r.length>a;a++)i=r[a].name,0===i.indexOf("data-")&&(i=x.camelCase(i.slice(5)),$(s,i,o[i]));x._data(s,"parsedAttrs",!0)}return o}return"object"==typeof e?this.each(function(){x.data(this,e)}):arguments.length>1?this.each(function(){x.data(this,e,n)}):s?$(s,e,x.data(s,e)):null},removeData:function(e){return this.each(function(){x.removeData(this,e)})}});function $(e,n,r){if(r===t&&1===e.nodeType){var i="data-"+n.replace(P,"-$1").toLowerCase();if(r=e.getAttribute(i),"string"==typeof r){try{r="true"===r?!0:"false"===r?!1:"null"===r?null:+r+""===r?+r:B.test(r)?x.parseJSON(r):r}catch(o){}x.data(e,n,r)}else r=t}return r}function I(e){var t;for(t in e)if(("data"!==t||!x.isEmptyObject(e[t]))&&"toJSON"!==t)return!1;return!0}x.extend({queue:function(e,n,r){var i;return e?(n=(n||"fx")+"queue",i=x._data(e,n),r&&(!i||x.isArray(r)?i=x._data(e,n,x.makeArray(r)):i.push(r)),i||[]):t},dequeue:function(e,t){t=t||"fx";var n=x.queue(e,t),r=n.length,i=n.shift(),o=x._queueHooks(e,t),a=function(){x.dequeue(e,t)};"inprogress"===i&&(i=n.shift(),r--),i&&("fx"===t&&n.unshift("inprogress"),delete o.stop,i.call(e,a,o)),!r&&o&&o.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return x._data(e,n)||x._data(e,n,{empty:x.Callbacks("once memory").add(function(){x._removeData(e,t+"queue"),x._removeData(e,n)})})}}),x.fn.extend({queue:function(e,n){var r=2;return"string"!=typeof e&&(n=e,e="fx",r--),r>arguments.length?x.queue(this[0],e):n===t?this:this.each(function(){var t=x.queue(this,e,n);x._queueHooks(this,e),"fx"===e&&"inprogress"!==t[0]&&x.dequeue(this,e)})},dequeue:function(e){return this.each(function(){x.dequeue(this,e)})},delay:function(e,t){return e=x.fx?x.fx.speeds[e]||e:e,t=t||"fx",this.queue(t,function(t,n){var r=setTimeout(t,e);n.stop=function(){clearTimeout(r)}})},clearQueue:function(e){return this.queue(e||"fx",[])},promise:function(e,n){var r,i=1,o=x.Deferred(),a=this,s=this.length,l=function(){--i||o.resolveWith(a,[a])};"string"!=typeof e&&(n=e,e=t),e=e||"fx";while(s--)r=x._data(a[s],e+"queueHooks"),r&&r.empty&&(i++,r.empty.add(l));return l(),o.promise(n)}});var z,X,U=/[\t\r\n\f]/g,V=/\r/g,Y=/^(?:input|select|textarea|button|object)$/i,J=/^(?:a|area)$/i,G=/^(?:checked|selected)$/i,Q=x.support.getSetAttribute,K=x.support.input;x.fn.extend({attr:function(e,t){return x.access(this,x.attr,e,t,arguments.length>1)},removeAttr:function(e){return this.each(function(){x.removeAttr(this,e)})},prop:function(e,t){return x.access(this,x.prop,e,t,arguments.length>1)},removeProp:function(e){return e=x.propFix[e]||e,this.each(function(){try{this[e]=t,delete this[e]}catch(n){}})},addClass:function(e){var t,n,r,i,o,a=0,s=this.length,l="string"==typeof e&&e;if(x.isFunction(e))return this.each(function(t){x(this).addClass(e.call(this,t,this.className))});if(l)for(t=(e||"").match(T)||[];s>a;a++)if(n=this[a],r=1===n.nodeType&&(n.className?(" "+n.className+" ").replace(U," "):" ")){o=0;while(i=t[o++])0>r.indexOf(" "+i+" ")&&(r+=i+" ");n.className=x.trim(r)}return this},removeClass:function(e){var t,n,r,i,o,a=0,s=this.length,l=0===arguments.length||"string"==typeof e&&e;if(x.isFunction(e))return this.each(function(t){x(this).removeClass(e.call(this,t,this.className))});if(l)for(t=(e||"").match(T)||[];s>a;a++)if(n=this[a],r=1===n.nodeType&&(n.className?(" "+n.className+" ").replace(U," "):"")){o=0;while(i=t[o++])while(r.indexOf(" "+i+" ")>=0)r=r.replace(" "+i+" "," ");n.className=e?x.trim(r):""}return this},toggleClass:function(e,t){var n=typeof e;return"boolean"==typeof t&&"string"===n?t?this.addClass(e):this.removeClass(e):x.isFunction(e)?this.each(function(n){x(this).toggleClass(e.call(this,n,this.className,t),t)}):this.each(function(){if("string"===n){var t,r=0,o=x(this),a=e.match(T)||[];while(t=a[r++])o.hasClass(t)?o.removeClass(t):o.addClass(t)}else(n===i||"boolean"===n)&&(this.className&&x._data(this,"__className__",this.className),this.className=this.className||e===!1?"":x._data(this,"__className__")||"")})},hasClass:function(e){var t=" "+e+" ",n=0,r=this.length;for(;r>n;n++)if(1===this[n].nodeType&&(" "+this[n].className+" ").replace(U," ").indexOf(t)>=0)return!0;return!1},val:function(e){var n,r,i,o=this[0];{if(arguments.length)return i=x.isFunction(e),this.each(function(n){var o;1===this.nodeType&&(o=i?e.call(this,n,x(this).val()):e,null==o?o="":"number"==typeof o?o+="":x.isArray(o)&&(o=x.map(o,function(e){return null==e?"":e+""})),r=x.valHooks[this.type]||x.valHooks[this.nodeName.toLowerCase()],r&&"set"in r&&r.set(this,o,"value")!==t||(this.value=o))});if(o)return r=x.valHooks[o.type]||x.valHooks[o.nodeName.toLowerCase()],r&&"get"in r&&(n=r.get(o,"value"))!==t?n:(n=o.value,"string"==typeof n?n.replace(V,""):null==n?"":n)}}}),x.extend({valHooks:{option:{get:function(e){var t=x.find.attr(e,"value");return null!=t?t:e.text}},select:{get:function(e){var t,n,r=e.options,i=e.selectedIndex,o="select-one"===e.type||0>i,a=o?null:[],s=o?i+1:r.length,l=0>i?s:o?i:0;for(;s>l;l++)if(n=r[l],!(!n.selected&&l!==i||(x.support.optDisabled?n.disabled:null!==n.getAttribute("disabled"))||n.parentNode.disabled&&x.nodeName(n.parentNode,"optgroup"))){if(t=x(n).val(),o)return t;a.push(t)}return a},set:function(e,t){var n,r,i=e.options,o=x.makeArray(t),a=i.length;while(a--)r=i[a],(r.selected=x.inArray(x(r).val(),o)>=0)&&(n=!0);return n||(e.selectedIndex=-1),o}}},attr:function(e,n,r){var o,a,s=e.nodeType;if(e&&3!==s&&8!==s&&2!==s)return typeof e.getAttribute===i?x.prop(e,n,r):(1===s&&x.isXMLDoc(e)||(n=n.toLowerCase(),o=x.attrHooks[n]||(x.expr.match.bool.test(n)?X:z)),r===t?o&&"get"in o&&null!==(a=o.get(e,n))?a:(a=x.find.attr(e,n),null==a?t:a):null!==r?o&&"set"in o&&(a=o.set(e,r,n))!==t?a:(e.setAttribute(n,r+""),r):(x.removeAttr(e,n),t))},removeAttr:function(e,t){var n,r,i=0,o=t&&t.match(T);if(o&&1===e.nodeType)while(n=o[i++])r=x.propFix[n]||n,x.expr.match.bool.test(n)?K&&Q||!G.test(n)?e[r]=!1:e[x.camelCase("default-"+n)]=e[r]=!1:x.attr(e,n,""),e.removeAttribute(Q?n:r)},attrHooks:{type:{set:function(e,t){if(!x.support.radioValue&&"radio"===t&&x.nodeName(e,"input")){var n=e.value;return e.setAttribute("type",t),n&&(e.value=n),t}}}},propFix:{"for":"htmlFor","class":"className"},prop:function(e,n,r){var i,o,a,s=e.nodeType;if(e&&3!==s&&8!==s&&2!==s)return a=1!==s||!x.isXMLDoc(e),a&&(n=x.propFix[n]||n,o=x.propHooks[n]),r!==t?o&&"set"in o&&(i=o.set(e,r,n))!==t?i:e[n]=r:o&&"get"in o&&null!==(i=o.get(e,n))?i:e[n]},propHooks:{tabIndex:{get:function(e){var t=x.find.attr(e,"tabindex");return t?parseInt(t,10):Y.test(e.nodeName)||J.test(e.nodeName)&&e.href?0:-1}}}}),X={set:function(e,t,n){return t===!1?x.removeAttr(e,n):K&&Q||!G.test(n)?e.setAttribute(!Q&&x.propFix[n]||n,n):e[x.camelCase("default-"+n)]=e[n]=!0,n}},x.each(x.expr.match.bool.source.match(/\w+/g),function(e,n){var r=x.expr.attrHandle[n]||x.find.attr;x.expr.attrHandle[n]=K&&Q||!G.test(n)?function(e,n,i){var o=x.expr.attrHandle[n],a=i?t:(x.expr.attrHandle[n]=t)!=r(e,n,i)?n.toLowerCase():null;return x.expr.attrHandle[n]=o,a}:function(e,n,r){return r?t:e[x.camelCase("default-"+n)]?n.toLowerCase():null}}),K&&Q||(x.attrHooks.value={set:function(e,n,r){return x.nodeName(e,"input")?(e.defaultValue=n,t):z&&z.set(e,n,r)}}),Q||(z={set:function(e,n,r){var i=e.getAttributeNode(r);return i||e.setAttributeNode(i=e.ownerDocument.createAttribute(r)),i.value=n+="","value"===r||n===e.getAttribute(r)?n:t}},x.expr.attrHandle.id=x.expr.attrHandle.name=x.expr.attrHandle.coords=function(e,n,r){var i;return r?t:(i=e.getAttributeNode(n))&&""!==i.value?i.value:null},x.valHooks.button={get:function(e,n){var r=e.getAttributeNode(n);return r&&r.specified?r.value:t},set:z.set},x.attrHooks.contenteditable={set:function(e,t,n){z.set(e,""===t?!1:t,n)}},x.each(["width","height"],function(e,n){x.attrHooks[n]={set:function(e,r){return""===r?(e.setAttribute(n,"auto"),r):t}}})),x.support.hrefNormalized||x.each(["href","src"],function(e,t){x.propHooks[t]={get:function(e){return e.getAttribute(t,4)}}}),x.support.style||(x.attrHooks.style={get:function(e){return e.style.cssText||t},set:function(e,t){return e.style.cssText=t+""}}),x.support.optSelected||(x.propHooks.selected={get:function(e){var t=e.parentNode;return t&&(t.selectedIndex,t.parentNode&&t.parentNode.selectedIndex),null}}),x.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){x.propFix[this.toLowerCase()]=this}),x.support.enctype||(x.propFix.enctype="encoding"),x.each(["radio","checkbox"],function(){x.valHooks[this]={set:function(e,n){return x.isArray(n)?e.checked=x.inArray(x(e).val(),n)>=0:t}},x.support.checkOn||(x.valHooks[this].get=function(e){return null===e.getAttribute("value")?"on":e.value})});var Z=/^(?:input|select|textarea)$/i,et=/^key/,tt=/^(?:mouse|contextmenu)|click/,nt=/^(?:focusinfocus|focusoutblur)$/,rt=/^([^.]*)(?:\.(.+)|)$/;function it(){return!0}function ot(){return!1}function at(){try{return a.activeElement}catch(e){}}x.event={global:{},add:function(e,n,r,o,a){var s,l,u,c,p,f,d,h,g,m,y,v=x._data(e);if(v){r.handler&&(c=r,r=c.handler,a=c.selector),r.guid||(r.guid=x.guid++),(l=v.events)||(l=v.events={}),(f=v.handle)||(f=v.handle=function(e){return typeof x===i||e&&x.event.triggered===e.type?t:x.event.dispatch.apply(f.elem,arguments)},f.elem=e),n=(n||"").match(T)||[""],u=n.length;while(u--)s=rt.exec(n[u])||[],g=y=s[1],m=(s[2]||"").split(".").sort(),g&&(p=x.event.special[g]||{},g=(a?p.delegateType:p.bindType)||g,p=x.event.special[g]||{},d=x.extend({type:g,origType:y,data:o,handler:r,guid:r.guid,selector:a,needsContext:a&&x.expr.match.needsContext.test(a),namespace:m.join(".")},c),(h=l[g])||(h=l[g]=[],h.delegateCount=0,p.setup&&p.setup.call(e,o,m,f)!==!1||(e.addEventListener?e.addEventListener(g,f,!1):e.attachEvent&&e.attachEvent("on"+g,f))),p.add&&(p.add.call(e,d),d.handler.guid||(d.handler.guid=r.guid)),a?h.splice(h.delegateCount++,0,d):h.push(d),x.event.global[g]=!0);e=null}},remove:function(e,t,n,r,i){var o,a,s,l,u,c,p,f,d,h,g,m=x.hasData(e)&&x._data(e);if(m&&(c=m.events)){t=(t||"").match(T)||[""],u=t.length;while(u--)if(s=rt.exec(t[u])||[],d=g=s[1],h=(s[2]||"").split(".").sort(),d){p=x.event.special[d]||{},d=(r?p.delegateType:p.bindType)||d,f=c[d]||[],s=s[2]&&RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"),l=o=f.length;while(o--)a=f[o],!i&&g!==a.origType||n&&n.guid!==a.guid||s&&!s.test(a.namespace)||r&&r!==a.selector&&("**"!==r||!a.selector)||(f.splice(o,1),a.selector&&f.delegateCount--,p.remove&&p.remove.call(e,a));l&&!f.length&&(p.teardown&&p.teardown.call(e,h,m.handle)!==!1||x.removeEvent(e,d,m.handle),delete c[d])}else for(d in c)x.event.remove(e,d+t[u],n,r,!0);x.isEmptyObject(c)&&(delete m.handle,x._removeData(e,"events"))}},trigger:function(n,r,i,o){var s,l,u,c,p,f,d,h=[i||a],g=v.call(n,"type")?n.type:n,m=v.call(n,"namespace")?n.namespace.split("."):[];if(u=f=i=i||a,3!==i.nodeType&&8!==i.nodeType&&!nt.test(g+x.event.triggered)&&(g.indexOf(".")>=0&&(m=g.split("."),g=m.shift(),m.sort()),l=0>g.indexOf(":")&&"on"+g,n=n[x.expando]?n:new x.Event(g,"object"==typeof n&&n),n.isTrigger=o?2:3,n.namespace=m.join("."),n.namespace_re=n.namespace?RegExp("(^|\\.)"+m.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,n.result=t,n.target||(n.target=i),r=null==r?[n]:x.makeArray(r,[n]),p=x.event.special[g]||{},o||!p.trigger||p.trigger.apply(i,r)!==!1)){if(!o&&!p.noBubble&&!x.isWindow(i)){for(c=p.delegateType||g,nt.test(c+g)||(u=u.parentNode);u;u=u.parentNode)h.push(u),f=u;f===(i.ownerDocument||a)&&h.push(f.defaultView||f.parentWindow||e)}d=0;while((u=h[d++])&&!n.isPropagationStopped())n.type=d>1?c:p.bindType||g,s=(x._data(u,"events")||{})[n.type]&&x._data(u,"handle"),s&&s.apply(u,r),s=l&&u[l],s&&x.acceptData(u)&&s.apply&&s.apply(u,r)===!1&&n.preventDefault();if(n.type=g,!o&&!n.isDefaultPrevented()&&(!p._default||p._default.apply(h.pop(),r)===!1)&&x.acceptData(i)&&l&&i[g]&&!x.isWindow(i)){f=i[l],f&&(i[l]=null),x.event.triggered=g;try{i[g]()}catch(y){}x.event.triggered=t,f&&(i[l]=f)}return n.result}},dispatch:function(e){e=x.event.fix(e);var n,r,i,o,a,s=[],l=g.call(arguments),u=(x._data(this,"events")||{})[e.type]||[],c=x.event.special[e.type]||{};if(l[0]=e,e.delegateTarget=this,!c.preDispatch||c.preDispatch.call(this,e)!==!1){s=x.event.handlers.call(this,e,u),n=0;while((o=s[n++])&&!e.isPropagationStopped()){e.currentTarget=o.elem,a=0;while((i=o.handlers[a++])&&!e.isImmediatePropagationStopped())(!e.namespace_re||e.namespace_re.test(i.namespace))&&(e.handleObj=i,e.data=i.data,r=((x.event.special[i.origType]||{}).handle||i.handler).apply(o.elem,l),r!==t&&(e.result=r)===!1&&(e.preventDefault(),e.stopPropagation()))}return c.postDispatch&&c.postDispatch.call(this,e),e.result}},handlers:function(e,n){var r,i,o,a,s=[],l=n.delegateCount,u=e.target;if(l&&u.nodeType&&(!e.button||"click"!==e.type))for(;u!=this;u=u.parentNode||this)if(1===u.nodeType&&(u.disabled!==!0||"click"!==e.type)){for(o=[],a=0;l>a;a++)i=n[a],r=i.selector+" ",o[r]===t&&(o[r]=i.needsContext?x(r,this).index(u)>=0:x.find(r,this,null,[u]).length),o[r]&&o.push(i);o.length&&s.push({elem:u,handlers:o})}return n.length>l&&s.push({elem:this,handlers:n.slice(l)}),s},fix:function(e){if(e[x.expando])return e;var t,n,r,i=e.type,o=e,s=this.fixHooks[i];s||(this.fixHooks[i]=s=tt.test(i)?this.mouseHooks:et.test(i)?this.keyHooks:{}),r=s.props?this.props.concat(s.props):this.props,e=new x.Event(o),t=r.length;while(t--)n=r[t],e[n]=o[n];return e.target||(e.target=o.srcElement||a),3===e.target.nodeType&&(e.target=e.target.parentNode),e.metaKey=!!e.metaKey,s.filter?s.filter(e,o):e},props:"altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),fixHooks:{},keyHooks:{props:"char charCode key keyCode".split(" "),filter:function(e,t){return null==e.which&&(e.which=null!=t.charCode?t.charCode:t.keyCode),e}},mouseHooks:{props:"button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "),filter:function(e,n){var r,i,o,s=n.button,l=n.fromElement;return null==e.pageX&&null!=n.clientX&&(i=e.target.ownerDocument||a,o=i.documentElement,r=i.body,e.pageX=n.clientX+(o&&o.scrollLeft||r&&r.scrollLeft||0)-(o&&o.clientLeft||r&&r.clientLeft||0),e.pageY=n.clientY+(o&&o.scrollTop||r&&r.scrollTop||0)-(o&&o.clientTop||r&&r.clientTop||0)),!e.relatedTarget&&l&&(e.relatedTarget=l===e.target?n.toElement:l),e.which||s===t||(e.which=1&s?1:2&s?3:4&s?2:0),e}},special:{load:{noBubble:!0},focus:{trigger:function(){if(this!==at()&&this.focus)try{return this.focus(),!1}catch(e){}},delegateType:"focusin"},blur:{trigger:function(){return this===at()&&this.blur?(this.blur(),!1):t},delegateType:"focusout"},click:{trigger:function(){return x.nodeName(this,"input")&&"checkbox"===this.type&&this.click?(this.click(),!1):t},_default:function(e){return x.nodeName(e.target,"a")}},beforeunload:{postDispatch:function(e){e.result!==t&&(e.originalEvent.returnValue=e.result)}}},simulate:function(e,t,n,r){var i=x.extend(new x.Event,n,{type:e,isSimulated:!0,originalEvent:{}});r?x.event.trigger(i,null,t):x.event.dispatch.call(t,i),i.isDefaultPrevented()&&n.preventDefault()}},x.removeEvent=a.removeEventListener?function(e,t,n){e.removeEventListener&&e.removeEventListener(t,n,!1)}:function(e,t,n){var r="on"+t;e.detachEvent&&(typeof e[r]===i&&(e[r]=null),e.detachEvent(r,n))},x.Event=function(e,n){return this instanceof x.Event?(e&&e.type?(this.originalEvent=e,this.type=e.type,this.isDefaultPrevented=e.defaultPrevented||e.returnValue===!1||e.getPreventDefault&&e.getPreventDefault()?it:ot):this.type=e,n&&x.extend(this,n),this.timeStamp=e&&e.timeStamp||x.now(),this[x.expando]=!0,t):new x.Event(e,n)},x.Event.prototype={isDefaultPrevented:ot,isPropagationStopped:ot,isImmediatePropagationStopped:ot,preventDefault:function(){var e=this.originalEvent;this.isDefaultPrevented=it,e&&(e.preventDefault?e.preventDefault():e.returnValue=!1)},stopPropagation:function(){var e=this.originalEvent;this.isPropagationStopped=it,e&&(e.stopPropagation&&e.stopPropagation(),e.cancelBubble=!0)},stopImmediatePropagation:function(){this.isImmediatePropagationStopped=it,this.stopPropagation()}},x.each({mouseenter:"mouseover",mouseleave:"mouseout"},function(e,t){x.event.special[e]={delegateType:t,bindType:t,handle:function(e){var n,r=this,i=e.relatedTarget,o=e.handleObj;return(!i||i!==r&&!x.contains(r,i))&&(e.type=o.origType,n=o.handler.apply(this,arguments),e.type=t),n}}}),x.support.submitBubbles||(x.event.special.submit={setup:function(){return x.nodeName(this,"form")?!1:(x.event.add(this,"click._submit keypress._submit",function(e){var n=e.target,r=x.nodeName(n,"input")||x.nodeName(n,"button")?n.form:t;r&&!x._data(r,"submitBubbles")&&(x.event.add(r,"submit._submit",function(e){e._submit_bubble=!0}),x._data(r,"submitBubbles",!0))}),t)},postDispatch:function(e){e._submit_bubble&&(delete e._submit_bubble,this.parentNode&&!e.isTrigger&&x.event.simulate("submit",this.parentNode,e,!0))},teardown:function(){return x.nodeName(this,"form")?!1:(x.event.remove(this,"._submit"),t)}}),x.support.changeBubbles||(x.event.special.change={setup:function(){return Z.test(this.nodeName)?(("checkbox"===this.type||"radio"===this.type)&&(x.event.add(this,"propertychange._change",function(e){"checked"===e.originalEvent.propertyName&&(this._just_changed=!0)}),x.event.add(this,"click._change",function(e){this._just_changed&&!e.isTrigger&&(this._just_changed=!1),x.event.simulate("change",this,e,!0)})),!1):(x.event.add(this,"beforeactivate._change",function(e){var t=e.target;Z.test(t.nodeName)&&!x._data(t,"changeBubbles")&&(x.event.add(t,"change._change",function(e){!this.parentNode||e.isSimulated||e.isTrigger||x.event.simulate("change",this.parentNode,e,!0)}),x._data(t,"changeBubbles",!0))}),t)},handle:function(e){var n=e.target;return this!==n||e.isSimulated||e.isTrigger||"radio"!==n.type&&"checkbox"!==n.type?e.handleObj.handler.apply(this,arguments):t},teardown:function(){return x.event.remove(this,"._change"),!Z.test(this.nodeName)}}),x.support.focusinBubbles||x.each({focus:"focusin",blur:"focusout"},function(e,t){var n=0,r=function(e){x.event.simulate(t,e.target,x.event.fix(e),!0)};x.event.special[t]={setup:function(){0===n++&&a.addEventListener(e,r,!0)},teardown:function(){0===--n&&a.removeEventListener(e,r,!0)}}}),x.fn.extend({on:function(e,n,r,i,o){var a,s;if("object"==typeof e){"string"!=typeof n&&(r=r||n,n=t);for(a in e)this.on(a,n,r,e[a],o);return this}if(null==r&&null==i?(i=n,r=n=t):null==i&&("string"==typeof n?(i=r,r=t):(i=r,r=n,n=t)),i===!1)i=ot;else if(!i)return this;return 1===o&&(s=i,i=function(e){return x().off(e),s.apply(this,arguments)},i.guid=s.guid||(s.guid=x.guid++)),this.each(function(){x.event.add(this,e,i,r,n)})},one:function(e,t,n,r){return this.on(e,t,n,r,1)},off:function(e,n,r){var i,o;if(e&&e.preventDefault&&e.handleObj)return i=e.handleObj,x(e.delegateTarget).off(i.namespace?i.origType+"."+i.namespace:i.origType,i.selector,i.handler),this;if("object"==typeof e){for(o in e)this.off(o,n,e[o]);return this}return(n===!1||"function"==typeof n)&&(r=n,n=t),r===!1&&(r=ot),this.each(function(){x.event.remove(this,e,r,n)})},trigger:function(e,t){return this.each(function(){x.event.trigger(e,t,this)})},triggerHandler:function(e,n){var r=this[0];return r?x.event.trigger(e,n,r,!0):t}});var st=/^.[^:#\[\.,]*$/,lt=/^(?:parents|prev(?:Until|All))/,ut=x.expr.match.needsContext,ct={children:!0,contents:!0,next:!0,prev:!0};x.fn.extend({find:function(e){var t,n=[],r=this,i=r.length;if("string"!=typeof e)return this.pushStack(x(e).filter(function(){for(t=0;i>t;t++)if(x.contains(r[t],this))return!0}));for(t=0;i>t;t++)x.find(e,r[t],n);return n=this.pushStack(i>1?x.unique(n):n),n.selector=this.selector?this.selector+" "+e:e,n},has:function(e){var t,n=x(e,this),r=n.length;return this.filter(function(){for(t=0;r>t;t++)if(x.contains(this,n[t]))return!0})},not:function(e){return this.pushStack(ft(this,e||[],!0))},filter:function(e){return this.pushStack(ft(this,e||[],!1))},is:function(e){return!!ft(this,"string"==typeof e&&ut.test(e)?x(e):e||[],!1).length},closest:function(e,t){var n,r=0,i=this.length,o=[],a=ut.test(e)||"string"!=typeof e?x(e,t||this.context):0;for(;i>r;r++)for(n=this[r];n&&n!==t;n=n.parentNode)if(11>n.nodeType&&(a?a.index(n)>-1:1===n.nodeType&&x.find.matchesSelector(n,e))){n=o.push(n);break}return this.pushStack(o.length>1?x.unique(o):o)},index:function(e){return e?"string"==typeof e?x.inArray(this[0],x(e)):x.inArray(e.jquery?e[0]:e,this):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(e,t){var n="string"==typeof e?x(e,t):x.makeArray(e&&e.nodeType?[e]:e),r=x.merge(this.get(),n);return this.pushStack(x.unique(r))},addBack:function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}});function pt(e,t){do e=e[t];while(e&&1!==e.nodeType);return e}x.each({parent:function(e){var t=e.parentNode;return t&&11!==t.nodeType?t:null},parents:function(e){return x.dir(e,"parentNode")},parentsUntil:function(e,t,n){return x.dir(e,"parentNode",n)},next:function(e){return pt(e,"nextSibling")},prev:function(e){return pt(e,"previousSibling")},nextAll:function(e){return x.dir(e,"nextSibling")},prevAll:function(e){return x.dir(e,"previousSibling")},nextUntil:function(e,t,n){return x.dir(e,"nextSibling",n)},prevUntil:function(e,t,n){return x.dir(e,"previousSibling",n)},siblings:function(e){return x.sibling((e.parentNode||{}).firstChild,e)},children:function(e){return x.sibling(e.firstChild)},contents:function(e){return x.nodeName(e,"iframe")?e.contentDocument||e.contentWindow.document:x.merge([],e.childNodes)}},function(e,t){x.fn[e]=function(n,r){var i=x.map(this,t,n);return"Until"!==e.slice(-5)&&(r=n),r&&"string"==typeof r&&(i=x.filter(r,i)),this.length>1&&(ct[e]||(i=x.unique(i)),lt.test(e)&&(i=i.reverse())),this.pushStack(i)}}),x.extend({filter:function(e,t,n){var r=t[0];return n&&(e=":not("+e+")"),1===t.length&&1===r.nodeType?x.find.matchesSelector(r,e)?[r]:[]:x.find.matches(e,x.grep(t,function(e){return 1===e.nodeType}))},dir:function(e,n,r){var i=[],o=e[n];while(o&&9!==o.nodeType&&(r===t||1!==o.nodeType||!x(o).is(r)))1===o.nodeType&&i.push(o),o=o[n];return i},sibling:function(e,t){var n=[];for(;e;e=e.nextSibling)1===e.nodeType&&e!==t&&n.push(e);return n}});function ft(e,t,n){if(x.isFunction(t))return x.grep(e,function(e,r){return!!t.call(e,r,e)!==n});if(t.nodeType)return x.grep(e,function(e){return e===t!==n});if("string"==typeof t){if(st.test(t))return x.filter(t,e,n);t=x.filter(t,e)}return x.grep(e,function(e){return x.inArray(e,t)>=0!==n})}function dt(e){var t=ht.split("|"),n=e.createDocumentFragment();if(n.createElement)while(t.length)n.createElement(t.pop());return n}var ht="abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",gt=/ jQuery\d+="(?:null|\d+)"/g,mt=RegExp("<(?:"+ht+")[\\s/>]","i"),yt=/^\s+/,vt=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,bt=/<([\w:]+)/,xt=/\s*$/g,At={option:[1,""],legend:[1,"
    ","
    "],area:[1,"",""],param:[1,"",""],thead:[1,"","
    "],tr:[2,"","
    "],col:[2,"","
    "],td:[3,"","
    "],_default:x.support.htmlSerialize?[0,"",""]:[1,"X
    ","
    "]},jt=dt(a),Dt=jt.appendChild(a.createElement("div"));At.optgroup=At.option,At.tbody=At.tfoot=At.colgroup=At.caption=At.thead,At.th=At.td,x.fn.extend({text:function(e){return x.access(this,function(e){return e===t?x.text(this):this.empty().append((this[0]&&this[0].ownerDocument||a).createTextNode(e))},null,e,arguments.length)},append:function(){return this.domManip(arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=Lt(this,e);t.appendChild(e)}})},prepend:function(){return this.domManip(arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=Lt(this,e);t.insertBefore(e,t.firstChild)}})},before:function(){return this.domManip(arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this)})},after:function(){return this.domManip(arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this.nextSibling)})},remove:function(e,t){var n,r=e?x.filter(e,this):this,i=0;for(;null!=(n=r[i]);i++)t||1!==n.nodeType||x.cleanData(Ft(n)),n.parentNode&&(t&&x.contains(n.ownerDocument,n)&&_t(Ft(n,"script")),n.parentNode.removeChild(n));return this},empty:function(){var e,t=0;for(;null!=(e=this[t]);t++){1===e.nodeType&&x.cleanData(Ft(e,!1));while(e.firstChild)e.removeChild(e.firstChild);e.options&&x.nodeName(e,"select")&&(e.options.length=0)}return this},clone:function(e,t){return e=null==e?!1:e,t=null==t?e:t,this.map(function(){return x.clone(this,e,t)})},html:function(e){return x.access(this,function(e){var n=this[0]||{},r=0,i=this.length;if(e===t)return 1===n.nodeType?n.innerHTML.replace(gt,""):t;if(!("string"!=typeof e||Tt.test(e)||!x.support.htmlSerialize&&mt.test(e)||!x.support.leadingWhitespace&&yt.test(e)||At[(bt.exec(e)||["",""])[1].toLowerCase()])){e=e.replace(vt,"<$1>");try{for(;i>r;r++)n=this[r]||{},1===n.nodeType&&(x.cleanData(Ft(n,!1)),n.innerHTML=e);n=0}catch(o){}}n&&this.empty().append(e)},null,e,arguments.length)},replaceWith:function(){var e=x.map(this,function(e){return[e.nextSibling,e.parentNode]}),t=0;return this.domManip(arguments,function(n){var r=e[t++],i=e[t++];i&&(r&&r.parentNode!==i&&(r=this.nextSibling),x(this).remove(),i.insertBefore(n,r))},!0),t?this:this.remove()},detach:function(e){return this.remove(e,!0)},domManip:function(e,t,n){e=d.apply([],e);var r,i,o,a,s,l,u=0,c=this.length,p=this,f=c-1,h=e[0],g=x.isFunction(h);if(g||!(1>=c||"string"!=typeof h||x.support.checkClone)&&Nt.test(h))return this.each(function(r){var i=p.eq(r);g&&(e[0]=h.call(this,r,i.html())),i.domManip(e,t,n)});if(c&&(l=x.buildFragment(e,this[0].ownerDocument,!1,!n&&this),r=l.firstChild,1===l.childNodes.length&&(l=r),r)){for(a=x.map(Ft(l,"script"),Ht),o=a.length;c>u;u++)i=l,u!==f&&(i=x.clone(i,!0,!0),o&&x.merge(a,Ft(i,"script"))),t.call(this[u],i,u);if(o)for(s=a[a.length-1].ownerDocument,x.map(a,qt),u=0;o>u;u++)i=a[u],kt.test(i.type||"")&&!x._data(i,"globalEval")&&x.contains(s,i)&&(i.src?x._evalUrl(i.src):x.globalEval((i.text||i.textContent||i.innerHTML||"").replace(St,"")));l=r=null}return this}});function Lt(e,t){return x.nodeName(e,"table")&&x.nodeName(1===t.nodeType?t:t.firstChild,"tr")?e.getElementsByTagName("tbody")[0]||e.appendChild(e.ownerDocument.createElement("tbody")):e}function Ht(e){return e.type=(null!==x.find.attr(e,"type"))+"/"+e.type,e}function qt(e){var t=Et.exec(e.type);return t?e.type=t[1]:e.removeAttribute("type"),e}function _t(e,t){var n,r=0;for(;null!=(n=e[r]);r++)x._data(n,"globalEval",!t||x._data(t[r],"globalEval"))}function Mt(e,t){if(1===t.nodeType&&x.hasData(e)){var n,r,i,o=x._data(e),a=x._data(t,o),s=o.events;if(s){delete a.handle,a.events={};for(n in s)for(r=0,i=s[n].length;i>r;r++)x.event.add(t,n,s[n][r])}a.data&&(a.data=x.extend({},a.data))}}function Ot(e,t){var n,r,i;if(1===t.nodeType){if(n=t.nodeName.toLowerCase(),!x.support.noCloneEvent&&t[x.expando]){i=x._data(t);for(r in i.events)x.removeEvent(t,r,i.handle);t.removeAttribute(x.expando)}"script"===n&&t.text!==e.text?(Ht(t).text=e.text,qt(t)):"object"===n?(t.parentNode&&(t.outerHTML=e.outerHTML),x.support.html5Clone&&e.innerHTML&&!x.trim(t.innerHTML)&&(t.innerHTML=e.innerHTML)):"input"===n&&Ct.test(e.type)?(t.defaultChecked=t.checked=e.checked,t.value!==e.value&&(t.value=e.value)):"option"===n?t.defaultSelected=t.selected=e.defaultSelected:("input"===n||"textarea"===n)&&(t.defaultValue=e.defaultValue)}}x.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(e,t){x.fn[e]=function(e){var n,r=0,i=[],o=x(e),a=o.length-1;for(;a>=r;r++)n=r===a?this:this.clone(!0),x(o[r])[t](n),h.apply(i,n.get());return this.pushStack(i)}});function Ft(e,n){var r,o,a=0,s=typeof e.getElementsByTagName!==i?e.getElementsByTagName(n||"*"):typeof e.querySelectorAll!==i?e.querySelectorAll(n||"*"):t;if(!s)for(s=[],r=e.childNodes||e;null!=(o=r[a]);a++)!n||x.nodeName(o,n)?s.push(o):x.merge(s,Ft(o,n));return n===t||n&&x.nodeName(e,n)?x.merge([e],s):s}function Bt(e){Ct.test(e.type)&&(e.defaultChecked=e.checked)}x.extend({clone:function(e,t,n){var r,i,o,a,s,l=x.contains(e.ownerDocument,e);if(x.support.html5Clone||x.isXMLDoc(e)||!mt.test("<"+e.nodeName+">")?o=e.cloneNode(!0):(Dt.innerHTML=e.outerHTML,Dt.removeChild(o=Dt.firstChild)),!(x.support.noCloneEvent&&x.support.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||x.isXMLDoc(e)))for(r=Ft(o),s=Ft(e),a=0;null!=(i=s[a]);++a)r[a]&&Ot(i,r[a]);if(t)if(n)for(s=s||Ft(e),r=r||Ft(o),a=0;null!=(i=s[a]);a++)Mt(i,r[a]);else Mt(e,o);return r=Ft(o,"script"),r.length>0&&_t(r,!l&&Ft(e,"script")),r=s=i=null,o},buildFragment:function(e,t,n,r){var i,o,a,s,l,u,c,p=e.length,f=dt(t),d=[],h=0;for(;p>h;h++)if(o=e[h],o||0===o)if("object"===x.type(o))x.merge(d,o.nodeType?[o]:o);else if(wt.test(o)){s=s||f.appendChild(t.createElement("div")),l=(bt.exec(o)||["",""])[1].toLowerCase(),c=At[l]||At._default,s.innerHTML=c[1]+o.replace(vt,"<$1>")+c[2],i=c[0];while(i--)s=s.lastChild;if(!x.support.leadingWhitespace&&yt.test(o)&&d.push(t.createTextNode(yt.exec(o)[0])),!x.support.tbody){o="table"!==l||xt.test(o)?""!==c[1]||xt.test(o)?0:s:s.firstChild,i=o&&o.childNodes.length;while(i--)x.nodeName(u=o.childNodes[i],"tbody")&&!u.childNodes.length&&o.removeChild(u)}x.merge(d,s.childNodes),s.textContent="";while(s.firstChild)s.removeChild(s.firstChild);s=f.lastChild}else d.push(t.createTextNode(o));s&&f.removeChild(s),x.support.appendChecked||x.grep(Ft(d,"input"),Bt),h=0;while(o=d[h++])if((!r||-1===x.inArray(o,r))&&(a=x.contains(o.ownerDocument,o),s=Ft(f.appendChild(o),"script"),a&&_t(s),n)){i=0;while(o=s[i++])kt.test(o.type||"")&&n.push(o)}return s=null,f},cleanData:function(e,t){var n,r,o,a,s=0,l=x.expando,u=x.cache,c=x.support.deleteExpando,f=x.event.special;for(;null!=(n=e[s]);s++)if((t||x.acceptData(n))&&(o=n[l],a=o&&u[o])){if(a.events)for(r in a.events)f[r]?x.event.remove(n,r):x.removeEvent(n,r,a.handle); +u[o]&&(delete u[o],c?delete n[l]:typeof n.removeAttribute!==i?n.removeAttribute(l):n[l]=null,p.push(o))}},_evalUrl:function(e){return x.ajax({url:e,type:"GET",dataType:"script",async:!1,global:!1,"throws":!0})}}),x.fn.extend({wrapAll:function(e){if(x.isFunction(e))return this.each(function(t){x(this).wrapAll(e.call(this,t))});if(this[0]){var t=x(e,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&t.insertBefore(this[0]),t.map(function(){var e=this;while(e.firstChild&&1===e.firstChild.nodeType)e=e.firstChild;return e}).append(this)}return this},wrapInner:function(e){return x.isFunction(e)?this.each(function(t){x(this).wrapInner(e.call(this,t))}):this.each(function(){var t=x(this),n=t.contents();n.length?n.wrapAll(e):t.append(e)})},wrap:function(e){var t=x.isFunction(e);return this.each(function(n){x(this).wrapAll(t?e.call(this,n):e)})},unwrap:function(){return this.parent().each(function(){x.nodeName(this,"body")||x(this).replaceWith(this.childNodes)}).end()}});var Pt,Rt,Wt,$t=/alpha\([^)]*\)/i,It=/opacity\s*=\s*([^)]*)/,zt=/^(top|right|bottom|left)$/,Xt=/^(none|table(?!-c[ea]).+)/,Ut=/^margin/,Vt=RegExp("^("+w+")(.*)$","i"),Yt=RegExp("^("+w+")(?!px)[a-z%]+$","i"),Jt=RegExp("^([+-])=("+w+")","i"),Gt={BODY:"block"},Qt={position:"absolute",visibility:"hidden",display:"block"},Kt={letterSpacing:0,fontWeight:400},Zt=["Top","Right","Bottom","Left"],en=["Webkit","O","Moz","ms"];function tn(e,t){if(t in e)return t;var n=t.charAt(0).toUpperCase()+t.slice(1),r=t,i=en.length;while(i--)if(t=en[i]+n,t in e)return t;return r}function nn(e,t){return e=t||e,"none"===x.css(e,"display")||!x.contains(e.ownerDocument,e)}function rn(e,t){var n,r,i,o=[],a=0,s=e.length;for(;s>a;a++)r=e[a],r.style&&(o[a]=x._data(r,"olddisplay"),n=r.style.display,t?(o[a]||"none"!==n||(r.style.display=""),""===r.style.display&&nn(r)&&(o[a]=x._data(r,"olddisplay",ln(r.nodeName)))):o[a]||(i=nn(r),(n&&"none"!==n||!i)&&x._data(r,"olddisplay",i?n:x.css(r,"display"))));for(a=0;s>a;a++)r=e[a],r.style&&(t&&"none"!==r.style.display&&""!==r.style.display||(r.style.display=t?o[a]||"":"none"));return e}x.fn.extend({css:function(e,n){return x.access(this,function(e,n,r){var i,o,a={},s=0;if(x.isArray(n)){for(o=Rt(e),i=n.length;i>s;s++)a[n[s]]=x.css(e,n[s],!1,o);return a}return r!==t?x.style(e,n,r):x.css(e,n)},e,n,arguments.length>1)},show:function(){return rn(this,!0)},hide:function(){return rn(this)},toggle:function(e){return"boolean"==typeof e?e?this.show():this.hide():this.each(function(){nn(this)?x(this).show():x(this).hide()})}}),x.extend({cssHooks:{opacity:{get:function(e,t){if(t){var n=Wt(e,"opacity");return""===n?"1":n}}}},cssNumber:{columnCount:!0,fillOpacity:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":x.support.cssFloat?"cssFloat":"styleFloat"},style:function(e,n,r,i){if(e&&3!==e.nodeType&&8!==e.nodeType&&e.style){var o,a,s,l=x.camelCase(n),u=e.style;if(n=x.cssProps[l]||(x.cssProps[l]=tn(u,l)),s=x.cssHooks[n]||x.cssHooks[l],r===t)return s&&"get"in s&&(o=s.get(e,!1,i))!==t?o:u[n];if(a=typeof r,"string"===a&&(o=Jt.exec(r))&&(r=(o[1]+1)*o[2]+parseFloat(x.css(e,n)),a="number"),!(null==r||"number"===a&&isNaN(r)||("number"!==a||x.cssNumber[l]||(r+="px"),x.support.clearCloneStyle||""!==r||0!==n.indexOf("background")||(u[n]="inherit"),s&&"set"in s&&(r=s.set(e,r,i))===t)))try{u[n]=r}catch(c){}}},css:function(e,n,r,i){var o,a,s,l=x.camelCase(n);return n=x.cssProps[l]||(x.cssProps[l]=tn(e.style,l)),s=x.cssHooks[n]||x.cssHooks[l],s&&"get"in s&&(a=s.get(e,!0,r)),a===t&&(a=Wt(e,n,i)),"normal"===a&&n in Kt&&(a=Kt[n]),""===r||r?(o=parseFloat(a),r===!0||x.isNumeric(o)?o||0:a):a}}),e.getComputedStyle?(Rt=function(t){return e.getComputedStyle(t,null)},Wt=function(e,n,r){var i,o,a,s=r||Rt(e),l=s?s.getPropertyValue(n)||s[n]:t,u=e.style;return s&&(""!==l||x.contains(e.ownerDocument,e)||(l=x.style(e,n)),Yt.test(l)&&Ut.test(n)&&(i=u.width,o=u.minWidth,a=u.maxWidth,u.minWidth=u.maxWidth=u.width=l,l=s.width,u.width=i,u.minWidth=o,u.maxWidth=a)),l}):a.documentElement.currentStyle&&(Rt=function(e){return e.currentStyle},Wt=function(e,n,r){var i,o,a,s=r||Rt(e),l=s?s[n]:t,u=e.style;return null==l&&u&&u[n]&&(l=u[n]),Yt.test(l)&&!zt.test(n)&&(i=u.left,o=e.runtimeStyle,a=o&&o.left,a&&(o.left=e.currentStyle.left),u.left="fontSize"===n?"1em":l,l=u.pixelLeft+"px",u.left=i,a&&(o.left=a)),""===l?"auto":l});function on(e,t,n){var r=Vt.exec(t);return r?Math.max(0,r[1]-(n||0))+(r[2]||"px"):t}function an(e,t,n,r,i){var o=n===(r?"border":"content")?4:"width"===t?1:0,a=0;for(;4>o;o+=2)"margin"===n&&(a+=x.css(e,n+Zt[o],!0,i)),r?("content"===n&&(a-=x.css(e,"padding"+Zt[o],!0,i)),"margin"!==n&&(a-=x.css(e,"border"+Zt[o]+"Width",!0,i))):(a+=x.css(e,"padding"+Zt[o],!0,i),"padding"!==n&&(a+=x.css(e,"border"+Zt[o]+"Width",!0,i)));return a}function sn(e,t,n){var r=!0,i="width"===t?e.offsetWidth:e.offsetHeight,o=Rt(e),a=x.support.boxSizing&&"border-box"===x.css(e,"boxSizing",!1,o);if(0>=i||null==i){if(i=Wt(e,t,o),(0>i||null==i)&&(i=e.style[t]),Yt.test(i))return i;r=a&&(x.support.boxSizingReliable||i===e.style[t]),i=parseFloat(i)||0}return i+an(e,t,n||(a?"border":"content"),r,o)+"px"}function ln(e){var t=a,n=Gt[e];return n||(n=un(e,t),"none"!==n&&n||(Pt=(Pt||x(" diff --git a/pagure/templates/edit_file.html b/pagure/templates/edit_file.html index 6b46ac1..18e956e 100644 --- a/pagure/templates/edit_file.html +++ b/pagure/templates/edit_file.html @@ -1,7 +1,8 @@ {% extends "repo_master.html" %} {% from "_formhelper.html" import render_field %} -{% block title %}Edit - {{ repo.name }}{% endblock %} +{% block title %}Edit - {{ + repo.namespace + '/' if repo.namespace }}{{ repo.name }}{% endblock %} {% set tag = "home" %} {% block header %} @@ -14,7 +15,7 @@
    {{ form.csrf_token }} diff --git a/pagure/templates/edit_tag.html b/pagure/templates/edit_tag.html index f406558..9c000cb 100644 --- a/pagure/templates/edit_tag.html +++ b/pagure/templates/edit_tag.html @@ -1,7 +1,8 @@ {% extends "repo_master.html" %} {% from "_formhelper.html" import render_field_in_row %} -{% block title %}Edit tag: {{ tag }} - {{ repo.name }}{% endblock %} +{% block title %}Edit tag: {{ tag }} - {{ + repo.namespace + '/' if repo.namespace }}{{ repo.name }{{ repo.name }}{% endblock %} {% set tag = "home" %} @@ -10,7 +11,7 @@

    Edit tag: {{ edit_tag }}

    -

    Enter in the field below the new name for the tag: "{{ edit_tag }}"

    diff --git a/pagure/templates/file.html b/pagure/templates/file.html index 6e25e51..7892d53 100644 --- a/pagure/templates/file.html +++ b/pagure/templates/file.html @@ -1,6 +1,7 @@ {% extends "repo_master.html" %} -{% block title %}Tree - {{ repo.name }}{% endblock %} +{% block title %}Tree - {{ + repo.namespace + '/' if repo.namespace }}{{ repo.name }}{% endblock %} {% set tag = "home" %} @@ -33,13 +34,13 @@ {% for branch in g.branches %} {% if origin == 'view_tree' %} {{ branch }} {% elif origin == 'view_file' %} {{ branch }} @@ -56,8 +57,8 @@
    {% elif output_type == 'image' %} {% elif output_type == 'binary' %} @@ -147,6 +150,7 @@ Binary files cannot be rendered.
    Please view the raw version @@ -173,8 +177,8 @@
    {% if entry.filemode == 16384 %}{% endif -%} {{ entry.name | unicode }} diff --git a/pagure/templates/forks.html b/pagure/templates/forks.html index 5b5a8f4..964c0a5 100644 --- a/pagure/templates/forks.html +++ b/pagure/templates/forks.html @@ -1,6 +1,7 @@ {% extends "repo_master.html" %} -{% block title %}Forks - {{ repo.name }}{% endblock %} +{% block title %}Forks - {{ + repo.namespace + '/' if repo.namespace }}{{ repo.name }}{% endblock %} {% set tag = "home" %} @@ -14,14 +15,17 @@ {% for fork in repo.forks %}
    - + {{ fork.user.user }}/{{ fork.name }}
    - {{ fork.user.default_email | avatar(20) | safe }} - {{ fork.user.fullname }} forked this project + + {{ fork.user.default_email | avatar(20) | safe }} + {{ fork.user.fullname }} + forked this project diff --git a/pagure/templates/index_auth.html b/pagure/templates/index_auth.html index f8d082f..7d04f17 100644 --- a/pagure/templates/index_auth.html +++ b/pagure/templates/index_auth.html @@ -35,18 +35,17 @@
    {% for repo in repos %}
    - {% if repo.is_fork %} - {% set url = url_for('view_repo', username=repo.user.username, repo=repo.name) %} - {% else %} - {% set url = url_for('view_repo', repo=repo.name) %} - {% endif %} + {% set url = url_for( + 'view_repo', namespace=repo.namespace, + username=repo.user.username if repo.is_fork else None, + repo=repo.name) %} {% if repo.avatar_email %}   {% else %} {% endif %} - {{ repo.name }} + {{ repo.namespace + '/' if repo.namespace }}{{ repo.name }}
    {% if config.get('ENABLE_TICKETS', True) and repo.settings.get('issue_tracker', True) %} @@ -74,7 +73,9 @@ {% else %} {{- repo.open_requests}} @@ -109,19 +110,22 @@
    {% for fork in forks %}
    - {% if fork.is_fork %} - {% set url = url_for('view_repo', username=fork.user.username, repo=fork.name) %} - {% else %} - {% set url = url_for('view_repo', repo=fork.name) %} - {% endif %} + {% set url = url_for( + 'view_repo', + username=fork.user.username if fork.is_fork else None, + repo=fork.name, namespace=fork.namespace) %} + - {{username}}/{{ fork.name }} + {{username}}/{{ fork.namespace + '/' if fork.namespace }}{{ fork.name }}
    forked from - - {{fork.parent.name}} + + {{ fork.parent.namespace + '/' if fork.parent.namespace + }}{{fork.parent.name}} {% if fork.settings.get('issue_tracker', True) %} {% if fork.open_tickets_public == 0 %} @@ -132,7 +136,9 @@ {% else %} {{- fork.open_tickets_public }} @@ -148,7 +154,9 @@ {% else %} {{- fork.open_requests}} @@ -197,16 +205,26 @@
    {% if repo.is_fork %} -
    -  {{ repo.user.username }}/{{ repo.name }} + 'view_repo', username=repo.user.username, repo=repo.name, + namespace=repo.namespace) }}"> +
    + + +  {{ repo.user.username }}/{{ + repo.namespace + '/' if repo.namespace + }}{{ repo.name }} +
    {% else %} -
    -  {{ repo.name }} + 'view_repo', repo=repo.name, namespace=repo.namespace) }}"> +
    + + +  {{ repo.namespace + '/' if repo.namespace + }}{{ repo.name }} +
    {% endif %} diff --git a/pagure/templates/issue.html b/pagure/templates/issue.html index 6cb8500..3f13553 100644 --- a/pagure/templates/issue.html +++ b/pagure/templates/issue.html @@ -18,8 +18,8 @@ {% block repo %}
    + namespace=repo.namespace, repo=repo.name, issueid=issueid) + }}" method="post" onsubmit="return try_async_comment(this)"> {{ form.csrf_token }}
    @@ -136,7 +136,8 @@ {% for tag in issue.tags %} + namespace=repo.namespace, + repo=repo.name, tags=tag.tag) }}"> {{ tag.tag }} {% endfor %} @@ -158,6 +159,7 @@ {% if issue.assignee %} {{ issue.assignee.username }} @@ -189,7 +191,8 @@ {% for ticket in issue.parents %} #{{ ticket.id }} {% endfor %}
    @@ -211,7 +214,8 @@ {% for ticket in issue.children %} #{{ ticket.id }} {% endfor %}
    @@ -263,7 +267,7 @@ {% block jscripts %} {{ super() }} + diff --git a/pagure/templates/new_issue.html b/pagure/templates/new_issue.html index 740a78e..6097b79 100644 --- a/pagure/templates/new_issue.html +++ b/pagure/templates/new_issue.html @@ -133,6 +133,9 @@ + + From 78f167f5189f8cdfee93b9e2e52f54f2f7d67b9d Mon Sep 17 00:00:00 2001 From: Ryan Lerch Date: Sep 13 2016 08:50:05 +0000 Subject: [PATCH 213/635] added missing emojicomplete file --- diff --git a/pagure/static/emoji/emojicomplete.js b/pagure/static/emoji/emojicomplete.js new file mode 100644 index 0000000..3af10fe --- /dev/null +++ b/pagure/static/emoji/emojicomplete.js @@ -0,0 +1,57 @@ +emoji_complete = function(json_url, folder) { + + emojione.imagePathPNG = folder; + emojione.imageType = 'png'; + emojione.sprites = true; + + var emojiStrategy; + if (!emojiStrategy) { + $.getJSON( + json_url, + function( data ) { + emojiStrategy = data; + } + ); + } + + $("textarea").textcomplete([ { + match: /\B:([\-+\w]*)$/, + search: function (term, callback) { + var results = []; + var results2 = []; + var results3 = []; + console.log(emojiStrategy); + $.each(emojiStrategy,function(shortname,data) { + if(shortname.indexOf(term) > -1) { results.push(shortname); } + else { + if((data.aliases !== null) && (data.aliases.indexOf(term) > -1)) { + results2.push(shortname); + } + else if((data.keywords !== null) && (data.keywords.indexOf(term) > -1)) { + results3.push(shortname); + } + } + }); + + if(term.length >= 3) { + results.sort(function(a,b) { return (a.length > b.length); }); + results2.sort(function(a,b) { return (a.length > b.length); }); + results3.sort(); + } + var newResults = results.concat(results2).concat(results3); + + callback(newResults); + }, + template: function (shortname) { + return ':'+shortname+':'; + }, + replace: function (shortname) { + return ':'+shortname+': '; + }, + index: 1, + maxCount: 10 + } + ],{ + footer: 'Browse All»' + }); +}; From 0474d342ed83b59c4f20bc5b8350a49e0a04a592 Mon Sep 17 00:00:00 2001 From: Ryan Lerch Date: Sep 13 2016 09:09:16 +0000 Subject: [PATCH 214/635] fix openiconic not loading --- diff --git a/pagure/templates/master.html b/pagure/templates/master.html index 6a758ca..721514f 100644 --- a/pagure/templates/master.html +++ b/pagure/templates/master.html @@ -9,7 +9,7 @@ type="text/css" rel="stylesheet" /> - From e7a276d00c874f3daf0c49551ee0bb274025dc1e Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Sep 13 2016 09:29:44 +0000 Subject: [PATCH 215/635] Add support to our local markdown processor for ~~striked~~ Fixes https://pagure.io/pagure/issue/1261 --- diff --git a/pagure/pfmarkdown.py b/pagure/pfmarkdown.py index f4762b8..4fa826a 100644 --- a/pagure/pfmarkdown.py +++ b/pagure/pfmarkdown.py @@ -35,6 +35,7 @@ EXPLICIT_FORK_ISSUE_RE = r'(\w+)/(\w+)#([0-9]+)' EXPLICIT_MAIN_ISSUE_RE = r'[^|\w](? Date: Sep 13 2016 11:07:11 +0000 Subject: [PATCH 216/635] Fix atwho This fixes @ and # by bringing back the right version of jquery.carret as well as updating jquery.atwho to 1.5.1 --- diff --git a/pagure/static/atwho/jquery.atwho-1.4.1.css b/pagure/static/atwho/jquery.atwho-1.4.1.css deleted file mode 100644 index a073908..0000000 --- a/pagure/static/atwho/jquery.atwho-1.4.1.css +++ /dev/null @@ -1,49 +0,0 @@ -.atwho-view { - position:absolute; - top: 0; - left: 0; - display: none; - margin-top: 18px; - background: white; - color: black; - border: 1px solid #DDD; - border-radius: 3px; - box-shadow: 0 0 5px rgba(0,0,0,0.1); - min-width: 120px; - max-height: 200px; - overflow: auto; - z-index: 11110 !important; -} - -.atwho-view .cur { - background: #3366FF; - color: white; -} -.atwho-view .cur small { - color: white; -} -.atwho-view strong { - color: #3366FF; -} -.atwho-view .cur strong { - color: white; - font:bold; -} -.atwho-view ul { - /* width: 100px; */ - list-style:none; - padding:0; - margin:auto; -} -.atwho-view ul li { - display: block; - padding: 5px 10px; - border-bottom: 1px solid #DDD; - cursor: pointer; - /* border-top: 1px solid #C8C8C8; */ -} -.atwho-view small { - font-size: smaller; - color: #777; - font-weight: normal; -} diff --git a/pagure/static/atwho/jquery.atwho-1.4.1.js b/pagure/static/atwho/jquery.atwho-1.4.1.js deleted file mode 100644 index 7f53f12..0000000 --- a/pagure/static/atwho/jquery.atwho-1.4.1.js +++ /dev/null @@ -1,1158 +0,0 @@ -/*! jquery.atwho - v1.4.0 %> -* Copyright (c) 2015 chord.luo ; -* homepage: http://ichord.github.com/At.js -* Licensed MIT -*/ -(function (root, factory) { - if (typeof define === 'function' && define.amd) { - // AMD. Register as an anonymous module unless amdModuleId is set - define(["jquery"], function (a0) { - return (factory(a0)); - }); - } else if (typeof exports === 'object') { - // Node. Does not work with strict CommonJS, but - // only CommonJS-like environments that support module.exports, - // like Node. - module.exports = factory(require("jquery")); - } else { - factory(jQuery); - } -}(this, function (jquery) { - -var $, Api, App, Controller, DEFAULT_CALLBACKS, EditableController, KEY_CODE, Model, TextareaController, View, - slice = [].slice, - extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, - hasProp = {}.hasOwnProperty; - -$ = jquery; - -App = (function() { - function App(inputor) { - this.currentFlag = null; - this.controllers = {}; - this.aliasMaps = {}; - this.$inputor = $(inputor); - this.setupRootElement(); - this.listen(); - } - - App.prototype.createContainer = function(doc) { - var ref; - if ((ref = this.$el) != null) { - ref.remove(); - } - return $(doc.body).append(this.$el = $("
    ")); - }; - - App.prototype.setupRootElement = function(iframe, asRoot) { - var error; - if (asRoot == null) { - asRoot = false; - } - if (iframe) { - this.window = iframe.contentWindow; - this.document = iframe.contentDocument || this.window.document; - this.iframe = iframe; - } else { - this.document = this.$inputor[0].ownerDocument; - this.window = this.document.defaultView || this.document.parentWindow; - try { - this.iframe = this.window.frameElement; - } catch (_error) { - error = _error; - this.iframe = null; - if ($.fn.atwho.debug) { - throw new Error("iframe auto-discovery is failed.\nPlease use `setIframe` to set the target iframe manually.\n" + error); - } - } - } - return this.createContainer((this.iframeAsRoot = asRoot) ? this.document : document); - }; - - App.prototype.controller = function(at) { - var c, current, currentFlag, ref; - if (this.aliasMaps[at]) { - current = this.controllers[this.aliasMaps[at]]; - } else { - ref = this.controllers; - for (currentFlag in ref) { - c = ref[currentFlag]; - if (currentFlag === at) { - current = c; - break; - } - } - } - if (current) { - return current; - } else { - return this.controllers[this.currentFlag]; - } - }; - - App.prototype.setContextFor = function(at) { - this.currentFlag = at; - return this; - }; - - App.prototype.reg = function(flag, setting) { - var base, controller; - controller = (base = this.controllers)[flag] || (base[flag] = this.$inputor.is('[contentEditable]') ? new EditableController(this, flag) : new TextareaController(this, flag)); - if (setting.alias) { - this.aliasMaps[setting.alias] = flag; - } - controller.init(setting); - return this; - }; - - App.prototype.listen = function() { - return this.$inputor.on('compositionstart', (function(_this) { - return function(e) { - var ref; - if ((ref = _this.controller()) != null) { - ref.view.hide(); - } - _this.isComposing = true; - return null; - }; - })(this)).on('compositionend', (function(_this) { - return function(e) { - _this.isComposing = false; - return null; - }; - })(this)).on('keyup.atwhoInner', (function(_this) { - return function(e) { - return _this.onKeyup(e); - }; - })(this)).on('keydown.atwhoInner', (function(_this) { - return function(e) { - return _this.onKeydown(e); - }; - })(this)).on('blur.atwhoInner', (function(_this) { - return function(e) { - var c; - if (c = _this.controller()) { - c.expectedQueryCBId = null; - return c.view.hide(e, c.getOpt("displayTimeout")); - } - }; - })(this)).on('click.atwhoInner', (function(_this) { - return function(e) { - return _this.dispatch(e); - }; - })(this)).on('scroll.atwhoInner', (function(_this) { - return function() { - var lastScrollTop; - lastScrollTop = _this.$inputor.scrollTop(); - return function(e) { - var currentScrollTop, ref; - currentScrollTop = e.target.scrollTop; - if (lastScrollTop !== currentScrollTop) { - if ((ref = _this.controller()) != null) { - ref.view.hide(e); - } - } - lastScrollTop = currentScrollTop; - return true; - }; - }; - })(this)()); - }; - - App.prototype.shutdown = function() { - var _, c, ref; - ref = this.controllers; - for (_ in ref) { - c = ref[_]; - c.destroy(); - delete this.controllers[_]; - } - this.$inputor.off('.atwhoInner'); - return this.$el.remove(); - }; - - App.prototype.dispatch = function(e) { - var _, c, ref, results; - ref = this.controllers; - results = []; - for (_ in ref) { - c = ref[_]; - results.push(c.lookUp(e)); - } - return results; - }; - - App.prototype.onKeyup = function(e) { - var ref; - switch (e.keyCode) { - case KEY_CODE.ESC: - e.preventDefault(); - if ((ref = this.controller()) != null) { - ref.view.hide(); - } - break; - case KEY_CODE.DOWN: - case KEY_CODE.UP: - case KEY_CODE.CTRL: - case KEY_CODE.ENTER: - $.noop(); - break; - case KEY_CODE.P: - case KEY_CODE.N: - if (!e.ctrlKey) { - this.dispatch(e); - } - break; - default: - this.dispatch(e); - } - }; - - App.prototype.onKeydown = function(e) { - var ref, view; - view = (ref = this.controller()) != null ? ref.view : void 0; - if (!(view && view.visible())) { - return; - } - switch (e.keyCode) { - case KEY_CODE.ESC: - e.preventDefault(); - view.hide(e); - break; - case KEY_CODE.UP: - e.preventDefault(); - view.prev(); - break; - case KEY_CODE.DOWN: - e.preventDefault(); - view.next(); - break; - case KEY_CODE.P: - if (!e.ctrlKey) { - return; - } - e.preventDefault(); - view.prev(); - break; - case KEY_CODE.N: - if (!e.ctrlKey) { - return; - } - e.preventDefault(); - view.next(); - break; - case KEY_CODE.TAB: - case KEY_CODE.ENTER: - case KEY_CODE.SPACE: - if (!view.visible()) { - return; - } - if (!this.controller().getOpt('spaceSelectsMatch') && e.keyCode === KEY_CODE.SPACE) { - return; - } - if (!this.controller().getOpt('tabSelectsMatch') && e.keyCode === KEY_CODE.TAB) { - return; - } - if (view.highlighted()) { - e.preventDefault(); - view.choose(e); - } else { - view.hide(e); - } - break; - default: - $.noop(); - } - }; - - return App; - -})(); - -Controller = (function() { - Controller.prototype.uid = function() { - return (Math.random().toString(16) + "000000000").substr(2, 8) + (new Date().getTime()); - }; - - function Controller(app1, at1) { - this.app = app1; - this.at = at1; - this.$inputor = this.app.$inputor; - this.id = this.$inputor[0].id || this.uid(); - this.expectedQueryCBId = null; - this.setting = null; - this.query = null; - this.pos = 0; - this.range = null; - if ((this.$el = $("#atwho-ground-" + this.id, this.app.$el)).length === 0) { - this.app.$el.append(this.$el = $("
    ")); - } - this.model = new Model(this); - this.view = new View(this); - } - - Controller.prototype.init = function(setting) { - this.setting = $.extend({}, this.setting || $.fn.atwho["default"], setting); - this.view.init(); - return this.model.reload(this.setting.data); - }; - - Controller.prototype.destroy = function() { - this.trigger('beforeDestroy'); - this.model.destroy(); - this.view.destroy(); - return this.$el.remove(); - }; - - Controller.prototype.callDefault = function() { - var args, error, funcName; - funcName = arguments[0], args = 2 <= arguments.length ? slice.call(arguments, 1) : []; - try { - return DEFAULT_CALLBACKS[funcName].apply(this, args); - } catch (_error) { - error = _error; - return $.error(error + " Or maybe At.js doesn't have function " + funcName); - } - }; - - Controller.prototype.trigger = function(name, data) { - var alias, eventName; - if (data == null) { - data = []; - } - data.push(this); - alias = this.getOpt('alias'); - eventName = alias ? name + "-" + alias + ".atwho" : name + ".atwho"; - return this.$inputor.trigger(eventName, data); - }; - - Controller.prototype.callbacks = function(funcName) { - return this.getOpt("callbacks")[funcName] || DEFAULT_CALLBACKS[funcName]; - }; - - Controller.prototype.getOpt = function(at, default_value) { - var e; - try { - return this.setting[at]; - } catch (_error) { - e = _error; - return null; - } - }; - - Controller.prototype.insertContentFor = function($li) { - var data, tpl; - tpl = this.getOpt('insertTpl'); - data = $.extend({}, $li.data('item-data'), { - 'atwho-at': this.at - }); - return this.callbacks("tplEval").call(this, tpl, data, "onInsert"); - }; - - Controller.prototype.renderView = function(data) { - var searchKey; - searchKey = this.getOpt("searchKey"); - data = this.callbacks("sorter").call(this, this.query.text, data.slice(0, 1001), searchKey); - return this.view.render(data.slice(0, this.getOpt('limit'))); - }; - - Controller.arrayToDefaultHash = function(data) { - var i, item, len, results; - if (!$.isArray(data)) { - return data; - } - results = []; - for (i = 0, len = data.length; i < len; i++) { - item = data[i]; - if ($.isPlainObject(item)) { - results.push(item); - } else { - results.push({ - name: item - }); - } - } - return results; - }; - - Controller.prototype.lookUp = function(e) { - var query, wait; - if (e && e.type === 'click' && !this.getOpt('lookUpOnClick')) { - return; - } - if (this.getOpt('suspendOnComposing') && this.app.isComposing) { - return; - } - query = this.catchQuery(e); - if (!query) { - this.expectedQueryCBId = null; - return query; - } - this.app.setContextFor(this.at); - if (wait = this.getOpt('delay')) { - this._delayLookUp(query, wait); - } else { - this._lookUp(query); - } - return query; - }; - - Controller.prototype._delayLookUp = function(query, wait) { - var now, remaining; - now = Date.now ? Date.now() : new Date().getTime(); - this.previousCallTime || (this.previousCallTime = now); - remaining = wait - (now - this.previousCallTime); - if ((0 < remaining && remaining < wait)) { - this.previousCallTime = now; - this._stopDelayedCall(); - return this.delayedCallTimeout = setTimeout((function(_this) { - return function() { - _this.previousCallTime = 0; - _this.delayedCallTimeout = null; - return _this._lookUp(query); - }; - })(this), wait); - } else { - this._stopDelayedCall(); - if (this.previousCallTime !== now) { - this.previousCallTime = 0; - } - return this._lookUp(query); - } - }; - - Controller.prototype._stopDelayedCall = function() { - if (this.delayedCallTimeout) { - clearTimeout(this.delayedCallTimeout); - return this.delayedCallTimeout = null; - } - }; - - Controller.prototype._generateQueryCBId = function() { - return {}; - }; - - Controller.prototype._lookUp = function(query) { - var _callback; - _callback = function(queryCBId, data) { - if (queryCBId !== this.expectedQueryCBId) { - return; - } - if (data && data.length > 0) { - return this.renderView(this.constructor.arrayToDefaultHash(data)); - } else { - return this.view.hide(); - } - }; - this.expectedQueryCBId = this._generateQueryCBId(); - return this.model.query(query.text, $.proxy(_callback, this, this.expectedQueryCBId)); - }; - - return Controller; - -})(); - -TextareaController = (function(superClass) { - extend(TextareaController, superClass); - - function TextareaController() { - return TextareaController.__super__.constructor.apply(this, arguments); - } - - TextareaController.prototype.catchQuery = function() { - var caretPos, content, end, isString, query, start, subtext; - content = this.$inputor.val(); - caretPos = this.$inputor.caret('pos', { - iframe: this.app.iframe - }); - subtext = content.slice(0, caretPos); - query = this.callbacks("matcher").call(this, this.at, subtext, this.getOpt('startWithSpace')); - isString = typeof query === 'string'; - if (isString && query.length < this.getOpt('minLen', 0)) { - return; - } - if (isString && query.length <= this.getOpt('maxLen', 20)) { - start = caretPos - query.length; - end = start + query.length; - this.pos = start; - query = { - 'text': query, - 'headPos': start, - 'endPos': end - }; - this.trigger("matched", [this.at, query.text]); - } else { - query = null; - this.view.hide(); - } - return this.query = query; - }; - - TextareaController.prototype.rect = function() { - var c, iframeOffset, scaleBottom; - if (!(c = this.$inputor.caret('offset', this.pos - 1, { - iframe: this.app.iframe - }))) { - return; - } - if (this.app.iframe && !this.app.iframeAsRoot) { - iframeOffset = $(this.app.iframe).offset(); - c.left += iframeOffset.left; - c.top += iframeOffset.top; - } - scaleBottom = this.app.document.selection ? 0 : 2; - return { - left: c.left, - top: c.top, - bottom: c.top + c.height + scaleBottom - }; - }; - - TextareaController.prototype.insert = function(content, $li) { - var $inputor, source, startStr, suffix, text; - $inputor = this.$inputor; - source = $inputor.val(); - startStr = source.slice(0, Math.max(this.query.headPos - this.at.length, 0)); - suffix = (suffix = this.getOpt('suffix')) === "" ? suffix : suffix || " "; - content += suffix; - text = "" + startStr + content + (source.slice(this.query['endPos'] || 0)); - $inputor.val(text); - $inputor.caret('pos', startStr.length + content.length, { - iframe: this.app.iframe - }); - if (!$inputor.is(':focus')) { - $inputor.focus(); - } - return $inputor.change(); - }; - - return TextareaController; - -})(Controller); - -EditableController = (function(superClass) { - extend(EditableController, superClass); - - function EditableController() { - return EditableController.__super__.constructor.apply(this, arguments); - } - - EditableController.prototype._getRange = function() { - var sel; - sel = this.app.window.getSelection(); - if (sel.rangeCount > 0) { - return sel.getRangeAt(0); - } - }; - - EditableController.prototype._setRange = function(position, node, range) { - if (range == null) { - range = this._getRange(); - } - if (!range) { - return; - } - node = $(node)[0]; - if (position === 'after') { - range.setEndAfter(node); - range.setStartAfter(node); - } else { - range.setEndBefore(node); - range.setStartBefore(node); - } - range.collapse(false); - return this._clearRange(range); - }; - - EditableController.prototype._clearRange = function(range) { - var sel; - if (range == null) { - range = this._getRange(); - } - sel = this.app.window.getSelection(); - if (this.ctrl_a_pressed == null) { - sel.removeAllRanges(); - return sel.addRange(range); - } - }; - - EditableController.prototype._movingEvent = function(e) { - var ref; - return e.type === 'click' || ((ref = e.which) === KEY_CODE.RIGHT || ref === KEY_CODE.LEFT || ref === KEY_CODE.UP || ref === KEY_CODE.DOWN); - }; - - EditableController.prototype._unwrap = function(node) { - var next; - node = $(node).unwrap().get(0); - if ((next = node.nextSibling) && next.nodeValue) { - node.nodeValue += next.nodeValue; - $(next).remove(); - } - return node; - }; - - EditableController.prototype.catchQuery = function(e) { - var $inserted, $query, _range, index, inserted, isString, lastNode, matched, offset, query, query_content, range; - if (!(range = this._getRange())) { - return; - } - if (!range.collapsed) { - return; - } - if (e.which === KEY_CODE.ENTER) { - ($query = $(range.startContainer).closest('.atwho-query')).contents().unwrap(); - if ($query.is(':empty')) { - $query.remove(); - } - ($query = $(".atwho-query", this.app.document)).text($query.text()).contents().last().unwrap(); - this._clearRange(); - return; - } - if (/firefox/i.test(navigator.userAgent)) { - if ($(range.startContainer).is(this.$inputor)) { - this._clearRange(); - return; - } - if (e.which === KEY_CODE.BACKSPACE && range.startContainer.nodeType === document.ELEMENT_NODE && (offset = range.startOffset - 1) >= 0) { - _range = range.cloneRange(); - _range.setStart(range.startContainer, offset); - if ($(_range.cloneContents()).contents().last().is('.atwho-inserted')) { - inserted = $(range.startContainer).contents().get(offset); - this._setRange('after', $(inserted).contents().last()); - } - } else if (e.which === KEY_CODE.LEFT && range.startContainer.nodeType === document.TEXT_NODE) { - $inserted = $(range.startContainer.previousSibling); - if ($inserted.is('.atwho-inserted') && range.startOffset === 0) { - this._setRange('after', $inserted.contents().last()); - } - } - } - $(range.startContainer).closest('.atwho-inserted').addClass('atwho-query').siblings().removeClass('atwho-query'); - if (($query = $(".atwho-query", this.app.document)).length > 0 && $query.is(':empty') && $query.text().length === 0) { - $query.remove(); - } - if (!this._movingEvent(e)) { - $query.removeClass('atwho-inserted'); - } - if ($query.length > 0) { - switch (e.which) { - case KEY_CODE.LEFT: - this._setRange('before', $query.get(0), range); - $query.removeClass('atwho-query'); - return; - case KEY_CODE.RIGHT: - this._setRange('after', $query.get(0).nextSibling, range); - $query.removeClass('atwho-query'); - return; - } - } - if ($query.length > 0 && (query_content = $query.attr('data-atwho-at-query'))) { - $query.empty().html(query_content).attr('data-atwho-at-query', null); - this._setRange('after', $query.get(0), range); - } - _range = range.cloneRange(); - _range.setStart(range.startContainer, 0); - matched = this.callbacks("matcher").call(this, this.at, _range.toString(), this.getOpt('startWithSpace')); - isString = typeof matched === 'string'; - if ($query.length === 0 && isString && (index = range.startOffset - this.at.length - matched.length) >= 0) { - range.setStart(range.startContainer, index); - $query = $('', this.app.document).attr(this.getOpt("editableAtwhoQueryAttrs")).addClass('atwho-query'); - range.surroundContents($query.get(0)); - lastNode = $query.contents().last().get(0); - if (/firefox/i.test(navigator.userAgent)) { - range.setStart(lastNode, lastNode.length); - range.setEnd(lastNode, lastNode.length); - this._clearRange(range); - } else { - this._setRange('after', lastNode, range); - } - } - if (isString && matched.length < this.getOpt('minLen', 0)) { - return; - } - if (isString && matched.length <= this.getOpt('maxLen', 20)) { - query = { - text: matched, - el: $query - }; - this.trigger("matched", [this.at, query.text]); - return this.query = query; - } else { - this.view.hide(); - this.query = { - el: $query - }; - if ($query.text().indexOf(this.at) >= 0) { - if (this._movingEvent(e) && $query.hasClass('atwho-inserted')) { - $query.removeClass('atwho-query'); - } else if (false !== this.callbacks('afterMatchFailed').call(this, this.at, $query)) { - this._setRange("after", this._unwrap($query.text($query.text()).contents().first())); - } - } - return null; - } - }; - - EditableController.prototype.rect = function() { - var $iframe, iframeOffset, rect; - rect = this.query.el.offset(); - if (this.app.iframe && !this.app.iframeAsRoot) { - iframeOffset = ($iframe = $(this.app.iframe)).offset(); - rect.left += iframeOffset.left - this.$inputor.scrollLeft(); - rect.top += iframeOffset.top - this.$inputor.scrollTop(); - } - rect.bottom = rect.top + this.query.el.height(); - return rect; - }; - - EditableController.prototype.insert = function(content, $li) { - var data, range, suffix, suffixNode; - suffix = (suffix = this.getOpt('suffix')) === "" ? suffix : suffix || "\u00A0"; - data = $li.data('item-data'); - this.query.el.removeClass('atwho-query').addClass('atwho-inserted').html(content).attr('data-atwho-at-query', "" + data['atwho-at'] + this.query.text); - if (range = this._getRange()) { - range.setEndAfter(this.query.el[0]); - range.collapse(false); - range.insertNode(suffixNode = this.app.document.createTextNode("\u200D" + suffix)); - this._setRange('after', suffixNode, range); - } - if (!this.$inputor.is(':focus')) { - this.$inputor.focus(); - } - return this.$inputor.change(); - }; - - return EditableController; - -})(Controller); - -Model = (function() { - function Model(context) { - this.context = context; - this.at = this.context.at; - this.storage = this.context.$inputor; - } - - Model.prototype.destroy = function() { - return this.storage.data(this.at, null); - }; - - Model.prototype.saved = function() { - return this.fetch() > 0; - }; - - Model.prototype.query = function(query, callback) { - var _remoteFilter, data, searchKey; - data = this.fetch(); - searchKey = this.context.getOpt("searchKey"); - data = this.context.callbacks('filter').call(this.context, query, data, searchKey) || []; - _remoteFilter = this.context.callbacks('remoteFilter'); - if (data.length > 0 || (!_remoteFilter && data.length === 0)) { - return callback(data); - } else { - return _remoteFilter.call(this.context, query, callback); - } - }; - - Model.prototype.fetch = function() { - return this.storage.data(this.at) || []; - }; - - Model.prototype.save = function(data) { - return this.storage.data(this.at, this.context.callbacks("beforeSave").call(this.context, data || [])); - }; - - Model.prototype.load = function(data) { - if (!(this.saved() || !data)) { - return this._load(data); - } - }; - - Model.prototype.reload = function(data) { - return this._load(data); - }; - - Model.prototype._load = function(data) { - if (typeof data === "string") { - return $.ajax(data, { - dataType: "json" - }).done((function(_this) { - return function(data) { - return _this.save(data); - }; - })(this)); - } else { - return this.save(data); - } - }; - - return Model; - -})(); - -View = (function() { - function View(context) { - this.context = context; - this.$el = $("
      "); - this.timeoutID = null; - this.context.$el.append(this.$el); - this.bindEvent(); - } - - View.prototype.init = function() { - var id; - id = this.context.getOpt("alias") || this.context.at.charCodeAt(0); - return this.$el.attr({ - 'id': "at-view-" + id - }); - }; - - View.prototype.destroy = function() { - return this.$el.remove(); - }; - - View.prototype.bindEvent = function() { - var $menu; - $menu = this.$el.find('ul'); - return $menu.on('mouseenter.atwho-view', 'li', function(e) { - $menu.find('.cur').removeClass('cur'); - return $(e.currentTarget).addClass('cur'); - }).on('click.atwho-view', 'li', (function(_this) { - return function(e) { - $menu.find('.cur').removeClass('cur'); - $(e.currentTarget).addClass('cur'); - _this.choose(e); - return e.preventDefault(); - }; - })(this)); - }; - - View.prototype.visible = function() { - return this.$el.is(":visible"); - }; - - View.prototype.highlighted = function() { - return this.$el.find(".cur").length > 0; - }; - - View.prototype.choose = function(e) { - var $li, content; - if (($li = this.$el.find(".cur")).length) { - content = this.context.insertContentFor($li); - this.context._stopDelayedCall(); - this.context.insert(this.context.callbacks("beforeInsert").call(this.context, content, $li), $li); - this.context.trigger("inserted", [$li, e]); - this.hide(e); - } - if (this.context.getOpt("hideWithoutSuffix")) { - return this.stopShowing = true; - } - }; - - View.prototype.reposition = function(rect) { - var _window, offset, overflowOffset, ref; - _window = this.context.app.iframeAsRoot ? this.context.app.window : window; - if (rect.bottom + this.$el.height() - $(_window).scrollTop() > $(_window).height()) { - rect.bottom = rect.top - this.$el.height(); - } - if (rect.left > (overflowOffset = $(_window).width() - this.$el.width() - 5)) { - rect.left = overflowOffset; - } - offset = { - left: rect.left, - top: rect.bottom - }; - if ((ref = this.context.callbacks("beforeReposition")) != null) { - ref.call(this.context, offset); - } - this.$el.offset(offset); - return this.context.trigger("reposition", [offset]); - }; - - View.prototype.next = function() { - var cur, next; - cur = this.$el.find('.cur').removeClass('cur'); - next = cur.next(); - if (!next.length) { - next = this.$el.find('li:first'); - } - next.addClass('cur'); - return this.scrollTop(Math.max(0, cur.innerHeight() * (next.index() + 2) - this.$el.height())); - }; - - View.prototype.prev = function() { - var cur, prev; - cur = this.$el.find('.cur').removeClass('cur'); - prev = cur.prev(); - if (!prev.length) { - prev = this.$el.find('li:last'); - } - prev.addClass('cur'); - return this.scrollTop(Math.max(0, cur.innerHeight() * (prev.index() + 2) - this.$el.height())); - }; - - View.prototype.scrollTop = function(scrollTop) { - var scrollDuration; - scrollDuration = this.context.getOpt('scrollDuration'); - if (scrollDuration) { - return this.$el.animate({ - scrollTop: scrollTop - }, scrollDuration); - } else { - return this.$el.scrollTop(scrollTop); - } - }; - - View.prototype.show = function() { - var rect; - if (this.stopShowing) { - this.stopShowing = false; - return; - } - if (!this.visible()) { - this.$el.show(); - this.$el.scrollTop(0); - this.context.trigger('shown'); - } - if (rect = this.context.rect()) { - return this.reposition(rect); - } - }; - - View.prototype.hide = function(e, time) { - var callback; - if (!this.visible()) { - return; - } - if (isNaN(time)) { - this.$el.hide(); - return this.context.trigger('hidden', [e]); - } else { - callback = (function(_this) { - return function() { - return _this.hide(); - }; - })(this); - clearTimeout(this.timeoutID); - return this.timeoutID = setTimeout(callback, time); - } - }; - - View.prototype.render = function(list) { - var $li, $ul, i, item, len, li, tpl; - if (!($.isArray(list) && list.length > 0)) { - this.hide(); - return; - } - this.$el.find('ul').empty(); - $ul = this.$el.find('ul'); - tpl = this.context.getOpt('displayTpl'); - for (i = 0, len = list.length; i < len; i++) { - item = list[i]; - item = $.extend({}, item, { - 'atwho-at': this.context.at - }); - li = this.context.callbacks("tplEval").call(this.context, tpl, item, "onDisplay"); - $li = $(this.context.callbacks("highlighter").call(this.context, li, this.context.query.text)); - $li.data("item-data", item); - $ul.append($li); - } - this.show(); - if (this.context.getOpt('highlightFirst')) { - return $ul.find("li:first").addClass("cur"); - } - }; - - return View; - -})(); - -KEY_CODE = { - DOWN: 40, - UP: 38, - ESC: 27, - TAB: 9, - ENTER: 13, - CTRL: 17, - A: 65, - P: 80, - N: 78, - LEFT: 37, - UP: 38, - RIGHT: 39, - DOWN: 40, - BACKSPACE: 8, - SPACE: 32 -}; - -DEFAULT_CALLBACKS = { - beforeSave: function(data) { - return Controller.arrayToDefaultHash(data); - }, - matcher: function(flag, subtext, should_startWithSpace, acceptSpaceBar) { - var _a, _y, match, regexp, space; - flag = flag.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); - if (should_startWithSpace) { - flag = '(?:^|\\s)' + flag; - } - _a = decodeURI("%C3%80"); - _y = decodeURI("%C3%BF"); - space = acceptSpaceBar ? "\ " : ""; - regexp = new RegExp(flag + "([A-Za-z" + _a + "-" + _y + "0-9_" + space + "\'\.\+\-]*)$|" + flag + "([^\\x00-\\xff]*)$", 'gi'); - match = regexp.exec(subtext); - if (match) { - return match[2] || match[1]; - } else { - return null; - } - }, - filter: function(query, data, searchKey) { - var _results, i, item, len; - _results = []; - for (i = 0, len = data.length; i < len; i++) { - item = data[i]; - if (~new String(item[searchKey]).toLowerCase().indexOf(query.toLowerCase())) { - _results.push(item); - } - } - return _results; - }, - remoteFilter: null, - sorter: function(query, items, searchKey) { - var _results, i, item, len; - if (!query) { - return items; - } - _results = []; - for (i = 0, len = items.length; i < len; i++) { - item = items[i]; - item.atwho_order = new String(item[searchKey]).toLowerCase().indexOf(query.toLowerCase()); - if (item.atwho_order > -1) { - _results.push(item); - } - } - return _results.sort(function(a, b) { - return a.atwho_order - b.atwho_order; - }); - }, - tplEval: function(tpl, map) { - var error, template; - template = tpl; - try { - if (typeof tpl !== 'string') { - template = tpl(map); - } - return template.replace(/\$\{([^\}]*)\}/g, function(tag, key, pos) { - return map[key]; - }); - } catch (_error) { - error = _error; - return ""; - } - }, - highlighter: function(li, query) { - var regexp; - if (!query) { - return li; - } - regexp = new RegExp(">\\s*(\\w*?)(" + query.replace("+", "\\+") + ")(\\w*)\\s*<", 'ig'); - return li.replace(regexp, function(str, $1, $2, $3) { - return '> ' + $1 + '' + $2 + '' + $3 + ' <'; - }); - }, - beforeInsert: function(value, $li) { - return value; - }, - beforeReposition: function(offset) { - return offset; - }, - afterMatchFailed: function(at, el) {} -}; - -Api = { - load: function(at, data) { - var c; - if (c = this.controller(at)) { - return c.model.load(data); - } - }, - isSelecting: function() { - var ref; - return !!((ref = this.controller()) != null ? ref.view.visible() : void 0); - }, - hide: function() { - var ref; - return (ref = this.controller()) != null ? ref.view.hide() : void 0; - }, - reposition: function() { - var c; - if (c = this.controller()) { - return c.view.reposition(c.rect()); - } - }, - setIframe: function(iframe, asRoot) { - this.setupRootElement(iframe, asRoot); - return null; - }, - run: function() { - return this.dispatch(); - }, - destroy: function() { - this.shutdown(); - return this.$inputor.data('atwho', null); - } -}; - -$.fn.atwho = function(method) { - var _args, result; - _args = arguments; - result = null; - this.filter('textarea, input, [contenteditable=""], [contenteditable=true]').each(function() { - var $this, app; - if (!(app = ($this = $(this)).data("atwho"))) { - $this.data('atwho', (app = new App(this))); - } - if (typeof method === 'object' || !method) { - return app.reg(method.at, method); - } else if (Api[method] && app) { - return result = Api[method].apply(app, Array.prototype.slice.call(_args, 1)); - } else { - return $.error("Method " + method + " does not exist on jQuery.atwho"); - } - }); - if (result != null) { - return result; - } else { - return this; - } -}; - -$.fn.atwho["default"] = { - at: void 0, - alias: void 0, - data: null, - displayTpl: "
    • ${name}
    • ", - insertTpl: "${atwho-at}${name}", - callbacks: DEFAULT_CALLBACKS, - searchKey: "name", - suffix: void 0, - hideWithoutSuffix: false, - startWithSpace: true, - highlightFirst: true, - limit: 5, - maxLen: 20, - minLen: 0, - displayTimeout: 300, - delay: null, - spaceSelectsMatch: false, - tabSelectsMatch: true, - editableAtwhoQueryAttrs: {}, - scrollDuration: 150, - suspendOnComposing: true, - lookUpOnClick: true -}; - -$.fn.atwho.debug = false; - - -})); diff --git a/pagure/static/atwho/jquery.atwho-1.4.1.min.css b/pagure/static/atwho/jquery.atwho-1.4.1.min.css deleted file mode 100644 index 075983d..0000000 --- a/pagure/static/atwho/jquery.atwho-1.4.1.min.css +++ /dev/null @@ -1 +0,0 @@ -.atwho-view{position:absolute;top:0;left:0;display:none;margin-top:18px;background:#fff;color:#000;border:1px solid #DDD;border-radius:3px;box-shadow:0 0 5px rgba(0,0,0,.1);min-width:120px;max-height:200px;overflow:auto;z-index:11110!important}.atwho-view .cur{background:#36F;color:#fff}.atwho-view .cur small{color:#fff}.atwho-view strong{color:#36F}.atwho-view .cur strong{color:#fff;font:700}.atwho-view ul{list-style:none;padding:0;margin:auto}.atwho-view ul li{display:block;padding:5px 10px;border-bottom:1px solid #DDD;cursor:pointer}.atwho-view small{font-size:smaller;color:#777;font-weight:400} \ No newline at end of file diff --git a/pagure/static/atwho/jquery.atwho-1.4.1.min.js b/pagure/static/atwho/jquery.atwho-1.4.1.min.js deleted file mode 100644 index 3488732..0000000 --- a/pagure/static/atwho/jquery.atwho-1.4.1.min.js +++ /dev/null @@ -1 +0,0 @@ -!function(a,b){"function"==typeof define&&define.amd?define(["jquery"],function(a){return b(a)}):"object"==typeof exports?module.exports=b(require("jquery")):b(jQuery)}(this,function(a){var b,c,d,e,f,g,h,i,j,k,l=[].slice,m=function(a,b){function c(){this.constructor=a}for(var d in b)n.call(b,d)&&(a[d]=b[d]);return c.prototype=b.prototype,a.prototype=new c,a.__super__=b.prototype,a},n={}.hasOwnProperty;b=a,d=function(){function a(a){this.currentFlag=null,this.controllers={},this.aliasMaps={},this.$inputor=b(a),this.setupRootElement(),this.listen()}return a.prototype.createContainer=function(a){var c;return null!=(c=this.$el)&&c.remove(),b(a.body).append(this.$el=b("
      "))},a.prototype.setupRootElement=function(a,c){var d;if(null==c&&(c=!1),a)this.window=a.contentWindow,this.document=a.contentDocument||this.window.document,this.iframe=a;else{this.document=this.$inputor[0].ownerDocument,this.window=this.document.defaultView||this.document.parentWindow;try{this.iframe=this.window.frameElement}catch(e){if(d=e,this.iframe=null,b.fn.atwho.debug)throw new Error("iframe auto-discovery is failed.\nPlease use `setIframe` to set the target iframe manually.\n"+d)}}return this.createContainer((this.iframeAsRoot=c)?this.document:document)},a.prototype.controller=function(a){var b,c,d,e;if(this.aliasMaps[a])c=this.controllers[this.aliasMaps[a]];else{e=this.controllers;for(d in e)if(b=e[d],d===a){c=b;break}}return c?c:this.controllers[this.currentFlag]},a.prototype.setContextFor=function(a){return this.currentFlag=a,this},a.prototype.reg=function(a,b){var c,d;return d=(c=this.controllers)[a]||(c[a]=this.$inputor.is("[contentEditable]")?new g(this,a):new j(this,a)),b.alias&&(this.aliasMaps[b.alias]=a),d.init(b),this},a.prototype.listen=function(){return this.$inputor.on("compositionstart",function(a){return function(b){var c;return null!=(c=a.controller())&&c.view.hide(),a.isComposing=!0,null}}(this)).on("compositionend",function(a){return function(b){return a.isComposing=!1,null}}(this)).on("keyup.atwhoInner",function(a){return function(b){return a.onKeyup(b)}}(this)).on("keydown.atwhoInner",function(a){return function(b){return a.onKeydown(b)}}(this)).on("blur.atwhoInner",function(a){return function(b){var c;return(c=a.controller())?(c.expectedQueryCBId=null,c.view.hide(b,c.getOpt("displayTimeout"))):void 0}}(this)).on("click.atwhoInner",function(a){return function(b){return a.dispatch(b)}}(this)).on("scroll.atwhoInner",function(a){return function(){var b;return b=a.$inputor.scrollTop(),function(c){var d,e;return d=c.target.scrollTop,b!==d&&null!=(e=a.controller())&&e.view.hide(c),b=d,!0}}}(this)())},a.prototype.shutdown=function(){var a,b,c;c=this.controllers;for(a in c)b=c[a],b.destroy(),delete this.controllers[a];return this.$inputor.off(".atwhoInner"),this.$el.remove()},a.prototype.dispatch=function(a){var b,c,d,e;d=this.controllers,e=[];for(b in d)c=d[b],e.push(c.lookUp(a));return e},a.prototype.onKeyup=function(a){var c;switch(a.keyCode){case h.ESC:a.preventDefault(),null!=(c=this.controller())&&c.view.hide();break;case h.DOWN:case h.UP:case h.CTRL:case h.ENTER:b.noop();break;case h.P:case h.N:a.ctrlKey||this.dispatch(a);break;default:this.dispatch(a)}},a.prototype.onKeydown=function(a){var c,d;if(d=null!=(c=this.controller())?c.view:void 0,d&&d.visible())switch(a.keyCode){case h.ESC:a.preventDefault(),d.hide(a);break;case h.UP:a.preventDefault(),d.prev();break;case h.DOWN:a.preventDefault(),d.next();break;case h.P:if(!a.ctrlKey)return;a.preventDefault(),d.prev();break;case h.N:if(!a.ctrlKey)return;a.preventDefault(),d.next();break;case h.TAB:case h.ENTER:case h.SPACE:if(!d.visible())return;if(!this.controller().getOpt("spaceSelectsMatch")&&a.keyCode===h.SPACE)return;if(!this.controller().getOpt("tabSelectsMatch")&&a.keyCode===h.TAB)return;d.highlighted()?(a.preventDefault(),d.choose(a)):d.hide(a);break;default:b.noop()}},a}(),e=function(){function a(a,c){this.app=a,this.at=c,this.$inputor=this.app.$inputor,this.id=this.$inputor[0].id||this.uid(),this.expectedQueryCBId=null,this.setting=null,this.query=null,this.pos=0,this.range=null,0===(this.$el=b("#atwho-ground-"+this.id,this.app.$el)).length&&this.app.$el.append(this.$el=b("
      ")),this.model=new i(this),this.view=new k(this)}return a.prototype.uid=function(){return(Math.random().toString(16)+"000000000").substr(2,8)+(new Date).getTime()},a.prototype.init=function(a){return this.setting=b.extend({},this.setting||b.fn.atwho["default"],a),this.view.init(),this.model.reload(this.setting.data)},a.prototype.destroy=function(){return this.trigger("beforeDestroy"),this.model.destroy(),this.view.destroy(),this.$el.remove()},a.prototype.callDefault=function(){var a,c,d;d=arguments[0],a=2<=arguments.length?l.call(arguments,1):[];try{return f[d].apply(this,a)}catch(e){return c=e,b.error(c+" Or maybe At.js doesn't have function "+d)}},a.prototype.trigger=function(a,b){var c,d;return null==b&&(b=[]),b.push(this),c=this.getOpt("alias"),d=c?a+"-"+c+".atwho":a+".atwho",this.$inputor.trigger(d,b)},a.prototype.callbacks=function(a){return this.getOpt("callbacks")[a]||f[a]},a.prototype.getOpt=function(a,b){var c;try{return this.setting[a]}catch(d){return c=d,null}},a.prototype.insertContentFor=function(a){var c,d;return d=this.getOpt("insertTpl"),c=b.extend({},a.data("item-data"),{"atwho-at":this.at}),this.callbacks("tplEval").call(this,d,c,"onInsert")},a.prototype.renderView=function(a){var b;return b=this.getOpt("searchKey"),a=this.callbacks("sorter").call(this,this.query.text,a.slice(0,1001),b),this.view.render(a.slice(0,this.getOpt("limit")))},a.arrayToDefaultHash=function(a){var c,d,e,f;if(!b.isArray(a))return a;for(f=[],c=0,e=a.length;e>c;c++)d=a[c],b.isPlainObject(d)?f.push(d):f.push({name:d});return f},a.prototype.lookUp=function(a){var b,c;if((!a||"click"!==a.type||this.getOpt("lookUpOnClick"))&&(!this.getOpt("suspendOnComposing")||!this.app.isComposing))return(b=this.catchQuery(a))?(this.app.setContextFor(this.at),(c=this.getOpt("delay"))?this._delayLookUp(b,c):this._lookUp(b),b):(this.expectedQueryCBId=null,b)},a.prototype._delayLookUp=function(a,b){var c,d;return c=Date.now?Date.now():(new Date).getTime(),this.previousCallTime||(this.previousCallTime=c),d=b-(c-this.previousCallTime),d>0&&b>d?(this.previousCallTime=c,this._stopDelayedCall(),this.delayedCallTimeout=setTimeout(function(b){return function(){return b.previousCallTime=0,b.delayedCallTimeout=null,b._lookUp(a)}}(this),b)):(this._stopDelayedCall(),this.previousCallTime!==c&&(this.previousCallTime=0),this._lookUp(a))},a.prototype._stopDelayedCall=function(){return this.delayedCallTimeout?(clearTimeout(this.delayedCallTimeout),this.delayedCallTimeout=null):void 0},a.prototype._generateQueryCBId=function(){return{}},a.prototype._lookUp=function(a){var c;return c=function(a,b){return a===this.expectedQueryCBId?b&&b.length>0?this.renderView(this.constructor.arrayToDefaultHash(b)):this.view.hide():void 0},this.expectedQueryCBId=this._generateQueryCBId(),this.model.query(a.text,b.proxy(c,this,this.expectedQueryCBId))},a}(),j=function(a){function c(){return c.__super__.constructor.apply(this,arguments)}return m(c,a),c.prototype.catchQuery=function(){var a,b,c,d,e,f,g;return b=this.$inputor.val(),a=this.$inputor.caret("pos",{iframe:this.app.iframe}),g=b.slice(0,a),e=this.callbacks("matcher").call(this,this.at,g,this.getOpt("startWithSpace")),d="string"==typeof e,d&&e.length0?a.getRangeAt(0):void 0},c.prototype._setRange=function(a,c,d){return null==d&&(d=this._getRange()),d?(c=b(c)[0],"after"===a?(d.setEndAfter(c),d.setStartAfter(c)):(d.setEndBefore(c),d.setStartBefore(c)),d.collapse(!1),this._clearRange(d)):void 0},c.prototype._clearRange=function(a){var b;return null==a&&(a=this._getRange()),b=this.app.window.getSelection(),null==this.ctrl_a_pressed?(b.removeAllRanges(),b.addRange(a)):void 0},c.prototype._movingEvent=function(a){var b;return"click"===a.type||(b=a.which)===h.RIGHT||b===h.LEFT||b===h.UP||b===h.DOWN},c.prototype._unwrap=function(a){var c;return a=b(a).unwrap().get(0),(c=a.nextSibling)&&c.nodeValue&&(a.nodeValue+=c.nodeValue,b(c).remove()),a},c.prototype.catchQuery=function(a){var c,d,e,f,g,i,j,k,l,m,n,o;if((o=this._getRange())&&o.collapsed){if(a.which===h.ENTER)return(d=b(o.startContainer).closest(".atwho-query")).contents().unwrap(),d.is(":empty")&&d.remove(),(d=b(".atwho-query",this.app.document)).text(d.text()).contents().last().unwrap(),void this._clearRange();if(/firefox/i.test(navigator.userAgent)){if(b(o.startContainer).is(this.$inputor))return void this._clearRange();a.which===h.BACKSPACE&&o.startContainer.nodeType===document.ELEMENT_NODE&&(l=o.startOffset-1)>=0?(e=o.cloneRange(),e.setStart(o.startContainer,l),b(e.cloneContents()).contents().last().is(".atwho-inserted")&&(g=b(o.startContainer).contents().get(l),this._setRange("after",b(g).contents().last()))):a.which===h.LEFT&&o.startContainer.nodeType===document.TEXT_NODE&&(c=b(o.startContainer.previousSibling),c.is(".atwho-inserted")&&0===o.startOffset&&this._setRange("after",c.contents().last()))}if(b(o.startContainer).closest(".atwho-inserted").addClass("atwho-query").siblings().removeClass("atwho-query"),(d=b(".atwho-query",this.app.document)).length>0&&d.is(":empty")&&0===d.text().length&&d.remove(),this._movingEvent(a)||d.removeClass("atwho-inserted"),d.length>0)switch(a.which){case h.LEFT:return this._setRange("before",d.get(0),o),void d.removeClass("atwho-query");case h.RIGHT:return this._setRange("after",d.get(0).nextSibling,o),void d.removeClass("atwho-query")}if(d.length>0&&(n=d.attr("data-atwho-at-query"))&&(d.empty().html(n).attr("data-atwho-at-query",null),this._setRange("after",d.get(0),o)),e=o.cloneRange(),e.setStart(o.startContainer,0),k=this.callbacks("matcher").call(this,this.at,e.toString(),this.getOpt("startWithSpace")),i="string"==typeof k,0===d.length&&i&&(f=o.startOffset-this.at.length-k.length)>=0&&(o.setStart(o.startContainer,f),d=b("",this.app.document).attr(this.getOpt("editableAtwhoQueryAttrs")).addClass("atwho-query"),o.surroundContents(d.get(0)),j=d.contents().last().get(0),/firefox/i.test(navigator.userAgent)?(o.setStart(j,j.length),o.setEnd(j,j.length),this._clearRange(o)):this._setRange("after",j,o)),!(i&&k.length=0&&(this._movingEvent(a)&&d.hasClass("atwho-inserted")?d.removeClass("atwho-query"):!1!==this.callbacks("afterMatchFailed").call(this,this.at,d)&&this._setRange("after",this._unwrap(d.text(d.text()).contents().first()))),null)}},c.prototype.rect=function(){var a,c,d;return d=this.query.el.offset(),this.app.iframe&&!this.app.iframeAsRoot&&(c=(a=b(this.app.iframe)).offset(),d.left+=c.left-this.$inputor.scrollLeft(),d.top+=c.top-this.$inputor.scrollTop()),d.bottom=d.top+this.query.el.height(),d},c.prototype.insert=function(a,b){var c,d,e,f;return e=""===(e=this.getOpt("suffix"))?e:e||" ",c=b.data("item-data"),this.query.el.removeClass("atwho-query").addClass("atwho-inserted").html(a).attr("data-atwho-at-query",""+c["atwho-at"]+this.query.text),(d=this._getRange())&&(d.setEndAfter(this.query.el[0]),d.collapse(!1),d.insertNode(f=this.app.document.createTextNode("‍"+e)),this._setRange("after",f,d)),this.$inputor.is(":focus")||this.$inputor.focus(),this.$inputor.change()},c}(e),i=function(){function a(a){this.context=a,this.at=this.context.at,this.storage=this.context.$inputor}return a.prototype.destroy=function(){return this.storage.data(this.at,null)},a.prototype.saved=function(){return this.fetch()>0},a.prototype.query=function(a,b){var c,d,e;return d=this.fetch(),e=this.context.getOpt("searchKey"),d=this.context.callbacks("filter").call(this.context,a,d,e)||[],c=this.context.callbacks("remoteFilter"),d.length>0||!c&&0===d.length?b(d):c.call(this.context,a,b)},a.prototype.fetch=function(){return this.storage.data(this.at)||[]},a.prototype.save=function(a){return this.storage.data(this.at,this.context.callbacks("beforeSave").call(this.context,a||[]))},a.prototype.load=function(a){return!this.saved()&&a?this._load(a):void 0},a.prototype.reload=function(a){return this._load(a)},a.prototype._load=function(a){return"string"==typeof a?b.ajax(a,{dataType:"json"}).done(function(a){return function(b){return a.save(b)}}(this)):this.save(a)},a}(),k=function(){function a(a){this.context=a,this.$el=b("
        "),this.timeoutID=null,this.context.$el.append(this.$el),this.bindEvent()}return a.prototype.init=function(){var a;return a=this.context.getOpt("alias")||this.context.at.charCodeAt(0),this.$el.attr({id:"at-view-"+a})},a.prototype.destroy=function(){return this.$el.remove()},a.prototype.bindEvent=function(){var a;return a=this.$el.find("ul"),a.on("mouseenter.atwho-view","li",function(c){return a.find(".cur").removeClass("cur"),b(c.currentTarget).addClass("cur")}).on("click.atwho-view","li",function(c){return function(d){return a.find(".cur").removeClass("cur"),b(d.currentTarget).addClass("cur"),c.choose(d),d.preventDefault()}}(this))},a.prototype.visible=function(){return this.$el.is(":visible")},a.prototype.highlighted=function(){return this.$el.find(".cur").length>0},a.prototype.choose=function(a){var b,c;return(b=this.$el.find(".cur")).length&&(c=this.context.insertContentFor(b),this.context._stopDelayedCall(),this.context.insert(this.context.callbacks("beforeInsert").call(this.context,c,b),b),this.context.trigger("inserted",[b,a]),this.hide(a)),this.context.getOpt("hideWithoutSuffix")?this.stopShowing=!0:void 0},a.prototype.reposition=function(a){var c,d,e,f;return c=this.context.app.iframeAsRoot?this.context.app.window:window,a.bottom+this.$el.height()-b(c).scrollTop()>b(c).height()&&(a.bottom=a.top-this.$el.height()),a.left>(e=b(c).width()-this.$el.width()-5)&&(a.left=e),d={left:a.left,top:a.bottom},null!=(f=this.context.callbacks("beforeReposition"))&&f.call(this.context,d),this.$el.offset(d),this.context.trigger("reposition",[d])},a.prototype.next=function(){var a,b;return a=this.$el.find(".cur").removeClass("cur"),b=a.next(),b.length||(b=this.$el.find("li:first")),b.addClass("cur"),this.scrollTop(Math.max(0,a.innerHeight()*(b.index()+2)-this.$el.height()))},a.prototype.prev=function(){var a,b;return a=this.$el.find(".cur").removeClass("cur"),b=a.prev(),b.length||(b=this.$el.find("li:last")),b.addClass("cur"),this.scrollTop(Math.max(0,a.innerHeight()*(b.index()+2)-this.$el.height()))},a.prototype.scrollTop=function(a){var b;return b=this.context.getOpt("scrollDuration"),b?this.$el.animate({scrollTop:a},b):this.$el.scrollTop(a)},a.prototype.show=function(){var a;return this.stopShowing?void(this.stopShowing=!1):(this.visible()||(this.$el.show(),this.$el.scrollTop(0),this.context.trigger("shown")),(a=this.context.rect())?this.reposition(a):void 0)},a.prototype.hide=function(a,b){var c;if(this.visible())return isNaN(b)?(this.$el.hide(),this.context.trigger("hidden",[a])):(c=function(a){return function(){return a.hide()}}(this),clearTimeout(this.timeoutID),this.timeoutID=setTimeout(c,b))},a.prototype.render=function(a){var c,d,e,f,g,h,i;if(!(b.isArray(a)&&a.length>0))return void this.hide();for(this.$el.find("ul").empty(),d=this.$el.find("ul"),i=this.context.getOpt("displayTpl"),e=0,g=a.length;g>e;e++)f=a[e],f=b.extend({},f,{"atwho-at":this.context.at}),h=this.context.callbacks("tplEval").call(this.context,i,f,"onDisplay"),c=b(this.context.callbacks("highlighter").call(this.context,h,this.context.query.text)),c.data("item-data",f),d.append(c);return this.show(),this.context.getOpt("highlightFirst")?d.find("li:first").addClass("cur"):void 0},a}(),h={DOWN:40,UP:38,ESC:27,TAB:9,ENTER:13,CTRL:17,A:65,P:80,N:78,LEFT:37,UP:38,RIGHT:39,DOWN:40,BACKSPACE:8,SPACE:32},f={beforeSave:function(a){return e.arrayToDefaultHash(a)},matcher:function(a,b,c,d){var e,f,g,h,i;return a=a.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g,"\\$&"),c&&(a="(?:^|\\s)"+a),e=decodeURI("%C3%80"),f=decodeURI("%C3%BF"),i=d?" ":"",h=new RegExp(a+"([A-Za-z"+e+"-"+f+"0-9_"+i+"'.+-]*)$|"+a+"([^\\x00-\\xff]*)$","gi"),g=h.exec(b),g?g[2]||g[1]:null},filter:function(a,b,c){var d,e,f,g;for(d=[],e=0,g=b.length;g>e;e++)f=b[e],~new String(f[c]).toLowerCase().indexOf(a.toLowerCase())&&d.push(f);return d},remoteFilter:null,sorter:function(a,b,c){var d,e,f,g;if(!a)return b;for(d=[],e=0,g=b.length;g>e;e++)f=b[e],f.atwho_order=new String(f[c]).toLowerCase().indexOf(a.toLowerCase()),f.atwho_order>-1&&d.push(f);return d.sort(function(a,b){return a.atwho_order-b.atwho_order})},tplEval:function(a,b){var c,d;d=a;try{return"string"!=typeof a&&(d=a(b)),d.replace(/\$\{([^\}]*)\}/g,function(a,c,d){return b[c]})}catch(e){return c=e,""}},highlighter:function(a,b){var c;return b?(c=new RegExp(">\\s*(\\w*?)("+b.replace("+","\\+")+")(\\w*)\\s*<","ig"),a.replace(c,function(a,b,c,d){return"> "+b+""+c+""+d+" <"})):a},beforeInsert:function(a,b){return a},beforeReposition:function(a){return a},afterMatchFailed:function(a,b){}},c={load:function(a,b){var c;return(c=this.controller(a))?c.model.load(b):void 0},isSelecting:function(){var a;return!!(null!=(a=this.controller())?a.view.visible():void 0)},hide:function(){var a;return null!=(a=this.controller())?a.view.hide():void 0},reposition:function(){var a;return(a=this.controller())?a.view.reposition(a.rect()):void 0},setIframe:function(a,b){return this.setupRootElement(a,b),null},run:function(){return this.dispatch()},destroy:function(){return this.shutdown(),this.$inputor.data("atwho",null)}},b.fn.atwho=function(a){var e,f;return e=arguments,f=null,this.filter('textarea, input, [contenteditable=""], [contenteditable=true]').each(function(){var g,h;return(h=(g=b(this)).data("atwho"))||g.data("atwho",h=new d(this)),"object"!=typeof a&&a?c[a]&&h?f=c[a].apply(h,Array.prototype.slice.call(e,1)):b.error("Method "+a+" does not exist on jQuery.atwho"):h.reg(a.at,a)}),null!=f?f:this},b.fn.atwho["default"]={at:void 0,alias:void 0,data:null,displayTpl:"
      • ${name}
      • ",insertTpl:"${atwho-at}${name}",callbacks:f,searchKey:"name",suffix:void 0,hideWithoutSuffix:!1,startWithSpace:!0,highlightFirst:!0,limit:5,maxLen:20,minLen:0,displayTimeout:300,delay:null,spaceSelectsMatch:!1,tabSelectsMatch:!0,editableAtwhoQueryAttrs:{},scrollDuration:150,suspendOnComposing:!0,lookUpOnClick:!0},b.fn.atwho.debug=!1}); \ No newline at end of file diff --git a/pagure/static/atwho/jquery.atwho-1.5.1.css b/pagure/static/atwho/jquery.atwho-1.5.1.css new file mode 100644 index 0000000..dad94ed --- /dev/null +++ b/pagure/static/atwho/jquery.atwho-1.5.1.css @@ -0,0 +1,72 @@ +.atwho-view { + position:absolute; + top: 0; + left: 0; + display: none; + margin-top: 18px; + background: white; + color: black; + border: 1px solid #DDD; + border-radius: 3px; + box-shadow: 0 0 5px rgba(0,0,0,0.1); + min-width: 120px; + z-index: 11110 !important; +} + +.atwho-view .atwho-header { + padding: 5px; + margin: 5px; + cursor: pointer; + border-bottom: solid 1px #eaeff1; + color: #6f8092; + font-size: 11px; + font-weight: bold; +} + +.atwho-view .atwho-header .small { + color: #6f8092; + float: right; + padding-top: 2px; + margin-right: -5px; + font-size: 12px; + font-weight: normal; +} + +.atwho-view .atwho-header:hover { + cursor: default; +} + +.atwho-view .cur { + background: #3366FF; + color: white; +} +.atwho-view .cur small { + color: white; +} +.atwho-view strong { + color: #3366FF; +} +.atwho-view .cur strong { + color: white; + font:bold; +} +.atwho-view ul { + /* width: 100px; */ + list-style:none; + padding:0; + margin:auto; + max-height: 200px; + overflow-y: auto; +} +.atwho-view ul li { + display: block; + padding: 5px 10px; + border-bottom: 1px solid #DDD; + cursor: pointer; + /* border-top: 1px solid #C8C8C8; */ +} +.atwho-view small { + font-size: smaller; + color: #777; + font-weight: normal; +} diff --git a/pagure/static/atwho/jquery.atwho-1.5.1.js b/pagure/static/atwho/jquery.atwho-1.5.1.js new file mode 100644 index 0000000..0d295eb --- /dev/null +++ b/pagure/static/atwho/jquery.atwho-1.5.1.js @@ -0,0 +1,1202 @@ +/** + * at.js - 1.5.1 + * Copyright (c) 2016 chord.luo ; + * Homepage: http://ichord.github.com/At.js + * License: MIT + */ +(function (root, factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module unless amdModuleId is set + define(["jquery"], function (a0) { + return (factory(a0)); + }); + } else if (typeof exports === 'object') { + // Node. Does not work with strict CommonJS, but + // only CommonJS-like environments that support module.exports, + // like Node. + module.exports = factory(require("jquery")); + } else { + factory(jQuery); + } +}(this, function ($) { +var DEFAULT_CALLBACKS, KEY_CODE; + +KEY_CODE = { + DOWN: 40, + UP: 38, + ESC: 27, + TAB: 9, + ENTER: 13, + CTRL: 17, + A: 65, + P: 80, + N: 78, + LEFT: 37, + UP: 38, + RIGHT: 39, + DOWN: 40, + BACKSPACE: 8, + SPACE: 32 +}; + +DEFAULT_CALLBACKS = { + beforeSave: function(data) { + return Controller.arrayToDefaultHash(data); + }, + matcher: function(flag, subtext, should_startWithSpace, acceptSpaceBar) { + var _a, _y, match, regexp, space; + flag = flag.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); + if (should_startWithSpace) { + flag = '(?:^|\\s)' + flag; + } + _a = decodeURI("%C3%80"); + _y = decodeURI("%C3%BF"); + space = acceptSpaceBar ? "\ " : ""; + regexp = new RegExp(flag + "([A-Za-z" + _a + "-" + _y + "0-9_" + space + "\'\.\+\-]*)$|" + flag + "([^\\x00-\\xff]*)$", 'gi'); + match = regexp.exec(subtext); + if (match) { + return match[2] || match[1]; + } else { + return null; + } + }, + filter: function(query, data, searchKey) { + var _results, i, item, len; + _results = []; + for (i = 0, len = data.length; i < len; i++) { + item = data[i]; + if (~new String(item[searchKey]).toLowerCase().indexOf(query.toLowerCase())) { + _results.push(item); + } + } + return _results; + }, + remoteFilter: null, + sorter: function(query, items, searchKey) { + var _results, i, item, len; + if (!query) { + return items; + } + _results = []; + for (i = 0, len = items.length; i < len; i++) { + item = items[i]; + item.atwho_order = new String(item[searchKey]).toLowerCase().indexOf(query.toLowerCase()); + if (item.atwho_order > -1) { + _results.push(item); + } + } + return _results.sort(function(a, b) { + return a.atwho_order - b.atwho_order; + }); + }, + tplEval: function(tpl, map) { + var error, error1, template; + template = tpl; + try { + if (typeof tpl !== 'string') { + template = tpl(map); + } + return template.replace(/\$\{([^\}]*)\}/g, function(tag, key, pos) { + return map[key]; + }); + } catch (error1) { + error = error1; + return ""; + } + }, + highlighter: function(li, query) { + var regexp; + if (!query) { + return li; + } + regexp = new RegExp(">\\s*(\\w*?)(" + query.replace("+", "\\+") + ")(\\w*)\\s*<", 'ig'); + return li.replace(regexp, function(str, $1, $2, $3) { + return '> ' + $1 + '' + $2 + '' + $3 + ' <'; + }); + }, + beforeInsert: function(value, $li, e) { + return value; + }, + beforeReposition: function(offset) { + return offset; + }, + afterMatchFailed: function(at, el) {} +}; + +var App; + +App = (function() { + function App(inputor) { + this.currentFlag = null; + this.controllers = {}; + this.aliasMaps = {}; + this.$inputor = $(inputor); + this.setupRootElement(); + this.listen(); + } + + App.prototype.createContainer = function(doc) { + var ref; + if ((ref = this.$el) != null) { + ref.remove(); + } + return $(doc.body).append(this.$el = $("
        ")); + }; + + App.prototype.setupRootElement = function(iframe, asRoot) { + var error, error1; + if (asRoot == null) { + asRoot = false; + } + if (iframe) { + this.window = iframe.contentWindow; + this.document = iframe.contentDocument || this.window.document; + this.iframe = iframe; + } else { + this.document = this.$inputor[0].ownerDocument; + this.window = this.document.defaultView || this.document.parentWindow; + try { + this.iframe = this.window.frameElement; + } catch (error1) { + error = error1; + this.iframe = null; + if ($.fn.atwho.debug) { + throw new Error("iframe auto-discovery is failed.\nPlease use `setIframe` to set the target iframe manually.\n" + error); + } + } + } + return this.createContainer((this.iframeAsRoot = asRoot) ? this.document : document); + }; + + App.prototype.controller = function(at) { + var c, current, currentFlag, ref; + if (this.aliasMaps[at]) { + current = this.controllers[this.aliasMaps[at]]; + } else { + ref = this.controllers; + for (currentFlag in ref) { + c = ref[currentFlag]; + if (currentFlag === at) { + current = c; + break; + } + } + } + if (current) { + return current; + } else { + return this.controllers[this.currentFlag]; + } + }; + + App.prototype.setContextFor = function(at) { + this.currentFlag = at; + return this; + }; + + App.prototype.reg = function(flag, setting) { + var base, controller; + controller = (base = this.controllers)[flag] || (base[flag] = this.$inputor.is('[contentEditable]') ? new EditableController(this, flag) : new TextareaController(this, flag)); + if (setting.alias) { + this.aliasMaps[setting.alias] = flag; + } + controller.init(setting); + return this; + }; + + App.prototype.listen = function() { + return this.$inputor.on('compositionstart', (function(_this) { + return function(e) { + var ref; + if ((ref = _this.controller()) != null) { + ref.view.hide(); + } + _this.isComposing = true; + return null; + }; + })(this)).on('compositionend', (function(_this) { + return function(e) { + _this.isComposing = false; + setTimeout(function(e) { + return _this.dispatch(e); + }); + return null; + }; + })(this)).on('keyup.atwhoInner', (function(_this) { + return function(e) { + return _this.onKeyup(e); + }; + })(this)).on('keydown.atwhoInner', (function(_this) { + return function(e) { + return _this.onKeydown(e); + }; + })(this)).on('blur.atwhoInner', (function(_this) { + return function(e) { + var c; + if (c = _this.controller()) { + c.expectedQueryCBId = null; + return c.view.hide(e, c.getOpt("displayTimeout")); + } + }; + })(this)).on('click.atwhoInner', (function(_this) { + return function(e) { + return _this.dispatch(e); + }; + })(this)).on('scroll.atwhoInner', (function(_this) { + return function() { + var lastScrollTop; + lastScrollTop = _this.$inputor.scrollTop(); + return function(e) { + var currentScrollTop, ref; + currentScrollTop = e.target.scrollTop; + if (lastScrollTop !== currentScrollTop) { + if ((ref = _this.controller()) != null) { + ref.view.hide(e); + } + } + lastScrollTop = currentScrollTop; + return true; + }; + }; + })(this)()); + }; + + App.prototype.shutdown = function() { + var _, c, ref; + ref = this.controllers; + for (_ in ref) { + c = ref[_]; + c.destroy(); + delete this.controllers[_]; + } + this.$inputor.off('.atwhoInner'); + return this.$el.remove(); + }; + + App.prototype.dispatch = function(e) { + var _, c, ref, results; + ref = this.controllers; + results = []; + for (_ in ref) { + c = ref[_]; + results.push(c.lookUp(e)); + } + return results; + }; + + App.prototype.onKeyup = function(e) { + var ref; + switch (e.keyCode) { + case KEY_CODE.ESC: + e.preventDefault(); + if ((ref = this.controller()) != null) { + ref.view.hide(); + } + break; + case KEY_CODE.DOWN: + case KEY_CODE.UP: + case KEY_CODE.CTRL: + case KEY_CODE.ENTER: + $.noop(); + break; + case KEY_CODE.P: + case KEY_CODE.N: + if (!e.ctrlKey) { + this.dispatch(e); + } + break; + default: + this.dispatch(e); + } + }; + + App.prototype.onKeydown = function(e) { + var ref, view; + view = (ref = this.controller()) != null ? ref.view : void 0; + if (!(view && view.visible())) { + return; + } + switch (e.keyCode) { + case KEY_CODE.ESC: + e.preventDefault(); + view.hide(e); + break; + case KEY_CODE.UP: + e.preventDefault(); + view.prev(); + break; + case KEY_CODE.DOWN: + e.preventDefault(); + view.next(); + break; + case KEY_CODE.P: + if (!e.ctrlKey) { + return; + } + e.preventDefault(); + view.prev(); + break; + case KEY_CODE.N: + if (!e.ctrlKey) { + return; + } + e.preventDefault(); + view.next(); + break; + case KEY_CODE.TAB: + case KEY_CODE.ENTER: + case KEY_CODE.SPACE: + if (!view.visible()) { + return; + } + if (!this.controller().getOpt('spaceSelectsMatch') && e.keyCode === KEY_CODE.SPACE) { + return; + } + if (!this.controller().getOpt('tabSelectsMatch') && e.keyCode === KEY_CODE.TAB) { + return; + } + if (view.highlighted()) { + e.preventDefault(); + view.choose(e); + } else { + view.hide(e); + } + break; + default: + $.noop(); + } + }; + + return App; + +})(); + +var Controller, + slice = [].slice; + +Controller = (function() { + Controller.prototype.uid = function() { + return (Math.random().toString(16) + "000000000").substr(2, 8) + (new Date().getTime()); + }; + + function Controller(app, at1) { + this.app = app; + this.at = at1; + this.$inputor = this.app.$inputor; + this.id = this.$inputor[0].id || this.uid(); + this.expectedQueryCBId = null; + this.setting = null; + this.query = null; + this.pos = 0; + this.range = null; + if ((this.$el = $("#atwho-ground-" + this.id, this.app.$el)).length === 0) { + this.app.$el.append(this.$el = $("
        ")); + } + this.model = new Model(this); + this.view = new View(this); + } + + Controller.prototype.init = function(setting) { + this.setting = $.extend({}, this.setting || $.fn.atwho["default"], setting); + this.view.init(); + return this.model.reload(this.setting.data); + }; + + Controller.prototype.destroy = function() { + this.trigger('beforeDestroy'); + this.model.destroy(); + this.view.destroy(); + return this.$el.remove(); + }; + + Controller.prototype.callDefault = function() { + var args, error, error1, funcName; + funcName = arguments[0], args = 2 <= arguments.length ? slice.call(arguments, 1) : []; + try { + return DEFAULT_CALLBACKS[funcName].apply(this, args); + } catch (error1) { + error = error1; + return $.error(error + " Or maybe At.js doesn't have function " + funcName); + } + }; + + Controller.prototype.trigger = function(name, data) { + var alias, eventName; + if (data == null) { + data = []; + } + data.push(this); + alias = this.getOpt('alias'); + eventName = alias ? name + "-" + alias + ".atwho" : name + ".atwho"; + return this.$inputor.trigger(eventName, data); + }; + + Controller.prototype.callbacks = function(funcName) { + return this.getOpt("callbacks")[funcName] || DEFAULT_CALLBACKS[funcName]; + }; + + Controller.prototype.getOpt = function(at, default_value) { + var e, error1; + try { + return this.setting[at]; + } catch (error1) { + e = error1; + return null; + } + }; + + Controller.prototype.insertContentFor = function($li) { + var data, tpl; + tpl = this.getOpt('insertTpl'); + data = $.extend({}, $li.data('item-data'), { + 'atwho-at': this.at + }); + return this.callbacks("tplEval").call(this, tpl, data, "onInsert"); + }; + + Controller.prototype.renderView = function(data) { + var searchKey; + searchKey = this.getOpt("searchKey"); + data = this.callbacks("sorter").call(this, this.query.text, data.slice(0, 1001), searchKey); + return this.view.render(data.slice(0, this.getOpt('limit'))); + }; + + Controller.arrayToDefaultHash = function(data) { + var i, item, len, results; + if (!$.isArray(data)) { + return data; + } + results = []; + for (i = 0, len = data.length; i < len; i++) { + item = data[i]; + if ($.isPlainObject(item)) { + results.push(item); + } else { + results.push({ + name: item + }); + } + } + return results; + }; + + Controller.prototype.lookUp = function(e) { + var query, wait; + if (e && e.type === 'click' && !this.getOpt('lookUpOnClick')) { + return; + } + if (this.getOpt('suspendOnComposing') && this.app.isComposing) { + return; + } + query = this.catchQuery(e); + if (!query) { + this.expectedQueryCBId = null; + return query; + } + this.app.setContextFor(this.at); + if (wait = this.getOpt('delay')) { + this._delayLookUp(query, wait); + } else { + this._lookUp(query); + } + return query; + }; + + Controller.prototype._delayLookUp = function(query, wait) { + var now, remaining; + now = Date.now ? Date.now() : new Date().getTime(); + this.previousCallTime || (this.previousCallTime = now); + remaining = wait - (now - this.previousCallTime); + if ((0 < remaining && remaining < wait)) { + this.previousCallTime = now; + this._stopDelayedCall(); + return this.delayedCallTimeout = setTimeout((function(_this) { + return function() { + _this.previousCallTime = 0; + _this.delayedCallTimeout = null; + return _this._lookUp(query); + }; + })(this), wait); + } else { + this._stopDelayedCall(); + if (this.previousCallTime !== now) { + this.previousCallTime = 0; + } + return this._lookUp(query); + } + }; + + Controller.prototype._stopDelayedCall = function() { + if (this.delayedCallTimeout) { + clearTimeout(this.delayedCallTimeout); + return this.delayedCallTimeout = null; + } + }; + + Controller.prototype._generateQueryCBId = function() { + return {}; + }; + + Controller.prototype._lookUp = function(query) { + var _callback; + _callback = function(queryCBId, data) { + if (queryCBId !== this.expectedQueryCBId) { + return; + } + if (data && data.length > 0) { + return this.renderView(this.constructor.arrayToDefaultHash(data)); + } else { + return this.view.hide(); + } + }; + this.expectedQueryCBId = this._generateQueryCBId(); + return this.model.query(query.text, $.proxy(_callback, this, this.expectedQueryCBId)); + }; + + return Controller; + +})(); + +var TextareaController, + extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, + hasProp = {}.hasOwnProperty; + +TextareaController = (function(superClass) { + extend(TextareaController, superClass); + + function TextareaController() { + return TextareaController.__super__.constructor.apply(this, arguments); + } + + TextareaController.prototype.catchQuery = function() { + var caretPos, content, end, isString, query, start, subtext; + content = this.$inputor.val(); + caretPos = this.$inputor.caret('pos', { + iframe: this.app.iframe + }); + subtext = content.slice(0, caretPos); + query = this.callbacks("matcher").call(this, this.at, subtext, this.getOpt('startWithSpace'), this.getOpt("acceptSpaceBar")); + isString = typeof query === 'string'; + if (isString && query.length < this.getOpt('minLen', 0)) { + return; + } + if (isString && query.length <= this.getOpt('maxLen', 20)) { + start = caretPos - query.length; + end = start + query.length; + this.pos = start; + query = { + 'text': query, + 'headPos': start, + 'endPos': end + }; + this.trigger("matched", [this.at, query.text]); + } else { + query = null; + this.view.hide(); + } + return this.query = query; + }; + + TextareaController.prototype.rect = function() { + var c, iframeOffset, scaleBottom; + if (!(c = this.$inputor.caret('offset', this.pos - 1, { + iframe: this.app.iframe + }))) { + return; + } + if (this.app.iframe && !this.app.iframeAsRoot) { + iframeOffset = $(this.app.iframe).offset(); + c.left += iframeOffset.left; + c.top += iframeOffset.top; + } + scaleBottom = this.app.document.selection ? 0 : 2; + return { + left: c.left, + top: c.top, + bottom: c.top + c.height + scaleBottom + }; + }; + + TextareaController.prototype.insert = function(content, $li) { + var $inputor, source, startStr, suffix, text; + $inputor = this.$inputor; + source = $inputor.val(); + startStr = source.slice(0, Math.max(this.query.headPos - this.at.length, 0)); + suffix = (suffix = this.getOpt('suffix')) === "" ? suffix : suffix || " "; + content += suffix; + text = "" + startStr + content + (source.slice(this.query['endPos'] || 0)); + $inputor.val(text); + $inputor.caret('pos', startStr.length + content.length, { + iframe: this.app.iframe + }); + if (!$inputor.is(':focus')) { + $inputor.focus(); + } + return $inputor.change(); + }; + + return TextareaController; + +})(Controller); + +var EditableController, + extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, + hasProp = {}.hasOwnProperty; + +EditableController = (function(superClass) { + extend(EditableController, superClass); + + function EditableController() { + return EditableController.__super__.constructor.apply(this, arguments); + } + + EditableController.prototype._getRange = function() { + var sel; + sel = this.app.window.getSelection(); + if (sel.rangeCount > 0) { + return sel.getRangeAt(0); + } + }; + + EditableController.prototype._setRange = function(position, node, range) { + if (range == null) { + range = this._getRange(); + } + if (!range) { + return; + } + node = $(node)[0]; + if (position === 'after') { + range.setEndAfter(node); + range.setStartAfter(node); + } else { + range.setEndBefore(node); + range.setStartBefore(node); + } + range.collapse(false); + return this._clearRange(range); + }; + + EditableController.prototype._clearRange = function(range) { + var sel; + if (range == null) { + range = this._getRange(); + } + sel = this.app.window.getSelection(); + if (this.ctrl_a_pressed == null) { + sel.removeAllRanges(); + return sel.addRange(range); + } + }; + + EditableController.prototype._movingEvent = function(e) { + var ref; + return e.type === 'click' || ((ref = e.which) === KEY_CODE.RIGHT || ref === KEY_CODE.LEFT || ref === KEY_CODE.UP || ref === KEY_CODE.DOWN); + }; + + EditableController.prototype._unwrap = function(node) { + var next; + node = $(node).unwrap().get(0); + if ((next = node.nextSibling) && next.nodeValue) { + node.nodeValue += next.nodeValue; + $(next).remove(); + } + return node; + }; + + EditableController.prototype.catchQuery = function(e) { + var $inserted, $query, _range, index, inserted, isString, lastNode, matched, offset, query, query_content, range; + if (!(range = this._getRange())) { + return; + } + if (!range.collapsed) { + return; + } + if (e.which === KEY_CODE.ENTER) { + ($query = $(range.startContainer).closest('.atwho-query')).contents().unwrap(); + if ($query.is(':empty')) { + $query.remove(); + } + ($query = $(".atwho-query", this.app.document)).text($query.text()).contents().last().unwrap(); + this._clearRange(); + return; + } + if (/firefox/i.test(navigator.userAgent)) { + if ($(range.startContainer).is(this.$inputor)) { + this._clearRange(); + return; + } + if (e.which === KEY_CODE.BACKSPACE && range.startContainer.nodeType === document.ELEMENT_NODE && (offset = range.startOffset - 1) >= 0) { + _range = range.cloneRange(); + _range.setStart(range.startContainer, offset); + if ($(_range.cloneContents()).contents().last().is('.atwho-inserted')) { + inserted = $(range.startContainer).contents().get(offset); + this._setRange('after', $(inserted).contents().last()); + } + } else if (e.which === KEY_CODE.LEFT && range.startContainer.nodeType === document.TEXT_NODE) { + $inserted = $(range.startContainer.previousSibling); + if ($inserted.is('.atwho-inserted') && range.startOffset === 0) { + this._setRange('after', $inserted.contents().last()); + } + } + } + $(range.startContainer).closest('.atwho-inserted').addClass('atwho-query').siblings().removeClass('atwho-query'); + if (($query = $(".atwho-query", this.app.document)).length > 0 && $query.is(':empty') && $query.text().length === 0) { + $query.remove(); + } + if (!this._movingEvent(e)) { + $query.removeClass('atwho-inserted'); + } + if ($query.length > 0) { + switch (e.which) { + case KEY_CODE.LEFT: + this._setRange('before', $query.get(0), range); + $query.removeClass('atwho-query'); + return; + case KEY_CODE.RIGHT: + this._setRange('after', $query.get(0).nextSibling, range); + $query.removeClass('atwho-query'); + return; + } + } + if ($query.length > 0 && (query_content = $query.attr('data-atwho-at-query'))) { + $query.empty().html(query_content).attr('data-atwho-at-query', null); + this._setRange('after', $query.get(0), range); + } + _range = range.cloneRange(); + _range.setStart(range.startContainer, 0); + matched = this.callbacks("matcher").call(this, this.at, _range.toString(), this.getOpt('startWithSpace'), this.getOpt("acceptSpaceBar")); + isString = typeof matched === 'string'; + if ($query.length === 0 && isString && (index = range.startOffset - this.at.length - matched.length) >= 0) { + range.setStart(range.startContainer, index); + $query = $('', this.app.document).attr(this.getOpt("editableAtwhoQueryAttrs")).addClass('atwho-query'); + range.surroundContents($query.get(0)); + lastNode = $query.contents().last().get(0); + if (/firefox/i.test(navigator.userAgent)) { + range.setStart(lastNode, lastNode.length); + range.setEnd(lastNode, lastNode.length); + this._clearRange(range); + } else { + this._setRange('after', lastNode, range); + } + } + if (isString && matched.length < this.getOpt('minLen', 0)) { + return; + } + if (isString && matched.length <= this.getOpt('maxLen', 20)) { + query = { + text: matched, + el: $query + }; + this.trigger("matched", [this.at, query.text]); + return this.query = query; + } else { + this.view.hide(); + this.query = { + el: $query + }; + if ($query.text().indexOf(this.at) >= 0) { + if (this._movingEvent(e) && $query.hasClass('atwho-inserted')) { + $query.removeClass('atwho-query'); + } else if (false !== this.callbacks('afterMatchFailed').call(this, this.at, $query)) { + this._setRange("after", this._unwrap($query.text($query.text()).contents().first())); + } + } + return null; + } + }; + + EditableController.prototype.rect = function() { + var $iframe, iframeOffset, rect; + rect = this.query.el.offset(); + if (this.app.iframe && !this.app.iframeAsRoot) { + iframeOffset = ($iframe = $(this.app.iframe)).offset(); + rect.left += iframeOffset.left - this.$inputor.scrollLeft(); + rect.top += iframeOffset.top - this.$inputor.scrollTop(); + } + rect.bottom = rect.top + this.query.el.height(); + return rect; + }; + + EditableController.prototype.insert = function(content, $li) { + var data, range, suffix, suffixNode; + if (!this.$inputor.is(':focus')) { + this.$inputor.focus(); + } + suffix = (suffix = this.getOpt('suffix')) === "" ? suffix : suffix || "\u00A0"; + data = $li.data('item-data'); + this.query.el.removeClass('atwho-query').addClass('atwho-inserted').html(content).attr('data-atwho-at-query', "" + data['atwho-at'] + this.query.text); + if (range = this._getRange()) { + range.setEndAfter(this.query.el[0]); + range.collapse(false); + range.insertNode(suffixNode = this.app.document.createTextNode("\u200D" + suffix)); + this._setRange('after', suffixNode, range); + } + if (!this.$inputor.is(':focus')) { + this.$inputor.focus(); + } + return this.$inputor.change(); + }; + + return EditableController; + +})(Controller); + +var Model; + +Model = (function() { + function Model(context) { + this.context = context; + this.at = this.context.at; + this.storage = this.context.$inputor; + } + + Model.prototype.destroy = function() { + return this.storage.data(this.at, null); + }; + + Model.prototype.saved = function() { + return this.fetch() > 0; + }; + + Model.prototype.query = function(query, callback) { + var _remoteFilter, data, searchKey; + data = this.fetch(); + searchKey = this.context.getOpt("searchKey"); + data = this.context.callbacks('filter').call(this.context, query, data, searchKey) || []; + _remoteFilter = this.context.callbacks('remoteFilter'); + if (data.length > 0 || (!_remoteFilter && data.length === 0)) { + return callback(data); + } else { + return _remoteFilter.call(this.context, query, callback); + } + }; + + Model.prototype.fetch = function() { + return this.storage.data(this.at) || []; + }; + + Model.prototype.save = function(data) { + return this.storage.data(this.at, this.context.callbacks("beforeSave").call(this.context, data || [])); + }; + + Model.prototype.load = function(data) { + if (!(this.saved() || !data)) { + return this._load(data); + } + }; + + Model.prototype.reload = function(data) { + return this._load(data); + }; + + Model.prototype._load = function(data) { + if (typeof data === "string") { + return $.ajax(data, { + dataType: "json" + }).done((function(_this) { + return function(data) { + return _this.save(data); + }; + })(this)); + } else { + return this.save(data); + } + }; + + return Model; + +})(); + +var View; + +View = (function() { + function View(context) { + this.context = context; + this.$el = $("
          "); + this.$elUl = this.$el.children(); + this.timeoutID = null; + this.context.$el.append(this.$el); + this.bindEvent(); + } + + View.prototype.init = function() { + var header_tpl, id; + id = this.context.getOpt("alias") || this.context.at.charCodeAt(0); + header_tpl = this.context.getOpt("headerTpl"); + if (header_tpl && this.$el.children().length === 1) { + this.$el.prepend(header_tpl); + } + return this.$el.attr({ + 'id': "at-view-" + id + }); + }; + + View.prototype.destroy = function() { + return this.$el.remove(); + }; + + View.prototype.bindEvent = function() { + var $menu, lastCoordX, lastCoordY; + $menu = this.$el.find('ul'); + lastCoordX = 0; + lastCoordY = 0; + return $menu.on('mousemove.atwho-view', 'li', (function(_this) { + return function(e) { + var $cur; + if (lastCoordX === e.clientX && lastCoordY === e.clientY) { + return; + } + lastCoordX = e.clientX; + lastCoordY = e.clientY; + $cur = $(e.currentTarget); + if ($cur.hasClass('cur')) { + return; + } + $menu.find('.cur').removeClass('cur'); + return $cur.addClass('cur'); + }; + })(this)).on('click.atwho-view', 'li', (function(_this) { + return function(e) { + $menu.find('.cur').removeClass('cur'); + $(e.currentTarget).addClass('cur'); + _this.choose(e); + return e.preventDefault(); + }; + })(this)); + }; + + View.prototype.visible = function() { + return this.$el.is(":visible"); + }; + + View.prototype.highlighted = function() { + return this.$el.find(".cur").length > 0; + }; + + View.prototype.choose = function(e) { + var $li, content; + if (($li = this.$el.find(".cur")).length) { + content = this.context.insertContentFor($li); + this.context._stopDelayedCall(); + this.context.insert(this.context.callbacks("beforeInsert").call(this.context, content, $li, e), $li); + this.context.trigger("inserted", [$li, e]); + this.hide(e); + } + if (this.context.getOpt("hideWithoutSuffix")) { + return this.stopShowing = true; + } + }; + + View.prototype.reposition = function(rect) { + var _window, offset, overflowOffset, ref; + _window = this.context.app.iframeAsRoot ? this.context.app.window : window; + if (rect.bottom + this.$el.height() - $(_window).scrollTop() > $(_window).height()) { + rect.bottom = rect.top - this.$el.height(); + } + if (rect.left > (overflowOffset = $(_window).width() - this.$el.width() - 5)) { + rect.left = overflowOffset; + } + offset = { + left: rect.left, + top: rect.bottom + }; + if ((ref = this.context.callbacks("beforeReposition")) != null) { + ref.call(this.context, offset); + } + this.$el.offset(offset); + return this.context.trigger("reposition", [offset]); + }; + + View.prototype.next = function() { + var cur, next, nextEl, offset; + cur = this.$el.find('.cur').removeClass('cur'); + next = cur.next(); + if (!next.length) { + next = this.$el.find('li:first'); + } + next.addClass('cur'); + nextEl = next[0]; + offset = nextEl.offsetTop + nextEl.offsetHeight + (nextEl.nextSibling ? nextEl.nextSibling.offsetHeight : 0); + return this.scrollTop(Math.max(0, offset - this.$el.height())); + }; + + View.prototype.prev = function() { + var cur, offset, prev, prevEl; + cur = this.$el.find('.cur').removeClass('cur'); + prev = cur.prev(); + if (!prev.length) { + prev = this.$el.find('li:last'); + } + prev.addClass('cur'); + prevEl = prev[0]; + offset = prevEl.offsetTop + prevEl.offsetHeight + (prevEl.nextSibling ? prevEl.nextSibling.offsetHeight : 0); + return this.scrollTop(Math.max(0, offset - this.$el.height())); + }; + + View.prototype.scrollTop = function(scrollTop) { + var scrollDuration; + scrollDuration = this.context.getOpt('scrollDuration'); + if (scrollDuration) { + return this.$elUl.animate({ + scrollTop: scrollTop + }, scrollDuration); + } else { + return this.$elUl.scrollTop(scrollTop); + } + }; + + View.prototype.show = function() { + var rect; + if (this.stopShowing) { + this.stopShowing = false; + return; + } + if (!this.visible()) { + this.$el.show(); + this.$el.scrollTop(0); + this.context.trigger('shown'); + } + if (rect = this.context.rect()) { + return this.reposition(rect); + } + }; + + View.prototype.hide = function(e, time) { + var callback; + if (!this.visible()) { + return; + } + if (isNaN(time)) { + this.$el.hide(); + return this.context.trigger('hidden', [e]); + } else { + callback = (function(_this) { + return function() { + return _this.hide(); + }; + })(this); + clearTimeout(this.timeoutID); + return this.timeoutID = setTimeout(callback, time); + } + }; + + View.prototype.render = function(list) { + var $li, $ul, i, item, len, li, tpl; + if (!($.isArray(list) && list.length > 0)) { + this.hide(); + return; + } + this.$el.find('ul').empty(); + $ul = this.$el.find('ul'); + tpl = this.context.getOpt('displayTpl'); + for (i = 0, len = list.length; i < len; i++) { + item = list[i]; + item = $.extend({}, item, { + 'atwho-at': this.context.at + }); + li = this.context.callbacks("tplEval").call(this.context, tpl, item, "onDisplay"); + $li = $(this.context.callbacks("highlighter").call(this.context, li, this.context.query.text)); + $li.data("item-data", item); + $ul.append($li); + } + this.show(); + if (this.context.getOpt('highlightFirst')) { + return $ul.find("li:first").addClass("cur"); + } + }; + + return View; + +})(); + +var Api; + +Api = { + load: function(at, data) { + var c; + if (c = this.controller(at)) { + return c.model.load(data); + } + }, + isSelecting: function() { + var ref; + return !!((ref = this.controller()) != null ? ref.view.visible() : void 0); + }, + hide: function() { + var ref; + return (ref = this.controller()) != null ? ref.view.hide() : void 0; + }, + reposition: function() { + var c; + if (c = this.controller()) { + return c.view.reposition(c.rect()); + } + }, + setIframe: function(iframe, asRoot) { + this.setupRootElement(iframe, asRoot); + return null; + }, + run: function() { + return this.dispatch(); + }, + destroy: function() { + this.shutdown(); + return this.$inputor.data('atwho', null); + } +}; + +$.fn.atwho = function(method) { + var _args, result; + _args = arguments; + result = null; + this.filter('textarea, input, [contenteditable=""], [contenteditable=true]').each(function() { + var $this, app; + if (!(app = ($this = $(this)).data("atwho"))) { + $this.data('atwho', (app = new App(this))); + } + if (typeof method === 'object' || !method) { + return app.reg(method.at, method); + } else if (Api[method] && app) { + return result = Api[method].apply(app, Array.prototype.slice.call(_args, 1)); + } else { + return $.error("Method " + method + " does not exist on jQuery.atwho"); + } + }); + if (result != null) { + return result; + } else { + return this; + } +}; + +$.fn.atwho["default"] = { + at: void 0, + alias: void 0, + data: null, + displayTpl: "
        • ${name}
        • ", + insertTpl: "${atwho-at}${name}", + headerTpl: null, + callbacks: DEFAULT_CALLBACKS, + searchKey: "name", + suffix: void 0, + hideWithoutSuffix: false, + startWithSpace: true, + acceptSpaceBar: false, + highlightFirst: true, + limit: 5, + maxLen: 20, + minLen: 0, + displayTimeout: 300, + delay: null, + spaceSelectsMatch: false, + tabSelectsMatch: true, + editableAtwhoQueryAttrs: {}, + scrollDuration: 150, + suspendOnComposing: true, + lookUpOnClick: true +}; + +$.fn.atwho.debug = false; + +})); diff --git a/pagure/static/atwho/jquery.atwho-1.5.1.min.css b/pagure/static/atwho/jquery.atwho-1.5.1.min.css new file mode 100644 index 0000000..f770dc7 --- /dev/null +++ b/pagure/static/atwho/jquery.atwho-1.5.1.min.css @@ -0,0 +1 @@ +.atwho-view{position:absolute;top:0;left:0;display:none;margin-top:18px;background:#fff;color:#000;border:1px solid #DDD;border-radius:3px;box-shadow:0 0 5px rgba(0,0,0,.1);min-width:120px;z-index:11110!important}.atwho-view .atwho-header{padding:5px;margin:5px;cursor:pointer;border-bottom:solid 1px #eaeff1;color:#6f8092;font-size:11px;font-weight:700}.atwho-view .atwho-header .small{color:#6f8092;float:right;padding-top:2px;margin-right:-5px;font-size:12px;font-weight:400}.atwho-view .atwho-header:hover{cursor:default}.atwho-view .cur{background:#36F;color:#fff}.atwho-view .cur small{color:#fff}.atwho-view strong{color:#36F}.atwho-view .cur strong{color:#fff;font:700}.atwho-view ul{list-style:none;padding:0;margin:auto;max-height:200px;overflow-y:auto}.atwho-view ul li{display:block;padding:5px 10px;border-bottom:1px solid #DDD;cursor:pointer}.atwho-view small{font-size:smaller;color:#777;font-weight:400} \ No newline at end of file diff --git a/pagure/static/atwho/jquery.atwho-1.5.1.min.js b/pagure/static/atwho/jquery.atwho-1.5.1.min.js new file mode 100644 index 0000000..71e9a10 --- /dev/null +++ b/pagure/static/atwho/jquery.atwho-1.5.1.min.js @@ -0,0 +1 @@ +!function(t,e){"function"==typeof define&&define.amd?define(["jquery"],function(t){return e(t)}):"object"==typeof exports?module.exports=e(require("jquery")):e(jQuery)}(this,function(t){var e,i;i={DOWN:40,UP:38,ESC:27,TAB:9,ENTER:13,CTRL:17,A:65,P:80,N:78,LEFT:37,UP:38,RIGHT:39,DOWN:40,BACKSPACE:8,SPACE:32},e={beforeSave:function(t){return r.arrayToDefaultHash(t)},matcher:function(t,e,i,n){var r,o,s,a,h;return t=t.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g,"\\$&"),i&&(t="(?:^|\\s)"+t),r=decodeURI("%C3%80"),o=decodeURI("%C3%BF"),h=n?" ":"",a=new RegExp(t+"([A-Za-z"+r+"-"+o+"0-9_"+h+"'.+-]*)$|"+t+"([^\\x00-\\xff]*)$","gi"),s=a.exec(e),s?s[2]||s[1]:null},filter:function(t,e,i){var n,r,o,s;for(n=[],r=0,s=e.length;s>r;r++)o=e[r],~new String(o[i]).toLowerCase().indexOf(t.toLowerCase())&&n.push(o);return n},remoteFilter:null,sorter:function(t,e,i){var n,r,o,s;if(!t)return e;for(n=[],r=0,s=e.length;s>r;r++)o=e[r],o.atwho_order=new String(o[i]).toLowerCase().indexOf(t.toLowerCase()),o.atwho_order>-1&&n.push(o);return n.sort(function(t,e){return t.atwho_order-e.atwho_order})},tplEval:function(t,e){var i,n,r;r=t;try{return"string"!=typeof t&&(r=t(e)),r.replace(/\$\{([^\}]*)\}/g,function(t,i,n){return e[i]})}catch(n){return i=n,""}},highlighter:function(t,e){var i;return e?(i=new RegExp(">\\s*(\\w*?)("+e.replace("+","\\+")+")(\\w*)\\s*<","ig"),t.replace(i,function(t,e,i,n){return"> "+e+""+i+""+n+" <"})):t},beforeInsert:function(t,e,i){return t},beforeReposition:function(t){return t},afterMatchFailed:function(t,e){}};var n;n=function(){function e(e){this.currentFlag=null,this.controllers={},this.aliasMaps={},this.$inputor=t(e),this.setupRootElement(),this.listen()}return e.prototype.createContainer=function(e){var i;return null!=(i=this.$el)&&i.remove(),t(e.body).append(this.$el=t("
          "))},e.prototype.setupRootElement=function(e,i){var n,r;if(null==i&&(i=!1),e)this.window=e.contentWindow,this.document=e.contentDocument||this.window.document,this.iframe=e;else{this.document=this.$inputor[0].ownerDocument,this.window=this.document.defaultView||this.document.parentWindow;try{this.iframe=this.window.frameElement}catch(r){if(n=r,this.iframe=null,t.fn.atwho.debug)throw new Error("iframe auto-discovery is failed.\nPlease use `setIframe` to set the target iframe manually.\n"+n)}}return this.createContainer((this.iframeAsRoot=i)?this.document:document)},e.prototype.controller=function(t){var e,i,n,r;if(this.aliasMaps[t])i=this.controllers[this.aliasMaps[t]];else{r=this.controllers;for(n in r)if(e=r[n],n===t){i=e;break}}return i?i:this.controllers[this.currentFlag]},e.prototype.setContextFor=function(t){return this.currentFlag=t,this},e.prototype.reg=function(t,e){var i,n;return n=(i=this.controllers)[t]||(i[t]=this.$inputor.is("[contentEditable]")?new l(this,t):new s(this,t)),e.alias&&(this.aliasMaps[e.alias]=t),n.init(e),this},e.prototype.listen=function(){return this.$inputor.on("compositionstart",function(t){return function(e){var i;return null!=(i=t.controller())&&i.view.hide(),t.isComposing=!0,null}}(this)).on("compositionend",function(t){return function(e){return t.isComposing=!1,setTimeout(function(e){return t.dispatch(e)}),null}}(this)).on("keyup.atwhoInner",function(t){return function(e){return t.onKeyup(e)}}(this)).on("keydown.atwhoInner",function(t){return function(e){return t.onKeydown(e)}}(this)).on("blur.atwhoInner",function(t){return function(e){var i;return(i=t.controller())?(i.expectedQueryCBId=null,i.view.hide(e,i.getOpt("displayTimeout"))):void 0}}(this)).on("click.atwhoInner",function(t){return function(e){return t.dispatch(e)}}(this)).on("scroll.atwhoInner",function(t){return function(){var e;return e=t.$inputor.scrollTop(),function(i){var n,r;return n=i.target.scrollTop,e!==n&&null!=(r=t.controller())&&r.view.hide(i),e=n,!0}}}(this)())},e.prototype.shutdown=function(){var t,e,i;i=this.controllers;for(t in i)e=i[t],e.destroy(),delete this.controllers[t];return this.$inputor.off(".atwhoInner"),this.$el.remove()},e.prototype.dispatch=function(t){var e,i,n,r;n=this.controllers,r=[];for(e in n)i=n[e],r.push(i.lookUp(t));return r},e.prototype.onKeyup=function(e){var n;switch(e.keyCode){case i.ESC:e.preventDefault(),null!=(n=this.controller())&&n.view.hide();break;case i.DOWN:case i.UP:case i.CTRL:case i.ENTER:t.noop();break;case i.P:case i.N:e.ctrlKey||this.dispatch(e);break;default:this.dispatch(e)}},e.prototype.onKeydown=function(e){var n,r;if(r=null!=(n=this.controller())?n.view:void 0,r&&r.visible())switch(e.keyCode){case i.ESC:e.preventDefault(),r.hide(e);break;case i.UP:e.preventDefault(),r.prev();break;case i.DOWN:e.preventDefault(),r.next();break;case i.P:if(!e.ctrlKey)return;e.preventDefault(),r.prev();break;case i.N:if(!e.ctrlKey)return;e.preventDefault(),r.next();break;case i.TAB:case i.ENTER:case i.SPACE:if(!r.visible())return;if(!this.controller().getOpt("spaceSelectsMatch")&&e.keyCode===i.SPACE)return;if(!this.controller().getOpt("tabSelectsMatch")&&e.keyCode===i.TAB)return;r.highlighted()?(e.preventDefault(),r.choose(e)):r.hide(e);break;default:t.noop()}},e}();var r,o=[].slice;r=function(){function i(e,i){this.app=e,this.at=i,this.$inputor=this.app.$inputor,this.id=this.$inputor[0].id||this.uid(),this.expectedQueryCBId=null,this.setting=null,this.query=null,this.pos=0,this.range=null,0===(this.$el=t("#atwho-ground-"+this.id,this.app.$el)).length&&this.app.$el.append(this.$el=t("
          ")),this.model=new u(this),this.view=new c(this)}return i.prototype.uid=function(){return(Math.random().toString(16)+"000000000").substr(2,8)+(new Date).getTime()},i.prototype.init=function(e){return this.setting=t.extend({},this.setting||t.fn.atwho["default"],e),this.view.init(),this.model.reload(this.setting.data)},i.prototype.destroy=function(){return this.trigger("beforeDestroy"),this.model.destroy(),this.view.destroy(),this.$el.remove()},i.prototype.callDefault=function(){var i,n,r,s;s=arguments[0],i=2<=arguments.length?o.call(arguments,1):[];try{return e[s].apply(this,i)}catch(r){return n=r,t.error(n+" Or maybe At.js doesn't have function "+s)}},i.prototype.trigger=function(t,e){var i,n;return null==e&&(e=[]),e.push(this),i=this.getOpt("alias"),n=i?t+"-"+i+".atwho":t+".atwho",this.$inputor.trigger(n,e)},i.prototype.callbacks=function(t){return this.getOpt("callbacks")[t]||e[t]},i.prototype.getOpt=function(t,e){var i,n;try{return this.setting[t]}catch(n){return i=n,null}},i.prototype.insertContentFor=function(e){var i,n;return n=this.getOpt("insertTpl"),i=t.extend({},e.data("item-data"),{"atwho-at":this.at}),this.callbacks("tplEval").call(this,n,i,"onInsert")},i.prototype.renderView=function(t){var e;return e=this.getOpt("searchKey"),t=this.callbacks("sorter").call(this,this.query.text,t.slice(0,1001),e),this.view.render(t.slice(0,this.getOpt("limit")))},i.arrayToDefaultHash=function(e){var i,n,r,o;if(!t.isArray(e))return e;for(o=[],i=0,r=e.length;r>i;i++)n=e[i],t.isPlainObject(n)?o.push(n):o.push({name:n});return o},i.prototype.lookUp=function(t){var e,i;if((!t||"click"!==t.type||this.getOpt("lookUpOnClick"))&&(!this.getOpt("suspendOnComposing")||!this.app.isComposing))return(e=this.catchQuery(t))?(this.app.setContextFor(this.at),(i=this.getOpt("delay"))?this._delayLookUp(e,i):this._lookUp(e),e):(this.expectedQueryCBId=null,e)},i.prototype._delayLookUp=function(t,e){var i,n;return i=Date.now?Date.now():(new Date).getTime(),this.previousCallTime||(this.previousCallTime=i),n=e-(i-this.previousCallTime),n>0&&e>n?(this.previousCallTime=i,this._stopDelayedCall(),this.delayedCallTimeout=setTimeout(function(e){return function(){return e.previousCallTime=0,e.delayedCallTimeout=null,e._lookUp(t)}}(this),e)):(this._stopDelayedCall(),this.previousCallTime!==i&&(this.previousCallTime=0),this._lookUp(t))},i.prototype._stopDelayedCall=function(){return this.delayedCallTimeout?(clearTimeout(this.delayedCallTimeout),this.delayedCallTimeout=null):void 0},i.prototype._generateQueryCBId=function(){return{}},i.prototype._lookUp=function(e){var i;return i=function(t,e){return t===this.expectedQueryCBId?e&&e.length>0?this.renderView(this.constructor.arrayToDefaultHash(e)):this.view.hide():void 0},this.expectedQueryCBId=this._generateQueryCBId(),this.model.query(e.text,t.proxy(i,this,this.expectedQueryCBId))},i}();var s,a=function(t,e){function i(){this.constructor=t}for(var n in e)h.call(e,n)&&(t[n]=e[n]);return i.prototype=e.prototype,t.prototype=new i,t.__super__=e.prototype,t},h={}.hasOwnProperty;s=function(e){function i(){return i.__super__.constructor.apply(this,arguments)}return a(i,e),i.prototype.catchQuery=function(){var t,e,i,n,r,o,s;return e=this.$inputor.val(),t=this.$inputor.caret("pos",{iframe:this.app.iframe}),s=e.slice(0,t),r=this.callbacks("matcher").call(this,this.at,s,this.getOpt("startWithSpace"),this.getOpt("acceptSpaceBar")),n="string"==typeof r,n&&r.length0?t.getRangeAt(0):void 0},n.prototype._setRange=function(e,i,n){return null==n&&(n=this._getRange()),n?(i=t(i)[0],"after"===e?(n.setEndAfter(i),n.setStartAfter(i)):(n.setEndBefore(i),n.setStartBefore(i)),n.collapse(!1),this._clearRange(n)):void 0},n.prototype._clearRange=function(t){var e;return null==t&&(t=this._getRange()),e=this.app.window.getSelection(),null==this.ctrl_a_pressed?(e.removeAllRanges(),e.addRange(t)):void 0},n.prototype._movingEvent=function(t){var e;return"click"===t.type||(e=t.which)===i.RIGHT||e===i.LEFT||e===i.UP||e===i.DOWN},n.prototype._unwrap=function(e){var i;return e=t(e).unwrap().get(0),(i=e.nextSibling)&&i.nodeValue&&(e.nodeValue+=i.nodeValue,t(i).remove()),e},n.prototype.catchQuery=function(e){var n,r,o,s,a,h,l,u,c,p,f,d;if((d=this._getRange())&&d.collapsed){if(e.which===i.ENTER)return(r=t(d.startContainer).closest(".atwho-query")).contents().unwrap(),r.is(":empty")&&r.remove(),(r=t(".atwho-query",this.app.document)).text(r.text()).contents().last().unwrap(),void this._clearRange();if(/firefox/i.test(navigator.userAgent)){if(t(d.startContainer).is(this.$inputor))return void this._clearRange();e.which===i.BACKSPACE&&d.startContainer.nodeType===document.ELEMENT_NODE&&(c=d.startOffset-1)>=0?(o=d.cloneRange(),o.setStart(d.startContainer,c),t(o.cloneContents()).contents().last().is(".atwho-inserted")&&(a=t(d.startContainer).contents().get(c),this._setRange("after",t(a).contents().last()))):e.which===i.LEFT&&d.startContainer.nodeType===document.TEXT_NODE&&(n=t(d.startContainer.previousSibling),n.is(".atwho-inserted")&&0===d.startOffset&&this._setRange("after",n.contents().last()))}if(t(d.startContainer).closest(".atwho-inserted").addClass("atwho-query").siblings().removeClass("atwho-query"),(r=t(".atwho-query",this.app.document)).length>0&&r.is(":empty")&&0===r.text().length&&r.remove(),this._movingEvent(e)||r.removeClass("atwho-inserted"),r.length>0)switch(e.which){case i.LEFT:return this._setRange("before",r.get(0),d),void r.removeClass("atwho-query");case i.RIGHT:return this._setRange("after",r.get(0).nextSibling,d),void r.removeClass("atwho-query")}if(r.length>0&&(f=r.attr("data-atwho-at-query"))&&(r.empty().html(f).attr("data-atwho-at-query",null),this._setRange("after",r.get(0),d)),o=d.cloneRange(),o.setStart(d.startContainer,0),u=this.callbacks("matcher").call(this,this.at,o.toString(),this.getOpt("startWithSpace"),this.getOpt("acceptSpaceBar")),h="string"==typeof u,0===r.length&&h&&(s=d.startOffset-this.at.length-u.length)>=0&&(d.setStart(d.startContainer,s),r=t("",this.app.document).attr(this.getOpt("editableAtwhoQueryAttrs")).addClass("atwho-query"),d.surroundContents(r.get(0)),l=r.contents().last().get(0),/firefox/i.test(navigator.userAgent)?(d.setStart(l,l.length),d.setEnd(l,l.length),this._clearRange(d)):this._setRange("after",l,d)),!(h&&u.length=0&&(this._movingEvent(e)&&r.hasClass("atwho-inserted")?r.removeClass("atwho-query"):!1!==this.callbacks("afterMatchFailed").call(this,this.at,r)&&this._setRange("after",this._unwrap(r.text(r.text()).contents().first()))),null)}},n.prototype.rect=function(){var e,i,n;return n=this.query.el.offset(),this.app.iframe&&!this.app.iframeAsRoot&&(i=(e=t(this.app.iframe)).offset(),n.left+=i.left-this.$inputor.scrollLeft(),n.top+=i.top-this.$inputor.scrollTop()),n.bottom=n.top+this.query.el.height(),n},n.prototype.insert=function(t,e){var i,n,r,o;return this.$inputor.is(":focus")||this.$inputor.focus(),r=""===(r=this.getOpt("suffix"))?r:r||" ",i=e.data("item-data"),this.query.el.removeClass("atwho-query").addClass("atwho-inserted").html(t).attr("data-atwho-at-query",""+i["atwho-at"]+this.query.text),(n=this._getRange())&&(n.setEndAfter(this.query.el[0]),n.collapse(!1),n.insertNode(o=this.app.document.createTextNode("‍"+r)),this._setRange("after",o,n)),this.$inputor.is(":focus")||this.$inputor.focus(),this.$inputor.change()},n}(r);var u;u=function(){function e(t){this.context=t,this.at=this.context.at,this.storage=this.context.$inputor}return e.prototype.destroy=function(){return this.storage.data(this.at,null)},e.prototype.saved=function(){return this.fetch()>0},e.prototype.query=function(t,e){var i,n,r;return n=this.fetch(),r=this.context.getOpt("searchKey"),n=this.context.callbacks("filter").call(this.context,t,n,r)||[],i=this.context.callbacks("remoteFilter"),n.length>0||!i&&0===n.length?e(n):i.call(this.context,t,e)},e.prototype.fetch=function(){return this.storage.data(this.at)||[]},e.prototype.save=function(t){return this.storage.data(this.at,this.context.callbacks("beforeSave").call(this.context,t||[]))},e.prototype.load=function(t){return!this.saved()&&t?this._load(t):void 0},e.prototype.reload=function(t){return this._load(t)},e.prototype._load=function(e){return"string"==typeof e?t.ajax(e,{dataType:"json"}).done(function(t){return function(e){return t.save(e)}}(this)):this.save(e)},e}();var c;c=function(){function e(e){this.context=e,this.$el=t("
            "),this.$elUl=this.$el.children(),this.timeoutID=null,this.context.$el.append(this.$el),this.bindEvent()}return e.prototype.init=function(){var t,e;return e=this.context.getOpt("alias")||this.context.at.charCodeAt(0),t=this.context.getOpt("headerTpl"),t&&1===this.$el.children().length&&this.$el.prepend(t),this.$el.attr({id:"at-view-"+e})},e.prototype.destroy=function(){return this.$el.remove()},e.prototype.bindEvent=function(){var e,i,n;return e=this.$el.find("ul"),i=0,n=0,e.on("mousemove.atwho-view","li",function(r){return function(r){var o;if((i!==r.clientX||n!==r.clientY)&&(i=r.clientX,n=r.clientY,o=t(r.currentTarget),!o.hasClass("cur")))return e.find(".cur").removeClass("cur"),o.addClass("cur")}}(this)).on("click.atwho-view","li",function(i){return function(n){return e.find(".cur").removeClass("cur"),t(n.currentTarget).addClass("cur"),i.choose(n),n.preventDefault()}}(this))},e.prototype.visible=function(){return this.$el.is(":visible")},e.prototype.highlighted=function(){return this.$el.find(".cur").length>0},e.prototype.choose=function(t){var e,i;return(e=this.$el.find(".cur")).length&&(i=this.context.insertContentFor(e),this.context._stopDelayedCall(),this.context.insert(this.context.callbacks("beforeInsert").call(this.context,i,e,t),e),this.context.trigger("inserted",[e,t]),this.hide(t)),this.context.getOpt("hideWithoutSuffix")?this.stopShowing=!0:void 0},e.prototype.reposition=function(e){var i,n,r,o;return i=this.context.app.iframeAsRoot?this.context.app.window:window,e.bottom+this.$el.height()-t(i).scrollTop()>t(i).height()&&(e.bottom=e.top-this.$el.height()),e.left>(r=t(i).width()-this.$el.width()-5)&&(e.left=r),n={left:e.left,top:e.bottom},null!=(o=this.context.callbacks("beforeReposition"))&&o.call(this.context,n),this.$el.offset(n),this.context.trigger("reposition",[n])},e.prototype.next=function(){var t,e,i,n;return t=this.$el.find(".cur").removeClass("cur"),e=t.next(),e.length||(e=this.$el.find("li:first")),e.addClass("cur"),i=e[0],n=i.offsetTop+i.offsetHeight+(i.nextSibling?i.nextSibling.offsetHeight:0),this.scrollTop(Math.max(0,n-this.$el.height()))},e.prototype.prev=function(){var t,e,i,n;return t=this.$el.find(".cur").removeClass("cur"),i=t.prev(),i.length||(i=this.$el.find("li:last")),i.addClass("cur"),n=i[0],e=n.offsetTop+n.offsetHeight+(n.nextSibling?n.nextSibling.offsetHeight:0),this.scrollTop(Math.max(0,e-this.$el.height()))},e.prototype.scrollTop=function(t){var e;return e=this.context.getOpt("scrollDuration"),e?this.$elUl.animate({scrollTop:t},e):this.$elUl.scrollTop(t)},e.prototype.show=function(){var t;return this.stopShowing?void(this.stopShowing=!1):(this.visible()||(this.$el.show(),this.$el.scrollTop(0),this.context.trigger("shown")),(t=this.context.rect())?this.reposition(t):void 0)},e.prototype.hide=function(t,e){var i;if(this.visible())return isNaN(e)?(this.$el.hide(),this.context.trigger("hidden",[t])):(i=function(t){return function(){return t.hide()}}(this),clearTimeout(this.timeoutID),this.timeoutID=setTimeout(i,e))},e.prototype.render=function(e){var i,n,r,o,s,a,h;if(!(t.isArray(e)&&e.length>0))return void this.hide();for(this.$el.find("ul").empty(),n=this.$el.find("ul"),h=this.context.getOpt("displayTpl"),r=0,s=e.length;s>r;r++)o=e[r],o=t.extend({},o,{"atwho-at":this.context.at}),a=this.context.callbacks("tplEval").call(this.context,h,o,"onDisplay"),i=t(this.context.callbacks("highlighter").call(this.context,a,this.context.query.text)),i.data("item-data",o),n.append(i);return this.show(),this.context.getOpt("highlightFirst")?n.find("li:first").addClass("cur"):void 0},e}();var p;p={load:function(t,e){var i;return(i=this.controller(t))?i.model.load(e):void 0},isSelecting:function(){var t;return!!(null!=(t=this.controller())?t.view.visible():void 0)},hide:function(){var t;return null!=(t=this.controller())?t.view.hide():void 0},reposition:function(){var t;return(t=this.controller())?t.view.reposition(t.rect()):void 0},setIframe:function(t,e){return this.setupRootElement(t,e),null},run:function(){return this.dispatch()},destroy:function(){return this.shutdown(),this.$inputor.data("atwho",null)}},t.fn.atwho=function(e){var i,r;return i=arguments,r=null,this.filter('textarea, input, [contenteditable=""], [contenteditable=true]').each(function(){var o,s;return(s=(o=t(this)).data("atwho"))||o.data("atwho",s=new n(this)),"object"!=typeof e&&e?p[e]&&s?r=p[e].apply(s,Array.prototype.slice.call(i,1)):t.error("Method "+e+" does not exist on jQuery.atwho"):s.reg(e.at,e)}),null!=r?r:this},t.fn.atwho["default"]={at:void 0,alias:void 0,data:null,displayTpl:"
          • ${name}
          • ",insertTpl:"${atwho-at}${name}",headerTpl:null,callbacks:e,searchKey:"name",suffix:void 0,hideWithoutSuffix:!1,startWithSpace:!0,acceptSpaceBar:!1,highlightFirst:!0,limit:5,maxLen:20,minLen:0,displayTimeout:300,delay:null,spaceSelectsMatch:!1,tabSelectsMatch:!0,editableAtwhoQueryAttrs:{},scrollDuration:150,suspendOnComposing:!0,lookUpOnClick:!0},t.fn.atwho.debug=!1}); \ No newline at end of file diff --git a/pagure/static/atwho/jquery.atwho.css b/pagure/static/atwho/jquery.atwho.css index 8d3cc6d..7cdb23b 120000 --- a/pagure/static/atwho/jquery.atwho.css +++ b/pagure/static/atwho/jquery.atwho.css @@ -1 +1 @@ -jquery.atwho-1.4.1.css \ No newline at end of file +jquery.atwho-1.5.1.css \ No newline at end of file diff --git a/pagure/static/atwho/jquery.atwho.js b/pagure/static/atwho/jquery.atwho.js index 6404683..71094fd 120000 --- a/pagure/static/atwho/jquery.atwho.js +++ b/pagure/static/atwho/jquery.atwho.js @@ -1 +1 @@ -jquery.atwho-1.4.1.js \ No newline at end of file +jquery.atwho-1.5.1.js \ No newline at end of file diff --git a/pagure/static/atwho/jquery.atwho.min.css b/pagure/static/atwho/jquery.atwho.min.css index 564d968..9f0145a 120000 --- a/pagure/static/atwho/jquery.atwho.min.css +++ b/pagure/static/atwho/jquery.atwho.min.css @@ -1 +1 @@ -jquery.atwho-1.4.1.min.css \ No newline at end of file +jquery.atwho-1.5.1.min.css \ No newline at end of file diff --git a/pagure/static/atwho/jquery.atwho.min.js b/pagure/static/atwho/jquery.atwho.min.js index 168ae58..e1879fd 120000 --- a/pagure/static/atwho/jquery.atwho.min.js +++ b/pagure/static/atwho/jquery.atwho.min.js @@ -1 +1 @@ -jquery.atwho-1.4.1.min.js \ No newline at end of file +jquery.atwho-1.5.1.min.js \ No newline at end of file diff --git a/pagure/static/atwho/jquery.caret-0.3.1.js b/pagure/static/atwho/jquery.caret-0.3.1.js new file mode 100644 index 0000000..811ec63 --- /dev/null +++ b/pagure/static/atwho/jquery.caret-0.3.1.js @@ -0,0 +1,436 @@ +(function (root, factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define(["jquery"], function ($) { + return (root.returnExportsGlobal = factory($)); + }); + } else if (typeof exports === 'object') { + // Node. Does not work with strict CommonJS, but + // only CommonJS-like enviroments that support module.exports, + // like Node. + module.exports = factory(require("jquery")); + } else { + factory(jQuery); + } +}(this, function ($) { + +/* + Implement Github like autocomplete mentions + http://ichord.github.com/At.js + + Copyright (c) 2013 chord.luo@gmail.com + Licensed under the MIT license. +*/ + +/* +本插件操作 textarea 或者 input 内的插入符 +只实现了获得插入符在文本框中的位置,我设置 +插入符的位置. +*/ + +"use strict"; +var EditableCaret, InputCaret, Mirror, Utils, discoveryIframeOf, methods, oDocument, oFrame, oWindow, pluginName, setContextBy; + +pluginName = 'caret'; + +EditableCaret = (function() { + function EditableCaret($inputor) { + this.$inputor = $inputor; + this.domInputor = this.$inputor[0]; + } + + EditableCaret.prototype.setPos = function(pos) { + var fn, found, offset, sel; + if (sel = oWindow.getSelection()) { + offset = 0; + found = false; + (fn = function(pos, parent) { + var node, range, _i, _len, _ref, _results; + _ref = parent.childNodes; + _results = []; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + node = _ref[_i]; + if (found) { + break; + } + if (node.nodeType === 3) { + if (offset + node.length >= pos) { + found = true; + range = oDocument.createRange(); + range.setStart(node, pos - offset); + sel.removeAllRanges(); + sel.addRange(range); + break; + } else { + _results.push(offset += node.length); + } + } else { + _results.push(fn(pos, node)); + } + } + return _results; + })(pos, this.domInputor); + } + return this.domInputor; + }; + + EditableCaret.prototype.getIEPosition = function() { + return this.getPosition(); + }; + + EditableCaret.prototype.getPosition = function() { + var inputor_offset, offset; + offset = this.getOffset(); + inputor_offset = this.$inputor.offset(); + offset.left -= inputor_offset.left; + offset.top -= inputor_offset.top; + return offset; + }; + + EditableCaret.prototype.getOldIEPos = function() { + var preCaretTextRange, textRange; + textRange = oDocument.selection.createRange(); + preCaretTextRange = oDocument.body.createTextRange(); + preCaretTextRange.moveToElementText(this.domInputor); + preCaretTextRange.setEndPoint("EndToEnd", textRange); + return preCaretTextRange.text.length; + }; + + EditableCaret.prototype.getPos = function() { + var clonedRange, pos, range; + if (range = this.range()) { + clonedRange = range.cloneRange(); + clonedRange.selectNodeContents(this.domInputor); + clonedRange.setEnd(range.endContainer, range.endOffset); + pos = clonedRange.toString().length; + clonedRange.detach(); + return pos; + } else if (oDocument.selection) { + return this.getOldIEPos(); + } + }; + + EditableCaret.prototype.getOldIEOffset = function() { + var range, rect; + range = oDocument.selection.createRange().duplicate(); + range.moveStart("character", -1); + rect = range.getBoundingClientRect(); + return { + height: rect.bottom - rect.top, + left: rect.left, + top: rect.top + }; + }; + + EditableCaret.prototype.getOffset = function(pos) { + var clonedRange, offset, range, rect, shadowCaret; + if (oWindow.getSelection && (range = this.range())) { + if (range.endOffset - 1 > 0 && range.endContainer !== this.domInputor) { + clonedRange = range.cloneRange(); + clonedRange.setStart(range.endContainer, range.endOffset - 1); + clonedRange.setEnd(range.endContainer, range.endOffset); + rect = clonedRange.getBoundingClientRect(); + offset = { + height: rect.height, + left: rect.left + rect.width, + top: rect.top + }; + clonedRange.detach(); + } + if (!offset || (offset != null ? offset.height : void 0) === 0) { + clonedRange = range.cloneRange(); + shadowCaret = $(oDocument.createTextNode("|")); + clonedRange.insertNode(shadowCaret[0]); + clonedRange.selectNode(shadowCaret[0]); + rect = clonedRange.getBoundingClientRect(); + offset = { + height: rect.height, + left: rect.left, + top: rect.top + }; + shadowCaret.remove(); + clonedRange.detach(); + } + } else if (oDocument.selection) { + offset = this.getOldIEOffset(); + } + if (offset) { + offset.top += $(oWindow).scrollTop(); + offset.left += $(oWindow).scrollLeft(); + } + return offset; + }; + + EditableCaret.prototype.range = function() { + var sel; + if (!oWindow.getSelection) { + return; + } + sel = oWindow.getSelection(); + if (sel.rangeCount > 0) { + return sel.getRangeAt(0); + } else { + return null; + } + }; + + return EditableCaret; + +})(); + +InputCaret = (function() { + function InputCaret($inputor) { + this.$inputor = $inputor; + this.domInputor = this.$inputor[0]; + } + + InputCaret.prototype.getIEPos = function() { + var endRange, inputor, len, normalizedValue, pos, range, textInputRange; + inputor = this.domInputor; + range = oDocument.selection.createRange(); + pos = 0; + if (range && range.parentElement() === inputor) { + normalizedValue = inputor.value.replace(/\r\n/g, "\n"); + len = normalizedValue.length; + textInputRange = inputor.createTextRange(); + textInputRange.moveToBookmark(range.getBookmark()); + endRange = inputor.createTextRange(); + endRange.collapse(false); + if (textInputRange.compareEndPoints("StartToEnd", endRange) > -1) { + pos = len; + } else { + pos = -textInputRange.moveStart("character", -len); + } + } + return pos; + }; + + InputCaret.prototype.getPos = function() { + if (oDocument.selection) { + return this.getIEPos(); + } else { + return this.domInputor.selectionStart; + } + }; + + InputCaret.prototype.setPos = function(pos) { + var inputor, range; + inputor = this.domInputor; + if (oDocument.selection) { + range = inputor.createTextRange(); + range.move("character", pos); + range.select(); + } else if (inputor.setSelectionRange) { + inputor.setSelectionRange(pos, pos); + } + return inputor; + }; + + InputCaret.prototype.getIEOffset = function(pos) { + var h, textRange, x, y; + textRange = this.domInputor.createTextRange(); + pos || (pos = this.getPos()); + textRange.move('character', pos); + x = textRange.boundingLeft; + y = textRange.boundingTop; + h = textRange.boundingHeight; + return { + left: x, + top: y, + height: h + }; + }; + + InputCaret.prototype.getOffset = function(pos) { + var $inputor, offset, position; + $inputor = this.$inputor; + if (oDocument.selection) { + offset = this.getIEOffset(pos); + offset.top += $(oWindow).scrollTop() + $inputor.scrollTop(); + offset.left += $(oWindow).scrollLeft() + $inputor.scrollLeft(); + return offset; + } else { + offset = $inputor.offset(); + position = this.getPosition(pos); + return offset = { + left: offset.left + position.left - $inputor.scrollLeft(), + top: offset.top + position.top - $inputor.scrollTop(), + height: position.height + }; + } + }; + + InputCaret.prototype.getPosition = function(pos) { + var $inputor, at_rect, end_range, format, html, mirror, start_range; + $inputor = this.$inputor; + format = function(value) { + value = value.replace(/<|>|`|"|&/g, '?').replace(/\r\n|\r|\n/g, "
            "); + if (/firefox/i.test(navigator.userAgent)) { + value = value.replace(/\s/g, ' '); + } + return value; + }; + if (pos === void 0) { + pos = this.getPos(); + } + start_range = $inputor.val().slice(0, pos); + end_range = $inputor.val().slice(pos); + html = "" + format(start_range) + ""; + html += "|"; + html += "" + format(end_range) + ""; + mirror = new Mirror($inputor); + return at_rect = mirror.create(html).rect(); + }; + + InputCaret.prototype.getIEPosition = function(pos) { + var h, inputorOffset, offset, x, y; + offset = this.getIEOffset(pos); + inputorOffset = this.$inputor.offset(); + x = offset.left - inputorOffset.left; + y = offset.top - inputorOffset.top; + h = offset.height; + return { + left: x, + top: y, + height: h + }; + }; + + return InputCaret; + +})(); + +Mirror = (function() { + Mirror.prototype.css_attr = ["borderBottomWidth", "borderLeftWidth", "borderRightWidth", "borderTopStyle", "borderRightStyle", "borderBottomStyle", "borderLeftStyle", "borderTopWidth", "boxSizing", "fontFamily", "fontSize", "fontWeight", "height", "letterSpacing", "lineHeight", "marginBottom", "marginLeft", "marginRight", "marginTop", "outlineWidth", "overflow", "overflowX", "overflowY", "paddingBottom", "paddingLeft", "paddingRight", "paddingTop", "textAlign", "textOverflow", "textTransform", "whiteSpace", "wordBreak", "wordWrap"]; + + function Mirror($inputor) { + this.$inputor = $inputor; + } + + Mirror.prototype.mirrorCss = function() { + var css, + _this = this; + css = { + position: 'absolute', + left: -9999, + top: 0, + zIndex: -20000 + }; + if (this.$inputor.prop('tagName') === 'TEXTAREA') { + this.css_attr.push('width'); + } + $.each(this.css_attr, function(i, p) { + return css[p] = _this.$inputor.css(p); + }); + return css; + }; + + Mirror.prototype.create = function(html) { + this.$mirror = $('
            '); + this.$mirror.css(this.mirrorCss()); + this.$mirror.html(html); + this.$inputor.after(this.$mirror); + return this; + }; + + Mirror.prototype.rect = function() { + var $flag, pos, rect; + $flag = this.$mirror.find("#caret"); + pos = $flag.position(); + rect = { + left: pos.left, + top: pos.top, + height: $flag.height() + }; + this.$mirror.remove(); + return rect; + }; + + return Mirror; + +})(); + +Utils = { + contentEditable: function($inputor) { + return !!($inputor[0].contentEditable && $inputor[0].contentEditable === 'true'); + } +}; + +methods = { + pos: function(pos) { + if (pos || pos === 0) { + return this.setPos(pos); + } else { + return this.getPos(); + } + }, + position: function(pos) { + if (oDocument.selection) { + return this.getIEPosition(pos); + } else { + return this.getPosition(pos); + } + }, + offset: function(pos) { + var offset; + offset = this.getOffset(pos); + return offset; + } +}; + +oDocument = null; + +oWindow = null; + +oFrame = null; + +setContextBy = function(settings) { + var iframe; + if (iframe = settings != null ? settings.iframe : void 0) { + oFrame = iframe; + oWindow = iframe.contentWindow; + return oDocument = iframe.contentDocument || oWindow.document; + } else { + oFrame = void 0; + oWindow = window; + return oDocument = document; + } +}; + +discoveryIframeOf = function($dom) { + var error; + oDocument = $dom[0].ownerDocument; + oWindow = oDocument.defaultView || oDocument.parentWindow; + try { + return oFrame = oWindow.frameElement; + } catch (_error) { + error = _error; + } +}; + +$.fn.caret = function(method, value, settings) { + var caret; + if (methods[method]) { + if ($.isPlainObject(value)) { + setContextBy(value); + value = void 0; + } else { + setContextBy(settings); + } + caret = Utils.contentEditable(this) ? new EditableCaret(this) : new InputCaret(this); + return methods[method].apply(caret, [value]); + } else { + return $.error("Method " + method + " does not exist on jQuery.caret"); + } +}; + +$.fn.caret.EditableCaret = EditableCaret; + +$.fn.caret.InputCaret = InputCaret; + +$.fn.caret.Utils = Utils; + +$.fn.caret.apis = methods; + + +})); diff --git a/pagure/static/atwho/jquery.caret-0.3.1.min.js b/pagure/static/atwho/jquery.caret-0.3.1.min.js new file mode 100644 index 0000000..a25584e --- /dev/null +++ b/pagure/static/atwho/jquery.caret-0.3.1.min.js @@ -0,0 +1,2 @@ +/*! jquery.caret 2016-02-27 */ +!function(a,b){"function"==typeof define&&define.amd?define(["jquery"],function(c){return a.returnExportsGlobal=b(c)}):"object"==typeof exports?module.exports=b(require("jquery")):b(jQuery)}(this,function(a){"use strict";var b,c,d,e,f,g,h,i,j,k,l;k="caret",b=function(){function b(a){this.$inputor=a,this.domInputor=this.$inputor[0]}return b.prototype.setPos=function(a){var b,c,d,e;return(e=j.getSelection())&&(d=0,c=!1,(b=function(a,f){var g,i,j,k,l,m;for(l=f.childNodes,m=[],j=0,k=l.length;k>j&&(g=l[j],!c);j++)if(3===g.nodeType){if(d+g.length>=a){c=!0,i=h.createRange(),i.setStart(g,a-d),e.removeAllRanges(),e.addRange(i);break}m.push(d+=g.length)}else m.push(b(a,g));return m})(a,this.domInputor)),this.domInputor},b.prototype.getIEPosition=function(){return this.getPosition()},b.prototype.getPosition=function(){var a,b;return b=this.getOffset(),a=this.$inputor.offset(),b.left-=a.left,b.top-=a.top,b},b.prototype.getOldIEPos=function(){var a,b;return b=h.selection.createRange(),a=h.body.createTextRange(),a.moveToElementText(this.domInputor),a.setEndPoint("EndToEnd",b),a.text.length},b.prototype.getPos=function(){var a,b,c;return(c=this.range())?(a=c.cloneRange(),a.selectNodeContents(this.domInputor),a.setEnd(c.endContainer,c.endOffset),b=a.toString().length,a.detach(),b):h.selection?this.getOldIEPos():void 0},b.prototype.getOldIEOffset=function(){var a,b;return a=h.selection.createRange().duplicate(),a.moveStart("character",-1),b=a.getBoundingClientRect(),{height:b.bottom-b.top,left:b.left,top:b.top}},b.prototype.getOffset=function(){var b,c,d,e,f;return j.getSelection&&(d=this.range())?(d.endOffset-1>0&&d.endContainer!==this.domInputor&&(b=d.cloneRange(),b.setStart(d.endContainer,d.endOffset-1),b.setEnd(d.endContainer,d.endOffset),e=b.getBoundingClientRect(),c={height:e.height,left:e.left+e.width,top:e.top},b.detach()),c&&0!==(null!=c?c.height:void 0)||(b=d.cloneRange(),f=a(h.createTextNode("|")),b.insertNode(f[0]),b.selectNode(f[0]),e=b.getBoundingClientRect(),c={height:e.height,left:e.left,top:e.top},f.remove(),b.detach())):h.selection&&(c=this.getOldIEOffset()),c&&(c.top+=a(j).scrollTop(),c.left+=a(j).scrollLeft()),c},b.prototype.range=function(){var a;if(j.getSelection)return a=j.getSelection(),a.rangeCount>0?a.getRangeAt(0):null},b}(),c=function(){function b(a){this.$inputor=a,this.domInputor=this.$inputor[0]}return b.prototype.getIEPos=function(){var a,b,c,d,e,f,g;return b=this.domInputor,f=h.selection.createRange(),e=0,f&&f.parentElement()===b&&(d=b.value.replace(/\r\n/g,"\n"),c=d.length,g=b.createTextRange(),g.moveToBookmark(f.getBookmark()),a=b.createTextRange(),a.collapse(!1),e=g.compareEndPoints("StartToEnd",a)>-1?c:-g.moveStart("character",-c)),e},b.prototype.getPos=function(){return h.selection?this.getIEPos():this.domInputor.selectionStart},b.prototype.setPos=function(a){var b,c;return b=this.domInputor,h.selection?(c=b.createTextRange(),c.move("character",a),c.select()):b.setSelectionRange&&b.setSelectionRange(a,a),b},b.prototype.getIEOffset=function(a){var b,c,d,e;return c=this.domInputor.createTextRange(),a||(a=this.getPos()),c.move("character",a),d=c.boundingLeft,e=c.boundingTop,b=c.boundingHeight,{left:d,top:e,height:b}},b.prototype.getOffset=function(b){var c,d,e;return c=this.$inputor,h.selection?(d=this.getIEOffset(b),d.top+=a(j).scrollTop()+c.scrollTop(),d.left+=a(j).scrollLeft()+c.scrollLeft(),d):(d=c.offset(),e=this.getPosition(b),d={left:d.left+e.left-c.scrollLeft(),top:d.top+e.top-c.scrollTop(),height:e.height})},b.prototype.getPosition=function(a){var b,c,e,f,g,h,i;return b=this.$inputor,f=function(a){return a=a.replace(/<|>|`|"|&/g,"?").replace(/\r\n|\r|\n/g,"
            "),/firefox/i.test(navigator.userAgent)&&(a=a.replace(/\s/g," ")),a},void 0===a&&(a=this.getPos()),i=b.val().slice(0,a),e=b.val().slice(a),g=""+f(i)+"",g+="|",g+=""+f(e)+"",h=new d(b),c=h.create(g).rect()},b.prototype.getIEPosition=function(a){var b,c,d,e,f;return d=this.getIEOffset(a),c=this.$inputor.offset(),e=d.left-c.left,f=d.top-c.top,b=d.height,{left:e,top:f,height:b}},b}(),d=function(){function b(a){this.$inputor=a}return b.prototype.css_attr=["borderBottomWidth","borderLeftWidth","borderRightWidth","borderTopStyle","borderRightStyle","borderBottomStyle","borderLeftStyle","borderTopWidth","boxSizing","fontFamily","fontSize","fontWeight","height","letterSpacing","lineHeight","marginBottom","marginLeft","marginRight","marginTop","outlineWidth","overflow","overflowX","overflowY","paddingBottom","paddingLeft","paddingRight","paddingTop","textAlign","textOverflow","textTransform","whiteSpace","wordBreak","wordWrap"],b.prototype.mirrorCss=function(){var b,c=this;return b={position:"absolute",left:-9999,top:0,zIndex:-2e4},"TEXTAREA"===this.$inputor.prop("tagName")&&this.css_attr.push("width"),a.each(this.css_attr,function(a,d){return b[d]=c.$inputor.css(d)}),b},b.prototype.create=function(b){return this.$mirror=a("
            "),this.$mirror.css(this.mirrorCss()),this.$mirror.html(b),this.$inputor.after(this.$mirror),this},b.prototype.rect=function(){var a,b,c;return a=this.$mirror.find("#caret"),b=a.position(),c={left:b.left,top:b.top,height:a.height()},this.$mirror.remove(),c},b}(),e={contentEditable:function(a){return!(!a[0].contentEditable||"true"!==a[0].contentEditable)}},g={pos:function(a){return a||0===a?this.setPos(a):this.getPos()},position:function(a){return h.selection?this.getIEPosition(a):this.getPosition(a)},offset:function(a){var b;return b=this.getOffset(a)}},h=null,j=null,i=null,l=function(a){var b;return(b=null!=a?a.iframe:void 0)?(i=b,j=b.contentWindow,h=b.contentDocument||j.document):(i=void 0,j=window,h=document)},f=function(a){var b;h=a[0].ownerDocument,j=h.defaultView||h.parentWindow;try{return i=j.frameElement}catch(c){b=c}},a.fn.caret=function(d,f,h){var i;return g[d]?(a.isPlainObject(f)?(l(f),f=void 0):l(h),i=e.contentEditable(this)?new b(this):new c(this),g[d].apply(i,[f])):a.error("Method "+d+" does not exist on jQuery.caret")},a.fn.caret.EditableCaret=b,a.fn.caret.InputCaret=c,a.fn.caret.Utils=e,a.fn.caret.apis=g}); \ No newline at end of file diff --git a/pagure/static/atwho/jquery.caret-1.5.2.js b/pagure/static/atwho/jquery.caret-1.5.2.js deleted file mode 100644 index c29f029..0000000 --- a/pagure/static/atwho/jquery.caret-1.5.2.js +++ /dev/null @@ -1,543 +0,0 @@ -/*! jQuery Caret Plugin - v1.5.2 - 2014-03-25 - * https://github.com/acdvorak/jquery.caret - * Copyright (c) 2012-2014 Andrew C. Dvorak; Licensed MIT */ -(function($, undefined) { - - var _input = document.createElement('input'); - - var _support = { - setSelectionRange: ('setSelectionRange' in _input) || ('selectionStart' in _input), - createTextRange: ('createTextRange' in _input) || ('selection' in document) - }; - - var _rNewlineIE = /\r\n/g, - _rCarriageReturn = /\r/g; - - var _getValue = function(input) { - if (typeof(input.value) !== 'undefined') { - return input.value; - } - return $(input).text(); - }; - - var _setValue = function(input, value) { - if (typeof(input.value) !== 'undefined') { - input.value = value; - } else { - $(input).text(value); - } - }; - - var _getIndex = function(input, pos) { - var norm = _getValue(input).replace(_rCarriageReturn, ''); - var len = norm.length; - - if (typeof(pos) === 'undefined') { - pos = len; - } - - pos = Math.floor(pos); - - // Negative index counts backward from the end of the input/textarea's value - if (pos < 0) { - pos = len + pos; - } - - // Enforce boundaries - if (pos < 0) { pos = 0; } - if (pos > len) { pos = len; } - - return pos; - }; - - var _hasAttr = function(input, attrName) { - return input.hasAttribute ? input.hasAttribute(attrName) : (typeof(input[attrName]) !== 'undefined'); - }; - - /** - * @class - * @constructor - */ - var Range = function(start, end, length, text) { - this.start = start || 0; - this.end = end || 0; - this.length = length || 0; - this.text = text || ''; - }; - - Range.prototype.toString = function() { - return JSON.stringify(this, null, ' '); - }; - - var _getCaretW3 = function(input) { - return input.selectionStart; - }; - - /** - * @see http://stackoverflow.com/q/6943000/467582 - */ - var _getCaretIE = function(input) { - var caret, range, textInputRange, rawValue, len, endRange; - - // Yeah, you have to focus twice for IE 7 and 8. *cries* - input.focus(); - input.focus(); - - range = document.selection.createRange(); - - if (range && range.parentElement() === input) { - rawValue = _getValue(input); - - len = rawValue.length; - - // Create a working TextRange that lives only in the input - textInputRange = input.createTextRange(); - textInputRange.moveToBookmark(range.getBookmark()); - - // Check if the start and end of the selection are at the very end - // of the input, since moveStart/moveEnd doesn't return what we want - // in those cases - endRange = input.createTextRange(); - endRange.collapse(false); - - if (textInputRange.compareEndPoints("StartToEnd", endRange) > -1) { - caret = rawValue.replace(_rNewlineIE, '\n').length; - } else { - caret = -textInputRange.moveStart("character", -len); - } - - return caret; - } - - // NOTE: This occurs when you highlight part of a textarea and then click in the middle of the highlighted portion in IE 6-10. - // There doesn't appear to be anything we can do about it. -// alert("Your browser is incredibly stupid. I don't know what else to say."); -// alert(range + '\n\n' + range.parentElement().tagName + '#' + range.parentElement().id); - - return 0; - }; - - /** - * Gets the position of the caret in the given input. - * @param {HTMLInputElement|HTMLTextAreaElement} input input or textarea element - * @returns {Number} - * @see http://stackoverflow.com/questions/263743/how-to-get-cursor-position-in-textarea/263796#263796 - */ - var _getCaret = function(input) { - if (!input) { - return undefined; - } - - // Mozilla, et al. - if (_support.setSelectionRange) { - return _getCaretW3(input); - } - // IE - else if (_support.createTextRange) { - return _getCaretIE(input); - } - - return undefined; - }; - - var _setCaretW3 = function(input, pos) { - input.setSelectionRange(pos, pos); - }; - - var _setCaretIE = function(input, pos) { - var range = input.createTextRange(); - range.move('character', pos); - range.select(); - }; - - /** - * Sets the position of the caret in the given input. - * @param {HTMLInputElement|HTMLTextAreaElement} input input or textarea element - * @param {Number} pos - * @see http://parentnode.org/javascript/working-with-the-cursor-position/ - */ - var _setCaret = function(input, pos) { - input.focus(); - - pos = _getIndex(input, pos); - - // Mozilla, et al. - if (_support.setSelectionRange) { - _setCaretW3(input, pos); - } - // IE - else if (_support.createTextRange) { - _setCaretIE(input, pos); - } - }; - - /** - * Inserts the specified text at the current caret position in the given input. - * @param {HTMLInputElement|HTMLTextAreaElement} input input or textarea element - * @param {String} text - * @see http://parentnode.org/javascript/working-with-the-cursor-position/ - */ - var _insertAtCaret = function(input, text) { - var curPos = _getCaret(input); - - var oldValueNorm = _getValue(input).replace(_rCarriageReturn, ''); - - var newLength = +(curPos + text.length + (oldValueNorm.length - curPos)); - var maxLength = +input.getAttribute('maxlength'); - - if(_hasAttr(input, 'maxlength') && newLength > maxLength) { - var delta = text.length - (newLength - maxLength); - text = text.substr(0, delta); - } - - _setValue(input, oldValueNorm.substr(0, curPos) + text + oldValueNorm.substr(curPos)); - - _setCaret(input, curPos + text.length); - }; - - var _getInputRangeW3 = function(input) { - var range = new Range(); - - range.start = input.selectionStart; - range.end = input.selectionEnd; - - var min = Math.min(range.start, range.end); - var max = Math.max(range.start, range.end); - - range.length = max - min; - range.text = _getValue(input).substring(min, max); - - return range; - }; - - /** @see http://stackoverflow.com/a/3648244/467582 */ - var _getInputRangeIE = function(input) { - var range = new Range(); - - input.focus(); - - var selection = document.selection.createRange(); - - if (selection && selection.parentElement() === input) { - var len, normalizedValue, textInputRange, endRange, start = 0, end = 0; - var rawValue = _getValue(input); - - len = rawValue.length; - normalizedValue = rawValue.replace(/\r\n/g, "\n"); - - // Create a working TextRange that lives only in the input - textInputRange = input.createTextRange(); - textInputRange.moveToBookmark(selection.getBookmark()); - - // Check if the start and end of the selection are at the very end - // of the input, since moveStart/moveEnd doesn't return what we want - // in those cases - endRange = input.createTextRange(); - endRange.collapse(false); - - if (textInputRange.compareEndPoints("StartToEnd", endRange) > -1) { - start = end = len; - } else { - start = -textInputRange.moveStart("character", -len); - start += normalizedValue.slice(0, start).split("\n").length - 1; - - if (textInputRange.compareEndPoints("EndToEnd", endRange) > -1) { - end = len; - } else { - end = -textInputRange.moveEnd("character", -len); - end += normalizedValue.slice(0, end).split("\n").length - 1; - } - } - - /// normalize newlines - start -= (rawValue.substring(0, start).split('\r\n').length - 1); - end -= (rawValue.substring(0, end).split('\r\n').length - 1); - /// normalize newlines - - range.start = start; - range.end = end; - range.length = range.end - range.start; - range.text = normalizedValue.substr(range.start, range.length); - } - - return range; - }; - - /** - * Gets the selected text range of the given input. - * @param {HTMLInputElement|HTMLTextAreaElement} input input or textarea element - * @returns {Range} - * @see http://stackoverflow.com/a/263796/467582 - * @see http://stackoverflow.com/a/2966703/467582 - */ - var _getInputRange = function(input) { - if (!input) { - return undefined; - } - - // Mozilla, et al. - if (_support.setSelectionRange) { - return _getInputRangeW3(input); - } - // IE - else if (_support.createTextRange) { - return _getInputRangeIE(input); - } - - return undefined; - }; - - var _setInputRangeW3 = function(input, startPos, endPos) { - input.setSelectionRange(startPos, endPos); - }; - - var _setInputRangeIE = function(input, startPos, endPos) { - var tr = input.createTextRange(); - tr.moveEnd('textedit', -1); - tr.moveStart('character', startPos); - tr.moveEnd('character', endPos - startPos); - tr.select(); - }; - - /** - * Sets the selected text range of (i.e., highlights text in) the given input. - * @param {HTMLInputElement|HTMLTextAreaElement} input input or textarea element - * @param {Number} startPos Zero-based index - * @param {Number} endPos Zero-based index - * @see http://parentnode.org/javascript/working-with-the-cursor-position/ - * @see http://stackoverflow.com/a/2966703/467582 - */ - var _setInputRange = function(input, startPos, endPos) { - startPos = _getIndex(input, startPos); - endPos = _getIndex(input, endPos); - - // Mozilla, et al. - if (_support.setSelectionRange) { - _setInputRangeW3(input, startPos, endPos); - } - // IE - else if (_support.createTextRange) { - _setInputRangeIE(input, startPos, endPos); - } - }; - - /** - * Replaces the currently selected text with the given string. - * @param {HTMLInputElement|HTMLTextAreaElement} input input or textarea element - * @param {String} text New text that will replace the currently selected text. - * @see http://parentnode.org/javascript/working-with-the-cursor-position/ - */ - var _replaceInputRange = function(input, text) { - var $input = $(input); - - var oldValue = $input.val(); - var selection = _getInputRange(input); - - var newLength = +(selection.start + text.length + (oldValue.length - selection.end)); - var maxLength = +$input.attr('maxlength'); - - if($input.is('[maxlength]') && newLength > maxLength) { - var delta = text.length - (newLength - maxLength); - text = text.substr(0, delta); - } - - // Now that we know what the user selected, we can replace it - var startText = oldValue.substr(0, selection.start); - var endText = oldValue.substr(selection.end); - - $input.val(startText + text + endText); - - // Reset the selection - var startPos = selection.start; - var endPos = startPos + text.length; - - _setInputRange(input, selection.length ? startPos : endPos, endPos); - }; - - var _selectAllW3 = function(elem) { - var selection = window.getSelection(); - var range = document.createRange(); - range.selectNodeContents(elem); - selection.removeAllRanges(); - selection.addRange(range); - }; - - var _selectAllIE = function(elem) { - var range = document.body.createTextRange(); - range.moveToElementText(elem); - range.select(); - }; - - /** - * Select all text in the given element. - * @param {HTMLElement} elem Any block or inline element other than a form element. - */ - var _selectAll = function(elem) { - var $elem = $(elem); - if ($elem.is('input, textarea') || elem.select) { - $elem.select(); - return; - } - - // Mozilla, et al. - if (_support.setSelectionRange) { - _selectAllW3(elem); - } - // IE - else if (_support.createTextRange) { - _selectAllIE(elem); - } - }; - - var _deselectAll = function() { - if (document.selection) { - document.selection.empty(); - } - else if (window.getSelection) { - window.getSelection().removeAllRanges(); - } - }; - - $.extend($.fn, { - - /** - * Gets or sets the position of the caret or inserts text at the current caret position in an input or textarea element. - * @returns {Number|jQuery} The current caret position if invoked as a getter (with no arguments) - * or this jQuery object if invoked as a setter or inserter. - * @see http://web.archive.org/web/20080704185920/http://parentnode.org/javascript/working-with-the-cursor-position/ - * @since 1.0.0 - * @example - *
            -         *    // Get position
            -         *    var pos = $('input:first').caret();
            -         * 
            - * @example - *
            -         *    // Set position
            -         *    $('input:first').caret(15);
            -         *    $('input:first').caret(-3);
            -         * 
            - * @example - *
            -         *    // Insert text at current position
            -         *    $('input:first').caret('Some text');
            -         * 
            - */ - caret: function() { - var $inputs = this.filter('input, textarea'); - - // getCaret() - if (arguments.length === 0) { - var input = $inputs.get(0); - return _getCaret(input); - } - // setCaret(position) - else if (typeof arguments[0] === 'number') { - var pos = arguments[0]; - $inputs.each(function(_i, input) { - _setCaret(input, pos); - }); - } - // insertAtCaret(text) - else { - var text = arguments[0]; - $inputs.each(function(_i, input) { - _insertAtCaret(input, text); - }); - } - - return this; - }, - - /** - * Gets or sets the selection range or replaces the currently selected text in an input or textarea element. - * @returns {Range|jQuery} The current selection range if invoked as a getter (with no arguments) - * or this jQuery object if invoked as a setter or replacer. - * @see http://stackoverflow.com/a/2966703/467582 - * @since 1.0.0 - * @example - *
            -         *    // Get selection range
            -         *    var range = $('input:first').range();
            -         * 
            - * @example - *
            -         *    // Set selection range
            -         *    $('input:first').range(15);
            -         *    $('input:first').range(15, 20);
            -         *    $('input:first').range(-3);
            -         *    $('input:first').range(-8, -3);
            -         * 
            - * @example - *
            -         *    // Replace the currently selected text
            -         *    $('input:first').range('Replacement text');
            -         * 
            - */ - range: function() { - var $inputs = this.filter('input, textarea'); - - // getRange() = { start: pos, end: pos } - if (arguments.length === 0) { - var input = $inputs.get(0); - return _getInputRange(input); - } - // setRange(startPos, endPos) - else if (typeof arguments[0] === 'number') { - var startPos = arguments[0]; - var endPos = arguments[1]; - $inputs.each(function(_i, input) { - _setInputRange(input, startPos, endPos); - }); - } - // replaceRange(text) - else { - var text = arguments[0]; - $inputs.each(function(_i, input) { - _replaceInputRange(input, text); - }); - } - - return this; - }, - - /** - * Selects all text in each element of this jQuery object. - * @returns {jQuery} This jQuery object - * @see http://stackoverflow.com/a/11128179/467582 - * @since 1.5.0 - * @example - *
            -         *     // Select the contents of span elements when clicked
            -         *     $('span').on('click', function() { $(this).highlight(); });
            -         * 
            - */ - selectAll: function() { - return this.each(function(_i, elem) { - _selectAll(elem); - }); - } - - }); - - $.extend($, { - /** - * Deselects all text on the page. - * @returns {jQuery} The jQuery function - * @since 1.5.0 - * @example - *
            -         *     // Select some text
            -         *     $('span').selectAll();
            -         *
            -         *     // Deselect the text
            -         *     $.deselectAll();
            -         * 
            - */ - deselectAll: function() { - _deselectAll(); - return this; - } - }); - -}(window.jQuery || window.Zepto || window.$)); diff --git a/pagure/static/atwho/jquery.caret-1.5.2.min.js b/pagure/static/atwho/jquery.caret-1.5.2.min.js deleted file mode 100644 index 832a87d..0000000 --- a/pagure/static/atwho/jquery.caret-1.5.2.min.js +++ /dev/null @@ -1,6 +0,0 @@ -/*! jQuery Caret Plugin - v1.5.2 - 2014-03-25 - * https://github.com/acdvorak/jquery.caret - * Copyright (c) 2012-2014 Andrew C. Dvorak; Licensed MIT */ - -!function(a,b){var c=document.createElement("input"),d={setSelectionRange:"setSelectionRange"in c||"selectionStart"in c,createTextRange:"createTextRange"in c||"selection"in document},e=/\r\n/g,f=/\r/g,g=function(b){return"undefined"!=typeof b.value?b.value:a(b).text()},h=function(b,c){"undefined"!=typeof b.value?b.value=c:a(b).text(c)},i=function(a,b){var c=g(a).replace(f,""),d=c.length;return"undefined"==typeof b&&(b=d),b=Math.floor(b),0>b&&(b=d+b),0>b&&(b=0),b>d&&(b=d),b},j=function(a,b){return a.hasAttribute?a.hasAttribute(b):"undefined"!=typeof a[b]},k=function(a,b,c,d){this.start=a||0,this.end=b||0,this.length=c||0,this.text=d||""};k.prototype.toString=function(){return JSON.stringify(this,null," ")};var l=function(a){return a.selectionStart},m=function(a){var b,c,d,f,h,i;return a.focus(),a.focus(),c=document.selection.createRange(),c&&c.parentElement()===a?(f=g(a),h=f.length,d=a.createTextRange(),d.moveToBookmark(c.getBookmark()),i=a.createTextRange(),i.collapse(!1),b=d.compareEndPoints("StartToEnd",i)>-1?f.replace(e,"\n").length:-d.moveStart("character",-h)):0},n=function(a){return a?d.setSelectionRange?l(a):d.createTextRange?m(a):b:b},o=function(a,b){a.setSelectionRange(b,b)},p=function(a,b){var c=a.createTextRange();c.move("character",b),c.select()},q=function(a,b){a.focus(),b=i(a,b),d.setSelectionRange?o(a,b):d.createTextRange&&p(a,b)},r=function(a,b){var c=n(a),d=g(a).replace(f,""),e=+(c+b.length+(d.length-c)),i=+a.getAttribute("maxlength");if(j(a,"maxlength")&&e>i){var k=b.length-(e-i);b=b.substr(0,k)}h(a,d.substr(0,c)+b+d.substr(c)),q(a,c+b.length)},s=function(a){var b=new k;b.start=a.selectionStart,b.end=a.selectionEnd;var c=Math.min(b.start,b.end),d=Math.max(b.start,b.end);return b.length=d-c,b.text=g(a).substring(c,d),b},t=function(a){var b=new k;a.focus();var c=document.selection.createRange();if(c&&c.parentElement()===a){var d,e,f,h,i=0,j=0,l=g(a);d=l.length,e=l.replace(/\r\n/g,"\n"),f=a.createTextRange(),f.moveToBookmark(c.getBookmark()),h=a.createTextRange(),h.collapse(!1),f.compareEndPoints("StartToEnd",h)>-1?i=j=d:(i=-f.moveStart("character",-d),i+=e.slice(0,i).split("\n").length-1,f.compareEndPoints("EndToEnd",h)>-1?j=d:(j=-f.moveEnd("character",-d),j+=e.slice(0,j).split("\n").length-1)),i-=l.substring(0,i).split("\r\n").length-1,j-=l.substring(0,j).split("\r\n").length-1,b.start=i,b.end=j,b.length=b.end-b.start,b.text=e.substr(b.start,b.length)}return b},u=function(a){return a?d.setSelectionRange?s(a):d.createTextRange?t(a):b:b},v=function(a,b,c){a.setSelectionRange(b,c)},w=function(a,b,c){var d=a.createTextRange();d.moveEnd("textedit",-1),d.moveStart("character",b),d.moveEnd("character",c-b),d.select()},x=function(a,b,c){b=i(a,b),c=i(a,c),d.setSelectionRange?v(a,b,c):d.createTextRange&&w(a,b,c)},y=function(b,c){var d=a(b),e=d.val(),f=u(b),g=+(f.start+c.length+(e.length-f.end)),h=+d.attr("maxlength");if(d.is("[maxlength]")&&g>h){var i=c.length-(g-h);c=c.substr(0,i)}var j=e.substr(0,f.start),k=e.substr(f.end);d.val(j+c+k);var l=f.start,m=l+c.length;x(b,f.length?l:m,m)},z=function(a){var b=window.getSelection(),c=document.createRange();c.selectNodeContents(a),b.removeAllRanges(),b.addRange(c)},A=function(a){var b=document.body.createTextRange();b.moveToElementText(a),b.select()},B=function(b){var c=a(b);return c.is("input, textarea")||b.select?(c.select(),void 0):(d.setSelectionRange?z(b):d.createTextRange&&A(b),void 0)},C=function(){document.selection?document.selection.empty():window.getSelection&&window.getSelection().removeAllRanges()};a.extend(a.fn,{caret:function(){var a=this.filter("input, textarea");if(0===arguments.length){var b=a.get(0);return n(b)}if("number"==typeof arguments[0]){var c=arguments[0];a.each(function(a,b){q(b,c)})}else{var d=arguments[0];a.each(function(a,b){r(b,d)})}return this},range:function(){var a=this.filter("input, textarea");if(0===arguments.length){var b=a.get(0);return u(b)}if("number"==typeof arguments[0]){var c=arguments[0],d=arguments[1];a.each(function(a,b){x(b,c,d)})}else{var e=arguments[0];a.each(function(a,b){y(b,e)})}return this},selectAll:function(){return this.each(function(a,b){B(b)})}}),a.extend(a,{deselectAll:function(){return C(),this}})}(window.jQuery||window.Zepto||window.$); -//# sourceMappingURL=dist/jquery.caret-1.5.2.min.map \ No newline at end of file diff --git a/pagure/static/atwho/jquery.caret.js b/pagure/static/atwho/jquery.caret.js index a73e1e2..e0b716b 120000 --- a/pagure/static/atwho/jquery.caret.js +++ b/pagure/static/atwho/jquery.caret.js @@ -1 +1 @@ -jquery.caret-1.5.2.js \ No newline at end of file +jquery.caret-0.3.1.js \ No newline at end of file diff --git a/pagure/static/atwho/jquery.caret.min.js b/pagure/static/atwho/jquery.caret.min.js index 9878694..0cfd9eb 120000 --- a/pagure/static/atwho/jquery.caret.min.js +++ b/pagure/static/atwho/jquery.caret.min.js @@ -1 +1 @@ -jquery.caret-1.5.2.min.js \ No newline at end of file +jquery.caret-0.3.1.min.js \ No newline at end of file From ea386a8a6ae3ecc1a061d9007ece955b338b2b46 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Sep 13 2016 15:18:37 +0000 Subject: [PATCH 217/635] Release 2.5 We invert the order of two alembic migrations as otherwise the migration for the namespaces will fail. --- diff --git a/UPGRADING.rst b/UPGRADING.rst index cc38368..8508d70 100644 --- a/UPGRADING.rst +++ b/UPGRADING.rst @@ -1,6 +1,16 @@ Upgrading Pagure ================ +From 2.4 to 2.5 +--------------- + +2.5 brings quite a few changes and some of them impacting the database scheme. + +Therefore when upgrading from 2.4 to 2.5, you will have to: + +* Update the database schame using alembic: ``alembic upgrade head`` + + From 2.3 to 2.4 --------------- diff --git a/alembic/versions/1640c7d75e5f_add_reports_field_to_project.py b/alembic/versions/1640c7d75e5f_add_reports_field_to_project.py index 126a197..9a37a24 100644 --- a/alembic/versions/1640c7d75e5f_add_reports_field_to_project.py +++ b/alembic/versions/1640c7d75e5f_add_reports_field_to_project.py @@ -1,14 +1,14 @@ """Add reports field to project Revision ID: 1640c7d75e5f -Revises: 350efb3f6baf +Revises: 1d18843a1994 Create Date: 2016-09-09 16:11:28.099423 """ # revision identifiers, used by Alembic. revision = '1640c7d75e5f' -down_revision = '350efb3f6baf' +down_revision = '1d18843a1994' from alembic import op import sqlalchemy as sa diff --git a/alembic/versions/350efb3f6baf_add_namespace_to_project.py b/alembic/versions/350efb3f6baf_add_namespace_to_project.py index ed007b9..8c234d9 100644 --- a/alembic/versions/350efb3f6baf_add_namespace_to_project.py +++ b/alembic/versions/350efb3f6baf_add_namespace_to_project.py @@ -1,14 +1,14 @@ """Add namespace to project Revision ID: 350efb3f6baf -Revises: 1d18843a1994 +Revises: 1640c7d75e5f Create Date: 2016-08-30 22:02:07.645138 """ # revision identifiers, used by Alembic. revision = '350efb3f6baf' -down_revision = '1d18843a1994' +down_revision = '1640c7d75e5f' from alembic import op import sqlalchemy as sa diff --git a/doc/contributors.rst b/doc/contributors.rst index 8e1a974..6e87d8d 100644 --- a/doc/contributors.rst +++ b/doc/contributors.rst @@ -3,17 +3,17 @@ Contributors to pagure Pagure would be nothing without its contributors. -On Aug 31, 2016 (release 2.4), the list looks as follow: +On Sep 13, 2016 (release 2.5), the list looks as follow: ================= =========== Number of commits Contributor ================= =========== - 4249 Pierre-Yves Chibon - 182 Ryan Lerch + 4369 Pierre-Yves Chibon + 186 Ryan Lerch 89 farhaanbukhsh 59 Johan Cwiklinski - 48 Clement Verna - 47 Vivek Anand + 51 Clement Verna + 49 Vivek Anand 27 Farhaan Bukhsh 18 Sayan Chowdhury 17 Lubomír Sedlář @@ -25,17 +25,18 @@ Number of commits Contributor 8 Lei Yang 5 Mike McLean 5 Oliver Gutierrez + 5 Paul W. Frields 5 vanzhiganov 5 yangl1996 4 Eric Barbour 4 Maciej Lasyk - 4 Paul W. Frields 3 Ankush Behl 3 Anthony Lackey 3 Dhriti Shikhar 3 Jan Pokorný 3 Kushal Khandelwal 3 Pedro Lima + 3 Sergio Durigan Junior 3 skrzepto 2 Daniel Mach 2 Nuno Maltez diff --git a/files/pagure.spec b/files/pagure.spec index 77135fe..95a9877 100644 --- a/files/pagure.spec +++ b/files/pagure.spec @@ -2,7 +2,7 @@ %distutils.sysconfig import get_python_lib; print (get_python_lib())")} Name: pagure -Version: 2.4 +Version: 2.5 Release: 1%{?dist} Summary: A git-centered forge @@ -298,6 +298,37 @@ install -m 644 pagure-ci/pagure_ci.service \ %changelog +* Tue Sep 13 2016 Pierre-Yves Chibon - 2.5-1 +- Update to 2.5 +- Don't track pagure_env (venv) dir (Paul W. Frields) +- Setting Mail-Followup-To when sending message to users (Sergio Durigan Junior) + (Fixed by Ryan Lerch and I) +- Fixed the tickets hook so that we dont ignore the files committed in the first + commit (Clement Verna) +- Fix behavior of view of tree if default branch is not 'master' (Vivek Anand) +- Fix checking the release folder for forks +- Improve the Remote PR page +- Improve the fatal error page to display the error message is there is one +- Avoid issues attachment containing json to be considered as an issue to be + created/updated (Clement Verna) +- Allow the html tag (Clement Verna) +- Specify rel="noopener noreferrer" to link including target='_blank' +- Show in the overview page when a branch is already concerned by a PR +- Fix viewing a tree when the identifier provided is one of a blob (not a tree) +- Port all the plugins to `uselist=False` in their backref to make the code + cleaner +- Fix pagure_ci for all sort of small issues but also simply so that it works as + expected +- Make the private method __get_user public as get_user +- Improve the documentation (fix typos and grammar errors) (Sergio Durigan + Junior) +- Drop the `fake` namespaces in favor of real ones +- Add the possibility to view all tickets/pull-requests of a project (regardless + of their status) +- Paginate the pages listing the tickets and the pull-requests +- Add the possibility to save a certain filtering on issues as reports +- Add support to our local markdown processor for ~~striked~~ + * Wed Aug 31 2016 Pierre-Yves Chibon - 2.4-1 - Update to 2.4 - - [Security] Avoid all html related mimetypes and force the download if any diff --git a/pagure/__init__.py b/pagure/__init__.py index 8ace550..8457374 100644 --- a/pagure/__init__.py +++ b/pagure/__init__.py @@ -12,7 +12,7 @@ __requires__ = ['SQLAlchemy >= 0.8', 'jinja2 >= 2.4'] import pkg_resources -__version__ = '2.4' +__version__ = '2.5' __api_version__ = '0.7' From 18b989f1dfa02d7343e7aa6b5ac45082cb6fe8da Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Sep 14 2016 08:40:59 +0000 Subject: [PATCH 218/635] Fix displaying the branches available for a new PR in the requests template We have changed the format of the data returned by the endpoint listing the branches ready to be in a PR so that it also includes branches that are already in a PR (the info is then displayed on the front page of the project). However, this same internal API endpoint is also called in the page listing all the pull-requests and this page had not been adjusted for this data format change. This commit fixes that. Fixes https://pagure.io/pagure/issue/1310 --- diff --git a/pagure/templates/requests.html b/pagure/templates/requests.html index d1ccc3b..22b0560 100644 --- a/pagure/templates/requests.html +++ b/pagure/templates/requests.html @@ -221,7 +221,7 @@ $(function() { success: function(res) { console.log("done"); if (res.code == 'OK'){ - for (branch in res.message){ + for (branch in res.message.new_branch){ var url = "{{ url_for( 'new_request_pull', repo=repo.name, @@ -260,7 +260,7 @@ $(function() { success: function(res) { console.log("done"); if (res.code == 'OK'){ - for (branch in res.message){ + for (branch in res.message.new_branch){ var url = "{{ url_for( 'new_request_pull', repo=repo.name, From 132407851a43a681d1dcc92cd970b24d0e6aaac9 Mon Sep 17 00:00:00 2001 From: Jason Tibbitts Date: Sep 14 2016 20:00:24 +0000 Subject: [PATCH 219/635] Fix grammar on issues page The current issues page will show something like: 100 Open Issues (on 140) "on" should be "of". "out of" also works, but I've gone with the former. --- diff --git a/pagure/templates/issues.html b/pagure/templates/issues.html index cfd0049..fcc6073 100644 --- a/pagure/templates/issues.html +++ b/pagure/templates/issues.html @@ -12,9 +12,9 @@

            {% if status|lower in ['open', 'true'] %} - {{ issues|count }} Open Issues (on {{ issues_cnt }}) + {{ issues|count }} Open Issues (of {{ issues_cnt }}) {% elif status|lower not in ['open', 'true', 'all', 'none'] %} - {{ issues|count }} Closed Issues (on {{ issues_cnt }}) + {{ issues|count }} Closed Issues (of {{ issues_cnt }}) {% else %} {{ issues|count }} Issues {% endif %} From 8c0d7528bda60da793585c665d15be7ad98cfab2 Mon Sep 17 00:00:00 2001 From: Jason Tibbitts Date: Sep 14 2016 23:23:29 +0000 Subject: [PATCH 220/635] Also fix grammar on pull request page. --- diff --git a/pagure/templates/requests.html b/pagure/templates/requests.html index 22b0560..8898169 100644 --- a/pagure/templates/requests.html +++ b/pagure/templates/requests.html @@ -16,7 +16,7 @@ %} {{ status }} {%- elif status|lower in ['closed', 'false'] %} Closed/Merged {%- endif - %} Pull Requests (on {{ requests_cnt }}) + %} Pull Requests (of {{ requests_cnt }}) {% if authenticated and repo.settings.get('pull_requests', True) %} From 97c5c35362e0dc9f93b3ea10e3c309a12c461c50 Mon Sep 17 00:00:00 2001 From: Vivek Anand Date: Sep 15 2016 15:52:48 +0000 Subject: [PATCH 221/635] use username for pygit2 signature in case fullnameisn't there while creating new projects --- diff --git a/pagure/lib/__init__.py b/pagure/lib/__init__.py index eefd0f1..2549648 100644 --- a/pagure/lib/__init__.py +++ b/pagure/lib/__init__.py @@ -1068,12 +1068,12 @@ def new_project(session, user, name, blacklist, allowed_prefix, else: temp_gitrepo_path = tempfile.mkdtemp(prefix='pagure-') temp_gitrepo = pygit2.init_repository(temp_gitrepo_path, bare=False) - author = pygit2.Signature( - userobj.fullname.encode('utf-8') - if six.PY2 else userobj.fullname, - userobj.default_email.encode('utf-8') - if six.PY2 else userobj.fullname - ) + author = userobj.fullname or userobj.user + author_email = userobj.default_email + if six.PY2: + author = author.encode('utf-8') + author_email = author_email.encode('utf-8') + author = pygit2.Signature(author, author_email) content = u"# %s\n\n%s" % (name, description) readme_file = os.path.join(temp_gitrepo.workdir, "README.md") with open(readme_file, 'wb') as stream: From 0593561d826b64252e80b0ebcc73111c5d09af40 Mon Sep 17 00:00:00 2001 From: Aleksandra Fedorova (bookwar) Date: Sep 16 2016 12:17:21 +0000 Subject: [PATCH 222/635] [docs] Fix typo in using_docs example We need to clone initial pagure repo to build docs from sources, not the docs one. --- diff --git a/doc/usage/using_doc.rst b/doc/usage/using_doc.rst index d56535d..e81ffa1 100644 --- a/doc/usage/using_doc.rst +++ b/doc/usage/using_doc.rst @@ -44,7 +44,7 @@ This is how it is built/updated: * Clone pagure's sources:: - git clone https://pagure.io/docs/pagure.git + git clone https://pagure.io/pagure.git * Move into its doc folder:: From a08c069872c3d47aa4d717d2eb9520f7f1b28033 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Sep 17 2016 08:25:25 +0000 Subject: [PATCH 223/635] Fix unit-tests due to the changes made in #1317 --- diff --git a/tests/test_pagure_flask_ui_fork.py b/tests/test_pagure_flask_ui_fork.py index b54e6bb..f73ec64 100644 --- a/tests/test_pagure_flask_ui_fork.py +++ b/tests/test_pagure_flask_ui_fork.py @@ -730,7 +730,7 @@ class PagureFlaskForktests(tests.Modeltests): output = self.app.get('/test/pull-requests') self.assertEqual(output.status_code, 200) self.assertIn( - '

            \n 0 Pull Requests (on 0)\n

            ', + '

            \n 0 Pull Requests (of 0)\n

            ', output.data) # Open is primary self.assertIn( @@ -745,7 +745,7 @@ class PagureFlaskForktests(tests.Modeltests): output = self.app.get('/test/pull-requests') self.assertEqual(output.status_code, 200) self.assertIn( - '

            \n 1 Pull Requests (on 1)\n

            ', + '

            \n 1 Pull Requests (of 1)\n

            ', output.data) # Open is primary self.assertIn( @@ -758,7 +758,7 @@ class PagureFlaskForktests(tests.Modeltests): output = self.app.get('/test/pull-requests?status=Closed') self.assertEqual(output.status_code, 200) self.assertIn( - '

            \n 0 Closed Pull Requests (on 0)\n

            ', + '

            \n 0 Closed Pull Requests (of 0)\n

            ', output.data) # Close is primary self.assertIn( @@ -771,7 +771,7 @@ class PagureFlaskForktests(tests.Modeltests): output = self.app.get('/test/pull-requests?status=0') self.assertEqual(output.status_code, 200) self.assertIn( - '

            \n 0 Closed/Merged Pull Requests (on 0)\n

            ', + '

            \n 0 Closed/Merged Pull Requests (of 0)\n

            ', output.data) # Close is primary self.assertIn( From c5854b0cdefe5d39cfe9723df6085b5fad58aa46 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Sep 19 2016 13:04:32 +0000 Subject: [PATCH 224/635] Fix viewing plugins and returning to the proper page on projects with a namespace --- diff --git a/pagure/ui/plugins.py b/pagure/ui/plugins.py index fa6b9f7..1e88233 100644 --- a/pagure/ui/plugins.py +++ b/pagure/ui/plugins.py @@ -96,7 +96,7 @@ def get_plugin(plugin_name): '/fork////settings//', methods=('GET', 'POST')) @login_required -def view_plugin(repo, plugin, username=None, full=True): +def view_plugin(repo, plugin, username=None, namespace=None, full=True): """ Presents the settings of the project. """ repo = flask.g.repo @@ -149,6 +149,7 @@ def view_plugin(repo, plugin, username=None, full=True): full=full, repo=repo, username=username, + namespace=namespace, plugin=plugin, form=form, fields=fields) @@ -174,13 +175,15 @@ def view_plugin(repo, plugin, username=None, full=True): SESSION.commit() return flask.redirect(flask.url_for( - 'view_settings', repo=repo.name, username=username)) + 'view_settings', repo=repo.name, username=username, + namespace=namespace)) return flask.render_template( 'plugin.html', select='settings', full=full, repo=repo, + namespace=namespace, username=username, plugin=plugin, form=form, From 3cea10fdced828f3db3e37a3956b862391af864e Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Sep 19 2016 13:10:04 +0000 Subject: [PATCH 225/635] Add a milestone column to the issues table --- diff --git a/pagure/lib/model.py b/pagure/lib/model.py index cfab888..e1c7216 100644 --- a/pagure/lib/model.py +++ b/pagure/lib/model.py @@ -586,6 +586,7 @@ class Issue(BASE): nullable=False) private = sa.Column(sa.Boolean, nullable=False, default=False) priority = sa.Column(sa.Integer, nullable=True, default=None) + milestone = sa.Column(sa.String(255), nullable=True, default=None) date_created = sa.Column(sa.DateTime, nullable=False, default=datetime.datetime.utcnow) From f52c45870a1d8a9b28ade507d886ca29ad6d3634 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Sep 19 2016 13:10:04 +0000 Subject: [PATCH 226/635] Add alembic migration script adding a milestone column to the issues table --- diff --git a/alembic/versions/36386a60b3fd_add_milestone_to_issues.py b/alembic/versions/36386a60b3fd_add_milestone_to_issues.py new file mode 100644 index 0000000..64c4a29 --- /dev/null +++ b/alembic/versions/36386a60b3fd_add_milestone_to_issues.py @@ -0,0 +1,32 @@ +"""Add milestone to issues + +Revision ID: 36386a60b3fd +Revises: 350efb3f6baf +Create Date: 2016-09-14 11:03:45.673932 + +""" + +# revision identifiers, used by Alembic. +revision = '36386a60b3fd' +down_revision = '350efb3f6baf' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + ''' Add the column milestone to the table issues. + ''' + op.add_column( + 'issues', + sa.Column('milestone', sa.String(255), nullable=True) + ) + + +def downgrade(): + ''' Add the column milestone to the table issues. + ''' + op.add_column( + 'issues', + sa.Column('milestone', sa.String(255), nullable=True) + ) From 8a9c7e85d511cef263d5f5652e700a84e743d2be Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Sep 19 2016 13:10:04 +0000 Subject: [PATCH 227/635] Add support to edit the issue's milestone on pagure.lib.edit_issue --- diff --git a/pagure/lib/__init__.py b/pagure/lib/__init__.py index 2549648..9af27ea 100644 --- a/pagure/lib/__init__.py +++ b/pagure/lib/__init__.py @@ -1269,7 +1269,7 @@ def new_pull_request(session, branch_from, def edit_issue(session, issue, ticketfolder, user, title=None, content=None, status=None, - priority=None, private=False): + priority=None, milestone=None, private=False): ''' Edit the specified issue. ''' user_obj = get_user(session, user) @@ -1304,6 +1304,9 @@ def edit_issue(session, issue, ticketfolder, user, if private in [True, False] and private != issue.private: issue.private = private edit.append('private') + if milestone != issue.milestone: + issue.milestone = milestone + edit.append('milestone') pagure.lib.git.update_git( issue, repo=issue.project, repofolder=ticketfolder) From a0e28719da832f54bcfa667bd7899b10b0550fae Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Sep 19 2016 13:10:04 +0000 Subject: [PATCH 228/635] Implement setting and editing milestone on issues --- diff --git a/pagure/forms.py b/pagure/forms.py index 8b46de4..379c08f 100644 --- a/pagure/forms.py +++ b/pagure/forms.py @@ -257,6 +257,11 @@ class UpdateIssueForm(wtf.Form): [wtforms.validators.Optional()], choices=[] ) + milestone = wtforms.SelectField( + 'Milestone', + [wtforms.validators.Optional()], + choices=[] + ) def __init__(self, *args, **kwargs): """ Calls the default constructor with the normal argument but @@ -276,6 +281,12 @@ class UpdateIssueForm(wtf.Form): (key, kwargs['priorities'][key]) ) + self.milestone.choices = [] + if 'milestones' in kwargs: + for key in sorted(kwargs['milestones']): + self.milestone.choices.append((key, key)) + self.milestone.choices.insert(0, ('', '')) + class AddPullRequestCommentForm(wtf.Form): ''' Form to add a comment to a pull-request. ''' diff --git a/pagure/templates/issue.html b/pagure/templates/issue.html index 9a6386b..06de571 100644 --- a/pagure/templates/issue.html +++ b/pagure/templates/issue.html @@ -254,6 +254,22 @@ {% endif %} + {% if repo.milestones %} + + {% if authenticated and g.repo_admin %} + {{ render_bootstrap_field(form.milestone, + formclass="issue-metadata-form") }} + {% endif%} + + {% endif %} + {% endif %} {% if g.repo.reports %} diff --git a/pagure/templates/roadmap.html b/pagure/templates/roadmap.html index 97ce6fb..57f6dbc 100644 --- a/pagure/templates/roadmap.html +++ b/pagure/templates/roadmap.html @@ -15,16 +15,27 @@ 'view_issues', repo=repo.name, username=username, - namespace=repo.namespace) }}" class="btn btn-secondary btn-sm"> - List - - - New Issue + namespace=repo.namespace) }}"> + + {% if g.repo.reports %} + + {% endif %}

            {% if oth_issues %} From ad30692837c782e831a2b8b391ac17a2434dbfff Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Sep 19 2016 13:10:04 +0000 Subject: [PATCH 232/635] Drop the unplanned since it's no longer a feature --- diff --git a/pagure/ui/issues.py b/pagure/ui/issues.py index 6f32d2b..6e5dac0 100644 --- a/pagure/ui/issues.py +++ b/pagure/ui/issues.py @@ -573,8 +573,6 @@ def view_roadmap(repo, username=None, namespace=None): break if saved: continue - if not milestone: - milestone_issues['unplanned'].append(issues[cnt]) if status: for key in milestone_issues.keys(): From c8f2691c02a570a020525c8448ca303f5fdeeae4 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Sep 19 2016 13:10:04 +0000 Subject: [PATCH 233/635] Adjust the unit-tests for the new behavior of issues' milestones --- diff --git a/tests/test_pagure_flask_ui_roadmap.py b/tests/test_pagure_flask_ui_roadmap.py index 9f514d6..4c47b78 100644 --- a/tests/test_pagure_flask_ui_roadmap.py +++ b/tests/test_pagure_flask_ui_roadmap.py @@ -162,7 +162,8 @@ class PagureFlaskRoadmaptests(tests.Modeltests): 'href="/test/issue/1/edit" title="Edit this issue">', output.data) self.assertIn( - u'\n Tag added: roadmap', + u'\n ' + u'Successfully edited issue #1', output.data) def test_update_milestones(self): @@ -392,6 +393,29 @@ class PagureFlaskRoadmaptests(tests.Modeltests): csrf_token = output.data.split( u'name="csrf_token" type="hidden" value="')[1].split(u'">')[0] + # Create an unplanned milestone + data = { + 'milestones': ['v1.0', 'v2.0', 'unplanned'], + 'milestone_dates': ['Tomorrow', '', ''], + 'csrf_token': csrf_token, + } + output = self.app.post( + '/test/update/milestones', data=data, follow_redirects=True) + self.assertEqual(output.status_code, 200) + # Check the redirect + self.assertIn( + u'Settings - test - Pagure', output.data) + self.assertIn(u'

            Settings for test

            ', output.data) + self.assertIn(u'Milestones updated', output.data) + # Check the result of the action -- Milestones recorded + repo = pagure.lib.get_project(self.session, 'test') + self.assertEqual( + repo.milestones, + { + u'v1.0': u'Tomorrow', u'v2.0': u'', u'unplanned': u'' + } + ) + # Create the issues for cnt in range(6): cnt += 1 @@ -399,7 +423,6 @@ class PagureFlaskRoadmaptests(tests.Modeltests): 'title': 'Test issue %s' % cnt, 'issue_content': 'We really should improve on this ' 'issue %s' % cnt, - 'status': 'Open', 'csrf_token': csrf_token, } @@ -417,8 +440,14 @@ class PagureFlaskRoadmaptests(tests.Modeltests): output.data) # Mark the ticket for the roadmap + mstone = 'v%s.0' % cnt + if cnt >= 3: + if (cnt % 3) == 0: + mstone = 'unplanned' + else: + mstone = 'v%s.0' % (cnt % 3) data = { - 'tag': ['roadmap, v%s.0' % cnt], + 'milestone': mstone, 'csrf_token': csrf_token, } output = self.app.post( @@ -436,20 +465,21 @@ class PagureFlaskRoadmaptests(tests.Modeltests): output.data) self.assertIn( u'\n ' - 'Tag added: v%s.0, roadmap' % cnt, + u'Successfully edited issue #%s' % cnt, output.data) repo = pagure.lib.get_project(self.session, 'test') # Mark ticket #1 as Fixed - ticket = pagure.lib.search_issues( - self.session, - repo, - issueid=1 - ) - ticket.status = 'Fixed' - self.session.add(ticket) - self.session.commit() + for iid in [1, 4]: + ticket = pagure.lib.search_issues( + self.session, + repo, + issueid=iid + ) + ticket.status = 'Fixed' + self.session.add(ticket) + self.session.commit() # test the roadmap view output = self.app.get('/test/roadmap') @@ -458,7 +488,7 @@ class PagureFlaskRoadmaptests(tests.Modeltests): self.assertIn(u'Milestone: v2.0', output.data) self.assertIn(u'Milestone: unplanned', output.data) self.assertEqual( - output.data.count(u'#'), 5) + output.data.count(u'#'), 4) # test the roadmap view for all milestones output = self.app.get('/test/roadmap?status=All') @@ -476,7 +506,7 @@ class PagureFlaskRoadmaptests(tests.Modeltests): self.assertIn(u'1 Milestones', output.data) self.assertIn(u'Milestone: v2.0', output.data) self.assertEqual( - output.data.count(u'#'), 1) + output.data.count(u'#'), 2) # test the roadmap view for a specific milestone - closed output = self.app.get('/test/roadmap?milestone=v1.0') @@ -492,7 +522,7 @@ class PagureFlaskRoadmaptests(tests.Modeltests): self.assertIn(u'1 Milestones', output.data) self.assertIn(u'Milestone: v1.0', output.data) self.assertEqual( - output.data.count(u'#'), 1) + output.data.count(u'#'), 2) # test the roadmap view for errors output = self.app.get('/foo/roadmap') From 26306f184e6069d7963daee704179c6770cc064b Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Sep 19 2016 13:10:04 +0000 Subject: [PATCH 234/635] Include the milestone in the JSON representation of an issue --- diff --git a/pagure/lib/model.py b/pagure/lib/model.py index e1c7216..2e50fb8 100644 --- a/pagure/lib/model.py +++ b/pagure/lib/model.py @@ -673,6 +673,7 @@ class Issue(BASE): 'assignee': self.assignee.to_json( public=public) if self.assignee else None, 'priority': self.priority, + 'milestone': self.milestone, } comments = [] From 725fcabe1edef944dcaa24eb50186336d254ce97 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Sep 19 2016 13:10:04 +0000 Subject: [PATCH 235/635] Adjust unit-tests for the change in JSON representation of an issue --- diff --git a/tests/test_pagure_flask_api_issue.py b/tests/test_pagure_flask_api_issue.py index f723942..7bd337d 100644 --- a/tests/test_pagure_flask_api_issue.py +++ b/tests/test_pagure_flask_api_issue.py @@ -164,6 +164,7 @@ class PagureFlaskApiIssuetests(tests.Modeltests): "closed_at": None, "depends": [], "id": 1, + "milestone": None, "priority": None, "private": False, "status": "Open", @@ -217,6 +218,7 @@ class PagureFlaskApiIssuetests(tests.Modeltests): "closed_at": None, "depends": [], "id": 1, + "milestone": None, "priority": None, "private": False, "status": "Open", @@ -273,6 +275,7 @@ class PagureFlaskApiIssuetests(tests.Modeltests): "closed_at": None, "depends": [], "id": 1, + "milestone": None, "priority": None, "private": False, "status": "Open", @@ -315,6 +318,7 @@ class PagureFlaskApiIssuetests(tests.Modeltests): "closed_at": None, "depends": [], "id": 2, + "milestone": None, "priority": None, "private": True, "status": "Open", @@ -334,6 +338,7 @@ class PagureFlaskApiIssuetests(tests.Modeltests): "closed_at": None, "depends": [], "id": 1, + "milestone": None, "priority": None, "private": False, "status": "Open", @@ -410,6 +415,7 @@ class PagureFlaskApiIssuetests(tests.Modeltests): "closed_at": None, "depends": [], "id": 2, + "milestone": None, "priority": None, "private": True, "status": "Open", @@ -429,6 +435,7 @@ class PagureFlaskApiIssuetests(tests.Modeltests): "closed_at": None, "depends": [], "id": 1, + "milestone": None, "priority": None, "private": False, "status": "Open", @@ -487,6 +494,7 @@ class PagureFlaskApiIssuetests(tests.Modeltests): "closed_at": None, "depends": [], "id": 1, + "milestone": None, "priority": None, "private": False, "status": "Open", @@ -582,6 +590,7 @@ class PagureFlaskApiIssuetests(tests.Modeltests): "closed_at": None, "depends": [], "id": 2, + "milestone": None, "priority": None, "private": True, "status": "Open", @@ -610,6 +619,7 @@ class PagureFlaskApiIssuetests(tests.Modeltests): "closed_at": None, "depends": [], "id": 2, + "milestone": None, "priority": None, "private": True, "status": "Open", diff --git a/tests/test_pagure_lib_git.py b/tests/test_pagure_lib_git.py index 926e7be..48122ee 100644 --- a/tests/test_pagure_lib_git.py +++ b/tests/test_pagure_lib_git.py @@ -476,7 +476,7 @@ new file mode 100644 index 0000000..60f7480 --- /dev/null +++ b/456 -@@ -0,0 +1,24 @@ +@@ -0,0 +1,25 @@ +{ + "assignee": null, + "blocks": [], @@ -486,6 +486,7 @@ index 0000000..60f7480 + "date_created": null, + "depends": [], + "id": 1, ++ "milestone": null, + "priority": null, + "private": false, + "status": "Open", From a3db50ba1c11472dfa8a11644f6def61c008bc9a Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Sep 19 2016 13:10:04 +0000 Subject: [PATCH 236/635] Adjust the documentation about using the roadmap feature --- diff --git a/doc/usage/_static/pagure_roadmap2.png b/doc/usage/_static/pagure_roadmap2.png index a08114f..c4509cc 100644 Binary files a/doc/usage/_static/pagure_roadmap2.png and b/doc/usage/_static/pagure_roadmap2.png differ diff --git a/doc/usage/roadmap.rst b/doc/usage/roadmap.rst index 8f4c400..d0b3e92 100644 --- a/doc/usage/roadmap.rst +++ b/doc/usage/roadmap.rst @@ -6,13 +6,12 @@ their tags. The principal is as follow: -* All the ticket with the tag ``roadmap`` will show up on the roadmap page. - * For each milestones defined in the settings of the project, the roadmap - will group tickets with the corresponding tag. + will group tickets with the corresponding milestone. -* Tickets with the tag ``roadmap`` that are not associated with any of the - milestones defined in the settings are group in an ``unplanned`` section. +* If your project has an ``unplanned`` milestone, this milestone will be + shown at the bottom of the roadmap page. This allowing you to put something + on the roadmap without assigning a real milestone to it. Example @@ -21,14 +20,14 @@ Example For a project named ``test`` on ``pagure.io``. - * First, go to the settings page of the project, create the milestones you like, for example: ``v1.0`` and ``v2.0``. * For the tickets you want to be on these milestones, go through each of them - and add them the tags: ``roadmap`` in combination with the milestone you want - ``v1.0`` or ``v2.0``, or none of them if the ticket is on the roadmap but - not assigned to any milestones. + and set their milestone to either ``v1.0`` or ``v2.0``, or none of them + if the ticket is on not on the roadmap. + You can set the milestone on the metadata panel on the right side of the + issue page. * And this is how it will look like From 5b73ec0a096a43a9bc140ab4285d20eec1d0d84f Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Sep 19 2016 13:10:04 +0000 Subject: [PATCH 237/635] Some more adjustments to the unit-tests for the change when editing an issue --- diff --git a/tests/test_pagure_flask_ui_issues.py b/tests/test_pagure_flask_ui_issues.py index ff4b7bb..a261241 100644 --- a/tests/test_pagure_flask_ui_issues.py +++ b/tests/test_pagure_flask_ui_issues.py @@ -617,10 +617,8 @@ class PagureFlaskIssuestests(tests.Modeltests): 'href="/test/issue/1/edit" title="Edit this issue">', output.data) self.assertIn( - '\n Tag added: tag2', - output.data) - self.assertNotIn( - '\n No changes to edit', + '\n ' + 'Successfully edited issue #1', output.data) self.assertTrue( '

            Woohoo a second comment !

            ' in output.data) @@ -941,7 +939,8 @@ class PagureFlaskIssuestests(tests.Modeltests): 'href="/test/issue/1/edit" title="Edit this issue">', output.data) self.assertIn( - '\n Dependency added', + '\n ' + 'Successfully edited issue #1', output.data) # Add an invalid dependent ticket @@ -960,7 +959,8 @@ class PagureFlaskIssuestests(tests.Modeltests): 'href="/test/issue/1/edit" title="Edit this issue">', output.data) self.assertNotIn( - '\n Dependency added', + '\n ' + 'Successfully edited issue #1', output.data) repo = pagure.lib.get_project(self.session, 'test') @@ -1036,7 +1036,8 @@ class PagureFlaskIssuestests(tests.Modeltests): 'href="/test/issue/1/edit" title="Edit this issue">', output.data) self.assertIn( - '\n Dependency added', + '\n ' + 'Successfully edited issue #1', output.data) # Add an invalid dependent ticket @@ -1055,7 +1056,8 @@ class PagureFlaskIssuestests(tests.Modeltests): 'href="/test/issue/1/edit" title="Edit this issue">', output.data) self.assertNotIn( - '\n Dependency added', + '\n ' + 'Successfully edited issue #1', output.data) repo = pagure.lib.get_project(self.session, 'test') From 40e8439b729cbd3907aa4bcad72fe6e4fc6e2878 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Sep 19 2016 13:10:04 +0000 Subject: [PATCH 238/635] Fix the alembic script to drop the milestone column upon downgrade --- diff --git a/alembic/versions/36386a60b3fd_add_milestone_to_issues.py b/alembic/versions/36386a60b3fd_add_milestone_to_issues.py index 64c4a29..d50b153 100644 --- a/alembic/versions/36386a60b3fd_add_milestone_to_issues.py +++ b/alembic/versions/36386a60b3fd_add_milestone_to_issues.py @@ -26,7 +26,4 @@ def upgrade(): def downgrade(): ''' Add the column milestone to the table issues. ''' - op.add_column( - 'issues', - sa.Column('milestone', sa.String(255), nullable=True) - ) + op.drop_column('issues', 'milestone') From b0e322b38fd982c5710eb4abfc878e452729e087 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Sep 19 2016 13:10:04 +0000 Subject: [PATCH 239/635] Fix typos on the roadmap doc pointed out by @vivekanand1101 --- diff --git a/doc/usage/roadmap.rst b/doc/usage/roadmap.rst index d0b3e92..d2c3902 100644 --- a/doc/usage/roadmap.rst +++ b/doc/usage/roadmap.rst @@ -2,7 +2,7 @@ Using the roadmap feature ========================= Pagure allows building the roadmap of the project using the tickets and -their tags. +their milestones. The principal is as follow: @@ -25,7 +25,7 @@ For a project named ``test`` on ``pagure.io``. * For the tickets you want to be on these milestones, go through each of them and set their milestone to either ``v1.0`` or ``v2.0``, or none of them - if the ticket is on not on the roadmap. + if the ticket is not on the roadmap. You can set the milestone on the metadata panel on the right side of the issue page. From fd251dc0c10478c20c3937cad510ad0dc90131f2 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Sep 19 2016 13:10:37 +0000 Subject: [PATCH 240/635] Ensure the login._check_session_cookie is always the first @before_request This in order to be sure that flask.g.fas_user is set to something when it should and thus that the order @before_request functions can rely on it. --- diff --git a/pagure/__init__.py b/pagure/__init__.py index 8457374..073551d 100644 --- a/pagure/__init__.py +++ b/pagure/__init__.py @@ -609,7 +609,7 @@ APP.register_blueprint(pagure.internal.PV) # Only import the login controller if the app is set up for local login if APP.config.get('PAGURE_AUTH', None) == 'local': import pagure.ui.login as login - APP.before_request(login._check_session_cookie) + APP.before_request_funcs[None].insert(0, login._check_session_cookie) APP.after_request(login._send_session_cookie) From 88ddf3a4d9881b71ec7e1aca1b8bcd7de367784a Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Sep 19 2016 13:28:15 +0000 Subject: [PATCH 241/635] Add a project's setting allowing to set the default privacy status Fixes https://pagure.io/pagure/issue/1285 --- diff --git a/pagure/lib/model.py b/pagure/lib/model.py index 2e50fb8..f46dc06 100644 --- a/pagure/lib/model.py +++ b/pagure/lib/model.py @@ -385,6 +385,7 @@ class Project(BASE): 'Web-hooks': None, 'Enforce_signed-off_commits_in_pull-request': False, 'always_merge': False, + 'issues_default_to_private': False, } if self._settings: diff --git a/pagure/ui/issues.py b/pagure/ui/issues.py index 6e5dac0..17211cb 100644 --- a/pagure/ui/issues.py +++ b/pagure/ui/issues.py @@ -715,6 +715,10 @@ def new_issue(repo, username=None, namespace=None): default, _ = pagure.doc_utils.convert_readme( default_file.data, 'md') + if flask.request.method == 'GET': + form.private.data = repo.settings.get( + 'issues_default_to_private', False) + return flask.render_template( 'new_issue.html', select='issues', From d9f811ee4872aa05473ff6e620757dff8b3ed266 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Sep 19 2016 13:33:51 +0000 Subject: [PATCH 242/635] Allow editing the privacy setting in the issue's metadata Fixes https://pagure.io/pagure/issue/1286 --- diff --git a/pagure/forms.py b/pagure/forms.py index 379c08f..cebe5f4 100644 --- a/pagure/forms.py +++ b/pagure/forms.py @@ -262,6 +262,10 @@ class UpdateIssueForm(wtf.Form): [wtforms.validators.Optional()], choices=[] ) + private = wtforms.BooleanField( + 'Private', + [wtforms.validators.optional()], + ) def __init__(self, *args, **kwargs): """ Calls the default constructor with the normal argument but diff --git a/pagure/templates/issue.html b/pagure/templates/issue.html index 06de571..fe8b982 100644 --- a/pagure/templates/issue.html +++ b/pagure/templates/issue.html @@ -270,6 +270,16 @@ {% endif %} + {% if issue.private %} + + {% endif %} + {% if authenticated and g.repo_admin %} + {{ render_bootstrap_field(form.private, + formclass="issue-metadata-form") }} + {% endif%} + + assignee=assignee) }}">Closed Date: Sep 20 2016 06:33:37 +0000 Subject: [PATCH 246/635] Add the possibility to send notification for issues/PRs With this commit, admins can specify arbitrary email addresses to notify when a ticket/issue or a pull-request is created, updated or closed. This can be used, for example to notify a mailing-list of ticket/issue update. Fixes https://pagure.io/pagure/issue/1258 --- diff --git a/pagure/forms.py b/pagure/forms.py index cebe5f4..57e68fb 100644 --- a/pagure/forms.py +++ b/pagure/forms.py @@ -526,3 +526,17 @@ class AddReportForm(wtf.Form): 'Report name*', [wtforms.validators.Required()] ) + + +class PublicNotificationForm(wtf.Form): + """ Form to verify that comment is not empty + """ + issue_notifs = wtforms.TextAreaField( + 'Public issue notification*', + [wtforms.validators.optional(), wtforms.validators.Email()] + ) + + pr_notifs = wtforms.TextAreaField( + 'Public PR notification*', + [wtforms.validators.optional(), wtforms.validators.Email()] + ) diff --git a/pagure/lib/notify.py b/pagure/lib/notify.py index 08ed65d..05aa38b 100644 --- a/pagure/lib/notify.py +++ b/pagure/lib/notify.py @@ -129,6 +129,14 @@ def _get_emails_for_issue(issue): for watcher in issue.project.watchers: emails.add(watcher.user.default_email) + # Add public notifications to lists/users set project-wide + if issue.isa == 'issue' and not issue.private: + for notifs in issue.project.notifications.get('issues'): + emails.add(notifs) + elif issue.isa == 'pull-request': + for notifs in issue.project.notifications.get('requests'): + emails.add(notifs) + # Remove the person list in unwatch for unwatcher in issue.project.unwatchers: if unwatcher.user.default_email in emails: diff --git a/pagure/templates/settings.html b/pagure/templates/settings.html index 7c09894..b235b93 100644 --- a/pagure/templates/settings.html +++ b/pagure/templates/settings.html @@ -255,6 +255,75 @@
            + Public notifications +
            +
            +

            + The email addresses entered below will receive all the notifications + related to (public) issue and pull-requests, this includes + notifications about new issue or pull-request, new comment + and status change. +

            +

            + To enter multiple addresses, simply delimit then with a comma. +

            +
            + + {{ tag_form.csrf_token }} +
            + +
            +
            + Issues notifications +
            +
            + +
            +
            +
            + +
            +
            +
            + +
            +
            + Pull-requests notifications +
            +
            + +
            +
            +
            + +
            +
            +
            + +
            +
            + +
            +
            + +
            + +
            +
            + +
            +
            +
            Re-generate git repos
            diff --git a/pagure/ui/repo.py b/pagure/ui/repo.py index 3111e42..fcefc1d 100644 --- a/pagure/ui/repo.py +++ b/pagure/ui/repo.py @@ -1986,3 +1986,60 @@ def watch_repo(repo, watch, username=None, namespace=None): flask.flash(msg, 'error') return flask.redirect(return_point) + + +@APP.route('//update/public_notif', methods=['POST']) +@APP.route('///public_notif', methods=['POST']) +@APP.route('/fork///public_notif', methods=['POST']) +@APP.route( + '/fork////public_notif', methods=['POST']) +@login_required +def update_public_notifications(repo, username=None, namespace=None): + """ Update the public notification settings of a project. + """ + if admin_session_timedout(): + flask.flash('Action canceled, try it again', 'error') + url = flask.url_for( + 'view_settings', username=username, repo=repo, + namespace=namespace) + return flask.redirect( + flask.url_for('auth_login', next=url)) + + repo = flask.g.repo + + if not flask.g.repo_admin: + flask.abort( + 403, + 'You are not allowed to change the settings for this project') + + form = pagure.forms.PublicNotificationForm() + + error = False + if form.validate_on_submit(): + issue_notifs = [ + w.strip() + for w in form.issue_notifs.data.split(',') + if w.strip() + ] + pr_notifs = [ + w.strip() + for w in form.pr_notifs.data.split(',') + if w.strip() + ] + + try: + notifs = repo.notifications + notifs['issues'] = issue_notifs + notifs['requests'] = pr_notifs + repo.notifications = notifs + + SESSION.add(repo) + SESSION.commit() + flask.flash('Project updated') + except SQLAlchemyError as err: # pragma: no cover + SESSION.rollback() + flask.flash(str(err), 'error') + + return flask.redirect(flask.url_for( + 'view_settings', username=username, repo=repo.name, + namespace=repo.namespace)) From b5190133fe6976316fbff837c074d868e668cca6 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Sep 20 2016 06:33:37 +0000 Subject: [PATCH 247/635] Add a custom validator allowing to enter multiple emails in one field --- diff --git a/pagure/forms.py b/pagure/forms.py index 57e68fb..219e425 100644 --- a/pagure/forms.py +++ b/pagure/forms.py @@ -28,6 +28,19 @@ PROJECT_NAME_REGEX = \ '^[a-zA-z0-9_][a-zA-Z0-9-_]*$' +class MultipleEmail(wtforms.validators.Email): + """ Split the value by comma and run them through the email validator + of wtforms. + """ + def __call__(self, form, field): + regex = re.compile(r'^.+@[^.].*\.[a-z]{2,10}$', re.IGNORECASE) + message = field.gettext('One or more invalid email address.') + for data in field.data.split(','): + data = data.strip() + if not self.regex.match(data or ''): + raise wtforms.validators.ValidationError(message) + + def file_virus_validator(form, field): if not pagure.APP.config['VIRUS_SCAN_ATTACHMENTS']: return @@ -533,10 +546,10 @@ class PublicNotificationForm(wtf.Form): """ issue_notifs = wtforms.TextAreaField( 'Public issue notification*', - [wtforms.validators.optional(), wtforms.validators.Email()] + [wtforms.validators.optional(), MultipleEmail()] ) pr_notifs = wtforms.TextAreaField( 'Public PR notification*', - [wtforms.validators.optional(), wtforms.validators.Email()] + [wtforms.validators.optional(), MultipleEmail()] ) From e1cd0d75ea757b7e6d25fa6e82286fdf94fb5e46 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Sep 20 2016 06:33:37 +0000 Subject: [PATCH 248/635] Inform the user when the data entered wasn't valid --- diff --git a/pagure/ui/repo.py b/pagure/ui/repo.py index fcefc1d..d25229e 100644 --- a/pagure/ui/repo.py +++ b/pagure/ui/repo.py @@ -2039,6 +2039,9 @@ def update_public_notifications(repo, username=None, namespace=None): except SQLAlchemyError as err: # pragma: no cover SESSION.rollback() flask.flash(str(err), 'error') + else: + flask.flash( + 'Unable to adjust one or more of the email provided', 'error') return flask.redirect(flask.url_for( 'view_settings', username=username, repo=repo.name, From d0e87de4f1a7b9ab6c695b10fe65ddb9d3e046cb Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Sep 20 2016 06:33:37 +0000 Subject: [PATCH 249/635] Misc small fixes fixing running the unit-tests --- diff --git a/pagure/lib/notify.py b/pagure/lib/notify.py index 05aa38b..0c47773 100644 --- a/pagure/lib/notify.py +++ b/pagure/lib/notify.py @@ -131,10 +131,10 @@ def _get_emails_for_issue(issue): # Add public notifications to lists/users set project-wide if issue.isa == 'issue' and not issue.private: - for notifs in issue.project.notifications.get('issues'): + for notifs in issue.project.notifications.get('issues', []): emails.add(notifs) elif issue.isa == 'pull-request': - for notifs in issue.project.notifications.get('requests'): + for notifs in issue.project.notifications.get('requests', []): emails.add(notifs) # Remove the person list in unwatch diff --git a/tests/test_pagure_lib.py b/tests/test_pagure_lib.py index f57522b..80b6860 100644 --- a/tests/test_pagure_lib.py +++ b/tests/test_pagure_lib.py @@ -933,6 +933,7 @@ class PagureLibtests(tests.Modeltests): 'Web-hooks': None, 'Enforce_signed-off_commits_in_pull-request': False, 'always_merge': False, + "issues_default_to_private": False, }, user='pingou', ) diff --git a/tests/test_pagure_lib_git.py b/tests/test_pagure_lib_git.py index 48122ee..27b39d7 100644 --- a/tests/test_pagure_lib_git.py +++ b/tests/test_pagure_lib_git.py @@ -713,7 +713,7 @@ new file mode 100644 index 0000000..60f7480 --- /dev/null +++ b/456 -@@ -0,0 +1,85 @@ +@@ -0,0 +1,87 @@ +{ + "assignee": null, + "branch": "master", @@ -741,6 +741,7 @@ index 0000000..60f7480 + "Web-hooks": null, + "always_merge": false, + "issue_tracker": true, ++ "issues_default_to_private": false, + "project_documentation": false, + "pull_requests": true + }, @@ -771,6 +772,7 @@ index 0000000..60f7480 + "Web-hooks": null, + "always_merge": false, + "issue_tracker": true, ++ "issues_default_to_private": false, + "project_documentation": false, + "pull_requests": true + }, From 618ada130d9288cd357f6ec223ed89b4d947f807 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Sep 20 2016 06:36:40 +0000 Subject: [PATCH 250/635] Allow specifying the root folder to pagure.lib.git.get_repo_namespace This allows specifying a different root folder for projects, tickets and requests. Fixes https://pagure.io/pagure/issue/1321 --- diff --git a/pagure/hooks/files/pagure_hook_requests.py b/pagure/hooks/files/pagure_hook_requests.py index 88c4613..f2cd573 100755 --- a/pagure/hooks/files/pagure_hook_requests.py +++ b/pagure/hooks/files/pagure_hook_requests.py @@ -67,7 +67,8 @@ def run_as_post_receive_hook(): reponame = pagure.lib.git.get_repo_name(abspath) username = pagure.lib.git.get_username(abspath) - namespace = pagure.lib.git.get_repo_namespace(abspath) + namespace = pagure.lib.git.get_repo_namespace( + abspath, gitfolder = pagure.APP.config['REQUESTS_FOLDER']) print 'repo:', reponame, username, namespace for filename in file_list: diff --git a/pagure/hooks/files/pagure_hook_tickets.py b/pagure/hooks/files/pagure_hook_tickets.py index 2f4cb93..5b1a9d0 100755 --- a/pagure/hooks/files/pagure_hook_tickets.py +++ b/pagure/hooks/files/pagure_hook_tickets.py @@ -72,7 +72,8 @@ def run_as_post_receive_hook(): reponame = pagure.lib.git.get_repo_name(abspath) username = pagure.lib.git.get_username(abspath) - namespace = pagure.lib.git.get_repo_namespace(abspath) + namespace = pagure.lib.git.get_repo_namespace( + abspath, gitfolder = pagure.APP.config['TICKETS_FOLDER']) print 'repo:', reponame, username, namespace for filename in file_list: diff --git a/pagure/lib/git.py b/pagure/lib/git.py index ec06296..4435e6a 100644 --- a/pagure/lib/git.py +++ b/pagure/lib/git.py @@ -924,15 +924,15 @@ def get_repo_name(abspath): return repo_name -def get_repo_namespace(abspath): +def get_repo_namespace(abspath, gitfolder=None): ''' Return the name of the git repo based on its path. ''' namespace = None + if not gitfolder: + gitfolder = pagure.APP.config['GIT_FOLDER'] short_path = os.path.abspath(abspath).replace( - os.path.abspath(pagure.APP.config['GIT_FOLDER']), - '' - ).strip('/') + os.path.abspath(gitfolder), '').strip('/') if short_path.startswith('forks/'): username, projectname = short_path.split('forks/', 1)[1].split('/', 1) From bd1a5ad8bd5e3101456b5e277160eb3d2de14ea8 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Sep 20 2016 07:29:59 +0000 Subject: [PATCH 251/635] Fix invalid indentation pointed out by @vivekanand1101 --- diff --git a/pagure/hooks/files/pagure_hook_requests.py b/pagure/hooks/files/pagure_hook_requests.py index f2cd573..2d703e1 100755 --- a/pagure/hooks/files/pagure_hook_requests.py +++ b/pagure/hooks/files/pagure_hook_requests.py @@ -67,7 +67,7 @@ def run_as_post_receive_hook(): reponame = pagure.lib.git.get_repo_name(abspath) username = pagure.lib.git.get_username(abspath) - namespace = pagure.lib.git.get_repo_namespace( + namespace = pagure.lib.git.get_repo_namespace( abspath, gitfolder = pagure.APP.config['REQUESTS_FOLDER']) print 'repo:', reponame, username, namespace From 193a96ca572f03cefa1743babfe0804845290c90 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Sep 20 2016 08:31:29 +0000 Subject: [PATCH 252/635] Small pep8 fix pointed out by @vivekanand1101 --- diff --git a/pagure/hooks/files/pagure_hook_requests.py b/pagure/hooks/files/pagure_hook_requests.py index 2d703e1..107b0e7 100755 --- a/pagure/hooks/files/pagure_hook_requests.py +++ b/pagure/hooks/files/pagure_hook_requests.py @@ -68,7 +68,7 @@ def run_as_post_receive_hook(): reponame = pagure.lib.git.get_repo_name(abspath) username = pagure.lib.git.get_username(abspath) namespace = pagure.lib.git.get_repo_namespace( - abspath, gitfolder = pagure.APP.config['REQUESTS_FOLDER']) + abspath, gitfolder=pagure.APP.config['REQUESTS_FOLDER']) print 'repo:', reponame, username, namespace for filename in file_list: diff --git a/pagure/hooks/files/pagure_hook_tickets.py b/pagure/hooks/files/pagure_hook_tickets.py index 5b1a9d0..18847db 100755 --- a/pagure/hooks/files/pagure_hook_tickets.py +++ b/pagure/hooks/files/pagure_hook_tickets.py @@ -73,7 +73,7 @@ def run_as_post_receive_hook(): reponame = pagure.lib.git.get_repo_name(abspath) username = pagure.lib.git.get_username(abspath) namespace = pagure.lib.git.get_repo_namespace( - abspath, gitfolder = pagure.APP.config['TICKETS_FOLDER']) + abspath, gitfolder=pagure.APP.config['TICKETS_FOLDER']) print 'repo:', reponame, username, namespace for filename in file_list: From 523fbd4a8a70f3fd45339f4861a68daf3cc742de Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Sep 20 2016 10:37:50 +0000 Subject: [PATCH 253/635] Release 2.6 --- diff --git a/UPGRADING.rst b/UPGRADING.rst index 8508d70..0788be4 100644 --- a/UPGRADING.rst +++ b/UPGRADING.rst @@ -1,6 +1,16 @@ Upgrading Pagure ================ +From 2.5 to 2.6 +--------------- + +2.6 brings quite a few changes and some of them impacting the database scheme. + +Therefore when upgrading from 2.4 to 2.6, you will have to: + +* Update the database schame using alembic: ``alembic upgrade head`` + + From 2.4 to 2.5 --------------- diff --git a/doc/contributors.rst b/doc/contributors.rst index 6e87d8d..1c5a0dd 100644 --- a/doc/contributors.rst +++ b/doc/contributors.rst @@ -3,17 +3,17 @@ Contributors to pagure Pagure would be nothing without its contributors. -On Sep 13, 2016 (release 2.5), the list looks as follow: +On Sep 20, 2016 (release 2.6), the list looks as follow: ================= =========== Number of commits Contributor ================= =========== - 4369 Pierre-Yves Chibon + 4400 Pierre-Yves Chibon 186 Ryan Lerch 89 farhaanbukhsh 59 Johan Cwiklinski 51 Clement Verna - 49 Vivek Anand + 50 Vivek Anand 27 Farhaan Bukhsh 18 Sayan Chowdhury 17 Lubomír Sedlář @@ -34,6 +34,7 @@ Number of commits Contributor 3 Anthony Lackey 3 Dhriti Shikhar 3 Jan Pokorný + 3 Jason Tibbitts 3 Kushal Khandelwal 3 Pedro Lima 3 Sergio Durigan Junior @@ -48,11 +49,12 @@ Number of commits Contributor 2 bruno 2 dhrish20 2 tenstormavi + 1 Aleksandra Fedorova (bookwar) 1 Anthony Lackey 1 David Caro 1 Eric Barbour 1 Haikel Guemar - 1 Jason Tibbitts + 1 Jeremy Cline 1 Kunaal Jain 1 Mathew Robinson 1 Pierre-YvesChibon diff --git a/files/pagure.spec b/files/pagure.spec index 95a9877..6664d69 100644 --- a/files/pagure.spec +++ b/files/pagure.spec @@ -2,7 +2,7 @@ %distutils.sysconfig import get_python_lib; print (get_python_lib())")} Name: pagure -Version: 2.5 +Version: 2.6 Release: 1%{?dist} Summary: A git-centered forge @@ -298,6 +298,28 @@ install -m 644 pagure-ci/pagure_ci.service \ %changelog +* Tue Sep 20 2016 Pierre-Yves Chibon - 2.6-1 +- Update to 2.6 +- Fix creating new PR from the page listing all the PRs +- Fix grammar error in the issues and PRs page (Jason Tibbitts) +- Fall back to the user's username if no fullname is provided (Vivek Anand) +- Fix typo in the using_docs documentation page (Aleksandra Fedorova (bookwar)) +- Fix viewing plugins when the project has a namespace (and the redirection + after that) +- Rework the milestone, so that a ticket can only be assigned to one milestone + and things look better +- Add a project wide setting allowing to make all new tickets private by default + (with the option to make them public) +- Allow toggling the privacy setting when editing the ticket's metadata +- Rework some of the logic of pagure-ci for when it searches the project related + to a receive notification +- Fix the label of the button to view all close issues to be consistent with the + PR page (Jeremy Cline) +- Add the possibility for projects to notify specific email addresses about + issues/PRs update +- Fix loading tickets from the ticket git repository (fixes importing project to + pagure) + * Tue Sep 13 2016 Pierre-Yves Chibon - 2.5-1 - Update to 2.5 - Don't track pagure_env (venv) dir (Paul W. Frields) diff --git a/pagure/__init__.py b/pagure/__init__.py index 073551d..66274b6 100644 --- a/pagure/__init__.py +++ b/pagure/__init__.py @@ -12,7 +12,7 @@ __requires__ = ['SQLAlchemy >= 0.8', 'jinja2 >= 2.4'] import pkg_resources -__version__ = '2.5' +__version__ = '2.6' __api_version__ = '0.7' From c06638a9a997f0b3df05edc45215db3584cb305b Mon Sep 17 00:00:00 2001 From: Vivek Anand Date: Sep 23 2016 18:47:50 +0000 Subject: [PATCH 254/635] remove unused import from lib/__init__ --- diff --git a/pagure/lib/__init__.py b/pagure/lib/__init__.py index 5de1cc9..8473bd1 100644 --- a/pagure/lib/__init__.py +++ b/pagure/lib/__init__.py @@ -33,14 +33,11 @@ import redis import six import sqlalchemy import sqlalchemy.schema -from datetime import timedelta from sqlalchemy import func from sqlalchemy import asc from sqlalchemy.orm import aliased from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import scoped_session -from sqlalchemy.orm.exc import NoResultFound -from sqlalchemy.exc import SQLAlchemyError import pygit2 From 322d8d198312b143808777d5f3206d2fdb76f2b9 Mon Sep 17 00:00:00 2001 From: Farhaan Bukhsh Date: Sep 28 2016 16:57:33 +0000 Subject: [PATCH 255/635] Fix NoneType error when pagure-ci form is inactively updated first time When pagure-ci form is updated for the first time and the active box is not checked it threw error, because the ci_hook was not generated that time. --- diff --git a/pagure/hooks/pagure_ci.py b/pagure/hooks/pagure_ci.py index 7a671a3..fd8d405 100644 --- a/pagure/hooks/pagure_ci.py +++ b/pagure/hooks/pagure_ci.py @@ -151,6 +151,7 @@ class PagureCi(BaseHook): should be installed ''' - for hook in project.ci_hook: - hook.pagure_ci_token = None - SESSION.commit() + if project.ci_hook is not None: + for hook in project.ci_hook: + hook.pagure_ci_token = None + SESSION.commit() From a2f3dd77d49e31116429eb1663deb3bcd9235b41 Mon Sep 17 00:00:00 2001 From: Jeremy Cline Date: Sep 28 2016 20:32:26 +0000 Subject: [PATCH 256/635] Fix minor typos in configuration documentation Signed-off-by: Jeremy Cline --- diff --git a/doc/configuration.rst b/doc/configuration.rst index 8bb2d68..0fdffef 100644 --- a/doc/configuration.rst +++ b/doc/configuration.rst @@ -185,7 +185,7 @@ Configure Gitolite Pagure uses `gitolite `_ as an authorization layer. Gitolite relies on `SSH `_ for -the authentication. In other words, SSH let you in and gitolite check if you +the authentication. In other words, SSH lets you in and gitolite checks if you are allowed to do what you are trying to do once you are inside. @@ -193,7 +193,7 @@ GITOLITE_HOME ~~~~~~~~~~~~~ This configuration key should point to the home of the user under which -gitolite is ran. +gitolite is run. GITOLITE_VERSION From 2eaa12d084cb1c8756a4987f4b05fe1ab81168a9 Mon Sep 17 00:00:00 2001 From: Jeremy Cline Date: Sep 29 2016 12:49:53 +0000 Subject: [PATCH 257/635] Use context managers to ensure files are closed There were several places where context managers were not being used and files were potentially not being closed. I started investigating this since the unit tests are not passing if the code directory is mounted with SSHFS. The test setUp failed to remove directories with ``shutil.rmtree`` with "directory not empty". Looking at the directories, there were .fuse_hidden* files so I assumed the file handles were being leaked. The tests are still failing so I've either missed something in my search or that was not the problem, but I thought this would be a good change in any case. Signed-off-by: Jeremy Cline --- diff --git a/pagure/hooks/files/git_multimail.py b/pagure/hooks/files/git_multimail.py index 88930c3..03b10bb 100755 --- a/pagure/hooks/files/git_multimail.py +++ b/pagure/hooks/files/git_multimail.py @@ -2091,8 +2091,8 @@ class ProjectdescEnvironmentMixin(Environment): git_dir = get_git_dir() try: - projectdesc = open( - os.path.join(git_dir, 'description')).readline().strip() + with open(os.path.join(git_dir, 'description')) as f: + projectdesc = f.readline().strip() if projectdesc and not projectdesc.startswith( 'Unnamed repository'): return projectdesc diff --git a/setup.py b/setup.py index efcf7ab..e11d55a 100644 --- a/setup.py +++ b/setup.py @@ -33,12 +33,12 @@ def get_requirements(requirements_file='requirements.txt'): :return type: list """ - lines = open(requirements_file).readlines() - return [ - line.rstrip().split('#')[0] - for line in lines - if not line.startswith('#') - ] + with open(requirements_file) as f: + return [ + line.rstrip().split('#')[0] + for line in f.readlines() + if not line.startswith('#') + ] setup( diff --git a/tests/test_pagure_flask_ui_issues.py b/tests/test_pagure_flask_ui_issues.py index a261241..e68de08 100644 --- a/tests/test_pagure_flask_ui_issues.py +++ b/tests/test_pagure_flask_ui_issues.py @@ -179,20 +179,19 @@ class PagureFlaskIssuestests(tests.Modeltests): csrf_token = output.data.split( 'name="csrf_token" type="hidden" value="')[1].split('">')[0] - stream = open(os.path.join(tests.HERE, 'placebo.png'), 'r') - data = { - 'title': 'Test issue', - 'issue_content': 'We really should improve on this issue\n' - '', - 'status': 'Open', - 'filestream': stream, - 'enctype': 'multipart/form-data', - 'csrf_token': csrf_token, - } - - output = self.app.post( - '/test/new_issue', data=data, follow_redirects=True) - stream.close() + with open(os.path.join(tests.HERE, 'placebo.png'), 'r') as stream: + data = { + 'title': 'Test issue', + 'issue_content': 'We really should improve on this issue\n' + '', + 'status': 'Open', + 'filestream': stream, + 'enctype': 'multipart/form-data', + 'csrf_token': csrf_token, + } + + output = self.app.post( + '/test/new_issue', data=data, follow_redirects=True) self.assertEqual(output.status_code, 200) self.assertIn( @@ -211,18 +210,18 @@ class PagureFlaskIssuestests(tests.Modeltests): user.username = 'pingou' with tests.user_set(pagure.APP, user): - stream = open(os.path.join(tests.HERE, 'placebo.png'), 'r') - data = { - 'title': 'Test issue', - 'issue_content': 'We really should improve on this issue', - 'status': 'Open', - 'filestream': stream, - 'enctype': 'multipart/form-data', - 'csrf_token': csrf_token, - } - - output = self.app.post( - '/test/new_issue', data=data, follow_redirects=True) + with open(os.path.join(tests.HERE, 'placebo.png'), 'r') as stream: + data = { + 'title': 'Test issue', + 'issue_content': 'We really should improve on this issue', + 'status': 'Open', + 'filestream': stream, + 'enctype': 'multipart/form-data', + 'csrf_token': csrf_token, + } + + output = self.app.post( + '/test/new_issue', data=data, follow_redirects=True) self.assertEqual(output.status_code, 404) @patch('pagure.lib.git.update_git') @@ -1130,16 +1129,15 @@ class PagureFlaskIssuestests(tests.Modeltests): with tempfile.NamedTemporaryFile() as eicarfile: eicarfile.write(pyclamd.ClamdUnixSocket().EICAR()) eicarfile.flush() - stream = open(eicarfile.name, 'rb') - data = { - 'csrf_token': csrf_token, - 'filestream': stream, - 'enctype': 'multipart/form-data', - } - output = self.app.post( - '/test/issue/1/upload', data=data, follow_redirects=True) + with open(eicarfile.name, 'rb') as stream: + data = { + 'csrf_token': csrf_token, + 'filestream': stream, + 'enctype': 'multipart/form-data', + } + output = self.app.post( + '/test/issue/1/upload', data=data, follow_redirects=True) self.assertEqual(output.status_code, 200) - stream.close() json_data = json.loads(output.data) exp = { 'output': 'notok', @@ -1147,15 +1145,14 @@ class PagureFlaskIssuestests(tests.Modeltests): self.assertDictEqual(json_data, exp) # Attach a file to a ticket - stream = open(os.path.join(tests.HERE, 'placebo.png'), 'rb') - data = { - 'csrf_token': csrf_token, - 'filestream': stream, - 'enctype': 'multipart/form-data', - } - output = self.app.post( - '/test/issue/1/upload', data=data, follow_redirects=True) - stream.close() + with open(os.path.join(tests.HERE, 'placebo.png'), 'rb') as stream: + data = { + 'csrf_token': csrf_token, + 'filestream': stream, + 'enctype': 'multipart/form-data', + } + output = self.app.post( + '/test/issue/1/upload', data=data, follow_redirects=True) self.assertEqual(output.status_code, 200) json_data = json.loads(output.data) diff --git a/tests/test_pagure_flask_ui_repo.py b/tests/test_pagure_flask_ui_repo.py index 7beab23..a442f6c 100644 --- a/tests/test_pagure_flask_ui_repo.py +++ b/tests/test_pagure_flask_ui_repo.py @@ -2886,8 +2886,9 @@ index 0000000..fb7093d img = os.path.join(tests.HERE, 'placebo.png') # Missing CSRF Token - data = {'filestream': open(img)} - output = self.app.post('/test/upload/', data=data) + with open(img, mode='rb') as stream: + data = {'filestream': stream} + output = self.app.post('/test/upload/', data=data) self.assertEqual(output.status_code, 200) self.assertIn('

            Upload a new release

            ', output.data) @@ -2895,9 +2896,10 @@ index 0000000..fb7093d 'name="csrf_token" type="hidden" value="')[1].split('">')[0] # Upload successful - data = {'filestream': open(img), 'csrf_token': csrf_token} - output = self.app.post( - '/test/upload/', data=data, follow_redirects=True) + with open(img, mode='rb') as stream: + data = {'filestream': stream, 'csrf_token': csrf_token} + output = self.app.post( + '/test/upload/', data=data, follow_redirects=True) self.assertEqual(output.status_code, 200) self.assertIn( '\n File', output.data) From 7bf8e284b95b258d040b22d2b1c969c41cd0aa0d Mon Sep 17 00:00:00 2001 From: Vivek Anand Date: Sep 30 2016 08:01:06 +0000 Subject: [PATCH 258/635] Adjust update_tickets_from_git to add milestones for issues as well --- diff --git a/pagure/lib/git.py b/pagure/lib/git.py index 4435e6a..0317bb8 100644 --- a/pagure/lib/git.py +++ b/pagure/lib/git.py @@ -470,6 +470,29 @@ def update_ticket_from_git( issue = pagure.lib.get_issue_by_uid(session, issue_uid=issue_uid) + # Update milestone + milestone = json_data.get('milestone') + + # If milestone is not in the repo settings, add it + if milestone: + if milestone.strip() not in repo.milestones: + try: + repo.milestones[milestone.strip()] = None + session.add(repo) + session.commit() + except SQLAlchemyError: + session.rollback() + try: + msg = pagure.lib.edit_issue( + session, + issue=issue, + ticketfolder=None, + user=user.username, + milestone=milestone, + ) + except SQLAlchemyError: + session.rollback() + # Update tags tags = json_data.get('tags', []) pagure.lib.update_tags( From f67632795076f90cd4aa260866302593d0de4331 Mon Sep 17 00:00:00 2001 From: Lubomír Sedlář Date: Sep 30 2016 08:08:25 +0000 Subject: [PATCH 259/635] Update milestone description in Settings The milestones are no longer based on tags, so the description should be updated to correctly describe how they work. --- diff --git a/pagure/templates/settings.html b/pagure/templates/settings.html index b235b93..f5308c5 100644 --- a/pagure/templates/settings.html +++ b/pagure/templates/settings.html @@ -561,9 +561,9 @@

            - Below are the milestones you may assign to a ticket, allowing - you to build a roadmap using the `roadmap` tag and a tag - corresponding to one of the milestones defined here. + Each issue can be assigned to a milestone. This way it is + possible to create a roadmap for your project. Below you can create + the milestones and optionally set dates for them.

            Date: Sep 30 2016 13:22:38 +0000 Subject: [PATCH 261/635] Check SSH keys before writing them out This is needed because Gitolite will abort all ACL and keyfile regeneration if there is a single invalid key in its keydir. Signed-off-by: Patrick Uiterwijk --- diff --git a/pagure/lib/__init__.py b/pagure/lib/__init__.py index 8473bd1..0364bc5 100644 --- a/pagure/lib/__init__.py +++ b/pagure/lib/__init__.py @@ -25,6 +25,7 @@ import markdown import os import shutil import tempfile +import subprocess import urlparse import uuid @@ -181,6 +182,18 @@ def search_user(session, username=None, email=None, token=None, pattern=None): return output +def is_valid_ssh_key(key): + key = key.strip() + if not key: + return None + proc = subprocess.Popen(['/usr/bin/ssh-keygen', '-l', '-f', '-'], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + proc.communicate(key) + return proc.returncode == 0 + + def create_user_ssh_keys_on_disk(user, gitolite_keydir): ''' Create the ssh keys for the user on the specific folder. @@ -211,6 +224,8 @@ def create_user_ssh_keys_on_disk(user, gitolite_keydir): for i in range(len(keys)): if not keys[i]: continue + if not is_valid_ssh_key(keys[i]): + continue keyline_dir = os.path.join(gitolite_keydir, 'keys_%i' % i) if not os.path.exists(keyline_dir): os.mkdir(keyline_dir) From f8d59053aaa1576773b94235d4c09c3e0339a181 Mon Sep 17 00:00:00 2001 From: Patrick Uiterwijk Date: Sep 30 2016 13:22:38 +0000 Subject: [PATCH 262/635] Add a validator to check ssh keys on edit Signed-off-by: Patrick Uiterwijk --- diff --git a/pagure/forms.py b/pagure/forms.py index 219e425..344d74c 100644 --- a/pagure/forms.py +++ b/pagure/forms.py @@ -20,6 +20,7 @@ import wtforms import tempfile import pagure +import pagure.lib STRICT_REGEX = '^[a-zA-Z0-9-_]+$' @@ -68,6 +69,11 @@ def file_virus_validator(form, field): raise wtforms.ValidationError('Error scanning uploaded file') +def ssh_key_validator(form, field): + if not pagure.lib.are_valid_ssh_keys(field.data): + raise wtforms.ValidationError('Invalid SSH keys') + + class ProjectFormSimplified(wtf.Form): ''' Form to edit the description of a project. ''' description = wtforms.TextField( @@ -336,7 +342,8 @@ class UserSettingsForm(wtf.Form): ''' Form to create or edit project. ''' ssh_key = wtforms.TextAreaField( 'Public SSH key *', - [wtforms.validators.Required()] + [wtforms.validators.Required(), + ssh_key_validator] ) diff --git a/pagure/lib/__init__.py b/pagure/lib/__init__.py index 0364bc5..57df7a1 100644 --- a/pagure/lib/__init__.py +++ b/pagure/lib/__init__.py @@ -194,6 +194,11 @@ def is_valid_ssh_key(key): return proc.returncode == 0 +def are_valid_ssh_keys(keys): + return all([is_valid_ssh_key(key) is not False + for key in keys.split('\n')]) + + def create_user_ssh_keys_on_disk(user, gitolite_keydir): ''' Create the ssh keys for the user on the specific folder. From f888fd9de0103687c30910f9ffdf42f55f8470ce Mon Sep 17 00:00:00 2001 From: Patrick Uiterwijk Date: Sep 30 2016 13:22:38 +0000 Subject: [PATCH 263/635] Add test case for SSH key checker Signed-off-by: Patrick Uiterwijk --- diff --git a/tests/test_pagure_flask_ui_app.py b/tests/test_pagure_flask_ui_app.py index eb08845..3293c99 100644 --- a/tests/test_pagure_flask_ui_app.py +++ b/tests/test_pagure_flask_ui_app.py @@ -395,7 +395,7 @@ class PagureFlaskApptests(tests.Modeltests): 'name="csrf_token" type="hidden" value="')[1].split('">')[0] data = { - 'ssh_key': 'this is my ssh key', + 'ssh_key': 'blah' } output = self.app.post('/settings/', data=data) @@ -403,24 +403,40 @@ class PagureFlaskApptests(tests.Modeltests): self.assertIn( '
            \n Basic Information\n' '
            ', output.data) - self.assertIn( - '', output.data) data['csrf_token'] = csrf_token output = self.app.post( '/settings/', data=data, follow_redirects=True) self.assertEqual(output.status_code, 200) - self.assertTrue( - '\n Public ssh key updated' - in output.data) + self.assertIn('Invalid SSH keys', output.data) + self.assertIn( + '
            \n Basic Information\n' + '
            ', output.data) + self.assertIn('>blah', output.data) + + csrf_token = output.data.split( + 'name="csrf_token" type="hidden" value="')[1].split('">')[0] + + data = { + 'ssh_key': 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDUkub32fZnNI' + '1zJYs43vhhx3c6IcYo4yzhw1gQ37BLhrrNeS6x8l5PKX4J8ZP5' + '1XhViPaLbeOpl94Vm5VSCbLy0xtY9KwLhMkbKj7g6vvfxLm2sT' + 'Osb15j4jzIkUYYgIE7cHhZMCLWR6UA1c1HEzo6mewMDsvpQ9wk' + 'cDnAuXjK3Q==', + 'csrf_token': csrf_token + } + + output = self.app.post( + '/settings/', data=data, follow_redirects=True) + self.assertEqual(output.status_code, 200) + self.assertIn('Public ssh key updated', output.data) self.assertIn( '
            \n Basic Information\n' '
            ', output.data) self.assertIn( '', output.data) + 'ssh-rsa AAAA', output.data) ast.return_value = True output = self.app.get('/settings/') From 0995381379985238d299fbf3dbebec6168b96c9f Mon Sep 17 00:00:00 2001 From: Jeremy Cline Date: Sep 30 2016 13:41:36 +0000 Subject: [PATCH 264/635] Remove hardcoded hostnames in unit tests This commit removes the hardcoded 'https://pagure.org/' in unit test assertions about error messages. Signed-off-by: Jeremy Cline --- diff --git a/tests/test_pagure_flask_api_auth.py b/tests/test_pagure_flask_api_auth.py index ef76281..ac9e574 100644 --- a/tests/test_pagure_flask_api_auth.py +++ b/tests/test_pagure_flask_api_auth.py @@ -22,6 +22,7 @@ from mock import patch sys.path.insert(0, os.path.join(os.path.dirname( os.path.abspath(__file__)), '..')) +import pagure.api import pagure.lib import tests @@ -47,28 +48,18 @@ class PagureFlaskApiAuthtests(tests.Modeltests): output = self.app.post('/api/0/foo/new_issue') self.assertEqual(output.status_code, 401) data = json.loads(output.data) - self.assertDictEqual( - data, - { - "error": "Invalid or expired token. Please visit " \ - "https://pagure.org/ to get or renew your API token.", - "error_code": "EINVALIDTOK", - } - ) + self.assertEqual(pagure.api.APIERROR.EINVALIDTOK.name, + data['error_code']) + self.assertEqual(pagure.api.APIERROR.EINVALIDTOK.value, data['error']) headers = {'Authorization': 'token aabbbccc'} output = self.app.post('/api/0/foo/new_issue', headers=headers) self.assertEqual(output.status_code, 401) data = json.loads(output.data) - self.assertDictEqual( - data, - { - "error": "Invalid or expired token. Please visit " \ - "https://pagure.org/ to get or renew your API token.", - "error_code": "EINVALIDTOK", - } - ) + self.assertEqual(pagure.api.APIERROR.EINVALIDTOK.name, + data['error_code']) + self.assertEqual(pagure.api.APIERROR.EINVALIDTOK.value, data['error']) def test_auth_noacl(self): """ Test the authentication when the token does not have any ACL. @@ -79,28 +70,18 @@ class PagureFlaskApiAuthtests(tests.Modeltests): output = self.app.post('/api/0/test/new_issue') self.assertEqual(output.status_code, 401) data = json.loads(output.data) - self.assertDictEqual( - data, - { - "error": "Invalid or expired token. Please visit " \ - "https://pagure.org/ to get or renew your API token.", - "error_code": "EINVALIDTOK", - } - ) + self.assertEqual(pagure.api.APIERROR.EINVALIDTOK.name, + data['error_code']) + self.assertEqual(pagure.api.APIERROR.EINVALIDTOK.value, data['error']) headers = {'Authorization': 'token aaabbbcccddd'} output = self.app.post('/api/0/test/new_issue', headers=headers) self.assertEqual(output.status_code, 401) data = json.loads(output.data) - self.assertDictEqual( - data, - { - "error": "Invalid or expired token. Please visit " \ - "https://pagure.org/ to get or renew your API token.", - "error_code": "EINVALIDTOK", - } - ) + self.assertEqual(pagure.api.APIERROR.EINVALIDTOK.name, + data['error_code']) + self.assertEqual(pagure.api.APIERROR.EINVALIDTOK.value, data['error']) def test_auth_expired(self): """ Test the authentication when the token has expired. @@ -111,28 +92,18 @@ class PagureFlaskApiAuthtests(tests.Modeltests): output = self.app.post('/api/0/test/new_issue') self.assertEqual(output.status_code, 401) data = json.loads(output.data) - self.assertDictEqual( - data, - { - "error": "Invalid or expired token. Please visit " \ - "https://pagure.org/ to get or renew your API token.", - "error_code": "EINVALIDTOK", - } - ) + self.assertEqual(pagure.api.APIERROR.EINVALIDTOK.name, + data['error_code']) + self.assertEqual(pagure.api.APIERROR.EINVALIDTOK.value, data['error']) headers = {'Authorization': 'token expired_token'} output = self.app.post('/api/0/test/new_issue', headers=headers) self.assertEqual(output.status_code, 401) data = json.loads(output.data) - self.assertDictEqual( - data, - { - "error": "Invalid or expired token. Please visit " \ - "https://pagure.org/ to get or renew your API token.", - "error_code": "EINVALIDTOK", - } - ) + self.assertEqual(pagure.api.APIERROR.EINVALIDTOK.name, + data['error_code']) + self.assertEqual(pagure.api.APIERROR.EINVALIDTOK.value, data['error']) def test_auth(self): """ Test the token based authentication. @@ -144,14 +115,9 @@ class PagureFlaskApiAuthtests(tests.Modeltests): output = self.app.post('/api/0/test/new_issue') self.assertEqual(output.status_code, 401) data = json.loads(output.data) - self.assertDictEqual( - data, - { - "error": "Invalid or expired token. Please visit " \ - "https://pagure.org/ to get or renew your API token.", - "error_code": "EINVALIDTOK", - } - ) + self.assertEqual(pagure.api.APIERROR.EINVALIDTOK.name, + data['error_code']) + self.assertEqual(pagure.api.APIERROR.EINVALIDTOK.value, data['error']) headers = {'Authorization': 'token aaabbbcccddd'} diff --git a/tests/test_pagure_flask_api_fork.py b/tests/test_pagure_flask_api_fork.py index 9ade4c8..916a6a6 100644 --- a/tests/test_pagure_flask_api_fork.py +++ b/tests/test_pagure_flask_api_fork.py @@ -23,6 +23,7 @@ from mock import patch sys.path.insert(0, os.path.join(os.path.dirname( os.path.abspath(__file__)), '..')) +import pagure.api import pagure.lib import tests @@ -341,14 +342,9 @@ class PagureFlaskApiForktests(tests.Modeltests): '/api/0/test2/pull-request/1/close', headers=headers) self.assertEqual(output.status_code, 401) data = json.loads(output.data) - self.assertDictEqual( - data, - { - "error": "Invalid or expired token. Please visit " \ - "https://pagure.org/ to get or renew your API token.", - "error_code": "EINVALIDTOK", - } - ) + self.assertEqual(pagure.api.APIERROR.EINVALIDTOK.name, + data['error_code']) + self.assertEqual(pagure.api.APIERROR.EINVALIDTOK.value, data['error']) # Invalid PR output = self.app.post( @@ -455,14 +451,9 @@ class PagureFlaskApiForktests(tests.Modeltests): '/api/0/test2/pull-request/1/merge', headers=headers) self.assertEqual(output.status_code, 401) data = json.loads(output.data) - self.assertDictEqual( - data, - { - "error": "Invalid or expired token. Please visit " \ - "https://pagure.org/ to get or renew your API token.", - "error_code": "EINVALIDTOK", - } - ) + self.assertEqual(pagure.api.APIERROR.EINVALIDTOK.name, + data['error_code']) + self.assertEqual(pagure.api.APIERROR.EINVALIDTOK.value, data['error']) # Invalid PR output = self.app.post( @@ -550,14 +541,9 @@ class PagureFlaskApiForktests(tests.Modeltests): '/api/0/test2/pull-request/1/comment', headers=headers) self.assertEqual(output.status_code, 401) data = json.loads(output.data) - self.assertDictEqual( - data, - { - "error": "Invalid or expired token. Please visit " \ - "https://pagure.org/ to get or renew your API token.", - "error_code": "EINVALIDTOK", - } - ) + self.assertEqual(pagure.api.APIERROR.EINVALIDTOK.name, + data['error_code']) + self.assertEqual(pagure.api.APIERROR.EINVALIDTOK.value, data['error']) # No input output = self.app.post( @@ -664,14 +650,9 @@ class PagureFlaskApiForktests(tests.Modeltests): '/api/0/test2/pull-request/1/flag', headers=headers) self.assertEqual(output.status_code, 401) data = json.loads(output.data) - self.assertDictEqual( - data, - { - "error": "Invalid or expired token. Please visit " \ - "https://pagure.org/ to get or renew your API token.", - "error_code": "EINVALIDTOK", - } - ) + self.assertEqual(pagure.api.APIERROR.EINVALIDTOK.name, + data['error_code']) + self.assertEqual(pagure.api.APIERROR.EINVALIDTOK.value, data['error']) # No input output = self.app.post( diff --git a/tests/test_pagure_flask_api_issue.py b/tests/test_pagure_flask_api_issue.py index 7bd337d..60c20e1 100644 --- a/tests/test_pagure_flask_api_issue.py +++ b/tests/test_pagure_flask_api_issue.py @@ -57,14 +57,9 @@ class PagureFlaskApiIssuetests(tests.Modeltests): output = self.app.post('/api/0/test2/new_issue', headers=headers) self.assertEqual(output.status_code, 401) data = json.loads(output.data) - self.assertDictEqual( - data, - { - "error": "Invalid or expired token. Please visit " \ - "https://pagure.org/ to get or renew your API token.", - "error_code": "EINVALIDTOK", - } - ) + self.assertEqual(pagure.api.APIERROR.EINVALIDTOK.name, + data['error_code']) + self.assertEqual(pagure.api.APIERROR.EINVALIDTOK.value, data['error']) # No input output = self.app.post('/api/0/test/new_issue', headers=headers) @@ -540,13 +535,9 @@ class PagureFlaskApiIssuetests(tests.Modeltests): output = self.app.get('/api/0/test/issue/2', headers=headers) self.assertEqual(output.status_code, 401) data = json.loads(output.data) - self.assertDictEqual( - data, - { - "error": "Invalid or expired token. Please visit https://pagure.org/ to get or renew your API token.", - "error_code": "EINVALIDTOK" - } - ) + self.assertEqual(pagure.api.APIERROR.EINVALIDTOK.name, + data['error_code']) + self.assertEqual(pagure.api.APIERROR.EINVALIDTOK.value, data['error']) # Create a new token for another user item = pagure.lib.model.Token( @@ -657,14 +648,9 @@ class PagureFlaskApiIssuetests(tests.Modeltests): output = self.app.post('/api/0/test2/issue/1/status', headers=headers) self.assertEqual(output.status_code, 401) data = json.loads(output.data) - self.assertDictEqual( - data, - { - "error": "Invalid or expired token. Please visit " \ - "https://pagure.org/ to get or renew your API token.", - "error_code": "EINVALIDTOK", - } - ) + self.assertEqual(pagure.api.APIERROR.EINVALIDTOK.name, + data['error_code']) + self.assertEqual(pagure.api.APIERROR.EINVALIDTOK.value, data['error']) # No input output = self.app.post('/api/0/test/issue/1/status', headers=headers) @@ -802,14 +788,9 @@ class PagureFlaskApiIssuetests(tests.Modeltests): '/api/0/foo/issue/1/status', data=data, headers=headers) self.assertEqual(output.status_code, 401) data = json.loads(output.data) - self.assertDictEqual( - data, - { - "error": "Invalid or expired token. Please " - "visit https://pagure.org/ to get or renew your API token.", - "error_code": "EINVALIDTOK" - } - ) + self.assertEqual(pagure.api.APIERROR.EINVALIDTOK.name, + data['error_code']) + self.assertEqual(pagure.api.APIERROR.EINVALIDTOK.value, data['error']) @patch('pagure.lib.git.update_git') @patch('pagure.lib.notify.send_email') @@ -840,14 +821,9 @@ class PagureFlaskApiIssuetests(tests.Modeltests): output = self.app.post('/api/0/test2/issue/1/comment', headers=headers) self.assertEqual(output.status_code, 401) data = json.loads(output.data) - self.assertDictEqual( - data, - { - "error": "Invalid or expired token. Please visit " \ - "https://pagure.org/ to get or renew your API token.", - "error_code": "EINVALIDTOK", - } - ) + self.assertEqual(pagure.api.APIERROR.EINVALIDTOK.name, + data['error_code']) + self.assertEqual(pagure.api.APIERROR.EINVALIDTOK.value, data['error']) # No input output = self.app.post('/api/0/test/issue/1/comment', headers=headers) @@ -982,14 +958,9 @@ class PagureFlaskApiIssuetests(tests.Modeltests): '/api/0/foo/issue/1/comment', data=data, headers=headers) self.assertEqual(output.status_code, 401) data = json.loads(output.data) - self.assertDictEqual( - data, - { - "error": "Invalid or expired token. Please " - "visit https://pagure.org/ to get or renew your API token.", - "error_code": "EINVALIDTOK" - } - ) + self.assertEqual(pagure.api.APIERROR.EINVALIDTOK.name, + data['error_code']) + self.assertEqual(pagure.api.APIERROR.EINVALIDTOK.value, data['error']) # No comment added repo = pagure.lib.get_project(self.session, 'foo') @@ -1166,14 +1137,9 @@ class PagureFlaskApiIssuetests(tests.Modeltests): output = self.app.post('/api/0/test2/issue/1/assign', headers=headers) self.assertEqual(output.status_code, 401) data = json.loads(output.data) - self.assertDictEqual( - data, - { - "error": "Invalid or expired token. Please visit " \ - "https://pagure.org/ to get or renew your API token.", - "error_code": "EINVALIDTOK", - } - ) + self.assertEqual(pagure.api.APIERROR.EINVALIDTOK.name, + data['error_code']) + self.assertEqual(pagure.api.APIERROR.EINVALIDTOK.value, data['error']) # No input output = self.app.post('/api/0/test/issue/1/assign', headers=headers) @@ -1308,14 +1274,9 @@ class PagureFlaskApiIssuetests(tests.Modeltests): '/api/0/foo/issue/1/assign', data=data, headers=headers) self.assertEqual(output.status_code, 401) data = json.loads(output.data) - self.assertDictEqual( - data, - { - "error": "Invalid or expired token. Please " - "visit https://pagure.org/ to get or renew your API token.", - "error_code": "EINVALIDTOK" - } - ) + self.assertEqual(pagure.api.APIERROR.EINVALIDTOK.name, + data['error_code']) + self.assertEqual(pagure.api.APIERROR.EINVALIDTOK.value, data['error']) # No comment added repo = pagure.lib.get_project(self.session, 'foo') diff --git a/tests/test_pagure_flask_api_project.py b/tests/test_pagure_flask_api_project.py index 4792e53..039becf 100644 --- a/tests/test_pagure_flask_api_project.py +++ b/tests/test_pagure_flask_api_project.py @@ -249,14 +249,9 @@ class PagureFlaskApiProjecttests(tests.Modeltests): output = self.app.post('/api/0/new', headers=headers) self.assertEqual(output.status_code, 401) data = json.loads(output.data) - self.assertDictEqual( - data, - { - "error": "Invalid or expired token. Please visit " \ - "https://pagure.org/ to get or renew your API token.", - "error_code": "EINVALIDTOK" - } - ) + self.assertEqual(pagure.api.APIERROR.EINVALIDTOK.name, + data['error_code']) + self.assertEqual(pagure.api.APIERROR.EINVALIDTOK.value, data['error']) headers = {'Authorization': 'token aaabbbcccddd'} @@ -340,14 +335,9 @@ class PagureFlaskApiProjecttests(tests.Modeltests): output = self.app.post('/api/0/fork', headers=headers) self.assertEqual(output.status_code, 401) data = json.loads(output.data) - self.assertDictEqual( - data, - { - "error": "Invalid or expired token. Please visit " \ - "https://pagure.org/ to get or renew your API token.", - "error_code": "EINVALIDTOK" - } - ) + self.assertEqual(pagure.api.APIERROR.EINVALIDTOK.name, + data['error_code']) + self.assertEqual(pagure.api.APIERROR.EINVALIDTOK.value, data['error']) headers = {'Authorization': 'token aaabbbcccddd'} diff --git a/tests/test_pagure_flask_ui_issues.py b/tests/test_pagure_flask_ui_issues.py index e68de08..77f863e 100644 --- a/tests/test_pagure_flask_ui_issues.py +++ b/tests/test_pagure_flask_ui_issues.py @@ -565,9 +565,9 @@ class PagureFlaskIssuestests(tests.Modeltests): '' in output.data) self.assertIn( - '

            ' + '

            ' '@pingou changed the status to Fixed' - '

            ', + '

            '.format(app_url=pagure.APP.config['APP_URL']), output.data) # Add new comment From 198c68372b0168e4f8413b75e8e5510c393fd05a Mon Sep 17 00:00:00 2001 From: Patrick Uiterwijk Date: Sep 30 2016 13:43:53 +0000 Subject: [PATCH 265/635] If there's no clamd, skip Signed-off-by: Patrick Uiterwijk --- diff --git a/tests/test_pagure_flask_ui_issues.py b/tests/test_pagure_flask_ui_issues.py index 77f863e..88c14d4 100644 --- a/tests/test_pagure_flask_ui_issues.py +++ b/tests/test_pagure_flask_ui_issues.py @@ -11,12 +11,16 @@ __requires__ = ['SQLAlchemy >= 0.8'] import pkg_resources +from unittest.case import SkipTest import json import unittest import shutil import sys import os -import pyclamd +try: + import pyclamd +except: + pyclamd = None import tempfile import pygit2 @@ -1068,6 +1072,8 @@ class PagureFlaskIssuestests(tests.Modeltests): @patch('pagure.lib.notify.send_email') def test_upload_issue(self, p_send_email, p_ugt): """ Test the upload_issue endpoint. """ + if not pyclamd: + raise SkipTest() p_send_email.return_value = True p_ugt.return_value = True From 72ed91a6dbdc523fdb655f697caa1516f11d57c3 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Sep 30 2016 13:46:05 +0000 Subject: [PATCH 266/635] Fix showing the PR number or New PR button on branches containing a dot Fixes https://pagure.io/pagure/issue/1361 --- diff --git a/pagure/templates/repo_info.html b/pagure/templates/repo_info.html index 3cf2c9c..86efe0b 100644 --- a/pagure/templates/repo_info.html +++ b/pagure/templates/repo_info.html @@ -327,7 +327,7 @@ $(function() { commit not in the main branch. Click to create new PR now.'+'"> New PR \
            '; {%endif%} - $('#branch-'+branch+' .branch_del').prepend(html2); + $('#branch-' + branch.replace('.', '\\.') + ' .branch_del').prepend(html2); $('[data-toggle="tooltip"]').tooltip({placement : 'bottom'}); } for (branch in res.message.branch_w_pr){ @@ -345,7 +345,7 @@ $(function() { + 'PR#' + res.message.branch_w_pr[branch] + ' \
            '; console.log(html); - $('#branch-' + branch + ' .branch_del').prepend(html); + $('#branch-' + branch.replace('.', '\\.') + ' .branch_del').prepend(html); $('[data-toggle="tooltip"]').tooltip({placement : 'bottom'}); } } From c60bf118d3e399ba9cc55b74147e9c14c9ba9aec Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Sep 30 2016 13:46:05 +0000 Subject: [PATCH 267/635] Fix deleting branches containing a dot in their name --- diff --git a/pagure/templates/repo_info.html b/pagure/templates/repo_info.html index 86efe0b..1508c9a 100644 --- a/pagure/templates/repo_info.html +++ b/pagure/templates/repo_info.html @@ -133,7 +133,7 @@ git push -u origin master onsubmit="return confirm('Are you sure you want to remove the branch: {{ branch }}?\nThis cannot be un-done!');"> {{ form.csrf_token }} + onclick="$('#delete_branch_form-{{ branch | replace('/', '_') | replace('.', '\\\\.') }}').submit();"> From 5a9d2e9b4280438d29a3d12f117561c0173bd2a6 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Sep 30 2016 14:48:46 +0000 Subject: [PATCH 268/635] Let only project admins create repo Fixes https://pagure.io/pagure/issue/1335 --- diff --git a/pagure/templates/issues.html b/pagure/templates/issues.html index 2159d21..aafee81 100644 --- a/pagure/templates/issues.html +++ b/pagure/templates/issues.html @@ -270,6 +270,8 @@
            + +{% if g.repo_admin %} \n ' - 'Successfully edited issue #1', - output.data) self.assertTrue( '

            Woohoo a second comment !

            ' in output.data) self.assertEqual(output.data.count('comment_body">'), 2) @@ -942,10 +938,6 @@ class PagureFlaskIssuestests(tests.Modeltests): '
            ', output.data) - self.assertIn( - '\n ' - 'Successfully edited issue #1', - output.data) # Add an invalid dependent ticket data = { @@ -1039,10 +1031,6 @@ class PagureFlaskIssuestests(tests.Modeltests): '', output.data) - self.assertIn( - '\n ' - 'Successfully edited issue #1', - output.data) # Add an invalid dependent ticket data = { diff --git a/tests/test_pagure_flask_ui_roadmap.py b/tests/test_pagure_flask_ui_roadmap.py index 4c47b78..0cca6ec 100644 --- a/tests/test_pagure_flask_ui_roadmap.py +++ b/tests/test_pagure_flask_ui_roadmap.py @@ -161,10 +161,7 @@ class PagureFlaskRoadmaptests(tests.Modeltests): u'', output.data) - self.assertIn( - u'\n ' - u'Successfully edited issue #1', - output.data) + def test_update_milestones(self): """ Test updating milestones of a repo. """ From 6bca9d2c15c7fdf042aaad05806289cb2a09716b Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Oct 04 2016 10:24:25 +0000 Subject: [PATCH 287/635] Fix displaying why a PR cannot be merged Fixes https://pagure.io/pagure/issue/1366 --- diff --git a/pagure/templates/pull_request.html b/pagure/templates/pull_request.html index 0dd83b9..86b622c 100644 --- a/pagure/templates/pull_request.html +++ b/pagure/templates/pull_request.html @@ -1034,16 +1034,7 @@ function setup_reply_btns() { {% if pull_request.status == 'Open' %} $(function(){ - $('#spinner').show(); - $.ajax({ - url: '{{ url_for("internal_ns.mergeable_request_pull") }}' , - type: 'POST', - data: { - requestid: "{{ pull_request.uid }}", - csrf_token: "{{ mergeform.csrf_token.current_token }}", - }, - dataType: 'json', - success: function(res) { + function process_response(res) { $('#spinner').hide(); if (res.code == 'FFORWARD'){ $('#merge_btn').addClass("btn-success"); @@ -1069,19 +1060,21 @@ function setup_reply_btns() { $('#merge-alert-message').append(res.message); $('#merge-alert').show(); } + }; + $('#spinner').show(); + $.ajax({ + url: '{{ url_for("internal_ns.mergeable_request_pull") }}' , + type: 'POST', + data: { + requestid: "{{ pull_request.uid }}", + csrf_token: "{{ mergeform.csrf_token.current_token }}", + }, + dataType: 'json', + success: function(res) { + process_response(res) }, error: function(res) { - $('#spinner').hide(); - var _obj = $('#pr_flags').find('tbody'); - if (res.responseJSON.message) { - _obj.append( - $('PR Status:'+ res.responseJSON.message +'')); - } else { - _obj.append( - $('PR Status:' - + res.responseJSON.message +'')); - } + process_response(res.responseJSON); $('#merge_btn').attr("disabled", "disabled"); } }); From 9d15568ba87bbed240e908d70c9364714d075eef Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Oct 04 2016 10:43:47 +0000 Subject: [PATCH 288/635] Add a project's settings to allow turning on/off fedmsg notifications Fixes https://pagure.io/pagure/issue/1380 --- diff --git a/pagure/lib/model.py b/pagure/lib/model.py index 0e62ad6..66bfe0b 100644 --- a/pagure/lib/model.py +++ b/pagure/lib/model.py @@ -387,6 +387,7 @@ class Project(BASE): 'Enforce_signed-off_commits_in_pull-request': False, 'always_merge': False, 'issues_default_to_private': False, + 'fedmsg_notifications': True, } if self._settings: diff --git a/pagure/lib/notify.py b/pagure/lib/notify.py index 0c47773..4f6ef18 100644 --- a/pagure/lib/notify.py +++ b/pagure/lib/notify.py @@ -52,7 +52,8 @@ def log(project, topic, msg, redis=None): occuring in pagure. ''' # Send fedmsg notification (if fedmsg is there and set-up) - fedmsg_publish(topic, msg) + if not project or project.settings.get('fedmsg_notifications', True): + fedmsg_publish(topic, msg) if redis and project: redis.publish( diff --git a/tests/test_pagure_lib.py b/tests/test_pagure_lib.py index 80b6860..e946395 100644 --- a/tests/test_pagure_lib.py +++ b/tests/test_pagure_lib.py @@ -933,7 +933,8 @@ class PagureLibtests(tests.Modeltests): 'Web-hooks': None, 'Enforce_signed-off_commits_in_pull-request': False, 'always_merge': False, - "issues_default_to_private": False, + 'issues_default_to_private': False, + 'fedmsg_notifications': True, }, user='pingou', ) @@ -950,6 +951,8 @@ class PagureLibtests(tests.Modeltests): 'Minimum_score_to_merge_pull-request': None, 'Web-hooks': '', 'Enforce_signed-off_commits_in_pull-request': False, + 'issues_default_to_private': False, + 'fedmsg_notifications': True, }, user='pingou', ) diff --git a/tests/test_pagure_lib_git.py b/tests/test_pagure_lib_git.py index 27b39d7..b3fc048 100644 --- a/tests/test_pagure_lib_git.py +++ b/tests/test_pagure_lib_git.py @@ -713,7 +713,7 @@ new file mode 100644 index 0000000..60f7480 --- /dev/null +++ b/456 -@@ -0,0 +1,87 @@ +@@ -0,0 +1,89 @@ +{ + "assignee": null, + "branch": "master", @@ -740,6 +740,7 @@ index 0000000..60f7480 + "Only_assignee_can_merge_pull-request": false, + "Web-hooks": null, + "always_merge": false, ++ "fedmsg_notifications": true, + "issue_tracker": true, + "issues_default_to_private": false, + "project_documentation": false, @@ -771,6 +772,7 @@ index 0000000..60f7480 + "Only_assignee_can_merge_pull-request": false, + "Web-hooks": null, + "always_merge": false, ++ "fedmsg_notifications": true, + "issue_tracker": true, + "issues_default_to_private": false, + "project_documentation": false, From d1cbf9d8b9d0353faece814d674dff1ff80d9393 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Oct 04 2016 15:53:14 +0000 Subject: [PATCH 289/635] Open a session to the DB for every message received and close it afterward This ensure the data isn't cached and that updating the settings of a project will be reflected in this service (thus if one updates the URL, the service will be updated). Fixes https://pagure.io/pagure/issue/1248 --- diff --git a/webhook-server/pagure-webhook-server.py b/webhook-server/pagure-webhook-server.py index 9c3c2f3..8697e0d 100644 --- a/webhook-server/pagure-webhook-server.py +++ b/webhook-server/pagure-webhook-server.py @@ -123,8 +123,10 @@ def handle_messages(): if '/' in projectname: namespace, projectname = projectname.split('/', 1) + session = pagure.lib.create_session(pagure.APP.config['DB_URL']) project = pagure.lib.get_project( - session=pagure.SESSION, name=projectname, user=username) + session=session, name=projectname, user=username) + session.close() log.info('Got the project, going to the webhooks') call_web_hooks(project, data['topic'], data['msg']) From 73001bc42ec9064e383d5bc0a71d23730629c148 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Oct 04 2016 17:47:41 +0000 Subject: [PATCH 290/635] Add close_status to issues The list of status is a property of the project and can be used to specify why/how a ticket was closed. --- diff --git a/alembic/versions/644ef887bb6f_add_close_status.py b/alembic/versions/644ef887bb6f_add_close_status.py new file mode 100644 index 0000000..9a20a41 --- /dev/null +++ b/alembic/versions/644ef887bb6f_add_close_status.py @@ -0,0 +1,85 @@ +"""Add close status + +Revision ID: 644ef887bb6f +Revises: 368fd931cf7f +Create Date: 2016-10-04 15:38:41.908679 + +""" + +# revision identifiers, used by Alembic. +revision = '644ef887bb6f' +down_revision = '368fd931cf7f' + +from alembic import op +import sqlalchemy as sa + + +try: + from pagure.lib import model +except ImportError: + import sys + sys.path.insert(0, '.') + from pagure.lib import model + + +def upgrade(): + ''' Add the column _close_status to the table projects. + ''' + op.add_column( + 'projects', + sa.Column('_close_status', sa.Text, nullable=True) + ) + op.add_column( + 'issues', + sa.Column('close_status', sa.Text, nullable=True) + ) + + engine = op.get_bind() + Session = sa.orm.scoped_session(sa.orm.sessionmaker()) + Session.configure(bind=engine) + session = Session() + + # Update all the existing projects + statuses = ['Invalid', 'Insufficient data', 'Fixed', 'Duplicate'] + for project in session.query(model.Project).all(): + project.close_status = statuses + session.add(project) + session.commit() + + # Add the status 'Closed' for issues + ticket_stat = model.StatusIssue(status='Closed') + session.add(ticket_stat) + session.commit() + + # Remove the old status + op.execute('''DELETE FROM "status_issue" WHERE "status" NOT IN ('Open', 'Closed'); ''') + + # Set the close_status for all the closed tickets + op.execute('''UPDATE "issues" SET "close_status"=status where status != 'Open'; ''') + + # Mark all the tickets as closed + op.execute('''UPDATE "issues" SET status='Closed' where status != 'Open'; ''') + + +def downgrade(): + ''' Add the column _close_status to the table projects. + ''' + engine = op.get_bind() + Session = sa.orm.scoped_session(sa.orm.sessionmaker()) + Session.configure(bind=engine) + session = Session() + + statuses = ['Invalid', 'Insufficient data', 'Fixed', 'Duplicate'] + for status in statuses: + ticket_stat = model.StatusIssue(status=status) + session.add(ticket_stat) + session.commit() + + # Set the close_status for all the closed tickets + op.execute('''UPDATE "issues" SET status=close_status where status != 'Open'; ''') + + # Remove the old status + op.execute('''DELETE FROM "status_issue" WHERE status = 'Closed'; ''') + + op.drop_column('projects', '_close_status') + op.drop_column('issues', 'close_status') diff --git a/pagure/lib/model.py b/pagure/lib/model.py index 66bfe0b..7edf9bd 100644 --- a/pagure/lib/model.py +++ b/pagure/lib/model.py @@ -102,7 +102,7 @@ def create_default_status(session, acls=None): """ Insert the defaults status in the status tables. """ - statuses = ['Open', 'Invalid', 'Insufficient data', 'Fixed', 'Duplicate'] + statuses = ['Open', 'Closed'] for status in statuses: ticket_stat = StatusIssue(status=status) session.add(ticket_stat) @@ -320,6 +320,7 @@ class Project(BASE): _milestones = sa.Column(sa.Text, nullable=True) _reports = sa.Column(sa.Text, nullable=True) _notifications = sa.Column(sa.Text, nullable=True) + _close_status = sa.Column(sa.Text, nullable=True) date_created = sa.Column(sa.DateTime, nullable=False, default=datetime.datetime.utcnow) @@ -478,6 +479,23 @@ class Project(BASE): self._reports = json.dumps(reports) @property + def close_status(self): + """ Return the dict stored as string in the database as an actual + dict object. + """ + close_status = [] + + if self._close_status: + close_status = json.loads(self._close_status) + + return close_status + + @close_status.setter + def close_status(self, close_status): + ''' Ensures the different close status are properly saved. ''' + self._close_status = json.dumps(close_status) + + @property def open_requests(self): ''' Returns the number of open pull-requests for this project. ''' return BASE.metadata.bind.query( @@ -607,6 +625,7 @@ class Issue(BASE): private = sa.Column(sa.Boolean, nullable=False, default=False) priority = sa.Column(sa.Integer, nullable=True, default=None) milestone = sa.Column(sa.String(255), nullable=True, default=None) + close_status = sa.Column(sa.Text, nullable=True) date_created = sa.Column(sa.DateTime, nullable=False, default=datetime.datetime.utcnow) From 827c774a239fbb670c0cb8167051aecfcb1d5bb8 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Oct 04 2016 17:47:41 +0000 Subject: [PATCH 291/635] Add possibility to set/remove the close status in the settings page of a project --- diff --git a/pagure/templates/settings.html b/pagure/templates/settings.html index f5308c5..1573ccf 100644 --- a/pagure/templates/settings.html +++ b/pagure/templates/settings.html @@ -619,6 +619,60 @@ + +
            +
            +
            Issue Tags
            @@ -807,5 +861,17 @@ $('#new_milestone').click(function(e) { ); }); +$('#new_close_status').click(function(e) { + console.log('new close status'); + $('#close_sstatus').append( + '
            \ +
            \ + \ +
            \ +
            ' + ); +}); + {% endblock %} diff --git a/pagure/ui/repo.py b/pagure/ui/repo.py index 4d19889..c857a4a 100644 --- a/pagure/ui/repo.py +++ b/pagure/ui/repo.py @@ -2046,3 +2046,53 @@ def update_public_notifications(repo, username=None, namespace=None): return flask.redirect(flask.url_for( 'view_settings', username=username, repo=repo.name, namespace=repo.namespace)) + + +@APP.route('//update/close_status', methods=['POST']) +@APP.route('///update/close_status', methods=['POST']) +@APP.route('/fork///update/close_status', methods=['POST']) +@APP.route( + '/fork////update/close_status', + methods=['POST']) +@login_required +def update_close_status(repo, username=None, namespace=None): + """ Update the close_status of a project. + """ + if admin_session_timedout(): + flask.flash('Action canceled, try it again', 'error') + url = flask.url_for( + 'view_settings', username=username, repo=repo, + namespace=namespace) + return flask.redirect( + flask.url_for('auth_login', next=url)) + + repo = flask.g.repo + + if not repo.settings.get('issue_tracker', True): + flask.abort(404, 'No issue tracker found for this project') + + if not flask.g.repo_admin: + flask.abort( + 403, + 'You are not allowed to change the settings for this project') + + form = pagure.forms.ConfirmationForm() + + error = False + if form.validate_on_submit(): + close_status = [ + w.strip() for w in flask.request.form.getlist('close_status') + if w.strip() + ] + try: + repo.close_status = close_status + SESSION.add(repo) + SESSION.commit() + flask.flash('List of close status updated') + except SQLAlchemyError as err: # pragma: no cover + SESSION.rollback() + flask.flash(str(err), 'error') + + return flask.redirect(flask.url_for( + 'view_settings', username=username, repo=repo.name, + namespace=namespace)) From 05fc3ce3397ea600e5c360e81fcd13f7e6393698 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Oct 04 2016 17:47:41 +0000 Subject: [PATCH 292/635] Adjust the issue view to include the close_status information Fixes https://pagure.io/pagure/issue/1373 --- diff --git a/pagure/forms.py b/pagure/forms.py index b8a2243..d632905 100644 --- a/pagure/forms.py +++ b/pagure/forms.py @@ -290,6 +290,11 @@ class UpdateIssueForm(FlaskForm): 'Private', [wtforms.validators.optional()], ) + close_status = wtforms.SelectField( + 'Closed as', + [wtforms.validators.Optional()], + choices=[] + ) def __init__(self, *args, **kwargs): """ Calls the default constructor with the normal argument but @@ -315,6 +320,12 @@ class UpdateIssueForm(FlaskForm): self.milestone.choices.append((key, key)) self.milestone.choices.insert(0, ('', '')) + self.close_status.choices = [] + if 'close_status' in kwargs: + for key in sorted(kwargs['close_status']): + self.close_status.choices.append((key, key)) + self.close_status.choices.insert(0, ('', '')) + class AddPullRequestCommentForm(FlaskForm): ''' Form to add a comment to a pull-request. ''' diff --git a/pagure/lib/__init__.py b/pagure/lib/__init__.py index 0d8d09d..b4f55bb 100644 --- a/pagure/lib/__init__.py +++ b/pagure/lib/__init__.py @@ -1289,13 +1289,13 @@ def new_pull_request(session, branch_from, def edit_issue(session, issue, ticketfolder, user, - title=None, content=None, status=None, + title=None, content=None, status=None, close_status=None, priority=None, milestone=None, private=False): ''' Edit the specified issue. ''' user_obj = get_user(session, user) - if status == 'Fixed' and issue.parents: + if status != 'Open' and issue.parents: for parent in issue.parents: if parent.status == 'Open': raise pagure.exceptions.PagureException( @@ -1314,6 +1314,9 @@ def edit_issue(session, issue, ticketfolder, user, if status.lower() != 'open': issue.closed_at = datetime.datetime.utcnow() edit.append('status') + if close_status and close_status != issue.close_status: + issue.close_status = close_status + edit.append('close_status') if priority: try: priority = int(priority) diff --git a/pagure/templates/issue.html b/pagure/templates/issue.html index fe8b982..0287100 100644 --- a/pagure/templates/issue.html +++ b/pagure/templates/issue.html @@ -116,18 +116,26 @@

            {{ issue.status }} + {% if issue.status == 'Closed' %} + + as: {{ issue.close_status }} + + {% endif %}

            {% if authenticated and g.repo_admin %} {{ render_bootstrap_field(form.status, formclass="issue-metadata-form") }} + {{ render_bootstrap_field(form.close_status, + formclass="issue-metadata-form") }} {% endif%}
            ', output.data) # Add some content to the git repo - tests.add_content_git_repo(os.path.join(tests.HERE, 'test.git')) - tests.add_readme_git_repo(os.path.join(tests.HERE, 'test.git')) + tests.add_content_git_repo(os.path.join(self.path, 'test.git')) + tests.add_readme_git_repo(os.path.join(self.path, 'test.git')) output = self.app.get('/test') self.assertEqual(output.status_code, 200) @@ -918,9 +918,9 @@ class PagureFlaskRepotests(tests.Modeltests): # Add some content to the git repo tests.add_content_git_repo( - os.path.join(tests.HERE, 'forks', 'pingou', 'test.git')) + os.path.join(self.path, 'forks', 'pingou', 'test.git')) tests.add_readme_git_repo( - os.path.join(tests.HERE, 'forks', 'pingou', 'test.git')) + os.path.join(self.path, 'forks', 'pingou', 'test.git')) output = self.app.get('/fork/pingou/test') self.assertEqual(output.status_code, 200) @@ -943,11 +943,11 @@ class PagureFlaskRepotests(tests.Modeltests): self.session.commit() tests.add_content_git_repo( - os.path.join(tests.HERE, 'forks', 'pingou', 'test3.git')) + os.path.join(self.path, 'forks', 'pingou', 'test3.git')) tests.add_readme_git_repo( - os.path.join(tests.HERE, 'forks', 'pingou', 'test3.git')) + os.path.join(self.path, 'forks', 'pingou', 'test3.git')) tests.add_commit_git_repo( - os.path.join(tests.HERE, 'forks', 'pingou', 'test3.git'), + os.path.join(self.path, 'forks', 'pingou', 'test3.git'), ncommits=10) output = self.app.get('/fork/pingou/test3') @@ -963,7 +963,7 @@ class PagureFlaskRepotests(tests.Modeltests): tests.create_projects(self.session) # Create a git repo to play with - gitrepo = os.path.join(tests.HERE, 'test.git') + gitrepo = os.path.join(self.path, 'test.git') pygit2.init_repository(gitrepo, bare=True) # Create a fork of this repo @@ -1019,14 +1019,14 @@ class PagureFlaskRepotests(tests.Modeltests): # No git repo associated self.assertEqual(output.status_code, 404) - tests.create_projects_git(tests.HERE, bare=True) + tests.create_projects_git(self.path, bare=True) output = self.app.get('/test/branch/master') self.assertEqual(output.status_code, 404) # Add some content to the git repo - tests.add_content_git_repo(os.path.join(tests.HERE, 'test.git')) - tests.add_readme_git_repo(os.path.join(tests.HERE, 'test.git')) + tests.add_content_git_repo(os.path.join(self.path, 'test.git')) + tests.add_readme_git_repo(os.path.join(self.path, 'test.git')) output = self.app.get('/test/branch/master') self.assertEqual(output.status_code, 200) @@ -1049,9 +1049,9 @@ class PagureFlaskRepotests(tests.Modeltests): # Add some content to the git repo tests.add_content_git_repo( - os.path.join(tests.HERE, 'forks', 'pingou', 'test.git')) + os.path.join(self.path, 'forks', 'pingou', 'test.git')) tests.add_readme_git_repo( - os.path.join(tests.HERE, 'forks', 'pingou', 'test.git')) + os.path.join(self.path, 'forks', 'pingou', 'test.git')) output = self.app.get('/fork/pingou/test/branch/master') self.assertEqual(output.status_code, 200) @@ -1074,11 +1074,11 @@ class PagureFlaskRepotests(tests.Modeltests): self.session.commit() tests.add_content_git_repo( - os.path.join(tests.HERE, 'forks', 'pingou', 'test3.git')) + os.path.join(self.path, 'forks', 'pingou', 'test3.git')) tests.add_readme_git_repo( - os.path.join(tests.HERE, 'forks', 'pingou', 'test3.git')) + os.path.join(self.path, 'forks', 'pingou', 'test3.git')) tests.add_commit_git_repo( - os.path.join(tests.HERE, 'forks', 'pingou', 'test3.git'), + os.path.join(self.path, 'forks', 'pingou', 'test3.git'), ncommits=10) output = self.app.get('/fork/pingou/test3/branch/master') @@ -1101,7 +1101,7 @@ class PagureFlaskRepotests(tests.Modeltests): # No git repo associated self.assertEqual(output.status_code, 404) - tests.create_projects_git(tests.HERE, bare=True) + tests.create_projects_git(self.path, bare=True) output = self.app.get('/test/commits') self.assertEqual(output.status_code, 200) @@ -1111,8 +1111,8 @@ class PagureFlaskRepotests(tests.Modeltests): 'test project #1
            ', output.data) # Add some content to the git repo - tests.add_content_git_repo(os.path.join(tests.HERE, 'test.git')) - tests.add_readme_git_repo(os.path.join(tests.HERE, 'test.git')) + tests.add_content_git_repo(os.path.join(self.path, 'test.git')) + tests.add_readme_git_repo(os.path.join(self.path, 'test.git')) output = self.app.get('/test/commits') self.assertEqual(output.status_code, 200) @@ -1144,9 +1144,9 @@ class PagureFlaskRepotests(tests.Modeltests): # Add some content to the git repo tests.add_content_git_repo( - os.path.join(tests.HERE, 'forks', 'pingou', 'test.git')) + os.path.join(self.path, 'forks', 'pingou', 'test.git')) tests.add_readme_git_repo( - os.path.join(tests.HERE, 'forks', 'pingou', 'test.git')) + os.path.join(self.path, 'forks', 'pingou', 'test.git')) output = self.app.get('/fork/pingou/test/commits?page=abc') self.assertEqual(output.status_code, 200) @@ -1169,11 +1169,11 @@ class PagureFlaskRepotests(tests.Modeltests): self.session.commit() tests.add_content_git_repo( - os.path.join(tests.HERE, 'forks', 'pingou', 'test3.git')) + os.path.join(self.path, 'forks', 'pingou', 'test3.git')) tests.add_readme_git_repo( - os.path.join(tests.HERE, 'forks', 'pingou', 'test3.git')) + os.path.join(self.path, 'forks', 'pingou', 'test3.git')) tests.add_commit_git_repo( - os.path.join(tests.HERE, 'forks', 'pingou', 'test3.git'), + os.path.join(self.path, 'forks', 'pingou', 'test3.git'), ncommits=10) output = self.app.get('/fork/pingou/test3/commits/fobranch') @@ -1313,26 +1313,26 @@ class PagureFlaskRepotests(tests.Modeltests): # No git repo associated self.assertEqual(output.status_code, 404) - tests.create_projects_git(tests.HERE, bare=True) + tests.create_projects_git(self.path, bare=True) output = self.app.get('/test/bar') self.assertEqual(output.status_code, 404) - repo = pygit2.Repository(os.path.join(tests.HERE, 'test.git')) + repo = pygit2.Repository(os.path.join(self.path, 'test.git')) # Add one commit to git repo tests.add_commit_git_repo( - os.path.join(tests.HERE, 'test.git'), ncommits=1) + os.path.join(self.path, 'test.git'), ncommits=1) c1 = repo.revparse_single('HEAD') # Add another commit to git repo tests.add_commit_git_repo( - os.path.join(tests.HERE, 'test.git'), ncommits=1) + os.path.join(self.path, 'test.git'), ncommits=1) c2 = repo.revparse_single('HEAD') # Add one more commit to git repo tests.add_commit_git_repo( - os.path.join(tests.HERE, 'test.git'), + os.path.join(self.path, 'test.git'), ncommits=1, filename='Šource') c3 = repo.revparse_single('HEAD') @@ -1357,18 +1357,18 @@ class PagureFlaskRepotests(tests.Modeltests): # No git repo associated self.assertEqual(output.status_code, 404) - tests.create_projects_git(tests.HERE, bare=True) + tests.create_projects_git(self.path, bare=True) output = self.app.get('/test/blob/foo/f/sources') self.assertEqual(output.status_code, 404) # Add some content to the git repo - tests.add_content_git_repo(os.path.join(tests.HERE, 'test.git')) - tests.add_readme_git_repo(os.path.join(tests.HERE, 'test.git')) + tests.add_content_git_repo(os.path.join(self.path, 'test.git')) + tests.add_readme_git_repo(os.path.join(self.path, 'test.git')) tests.add_binary_git_repo( - os.path.join(tests.HERE, 'test.git'), 'test.jpg') + os.path.join(self.path, 'test.git'), 'test.jpg') tests.add_binary_git_repo( - os.path.join(tests.HERE, 'test.git'), 'test_binary') + os.path.join(self.path, 'test.git'), 'test_binary') output = self.app.get('/test/blob/master/foofile') self.assertEqual(output.status_code, 404) @@ -1393,7 +1393,7 @@ class PagureFlaskRepotests(tests.Modeltests): output.data) # View by commit id - repo = pygit2.Repository(os.path.join(tests.HERE, 'test.git')) + repo = pygit2.Repository(os.path.join(self.path, 'test.git')) commit = repo.revparse_single('HEAD') output = self.app.get('/test/blob/%s/f/test.jpg' % commit.oid.hex) @@ -1435,7 +1435,7 @@ class PagureFlaskRepotests(tests.Modeltests): # View file with a non-ascii name tests.add_commit_git_repo( - os.path.join(tests.HERE, 'test.git'), + os.path.join(self.path, 'test.git'), ncommits=1, filename='Šource') output = self.app.get('/test/blob/master/f/Šource') self.assertEqual(output.status_code, 200) @@ -1464,11 +1464,11 @@ class PagureFlaskRepotests(tests.Modeltests): self.session.commit() tests.add_content_git_repo( - os.path.join(tests.HERE, 'forks', 'pingou', 'test3.git')) + os.path.join(self.path, 'forks', 'pingou', 'test3.git')) tests.add_readme_git_repo( - os.path.join(tests.HERE, 'forks', 'pingou', 'test3.git')) + os.path.join(self.path, 'forks', 'pingou', 'test3.git')) tests.add_commit_git_repo( - os.path.join(tests.HERE, 'forks', 'pingou', 'test3.git'), + os.path.join(self.path, 'forks', 'pingou', 'test3.git'), ncommits=10) output = self.app.get('/fork/pingou/test3/blob/master/f/sources') @@ -1492,13 +1492,13 @@ class PagureFlaskRepotests(tests.Modeltests): # No git repo associated self.assertEqual(output.status_code, 404) - tests.create_projects_git(tests.HERE, bare=True) + tests.create_projects_git(self.path, bare=True) output = self.app.get('/test/raw/foo/sources') self.assertEqual(output.status_code, 404) # Add some content to the git repo - tests.add_readme_git_repo(os.path.join(tests.HERE, 'test.git')) + tests.add_readme_git_repo(os.path.join(self.path, 'test.git')) # View first commit output = self.app.get('/test/raw/master') @@ -1506,11 +1506,11 @@ class PagureFlaskRepotests(tests.Modeltests): self.assertTrue(':Author: Pierre-Yves Chibon' in output.data) # Add some more content to the repo - tests.add_content_git_repo(os.path.join(tests.HERE, 'test.git')) + tests.add_content_git_repo(os.path.join(self.path, 'test.git')) tests.add_binary_git_repo( - os.path.join(tests.HERE, 'test.git'), 'test.jpg') + os.path.join(self.path, 'test.git'), 'test.jpg') tests.add_binary_git_repo( - os.path.join(tests.HERE, 'test.git'), 'test_binary') + os.path.join(self.path, 'test.git'), 'test_binary') output = self.app.get('/test/raw/master/f/foofile') self.assertEqual(output.status_code, 404) @@ -1526,7 +1526,7 @@ class PagureFlaskRepotests(tests.Modeltests): self.assertTrue(output.data.startswith('\x00\x00\x01\x00')) # View by commit id - repo = pygit2.Repository(os.path.join(tests.HERE, 'test.git')) + repo = pygit2.Repository(os.path.join(self.path, 'test.git')) commit = repo.revparse_single('HEAD') output = self.app.get('/test/raw/%s/f/test.jpg' % commit.oid.hex) @@ -1579,11 +1579,11 @@ class PagureFlaskRepotests(tests.Modeltests): self.session.commit() tests.add_content_git_repo( - os.path.join(tests.HERE, 'forks', 'pingou', 'test3.git')) + os.path.join(self.path, 'forks', 'pingou', 'test3.git')) tests.add_readme_git_repo( - os.path.join(tests.HERE, 'forks', 'pingou', 'test3.git')) + os.path.join(self.path, 'forks', 'pingou', 'test3.git')) tests.add_commit_git_repo( - os.path.join(tests.HERE, 'forks', 'pingou', 'test3.git'), + os.path.join(self.path, 'forks', 'pingou', 'test3.git'), ncommits=10) output = self.app.get('/fork/pingou/test3/raw/master/f/sources') @@ -1602,14 +1602,14 @@ class PagureFlaskRepotests(tests.Modeltests): # No git repo associated self.assertEqual(output.status_code, 404) - tests.create_projects_git(tests.HERE, bare=True) + tests.create_projects_git(self.path, bare=True) output = self.app.get('/test/c/bar') self.assertEqual(output.status_code, 404) # Add a README to the git repo - First commit - tests.add_readme_git_repo(os.path.join(tests.HERE, 'test.git')) - repo = pygit2.Repository(os.path.join(tests.HERE, 'test.git')) + tests.add_readme_git_repo(os.path.join(self.path, 'test.git')) + repo = pygit2.Repository(os.path.join(self.path, 'test.git')) commit = repo.revparse_single('HEAD') # View first commit @@ -1632,9 +1632,9 @@ class PagureFlaskRepotests(tests.Modeltests): self.assertIn('

            Project not found

            ', output.data) # Add some content to the git repo - tests.add_content_git_repo(os.path.join(tests.HERE, 'test.git')) + tests.add_content_git_repo(os.path.join(self.path, 'test.git')) - repo = pygit2.Repository(os.path.join(tests.HERE, 'test.git')) + repo = pygit2.Repository(os.path.join(self.path, 'test.git')) commit = repo.revparse_single('HEAD') # View another commit @@ -1685,7 +1685,7 @@ class PagureFlaskRepotests(tests.Modeltests): self.session.add(item) self.session.commit() forkedgit = os.path.join( - tests.HERE, 'forks', 'pingou', 'test3.git') + self.path, 'forks', 'pingou', 'test3.git') tests.add_content_git_repo(forkedgit) tests.add_readme_git_repo(forkedgit) @@ -1746,14 +1746,14 @@ class PagureFlaskRepotests(tests.Modeltests): # No git repo associated self.assertEqual(output.status_code, 404) - tests.create_projects_git(tests.HERE, bare=True) + tests.create_projects_git(self.path, bare=True) output = self.app.get('/test/c/bar.patch') self.assertEqual(output.status_code, 404) # Add a README to the git repo - First commit - tests.add_readme_git_repo(os.path.join(tests.HERE, 'test.git')) - repo = pygit2.Repository(os.path.join(tests.HERE, 'test.git')) + tests.add_readme_git_repo(os.path.join(self.path, 'test.git')) + repo = pygit2.Repository(os.path.join(self.path, 'test.git')) commit = repo.revparse_single('HEAD') # View first commit @@ -1785,9 +1785,9 @@ index 0000000..fb7093d self.assertTrue('Subject: Add a README file' in output.data) # Add some content to the git repo - tests.add_content_git_repo(os.path.join(tests.HERE, 'test.git')) + tests.add_content_git_repo(os.path.join(self.path, 'test.git')) - repo = pygit2.Repository(os.path.join(tests.HERE, 'test.git')) + repo = pygit2.Repository(os.path.join(self.path, 'test.git')) commit = repo.revparse_single('HEAD') # View another commit @@ -1819,7 +1819,7 @@ index 0000000..11980b1 ) self.session.add(item) self.session.commit() - forkedgit = os.path.join(tests.HERE, 'forks', 'pingou', 'test3.git') + forkedgit = os.path.join(self.path, 'forks', 'pingou', 'test3.git') tests.add_content_git_repo(forkedgit) tests.add_readme_git_repo(forkedgit) @@ -1871,7 +1871,7 @@ index 0000000..fb7093d # No git repo associated self.assertEqual(output.status_code, 404) - tests.create_projects_git(tests.HERE, bare=True) + tests.create_projects_git(self.path, bare=True) output = self.app.get('/test/tree/') self.assertEqual(output.status_code, 200) @@ -1889,8 +1889,8 @@ index 0000000..fb7093d 'No content found in this repository' in output.data) # Add a README to the git repo - First commit - tests.add_readme_git_repo(os.path.join(tests.HERE, 'test.git')) - repo = pygit2.Repository(os.path.join(tests.HERE, 'test.git')) + tests.add_readme_git_repo(os.path.join(self.path, 'test.git')) + repo = pygit2.Repository(os.path.join(self.path, 'test.git')) commit = repo.revparse_single('HEAD') # View first commit @@ -1926,7 +1926,7 @@ index 0000000..fb7093d ) self.session.add(item) self.session.commit() - forkedgit = os.path.join(tests.HERE, 'forks', 'pingou', 'test3.git') + forkedgit = os.path.join(self.path, 'forks', 'pingou', 'test3.git') tests.add_content_git_repo(forkedgit) @@ -1969,7 +1969,7 @@ index 0000000..fb7093d user = tests.FakeUser(username='pingou') with tests.user_set(pagure.APP, user): tests.create_projects(self.session) - tests.create_projects_git(tests.HERE) + tests.create_projects_git(self.path) output = self.app.post('/test/delete', follow_redirects=True) self.assertEqual(output.status_code, 404) @@ -2000,8 +2000,8 @@ index 0000000..fb7093d ) self.session.add(item) self.session.commit() - tests.create_projects_git(tests.HERE) - tests.create_projects_git(os.path.join(tests.HERE, 'docs')) + tests.create_projects_git(self.path) + tests.create_projects_git(os.path.join(self.path, 'docs')) output = self.app.post('/test/delete', follow_redirects=True) self.assertEqual(output.status_code, 404) @@ -2016,12 +2016,12 @@ index 0000000..fb7093d self.session.commit() # Create all the git repos - tests.create_projects_git(tests.HERE) - tests.create_projects_git(os.path.join(tests.HERE, 'docs')) + tests.create_projects_git(self.path) + tests.create_projects_git(os.path.join(self.path, 'docs')) tests.create_projects_git( - os.path.join(tests.HERE, 'tickets'), bare=True) + os.path.join(self.path, 'tickets'), bare=True) tests.create_projects_git( - os.path.join(tests.HERE, 'requests'), bare=True) + os.path.join(self.path, 'requests'), bare=True) # Check repo was created output = self.app.get('/') @@ -2041,7 +2041,7 @@ index 0000000..fb7093d title='Test issue', content='We should work on this', user='pingou', - ticketfolder=os.path.join(tests.HERE, 'tickets') + ticketfolder=os.path.join(self.path, 'tickets') ) self.session.commit() self.assertEqual(msg.title, 'Test issue') @@ -2052,7 +2052,7 @@ index 0000000..fb7093d title='Test issue #2', content='We should work on this, really', user='pingou', - ticketfolder=os.path.join(tests.HERE, 'tickets') + ticketfolder=os.path.join(self.path, 'tickets') ) self.session.commit() self.assertEqual(msg.title, 'Test issue #2') @@ -2078,7 +2078,7 @@ index 0000000..fb7093d branch_to='master', title='test pull-request', user='pingou', - requestfolder=os.path.join(tests.HERE, 'requests'), + requestfolder=os.path.join(self.path, 'requests'), ) self.session.commit() self.assertEqual(req.id, 3) @@ -2092,7 +2092,7 @@ index 0000000..fb7093d branch_to='master', title='test pull-request', user='pingou', - requestfolder=os.path.join(tests.HERE, 'requests'), + requestfolder=os.path.join(self.path, 'requests'), ) self.session.commit() self.assertEqual(req.id, 4) @@ -2145,11 +2145,11 @@ index 0000000..fb7093d self.session.add(item) self.session.commit() tests.add_content_git_repo( - os.path.join(tests.HERE, 'forks', 'pingou', 'test3.git')) + os.path.join(self.path, 'forks', 'pingou', 'test3.git')) tests.add_content_git_repo( - os.path.join(tests.HERE, 'docs', 'pingou', 'test3.git')) + os.path.join(self.path, 'docs', 'pingou', 'test3.git')) tests.add_content_git_repo( - os.path.join(tests.HERE, 'tickets', 'pingou', 'test3.git')) + os.path.join(self.path, 'tickets', 'pingou', 'test3.git')) # Check before deleting the fork output = self.app.get('/') @@ -2182,7 +2182,7 @@ index 0000000..fb7093d user = tests.FakeUser() with tests.user_set(pagure.APP, user): tests.create_projects(self.session) - tests.create_projects_git(tests.HERE) + tests.create_projects_git(self.path) # No project registered in the DB (no git repo) output = self.app.post('/foo/delete') @@ -2198,7 +2198,7 @@ index 0000000..fb7093d user = tests.FakeUser(username='pingou') with tests.user_set(pagure.APP, user): - tests.create_projects_git(tests.HERE) + tests.create_projects_git(self.path) ast.return_value = True output = self.app.post('/test/delete') @@ -2226,7 +2226,7 @@ index 0000000..fb7093d ) self.session.add(item) self.session.commit() - tests.create_projects_git(tests.HERE) + tests.create_projects_git(self.path) output = self.app.post('/test/delete', follow_redirects=True) self.assertEqual(output.status_code, 200) @@ -2249,8 +2249,8 @@ index 0000000..fb7093d ) self.session.add(item) self.session.commit() - tests.create_projects_git(tests.HERE) - tests.create_projects_git(os.path.join(tests.HERE, 'docs')) + tests.create_projects_git(self.path) + tests.create_projects_git(os.path.join(self.path, 'docs')) output = self.app.post('/test/delete', follow_redirects=True) self.assertEqual(output.status_code, 200) self.assertTrue( @@ -2268,12 +2268,12 @@ index 0000000..fb7093d self.session.commit() # Create all the git repos - tests.create_projects_git(tests.HERE) - tests.create_projects_git(os.path.join(tests.HERE, 'docs')) + tests.create_projects_git(self.path) + tests.create_projects_git(os.path.join(self.path, 'docs')) tests.create_projects_git( - os.path.join(tests.HERE, 'tickets'), bare=True) + os.path.join(self.path, 'tickets'), bare=True) tests.create_projects_git( - os.path.join(tests.HERE, 'requests'), bare=True) + os.path.join(self.path, 'requests'), bare=True) # Check repo was created output = self.app.get('/') @@ -2293,7 +2293,7 @@ index 0000000..fb7093d title='Test issue', content='We should work on this', user='pingou', - ticketfolder=os.path.join(tests.HERE, 'tickets') + ticketfolder=os.path.join(self.path, 'tickets') ) self.session.commit() self.assertEqual(msg.title, 'Test issue') @@ -2304,7 +2304,7 @@ index 0000000..fb7093d title='Test issue #2', content='We should work on this, really', user='pingou', - ticketfolder=os.path.join(tests.HERE, 'tickets') + ticketfolder=os.path.join(self.path, 'tickets') ) self.session.commit() self.assertEqual(msg.title, 'Test issue #2') @@ -2330,7 +2330,7 @@ index 0000000..fb7093d branch_to='master', title='test pull-request', user='pingou', - requestfolder=os.path.join(tests.HERE, 'requests'), + requestfolder=os.path.join(self.path, 'requests'), ) self.session.commit() self.assertEqual(req.id, 3) @@ -2344,7 +2344,7 @@ index 0000000..fb7093d branch_to='master', title='test pull-request', user='pingou', - requestfolder=os.path.join(tests.HERE, 'requests'), + requestfolder=os.path.join(self.path, 'requests'), ) self.session.commit() self.assertEqual(req.id, 4) @@ -2403,11 +2403,11 @@ index 0000000..fb7093d self.session.add(item) self.session.commit() tests.add_content_git_repo( - os.path.join(tests.HERE, 'forks', 'pingou', 'test3.git')) + os.path.join(self.path, 'forks', 'pingou', 'test3.git')) tests.add_content_git_repo( - os.path.join(tests.HERE, 'docs', 'pingou', 'test3.git')) + os.path.join(self.path, 'docs', 'pingou', 'test3.git')) tests.add_content_git_repo( - os.path.join(tests.HERE, 'tickets', 'pingou', 'test3.git')) + os.path.join(self.path, 'tickets', 'pingou', 'test3.git')) # Check before deleting the fork output = self.app.get('/') @@ -2434,7 +2434,7 @@ index 0000000..fb7093d """ Test the new_repo_hook_token endpoint. """ ast.return_value = False tests.create_projects(self.session) - tests.create_projects_git(tests.HERE) + tests.create_projects_git(self.path) repo = pagure.lib.get_project(self.session, 'test') self.assertEqual(repo.hook_token, 'aaabbbccc') @@ -2496,7 +2496,7 @@ index 0000000..fb7093d upgit.return_value = True sendmail.return_value = True tests.create_projects(self.session) - tests.create_projects_git(tests.HERE) + tests.create_projects_git(self.path) user = tests.FakeUser() with tests.user_set(pagure.APP, user): @@ -2588,15 +2588,15 @@ index 0000000..fb7093d # No git repo associated self.assertEqual(output.status_code, 404) - tests.create_projects_git(tests.HERE, bare=True) + tests.create_projects_git(self.path, bare=True) output = self.app.get('/test/releases') self.assertEqual(output.status_code, 200) self.assertIn('This project has not been tagged.', output.data) # Add a README to the git repo - First commit - tests.add_readme_git_repo(os.path.join(tests.HERE, 'test.git')) - repo = pygit2.Repository(os.path.join(tests.HERE, 'test.git')) + tests.add_readme_git_repo(os.path.join(self.path, 'test.git')) + repo = pygit2.Repository(os.path.join(self.path, 'test.git')) first_commit = repo.revparse_single('HEAD') tagger = pygit2.Signature('Alice Doe', 'adoe@example.com', 12347, 0) repo.create_tag( @@ -2623,7 +2623,7 @@ index 0000000..fb7093d self.assertEqual(output.status_code, 404) tests.create_projects(self.session) - tests.create_projects_git(tests.HERE, bare=True) + tests.create_projects_git(self.path, bare=True) # No a repo admin output = self.app.get('/test/edit/foo/f/sources') @@ -2641,12 +2641,12 @@ index 0000000..fb7093d self.assertEqual(output.status_code, 404) # Add some content to the git repo - tests.add_content_git_repo(os.path.join(tests.HERE, 'test.git')) - tests.add_readme_git_repo(os.path.join(tests.HERE, 'test.git')) + tests.add_content_git_repo(os.path.join(self.path, 'test.git')) + tests.add_readme_git_repo(os.path.join(self.path, 'test.git')) tests.add_binary_git_repo( - os.path.join(tests.HERE, 'test.git'), 'test.jpg') + os.path.join(self.path, 'test.git'), 'test.jpg') tests.add_binary_git_repo( - os.path.join(tests.HERE, 'test.git'), 'test_binary') + os.path.join(self.path, 'test.git'), 'test_binary') output = self.app.get('/test/edit/master/foofile') self.assertEqual(output.status_code, 404) @@ -2737,11 +2737,11 @@ index 0000000..fb7093d self.session.commit() tests.add_content_git_repo( - os.path.join(tests.HERE, 'forks', 'pingou', 'test3.git')) + os.path.join(self.path, 'forks', 'pingou', 'test3.git')) tests.add_readme_git_repo( - os.path.join(tests.HERE, 'forks', 'pingou', 'test3.git')) + os.path.join(self.path, 'forks', 'pingou', 'test3.git')) tests.add_commit_git_repo( - os.path.join(tests.HERE, 'forks', 'pingou', 'test3.git'), + os.path.join(self.path, 'forks', 'pingou', 'test3.git'), ncommits=10) output = self.app.get('/fork/pingou/test3/edit/master/f/sources') @@ -2776,7 +2776,7 @@ index 0000000..fb7093d self.assertEqual(output.status_code, 404) tests.create_projects(self.session) - repos = tests.create_projects_git(tests.HERE) + repos = tests.create_projects_git(self.path) output = self.app.post('/test/default/branch/') self.assertEqual(output.status_code, 403) @@ -2872,7 +2872,7 @@ index 0000000..fb7093d self.assertEqual(output.status_code, 404) tests.create_projects(self.session) - repo = tests.create_projects_git(tests.HERE) + repo = tests.create_projects_git(self.path) output = self.app.post('/test/upload/') self.assertEqual(output.status_code, 403) @@ -2883,7 +2883,8 @@ index 0000000..fb7093d user.username = 'pingou' with tests.user_set(pagure.APP, user): - img = os.path.join(tests.HERE, 'placebo.png') + img = os.path.join(os.path.abspath(os.path.dirname(__file__)), + 'placebo.png') # Missing CSRF Token with open(img, mode='rb') as stream: @@ -2922,7 +2923,7 @@ index 0000000..fb7093d self.assertEqual(output.status_code, 404) tests.create_projects(self.session) - tests.create_projects_git(tests.HERE, bare=True) + tests.create_projects_git(self.path, bare=True) output = self.app.get('/test/token/new/') self.assertEqual(output.status_code, 403) @@ -2988,7 +2989,7 @@ index 0000000..fb7093d self.assertEqual(output.status_code, 404) tests.create_projects(self.session) - tests.create_projects_git(tests.HERE, bare=True) + tests.create_projects_git(self.path, bare=True) output = self.app.post('/test/token/revoke/123') self.assertEqual(output.status_code, 403) @@ -3064,7 +3065,7 @@ index 0000000..fb7093d self.assertEqual(output.status_code, 404) tests.create_projects(self.session) - tests.create_projects_git(tests.HERE, bare=True) + tests.create_projects_git(self.path, bare=True) # User not logged in output = self.app.post('/test/b/master/delete') @@ -3092,7 +3093,7 @@ index 0000000..fb7093d self.assertIn('

            Branch no found

            ', output.data) # Add a branch that we can delete - path = os.path.join(tests.HERE, 'test.git') + path = os.path.join(self.path, 'test.git') tests.add_content_git_repo(path) repo = pygit2.Repository(path) repo.create_branch('foo', repo.head.get_object()) @@ -3121,7 +3122,7 @@ index 0000000..fb7093d output.data) # Add a branch with a '/' in its name that we can delete - path = os.path.join(tests.HERE, 'test.git') + path = os.path.join(self.path, 'test.git') tests.add_content_git_repo(path) repo = pygit2.Repository(path) repo.create_branch('feature/foo', repo.head.get_object()) @@ -3162,7 +3163,7 @@ index 0000000..fb7093d # No git repo associated self.assertEqual(output.status_code, 404) - tests.create_projects_git(tests.HERE, bare=True) + tests.create_projects_git(self.path, bare=True) output = self.app.get('/docs/test/') self.assertEqual(output.status_code, 404) @@ -3170,7 +3171,7 @@ index 0000000..fb7093d def test_view_project_activity(self): """ Test the view_project_activity endpoint. """ tests.create_projects(self.session) - tests.create_projects_git(tests.HERE, bare=True) + tests.create_projects_git(self.path, bare=True) # Project Exists, but No DATAGREPPER_URL set output = self.app.get('/test/activity/') @@ -3196,7 +3197,7 @@ index 0000000..fb7093d self.assertEqual(output.status_code, 405) tests.create_projects(self.session) - tests.create_projects_git(tests.HERE, bare=True) + tests.create_projects_git(self.path, bare=True) user = tests.FakeUser() user.username = 'pingou' @@ -3246,7 +3247,7 @@ index 0000000..fb7093d ) self.session.add(item) self.session.commit() - gitrepo = os.path.join(tests.HERE, 'forks', 'foo', 'test.git') + gitrepo = os.path.join(self.path, 'forks', 'foo', 'test.git') pygit2.init_repository(gitrepo, bare=True) output = self.app.post( diff --git a/tests/test_pagure_flask_ui_repo_slash_name.py b/tests/test_pagure_flask_ui_repo_slash_name.py index 5ca66c3..8a07ce4 100644 --- a/tests/test_pagure_flask_ui_repo_slash_name.py +++ b/tests/test_pagure_flask_ui_repo_slash_name.py @@ -46,21 +46,21 @@ class PagureFlaskSlashInNametests(tests.Modeltests): pagure.ui.repo.SESSION = self.session pagure.ui.issues.SESSION = self.session - pagure.APP.config['GIT_FOLDER'] = os.path.join(tests.HERE, 'repos') - pagure.APP.config['FORK_FOLDER'] = os.path.join(tests.HERE, 'forks') + pagure.APP.config['GIT_FOLDER'] = os.path.join(self.path, 'repos') + pagure.APP.config['FORK_FOLDER'] = os.path.join(self.path, 'forks') pagure.APP.config['TICKETS_FOLDER'] = os.path.join( - tests.HERE, 'tickets') + self.path, 'tickets') pagure.APP.config['DOCS_FOLDER'] = os.path.join( - tests.HERE, 'docs') + self.path, 'docs') pagure.APP.config['REQUESTS_FOLDER'] = os.path.join( - tests.HERE, 'requests') + self.path, 'requests') self.app = pagure.APP.test_client() def set_up_git_repo(self, name='test'): """ Set up the git repo to play with. """ # Create a git repo to play with - gitrepo = os.path.join(tests.HERE, 'repos', '%s.git' % name) + gitrepo = os.path.join(self.path, 'repos', '%s.git' % name) repo = pygit2.init_repository(gitrepo, bare=True) newpath = tempfile.mkdtemp(prefix='pagure-other-test') @@ -106,7 +106,7 @@ class PagureFlaskSlashInNametests(tests.Modeltests): self.assertEqual(output.status_code, 404) # Create a git repo to play with - gitrepo = os.path.join(tests.HERE, 'repos', 'test.git') + gitrepo = os.path.join(self.path, 'repos', 'test.git') repo = pygit2.init_repository(gitrepo, bare=True) # With git repo @@ -150,7 +150,7 @@ class PagureFlaskSlashInNametests(tests.Modeltests): self.session.commit() # Create a git repo to play with - gitrepo = os.path.join(tests.HERE, 'repos', 'forks/test.git') + gitrepo = os.path.join(self.path, 'repos', 'forks/test.git') repo = pygit2.init_repository(gitrepo, bare=True) output = self.app.get('/forks/test') @@ -243,7 +243,7 @@ class PagureFlaskSlashInNametests(tests.Modeltests): output.data) # Try accessing the commit - gitrepo = os.path.join(tests.HERE, 'repos', 'forks/test.git') + gitrepo = os.path.join(self.path, 'repos', 'forks/test.git') repo = pygit2.Repository(gitrepo) master_branch = repo.lookup_branch('master') first_commit = master_branch.get_object().hex diff --git a/tests/test_pagure_flask_ui_roadmap.py b/tests/test_pagure_flask_ui_roadmap.py index 0cca6ec..06bdd16 100644 --- a/tests/test_pagure_flask_ui_roadmap.py +++ b/tests/test_pagure_flask_ui_roadmap.py @@ -45,13 +45,13 @@ class PagureFlaskRoadmaptests(tests.Modeltests): pagure.ui.repo.SESSION = self.session pagure.ui.issues.SESSION = self.session - pagure.APP.config['GIT_FOLDER'] = tests.HERE + pagure.APP.config['GIT_FOLDER'] = self.path pagure.APP.config['REQUESTS_FOLDER'] = os.path.join( - tests.HERE, 'requests') + self.path, 'requests') pagure.APP.config['TICKETS_FOLDER'] = os.path.join( - tests.HERE, 'tickets') + self.path, 'tickets') pagure.APP.config['DOCS_FOLDER'] = os.path.join( - tests.HERE, 'docs') + self.path, 'docs') self.app = pagure.APP.test_client() @patch('pagure.lib.git.update_git') @@ -62,7 +62,7 @@ class PagureFlaskRoadmaptests(tests.Modeltests): p_ugt.return_value = True tests.create_projects(self.session) - tests.create_projects_git(os.path.join(tests.HERE), bare=True) + tests.create_projects_git(os.path.join(self.path), bare=True) user = tests.FakeUser() user.username = 'pingou' @@ -105,7 +105,7 @@ class PagureFlaskRoadmaptests(tests.Modeltests): p_ugt.return_value = True tests.create_projects(self.session) - tests.create_projects_git(os.path.join(tests.HERE), bare=True) + tests.create_projects_git(os.path.join(self.path), bare=True) # Set some milestone repo = pagure.lib.get_project(self.session, 'test') @@ -166,7 +166,7 @@ class PagureFlaskRoadmaptests(tests.Modeltests): def test_update_milestones(self): """ Test updating milestones of a repo. """ tests.create_projects(self.session) - tests.create_projects_git(os.path.join(tests.HERE), bare=True) + tests.create_projects_git(os.path.join(self.path), bare=True) # Set some milestones repo = pagure.lib.get_project(self.session, 'test') @@ -340,7 +340,7 @@ class PagureFlaskRoadmaptests(tests.Modeltests): def test_milestones_without_dates(self, p_send_email, p_ugt): """ Test creating two milestones with no dates. """ tests.create_projects(self.session) - tests.create_projects_git(os.path.join(tests.HERE), bare=True) + tests.create_projects_git(os.path.join(self.path), bare=True) user = tests.FakeUser() user.username = 'pingou' diff --git a/tests/test_pagure_flask_ui_slash_branch_name.py b/tests/test_pagure_flask_ui_slash_branch_name.py index b1971a4..2d20ece 100644 --- a/tests/test_pagure_flask_ui_slash_branch_name.py +++ b/tests/test_pagure_flask_ui_slash_branch_name.py @@ -45,21 +45,21 @@ class PagureFlaskSlashInBranchtests(tests.Modeltests): pagure.ui.fork.SESSION = self.session pagure.ui.repo.SESSION = self.session - pagure.APP.config['GIT_FOLDER'] = os.path.join(tests.HERE, 'repos') - pagure.APP.config['FORK_FOLDER'] = os.path.join(tests.HERE, 'forks') + pagure.APP.config['GIT_FOLDER'] = os.path.join(self.path, 'repos') + pagure.APP.config['FORK_FOLDER'] = os.path.join(self.path, 'forks') pagure.APP.config['TICKETS_FOLDER'] = os.path.join( - tests.HERE, 'tickets') + self.path, 'tickets') pagure.APP.config['DOCS_FOLDER'] = os.path.join( - tests.HERE, 'docs') + self.path, 'docs') pagure.APP.config['REQUESTS_FOLDER'] = os.path.join( - tests.HERE, 'requests') + self.path, 'requests') self.app = pagure.APP.test_client() def set_up_git_repo(self): """ Set up the git repo to play with. """ # Create a git repo to play with - gitrepo = os.path.join(tests.HERE, 'repos', 'test.git') + gitrepo = os.path.join(self.path, 'repos', 'test.git') repo = pygit2.init_repository(gitrepo, bare=True) newpath = tempfile.mkdtemp(prefix='pagure-other-test') diff --git a/tests/test_pagure_lib.py b/tests/test_pagure_lib.py index 09fb942..cb2d7ba 100644 --- a/tests/test_pagure_lib.py +++ b/tests/test_pagure_lib.py @@ -727,11 +727,6 @@ class PagureLibtests(tests.Modeltests): ticketfolder = os.path.join(self.path, 'tickets') requestfolder = os.path.join(self.path, 'requests') - os.mkdir(gitfolder) - os.mkdir(docfolder) - os.mkdir(ticketfolder) - os.mkdir(requestfolder) - # Try creating a blacklisted project self.assertRaises( pagure.exceptions.PagureException, @@ -1146,10 +1141,6 @@ class PagureLibtests(tests.Modeltests): ticketfolder = os.path.join(self.path, 'tickets') requestfolder = os.path.join(self.path, 'requests') - os.mkdir(gitfolder) - os.mkdir(docfolder) - os.mkdir(ticketfolder) - projects = pagure.lib.search_projects(self.session) self.assertEqual(len(projects), 0) diff --git a/tests/test_pagure_lib_git.py b/tests/test_pagure_lib_git.py index b3fc048..c37fd29 100644 --- a/tests/test_pagure_lib_git.py +++ b/tests/test_pagure_lib_git.py @@ -38,15 +38,15 @@ class PagureLibGittests(tests.Modeltests): pagure.lib.git.SESSION = self.session pagure.APP.config['GIT_FOLDER'] = os.path.join( - tests.HERE, 'repos') + self.path, 'repos') pagure.APP.config['FORK_FOLDER'] = os.path.join( - tests.HERE, 'forks') + self.path, 'forks') pagure.APP.config['TICKETS_FOLDER'] = os.path.join( - tests.HERE, 'tickets') + self.path, 'tickets') pagure.APP.config['DOCS_FOLDER'] = os.path.join( - tests.HERE, 'docs') + self.path, 'docs') pagure.APP.config['REQUESTS_FOLDER'] = os.path.join( - tests.HERE, 'requests') + self.path, 'requests') def test_write_gitolite_acls(self): """ Test the write_gitolite_acls function of pagure.lib.git. """ @@ -74,7 +74,7 @@ class PagureLibGittests(tests.Modeltests): self.session.add(item) self.session.commit() - outputconf = os.path.join(tests.HERE, 'test_gitolite.conf') + outputconf = os.path.join(self.path, 'test_gitolite.conf') pagure.lib.git.write_gitolite_acls(self.session, outputconf) @@ -210,7 +210,7 @@ repo requests/forks/pingou/test3 self.session.add(item) self.session.commit() - outputconf = os.path.join(tests.HERE, 'test_gitolite.conf') + outputconf = os.path.join(self.path, 'test_gitolite.conf') pagure.lib.git.write_gitolite_acls(self.session, outputconf) @@ -282,7 +282,7 @@ repo requests/forks/pingou/test2 def test_commit_to_patch(self): """ Test the commit_to_patch function of pagure.lib.git. """ # Create a git repo to play with - self.gitrepo = os.path.join(tests.HERE, 'test_repo.git') + self.gitrepo = os.path.join(self.path, 'test_repo.git') os.makedirs(self.gitrepo) repo = pygit2.init_repository(self.gitrepo) @@ -441,7 +441,7 @@ index 9f44358..2a552bb 100644 self.session.commit() # Create repo - self.gitrepo = os.path.join(tests.HERE, 'test_ticket_repo.git') + self.gitrepo = os.path.join(self.path, 'test_ticket_repo.git') os.makedirs(self.gitrepo) repo_obj = pygit2.init_repository(self.gitrepo, bare=True) @@ -453,11 +453,11 @@ index 9f44358..2a552bb 100644 title='Test issue', content='We should work on this', user='pingou', - ticketfolder=tests.HERE + ticketfolder=self.path ) self.assertEqual(msg.title, 'Test issue') issue = pagure.lib.search_issues(self.session, repo, issueid=1) - pagure.lib.git.update_git(issue, repo, tests.HERE) + pagure.lib.git.update_git(issue, repo, self.path) repo = pygit2.Repository(self.gitrepo) commit = repo.revparse_single('HEAD') @@ -541,7 +541,7 @@ index 0000000..60f7480 issue=issue, comment='Hey look a comment!', user='foo', - ticketfolder=tests.HERE + ticketfolder=self.path ) self.session.commit() self.assertEqual(msg, 'Comment added') @@ -628,7 +628,7 @@ index 458821a..77674a8 self.test_update_git() - gitpath = os.path.join(tests.HERE, 'test_ticket_repo.git') + gitpath = os.path.join(self.path, 'test_ticket_repo.git') gitrepo = pygit2.init_repository(gitpath, bare=True) # Get the uid of the ticket created @@ -646,7 +646,7 @@ index 458821a..77674a8 repo = pagure.lib.get_project(self.session, 'test_ticket_repo') issue = pagure.lib.search_issues(self.session, repo, issueid=1) - pagure.lib.git.clean_git(issue, repo, tests.HERE) + pagure.lib.git.clean_git(issue, repo, self.path) # No more files in the git repo commit = gitrepo.revparse_single('HEAD') @@ -669,7 +669,7 @@ index 458821a..77674a8 self.session.commit() # Create repo - self.gitrepo = os.path.join(tests.HERE, 'test_ticket_repo.git') + self.gitrepo = os.path.join(self.path, 'test_ticket_repo.git') os.makedirs(self.gitrepo) repo_obj = pygit2.init_repository(self.gitrepo, bare=True) @@ -683,7 +683,7 @@ index 458821a..77674a8 branch_to='master', title='test PR', user='pingou', - requestfolder=tests.HERE, + requestfolder=self.path, requestuid='foobar', requestid=None, status='Open', @@ -694,7 +694,7 @@ index 458821a..77674a8 request = repo.requests[0] self.assertEqual(request.title, 'test PR') - pagure.lib.git.update_git(request, request.project, tests.HERE) + pagure.lib.git.update_git(request, request.project, self.path) repo = pygit2.Repository(self.gitrepo) commit = repo.revparse_single('HEAD') @@ -962,7 +962,7 @@ index 0000000..60f7480 def test_update_request_from_git(self): """ Test the update_request_from_git method from pagure.lib.git. """ tests.create_projects(self.session) - tests.create_projects_git(os.path.join(tests.HERE, 'repos')) + tests.create_projects_git(os.path.join(self.path, 'repos')) repo = pagure.lib.get_project(self.session, 'test') @@ -1110,10 +1110,10 @@ index 0000000..60f7480 username=None, request_uid='d4182a2ac2d541d884742d3037c26e56', json_data=data, - gitfolder=tests.HERE, - docfolder=os.path.join(tests.HERE, 'docs'), - ticketfolder=os.path.join(tests.HERE, 'tickets'), - requestfolder=os.path.join(tests.HERE, 'requests') + gitfolder=self.path, + docfolder=os.path.join(self.path, 'docs'), + ticketfolder=os.path.join(self.path, 'tickets'), + requestfolder=os.path.join(self.path, 'requests') ) pagure.lib.git.update_request_from_git( @@ -1123,10 +1123,10 @@ index 0000000..60f7480 username=None, request_uid='d4182a2ac2d541d884742d3037c26e56', json_data=data, - gitfolder=tests.HERE, - docfolder=os.path.join(tests.HERE, 'docs'), - ticketfolder=os.path.join(tests.HERE, 'tickets'), - requestfolder=os.path.join(tests.HERE, 'requests') + gitfolder=self.path, + docfolder=os.path.join(self.path, 'docs'), + ticketfolder=os.path.join(self.path, 'tickets'), + requestfolder=os.path.join(self.path, 'requests') ) self.session.commit() @@ -1227,10 +1227,10 @@ index 0000000..60f7480 username=None, request_uid='d4182a2ac2d541d884742d3037c26e57', json_data=data, - gitfolder=tests.HERE, - docfolder=os.path.join(tests.HERE, 'docs'), - ticketfolder=os.path.join(tests.HERE, 'tickets'), - requestfolder=os.path.join(tests.HERE, 'requests') + gitfolder=self.path, + docfolder=os.path.join(self.path, 'docs'), + ticketfolder=os.path.join(self.path, 'tickets'), + requestfolder=os.path.join(self.path, 'requests') ) self.session.commit() @@ -1252,7 +1252,7 @@ index 0000000..60f7480 """ Test the read_git_lines method of pagure.lib.git. """ self.test_update_git() - gitrepo = os.path.join(tests.HERE, 'test_ticket_repo.git') + gitrepo = os.path.join(self.path, 'test_ticket_repo.git') output = pagure.lib.git.read_git_lines( ['log', '-1', "--pretty='%s'"], gitrepo) self.assertEqual(len(output), 1) @@ -1276,7 +1276,7 @@ index 0000000..60f7480 self.test_update_git() - gitrepo = os.path.join(tests.HERE, 'test_ticket_repo.git') + gitrepo = os.path.join(self.path, 'test_ticket_repo.git') output = pagure.lib.git.read_git_lines( ['log', '-3', "--pretty='%H'"], gitrepo) self.assertEqual(len(output), 2) @@ -1343,7 +1343,7 @@ index 0000000..60f7480 self.test_update_git() - gitrepo = os.path.join(tests.HERE, 'test_ticket_repo.git') + gitrepo = os.path.join(self.path, 'test_ticket_repo.git') output = pagure.lib.git.read_git_lines( ['log', '-3', "--pretty='%H'"], gitrepo) self.assertEqual(len(output), 2) @@ -1357,7 +1357,7 @@ index 0000000..60f7480 self.test_update_git() - gitrepo = os.path.join(tests.HERE, 'test_ticket_repo.git') + gitrepo = os.path.join(self.path, 'test_ticket_repo.git') output = pagure.lib.git.read_git_lines( ['log', '-3', "--pretty='%H'"], gitrepo) self.assertEqual(len(output), 2) @@ -1368,7 +1368,7 @@ index 0000000..60f7480 def test_get_repo_name(self): """ Test the get_repo_name method of pagure.lib.git. """ - gitrepo = os.path.join(tests.HERE, 'test_ticket_repo.git') + gitrepo = os.path.join(self.path, 'test_ticket_repo.git') repo_name = pagure.lib.git.get_repo_name(gitrepo) self.assertEqual(repo_name, 'test_ticket_repo') @@ -1380,7 +1380,7 @@ index 0000000..60f7480 def test_get_username(self): """ Test the get_username method of pagure.lib.git. """ - gitrepo = os.path.join(tests.HERE, 'test_ticket_repo.git') + gitrepo = os.path.join(self.path, 'test_ticket_repo.git') repo_name = pagure.lib.git.get_username(gitrepo) self.assertEqual(repo_name, None) @@ -1391,45 +1391,45 @@ index 0000000..60f7480 self.assertEqual(repo_name, None) repo_name = pagure.lib.git.get_username( - os.path.join(tests.HERE, 'forks', 'pingou', 'foo.test.git')) + os.path.join(self.path, 'forks', 'pingou', 'foo.test.git')) self.assertEqual(repo_name, 'pingou') repo_name = pagure.lib.git.get_username( - os.path.join(tests.HERE, 'forks', 'pingou', 'bar/foo.test.git')) + os.path.join(self.path, 'forks', 'pingou', 'bar/foo.test.git')) self.assertEqual(repo_name, 'pingou') repo_name = pagure.lib.git.get_username(os.path.join( - tests.HERE, 'forks', 'pingou', 'fooo/bar/foo.test.git')) + self.path, 'forks', 'pingou', 'fooo/bar/foo.test.git')) self.assertEqual(repo_name, 'pingou') def test_get_repo_namespace(self): """ Test the get_repo_namespace method of pagure.lib.git. """ repo_name = pagure.lib.git.get_repo_namespace( - os.path.join(tests.HERE, 'repos', 'test_ticket_repo.git')) + os.path.join(self.path, 'repos', 'test_ticket_repo.git')) self.assertEqual(repo_name, None) repo_name = pagure.lib.git.get_repo_namespace( - os.path.join(tests.HERE, 'repos', 'foo/bar/baz/test.git')) + os.path.join(self.path, 'repos', 'foo/bar/baz/test.git')) self.assertEqual(repo_name, 'foo/bar/baz') repo_name = pagure.lib.git.get_repo_namespace( - os.path.join(tests.HERE, 'repos', 'foo.test.git')) + os.path.join(self.path, 'repos', 'foo.test.git')) self.assertEqual(repo_name, None) repo_name = pagure.lib.git.get_repo_namespace(os.path.join( - tests.HERE, 'repos', 'forks', 'user', 'foo.test.git')) + self.path, 'repos', 'forks', 'user', 'foo.test.git')) self.assertEqual(repo_name, None) repo_name = pagure.lib.git.get_repo_namespace(os.path.join( - tests.HERE, 'repos', 'forks', 'user', 'bar/foo.test.git')) + self.path, 'repos', 'forks', 'user', 'bar/foo.test.git')) self.assertEqual(repo_name, 'bar') repo_name = pagure.lib.git.get_repo_namespace(os.path.join( - tests.HERE, 'repos', 'forks', 'user', 'ns/bar/foo.test.git')) + self.path, 'repos', 'forks', 'user', 'ns/bar/foo.test.git')) self.assertEqual(repo_name, 'ns/bar') repo_name = pagure.lib.git.get_repo_namespace(os.path.join( - tests.HERE, 'repos', 'forks', 'user', '/bar/foo.test.git')) + self.path, 'repos', 'forks', 'user', '/bar/foo.test.git')) self.assertEqual(repo_name, 'bar') diff --git a/tests/test_pagure_lib_git_get_tags_objects.py b/tests/test_pagure_lib_git_get_tags_objects.py index 0532273..9c05d0e 100644 --- a/tests/test_pagure_lib_git_get_tags_objects.py +++ b/tests/test_pagure_lib_git_get_tags_objects.py @@ -30,12 +30,12 @@ def get_tag_name(tags): return output -def add_repo_tag(repo, tags, repo_name): +def add_repo_tag(git_dir, repo, tags, repo_name): """ Use a list to create multiple tags on a git repo """ for tag in reversed(tags): time.sleep(1) tests.add_commit_git_repo( - os.path.join(tests.HERE, 'repos', repo_name), + os.path.join(git_dir, 'repos', repo_name), ncommits=1) first_commit = repo.revparse_single('HEAD') tagger = pygit2.Signature('Alice Doe', 'adoe@example.com', 12347, 0) @@ -52,20 +52,20 @@ class PagureLibGitGetTagstests(tests.Modeltests): pagure.lib.git.SESSION = self.session pagure.APP.config['GIT_FOLDER'] = os.path.join( - tests.HERE, 'repos') + self.path, 'repos') pagure.APP.config['FORK_FOLDER'] = os.path.join( - tests.HERE, 'forks') + self.path, 'forks') pagure.APP.config['TICKETS_FOLDER'] = os.path.join( - tests.HERE, 'tickets') + self.path, 'tickets') pagure.APP.config['DOCS_FOLDER'] = os.path.join( - tests.HERE, 'docs') + self.path, 'docs') pagure.APP.config['REQUESTS_FOLDER'] = os.path.join( - tests.HERE, 'requests') + self.path, 'requests') def test_get_git_tags_objects(self): """ Test the get_git_tags_objects method of pagure.lib.git. """ tests.create_projects(self.session) - tests.create_projects_git(os.path.join(tests.HERE, 'repos'), bare=True) + tests.create_projects_git(os.path.join(self.path, 'repos'), bare=True) project = pagure.lib.get_project(self.session, 'test') # Case 1 - Empty repo with no tags @@ -74,9 +74,9 @@ class PagureLibGitGetTagstests(tests.Modeltests): self.assertEqual(exp, get_tag_name(tags)) tests.add_readme_git_repo(os.path.join(os.path.join( - tests.HERE, 'repos'), 'test.git')) + self.path, 'repos'), 'test.git')) repo = pygit2.Repository(os.path.join(os.path.join( - tests.HERE, 'repos'), 'test.git')) + self.path, 'repos'), 'test.git')) # Case 2 - Repo with one commit and no tags exp = [] @@ -86,20 +86,20 @@ class PagureLibGitGetTagstests(tests.Modeltests): # Case 3 - Simple sort exp = ['0.1.0', 'test-0.0.21', '0.0.12-beta', '0.0.12-alpha', '0.0.12', '0.0.11', '0.0.3', 'foo-0.0.2', '0.0.1'] - add_repo_tag(repo, exp, 'test.git') + add_repo_tag(self.path, repo, exp, 'test.git') tags = pagure.lib.git.get_git_tags_objects(project) self.assertEqual(exp, get_tag_name(tags)) # Case 4 - Sorting with different splitting characters project = pagure.lib.get_project(self.session, 'test2') tests.add_readme_git_repo(os.path.join(os.path.join( - tests.HERE, 'repos'), 'test2.git')) + self.path, 'repos'), 'test2.git')) repo = pygit2.Repository(os.path.join(os.path.join( - tests.HERE, 'repos'), 'test2.git')) + self.path, 'repos'), 'test2.git')) exp = ['1.0-0_2', '1.0-0_1', '0.1-1_0', '0.1-0_0', '0.0-2_0', '0.0-1_34', '0.0-1_11', '0.0-1_3', '0.0-1_2', '0.0-1_1'] - add_repo_tag(repo, exp, 'test2.git') + add_repo_tag(self.path, repo, exp, 'test2.git') tags = pagure.lib.git.get_git_tags_objects(project) self.assertEqual(exp, get_tag_name(tags)) diff --git a/tests/test_zzz_pagure_flask_ui_old_commit.py b/tests/test_zzz_pagure_flask_ui_old_commit.py index 87e7309..43b1ade 100644 --- a/tests/test_zzz_pagure_flask_ui_old_commit.py +++ b/tests/test_zzz_pagure_flask_ui_old_commit.py @@ -53,17 +53,17 @@ class PagureFlaskRepoOldUrltests(tests.Modeltests): pagure.APP.config['OLD_VIEW_COMMIT_ENABLED'] = True pagure.APP.config['EMAIL_SEND'] = False - pagure.APP.config['GIT_FOLDER'] = tests.HERE + pagure.APP.config['GIT_FOLDER'] = self.path pagure.APP.config['FORK_FOLDER'] = os.path.join( - tests.HERE, 'forks') + self.path, 'forks') pagure.APP.config['REQUESTS_FOLDER'] = os.path.join( - tests.HERE, 'requests') + self.path, 'requests') pagure.APP.config['TICKETS_FOLDER'] = os.path.join( - tests.HERE, 'tickets') + self.path, 'tickets') pagure.APP.config['DOCS_FOLDER'] = os.path.join( - tests.HERE, 'docs') + self.path, 'docs') pagure.APP.config['UPLOAD_FOLDER_PATH'] = os.path.join( - tests.HERE, 'releases') + self.path, 'releases') self.app = pagure.APP.test_client() def tearDown(self): @@ -79,11 +79,11 @@ class PagureFlaskRepoOldUrltests(tests.Modeltests): """ Test the view_commit_old endpoint. """ tests.create_projects(self.session) - tests.create_projects_git(tests.HERE, bare=True) + tests.create_projects_git(self.path, bare=True) # Add a README to the git repo - First commit - tests.add_readme_git_repo(os.path.join(tests.HERE, 'test.git')) - repo = pygit2.Repository(os.path.join(tests.HERE, 'test.git')) + tests.add_readme_git_repo(os.path.join(self.path, 'test.git')) + repo = pygit2.Repository(os.path.join(self.path, 'test.git')) commit = repo.revparse_single('HEAD') # View first commit @@ -119,9 +119,9 @@ class PagureFlaskRepoOldUrltests(tests.Modeltests): '+ ======' in output.data) # Add some content to the git repo - tests.add_content_git_repo(os.path.join(tests.HERE, 'test.git')) + tests.add_content_git_repo(os.path.join(self.path, 'test.git')) - repo = pygit2.Repository(os.path.join(tests.HERE, 'test.git')) + repo = pygit2.Repository(os.path.join(self.path, 'test.git')) commit = repo.revparse_single('HEAD') # View another commit @@ -159,7 +159,7 @@ class PagureFlaskRepoOldUrltests(tests.Modeltests): self.session.add(item) self.session.commit() forkedgit = os.path.join( - tests.HERE, 'forks', 'pingou', 'test3.git') + self.path, 'forks', 'pingou', 'test3.git') tests.add_content_git_repo(forkedgit) tests.add_readme_git_repo(forkedgit) From 374115e782c4cf82cb7bd6547cc518473262efba Mon Sep 17 00:00:00 2001 From: Lubomír Sedlář Date: Oct 05 2016 16:39:26 +0000 Subject: [PATCH 299/635] Use long dash in footer instead of two short ones This way there is no gap in between them. --- diff --git a/pagure/templates/master.html b/pagure/templates/master.html index 721514f..e622b64 100644 --- a/pagure/templates/master.html +++ b/pagure/templates/master.html @@ -120,7 +120,7 @@

            Copyright © 2014-2016 Red Hat - pagure -- + pagure — {{version}}

            SSH Hostkey/Fingerprint

            From da0fcb833c119fa5b4ab42f113906a15ce4c55a3 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Oct 06 2016 09:47:24 +0000 Subject: [PATCH 300/635] Add a welcome screen to new users of pagure Fixes https://pagure.io/pagure/issue/1234 --- diff --git a/pagure/__init__.py b/pagure/__init__.py index 66274b6..591d27e 100644 --- a/pagure/__init__.py +++ b/pagure/__init__.py @@ -96,6 +96,11 @@ if APP.config.get('PAGURE_AUTH', None) in ['fas', 'openid']: @FAS.postlogin def set_user(return_url): ''' After login method. ''' + flask.session['_new_user'] = False + if not pagure.lib.search_user( + SESSION, username=flask.g.fas_user.username): + flask.session['_new_user'] = True + try: pagure.lib.set_up_user( session=SESSION, @@ -353,12 +358,19 @@ def inject_variables(): namespace=namespace) return watch + new_user = False + if flask.session.get('_new_user'): + new_user = True + flask.flash('Welcome to pagure') + flask.session['_new_user'] = False + return dict( version=__version__, admin=user_admin, authenticated=authenticated(), forkbuttonform=forkbuttonform, is_watching=is_watching, + new_user=new_user, ) diff --git a/pagure/templates/master.html b/pagure/templates/master.html index e622b64..7d71fcc 100644 --- a/pagure/templates/master.html +++ b/pagure/templates/master.html @@ -127,6 +127,42 @@
            + {% if new_user %} + + {% endif %} + {% block jscripts %} + {% if new_user %} + + {% endif %} {% endblock %} {% if config['FEDMENU_URL'] %} From c2a3631f7031be8a06566a4b870ba69a8a65354c Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Oct 06 2016 09:47:24 +0000 Subject: [PATCH 301/635] Small grammar improvements suggested by @tibbs, thanks! --- diff --git a/pagure/templates/master.html b/pagure/templates/master.html index 7d71fcc..c00cfc8 100644 --- a/pagure/templates/master.html +++ b/pagure/templates/master.html @@ -137,16 +137,18 @@ aria-label="Close"> - + {% endif %} {% endwith %} + {% if new_user %} +
            +
            + +
            +
            + {% endif %} {% block content %}{% endblock %} @@ -127,44 +157,6 @@ - {% if new_user %} - - {% endif %} - {% block jscripts %} - {% if new_user %} - - {% endif %} {% endblock %} {% if config['FEDMENU_URL'] %} From 2eacfa5a833f9fcf963b778d35a313a5145c555d Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Oct 06 2016 09:47:24 +0000 Subject: [PATCH 303/635] Drop this flash message, duplicates the welcome screen --- diff --git a/pagure/__init__.py b/pagure/__init__.py index 591d27e..45fed05 100644 --- a/pagure/__init__.py +++ b/pagure/__init__.py @@ -361,7 +361,6 @@ def inject_variables(): new_user = False if flask.session.get('_new_user'): new_user = True - flask.flash('Welcome to pagure') flask.session['_new_user'] = False return dict( From bc0554cb86f9b08ca7adc062ae3a955a346adedd Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Oct 06 2016 09:57:47 +0000 Subject: [PATCH 304/635] Make the instance name configurable and reword a little the welcome screen --- diff --git a/doc/configuration.rst b/doc/configuration.rst index 0fdffef..f3e0e3b 100644 --- a/doc/configuration.rst +++ b/doc/configuration.rst @@ -530,3 +530,15 @@ the only CI service supported at the moment). Defaults to: ``None``. .. warning:: Requires `Redis` to be configured and running. + + +INSTANCE_NAME +~~~~~~~~~~~~~ + +This allows giving a name to this running instance of pagure. The name is +then used in the welcome screen showns upon first login. + +Defaults to: ``Pagure`` + +.. note: the welcome screen currently does not work with the `local` + authentication. diff --git a/pagure/default_config.py b/pagure/default_config.py index 932d4d5..6b03967 100644 --- a/pagure/default_config.py +++ b/pagure/default_config.py @@ -21,6 +21,10 @@ SECRET_KEY = '' # url to the database server: DB_URL = 'sqlite:////var/tmp/pagure_dev.sqlite' +# Name the instance, used in the welcome screen upon first login (not +# working with `local` auth) +INSTANCE_NAME = 'Pagure' + # url to datagrepper (optional): #DATAGREPPER_URL = 'https://apps.fedoraproject.org/datagrepper' #DATAGREPPER_CATEGORY = 'pagure' diff --git a/pagure/templates/master.html b/pagure/templates/master.html index e227a20..d20f9be 100644 --- a/pagure/templates/master.html +++ b/pagure/templates/master.html @@ -116,20 +116,23 @@