mirror of
https://gitlab.com/allianceauth/allianceauth.git
synced 2026-02-12 18:16:24 +01:00
Compare commits
24 Commits
v5.0.0a1
...
2891b7e439
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2891b7e439 | ||
|
|
5cb5aef7e4 | ||
|
|
aa21cab967 | ||
|
|
0e588bf5cd | ||
|
|
39071f7fc3 | ||
|
|
97f603c138 | ||
|
|
c9b07c12a0 | ||
|
|
fd84f7fe15 | ||
|
|
92d8c699eb | ||
|
|
9cc3283399 | ||
|
|
401c093b74 | ||
|
|
b3534f4f44 | ||
|
|
f88249c8fc | ||
|
|
ec34d7fd29 | ||
|
|
cd9d985732 | ||
|
|
1c1e219037 | ||
|
|
49a271a99f | ||
|
|
af87da876b | ||
|
|
57b3841293 | ||
|
|
b02413c30c | ||
|
|
7ba1699dc6 | ||
|
|
75d67aa1b1 | ||
|
|
876f1e48e7 | ||
|
|
c7db4f0bd3 |
@@ -25,13 +25,13 @@ exclude: |
|
||||
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.9.9
|
||||
rev: v0.11.4
|
||||
hooks:
|
||||
# Run the linter, and only the linter
|
||||
- id: ruff
|
||||
|
||||
- repo: https://github.com/adamchainz/django-upgrade
|
||||
rev: 1.23.1
|
||||
rev: 1.24.0
|
||||
hooks:
|
||||
- id: django-upgrade
|
||||
args: [--target-version=5.1]
|
||||
@@ -59,7 +59,7 @@ repos:
|
||||
- id: detect-private-key
|
||||
- id: check-case-conflict
|
||||
# Python checks
|
||||
# - id: check-docstring-first
|
||||
#
|
||||
- id: debug-statements
|
||||
# - id: requirements-txt-fixer
|
||||
- id: fix-encoding-pragma
|
||||
@@ -73,7 +73,7 @@ repos:
|
||||
- id: check-executables-have-shebangs
|
||||
- id: end-of-file-fixer
|
||||
- repo: https://github.com/editorconfig-checker/editorconfig-checker.python
|
||||
rev: 3.2.0
|
||||
rev: 3.2.1
|
||||
hooks:
|
||||
- id: editorconfig-checker
|
||||
- repo: https://github.com/igorshubovych/markdownlint-cli
|
||||
@@ -85,18 +85,18 @@ repos:
|
||||
- --disable=MD013
|
||||
# Infrastructure
|
||||
- repo: https://github.com/tox-dev/pyproject-fmt
|
||||
rev: v2.5.0
|
||||
rev: v2.5.1
|
||||
hooks:
|
||||
- id: pyproject-fmt
|
||||
name: pyproject.toml formatter
|
||||
description: "Format the pyproject.toml file."
|
||||
args:
|
||||
- --indent=4
|
||||
additional_dependencies:
|
||||
- tox==4.24.1 # https://github.com/tox-dev/tox/releases/latest
|
||||
- repo: https://github.com/tox-dev/tox-ini-fmt
|
||||
rev: 1.5.0
|
||||
hooks:
|
||||
- id: tox-ini-fmt
|
||||
- repo: https://github.com/abravalheri/validate-pyproject
|
||||
rev: v0.23
|
||||
rev: v0.24.1
|
||||
hooks:
|
||||
- id: validate-pyproject
|
||||
name: Validate pyproject.toml
|
||||
description: "Validate the pyproject.toml file."
|
||||
|
||||
0
allianceauth/admin_status/__init__.py
Normal file
0
allianceauth/admin_status/__init__.py
Normal file
19
allianceauth/admin_status/admin.py
Normal file
19
allianceauth/admin_status/admin.py
Normal file
@@ -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
|
||||
6
allianceauth/admin_status/apps.py
Normal file
6
allianceauth/admin_status/apps.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class AdminStatusApplication(AppConfig):
|
||||
name = 'allianceauth.admin_status'
|
||||
label = 'admin_status'
|
||||
207
allianceauth/admin_status/hooks.py
Normal file
207
allianceauth/admin_status/hooks.py
Normal file
@@ -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 `<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 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
|
||||
57
allianceauth/admin_status/managers.py
Normal file
57
allianceauth/admin_status/managers.py
Normal file
@@ -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(),
|
||||
)
|
||||
33
allianceauth/admin_status/migrations/0001_initial.py
Normal file
33
allianceauth/admin_status/migrations/0001_initial.py
Normal file
@@ -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()),
|
||||
],
|
||||
),
|
||||
]
|
||||
0
allianceauth/admin_status/migrations/__init__.py
Normal file
0
allianceauth/admin_status/migrations/__init__.py
Normal file
45
allianceauth/admin_status/models.py
Normal file
45
allianceauth/admin_status/models.py
Normal file
@@ -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
|
||||
@@ -2,44 +2,24 @@
|
||||
{% load humanize %}
|
||||
|
||||
{% if notifications %}
|
||||
<div id="aa-dashboard-panel-admin-notifications" class="col-12 mb-3">
|
||||
<div id="aa-dashboard-panel-admin-application-notifications" class="col-12 mb-3">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
{% translate "Alliance Auth Notifications" as widget_title %}
|
||||
{% translate "Announcements" as widget_title %}
|
||||
{% include "framework/dashboard/widget-title.html" with title=widget_title %}
|
||||
|
||||
<div>
|
||||
<ul class="list-group">
|
||||
{% for notif in notifications %}
|
||||
<li class="list-group-item">
|
||||
{% if notif.state == 'opened' %}
|
||||
<span class="badge bg-success me-2">{% translate "Open" %}</span>
|
||||
{% else %}
|
||||
<span class="badge bg-danger me-2">{% translate "Closed" %}</span>
|
||||
{% endif %}
|
||||
<a href="{{ notif.web_url }}" target="_blank">#{{ notif.iid }} {{ notif.title }}</a>
|
||||
</li>
|
||||
{% empty %}
|
||||
<div class="alert alert-primary" role="alert">
|
||||
{% translate "No notifications at this time" %}
|
||||
</div>
|
||||
{% if not notif.is_hidden %}
|
||||
<li class="list-group-item">
|
||||
<span class="badge bg-info me-2">{{ notif.application_name }}</span>
|
||||
<a href="{{ notif.announcement_url }}" target="_blank">#{{ notif.announcement_number }} {{ notif.announcement_text }}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<div class="text-end pt-3">
|
||||
<a href="https://gitlab.com/allianceauth/allianceauth/issues" target="_blank" class="me-1 text-decoration-none">
|
||||
<span class="badge" style="background-color: rgb(230 83 40);">
|
||||
<i class="fab fa-gitlab" aria-hidden="true"></i>
|
||||
{% translate 'Powered by GitLab' %}
|
||||
</span>
|
||||
</a>
|
||||
<a href="https://discord.com/invite/fjnHAmk" target="_blank" class="text-decoration-none">
|
||||
<span class="badge" style="background-color: rgb(110 133 211);">
|
||||
<i class="fab fa-discord" aria-hidden="true"></i>
|
||||
{% translate 'Support Discord' %}
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
{# TODO maybe add some disclaimer that those are managed by application devs? #}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -107,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 %}
|
||||
</div>
|
||||
|
||||
<p>
|
||||
@@ -118,6 +98,20 @@
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-end pt-3">
|
||||
<a href="https://gitlab.com/allianceauth/allianceauth/issues" target="_blank" class="me-1 text-decoration-none">
|
||||
<span class="badge" style="background-color: rgb(230 83 40);">
|
||||
<i class="fab fa-gitlab" aria-hidden="true"></i>
|
||||
{% translate 'Powered by GitLab' %}
|
||||
</span>
|
||||
</a>
|
||||
<a href="https://discord.com/invite/fjnHAmk" target="_blank" class="text-decoration-none">
|
||||
<span class="badge" style="background-color: rgb(110 133 211);">
|
||||
<i class="fab fa-discord" aria-hidden="true"></i>
|
||||
{% translate 'Support Discord' %}
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
0
allianceauth/admin_status/templatetags/__init__.py
Normal file
0
allianceauth/admin_status/templatetags/__init__.py
Normal file
@@ -8,6 +8,7 @@ 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,
|
||||
)
|
||||
@@ -25,10 +26,6 @@ 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__)
|
||||
|
||||
@@ -41,7 +38,7 @@ def decimal_widthratio(this_value, max_value, max_width) -> str:
|
||||
return str(round(this_value / max_value * max_width, 2))
|
||||
|
||||
|
||||
@register.inclusion_tag('allianceauth/admin-status/overview.html')
|
||||
@register.inclusion_tag('admin-status/overview.html')
|
||||
def status_overview() -> dict:
|
||||
response = {
|
||||
"notifications": [],
|
||||
@@ -73,32 +70,15 @@ def _celery_stats() -> dict:
|
||||
|
||||
|
||||
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 = []
|
||||
"""returns announcements from AllianceAuth and third party applications"""
|
||||
|
||||
application_notifications = ApplicationAnnouncement.object.sync_and_return()
|
||||
|
||||
response = {
|
||||
'notifications': top_notifications,
|
||||
'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:
|
||||
0
allianceauth/admin_status/tests/__init__.py
Normal file
0
allianceauth/admin_status/tests/__init__.py
Normal file
194
allianceauth/admin_status/tests/test_hooks.py
Normal file
194
allianceauth/admin_status/tests/test_hooks.py
Normal file
@@ -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)
|
||||
75
allianceauth/admin_status/tests/test_managers.py
Normal file
75
allianceauth/admin_status/tests/test_managers.py
Normal file
@@ -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())
|
||||
@@ -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):
|
||||
@@ -32,7 +32,7 @@ class State(models.Model):
|
||||
class Meta:
|
||||
ordering = ['-priority']
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
||||
|
||||
def available_to_character(self, character):
|
||||
@@ -153,7 +153,7 @@ class CharacterOwnership(models.Model):
|
||||
class Meta:
|
||||
default_permissions = ('change', 'delete')
|
||||
ordering = ['user', 'character__character_name']
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return f"{self.user}: {self.character}"
|
||||
|
||||
|
||||
@@ -166,5 +166,5 @@ class OwnershipRecord(models.Model):
|
||||
class Meta:
|
||||
ordering = ['-created']
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return f"{self.user}: {self.character} on {self.created}"
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ class CorpStats(models.Model):
|
||||
|
||||
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return f"{self.__class__.__name__} for {self.corp}"
|
||||
|
||||
def update(self):
|
||||
@@ -154,7 +154,7 @@ class CorpMember(models.Model):
|
||||
unique_together = ('corpstats', 'character_id')
|
||||
ordering = ['character_name']
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return self.character_name
|
||||
|
||||
@property
|
||||
|
||||
@@ -81,7 +81,7 @@ class AutogroupsConfig(models.Model):
|
||||
|
||||
objects = AutogroupsConfigManager()
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return 'States: ' + (' '.join(list(self.states.all().values_list('name', flat=True))) if self.pk else str(None))
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -235,7 +235,7 @@ class ManagedGroup(models.Model):
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return f"Managed Group: {self.group.name}"
|
||||
|
||||
class ManagedCorpGroup(ManagedGroup):
|
||||
|
||||
@@ -32,7 +32,7 @@ class EveFactionInfo(models.Model):
|
||||
|
||||
provider = providers.provider
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return self.faction_name
|
||||
|
||||
@staticmethod
|
||||
@@ -80,7 +80,7 @@ class EveAllianceInfo(models.Model):
|
||||
|
||||
class Meta:
|
||||
indexes = [models.Index(fields=['executor_corp_id',])]
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return self.alliance_name
|
||||
def populate_alliance(self):
|
||||
alliance = self.provider.get_alliance(self.alliance_id)
|
||||
@@ -152,7 +152,7 @@ class EveCorporationInfo(models.Model):
|
||||
|
||||
class Meta:
|
||||
indexes = [models.Index(fields=['ceo_id',]),]
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return self.corporation_name
|
||||
def update_corporation(self, corp: providers.Corporation = None):
|
||||
if corp is None:
|
||||
@@ -226,7 +226,7 @@ class EveCharacter(models.Model):
|
||||
models.Index(fields=['faction_id',]),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return self.character_name
|
||||
|
||||
@property
|
||||
|
||||
@@ -36,7 +36,7 @@ class ObjectNotFound(Exception):
|
||||
self.id = obj_id
|
||||
self.type = type_name
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return f'{self.type} with ID {self.id} not found.'
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ class Entity:
|
||||
self.id = id
|
||||
self.name = name
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
||||
|
||||
def __repr__(self):
|
||||
@@ -206,7 +206,7 @@ class EveSwaggerProvider(EveProvider):
|
||||
)
|
||||
return self._client
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return 'esi'
|
||||
|
||||
def get_alliance(self, alliance_id: int) -> Alliance:
|
||||
|
||||
@@ -13,7 +13,7 @@ class BravadoResponseStub:
|
||||
self.headers = headers if headers else {}
|
||||
self.raw_bytes = raw_bytes
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return f"{self.status_code} {self.reason}"
|
||||
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ class Fatlink(models.Model):
|
||||
hash = models.CharField(max_length=254, unique=True)
|
||||
creator = models.ForeignKey(User, on_delete=models.SET(get_sentinel_user))
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return self.fleet
|
||||
|
||||
|
||||
@@ -28,5 +28,5 @@ class Fat(models.Model):
|
||||
class Meta:
|
||||
unique_together = (('character', 'fatlink'),)
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return f"Fat-link for {self.character.character_name}"
|
||||
|
||||
@@ -15,7 +15,7 @@ class GroupRequest(models.Model):
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
group = models.ForeignKey(Group, on_delete=models.CASCADE)
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return self.user.username + ":" + self.group.name
|
||||
|
||||
@property
|
||||
@@ -50,7 +50,7 @@ class RequestLog(models.Model):
|
||||
request_actor = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
date = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return self.pk
|
||||
|
||||
def requestor(self):
|
||||
@@ -179,7 +179,7 @@ class AuthGroup(models.Model):
|
||||
)
|
||||
default_permissions = ()
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return self.group.name
|
||||
|
||||
def group_request_approvers(self) -> set[User]:
|
||||
|
||||
@@ -13,7 +13,7 @@ class ApplicationQuestion(models.Model):
|
||||
help_text = models.CharField(max_length=254, blank=True)
|
||||
multi_select = models.BooleanField(default=False)
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return "Question: " + self.title
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ class ApplicationChoice(models.Model):
|
||||
question = models.ForeignKey(ApplicationQuestion,on_delete=models.CASCADE,related_name="choices")
|
||||
choice_text = models.CharField(max_length=200, verbose_name='Choice')
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return self.choice_text
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ class ApplicationForm(models.Model):
|
||||
questions = SortedManyToManyField(ApplicationQuestion)
|
||||
corp = models.OneToOneField(EveCorporationInfo, on_delete=models.CASCADE)
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return str(self.corp)
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ class Application(models.Model):
|
||||
('view_apis', 'Can view applicant APIs'),)
|
||||
unique_together = ('form', 'user')
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return str(self.user) + " Application To " + str(self.form)
|
||||
|
||||
@property
|
||||
@@ -77,7 +77,7 @@ class ApplicationResponse(models.Model):
|
||||
answer = models.TextField()
|
||||
class Meta:
|
||||
unique_together = ('question', 'application')
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return str(self.application) + " Answer To " + str(self.question)
|
||||
|
||||
|
||||
@@ -89,5 +89,5 @@ class ApplicationComment(models.Model):
|
||||
text = models.TextField()
|
||||
created = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return str(self.user) + " comment on " + str(self.application)
|
||||
|
||||
@@ -19,7 +19,7 @@ class OpTimerType(models.Model):
|
||||
ordering = ['type']
|
||||
default_permissions = ()
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return self.type
|
||||
|
||||
class OpTimer(models.Model):
|
||||
@@ -39,5 +39,5 @@ class OpTimer(models.Model):
|
||||
class Meta:
|
||||
ordering = ['start']
|
||||
default_permissions = ()
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return self.operation_name
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
#!/usr/bin/env python
|
||||
"""Django's command-line utility for administrative tasks."""
|
||||
import os
|
||||
import sys
|
||||
|
||||
if __name__ == "__main__":
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "{{ project_name}}.settings.local")
|
||||
|
||||
def main() -> None:
|
||||
"""Run administrative tasks."""
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', '{{ project_name }}.settings')
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError as err:
|
||||
# The above import may fail for some other reason. Ensure that the
|
||||
# issue is really that Django is missing to avoid masking other
|
||||
# exceptions on Python 2.
|
||||
try:
|
||||
import django # noqa: F401
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"Couldn't import Django. Are you sure it's installed and "
|
||||
"available on your PYTHONPATH environment variable? Did you "
|
||||
"forget to activate a virtual environment?"
|
||||
) from err
|
||||
raise
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
"Couldn't import Django. Are you sure it's installed and "
|
||||
"available on your PYTHONPATH environment variable? Did you "
|
||||
"forget to activate a virtual environment?"
|
||||
) from exc
|
||||
execute_from_command_line(sys.argv)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
@@ -6,6 +6,7 @@ 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
|
||||
|
||||
@@ -119,7 +120,7 @@ class ServicesHook:
|
||||
"""
|
||||
return ''
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return self.name or 'Unknown Service Module'
|
||||
|
||||
class Urls:
|
||||
@@ -145,6 +146,16 @@ class MenuItemHook(MenuItemHook):
|
||||
def __init_subclass__(cls) -> None:
|
||||
return super().__init_subclass__()
|
||||
|
||||
class AppAnnouncementHook(AppAnnouncementHook):
|
||||
"""
|
||||
AppAnnouncementHook shim to allianceauth.admin_status.hooks
|
||||
|
||||
: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.
|
||||
|
||||
@@ -26,7 +26,7 @@ class NameFormatConfig(models.Model):
|
||||
"formatter for each state for each service."
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return '{}: {}'.format(
|
||||
self.service_name, ', '.join([str(x) for x in self.states.all()])
|
||||
)
|
||||
|
||||
@@ -16,5 +16,5 @@ class DiscourseUser(models.Model):
|
||||
("access_discourse", "Can access the Discourse service"),
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return self.user.username
|
||||
|
||||
@@ -18,7 +18,7 @@ class SrpFleetMain(models.Model):
|
||||
class Meta:
|
||||
permissions = (('access_srp', 'Can access SRP module'),)
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return self.fleet_name
|
||||
|
||||
@property
|
||||
@@ -46,5 +46,5 @@ class SrpUserRequest(models.Model):
|
||||
srp_ship_name = models.CharField(max_length=254, default="")
|
||||
post_time = models.DateTimeField(default=timezone.now)
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return self.character.character_name + ' SRP request for ' + self.srp_ship_name
|
||||
|
||||
@@ -14,6 +14,10 @@ app = Celery('myauth')
|
||||
# Celery startup if it is unavailable.
|
||||
app.conf.broker_connection_retry_on_startup = True
|
||||
|
||||
# Set a hard task execution time of 5 minutes before celery will cold restart
|
||||
app.conf.worker_soft_shutdown_timeout = 300
|
||||
app.conf.worker_enable_soft_shutdown_on_idle = True
|
||||
|
||||
# Using a string here means the worker don't have to serialize
|
||||
# the configuration object to child processes.
|
||||
app.config_from_object('django.conf:settings')
|
||||
|
||||
@@ -89,8 +89,8 @@ services:
|
||||
container_name: allianceauth_gunicorn
|
||||
<<: [*allianceauth-base]
|
||||
entrypoint: ["gunicorn", "myauth.wsgi", "--bind=0.0.0.0:8000", "--workers=3", "--timeout=120", "--max-requests=500", "--max-requests-jitter=50"]
|
||||
ports:
|
||||
- 8000:8000
|
||||
expose:
|
||||
- 8000
|
||||
|
||||
allianceauth_beat:
|
||||
container_name: allianceauth_worker_beat
|
||||
|
||||
53
docs/development/custom/app-announcement-hooks.md
Normal file
53
docs/development/custom/app-announcement-hooks.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# 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.Service.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`.
|
||||
|
||||
### Service
|
||||
|
||||
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.Service
|
||||
: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: 22 KiB |
@@ -8,6 +8,7 @@ This section describes how to extend **Alliance Auth** with custom apps, service
|
||||
integrating-services
|
||||
menu-hooks
|
||||
url-hooks
|
||||
app-announcement-hooks
|
||||
logging
|
||||
custom-themes
|
||||
aa-framework
|
||||
|
||||
@@ -67,13 +67,13 @@ sudo apt-get install gettext
|
||||
Next, we need to install Python and related development tools.
|
||||
|
||||
:::{note}
|
||||
Should your Ubuntu come with a newer version of Python we recommend to still set up your dev environment with the oldest Python 3 version currently supported by AA (e.g., Python 3.8 at this time of writing) to ensure your apps are compatible with all current AA installations
|
||||
Should your Ubuntu come with a newer version of Python we recommend to still set up your dev environment with the oldest Python 3 version currently supported by AA (e.g., Python 3.10 at this time of writing) to ensure your apps are compatible with all current AA installations
|
||||
You can check out this `page <https://askubuntu.com/questions/682869/how-do-i-install-a-different-python-version-using-apt-get/1195153>`_ on how to install additional Python versions on Ubuntu.
|
||||
|
||||
If you install a different python version from the default, you need to adjust some commands below to install appopriate versions of those packages, for example, using Python 3.8 you might need to run the following after using the setup steps for the repository mentioned in the AskUbuntu post above:
|
||||
If you install a different python version from the default, you need to adjust some commands below to install appopriate versions of those packages, for example, using Python 3.10 you might need to run the following after using the setup steps for the repository mentioned in the AskUbuntu post above:
|
||||
|
||||
```shell
|
||||
sudo apt-get install python3.8 python3.8-dev python3.8-venv python3-setuptools python3-pip python-pip
|
||||
sudo apt-get install python3.10 python3.10-dev python3.10-venv python3-setuptools python3-pip python-pip
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
@@ -17,14 +17,14 @@ This guide is currently for Ubuntu only.
|
||||
The mumble server package can be retrieved from a repository, which we need to add:
|
||||
|
||||
::::{tabs}
|
||||
:::{group-tab} Ubuntu 2004, 2204, 2404
|
||||
:::{group-tab} Ubuntu 2204, 2404
|
||||
|
||||
```shell
|
||||
sudo apt-add-repository ppa:mumble/release
|
||||
```
|
||||
|
||||
:::
|
||||
:::{group-tab} CentOS 7, Stream 8, Stream 9
|
||||
:::{group-tab} CentOS Stream 9, 10
|
||||
|
||||
sudo yum install epel-release
|
||||
sudo yum update
|
||||
@@ -35,14 +35,14 @@ sudo yum update
|
||||
Now three packages need to be installed:
|
||||
|
||||
::::{tabs}
|
||||
:::{group-tab} Ubuntu 2004, 2204, 2404
|
||||
:::{group-tab} Ubuntu 2204, 2404
|
||||
|
||||
```shell
|
||||
sudo apt-get install software-properties-common mumble-server libqt5sql5-mysql
|
||||
```
|
||||
|
||||
:::
|
||||
:::{group-tab} CentOS 7, Stream 8, Stream 9
|
||||
:::{group-tab} CentOS Stream 9, 10
|
||||
|
||||
sudo yum install mumble-server
|
||||
|
||||
|
||||
@@ -24,28 +24,14 @@ BROADCAST_SERVICE_NAME = "broadcast"
|
||||
Openfire require a Java 8 runtime environment.
|
||||
|
||||
::::{tabs}
|
||||
:::{group-tab} Ubuntu 2004, 2204
|
||||
:::{group-tab} Ubuntu 2204, 2404
|
||||
|
||||
```shell
|
||||
sudo apt-get install openjdk-11-jre
|
||||
```
|
||||
|
||||
:::
|
||||
:::{group-tab} CentOS 7
|
||||
|
||||
```shell
|
||||
sudo yum install java-11-openjdk java-11-openjdk-devel
|
||||
```
|
||||
|
||||
:::
|
||||
:::{group-tab} CentOS Stream 8
|
||||
|
||||
```shell
|
||||
sudo dnf install java-11-openjdk java-11-openjdk-devel
|
||||
```
|
||||
|
||||
:::
|
||||
:::{group-tab} CentOS Stream 9
|
||||
:::{group-tab} CentOS Stream 9, 10
|
||||
|
||||
```shell
|
||||
sudo dnf install java-11-openjdk java-11-openjdk-devel
|
||||
@@ -73,18 +59,10 @@ cd ~
|
||||
Download and install the package, replacing the URL with the latest you got from the Openfire download page earlier
|
||||
|
||||
::::{tabs}
|
||||
:::{group-tab} Ubuntu 2004, 2204
|
||||
:::{group-tab} Ubuntu 2204, 2404
|
||||
|
||||
:::
|
||||
:::{group-tab} CentOS 7
|
||||
wget <https://www.igniterealtime.org/downloadServlet?filename=openfire/openfire_4.7.2_all.deb>
|
||||
dpkg -i openfire_4.7.2_all.deb
|
||||
:::
|
||||
:::{group-tab} CentOS Stream 8
|
||||
wget <https://www.igniterealtime.org/downloadServlet?filename=openfire/openfire-4.7.2-1.noarch.rpm>
|
||||
yum install -y openfire-4.7.2-1.noarch.rpm
|
||||
:::
|
||||
:::{group-tab} CentOS Stream 9
|
||||
:::{group-tab} CentOS Stream 9, 10
|
||||
wget <https://www.igniterealtime.org/downloadServlet?filename=openfire/openfire-4.7.2-1.noarch.rpm>
|
||||
yum install -y openfire-4.7.2-1.noarch.rpm
|
||||
:::
|
||||
|
||||
@@ -14,11 +14,10 @@ Alliance Auth can be installed on any in-support *nix operating system.
|
||||
|
||||
Our install documentation targets the following operating systems.
|
||||
|
||||
- Ubuntu 20.04 - Not Recommended for new installs
|
||||
- Ubuntu 22.04
|
||||
- Centos 7
|
||||
- CentOS Stream 8
|
||||
- Ubuntu 22.04 (New installs please use 2404)
|
||||
- Ubutnu 24.04
|
||||
- CentOS Stream 9
|
||||
- CentOS Stream 10
|
||||
|
||||
To install on your favorite flavour of Linux, identify and install equivalent packages to the ones listed here.
|
||||
|
||||
@@ -27,7 +26,7 @@ To install on your favorite flavour of Linux, identify and install equivalent pa
|
||||
It is recommended to ensure your OS is fully up-to-date before proceeding. We may also add Package Repositories here, used later in the documentation.
|
||||
|
||||
::::{tabs}
|
||||
:::{group-tab} Ubuntu 2004, 2204, 2404
|
||||
:::{group-tab} Ubuntu 2204, 2404
|
||||
|
||||
```shell
|
||||
sudo apt-get update
|
||||
@@ -36,24 +35,7 @@ sudo do-dist-upgrade
|
||||
```
|
||||
|
||||
:::
|
||||
:::{group-tab} CentOS 7
|
||||
|
||||
```shell
|
||||
yum install epel-release
|
||||
sudo yum upgrade
|
||||
```
|
||||
|
||||
:::
|
||||
:::{group-tab} CentOS Stream 8
|
||||
|
||||
```shell
|
||||
sudo dnf config-manager --set-enabled powertools
|
||||
sudo dnf install epel-release epel-next-release
|
||||
sudo yum upgrade
|
||||
```
|
||||
|
||||
:::
|
||||
:::{group-tab} CentOS Stream 9
|
||||
:::{group-tab} CentOS Stream 9, 10
|
||||
|
||||
```shell
|
||||
sudo dnf config-manager --set-enabled crb
|
||||
@@ -66,58 +48,33 @@ sudo yum upgrade
|
||||
|
||||
### Python
|
||||
|
||||
Install Python 3.11 and related tools on your system.
|
||||
Install Python 3.12 and related tools on your system.
|
||||
|
||||
::::{tabs}
|
||||
|
||||
:::{group-tab} Ubuntu 2004, 2204, 2404
|
||||
:::{group-tab} Ubuntu 2204
|
||||
|
||||
```shell
|
||||
sudo add-apt-repository ppa:deadsnakes/ppa
|
||||
sudo apt-get update
|
||||
sudo apt-get install python3.11 python3.11-dev python3.11-venv
|
||||
sudo apt-get install python3.12 python3.12-dev python3.12-venv
|
||||
```
|
||||
|
||||
:::
|
||||
:::{group-tab} CentOS 7
|
||||
We need to build Python from source
|
||||
:::{group-tab} Ubuntu 2404
|
||||
|
||||
```bash
|
||||
cd ~
|
||||
sudo yum install gcc openssl-devel bzip2-devel libffi-devel wget
|
||||
wget https://www.python.org/ftp/python/3.11.7/Python-3.11.7.tgz
|
||||
tar xvf Python-3.11.7.tgz
|
||||
cd Python-3.11.7/
|
||||
./configure --enable-optimizations --enable-shared
|
||||
sudo make altinstall
|
||||
```shell
|
||||
sudo add-apt-repository ppa:deadsnakes/ppa
|
||||
sudo apt-get update
|
||||
sudo apt-get install python3.12 python3.12-dev python3.12-venv
|
||||
```
|
||||
|
||||
:::
|
||||
:::{group-tab} CentOS Stream 8
|
||||
We need to build Python from source
|
||||
:::{group-tab} CentOS Stream 9, 10
|
||||
|
||||
```bash
|
||||
cd ~
|
||||
sudo yum install gcc openssl-devel bzip2-devel libffi-devel wget
|
||||
wget https://www.python.org/ftp/python/3.11.7/Python-3.11.7.tgz
|
||||
tar xvf Python-3.11.7.tgz
|
||||
cd Python-3.11.7/
|
||||
./configure --enable-optimizations --enable-shared
|
||||
sudo make altinstall
|
||||
```
|
||||
sudo dnf update
|
||||
sudo dnf install python3.12 python3.12-dev python3.12-venv
|
||||
|
||||
:::
|
||||
:::{group-tab} CentOS Stream 9
|
||||
We need to build Python from source
|
||||
|
||||
```bash
|
||||
cd ~
|
||||
sudo yum install gcc openssl-devel bzip2-devel libffi-devel wget
|
||||
wget https://www.python.org/ftp/python/3.11.7/Python-3.11.7.tgz
|
||||
tar xvf Python-3.11.7.tgz
|
||||
cd Python-3.11.7/
|
||||
./configure --enable-optimizations --enable-shared
|
||||
sudo make altinstall
|
||||
```
|
||||
|
||||
:::
|
||||
@@ -128,32 +85,24 @@ sudo make altinstall
|
||||
It's recommended to use a database service instead of SQLite. Many options are available, but this guide will use MariaDB 10.11
|
||||
|
||||
::::{tabs}
|
||||
:::{group-tab} Ubuntu 2004, 2204, 2404
|
||||
Follow the instructions at <https://mariadb.org/download/?t=repo-config&d=20.04+%22focal%22&v=10.11&r_m=osuosl> to add the MariaDB repository to your host.
|
||||
:::{group-tab} Ubuntu 2204
|
||||
Follow the instructions at <https://mariadb.org/download/?t=repo-config&d=22.04+%22noble%22&v=11.4> to add the MariaDB repository to your host.
|
||||
|
||||
```shell
|
||||
sudo apt-get install mariadb-server mariadb-client libmysqlclient-dev
|
||||
```
|
||||
|
||||
:::
|
||||
:::{group-tab} CentOS 7
|
||||
Follow the instructions at <https://mariadb.org/download/?t=repo-config&d=CentOS+7&v=10.11&r_m=osuosl> to add the MariaDB repository to your host.
|
||||
:::{group-tab} Ubuntu 2404
|
||||
Follow the instructions at <https://mariadb.org/download/?t=repo-config&d=24.04+%22noble%22&v=11.4> to add the MariaDB repository to your host.
|
||||
|
||||
```shell
|
||||
sudo yum install MariaDB-server MariaDB-client MariaDB-devel MariaDB-shared
|
||||
sudo apt-get install mariadb-server mariadb-client libmysqlclient-dev
|
||||
```
|
||||
|
||||
:::
|
||||
:::{group-tab} CentOS Stream 8
|
||||
Follow the instructions at <https://mariadb.org/download/?t=repo-config&d=CentOS+Stream&v=10.11&r_m=osuosl> to add the MariaDB repository to your host.
|
||||
|
||||
```shell
|
||||
sudo dnf install mariadb mariadb-server mariadb-devel
|
||||
```
|
||||
|
||||
:::
|
||||
:::{group-tab} CentOS Stream 9
|
||||
Follow the instructions at <https://mariadb.org/download/?t=repo-config&d=CentOS+Stream&v=10.11&r_m=osuosl> to add the MariaDB repository to your host.
|
||||
:::{group-tab} CentOS Stream 9, 10
|
||||
Follow the instructions at <https://mariadb.org/download/?t=repo-config&d=CentOS+Stream&v=11.4> to add the MariaDB repository to your host.
|
||||
|
||||
```shell
|
||||
sudo dnf install mariadb mariadb-server mariadb-devel
|
||||
@@ -164,16 +113,10 @@ sudo dnf install mariadb mariadb-server mariadb-devel
|
||||
|
||||
:::::{important}
|
||||
::::{tabs}
|
||||
:::{group-tab} Ubuntu 2004, 2204, 2404
|
||||
:::{group-tab} Ubuntu 2204, 2404
|
||||
If you don't plan on running the database on the same server as auth you still need to install the `libmysqlclient-dev` package
|
||||
:::
|
||||
:::{group-tab} CentOS 7
|
||||
If you don't plan on running the database on the same server as auth you still need to install the `mariadb-devel` package
|
||||
:::
|
||||
:::{group-tab} CentOS Stream 8
|
||||
If you don't plan on running the database on the same server as auth you still need to install the `mariadb-devel` package
|
||||
:::
|
||||
:::{group-tab} CentOS Stream 9
|
||||
:::{group-tab} CentOS Stream 9, 10
|
||||
If you don't plan on running the database on the same server as auth you still need to install the `mariadb-devel` package
|
||||
:::
|
||||
::::
|
||||
@@ -185,9 +128,10 @@ A few extra utilities are also required for the installation of packages.
|
||||
|
||||
::::{tabs}
|
||||
|
||||
:::{group-tab} Ubuntu 2004, 2204, 2404
|
||||
:::{group-tab} Ubuntu 2204, 2404
|
||||
|
||||
```shell
|
||||
sudo apt-get install lsb-release curl gpg
|
||||
curl -fsSL https://packages.redis.io/gpg | sudo gpg --dearmor -o /usr/share/keyrings/redis-archive-keyring.gpg
|
||||
sudo chmod 644 /usr/share/keyrings/redis-archive-keyring.gpg
|
||||
echo "deb [signed-by=/usr/share/keyrings/redis-archive-keyring.gpg] https://packages.redis.io/deb $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/redis.list
|
||||
@@ -195,32 +139,13 @@ sudo apt-get update
|
||||
sudo apt-get install unzip git redis-server curl libssl-dev libbz2-dev libffi-dev build-essential pkg-config
|
||||
```
|
||||
|
||||
:::
|
||||
:::{group-tab} CentOS 7
|
||||
|
||||
```shell
|
||||
sudo yum install gcc gcc-c++ unzip git redis curl bzip2-devel openssl-devel libffi-devel wget pkg-config
|
||||
```
|
||||
|
||||
```shell
|
||||
sudo systemctl enable redis.service
|
||||
sudo systemctl start redis.service
|
||||
sudo systemctl enable redis-server
|
||||
sudo systemctl start redis-server
|
||||
```
|
||||
|
||||
:::
|
||||
:::{group-tab} CentOS Stream 8
|
||||
|
||||
```shell
|
||||
sudo dnf install gcc gcc-c++ unzip git redis curl bzip2-devel openssl-devel libffi-devel wget
|
||||
```
|
||||
|
||||
```shell
|
||||
sudo systemctl enable redis.service
|
||||
sudo systemctl start redis.service
|
||||
```
|
||||
|
||||
:::
|
||||
:::{group-tab} CentOS Stream 9
|
||||
:::{group-tab} CentOS Stream 9, 10
|
||||
|
||||
```shell
|
||||
sudo dnf install gcc gcc-c++ unzip git redis curl bzip2-devel openssl-devel libffi-devel wget
|
||||
@@ -282,28 +207,15 @@ mysql_secure_installation
|
||||
For security and permissions, it's highly recommended you create a separate user to install auth under. Do not log in as this account.
|
||||
::::{tabs}
|
||||
|
||||
:::{group-tab} Ubuntu 2004, 2204, 2404
|
||||
:::{group-tab} Ubuntu 2204, 2404
|
||||
|
||||
```shell
|
||||
sudo adduser --disabled-login allianceserver --shell /bin/bash
|
||||
```
|
||||
|
||||
:::
|
||||
:::{group-tab} CentOS 7
|
||||
|
||||
```shell
|
||||
sudo passwd -l allianceserver
|
||||
```
|
||||
|
||||
:::
|
||||
:::{group-tab} CentOS Stream 8
|
||||
|
||||
```shell
|
||||
sudo passwd -l allianceserver
|
||||
```
|
||||
|
||||
:::
|
||||
:::{group-tab} CentOS Stream 9
|
||||
:::{group-tab} CentOS Stream 9, 10
|
||||
|
||||
```shell
|
||||
sudo passwd -l allianceserver
|
||||
@@ -354,7 +266,7 @@ Your python3.x command/version may vary depending on your installed python versi
|
||||
:::
|
||||
|
||||
```shell
|
||||
python3.11 -m venv /home/allianceserver/venv/auth/
|
||||
python3.12 -m venv /home/allianceserver/venv/auth/
|
||||
```
|
||||
|
||||
:::{tip}
|
||||
@@ -497,44 +409,14 @@ exit
|
||||
|
||||
::::{tabs}
|
||||
|
||||
:::{group-tab} Ubuntu 2004, 2204, 2404
|
||||
:::{group-tab} Ubuntu 2204, 2404
|
||||
|
||||
```shell
|
||||
sudo apt-get install supervisor
|
||||
```
|
||||
|
||||
:::
|
||||
:::{group-tab} CentOS 7
|
||||
|
||||
```shell
|
||||
sudo dnf install supervisor
|
||||
```
|
||||
|
||||
```shell
|
||||
sudo systemctl enable supervisord.service
|
||||
```
|
||||
|
||||
```shell
|
||||
sudo systemctl start supervisord.service
|
||||
```
|
||||
|
||||
:::
|
||||
:::{group-tab} CentOS Stream 8
|
||||
|
||||
```shell
|
||||
sudo dnf install supervisor
|
||||
```
|
||||
|
||||
```shell
|
||||
sudo systemctl enable supervisord.service
|
||||
```
|
||||
|
||||
```shell
|
||||
sudo systemctl start supervisord.service
|
||||
```
|
||||
|
||||
:::
|
||||
:::{group-tab} CentOS Stream 9
|
||||
:::{group-tab} CentOS Stream 9, 10
|
||||
|
||||
```shell
|
||||
sudo dnf install supervisor
|
||||
@@ -554,28 +436,14 @@ sudo systemctl start supervisord.service
|
||||
Once installed, it needs a configuration file to know which processes to watch. Your Alliance Auth project comes with a ready-to-use template which will ensure the Celery workers, Celery task scheduler and Gunicorn are all running.
|
||||
::::{tabs}
|
||||
|
||||
:::{group-tab} Ubuntu 2004, 2204, 2404
|
||||
:::{group-tab} Ubuntu 2204, 2404
|
||||
|
||||
```shell
|
||||
ln -s /home/allianceserver/myauth/supervisor.conf /etc/supervisor/conf.d/myauth.conf
|
||||
```
|
||||
|
||||
:::
|
||||
:::{group-tab} CentOS 7
|
||||
|
||||
```shell
|
||||
sudo ln -s /home/allianceserver/myauth/supervisor.conf /etc/supervisord.d/myauth.ini
|
||||
```
|
||||
|
||||
:::
|
||||
:::{group-tab} CentOS Stream 8
|
||||
|
||||
```shell
|
||||
sudo ln -s /home/allianceserver/myauth/supervisor.conf /etc/supervisord.d/myauth.ini
|
||||
```
|
||||
|
||||
:::
|
||||
:::{group-tab} CentOS Stream 9
|
||||
:::{group-tab} CentOS Stream 9, 10
|
||||
|
||||
```shell
|
||||
sudo ln -s /home/allianceserver/myauth/supervisor.conf /etc/supervisord.d/myauth.ini
|
||||
|
||||
@@ -10,28 +10,14 @@ If you're using a small VPS to host services with very limited memory, consider
|
||||
|
||||
::::{tabs}
|
||||
|
||||
:::{group-tab} Ubuntu 2004, 2204, 2404
|
||||
:::{group-tab} Ubuntu 2204, 2404
|
||||
|
||||
```shell
|
||||
apt-get install apache2
|
||||
```
|
||||
|
||||
:::
|
||||
:::{group-tab} CentOS 7
|
||||
|
||||
```shell
|
||||
yum install httpd
|
||||
```
|
||||
|
||||
:::
|
||||
:::{group-tab} CentOS Stream 8
|
||||
|
||||
```shell
|
||||
dnf install httpd
|
||||
```
|
||||
|
||||
:::
|
||||
:::{group-tab} CentOS Stream 9
|
||||
:::{group-tab} CentOS Stream 9, 10
|
||||
|
||||
```shell
|
||||
systemctl enable httpd
|
||||
@@ -41,8 +27,6 @@ systemctl start httpd
|
||||
:::
|
||||
::::
|
||||
|
||||
CentOS 7, Stream 8, Stream 9
|
||||
|
||||
## Configuration
|
||||
|
||||
### Permissions
|
||||
@@ -50,28 +34,14 @@ CentOS 7, Stream 8, Stream 9
|
||||
Apache needs to be able to read the folder containing your auth project's static files.
|
||||
|
||||
::::{tabs}
|
||||
:::{group-tab} Ubuntu 2004, 2204, 2404
|
||||
:::{group-tab} Ubuntu 2204, 2404
|
||||
|
||||
```shell
|
||||
chown -R www-data:www-data /var/www/myauth/static
|
||||
```
|
||||
|
||||
:::
|
||||
:::{group-tab} CentOS 7
|
||||
|
||||
```shell
|
||||
chown -R apache:apache /var/www/myauth/static
|
||||
```
|
||||
|
||||
:::
|
||||
:::{group-tab} CentOS Stream 8
|
||||
|
||||
```shell
|
||||
chown -R apache:apache /var/www/myauth/static
|
||||
```
|
||||
|
||||
:::
|
||||
:::{group-tab} CentOS Stream 9
|
||||
:::{group-tab} CentOS Stream 9, 10
|
||||
|
||||
```shell
|
||||
chown -R apache:apache /var/www/myauth/static
|
||||
@@ -87,7 +57,7 @@ Apache serves sites through defined virtual hosts. These are located in `/etc/ap
|
||||
A virtual host for auth needs only proxy requests to your WSGI server (Gunicorn if you followed the installation guide) and serve static files. Examples can be found below. Create your config in its own file e.g. `myauth.conf`
|
||||
|
||||
::::{tabs}
|
||||
:::{group-tab} Ubuntu 2004, 2204, 2404
|
||||
:::{group-tab} Ubuntu 2204, 2404
|
||||
To proxy and modify headers a few mods need to be enabled.
|
||||
|
||||
```shell
|
||||
@@ -98,13 +68,7 @@ a2enmod headers
|
||||
|
||||
Create a new config file for auth e.g. `/etc/apache2/sites-available/myauth.conf` and fill out the virtual host configuration. To enable your config use `a2ensite myauth.conf` and then reload apache with `service apache2 reload`.
|
||||
:::
|
||||
:::{group-tab} CentOS 7
|
||||
Place your virtual host configuration in the appropriate section within `/etc/httpd/conf.d/httpd.conf` and restart the httpd service with `systemctl restart httpd`.
|
||||
:::
|
||||
:::{group-tab} CentOS Stream 8
|
||||
Place your virtual host configuration in the appropriate section within `/etc/httpd/conf.d/httpd.conf` and restart the httpd service with `systemctl restart httpd`.
|
||||
:::
|
||||
:::{group-tab} CentOS Stream 9
|
||||
:::{group-tab} CentOS Stream 9, 10
|
||||
Place your virtual host configuration in the appropriate section within `/etc/httpd/conf.d/httpd.conf` and restart the httpd service with `systemctl restart httpd`.
|
||||
:::
|
||||
::::
|
||||
|
||||
@@ -42,28 +42,14 @@ You will need to have [Gunicorn](gunicorn.md) or some other WSGI server setup fo
|
||||
## Install
|
||||
|
||||
::::{tabs}
|
||||
:::{group-tab} Ubuntu 2004, 2204, 2404
|
||||
:::{group-tab} Ubuntu 2204, 2404
|
||||
|
||||
```shell
|
||||
sudo apt-get install nginx
|
||||
```
|
||||
|
||||
:::
|
||||
:::{group-tab} CentOS 7
|
||||
|
||||
```shell
|
||||
sudo yum install nginx
|
||||
```
|
||||
|
||||
:::
|
||||
:::{group-tab} CentOS Stream 8
|
||||
|
||||
```shell
|
||||
sudo dnf install nginx
|
||||
```
|
||||
|
||||
:::
|
||||
:::{group-tab} CentOS Stream 9
|
||||
:::{group-tab} CentOS Stream 9, 10
|
||||
|
||||
```shell
|
||||
sudo dnf install nginx
|
||||
|
||||
@@ -14,62 +14,29 @@ To run AA with a newer Python 3 version than your system's default, you need to
|
||||
|
||||
To install other Python versions than those included with your distribution, you need to add a new installation repository. Then you can install the specific Python 3 to your system.
|
||||
|
||||
:::{note}
|
||||
Ubuntu 2204 ships with Python 3.10 already
|
||||
:::
|
||||
|
||||
Centos Stream 8/9:
|
||||
:::{note}
|
||||
A Python 3.9 Package is available for Stream 8 and 9. You _may_ use this instead of building your own package. But our documentation will assume Python3.11, and you may need to substitute as necessary
|
||||
sudo dnf install python39 python39-devel
|
||||
:::
|
||||
|
||||
::::{tabs}
|
||||
:::{group-tab} Ubuntu 2004, 2204, 2404
|
||||
:::{group-tab} Ubuntu 2204, 2404
|
||||
|
||||
```shell
|
||||
sudo add-apt-repository ppa:deadsnakes/ppa
|
||||
sudo apt-get update
|
||||
sudo apt-get install python3.11 python3.11-dev python3.11-venv
|
||||
sudo apt-get install python3.12 python3.12-dev python3.12-venv
|
||||
```
|
||||
|
||||
:::
|
||||
:::{group-tab} CentOS 7
|
||||
:::{group-tab} Ubuntu 2404
|
||||
|
||||
```bash
|
||||
cd ~
|
||||
sudo yum install gcc openssl-devel bzip2-devel libffi-devel wget
|
||||
wget https://www.python.org/ftp/python/3.11.7/Python-3.11.7.tgz
|
||||
tar xvf Python-3.11.7.tgz
|
||||
cd Python-3.11.7/
|
||||
./configure --enable-optimizations --enable-shared
|
||||
sudo make altinstall
|
||||
```shell
|
||||
sudo apt-get update
|
||||
sudo apt-get install python3.12 python3.12-dev python3.12-venv
|
||||
```
|
||||
|
||||
:::
|
||||
:::{group-tab} CentOS Stream 8
|
||||
:::{group-tab} CentOS Stream 9, 10
|
||||
|
||||
```bash
|
||||
cd ~
|
||||
sudo yum install gcc openssl-devel bzip2-devel libffi-devel wget
|
||||
wget https://www.python.org/ftp/python/3.11.7/Python-3.11.7.tgz
|
||||
tar xvf Python-3.11.7.tgz
|
||||
cd Python-3.11.7/
|
||||
./configure --enable-optimizations --enable-shared
|
||||
sudo make altinstall
|
||||
```
|
||||
|
||||
:::
|
||||
:::{group-tab} CentOS Stream 9
|
||||
|
||||
```bash
|
||||
cd ~
|
||||
sudo yum install gcc openssl-devel bzip2-devel libffi-devel wget
|
||||
wget https://www.python.org/ftp/python/3.11.7/Python-3.11.7.tgz
|
||||
tar xvf Python-3.11.7.tgz
|
||||
cd Python-3.11.7/
|
||||
./configure --enable-optimizations --enable-shared
|
||||
sudo make altinstall
|
||||
sudo dnf update
|
||||
sudo dnf install python3.12 python3.12-dev python3.12-venv
|
||||
```
|
||||
|
||||
:::
|
||||
@@ -201,10 +168,10 @@ mv /home/allianceserver/venv/auth /home/allianceserver/venv/auth_old
|
||||
|
||||
## Create your new venv
|
||||
|
||||
Now let's create our new venv with Python 3.11 and activate it:
|
||||
Now let's create our new venv with Python 3.12 and activate it:
|
||||
|
||||
```shell
|
||||
python3.11 -m venv /home/allianceserver/venv/auth
|
||||
python3.12 -m venv /home/allianceserver/venv/auth
|
||||
```
|
||||
|
||||
```shell
|
||||
|
||||
@@ -40,7 +40,7 @@ dynamic = [
|
||||
dependencies = [
|
||||
"bcrypt",
|
||||
"beautifulsoup4",
|
||||
"celery>=5.4,<6",
|
||||
"celery>=5.5,<6",
|
||||
"celery-once>=3.0.1",
|
||||
"django>=5.1,<5.2",
|
||||
"django-bootstrap-form",
|
||||
|
||||
32
runtests.py
32
runtests.py
@@ -1,21 +1,23 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
Django's command-line utility for administrative tasks.
|
||||
Modified to insert Tests as the first argument
|
||||
"""
|
||||
import sys
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
def main() -> None:
|
||||
"""Run tests"""
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError as err:
|
||||
# The above import may fail for some other reason. Ensure that the
|
||||
# issue is really that Django is missing to avoid masking other
|
||||
# exceptions on Python 2.
|
||||
try:
|
||||
import django # noqa: F401
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"Couldn't import Django. Are you sure it's installed and "
|
||||
"available on your PYTHONPATH environment variable? Did you "
|
||||
"forget to activate a virtual environment?"
|
||||
) from err # Provide context for the original error
|
||||
raise # Re-raise original exception with context
|
||||
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
"Couldn't import Django. Are you sure it's installed and "
|
||||
"available on your PYTHONPATH environment variable? Did you "
|
||||
"forget to activate a virtual environment?"
|
||||
) from exc
|
||||
execute_from_command_line(sys.argv.insert(1, "test"))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
25
tox.ini
25
tox.ini
@@ -1,30 +1,33 @@
|
||||
[tox]
|
||||
isolated_build = true
|
||||
skipsdist = true
|
||||
requires =
|
||||
tox>=4.2
|
||||
env_list =
|
||||
docs
|
||||
py{313, 312, 311, 310}-{all, core}
|
||||
no_package = true
|
||||
usedevelop = true
|
||||
envlist = py{310,311,312,313}-{all,core}, docs
|
||||
|
||||
[testenv]
|
||||
setenv =
|
||||
all: DJANGO_SETTINGS_MODULE = tests.settings_all
|
||||
core: DJANGO_SETTINGS_MODULE = tests.settings_core
|
||||
basepython =
|
||||
base_python =
|
||||
py310: python3.10
|
||||
py311: python3.11
|
||||
py312: python3.12
|
||||
py313: python3.13
|
||||
deps=
|
||||
deps =
|
||||
coverage
|
||||
install_command = pip install -e ".[test]" -U {opts} {packages}
|
||||
set_env =
|
||||
all: DJANGO_SETTINGS_MODULE = tests.settings_all
|
||||
core: DJANGO_SETTINGS_MODULE = tests.settings_core
|
||||
commands =
|
||||
all: coverage run runtests.py -v 2 --debug-mode
|
||||
core: coverage run runtests.py allianceauth.authentication.tests.test_app_settings -v 2 --debug-mode
|
||||
all: coverage report -m
|
||||
all: coverage xml
|
||||
install_command = pip install -e ".[test]" -U {opts} {packages}
|
||||
|
||||
[testenv:docs]
|
||||
description = invoke sphinx-build to build the HTML docs
|
||||
basepython = python3.12
|
||||
install_command = pip install -e ".[docs]" -U {opts} {packages}
|
||||
base_python = python3.12
|
||||
commands =
|
||||
sphinx-build -T -E -b html -d "{toxworkdir}/docs_doctree" -D language=en docs "{toxworkdir}/docs_out" {posargs}
|
||||
install_command = pip install -e ".[docs]" -U {opts} {packages}
|
||||
|
||||
Reference in New Issue
Block a user