From c2e7b07e92817fbdcce524288f138aa9bcb31c85 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Sep 20 2016 06:33:37 +0000 Subject: [PATCH 1/5] Add a _notifications column to the projects table --- diff --git a/alembic/versions/368fd931cf7f_add_the_notifications_column_to_projects.py b/alembic/versions/368fd931cf7f_add_the_notifications_column_to_projects.py new file mode 100644 index 0000000..cb5825b --- /dev/null +++ b/alembic/versions/368fd931cf7f_add_the_notifications_column_to_projects.py @@ -0,0 +1,29 @@ +"""Add the notifications column to projects + +Revision ID: 368fd931cf7f +Revises: 36386a60b3fd +Create Date: 2016-09-18 18:51:09.625322 + +""" + +# revision identifiers, used by Alembic. +revision = '368fd931cf7f' +down_revision = '36386a60b3fd' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + ''' Add the column notifications to the table projects. + ''' + op.add_column( + 'projects', + sa.Column('_notifications', sa.String(255), nullable=True) + ) + + +def downgrade(): + ''' Add the column notifications to the table projects. + ''' + op.drop_column('projects', '_notifications') diff --git a/pagure/lib/model.py b/pagure/lib/model.py index f46dc06..0e62ad6 100644 --- a/pagure/lib/model.py +++ b/pagure/lib/model.py @@ -319,6 +319,7 @@ class Project(BASE): _priorities = sa.Column(sa.Text, nullable=True) _milestones = sa.Column(sa.Text, nullable=True) _reports = sa.Column(sa.Text, nullable=True) + _notifications = sa.Column(sa.Text, nullable=True) date_created = sa.Column(sa.DateTime, nullable=False, default=datetime.datetime.utcnow) @@ -442,6 +443,23 @@ class Project(BASE): self._priorities = json.dumps(priorities) @property + def notifications(self): + """ Return the dict stored as string in the database as an actual + dict object. + """ + notifications = {} + + if self._notifications: + notifications = json.loads(self._notifications) + + return notifications + + @notifications.setter + def notifications(self, notifications): + ''' Ensures the notifications are properly saved. ''' + self._notifications = json.dumps(notifications) + + @property def reports(self): """ Return the dict stored as string in the database as an actual dict object. From 080c159c3c8a514efb880a75dd6d3803acc2a54c Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Sep 20 2016 06:33:37 +0000 Subject: [PATCH 2/5] 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 3/5] 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 4/5] 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 5/5] 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 + },