Skip to content
Snippets Groups Projects
Commit 868a5198 authored by Nadja Geisler's avatar Nadja Geisler :sunny:
Browse files

Merge branch 'scheduling-constraints' into 'main'

Constraint Violation checking & visualization

See merge request kif/akplanning!99
parents 8d36e628 cef8782a
No related branches found
No related tags found
1 merge request!99Constraint Violation checking & visualization
Pipeline #22755 passed
......@@ -4,7 +4,7 @@ from django.contrib import admin
from django.contrib.admin import SimpleListFilter
from django.db.models import Count, F
from django.shortcuts import render, redirect
from django.urls import path, reverse_lazy
from django.urls import reverse_lazy
from django.utils import timezone
from django.utils.html import format_html
from django.utils.safestring import mark_safe
......@@ -16,11 +16,7 @@ from AKModel.availability.forms import AvailabilitiesFormMixin
from AKModel.availability.models import Availability
from AKModel.models import Event, AKOwner, AKCategory, AKTrack, AKTag, AKRequirement, AK, AKSlot, Room, AKOrgaMessage, \
ConstraintViolation
from AKModel.views import EventStatusView, AKCSVExportView, AKWikiExportView, AKMessageDeleteView, \
AKRequirementOverview, \
NewEventWizardStartView, NewEventWizardSettingsView, NewEventWizardPrepareImportView, NewEventWizardFinishView, \
NewEventWizardImportView, NewEventWizardActivateView
from AKModel.views import export_slides
from AKModel.urls import get_admin_urls_event_wizard, get_admin_urls_event
@admin.register(Event)
......@@ -32,42 +28,24 @@ class EventAdmin(admin.ModelAdmin):
ordering = ['-start']
def add_view(self, request, form_url='', extra_context=None):
# Always use wizard to create new events
# (the built-in form wouldn't work anyways since the timezone cannot be specified before starting to fill the form)
# Always use wizard to create new events (the built-in form wouldn't work anyways since the timezone cannot
# be specified before starting to fill the form)
return redirect("admin:new_event_wizard_start")
def get_urls(self):
urls = super().get_urls()
custom_urls = [
path('add/wizard/start/', self.admin_site.admin_view(NewEventWizardStartView.as_view()),
name="new_event_wizard_start"),
path('add/wizard/settings/', self.admin_site.admin_view(NewEventWizardSettingsView.as_view()),
name="new_event_wizard_settings"),
path('add/wizard/created/<slug:event_slug>/', self.admin_site.admin_view(NewEventWizardPrepareImportView.as_view()),
name="new_event_wizard_prepare_import"),
path('add/wizard/import/<slug:event_slug>/from/<slug:import_slug>/',
self.admin_site.admin_view(NewEventWizardImportView.as_view()),
name="new_event_wizard_import"),
path('add/wizard/activate/<slug:slug>/',
self.admin_site.admin_view(NewEventWizardActivateView.as_view()),
name="new_event_wizard_activate"),
path('add/wizard/finish/<slug:slug>/',
self.admin_site.admin_view(NewEventWizardFinishView.as_view()),
name="new_event_wizard_finish"),
path('<slug:slug>/status/', self.admin_site.admin_view(EventStatusView.as_view()), name="event_status"),
path('<slug:event_slug>/requirements/', self.admin_site.admin_view(AKRequirementOverview.as_view()), name="event_requirement_overview"),
path('<slug:event_slug>/ak-csv-export/', self.admin_site.admin_view(AKCSVExportView.as_view()), name="ak_csv_export"),
path('<slug:slug>/ak-wiki-export/', self.admin_site.admin_view(AKWikiExportView.as_view()), name="ak_wiki_export"),
path('<slug:event_slug>/ak-slide-export/', export_slides, name="ak_slide_export"),
path('<slug:slug>/delete-orga-messages/', self.admin_site.admin_view(AKMessageDeleteView.as_view()),
name="ak_delete_orga_messages"),
]
return custom_urls + urls
urls = get_admin_urls_event_wizard(self.admin_site)
urls.extend(get_admin_urls_event(self.admin_site))
if apps.is_installed("AKScheduling"):
from AKScheduling.urls import get_admin_urls_scheduling
urls.extend(get_admin_urls_scheduling(self.admin_site))
urls.extend(super().get_urls())
return urls
def status_url(self, obj):
return format_html("<a href='{url}'>{text}</a>",
url=reverse_lazy('admin:event_status', kwargs={'slug': obj.slug}), text=_("Status"))
status_url.short_description = text=_("Status")
status_url.short_description = _("Status")
def get_form(self, request, obj=None, change=False, **kwargs):
# Use timezone of event
......@@ -116,18 +94,6 @@ class AKTrackAdmin(admin.ModelAdmin):
kwargs['initial'] = Event.get_next_active()
return super(AKTrackAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)
def get_urls(self):
urls = super().get_urls()
custom_urls = []
if apps.is_installed("AKScheduling"):
from AKScheduling.views import TrackAdminView
custom_urls.extend([
path('<slug:event_slug>/manage/', self.admin_site.admin_view(TrackAdminView.as_view()),
name="tracks_manage"),
])
return custom_urls + urls
@admin.register(AKTag)
class AKTagAdmin(admin.ModelAdmin):
......@@ -240,7 +206,6 @@ class RoomForm(AvailabilitiesFormMixin, forms.ModelForm):
self.fields["properties"].queryset = AKRequirement.objects.filter(event=self.instance.event)
@admin.register(Room)
class RoomAdmin(admin.ModelAdmin):
model = Room
......@@ -281,20 +246,6 @@ class AKSlotAdmin(admin.ModelAdmin):
readonly_fields = ['ak_details_link', 'updated']
form = AKSlotAdminForm
def get_urls(self):
urls = super().get_urls()
custom_urls = []
if apps.is_installed("AKScheduling"):
from AKScheduling.views import SchedulingAdminView, UnscheduledSlotsAdminView
custom_urls.extend([
path('<slug:event_slug>/schedule/', self.admin_site.admin_view(SchedulingAdminView.as_view()),
name="schedule"),
path('<slug:event_slug>/unscheduled/', self.admin_site.admin_view(UnscheduledSlotsAdminView.as_view()),
name="slots_unscheduled"),
])
return custom_urls + urls
def get_form(self, request, obj=None, change=False, **kwargs):
# Use timezone of associated event
if obj is not None and obj.event.timezone:
......@@ -310,10 +261,11 @@ class AKSlotAdmin(admin.ModelAdmin):
return super(AKSlotAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)
def ak_details_link(self, akslot):
if apps.is_installed("AKScheduling") and akslot.ak is not None:
if apps.is_installed("AKSubmission") and akslot.ak is not None:
link = f"<a href={reverse('submit:ak_detail', args=[akslot.event.slug, akslot.ak.pk])}>{str(akslot.ak)}</a>"
return mark_safe(link)
return "-"
ak_details_link.short_description = _('AK Details')
......
......@@ -7,6 +7,7 @@ from django_tex.environment import environment
# and would hence cause compilation errors
utf8_replace_pattern = re.compile(u'[^\u0000-\u206F]', re.UNICODE)
def latex_escape_utf8(value):
"""
Escape latex special chars and remove invalid utf-8 values
......@@ -16,7 +17,10 @@ def latex_escape_utf8(value):
:return: escaped string
:rtype: str
"""
return utf8_replace_pattern.sub('', value).replace('&', '\&').replace('_', '\_').replace('#', '\#').replace('$', '\$').replace('%', '\%').replace('{', '\{').replace('}', '\}')
return utf8_replace_pattern.sub('', value).replace('&', '\&').replace('_', '\_').replace('#', '\#').replace('$',
'\$').replace(
'%', '\%').replace('{', '\{').replace('}', '\}')
def improved_tex_environment(**options):
env = environment(**options)
......
......@@ -360,7 +360,8 @@ class AKSlot(models.Model):
duration = models.DecimalField(max_digits=4, decimal_places=2, default=2, verbose_name=_('Duration'),
help_text=_('Length in hours'))
fixed = models.BooleanField(default=False, verbose_name=_('Scheduling fixed'), help_text=_('Length and time of this AK should not be changed'))
fixed = models.BooleanField(default=False, verbose_name=_('Scheduling fixed'),
help_text=_('Length and time of this AK should not be changed'))
event = models.ForeignKey(to=Event, on_delete=models.CASCADE, verbose_name=_('Event'),
help_text=_('Associated event'))
......@@ -423,6 +424,9 @@ class AKSlot(models.Model):
"""
return (timezone.now() - self.updated).total_seconds()
def overlaps(self, other: "AKSlot"):
return self.start <= other.end <= self.end or self.start <= other.start <= self.end
class AKOrgaMessage(models.Model):
ak = models.ForeignKey(to=AK, on_delete=models.CASCADE, verbose_name=_('AK'),
......@@ -494,7 +498,12 @@ class ConstraintViolation(models.Model):
help_text=_('Mark this violation manually as resolved'))
fields = ['ak_owner', 'room', 'requirement', 'category']
fields_mm = ['aks', 'ak_slots']
fields_mm = ['_aks', '_ak_slots']
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.aks_tmp = set()
self.ak_slots_tmp = set()
def get_details(self):
"""
......@@ -502,10 +511,10 @@ class ConstraintViolation(models.Model):
:return: string of details
:rtype: str
"""
output = []
# Stringify all ManyToMany fields
for field_mm in self.fields_mm:
output.append(f"{field_mm}: {', '.join(str(a) for a in getattr(self, field_mm).all())}")
# Stringify aks and ak slots fields (m2m)
output = [f"{_('AKs')}: {self._aks_str}",
f"{_('AK Slots')}: {self._ak_slots_str}"]
# Stringify all other fields
for field in self.fields:
a = getattr(self, field, None)
......@@ -515,5 +524,102 @@ class ConstraintViolation(models.Model):
get_details.short_description = _('Details')
@property
def details(self):
return self.get_details()
@property
def level_display(self):
return self.get_level_display()
@property
def type_display(self):
return self.get_type_display()
@property
def timestamp_display(self):
return self.timestamp.astimezone(self.event.timezone).strftime('%d.%m.%y %H:%M')
@property
def _aks(self):
"""
Get all AKs belonging to this constraint violation
The distinction between real and tmp relationships is needed since many to many
relations only work for objects already persisted in the database
:return: set of all AKs belonging to this constraint violation
:rtype: set(AK)
"""
if self.pk and self.pk > 0:
return set(self.aks.all())
return self.aks_tmp
@property
def _aks_str(self):
if self.pk and self.pk > 0:
return ', '.join(str(a) for a in self.aks.all())
return ', '.join(str(a) for a in self.aks_tmp)
@property
def _ak_slots(self):
"""
Get all AK Slots belonging to this constraint violation
The distinction between real and tmp relationships is needed since many to many
relations only work for objects already persisted in the database
:return: set of all AK Slots belonging to this constraint violation
:rtype: set(AKSlot)
"""
if self.pk and self.pk > 0:
return set(self.ak_slots.all())
return self.ak_slots_tmp
@property
def _ak_slots_str(self):
if self.pk and self.pk > 0:
return ', '.join(str(a) for a in self.ak_slots.all())
return ', '.join(str(a) for a in self.ak_slots_tmp)
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
# Store temporary m2m-relations in db
for ak in self.aks_tmp:
self.aks.add(ak)
for ak_slot in self.ak_slots_tmp:
self.ak_slots.add(ak_slot)
def __str__(self):
return f"{self.get_level_display()}: {self.get_type_display()} [{self.get_details()}]"
def matches(self, other):
"""
Check whether one constraint violation instance matches another,
this means has the same type, room, requirement, owner, category
as well as the same lists of aks and ak slots.
PK, timestamp, comments and manual resolving are ignored.
:param other: second instance to compare to
:type other: ConstraintViolation
:return: true if both instances are similar in the way described, false if not
:rtype: bool
"""
if not isinstance(other, ConstraintViolation):
return False
# Check type
if self.type != other.type:
return False
# Make sure both have the same aks and ak slots
for field_mm in self.fields_mm:
s: set = getattr(self, field_mm)
o: set = getattr(other, field_mm)
if len(s) != len(o):
return False
if len(s.intersection(o)) != len(s):
return False
# Check other "defining" fields
for field in self.fields:
if getattr(self, field) != getattr(other, field):
return False
return True
......@@ -74,6 +74,10 @@
<a class="btn btn-success"
href="{% url 'admin:schedule' event_slug=event.slug %}">{% trans "Scheduling" %}</a>
{% if "AKScheduling | is_installed" %}
<a class="btn btn-success"
href="{% url 'admin:constraint-violations' slug=event.slug %}">{% trans "Constraint Violations" %} <span class="badge badge-secondary">{{ event.constraintviolation_set.count }}</span></a>
{% endif %}
<a class="btn btn-success"
href="{% url 'admin:tracks_manage' event_slug=event.slug %}">{% trans "Manage ak tracks" %}</a>
<a class="btn btn-success"
......
......@@ -3,6 +3,9 @@ from django.urls import include, path
from rest_framework.routers import DefaultRouter
from AKModel import views
from AKModel.views import NewEventWizardStartView, NewEventWizardSettingsView, NewEventWizardPrepareImportView, \
NewEventWizardImportView, NewEventWizardActivateView, NewEventWizardFinishView, EventStatusView, \
AKRequirementOverview, AKCSVExportView, AKWikiExportView, AKMessageDeleteView, export_slides
api_router = DefaultRouter()
api_router.register('akowner', views.AKOwnerViewSet, basename='AKOwner')
......@@ -12,29 +15,29 @@ api_router.register('ak', views.AKViewSet, basename='AK')
api_router.register('room', views.RoomViewSet, basename='Room')
api_router.register('akslot', views.AKSlotViewSet, basename='AKSlot')
extra_paths = []
if apps.is_installed("AKScheduling"):
from AKScheduling.api import ResourcesViewSet, RoomAvailabilitiesView, EventsView, EventsViewSet
from AKScheduling.api import ResourcesViewSet, RoomAvailabilitiesView, EventsView, EventsViewSet, \
ConstraintViolationsViewSet
api_router.register('scheduling-resources', ResourcesViewSet, basename='scheduling-resources')
api_router.register('scheduling-event', EventsViewSet, basename='scheduling-event')
api_router.register('scheduling-constraint-violations', ConstraintViolationsViewSet,
basename='scheduling-constraint-violations')
extra_paths = [
path('api/scheduling-events/', EventsView.as_view(), name='scheduling-events'),
path('api/scheduling-room-availabilities/', RoomAvailabilitiesView.as_view(), name='scheduling-room-availabilities'),
path('api/scheduling-room-availabilities/', RoomAvailabilitiesView.as_view(),
name='scheduling-room-availabilities'),
]
event_specific_paths = [
path('api/', include(api_router.urls), name='api'),
]
event_specific_paths.extend(extra_paths)
app_name = 'model'
urlpatterns = [
path(
'<slug:event_slug>/',
......@@ -42,3 +45,39 @@ urlpatterns = [
),
path('user/', views.UserView.as_view(), name="user"),
]
def get_admin_urls_event_wizard(admin_site):
return [
path('add/wizard/start/', admin_site.admin_view(NewEventWizardStartView.as_view()),
name="new_event_wizard_start"),
path('add/wizard/settings/', admin_site.admin_view(NewEventWizardSettingsView.as_view()),
name="new_event_wizard_settings"),
path('add/wizard/created/<slug:event_slug>/', admin_site.admin_view(NewEventWizardPrepareImportView.as_view()),
name="new_event_wizard_prepare_import"),
path('add/wizard/import/<slug:event_slug>/from/<slug:import_slug>/',
admin_site.admin_view(NewEventWizardImportView.as_view()),
name="new_event_wizard_import"),
path('add/wizard/activate/<slug:slug>/',
admin_site.admin_view(NewEventWizardActivateView.as_view()),
name="new_event_wizard_activate"),
path('add/wizard/finish/<slug:slug>/',
admin_site.admin_view(NewEventWizardFinishView.as_view()),
name="new_event_wizard_finish"),
]
def get_admin_urls_event(admin_site):
return [
path('<slug:slug>/status/', admin_site.admin_view(EventStatusView.as_view()), name="event_status"),
path('<slug:event_slug>/requirements/', admin_site.admin_view(AKRequirementOverview.as_view()),
name="event_requirement_overview"),
path('<slug:event_slug>/ak-csv-export/', admin_site.admin_view(AKCSVExportView.as_view()),
name="ak_csv_export"),
path('<slug:slug>/ak-wiki-export/', admin_site.admin_view(AKWikiExportView.as_view()),
name="ak_wiki_export"),
path('<slug:slug>/delete-orga-messages/', admin_site.admin_view(AKMessageDeleteView.as_view()),
name="ak_delete_orga_messages"),
path('<slug:event_slug>/ak-slide-export/', export_slides, name="ak_slide_export"),
]
......@@ -7,8 +7,8 @@ from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse_lazy
from django.utils.translation import gettext_lazy as _
from django.views.generic import TemplateView, DetailView, ListView, DeleteView, CreateView, FormView, UpdateView
from rest_framework import viewsets, permissions, mixins
from django_tex.shortcuts import render_to_pdf
from rest_framework import viewsets, permissions, mixins
from AKModel.forms import NewEventWizardStartForm, NewEventWizardSettingsForm, NewEventWizardPrepareImportForm, \
NewEventWizardImportForm, NewEventWizardActivateForm
......
......@@ -7,7 +7,7 @@ from django.views.generic import ListView
from rest_framework import viewsets, mixins, serializers, permissions
from AKModel.availability.models import Availability
from AKModel.models import Room, AKSlot
from AKModel.models import Room, AKSlot, ConstraintViolation
from AKModel.views import EventSlugMixin
......@@ -109,3 +109,20 @@ class EventsViewSet(EventSlugMixin, viewsets.ModelViewSet):
def get_queryset(self):
return AKSlot.objects.filter(event=self.event)
class ConstraintViolationSerializer(serializers.ModelSerializer):
class Meta:
model = ConstraintViolation
fields = ['pk', 'type_display', 'aks', 'ak_slots', 'ak_owner', 'room', 'requirement', 'category', 'comment', 'timestamp_display', 'manually_resolved', 'level_display', 'details']
class ConstraintViolationsViewSet(EventSlugMixin, viewsets.ModelViewSet):
permission_classes = (permissions.DjangoModelPermissions,)
serializer_class = ConstraintViolationSerializer
def get_object(self):
return get_object_or_404(ConstraintViolation, pk=self.kwargs["pk"])
def get_queryset(self):
return ConstraintViolation.objects.filter(event=self.event)
......@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-04-29 22:48+0000\n"
"POT-Creation-Date: 2021-05-09 18:23+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
......@@ -17,49 +17,92 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:11
#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:11
#: AKScheduling/templates/admin/AKScheduling/scheduling.html:10
msgid "Scheduling for"
msgstr "Scheduling für"
#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:126
#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:74
#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:128
msgid "No violations"
msgstr "Keine Verletzungen"
#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:81
msgid "Cannot load current violations from server"
msgstr "Kann die aktuellen Verletzungen nicht vom Server laden"
#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:106
msgid "Violation(s)"
msgstr "Verletzung(en)"
#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:109
msgid "Auto reload?"
msgstr "Automatisch neu laden?"
#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:113
msgid "Reload now"
msgstr "Jetzt neu laden"
#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:118
msgid "Violation"
msgstr "Verletzung"
#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:119
msgid "Problem"
msgstr "Problem"
#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:120
msgid "Details"
msgstr "Details"
#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:121
msgid "Since"
msgstr "Seit"
#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:134
#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:243
#: AKScheduling/templates/admin/AKScheduling/scheduling.html:197
#: AKScheduling/templates/admin/AKScheduling/unscheduled.html:34
msgid "Event Status"
msgstr "Event-Status"
#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:136
msgid "Scheduling"
msgstr "Scheduling"
#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:129
msgid "Name of new ak track"
msgstr "Name des neuen AK-Tracks"
#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:142
#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:145
msgid "Could not create ak track"
msgstr "Konnte neuen AK-Track nicht anlegen"
#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:168
#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:171
msgid "Could not update ak track name"
msgstr "Konnte Namen des AK-Tracks nicht ändern"
#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:174
#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:177
msgid "Do you really want to delete this ak track?"
msgstr "Soll dieser AK-Track wirklich gelöscht werden?"
#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:188
#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:191
msgid "Could not delete ak track"
msgstr "AK-Track konnte nicht gelöscht werden"
#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:200
#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:203
msgid "Manage AK Tracks"
msgstr "AK-Tracks verwalten"
#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:201
#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:204
msgid "Add ak track"
msgstr "AK-Track hinzufügen"
#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:206
#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:209
msgid "AKs without track"
msgstr "AKs ohne Track"
#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:240
#: AKScheduling/templates/admin/AKScheduling/scheduling.html:197
#: AKScheduling/templates/admin/AKScheduling/unscheduled.html:34
msgid "Event Status"
msgstr "Event-Status"
#: AKScheduling/templates/admin/AKScheduling/scheduling.html:87
msgid "Day (Horizontal)"
msgstr "Tag (horizontal)"
......
# Create your models here.
from django.db.models.signals import post_save, m2m_changed
from django.dispatch import receiver
from AKModel.availability.models import Availability
from AKModel.models import AK, AKSlot, Room, Event, AKOwner, ConstraintViolation
def update_constraint_violations(new_violations, existing_violations_to_check):
"""
Update existing constraint violations (subset for which new violations were computed) based on these new violations.
This will add all new violations without a match, preserve the matching ones
and delete the obsolete ones (those without a match from the newly calculated violations).
:param new_violations: list of new (not yet saved) violations that exist after the last change
:type new_violations: list[ConstraintViolation]
:param existing_violations_to_check: list of related violations currently in the db
:type existing_violations_to_check: list[ConstraintViolation]
"""
for new_violation in new_violations:
found_match = False
for existing_violation in existing_violations_to_check:
if existing_violation.matches(new_violation):
# Remove from existing violations set since it should stay in db
existing_violations_to_check.remove(existing_violation)
found_match = True
break
# Only save new violation if no match was found
if not found_match:
new_violation.save()
# Cleanup obsolete violations (ones without matches computed under current conditions)
for outdated_violation in existing_violations_to_check:
outdated_violation.delete()
def update_cv_reso_deadline_for_slot(slot):
"""
Update constraint violation AK_AFTER_RESODEADLINE for given slot
:param slot: slot to check/update
:type slot: AKSlot
"""
event = slot.event
if slot.ak.reso and slot.event.reso_deadline and slot.start:
violation_type = ConstraintViolation.ViolationType.AK_AFTER_RESODEADLINE
new_violations = []
if slot.end > event.reso_deadline:
c = ConstraintViolation(
type=violation_type,
level=ConstraintViolation.ViolationLevel.VIOLATION,
event=event,
)
c.aks_tmp.add(slot.ak)
c.ak_slots_tmp.add(slot)
new_violations.append(c)
update_constraint_violations(new_violations, list(slot.constraintviolation_set.filter(type=violation_type)))
@receiver(post_save, sender=AK)
def ak_changed_handler(sender, instance: AK, **kwargs):
# Changes might affect: Reso intention, Category, Interest
# TODO Reso intention changes
pass
# TODO adapt for Room's reauirements
@receiver(m2m_changed, sender=AK.owners.through)
def ak_owners_changed_handler(sender, instance: AK, action: str, **kwargs):
"""
Owners of AK changed
"""
# Only signal after change (post_add, post_delete, post_clear) are relevant
if not action.startswith("post"):
return
# print(f"{instance} changed")
event = instance.event
# Owner(s) changed: Might affect multiple AKs by the same owner(s) at the same time
violation_type = ConstraintViolation.ViolationType.OWNER_TWO_SLOTS
new_violations = []
slots_of_this_ak: [AKSlot] = instance.akslot_set.filter(start__isnull=False)
# For all owners (after recent change)...
for owner in instance.owners.all():
# ...find other slots that might be overlapping...
for ak in owner.ak_set.all():
# ...find overlapping slots...
if ak != instance:
for slot in slots_of_this_ak:
for other_slot in ak.akslot_set.filter(start__isnull=False):
if slot.overlaps(other_slot):
# ...and create a temporary violation if necessary...
c = ConstraintViolation(
type=violation_type,
level=ConstraintViolation.ViolationLevel.VIOLATION,
event=event,
ak_owner=owner
)
c.aks_tmp.add(instance)
c.aks_tmp.add(other_slot.ak)
c.ak_slots_tmp.add(slot)
c.ak_slots_tmp.add(other_slot)
new_violations.append(c)
print(f"{owner} has the following conflicts: {new_violations}")
# ... and compare to/update list of existing violations of this type
# belonging to the AK that was recently changed (important!)
existing_violations_to_check = list(instance.constraintviolation_set.filter(type=violation_type))
# print(existing_violations_to_check)
update_constraint_violations(new_violations, existing_violations_to_check)
@receiver(m2m_changed, sender=AK.conflicts.through)
def ak_conflicts_changed_handler(sender, instance: AK, action: str, **kwargs):
"""
Conflicts of AK changed
"""
# Only signal after change (post_add, post_delete, post_clear) are relevant
if not action.startswith("post"):
return
# print(f"{instance} changed")
event = instance.event
# Conflict(s) changed: Might affect multiple AKs that are conflicts of each other
violation_type = ConstraintViolation.ViolationType.AK_CONFLICT_COLLISION
new_violations = []
slots_of_this_ak: [AKSlot] = instance.akslot_set.filter(start__isnull=False)
conflicts_of_this_ak: [AK] = instance.conflicts.all()
for ak in conflicts_of_this_ak:
if ak != instance:
for other_slot in ak.akslot_set.filter(start__isnull=False):
for slot in slots_of_this_ak:
# ...find overlapping slots...
if slot.overlaps(other_slot):
# ...and create a temporary violation if necessary...
c = ConstraintViolation(
type=violation_type,
level=ConstraintViolation.ViolationLevel.VIOLATION,
event=event,
)
c.aks_tmp.add(instance)
c.ak_slots_tmp.add(slot)
c.ak_slots_tmp.add(other_slot)
new_violations.append(c)
print(f"{instance} has the following conflicts: {new_violations}")
# ... and compare to/update list of existing violations of this type
# belonging to the AK that was recently changed (important!)
existing_violations_to_check = list(instance.constraintviolation_set.filter(type=violation_type))
# print(existing_violations_to_check)
update_constraint_violations(new_violations, existing_violations_to_check)
@receiver(m2m_changed, sender=AK.prerequisites.through)
def ak_prerequisites_changed_handler(sender, instance: AK, action: str, **kwargs):
"""
Prerequisites of AK changed
"""
# Only signal after change (post_add, post_delete, post_clear) are relevant
if not action.startswith("post"):
return
# print(f"{instance} changed")
event = instance.event
# Conflict(s) changed: Might affect multiple AKs that are conflicts of each other
violation_type = ConstraintViolation.ViolationType.AK_BEFORE_PREREQUISITE
new_violations = []
slots_of_this_ak: [AKSlot] = instance.akslot_set.filter(start__isnull=False)
prerequisites_of_this_ak: [AK] = instance.prerequisites.all()
for ak in prerequisites_of_this_ak:
if ak != instance:
for other_slot in ak.akslot_set.filter(start__isnull=False):
for slot in slots_of_this_ak:
# ...find overlapping slots...
if other_slot.end > slot.start:
# ...and create a temporary violation if necessary...
c = ConstraintViolation(
type=violation_type,
level=ConstraintViolation.ViolationLevel.VIOLATION,
event=event,
)
c.aks_tmp.add(instance)
c.ak_slots_tmp.add(slot)
c.ak_slots_tmp.add(other_slot)
new_violations.append(c)
print(f"{instance} has the following conflicts: {new_violations}")
# ... and compare to/update list of existing violations of this type
# belonging to the AK that was recently changed (important!)
existing_violations_to_check = list(instance.constraintviolation_set.filter(type=violation_type))
# print(existing_violations_to_check)
update_constraint_violations(new_violations, existing_violations_to_check)
@receiver(m2m_changed, sender=AK.requirements.through)
def ak_requirements_changed_handler(sender, instance: AK, action: str, **kwargs):
"""
Requirements of AK changed
"""
# Only signal after change (post_add, post_delete, post_clear) are relevant
if not action.startswith("post"):
return
# print(f"{instance} changed")
event = instance.event
# Requirement(s) changed: Might affect slots and rooms
violation_type = ConstraintViolation.ViolationType.REQUIRE_NOT_GIVEN
new_violations = []
slots_of_this_ak: [AKSlot] = instance.akslot_set.filter(start__isnull=False)
# For all requirements (after recent change)...
for slot in slots_of_this_ak:
room = slot.room
room_requirements = room.properties.all()
for requirement in instance.requirements.all():
if not requirement in room_requirements:
# ...and create a temporary violation if necessary...
c = ConstraintViolation(
type=violation_type,
level=ConstraintViolation.ViolationLevel.VIOLATION,
event=event,
requirement=requirement,
room=room,
)
c.aks_tmp.add(instance)
c.ak_slots_tmp.add(slot)
new_violations.append(c)
print(f"{instance} has the following conflicts: {new_violations}")
# ... and compare to/update list of existing violations of this type
# belonging to the AK that was recently changed (important!)
existing_violations_to_check = list(instance.constraintviolation_set.filter(type=violation_type))
# print(existing_violations_to_check)
update_constraint_violations(new_violations, existing_violations_to_check)
@receiver(post_save, sender=AKSlot)
def akslot_changed_handler(sender, instance: AKSlot, **kwargs):
# Changes might affect: Duplicate parallel, Two in room, Resodeadline
print(f"{sender} changed")
event = instance.event
# == Check for two parallel slots by one of the owners ==
violation_type = ConstraintViolation.ViolationType.OWNER_TWO_SLOTS
new_violations = []
# For all owners (after recent change)...
for owner in instance.ak.owners.all():
# ...find other slots that might be overlapping...
for ak in owner.ak_set.all():
# ...find overlapping slots...
if ak != instance.ak:
for other_slot in ak.akslot_set.filter(start__isnull=False):
if instance.overlaps(other_slot):
# ...and create a temporary violation if necessary...
c = ConstraintViolation(
type=violation_type,
level=ConstraintViolation.ViolationLevel.VIOLATION,
event=event,
ak_owner=owner
)
c.aks_tmp.add(instance.ak)
c.aks_tmp.add(other_slot.ak)
c.ak_slots_tmp.add(instance)
c.ak_slots_tmp.add(other_slot)
new_violations.append(c)
print(f"{owner} has the following conflicts: {new_violations}")
# ... and compare to/update list of existing violations of this type
# belonging to the AK that was recently changed (important!)
existing_violations_to_check = list(instance.constraintviolation_set.filter(type=violation_type))
# print(existing_violations_to_check)
update_constraint_violations(new_violations, existing_violations_to_check)
# == Check for two aks in the same room at the same time ==
violation_type = ConstraintViolation.ViolationType.ROOM_TWO_SLOTS
new_violations = []
# For all slots in this room...
if instance.room:
for other_slot in instance.room.akslot_set.all():
if other_slot != instance:
# ... find overlapping slots...
if instance.overlaps(other_slot):
# ...and create a temporary violation if necessary...
c = ConstraintViolation(
type=violation_type,
level=ConstraintViolation.ViolationLevel.WARNING,
event=event,
room=instance.room
)
c.aks_tmp.add(instance.ak)
c.aks_tmp.add(other_slot.ak)
c.ak_slots_tmp.add(instance)
c.ak_slots_tmp.add(other_slot)
new_violations.append(c)
print(f"Multiple slots in room {instance.room}: {new_violations}")
# ... and compare to/update list of existing violations of this type
# belonging to the slot that was recently changed (important!)
existing_violations_to_check = list(instance.constraintviolation_set.filter(type=violation_type))
# print(existing_violations_to_check)
update_constraint_violations(new_violations, existing_violations_to_check)
# == Check for reso ak after reso deadline ==
update_cv_reso_deadline_for_slot(instance)
# == Check for two slots of the same AK at the same time (warning) ==
violation_type = ConstraintViolation.ViolationType.AK_SLOT_COLLISION
new_violations = []
# For all other slots of this ak...
for other_slot in instance.ak.akslot_set.filter(start__isnull=False):
if other_slot != instance:
# ... find overlapping slots...
if instance.overlaps(other_slot):
# ...and create a temporary violation if necessary...
c = ConstraintViolation(
type=violation_type,
level=ConstraintViolation.ViolationLevel.WARNING,
event=event,
)
c.aks_tmp.add(instance.ak)
c.ak_slots_tmp.add(instance)
c.ak_slots_tmp.add(other_slot)
new_violations.append(c)
# ... and compare to/update list of existing violations of this type
# belonging to the slot that was recently changed (important!)
existing_violations_to_check = list(instance.constraintviolation_set.filter(type=violation_type))
update_constraint_violations(new_violations, existing_violations_to_check)
# == Check for slot outside availability ==
# An AK's availability changed: Might affect AK slots scheduled outside the permitted time
violation_type = ConstraintViolation.ViolationType.SLOT_OUTSIDE_AVAIL
new_violations = []
if instance.start:
availabilities_of_this_ak: [Availability] = instance.ak.availabilities.all()
covered = False
for availability in availabilities_of_this_ak:
covered = availability.start <= instance.start and availability.end >= instance.end
if covered:
break
if not covered:
c = ConstraintViolation(
type=violation_type,
level=ConstraintViolation.ViolationLevel.VIOLATION,
event=event
)
c.aks_tmp.add(instance.ak)
c.ak_slots_tmp.add(instance)
new_violations.append(c)
print(f"{instance.ak} has the following slots outside availabilities: {new_violations}")
# ... and compare to/update list of existing violations of this type
# belonging to the AK that was recently changed (important!)
existing_violations_to_check = list(instance.constraintviolation_set.filter(type=violation_type))
# print(existing_violations_to_check)
update_constraint_violations(new_violations, existing_violations_to_check)
# == Check for requirement not fulfilled by room ==
# Room(s) changed: Might affect slots and rooms
violation_type = ConstraintViolation.ViolationType.REQUIRE_NOT_GIVEN
new_violations = []
if instance.room:
room_requirements = instance.room.properties.all()
for requirement in instance.ak.requirements.all():
if requirement not in room_requirements:
# ...and create a temporary violation if necessary...
c = ConstraintViolation(
type=violation_type,
level=ConstraintViolation.ViolationLevel.VIOLATION,
event=event,
requirement=requirement,
room=instance.room,
)
c.aks_tmp.add(instance.ak)
c.ak_slots_tmp.add(instance)
new_violations.append(c)
print(f"{instance} has the following conflicts: {new_violations}")
# ... and compare to/update list of existing violations of this type
# belonging to the AK that was recently changed (important!)
existing_violations_to_check = list(instance.constraintviolation_set.filter(type=violation_type))
# print(existing_violations_to_check)
update_constraint_violations(new_violations, existing_violations_to_check)
# == check for simultaneous slots of conflicting AKs ==
violation_type = ConstraintViolation.ViolationType.AK_CONFLICT_COLLISION
new_violations = []
if instance.start:
conflicts_of_this_ak: [AK] = instance.ak.conflicts.all()
for ak in conflicts_of_this_ak:
if ak != instance.ak:
for other_slot in ak.akslot_set.filter(start__isnull=False):
# ...find overlapping slots...
if instance.overlaps(other_slot):
# ...and create a temporary violation if necessary...
c = ConstraintViolation(
type=violation_type,
level=ConstraintViolation.ViolationLevel.VIOLATION,
event=event,
)
c.aks_tmp.add(instance.ak)
c.ak_slots_tmp.add(instance)
c.ak_slots_tmp.add(other_slot)
new_violations.append(c)
print(f"{instance} has the following conflicts: {new_violations}")
# ... and compare to/update list of existing violations of this type
# belonging to the AK that was recently changed (important!)
existing_violations_to_check = list(instance.ak.constraintviolation_set.filter(type=violation_type))
# print(existing_violations_to_check)
update_constraint_violations(new_violations, existing_violations_to_check)
# == check for missing prerequisites ==
violation_type = ConstraintViolation.ViolationType.AK_BEFORE_PREREQUISITE
new_violations = []
if instance.start:
prerequisites_of_this_ak: [AK] = instance.ak.prerequisites.all()
for ak in prerequisites_of_this_ak:
if ak != instance.ak:
for other_slot in ak.akslot_set.filter(start__isnull=False):
# ...find slots in the wrong order...
if other_slot.end > instance.start:
# ...and create a temporary violation if necessary...
c = ConstraintViolation(
type=violation_type,
level=ConstraintViolation.ViolationLevel.VIOLATION,
event=event,
)
c.aks_tmp.add(instance.ak)
c.ak_slots_tmp.add(instance)
c.ak_slots_tmp.add(other_slot)
new_violations.append(c)
print(f"{instance} has the following conflicts: {new_violations}")
# ... and compare to/update list of existing violations of this type
# belonging to the AK that was recently changed (important!)
existing_violations_to_check = list(instance.ak.constraintviolation_set.filter(type=violation_type))
# print(existing_violations_to_check)
update_constraint_violations(new_violations, existing_violations_to_check)
@receiver(post_save, sender=Room)
def room_changed_handler(sender, **kwargs):
# Changes might affect: Room size
print(f"{sender} changed")
@receiver(post_save, sender=Availability)
def availability_changed_handler(sender, instance: Availability, **kwargs):
# Changes might affect: category availability, AK availability, Room availability
print(f"{instance} changed")
event = instance.event
# An AK's availability changed: Might affect AK slots scheduled outside the permitted time
if instance.ak:
violation_type = ConstraintViolation.ViolationType.SLOT_OUTSIDE_AVAIL
new_violations = []
availabilities_of_this_ak: [Availability] = instance.ak.availabilities.all()
slots_of_this_ak: [AKSlot] = instance.ak.akslot_set.filter(start__isnull=False)
for slot in slots_of_this_ak:
covered = False
for availability in availabilities_of_this_ak:
covered = availability.start <= slot.start and availability.end >= slot.end
if covered:
break
if not covered:
c = ConstraintViolation(
type=violation_type,
level=ConstraintViolation.ViolationLevel.VIOLATION,
event=event
)
c.aks_tmp.add(instance.ak)
c.ak_slots_tmp.add(slot)
new_violations.append(c)
print(f"{instance.ak} has the following slots putside availabilities: {new_violations}")
# ... and compare to/update list of existing violations of this type
# belonging to the AK that was recently changed (important!)
existing_violations_to_check = list(instance.ak.constraintviolation_set.filter(type=violation_type))
# print(existing_violations_to_check)
update_constraint_violations(new_violations, existing_violations_to_check)
@receiver(post_save, sender=Event)
def event_changed_handler(sender, instance, **kwargs):
# == Check for reso ak after reso deadline (which might have changed) ==
if instance.reso_deadline:
for slot in instance.akslot_set.filter(start__isnull=False, ak__reso=True):
update_cv_reso_deadline_for_slot(slot)
{% extends "admin/base_site.html" %}
{% load tags_AKModel %}
{% load i18n %}
{% load l10n %}
{% load tz %}
{% load static %}
{% load tags_AKPlan %}
{% load fontawesome_5 %}
{% block title %}{% trans "Scheduling for" %} {{event}}{% endblock %}
{% block extrahead %}
{{ block.super }}
<script>
document.addEventListener('DOMContentLoaded', function () {
// CSRF Protection/Authentication
function getCookie(name) {
let cookieValue = null;
if (document.cookie && document.cookie !== '') {
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
// Does this cookie string begin with the name we want?
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
const csrftoken = getCookie('csrftoken');
function csrfSafeMethod(method) {
// these HTTP methods do not require CSRF protection
return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
}
$.ajaxSetup({
beforeSend: function (xhr, settings) {
if (!csrfSafeMethod(settings.type) && !this.crossDomain) {
xhr.setRequestHeader("X-CSRFToken", csrftoken);
}
}
});
// (Re-)Load constraint violations using AJAX and visualize using violation count badge and violation table
function reload() {
$.ajax({
url: "{% url "model:scheduling-constraint-violations-list" event_slug=event.slug %}",
type: 'GET',
success: function (response) {
console.log(response);
let table_html = '';
if(response.length > 0) {
// Update violation count badge
$('#violationCountBadge').html(response.length).removeClass('badge-success').addClass('badge-warning');
// Update violations table
for(let i=0;i<response.length;i++) {
table_html += "<tr><td>" + response[i].level_display + "</td><td>" + response[i].type_display + "</td><td>" + response[i].details + "</td><td>" + response[i].timestamp_display + "</td><td></td></tr>";
}
}
else {
// Update violation count badge
$('#violationCountBadge').html(0).removeClass('badge-warning').addClass('badge-success');
// Update violations table
table_html ='<tr class="text-muted"><td colspan="5" class="text-center">{% trans "No violations" %}</td></tr>'
}
// Show violation list (potentially empty) in violations table
$('#violationsTableBody').html(table_html);
},
error: function (response) {
alert("{% trans 'Cannot load current violations from server' %}");
}
});
}
reload();
// Bind reload button
$('#btnReloadNow').click(reload);
// Toggle automatic reloading (every 30 s) based on checkbox
let autoReloadInterval = undefined;
$('#cbxAutoReload').change(function () {
if(this.checked) {
autoReloadInterval = setInterval(reload, 30*1000);
}
else {
if(autoReloadInterval !== undefined)
clearInterval(autoReloadInterval);
}
});
});
</script>
{% endblock extrahead %}
{% block content %}
<h4 class="mt-4 mb-4"><span id="violationCountBadge" class="badge badge-success">0</span> {% trans "Violation(s)" %}</h4>
<input type="checkbox" id="cbxAutoReload">
<label for="cbxAutoReload">{% trans "Auto reload?" %}</label>
<br>
<a href="#" id="btnReloadNow" class="btn btn-info">{% fa5_icon "sync-alt" "fas" %} {% trans "Reload now" %}</a>
<table class="table table-striped mt-4 mb-4">
<thead>
<tr>
<th>{% trans "Violation" %}</th>
<th>{% trans "Problem" %}</th>
<th>{% trans "Details" %}</th>
<th>{% trans "Since" %}</th>
<th></th>
</tr>
</thead>
<tbody id="violationsTableBody">
<tr class="text-muted">
<td colspan="5" class="text-center">
{% trans "No violations" %}
</td>
</tr>
</tbody>
</table>
<a href="{% url 'admin:event_status' event.slug %}">{% trans "Event Status" %}</a>
&middot;
<a href="{% url 'admin:schedule' event.slug %}">{% trans "Scheduling" %}</a>
{% endblock %}
from django.urls import path
from AKScheduling.views import SchedulingAdminView, UnscheduledSlotsAdminView, TrackAdminView, \
ConstraintViolationsAdminView
def get_admin_urls_scheduling(admin_site):
return [
path('<slug:event_slug>/schedule/', admin_site.admin_view(SchedulingAdminView.as_view()),
name="schedule"),
path('<slug:event_slug>/unscheduled/', admin_site.admin_view(UnscheduledSlotsAdminView.as_view()),
name="slots_unscheduled"),
path('<slug:slug>/constraint-violations/', admin_site.admin_view(ConstraintViolationsAdminView.as_view()),
name="constraint-violations"),
path('<slug:event_slug>/tracks/', admin_site.admin_view(TrackAdminView.as_view()),
name="tracks_manage"),
]
from django.views.generic import ListView
from django.utils.translation import gettext_lazy as _
from django.views.generic import ListView, DetailView
from AKModel.models import AKSlot, AKTrack
from AKModel.models import AKSlot, AKTrack, Event
from AKModel.views import AdminViewMixin, FilterByEventSlugMixin
......@@ -47,3 +47,14 @@ class TrackAdminView(AdminViewMixin, FilterByEventSlugMixin, ListView):
context = super().get_context_data(object_list=object_list, **kwargs)
context["aks_without_track"] = self.event.ak_set.filter(track=None)
return context
class ConstraintViolationsAdminView(AdminViewMixin, DetailView):
template_name = "admin/AKScheduling/constraint_violations.html"
model = Event
context_object_name = "event"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["title"] = f"{_('Constraint violations for')} {context['event']}"
return context
......@@ -2,17 +2,18 @@
This repository contains a Django project with several apps.
## Requirements
AKPlanning has two types of requirements: System requirements are dependent on operating system and need to be installed manually beforehand. Python requirements will be installed inside a virtual environment (strongly recommended) during setup.
AKPlanning has two types of requirements: System requirements are dependent on operating system and need to be installed
manually beforehand. Python requirements will be installed inside a virtual environment (strongly recommended) during
setup.
### System Requirements
* Python 3.7 incl. development tools
* Virtualenv
* pdflatex & beamer class (`texlive-latex-base texlive-latex-recommended texlive-latex-extra texlive-fonts-extra texlive-luatex`)
* pdflatex & beamer
class (`texlive-latex-base texlive-latex-recommended texlive-latex-extra texlive-fonts-extra texlive-luatex`)
* for production using uwsgi:
* C compiler e.g. gcc
* uwsgi
......@@ -20,24 +21,20 @@ AKPlanning has two types of requirements: System requirements are dependent on o
* for production using Apache (in addition to uwsgi)
* the mod proxy uwsgi plugin for apache2
### Python Requirements
Python requirements are listed in ``requirements.txt``. They can be installed with pip using ``-r requirements.txt``.
## Development Setup
* create a new directory that should contain the files in future, e.g. ``mkdir AKPlanning``
* change into that directory ``cd AKPlanning``
* clone this repository ``git clone URL .``
### Automatic Setup
1. execute the setup bash script ``Utils/setup.sh``
### Manual Setup
1. setup a virtual environment using the proper python version ``virtualenv venv -p python3.7``
......@@ -49,7 +46,6 @@ Python requirements are listed in ``requirements.txt``. They can be installed wi
1. create a priviledged user, credentials are entered interactively on CLI ``python manage.py createsuperuser``
1. deactivate virtualenv ``deactivate``
### Development Server
**Do not use this for deployment!**
......@@ -60,11 +56,10 @@ To start the application for development, in the root directory,
1. start development server ``python manage.py runserver 0:8000``
1. In your browser, access ``http://127.0.0.1:8000/admin/`` and continue from there.
## Deployment Setup
This application can be deployed using a web server as any other Django application.
Remember to use a secret key that is not stored in any repository or similar, and disable DEBUG mode (``settings.py``).
This application can be deployed using a web server as any other Django application. Remember to use a secret key that
is not stored in any repository or similar, and disable DEBUG mode (``settings.py``).
**Step-by-Step Instructions**
......@@ -77,9 +72,12 @@ Remember to use a secret key that is not stored in any repository or similar, an
1. activate virtualenv ``source venv/bin/activate``
1. update tools ``pip install --upgrade setuptools pip wheel``
1. install python requirements ``pip install -r requirements.txt``
1. create the file ``AKPlanning/settings_secrets.py`` (copy from ``settings_secrets.py.sample``) and fill it with the necessary secrets (e.g. generated by ``tr -dc 'a-z0-9!@#$%^&*(-_=+)' < /dev/urandom | head -c50``) (it is a good idea to restrict read permissions from others)
1. create the file ``AKPlanning/settings_secrets.py`` (copy from ``settings_secrets.py.sample``) and fill it with the
necessary secrets (e.g. generated by ``tr -dc 'a-z0-9!@#$%^&*(-_=+)' < /dev/urandom | head -c50``) (it is a good idea
to restrict read permissions from others)
1. if necessary enable uwsgi proxy plugin for Apache e.g.``a2enmod proxy_uwsgi``
1. edit the apache config to serve the application and the static files, e.g. on a dedicated system in ``/etc/apache2/sites-enabled/000-default.conf`` within the ``VirtualHost`` tag add:
1. edit the apache config to serve the application and the static files, e.g. on a dedicated system
in ``/etc/apache2/sites-enabled/000-default.conf`` within the ``VirtualHost`` tag add:
```
Alias /static /srv/AKPlanning/static
......@@ -91,19 +89,25 @@ Remember to use a secret key that is not stored in any repository or similar, an
ProxyPass / uwsgi://127.0.0.1:3035/
```
or create a new config (.conf) file (similar to ``apache-akplanning.conf``) replacing $SUBDOMAIN with the subdomain the system should be available under, and $MAILADDRESS with the e-mail address of your administrator and $PATHTO with the appropriate paths. Copy or symlink it to ``/etc/apache2/sites-available``. Then symlink it to ``sites-enabled`` e.g. by using ``ln -s /etc/apache2/sites-available/akplanning.conf /etc/apache2/sites-enabled/akplanning.conf``.
or create a new config (.conf) file (similar to ``apache-akplanning.conf``) replacing $SUBDOMAIN with the subdomain
the system should be available under, and $MAILADDRESS with the e-mail address of your administrator and $PATHTO with
the appropriate paths. Copy or symlink it to ``/etc/apache2/sites-available``. Then symlink it to ``sites-enabled``
e.g. by using ``ln -s /etc/apache2/sites-available/akplanning.conf /etc/apache2/sites-enabled/akplanning.conf``.
1. restart Apache ``sudo systemctl restart apache2.service``
1. create a dedicated user, e.g. ``adduser django``
1. transfer ownership of the folder to the new user ``chown -R django:django /srv/AKPlanning``
1. Copy or symlink the uwsgi config in ``uwsgi-akplanning.ini`` to ``/etc/uwsgi/apps-available/`` and then symlink it to ``/etc/uwsgi/apps-enabled/`` using e.g., ``ln -s /srv/AKPlanning/uwsgi-akplanning.ini /etc/uwsgi/apps-available/akplanning.ini`` and ``ln -s /etc/uwsgi/apps-available/akplanning.ini /etc/uwsgi/apps-enabled/akplanning.ini``
1. Copy or symlink the uwsgi config in ``uwsgi-akplanning.ini`` to ``/etc/uwsgi/apps-available/`` and then symlink it
to ``/etc/uwsgi/apps-enabled/`` using
e.g., ``ln -s /srv/AKPlanning/uwsgi-akplanning.ini /etc/uwsgi/apps-available/akplanning.ini``
and ``ln -s /etc/uwsgi/apps-available/akplanning.ini /etc/uwsgi/apps-enabled/akplanning.ini``
start uwsgi using the configuration file ``uwsgi --ini uwsgi-akplanning.ini``
1. restart uwsgi ``sudo systemctl restart uwsgi``
1. execute the update script ``./Utils/update.sh --prod``
## Updates
To update the setup to the current version on the main branch of the repository use the update script ``Utils/update.sh`` or ``Utils/update.sh --prod`` in production.
To update the setup to the current version on the main branch of the repository use the update
script ``Utils/update.sh`` or ``Utils/update.sh --prod`` in production.
Afterwards, you may check your setup by executing ``Utils/check.sh`` or ``Utils/check.sh --prod`` in production.
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment