From 99ab6e7fe2786b3e75cacbdf6dcfc4898929a780 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Oct 11 2016 10:31:26 +0000 Subject: [PATCH 1/11] Adjust the DB model for storing the custom fields of tickets/projects --- diff --git a/pagure/lib/model.py b/pagure/lib/model.py index cfdeb1e..e15d467 100644 --- a/pagure/lib/model.py +++ b/pagure/lib/model.py @@ -840,6 +840,66 @@ class IssueComment(BASE): return output +class IssueKeys(BASE): + """ Stores the custom keys a project can use on issues. + + Table -- issue_keys + """ + + __tablename__ = 'issue_keys' + + id = sa.Column(sa.Integer, primary_key=True) + project_id = sa.Column( + sa.Integer, + sa.ForeignKey( + 'projects.id', onupdate='CASCADE', + ), + nullable=True) + name = sa.Column(sa.Text(), nullable=False) + type_ = sa.Column(sa.String(255), nullable=False) + + project = relation( + 'Project', foreign_keys=[project_id], remote_side=[Project.id], + backref=backref( + 'issue_keys', cascade="delete, delete-orphan", single_parent=True) + ) + + +class IssueValues(BASE): + """ Stores the values of the custom keys set by project on issues. + + Table -- issue_values + """ + + __tablename__ = 'issue_values' + + id = sa.Column(sa.Integer, primary_key=True) + key_id = sa.Column( + sa.Integer, + sa.ForeignKey( + 'issue_keys.id', ondelete='CASCADE', onupdate='CASCADE', + ), + index=True) + issue_uid = sa.Column( + sa.String(32), + sa.ForeignKey( + 'issues.uid', ondelete='CASCADE', onupdate='CASCADE', + ), + index=True) + value = sa.Column(sa.Text(), nullable=False) + + issue = relation( + 'Issue', foreign_keys=[issue_uid], remote_side=[Issue.uid], + backref=backref( + 'other_fields', cascade="delete, delete-orphan", single_parent=True) + ) + + key = relation( + 'IssueKeys', foreign_keys=[key_id], remote_side=[IssueKeys.id], + backref=backref('values', cascade="delete, delete-orphan") + ) + + class Tag(BASE): """ Stores the tags. From cdb5d47dac85db5e643a780156bddefa738730cf Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Oct 11 2016 10:31:26 +0000 Subject: [PATCH 2/11] Add methods to pagure.lib to set the custom fields and values on issues --- diff --git a/pagure/lib/__init__.py b/pagure/lib/__init__.py index a52a5d2..0354047 100644 --- a/pagure/lib/__init__.py +++ b/pagure/lib/__init__.py @@ -3107,3 +3107,57 @@ def save_report(session, repo, name, url, username): reports[name] = query repo.reports = reports session.add(repo) + + +def set_custom_key_fields(session, project, fields, types): + """ Set or update the custom key fields of a project with the values + provided. + """ + + if len(fields) != len(types): + raise pagure.exceptions.PagureException( + 'Not all the custom fields have a type specified') + + current_keys = {} + for key in project.issue_keys: + current_keys[key.name] = key + + for idx, key in enumerate(fields): + if key in current_keys: + issuekey = current_keys[key] + issuekey.type_ = types[idx] + else: + issuekey = model.IssueKeys( + project_id=project.id, + name=key, + type_=types[idx], + ) + session.add(issuekey) + + return 'List of custom fields updated' + + +def set_custom_key_value(session, issue, key, value): + """ Set or update the value of the specified custom key. + """ + + query = session.query( + model.IssueValues + ).filter( + model.IssueKeys.id == key.id + ).filter( + model.IssueValues.issue_uid == issue.uid + ) + + current_field = query.first() + if current_field: + current_field.value = value + else: + current_field = model.IssueValues( + issue_uid=issue.uid, + key_id=key.id, + value=value, + ) + session.add(current_field) + + return 'Custom key adjusted' From 3841b36a252e6e832fdf17a3f0900d472b39841a Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Oct 11 2016 10:31:26 +0000 Subject: [PATCH 3/11] Add endpoint to save the custom fields set for a project --- diff --git a/pagure/ui/repo.py b/pagure/ui/repo.py index f68584a..9bb1bfb 100644 --- a/pagure/ui/repo.py +++ b/pagure/ui/repo.py @@ -2097,3 +2097,61 @@ def update_close_status(repo, username=None, namespace=None): return flask.redirect(flask.url_for( 'view_settings', username=username, repo=repo.name, namespace=namespace)) + + +@APP.route('//update/custom_keys', methods=['POST']) +@APP.route('///update/custom_keys', methods=['POST']) +@APP.route('/fork///update/custom_keys', methods=['POST']) +@APP.route( + '/fork////update/custom_keys', + methods=['POST']) +@login_required +def update_custom_keys(repo, username=None, namespace=None): + """ Update the custom_keys 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(): + custom_keys = [ + w.strip() for w in flask.request.form.getlist('custom_keys') + if w.strip() + ] + custom_keys_type = [ + w.strip() for w in flask.request.form.getlist('custom_keys_type') + if w.strip() + ] + + try: + msg = pagure.lib.set_custom_key_fields( + SESSION, repo, custom_keys, custom_keys_type) + SESSION.commit() + flask.flash(msg) + except pagure.exceptions.PagureException as msg: + SESSION.rollback() + flask.flash(msg, 'error') + 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 a33a896ef3a07d62d8a1a274e7426c22b4034084 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Oct 11 2016 10:31:26 +0000 Subject: [PATCH 4/11] Adjust the settings page to allow setting custom fields for issues --- diff --git a/pagure/templates/settings.html b/pagure/templates/settings.html index 1573ccf..fe31308 100644 --- a/pagure/templates/settings.html +++ b/pagure/templates/settings.html @@ -673,6 +673,69 @@
+ Custom issue fields +
+
+

+ Set some custom fields for your issues. +

+
+
+ {{ tag_form.csrf_token }} +
+
+
+ Fields +
+
+
+ {% for field in repo.issue_keys | sort %} +
+
+ +
+
+ +
+
+ {% endfor %} +
+ +
+
+ +
+
+
+
+
+
+ +
+
+
Issue Tags
@@ -873,5 +936,23 @@ $('#new_close_status').click(function(e) { ); }); +$('#new_custom_field').click(function(e) { + console.log('new custom fields'); + $('#custom_fields').append( + '
\ +
\ + \ +
\ +
\ + \ +
\ +
' + ); +}); + {% endblock %} From 0f61bb1de902233faeb696ca96ad7a20ffb5f73b Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Oct 11 2016 10:31:26 +0000 Subject: [PATCH 5/11] Display the custom fields on tickets and allow updating/saving them --- diff --git a/pagure/static/pagure.css b/pagure/static/pagure.css index b81dffc..b7a79c6 100644 --- a/pagure/static/pagure.css +++ b/pagure/static/pagure.css @@ -120,7 +120,12 @@ white-space: nowrap; .issue-metadata-form { -display:none; + display:none; +} + +.issue-custom-form +{ + display:none; } .bodycontent diff --git a/pagure/templates/issue.html b/pagure/templates/issue.html index 0287100..fdcc82d 100644 --- a/pagure/templates/issue.html +++ b/pagure/templates/issue.html @@ -289,9 +289,9 @@ {% endif%} - + {% if authenticated and g.repo_admin %}
+
@@ -564,6 +600,8 @@ function try_async_comment(form) { $('#comments').find('.edit_comment').remove(); $( ".issue-metadata-form" ).hide(); $( ".issue-metadata-display" ).show(); + $( ".issue-custom-form" ).hide(); + $( ".issue-custom-display" ).show(); } else { // Make the browser submit the form sync $(form).off('submit'); @@ -611,6 +649,13 @@ $( document ).ready(function() { } ); + $( ".edit_custom_toggle" ).click( + function() { + $( ".issue-custom-form" ).toggle(); + $( ".issue-custom-display" ).toggle(); + } + ); + $('#assignee').selectize({ valueField: 'user', labelField: 'user', diff --git a/pagure/ui/issues.py b/pagure/ui/issues.py index 4a25511..f879bbb 100644 --- a/pagure/ui/issues.py +++ b/pagure/ui/issues.py @@ -267,6 +267,15 @@ def update_issue(repo, issueid, username=None, namespace=None): if message: messages.add(message) + # Update the custom keys/fields + for key in repo.issue_keys: + value = flask.request.form.get(key.name) + if value: + messages.add( + pagure.lib.set_custom_key_value( + SESSION, issue, key, value) + ) + # Update ticket this one depends on messages.union(set(pagure.lib.update_dependency_issue( SESSION, repo, issue, depends, @@ -789,6 +798,11 @@ def view_issue(repo, issueid, username=None, namespace=None): form.private.data = issue.private form.close_status.data = issue.close_status tag_list = pagure.lib.get_tags_of_project(SESSION, repo) + + knowns_keys = {} + for key in issue.other_fields: + knowns_keys[key.key.name] = key + return flask.render_template( 'issue.html', select='issues', @@ -798,6 +812,7 @@ def view_issue(repo, issueid, username=None, namespace=None): issue=issue, issueid=issueid, form=form, + knowns_keys=knowns_keys, ) From 4371c197cafbd6644fdbda2c52d6a89f4f21da58 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Oct 11 2016 10:31:26 +0000 Subject: [PATCH 6/11] Fix setting the custom key value --- diff --git a/pagure/lib/__init__.py b/pagure/lib/__init__.py index 0354047..54c64ff 100644 --- a/pagure/lib/__init__.py +++ b/pagure/lib/__init__.py @@ -3144,7 +3144,7 @@ def set_custom_key_value(session, issue, key, value): query = session.query( model.IssueValues ).filter( - model.IssueKeys.id == key.id + model.IssueValues.key_id == key.id ).filter( model.IssueValues.issue_uid == issue.uid ) From b4970fae52d238273dec31838549c67358fafd48 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Oct 11 2016 10:31:26 +0000 Subject: [PATCH 7/11] Allow filtering issues using the custom fields set-up --- diff --git a/pagure/lib/__init__.py b/pagure/lib/__init__.py index 54c64ff..d3227b3 100644 --- a/pagure/lib/__init__.py +++ b/pagure/lib/__init__.py @@ -1685,7 +1685,7 @@ def search_issues( session, repo, issueid=None, issueuid=None, status=None, closed=False, tags=None, assignee=None, author=None, private=None, priority=None, milestones=None, count=False, offset=None, - limit=None, search_pattern=None): + limit=None, search_pattern=None, custom_search=None): ''' Retrieve one or more issues associated to a project with the given criterias. @@ -1734,6 +1734,9 @@ def search_issues( :type count: boolean :kwarg search_pattern: a string to search in issues title :type search_pattern: str or None + :kwarg custom_search: a dictionary of key/values to be used when + searching issues with a custom key constraint + :type custom_search: dict or None :return: A single Issue object if issueid is specified, a list of Project objects otherwise. @@ -1875,6 +1878,37 @@ def search_issues( model.Issue.milestone.in_(milestones) ) + if custom_search: + constraints = [] + for key in custom_search: + value = custom_search[key] + if '*' in value: + value = value.replace('*', '%') + constraints.append( + sqlalchemy.and_( + model.IssueKeys.name == key, + model.IssueValues.value.ilike(value) + ) + ) + else: + constraints.append( + sqlalchemy.and_( + model.IssueKeys.name == key, + model.IssueValues.value == value + ) + ) + if constraints: + query = query.filter( + model.Issue.uid == model.IssueValues.issue_uid + ).filter( + model.IssueValues.key_id == model.IssueKeys.id + ) + query = query.filter( + sqlalchemy.or_( + (const for const in constraints) + ) + ) + query = session.query( model.Issue ).filter( diff --git a/pagure/ui/issues.py b/pagure/ui/issues.py index f879bbb..aee72dc 100644 --- a/pagure/ui/issues.py +++ b/pagure/ui/issues.py @@ -446,6 +446,14 @@ def view_issues(repo, username=None, namespace=None): author = flask.request.args.get('author', None) search_pattern = flask.request.args.get('search_pattern', None) + # Custom fields + custom_keys = flask.request.args.getlist('ckeys') + custom_values = flask.request.args.getlist('cvalue') + custom_search = {} + if len(custom_keys) == len(custom_values): + for idx, key in enumerate(custom_keys): + custom_search[key] = custom_values[idx] + repo = flask.g.repo if not repo.settings.get('issue_tracker', True): @@ -483,6 +491,7 @@ def view_issues(repo, username=None, namespace=None): offset=flask.g.offset, limit=flask.g.limit, search_pattern=search_pattern, + custom_search=custom_search, ) issues_cnt = pagure.lib.search_issues( SESSION, @@ -507,6 +516,7 @@ def view_issues(repo, username=None, namespace=None): priority=priority, count=True, search_pattern=search_pattern, + custom_search=custom_search, ) else: issues = pagure.lib.search_issues( @@ -514,6 +524,7 @@ def view_issues(repo, username=None, namespace=None): author=author, private=private, priority=priority, offset=flask.g.offset, limit=flask.g.limit, search_pattern=search_pattern, + custom_search=custom_search, ) issues_cnt = pagure.lib.search_issues( SESSION, repo, tags=tags, assignee=assignee, From 14737744adea8593f9923c3efca70b4291611eae Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Oct 11 2016 10:31:26 +0000 Subject: [PATCH 8/11] Adjust the database model with the feedback from @puiterwijk --- diff --git a/pagure/lib/model.py b/pagure/lib/model.py index e15d467..d02c13c 100644 --- a/pagure/lib/model.py +++ b/pagure/lib/model.py @@ -854,9 +854,11 @@ class IssueKeys(BASE): sa.ForeignKey( 'projects.id', onupdate='CASCADE', ), - nullable=True) + nullable=False) name = sa.Column(sa.Text(), nullable=False) - type_ = sa.Column(sa.String(255), nullable=False) + key_type = sa.Column(sa.String(255), nullable=False) + + __table_args__ = (sa.UniqueConstraint('project_id', 'name'),) project = relation( 'Project', foreign_keys=[project_id], remote_side=[Project.id], @@ -873,19 +875,18 @@ class IssueValues(BASE): __tablename__ = 'issue_values' - id = sa.Column(sa.Integer, primary_key=True) key_id = sa.Column( sa.Integer, sa.ForeignKey( 'issue_keys.id', ondelete='CASCADE', onupdate='CASCADE', ), - index=True) + primary_key=True) issue_uid = sa.Column( sa.String(32), sa.ForeignKey( 'issues.uid', ondelete='CASCADE', onupdate='CASCADE', ), - index=True) + primary_key=True) value = sa.Column(sa.Text(), nullable=False) issue = relation( From 1fb9059850ee9ba3fb4047eac149ecbf87e9a1e6 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Oct 11 2016 10:31:26 +0000 Subject: [PATCH 9/11] Fix setting custom fields for the adjusted DB model --- diff --git a/pagure/lib/__init__.py b/pagure/lib/__init__.py index d3227b3..130adfd 100644 --- a/pagure/lib/__init__.py +++ b/pagure/lib/__init__.py @@ -3159,12 +3159,12 @@ def set_custom_key_fields(session, project, fields, types): for idx, key in enumerate(fields): if key in current_keys: issuekey = current_keys[key] - issuekey.type_ = types[idx] + issuekey.key_type = types[idx] else: issuekey = model.IssueKeys( project_id=project.id, name=key, - type_=types[idx], + key_type=types[idx], ) session.add(issuekey) From bdc36f5ea23eec4f4bd3f272bf84208ff084a103 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Oct 11 2016 10:31:26 +0000 Subject: [PATCH 10/11] Ensure when the project is deleted, its custom fields are as well --- diff --git a/pagure/lib/model.py b/pagure/lib/model.py index d02c13c..ba58a11 100644 --- a/pagure/lib/model.py +++ b/pagure/lib/model.py @@ -852,7 +852,7 @@ class IssueKeys(BASE): project_id = sa.Column( sa.Integer, sa.ForeignKey( - 'projects.id', onupdate='CASCADE', + 'projects.id', onupdate='CASCADE', ondelete='CASCADE', ), nullable=False) name = sa.Column(sa.Text(), nullable=False) From 8d21e8cfdc86ee3e682b325180a26bdf96824668 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Oct 11 2016 10:31:26 +0000 Subject: [PATCH 11/11] Make the UI reflects the key_type (text/boolean) --- diff --git a/pagure/templates/issue.html b/pagure/templates/issue.html index fdcc82d..eda3fac 100644 --- a/pagure/templates/issue.html +++ b/pagure/templates/issue.html @@ -316,9 +316,15 @@ {% if authenticated and g.repo_admin %}
- - + +
{% endif %} {% endfor %}