Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • konstantin/akplanning
  • matedealer/akplanning
  • kif/akplanning
  • mirco/akplanning
  • lordofthevoid/akplanning
  • voidptr/akplanning
  • xayomer/akplanning-fork
  • mollux/akplanning
  • neumantm/akplanning
  • mmarx/akplanning
  • nerf/akplanning
  • felix_bonn/akplanning
  • sebastian.uschmann/akplanning
13 results
Show changes
Showing
with 1474 additions and 176 deletions
{% extends 'AKSubmission/submit_new.html' %}
{% load i18n %}
{% load static %}
{% block title %}{% trans "AKs" %}: {{ event.name }} - {% trans "New AK Wish" %}{% endblock %}
{% block breadcrumbs %}
{% include "AKSubmission/submission_breadcrumbs.html" %}
<li class="breadcrumb-item"><a
href="{% url 'submit:submission_overview' event_slug=event.slug %}">{% trans "AK Submission" %}</a></li>
<li class="breadcrumb-item active">{% trans "New AK Wish" %}</li>
{% endblock %}
{% block headline %}
<h2>{% trans 'New AK Wish' %}</h2>
{% endblock %}
{% for tag in tags.all %}
<a href="{% url 'submit:ak_list_by_tag' event_slug=event_slug tag_pk=tag.pk %}"><span class="badge badge-info">{{ tag }}</span></a>
{% endfor %}
{% for track in tracks.all %}
<a href="{% url 'submit:ak_list_by_track' event_slug=event_slug track_pk=track.pk %}"><span
class="badge bg-info">{{ track }}</span></a>
{% endfor %}
from django import template
from fontawesome.templatetags.fontawesome import fontawesome_icon
from fontawesome_6.templatetags.fontawesome_6 import fa6_icon
register = template.Library()
@register.filter
def bool_symbol(bool_val):
"""
Show a nice icon instead of the string true/false
:param bool_val: boolean value to iconify
:return: check or times icon depending on the value
"""
if bool_val:
return fontawesome_icon("check")
return fontawesome_icon("times")
return fa6_icon("check", "fas")
return fa6_icon("times", "fas")
@register.inclusion_tag("AKSubmission/tags_list.html")
def tag_list(tags, event_slug):
return {"tags": tags, "event_slug": event_slug}
@register.inclusion_tag("AKSubmission/tracks_list.html")
def track_list(tracks, event_slug):
"""
Generate a clickable list of tracks (one badge per track) based upon the tracks_list template
:param tracks: tracks to consider
:param event_slug: slug of this event, required for link creation
:return: html fragment containing track links
"""
return {"tracks": tracks, "event_slug": event_slug}
@register.inclusion_tag("AKSubmission/category_list.html")
def category_list(categories, event_slug):
"""
Generate a clickable list of categories (one badge per category) based upon the category_list template
:param categories: categories to consider
:param event_slug: slug of this event, required for link creation
:return: html fragment containing category links
"""
return {"categories": categories, "event_slug": event_slug}
@register.inclusion_tag("AKSubmission/category_linked_badge.html")
def category_linked_badge(category, event_slug):
"""
Generate a clickable category badge based upon the category_linked_badge template
:param category: category to show/link
:param event_slug: slug of this event, required for link creation
:return: html fragment containing badge
"""
return {"category": category, "event_slug": event_slug}
# Create your tests here.
from datetime import datetime, timedelta
from django.test import TestCase
from django.urls import reverse_lazy
from AKModel.models import AK, AKSlot, Event
from AKModel.tests import BasicViewTests
from AKSubmission.forms import AKSubmissionForm
class ModelViewTests(BasicViewTests, TestCase):
"""
Testcases for AKSubmission app.
This extends :class:`BasicViewTests` for standard view and edit testcases
that are specified in this class as VIEWS and EDIT_TESTCASES.
Additionally several additional testcases, in particular to test the API
and the dispatching for owner selection and editing are specified.
"""
fixtures = ['model.json']
VIEWS = [
('submission_overview', {'event_slug': 'kif42'}),
('ak_detail', {'event_slug': 'kif42', 'pk': 1}),
('ak_history', {'event_slug': 'kif42', 'pk': 1}),
('ak_edit', {'event_slug': 'kif42', 'pk': 1}),
('akslot_add', {'event_slug': 'kif42', 'pk': 1}),
('akmessage_add', {'event_slug': 'kif42', 'pk': 1}),
('akslot_edit', {'event_slug': 'kif42', 'pk': 5}),
('akslot_delete', {'event_slug': 'kif42', 'pk': 5}),
('ak_list', {'event_slug': 'kif42'}),
('ak_list_by_category', {'event_slug': 'kif42', 'category_pk': 4}),
('ak_list_by_track', {'event_slug': 'kif42', 'track_pk': 1}),
('akowner_create', {'event_slug': 'kif42'}),
('akowner_edit', {'event_slug': 'kif42', 'slug': 'a'}),
('submit_ak', {'event_slug': 'kif42', 'owner_slug': 'a'}),
('submit_ak_wish', {'event_slug': 'kif42'}),
('error_not_configured', {'event_slug': 'kif42'}),
]
APP_NAME = 'submit'
EDIT_TESTCASES = [
{'view': 'ak_edit', 'target_view': 'ak_detail', 'kwargs': {'event_slug': 'kif42', 'pk': 1},
'expected_message': "AK successfully updated"},
{'view': 'akslot_edit', 'target_view': 'ak_detail', 'kwargs': {'event_slug': 'kif42', 'pk': 5},
'target_kwargs': {'event_slug': 'kif42', 'pk': 1}, 'expected_message': "AK Slot successfully updated"},
{'view': 'akowner_edit', 'target_view': 'submission_overview', 'kwargs': {'event_slug': 'kif42', 'slug': 'a'},
'target_kwargs': {'event_slug': 'kif42'}, 'expected_message': "Person Info successfully updated"},
]
def test_akslot_edit_delete_prevention(self):
"""
Slots planned already may not be modified or deleted in front end
"""
self.client.logout()
_, url = self._name_and_url(('akslot_edit', {'event_slug': 'kif42', 'pk': 1}))
response = self.client.get(url)
self.assertEqual(response.status_code, 302,
msg=f"AK Slot editing ({url}) possible even though slot was already scheduled")
self._assert_message(response, "You cannot edit a slot that has already been scheduled")
_, url = self._name_and_url(('akslot_delete', {'event_slug': 'kif42', 'pk': 1}))
response = self.client.get(url)
self.assertEqual(response.status_code, 302,
msg=f"AK Slot deletion ({url}) possible even though slot was already scheduled")
self._assert_message(response, "You cannot delete a slot that has already been scheduled")
def test_slot_creation_deletion(self):
"""
Test creation and deletion of slots in frontend
"""
ak_args = {'event_slug': 'kif42', 'pk': 1}
redirect_url = reverse_lazy(f"{self.APP_NAME}:ak_detail", kwargs=ak_args)
# Create a valid slot -> Redirect to AK detail page, message to user
count_slots = AK.objects.get(pk=1).akslot_set.count()
create_url = reverse_lazy(f"{self.APP_NAME}:akslot_add", kwargs=ak_args)
response = self.client.post(create_url, {'ak': 1, 'event': 2, 'duration': 1.5})
self.assertRedirects(response, redirect_url, status_code=302, target_status_code=200,
msg_prefix="Did not correctly trigger redirect")
self.assertEqual(AK.objects.get(pk=1).akslot_set.count(), count_slots + 1,
msg="New slot was not correctly saved")
# Get primary key of newly created Slot
slot_pk = AK.objects.get(pk=1).akslot_set.order_by('pk').last().pk
# Edit the recently created slot: Make sure view is accessible, post change
# -> redirect to detail page, duration updated
edit_url = reverse_lazy(f"{self.APP_NAME}:akslot_edit", kwargs={'event_slug': 'kif42', 'pk': slot_pk})
response = self.client.get(edit_url)
self.assertEqual(response.status_code, 200, msg=f"Cant open edit view for newly created slot ({edit_url})")
response = self.client.post(edit_url, {'ak': 1, 'event': 2, 'duration': 2})
self.assertRedirects(response, redirect_url, status_code=302, target_status_code=200,
msg_prefix="Did not correctly trigger redirect")
self.assertEqual(AKSlot.objects.get(pk=slot_pk).duration, 2,
msg="Slot was not correctly changed")
# Delete recently created slot: Make sure view is accessible, post deletion
# -> redirect to detail page, slot deleted, message to user
deletion_url = reverse_lazy(f"{self.APP_NAME}:akslot_delete", kwargs={'event_slug': 'kif42', 'pk': slot_pk})
response = self.client.get(deletion_url)
self.assertEqual(response.status_code, 200,
msg="Cant open deletion view for newly created slot ({deletion_url})")
response = self.client.post(deletion_url, {})
self.assertRedirects(response, redirect_url, status_code=302, target_status_code=200,
msg_prefix="Did not correctly trigger redirect")
self.assertFalse(AKSlot.objects.filter(pk=slot_pk).exists(), msg="Slot was not correctly deleted")
self.assertEqual(AK.objects.get(pk=1).akslot_set.count(), count_slots, msg="AK still has to many slots")
def test_ak_owner_editing(self):
"""
Test dispatch of user editing requests
"""
edit_url = reverse_lazy(f"{self.APP_NAME}:akowner_edit_dispatch", kwargs={'event_slug': 'kif42'})
base_url = reverse_lazy(f"{self.APP_NAME}:submission_overview", kwargs={'event_slug': 'kif42'})
# Empty form/no user selected -> start page
response = self.client.post(edit_url, {'owner_id': -1})
self.assertRedirects(response, base_url, status_code=302, target_status_code=200,
msg_prefix="Did not redirect to start page even though no user was selected")
self._assert_message(response, "No user selected")
# Correct selection -> user edit page for that user
edit_redirect_url = reverse_lazy(f"{self.APP_NAME}:akowner_edit", kwargs={'event_slug': 'kif42', 'slug': 'a'})
response = self.client.post(edit_url, {'owner_id': 1})
self.assertRedirects(response, edit_redirect_url, status_code=302, target_status_code=200,
msg_prefix=f"Dispatch redirect failed (should go to {edit_redirect_url})")
def test_ak_owner_selection(self):
"""
Test dispatch of owner selection requests
"""
select_url = reverse_lazy(f"{self.APP_NAME}:akowner_select", kwargs={'event_slug': 'kif42'})
create_url = reverse_lazy(f"{self.APP_NAME}:akowner_create", kwargs={'event_slug': 'kif42'})
# Empty user selection -> create a new user view
response = self.client.post(select_url, {'owner_id': -1})
self.assertRedirects(response, create_url, status_code=302, target_status_code=200,
msg_prefix="Did not redirect to user create view even though no user was specified")
# Valid user selected -> redirect to view that allows to add a new AK with this user as owner
add_redirect_url = reverse_lazy(f"{self.APP_NAME}:submit_ak", kwargs={'event_slug': 'kif42', 'owner_slug': 'a'})
response = self.client.post(select_url, {'owner_id': 1})
self.assertRedirects(response, add_redirect_url, status_code=302, target_status_code=200,
msg_prefix=f"Dispatch redirect to ak submission page failed "
f"(should go to {add_redirect_url})")
def test_orga_message_submission(self):
"""
Test submission and storing of direct confident messages to organizers
"""
form_url = reverse_lazy(f"{self.APP_NAME}:akmessage_add", kwargs={'event_slug': 'kif42', 'pk': 1})
detail_url = reverse_lazy(f"{self.APP_NAME}:ak_detail", kwargs={'event_slug': 'kif42', 'pk': 1})
count_messages = AK.objects.get(pk=1).akorgamessage_set.count()
# Test that submission view is accessible
response = self.client.get(form_url)
self.assertEqual(response.status_code, 200, msg="Could not load message form view")
# Test submission itself and the following redirect -> AK detail page
response = self.client.post(form_url, {'ak': 1, 'event': 2, 'text': 'Test message text'})
self.assertRedirects(response, detail_url, status_code=302, target_status_code=200,
msg_prefix=f"Did not trigger redirect to ak detail page ({detail_url})")
# Make sure message was correctly saved in database and user is notified about that
self._assert_message(response, "Message to organizers successfully saved")
self.assertEqual(AK.objects.get(pk=1).akorgamessage_set.count(), count_messages + 1,
msg="Message was not correctly saved")
def test_interest_api(self):
"""
Test interest indicating API (access, functionality)
"""
interest_api_url = "/kif42/api/ak/1/indicate-interest/"
ak = AK.objects.get(pk=1)
event = Event.objects.get(slug='kif42')
ak_interest_counter = ak.interest_counter
# Check Access method (only POST)
response = self.client.get(interest_api_url)
self.assertEqual(response.status_code, 405, "Should not be accessible via GET")
event.interest_start = datetime.now().astimezone(event.timezone) + timedelta(minutes=-10)
event.interest_end = datetime.now().astimezone(event.timezone) + timedelta(minutes=+10)
event.save()
# Test correct indication -> HTTP 200, counter increased
response = self.client.post(interest_api_url)
self.assertEqual(response.status_code, 200, f"API end point not working ({interest_api_url})")
self.assertEqual(AK.objects.get(pk=1).interest_counter, ak_interest_counter + 1, "Counter was not increased")
event.interest_end = datetime.now().astimezone(event.timezone) + timedelta(minutes=-2)
event.save()
# Test indication outside of indication window -> HTTP 403, counter not increased
response = self.client.post(interest_api_url)
self.assertEqual(response.status_code, 403,
"API end point still reachable even though interest indication window ended "
"({interest_api_url})")
self.assertEqual(AK.objects.get(pk=1).interest_counter, ak_interest_counter + 1,
"Counter was increased even though interest indication window ended")
# Test call for non-existing AK -> HTTP 403
invalid_interest_api_url = "/kif42/api/ak/-1/indicate-interest/"
response = self.client.post(invalid_interest_api_url)
self.assertEqual(response.status_code, 404, f"Invalid URL reachable ({interest_api_url})")
def test_adding_of_unknown_user(self):
"""
Test adding of a previously not existing owner to an AK
"""
# Pre-Check: AK detail page existing?
detail_url = reverse_lazy(f"{self.APP_NAME}:ak_detail", kwargs={'event_slug': 'kif42', 'pk': 1})
response = self.client.get(detail_url)
self.assertEqual(response.status_code, 200, msg="Could not load ak detail view")
# Make sure AK detail page contains a link to add a new owner
edit_url = reverse_lazy(f"{self.APP_NAME}:ak_edit", kwargs={'event_slug': 'kif42', 'pk': 1})
response = self.client.get(edit_url)
self.assertEqual(response.status_code, 200, msg="Could not load ak detail view")
self.assertContains(response, "Add person not in the list yet",
msg_prefix="Link to add unknown user not contained")
# Check adding of a new owner by posting an according request
# -> Redirect to AK detail page, message to user, owners list updated
self.assertEqual(AK.objects.get(pk=1).owners.count(), 1)
add_new_user_to_ak_url = reverse_lazy(f"{self.APP_NAME}:akowner_create", kwargs={'event_slug': 'kif42'}) \
+ "?add_to_existing_ak=1"
response = self.client.post(add_new_user_to_ak_url,
{'name': 'New test owner', 'event': Event.get_by_slug('kif42').pk})
self.assertRedirects(response, detail_url,
msg_prefix=f"No correct redirect: {add_new_user_to_ak_url} (POST) -> {detail_url}")
self._assert_message(response, "Added 'New test owner' as new owner of 'Test AK Inhalt'")
self.assertEqual(AK.objects.get(pk=1).owners.count(), 2)
def test_visibility_requirements_in_submission_form(self):
"""
Test visibility of requirements field in submission form
"""
event = Event.get_by_slug('kif42')
form = AKSubmissionForm(data={'name': 'Test AK', 'event': event}, instance=None, initial={"event": event})
self.assertIn('requirements', form.fields,
msg="Requirements field not present in form even though event has requirements")
event2 = Event.objects.create(name='Event without requirements',
slug='no_req',
start=datetime.now().astimezone(event.timezone),
end=datetime.now().astimezone(event.timezone),
active=True)
form2 = AKSubmissionForm(data={'name': 'Test AK', 'event': event2}, instance=None, initial={"event": event2})
self.assertNotIn('requirements', form2.fields,
msg="Requirements field should not be present for events without requirements")
def test_visibility_types_in_submission_form(self):
"""
Test visibility of types field in submission form
"""
event = Event.get_by_slug('kif42')
form = AKSubmissionForm(data={'name': 'Test AK', 'event': event}, instance=None, initial={"event": event})
self.assertIn('types', form.fields,
msg="Requirements field not present in form even though event has requirements")
event2 = Event.objects.create(name='Event without types',
slug='no_types',
start=datetime.now().astimezone(event.timezone),
end=datetime.now().astimezone(event.timezone),
active=True)
form2 = AKSubmissionForm(data={'name': 'Test AK', 'event': event2}, instance=None, initial={"event": event2})
self.assertNotIn('types', form2.fields,
msg="Requirements field should not be present for events without types")
......@@ -9,11 +9,23 @@ urlpatterns = [
'<slug:event_slug>/submission/',
include([
path('', views.SubmissionOverviewView.as_view(), name='submission_overview'),
path('ak/<int:pk>', views.AKDetailView.as_view(), name='ak_detail'),
path('aks/', views.AKListView.as_view(), name='ak_list'),
path('aks/category/<int:category_pk>', views.AKListByCategoryView.as_view(), name='ak_list_by_category'),
path('aks/tag/<int:tag_pk>', views.AKListByTagView.as_view(), name='ak_list_by_tag'),
path('new/', views.AKSubmissionView.as_view(), name='submit_ak'),
path('ak/<int:pk>/', views.AKDetailView.as_view(), name='ak_detail'),
path('ak/<int:pk>/history/', views.AKHistoryView.as_view(), name='ak_history'),
path('ak/<int:pk>/edit/', views.AKEditView.as_view(), name='ak_edit'),
path('ak/<int:pk>/add_slot/', views.AKSlotAddView.as_view(), name='akslot_add'),
path('ak/<int:pk>/add_message/', views.AKAddOrgaMessageView.as_view(), name='akmessage_add'),
path('akslot/<int:pk>/edit/', views.AKSlotEditView.as_view(), name='akslot_edit'),
path('akslot/<int:pk>/delete/', views.AKSlotDeleteView.as_view(), name='akslot_delete'),
path('aks/', views.AKOverviewView.as_view(), name='ak_list'),
path('aks/category/<int:category_pk>/', views.AKListByCategoryView.as_view(), name='ak_list_by_category'),
path('aks/track/<int:track_pk>/', views.AKListByTrackView.as_view(), name='ak_list_by_track'),
path('owner/', views.AKOwnerCreateView.as_view(), name='akowner_create'),
path('new/', views.AKOwnerSelectDispatchView.as_view(), name='akowner_select'),
path('owner/edit/', views.AKOwnerEditDispatchView.as_view(), name='akowner_edit_dispatch'),
path('<slug:slug>/edit/', views.AKOwnerEditView.as_view(), name='akowner_edit'),
path('<slug:owner_slug>/new/', views.AKSubmissionView.as_view(), name='submit_ak'),
path('new_wish/', views.AKWishSubmissionView.as_view(), name='submit_ak_wish'),
path('error/', views.SubmissionErrorNotConfiguredView.as_view(), name='error_not_configured'),
])
),
]
from django.http import Http404
from abc import ABC, abstractmethod
from datetime import datetime, timedelta
from math import floor
from django.apps import apps
from django.conf import settings
from django.contrib import messages
from django.http import HttpResponseRedirect
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 ListView, DetailView, CreateView
from django.views import View
from django.views.generic import CreateView, DeleteView, DetailView, ListView, TemplateView, UpdateView
from AKModel.models import AK, AKCategory, AKTag
from AKModel.views import FilterByEventSlugMixin
from AKSubmission.forms import AKForm
from AKModel.availability.models import Availability
from AKModel.metaviews import status_manager
from AKModel.metaviews.admin import EventSlugMixin, FilterByEventSlugMixin
from AKModel.metaviews.status import TemplateStatusWidget
from AKModel.models import AK, AKCategory, AKOrgaMessage, AKOwner, AKSlot, AKTrack
from AKSubmission.api import ak_interest_indication_active
from AKSubmission.forms import AKDurationForm, AKForm, AKOrgaMessageForm, AKOwnerForm, AKSubmissionForm, AKWishForm
class SubmissionOverviewView(FilterByEventSlugMixin, ListView):
model = AK
context_object_name = "AKs"
class SubmissionErrorNotConfiguredView(EventSlugMixin, TemplateView):
"""
View to show when submission is not correctly configured yet for this event
and hence the submission component cannot be used already.
"""
template_name = "AKSubmission/submission_not_configured.html"
class AKOverviewView(FilterByEventSlugMixin, ListView):
"""
View: Show a tabbed list of AKs belonging to this event split by categories
Wishes show up in between of the other AKs in the category they belong to.
In contrast to :class:`SubmissionOverviewView` that inherits from this view,
on this view there is no form to add new AKs or edit owners.
Since the inherited version of this view will have a slightly different behaviour,
this view contains multiple methods that can be overriden for this adaption.
"""
model = AKCategory
context_object_name = "categories"
template_name = "AKSubmission/ak_overview.html"
wishes_as_category = False
def filter_aks(self, context, category): # pylint: disable=unused-argument
"""
Filter which AKs to display based on the given context and category
In the default case, all AKs of that category are returned (including wishes)
:param context: context of the view
:param category: category to filter the AK list for
:return: filtered list of AKs for the given category
:rtype: QuerySet[AK]
"""
# Use prefetching and relation selection/joining to reduce the amount of necessary queries
return category.ak_set.select_related('event').prefetch_related('owners').prefetch_related('types').all()
def get_active_category_name(self, context):
"""
Get the category name to display by default/before further user interaction
In the default case, simply the first category (the one with the lowest ID for this event) is used
:param context: context of the view
:return: name of the default category
:rtype: str
"""
return context["categories_with_aks"][0][0].name
def get_table_title(self, context): # pylint: disable=unused-argument
"""
Specify the title above the AK list/table in this view
:param context: context of the view
:return: title to use
:rtype: str
"""
return _("All AKs")
def get(self, request, *args, **kwargs):
"""
Handle GET request
Overriden to allow checking for correct configuration and
redirect to error page if necessary (see :class:`SubmissionErrorNotConfiguredView`)
"""
self._load_event()
self.object_list = self.get_queryset() # pylint: disable=attribute-defined-outside-init
# No categories yet? Redirect to configuration error page
if self.object_list.count() == 0:
return redirect(reverse_lazy("submit:error_not_configured", kwargs={'event_slug': self.event.slug}))
context = self.get_context_data()
return self.render_to_response(context)
def get_context_data(self, *, object_list=None, **kwargs):
context = super().get_context_data(object_list=object_list, **kwargs)
# ==========================================================
# Sort AKs into different lists (by their category)
# ==========================================================
ak_wishes = []
categories_with_aks = []
# Loop over categories, load AKs (while filtering them if necessary) and create a list of (category, aks)-tuples
# Depending on the setting of self.wishes_as_category, wishes are either included
# or added to a special "Wish"-Category that is created on-the-fly to provide consistent handling in the
# template (without storing it in the database)
for category in context["categories"]:
aks_for_category = []
for ak in self.filter_aks(context, category):
if self.wishes_as_category and ak.wish:
ak_wishes.append(ak)
else:
aks_for_category.append(ak)
categories_with_aks.append((category, aks_for_category))
if self.wishes_as_category:
categories_with_aks.append(
(AKCategory(name=_("Wishes"), pk=0, description=_("AKs one would like to have")), ak_wishes))
context["categories_with_aks"] = categories_with_aks
context["active_category"] = self.get_active_category_name(context)
context['table_title'] = self.get_table_title(context)
context['show_types'] = self.event.aktype_set.count() > 0
# ==========================================================
# Display interest indication button?
# ==========================================================
current_timestamp = datetime.now().astimezone(self.event.timezone)
context['interest_indication_active'] = ak_interest_indication_active(self.event, current_timestamp)
return context
class SubmissionOverviewView(AKOverviewView):
"""
View: List of AKs and possibility to add AKs or adapt owner information
Main/start view of the component.
This view inherits from :class:`AKOverviewView`, but treats wishes as separate category if requested in the settings
and handles the change actions mentioned above.
"""
model = AKCategory
context_object_name = "categories"
template_name = "AKSubmission/submission_overview.html"
ordering = ['category']
# this mainly steers the different handling of wishes
# since the code for that is already included in the parent class
wishes_as_category = settings.WISHES_AS_CATEGORY
def get_table_title(self, context):
"""
Specify the title above the AK list/table in this view
:param context: context of the view
:return: title to use
:rtype: str
"""
return _("Currently planned AKs")
def get_context_data(self, *, object_list=None, **kwargs):
context = super().get_context_data(object_list=object_list, **kwargs)
# Get list of existing owners for event (for AK submission start)
context["existingOwners"] = AKOwner.objects.filter(event=self.event)
return context
class AKListByCategoryView(AKOverviewView):
"""
View: List of only the AKs belonging to a certain category.
This view inherits from :class:`AKOverviewView`, but produces only one list instead of a tabbed one.
"""
def dispatch(self, request, *args, **kwargs):
# Override dispatching
# Needed to handle the checking whether the category exists
# noinspection PyAttributeOutsideInit
# pylint: disable=attribute-defined-outside-init
self.category = get_object_or_404(AKCategory, pk=kwargs['category_pk'])
return super().dispatch(request, *args, **kwargs)
class AKDetailView(DetailView):
def get_active_category_name(self, context):
"""
Get the category name to display by default/before further user interaction
In this case, this will be the name of the category specified via pk
:param context: context of the view
:return: name of the category
:rtype: str
"""
return self.category.name
class AKListByTrackView(AKOverviewView):
"""
View: List of only the AKs belonging to a certain track.
This view inherits from :class:`AKOverviewView` and there will be one list per category
-- but only AKs of a certain given track will be included in them.
"""
def dispatch(self, request, *args, **kwargs):
# Override dispatching
# Needed to handle the checking whether the track exists
self.track = get_object_or_404(AKTrack, pk=kwargs['track_pk']) # pylint: disable=attribute-defined-outside-init
return super().dispatch(request, *args, **kwargs)
def filter_aks(self, context, category):
"""
Filter which AKs to display based on the given context and category
In this case, the list is further restricted by the track
:param context: context of the view
:param category: category to filter the AK list for
:return: filtered list of AKs for the given category
:rtype: QuerySet[AK]
"""
return super().filter_aks(context, category).filter(track=self.track)
def get_table_title(self, context):
return f"{_('AKs with Track')} = {self.track.name}"
class AKDetailView(EventSlugMixin, DetailView):
"""
View: AK Details
"""
model = AK
context_object_name = "ak"
template_name = "AKSubmission/ak_detail.html"
class AKListView(FilterByEventSlugMixin, ListView):
model = AK
context_object_name = "AKs"
template_name = "AKSubmission/ak_list.html"
filter_condition_string = ""
def get_queryset(self):
# Get information about the AK and do some query optimization
return super().get_queryset().select_related('event').prefetch_related('owners')
def get_context_data(self, *, object_list=None, **kwargs):
context = super().get_context_data(object_list=object_list, **kwargs)
context['categories'] = AKCategory.objects.all()
context["tags"] = AKTag.objects.all()
context["filter_condition_string"] = self.filter_condition_string
context["availabilities"] = Availability.objects.filter(ak=context["ak"])
current_timestamp = datetime.now().astimezone(self.event.timezone)
# Is this AK taking place now or soon (used for top page visualization)
context["featured_slot_type"] = "NONE"
if apps.is_installed("AKPlan"):
in_two_hours = current_timestamp + timedelta(hours=2)
slots = context["ak"].akslot_set.filter(start__isnull=False, room__isnull=False).select_related('room')
for slot in slots:
if slot.end > current_timestamp:
if slot.start <= current_timestamp:
context["featured_slot_type"] = "CURRENT"
remaining = slot.end - current_timestamp
elif slot.start <= in_two_hours:
context["featured_slot_type"] = "UPCOMING"
remaining = slot.start - current_timestamp
else:
continue
context["featured_slot"] = slot
context["featured_slot_remaining"] = floor(remaining.days * 24 * 60 + remaining.seconds / 60)
break
# Display interest indication button?
context['interest_indication_active'] = ak_interest_indication_active(self.event, current_timestamp)
return context
class AKListByCategoryView(AKListView):
category = None
class AKHistoryView(EventSlugMixin, DetailView):
"""
View: Show history of a given AK
"""
model = AK
context_object_name = "ak"
template_name = "AKSubmission/ak_history.html"
def get_queryset(self):
# Find category based on event slug
try:
self.category = AKCategory.objects.get(pk=self.kwargs['category_pk'])
self.filter_condition_string = f"{_('Category')} = {self.category.name}"
except AKCategory.DoesNotExist:
raise Http404
return super().get_queryset().filter(category=self.category)
class EventInactiveRedirectMixin:
"""
Mixin that will cause a redirect when actions are performed on an inactive event.
Will add a message explaining why the action was not performed to the user
and then redirect to start page of the submission component
"""
class AKListByTagView(AKListView):
tag = None
def get_error_message(self):
"""
Error message to display after redirect (can be adjusted by this method)
def get_queryset(self):
# Find category based on event slug
try:
self.tag = AKTag.objects.get(pk=self.kwargs['tag_pk'])
self.filter_condition_string = f"{_('Tag')} = {self.tag.name}"
except AKTag.DoesNotExist:
raise Http404
return super().get_queryset().filter(tags=self.tag)
:return: error message
:rtype: str
"""
return _("Event inactive. Cannot create or update.")
def get(self, request, *args, **kwargs):
"""
Override GET request handling
Will either perform the redirect including the message creation or continue with the planned dispatching
"""
s = super().get(request, *args, **kwargs)
if not self.event.active:
messages.add_message(self.request, messages.ERROR, self.get_error_message())
return redirect(reverse_lazy('submit:submission_overview', kwargs={'event_slug': self.event.slug}))
return s
class AKSubmissionView(CreateView):
class AKAndAKWishSubmissionView(EventSlugMixin, EventInactiveRedirectMixin, CreateView):
"""
View: Submission form for AKs and Wishes
Base view, will be used by :class:`AKSubmissionView` and :class:`AKWishSubmissionView`
"""
model = AK
template_name = 'AKSubmission/submit_new.html'
form_class = AKSubmissionForm
def get_success_url(self):
messages.add_message(self.request, messages.SUCCESS, _("AK successfully created"))
return self.object.detail_url
def form_valid(self, form):
if not form.cleaned_data["event"].active:
messages.add_message(self.request, messages.ERROR, self.get_error_message())
return redirect(reverse_lazy('submit:submission_overview',
kwargs={'event_slug': form.cleaned_data["event"].slug}))
# Try to save AK and get redirect URL
super_form_valid = super().form_valid(form)
# Generate slot(s) (but not for wishes)
if "durations" in form.cleaned_data:
for duration in form.cleaned_data["durations"]:
new_slot = AKSlot(ak=self.object, duration=duration, event=self.object.event)
new_slot.save()
return super_form_valid
class AKSubmissionView(AKAndAKWishSubmissionView):
"""
View: AK submission form
Extends :class:`AKAndAKWishSubmissionView`
"""
def get_initial(self):
# Load initial values for the form
# Used to directly add the first owner and the event this AK will belong to
initials = super(AKAndAKWishSubmissionView, self).get_initial()
initials['owners'] = [AKOwner.get_by_slug(self.event, self.kwargs['owner_slug'])]
initials['event'] = self.event
return initials
def get_context_data(self, *, object_list=None, **kwargs):
context = super().get_context_data(object_list=object_list, **kwargs)
context['owner'] = get_object_or_404(AKOwner, event=self.event, slug=self.kwargs['owner_slug'])
return context
class AKWishSubmissionView(AKAndAKWishSubmissionView):
"""
View: Wish submission form
Extends :class:`AKAndAKWishSubmissionView`
"""
template_name = 'AKSubmission/submit_new_wish.html'
form_class = AKWishForm
def get_initial(self):
# Load initial values for the form
# Used to directly select the event this AK will belong to
initials = super(AKAndAKWishSubmissionView, self).get_initial()
initials['event'] = self.event
return initials
class AKEditView(EventSlugMixin, EventInactiveRedirectMixin, UpdateView):
"""
View: Update an AK
This allows to change most fields of an AK as specified in :class:`AKSubmission.forms.AKForm`,
including the availabilities.
It will also handle the change from AK to wish and vice versa (triggered by adding or removing owners)
and automatically create or delete (unscheduled) slots
"""
model = AK
template_name = 'AKSubmission/ak_edit.html'
form_class = AKForm
def get_success_url(self):
# Redirection after successfully saving to detail page of AK where also a success message is displayed
messages.add_message(self.request, messages.SUCCESS, _("AK successfully updated"))
return self.object.detail_url
def form_valid(self, form):
# Handle valid form submission
# Only save when event is active, otherwise redirect
if not form.cleaned_data["event"].active:
messages.add_message(self.request, messages.ERROR, self.get_error_message())
return redirect(reverse_lazy('submit:submission_overview',
kwargs={'event_slug': form.cleaned_data["event"].slug}))
# Remember owner count before saving to know whether the AK changed its state between AK and wish
previous_owner_count = self.object.owners.count()
# Perform saving and redirect handling by calling default/parent implementation of form_valid
redirect_response = super().form_valid(form)
# Did this AK change from wish to AK or vice versa?
new_owner_count = self.object.owners.count()
# Now AK:
if previous_owner_count == 0 and new_owner_count > 0 and self.object.akslot_set.count() == 0:
# Create one slot with default length
AKSlot.objects.create(ak=self.object, duration=self.object.event.default_slot, event=self.object.event)
# Now wish:
elif previous_owner_count > 0 and new_owner_count == 0:
# Delete all unscheduled slots
self.object.akslot_set.filter(start__isnull=True).delete()
# Redirect to success url
return redirect_response
class AKOwnerCreateView(EventSlugMixin, EventInactiveRedirectMixin, CreateView):
"""
View: Create a new owner
"""
model = AKOwner
template_name = 'AKSubmission/akowner_create_update.html'
form_class = AKOwnerForm
def get_success_url(self):
# The redirect url depends on the source this view was called from:
# Called from an existing AK? Add the new owner as an owner of that AK, notify the user and redirect to detail
# page of that AK
if "add_to_existing_ak" in self.request.GET:
ak_pk = self.request.GET['add_to_existing_ak']
ak = get_object_or_404(AK, pk=ak_pk)
ak.owners.add(self.object)
messages.add_message(self.request, messages.SUCCESS,
_("Added '{owner}' as new owner of '{ak.name}'").format(owner=self.object, ak=ak))
return ak.detail_url
# Called from the submission overview? Offer the user to create a new AK with the recently created owner
# prefilled as owner of that AK in the creation form
return reverse_lazy('submit:submit_ak',
kwargs={'event_slug': self.kwargs['event_slug'], 'owner_slug': self.object.slug})
def get_initial(self):
# Set the event in the (hidden) event field in the form based on the URL this view was called with
initials = super().get_initial()
initials['event'] = self.event
return initials
def form_valid(self, form):
# Prevent changes if event is not active
if not form.cleaned_data["event"].active:
messages.add_message(self.request, messages.ERROR, self.get_error_message())
return redirect(reverse_lazy('submit:submission_overview',
kwargs={'event_slug': form.cleaned_data["event"].slug}))
return super().form_valid(form)
class AKOwnerDispatchView(ABC, EventSlugMixin, View):
"""
Base view: Dispatch to correct view based upon
Will be used by :class:`AKOwnerSelectDispatchView` and :class:`AKOwnerEditDispatchView` to handle button clicks for
"New AK" and "Edit Person Info" in submission overview based upon the selection in the owner dropdown field
"""
@abstractmethod
def get_new_owner_redirect(self, event_slug):
"""
Get redirect when user selected "I do not own AKs yet"
:param event_slug: slug of the event, needed for constructing redirect
:return: redirect to perform
:rtype: HttpResponseRedirect
"""
@abstractmethod
def get_valid_owner_redirect(self, event_slug, owner):
"""
Get redirect when user selected "I do not own AKs yet"
:param event_slug: slug of the event, needed for constructing redirect
:param owner: owner to perform the dispatching for
:return: redirect to perform
:rtype: HttpResponseRedirect
"""
def post(self, request, *args, **kwargs):
# This view is solely meant to handle POST requests
# Perform dispatching based on the submitted owner_id
# No owner_id? Redirect to submission overview view
if "owner_id" not in request.POST:
return redirect('submit:submission_overview', event_slug=kwargs['event_slug'])
owner_id = request.POST["owner_id"]
# Special owner_id "-1" (value of "I do not own AKs yet)? Redirect to owner creation view
if owner_id == "-1":
return self.get_new_owner_redirect(kwargs['event_slug'])
# Normal owner_id given? Check vor validity and redirect to AK submission page with that owner prefilled
# or display a 404 error page if no owner for the given id can be found. The latter should only happen when the
# user manipulated the value before sending or when the owner was deleted in backend and the user did not
# reload the dropdown between deletion and sending the dispatch request
owner = get_object_or_404(AKOwner, pk=request.POST["owner_id"])
return self.get_valid_owner_redirect(kwargs['event_slug'], owner)
def get(self, request, *args, **kwargs):
# This view should never be called with GET, perform a redirect to overview in that case
return redirect('submit:submission_overview', event_slug=kwargs['event_slug'])
class AKOwnerSelectDispatchView(AKOwnerDispatchView):
"""
View: Handle submission from the owner selection dropdown in submission overview for AK creation
("New AK" button)
This view will perform redirects depending on the selection in the owner dropdown field.
Based upon the abstract base view :class:`AKOwnerDispatchView`.
"""
def get_new_owner_redirect(self, event_slug):
return redirect('submit:akowner_create', event_slug=event_slug)
def get_valid_owner_redirect(self, event_slug, owner):
return redirect('submit:submit_ak', event_slug=event_slug, owner_slug=owner.slug)
class AKOwnerEditDispatchView(AKOwnerDispatchView):
"""
View: Handle submission from the owner selection dropdown in submission overview for owner editing
("Edit Person Info" button)
This view will perform redirects depending on the selection in the owner dropdown field.
Based upon the abstract base view :class:`AKOwnerDispatchView`.
"""
def get_new_owner_redirect(self, event_slug):
messages.add_message(self.request, messages.WARNING, _("No user selected"))
return redirect('submit:submission_overview', event_slug)
def get_valid_owner_redirect(self, event_slug, owner):
return redirect('submit:akowner_edit', event_slug=event_slug, slug=owner.slug)
class AKOwnerEditView(FilterByEventSlugMixin, EventSlugMixin, EventInactiveRedirectMixin, UpdateView):
"""
View: Edit an owner
"""
model = AKOwner
template_name = "AKSubmission/akowner_create_update.html"
form_class = AKOwnerForm
def get_success_url(self):
messages.add_message(self.request, messages.SUCCESS, _("Person Info successfully updated"))
return reverse_lazy('submit:submission_overview', kwargs={'event_slug': self.kwargs['event_slug']})
def form_valid(self, form):
# Prevent updating if event is not active
if not form.cleaned_data["event"].active:
messages.add_message(self.request, messages.ERROR, self.get_error_message())
return redirect(reverse_lazy('submit:submission_overview',
kwargs={'event_slug': form.cleaned_data["event"].slug}))
return super().form_valid(form)
class AKSlotAddView(EventSlugMixin, EventInactiveRedirectMixin, CreateView):
"""
View: Add an additional slot to an AK
The user has to select the duration of the slot in this view
The view will only process the request when the event is active (as steered by :class:`EventInactiveRedirectMixin`)
"""
model = AKSlot
form_class = AKDurationForm
template_name = "AKSubmission/akslot_add_update.html"
def get_initial(self):
initials = super().get_initial()
initials['event'] = self.event
initials['ak'] = get_object_or_404(AK, pk=self.kwargs['pk'])
initials['duration'] = self.event.default_slot
return initials
def get_context_data(self, *, object_list=None, **kwargs):
context = super().get_context_data(object_list=object_list, **kwargs)
context['ak'] = get_object_or_404(AK, pk=self.kwargs['pk'])
return context
def get_success_url(self):
messages.add_message(self.request, messages.SUCCESS, _("AK Slot successfully added"))
return self.object.ak.detail_url
class AKSlotEditView(EventSlugMixin, EventInactiveRedirectMixin, UpdateView):
"""
View: Update the duration of an AK slot
The view will only process the request when the event is active (as steered by :class:`EventInactiveRedirectMixin`)
and only slots that are not scheduled yet may be changed
"""
model = AKSlot
form_class = AKDurationForm
template_name = "AKSubmission/akslot_add_update.html"
def get(self, request, *args, **kwargs):
akslot = get_object_or_404(AKSlot, pk=kwargs["pk"])
if akslot.start is not None:
messages.add_message(self.request, messages.WARNING,
_("You cannot edit a slot that has already been scheduled"))
return HttpResponseRedirect(akslot.ak.detail_url)
return super().get(request, *args, **kwargs)
def get_context_data(self, *, object_list=None, **kwargs):
context = super().get_context_data(object_list=object_list, **kwargs)
context['ak'] = self.object.ak
return context
def get_success_url(self):
messages.add_message(self.request, messages.SUCCESS, _("AK Slot successfully updated"))
return self.object.ak.detail_url
class AKSlotDeleteView(EventSlugMixin, EventInactiveRedirectMixin, DeleteView):
"""
View: Delete an AK slot
The view will only process the request when the event is active (as steered by :class:`EventInactiveRedirectMixin`)
and only slots that are not scheduled yet may be deleted
"""
model = AKSlot
template_name = "AKSubmission/akslot_delete.html"
def get(self, request, *args, **kwargs):
akslot = get_object_or_404(AKSlot, pk=kwargs["pk"])
if akslot.start is not None:
messages.add_message(self.request, messages.WARNING,
_("You cannot delete a slot that has already been scheduled"))
return HttpResponseRedirect(akslot.ak.detail_url)
return super().get(request, *args, **kwargs)
def get_context_data(self, *, object_list=None, **kwargs):
context = super().get_context_data(object_list=object_list, **kwargs)
context['ak'] = self.object.ak
return context
def get_success_url(self):
messages.add_message(self.request, messages.SUCCESS, _("AK Slot successfully deleted"))
return self.object.ak.detail_url
@status_manager.register(name="event_ak_messages")
class EventAKMessagesWidget(TemplateStatusWidget):
"""
Status page widget: AK Messages
A widget to display information about AK-related messages sent to organizers for the given event
"""
required_context_type = "event"
title = _("Messages")
template_name = "admin/AKModel/render_ak_messages.html"
def get_context_data(self, context) -> dict:
context["ak_messages"] = AKOrgaMessage.objects.filter(ak__event=context["event"])
return context
def render_actions(self, context: {}) -> list[dict]:
return [
{
"text": _("Delete all messages"),
"url": reverse_lazy("admin:ak_delete_orga_messages", kwargs={"event_slug": context["event"].slug}),
},
]
class AKAddOrgaMessageView(EventSlugMixin, CreateView):
"""
View: Form to create a (confidential) message to the organizers as defined in
:class:`AKSubmission.forms.AKOrgaMessageForm`
"""
model = AKOrgaMessage
form_class = AKOrgaMessageForm
template_name = "AKSubmission/akmessage_add.html"
def get_initial(self):
initials = super().get_initial()
initials['ak'] = get_object_or_404(AK, pk=self.kwargs['pk'])
initials['event'] = initials['ak'].event
return initials
def get_context_data(self, *, object_list=None, **kwargs):
context = super().get_context_data(object_list=object_list, **kwargs)
context['ak'] = get_object_or_404(AK, pk=self.kwargs['pk'])
return context
def get_success_url(self):
messages.add_message(self.request, messages.SUCCESS, _("Message to organizers successfully saved"))
return self.object.ak.detail_url
......@@ -61,7 +61,7 @@ Provide more context by answering these questions:
Include details about your configuration and environment:
* **Which version (commit)) are you using?**
* **Which version (commit) are you using?**
* **What's the OS you're using**?
### Suggesting Enhancements
......
......@@ -14,4 +14,6 @@ AKPlanning is currently being maintained by:
Further contributions in the form of code, testing, documentation etc. were made by:
*
* R. Zameitat [xayomer](https://gitlab.fachschaften.org/xayomer)
* N. Steinger [voidptr](https://gitlab.fachschaften.org/voidptr)
* T. Neumann [neumantm](https://gitlab.fachschaften.org/neumantm)
FROM python:3-alpine
RUN apk add --no-cache gcc python3-dev musl-dev libffi-dev mariadb-connector-c-dev gettext
ADD . /app
WORKDIR /app
RUN pip install -r requirements.txt -r .docker/extra_requirements.txt
ENV DJANGO_SETTINGS_MODULE=AKPlanning.settings_production
RUN mkdir /app/AKPlanning/settings
EXPOSE 3035
CMD ["sh", "/app/.docker/entrypoint.sh"]
# AK Planning: Setup
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.
### System Requirements
* Python3.11+ incl. development tools
* Virtualenv
* 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
* uwsgi Python3 plugin
* 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``
1. activate virtualenv ``source venv/bin/activate``
1. install python requirements ``pip install -r requirements.txt``
1. setup necessary database tables etc. ``python manage.py migrate``
1. prepare static files (can be omitted for dev setups) ``python manage.py collectstatic``
1. compile translations ``python manage.py compilemessages``
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!**
To start the application for development, in the root directory,
1. activate virtualenv ``source venv/bin/activate``
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``).
**Step-by-Step Instructions**
1. log into your system with a sudo user
1. install system requirements
1. create a folder, e.g. ``mkdir /srv/AKPlanning/``
1. change to the new directory ``cd /srv/AKPlanning/``
1. clone this repository ``git clone URL .``
1. setup a virtual environment using the proper python version ``virtualenv venv -p python3.7``
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. 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:
```
Alias /static /srv/AKPlanning/static
<Directory /srv/AKPlanning/static>
Require all granted
</Directory>
ProxyPassMatch ^/static/ !
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``.
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``
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``
## Deployment Setup using Docker
This project also provides a docker file for easy deployment.
The container described by the docker file only contains the project itself.
Additional containers for the database and webserver are needed to use it.
The following [docker-compose](https://docs.docker.com/compose/) file shows a typical usage:
```
version: "3"
networks:
akplanning:
external: false
volumes:
static-files:
services:
mariadb:
image: mariadb:10
restart: always
environment:
MYSQL_ROOT_PASSWORD: supermegasecrey
MYSQL_DATABASE: akplanning
MYSQL_USER: akplanning
MYSQL_PASSWORD: secret
TZ: Europe/Berlin
networks:
- akplanning
akplanning-server:
image: neumantm/akplanning:2021-03-10
restart: always
environment:
SECRET_KEY: superlongandsupersecret
DB_HOST: mariadb
DB_USER: akplanning
DB_NAME: akplanning
DB_PASSWORD: secret
HOSTS: "['akplanning.example.net', 'akplanning.example.de']"
TZ: Europe/Berlin
AUTO_MIGRATE_DB: 'true'
DJANGO_SUPERUSER_USERNAME: admin
DJANGO_SUPERUSER_EMAIL: admin@example.com
DJANGO_SUPERUSER_PASSWORD: supersecret
EXTRA_DJANGO_SETTING_FOO: DJANGO_FOO = True\nDJANGO_BAR = False
depends_on:
- mariadb
networks:
- akplanning
volumes:
- static-files:/app/static
web-server:
image: nginx
restart: always
volumes:
- /path/to/nginx.conf:/etc/nginx/nginx.conf:ro
- static-files:/var/www/akplanning-static
ports:
- "8080:80"
depends_on:
- akplanning-server
networks:
- akplanning
```
The `nginx.conf` would look like this:
```
user nginx;
worker_processes 1;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
keepalive_timeout 65;
server {
listen 80;
server_name localhost:8080;
location /static/ {
alias /var/www/akplanning-static/;
}
location / {
include /etc/nginx/uwsgi_params;
uwsgi_pass uwsgi://akplanning-server:3035;
}
}
}
```
### Initializing and migrating database
On the first start, the database must be initialized (the Tables created and so on).
When updating the project the database must be migrated.
Both are done using the `migrate` command.
This can be done manually by running the following command after the container has started:
`docker-compose exec -it akplanning-server ./manage.py migrate`
It can also be done automatically on each container start by setting `AUTO_MIGRATE_DB` to the string `true`
(as shown in the docker-compose file above).
Database migration may lead to the corruption or loss of data in some cases.
Make sure you have a backup before running the command and be very careful with enabling auto migration.
### Creating initial superuser
There are two ways to create the initial superuser when using the docker container.
For both the database must have been intialized before.
The first way is already shown in the docker-compose file above:
Using the environment variables `DJANGO_SUPERUSER_{USERNAME,EMAIL,PASSWORD}`.
The second way is to run the following command after the container has started:
`docker-compose exec -it akplanning-server ./manage.py createsuperuser`
### Extra django settings
For simple cases you can pass environment variables starting with `EXTRA_DJANGO_SETTING`.
The content of such variables is written into python files, which are loaded as settings.
For more complex scenarios you can also mount a docker volume to `/app/AKPlanning/settings` and add any number of python files to the volume.
## 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.
Afterwards, you may check your setup by executing ``Utils/check.sh`` or ``Utils/check.sh --prod`` in production.
### Updating when using docker
To update when using docker, just switch the tag of the image for `akplanning-server`.
Then (if `AUTO_MIGRATE_DB` is not enabled), do a database migration as described in [Initializing and migrating database](#initializing-and-migrating-database)
......@@ -7,116 +7,28 @@ AKPlanning is a tool used for modeling, submitting, scheduling and displaying AK
It was built for KIF (German: Konferenz der deutschsprachigen Informatikfachschaften), refer to [the wiki](wiki.kif.rocks) for more Information.
## Setup
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.
#### System Requirements
* Python 3.7 incl. development tools
* Virtualenv
* for production using uwsgi:
* C compiler e.g. gcc
* uwsgi
* uwsgi Python3 plugin
* 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 env -p python3.7``
1. activate virtualenv ``source env/bin/activate``
1. install python requirements ``pip install -r requirements.txt``
1. setup necessary database tables etc. ``python manage.py migrate``
1. create a priviledged user, credentials are entered interactively on CLI ``python manage.py createsuperuser``
1. deactivate virtualenv ``deactivate``
**Development Server**
To start the application for development use ``python manage.py runserver 0:8000`` from the root directory.
*Do not use this for deployment!*
In your browser, access ``http://127.0.0.1:8000/`` 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``).
**Step-by-Step Instructions**
1. log into your system with a sudo user
1. install system requirements
1. create a folder, e.g. ``mkdir /srv/AKPlanning/``
1. change to the new directory ``cd /srv/AKPlanning/``
1. clone this repository ``git clone URL .``
1. setup a virtual environment using the proper python version ``virtualenv env -p python3.7``
1. activate virtualenv ``source env/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. 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:
## Structure
```
Alias /static /srv/AKPlanning/static
<Directory /srv/AKPlanning/static>
Require all granted
</Directory>
This repository contains a Django project called AKPlanning. The functionality is encapsulated into Django apps:
ProxyPassMatch ^/static/ !
ProxyPass / uwsgi://127.0.0.1:3035/
```
1. **AKModel**: This app contains the general Django models used to represent events, users, rooms, scheduling constraints etc. This app is a basic requirements for the other apps. Data Import/Export also goes here.
1. **AKDashboard**: This app provides a landing page for the project. Per Event it provides links to all relevant functionalities and views.
1. **AKSubmission**: This app provides forms to submit all kinds of AKs, edit or delete them, as well as a list of all submitted AKs for an event.
1. **AKScheduling**: This app allows organizers to schedule AKs, i.e. assigning rooms, slots, etc. It marks conflicts of all modeled constraints and assists in creating a suitable schedule.
1. **AKPlan**: This app displays AKs and where/when they will take place for each event. Views are optimised according to usage/purpose.
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. 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/WannaDB``
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``
## Setup instructions
### Updates
See [INSTALL.md](INSTALL.md) for detailed instructions on development and production setups.
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.
## Structure
This repository contains a Django project called AKPlanning. The functionality is encapsulated into Django apps:
1. **AKModel**: This app contains the general Django models used to represent events, users, rooms, scheduling constraints etc. This app is a basic requirements for the other apps. Data Import/Export also goes here.
1. **AKDashboard**: This app provides a landing page for the project. Per Event it provides links to all relevant functionalities and views.
1. **AKSubmission**: This app provides forms to submit all kinds of AKs, edit or delete them, as well as a list of all submitted AKs for an event.
1. **AKScheduling**: This app allows organizers to schedule AKs, i.e. assigning rooms, slots, etc. It marks conflicts of all modeled constraints and assists in creating a suitable schedule.
1. **AKPlan**: This app displays AKs and where/when they will take place for each event. Views are optimised according to usage/purpose.
## Developer Notes
* to regenerate translations use ````python manage.py makemessages -l de_DE --ignore venv````
* to create a data backup use ````python manage.py dumpdata --indent=2 > db.json --traceback````
* to export all database items belonging to a certain event use ````./Utils/json_export.sh <event_id> <export_prefix> [--prod]````. The results will be saved in ````backups/<export_prefix>.json````
......@@ -7,3 +7,4 @@ All scripts should be executed from the project folder (repository root).
* **setup** installation script for development setup
* **update** update script for development or production (--prod) setup
* **check** setup checking script for development and production (--prod) setup
* **json_export** export script for development and production (--prod) -- can be used to export all database items belonging to a given event
\ No newline at end of file
......@@ -4,18 +4,25 @@
# activate virtualenv when necessary
if [ -z ${VIRTUAL_ENV+x} ]; then
source env/bin/activate
source venv/bin/activate
fi
# enable really all warnings, some of them are silenced by default
if [[ "$@" == *"--all"* ]]; then
export PYTHONWARNINGS=all
fi
for arg in "$@"; do
if [[ "$arg" == "--all" ]]; then
export PYTHONWARNINGS=all
fi
done
# in case of checking production setup
if [[ "$@" == *"--prod"* ]]; then
export DJANGO_SETTINGS_MODULE=AKPlanning.settings_production
./manage.py check --deploy
fi
for arg in "$@"; do
if [[ "$arg" == "--prod" ]]; then
export DJANGO_SETTINGS_MODULE=AKPlanning.settings_production
./manage.py check --deploy
./manage.py makemigrations --dry-run --check
fi
done
# check the setup
./manage.py check
./manage.py makemigrations --dry-run --check
import json
import sys
event_id = int(sys.argv[1])
target_name = sys.argv[2]
print(f"Creating export for event '{event_id}' as '{target_name}'")
# Load json file just created by django
with open('backups/akplanning_only.json', 'r') as json_file:
exported_entries = json.load(json_file)
print(f"Loaded {len(exported_entries)} entries in total, restricting to event...")
entries_without_event = 0
entries_out = []
virtual_rooms_to_preserve = set()
# Loop over all dumped entries
for entry in exported_entries:
# Handle all entries with event reference
if "event" in entry['fields']:
event = int(entry['fields']['event'])
# Does this entry belong to the event we are looking for?
if event == event_id:
# Store for backup
entries_out.append(entry)
# Remember the primary keys of all rooms of this event
# Required for special handling of virtual rooms,
# since they inherit from normal rooms and have no direct event reference
if entry['model'] == "AKModel.room":
virtual_rooms_to_preserve.add(entry['pk'])
# Handle entries without event reference
else:
# Backup virtual rooms of that event
if entry['model'] == "AKOnline.virtualroom":
if entry['pk'] in virtual_rooms_to_preserve:
entries_out.append(entry)
# Backup the event itself
elif entry['model'] == "AKModel.event":
if int(entry['pk']) == event_id:
entries_out.append(entry)
else:
# This should normally not happen (all other models should have a reference to the event)
entries_without_event += 1
print(entry)
print(f"Ignored entries without event: {entries_without_event}")
print(f"Exporting {len(entries_out)} entries for event")
with open(f'backups/{target_name}.json', 'w') as json_file:
json.dump(entries_out, json_file, indent=2)
#!/usr/bin/env bash
# Update AKPlanning
# execute as Utils/update.sh id_to_export target_name_to_export_to [--prod]
# abort on error, print executed commands
set -ex
# activate virtualenv if necessary
if [ -z ${VIRTUAL_ENV+x} ]; then
source venv/bin/activate
fi
# set environment variable when we want to update in production
if [ "$3" = "--prod" ]; then
export DJANGO_SETTINGS_MODULE=AKPlanning.settings_production
fi
mkdir -p ../backups/
python manage.py dumpdata AKDashboard AKModel AKOnline AKPlan AKScheduling AKSubmission --indent=2 > "backups/akplanning_only.json" --traceback
python ./Utils/json_export.py $1 $2
rm backups/akplanning_only.json
#!/usr/bin/env bash
# Setup AKPlanning
# execute as Utils/check.sh
# execute as Utils/setup.sh
# abort on error, print executed commands
set -ex
# remove old virtualenv
rm -rf env/
rm -rf venv/
# Setup Python Environment
# Requires: Virtualenv, appropriate Python installation
virtualenv env -p python3.7
source env/bin/activate
virtualenv venv -p python3.11
source venv/bin/activate
pip install --upgrade setuptools pip wheel
pip install -r requirements.txt
# set environment variable when we want to update in production
if [ "$1" = "--prod" ]; then
export DJANGO_SETTINGS_MODULE=AKPlanning.settings_production
fi
if [ "$1" = "--ci" ]; then
export DJANGO_SETTINGS_MODULE=AKPlanning.settings_ci
fi
# Setup database
python manage.py migrate
# Prepare static files and translations
python manage.py collectstatic --noinput
python manage.py compilemessages -l de_DE
# Create superuser
# Credentials are entered interactively on CLI
python manage.py createsuperuser
# Generate documentation (but not for CI use)
if [ -n "$1" = "--ci" ]; then
cd docs
make html
cd ..
fi
deactivate
#!/usr/bin/env bash
# Test the AKPlanning setup
# execute as Utils/test.sh
# activate virtualenv when necessary
if [ -z ${VIRTUAL_ENV+x} ]; then
source venv/bin/activate
fi
# enable really all warnings, some of them are silenced by default
for arg in "$@"; do
if [[ "$arg" == "--all" ]]; then
export PYTHONWARNINGS=all
fi
done
# in case of checking production setup
for arg in "$@"; do
if [[ "$arg" == "--prod" ]]; then
export DJANGO_SETTINGS_MODULE=AKPlanning.settings_production
./manage.py test --deploy
fi
done
# run tests
./manage.py test
#!/usr/bin/env bash
# Update AKPlanning
# execute as Utils/check.sh
# execute as Utils/update.sh
# abort on error, print executed commands
set -ex
# activate virtualenv if necessary
if [ -z ${VIRTUAL_ENV+x} ]; then
source env/bin/activate
source venv/bin/activate
fi
# set environment variable when we want to update in production
......@@ -15,13 +15,22 @@ if [ "$1" = "--prod" ]; then
export DJANGO_SETTINGS_MODULE=AKPlanning.settings_production
fi
# before potentially breaking anything, create a data backup
mkdir -p backups/
python manage.py dumpdata --indent=2 > "backups/$(date +"%Y%m%d%H%M")_datadump.json" --traceback
git pull
pip install --upgrade setuptools pip wheel
pip install --upgrade -r requirements.txt
if [ "$1" = "--prod" ]; then
./manage.py collectstatic --noinput
fi
./manage.py migrate
./manage.py collectstatic --noinput
./manage.py compilemessages -l de_DE
# Update documentation
cd docs
make html
cd ..
touch AKPlanning/wsgi.py