aa-loa 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- aa_loa/__init__.py +2 -0
- aa_loa/admin.py +15 -0
- aa_loa/apps.py +10 -0
- aa_loa/auth_hooks.py +30 -0
- aa_loa/forms.py +50 -0
- aa_loa/migrations/0001_initial.py +49 -0
- aa_loa/migrations/__init__.py +0 -0
- aa_loa/models.py +81 -0
- aa_loa/signals.py +14 -0
- aa_loa/tasks.py +106 -0
- aa_loa/templates/aa_loa/hr_dashboard.html +147 -0
- aa_loa/templates/aa_loa/index.html +134 -0
- aa_loa/urls.py +12 -0
- aa_loa/views.py +81 -0
- aa_loa-0.0.1.dist-info/METADATA +128 -0
- aa_loa-0.0.1.dist-info/RECORD +18 -0
- aa_loa-0.0.1.dist-info/WHEEL +4 -0
- aa_loa-0.0.1.dist-info/licenses/LICENSE +21 -0
aa_loa/__init__.py
ADDED
aa_loa/admin.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from django.contrib import admin
|
|
2
|
+
|
|
3
|
+
from .models import LeaveOfAbsence, LOAConfig
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@admin.register(LOAConfig)
|
|
7
|
+
class LOAConfigAdmin(admin.ModelAdmin):
|
|
8
|
+
list_display = ["__str__", "loa_group"]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@admin.register(LeaveOfAbsence)
|
|
12
|
+
class LeaveOfAbsenceAdmin(admin.ModelAdmin):
|
|
13
|
+
list_display = ["user", "start_date", "end_date", "is_active", "is_revoked"]
|
|
14
|
+
list_filter = ["is_revoked", "start_date", "end_date"]
|
|
15
|
+
search_fields = ["user__username"]
|
aa_loa/apps.py
ADDED
aa_loa/auth_hooks.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from allianceauth import hooks
|
|
2
|
+
from allianceauth.services.hooks import MenuItemHook, UrlHook
|
|
3
|
+
|
|
4
|
+
from . import urls
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class LoaMenuItem(MenuItemHook):
|
|
8
|
+
def __init__(self):
|
|
9
|
+
MenuItemHook.__init__(
|
|
10
|
+
self,
|
|
11
|
+
"Leave of Absence",
|
|
12
|
+
"fas fa-plane-departure fa-fw",
|
|
13
|
+
"aa_loa:index",
|
|
14
|
+
navactive=["aa_loa:"],
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
def render(self, request):
|
|
18
|
+
if request.user.has_perm("aa_loa.basic_access"):
|
|
19
|
+
return MenuItemHook.render(self, request)
|
|
20
|
+
return ""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@hooks.register("menu_item_hook")
|
|
24
|
+
def register_menu():
|
|
25
|
+
return LoaMenuItem()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@hooks.register("url_hook")
|
|
29
|
+
def register_urls():
|
|
30
|
+
return UrlHook(urls, "aa_loa", r"^loa/")
|
aa_loa/forms.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from django import forms
|
|
2
|
+
from django.contrib.auth.models import User
|
|
3
|
+
from django.utils import timezone
|
|
4
|
+
|
|
5
|
+
from .models import LeaveOfAbsence
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class PlayerLOAForm(forms.ModelForm):
|
|
9
|
+
class Meta:
|
|
10
|
+
model = LeaveOfAbsence
|
|
11
|
+
fields = ["start_date", "end_date", "reason"]
|
|
12
|
+
widgets = {
|
|
13
|
+
"start_date": forms.DateInput(attrs={"type": "date", "class": "form-control", "onkeydown": "return false"}),
|
|
14
|
+
"end_date": forms.DateInput(attrs={"type": "date", "class": "form-control", "onkeydown": "return false"}),
|
|
15
|
+
"reason": forms.Textarea(attrs={"class": "form-control", "rows": 3}),
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
def __init__(self, *args, **kwargs):
|
|
19
|
+
super().__init__(*args, **kwargs)
|
|
20
|
+
# Set minimum date for HTML5 date pickers to today
|
|
21
|
+
today_str = timezone.localdate().strftime("%Y-%m-%d")
|
|
22
|
+
self.fields['start_date'].widget.attrs.update({'min': today_str})
|
|
23
|
+
self.fields['end_date'].widget.attrs.update({'min': today_str})
|
|
24
|
+
|
|
25
|
+
def clean_start_date(self):
|
|
26
|
+
start_date = self.cleaned_data.get("start_date")
|
|
27
|
+
if start_date and start_date < timezone.localdate():
|
|
28
|
+
raise forms.ValidationError("Start date cannot be in the past.")
|
|
29
|
+
return start_date
|
|
30
|
+
|
|
31
|
+
def clean(self):
|
|
32
|
+
cleaned_data = super().clean()
|
|
33
|
+
start_date = cleaned_data.get("start_date")
|
|
34
|
+
end_date = cleaned_data.get("end_date")
|
|
35
|
+
|
|
36
|
+
if start_date and end_date:
|
|
37
|
+
if start_date > end_date:
|
|
38
|
+
raise forms.ValidationError("End date cannot be before start date.")
|
|
39
|
+
return cleaned_data
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class DirectorLOAForm(PlayerLOAForm):
|
|
43
|
+
user = forms.ModelChoiceField(
|
|
44
|
+
queryset=User.objects.all().order_by("username"),
|
|
45
|
+
widget=forms.Select(attrs={"class": "form-control"}),
|
|
46
|
+
label="Player",
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
class Meta(PlayerLOAForm.Meta):
|
|
50
|
+
fields = ["user", "start_date", "end_date", "reason"]
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Generated by Django 5.2.15 on 2026-07-24 14:45
|
|
2
|
+
|
|
3
|
+
import django.db.models.deletion
|
|
4
|
+
from django.conf import settings
|
|
5
|
+
from django.db import migrations, models
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Migration(migrations.Migration):
|
|
9
|
+
|
|
10
|
+
initial = True
|
|
11
|
+
|
|
12
|
+
dependencies = [
|
|
13
|
+
('auth', '0012_alter_user_first_name_max_length'),
|
|
14
|
+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
operations = [
|
|
18
|
+
migrations.CreateModel(
|
|
19
|
+
name='LeaveOfAbsence',
|
|
20
|
+
fields=[
|
|
21
|
+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
22
|
+
('start_date', models.DateField()),
|
|
23
|
+
('end_date', models.DateField()),
|
|
24
|
+
('reason', models.TextField(blank=True, null=True)),
|
|
25
|
+
('created_at', models.DateTimeField(auto_now_add=True)),
|
|
26
|
+
('updated_at', models.DateTimeField(auto_now=True)),
|
|
27
|
+
('is_revoked', models.BooleanField(default=False, help_text='Set to true if the user returned early.')),
|
|
28
|
+
('notified_return', models.BooleanField(default=False, help_text='Set to true once the welcome back notification is sent.')),
|
|
29
|
+
('submitted_by', models.ForeignKey(blank=True, help_text='If submitted by a Director, this logs who did it.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='proxy_loas', to=settings.AUTH_USER_MODEL)),
|
|
30
|
+
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='loas', to=settings.AUTH_USER_MODEL)),
|
|
31
|
+
],
|
|
32
|
+
options={
|
|
33
|
+
'ordering': ['-start_date'],
|
|
34
|
+
'permissions': (('basic_access', 'Can access the LOA module'), ('manage_loa', 'Can manage LOAs for other users and view HR dashboard')),
|
|
35
|
+
},
|
|
36
|
+
),
|
|
37
|
+
migrations.CreateModel(
|
|
38
|
+
name='LOAConfig',
|
|
39
|
+
fields=[
|
|
40
|
+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
41
|
+
('discord_webhook_url', models.URLField(blank=True, help_text='Optional Discord Webhook URL for Director notifications when LOAs are created.', null=True)),
|
|
42
|
+
('loa_group', models.ForeignKey(blank=True, help_text='The Django Group to assign to users while their LOA is active.', null=True, on_delete=django.db.models.deletion.SET_NULL, to='auth.group')),
|
|
43
|
+
],
|
|
44
|
+
options={
|
|
45
|
+
'verbose_name': 'LOA Config',
|
|
46
|
+
'verbose_name_plural': 'LOA Config',
|
|
47
|
+
},
|
|
48
|
+
),
|
|
49
|
+
]
|
|
File without changes
|
aa_loa/models.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
from django.contrib.auth.models import Group, User
|
|
2
|
+
from django.db import models
|
|
3
|
+
from django.utils import timezone
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class LOAConfig(models.Model):
|
|
7
|
+
loa_group = models.ForeignKey(
|
|
8
|
+
Group,
|
|
9
|
+
on_delete=models.SET_NULL,
|
|
10
|
+
null=True,
|
|
11
|
+
blank=True,
|
|
12
|
+
help_text="The Django Group to assign to users while their LOA is active.",
|
|
13
|
+
)
|
|
14
|
+
discord_webhook_url = models.URLField(
|
|
15
|
+
blank=True,
|
|
16
|
+
null=True,
|
|
17
|
+
help_text="Optional Discord Webhook URL for Director notifications when LOAs are created.",
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
class Meta:
|
|
21
|
+
verbose_name = "LOA Config"
|
|
22
|
+
verbose_name_plural = "LOA Config"
|
|
23
|
+
|
|
24
|
+
def __str__(self):
|
|
25
|
+
return "Leave of Absence Configuration"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class LeaveOfAbsence(models.Model):
|
|
29
|
+
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="loas")
|
|
30
|
+
start_date = models.DateField()
|
|
31
|
+
end_date = models.DateField()
|
|
32
|
+
reason = models.TextField(blank=True, null=True)
|
|
33
|
+
|
|
34
|
+
submitted_by = models.ForeignKey(
|
|
35
|
+
User,
|
|
36
|
+
on_delete=models.SET_NULL,
|
|
37
|
+
null=True,
|
|
38
|
+
blank=True,
|
|
39
|
+
related_name="proxy_loas",
|
|
40
|
+
help_text="If submitted by a Director, this logs who did it.",
|
|
41
|
+
)
|
|
42
|
+
created_at = models.DateTimeField(auto_now_add=True)
|
|
43
|
+
updated_at = models.DateTimeField(auto_now=True)
|
|
44
|
+
|
|
45
|
+
is_revoked = models.BooleanField(
|
|
46
|
+
default=False, help_text="Set to true if the user returned early."
|
|
47
|
+
)
|
|
48
|
+
notified_return = models.BooleanField(
|
|
49
|
+
default=False, help_text="Set to true once the welcome back notification is sent."
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
class Meta:
|
|
53
|
+
permissions = (
|
|
54
|
+
("basic_access", "Can access the LOA module"),
|
|
55
|
+
("manage_loa", "Can manage LOAs for other users and view HR dashboard"),
|
|
56
|
+
)
|
|
57
|
+
ordering = ["-start_date"]
|
|
58
|
+
|
|
59
|
+
def __str__(self):
|
|
60
|
+
return f"LOA: {self.user.username} ({self.start_date} to {self.end_date})"
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def is_active(self):
|
|
64
|
+
if self.is_revoked:
|
|
65
|
+
return False
|
|
66
|
+
today = timezone.now().date()
|
|
67
|
+
return self.start_date <= today <= self.end_date
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def is_past(self):
|
|
71
|
+
if self.is_revoked:
|
|
72
|
+
return True
|
|
73
|
+
today = timezone.now().date()
|
|
74
|
+
return self.end_date < today
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def is_future(self):
|
|
78
|
+
if self.is_revoked:
|
|
79
|
+
return False
|
|
80
|
+
today = timezone.now().date()
|
|
81
|
+
return self.start_date > today
|
aa_loa/signals.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from django.db.models.signals import post_save
|
|
2
|
+
from django.dispatch import receiver
|
|
3
|
+
|
|
4
|
+
from .models import LeaveOfAbsence
|
|
5
|
+
from .tasks import send_loa_webhook, sync_loa_groups
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@receiver(post_save, sender=LeaveOfAbsence)
|
|
9
|
+
def on_loa_saved(sender, instance, created, **kwargs):
|
|
10
|
+
if created:
|
|
11
|
+
send_loa_webhook.delay(instance.id)
|
|
12
|
+
|
|
13
|
+
# Trigger a sync immediately so if it's active today, they get the role
|
|
14
|
+
sync_loa_groups.delay()
|
aa_loa/tasks.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
import requests
|
|
4
|
+
from celery import shared_task
|
|
5
|
+
from django.utils import timezone
|
|
6
|
+
from django.db import models
|
|
7
|
+
from allianceauth.services.hooks import get_extension_logger
|
|
8
|
+
from allianceauth.notifications import notify
|
|
9
|
+
|
|
10
|
+
from .models import LeaveOfAbsence, LOAConfig
|
|
11
|
+
|
|
12
|
+
logger = get_extension_logger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@shared_task
|
|
16
|
+
def sync_loa_groups():
|
|
17
|
+
"""
|
|
18
|
+
Checks all LOAs and adds/removes users from the designated LOA Django group.
|
|
19
|
+
"""
|
|
20
|
+
config = LOAConfig.objects.first()
|
|
21
|
+
if not config or not config.loa_group:
|
|
22
|
+
logger.warning("LOA Config or LOA Group is not set. Cannot sync groups.")
|
|
23
|
+
return
|
|
24
|
+
|
|
25
|
+
loa_group = config.loa_group
|
|
26
|
+
today = timezone.now().date()
|
|
27
|
+
|
|
28
|
+
# 1. Add users with active LOAs
|
|
29
|
+
active_loas = LeaveOfAbsence.objects.filter(
|
|
30
|
+
start_date__lte=today,
|
|
31
|
+
end_date__gte=today,
|
|
32
|
+
is_revoked=False
|
|
33
|
+
)
|
|
34
|
+
for loa in active_loas:
|
|
35
|
+
if not loa.user.groups.filter(id=loa_group.id).exists():
|
|
36
|
+
loa.user.groups.add(loa_group)
|
|
37
|
+
logger.info(f"Added {loa.user.username} to LOA Group.")
|
|
38
|
+
|
|
39
|
+
# 2. Remove users whose LOAs have ended or been revoked,
|
|
40
|
+
# but ONLY if they don't have another overlapping active LOA.
|
|
41
|
+
inactive_loas = LeaveOfAbsence.objects.filter(
|
|
42
|
+
models.Q(end_date__lt=today) | models.Q(is_revoked=True)
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
for loa in inactive_loas:
|
|
46
|
+
user = loa.user
|
|
47
|
+
# Check if they have another active LOA right now
|
|
48
|
+
has_active = LeaveOfAbsence.objects.filter(
|
|
49
|
+
user=user,
|
|
50
|
+
start_date__lte=today,
|
|
51
|
+
end_date__gte=today,
|
|
52
|
+
is_revoked=False
|
|
53
|
+
).exists()
|
|
54
|
+
|
|
55
|
+
if not has_active and user.groups.filter(id=loa_group.id).exists():
|
|
56
|
+
user.groups.remove(loa_group)
|
|
57
|
+
logger.info(f"Removed {user.username} from LOA Group.")
|
|
58
|
+
|
|
59
|
+
# Send Welcome Back Notification if not sent
|
|
60
|
+
if not loa.notified_return and loa.end_date < today and not loa.is_revoked:
|
|
61
|
+
notify(
|
|
62
|
+
user=user,
|
|
63
|
+
title="Welcome Back from LOA",
|
|
64
|
+
message="Welcome back! Your LOA has expired. Let us know if you need an extension.",
|
|
65
|
+
level="info"
|
|
66
|
+
)
|
|
67
|
+
loa.notified_return = True
|
|
68
|
+
loa.save(update_fields=["notified_return"])
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@shared_task
|
|
72
|
+
def send_loa_webhook(loa_id):
|
|
73
|
+
"""
|
|
74
|
+
Sends a Discord webhook notification when a new LOA is submitted.
|
|
75
|
+
"""
|
|
76
|
+
config = LOAConfig.objects.first()
|
|
77
|
+
if not config or not config.discord_webhook_url:
|
|
78
|
+
return
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
loa = LeaveOfAbsence.objects.get(id=loa_id)
|
|
82
|
+
except LeaveOfAbsence.DoesNotExist:
|
|
83
|
+
return
|
|
84
|
+
|
|
85
|
+
title = "New Leave of Absence"
|
|
86
|
+
if loa.submitted_by:
|
|
87
|
+
title = f"Proxy LOA submitted by {loa.submitted_by.username}"
|
|
88
|
+
|
|
89
|
+
embed = {
|
|
90
|
+
"title": title,
|
|
91
|
+
"color": 16753920, # Orange
|
|
92
|
+
"fields": [
|
|
93
|
+
{"name": "Player", "value": loa.user.username, "inline": True},
|
|
94
|
+
{"name": "Start", "value": str(loa.start_date), "inline": True},
|
|
95
|
+
{"name": "End", "value": str(loa.end_date), "inline": True},
|
|
96
|
+
]
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if loa.reason:
|
|
100
|
+
embed["fields"].append({"name": "Reason", "value": loa.reason[:1000], "inline": False})
|
|
101
|
+
|
|
102
|
+
payload = {"embeds": [embed]}
|
|
103
|
+
try:
|
|
104
|
+
requests.post(config.discord_webhook_url, json=payload, timeout=5)
|
|
105
|
+
except Exception as e:
|
|
106
|
+
logger.error(f"Failed to send LOA webhook: {e}")
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
{% extends "allianceauth/base-bs5.html" %}
|
|
2
|
+
{% load i18n %}
|
|
3
|
+
{% load django_bootstrap5 %}
|
|
4
|
+
|
|
5
|
+
{% block page_title %}{% translate "LOA HR Dashboard" %}{% endblock page_title %}
|
|
6
|
+
|
|
7
|
+
{% block extra_css %}
|
|
8
|
+
{% include 'bundles/datatables-css-bs5.html' %}
|
|
9
|
+
{% endblock %}
|
|
10
|
+
|
|
11
|
+
{% block content %}
|
|
12
|
+
<div class="col-lg-12">
|
|
13
|
+
<h1 class="page-header text-center">Leave of Absence - HR Dashboard</h1>
|
|
14
|
+
|
|
15
|
+
<div class="row" style="margin-bottom: 20px;">
|
|
16
|
+
<div class="col-md-12 text-right">
|
|
17
|
+
<a href="{% url 'aa_loa:index' %}" class="btn btn-primary">Go to Player Dashboard</a>
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
<div class="row">
|
|
22
|
+
<!-- Proxy Submit -->
|
|
23
|
+
<div class="col-md-4">
|
|
24
|
+
<div class="panel panel-warning">
|
|
25
|
+
<div class="panel-heading">
|
|
26
|
+
<h3 class="panel-title">Proxy Submit LOA</h3>
|
|
27
|
+
</div>
|
|
28
|
+
<div class="panel-body">
|
|
29
|
+
<form method="post" action="{% url 'aa_loa:hr_dashboard' %}">
|
|
30
|
+
{% csrf_token %}
|
|
31
|
+
{% bootstrap_form form %}
|
|
32
|
+
<br>
|
|
33
|
+
<button type="submit" class="btn btn-warning btn-block">Submit Proxy LOA</button>
|
|
34
|
+
</form>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<!-- All LOAs -->
|
|
40
|
+
<div class="col-md-8">
|
|
41
|
+
<div class="panel panel-default">
|
|
42
|
+
<div class="panel-heading">
|
|
43
|
+
<h3 class="panel-title">All LOAs</h3>
|
|
44
|
+
</div>
|
|
45
|
+
<div class="panel-body">
|
|
46
|
+
<table class="table table-striped table-hover" id="loaTable">
|
|
47
|
+
<thead>
|
|
48
|
+
<tr>
|
|
49
|
+
<th>Player</th>
|
|
50
|
+
<th>Status</th>
|
|
51
|
+
<th>Start Date</th>
|
|
52
|
+
<th>End Date</th>
|
|
53
|
+
<th>Reason</th>
|
|
54
|
+
<th>Proxy</th>
|
|
55
|
+
<th>Actions</th>
|
|
56
|
+
</tr>
|
|
57
|
+
</thead>
|
|
58
|
+
<tbody>
|
|
59
|
+
{% for loa in loas %}
|
|
60
|
+
<tr>
|
|
61
|
+
<td>{{ loa.user.username }}</td>
|
|
62
|
+
<td>
|
|
63
|
+
{% if loa.is_revoked %}
|
|
64
|
+
<span class="label label-default">Cancelled</span>
|
|
65
|
+
{% elif loa.is_active %}
|
|
66
|
+
<span class="label label-success">Active</span>
|
|
67
|
+
{% elif loa.is_future %}
|
|
68
|
+
<span class="label label-info">Upcoming</span>
|
|
69
|
+
{% else %}
|
|
70
|
+
<span class="label label-default">Past</span>
|
|
71
|
+
{% endif %}
|
|
72
|
+
</td>
|
|
73
|
+
<td>{{ loa.start_date }}</td>
|
|
74
|
+
<td>{{ loa.end_date }}</td>
|
|
75
|
+
<td>{{ loa.reason|default:"-" }}</td>
|
|
76
|
+
<td>{{ loa.submitted_by.username|default:"-" }}</td>
|
|
77
|
+
<td>
|
|
78
|
+
{% if not loa.is_revoked and not loa.is_past %}
|
|
79
|
+
<!-- Trigger Modal -->
|
|
80
|
+
<button type="button" class="btn btn-xs btn-danger" data-bs-toggle="modal" data-bs-target="#hrRevokeModal{{ loa.id }}">
|
|
81
|
+
Cancel
|
|
82
|
+
</button>
|
|
83
|
+
|
|
84
|
+
<!-- Modal -->
|
|
85
|
+
<div class="modal fade" id="hrRevokeModal{{ loa.id }}" tabindex="-1" aria-labelledby="hrRevokeModalLabel{{ loa.id }}" aria-hidden="true">
|
|
86
|
+
<div class="modal-dialog">
|
|
87
|
+
<div class="modal-content">
|
|
88
|
+
<div class="modal-header">
|
|
89
|
+
<h5 class="modal-title" id="hrRevokeModalLabel{{ loa.id }}">Confirm Cancellation</h5>
|
|
90
|
+
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
91
|
+
</div>
|
|
92
|
+
<div class="modal-body">
|
|
93
|
+
Are you sure you want to cancel the Leave of Absence for <strong>{{ loa.user.username }}</strong>?
|
|
94
|
+
</div>
|
|
95
|
+
<div class="modal-footer">
|
|
96
|
+
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Go Back</button>
|
|
97
|
+
<form method="post" action="{% url 'aa_loa:hr_revoke' loa.id %}" style="display:inline;">
|
|
98
|
+
{% csrf_token %}
|
|
99
|
+
<button type="submit" class="btn btn-danger">Yes, Cancel LOA</button>
|
|
100
|
+
</form>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
{% else %}
|
|
106
|
+
-
|
|
107
|
+
{% endif %}
|
|
108
|
+
</td>
|
|
109
|
+
</tr>
|
|
110
|
+
{% endfor %}
|
|
111
|
+
</tbody>
|
|
112
|
+
</table>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
{% endblock content %}
|
|
119
|
+
|
|
120
|
+
{% block extra_javascript %}
|
|
121
|
+
{% include 'bundles/datatables-js-bs5.html' %}
|
|
122
|
+
<script>
|
|
123
|
+
$(document).ready(function() {
|
|
124
|
+
$('#loaTable').DataTable({
|
|
125
|
+
"order": [[ 2, "desc" ]],
|
|
126
|
+
"dom": "<'row'<'col-sm-12 col-md-6'l><'col-sm-12 col-md-6'f>>" +
|
|
127
|
+
"<'row'<'col-sm-12 mb-2'p>>" +
|
|
128
|
+
"<'row'<'col-sm-12'tr>>" +
|
|
129
|
+
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>"
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// When start_date changes, update the min attribute of end_date
|
|
133
|
+
$('#id_start_date').on('change', function() {
|
|
134
|
+
var startDate = $(this).val();
|
|
135
|
+
if (startDate) {
|
|
136
|
+
$('#id_end_date').attr('min', startDate);
|
|
137
|
+
|
|
138
|
+
// If end_date is now before start_date, clear it
|
|
139
|
+
var endDate = $('#id_end_date').val();
|
|
140
|
+
if (endDate && endDate < startDate) {
|
|
141
|
+
$('#id_end_date').val('');
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
</script>
|
|
147
|
+
{% endblock %}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
{% extends "allianceauth/base-bs5.html" %}
|
|
2
|
+
{% load i18n %}
|
|
3
|
+
{% load django_bootstrap5 %}
|
|
4
|
+
|
|
5
|
+
{% block page_title %}{% translate "Leave of Absence" %}{% endblock page_title %}
|
|
6
|
+
|
|
7
|
+
{% block content %}
|
|
8
|
+
<div class="col-lg-12">
|
|
9
|
+
<h1 class="page-header text-center">Leave of Absence</h1>
|
|
10
|
+
|
|
11
|
+
{% if perms.aa_loa.manage_loa %}
|
|
12
|
+
<div class="row" style="margin-bottom: 20px;">
|
|
13
|
+
<div class="col-md-12 text-right">
|
|
14
|
+
<a href="{% url 'aa_loa:hr_dashboard' %}" class="btn btn-warning">Go to HR Dashboard</a>
|
|
15
|
+
</div>
|
|
16
|
+
</div>
|
|
17
|
+
{% endif %}
|
|
18
|
+
|
|
19
|
+
<div class="row">
|
|
20
|
+
<!-- New LOA Form -->
|
|
21
|
+
<div class="col-md-4">
|
|
22
|
+
<div class="panel panel-primary">
|
|
23
|
+
<div class="panel-heading">
|
|
24
|
+
<h3 class="panel-title">Submit LOA</h3>
|
|
25
|
+
</div>
|
|
26
|
+
<div class="panel-body">
|
|
27
|
+
<form method="post" action="{% url 'aa_loa:index' %}">
|
|
28
|
+
{% csrf_token %}
|
|
29
|
+
{% bootstrap_form form %}
|
|
30
|
+
<br>
|
|
31
|
+
<button type="submit" class="btn btn-success btn-block">Submit</button>
|
|
32
|
+
</form>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<!-- Existing LOAs -->
|
|
38
|
+
<div class="col-md-8">
|
|
39
|
+
<div class="panel panel-default">
|
|
40
|
+
<div class="panel-heading">
|
|
41
|
+
<h3 class="panel-title">Your LOAs</h3>
|
|
42
|
+
</div>
|
|
43
|
+
<div class="panel-body">
|
|
44
|
+
{% if loas %}
|
|
45
|
+
<table class="table table-striped table-hover">
|
|
46
|
+
<thead>
|
|
47
|
+
<tr>
|
|
48
|
+
<th>Status</th>
|
|
49
|
+
<th>Start Date</th>
|
|
50
|
+
<th>End Date</th>
|
|
51
|
+
<th>Reason</th>
|
|
52
|
+
<th>Actions</th>
|
|
53
|
+
</tr>
|
|
54
|
+
</thead>
|
|
55
|
+
<tbody>
|
|
56
|
+
{% for loa in loas %}
|
|
57
|
+
<tr>
|
|
58
|
+
<td>
|
|
59
|
+
{% if loa.is_revoked %}
|
|
60
|
+
<span class="label label-default">Cancelled</span>
|
|
61
|
+
{% elif loa.is_active %}
|
|
62
|
+
<span class="label label-success">Active</span>
|
|
63
|
+
{% elif loa.is_future %}
|
|
64
|
+
<span class="label label-info">Upcoming</span>
|
|
65
|
+
{% else %}
|
|
66
|
+
<span class="label label-default">Past</span>
|
|
67
|
+
{% endif %}
|
|
68
|
+
</td>
|
|
69
|
+
<td>{{ loa.start_date }}</td>
|
|
70
|
+
<td>{{ loa.end_date }}</td>
|
|
71
|
+
<td>{{ loa.reason|default:"-"|truncatechars:50 }}</td>
|
|
72
|
+
<td>
|
|
73
|
+
{% if loa.is_active or loa.is_future %}
|
|
74
|
+
<!-- Trigger Modal -->
|
|
75
|
+
<button type="button" class="btn btn-xs btn-danger" data-bs-toggle="modal" data-bs-target="#revokeModal{{ loa.id }}">
|
|
76
|
+
Return Early / Cancel
|
|
77
|
+
</button>
|
|
78
|
+
|
|
79
|
+
<!-- Modal -->
|
|
80
|
+
<div class="modal fade" id="revokeModal{{ loa.id }}" tabindex="-1" aria-labelledby="revokeModalLabel{{ loa.id }}" aria-hidden="true">
|
|
81
|
+
<div class="modal-dialog">
|
|
82
|
+
<div class="modal-content">
|
|
83
|
+
<div class="modal-header">
|
|
84
|
+
<h5 class="modal-title" id="revokeModalLabel{{ loa.id }}">Confirm Cancellation</h5>
|
|
85
|
+
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
86
|
+
</div>
|
|
87
|
+
<div class="modal-body">
|
|
88
|
+
Are you sure you want to return early and cancel this Leave of Absence?
|
|
89
|
+
</div>
|
|
90
|
+
<div class="modal-footer">
|
|
91
|
+
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Go Back</button>
|
|
92
|
+
<form method="post" action="{% url 'aa_loa:revoke' loa.id %}" style="display:inline;">
|
|
93
|
+
{% csrf_token %}
|
|
94
|
+
<button type="submit" class="btn btn-danger">Yes, Cancel LOA</button>
|
|
95
|
+
</form>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
{% endif %}
|
|
101
|
+
</td>
|
|
102
|
+
</tr>
|
|
103
|
+
{% endfor %}
|
|
104
|
+
</tbody>
|
|
105
|
+
</table>
|
|
106
|
+
{% else %}
|
|
107
|
+
<p class="text-muted">You have no active or historical LOAs.</p>
|
|
108
|
+
{% endif %}
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
{% endblock content %}
|
|
115
|
+
|
|
116
|
+
{% block extra_javascript %}
|
|
117
|
+
<script>
|
|
118
|
+
$(document).ready(function() {
|
|
119
|
+
// When start_date changes, update the min attribute of end_date
|
|
120
|
+
$('#id_start_date').on('change', function() {
|
|
121
|
+
var startDate = $(this).val();
|
|
122
|
+
if (startDate) {
|
|
123
|
+
$('#id_end_date').attr('min', startDate);
|
|
124
|
+
|
|
125
|
+
// If end_date is now before start_date, clear it
|
|
126
|
+
var endDate = $('#id_end_date').val();
|
|
127
|
+
if (endDate && endDate < startDate) {
|
|
128
|
+
$('#id_end_date').val('');
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
</script>
|
|
134
|
+
{% endblock extra_javascript %}
|
aa_loa/urls.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from django.urls import path
|
|
2
|
+
|
|
3
|
+
from . import views
|
|
4
|
+
|
|
5
|
+
app_name = "aa_loa"
|
|
6
|
+
|
|
7
|
+
urlpatterns = [
|
|
8
|
+
path("", views.loa_index, name="index"),
|
|
9
|
+
path("revoke/<int:loa_id>/", views.revoke_loa, name="revoke"),
|
|
10
|
+
path("hr/", views.loa_hr_dashboard, name="hr_dashboard"),
|
|
11
|
+
path("hr/revoke/<int:loa_id>/", views.hr_revoke_loa, name="hr_revoke"),
|
|
12
|
+
]
|
aa_loa/views.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
from django.contrib import messages
|
|
2
|
+
from django.contrib.auth.decorators import login_required, permission_required
|
|
3
|
+
from django.shortcuts import get_object_or_404, redirect, render
|
|
4
|
+
from django.utils import timezone
|
|
5
|
+
|
|
6
|
+
from .forms import DirectorLOAForm, PlayerLOAForm
|
|
7
|
+
from .models import LeaveOfAbsence
|
|
8
|
+
from allianceauth.notifications import notify
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@login_required
|
|
12
|
+
@permission_required("aa_loa.basic_access")
|
|
13
|
+
def loa_index(request):
|
|
14
|
+
user_loas = LeaveOfAbsence.objects.filter(user=request.user).order_by("-start_date")
|
|
15
|
+
|
|
16
|
+
if request.method == "POST":
|
|
17
|
+
form = PlayerLOAForm(request.POST)
|
|
18
|
+
if form.is_valid():
|
|
19
|
+
loa = form.save(commit=False)
|
|
20
|
+
loa.user = request.user
|
|
21
|
+
loa.save()
|
|
22
|
+
messages.success(request, "Leave of Absence submitted successfully.")
|
|
23
|
+
return redirect("aa_loa:index")
|
|
24
|
+
else:
|
|
25
|
+
form = PlayerLOAForm()
|
|
26
|
+
|
|
27
|
+
return render(
|
|
28
|
+
request,
|
|
29
|
+
"aa_loa/index.html",
|
|
30
|
+
{"loas": user_loas, "form": form},
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@login_required
|
|
35
|
+
@permission_required("aa_loa.basic_access")
|
|
36
|
+
def revoke_loa(request, loa_id):
|
|
37
|
+
loa = get_object_or_404(LeaveOfAbsence, id=loa_id, user=request.user)
|
|
38
|
+
if request.method == "POST":
|
|
39
|
+
loa.is_revoked = True
|
|
40
|
+
loa.save()
|
|
41
|
+
messages.success(request, "LOA cancelled/revoked.")
|
|
42
|
+
return redirect("aa_loa:index")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@login_required
|
|
46
|
+
@permission_required("aa_loa.manage_loa")
|
|
47
|
+
def hr_revoke_loa(request, loa_id):
|
|
48
|
+
loa = get_object_or_404(LeaveOfAbsence, id=loa_id)
|
|
49
|
+
if request.method == "POST":
|
|
50
|
+
loa.is_revoked = True
|
|
51
|
+
loa.save()
|
|
52
|
+
messages.success(request, f"LOA for {loa.user.username} cancelled by HR.")
|
|
53
|
+
notify(
|
|
54
|
+
user=loa.user,
|
|
55
|
+
title="Leave of Absence Cancelled",
|
|
56
|
+
message=f"Your Leave of Absence starting on {loa.start_date} was cancelled by HR ({request.user.username}).",
|
|
57
|
+
level="warning"
|
|
58
|
+
)
|
|
59
|
+
return redirect("aa_loa:hr_dashboard")
|
|
60
|
+
|
|
61
|
+
@login_required
|
|
62
|
+
@permission_required("aa_loa.manage_loa")
|
|
63
|
+
def loa_hr_dashboard(request):
|
|
64
|
+
all_loas = LeaveOfAbsence.objects.all().select_related("user").order_by("-start_date")
|
|
65
|
+
|
|
66
|
+
if request.method == "POST":
|
|
67
|
+
form = DirectorLOAForm(request.POST)
|
|
68
|
+
if form.is_valid():
|
|
69
|
+
loa = form.save(commit=False)
|
|
70
|
+
loa.submitted_by = request.user
|
|
71
|
+
loa.save()
|
|
72
|
+
messages.success(request, f"Proxy LOA submitted for {loa.user.username}.")
|
|
73
|
+
return redirect("aa_loa:hr_dashboard")
|
|
74
|
+
else:
|
|
75
|
+
form = DirectorLOAForm()
|
|
76
|
+
|
|
77
|
+
return render(
|
|
78
|
+
request,
|
|
79
|
+
"aa_loa/hr_dashboard.html",
|
|
80
|
+
{"loas": all_loas, "form": form},
|
|
81
|
+
)
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aa-loa
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Leave of Absence module for Alliance Auth
|
|
5
|
+
Project-URL: Changelog, https://github.com/mbroekman/aa-loa/blob/main/CHANGELOG.md
|
|
6
|
+
Project-URL: Homepage, https://github.com/mbroekman/aa-loa
|
|
7
|
+
Project-URL: Source, https://github.com/mbroekman/aa-loa
|
|
8
|
+
Project-URL: Tracker, https://github.com/mbroekman/aa-loa/issues
|
|
9
|
+
Author-email: Maddog Broekman <maddogbroekman@gmail.com>
|
|
10
|
+
License: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: allianceauth,leave_of_absence,loa
|
|
13
|
+
Classifier: Environment :: Web Environment
|
|
14
|
+
Classifier: Framework :: Django
|
|
15
|
+
Classifier: Framework :: Django :: 4.2
|
|
16
|
+
Classifier: Intended Audience :: Developers
|
|
17
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
18
|
+
Classifier: Operating System :: OS Independent
|
|
19
|
+
Classifier: Programming Language :: Python
|
|
20
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
24
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
25
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
|
26
|
+
Requires-Python: <3.14,>=3.10
|
|
27
|
+
Requires-Dist: allianceauth>=3.6.0
|
|
28
|
+
Requires-Dist: discord-webhook>=1.3.0
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# Alliance Auth - Leave of Absence (aa-loa)
|
|
32
|
+
|
|
33
|
+
[](https://pypi.org/project/aa-loa/)
|
|
34
|
+
[](https://pypi.org/project/aa-loa/)
|
|
35
|
+
[](https://github.com/mbroekman/aa-loa/actions/workflows/automated-checks.yml)
|
|
36
|
+
|
|
37
|
+
A complete Leave of Absence (LOA) management module for Alliance Auth. This module allows members to declare periods of absence so that leadership is aware of their inactivity, while seamlessly integrating with other Alliance Auth features (like Discord and Activity Trackers) to prevent accidental purges.
|
|
38
|
+
|
|
39
|
+
## Features
|
|
40
|
+
|
|
41
|
+
- **Player Dashboard:** Easy-to-use form for players to submit a start date, end date, and an optional reason. Players can also view their past LOAs, and cancel/revoke an active LOA if they return early.
|
|
42
|
+
- **HR Dashboard:** A dedicated view for Directors/HR to see a live table of all active and upcoming LOAs in the alliance.
|
|
43
|
+
- **Proxy Submission:** Directors can submit an LOA on behalf of a member who might be unable to access a PC (e.g. emergencies).
|
|
44
|
+
- **Discord Role Sync:** Assigns members to a specific Django Group (e.g., `[On Leave]`) when their LOA becomes active. Alliance Auth will automatically sync this to Discord, TS, or Mumble.
|
|
45
|
+
- **Purge Protection:** Because the member is in the `[On Leave]` group, audit modules (like `aa-inactives` or `opcalendar`) can simply whitelist this group to exempt the member from activity purges.
|
|
46
|
+
- **Webhooks:** Automatically posts a Discord Embed to a webhook URL whenever an LOA is submitted.
|
|
47
|
+
- **Welcome Back Notification:** A Celery task automatically removes the member from the LOA group when the end date passes, and sends a "Welcome Back" Alliance Auth notification.
|
|
48
|
+
|
|
49
|
+
### 🤖 Optional: Discord Bot Integration (`aa-discordbot`)
|
|
50
|
+
|
|
51
|
+
While `aa-loa` works perfectly fine on its own, it integrates natively with [aa-discordbot](https://github.com/pvyParts/allianceauth-discordbot).
|
|
52
|
+
If your alliance has `aa-discordbot` installed, the internal Alliance Auth notifications generated by this module will automatically be forwarded as **Direct Messages (DMs)** on Discord to the user. This applies to:
|
|
53
|
+
- 📩 The "Welcome Back" notification when an LOA automatically expires.
|
|
54
|
+
- 🛑 The "Cancellation" notification when HR manually cancels a player's LOA.
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Installation
|
|
59
|
+
|
|
60
|
+
### 1. Install the Python Package
|
|
61
|
+
Activate your Alliance Auth virtual environment and install the package:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
pip install -e /path/to/aa-loa/
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### 2. Update Alliance Auth Settings
|
|
68
|
+
Open your `myauth/settings/local.py` file and add `'aa_loa'` to your `INSTALLED_APPS`:
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
INSTALLED_APPS += [
|
|
72
|
+
# ... other apps
|
|
73
|
+
'aa_loa',
|
|
74
|
+
]
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### 3. Run Database Migrations
|
|
78
|
+
Run the migrations to create the database tables:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
python manage.py makemigrations aa_loa
|
|
82
|
+
python manage.py migrate
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### 4. Restart Services
|
|
86
|
+
Restart your Alliance Auth web service and Celery workers so they pick up the new code and background tasks:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
sudo systemctl restart supervisor
|
|
90
|
+
```
|
|
91
|
+
*(Or restart the specific services depending on your hosting environment).*
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Configuration
|
|
96
|
+
|
|
97
|
+
### 1. Setup the LOA Group
|
|
98
|
+
1. Go to the **Django Admin Panel** (`/admin/`).
|
|
99
|
+
2. Navigate to **Authentication and Authorization** -> **Groups** and create a new group (e.g., `On Leave`).
|
|
100
|
+
- *Optional:* Configure this group in your Discord/TS services to map to a specific `[On Leave]` Discord Role.
|
|
101
|
+
3. Navigate to **Leave of Absence Configuration** -> **LOA Config**.
|
|
102
|
+
4. Add a new configuration row.
|
|
103
|
+
5. Select the `On Leave` group you just created.
|
|
104
|
+
6. *(Optional)* Paste a Discord Webhook URL if you want real-time notifications in an HR channel when LOAs are submitted.
|
|
105
|
+
|
|
106
|
+
### 2. Permissions
|
|
107
|
+
Assign the following permissions to the appropriate states or groups in Alliance Auth:
|
|
108
|
+
|
|
109
|
+
| Permission | Description |
|
|
110
|
+
|---|---|
|
|
111
|
+
| `aa_loa.basic_access` | Grants access to the LOA module for normal members. Allows them to submit and manage their own LOAs. |
|
|
112
|
+
| `aa_loa.manage_loa` | Grants access to the HR Dashboard. Allows users to view all LOAs and submit proxy LOAs for other members. |
|
|
113
|
+
|
|
114
|
+
### 3. Setup the Celery Periodic Task
|
|
115
|
+
To ensure LOA groups are automatically assigned and removed every night, you need to configure the daily Celery task in your `local.py` settings file.
|
|
116
|
+
|
|
117
|
+
Open your `myauth/settings/local.py` and add the following to your `CELERYBEAT_SCHEDULE` dictionary:
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
from celery.schedules import crontab
|
|
121
|
+
|
|
122
|
+
CELERYBEAT_SCHEDULE['aa_loa_sync_groups'] = {
|
|
123
|
+
'task': 'aa_loa.tasks.sync_loa_groups',
|
|
124
|
+
'schedule': crontab(minute='0', hour='0'),
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Restart your celery worker (`sudo systemctl restart supervisor`) and the system will automatically activate/deactivate LOAs every day at midnight!
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
aa_loa/__init__.py,sha256=k2KYa-hsxaQlMzBSrud0fEjnjx6snYIrQar-O2kGdNY,67
|
|
2
|
+
aa_loa/admin.py,sha256=AeQSpMdzmR5AJ_EOlp-of1I8wxxRgsXDzab9BkP2sW4,451
|
|
3
|
+
aa_loa/apps.py,sha256=yXJA1czRKGfCET_608L-PVy0mF8uYyqBObQPtf-YKEU,203
|
|
4
|
+
aa_loa/auth_hooks.py,sha256=MNgqbzKvX_NFlWO73SqgSdy5_plv-lzS5E6s4QA6vUE,709
|
|
5
|
+
aa_loa/forms.py,sha256=RfQrTtQ_Sl7HW0UxekIgrnZLYFwBdJGLk89qqQr_Mno,1910
|
|
6
|
+
aa_loa/models.py,sha256=30-K3earDj1euTAE6RuhhQk3l7dZq78Mc-ZZ52OCuP0,2433
|
|
7
|
+
aa_loa/signals.py,sha256=G0g1z4Vad9d5hhoIi_9fSJUdKDgFj4r1BcqPgcVXe9w,443
|
|
8
|
+
aa_loa/tasks.py,sha256=ExyT3jA3GEybtKhaQsIZ-jDYynGmqUjHd1Hrr1xB288,3511
|
|
9
|
+
aa_loa/urls.py,sha256=wS5cgoxbomwfuyxo5OfYOuJJZqpqEl9C5QFqCUulzuI,340
|
|
10
|
+
aa_loa/views.py,sha256=xabEaRT4q-LCYSf0iH0cYzv1_GWco2sJTuWfZOUmsVI,2673
|
|
11
|
+
aa_loa/migrations/0001_initial.py,sha256=43iiOgkYxgPfcuIKhE8tHDwCR5KX6yZHhleL6qwXVfA,2564
|
|
12
|
+
aa_loa/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
|
+
aa_loa/templates/aa_loa/hr_dashboard.html,sha256=ZvUeJdCc89eASFdcZJhK8-VbuAdgchjP97lRwm02D2A,7027
|
|
14
|
+
aa_loa/templates/aa_loa/index.html,sha256=ZsKVWCMlOHDzLXsnBhkss8jpFO2E-cfjVA5VbfjprQM,6517
|
|
15
|
+
aa_loa-0.0.1.dist-info/METADATA,sha256=1F8PO2M6QDtAkYZYW17IL4Eb_9yECELvxmr4jHgqJo4,6116
|
|
16
|
+
aa_loa-0.0.1.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
17
|
+
aa_loa-0.0.1.dist-info/licenses/LICENSE,sha256=VpXurLL80VG0dI9a9tuQl8t4iSOxm_6dbRscbI9LVdY,1072
|
|
18
|
+
aa_loa-0.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Maddog Broekman
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|