From 27896af3a1a92ed28ed641843fb4a6e485d565cc Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: May 09 2016 08:04:57 +0000 Subject: [PATCH 1/14] Fix indentation and trailing spaces where we detect the DBMS used --- diff --git a/pagure/lib/model.py b/pagure/lib/model.py index 601a644..17e9437 100644 --- a/pagure/lib/model.py +++ b/pagure/lib/model.py @@ -47,8 +47,8 @@ def create_tables(db_url, alembic_ini=None, acls=None, debug=False): :return a session that can be used to query the database. """ - if db_url.startswith('sqlite'): - engine = create_engine(db_url, echo=debug) + if db_url.startswith('sqlite'): + engine = create_engine(db_url, echo=debug) else: engine = create_engine(db_url, echo=debug, client_encoding='utf8') From 4557f80d9e5cfae085cae949910ff7dfd02add7a Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: May 09 2016 08:04:57 +0000 Subject: [PATCH 2/14] Add a milestones field to the project --- diff --git a/pagure/lib/model.py b/pagure/lib/model.py index 17e9437..c325c32 100644 --- a/pagure/lib/model.py +++ b/pagure/lib/model.py @@ -289,6 +289,7 @@ class Project(BASE): sa.ForeignKey('projects.id', onupdate='CASCADE'), nullable=True) _priorities = sa.Column(sa.Text, nullable=True) + _milestones = sa.Column(sa.Text, nullable=True) date_created = sa.Column(sa.DateTime, nullable=False, default=datetime.datetime.utcnow) @@ -374,6 +375,23 @@ class Project(BASE): self._settings = json.dumps(settings) @property + def milestones(self): + """ Return the dict stored as string in the database as an actual + dict object. + """ + milestones = {} + + if self._milestones: + milestones = json.loads(self._milestones) + + return milestones + + @milestones.setter + def milestones(self, milestones): + ''' Ensures the milestones are properly saved. ''' + self._milestones = json.dumps(milestones) + + @property def priorities(self): """ Return the dict stored as string in the database as an actual dict object. From 6667f60b0a5543f0a9fd9a3300b3e621fe271a4c Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: May 09 2016 08:04:57 +0000 Subject: [PATCH 3/14] Add an alembic revision adding the milestones field to the projects --- diff --git a/alembic/versions/2aa7b3958bc5_add_the_milestones_column.py b/alembic/versions/2aa7b3958bc5_add_the_milestones_column.py new file mode 100644 index 0000000..5a00dbb --- /dev/null +++ b/alembic/versions/2aa7b3958bc5_add_the_milestones_column.py @@ -0,0 +1,31 @@ +"""Add the milestones column + +Revision ID: 2aa7b3958bc5 +Revises: 443e090da188 +Create Date: 2016-05-03 15:59:04.992414 + +""" + +# revision identifiers, used by Alembic. +revision = '2aa7b3958bc5' +down_revision = '443e090da188' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + ''' Add the column _milestones to the table projects + and the column milestone to the table issues. + ''' + op.add_column( + 'projects', + sa.Column('_milestones', sa.Text, nullable=True) + ) + + +def downgrade(): + ''' Drop the column _milestones from the table projects + and the column milestone from the table issues. + ''' + op.drop_column('projects', '_milestones') From ccf782e4964b14b3dbb40744774615f58492f79f Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: May 09 2016 08:04:57 +0000 Subject: [PATCH 4/14] Add an endpoint to save the milestones added to a project in the settings page --- diff --git a/pagure/ui/repo.py b/pagure/ui/repo.py index e1e5882..d73e14f 100644 --- a/pagure/ui/repo.py +++ b/pagure/ui/repo.py @@ -1031,6 +1031,88 @@ def update_priorities(repo, username=None): 'view_settings', username=username, repo=repo.name)) +@APP.route('//update/milestones', methods=['POST']) +@APP.route('/fork///update/milestones', methods=['POST']) +@login_required +def update_milestones(repo, username=None): + """ Update the milestones 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) + 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') + + if not repo.settings.get('issue_tracker', True): + flask.abort(404, 'No issue tracker found for this project') + + if not is_repo_admin(repo): + 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(): + milestones = [ + w.strip() for w in flask.request.form.getlist('milestones') + if w.strip() + ] + + milestone_dates = [ + p.strip() for p in flask.request.form.getlist('milestone_dates') + ] + + if len(milestones) != len(milestone_dates): + flask.flash( + 'Milestones and dates are not of the same length', + 'error') + error = True + + for milestone in milestones: + if milestones.count(milestone) != 1: + flask.flash( + 'Milestone %s is present %s times' % ( + milestone, milestones.count(milestone) + ), + 'error') + error = True + break + + for milestone_date in milestone_dates: + if milestone_dates.count(milestone_date) != 1: + flask.flash( + 'Date %s is present %s times' % ( + milestone_date, milestone_dates.count(milestone_date) + ), + 'error') + error = True + break + + if not error: + miles = {} + for cnt in range(len(milestones)): + miles[milestones[cnt]] = milestone_dates[cnt] + try: + repo.milestones = miles + SESSION.add(repo) + SESSION.commit() + flask.flash('Milestones 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)) + + @APP.route('//default/branch/', methods=['POST']) @APP.route('/fork///default/branch/', methods=['POST']) @login_required From 80b356f3752968463aeb2a840145ae873192c52a Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: May 09 2016 08:04:57 +0000 Subject: [PATCH 5/14] Add a new endpoint to display the project's roadmap --- diff --git a/pagure/templates/roadmap.html b/pagure/templates/roadmap.html new file mode 100644 index 0000000..cdde25d --- /dev/null +++ b/pagure/templates/roadmap.html @@ -0,0 +1,169 @@ +{% extends "repo_master.html" %} + +{% block title %}Roadmap - {{ repo.name }}{% endblock %} +{% set tag = "home"%} + + +{% block repo %} + + +

+ {{ issues|count }} Milestones + + + + + + + + +

+ {% if oth_issues %} +
+ {% if (issues | length + oth_issues) %} + + {{ (100.0 * (1 - issues | length / (issues | length + oth_issues)))|round|int }}% + + {% endif %} +
+ {% endif %} + +
+ + {% if not status %} + Open + All + {% else %} + Open + All + {% endif %} + + + + {% for tag in tag_list %} + {% if tag.tag in tags %} + {% if status and status != 'Open' %} + + {% else %} + + {% endif %} + {% else %} + + {% endif %} + + {{ tag.tag }} + {% endfor %} + +
+ +{% for milestone in issues | sort %} +
+
+
+ Milestone: {{ milestone }} + {% if repo.milestones[milestone] %} + + Due: {{ repo.milestones[milestone] }} + + {% endif %} +
+
+ +
+ + + + + {% for issue in issues[milestone] |sort(attribute='priority') %} + + + + + + + {% else %} + + + + {% endfor %} + +
+ #{{ issue.id }} + {% if status and status != 'Open' %} + {{issue.status}} + {% endif %} + {% if issue.private %} + + {% endif %} + + {{ issue.title | noJS("img") | safe }} + +    + {% if issue.comments|count > 0 %} + + + {{issue.comments|count}} + + {% endif %} + {% for tag in issue.tags%} + {{tag.tag}} + {% endfor%} + + {{ + issue.date_created | humanize}} + + {% if issue.priority %} + {{repo.priorities[issue.priority | string] }} + {% endif %} + + {% if issue.status != 'Open' %} + {{ issue.status }} + {% else %} + {% if issue.assignee %} + {{ issue.assignee.default_email | avatar(16) | safe }} + {{ issue.assignee.user }} + {% else %} + unassigned + {% endif %} + {% endif %} +
No issues found
+
+
+{% endfor %} +{% endblock %} +{% block jscripts %} +{{ super() }} + + + +{% endblock %} diff --git a/pagure/ui/issues.py b/pagure/ui/issues.py index 5642016..ae06a30 100644 --- a/pagure/ui/issues.py +++ b/pagure/ui/issues.py @@ -10,6 +10,7 @@ import flask import os +from collections import defaultdict import pygit2 from sqlalchemy.exc import SQLAlchemyError @@ -446,6 +447,88 @@ def view_issues(repo, username=None): ) +@APP.route('//roadmap/') +@APP.route('//roadmap') +@APP.route('/fork///roadmap/') +@APP.route('/fork///roadmap') +def view_roadmap(repo, username=None): + """ List all issues associated to a repo as roadmap + """ + status = flask.request.args.get('status', 'Open') + if status.lower() == 'all': + status = None + milestone = flask.request.args.getlist('milestone', None) + + repo = pagure.lib.get_project(SESSION, repo, user=username) + + if repo is None: + flask.abort(404, 'Project not found') + + if not repo.settings.get('issue_tracker', True): + flask.abort(404, 'No issue tracker found for this project') + + # Hide private tickets + private = False + # If user is authenticated, show him/her his/her private tickets + if authenticated(): + private = flask.g.fas_user.username + # If user is repo admin, show all tickets included the private ones + if is_repo_admin(repo): + private = None + + milestones = milestone or list(repo.milestones.keys()) + tags = ['roadmap'] + milestones + + issues = pagure.lib.search_issues( + SESSION, + repo, + tags=tags, + private=private, + ) + + # Change from a list of issues to a dict of milestone/issues + milestone_issues = defaultdict(list) + for cnt in range(len(issues)): + saved = False + for milestone in sorted(milestones): + if milestone in issues[cnt].tags_text: + milestone_issues[milestone].append(issues[cnt]) + saved = True + break + if saved: + continue + milestone_issues['unplaned'].append(issues[cnt]) + + if status: + for key in milestone_issues.keys(): + active = False + for issue in milestone_issues[key]: + if issue.status == 'Open': + active = True + break + if not active: + del(milestone_issues[key]) + + tag_list = pagure.lib.get_tags_of_project(SESSION, repo) + + reponame = pagure.get_repo_path(repo) + repo_obj = pygit2.Repository(reponame) + + return flask.render_template( + 'roadmap.html', + select='issues', + repo=repo, + username=username, + tag_list=tag_list, + status=status, + issues=milestone_issues, + tags=tags, + repo_admin=is_repo_admin(repo), + repo_obj=repo_obj, + ) + + + @APP.route('//new_issue/', methods=('GET', 'POST')) @APP.route('//new_issue', methods=('GET', 'POST')) @APP.route('/fork///new_issue/', methods=('GET', 'POST')) From 5b3063e76c5c1914556f42439dd76946db385156 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: May 09 2016 08:04:57 +0000 Subject: [PATCH 6/14] Adjust the settings page to allow adding/editing milestones --- diff --git a/pagure/templates/settings.html b/pagure/templates/settings.html index 94bc1e2..a02b153 100644 --- a/pagure/templates/settings.html +++ b/pagure/templates/settings.html @@ -440,6 +440,65 @@
+ Roadmap +
+
+

+ Below are the milestons 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. +

+
+
+ {{ tag_form.csrf_token }} +
+
+
+ Milestone +
+
+ Date (optional) +
+
+
+ {% for milestone in repo.milestones | sort %} +
+
+ +
+
+ +
+
+ {% endfor %} +
+ +
+
+ +
+
+
+
+
+
+ +
+
+
Issue Tags
@@ -568,6 +627,7 @@ $('#new_priority').click(function(e) {
' ); }); + {% if not repo.priorities %} $('#default_priorities').click(function(e) { $('#priorities').html(''); @@ -590,5 +650,22 @@ $('#default_priorities').click(function(e) { }); {% endif %} +$('#new_milestone').click(function(e) { + console.log('new milestone'); + console.log($('#milestones')); + $('#milestones').append( + '
\ +
\ + \ +
\ +
\ + \ +
\ +
' + ); +}); + {% endblock %} From a237ac55df4aa6654fcf6d267908a0c83a1b2d84 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: May 09 2016 08:04:57 +0000 Subject: [PATCH 7/14] Add a link to the project's roadmap from the list of tickets --- diff --git a/pagure/templates/issues.html b/pagure/templates/issues.html index 23f36de..e1b2ba4 100644 --- a/pagure/templates/issues.html +++ b/pagure/templates/issues.html @@ -13,9 +13,17 @@ {% else %} {{ issues|count }} Open Issues {% endif %} - - - + + {% if repo.milestones %} + + + + {% endif %} + + + + {% if oth_issues %}
From 1df3032b83462ccc9bcfe81952b1b55bd7644902 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: May 09 2016 08:04:57 +0000 Subject: [PATCH 8/14] Fix the ordering of the milestones so that 'unplanned' appears always last --- diff --git a/pagure/templates/roadmap.html b/pagure/templates/roadmap.html index cdde25d..60f3837 100644 --- a/pagure/templates/roadmap.html +++ b/pagure/templates/roadmap.html @@ -69,7 +69,7 @@ -{% for milestone in issues | sort %} +{% for milestone in milestones %}
diff --git a/pagure/ui/issues.py b/pagure/ui/issues.py index ae06a30..eb33733 100644 --- a/pagure/ui/issues.py +++ b/pagure/ui/issues.py @@ -513,6 +513,11 @@ def view_roadmap(repo, username=None): reponame = pagure.get_repo_path(repo) repo_obj = pygit2.Repository(reponame) + milestones_ordered = sorted(list(milestone_issues.keys())) + if 'unplaned' in milestones_ordered: + index = milestones_ordered.index('unplaned') + cnt = len(milestones_ordered) + milestones_ordered.insert(cnt, milestones_ordered.pop(index)) return flask.render_template( 'roadmap.html', @@ -521,6 +526,7 @@ def view_roadmap(repo, username=None): username=username, tag_list=tag_list, status=status, + milestones=milestones_ordered, issues=milestone_issues, tags=tags, repo_admin=is_repo_admin(repo), From 249d8a43012894bfec38ddae8a31097dddbe2b2b Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: May 09 2016 08:04:57 +0000 Subject: [PATCH 9/14] Fix filtering the roadmap for one or more milestones --- diff --git a/pagure/templates/roadmap.html b/pagure/templates/roadmap.html index 60f3837..8ca9cee 100644 --- a/pagure/templates/roadmap.html +++ b/pagure/templates/roadmap.html @@ -47,21 +47,17 @@ {% for tag in tag_list %} {% if tag.tag in tags %} - {% if status and status != 'Open' %} - {% else %} - - {% endif %} + repo=repo.name, milestone=tag.tag, + status='All' if not status else None) }}" + title="Filter issues by milestone"> {% else %} + repo=repo.name, milestone=tag.tag, + status='All' if not status else None) }}" + title="Filter issues by milestone"> {% endif %} {{ tag.tag }} diff --git a/pagure/ui/issues.py b/pagure/ui/issues.py index eb33733..abc731a 100644 --- a/pagure/ui/issues.py +++ b/pagure/ui/issues.py @@ -490,14 +490,15 @@ def view_roadmap(repo, username=None): milestone_issues = defaultdict(list) for cnt in range(len(issues)): saved = False - for milestone in sorted(milestones): - if milestone in issues[cnt].tags_text: - milestone_issues[milestone].append(issues[cnt]) + for mlstone in sorted(milestones): + if mlstone in issues[cnt].tags_text: + milestone_issues[mlstone].append(issues[cnt]) saved = True break if saved: continue - milestone_issues['unplaned'].append(issues[cnt]) + if not milestone: + milestone_issues['unplaned'].append(issues[cnt]) if status: for key in milestone_issues.keys(): @@ -528,7 +529,7 @@ def view_roadmap(repo, username=None): status=status, milestones=milestones_ordered, issues=milestone_issues, - tags=tags, + tags=milestone, repo_admin=is_repo_admin(repo), repo_obj=repo_obj, ) From 1b6f5f3c9641b1576cb52e3adbfa2bff0babd54f Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: May 09 2016 08:04:57 +0000 Subject: [PATCH 10/14] If one or more specific milestones are asked, make sure we always return them The tables may be empty if there are not tickets, but the headers will be present --- diff --git a/pagure/ui/issues.py b/pagure/ui/issues.py index abc731a..330b4c9 100644 --- a/pagure/ui/issues.py +++ b/pagure/ui/issues.py @@ -510,6 +510,11 @@ def view_roadmap(repo, username=None): if not active: del(milestone_issues[key]) + if milestone: + for mlstone in milestone: + if mlstone not in milestone_issues: + milestone_issues[mlstone] = [] + tag_list = pagure.lib.get_tags_of_project(SESSION, repo) reponame = pagure.get_repo_path(repo) From cf560ae6a829e5e0f94faf665148d189cba1de6c Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: May 09 2016 08:04:57 +0000 Subject: [PATCH 11/14] Add unit-tests for the roadmap feature --- diff --git a/tests/test_pagure_flask_ui_roadmap.py b/tests/test_pagure_flask_ui_roadmap.py new file mode 100644 index 0000000..5789e8c --- /dev/null +++ b/tests/test_pagure_flask_ui_roadmap.py @@ -0,0 +1,483 @@ +# -*- coding: utf-8 -*- + +""" + (c) 2016 - Copyright Red Hat Inc + + Authors: + Pierre-Yves Chibon + +""" + +__requires__ = ['SQLAlchemy >= 0.8'] +import pkg_resources + +import datetime +import json +import unittest +import shutil +import sys +import tempfile +import os + +import pygit2 +from mock import patch + +sys.path.insert(0, os.path.join(os.path.dirname( + os.path.abspath(__file__)), '..')) + +import pagure.lib +import tests +from pagure.lib.repo import PagureRepo + + +class PagureFlaskRoadmaptests(tests.Modeltests): + """ Tests for the pagure's roadmap """ + + def setUp(self): + """ Set up the environnment, ran before every tests. """ + super(PagureFlaskRoadmaptests, self).setUp() + + pagure.APP.config['TESTING'] = True + pagure.SESSION = self.session + pagure.ui.SESSION = self.session + pagure.ui.app.SESSION = self.session + pagure.ui.filters.SESSION = self.session + pagure.ui.repo.SESSION = self.session + pagure.ui.issues.SESSION = self.session + + pagure.APP.config['GIT_FOLDER'] = tests.HERE + pagure.APP.config['REQUESTS_FOLDER'] = os.path.join( + tests.HERE, 'requests') + pagure.APP.config['TICKETS_FOLDER'] = os.path.join( + tests.HERE, 'tickets') + pagure.APP.config['DOCS_FOLDER'] = os.path.join( + tests.HERE, 'docs') + self.app = pagure.APP.test_client() + + @patch('pagure.lib.git.update_git') + @patch('pagure.lib.notify.send_email') + def test_ticket_with_no_roadmap(self, p_send_email, p_ugt): + """ Test creating a ticket without roadmap. """ + p_send_email.return_value = True + p_ugt.return_value = True + + tests.create_projects(self.session) + tests.create_projects_git(os.path.join(tests.HERE), bare=True) + + user = tests.FakeUser() + user.username = 'pingou' + with tests.user_set(pagure.APP, user): + + # Get the CSRF token + output = self.app.get('/test/new_issue') + self.assertEqual(output.status_code, 200) + self.assertTrue( + '
\n New issue' + in output.data) + + csrf_token = output.data.split( + 'name="csrf_token" type="hidden" value="')[1].split('">')[0] + + data = { + 'title': 'Test issue', + 'issue_content': 'We really should improve on this issue', + 'status': 'Open', + 'csrf_token': csrf_token, + } + + # Create the issue + output = self.app.post( + '/test/new_issue', data=data, follow_redirects=True) + self.assertEqual(output.status_code, 200) + self.assertIn( + 'Issue #1: Test issue - test - Pagure', + output.data) + self.assertIn( + '', + output.data) + + @patch('pagure.lib.git.update_git') + @patch('pagure.lib.notify.send_email') + def test_ticket_with_roadmap(self, p_send_email, p_ugt): + """ Test creating a ticket with roadmap. """ + p_send_email.return_value = True + p_ugt.return_value = True + + tests.create_projects(self.session) + tests.create_projects_git(os.path.join(tests.HERE), bare=True) + + # Set some milestone + repo = pagure.lib.get_project(self.session, 'test') + repo.milestone = {'v1.0': '', 'v2.0': 'Tomorrow!'} + self.session.add(repo) + self.session.commit() + + user = tests.FakeUser() + user.username = 'pingou' + with tests.user_set(pagure.APP, user): + + # Get the CSRF token + output = self.app.get('/test/new_issue') + self.assertEqual(output.status_code, 200) + self.assertTrue( + u'
\n New issue' + in output.data) + + csrf_token = output.data.split( + u'name="csrf_token" type="hidden" value="')[1].split(u'">')[0] + + data = { + 'title': 'Test issue', + 'issue_content': 'We really should improve on this issue', + 'status': 'Open', + 'csrf_token': csrf_token, + } + + # Create the issue + output = self.app.post( + '/test/new_issue', data=data, follow_redirects=True) + self.assertEqual(output.status_code, 200) + self.assertIn( + u'Issue #1: Test issue - test - Pagure', + output.data) + self.assertIn( + u'', + output.data) + + # Mark the ticket for the roadmap + data = { + 'tag': 'roadmap', + 'csrf_token': csrf_token, + } + output = self.app.post( + '/test/issue/1/update', data=data, follow_redirects=True) + self.assertEqual(output.status_code, 200) + self.assertIn( + u'Issue #1: Test issue - test - Pagure', + output.data) + self.assertIn( + u'', + output.data) + self.assertIn( + u'\n Tag added: roadmap', + output.data) + + 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) + + # Set some milestones + repo = pagure.lib.get_project(self.session, 'test') + self.assertEqual(repo.milestones, {}) + + user = tests.FakeUser() + user.username = 'pingou' + with tests.user_set(pagure.APP, user): + + # Get the CSRF token + output = self.app.get('/test/settings') + self.assertEqual(output.status_code, 200) + self.assertIn( + 'Settings - test - Pagure', output.data) + self.assertIn('

Settings for test

', output.data) + + csrf_token = output.data.split( + 'name="csrf_token" type="hidden" value="')[1].split('">')[0] + + data = { + 'milestones': 1, + 'milestone_dates': 'Tomorrow', + } + output = self.app.post( + '/test/update/milestones', data=data, follow_redirects=True) + self.assertEqual(output.status_code, 200) + # Check the redirect + self.assertIn( + 'Settings - test - Pagure', output.data) + self.assertIn('

Settings for test

', output.data) + # Check the result of the action -- None, no CSRF + repo = pagure.lib.get_project(self.session, 'test') + self.assertEqual(repo.milestones, {}) + + data = { + 'milestones': 1, + '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( + 'Settings - test - Pagure', output.data) + self.assertIn('

Settings for test

', output.data) + self.assertIn('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'1': u'Tomorrow'}) + + data = { + 'milestones': ['v1.0', 'v2.0'], + '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( + 'Settings - test - Pagure', output.data) + self.assertIn('

Settings for test

', output.data) + self.assertIn('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''} + ) + + # Check error - less milestones than dates + data = { + 'milestones': ['v1.0', 'v2.0'], + 'milestone_dates': ['Tomorrow', 'Next week', 'Next Year'], + '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( + 'Settings - test - Pagure', output.data) + self.assertIn('

Settings for test

', output.data) + self.assertIn( + '\n' + ' Milestones and dates are not of the ' + 'same length', output.data) + # Check the result of the action -- Milestones un-changed + repo = pagure.lib.get_project(self.session, 'test') + self.assertEqual( + repo.milestones, {u'v1.0': u'Tomorrow', u'v2.0': u''} + ) + + # Check error - Twice the same milestone + data = { + 'milestones': ['v1.0', 'v2.0', 'v2.0'], + 'milestone_dates': ['Tomorrow', 'Next week', 'Next Year'], + '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( + 'Settings - test - Pagure', output.data) + self.assertIn('

Settings for test

', output.data) + self.assertIn( + '\n' + ' Milestone v2.0 is present 2 times', + output.data) + # Check the result of the action -- Milestones un-changed + repo = pagure.lib.get_project(self.session, 'test') + self.assertEqual( + repo.milestones, {u'v1.0': u'Tomorrow', u'v2.0': u''} + ) + + # Check error - Twice the same date + data = { + 'milestones': ['v1.0', 'v2.0', 'v3.0'], + 'milestone_dates': ['Tomorrow', 'Next week', 'Next week'], + '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( + 'Settings - test - Pagure', output.data) + self.assertIn('

Settings for test

', output.data) + self.assertIn( + '\n' + ' Date Next week is present 2 times', + output.data) + # Check the result of the action -- Milestones un-changed + repo = pagure.lib.get_project(self.session, 'test') + self.assertEqual( + repo.milestones, {u'v1.0': u'Tomorrow', u'v2.0': u''} + ) + + # Check for an invalid project + output = self.app.post( + '/foo/update/milestones', data=data) + self.assertEqual(output.status_code, 404) + + # Check the behavior if the project disabled the issue tracker + settings = repo.settings + settings['issue_tracker'] = False + repo.settings = settings + self.session.add(repo) + self.session.commit() + + output = self.app.post( + '/test/update/milestones', data=data) + self.assertEqual(output.status_code, 404) + + # Check for a non-admin user + settings = repo.settings + settings['issue_tracker'] = True + repo.settings = settings + self.session.add(repo) + self.session.commit() + + user.username = 'ralph' + with tests.user_set(pagure.APP, user): + output = self.app.post( + '/test/update/milestones', data=data) + self.assertEqual(output.status_code, 403) + + @patch('pagure.lib.git.update_git') + @patch('pagure.lib.notify.send_email') + def test_roadmap_ui(self, p_send_email, p_ugt): + """ Test viewing the roadmap of a repo. """ + p_send_email.return_value = True + p_ugt.return_value = True + + self.test_update_milestones() + + user = tests.FakeUser() + user.username = 'pingou' + with tests.user_set(pagure.APP, user): + + # Get the CSRF token + output = self.app.get('/test/new_issue') + self.assertEqual(output.status_code, 200) + self.assertTrue( + u'
\n New issue' + in output.data) + + csrf_token = output.data.split( + u'name="csrf_token" type="hidden" value="')[1].split(u'">')[0] + + # Create the issues + for cnt in range(6): + cnt += 1 + data = { + 'title': 'Test issue %s' % cnt, + 'issue_content': 'We really should improve on this ' + 'issue %s' % cnt, + 'status': 'Open', + 'csrf_token': csrf_token, + } + + output = self.app.post( + '/test/new_issue', data=data, follow_redirects=True) + self.assertEqual(output.status_code, 200) + self.assertIn( + u'Issue #{0}: Test issue {0} - test - ' + 'Pagure'.format(cnt), + output.data) + self.assertIn( + u'' % cnt, + output.data) + + # Mark the ticket for the roadmap + data = { + 'tag': ['roadmap, v%s.0' % cnt], + 'csrf_token': csrf_token, + } + output = self.app.post( + '/test/issue/%s/update' % cnt, data=data, + follow_redirects=True) + self.assertEqual(output.status_code, 200) + self.assertIn( + u'Issue #{0}: Test issue {0} - test - ' + 'Pagure'.format(cnt), + output.data) + self.assertIn( + u'' % cnt, + output.data) + self.assertIn( + u'\n ' + 'Tag added: v%s.0, roadmap' % 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() + + # test the roadmap view + output = self.app.get('/test/roadmap') + self.assertEqual(output.status_code, 200) + self.assertIn(u'2 Milestones', output.data) + self.assertIn(u'Milestone: v2.0', output.data) + self.assertIn(u'Milestone: unplaned', output.data) + self.assertEqual( + output.data.count(u'#'), 5) + + # test the roadmap view for all milestones + output = self.app.get('/test/roadmap?status=All') + self.assertEqual(output.status_code, 200) + self.assertIn(u'3 Milestones', output.data) + self.assertIn(u'Milestone: v1.0', output.data) + self.assertIn(u'Milestone: v2.0', output.data) + self.assertIn(u'Milestone: unplaned', output.data) + self.assertEqual( + output.data.count(u'#'), 6) + + # test the roadmap view for a specific milestone + output = self.app.get('/test/roadmap?milestone=v2.0') + self.assertEqual(output.status_code, 200) + self.assertIn(u'1 Milestones', output.data) + self.assertIn(u'Milestone: v2.0', output.data) + self.assertEqual( + output.data.count(u'#'), 1) + + # test the roadmap view for a specific milestone - closed + output = self.app.get('/test/roadmap?milestone=v1.0') + self.assertEqual(output.status_code, 200) + self.assertIn(u'1 Milestones', output.data) + self.assertIn(u'Milestone: v1.0', output.data) + self.assertEqual( + output.data.count(u'#'), 0) + + # test the roadmap view for a specific milestone - closed + output = self.app.get('/test/roadmap?milestone=v1.0&status=All') + self.assertEqual(output.status_code, 200) + self.assertIn(u'1 Milestones', output.data) + self.assertIn(u'Milestone: v1.0', output.data) + self.assertEqual( + output.data.count(u'#'), 1) + + # test the roadmap view for errors + output = self.app.get('/foo/roadmap') + self.assertEqual(output.status_code, 404) + + repo = pagure.lib.get_project(self.session, 'test') + settings = repo.settings + settings['issue_tracker'] = False + repo.settings = settings + self.session.add(repo) + self.session.commit() + + output = self.app.get('/test/roadmap', data=data) + self.assertEqual(output.status_code, 404) + + +if __name__ == '__main__': + SUITE = unittest.TestLoader().loadTestsFromTestCase( + PagureFlaskRoadmaptests) + unittest.TextTestRunner(verbosity=2).run(SUITE) From 593f0ac628ff764fd6806009206557200a205e32 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: May 09 2016 08:04:57 +0000 Subject: [PATCH 12/14] Make all the strings in the tests are unicode --- diff --git a/tests/test_pagure_flask_ui_roadmap.py b/tests/test_pagure_flask_ui_roadmap.py index 5789e8c..55709fb 100644 --- a/tests/test_pagure_flask_ui_roadmap.py +++ b/tests/test_pagure_flask_ui_roadmap.py @@ -72,7 +72,7 @@ class PagureFlaskRoadmaptests(tests.Modeltests): output = self.app.get('/test/new_issue') self.assertEqual(output.status_code, 200) self.assertTrue( - '
\n New issue' + u'
\n New issue' in output.data) csrf_token = output.data.split( @@ -90,10 +90,10 @@ class PagureFlaskRoadmaptests(tests.Modeltests): '/test/new_issue', data=data, follow_redirects=True) self.assertEqual(output.status_code, 200) self.assertIn( - 'Issue #1: Test issue - test - Pagure', + u'Issue #1: Test issue - test - Pagure', output.data) self.assertIn( - '', output.data) @@ -182,8 +182,8 @@ class PagureFlaskRoadmaptests(tests.Modeltests): output = self.app.get('/test/settings') self.assertEqual(output.status_code, 200) self.assertIn( - 'Settings - test - Pagure', output.data) - self.assertIn('

Settings for test

', output.data) + u'Settings - test - Pagure', output.data) + self.assertIn(u'

Settings for test

', output.data) csrf_token = output.data.split( 'name="csrf_token" type="hidden" value="')[1].split('">')[0] @@ -197,7 +197,7 @@ class PagureFlaskRoadmaptests(tests.Modeltests): self.assertEqual(output.status_code, 200) # Check the redirect self.assertIn( - 'Settings - test - Pagure', output.data) + u'Settings - test - Pagure', output.data) self.assertIn('

Settings for test

', output.data) # Check the result of the action -- None, no CSRF repo = pagure.lib.get_project(self.session, 'test') @@ -213,9 +213,9 @@ class PagureFlaskRoadmaptests(tests.Modeltests): self.assertEqual(output.status_code, 200) # Check the redirect self.assertIn( - 'Settings - test - Pagure', output.data) - self.assertIn('

Settings for test

', output.data) - self.assertIn('Milestones updated', output.data) + 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'1': u'Tomorrow'}) @@ -230,9 +230,9 @@ class PagureFlaskRoadmaptests(tests.Modeltests): self.assertEqual(output.status_code, 200) # Check the redirect self.assertIn( - 'Settings - test - Pagure', output.data) - self.assertIn('

Settings for test

', output.data) - self.assertIn('Milestones updated', output.data) + 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( @@ -250,10 +250,10 @@ class PagureFlaskRoadmaptests(tests.Modeltests): self.assertEqual(output.status_code, 200) # Check the redirect self.assertIn( - 'Settings - test - Pagure', output.data) - self.assertIn('

Settings for test

', output.data) + u'Settings - test - Pagure', output.data) + self.assertIn(u'

Settings for test

', output.data) self.assertIn( - '\n' + u'\n' ' Milestones and dates are not of the ' 'same length', output.data) # Check the result of the action -- Milestones un-changed @@ -273,10 +273,10 @@ class PagureFlaskRoadmaptests(tests.Modeltests): self.assertEqual(output.status_code, 200) # Check the redirect self.assertIn( - 'Settings - test - Pagure', output.data) - self.assertIn('

Settings for test

', output.data) + u'Settings - test - Pagure', output.data) + self.assertIn(u'

Settings for test

', output.data) self.assertIn( - '\n' + u'\n' ' Milestone v2.0 is present 2 times', output.data) # Check the result of the action -- Milestones un-changed @@ -296,10 +296,10 @@ class PagureFlaskRoadmaptests(tests.Modeltests): self.assertEqual(output.status_code, 200) # Check the redirect self.assertIn( - 'Settings - test - Pagure', output.data) - self.assertIn('

Settings for test

', output.data) + u'Settings - test - Pagure', output.data) + self.assertIn(u'

Settings for test

', output.data) self.assertIn( - '\n' + u'\n' ' Date Next week is present 2 times', output.data) # Check the result of the action -- Milestones un-changed From f6c7d8aae5004ffc06afcc05536a37600f09ae1d Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: May 09 2016 08:04:57 +0000 Subject: [PATCH 13/14] Start working on the doc for the roadmap feature --- diff --git a/doc/usage/roadmap.rst b/doc/usage/roadmap.rst new file mode 100644 index 0000000..d4ec443 --- /dev/null +++ b/doc/usage/roadmap.rst @@ -0,0 +1,11 @@ +Using the roadmap feature +========================= + +Pagure allows building the roadmap of the project using the tickets and +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. From b4817ef73e32c26ae506cd37c277eccfe76c66af Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: May 09 2016 09:50:39 +0000 Subject: [PATCH 14/14] Fix typo unplaned -> unplanned --- diff --git a/pagure/ui/issues.py b/pagure/ui/issues.py index 330b4c9..8893408 100644 --- a/pagure/ui/issues.py +++ b/pagure/ui/issues.py @@ -498,7 +498,7 @@ def view_roadmap(repo, username=None): if saved: continue if not milestone: - milestone_issues['unplaned'].append(issues[cnt]) + milestone_issues['unplanned'].append(issues[cnt]) if status: for key in milestone_issues.keys(): @@ -520,8 +520,8 @@ def view_roadmap(repo, username=None): reponame = pagure.get_repo_path(repo) repo_obj = pygit2.Repository(reponame) milestones_ordered = sorted(list(milestone_issues.keys())) - if 'unplaned' in milestones_ordered: - index = milestones_ordered.index('unplaned') + if 'unplanned' in milestones_ordered: + index = milestones_ordered.index('unplanned') cnt = len(milestones_ordered) milestones_ordered.insert(cnt, milestones_ordered.pop(index)) diff --git a/tests/test_pagure_flask_ui_roadmap.py b/tests/test_pagure_flask_ui_roadmap.py index 55709fb..9cc94f4 100644 --- a/tests/test_pagure_flask_ui_roadmap.py +++ b/tests/test_pagure_flask_ui_roadmap.py @@ -424,7 +424,7 @@ class PagureFlaskRoadmaptests(tests.Modeltests): self.assertEqual(output.status_code, 200) self.assertIn(u'2 Milestones', output.data) self.assertIn(u'Milestone: v2.0', output.data) - self.assertIn(u'Milestone: unplaned', output.data) + self.assertIn(u'Milestone: unplanned', output.data) self.assertEqual( output.data.count(u'#'), 5) @@ -434,7 +434,7 @@ class PagureFlaskRoadmaptests(tests.Modeltests): self.assertIn(u'3 Milestones', output.data) self.assertIn(u'Milestone: v1.0', output.data) self.assertIn(u'Milestone: v2.0', output.data) - self.assertIn(u'Milestone: unplaned', output.data) + self.assertIn(u'Milestone: unplanned', output.data) self.assertEqual( output.data.count(u'#'), 6)