From 1c1e21903741a6d00b6f25a5242ad893927f202e Mon Sep 17 00:00:00 2001 From: T'rahk Rokym Date: Mon, 21 Apr 2025 01:12:40 +0200 Subject: [PATCH 01/16] 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 From cd9d985732f767cf5dc718e704cd63cee31b2ba8 Mon Sep 17 00:00:00 2001 From: T'rahk Rokym Date: Mon, 21 Apr 2025 12:25:53 +0200 Subject: [PATCH 02/16] Upgrades - Error handling per hook - Fix github redirect - Better code for github pagination --- .../allianceauth/admin-status/overview.html | 2 +- allianceauth/templatetags/admin_status.py | 48 ++++++++++--------- 2 files changed, 26 insertions(+), 24 deletions(-) diff --git a/allianceauth/templates/allianceauth/admin-status/overview.html b/allianceauth/templates/allianceauth/admin-status/overview.html index 482c1cc8..eff882d3 100644 --- a/allianceauth/templates/allianceauth/admin-status/overview.html +++ b/allianceauth/templates/allianceauth/admin-status/overview.html @@ -63,7 +63,7 @@ {% translate "Closed" %} {% endif %} {{ notif.app_name }} - #{{ notif.iid }}{{ notif.number }} {{ notif.title }} + #{{ notif.iid }}{{ notif.number }} {{ notif.title }} {% endfor %} diff --git a/allianceauth/templatetags/admin_status.py b/allianceauth/templatetags/admin_status.py index d576d474..989a820e 100644 --- a/allianceauth/templatetags/admin_status.py +++ b/allianceauth/templatetags/admin_status.py @@ -88,7 +88,7 @@ class AppAnnouncementHook: @hooks.register("app_announcement_hook") def test_hook(): - return AppAnnouncementHook("test GitHub app", "r0kym/allianceauth-example-plugin", RepositoryKind.GITLAB) + return AppAnnouncementHook("test GitLab app", "r0kym/allianceauth-example-plugin", RepositoryKind.GITLAB) @hooks.register("app_announcement_hook") def test_hook_2(): @@ -141,10 +141,20 @@ 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: + except requests.HTTPError: + logger.warning('Error while getting gitlab notifications', exc_info=True) + top_notifications = [] + else: + if notifications: + top_notifications = notifications[:5] + else: + top_notifications = [] + + app_notifications = [] + hooks = [fn() for fn in get_hooks("app_announcement_hook")] + for hook in hooks: + logger.info(hook) + try: app_notifications.extend(hook.get_announcement_list()) """ app_notifications.extend(cache.get_or_set( @@ -153,20 +163,13 @@ def _current_notifications() -> dict: 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 = [] + except requests.HTTPError: + logger.warning("Error when getting %s notifications", hook, exc_info=True) - if app_notifications: - application_notifications = app_notifications[:10] - else: - application_notifications = [] + if app_notifications: + application_notifications = app_notifications[:10] + else: + application_notifications = [] response = { 'notifications': top_notifications, @@ -282,7 +285,6 @@ def _fetch_list_from_gitlab(url: str, max_pages: int = MAX_PAGES) -> list: 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): @@ -308,9 +310,9 @@ def _fetch_list_from_github(url: str, max_pages: int = MAX_PAGES) -> list: result += request.json() - if 'link' in request.headers and 'rel=\"next\"' in request.headers['link']: - continue - - break + # https://docs.github.com/en/rest/using-the-rest-api/using-pagination-in-the-rest-api?apiVersion=2022-11-28 + # See Example creating a pagination metho + if not ('link' in request.headers and 'rel=\"next\"' in request.headers['link']): + break return result From ec34d7fd29048b49f59dff2fb302c987e29373e3 Mon Sep 17 00:00:00 2001 From: T'rahk Rokym Date: Mon, 21 Apr 2025 12:35:03 +0200 Subject: [PATCH 03/16] Properly translates GitHub attributes to GitLab --- .../templates/allianceauth/admin-status/overview.html | 2 +- allianceauth/templatetags/admin_status.py | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/allianceauth/templates/allianceauth/admin-status/overview.html b/allianceauth/templates/allianceauth/admin-status/overview.html index eff882d3..2c8e685f 100644 --- a/allianceauth/templates/allianceauth/admin-status/overview.html +++ b/allianceauth/templates/allianceauth/admin-status/overview.html @@ -63,7 +63,7 @@ {% translate "Closed" %} {% endif %} {{ notif.app_name }} - #{{ notif.iid }}{{ notif.number }} {{ notif.title }} + #{{ notif.iid }} {{ notif.title }} {% endfor %} diff --git a/allianceauth/templatetags/admin_status.py b/allianceauth/templatetags/admin_status.py index 989a820e..ca055345 100644 --- a/allianceauth/templatetags/admin_status.py +++ b/allianceauth/templatetags/admin_status.py @@ -78,7 +78,14 @@ class AppAnnouncementHook: 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")] + # Translates GitHub attributes to GitLab and filters out pull requests + clean_list = [] + for element in raw_list: + if not element.get("pull_request"): + element["web_url"] = element["html_url"] + element["iid"] = element["number"] + clean_list.append(element) + return clean_list def _get_gitlab_announcement_list(self) -> list: """Return the issues list for a GitLab repository""" From f88249c8fce4a078a49086d166f6e34ef8ed17ec Mon Sep 17 00:00:00 2001 From: T'rahk Rokym Date: Mon, 21 Apr 2025 12:35:55 +0200 Subject: [PATCH 04/16] Enable caching --- allianceauth/templatetags/admin_status.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/allianceauth/templatetags/admin_status.py b/allianceauth/templatetags/admin_status.py index ca055345..09b41b50 100644 --- a/allianceauth/templatetags/admin_status.py +++ b/allianceauth/templatetags/admin_status.py @@ -162,14 +162,11 @@ def _current_notifications() -> dict: for hook in hooks: logger.info(hook) try: - 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 when getting %s notifications", hook, exc_info=True) From b3534f4f44549e4f8b074167bab88f0e44a868aa Mon Sep 17 00:00:00 2001 From: T'rahk Rokym Date: Mon, 21 Apr 2025 13:16:21 +0200 Subject: [PATCH 05/16] Basic documentation --- allianceauth/services/hooks.py | 11 ++++ allianceauth/templatetags/admin_status.py | 32 +++++++---- .../custom/app-announcement-hooks.md | 52 ++++++++++++++++++ .../img/app_announcement_hook_example.png | Bin 0 -> 38454 bytes docs/development/custom/index.md | 1 + 5 files changed, 85 insertions(+), 11 deletions(-) create mode 100644 docs/development/custom/app-announcement-hooks.md create mode 100755 docs/development/custom/img/app_announcement_hook_example.png diff --git a/allianceauth/services/hooks.py b/allianceauth/services/hooks.py index a2da7827..75ed30dc 100644 --- a/allianceauth/services/hooks.py +++ b/allianceauth/services/hooks.py @@ -8,6 +8,7 @@ from django.utils.functional import cached_property from allianceauth.hooks import get_hooks from allianceauth.menu.hooks import MenuItemHook +from allianceauth.templatetags.admin_status import AppAnnouncementHook from .models import NameFormatConfig @@ -145,6 +146,16 @@ class MenuItemHook(MenuItemHook): def __init_subclass__(cls) -> None: return super().__init_subclass__() +class AppAnnouncementHook(AppAnnouncementHook): + """ + AppAnnouncementHook shim to allianceauth.templatetags.admin_status + + :param AppAnnouncementHook: _description_ + :type AppAnnouncementHook: _type_ + """ + def __init_subclass__(cls) -> None: + return super().__init_subclass__() + class UrlHook: """A hook for registering the URLs of a Django app. diff --git a/allianceauth/templatetags/admin_status.py b/allianceauth/templatetags/admin_status.py index 09b41b50..c91b19b0 100644 --- a/allianceauth/templatetags/admin_status.py +++ b/allianceauth/templatetags/admin_status.py @@ -1,6 +1,6 @@ import logging from dataclasses import dataclass -from enum import Enum, auto +from enum import Enum from urllib.parse import quote_plus import requests @@ -36,14 +36,24 @@ 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""" + """ + A hook for an application to send GitHub/GitLab issues as announcements on the dashboard + + Args: + - app_name: The name of your application + - repository_namespace: The namespace of the remote repository of your application source code. + It should look like `/`. + - repository_kind: Enumeration to determine if your repository is a GitHub or GitLab repository. + - label: The label applied to issues that should be seen as announcements, case-sensitive. + Default value: `announcement` + """ + class RepositoryKind(Enum): + """Simple enumeration to determine which api should be called to access issues""" + GITLAB = "gitlab" + GITHUB = "github" + app_name: str repository_namespace: str repository_kind: RepositoryKind @@ -56,9 +66,9 @@ class AppAnnouncementHook: be displayed. """ match self.repository_kind: - case RepositoryKind.GITHUB: + case AppAnnouncementHook.RepositoryKind.GITHUB: announcement_list = self._get_github_announcement_list() - case RepositoryKind.GITLAB: + case AppAnnouncementHook.RepositoryKind.GITLAB: announcement_list = self._get_gitlab_announcement_list() case _: return [] @@ -95,11 +105,11 @@ class AppAnnouncementHook: @hooks.register("app_announcement_hook") def test_hook(): - return AppAnnouncementHook("test GitLab app", "r0kym/allianceauth-example-plugin", RepositoryKind.GITLAB) + return AppAnnouncementHook("test GitLab app", "r0kym/allianceauth-example-plugin", AppAnnouncementHook.RepositoryKind.GITLAB) @hooks.register("app_announcement_hook") def test_hook_2(): - return AppAnnouncementHook("test GitHub app", "r0kym/test", RepositoryKind.GITHUB) + return AppAnnouncementHook("test GitHub app", "r0kym/test", AppAnnouncementHook.RepositoryKind.GITHUB) @register.simple_tag() def decimal_widthratio(this_value, max_value, max_width) -> str: diff --git a/docs/development/custom/app-announcement-hooks.md b/docs/development/custom/app-announcement-hooks.md new file mode 100644 index 00000000..e5886e4b --- /dev/null +++ b/docs/development/custom/app-announcement-hooks.md @@ -0,0 +1,52 @@ +# Announcement Hooks + +This hook allows the issues opened on your application repository to be displayed on the alliance auth front page to +administrators. + +![app_announcement_hook_example](img/app_announcement_hook_example.png) + +To register an AppAnnouncementHook class, you would do the following: + +```python +from allianceauth import hooks +from allianceauth.services.hooks import AppAnnouncementHook + +@hooks.register('app_announcement_hook') +def announcement_hook(): + return AppAnnouncementHook("Your app name", "USERNAME/REPOSITORY_NAME", AppAnnouncementHook.RepositoryKind.GITLAB) +``` + +The `AppAnnouncementHook` class will + +```{eval-rst} +.. autoclass:: allianceauth.services.hooks.AppAnnouncementHook + :members: __init__ + :undoc-members: +``` + +## Parameters + +### app_name + +The name of your application. + +### repository_namespace + +Here you should enter the namespace of your repository. +The structure stays the same for both GitHub and GitLab repositories. \ +A repository with the url `https://gitlab.com/username/appname` will have a namespace of `username/appname`. + +### repository_kind + +This variable is an enumeration of the class `AppAnnouncemementHook.RepositoryKind` + +```{eval-rst} +.. autoclass:: allianceauth.services.hooks.AppAnnouncementHook.RepositoryKind + :members: GITLAB, GITHUB + :undoc-members: +``` + +### label + +The label that will determine if issues should be seen as an announcement. +This value is case-sensitive and the default value is `"announcement"`. diff --git a/docs/development/custom/img/app_announcement_hook_example.png b/docs/development/custom/img/app_announcement_hook_example.png new file mode 100755 index 0000000000000000000000000000000000000000..dab4766f821a2ad09fb9a5b96bdbd4b7da50806c GIT binary patch literal 38454 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¨q5n=5 z|6A7^S5N0Xs5XUE3`r^M!&2w0iT|nO{rmS1d6>Hyw~Z27oWUvDsO&*@bz{YMvB^jb z;@+}!%teqBUCs=X_LTePW-5gu?@eM6&+~|V?|9;#L0&3XzP5$3Ctg{d&=`s!6Q`3A z3E)Y#l#{p>7Z6{Zo+cGTF!-+Xx2 z?ERSEdvg%hhIW}zUTI`cIZeEHU8pH$N`kQ?@Ve{{*dH?)qz}{!j!=a?juaO^K21}s z*I8L>?R;}b8vECW@a0KvRRV^>9kAxLRv_&$WpU)+xYD6AumAc@|Ko>#iq!wFsWSjn zK-Wa6YK{nJRb^qb{*t$hL_54Yd^;c1uh0oj1Yz(~T6N#2T*ca{j>6aCU%I2UqrIaW z7_%7i4?PYNKY*HmnS-?Zc15m!NzaeR zP26O95MW9cT%JIf{ZsqGj(-l^UQO;&Mao|J57KXlevY;Rkw;fzID+IS?D1DjB^B#W z7Uiei0$eWT|Ml!H0GTw7@+mAD3}m`>Mdmkeq|DO_+H2Y+3J1UHVdi;TU3x=@m%9Ur zN0~_|TIS@pEPnMB^1#>QwTI?w`31W;NR3%}Ma8LQN^BW;su6CqMi8Y8g;UFN)6dT` z{raqE91@Br`sUN)0&!8s)px)hB1(>XHC?>$;VuNkO3iAQ)Abm=07VGhCR*(H1HAxz zTUEJVpw6ylkG8eImn&p}T_lR#ran6p-#y1(TawFgg7)`(j*2A|+|% z&05;=AiD#kG5VM}w}3lUEt&h&JEgTF(;qtYi~J zC@_>95#QXiZQTist~=@tT32t@mlLn2oogU|4O+;SW$kb5 zeERBZolF&6W2PY&rSEPgC;VNsirliBS1P_lfk%X-!^u_UchzXZ_CzI{AI`nRwuM~U zLoM7-I+3T^AiYr!CSMWzM<042+-0i9MN38lp?A?s2>mqrRr~1}E{58UT!1iM-Wu1al-p#pIA&O!9AwehE zO^w;xtMj}Ft1W_KondqFcjXkqwde;1xH&V+Vn#A6keWkSIN)?eXkOn43!N3;GYzyZ z<#c2o{pr#+G96TUz@>ilCr@6JF7Jc!hgsPF7)7ei21|e{`J)h{xD?7_%@u1 zlQbWW%Ul%YRYE$V8IVe3+wu183&Ye3LVtwiAsh$UFNw&R&*~tzzStk`53xJ+LrZzU zOF1_nEBmok?>sKQwE#Amv?wnxiHe)rn`+7ZZswUkatJVc7x=YA*xz_t(s>9PhZx5M_)@; zIe7H`J$mnlsjyl_71(GX_E5fhOUCZNcc?-vRcpmyl6A~n)tF?GR=Df{Foh|vPdf0r zy$JRGv$*V!Vr>^Y-Oy3`md6SmHy(uHeK}sK1kOBLf-7y3uEf`==r0+%pY=_CZ$y{C zc8|_gj<;v^SwlN{gjr*|mi?9NWdl1Wj!vJGL1G1nQ~eeA3nafJo)l7em<%( zwz8)iq4&%3KX=C-zVTcAgBekgo~GvcIOYJ-IJKibNhqF5yj)EOn36KTh+O>|2X(a2 zfZvE^dXNUUfqxpFf?NTKl(!Ub%4$Me(ip`S0J~RF>Yelk`XCiZt&rZPq8h zIC5#%Fb+fTNvID|r~BTi*$=e#G{7)^oPRKHPW5zfx%v!ZQ21iGjY&4AHowm-xc~Lm z32mL525~MgM@LwncA}^4nxi@V8CUwGdS*r9yTN&qC(jf%d>88QLXC$5D z8B-0NB)d=>QZVmzaEeMNltrklH;gWrdV)Y(ybi@&!-Rm_xovW8KF;fZ-Pb@j-Pkm2 zV-!1&l{jPV=NgQ){k>3QD8tYsbCTh-$hNk~;#>%+(Vv<$7*a@h3t?GRXc3ojSvjH; z6)8ZObJNcV_3)rlF$yQ3#s7UQUD8tm{a7i^3vgVV7HRMJ?3c?w>7@9~6&l5Nna>3d zLf$Jj>ThY&*aPQ~rFPVdhgq@Sc%}C@mPc`V=ML$4LpnZ4Mi&E(X9=D?D);e(A`m+(2CV<{nUOud2YE5 zR?=bx*i*#Uj<L2xNeOQIH>1hRY>14?6v2Xvz zyNXKziIr1n<2Ctqa=sndpTFVq=S3p?EuCIgg}<2<4`(|^Q*9BpI}cYU+CUt7;Hdd7 zh4-A5@A7oKMvm>Zio%rdVG#hpE*}xpZ*f|H?CLii_9Botv$ePc9zJ%WoRgcJdz?dZ z+>M*OJ|FV5L(StmL&u>5;ye=J1y6U+zNI+HlDl($N;vP`ZEHvqp03D|btSz+XKYm3 z0ktWjF@U~CmX&nXtXc5;=(yyUn|0a*MPG;b4{C$)nET5n&-|R)qZ0G3q5zyVbBLfT)w$B znH1<0ZOr+p1 zvVI6?F&%O5DWP3^b8roIg0fVH8te&z0J}LvsaNYJ^7CC??t>wgcn;=w@E@@-s9XfR zzH63vRlcy;$hyT5R+X%)jH}O5!1_3sCu{v%Ob9XrrVEdXvjl~PFI}x?mum&7O}V&$ ztjc-9vy~KkI{1gQe8Z6lA7yzHlNG@;dX-)MD`^1vdx3NFnk66KpG8aTu{3o!Eph_W z)YM|#ln`~GJL%c<>_Z`J(`-P@W&Qxq4IB6f8la4#dGI4*7pv{J}7 zrO<{_^nCB=Ijt@?t1J{gvC%G${82E;Klps~Vrlw?5)S zh~0juNxi=zQQdOmYv()PWqYYfb21313@>i}ei#x4`J;FC-c)XM00VsXUeO;E?4=#M z>*AUQjl-tl4&j6a83(%@zEto#eSyL5`7LkH8P6Hz?k|nV$s5{>9B!b-y@=y0N{cOh z6ms|T5(308mUy$V33ZWA{0t^Pf z(m3S4jxu{tOaRHxtQ%q;#DU=UtD+L}@Ah~Pb>0;9Sy(PfKcHU!a*P==HlNs+a>#Y; z1(u}~N0;rHT-nNtUaG?V5{%WXKWQ;&=L0-96R4H`w6@pu1!?u3wu_ifG+|c@+N)1M zyj9=m>%hf~O5>RjCo00M@kfWxX8lwT_gxc6j_$9nOY+pFrpLVu%Ri_qWf^m`%7Au{&vKPn%b}B_wGd2{a;P!OPB%+5KY&hv zo3-FfwJWaqhiGxYGo(?pGW?vn&zSfftIN4CI$-dN!99e(Tm$TVSX16$T^|8xBauUK zPJx((=j_0lO!kGWENlu|gLsm&Uw@XzL|u!tZwoqx7jpAZ%57F}KG15IozX4Ld4if6 z6&VEP4}wXK6GZ&Qf`jQLKCLUCFMc>4Q-&+7$VY0IuA1 zBNWN7l{DD6FGr4>v>0Vm+Q!GKz|ZRV8g*<<8LeQP&Pyc;>J0a=TGau3`k~_$>#{qZ zNxwrTH&R&exS1EvLy=7~PnAcY<_Kr@ZnHk~+w$cJc4(ml-_RIH0VdCQUO1J#%p`70 zQCLgXfDXH=7Vo(^NKc=N;L=Gug2d7)+9fi^uil9p5O{Zfm1oc_ztQGzQL{+Y@NNSm zC=w!xn)EK+qix!2U-i4S-Q#5KQr}MK{YhhF0Gb<~Hd!&GHK(j&Q>j2_IsLvO6-t-1 z1>8Y|3K|zm#1PP)@|r<~m%e1LKcP@jUuI9X;_47J=g3Qn-kT2%)vZ(zozL+W}=>0{+$r6!^h z`h<4KzQ)I{GG$Z{Q2owBQj{d)AJU}6rdq>3Mef*OcIh>8OXH?)yNCFzDh1h{td2zA z>}8?yeF`v+5+(p85b_0bgr#SUt+Lh$o#Z}eMDBA_%@`2bZc;bfb#NedV;G3=E7ED= zdm4mwdu-Dg#$=4gSTZQ6mY9(*>RT`y3`D;Xd+OsXVrs75Y1jJr$6W}3bTIb- zgvWsy_v>4v-Fy9FSDwL(>uLqa0y;68Z)t1)L7#aS9pI-^CjjX(P0MEhmXd1&!Hfs7 z8~IRPin@TY8+?Z;y6U)xaIxccnm-^-mo%AwB?`TM%4j3{#TJQOiC|~PPAR$Cp+o-m zF98pT{d-@XKz2zMl=z8uZm4BkLeV*=Spdh3d3qhNL+D*T90RQ&>5JumZ`l=DSc1jl ze@hxw!6Z)eMesi3suobVUv>3=&EJ@JT+<9R^#7fv#x)S}?*qvIbLQ91B5!nPfNm8q z*aE;Qz%l*z8Y}te33lVT(Ts8PC27GJq4%`>8 zs-J^L9l*TIG<(GGvF`hjta7KbL^*-7T}r77_VhC@>C{7lF5?sv(inL3=B z;Uv>1PwFs+Z$p=353xH$bc)6&)N>^uW$^9;mH2Nige_{%feO~HvfoVb^RD^7aav@* z^8!U`8Laz|=TsJBf53nYg}?VrdtQFl%q^qjr}UFyKl}Tn$~#iy3fzgv5}aNL(1Jg& zZ1@2;Y#wO(B=2KI5hbkeo10Ng$vu`=+}+h+ez3Qfta03O95Z%i0*U^!>RTsHQQs#S zE3qe?(CCn4pC~^L3EMQG=Wzx$Bf^n`dF&<4zh8#w7f)I_H%3$@KAtpc84|j&wJ3Y# zrQC7;<)!Qbi>1~@;kUJ*_AU8${jWL`MJef9hb<>aiLpL$h+dd~`t)p*d-F#!ovq=4 z8ga3z$ae$16+t!W+-=ahtU)xMDuw#g+Q&F%o;rI+;_tfpVSytv9u^AK`iGtUTKU^f z`z783yHHmef28oKt%!PrOL|U5)oN&g&2jp?aG{^1(e}~)Kz|u!ygBk&OXv^dsk=G86?PM9 z{NhPeuTKdbo=T=}0nNIqhP2~y7J~oTBS{|A4^KUGP@gb~#Q?5{@zmFtR^RivOqkbP zuS&-sxab#t(@@vh+YS07O^;Wf#C*r)dnR{er*K>;PHNc^{D zxKTGNLT&$$8Hg>f9M$>w-H9vt%-5gvqOn$6$=oqqMJdPe#3YXTtEuDRxdf1w3wGC| zRZ-4qFd}pj-8yc~mn(EIXUKeD4AhL_IxHnuX4Lxc%1aLR^S>XQAkOhba*B2KkSs={ zGnp*;^*SVSyRg(Q+bp_Re%uiDAEwoFEux_y2{j9wn`EG%ifhyMEg z10zu98pX?ziyChb1Ft2fUm|)n$#c?kF4N93G%pfF1&=B~5#YLH8#H3eqb|R_YaM@g#`Ts!x8O&elhgivK1!JKF^+yDH z2QDYpxw^Y3@u4@8xDy%TKogwj40oBkxe$;-r^JunZgus{F70thvq@%>If(|M|shK~O2Uo^tZ;9}nQ&?DPS2-3wQ&v$eBMRGjMBvwE%5luURY+#}+ zQ}bWtrYuj`;~OD)G@n?{nXQb&*na&2hbL0FAio#7S7M5H&Ny_Pgc zgbie-Wc8#X)rmBT`Y2U&=bM|TR)NN-Rn>v1*ZMQ9T+*EcKj*G0}EhEk~w%>igV%nzwZFRcJYc^SkCN&}sbgYgbXbI=E8kIvay{Q-fhBa#UNiCg(Q9 z#30gcsN3|o^ji$d1$V(?BN0RT;f zf*(i$&Cn7+jH=~sYn+0%zdw#es=y(#j~ygFe6^(QeWsj?s-w5SznQ&sj7JRd1&Nn^ zE$n!b3sGEgayMiDZmeBc5sxf!>y!MF(*Z$T@}q_Z26S(abwFVnslt|OB`6%_ka4O` zC*Lw%YjkQTQUXTLl(bt}PSzq?MyoQ@U1Jpy?bUgF*b^sF?qkuIH;_kD$q*QRzq|>P z!6dZ3M>*gnsD14AJ21tpXkDP;>kbIDEOyE8svZW~6FpSJ#nqZ4@e2BRi|7O&{rr1T z+?c7Y;mzF!@$EHy7GJNNn3DKVO%70`p zQ{eV}mGBXHE=XwibmlHBr&0d>)tN+H(pO~kyRsUiuS#3?Uuy_fVw=PCw!!T&NY*rd zPT6F;mDn(5H(g?}CUPT07v*x9c|o&EH=H~wT>Wp| zh2X|&>K$VE)j((Ka&jIpm3q`2Ui9-vyyaw!PfFIc&qa9LL<%R*!j1A zJ$7-lG^sa;si6e2Wj9{I9z>dhCZJLkwO95SgJQ34kn}7BbiPqoVze$KGk2F3DtXI` z^~+8eaub63vYSkRor*4)UCuxE4N>g9dL=8+4Z<|*VCLtMx3|@}v98`z37l<4w`%iw z*`e!6Ff3p6p1}*rFQuLC>G>06!n_z0(9e)$)2u>5}vKUZYnZM@0iQImvZR z{?!~5=$~PA8d|Q;?eK_ajO%E1@)Ji4}ki=yLcH@PCV`@Q`+{UQast zIKO4BnTLOulGi+W4)zr6>EN)kwF*-+Q2;qgeyG6rr+E&|65?snO-zy_M9yX6qhg8Y z2PHL*UpVmCeSxOS;MN@Df?pfmc}qQ}lpCp(Y6z($2RrXFEUPn;eP%lU*1n+*=;wfhuH_~rdgz*jsdyfSl4G|%ZiD+W>^bT^1y#ANEM~uVRb5)b znMn}*QKk(a@K}hHc0aHryR2<+y#j4FCfs7~tw67uU>EefCnFX-5fA^3wbo;jd`7dN z29)8sX2Y%L=il@3F0t;KBqx~bH0)Z-Crm3LwA(ol0f&qWta?W0Vxp{vN9lsGs=buA zozh~PZxm5Ltl|?2muC{d_s&MKsI?~a5F&u{G*Po(f^cS8RR+iJ`pf#SKDm&Lm3TWy zZ-9A;5>U!e2A06fTTF|C!o5B#F2ai&S%Jq*rv7tapbDS9uEImE?o^q2Tp+~0J{e75 zf-M%}VvP=AQO;GmSWGI8+b<(yjPcy{0;)4mZ*Rjc0du<&H?ofl>Cn4wbYh5`f+Tc$XmUz$tJD9y~^0_0Y2AQK^ z7&3w>$t?sQbqFwF%yw})_j>tAbW8qJ0{<>Ima7_+H!bI7Bqi;`n%9ir80AO&0|`Ywl5 z;;1SSjE^wOlC$L-Nx)^4<%wfkwgxqapPd_}Be63T-)P)a20w@9whF`F>D24od?KrE zJnGkeao`JGC0YwSwstFC7vqr5D`7q-J&?90Lq9%^5vCpp2vSV>kXxf(Z6LcdD`TLQ zD8-2bSZWbvO+m&Nx$sk<|Eu9&bk(CfD?!U-pQ)e)dJkpEPQ@&D@3E{+ak9GCKighn zGqv#zO)|s-Ga1(KjmefqreXEw12X&{GF#@LUrb(!rtnXZZG1aQ$UFv+Y33sOZP7T+<%SRQQ#EBA@>|AwxNI>Nw*7h@RMf93nor zz$_L)O5t>IC8xj2{z}ofor%&Xbj81KQ1GdExi>W68P`v_{m)6+e`QPD0-EpdoIGF2 ze6Iah`NoI12Yq%o0B_`F`XYfp|B7W#(K5YCy;+`;nDdspn~)I7yrdSTp?7zfAn7u9RJ@x zhN~S6uP)*LM#8y5gsxkONmN&Dy3AGtwTAbYP`lW(>HJAfJHkI=74-RSjQ%J!)D3}9 z@V|MuB#u5o>|F(Bq$ywLgp7wHvvW~E-h(GKBjYbCh4q2_j#*k=%pTv^)%K{OenZ^@LX^q2TeadqWvC!C}QLgsp!!30$o4f|8w2_wLmgCg5tY*CBxx%0uQw8vnKFiNlo(5Qh&!LeYU*mH9u(xMQ*?eS- zri?wAqo8jCtW^OvHW~|a#Gk-Ax1%N}ylJ1L|I86icwsWkv{=YaV>JcX-(9`Firz`u z%kwQVQ!fDZc10j`hH~vFyKxg%j~wwN4MPLV9f`+vb*4pBRun|E4$pb7e0awJzP-!K z@xqiFLXwTyy$`q;Dv0#*uF;8$$*Vf{uiG358UGh(g36e2%z}S-sv$9^YP{qH7SI_E zSQ?#dIq|0Ex*`pJ=za3rbSP;DTEReRCLNl~y&VnM4^NbJqZ}P$k69%D*suw|!ZrYp zFyG4LZf@bS{43p>hsAY|Tm>(Hlpj9JPE$`;I^nTIQvk9^|2!T(WYMM$vwDhVs~uiA zJ4RZ!be$?36)cVl(sT@(7~lf=^w%f-e@RZ*{wp~_5q2zEpz$tv<1WQXpO~HiWzA^P z!unG6`f5|@-NeWZN72)jfd^G8xQTSVPN5x3F2dd_6o3#+>~D0L;~duI>6&<6bM$^_ zjmq{uz<+3C=+f>sQ{I|fkEkTWI4?(^5;@Q#t{kpkngA$EficYy|F0wJL=WD*5E99> zIln#p$O8@HL*Nk#TUeR}5A2&R7W#*ox;8k@p}-lQBw$}P4faVdAeQ3gr0OjMjg>}) zd-ouerucgowOHXu1)qJXv~NNn@&0lLLHU0YwIJwrEaT3OT>wF>p}7iPYl_~@PQ z9v+XK$9+5Y&ykTISx>d+1G7)Rtb91^pM#pb+JKJEM||UF?TgbJw$v^vHu5;@;Y}ka zjY7w1bJYVO&=c?a3w`IvXzzdCMFRzXbyGsJP03v?;jxUTv33CWTA*YR(0S^!F;&9D ze40Mt?K5a+rstYI_V6C2#=4IW{c+g0=!M%R$#iT3)s+r&@x0@~E<#Mf!rgx22uZZ1 z*f;j>l|Z{ZQkbM3sX3UrxnvjygEX=%K4RzVU3HejVZ7cHL7_@_RML?s?V$SQNLVQW z*H=tC#kUaM-xM5wzc=S|m_UIJswiC~L9=_E5&+6vZ-dL!y*}~DMN2F0<%~giD>PMO z+WaYrmElDaFm9=v=Gz+WR!%nIR5#px(so<05PI$&O)66ND@>(=|a-s!CHbuDj!U~h{R9NJ($F__tkA^ z?%nz65uiiD3TXP?>8vmsp;n;zJE0kyTXl%dYHdP6t7-Rx9l?aW2;#kS0aH2jZ=ym6 zge=?L5@yUTfGS;1w_#bQh~rQS)VLb>7R0pxJrJ#nBs<&nE&mfUV{oa8ab68*%uLQz z0;mS^ubR2V>{*xIdT;bws~TyEkX!0R1VMMquXFB!PCU<)Nd9t)W$U`2S{nzVMDp|N z>@3aD_st9x{m#Zuhc@V2@kRrCU%0!s>*+++WVAwuQfEDh52v5hc{j{+@X4Vm==0YI z1|)C{FjCb`Jm+`*e3AB|JmSCARVZWmf^=qx?FA_6Lpn7R85P@Aq{oWUxthkDlasj0 zU3Tk-G#|%wlY06-BTvJ=$fOHk3L1dK9O7FH)-^78o%Y@Om@s~_Q(z&vMTpaw7 zD0^$}eGE-TDU|9pMQtL0u&gX zsNQJ}g^Nyx|U&2Tvt#Q5RstMcg^+dGP z$kT~cKtkWQ-ik5>wofa5p7?r{xdMc~e>y?W2y?l%UV#>7)Go;bn?j$fc043jF_Za> z?|#!UN5hfr(>C49Vc&0tExJlsI2Mc41MhdL^VE;NZM|@>D)NNdJDeU+CsLJoFS(67 zgXfD2A1@ZEWu3&k|CY2D6prjM*Gr{sye;;{$Zh1$QaIAXGn{?#*fAl`By~Tj>1^9#>TWWbFsf5j$XG5ngR&{Y5wnViOP5tthH)MX1H7eQ%a3Bb6~qAv?CsH8}g2LR%gyV zmdEuw98-$#GMD(hK#t+TPl#?_NmNhv~~ktYvID>v$} zm1v-Q9^V7`Q{%EAz^{yIK+EpTaYp2#q*SMw#~)IjB?x_b0`yk7eVHP66!f~@cMgts zLn|7u2#!D31!}Y!aDdLzCxbG}ZckTuOzn-H7FY}ZLR>|SJb?X=NhyEBY%ozd^va%Y zs7m2b&0H5OuyD8>C~YKBVyOB{ID9O3M48TJyG6U7w4Q9z?}DUc&Ns`i22LYC)#NaO z^BtJ_AGzOa8YcU&i0e;#2brNwo#Fkc4=28-qI$>|e|Z}5@8Z`;;_9c3#zPyE5(_tB zRz*gkDL9E&I7RKaRc0u%CrjAwbGm-wT6_$5mIn9OQ5dqOi49RyIXQe-t(&&edwi1% zp$|G6dTT*a>`)|un%W#6pL#}&#+hk+%(_^g{XScbd+uAmAF7`C6;;svJ2@Gdqqo%U zF-(fPl_9V9`vsrnu@r%vGT|avJ35|-$bOSe zsP95K`&s2%Dm$xf?=i|Xrlr2f=JxPDuniiW?k|ku#&s(Ict#NKQb(LrwwpJB6%N8J zQVJv{NctA|Ie2ZeYUxubZ7PMx?{zzP)|`J2H=Zi?K2sHH_MI>D9eNY4n>%*EP~Wyt znHCh1o`BqFph@prm<#BHO0#GonkD*xWQff_nQc&Ct(?vD7{KN35Y<1Z_2kOk!k?n@_Mv*ll=&XRl>^#Ey8izmsqg5bS+Qc5h9Mn-%TeWDYu) zT6Qln$(9FH{q|gfGgGoY^TlN0YT53Sh)^SQAXn^@m|*m#wj{(&Ycf+|vVa1uQ)!`v z7(D&}RPh(T|PhO)6&j-nbxREt;*XtfvXEAR%TadKf(AR!DELa@Ug}b?i8xiEiUZl2WAQ zFYC#k%`l;CRfLlR6N3L1ga7$!j%JW*4PQ6+6lo};qU;G!mNB8g4DuLYe|e;Aar3~_ zoTrm~POQiALY}u)s?MonzJ3iUX`Yx32t(4;;)jq(0gI{D#HFNO`MNG(!tvQ9L95eR z$0`;R$fzsdzA7(?3q=gJ?t3P92n!Z&?=ei6)vpW^dh*8vW0#l|ur=?Bme(7tr}Ul- z0xOwXo)>@Ym^R$?e!(Gk0ZL2Hnd-hg4x`uqBP2!df)YSdxc*Nh#W#mF2hcfn(L$*5*S&l&QW7;aG+JA3RfiAC%ciqBH2Dy5u8G3vmq@wbM_XS2 z4~NTj!qEEs$bIaXrm>ZhgGqdn3!dY!coIuhW4TIQ-UhXj~2at2JPRGQi{!-N2?Yi8l#t%b0H_A}llHbgEOCh*;kq20kk;#vsan zMk*{~0Gz_+3aOwDJiqpE9{k;(Ju7`-^2zKb#V@4dw8u29m?7xSqigSV)EqRIZ$VZS zd(Vj*i*YcTOhDDR=K%P3Y59kLexZq@+&i z%RZH7D*r$@{wtZG|9|yv00iY@$t~e4;dlnLQrREn&<@gB>2LrR+$+5ePJpSFHf(q) zFwD5B_qUVwm!ZOkKqA~&U&xnJt1@oIyAi%Y2{)3^WFPaZ8vcFZ)_+~Nb;FuHI{)BJHoA)(u})?EN+F(#{2pyLh4lB15cqi!T)? zRb{nlg?Mk|O+`lUJsq}WEds2Qso-ag8@JZ+%VIVJDA(;~)-kpJaA5Sy;FC1M;%PP6+k!J0 zy3bthn?c*0(kLhjWSxxch)2in&L<5rUzset5PgEiDJ0^&nHwvf6p@{C`nK5UUDI7a z*ti(b(3!85l>iTtD$TD4$%?Et_72bt7H;RE)75$-T!d%RHFDaACwG>%M4a}|<_Tvk z>28Dm3ojQ+FT>9*CxC@e2eu-x7yE#lx*j&N+VOsCN#mbP14NkTMA1*Xz$*ixI-F~_Q)s8^j!!T2p`#iMZ<5kpsmPT_YySP^dapniO z4W>&f9Z+!909<}v(HjdamXZLl3K{#vLguy`fN8lS{j3Vrhj4Yz-x8Vf#=N6=GKJ7t z1%YNpbfesY2@337*Dr&9!3xj+Ev&#I;<+Wy8OX`f$RaXHm`8hSYW+0lslB>WJF#d# zFd#;{)_6J^8{;Wl3pq%RmvHD3zG{^tyC?-%jf~N)Rmj`G+?=$mS2&8sZQO7?eJ;R; zg4}HVm2`2BtyXe3f{tDLS>h&P4zaCo=E#HvWKqL_VkX)HlLV{wlnPzr1oM@5>hJeQ4; z!e#;x@1rl;({=vMj$mM|)^?>K0t-86WfM0Tw z;+Q^cSx80=7;8dZwNIY^JyS6xc25JowVPbp zKZLCn!k~P(lpbi1sE&$Mll}9o4$0YgBC}(bIN>44PB+7wcaNWOSjGsgTaGIIIujf)eF5^9Fz4ZM+<@%0ry0rp-h#B&t>TB4Cs=JA1WV2IRfsKg7 zg=L=fh{bLVSL=rVg8a{A`R|qgz3Ji!H~_7)L;Ua&V0pnF%Y{Ysn9{Db18vKS$egW zJmO0;o+daQAowvIyOFb-d1=&)Z>1Em=69GIhh;(x-8~h*Vh#YJp2}yjjD{{k_ssz+ z$q`mBZ>Cd}0!;toyMm?xUZDQ1fyc)O6L}@C&bH1Coo?!|mVFwp!(kmb@82l1+dbkr zoiJSOWdX)ecdp;yfYc69o`=fc2S8dN?1j4i1hruj1b~XCwo`RcfDdVD`1y)C61o7a zfHl`gilrZpx!D=B*x_ppyus|1$L^-(33rqd;bla732m%m$2}=dE|Z43h_CmmbW?yY zO8HVZ>=^f`^uefhud#BMK;x$&?cON+@BI}3##QmoV0Y5DWA~F5-6srp?&qR+7v>B- zTtOGS1JFPj03_gy=U|0Ar>*Okw`@2Pe>PV29K5dG zav*OXafJy`w_IZaT{$^yB%Ec=u`Sx*kB?CcxcwuACM#IMwM81)U`jJ!P{S`?r8kGo zK$6`X^H4eTG$0Xvy!betB;RVjQ+Yv_5Gi>L32cAgH4Ogu^8tzRa!jmy!1q8(X7Ahh zxTw^xqbVbFt>Z*QYUpa^5*S+gh8zIB0)DQz#FzJ}5Svh`*JNrmS}C&djAyUR#IU&G zcOa2Tz64VHa+{QTxQ0cha7z%}OfIOMU6!7Egq_9>>O_Dx=v7GAI%(3kG5& zW$!LKQvv;c33hOpzF!iwL6U`{ecaP^HHC7Us%e{X`eKn^YiHu zU}A~)cHKFq5G~oy_JT|&+5F1}Riq}_k_Xa0Kl3qixdXT$+Xlewj6C(<=mw(`hX~c0 z4G5_~jAGTQSAJLxmnC5hWD!9C6PpKR3Do(0sWtwA4Lizn%6dA+c!Fsi&29W#2K0IQo1e`-aOP&GD$$An_ zqQgl=4#a6NHE+|;DS%%pO@1fxHGr$9 zCDgN4a+F~;2*YW$$H?}@)Npd`UPa&}1oEZ69b?!+b>WVtU1?YkDei}qjI_di1ZYTw8Hg#a|`BJ?MNn+=su z0iBRp(XU!B-pjaZ@~>jy-?#6nv~L3Kd(|gBo`}8vs|0~x%mC1x{+SFAvv^JQ|0fU~ zNF_kLc|Q*vGBE4wa=ldabW{Kvl4}?BKlp3=@S7dzj)AMRd?1&=cH^6@FKKIh($!DE z9xxcQ`MCk{{#Sd5GicLEnLVbDhgbC4)%G9h;p6AJPD5dNtzwKW~e6}Ms5q@(3lps zI&`g0+9}92 Date: Mon, 21 Apr 2025 15:26:00 +0200 Subject: [PATCH 06/16] Remove template hooks --- allianceauth/templatetags/admin_status.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/allianceauth/templatetags/admin_status.py b/allianceauth/templatetags/admin_status.py index c91b19b0..3f8550c5 100644 --- a/allianceauth/templatetags/admin_status.py +++ b/allianceauth/templatetags/admin_status.py @@ -10,7 +10,7 @@ from django import template from django.conf import settings from django.core.cache import cache -from allianceauth import __version__, hooks +from allianceauth import __version__ from allianceauth.authentication.task_statistics.counters import ( dashboard_results, ) @@ -103,14 +103,6 @@ class AppAnnouncementHook: 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 GitLab app", "r0kym/allianceauth-example-plugin", AppAnnouncementHook.RepositoryKind.GITLAB) - -@hooks.register("app_announcement_hook") -def test_hook_2(): - return AppAnnouncementHook("test GitHub app", "r0kym/test", AppAnnouncementHook.RepositoryKind.GITHUB) - @register.simple_tag() def decimal_widthratio(this_value, max_value, max_width) -> str: if max_value == 0: From 9cc3283399cadaa6b48134b24ea628f0da36eb9b Mon Sep 17 00:00:00 2001 From: T'rahk Rokym Date: Mon, 21 Apr 2025 15:44:15 +0200 Subject: [PATCH 07/16] Move logger statements to debug --- allianceauth/services/hooks.py | 2 +- allianceauth/templatetags/admin_status.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/allianceauth/services/hooks.py b/allianceauth/services/hooks.py index 75ed30dc..b0c3b718 100644 --- a/allianceauth/services/hooks.py +++ b/allianceauth/services/hooks.py @@ -16,7 +16,7 @@ from .models import NameFormatConfig def get_extension_logger(name): """ Takes the name of a plugin/extension and generates a child logger of the extensions logger - to be used by the extension to log events to the extensions logger. + to be used by the extension to log events to the extensions The logging level is determined by the level defined for the parent logger. diff --git a/allianceauth/templatetags/admin_status.py b/allianceauth/templatetags/admin_status.py index 3f8550c5..5747960a 100644 --- a/allianceauth/templatetags/admin_status.py +++ b/allianceauth/templatetags/admin_status.py @@ -162,7 +162,7 @@ def _current_notifications() -> dict: app_notifications = [] hooks = [fn() for fn in get_hooks("app_announcement_hook")] for hook in hooks: - logger.info(hook) + logger.debug(hook) try: app_notifications.extend(cache.get_or_set( f"{hook.app_name}_notification_issues", @@ -173,6 +173,7 @@ def _current_notifications() -> dict: logger.warning("Error when getting %s notifications", hook, exc_info=True) if app_notifications: + logger.debug(app_notifications) application_notifications = app_notifications[:10] else: application_notifications = [] @@ -315,6 +316,7 @@ def _fetch_list_from_github(url: str, max_pages: int = MAX_PAGES) -> list: return result result += request.json() + logger.debug(request.json()) # https://docs.github.com/en/rest/using-the-rest-api/using-pagination-in-the-rest-api?apiVersion=2022-11-28 # See Example creating a pagination metho From 92d8c699ebece36c5b2bf7a4079f5ba65f4393cd Mon Sep 17 00:00:00 2001 From: T'rahk Rokym Date: Mon, 21 Apr 2025 16:10:58 +0200 Subject: [PATCH 08/16] Ignore closed issues --- .../allianceauth/admin-status/overview.html | 8 +------- allianceauth/templatetags/admin_status.py | 6 +++--- .../img/app_announcement_hook_example.png | Bin 38454 -> 29395 bytes 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/allianceauth/templates/allianceauth/admin-status/overview.html b/allianceauth/templates/allianceauth/admin-status/overview.html index 2c8e685f..867322b6 100644 --- a/allianceauth/templates/allianceauth/admin-status/overview.html +++ b/allianceauth/templates/allianceauth/admin-status/overview.html @@ -57,19 +57,13 @@ - {# TODO maybe add some disclaimer that those are managed by application devs? #} -
diff --git a/allianceauth/templatetags/admin_status.py b/allianceauth/templatetags/admin_status.py index 5747960a..320f2ed4 100644 --- a/allianceauth/templatetags/admin_status.py +++ b/allianceauth/templatetags/admin_status.py @@ -20,7 +20,7 @@ register = template.Library() # cache timers TAG_CACHE_TIME = 3600 # 1 hours -NOTIFICATION_CACHE_TIME = 300 # 5 minutes +NOTIFICATION_CACHE_TIME = 10 # 5 minutes # timeout for all requests REQUESTS_TIMEOUT = 5 # 5 seconds # max pages to be fetched from gitlab @@ -86,7 +86,7 @@ class AppAnnouncementHook: """ raw_list = _fetch_list_from_github( f"https://api.github.com/repos/{self.repository_namespace}/issues" - f"?labels={self.label}&state=all" + f"?labels={self.label}" ) # Translates GitHub attributes to GitLab and filters out pull requests clean_list = [] @@ -101,7 +101,7 @@ class AppAnnouncementHook: """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}") + f"?labels={self.label}&state=opened") @register.simple_tag() def decimal_widthratio(this_value, max_value, max_width) -> str: diff --git a/docs/development/custom/img/app_announcement_hook_example.png b/docs/development/custom/img/app_announcement_hook_example.png index dab4766f821a2ad09fb9a5b96bdbd4b7da50806c..b57c6950830ffd69a39b1320601776fd978dc846 100755 GIT binary patch literal 29395 zcmeFZXHXPTw>BywAX#z-1(iJHoKz50GDse>1c@``jN}X=%8(I|EIAE13@}Ir$$1!Z z&J6jE`kqs#&YxR#f81MN)!kKH-Mzbet=+qKSnGM#?g%v%1wuSpynFZV5x#vRr*ZGz zeaO9g7{G_v=$!Q8Z{Fw~hO>skt9zw`^qc6D2UahYU*5Y{5r=4V+GY@aloVg0V4$F~u8kL*u2Qr_zl|R;9 zn)2=njLd%Z>earo7v9@9pYCCdzF}!Tul+Sq?>~7ptu&QBKVaAK9NVe2bi4lXZ@1p( z&ik5{cQTJi&?%(n&MNJN96F5|5J3Ly$@%~HJ`Yiumxt}?>G?)c(cJVuiRQy=r|&E6 zCRhXz?P4wapFe&)u(!AGp8g|3@ZlSxuaa!_YDZbgpAZ_tY6b?jX-a>t1btq4^()Ns zI#jXn@@w$BKUqH#e5@zr_C$d1I4m?p|CGt|i;uOo_a-`LU77h$VyQ~&0locm^Gkug zi2oE%@c)F&p!E5`%Obq5lhti`G*+Y}p8v`pFr~V*J*!~eGt5iSeD3~1A_m_Bw1%9# zf}8Y|7ayoJ&HFS6;Ujn!0xNW+qvOnN`Jj92v^FAxok>}yTTM+Z#r1b_i+bU&iR0Qo zXX%)!{2UM!@u=19uje9=ElZEk!eX2h`82^@W`Y{#jrJ74wmK+kAJGSG$KJALLPJDPV9LymGtlLozpcO-`>=pZjy=*Uo+f-4ifzrs!< zlzB(k$8T)N6(0=!ihS63$4YN609fK4LsZV1SHqF%5$RcnN1owlUM~r`6)2Zlz?J(* z@ccGLei15OF<{QhpQj7HYkrJ14j*4CHrt`n);y zUKCi&@Y5q2P{Zld#9Mc~Qm7lPbQB#>2r`(q2-4A|2DDtN%t`t^4UJBT?dk#`&8&JN z0$cOUHV*EY<`Q?Te$L7P-!$otxjFO6(gBoX)oow)@(eA_L?rOtOy(@q_{Jun4l^pK z`Rz#acm|2CTlPe(KwNTT`~6PLuxIg1`r}@bz8|SWqtjw#&TgY6U>%?limF5R?;PDP zp70N!yzC2w885^}g~fJu-r2)L^>miA(JJAA2_R}JDHgYqIun+LSf@BZ|Gmp@wb-9OF5RCo}{(pPN|o_PUEsVPIF9B(3JIPc`3HiPdP+CGOWR+;aFa(e-~d#|pNUFRpN&r$_MDVewY zEydyyUwV4tL-zN-1V}=Vn?gdvORs*7((ILA9GH=#=%&cnY%J@A)A#gIT}P1;#N5DNHOy>Et8w-s+g3dJd`m zj|Ka$-2_JIZsd3G2@2Y!hA2wFR!qfq&Ym`T@3IllxG^k5p8hr+Ne;p~@AYgTx|?;B zRJ}Y@gnHv{|IpU6?HD}YZ`aa^hIM(_o9WO*^erm8Fs151?G$!L#!;v)9Pj`I#1xfO z4Ek+;T+UX=VM6$UN(va85)nfYb~rAz*YZQw@!}zvu%)nM7PM*eYHb25>84W(x^>jY zYd5wEU0zc-m{U+BF&+hb)tkhTxc%^vrlBHch)Hc#)P#?Exjbt@?I`-3bdXa7^VDB> zHl0sHP@VZcA!WjrS7Lq=97I3c^2?+rE{vcA5F>A%jHD}^GjiRg26s`q%5u+k$$qF6 zf48wXB@Av9l*;5!TLC5xM*VeSA{7x|1x90>N%kp2Fy$~e>SSZbx(>~NRWUk zrk>GV$sd=e2}Q66-O_NaYIeEQSn=R04J(c+O9_B5woWg2Q*PwDe9 zpCvz|=Z&n-WtUab@dloqu?<&`WG3oo9(MzR6pu9<#*B=^~DF8q%#=SWy*a zcCyWUai@a7j@aw@sKeiJOiLs*_xN|-Ns2mRpbLYK-Z*c=0il<6!xR8G@|G$Su4QTt zzW7Sxan>quIQ6xd_kG7eLF3_c`^4TA%qz5ylCZYvjig_F+GKpv!6^1uYU%I?1dy{= zKmBjCg&cb}w!xE{Wr%M&#vLqm${JTrBd9{u^ZG1j<3LGynSh_%7YdSI+l162USB2J z2$`bFE0BDnIl(b5Lv$D2jI#G?mrzWq%TAs=1axu5OgkKLp!_Y9=}5RtpE4t}rl}ScSkthRbfkXAf8U!+70x20T8TQ6=3S-}kw^l!XFgyF7Mx zJ#~+QPudwHD(a+z^q71i-4}gG+?Bq}O#DLe*590*MPOt6omN(IDZCe3wCCjjnN?KW zmmf+nQg0>NZweCQh%YDJKfsHUN-rH}cDuFfzjVMKrBhjaRWpG^?X@V5A7#YtZQ|+3 z!k1E#`{E~dNJ8i>bT8)QfPyXrdzV6W$Il6^PrdmJ#wk@g#xjX>29Yh>HoYAaJtc1( zogMRqHZ|uS`yQA2VR0Ty#m&S9)|8nnm!m(*gpr_YYTI1eh5xeO^kjM1f$hR3mFthY zaD&xQ=`|WIPLT)WjA4B$u`WIm7A{Mc6Qv-U(5UBwZ6bWnHi{Nq$ITaU_EMV5^R=_G z8+Udn02YDx-)0v($G6xqA;|WSWTSnGJH%*ge+V&)oi(-m+y$T8H!nHy5}J<)9e zUZCJdqFPVCH@p(v4)>GoC+(uCX+urp6teoaQNRM-GxE?2Tu9>AGLUnU;FZj$ICD

OHA^F`sh;2 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 WE8;bB01l!&qo%BRzf{rk`F{Xp_%urZ literal 38454 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¨q5n=5 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 Date: Mon, 21 Apr 2025 16:12:00 +0200 Subject: [PATCH 09/16] Doc fix --- docs/development/custom/app-announcement-hooks.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/development/custom/app-announcement-hooks.md b/docs/development/custom/app-announcement-hooks.md index e5886e4b..271f1140 100644 --- a/docs/development/custom/app-announcement-hooks.md +++ b/docs/development/custom/app-announcement-hooks.md @@ -16,8 +16,6 @@ def announcement_hook(): return AppAnnouncementHook("Your app name", "USERNAME/REPOSITORY_NAME", AppAnnouncementHook.RepositoryKind.GITLAB) ``` -The `AppAnnouncementHook` class will - ```{eval-rst} .. autoclass:: allianceauth.services.hooks.AppAnnouncementHook :members: __init__ From c9b07c12a0c6446fb3d8f4030602d25384ccf8ea Mon Sep 17 00:00:00 2001 From: T'rahk Rokym Date: Mon, 21 Apr 2025 16:31:39 +0200 Subject: [PATCH 10/16] Last fixes --- allianceauth/services/hooks.py | 2 +- allianceauth/templatetags/admin_status.py | 6 +++--- docs/development/custom/app-announcement-hooks.md | 2 ++ 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/allianceauth/services/hooks.py b/allianceauth/services/hooks.py index b0c3b718..75ed30dc 100644 --- a/allianceauth/services/hooks.py +++ b/allianceauth/services/hooks.py @@ -16,7 +16,7 @@ from .models import NameFormatConfig def get_extension_logger(name): """ Takes the name of a plugin/extension and generates a child logger of the extensions logger - to be used by the extension to log events to the extensions + to be used by the extension to log events to the extensions logger. The logging level is determined by the level defined for the parent logger. diff --git a/allianceauth/templatetags/admin_status.py b/allianceauth/templatetags/admin_status.py index 320f2ed4..6768a9a7 100644 --- a/allianceauth/templatetags/admin_status.py +++ b/allianceauth/templatetags/admin_status.py @@ -20,7 +20,7 @@ register = template.Library() # cache timers TAG_CACHE_TIME = 3600 # 1 hours -NOTIFICATION_CACHE_TIME = 10 # 5 minutes +NOTIFICATION_CACHE_TIME = 300 # 5 minutes # timeout for all requests REQUESTS_TIMEOUT = 5 # 5 seconds # max pages to be fetched from gitlab @@ -213,7 +213,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, @@ -319,7 +319,7 @@ def _fetch_list_from_github(url: str, max_pages: int = MAX_PAGES) -> list: logger.debug(request.json()) # https://docs.github.com/en/rest/using-the-rest-api/using-pagination-in-the-rest-api?apiVersion=2022-11-28 - # See Example creating a pagination metho + # See Example creating a pagination method if not ('link' in request.headers and 'rel=\"next\"' in request.headers['link']): break diff --git a/docs/development/custom/app-announcement-hooks.md b/docs/development/custom/app-announcement-hooks.md index 271f1140..03650a87 100644 --- a/docs/development/custom/app-announcement-hooks.md +++ b/docs/development/custom/app-announcement-hooks.md @@ -38,6 +38,8 @@ A repository with the url `https://gitlab.com/username/appname` will have a name This variable is an enumeration of the class `AppAnnouncemementHook.RepositoryKind` +It is mandatory to specify this variable so alliance auth contacts the correct API when fetching your repository issues. + ```{eval-rst} .. autoclass:: allianceauth.services.hooks.AppAnnouncementHook.RepositoryKind :members: GITLAB, GITHUB From 97f603c13802ea68972d6389804ab890b5f3601b Mon Sep 17 00:00:00 2001 From: T'rahk Rokym Date: Sun, 27 Apr 2025 12:33:02 +0200 Subject: [PATCH 11/16] Move all the code in an application and stores Announcements in the database so they can be marked as closed --- allianceauth/admin_status/__init__.py | 0 allianceauth/admin_status/admin.py | 19 + allianceauth/admin_status/apps.py | 6 + allianceauth/admin_status/hooks.py | 207 +++++++++++ allianceauth/admin_status/managers.py | 57 +++ .../admin_status/migrations/0001_initial.py | 33 ++ .../admin_status/migrations/__init__.py | 0 allianceauth/admin_status/models.py | 45 +++ .../admin-status/celery_bar_partial.html | 0 .../templates}/admin-status/esi_check.html | 0 .../templates}/admin-status/include.html | 0 .../templates}/admin-status/overview.html | 80 ++--- .../admin_status/templatetags/__init__.py | 0 .../admin_status/templatetags/admin_status.py | 181 ++++++++++ allianceauth/admin_status/tests/__init__.py | 0 .../admin_status/tests/test_managers.py | 75 ++++ .../tests/test_templatetags.py | 70 +++- allianceauth/authentication/views.py | 4 +- allianceauth/services/hooks.py | 2 +- allianceauth/templatetags/admin_status.py | 326 ------------------ .../custom/app-announcement-hooks.md | 5 +- 21 files changed, 706 insertions(+), 404 deletions(-) create mode 100644 allianceauth/admin_status/__init__.py create mode 100644 allianceauth/admin_status/admin.py create mode 100644 allianceauth/admin_status/apps.py create mode 100644 allianceauth/admin_status/hooks.py create mode 100644 allianceauth/admin_status/managers.py create mode 100644 allianceauth/admin_status/migrations/0001_initial.py create mode 100644 allianceauth/admin_status/migrations/__init__.py create mode 100644 allianceauth/admin_status/models.py rename allianceauth/{templates/allianceauth => admin_status/templates}/admin-status/celery_bar_partial.html (100%) rename allianceauth/{templates/allianceauth => admin_status/templates}/admin-status/esi_check.html (100%) rename allianceauth/{templates/allianceauth => admin_status/templates}/admin-status/include.html (100%) rename allianceauth/{templates/allianceauth => admin_status/templates}/admin-status/overview.html (69%) create mode 100644 allianceauth/admin_status/templatetags/__init__.py create mode 100644 allianceauth/admin_status/templatetags/admin_status.py create mode 100644 allianceauth/admin_status/tests/__init__.py create mode 100644 allianceauth/admin_status/tests/test_managers.py rename allianceauth/{authentication => admin_status}/tests/test_templatetags.py (80%) delete mode 100644 allianceauth/templatetags/admin_status.py diff --git a/allianceauth/admin_status/__init__.py b/allianceauth/admin_status/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/allianceauth/admin_status/admin.py b/allianceauth/admin_status/admin.py new file mode 100644 index 00000000..a6a10970 --- /dev/null +++ b/allianceauth/admin_status/admin.py @@ -0,0 +1,19 @@ +"""Admin site for admin status applicaton""" +from django.contrib import admin + +from allianceauth.admin_status.models import ApplicationAnnouncement + + +@admin.register(ApplicationAnnouncement) +class ApplicationAnnouncementAdmin(admin.ModelAdmin): + list_display = ["application_name", "announcement_number", "announcement_text", "hide_announcement"] + list_filter = ["hide_announcement"] + ordering = ["application_name", "announcement_number"] + readonly_fields = ["application_name", "announcement_number", "announcement_text", "announcement_url"] + fields = ["application_name", "announcement_number", "announcement_text", "announcement_url", "hide_announcement"] + + def has_add_permission(self, request): + return False + + def has_delete_permission(self, request, obj=None): + return False diff --git a/allianceauth/admin_status/apps.py b/allianceauth/admin_status/apps.py new file mode 100644 index 00000000..35f5e6dd --- /dev/null +++ b/allianceauth/admin_status/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class AdminStatusApplication(AppConfig): + name = 'allianceauth.admin_status' + label = 'admin_status' diff --git a/allianceauth/admin_status/hooks.py b/allianceauth/admin_status/hooks.py new file mode 100644 index 00000000..fc5ca081 --- /dev/null +++ b/allianceauth/admin_status/hooks.py @@ -0,0 +1,207 @@ +import hashlib +import logging +from dataclasses import dataclass +from enum import Enum +from urllib.parse import quote_plus + +import requests + +from django.core.cache import cache + +from allianceauth.hooks import get_hooks, register + +logger = logging.getLogger(__name__) + +# timeout for all requests +REQUESTS_TIMEOUT = 5 # 5 seconds +# max pages to be fetched from gitlab +MAX_PAGES = 50 +# Cache time +NOTIFICATION_CACHE_TIME = 300 # 5 minutes + + +@dataclass +class Announcement: + """ + Dataclass storing all data for an announcement to be sent arround + """ + application_name: str + announcement_url: str + announcement_number: int + announcement_text: str + + @classmethod + def build_from_gitlab_issue_dict(cls, application_name: str, gitlab_issue: dict) -> "Announcement": + """Builds the announcement from the JSON dict of a GitLab issue""" + return Announcement(application_name, gitlab_issue["web_url"], gitlab_issue["iid"], gitlab_issue["title"]) + + @classmethod + def build_from_github_issue_dict(cls, application_name: str, github_issue: dict) -> "Announcement": + """Builds the announcement from the JSON dict of a GitHub issue""" + return Announcement(application_name, github_issue["html_url"], github_issue["number"], github_issue["title"]) + + def get_hash(self): + """Get a hash of the Announcement for comparison""" + name = f"{self.application_name}.{self.announcement_number}" + hash_value = hashlib.sha256(name.encode("utf-8")).hexdigest() + return hash_value + + +@dataclass +class AppAnnouncementHook: + """ + A hook for an application to send GitHub/GitLab issues as announcements on the dashboard + + Args: + - app_name: The name of your application + - repository_namespace: The namespace of the remote repository of your application source code. + It should look like `/`. + - repository_kind: Enumeration to determine if your repository is a GitHub or GitLab repository. + - label: The label applied to issues that should be seen as announcements, case-sensitive. + Default value: `announcement` + """ + class Service(Enum): + """Simple enumeration to determine which api should be called to access issues""" + GITLAB = "gitlab" + GITHUB = "github" + + app_name: str + repository_namespace: str + repository_kind: Service + label: str = "announcement" + + + def get_announcement_list(self) -> list[Announcement]: + """ + Checks the application repository to find issues with the `Announcement` tag and return their title and link to + be displayed. + """ + logger.debug("Getting announcement list for the app %s", self.app_name) + match self.repository_kind: + case AppAnnouncementHook.Service.GITHUB: + announcement_list = self._get_github_announcement_list() + case AppAnnouncementHook.Service.GITLAB: + announcement_list = self._get_gitlab_announcement_list() + case _: + announcement_list = [] + + logger.debug("Announcements for app %s: %s", self.app_name, announcement_list) + return announcement_list + + def _get_github_announcement_list(self) -> list[Announcement]: + """ + 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}" + ) + return [Announcement.build_from_github_issue_dict(self.app_name, github_issue) for github_issue in raw_list] + + def _get_gitlab_announcement_list(self) -> list[Announcement]: + """Return the issues list for a GitLab repository""" + raw_list = _fetch_list_from_gitlab( + f"https://gitlab.com/api/v4/projects/{quote_plus(self.repository_namespace)}/issues" + f"?labels={self.label}&state=opened") + return [Announcement.build_from_gitlab_issue_dict(self.app_name, gitlab_issue) for gitlab_issue in raw_list] + +@register("app_announcement_hook") +def alliance_auth_announcements_hook(): + return AppAnnouncementHook("AllianceAuth", "allianceauth/allianceauth", AppAnnouncementHook.Service.GITLAB) + +def get_all_applications_announcements() -> list[Announcement]: + """ + Retrieve all known application announcements and returns them + """ + application_notifications = [] + + hooks = [fn() for fn in get_hooks("app_announcement_hook")] + for hook in hooks: + logger.debug(hook) + try: + application_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 when getting %s notifications", hook, exc_info=True) + + logger.debug(application_notifications) + if application_notifications: + application_notifications = application_notifications[:10] + + return application_notifications + + +def _fetch_list_from_gitlab(url: str, max_pages: int = MAX_PAGES) -> list: + """returns a list from the GitLab API. Supports paging""" + result = [] + + for page in range(1, max_pages + 1): + try: + request = requests.get( + url, params={'page': page}, timeout=REQUESTS_TIMEOUT + ) + request.raise_for_status() + except requests.exceptions.RequestException as e: + error_str = str(e) + + logger.warning( + f'Unable to fetch from GitLab API. Error: {error_str}', + exc_info=True, + ) + + return result + + result += request.json() + + if 'x-total-pages' in request.headers: + try: + total_pages = int(request.headers['x-total-pages']) + except ValueError: + total_pages = None + else: + total_pages = None + + if not total_pages or page >= total_pages: + 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""" + + 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" + }, + timeout=REQUESTS_TIMEOUT, + ) + 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() + logger.debug(request.json()) + + # https://docs.github.com/en/rest/using-the-rest-api/using-pagination-in-the-rest-api?apiVersion=2022-11-28 + # See Example creating a pagination method + if not ('link' in request.headers and 'rel=\"next\"' in request.headers['link']): + break + + return result diff --git a/allianceauth/admin_status/managers.py b/allianceauth/admin_status/managers.py new file mode 100644 index 00000000..f445fad2 --- /dev/null +++ b/allianceauth/admin_status/managers.py @@ -0,0 +1,57 @@ +from typing import TYPE_CHECKING + +from django.db import models + +from allianceauth.admin_status.hooks import ( + Announcement, + get_all_applications_announcements, +) +from allianceauth.services.hooks import get_extension_logger + +if TYPE_CHECKING: + from .models import ApplicationAnnouncement + +logger = get_extension_logger(__name__) + +class ApplicationAnnouncementManager(models.Manager): + + def sync_and_return(self): + """ + Checks all hooks if new notifications need to be created. + Return all notification objects after + """ + logger.info("Syncing announcements") + current_announcements = get_all_applications_announcements() + self._delete_obsolete_announcements(current_announcements) + self._store_new_announcements(current_announcements) + + return self.all() + + def _delete_obsolete_announcements(self, current_announcements: list[Announcement]): + """Deletes all announcements stored in the database that aren't retrieved anymore""" + hashes = [announcement.get_hash() for announcement in current_announcements] + self.exclude(announcement_hash__in=hashes).delete() + + def _store_new_announcements(self, current_announcements: list[Announcement]): + """Stores a new database object for new application announcements""" + + for current_announcement in current_announcements: + try: + announcement = self.get(announcement_hash=current_announcement.get_hash()) + except self.model.DoesNotExist: + self.create_from_announcement(current_announcement) + else: + # if exists update the text only + if announcement.announcement_text != current_announcement.announcement_text: + announcement.announcement_text = current_announcement.announcement_text + announcement.save() + + def create_from_announcement(self, announcement: Announcement) -> "ApplicationAnnouncement": + """Creates from the Announcement dataclass""" + return self.create( + application_name=announcement.application_name, + announcement_number=announcement.announcement_number, + announcement_text=announcement.announcement_text, + announcement_url=announcement.announcement_url, + announcement_hash=announcement.get_hash(), + ) diff --git a/allianceauth/admin_status/migrations/0001_initial.py b/allianceauth/admin_status/migrations/0001_initial.py new file mode 100644 index 00000000..f7bcdc26 --- /dev/null +++ b/allianceauth/admin_status/migrations/0001_initial.py @@ -0,0 +1,33 @@ +# Generated by Django 5.1.9 on 2025-05-18 15:43 + +import django.db.models.manager +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='ApplicationAnnouncement', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('application_name', models.CharField(help_text='Name of the application that issued the announcement', max_length=50)), + ('announcement_number', models.IntegerField(help_text='Issue number on the notification source')), + ('announcement_text', models.TextField(help_text='Issue title text displayed on the dashboard', max_length=300)), + ('announcement_url', models.TextField(max_length=200)), + ('announcement_hash', models.CharField(default=None, editable=False, help_text='hash of an announcement. Must be nullable for unique comparison.', max_length=64, null=True, unique=True)), + ('hide_announcement', models.BooleanField(default=False, help_text='Set to true if the announcement should not be displayed on the dashboard')), + ], + options={ + 'constraints': [models.UniqueConstraint(fields=('application_name', 'announcement_number'), name='functional_pk_applicationissuenumber')], + }, + managers=[ + ('object', django.db.models.manager.Manager()), + ], + ), + ] diff --git a/allianceauth/admin_status/migrations/__init__.py b/allianceauth/admin_status/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/allianceauth/admin_status/models.py b/allianceauth/admin_status/models.py new file mode 100644 index 00000000..7b048378 --- /dev/null +++ b/allianceauth/admin_status/models.py @@ -0,0 +1,45 @@ +from django.db import models +from django.utils.translation import gettext_lazy as _ + +from allianceauth.admin_status.managers import ApplicationAnnouncementManager + + +class ApplicationAnnouncement(models.Model): + """ + Announcement originating from an application + """ + object = ApplicationAnnouncementManager() + + application_name = models.CharField(max_length=50, help_text=_("Name of the application that issued the announcement")) + announcement_number = models.IntegerField(help_text=_("Issue number on the notification source")) + announcement_text = models.TextField(max_length=300, help_text=_("Issue title text displayed on the dashboard")) + announcement_url = models.TextField(max_length=200) + + announcement_hash = models.CharField( + max_length=64, + default=None, + unique=True, + editable=False, + help_text="hash of an announcement." + ) + + hide_announcement = models.BooleanField( + default=False, + help_text=_("Set to true if the announcement should not be displayed on the dashboard") + ) + + class Meta: + # Should be updated to a composite key when the switch to Django 5.2 is made + # https://docs.djangoproject.com/en/5.2/topics/composite-primary-key/ + constraints = [ + models.UniqueConstraint( + fields=["application_name", "announcement_number"], name="functional_pk_applicationissuenumber" + ) + ] + + def __str__(self): + return f"{self.application_name} announcement #{self.announcement_number}" + + def is_hidden(self) -> bool: + """Function in case rules are made in the future to force hide/force show some announcements""" + return self.hide_announcement diff --git a/allianceauth/templates/allianceauth/admin-status/celery_bar_partial.html b/allianceauth/admin_status/templates/admin-status/celery_bar_partial.html similarity index 100% rename from allianceauth/templates/allianceauth/admin-status/celery_bar_partial.html rename to allianceauth/admin_status/templates/admin-status/celery_bar_partial.html diff --git a/allianceauth/templates/allianceauth/admin-status/esi_check.html b/allianceauth/admin_status/templates/admin-status/esi_check.html similarity index 100% rename from allianceauth/templates/allianceauth/admin-status/esi_check.html rename to allianceauth/admin_status/templates/admin-status/esi_check.html diff --git a/allianceauth/templates/allianceauth/admin-status/include.html b/allianceauth/admin_status/templates/admin-status/include.html similarity index 100% rename from allianceauth/templates/allianceauth/admin-status/include.html rename to allianceauth/admin_status/templates/admin-status/include.html diff --git a/allianceauth/templates/allianceauth/admin-status/overview.html b/allianceauth/admin_status/templates/admin-status/overview.html similarity index 69% rename from allianceauth/templates/allianceauth/admin-status/overview.html rename to allianceauth/admin_status/templates/admin-status/overview.html index 867322b6..448a58e5 100644 --- a/allianceauth/templates/allianceauth/admin-status/overview.html +++ b/allianceauth/admin_status/templates/admin-status/overview.html @@ -2,65 +2,21 @@ {% load humanize %} {% if notifications %} -

+ -{% 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? #} @@ -131,9 +87,9 @@ style="height: 21px;" title="{{ tasks_succeeded|intcomma }} succeeded, {{ tasks_retried|intcomma }} retried, {{ tasks_failed|intcomma }} failed" > - {% include "allianceauth/admin-status/celery_bar_partial.html" with label="suceeded" level="success" tasks_count=tasks_succeeded %} - {% include "allianceauth/admin-status/celery_bar_partial.html" with label="retried" level="info" tasks_count=tasks_retried %} - {% include "allianceauth/admin-status/celery_bar_partial.html" with label="failed" level="danger" tasks_count=tasks_failed %} + {% include "admin-status/celery_bar_partial.html" with label="suceeded" level="success" tasks_count=tasks_succeeded %} + {% include "admin-status/celery_bar_partial.html" with label="retried" level="info" tasks_count=tasks_retried %} + {% include "admin-status/celery_bar_partial.html" with label="failed" level="danger" tasks_count=tasks_failed %}

@@ -142,6 +98,20 @@

+
diff --git a/allianceauth/admin_status/templatetags/__init__.py b/allianceauth/admin_status/templatetags/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/allianceauth/admin_status/templatetags/admin_status.py b/allianceauth/admin_status/templatetags/admin_status.py new file mode 100644 index 00000000..af1f8f48 --- /dev/null +++ b/allianceauth/admin_status/templatetags/admin_status.py @@ -0,0 +1,181 @@ +import logging + +import requests +from packaging.version import InvalidVersion, Version as Pep440Version + +from django import template +from django.conf import settings +from django.core.cache import cache + +from allianceauth import __version__ +from allianceauth.admin_status.models import ApplicationAnnouncement +from allianceauth.authentication.task_statistics.counters import ( + dashboard_results, +) + +register = template.Library() + +# cache timers +TAG_CACHE_TIME = 3600 # 1 hours +NOTIFICATION_CACHE_TIME = 300 # 5 minutes +# timeout for all requests +REQUESTS_TIMEOUT = 5 # 5 seconds +# max pages to be fetched from gitlab +MAX_PAGES = 50 + +GITLAB_AUTH_REPOSITORY_TAGS_URL = ( + 'https://gitlab.com/api/v4/projects/allianceauth%2Fallianceauth/repository/tags' +) + +logger = logging.getLogger(__name__) + + +@register.simple_tag() +def decimal_widthratio(this_value, max_value, max_width) -> str: + if max_value == 0: + return str(0) + + return str(round(this_value / max_value * max_width, 2)) + + +@register.inclusion_tag('admin-status/overview.html') +def status_overview() -> dict: + response = { + "notifications": [], + "current_version": __version__, + "tasks_succeeded": 0, + "tasks_retried": 0, + "tasks_failed": 0, + "tasks_total": 0, + "tasks_hours": 0, + "earliest_task": None, + } + response.update(_current_notifications()) + response.update(_current_version_summary()) + response.update(_celery_stats()) + return response + + +def _celery_stats() -> dict: + hours = getattr(settings, "ALLIANCEAUTH_DASHBOARD_TASKS_MAX_HOURS", 24) + results = dashboard_results(hours=hours) + return { + "tasks_succeeded": results.succeeded, + "tasks_retried": results.retried, + "tasks_failed": results.failed, + "tasks_total": results.total, + "tasks_hours": results.hours, + "earliest_task": results.earliest_task, + } + + +def _current_notifications() -> dict: + """returns announcements from AllianceAuth and third party applications""" + + application_notifications = ApplicationAnnouncement.object.sync_and_return() + + response = { + 'notifications': application_notifications, + } + return response + +def _current_version_summary() -> dict: + """returns the current version info""" + try: + tags = cache.get_or_set( + 'git_release_tags', _fetch_tags_from_gitlab, TAG_CACHE_TIME + ) + except requests.HTTPError: + logger.warning('Error while getting gitlab release tags', exc_info=True) + return {} + + if not tags: + return {} + + ( + latest_patch_version, + latest_beta_version + ) = _latests_versions(tags) + current_version = Pep440Version(__version__) + + has_latest_patch = \ + current_version >= latest_patch_version if latest_patch_version else False + has_current_beta = \ + current_version <= latest_beta_version \ + and latest_patch_version <= latest_beta_version \ + if latest_beta_version else False + + response = { + 'latest_patch': has_latest_patch, + 'latest_beta': has_current_beta, + 'current_version': str(current_version), + 'latest_patch_version': str(latest_patch_version), + 'latest_beta_version': str(latest_beta_version) + } + return response + + +def _fetch_tags_from_gitlab(): + return _fetch_list_from_gitlab(GITLAB_AUTH_REPOSITORY_TAGS_URL) + + +def _latests_versions(tags: list) -> tuple: + """returns latests version from given tags list + + Non-compliant tags will be ignored + """ + versions = [] + betas = [] + for tag in tags: + try: + version = Pep440Version(tag.get('name')) + except InvalidVersion: + pass + else: + if version.is_prerelease or version.is_devrelease: + betas.append(version) + else: + versions.append(version) + + latest_patch_version = max(versions) + latest_beta_version = max(betas) + return ( + latest_patch_version, + latest_beta_version + ) + + +def _fetch_list_from_gitlab(url: str, max_pages: int = MAX_PAGES) -> list: + """returns a list from the GitLab API. Supports paging""" + result = [] + + for page in range(1, max_pages + 1): + try: + request = requests.get( + url, params={'page': page}, timeout=REQUESTS_TIMEOUT + ) + request.raise_for_status() + except requests.exceptions.RequestException as e: + error_str = str(e) + + logger.warning( + f'Unable to fetch from GitLab API. Error: {error_str}', + exc_info=True, + ) + + return result + + result += request.json() + + if 'x-total-pages' in request.headers: + try: + total_pages = int(request.headers['x-total-pages']) + except ValueError: + total_pages = None + else: + total_pages = None + + if not total_pages or page >= total_pages: + break + + return result diff --git a/allianceauth/admin_status/tests/__init__.py b/allianceauth/admin_status/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/allianceauth/admin_status/tests/test_managers.py b/allianceauth/admin_status/tests/test_managers.py new file mode 100644 index 00000000..9735e00d --- /dev/null +++ b/allianceauth/admin_status/tests/test_managers.py @@ -0,0 +1,75 @@ +from unittest.mock import patch + +from allianceauth.admin_status.hooks import Announcement +from allianceauth.admin_status.models import ApplicationAnnouncement +from allianceauth.utils.testing import NoSocketsTestCase + +MODULE_PATH = 'allianceauth.admin_status.managers' + +DEFAULT_ANNOUNCEMENTS = [ + Announcement( + application_name="Test GitHub Application", + announcement_number=1, + announcement_text="GitHub issue", + announcement_url="https://github.com/r0kym/test/issues/1", + ), + Announcement( + application_name="Test Gitlab Application", + announcement_number=1, + announcement_text="GitLab issue", + announcement_url="https://gitlab.com/r0kym/allianceauth-example-plugin/-/issues/1", + ) +] + +class TestSyncManager(NoSocketsTestCase): + + def setUp(self): + ApplicationAnnouncement.object.create( + application_name="Test GitHub Application", + announcement_number=1, + announcement_text="GitHub issue", + announcement_url="https://github.com/r0kym/test/issues/1", + announcement_hash="9dbedb9c47529bb43cfecb704768a35d085b145930e13cced981623e5f162a85", + ) + ApplicationAnnouncement.object.create( + application_name="Test Gitlab Application", + announcement_number=1, + announcement_text="GitLab issue", + announcement_url="https://gitlab.com/r0kym/allianceauth-example-plugin/-/issues/1", + announcement_hash="8955a9c12a1cfa9e1776662bdaf111147b84e35c79f24bfb758e35333a18b1bd", + ) + + @patch(MODULE_PATH + '.get_all_applications_announcements') + def test_announcements_stay_as_is(self, all_announcements_mocker): + # given + announcement_ids = set(ApplicationAnnouncement.object.values_list("id", flat=True)) + all_announcements_mocker.return_value = DEFAULT_ANNOUNCEMENTS + # when + ApplicationAnnouncement.object.sync_and_return() + # then + self.assertEqual(ApplicationAnnouncement.object.count(), 2) + self.assertEqual(set(ApplicationAnnouncement.object.values_list("id", flat=True)), announcement_ids) + + @patch(MODULE_PATH + '.get_all_applications_announcements') + def test_announcement_add(self, all_announcements_mocker): + # given + returned_announcements = DEFAULT_ANNOUNCEMENTS + [Announcement(application_name="Test Application", announcement_number=1, announcement_text="New test announcement", announcement_url="https://example.com")] + all_announcements_mocker.return_value = returned_announcements + # when + ApplicationAnnouncement.object.sync_and_return() + # then + self.assertEqual(ApplicationAnnouncement.object.count(), 3) + self.assertTrue(ApplicationAnnouncement.object.filter(application_name="Test Application", announcement_number=1, announcement_text="New test announcement", announcement_url="https://example.com")) + + @patch(MODULE_PATH + '.get_all_applications_announcements') + def test_announcement_remove(self, all_announcements_mocker): + # given + all_announcements_mocker.return_value = DEFAULT_ANNOUNCEMENTS + ApplicationAnnouncement.object.sync_and_return() + self.assertEqual(ApplicationAnnouncement.object.count(), 2) + all_announcements_mocker.return_value = DEFAULT_ANNOUNCEMENTS[:1] + # when + ApplicationAnnouncement.object.sync_and_return() + # then + self.assertEqual(ApplicationAnnouncement.object.count(), 1) + self.assertTrue(ApplicationAnnouncement.object.filter(application_name="Test GitHub Application").exists()) diff --git a/allianceauth/authentication/tests/test_templatetags.py b/allianceauth/admin_status/tests/test_templatetags.py similarity index 80% rename from allianceauth/authentication/tests/test_templatetags.py rename to allianceauth/admin_status/tests/test_templatetags.py index 7f42f830..9f5d6234 100644 --- a/allianceauth/authentication/tests/test_templatetags.py +++ b/allianceauth/admin_status/tests/test_templatetags.py @@ -8,23 +8,61 @@ from packaging.version import Version as Pep440Version from django.core.cache import cache from django.test import TestCase -from allianceauth.templatetags.admin_status import ( +from allianceauth.admin_status.models import ApplicationAnnouncement +from allianceauth.admin_status.templatetags.admin_status import ( _current_notifications, _current_version_summary, _fetch_list_from_gitlab, - _fetch_notification_issues_from_gitlab, _latests_versions, status_overview, ) -MODULE_PATH = 'allianceauth.templatetags' +MODULE_PATH = 'allianceauth.admin_status.templatetags' def create_tags_list(tag_names: list): return [{'name': str(tag_name)} for tag_name in tag_names] +def get_app_announcement_as_dict(app_announcement: ApplicationAnnouncement) -> dict: + """Transforms an app announcement object in a dict easy to compare""" + return { + "application_name": app_announcement.application_name, + "announcement_number": app_announcement.announcement_number, + "announcement_text": app_announcement.announcement_text, + "announcement_url": app_announcement.announcement_url, + } + GITHUB_TAGS = create_tags_list(['v2.4.6a1', 'v2.4.5', 'v2.4.0', 'v2.0.0', 'v1.1.1']) +STORED_NOTIFICATIONS = [ + ApplicationAnnouncement( + application_name="Test GitHub Application", + announcement_number=1, + announcement_text="GitHub issue", + announcement_url="https://github.com/r0kym/test/issues/1", + announcement_hash="hash1", + ), + ApplicationAnnouncement( + application_name="Test Gitlab Application", + announcement_number=1, + announcement_text="GitLab issue", + announcement_url="https://gitlab.com/r0kym/allianceauth-example-plugin/-/issues/1", + announcement_hash="hash2", + ), +] +ANNOUNCEMENT_DICT = [ + { + "application_name": "Test GitHub Application", + "announcement_number": 1, + "announcement_text": "GitHub issue", + "announcement_url": "https://github.com/r0kym/test/issues/1", + }, { + "application_name": "Test Gitlab Application", + "announcement_number": 1, + "announcement_text": "GitLab issue", + "announcement_url": "https://gitlab.com/r0kym/allianceauth-example-plugin/-/issues/1", + } +] GITHUB_NOTIFICATION_ISSUES = [ { 'id': 1, @@ -52,6 +90,10 @@ GITHUB_NOTIFICATION_ISSUES = [ }, ] TEST_VERSION = '2.6.5' +GITLAB_AUTH_ANNOUNCEMENT_ISSUES_URL = ( + 'https://gitlab.com/api/v4/projects/allianceauth%2Fallianceauth/issues' + '?labels=announcement&state=opened' +) class TestStatusOverviewTag(TestCase): @@ -107,18 +149,19 @@ class TestNotifications(TestCase): ) requests_mocker.get(url, json=GITHUB_NOTIFICATION_ISSUES) # when - result = _fetch_notification_issues_from_gitlab() + result = _fetch_list_from_gitlab(GITLAB_AUTH_ANNOUNCEMENT_ISSUES_URL, 10) # then self.assertEqual(result, GITHUB_NOTIFICATION_ISSUES) - @patch(MODULE_PATH + '.admin_status.cache') - def test_current_notifications_normal(self, mock_cache): + @patch(MODULE_PATH + '.admin_status.ApplicationAnnouncement') + def test_current_notifications_normal(self, mock_application_announcement): # given - mock_cache.get_or_set.return_value = GITHUB_NOTIFICATION_ISSUES + mock_application_announcement.object.sync_and_return.return_value = STORED_NOTIFICATIONS # when result = _current_notifications() # then - self.assertEqual(result['notifications'], GITHUB_NOTIFICATION_ISSUES[:5]) + for notification in result["notifications"]: + self.assertIn(get_app_announcement_as_dict(notification), ANNOUNCEMENT_DICT) @requests_mock.mock() def test_current_notifications_failed(self, requests_mocker): @@ -131,16 +174,7 @@ class TestNotifications(TestCase): # when result = _current_notifications() # then - self.assertEqual(result['notifications'], []) - - @patch(MODULE_PATH + '.admin_status.cache') - def test_current_notifications_is_none(self, mock_cache): - # given - mock_cache.get_or_set.return_value = None - # when - result = _current_notifications() - # then - self.assertEqual(result['notifications'], []) + self.assertEqual(list(result['notifications']), []) class TestCeleryQueueLength(TestCase): diff --git a/allianceauth/authentication/views.py b/allianceauth/authentication/views.py index b5983a27..63c2d6c7 100644 --- a/allianceauth/authentication/views.py +++ b/allianceauth/authentication/views.py @@ -74,14 +74,14 @@ def dashboard_characters(request): def dashboard_admin(request): if request.user.is_superuser: - return render_to_string('allianceauth/admin-status/include.html', request=request) + return render_to_string('admin-status/include.html', request=request) else: return "" def dashboard_esi_check(request): if request.user.is_superuser: - return render_to_string('allianceauth/admin-status/esi_check.html', request=request) + return render_to_string('admin-status/esi_check.html', request=request) else: return "" diff --git a/allianceauth/services/hooks.py b/allianceauth/services/hooks.py index 75ed30dc..b52527e0 100644 --- a/allianceauth/services/hooks.py +++ b/allianceauth/services/hooks.py @@ -8,7 +8,7 @@ from django.utils.functional import cached_property from allianceauth.hooks import get_hooks from allianceauth.menu.hooks import MenuItemHook -from allianceauth.templatetags.admin_status import AppAnnouncementHook +from allianceauth.admin_status.hooks import AppAnnouncementHook from .models import NameFormatConfig diff --git a/allianceauth/templatetags/admin_status.py b/allianceauth/templatetags/admin_status.py deleted file mode 100644 index 6768a9a7..00000000 --- a/allianceauth/templatetags/admin_status.py +++ /dev/null @@ -1,326 +0,0 @@ -import logging -from dataclasses import dataclass -from enum import Enum -from urllib.parse import quote_plus - -import requests -from packaging.version import InvalidVersion, Version as Pep440Version - -from django import template -from django.conf import settings -from django.core.cache import cache - -from allianceauth import __version__ -from allianceauth.authentication.task_statistics.counters import ( - dashboard_results, -) -from allianceauth.hooks import get_hooks - -register = template.Library() - -# cache timers -TAG_CACHE_TIME = 3600 # 1 hours -NOTIFICATION_CACHE_TIME = 300 # 5 minutes -# timeout for all requests -REQUESTS_TIMEOUT = 5 # 5 seconds -# max pages to be fetched from gitlab -MAX_PAGES = 50 - -GITLAB_AUTH_REPOSITORY_TAGS_URL = ( - 'https://gitlab.com/api/v4/projects/allianceauth%2Fallianceauth/repository/tags' -) -GITLAB_AUTH_ANNOUNCEMENT_ISSUES_URL = ( - 'https://gitlab.com/api/v4/projects/allianceauth%2Fallianceauth/issues' - '?labels=announcement&state=opened' -) - -logger = logging.getLogger(__name__) - -@dataclass -class AppAnnouncementHook: - """ - A hook for an application to send GitHub/GitLab issues as announcements on the dashboard - - Args: - - app_name: The name of your application - - repository_namespace: The namespace of the remote repository of your application source code. - It should look like `/`. - - repository_kind: Enumeration to determine if your repository is a GitHub or GitLab repository. - - label: The label applied to issues that should be seen as announcements, case-sensitive. - Default value: `announcement` - """ - class RepositoryKind(Enum): - """Simple enumeration to determine which api should be called to access issues""" - GITLAB = "gitlab" - GITHUB = "github" - - 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 AppAnnouncementHook.RepositoryKind.GITHUB: - announcement_list = self._get_github_announcement_list() - case AppAnnouncementHook.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}" - ) - # Translates GitHub attributes to GitLab and filters out pull requests - clean_list = [] - for element in raw_list: - if not element.get("pull_request"): - element["web_url"] = element["html_url"] - element["iid"] = element["number"] - clean_list.append(element) - return clean_list - - 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}&state=opened") - -@register.simple_tag() -def decimal_widthratio(this_value, max_value, max_width) -> str: - if max_value == 0: - return str(0) - - return str(round(this_value / max_value * max_width, 2)) - - -@register.inclusion_tag('allianceauth/admin-status/overview.html') -def status_overview() -> dict: - response = { - "notifications": [], - "current_version": __version__, - "tasks_succeeded": 0, - "tasks_retried": 0, - "tasks_failed": 0, - "tasks_total": 0, - "tasks_hours": 0, - "earliest_task": None, - } - response.update(_current_notifications()) - response.update(_current_version_summary()) - response.update(_celery_stats()) - return response - - -def _celery_stats() -> dict: - hours = getattr(settings, "ALLIANCEAUTH_DASHBOARD_TASKS_MAX_HOURS", 24) - results = dashboard_results(hours=hours) - return { - "tasks_succeeded": results.succeeded, - "tasks_retried": results.retried, - "tasks_failed": results.failed, - "tasks_total": results.total, - "tasks_hours": results.hours, - "earliest_task": results.earliest_task, - } - - -def _current_notifications() -> dict: - """returns the newest 5 announcement issues""" - try: - notifications = cache.get_or_set( - 'gitlab_notification_issues', - _fetch_notification_issues_from_gitlab, - NOTIFICATION_CACHE_TIME - ) - except requests.HTTPError: - logger.warning('Error while getting gitlab notifications', exc_info=True) - top_notifications = [] - else: - if notifications: - top_notifications = notifications[:5] - else: - top_notifications = [] - - app_notifications = [] - hooks = [fn() for fn in get_hooks("app_announcement_hook")] - for hook in hooks: - logger.debug(hook) - try: - 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 when getting %s notifications", hook, exc_info=True) - - if app_notifications: - logger.debug(app_notifications) - application_notifications = app_notifications[:10] - else: - application_notifications = [] - - response = { - 'notifications': top_notifications, - 'application_notifications': application_notifications, - } - return response - - -def _fetch_notification_issues_from_gitlab() -> list: - return _fetch_list_from_gitlab(GITLAB_AUTH_ANNOUNCEMENT_ISSUES_URL, max_pages=10) - - -def _current_version_summary() -> dict: - """returns the current version info""" - try: - tags = cache.get_or_set( - 'git_release_tags', _fetch_tags_from_gitlab, TAG_CACHE_TIME - ) - except requests.HTTPError: - logger.warning('Error while getting gitlab release tags', exc_info=True) - return {} - - if not tags: - return {} - - ( - latest_patch_version, - latest_beta_version - ) = _latests_versions(tags) - current_version = Pep440Version(__version__) - - has_latest_patch = \ - current_version >= latest_patch_version if latest_patch_version else False - has_current_beta = \ - current_version <= latest_beta_version \ - and latest_patch_version <= latest_beta_version \ - if latest_beta_version else False - - response = { - 'latest_patch': has_latest_patch, - 'latest_beta': has_current_beta, - 'current_version': str(current_version), - 'latest_patch_version': str(latest_patch_version), - 'latest_beta_version': str(latest_beta_version) - } - return response - - -def _fetch_tags_from_gitlab(): - return _fetch_list_from_gitlab(GITLAB_AUTH_REPOSITORY_TAGS_URL) - - -def _latests_versions(tags: list) -> tuple: - """returns latests version from given tags list - - Non-compliant tags will be ignored - """ - versions = [] - betas = [] - for tag in tags: - try: - version = Pep440Version(tag.get('name')) - except InvalidVersion: - pass - else: - if version.is_prerelease or version.is_devrelease: - betas.append(version) - else: - versions.append(version) - - latest_patch_version = max(versions) - latest_beta_version = max(betas) - return ( - latest_patch_version, - latest_beta_version - ) - - -def _fetch_list_from_gitlab(url: str, max_pages: int = MAX_PAGES) -> list: - """returns a list from the GitLab API. Supports paging""" - result = [] - - for page in range(1, max_pages + 1): - try: - request = requests.get( - url, params={'page': page}, timeout=REQUESTS_TIMEOUT - ) - request.raise_for_status() - except requests.exceptions.RequestException as e: - error_str = str(e) - - logger.warning( - f'Unable to fetch from GitLab API. Error: {error_str}', - exc_info=True, - ) - - return result - - result += request.json() - - if 'x-total-pages' in request.headers: - try: - total_pages = int(request.headers['x-total-pages']) - except ValueError: - total_pages = None - else: - total_pages = None - - if not total_pages or page >= total_pages: - 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""" - - 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() - logger.debug(request.json()) - - # https://docs.github.com/en/rest/using-the-rest-api/using-pagination-in-the-rest-api?apiVersion=2022-11-28 - # See Example creating a pagination method - if not ('link' in request.headers and 'rel=\"next\"' in request.headers['link']): - break - - return result diff --git a/docs/development/custom/app-announcement-hooks.md b/docs/development/custom/app-announcement-hooks.md index 03650a87..594a1d90 100644 --- a/docs/development/custom/app-announcement-hooks.md +++ b/docs/development/custom/app-announcement-hooks.md @@ -9,11 +9,12 @@ To register an AppAnnouncementHook class, you would do the following: ```python from allianceauth import hooks -from allianceauth.services.hooks import AppAnnouncementHook +from allianceauth.admin_status.hooks import AppAnnouncementHook + @hooks.register('app_announcement_hook') def announcement_hook(): - return AppAnnouncementHook("Your app name", "USERNAME/REPOSITORY_NAME", AppAnnouncementHook.RepositoryKind.GITLAB) + return AppAnnouncementHook("Your app name", "USERNAME/REPOSITORY_NAME", AppAnnouncementHook.Service.GITLAB) ``` ```{eval-rst} From 39071f7fc3d114b73132e8ce9bf5e162313c4f81 Mon Sep 17 00:00:00 2001 From: T'rahk Rokym Date: Sun, 18 May 2025 23:20:42 +0200 Subject: [PATCH 12/16] fix pre-commit --- allianceauth/services/hooks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/allianceauth/services/hooks.py b/allianceauth/services/hooks.py index b52527e0..0049ecd9 100644 --- a/allianceauth/services/hooks.py +++ b/allianceauth/services/hooks.py @@ -6,9 +6,9 @@ from django.core.exceptions import ObjectDoesNotExist from django.urls import include, re_path from django.utils.functional import cached_property +from allianceauth.admin_status.hooks import AppAnnouncementHook from allianceauth.hooks import get_hooks from allianceauth.menu.hooks import MenuItemHook -from allianceauth.admin_status.hooks import AppAnnouncementHook from .models import NameFormatConfig From 0e588bf5cd2a617baf2bfbc10b3c8550a69a189b Mon Sep 17 00:00:00 2001 From: T'rahk Rokym Date: Mon, 19 May 2025 18:13:15 +0200 Subject: [PATCH 13/16] Change announcement card title --- allianceauth/admin_status/templates/admin-status/overview.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/allianceauth/admin_status/templates/admin-status/overview.html b/allianceauth/admin_status/templates/admin-status/overview.html index 448a58e5..9970328d 100644 --- a/allianceauth/admin_status/templates/admin-status/overview.html +++ b/allianceauth/admin_status/templates/admin-status/overview.html @@ -5,7 +5,7 @@
- {% translate "AllianceAuth and 3rd party Applications Notifications" as widget_title %} + {% translate "Announcements" as widget_title %} {% include "framework/dashboard/widget-title.html" with title=widget_title %}
From aa21cab96702f6cb80b018ed69c1ecfea57e811d Mon Sep 17 00:00:00 2001 From: T'rahk Rokym Date: Mon, 19 May 2025 18:34:54 +0200 Subject: [PATCH 14/16] Increase test coverage --- allianceauth/admin_status/tests/test_hooks.py | 194 ++++++++++++++++++ 1 file changed, 194 insertions(+) create mode 100644 allianceauth/admin_status/tests/test_hooks.py diff --git a/allianceauth/admin_status/tests/test_hooks.py b/allianceauth/admin_status/tests/test_hooks.py new file mode 100644 index 00000000..5a1ab8fd --- /dev/null +++ b/allianceauth/admin_status/tests/test_hooks.py @@ -0,0 +1,194 @@ +import requests_mock + +from allianceauth.admin_status.hooks import Announcement +from allianceauth.services.hooks import AppAnnouncementHook +from allianceauth.utils.testing import NoSocketsTestCase + + +class TestHooks(NoSocketsTestCase): + + @requests_mock.mock() + def test_fetch_gitlab(self, requests_mocker): + # given + announcement_hook = AppAnnouncementHook("test GitLab app", "r0kym/allianceauth-example-plugin", + AppAnnouncementHook.Service.GITLAB) + requests_mocker.get( + "https://gitlab.com/api/v4/projects/r0kym%2Fallianceauth-example-plugin/issues?labels=announcement&state=opened", + json=[ + { + "id": 166279127, + "iid": 1, + "project_id": 67653102, + "title": "Test GitLab issue", + "description": "Test issue", + "state": "opened", + "created_at": "2025-04-20T21:26:57.914Z", + "updated_at": "2025-04-21T11:04:30.501Z", + "closed_at": None, + "closed_by": None, + "labels": [ + "announcement" + ], + "milestone": None, + "assignees": [], + "author": { + "id": 14491514, + "username": "r0kym", + "public_email": "", + "name": "T'rahk Rokym", + "state": "active", + "locked": False, + "avatar_url": "https://gitlab.com/uploads/-/system/user/avatar/14491514/avatar.png", + "web_url": "https://gitlab.com/r0kym" + }, + "type": "ISSUE", + "assignee": None, + "user_notes_count": 0, + "merge_requests_count": 0, + "upvotes": 0, + "downvotes": 0, + "due_date": None, + "confidential": False, + "discussion_locked": None, + "issue_type": "issue", + "web_url": "https://gitlab.com/r0kym/allianceauth-example-plugin/-/issues/1", + "time_stats": { + "time_estimate": 0, + "total_time_spent": 0, + "human_time_estimate": None, + "human_total_time_spent": None + }, + "task_completion_status": { + "count": 0, + "completed_count": 0 + }, + "blocking_issues_count": 0, + "has_tasks": True, + "task_status": "0 of 0 checklist items completed", + "_links": { + "self": "https://gitlab.com/api/v4/projects/67653102/issues/1", + "notes": "https://gitlab.com/api/v4/projects/67653102/issues/1/notes", + "award_emoji": "https://gitlab.com/api/v4/projects/67653102/issues/1/award_emoji", + "project": "https://gitlab.com/api/v4/projects/67653102", + "closed_as_duplicate_of": None + }, + "references": { + "short": "#1", + "relative": "#1", + "full": "r0kym/allianceauth-example-plugin#1" + }, + "severity": "UNKNOWN", + "moved_to_id": None, + "imported": False, + "imported_from": "none", + "service_desk_reply_to": None + } + ] + ) + # when + announcements = announcement_hook.get_announcement_list() + # then + self.assertEqual(len(announcements), 1) + self.assertIn(Announcement( + application_name="test GitLab app", + announcement_url="https://gitlab.com/r0kym/allianceauth-example-plugin/-/issues/1", + announcement_number=1, + announcement_text="Test GitLab issue" + ), announcements) + + @requests_mock.mock() + def test_fetch_github(self, requests_mocker): + # given + announcement_hook = AppAnnouncementHook("test GitHub app", "r0kym/test", AppAnnouncementHook.Service.GITHUB) + requests_mocker.get( + "https://api.github.com/repos/r0kym/test/issues?labels=announcement", + json=[ + { + "url": "https://api.github.com/repos/r0kym/test/issues/1", + "repository_url": "https://api.github.com/repos/r0kym/test", + "labels_url": "https://api.github.com/repos/r0kym/test/issues/1/labels{/name}", + "comments_url": "https://api.github.com/repos/r0kym/test/issues/1/comments", + "events_url": "https://api.github.com/repos/r0kym/test/issues/1/events", + "html_url": "https://github.com/r0kym/test/issues/1", + "id": 3007269496, + "node_id": "I_kwDOOc2YvM6zP0p4", + "number": 1, + "title": "GitHub issue", + "user": { + "login": "r0kym", + "id": 56434393, + "node_id": "MDQ6VXNlcjU2NDM0Mzkz", + "avatar_url": "https://avatars.githubusercontent.com/u/56434393?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/r0kym", + "html_url": "https://github.com/r0kym", + "followers_url": "https://api.github.com/users/r0kym/followers", + "following_url": "https://api.github.com/users/r0kym/following{/other_user}", + "gists_url": "https://api.github.com/users/r0kym/gists{/gist_id}", + "starred_url": "https://api.github.com/users/r0kym/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/r0kym/subscriptions", + "organizations_url": "https://api.github.com/users/r0kym/orgs", + "repos_url": "https://api.github.com/users/r0kym/repos", + "events_url": "https://api.github.com/users/r0kym/events{/privacy}", + "received_events_url": "https://api.github.com/users/r0kym/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": False + }, + "labels": [ + { + "id": 8487814480, + "node_id": "LA_kwDOOc2YvM8AAAAB-enFUA", + "url": "https://api.github.com/repos/r0kym/test/labels/announcement", + "name": "announcement", + "color": "aaaaaa", + "default": False, + "description": None + } + ], + "state": "open", + "locked": False, + "assignee": None, + "assignees": [], + "milestone": None, + "comments": 0, + "created_at": "2025-04-20T22:41:10Z", + "updated_at": "2025-04-21T11:05:08Z", + "closed_at": None, + "author_association": "OWNER", + "active_lock_reason": None, + "sub_issues_summary": { + "total": 0, + "completed": 0, + "percent_completed": 0 + }, + "body": None, + "closed_by": None, + "reactions": { + "url": "https://api.github.com/repos/r0kym/test/issues/1/reactions", + "total_count": 0, + "+1": 0, + "-1": 0, + "laugh": 0, + "hooray": 0, + "confused": 0, + "heart": 0, + "rocket": 0, + "eyes": 0 + }, + "timeline_url": "https://api.github.com/repos/r0kym/test/issues/1/timeline", + "performed_via_github_app": None, + "state_reason": None + } + ] + ) + # when + announcements = announcement_hook.get_announcement_list() + # then + self.assertEqual(len(announcements), 1) + self.assertIn(Announcement( + application_name="test GitHub app", + announcement_url="https://github.com/r0kym/test/issues/1", + announcement_number=1, + announcement_text="GitHub issue" + ), announcements) From 5cb5aef7e45921ddf796453bae6947efa5c5ea27 Mon Sep 17 00:00:00 2001 From: T'rahk Rokym Date: Mon, 19 May 2025 23:04:26 +0200 Subject: [PATCH 15/16] Fix doc --- allianceauth/services/hooks.py | 2 +- .../custom/app-announcement-hooks.md | 8 ++++---- .../img/app_announcement_hook_example.png | Bin 29395 -> 0 bytes 3 files changed, 5 insertions(+), 5 deletions(-) delete mode 100755 docs/development/custom/img/app_announcement_hook_example.png diff --git a/allianceauth/services/hooks.py b/allianceauth/services/hooks.py index 0049ecd9..7e76d6d5 100644 --- a/allianceauth/services/hooks.py +++ b/allianceauth/services/hooks.py @@ -148,7 +148,7 @@ class MenuItemHook(MenuItemHook): class AppAnnouncementHook(AppAnnouncementHook): """ - AppAnnouncementHook shim to allianceauth.templatetags.admin_status + AppAnnouncementHook shim to allianceauth.admin_status.hooks :param AppAnnouncementHook: _description_ :type AppAnnouncementHook: _type_ diff --git a/docs/development/custom/app-announcement-hooks.md b/docs/development/custom/app-announcement-hooks.md index 594a1d90..e4992864 100644 --- a/docs/development/custom/app-announcement-hooks.md +++ b/docs/development/custom/app-announcement-hooks.md @@ -9,7 +9,7 @@ To register an AppAnnouncementHook class, you would do the following: ```python from allianceauth import hooks -from allianceauth.admin_status.hooks import AppAnnouncementHook +from allianceauth.services.hooks import AppAnnouncementHook @hooks.register('app_announcement_hook') @@ -35,14 +35,14 @@ Here you should enter the namespace of your repository. The structure stays the same for both GitHub and GitLab repositories. \ A repository with the url `https://gitlab.com/username/appname` will have a namespace of `username/appname`. -### repository_kind +### Service -This variable is an enumeration of the class `AppAnnouncemementHook.RepositoryKind` +This variable is an enumeration of the class `AppAnnouncemementHook.Service` It is mandatory to specify this variable so alliance auth contacts the correct API when fetching your repository issues. ```{eval-rst} -.. autoclass:: allianceauth.services.hooks.AppAnnouncementHook.RepositoryKind +.. autoclass:: allianceauth.services.hooks.AppAnnouncementHook.Service :members: GITLAB, GITHUB :undoc-members: ``` diff --git a/docs/development/custom/img/app_announcement_hook_example.png b/docs/development/custom/img/app_announcement_hook_example.png deleted file mode 100755 index b57c6950830ffd69a39b1320601776fd978dc846..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 29395 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

OHA^F`sh;2 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 WE8;bB01l!&qo%BRzf{rk`F{Xp_%urZ From 2891b7e43923c33570dd01c09f99e5c46ce5455f Mon Sep 17 00:00:00 2001 From: T'rahk Rokym Date: Mon, 19 May 2025 23:05:54 +0200 Subject: [PATCH 16/16] The image wasn't added correctly --- .../img/app_announcement_hook_example.png | Bin 0 -> 23079 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100755 docs/development/custom/img/app_announcement_hook_example.png diff --git a/docs/development/custom/img/app_announcement_hook_example.png b/docs/development/custom/img/app_announcement_hook_example.png new file mode 100755 index 0000000000000000000000000000000000000000..6b9b494417a853778c87cb3f7b792842bda80bf2 GIT binary patch literal 23079 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