From 1c1e21903741a6d00b6f25a5242ad893927f202e Mon Sep 17 00:00:00 2001 From: T'rahk Rokym Date: Mon, 21 Apr 2025 01:12:40 +0200 Subject: [PATCH] Basic implementation of app hooks Still need to remove the examples and add tests --- .../allianceauth/admin-status/overview.html | 30 +++++ allianceauth/templatetags/admin_status.py | 119 +++++++++++++++++- 2 files changed, 147 insertions(+), 2 deletions(-) diff --git a/allianceauth/templates/allianceauth/admin-status/overview.html b/allianceauth/templates/allianceauth/admin-status/overview.html index 47484a57..482c1cc8 100644 --- a/allianceauth/templates/allianceauth/admin-status/overview.html +++ b/allianceauth/templates/allianceauth/admin-status/overview.html @@ -46,6 +46,36 @@ {% endif %} +{% if application_notifications %} +
+
+
+ {% translate "Application Notifications" as widget_title %} + {% include "framework/dashboard/widget-title.html" with title=widget_title %} + +
+ + + {# TODO maybe add some disclaimer that those are managed by application devs? #} + +
+
+
+
+{% endif %} +
diff --git a/allianceauth/templatetags/admin_status.py b/allianceauth/templatetags/admin_status.py index d7084dc5..d576d474 100644 --- a/allianceauth/templatetags/admin_status.py +++ b/allianceauth/templatetags/admin_status.py @@ -1,4 +1,7 @@ import logging +from dataclasses import dataclass +from enum import Enum, auto +from urllib.parse import quote_plus import requests from packaging.version import InvalidVersion, Version as Pep440Version @@ -7,10 +10,11 @@ from django import template from django.conf import settings from django.core.cache import cache -from allianceauth import __version__ +from allianceauth import __version__, hooks from allianceauth.authentication.task_statistics.counters import ( dashboard_results, ) +from allianceauth.hooks import get_hooks register = template.Library() @@ -32,6 +36,63 @@ GITLAB_AUTH_ANNOUNCEMENT_ISSUES_URL = ( logger = logging.getLogger(__name__) +class RepositoryKind(Enum): + """What kind of repository is being used""" + GITLAB = auto() + GITHUB = auto() + +@dataclass +class AppAnnouncementHook: + """Hook for an application to send GitHub/GitLab issues as announcements""" + app_name: str + repository_namespace: str + repository_kind: RepositoryKind + label: str = "announcement" + + + def get_announcement_list(self) -> list: + """ + Checks the application repository to find issues with the `Announcement` tag and return their title and link to + be displayed. + """ + match self.repository_kind: + case RepositoryKind.GITHUB: + announcement_list = self._get_github_announcement_list() + case RepositoryKind.GITLAB: + announcement_list = self._get_gitlab_announcement_list() + case _: + return [] + + for announcement in announcement_list: + announcement["app_name"] = self.app_name + + return announcement_list + + + def _get_github_announcement_list(self) -> list: + """ + Return the issue list for a GitHub repository + Will filter if the `pull_request` attribute is present + """ + raw_list = _fetch_list_from_github( + f"https://api.github.com/repos/{self.repository_namespace}/issues" + f"?labels={self.label}&state=all" + ) + return [element for element in raw_list if not element.get("pull_request")] + + def _get_gitlab_announcement_list(self) -> list: + """Return the issues list for a GitLab repository""" + return _fetch_list_from_gitlab( + f"https://gitlab.com/api/v4/projects/{quote_plus(self.repository_namespace)}/issues" + f"?labels={self.label}") + +@hooks.register("app_announcement_hook") +def test_hook(): + return AppAnnouncementHook("test GitHub app", "r0kym/allianceauth-example-plugin", RepositoryKind.GITLAB) + +@hooks.register("app_announcement_hook") +def test_hook_2(): + return AppAnnouncementHook("test GitHub app", "r0kym/test", RepositoryKind.GITHUB) @register.simple_tag() def decimal_widthratio(this_value, max_value, max_width) -> str: @@ -80,17 +141,36 @@ def _current_notifications() -> dict: _fetch_notification_issues_from_gitlab, NOTIFICATION_CACHE_TIME ) + app_notifications = [] + hooks = get_hooks("app_announcement_hook") + items = [fn() for fn in hooks] + for hook in items: + app_notifications.extend(hook.get_announcement_list()) + """ + app_notifications.extend(cache.get_or_set( + f"{hook.app_name}_notification_issues", + hook.get_announcement_list, + NOTIFICATION_CACHE_TIME, + )) + """ except requests.HTTPError: logger.warning('Error while getting gitlab notifications', exc_info=True) top_notifications = [] + application_notifications = [] else: if notifications: top_notifications = notifications[:5] else: top_notifications = [] + if app_notifications: + application_notifications = app_notifications[:10] + else: + application_notifications = [] + response = { 'notifications': top_notifications, + 'application_notifications': application_notifications, } return response @@ -123,7 +203,7 @@ def _current_version_summary() -> dict: has_current_beta = \ current_version <= latest_beta_version \ and latest_patch_version <= latest_beta_version \ - if latest_beta_version else False + if latest_beta_version else False response = { 'latest_patch': has_latest_patch, @@ -199,3 +279,38 @@ def _fetch_list_from_gitlab(url: str, max_pages: int = MAX_PAGES) -> list: break return result + +def _fetch_list_from_github(url: str, max_pages: int = MAX_PAGES) -> list: + """returns a list from the GitHub API. Supports paging""" + # TODO actual paging + + result = [] + for page in range(1, max_pages+1): + try: + request = requests.get( + url, + params={'page': page}, + headers={ + "Accept": "application/vnd.github+json", + "X-GitHub-Api-Version": "2022-11-28" + } + ) + request.raise_for_status() + except requests.exceptions.RequestException as e: + error_str = str(e) + + logger.warning( + f'Unable to fetch from GitHub API. Error: {error_str}', + exc_info=True, + ) + + return result + + result += request.json() + + if 'link' in request.headers and 'rel=\"next\"' in request.headers['link']: + continue + + break + + return result