From b378511e1017231a97bee6e339aba0319e35869e Mon Sep 17 00:00:00 2001
From: Pierre-Yves Chibon <pingou@pingoured.fr>
Date: Dec 08 2016 17:54:31 +0000
Subject: [PATCH 1/2] Allow dropping of assignments for an issue (Fixes: #1255)


Signed-off-by: Paul W. Frields <stickster@gmail.com>

---

diff --git a/pagure/api/issue.py b/pagure/api/issue.py
index 7462ea0..1ef6edf 100644
--- a/pagure/api/issue.py
+++ b/pagure/api/issue.py
@@ -837,6 +837,9 @@ def api_assign_issue(repo, issueid, username=None, namespace=None):
             )
             SESSION.commit()
             output['message'] = message
+        except pagure.exceptions.PagureException as err:  # pragma: no cover
+            raise pagure.exceptions.APIError(
+                400, error_code=APIERROR.ENOCODE, error=str(err))
         except SQLAlchemyError as err:  # pragma: no cover
             SESSION.rollback()
             APP.logger.exception(err)
diff --git a/pagure/forms.py b/pagure/forms.py
index 71e6542..85b1768 100644
--- a/pagure/forms.py
+++ b/pagure/forms.py
@@ -416,7 +416,7 @@ class AssignIssueForm(PagureForm):
     ''' Form to assign an user to an issue. '''
     assignee = wtforms.TextField(
         'Assignee <span class="error">*</span>',
-        [wtforms.validators.Required()]
+        [wtforms.validators.Optional()]
     )
 
 
diff --git a/pagure/lib/__init__.py b/pagure/lib/__init__.py
index 38886c1..c3efb33 100644
--- a/pagure/lib/__init__.py
+++ b/pagure/lib/__init__.py
@@ -384,7 +384,7 @@ def add_issue_assignee(session, issue, assignee, user, ticketfolder,
     ''' Add an assignee to an issue, in other words, assigned an issue. '''
     user_obj = get_user(session, user)
 
-    if assignee is None and issue.assignee is not None:
+    if not assignee and issue.assignee is not None:
         issue.assignee_id = None
         issue.last_updated = datetime.datetime.utcnow()
         session.add(issue)
@@ -413,8 +413,8 @@ def add_issue_assignee(session, issue, assignee, user, ticketfolder,
                 {'unassigned': '-'}))
 
         return 'Assignee reset'
-    elif assignee is None and issue.assignee is None:
-        return
+    elif not assignee and issue.assignee is None:
+        return 'Nothing to change'
 
     # Validate the assignee
     assignee_obj = get_user(session, assignee)
diff --git a/pagure/templates/issue.html b/pagure/templates/issue.html
index 0e8ac65..12fee5c 100644
--- a/pagure/templates/issue.html
+++ b/pagure/templates/issue.html
@@ -192,6 +192,12 @@
                     assignee=issue.assignee.username) }}">
                     {{ issue.assignee.username }}
                   </a>
+		{% if authenticated and (issue.assignee.username == g.fas_user.username) %}
+		  <button class="btn btn-sm pull-xs-right" id="drop-btn"
+			  title="drop the assignment of this issue">
+		    Drop
+		  </button>
+		{% endif %}
               {% else %}
                 unassigned
               {% endif %}
@@ -698,6 +704,60 @@ function try_async_comment(form) {
 </script>
 {% endif %}
 <script type="text/javascript">
+
+{% if authenticated and g.repo_admin %}
+function take_issue(){
+  var _url = "{{ url_for('api_ns.api_assign_issue', repo=repo.name, username=username, issueid=issueid) }}";
+  var _data = {assignee: "{{ g.fas_user.username }}"};
+  $.post (_url, _data ).done(
+    function(data) {
+      var _user_url = '\n<a href="{{ url_for("view_issues", repo=repo.name, username=username) }}'
+        + '?assignee={{ g.fas_user.username }}">'
+        + '{{ g.fas_user.username }}</a>'
+        + '<button class="btn btn-sm pull-xs-right" id="drop-btn" title="drop the assignment of this issue">Drop</button>';
+      $('#assignee_plain').html(_user_url);
+      $('#assignee').val("{{ g.fas_user.username }}");
+      setup_btn_take_drop();
+    }
+  )
+  return false;
+}
+{% endif %}
+
+{% if authenticated and (
+    g.repo_admin
+    or issue.user.user == g.fas_user.username
+    or issue.assignee.user == g.fas_user.username) %}
+function drop_issue(){
+  var _url = "{{ url_for(
+      'api_ns.api_assign_issue', repo=repo.name, username=username, issueid=issueid
+      ) }}";
+  var _data = {assignee: ""};
+  $.post( _url, _data ).done(
+    function(data) {
+      var _user_url = '\nunassigned'
+        + '<button class="btn btn-sm pull-xs-right" id="take-btn" title="assign this issue to you">Take</button>';
+      $('#assignee_plain').html(_user_url);
+      $('#assignee').val("");
+      setup_btn_take_drop();
+    }
+  )
+  return false;
+}
+{% endif %}
+
+function setup_btn_take_drop(){
+  {% if authenticated and g.repo_admin %}
+  $("#take-btn").click(take_issue)
+  {% endif %}
+  {% if authenticated and (
+    g.repo_admin
+    or issue.user.user == g.fas_user.username
+    or issue.assignee.user == g.fas_user.username) %}
+  $("#drop-btn").click(drop_issue);
+  {% endif %}
+}
+
 $( document ).ready(function() {
 
 
@@ -869,31 +929,12 @@ $( document ).ready(function() {
       }
     }
   );
-
-  {% if authenticated and g.repo_admin %}
-    $("#take-btn").click(function(){
-      var _url = "{{ url_for(
-        'api_ns.api_assign_issue',
-        repo=repo.name,
-        username=username,
-        namespace=repo.namespace,
-        issueid=issueid
-        ) }}";
-      var _data = {assignee: "{{ g.fas_user.username }}"};
-      $.post( _url, _data ).done(
-        function(data) {
-          var _user_url = '\n<a href="{{ url_for("view_issues",
-            repo=repo.name,
-            username=username,
-            namespace=repo.namespace) }}'
-            + '?assignee={{ g.fas_user.username }}">'
-            + '{{ g.fas_user.username }}</a>';
-          $('#assignee_plain').html(_user_url);
-          $('#assignee').val("{{ g.fas_user.username }}");
-        }
-      )
-      return false;
-    });
+  
+  {% if authenticated and (
+    g.repo_admin
+    or issue.user.user == g.fas_user.username
+    or issue.assignee.user == g.fas_user.username) %}
+  setup_btn_take_drop();
   {% endif %}
 
   {% if authenticated %}

From 850e72f80265642e1ec58ed0644bab9c5b92094d Mon Sep 17 00:00:00 2001
From: Pierre-Yves Chibon <pingou@pingoured.fr>
Date: Dec 08 2016 17:54:31 +0000
Subject: [PATCH 2/2] Fix unit tests for dropping assignments


Signed-off-by: Paul W. Frields <stickster@gmail.com>

---

diff --git a/pagure/api/issue.py b/pagure/api/issue.py
index 1ef6edf..41ed130 100644
--- a/pagure/api/issue.py
+++ b/pagure/api/issue.py
@@ -825,7 +825,7 @@ def api_assign_issue(repo, issueid, username=None, namespace=None):
 
     form = pagure.forms.AssignIssueForm(csrf_enabled=False)
     if form.validate_on_submit():
-        assignee = form.assignee.data
+        assignee = form.assignee.data or None
         try:
             # New comment
             message = pagure.lib.add_issue_assignee(
diff --git a/tests/test_pagure_flask_api_issue.py b/tests/test_pagure_flask_api_issue.py
index dec2bb1..da78b94 100644
--- a/tests/test_pagure_flask_api_issue.py
+++ b/tests/test_pagure_flask_api_issue.py
@@ -1443,34 +1443,76 @@ class PagureFlaskApiIssuetests(tests.Modeltests):
         issue = pagure.lib.search_issues(self.session, repo, issueid=1)
         self.assertEqual(len(issue.comments), 0)
 
+        # No change
+        repo = pagure.lib.get_project(self.session, 'test')
+        issue = pagure.lib.search_issues(self.session, repo, issueid=1)
+        self.assertEqual(issue.status, 'Open')
+
         data = {
-            'title': 'test issue',
+            'assignee': 'pingou',
         }
 
-        # Incomplete request
+        # Valid request
         output = self.app.post(
             '/api/0/test/issue/1/assign', data=data, headers=headers)
-        self.assertEqual(output.status_code, 400)
+        self.assertEqual(output.status_code, 200)
         data = json.loads(output.data)
         self.assertDictEqual(
             data,
-            {
-              "error": "Invalid or incomplete input submited",
-              "error_code": "EINVALIDREQ",
-              "errors": {"assignee": ["This field is required."]}
-            }
+            {'message': 'Issue assigned'}
         )
 
-        # No change
+        # Un-assign
+        output = self.app.post(
+            '/api/0/test/issue/1/assign', data=data, headers=headers)
+        self.assertEqual(output.status_code, 200)
+        data = json.loads(output.data)
+        self.assertDictEqual(
+            data,
+            {'message': 'Assignee reset'}
+        )
         repo = pagure.lib.get_project(self.session, 'test')
         issue = pagure.lib.search_issues(self.session, repo, issueid=1)
-        self.assertEqual(issue.status, 'Open')
+        self.assertEqual(issue.assignee, None)
 
-        data = {
-            'assignee': 'pingou',
-        }
+        # Un-assign
+        data = {'assignee': None}
+        output = self.app.post(
+            '/api/0/test/issue/1/assign', data=data, headers=headers)
+        self.assertEqual(output.status_code, 200)
+        data = json.loads(output.data)
+        self.assertDictEqual(
+            data,
+            {'message': 'Nothing to change'}
+        )
+        repo = pagure.lib.get_project(self.session, 'test')
+        issue = pagure.lib.search_issues(self.session, repo, issueid=1)
+        self.assertEqual(issue.assignee, None)
 
-        # Valid request
+        # Re-assign for the rest of the tests
+        data = {'assignee': 'pingou'}
+        output = self.app.post(
+            '/api/0/test/issue/1/assign', data=data, headers=headers)
+        self.assertEqual(output.status_code, 200)
+        data = json.loads(output.data)
+        self.assertDictEqual(
+            data,
+            {'message': 'Issue assigned'}
+        )
+
+        # Un-assign
+        data = {'assignee': ''}
+        output = self.app.post(
+            '/api/0/test/issue/1/assign', data=data, headers=headers)
+        self.assertEqual(output.status_code, 200)
+        data = json.loads(output.data)
+        self.assertDictEqual(
+            data,
+            {'message': 'Assignee reset'}
+        )
+
+        # Re-assign for the rest of the tests
+        data = {'assignee': 'pingou'}
         output = self.app.post(
             '/api/0/test/issue/1/assign', data=data, headers=headers)
         self.assertEqual(output.status_code, 200)

