Compare commits

..

47 Commits

Author SHA1 Message Date
Ariel Rin
7412675bfb Version Bump v2.8.8 2021-10-17 09:34:41 +00:00
Joel Falknau
ad953efe77 Require Django-ESI 3.x
(cherry picked from commit 98619a0eb8)
2021-10-17 09:31:54 +00:00
Ariel Rin
5b26757662 Merge branch 'features/kick-from-discord-admin' into 'master'
Add override to delete the user from discord itself

See merge request allianceauth/allianceauth!1337
2021-10-17 07:34:22 +00:00
Crashtec
8486b95917 Add override to delete the user from discord itself 2021-10-10 15:19:46 -04:00
Ariel Rin
bb15de6d1a Version Bump v2.8.7 2021-09-14 04:32:37 +00:00
Ariel Rin
0f0c0441a9 Merge branch 'fix_logging_notifications' into 'master'
Fix logging notifications

See merge request allianceauth/allianceauth!1332
2021-08-20 04:03:48 +00:00
Erik Kalkoken
a0db8e8e2c Fix logging notifications 2021-08-20 04:03:48 +00:00
Ariel Rin
a7f468efd1 Merge branch 'fix_group_creation' into 'master'
Fix Group Creation

Closes #1161

See merge request allianceauth/allianceauth!1327
2021-08-11 06:07:24 +00:00
Ariel Rin
9f4ab9540b Merge branch 'improve_notifications' into 'master'
Improve notifications

See merge request allianceauth/allianceauth!1324
2021-08-11 06:06:24 +00:00
Erik Kalkoken
1e133b7c5d Improve notifications 2021-08-11 06:06:23 +00:00
Ariel Rin
4aa7530bbc Merge branch 'fix_hasgroupleader' into 'master'
Fix `Has Leader` column for groups that have group leader groups.

See merge request allianceauth/allianceauth!1328
2021-08-11 06:06:13 +00:00
colcrunch
2e0ddf2e7a has_leader should return true when a group has a group_leader_group 2021-07-13 18:00:21 -04:00
colcrunch
e24bc2a05d Revert "Update tests."
This reverts commit 7c1d1074f9.
2021-07-13 11:35:19 -04:00
colcrunch
a8c0db3fd7 Revert "Update autogroups."
This reverts commit eaa1cde01a.
2021-07-13 11:35:06 -04:00
colcrunch
7b77a6cd40 Revert "Add authutil for creating groups with authgroups."
This reverts commit 15db817382.
2021-07-13 11:34:55 -04:00
colcrunch
b8b8e470f2 Revert "Update tests to use the create_group util."
This reverts commit 0897383e41.
2021-07-13 11:34:46 -04:00
colcrunch
ad92ea243d Revert "Fix missed test."
This reverts commit 37005b1c68.
2021-07-13 11:34:35 -04:00
colcrunch
489a8456f7 Revert "More test fixes."
This reverts commit 6c3650d9f2.
2021-07-13 11:34:19 -04:00
colcrunch
122e389c38 Add signals back. 2021-07-13 11:33:51 -04:00
colcrunch
8318add6d5 Update test_admin.py 2021-07-13 10:18:39 -04:00
colcrunch
6c3650d9f2 More test fixes. 2021-07-13 09:28:31 -04:00
colcrunch
37005b1c68 Fix missed test. 2021-07-13 09:18:53 -04:00
colcrunch
0897383e41 Update tests to use the create_group util. 2021-07-13 09:13:47 -04:00
colcrunch
15db817382 Add authutil for creating groups with authgroups. 2021-07-13 09:13:17 -04:00
colcrunch
eaa1cde01a Update autogroups. 2021-07-13 08:41:36 -04:00
colcrunch
7c1d1074f9 Update tests. 2021-07-13 08:41:25 -04:00
colcrunch
0f0f9b6062 Fix group creation ignoring AuthGroup settings. (Fixes #1161) 2021-07-13 08:09:40 -04:00
Ariel Rin
6f2807cba2 Version Bump v2.8.6 2021-07-02 16:47:06 +00:00
Ariel Rin
39a40a8c43 Limit Django-Celery-Beat to 2.2.0 for Celery 4.x 2021-07-02 16:42:07 +00:00
Ariel Rin
5f98b5350e Version Bump v2.8.5 2021-06-29 03:27:30 +00:00
Ariel Rin
9de4d557e3 Merge branch 'discord-existing' into 'master'
Update member when they are already in Discord server.

See merge request allianceauth/allianceauth!1322
2021-06-29 03:10:23 +00:00
Aaron Kable
1d5f2634f1 Update member when they are already in Discord server. 2021-06-29 03:10:23 +00:00
Ariel Rin
710d26d36d Merge branch 'message-icons-fix' into 'master'
[FIX] icons in alert messages

See merge request allianceauth/allianceauth!1323
2021-06-29 03:10:01 +00:00
Peter Pfeufer
47793e6198 [FIX] icons in alert messages
This way they don't interfere with possible HTML in the message itself.
2021-06-28 21:20:59 +02:00
Ariel Rin
5fcb56a087 Merge branch 'notification_with_icons' into 'master'
Improve messages

See merge request allianceauth/allianceauth!1320
2021-06-21 10:39:56 +00:00
Erik Kalkoken
808080d5ee Improve messages 2021-06-21 10:39:56 +00:00
Ariel Rin
e6037f1680 Typo (Allince<Alliance) Credit @Thalimet 2021-06-21 10:23:40 +00:00
Ariel Rin
5c3ded6b07 Merge branch 'admin_performance_tuning' into 'master'
Admin performance tuning for group and state

See merge request allianceauth/allianceauth!1318
2021-06-01 11:40:32 +00:00
Erik Kalkoken
0c14e35d15 Admin performance tuning for group and state 2021-06-01 11:40:32 +00:00
Ariel Rin
c13be5d39a Merge branch 'ts3_update_button' into 'master'
Add button to update TS3 groups to admin page

See merge request allianceauth/allianceauth!1313
2021-06-01 11:38:24 +00:00
Erik Kalkoken
e4b515c1d5 Add button to update TS3 groups to admin page 2021-06-01 11:38:24 +00:00
Ariel Rin
65e2c87e8f Merge branch 'improve_eveonline_manager' into 'master'
Improve performance in EveCharacter manager

See merge request allianceauth/allianceauth!1314
2021-06-01 11:36:29 +00:00
Ariel Rin
68ce25854a Merge branch 'quick-syntax-fix-in-docu' into 'master'
quick fix in documentation

See merge request allianceauth/allianceauth!1317
2021-06-01 11:35:20 +00:00
Peter Pfeufer
3df6643513 this is a shell command ... 2021-05-17 17:41:28 +02:00
Ariel Rin
6ea0ebc9f9 Merge branch 'db-install-docs-update' into 'master'
Added a quick hint that it's ok to leave the SQL shell, and some commas

See merge request allianceauth/allianceauth!1315
2021-05-16 02:38:43 +00:00
Peter Pfeufer
26236f5886 Added a quick hint that it's ok to leave the SQL shell, and some commas 2021-05-15 15:27:50 +02:00
ErikKalkoken
1420c71ec5 Improve get_character_by_id() 2021-05-15 13:58:23 +02:00
49 changed files with 783 additions and 457 deletions

View File

@@ -25,6 +25,15 @@ dependency_scanning:
- python -V
- pip install wheel tox
test-3.6-core:
image: python:3.6-buster
script:
- tox -e py36-core
artifacts:
when: always
reports:
cobertura: coverage.xml
test-3.7-core:
image: python:3.7-buster
script:
@@ -43,10 +52,10 @@ test-3.8-core:
reports:
cobertura: coverage.xml
test-3.9-core:
image: python:3.9-buster
test-3.6-all:
image: python:3.6-buster
script:
- tox -e py39-core
- tox -e py36-all
artifacts:
when: always
reports:
@@ -70,18 +79,9 @@ test-3.8-all:
reports:
cobertura: coverage.xml
test-3.9-all:
image: python:3.9-buster
script:
- tox -e py39-all
artifacts:
when: always
reports:
cobertura: coverage.xml
deploy_production:
stage: deploy
image: python:3.9-buster
image: python:3.8-buster
before_script:
- pip install twine wheel

View File

@@ -1,7 +1,7 @@
# This will make sure the app is always imported when
# Django starts so that shared_task will use this app.
__version__ = '2.9.0a2'
__version__ = '2.8.8'
__title__ = 'Alliance Auth'
__url__ = 'https://gitlab.com/allianceauth/allianceauth'
NAME = '%s v%s' % (__title__, __version__)

View File

@@ -399,7 +399,7 @@ class UserAdmin(BaseUserAdmin):
def formfield_for_manytomany(self, db_field, request, **kwargs):
"""overriding this formfield to have sorted lists in the form"""
if db_field.name == "groups":
kwargs["queryset"] = Group.objects.all().order_by(Lower('name'))
kwargs["queryset"] = Group.objects.all().order_by(Lower('name'))
return super().formfield_for_manytomany(db_field, request, **kwargs)
@@ -438,7 +438,7 @@ class StateAdmin(admin.ModelAdmin):
]
def formfield_for_manytomany(self, db_field, request, **kwargs):
"""overriding this formfield to have sorted lists in the form"""
"""overriding this formfield to have sorted lists in the form"""
if db_field.name == "member_characters":
kwargs["queryset"] = EveCharacter.objects.all()\
.order_by(Lower('character_name'))
@@ -448,8 +448,10 @@ class StateAdmin(admin.ModelAdmin):
elif db_field.name == "member_alliances":
kwargs["queryset"] = EveAllianceInfo.objects.all()\
.order_by(Lower('alliance_name'))
elif db_field.name == "permissions":
kwargs["queryset"] = Permission.objects.select_related("content_type").all()
return super().formfield_for_manytomany(db_field, request, **kwargs)
def has_delete_permission(self, request, obj=None):
if obj == get_guest_state():
return False

View File

@@ -32,10 +32,12 @@ class EveCharacterManager(models.Manager):
def update_character(self, character_id):
return self.get(character_id=character_id).update_character()
def get_character_by_id(self, char_id):
if self.filter(character_id=char_id).exists():
return self.get(character_id=char_id)
return None
def get_character_by_id(self, character_id: int):
"""Return character by character ID or None if not found."""
try:
return self.get(character_id=character_id)
except self.model.DoesNotExist:
return None
class EveAllianceProviderManager:

View File

@@ -42,7 +42,6 @@
</div>
{% endblock content %}
{% block extra_script %}
$(document).ready(function () {
$("[rel=tooltip]").tooltip();
});
$(document).ready(function(){
$("[rel=tooltip]").tooltip();
{% endblock extra_script %}

View File

@@ -44,7 +44,6 @@
</div>
{% endblock content %}
{% block extra_script %}
$(document).ready(function () {
$("[rel=tooltip]").tooltip();
});
$(document).ready(function(){
$("[rel=tooltip]").tooltip();
{% endblock extra_script %}

View File

@@ -1,4 +1,5 @@
from django.apps import apps
from django.contrib.auth.models import Permission
from django.contrib import admin
from django.contrib.auth.models import Group as BaseGroup, User
from django.db.models import Count
@@ -39,9 +40,6 @@ class AuthGroupInlineAdmin(admin.StackedInline):
kwargs["queryset"] = Group.objects.order_by(Lower('name'))
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):
return False
@@ -96,8 +94,7 @@ class HasLeaderFilter(admin.SimpleListFilter):
return queryset
class GroupAdmin(admin.ModelAdmin):
list_select_related = ('authgroup',)
class GroupAdmin(admin.ModelAdmin):
ordering = ('name',)
list_display = (
'name',
@@ -122,7 +119,7 @@ class GroupAdmin(admin.ModelAdmin):
qs = super().get_queryset(request)
if _has_auto_groups:
qs = qs.prefetch_related('managedalliancegroup_set', 'managedcorpgroup_set')
qs = qs.prefetch_related('authgroup__group_leaders')
qs = qs.prefetch_related('authgroup__group_leaders').select_related('authgroup')
qs = qs.annotate(
member_count=Count('user', distinct=True),
)
@@ -138,7 +135,7 @@ class GroupAdmin(admin.ModelAdmin):
_member_count.admin_order_field = 'member_count'
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
@@ -168,6 +165,18 @@ class GroupAdmin(admin.ModelAdmin):
filter_horizontal = ('permissions',)
inlines = (AuthGroupInlineAdmin,)
def formfield_for_manytomany(self, db_field, request, **kwargs):
if db_field.name == "permissions":
kwargs["queryset"] = Permission.objects.select_related("content_type").all()
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 Meta:

View File

@@ -55,7 +55,6 @@ class RequestLog(models.Model):
return user.profile.main_character
class AuthGroup(models.Model):
"""
Extends Django Group model with a one-to-one field
@@ -107,7 +106,8 @@ class AuthGroup(models.Model):
help_text="States listed here will have the ability to join this group provided "
"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):
return self.group.name

View File

@@ -83,7 +83,7 @@
{% block extra_script %}
$.fn.dataTable.moment = function(format, locale) {
let types = $.fn.dataTable.ext.type;
var types = $.fn.dataTable.ext.type;
// Add type detection
types.detect.unshift(function(d) {

View File

@@ -47,7 +47,8 @@ class TestGroupAdmin(TestCase):
# group 2 - no leader
cls.group_2 = Group.objects.create(name='Group 2')
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()
# group 3 - has leader
@@ -237,10 +238,14 @@ class TestGroupAdmin(TestCase):
result = self.modeladmin._member_count(obj)
self.assertEqual(result, expected)
def test_has_leader(self):
def test_has_leader_user(self):
result = self.modeladmin.has_leader(self.group_1)
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):
expected = ['Default']
result = self.modeladmin._properties(self.group_1)

View File

@@ -1,4 +1,35 @@
from django.contrib import admin
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

View File

@@ -12,21 +12,20 @@ class NotificationHandler(logging.Handler):
try:
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:
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
)

View File

@@ -12,7 +12,7 @@ class NotificationQuerySet(models.QuerySet):
"""Custom QuerySet for Notification model"""
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)
user_pks = set(self.select_related("user").values_list('user__pk', flat=True))
for user_pk in user_pks:
@@ -43,6 +43,8 @@ class NotificationManager(models.Manager):
if not message:
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)
logger.info("Created notification %s", obj)
return obj

View File

@@ -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),
),
]

View File

@@ -2,6 +2,7 @@ import logging
from django.db import models
from django.contrib.auth.models import User
from django.utils.translation import gettext_lazy as _
from .managers import NotificationManager
@@ -13,17 +14,43 @@ class Notification(models.Model):
NOTIFICATIONS_MAX_PER_USER_DEFAULT = 50
NOTIFICATIONS_REFRESH_TIME_DEFAULT = 30
LEVEL_CHOICES = (
('danger', 'CRITICAL'),
('danger', 'ERROR'),
('warning', 'WARN'),
('info', 'INFO'),
('success', 'DEBUG'),
)
class Level(models.TextChoices):
"""A notification level."""
DANGER = 'danger', _('danger') #:
WARNING = 'warning', _('warning') #:
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)
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)
message = models.TextField()
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)
def mark_viewed(self) -> None:
"""mark notification as viewed"""
"""Mark notification as viewed."""
logger.info("Marking notification as viewed: %s" % self)
self.viewed = True
self.save()
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
"""
try:
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
Raises ValueError on invalid level names.
"""
self.level = self.Level.from_old_name(level_name)
self.save()

View 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")

View File

@@ -64,6 +64,35 @@ class TestUserNotify(TestCase):
self.assertEqual(obj.user, self.user)
self.assertEqual(obj.title, 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)
def test_remove_when_too_many_notifications(self):

View File

@@ -34,15 +34,17 @@
{% endblock %}
{% block extra_script %}
$('#id_start').datetimepicker({
setlocale: '{{ LANGUAGE_CODE }}',
{% if NIGHT_MODE %}
theme: 'dark',
{% else %}
theme: 'default',
{% endif %}
mask: true,
format: 'Y-m-d H:i',
minDate: 0
setlocale: '{{ LANGUAGE_CODE }}',
{% if NIGHT_MODE %}
theme: 'dark',
{% else %}
theme: 'default',
{% endif %}
mask: true,
format: 'Y-m-d H:i',
minDate: 0
});
{% endblock extra_script %}

View File

@@ -41,10 +41,9 @@
{% include 'bundles/moment-js.html' with locale=True %}
<script src="{% static 'js/timers.js' %}"></script>
<script type="application/javascript">
// Data
let timers = [
var timers = [
{% for op in optimer %}
{
'id': {{ op.id }},
@@ -53,66 +52,67 @@
},
{% endfor %}
];
</script>
<script type="application/javascript">
timedUpdate();
setAllLocalTimes();
// Start timed updates
setInterval(timedUpdate, 1000);
function timedUpdate() {
updateClock();
updateAllTimers();
}
function updateAllTimers () {
var l = timers.length;
for (var i=0; i < l; ++i) {
if (timers[i].expired) continue;
updateTimer(timers[i]);
}
}
/**
* Update a timer
* @param timer Timer information
* @param timer.start Date of the timer
* @param timer.id Id number of the timer
* @param timer.expired
*/
let updateTimer = function (timer) {
function updateTimer(timer) {
if (timer.start.isAfter(Date.now())) {
let duration = moment.duration(timer.start - moment(), 'milliseconds');
var duration = moment.duration(timer.start - moment(), 'milliseconds');
document.getElementById("countdown" + timer.id).innerHTML = getDurationString(duration);
} else {
timer.expired = true;
document.getElementById("countdown" + timer.id).innerHTML = "";
}
};
let updateAllTimers = function () {
let l = timers.length;
for (var i=0; i < l; ++i) {
if (timers[i].expired) continue;
updateTimer(timers[i]);
}
};
/**
* Set the local time info for the timer
* @param timer Timer information
*/
let setLocalTime = function (timer) {
document.getElementById("localtime" + timer.id).innerHTML = timer.start.format("ddd @ LT");
};
}
/**
* Set all local time fields
*/
let setAllLocalTimes = function () {
let l = timers.length;
function setAllLocalTimes() {
var l = timers.length;
for (var i=0; i < l; ++i) {
setLocalTime(timers[i]);
}
};
}
let updateClock = function () {
/**
* Set the local time info for the timer
* @param timer Timer information
* @param timer.start Date of the timer
* @param timer.id Id number of the timer
*/
function setLocalTime(timer) {
document.getElementById("localtime" + timer.id).innerHTML = timer.start.format("ddd @ LT");
}
function updateClock() {
document.getElementById("current-time").innerHTML = getCurrentEveTimeString();
};
let timedUpdate = function () {
updateClock();
updateAllTimers();
};
// Set initial values
setAllLocalTimes();
timedUpdate();
// Start timed updates
setInterval(timedUpdate, 1000);
}
</script>
{% endblock content %}

View File

@@ -24,7 +24,7 @@
</tr>
</thead>
<tbody>
{% for user in permission.users %}
{% for user in permission.users %}
{% include 'permissions_tool/audit_row.html' with type="User" name="Permission granted directlty" %}
{% endfor %}
{% for group in permission.groups %}
@@ -35,13 +35,13 @@
{% for state in permission.states %}
{% for profile in state.userprofile_set.all %}
{% with profile.user as user %}
{% include 'permissions_tool/audit_row.html' with type="State" name=state%}
{% include 'permissions_tool/audit_row.html' with type="State" name=state%}
{% endwith %}
{% endfor %}
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endblock content %}
@@ -56,16 +56,15 @@
{% block extra_script %}
$(document).ready(function() {
let groupColumn = 0;
$('#tab_permissions_audit').DataTable({
var groupColumn = 0;
var table = $('#tab_permissions_audit').DataTable({
columnDefs: [
{ "visible": false, "targets": groupColumn }
],
order: [[ groupColumn, 'asc' ], [ 2, 'asc' ] ],
filterDropDown:
{
columns: [
columns: [
{
idx: 0,
title: 'Source'
@@ -74,20 +73,20 @@
bootstrap: true
},
drawCallback: function ( settings ) {
let api = this.api();
let rows = api.rows( {page:'current'} ).nodes();
let last = null;
var api = this.api();
var rows = api.rows( {page:'current'} ).nodes();
var last=null;
api.column(groupColumn, {page:'current'} ).data().each( function ( group, i ) {
if ( last !== group ) {
$(rows).eq( i ).before(
'<tr class="tr-group"><td colspan="3">' + group + '</td></tr>'
);
last = group;
}
} );
}
} );
} );
{% endblock %}
{% endblock %}

View File

@@ -9,9 +9,9 @@
<div class="col-sm-12">
<h1 class="page-header">{% trans "Permissions Overview" %}</h1>
<p>
{% if request.GET.all != 'yes' %}
{% if request.GET.all != 'yes' %}
{% blocktrans %}Showing only applied permissions{% endblocktrans %}
<a href="{% url 'permissions_tool:overview' %}?all=yes" class="btn btn-primary">{% trans "Show All" %}</a>
<a href="{% url 'permissions_tool:overview' %}?all=yes" class="btn btn-primary">{% trans "Show All" %}</a>
{% else %}
{% blocktrans %}Showing all permissions{% endblocktrans %}
<a href="{% url 'permissions_tool:overview' %}?all=no" class="btn btn-primary">{% trans "Show Applied" %}</a>
@@ -79,7 +79,7 @@
{% endblock content %}
{% block extra_javascript %}
{% include 'bundles/datatables-js.html' %}
{% include 'bundles/datatables-js.html' %}
<script type="application/javascript" src="{% static 'js/filterDropDown/filterDropDown.min.js' %}"></script>
{% endblock %}
@@ -89,16 +89,15 @@
{% block extra_script %}
$(document).ready(function() {
let groupColumn = 0;
$('#tab_permissions_overview').DataTable({
var groupColumn = 0;
var table = $('#tab_permissions_overview').DataTable({
columnDefs: [
{ "visible": false, "targets": groupColumn }
],
order: [[ groupColumn, 'asc' ], [ 1, 'asc' ], [ 2, 'asc' ] ],
filterDropDown:
{
columns: [
columns: [
{
idx: 0
},
@@ -109,20 +108,20 @@
bootstrap: true
},
drawCallback: function ( settings ) {
let api = this.api();
let rows = api.rows( {page:'current'} ).nodes();
let last = null;
var api = this.api();
var rows = api.rows( {page:'current'} ).nodes();
var last=null;
api.column(groupColumn, {page:'current'} ).data().each( function ( group, i ) {
if ( last !== group ) {
$(rows).eq( i ).before(
'<tr class="tr-group"><td colspan="6">' + group + '</td></tr>'
);
last = group;
}
} );
}
} );
} );
{% endblock %}
{% endblock %}

View File

@@ -262,5 +262,3 @@ LOGGING = {
},
}
}
DEFAULT_AUTO_FIELD = "django.db.models.AutoField"

View File

@@ -30,5 +30,10 @@ class DiscordUserAdmin(ServicesUserAdmin):
else:
return ''
def delete_queryset(self, request, queryset):
for user in queryset:
user.delete_user()
_username.short_description = 'Discord Username'
_username.admin_order_field = 'username'
_username.admin_order_field = 'username'

View File

@@ -83,10 +83,31 @@ class DiscordUserManager(models.Manager):
if created is not False:
if created is None:
logger.debug(
"User %s with Discord ID %s is already a member.",
"User %s with Discord ID %s is already a member. Forcing a Refresh",
user,
user_id,
)
# Force an update cause the discord API won't do it for us.
if role_ids:
role_ids = list(role_ids)
updated = bot_client.modify_guild_member(
guild_id=DISCORD_GUILD_ID,
user_id=user_id,
role_ids=role_ids,
nick=nickname
)
if not updated:
# Could not update the new user so fail.
logger.warning(
"Failed to add user %s with Discord ID %s to Discord server",
user,
user_id,
)
return False
self.update_or_create(
user=user,
defaults={

View File

@@ -111,6 +111,40 @@ class TestAddUser(TestCase):
self.assertSetEqual(set(kwargs['role_ids']), {1, 2, 3})
self.assertIsNone(kwargs['nick'])
def test_can_activate_existing_user_with_roles_no_nick(
self,
mock_user_formatted_nick,
mock_user_group_names,
mock_exchange_auth_code_for_token,
mock_DiscordClient
):
roles = [
create_matched_role(ROLE_ALPHA),
create_matched_role(ROLE_BRAVO),
create_matched_role(ROLE_CHARLIE)
]
mock_user_formatted_nick.return_value = None
mock_user_group_names.return_value = ['a', 'b', 'c']
mock_exchange_auth_code_for_token.return_value = self.access_token
mock_DiscordClient.return_value.current_user.return_value = self.user_info
mock_DiscordClient.return_value.match_or_create_roles_from_names\
.return_value = roles
mock_DiscordClient.return_value.add_guild_member.return_value = None
mock_DiscordClient.return_value.modify_guild_member.return_value = True
result = DiscordUser.objects.add_user(self.user, authorization_code='abcdef')
self.assertTrue(result)
self.assertTrue(
DiscordUser.objects.filter(user=self.user, uid=TEST_USER_ID).exists()
)
self.assertTrue(mock_DiscordClient.return_value.add_guild_member.called)
self.assertTrue(mock_DiscordClient.return_value.modify_guild_member.called)
args, kwargs = mock_DiscordClient.return_value.modify_guild_member.call_args
self.assertEqual(kwargs['guild_id'], TEST_GUILD_ID)
self.assertEqual(kwargs['user_id'], TEST_USER_ID)
self.assertSetEqual(set(kwargs['role_ids']), {1, 2, 3})
self.assertIsNone(kwargs['nick'])
@patch(MODULE_PATH + '.managers.DISCORD_SYNC_NAMES', True)
def test_can_create_user_no_roles_with_nick(
self,
@@ -140,6 +174,36 @@ class TestAddUser(TestCase):
self.assertIsNone(kwargs['role_ids'])
self.assertEqual(kwargs['nick'], TEST_MAIN_NAME)
@patch(MODULE_PATH + '.managers.DISCORD_SYNC_NAMES', True)
def test_can_activate_existing_user_no_roles_with_nick(
self,
mock_user_formatted_nick,
mock_user_group_names,
mock_exchange_auth_code_for_token,
mock_DiscordClient
):
mock_user_formatted_nick.return_value = TEST_MAIN_NAME
mock_user_group_names.return_value = []
mock_exchange_auth_code_for_token.return_value = self.access_token
mock_DiscordClient.return_value.current_user.return_value = self.user_info
mock_DiscordClient.return_value.match_or_create_roles_from_names\
.return_value = []
mock_DiscordClient.return_value.add_guild_member.return_value = None
mock_DiscordClient.return_value.modify_guild_member.return_value = True
result = DiscordUser.objects.add_user(self.user, authorization_code='abcdef')
self.assertTrue(result)
self.assertTrue(
DiscordUser.objects.filter(user=self.user, uid=TEST_USER_ID).exists()
)
self.assertTrue(mock_DiscordClient.return_value.add_guild_member.called)
self.assertTrue(mock_DiscordClient.return_value.modify_guild_member.called)
args, kwargs = mock_DiscordClient.return_value.modify_guild_member.call_args
self.assertEqual(kwargs['guild_id'], TEST_GUILD_ID)
self.assertEqual(kwargs['user_id'], TEST_USER_ID)
self.assertIsNone(kwargs['role_ids'])
self.assertEqual(kwargs['nick'], TEST_MAIN_NAME)
@patch(MODULE_PATH + '.managers.DISCORD_SYNC_NAMES', False)
def test_can_create_user_no_roles_and_without_nick_if_turned_off(
self,
@@ -183,6 +247,7 @@ class TestAddUser(TestCase):
mock_DiscordClient.return_value.match_or_create_roles_from_names\
.return_value = []
mock_DiscordClient.return_value.add_guild_member.return_value = None
mock_DiscordClient.return_value.modify_guild_member.return_value = True
result = DiscordUser.objects.add_user(self.user, authorization_code='abcdef')
self.assertTrue(result)
@@ -190,6 +255,31 @@ class TestAddUser(TestCase):
DiscordUser.objects.filter(user=self.user, uid=TEST_USER_ID).exists()
)
self.assertTrue(mock_DiscordClient.return_value.add_guild_member.called)
self.assertTrue(mock_DiscordClient.return_value.modify_guild_member.called)
def test_can_activate_existing_guild_member_failure(
self,
mock_user_formatted_nick,
mock_user_group_names,
mock_exchange_auth_code_for_token,
mock_DiscordClient
):
mock_user_formatted_nick.return_value = None
mock_user_group_names.return_value = []
mock_exchange_auth_code_for_token.return_value = self.access_token
mock_DiscordClient.return_value.current_user.return_value = self.user_info
mock_DiscordClient.return_value.match_or_create_roles_from_names\
.return_value = []
mock_DiscordClient.return_value.add_guild_member.return_value = None
mock_DiscordClient.return_value.modify_guild_member.return_value = False
result = DiscordUser.objects.add_user(self.user, authorization_code='abcdef')
self.assertFalse(result)
self.assertFalse(
DiscordUser.objects.filter(user=self.user, uid=TEST_USER_ID).exists()
)
self.assertTrue(mock_DiscordClient.return_value.add_guild_member.called)
self.assertTrue(mock_DiscordClient.return_value.modify_guild_member.called)
def test_return_false_when_user_creation_fails(
self,

View File

@@ -12,12 +12,20 @@ import base64
import hmac
import hashlib
from urllib.parse import unquote, urlencode, parse_qs
try:
from urllib import unquote, urlencode
except ImportError: #py3
from urllib.parse import unquote, urlencode
try:
from urlparse import parse_qs
except ImportError: #py3
from urllib.parse import parse_qs
import logging
logger = logging.getLogger(__name__)
ACCESS_PERM = 'discourse.access_discourse'
@@ -47,7 +55,7 @@ def discourse_sso(request):
# Validate the payload
try:
payload = unquote(payload).encode('utf-8')
decoded = base64.decodebytes(payload).decode('utf-8')
decoded = base64.decodestring(payload).decode('utf-8')
assert 'nonce' in decoded
assert len(payload) > 0
except AssertionError:
@@ -78,7 +86,7 @@ def discourse_sso(request):
if main_char:
params['avatar_url'] = main_char.portrait_url(256)
return_payload = base64.encodebytes(urlencode(params).encode('utf-8'))
return_payload = base64.encodestring(urlencode(params).encode('utf-8'))
h = hmac.new(key, return_payload, digestmod=hashlib.sha256)
query_string = urlencode({'sso': return_payload, 'sig': h.hexdigest()})

View File

@@ -15,6 +15,7 @@ class Teamspeak3UserAdmin(ServicesUserAdmin):
@admin.register(AuthTS)
class AuthTSgroupAdmin(admin.ModelAdmin):
change_list_template = 'admin/teamspeak3/authts/change_list.html'
ordering = ('auth_group__name', )
list_select_related = True
@@ -28,7 +29,7 @@ class AuthTSgroupAdmin(admin.ModelAdmin):
return [x for x in obj.ts_group.all().order_by('ts_group_id')]
_ts_group.short_description = 'ts groups'
#_ts_group.admin_order_field = 'profile__state'
# _ts_group.admin_order_field = 'profile__state'
@admin.register(StateGroup)

View File

@@ -0,0 +1,11 @@
{% extends "admin/change_list.html" %}
{% load i18n static %}
{% block object-tools-items %}
{{ block.super }}
<li>
<a href="{% url 'teamspeak3:admin_update_ts3_groups' %}" class="btn btn-high">
Update TS3 groups
</a>
</li>
{% endblock %}

View File

@@ -216,6 +216,24 @@ class Teamspeak3ViewsTestCase(TestCase):
self.assertEqual(ts3_user.perm_key, '123abc')
self.assertTrue(tasks_manager.return_value.__enter__.return_value.update_groups.called)
@mock.patch(MODULE_PATH + '.views.Teamspeak3Tasks')
@mock.patch(MODULE_PATH + '.views.messages')
def test_should_update_ts_groups(self, messages, Teamspeak3Tasks):
# given
self.member.is_superuser = True
self.member.is_staff = True
self.member.save()
self.login()
# when
response = self.client.get(urls.reverse('teamspeak3:admin_update_ts3_groups'))
# then
self.assertRedirects(
response, urls.reverse('admin:teamspeak3_authts_changelist'),
target_status_code=200
)
self.assertTrue(messages.info.called)
self.assertTrue(Teamspeak3Tasks.run_ts3_group_update.delay.called)
class Teamspeak3SignalsTestCase(TestCase):
def setUp(self):

View File

@@ -6,12 +6,14 @@ app_name = 'teamspeak3'
module_urls = [
# Teamspeak3 service control
url(r'^activate/$', views.activate_teamspeak3,
name='activate'),
url(r'^deactivate/$', views.deactivate_teamspeak3,
name='deactivate'),
url(r'^reset_perm/$', views.reset_teamspeak3_perm,
name='reset_perm'),
url(r'^activate/$', views.activate_teamspeak3, name='activate'),
url(r'^deactivate/$', views.deactivate_teamspeak3, name='deactivate'),
url(r'^reset_perm/$', views.reset_teamspeak3_perm, name='reset_perm'),
url(
r'^admin_update_ts3_groups/$',
views.admin_update_ts3_groups,
name='admin_update_ts3_groups'
),
# Teamspeak Urls
url(r'^verify/$', views.verify_teamspeak3, name='verify'),

View File

@@ -1,6 +1,7 @@
import logging
from django.contrib import messages
from django.contrib.admin.views.decorators import staff_member_required
from django.contrib.auth.decorators import login_required, permission_required
from django.shortcuts import render, redirect
from django.conf import settings
@@ -99,3 +100,11 @@ def reset_teamspeak3_perm(request):
logger.error("Unsuccessful attempt to reset TS3 permission key for user %s" % request.user)
messages.error(request, _('An error occurred while processing your TeamSpeak3 account.'))
return redirect("services:services")
@login_required
@staff_member_required
def admin_update_ts3_groups(request):
Teamspeak3Tasks.run_ts3_group_update.delay()
messages.info(request, "Started updating TS3 server groups...")
return redirect("admin:teamspeak3_authts_changelist")

View File

@@ -43,15 +43,17 @@
{% endblock %}
{% block extra_script %}
$('#id_fleet_time').datetimepicker({
setlocale: '{{ LANGUAGE_CODE }}',
{% if NIGHT_MODE %}
theme: 'dark',
{% else %}
theme: 'default',
{% endif %}
mask: true,
format: 'Y-m-d H:i',
minDate: 0
setlocale: '{{ LANGUAGE_CODE }}',
{% if NIGHT_MODE %}
theme: 'dark',
{% else %}
theme: 'default',
{% endif %}
mask: true,
format: 'Y-m-d H:i',
minDate: 0
});
{% endblock extra_script %}

View File

@@ -187,7 +187,7 @@ ESC to cancel{% endblocktrans %}"id="blah"></i></th>
{% include 'bundles/x-editable-js.html' %}
{% include 'bundles/moment-js.html' %}
{% include 'bundles/clipboard-js.html' %}
<script>
var clipboard = new ClipboardJS('.copy-text-fa-icon');
clipboard.on('success', function (e) {
@@ -206,71 +206,73 @@ ESC to cancel{% endblocktrans %}"id="blah"></i></th>
{% endblock extra_javascript %}
{% block extra_script %}
$(document).ready(function() {
$.fn.editable.defaults.mode = 'inline';
$.fn.editable.defaults.showbuttons = false;
$.fn.editable.defaults.highlight = "#AAFF80";
$(document).ready(function() {
$.fn.editable.defaults.mode = 'inline';
$.fn.editable.defaults.showbuttons = false;
$.fn.editable.defaults.highlight = "#AAFF80";
$.fn.dataTable.moment = function(format, locale) {
let types = $.fn.dataTable.ext.type;
// Add type detection
types.detect.unshift(function(d) {
return moment(d, format, locale, true).isValid() ?
'moment-' + format :
null;
});
$('.srp').editable({
display: function(value, response) {
return false;
},
success: function(response, newValue) {
newValue = parseInt(newValue);
newvalue = newValue.toLocaleString() + " ISK";
$(this).html(newvalue.bold());
},
validate: function(value) {
if (value === null || value === '') {
return 'Empty values not allowed';
}
}
});
$('.srp').on('hidden', function(e, reason){
if(reason === 'save' || reason === 'nochange') {
var $next = $(this).closest('tr').next().find('.editable');
setTimeout(function() {
$next.editable('show');
}, 400);
}
});
});
// Add sorting method - use an integer for the sorting
types.order[ 'moment-' + format+'-pre' ] = function(d) {
return moment(d, format, locale, true).unix();
};
};
$.fn.dataTable.moment('YYYY-MMM-D, HH:mm');
$(document).ready(function(){
$("[rel=tooltip]").tooltip({ placement: 'top'});
});
$('.srp').editable({
display: function(value, response) {
return false;
},
success: function(response, newValue) {
newValue = parseInt(newValue);
let newValueOutput = newValue.toLocaleString() + " ISK";
$.fn.dataTable.moment = function(format, locale) {
var types = $.fn.dataTable.ext.type;
$(this).html(newValueOutput.bold());
},
validate: function(value) {
if (value === null || value === '') {
return 'Empty values not allowed';
}
}
});
// Add type detection
types.detect.unshift(function(d) {
return moment(d, format, locale, true).isValid() ?
'moment-'+format :
null;
} );
$('.srp').on('hidden', function(e, reason){
if(reason === 'save' || reason === 'nochange') {
let $next = $(this).closest('tr').next().find('.editable');
// Add sorting method - use an integer for the sorting
types.order[ 'moment-'+format+'-pre' ] = function(d) {
return moment(d, format, locale, true).unix();
};
};
setTimeout(function() {
$next.editable('show');
}, 400);
}
});
$(document).ready( function(){
$.fn.dataTable.moment('YYYY-MMM-D, HH:mm');
$('table.srplist').DataTable({
"order": [[ 6, "asc" ]],
"paging": false,
"columnDefs": [
{
"targets": [1, 8],
"orderable": false
},
{
"targets": [4, 5],
"type": "num"
}
]
});
$('table.srplist').DataTable({
"order": [[ 6, "asc" ]],
"paging": false,
"columnDefs": [{
"targets": [1, 8],
"orderable": false
},
{
"targets": [4, 5],
"type": "num"
}]
// tooltip
$("[rel=tooltip]").tooltip({ placement: 'top'});
});
});
{% endblock extra_script %}

View File

@@ -1,5 +1,3 @@
/* global notificationUPdateSettings */
/*
This script refreshed the unread notification count in the top menu
on a regular basis so to keep the user apprised about newly arrived
@@ -8,67 +6,70 @@
The refresh rate can be changes via the Django setting NOTIFICATIONS_REFRESH_TIME.
See documentation for details.
*/
$(function () {
'use strict';
let notificationsListViewUrl = notificationUPdateSettings.notificationsListViewUrl;
let notificationsRefreshTime = notificationUPdateSettings.notificationsRefreshTime;
let userNotificationsCountViewUrl = notificationUPdateSettings.userNotificationsCountViewUrl;
$(function () {
var elem = document.getElementById("dataExport");
var notificationsListViewUrl = elem.getAttribute("data-notificationsListViewUrl");
var notificationsRefreshTime = elem.getAttribute("data-notificationsRefreshTime");
var userNotificationsCountViewUrl = elem.getAttribute(
"data-userNotificationsCountViewUrl"
);
// update the notification unread count in the top menu
let updateNotifications = function () {
function update_notifications() {
$.getJSON(userNotificationsCountViewUrl, function (data, status) {
if (status === 'success') {
let innerHtml = '';
let unreadCount = data.unread_count;
if (unreadCount > 0) {
if (status == 'success') {
var innerHtml = "";
var unread_count = data.unread_count;
if (unread_count > 0) {
innerHtml = (
`Notifications <span class="badge">${unreadCount}</span>`
);
} else {
innerHtml = '<i class="far fa-bell"></i>';
`Notifications <span class="badge">${unread_count}</span>`
)
}
$('#menu_item_notifications').html(
else {
innerHtml = '<i class="far fa-bell"></i>'
}
$("#menu_item_notifications").html(
`<a href="${notificationsListViewUrl}">${innerHtml}</a>`
);
} else {
}
else {
console.error(
`Failed to load HTMl to render notifications item. Error: ${xhr.status}': '${xhr.statusText}`
`Failed to load HTMl to render notifications item. Error: `
`${xhr.status}': '${xhr.statusText}`
);
}
});
};
}
let myInterval;
var myInterval;
// activate automatic refreshing every x seconds
let activateRefreshing = function () {
function activate_refreshing() {
if (notificationsRefreshTime > 0) {
myInterval = setInterval(
updateNotifications, notificationsRefreshTime * 1000
update_notifications, notificationsRefreshTime * 1000
);
}
};
}
// deactivate automatic refreshing
let deactivateRefreshing = function () {
function deactivate_refreshing() {
if ((notificationsRefreshTime > 0) && (typeof myInterval !== 'undefined')) {
clearInterval(myInterval);
clearInterval(myInterval)
}
};
}
// refreshing only happens on active browser tab
$(document).on({
'show': function () {
activateRefreshing();
activate_refreshing()
},
'hide': function () {
deactivateRefreshing();
deactivate_refreshing()
}
});
// Initial start of refreshing on script loading
activateRefreshing();
activate_refreshing()
});

View File

@@ -1,49 +1,23 @@
/* global moment */
/**
* Get a duration string like countdown.js
* e.g. "1y 2d 3h 4m 5s"
*
* @param duration
* @returns {string}
*/
let getDurationString = function (duration) {
'use strict';
let out = '';
* Get a duration string like countdown.js
* e.g. "1y 2d 3h 4m 5s"
* @param duration moment.duration
*/
function getDurationString(duration) {
var out = "";
if (duration.years()) {
out += duration.years() + 'y ';
}
if (duration.months()) {
out += duration.months() + 'm ';
}
if (duration.days()) {
out += duration.days() + 'd ';
}
return out + duration.hours() + "h " + duration.minutes() + "m " + duration.seconds() + "s";
}
return out + duration.hours() + 'h ' + duration.minutes() + 'm ' + duration.seconds() + 's';
};
/**
* returns the current eve time as a formatted string
*
* condition:
* only if moment.js is loaded before,
* if not this function returns an empty string to avoid JS errors from happening.
*
* @returns {string}
*/
let getCurrentEveTimeString = function () {
'use strict';
let returnValue = '';
if (window.moment) {
returnValue = moment().utc().format('dddd LL HH:mm:ss');
}
return returnValue;
};
function getCurrentEveTimeString() {
return moment().utc().format('dddd LL HH:mm:ss')
}

View File

@@ -1,2 +0,0 @@
User-agent: *
Disallow: /

View File

@@ -30,15 +30,7 @@
<div class="row" id="site-body-wrapper">
{% include 'allianceauth/side-menu.html' %}
<div class="col-sm-10">
{% if messages %}
<br>
{% for message in messages %}
<div class="alert alert-{{ message.level_tag }}">{{ message }}</div>
{% if not forloop.last %}
<br>
{% endif %}
{% endfor %}
{% endif %}
{% include 'allianceauth/messages.html' %}
{% block content %}
{% endblock content %}
</div>
@@ -46,20 +38,19 @@
</div>
</div>
{% endif %}
<!-- share data with JS part -->
<div
id="dataExport"
data-notificationsListViewUrl="{% url 'notifications:list' %}"
data-notificationsRefreshTime="{% notifications_refresh_time %}"
data-userNotificationsCountViewUrl="{% url 'notifications:user_notifications_count' request.user.pk %}"
>
</div>
{% include 'bundles/bootstrap-js.html' %}
{% include 'bundles/jquery-visibility-js.html' %}
<script type="application/javascript">
let notificationUPdateSettings = {
notificationsListViewUrl: "{% url 'notifications:list' %}",
notificationsRefreshTime: "{% notifications_refresh_time %}",
userNotificationsCountViewUrl: "{% url 'notifications:user_notifications_count' request.user.pk %}"
};
</script>
<script src="{% static 'js/refresh_notifications.js' %}"></script>
{% block extra_javascript %}
{% block extra_javascript %}
{% endblock extra_javascript %}
<script>
{% block extra_script %}

View File

@@ -0,0 +1,26 @@
{% if messages %}
<br>
{% for message in messages %}
<div class="alert alert-{{ message.level_tag }} alert-dismissible" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<div class="message-icon pull-left" style="margin-right: 0.5rem;">
{% if message.level_tag == "info" %}
<i class="fas fa-info-circle"></i>
{% elif message.level_tag == "success" %}
<i class="fas fa-check-circle"></i>
{% elif message.level_tag == "warning" %}
<i class="fas fa-exclamation-circle"></i>
{% elif message.level_tag == "danger" %}
<i class="fas fa-exclamation-triangle"></i>
{% endif %}
</div>
<div class="message-text" style="margin-left: 2.5rem;">
{{ message }}
</div>
</div>
{% endfor %}
{% endif %}

View File

@@ -526,7 +526,9 @@
{% include 'bundles/moment-js.html' with locale=True %}
<script src="{% static 'js/timers.js' %}"></script>
<script type="application/javascript">
let timers = [
var locale = "{{ LANGUAGE_CODE }}";
var timers = [
{% for timer in timers %}
{
'id': {{ timer.id }},
@@ -543,64 +545,67 @@
{% endfor %}
];
/**
* Update a timer
* @param timer Timer information
*/
let updateTimer = function (timer) {
if (timer.targetDate.isAfter(Date.now())) {
let duration = moment.duration(timer.targetDate - moment(), 'milliseconds');
moment.locale(locale);
document.getElementById("countdown" + timer.id).innerHTML = getDurationString(duration);
} else {
timer.expired = true;
document.getElementById("countdown" + timer.id).innerHTML = "";
}
};
let updateAllTimers = function () {
let l = timers.length;
for (var i=0; i < l; ++i) {
if (timers[i].expired) continue;
updateTimer(timers[i]);
}
};
function timedUpdate() {
updateClock();
updateAllTimers();
}
/**
* Set the local time info for the timer
* @param timer Timer information
*/
let setLocalTime = function (timer) {
document.getElementById("localtime" + timer.id).innerHTML = timer.targetDate.format("ddd @ LT");
};
/**
* Set all local time fields
*/
let setAllLocalTimes = function () {
let l = timers.length;
for (var i=0; i < l; ++i) {
setLocalTime(timers[i]);
}
};
let updateClock = function () {
document.getElementById("current-time").innerHTML = getCurrentEveTimeString();
};
// Set initial values
setAllLocalTimes();
timedUpdate();
// Start timed updates
setInterval(timedUpdate, 1000);
function timedUpdate() {
updateClock();
updateAllTimers();
}
function updateAllTimers () {
var l = timers.length;
for (var i=0; i < l; ++i) {
if (timers[i].expired) continue;
updateTimer(timers[i]);
}
}
/**
* Update a timer
* @param timer Timer information
* @param timer.targetDate Date of the timer
* @param timer.id Id number of the timer
* @param timer.expired
*/
function updateTimer(timer) {
if (timer.targetDate.isAfter(Date.now())) {
duration = moment.duration(timer.targetDate - moment(), 'milliseconds');
document.getElementById("countdown" + timer.id).innerHTML = getDurationString(duration);
} else {
timer.expired = true;
document.getElementById("countdown" + timer.id).innerHTML = "";
}
}
/**
* Set all local time fields
*/
function setAllLocalTimes() {
var l = timers.length;
for (var i=0; i < l; ++i) {
setLocalTime(timers[i]);
}
}
/**
* Set the local time info for the timer
* @param timer Timer information
* @param timer.targetDate Date of the timer
* @param timer.id Id number of the timer
*/
function setLocalTime(timer) {
document.getElementById("localtime" + timer.id).innerHTML = timer.targetDate.format("ddd @ LT");
}
function updateClock() {
document.getElementById("current-time").innerHTML = getCurrentEveTimeString();
}
</script>
{% endblock content %}

View File

@@ -23,7 +23,7 @@ Within your auth project exists two folders named `static` and `templates`. Thes
You can add extra static or templates by putting files in these folders. Note that changes to static requires running the `python manage.py collectstatic` command to copy to the web server directory.
It is possible to overload static and templates shipped with Django or Alliance Auth by including a file with the exact path of the one you wish to overload. For instance if you wish to add extra links to the menu bar by editing the template, you would make a copy of the `allianceauth/templates/allianceauth/base.html` file to `myauth/templates/allinceauth/base.html` and edit it there. Notice the paths are identical after the `templates/` directory - this is critical for it to be recognized. Your custom template would be used instead of the one included with Alliance Auth when Django renders the web page. Similar idea for static: put CSS or images at an identical path after the `static/` directory and they will be copied to the web server directory instead of the ones included.
It is possible to overload static and templates shipped with Django or Alliance Auth by including a file with the exact path of the one you wish to overload. For instance if you wish to add extra links to the menu bar by editing the template, you would make a copy of the `allianceauth/templates/allianceauth/base.html` file to `myauth/templates/allianceauth/base.html` and edit it there. Notice the paths are identical after the `templates/` directory - this is critical for it to be recognized. Your custom template would be used instead of the one included with Alliance Auth when Django renders the web page. Similar idea for static: put CSS or images at an identical path after the `static/` directory and they will be copied to the web server directory instead of the ones included.
## Custom URLs and Views

View File

@@ -7,19 +7,16 @@ The notifications package has an API for sending notifications.
Location: ``allianceauth.notifications``
.. automodule:: allianceauth.notifications.__init__
:members: notify
:undoc-members:
:members: notify
models
===========
.. autoclass:: allianceauth.notifications.models.Notification
:members: LEVEL_CHOICES, mark_viewed, set_level
:undoc-members:
:members: Level, mark_viewed, set_level
managers
===========
.. autoclass:: allianceauth.notifications.managers.NotificationManager
:members: notify_user, user_unread_count
:undoc-members:

View File

@@ -105,7 +105,9 @@ Click the URL provided to automatically connect to our server. It will prompt yo
Now we need to make groups. AllianceAuth handles groups in teamspeak differently: instead of creating groups it creates an association between groups in TeamSpeak and groups in AllianceAuth. Go ahead and make the groups you want to associate with auth groups, keeping in mind multiple TeamSpeak groups can be associated with a single auth group.
Navigate back to the AllianceAuth admin interface (example.com/admin) and under `Services`, select `Auth / TS Groups`. In the top-right corner click `Add`.
Navigate back to the AllianceAuth admin interface (example.com/admin) and under `Teamspeak3`, select `Auth / TS Groups`.
In the top-right corner click, first click on `Update TS3 Groups` to fetch the newly created server groups from TS3 (this may take a minute to complete). Then click on `Add Auth / TS Group` to link Auth groups with TS3 server groups.
The dropdown box provides all auth groups. Select one and assign TeamSpeak groups from the panels below. If these panels are empty, wait a minute for the database update to run, or see the [troubleshooting section](#ts-group-models-not-populating-on-admin-site) below.
@@ -119,13 +121,23 @@ To enable advanced permissions, on your client go to the `Tools` menu, `Applicat
### TS group models not populating on admin site
The method which populates these runs every 30 minutes. To populate manually, start a django shell:
The method which populates these runs every 30 minutes. To populate manually you start the process from the admin site or from the Django shell.
#### Admin Site
Navigate to the AllianceAuth admin interface and under `Teamspeak3`, select `Auth / TS Groups`.
Then, in the top-right corner click, click on `Update TS3 Groups` to start the process of fetching the server groups from TS3 (this may take a minute to complete).
#### Django Shell
Start a django shell with:
```bash
python manage.py shell
```
And execute the update:
And execute the update as follows:
```python
from allianceauth.services.modules.teamspeak3.tasks import Teamspeak3Tasks

View File

@@ -104,6 +104,8 @@ CREATE DATABASE alliance_auth CHARACTER SET utf8mb4;
GRANT ALL PRIVILEGES ON alliance_auth . * TO 'allianceserver'@'localhost';
```
Once your database is set up, you can leave the SQL shell with `exit`.
Add timezone tables to your mysql installation:
```bash
@@ -177,7 +179,7 @@ source /home/allianceserver/venv/auth/bin/activate
You need to have a dedicated Eve SSO app for Alliance auth. Please go to [EVE Developer](https://developers.eveonline.com/applications) to create one.
For **scopes** your SSO app needs to have at least `publicData`. Additional scopes depends on which Alliance Auth apps you will be using. For convenience we recommend adding all available ESO scopes to your SSO app. Note that Alliance Auth will always ask the users to approve specific scopes before they are used.
For **scopes** your SSO app needs to have at least `publicData`. Additional scopes depends on which Alliance Auth apps you will be using. For convenience, we recommend adding all available ESO scopes to your SSO app. Note that Alliance Auth will always ask the users to approve specific scopes before they are used.
As **callback URL** you want to define the URL of your Alliance Auth site plus the route: `/sso/callback`. Example for a valid callback URL: `https://auth.example.com/sso/callback`
@@ -236,7 +238,7 @@ Check to ensure your settings are valid.
python /home/allianceserver/myauth/manage.py check
```
And finally ensure the allianceserver user has read/write permissions to this directory before proceeding.
Finally, ensure the allianceserver user has read/write permissions to this directory before proceeding.
```bash
chown -R allianceserver:allianceserver /home/allianceserver/myauth
@@ -244,7 +246,7 @@ chown -R allianceserver:allianceserver /home/allianceserver/myauth
## Services
Alliance Auth needs some additional services to run, which we will setup and configure next.
Alliance Auth needs some additional services to run, which we will set up and configure next.
### Gunicorn
@@ -275,7 +277,7 @@ systemctl enable supervisord.service
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.
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.
Ubuntu:
@@ -289,7 +291,7 @@ CentOS:
ln -s /home/allianceserver/myauth/supervisor.conf /etc/supervisord.d/myauth.ini
```
And activate it with `supervisorctl reload`.
Activate it with `supervisorctl reload`.
You can check the status of the processes with `supervisorctl status`. Logs from these processes are available in `/home/allianceserver/myauth/log` named by process.
@@ -304,11 +306,11 @@ You can check the status of the processes with `supervisorctl status`. Logs from
Once installed, decide on whether you're going to use [NGINX](nginx.md) or [Apache](apache.md) and follow the respective guide.
Note that Alliance Auth is designed to run with web servers on HTTPS. While running on HTTP is technically possible, it is not recommended for production use and some functions (e.g. Email confirmation links) will not work properly.
Note that Alliance Auth is designed to run with web servers on HTTPS. While running on HTTP is technically possible, it is not recommended for production use, and some functions (e.g. Email confirmation links) will not work properly.
## Superuser
Before using your auth site it is essential to create a superuser account. This account will have all permissions in Alliance Auth. It's OK to use this as your personal auth account.
Before using your auth site, it is essential to create a superuser account. This account will have all permissions in Alliance Auth. It's OK to use this as your personal auth account.
```bash
python /home/allianceserver/myauth/manage.py createsuperuser
@@ -316,7 +318,7 @@ python /home/allianceserver/myauth/manage.py createsuperuser
The superuser account is accessed by logging in via the admin site at `https://example.com/admin`.
If you intend to use this account as your personal auth account you need to add a main character. Navigate to the normal user dashboard (at `https://example.com`) after logging in via the admin site and select `Change Main`. Once a main character has been added it is possible to use SSO to login to this account.
If you intend to use this account as your personal auth account you need to add a main character. Navigate to the normal user dashboard (at `https://example.com`) after logging in via the admin site and select `Change Main`. Once a main character has been added, it is possible to use SSO to login to this account.
## Updating
@@ -340,7 +342,7 @@ Some releases come with new or changed models. Update your database to reflect t
python /home/allianceserver/myauth/manage.py migrate
```
Finally some releases come with new or changed static files. Run the following command to update your static files folder:
Finally, some releases come with new or changed static files. Run the following command to update your static files folder:
```bash
python /home/allianceserver/myauth/manage.py collectstatic

View File

@@ -45,25 +45,17 @@ Place your virtual host configuration in the appropriate section within `/etc/ht
```
<VirtualHost *:80>
ServerName auth.example.com
ProxyPassMatch ^/static !
ProxyPassMatch ^/robots.txt !
ProxyPassMatch ^/static !
ProxyPass / http://127.0.0.1:8000/
ProxyPassReverse / http://127.0.0.1:8000/
ProxyPreserveHost On
Alias "/static" "/var/www/myauth/static"
Alias "/robots.txt" "/var/www/myauth/static/robots.txt"
<Directory "/var/www/myauth/static">
Require all granted
</Directory>
<Location "/robots.txt">
SetHandler None
Require all granted
</Location>
</VirtualHost>
```

View File

@@ -69,10 +69,6 @@ server {
autoindex off;
}
location /robots.txt {
alias /var/www/myauth/static/robots.txt;
}
# Gunicorn config goes below
location / {
include proxy_params;

View File

@@ -18,17 +18,12 @@ To run AA with a newer Python 3 version than your system's default you need to i
```eval_rst
.. note::
For stability and performance we currently recommend to run AA with Python 3.7. It has proven to be the fastest and most stable version in use currently.
For stability and performance we currently recommend to run AA with Python 3.7. Since at the time of writing Python 3.7 was not available for CentOS through yum install this guide will upgrade to Python 3.6. For Ubuntu one can just replace "3.6" with "3.7" in the installation commands to get Python 3.7.
```
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.
To install other Python versions than come with your distro you need to add a new installation repository. Then you can install the specific Python 3 to your system.
Ubuntu 1604 1804:
```eval_rst
.. note::
Ubuntu 2004 ships with Python 3.8, No updates required.
```
Ubuntu:
```bash
add-apt-repository ppa:deadsnakes/ppa
@@ -39,38 +34,23 @@ apt-get update
```
```bash
apt-get install python3.7 python3.7-dev python3.7-venv
apt-get install python3.6 python3.6-dev python3.6-venv
```
CentOS 7/8:
CentOS:
```bash
cd ~
yum install https://centos7.iuscommunity.org/ius-release.rpm
```
```bash
sudo yum install gcc openssl-devel bzip2-devel libffi-devel wget
yum update
```
```bash
wget https://www.python.org/ftp/python/3.7.10/Python-3.7.10.tgz
yum install python36u python36u-pip python36u-devel
```
```bash
tar xvf Python-3.7.10.tgz
```
```bash
cd Python-3.7.10/
```
```bash
./configure --enable-optimizations --enable-shared
```
```bash
make altinstall
```
## Preparing your venv
Before updating your venv it is important to make sure that your current installation is stable. Otherwise your new venv might not be consistent with your data, which might create problems.
@@ -117,6 +97,12 @@ If you unsure which apps you have installed from repos check `INSTALLED_APPS` in
pip list
```
Some AA installations might still be running an older version of django-celery-beat. We would recommend to upgrade to the current version before doing the Python update:
```bash
pip install -U 'django-celery-beat<2.00'
```
```bash
python manage.py migrate
```
@@ -185,7 +171,7 @@ mv /home/allianceserver/venv/auth /home/allianceserver/venv/auth_old
Now let's create our new venv with Python 3.6 and activate it:
```bash
python3.7 -m venv /home/allianceserver/venv/auth
python3.6 -m venv /home/allianceserver/venv/auth
```
```bash

View File

@@ -17,8 +17,8 @@ Mature Alliance Auth installations, or those with actively developed extensions
This can make it confusing for admins to apply the right permissions, contribute to larger queries in backend management or simply look unsightly.
```python
python manage.py remove_stale_contenttypes --include-stale-apps
```shell
python manage.py remove_stale_contenttypes --include-stale-apps
```
This inbuilt Django command will step through each contenttype and offer to delete it, displaying what exactly this will cascade to delete. Pay attention and ensure you understand exactly what is being removed before answering `yes`.

View File

@@ -22,18 +22,18 @@ install_requires = [
'celery>=4.3.0,<5.0.0,!=4.4.4', # 4.4.4 is missing a dependency
'celery_once>=2.0.1',
'django>=3.1.1,<4.0.0',
'django>=3.1.1,<3.2.0',
'django-bootstrap-form',
'django-registration>=3.1',
'django-sortedm2m',
'django-redis-cache>=3.0.0',
'django-celery-beat>=2.0.0',
'django-celery-beat>=2.0.0,<2.2.1',
'openfire-restapi',
'sleekxmpp',
'pydiscourse',
'django-esi>=2.0.4,<3.0'
'django-esi>=3.0.0,<4.0.0'
]
testing_extras = [
@@ -58,7 +58,7 @@ setup(
extras_require={
'testing': testing_extras
},
python_requires='~=3.7',
python_requires='~=3.6',
license='GPLv2',
packages=['allianceauth'],
url=allianceauth.__url__,
@@ -72,20 +72,15 @@ setup(
'Environment :: Web Environment',
'Framework :: Django',
'Framework :: Django :: 3.1',
'Framework :: Django :: 3.2',
'Intended Audience :: Developers',
'License :: OSI Approved :: GNU General Public License v2 (GPLv2)',
'Operating System :: POSIX :: Linux',
'Programming Language :: Python',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
'Topic :: Internet :: WWW/HTTP',
'Topic :: Internet :: WWW/HTTP :: Dynamic Content',
],
project_urls={
'Documentation': 'https://allianceauth.readthedocs.io/',
},
)

View File

@@ -1,16 +1,16 @@
[tox]
skipsdist = true
usedevelop = true
envlist = py{37,38,39}-{all}
envlist = py{36,37,38}-{all}
[testenv]
setenv =
all: DJANGO_SETTINGS_MODULE = tests.settings_all
core: DJANGO_SETTINGS_MODULE = tests.settings_core
basepython =
py36: python3.6
py37: python3.7
py38: python3.8
py39: python3.9
deps=
coverage
install_command = pip install -e ".[testing]" -U {opts} {packages}