diff --git a/.gitignore b/.gitignore index e592bf9..053f1e7 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,6 @@ templates/* # Logs marvin.log + +# pycache +__pycache__/ diff --git a/example_config.yaml b/example_config.yaml index 1edbed1..ffad2a9 100644 --- a/example_config.yaml +++ b/example_config.yaml @@ -4,8 +4,9 @@ redmine: url: ${REDMINE_URL} version: ${REDMINE_VERSION} api_key: ${REDMINE_API_KEY} - issue_closed_id: 5 # Id of the "closed" status - + time_zone: "Europe/Berlin" + issue_closed_status: "Done" # Name of the "closed" status + no_bot_tag: "#nobot" # a tag that signals bot not to handle an issue actions: @@ -26,7 +27,7 @@ actions: - "IT Tickets" status: - "In revision" - change_status_to: 4 + change_status_to: "Waiting for feedback" template: "nudge_ticket" diff --git a/exceptions.py b/exceptions.py new file mode 100644 index 0000000..0b516e9 --- /dev/null +++ b/exceptions.py @@ -0,0 +1,6 @@ +class IssueStatusNotFoundException(Exception): + def __init__(self, status_name: str): + self.status_name = status_name + + def __str__(self): + return f"No issue status with the name \"{self.status_name}\" could be found. Please check your config.yaml." diff --git a/main.py b/main.py index a33a23b..0ec93eb 100644 --- a/main.py +++ b/main.py @@ -1,15 +1,27 @@ #!/usr/bin/env python3 import sys import os -from datetime import date, timedelta +from datetime import date, datetime, timedelta +import pytz import logging from redminelib import Redmine from envyaml import EnvYAML from jinja2 import Template +from exceptions import IssueStatusNotFoundException + + +def get_issue_status_id(status_name: str, redmine: Redmine): + all_status = redmine.issue_status.all() + statuses = all_status.filter(name=status_name) + if len(statuses) < 1: + raise IssueStatusNotFoundException(status_name) + elif len(statuses) > 1: + logging.warning(f'Expected one issue status with the name {status_name} but found ' + f'{len(statuses)}') + return statuses[0].id def treat_issues(): - dir_path = os.path.dirname(os.path.realpath(__file__)) + '/' # Set up logging @@ -25,6 +37,9 @@ def treat_issues(): url = config['redmine']['url'] version = config['redmine']['version'] api_key = config['redmine']['api_key'] + time_zone = pytz.timezone(config['redmine']['time_zone']) + issue_closed_status = config['redmine']['issue_closed_status'] + no_bot_tag = config['redmine']['no_bot_tag'] actions = config['actions'] logging.getLogger().setLevel(config['logging']['level'].upper()) except Exception as e: @@ -38,28 +53,68 @@ def treat_issues(): logging.error('Could not instantiate Redmine object', exc_info=True) sys.exit(1) + # Get status id for closed issues + issue_closed_status_id = None + try: + issue_closed_status_id = get_issue_status_id(issue_closed_status, redmine) + except IssueStatusNotFoundException as e: + logging.error(e, exc_info=True) + # Loop through all actions defined in config.yaml for action in actions.values(): # Calculate end_date end_date = date.today() - timedelta(days=+int(action['time_range'])) + # Get change_status_id + change_status_id = None + if action.get('change_status_to'): + try: + change_status_id = get_issue_status_id(action['change_status_to'], redmine) + except IssueStatusNotFoundException as e: + logging.error(e, exc_info=True) + continue + # Loop through affected issues try: for issue in redmine.issue \ .filter(updated_on=f"><{action['start_date']}|{end_date.isoformat()}") \ .filter(project__name__in=action['projects'], status__name__in=action['status'], closed_on=None): + + # Skip issue if start date is not yet reached + if hasattr(issue, 'start_date') and date.today() < issue.start_date: + continue + + # Skip issue if due date is not yet reached + if hasattr(issue, 'due_date') and date.today() < issue.due_date: + continue + + # Skip issue if a no_bot_tag is found in the issue description or any of its journals + def find_no_bot_tag_in_journals(journals): + for journal in journals: + if hasattr(journal, 'notes') and no_bot_tag in journal.notes: + return True + return False + if no_bot_tag in issue.description or find_no_bot_tag_in_journals(issue.journals): + continue + + # Render message template with open(f"{dir_path}templates/{action['template']}", newline='\r\n') as f: content = f.read() template = Template(content) - notes = template.render(author=issue.author.name, time_range=action['time_range'], url=issue.url) + days_since_last_update = (datetime.now(time_zone) - issue.updated_on.replace(tzinfo=time_zone)).days + 1 + notes = template.render( + issue=issue, + time_range=action['time_range'], + days_since_last_update=days_since_last_update + ) # Update issue - if action.get('close_ticket') is True: - redmine.issue.update(issue.id, notes=notes, status_id=config['redmine']['issue_closed_id']) + if action.get('close_ticket') is True and issue_closed_status_id is not None: + redmine.issue.update(issue.id, notes=notes, status_id=issue_closed_status_id) logging.info(f"Ticket ID: {issue.id}, ticket closed") - elif action.get('change_status_to') is not None and isinstance(action.get('change_status_to'), int): - redmine.issue.update(issue.id, notes=notes, status_id=action['change_status_to']) + elif change_status_id is not None: + redmine.issue.update(issue.id, notes=notes, status_id=change_status_id) logging.info(f"Ticket ID: {issue.id}, changed ticket status") else: redmine.issue.update(issue.id, notes=notes) diff --git a/requirements.txt b/requirements.txt index 1fdc5f6..ff66481 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,3 +8,4 @@ python-redmine==2.3.0 PyYAML==5.4.1 requests==2.25.1 urllib3==1.26.5 +pytz~=2021.3 \ No newline at end of file diff --git a/templates/example_template b/templates/example_template index 94a5b1f..4d76ef5 100644 --- a/templates/example_template +++ b/templates/example_template @@ -1,7 +1,13 @@ -Hello {{ author }}, +{% if issue.assigned_to is defined %} +Hello {{ issue.assigned_to }}, -this ticket wasn't updated for at least {{ time_range }}. We therefore assume that the problem has been solved in the meantime. If the issue persists, please reopen the ticket and give us a brief update on the situation. -Here is the ticket: {{ url }} +this ticket is assigned to you but wasn't updated for at least {{ days_since_last_update }}. We therefore assume that the problem has been solved in the meantime. If the issue persists, please reopen the ticket and give us a brief update on the situation. +{% else %} +Hello {{ issue.author }}, + +this ticket wasn't updated for at least {{ days_since_last_update }}. We therefore assume that the problem has been solved in the meantime. If the issue persists, please reopen the ticket and give us a brief update on the situation. +{% endif %} +Here is the ticket: {{ issue.url }} Yours sincerely Your IT Team \ No newline at end of file