mirror of
https://gitlab.com/allianceauth/allianceauth.git
synced 2025-07-21 02:08:59 +02:00
Merge branch 'app-announcement' into 'v5.x'
Application Announcement Hook See merge request allianceauth/allianceauth!1717
This commit is contained in:
commit
a63f602e7d
@ -8,6 +8,7 @@ from django.utils.functional import cached_property
|
|||||||
|
|
||||||
from allianceauth.hooks import get_hooks
|
from allianceauth.hooks import get_hooks
|
||||||
from allianceauth.menu.hooks import MenuItemHook
|
from allianceauth.menu.hooks import MenuItemHook
|
||||||
|
from allianceauth.templatetags.admin_status import AppAnnouncementHook
|
||||||
|
|
||||||
from .models import NameFormatConfig
|
from .models import NameFormatConfig
|
||||||
|
|
||||||
@ -145,6 +146,16 @@ class MenuItemHook(MenuItemHook):
|
|||||||
def __init_subclass__(cls) -> None:
|
def __init_subclass__(cls) -> None:
|
||||||
return super().__init_subclass__()
|
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:
|
class UrlHook:
|
||||||
"""A hook for registering the URLs of a Django app.
|
"""A hook for registering the URLs of a Django app.
|
||||||
|
@ -46,6 +46,30 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% if application_notifications %}
|
||||||
|
<div id="aa-dashboard-panel-admin-application-notifications" class="col-12 mb-3">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
{% translate "Application Notifications" as widget_title %}
|
||||||
|
{% include "framework/dashboard/widget-title.html" with title=widget_title %}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<ul class="list-group">
|
||||||
|
{% for notif in application_notifications %}
|
||||||
|
<li class="list-group-item">
|
||||||
|
<span class="badge bg-success me-2">{% translate "Open" %}</span>
|
||||||
|
<span class="badge bg-info me-2">{{ notif.app_name }}</span>
|
||||||
|
<a href="{{ notif.web_url }}" target="_blank">#{{ notif.iid }} {{ notif.title }}</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{# TODO maybe add some disclaimer that those are managed by application devs? #}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<div class="col-12 mb-3">
|
<div class="col-12 mb-3">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-body row">
|
<div class="card-body row">
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
import logging
|
import logging
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from enum import Enum
|
||||||
|
from urllib.parse import quote_plus
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from packaging.version import InvalidVersion, Version as Pep440Version
|
from packaging.version import InvalidVersion, Version as Pep440Version
|
||||||
@ -11,6 +14,7 @@ from allianceauth import __version__
|
|||||||
from allianceauth.authentication.task_statistics.counters import (
|
from allianceauth.authentication.task_statistics.counters import (
|
||||||
dashboard_results,
|
dashboard_results,
|
||||||
)
|
)
|
||||||
|
from allianceauth.hooks import get_hooks
|
||||||
|
|
||||||
register = template.Library()
|
register = template.Library()
|
||||||
|
|
||||||
@ -32,6 +36,72 @@ GITLAB_AUTH_ANNOUNCEMENT_ISSUES_URL = (
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
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 `<username>/<application_name>`.
|
||||||
|
- 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()
|
@register.simple_tag()
|
||||||
def decimal_widthratio(this_value, max_value, max_width) -> str:
|
def decimal_widthratio(this_value, max_value, max_width) -> str:
|
||||||
@ -89,8 +159,28 @@ def _current_notifications() -> dict:
|
|||||||
else:
|
else:
|
||||||
top_notifications = []
|
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 = {
|
response = {
|
||||||
'notifications': top_notifications,
|
'notifications': top_notifications,
|
||||||
|
'application_notifications': application_notifications,
|
||||||
}
|
}
|
||||||
return response
|
return response
|
||||||
|
|
||||||
@ -199,3 +289,38 @@ def _fetch_list_from_gitlab(url: str, max_pages: int = MAX_PAGES) -> list:
|
|||||||
break
|
break
|
||||||
|
|
||||||
return result
|
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
|
||||||
|
52
docs/development/custom/app-announcement-hooks.md
Normal file
52
docs/development/custom/app-announcement-hooks.md
Normal file
@ -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.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
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)
|
||||||
|
```
|
||||||
|
|
||||||
|
```{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`
|
||||||
|
|
||||||
|
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
|
||||||
|
: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"`.
|
BIN
docs/development/custom/img/app_announcement_hook_example.png
Executable file
BIN
docs/development/custom/img/app_announcement_hook_example.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 29 KiB |
@ -8,6 +8,7 @@ This section describes how to extend **Alliance Auth** with custom apps, service
|
|||||||
integrating-services
|
integrating-services
|
||||||
menu-hooks
|
menu-hooks
|
||||||
url-hooks
|
url-hooks
|
||||||
|
app-announcement-hooks
|
||||||
logging
|
logging
|
||||||
custom-themes
|
custom-themes
|
||||||
aa-framework
|
aa-framework
|
||||||
|
Loading…
x
Reference in New Issue
Block a user