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_csp-4.x
3 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
  • ak-import
  • feature/clear-schedule-button
  • feature/json-export-via-rest-framework
  • feature/json-schedule-import-tests
  • feature/preference-polling
  • feature/preference-polling-form
  • feature/preference-polling-form-rebased
  • feature/preference-polling-rebased
  • fix/add-room-import-only-once
  • main
  • merge-to-upstream
  • renovate/django-5.x
  • renovate/django-debug-toolbar-4.x
  • renovate/django-simple-history-3.x
  • renovate/mysqlclient-2.x
15 results
Show changes
Commits on Source (161)
Showing
with 2522 additions and 345 deletions
uwsgi==2.0.25.1 uwsgi==2.0.29
image: python:3.9 image: python:3.11
services: services:
- mysql - mysql
...@@ -26,10 +26,9 @@ cache: ...@@ -26,10 +26,9 @@ cache:
- pip install pylint-gitlab pylint-django - pip install pylint-gitlab pylint-django
- mysql --version - mysql --version
check: migrations:
extends: .before_script_template extends: .before_script_template
script: script:
- ./Utils/check.sh --all
- source venv/bin/activate - source venv/bin/activate
- ./manage.py makemigrations --dry-run --check - ./manage.py makemigrations --dry-run --check
...@@ -38,7 +37,7 @@ test: ...@@ -38,7 +37,7 @@ test:
script: script:
- source venv/bin/activate - source venv/bin/activate
- echo "GRANT ALL on *.* to '${MYSQL_USER}';"| mysql -u root --password="${MYSQL_ROOT_PASSWORD}" -h mysql - echo "GRANT ALL on *.* to '${MYSQL_USER}';"| mysql -u root --password="${MYSQL_ROOT_PASSWORD}" -h mysql
- pip install pytest-cov unittest-xml-reporting - pip install pytest-cov
- coverage run --source='.' manage.py test --settings AKPlanning.settings_ci - coverage run --source='.' manage.py test --settings AKPlanning.settings_ci
after_script: after_script:
- source venv/bin/activate - source venv/bin/activate
......
import zoneinfo import zoneinfo
from django.apps import apps from django.apps import apps
from django.test import TestCase, override_settings from django.test import override_settings, TestCase
from django.urls import reverse from django.urls import reverse
from django.utils.timezone import now from django.utils.timezone import now
from AKDashboard.models import DashboardButton from AKDashboard.models import DashboardButton
from AKModel.models import Event, AK, AKCategory from AKModel.models import AK, AKCategory, Event
from AKModel.tests import BasicViewTests from AKModel.tests.test_views import BasicViewTests
class DashboardTests(TestCase): class DashboardTests(TestCase):
""" """
Specific Dashboard Tests Specific Dashboard Tests
""" """
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
""" """
...@@ -20,17 +22,17 @@ class DashboardTests(TestCase): ...@@ -20,17 +22,17 @@ class DashboardTests(TestCase):
""" """
super().setUpTestData() super().setUpTestData()
cls.event = Event.objects.create( cls.event = Event.objects.create(
name="Dashboard Test Event", name="Dashboard Test Event",
slug="dashboardtest", slug="dashboardtest",
timezone=zoneinfo.ZoneInfo("Europe/Berlin"), timezone=zoneinfo.ZoneInfo("Europe/Berlin"),
start=now(), start=now(),
end=now(), end=now(),
active=True, active=True,
plan_hidden=False, plan_hidden=False,
) )
cls.default_category = AKCategory.objects.create( cls.default_category = AKCategory.objects.create(
name="Test Category", name="Test Category",
event=cls.event, event=cls.event,
) )
def test_dashboard_view(self): def test_dashboard_view(self):
...@@ -62,12 +64,12 @@ class DashboardTests(TestCase): ...@@ -62,12 +64,12 @@ class DashboardTests(TestCase):
# History should be empty # History should be empty
response = self.client.get(url) response = self.client.get(url)
self.assertQuerysetEqual(response.context["recent_changes"], []) self.assertQuerySetEqual(response.context["recent_changes"], [])
AK.objects.create( AK.objects.create(
name="Test AK", name="Test AK",
category=self.default_category, category=self.default_category,
event=self.event, event=self.event,
) )
# History should now contain one AK (Test AK) # History should now contain one AK (Test AK)
...@@ -154,8 +156,8 @@ class DashboardTests(TestCase): ...@@ -154,8 +156,8 @@ class DashboardTests(TestCase):
self.assertNotContains(response, "Dashboard Button Test") self.assertNotContains(response, "Dashboard Button Test")
DashboardButton.objects.create( DashboardButton.objects.create(
text="Dashboard Button Test", text="Dashboard Button Test",
event=self.event event=self.event
) )
response = self.client.get(url_event_dashboard) response = self.client.get(url_event_dashboard)
......
...@@ -2,6 +2,8 @@ from django import forms ...@@ -2,6 +2,8 @@ from django import forms
from django.apps import apps from django.apps import apps
from django.contrib import admin, messages from django.contrib import admin, messages
from django.contrib.admin import SimpleListFilter, RelatedFieldListFilter, action, display from django.contrib.admin import SimpleListFilter, RelatedFieldListFilter, action, display
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.contrib.auth.models import User # pylint: disable=E5142
from django.db.models import Count, F from django.db.models import Count, F
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect
from django.shortcuts import render, redirect from django.shortcuts import render, redirect
...@@ -15,7 +17,7 @@ from simple_history.admin import SimpleHistoryAdmin ...@@ -15,7 +17,7 @@ from simple_history.admin import SimpleHistoryAdmin
from AKModel.availability.models import Availability from AKModel.availability.models import Availability
from AKModel.forms import RoomFormWithAvailabilities from AKModel.forms import RoomFormWithAvailabilities
from AKModel.models import Event, AKOwner, AKCategory, AKTrack, AKRequirement, AK, AKSlot, Room, AKOrgaMessage, \ from AKModel.models import Event, AKOwner, AKCategory, AKTrack, AKRequirement, AK, AKSlot, Room, AKOrgaMessage, \
ConstraintViolation, DefaultSlot ConstraintViolation, DefaultSlot, AKType
from AKModel.urls import get_admin_urls_event_wizard, get_admin_urls_event from AKModel.urls import get_admin_urls_event_wizard, get_admin_urls_event
from AKModel.views.ak import AKResetInterestView, AKResetInterestCounterView from AKModel.views.ak import AKResetInterestView, AKResetInterestCounterView
from AKModel.views.manage import CVMarkResolvedView, CVSetLevelViolationView, CVSetLevelWarningView from AKModel.views.manage import CVMarkResolvedView, CVSetLevelViolationView, CVSetLevelWarningView
...@@ -215,6 +217,18 @@ class AKRequirementAdmin(PrepopulateWithNextActiveEventMixin, admin.ModelAdmin): ...@@ -215,6 +217,18 @@ class AKRequirementAdmin(PrepopulateWithNextActiveEventMixin, admin.ModelAdmin):
ordering = ['name'] ordering = ['name']
@admin.register(AKType)
class AKTypeAdmin(PrepopulateWithNextActiveEventMixin, admin.ModelAdmin):
"""
Admin interface for AKRequirements
"""
model = AKType
list_display = ['name', 'event']
list_filter = ['event']
list_editable = []
ordering = ['name']
class WishFilter(SimpleListFilter): class WishFilter(SimpleListFilter):
""" """
Re-usable filter for wishes Re-usable filter for wishes
...@@ -257,6 +271,7 @@ class AKAdminForm(forms.ModelForm): ...@@ -257,6 +271,7 @@ class AKAdminForm(forms.ModelForm):
self.fields["requirements"].queryset = AKRequirement.objects.filter(event=self.instance.event) self.fields["requirements"].queryset = AKRequirement.objects.filter(event=self.instance.event)
self.fields["conflicts"].queryset = AK.objects.filter(event=self.instance.event) self.fields["conflicts"].queryset = AK.objects.filter(event=self.instance.event)
self.fields["prerequisites"].queryset = AK.objects.filter(event=self.instance.event) self.fields["prerequisites"].queryset = AK.objects.filter(event=self.instance.event)
self.fields["types"].queryset = AKType.objects.filter(event=self.instance.event)
@admin.register(AK) @admin.register(AK)
...@@ -555,3 +570,41 @@ class DefaultSlotAdmin(EventTimezoneFormMixin, admin.ModelAdmin): ...@@ -555,3 +570,41 @@ class DefaultSlotAdmin(EventTimezoneFormMixin, admin.ModelAdmin):
list_display = ['start_simplified', 'end_simplified', 'event'] list_display = ['start_simplified', 'end_simplified', 'event']
list_filter = ['event'] list_filter = ['event']
form = DefaultSlotAdminForm form = DefaultSlotAdminForm
# Define a new User admin
class UserAdmin(BaseUserAdmin):
"""
Admin interface for Users
Enhances the built-in UserAdmin with additional actions to activate and deactivate users and a custom selection
of displayed properties in overview list
"""
list_display = ["username", "email", "is_active", "is_staff", "is_superuser"]
actions = ['activate', 'deactivate']
@admin.action(description=_("Activate selected users"))
def activate(self, request, queryset):
"""
Bulk activate users
:param request: HTTP request
:param queryset: queryset containing all users that should be activated
"""
queryset.update(is_active=True)
self.message_user(request, _("The selected users have been activated."))
@admin.action(description=_("Deactivate selected users"))
def deactivate(self, request, queryset):
"""
Bulk deactivate users
:param request: HTTP request
:param queryset: queryset containing all users that should be deactivated
"""
queryset.update(is_active=False)
self.message_user(request, _("The selected users have been deactivated."))
# Re-register UserAdmin
admin.site.unregister(User)
admin.site.register(User, UserAdmin)
...@@ -183,7 +183,7 @@ class AvailabilitiesFormMixin(forms.Form): ...@@ -183,7 +183,7 @@ class AvailabilitiesFormMixin(forms.Form):
for avail in availabilities: for avail in availabilities:
setattr(avail, reference_name, instance.id) setattr(avail, reference_name, instance.id)
def _replace_availabilities(self, instance, availabilities: [Availability]): def _replace_availabilities(self, instance, availabilities: list[Availability]):
""" """
Replace the existing list of availabilities belonging to an entity with a new, updated one Replace the existing list of availabilities belonging to an entity with a new, updated one
......
...@@ -151,9 +151,12 @@ class Availability(models.Model): ...@@ -151,9 +151,12 @@ class Availability(models.Model):
if not other.overlaps(self, strict=False): if not other.overlaps(self, strict=False):
raise Exception('Only overlapping Availabilities can be merged.') raise Exception('Only overlapping Availabilities can be merged.')
return Availability( avail = Availability(
start=min(self.start, other.start), end=max(self.end, other.end) start=min(self.start, other.start), end=max(self.end, other.end)
) )
if self.event == other.event:
avail.event = self.event
return avail
def __or__(self, other: 'Availability') -> 'Availability': def __or__(self, other: 'Availability') -> 'Availability':
"""Performs the merge operation: ``availability1 | availability2``""" """Performs the merge operation: ``availability1 | availability2``"""
...@@ -168,9 +171,12 @@ class Availability(models.Model): ...@@ -168,9 +171,12 @@ class Availability(models.Model):
if not other.overlaps(self, False): if not other.overlaps(self, False):
raise Exception('Only overlapping Availabilities can be intersected.') raise Exception('Only overlapping Availabilities can be intersected.')
return Availability( avail = Availability(
start=max(self.start, other.start), end=min(self.end, other.end) start=max(self.start, other.start), end=min(self.end, other.end)
) )
if self.event == other.event:
avail.event = self.event
return avail
def __and__(self, other: 'Availability') -> 'Availability': def __and__(self, other: 'Availability') -> 'Availability':
"""Performs the intersect operation: ``availability1 & """Performs the intersect operation: ``availability1 &
...@@ -247,7 +253,14 @@ class Availability(models.Model): ...@@ -247,7 +253,14 @@ class Availability(models.Model):
f'{self.end.astimezone(self.event.timezone).strftime("%a %H:%M")}') f'{self.end.astimezone(self.event.timezone).strftime("%a %H:%M")}')
@classmethod @classmethod
def with_event_length(cls, event, person=None, room=None, ak=None, ak_category=None): def with_event_length(
cls,
event: Event,
person: AKOwner | None = None,
room: Room | None = None,
ak: AK | None = None,
ak_category: AKCategory | None = None,
) -> "Availability":
""" """
Create an availability covering exactly the time between event start and event end. Create an availability covering exactly the time between event start and event end.
Can e.g., be used to create default availabilities. Can e.g., be used to create default availabilities.
...@@ -267,6 +280,30 @@ class Availability(models.Model): ...@@ -267,6 +280,30 @@ class Availability(models.Model):
return Availability(start=timeframe_start, end=timeframe_end, event=event, person=person, return Availability(start=timeframe_start, end=timeframe_end, event=event, person=person,
room=room, ak=ak, ak_category=ak_category) room=room, ak=ak, ak_category=ak_category)
def is_covered(self, availabilities: List['Availability']):
"""Check if list of availibilities cover this object.
:param availabilities: availabilities to check.
:return: whether the availabilities cover full event.
:rtype: bool
"""
avail_union = Availability.union(availabilities)
return any(avail.contains(self) for avail in avail_union)
@classmethod
def is_event_covered(cls, event: Event, availabilities: List['Availability']) -> bool:
"""Check if list of availibilities cover whole event.
:param event: event to check.
:param availabilities: availabilities to check.
:return: whether the availabilities cover full event.
:rtype: bool
"""
# NOTE: Cannot use `Availability.with_event_length` as its end is the
# event end + 1 day
full_event = Availability(event=event, start=event.start, end=event.end)
return full_event.is_covered(availabilities)
class Meta: class Meta:
verbose_name = _('Availability') verbose_name = _('Availability')
verbose_name_plural = _('Availabilities') verbose_name_plural = _('Availabilities')
......
...@@ -93,7 +93,7 @@ ...@@ -93,7 +93,7 @@
"model": "AKModel.akcategory", "model": "AKModel.akcategory",
"pk": 1, "pk": 1,
"fields": { "fields": {
"name": "Spa", "name": "Spaß",
"color": "275246", "color": "275246",
"description": "", "description": "",
"present_by_default": true, "present_by_default": true,
...@@ -115,7 +115,7 @@ ...@@ -115,7 +115,7 @@
"model": "AKModel.akcategory", "model": "AKModel.akcategory",
"pk": 3, "pk": 3,
"fields": { "fields": {
"name": "Spa/Kultur", "name": "Spaß/Kultur",
"color": "333333", "color": "333333",
"description": "", "description": "",
"present_by_default": true, "present_by_default": true,
...@@ -193,6 +193,14 @@ ...@@ -193,6 +193,14 @@
"event": 2 "event": 2
} }
}, },
{
"model": "AKModel.aktype",
"pk": 1,
"fields": {
"name": "Input",
"event": 2
}
},
{ {
"model": "AKModel.historicalak", "model": "AKModel.historicalak",
"pk": 1, "pk": 1,
...@@ -206,7 +214,6 @@ ...@@ -206,7 +214,6 @@
"reso": false, "reso": false,
"present": true, "present": true,
"notes": "", "notes": "",
"interest": -1,
"category": 4, "category": 4,
"track": null, "track": null,
"event": 2, "event": 2,
...@@ -229,7 +236,6 @@ ...@@ -229,7 +236,6 @@
"reso": false, "reso": false,
"present": true, "present": true,
"notes": "", "notes": "",
"interest": -1,
"category": 4, "category": 4,
"track": null, "track": null,
"event": 2, "event": 2,
...@@ -252,7 +258,6 @@ ...@@ -252,7 +258,6 @@
"reso": false, "reso": false,
"present": null, "present": null,
"notes": "", "notes": "",
"interest": -1,
"category": 5, "category": 5,
"track": null, "track": null,
"event": 2, "event": 2,
...@@ -275,7 +280,6 @@ ...@@ -275,7 +280,6 @@
"reso": false, "reso": false,
"present": null, "present": null,
"notes": "", "notes": "",
"interest": -1,
"category": 5, "category": 5,
"track": null, "track": null,
"event": 2, "event": 2,
...@@ -298,7 +302,6 @@ ...@@ -298,7 +302,6 @@
"reso": false, "reso": false,
"present": null, "present": null,
"notes": "We need to find a volunteer first...", "notes": "We need to find a volunteer first...",
"interest": -1,
"category": 3, "category": 3,
"track": null, "track": null,
"event": 2, "event": 2,
...@@ -321,7 +324,6 @@ ...@@ -321,7 +324,6 @@
"reso": false, "reso": false,
"present": null, "present": null,
"notes": "We need to find a volunteer first...", "notes": "We need to find a volunteer first...",
"interest": -1,
"category": 3, "category": 3,
"track": null, "track": null,
"event": 2, "event": 2,
...@@ -344,7 +346,6 @@ ...@@ -344,7 +346,6 @@
"reso": false, "reso": false,
"present": null, "present": null,
"notes": "", "notes": "",
"interest": -1,
"category": 5, "category": 5,
"track": 1, "track": 1,
"event": 2, "event": 2,
...@@ -436,6 +437,62 @@ ...@@ -436,6 +437,62 @@
] ]
} }
}, },
{
"model": "AKModel.ak",
"pk": 4,
"fields": {
"name": "Test AK fixed slots",
"short_name": "testfixed",
"description": "",
"link": "",
"protocol_link": "",
"category": 4,
"track": null,
"reso": false,
"present": true,
"notes": "",
"interest": -1,
"interest_counter": 0,
"include_in_export": false,
"event": 2,
"owners": [
1
],
"requirements": [
3
],
"conflicts": [],
"prerequisites": []
}
},
{
"model": "AKModel.ak",
"pk": 5,
"fields": {
"name": "Test AK Ernst",
"short_name": "testernst",
"description": "",
"link": "",
"protocol_link": "",
"category": 2,
"track": null,
"reso": false,
"present": true,
"notes": "",
"interest": -1,
"interest_counter": 0,
"include_in_export": false,
"event": 1,
"owners": [
3
],
"requirements": [
2
],
"conflicts": [],
"prerequisites": []
}
},
{ {
"model": "AKModel.room", "model": "AKModel.room",
"pk": 1, "pk": 1,
...@@ -460,6 +517,19 @@ ...@@ -460,6 +517,19 @@
"properties": [] "properties": []
} }
}, },
{
"model": "AKModel.room",
"pk": 3,
"fields": {
"name": "BBB Session 1",
"location": "",
"capacity": -1,
"event": 1,
"properties": [
2
]
}
},
{ {
"model": "AKModel.akslot", "model": "AKModel.akslot",
"pk": 1, "pk": 1,
...@@ -525,6 +595,58 @@ ...@@ -525,6 +595,58 @@
"updated": "2022-12-02T12:23:11.856Z" "updated": "2022-12-02T12:23:11.856Z"
} }
}, },
{
"model": "AKModel.akslot",
"pk": 6,
"fields": {
"ak": 4,
"room": null,
"start": "2020-11-08T18:30:00Z",
"duration": "2.00",
"fixed": true,
"event": 2,
"updated": "2022-12-02T12:23:11.856Z"
}
},
{
"model": "AKModel.akslot",
"pk": 7,
"fields": {
"ak": 4,
"room": 2,
"start": null,
"duration": "2.00",
"fixed": true,
"event": 2,
"updated": "2022-12-02T12:23:11.856Z"
}
},
{
"model": "AKModel.akslot",
"pk": 8,
"fields": {
"ak": 4,
"room": 2,
"start": "2020-11-07T16:00:00Z",
"duration": "2.00",
"fixed": true,
"event": 2,
"updated": "2022-12-02T12:23:11.856Z"
}
},
{
"model": "AKModel.akslot",
"pk": 9,
"fields": {
"ak": 5,
"room": null,
"start": null,
"duration": "2.00",
"fixed": false,
"event": 1,
"updated": "2022-12-02T12:23:11.856Z"
}
},
{ {
"model": "AKModel.constraintviolation", "model": "AKModel.constraintviolation",
"pk": 1, "pk": 1,
...@@ -668,5 +790,71 @@ ...@@ -668,5 +790,71 @@
"start": "2020-11-07T18:30:00Z", "start": "2020-11-07T18:30:00Z",
"end": "2020-11-07T21:30:00Z" "end": "2020-11-07T21:30:00Z"
} }
},
{
"model": "AKModel.availability",
"pk": 7,
"fields": {
"event": 1,
"person": null,
"room": null,
"ak": 5,
"ak_category": null,
"start": "2020-10-01T17:41:22Z",
"end": "2020-10-04T17:41:30Z"
}
},
{
"model": "AKModel.availability",
"pk": 8,
"fields": {
"event": 1,
"person": null,
"room": 3,
"ak": null,
"ak_category": null,
"start": "2020-10-01T17:41:22Z",
"end": "2020-10-04T17:41:30Z"
}
},
{
"model": "AKModel.defaultslot",
"pk": 1,
"fields": {
"event": 2,
"start": "2020-11-07T08:00:00Z",
"end": "2020-11-07T12:00:00Z",
"primary_categories": [5]
}
},
{
"model": "AKModel.defaultslot",
"pk": 2,
"fields": {
"event": 2,
"start": "2020-11-07T14:00:00Z",
"end": "2020-11-07T17:00:00Z",
"primary_categories": [4]
}
},
{
"model": "AKModel.defaultslot",
"pk": 3,
"fields": {
"event": 2,
"start": "2020-11-08T08:00:00Z",
"end": "2020-11-08T19:00:00Z",
"primary_categories": [4, 5]
}
},
{
"model": "AKModel.defaultslot",
"pk": 4,
"fields": {
"event": 2,
"start": "2020-11-09T17:00:00Z",
"end": "2020-11-10T01:00:00Z",
"primary_categories": [4, 5, 3]
}
} }
] ]
...@@ -4,13 +4,17 @@ Central and admin forms ...@@ -4,13 +4,17 @@ Central and admin forms
import csv import csv
import io import io
import json
from django import forms from django import forms
from django.core.exceptions import ValidationError
from django.forms.utils import ErrorList from django.forms.utils import ErrorList
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from jsonschema.exceptions import best_match
from AKModel.availability.forms import AvailabilitiesFormMixin from AKModel.availability.forms import AvailabilitiesFormMixin
from AKModel.models import Event, AKCategory, AKRequirement, Room from AKModel.models import Event, AKCategory, AKRequirement, Room, AKType
from AKModel.utils import construct_schema_validator
class DateTimeInput(forms.DateInput): class DateTimeInput(forms.DateInput):
...@@ -101,6 +105,13 @@ class NewEventWizardImportForm(forms.Form): ...@@ -101,6 +105,13 @@ class NewEventWizardImportForm(forms.Form):
required=False, required=False,
) )
import_types = forms.ModelMultipleChoiceField(
queryset=AKType.objects.all(),
widget=forms.CheckboxSelectMultiple,
label=_("Copy types"),
required=False,
)
# pylint: disable=too-many-arguments # pylint: disable=too-many-arguments
def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, initial=None, error_class=ErrorList, def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, initial=None, error_class=ErrorList,
label_suffix=None, empty_permitted=False, field_order=None, use_required_attribute=None, label_suffix=None, empty_permitted=False, field_order=None, use_required_attribute=None,
...@@ -111,6 +122,8 @@ class NewEventWizardImportForm(forms.Form): ...@@ -111,6 +122,8 @@ class NewEventWizardImportForm(forms.Form):
event=self.initial["import_event"]) event=self.initial["import_event"])
self.fields["import_requirements"].queryset = self.fields["import_requirements"].queryset.filter( self.fields["import_requirements"].queryset = self.fields["import_requirements"].queryset.filter(
event=self.initial["import_event"]) event=self.initial["import_event"])
self.fields["import_types"].queryset = self.fields["import_types"].queryset.filter(
event=self.initial["import_event"])
# pylint: disable=import-outside-toplevel # pylint: disable=import-outside-toplevel
# Local imports used to prevent cyclic imports and to only import when AKDashboard is available # Local imports used to prevent cyclic imports and to only import when AKDashboard is available
...@@ -272,3 +285,88 @@ class RoomFormWithAvailabilities(AvailabilitiesFormMixin, RoomForm): ...@@ -272,3 +285,88 @@ class RoomFormWithAvailabilities(AvailabilitiesFormMixin, RoomForm):
# Filter possible values for m2m when event is specified # Filter possible values for m2m when event is specified
if hasattr(self.instance, "event") and self.instance.event is not None: if hasattr(self.instance, "event") and self.instance.event is not None:
self.fields["properties"].queryset = AKRequirement.objects.filter(event=self.instance.event) self.fields["properties"].queryset = AKRequirement.objects.filter(event=self.instance.event)
class JSONScheduleImportForm(AdminIntermediateForm):
"""Form to import an AK schedule from a json file."""
json_data = forms.CharField(
required=False,
widget=forms.Textarea,
label=_("JSON data"),
help_text=_("JSON data from the scheduling solver"),
)
json_file = forms.FileField(
required=False,
label=_("File with JSON data"),
help_text=_("File with JSON data from the scheduling solver"),
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.json_schema_validator = construct_schema_validator(
schema="solver-output.schema.json"
)
def _check_json_data(self, data: str):
"""Validate `data` against our JSON schema.
:param data: The JSON string to validate using `self.json_schema_validator`.
:type data: str
:raises ValidationError: if the validation fails, with a description of the cause.
:return: The parsed JSON dict, if validation is successful.
"""
try:
schedule = json.loads(data)
except json.JSONDecodeError as ex:
raise ValidationError(_("Cannot decode as JSON"), "invalid") from ex
error = best_match(self.json_schema_validator.iter_errors(schedule))
if error:
raise ValidationError(
_("Invalid JSON format: %(msg)s at %(error_path)s"),
"invalid",
params={
"msg": error.message,
"error_path": error.json_path
}
) from error
return schedule
def clean(self):
"""Extract and validate entered JSON data.
We allow entering of the schedule from two sources:
1. from an uploaded file
2. from a text field.
This function checks that data is entered from exactly one source.
If so, the entered JSON string is validated against our schema.
Any errors are reported at the corresponding form field.
"""
cleaned_data = super().clean()
if cleaned_data.get("json_file") and cleaned_data.get("json_data"):
err = ValidationError(
_("Please enter data as a file OR via text, not both."), "invalid"
)
self.add_error("json_data", err)
self.add_error("json_file", err)
elif not (cleaned_data.get("json_file") or cleaned_data.get("json_data")):
err = ValidationError(
_("No data entered. Please enter data as a file or via text."), "invalid"
)
self.add_error("json_data", err)
self.add_error("json_file", err)
else:
source_field = "json_data"
data = cleaned_data.get(source_field)
if not data:
source_field = "json_file"
with cleaned_data.get(source_field).open() as ff:
data = ff.read()
try:
cleaned_data["data"] = self._check_json_data(data)
except ValidationError as ex:
self.add_error(source_field, ex)
return cleaned_data
# Generated by Django 4.2.13 on 2025-02-25 20:58
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('AKModel', '0060_orga_message_resolved'),
]
operations = [
migrations.CreateModel(
name='AKType',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(help_text='Name describing the type', max_length=128, verbose_name='Name')),
('event', models.ForeignKey(help_text='Associated event', on_delete=django.db.models.deletion.CASCADE, to='AKModel.event', verbose_name='Event')),
],
options={
'verbose_name': 'AK Type',
'verbose_name_plural': 'AK Types',
'ordering': ['name'],
'unique_together': {('event', 'name')},
},
),
migrations.AddField(
model_name='ak',
name='types',
field=models.ManyToManyField(blank=True, help_text='This AK is', to='AKModel.aktype', verbose_name='Types'),
),
]
# Generated by Django 4.2.13 on 2025-02-26 22:35
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('AKModel', '0061_types'),
]
operations = [
migrations.RemoveField(
model_name='historicalak',
name='interest',
),
]
# Generated by Django 4.2.13 on 2025-03-03 19:59
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('AKModel', '0062_interest_no_history'),
]
operations = [
migrations.AlterField(
model_name='ak',
name='name',
field=models.CharField(help_text='Name of the AK', max_length=256, validators=[django.core.validators.RegexValidator(inverse_match=True, message='May not contain quotation marks', regex='[\'\\"´`]+'), django.core.validators.RegexValidator(message='Must contain at least one letter or digit', regex='[\\w\\s]+')], verbose_name='Name'),
),
migrations.AlterField(
model_name='ak',
name='short_name',
field=models.CharField(blank=True, help_text='Name displayed in the schedule', max_length=64, validators=[django.core.validators.RegexValidator(inverse_match=True, message='May not contain quotation marks', regex='[\'\\"´`]+')], verbose_name='Short Name'),
),
migrations.AlterField(
model_name='akowner',
name='name',
field=models.CharField(help_text='Name to identify an AK owner by', max_length=64, validators=[django.core.validators.RegexValidator(inverse_match=True, message='May not contain quotation marks', regex='[\'\\"´`]+'), django.core.validators.RegexValidator(message='Must contain at least one letter or digit', regex='[\\w\\s]+')], verbose_name='Nickname'),
),
migrations.AlterField(
model_name='historicalak',
name='name',
field=models.CharField(help_text='Name of the AK', max_length=256, validators=[django.core.validators.RegexValidator(inverse_match=True, message='May not contain quotation marks', regex='[\'\\"´`]+'), django.core.validators.RegexValidator(message='Must contain at least one letter or digit', regex='[\\w\\s]+')], verbose_name='Name'),
),
migrations.AlterField(
model_name='historicalak',
name='short_name',
field=models.CharField(blank=True, help_text='Name displayed in the schedule', max_length=64, validators=[django.core.validators.RegexValidator(inverse_match=True, message='May not contain quotation marks', regex='[\'\\"´`]+')], verbose_name='Short Name'),
),
]
# Generated by Django 5.1.6 on 2025-03-29 22:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("AKModel", "0063_field_validators"),
]
operations = [
migrations.AddField(
model_name="event",
name="export_slot",
field=models.DecimalField(
decimal_places=2,
default=1,
help_text="Slot duration in hours that is used in the timeslot discretization, when this event is exported for the solver.",
max_digits=4,
verbose_name="Export Slot Length",
),
),
]
This diff is collapsed.
...@@ -4,8 +4,8 @@ ...@@ -4,8 +4,8 @@
{% block content %} {% block content %}
<pre> <pre>
title;duration;who;requirements;prerequisites;conflicts;availabilities;category;track;reso;notes; title;duration;who;requirements;prerequisites;conflicts;availabilities;category;types;track;reso;notes;
{% for slot in slots %}{{ slot.ak.short_name }};{{ slot.duration }};{{ slot.ak.owners.all|join:", " }};{{ slot.ak.requirements.all|join:", " }};{{ slot.ak.prerequisites.all|join:", " }};{{ slot.ak.conflicts.all|join:", " }};{% for a in slot.ak.availabilities.all %}{{ a.start | timezone:event.timezone | date:"l H:i" }} - {{ a.end | timezone:event.timezone | date:"l H:i" }}, {% endfor %};{{ slot.ak.category }};{{ slot.ak.track }};{{ slot.ak.reso }};{{ slot.ak.notes }}; {% for slot in slots %}{{ slot.ak.short_name }};{{ slot.duration }};{{ slot.ak.owners.all|join:", " }};{{ slot.ak.requirements.all|join:", " }};{{ slot.ak.prerequisites.all|join:", " }};{{ slot.ak.conflicts.all|join:", " }};{% for a in slot.ak.availabilities.all %}{{ a.start | timezone:event.timezone | date:"l H:i" }} - {{ a.end | timezone:event.timezone | date:"l H:i" }}, {% endfor %};{{ slot.ak.category }};{{ slot.ak.types.all|join:", " }};{{ slot.ak.track }};{{ slot.ak.reso }};{{ slot.ak.notes }};
{% endfor %} {% endfor %}
</pre> </pre>
{% endblock %} {% endblock %}
{% extends "admin/base_site.html" %}
{% load tz %}
{% block content %}
<div class="mb-3">
<label for="export_single_line" class="form-label">Exported JSON:</label>
<input readonly type="text" name="export_single_line" class="form-control form-control-sm" value="{{ json_data_oneline }}" style="font-family:var(--font-family-monospace);">
</div>
<div class="mb-3">
<label class="form-label">Exported JSON (indented for better readability):</label>
<pre class="prettyprint border rounded p-2">{{ json_data }}</pre>
</div>
<script>
$(document).ready(function() {
// JSON highlighting.
prettyPrint();
}
)
</script>
{% endblock %}
{% extends "admin/base_site.html" %}
{% load tags_AKModel %}
{% load i18n %}
{% load django_bootstrap5 %}
{% load fontawesome_6 %}
{% block title %}{{event}}: {{ title }}{% endblock %}
{% block content %}
{% block action_preview %}
<p>
{{ preview|linebreaksbr }}
</p>
{% endblock %}
<form enctype="multipart/form-data" method="post">{% csrf_token %}
{% bootstrap_form form %}
<div class="float-end">
<button type="submit" class="save btn btn-success" value="Submit">
{% fa6_icon "check" 'fas' %} {% trans "Confirm" %}
</button>
</div>
<a href="javascript:history.back()" class="btn btn-info">
{% fa6_icon "times" 'fas' %} {% trans "Cancel" %}
</a>
</form>
{% endblock %}
This diff is collapsed.
...@@ -7,8 +7,19 @@ from django.contrib.messages.storage.base import Message ...@@ -7,8 +7,19 @@ from django.contrib.messages.storage.base import Message
from django.test import TestCase from django.test import TestCase
from django.urls import reverse_lazy, reverse from django.urls import reverse_lazy, reverse
from AKModel.models import Event, AKOwner, AKCategory, AKTrack, AKRequirement, AK, Room, AKSlot, AKOrgaMessage, \ from AKModel.models import (
ConstraintViolation, DefaultSlot Event,
AKOwner,
AKCategory,
AKTrack,
AKRequirement,
AK,
Room,
AKSlot,
AKOrgaMessage,
ConstraintViolation,
DefaultSlot,
)
class BasicViewTests: class BasicViewTests:
...@@ -29,9 +40,10 @@ class BasicViewTests: ...@@ -29,9 +40,10 @@ class BasicViewTests:
since the test framework does not understand the concept of abstract test definitions and would handle this class since the test framework does not understand the concept of abstract test definitions and would handle this class
as real test case otherwise, distorting the test results. as real test case otherwise, distorting the test results.
""" """
# pylint: disable=no-member # pylint: disable=no-member
VIEWS = [] VIEWS = []
APP_NAME = '' APP_NAME = ""
VIEWS_STAFF_ONLY = [] VIEWS_STAFF_ONLY = []
EDIT_TESTCASES = [] EDIT_TESTCASES = []
...@@ -41,16 +53,26 @@ class BasicViewTests: ...@@ -41,16 +53,26 @@ class BasicViewTests:
""" """
user_model = get_user_model() user_model = get_user_model()
self.staff_user = user_model.objects.create( self.staff_user = user_model.objects.create(
username='Test Staff User', email='teststaff@example.com', password='staffpw', username="Test Staff User",
is_staff=True, is_active=True email="teststaff@example.com",
password="staffpw",
is_staff=True,
is_active=True,
) )
self.admin_user = user_model.objects.create( self.admin_user = user_model.objects.create(
username='Test Admin User', email='testadmin@example.com', password='adminpw', username="Test Admin User",
is_staff=True, is_superuser=True, is_active=True email="testadmin@example.com",
password="adminpw",
is_staff=True,
is_superuser=True,
is_active=True,
) )
self.deactivated_user = user_model.objects.create( self.deactivated_user = user_model.objects.create(
username='Test Deactivated User', email='testdeactivated@example.com', password='deactivatedpw', username="Test Deactivated User",
is_staff=True, is_active=False email="testdeactivated@example.com",
password="deactivatedpw",
is_staff=True,
is_active=False,
) )
def _name_and_url(self, view_name): def _name_and_url(self, view_name):
...@@ -62,7 +84,9 @@ class BasicViewTests: ...@@ -62,7 +84,9 @@ class BasicViewTests:
:return: full view name with prefix if applicable, url of the view :return: full view name with prefix if applicable, url of the view
:rtype: str, str :rtype: str, str
""" """
view_name_with_prefix = f"{self.APP_NAME}:{view_name[0]}" if self.APP_NAME != "" else view_name[0] view_name_with_prefix = (
f"{self.APP_NAME}:{view_name[0]}" if self.APP_NAME != "" else view_name[0]
)
url = reverse(view_name_with_prefix, kwargs=view_name[1]) url = reverse(view_name_with_prefix, kwargs=view_name[1])
return view_name_with_prefix, url return view_name_with_prefix, url
...@@ -74,7 +98,7 @@ class BasicViewTests: ...@@ -74,7 +98,7 @@ class BasicViewTests:
:param expected_message: message that should be shown :param expected_message: message that should be shown
:param msg_prefix: prefix for the error message when test fails :param msg_prefix: prefix for the error message when test fails
""" """
messages:List[Message] = list(get_messages(response.wsgi_request)) messages: List[Message] = list(get_messages(response.wsgi_request))
msg_count = "No message shown to user" msg_count = "No message shown to user"
msg_content = "Wrong message, expected '{expected_message}'" msg_content = "Wrong message, expected '{expected_message}'"
...@@ -95,10 +119,16 @@ class BasicViewTests: ...@@ -95,10 +119,16 @@ class BasicViewTests:
view_name_with_prefix, url = self._name_and_url(view_name) view_name_with_prefix, url = self._name_and_url(view_name)
try: try:
response = self.client.get(url) response = self.client.get(url)
self.assertEqual(response.status_code, 200, msg=f"{view_name_with_prefix} ({url}) broken") self.assertEqual(
except Exception: # pylint: disable=broad-exception-caught response.status_code,
self.fail(f"An error occurred during rendering of {view_name_with_prefix} ({url}):" 200,
f"\n\n{traceback.format_exc()}") msg=f"{view_name_with_prefix} ({url}) broken",
)
except Exception: # pylint: disable=broad-exception-caught
self.fail(
f"An error occurred during rendering of {view_name_with_prefix} ({url}):"
f"\n\n{traceback.format_exc()}"
)
def test_access_control_staff_only(self): def test_access_control_staff_only(self):
""" """
...@@ -107,11 +137,16 @@ class BasicViewTests: ...@@ -107,11 +137,16 @@ class BasicViewTests:
# Not logged in? Views should not be visible # Not logged in? Views should not be visible
self.client.logout() self.client.logout()
for view_name_info in self.VIEWS_STAFF_ONLY: for view_name_info in self.VIEWS_STAFF_ONLY:
expected_response_code = 302 if len(view_name_info) == 2 else view_name_info[2] expected_response_code = (
302 if len(view_name_info) == 2 else view_name_info[2]
)
view_name_with_prefix, url = self._name_and_url(view_name_info) view_name_with_prefix, url = self._name_and_url(view_name_info)
response = self.client.get(url) response = self.client.get(url)
self.assertEqual(response.status_code, expected_response_code, self.assertEqual(
msg=f"{view_name_with_prefix} ({url}) accessible by non-staff") response.status_code,
expected_response_code,
msg=f"{view_name_with_prefix} ({url}) accessible by non-staff",
)
# Logged in? Views should be visible # Logged in? Views should be visible
self.client.force_login(self.staff_user) self.client.force_login(self.staff_user)
...@@ -119,20 +154,30 @@ class BasicViewTests: ...@@ -119,20 +154,30 @@ class BasicViewTests:
view_name_with_prefix, url = self._name_and_url(view_name_info) view_name_with_prefix, url = self._name_and_url(view_name_info)
try: try:
response = self.client.get(url) response = self.client.get(url)
self.assertEqual(response.status_code, 200, self.assertEqual(
msg=f"{view_name_with_prefix} ({url}) should be accessible for staff (but isn't)") response.status_code,
200,
msg=f"{view_name_with_prefix} ({url}) should be accessible for staff (but isn't)",
)
except Exception: # pylint: disable=broad-exception-caught except Exception: # pylint: disable=broad-exception-caught
self.fail(f"An error occurred during rendering of {view_name_with_prefix} ({url}):" self.fail(
f"\n\n{traceback.format_exc()}") f"An error occurred during rendering of {view_name_with_prefix} ({url}):"
f"\n\n{traceback.format_exc()}"
)
# Disabled user? Views should not be visible # Disabled user? Views should not be visible
self.client.force_login(self.deactivated_user) self.client.force_login(self.deactivated_user)
for view_name_info in self.VIEWS_STAFF_ONLY: for view_name_info in self.VIEWS_STAFF_ONLY:
expected_response_code = 302 if len(view_name_info) == 2 else view_name_info[2] expected_response_code = (
302 if len(view_name_info) == 2 else view_name_info[2]
)
view_name_with_prefix, url = self._name_and_url(view_name_info) view_name_with_prefix, url = self._name_and_url(view_name_info)
response = self.client.get(url) response = self.client.get(url)
self.assertEqual(response.status_code, expected_response_code, self.assertEqual(
msg=f"{view_name_with_prefix} ({url}) still accessible for deactivated user") response.status_code,
expected_response_code,
msg=f"{view_name_with_prefix} ({url}) still accessible for deactivated user",
)
def _to_sendable_value(self, val): def _to_sendable_value(self, val):
""" """
...@@ -182,16 +227,26 @@ class BasicViewTests: ...@@ -182,16 +227,26 @@ class BasicViewTests:
self.client.logout() self.client.logout()
response = self.client.get(url) response = self.client.get(url)
self.assertEqual(response.status_code, 200, msg=f"{name}: Could not load edit form via GET ({url})") self.assertEqual(
response.status_code,
200,
msg=f"{name}: Could not load edit form via GET ({url})",
)
form = response.context[form_name] form = response.context[form_name]
data = {k:self._to_sendable_value(v) for k,v in form.initial.items()} data = {k: self._to_sendable_value(v) for k, v in form.initial.items()}
response = self.client.post(url, data=data) response = self.client.post(url, data=data)
if expected_code == 200: if expected_code == 200:
self.assertEqual(response.status_code, 200, msg=f"{name}: Did not return 200 ({url}") self.assertEqual(
response.status_code, 200, msg=f"{name}: Did not return 200 ({url}"
)
elif expected_code == 302: elif expected_code == 302:
self.assertRedirects(response, target_url, msg_prefix=f"{name}: Did not redirect ({url} -> {target_url}") self.assertRedirects(
response,
target_url,
msg_prefix=f"{name}: Did not redirect ({url} -> {target_url}",
)
if expected_message != "": if expected_message != "":
self._assert_message(response, expected_message, msg_prefix=f"{name}") self._assert_message(response, expected_message, msg_prefix=f"{name}")
...@@ -200,30 +255,44 @@ class ModelViewTests(BasicViewTests, TestCase): ...@@ -200,30 +255,44 @@ class ModelViewTests(BasicViewTests, TestCase):
""" """
Basic view test cases for views from AKModel plus some custom tests Basic view test cases for views from AKModel plus some custom tests
""" """
fixtures = ['model.json']
fixtures = ["model.json"]
ADMIN_MODELS = [ ADMIN_MODELS = [
(Event, 'event'), (AKOwner, 'akowner'), (AKCategory, 'akcategory'), (AKTrack, 'aktrack'), (Event, "event"),
(AKRequirement, 'akrequirement'), (AK, 'ak'), (Room, 'room'), (AKSlot, 'akslot'), (AKOwner, "akowner"),
(AKOrgaMessage, 'akorgamessage'), (ConstraintViolation, 'constraintviolation'), (AKCategory, "akcategory"),
(DefaultSlot, 'defaultslot') (AKTrack, "aktrack"),
(AKRequirement, "akrequirement"),
(AK, "ak"),
(Room, "room"),
(AKSlot, "akslot"),
(AKOrgaMessage, "akorgamessage"),
(ConstraintViolation, "constraintviolation"),
(DefaultSlot, "defaultslot"),
] ]
VIEWS_STAFF_ONLY = [ VIEWS_STAFF_ONLY = [
('admin:index', {}), ("admin:index", {}),
('admin:event_status', {'event_slug': 'kif42'}), ("admin:event_status", {"event_slug": "kif42"}),
('admin:event_requirement_overview', {'event_slug': 'kif42'}), ("admin:event_requirement_overview", {"event_slug": "kif42"}),
('admin:ak_csv_export', {'event_slug': 'kif42'}), ("admin:ak_csv_export", {"event_slug": "kif42"}),
('admin:ak_wiki_export', {'slug': 'kif42'}), ("admin:ak_json_export", {"event_slug": "kif42"}),
('admin:ak_delete_orga_messages', {'event_slug': 'kif42'}), ("admin:ak_wiki_export", {"slug": "kif42"}),
('admin:ak_slide_export', {'event_slug': 'kif42'}), ("admin:ak_schedule_json_import", {"event_slug": "kif42"}),
('admin:default-slots-editor', {'event_slug': 'kif42'}), ("admin:ak_delete_orga_messages", {"event_slug": "kif42"}),
('admin:room-import', {'event_slug': 'kif42'}), ("admin:ak_slide_export", {"event_slug": "kif42"}),
('admin:new_event_wizard_start', {}), ("admin:default-slots-editor", {"event_slug": "kif42"}),
("admin:room-import", {"event_slug": "kif42"}),
("admin:new_event_wizard_start", {}),
] ]
EDIT_TESTCASES = [ EDIT_TESTCASES = [
{'view': 'admin:default-slots-editor', 'kwargs': {'event_slug': 'kif42'}, "admin": True}, {
"view": "admin:default-slots-editor",
"kwargs": {"event_slug": "kif42"},
"admin": True,
},
] ]
def test_admin(self): def test_admin(self):
...@@ -234,24 +303,32 @@ class ModelViewTests(BasicViewTests, TestCase): ...@@ -234,24 +303,32 @@ class ModelViewTests(BasicViewTests, TestCase):
for model in self.ADMIN_MODELS: for model in self.ADMIN_MODELS:
# Special treatment for a subset of views (where we exchanged default functionality, e.g., create views) # Special treatment for a subset of views (where we exchanged default functionality, e.g., create views)
if model[1] == "event": if model[1] == "event":
_, url = self._name_and_url(('admin:new_event_wizard_start', {})) _, url = self._name_and_url(("admin:new_event_wizard_start", {}))
elif model[1] == "room": elif model[1] == "room":
_, url = self._name_and_url(('admin:room-new', {})) _, url = self._name_and_url(("admin:room-new", {}))
# Otherwise, just call the creation form view # Otherwise, just call the creation form view
else: else:
_, url = self._name_and_url((f'admin:AKModel_{model[1]}_add', {})) _, url = self._name_and_url((f"admin:AKModel_{model[1]}_add", {}))
response = self.client.get(url) response = self.client.get(url)
self.assertEqual(response.status_code, 200, msg=f"Add form for model {model[1]} ({url}) broken") self.assertEqual(
response.status_code,
200,
msg=f"Add form for model {model[1]} ({url}) broken",
)
for model in self.ADMIN_MODELS: for model in self.ADMIN_MODELS:
# Test the update view using the first existing instance of each model # Test the update view using the first existing instance of each model
m = model[0].objects.first() m = model[0].objects.first()
if m is not None: if m is not None:
_, url = self._name_and_url( _, url = self._name_and_url(
(f'admin:AKModel_{model[1]}_change', {'object_id': m.pk}) (f"admin:AKModel_{model[1]}_change", {"object_id": m.pk})
) )
response = self.client.get(url) response = self.client.get(url)
self.assertEqual(response.status_code, 200, msg=f"Edit form for model {model[1]} ({url}) broken") self.assertEqual(
response.status_code,
200,
msg=f"Edit form for model {model[1]} ({url}) broken",
)
def test_wiki_export(self): def test_wiki_export(self):
""" """
...@@ -260,17 +337,27 @@ class ModelViewTests(BasicViewTests, TestCase): ...@@ -260,17 +337,27 @@ class ModelViewTests(BasicViewTests, TestCase):
""" """
self.client.force_login(self.admin_user) self.client.force_login(self.admin_user)
export_url = reverse_lazy("admin:ak_wiki_export", kwargs={'slug': 'kif42'}) export_url = reverse_lazy("admin:ak_wiki_export", kwargs={"slug": "kif42"})
response = self.client.get(export_url) response = self.client.get(export_url)
self.assertEqual(response.status_code, 200, "Export not working at all") self.assertEqual(response.status_code, 200, "Export not working at all")
export_count = 0 export_count = 0
for _, aks in response.context["categories_with_aks"]: for _, aks in response.context["categories_with_aks"]:
for ak in aks: for ak in aks:
self.assertEqual(ak.include_in_export, True, self.assertEqual(
f"AK with export flag set to False (pk={ak.pk}) included in export") ak.include_in_export,
self.assertNotEqual(ak.pk, 1, "AK known to be excluded from export (PK 1) included in export") True,
f"AK with export flag set to False (pk={ak.pk}) included in export",
)
self.assertNotEqual(
ak.pk,
1,
"AK known to be excluded from export (PK 1) included in export",
)
export_count += 1 export_count += 1
self.assertEqual(export_count, AK.objects.filter(event_id=2, include_in_export=True).count(), self.assertEqual(
"Wiki export contained the wrong number of AKs") export_count,
AK.objects.filter(event_id=2, include_in_export=True).count(),
"Wiki export contained the wrong number of AKs",
)