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

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
Show changes
Commits on Source (288)
Showing
with 1188 additions and 326 deletions
uwsgi==2.0.25.1
uwsgi==2.0.30
image: python:3.9
image: python:3.11
services:
- mysql
......@@ -26,10 +26,9 @@ cache:
- pip install pylint-gitlab pylint-django
- mysql --version
check:
migrations:
extends: .before_script_template
script:
- ./Utils/check.sh --all
- source venv/bin/activate
- ./manage.py makemigrations --dry-run --check
......@@ -38,7 +37,7 @@ test:
script:
- source venv/bin/activate
- 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
after_script:
- source venv/bin/activate
......
......@@ -113,6 +113,10 @@ msgstr "AK-Einreichung"
msgid "AK History"
msgstr "AK-Verlauf"
#: AKDashboard/templates/AKDashboard/dashboard_row.html:71
msgid "AK Preferences"
msgstr "AK-Präferenzen"
#: AKDashboard/views.py:69
#, python-format
msgid "New AK: %(ak)s."
......
......@@ -9,6 +9,7 @@
{% endblock %}
{% block content %}
{% include "messages.html" %}
{% if total_event_count > 0 %}
{% for event in active_and_current_events %}
<div class="dashboard-row">
......
......@@ -12,6 +12,7 @@
{% endblock %}
{% block content %}
{% include "messages.html" %}
<div class="dashboard-row">
{% include "AKDashboard/dashboard_row.html" %}
......
......@@ -63,6 +63,17 @@
<span class='text'>{% trans 'AK History' %}</span>
</div>
</a>
{% if 'AKPreference'|check_app_installed and event.active %}
{% if not event.poll_hidden or user.is_staff %}
<a class="dashboard-box btn btn-primary"
href="{% url 'poll:poll' event_slug=event.slug %}">
<div class="col-sm-12 col-md-3 col-lg-2 dashboard-button">
<span class="fa fa-poll"></span>
<span class='text'>{% trans 'AK Preferences' %}</span>
</div>
</a>
{% endif %}
{% endif %}
{% for button in event.dashboardbutton_set.all %}
<a class="dashboard-box btn btn-{{ button.get_color_display }}"
href="{{ button.url }}">
......
import zoneinfo
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.utils.timezone import now
from AKDashboard.models import DashboardButton
from AKModel.models import Event, AK, AKCategory
from AKModel.tests import BasicViewTests
from AKModel.models import AK, AKCategory, Event
from AKModel.tests.test_views import BasicViewTests
class DashboardTests(TestCase):
"""
Specific Dashboard Tests
"""
@classmethod
def setUpTestData(cls):
"""
......@@ -20,17 +22,18 @@ class DashboardTests(TestCase):
"""
super().setUpTestData()
cls.event = Event.objects.create(
name="Dashboard Test Event",
slug="dashboardtest",
timezone=zoneinfo.ZoneInfo("Europe/Berlin"),
start=now(),
end=now(),
active=True,
plan_hidden=False,
name="Dashboard Test Event",
slug="dashboardtest",
timezone=zoneinfo.ZoneInfo("Europe/Berlin"),
start=now(),
end=now(),
active=True,
plan_hidden=False,
poll_hidden=False,
)
cls.default_category = AKCategory.objects.create(
name="Test Category",
event=cls.event,
name="Test Category",
event=cls.event,
)
def test_dashboard_view(self):
......@@ -62,12 +65,12 @@ class DashboardTests(TestCase):
# History should be empty
response = self.client.get(url)
self.assertQuerysetEqual(response.context["recent_changes"], [])
self.assertQuerySetEqual(response.context["recent_changes"], [])
AK.objects.create(
name="Test AK",
category=self.default_category,
event=self.event,
name="Test AK",
category=self.default_category,
event=self.event,
)
# History should now contain one AK (Test AK)
......@@ -144,6 +147,26 @@ class DashboardTests(TestCase):
self.assertContains(response, "Current AKs")
self.assertContains(response, "AK Wall")
def test_poll_hidden(self):
"""
Test visibility of poll buttons with regard to poll visibility status for a given event
"""
url_event_dashboard = reverse('dashboard:dashboard_event', kwargs={"slug": self.event.slug})
if apps.is_installed('AKPreference'):
# Poll hidden? No buttons should show up
self.event.poll_hidden = True
self.event.save()
response = self.client.get(url_event_dashboard)
self.assertNotContains(response, "AK Preferences")
# Poll not hidden?
# Buttons to preference poll should be on the page
self.event.poll_hidden = False
self.event.save()
response = self.client.get(url_event_dashboard)
self.assertContains(response, "AK Preferences")
def test_dashboard_buttons(self):
"""
Make sure manually added buttons are displayed correctly
......@@ -154,8 +177,8 @@ class DashboardTests(TestCase):
self.assertNotContains(response, "Dashboard Button Test")
DashboardButton.objects.create(
text="Dashboard Button Test",
event=self.event
text="Dashboard Button Test",
event=self.event
)
response = self.client.get(url_event_dashboard)
......
......@@ -20,7 +20,7 @@ from AKModel.models import Event, AKOwner, AKCategory, AKTrack, AKRequirement, A
ConstraintViolation, DefaultSlot, AKType
from AKModel.urls import get_admin_urls_event_wizard, get_admin_urls_event
from AKModel.views.ak import AKResetInterestView, AKResetInterestCounterView
from AKModel.views.manage import CVMarkResolvedView, CVSetLevelViolationView, CVSetLevelWarningView
from AKModel.views.manage import CVMarkResolvedView, CVSetLevelViolationView, CVSetLevelWarningView, ClearScheduleView
class EventRelatedFieldListFilter(RelatedFieldListFilter):
......@@ -51,12 +51,16 @@ class EventAdmin(admin.ModelAdmin):
wizard.
"""
model = Event
list_display = ['name', 'status_url', 'place', 'start', 'end', 'active', 'plan_hidden']
list_display = ['name', 'status_url', 'place', 'start', 'end', 'active', 'plan_hidden', 'poll_hidden']
list_filter = ['active']
list_editable = ['active']
ordering = ['-start']
readonly_fields = ['status_url', 'plan_hidden', 'plan_published_at', 'toggle_plan_visibility']
actions = ['publish', 'unpublish']
readonly_fields = [
'status_url',
'plan_hidden', 'plan_published_at', 'toggle_plan_visibility',
'poll_hidden', 'poll_published_at', 'toggle_poll_visibility',
]
actions = ['publish_plan', 'unpublish_plan', 'publish_poll', 'unpublish_poll']
def add_view(self, request, form_url='', extra_context=None):
# Override
......@@ -81,6 +85,10 @@ class EventAdmin(admin.ModelAdmin):
from AKScheduling.urls import get_admin_urls_scheduling # pylint: disable=import-outside-toplevel
urls.extend(get_admin_urls_scheduling(self.admin_site))
if apps.is_installed("AKSolverInterface"):
from AKSolverInterface.urls import get_admin_urls_solver_interface # pylint: disable=import-outside-toplevel
urls.extend(get_admin_urls_solver_interface(self.admin_site))
# Make sure built-in URLs are available as well
urls.extend(super().get_urls())
return urls
......@@ -115,13 +123,31 @@ class EventAdmin(admin.ModelAdmin):
text = _('Unpublish plan')
return format_html("<a href='{url}'>{text}</a>", url=url, text=text)
@display(description=_("Toggle poll visibility"))
def toggle_poll_visibility(self, obj):
"""
Define a read-only field to toggle the visibility of the preference poll of this event
This will choose from two different link targets/views depending on the current visibility status
:param obj: event to change the visibility of the poll for
:return: toggling link (HTML)
:rtype: str
"""
if obj.poll_hidden:
url = f"{reverse_lazy('admin:poll-publish')}?pks={obj.pk}"
text = _('Publish preference poll')
else:
url = f"{reverse_lazy('admin:poll-unpublish')}?pks={obj.pk}"
text = _('Unpublish preference poll')
return format_html("<a href='{url}'>{text}</a>", url=url, text=text)
def get_form(self, request, obj=None, change=False, **kwargs):
# Override (update) form rendering to make sure the timezone of the event is used
timezone.activate(obj.timezone)
return super().get_form(request, obj, change, **kwargs)
@action(description=_('Publish plan'))
def publish(self, request, queryset):
def publish_plan(self, request, queryset):
"""
Admin action to publish the plan
"""
......@@ -129,7 +155,7 @@ class EventAdmin(admin.ModelAdmin):
return HttpResponseRedirect(f"{reverse_lazy('admin:plan-publish')}?pks={','.join(str(pk) for pk in selected)}")
@action(description=_('Unpublish plan'))
def unpublish(self, request, queryset):
def unpublish_plan(self, request, queryset):
"""
Admin action to hide the plan
"""
......@@ -137,6 +163,23 @@ class EventAdmin(admin.ModelAdmin):
return HttpResponseRedirect(
f"{reverse_lazy('admin:plan-unpublish')}?pks={','.join(str(pk) for pk in selected)}")
@action(description=_('Publish preference poll'))
def publish_poll(self, request, queryset):
"""
Admin action to publish the preference poll
"""
selected = queryset.values_list('pk', flat=True)
return HttpResponseRedirect(f"{reverse_lazy('admin:poll-publish')}?pks={','.join(str(pk) for pk in selected)}")
@action(description=_('Unpublish preference poll'))
def unpublish_poll(self, request, queryset):
"""
Admin action to hide the preference poll
"""
selected = queryset.values_list('pk', flat=True)
return HttpResponseRedirect(
f"{reverse_lazy('admin:poll-unpublish')}?pks={','.join(str(pk) for pk in selected)}")
class PrepopulateWithNextActiveEventMixin:
"""
......@@ -286,7 +329,8 @@ class AKAdmin(PrepopulateWithNextActiveEventMixin, SimpleHistoryAdmin):
list_filter = ['event',
WishFilter,
('category', EventRelatedFieldListFilter),
('requirements', EventRelatedFieldListFilter)
('requirements', EventRelatedFieldListFilter),
('types', EventRelatedFieldListFilter),
]
list_editable = ['short_name', 'track', 'interest_counter']
ordering = ['pk']
......@@ -436,10 +480,12 @@ class AKSlotAdmin(EventTimezoneFormMixin, PrepopulateWithNextActiveEventMixin, a
"""
model = AKSlot
list_display = ['id', 'ak', 'room', 'start', 'duration', 'event']
list_filter = ['event', ('room', EventRelatedFieldListFilter)]
list_filter = ['event', "fixed", ('room', EventRelatedFieldListFilter),
('ak__category', EventRelatedFieldListFilter)]
ordering = ['start']
readonly_fields = ['ak_details_link', 'updated']
form = AKSlotAdminForm
actions = ["reset_scheduling"]
@display(description=_('AK Details'))
def ak_details_link(self, akslot):
......@@ -455,6 +501,36 @@ class AKSlotAdmin(EventTimezoneFormMixin, PrepopulateWithNextActiveEventMixin, a
return mark_safe(str(link))
return "-"
def get_urls(self):
"""
Add additional URLs/views
"""
urls = [
path('clear-schedule/', ClearScheduleView.as_view(), name="clear-schedule"),
]
urls.extend(super().get_urls())
return urls
@action(description=_("Clear start/rooms"))
def reset_scheduling(self, request, queryset):
"""
Action: Reset start and room field for the given AKs
Will use a typical admin confirmation view flow
"""
if queryset.filter(fixed=True).exists():
self.message_user(
request,
_(
"Cannot reset scheduling for fixed AKs. "
"Please make sure to filter out fixed AKs first."
),
messages.ERROR,
)
return redirect('admin:AKModel_akslot_changelist')
selected = queryset.values_list('pk', flat=True)
return HttpResponseRedirect(
f"{reverse_lazy('admin:clear-schedule')}?pks={','.join(str(pk) for pk in selected)}")
ak_details_link.short_description = _('AK Details')
......
......@@ -12,7 +12,7 @@ from django.utils.dateparse import parse_datetime
from django.utils.translation import gettext_lazy as _
from AKModel.availability.models import Availability
from AKModel.availability.serializers import AvailabilitySerializer
from AKModel.availability.serializers import AvailabilityFormSerializer
from AKModel.models import Event
......@@ -41,22 +41,11 @@ class AvailabilitiesFormMixin(forms.Form):
:rtype: str
"""
if instance:
availabilities = AvailabilitySerializer(
instance.availabilities.all(), many=True
).data
availabilities = instance.availabilities.all()
else:
availabilities = []
return json.dumps(
{
'availabilities': availabilities,
'event': {
# 'timezone': event.timezone,
'date_from': str(event.start),
'date_to': str(event.end),
},
}
)
return json.dumps(AvailabilityFormSerializer((availabilities, event)).data)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
......@@ -65,9 +54,10 @@ class AvailabilitiesFormMixin(forms.Form):
if isinstance(self.event, int):
self.event = Event.objects.get(pk=self.event)
initial = kwargs.pop('initial', {})
initial['availabilities'] = self._serialize(self.event, kwargs['instance'])
if 'availabilities' not in initial:
initial['availabilities'] = self._serialize(self.event, kwargs.get('instance'))
if not isinstance(self, forms.BaseModelForm):
kwargs.pop('instance')
kwargs.pop('instance', None)
kwargs['initial'] = initial
def _parse_availabilities_json(self, jsonavailabilities):
......@@ -183,7 +173,7 @@ class AvailabilitiesFormMixin(forms.Form):
for avail in availabilities:
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
......
......@@ -11,6 +11,8 @@ from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _
from AKModel.models import Event, AKOwner, Room, AK, AKCategory
# TODO: Decouple from AKPreference app
from AKPreference.models import EventParticipant
zero_time = datetime.time(0, 0)
......@@ -24,6 +26,7 @@ zero_time = datetime.time(0, 0)
# enable availabilities for AKs and AKCategories
# add verbose names and help texts to model attributes
# adapt or extemd documentation
# add participants
class Availability(models.Model):
......@@ -79,20 +82,48 @@ class Availability(models.Model):
verbose_name=_('AK Category'),
help_text=_('AK Category whose availability this is'),
)
participant = models.ForeignKey(
to=EventParticipant,
related_name='availabilities',
on_delete=models.CASCADE,
null=True,
blank=True,
verbose_name=_('Participant'),
help_text=_('Participant whose availability this is'),
)
start = models.DateTimeField()
end = models.DateTimeField()
def __str__(self) -> str:
person = self.person.name if self.person else None
participant = str(self.participant) if self.participant else None
room = getattr(self.room, 'name', None)
event = getattr(getattr(self, 'event', None), 'name', None)
ak = getattr(self.ak, 'name', None)
ak_category = getattr(self.ak_category, 'name', None)
return f'Availability(event={event}, person={person}, room={room}, ak={ak}, ak category={ak_category})'
arg_list = [
f"event={event}",
f"person={person}",
f"room={room}",
f"ak={ak}",
f"ak category={ak_category}",
f"participant={participant}",
]
return f'Availability({", ".join(arg_list)})'
def __hash__(self):
return hash(
(getattr(self, 'event', None), self.person, self.room, self.ak, self.ak_category, self.start, self.end))
(
getattr(self, 'event', None),
self.person,
self.room,
self.ak,
self.ak_category,
self.participant,
self.start,
self.end,
)
)
def __eq__(self, other: 'Availability') -> bool:
"""Comparisons like ``availability1 == availability2``.
......@@ -103,7 +134,7 @@ class Availability(models.Model):
return all(
(
getattr(self, attribute, None) == getattr(other, attribute, None)
for attribute in ['event', 'person', 'room', 'ak', 'ak_category', 'start', 'end']
for attribute in ['event', 'person', 'room', 'ak', 'ak_category', 'participant', 'start', 'end']
)
)
......@@ -151,9 +182,12 @@ class Availability(models.Model):
if not other.overlaps(self, strict=False):
raise Exception('Only overlapping Availabilities can be merged.')
return Availability(
avail = Availability(
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':
"""Performs the merge operation: ``availability1 | availability2``"""
......@@ -168,9 +202,12 @@ class Availability(models.Model):
if not other.overlaps(self, False):
raise Exception('Only overlapping Availabilities can be intersected.')
return Availability(
avail = Availability(
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':
"""Performs the intersect operation: ``availability1 &
......@@ -247,7 +284,15 @@ class Availability(models.Model):
f'{self.end.astimezone(self.event.timezone).strftime("%a %H:%M")}')
@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,
participant: EventParticipant | None = None,
) -> "Availability":
"""
Create an availability covering exactly the time between event start and event end.
Can e.g., be used to create default availabilities.
......@@ -265,7 +310,31 @@ class Availability(models.Model):
timeframe_end = event.end # adapt to our event model
timeframe_end = timeframe_end + datetime.timedelta(days=1)
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, participant=participant)
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:
verbose_name = _('Availability')
......
......@@ -4,9 +4,10 @@
# Documentation was mainly added by us, other changes are marked in the code
from django.utils import timezone
from rest_framework.fields import SerializerMethodField
from rest_framework.serializers import ModelSerializer
from rest_framework.serializers import BaseSerializer, ModelSerializer
from AKModel.availability.models import Availability
from AKModel.models import Event
class AvailabilitySerializer(ModelSerializer):
......@@ -44,3 +45,28 @@ class AvailabilitySerializer(ModelSerializer):
class Meta:
model = Availability
fields = ('id', 'start', 'end', 'allDay')
class AvailabilityFormSerializer(BaseSerializer):
"""Serializer to configure an availability form."""
def create(self, validated_data):
raise ValueError("`AvailabilityFormSerializer` is read-only.")
def to_internal_value(self, data):
raise ValueError("`AvailabilityFormSerializer` is read-only.")
def update(self, instance, validated_data):
raise ValueError("`AvailabilityFormSerializer` is read-only.")
def to_representation(self, instance: tuple[Availability, Event], **kwargs):
availabilities, event = instance
return {
'availabilities': AvailabilitySerializer(availabilities, many=True).data,
'event': {
# 'timezone': event.timezone,
'date_from': str(event.start),
'date_to': str(event.end),
},
}
......@@ -93,7 +93,7 @@
"model": "AKModel.akcategory",
"pk": 1,
"fields": {
"name": "Spa",
"name": "Spaß",
"color": "275246",
"description": "",
"present_by_default": true,
......@@ -115,7 +115,7 @@
"model": "AKModel.akcategory",
"pk": 3,
"fields": {
"name": "Spa/Kultur",
"name": "Spaß/Kultur",
"color": "333333",
"description": "",
"present_by_default": true,
......@@ -198,7 +198,8 @@
"pk": 1,
"fields": {
"name": "Input",
"event": 2
"event": 2,
"slug": "input"
}
},
{
......@@ -437,6 +438,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",
"pk": 1,
......@@ -461,6 +518,19 @@
"properties": []
}
},
{
"model": "AKModel.room",
"pk": 3,
"fields": {
"name": "BBB Session 1",
"location": "",
"capacity": -1,
"event": 1,
"properties": [
2
]
}
},
{
"model": "AKModel.akslot",
"pk": 1,
......@@ -526,6 +596,58 @@
"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",
"pk": 1,
......@@ -669,5 +791,71 @@
"start": "2020-11-07T18: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]
}
}
]
......@@ -34,9 +34,10 @@ class NewEventWizardStartForm(forms.ModelForm):
"""
class Meta:
model = Event
fields = ['name', 'slug', 'timezone', 'plan_hidden']
fields = ['name', 'slug', 'timezone', 'plan_hidden', 'poll_hidden']
widgets = {
'plan_hidden': forms.HiddenInput(),
'poll_hidden': forms.HiddenInput(),
}
# Special hidden field for wizard state handling
......@@ -53,7 +54,7 @@ class NewEventWizardSettingsForm(forms.ModelForm):
class Meta:
model = Event
fields = "__all__"
exclude = ['plan_published_at']
exclude = ['plan_published_at', 'poll_published_at']
widgets = {
'name': forms.HiddenInput(),
'slug': forms.HiddenInput(),
......@@ -65,6 +66,7 @@ class NewEventWizardSettingsForm(forms.ModelForm):
'interest_end': DateTimeInput(),
'reso_deadline': DateTimeInput(),
'plan_hidden': forms.HiddenInput(),
'poll_hidden': forms.HiddenInput(),
}
......@@ -173,6 +175,18 @@ class SlideExportForm(AdminIntermediateForm):
initial=3,
label=_("# next AKs"),
help_text=_("How many next AKs should be shown on a slide?"))
types = forms.MultipleChoiceField(
label=_("AK Types"),
help_text=_("Which AK types should be included in the slides?"),
widget=forms.CheckboxSelectMultiple,
choices=[],
required=False)
types_all_selected_only = forms.BooleanField(
initial=False,
label=_("Only show AKs with all selected types?"),
help_text=_("If checked, only AKs that have all selected types will be shown in the slides. "
"If unchecked, AKs with at least one of the selected types will be shown."),
required=False)
presentation_mode = forms.TypedChoiceField(
initial=False,
label=_("Presentation only?"),
......
# 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",
),
),
]
# Generated by Django 4.2.13 on 2025-02-10 10:23
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("AKModel", "0064_event_export_slot"),
]
operations = [
migrations.CreateModel(
name="EventParticipant",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"name",
models.CharField(
blank=True,
help_text="Name to identify a participant by (in case of questions from the organizers)",
max_length=64,
verbose_name="Nickname",
),
),
(
"institution",
models.CharField(
blank=True,
help_text="Uni etc.",
max_length=128,
verbose_name="Institution",
),
),
(
"event",
models.ForeignKey(
help_text="Associated event",
on_delete=django.db.models.deletion.CASCADE,
to="AKModel.event",
verbose_name="Event",
),
),
],
options={
"verbose_name": "Participant",
"verbose_name_plural": "Participants",
"ordering": ["name"],
},
),
migrations.CreateModel(
name="AKPreference",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"preference",
models.PositiveSmallIntegerField(
choices=[
(0, "Ignore"),
(1, "Prefer"),
(2, "Strong prefer"),
(3, "Required"),
],
default=0,
help_text="Preference level for the AK",
verbose_name="Preference",
),
),
(
"ak",
models.ForeignKey(
help_text="AK this preference belongs to",
on_delete=django.db.models.deletion.CASCADE,
to="AKModel.ak",
verbose_name="AK",
),
),
(
"event",
models.ForeignKey(
help_text="Associated event",
on_delete=django.db.models.deletion.CASCADE,
to="AKModel.event",
verbose_name="Event",
),
),
(
"participant",
models.ForeignKey(
help_text="Participant this preference belongs to",
on_delete=django.db.models.deletion.CASCADE,
to="AKModel.eventparticipant",
verbose_name="Participant",
),
),
],
options={
"verbose_name": "AK Preference",
"verbose_name_plural": "AK Preferences",
},
),
migrations.AddField(
model_name="availability",
name="participant",
field=models.ForeignKey(
blank=True,
help_text="Participant whose availability this is",
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="availabilities",
to="AKModel.eventparticipant",
verbose_name="Participant",
),
),
]
# Generated by Django 4.2.13 on 2025-02-10 22:31
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("AKModel", "0065_eventparticipant_akpreference_and_more"),
]
operations = [
migrations.AddField(
model_name="akpreference",
name="slot",
field=models.ForeignKey(
default=None,
help_text="AKSlot this preference belongs to",
on_delete=django.db.models.deletion.CASCADE,
to="AKModel.akslot",
verbose_name="AKSlot",
),
preserve_default=False,
),
migrations.AlterUniqueTogether(
name="akpreference",
unique_together={("event", "participant", "slot")},
),
migrations.RemoveField(
model_name="akpreference",
name="ak",
),
]
# Generated by Django 4.2.13 on 2025-02-11 00:23
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
(
"AKModel",
"0066_akpreference_slot_alter_akpreference_unique_together_and_more",
),
]
operations = [
migrations.AddField(
model_name="eventparticipant",
name="requirements",
field=models.ManyToManyField(
blank=True,
help_text="Participant's Requirements",
to="AKModel.akrequirement",
verbose_name="Requirements",
),
),
migrations.AlterField(
model_name="akpreference",
name="slot",
field=models.ForeignKey(
help_text="AK Slot this preference belongs to",
on_delete=django.db.models.deletion.CASCADE,
to="AKModel.akslot",
verbose_name="AK Slot",
),
),
]
# Generated by Django 5.1.6 on 2025-05-11 15:21
from django.db import migrations, models
def create_slugs(apps, schema_editor):
"""
Automatically generate slugs from existing type names
"""
AKType = apps.get_model("AKModel", "AKType")
for ak_type in AKType.objects.all():
slug = ak_type.name.lower().split(" ")[0]
ak_type.slug = slug[:30] if len(slug) > 30 else slug
ak_type.save()
class Migration(migrations.Migration):
dependencies = [
('AKModel', '0067_eventparticipant_requirements_and_more'),
]
operations = [
migrations.AddField(
model_name='aktype',
name='slug',
field=models.SlugField(max_length=30, null=True, verbose_name='Slug'),
),
migrations.RunPython(create_slugs, migrations.RunPython.noop),
migrations.AlterUniqueTogether(
name='aktype',
unique_together={('event', 'name')},
),
migrations.AlterField(
model_name='aktype',
name='slug',
field=models.SlugField(max_length=30, verbose_name='Slug'),
),
migrations.AlterUniqueTogether(
name='aktype',
unique_together={('event', 'name'), ('event', 'slug')},
),
]