Adding working Redmine integration
5 files changed, 149 insertions(+), 111 deletions(-)

M README.rst
A => pyproject.toml
M trello_broker/settings.py
M trello_broker/utils.py
M trello_broker/views.py
M README.rst +7 -0
@@ 5,6 5,13 @@ django-trello-broker
 :Version: 0.3
 :Author: Peter Sanchez <peter@netlandish.com> - Netlandish Inc. (http://www.netlandish.com)
 
+Attention
+=========
+
+DO NOT USE THIS BRANCH! This project is essentially abandoned as we no longer use Trello.
+
+This branch (redmine) in particular is just a band-aid until we create a more universal solution that will plan to share in the future. This branch is literally just a hack to get our current setup working and integrated with our Redmine install.
+
 Dependencies
 ============
 

          
A => pyproject.toml +29 -0
@@ 0,0 1,29 @@ 
+[tool.black]
+line-length = 79
+target-version = ['py37', 'py38']
+include = '\.pyi?$'
+exclude = '''
+
+(
+  /(
+      \.eggs         # exclude a few common directories in the
+    | \.git          # root of the project
+    | \.hg
+    | \.mypy_cache
+    | \.tox
+    | \.venv
+    | env
+    | _build
+    | buck-out
+    | build
+    | dist
+  )/
+  | foo.py           # also separately exclude a file named foo.py in
+                     # the root of the project
+)
+'''
+
+[tool.isort]
+line_length=79
+multi_line_output=5
+include_trailing_comma=true

          
M trello_broker/settings.py +42 -41
@@ 1,60 1,61 @@ 
 from django.conf import settings
 
-
 # Hard coded for now to avoid external requirement. Will change
 # if the list becomes unmanageable or once we're all moved over to Py3 :)
 
 # https://confluence.atlassian.com/bitbucket/what-are-the-bitbucket-cloud-ip-addresses-i-should-use-to-configure-my-corporate-firewall-343343385.html
 
 BITBUCKET_IP_ADDRESSES = [
-    '131.103.20.165',  # Remove after 1/10/2016
-    '131.103.20.166',  # Remove after 1/10/2016
-
-    '104.192.143.193', # 104.192.143.192/28
-    '104.192.143.194',
-    '104.192.143.195',
-    '104.192.143.196',
-    '104.192.143.197',
-    '104.192.143.198',
-    '104.192.143.199',
-    '104.192.143.200',
-    '104.192.143.201',
-    '104.192.143.202',
-    '104.192.143.203',
-    '104.192.143.204',
-    '104.192.143.205',
-    '104.192.143.206',
-
-    '104.192.143.209', # 104.192.143.208/28
-    '104.192.143.210',
-    '104.192.143.211',
-    '104.192.143.212',
-    '104.192.143.213',
-    '104.192.143.214',
-    '104.192.143.215',
-    '104.192.143.216',
-    '104.192.143.217',
-    '104.192.143.218',
-    '104.192.143.219',
-    '104.192.143.220',
-    '104.192.143.221',
-    '104.192.143.222',
+    "131.103.20.165",  # Remove after 1/10/2016
+    "131.103.20.166",  # Remove after 1/10/2016
+    "104.192.143.193",  # 104.192.143.192/28
+    "104.192.143.194",
+    "104.192.143.195",
+    "104.192.143.196",
+    "104.192.143.197",
+    "104.192.143.198",
+    "104.192.143.199",
+    "104.192.143.200",
+    "104.192.143.201",
+    "104.192.143.202",
+    "104.192.143.203",
+    "104.192.143.204",
+    "104.192.143.205",
+    "104.192.143.206",
+    "104.192.143.209",  # 104.192.143.208/28
+    "104.192.143.210",
+    "104.192.143.211",
+    "104.192.143.212",
+    "104.192.143.213",
+    "104.192.143.214",
+    "104.192.143.215",
+    "104.192.143.216",
+    "104.192.143.217",
+    "104.192.143.218",
+    "104.192.143.219",
+    "104.192.143.220",
+    "104.192.143.221",
+    "104.192.143.222",
 ]
 
 
-USE_CELERY = getattr(settings, 'TRELLO_BROKER_USE_CELERY', False)
+USE_CELERY = getattr(settings, "TRELLO_BROKER_USE_CELERY", False)
 
-RESTRICT_IPS = getattr(settings, 'TRELLO_BROKER_RESTRICT_IPS', False)
+RESTRICT_IPS = getattr(settings, "TRELLO_BROKER_RESTRICT_IPS", False)
 
 # https://confluence.atlassian.com/display/BITBUCKET/What+are+the+Bitbucket+IP+addresses+I+should+use+to+configure+my+corporate+firewall
 # Only needed if TRELLO_BROKER_RESTRICT_IPS is True
 BITBUCKET_IPS = getattr(
-    settings,
-    'TRELLO_BROKER_BITBUCKET_IPS',
-    BITBUCKET_IP_ADDRESSES,
+    settings, "TRELLO_BROKER_BITBUCKET_IPS", BITBUCKET_IP_ADDRESSES,
 )
 
-RULE_CLASSES = getattr(settings,
-    'TRELLO_BROKER_RULE_CLASSES',
-    ['trello_broker.rules.DefaultRuleProc'],
+RULE_CLASSES = getattr(
+    settings,
+    "TRELLO_BROKER_RULE_CLASSES",
+    ["trello_broker.rules.DefaultRuleProc"],
 )
+
+REDMINE_API_KEY = getattr(settings, "TRELLO_BROKER_REDMINE_API_KEY", None)
+
+# No trailing slash!
+REDMINE_ROOT_URL = getattr(settings, "TRELLO_BROKER_REDMINE_ROOT_URL", None)

          
M trello_broker/utils.py +48 -56
@@ 1,14 1,20 @@ 
+import json
 import re
+
+import requests
+from django.template.loader import render_to_string
+
+from . import settings
+
 try:
     from urlparse import urljoin
 except ImportError:
     from urllib.parse import urljoin
-from django.template.loader import render_to_string
-from . import api
 
 
 # Based on pattern from https://bitbucket.org/sntran/trello-broker
-re_pattern = re.compile(r'''
+re_pattern = re.compile(
+    r"""
     (               # start capturing the verb
     fix             # contains 'fix'
     | close         # or 'close'

          
@@ 19,79 25,65 @@ re_pattern = re.compile(r'''
     \s              # then a white space
     [#]             # and '#' to indicate the card
     ([0-9]+)        # with the card's short id.
-    ''',
+    """,
     re.VERBOSE | re.IGNORECASE,
 )
 
 
 def process_commits(repo, json_data):
-    ''' Function to parse commit messages and act on
+    """ Function to parse commit messages and act on
         BitBucketRule sets.
-    '''
-    base_repo_url = u'{0}{1}'.format(
-        json_data['canon_url'],
-        json_data['repository']['absolute_url'],
+    """
+    base_repo_url = "{0}{1}".format(
+        json_data["canon_url"], json_data["repository"]["absolute_url"],
     )
-    base_repo_url += '/' if base_repo_url[-1] != '/' else ''
-    base_commit_url = urljoin(base_repo_url, 'commits/')
-    trello_token = repo.trello_board.trello_token
+    base_repo_url += "/" if base_repo_url[-1] != "/" else ""
+    base_commit_url = urljoin(base_repo_url, "commits/")
 
-    for commit in json_data['commits']:
+    for commit in json_data["commits"]:
+        print("in commit")
         proc_cards = []
-        msg = commit['message']
+        msg = commit["message"]
         context = {
-            'author': commit['author'],
-            'author_full': commit['raw_author'],
-            'changeset': commit['node'],
-            'changeset_full': commit['raw_node'],
-            'commit_url': commit.get(
-                'commit_url',
-                urljoin(base_commit_url, commit['raw_node']),
+            "author": commit["author"],
+            "author_full": commit["raw_author"],
+            "changeset": commit["node"],
+            "changeset_full": commit["raw_node"],
+            "commit_url": commit.get(
+                "commit_url", urljoin(base_commit_url, commit["raw_node"]),
             ),
-            'commit_message': msg,
+            "commit_message": msg,
         }
 
         for action, card_id in re_pattern.findall(msg):
+            print(f"in {action} -> {card_id}")
             if card_id in proc_cards:
                 # Referenced more than once in the commit msg
                 continue
 
             proc_cards.append(card_id)
-            use_rule = 'fix_rule' \
-                if action.lower() in ['fix', 'close'] else 'ref_rule'
-            rule = getattr(repo, use_rule)
-            if not rule:
-                continue
 
-            # Get Card information
-            card = api.get_card_from_board(
-                trello_token,
-                repo.trello_board.trello_id,
-                card_id,
+            # Update the card.
+            card_msg = render_to_string(
+                "trello_broker/update_message.txt", context
             )
-            full_card_id = card['id']
+            update = {"issue": {"notes": card_msg,}}
 
-            # Let's process the rule
-            if rule.update:
-                # Update the card.
-                card_msg = render_to_string(
-                    'trello_broker/update_message.txt',
-                    context,
-                )
-                api.add_comment_to_card(
-                    trello_token,
-                    full_card_id,
-                    card_msg,
-                )
+            if action.lower() == "close":
+                # FYI, this is hard coded for our own use case.
+                # DO NOT USE!
+                print("updating status")
+                update["issue"]["status_id"] = 15
+            print(f"Final result: {update}")
 
-            if rule.move:
-                # Move the card
-                api.move_card(
-                    trello_token,
-                    full_card_id,
-                    rule.trello_list.trello_id,
-                )
-
-            if rule.archive:
-                # Archive the card
-                api.archive_card(trello_token, full_card_id)
+            result = requests.put(
+                f"{settings.REDMINE_ROOT_URL}/issues/{card_id}.json",
+                data=json.dumps(update),
+                headers={
+                    "X-Redmine-API-Key": settings.REDMINE_API_KEY,
+                    "Content-Type": "application/json",
+                },
+            )
+            if result.status_code >= 400:
+                # XXX Log something
+                pass

          
M trello_broker/views.py +23 -14
@@ 1,12 1,14 @@ 
 import json
-from django.views.generic import View
+
+from django.http import HttpResponse, HttpResponseBadRequest
 from django.shortcuts import get_object_or_404
-from django.views.decorators.csrf import csrf_exempt
 from django.utils.decorators import method_decorator
-from django.http import HttpResponse, HttpResponseBadRequest, Http404
-from .models import Q, BitBucketRepo
+from django.views.decorators.csrf import csrf_exempt
+from django.views.generic import View
+
+from . import settings
+from .models import BitBucketRepo, Q
 from .utils import process_commits
-from . import settings
 
 if settings.USE_CELERY:
     from .tasks import celery_process_commits

          
@@ 15,9 17,9 @@ else:
 
 
 def trello_distribute(repo, json_data):
-    ''' Helper function that decides how to process
+    """ Helper function that decides how to process
         Trello interaction (celery or not)
-    '''
+    """
     if celery_process_commits is not None:
         celery_process_commits.delay(repo.pk, json_data)
     else:

          
@@ 29,22 31,29 @@ class BitBucketPostView(View):
         return HttpResponseBadRequest()
 
     def post(self, request, *args, **kwargs):
+        if settings.REDMINE_API_KEY is None:
+            return HttpResponseBadRequest("Redmine API key required.")
+
+        if settings.REDMINE_ROOT_URL is None:
+            return HttpResponseBadRequest("Redmine API key required.")
+
         if settings.RESTRICT_IPS:
-            if request.META['REMOTE_ADDR'] not in settings.BITBUCKET_IPS:
+            if request.META["REMOTE_ADDR"] not in settings.BITBUCKET_IPS:
                 return HttpResponseBadRequest()
 
         try:
-            json_data = json.loads(request.POST['payload'])
-            repo_slug = json_data['repository']['slug']
-        except (ValueError, KeyError) as e:
+            json_data = json.loads(request.POST["payload"])
+            repo_slug = json_data["repository"]["slug"]
+        except (ValueError, KeyError):
             return HttpResponseBadRequest()
 
-        access_key = \
-            request.POST.get('access_key', request.GET.get('access_key', ''))
+        access_key = request.POST.get(
+            "access_key", request.GET.get("access_key", "")
+        )
         query = Q(slug=repo_slug) & Q(access_key=access_key)
         repo = get_object_or_404(BitBucketRepo, query)
         trello_distribute(repo, json_data)
-        return HttpResponse('OK')
+        return HttpResponse("OK")
 
     @method_decorator(csrf_exempt)
     def dispatch(self, *args, **kwargs):