Skip to content
Snippets Groups Projects

Compare revisions

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

Source

Select target project
No results found

Target

Select target project
  • konstantin/akplanning
  • matedealer/akplanning
  • kif/akplanning
  • mirco/akplanning
  • lordofthevoid/akplanning
  • voidptr/akplanning
  • xayomer/akplanning-fork
  • mollux/akplanning
  • neumantm/akplanning
  • mmarx/akplanning
  • nerf/akplanning
  • felix_bonn/akplanning
  • sebastian.uschmann/akplanning
13 results
Show changes
Showing
with 1334 additions and 44 deletions
# Generated by Django 3.1.8 on 2021-10-28 16:34
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('AKModel', '0046_present_by_default'),
]
operations = [
migrations.AlterField(
model_name='room',
name='capacity',
field=models.IntegerField(help_text='Maximum number of people (-1 for unlimited).', verbose_name='Capacity'),
),
]
# Generated by Django 3.1.8 on 2021-10-28 20:47
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('AKModel', '0047_room_capacity_help'),
]
operations = [
migrations.AlterField(
model_name='constraintviolation',
name='type',
field=models.CharField(choices=[('ots', 'Owner has two parallel slots'), ('soa', "AK Slot was scheduled outside the AK's availabilities"), ('rts', 'Room has two AK slots scheduled at the same time'), ('rng', 'Room does not satisfy the requirement of the scheduled AK'), ('acc', 'AK Slot is scheduled at the same time as an AK listed as a conflict'), ('abp', 'AK Slot is scheduled before an AK listed as a prerequisite'), ('aar', 'AK Slot for AK with intention to submit a resolution is scheduled after resolution deadline'), ('acm', 'AK Slot in a category is outside that categories availabilities'), ('asc', 'Two AK Slots for the same AK scheduled at the same time'), ('rce', 'Room does not have enough space for interest in scheduled AK Slot'), ('soe', "AK Slot is scheduled outside the event's availabilities")], help_text='Type of violation, i.e. what kind of constraint was violated', max_length=3, verbose_name='Type'),
),
]
# Generated by Django 3.1.8 on 2021-10-29 10:01
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('AKModel', '0048_constraint_violation_text'),
]
operations = [
migrations.AddField(
model_name='event',
name='interest_end',
field=models.DateTimeField(blank=True, help_text='Closing time for expression of interest.', null=True,
verbose_name='Interest Window End'),
),
migrations.AddField(
model_name='event',
name='interest_start',
field=models.DateTimeField(blank=True, help_text='Opening time for expression of interest.', null=True,
verbose_name='Interest Window Start'),
),
]
# Generated by Django 3.1.8 on 2022-05-12 16:57
from django.db import migrations, models
import django.db.models.deletion
def forwards_func(apps, schema_editor):
# Set event to the corresponding even (from the AK) each
AKOrgaMessage = apps.get_model("AKModel", "AKOrgaMessage")
for message in AKOrgaMessage.objects.all():
message.event = message.ak.event
message.save()
def reverse_func(apps, schema_editor):
# No need to do something here, field will be deleted anyway
pass
class Migration(migrations.Migration):
dependencies = [
('AKModel', '0049_interest_window'),
]
operations = [
migrations.AddField(
model_name='akorgamessage',
name='event',
field=models.ForeignKey(blank=True, help_text='Associated event', null=True,
on_delete=django.db.models.deletion.CASCADE, to='AKModel.event',
verbose_name='Event'),
),
migrations.RunPython(forwards_func, reverse_func),
migrations.AlterField(
model_name='akorgamessage',
name='event',
field=models.ForeignKey(help_text='Associated event', on_delete=django.db.models.deletion.CASCADE,
to='AKModel.event', verbose_name='Event'),
),
]
# Generated by Django 3.1.8 on 2022-08-17 17:41
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('AKModel', '0050_message_event_reference'),
]
operations = [
migrations.AlterField(
model_name='ak',
name='notes',
field=models.TextField(blank=True, help_text='Notes to organizers. These are public. For private notes, please use the button for private messages on the detail page of this AK (after creation/editing).', verbose_name='Organizational Notes'),
),
migrations.AlterField(
model_name='historicalak',
name='notes',
field=models.TextField(blank=True, help_text='Notes to organizers. These are public. For private notes, please use the button for private messages on the detail page of this AK (after creation/editing).', verbose_name='Organizational Notes'),
),
]
# Generated by Django 3.2.16 on 2022-10-12 22:51
from django.db import migrations, models
import timezone_field.fields
class Migration(migrations.Migration):
dependencies = [
('AKModel', '0051_ak_notes_help_text'),
]
operations = [
migrations.AlterModelOptions(
name='historicalak',
options={'get_latest_by': ('history_date', 'history_id'), 'ordering': ('-history_date', '-history_id'), 'verbose_name': 'historical AK', 'verbose_name_plural': 'historical AKs'},
),
migrations.AlterField(
model_name='event',
name='timezone',
field=timezone_field.fields.TimeZoneField(choices=[('Pacific/Apia', 'GMT-11:00 Pacific/Apia'), ('Pacific/Fakaofo', 'GMT-11:00 Pacific/Fakaofo'), ('Pacific/Midway', 'GMT-11:00 Pacific/Midway'), ('Pacific/Niue', 'GMT-11:00 Pacific/Niue'), ('Pacific/Pago_Pago', 'GMT-11:00 Pacific/Pago Pago'), ('America/Adak', 'GMT-10:00 America/Adak'), ('Pacific/Honolulu', 'GMT-10:00 Pacific/Honolulu'), ('Pacific/Rarotonga', 'GMT-10:00 Pacific/Rarotonga'), ('Pacific/Tahiti', 'GMT-10:00 Pacific/Tahiti'), ('US/Hawaii', 'GMT-10:00 US/Hawaii'), ('Pacific/Marquesas', 'GMT-09:00 Pacific/Marquesas'), ('America/Anchorage', 'GMT-09:00 America/Anchorage'), ('America/Juneau', 'GMT-09:00 America/Juneau'), ('America/Nome', 'GMT-09:00 America/Nome'), ('America/Sitka', 'GMT-09:00 America/Sitka'), ('America/Yakutat', 'GMT-09:00 America/Yakutat'), ('Pacific/Gambier', 'GMT-09:00 Pacific/Gambier'), ('US/Alaska', 'GMT-09:00 US/Alaska'), ('America/Dawson', 'GMT-08:00 America/Dawson'), ('America/Fort_Nelson', 'GMT-08:00 America/Fort Nelson'), ('America/Los_Angeles', 'GMT-08:00 America/Los Angeles'), ('America/Metlakatla', 'GMT-08:00 America/Metlakatla'), ('America/Tijuana', 'GMT-08:00 America/Tijuana'), ('America/Vancouver', 'GMT-08:00 America/Vancouver'), ('America/Whitehorse', 'GMT-08:00 America/Whitehorse'), ('Canada/Pacific', 'GMT-08:00 Canada/Pacific'), ('Pacific/Pitcairn', 'GMT-08:00 Pacific/Pitcairn'), ('US/Pacific', 'GMT-08:00 US/Pacific'), ('America/Bahia_Banderas', 'GMT-07:00 America/Bahia Banderas'), ('America/Boise', 'GMT-07:00 America/Boise'), ('America/Chihuahua', 'GMT-07:00 America/Chihuahua'), ('America/Creston', 'GMT-07:00 America/Creston'), ('America/Dawson_Creek', 'GMT-07:00 America/Dawson Creek'), ('America/Denver', 'GMT-07:00 America/Denver'), ('America/Edmonton', 'GMT-07:00 America/Edmonton'), ('America/Hermosillo', 'GMT-07:00 America/Hermosillo'), ('America/Inuvik', 'GMT-07:00 America/Inuvik'), ('America/Mazatlan', 'GMT-07:00 America/Mazatlan'), ('America/North_Dakota/Beulah', 'GMT-07:00 America/North Dakota/Beulah'), ('America/North_Dakota/New_Salem', 'GMT-07:00 America/North Dakota/New Salem'), ('America/Ojinaga', 'GMT-07:00 America/Ojinaga'), ('America/Phoenix', 'GMT-07:00 America/Phoenix'), ('America/Yellowknife', 'GMT-07:00 America/Yellowknife'), ('Canada/Mountain', 'GMT-07:00 Canada/Mountain'), ('US/Arizona', 'GMT-07:00 US/Arizona'), ('US/Mountain', 'GMT-07:00 US/Mountain'), ('America/Belize', 'GMT-06:00 America/Belize'), ('America/Cambridge_Bay', 'GMT-06:00 America/Cambridge Bay'), ('America/Cancun', 'GMT-06:00 America/Cancun'), ('America/Chicago', 'GMT-06:00 America/Chicago'), ('America/Costa_Rica', 'GMT-06:00 America/Costa Rica'), ('America/El_Salvador', 'GMT-06:00 America/El Salvador'), ('America/Guatemala', 'GMT-06:00 America/Guatemala'), ('America/Iqaluit', 'GMT-06:00 America/Iqaluit'), ('America/Kentucky/Monticello', 'GMT-06:00 America/Kentucky/Monticello'), ('America/Managua', 'GMT-06:00 America/Managua'), ('America/Matamoros', 'GMT-06:00 America/Matamoros'), ('America/Menominee', 'GMT-06:00 America/Menominee'), ('America/Merida', 'GMT-06:00 America/Merida'), ('America/Mexico_City', 'GMT-06:00 America/Mexico City'), ('America/Monterrey', 'GMT-06:00 America/Monterrey'), ('America/North_Dakota/Center', 'GMT-06:00 America/North Dakota/Center'), ('America/Pangnirtung', 'GMT-06:00 America/Pangnirtung'), ('America/Rainy_River', 'GMT-06:00 America/Rainy River'), ('America/Rankin_Inlet', 'GMT-06:00 America/Rankin Inlet'), ('America/Regina', 'GMT-06:00 America/Regina'), ('America/Resolute', 'GMT-06:00 America/Resolute'), ('America/Swift_Current', 'GMT-06:00 America/Swift Current'), ('America/Tegucigalpa', 'GMT-06:00 America/Tegucigalpa'), ('America/Winnipeg', 'GMT-06:00 America/Winnipeg'), ('Canada/Central', 'GMT-06:00 Canada/Central'), ('Pacific/Galapagos', 'GMT-06:00 Pacific/Galapagos'), ('US/Central', 'GMT-06:00 US/Central'), ('America/Atikokan', 'GMT-05:00 America/Atikokan'), ('America/Bogota', 'GMT-05:00 America/Bogota'), ('America/Cayman', 'GMT-05:00 America/Cayman'), ('America/Detroit', 'GMT-05:00 America/Detroit'), ('America/Eirunepe', 'GMT-05:00 America/Eirunepe'), ('America/Grand_Turk', 'GMT-05:00 America/Grand Turk'), ('America/Guayaquil', 'GMT-05:00 America/Guayaquil'), ('America/Havana', 'GMT-05:00 America/Havana'), ('America/Indiana/Indianapolis', 'GMT-05:00 America/Indiana/Indianapolis'), ('America/Indiana/Knox', 'GMT-05:00 America/Indiana/Knox'), ('America/Indiana/Marengo', 'GMT-05:00 America/Indiana/Marengo'), ('America/Indiana/Petersburg', 'GMT-05:00 America/Indiana/Petersburg'), ('America/Indiana/Tell_City', 'GMT-05:00 America/Indiana/Tell City'), ('America/Indiana/Vevay', 'GMT-05:00 America/Indiana/Vevay'), ('America/Indiana/Vincennes', 'GMT-05:00 America/Indiana/Vincennes'), ('America/Indiana/Winamac', 'GMT-05:00 America/Indiana/Winamac'), ('America/Jamaica', 'GMT-05:00 America/Jamaica'), ('America/Kentucky/Louisville', 'GMT-05:00 America/Kentucky/Louisville'), ('America/Lima', 'GMT-05:00 America/Lima'), ('America/Nassau', 'GMT-05:00 America/Nassau'), ('America/New_York', 'GMT-05:00 America/New York'), ('America/Nipigon', 'GMT-05:00 America/Nipigon'), ('America/Panama', 'GMT-05:00 America/Panama'), ('America/Port-au-Prince', 'GMT-05:00 America/Port-au-Prince'), ('America/Rio_Branco', 'GMT-05:00 America/Rio Branco'), ('America/Thunder_Bay', 'GMT-05:00 America/Thunder Bay'), ('America/Toronto', 'GMT-05:00 America/Toronto'), ('Canada/Eastern', 'GMT-05:00 Canada/Eastern'), ('Pacific/Easter', 'GMT-05:00 Pacific/Easter'), ('US/Eastern', 'GMT-05:00 US/Eastern'), ('America/Anguilla', 'GMT-04:00 America/Anguilla'), ('America/Antigua', 'GMT-04:00 America/Antigua'), ('America/Aruba', 'GMT-04:00 America/Aruba'), ('America/Barbados', 'GMT-04:00 America/Barbados'), ('America/Blanc-Sablon', 'GMT-04:00 America/Blanc-Sablon'), ('America/Caracas', 'GMT-04:00 America/Caracas'), ('America/Curacao', 'GMT-04:00 America/Curacao'), ('America/Dominica', 'GMT-04:00 America/Dominica'), ('America/Glace_Bay', 'GMT-04:00 America/Glace Bay'), ('America/Goose_Bay', 'GMT-04:00 America/Goose Bay'), ('America/Grenada', 'GMT-04:00 America/Grenada'), ('America/Guadeloupe', 'GMT-04:00 America/Guadeloupe'), ('America/Guyana', 'GMT-04:00 America/Guyana'), ('America/Halifax', 'GMT-04:00 America/Halifax'), ('America/Kralendijk', 'GMT-04:00 America/Kralendijk'), ('America/La_Paz', 'GMT-04:00 America/La Paz'), ('America/Lower_Princes', 'GMT-04:00 America/Lower Princes'), ('America/Manaus', 'GMT-04:00 America/Manaus'), ('America/Marigot', 'GMT-04:00 America/Marigot'), ('America/Martinique', 'GMT-04:00 America/Martinique'), ('America/Moncton', 'GMT-04:00 America/Moncton'), ('America/Montserrat', 'GMT-04:00 America/Montserrat'), ('America/Port_of_Spain', 'GMT-04:00 America/Port of Spain'), ('America/Porto_Velho', 'GMT-04:00 America/Porto Velho'), ('America/Puerto_Rico', 'GMT-04:00 America/Puerto Rico'), ('America/Santarem', 'GMT-04:00 America/Santarem'), ('America/Santo_Domingo', 'GMT-04:00 America/Santo Domingo'), ('America/St_Barthelemy', 'GMT-04:00 America/St Barthelemy'), ('America/St_Kitts', 'GMT-04:00 America/St Kitts'), ('America/St_Lucia', 'GMT-04:00 America/St Lucia'), ('America/St_Thomas', 'GMT-04:00 America/St Thomas'), ('America/St_Vincent', 'GMT-04:00 America/St Vincent'), ('America/Thule', 'GMT-04:00 America/Thule'), ('America/Tortola', 'GMT-04:00 America/Tortola'), ('Atlantic/Bermuda', 'GMT-04:00 Atlantic/Bermuda'), ('Canada/Atlantic', 'GMT-04:00 Canada/Atlantic'), ('America/St_Johns', 'GMT-03:00 America/St Johns'), ('Canada/Newfoundland', 'GMT-03:00 Canada/Newfoundland'), ('America/Argentina/Buenos_Aires', 'GMT-03:00 America/Argentina/Buenos Aires'), ('America/Argentina/Catamarca', 'GMT-03:00 America/Argentina/Catamarca'), ('America/Argentina/Cordoba', 'GMT-03:00 America/Argentina/Cordoba'), ('America/Argentina/Jujuy', 'GMT-03:00 America/Argentina/Jujuy'), ('America/Argentina/La_Rioja', 'GMT-03:00 America/Argentina/La Rioja'), ('America/Argentina/Mendoza', 'GMT-03:00 America/Argentina/Mendoza'), ('America/Argentina/Rio_Gallegos', 'GMT-03:00 America/Argentina/Rio Gallegos'), ('America/Argentina/Salta', 'GMT-03:00 America/Argentina/Salta'), ('America/Argentina/San_Juan', 'GMT-03:00 America/Argentina/San Juan'), ('America/Argentina/San_Luis', 'GMT-03:00 America/Argentina/San Luis'), ('America/Argentina/Tucuman', 'GMT-03:00 America/Argentina/Tucuman'), ('America/Argentina/Ushuaia', 'GMT-03:00 America/Argentina/Ushuaia'), ('America/Asuncion', 'GMT-03:00 America/Asuncion'), ('America/Belem', 'GMT-03:00 America/Belem'), ('America/Boa_Vista', 'GMT-03:00 America/Boa Vista'), ('America/Campo_Grande', 'GMT-03:00 America/Campo Grande'), ('America/Cayenne', 'GMT-03:00 America/Cayenne'), ('America/Cuiaba', 'GMT-03:00 America/Cuiaba'), ('America/Miquelon', 'GMT-03:00 America/Miquelon'), ('America/Montevideo', 'GMT-03:00 America/Montevideo'), ('America/Nuuk', 'GMT-03:00 America/Nuuk'), ('America/Paramaribo', 'GMT-03:00 America/Paramaribo'), ('America/Punta_Arenas', 'GMT-03:00 America/Punta Arenas'), ('America/Santiago', 'GMT-03:00 America/Santiago'), ('Antarctica/Palmer', 'GMT-03:00 Antarctica/Palmer'), ('Antarctica/Rothera', 'GMT-03:00 Antarctica/Rothera'), ('Atlantic/Stanley', 'GMT-03:00 Atlantic/Stanley'), ('America/Araguaina', 'GMT-02:00 America/Araguaina'), ('America/Bahia', 'GMT-02:00 America/Bahia'), ('America/Fortaleza', 'GMT-02:00 America/Fortaleza'), ('America/Maceio', 'GMT-02:00 America/Maceio'), ('America/Recife', 'GMT-02:00 America/Recife'), ('America/Sao_Paulo', 'GMT-02:00 America/Sao Paulo'), ('Atlantic/South_Georgia', 'GMT-02:00 Atlantic/South Georgia'), ('America/Noronha', 'GMT-01:00 America/Noronha'), ('America/Scoresbysund', 'GMT-01:00 America/Scoresbysund'), ('Atlantic/Azores', 'GMT-01:00 Atlantic/Azores'), ('Atlantic/Cape_Verde', 'GMT-01:00 Atlantic/Cape Verde'), ('Africa/Abidjan', 'GMT+00:00 Africa/Abidjan'), ('Africa/Accra', 'GMT+00:00 Africa/Accra'), ('Africa/Bamako', 'GMT+00:00 Africa/Bamako'), ('Africa/Banjul', 'GMT+00:00 Africa/Banjul'), ('Africa/Bissau', 'GMT+00:00 Africa/Bissau'), ('Africa/Casablanca', 'GMT+00:00 Africa/Casablanca'), ('Africa/Conakry', 'GMT+00:00 Africa/Conakry'), ('Africa/Dakar', 'GMT+00:00 Africa/Dakar'), ('Africa/El_Aaiun', 'GMT+00:00 Africa/El Aaiun'), ('Africa/Freetown', 'GMT+00:00 Africa/Freetown'), ('Africa/Lome', 'GMT+00:00 Africa/Lome'), ('Africa/Monrovia', 'GMT+00:00 Africa/Monrovia'), ('Africa/Nouakchott', 'GMT+00:00 Africa/Nouakchott'), ('Africa/Ouagadougou', 'GMT+00:00 Africa/Ouagadougou'), ('Africa/Sao_Tome', 'GMT+00:00 Africa/Sao Tome'), ('America/Danmarkshavn', 'GMT+00:00 America/Danmarkshavn'), ('Antarctica/Troll', 'GMT+00:00 Antarctica/Troll'), ('Atlantic/Canary', 'GMT+00:00 Atlantic/Canary'), ('Atlantic/Faroe', 'GMT+00:00 Atlantic/Faroe'), ('Atlantic/Madeira', 'GMT+00:00 Atlantic/Madeira'), ('Atlantic/Reykjavik', 'GMT+00:00 Atlantic/Reykjavik'), ('Atlantic/St_Helena', 'GMT+00:00 Atlantic/St Helena'), ('Europe/Dublin', 'GMT+00:00 Europe/Dublin'), ('Europe/Guernsey', 'GMT+00:00 Europe/Guernsey'), ('Europe/Isle_of_Man', 'GMT+00:00 Europe/Isle of Man'), ('Europe/Jersey', 'GMT+00:00 Europe/Jersey'), ('Europe/Lisbon', 'GMT+00:00 Europe/Lisbon'), ('Europe/London', 'GMT+00:00 Europe/London'), ('GMT', 'GMT+00:00 GMT'), ('UTC', 'GMT+00:00 UTC'), ('Africa/Algiers', 'GMT+01:00 Africa/Algiers'), ('Africa/Bangui', 'GMT+01:00 Africa/Bangui'), ('Africa/Brazzaville', 'GMT+01:00 Africa/Brazzaville'), ('Africa/Ceuta', 'GMT+01:00 Africa/Ceuta'), ('Africa/Douala', 'GMT+01:00 Africa/Douala'), ('Africa/Kinshasa', 'GMT+01:00 Africa/Kinshasa'), ('Africa/Lagos', 'GMT+01:00 Africa/Lagos'), ('Africa/Libreville', 'GMT+01:00 Africa/Libreville'), ('Africa/Luanda', 'GMT+01:00 Africa/Luanda'), ('Africa/Malabo', 'GMT+01:00 Africa/Malabo'), ('Africa/Ndjamena', 'GMT+01:00 Africa/Ndjamena'), ('Africa/Niamey', 'GMT+01:00 Africa/Niamey'), ('Africa/Porto-Novo', 'GMT+01:00 Africa/Porto-Novo'), ('Africa/Tunis', 'GMT+01:00 Africa/Tunis'), ('Arctic/Longyearbyen', 'GMT+01:00 Arctic/Longyearbyen'), ('Europe/Amsterdam', 'GMT+01:00 Europe/Amsterdam'), ('Europe/Andorra', 'GMT+01:00 Europe/Andorra'), ('Europe/Belgrade', 'GMT+01:00 Europe/Belgrade'), ('Europe/Berlin', 'GMT+01:00 Europe/Berlin'), ('Europe/Bratislava', 'GMT+01:00 Europe/Bratislava'), ('Europe/Brussels', 'GMT+01:00 Europe/Brussels'), ('Europe/Budapest', 'GMT+01:00 Europe/Budapest'), ('Europe/Busingen', 'GMT+01:00 Europe/Busingen'), ('Europe/Copenhagen', 'GMT+01:00 Europe/Copenhagen'), ('Europe/Gibraltar', 'GMT+01:00 Europe/Gibraltar'), ('Europe/Ljubljana', 'GMT+01:00 Europe/Ljubljana'), ('Europe/Luxembourg', 'GMT+01:00 Europe/Luxembourg'), ('Europe/Madrid', 'GMT+01:00 Europe/Madrid'), ('Europe/Malta', 'GMT+01:00 Europe/Malta'), ('Europe/Monaco', 'GMT+01:00 Europe/Monaco'), ('Europe/Oslo', 'GMT+01:00 Europe/Oslo'), ('Europe/Paris', 'GMT+01:00 Europe/Paris'), ('Europe/Podgorica', 'GMT+01:00 Europe/Podgorica'), ('Europe/Prague', 'GMT+01:00 Europe/Prague'), ('Europe/Rome', 'GMT+01:00 Europe/Rome'), ('Europe/San_Marino', 'GMT+01:00 Europe/San Marino'), ('Europe/Sarajevo', 'GMT+01:00 Europe/Sarajevo'), ('Europe/Skopje', 'GMT+01:00 Europe/Skopje'), ('Europe/Stockholm', 'GMT+01:00 Europe/Stockholm'), ('Europe/Tirane', 'GMT+01:00 Europe/Tirane'), ('Europe/Vaduz', 'GMT+01:00 Europe/Vaduz'), ('Europe/Vatican', 'GMT+01:00 Europe/Vatican'), ('Europe/Vienna', 'GMT+01:00 Europe/Vienna'), ('Europe/Warsaw', 'GMT+01:00 Europe/Warsaw'), ('Europe/Zagreb', 'GMT+01:00 Europe/Zagreb'), ('Europe/Zurich', 'GMT+01:00 Europe/Zurich'), ('Africa/Blantyre', 'GMT+02:00 Africa/Blantyre'), ('Africa/Bujumbura', 'GMT+02:00 Africa/Bujumbura'), ('Africa/Cairo', 'GMT+02:00 Africa/Cairo'), ('Africa/Gaborone', 'GMT+02:00 Africa/Gaborone'), ('Africa/Harare', 'GMT+02:00 Africa/Harare'), ('Africa/Johannesburg', 'GMT+02:00 Africa/Johannesburg'), ('Africa/Juba', 'GMT+02:00 Africa/Juba'), ('Africa/Khartoum', 'GMT+02:00 Africa/Khartoum'), ('Africa/Kigali', 'GMT+02:00 Africa/Kigali'), ('Africa/Lubumbashi', 'GMT+02:00 Africa/Lubumbashi'), ('Africa/Lusaka', 'GMT+02:00 Africa/Lusaka'), ('Africa/Maputo', 'GMT+02:00 Africa/Maputo'), ('Africa/Maseru', 'GMT+02:00 Africa/Maseru'), ('Africa/Mbabane', 'GMT+02:00 Africa/Mbabane'), ('Africa/Tripoli', 'GMT+02:00 Africa/Tripoli'), ('Africa/Windhoek', 'GMT+02:00 Africa/Windhoek'), ('Asia/Amman', 'GMT+02:00 Asia/Amman'), ('Asia/Beirut', 'GMT+02:00 Asia/Beirut'), ('Asia/Damascus', 'GMT+02:00 Asia/Damascus'), ('Asia/Famagusta', 'GMT+02:00 Asia/Famagusta'), ('Asia/Gaza', 'GMT+02:00 Asia/Gaza'), ('Asia/Hebron', 'GMT+02:00 Asia/Hebron'), ('Asia/Jerusalem', 'GMT+02:00 Asia/Jerusalem'), ('Asia/Nicosia', 'GMT+02:00 Asia/Nicosia'), ('Europe/Athens', 'GMT+02:00 Europe/Athens'), ('Europe/Bucharest', 'GMT+02:00 Europe/Bucharest'), ('Europe/Chisinau', 'GMT+02:00 Europe/Chisinau'), ('Europe/Helsinki', 'GMT+02:00 Europe/Helsinki'), ('Europe/Istanbul', 'GMT+02:00 Europe/Istanbul'), ('Europe/Kaliningrad', 'GMT+02:00 Europe/Kaliningrad'), ('Europe/Kyiv', 'GMT+02:00 Europe/Kyiv'), ('Europe/Mariehamn', 'GMT+02:00 Europe/Mariehamn'), ('Europe/Minsk', 'GMT+02:00 Europe/Minsk'), ('Europe/Riga', 'GMT+02:00 Europe/Riga'), ('Europe/Simferopol', 'GMT+02:00 Europe/Simferopol'), ('Europe/Sofia', 'GMT+02:00 Europe/Sofia'), ('Europe/Tallinn', 'GMT+02:00 Europe/Tallinn'), ('Europe/Vilnius', 'GMT+02:00 Europe/Vilnius'), ('Africa/Addis_Ababa', 'GMT+03:00 Africa/Addis Ababa'), ('Africa/Asmara', 'GMT+03:00 Africa/Asmara'), ('Africa/Dar_es_Salaam', 'GMT+03:00 Africa/Dar es Salaam'), ('Africa/Djibouti', 'GMT+03:00 Africa/Djibouti'), ('Africa/Kampala', 'GMT+03:00 Africa/Kampala'), ('Africa/Mogadishu', 'GMT+03:00 Africa/Mogadishu'), ('Africa/Nairobi', 'GMT+03:00 Africa/Nairobi'), ('Antarctica/Syowa', 'GMT+03:00 Antarctica/Syowa'), ('Asia/Aden', 'GMT+03:00 Asia/Aden'), ('Asia/Baghdad', 'GMT+03:00 Asia/Baghdad'), ('Asia/Bahrain', 'GMT+03:00 Asia/Bahrain'), ('Asia/Kuwait', 'GMT+03:00 Asia/Kuwait'), ('Asia/Qatar', 'GMT+03:00 Asia/Qatar'), ('Asia/Riyadh', 'GMT+03:00 Asia/Riyadh'), ('Europe/Astrakhan', 'GMT+03:00 Europe/Astrakhan'), ('Europe/Kirov', 'GMT+03:00 Europe/Kirov'), ('Europe/Moscow', 'GMT+03:00 Europe/Moscow'), ('Europe/Saratov', 'GMT+03:00 Europe/Saratov'), ('Europe/Ulyanovsk', 'GMT+03:00 Europe/Ulyanovsk'), ('Europe/Volgograd', 'GMT+03:00 Europe/Volgograd'), ('Indian/Antananarivo', 'GMT+03:00 Indian/Antananarivo'), ('Indian/Comoro', 'GMT+03:00 Indian/Comoro'), ('Indian/Mayotte', 'GMT+03:00 Indian/Mayotte'), ('Asia/Tehran', 'GMT+03:00 Asia/Tehran'), ('Asia/Aqtau', 'GMT+04:00 Asia/Aqtau'), ('Asia/Atyrau', 'GMT+04:00 Asia/Atyrau'), ('Asia/Baku', 'GMT+04:00 Asia/Baku'), ('Asia/Dubai', 'GMT+04:00 Asia/Dubai'), ('Asia/Muscat', 'GMT+04:00 Asia/Muscat'), ('Asia/Oral', 'GMT+04:00 Asia/Oral'), ('Asia/Tbilisi', 'GMT+04:00 Asia/Tbilisi'), ('Asia/Yerevan', 'GMT+04:00 Asia/Yerevan'), ('Europe/Samara', 'GMT+04:00 Europe/Samara'), ('Indian/Mahe', 'GMT+04:00 Indian/Mahe'), ('Indian/Mauritius', 'GMT+04:00 Indian/Mauritius'), ('Indian/Reunion', 'GMT+04:00 Indian/Reunion'), ('Asia/Kabul', 'GMT+04:00 Asia/Kabul'), ('Asia/Aqtobe', 'GMT+05:00 Asia/Aqtobe'), ('Asia/Ashgabat', 'GMT+05:00 Asia/Ashgabat'), ('Asia/Bishkek', 'GMT+05:00 Asia/Bishkek'), ('Asia/Dushanbe', 'GMT+05:00 Asia/Dushanbe'), ('Asia/Karachi', 'GMT+05:00 Asia/Karachi'), ('Asia/Qostanay', 'GMT+05:00 Asia/Qostanay'), ('Asia/Qyzylorda', 'GMT+05:00 Asia/Qyzylorda'), ('Asia/Samarkand', 'GMT+05:00 Asia/Samarkand'), ('Asia/Tashkent', 'GMT+05:00 Asia/Tashkent'), ('Asia/Yekaterinburg', 'GMT+05:00 Asia/Yekaterinburg'), ('Indian/Kerguelen', 'GMT+05:00 Indian/Kerguelen'), ('Indian/Maldives', 'GMT+05:00 Indian/Maldives'), ('Asia/Kolkata', 'GMT+05:00 Asia/Kolkata'), ('Asia/Kathmandu', 'GMT+05:00 Asia/Kathmandu'), ('Antarctica/Mawson', 'GMT+06:00 Antarctica/Mawson'), ('Antarctica/Vostok', 'GMT+06:00 Antarctica/Vostok'), ('Asia/Almaty', 'GMT+06:00 Asia/Almaty'), ('Asia/Barnaul', 'GMT+06:00 Asia/Barnaul'), ('Asia/Colombo', 'GMT+06:00 Asia/Colombo'), ('Asia/Dhaka', 'GMT+06:00 Asia/Dhaka'), ('Asia/Novosibirsk', 'GMT+06:00 Asia/Novosibirsk'), ('Asia/Omsk', 'GMT+06:00 Asia/Omsk'), ('Asia/Thimphu', 'GMT+06:00 Asia/Thimphu'), ('Asia/Urumqi', 'GMT+06:00 Asia/Urumqi'), ('Indian/Chagos', 'GMT+06:00 Indian/Chagos'), ('Asia/Yangon', 'GMT+06:00 Asia/Yangon'), ('Indian/Cocos', 'GMT+06:00 Indian/Cocos'), ('Antarctica/Davis', 'GMT+07:00 Antarctica/Davis'), ('Asia/Bangkok', 'GMT+07:00 Asia/Bangkok'), ('Asia/Ho_Chi_Minh', 'GMT+07:00 Asia/Ho Chi Minh'), ('Asia/Hovd', 'GMT+07:00 Asia/Hovd'), ('Asia/Jakarta', 'GMT+07:00 Asia/Jakarta'), ('Asia/Krasnoyarsk', 'GMT+07:00 Asia/Krasnoyarsk'), ('Asia/Novokuznetsk', 'GMT+07:00 Asia/Novokuznetsk'), ('Asia/Phnom_Penh', 'GMT+07:00 Asia/Phnom Penh'), ('Asia/Pontianak', 'GMT+07:00 Asia/Pontianak'), ('Asia/Tomsk', 'GMT+07:00 Asia/Tomsk'), ('Asia/Vientiane', 'GMT+07:00 Asia/Vientiane'), ('Indian/Christmas', 'GMT+07:00 Indian/Christmas'), ('Antarctica/Casey', 'GMT+08:00 Antarctica/Casey'), ('Asia/Brunei', 'GMT+08:00 Asia/Brunei'), ('Asia/Dili', 'GMT+08:00 Asia/Dili'), ('Asia/Hong_Kong', 'GMT+08:00 Asia/Hong Kong'), ('Asia/Irkutsk', 'GMT+08:00 Asia/Irkutsk'), ('Asia/Kuala_Lumpur', 'GMT+08:00 Asia/Kuala Lumpur'), ('Asia/Kuching', 'GMT+08:00 Asia/Kuching'), ('Asia/Macau', 'GMT+08:00 Asia/Macau'), ('Asia/Makassar', 'GMT+08:00 Asia/Makassar'), ('Asia/Manila', 'GMT+08:00 Asia/Manila'), ('Asia/Shanghai', 'GMT+08:00 Asia/Shanghai'), ('Asia/Singapore', 'GMT+08:00 Asia/Singapore'), ('Asia/Taipei', 'GMT+08:00 Asia/Taipei'), ('Asia/Ulaanbaatar', 'GMT+08:00 Asia/Ulaanbaatar'), ('Australia/Perth', 'GMT+08:00 Australia/Perth'), ('Australia/Eucla', 'GMT+08:00 Australia/Eucla'), ('Asia/Chita', 'GMT+09:00 Asia/Chita'), ('Asia/Choibalsan', 'GMT+09:00 Asia/Choibalsan'), ('Asia/Jayapura', 'GMT+09:00 Asia/Jayapura'), ('Asia/Khandyga', 'GMT+09:00 Asia/Khandyga'), ('Asia/Pyongyang', 'GMT+09:00 Asia/Pyongyang'), ('Asia/Seoul', 'GMT+09:00 Asia/Seoul'), ('Asia/Tokyo', 'GMT+09:00 Asia/Tokyo'), ('Asia/Yakutsk', 'GMT+09:00 Asia/Yakutsk'), ('Pacific/Palau', 'GMT+09:00 Pacific/Palau'), ('Australia/Darwin', 'GMT+09:00 Australia/Darwin'), ('Antarctica/DumontDUrville', 'GMT+10:00 Antarctica/DumontDUrville'), ('Asia/Sakhalin', 'GMT+10:00 Asia/Sakhalin'), ('Asia/Vladivostok', 'GMT+10:00 Asia/Vladivostok'), ('Australia/Brisbane', 'GMT+10:00 Australia/Brisbane'), ('Australia/Lindeman', 'GMT+10:00 Australia/Lindeman'), ('Pacific/Bougainville', 'GMT+10:00 Pacific/Bougainville'), ('Pacific/Chuuk', 'GMT+10:00 Pacific/Chuuk'), ('Pacific/Guam', 'GMT+10:00 Pacific/Guam'), ('Pacific/Port_Moresby', 'GMT+10:00 Pacific/Port Moresby'), ('Pacific/Saipan', 'GMT+10:00 Pacific/Saipan'), ('Australia/Adelaide', 'GMT+10:00 Australia/Adelaide'), ('Australia/Broken_Hill', 'GMT+10:00 Australia/Broken Hill'), ('Antarctica/Macquarie', 'GMT+11:00 Antarctica/Macquarie'), ('Asia/Magadan', 'GMT+11:00 Asia/Magadan'), ('Asia/Srednekolymsk', 'GMT+11:00 Asia/Srednekolymsk'), ('Asia/Ust-Nera', 'GMT+11:00 Asia/Ust-Nera'), ('Australia/Hobart', 'GMT+11:00 Australia/Hobart'), ('Australia/Lord_Howe', 'GMT+11:00 Australia/Lord Howe'), ('Australia/Melbourne', 'GMT+11:00 Australia/Melbourne'), ('Australia/Sydney', 'GMT+11:00 Australia/Sydney'), ('Pacific/Efate', 'GMT+11:00 Pacific/Efate'), ('Pacific/Guadalcanal', 'GMT+11:00 Pacific/Guadalcanal'), ('Pacific/Kosrae', 'GMT+11:00 Pacific/Kosrae'), ('Pacific/Noumea', 'GMT+11:00 Pacific/Noumea'), ('Pacific/Pohnpei', 'GMT+11:00 Pacific/Pohnpei'), ('Pacific/Norfolk', 'GMT+11:00 Pacific/Norfolk'), ('Asia/Anadyr', 'GMT+12:00 Asia/Anadyr'), ('Asia/Kamchatka', 'GMT+12:00 Asia/Kamchatka'), ('Pacific/Funafuti', 'GMT+12:00 Pacific/Funafuti'), ('Pacific/Kwajalein', 'GMT+12:00 Pacific/Kwajalein'), ('Pacific/Majuro', 'GMT+12:00 Pacific/Majuro'), ('Pacific/Nauru', 'GMT+12:00 Pacific/Nauru'), ('Pacific/Tarawa', 'GMT+12:00 Pacific/Tarawa'), ('Pacific/Wake', 'GMT+12:00 Pacific/Wake'), ('Pacific/Wallis', 'GMT+12:00 Pacific/Wallis'), ('Antarctica/McMurdo', 'GMT+13:00 Antarctica/McMurdo'), ('Pacific/Auckland', 'GMT+13:00 Pacific/Auckland'), ('Pacific/Fiji', 'GMT+13:00 Pacific/Fiji'), ('Pacific/Kanton', 'GMT+13:00 Pacific/Kanton'), ('Pacific/Chatham', 'GMT+13:00 Pacific/Chatham'), ('Pacific/Kiritimati', 'GMT+14:00 Pacific/Kiritimati'), ('Pacific/Tongatapu', 'GMT+14:00 Pacific/Tongatapu')], default='Europe/Berlin', help_text='Time Zone where this event takes place in', verbose_name='Time Zone'),
),
migrations.AlterField(
model_name='historicalak',
name='history_date',
field=models.DateTimeField(db_index=True),
),
]
# Generated by Django 3.2.16 on 2022-10-15 10:18
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('AKModel', '0052_history_upgrade'),
]
operations = [
migrations.AddField(
model_name='event',
name='plan_published_at',
field=models.DateTimeField(blank=True, help_text='Timestamp at which the plan was published', null=True, verbose_name='Plan published at'),
),
]
# Generated by Django 3.2.16 on 2022-10-22 12:18
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('AKModel', '0053_plan_published_at'),
]
operations = [
migrations.CreateModel(
name='DefaultSlot',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('start', models.DateTimeField(help_text='Time and date the slot begins', verbose_name='Slot Begin')),
('end', models.DateTimeField(help_text='Time and date the slot ends', verbose_name='Slot End')),
('event', models.ForeignKey(help_text='Associated event', on_delete=django.db.models.deletion.CASCADE, to='AKModel.event', verbose_name='Event')),
('primary_categories', models.ManyToManyField(blank=True, help_text='Categories that should be assigned to this slot primarily', to='AKModel.AKCategory', verbose_name='Primary categories')),
],
options={
'verbose_name': 'Default Slot',
'verbose_name_plural': 'Default Slots',
'ordering': ['-start'],
},
),
]
# Generated by Django 3.2.16 on 2022-12-26 22:38
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('AKModel', '0054_default_slots'),
]
operations = [
migrations.AddField(
model_name='ak',
name='include_in_export',
field=models.BooleanField(default=True, help_text='Include AK in wiki export?', verbose_name='Export?'),
),
]
# Generated by Django 3.2.16 on 2023-01-01 18:47
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('AKModel', '0055_ak_export'),
]
operations = [
migrations.RemoveField(
model_name='ak',
name='tags',
),
migrations.DeleteModel(
name='AKTag',
),
]
# Generated by Django 4.1.5 on 2023-01-03 17:04
from django.db import migrations
import timezone_field.fields
class Migration(migrations.Migration):
dependencies = [
('AKModel', '0056_remove_tags'),
]
operations = [
migrations.AlterField(
model_name='event',
name='timezone',
field=timezone_field.fields.TimeZoneField(choices_display='WITH_GMT_OFFSET', default='Europe/Berlin', help_text='Time Zone where this event takes place in', verbose_name='Time Zone'),
),
]
# Generated by Django 4.1.9 on 2023-05-15 18:47
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('AKModel', '0057_upgrades'),
]
operations = [
migrations.AlterModelOptions(
name='ak',
options={'ordering': ['pk'], 'verbose_name': 'AK', 'verbose_name_plural': 'AKs'},
),
]
# Generated by Django 4.2.11 on 2024-04-21 14:54
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('AKModel', '0058_alter_ak_options'),
]
operations = [
migrations.AlterField(
model_name='event',
name='interest_start',
field=models.DateTimeField(blank=True, help_text='Opening time for expression of interest. When left blank, no interest indication will be possible.', null=True, verbose_name='Interest Window Start'),
),
]
# Generated by Django 4.2.11 on 2024-04-24 21:39
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('AKModel', '0059_interest_default'),
]
operations = [
migrations.AddField(
model_name='akorgamessage',
name='resolved',
field=models.BooleanField(default=False, help_text='This message has been resolved (no further action needed)', verbose_name='Resolved'),
),
]
# 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'),
),
]
# Create your models here.
import itertools
from datetime import datetime, timedelta
from django.core.validators import RegexValidator
from django.apps import apps
from django.db import models
from django.db.models import Count
from django.urls import reverse_lazy
from django.utils import timezone
from django.utils.text import slugify
from django.utils.translation import gettext_lazy as _
from simple_history.models import HistoricalRecords
from timezone_field import TimeZoneField
# Custom validators to be used for some of the fields
# Prevent inclusion of the quotation marks ' " ´ `
# This may be necessary to prevent javascript issues
no_quotation_marks_validator = RegexValidator(regex=r"['\"´`]+", inverse_match=True,
message=_('May not contain quotation marks'))
# Enforce that the field contains of at least one letter or digit (and not just special characters
# This prevents issues when autogenerating slugs from that field
slugable_validator = RegexValidator(regex=r"[\w\s]+", message=_('Must contain at least one letter or digit'))
class Event(models.Model):
""" An event supplies the frame for all Aks.
"""
An event supplies the frame for all Aks.
"""
name = models.CharField(max_length=64, unique=True, verbose_name=_('Name'),
help_text=_('Name or iteration of the event'))
slug = models.SlugField(max_length=32, unique=True, verbose_name=_('Short Form'),
help_text=_('Short name of letters/numbers/dots/dashes/underscores used in URLs.'))
place = models.CharField(max_length=128, blank=True, verbose_name=_('Place'),
help_text=_('City etc. the event takes place in'))
timezone = TimeZoneField(default='Europe/Berlin', blank=False, choices_display="WITH_GMT_OFFSET",
verbose_name=_('Time Zone'), help_text=_('Time Zone where this event takes place in'))
start = models.DateTimeField(verbose_name=_('Start'), help_text=_('Time the event begins'))
end = models.DateTimeField(verbose_name=_('End'), help_text=_('Time the event ends'))
place = models.CharField(max_length=128, verbose_name=_('Place'), help_text=_('City etc. the event takes place in'))
reso_deadline = models.DateTimeField(verbose_name=_('Resolution Deadline'), blank=True, null=True,
help_text=_('When should AKs with intention to submit a resolution be done?'))
interest_start = models.DateTimeField(verbose_name=_('Interest Window Start'), blank=True, null=True,
help_text=
_('Opening time for expression of interest. When left blank, no interest '
'indication will be possible.'))
interest_end = models.DateTimeField(verbose_name=_('Interest Window End'), blank=True, null=True,
help_text=_('Closing time for expression of interest.'))
public = models.BooleanField(verbose_name=_('Public event'), default=True,
help_text=_('Show this event on overview page.'))
active = models.BooleanField(verbose_name=_('Active State'), help_text=_('Marks currently active events'))
plan_hidden = models.BooleanField(verbose_name=_('Plan Hidden'), help_text=_('Hides plan for non-staff users'),
default=True)
plan_published_at = models.DateTimeField(verbose_name=_('Plan published at'), blank=True, null=True,
help_text=_('Timestamp at which the plan was published'))
base_url = models.URLField(verbose_name=_("Base URL"), help_text=_("Prefix for wiki link construction"), blank=True)
wiki_export_template_name = models.CharField(verbose_name=_("Wiki Export Template Name"), blank=True, max_length=50)
default_slot = models.DecimalField(max_digits=4, decimal_places=2, default=2, verbose_name=_('Default Slot Length'),
help_text=_('Default length in hours that is assumed for AKs in this event.'))
contact_email = models.EmailField(verbose_name=_("Contact email address"), blank=True,
help_text=_("An email address that is displayed on every page "
"and can be used for all kinds of questions"))
class Meta:
verbose_name = _('Event')
verbose_name_plural = _('Events')
ordering = ['name']
ordering = ['-start']
def __str__(self):
return self.name
@staticmethod
def get_by_slug(slug):
"""
Get event by its slug
:param slug: slug of the event
:return: event identified by the slug
:rtype: Event
"""
return Event.objects.get(slug=slug)
@staticmethod
def get_next_active():
"""
Get first active event taking place
:return: matching event (if any) or None
:rtype: Event
"""
event = Event.objects.filter(active=True).order_by('start').first()
# No active event? Return the next event taking place
if event is None:
event = Event.objects.order_by('start').filter(start__gt=datetime.now().astimezone()).first()
return event
def get_categories_with_aks(self, wishes_seperately=False,
filter_func=lambda ak: True, hide_empty_categories=False):
"""
Get AKCategories as well as a list of AKs belonging to the category for this event
:param wishes_seperately: Return wishes as individual list.
:type wishes_seperately: bool
:param filter_func: Optional filter predicate, only include AK in list if filter returns True
:type filter_func: (AK)->bool
:return: list of category-AK-list-tuples, optionally the additional list of AK wishes
:rtype: list[(AKCategory, list[AK])] [, list[AK]]
"""
categories = self.akcategory_set.select_related('event').all()
categories_with_aks = []
ak_wishes = []
# Fill lists by iterating
# A different behavior is needed depending on whether wishes should show up inside their categories
# or as a separate category
def _get_category_aks(category):
"""
Get all AKs belonging to a category
Use joining and prefetching to reduce the number of necessary SQL queries
:param category: category the AKs should belong to
:return: QuerySet over AKs
:return: QuerySet[AK]
"""
return category.ak_set.select_related('event').prefetch_related('owners', 'akslot_set').all()
if wishes_seperately:
for category in categories:
ak_list = []
for ak in _get_category_aks(category):
if filter_func(ak):
if ak.wish:
ak_wishes.append(ak)
else:
ak_list.append(ak)
if not hide_empty_categories or len(ak_list) > 0:
categories_with_aks.append((category, ak_list))
return categories_with_aks, ak_wishes
for category in categories:
ak_list = []
for ak in _get_category_aks(category):
if filter_func(ak):
ak_list.append(ak)
if not hide_empty_categories or len(ak_list) > 0:
categories_with_aks.append((category, ak_list))
return categories_with_aks
def get_unscheduled_wish_slots(self):
"""
Get all slots of wishes that are currently not scheduled
:return: queryset of theses slots
:rtype: QuerySet[AKSlot]
"""
return self.akslot_set.filter(start__isnull=True).annotate(Count('ak__owners')).filter(ak__owners__count=0)
def get_aks_without_availabilities(self):
"""
Gt all AKs that don't have any availability at all
:return: generator over these AKs
:rtype: Generator[AK]
"""
return (self.ak_set
.annotate(Count('availabilities', distinct=True))
.annotate(Count('owners', distinct=True))
.filter(availabilities__count=0, owners__count__gt=0)
)
class AKOwner(models.Model):
""" An AKOwner describes the person organizing/holding an AK.
"""
name = models.CharField(max_length=256, verbose_name=_('Nickname'), help_text=_('Name to identify an AK owner by'))
email = models.EmailField(max_length=128, blank=True, verbose_name=_('E-Mail Address'), help_text=_('Contact mail'))
name = models.CharField(max_length=64, verbose_name=_('Nickname'),
validators=[no_quotation_marks_validator, slugable_validator],
help_text=_('Name to identify an AK owner by'))
slug = models.SlugField(max_length=64, blank=True, verbose_name=_('Slug'), help_text=_('Slug for URL generation'))
institution = models.CharField(max_length=128, blank=True, verbose_name=_('Institution'), help_text=_('Uni etc.'))
link = models.URLField(blank=True, verbose_name=_('Web Link'), help_text=_('Link to Homepage'))
......@@ -35,50 +191,120 @@ class AKOwner(models.Model):
verbose_name = _('AK Owner')
verbose_name_plural = _('AK Owners')
ordering = ['name']
unique_together = [['name', 'institution']]
class AKType(models.Model):
""" An AKType describes the characteristics of an AK, e.g. content vs. recreational.
unique_together = [['event', 'name', 'institution'], ['event', 'slug']]
def __str__(self):
if self.institution:
return f"{self.name} ({self.institution})"
return self.name
def _generate_slug(self):
"""
Auto-generate a slug for an owner
This will start with a very simple slug (the name truncated to a maximum length) and then gradually produce
more complicated slugs when the previous candidates are already used
:return: the slug
:rtype: str
"""
max_length = self._meta.get_field('slug').max_length
# Try name alone (truncated if necessary)
slug_candidate = slugify(self.name)[:max_length]
if not AKOwner.objects.filter(event=self.event, slug=slug_candidate).exists():
self.slug = slug_candidate
return
# Try name and institution separated by '_' (truncated if necessary)
slug_candidate = slugify(slug_candidate + '_' + self.institution)[:max_length]
if not AKOwner.objects.filter(event=self.event, slug=slug_candidate).exists():
self.slug = slug_candidate
return
# Try name + institution + an incrementing digit
for i in itertools.count(1):
if not AKOwner.objects.filter(event=self.event, slug=slug_candidate).exists():
break
digits = len(str(i))
slug_candidate = f'{slug_candidate[:-(digits + 1)]}-{i}'
self.slug = slug_candidate
def save(self, *args, **kwargs):
if not self.slug:
self._generate_slug()
super().save(*args, **kwargs)
@staticmethod
def get_by_slug(event, slug):
"""
Get owner by slug
Will be identified by the combination of event slug and owner slug which is unique
:param event: event
:param slug: slug of the owner
:return: owner identified by slugs
:rtype: AKOwner
"""
return AKOwner.objects.get(event=event, slug=slug)
class AKCategory(models.Model):
""" An AKCategory describes the characteristics of an AK, e.g. content vs. recreational.
"""
name = models.CharField(max_length=64, unique=True, verbose_name=_('Name'), help_text=_('Name of the AK Type'))
name = models.CharField(max_length=64, verbose_name=_('Name'), help_text=_('Name of the AK Category'))
color = models.CharField(max_length=7, blank=True, verbose_name=_('Color'), help_text=_('Color for displaying'))
description = models.TextField(blank=True, verbose_name=_("Description"),
help_text=_("Short description of this AK Category"))
present_by_default = models.BooleanField(blank=True, default=True, verbose_name=_("Present by default"),
help_text=_("Present AKs of this category by default if AK owner did not "
"specify whether this AK should be presented?"))
# TODO model availability
event = models.ForeignKey(to=Event, on_delete=models.CASCADE, verbose_name=_('Event'),
help_text=_('Associated event'))
class Meta:
verbose_name = _('AK Type')
verbose_name_plural = _('AK Types')
verbose_name = _('AK Category')
verbose_name_plural = _('AK Categories')
ordering = ['name']
unique_together = ['event', 'name']
def __str__(self):
return self.name
class AKTrack(models.Model):
""" An AKTrack describes a set of semantically related AKs.
"""
name = models.CharField(max_length=64, unique=True, verbose_name=_('Name'), help_text=_('Name of the AK Track'))
name = models.CharField(max_length=64, verbose_name=_('Name'), help_text=_('Name of the AK Track'))
color = models.CharField(max_length=7, blank=True, verbose_name=_('Color'), help_text=_('Color for displaying'))
event = models.ForeignKey(to=Event, on_delete=models.CASCADE, verbose_name=_('Event'),
help_text=_('Associated event'))
class Meta:
verbose_name = _('AK Track')
verbose_name_plural = _('AK Tracks')
ordering = ['name']
unique_together = ['event', 'name']
def __str__(self):
return self.name
class AKTag(models.Model):
""" An AKTag is a keyword given to an AK by (one of) its owner(s).
"""
name = models.CharField(max_length=64, unique=True, verbose_name=_('Name'), help_text=_('Name of the AK Tag'))
class Meta:
verbose_name = _('AK Tag')
verbose_name_plural = _('AK Tags')
ordering = ['name']
def aks_with_category(self):
"""
Get all AKs that belong to this track with category already joined to prevent additional SQL queries
:return: queryset over the AKs
:rtype: QuerySet[AK]
"""
return self.ak_set.select_related('category').all()
class AKRequirement(models.Model):
""" An AKRequirement describes something needed to hold an AK, e.g. infrastructure.
"""
name = models.CharField(max_length=128, unique=True, verbose_name=_('Name'), help_text=_('Name of the Requirement'))
name = models.CharField(max_length=128, verbose_name=_('Name'), help_text=_('Name of the Requirement'))
event = models.ForeignKey(to=Event, on_delete=models.CASCADE, verbose_name=_('Event'),
help_text=_('Associated event'))
......@@ -87,28 +313,60 @@ class AKRequirement(models.Model):
verbose_name = _('AK Requirement')
verbose_name_plural = _('AK Requirements')
ordering = ['name']
unique_together = ['event', 'name']
def __str__(self):
return self.name
class AKType(models.Model):
""" An AKType allows to associate one or multiple types with an AK, e.g., to better describe the format of that AK
or to which group of people it is addressed. Types are specified per event and are an optional feature.
"""
name = models.CharField(max_length=128, verbose_name=_('Name'), help_text=_('Name describing the type'))
event = models.ForeignKey(to=Event, on_delete=models.CASCADE, verbose_name=_('Event'),
help_text=_('Associated event'))
class Meta:
verbose_name = _('AK Type')
verbose_name_plural = _('AK Types')
ordering = ['name']
unique_together = ['event', 'name']
def __str__(self):
return self.name
class AK(models.Model):
""" An AK is a slot-based activity to be scheduled during an event.
"""
name = models.CharField(max_length=256, unique=True, verbose_name=_('Name'), help_text=_('Name of the AK'))
short_name = models.CharField(max_length=64, unique=True, blank=True, verbose_name=_('Short Name'),
name = models.CharField(max_length=256, verbose_name=_('Name'), help_text=_('Name of the AK'),
validators=[no_quotation_marks_validator, slugable_validator])
short_name = models.CharField(max_length=64, blank=True, verbose_name=_('Short Name'),
validators=[no_quotation_marks_validator],
help_text=_('Name displayed in the schedule'))
description = models.TextField(blank=True, verbose_name=_('Description'), help_text=_('Description of the AK'))
owners = models.ManyToManyField(to=AKOwner, verbose_name=_('Owners'), help_text=_('Those organizing the AK'))
owners = models.ManyToManyField(to=AKOwner, blank=True, verbose_name=_('Owners'),
help_text=_('Those organizing the AK'))
# TODO generate automatically
# Will be automatically generated in save method if not set
link = models.URLField(blank=True, verbose_name=_('Web Link'), help_text=_('Link to wiki page'))
protocol_link = models.URLField(blank=True, verbose_name=_('Protocol Link'), help_text=_('Link to protocol'))
type = models.ForeignKey(to=AKType, on_delete=models.PROTECT, verbose_name=_('Type'), help_text=_('Type of the AK'))
tags = models.ManyToManyField(to=AKTag, blank=True, verbose_name=_('Tags'), help_text=_('Tags provided by owners'))
track = models.ForeignKey(to=AKTrack, on_delete=models.SET_NULL, null=True, verbose_name=_('Track'),
category = models.ForeignKey(to=AKCategory, on_delete=models.PROTECT, verbose_name=_('Category'),
help_text=_('Category of the AK'))
types = models.ManyToManyField(to=AKType, blank=True, verbose_name=_('Types'),
help_text=_("This AK is"))
track = models.ForeignKey(to=AKTrack, blank=True, on_delete=models.SET_NULL, null=True, verbose_name=_('Track'),
help_text=_('Track the AK belongs to'))
reso = models.BooleanField(verbose_name=_('Resolution Intention'), default=False,
help_text=_('Intends to submit a resolution'))
present = models.BooleanField(verbose_name=_("Present this AK"), null=True,
help_text=_("Present results of this AK"))
requirements = models.ManyToManyField(to=AKRequirement, blank=True, verbose_name=_('Requirements'),
help_text=_("AK's Requirements"))
......@@ -116,55 +374,588 @@ 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'))
notes = models.TextField(blank=True, verbose_name=_('Organizational Notes'), help_text=_(
'Notes to organizers. These are public. For private notes, please use the button for private messages '
'on the detail page of this AK (after creation/editing).'))
interest = models.IntegerField(default=-1, verbose_name=_('Interest'), help_text=_('Expected number of people'))
interest_counter = models.IntegerField(default=0, verbose_name=_('Interest Counter'),
help_text=_('People who have indicated interest online'))
event = models.ForeignKey(to=Event, on_delete=models.CASCADE, verbose_name=_('Event'),
help_text=_('Associated event'))
include_in_export = models.BooleanField(default=True, verbose_name=_('Export?'),
help_text=_("Include AK in wiki export?"))
history = HistoricalRecords(excluded_fields=['interest', 'interest_counter', 'include_in_export'])
class Meta:
verbose_name = _('AK')
verbose_name_plural = _('AKs')
unique_together = [['event', 'name'], ['event', 'short_name']]
ordering = ['pk']
def __str__(self):
if self.short_name:
return self.short_name
return self.name
@property
def details(self):
"""
Generate a detailed string representation, e.g., for usage in scheduling
:return: string representation of that AK with all details
:rtype: str
"""
# local import to prevent cyclic import
# pylint: disable=import-outside-toplevel
from AKModel.availability.models import Availability
availabilities = ', \n'.join(f'{a.simplified}' for a in Availability.objects.select_related('event')
.filter(ak=self))
detail_string = f"""{self.name}{" (R)" if self.reso else ""}:
{self.owners_list}
{_('Interest')}: {self.interest}"""
if self.requirements.count() > 0:
detail_string += f"\n{_('Requirements')}: {', '.join(str(r) for r in self.requirements.all())}"
if self.types.count() > 0:
detail_string += f"\n{_('Types')}: {', '.join(str(r) for r in self.types.all())}"
# Find conflicts
# (both directions, those specified for this AK and those were this AK was specified as conflict)
# Deduplicate and order list alphabetically
conflicts = set()
if self.conflicts.count() > 0:
for c in self.conflicts.all():
conflicts.add(str(c))
if self.conflict.count() > 0:
for c in self.conflict.all():
conflicts.add(str(c))
if len(conflicts) > 0:
conflicts = list(conflicts)
conflicts.sort()
detail_string += f"\n{_('Conflicts')}: {', '.join(conflicts)}"
if self.prerequisites.count() > 0:
detail_string += f"\n{_('Prerequisites')}: {', '.join(str(p) for p in self.prerequisites.all())}"
detail_string += f"\n{_('Availabilities')}: \n{availabilities}"
return detail_string
@property
def owners_list(self):
"""
Get a list of stringified representations of all owners
:return: list of owners
:rtype: List[str]
"""
return ", ".join(str(owner) for owner in self.owners.all())
@property
def durations_list(self):
"""
Get a list of stringified representations of all durations of associated slots
:return: list of durations
:rtype: List[str]
"""
return ", ".join(str(slot.duration_simplified) for slot in self.akslot_set.select_related('event').all())
@property
def wish(self):
"""
Is the AK a wish?
:return: true if wish, false if not
:rtype: bool
"""
return self.owners.count() == 0
def increment_interest(self):
"""
Increment the interest counter for this AK by one
without tracking that change to prevent an unreadable and large history
"""
self.interest_counter += 1
self.skip_history_when_saving = True # pylint: disable=attribute-defined-outside-init
self.save()
del self.skip_history_when_saving
@property
def availabilities(self):
"""
Get all availabilities associated to this AK
:return: availabilities
:rtype: QuerySet[Availability]
"""
return "Availability".objects.filter(ak=self)
@property
def edit_url(self):
"""
Get edit URL for this AK
Will link to frontend if AKSubmission is active, otherwise to the edit view for this object in admin interface
:return: URL
:rtype: str
"""
if apps.is_installed("AKSubmission"):
return reverse_lazy('submit:ak_edit', kwargs={'event_slug': self.event.slug, 'pk': self.id})
return reverse_lazy('admin:AKModel_ak_change', kwargs={'object_id': self.id})
@property
def detail_url(self):
"""
Get detail URL for this AK
Will link to frontend if AKSubmission is active, otherwise to the edit view for this object in admin interface
:return: URL
:rtype: str
"""
if apps.is_installed("AKSubmission"):
return reverse_lazy('submit:ak_detail', kwargs={'event_slug': self.event.slug, 'pk': self.id})
return self.edit_url
def save(self, *args, force_insert=False, force_update=False, using=None, update_fields=None):
# Auto-Generate Link if not set yet
if self.link == "":
link = self.event.base_url + self.name.replace(" ", "_")
# Truncate links longer than 200 characters (default length of URL fields in django)
self.link = link[:200]
# Tell Django that we have updated the link field
if update_fields is not None:
update_fields = {"link"}.union(update_fields)
super().save(*args,
force_insert=force_insert, force_update=force_update, using=using, update_fields=update_fields)
class Room(models.Model):
""" A room describes where an AK can be held.
"""
name = models.CharField(max_length=64, verbose_name=_('Name'), help_text=_('Name or number of the room'))
building = models.CharField(max_length=256, verbose_name=_('Building'), help_text=_('Name/number of the building'))
capacity = models.IntegerField(verbose_name=_('Capacity'), help_text=_('Maximum number of people'))
properties = models.ManyToManyField(to=AKRequirement, verbose_name=_('Properties'),
location = models.CharField(max_length=256, blank=True, verbose_name=_('Location'),
help_text=_('Name or number of the location'))
capacity = models.IntegerField(verbose_name=_('Capacity'),
help_text=_('Maximum number of people (-1 for unlimited).'))
properties = models.ManyToManyField(to=AKRequirement, blank=True, 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'))
class Meta:
verbose_name = _('Room')
verbose_name_plural = _('Rooms')
ordering = ['building', 'name']
unique_together = [['name', 'building']]
ordering = ['location', 'name']
unique_together = ['event', 'name', 'location']
@property
def title(self):
"""
Get title of a room, which consists of location and name if location is set, otherwise only the name
:return: title
:rtype: str
"""
if self.location:
return f"{self.location} {self.name}"
return self.name
def __str__(self):
return self.title
class AKSlot(models.Model):
""" An AK Mapping matches an AK to a room during a certain time.
"""
ak = models.ForeignKey(to=AK, on_delete=models.CASCADE, verbose_name=_('AK'), help_text=_('AK being mapped'))
room = models.ForeignKey(to=Room, null=True, on_delete=models.SET_NULL, verbose_name=_('Room'),
room = models.ForeignKey(to=Room, blank=True, null=True, on_delete=models.SET_NULL, verbose_name=_('Room'),
help_text=_('Room the AK will take place in'))
start = models.DateTimeField(verbose_name=_('Slot Begin'), help_text=_('Time and date the slot begins'))
start = models.DateTimeField(verbose_name=_('Slot Begin'), help_text=_('Time and date the slot begins'),
blank=True, null=True)
duration = models.DecimalField(max_digits=4, decimal_places=2, default=2, verbose_name=_('Duration'),
help_text=_('Length in hours'))
fixed = models.BooleanField(default=False, verbose_name=_('Scheduling fixed'),
help_text=_('Length and time of this AK should not be changed'))
event = models.ForeignKey(to=Event, on_delete=models.CASCADE, verbose_name=_('Event'),
help_text=_('Associated event'))
updated = models.DateTimeField(auto_now=True, verbose_name=_("Last update"))
class Meta:
verbose_name = _('AK Slot')
verbose_name_plural = _('AK Slots')
ordering = ['start', 'room']
def __str__(self):
if self.room:
return f"{self.ak} @ {self.start_simplified} in {self.room}"
return f"{self.ak} @ {self.start_simplified}"
@property
def duration_simplified(self):
"""
Display duration of slot in format hours:minutes, e.g. 1.5 -> "1:30"
"""
hours, minutes = divmod(self.duration * 60, 60)
return f"{int(hours)}:{int(minutes):02}"
@property
def start_simplified(self):
"""
Display start time of slot in format weekday + time, e.g. "Fri 14:00"
"""
if self.start is None:
return _("Not scheduled yet")
return self.start.astimezone(self.event.timezone).strftime('%a %H:%M')
@property
def time_simplified(self):
"""
Display start and end time of slot in format weekday + time, e.g. "Fri 14:00 - 15:30" or "Fri 22:00 - Sat 02:00"
"""
if self.start is None:
return _("Not scheduled yet")
start = self.start.astimezone(self.event.timezone)
end = self.end.astimezone(self.event.timezone)
return (f"{start.strftime('%a %H:%M')} - "
f"{end.strftime('%H:%M') if start.day == end.day else end.strftime('%a %H:%M')}")
@property
def end(self):
"""
Retrieve end time of the AK slot
"""
return self.start + timedelta(hours=float(self.duration))
@property
def seconds_since_last_update(self):
"""
Return minutes since last update
:return: minutes since last update
:rtype: float
"""
return (timezone.now() - self.updated).total_seconds()
def overlaps(self, other: "AKSlot"):
"""
Check whether two slots overlap
:param other: second slot to compare with
:return: true if they overlap, false if not:
:rtype: bool
"""
return self.start < other.end <= self.end or self.start <= other.start < self.end
def save(self, *args, force_insert=False, force_update=False, using=None, update_fields=None):
# Make sure duration is not longer than the event
if update_fields is None or 'duration' in update_fields:
event_duration = self.event.end - self.event.start
event_duration_hours = event_duration.days * 24 + event_duration.seconds // 3600
self.duration = min(self.duration, event_duration_hours)
super().save(*args,
force_insert=force_insert, force_update=force_update, using=using, update_fields=update_fields)
class AKOrgaMessage(models.Model):
"""
Model representing confidential messages to the organizers/scheduling people, belonging to a certain AK
"""
ak = models.ForeignKey(to=AK, on_delete=models.CASCADE, verbose_name=_('AK'),
help_text=_('AK this message belongs to'))
text = models.TextField(verbose_name=_("Message text"),
help_text=_("Message to the organizers. This is not publicly visible."))
timestamp = models.DateTimeField(auto_now_add=True)
event = models.ForeignKey(to=Event, on_delete=models.CASCADE, verbose_name=_('Event'),
help_text=_('Associated event'))
resolved = models.BooleanField(verbose_name=_('Resolved'), default=False,
help_text=_('This message has been resolved (no further action needed)'))
class Meta:
verbose_name = _('AK Orga Message')
verbose_name_plural = _('AK Orga Messages')
ordering = ['-timestamp']
def __str__(self):
return f'AK Orga Message for "{self.ak}" @ {self.timestamp}'
class ConstraintViolation(models.Model):
"""
Model to represent any kind of constraint violation
Can have two different severities: violation and warning
The list of possible types is defined in :class:`ViolationType`
Depending on the type, different fields (references to other models) will be filled. Each violation should always
be related to an event and at least on other instance of a causing entity
"""
class Meta:
verbose_name = _('Constraint Violation')
verbose_name_plural = _('Constraint Violations')
ordering = ['-timestamp']
class ViolationType(models.TextChoices):
"""
Possible types of violations with their text representation
"""
OWNER_TWO_SLOTS = 'ots', _('Owner has two parallel slots')
SLOT_OUTSIDE_AVAIL = 'soa', _('AK Slot was scheduled outside the AK\'s availabilities')
ROOM_TWO_SLOTS = 'rts', _('Room has two AK slots scheduled at the same time')
REQUIRE_NOT_GIVEN = 'rng', _('Room does not satisfy the requirement of the scheduled AK')
AK_CONFLICT_COLLISION = 'acc', _('AK Slot is scheduled at the same time as an AK listed as a conflict')
AK_BEFORE_PREREQUISITE = 'abp', _('AK Slot is scheduled before an AK listed as a prerequisite')
AK_AFTER_RESODEADLINE = 'aar', _(
'AK Slot for AK with intention to submit a resolution is scheduled after resolution deadline')
AK_CATEGORY_MISMATCH = 'acm', _('AK Slot in a category is outside that categories availabilities')
AK_SLOT_COLLISION = 'asc', _('Two AK Slots for the same AK scheduled at the same time')
ROOM_CAPACITY_EXCEEDED = 'rce', _('Room does not have enough space for interest in scheduled AK Slot')
SLOT_OUTSIDE_EVENT = 'soe', _('AK Slot is scheduled outside the event\'s availabilities')
class ViolationLevel(models.IntegerChoices):
"""
Possible severities/levels of a CV
"""
WARNING = 1, _('Warning')
VIOLATION = 10, _('Violation')
type = models.CharField(verbose_name=_('Type'), max_length=3, choices=ViolationType.choices,
help_text=_('Type of violation, i.e. what kind of constraint was violated'))
level = models.PositiveSmallIntegerField(verbose_name=_('Level'), choices=ViolationLevel.choices,
help_text=_('Severity level of the violation'))
event = models.ForeignKey(to=Event, on_delete=models.CASCADE, verbose_name=_('Event'),
help_text=_('Associated event'))
# Possible "causes":
aks = models.ManyToManyField(to=AK, blank=True, verbose_name=_('AKs'),
help_text=_('AK(s) belonging to this constraint'))
ak_slots = models.ManyToManyField(to=AKSlot, blank=True, verbose_name=_('AK Slots'),
help_text=_('AK Slot(s) belonging to this constraint'))
ak_owner = models.ForeignKey(to=AKOwner, on_delete=models.CASCADE, blank=True, null=True,
verbose_name=_('AK Owner'), help_text=_('AK Owner belonging to this constraint'))
room = models.ForeignKey(to=Room, on_delete=models.CASCADE, blank=True, null=True, verbose_name=_('Room'),
help_text=_('Room belonging to this constraint'))
requirement = models.ForeignKey(to=AKRequirement, on_delete=models.CASCADE, blank=True, null=True,
verbose_name=_('AK Requirement'),
help_text=_('AK Requirement belonging to this constraint'))
category = models.ForeignKey(to=AKCategory, on_delete=models.CASCADE, blank=True, null=True,
verbose_name=_('AK Category'), help_text=_('AK Category belonging to this constraint'))
comment = models.TextField(verbose_name=_('Comment'), help_text=_('Comment or further details for this violation'),
blank=True)
timestamp = models.DateTimeField(auto_now_add=True, verbose_name=_('Timestamp'), help_text=_('Time of creation'))
manually_resolved = models.BooleanField(verbose_name=_('Manually Resolved'), default=False,
help_text=_('Mark this violation manually as resolved'))
fields = ['ak_owner', 'room', 'requirement', 'category', 'comment']
fields_mm = ['_aks', '_ak_slots']
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.aks_tmp = set()
self.ak_slots_tmp = set()
def get_details(self):
"""
Get details of this constraint (all fields connected to it)
:return: string of details
:rtype: str
"""
# Stringify aks and ak slots fields (m2m)
output = [f"{_('AKs')}: {self._aks_str}",
f"{_('AK Slots')}: {self._ak_slots_str}"]
# Stringify all other fields
for field in self.fields:
a = getattr(self, field, None)
if a is not None and str(a) != '':
output.append(f"{field}: {a}")
return ", ".join(output)
get_details.short_description = _('Details')
@property
def details(self):
"""
Property: Details
"""
return self.get_details()
@property
def edit_url(self) -> str:
"""
Property: Edit URL for this CV
"""
return reverse_lazy('admin:AKModel_constraintviolation_change', kwargs={'object_id': self.pk})
@property
def level_display(self) -> str:
"""
Property: Severity as string
"""
return self.get_level_display()
@property
def type_display(self) -> str:
"""
Property: Type as string
"""
return self.get_type_display()
@property
def timestamp_display(self) -> str:
"""
Property: Creation timestamp as string
"""
return self.timestamp.astimezone(self.event.timezone).strftime('%d.%m.%y %H:%M')
@property
def _aks(self):
"""
Get all AKs belonging to this constraint violation
The distinction between real and tmp relationships is needed since many to many
relations only work for objects already persisted in the database
:return: set of all AKs belonging to this constraint violation
:rtype: set(AK)
"""
if self.pk and self.pk > 0:
return set(self.aks.all())
return self.aks_tmp
@property
def _aks_str(self) -> str:
"""
Property: AKs as string
"""
if self.pk and self.pk > 0:
return ', '.join(str(a) for a in self.aks.all())
return ', '.join(str(a) for a in self.aks_tmp)
@property
def _ak_slots(self):
"""
Get all AK Slots belonging to this constraint violation
The distinction between real and tmp relationships is needed since many to many
relations only work for objects already persisted in the database
:return: set of all AK Slots belonging to this constraint violation
:rtype: set(AKSlot)
"""
if self.pk and self.pk > 0:
return set(self.ak_slots.all())
return self.ak_slots_tmp
@property
def _ak_slots_str(self) -> str:
"""
Property: Slots as string
"""
if self.pk and self.pk > 0:
return ', '.join(str(a) for a in self.ak_slots.select_related('event').all())
return ', '.join(str(a) for a in self.ak_slots_tmp)
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
# Store temporary m2m-relations in db
for ak in self.aks_tmp:
self.aks.add(ak)
for ak_slot in self.ak_slots_tmp:
self.ak_slots.add(ak_slot)
def __str__(self):
return f"{self.get_level_display()}: {self.get_type_display()} [{self.get_details()}]"
def matches(self, other):
"""
Check whether one constraint violation instance matches another,
this means has the same type, room, requirement, owner, category
as well as the same lists of aks and ak slots.
PK, timestamp, comments and manual resolving are ignored.
:param other: second instance to compare to
:type other: ConstraintViolation
:return: true if both instances are similar in the way described, false if not
:rtype: bool
"""
if not isinstance(other, ConstraintViolation):
return False
# Check type
if self.type != other.type:
return False
# Make sure both have the same aks and ak slots
for field_mm in self.fields_mm:
s: set = getattr(self, field_mm)
o: set = getattr(other, field_mm)
if len(s) != len(o):
return False
if len(s.intersection(o)) != len(s):
return False
# Check other "defining" fields
for field in self.fields:
if getattr(self, field) != getattr(other, field):
return False
return True
class DefaultSlot(models.Model):
"""
Model representing a default slot,
i.e., a prefered slot to use for typical AKs in the schedule to guarantee enough breaks etc.
"""
class Meta:
verbose_name = _('Default Slot')
verbose_name_plural = _('Default Slots')
ordering = ['-start']
start = models.DateTimeField(verbose_name=_('Slot Begin'), help_text=_('Time and date the slot begins'))
end = models.DateTimeField(verbose_name=_('Slot End'), help_text=_('Time and date the slot ends'))
event = models.ForeignKey(to=Event, on_delete=models.CASCADE, verbose_name=_('Event'),
help_text=_('Associated event'))
primary_categories = models.ManyToManyField(to=AKCategory, verbose_name=_('Primary categories'), blank=True,
help_text=_(
'Categories that should be assigned to this slot primarily'))
@property
def start_simplified(self) -> str:
"""
Property: Simplified version of the start timetstamp (weekday, hour, minute) as string
"""
return self.start.astimezone(self.event.timezone).strftime('%a %H:%M')
@property
def start_iso(self) -> str:
"""
Property: Start timestamp as ISO timestamp for usage in calendar views
"""
return timezone.localtime(self.start, self.event.timezone).strftime("%Y-%m-%dT%H:%M:%S")
@property
def end_simplified(self) -> str:
"""
Property: Simplified version of the end timetstamp (weekday, hour, minute) as string
"""
return self.end.astimezone(self.event.timezone).strftime('%a %H:%M')
@property
def end_iso(self) -> str:
"""
Property: End timestamp as ISO timestamp for usage in calendar views
"""
return timezone.localtime(self.end, self.event.timezone).strftime("%Y-%m-%dT%H:%M:%S")
def __str__(self):
return f"{self.event}: {self.start_simplified} - {self.end_simplified}"
from rest_framework import serializers
from AKModel.models import AK, Room, AKSlot, AKTrack, AKCategory, AKOwner
class AKOwnerSerializer(serializers.ModelSerializer):
"""
REST Framework Serializer for AKOwner
"""
class Meta:
model = AKOwner
fields = '__all__'
class AKCategorySerializer(serializers.ModelSerializer):
"""
REST Framework Serializer for AKCategory
"""
class Meta:
model = AKCategory
fields = '__all__'
class AKTrackSerializer(serializers.ModelSerializer):
"""
REST Framework Serializer for AKTrack
"""
class Meta:
model = AKTrack
fields = '__all__'
class AKSerializer(serializers.ModelSerializer):
"""
REST Framework Serializer for AK
"""
class Meta:
model = AK
fields = '__all__'
class RoomSerializer(serializers.ModelSerializer):
"""
REST Framework Serializer for Room
"""
class Meta:
model = Room
fields = '__all__'
class AKSlotSerializer(serializers.ModelSerializer):
"""
REST Framework Serializer for AKSlot
"""
class Meta:
model = AKSlot
fields = '__all__'
treat_as_local = serializers.BooleanField(required=False, default=False, write_only=True)
def create(self, validated_data:dict):
# Handle timezone adaption based upon the control field "treat_as_local":
# If it is set, ignore timezone submitted from the browser (will always be UTC)
# and treat it as input in the events timezone instead
if validated_data['treat_as_local']:
validated_data['start'] = validated_data['start'].replace(tzinfo=None).astimezone(
validated_data['event'].timezone)
del validated_data['treat_as_local']
return super().create(validated_data)
from django.contrib.admin import AdminSite
from django.utils.translation import gettext_lazy as _
# from django.urls import path
from AKModel.models import Event
class AKAdminSite(AdminSite):
"""
Custom admin interface definition (extend the admin functionality of Django)
"""
index_template = "admin/ak_index.html"
site_header = f"AKPlanning - {_('Administration')}"
index_title = _('Administration')
def get_urls(self):
"""
Get URLs -- add further views that are not related to a certain model here if needed
"""
urls = super().get_urls()
urls += [
# path('...', self.admin_view(...)),
]
return urls
def index(self, request, extra_context=None):
# Override index page rendering to provide extra context (the list of active events)
# to be used in the adapted template
if extra_context is None:
extra_context = {}
extra_context["active_events"] = Event.objects.filter(active=True)
return super().index(request, extra_context)