Merge branch 'dev' into main
This commit is contained in:
commit
3fcd50ad45
6 changed files with 85 additions and 13 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -14,3 +14,6 @@ templates/*
|
|||
|
||||
# Logs
|
||||
marvin.log
|
||||
|
||||
# pycache
|
||||
__pycache__/
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
||||
|
|
6
exceptions.py
Normal file
6
exceptions.py
Normal file
|
@ -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."
|
69
main.py
69
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)
|
||||
|
|
|
@ -8,3 +8,4 @@ python-redmine==2.3.0
|
|||
PyYAML==5.4.1
|
||||
requests==2.25.1
|
||||
urllib3==1.26.5
|
||||
pytz~=2021.3
|
|
@ -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
|
Loading…
Add table
Add a link
Reference in a new issue