mirror of
https://gitlab.com/allianceauth/allianceauth.git
synced 2026-02-05 06:36:19 +01:00
Compare commits
58 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f49ca2bccb | ||
|
|
9c71a8d9a3 | ||
|
|
0032f91525 | ||
|
|
7f3492f978 | ||
|
|
2b9110e417 | ||
|
|
98619a0eb8 | ||
|
|
7a4aa05c39 | ||
|
|
ba597cd2ad | ||
|
|
5992ddd48e | ||
|
|
8486b95917 | ||
|
|
564f4fb5f9 | ||
|
|
688c11ff18 | ||
|
|
4a9a2a670c | ||
|
|
5b25637de5 | ||
|
|
bb15de6d1a | ||
|
|
cf2e368978 | ||
|
|
0d67673542 | ||
|
|
d7e58fb557 | ||
|
|
f8cffb64a1 | ||
|
|
dc97fb1cc5 | ||
|
|
392a0c4dcb | ||
|
|
970a111386 | ||
|
|
cafa7cbf17 | ||
|
|
0f0c0441a9 | ||
|
|
a0db8e8e2c | ||
|
|
641356f31d | ||
|
|
191b238a04 | ||
|
|
f6936c5f33 | ||
|
|
e8f8cb8aa3 | ||
|
|
96170a668f | ||
|
|
4a3e807066 | ||
|
|
ab369d9aac | ||
|
|
742864fe6c | ||
|
|
c3df1c4d1f | ||
|
|
63d7fb80e6 | ||
|
|
a7f468efd1 | ||
|
|
9f4ab9540b | ||
|
|
1e133b7c5d | ||
|
|
4aa7530bbc | ||
|
|
2e0ddf2e7a | ||
|
|
e24bc2a05d | ||
|
|
a8c0db3fd7 | ||
|
|
7b77a6cd40 | ||
|
|
b8b8e470f2 | ||
|
|
ad92ea243d | ||
|
|
489a8456f7 | ||
|
|
122e389c38 | ||
|
|
8318add6d5 | ||
|
|
6c3650d9f2 | ||
|
|
37005b1c68 | ||
|
|
0897383e41 | ||
|
|
15db817382 | ||
|
|
eaa1cde01a | ||
|
|
7c1d1074f9 | ||
|
|
0f0f9b6062 | ||
|
|
839232dc98 | ||
|
|
6f2807cba2 | ||
|
|
39a40a8c43 |
@@ -33,13 +33,13 @@ sast:
|
|||||||
dependency_scanning:
|
dependency_scanning:
|
||||||
stage: gitlab
|
stage: gitlab
|
||||||
before_script:
|
before_script:
|
||||||
- apt-get update && apt-get install redis-server libmariadbclient-dev -y
|
- apt-get update && apt-get install redis-server libmariadb-dev -y
|
||||||
- redis-server --daemonize yes
|
- redis-server --daemonize yes
|
||||||
- python -V
|
- python -V
|
||||||
- pip install wheel tox
|
- pip install wheel tox
|
||||||
|
|
||||||
test-3.7-core:
|
test-3.7-core:
|
||||||
image: python:3.7-buster
|
image: python:3.7-bullseye
|
||||||
script:
|
script:
|
||||||
- tox -e py37-core
|
- tox -e py37-core
|
||||||
artifacts:
|
artifacts:
|
||||||
@@ -48,7 +48,7 @@ test-3.7-core:
|
|||||||
cobertura: coverage.xml
|
cobertura: coverage.xml
|
||||||
|
|
||||||
test-3.8-core:
|
test-3.8-core:
|
||||||
image: python:3.8-buster
|
image: python:3.8-bullseye
|
||||||
script:
|
script:
|
||||||
- tox -e py38-core
|
- tox -e py38-core
|
||||||
artifacts:
|
artifacts:
|
||||||
@@ -57,7 +57,7 @@ test-3.8-core:
|
|||||||
cobertura: coverage.xml
|
cobertura: coverage.xml
|
||||||
|
|
||||||
test-3.9-core:
|
test-3.9-core:
|
||||||
image: python:3.9-buster
|
image: python:3.9-bullseye
|
||||||
script:
|
script:
|
||||||
- tox -e py39-core
|
- tox -e py39-core
|
||||||
artifacts:
|
artifacts:
|
||||||
@@ -66,7 +66,7 @@ test-3.9-core:
|
|||||||
cobertura: coverage.xml
|
cobertura: coverage.xml
|
||||||
|
|
||||||
test-3.7-all:
|
test-3.7-all:
|
||||||
image: python:3.7-buster
|
image: python:3.7-bullseye
|
||||||
script:
|
script:
|
||||||
- tox -e py37-all
|
- tox -e py37-all
|
||||||
artifacts:
|
artifacts:
|
||||||
@@ -75,7 +75,7 @@ test-3.7-all:
|
|||||||
cobertura: coverage.xml
|
cobertura: coverage.xml
|
||||||
|
|
||||||
test-3.8-all:
|
test-3.8-all:
|
||||||
image: python:3.8-buster
|
image: python:3.8-bullseye
|
||||||
script:
|
script:
|
||||||
- tox -e py38-all
|
- tox -e py38-all
|
||||||
artifacts:
|
artifacts:
|
||||||
@@ -84,7 +84,7 @@ test-3.8-all:
|
|||||||
cobertura: coverage.xml
|
cobertura: coverage.xml
|
||||||
|
|
||||||
test-3.9-all:
|
test-3.9-all:
|
||||||
image: python:3.9-buster
|
image: python:3.9-bullseye
|
||||||
script:
|
script:
|
||||||
- tox -e py39-all
|
- tox -e py39-all
|
||||||
artifacts:
|
artifacts:
|
||||||
@@ -94,7 +94,7 @@ test-3.9-all:
|
|||||||
|
|
||||||
deploy_production:
|
deploy_production:
|
||||||
stage: deploy
|
stage: deploy
|
||||||
image: python:3.9-buster
|
image: python:3.9-bullseye
|
||||||
|
|
||||||
before_script:
|
before_script:
|
||||||
- pip install twine wheel
|
- pip install twine wheel
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# This will make sure the app is always imported when
|
# This will make sure the app is always imported when
|
||||||
# Django starts so that shared_task will use this app.
|
# Django starts so that shared_task will use this app.
|
||||||
|
|
||||||
__version__ = '2.9.0a4'
|
__version__ = '2.9.0'
|
||||||
__title__ = 'Alliance Auth'
|
__title__ = 'Alliance Auth'
|
||||||
__url__ = 'https://gitlab.com/allianceauth/allianceauth'
|
__url__ = 'https://gitlab.com/allianceauth/allianceauth'
|
||||||
NAME = '%s v%s' % (__title__, __version__)
|
NAME = '%s v%s' % (__title__, __version__)
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ from django.utils.deprecation import MiddlewareMixin
|
|||||||
from .models import AnalyticsTokens, AnalyticsIdentifier
|
from .models import AnalyticsTokens, AnalyticsIdentifier
|
||||||
from .tasks import send_ga_tracking_web_view
|
from .tasks import send_ga_tracking_web_view
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
class AnalyticsMiddleware(MiddlewareMixin):
|
class AnalyticsMiddleware(MiddlewareMixin):
|
||||||
def process_response(self, request, response):
|
def process_response(self, request, response):
|
||||||
@@ -20,7 +22,13 @@ class AnalyticsMiddleware(MiddlewareMixin):
|
|||||||
if token.send_page_views is False:
|
if token.send_page_views is False:
|
||||||
continue
|
continue
|
||||||
# Check Exclusions
|
# Check Exclusions
|
||||||
if request.path in token.ignore_paths.all():
|
ignore = False
|
||||||
|
for ignore_path in token.ignore_paths.values():
|
||||||
|
ignore_path_regex = re.compile(ignore_path["ignore_path"])
|
||||||
|
if re.search(ignore_path_regex, request.path) is not None:
|
||||||
|
ignore = True
|
||||||
|
|
||||||
|
if ignore is True:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
tracking_id = token.token
|
tracking_id = token.token
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ def add_aa_team_token(apps, schema_editor):
|
|||||||
token.type = 'GA-U'
|
token.type = 'GA-U'
|
||||||
token.token = 'UA-186249766-2'
|
token.token = 'UA-186249766-2'
|
||||||
token.send_page_views = True
|
token.send_page_views = True
|
||||||
token.send_celery_views = True
|
token.send_celery_tasks = True
|
||||||
token.send_stats = True
|
token.send_stats = True
|
||||||
token.name = 'AA Team Public Google Analytics (Universal)'
|
token.name = 'AA Team Public Google Analytics (Universal)'
|
||||||
token.save()
|
token.save()
|
||||||
|
|||||||
31
allianceauth/analytics/migrations/0004_auto_20211015_0502.py
Normal file
31
allianceauth/analytics/migrations/0004_auto_20211015_0502.py
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# Generated by Django 3.1.13 on 2021-10-15 05:02
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
def modify_aa_team_token_add_page_ignore_paths(apps, schema_editor):
|
||||||
|
# We can't import the Person model directly as it may be a newer
|
||||||
|
# version than this migration expects. We use the historical version.
|
||||||
|
|
||||||
|
AnalyticsPath = apps.get_model('analytics', 'AnalyticsPath')
|
||||||
|
admin = AnalyticsPath.objects.create(ignore_path=r"^\/admin\/.*")
|
||||||
|
user_notifications_count = AnalyticsPath.objects.create(ignore_path=r"^\/user_notifications_count\/.*")
|
||||||
|
|
||||||
|
Tokens = apps.get_model('analytics', 'AnalyticsTokens')
|
||||||
|
token = Tokens.objects.get(token="UA-186249766-2")
|
||||||
|
token.ignore_paths.add(admin, user_notifications_count)
|
||||||
|
|
||||||
|
|
||||||
|
def undo_modify_aa_team_token_add_page_ignore_paths(apps, schema_editor):
|
||||||
|
# nothing should need to migrate away here?
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('analytics', '0003_Generate_Identifier'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [migrations.RunPython(modify_aa_team_token_add_page_ignore_paths, undo_modify_aa_team_token_add_page_ignore_paths)
|
||||||
|
]
|
||||||
@@ -20,7 +20,7 @@ class AnalyticsIdentifier(models.Model):
|
|||||||
|
|
||||||
|
|
||||||
class AnalyticsPath(models.Model):
|
class AnalyticsPath(models.Model):
|
||||||
ignore_path = models.CharField(max_length=254, default="/example/")
|
ignore_path = models.CharField(max_length=254, default="/example/", help_text="Regex Expression, If matched no Analytics Page View is sent")
|
||||||
|
|
||||||
|
|
||||||
class AnalyticsTokens(models.Model):
|
class AnalyticsTokens(models.Model):
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ def process_failure_signal(
|
|||||||
|
|
||||||
action = sender.__name__
|
action = sender.__name__
|
||||||
|
|
||||||
label = f"{exception.__class__.__name__}: {str(exception)}"
|
label = f"{exception.__class__.__name__}"
|
||||||
|
|
||||||
analytics_event(category=category,
|
analytics_event(category=category,
|
||||||
action=action,
|
action=action,
|
||||||
|
|||||||
@@ -40,9 +40,6 @@ class AuthGroupInlineAdmin(admin.StackedInline):
|
|||||||
kwargs["queryset"] = Group.objects.order_by(Lower('name'))
|
kwargs["queryset"] = Group.objects.order_by(Lower('name'))
|
||||||
return super().formfield_for_manytomany(db_field, request, **kwargs)
|
return super().formfield_for_manytomany(db_field, request, **kwargs)
|
||||||
|
|
||||||
def has_add_permission(self, request, obj=None):
|
|
||||||
return False
|
|
||||||
|
|
||||||
def has_delete_permission(self, request, obj=None):
|
def has_delete_permission(self, request, obj=None):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -139,7 +136,7 @@ class GroupAdmin(admin.ModelAdmin):
|
|||||||
_member_count.admin_order_field = 'member_count'
|
_member_count.admin_order_field = 'member_count'
|
||||||
|
|
||||||
def has_leader(self, obj):
|
def has_leader(self, obj):
|
||||||
return obj.authgroup.group_leaders.exists()
|
return obj.authgroup.group_leaders.exists() or obj.authgroup.group_leader_groups.exists()
|
||||||
|
|
||||||
has_leader.boolean = True
|
has_leader.boolean = True
|
||||||
|
|
||||||
@@ -174,6 +171,13 @@ class GroupAdmin(admin.ModelAdmin):
|
|||||||
kwargs["queryset"] = Permission.objects.select_related("content_type").all()
|
kwargs["queryset"] = Permission.objects.select_related("content_type").all()
|
||||||
return super().formfield_for_manytomany(db_field, request, **kwargs)
|
return super().formfield_for_manytomany(db_field, request, **kwargs)
|
||||||
|
|
||||||
|
def save_formset(self, request, form, formset, change):
|
||||||
|
for inline_form in formset:
|
||||||
|
ag_instance = inline_form.save(commit=False)
|
||||||
|
ag_instance.group = form.instance
|
||||||
|
ag_instance.save()
|
||||||
|
formset.save()
|
||||||
|
|
||||||
|
|
||||||
class Group(BaseGroup):
|
class Group(BaseGroup):
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|||||||
@@ -55,7 +55,6 @@ class RequestLog(models.Model):
|
|||||||
return user.profile.main_character
|
return user.profile.main_character
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class AuthGroup(models.Model):
|
class AuthGroup(models.Model):
|
||||||
"""
|
"""
|
||||||
Extends Django Group model with a one-to-one field
|
Extends Django Group model with a one-to-one field
|
||||||
@@ -106,7 +105,8 @@ class AuthGroup(models.Model):
|
|||||||
help_text="States listed here will have the ability to join this group provided "
|
help_text="States listed here will have the ability to join this group provided "
|
||||||
"they have the proper permissions.")
|
"they have the proper permissions.")
|
||||||
|
|
||||||
description = models.TextField(max_length=512, blank=True, help_text="Short description <i>(max. 512 characters)</i> of the group shown to users.")
|
description = models.TextField(max_length=512, blank=True, help_text="Short description <i>(max. 512 characters)"
|
||||||
|
"</i> of the group shown to users.")
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.group.name
|
return self.group.name
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ class TestGroupAdmin(TestCase):
|
|||||||
cls.group_2 = Group.objects.create(name='Group 2')
|
cls.group_2 = Group.objects.create(name='Group 2')
|
||||||
cls.group_2.authgroup.description = 'Internal Group'
|
cls.group_2.authgroup.description = 'Internal Group'
|
||||||
cls.group_2.authgroup.internal = True
|
cls.group_2.authgroup.internal = True
|
||||||
|
cls.group_2.authgroup.group_leader_groups.add(cls.group_1)
|
||||||
cls.group_2.authgroup.save()
|
cls.group_2.authgroup.save()
|
||||||
|
|
||||||
# group 3 - has leader
|
# group 3 - has leader
|
||||||
@@ -237,10 +238,14 @@ class TestGroupAdmin(TestCase):
|
|||||||
result = self.modeladmin._member_count(obj)
|
result = self.modeladmin._member_count(obj)
|
||||||
self.assertEqual(result, expected)
|
self.assertEqual(result, expected)
|
||||||
|
|
||||||
def test_has_leader(self):
|
def test_has_leader_user(self):
|
||||||
result = self.modeladmin.has_leader(self.group_1)
|
result = self.modeladmin.has_leader(self.group_1)
|
||||||
self.assertTrue(result)
|
self.assertTrue(result)
|
||||||
|
|
||||||
|
def test_has_leader_group(self):
|
||||||
|
result = self.modeladmin.has_leader(self.group_2)
|
||||||
|
self.assertTrue(result)
|
||||||
|
|
||||||
def test_properties_1(self):
|
def test_properties_1(self):
|
||||||
expected = ['Default']
|
expected = ['Default']
|
||||||
result = self.modeladmin._properties(self.group_1)
|
result = self.modeladmin._properties(self.group_1)
|
||||||
|
|||||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
BIN
allianceauth/locale/it_IT/LC_MESSAGES/django.mo
Normal file
BIN
allianceauth/locale/it_IT/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
2136
allianceauth/locale/it_IT/LC_MESSAGES/django.po
Normal file
2136
allianceauth/locale/it_IT/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,35 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from .models import Notification
|
from .models import Notification
|
||||||
|
|
||||||
admin.site.register(Notification)
|
|
||||||
|
@admin.register(Notification)
|
||||||
|
class NotificationAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ("timestamp", "_main", "_state", "title", "level", "viewed")
|
||||||
|
list_select_related = ("user", "user__profile__main_character", "user__profile__state")
|
||||||
|
list_filter = (
|
||||||
|
"level",
|
||||||
|
"timestamp",
|
||||||
|
"user__profile__state",
|
||||||
|
('user__profile__main_character', admin.RelatedOnlyFieldListFilter),
|
||||||
|
)
|
||||||
|
ordering = ("-timestamp", )
|
||||||
|
search_fields = ["user__username", "user__profile__main_character__character_name"]
|
||||||
|
|
||||||
|
def _main(self, obj):
|
||||||
|
try:
|
||||||
|
return obj.user.profile.main_character
|
||||||
|
except AttributeError:
|
||||||
|
return obj.user
|
||||||
|
|
||||||
|
_main.admin_order_field = "user__profile__main_character__character_name"
|
||||||
|
|
||||||
|
def _state(self, obj):
|
||||||
|
return obj.user.profile.state
|
||||||
|
|
||||||
|
_state.admin_order_field = "user__profile__state__name"
|
||||||
|
|
||||||
|
def has_change_permission(self, request, obj=None):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def has_add_permission(self, request) -> bool:
|
||||||
|
return False
|
||||||
|
|||||||
@@ -12,21 +12,20 @@ class NotificationHandler(logging.Handler):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
perm = Permission.objects.get(codename="logging_notifications")
|
perm = Permission.objects.get(codename="logging_notifications")
|
||||||
|
|
||||||
message = record.getMessage()
|
|
||||||
if record.exc_text:
|
|
||||||
message += "\n\n"
|
|
||||||
message = message + record.exc_text
|
|
||||||
|
|
||||||
users = User.objects.filter(
|
|
||||||
Q(groups__permissions=perm) | Q(user_permissions=perm) | Q(is_superuser=True)).distinct()
|
|
||||||
|
|
||||||
for user in users:
|
|
||||||
notify(
|
|
||||||
user,
|
|
||||||
"%s [%s:%s]" % (record.levelname, record.funcName, record.lineno),
|
|
||||||
level=str([item[0] for item in Notification.LEVEL_CHOICES if item[1] == record.levelname][0]),
|
|
||||||
message=message
|
|
||||||
)
|
|
||||||
except Permission.DoesNotExist:
|
except Permission.DoesNotExist:
|
||||||
pass
|
return
|
||||||
|
|
||||||
|
message = record.getMessage()
|
||||||
|
if record.exc_text:
|
||||||
|
message += "\n\n"
|
||||||
|
message = message + record.exc_text
|
||||||
|
|
||||||
|
users = User.objects.filter(
|
||||||
|
Q(groups__permissions=perm) | Q(user_permissions=perm) | Q(is_superuser=True)).distinct()
|
||||||
|
for user in users:
|
||||||
|
notify(
|
||||||
|
user,
|
||||||
|
"%s [%s:%s]" % (record.levelname, record.funcName, record.lineno),
|
||||||
|
level=Notification.Level.from_old_name(record.levelname),
|
||||||
|
message=message
|
||||||
|
)
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ class NotificationQuerySet(models.QuerySet):
|
|||||||
"""Custom QuerySet for Notification model"""
|
"""Custom QuerySet for Notification model"""
|
||||||
|
|
||||||
def update(self, *args, **kwargs):
|
def update(self, *args, **kwargs):
|
||||||
# overriden update to ensure cache is invaidated on very call
|
"""Override update to ensure cache is invalidated on very call."""
|
||||||
super().update(*args, **kwargs)
|
super().update(*args, **kwargs)
|
||||||
user_pks = set(self.select_related("user").values_list('user__pk', flat=True))
|
user_pks = set(self.select_related("user").values_list('user__pk', flat=True))
|
||||||
for user_pk in user_pks:
|
for user_pk in user_pks:
|
||||||
@@ -43,6 +43,8 @@ class NotificationManager(models.Manager):
|
|||||||
if not message:
|
if not message:
|
||||||
message = title
|
message = title
|
||||||
|
|
||||||
|
if level not in self.model.Level:
|
||||||
|
level = self.model.Level.INFO
|
||||||
obj = self.create(user=user, title=title, message=message, level=level)
|
obj = self.create(user=user, title=title, message=message, level=level)
|
||||||
logger.info("Created notification %s", obj)
|
logger.info("Created notification %s", obj)
|
||||||
return obj
|
return obj
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 3.1.12 on 2021-07-01 21:22
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('notifications', '0004_performance_tuning'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='notification',
|
||||||
|
name='level',
|
||||||
|
field=models.CharField(choices=[('danger', 'danger'), ('warning', 'warning'), ('info', 'info'), ('success', 'success')], default='info', max_length=10),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -2,6 +2,7 @@ import logging
|
|||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from .managers import NotificationManager
|
from .managers import NotificationManager
|
||||||
|
|
||||||
@@ -14,16 +15,42 @@ class Notification(models.Model):
|
|||||||
NOTIFICATIONS_MAX_PER_USER_DEFAULT = 50
|
NOTIFICATIONS_MAX_PER_USER_DEFAULT = 50
|
||||||
NOTIFICATIONS_REFRESH_TIME_DEFAULT = 30
|
NOTIFICATIONS_REFRESH_TIME_DEFAULT = 30
|
||||||
|
|
||||||
LEVEL_CHOICES = (
|
class Level(models.TextChoices):
|
||||||
('danger', 'CRITICAL'),
|
"""A notification level."""
|
||||||
('danger', 'ERROR'),
|
|
||||||
('warning', 'WARN'),
|
DANGER = 'danger', _('danger') #:
|
||||||
('info', 'INFO'),
|
WARNING = 'warning', _('warning') #:
|
||||||
('success', 'DEBUG'),
|
INFO = 'info', _('info') #:
|
||||||
)
|
SUCCESS = 'success', _('success') #:
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_old_name(cls, name: str) -> object:
|
||||||
|
"""Map old name to enum.
|
||||||
|
|
||||||
|
Raises ValueError for invalid names.
|
||||||
|
"""
|
||||||
|
name_map = {
|
||||||
|
"CRITICAL": cls.DANGER,
|
||||||
|
"ERROR": cls.DANGER,
|
||||||
|
"WARN": cls.WARNING,
|
||||||
|
"INFO": cls.INFO,
|
||||||
|
"DEBUG": cls.SUCCESS,
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
return name_map[name]
|
||||||
|
except KeyError:
|
||||||
|
raise ValueError(f"Unknown name: {name}") from None
|
||||||
|
|
||||||
|
# LEVEL_CHOICES = (
|
||||||
|
# ('danger', 'CRITICAL'),
|
||||||
|
# ('danger', 'ERROR'),
|
||||||
|
# ('warning', 'WARN'),
|
||||||
|
# ('info', 'INFO'),
|
||||||
|
# ('success', 'DEBUG'),
|
||||||
|
# )
|
||||||
|
|
||||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||||
level = models.CharField(choices=LEVEL_CHOICES, max_length=10)
|
level = models.CharField(choices=Level.choices, max_length=10, default=Level.INFO)
|
||||||
title = models.CharField(max_length=254)
|
title = models.CharField(max_length=254)
|
||||||
message = models.TextField()
|
message = models.TextField()
|
||||||
timestamp = models.DateTimeField(auto_now_add=True, db_index=True)
|
timestamp = models.DateTimeField(auto_now_add=True, db_index=True)
|
||||||
@@ -45,22 +72,15 @@ class Notification(models.Model):
|
|||||||
Notification.objects.invalidate_user_notification_cache(self.user.pk)
|
Notification.objects.invalidate_user_notification_cache(self.user.pk)
|
||||||
|
|
||||||
def mark_viewed(self) -> None:
|
def mark_viewed(self) -> None:
|
||||||
"""mark notification as viewed"""
|
"""Mark notification as viewed."""
|
||||||
logger.info("Marking notification as viewed: %s" % self)
|
logger.info("Marking notification as viewed: %s" % self)
|
||||||
self.viewed = True
|
self.viewed = True
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
def set_level(self, level_name: str) -> None:
|
def set_level(self, level_name: str) -> None:
|
||||||
"""set notification level according to level name, e.g. 'CRITICAL'
|
"""Set notification level according to old level name, e.g. 'CRITICAL'.
|
||||||
|
|
||||||
raised exception on invalid level names
|
Raises ValueError on invalid level names.
|
||||||
"""
|
"""
|
||||||
try:
|
self.level = self.Level.from_old_name(level_name)
|
||||||
new_level = [
|
|
||||||
item[0] for item in self.LEVEL_CHOICES if item[1] == level_name
|
|
||||||
][0]
|
|
||||||
except IndexError:
|
|
||||||
raise ValueError('Invalid level name: %s' % level_name)
|
|
||||||
|
|
||||||
self.level = new_level
|
|
||||||
self.save()
|
self.save()
|
||||||
|
|||||||
69
allianceauth/notifications/tests/test_handlers.py
Normal file
69
allianceauth/notifications/tests/test_handlers.py
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
from logging import LogRecord, DEBUG
|
||||||
|
|
||||||
|
from django.contrib.auth.models import Permission, Group, User
|
||||||
|
from django.test import TestCase
|
||||||
|
from allianceauth.tests.auth_utils import AuthUtils
|
||||||
|
|
||||||
|
from ..handlers import NotificationHandler
|
||||||
|
from ..models import Notification
|
||||||
|
|
||||||
|
MODULE_PATH = 'allianceauth.notifications.handlers'
|
||||||
|
|
||||||
|
|
||||||
|
class TestHandler(TestCase):
|
||||||
|
def test_do_nothing_if_permission_does_not_exist(self):
|
||||||
|
# given
|
||||||
|
Permission.objects.get(codename="logging_notifications").delete()
|
||||||
|
handler = NotificationHandler()
|
||||||
|
record = LogRecord(
|
||||||
|
name="name",
|
||||||
|
level=DEBUG,
|
||||||
|
pathname="pathname",
|
||||||
|
lineno=42,
|
||||||
|
msg="msg",
|
||||||
|
args=[],
|
||||||
|
exc_info=None,
|
||||||
|
func="func"
|
||||||
|
)
|
||||||
|
# when
|
||||||
|
handler.emit(record)
|
||||||
|
# then
|
||||||
|
self.assertEqual(Notification.objects.count(), 0)
|
||||||
|
|
||||||
|
def test_should_emit_message_to_users_with_permission_only(self):
|
||||||
|
# given
|
||||||
|
AuthUtils.create_user('Lex Luthor')
|
||||||
|
user_permission = AuthUtils.create_user('Bruce Wayne')
|
||||||
|
user_permission = AuthUtils.add_permission_to_user_by_name(
|
||||||
|
"auth.logging_notifications", user_permission
|
||||||
|
)
|
||||||
|
group = Group.objects.create(name="Dummy Group")
|
||||||
|
perm = Permission.objects.get(codename="logging_notifications")
|
||||||
|
group.permissions.add(perm)
|
||||||
|
user_group = AuthUtils.create_user('Peter Parker')
|
||||||
|
user_group.groups.add(group)
|
||||||
|
user_superuser = User.objects.create_superuser("Clark Kent")
|
||||||
|
handler = NotificationHandler()
|
||||||
|
record = LogRecord(
|
||||||
|
name="name",
|
||||||
|
level=DEBUG,
|
||||||
|
pathname="pathname",
|
||||||
|
lineno=42,
|
||||||
|
msg="msg",
|
||||||
|
args=[],
|
||||||
|
exc_info=None,
|
||||||
|
func="func"
|
||||||
|
)
|
||||||
|
# when
|
||||||
|
handler.emit(record)
|
||||||
|
# then
|
||||||
|
self.assertEqual(Notification.objects.count(), 3)
|
||||||
|
users = set(Notification.objects.values_list("user__pk", flat=True))
|
||||||
|
self.assertSetEqual(
|
||||||
|
users, {user_permission.pk, user_group.pk, user_superuser.pk}
|
||||||
|
)
|
||||||
|
notif = Notification.objects.first()
|
||||||
|
self.assertEqual(notif.user, user_permission)
|
||||||
|
self.assertEqual(notif.title, "DEBUG [func:42]")
|
||||||
|
self.assertEqual(notif.level, "success")
|
||||||
|
self.assertEqual(notif.message, "msg")
|
||||||
@@ -65,6 +65,35 @@ class TestUserNotify(TestCase):
|
|||||||
self.assertEqual(obj.title, title)
|
self.assertEqual(obj.title, title)
|
||||||
self.assertEqual(obj.message, title)
|
self.assertEqual(obj.message, title)
|
||||||
|
|
||||||
|
def test_should_use_default_level_when_not_specified(self):
|
||||||
|
# given
|
||||||
|
title = 'dummy_title'
|
||||||
|
message = 'dummy message'
|
||||||
|
# when
|
||||||
|
Notification.objects.notify_user(self.user, title, message)
|
||||||
|
# then
|
||||||
|
self.assertEqual(Notification.objects.filter(user=self.user).count(), 1)
|
||||||
|
obj = Notification.objects.first()
|
||||||
|
self.assertEqual(obj.user, self.user)
|
||||||
|
self.assertEqual(obj.title, title)
|
||||||
|
self.assertEqual(obj.message, message)
|
||||||
|
self.assertEqual(obj.level, Notification.Level.INFO)
|
||||||
|
|
||||||
|
def test_should_use_default_level_when_invalid_level_given(self):
|
||||||
|
# given
|
||||||
|
title = 'dummy_title'
|
||||||
|
message = 'dummy message'
|
||||||
|
level = "invalid"
|
||||||
|
# when
|
||||||
|
Notification.objects.notify_user(self.user, title, message, level)
|
||||||
|
# then
|
||||||
|
self.assertEqual(Notification.objects.filter(user=self.user).count(), 1)
|
||||||
|
obj = Notification.objects.first()
|
||||||
|
self.assertEqual(obj.user, self.user)
|
||||||
|
self.assertEqual(obj.title, title)
|
||||||
|
self.assertEqual(obj.message, message)
|
||||||
|
self.assertEqual(obj.level, Notification.Level.INFO)
|
||||||
|
|
||||||
@override_settings(NOTIFICATIONS_MAX_PER_USER=3)
|
@override_settings(NOTIFICATIONS_MAX_PER_USER=3)
|
||||||
def test_remove_when_too_many_notifications(self):
|
def test_remove_when_too_many_notifications(self):
|
||||||
Notification.objects.notify_user(self.user, 'dummy')
|
Notification.objects.notify_user(self.user, 'dummy')
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from django.contrib import messages
|
|||||||
from celery.schedules import crontab
|
from celery.schedules import crontab
|
||||||
|
|
||||||
INSTALLED_APPS = [
|
INSTALLED_APPS = [
|
||||||
|
'allianceauth', # needs to be on top of this list to support favicons in Django admin (see https://gitlab.com/allianceauth/allianceauth/-/issues/1301)
|
||||||
'django.contrib.admin',
|
'django.contrib.admin',
|
||||||
'django.contrib.auth',
|
'django.contrib.auth',
|
||||||
'django.contrib.contenttypes',
|
'django.contrib.contenttypes',
|
||||||
@@ -23,7 +24,6 @@ INSTALLED_APPS = [
|
|||||||
'bootstrapform',
|
'bootstrapform',
|
||||||
'sortedm2m',
|
'sortedm2m',
|
||||||
'esi',
|
'esi',
|
||||||
'allianceauth',
|
|
||||||
'allianceauth.authentication',
|
'allianceauth.authentication',
|
||||||
'allianceauth.services',
|
'allianceauth.services',
|
||||||
'allianceauth.eveonline',
|
'allianceauth.eveonline',
|
||||||
@@ -93,6 +93,7 @@ LANGUAGES = (
|
|||||||
('ko', ugettext('Korean')),
|
('ko', ugettext('Korean')),
|
||||||
('fr', ugettext('French')),
|
('fr', ugettext('French')),
|
||||||
('ja', ugettext('Japanese')),
|
('ja', ugettext('Japanese')),
|
||||||
|
('it', ugettext('Italian')),
|
||||||
)
|
)
|
||||||
|
|
||||||
TEMPLATES = [
|
TEMPLATES = [
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from django.conf.urls import include, url
|
from django.conf.urls import include, url
|
||||||
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from django.template.loader import render_to_string
|
from django.template.loader import render_to_string
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|||||||
@@ -30,5 +30,9 @@ class DiscordUserAdmin(ServicesUserAdmin):
|
|||||||
else:
|
else:
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
|
def delete_queryset(self, request, queryset):
|
||||||
|
for user in queryset:
|
||||||
|
user.delete_user()
|
||||||
|
|
||||||
_username.short_description = 'Discord Username'
|
_username.short_description = 'Discord Username'
|
||||||
_username.admin_order_field = 'username'
|
_username.admin_order_field = 'username'
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ class SrpFleetMainForm(forms.Form):
|
|||||||
class SrpFleetUserRequestForm(forms.Form):
|
class SrpFleetUserRequestForm(forms.Form):
|
||||||
additional_info = forms.CharField(required=False, max_length=25, label=_("Additional Info"))
|
additional_info = forms.CharField(required=False, max_length=25, label=_("Additional Info"))
|
||||||
killboard_link = forms.CharField(
|
killboard_link = forms.CharField(
|
||||||
label=_("zKillboard Link"),
|
label=_("Killboard Link (zkillboard.com or kb.evetools.org)"),
|
||||||
max_length=255,
|
max_length=255,
|
||||||
required=True
|
required=True
|
||||||
|
|
||||||
@@ -21,13 +21,31 @@ class SrpFleetUserRequestForm(forms.Form):
|
|||||||
|
|
||||||
def clean_killboard_link(self):
|
def clean_killboard_link(self):
|
||||||
data = self.cleaned_data['killboard_link']
|
data = self.cleaned_data['killboard_link']
|
||||||
if "zkillboard.com" not in data:
|
|
||||||
raise forms.ValidationError(_("Invalid Link. Please use zKillboard.com"))
|
|
||||||
|
|
||||||
if not re.match(r"http[s]?://zkillboard\.com/kill/\d+\/", data):
|
# Check if it's a link from one of the accepted kill boards
|
||||||
|
if not any(
|
||||||
|
re.match(regex, data)
|
||||||
|
for regex in [
|
||||||
|
r"^http[s]?:\/\/zkillboard\.com\/",
|
||||||
|
r"^http[s]?:\/\/kb\.evetools\.org\/",
|
||||||
|
]
|
||||||
|
):
|
||||||
|
raise forms.ValidationError(
|
||||||
|
_("Invalid Link. Please use zkillboard.com or kb.evetools.org")
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if it's an actual kill mail
|
||||||
|
if not any(
|
||||||
|
re.match(regex, data)
|
||||||
|
for regex in [
|
||||||
|
r"^http[s]?:\/\/zkillboard\.com\/kill\/\d+\/",
|
||||||
|
r"^http[s]?:\/\/kb\.evetools\.org\/kill\/\d+",
|
||||||
|
]
|
||||||
|
):
|
||||||
raise forms.ValidationError(
|
raise forms.ValidationError(
|
||||||
_("Invalid Link. Please post a direct link to a killmail.")
|
_("Invalid Link. Please post a direct link to a killmail.")
|
||||||
)
|
)
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -58,6 +58,6 @@ class SRPManager:
|
|||||||
"""returns the number of open SRP requests for given user
|
"""returns the number of open SRP requests for given user
|
||||||
or None if user has no permission"""
|
or None if user has no permission"""
|
||||||
if user.has_perm("auth.srp_management"):
|
if user.has_perm("auth.srp_management"):
|
||||||
return SrpUserRequest.objects.filter(srp_status="pending").count()
|
return SrpUserRequest.objects.filter(srp_status="Pending").count()
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -170,9 +170,13 @@ def srp_request_view(request, fleet_srp):
|
|||||||
logger.debug("Request type POST contains form valid: %s" % form.is_valid())
|
logger.debug("Request type POST contains form valid: %s" % form.is_valid())
|
||||||
|
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
if SrpUserRequest.objects.filter(killboard_link=form.cleaned_data['killboard_link']).exists():
|
request_killboard_link = form.cleaned_data['killboard_link']
|
||||||
|
killmail_id = SRPManager.get_kill_id(killboard_link=request_killboard_link)
|
||||||
|
|
||||||
|
# check if the killmail_id is already present
|
||||||
|
if SrpUserRequest.objects.filter(killboard_link__icontains="/kill/" + killmail_id).exists():
|
||||||
messages.error(request,
|
messages.error(request,
|
||||||
_("This Killboard link has already been posted."))
|
_("This kill mail has already been posted."))
|
||||||
return redirect("srp:management")
|
return redirect("srp:management")
|
||||||
|
|
||||||
character = request.user.profile.main_character
|
character = request.user.profile.main_character
|
||||||
@@ -180,7 +184,7 @@ def srp_request_view(request, fleet_srp):
|
|||||||
post_time = timezone.now()
|
post_time = timezone.now()
|
||||||
|
|
||||||
srp_request = SrpUserRequest()
|
srp_request = SrpUserRequest()
|
||||||
srp_request.killboard_link = form.cleaned_data['killboard_link']
|
srp_request.killboard_link = request_killboard_link
|
||||||
srp_request.additional_info = form.cleaned_data['additional_info']
|
srp_request.additional_info = form.cleaned_data['additional_info']
|
||||||
srp_request.character = character
|
srp_request.character = character
|
||||||
srp_request.srp_fleet_main = srp_fleet_main
|
srp_request.srp_fleet_main = srp_fleet_main
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.notification-bell-color {
|
.notification-bell-color {
|
||||||
color: #A88F1E;
|
color: #a88f1e;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-brand {
|
.navbar-brand {
|
||||||
@@ -20,12 +20,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
#wrapper {
|
#wrapper {
|
||||||
width: 100%;
|
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
#site-body-wrapper {
|
#site-body-wrapper {
|
||||||
margin-right:0;
|
margin-right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Horizontal list group */
|
/* Horizontal list group */
|
||||||
@@ -46,7 +46,7 @@ ul.list-group.list-group-horizontal > li.list-group-item {
|
|||||||
@media all {
|
@media all {
|
||||||
/* style nav tabs in dark mode*/
|
/* style nav tabs in dark mode*/
|
||||||
.template-dark-mode .nav-tabs > li.active > a {
|
.template-dark-mode .nav-tabs > li.active > a {
|
||||||
background-color: rgb(70, 69, 69)!important;
|
background-color: rgb(70, 69, 69) !important;
|
||||||
color: rgb(255, 255, 255) !important;
|
color: rgb(255, 255, 255) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,40 +58,47 @@ ul.list-group.list-group-horizontal > li.list-group-item {
|
|||||||
|
|
||||||
/* style group headers within a table */
|
/* style group headers within a table */
|
||||||
.template-light-mode .tr-group {
|
.template-light-mode .tr-group {
|
||||||
font-weight: bold;
|
|
||||||
background-color: #e6e6e6 !important;
|
background-color: #e6e6e6 !important;
|
||||||
}
|
|
||||||
.template-dark-mode .tr-group {
|
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-dark-mode .tr-group {
|
||||||
background-color: rgb(105, 105, 105) !important;
|
background-color: rgb(105, 105, 105) !important;
|
||||||
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* default style for tables */
|
/* default style for tables */
|
||||||
.template-light-mode .table-aa > thead > tr > th{
|
.template-light-mode .table-aa > thead > tr > th {
|
||||||
border-bottom: 1px solid #f2f2f2;
|
border-bottom: 1px solid #f2f2f2;
|
||||||
}
|
}
|
||||||
.template-dark-mode .table-aa > thead > tr > th{
|
|
||||||
|
.template-dark-mode .table-aa > thead > tr > th {
|
||||||
border-bottom: 1px solid rgb(70, 69, 69);
|
border-bottom: 1px solid rgb(70, 69, 69);
|
||||||
}
|
}
|
||||||
.table-aa > thead > tr > th{
|
|
||||||
|
.table-aa > thead > tr > th {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
.template-light-mode .table-aa > tbody > tr > td{
|
|
||||||
|
.template-light-mode .table-aa > tbody > tr > td {
|
||||||
border-bottom: 1px solid #f2f2f2;
|
border-bottom: 1px solid #f2f2f2;
|
||||||
}
|
}
|
||||||
.template-dark-mode .table-aa > tbody > tr > td{
|
|
||||||
|
.template-dark-mode .table-aa > tbody > tr > td {
|
||||||
border-bottom: 1px solid rgb(70, 69, 69);
|
border-bottom: 1px solid rgb(70, 69, 69);
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-aa > tbody > tr > td {
|
.table-aa > tbody > tr > td {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-aa > tbody > tr:last-child {
|
.table-aa > tbody > tr:last-child {
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* highlight active menu items
|
/* highlight active menu items
|
||||||
--------------------------------------------------------------------------------------------------------------------- */
|
------------------------------------------------------------------------------------- */
|
||||||
@media all {
|
@media all {
|
||||||
.template-light-mode .nav-pills > li > a.active {
|
.template-light-mode .nav-pills > li > a.active {
|
||||||
background-color: rgb(236, 240, 241);
|
background-color: rgb(236, 240, 241);
|
||||||
@@ -102,15 +109,99 @@ ul.list-group.list-group-horizontal > li.list-group-item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* user menu
|
||||||
|
------------------------------------------------------------------------------------- */
|
||||||
|
@media all {
|
||||||
|
img {
|
||||||
|
height: auto;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-nav > li.top-user-menu.with-main-character > .dropdown-menu {
|
||||||
|
padding-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-nav > li.top-user-menu.with-main-character a {
|
||||||
|
padding: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-menu > li > a {
|
||||||
|
clear: both;
|
||||||
|
color: rgb(123, 138, 139);
|
||||||
|
display: block;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 1.42857143;
|
||||||
|
padding: 3px 20px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-user-menu {
|
||||||
|
color: rgb(255, 255, 255);
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-menu-bar-language-select form {
|
||||||
|
border-bottom: 1px solid transparent;
|
||||||
|
border-top: 1px solid transparent;
|
||||||
|
-webkit-box-shadow: inset 0 1px 0 rgb(255 255 255), 0 1px 0 rgb(255 255 255);
|
||||||
|
box-shadow: inset 0 1px 0 rgb(255 255 255), 0 1px 0 rgb(255 255 255);
|
||||||
|
margin-bottom: 7.5px;
|
||||||
|
margin-left: 5px;
|
||||||
|
margin-right: 5px;
|
||||||
|
margin-top: 7.5px;
|
||||||
|
padding: 10px 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media all and (max-width: 768px) {
|
||||||
|
.navbar-nav .open .dropdown-menu .dropdown-header, .navbar-nav .open .dropdown-menu > li > a {
|
||||||
|
padding: 5px 15px 5px 25px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media all and (min-width: 768px) {
|
||||||
|
.top-user-menu {
|
||||||
|
color: rgb(123, 138, 139);
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-menu-bar-language-select form {
|
||||||
|
border: 0;
|
||||||
|
-webkit-box-shadow: none;
|
||||||
|
box-shadow: none;
|
||||||
|
margin-left: 0;
|
||||||
|
margin-right: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
|
padding-top: 0;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* eve time in navbar
|
||||||
|
------------------------------------------------------------------------------------- */
|
||||||
|
@media all {
|
||||||
|
.nav-item-eve-time .eve-time-wrapper {
|
||||||
|
color: rgb(255, 255, 255);
|
||||||
|
display: block;
|
||||||
|
line-height: 21px;
|
||||||
|
padding: 10px 15px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media all and (min-width: 768px) {
|
||||||
|
.nav-item-eve-time .eve-time-wrapper {
|
||||||
|
padding-bottom: 19.5px;
|
||||||
|
padding-top: 19.5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Small devices (tablets, 768px and up) */
|
/* Small devices (tablets, 768px and up) */
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
|
|
||||||
/* class for vertically aligning columns in a bootstrap row */
|
/* class for vertically aligning columns in a bootstrap row */
|
||||||
.row.vertical-flexbox-row2 {
|
.row.vertical-flexbox-row2 {
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
display: -webkit-flex;
|
display: -webkit-flex;
|
||||||
display: -ms-flexbox;
|
display: -ms-flexbox;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,6 +214,6 @@ ul.list-group.list-group-horizontal > li.list-group-item {
|
|||||||
/* Extra Small devices (Phones, <768px) */
|
/* Extra Small devices (Phones, <768px) */
|
||||||
@media (max-width: 767px) {
|
@media (max-width: 767px) {
|
||||||
.button-wrapper .btn {
|
.button-wrapper .btn {
|
||||||
margin-bottom:5px;
|
margin-bottom: 5px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
61
allianceauth/static/js/eve-time.js
Normal file
61
allianceauth/static/js/eve-time.js
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
$(document).ready(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* check time
|
||||||
|
* @param i
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
let checkTime = function (i) {
|
||||||
|
if (i < 10) {
|
||||||
|
i = '0' + i;
|
||||||
|
}
|
||||||
|
|
||||||
|
return i;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* render a JS clock for Eve Time
|
||||||
|
* @param element
|
||||||
|
* @param utcOffset
|
||||||
|
*/
|
||||||
|
let renderClock = function (element, utcOffset) {
|
||||||
|
let today = new Date();
|
||||||
|
let h = today.getUTCHours();
|
||||||
|
let m = today.getUTCMinutes();
|
||||||
|
let s = today.getUTCSeconds();
|
||||||
|
|
||||||
|
h = h + utcOffset;
|
||||||
|
|
||||||
|
if (h > 24) {
|
||||||
|
h = h - 24;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (h < 0) {
|
||||||
|
h = h + 24;
|
||||||
|
}
|
||||||
|
|
||||||
|
h = checkTime(h);
|
||||||
|
m = checkTime(m);
|
||||||
|
s = checkTime(s);
|
||||||
|
|
||||||
|
// document.getElementById('clock').innerHTML = h + ":" + m + ":" + s;
|
||||||
|
element.html(h + ':' + m + ':' + s);
|
||||||
|
|
||||||
|
setTimeout(function () {
|
||||||
|
renderClock(element, 0);
|
||||||
|
}, 500);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* functions that need to be executed on load
|
||||||
|
*/
|
||||||
|
let init = function () {
|
||||||
|
renderClock($('.eve-time-wrapper .eve-time-clock'), 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* start the show
|
||||||
|
*/
|
||||||
|
init();
|
||||||
|
});
|
||||||
5
allianceauth/templates/admin/base_site.html
Normal file
5
allianceauth/templates/admin/base_site.html
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{% extends "admin/base_site.html" %}
|
||||||
|
|
||||||
|
{% block extrahead %}
|
||||||
|
{% include "allianceauth/icons.html" %}
|
||||||
|
{% endblock %}
|
||||||
@@ -50,6 +50,7 @@
|
|||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<script src="{% static 'js/refresh_notifications.js' %}"></script>
|
<script src="{% static 'js/refresh_notifications.js' %}"></script>
|
||||||
|
<script src="{% static 'js/eve-time.js' %}"></script>
|
||||||
|
|
||||||
{% block extra_javascript %}
|
{% block extra_javascript %}
|
||||||
{% endblock extra_javascript %}
|
{% endblock extra_javascript %}
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
<a href="{% url 'nightmode' %}?next={{ request.path|urlencode }}">
|
|
||||||
{% trans "Night" %}
|
<li>
|
||||||
<i class="fas {% if NIGHT_MODE %}fa-toggle-on{% else %}fa-toggle-off{% endif %}" aria-hidden="true"></i>
|
<a href="{% url 'nightmode' %}?next={{ request.path|urlencode }}">
|
||||||
</a>
|
<i class="fas {% if NIGHT_MODE %}fa-toggle-on{% else %}fa-toggle-off{% endif %}" aria-hidden="true"></i>
|
||||||
|
{% trans "Night Mode" %}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|||||||
29
allianceauth/templates/allianceauth/top-menu-admin.html
Normal file
29
allianceauth/templates/allianceauth/top-menu-admin.html
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
|
||||||
|
{% if user.is_authenticated %}
|
||||||
|
{% if user.is_staff %}
|
||||||
|
<li role="separator" class="divider"></li>
|
||||||
|
<li><a href="{% url 'admin:index' %}" target="_blank" rel="noopener noreferer">
|
||||||
|
<i class="fas fa-user-shield"></i>
|
||||||
|
{% translate "Admin" %}</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if user.is_superuser %}
|
||||||
|
<li role="separator" class="divider"></li>
|
||||||
|
<li>
|
||||||
|
<a href="https://allianceauth.readthedocs.io/" target="_blank" rel="noopener noreferer">
|
||||||
|
<i class="fas fa-question-circle fa-fw"></i>
|
||||||
|
{% translate "AA Documentation" %}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<a href="https://discord.gg/fjnHAmk" target="_blank" rel="noopener noreferer">
|
||||||
|
<i class="fab fa-discord fa-fw"></i>
|
||||||
|
{% translate "AA Support Discord" %}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
{% load i18n %}
|
||||||
|
{% load evelinks %}
|
||||||
|
|
||||||
|
<li class="top-user-menu dropdown{% if request.user.profile.main_character %} with-main-character{% endif %}">
|
||||||
|
<a href="#" class="dropdown-toggle" type="button" id="top-user-menu" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
|
||||||
|
{% if request.user.profile.main_character %}
|
||||||
|
{% with request.user.profile.main_character as main %}
|
||||||
|
<img class="img-rounded ra-avatar" src="{{ main.character_id|character_portrait_url:32 }}" alt="{{ main.character_name }}">
|
||||||
|
<span class="hidden-sm hidden-md hidden-lg">
|
||||||
|
{{ main.character_name }} - {% translate "User Menu" %}
|
||||||
|
</span>
|
||||||
|
{% endwith %}
|
||||||
|
{% else %}
|
||||||
|
{% translate "User Menu" %}
|
||||||
|
{% endif %}
|
||||||
|
<span class="caret"></span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<ul class="dropdown-menu top-user-menu" aria-labelledby="top-user-menu">
|
||||||
|
<!-- user avatar -->
|
||||||
|
{% if request.user.profile.main_character %}
|
||||||
|
<li>
|
||||||
|
{% with request.user.profile.main_character as main %}
|
||||||
|
<p class="text-center">
|
||||||
|
<img class="img-rounded ra-avatar" src="{{ main.character_id|character_portrait_url:256 }}" alt="{{ main.character_name }}">
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p class="text-center">
|
||||||
|
<span style="display: block;">{{ main.character_name }}</span>
|
||||||
|
<span style="display: block;">{{ main.corporation_name }}</span>
|
||||||
|
|
||||||
|
{% if main.alliance_name %}
|
||||||
|
<span style="display: block;">{{ main.alliance_name }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
{% endwith %}
|
||||||
|
</li>
|
||||||
|
<li role="separator" class="divider"></li>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<li class="top-menu-bar-language-select">
|
||||||
|
{% include 'public/lang_select.html' %}
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li role="separator" class="divider"></li>
|
||||||
|
|
||||||
|
<!-- night mode toggle -->
|
||||||
|
{% include 'allianceauth/night-toggle.html' %}
|
||||||
|
|
||||||
|
<!-- admin related menu items -->
|
||||||
|
{% include 'allianceauth/top-menu-admin.html' %}
|
||||||
|
|
||||||
|
<!-- logout / login -->
|
||||||
|
<li role="separator" class="divider"></li>
|
||||||
|
{% if user.is_authenticated %}
|
||||||
|
<li><a href="{% url 'logout' %}">{% translate "Logout" %}</a></li>
|
||||||
|
{% else %}
|
||||||
|
<li><a href="{% url 'authentication:login' %}">{% translate "Login" %}</a></li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
@@ -10,51 +10,24 @@
|
|||||||
<span class="icon-bar"></span>
|
<span class="icon-bar"></span>
|
||||||
<span class="icon-bar"></span>
|
<span class="icon-bar"></span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<a class="navbar-brand">
|
<a class="navbar-brand">
|
||||||
<img src="{% static 'icons/favicon-32x32.png' %}" style="display: inline-block;" height="32" width="32"/>
|
<img src="{% static 'icons/favicon-32x32.png' %}" style="display: inline-block;" height="32" width="32"/>
|
||||||
{{ SITE_NAME }}
|
{{ SITE_NAME }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="collapse navbar-collapse auth-menus-collapse">
|
<div class="collapse navbar-collapse auth-menus-collapse">
|
||||||
<ul class="nav navbar-nav navbar-right navbar-pills">
|
<ul class="nav navbar-nav navbar-right navbar-pills">
|
||||||
<li>
|
<li class="nav-item-eve-time">
|
||||||
{% include 'allianceauth/night-toggle.html' %}
|
<div class="eve-time-wrapper">Eve Time: <span class="eve-time-clock"></span></div>
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li class="{% navactive request 'notifications:' %}" id="menu_item_notifications">
|
||||||
class="{% navactive request 'notifications:' %}" id="menu_item_notifications"
|
|
||||||
>
|
|
||||||
{% include 'allianceauth/notifications_menu_item.html' %}
|
{% include 'allianceauth/notifications_menu_item.html' %}
|
||||||
</li>
|
</li>
|
||||||
{% if user.is_authenticated %}
|
|
||||||
{% if user.is_staff %}
|
{% include 'allianceauth/top-menu-user-dropdown.html' %}
|
||||||
<li><a href="{% url 'admin:index' %}">{% trans "Admin" %}</a></li>
|
|
||||||
{% endif %}
|
|
||||||
<li><a href="{% url 'logout' %}">{% trans "Logout" %}</a></li>
|
|
||||||
{% else %}
|
|
||||||
<li><a href="{% url 'authentication:login' %}">{% trans "Login" %}</a></li>
|
|
||||||
{% endif %}
|
|
||||||
{% if user.is_superuser %}
|
|
||||||
<li>
|
|
||||||
<a class="navbar-brand" style="margin-left: -4px;" href="https://allianceauth.readthedocs.io/" target="_blank">
|
|
||||||
<i class="fas fa-question-circle fa-fw"></i>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
</ul>
|
</ul>
|
||||||
<form id="f-lang-select" class="navbar-form navbar-right" action="{% url 'set_language' %}" method="post">
|
|
||||||
{% csrf_token %}
|
|
||||||
<div class="form-group">
|
|
||||||
<select onchange="this.form.submit()" class="form-control" id="lang-select" name="language">
|
|
||||||
{% get_language_info_list for LANGUAGES as languages %}
|
|
||||||
{% for language in languages %}
|
|
||||||
<option value="{{ language.code }}"{% if language.code == LANGUAGE_CODE %}
|
|
||||||
selected="selected"{% endif %}>
|
|
||||||
{{ language.name_local }} ({{ language.code }})
|
|
||||||
</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ In addition all tools described in this guide are open source or free software.
|
|||||||
The development environment consists of the following components:
|
The development environment consists of the following components:
|
||||||
|
|
||||||
- Visual Studio Code with Remote WSL and Python extension
|
- Visual Studio Code with Remote WSL and Python extension
|
||||||
- WSL with Ubunutu 18.04. LTS
|
- WSL with Ubuntu 18.04. LTS
|
||||||
- Python 3.6 environment on WSL
|
- Python 3.7 environment on WSL
|
||||||
- MySQL server on WSL
|
- MySQL server on WSL
|
||||||
- Redis on WSL
|
- Redis on WSL
|
||||||
- Alliance Auth on WSL
|
- Alliance Auth on WSL
|
||||||
@@ -71,7 +71,7 @@ sudo apt-get install gettext
|
|||||||
|
|
||||||
### Install Python
|
### Install Python
|
||||||
|
|
||||||
For AA we want to develop with Python 3.6, because that provides the maximum compatibility with today's AA installations. This also happens to be the default Python 3 version for Ubuntu 18.04. at the point of this writing.
|
For AA we want to develop with Python 3.7, because that provides the maximum compatibility with today's AA installations.
|
||||||
|
|
||||||
```eval_rst
|
```eval_rst
|
||||||
.. hint::
|
.. hint::
|
||||||
@@ -80,7 +80,7 @@ For AA we want to develop with Python 3.6, because that provides the maximum com
|
|||||||
|
|
||||||
```eval_rst
|
```eval_rst
|
||||||
.. note::
|
.. note::
|
||||||
Should your Ubuntu come with a newer version of Python we recommend to still setup your dev environment with the oldest Python 3 version supported by AA, e.g Python 3.6
|
Should your Ubuntu come with a newer version of Python we recommend to still setup your dev environment with the oldest Python 3 version supported by AA, e.g Python 3.7
|
||||||
You an 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.
|
You an 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.
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -8,18 +8,15 @@ Location: ``allianceauth.notifications``
|
|||||||
|
|
||||||
.. automodule:: allianceauth.notifications.__init__
|
.. automodule:: allianceauth.notifications.__init__
|
||||||
:members: notify
|
:members: notify
|
||||||
:undoc-members:
|
|
||||||
|
|
||||||
models
|
models
|
||||||
===========
|
===========
|
||||||
|
|
||||||
.. autoclass:: allianceauth.notifications.models.Notification
|
.. autoclass:: allianceauth.notifications.models.Notification
|
||||||
:members: LEVEL_CHOICES, mark_viewed, set_level
|
:members: Level, mark_viewed, set_level
|
||||||
:undoc-members:
|
|
||||||
|
|
||||||
managers
|
managers
|
||||||
===========
|
===========
|
||||||
|
|
||||||
.. autoclass:: allianceauth.notifications.managers.NotificationManager
|
.. autoclass:: allianceauth.notifications.managers.NotificationManager
|
||||||
:members: notify_user, user_unread_count
|
:members: notify_user, user_unread_count
|
||||||
:undoc-members:
|
|
||||||
|
|||||||
@@ -18,28 +18,57 @@ This document describes how to install **Alliance Auth** from scratch.
|
|||||||
|
|
||||||
Alliance Auth can be installed on any Unix like operating system. Dependencies are provided below for two of the most popular Linux platforms: Ubuntu and CentOS. To install on your favorite flavour of Linux, identify and install equivalent packages to the ones listed here.
|
Alliance Auth can be installed on any Unix like operating system. Dependencies are provided below for two of the most popular Linux platforms: Ubuntu and CentOS. To install on your favorite flavour of Linux, identify and install equivalent packages to the ones listed here.
|
||||||
|
|
||||||
```eval_rst
|
|
||||||
.. hint::
|
|
||||||
CentOS: A few packages are included in a non-default repository. Add it and update the package lists. ::
|
|
||||||
|
|
||||||
yum -y install https://centos7.iuscommunity.org/ius-release.rpm
|
|
||||||
yum update
|
|
||||||
```
|
|
||||||
|
|
||||||
### Python
|
### Python
|
||||||
|
|
||||||
Alliance Auth requires Python 3.6 or higher. Ensure it is installed on your server before proceeding.
|
Alliance Auth requires Python 3.7 or higher. Ensure it is installed on your server before proceeding.
|
||||||
|
|
||||||
Ubuntu:
|
Ubuntu 1604 1804:
|
||||||
|
|
||||||
```bash
|
```eval_rst
|
||||||
apt-get install python3 python3-dev python3-venv python3-setuptools python3-pip
|
.. note::
|
||||||
|
Ubuntu 2004 ships with Python 3.8, No updates required.
|
||||||
```
|
```
|
||||||
|
|
||||||
CentOS:
|
```bash
|
||||||
|
add-apt-repository ppa:deadsnakes/ppa
|
||||||
|
```
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
yum install python36u python36u-devel python36u-setuptools python36u-pip
|
apt-get update
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
apt-get install python3.7 python3.7-dev python3.7-venv
|
||||||
|
```
|
||||||
|
|
||||||
|
CentOS 7/8:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo yum install gcc openssl-devel bzip2-devel libffi-devel wget
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wget https://www.python.org/ftp/python/3.7.11/Python-3.7.11.tgz
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tar xvf Python-3.7.11.tgz
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd Python-3.7.11/
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./configure --enable-optimizations --enable-shared
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make altinstall
|
||||||
```
|
```
|
||||||
|
|
||||||
### Database
|
### Database
|
||||||
@@ -155,7 +184,7 @@ python3 -m venv /home/allianceserver/venv/auth/
|
|||||||
|
|
||||||
```eval_rst
|
```eval_rst
|
||||||
.. warning::
|
.. warning::
|
||||||
The python3 command may not be available on all installations. Try a specific version such as ``python3.6`` if this is the case.
|
The python3 command may not be available on all installations. Try a specific version such as ``python3.7`` if this is the case.
|
||||||
```
|
```
|
||||||
|
|
||||||
```eval_rst
|
```eval_rst
|
||||||
|
|||||||
@@ -53,15 +53,15 @@ sudo yum install gcc openssl-devel bzip2-devel libffi-devel wget
|
|||||||
```
|
```
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
wget https://www.python.org/ftp/python/3.7.10/Python-3.7.10.tgz
|
wget https://www.python.org/ftp/python/3.7.11/Python-3.7.11.tgz
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
tar xvf Python-3.7.10.tgz
|
tar xvf Python-3.7.11.tgz
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd Python-3.7.10/
|
cd Python-3.7.11/
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -182,7 +182,7 @@ mv /home/allianceserver/venv/auth /home/allianceserver/venv/auth_old
|
|||||||
|
|
||||||
## Create your new venv
|
## Create your new venv
|
||||||
|
|
||||||
Now let's create our new venv with Python 3.6 and activate it:
|
Now let's create our new venv with Python 3.7 and activate it:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3.7 -m venv /home/allianceserver/venv/auth
|
python3.7 -m venv /home/allianceserver/venv/auth
|
||||||
|
|||||||
4
setup.py
4
setup.py
@@ -22,7 +22,7 @@ install_requires = [
|
|||||||
'celery>=4.3.0,<6.0.0,!=4.4.4', # 4.4.4 is missing a dependency
|
'celery>=4.3.0,<6.0.0,!=4.4.4', # 4.4.4 is missing a dependency
|
||||||
'celery_once>=2.0.1',
|
'celery_once>=2.0.1',
|
||||||
|
|
||||||
'django>=3.1.1,<4.0.0',
|
'django>=3.2.7,<4.0.0',
|
||||||
'django-bootstrap-form',
|
'django-bootstrap-form',
|
||||||
'django-registration>=3.1',
|
'django-registration>=3.1',
|
||||||
'django-sortedm2m',
|
'django-sortedm2m',
|
||||||
@@ -33,7 +33,7 @@ install_requires = [
|
|||||||
'sleekxmpp',
|
'sleekxmpp',
|
||||||
'pydiscourse',
|
'pydiscourse',
|
||||||
|
|
||||||
'django-esi>=2.0.4,<3.0'
|
'django-esi>=3.0.0,<4.0.0'
|
||||||
]
|
]
|
||||||
|
|
||||||
testing_extras = [
|
testing_extras = [
|
||||||
|
|||||||
Reference in New Issue
Block a user