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

add availability model

add model class for availabilities
register availability model for admin view
add availability migration
add German translations for availability model
remove availability modelling TODOs
parent b15b4052
No related branches found
No related tags found
No related merge requests found
......@@ -2,6 +2,7 @@
from django.contrib import admin
from AKModel.availability import Availability
from AKModel.models import Event, AKOwner, AKType, AKTrack, AKTag, AKRequirement, AK, Room, AKSlot
admin.site.register(Event)
......@@ -17,3 +18,5 @@ admin.site.register(AK)
admin.site.register(Room)
admin.site.register(AKSlot)
admin.site.register(Availability)
# This part of the code was adapted from pretalx (https://github.com/pretalx/pretalx)
# Copyright 2017-2019, Tobias Kunze
# Original Copyrights licensed under the Apache License, Version 2.0 http://www.apache.org/licenses/LICENSE-2.0
# Changes are marked in the code
import datetime
from typing import List
from django.db import models
from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _
from AKModel.models import Event, AKOwner, Room, AK, AKType
zero_time = datetime.time(0, 0)
# CHANGES:
# ScopeManager and LogMixin removed as they are not used in this project
# adapted to event, people and room models
# remove serialization as requirements are not covered
# add translation
# add meta class
# enable availabilites for AKs and AKTypes
# add verbose names and help texts to model attributes
class Availability(models.Model):
"""The Availability class models when people or rooms are available for.
The power of this class is not within its rather simple data model,
but with the operations available on it. An availability object can
span multiple days, but due to our choice of input widget, it will
usually only span a single day at most.
"""
event = models.ForeignKey(
to=Event,
related_name='availabilities',
on_delete=models.CASCADE,
verbose_name=_('Event'),
help_text=_('Associated event'),
)
person = models.ForeignKey(
to=AKOwner,
related_name='availabilities',
on_delete=models.CASCADE,
null=True,
blank=True,
verbose_name=_('Person'),
help_text=_('Person whose availability this is'),
)
room = models.ForeignKey(
to=Room,
related_name='availabilities',
on_delete=models.CASCADE,
null=True,
blank=True,
verbose_name=_('Room'),
help_text=_('Room whose availability this is'),
)
ak = models.ForeignKey(
to=AK,
related_name='availabilities',
on_delete=models.CASCADE,
null=True,
blank=True,
verbose_name=_('AK'),
help_text=_('AK whose availability this is'),
)
ak_type = models.ForeignKey(
to=AKType,
related_name='availabilities',
on_delete=models.CASCADE,
null=True,
blank=True,
verbose_name=_('AK Type'),
help_text=_('AK Type whose availability this is'),
)
start = models.DateTimeField()
end = models.DateTimeField()
def __str__(self) -> str:
person = self.person.name if self.person else None
room = getattr(self.room, 'name', None)
event = getattr(getattr(self, 'event', None), 'name', None)
ak = getattr(self.ak, 'name', None)
ak_type = getattr(self.ak_type, 'name', None)
return f'Availability(event={event}, person={person}, room={room}, ak={ak}, ak type={ak_type})'
def __hash__(self):
return hash((getattr(self, 'event', None), self.person, self.room, self.ak, self.ak_type, self.start, self.end))
def __eq__(self, other: 'Availability') -> bool:
"""Comparisons like ``availability1 == availability2``.
Checks if ``event``, ``person``, ``room``, ``ak``, ``ak_type``, ``start`` and ``end``
are the same.
"""
return all(
[
getattr(self, attribute, None) == getattr(other, attribute, None)
for attribute in ['event', 'person', 'room', 'ak', 'ak_type', 'start', 'end']
]
)
@cached_property
def all_day(self) -> bool:
"""Checks if the Availability spans one (or, technically: multiple)
complete day."""
return self.start.time() == zero_time and self.end.time() == zero_time
def overlaps(self, other: 'Availability', strict: bool) -> bool:
"""Test if two Availabilities overlap.
:param other:
:param strict: Only count a real overlap as overlap, not direct adjacency.
"""
if not isinstance(other, Availability):
raise Exception('Please provide an Availability object')
if strict:
return (
(self.start <= other.start < self.end)
or (self.start < other.end <= self.end)
or (other.start <= self.start < other.end)
or (other.start < self.end <= other.end)
)
return (
(self.start <= other.start <= self.end)
or (self.start <= other.end <= self.end)
or (other.start <= self.start <= other.end)
or (other.start <= self.end <= other.end)
)
def contains(self, other: 'Availability') -> bool:
"""Tests if this availability starts before and ends after the
other."""
return self.start <= other.start and self.end >= other.end
def merge_with(self, other: 'Availability') -> 'Availability':
"""Return a new Availability which spans the range of this one and the
given one."""
if not isinstance(other, Availability):
raise Exception('Please provide an Availability object.')
if not other.overlaps(self, strict=False):
raise Exception('Only overlapping Availabilities can be merged.')
return Availability(
start=min(self.start, other.start), end=max(self.end, other.end)
)
def __or__(self, other: 'Availability') -> 'Availability':
"""Performs the merge operation: ``availability1 | availability2``"""
return self.merge_with(other)
def intersect_with(self, other: 'Availability') -> 'Availability':
"""Return a new Availability which spans the range covered both by this
one and the given one."""
if not isinstance(other, Availability):
raise Exception('Please provide an Availability object.')
if not other.overlaps(self, False):
raise Exception('Only overlapping Availabilities can be intersected.')
return Availability(
start=max(self.start, other.start), end=min(self.end, other.end)
)
def __and__(self, other: 'Availability') -> 'Availability':
"""Performs the intersect operation: ``availability1 &
availability2``"""
return self.intersect_with(other)
@classmethod
def union(cls, availabilities: List['Availability']) -> List['Availability']:
"""Return the minimal list of Availability objects which are covered by
at least one given Availability."""
if not availabilities:
return []
availabilities = sorted(availabilities, key=lambda a: a.start)
result = [availabilities[0]]
availabilities = availabilities[1:]
for avail in availabilities:
if avail.overlaps(result[-1], False):
result[-1] = result[-1].merge_with(avail)
else:
result.append(avail)
return result
@classmethod
def _pair_intersection(
cls,
availabilities_a: List['Availability'],
availabilities_b: List['Availability'],
) -> List['Availability']:
"""return the list of Availabilities, which are covered by each of the
given sets."""
result = []
# yay for O(b*a) time! I am sure there is some fancy trick to make this faster,
# but we're dealing with less than 100 items in total, sooo.. ¯\_(ツ)_/¯
for a in availabilities_a:
for b in availabilities_b:
if a.overlaps(b, True):
result.append(a.intersect_with(b))
return result
@classmethod
def intersection(
cls, *availabilitysets: List['Availability']
) -> List['Availability']:
"""Return the list of Availabilities which are covered by all of the
given sets."""
# get rid of any overlaps and unmerged ranges in each set
availabilitysets = [cls.union(avialset) for avialset in availabilitysets]
# bail out for obvious cases (there are no sets given, one of the sets is empty)
if not availabilitysets:
return []
if not all(availabilitysets):
return []
# start with the very first set ...
result = availabilitysets[0]
for availset in availabilitysets[1:]:
# ... subtract each of the other sets
result = cls._pair_intersection(result, availset)
return result
class Meta:
verbose_name = _('Availability')
verbose_name_plural = _('Availabilities')
ordering = ['event', 'start']
......@@ -2,7 +2,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-10-12 12:40+0000\n"
"POT-Creation-Date: 2019-10-12 14:29+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
......@@ -11,8 +11,60 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: models.py:10 models.py:44 models.py:58 models.py:70 models.py:81
#: models.py:95 models.py:136
#: availability.py:38 models.py:18 models.py:31 models.py:81 models.py:122
#: models.py:139 models.py:159
msgid "Event"
msgstr "Event"
#: availability.py:39 models.py:32 models.py:82 models.py:123 models.py:140
#: models.py:160
msgid "Associated event"
msgstr "Zugehöriges Event"
#: availability.py:47
msgid "Person"
msgstr "Person"
#: availability.py:48
msgid "Person whose availability this is"
msgstr "Person deren Verfügbarkeit hier abgebildet wird"
#: availability.py:56 models.py:143 models.py:153
msgid "Room"
msgstr "Raum"
#: availability.py:57
msgid "Room whose availability this is"
msgstr "Raum dessen Verfügbarkeit hier abgebildet wird"
#: availability.py:65 models.py:126 models.py:152
msgid "AK"
msgstr "AK"
#: availability.py:66
#, fuzzy
#| msgid "Availabilities"
msgid "AK whose availability this is"
msgstr "Verfügbarkeiten"
#: availability.py:74 models.py:48
msgid "AK Type"
msgstr "AK Typ"
#: availability.py:75
msgid "AK Type whose availability this is"
msgstr "AK Typ dessen Verfügbarkeit hier abgebildet wird"
#: availability.py:234
msgid "Availability"
msgstr "Verfügbarkeit"
#: availability.py:235
msgid "Availabilities"
msgstr "Verfügbarkeiten"
#: models.py:10 models.py:44 models.py:56 models.py:68 models.py:79
#: models.py:93 models.py:133
msgid "Name"
msgstr "Name"
......@@ -52,11 +104,6 @@ msgstr "Aktiver Status"
msgid "Marks currently active events"
msgstr "Markiert aktuell aktive Events"
#: models.py:18 models.py:31 models.py:83 models.py:125 models.py:144
#: models.py:164
msgid "Event"
msgstr "Event"
#: models.py:19
msgid "Events"
msgstr "Events"
......@@ -85,7 +132,7 @@ msgstr "Instutution"
msgid "Uni etc."
msgstr "Universität o.ä."
#: models.py:29 models.py:103
#: models.py:29 models.py:101
msgid "Web Link"
msgstr "Internet Link"
......@@ -93,10 +140,6 @@ msgstr "Internet Link"
msgid "Link to Homepage"
msgstr "Link zu Homepage oder Webseite"
#: models.py:32 models.py:84 models.py:126 models.py:145 models.py:165
msgid "Associated event"
msgstr "Zugehöriges Event"
#: models.py:35
msgid "AK Owner"
msgstr "AK Leitung"
......@@ -109,234 +152,222 @@ msgstr "AK Leitungen"
msgid "Name of the AK Type"
msgstr "Name des AK Typs"
#: models.py:45 models.py:59
#: models.py:45 models.py:57
msgid "Color"
msgstr "Farbe"
#: models.py:45 models.py:59
#: models.py:45 models.py:57
msgid "Color for displaying"
msgstr "Farbe für die Anzeige"
#: models.py:50
msgid "AK Type"
msgstr "AK Typ"
#: models.py:51
#: models.py:49
msgid "AK Types"
msgstr "AK Typen"
#: models.py:58
#: models.py:56
msgid "Name of the AK Track"
msgstr "Name des AK Tracks"
#: models.py:62
#: models.py:60
msgid "AK Track"
msgstr "AK Track"
#: models.py:63
#: models.py:61
msgid "AK Tracks"
msgstr "AK Tracks"
#: models.py:70
#: models.py:68
msgid "Name of the AK Tag"
msgstr "Name das AK Tags"
#: models.py:73
#: models.py:71
msgid "AK Tag"
msgstr "AK Tag"
#: models.py:74
#: models.py:72
msgid "AK Tags"
msgstr "AK Tags"
#: models.py:81
#: models.py:79
msgid "Name of the Requirement"
msgstr "Name der Anforderung"
#: models.py:87
#: models.py:85
msgid "AK Requirement"
msgstr "AK Anforderung"
#: models.py:88
#: models.py:86
msgid "AK Requirements"
msgstr "AK Anforderungen"
#: models.py:95
#: models.py:93
msgid "Name of the AK"
msgstr "Name des AKs"
#: models.py:96
#: models.py:94
msgid "Short Name"
msgstr "Kurzer Name"
#: models.py:97
#: models.py:95
msgid "Name displayed in the schedule"
msgstr "Name zur Anzeige im AK Plan"
#: models.py:98
#: models.py:96
msgid "Description"
msgstr "Beschreibung"
#: models.py:98
#: models.py:96
msgid "Description of the AK"
msgstr "Beschreibung des AKs"
#: models.py:100
#: models.py:98
msgid "Owners"
msgstr "Leitungen"
#: models.py:100
#: models.py:98
msgid "Those organizing the AK"
msgstr "Menschen, die den AK organisieren und halten"
#: models.py:103
#: models.py:101
msgid "Link to wiki page"
msgstr "Link zur Wiki Seite"
#: models.py:105
#: models.py:103
msgid "Type"
msgstr "Typ"
#: models.py:105
#: models.py:103
msgid "Type of the AK"
msgstr "Typ des AKs"
#: models.py:106
#: models.py:104
msgid "Tags"
msgstr "Tags"
#: models.py:106
#: models.py:104
msgid "Tags provided by owners"
msgstr "Tags, die durch die AK Leitung vergeben wurden"
#: models.py:107
#: models.py:105
msgid "Track"
msgstr "Track"
#: models.py:108
#: models.py:106
msgid "Track the AK belongs to"
msgstr "Track zu dem der AK gehört"
#: models.py:110
#: models.py:108
msgid "Resolution Intention"
msgstr "Resolutionsabsicht"
#: models.py:111
#: models.py:109
msgid "Intends to submit a resolution"
msgstr "Beabsichtigt eine Resolution einzureichen"
#: models.py:112
#: models.py:110
msgid "Requirements"
msgstr "Anforderungen"
#: models.py:113
#: models.py:111
msgid "AK's Requirements"
msgstr "Anforderungen des AKs"
#: models.py:115
#: models.py:113
msgid "Conflicting AKs"
msgstr "AK Konflikte"
#: models.py:116
#: models.py:114
msgid "AKs that conflict and thus must not take place at the same time"
msgstr "AKs, die Konflikte haben und deshalb nicht gleichzeitig stattfinden dürfen"
#: models.py:117
#: models.py:115
msgid "Prerequisite AKs"
msgstr "Vorausgesetzte AKs"
#: models.py:118
#: models.py:116
msgid "AKs that should precede this AK in the schedule"
msgstr "AKS die im AK Plan vor diesem AK stattfinden müssen"
#: models.py:121
#: models.py:118
msgid "Internal Notes"
msgstr "Interne Notizen"
#: models.py:121
#: models.py:118
msgid "Notes to organizers"
msgstr "Notizen an die Organisator*innen"
#: models.py:123
#: models.py:120
msgid "Interest"
msgstr "Interesse"
#: models.py:123
#: models.py:120
msgid "Expected number of people"
msgstr "Erwartete Personenzahl"
#: models.py:129 models.py:157
msgid "AK"
msgstr "AK"
#: models.py:130
#: models.py:127
msgid "AKs"
msgstr "AKs"
#: models.py:136
#: models.py:133
msgid "Name or number of the room"
msgstr "Name oder Nummer des Raums"
#: models.py:137
#: models.py:134
msgid "Building"
msgstr "Gebäude"
#: models.py:137
#: models.py:134
msgid "Name/number of the building"
msgstr "Name oder Nummer des Gebäudes"
#: models.py:138
#: models.py:135
msgid "Capacity"
msgstr "Kapazität"
#: models.py:138
#: models.py:135
msgid "Maximum number of people"
msgstr "Maximale Personenzahl"
#: models.py:139
#: models.py:136
msgid "Properties"
msgstr "Eigenschaften"
#: models.py:140
#: models.py:137
msgid "AK requirements fulfilled by the room"
msgstr "AK Anforderungen, die dieser Raum erfüllt"
#: models.py:148 models.py:158
msgid "Room"
msgstr "Raum"
#: models.py:149
#: models.py:144
msgid "Rooms"
msgstr "Räume"
#: models.py:157
#: models.py:152
msgid "AK being mapped"
msgstr "AK, der zugeordnet wird"
#: models.py:159
#: models.py:154
msgid "Room the AK will take place in"
msgstr "Raum in dem der AK stattfindet"
#: models.py:160
#: models.py:155
msgid "Slot Begin"
msgstr "Beginn des Slots"
#: models.py:160
#: models.py:155
msgid "Time and date the slot begins"
msgstr "Zeit und Datum zu der der AK beginnt"
#: models.py:161
#: models.py:156
msgid "Duration"
msgstr "Dauer"
#: models.py:162
#: models.py:157
msgid "Length in hours"
msgstr "Länge in Stunden"
#: models.py:168
#: models.py:163
msgid "AK Slot"
msgstr "AK Slot"
#: models.py:169
#: models.py:164
msgid "AK Slots"
msgstr "AK Slot"
# Generated by Django 2.2.6 on 2019-10-12 14:35
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('AKModel', '0006_translation_akmodel'),
]
operations = [
migrations.CreateModel(
name='Availability',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('start', models.DateTimeField()),
('end', models.DateTimeField()),
('ak', models.ForeignKey(blank=True, help_text='AK whose availability this is', null=True,
on_delete=django.db.models.deletion.CASCADE, related_name='availabilities',
to='AKModel.AK', verbose_name='AK')),
('ak_type', models.ForeignKey(blank=True, help_text='AK Type whose availability this is', null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name='availabilities', to='AKModel.AKType',
verbose_name='AK Type')),
('event', models.ForeignKey(help_text='Associated event', on_delete=django.db.models.deletion.CASCADE,
related_name='availabilities', to='AKModel.Event', verbose_name='Event')),
('person', models.ForeignKey(blank=True, help_text='Person whose availability this is', null=True,
on_delete=django.db.models.deletion.CASCADE, related_name='availabilities',
to='AKModel.AKOwner', verbose_name='Person')),
('room', models.ForeignKey(blank=True, help_text='Room whose availability this is', null=True,
on_delete=django.db.models.deletion.CASCADE, related_name='availabilities',
to='AKModel.Room', verbose_name='Room')),
],
options={
'verbose_name': 'Availability',
'verbose_name_plural': 'Availabilities',
'ordering': ['event', 'start'],
},
),
]
......@@ -44,8 +44,6 @@ class AKType(models.Model):
name = models.CharField(max_length=64, unique=True, verbose_name=_('Name'), help_text=_('Name of the AK Type'))
color = models.CharField(max_length=7, blank=True, verbose_name=_('Color'), help_text=_('Color for displaying'))
# TODO model availability
class Meta:
verbose_name = _('AK Type')
verbose_name_plural = _('AK Types')
......@@ -116,7 +114,6 @@ class AK(models.Model):
help_text=_('AKs that conflict and thus must not take place at the same time'))
prerequisites = models.ManyToManyField(to='AK', blank=True, verbose_name=_('Prerequisite AKs'),
help_text=_('AKs that should precede this AK in the schedule'))
# TODO model availability
notes = models.TextField(blank=True, verbose_name=_('Internal Notes'), help_text=_('Notes to organizers'))
......@@ -139,8 +136,6 @@ class Room(models.Model):
properties = models.ManyToManyField(to=AKRequirement, verbose_name=_('Properties'),
help_text=_('AK requirements fulfilled by the room'))
# TODO model availability
event = models.ForeignKey(to=Event, on_delete=models.CASCADE, verbose_name=_('Event'),
help_text=_('Associated event'))
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment