#93: reorganise Rerun, Diffusion, Schedule module #95

Merged
thomas merged 5 commits from dev-1.0-93-rerun-diffusion-schedule-modules into develop-1.0 2023-04-02 18:37:48 +00:00
37 changed files with 4791 additions and 842 deletions

View File

@ -16,7 +16,7 @@ repos:
rev: 6.0.0
hooks:
- id: flake8
exclude: instance/settings/
exclude: ^instance/settings/|migrations/
- repo: https://github.com/PyCQA/docformatter.git
rev: v1.5.1
hooks:

View File

@ -1,9 +1,11 @@
from . import filters
from .article import ArticleAdmin
from .episode import DiffusionAdmin, EpisodeAdmin
from .diffusion import DiffusionAdmin
from .episode import EpisodeAdmin
from .log import LogAdmin
from .page import PageAdmin, StaticPageAdmin
from .program import ProgramAdmin, ScheduleAdmin, StreamAdmin
from .program import ProgramAdmin, StreamAdmin
from .schedule import ScheduleAdmin
from .sound import SoundAdmin, TrackAdmin
from .station import StationAdmin

48
aircox/admin/diffusion.py Normal file
View File

@ -0,0 +1,48 @@
from django.contrib import admin
from django.utils.translation import gettext as _
from aircox.models import Diffusion
__all__ = ("DiffusionBaseAdmin", "DiffusionAdmin", "DiffusionInline")
class DiffusionBaseAdmin:
fields = ("type", "start", "end", "schedule")
readonly_fields = ("schedule",)
def get_readonly_fields(self, request, obj=None):
fields = super().get_readonly_fields(request, obj)
if not request.user.has_perm("aircox_program.scheduling"):
fields = fields + ("program", "start", "end")
return [field for field in fields if field in self.fields]
@admin.register(Diffusion)
class DiffusionAdmin(DiffusionBaseAdmin, admin.ModelAdmin):
def start_date(self, obj):
return obj.local_start.strftime("%Y/%m/%d %H:%M")
start_date.short_description = _("start")
def end_date(self, obj):
return obj.local_end.strftime("%H:%M")
end_date.short_description = _("end")
list_display = ("episode", "start_date", "end_date", "type", "initial")
list_filter = ("type", "start", "program")
list_editable = ("type",)
ordering = ("-start", "id")
fields = ("type", "start", "end", "initial", "program", "schedule")
readonly_fields = ("schedule",)
class DiffusionInline(DiffusionBaseAdmin, admin.TabularInline):
model = Diffusion
fk_name = "episode"
extra = 0
def has_add_permission(self, request, obj):
return request.user.has_perm("aircox_program.scheduling")

View File

@ -1,52 +1,11 @@
from adminsortable2.admin import SortableAdminBase
from django.contrib import admin
from django.forms import ModelForm
from django.utils.translation import gettext as _
from ..models import Diffusion, Episode
from aircox.models import Episode
from .page import PageAdmin
from .sound import SoundInline, TrackInline
class DiffusionBaseAdmin:
fields = ("type", "start", "end", "schedule")
readonly_fields = ("schedule",)
def get_readonly_fields(self, request, obj=None):
fields = super().get_readonly_fields(request, obj)
if not request.user.has_perm("aircox_program.scheduling"):
fields = fields + ("program", "start", "end")
return [field for field in fields if field in self.fields]
@admin.register(Diffusion)
class DiffusionAdmin(DiffusionBaseAdmin, admin.ModelAdmin):
def start_date(self, obj):
return obj.local_start.strftime("%Y/%m/%d %H:%M")
start_date.short_description = _("start")
def end_date(self, obj):
return obj.local_end.strftime("%H:%M")
end_date.short_description = _("end")
list_display = ("episode", "start_date", "end_date", "type", "initial")
list_filter = ("type", "start", "program")
list_editable = ("type",)
ordering = ("-start", "id")
fields = ("type", "start", "end", "initial", "program", "schedule")
readonly_fields = ("schedule",)
class DiffusionInline(DiffusionBaseAdmin, admin.TabularInline):
model = Diffusion
fk_name = "episode"
extra = 0
def has_add_permission(self, request, obj):
return request.user.has_perm("aircox_program.scheduling")
from .diffusion import DiffusionInline
class EpisodeAdminForm(ModelForm):

View File

@ -1,26 +1,12 @@
from django.contrib import admin
from django.forms import ModelForm
from django.utils.translation import gettext_lazy as _
from ..models import Program, Schedule, Stream
from aircox.models import Program, Schedule, Stream
from .page import PageAdmin
from .schedule import ScheduleInline
# In order to simplify schedule_post_save algorithm, an existing schedule can't
# update the following fields: "frequency", "date"
class ScheduleInlineForm(ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.initial:
self.fields["date"].disabled = True
self.fields["frequency"].disabled = True
class ScheduleInline(admin.TabularInline):
model = Schedule
form = ScheduleInlineForm
readonly_fields = ("timezone",)
extra = 1
__all__ = ("ProgramAdmin", "StreamInline", "StreamAdmin")
class StreamInline(admin.TabularInline):
@ -58,36 +44,6 @@ class ProgramAdmin(PageAdmin):
return fields
@admin.register(Schedule)
class ScheduleAdmin(admin.ModelAdmin):
def program_title(self, obj):
return obj.program.title
program_title.short_description = _("Program")
def freq(self, obj):
return obj.get_frequency_verbose()
freq.short_description = _("Day")
list_filter = ["frequency", "program"]
list_display = [
"program_title",
"freq",
"time",
"timezone",
"duration",
"initial",
]
list_editable = ["time", "duration", "initial"]
def get_readonly_fields(self, request, obj=None):
if obj:
return ["program", "date", "frequency"]
else:
return []
@admin.register(Stream)
class StreamAdmin(admin.ModelAdmin):
list_display = ("id", "program", "delay", "begin", "end")

55
aircox/admin/schedule.py Normal file
View File

@ -0,0 +1,55 @@
from django.contrib import admin
from django.forms import ModelForm
from django.utils.translation import gettext_lazy as _
from aircox.models import Schedule
__all__ = ("ScheduleInlineForm", "ScheduleInline", "ScheduleAdmin")
# In order to simplify schedule_post_save algorithm, an existing schedule can't
# update the following fields: "frequency", "date"
class ScheduleInlineForm(ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.initial:
self.fields["date"].disabled = True
self.fields["frequency"].disabled = True
class ScheduleInline(admin.TabularInline):
model = Schedule
form = ScheduleInlineForm
readonly_fields = ("timezone",)
extra = 1
@admin.register(Schedule)
class ScheduleAdmin(admin.ModelAdmin):
def program_title(self, obj):
return obj.program.title
program_title.short_description = _("Program")
def freq(self, obj):
return obj.get_frequency_display()
freq.short_description = _("Day")
list_filter = ["frequency", "program"]
list_display = [
"program_title",
"freq",
"time",
"timezone",
"duration",
"initial",
]
list_editable = ["time", "duration", "initial"]
def get_readonly_fields(self, request, obj=None):
if obj:
return ["program", "date", "frequency"]
else:
return []

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,17 @@
# Generated by Django 3.0.6 on 2020-05-26 13:16
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("aircox", "0001_initial"),
]
operations = [
migrations.RenameField(
model_name="staticpage",
old_name="view",
new_name="attach_to",
),
]

View File

@ -0,0 +1,146 @@
# Generated by Django 3.0.6 on 2020-05-30 11:16
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import filer.fields.image
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.FILER_IMAGE_MODEL),
("aircox", "0002_auto_20200526_1516"),
]
operations = [
migrations.AlterModelOptions(
name="log",
options={"verbose_name": "Log", "verbose_name_plural": "Logs"},
),
migrations.AlterModelOptions(
name="page",
options={
"verbose_name": "Publication",
"verbose_name_plural": "Publications",
},
),
migrations.AlterModelOptions(
name="program",
options={
"verbose_name": "Program",
"verbose_name_plural": "Programs",
},
),
migrations.RemoveField(
model_name="article",
name="is_static",
),
migrations.AddField(
model_name="diffusion",
name="schedule",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
to="aircox.Schedule",
verbose_name="schedule",
),
),
migrations.AlterField(
model_name="diffusion",
name="initial",
field=models.ForeignKey(
blank=True,
limit_choices_to={"initial__isnull": True},
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="rerun_set",
to="aircox.Diffusion",
verbose_name="rerun of",
),
),
migrations.AlterField(
model_name="navitem",
name="page",
field=models.ForeignKey(
blank=True,
limit_choices_to={"attach_to__isnull": True},
null=True,
on_delete=django.db.models.deletion.CASCADE,
to="aircox.StaticPage",
verbose_name="page",
),
),
migrations.AlterField(
model_name="schedule",
name="frequency",
field=models.SmallIntegerField(
choices=[
(0, "ponctual"),
(1, "1st {day} of the month"),
(2, "2nd {day} of the month"),
(4, "3rd {day} of the month"),
(8, "4th {day} of the month"),
(16, "last {day} of the month"),
(5, "1st and 3rd {day} of the month"),
(10, "2nd and 4th {day} of the month"),
(31, "every {day}"),
(32, "one {day} on two"),
],
verbose_name="frequency",
),
),
migrations.AlterField(
model_name="schedule",
name="initial",
field=models.ForeignKey(
blank=True,
limit_choices_to={"initial__isnull": True},
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="rerun_set",
to="aircox.Schedule",
verbose_name="rerun of",
),
),
migrations.AlterField(
model_name="staticpage",
name="attach_to",
field=models.SmallIntegerField(
blank=True,
choices=[
(0, "Home page"),
(1, "Diffusions page"),
(2, "Logs page"),
(3, "Programs list"),
(4, "Episodes list"),
(5, "Articles list"),
],
help_text="display this page content to related element",
null=True,
verbose_name="attach to",
),
),
migrations.AlterField(
model_name="station",
name="default_cover",
field=filer.fields.image.FilerImageField(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="+",
to=settings.FILER_IMAGE_MODEL,
verbose_name="Default pages' cover",
),
),
migrations.AlterField(
model_name="track",
name="timestamp",
field=models.PositiveSmallIntegerField(
blank=True,
help_text="position (in seconds)",
null=True,
verbose_name="timestamp",
),
),
]

View File

@ -0,0 +1,59 @@
# Generated by Django 3.1.1 on 2020-09-21 23:56
import ckeditor_uploader.fields
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("aircox", "0003_auto_20200530_1116"),
]
operations = [
migrations.AlterModelOptions(
name="comment",
options={
"verbose_name": "Comment",
"verbose_name_plural": "Comments",
},
),
migrations.AlterModelOptions(
name="navitem",
options={
"ordering": ("order", "pk"),
"verbose_name": "Menu item",
"verbose_name_plural": "Menu items",
},
),
migrations.RemoveField(
model_name="sound",
name="embed",
),
migrations.AlterField(
model_name="page",
name="content",
field=ckeditor_uploader.fields.RichTextUploadingField(
blank=True, null=True, verbose_name="content"
),
),
migrations.AlterField(
model_name="sound",
name="program",
field=models.ForeignKey(
default=1,
help_text="program related to it",
on_delete=django.db.models.deletion.CASCADE,
to="aircox.program",
verbose_name="program",
),
preserve_default=False,
),
migrations.AlterField(
model_name="staticpage",
name="content",
field=ckeditor_uploader.fields.RichTextUploadingField(
blank=True, null=True, verbose_name="content"
),
),
]

View File

@ -0,0 +1,839 @@
# Generated by Django 3.2.12 on 2022-03-18 12:05
import aircox.models.sound
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
("aircox", "0004_auto_20200921_2356"),
]
operations = [
migrations.RemoveField(
model_name="sound",
name="path",
),
migrations.AddField(
model_name="sound",
name="file",
field=models.FileField(
default="",
upload_to=aircox.models.sound.Sound._upload_to,
verbose_name="file",
),
preserve_default=False,
),
migrations.AlterField(
model_name="category",
name="id",
field=models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
migrations.AlterField(
model_name="comment",
name="id",
field=models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
migrations.AlterField(
model_name="diffusion",
name="end",
field=models.DateTimeField(db_index=True, verbose_name="end"),
),
migrations.AlterField(
model_name="diffusion",
name="id",
field=models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
migrations.AlterField(
model_name="diffusion",
name="start",
field=models.DateTimeField(db_index=True, verbose_name="start"),
),
migrations.AlterField(
model_name="log",
name="id",
field=models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
migrations.AlterField(
model_name="navitem",
name="id",
field=models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
migrations.AlterField(
model_name="navitem",
name="page",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
to="aircox.staticpage",
verbose_name="page",
),
),
migrations.AlterField(
model_name="page",
name="id",
field=models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
migrations.AlterField(
model_name="port",
name="id",
field=models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
migrations.AlterField(
model_name="schedule",
name="id",
field=models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
migrations.AlterField(
model_name="schedule",
name="timezone",
field=models.CharField(
choices=[
("Africa/Abidjan", "Africa/Abidjan"),
("Africa/Accra", "Africa/Accra"),
("Africa/Addis_Ababa", "Africa/Addis_Ababa"),
("Africa/Algiers", "Africa/Algiers"),
("Africa/Asmara", "Africa/Asmara"),
("Africa/Asmera", "Africa/Asmera"),
("Africa/Bamako", "Africa/Bamako"),
("Africa/Bangui", "Africa/Bangui"),
("Africa/Banjul", "Africa/Banjul"),
("Africa/Bissau", "Africa/Bissau"),
("Africa/Blantyre", "Africa/Blantyre"),
("Africa/Brazzaville", "Africa/Brazzaville"),
("Africa/Bujumbura", "Africa/Bujumbura"),
("Africa/Cairo", "Africa/Cairo"),
("Africa/Casablanca", "Africa/Casablanca"),
("Africa/Ceuta", "Africa/Ceuta"),
("Africa/Conakry", "Africa/Conakry"),
("Africa/Dakar", "Africa/Dakar"),
("Africa/Dar_es_Salaam", "Africa/Dar_es_Salaam"),
("Africa/Djibouti", "Africa/Djibouti"),
("Africa/Douala", "Africa/Douala"),
("Africa/El_Aaiun", "Africa/El_Aaiun"),
("Africa/Freetown", "Africa/Freetown"),
("Africa/Gaborone", "Africa/Gaborone"),
("Africa/Harare", "Africa/Harare"),
("Africa/Johannesburg", "Africa/Johannesburg"),
("Africa/Juba", "Africa/Juba"),
("Africa/Kampala", "Africa/Kampala"),
("Africa/Khartoum", "Africa/Khartoum"),
("Africa/Kigali", "Africa/Kigali"),
("Africa/Kinshasa", "Africa/Kinshasa"),
("Africa/Lagos", "Africa/Lagos"),
("Africa/Libreville", "Africa/Libreville"),
("Africa/Lome", "Africa/Lome"),
("Africa/Luanda", "Africa/Luanda"),
("Africa/Lubumbashi", "Africa/Lubumbashi"),
("Africa/Lusaka", "Africa/Lusaka"),
("Africa/Malabo", "Africa/Malabo"),
("Africa/Maputo", "Africa/Maputo"),
("Africa/Maseru", "Africa/Maseru"),
("Africa/Mbabane", "Africa/Mbabane"),
("Africa/Mogadishu", "Africa/Mogadishu"),
("Africa/Monrovia", "Africa/Monrovia"),
("Africa/Nairobi", "Africa/Nairobi"),
("Africa/Ndjamena", "Africa/Ndjamena"),
("Africa/Niamey", "Africa/Niamey"),
("Africa/Nouakchott", "Africa/Nouakchott"),
("Africa/Ouagadougou", "Africa/Ouagadougou"),
("Africa/Porto-Novo", "Africa/Porto-Novo"),
("Africa/Sao_Tome", "Africa/Sao_Tome"),
("Africa/Timbuktu", "Africa/Timbuktu"),
("Africa/Tripoli", "Africa/Tripoli"),
("Africa/Tunis", "Africa/Tunis"),
("Africa/Windhoek", "Africa/Windhoek"),
("America/Adak", "America/Adak"),
("America/Anchorage", "America/Anchorage"),
("America/Anguilla", "America/Anguilla"),
("America/Antigua", "America/Antigua"),
("America/Araguaina", "America/Araguaina"),
(
"America/Argentina/Buenos_Aires",
"America/Argentina/Buenos_Aires",
),
(
"America/Argentina/Catamarca",
"America/Argentina/Catamarca",
),
(
"America/Argentina/ComodRivadavia",
"America/Argentina/ComodRivadavia",
),
("America/Argentina/Cordoba", "America/Argentina/Cordoba"),
("America/Argentina/Jujuy", "America/Argentina/Jujuy"),
(
"America/Argentina/La_Rioja",
"America/Argentina/La_Rioja",
),
("America/Argentina/Mendoza", "America/Argentina/Mendoza"),
(
"America/Argentina/Rio_Gallegos",
"America/Argentina/Rio_Gallegos",
),
("America/Argentina/Salta", "America/Argentina/Salta"),
(
"America/Argentina/San_Juan",
"America/Argentina/San_Juan",
),
(
"America/Argentina/San_Luis",
"America/Argentina/San_Luis",
),
("America/Argentina/Tucuman", "America/Argentina/Tucuman"),
("America/Argentina/Ushuaia", "America/Argentina/Ushuaia"),
("America/Aruba", "America/Aruba"),
("America/Asuncion", "America/Asuncion"),
("America/Atikokan", "America/Atikokan"),
("America/Atka", "America/Atka"),
("America/Bahia", "America/Bahia"),
("America/Bahia_Banderas", "America/Bahia_Banderas"),
("America/Barbados", "America/Barbados"),
("America/Belem", "America/Belem"),
("America/Belize", "America/Belize"),
("America/Blanc-Sablon", "America/Blanc-Sablon"),
("America/Boa_Vista", "America/Boa_Vista"),
("America/Bogota", "America/Bogota"),
("America/Boise", "America/Boise"),
("America/Buenos_Aires", "America/Buenos_Aires"),
("America/Cambridge_Bay", "America/Cambridge_Bay"),
("America/Campo_Grande", "America/Campo_Grande"),
("America/Cancun", "America/Cancun"),
("America/Caracas", "America/Caracas"),
("America/Catamarca", "America/Catamarca"),
("America/Cayenne", "America/Cayenne"),
("America/Cayman", "America/Cayman"),
("America/Chicago", "America/Chicago"),
("America/Chihuahua", "America/Chihuahua"),
("America/Coral_Harbour", "America/Coral_Harbour"),
("America/Cordoba", "America/Cordoba"),
("America/Costa_Rica", "America/Costa_Rica"),
("America/Creston", "America/Creston"),
("America/Cuiaba", "America/Cuiaba"),
("America/Curacao", "America/Curacao"),
("America/Danmarkshavn", "America/Danmarkshavn"),
("America/Dawson", "America/Dawson"),
("America/Dawson_Creek", "America/Dawson_Creek"),
("America/Denver", "America/Denver"),
("America/Detroit", "America/Detroit"),
("America/Dominica", "America/Dominica"),
("America/Edmonton", "America/Edmonton"),
("America/Eirunepe", "America/Eirunepe"),
("America/El_Salvador", "America/El_Salvador"),
("America/Ensenada", "America/Ensenada"),
("America/Fort_Nelson", "America/Fort_Nelson"),
("America/Fort_Wayne", "America/Fort_Wayne"),
("America/Fortaleza", "America/Fortaleza"),
("America/Glace_Bay", "America/Glace_Bay"),
("America/Godthab", "America/Godthab"),
("America/Goose_Bay", "America/Goose_Bay"),
("America/Grand_Turk", "America/Grand_Turk"),
("America/Grenada", "America/Grenada"),
("America/Guadeloupe", "America/Guadeloupe"),
("America/Guatemala", "America/Guatemala"),
("America/Guayaquil", "America/Guayaquil"),
("America/Guyana", "America/Guyana"),
("America/Halifax", "America/Halifax"),
("America/Havana", "America/Havana"),
("America/Hermosillo", "America/Hermosillo"),
(
"America/Indiana/Indianapolis",
"America/Indiana/Indianapolis",
),
("America/Indiana/Knox", "America/Indiana/Knox"),
("America/Indiana/Marengo", "America/Indiana/Marengo"),
(
"America/Indiana/Petersburg",
"America/Indiana/Petersburg",
),
("America/Indiana/Tell_City", "America/Indiana/Tell_City"),
("America/Indiana/Vevay", "America/Indiana/Vevay"),
("America/Indiana/Vincennes", "America/Indiana/Vincennes"),
("America/Indiana/Winamac", "America/Indiana/Winamac"),
("America/Indianapolis", "America/Indianapolis"),
("America/Inuvik", "America/Inuvik"),
("America/Iqaluit", "America/Iqaluit"),
("America/Jamaica", "America/Jamaica"),
("America/Jujuy", "America/Jujuy"),
("America/Juneau", "America/Juneau"),
(
"America/Kentucky/Louisville",
"America/Kentucky/Louisville",
),
(
"America/Kentucky/Monticello",
"America/Kentucky/Monticello",
),
("America/Knox_IN", "America/Knox_IN"),
("America/Kralendijk", "America/Kralendijk"),
("America/La_Paz", "America/La_Paz"),
("America/Lima", "America/Lima"),
("America/Los_Angeles", "America/Los_Angeles"),
("America/Louisville", "America/Louisville"),
("America/Lower_Princes", "America/Lower_Princes"),
("America/Maceio", "America/Maceio"),
("America/Managua", "America/Managua"),
("America/Manaus", "America/Manaus"),
("America/Marigot", "America/Marigot"),
("America/Martinique", "America/Martinique"),
("America/Matamoros", "America/Matamoros"),
("America/Mazatlan", "America/Mazatlan"),
("America/Mendoza", "America/Mendoza"),
("America/Menominee", "America/Menominee"),
("America/Merida", "America/Merida"),
("America/Metlakatla", "America/Metlakatla"),
("America/Mexico_City", "America/Mexico_City"),
("America/Miquelon", "America/Miquelon"),
("America/Moncton", "America/Moncton"),
("America/Monterrey", "America/Monterrey"),
("America/Montevideo", "America/Montevideo"),
("America/Montreal", "America/Montreal"),
("America/Montserrat", "America/Montserrat"),
("America/Nassau", "America/Nassau"),
("America/New_York", "America/New_York"),
("America/Nipigon", "America/Nipigon"),
("America/Nome", "America/Nome"),
("America/Noronha", "America/Noronha"),
(
"America/North_Dakota/Beulah",
"America/North_Dakota/Beulah",
),
(
"America/North_Dakota/Center",
"America/North_Dakota/Center",
),
(
"America/North_Dakota/New_Salem",
"America/North_Dakota/New_Salem",
),
("America/Nuuk", "America/Nuuk"),
("America/Ojinaga", "America/Ojinaga"),
("America/Panama", "America/Panama"),
("America/Pangnirtung", "America/Pangnirtung"),
("America/Paramaribo", "America/Paramaribo"),
("America/Phoenix", "America/Phoenix"),
("America/Port-au-Prince", "America/Port-au-Prince"),
("America/Port_of_Spain", "America/Port_of_Spain"),
("America/Porto_Acre", "America/Porto_Acre"),
("America/Porto_Velho", "America/Porto_Velho"),
("America/Puerto_Rico", "America/Puerto_Rico"),
("America/Punta_Arenas", "America/Punta_Arenas"),
("America/Rainy_River", "America/Rainy_River"),
("America/Rankin_Inlet", "America/Rankin_Inlet"),
("America/Recife", "America/Recife"),
("America/Regina", "America/Regina"),
("America/Resolute", "America/Resolute"),
("America/Rio_Branco", "America/Rio_Branco"),
("America/Rosario", "America/Rosario"),
("America/Santa_Isabel", "America/Santa_Isabel"),
("America/Santarem", "America/Santarem"),
("America/Santiago", "America/Santiago"),
("America/Santo_Domingo", "America/Santo_Domingo"),
("America/Sao_Paulo", "America/Sao_Paulo"),
("America/Scoresbysund", "America/Scoresbysund"),
("America/Shiprock", "America/Shiprock"),
("America/Sitka", "America/Sitka"),
("America/St_Barthelemy", "America/St_Barthelemy"),
("America/St_Johns", "America/St_Johns"),
("America/St_Kitts", "America/St_Kitts"),
("America/St_Lucia", "America/St_Lucia"),
("America/St_Thomas", "America/St_Thomas"),
("America/St_Vincent", "America/St_Vincent"),
("America/Swift_Current", "America/Swift_Current"),
("America/Tegucigalpa", "America/Tegucigalpa"),
("America/Thule", "America/Thule"),
("America/Thunder_Bay", "America/Thunder_Bay"),
("America/Tijuana", "America/Tijuana"),
("America/Toronto", "America/Toronto"),
("America/Tortola", "America/Tortola"),
("America/Vancouver", "America/Vancouver"),
("America/Virgin", "America/Virgin"),
("America/Whitehorse", "America/Whitehorse"),
("America/Winnipeg", "America/Winnipeg"),
("America/Yakutat", "America/Yakutat"),
("America/Yellowknife", "America/Yellowknife"),
("Antarctica/Casey", "Antarctica/Casey"),
("Antarctica/Davis", "Antarctica/Davis"),
("Antarctica/DumontDUrville", "Antarctica/DumontDUrville"),
("Antarctica/Macquarie", "Antarctica/Macquarie"),
("Antarctica/Mawson", "Antarctica/Mawson"),
("Antarctica/McMurdo", "Antarctica/McMurdo"),
("Antarctica/Palmer", "Antarctica/Palmer"),
("Antarctica/Rothera", "Antarctica/Rothera"),
("Antarctica/South_Pole", "Antarctica/South_Pole"),
("Antarctica/Syowa", "Antarctica/Syowa"),
("Antarctica/Troll", "Antarctica/Troll"),
("Antarctica/Vostok", "Antarctica/Vostok"),
("Arctic/Longyearbyen", "Arctic/Longyearbyen"),
("Asia/Aden", "Asia/Aden"),
("Asia/Almaty", "Asia/Almaty"),
("Asia/Amman", "Asia/Amman"),
("Asia/Anadyr", "Asia/Anadyr"),
("Asia/Aqtau", "Asia/Aqtau"),
("Asia/Aqtobe", "Asia/Aqtobe"),
("Asia/Ashgabat", "Asia/Ashgabat"),
("Asia/Ashkhabad", "Asia/Ashkhabad"),
("Asia/Atyrau", "Asia/Atyrau"),
("Asia/Baghdad", "Asia/Baghdad"),
("Asia/Bahrain", "Asia/Bahrain"),
("Asia/Baku", "Asia/Baku"),
("Asia/Bangkok", "Asia/Bangkok"),
("Asia/Barnaul", "Asia/Barnaul"),
("Asia/Beirut", "Asia/Beirut"),
("Asia/Bishkek", "Asia/Bishkek"),
("Asia/Brunei", "Asia/Brunei"),
("Asia/Calcutta", "Asia/Calcutta"),
("Asia/Chita", "Asia/Chita"),
("Asia/Choibalsan", "Asia/Choibalsan"),
("Asia/Chongqing", "Asia/Chongqing"),
("Asia/Chungking", "Asia/Chungking"),
("Asia/Colombo", "Asia/Colombo"),
("Asia/Dacca", "Asia/Dacca"),
("Asia/Damascus", "Asia/Damascus"),
("Asia/Dhaka", "Asia/Dhaka"),
("Asia/Dili", "Asia/Dili"),
("Asia/Dubai", "Asia/Dubai"),
("Asia/Dushanbe", "Asia/Dushanbe"),
("Asia/Famagusta", "Asia/Famagusta"),
("Asia/Gaza", "Asia/Gaza"),
("Asia/Harbin", "Asia/Harbin"),
("Asia/Hebron", "Asia/Hebron"),
("Asia/Ho_Chi_Minh", "Asia/Ho_Chi_Minh"),
("Asia/Hong_Kong", "Asia/Hong_Kong"),
("Asia/Hovd", "Asia/Hovd"),
("Asia/Irkutsk", "Asia/Irkutsk"),
("Asia/Istanbul", "Asia/Istanbul"),
("Asia/Jakarta", "Asia/Jakarta"),
("Asia/Jayapura", "Asia/Jayapura"),
("Asia/Jerusalem", "Asia/Jerusalem"),
("Asia/Kabul", "Asia/Kabul"),
("Asia/Kamchatka", "Asia/Kamchatka"),
("Asia/Karachi", "Asia/Karachi"),
("Asia/Kashgar", "Asia/Kashgar"),
("Asia/Kathmandu", "Asia/Kathmandu"),
("Asia/Katmandu", "Asia/Katmandu"),
("Asia/Khandyga", "Asia/Khandyga"),
("Asia/Kolkata", "Asia/Kolkata"),
("Asia/Krasnoyarsk", "Asia/Krasnoyarsk"),
("Asia/Kuala_Lumpur", "Asia/Kuala_Lumpur"),
("Asia/Kuching", "Asia/Kuching"),
("Asia/Kuwait", "Asia/Kuwait"),
("Asia/Macao", "Asia/Macao"),
("Asia/Macau", "Asia/Macau"),
("Asia/Magadan", "Asia/Magadan"),
("Asia/Makassar", "Asia/Makassar"),
("Asia/Manila", "Asia/Manila"),
("Asia/Muscat", "Asia/Muscat"),
("Asia/Nicosia", "Asia/Nicosia"),
("Asia/Novokuznetsk", "Asia/Novokuznetsk"),
("Asia/Novosibirsk", "Asia/Novosibirsk"),
("Asia/Omsk", "Asia/Omsk"),
("Asia/Oral", "Asia/Oral"),
("Asia/Phnom_Penh", "Asia/Phnom_Penh"),
("Asia/Pontianak", "Asia/Pontianak"),
("Asia/Pyongyang", "Asia/Pyongyang"),
("Asia/Qatar", "Asia/Qatar"),
("Asia/Qostanay", "Asia/Qostanay"),
("Asia/Qyzylorda", "Asia/Qyzylorda"),
("Asia/Rangoon", "Asia/Rangoon"),
("Asia/Riyadh", "Asia/Riyadh"),
("Asia/Saigon", "Asia/Saigon"),
("Asia/Sakhalin", "Asia/Sakhalin"),
("Asia/Samarkand", "Asia/Samarkand"),
("Asia/Seoul", "Asia/Seoul"),
("Asia/Shanghai", "Asia/Shanghai"),
("Asia/Singapore", "Asia/Singapore"),
("Asia/Srednekolymsk", "Asia/Srednekolymsk"),
("Asia/Taipei", "Asia/Taipei"),
("Asia/Tashkent", "Asia/Tashkent"),
("Asia/Tbilisi", "Asia/Tbilisi"),
("Asia/Tehran", "Asia/Tehran"),
("Asia/Tel_Aviv", "Asia/Tel_Aviv"),
("Asia/Thimbu", "Asia/Thimbu"),
("Asia/Thimphu", "Asia/Thimphu"),
("Asia/Tokyo", "Asia/Tokyo"),
("Asia/Tomsk", "Asia/Tomsk"),
("Asia/Ujung_Pandang", "Asia/Ujung_Pandang"),
("Asia/Ulaanbaatar", "Asia/Ulaanbaatar"),
("Asia/Ulan_Bator", "Asia/Ulan_Bator"),
("Asia/Urumqi", "Asia/Urumqi"),
("Asia/Ust-Nera", "Asia/Ust-Nera"),
("Asia/Vientiane", "Asia/Vientiane"),
("Asia/Vladivostok", "Asia/Vladivostok"),
("Asia/Yakutsk", "Asia/Yakutsk"),
("Asia/Yangon", "Asia/Yangon"),
("Asia/Yekaterinburg", "Asia/Yekaterinburg"),
("Asia/Yerevan", "Asia/Yerevan"),
("Atlantic/Azores", "Atlantic/Azores"),
("Atlantic/Bermuda", "Atlantic/Bermuda"),
("Atlantic/Canary", "Atlantic/Canary"),
("Atlantic/Cape_Verde", "Atlantic/Cape_Verde"),
("Atlantic/Faeroe", "Atlantic/Faeroe"),
("Atlantic/Faroe", "Atlantic/Faroe"),
("Atlantic/Jan_Mayen", "Atlantic/Jan_Mayen"),
("Atlantic/Madeira", "Atlantic/Madeira"),
("Atlantic/Reykjavik", "Atlantic/Reykjavik"),
("Atlantic/South_Georgia", "Atlantic/South_Georgia"),
("Atlantic/St_Helena", "Atlantic/St_Helena"),
("Atlantic/Stanley", "Atlantic/Stanley"),
("Australia/ACT", "Australia/ACT"),
("Australia/Adelaide", "Australia/Adelaide"),
("Australia/Brisbane", "Australia/Brisbane"),
("Australia/Broken_Hill", "Australia/Broken_Hill"),
("Australia/Canberra", "Australia/Canberra"),
("Australia/Currie", "Australia/Currie"),
("Australia/Darwin", "Australia/Darwin"),
("Australia/Eucla", "Australia/Eucla"),
("Australia/Hobart", "Australia/Hobart"),
("Australia/LHI", "Australia/LHI"),
("Australia/Lindeman", "Australia/Lindeman"),
("Australia/Lord_Howe", "Australia/Lord_Howe"),
("Australia/Melbourne", "Australia/Melbourne"),
("Australia/NSW", "Australia/NSW"),
("Australia/North", "Australia/North"),
("Australia/Perth", "Australia/Perth"),
("Australia/Queensland", "Australia/Queensland"),
("Australia/South", "Australia/South"),
("Australia/Sydney", "Australia/Sydney"),
("Australia/Tasmania", "Australia/Tasmania"),
("Australia/Victoria", "Australia/Victoria"),
("Australia/West", "Australia/West"),
("Australia/Yancowinna", "Australia/Yancowinna"),
("Brazil/Acre", "Brazil/Acre"),
("Brazil/DeNoronha", "Brazil/DeNoronha"),
("Brazil/East", "Brazil/East"),
("Brazil/West", "Brazil/West"),
("CET", "CET"),
("CST6CDT", "CST6CDT"),
("Canada/Atlantic", "Canada/Atlantic"),
("Canada/Central", "Canada/Central"),
("Canada/Eastern", "Canada/Eastern"),
("Canada/Mountain", "Canada/Mountain"),
("Canada/Newfoundland", "Canada/Newfoundland"),
("Canada/Pacific", "Canada/Pacific"),
("Canada/Saskatchewan", "Canada/Saskatchewan"),
("Canada/Yukon", "Canada/Yukon"),
("Chile/Continental", "Chile/Continental"),
("Chile/EasterIsland", "Chile/EasterIsland"),
("Cuba", "Cuba"),
("EET", "EET"),
("EST", "EST"),
("EST5EDT", "EST5EDT"),
("Egypt", "Egypt"),
("Eire", "Eire"),
("Etc/GMT", "Etc/GMT"),
("Etc/GMT+0", "Etc/GMT+0"),
("Etc/GMT+1", "Etc/GMT+1"),
("Etc/GMT+10", "Etc/GMT+10"),
("Etc/GMT+11", "Etc/GMT+11"),
("Etc/GMT+12", "Etc/GMT+12"),
("Etc/GMT+2", "Etc/GMT+2"),
("Etc/GMT+3", "Etc/GMT+3"),
("Etc/GMT+4", "Etc/GMT+4"),
("Etc/GMT+5", "Etc/GMT+5"),
("Etc/GMT+6", "Etc/GMT+6"),
("Etc/GMT+7", "Etc/GMT+7"),
("Etc/GMT+8", "Etc/GMT+8"),
("Etc/GMT+9", "Etc/GMT+9"),
("Etc/GMT-0", "Etc/GMT-0"),
("Etc/GMT-1", "Etc/GMT-1"),
("Etc/GMT-10", "Etc/GMT-10"),
("Etc/GMT-11", "Etc/GMT-11"),
("Etc/GMT-12", "Etc/GMT-12"),
("Etc/GMT-13", "Etc/GMT-13"),
("Etc/GMT-14", "Etc/GMT-14"),
("Etc/GMT-2", "Etc/GMT-2"),
("Etc/GMT-3", "Etc/GMT-3"),
("Etc/GMT-4", "Etc/GMT-4"),
("Etc/GMT-5", "Etc/GMT-5"),
("Etc/GMT-6", "Etc/GMT-6"),
("Etc/GMT-7", "Etc/GMT-7"),
("Etc/GMT-8", "Etc/GMT-8"),
("Etc/GMT-9", "Etc/GMT-9"),
("Etc/GMT0", "Etc/GMT0"),
("Etc/Greenwich", "Etc/Greenwich"),
("Etc/UCT", "Etc/UCT"),
("Etc/UTC", "Etc/UTC"),
("Etc/Universal", "Etc/Universal"),
("Etc/Zulu", "Etc/Zulu"),
("Europe/Amsterdam", "Europe/Amsterdam"),
("Europe/Andorra", "Europe/Andorra"),
("Europe/Astrakhan", "Europe/Astrakhan"),
("Europe/Athens", "Europe/Athens"),
("Europe/Belfast", "Europe/Belfast"),
("Europe/Belgrade", "Europe/Belgrade"),
("Europe/Berlin", "Europe/Berlin"),
("Europe/Bratislava", "Europe/Bratislava"),
("Europe/Brussels", "Europe/Brussels"),
("Europe/Bucharest", "Europe/Bucharest"),
("Europe/Budapest", "Europe/Budapest"),
("Europe/Busingen", "Europe/Busingen"),
("Europe/Chisinau", "Europe/Chisinau"),
("Europe/Copenhagen", "Europe/Copenhagen"),
("Europe/Dublin", "Europe/Dublin"),
("Europe/Gibraltar", "Europe/Gibraltar"),
("Europe/Guernsey", "Europe/Guernsey"),
("Europe/Helsinki", "Europe/Helsinki"),
("Europe/Isle_of_Man", "Europe/Isle_of_Man"),
("Europe/Istanbul", "Europe/Istanbul"),
("Europe/Jersey", "Europe/Jersey"),
("Europe/Kaliningrad", "Europe/Kaliningrad"),
("Europe/Kiev", "Europe/Kiev"),
("Europe/Kirov", "Europe/Kirov"),
("Europe/Lisbon", "Europe/Lisbon"),
("Europe/Ljubljana", "Europe/Ljubljana"),
("Europe/London", "Europe/London"),
("Europe/Luxembourg", "Europe/Luxembourg"),
("Europe/Madrid", "Europe/Madrid"),
("Europe/Malta", "Europe/Malta"),
("Europe/Mariehamn", "Europe/Mariehamn"),
("Europe/Minsk", "Europe/Minsk"),
("Europe/Monaco", "Europe/Monaco"),
("Europe/Moscow", "Europe/Moscow"),
("Europe/Nicosia", "Europe/Nicosia"),
("Europe/Oslo", "Europe/Oslo"),
("Europe/Paris", "Europe/Paris"),
("Europe/Podgorica", "Europe/Podgorica"),
("Europe/Prague", "Europe/Prague"),
("Europe/Riga", "Europe/Riga"),
("Europe/Rome", "Europe/Rome"),
("Europe/Samara", "Europe/Samara"),
("Europe/San_Marino", "Europe/San_Marino"),
("Europe/Sarajevo", "Europe/Sarajevo"),
("Europe/Saratov", "Europe/Saratov"),
("Europe/Simferopol", "Europe/Simferopol"),
("Europe/Skopje", "Europe/Skopje"),
("Europe/Sofia", "Europe/Sofia"),
("Europe/Stockholm", "Europe/Stockholm"),
("Europe/Tallinn", "Europe/Tallinn"),
("Europe/Tirane", "Europe/Tirane"),
("Europe/Tiraspol", "Europe/Tiraspol"),
("Europe/Ulyanovsk", "Europe/Ulyanovsk"),
("Europe/Uzhgorod", "Europe/Uzhgorod"),
("Europe/Vaduz", "Europe/Vaduz"),
("Europe/Vatican", "Europe/Vatican"),
("Europe/Vienna", "Europe/Vienna"),
("Europe/Vilnius", "Europe/Vilnius"),
("Europe/Volgograd", "Europe/Volgograd"),
("Europe/Warsaw", "Europe/Warsaw"),
("Europe/Zagreb", "Europe/Zagreb"),
("Europe/Zaporozhye", "Europe/Zaporozhye"),
("Europe/Zurich", "Europe/Zurich"),
("GB", "GB"),
("GB-Eire", "GB-Eire"),
("GMT", "GMT"),
("GMT+0", "GMT+0"),
("GMT-0", "GMT-0"),
("GMT0", "GMT0"),
("Greenwich", "Greenwich"),
("HST", "HST"),
("Hongkong", "Hongkong"),
("Iceland", "Iceland"),
("Indian/Antananarivo", "Indian/Antananarivo"),
("Indian/Chagos", "Indian/Chagos"),
("Indian/Christmas", "Indian/Christmas"),
("Indian/Cocos", "Indian/Cocos"),
("Indian/Comoro", "Indian/Comoro"),
("Indian/Kerguelen", "Indian/Kerguelen"),
("Indian/Mahe", "Indian/Mahe"),
("Indian/Maldives", "Indian/Maldives"),
("Indian/Mauritius", "Indian/Mauritius"),
("Indian/Mayotte", "Indian/Mayotte"),
("Indian/Reunion", "Indian/Reunion"),
("Iran", "Iran"),
("Israel", "Israel"),
("Jamaica", "Jamaica"),
("Japan", "Japan"),
("Kwajalein", "Kwajalein"),
("Libya", "Libya"),
("MET", "MET"),
("MST", "MST"),
("MST7MDT", "MST7MDT"),
("Mexico/BajaNorte", "Mexico/BajaNorte"),
("Mexico/BajaSur", "Mexico/BajaSur"),
("Mexico/General", "Mexico/General"),
("NZ", "NZ"),
("NZ-CHAT", "NZ-CHAT"),
("Navajo", "Navajo"),
("PRC", "PRC"),
("PST8PDT", "PST8PDT"),
("Pacific/Apia", "Pacific/Apia"),
("Pacific/Auckland", "Pacific/Auckland"),
("Pacific/Bougainville", "Pacific/Bougainville"),
("Pacific/Chatham", "Pacific/Chatham"),
("Pacific/Chuuk", "Pacific/Chuuk"),
("Pacific/Easter", "Pacific/Easter"),
("Pacific/Efate", "Pacific/Efate"),
("Pacific/Enderbury", "Pacific/Enderbury"),
("Pacific/Fakaofo", "Pacific/Fakaofo"),
("Pacific/Fiji", "Pacific/Fiji"),
("Pacific/Funafuti", "Pacific/Funafuti"),
("Pacific/Galapagos", "Pacific/Galapagos"),
("Pacific/Gambier", "Pacific/Gambier"),
("Pacific/Guadalcanal", "Pacific/Guadalcanal"),
("Pacific/Guam", "Pacific/Guam"),
("Pacific/Honolulu", "Pacific/Honolulu"),
("Pacific/Johnston", "Pacific/Johnston"),
("Pacific/Kanton", "Pacific/Kanton"),
("Pacific/Kiritimati", "Pacific/Kiritimati"),
("Pacific/Kosrae", "Pacific/Kosrae"),
("Pacific/Kwajalein", "Pacific/Kwajalein"),
("Pacific/Majuro", "Pacific/Majuro"),
("Pacific/Marquesas", "Pacific/Marquesas"),
("Pacific/Midway", "Pacific/Midway"),
("Pacific/Nauru", "Pacific/Nauru"),
("Pacific/Niue", "Pacific/Niue"),
("Pacific/Norfolk", "Pacific/Norfolk"),
("Pacific/Noumea", "Pacific/Noumea"),
("Pacific/Pago_Pago", "Pacific/Pago_Pago"),
("Pacific/Palau", "Pacific/Palau"),
("Pacific/Pitcairn", "Pacific/Pitcairn"),
("Pacific/Pohnpei", "Pacific/Pohnpei"),
("Pacific/Ponape", "Pacific/Ponape"),
("Pacific/Port_Moresby", "Pacific/Port_Moresby"),
("Pacific/Rarotonga", "Pacific/Rarotonga"),
("Pacific/Saipan", "Pacific/Saipan"),
("Pacific/Samoa", "Pacific/Samoa"),
("Pacific/Tahiti", "Pacific/Tahiti"),
("Pacific/Tarawa", "Pacific/Tarawa"),
("Pacific/Tongatapu", "Pacific/Tongatapu"),
("Pacific/Truk", "Pacific/Truk"),
("Pacific/Wake", "Pacific/Wake"),
("Pacific/Wallis", "Pacific/Wallis"),
("Pacific/Yap", "Pacific/Yap"),
("Poland", "Poland"),
("Portugal", "Portugal"),
("ROC", "ROC"),
("ROK", "ROK"),
("Singapore", "Singapore"),
("Turkey", "Turkey"),
("UCT", "UCT"),
("US/Alaska", "US/Alaska"),
("US/Aleutian", "US/Aleutian"),
("US/Arizona", "US/Arizona"),
("US/Central", "US/Central"),
("US/East-Indiana", "US/East-Indiana"),
("US/Eastern", "US/Eastern"),
("US/Hawaii", "US/Hawaii"),
("US/Indiana-Starke", "US/Indiana-Starke"),
("US/Michigan", "US/Michigan"),
("US/Mountain", "US/Mountain"),
("US/Pacific", "US/Pacific"),
("US/Samoa", "US/Samoa"),
("UTC", "UTC"),
("Universal", "Universal"),
("W-SU", "W-SU"),
("WET", "WET"),
("Zulu", "Zulu"),
],
default=django.utils.timezone.get_current_timezone,
help_text="timezone used for the date",
max_length=100,
verbose_name="timezone",
),
),
migrations.AlterField(
model_name="sound",
name="id",
field=models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
migrations.AlterField(
model_name="sound",
name="program",
field=models.ForeignKey(
blank=True,
help_text="program related to it",
on_delete=django.db.models.deletion.CASCADE,
to="aircox.program",
verbose_name="program",
),
),
migrations.AlterField(
model_name="staticpage",
name="id",
field=models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
migrations.AlterField(
model_name="station",
name="id",
field=models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
migrations.AlterField(
model_name="stream",
name="id",
field=models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
migrations.AlterField(
model_name="track",
name="id",
field=models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
]

View File

@ -0,0 +1,23 @@
# Generated by Django 3.2.12 on 2022-03-26 15:21
import aircox.models.sound
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("aircox", "0005_auto_20220318_1205"),
]
operations = [
migrations.AlterField(
model_name="sound",
name="file",
field=models.FileField(
db_index=True,
max_length=256,
upload_to=aircox.models.sound.Sound._upload_to,
verbose_name="file",
),
),
]

View File

@ -0,0 +1,710 @@
# Generated by Django 4.1 on 2022-10-06 13:47
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
("aircox", "0006_alter_sound_file"),
]
operations = [
migrations.AddField(
model_name="sound",
name="is_downloadable",
field=models.BooleanField(
default=False,
help_text="whether it can be publicly downloaded by visitors (sound must be public)",
verbose_name="downloadable",
),
),
migrations.AlterField(
model_name="page",
name="pub_date",
field=models.DateTimeField(
blank=True,
db_index=True,
null=True,
verbose_name="publication date",
),
),
migrations.AlterField(
model_name="schedule",
name="timezone",
field=models.CharField(
choices=[
("Africa/Abidjan", "Africa/Abidjan"),
("Africa/Accra", "Africa/Accra"),
("Africa/Addis_Ababa", "Africa/Addis_Ababa"),
("Africa/Algiers", "Africa/Algiers"),
("Africa/Asmara", "Africa/Asmara"),
("Africa/Asmera", "Africa/Asmera"),
("Africa/Bamako", "Africa/Bamako"),
("Africa/Bangui", "Africa/Bangui"),
("Africa/Banjul", "Africa/Banjul"),
("Africa/Bissau", "Africa/Bissau"),
("Africa/Blantyre", "Africa/Blantyre"),
("Africa/Brazzaville", "Africa/Brazzaville"),
("Africa/Bujumbura", "Africa/Bujumbura"),
("Africa/Cairo", "Africa/Cairo"),
("Africa/Casablanca", "Africa/Casablanca"),
("Africa/Ceuta", "Africa/Ceuta"),
("Africa/Conakry", "Africa/Conakry"),
("Africa/Dakar", "Africa/Dakar"),
("Africa/Dar_es_Salaam", "Africa/Dar_es_Salaam"),
("Africa/Djibouti", "Africa/Djibouti"),
("Africa/Douala", "Africa/Douala"),
("Africa/El_Aaiun", "Africa/El_Aaiun"),
("Africa/Freetown", "Africa/Freetown"),
("Africa/Gaborone", "Africa/Gaborone"),
("Africa/Harare", "Africa/Harare"),
("Africa/Johannesburg", "Africa/Johannesburg"),
("Africa/Juba", "Africa/Juba"),
("Africa/Kampala", "Africa/Kampala"),
("Africa/Khartoum", "Africa/Khartoum"),
("Africa/Kigali", "Africa/Kigali"),
("Africa/Kinshasa", "Africa/Kinshasa"),
("Africa/Lagos", "Africa/Lagos"),
("Africa/Libreville", "Africa/Libreville"),
("Africa/Lome", "Africa/Lome"),
("Africa/Luanda", "Africa/Luanda"),
("Africa/Lubumbashi", "Africa/Lubumbashi"),
("Africa/Lusaka", "Africa/Lusaka"),
("Africa/Malabo", "Africa/Malabo"),
("Africa/Maputo", "Africa/Maputo"),
("Africa/Maseru", "Africa/Maseru"),
("Africa/Mbabane", "Africa/Mbabane"),
("Africa/Mogadishu", "Africa/Mogadishu"),
("Africa/Monrovia", "Africa/Monrovia"),
("Africa/Nairobi", "Africa/Nairobi"),
("Africa/Ndjamena", "Africa/Ndjamena"),
("Africa/Niamey", "Africa/Niamey"),
("Africa/Nouakchott", "Africa/Nouakchott"),
("Africa/Ouagadougou", "Africa/Ouagadougou"),
("Africa/Porto-Novo", "Africa/Porto-Novo"),
("Africa/Sao_Tome", "Africa/Sao_Tome"),
("Africa/Timbuktu", "Africa/Timbuktu"),
("Africa/Tripoli", "Africa/Tripoli"),
("Africa/Tunis", "Africa/Tunis"),
("Africa/Windhoek", "Africa/Windhoek"),
("America/Adak", "America/Adak"),
("America/Anchorage", "America/Anchorage"),
("America/Anguilla", "America/Anguilla"),
("America/Antigua", "America/Antigua"),
("America/Araguaina", "America/Araguaina"),
(
"America/Argentina/Buenos_Aires",
"America/Argentina/Buenos_Aires",
),
(
"America/Argentina/Catamarca",
"America/Argentina/Catamarca",
),
(
"America/Argentina/ComodRivadavia",
"America/Argentina/ComodRivadavia",
),
("America/Argentina/Cordoba", "America/Argentina/Cordoba"),
("America/Argentina/Jujuy", "America/Argentina/Jujuy"),
(
"America/Argentina/La_Rioja",
"America/Argentina/La_Rioja",
),
("America/Argentina/Mendoza", "America/Argentina/Mendoza"),
(
"America/Argentina/Rio_Gallegos",
"America/Argentina/Rio_Gallegos",
),
("America/Argentina/Salta", "America/Argentina/Salta"),
(
"America/Argentina/San_Juan",
"America/Argentina/San_Juan",
),
(
"America/Argentina/San_Luis",
"America/Argentina/San_Luis",
),
("America/Argentina/Tucuman", "America/Argentina/Tucuman"),
("America/Argentina/Ushuaia", "America/Argentina/Ushuaia"),
("America/Aruba", "America/Aruba"),
("America/Asuncion", "America/Asuncion"),
("America/Atikokan", "America/Atikokan"),
("America/Atka", "America/Atka"),
("America/Bahia", "America/Bahia"),
("America/Bahia_Banderas", "America/Bahia_Banderas"),
("America/Barbados", "America/Barbados"),
("America/Belem", "America/Belem"),
("America/Belize", "America/Belize"),
("America/Blanc-Sablon", "America/Blanc-Sablon"),
("America/Boa_Vista", "America/Boa_Vista"),
("America/Bogota", "America/Bogota"),
("America/Boise", "America/Boise"),
("America/Buenos_Aires", "America/Buenos_Aires"),
("America/Cambridge_Bay", "America/Cambridge_Bay"),
("America/Campo_Grande", "America/Campo_Grande"),
("America/Cancun", "America/Cancun"),
("America/Caracas", "America/Caracas"),
("America/Catamarca", "America/Catamarca"),
("America/Cayenne", "America/Cayenne"),
("America/Cayman", "America/Cayman"),
("America/Chicago", "America/Chicago"),
("America/Chihuahua", "America/Chihuahua"),
("America/Coral_Harbour", "America/Coral_Harbour"),
("America/Cordoba", "America/Cordoba"),
("America/Costa_Rica", "America/Costa_Rica"),
("America/Creston", "America/Creston"),
("America/Cuiaba", "America/Cuiaba"),
("America/Curacao", "America/Curacao"),
("America/Danmarkshavn", "America/Danmarkshavn"),
("America/Dawson", "America/Dawson"),
("America/Dawson_Creek", "America/Dawson_Creek"),
("America/Denver", "America/Denver"),
("America/Detroit", "America/Detroit"),
("America/Dominica", "America/Dominica"),
("America/Edmonton", "America/Edmonton"),
("America/Eirunepe", "America/Eirunepe"),
("America/El_Salvador", "America/El_Salvador"),
("America/Ensenada", "America/Ensenada"),
("America/Fort_Nelson", "America/Fort_Nelson"),
("America/Fort_Wayne", "America/Fort_Wayne"),
("America/Fortaleza", "America/Fortaleza"),
("America/Glace_Bay", "America/Glace_Bay"),
("America/Godthab", "America/Godthab"),
("America/Goose_Bay", "America/Goose_Bay"),
("America/Grand_Turk", "America/Grand_Turk"),
("America/Grenada", "America/Grenada"),
("America/Guadeloupe", "America/Guadeloupe"),
("America/Guatemala", "America/Guatemala"),
("America/Guayaquil", "America/Guayaquil"),
("America/Guyana", "America/Guyana"),
("America/Halifax", "America/Halifax"),
("America/Havana", "America/Havana"),
("America/Hermosillo", "America/Hermosillo"),
(
"America/Indiana/Indianapolis",
"America/Indiana/Indianapolis",
),
("America/Indiana/Knox", "America/Indiana/Knox"),
("America/Indiana/Marengo", "America/Indiana/Marengo"),
(
"America/Indiana/Petersburg",
"America/Indiana/Petersburg",
),
("America/Indiana/Tell_City", "America/Indiana/Tell_City"),
("America/Indiana/Vevay", "America/Indiana/Vevay"),
("America/Indiana/Vincennes", "America/Indiana/Vincennes"),
("America/Indiana/Winamac", "America/Indiana/Winamac"),
("America/Indianapolis", "America/Indianapolis"),
("America/Inuvik", "America/Inuvik"),
("America/Iqaluit", "America/Iqaluit"),
("America/Jamaica", "America/Jamaica"),
("America/Jujuy", "America/Jujuy"),
("America/Juneau", "America/Juneau"),
(
"America/Kentucky/Louisville",
"America/Kentucky/Louisville",
),
(
"America/Kentucky/Monticello",
"America/Kentucky/Monticello",
),
("America/Knox_IN", "America/Knox_IN"),
("America/Kralendijk", "America/Kralendijk"),
("America/La_Paz", "America/La_Paz"),
("America/Lima", "America/Lima"),
("America/Los_Angeles", "America/Los_Angeles"),
("America/Louisville", "America/Louisville"),
("America/Lower_Princes", "America/Lower_Princes"),
("America/Maceio", "America/Maceio"),
("America/Managua", "America/Managua"),
("America/Manaus", "America/Manaus"),
("America/Marigot", "America/Marigot"),
("America/Martinique", "America/Martinique"),
("America/Matamoros", "America/Matamoros"),
("America/Mazatlan", "America/Mazatlan"),
("America/Mendoza", "America/Mendoza"),
("America/Menominee", "America/Menominee"),
("America/Merida", "America/Merida"),
("America/Metlakatla", "America/Metlakatla"),
("America/Mexico_City", "America/Mexico_City"),
("America/Miquelon", "America/Miquelon"),
("America/Moncton", "America/Moncton"),
("America/Monterrey", "America/Monterrey"),
("America/Montevideo", "America/Montevideo"),
("America/Montreal", "America/Montreal"),
("America/Montserrat", "America/Montserrat"),
("America/Nassau", "America/Nassau"),
("America/New_York", "America/New_York"),
("America/Nipigon", "America/Nipigon"),
("America/Nome", "America/Nome"),
("America/Noronha", "America/Noronha"),
(
"America/North_Dakota/Beulah",
"America/North_Dakota/Beulah",
),
(
"America/North_Dakota/Center",
"America/North_Dakota/Center",
),
(
"America/North_Dakota/New_Salem",
"America/North_Dakota/New_Salem",
),
("America/Nuuk", "America/Nuuk"),
("America/Ojinaga", "America/Ojinaga"),
("America/Panama", "America/Panama"),
("America/Pangnirtung", "America/Pangnirtung"),
("America/Paramaribo", "America/Paramaribo"),
("America/Phoenix", "America/Phoenix"),
("America/Port-au-Prince", "America/Port-au-Prince"),
("America/Port_of_Spain", "America/Port_of_Spain"),
("America/Porto_Acre", "America/Porto_Acre"),
("America/Porto_Velho", "America/Porto_Velho"),
("America/Puerto_Rico", "America/Puerto_Rico"),
("America/Punta_Arenas", "America/Punta_Arenas"),
("America/Rainy_River", "America/Rainy_River"),
("America/Rankin_Inlet", "America/Rankin_Inlet"),
("America/Recife", "America/Recife"),
("America/Regina", "America/Regina"),
("America/Resolute", "America/Resolute"),
("America/Rio_Branco", "America/Rio_Branco"),
("America/Rosario", "America/Rosario"),
("America/Santa_Isabel", "America/Santa_Isabel"),
("America/Santarem", "America/Santarem"),
("America/Santiago", "America/Santiago"),
("America/Santo_Domingo", "America/Santo_Domingo"),
("America/Sao_Paulo", "America/Sao_Paulo"),
("America/Scoresbysund", "America/Scoresbysund"),
("America/Shiprock", "America/Shiprock"),
("America/Sitka", "America/Sitka"),
("America/St_Barthelemy", "America/St_Barthelemy"),
("America/St_Johns", "America/St_Johns"),
("America/St_Kitts", "America/St_Kitts"),
("America/St_Lucia", "America/St_Lucia"),
("America/St_Thomas", "America/St_Thomas"),
("America/St_Vincent", "America/St_Vincent"),
("America/Swift_Current", "America/Swift_Current"),
("America/Tegucigalpa", "America/Tegucigalpa"),
("America/Thule", "America/Thule"),
("America/Thunder_Bay", "America/Thunder_Bay"),
("America/Tijuana", "America/Tijuana"),
("America/Toronto", "America/Toronto"),
("America/Tortola", "America/Tortola"),
("America/Vancouver", "America/Vancouver"),
("America/Virgin", "America/Virgin"),
("America/Whitehorse", "America/Whitehorse"),
("America/Winnipeg", "America/Winnipeg"),
("America/Yakutat", "America/Yakutat"),
("America/Yellowknife", "America/Yellowknife"),
("Antarctica/Casey", "Antarctica/Casey"),
("Antarctica/Davis", "Antarctica/Davis"),
("Antarctica/DumontDUrville", "Antarctica/DumontDUrville"),
("Antarctica/Macquarie", "Antarctica/Macquarie"),
("Antarctica/Mawson", "Antarctica/Mawson"),
("Antarctica/McMurdo", "Antarctica/McMurdo"),
("Antarctica/Palmer", "Antarctica/Palmer"),
("Antarctica/Rothera", "Antarctica/Rothera"),
("Antarctica/South_Pole", "Antarctica/South_Pole"),
("Antarctica/Syowa", "Antarctica/Syowa"),
("Antarctica/Troll", "Antarctica/Troll"),
("Antarctica/Vostok", "Antarctica/Vostok"),
("Arctic/Longyearbyen", "Arctic/Longyearbyen"),
("Asia/Aden", "Asia/Aden"),
("Asia/Almaty", "Asia/Almaty"),
("Asia/Amman", "Asia/Amman"),
("Asia/Anadyr", "Asia/Anadyr"),
("Asia/Aqtau", "Asia/Aqtau"),
("Asia/Aqtobe", "Asia/Aqtobe"),
("Asia/Ashgabat", "Asia/Ashgabat"),
("Asia/Ashkhabad", "Asia/Ashkhabad"),
("Asia/Atyrau", "Asia/Atyrau"),
("Asia/Baghdad", "Asia/Baghdad"),
("Asia/Bahrain", "Asia/Bahrain"),
("Asia/Baku", "Asia/Baku"),
("Asia/Bangkok", "Asia/Bangkok"),
("Asia/Barnaul", "Asia/Barnaul"),
("Asia/Beirut", "Asia/Beirut"),
("Asia/Bishkek", "Asia/Bishkek"),
("Asia/Brunei", "Asia/Brunei"),
("Asia/Calcutta", "Asia/Calcutta"),
("Asia/Chita", "Asia/Chita"),
("Asia/Choibalsan", "Asia/Choibalsan"),
("Asia/Chongqing", "Asia/Chongqing"),
("Asia/Chungking", "Asia/Chungking"),
("Asia/Colombo", "Asia/Colombo"),
("Asia/Dacca", "Asia/Dacca"),
("Asia/Damascus", "Asia/Damascus"),
("Asia/Dhaka", "Asia/Dhaka"),
("Asia/Dili", "Asia/Dili"),
("Asia/Dubai", "Asia/Dubai"),
("Asia/Dushanbe", "Asia/Dushanbe"),
("Asia/Famagusta", "Asia/Famagusta"),
("Asia/Gaza", "Asia/Gaza"),
("Asia/Harbin", "Asia/Harbin"),
("Asia/Hebron", "Asia/Hebron"),
("Asia/Ho_Chi_Minh", "Asia/Ho_Chi_Minh"),
("Asia/Hong_Kong", "Asia/Hong_Kong"),
("Asia/Hovd", "Asia/Hovd"),
("Asia/Irkutsk", "Asia/Irkutsk"),
("Asia/Istanbul", "Asia/Istanbul"),
("Asia/Jakarta", "Asia/Jakarta"),
("Asia/Jayapura", "Asia/Jayapura"),
("Asia/Jerusalem", "Asia/Jerusalem"),
("Asia/Kabul", "Asia/Kabul"),
("Asia/Kamchatka", "Asia/Kamchatka"),
("Asia/Karachi", "Asia/Karachi"),
("Asia/Kashgar", "Asia/Kashgar"),
("Asia/Kathmandu", "Asia/Kathmandu"),
("Asia/Katmandu", "Asia/Katmandu"),
("Asia/Khandyga", "Asia/Khandyga"),
("Asia/Kolkata", "Asia/Kolkata"),
("Asia/Krasnoyarsk", "Asia/Krasnoyarsk"),
("Asia/Kuala_Lumpur", "Asia/Kuala_Lumpur"),
("Asia/Kuching", "Asia/Kuching"),
("Asia/Kuwait", "Asia/Kuwait"),
("Asia/Macao", "Asia/Macao"),
("Asia/Macau", "Asia/Macau"),
("Asia/Magadan", "Asia/Magadan"),
("Asia/Makassar", "Asia/Makassar"),
("Asia/Manila", "Asia/Manila"),
("Asia/Muscat", "Asia/Muscat"),
("Asia/Nicosia", "Asia/Nicosia"),
("Asia/Novokuznetsk", "Asia/Novokuznetsk"),
("Asia/Novosibirsk", "Asia/Novosibirsk"),
("Asia/Omsk", "Asia/Omsk"),
("Asia/Oral", "Asia/Oral"),
("Asia/Phnom_Penh", "Asia/Phnom_Penh"),
("Asia/Pontianak", "Asia/Pontianak"),
("Asia/Pyongyang", "Asia/Pyongyang"),
("Asia/Qatar", "Asia/Qatar"),
("Asia/Qostanay", "Asia/Qostanay"),
("Asia/Qyzylorda", "Asia/Qyzylorda"),
("Asia/Rangoon", "Asia/Rangoon"),
("Asia/Riyadh", "Asia/Riyadh"),
("Asia/Saigon", "Asia/Saigon"),
("Asia/Sakhalin", "Asia/Sakhalin"),
("Asia/Samarkand", "Asia/Samarkand"),
("Asia/Seoul", "Asia/Seoul"),
("Asia/Shanghai", "Asia/Shanghai"),
("Asia/Singapore", "Asia/Singapore"),
("Asia/Srednekolymsk", "Asia/Srednekolymsk"),
("Asia/Taipei", "Asia/Taipei"),
("Asia/Tashkent", "Asia/Tashkent"),
("Asia/Tbilisi", "Asia/Tbilisi"),
("Asia/Tehran", "Asia/Tehran"),
("Asia/Tel_Aviv", "Asia/Tel_Aviv"),
("Asia/Thimbu", "Asia/Thimbu"),
("Asia/Thimphu", "Asia/Thimphu"),
("Asia/Tokyo", "Asia/Tokyo"),
("Asia/Tomsk", "Asia/Tomsk"),
("Asia/Ujung_Pandang", "Asia/Ujung_Pandang"),
("Asia/Ulaanbaatar", "Asia/Ulaanbaatar"),
("Asia/Ulan_Bator", "Asia/Ulan_Bator"),
("Asia/Urumqi", "Asia/Urumqi"),
("Asia/Ust-Nera", "Asia/Ust-Nera"),
("Asia/Vientiane", "Asia/Vientiane"),
("Asia/Vladivostok", "Asia/Vladivostok"),
("Asia/Yakutsk", "Asia/Yakutsk"),
("Asia/Yangon", "Asia/Yangon"),
("Asia/Yekaterinburg", "Asia/Yekaterinburg"),
("Asia/Yerevan", "Asia/Yerevan"),
("Atlantic/Azores", "Atlantic/Azores"),
("Atlantic/Bermuda", "Atlantic/Bermuda"),
("Atlantic/Canary", "Atlantic/Canary"),
("Atlantic/Cape_Verde", "Atlantic/Cape_Verde"),
("Atlantic/Faeroe", "Atlantic/Faeroe"),
("Atlantic/Faroe", "Atlantic/Faroe"),
("Atlantic/Jan_Mayen", "Atlantic/Jan_Mayen"),
("Atlantic/Madeira", "Atlantic/Madeira"),
("Atlantic/Reykjavik", "Atlantic/Reykjavik"),
("Atlantic/South_Georgia", "Atlantic/South_Georgia"),
("Atlantic/St_Helena", "Atlantic/St_Helena"),
("Atlantic/Stanley", "Atlantic/Stanley"),
("Australia/ACT", "Australia/ACT"),
("Australia/Adelaide", "Australia/Adelaide"),
("Australia/Brisbane", "Australia/Brisbane"),
("Australia/Broken_Hill", "Australia/Broken_Hill"),
("Australia/Canberra", "Australia/Canberra"),
("Australia/Currie", "Australia/Currie"),
("Australia/Darwin", "Australia/Darwin"),
("Australia/Eucla", "Australia/Eucla"),
("Australia/Hobart", "Australia/Hobart"),
("Australia/LHI", "Australia/LHI"),
("Australia/Lindeman", "Australia/Lindeman"),
("Australia/Lord_Howe", "Australia/Lord_Howe"),
("Australia/Melbourne", "Australia/Melbourne"),
("Australia/NSW", "Australia/NSW"),
("Australia/North", "Australia/North"),
("Australia/Perth", "Australia/Perth"),
("Australia/Queensland", "Australia/Queensland"),
("Australia/South", "Australia/South"),
("Australia/Sydney", "Australia/Sydney"),
("Australia/Tasmania", "Australia/Tasmania"),
("Australia/Victoria", "Australia/Victoria"),
("Australia/West", "Australia/West"),
("Australia/Yancowinna", "Australia/Yancowinna"),
("Brazil/Acre", "Brazil/Acre"),
("Brazil/DeNoronha", "Brazil/DeNoronha"),
("Brazil/East", "Brazil/East"),
("Brazil/West", "Brazil/West"),
("CET", "CET"),
("CST6CDT", "CST6CDT"),
("Canada/Atlantic", "Canada/Atlantic"),
("Canada/Central", "Canada/Central"),
("Canada/Eastern", "Canada/Eastern"),
("Canada/Mountain", "Canada/Mountain"),
("Canada/Newfoundland", "Canada/Newfoundland"),
("Canada/Pacific", "Canada/Pacific"),
("Canada/Saskatchewan", "Canada/Saskatchewan"),
("Canada/Yukon", "Canada/Yukon"),
("Chile/Continental", "Chile/Continental"),
("Chile/EasterIsland", "Chile/EasterIsland"),
("Cuba", "Cuba"),
("EET", "EET"),
("EST", "EST"),
("EST5EDT", "EST5EDT"),
("Egypt", "Egypt"),
("Eire", "Eire"),
("Etc/GMT", "Etc/GMT"),
("Etc/GMT+0", "Etc/GMT+0"),
("Etc/GMT+1", "Etc/GMT+1"),
("Etc/GMT+10", "Etc/GMT+10"),
("Etc/GMT+11", "Etc/GMT+11"),
("Etc/GMT+12", "Etc/GMT+12"),
("Etc/GMT+2", "Etc/GMT+2"),
("Etc/GMT+3", "Etc/GMT+3"),
("Etc/GMT+4", "Etc/GMT+4"),
("Etc/GMT+5", "Etc/GMT+5"),
("Etc/GMT+6", "Etc/GMT+6"),
("Etc/GMT+7", "Etc/GMT+7"),
("Etc/GMT+8", "Etc/GMT+8"),
("Etc/GMT+9", "Etc/GMT+9"),
("Etc/GMT-0", "Etc/GMT-0"),
("Etc/GMT-1", "Etc/GMT-1"),
("Etc/GMT-10", "Etc/GMT-10"),
("Etc/GMT-11", "Etc/GMT-11"),
("Etc/GMT-12", "Etc/GMT-12"),
("Etc/GMT-13", "Etc/GMT-13"),
("Etc/GMT-14", "Etc/GMT-14"),
("Etc/GMT-2", "Etc/GMT-2"),
("Etc/GMT-3", "Etc/GMT-3"),
("Etc/GMT-4", "Etc/GMT-4"),
("Etc/GMT-5", "Etc/GMT-5"),
("Etc/GMT-6", "Etc/GMT-6"),
("Etc/GMT-7", "Etc/GMT-7"),
("Etc/GMT-8", "Etc/GMT-8"),
("Etc/GMT-9", "Etc/GMT-9"),
("Etc/GMT0", "Etc/GMT0"),
("Etc/Greenwich", "Etc/Greenwich"),
("Etc/UCT", "Etc/UCT"),
("Etc/UTC", "Etc/UTC"),
("Etc/Universal", "Etc/Universal"),
("Etc/Zulu", "Etc/Zulu"),
("Europe/Amsterdam", "Europe/Amsterdam"),
("Europe/Andorra", "Europe/Andorra"),
("Europe/Astrakhan", "Europe/Astrakhan"),
("Europe/Athens", "Europe/Athens"),
("Europe/Belfast", "Europe/Belfast"),
("Europe/Belgrade", "Europe/Belgrade"),
("Europe/Berlin", "Europe/Berlin"),
("Europe/Bratislava", "Europe/Bratislava"),
("Europe/Brussels", "Europe/Brussels"),
("Europe/Bucharest", "Europe/Bucharest"),
("Europe/Budapest", "Europe/Budapest"),
("Europe/Busingen", "Europe/Busingen"),
("Europe/Chisinau", "Europe/Chisinau"),
("Europe/Copenhagen", "Europe/Copenhagen"),
("Europe/Dublin", "Europe/Dublin"),
("Europe/Gibraltar", "Europe/Gibraltar"),
("Europe/Guernsey", "Europe/Guernsey"),
("Europe/Helsinki", "Europe/Helsinki"),
("Europe/Isle_of_Man", "Europe/Isle_of_Man"),
("Europe/Istanbul", "Europe/Istanbul"),
("Europe/Jersey", "Europe/Jersey"),
("Europe/Kaliningrad", "Europe/Kaliningrad"),
("Europe/Kiev", "Europe/Kiev"),
("Europe/Kirov", "Europe/Kirov"),
("Europe/Kyiv", "Europe/Kyiv"),
("Europe/Lisbon", "Europe/Lisbon"),
("Europe/Ljubljana", "Europe/Ljubljana"),
("Europe/London", "Europe/London"),
("Europe/Luxembourg", "Europe/Luxembourg"),
("Europe/Madrid", "Europe/Madrid"),
("Europe/Malta", "Europe/Malta"),
("Europe/Mariehamn", "Europe/Mariehamn"),
("Europe/Minsk", "Europe/Minsk"),
("Europe/Monaco", "Europe/Monaco"),
("Europe/Moscow", "Europe/Moscow"),
("Europe/Nicosia", "Europe/Nicosia"),
("Europe/Oslo", "Europe/Oslo"),
("Europe/Paris", "Europe/Paris"),
("Europe/Podgorica", "Europe/Podgorica"),
("Europe/Prague", "Europe/Prague"),
("Europe/Riga", "Europe/Riga"),
("Europe/Rome", "Europe/Rome"),
("Europe/Samara", "Europe/Samara"),
("Europe/San_Marino", "Europe/San_Marino"),
("Europe/Sarajevo", "Europe/Sarajevo"),
("Europe/Saratov", "Europe/Saratov"),
("Europe/Simferopol", "Europe/Simferopol"),
("Europe/Skopje", "Europe/Skopje"),
("Europe/Sofia", "Europe/Sofia"),
("Europe/Stockholm", "Europe/Stockholm"),
("Europe/Tallinn", "Europe/Tallinn"),
("Europe/Tirane", "Europe/Tirane"),
("Europe/Tiraspol", "Europe/Tiraspol"),
("Europe/Ulyanovsk", "Europe/Ulyanovsk"),
("Europe/Uzhgorod", "Europe/Uzhgorod"),
("Europe/Vaduz", "Europe/Vaduz"),
("Europe/Vatican", "Europe/Vatican"),
("Europe/Vienna", "Europe/Vienna"),
("Europe/Vilnius", "Europe/Vilnius"),
("Europe/Volgograd", "Europe/Volgograd"),
("Europe/Warsaw", "Europe/Warsaw"),
("Europe/Zagreb", "Europe/Zagreb"),
("Europe/Zaporozhye", "Europe/Zaporozhye"),
("Europe/Zurich", "Europe/Zurich"),
("GB", "GB"),
("GB-Eire", "GB-Eire"),
("GMT", "GMT"),
("GMT+0", "GMT+0"),
("GMT-0", "GMT-0"),
("GMT0", "GMT0"),
("Greenwich", "Greenwich"),
("HST", "HST"),
("Hongkong", "Hongkong"),
("Iceland", "Iceland"),
("Indian/Antananarivo", "Indian/Antananarivo"),
("Indian/Chagos", "Indian/Chagos"),
("Indian/Christmas", "Indian/Christmas"),
("Indian/Cocos", "Indian/Cocos"),
("Indian/Comoro", "Indian/Comoro"),
("Indian/Kerguelen", "Indian/Kerguelen"),
("Indian/Mahe", "Indian/Mahe"),
("Indian/Maldives", "Indian/Maldives"),
("Indian/Mauritius", "Indian/Mauritius"),
("Indian/Mayotte", "Indian/Mayotte"),
("Indian/Reunion", "Indian/Reunion"),
("Iran", "Iran"),
("Israel", "Israel"),
("Jamaica", "Jamaica"),
("Japan", "Japan"),
("Kwajalein", "Kwajalein"),
("Libya", "Libya"),
("MET", "MET"),
("MST", "MST"),
("MST7MDT", "MST7MDT"),
("Mexico/BajaNorte", "Mexico/BajaNorte"),
("Mexico/BajaSur", "Mexico/BajaSur"),
("Mexico/General", "Mexico/General"),
("NZ", "NZ"),
("NZ-CHAT", "NZ-CHAT"),
("Navajo", "Navajo"),
("PRC", "PRC"),
("PST8PDT", "PST8PDT"),
("Pacific/Apia", "Pacific/Apia"),
("Pacific/Auckland", "Pacific/Auckland"),
("Pacific/Bougainville", "Pacific/Bougainville"),
("Pacific/Chatham", "Pacific/Chatham"),
("Pacific/Chuuk", "Pacific/Chuuk"),
("Pacific/Easter", "Pacific/Easter"),
("Pacific/Efate", "Pacific/Efate"),
("Pacific/Enderbury", "Pacific/Enderbury"),
("Pacific/Fakaofo", "Pacific/Fakaofo"),
("Pacific/Fiji", "Pacific/Fiji"),
("Pacific/Funafuti", "Pacific/Funafuti"),
("Pacific/Galapagos", "Pacific/Galapagos"),
("Pacific/Gambier", "Pacific/Gambier"),
("Pacific/Guadalcanal", "Pacific/Guadalcanal"),
("Pacific/Guam", "Pacific/Guam"),
("Pacific/Honolulu", "Pacific/Honolulu"),
("Pacific/Johnston", "Pacific/Johnston"),
("Pacific/Kanton", "Pacific/Kanton"),
("Pacific/Kiritimati", "Pacific/Kiritimati"),
("Pacific/Kosrae", "Pacific/Kosrae"),
("Pacific/Kwajalein", "Pacific/Kwajalein"),
("Pacific/Majuro", "Pacific/Majuro"),
("Pacific/Marquesas", "Pacific/Marquesas"),
("Pacific/Midway", "Pacific/Midway"),
("Pacific/Nauru", "Pacific/Nauru"),
("Pacific/Niue", "Pacific/Niue"),
("Pacific/Norfolk", "Pacific/Norfolk"),
("Pacific/Noumea", "Pacific/Noumea"),
("Pacific/Pago_Pago", "Pacific/Pago_Pago"),
("Pacific/Palau", "Pacific/Palau"),
("Pacific/Pitcairn", "Pacific/Pitcairn"),
("Pacific/Pohnpei", "Pacific/Pohnpei"),
("Pacific/Ponape", "Pacific/Ponape"),
("Pacific/Port_Moresby", "Pacific/Port_Moresby"),
("Pacific/Rarotonga", "Pacific/Rarotonga"),
("Pacific/Saipan", "Pacific/Saipan"),
("Pacific/Samoa", "Pacific/Samoa"),
("Pacific/Tahiti", "Pacific/Tahiti"),
("Pacific/Tarawa", "Pacific/Tarawa"),
("Pacific/Tongatapu", "Pacific/Tongatapu"),
("Pacific/Truk", "Pacific/Truk"),
("Pacific/Wake", "Pacific/Wake"),
("Pacific/Wallis", "Pacific/Wallis"),
("Pacific/Yap", "Pacific/Yap"),
("Poland", "Poland"),
("Portugal", "Portugal"),
("ROC", "ROC"),
("ROK", "ROK"),
("Singapore", "Singapore"),
("Turkey", "Turkey"),
("UCT", "UCT"),
("US/Alaska", "US/Alaska"),
("US/Aleutian", "US/Aleutian"),
("US/Arizona", "US/Arizona"),
("US/Central", "US/Central"),
("US/East-Indiana", "US/East-Indiana"),
("US/Eastern", "US/Eastern"),
("US/Hawaii", "US/Hawaii"),
("US/Indiana-Starke", "US/Indiana-Starke"),
("US/Michigan", "US/Michigan"),
("US/Mountain", "US/Mountain"),
("US/Pacific", "US/Pacific"),
("US/Samoa", "US/Samoa"),
("UTC", "UTC"),
("Universal", "Universal"),
("W-SU", "W-SU"),
("WET", "WET"),
("Zulu", "Zulu"),
],
default=django.utils.timezone.get_current_timezone,
help_text="timezone used for the date",
max_length=100,
verbose_name="timezone",
),
),
migrations.AlterField(
model_name="sound",
name="is_public",
field=models.BooleanField(
default=False,
help_text="whether it is publicly available as podcast",
verbose_name="public",
),
),
migrations.AlterField(
model_name="stream",
name="begin",
field=models.TimeField(
blank=True,
help_text="used to define a time range this stream is played",
null=True,
verbose_name="begin",
),
),
migrations.AlterField(
model_name="stream",
name="end",
field=models.TimeField(
blank=True,
help_text="used to define a time range this stream is played",
null=True,
verbose_name="end",
),
),
]

View File

@ -0,0 +1,48 @@
# Generated by Django 4.1 on 2022-12-09 13:46
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("aircox", "0007_sound_is_downloadable_alter_page_pub_date_and_more"),
]
operations = [
migrations.AlterModelOptions(
name="diffusion",
options={
"permissions": (
("programming", "edit the diffusions' planification"),
),
"verbose_name": "Diffusion",
"verbose_name_plural": "Diffusions",
},
),
migrations.AddField(
model_name="track",
name="album",
field=models.CharField(
default="", max_length=128, verbose_name="album"
),
),
migrations.AlterField(
model_name="schedule",
name="frequency",
field=models.SmallIntegerField(
choices=[
(0, "ponctual"),
(1, "1st {day} of the month"),
(2, "2nd {day} of the month"),
(4, "3rd {day} of the month"),
(8, "4th {day} of the month"),
(16, "last {day} of the month"),
(5, "1st and 3rd {day} of the month"),
(10, "2nd and 4th {day} of the month"),
(31, "{day}"),
(32, "one {day} on two"),
],
verbose_name="frequency",
),
),
]

View File

@ -0,0 +1,19 @@
# Generated by Django 4.1 on 2022-12-09 13:50
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("aircox", "0008_alter_diffusion_options_track_album_and_more"),
]
operations = [
migrations.AddField(
model_name="track",
name="year",
field=models.IntegerField(
blank=True, null=True, verbose_name="year"
),
),
]

View File

@ -0,0 +1,19 @@
# Generated by Django 4.1 on 2022-12-09 18:13
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("aircox", "0009_track_year"),
]
operations = [
migrations.AlterField(
model_name="track",
name="album",
field=models.CharField(
blank=True, max_length=128, null=True, verbose_name="album"
),
),
]

View File

@ -0,0 +1,48 @@
# Generated by Django 4.1 on 2022-12-11 12:24
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("aircox", "0010_alter_track_album"),
]
operations = [
migrations.CreateModel(
name="UserSettings",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"playlist_editor_columns",
models.JSONField(verbose_name="Playlist Editor Columns"),
),
(
"playlist_editor_sep",
models.CharField(
max_length=16, verbose_name="Playlist Editor Separator"
),
),
(
"user",
models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
related_name="aircox_settings",
to=settings.AUTH_USER_MODEL,
verbose_name="User",
),
),
],
),
]

View File

@ -0,0 +1,33 @@
# Generated by Django 4.1 on 2023-01-25 15:18
import aircox.models.sound
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("aircox", "0011_usersettings"),
]
operations = [
migrations.AlterField(
model_name="sound",
name="file",
field=models.FileField(
db_index=True,
max_length=256,
unique=True,
upload_to=aircox.models.sound.Sound._upload_to,
verbose_name="file",
),
),
migrations.AlterField(
model_name="station",
name="default",
field=models.BooleanField(
default=False,
help_text="use this station as the main one.",
verbose_name="default station",
),
),
]

View File

View File

@ -1,17 +1,11 @@
from . import signals
from .article import Article
from .episode import Diffusion, DiffusionQuerySet, Episode
from .diffusion import Diffusion, DiffusionQuerySet
from .episode import Episode
from .log import Log, LogArchiver, LogQuerySet
from .page import Category, Comment, NavItem, Page, PageQuerySet, StaticPage
from .program import (
BaseRerun,
BaseRerunQuerySet,
Program,
ProgramChildQuerySet,
ProgramQuerySet,
Schedule,
Stream,
)
from .program import Program, ProgramChildQuerySet, ProgramQuerySet, Stream
from .schedule import Schedule
from .sound import Sound, SoundQuerySet, Track
from .station import Port, Station, StationQuerySet
from .user_settings import UserSettings
@ -36,8 +30,6 @@ __all__ = (
"Stream",
"Schedule",
"ProgramChildQuerySet",
"BaseRerun",
"BaseRerunQuerySet",
"Sound",
"SoundQuerySet",
"Track",

282
aircox/models/diffusion.py Normal file
View File

@ -0,0 +1,282 @@
import datetime
from django.db import models
from django.db.models import Q
from django.utils import timezone as tz
from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _
from aircox import utils
from .episode import Episode
from .schedule import Schedule
from .rerun import Rerun, RerunQuerySet
__all__ = ("Diffusion", "DiffusionQuerySet")
class DiffusionQuerySet(RerunQuerySet):
def episode(self, episode=None, id=None):
"""Diffusions for this episode."""
return (
self.filter(episode=episode)
if id is None
else self.filter(episode__id=id)
)
def on_air(self):
"""On air diffusions."""
return self.filter(type=Diffusion.TYPE_ON_AIR)
# TODO: rename to `datetime`
def now(self, now=None, order=True):
"""Diffusions occuring now."""
now = now or tz.now()
qs = self.filter(start__lte=now, end__gte=now).distinct()
return qs.order_by("start") if order else qs
def date(self, date=None, order=True):
"""Diffusions occuring date."""
date = date or datetime.date.today()
start = tz.datetime.combine(date, datetime.time())
end = tz.datetime.combine(date, datetime.time(23, 59, 59, 999))
# start = tz.get_current_timezone().localize(start)
# end = tz.get_current_timezone().localize(end)
qs = self.filter(start__range=(start, end))
return qs.order_by("start") if order else qs
def at(self, date, order=True):
"""Return diffusions at specified date or datetime."""
return (
self.now(date, order)
if isinstance(date, tz.datetime)
else self.date(date, order)
)
def after(self, date=None):
"""Return a queryset of diffusions that happen after the given date
(default: today)."""
date = utils.date_or_default(date)
if isinstance(date, tz.datetime):
qs = self.filter(Q(start__gte=date) | Q(end__gte=date))
else:
qs = self.filter(Q(start__date__gte=date) | Q(end__date__gte=date))
return qs.order_by("start")
def before(self, date=None):
"""Return a queryset of diffusions that finish before the given date
(default: today)."""
date = utils.date_or_default(date)
if isinstance(date, tz.datetime):
qs = self.filter(start__lt=date)
else:
qs = self.filter(start__date__lt=date)
return qs.order_by("start")
def range(self, start, end):
# FIXME can return dates that are out of range...
return self.after(start).before(end)
class Diffusion(Rerun):
"""A Diffusion is an occurrence of a Program that is scheduled on the
station's timetable. It can be a rerun of a previous diffusion. In such a
case, use rerun's info instead of its own.
A Diffusion without any rerun is named Episode (previously, a
Diffusion was different from an Episode, but in the end, an
episode only has a name, a linked program, and a list of sounds, so we
finally merge theme).
A Diffusion can have different types:
- default: simple diffusion that is planified / did occurred
- unconfirmed: a generated diffusion that has not been confirmed and thus
is not yet planified
- cancel: the diffusion has been canceled
- stop: the diffusion has been manually stopped
"""
objects = DiffusionQuerySet.as_manager()
TYPE_ON_AIR = 0x00
TYPE_UNCONFIRMED = 0x01
TYPE_CANCEL = 0x02
TYPE_CHOICES = (
(TYPE_ON_AIR, _("on air")),
(TYPE_UNCONFIRMED, _("not confirmed")),
(TYPE_CANCEL, _("cancelled")),
)
episode = models.ForeignKey(
Episode,
models.CASCADE,
verbose_name=_("episode"),
)
schedule = models.ForeignKey(
Schedule,
models.CASCADE,
verbose_name=_("schedule"),
blank=True,
null=True,
)
type = models.SmallIntegerField(
verbose_name=_("type"),
default=TYPE_ON_AIR,
choices=TYPE_CHOICES,
)
start = models.DateTimeField(_("start"), db_index=True)
end = models.DateTimeField(_("end"), db_index=True)
# port = models.ForeignKey(
# 'self',
# verbose_name = _('port'),
# blank = True, null = True,
# on_delete=models.SET_NULL,
# help_text = _('use this input port'),
# )
item_template_name = "aircox/widgets/diffusion_item.html"
class Meta:
verbose_name = _("Diffusion")
verbose_name_plural = _("Diffusions")
permissions = (
("programming", _("edit the diffusions' planification")),
)
def __str__(self):
str_ = "{episode} - {date}".format(
episode=self.episode and self.episode.title,
date=self.local_start.strftime("%Y/%m/%d %H:%M%z"),
)
if self.initial:
str_ += " ({})".format(_("rerun"))
return str_
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
if self.is_initial and self.episode != self._initial["episode"]:
self.rerun_set.update(episode=self.episode, program=self.program)
# def save(self, no_check=False, *args, **kwargs):
# if self.start != self._initial['start'] or \
# self.end != self._initial['end']:
# self.check_conflicts()
def save_rerun(self):
self.episode = self.initial.episode
super().save_rerun()
def save_initial(self):
self.program = self.episode.program
@property
def duration(self):
return self.end - self.start
@property
def date(self):
"""Return diffusion start as a date."""
return utils.cast_date(self.start)
@cached_property
def local_start(self):
"""Return a version of self.date that is localized to self.timezone;
This is needed since datetime are stored as UTC date and we want to get
it as local time."""
return tz.localtime(self.start, tz.get_current_timezone())
@property
def local_end(self):
"""Return a version of self.date that is localized to self.timezone;
This is needed since datetime are stored as UTC date and we want to get
it as local time."""
return tz.localtime(self.end, tz.get_current_timezone())
@property
def is_now(self):
"""True if diffusion is currently running."""
now = tz.now()
return (
self.type == self.TYPE_ON_AIR
and self.start <= now
and self.end >= now
)
@property
def is_live(self):
"""True if Diffusion is live (False if there are sounds files)."""
return (
self.type == self.TYPE_ON_AIR
and not self.episode.sound_set.archive().count()
)
def get_playlist(self, **types):
"""Returns sounds as a playlist (list of *local* archive file path).
The given arguments are passed to ``get_sounds``.
"""
from .sound import Sound
return list(
self.get_sounds(**types)
.filter(path__isnull=False, type=Sound.TYPE_ARCHIVE)
.values_list("path", flat=True)
)
def get_sounds(self, **types):
"""Return a queryset of sounds related to this diffusion, ordered by
type then path.
**types: filter on the given sound types name, as `archive=True`
"""
from .sound import Sound
sounds = (self.initial or self).sound_set.order_by("type", "path")
_in = [
getattr(Sound.Type, name) for name, value in types.items() if value
]
return sounds.filter(type__in=_in)
def is_date_in_range(self, date=None):
"""Return true if the given date is in the diffusion's start-end
range."""
date = date or tz.now()
return self.start < date < self.end
def get_conflicts(self):
"""Return conflicting diffusions queryset."""
# conflicts=Diffusion.objects.filter(
# Q(start__lt=OuterRef('start'), end__gt=OuterRef('end')) |
# Q(start__gt=OuterRef('start'), start__lt=OuterRef('end'))
# )
# diffs= Diffusion.objects.annotate(conflict_with=Exists(conflicts))
# .filter(conflict_with=True)
return (
Diffusion.objects.filter(
Q(start__lt=self.start, end__gt=self.start)
| Q(start__gt=self.start, start__lt=self.end)
)
.exclude(pk=self.pk)
.distinct()
)
def check_conflicts(self):
conflicts = self.get_conflicts()
self.conflicts.set(conflicts)
_initial = None
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._initial = {
"start": self.start,
"end": self.end,
"episode": getattr(self, "episode", None),
}

View File

@ -1,24 +1,13 @@
import datetime
from django.db import models
from django.db.models import Q
from django.utils import timezone as tz
from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _
from easy_thumbnails.files import get_thumbnailer
from aircox.conf import settings
from aircox import utils
from .page import Page
from .program import (
BaseRerun,
BaseRerunQuerySet,
ProgramChildQuerySet,
Schedule,
)
from .program import ProgramChildQuerySet
__all__ = ("Episode", "Diffusion", "DiffusionQuerySet")
__all__ = ("Episode",)
class Episode(Page):
@ -90,269 +79,3 @@ class Episode(Page):
return super().get_init_kwargs_from(
page, title=title, program=page, **kwargs
)
class DiffusionQuerySet(BaseRerunQuerySet):
def episode(self, episode=None, id=None):
"""Diffusions for this episode."""
return (
self.filter(episode=episode)
if id is None
else self.filter(episode__id=id)
)
def on_air(self):
"""On air diffusions."""
return self.filter(type=Diffusion.TYPE_ON_AIR)
# TODO: rename to `datetime`
def now(self, now=None, order=True):
"""Diffusions occuring now."""
now = now or tz.now()
qs = self.filter(start__lte=now, end__gte=now).distinct()
return qs.order_by("start") if order else qs
def date(self, date=None, order=True):
"""Diffusions occuring date."""
date = date or datetime.date.today()
start = tz.datetime.combine(date, datetime.time())
end = tz.datetime.combine(date, datetime.time(23, 59, 59, 999))
# start = tz.get_current_timezone().localize(start)
# end = tz.get_current_timezone().localize(end)
qs = self.filter(start__range=(start, end))
return qs.order_by("start") if order else qs
def at(self, date, order=True):
"""Return diffusions at specified date or datetime."""
return (
self.now(date, order)
if isinstance(date, tz.datetime)
else self.date(date, order)
)
def after(self, date=None):
"""Return a queryset of diffusions that happen after the given date
(default: today)."""
date = utils.date_or_default(date)
if isinstance(date, tz.datetime):
qs = self.filter(Q(start__gte=date) | Q(end__gte=date))
else:
qs = self.filter(Q(start__date__gte=date) | Q(end__date__gte=date))
return qs.order_by("start")
def before(self, date=None):
"""Return a queryset of diffusions that finish before the given date
(default: today)."""
date = utils.date_or_default(date)
if isinstance(date, tz.datetime):
qs = self.filter(start__lt=date)
else:
qs = self.filter(start__date__lt=date)
return qs.order_by("start")
def range(self, start, end):
# FIXME can return dates that are out of range...
return self.after(start).before(end)
class Diffusion(BaseRerun):
"""A Diffusion is an occurrence of a Program that is scheduled on the
station's timetable. It can be a rerun of a previous diffusion. In such a
case, use rerun's info instead of its own.
A Diffusion without any rerun is named Episode (previously, a
Diffusion was different from an Episode, but in the end, an
episode only has a name, a linked program, and a list of sounds, so we
finally merge theme).
A Diffusion can have different types:
- default: simple diffusion that is planified / did occurred
- unconfirmed: a generated diffusion that has not been confirmed and thus
is not yet planified
- cancel: the diffusion has been canceled
- stop: the diffusion has been manually stopped
"""
objects = DiffusionQuerySet.as_manager()
TYPE_ON_AIR = 0x00
TYPE_UNCONFIRMED = 0x01
TYPE_CANCEL = 0x02
TYPE_CHOICES = (
(TYPE_ON_AIR, _("on air")),
(TYPE_UNCONFIRMED, _("not confirmed")),
(TYPE_CANCEL, _("cancelled")),
)
episode = models.ForeignKey(
Episode,
models.CASCADE,
verbose_name=_("episode"),
)
schedule = models.ForeignKey(
Schedule,
models.CASCADE,
verbose_name=_("schedule"),
blank=True,
null=True,
)
type = models.SmallIntegerField(
verbose_name=_("type"),
default=TYPE_ON_AIR,
choices=TYPE_CHOICES,
)
start = models.DateTimeField(_("start"), db_index=True)
end = models.DateTimeField(_("end"), db_index=True)
# port = models.ForeignKey(
# 'self',
# verbose_name = _('port'),
# blank = True, null = True,
# on_delete=models.SET_NULL,
# help_text = _('use this input port'),
# )
item_template_name = "aircox/widgets/diffusion_item.html"
class Meta:
verbose_name = _("Diffusion")
verbose_name_plural = _("Diffusions")
permissions = (
("programming", _("edit the diffusions' planification")),
)
def __str__(self):
str_ = "{episode} - {date}".format(
episode=self.episode and self.episode.title,
date=self.local_start.strftime("%Y/%m/%d %H:%M%z"),
)
if self.initial:
str_ += " ({})".format(_("rerun"))
return str_
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
if self.is_initial and self.episode != self._initial["episode"]:
self.rerun_set.update(episode=self.episode, program=self.program)
# def save(self, no_check=False, *args, **kwargs):
# if self.start != self._initial['start'] or \
# self.end != self._initial['end']:
# self.check_conflicts()
def save_rerun(self):
self.episode = self.initial.episode
self.program = self.episode.program
def save_initial(self):
self.program = self.episode.program
@property
def duration(self):
return self.end - self.start
@property
def date(self):
"""Return diffusion start as a date."""
return utils.cast_date(self.start)
@cached_property
def local_start(self):
"""Return a version of self.date that is localized to self.timezone;
This is needed since datetime are stored as UTC date and we want to get
it as local time."""
return tz.localtime(self.start, tz.get_current_timezone())
@property
def local_end(self):
"""Return a version of self.date that is localized to self.timezone;
This is needed since datetime are stored as UTC date and we want to get
it as local time."""
return tz.localtime(self.end, tz.get_current_timezone())
@property
def is_now(self):
"""True if diffusion is currently running."""
now = tz.now()
return (
self.type == self.TYPE_ON_AIR
and self.start <= now
and self.end >= now
)
@property
def is_live(self):
"""True if Diffusion is live (False if there are sounds files)."""
return (
self.type == self.TYPE_ON_AIR
and not self.episode.sound_set.archive().count()
)
def get_playlist(self, **types):
"""Returns sounds as a playlist (list of *local* archive file path).
The given arguments are passed to ``get_sounds``.
"""
from .sound import Sound
return list(
self.get_sounds(**types)
.filter(path__isnull=False, type=Sound.TYPE_ARCHIVE)
.values_list("path", flat=True)
)
def get_sounds(self, **types):
"""Return a queryset of sounds related to this diffusion, ordered by
type then path.
**types: filter on the given sound types name, as `archive=True`
"""
from .sound import Sound
sounds = (self.initial or self).sound_set.order_by("type", "path")
_in = [
getattr(Sound.Type, name) for name, value in types.items() if value
]
return sounds.filter(type__in=_in)
def is_date_in_range(self, date=None):
"""Return true if the given date is in the diffusion's start-end
range."""
date = date or tz.now()
return self.start < date < self.end
def get_conflicts(self):
"""Return conflicting diffusions queryset."""
# conflicts=Diffusion.objects.filter(
# Q(start__lt=OuterRef('start'), end__gt=OuterRef('end')) |
# Q(start__gt=OuterRef('start'), start__lt=OuterRef('end'))
# )
# diffs= Diffusion.objects.annotate(conflict_with=Exists(conflicts))
# .filter(conflict_with=True)
return (
Diffusion.objects.filter(
Q(start__lt=self.start, end__gt=self.start)
| Q(start__gt=self.start, start__lt=self.end)
)
.exclude(pk=self.pk)
.distinct()
)
def check_conflicts(self):
conflicts = self.get_conflicts()
self.conflicts.set(conflicts)
_initial = None
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._initial = {
"start": self.start,
"end": self.end,
"episode": getattr(self, "episode", None),
}

View File

@ -13,7 +13,7 @@ from django.utils.translation import gettext_lazy as _
from aircox.conf import settings
__all__ = ("Settings", "settings")
from .episode import Diffusion
from .diffusion import Diffusion
from .sound import Sound, Track
from .station import Station

View File

@ -1,21 +1,13 @@
import calendar
import logging
import os
import shutil
from collections import OrderedDict
from enum import IntEnum
import pytz
from django.conf import settings as conf
from django.core.exceptions import ValidationError
from django.db import models
from django.db.models import F
from django.db.models.functions import Concat, Substr
from django.utils import timezone as tz
from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _
from aircox import utils
from aircox.conf import settings
from .page import Page, PageQuerySet
@ -26,12 +18,9 @@ logger = logging.getLogger("aircox")
__all__ = (
"Program",
"ProgramChildQuerySet",
"ProgramQuerySet",
"Stream",
"Schedule",
"ProgramChildQuerySet",
"BaseRerun",
"BaseRerunQuerySet",
)
@ -167,352 +156,6 @@ class ProgramChildQuerySet(PageQuerySet):
return self.parent(program, id)
class BaseRerunQuerySet(models.QuerySet):
"""Queryset for BaseRerun (sub)classes."""
def station(self, station=None, id=None):
return (
self.filter(program__station=station)
if id is None
else self.filter(program__station__id=id)
)
def program(self, program=None, id=None):
return (
self.filter(program=program)
if id is None
else self.filter(program__id=id)
)
def rerun(self):
return self.filter(initial__isnull=False)
def initial(self):
return self.filter(initial__isnull=True)
class BaseRerun(models.Model):
"""Abstract model offering rerun facilities.
Assume `start` is a datetime field or attribute implemented by
subclass.
"""
program = models.ForeignKey(
Program,
models.CASCADE,
db_index=True,
verbose_name=_("related program"),
)
initial = models.ForeignKey(
"self",
models.SET_NULL,
related_name="rerun_set",
verbose_name=_("rerun of"),
limit_choices_to={"initial__isnull": True},
blank=True,
null=True,
db_index=True,
)
objects = BaseRerunQuerySet.as_manager()
class Meta:
abstract = True
def save(self, *args, **kwargs):
if self.initial is not None:
self.initial = self.initial.get_initial()
if self.initial == self:
self.initial = None
if self.is_rerun:
self.save_rerun()
else:
self.save_initial()
super().save(*args, **kwargs)
def save_rerun(self):
pass
def save_initial(self):
pass
@property
def is_initial(self):
return self.initial is None
@property
def is_rerun(self):
return self.initial is not None
def get_initial(self):
"""Return the initial schedule (self or initial)"""
return self if self.initial is None else self.initial.get_initial()
def clean(self):
super().clean()
if self.initial is not None and self.initial.start >= self.start:
raise ValidationError(
{"initial": _("rerun must happen after original")}
)
# ? BIG FIXME: self.date is still used as datetime
class Schedule(BaseRerun):
"""A Schedule defines time slots of programs' diffusions.
It can be an initial run or a rerun (in such case it is linked to
the related schedule).
"""
# Frequency for schedules. Basically, it is a mask of bits where each bit
# is a week. Bits > rank 5 are used for special schedules.
# Important: the first week is always the first week where the weekday of
# the schedule is present.
# For ponctual programs, there is no need for a schedule, only a diffusion
class Frequency(IntEnum):
ponctual = 0b000000
first = 0b000001
second = 0b000010
third = 0b000100
fourth = 0b001000
last = 0b010000
first_and_third = 0b000101
second_and_fourth = 0b001010
every = 0b011111
one_on_two = 0b100000
date = models.DateField(
_("date"),
help_text=_("date of the first diffusion"),
)
time = models.TimeField(
_("time"),
help_text=_("start time"),
)
timezone = models.CharField(
_("timezone"),
default=tz.get_current_timezone,
max_length=100,
choices=[(x, x) for x in pytz.all_timezones],
help_text=_("timezone used for the date"),
)
duration = models.TimeField(
_("duration"),
help_text=_("regular duration"),
)
frequency = models.SmallIntegerField(
_("frequency"),
choices=[
(
int(y),
{
"ponctual": _("ponctual"),
"first": _("1st {day} of the month"),
"second": _("2nd {day} of the month"),
"third": _("3rd {day} of the month"),
"fourth": _("4th {day} of the month"),
"last": _("last {day} of the month"),
"first_and_third": _("1st and 3rd {day} of the month"),
"second_and_fourth": _("2nd and 4th {day} of the month"),
"every": _("{day}"),
"one_on_two": _("one {day} on two"),
}[x],
)
for x, y in Frequency.__members__.items()
],
)
class Meta:
verbose_name = _("Schedule")
verbose_name_plural = _("Schedules")
def __str__(self):
return "{} - {}, {}".format(
self.program.title,
self.get_frequency_verbose(),
self.time.strftime("%H:%M"),
)
def save_rerun(self, *args, **kwargs):
self.program = self.initial.program
self.duration = self.initial.duration
self.frequency = self.initial.frequency
@cached_property
def tz(self):
"""Pytz timezone of the schedule."""
import pytz
return pytz.timezone(self.timezone)
@cached_property
def start(self):
"""Datetime of the start (timezone unaware)"""
return tz.datetime.combine(self.date, self.time)
@cached_property
def end(self):
"""Datetime of the end."""
return self.start + utils.to_timedelta(self.duration)
def get_frequency_verbose(self):
"""Return frequency formated for display."""
from django.template.defaultfilters import date
return (
self.get_frequency_display()
.format(day=date(self.date, "l"))
.capitalize()
)
# initial cached data
__initial = None
def changed(self, fields=["date", "duration", "frequency", "timezone"]):
initial = self._Schedule__initial
if not initial:
return
this = self.__dict__
for field in fields:
if initial.get(field) != this.get(field):
return True
return False
def normalize(self, date):
"""Return a datetime set to schedule's time for the provided date,
handling timezone (based on schedule's timezone)."""
date = tz.datetime.combine(date, self.time)
return self.tz.normalize(self.tz.localize(date))
def dates_of_month(self, date):
"""Return normalized diffusion dates of provided date's month."""
if self.frequency == Schedule.Frequency.ponctual:
return []
sched_wday, freq = self.date.weekday(), self.frequency
date = date.replace(day=1)
# last of the month
if freq == Schedule.Frequency.last:
date = date.replace(
day=calendar.monthrange(date.year, date.month)[1]
)
date_wday = date.weekday()
# end of month before the wanted weekday: move one week back
if date_wday < sched_wday:
date -= tz.timedelta(days=7)
date += tz.timedelta(days=sched_wday - date_wday)
return [self.normalize(date)]
# move to the first day of the month that matches the schedule's
# weekday. Check on SO#3284452 for the formula
date_wday, month = date.weekday(), date.month
date += tz.timedelta(
days=(7 if date_wday > sched_wday else 0) - date_wday + sched_wday
)
if freq == Schedule.Frequency.one_on_two:
# - adjust date with modulo 14 (= 2 weeks in days)
# - there are max 3 "weeks on two" per month
if (date - self.date).days % 14:
date += tz.timedelta(days=7)
dates = (date + tz.timedelta(days=14 * i) for i in range(0, 3))
else:
dates = (
date + tz.timedelta(days=7 * week)
for week in range(0, 5)
if freq & (0b1 << week)
)
return [self.normalize(date) for date in dates if date.month == month]
def _exclude_existing_date(self, dates):
from .episode import Diffusion
saved = set(
Diffusion.objects.filter(start__in=dates).values_list(
"start", flat=True
)
)
return [date for date in dates if date not in saved]
def diffusions_of_month(self, date):
"""Get episodes and diffusions for month of provided date, including
reruns.
:returns: tuple([Episode], [Diffusion])
"""
from .episode import Diffusion, Episode
if (
self.initial is not None
or self.frequency == Schedule.Frequency.ponctual
):
return [], []
# dates for self and reruns as (date, initial)
reruns = [
(rerun, rerun.date - self.date) for rerun in self.rerun_set.all()
]
dates = OrderedDict((date, None) for date in self.dates_of_month(date))
dates.update(
[
(rerun.normalize(date.date() + delta), date)
for date in dates.keys()
for rerun, delta in reruns
]
)
# remove dates corresponding to existing diffusions
saved = set(
Diffusion.objects.filter(
start__in=dates.keys(), program=self.program, schedule=self
).values_list("start", flat=True)
)
# make diffs
duration = utils.to_timedelta(self.duration)
diffusions = {}
episodes = {}
for date, initial in dates.items():
if date in saved:
continue
if initial is None:
episode = Episode.from_page(self.program, date=date)
episode.date = date
episodes[date] = episode
else:
episode = episodes[initial]
initial = diffusions[initial]
diffusions[date] = Diffusion(
episode=episode,
schedule=self,
type=Diffusion.TYPE_ON_AIR,
initial=initial,
start=date,
end=date + duration,
)
return episodes.values(), diffusions.values()
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# TODO/FIXME: use validators?
if self.initial is not None and self.date > self.date:
raise ValueError("initial must be later")
class Stream(models.Model):
"""When there are no program scheduled, it is possible to play sounds in
order to avoid blanks. A Stream is a Program that plays this role, and

106
aircox/models/rerun.py Normal file
View File

@ -0,0 +1,106 @@
from django.core.exceptions import ValidationError
from django.db import models
from django.utils.translation import gettext_lazy as _
from .program import Program
__all__ = (
"Rerun",
"RerunQuerySet",
)
class RerunQuerySet(models.QuerySet):
"""Queryset for Rerun (sub)classes."""
def station(self, station=None, id=None):
return (
self.filter(program__station=station)
if id is None
else self.filter(program__station__id=id)
)
def program(self, program=None, id=None):
return (
self.filter(program=program)
if id is None
else self.filter(program__id=id)
)
def rerun(self):
return self.filter(initial__isnull=False)
def initial(self):
return self.filter(initial__isnull=True)
class Rerun(models.Model):
"""Abstract model offering rerun facilities.
Assume `start` is a datetime field or attribute implemented by
subclass.
"""
program = models.ForeignKey(
Program,
models.CASCADE,
db_index=True,
verbose_name=_("related program"),
)
initial = models.ForeignKey(
"self",
models.SET_NULL,
related_name="rerun_set",
verbose_name=_("rerun of"),
limit_choices_to={"initial__isnull": True},
blank=True,
null=True,
db_index=True,
)
objects = RerunQuerySet.as_manager()
class Meta:
abstract = True
@property
def is_initial(self):
return self.initial is None
@property
def is_rerun(self):
return self.initial is not None
def get_initial(self):
"""Return the initial schedule (self or initial)"""
return self if self.initial is None else self.initial.get_initial()
def clean(self):
super().clean()
if (
hasattr(self, "start")
and self.initial is not None
and self.initial.start >= self.start
):
raise ValidationError(
{"initial": _("rerun must happen after original")}
)
def save_rerun(self):
self.program = self.initial.program
def save_initial(self):
pass
def save(self, *args, **kwargs):
if self.initial is not None:
self.initial = self.initial.get_initial()
if self.initial == self:
self.initial = None
if self.is_rerun:
self.save_rerun()
else:
self.save_initial()
super().save(*args, **kwargs)

217
aircox/models/schedule.py Normal file
View File

@ -0,0 +1,217 @@
import calendar
import pytz
from django.db import models
from django.utils import timezone as tz
from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _
from aircox import utils
from .rerun import Rerun
__all__ = ("Schedule",)
# ? BIG FIXME: self.date is still used as datetime
class Schedule(Rerun):
"""A Schedule defines time slots of programs' diffusions.
It can be an initial run or a rerun (in such case it is linked to
the related schedule).
"""
# Frequency for schedules. Basically, it is a mask of bits where each bit
# is a week. Bits > rank 5 are used for special schedules.
# Important: the first week is always the first week where the weekday of
# the schedule is present.
# For ponctual programs, there is no need for a schedule, only a diffusion
class Frequency(models.IntegerChoices):
ponctual = 0b000000, _("ponctual")
first = 0b000001, _("1st {day} of the month")
second = 0b000010, _("2nd {day} of the month")
third = 0b000100, _("3rd {day} of the month")
fourth = 0b001000, _("4th {day} of the month")
last = 0b010000, _("last {day} of the month")
first_and_third = 0b000101, _("1st and 3rd {day} of the month")
second_and_fourth = 0b001010, _("2nd and 4th {day} of the month")
every = 0b011111, _("{day}")
one_on_two = 0b100000, _("one {day} on two")
date = models.DateField(
_("date"),
help_text=_("date of the first diffusion"),
)
time = models.TimeField(
_("time"),
help_text=_("start time"),
)
timezone = models.CharField(
_("timezone"),
default=lambda: tz.get_current_timezone().zone,
max_length=100,
choices=[(x, x) for x in pytz.all_timezones],
help_text=_("timezone used for the date"),
)
duration = models.TimeField(
_("duration"),
help_text=_("regular duration"),
)
frequency = models.SmallIntegerField(
_("frequency"),
choices=Frequency.choices,
)
class Meta:
verbose_name = _("Schedule")
verbose_name_plural = _("Schedules")
def __str__(self):
return "{} - {}, {}".format(
self.program.title,
self.get_frequency_display(),
self.time.strftime("%H:%M"),
)
def save_rerun(self):
super().save_rerun()
self.duration = self.initial.duration
self.frequency = self.initial.frequency
@cached_property
def tz(self):
"""Pytz timezone of the schedule."""
import pytz
return pytz.timezone(self.timezone)
@cached_property
def start(self):
"""Datetime of the start (timezone unaware)"""
return tz.datetime.combine(self.date, self.time)
@cached_property
def end(self):
"""Datetime of the end."""
return self.start + utils.to_timedelta(self.duration)
def get_frequency_display(self):
"""Return frequency formated for display."""
from django.template.defaultfilters import date
return (
self._get_FIELD_display(self._meta.get_field("frequency"))
.format(day=date(self.date, "l"))
.capitalize()
)
def normalize(self, date):
"""Return a datetime set to schedule's time for the provided date,
handling timezone (based on schedule's timezone)."""
date = tz.datetime.combine(date, self.time)
return self.tz.normalize(self.tz.localize(date))
def dates_of_month(self, date):
"""Return normalized diffusion dates of provided date's month."""
if self.frequency == Schedule.Frequency.ponctual:
return []
sched_wday, freq = self.date.weekday(), self.frequency
date = date.replace(day=1)
# last of the month
if freq == Schedule.Frequency.last:
date = date.replace(
day=calendar.monthrange(date.year, date.month)[1]
)
date_wday = date.weekday()
# end of month before the wanted weekday: move one week back
if date_wday < sched_wday:
date -= tz.timedelta(days=7)
date += tz.timedelta(days=sched_wday - date_wday)
return [self.normalize(date)]
# move to the first day of the month that matches the schedule's
# weekday. Check on SO#3284452 for the formula
date_wday, month = date.weekday(), date.month
date += tz.timedelta(
days=(7 if date_wday > sched_wday else 0) - date_wday + sched_wday
)
if freq == Schedule.Frequency.one_on_two:
# - adjust date with modulo 14 (= 2 weeks in days)
# - there are max 3 "weeks on two" per month
if (date - self.date).days % 14:
date += tz.timedelta(days=7)
dates = (date + tz.timedelta(days=14 * i) for i in range(0, 3))
else:
dates = (
date + tz.timedelta(days=7 * week)
for week in range(0, 5)
if freq & (0b1 << week)
)
return [self.normalize(date) for date in dates if date.month == month]
def diffusions_of_month(self, date):
"""Get episodes and diffusions for month of provided date, including
reruns.
:returns: tuple([Episode], [Diffusion])
"""
from .diffusion import Diffusion
from .episode import Episode
if (
self.initial is not None
or self.frequency == Schedule.Frequency.ponctual
):
return [], []
# dates for self and reruns as (date, initial)
reruns = [
(rerun, rerun.date - self.date) for rerun in self.rerun_set.all()
]
dates = {date: None for date in self.dates_of_month(date)}
dates.update(
(rerun.normalize(date.date() + delta), date)
for date in list(dates.keys())
for rerun, delta in reruns
)
# remove dates corresponding to existing diffusions
saved = set(
Diffusion.objects.filter(
start__in=dates.keys(), program=self.program, schedule=self
).values_list("start", flat=True)
)
# make diffs
duration = utils.to_timedelta(self.duration)
diffusions = {}
episodes = {}
for date, initial in dates.items():
if date in saved:
continue
if initial is None:
episode = Episode.from_page(self.program, date=date)
episode.date = date
episodes[date] = episode
else:
episode = episodes[initial]
initial = diffusions[initial]
diffusions[date] = Diffusion(
episode=episode,
schedule=self,
type=Diffusion.TYPE_ON_AIR,
initial=initial,
start=date,
end=date + duration,
)
return episodes.values(), diffusions.values()

View File

@ -6,9 +6,11 @@ from django.utils import timezone as tz
from aircox import utils
from aircox.conf import settings
from .episode import Episode, Diffusion
from .diffusion import Diffusion
from .episode import Episode
from .page import Page
from .program import Program, Schedule
from .program import Program
from .schedule import Schedule
# Add a default group to a user when it is created. It also assigns a list

View File

@ -43,7 +43,7 @@
<section>
<h4 class="title is-4">{% translate "Diffusions" %}</h4>
{% for schedule in program.schedule_set.all %}
{{ schedule.get_frequency_verbose }}
{{ schedule.get_frequency_display }}
{% with schedule.start|date:"H:i" as start %}
{% with schedule.end|date:"H:i" as end %}
<time datetime="{{ start }}">{{ start }}</time>

72
aircox/tests/conftest.py Normal file
View File

@ -0,0 +1,72 @@
from datetime import time, timedelta
import itertools
import pytest
from model_bakery import baker
from aircox import models
@pytest.fixture
def stations():
return baker.make("aircox.station", _quantity=2)
@pytest.fixture
def programs(stations):
items = list(
itertools.chain(
*(
baker.make("aircox.program", station=station, _quantity=3)
for station in stations
)
)
)
for item in items:
item.save()
return items
@pytest.fixture
def sched_initials(programs):
# use concrete class; timezone is provided in order to ensure DST
items = [
baker.prepare(
"aircox.schedule",
program=program,
time=time(16, 00),
timezone="Europe/Brussels",
)
for program in programs
]
models.Schedule.objects.bulk_create(items)
return items
@pytest.fixture
def sched_reruns(sched_initials):
# use concrete class
items = [
baker.prepare(
"aircox.schedule",
initial=initial,
program=initial.program,
date=initial.date,
time=(initial.start + timedelta(hours=1)).time(),
)
for initial in sched_initials
]
models.Schedule.objects.bulk_create(items)
return items
@pytest.fixture
def schedules(sched_initials, sched_reruns):
return sched_initials + sched_reruns
@pytest.fixture
def episodes(programs):
return [
baker.make("aircox.episode", parent=program) for program in programs
]

View File

@ -0,0 +1,19 @@
import pytest
class TestDiffusionQuerySet:
@pytest.mark.django_db
def test_episode_by_obj(self, episodes):
pass
@pytest.mark.django_db
def test_episode_by_id(self, episodes):
pass
@pytest.mark.django_db
def test_on_air(self, episodes):
pass
@pytest.mark.django_db
def test_now(self, episodes):
pass

View File

@ -0,0 +1,117 @@
from datetime import timedelta
import pytest
from django.core.exceptions import ValidationError
# we use Schedule as concrete class (Rerun is abstract)
from aircox.models import Schedule
class TestRerunQuerySet:
@pytest.mark.django_db
def test_station_by_obj(self, stations, schedules):
for station in stations:
queryset = (
Schedule.objects.station(station)
.distinct()
.values_list("program__station", flat=True)
)
assert queryset.count() == 1
assert queryset.first() == station.pk
@pytest.mark.django_db
def test_station_by_id(self, stations, schedules):
for station in stations:
queryset = (
Schedule.objects.station(id=station.pk)
.distinct()
.values_list("program__station", flat=True)
)
assert queryset.count() == 1
assert queryset.first() == station.pk
@pytest.mark.django_db
def test_program_by_obj(self, programs, schedules):
for program in programs:
queryset = (
Schedule.objects.program(program)
.distinct()
.values_list("program", flat=True)
)
assert queryset.count() == 1
assert queryset.first() == program.pk
@pytest.mark.django_db
def test_program_by_id(self, programs, schedules):
for program in programs:
queryset = (
Schedule.objects.program(id=program.pk)
.distinct()
.values_list("program", flat=True)
)
assert queryset.count() == 1
assert queryset.first() == program.pk
@pytest.mark.django_db
def test_rerun(self, schedules):
queryset = Schedule.objects.rerun().values_list("initial", flat=True)
assert None not in queryset
@pytest.mark.django_db
def test_initial(self, schedules):
queryset = (
Schedule.objects.initial()
.distinct()
.values_list("initial", flat=True)
)
assert queryset.count() == 1
assert queryset.first() is None
class TestRerun:
@pytest.mark.django_db
def test_is_initial_true(self, sched_initials):
assert all(r.is_initial for r in sched_initials)
@pytest.mark.django_db
def test_is_initial_false(self, sched_reruns):
assert all(not r.is_initial for r in sched_reruns)
@pytest.mark.django_db
def test_is_rerun_true(self, sched_reruns):
assert all(r.is_rerun for r in sched_reruns)
@pytest.mark.django_db
def test_is_rerun_false(self, sched_initials):
assert all(not r.is_rerun for r in sched_initials)
@pytest.mark.django_db
def test_get_initial_of_initials(self, sched_initials):
assert all(r.get_initial() is r for r in sched_initials)
@pytest.mark.django_db
def test_get_initial_of_reruns(self, sched_reruns):
assert all(r.get_initial() is r.initial for r in sched_reruns)
@pytest.mark.django_db
def test_clean_success(self, sched_reruns):
for rerun in sched_reruns:
rerun.clean()
@pytest.mark.django_db
def test_clean_fails(self, sched_reruns):
for rerun in sched_reruns:
rerun.time = (rerun.initial.start - timedelta(hours=2)).time()
with pytest.raises(ValidationError):
rerun.clean()
@pytest.mark.django_db
def test_save_rerun(self, sched_reruns):
for rerun in sched_reruns:
rerun.program = None
rerun.save_rerun()
assert rerun.program == rerun.initial.program
# TODO: save()
# save_initial is empty, thus not tested

View File

@ -0,0 +1,135 @@
from datetime import date, datetime, time, timedelta
import pytest
from model_bakery import baker
import calendar
from dateutil.relativedelta import relativedelta
from aircox import utils
from aircox.models import Diffusion, Schedule
class TestSchedule:
@pytest.mark.django_db
def test_save_rerun(self, sched_reruns):
for schedule in sched_reruns:
schedule.duration = None
schedule.frequency = None
schedule.save_rerun()
assert schedule.program == schedule.initial.program
assert schedule.duration == schedule.initial.duration
assert schedule.frequency == schedule.initial.frequency
@pytest.mark.django_db
def test_tz(self, schedules):
for schedule in schedules:
assert schedule.timezone == schedule.tz.zone
@pytest.mark.django_db
def test_start(self, schedules):
for schedule in schedules:
assert schedule.start.date() == schedule.date
assert schedule.start.time() == schedule.time
@pytest.mark.django_db
def test_end(self, schedules):
for schedule in schedules:
delta = utils.to_timedelta(schedule.duration)
assert schedule.end - schedule.start == delta
# def test_get_frequency_display(self):
# pass
@pytest.mark.django_db
def test_normalize(self, schedules):
for schedule in schedules:
dt = datetime.combine(schedule.date, schedule.time)
assert schedule.normalize(dt).tzinfo.zone == schedule.timezone
@pytest.mark.django_db
def test_dates_of_month_ponctual(self):
schedule = baker.prepare(
Schedule, frequency=Schedule.Frequency.ponctual
)
at = schedule.date + relativedelta(months=4)
assert schedule.dates_of_month(at) == []
@pytest.mark.django_db
@pytest.mark.parametrize("months", range(0, 25, 2))
@pytest.mark.parametrize("hour", range(0, 24, 3))
def test_dates_of_month_last(self, months, hour):
schedule = baker.prepare(
Schedule, time=time(hour, 00), frequency=Schedule.Frequency.last
)
at = schedule.date + relativedelta(months=months)
datetimes = schedule.dates_of_month(at)
assert len(datetimes) == 1
dt = datetimes[0]
self._assert_date(schedule, at, dt)
month_info = calendar.monthrange(at.year, at.month)
at = date(at.year, at.month, month_info[1])
if at.weekday() < schedule.date.weekday():
at -= timedelta(days=7)
at += timedelta(days=schedule.date.weekday()) - timedelta(
days=at.weekday()
)
assert dt.date() == at
# since the same method is used for first, second, etc. frequencies
# we assume testing every is sufficient
@pytest.mark.django_db
@pytest.mark.parametrize("months", range(0, 25, 2))
@pytest.mark.parametrize("hour", range(0, 24, 3))
def test_dates_of_month_every(self, months, hour):
schedule = baker.prepare(
Schedule, time=time(hour, 00), frequency=Schedule.Frequency.every
)
at = schedule.date + relativedelta(months=months)
datetimes = schedule.dates_of_month(at)
last = None
for dt in datetimes:
self._assert_date(schedule, at, dt)
if last:
assert (dt - last).days == 7
last = dt
@pytest.mark.django_db
@pytest.mark.parametrize("months", range(0, 25, 2))
@pytest.mark.parametrize("hour", range(0, 24, 3))
def test_dates_of_month_one_on_two(self, months, hour):
schedule = baker.prepare(
Schedule,
time=time(hour, 00),
frequency=Schedule.Frequency.one_on_two,
)
at = schedule.date + relativedelta(months=months)
datetimes = schedule.dates_of_month(at)
for dt in datetimes:
self._assert_date(schedule, at, dt)
delta = dt.date() - schedule.date
assert delta.days % 14 == 0
def _assert_date(self, schedule, at, dt):
assert dt.year == at.year
assert dt.month == at.month
assert dt.weekday() == schedule.date.weekday()
assert dt.time() == schedule.time
assert dt.tzinfo.zone == schedule.timezone
@pytest.mark.django_db
def test_diffusions_of_month(self, sched_initials):
# TODO: test values of initial, rerun
for schedule in sched_initials:
at = schedule.date + timedelta(days=30)
dates = set(schedule.dates_of_month(at))
episodes, diffusions = schedule.diffusions_of_month(at)
assert all(r.date in dates for r in episodes)
assert all(
(not r.initial or r.date in dates)
and r.type == Diffusion.TYPE_ON_AIR
for r in diffusions
)

View File

@ -1,66 +0,0 @@
import calendar
import datetime
import logging
from dateutil.relativedelta import relativedelta
from django.test import TestCase
from django.utils import timezone as tz
from aircox.models import Schedule
logger = logging.getLogger("aircox.test")
logger.setLevel("INFO")
class ScheduleCheck(TestCase):
def setUp(self):
self.schedules = [
Schedule(
date=tz.now(),
duration=datetime.time(1, 30),
frequency=frequency,
)
for frequency in Schedule.Frequency.__members__.values()
]
def test_frequencies(self):
for schedule in self.schedules:
logger.info(
"- test frequency %s" % schedule.get_frequency_display()
)
date = schedule.date
count = 24
while count:
logger.info(
"- month %(month)s/%(year)s"
% {"month": date.month, "year": date.year}
)
count -= 1
dates = schedule.dates_of_month(date)
if schedule.frequency == schedule.Frequency.one_on_two:
self.check_one_on_two(schedule, date, dates)
elif schedule.frequency == schedule.Frequency.last:
self.check_last(schedule, date, dates)
else:
pass
date += relativedelta(months=1)
def check_one_on_two(self, schedule, date, dates):
for date in dates:
delta = date.date() - schedule.date.date()
self.assertEqual(delta.days % 14, 0)
def check_last(self, schedule, date, dates):
month_info = calendar.monthrange(date.year, date.month)
date = datetime.date(date.year, date.month, month_info[1])
# end of month before the wanted weekday: move one week back
if date.weekday() < schedule.date.weekday():
date -= datetime.timedelta(days=7)
date -= datetime.timedelta(days=date.weekday())
date += datetime.timedelta(days=schedule.date.weekday())
self.assertEqual(date, dates[0].date())
def check_n_of_week(self, schedule, date, dates):
pass

View File

@ -1,7 +1,8 @@
from . import admin
from .article import ArticleDetailView, ArticleListView
from .base import BaseAPIView, BaseView
from .episode import DiffusionListView, EpisodeDetailView, EpisodeListView
from .diffusion import DiffusionListView
from .episode import EpisodeDetailView, EpisodeListView
from .home import HomeView
from .log import LogListAPIView, LogListView
from .page import (

30
aircox/views/diffusion.py Normal file
View File

@ -0,0 +1,30 @@
import datetime
from django.views.generic import ListView
from aircox.models import Diffusion, StaticPage
from .base import BaseView
from .mixins import AttachedToMixin, GetDateMixin
__all__ = ("DiffusionListView",)
class DiffusionListView(GetDateMixin, AttachedToMixin, BaseView, ListView):
"""View for timetables."""
model = Diffusion
has_filters = True
redirect_date_url = "diffusion-list"
attach_to_value = StaticPage.ATTACH_TO_DIFFUSIONS
def get_date(self):
date = super().get_date()
return date if date is not None else datetime.date.today()
def get_queryset(self):
return super().get_queryset().date(self.date).order_by("start")
def get_context_data(self, **kwargs):
start = self.date - datetime.timedelta(days=self.date.weekday())
dates = [start + datetime.timedelta(days=i) for i in range(0, 7)]
return super().get_context_data(date=self.date, dates=dates, **kwargs)

View File

@ -1,18 +1,11 @@
import datetime
from django.views.generic import ListView
from ..filters import EpisodeFilters
from ..models import Diffusion, Episode, Program, StaticPage
from .base import BaseView
from .mixins import AttachedToMixin, GetDateMixin
from ..models import Episode, Program, StaticPage
from .page import PageListView
from .program import ProgramPageDetailView
__all__ = (
"EpisodeDetailView",
"EpisodeListView",
"DiffusionListView",
)
@ -32,24 +25,3 @@ class EpisodeListView(PageListView):
has_headline = True
parent_model = Program
attach_to_value = StaticPage.ATTACH_TO_EPISODES
class DiffusionListView(GetDateMixin, AttachedToMixin, BaseView, ListView):
"""View for timetables."""
model = Diffusion
has_filters = True
redirect_date_url = "diffusion-list"
attach_to_value = StaticPage.ATTACH_TO_DIFFUSIONS
def get_date(self):
date = super().get_date()
return date if date is not None else datetime.date.today()
def get_queryset(self):
return super().get_queryset().date(self.date).order_by("start")
def get_context_data(self, **kwargs):
start = self.date - datetime.timedelta(days=self.date.weekday())
dates = [start + datetime.timedelta(days=i) for i in range(0, 7)]
return super().get_context_data(date=self.date, dates=dates, **kwargs)

View File

@ -1,2 +1,3 @@
pytest~=7.2
pytest-django~=4.5
model_bakery~=1.10