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
Select Git revision
  • komasolver
  • main
  • renovate/django-5.x
  • renovate/django_csp-4.x
  • renovate/jsonschema-4.x
  • renovate/uwsgi-2.x
6 results

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
Select Git revision
  • 520-akowner
  • 520-fix-event-wizard-datepicker
  • 520-fix-scheduling
  • 520-improve-scheduling
  • 520-improve-scheduling-2
  • 520-improve-submission
  • 520-improve-trackmanager
  • 520-improve-wall
  • 520-message-resolved
  • 520-status
  • 520-upgrades
  • add_express_interest_to_ak_overview
  • admin-production-color
  • bugfixes
  • csp
  • featire-ical-export
  • feature-ak-requirement-lists
  • feature-akslide-export-better-filename
  • feature-akslides
  • feature-better-admin
  • feature-better-cv-list
  • feature-colors
  • feature-constraint-checking
  • feature-constraint-checking-wip
  • feature-dashboard-history-button
  • feature-event-status
  • feature-event-wizard
  • feature-export-flag
  • feature-improve-admin
  • feature-improve-filters
  • feature-improved-user-creation-workflow
  • feature-interest-view
  • feature-mails
  • feature-modular-status
  • feature-plan-autoreload
  • feature-present-default
  • feature-register-link
  • feature-remaining-constraint-validation
  • feature-room-import
  • feature-scheduler-improve
  • feature-scheduling-2.0
  • feature-special-attention
  • feature-time-input
  • feature-tracker
  • feature-wiki-wishes
  • feature-wish-slots
  • feature-wizard-buttons
  • features-availabilities
  • fix-ak-times-above-folg
  • fix-api
  • fix-constraint-violation-string
  • fix-cv-checking
  • fix-default-slot-length
  • fix-default-slot-localization
  • fix-doc-minor
  • fix-duration-display
  • fix-event-tz-pytz-update
  • fix-history-interest
  • fix-interest-view
  • fix-js
  • fix-pipeline
  • fix-plan-timezone-now
  • fix-room-add
  • fix-scheduling-drag
  • fix-slot-defaultlength
  • fix-timezone
  • fix-translation-scheduling
  • fix-virtual-room-admin
  • fix-wizard-csp
  • font-locally
  • improve-admin
  • improve-online
  • improve-slides
  • improve-submission-coupling
  • interest_restriction
  • main
  • master
  • meta-debug-toolbar
  • meta-export
  • meta-makemessages
  • meta-performance
  • meta-tests
  • meta-tests-gitlab-test
  • meta-upgrades
  • mollux-master-patch-02906
  • port-availabilites-fullcalendar
  • qs
  • remove-tags
  • renovate/configure
  • renovate/django-4.x
  • renovate/django-5.x
  • renovate/django-bootstrap-datepicker-plus-5.x
  • renovate/django-bootstrap5-23.x
  • renovate/django-bootstrap5-24.x
  • renovate/django-compressor-4.x
  • renovate/django-debug-toolbar-4.x
  • renovate/django-registration-redux-2.x
  • renovate/django-simple-history-3.x
  • renovate/django-split-settings-1.x
  • renovate/django-timezone-field-5.x
100 results
Show changes
Showing
with 772 additions and 106 deletions
...@@ -27,6 +27,55 @@ ...@@ -27,6 +27,55 @@
{% block imports %} {% block imports %}
{% include "AKPlan/plan_akslot.html" %} {% include "AKPlan/plan_akslot.html" %}
<script type="module">
const { createApp } = Vue
function getCurrentTimestamp() {
return Date.now() / 1000
}
createApp({
delimiters: ["[[", "]]"],
data() {
return {
featuredSlot: "{% if featured_slot %}true{% else %}false{% endif %}",
timer: null,
now: getCurrentTimestamp(),
akStart: "{{ featured_slot.start | date:'U' }}",
akEnd: "{{ featured_slot.end | date:'U' }}",
showBoxWithoutJS: false,
}
},
computed: {
showFeatured() {
return this.featuredSlot && this.now < this.akEnd
},
isBefore() {
return this.featuredSlot && this.now < this.akStart
},
isDuring() {
return this.featuredSlot && this.akStart < this.now && this.now < this.akEnd
},
timeUntilStart() {
return Math.ceil((this.akStart - this.now) / 60)
},
timeUntilEnd() {
return Math.floor((this.akEnd - this.now) / 60)
}
},
mounted: function () {
if(this.featuredSlot) {
this.timer = setInterval(() => {
this.now = getCurrentTimestamp()
}, 10000)
}
},
beforeUnmount() {
clearInterval(this.timer)
}
}).mount('#app')
</script>
<script> <script>
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', function () {
// CSRF Protection/Authentication // CSRF Protection/Authentication
...@@ -128,30 +177,28 @@ ...@@ -128,30 +177,28 @@
<h2>{% if ak.wish %}{% trans "AK Wish" %}: {% endif %}{{ ak.name }}</h2> <h2>{% if ak.wish %}{% trans "AK Wish" %}: {% endif %}{{ ak.name }}</h2>
{# Show current or upcoming slot featured in a box on top of the plage #} <div id="app">
{% if featured_slot_type != "NONE" %} {# Show current or upcoming slot featured in a box on top of the plage #}
<div class="card border-success mt-3 mb-3"> {% if featured_slot_type != "NONE" %}
<div class="card-body font-weight-bold"> <div class="card border-success mt-3 mb-3" v-show="showFeatured">
{% if featured_slot_type == "CURRENT" %} <div class="card-body font-weight-bold">
{% blocktrans with room=featured_slot.room %} <span v-show="isDuring" style="{% if not featured_slot_type == "CURRENT" %}display:none;{% endif %}">
This AK currently takes place for another {{ featured_slot_remaining }} minute(s) in {{ room }}. {% blocktrans with room=featured_slot.room %}This AK currently takes place for another <span v-html="timeUntilEnd">{{ featured_slot_remaining }}</span> minute(s) in {{ room }}.&nbsp;{% endblocktrans %}
&nbsp; </span>
{% endblocktrans %} <span v-show="isBefore" style="{% if not featured_slot_type == "UPCOMING" %}display:none;{% endif %}">
{% blocktrans with room=featured_slot.room %}This AK starts in <span v-html="timeUntilStart">{{ featured_slot_remaining }}</span> minute(s) in {{ room }}.&nbsp;{% endblocktrans %}
{% elif featured_slot_type == "UPCOMING" %} </span>
{% blocktrans with room=featured_slot.room %}
This AK starts in {{ featured_slot_remaining }} minute(s) in {{ room }}.&nbsp;
{% endblocktrans %}
{% endif %}
{% if "AKOnline"|check_app_installed and featured_slot.room.virtual and featured_slot.room.virtual.url != '' %} {% if "AKOnline"|check_app_installed and featured_slot.room.virtual and featured_slot.room.virtual.url != '' %}
<a class="btn btn-success" target="_parent" href="{{ featured_slot.room.virtual.url }}"> <a class="btn btn-success" target="_parent" href="{{ featured_slot.room.virtual.url }}">
{% fa6_icon 'external-link-alt' 'fas' %} {% trans "Go to virtual room" %} {% fa6_icon 'external-link-alt' 'fas' %} {% trans "Go to virtual room" %}
</a> </a>
{% endif %} {% endif %}
</div>
</div> </div>
</div> {% endif %}
{% endif %} </div>
<table class="table table-borderless"> <table class="table table-borderless">
<tr> <tr>
...@@ -166,6 +213,16 @@ ...@@ -166,6 +213,16 @@
{% category_linked_badge ak.category ak.event.slug %} {% category_linked_badge ak.category ak.event.slug %}
</td> </td>
</tr> </tr>
{% if ak.types.count > 0 %}
<tr>
<td>{% trans "Types" %}</td>
<td>
{% for type in ak.types.all %}
<span class="badge bg-info">{{ type }}</span>
{% endfor %}
</td>
</tr>
{% endif %}
{% if ak.track %} {% if ak.track %}
<tr> <tr>
<td>{% trans 'Track' %}</td> <td>{% trans 'Track' %}</td>
......
...@@ -9,6 +9,9 @@ ...@@ -9,6 +9,9 @@
<th>{% trans "Name" %}</th> <th>{% trans "Name" %}</th>
<th>{% trans "Who?" %}</th> <th>{% trans "Who?" %}</th>
<th>{% trans 'Category' %}</th> <th>{% trans 'Category' %}</th>
{% if show_types %}
<th>{% trans 'Types' %}</th>
{% endif %}
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
...@@ -37,6 +40,13 @@ ...@@ -37,6 +40,13 @@
{% endif %} {% endif %}
</td> </td>
<td>{% category_linked_badge ak.category event.slug %}</td> <td>{% category_linked_badge ak.category event.slug %}</td>
{% if show_types %}
<td>
{% for aktype in ak.types.all %}
<span class="badge bg-info">{{ aktype }}</span>
{% endfor %}
</td>
{% endif %}
<td class="text-end" style="white-space: nowrap;"> <td class="text-end" style="white-space: nowrap;">
<a href="{{ ak.detail_url }}" data-bs-toggle="tooltip" <a href="{{ ak.detail_url }}" data-bs-toggle="tooltip"
title="{% trans 'Details' %}" title="{% trans 'Details' %}"
......
...@@ -23,6 +23,13 @@ ...@@ -23,6 +23,13 @@
); );
}); });
</script> </script>
<style>
#id_present_helptext::after {
content: " ({% trans "only relevant for KIF-AKs - determines whether the AK appears in the slides for the closing plenary session" %})";
color: #6c757d;
}
</style>
{% endblock %} {% endblock %}
{% block breadcrumbs %} {% block breadcrumbs %}
......
...@@ -6,6 +6,11 @@ register = template.Library() ...@@ -6,6 +6,11 @@ register = template.Library()
@register.filter @register.filter
def bool_symbol(bool_val): 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: if bool_val:
return fa6_icon("check", "fas") return fa6_icon("check", "fas")
return fa6_icon("times", "fas") return fa6_icon("times", "fas")
...@@ -13,14 +18,34 @@ def bool_symbol(bool_val): ...@@ -13,14 +18,34 @@ def bool_symbol(bool_val):
@register.inclusion_tag("AKSubmission/tracks_list.html") @register.inclusion_tag("AKSubmission/tracks_list.html")
def track_list(tracks, event_slug): 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} return {"tracks": tracks, "event_slug": event_slug}
@register.inclusion_tag("AKSubmission/category_list.html") @register.inclusion_tag("AKSubmission/category_list.html")
def category_list(categories, event_slug): 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} return {"categories": categories, "event_slug": event_slug}
@register.inclusion_tag("AKSubmission/category_linked_badge.html") @register.inclusion_tag("AKSubmission/category_linked_badge.html")
def category_linked_badge(category, event_slug): 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} return {"category": category, "event_slug": event_slug}
from datetime import timedelta from datetime import datetime, timedelta
from django.test import TestCase from django.test import TestCase
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils.datetime_safe import datetime
from AKModel.models import AK, AKSlot, Event from AKModel.models import AK, AKSlot, Event
from AKModel.tests import BasicViewTests from AKModel.tests.test_views import BasicViewTests
from AKSubmission.forms import AKSubmissionForm
class ModelViewTests(BasicViewTests, TestCase): 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'] fixtures = ['model.json']
VIEWS = [ VIEWS = [
...@@ -37,8 +46,8 @@ class ModelViewTests(BasicViewTests, TestCase): ...@@ -37,8 +46,8 @@ class ModelViewTests(BasicViewTests, TestCase):
'expected_message': "AK successfully updated"}, 'expected_message': "AK successfully updated"},
{'view': 'akslot_edit', 'target_view': 'ak_detail', 'kwargs': {'event_slug': 'kif42', 'pk': 5}, {'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"}, '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'}, {'view': 'akowner_edit', 'target_view': 'submission_overview', 'kwargs': {'event_slug': 'kif42', 'slug': 'a'},
'target_kwargs': {'event_slug': 'kif42'}, 'expected_message': "Person Info successfully updated"}, 'target_kwargs': {'event_slug': 'kif42'}, 'expected_message': "Person Info successfully updated"},
] ]
def test_akslot_edit_delete_prevention(self): def test_akslot_edit_delete_prevention(self):
...@@ -47,24 +56,27 @@ class ModelViewTests(BasicViewTests, TestCase): ...@@ -47,24 +56,27 @@ class ModelViewTests(BasicViewTests, TestCase):
""" """
self.client.logout() self.client.logout()
view_name_with_prefix, url = self._name_and_url(('akslot_edit', {'event_slug': 'kif42', 'pk': 1})) _, url = self._name_and_url(('akslot_edit', {'event_slug': 'kif42', 'pk': 1}))
response = self.client.get(url) response = self.client.get(url)
self.assertEqual(response.status_code, 302, self.assertEqual(response.status_code, 302,
msg=f"AK Slot editing ({url}) possible even though slot was already scheduled") 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") self._assert_message(response, "You cannot edit a slot that has already been scheduled")
view_name_with_prefix, url = self._name_and_url(('akslot_delete', {'event_slug': 'kif42', 'pk': 1})) _, url = self._name_and_url(('akslot_delete', {'event_slug': 'kif42', 'pk': 1}))
response = self.client.get(url) response = self.client.get(url)
self.assertEqual(response.status_code, 302, self.assertEqual(response.status_code, 302,
msg=f"AK Slot deletion ({url}) possible even though slot was already scheduled") 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") self._assert_message(response, "You cannot delete a slot that has already been scheduled")
def test_slot_creation_deletion(self): def test_slot_creation_deletion(self):
"""
Test creation and deletion of slots in frontend
"""
ak_args = {'event_slug': 'kif42', 'pk': 1} ak_args = {'event_slug': 'kif42', 'pk': 1}
redirect_url = reverse_lazy(f"{self.APP_NAME}:ak_detail", kwargs=ak_args) 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() count_slots = AK.objects.get(pk=1).akslot_set.count()
create_url = reverse_lazy(f"{self.APP_NAME}:akslot_add", kwargs=ak_args) 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}) 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, self.assertRedirects(response, redirect_url, status_code=302, target_status_code=200,
...@@ -75,6 +87,8 @@ class ModelViewTests(BasicViewTests, TestCase): ...@@ -75,6 +87,8 @@ class ModelViewTests(BasicViewTests, TestCase):
# Get primary key of newly created Slot # Get primary key of newly created Slot
slot_pk = AK.objects.get(pk=1).akslot_set.order_by('pk').last().pk 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}) edit_url = reverse_lazy(f"{self.APP_NAME}:akslot_edit", kwargs={'event_slug': 'kif42', 'pk': slot_pk})
response = self.client.get(edit_url) response = self.client.get(edit_url)
self.assertEqual(response.status_code, 200, msg=f"Cant open edit view for newly created slot ({edit_url})") self.assertEqual(response.status_code, 200, msg=f"Cant open edit view for newly created slot ({edit_url})")
...@@ -84,6 +98,8 @@ class ModelViewTests(BasicViewTests, TestCase): ...@@ -84,6 +98,8 @@ class ModelViewTests(BasicViewTests, TestCase):
self.assertEqual(AKSlot.objects.get(pk=slot_pk).duration, 2, self.assertEqual(AKSlot.objects.get(pk=slot_pk).duration, 2,
msg="Slot was not correctly changed") 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}) deletion_url = reverse_lazy(f"{self.APP_NAME}:akslot_delete", kwargs={'event_slug': 'kif42', 'pk': slot_pk})
response = self.client.get(deletion_url) response = self.client.get(deletion_url)
self.assertEqual(response.status_code, 200, self.assertEqual(response.status_code, 200,
...@@ -95,55 +111,78 @@ class ModelViewTests(BasicViewTests, TestCase): ...@@ -95,55 +111,78 @@ class ModelViewTests(BasicViewTests, TestCase):
self.assertEqual(AK.objects.get(pk=1).akslot_set.count(), count_slots, msg="AK still has to many slots") 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): def test_ak_owner_editing(self):
# Test editing of new user """
Test dispatch of user editing requests
"""
edit_url = reverse_lazy(f"{self.APP_NAME}:akowner_edit_dispatch", kwargs={'event_slug': 'kif42'}) 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'}) 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}) response = self.client.post(edit_url, {'owner_id': -1})
self.assertRedirects(response, base_url, status_code=302, target_status_code=200, 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") msg_prefix="Did not redirect to start page even though no user was selected")
self._assert_message(response, "No user 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'}) 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}) response = self.client.post(edit_url, {'owner_id': 1})
self.assertRedirects(response, edit_redirect_url, status_code=302, target_status_code=200, 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})") msg_prefix=f"Dispatch redirect failed (should go to {edit_redirect_url})")
def test_ak_owner_selection(self): 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'}) 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'}) 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}) response = self.client.post(select_url, {'owner_id': -1})
self.assertRedirects(response, create_url, status_code=302, target_status_code=200, 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") 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'}) 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}) response = self.client.post(select_url, {'owner_id': 1})
self.assertRedirects(response, add_redirect_url, status_code=302, target_status_code=200, self.assertRedirects(response, add_redirect_url, status_code=302, target_status_code=200,
msg_prefix=f"Dispatch redirect to ak submission page failed (should go to {add_redirect_url})") msg_prefix=f"Dispatch redirect to ak submission page failed "
f"(should go to {add_redirect_url})")
def test_orga_message_submission(self): 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}) 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}) 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() count_messages = AK.objects.get(pk=1).akorgamessage_set.count()
# Test that submission view is accessible
response = self.client.get(form_url) response = self.client.get(form_url)
self.assertEqual(response.status_code, 200, msg="Could not load message form view") 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'}) 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, 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})") 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._assert_message(response, "Message to organizers successfully saved")
self.assertEqual(AK.objects.get(pk=1).akorgamessage_set.count(), count_messages + 1, self.assertEqual(AK.objects.get(pk=1).akorgamessage_set.count(), count_messages + 1,
msg="Message was not correctly saved") msg="Message was not correctly saved")
def test_interest_api(self): def test_interest_api(self):
"""
Test interest indicating API (access, functionality)
"""
interest_api_url = "/kif42/api/ak/1/indicate-interest/" interest_api_url = "/kif42/api/ak/1/indicate-interest/"
ak = AK.objects.get(pk=1) ak = AK.objects.get(pk=1)
event = Event.objects.get(slug='kif42') event = Event.objects.get(slug='kif42')
ak_interest_counter = ak.interest_counter ak_interest_counter = ak.interest_counter
# Check Access method (only POST)
response = self.client.get(interest_api_url) response = self.client.get(interest_api_url)
self.assertEqual(response.status_code, 405, "Should not be accessible via GET") self.assertEqual(response.status_code, 405, "Should not be accessible via GET")
...@@ -151,6 +190,7 @@ class ModelViewTests(BasicViewTests, TestCase): ...@@ -151,6 +190,7 @@ class ModelViewTests(BasicViewTests, TestCase):
event.interest_end = datetime.now().astimezone(event.timezone) + timedelta(minutes=+10) event.interest_end = datetime.now().astimezone(event.timezone) + timedelta(minutes=+10)
event.save() event.save()
# Test correct indication -> HTTP 200, counter increased
response = self.client.post(interest_api_url) response = self.client.post(interest_api_url)
self.assertEqual(response.status_code, 200, f"API end point not working ({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") self.assertEqual(AK.objects.get(pk=1).interest_counter, ak_interest_counter + 1, "Counter was not increased")
...@@ -158,31 +198,79 @@ class ModelViewTests(BasicViewTests, TestCase): ...@@ -158,31 +198,79 @@ class ModelViewTests(BasicViewTests, TestCase):
event.interest_end = datetime.now().astimezone(event.timezone) + timedelta(minutes=-2) event.interest_end = datetime.now().astimezone(event.timezone) + timedelta(minutes=-2)
event.save() event.save()
# Test indication outside of indication window -> HTTP 403, counter not increased
response = self.client.post(interest_api_url) response = self.client.post(interest_api_url)
self.assertEqual(response.status_code, 403, self.assertEqual(response.status_code, 403,
"API end point still reachable even though interest indication window ended ({interest_api_url})") "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, self.assertEqual(AK.objects.get(pk=1).interest_counter, ak_interest_counter + 1,
"Counter was increased even though interest indication window ended") "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/" invalid_interest_api_url = "/kif42/api/ak/-1/indicate-interest/"
response = self.client.post(invalid_interest_api_url) response = self.client.post(invalid_interest_api_url)
self.assertEqual(response.status_code, 404, f"Invalid URL reachable ({interest_api_url})") self.assertEqual(response.status_code, 404, f"Invalid URL reachable ({interest_api_url})")
def test_adding_of_unknown_user(self): 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}) detail_url = reverse_lazy(f"{self.APP_NAME}:ak_detail", kwargs={'event_slug': 'kif42', 'pk': 1})
response = self.client.get(detail_url) response = self.client.get(detail_url)
self.assertEqual(response.status_code, 200, msg="Could not load ak detail view") 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}) edit_url = reverse_lazy(f"{self.APP_NAME}:ak_edit", kwargs={'event_slug': 'kif42', 'pk': 1})
response = self.client.get(edit_url) response = self.client.get(edit_url)
self.assertEqual(response.status_code, 200, msg="Could not load ak detail view") self.assertEqual(response.status_code, 200, msg="Could not load ak detail view")
self.assertContains(response, "Add person not in the list yet", self.assertContains(response, "Add person not in the list yet",
msg_prefix="Link to add unknown user not contained") 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) 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'}) + f"?add_to_existing_ak=1" add_new_user_to_ak_url = reverse_lazy(f"{self.APP_NAME}:akowner_create", kwargs={'event_slug': 'kif42'}) \
response = self.client.post(add_new_user_to_ak_url, {'name': 'New test owner', 'event': Event.get_by_slug('kif42').pk}) + "?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, self.assertRedirects(response, detail_url,
msg_prefix=f"No correct redirect: {add_new_user_to_ak_url} (POST) -> {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._assert_message(response, "Added 'New test owner' as new owner of 'Test AK Inhalt'")
self.assertEqual(AK.objects.get(pk=1).owners.count(), 2) 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")
from datetime import timedelta from abc import ABC, abstractmethod
from datetime import datetime, timedelta
from math import floor from math import floor
from django.apps import apps from django.apps import apps
...@@ -7,43 +8,88 @@ from django.contrib import messages ...@@ -7,43 +8,88 @@ from django.contrib import messages
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, redirect from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils.datetime_safe import datetime
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views import View from django.views import View
from django.views.generic import ListView, DetailView, CreateView, UpdateView, DeleteView, TemplateView from django.views.generic import CreateView, DeleteView, DetailView, ListView, TemplateView, UpdateView
from AKModel.availability.models import Availability from AKModel.availability.models import Availability
from AKModel.metaviews import status_manager from AKModel.metaviews import status_manager
from AKModel.metaviews.status import TemplateStatusWidget
from AKModel.models import AK, AKCategory, AKOwner, AKSlot, AKTrack, AKOrgaMessage
from AKModel.metaviews.admin import EventSlugMixin, FilterByEventSlugMixin 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.api import ak_interest_indication_active
from AKSubmission.forms import AKWishForm, AKOwnerForm, AKSubmissionForm, AKDurationForm, AKOrgaMessageForm, \ from AKSubmission.forms import AKDurationForm, AKForm, AKOrgaMessageForm, AKOwnerForm, AKSubmissionForm, AKWishForm
AKForm
class SubmissionErrorNotConfiguredView(EventSlugMixin, TemplateView): 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" template_name = "AKSubmission/submission_not_configured.html"
class AKOverviewView(FilterByEventSlugMixin, ListView): 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 model = AKCategory
context_object_name = "categories" context_object_name = "categories"
template_name = "AKSubmission/ak_overview.html" template_name = "AKSubmission/ak_overview.html"
wishes_as_category = False wishes_as_category = False
def filter_aks(self, context, category): def filter_aks(self, context, category): # pylint: disable=unused-argument
return category.ak_set.select_related('event').prefetch_related('owners').all() """
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): 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 return context["categories_with_aks"][0][0].name
def get_table_title(self, context): 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") return _("All AKs")
def get(self, request, *args, **kwargs): 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._load_event()
self.object_list = self.get_queryset() self.object_list = self.get_queryset() # pylint: disable=attribute-defined-outside-init
# No categories yet? Redirect to configuration error page # No categories yet? Redirect to configuration error page
if self.object_list.count() == 0: if self.object_list.count() == 0:
...@@ -55,10 +101,16 @@ class AKOverviewView(FilterByEventSlugMixin, ListView): ...@@ -55,10 +101,16 @@ class AKOverviewView(FilterByEventSlugMixin, ListView):
def get_context_data(self, *, object_list=None, **kwargs): def get_context_data(self, *, object_list=None, **kwargs):
context = super().get_context_data(object_list=object_list, **kwargs) context = super().get_context_data(object_list=object_list, **kwargs)
# ==========================================================
# Sort AKs into different lists (by their category) # Sort AKs into different lists (by their category)
# ==========================================================
ak_wishes = [] ak_wishes = []
categories_with_aks = [] 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"]: for category in context["categories"]:
aks_for_category = [] aks_for_category = []
for ak in self.filter_aks(context, category): for ak in self.filter_aks(context, category):
...@@ -70,13 +122,17 @@ class AKOverviewView(FilterByEventSlugMixin, ListView): ...@@ -70,13 +122,17 @@ class AKOverviewView(FilterByEventSlugMixin, ListView):
if self.wishes_as_category: if self.wishes_as_category:
categories_with_aks.append( categories_with_aks.append(
(AKCategory(name=_("Wishes"), pk=0, description=_("AKs one would like to have")), ak_wishes)) (AKCategory(name=_("Wishes"), pk=0, description=_("AKs one would like to have")), ak_wishes))
context["categories_with_aks"] = categories_with_aks context["categories_with_aks"] = categories_with_aks
context["active_category"] = self.get_active_category_name(context) context["active_category"] = self.get_active_category_name(context)
context['table_title'] = self.get_table_title(context) context['table_title'] = self.get_table_title(context)
context['show_types'] = self.event.aktype_set.count() > 0
# ==========================================================
# Display interest indication button? # Display interest indication button?
# ==========================================================
current_timestamp = datetime.now().astimezone(self.event.timezone) current_timestamp = datetime.now().astimezone(self.event.timezone)
context['interest_indication_active'] = ak_interest_indication_active(self.event, current_timestamp) context['interest_indication_active'] = ak_interest_indication_active(self.event, current_timestamp)
...@@ -84,12 +140,30 @@ class AKOverviewView(FilterByEventSlugMixin, ListView): ...@@ -84,12 +140,30 @@ class AKOverviewView(FilterByEventSlugMixin, ListView):
class SubmissionOverviewView(AKOverviewView): 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 model = AKCategory
context_object_name = "categories" context_object_name = "categories"
template_name = "AKSubmission/submission_overview.html" template_name = "AKSubmission/submission_overview.html"
# 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 wishes_as_category = settings.WISHES_AS_CATEGORY
def get_table_title(self, context): 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") return _("Currently planned AKs")
def get_context_data(self, *, object_list=None, **kwargs): def get_context_data(self, *, object_list=None, **kwargs):
...@@ -102,32 +176,75 @@ class SubmissionOverviewView(AKOverviewView): ...@@ -102,32 +176,75 @@ class SubmissionOverviewView(AKOverviewView):
class AKListByCategoryView(AKOverviewView): 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): 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']) self.category = get_object_or_404(AKCategory, pk=kwargs['category_pk'])
return super().dispatch(request, *args, **kwargs) return super().dispatch(request, *args, **kwargs)
def get_active_category_name(self, context): 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 return self.category.name
class AKListByTrackView(AKOverviewView): 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): def dispatch(self, request, *args, **kwargs):
self.track = get_object_or_404(AKTrack, pk=kwargs['track_pk']) # 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) return super().dispatch(request, *args, **kwargs)
def filter_aks(self, context, category): def filter_aks(self, context, category):
return category.ak_set.filter(track=self.track) """
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): def get_table_title(self, context):
return f"{_('AKs with Track')} = {self.track.name}" return f"{_('AKs with Track')} = {self.track.name}"
class AKDetailView(EventSlugMixin, DetailView): class AKDetailView(EventSlugMixin, DetailView):
"""
View: AK Details
"""
model = AK model = AK
context_object_name = "ak" context_object_name = "ak"
template_name = "AKSubmission/ak_detail.html" template_name = "AKSubmission/ak_detail.html"
def get_queryset(self): def get_queryset(self):
# Get information about the AK and do some query optimization
return super().get_queryset().select_related('event').prefetch_related('owners') return super().get_queryset().select_related('event').prefetch_related('owners')
def get_context_data(self, *, object_list=None, **kwargs): def get_context_data(self, *, object_list=None, **kwargs):
...@@ -163,29 +280,35 @@ class AKDetailView(EventSlugMixin, DetailView): ...@@ -163,29 +280,35 @@ class AKDetailView(EventSlugMixin, DetailView):
class AKHistoryView(EventSlugMixin, DetailView): class AKHistoryView(EventSlugMixin, DetailView):
"""
View: Show history of a given AK
"""
model = AK model = AK
context_object_name = "ak" context_object_name = "ak"
template_name = "AKSubmission/ak_history.html" template_name = "AKSubmission/ak_history.html"
class AKListView(FilterByEventSlugMixin, ListView):
model = AK
context_object_name = "AKs"
template_name = "AKSubmission/ak_overview.html"
table_title = ""
def get_context_data(self, *, object_list=None, **kwargs):
context = super().get_context_data(object_list=object_list, **kwargs)
context['categories'] = AKCategory.objects.filter(event=self.event)
context['tracks'] = AKTrack.objects.filter(event=self.event)
return context
class EventInactiveRedirectMixin: 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
"""
def get_error_message(self): def get_error_message(self):
"""
Error message to display after redirect (can be adjusted by this method)
:return: error message
:rtype: str
"""
return _("Event inactive. Cannot create or update.") return _("Event inactive. Cannot create or update.")
def get(self, request, *args, **kwargs): 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) s = super().get(request, *args, **kwargs)
if not self.event.active: if not self.event.active:
messages.add_message(self.request, messages.ERROR, self.get_error_message()) messages.add_message(self.request, messages.ERROR, self.get_error_message())
...@@ -194,6 +317,11 @@ class EventInactiveRedirectMixin: ...@@ -194,6 +317,11 @@ class EventInactiveRedirectMixin:
class AKAndAKWishSubmissionView(EventSlugMixin, EventInactiveRedirectMixin, 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 model = AK
template_name = 'AKSubmission/submit_new.html' template_name = 'AKSubmission/submit_new.html'
form_class = AKSubmissionForm form_class = AKSubmissionForm
...@@ -221,7 +349,15 @@ class AKAndAKWishSubmissionView(EventSlugMixin, EventInactiveRedirectMixin, Crea ...@@ -221,7 +349,15 @@ class AKAndAKWishSubmissionView(EventSlugMixin, EventInactiveRedirectMixin, Crea
class AKSubmissionView(AKAndAKWishSubmissionView): class AKSubmissionView(AKAndAKWishSubmissionView):
"""
View: AK submission form
Extends :class:`AKAndAKWishSubmissionView`
"""
def get_initial(self): 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 = super(AKAndAKWishSubmissionView, self).get_initial()
initials['owners'] = [AKOwner.get_by_slug(self.event, self.kwargs['owner_slug'])] initials['owners'] = [AKOwner.get_by_slug(self.event, self.kwargs['owner_slug'])]
initials['event'] = self.event initials['event'] = self.event
...@@ -234,33 +370,54 @@ class AKSubmissionView(AKAndAKWishSubmissionView): ...@@ -234,33 +370,54 @@ class AKSubmissionView(AKAndAKWishSubmissionView):
class AKWishSubmissionView(AKAndAKWishSubmissionView): class AKWishSubmissionView(AKAndAKWishSubmissionView):
"""
View: Wish submission form
Extends :class:`AKAndAKWishSubmissionView`
"""
template_name = 'AKSubmission/submit_new_wish.html' template_name = 'AKSubmission/submit_new_wish.html'
form_class = AKWishForm form_class = AKWishForm
def get_initial(self): 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 = super(AKAndAKWishSubmissionView, self).get_initial()
initials['event'] = self.event initials['event'] = self.event
return initials return initials
class AKEditView(EventSlugMixin, EventInactiveRedirectMixin, UpdateView): 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 model = AK
template_name = 'AKSubmission/ak_edit.html' template_name = 'AKSubmission/ak_edit.html'
form_class = AKForm form_class = AKForm
def get_success_url(self): 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")) messages.add_message(self.request, messages.SUCCESS, _("AK successfully updated"))
return self.object.detail_url return self.object.detail_url
def form_valid(self, form): def form_valid(self, form):
# Handle valid form submission
# Only save when event is active, otherwise redirect
if not form.cleaned_data["event"].active: if not form.cleaned_data["event"].active:
messages.add_message(self.request, messages.ERROR, self.get_error_message()) messages.add_message(self.request, messages.ERROR, self.get_error_message())
return redirect(reverse_lazy('submit:submission_overview', return redirect(reverse_lazy('submit:submission_overview',
kwargs={'event_slug': form.cleaned_data["event"].slug})) 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() previous_owner_count = self.object.owners.count()
super_form_valid = super().form_valid(form) # 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? # Did this AK change from wish to AK or vice versa?
new_owner_count = self.object.owners.count() new_owner_count = self.object.owners.count()
...@@ -273,15 +430,23 @@ class AKEditView(EventSlugMixin, EventInactiveRedirectMixin, UpdateView): ...@@ -273,15 +430,23 @@ class AKEditView(EventSlugMixin, EventInactiveRedirectMixin, UpdateView):
# Delete all unscheduled slots # Delete all unscheduled slots
self.object.akslot_set.filter(start__isnull=True).delete() self.object.akslot_set.filter(start__isnull=True).delete()
return super_form_valid # Redirect to success url
return redirect_response
class AKOwnerCreateView(EventSlugMixin, EventInactiveRedirectMixin, CreateView): class AKOwnerCreateView(EventSlugMixin, EventInactiveRedirectMixin, CreateView):
"""
View: Create a new owner
"""
model = AKOwner model = AKOwner
template_name = 'AKSubmission/akowner_create_update.html' template_name = 'AKSubmission/akowner_create_update.html'
form_class = AKOwnerForm form_class = AKOwnerForm
def get_success_url(self): 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: if "add_to_existing_ak" in self.request.GET:
ak_pk = self.request.GET['add_to_existing_ak'] ak_pk = self.request.GET['add_to_existing_ak']
ak = get_object_or_404(AK, pk=ak_pk) ak = get_object_or_404(AK, pk=ak_pk)
...@@ -289,15 +454,20 @@ class AKOwnerCreateView(EventSlugMixin, EventInactiveRedirectMixin, CreateView): ...@@ -289,15 +454,20 @@ class AKOwnerCreateView(EventSlugMixin, EventInactiveRedirectMixin, CreateView):
messages.add_message(self.request, messages.SUCCESS, messages.add_message(self.request, messages.SUCCESS,
_("Added '{owner}' as new owner of '{ak.name}'").format(owner=self.object, ak=ak)) _("Added '{owner}' as new owner of '{ak.name}'").format(owner=self.object, ak=ak))
return ak.detail_url 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', return reverse_lazy('submit:submit_ak',
kwargs={'event_slug': self.kwargs['event_slug'], 'owner_slug': self.object.slug}) kwargs={'event_slug': self.kwargs['event_slug'], 'owner_slug': self.object.slug})
def get_initial(self): def get_initial(self):
initials = super(AKOwnerCreateView, self).get_initial() # 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 initials['event'] = self.event
return initials return initials
def form_valid(self, form): def form_valid(self, form):
# Prevent changes if event is not active
if not form.cleaned_data["event"].active: if not form.cleaned_data["event"].active:
messages.add_message(self.request, messages.ERROR, self.get_error_message()) messages.add_message(self.request, messages.ERROR, self.get_error_message())
return redirect(reverse_lazy('submit:submission_overview', return redirect(reverse_lazy('submit:submission_overview',
...@@ -305,26 +475,97 @@ class AKOwnerCreateView(EventSlugMixin, EventInactiveRedirectMixin, CreateView): ...@@ -305,26 +475,97 @@ class AKOwnerCreateView(EventSlugMixin, EventInactiveRedirectMixin, CreateView):
return super().form_valid(form) return super().form_valid(form)
class AKOwnerSelectDispatchView(EventSlugMixin, View): class AKOwnerDispatchView(ABC, EventSlugMixin, View):
""" """
This view only serves as redirect to prepopulate the owners field in submission create 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): 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: if "owner_id" not in request.POST:
return redirect('submit:submission_overview', event_slug=kwargs['event_slug']) return redirect('submit:submission_overview', event_slug=kwargs['event_slug'])
owner_id = request.POST["owner_id"] 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": if owner_id == "-1":
return HttpResponseRedirect( return self.get_new_owner_redirect(kwargs['event_slug'])
reverse_lazy('submit:akowner_create', kwargs={'event_slug': 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"]) owner = get_object_or_404(AKOwner, pk=request.POST["owner_id"])
return HttpResponseRedirect( return self.get_valid_owner_redirect(kwargs['event_slug'], owner)
reverse_lazy('submit:submit_ak', kwargs={'event_slug': kwargs['event_slug'], 'owner_slug': owner.slug}))
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, UpdateView):
class AKOwnerEditView(FilterByEventSlugMixin, EventSlugMixin, EventInactiveRedirectMixin, UpdateView):
"""
View: Edit an owner
"""
model = AKOwner model = AKOwner
template_name = "AKSubmission/akowner_create_update.html" template_name = "AKSubmission/akowner_create_update.html"
form_class = AKOwnerForm form_class = AKOwnerForm
...@@ -334,6 +575,7 @@ class AKOwnerEditView(FilterByEventSlugMixin, EventSlugMixin, UpdateView): ...@@ -334,6 +575,7 @@ class AKOwnerEditView(FilterByEventSlugMixin, EventSlugMixin, UpdateView):
return reverse_lazy('submit:submission_overview', kwargs={'event_slug': self.kwargs['event_slug']}) return reverse_lazy('submit:submission_overview', kwargs={'event_slug': self.kwargs['event_slug']})
def form_valid(self, form): def form_valid(self, form):
# Prevent updating if event is not active
if not form.cleaned_data["event"].active: if not form.cleaned_data["event"].active:
messages.add_message(self.request, messages.ERROR, self.get_error_message()) messages.add_message(self.request, messages.ERROR, self.get_error_message())
return redirect(reverse_lazy('submit:submission_overview', return redirect(reverse_lazy('submit:submission_overview',
...@@ -341,33 +583,19 @@ class AKOwnerEditView(FilterByEventSlugMixin, EventSlugMixin, UpdateView): ...@@ -341,33 +583,19 @@ class AKOwnerEditView(FilterByEventSlugMixin, EventSlugMixin, UpdateView):
return super().form_valid(form) return super().form_valid(form)
class AKOwnerEditDispatchView(EventSlugMixin, View): class AKSlotAddView(EventSlugMixin, EventInactiveRedirectMixin, CreateView):
"""
This view only serves as redirect choose the correct edit view
""" """
View: Add an additional slot to an AK
The user has to select the duration of the slot in this view
def post(self, request, *args, **kwargs): The view will only process the request when the event is active (as steered by :class:`EventInactiveRedirectMixin`)
if "owner_id" not in request.POST: """
return redirect('submit:submission_overview', event_slug=kwargs['event_slug'])
owner_id = request.POST["owner_id"]
if owner_id == "-1":
messages.add_message(self.request, messages.WARNING, _("No user selected"))
return HttpResponseRedirect(
reverse_lazy('submit:submission_overview', kwargs={'event_slug': kwargs['event_slug']}))
owner = get_object_or_404(AKOwner, pk=request.POST["owner_id"])
return HttpResponseRedirect(
reverse_lazy('submit:akowner_edit', kwargs={'event_slug': kwargs['event_slug'], 'slug': owner.slug}))
class AKSlotAddView(EventSlugMixin, EventInactiveRedirectMixin, CreateView):
model = AKSlot model = AKSlot
form_class = AKDurationForm form_class = AKDurationForm
template_name = "AKSubmission/akslot_add_update.html" template_name = "AKSubmission/akslot_add_update.html"
def get_initial(self): def get_initial(self):
initials = super(AKSlotAddView, self).get_initial() initials = super().get_initial()
initials['event'] = self.event initials['event'] = self.event
initials['ak'] = get_object_or_404(AK, pk=self.kwargs['pk']) initials['ak'] = get_object_or_404(AK, pk=self.kwargs['pk'])
initials['duration'] = self.event.default_slot initials['duration'] = self.event.default_slot
...@@ -384,6 +612,12 @@ class AKSlotAddView(EventSlugMixin, EventInactiveRedirectMixin, CreateView): ...@@ -384,6 +612,12 @@ class AKSlotAddView(EventSlugMixin, EventInactiveRedirectMixin, CreateView):
class AKSlotEditView(EventSlugMixin, EventInactiveRedirectMixin, UpdateView): 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 model = AKSlot
form_class = AKDurationForm form_class = AKDurationForm
template_name = "AKSubmission/akslot_add_update.html" template_name = "AKSubmission/akslot_add_update.html"
...@@ -407,6 +641,12 @@ class AKSlotEditView(EventSlugMixin, EventInactiveRedirectMixin, UpdateView): ...@@ -407,6 +641,12 @@ class AKSlotEditView(EventSlugMixin, EventInactiveRedirectMixin, UpdateView):
class AKSlotDeleteView(EventSlugMixin, EventInactiveRedirectMixin, DeleteView): 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 model = AKSlot
template_name = "AKSubmission/akslot_delete.html" template_name = "AKSubmission/akslot_delete.html"
...@@ -430,6 +670,11 @@ class AKSlotDeleteView(EventSlugMixin, EventInactiveRedirectMixin, DeleteView): ...@@ -430,6 +670,11 @@ class AKSlotDeleteView(EventSlugMixin, EventInactiveRedirectMixin, DeleteView):
@status_manager.register(name="event_ak_messages") @status_manager.register(name="event_ak_messages")
class EventAKMessagesWidget(TemplateStatusWidget): 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" required_context_type = "event"
title = _("Messages") title = _("Messages")
template_name = "admin/AKModel/render_ak_messages.html" template_name = "admin/AKModel/render_ak_messages.html"
...@@ -448,12 +693,16 @@ class EventAKMessagesWidget(TemplateStatusWidget): ...@@ -448,12 +693,16 @@ class EventAKMessagesWidget(TemplateStatusWidget):
class AKAddOrgaMessageView(EventSlugMixin, CreateView): class AKAddOrgaMessageView(EventSlugMixin, CreateView):
"""
View: Form to create a (confidential) message to the organizers as defined in
:class:`AKSubmission.forms.AKOrgaMessageForm`
"""
model = AKOrgaMessage model = AKOrgaMessage
form_class = AKOrgaMessageForm form_class = AKOrgaMessageForm
template_name = "AKSubmission/akmessage_add.html" template_name = "AKSubmission/akmessage_add.html"
def get_initial(self): def get_initial(self):
initials = super(AKAddOrgaMessageView, self).get_initial() initials = super().get_initial()
initials['ak'] = get_object_or_404(AK, pk=self.kwargs['pk']) initials['ak'] = get_object_or_404(AK, pk=self.kwargs['pk'])
initials['event'] = initials['ak'].event initials['event'] = initials['ak'].event
return initials return initials
......
...@@ -61,7 +61,7 @@ Provide more context by answering these questions: ...@@ -61,7 +61,7 @@ Provide more context by answering these questions:
Include details about your configuration and environment: 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**? * **What's the OS you're using**?
### Suggesting Enhancements ### Suggesting Enhancements
......
...@@ -17,3 +17,6 @@ Further contributions in the form of code, testing, documentation etc. were mad ...@@ -17,3 +17,6 @@ Further contributions in the form of code, testing, documentation etc. were mad
* R. Zameitat [xayomer](https://gitlab.fachschaften.org/xayomer) * R. Zameitat [xayomer](https://gitlab.fachschaften.org/xayomer)
* N. Steinger [voidptr](https://gitlab.fachschaften.org/voidptr) * N. Steinger [voidptr](https://gitlab.fachschaften.org/voidptr)
* T. Neumann [neumantm](https://gitlab.fachschaften.org/neumantm) * T. Neumann [neumantm](https://gitlab.fachschaften.org/neumantm)
* F. Blanke [felix_bonn](https://gitlab.fachschaften.org/felix_bonn)
* L. Conti [lorax66](https://gitlab.fachschaften.org/lorax66)
* M. Marx [mmarx](https://gitlab.fachschaften.org/mmarx)
...@@ -10,7 +10,7 @@ setup. ...@@ -10,7 +10,7 @@ setup.
### System Requirements ### System Requirements
* Python 3.8+ incl. development tools * Python3.11+ incl. development tools
* Virtualenv * Virtualenv
* pdflatex & beamer * pdflatex & beamer
class (`texlive-latex-base texlive-latex-recommended texlive-latex-extra texlive-fonts-extra texlive-luatex`) class (`texlive-latex-base texlive-latex-recommended texlive-latex-extra texlive-fonts-extra texlive-luatex`)
...@@ -37,7 +37,7 @@ Python requirements are listed in ``requirements.txt``. They can be installed wi ...@@ -37,7 +37,7 @@ Python requirements are listed in ``requirements.txt``. They can be installed wi
### Manual Setup ### Manual Setup
1. setup a virtual environment using the proper python version ``virtualenv venv -p python3.7`` 1. setup a virtual environment using the proper python version ``virtualenv venv -p python3.11``
1. activate virtualenv ``source venv/bin/activate`` 1. activate virtualenv ``source venv/bin/activate``
1. install python requirements ``pip install -r requirements.txt`` 1. install python requirements ``pip install -r requirements.txt``
1. setup necessary database tables etc. ``python manage.py migrate`` 1. setup necessary database tables etc. ``python manage.py migrate``
...@@ -68,7 +68,7 @@ is not stored in any repository or similar, and disable DEBUG mode (``settings.p ...@@ -68,7 +68,7 @@ is not stored in any repository or similar, and disable DEBUG mode (``settings.p
1. create a folder, e.g. ``mkdir /srv/AKPlanning/`` 1. create a folder, e.g. ``mkdir /srv/AKPlanning/``
1. change to the new directory ``cd /srv/AKPlanning/`` 1. change to the new directory ``cd /srv/AKPlanning/``
1. clone this repository ``git clone URL .`` 1. clone this repository ``git clone URL .``
1. setup a virtual environment using the proper python version ``virtualenv venv -p python3.7`` 1. setup a virtual environment using the proper python version ``virtualenv venv -p python3.11``
1. activate virtualenv ``source venv/bin/activate`` 1. activate virtualenv ``source venv/bin/activate``
1. update tools ``pip install --upgrade setuptools pip wheel`` 1. update tools ``pip install --upgrade setuptools pip wheel``
1. install python requirements ``pip install -r requirements.txt`` 1. install python requirements ``pip install -r requirements.txt``
......
...@@ -8,14 +8,21 @@ if [ -z ${VIRTUAL_ENV+x} ]; then ...@@ -8,14 +8,21 @@ if [ -z ${VIRTUAL_ENV+x} ]; then
fi fi
# enable really all warnings, some of them are silenced by default # enable really all warnings, some of them are silenced by default
if [[ "$@" == *"--all"* ]]; then for arg in "$@"; do
export PYTHONWARNINGS=all if [[ "$arg" == "--all" ]]; then
fi export PYTHONWARNINGS=all
fi
done
# in case of checking production setup # in case of checking production setup
if [[ "$@" == *"--prod"* ]]; then for arg in "$@"; do
export DJANGO_SETTINGS_MODULE=AKPlanning.settings_production if [[ "$arg" == "--prod" ]]; then
./manage.py check --deploy export DJANGO_SETTINGS_MODULE=AKPlanning.settings_production
fi ./manage.py check --deploy
./manage.py makemigrations --dry-run --check
fi
done
# check the setup
./manage.py check ./manage.py check
./manage.py makemigrations --dry-run --check
...@@ -10,11 +10,19 @@ rm -rf venv/ ...@@ -10,11 +10,19 @@ rm -rf venv/
# Setup Python Environment # Setup Python Environment
# Requires: Virtualenv, appropriate Python installation # Requires: Virtualenv, appropriate Python installation
virtualenv venv -p python3.9 virtualenv venv -p python3.11
source venv/bin/activate source venv/bin/activate
pip install --upgrade setuptools pip wheel pip install --upgrade setuptools pip wheel
pip install -r requirements.txt 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 # Setup database
python manage.py migrate python manage.py migrate
...@@ -26,4 +34,12 @@ python manage.py compilemessages -l de_DE ...@@ -26,4 +34,12 @@ python manage.py compilemessages -l de_DE
# Credentials are entered interactively on CLI # Credentials are entered interactively on CLI
python manage.py createsuperuser python manage.py createsuperuser
# Generate documentation (but not for CI use)
if [ -n "$1" = "--ci" ]; then
cd docs
make html
cd ..
fi
deactivate 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
...@@ -27,4 +27,10 @@ pip install --upgrade -r requirements.txt ...@@ -27,4 +27,10 @@ pip install --upgrade -r requirements.txt
./manage.py collectstatic --noinput ./manage.py collectstatic --noinput
./manage.py compilemessages -l de_DE ./manage.py compilemessages -l de_DE
# Update documentation
cd docs
make html
cd ..
touch AKPlanning/wsgi.py touch AKPlanning/wsgi.py
_build/
code/
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?=
SPHINXBUILD ?= sphinx-build
SOURCEDIR = .
BUILDDIR = _build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
Code
=====
.. toctree::
code/AKDashboard
code/AKModel
code/AKOnline
code/AKPlan
code/AKScheduling
code/AKSubmission
# Configuration file for the Sphinx documentation builder.
#
# For the full list of built-in configuration values, see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
import os
import sys
from recommonmark.parser import CommonMarkParser
import django
# -- Project information -----------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
project = 'AK Planning'
copyright = '2025, N. Geisler, B. Hättasch & more'
author = 'N. Geisler, B. Hättasch & more'
# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
extensions = [
'sphinxcontrib.apidoc', # runs sphinx-apidoc automatically as part of sphinx-build
'sphinx.ext.autodoc', # the autodoc extensions uses files generated by apidoc
"sphinx.ext.autosummary",
"sphinxcontrib_django",
'sphinx.ext.viewcode', # enable viewing autodoc'd code
]
templates_path = ['_templates']
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
# -- Django specific settings ------------------------------------------------
# Add source directory to sys.path
sys.path.insert(0, os.path.abspath(".."))
# Configure the path to the Django settings module
django_settings = "AKPlanning.settings"
os.environ['DJANGO_SETTINGS_MODULE'] = django_settings
django.setup()
# Include the database table names of Django models
django_show_db_tables = True # Boolean, default: False
# Add abstract database tables names (only takes effect if django_show_db_tables is True)
django_show_db_tables_abstract = True # Boolean, default: False
# Auto-generate API documentation.
os.environ['SPHINX_APIDOC_OPTIONS'] = "members,show-inheritance"
# -- Input ----
source_parsers = {
'.md': CommonMarkParser,
}
source_suffix = ['.rst', '.md']
# -- Extension Conf ----
autodoc_member_order = 'bysource'
autodoc_inherit_docstrings = False
apidoc_module_dir = '../'
apidoc_output_dir = 'code'
apidoc_excluded_paths = ['*/migrations',
'AKPlanning/',
'manage.py',
'docs',
'locale',
'Utils',
'*/urls.py',
]
apidoc_separate_modules = True
apidoc_toc_file = False
apidoc_module_first = True
apidoc_extra_args = ['-f']
apidoc_project = project
# -- Options for HTML output -------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
html_static_path = ['_static']
html_theme = 'sphinx_rtd_theme'
.. AK Planning documentation master file, created by
sphinx-quickstart on Wed Jun 21 09:54:11 2023.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
Start
=======================================
.. toctree::
:maxdepth: 2
:caption: Contents:
usage/usage
code
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`
@ECHO OFF
pushd %~dp0
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set SOURCEDIR=.
set BUILDDIR=_build
%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.https://www.sphinx-doc.org/
exit /b 1
)
if "%1" == "" goto help
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
goto end
:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
:end
popd