import base64 import os import re from datetime import datetime from difflib import SequenceMatcher from operator import itemgetter from typing import Any, Dict, List, Tuple import requests from django.core.cache import cache from django.utils.timezone import make_aware from rest_framework import exceptions from config.utils.datetime import get_now from .models import Issue, Reciever, Status data_acq_url = os.environ.get('DATA_ACQUISITION_URL') redmine_params = '?limit=200&offset=0' def usernames_match(u1: str, u2: str, min_ratio: float = 0.84) -> Tuple[bool, float]: def normalize_username(u: str) -> str: return u.replace('.', ' ').lower() ratio = SequenceMatcher(None, normalize_username(u1), normalize_username(u2)).quick_ratio() if ratio >= min_ratio: return (True, ratio) return (False, ratio) class RedmineStatus: NEW = 1 IN_PROGRESS = 2 CLOSED = 5 DELETED = -1 def get_redmine_user_by_username( config: Dict[str, Any], username: str, project_identifier: str, ) -> Dict[str, Any] | None: api_url = config['api-url'] url = f'{api_url}projects/{project_identifier}/memberships.json{redmine_params}' headers = {'X-Redmine-API-Key': config['api-key']} response = requests.get(url, headers=headers) if (response.status_code == 200 and response.json()): memberships = response.json()['memberships'] matched_users: List[Tuple[float, Dict[str, Any]]] = [] for i in memberships: user = i.get('user', None) if user: redmine_username = user.get('name', '') # append matched user to list to get highest match later match = usernames_match(username, redmine_username) if match[0]: matched_users.append((match[1], i['user'])) # return user with highest match if matched_users: return max(matched_users, key=itemgetter(0))[1] return None def get_user(config: Dict[str, Any], user_id: int, project_identifier: str) -> List[Dict[str, Any]]: url = f'{data_acq_url}sebn-data-acquisition/api/sebn-taskbar-manager/employee-info/' response = requests.post(url, json={'id': user_id}) if (response.status_code == 200 and response.json()): employee = response.json()[0] api_url = config['api-url'] url = f'{api_url}projects/{project_identifier}/memberships.json{redmine_params}' headers = {'X-Redmine-API-Key': config['api-key']} response = requests.get(url, json={'id': user_id}, headers=headers) if (response.status_code == 200 and response.json()): memberships = response.json()['memberships'] user = [ i for i in memberships if employee['full_name_en'] in i['user']['name'] ] return user raise exceptions.APIException( code='503', detail="Can't fetch redmine memberships.") raise exceptions.APIException(code='503', detail="Can't fetch employee.") def is_redmine(reciever: Reciever) -> int: redmine_match = re.match(r'redmine', getattr(reciever, 'name', ''), re.IGNORECASE) return True if redmine_match else False def create_redmine_issue(reciever: Reciever, data: Dict[str, Any]) -> Tuple[bool, Dict[str, Any]]: def generate_json(data: Dict[str, Any]) -> dict[str, Any]: issue = {} # { # issue: { # project_id # tracker_id # status_id # priority_id # subject # description # category_id # fixed_version_id # assigned_to_id # parent_issue_id # custom_fields # watcher_user_ids # is_private # estimated_hours # } # } category = data.get('category', 'No category') department = data.get('department', None) if not department: raise exceptions.ValidationError( "Can't create redmine issue: department is None") project_id = getattr( category, 'configuration', {}).get('redmine', {}).get( 'project_id', None) or department.configuration.get( 'redmine', {}).get('identifier', None) if not project_id: raise exceptions.ValidationError( "Can't create redmine issue: project_id is None") issue['project_id'] = project_id issue['subject'] = data['title'] issue['description'] = data['description'] issue['priority_id'] = data['priority'] issue['uploads'] = data.get('uploads', []) return {"issue": issue} api_url = reciever.backend_configuration['api-url'] url = f'{api_url}issues.json' headers = { 'Content-Type': 'application/json; charset=utf-8', 'X-Redmine-API-Key': reciever.backend_configuration['api-key'] } json_data = generate_json(data) response = requests.post(url, json=json_data, headers=headers) if (response.status_code == 201): json_body = response.json() uploads = json_data['issue']['uploads'] issue_id = int(json_body['issue']['id']) # Dont forget change tgbot.handlers.helpdesk.handlers.parse_issue regex on text change href = f'{api_url}issues/{issue_id}' priority = reciever.configuration.get('priority', {}).get('data', {}).get( str(data['priority']), '') priority_emoji = "" match data['priority']: case 1: priority_emoji = '⚪' case 2: priority_emoji = '🔵' case 3: priority_emoji = '🟡' case 4: priority_emoji = '🟠' case 5: priority_emoji = '🔴' priority_text = f'{priority_emoji} {priority.lower()}' if priority else '' text = f'Redmine issue #{issue_id} {"🖼️" if uploads else ""} {priority_text}' return (True, {'issue_id': issue_id, 'text': text}) return (False, { 'status_code': response.status_code, 'text': response.text }) def fetch_redmine_issue(config: Dict[str, Any], id: int, query_params: str | None = None) -> Any: api_url = config['api-url'] url = f'{api_url}issues/{id}.json?{query_params}' headers = {'X-Redmine-API-Key': config['api-key']} response = requests.get(url, headers=headers) if (response.status_code == 200): return response.json() if (response.status_code == 404): values: Dict[str, Any] = { 'issue': { 'status': { 'id': -1, 'name': 'Deleted' }, 'closed_on': get_now().isoformat() } } return values return None def format_datetime(string: str) -> datetime: dt = string.replace("Z", "") return datetime.fromisoformat(dt) def parse_notes(journals: List[Dict[str, Any]]) -> str: notes = '' if journals: for i, val in enumerate(journals): if (val['notes'] and not val['private_notes']): note = f"{val['user']['name']} ({format_datetime(val['created_on'])}):\n {val['notes']}" notes = f'{note}\n\n{notes}' return notes def save_redmine_fields(issue: Issue, fields: Dict[str, Any]) -> Issue: issue_status_id = fields['issue']['status']['id'] cached_existed_status_ids = cache.get('status_ids', []) if not cached_existed_status_ids: cached_existed_status_ids = Status.objects.all().values( 'id', 'redmine_code', 'sysaid_code') cache.set('status_ids', cached_existed_status_ids) for s in cached_existed_status_ids: if s['redmine_code'] == issue_status_id: issue.status_id = s['id'] # type: ignore ## not cached old code # status = Status.objects.filter( # redmine_code=fields['issue']['status']['id']).first() # if status: issue.status = status if (fields['issue'].get('assigned_to')): issue.responsibility = fields['issue']['assigned_to']['name'] issue.comments = parse_notes(fields['issue'].get('journals')) if (fields['issue'].get('updated_on')): issue.update_datetime = make_aware( format_datetime(fields['issue']['updated_on'])) if (fields['issue'].get('closed_on')): issue.close_datetime = make_aware( format_datetime(fields['issue']['closed_on'])) issue.save() return issue def update_redmine_issue(config: Dict[str, Any], data: Dict[str, Any]) -> Tuple[bool, int]: api_url = config['api-url'] url = f'{api_url}issues/{data["issue"]["issue_id"]}.json' headers = {'X-Redmine-API-Key': config['api-key']} response = requests.put(url, json=data, headers=headers) if (response.status_code == 204): return (True, 204) return (False, response.status_code) def upload_file(config: Dict[str, Any], data_uri: Any, filename: str = '') -> Tuple[bool, str]: header, encoded = data_uri.split(",", 1) ext = header.split('/')[1] filename = filename or 'attachment' api_url = config['api-url'] url = f'{api_url}uploads.json?filename={filename}.{ext}' headers = { 'X-Redmine-API-Key': config['api-key'], 'Content-Type': 'application/octet-stream', } response = requests.post(url, data=base64.b64decode(encoded), headers=headers) if (response.status_code == 201): return (True, response.text) return (False, f'{response.status_code}: {response.text}')