Compare commits
2 Commits
develop-1.
...
6974c617a5
| Author | SHA1 | Date | |
|---|---|---|---|
| 6974c617a5 | |||
| 9c6d7a4741 |
3
.gitignore
vendored
3
.gitignore
vendored
@ -5,6 +5,3 @@ venv/
|
||||
node_modules/
|
||||
*.egg-info/
|
||||
*.egg
|
||||
|
||||
db.sqlite3
|
||||
instance/settings/settings.py
|
||||
|
||||
@ -9,11 +9,14 @@ repos:
|
||||
rev: 23.1.0
|
||||
hooks:
|
||||
- id: black
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.0.292
|
||||
args:
|
||||
- --line-length=79
|
||||
- --exclude="""\.git|\.__pycache__|venv|_build|buck-out|build|dist"""
|
||||
- repo: https://github.com/PyCQA/flake8.git
|
||||
rev: 6.0.0
|
||||
hooks:
|
||||
- id: ruff
|
||||
args: [--fix, --exit-non-zero-on-fix]
|
||||
- id: flake8
|
||||
exclude: ^instance/settings/|migrations/
|
||||
- repo: https://github.com/PyCQA/docformatter.git
|
||||
rev: v1.5.1
|
||||
hooks:
|
||||
|
||||
54
README.md
Normal file → Executable file
54
README.md
Normal file → Executable file
@ -1,9 +1,10 @@
|
||||

|
||||
|
||||
A platform to manage radio schedules, website content, and more. It uses the power of great tools like Django or Liquidsoap.
|
||||
Platform to manage a radio, schedules, website, and so on. We use the power of great tools like Django or Liquidsoap.
|
||||
|
||||
This project is distributed under GPL version 3. More information in the LICENSE file, except for some files whose license is indicated inside source code.
|
||||
|
||||
|
||||
## Features
|
||||
* **streams**: multiple random music streams when no program is played. We also can specify a time range and frequency for each;
|
||||
* **diffusions**: generate diffusions time slot for programs that have schedule informations. Check for conflicts and rerun.
|
||||
@ -14,51 +15,7 @@ This project is distributed under GPL version 3. More information in the LICENSE
|
||||
* **cms**: content management system.
|
||||
|
||||
|
||||
## Architecture and concepts
|
||||
Aircox is divided in two main modules:
|
||||
* `aircox`: basics of Aircox (programs, diffusions, sounds, etc. management); interface for managing a website with Aircox elements (playlists, timetable, players on the website);
|
||||
* `aircox_streamer`: interact with application to generate audio stream (LiquidSoap);
|
||||
|
||||
## Development setup
|
||||
Start installing a virtual environment :
|
||||
|
||||
```
|
||||
virtualenv venv
|
||||
source venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
pip install -r requirements_tests.txt
|
||||
```
|
||||
|
||||
Then copy the default settings and initiate the database :
|
||||
|
||||
```
|
||||
cp instance/settings/sample.py instance/settings/settings.py
|
||||
python -c "from django.core.management.utils import get_random_secret_key; print('SECRET_KEY = \"%s\"' % get_random_secret_key())" >> instance/settings/settings.py
|
||||
DJANGO_SETTINGS_MODULE=instance.settings.dev ./manage.py migrate
|
||||
```
|
||||
|
||||
Finally test and run the instance using development settings, and point your browser to http://localhost:8000 :
|
||||
|
||||
```
|
||||
DJANGO_SETTINGS_MODULE=instance.settings.dev pytest
|
||||
DJANGO_SETTINGS_MODULE=instance.settings.dev ./manage.py runserver
|
||||
```
|
||||
|
||||
Before requesting a merge, enable pre-commit :
|
||||
|
||||
```
|
||||
pip install pre-commit
|
||||
pre-commit install
|
||||
```
|
||||
|
||||
## Installation
|
||||
Running Aircox on production involves:
|
||||
* Aircox modules and a running Django project;
|
||||
* a supervisor for common tasks (sounds monitoring, stream control, etc.) -- `supervisord`;
|
||||
* a wsgi and an HTTP server -- `gunicorn`, `nginx`;
|
||||
* a database supported by Django (MySQL, SQLite, PostGresSQL);
|
||||
|
||||
### Scripts
|
||||
## Scripts
|
||||
Are included various configuration scripts that can be used to ease setup. They
|
||||
assume that the project is present in `/srv/apps/aircox`:
|
||||
|
||||
@ -70,6 +27,7 @@ The scripts are written with a combination of `cron`, `supervisord`, `nginx`
|
||||
and `gunicorn` in mind.
|
||||
|
||||
|
||||
## Installation
|
||||
### Dependencies
|
||||
For python dependencies take a peek at the `requirements.txt` file, plus
|
||||
dependencies specific to Django (e.g. for database: `mysqlclient` for MySql
|
||||
@ -104,8 +62,8 @@ pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### Configuration
|
||||
You must write a settings.py file in the `instance/settings` directory (you can just
|
||||
copy and paste `instance/settings/sample.py`. There still is configuration
|
||||
You must write a settings.py file in the `instance` directory (you can just
|
||||
copy and paste `instance/sample_settings.py`. There still is configuration
|
||||
required in this file, check it in for more info.
|
||||
|
||||
|
||||
|
||||
@ -4,7 +4,7 @@ from .diffusion import DiffusionAdmin
|
||||
from .episode import EpisodeAdmin
|
||||
from .log import LogAdmin
|
||||
from .page import PageAdmin, StaticPageAdmin
|
||||
from .program import ProgramAdmin
|
||||
from .program import ProgramAdmin, StreamAdmin
|
||||
from .schedule import ScheduleAdmin
|
||||
from .sound import SoundAdmin, TrackAdmin
|
||||
from .station import StationAdmin
|
||||
@ -19,6 +19,7 @@ __all__ = (
|
||||
"StaticPageAdmin",
|
||||
"ProgramAdmin",
|
||||
"ScheduleAdmin",
|
||||
"StreamAdmin",
|
||||
"SoundAdmin",
|
||||
"TrackAdmin",
|
||||
"StationAdmin",
|
||||
|
||||
@ -30,14 +30,12 @@ class DiffusionAdmin(DiffusionBaseAdmin, admin.ModelAdmin):
|
||||
|
||||
end_date.short_description = _("end")
|
||||
|
||||
list_display = ("episode", "start", "end", "type", "initial")
|
||||
list_display = ("episode", "start_date", "end_date", "type", "initial")
|
||||
list_filter = ("type", "start", "program")
|
||||
list_editable = ("type", "start", "end")
|
||||
list_editable = ("type",)
|
||||
ordering = ("-start", "id")
|
||||
search_fields = ("program__title", "episode__title")
|
||||
|
||||
fields = ("type", "start", "end", "initial", "program", "schedule")
|
||||
autocomplete_fields = ("episode", "program", "initial")
|
||||
readonly_fields = ("schedule",)
|
||||
|
||||
|
||||
|
||||
@ -2,23 +2,12 @@ from adminsortable2.admin import SortableAdminBase
|
||||
from django.contrib import admin
|
||||
from django.forms import ModelForm
|
||||
|
||||
from aircox.models import Episode, EpisodeSound
|
||||
from .page import ChildPageAdmin
|
||||
from .sound import TrackInline
|
||||
from aircox.models import Episode
|
||||
from .page import PageAdmin
|
||||
from .sound import SoundInline, TrackInline
|
||||
from .diffusion import DiffusionInline
|
||||
|
||||
|
||||
class EpisodeSoundInline(admin.TabularInline):
|
||||
model = EpisodeSound
|
||||
extra = 0
|
||||
fields = (
|
||||
"sound",
|
||||
"position",
|
||||
"broadcast",
|
||||
)
|
||||
autocomplete_fields = ("sound",)
|
||||
|
||||
|
||||
class EpisodeAdminForm(ModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
@ -26,14 +15,28 @@ class EpisodeAdminForm(ModelForm):
|
||||
|
||||
|
||||
@admin.register(Episode)
|
||||
class EpisodeAdmin(SortableAdminBase, ChildPageAdmin):
|
||||
class EpisodeAdmin(SortableAdminBase, PageAdmin):
|
||||
form = EpisodeAdminForm
|
||||
list_display = ChildPageAdmin.list_display
|
||||
list_filter = tuple(f for f in ChildPageAdmin.list_filter if f != "pub_date") + (
|
||||
list_display = PageAdmin.list_display
|
||||
list_filter = tuple(
|
||||
f for f in PageAdmin.list_filter if f != "pub_date"
|
||||
) + (
|
||||
"diffusion__start",
|
||||
"pub_date",
|
||||
)
|
||||
search_fields = ChildPageAdmin.search_fields + ("parent__title",)
|
||||
search_fields = PageAdmin.search_fields + ("parent__title",)
|
||||
# readonly_fields = ('parent',)
|
||||
|
||||
inlines = (TrackInline, EpisodeSoundInline, DiffusionInline)
|
||||
inlines = [TrackInline, SoundInline, DiffusionInline]
|
||||
|
||||
def add_view(self, request, object_id, form_url="", context=None):
|
||||
context = context or {}
|
||||
context["init_app"] = True
|
||||
context["init_el"] = "#inline-tracks"
|
||||
return super().change_view(request, object_id, form_url, context)
|
||||
|
||||
def change_view(self, request, object_id, form_url="", context=None):
|
||||
context = context or {}
|
||||
context["init_app"] = True
|
||||
context["init_el"] = "#inline-tracks"
|
||||
return super().change_view(request, object_id, form_url, context)
|
||||
|
||||
@ -13,10 +13,14 @@ class DateFieldFilter(filters.FieldListFilter):
|
||||
input_type = "date"
|
||||
|
||||
def __init__(self, field, request, params, model, model_admin, field_path):
|
||||
self.field_generic = f"{field_path}__"
|
||||
self.date_params = {k: v for k, v in params.items() if k.startswith(self.field_generic)}
|
||||
self.field_generic = "%s__" % field_path
|
||||
self.date_params = {
|
||||
k: v for k, v in params.items() if k.startswith(self.field_generic)
|
||||
}
|
||||
|
||||
exact_lookup = "date" if isinstance(field, models.DateTimeField) else "exact"
|
||||
exact_lookup = (
|
||||
"date" if isinstance(field, models.DateTimeField) else "exact"
|
||||
)
|
||||
|
||||
# links as: (label, param, input_type|None, value)
|
||||
self.links = [
|
||||
@ -25,11 +29,17 @@ class DateFieldFilter(filters.FieldListFilter):
|
||||
(_("Until"), self.field_generic + "lte", self.input_type),
|
||||
]
|
||||
if field.null:
|
||||
self.links.insert(0, (_("None"), self.field_generic + "isnull", None, "1"))
|
||||
self.links.insert(
|
||||
0, (_("None"), self.field_generic + "isnull", None, "1")
|
||||
)
|
||||
|
||||
self.query_attrs = {k: v for k, v in request.GET.items() if k not in self.date_params}
|
||||
self.query_attrs = {
|
||||
k: v for k, v in request.GET.items() if k not in self.date_params
|
||||
}
|
||||
self.query_string = urlencode(self.query_attrs)
|
||||
super().__init__(field, request, params, model, model_admin, field_path)
|
||||
super().__init__(
|
||||
field, request, params, model, model_admin, field_path
|
||||
)
|
||||
|
||||
def expected_parameters(self):
|
||||
return [link[1] for link in self.links]
|
||||
@ -49,7 +59,11 @@ class DateFieldFilter(filters.FieldListFilter):
|
||||
"value": value,
|
||||
"type": link[2],
|
||||
"query_attrs": self.query_attrs,
|
||||
"query_string": urlencode({link[1]: value}) + "&" + self.query_string if value else self.query_string,
|
||||
"query_string": urlencode({link[1]: value})
|
||||
+ "&"
|
||||
+ self.query_string
|
||||
if value
|
||||
else self.query_string,
|
||||
}
|
||||
|
||||
|
||||
|
||||
41
aircox/admin/mixins.py
Normal file
41
aircox/admin/mixins.py
Normal file
@ -0,0 +1,41 @@
|
||||
class UnrelatedInlineMixin:
|
||||
"""Inline class that can be included in an admin change view whose model is
|
||||
not directly related to inline's model."""
|
||||
|
||||
view_model = None
|
||||
parent_model = None
|
||||
parent_fk = ""
|
||||
|
||||
def __init__(self, parent_model, admin_site):
|
||||
self.view_model = parent_model
|
||||
super().__init__(self.parent_model, admin_site)
|
||||
|
||||
def get_parent(self, view_obj):
|
||||
"""Get formset's instance from `obj` of AdminSite's change form."""
|
||||
field = self.parent_model._meta.get_field(self.parent_fk).remote_field
|
||||
return getattr(view_obj, field.name, None)
|
||||
|
||||
def save_parent(self, parent, view_obj):
|
||||
"""Save formset's instance."""
|
||||
setattr(parent, self.parent_fk, view_obj)
|
||||
parent.save()
|
||||
return parent
|
||||
|
||||
def get_formset(self, request, obj):
|
||||
ParentFormSet = super().get_formset(request, obj)
|
||||
inline = self
|
||||
|
||||
class FormSet(ParentFormSet):
|
||||
view_obj = None
|
||||
|
||||
def __init__(self, *args, instance=None, **kwargs):
|
||||
self.view_obj = instance
|
||||
instance = inline.get_parent(instance)
|
||||
self.instance = instance
|
||||
super().__init__(*args, instance=instance, **kwargs)
|
||||
|
||||
def save(self):
|
||||
inline.save_parent(self.instance, self.view_obj)
|
||||
return super().save()
|
||||
|
||||
return FormSet
|
||||
@ -18,11 +18,10 @@ class CategoryAdmin(admin.ModelAdmin):
|
||||
search_fields = ["title"]
|
||||
fields = ["title", "slug"]
|
||||
prepopulated_fields = {"slug": ("title",)}
|
||||
ordering = ("title",)
|
||||
|
||||
|
||||
class BasePageAdmin(admin.ModelAdmin):
|
||||
list_display = ("cover_thumb", "title", "status")
|
||||
list_display = ("cover_thumb", "title", "status", "parent")
|
||||
list_display_links = ("cover_thumb", "title")
|
||||
list_editable = ("status",)
|
||||
list_filter = ("status",)
|
||||
@ -43,49 +42,19 @@ class BasePageAdmin(admin.ModelAdmin):
|
||||
(
|
||||
_("Publication Settings"),
|
||||
{
|
||||
"fields": [
|
||||
"status",
|
||||
],
|
||||
"fields": ["status", "parent"],
|
||||
},
|
||||
),
|
||||
]
|
||||
|
||||
change_form_template = "admin/aircox/page_change_form.html"
|
||||
|
||||
def cover_thumb(self, obj):
|
||||
if obj.cover and obj.cover.thumbnails:
|
||||
return mark_safe('<img src="{}"/>'.format(obj.cover.icons["64"]))
|
||||
return ""
|
||||
|
||||
def _get_extra_context(self, query, **extra_context):
|
||||
return extra_context
|
||||
|
||||
def add_view(self, request, form_url="", extra_context=None):
|
||||
filters = QueryDict(request.GET.get("_changelist_filters", ""))
|
||||
extra_context = self._get_extra_context(filters, **(extra_context or {}))
|
||||
return super().add_view(request, form_url, extra_context)
|
||||
|
||||
def changelist_view(self, request, extra_context=None):
|
||||
extra_context = self._get_extra_context(request.GET, **(extra_context or {}))
|
||||
return super().changelist_view(request, extra_context)
|
||||
|
||||
|
||||
@admin.register(Page)
|
||||
class PageAdmin(BasePageAdmin):
|
||||
list_display = BasePageAdmin.list_display + ("category",)
|
||||
list_editable = BasePageAdmin.list_editable + ("category",)
|
||||
list_filter = BasePageAdmin.list_filter + ("category", "pub_date")
|
||||
search_fields = BasePageAdmin.search_fields + ("category__title",)
|
||||
|
||||
fieldsets = deepcopy(BasePageAdmin.fieldsets)
|
||||
fieldsets[0][1]["fields"].insert(fieldsets[0][1]["fields"].index("slug") + 1, "category")
|
||||
fieldsets[1][1]["fields"] += ("featured", "allow_comments")
|
||||
|
||||
|
||||
class ChildPageAdmin(PageAdmin):
|
||||
list_display = PageAdmin.list_display + ("parent",)
|
||||
autocomplete_fields = ("parent",)
|
||||
|
||||
fieldsets = deepcopy(PageAdmin.fieldsets)
|
||||
fieldsets[1][1]["fields"] += ("parent",)
|
||||
return (
|
||||
mark_safe('<img src="{}"/>'.format(obj.cover.icons["64"]))
|
||||
if obj.cover
|
||||
else ""
|
||||
)
|
||||
|
||||
def get_changeform_initial_data(self, request):
|
||||
data = super().get_changeform_initial_data(request)
|
||||
@ -93,22 +62,49 @@ class ChildPageAdmin(PageAdmin):
|
||||
data["parent"] = filters.get("parent", None)
|
||||
return data
|
||||
|
||||
def _get_extra_context(self, query, **extra_context):
|
||||
def _get_common_context(self, query, extra_context=None):
|
||||
extra_context = extra_context or {}
|
||||
parent = query.get("parent", None)
|
||||
extra_context["parent"] = None if parent is None else Page.objects.get_subclass(id=parent)
|
||||
return super()._get_extra_context(query, **extra_context)
|
||||
extra_context["parent"] = (
|
||||
None if parent is None else Page.objects.get_subclass(id=parent)
|
||||
)
|
||||
return extra_context
|
||||
|
||||
def render_change_form(self, request, context, *args, **kwargs):
|
||||
if context["original"] and "parent" not in context:
|
||||
context["parent"] = context["original"].parent
|
||||
return super().render_change_form(request, context, *args, **kwargs)
|
||||
|
||||
def add_view(self, request, form_url="", extra_context=None):
|
||||
filters = QueryDict(request.GET.get("_changelist_filters", ""))
|
||||
extra_context = self._get_common_context(filters, extra_context)
|
||||
return super().add_view(request, form_url, extra_context)
|
||||
|
||||
def changelist_view(self, request, extra_context=None):
|
||||
extra_context = self._get_common_context(request.GET, extra_context)
|
||||
return super().changelist_view(request, extra_context)
|
||||
|
||||
|
||||
class PageAdmin(BasePageAdmin):
|
||||
change_list_template = "admin/aircox/page_change_list.html"
|
||||
|
||||
list_display = BasePageAdmin.list_display + ("category",)
|
||||
list_editable = BasePageAdmin.list_editable + ("category",)
|
||||
list_filter = BasePageAdmin.list_filter + ("category", "pub_date")
|
||||
search_fields = BasePageAdmin.search_fields + ("category__title",)
|
||||
fieldsets = deepcopy(BasePageAdmin.fieldsets)
|
||||
|
||||
fieldsets[0][1]["fields"].insert(
|
||||
fieldsets[0][1]["fields"].index("slug") + 1, "category"
|
||||
)
|
||||
fieldsets[1][1]["fields"] += ("featured", "allow_comments")
|
||||
|
||||
|
||||
@admin.register(StaticPage)
|
||||
class StaticPageAdmin(BasePageAdmin):
|
||||
list_display = BasePageAdmin.list_display + ("attach_to",)
|
||||
list_editable = BasePageAdmin.list_editable + ("attach_to",)
|
||||
fieldsets = deepcopy(BasePageAdmin.fieldsets)
|
||||
|
||||
fieldsets[1][1]["fields"] += ("attach_to",)
|
||||
|
||||
|
||||
@ -117,7 +113,6 @@ class CommentAdmin(admin.ModelAdmin):
|
||||
list_display = ("page_title", "date", "nickname")
|
||||
list_filter = ("date",)
|
||||
search_fields = ("page__title", "nickname")
|
||||
readonly_fields = ("page",)
|
||||
|
||||
def page_title(self, obj):
|
||||
return obj.page.title
|
||||
|
||||
@ -6,10 +6,7 @@ from .page import PageAdmin
|
||||
from .schedule import ScheduleInline
|
||||
|
||||
|
||||
__all__ = (
|
||||
"ProgramAdmin",
|
||||
"StreamInline",
|
||||
)
|
||||
__all__ = ("ProgramAdmin", "StreamInline", "StreamAdmin")
|
||||
|
||||
|
||||
class StreamInline(admin.TabularInline):
|
||||
@ -30,7 +27,6 @@ class ProgramAdmin(PageAdmin):
|
||||
list_filter = PageAdmin.list_filter + ("station", "active")
|
||||
prepopulated_fields = {"slug": ("title",)}
|
||||
search_fields = ("title",)
|
||||
ordering = ("title",)
|
||||
|
||||
inlines = [ScheduleInline, StreamInline]
|
||||
|
||||
@ -46,3 +42,8 @@ class ProgramAdmin(PageAdmin):
|
||||
)
|
||||
]
|
||||
return fields
|
||||
|
||||
|
||||
@admin.register(Stream)
|
||||
class StreamAdmin(admin.ModelAdmin):
|
||||
list_display = ("id", "program", "delay", "begin", "end")
|
||||
|
||||
@ -22,7 +22,6 @@ class ScheduleInline(admin.TabularInline):
|
||||
model = Schedule
|
||||
form = ScheduleInlineForm
|
||||
readonly_fields = ("timezone",)
|
||||
autocomplete_fields = ("initial",)
|
||||
extra = 1
|
||||
|
||||
|
||||
@ -34,7 +33,7 @@ class ScheduleAdmin(admin.ModelAdmin):
|
||||
program_title.short_description = _("Program")
|
||||
|
||||
def freq(self, obj):
|
||||
return obj.get_frequency_display()
|
||||
return obj.get_frequency_verbose()
|
||||
|
||||
freq.short_description = _("Day")
|
||||
|
||||
@ -47,10 +46,7 @@ class ScheduleAdmin(admin.ModelAdmin):
|
||||
"duration",
|
||||
"initial",
|
||||
]
|
||||
list_editable = ("time", "duration", "initial")
|
||||
autocomplete_fields = ("initial",)
|
||||
search_fields = ("program__title",)
|
||||
ordering = ("program__title", "initial", "date")
|
||||
list_editable = ["time", "duration", "initial"]
|
||||
|
||||
def get_readonly_fields(self, request, obj=None):
|
||||
if obj:
|
||||
|
||||
@ -9,6 +9,7 @@ from ..models import Sound, Track
|
||||
|
||||
|
||||
class TrackInline(admin.TabularInline):
|
||||
template = "admin/aircox/playlist_inline.html"
|
||||
model = Track
|
||||
extra = 0
|
||||
fields = ("position", "artist", "title", "tags", "album", "year", "info")
|
||||
@ -24,24 +25,28 @@ class SoundTrackInline(TrackInline):
|
||||
class SoundInline(admin.TabularInline):
|
||||
model = Sound
|
||||
fields = [
|
||||
"type",
|
||||
"name",
|
||||
"audio",
|
||||
"duration",
|
||||
"broadcast",
|
||||
"is_good_quality",
|
||||
"is_public",
|
||||
"is_downloadable",
|
||||
"is_removed",
|
||||
]
|
||||
readonly_fields = ["broadcast", "audio", "duration", "is_good_quality"]
|
||||
readonly_fields = ["type", "audio", "duration", "is_good_quality"]
|
||||
extra = 0
|
||||
max_num = 0
|
||||
|
||||
def audio(self, obj):
|
||||
return mark_safe('<audio src="{}" controls></audio>'.format(obj.file.url))
|
||||
return mark_safe(
|
||||
'<audio src="{}" controls></audio>'.format(obj.file.url)
|
||||
)
|
||||
|
||||
audio.short_description = _("Audio")
|
||||
|
||||
def get_queryset(self, request):
|
||||
return super().get_queryset(request).available()
|
||||
|
||||
|
||||
@admin.register(Sound)
|
||||
class SoundAdmin(SortableAdminBase, admin.ModelAdmin):
|
||||
@ -49,31 +54,20 @@ class SoundAdmin(SortableAdminBase, admin.ModelAdmin):
|
||||
list_display = [
|
||||
"id",
|
||||
"name",
|
||||
# "related",
|
||||
"broadcast",
|
||||
"related",
|
||||
"type",
|
||||
"duration",
|
||||
"is_public",
|
||||
"is_good_quality",
|
||||
"is_downloadable",
|
||||
"audio",
|
||||
]
|
||||
list_filter = ("broadcast", "is_good_quality", "is_public")
|
||||
list_filter = ("type", "is_good_quality", "is_public")
|
||||
list_editable = ["name", "is_public", "is_downloadable"]
|
||||
|
||||
search_fields = ["name", "program__title", "file"]
|
||||
autocomplete_fields = ("program",)
|
||||
search_fields = ["name", "program__title"]
|
||||
fieldsets = [
|
||||
(
|
||||
None,
|
||||
{
|
||||
"fields": [
|
||||
"name",
|
||||
"file",
|
||||
"broadcast",
|
||||
"program",
|
||||
]
|
||||
},
|
||||
),
|
||||
(None, {"fields": ["name", "file", "type", "program", "episode"]}),
|
||||
(
|
||||
None,
|
||||
{
|
||||
@ -87,19 +81,27 @@ class SoundAdmin(SortableAdminBase, admin.ModelAdmin):
|
||||
},
|
||||
),
|
||||
]
|
||||
readonly_fields = ("file", "duration", "is_removed")
|
||||
readonly_fields = ("file", "duration", "type")
|
||||
inlines = [SoundTrackInline]
|
||||
|
||||
def related(self, obj):
|
||||
# # TODO: link to episode or program edit
|
||||
return obj.program.title if obj.program else ""
|
||||
# TODO: link to episode or program edit
|
||||
return (
|
||||
obj.episode.title
|
||||
if obj.episode
|
||||
else obj.program.title
|
||||
if obj.program
|
||||
else ""
|
||||
)
|
||||
|
||||
# return obj.episode.title if obj.episode else obj.program.title if obj.program else ""
|
||||
|
||||
related.short_description = _("Program")
|
||||
related.short_description = _("Program / Episode")
|
||||
|
||||
def audio(self, obj):
|
||||
return mark_safe('<audio src="{}" controls></audio>'.format(obj.file.url)) if not obj.is_removed else ""
|
||||
return (
|
||||
mark_safe('<audio src="{}" controls></audio>'.format(obj.file.url))
|
||||
if obj.type != Sound.TYPE_REMOVED
|
||||
else ""
|
||||
)
|
||||
|
||||
audio.short_description = _("Audio")
|
||||
|
||||
|
||||
74
aircox/admin_site.py
Normal file
74
aircox/admin_site.py
Normal file
@ -0,0 +1,74 @@
|
||||
from django.contrib import admin
|
||||
from django.urls import include, path, reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework.routers import DefaultRouter
|
||||
|
||||
from .models import Comment, Diffusion, Program
|
||||
from .views.admin import StatisticsView
|
||||
|
||||
__all__ = ["AdminSite"]
|
||||
|
||||
|
||||
class AdminSite(admin.AdminSite):
|
||||
extra_urls = None
|
||||
tools = [
|
||||
(_("Statistics"), "admin:tools-stats"),
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.router = DefaultRouter()
|
||||
self.extra_urls = []
|
||||
self.tools = type(self).tools.copy()
|
||||
|
||||
def each_context(self, request):
|
||||
context = super().each_context(request)
|
||||
context.update(
|
||||
{
|
||||
# all programs
|
||||
"programs": Program.objects.active()
|
||||
.values("pk", "title")
|
||||
.order_by("title"),
|
||||
# today's diffusions
|
||||
"diffusions": Diffusion.objects.date()
|
||||
.order_by("start")
|
||||
.select_related("episode"),
|
||||
# TODO: only for dashboard
|
||||
# last comments
|
||||
"comments": Comment.objects.order_by("-date").select_related(
|
||||
"page"
|
||||
)[0:10],
|
||||
}
|
||||
)
|
||||
return context
|
||||
|
||||
def get_urls(self):
|
||||
urls = (
|
||||
[
|
||||
path("api/", include((self.router.urls, "api"))),
|
||||
path(
|
||||
"tools/statistics/",
|
||||
self.admin_view(StatisticsView.as_view()),
|
||||
name="tools-stats",
|
||||
),
|
||||
path(
|
||||
"tools/statistics/<date:date>/",
|
||||
self.admin_view(StatisticsView.as_view()),
|
||||
name="tools-stats",
|
||||
),
|
||||
]
|
||||
+ self.extra_urls
|
||||
+ super().get_urls()
|
||||
)
|
||||
return urls
|
||||
|
||||
def get_tools(self):
|
||||
return [(label, reverse(url)) for label, url in self.tools]
|
||||
|
||||
def route_view(self, url, view, name, admin_view=True, label=None):
|
||||
self.extra_urls.append(
|
||||
path(url, self.admin_view(view) if admin_view else view, name=name)
|
||||
)
|
||||
|
||||
if label:
|
||||
self.tools.append((label, "admin:" + name))
|
||||
@ -1,6 +1,11 @@
|
||||
from django.apps import AppConfig
|
||||
from django.contrib.admin.apps import AdminConfig
|
||||
|
||||
|
||||
class AircoxConfig(AppConfig):
|
||||
name = "aircox"
|
||||
verbose_name = "Aircox"
|
||||
|
||||
|
||||
class AircoxAdminConfig(AdminConfig):
|
||||
default_site = "aircox.admin_site.AdminSite"
|
||||
|
||||
@ -2,7 +2,6 @@ import os
|
||||
|
||||
import inspect
|
||||
|
||||
from bleach import sanitizer
|
||||
from django.conf import settings as d_settings
|
||||
|
||||
|
||||
@ -87,8 +86,8 @@ class Settings(BaseSettings):
|
||||
# TODO include content_type in order to avoid clash with potential
|
||||
# extra applications
|
||||
# aircox
|
||||
"view_program",
|
||||
"view_episode",
|
||||
"change_program",
|
||||
"change_episode",
|
||||
"change_diffusion",
|
||||
"add_comment",
|
||||
"change_comment",
|
||||
@ -141,7 +140,7 @@ class Settings(BaseSettings):
|
||||
"""In days, minimal age of a log before it is archived."""
|
||||
|
||||
# --- Sounds
|
||||
SOUND_BROADCASTS_SUBDIR = "archives"
|
||||
SOUND_ARCHIVES_SUBDIR = "archives"
|
||||
"""Sub directory used for the complete episode sounds."""
|
||||
SOUND_EXCERPTS_SUBDIR = "excerpts"
|
||||
"""Sub directory used for the excerpts of the episode."""
|
||||
@ -177,13 +176,5 @@ class Settings(BaseSettings):
|
||||
IMPORT_PLAYLIST_CSV_TEXT_QUOTE = '"'
|
||||
"""Text delimiter of csv text files."""
|
||||
|
||||
ALLOW_COMMENTS = True
|
||||
"""Allow comments."""
|
||||
|
||||
# ---- bleach
|
||||
ALLOWED_TAGS = [*sanitizer.ALLOWED_TAGS, "br", "p", "h3", "h4", "h5"]
|
||||
ALLOWED_ATTRIBUTES = sanitizer.ALLOWED_ATTRIBUTES
|
||||
ALLOWED_PROTOCOLS = sanitizer.ALLOWED_PROTOCOLS
|
||||
|
||||
|
||||
settings = Settings("AIRCOX")
|
||||
|
||||
@ -1,4 +0,0 @@
|
||||
def station(request):
|
||||
station = request.station
|
||||
audio_streams = station.streams if station else None
|
||||
return {"station": station, "audio_streams": audio_streams}
|
||||
@ -1,8 +0,0 @@
|
||||
# aircox.controllers
|
||||
This module provides the following controllers classes:
|
||||
- `log_archiver.LogArchiver`: dumps and load gzip archives from Log models.
|
||||
- `sound_file.SoundFile`: handle synchronisation between filesystem and database for a sound file.
|
||||
- `sound_monitor.SoundMonitor`: monitor filesystem for changes on audio files and synchronise database.
|
||||
- `sound_stats.SoundStats` (+ `SoxStats`): get audio statistics of an audio file using Sox.
|
||||
- `diffuions.Diffusions`: generate, update and clean diffusions.
|
||||
- `playlist_import.PlaylistImport`: import playlists from CSV.
|
||||
@ -1,58 +0,0 @@
|
||||
from datetime import datetime, time
|
||||
import logging
|
||||
|
||||
from django.db import transaction
|
||||
from django.utils import timezone as tz
|
||||
|
||||
from aircox.models import Diffusion, Schedule
|
||||
|
||||
logger = logging.getLogger("aircox.commands")
|
||||
|
||||
|
||||
__all__ = ("DiffusionMonitor",)
|
||||
|
||||
|
||||
class DiffusionMonitor:
|
||||
"""Handle generation and update of Diffusion instances."""
|
||||
|
||||
date = None
|
||||
|
||||
def __init__(self, date):
|
||||
self.date = date or date.today()
|
||||
|
||||
def update(self):
|
||||
episodes, diffusions = [], []
|
||||
for schedule in Schedule.objects.filter(program__active=True, initial__isnull=True):
|
||||
eps, diffs = schedule.diffusions_of_month(self.date)
|
||||
if eps:
|
||||
episodes += eps
|
||||
if diffs:
|
||||
diffusions += diffs
|
||||
|
||||
logger.info(
|
||||
"[update] %s: %d episodes, %d diffusions and reruns",
|
||||
str(schedule),
|
||||
len(eps),
|
||||
len(diffs),
|
||||
)
|
||||
|
||||
with transaction.atomic():
|
||||
logger.info(
|
||||
"[update] save %d episodes and %d diffusions",
|
||||
len(episodes),
|
||||
len(diffusions),
|
||||
)
|
||||
for episode in episodes:
|
||||
episode.save()
|
||||
for diffusion in diffusions:
|
||||
# force episode id's update
|
||||
diffusion.episode = diffusion.episode
|
||||
diffusion.save()
|
||||
|
||||
def clean(self):
|
||||
qs = Diffusion.objects.filter(
|
||||
type=Diffusion.TYPE_UNCONFIRMED,
|
||||
start__lt=tz.make_aware(datetime.combine(self.date, time.min)),
|
||||
)
|
||||
logger.info("[clean] %d diffusions will be removed", qs.count())
|
||||
qs.delete()
|
||||
@ -1,107 +0,0 @@
|
||||
import gzip
|
||||
import os
|
||||
|
||||
import yaml
|
||||
from django.utils.functional import cached_property
|
||||
|
||||
from aircox.conf import settings
|
||||
from aircox.models import Diffusion, Sound, Track, Log
|
||||
|
||||
|
||||
__all__ = ("LogArchiver",)
|
||||
|
||||
|
||||
class LogArchiver:
|
||||
"""Commodity class used to manage archives of logs."""
|
||||
|
||||
@cached_property
|
||||
def fields(self):
|
||||
return Log._meta.get_fields()
|
||||
|
||||
@staticmethod
|
||||
def get_path(station, date):
|
||||
return os.path.join(
|
||||
settings.LOGS_ARCHIVES_DIR_ABS,
|
||||
"{}_{}.log.gz".format(date.strftime("%Y%m%d"), station.pk),
|
||||
)
|
||||
|
||||
def archive(self, qs, keep=False):
|
||||
"""Archive logs of the given queryset.
|
||||
|
||||
Delete archived logs if not `keep`. Return the count of archived
|
||||
logs
|
||||
"""
|
||||
if not qs.exists():
|
||||
return 0
|
||||
|
||||
os.makedirs(settings.LOGS_ARCHIVES_DIR_ABS, exist_ok=True)
|
||||
count = qs.count()
|
||||
logs = self.sort_logs(qs)
|
||||
|
||||
# Note: since we use Yaml, we can just append new logs when file
|
||||
# exists yet <3
|
||||
for (station, date), logs in logs.items():
|
||||
path = self.get_path(station, date)
|
||||
# FIXME: remove binary mode
|
||||
with gzip.open(path, "ab") as archive:
|
||||
data = yaml.dump([self.serialize(line) for line in logs]).encode("utf8")
|
||||
archive.write(data)
|
||||
|
||||
if not keep:
|
||||
qs.delete()
|
||||
|
||||
return count
|
||||
|
||||
@staticmethod
|
||||
def sort_logs(qs):
|
||||
"""Sort logs by station and date and return a dict of `{
|
||||
(station,date): [logs] }`."""
|
||||
qs = qs.order_by("date")
|
||||
logs = {}
|
||||
for log in qs:
|
||||
key = (log.station, log.date.date())
|
||||
logs.setdefault(key, []).append(log)
|
||||
return logs
|
||||
|
||||
def serialize(self, log):
|
||||
"""Serialize log."""
|
||||
return {i.attname: getattr(log, i.attname) for i in self.fields}
|
||||
|
||||
def load(self, station, date):
|
||||
"""Load an archive returning logs in a list."""
|
||||
path = self.get_path(station, date)
|
||||
|
||||
if not os.path.exists(path):
|
||||
return []
|
||||
return self.load_file(path)
|
||||
|
||||
def load_file(self, path):
|
||||
with gzip.open(path, "rb") as archive:
|
||||
data = archive.read()
|
||||
logs = yaml.safe_load(data)
|
||||
|
||||
# we need to preload diffusions, sounds and tracks
|
||||
rels = {
|
||||
"diffusion": self.get_relations(logs, Diffusion, "diffusion"),
|
||||
"sound": self.get_relations(logs, Sound, "sound"),
|
||||
"track": self.get_relations(logs, Track, "track"),
|
||||
}
|
||||
|
||||
def rel_obj(log, attr):
|
||||
rel_id = log.get(attr + "_id")
|
||||
return rels[attr][rel_id] if rel_id else None
|
||||
|
||||
return [
|
||||
Log(
|
||||
diffusion=rel_obj(log, "diffusion"), sound=rel_obj(log, "sound"), track=rel_obj(log, "track"), **log
|
||||
)
|
||||
for log in logs
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def get_relations(logs, model, attr):
|
||||
"""From a list of dict representing logs, retrieve related objects of
|
||||
the given type."""
|
||||
attr_id = attr + "_id"
|
||||
pks = {log[attr_id] for log in logs if attr_id in log}
|
||||
return {rel.pk: rel for rel in model.objects.filter(pk__in=pks)}
|
||||
@ -1,100 +0,0 @@
|
||||
import csv
|
||||
import logging
|
||||
import os
|
||||
|
||||
|
||||
from aircox.conf import settings
|
||||
from aircox.models import Track
|
||||
|
||||
|
||||
__all__ = ("PlaylistImport",)
|
||||
|
||||
|
||||
logger = logging.getLogger("aircox.commands")
|
||||
|
||||
|
||||
class PlaylistImport:
|
||||
"""Import one or more playlist for the given sound. Attach it to the
|
||||
provided sound.
|
||||
|
||||
Playlists are in CSV format, where columns are separated with a
|
||||
'{settings.IMPORT_PLAYLIST_CSV_DELIMITER}'. Text quote is
|
||||
{settings.IMPORT_PLAYLIST_CSV_TEXT_QUOTE}.
|
||||
|
||||
If 'minutes' or 'seconds' are given, position will be expressed as timed
|
||||
position, instead of position in playlist.
|
||||
"""
|
||||
|
||||
path = None
|
||||
data = None
|
||||
tracks = None
|
||||
track_kwargs = {}
|
||||
|
||||
def __init__(self, path=None, **track_kwargs):
|
||||
self.path = path
|
||||
self.track_kwargs = track_kwargs
|
||||
|
||||
def reset(self):
|
||||
self.data = None
|
||||
self.tracks = None
|
||||
|
||||
def run(self):
|
||||
self.read()
|
||||
if self.track_kwargs.get("sound") is not None:
|
||||
self.make_playlist()
|
||||
|
||||
def read(self):
|
||||
if not os.path.exists(self.path):
|
||||
return True
|
||||
with open(self.path, "r") as file:
|
||||
logger.info("start reading csv " + self.path)
|
||||
self.data = list(
|
||||
csv.DictReader(
|
||||
(row for row in file if not (row.startswith("#") or row.startswith("\ufeff#")) and row.strip()),
|
||||
fieldnames=settings.IMPORT_PLAYLIST_CSV_COLS,
|
||||
delimiter=settings.IMPORT_PLAYLIST_CSV_DELIMITER,
|
||||
quotechar=settings.IMPORT_PLAYLIST_CSV_TEXT_QUOTE,
|
||||
)
|
||||
)
|
||||
|
||||
def make_playlist(self):
|
||||
"""Make a playlist from the read data, and return it.
|
||||
|
||||
If save is true, save it into the database
|
||||
"""
|
||||
if self.track_kwargs.get("sound") is None:
|
||||
logger.error("related track's sound is missing. Skip import of " + self.path + ".")
|
||||
return
|
||||
|
||||
maps = settings.IMPORT_PLAYLIST_CSV_COLS
|
||||
tracks = []
|
||||
|
||||
logger.info("parse csv file " + self.path)
|
||||
has_timestamp = ("minutes" or "seconds") in maps
|
||||
for index, line in enumerate(self.data):
|
||||
if ("title" or "artist") not in line:
|
||||
return
|
||||
try:
|
||||
timestamp = (
|
||||
int(line.get("minutes") or 0) * 60 + int(line.get("seconds") or 0) if has_timestamp else None
|
||||
)
|
||||
|
||||
track, created = Track.objects.get_or_create(
|
||||
title=line.get("title"), artist=line.get("artist"), position=index, **self.track_kwargs
|
||||
)
|
||||
track.timestamp = timestamp
|
||||
track.info = line.get("info")
|
||||
tags = line.get("tags")
|
||||
if tags:
|
||||
track.tags.add(*tags.lower().split(","))
|
||||
except Exception as err:
|
||||
logger.warning(
|
||||
"an error occured for track {index}, it may not "
|
||||
"have been saved: {err}".format(index=index, err=err)
|
||||
)
|
||||
continue
|
||||
|
||||
track.save()
|
||||
tracks.append(track)
|
||||
self.tracks = tracks
|
||||
return tracks
|
||||
@ -1,95 +0,0 @@
|
||||
#! /usr/bin/env python3
|
||||
"""Provide SoundFile which is used to link between database and file system.
|
||||
|
||||
File name
|
||||
=========
|
||||
It tries to parse the file name to get the date of the diffusion of an
|
||||
episode and associate the file with it; We use the following format:
|
||||
yyyymmdd[_n][_][name]
|
||||
|
||||
Where:
|
||||
'yyyy' the year of the episode's diffusion;
|
||||
'mm' the month of the episode's diffusion;
|
||||
'dd' the day of the episode's diffusion;
|
||||
'n' the number of the episode (if multiple episodes);
|
||||
'name' the title of the sound;
|
||||
|
||||
Sound Quality
|
||||
=============
|
||||
To check quality of files, call the command sound_quality_check using the
|
||||
parameters given by the setting SOUND_QUALITY. This script requires
|
||||
Sox (and soxi).
|
||||
"""
|
||||
import logging
|
||||
|
||||
from django.conf import settings as conf
|
||||
|
||||
from aircox.models import Program, Sound, EpisodeSound
|
||||
|
||||
|
||||
logger = logging.getLogger("aircox.commands")
|
||||
|
||||
|
||||
__all__ = ("SoundFile",)
|
||||
|
||||
|
||||
class SoundFile:
|
||||
"""Handle synchronisation between sounds on files and database."""
|
||||
|
||||
path = None
|
||||
info = None
|
||||
path_info = None
|
||||
sound = None
|
||||
|
||||
def __init__(self, path):
|
||||
self.path = path
|
||||
|
||||
@property
|
||||
def sound_path(self):
|
||||
"""Relative path name."""
|
||||
return self.path.replace(conf.MEDIA_ROOT + "/", "")
|
||||
|
||||
@property
|
||||
def episode(self):
|
||||
return self.sound and self.sound.episode
|
||||
|
||||
def sync(self, sound=None, program=None, deleted=False, keep_deleted=False, **kwargs):
|
||||
"""Update related sound model and save it."""
|
||||
if deleted:
|
||||
self.sound = self._on_delete(self.path, keep_deleted)
|
||||
return self.sound
|
||||
|
||||
program = sound and sound.program or Program.get_from_path(self.path)
|
||||
if program:
|
||||
kwargs["program_id"] = program.pk
|
||||
|
||||
created = False
|
||||
if not sound:
|
||||
sound, created = Sound.objects.get_or_create(file=self.sound_path, defaults=kwargs)
|
||||
|
||||
self.sound = sound
|
||||
sound.sync_fs(on_update=True, find_playlist=True)
|
||||
sound.save()
|
||||
|
||||
if not sound.episodesound_set.all().exists():
|
||||
self.create_episode_sound(sound)
|
||||
return sound
|
||||
|
||||
def create_episode_sound(self, sound):
|
||||
episode = sound.find_episode()
|
||||
if episode:
|
||||
# FIXME: position from name
|
||||
item = EpisodeSound(
|
||||
episode=episode, sound=sound, position=episode.episodesound_set.all().count(), broadcast=sound.broadcast
|
||||
)
|
||||
item.save()
|
||||
|
||||
def _on_delete(self, path, keep_deleted):
|
||||
sound = None
|
||||
if keep_deleted:
|
||||
if sound := Sound.objects.path(self.path).first():
|
||||
sound.is_removed = True
|
||||
sound.save(sync=False)
|
||||
elif sound := Sound.objects.path(self.path):
|
||||
sound.delete()
|
||||
return sound
|
||||
@ -1,309 +0,0 @@
|
||||
#! /usr/bin/env python3
|
||||
|
||||
"""Monitor sound files; For each program, check for:
|
||||
|
||||
- new files;
|
||||
- deleted files;
|
||||
- differences between files and sound;
|
||||
- quality of the files;
|
||||
|
||||
It tries to parse the file name to get the date of the diffusion of an
|
||||
episode and associate the file with it; WNotifye the following format:
|
||||
yyyymmdd[_n][_][name]
|
||||
|
||||
Where:
|
||||
'yyyy' the year Notifyhe episode's diffusion;
|
||||
'mm' the month of the episode's difNotifyon;
|
||||
'dd' the day of the episode's diffusion;
|
||||
'n' the number of the episode (if multiple episodes);
|
||||
'name' the title of the sNotify;
|
||||
|
||||
|
||||
To check quality of files, call the command sound_quality_check using the
|
||||
parameters given by the setting SOUND_QUALITY. This script requires
|
||||
Sox (and soxi).
|
||||
"""
|
||||
import atexit
|
||||
from concurrent import futures
|
||||
import logging
|
||||
import time
|
||||
import os
|
||||
|
||||
# from datetime import datetime, timedelta
|
||||
|
||||
from django.utils.timezone import datetime, timedelta
|
||||
|
||||
from watchdog.observers import Observer
|
||||
from watchdog.events import PatternMatchingEventHandler
|
||||
|
||||
from aircox.conf import settings
|
||||
from aircox.models import Sound, Program
|
||||
|
||||
from .sound_file import SoundFile
|
||||
|
||||
|
||||
# FIXME: logger should be different in used classes (e.g. "aircox.commands")
|
||||
# defaulting to logging.
|
||||
logger = logging.getLogger("aircox.commands")
|
||||
|
||||
|
||||
__all__ = (
|
||||
"Task",
|
||||
"CreateTask",
|
||||
"DeleteTask",
|
||||
"MoveTask",
|
||||
"ModifiedTask",
|
||||
"MonitorHandler",
|
||||
)
|
||||
|
||||
|
||||
class Task:
|
||||
"""Base class used to execute a specific task on file change event.
|
||||
|
||||
Handlers are sent to a multithread pool.
|
||||
"""
|
||||
|
||||
future = None
|
||||
"""Future that promised the handler's call."""
|
||||
log_msg = None
|
||||
"""Log message to display on event happens."""
|
||||
timestamp = None
|
||||
"""Last ping timestamp (the event happened)."""
|
||||
|
||||
def __init__(self, logger=logging):
|
||||
self.ping()
|
||||
|
||||
def ping(self):
|
||||
""""""
|
||||
self.timestamp = datetime.now()
|
||||
|
||||
def __call__(self, event, path=None, logger=logging, **kw):
|
||||
sound_file = SoundFile(path or event.src_path)
|
||||
if self.log_msg:
|
||||
msg = self.log_msg.format(event=event, sound_file=sound_file)
|
||||
logger.info(msg)
|
||||
|
||||
sound_file.sync(**kw)
|
||||
return sound_file
|
||||
|
||||
|
||||
class CreateTask(Task):
|
||||
log_msg = "Sound file created: {sound_file.path}"
|
||||
|
||||
|
||||
class DeleteTask(Task):
|
||||
log_msg = "Sound file deleted: {sound_file.path}"
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
kwargs["deleted"] = True
|
||||
return super().__call__(*args, **kwargs)
|
||||
|
||||
|
||||
class MoveTask(Task):
|
||||
log_msg = "Sound file moved: {event.src_path} -> {event.dest_path}"
|
||||
|
||||
def __call__(self, event, **kw):
|
||||
sound = Sound.objects.filter(file=event.src_path).first()
|
||||
if sound:
|
||||
kw = {**kw, "sound": sound, "path": event.src_path}
|
||||
else:
|
||||
kw["path"] = event.dest_path
|
||||
return super().__call__(event, **kw)
|
||||
|
||||
|
||||
class ModifiedTask(Task):
|
||||
timeout_delta = timedelta(seconds=30)
|
||||
log_msg = "Sound file updated: {sound_file.path}"
|
||||
|
||||
def wait(self):
|
||||
# multiple call of this handler can be done consecutively, we block
|
||||
# its thread using timeout
|
||||
# Note: this method may be subject to some race conflicts, but this
|
||||
# should not be big a real issue.
|
||||
timeout = self.timestamp + self.timeout_delta
|
||||
while datetime.now() < timeout:
|
||||
time.sleep(self.timeout_delta.total_seconds())
|
||||
timeout = self.timestamp + self.timeout_delta
|
||||
|
||||
def __call__(self, event, **kw):
|
||||
self.wait()
|
||||
return super().__call__(event, **kw)
|
||||
|
||||
|
||||
class MonitorHandler(PatternMatchingEventHandler):
|
||||
"""MonitorHandler is used as a Watchdog event handler.
|
||||
|
||||
It uses a multithread pool in order to execute tasks on events. If a
|
||||
job already exists for this file and event, it pings existing job
|
||||
without creating a new one.
|
||||
"""
|
||||
|
||||
pool = None
|
||||
jobs = None
|
||||
|
||||
def __init__(self, subdir, pool, jobs=None, **sync_kw):
|
||||
"""
|
||||
:param str subdir: sub-directory in program dirs to monitor \
|
||||
(SOUND_ARCHIVES_SUBDIR or SOUND_EXCERPTS_SUBDIR);
|
||||
:param concurrent.futures.Executor pool: pool executing jobs on file
|
||||
change;
|
||||
:param **sync_kw: kwargs passed to `SoundFile.sync`;
|
||||
"""
|
||||
self.subdir = subdir
|
||||
self.pool = pool
|
||||
self.jobs = jobs or {}
|
||||
self.sync_kw = sync_kw
|
||||
|
||||
patterns = ["*/{}/*{}".format(self.subdir, ext) for ext in settings.SOUND_FILE_EXT]
|
||||
super().__init__(patterns=patterns, ignore_directories=True)
|
||||
|
||||
def on_created(self, event):
|
||||
self._submit(CreateTask(), event, "new", **self.sync_kw)
|
||||
|
||||
def on_deleted(self, event):
|
||||
self._submit(DeleteTask(), event, "del")
|
||||
|
||||
def on_moved(self, event):
|
||||
self._submit(MoveTask(), event, "mv", **self.sync_kw)
|
||||
|
||||
def on_modified(self, event):
|
||||
self._submit(ModifiedTask(), event, "up", **self.sync_kw)
|
||||
|
||||
def _submit(self, handler, event, job_key_prefix, **kwargs):
|
||||
"""Send handler job to pool if not already running.
|
||||
|
||||
Return tuple with running job and boolean indicating if its a
|
||||
new one.
|
||||
"""
|
||||
key = job_key_prefix + ":" + event.src_path
|
||||
job = self.jobs.get(key)
|
||||
if job and not job.future.done():
|
||||
job.ping()
|
||||
return job, False
|
||||
|
||||
handler.future = self.pool.submit(handler, event, **kwargs)
|
||||
self.jobs[key] = handler
|
||||
|
||||
def done(r):
|
||||
if self.jobs.get(key) is handler:
|
||||
del self.jobs[key]
|
||||
|
||||
handler.future.add_done_callback(done)
|
||||
return handler, True
|
||||
|
||||
|
||||
class SoundMonitor:
|
||||
"""Monitor for filesystem changes in order to synchronise database and
|
||||
analyse files of a provided program."""
|
||||
|
||||
def report(self, program=None, component=None, *content, logger=logging):
|
||||
content = " ".join([str(c) for c in content])
|
||||
logger.info(f"{program}: {content}" if not component else f"{program}, {component}: {content}")
|
||||
|
||||
def scan(self, logger=logging):
|
||||
"""For all programs, scan dirs.
|
||||
|
||||
Return scanned directories.
|
||||
"""
|
||||
logger.info("scan all programs...")
|
||||
programs = Program.objects.filter()
|
||||
|
||||
dirs = []
|
||||
for program in programs:
|
||||
logger.info(f"#{program.id} {program.title}")
|
||||
self.scan_for_program(
|
||||
program,
|
||||
settings.SOUND_BROADCASTS_SUBDIR,
|
||||
logger=logger,
|
||||
broadcast=True,
|
||||
)
|
||||
self.scan_for_program(
|
||||
program,
|
||||
settings.SOUND_EXCERPTS_SUBDIR,
|
||||
logger=logger,
|
||||
broadcast=False,
|
||||
)
|
||||
dirs.append(program.abspath)
|
||||
return dirs
|
||||
|
||||
def scan_for_program(self, program, subdir, logger=logging, **sound_kwargs):
|
||||
"""Scan a given directory that is associated to the given program, and
|
||||
update sounds information."""
|
||||
logger.info("- %s/", subdir)
|
||||
if not program.ensure_dir(subdir):
|
||||
return
|
||||
|
||||
abs_subdir = os.path.join(program.abspath, subdir)
|
||||
sounds = []
|
||||
|
||||
# sounds in directory
|
||||
for path in os.listdir(abs_subdir):
|
||||
path = os.path.join(abs_subdir, path)
|
||||
if not path.endswith(settings.SOUND_FILE_EXT):
|
||||
continue
|
||||
|
||||
sound_file = SoundFile(path)
|
||||
sound_file.sync(program=program, **sound_kwargs)
|
||||
sounds.append(sound_file.sound.pk)
|
||||
|
||||
# sounds in db & unchecked
|
||||
sounds = Sound.objects.filter(file__startswith=program.path).exclude(pk__in=sounds)
|
||||
self.check_sounds(sounds, program=program)
|
||||
|
||||
def check_sounds(self, qs, **sync_kwargs):
|
||||
"""Only check for the sound existence or update."""
|
||||
# check files
|
||||
for sound in qs:
|
||||
if sound.sync_fs(on_update=True):
|
||||
SoundFile(sound.file.path).sync(sound=sound, **sync_kwargs)
|
||||
|
||||
_running = False
|
||||
|
||||
def monitor(self, logger=logging):
|
||||
if self._running:
|
||||
raise RuntimeError("already running")
|
||||
|
||||
"""Run in monitor mode."""
|
||||
with futures.ThreadPoolExecutor() as pool:
|
||||
archives_handler = MonitorHandler(
|
||||
settings.SOUND_BROADCASTS_SUBDIR,
|
||||
pool,
|
||||
broadcast=True,
|
||||
logger=logger,
|
||||
)
|
||||
excerpts_handler = MonitorHandler(
|
||||
settings.SOUND_EXCERPTS_SUBDIR,
|
||||
pool,
|
||||
broadcast=False,
|
||||
logger=logger,
|
||||
)
|
||||
|
||||
observer = Observer()
|
||||
observer.schedule(
|
||||
archives_handler,
|
||||
settings.PROGRAMS_DIR_ABS,
|
||||
recursive=True,
|
||||
)
|
||||
observer.schedule(
|
||||
excerpts_handler,
|
||||
settings.PROGRAMS_DIR_ABS,
|
||||
recursive=True,
|
||||
)
|
||||
observer.start()
|
||||
|
||||
def leave():
|
||||
observer.stop()
|
||||
observer.join()
|
||||
|
||||
atexit.register(leave)
|
||||
|
||||
self._running = True
|
||||
while self._running:
|
||||
time.sleep(1)
|
||||
|
||||
leave()
|
||||
atexit.unregister(leave)
|
||||
|
||||
def stop(self):
|
||||
"""Stop monitor() loop."""
|
||||
self._running = False
|
||||
@ -35,7 +35,11 @@ class WeekConverter:
|
||||
return datetime.datetime.strptime(value + "/1", "%G/%V/%u").date()
|
||||
|
||||
def to_url(self, value):
|
||||
return value if isinstance(value, str) else "{:04d}/{:02d}".format(*value.isocalendar())
|
||||
return (
|
||||
value
|
||||
if isinstance(value, str)
|
||||
else "{:04d}/{:02d}".format(*value.isocalendar())
|
||||
)
|
||||
|
||||
|
||||
class DateConverter:
|
||||
@ -48,4 +52,10 @@ class DateConverter:
|
||||
return datetime.date(int(value[0]), int(value[1]), int(value[2]))
|
||||
|
||||
def to_url(self, value):
|
||||
return value if isinstance(value, str) else "{:04d}/{:02d}/{:02d}".format(value.year, value.month, value.day)
|
||||
return (
|
||||
value
|
||||
if isinstance(value, str)
|
||||
else "{:04d}/{:02d}/{:02d}".format(
|
||||
value.year, value.month, value.day
|
||||
)
|
||||
)
|
||||
|
||||
@ -1,30 +1,16 @@
|
||||
from django.contrib.auth.models import User
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
import django_filters as filters
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from . import models
|
||||
|
||||
|
||||
__all__ = (
|
||||
"PageFilters",
|
||||
"EpisodeFilters",
|
||||
"ImageFilterSet",
|
||||
"SoundFilterSet",
|
||||
"TrackFilterSet",
|
||||
"UserFilterSet",
|
||||
"GroupFilterSet",
|
||||
"UserGroupFilterSet",
|
||||
)
|
||||
from .models import Episode, Page
|
||||
|
||||
|
||||
class PageFilters(filters.FilterSet):
|
||||
q = filters.CharFilter(method="search_filter", label=_("Search"))
|
||||
|
||||
class Meta:
|
||||
model = models.Page
|
||||
model = Page
|
||||
fields = {
|
||||
"category__id": ["in", "exact"],
|
||||
"category__id": ["in"],
|
||||
"pub_date": ["exact", "gte", "lte"],
|
||||
}
|
||||
|
||||
@ -33,84 +19,15 @@ class PageFilters(filters.FilterSet):
|
||||
|
||||
|
||||
class EpisodeFilters(PageFilters):
|
||||
podcast = filters.BooleanFilter(method="podcast_filter", label=_("Podcast"))
|
||||
podcast = filters.BooleanFilter(
|
||||
method="podcast_filter", label=_("Podcast")
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = models.Episode
|
||||
model = Episode
|
||||
fields = PageFilters.Meta.fields.copy()
|
||||
|
||||
def podcast_filter(self, queryset, name, value):
|
||||
if value:
|
||||
return queryset.filter(sound__is_public=True).distinct()
|
||||
return queryset.filter(sound__isnull=True)
|
||||
|
||||
|
||||
class ImageFilterSet(filters.FilterSet):
|
||||
search = filters.CharFilter(field_name="search", method="search_filter")
|
||||
|
||||
def search_filter(self, queryset, name, value):
|
||||
return queryset.filter(original_filename__icontains=value)
|
||||
|
||||
|
||||
class SoundFilterSet(filters.FilterSet):
|
||||
station = filters.NumberFilter(field_name="program__station__id")
|
||||
program = filters.NumberFilter(field_name="program_id")
|
||||
# episode = filters.NumberFilter(field_name="episode_id")
|
||||
search = filters.CharFilter(field_name="search", method="search_filter")
|
||||
|
||||
class Meta:
|
||||
model = models.Sound
|
||||
fields = {
|
||||
# "episode": ["in", "exact", "isnull"],
|
||||
}
|
||||
|
||||
def search_filter(self, queryset, name, value):
|
||||
return queryset.search(value)
|
||||
|
||||
|
||||
class TrackFilterSet(filters.FilterSet):
|
||||
artist = filters.CharFilter(field_name="artist", lookup_expr="icontains")
|
||||
album = filters.CharFilter(field_name="album", lookup_expr="icontains")
|
||||
title = filters.CharFilter(field_name="title", lookup_expr="icontains")
|
||||
|
||||
|
||||
class UserFilterSet(filters.FilterSet):
|
||||
search = filters.CharFilter(field_name="search", method="search_filter")
|
||||
in_group = filters.NumberFilter(field_name="in_group", method="in_group_filter")
|
||||
not_in_group = filters.NumberFilter(field_name="not_in_group", method="not_in_group_filter")
|
||||
|
||||
def in_group_filter(self, queryset, name, value):
|
||||
return queryset.filter(groups__in=[value])
|
||||
|
||||
def not_in_group_filter(self, queryset, name, value):
|
||||
return queryset.exclude(groups__in=[value])
|
||||
|
||||
def search_filter(self, queryset, name, value):
|
||||
return queryset.filter(
|
||||
Q(username__icontains=value) | Q(first_name__icontains=value) | Q(last_name__icontains=value)
|
||||
)
|
||||
|
||||
|
||||
class GroupFilterSet(filters.FilterSet):
|
||||
search = filters.CharFilter(field_name="search", method="search_filter")
|
||||
no_user = filters.NumberFilter(field_name="no_user", method="no_user_filter")
|
||||
|
||||
def no_user_filter(self, queryset, name, value):
|
||||
return queryset.exclude(user__in=[value])
|
||||
|
||||
def search_filter(self, queryset, name, value):
|
||||
return queryset.filter(Q(name__icontains=value) | Q(program__title__icontains=value))
|
||||
|
||||
|
||||
class UserGroupFilterSet(filters.FilterSet):
|
||||
class Meta:
|
||||
model = User.groups.through
|
||||
fields = ["group", "user"]
|
||||
|
||||
def filter_queryset(self, queryset):
|
||||
queryset = super().filter_queryset(queryset)
|
||||
if self.form.cleaned_data.get("user"):
|
||||
queryset = queryset.order_by("group__name")
|
||||
elif self.form.cleaned_data.get("group"):
|
||||
queryset = queryset.order_by("user__first_name")
|
||||
return queryset
|
||||
|
||||
18
aircox/forms.py
Normal file
18
aircox/forms.py
Normal file
@ -0,0 +1,18 @@
|
||||
from django import forms
|
||||
from django.forms import ModelForm
|
||||
|
||||
from .models import Comment
|
||||
|
||||
|
||||
class CommentForm(ModelForm):
|
||||
nickname = forms.CharField()
|
||||
email = forms.EmailField(required=False)
|
||||
content = forms.CharField(widget=forms.Textarea())
|
||||
|
||||
nickname.widget.attrs.update({"class": "input"})
|
||||
email.widget.attrs.update({"class": "input"})
|
||||
content.widget.attrs.update({"class": "textarea"})
|
||||
|
||||
class Meta:
|
||||
model = Comment
|
||||
fields = ["nickname", "email", "content"]
|
||||
@ -1,23 +0,0 @@
|
||||
from . import widgets
|
||||
|
||||
from .episode import EpisodeForm, EpisodeSoundFormSet
|
||||
from .program import ProgramForm
|
||||
from .page import CommentForm, ImageForm, PageForm, ChildPageForm
|
||||
from .sound import SoundForm, SoundCreateForm
|
||||
from .track import TrackFormSet
|
||||
|
||||
|
||||
__all__ = (
|
||||
widgets,
|
||||
# ---- forms
|
||||
EpisodeForm,
|
||||
EpisodeSoundFormSet,
|
||||
ProgramForm,
|
||||
CommentForm,
|
||||
ImageForm,
|
||||
PageForm,
|
||||
ChildPageForm,
|
||||
SoundForm,
|
||||
SoundCreateForm,
|
||||
TrackFormSet,
|
||||
)
|
||||
@ -1,34 +0,0 @@
|
||||
from django import forms
|
||||
from django.forms.models import modelformset_factory
|
||||
|
||||
from aircox import models
|
||||
from .page import ChildPageForm
|
||||
|
||||
|
||||
__all__ = ("EpisodeForm", "EpisodeSoundFormSet")
|
||||
|
||||
|
||||
class EpisodeForm(ChildPageForm):
|
||||
class Meta:
|
||||
model = models.Episode
|
||||
fields = ChildPageForm.Meta.fields
|
||||
|
||||
|
||||
EpisodeSoundFormSet = modelformset_factory(
|
||||
models.EpisodeSound,
|
||||
fields=(
|
||||
"position",
|
||||
"episode",
|
||||
"sound",
|
||||
"broadcast",
|
||||
),
|
||||
widgets={
|
||||
"broadcast": forms.CheckboxInput(),
|
||||
"episode": forms.HiddenInput(),
|
||||
# "sound": forms.HiddenInput(),
|
||||
"position": forms.HiddenInput(),
|
||||
},
|
||||
can_delete=True,
|
||||
extra=0,
|
||||
)
|
||||
"""Formset used in EpisodeUpdateView."""
|
||||
@ -1,37 +0,0 @@
|
||||
from django import forms
|
||||
|
||||
|
||||
from aircox import models
|
||||
|
||||
|
||||
__all__ = ("CommentForm", "ImageForm", "PageForm", "ChildPageForm")
|
||||
|
||||
|
||||
class CommentForm(forms.ModelForm):
|
||||
nickname = forms.CharField()
|
||||
email = forms.EmailField(required=False)
|
||||
content = forms.CharField(widget=forms.Textarea())
|
||||
|
||||
nickname.widget.attrs.update({"class": "input"})
|
||||
email.widget.attrs.update({"class": "input"})
|
||||
content.widget.attrs.update({"class": "textarea"})
|
||||
|
||||
class Meta:
|
||||
model = models.Comment
|
||||
fields = ["nickname", "email", "content"]
|
||||
|
||||
|
||||
class ImageForm(forms.Form):
|
||||
file = forms.ImageField()
|
||||
|
||||
|
||||
class PageForm(forms.ModelForm):
|
||||
class Meta:
|
||||
fields = ("title", "category", "status", "cover", "content")
|
||||
model = models.Page
|
||||
|
||||
|
||||
class ChildPageForm(forms.ModelForm):
|
||||
class Meta:
|
||||
fields = ("title", "status", "cover", "content")
|
||||
model = models.Page
|
||||
@ -1,11 +0,0 @@
|
||||
from aircox import models
|
||||
from .page import PageForm
|
||||
|
||||
|
||||
__all__ = ("ProgramForm",)
|
||||
|
||||
|
||||
class ProgramForm(PageForm):
|
||||
class Meta:
|
||||
fields = PageForm.Meta.fields
|
||||
model = models.Program
|
||||
@ -1,26 +0,0 @@
|
||||
from django import forms
|
||||
|
||||
from aircox import models
|
||||
|
||||
|
||||
__all__ = (
|
||||
"SoundForm",
|
||||
"SoundCreateForm",
|
||||
)
|
||||
|
||||
|
||||
class SoundForm(forms.ModelForm):
|
||||
"""SoundForm used in EpisodeUpdateView."""
|
||||
|
||||
class Meta:
|
||||
model = models.Sound
|
||||
fields = ["name", "program", "file", "broadcast", "duration", "is_public", "is_downloadable"]
|
||||
|
||||
|
||||
class SoundCreateForm(forms.ModelForm):
|
||||
"""SoundForm used in EpisodeUpdateView."""
|
||||
|
||||
class Meta:
|
||||
model = models.Sound
|
||||
fields = ["name", "program", "file", "broadcast", "is_public", "is_downloadable"]
|
||||
widgets = {"program": forms.HiddenInput()}
|
||||
@ -1,23 +0,0 @@
|
||||
from django import forms
|
||||
from django.forms.models import modelformset_factory
|
||||
|
||||
from aircox import models
|
||||
|
||||
|
||||
__all__ = ("TrackFormSet",)
|
||||
|
||||
|
||||
TrackFormSet = modelformset_factory(
|
||||
models.Track,
|
||||
fields=[
|
||||
"position",
|
||||
"episode",
|
||||
"artist",
|
||||
"title",
|
||||
"tags",
|
||||
],
|
||||
widgets={"episode": forms.HiddenInput(), "position": forms.HiddenInput()},
|
||||
can_delete=True,
|
||||
extra=0,
|
||||
)
|
||||
"""Track formset used in EpisodeUpdateView."""
|
||||
@ -1,89 +0,0 @@
|
||||
from itertools import chain
|
||||
from functools import cached_property
|
||||
|
||||
from django import forms, http
|
||||
from django.urls import reverse
|
||||
|
||||
|
||||
__all__ = (
|
||||
"VueWidget",
|
||||
"VueAutoComplete",
|
||||
)
|
||||
|
||||
|
||||
class VueWidget(forms.Widget):
|
||||
binds = None
|
||||
"""Dict of `{attribute: value}` attrs set as bindings."""
|
||||
events = None
|
||||
"""Dict of `{event: value}` attrs set as events."""
|
||||
v_model = ""
|
||||
"""ES6 Model instance to bind to (`v-model`)."""
|
||||
|
||||
def __init__(self, *args, binds=None, events=None, v_model=None, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.binds = binds or []
|
||||
self.events = events or []
|
||||
|
||||
@cached_property
|
||||
def vue_attrs(self):
|
||||
"""Dict of Vue specific attributes."""
|
||||
binds, events = self.binds, self.events
|
||||
if isinstance(binds, dict):
|
||||
binds = binds.items()
|
||||
if isinstance(events, dict):
|
||||
events = events.items()
|
||||
|
||||
return dict(
|
||||
chain(
|
||||
((":" + key, value) for key, value in binds),
|
||||
(("@" + key, value) for key, value in events),
|
||||
)
|
||||
)
|
||||
|
||||
def build_attrs(self, base_attrs, extra_attrs=None):
|
||||
extra_attrs = extra_attrs or {}
|
||||
extra_attrs.update(self.vue_attrs)
|
||||
return super().build_attrs(base_attrs, extra_attrs)
|
||||
|
||||
|
||||
class VueAutoComplete(VueWidget, forms.TextInput):
|
||||
"""Autocomplete Vue component."""
|
||||
|
||||
template_name = "aircox/widgets/autocomplete.html"
|
||||
|
||||
url: str = ""
|
||||
"""Url to autocomplete API view.
|
||||
|
||||
If it has query parameters, does not generate it based on lookup
|
||||
(see `get_url()` doc).
|
||||
"""
|
||||
lookup: str = ""
|
||||
"""Field name used as lookup (instead as provided one)."""
|
||||
params: http.QueryDict
|
||||
|
||||
def __init__(self, url_name, *args, lookup=None, params=None, **kwargs):
|
||||
self.url_name = url_name
|
||||
self.lookup = lookup
|
||||
self.params = params
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def get_context(self, name, value, attrs):
|
||||
context = super().get_context(name, value, attrs)
|
||||
context["url"] = self.get_url(name, self.lookup, self.params)
|
||||
return context
|
||||
|
||||
def get_url(self, name, lookup, params=None):
|
||||
"""Return url to autocomplete API. When query parameters are not
|
||||
provided generate them using `?{lookup}=${query}&field={name}` (where
|
||||
`${query} is Vue `a-autocomplete` specific).
|
||||
|
||||
:param str name: field name (not used by default)
|
||||
:param str lookup: lookup query parameter
|
||||
:param http.QueryDict params: additional mutable parameter
|
||||
"""
|
||||
url = reverse(self.url_name)
|
||||
query = http.QueryDict(mutable=True)
|
||||
if params:
|
||||
query.update(params)
|
||||
query.update({lookup: "${query}"})
|
||||
return f"{url}?{query.urlencode()}"
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@ -30,7 +30,8 @@ class Command(BaseCommand):
|
||||
"--age",
|
||||
type=int,
|
||||
default=settings.LOGS_ARCHIVES_AGE,
|
||||
help="minimal age in days of logs to archive. Default is " "settings.LOGS_ARCHIVES_AGE",
|
||||
help="minimal age in days of logs to archive. Default is "
|
||||
"settings.LOGS_ARCHIVES_AGE",
|
||||
)
|
||||
group.add_argument(
|
||||
"-k",
|
||||
|
||||
@ -9,13 +9,59 @@ import logging
|
||||
from argparse import RawTextHelpFormatter
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.db import transaction
|
||||
from django.utils import timezone as tz
|
||||
|
||||
from aircox.controllers.diffusion_monitor import DiffusionMonitor
|
||||
from aircox.models import Diffusion, Schedule
|
||||
|
||||
logger = logging.getLogger("aircox.commands")
|
||||
|
||||
|
||||
class Actions:
|
||||
date = None
|
||||
|
||||
def __init__(self, date):
|
||||
self.date = date or datetime.date.today()
|
||||
|
||||
def update(self):
|
||||
episodes, diffusions = [], []
|
||||
for schedule in Schedule.objects.filter(
|
||||
program__active=True, initial__isnull=True
|
||||
):
|
||||
eps, diffs = schedule.diffusions_of_month(self.date)
|
||||
if eps:
|
||||
episodes += eps
|
||||
if diffs:
|
||||
diffusions += diffs
|
||||
|
||||
logger.info(
|
||||
"[update] %s: %d episodes, %d diffusions and reruns",
|
||||
str(schedule),
|
||||
len(eps),
|
||||
len(diffs),
|
||||
)
|
||||
|
||||
with transaction.atomic():
|
||||
logger.info(
|
||||
"[update] save %d episodes and %d diffusions",
|
||||
len(episodes),
|
||||
len(diffusions),
|
||||
)
|
||||
for episode in episodes:
|
||||
episode.save()
|
||||
for diffusion in diffusions:
|
||||
# force episode id's update
|
||||
diffusion.episode = diffusion.episode
|
||||
diffusion.save()
|
||||
|
||||
def clean(self):
|
||||
qs = Diffusion.objects.filter(
|
||||
type=Diffusion.TYPE_UNCONFIRMED, start__lt=self.date
|
||||
)
|
||||
logger.info("[clean] %d diffusions will be removed", qs.count())
|
||||
qs.delete()
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = __doc__
|
||||
|
||||
@ -55,11 +101,14 @@ class Command(BaseCommand):
|
||||
group.add_argument(
|
||||
"--next-month",
|
||||
action="store_true",
|
||||
help="set the date to the next month of given date" " (if next month from today",
|
||||
help="set the date to the next month of given date"
|
||||
" (if next month from today",
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
date = datetime.date(year=options["year"], month=options["month"], day=1)
|
||||
date = datetime.date(
|
||||
year=options["year"], month=options["month"], day=1
|
||||
)
|
||||
if options.get("next_month"):
|
||||
month = options.get("month")
|
||||
date += tz.timedelta(days=28)
|
||||
@ -67,7 +116,7 @@ class Command(BaseCommand):
|
||||
date += tz.timedelta(days=28)
|
||||
date = date.replace(day=1)
|
||||
|
||||
actions = DiffusionMonitor(date)
|
||||
actions = Actions(date)
|
||||
if options.get("update"):
|
||||
actions.update()
|
||||
if options.get("clean"):
|
||||
|
||||
@ -9,6 +9,7 @@ The order of the elements is: {settings.IMPORT_PLAYLIST_CSV_COLS}
|
||||
If 'minutes' or 'seconds' are given, position will be expressed as timed
|
||||
position, instead of position in playlist.
|
||||
"""
|
||||
import csv
|
||||
import logging
|
||||
import os
|
||||
from argparse import RawTextHelpFormatter
|
||||
@ -16,19 +17,109 @@ from argparse import RawTextHelpFormatter
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from aircox.conf import settings
|
||||
from aircox.models import Sound
|
||||
from aircox.controllers.playlist_import import PlaylistImport
|
||||
|
||||
from aircox.models import Sound, Track
|
||||
|
||||
__doc__ = __doc__.format(settings=settings)
|
||||
|
||||
|
||||
__all__ = ("Command",)
|
||||
__all__ = ("PlaylistImport", "Command")
|
||||
|
||||
|
||||
logger = logging.getLogger("aircox.commands")
|
||||
|
||||
|
||||
class PlaylistImport:
|
||||
path = None
|
||||
data = None
|
||||
tracks = None
|
||||
track_kwargs = {}
|
||||
|
||||
def __init__(self, path=None, **track_kwargs):
|
||||
self.path = path
|
||||
self.track_kwargs = track_kwargs
|
||||
|
||||
def reset(self):
|
||||
self.data = None
|
||||
self.tracks = None
|
||||
|
||||
def run(self):
|
||||
self.read()
|
||||
if self.track_kwargs.get("sound") is not None:
|
||||
self.make_playlist()
|
||||
|
||||
def read(self):
|
||||
if not os.path.exists(self.path):
|
||||
return True
|
||||
with open(self.path, "r") as file:
|
||||
logger.info("start reading csv " + self.path)
|
||||
self.data = list(
|
||||
csv.DictReader(
|
||||
(
|
||||
row
|
||||
for row in file
|
||||
if not (
|
||||
row.startswith("#") or row.startswith("\ufeff#")
|
||||
)
|
||||
and row.strip()
|
||||
),
|
||||
fieldnames=settings.IMPORT_PLAYLIST_CSV_COLS,
|
||||
delimiter=settings.IMPORT_PLAYLIST_CSV_DELIMITER,
|
||||
quotechar=settings.IMPORT_PLAYLIST_CSV_TEXT_QUOTE,
|
||||
)
|
||||
)
|
||||
|
||||
def make_playlist(self):
|
||||
"""Make a playlist from the read data, and return it.
|
||||
|
||||
If save is true, save it into the database
|
||||
"""
|
||||
if self.track_kwargs.get("sound") is None:
|
||||
logger.error(
|
||||
"related track's sound is missing. Skip import of "
|
||||
+ self.path
|
||||
+ "."
|
||||
)
|
||||
return
|
||||
|
||||
maps = settings.IMPORT_PLAYLIST_CSV_COLS
|
||||
tracks = []
|
||||
|
||||
logger.info("parse csv file " + self.path)
|
||||
has_timestamp = ("minutes" or "seconds") in maps
|
||||
for index, line in enumerate(self.data):
|
||||
if ("title" or "artist") not in line:
|
||||
return
|
||||
try:
|
||||
timestamp = (
|
||||
int(line.get("minutes") or 0) * 60
|
||||
+ int(line.get("seconds") or 0)
|
||||
if has_timestamp
|
||||
else None
|
||||
)
|
||||
|
||||
track, created = Track.objects.get_or_create(
|
||||
title=line.get("title"),
|
||||
artist=line.get("artist"),
|
||||
position=index,
|
||||
**self.track_kwargs
|
||||
)
|
||||
track.timestamp = timestamp
|
||||
track.info = line.get("info")
|
||||
tags = line.get("tags")
|
||||
if tags:
|
||||
track.tags.add(*tags.lower().split(","))
|
||||
except Exception as err:
|
||||
logger.warning(
|
||||
"an error occured for track {index}, it may not "
|
||||
"have been saved: {err}".format(index=index, err=err)
|
||||
)
|
||||
continue
|
||||
|
||||
track.save()
|
||||
tracks.append(track)
|
||||
self.tracks = tracks
|
||||
return tracks
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = __doc__
|
||||
|
||||
@ -51,13 +142,18 @@ class Command(BaseCommand):
|
||||
def handle(self, path, *args, **options):
|
||||
# FIXME: absolute/relative path of sounds vs given path
|
||||
if options.get("sound"):
|
||||
sound = Sound.objects.filter(file__icontains=options.get("sound")).first()
|
||||
sound = Sound.objects.filter(
|
||||
file__icontains=options.get("sound")
|
||||
).first()
|
||||
else:
|
||||
path_, ext = os.path.splitext(path)
|
||||
sound = Sound.objects.filter(path__icontains=path_).first()
|
||||
|
||||
if not sound:
|
||||
logger.error("no sound found in the database for the path " "{path}".format(path=path))
|
||||
logger.error(
|
||||
"no sound found in the database for the path "
|
||||
"{path}".format(path=path)
|
||||
)
|
||||
return
|
||||
|
||||
# FIXME: auto get sound.episode if any
|
||||
|
||||
@ -23,12 +23,20 @@ To check quality of files, call the command sound_quality_check using the
|
||||
parameters given by the setting SOUND_QUALITY. This script requires
|
||||
Sox (and soxi).
|
||||
"""
|
||||
import atexit
|
||||
import concurrent.futures as futures
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
from argparse import RawTextHelpFormatter
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
from watchdog.observers import Observer
|
||||
|
||||
from aircox.controllers.sound_monitor import SoundMonitor
|
||||
from aircox.conf import settings
|
||||
from aircox.management.sound_file import SoundFile
|
||||
from aircox.management.sound_monitor import MonitorHandler
|
||||
from aircox.models import Program, Sound
|
||||
|
||||
logger = logging.getLogger("aircox.commands")
|
||||
|
||||
@ -36,13 +44,117 @@ logger = logging.getLogger("aircox.commands")
|
||||
class Command(BaseCommand):
|
||||
help = __doc__
|
||||
|
||||
def report(self, program=None, component=None, *content):
|
||||
if not component:
|
||||
logger.info(
|
||||
"%s: %s", str(program), " ".join([str(c) for c in content])
|
||||
)
|
||||
else:
|
||||
logger.info(
|
||||
"%s, %s: %s",
|
||||
str(program),
|
||||
str(component),
|
||||
" ".join([str(c) for c in content]),
|
||||
)
|
||||
|
||||
def scan(self):
|
||||
"""For all programs, scan dirs."""
|
||||
logger.info("scan all programs...")
|
||||
programs = Program.objects.filter()
|
||||
|
||||
dirs = []
|
||||
for program in programs:
|
||||
logger.info("#%d %s", program.id, program.title)
|
||||
self.scan_for_program(
|
||||
program,
|
||||
settings.SOUND_ARCHIVES_SUBDIR,
|
||||
type=Sound.TYPE_ARCHIVE,
|
||||
)
|
||||
self.scan_for_program(
|
||||
program,
|
||||
settings.SOUND_EXCERPTS_SUBDIR,
|
||||
type=Sound.TYPE_EXCERPT,
|
||||
)
|
||||
dirs.append(os.path.join(program.abspath))
|
||||
return dirs
|
||||
|
||||
def scan_for_program(self, program, subdir, **sound_kwargs):
|
||||
"""Scan a given directory that is associated to the given program, and
|
||||
update sounds information."""
|
||||
logger.info("- %s/", subdir)
|
||||
if not program.ensure_dir(subdir):
|
||||
return
|
||||
|
||||
subdir = os.path.join(program.abspath, subdir)
|
||||
sounds = []
|
||||
|
||||
# sounds in directory
|
||||
for path in os.listdir(subdir):
|
||||
path = os.path.join(subdir, path)
|
||||
if not path.endswith(settings.SOUND_FILE_EXT):
|
||||
continue
|
||||
|
||||
sound_file = SoundFile(path)
|
||||
sound_file.sync(program=program, **sound_kwargs)
|
||||
sounds.append(sound_file.sound.pk)
|
||||
|
||||
# sounds in db & unchecked
|
||||
sounds = Sound.objects.filter(file__startswith=subdir).exclude(
|
||||
pk__in=sounds
|
||||
)
|
||||
self.check_sounds(sounds, program=program)
|
||||
|
||||
def check_sounds(self, qs, **sync_kwargs):
|
||||
"""Only check for the sound existence or update."""
|
||||
# check files
|
||||
for sound in qs:
|
||||
if sound.check_on_file():
|
||||
SoundFile(sound.file.path).sync(sound=sound, **sync_kwargs)
|
||||
|
||||
def monitor(self):
|
||||
"""Run in monitor mode."""
|
||||
with futures.ThreadPoolExecutor() as pool:
|
||||
archives_handler = MonitorHandler(
|
||||
settings.SOUND_ARCHIVES_SUBDIR,
|
||||
pool,
|
||||
type=Sound.TYPE_ARCHIVE,
|
||||
)
|
||||
excerpts_handler = MonitorHandler(
|
||||
settings.SOUND_EXCERPTS_SUBDIR,
|
||||
pool,
|
||||
type=Sound.TYPE_EXCERPT,
|
||||
)
|
||||
|
||||
observer = Observer()
|
||||
observer.schedule(
|
||||
archives_handler,
|
||||
settings.PROGRAMS_DIR_ABS,
|
||||
recursive=True,
|
||||
)
|
||||
observer.schedule(
|
||||
excerpts_handler,
|
||||
settings.PROGRAMS_DIR_ABS,
|
||||
recursive=True,
|
||||
)
|
||||
observer.start()
|
||||
|
||||
def leave():
|
||||
observer.stop()
|
||||
observer.join()
|
||||
|
||||
atexit.register(leave)
|
||||
|
||||
while True:
|
||||
time.sleep(1)
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.formatter_class = RawTextHelpFormatter
|
||||
parser.add_argument(
|
||||
"-q",
|
||||
"--quality_check",
|
||||
action="store_true",
|
||||
help="Enable quality check using sound_quality_check on all " "sounds marqued as not good",
|
||||
help="Enable quality check using sound_quality_check on all "
|
||||
"sounds marqued as not good",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-s",
|
||||
@ -55,12 +167,14 @@ class Command(BaseCommand):
|
||||
"-m",
|
||||
"--monitor",
|
||||
action="store_true",
|
||||
help="Run in monitor mode, watch for modification in the " "filesystem and react in consequence",
|
||||
help="Run in monitor mode, watch for modification in the "
|
||||
"filesystem and react in consequence",
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
monitor = SoundMonitor()
|
||||
if options.get("scan"):
|
||||
monitor.scan()
|
||||
self.scan()
|
||||
# if options.get('quality_check'):
|
||||
# self.check_quality(check=(not options.get('scan')))
|
||||
if options.get("monitor"):
|
||||
monitor.monitor()
|
||||
self.monitor()
|
||||
|
||||
@ -4,7 +4,7 @@ from argparse import RawTextHelpFormatter
|
||||
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
|
||||
from aircox.controllers.sound_stats import SoundStats, SoxStats
|
||||
from aircox.management.sound_stats import SoundStats, SoxStats
|
||||
|
||||
logger = logging.getLogger("aircox.commands")
|
||||
|
||||
@ -28,7 +28,8 @@ class Command(BaseCommand):
|
||||
"--sample_length",
|
||||
type=int,
|
||||
default=120,
|
||||
help="size of sample to analyse in seconds. If not set (or 0), " "does not analyse by sample",
|
||||
help="size of sample to analyse in seconds. If not set (or 0), "
|
||||
"does not analyse by sample",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-a",
|
||||
@ -42,7 +43,8 @@ class Command(BaseCommand):
|
||||
"--range",
|
||||
type=float,
|
||||
nargs=2,
|
||||
help="range of minimal and maximal accepted value such as: " "--range min max",
|
||||
help="range of minimal and maximal accepted value such as: "
|
||||
"--range min max",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-i",
|
||||
@ -62,7 +64,10 @@ class Command(BaseCommand):
|
||||
raise CommandError("no attribute specified")
|
||||
|
||||
# sound analyse and checks
|
||||
self.sounds = [SoundStats(path, options.get("sample_length")) for path in options.get("files")]
|
||||
self.sounds = [
|
||||
SoundStats(path, options.get("sample_length"))
|
||||
for path in options.get("files")
|
||||
]
|
||||
self.bad = []
|
||||
self.good = []
|
||||
for sound in self.sounds:
|
||||
|
||||
236
aircox/management/sound_file.py
Normal file
236
aircox/management/sound_file.py
Normal file
@ -0,0 +1,236 @@
|
||||
#! /usr/bin/env python3
|
||||
"""Provide SoundFile which is used to link between database and file system.
|
||||
|
||||
File name
|
||||
=========
|
||||
It tries to parse the file name to get the date of the diffusion of an
|
||||
episode and associate the file with it; We use the following format:
|
||||
yyyymmdd[_n][_][name]
|
||||
|
||||
Where:
|
||||
'yyyy' the year of the episode's diffusion;
|
||||
'mm' the month of the episode's diffusion;
|
||||
'dd' the day of the episode's diffusion;
|
||||
'n' the number of the episode (if multiple episodes);
|
||||
'name' the title of the sound;
|
||||
|
||||
Sound Quality
|
||||
=============
|
||||
To check quality of files, call the command sound_quality_check using the
|
||||
parameters given by the setting SOUND_QUALITY. This script requires
|
||||
Sox (and soxi).
|
||||
"""
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from datetime import date
|
||||
|
||||
import mutagen
|
||||
from django.conf import settings as conf
|
||||
from django.utils import timezone as tz
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from aircox import utils
|
||||
from aircox.models import Program, Sound, Track
|
||||
|
||||
from .commands.import_playlist import PlaylistImport
|
||||
|
||||
logger = logging.getLogger("aircox.commands")
|
||||
|
||||
|
||||
class SoundFile:
|
||||
"""Handle synchronisation between sounds on files and database."""
|
||||
|
||||
path = None
|
||||
info = None
|
||||
path_info = None
|
||||
sound = None
|
||||
|
||||
def __init__(self, path):
|
||||
self.path = path
|
||||
|
||||
@property
|
||||
def sound_path(self):
|
||||
"""Relative path name."""
|
||||
return self.path.replace(conf.MEDIA_ROOT + "/", "")
|
||||
|
||||
@property
|
||||
def episode(self):
|
||||
return self.sound and self.sound.episode
|
||||
|
||||
def sync(
|
||||
self,
|
||||
sound=None,
|
||||
program=None,
|
||||
deleted=False,
|
||||
keep_deleted=False,
|
||||
**kwargs
|
||||
):
|
||||
"""Update related sound model and save it."""
|
||||
if deleted:
|
||||
return self._on_delete(self.path, keep_deleted)
|
||||
|
||||
# FIXME: sound.program as not null
|
||||
if not program:
|
||||
program = Program.get_from_path(self.path)
|
||||
logger.debug('program from path "%s" -> %s', self.path, program)
|
||||
kwargs["program_id"] = program.pk
|
||||
|
||||
if sound:
|
||||
created = False
|
||||
else:
|
||||
sound, created = Sound.objects.get_or_create(
|
||||
file=self.sound_path, defaults=kwargs
|
||||
)
|
||||
|
||||
self.sound = sound
|
||||
self.path_info = self.read_path(self.path)
|
||||
|
||||
sound.program = program
|
||||
if created or sound.check_on_file():
|
||||
sound.name = self.path_info.get("name")
|
||||
self.info = self.read_file_info()
|
||||
if self.info is not None:
|
||||
sound.duration = utils.seconds_to_time(self.info.info.length)
|
||||
|
||||
# check for episode
|
||||
if sound.episode is None and "year" in self.path_info:
|
||||
sound.episode = self.find_episode(sound, self.path_info)
|
||||
sound.save()
|
||||
|
||||
# check for playlist
|
||||
self.find_playlist(sound)
|
||||
return sound
|
||||
|
||||
def _on_delete(self, path, keep_deleted):
|
||||
# TODO: remove from db on delete
|
||||
if keep_deleted:
|
||||
sound = Sound.objects.path(self.path).first()
|
||||
if sound:
|
||||
if keep_deleted:
|
||||
sound.type = sound.TYPE_REMOVED
|
||||
sound.check_on_file()
|
||||
sound.save()
|
||||
return sound
|
||||
else:
|
||||
Sound.objects.path(self.path).delete()
|
||||
|
||||
def read_path(self, path):
|
||||
"""Parse path name returning dictionary of extracted info. It can
|
||||
contain:
|
||||
|
||||
- `year`, `month`, `day`: diffusion date
|
||||
- `hour`, `minute`: diffusion time
|
||||
- `n`: sound arbitrary number (used for sound ordering)
|
||||
- `name`: cleaned name extracted or file name (without extension)
|
||||
"""
|
||||
basename = os.path.basename(path)
|
||||
basename = os.path.splitext(basename)[0]
|
||||
reg_match = self._path_re.search(basename)
|
||||
if reg_match:
|
||||
info = reg_match.groupdict()
|
||||
for k in ("year", "month", "day", "hour", "minute", "n"):
|
||||
if info.get(k) is not None:
|
||||
info[k] = int(info[k])
|
||||
|
||||
name = info.get("name")
|
||||
info["name"] = name and self._into_name(name) or basename
|
||||
else:
|
||||
info = {"name": basename}
|
||||
return info
|
||||
|
||||
_path_re = re.compile(
|
||||
"^(?P<year>[0-9]{4})(?P<month>[0-9]{2})(?P<day>[0-9]{2})"
|
||||
"(_(?P<hour>[0-9]{2})h(?P<minute>[0-9]{2}))?"
|
||||
"(_(?P<n>[0-9]+))?"
|
||||
"_?[ -]*(?P<name>.*)$"
|
||||
)
|
||||
|
||||
def _into_name(self, name):
|
||||
name = name.replace("_", " ")
|
||||
return " ".join(r.capitalize() for r in name.split(" "))
|
||||
|
||||
def read_file_info(self):
|
||||
"""Read file information and metadata."""
|
||||
try:
|
||||
if os.path.exists(self.path):
|
||||
return mutagen.File(self.path)
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
def find_episode(self, sound, path_info):
|
||||
"""For a given program, check if there is an initial diffusion to
|
||||
associate to, using the date info we have. Update self.sound and save
|
||||
it consequently.
|
||||
|
||||
We only allow initial diffusion since there should be no rerun.
|
||||
"""
|
||||
program, pi = sound.program, path_info
|
||||
if "year" not in pi or not sound or sound.episode:
|
||||
return None
|
||||
|
||||
year, month, day = pi.get("year"), pi.get("month"), pi.get("day")
|
||||
if pi.get("hour") is not None:
|
||||
at = tz.datetime(
|
||||
year, month, day, pi.get("hour", 0), pi.get("minute", 0)
|
||||
)
|
||||
at = tz.make_aware(at)
|
||||
else:
|
||||
at = date(year, month, day)
|
||||
|
||||
diffusion = program.diffusion_set.at(at).first()
|
||||
if not diffusion:
|
||||
return None
|
||||
|
||||
logger.debug("%s <--> %s", sound.file.name, str(diffusion.episode))
|
||||
return diffusion.episode
|
||||
|
||||
def find_playlist(self, sound=None, use_meta=True):
|
||||
"""Find a playlist file corresponding to the sound path, such as:
|
||||
my_sound.ogg => my_sound.csv.
|
||||
|
||||
Use sound's file metadata if no corresponding playlist has been
|
||||
found and `use_meta` is True.
|
||||
"""
|
||||
if sound is None:
|
||||
sound = self.sound
|
||||
if sound.track_set.count() > 1:
|
||||
return
|
||||
|
||||
# import playlist
|
||||
path_noext, ext = os.path.splitext(self.sound.file.path)
|
||||
path = path_noext + ".csv"
|
||||
if os.path.exists(path):
|
||||
PlaylistImport(path, sound=sound).run()
|
||||
# use metadata
|
||||
elif use_meta:
|
||||
if self.info is None:
|
||||
self.read_file_info()
|
||||
if self.info and self.info.tags:
|
||||
tags = self.info.tags
|
||||
title, artist, album, year = tuple(
|
||||
t and ", ".join(t)
|
||||
for t in (
|
||||
tags.get(k)
|
||||
for k in ("title", "artist", "album", "year")
|
||||
)
|
||||
)
|
||||
title = (
|
||||
title
|
||||
or (self.path_info and self.path_info.get("name"))
|
||||
or os.path.basename(path_noext)
|
||||
)
|
||||
info = (
|
||||
"{} ({})".format(album, year)
|
||||
if album and year
|
||||
else album or year or ""
|
||||
)
|
||||
track = Track(
|
||||
sound=sound,
|
||||
position=int(tags.get("tracknumber", 0)),
|
||||
title=title,
|
||||
artist=artist or _("unknown"),
|
||||
info=info,
|
||||
)
|
||||
track.save()
|
||||
171
aircox/management/sound_monitor.py
Normal file
171
aircox/management/sound_monitor.py
Normal file
@ -0,0 +1,171 @@
|
||||
#! /usr/bin/env python3
|
||||
|
||||
"""Monitor sound files; For each program, check for:
|
||||
|
||||
- new files;
|
||||
- deleted files;
|
||||
- differences between files and sound;
|
||||
- quality of the files;
|
||||
|
||||
It tries to parse the file name to get the date of the diffusion of an
|
||||
episode and associate the file with it; WNotifye the following format:
|
||||
yyyymmdd[_n][_][name]
|
||||
|
||||
Where:
|
||||
'yyyy' the year Notifyhe episode's diffusion;
|
||||
'mm' the month of the episode's difNotifyon;
|
||||
'dd' the day of the episode's diffusion;
|
||||
'n' the number of the episode (if multiple episodes);
|
||||
'name' the title of the sNotify;
|
||||
|
||||
|
||||
To check quality of files, call the command sound_quality_check using the
|
||||
parameters given by the setting SOUND_QUALITY. This script requires
|
||||
Sox (and soxi).
|
||||
"""
|
||||
import logging
|
||||
import time
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from watchdog.events import PatternMatchingEventHandler
|
||||
|
||||
from aircox.conf import settings
|
||||
from aircox.models import Sound
|
||||
|
||||
from .sound_file import SoundFile
|
||||
|
||||
logger = logging.getLogger("aircox.commands")
|
||||
|
||||
|
||||
__all__ = (
|
||||
"NotifyHandler",
|
||||
"CreateHandler",
|
||||
"DeleteHandler",
|
||||
"MoveHandler",
|
||||
"ModifiedHandler",
|
||||
"MonitorHandler",
|
||||
)
|
||||
|
||||
|
||||
class NotifyHandler:
|
||||
future = None
|
||||
log_msg = None
|
||||
|
||||
def __init__(self):
|
||||
self.timestamp = datetime.now()
|
||||
|
||||
def ping(self):
|
||||
self.timestamp = datetime.now()
|
||||
|
||||
def __call__(self, event, path=None, **kw):
|
||||
sound_file = SoundFile(path or event.src_path)
|
||||
if self.log_msg:
|
||||
msg = self.log_msg.format(event=event, sound_file=sound_file)
|
||||
logger.info(msg)
|
||||
|
||||
sound_file.sync(**kw)
|
||||
return sound_file
|
||||
|
||||
|
||||
class CreateHandler(NotifyHandler):
|
||||
log_msg = "Sound file created: {sound_file.path}"
|
||||
|
||||
|
||||
class DeleteHandler(NotifyHandler):
|
||||
log_msg = "Sound file deleted: {sound_file.path}"
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
kwargs["deleted"] = True
|
||||
return super().__call__(*args, **kwargs)
|
||||
|
||||
|
||||
class MoveHandler(NotifyHandler):
|
||||
log_msg = "Sound file moved: {event.src_path} -> {event.dest_path}"
|
||||
|
||||
def __call__(self, event, **kw):
|
||||
sound = Sound.objects.filter(file=event.src_path)
|
||||
# FIXME: this is wrong
|
||||
if sound:
|
||||
kw["sound"] = sound
|
||||
kw["path"] = event.src_path
|
||||
else:
|
||||
kw["path"] = event.dest_path
|
||||
return super().__call__(event, **kw)
|
||||
|
||||
|
||||
class ModifiedHandler(NotifyHandler):
|
||||
timeout_delta = timedelta(seconds=30)
|
||||
log_msg = "Sound file updated: {sound_file.path}"
|
||||
|
||||
def wait(self):
|
||||
# multiple call of this handler can be done consecutively, we block
|
||||
# its thread using timeout
|
||||
# Note: this method may be subject to some race conflicts, but this
|
||||
# should not be big a real issue.
|
||||
timeout = self.timestamp + self.timeout_delta
|
||||
while datetime.now() < timeout:
|
||||
time.sleep(self.timeout_delta.total_seconds())
|
||||
timeout = self.timestamp + self.timeout_delta
|
||||
|
||||
def __call__(self, event, **kw):
|
||||
self.wait()
|
||||
return super().__call__(event, **kw)
|
||||
|
||||
|
||||
class MonitorHandler(PatternMatchingEventHandler):
|
||||
"""Event handler for watchdog, in order to be used in monitoring."""
|
||||
|
||||
pool = None
|
||||
jobs = {}
|
||||
|
||||
def __init__(self, subdir, pool, **sync_kw):
|
||||
"""
|
||||
:param str subdir: sub-directory in program dirs to monitor \
|
||||
(SOUND_ARCHIVES_SUBDIR or SOUND_EXCERPTS_SUBDIR);
|
||||
:param concurrent.futures.Executor pool: pool executing jobs on file
|
||||
change;
|
||||
:param **sync_kw: kwargs passed to `SoundFile.sync`;
|
||||
"""
|
||||
self.subdir = subdir
|
||||
self.pool = pool
|
||||
self.sync_kw = sync_kw
|
||||
|
||||
patterns = [
|
||||
"*/{}/*{}".format(self.subdir, ext)
|
||||
for ext in settings.SOUND_FILE_EXT
|
||||
]
|
||||
super().__init__(patterns=patterns, ignore_directories=True)
|
||||
|
||||
def on_created(self, event):
|
||||
self._submit(CreateHandler(), event, "new", **self.sync_kw)
|
||||
|
||||
def on_deleted(self, event):
|
||||
self._submit(DeleteHandler(), event, "del")
|
||||
|
||||
def on_moved(self, event):
|
||||
self._submit(MoveHandler(), event, "mv", **self.sync_kw)
|
||||
|
||||
def on_modified(self, event):
|
||||
self._submit(ModifiedHandler(), event, "up", **self.sync_kw)
|
||||
|
||||
def _submit(self, handler, event, job_key_prefix, **kwargs):
|
||||
"""Send handler job to pool if not already running.
|
||||
|
||||
Return tuple with running job and boolean indicating if its a
|
||||
new one.
|
||||
"""
|
||||
key = job_key_prefix + ":" + event.src_path
|
||||
job = self.jobs.get(key)
|
||||
if job and not job.future.done():
|
||||
job.ping()
|
||||
return job, False
|
||||
|
||||
handler.future = self.pool.submit(handler, event, **kwargs)
|
||||
self.jobs[key] = handler
|
||||
|
||||
def done(r):
|
||||
if self.jobs.get(key) is handler:
|
||||
del self.jobs[key]
|
||||
|
||||
handler.future.add_done_callback(done)
|
||||
return handler, True
|
||||
@ -24,28 +24,16 @@ class SoxStats:
|
||||
"Length s",
|
||||
]
|
||||
|
||||
values = None
|
||||
|
||||
def __init__(self, path=None, **kwargs):
|
||||
def __init__(self, path, **kwargs):
|
||||
"""If path is given, call analyse with path and kwargs."""
|
||||
self.values = {}
|
||||
if path:
|
||||
self.analyse(path, **kwargs)
|
||||
|
||||
def analyse(self, path, at=None, length=None):
|
||||
"""If at and length are given use them as excerpt to analyse."""
|
||||
args = ["sox", path, "-n"]
|
||||
if at is not None and length is not None:
|
||||
args += ["trim", str(at), str(length)]
|
||||
args.append("stats")
|
||||
|
||||
p = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
# sox outputs to stderr (my god WHYYYY)
|
||||
out_, out = p.communicate()
|
||||
self.values = self.parse(str(out, encoding="utf-8"))
|
||||
def get(self, attr):
|
||||
return self.values.get(attr)
|
||||
|
||||
def parse(self, output):
|
||||
"""Parse sox output, settubg values from it."""
|
||||
values = {}
|
||||
for attr in self.attributes:
|
||||
value = re.search(attr + r"\s+(?P<value>\S+)", output)
|
||||
value = value and value.groupdict()
|
||||
@ -54,12 +42,24 @@ class SoxStats:
|
||||
value = float(value.get("value"))
|
||||
except ValueError:
|
||||
value = None
|
||||
values[attr] = value
|
||||
values["length"] = values.pop("Length s", None)
|
||||
return values
|
||||
self.values[attr] = value
|
||||
self.values["length"] = self.values["Length s"]
|
||||
|
||||
def get(self, attr):
|
||||
return self.values.get(attr)
|
||||
def analyse(self, path, at=None, length=None):
|
||||
"""If at and length are given use them as excerpt to analyse."""
|
||||
args = ["sox", path, "-n"]
|
||||
|
||||
if at is not None and length is not None:
|
||||
args += ["trim", str(at), str(length)]
|
||||
|
||||
args.append("stats")
|
||||
|
||||
p = subprocess.Popen(
|
||||
args, stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
||||
)
|
||||
# sox outputs to stderr (my god WHYYYY)
|
||||
out_, out = p.communicate()
|
||||
self.parse(str(out, encoding="utf-8"))
|
||||
|
||||
|
||||
class SoundStats:
|
||||
@ -71,17 +71,19 @@ class SoundStats:
|
||||
|
||||
def __init__(self, path, sample_length=None):
|
||||
self.path = path
|
||||
if sample_length is not None:
|
||||
self.sample_length = sample_length
|
||||
self.sample_length = (
|
||||
sample_length if sample_length is not None else self.sample_length
|
||||
)
|
||||
|
||||
def get_file_stats(self):
|
||||
return self.stats and self.stats[0] or None
|
||||
return self.stats and self.stats[0]
|
||||
|
||||
def analyse(self):
|
||||
logger.debug("complete file analysis")
|
||||
self.stats = [SoxStats(self.path)]
|
||||
position = 0
|
||||
length = self.stats[0].get("length")
|
||||
|
||||
if not self.sample_length:
|
||||
return
|
||||
|
||||
@ -92,24 +94,36 @@ class SoundStats:
|
||||
position += self.sample_length
|
||||
|
||||
def check(self, name, min_val, max_val):
|
||||
self.good = [index for index, stats in enumerate(self.stats) if min_val <= stats.get(name) <= max_val]
|
||||
self.bad = [index for index, stats in enumerate(self.stats) if index not in self.good]
|
||||
self.good = [
|
||||
index
|
||||
for index, stats in enumerate(self.stats)
|
||||
if min_val <= stats.get(name) <= max_val
|
||||
]
|
||||
self.bad = [
|
||||
index
|
||||
for index, stats in enumerate(self.stats)
|
||||
if index not in self.good
|
||||
]
|
||||
self.resume()
|
||||
|
||||
def resume(self):
|
||||
def view(array):
|
||||
return [
|
||||
"file"
|
||||
if index == 0
|
||||
else "sample {} (at {} seconds)".format(
|
||||
index, (index - 1) * self.sample_length
|
||||
)
|
||||
for index in array
|
||||
]
|
||||
|
||||
if self.good:
|
||||
logger.debug(
|
||||
self.path + " -> good: \033[92m%s\033[0m",
|
||||
", ".join(self._view(self.good)),
|
||||
", ".join(view(self.good)),
|
||||
)
|
||||
if self.bad:
|
||||
logger.debug(
|
||||
self.path + " -> bad: \033[91m%s\033[0m",
|
||||
", ".join(self._view(self.bad)),
|
||||
", ".join(view(self.bad)),
|
||||
)
|
||||
|
||||
def _view(self, array):
|
||||
return [
|
||||
"file" if index == 0 else "sample {} (at {} seconds)".format(index, (index - 1) * self.sample_length)
|
||||
for index in array
|
||||
]
|
||||
@ -1,5 +1,4 @@
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
import pytz
|
||||
from django.db.models import Q
|
||||
from django.utils import timezone as tz
|
||||
|
||||
@ -12,46 +11,41 @@ __all__ = ("AircoxMiddleware",)
|
||||
class AircoxMiddleware(object):
|
||||
"""Middleware used to get default info for the given website.
|
||||
|
||||
It provide following request attributes:
|
||||
- ``mobile``: set to True if mobile device is detected
|
||||
- ``station``: current Station
|
||||
|
||||
Theses
|
||||
This middleware must be set after the middleware
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
"""
|
||||
|
||||
timezone_session_key = "aircox.timezone"
|
||||
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
|
||||
def is_mobile(self, request):
|
||||
if agent := request.META.get("HTTP_USER_AGENT"):
|
||||
return " Mobi" in agent
|
||||
return False
|
||||
|
||||
def get_station(self, request):
|
||||
"""Return station for the provided request."""
|
||||
host = request.get_host()
|
||||
expr = Q(default=True) | Q(hosts=host) | Q(hosts__contains=host + "\n")
|
||||
expr = Q(default=True) | Q(hosts__contains=request.get_host())
|
||||
# case = Case(When(hosts__contains=request.get_host(), then=Value(0)),
|
||||
# When(default=True, then=Value(32)))
|
||||
return Station.objects.filter(expr).order_by("default").first()
|
||||
# .annotate(resolve_priority=case) \
|
||||
# .order_by('resolve_priority').first()
|
||||
|
||||
def init_timezone(self, request):
|
||||
# note: later we can use http://freegeoip.net/ on user side if
|
||||
# required
|
||||
timezone = None
|
||||
try:
|
||||
timezone = request.session.get(self.timezone_session_key)
|
||||
timezone = request.session.get("aircox.timezone")
|
||||
if timezone:
|
||||
timezone = ZoneInfo(timezone)
|
||||
tz.activate(timezone)
|
||||
timezone = pytz.timezone(timezone)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if not timezone:
|
||||
timezone = tz.get_current_timezone()
|
||||
tz.activate(timezone)
|
||||
|
||||
def __call__(self, request):
|
||||
self.init_timezone(request)
|
||||
request.station = self.get_station(request)
|
||||
request.is_mobile = self.is_mobile(request)
|
||||
try:
|
||||
return self.get_response(request)
|
||||
except Redirect:
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
# Generated by Django 3.0.6 on 2020-05-26 12:57
|
||||
|
||||
import ckeditor.fields
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
@ -83,7 +84,9 @@ class Migration(migrations.Migration):
|
||||
options={
|
||||
"verbose_name": "Diffusion",
|
||||
"verbose_name_plural": "Diffusions",
|
||||
"permissions": (("programming", "edit the diffusion's planification"),),
|
||||
"permissions": (
|
||||
("programming", "edit the diffusion's planification"),
|
||||
),
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
@ -122,16 +125,22 @@ class Migration(migrations.Migration):
|
||||
),
|
||||
(
|
||||
"content",
|
||||
models.TextField(blank=True, null=True, verbose_name="content"),
|
||||
ckeditor.fields.RichTextField(
|
||||
blank=True, null=True, verbose_name="content"
|
||||
),
|
||||
),
|
||||
("pub_date", models.DateTimeField(blank=True, null=True)),
|
||||
(
|
||||
"featured",
|
||||
models.BooleanField(default=False, verbose_name="featured"),
|
||||
models.BooleanField(
|
||||
default=False, verbose_name="featured"
|
||||
),
|
||||
),
|
||||
(
|
||||
"allow_comments",
|
||||
models.BooleanField(default=True, verbose_name="allow comments"),
|
||||
models.BooleanField(
|
||||
default=True, verbose_name="allow comments"
|
||||
),
|
||||
),
|
||||
(
|
||||
"category",
|
||||
@ -332,7 +341,7 @@ class Migration(migrations.Migration):
|
||||
"active",
|
||||
models.BooleanField(
|
||||
default=True,
|
||||
help_text="if not chemodels.onger active",
|
||||
help_text="if not checked this program is no longer active",
|
||||
verbose_name="active",
|
||||
),
|
||||
),
|
||||
@ -449,7 +458,9 @@ class Migration(migrations.Migration):
|
||||
("name", models.CharField(max_length=64, verbose_name="name")),
|
||||
(
|
||||
"slug",
|
||||
models.SlugField(max_length=64, unique=True, verbose_name="slug"),
|
||||
models.SlugField(
|
||||
max_length=64, unique=True, verbose_name="slug"
|
||||
),
|
||||
),
|
||||
(
|
||||
"path",
|
||||
@ -555,7 +566,9 @@ class Migration(migrations.Migration):
|
||||
),
|
||||
(
|
||||
"content",
|
||||
models.TextField(blank=True, null=True, verbose_name="content"),
|
||||
ckeditor.fields.RichTextField(
|
||||
blank=True, null=True, verbose_name="content"
|
||||
),
|
||||
),
|
||||
(
|
||||
"view",
|
||||
@ -936,7 +949,9 @@ class Migration(migrations.Migration):
|
||||
),
|
||||
(
|
||||
"time",
|
||||
models.TimeField(help_text="start time", verbose_name="time"),
|
||||
models.TimeField(
|
||||
help_text="start time", verbose_name="time"
|
||||
),
|
||||
),
|
||||
(
|
||||
"timezone",
|
||||
@ -1628,7 +1643,9 @@ class Migration(migrations.Migration):
|
||||
),
|
||||
(
|
||||
"duration",
|
||||
models.TimeField(help_text="regular duration", verbose_name="duration"),
|
||||
models.TimeField(
|
||||
help_text="regular duration", verbose_name="duration"
|
||||
),
|
||||
),
|
||||
(
|
||||
"frequency",
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
# 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
|
||||
|
||||
@ -29,6 +30,13 @@ class Migration(migrations.Migration):
|
||||
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",
|
||||
@ -41,4 +49,11 @@ class Migration(migrations.Migration):
|
||||
),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="staticpage",
|
||||
name="content",
|
||||
field=ckeditor_uploader.fields.RichTextUploadingField(
|
||||
blank=True, null=True, verbose_name="content"
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
@ -12,7 +12,9 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterModelOptions(
|
||||
name="diffusion",
|
||||
options={
|
||||
"permissions": (("programming", "edit the diffusions' planification"),),
|
||||
"permissions": (
|
||||
("programming", "edit the diffusions' planification"),
|
||||
),
|
||||
"verbose_name": "Diffusion",
|
||||
"verbose_name_plural": "Diffusions",
|
||||
},
|
||||
@ -20,7 +22,9 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name="track",
|
||||
name="album",
|
||||
field=models.CharField(default="", max_length=128, verbose_name="album"),
|
||||
field=models.CharField(
|
||||
default="", max_length=128, verbose_name="album"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="schedule",
|
||||
|
||||
@ -12,6 +12,8 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name="track",
|
||||
name="year",
|
||||
field=models.IntegerField(blank=True, null=True, verbose_name="year"),
|
||||
field=models.IntegerField(
|
||||
blank=True, null=True, verbose_name="year"
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
@ -12,6 +12,8 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name="track",
|
||||
name="album",
|
||||
field=models.CharField(blank=True, max_length=128, null=True, verbose_name="album"),
|
||||
field=models.CharField(
|
||||
blank=True, max_length=128, null=True, verbose_name="album"
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
@ -30,7 +30,9 @@ class Migration(migrations.Migration):
|
||||
),
|
||||
(
|
||||
"playlist_editor_sep",
|
||||
models.CharField(max_length=16, verbose_name="Playlist Editor Separator"),
|
||||
models.CharField(
|
||||
max_length=16, verbose_name="Playlist Editor Separator"
|
||||
),
|
||||
),
|
||||
(
|
||||
"user",
|
||||
|
||||
@ -1,675 +0,0 @@
|
||||
# Generated by Django 4.2.1 on 2023-09-28 11:07
|
||||
|
||||
import aircox.models.schedule
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("aircox", "0012_alter_sound_file_alter_station_default"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="schedule",
|
||||
name="timezone",
|
||||
field=models.CharField(
|
||||
choices=[
|
||||
("Africa/Mogadishu", "Africa/Mogadishu"),
|
||||
("Pacific/Guadalcanal", "Pacific/Guadalcanal"),
|
||||
("Asia/Baku", "Asia/Baku"),
|
||||
("America/Thunder_Bay", "America/Thunder_Bay"),
|
||||
("Etc/GMT-10", "Etc/GMT-10"),
|
||||
("UTC", "UTC"),
|
||||
("Europe/Uzhgorod", "Europe/Uzhgorod"),
|
||||
("ROC", "ROC"),
|
||||
("Asia/Seoul", "Asia/Seoul"),
|
||||
("Europe/Moscow", "Europe/Moscow"),
|
||||
("Australia/Melbourne", "Australia/Melbourne"),
|
||||
("Asia/Manila", "Asia/Manila"),
|
||||
("America/Tegucigalpa", "America/Tegucigalpa"),
|
||||
("Australia/Adelaide", "Australia/Adelaide"),
|
||||
(
|
||||
"America/Argentina/Rio_Gallegos",
|
||||
"America/Argentina/Rio_Gallegos",
|
||||
),
|
||||
("Brazil/Acre", "Brazil/Acre"),
|
||||
("America/Porto_Acre", "America/Porto_Acre"),
|
||||
("Europe/Nicosia", "Europe/Nicosia"),
|
||||
("Europe/Vienna", "Europe/Vienna"),
|
||||
("GB-Eire", "GB-Eire"),
|
||||
("US/Mountain", "US/Mountain"),
|
||||
("Etc/GMT-2", "Etc/GMT-2"),
|
||||
("America/Buenos_Aires", "America/Buenos_Aires"),
|
||||
("Africa/Malabo", "Africa/Malabo"),
|
||||
("Asia/Qostanay", "Asia/Qostanay"),
|
||||
("America/Noronha", "America/Noronha"),
|
||||
("Etc/GMT+2", "Etc/GMT+2"),
|
||||
("Asia/Novosibirsk", "Asia/Novosibirsk"),
|
||||
("America/Ensenada", "America/Ensenada"),
|
||||
("Africa/Bujumbura", "Africa/Bujumbura"),
|
||||
("America/Anchorage", "America/Anchorage"),
|
||||
("America/Miquelon", "America/Miquelon"),
|
||||
("Europe/Simferopol", "Europe/Simferopol"),
|
||||
("America/Martinique", "America/Martinique"),
|
||||
("Canada/Eastern", "Canada/Eastern"),
|
||||
("Asia/Ujung_Pandang", "Asia/Ujung_Pandang"),
|
||||
("America/St_Vincent", "America/St_Vincent"),
|
||||
("America/Dawson_Creek", "America/Dawson_Creek"),
|
||||
("Pacific/Yap", "Pacific/Yap"),
|
||||
("America/St_Lucia", "America/St_Lucia"),
|
||||
("CET", "CET"),
|
||||
("Africa/Monrovia", "Africa/Monrovia"),
|
||||
("Etc/Universal", "Etc/Universal"),
|
||||
("America/Belem", "America/Belem"),
|
||||
("US/Pacific", "US/Pacific"),
|
||||
("Africa/Dakar", "Africa/Dakar"),
|
||||
("Europe/Belfast", "Europe/Belfast"),
|
||||
("Pacific/Funafuti", "Pacific/Funafuti"),
|
||||
("Africa/Casablanca", "Africa/Casablanca"),
|
||||
(
|
||||
"America/Kentucky/Monticello",
|
||||
"America/Kentucky/Monticello",
|
||||
),
|
||||
("Etc/Greenwich", "Etc/Greenwich"),
|
||||
("Indian/Chagos", "Indian/Chagos"),
|
||||
("Asia/Shanghai", "Asia/Shanghai"),
|
||||
("Mexico/BajaSur", "Mexico/BajaSur"),
|
||||
("Europe/Madrid", "Europe/Madrid"),
|
||||
("America/Lower_Princes", "America/Lower_Princes"),
|
||||
("Europe/Busingen", "Europe/Busingen"),
|
||||
("Asia/Macao", "Asia/Macao"),
|
||||
("Australia/Tasmania", "Australia/Tasmania"),
|
||||
("Asia/Saigon", "Asia/Saigon"),
|
||||
("America/Nipigon", "America/Nipigon"),
|
||||
("MST", "MST"),
|
||||
("America/Juneau", "America/Juneau"),
|
||||
("Singapore", "Singapore"),
|
||||
("Pacific/Kosrae", "Pacific/Kosrae"),
|
||||
("America/Argentina/Cordoba", "America/Argentina/Cordoba"),
|
||||
("HST", "HST"),
|
||||
("Indian/Christmas", "Indian/Christmas"),
|
||||
("Indian/Kerguelen", "Indian/Kerguelen"),
|
||||
("America/Port-au-Prince", "America/Port-au-Prince"),
|
||||
("Europe/Monaco", "Europe/Monaco"),
|
||||
("Asia/Pyongyang", "Asia/Pyongyang"),
|
||||
("Australia/Darwin", "Australia/Darwin"),
|
||||
("Asia/Ulaanbaatar", "Asia/Ulaanbaatar"),
|
||||
("Asia/Amman", "Asia/Amman"),
|
||||
(
|
||||
"America/Argentina/San_Juan",
|
||||
"America/Argentina/San_Juan",
|
||||
),
|
||||
("Indian/Reunion", "Indian/Reunion"),
|
||||
("America/Coral_Harbour", "America/Coral_Harbour"),
|
||||
("Antarctica/Davis", "Antarctica/Davis"),
|
||||
("Europe/Kyiv", "Europe/Kyiv"),
|
||||
("America/Argentina/Tucuman", "America/Argentina/Tucuman"),
|
||||
("Pacific/Tarawa", "Pacific/Tarawa"),
|
||||
("Pacific/Kwajalein", "Pacific/Kwajalein"),
|
||||
("America/Metlakatla", "America/Metlakatla"),
|
||||
("Australia/Canberra", "Australia/Canberra"),
|
||||
("Europe/Rome", "Europe/Rome"),
|
||||
("Pacific/Fakaofo", "Pacific/Fakaofo"),
|
||||
("Europe/Tirane", "Europe/Tirane"),
|
||||
("Asia/Dhaka", "Asia/Dhaka"),
|
||||
("Europe/Mariehamn", "Europe/Mariehamn"),
|
||||
("America/New_York", "America/New_York"),
|
||||
("Pacific/Johnston", "Pacific/Johnston"),
|
||||
("Africa/Abidjan", "Africa/Abidjan"),
|
||||
("Pacific/Noumea", "Pacific/Noumea"),
|
||||
("Canada/Central", "Canada/Central"),
|
||||
("Pacific/Pohnpei", "Pacific/Pohnpei"),
|
||||
("America/Rosario", "America/Rosario"),
|
||||
("Asia/Baghdad", "Asia/Baghdad"),
|
||||
("America/Argentina/Salta", "America/Argentina/Salta"),
|
||||
("Canada/Pacific", "Canada/Pacific"),
|
||||
("US/Indiana-Starke", "US/Indiana-Starke"),
|
||||
("America/Cuiaba", "America/Cuiaba"),
|
||||
("Asia/Barnaul", "Asia/Barnaul"),
|
||||
("Pacific/Gambier", "Pacific/Gambier"),
|
||||
("America/Mazatlan", "America/Mazatlan"),
|
||||
("Europe/Helsinki", "Europe/Helsinki"),
|
||||
("Asia/Urumqi", "Asia/Urumqi"),
|
||||
("Indian/Maldives", "Indian/Maldives"),
|
||||
("CST6CDT", "CST6CDT"),
|
||||
("Africa/Blantyre", "Africa/Blantyre"),
|
||||
("Europe/Minsk", "Europe/Minsk"),
|
||||
("Asia/Samarkand", "Asia/Samarkand"),
|
||||
("US/Michigan", "US/Michigan"),
|
||||
("Etc/GMT+6", "Etc/GMT+6"),
|
||||
("Asia/Nicosia", "Asia/Nicosia"),
|
||||
("America/Bahia_Banderas", "America/Bahia_Banderas"),
|
||||
("Europe/Bratislava", "Europe/Bratislava"),
|
||||
("Atlantic/South_Georgia", "Atlantic/South_Georgia"),
|
||||
("NZ-CHAT", "NZ-CHAT"),
|
||||
("Antarctica/Troll", "Antarctica/Troll"),
|
||||
(
|
||||
"America/Argentina/La_Rioja",
|
||||
"America/Argentina/La_Rioja",
|
||||
),
|
||||
("Etc/GMT+12", "Etc/GMT+12"),
|
||||
("Africa/Gaborone", "Africa/Gaborone"),
|
||||
("Asia/Ust-Nera", "Asia/Ust-Nera"),
|
||||
("Etc/GMT-14", "Etc/GMT-14"),
|
||||
("Africa/Luanda", "Africa/Luanda"),
|
||||
("America/Denver", "America/Denver"),
|
||||
("Antarctica/Vostok", "Antarctica/Vostok"),
|
||||
("America/Pangnirtung", "America/Pangnirtung"),
|
||||
("Africa/Ndjamena", "Africa/Ndjamena"),
|
||||
("GMT-0", "GMT-0"),
|
||||
("Australia/Victoria", "Australia/Victoria"),
|
||||
("Africa/Ouagadougou", "Africa/Ouagadougou"),
|
||||
("Europe/Berlin", "Europe/Berlin"),
|
||||
("Etc/GMT0", "Etc/GMT0"),
|
||||
("America/Halifax", "America/Halifax"),
|
||||
(
|
||||
"America/North_Dakota/New_Salem",
|
||||
"America/North_Dakota/New_Salem",
|
||||
),
|
||||
("NZ", "NZ"),
|
||||
("America/Nome", "America/Nome"),
|
||||
("Europe/Brussels", "Europe/Brussels"),
|
||||
("Europe/Gibraltar", "Europe/Gibraltar"),
|
||||
("Africa/Asmara", "Africa/Asmara"),
|
||||
("Africa/Lusaka", "Africa/Lusaka"),
|
||||
("America/Cancun", "America/Cancun"),
|
||||
("Iran", "Iran"),
|
||||
("Asia/Brunei", "Asia/Brunei"),
|
||||
("America/Barbados", "America/Barbados"),
|
||||
("Asia/Aqtau", "Asia/Aqtau"),
|
||||
("Asia/Ashkhabad", "Asia/Ashkhabad"),
|
||||
("America/Punta_Arenas", "America/Punta_Arenas"),
|
||||
("America/Dominica", "America/Dominica"),
|
||||
("Etc/GMT-1", "Etc/GMT-1"),
|
||||
("Etc/GMT", "Etc/GMT"),
|
||||
("Europe/Kaliningrad", "Europe/Kaliningrad"),
|
||||
(
|
||||
"America/Indiana/Petersburg",
|
||||
"America/Indiana/Petersburg",
|
||||
),
|
||||
("Africa/Harare", "Africa/Harare"),
|
||||
("US/Alaska", "US/Alaska"),
|
||||
("Asia/Chongqing", "Asia/Chongqing"),
|
||||
("Asia/Jakarta", "Asia/Jakarta"),
|
||||
("Etc/GMT-8", "Etc/GMT-8"),
|
||||
("Asia/Katmandu", "Asia/Katmandu"),
|
||||
("Africa/Maputo", "Africa/Maputo"),
|
||||
("Indian/Antananarivo", "Indian/Antananarivo"),
|
||||
("America/Havana", "America/Havana"),
|
||||
("Asia/Chungking", "Asia/Chungking"),
|
||||
("Pacific/Pago_Pago", "Pacific/Pago_Pago"),
|
||||
("America/Fortaleza", "America/Fortaleza"),
|
||||
("America/Campo_Grande", "America/Campo_Grande"),
|
||||
("America/Rio_Branco", "America/Rio_Branco"),
|
||||
("America/Bogota", "America/Bogota"),
|
||||
("Asia/Kuala_Lumpur", "Asia/Kuala_Lumpur"),
|
||||
("Australia/North", "Australia/North"),
|
||||
("Etc/GMT-6", "Etc/GMT-6"),
|
||||
("Europe/Samara", "Europe/Samara"),
|
||||
("GMT0", "GMT0"),
|
||||
("Europe/Paris", "Europe/Paris"),
|
||||
("America/Vancouver", "America/Vancouver"),
|
||||
("America/Santiago", "America/Santiago"),
|
||||
("America/Paramaribo", "America/Paramaribo"),
|
||||
("America/Blanc-Sablon", "America/Blanc-Sablon"),
|
||||
("America/Manaus", "America/Manaus"),
|
||||
("America/Grand_Turk", "America/Grand_Turk"),
|
||||
("America/Yakutat", "America/Yakutat"),
|
||||
("Africa/El_Aaiun", "Africa/El_Aaiun"),
|
||||
("America/Edmonton", "America/Edmonton"),
|
||||
("Europe/Athens", "Europe/Athens"),
|
||||
("America/Guayaquil", "America/Guayaquil"),
|
||||
("America/Puerto_Rico", "America/Puerto_Rico"),
|
||||
("Atlantic/St_Helena", "Atlantic/St_Helena"),
|
||||
("Pacific/Kanton", "Pacific/Kanton"),
|
||||
("Africa/Ceuta", "Africa/Ceuta"),
|
||||
("America/Kralendijk", "America/Kralendijk"),
|
||||
("Pacific/Midway", "Pacific/Midway"),
|
||||
("Zulu", "Zulu"),
|
||||
("Asia/Tehran", "Asia/Tehran"),
|
||||
(
|
||||
"America/North_Dakota/Beulah",
|
||||
"America/North_Dakota/Beulah",
|
||||
),
|
||||
(
|
||||
"America/Argentina/Buenos_Aires",
|
||||
"America/Argentina/Buenos_Aires",
|
||||
),
|
||||
("Asia/Novokuznetsk", "Asia/Novokuznetsk"),
|
||||
("America/Danmarkshavn", "America/Danmarkshavn"),
|
||||
("America/Yellowknife", "America/Yellowknife"),
|
||||
("America/Indiana/Marengo", "America/Indiana/Marengo"),
|
||||
("Africa/Tripoli", "Africa/Tripoli"),
|
||||
("Europe/Skopje", "Europe/Skopje"),
|
||||
("Australia/NSW", "Australia/NSW"),
|
||||
("Australia/Currie", "Australia/Currie"),
|
||||
("Antarctica/Rothera", "Antarctica/Rothera"),
|
||||
("Asia/Gaza", "Asia/Gaza"),
|
||||
("Africa/Douala", "Africa/Douala"),
|
||||
("Africa/Nouakchott", "Africa/Nouakchott"),
|
||||
("Poland", "Poland"),
|
||||
("America/Sao_Paulo", "America/Sao_Paulo"),
|
||||
(
|
||||
"America/Argentina/Catamarca",
|
||||
"America/Argentina/Catamarca",
|
||||
),
|
||||
("Antarctica/Palmer", "Antarctica/Palmer"),
|
||||
("Europe/London", "Europe/London"),
|
||||
("America/Indiana/Winamac", "America/Indiana/Winamac"),
|
||||
("America/Godthab", "America/Godthab"),
|
||||
("Europe/Warsaw", "Europe/Warsaw"),
|
||||
("Etc/Zulu", "Etc/Zulu"),
|
||||
("Africa/Cairo", "Africa/Cairo"),
|
||||
("Africa/Brazzaville", "Africa/Brazzaville"),
|
||||
("Indian/Comoro", "Indian/Comoro"),
|
||||
("Europe/Riga", "Europe/Riga"),
|
||||
("America/Port_of_Spain", "America/Port_of_Spain"),
|
||||
("Pacific/Samoa", "Pacific/Samoa"),
|
||||
("Pacific/Fiji", "Pacific/Fiji"),
|
||||
("Africa/Timbuktu", "Africa/Timbuktu"),
|
||||
("Etc/GMT-9", "Etc/GMT-9"),
|
||||
("Asia/Thimphu", "Asia/Thimphu"),
|
||||
("Pacific/Auckland", "Pacific/Auckland"),
|
||||
("Africa/Windhoek", "Africa/Windhoek"),
|
||||
("America/Los_Angeles", "America/Los_Angeles"),
|
||||
("America/Managua", "America/Managua"),
|
||||
("Pacific/Majuro", "Pacific/Majuro"),
|
||||
("America/Adak", "America/Adak"),
|
||||
("Etc/UCT", "Etc/UCT"),
|
||||
("Mexico/BajaNorte", "Mexico/BajaNorte"),
|
||||
("US/Hawaii", "US/Hawaii"),
|
||||
("Europe/Vilnius", "Europe/Vilnius"),
|
||||
("Asia/Dushanbe", "Asia/Dushanbe"),
|
||||
("Asia/Kuwait", "Asia/Kuwait"),
|
||||
("Asia/Dili", "Asia/Dili"),
|
||||
("America/El_Salvador", "America/El_Salvador"),
|
||||
("US/Aleutian", "US/Aleutian"),
|
||||
("Etc/GMT-3", "Etc/GMT-3"),
|
||||
("Pacific/Rarotonga", "Pacific/Rarotonga"),
|
||||
("America/Moncton", "America/Moncton"),
|
||||
("America/Rankin_Inlet", "America/Rankin_Inlet"),
|
||||
("Africa/Kinshasa", "Africa/Kinshasa"),
|
||||
("Asia/Chita", "Asia/Chita"),
|
||||
("America/Cayenne", "America/Cayenne"),
|
||||
("Africa/Bissau", "Africa/Bissau"),
|
||||
("Pacific/Bougainville", "Pacific/Bougainville"),
|
||||
("America/Porto_Velho", "America/Porto_Velho"),
|
||||
("Africa/Niamey", "Africa/Niamey"),
|
||||
("Asia/Famagusta", "Asia/Famagusta"),
|
||||
("Etc/UTC", "Etc/UTC"),
|
||||
("Greenwich", "Greenwich"),
|
||||
("America/Grenada", "America/Grenada"),
|
||||
("Asia/Kathmandu", "Asia/Kathmandu"),
|
||||
("W-SU", "W-SU"),
|
||||
("Factory", "Factory"),
|
||||
("Europe/Bucharest", "Europe/Bucharest"),
|
||||
("America/St_Kitts", "America/St_Kitts"),
|
||||
("Africa/Sao_Tome", "Africa/Sao_Tome"),
|
||||
("Asia/Bangkok", "Asia/Bangkok"),
|
||||
("Africa/Dar_es_Salaam", "Africa/Dar_es_Salaam"),
|
||||
("Egypt", "Egypt"),
|
||||
("Africa/Maseru", "Africa/Maseru"),
|
||||
("Pacific/Galapagos", "Pacific/Galapagos"),
|
||||
("Asia/Harbin", "Asia/Harbin"),
|
||||
("Asia/Beirut", "Asia/Beirut"),
|
||||
("America/Monterrey", "America/Monterrey"),
|
||||
("Africa/Kampala", "Africa/Kampala"),
|
||||
("Asia/Ashgabat", "Asia/Ashgabat"),
|
||||
("America/Chihuahua", "America/Chihuahua"),
|
||||
("Eire", "Eire"),
|
||||
("Europe/Saratov", "Europe/Saratov"),
|
||||
("Cuba", "Cuba"),
|
||||
("Asia/Tashkent", "Asia/Tashkent"),
|
||||
("Pacific/Guam", "Pacific/Guam"),
|
||||
("America/Jamaica", "America/Jamaica"),
|
||||
("America/Hermosillo", "America/Hermosillo"),
|
||||
("Australia/Hobart", "Australia/Hobart"),
|
||||
("Asia/Krasnoyarsk", "Asia/Krasnoyarsk"),
|
||||
("America/Antigua", "America/Antigua"),
|
||||
("Indian/Mauritius", "Indian/Mauritius"),
|
||||
("America/Ciudad_Juarez", "America/Ciudad_Juarez"),
|
||||
("Asia/Muscat", "Asia/Muscat"),
|
||||
("Europe/Budapest", "Europe/Budapest"),
|
||||
("MET", "MET"),
|
||||
("Navajo", "Navajo"),
|
||||
("Etc/GMT-4", "Etc/GMT-4"),
|
||||
("America/Nassau", "America/Nassau"),
|
||||
("Asia/Bishkek", "Asia/Bishkek"),
|
||||
("America/Argentina/Jujuy", "America/Argentina/Jujuy"),
|
||||
("America/Nuuk", "America/Nuuk"),
|
||||
("Etc/GMT+9", "Etc/GMT+9"),
|
||||
("Australia/LHI", "Australia/LHI"),
|
||||
("America/Scoresbysund", "America/Scoresbysund"),
|
||||
("Asia/Yekaterinburg", "Asia/Yekaterinburg"),
|
||||
("Etc/GMT-0", "Etc/GMT-0"),
|
||||
("America/Creston", "America/Creston"),
|
||||
("Indian/Mahe", "Indian/Mahe"),
|
||||
(
|
||||
"America/Indiana/Indianapolis",
|
||||
"America/Indiana/Indianapolis",
|
||||
),
|
||||
("Pacific/Wallis", "Pacific/Wallis"),
|
||||
("America/Jujuy", "America/Jujuy"),
|
||||
("Europe/Zurich", "Europe/Zurich"),
|
||||
("Australia/Brisbane", "Australia/Brisbane"),
|
||||
("Etc/GMT-13", "Etc/GMT-13"),
|
||||
("Etc/GMT-5", "Etc/GMT-5"),
|
||||
("Hongkong", "Hongkong"),
|
||||
("Asia/Tel_Aviv", "Asia/Tel_Aviv"),
|
||||
("America/Recife", "America/Recife"),
|
||||
("America/Knox_IN", "America/Knox_IN"),
|
||||
("Australia/Lindeman", "Australia/Lindeman"),
|
||||
("Etc/GMT+11", "Etc/GMT+11"),
|
||||
("Canada/Yukon", "Canada/Yukon"),
|
||||
("Africa/Banjul", "Africa/Banjul"),
|
||||
("America/Belize", "America/Belize"),
|
||||
("Asia/Hovd", "Asia/Hovd"),
|
||||
("Etc/GMT+4", "Etc/GMT+4"),
|
||||
("Africa/Djibouti", "Africa/Djibouti"),
|
||||
("Africa/Nairobi", "Africa/Nairobi"),
|
||||
("Iceland", "Iceland"),
|
||||
("Australia/Yancowinna", "Australia/Yancowinna"),
|
||||
("Canada/Saskatchewan", "Canada/Saskatchewan"),
|
||||
("Asia/Magadan", "Asia/Magadan"),
|
||||
("America/Lima", "America/Lima"),
|
||||
("America/Cambridge_Bay", "America/Cambridge_Bay"),
|
||||
("Europe/Ulyanovsk", "Europe/Ulyanovsk"),
|
||||
("America/Merida", "America/Merida"),
|
||||
("America/Aruba", "America/Aruba"),
|
||||
("Pacific/Port_Moresby", "Pacific/Port_Moresby"),
|
||||
("Europe/Kirov", "Europe/Kirov"),
|
||||
("America/St_Johns", "America/St_Johns"),
|
||||
("Africa/Bamako", "Africa/Bamako"),
|
||||
("Asia/Ulan_Bator", "Asia/Ulan_Bator"),
|
||||
("Australia/Queensland", "Australia/Queensland"),
|
||||
("America/Santo_Domingo", "America/Santo_Domingo"),
|
||||
("Europe/Tallinn", "Europe/Tallinn"),
|
||||
("Europe/Lisbon", "Europe/Lisbon"),
|
||||
("America/Catamarca", "America/Catamarca"),
|
||||
("America/Phoenix", "America/Phoenix"),
|
||||
("America/Indiana/Vevay", "America/Indiana/Vevay"),
|
||||
("Asia/Karachi", "Asia/Karachi"),
|
||||
("America/Curacao", "America/Curacao"),
|
||||
("MST7MDT", "MST7MDT"),
|
||||
("Europe/Podgorica", "Europe/Podgorica"),
|
||||
("Asia/Makassar", "Asia/Makassar"),
|
||||
("America/Regina", "America/Regina"),
|
||||
("Asia/Aden", "Asia/Aden"),
|
||||
("Europe/Luxembourg", "Europe/Luxembourg"),
|
||||
("Asia/Vientiane", "Asia/Vientiane"),
|
||||
("US/Eastern", "US/Eastern"),
|
||||
("Asia/Tokyo", "Asia/Tokyo"),
|
||||
("America/Fort_Wayne", "America/Fort_Wayne"),
|
||||
("America/Tijuana", "America/Tijuana"),
|
||||
("America/Montevideo", "America/Montevideo"),
|
||||
("Europe/Oslo", "Europe/Oslo"),
|
||||
("America/La_Paz", "America/La_Paz"),
|
||||
("Asia/Aqtobe", "Asia/Aqtobe"),
|
||||
("Europe/Volgograd", "Europe/Volgograd"),
|
||||
("America/Costa_Rica", "America/Costa_Rica"),
|
||||
("GMT+0", "GMT+0"),
|
||||
("America/Guadeloupe", "America/Guadeloupe"),
|
||||
("America/Bahia", "America/Bahia"),
|
||||
("Africa/Khartoum", "Africa/Khartoum"),
|
||||
("Europe/Belgrade", "Europe/Belgrade"),
|
||||
("Pacific/Chuuk", "Pacific/Chuuk"),
|
||||
("America/Swift_Current", "America/Swift_Current"),
|
||||
("Asia/Macau", "Asia/Macau"),
|
||||
("America/Dawson", "America/Dawson"),
|
||||
("Asia/Thimbu", "Asia/Thimbu"),
|
||||
("America/Panama", "America/Panama"),
|
||||
("Europe/Ljubljana", "Europe/Ljubljana"),
|
||||
("Africa/Mbabane", "Africa/Mbabane"),
|
||||
("Africa/Libreville", "Africa/Libreville"),
|
||||
("PST8PDT", "PST8PDT"),
|
||||
("Brazil/DeNoronha", "Brazil/DeNoronha"),
|
||||
("Europe/Amsterdam", "Europe/Amsterdam"),
|
||||
("Asia/Jayapura", "Asia/Jayapura"),
|
||||
(
|
||||
"America/North_Dakota/Center",
|
||||
"America/North_Dakota/Center",
|
||||
),
|
||||
("Etc/GMT-11", "Etc/GMT-11"),
|
||||
("Etc/GMT-12", "Etc/GMT-12"),
|
||||
("GB", "GB"),
|
||||
("Africa/Lubumbashi", "Africa/Lubumbashi"),
|
||||
("Africa/Kigali", "Africa/Kigali"),
|
||||
("America/Marigot", "America/Marigot"),
|
||||
("Asia/Oral", "Asia/Oral"),
|
||||
("Brazil/West", "Brazil/West"),
|
||||
("Antarctica/Casey", "Antarctica/Casey"),
|
||||
("US/Central", "US/Central"),
|
||||
("America/Ojinaga", "America/Ojinaga"),
|
||||
("America/Santa_Isabel", "America/Santa_Isabel"),
|
||||
("America/Argentina/Ushuaia", "America/Argentina/Ushuaia"),
|
||||
("Atlantic/Stanley", "Atlantic/Stanley"),
|
||||
("Africa/Conakry", "Africa/Conakry"),
|
||||
("Europe/Andorra", "Europe/Andorra"),
|
||||
("Pacific/Apia", "Pacific/Apia"),
|
||||
("America/Santarem", "America/Santarem"),
|
||||
("Europe/Kiev", "Europe/Kiev"),
|
||||
("Australia/West", "Australia/West"),
|
||||
("Asia/Taipei", "Asia/Taipei"),
|
||||
("America/Goose_Bay", "America/Goose_Bay"),
|
||||
("America/Indiana/Knox", "America/Indiana/Knox"),
|
||||
("Asia/Yakutsk", "Asia/Yakutsk"),
|
||||
("Pacific/Niue", "Pacific/Niue"),
|
||||
("Africa/Lome", "Africa/Lome"),
|
||||
("Europe/Tiraspol", "Europe/Tiraspol"),
|
||||
("Atlantic/Jan_Mayen", "Atlantic/Jan_Mayen"),
|
||||
("Indian/Mayotte", "Indian/Mayotte"),
|
||||
("America/Indiana/Vincennes", "America/Indiana/Vincennes"),
|
||||
("Etc/GMT+7", "Etc/GMT+7"),
|
||||
("America/Mendoza", "America/Mendoza"),
|
||||
("America/Atka", "America/Atka"),
|
||||
("Asia/Qatar", "Asia/Qatar"),
|
||||
("Pacific/Pitcairn", "Pacific/Pitcairn"),
|
||||
("America/Asuncion", "America/Asuncion"),
|
||||
("Europe/Prague", "Europe/Prague"),
|
||||
("EET", "EET"),
|
||||
("America/Anguilla", "America/Anguilla"),
|
||||
("America/Sitka", "America/Sitka"),
|
||||
("Asia/Kamchatka", "Asia/Kamchatka"),
|
||||
("Asia/Irkutsk", "Asia/Irkutsk"),
|
||||
("Jamaica", "Jamaica"),
|
||||
("America/St_Thomas", "America/St_Thomas"),
|
||||
(
|
||||
"America/Argentina/San_Luis",
|
||||
"America/Argentina/San_Luis",
|
||||
),
|
||||
("Chile/Continental", "Chile/Continental"),
|
||||
("Asia/Jerusalem", "Asia/Jerusalem"),
|
||||
("Africa/Lagos", "Africa/Lagos"),
|
||||
("Antarctica/Syowa", "Antarctica/Syowa"),
|
||||
("Atlantic/Canary", "Atlantic/Canary"),
|
||||
("Europe/Vatican", "Europe/Vatican"),
|
||||
("America/Guatemala", "America/Guatemala"),
|
||||
("Africa/Addis_Ababa", "Africa/Addis_Ababa"),
|
||||
("America/Indianapolis", "America/Indianapolis"),
|
||||
("Asia/Calcutta", "Asia/Calcutta"),
|
||||
("Indian/Cocos", "Indian/Cocos"),
|
||||
("Pacific/Tongatapu", "Pacific/Tongatapu"),
|
||||
("Europe/San_Marino", "Europe/San_Marino"),
|
||||
("Australia/Broken_Hill", "Australia/Broken_Hill"),
|
||||
("Etc/GMT+8", "Etc/GMT+8"),
|
||||
("Asia/Atyrau", "Asia/Atyrau"),
|
||||
("Arctic/Longyearbyen", "Arctic/Longyearbyen"),
|
||||
("Pacific/Kiritimati", "Pacific/Kiritimati"),
|
||||
("Asia/Istanbul", "Asia/Istanbul"),
|
||||
("America/Fort_Nelson", "America/Fort_Nelson"),
|
||||
("Africa/Algiers", "Africa/Algiers"),
|
||||
("Asia/Almaty", "Asia/Almaty"),
|
||||
("Antarctica/Macquarie", "Antarctica/Macquarie"),
|
||||
("Africa/Freetown", "Africa/Freetown"),
|
||||
("Asia/Kabul", "Asia/Kabul"),
|
||||
("Asia/Choibalsan", "Asia/Choibalsan"),
|
||||
("America/Detroit", "America/Detroit"),
|
||||
("America/Cordoba", "America/Cordoba"),
|
||||
("America/Whitehorse", "America/Whitehorse"),
|
||||
("Asia/Riyadh", "Asia/Riyadh"),
|
||||
("Asia/Dubai", "Asia/Dubai"),
|
||||
("Universal", "Universal"),
|
||||
("America/Boise", "America/Boise"),
|
||||
("Africa/Tunis", "Africa/Tunis"),
|
||||
("Asia/Yangon", "Asia/Yangon"),
|
||||
("America/Araguaina", "America/Araguaina"),
|
||||
("Chile/EasterIsland", "Chile/EasterIsland"),
|
||||
("America/Caracas", "America/Caracas"),
|
||||
("Antarctica/DumontDUrville", "Antarctica/DumontDUrville"),
|
||||
("Atlantic/Faroe", "Atlantic/Faroe"),
|
||||
("Europe/Astrakhan", "Europe/Astrakhan"),
|
||||
("Asia/Rangoon", "Asia/Rangoon"),
|
||||
("Australia/Eucla", "Australia/Eucla"),
|
||||
("PRC", "PRC"),
|
||||
("Pacific/Tahiti", "Pacific/Tahiti"),
|
||||
("Australia/South", "Australia/South"),
|
||||
(
|
||||
"America/Kentucky/Louisville",
|
||||
"America/Kentucky/Louisville",
|
||||
),
|
||||
("America/Iqaluit", "America/Iqaluit"),
|
||||
("Antarctica/South_Pole", "Antarctica/South_Pole"),
|
||||
("Asia/Damascus", "Asia/Damascus"),
|
||||
("America/Glace_Bay", "America/Glace_Bay"),
|
||||
("Atlantic/Bermuda", "Atlantic/Bermuda"),
|
||||
("Asia/Pontianak", "Asia/Pontianak"),
|
||||
("Asia/Kolkata", "Asia/Kolkata"),
|
||||
("Pacific/Marquesas", "Pacific/Marquesas"),
|
||||
("Asia/Vladivostok", "Asia/Vladivostok"),
|
||||
("WET", "WET"),
|
||||
("Atlantic/Reykjavik", "Atlantic/Reykjavik"),
|
||||
("EST5EDT", "EST5EDT"),
|
||||
("Europe/Zagreb", "Europe/Zagreb"),
|
||||
("America/Toronto", "America/Toronto"),
|
||||
(
|
||||
"America/Argentina/ComodRivadavia",
|
||||
"America/Argentina/ComodRivadavia",
|
||||
),
|
||||
("Pacific/Chatham", "Pacific/Chatham"),
|
||||
("Europe/Istanbul", "Europe/Istanbul"),
|
||||
("Asia/Singapore", "Asia/Singapore"),
|
||||
("Asia/Srednekolymsk", "Asia/Srednekolymsk"),
|
||||
("Atlantic/Cape_Verde", "Atlantic/Cape_Verde"),
|
||||
("US/Arizona", "US/Arizona"),
|
||||
("America/Montreal", "America/Montreal"),
|
||||
("America/Resolute", "America/Resolute"),
|
||||
("America/Boa_Vista", "America/Boa_Vista"),
|
||||
("Antarctica/McMurdo", "Antarctica/McMurdo"),
|
||||
("Atlantic/Madeira", "Atlantic/Madeira"),
|
||||
("Canada/Atlantic", "Canada/Atlantic"),
|
||||
("Australia/Perth", "Australia/Perth"),
|
||||
("Kwajalein", "Kwajalein"),
|
||||
("Asia/Phnom_Penh", "Asia/Phnom_Penh"),
|
||||
("Europe/Malta", "Europe/Malta"),
|
||||
("America/Indiana/Tell_City", "America/Indiana/Tell_City"),
|
||||
("America/Guyana", "America/Guyana"),
|
||||
("Pacific/Palau", "Pacific/Palau"),
|
||||
("America/Winnipeg", "America/Winnipeg"),
|
||||
("UCT", "UCT"),
|
||||
("Atlantic/Azores", "Atlantic/Azores"),
|
||||
("Mexico/General", "Mexico/General"),
|
||||
("Pacific/Nauru", "Pacific/Nauru"),
|
||||
("Asia/Hebron", "Asia/Hebron"),
|
||||
("Asia/Khandyga", "Asia/Khandyga"),
|
||||
("Australia/Lord_Howe", "Australia/Lord_Howe"),
|
||||
("Portugal", "Portugal"),
|
||||
("Etc/GMT-7", "Etc/GMT-7"),
|
||||
("ROK", "ROK"),
|
||||
("Libya", "Libya"),
|
||||
("Europe/Jersey", "Europe/Jersey"),
|
||||
("Israel", "Israel"),
|
||||
("Pacific/Wake", "Pacific/Wake"),
|
||||
("Africa/Porto-Novo", "Africa/Porto-Novo"),
|
||||
("Africa/Asmera", "Africa/Asmera"),
|
||||
("America/Maceio", "America/Maceio"),
|
||||
("Europe/Sarajevo", "Europe/Sarajevo"),
|
||||
("US/East-Indiana", "US/East-Indiana"),
|
||||
("America/Rainy_River", "America/Rainy_River"),
|
||||
("Europe/Stockholm", "Europe/Stockholm"),
|
||||
("America/Thule", "America/Thule"),
|
||||
("Pacific/Enderbury", "Pacific/Enderbury"),
|
||||
("Pacific/Truk", "Pacific/Truk"),
|
||||
("Pacific/Ponape", "Pacific/Ponape"),
|
||||
("America/St_Barthelemy", "America/St_Barthelemy"),
|
||||
("Turkey", "Turkey"),
|
||||
("Antarctica/Mawson", "Antarctica/Mawson"),
|
||||
("Etc/GMT+0", "Etc/GMT+0"),
|
||||
("Europe/Sofia", "Europe/Sofia"),
|
||||
("Asia/Tbilisi", "Asia/Tbilisi"),
|
||||
("Australia/ACT", "Australia/ACT"),
|
||||
("Canada/Mountain", "Canada/Mountain"),
|
||||
("Europe/Isle_of_Man", "Europe/Isle_of_Man"),
|
||||
("Asia/Kashgar", "Asia/Kashgar"),
|
||||
("Europe/Chisinau", "Europe/Chisinau"),
|
||||
("Pacific/Efate", "Pacific/Efate"),
|
||||
("Pacific/Norfolk", "Pacific/Norfolk"),
|
||||
("America/Eirunepe", "America/Eirunepe"),
|
||||
("Europe/Guernsey", "Europe/Guernsey"),
|
||||
("Europe/Vaduz", "Europe/Vaduz"),
|
||||
("US/Samoa", "US/Samoa"),
|
||||
("Africa/Bangui", "Africa/Bangui"),
|
||||
("GMT", "GMT"),
|
||||
("Asia/Omsk", "Asia/Omsk"),
|
||||
("America/Menominee", "America/Menominee"),
|
||||
("America/Matamoros", "America/Matamoros"),
|
||||
("Canada/Newfoundland", "Canada/Newfoundland"),
|
||||
("Asia/Hong_Kong", "Asia/Hong_Kong"),
|
||||
("America/Montserrat", "America/Montserrat"),
|
||||
("Australia/Sydney", "Australia/Sydney"),
|
||||
("Asia/Qyzylorda", "Asia/Qyzylorda"),
|
||||
("Asia/Colombo", "Asia/Colombo"),
|
||||
("America/Argentina/Mendoza", "America/Argentina/Mendoza"),
|
||||
("Etc/GMT+1", "Etc/GMT+1"),
|
||||
("Asia/Dacca", "Asia/Dacca"),
|
||||
("America/Louisville", "America/Louisville"),
|
||||
("Asia/Sakhalin", "Asia/Sakhalin"),
|
||||
("Africa/Juba", "Africa/Juba"),
|
||||
("Japan", "Japan"),
|
||||
("America/Inuvik", "America/Inuvik"),
|
||||
("America/Cayman", "America/Cayman"),
|
||||
("Africa/Johannesburg", "Africa/Johannesburg"),
|
||||
("Pacific/Honolulu", "Pacific/Honolulu"),
|
||||
("Asia/Anadyr", "Asia/Anadyr"),
|
||||
("America/Atikokan", "America/Atikokan"),
|
||||
("Asia/Tomsk", "Asia/Tomsk"),
|
||||
("Europe/Zaporozhye", "Europe/Zaporozhye"),
|
||||
("Pacific/Saipan", "Pacific/Saipan"),
|
||||
("America/Virgin", "America/Virgin"),
|
||||
("Asia/Ho_Chi_Minh", "Asia/Ho_Chi_Minh"),
|
||||
("Pacific/Easter", "Pacific/Easter"),
|
||||
("Brazil/East", "Brazil/East"),
|
||||
("Africa/Accra", "Africa/Accra"),
|
||||
("America/Mexico_City", "America/Mexico_City"),
|
||||
("Europe/Dublin", "Europe/Dublin"),
|
||||
("America/Chicago", "America/Chicago"),
|
||||
("Etc/GMT+3", "Etc/GMT+3"),
|
||||
("Etc/GMT+5", "Etc/GMT+5"),
|
||||
("America/Tortola", "America/Tortola"),
|
||||
("Europe/Copenhagen", "Europe/Copenhagen"),
|
||||
("Asia/Bahrain", "Asia/Bahrain"),
|
||||
("Asia/Kuching", "Asia/Kuching"),
|
||||
("EST", "EST"),
|
||||
("Atlantic/Faeroe", "Atlantic/Faeroe"),
|
||||
("America/Shiprock", "America/Shiprock"),
|
||||
("Asia/Yerevan", "Asia/Yerevan"),
|
||||
("Etc/GMT+10", "Etc/GMT+10"),
|
||||
],
|
||||
default=aircox.models.schedule.current_timezone_key,
|
||||
help_text="timezone used for the date",
|
||||
max_length=100,
|
||||
verbose_name="timezone",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="station",
|
||||
name="hosts",
|
||||
field=models.TextField(
|
||||
blank=True,
|
||||
help_text="specify one domain per line, without 'http://' prefix",
|
||||
max_length=512,
|
||||
null=True,
|
||||
verbose_name="website's urls",
|
||||
),
|
||||
),
|
||||
]
|
||||
@ -1,623 +0,0 @@
|
||||
# Generated by Django 4.2.5 on 2023-10-18 07:26
|
||||
|
||||
import aircox.models.schedule
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("aircox", "0013_alter_schedule_timezone_alter_station_hosts"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
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/Ciudad_Juarez", "America/Ciudad_Juarez"),
|
||||
("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"),
|
||||
("Factory", "Factory"),
|
||||
("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"),
|
||||
("localtime", "localtime"),
|
||||
],
|
||||
default=aircox.models.schedule.current_timezone_key,
|
||||
help_text="timezone used for the date",
|
||||
max_length=100,
|
||||
verbose_name="timezone",
|
||||
),
|
||||
),
|
||||
]
|
||||
@ -1,641 +0,0 @@
|
||||
# Generated by Django 4.2.1 on 2023-11-24 21:11
|
||||
|
||||
import aircox.models.schedule
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("aircox", "0014_alter_schedule_timezone"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
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/Ciudad_Juarez", "America/Ciudad_Juarez"),
|
||||
("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"),
|
||||
("Factory", "Factory"),
|
||||
("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=aircox.models.schedule.current_timezone_key,
|
||||
help_text="timezone used for the date",
|
||||
max_length=100,
|
||||
verbose_name="timezone",
|
||||
),
|
||||
),
|
||||
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"),
|
||||
(6, "Publications list"),
|
||||
],
|
||||
help_text="display this page content to related element",
|
||||
null=True,
|
||||
verbose_name="attach to",
|
||||
),
|
||||
),
|
||||
]
|
||||
@ -1,25 +0,0 @@
|
||||
# Generated by Django 4.2.5 on 2023-10-18 13:50
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("auth", "0012_alter_user_first_name_max_length"),
|
||||
("aircox", "0014_alter_schedule_timezone"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="program",
|
||||
name="editors_group",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to="auth.group",
|
||||
verbose_name="editors",
|
||||
),
|
||||
),
|
||||
]
|
||||
@ -1,32 +0,0 @@
|
||||
# Generated by Django 4.2.1 on 2023-11-28 01:15
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("aircox", "0015_alter_schedule_timezone_alter_staticpage_attach_to"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
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"),
|
||||
(6, "Publications list"),
|
||||
(7, "Podcasts list"),
|
||||
],
|
||||
help_text="display this page content to related element",
|
||||
null=True,
|
||||
verbose_name="attach to",
|
||||
),
|
||||
),
|
||||
]
|
||||
@ -1,36 +0,0 @@
|
||||
# Generated by Django 4.2.1 on 2023-12-12 16:58
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("aircox", "0016_alter_staticpage_attach_to"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="navitem",
|
||||
name="text",
|
||||
field=models.CharField(blank=True, max_length=64, null=True, verbose_name="title"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="staticpage",
|
||||
name="attach_to",
|
||||
field=models.SmallIntegerField(
|
||||
blank=True,
|
||||
choices=[
|
||||
(0, "Home page"),
|
||||
(1, "Diffusions page"),
|
||||
(3, "Programs list"),
|
||||
(4, "Episodes list"),
|
||||
(5, "Articles list"),
|
||||
(6, "Publications list"),
|
||||
(7, "Podcasts list"),
|
||||
],
|
||||
help_text="display this page content to related element",
|
||||
null=True,
|
||||
verbose_name="attach to",
|
||||
),
|
||||
),
|
||||
]
|
||||
@ -1,32 +0,0 @@
|
||||
# Generated by Django 4.2.1 on 2023-12-12 18:17
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("aircox", "0017_alter_navitem_text_alter_staticpage_attach_to"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="staticpage",
|
||||
name="attach_to",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("", "Home Page"),
|
||||
("timetable-list", "Timetable"),
|
||||
("program-list", "Programs list"),
|
||||
("episode-list", "Episodes list"),
|
||||
("article-list", "Articles list"),
|
||||
("page-list", "Publications list"),
|
||||
("podcast-list", "Podcasts list"),
|
||||
],
|
||||
help_text="display this page content to related element",
|
||||
max_length=32,
|
||||
null=True,
|
||||
verbose_name="attach to",
|
||||
),
|
||||
),
|
||||
]
|
||||
@ -1,12 +0,0 @@
|
||||
# Generated by Django 4.2.7 on 2024-01-19 09:22
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("aircox", "0015_program_editors"),
|
||||
("aircox", "0018_alter_staticpage_attach_to"),
|
||||
]
|
||||
|
||||
operations = []
|
||||
@ -1,42 +0,0 @@
|
||||
# Generated by Django 4.2.1 on 2024-02-01 18:12
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("aircox", "0018_alter_staticpage_attach_to"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="station",
|
||||
name="music_stream_title",
|
||||
field=models.CharField(
|
||||
default="Music stream",
|
||||
max_length=64,
|
||||
verbose_name="Music stream's title",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="staticpage",
|
||||
name="attach_to",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("", "None"),
|
||||
("home", "Home Page"),
|
||||
("timetable-list", "Timetable"),
|
||||
("program-list", "Programs list"),
|
||||
("episode-list", "Episodes list"),
|
||||
("article-list", "Articles list"),
|
||||
("page-list", "Publications list"),
|
||||
("podcast-list", "Podcasts list"),
|
||||
],
|
||||
help_text="display this page content to related element",
|
||||
max_length=32,
|
||||
null=True,
|
||||
verbose_name="attach to",
|
||||
),
|
||||
),
|
||||
]
|
||||
@ -1,12 +0,0 @@
|
||||
# Generated by Django 4.2.7 on 2024-02-05 09:27
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("aircox", "0019_merge_20240119_1022"),
|
||||
("aircox", "0019_station_program_streams_title_and_more"),
|
||||
]
|
||||
|
||||
operations = []
|
||||
@ -1,623 +0,0 @@
|
||||
# Generated by Django 4.2.7 on 2024-02-06 08:13
|
||||
|
||||
import aircox.models.schedule
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("aircox", "0020_merge_20240205_1027"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
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/Ciudad_Juarez", "America/Ciudad_Juarez"),
|
||||
("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"),
|
||||
("Factory", "Factory"),
|
||||
("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"),
|
||||
("localtime", "localtime"),
|
||||
],
|
||||
default=aircox.models.schedule.current_timezone_key,
|
||||
help_text="timezone used for the date",
|
||||
max_length=100,
|
||||
verbose_name="timezone",
|
||||
),
|
||||
),
|
||||
]
|
||||
@ -1,31 +0,0 @@
|
||||
from django.db import migrations, models, transaction
|
||||
|
||||
|
||||
def init_groups_and_permissions(app, schema_editor):
|
||||
from aircox import permissions
|
||||
|
||||
Program = app.get_model("aircox", "Program")
|
||||
|
||||
with transaction.atomic():
|
||||
for program in Program.objects.all():
|
||||
permissions.program.init(program)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
atomic = False
|
||||
dependencies = [
|
||||
("aircox", "0021_alter_schedule_timezone"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(init_groups_and_permissions),
|
||||
migrations.AlterField(
|
||||
model_name="program",
|
||||
name="editors_group",
|
||||
field=models.ForeignKey(
|
||||
on_delete=models.deletion.CASCADE,
|
||||
to="auth.group",
|
||||
verbose_name="editors",
|
||||
),
|
||||
),
|
||||
]
|
||||
@ -1,634 +0,0 @@
|
||||
# Generated by Django 4.2.9 on 2024-03-15 19:56
|
||||
|
||||
import aircox.models.schedule
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("filer", "0017_image__transparent"),
|
||||
("aircox", "0022_set_group_ownership"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="station",
|
||||
name="legal_label",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
default="",
|
||||
help_text="Displayed at the bottom of pages.",
|
||||
max_length=64,
|
||||
verbose_name="Legal label",
|
||||
),
|
||||
),
|
||||
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/Ciudad_Juarez", "America/Ciudad_Juarez"),
|
||||
("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"),
|
||||
("Factory", "Factory"),
|
||||
("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=aircox.models.schedule.current_timezone_key,
|
||||
help_text="timezone used for the date",
|
||||
max_length=100,
|
||||
verbose_name="timezone",
|
||||
),
|
||||
),
|
||||
]
|
||||
@ -1,22 +0,0 @@
|
||||
# Generated by Django 4.2.9 on 2024-03-19 22:38
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("aircox", "0023_station_legal_label_alter_schedule_timezone"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name="usersettings",
|
||||
old_name="playlist_editor_columns",
|
||||
new_name="tracklist_editor_columns",
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name="usersettings",
|
||||
old_name="playlist_editor_sep",
|
||||
new_name="tracklist_editor_sep",
|
||||
),
|
||||
]
|
||||
@ -1,36 +0,0 @@
|
||||
# Generated by Django 4.2.9 on 2024-03-25 20:23
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("aircox", "0024_rename_playlist_editor_columns_usersettings_tracklist_editor_columns_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="sound",
|
||||
name="is_removed",
|
||||
field=models.BooleanField(default=False, help_text="file has been removed", verbose_name="removed"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="sound",
|
||||
name="is_downloadable",
|
||||
field=models.BooleanField(
|
||||
default=False,
|
||||
help_text="sound can be downloaded by visitors (sound must be public)",
|
||||
verbose_name="downloadable",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="sound",
|
||||
name="is_public",
|
||||
field=models.BooleanField(default=False, help_text="sound is available as podcast", verbose_name="public"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="sound",
|
||||
name="type",
|
||||
field=models.SmallIntegerField(choices=[(0, "other"), (1, "archive"), (2, "excerpt")], verbose_name="type"),
|
||||
),
|
||||
]
|
||||
@ -1,162 +0,0 @@
|
||||
# Generated by Django 4.2.9 on 2024-03-26 02:53
|
||||
|
||||
import aircox.models.file
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
sounds_info = {}
|
||||
|
||||
|
||||
def get_sounds_info(apps, schema_editor):
|
||||
Sound = apps.get_model("aircox", "Sound")
|
||||
objs = Sound.objects.filter().values(
|
||||
"pk",
|
||||
"episode_id",
|
||||
"position",
|
||||
"type",
|
||||
)
|
||||
sounds_info.update({obj["pk"]: obj for obj in objs})
|
||||
|
||||
|
||||
def restore_sounds_info(apps, schema_editor):
|
||||
try:
|
||||
Sound = apps.get_model("aircox", "Sound")
|
||||
EpisodeSound = apps.get_model("aircox", "EpisodeSound")
|
||||
TYPE_ARCHIVE = 0x01
|
||||
TYPE_REMOVED = 0x03
|
||||
|
||||
episode_sounds = []
|
||||
sounds = []
|
||||
for sound in Sound.objects.all():
|
||||
info = sounds_info.get(sound.pk)
|
||||
if not info:
|
||||
continue
|
||||
|
||||
sound.broadcast = info["type"] == TYPE_ARCHIVE
|
||||
sound.is_removed = info["type"] == TYPE_REMOVED
|
||||
sounds.append(sound)
|
||||
if not sound.is_removed and info["episode_id"]:
|
||||
obj = EpisodeSound(
|
||||
sound=sound,
|
||||
episode_id=info["episode_id"],
|
||||
position=info["position"],
|
||||
broadcast=sound.broadcast,
|
||||
)
|
||||
episode_sounds.append(obj)
|
||||
|
||||
Sound.objects.bulk_update(sounds, ("broadcast", "is_removed"))
|
||||
EpisodeSound.objects.bulk_create(episode_sounds)
|
||||
|
||||
print(f"\n>>> {len(sounds)} Sound have been updated.")
|
||||
print(f">>> {len(episode_sounds)} EpisodeSound have been created.")
|
||||
except Exception as err:
|
||||
print(err)
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("aircox", "0025_sound_is_removed_alter_sound_is_downloadable_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(get_sounds_info),
|
||||
migrations.AlterModelOptions(
|
||||
name="sound",
|
||||
options={"verbose_name": "Sound file", "verbose_name_plural": "Sound files"},
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="sound",
|
||||
name="episode",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="sound",
|
||||
name="position",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="sound",
|
||||
name="type",
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="sound",
|
||||
name="broadcast",
|
||||
field=models.BooleanField(
|
||||
default=False, help_text="The sound is broadcasted on air", verbose_name="Broadcast"
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="sound",
|
||||
name="description",
|
||||
field=models.TextField(blank=True, default="", max_length=256, verbose_name="description"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="sound",
|
||||
name="file",
|
||||
field=models.FileField(
|
||||
db_index=True, max_length=256, upload_to=aircox.models.file.File._upload_to, verbose_name="file"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="sound",
|
||||
name="is_downloadable",
|
||||
field=models.BooleanField(
|
||||
default=False, help_text="sound can be downloaded by visitors", verbose_name="downloadable"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="sound",
|
||||
name="is_public",
|
||||
field=models.BooleanField(default=False, help_text="file is publicly accessible", verbose_name="public"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="sound",
|
||||
name="is_removed",
|
||||
field=models.BooleanField(
|
||||
db_index=True, default=False, help_text="file has been removed from server", verbose_name="removed"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="sound",
|
||||
name="name",
|
||||
field=models.CharField(db_index=True, max_length=64, verbose_name="name"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="sound",
|
||||
name="program",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to="aircox.program",
|
||||
verbose_name="Program",
|
||||
),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="EpisodeSound",
|
||||
fields=[
|
||||
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
|
||||
(
|
||||
"position",
|
||||
models.PositiveSmallIntegerField(
|
||||
default=0, help_text="position in the playlist", verbose_name="order"
|
||||
),
|
||||
),
|
||||
(
|
||||
"broadcast",
|
||||
models.BooleanField(
|
||||
blank=None, help_text="The sound is broadcasted on air", verbose_name="Broadcast"
|
||||
),
|
||||
),
|
||||
("episode", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="aircox.episode")),
|
||||
("sound", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="aircox.sound")),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Episode Sound",
|
||||
"verbose_name_plural": "Episode Sounds",
|
||||
},
|
||||
),
|
||||
migrations.RunPython(restore_sounds_info),
|
||||
]
|
||||
@ -1,78 +0,0 @@
|
||||
# Generated by Django 5.0 on 2024-04-10 08:38
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
children_infos = {}
|
||||
|
||||
|
||||
def get_children_infos(apps, schema_editor):
|
||||
Page = apps.get_model("aircox", "page")
|
||||
query = Page.objects.filter(parent__isnull=False).values("pk", "parent_id", "category_id", "parent__category_id")
|
||||
children_infos.update((r["pk"], r) for r in query)
|
||||
|
||||
|
||||
def restore_children_infos(apps, schema_editor):
|
||||
Episode = apps.get_model("aircox", "Episode")
|
||||
|
||||
pks = set(children_infos.keys())
|
||||
eps = _restore_for_objs(Episode.objects.filter(pk__in=pks))
|
||||
Episode.objects.bulk_update(eps, ("parent_id", "category_id"))
|
||||
print(f">> {len(eps)} episodes restored")
|
||||
|
||||
|
||||
def _restore_for_objs(objs):
|
||||
updated = []
|
||||
for obj in objs:
|
||||
info = children_infos.get(obj.pk)
|
||||
if info:
|
||||
obj.parent_id = info["parent_id"]
|
||||
obj.category_id = info["category_id"] or info["parent__category_id"]
|
||||
updated.append(obj)
|
||||
return updated
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("aircox", "0026_alter_sound_options_remove_sound_episode_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(get_children_infos),
|
||||
migrations.RemoveField(
|
||||
model_name="page",
|
||||
name="parent",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="staticpage",
|
||||
name="parent",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="station",
|
||||
name="path",
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="article",
|
||||
name="parent",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="%(class)s_set",
|
||||
to="aircox.page",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="episode",
|
||||
name="parent",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="%(class)s_set",
|
||||
to="aircox.page",
|
||||
),
|
||||
),
|
||||
migrations.RunPython(restore_children_infos),
|
||||
]
|
||||
@ -1,64 +0,0 @@
|
||||
# Generated by Django 5.0.4 on 2024-05-27 12:40
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("aircox", "0027_remove_page_parent_remove_staticpage_parent_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name="episodesound",
|
||||
options={"verbose_name": "Podcast", "verbose_name_plural": "Podcasts"},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="diffusion",
|
||||
name="initial",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
limit_choices_to=models.Q(("initial__isnull", True), ("program", models.F("program"))),
|
||||
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="log",
|
||||
name="source",
|
||||
field=models.CharField(
|
||||
blank=True, help_text="Identifier of the log's source.", max_length=64, null=True, verbose_name="source"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="program",
|
||||
name="active",
|
||||
field=models.BooleanField(
|
||||
default=True, help_text="if not checked this program is no longer active", verbose_name="active"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="schedule",
|
||||
name="initial",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
limit_choices_to=models.Q(("initial__isnull", True), ("program", models.F("program"))),
|
||||
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="sound",
|
||||
name="is_downloadable",
|
||||
field=models.BooleanField(
|
||||
default=False, help_text="Sound can be downloaded by website visitors.", verbose_name="downloadable"
|
||||
),
|
||||
),
|
||||
]
|
||||
@ -1,30 +1,29 @@
|
||||
from . import signals
|
||||
from .article import Article
|
||||
from .diffusion import Diffusion, DiffusionQuerySet
|
||||
from .episode import Episode, EpisodeSound
|
||||
from .log import Log, LogQuerySet
|
||||
from .episode import Episode
|
||||
from .log import Log, LogArchiver, LogQuerySet
|
||||
from .page import Category, Comment, NavItem, Page, PageQuerySet, StaticPage
|
||||
from .program import Program, ProgramChildQuerySet, ProgramQuerySet, Stream
|
||||
from .schedule import Schedule
|
||||
from .sound import Sound, SoundQuerySet
|
||||
from .sound import Sound, SoundQuerySet, Track
|
||||
from .station import Port, Station, StationQuerySet
|
||||
from .track import Track
|
||||
from .user_settings import UserSettings
|
||||
|
||||
__all__ = (
|
||||
"signals",
|
||||
"Article",
|
||||
"Category",
|
||||
"Comment",
|
||||
"Episode",
|
||||
"Diffusion",
|
||||
"DiffusionQuerySet",
|
||||
"Episode",
|
||||
"EpisodeSound",
|
||||
"Log",
|
||||
"LogQuerySet",
|
||||
"LogArchiver",
|
||||
"Category",
|
||||
"PageQuerySet",
|
||||
"Page",
|
||||
"StaticPage",
|
||||
"Comment",
|
||||
"NavItem",
|
||||
"Program",
|
||||
"ProgramQuerySet",
|
||||
|
||||
@ -1,14 +1,13 @@
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from .page import ChildPage
|
||||
from .page import Page
|
||||
from .program import ProgramChildQuerySet
|
||||
|
||||
__all__ = ("Article",)
|
||||
|
||||
|
||||
class Article(ChildPage):
|
||||
class Article(Page):
|
||||
detail_url_name = "article-detail"
|
||||
template_prefix = "article"
|
||||
|
||||
objects = ProgramChildQuerySet.as_manager()
|
||||
|
||||
|
||||
@ -17,13 +17,13 @@ __all__ = ("Diffusion", "DiffusionQuerySet")
|
||||
|
||||
|
||||
class DiffusionQuerySet(RerunQuerySet):
|
||||
def editor(self, user):
|
||||
episodes = Episode.objects.editor(user)
|
||||
return self.filter(episode__in=episodes)
|
||||
|
||||
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)
|
||||
return (
|
||||
self.filter(episode=episode)
|
||||
if id is None
|
||||
else self.filter(episode__id=id)
|
||||
)
|
||||
|
||||
def on_air(self):
|
||||
"""On air diffusions."""
|
||||
@ -39,8 +39,8 @@ class DiffusionQuerySet(RerunQuerySet):
|
||||
def date(self, date=None, order=True):
|
||||
"""Diffusions occuring date."""
|
||||
date = date or datetime.date.today()
|
||||
start = tz.make_aware(tz.datetime.combine(date, datetime.time()))
|
||||
end = tz.make_aware(tz.datetime.combine(date, datetime.time(23, 59, 59, 999)))
|
||||
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))
|
||||
@ -48,7 +48,11 @@ class DiffusionQuerySet(RerunQuerySet):
|
||||
|
||||
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)
|
||||
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
|
||||
@ -93,8 +97,6 @@ class Diffusion(Rerun):
|
||||
- stop: the diffusion has been manually stopped
|
||||
"""
|
||||
|
||||
list_url_name = "timetable-list"
|
||||
|
||||
objects = DiffusionQuerySet.as_manager()
|
||||
|
||||
TYPE_ON_AIR = 0x00
|
||||
@ -133,10 +135,14 @@ class Diffusion(Rerun):
|
||||
# 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")),)
|
||||
permissions = (
|
||||
("programming", _("edit the diffusions' planification")),
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
str_ = "{episode} - {date}".format(
|
||||
@ -194,17 +200,47 @@ class Diffusion(Rerun):
|
||||
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_today(self):
|
||||
"""True if diffusion is currently today."""
|
||||
return self.start.date() == datetime.date.today()
|
||||
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.episodesound_set.all().broadcast()
|
||||
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
|
||||
@ -224,7 +260,8 @@ class Diffusion(Rerun):
|
||||
# .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)
|
||||
Q(start__lt=self.start, end__gt=self.start)
|
||||
| Q(start__gt=self.start, start__lt=self.end)
|
||||
)
|
||||
.exclude(pk=self.pk)
|
||||
.distinct()
|
||||
|
||||
@ -1,65 +1,58 @@
|
||||
import os
|
||||
|
||||
from django.conf import settings as d_settings
|
||||
from django.db import models
|
||||
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 .page import ChildPage
|
||||
from .page import Page
|
||||
from .program import ProgramChildQuerySet
|
||||
from .sound import Sound
|
||||
|
||||
__all__ = ("Episode",)
|
||||
|
||||
|
||||
class EpisodeQuerySet(ProgramChildQuerySet):
|
||||
def with_podcasts(self):
|
||||
return self.filter(episodesound__sound__is_public=True, episodesound__sound__is_removed=False).distinct()
|
||||
|
||||
|
||||
class Episode(ChildPage):
|
||||
objects = EpisodeQuerySet.as_manager()
|
||||
class Episode(Page):
|
||||
objects = ProgramChildQuerySet.as_manager()
|
||||
detail_url_name = "episode-detail"
|
||||
list_url_name = "episode-list"
|
||||
edit_url_name = "episode-edit"
|
||||
template_prefix = "episode"
|
||||
item_template_name = "aircox/widgets/episode_item.html"
|
||||
|
||||
@property
|
||||
def program(self):
|
||||
return self.parent_subclass
|
||||
return getattr(self.parent, "program", None)
|
||||
|
||||
@cached_property
|
||||
def podcasts(self):
|
||||
"""Return serialized data about podcasts."""
|
||||
from ..serializers import PodcastSerializer
|
||||
|
||||
podcasts = [
|
||||
PodcastSerializer(s).data
|
||||
for s in self.sound_set.public().order_by("type")
|
||||
]
|
||||
if self.cover:
|
||||
options = {"size": (128, 128), "crop": "scale"}
|
||||
cover = get_thumbnailer(self.cover).get_thumbnail(options).url
|
||||
else:
|
||||
cover = None
|
||||
|
||||
for index, podcast in enumerate(podcasts):
|
||||
podcasts[index]["cover"] = cover
|
||||
podcasts[index]["page_url"] = self.get_absolute_url()
|
||||
podcasts[index]["page_title"] = self.title
|
||||
return podcasts
|
||||
|
||||
@program.setter
|
||||
def program(self, value):
|
||||
self.parent = value
|
||||
|
||||
@cached_property
|
||||
def podcasts(self):
|
||||
"""Return serialized data about podcasts."""
|
||||
query = self.episodesound_set.available().public().order_by("-broadcast", "position")
|
||||
return self._to_podcasts(query)
|
||||
|
||||
@cached_property
|
||||
def sounds(self):
|
||||
"""Return serialized data about all related sounds."""
|
||||
query = self.episodesound_set.all().order_by("-broadcast", "position")
|
||||
return self._to_podcasts(query)
|
||||
|
||||
def _to_podcasts(self, query):
|
||||
from ..serializers import EpisodeSoundSerializer as serializer_class
|
||||
|
||||
query = query.select_related("sound")
|
||||
podcasts = [serializer_class(s).data for s in query]
|
||||
for index, podcast in enumerate(podcasts):
|
||||
podcasts[index]["page_url"] = self.get_absolute_url()
|
||||
podcasts[index]["page_title"] = self.title
|
||||
return podcasts
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Episode")
|
||||
verbose_name_plural = _("Episodes")
|
||||
|
||||
def get_absolute_url(self):
|
||||
if not self.is_published:
|
||||
return self.program.get_absolute_url()
|
||||
return super().get_absolute_url()
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self.parent is None:
|
||||
raise ValueError("missing parent program")
|
||||
@ -73,7 +66,7 @@ class Episode(ChildPage):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_init_kwargs_from(cls, page, date=None, title=None, **kwargs):
|
||||
def get_init_kwargs_from(cls, page, date, title=None, **kwargs):
|
||||
"""Get default Episode's title."""
|
||||
title = (
|
||||
settings.EPISODE_TITLE.format(
|
||||
@ -83,55 +76,6 @@ class Episode(ChildPage):
|
||||
if title is None
|
||||
else title
|
||||
)
|
||||
return super().get_init_kwargs_from(page, title=title, program=page, **kwargs)
|
||||
|
||||
|
||||
class EpisodeSoundQuerySet(models.QuerySet):
|
||||
def episode(self, episode):
|
||||
if isinstance(episode, int):
|
||||
return self.filter(episode_id=episode)
|
||||
return self.filter(episode=episode)
|
||||
|
||||
def available(self):
|
||||
return self.filter(sound__is_removed=False)
|
||||
|
||||
def public(self):
|
||||
return self.filter(sound__is_public=True)
|
||||
|
||||
def broadcast(self):
|
||||
return self.available().filter(broadcast=True)
|
||||
|
||||
def playlist(self, order="position"):
|
||||
# TODO: subquery expression
|
||||
if order:
|
||||
self = self.order_by(order)
|
||||
query = self.filter(sound__file__isnull=False, sound__is_removed=False).values_list("sound__file", flat=True)
|
||||
return [os.path.join(d_settings.MEDIA_ROOT, file) for file in query]
|
||||
|
||||
|
||||
class EpisodeSound(models.Model):
|
||||
"""Element of an episode playlist."""
|
||||
|
||||
episode = models.ForeignKey(Episode, on_delete=models.CASCADE)
|
||||
sound = models.ForeignKey(Sound, on_delete=models.CASCADE)
|
||||
position = models.PositiveSmallIntegerField(
|
||||
_("order"),
|
||||
default=0,
|
||||
help_text=_("position in the playlist"),
|
||||
)
|
||||
broadcast = models.BooleanField(
|
||||
_("Broadcast"),
|
||||
blank=None,
|
||||
help_text=_("The sound is broadcasted on air"),
|
||||
)
|
||||
|
||||
objects = EpisodeSoundQuerySet.as_manager()
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Podcast")
|
||||
verbose_name_plural = _("Podcasts")
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self.broadcast is None:
|
||||
self.broadcast = self.sound.broadcast
|
||||
super().save(*args, **kwargs)
|
||||
return super().get_init_kwargs_from(
|
||||
page, title=title, program=page, **kwargs
|
||||
)
|
||||
|
||||
@ -1,153 +0,0 @@
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.utils import timezone as tz
|
||||
|
||||
from .program import Program
|
||||
|
||||
|
||||
class FileQuerySet(models.QuerySet):
|
||||
def station(self, station=None, id=None):
|
||||
id = station.pk if id is None else id
|
||||
return self.filter(program__station__id=id)
|
||||
|
||||
def available(self):
|
||||
return self.exclude(is_removed=False)
|
||||
|
||||
def public(self):
|
||||
"""Return sounds available as podcasts."""
|
||||
return self.filter(is_public=True)
|
||||
|
||||
def path(self, paths):
|
||||
if isinstance(paths, str):
|
||||
return self.filter(file=paths.replace(settings.MEDIA_ROOT + "/", ""))
|
||||
return self.filter(file__in=(p.replace(settings.MEDIA_ROOT + "/", "") for p in paths))
|
||||
|
||||
def search(self, query):
|
||||
return self.filter(Q(name__icontains=query) | Q(file__icontains=query) | Q(program__title__icontains=query))
|
||||
|
||||
|
||||
class File(models.Model):
|
||||
def _upload_to(self, filename):
|
||||
dir = self.program and self.program.path or self.default_upload_path
|
||||
subdir = self.get_upload_dir()
|
||||
if subdir:
|
||||
return os.path.join(dir, subdir, filename)
|
||||
return os.path.join(dir, filename)
|
||||
|
||||
program = models.ForeignKey(
|
||||
Program,
|
||||
models.SET_NULL,
|
||||
verbose_name=_("Program"),
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
file = models.FileField(
|
||||
_("file"),
|
||||
upload_to=_upload_to,
|
||||
max_length=256,
|
||||
db_index=True,
|
||||
)
|
||||
name = models.CharField(
|
||||
_("name"),
|
||||
max_length=64,
|
||||
db_index=True,
|
||||
)
|
||||
description = models.TextField(
|
||||
_("description"),
|
||||
max_length=256,
|
||||
blank=True,
|
||||
default="",
|
||||
)
|
||||
mtime = models.DateTimeField(
|
||||
_("modification time"),
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text=_("last modification date and time"),
|
||||
)
|
||||
is_public = models.BooleanField(
|
||||
_("public"),
|
||||
help_text=_("file is publicly accessible"),
|
||||
default=False,
|
||||
)
|
||||
is_removed = models.BooleanField(
|
||||
_("removed"),
|
||||
help_text=_("file has been removed from server"),
|
||||
default=False,
|
||||
db_index=True,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
objects = FileQuerySet.as_manager()
|
||||
|
||||
default_upload_path = Path(settings.MEDIA_ROOT)
|
||||
"""Default upload directory when no program is provided."""
|
||||
upload_dir = "uploads"
|
||||
"""Upload sub-directory."""
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
return self.file and self.file.url
|
||||
|
||||
def get_upload_dir(self):
|
||||
return self.upload_dir
|
||||
|
||||
def get_mtime(self):
|
||||
"""Get the last modification date from file."""
|
||||
mtime = os.stat(self.file.path).st_mtime
|
||||
mtime = tz.datetime.fromtimestamp(mtime)
|
||||
mtime = mtime.replace(microsecond=0)
|
||||
return tz.make_aware(mtime, tz.get_current_timezone())
|
||||
|
||||
def file_updated(self):
|
||||
"""Return True when file has been updated on filesystem."""
|
||||
exists = self.file_exists()
|
||||
if self.is_removed != (not exists):
|
||||
return True
|
||||
return exists and self.mtime != self.get_mtime()
|
||||
|
||||
def file_exists(self):
|
||||
"""Return true if the file still exists."""
|
||||
return os.path.exists(self.file.path)
|
||||
|
||||
def sync_fs(self, on_update=False):
|
||||
"""Sync model to file on the filesystem.
|
||||
|
||||
:param bool on_update: only check if `file_updated`.
|
||||
:return True wether a change happened.
|
||||
"""
|
||||
if on_update and not self.file_updated():
|
||||
return
|
||||
|
||||
# check on name/remove/modification time
|
||||
name = self.name
|
||||
if not self.name and self.file and self.file.name:
|
||||
name = os.path.basename(self.file.name)
|
||||
name = os.path.splitext(name)[0]
|
||||
name = name.replace("_", " ").strip()
|
||||
|
||||
is_removed = not self.file_exists()
|
||||
mtime = (not is_removed and self.get_mtime()) or None
|
||||
|
||||
changed = is_removed != self.is_removed or mtime != self.mtime or name != self.name
|
||||
self.name, self.is_removed, self.mtime = name, is_removed, mtime
|
||||
|
||||
# read metadata
|
||||
if changed and not self.is_removed:
|
||||
metadata = self.read_metadata()
|
||||
metadata and self.__dict__.update(metadata)
|
||||
return changed
|
||||
|
||||
def read_metadata(self):
|
||||
return {}
|
||||
|
||||
def save(self, sync=True, *args, **kwargs):
|
||||
if sync and self.file_exists():
|
||||
self.sync_fs(on_update=True)
|
||||
super().save(*args, **kwargs)
|
||||
@ -1,27 +1,35 @@
|
||||
import datetime
|
||||
import gzip
|
||||
import logging
|
||||
import operator
|
||||
import os
|
||||
from collections import deque
|
||||
|
||||
import yaml
|
||||
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.conf import settings
|
||||
|
||||
__all__ = ("Settings", "settings")
|
||||
from .diffusion import Diffusion
|
||||
from .sound import Sound
|
||||
from .sound import Sound, Track
|
||||
from .station import Station
|
||||
from .track import Track
|
||||
from .page import Renderable
|
||||
|
||||
logger = logging.getLogger("aircox")
|
||||
|
||||
|
||||
__all__ = ("Log", "LogQuerySet")
|
||||
__all__ = ("Log", "LogQuerySet", "LogArchiver")
|
||||
|
||||
|
||||
class LogQuerySet(models.QuerySet):
|
||||
def station(self, station=None, id=None):
|
||||
return self.filter(station=station) if id is None else self.filter(station_id=id)
|
||||
return (
|
||||
self.filter(station=station)
|
||||
if id is None
|
||||
else self.filter(station_id=id)
|
||||
)
|
||||
|
||||
def date(self, date):
|
||||
start = tz.datetime.combine(date, datetime.time())
|
||||
@ -31,10 +39,11 @@ class LogQuerySet(models.QuerySet):
|
||||
# return self.filter(date__date=date)
|
||||
|
||||
def after(self, date):
|
||||
return self.filter(date__gte=date) if isinstance(date, tz.datetime) else self.filter(date__date__gte=date)
|
||||
|
||||
def before(self, date):
|
||||
return self.filter(date__lte=date) if isinstance(date, tz.datetime) else self.filter(date__date__lte=date)
|
||||
return (
|
||||
self.filter(date__gte=date)
|
||||
if isinstance(date, tz.datetime)
|
||||
else self.filter(date__date__gte=date)
|
||||
)
|
||||
|
||||
def on_air(self):
|
||||
return self.filter(type=Log.TYPE_ON_AIR)
|
||||
@ -52,15 +61,13 @@ class LogQuerySet(models.QuerySet):
|
||||
return self.filter(track__isnull=not with_it)
|
||||
|
||||
|
||||
class Log(Renderable, models.Model):
|
||||
class Log(models.Model):
|
||||
"""Log sounds and diffusions that are played on the station.
|
||||
|
||||
This only remember what has been played on the outputs, not on each
|
||||
source; Source designate here which source is responsible of that.
|
||||
"""
|
||||
|
||||
template_prefix = "log"
|
||||
|
||||
TYPE_STOP = 0x00
|
||||
"""Source has been stopped, e.g. manually."""
|
||||
# Rule: \/ diffusion != null \/ sound != null
|
||||
@ -98,7 +105,7 @@ class Log(Renderable, models.Model):
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name=_("source"),
|
||||
help_text=_("Identifier of the log's source."),
|
||||
help_text=_("identifier of the source related to this log"),
|
||||
)
|
||||
comment = models.CharField(
|
||||
max_length=512,
|
||||
@ -168,22 +175,21 @@ class Log(Renderable, models.Model):
|
||||
object_list += [cls(obj) for obj in items]
|
||||
|
||||
@classmethod
|
||||
def merge_diffusions(cls, logs, diffs, count=None, diff_count=None, group_logs=False):
|
||||
def merge_diffusions(cls, logs, diffs, count=None):
|
||||
"""Merge logs and diffusions together.
|
||||
|
||||
`logs` can either be a queryset or a list ordered by `Log.date`.
|
||||
"""
|
||||
# TODO: limit count
|
||||
# FIXME: log may be iterable (in stats view)
|
||||
if isinstance(logs, models.QuerySet):
|
||||
logs = list(logs.order_by("-date"))
|
||||
diffs = diffs.on_air().order_by("-start")
|
||||
if diff_count:
|
||||
diffs = diffs[:diff_count]
|
||||
diffs = deque(diffs)
|
||||
diffs = deque(diffs.on_air().before().order_by("-start"))
|
||||
object_list = []
|
||||
|
||||
while True:
|
||||
if not len(diffs):
|
||||
cls._append_logs(object_list, logs, len(logs), group=group_logs)
|
||||
object_list += logs
|
||||
break
|
||||
|
||||
if not len(logs):
|
||||
@ -193,8 +199,13 @@ class Log(Renderable, models.Model):
|
||||
diff = diffs.popleft()
|
||||
|
||||
# - takes all logs after diff start
|
||||
index = cls._next_index(logs, diff.end, len(logs), pred=operator.le)
|
||||
cls._append_logs(object_list, logs, index, group=group_logs)
|
||||
index = next(
|
||||
(i for i, v in enumerate(logs) if v.date <= diff.end),
|
||||
len(logs),
|
||||
)
|
||||
if index is not None and index > 0:
|
||||
object_list += logs[:index]
|
||||
logs = logs[index:]
|
||||
|
||||
if len(logs):
|
||||
# FIXME
|
||||
@ -203,7 +214,10 @@ class Log(Renderable, models.Model):
|
||||
# object_list.append(logs[0])
|
||||
|
||||
# - skips logs while diff is running
|
||||
index = cls._next_index(logs, diff.start, len(logs))
|
||||
index = next(
|
||||
(i for i, v in enumerate(logs) if v.date < diff.start),
|
||||
len(logs),
|
||||
)
|
||||
if index is not None and index > 0:
|
||||
logs = logs[index:]
|
||||
|
||||
@ -212,40 +226,6 @@ class Log(Renderable, models.Model):
|
||||
|
||||
return object_list if count is None else object_list[:count]
|
||||
|
||||
@classmethod
|
||||
def _next_index(cls, items, date, default, pred=operator.lt):
|
||||
iter = (i for i, v in enumerate(items) if pred(v.date, date))
|
||||
return next(iter, default)
|
||||
|
||||
@classmethod
|
||||
def _append_logs(cls, object_list, logs, count, group=False):
|
||||
logs = logs[:count]
|
||||
if not logs:
|
||||
return object_list
|
||||
|
||||
if group:
|
||||
grouped = cls._group_logs_by_time(logs)
|
||||
object_list.extend(grouped)
|
||||
else:
|
||||
object_list += logs
|
||||
return object_list
|
||||
|
||||
@classmethod
|
||||
def _group_logs_by_time(cls, logs):
|
||||
last_time = -1
|
||||
cum = []
|
||||
for log in logs:
|
||||
hour = log.date.time().hour
|
||||
if hour != last_time:
|
||||
if cum:
|
||||
yield cum
|
||||
cum = []
|
||||
last_time = hour
|
||||
# reverse from lowest to highest date
|
||||
cum.insert(0, log)
|
||||
if cum:
|
||||
yield cum
|
||||
|
||||
def print(self):
|
||||
r = []
|
||||
if self.diffusion:
|
||||
@ -260,3 +240,104 @@ class Log(Renderable, models.Model):
|
||||
self.comment or "",
|
||||
" (" + ", ".join(r) + ")" if r else "",
|
||||
)
|
||||
|
||||
|
||||
class LogArchiver:
|
||||
"""Commodity class used to manage archives of logs."""
|
||||
|
||||
@cached_property
|
||||
def fields(self):
|
||||
return Log._meta.get_fields()
|
||||
|
||||
@staticmethod
|
||||
def get_path(station, date):
|
||||
return os.path.join(
|
||||
settings.LOGS_ARCHIVES_DIR_ABS,
|
||||
"{}_{}.log.gz".format(date.strftime("%Y%m%d"), station.pk),
|
||||
)
|
||||
|
||||
def archive(self, qs, keep=False):
|
||||
"""Archive logs of the given queryset.
|
||||
|
||||
Delete archived logs if not `keep`. Return the count of archived
|
||||
logs
|
||||
"""
|
||||
if not qs.exists():
|
||||
return 0
|
||||
|
||||
os.makedirs(settings.LOGS_ARCHIVES_DIR_ABS, exist_ok=True)
|
||||
count = qs.count()
|
||||
logs = self.sort_logs(qs)
|
||||
|
||||
# Note: since we use Yaml, we can just append new logs when file
|
||||
# exists yet <3
|
||||
for (station, date), logs in logs.items():
|
||||
path = self.get_path(station, date)
|
||||
with gzip.open(path, "ab") as archive:
|
||||
data = yaml.dump(
|
||||
[self.serialize(line) for line in logs]
|
||||
).encode("utf8")
|
||||
archive.write(data)
|
||||
|
||||
if not keep:
|
||||
qs.delete()
|
||||
|
||||
return count
|
||||
|
||||
@staticmethod
|
||||
def sort_logs(qs):
|
||||
"""Sort logs by station and date and return a dict of `{
|
||||
(station,date): [logs] }`."""
|
||||
qs = qs.order_by("date")
|
||||
logs = {}
|
||||
for log in qs:
|
||||
key = (log.station, log.date)
|
||||
if key not in logs:
|
||||
logs[key] = [log]
|
||||
else:
|
||||
logs[key].append(log)
|
||||
return logs
|
||||
|
||||
def serialize(self, log):
|
||||
"""Serialize log."""
|
||||
return {i.attname: getattr(log, i.attname) for i in self.fields}
|
||||
|
||||
def load(self, station, date):
|
||||
"""Load an archive returning logs in a list."""
|
||||
path = self.get_path(station, date)
|
||||
|
||||
if not os.path.exists(path):
|
||||
return []
|
||||
|
||||
with gzip.open(path, "rb") as archive:
|
||||
data = archive.read()
|
||||
logs = yaml.load(data)
|
||||
|
||||
# we need to preload diffusions, sounds and tracks
|
||||
rels = {
|
||||
"diffusion": self.get_relations(logs, Diffusion, "diffusion"),
|
||||
"sound": self.get_relations(logs, Sound, "sound"),
|
||||
"track": self.get_relations(logs, Track, "track"),
|
||||
}
|
||||
|
||||
def rel_obj(log, attr):
|
||||
rel_id = log.get(attr + "_id")
|
||||
return rels[attr][rel_id] if rel_id else None
|
||||
|
||||
return [
|
||||
Log(
|
||||
diffusion=rel_obj(log, "diffusion"),
|
||||
sound=rel_obj(log, "sound"),
|
||||
track=rel_obj(log, "track"),
|
||||
**log
|
||||
)
|
||||
for log in logs
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def get_relations(logs, model, attr):
|
||||
"""From a list of dict representing logs, retrieve related objects of
|
||||
the given type."""
|
||||
attr_id = attr + "_id"
|
||||
pks = (log[attr_id] for log in logs if attr_id in log)
|
||||
return {rel.pk: rel for rel in model.objects.filter(pk__in=pks)}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import re
|
||||
|
||||
import bleach
|
||||
from ckeditor_uploader.fields import RichTextUploadingField
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone as tz
|
||||
@ -12,11 +13,9 @@ from django.utils.translation import gettext_lazy as _
|
||||
from filer.fields.image import FilerImageField
|
||||
from model_utils.managers import InheritanceQuerySet
|
||||
|
||||
from ..conf import settings
|
||||
from .station import Station
|
||||
|
||||
__all__ = (
|
||||
"Renderable",
|
||||
"Category",
|
||||
"PageQuerySet",
|
||||
"Page",
|
||||
@ -26,17 +25,9 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
headline_clean_re = re.compile(r"\n(\s| )+", re.MULTILINE)
|
||||
headline_re = re.compile(r"(?P<headline>([\S+]|\s+){1,240}\S+)", re.MULTILINE)
|
||||
|
||||
|
||||
class Renderable:
|
||||
template_prefix = "page"
|
||||
template_name = "aircox/widgets/{prefix}.html"
|
||||
|
||||
def get_template_name(self, widget):
|
||||
"""Return template name for the provided widget."""
|
||||
return self.template_name.format(prefix=self.template_prefix, widget=widget)
|
||||
headline_re = re.compile(
|
||||
r"(<p>)?" r"(?P<headline>[^\n]{1,140}(\n|[^\.]*?\.))" r"(</p>)?"
|
||||
)
|
||||
|
||||
|
||||
class Category(models.Model):
|
||||
@ -61,20 +52,23 @@ class BasePageQuerySet(InheritanceQuerySet):
|
||||
def trash(self):
|
||||
return self.filter(status=Page.STATUS_TRASH)
|
||||
|
||||
def by_last(self):
|
||||
return self.order_by("-pub_date")
|
||||
|
||||
def parent(self, parent=None, id=None):
|
||||
"""Return pages having this parent."""
|
||||
return self.filter(parent=parent) if id is None else self.filter(parent__id=id)
|
||||
return (
|
||||
self.filter(parent=parent)
|
||||
if id is None
|
||||
else self.filter(parent__id=id)
|
||||
)
|
||||
|
||||
def search(self, q, search_content=True):
|
||||
if search_content:
|
||||
return self.filter(models.Q(title__icontains=q) | models.Q(content__icontains=q))
|
||||
return self.filter(
|
||||
models.Q(title__icontains=q) | models.Q(content__icontains=q)
|
||||
)
|
||||
return self.filter(title__icontains=q)
|
||||
|
||||
|
||||
class BasePage(Renderable, models.Model):
|
||||
class BasePage(models.Model):
|
||||
"""Base class for publishable content."""
|
||||
|
||||
STATUS_DRAFT = 0x00
|
||||
@ -86,8 +80,18 @@ class BasePage(Renderable, models.Model):
|
||||
(STATUS_TRASH, _("trash")),
|
||||
)
|
||||
|
||||
parent = models.ForeignKey(
|
||||
"self",
|
||||
models.CASCADE,
|
||||
blank=True,
|
||||
null=True,
|
||||
db_index=True,
|
||||
related_name="child_set",
|
||||
)
|
||||
title = models.CharField(max_length=100)
|
||||
slug = models.SlugField(_("slug"), max_length=120, blank=True, unique=True, db_index=True)
|
||||
slug = models.SlugField(
|
||||
_("slug"), max_length=120, blank=True, unique=True, db_index=True
|
||||
)
|
||||
status = models.PositiveSmallIntegerField(
|
||||
_("status"),
|
||||
default=STATUS_DRAFT,
|
||||
@ -99,42 +103,40 @@ class BasePage(Renderable, models.Model):
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
content = models.TextField(_("content"), blank=True, null=True)
|
||||
content = RichTextUploadingField(
|
||||
_("content"),
|
||||
blank=True,
|
||||
null=True,
|
||||
)
|
||||
|
||||
objects = BasePageQuerySet.as_manager()
|
||||
|
||||
detail_url_name = None
|
||||
item_template_name = "aircox/widgets/page_item.html"
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
@property
|
||||
def cover_url(self):
|
||||
return self.cover_id and self.cover.url
|
||||
|
||||
def __str__(self):
|
||||
return "{}".format(self.title or self.pk)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self.content:
|
||||
self.content = bleach.clean(
|
||||
self.content,
|
||||
tags=settings.ALLOWED_TAGS,
|
||||
attributes=settings.ALLOWED_ATTRIBUTES,
|
||||
protocols=settings.ALLOWED_PROTOCOLS,
|
||||
)
|
||||
|
||||
if not self.slug:
|
||||
self.slug = slugify(self.title)[:100]
|
||||
count = Page.objects.filter(slug__startswith=self.slug).count()
|
||||
if count:
|
||||
self.slug += "-" + str(count)
|
||||
|
||||
if self.parent and not self.cover:
|
||||
self.cover = self.parent.cover
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def get_absolute_url(self):
|
||||
if self.is_published:
|
||||
return reverse(self.detail_url_name, kwargs={"slug": self.slug})
|
||||
return ""
|
||||
return (
|
||||
reverse(self.detail_url_name, kwargs={"slug": self.slug})
|
||||
if self.is_published
|
||||
else "#"
|
||||
)
|
||||
|
||||
@property
|
||||
def is_draft(self):
|
||||
@ -150,24 +152,17 @@ class BasePage(Renderable, models.Model):
|
||||
|
||||
@property
|
||||
def display_title(self):
|
||||
return self.is_published and self.title or ""
|
||||
if self.is_published():
|
||||
return self.title
|
||||
return self.parent.display_title()
|
||||
|
||||
@cached_property
|
||||
def display_headline(self):
|
||||
content = bleach.clean(self.content, tags=[], strip=True)
|
||||
content = headline_clean_re.sub("\n", content)
|
||||
if content.startswith("\n"):
|
||||
content = content[1:]
|
||||
headline = headline_re.search(content)
|
||||
if not headline:
|
||||
def headline(self):
|
||||
if not self.content:
|
||||
return ""
|
||||
|
||||
headline = headline.groupdict()["headline"]
|
||||
suffix = "<b>...</b>" if len(headline) < len(content) else ""
|
||||
|
||||
headline = headline.split("\n")[:3]
|
||||
headline[-1] += suffix
|
||||
return mark_safe(" ".join(headline))
|
||||
content = bleach.clean(self.content, tags=[], strip=True)
|
||||
headline = headline_re.search(content)
|
||||
return mark_safe(headline.groupdict()["headline"]) if headline else ""
|
||||
|
||||
@classmethod
|
||||
def get_init_kwargs_from(cls, page, **kwargs):
|
||||
@ -180,10 +175,11 @@ class BasePage(Renderable, models.Model):
|
||||
return cls(**cls.get_init_kwargs_from(page, **kwargs))
|
||||
|
||||
|
||||
# FIXME: rename
|
||||
class PageQuerySet(BasePageQuerySet):
|
||||
def published(self):
|
||||
return self.filter(status=Page.STATUS_PUBLISHED, pub_date__lte=tz.now())
|
||||
return self.filter(
|
||||
status=Page.STATUS_PUBLISHED, pub_date__lte=tz.now()
|
||||
)
|
||||
|
||||
|
||||
class Page(BasePage):
|
||||
@ -197,7 +193,9 @@ class Page(BasePage):
|
||||
null=True,
|
||||
db_index=True,
|
||||
)
|
||||
pub_date = models.DateTimeField(_("publication date"), blank=True, null=True, db_index=True)
|
||||
pub_date = models.DateTimeField(
|
||||
_("publication date"), blank=True, null=True, db_index=True
|
||||
)
|
||||
featured = models.BooleanField(
|
||||
_("featured"),
|
||||
default=False,
|
||||
@ -209,67 +207,18 @@ class Page(BasePage):
|
||||
|
||||
objects = PageQuerySet.as_manager()
|
||||
|
||||
detail_url_name = ""
|
||||
list_url_name = "page-list"
|
||||
edit_url_name = ""
|
||||
|
||||
@classmethod
|
||||
def get_list_url(cls, kwargs={}):
|
||||
return reverse(cls.list_url_name, kwargs=kwargs)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Publication")
|
||||
verbose_name_plural = _("Publications")
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.__initial_cover = self.cover
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self.is_published and self.pub_date is None:
|
||||
self.pub_date = tz.now()
|
||||
elif not self.is_published:
|
||||
self.pub_date = None
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
class ChildPage(Page):
|
||||
parent = models.ForeignKey(Page, models.CASCADE, blank=True, null=True, db_index=True, related_name="%(class)s_set")
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
@property
|
||||
def display_title(self):
|
||||
if self.is_published:
|
||||
return self.title
|
||||
return self.parent and self.parent.title or ""
|
||||
|
||||
@property
|
||||
def display_headline(self):
|
||||
if not self.content or not self.is_published:
|
||||
return self.parent and self.parent.display_headline or ""
|
||||
return super().display_headline
|
||||
|
||||
@cached_property
|
||||
def parent_subclass(self):
|
||||
if self.parent_id:
|
||||
return Page.objects.get_subclass(id=self.parent_id)
|
||||
return None
|
||||
|
||||
def get_absolute_url(self):
|
||||
if not self.is_published and self.parent_subclass:
|
||||
return self.parent_subclass.get_absolute_url()
|
||||
return super().get_absolute_url()
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self.parent:
|
||||
if self.parent == self:
|
||||
self.parent = None
|
||||
if not self.cover:
|
||||
self.cover = self.parent.cover
|
||||
if not self.category:
|
||||
self.category = self.parent.category
|
||||
if self.parent and not self.category:
|
||||
self.category = self.parent.category
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
@ -278,37 +227,45 @@ class StaticPage(BasePage):
|
||||
|
||||
detail_url_name = "static-page-detail"
|
||||
|
||||
class Target(models.TextChoices):
|
||||
NONE = "", _("None")
|
||||
HOME = "home", _("Home Page")
|
||||
TIMETABLE = "timetable-list", _("Timetable")
|
||||
PROGRAMS = "program-list", _("Programs list")
|
||||
EPISODES = "episode-list", _("Episodes list")
|
||||
ARTICLES = "article-list", _("Articles list")
|
||||
PAGES = "page-list", _("Publications list")
|
||||
PODCASTS = "podcast-list", _("Podcasts list")
|
||||
ATTACH_TO_HOME = 0x00
|
||||
ATTACH_TO_DIFFUSIONS = 0x01
|
||||
ATTACH_TO_LOGS = 0x02
|
||||
ATTACH_TO_PROGRAMS = 0x03
|
||||
ATTACH_TO_EPISODES = 0x04
|
||||
ATTACH_TO_ARTICLES = 0x05
|
||||
|
||||
attach_to = models.CharField(
|
||||
ATTACH_TO_CHOICES = (
|
||||
(ATTACH_TO_HOME, _("Home page")),
|
||||
(ATTACH_TO_DIFFUSIONS, _("Diffusions page")),
|
||||
(ATTACH_TO_LOGS, _("Logs page")),
|
||||
(ATTACH_TO_PROGRAMS, _("Programs list")),
|
||||
(ATTACH_TO_EPISODES, _("Episodes list")),
|
||||
(ATTACH_TO_ARTICLES, _("Articles list")),
|
||||
)
|
||||
VIEWS = {
|
||||
ATTACH_TO_HOME: "home",
|
||||
ATTACH_TO_DIFFUSIONS: "diffusion-list",
|
||||
ATTACH_TO_LOGS: "log-list",
|
||||
ATTACH_TO_PROGRAMS: "program-list",
|
||||
ATTACH_TO_EPISODES: "episode-list",
|
||||
ATTACH_TO_ARTICLES: "article-list",
|
||||
}
|
||||
|
||||
attach_to = models.SmallIntegerField(
|
||||
_("attach to"),
|
||||
choices=Target.choices,
|
||||
max_length=32,
|
||||
choices=ATTACH_TO_CHOICES,
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text=_("display this page content to related element"),
|
||||
)
|
||||
|
||||
def get_related_view(self):
|
||||
from ..views.page import attached_views
|
||||
|
||||
return self.attach_to and attached_views.get(self.attach_to) or None
|
||||
|
||||
def get_absolute_url(self):
|
||||
if self.attach_to:
|
||||
return reverse(self.attach_to)
|
||||
return reverse(self.VIEWS[self.attach_to])
|
||||
return super().get_absolute_url()
|
||||
|
||||
|
||||
class Comment(Renderable, models.Model):
|
||||
class Comment(models.Model):
|
||||
page = models.ForeignKey(
|
||||
Page,
|
||||
models.CASCADE,
|
||||
@ -321,16 +278,6 @@ class Comment(Renderable, models.Model):
|
||||
date = models.DateTimeField(auto_now_add=True)
|
||||
content = models.TextField(_("content"), max_length=1024)
|
||||
|
||||
template_prefix = "comment"
|
||||
|
||||
@cached_property
|
||||
def parent(self):
|
||||
"""Return Page as its subclass."""
|
||||
return Page.objects.select_subclasses().filter(id=self.page_id).first()
|
||||
|
||||
def get_absolute_url(self):
|
||||
return self.parent.get_absolute_url() + f"#{self._meta.label_lower}-{self.pk}"
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Comment")
|
||||
verbose_name_plural = _("Comments")
|
||||
@ -339,10 +286,12 @@ class Comment(Renderable, models.Model):
|
||||
class NavItem(models.Model):
|
||||
"""Navigation menu items."""
|
||||
|
||||
station = models.ForeignKey(Station, models.CASCADE, verbose_name=_("station"))
|
||||
station = models.ForeignKey(
|
||||
Station, models.CASCADE, verbose_name=_("station")
|
||||
)
|
||||
menu = models.SlugField(_("menu"), max_length=24)
|
||||
order = models.PositiveSmallIntegerField(_("order"))
|
||||
text = models.CharField(_("title"), max_length=64, blank=True, null=True)
|
||||
text = models.CharField(_("title"), max_length=64)
|
||||
url = models.CharField(_("url"), max_length=256, blank=True, null=True)
|
||||
page = models.ForeignKey(
|
||||
StaticPage,
|
||||
@ -359,23 +308,24 @@ class NavItem(models.Model):
|
||||
ordering = ("order", "pk")
|
||||
|
||||
def get_url(self):
|
||||
return self.url if self.url else self.page.get_absolute_url() if self.page else None
|
||||
|
||||
def get_label(self):
|
||||
if self.text:
|
||||
return self.text
|
||||
elif self.page:
|
||||
return self.page.title
|
||||
return (
|
||||
self.url
|
||||
if self.url
|
||||
else self.page.get_absolute_url()
|
||||
if self.page
|
||||
else None
|
||||
)
|
||||
|
||||
def render(self, request, css_class="", active_class=""):
|
||||
url = self.get_url()
|
||||
label = self.get_label()
|
||||
if active_class and request.path.startswith(url):
|
||||
css_class += " " + active_class
|
||||
|
||||
if not url:
|
||||
return label
|
||||
return self.text
|
||||
elif not css_class:
|
||||
return format_html('<a href="{}">{}</a>', url, label)
|
||||
return format_html('<a href="{}">{}</a>', url, self.text)
|
||||
else:
|
||||
return format_html('<a href="{}" class="{}">{}</a>', url, css_class, label)
|
||||
return format_html(
|
||||
'<a href="{}" class="{}">{}</a>', url, css_class, self.text
|
||||
)
|
||||
|
||||
@ -1,8 +1,11 @@
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
|
||||
from django.conf import settings as conf
|
||||
from django.contrib.auth.models import Group
|
||||
from django.db import models
|
||||
from django.db.models import F
|
||||
from django.db.models.functions import Concat, Substr
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from aircox.conf import settings
|
||||
@ -10,11 +13,13 @@ from aircox.conf import settings
|
||||
from .page import Page, PageQuerySet
|
||||
from .station import Station
|
||||
|
||||
logger = logging.getLogger("aircox")
|
||||
|
||||
|
||||
__all__ = (
|
||||
"ProgramQuerySet",
|
||||
"Program",
|
||||
"ProgramChildQuerySet",
|
||||
"ProgramQuerySet",
|
||||
"Stream",
|
||||
)
|
||||
|
||||
@ -27,16 +32,6 @@ class ProgramQuerySet(PageQuerySet):
|
||||
def active(self):
|
||||
return self.filter(active=True)
|
||||
|
||||
def editor(self, user):
|
||||
"""Return programs for which user is an editor.
|
||||
|
||||
Superuser is considered as editor of all groups.
|
||||
"""
|
||||
if user.is_superuser:
|
||||
return self
|
||||
groups = self.request.user.groups.all()
|
||||
return self.filter(editors_group__in=groups)
|
||||
|
||||
|
||||
class Program(Page):
|
||||
"""A Program can either be a Streamed or a Scheduled program.
|
||||
@ -52,7 +47,9 @@ class Program(Page):
|
||||
"""
|
||||
|
||||
# explicit foreign key in order to avoid related name clashes
|
||||
station = models.ForeignKey(Station, models.CASCADE, verbose_name=_("station"))
|
||||
station = models.ForeignKey(
|
||||
Station, models.CASCADE, verbose_name=_("station")
|
||||
)
|
||||
active = models.BooleanField(
|
||||
_("active"),
|
||||
default=True,
|
||||
@ -63,12 +60,9 @@ class Program(Page):
|
||||
default=True,
|
||||
help_text=_("update later diffusions according to schedule changes"),
|
||||
)
|
||||
editors_group = models.ForeignKey(Group, models.CASCADE, verbose_name=_("editors"))
|
||||
|
||||
objects = ProgramQuerySet.as_manager()
|
||||
detail_url_name = "program-detail"
|
||||
list_url_name = "program-list"
|
||||
edit_url_name = "program-edit"
|
||||
|
||||
@property
|
||||
def path(self):
|
||||
@ -88,10 +82,11 @@ class Program(Page):
|
||||
def excerpts_path(self):
|
||||
return os.path.join(self.path, settings.SOUND_ARCHIVES_SUBDIR)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
def __init__(self, *kargs, **kwargs):
|
||||
super().__init__(*kargs, **kwargs)
|
||||
if self.slug:
|
||||
self.__initial_path = self.path
|
||||
self.__initial_cover = self.cover
|
||||
|
||||
@classmethod
|
||||
def get_from_path(cl, path):
|
||||
@ -123,21 +118,34 @@ class Program(Page):
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.editors_group_id:
|
||||
from aircox import permissions
|
||||
def save(self, *kargs, **kwargs):
|
||||
from .sound import Sound
|
||||
|
||||
saved = permissions.program.init(self)
|
||||
if saved:
|
||||
return
|
||||
super().save(*kargs, **kwargs)
|
||||
|
||||
super().save()
|
||||
# TODO: move in signals
|
||||
path_ = getattr(self, "__initial_path", None)
|
||||
abspath = path_ and os.path.join(conf.MEDIA_ROOT, path_)
|
||||
if (
|
||||
path_ is not None
|
||||
and path_ != self.path
|
||||
and os.path.exists(abspath)
|
||||
and not os.path.exists(self.abspath)
|
||||
):
|
||||
logger.info(
|
||||
"program #%s's dir changed to %s - update it.",
|
||||
self.id,
|
||||
self.title,
|
||||
)
|
||||
|
||||
shutil.move(abspath, self.abspath)
|
||||
Sound.objects.filter(path__startswith=path_).update(
|
||||
file=Concat("file", Substr(F("file"), len(path_)))
|
||||
)
|
||||
|
||||
|
||||
class ProgramChildQuerySet(PageQuerySet):
|
||||
def station(self, station=None, id=None):
|
||||
# lookup `__program` is due to parent being a page subclass (page is
|
||||
# concrete).
|
||||
return (
|
||||
self.filter(parent__program__station=station)
|
||||
if id is None
|
||||
@ -147,10 +155,6 @@ class ProgramChildQuerySet(PageQuerySet):
|
||||
def program(self, program=None, id=None):
|
||||
return self.parent(program, id)
|
||||
|
||||
def editor(self, user):
|
||||
programs = Program.objects.editor(user)
|
||||
return self.filter(parent__program__in=programs)
|
||||
|
||||
|
||||
class Stream(models.Model):
|
||||
"""When there are no program scheduled, it is possible to play sounds in
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.db.models import F, Q
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from .program import Program
|
||||
@ -16,10 +15,18 @@ 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)
|
||||
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)
|
||||
return (
|
||||
self.filter(program=program)
|
||||
if id is None
|
||||
else self.filter(program__id=id)
|
||||
)
|
||||
|
||||
def rerun(self):
|
||||
return self.filter(initial__isnull=False)
|
||||
@ -46,7 +53,7 @@ class Rerun(models.Model):
|
||||
models.SET_NULL,
|
||||
related_name="rerun_set",
|
||||
verbose_name=_("rerun of"),
|
||||
limit_choices_to=Q(initial__isnull=True) & Q(program=F("program")),
|
||||
limit_choices_to={"initial__isnull": True},
|
||||
blank=True,
|
||||
null=True,
|
||||
db_index=True,
|
||||
@ -71,14 +78,17 @@ class Rerun(models.Model):
|
||||
|
||||
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")})
|
||||
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):
|
||||
if not self.program_id:
|
||||
self.program = self.initial.program
|
||||
if self.program != self.initial.program:
|
||||
raise ValidationError("Program for the rerun should be the same")
|
||||
self.program = self.initial.program
|
||||
|
||||
def save_initial(self):
|
||||
pass
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import calendar
|
||||
import zoneinfo
|
||||
from collections import OrderedDict
|
||||
|
||||
import pytz
|
||||
from django.db import models
|
||||
from django.utils import timezone as tz
|
||||
from django.utils.functional import cached_property
|
||||
@ -14,10 +15,6 @@ from .rerun import Rerun
|
||||
__all__ = ("Schedule",)
|
||||
|
||||
|
||||
def current_timezone_key():
|
||||
return tz.get_current_timezone().key
|
||||
|
||||
|
||||
# ? BIG FIXME: self.date is still used as datetime
|
||||
class Schedule(Rerun):
|
||||
"""A Schedule defines time slots of programs' diffusions.
|
||||
@ -31,7 +28,7 @@ class Schedule(Rerun):
|
||||
# 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):
|
||||
class Frequency(models.IntegerChoice):
|
||||
ponctual = 0b000000, _("ponctual")
|
||||
first = 0b000001, _("1st {day} of the month")
|
||||
second = 0b000010, _("2nd {day} of the month")
|
||||
@ -42,7 +39,6 @@ class Schedule(Rerun):
|
||||
second_and_fourth = 0b001010, _("2nd and 4th {day} of the month")
|
||||
every = 0b011111, _("{day}")
|
||||
one_on_two = 0b100000, _("one {day} on two")
|
||||
# every_weekday = 0b10000000 _("from Monday to Friday")
|
||||
|
||||
date = models.DateField(
|
||||
_("date"),
|
||||
@ -54,9 +50,9 @@ class Schedule(Rerun):
|
||||
)
|
||||
timezone = models.CharField(
|
||||
_("timezone"),
|
||||
default=current_timezone_key,
|
||||
default=tz.get_current_timezone,
|
||||
max_length=100,
|
||||
choices=sorted([(x, x) for x in zoneinfo.available_timezones()]),
|
||||
choices=[(x, x) for x in pytz.all_timezones],
|
||||
help_text=_("timezone used for the date"),
|
||||
)
|
||||
duration = models.TimeField(
|
||||
@ -72,14 +68,10 @@ class Schedule(Rerun):
|
||||
verbose_name = _("Schedule")
|
||||
verbose_name_plural = _("Schedules")
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self._initial = kwargs
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def __str__(self):
|
||||
return "{} - {}, {}".format(
|
||||
self.program.title,
|
||||
self.get_frequency_display(),
|
||||
self.get_frequency_verbose(),
|
||||
self.time.strftime("%H:%M"),
|
||||
)
|
||||
|
||||
@ -91,7 +83,9 @@ class Schedule(Rerun):
|
||||
@cached_property
|
||||
def tz(self):
|
||||
"""Pytz timezone of the schedule."""
|
||||
return zoneinfo.ZoneInfo(self.timezone)
|
||||
import pytz
|
||||
|
||||
return pytz.timezone(self.timezone)
|
||||
|
||||
@cached_property
|
||||
def start(self):
|
||||
@ -103,41 +97,35 @@ class Schedule(Rerun):
|
||||
"""Datetime of the end."""
|
||||
return self.start + utils.to_timedelta(self.duration)
|
||||
|
||||
def get_frequency_display(self):
|
||||
def get_frequency_verbose(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()
|
||||
return (
|
||||
self.get_frequency_display()
|
||||
.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 date.replace(tzinfo=self.tz)
|
||||
return self.tz.normalize(self.tz.localize(date))
|
||||
|
||||
def dates_of_month(self, date, frequency=None, sched_date=None):
|
||||
"""Return normalized diffusion dates of provided date's month.
|
||||
|
||||
:param Date date: date of the month to get dates from;
|
||||
:param Schedule.Frequency frequency: frequency (defaults to ``self.frequency``)
|
||||
:param Date sched_date: schedule start date (defaults to ``self.date``)
|
||||
:return list of diffusion dates
|
||||
"""
|
||||
if frequency is None:
|
||||
frequency = self.frequency
|
||||
|
||||
if sched_date is None:
|
||||
sched_date = self.date
|
||||
|
||||
if frequency == Schedule.Frequency.ponctual:
|
||||
def dates_of_month(self, date):
|
||||
"""Return normalized diffusion dates of provided date's month."""
|
||||
if self.frequency == Schedule.Frequency.ponctual:
|
||||
return []
|
||||
|
||||
sched_wday = sched_date.weekday()
|
||||
sched_wday, freq = self.date.weekday(), self.frequency
|
||||
date = date.replace(day=1)
|
||||
|
||||
# last of the month
|
||||
if frequency == Schedule.Frequency.last:
|
||||
date = date.replace(day=calendar.monthrange(date.year, date.month)[1])
|
||||
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
|
||||
@ -149,53 +137,69 @@ class Schedule(Rerun):
|
||||
# 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)
|
||||
date += tz.timedelta(
|
||||
days=(7 if date_wday > sched_wday else 0) - date_wday + sched_wday
|
||||
)
|
||||
|
||||
if frequency == Schedule.Frequency.one_on_two:
|
||||
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 - sched_date).days % 14:
|
||||
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 frequency & (0b1 << week))
|
||||
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, frequency=None, sched_date=None):
|
||||
def _exclude_existing_date(self, dates):
|
||||
from .diffusion 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.
|
||||
|
||||
:param Date date: date of the month to get diffusions from;
|
||||
:param Schedule.Frequency frequency: frequency (defaults to ``self.frequency``)
|
||||
:param Date sched_date: schedule start date (defaults to ``self.date``)
|
||||
:returns: tuple([Episode], [Diffusion])
|
||||
"""
|
||||
from .diffusion import Diffusion
|
||||
from .episode import Episode
|
||||
|
||||
if frequency is None:
|
||||
frequency = self.frequency
|
||||
|
||||
if sched_date is None:
|
||||
sched_date = self.date
|
||||
|
||||
if self.initial is not None or frequency == Schedule.Frequency.ponctual:
|
||||
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 - sched_date) for rerun in self.rerun_set.all()]
|
||||
reruns = [
|
||||
(rerun, rerun.date - self.date) for rerun in self.rerun_set.all()
|
||||
]
|
||||
|
||||
dates = {date: None for date in self.dates_of_month(date, frequency, sched_date)}
|
||||
dates = OrderedDict((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
|
||||
[
|
||||
(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
|
||||
)
|
||||
Diffusion.objects.filter(
|
||||
start__in=dates.keys(), program=self.program, schedule=self
|
||||
).values_list("start", flat=True)
|
||||
)
|
||||
|
||||
# make diffs
|
||||
|
||||
@ -1,27 +1,16 @@
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
|
||||
from django.conf import settings as conf
|
||||
from django.contrib.auth.models import Group, Permission, User
|
||||
from django.db import transaction
|
||||
from django.db.models import signals, F
|
||||
from django.db.models.functions import Concat, Substr
|
||||
from django.db.models import signals
|
||||
from django.dispatch import receiver
|
||||
from django.utils import timezone as tz
|
||||
|
||||
from aircox import utils
|
||||
from aircox.conf import settings
|
||||
from .article import Article
|
||||
from .diffusion import Diffusion
|
||||
from .episode import Episode
|
||||
from .page import Page
|
||||
from .program import Program
|
||||
from .schedule import Schedule
|
||||
from .sound import Sound
|
||||
|
||||
|
||||
logger = logging.getLogger("aircox")
|
||||
|
||||
|
||||
# Add a default group to a user when it is created. It also assigns a list
|
||||
@ -43,50 +32,40 @@ def user_default_groups(sender, instance, created, *args, **kwargs):
|
||||
group, created = Group.objects.get_or_create(name=group_name)
|
||||
if created and permissions:
|
||||
for codename in permissions:
|
||||
permission = Permission.objects.filter(codename=codename).first()
|
||||
permission = Permission.objects.filter(
|
||||
codename=codename
|
||||
).first()
|
||||
if permission:
|
||||
group.permissions.add(permission)
|
||||
group.save()
|
||||
instance.groups.add(group)
|
||||
|
||||
|
||||
# ---- page
|
||||
@receiver(signals.post_save, sender=Page)
|
||||
def page_post_save__child_page_defaults(sender, instance, created, *args, **kwargs):
|
||||
initial_cover = getattr(instance, "__initial_cover", None)
|
||||
if initial_cover is None and instance.cover is not None:
|
||||
Episode.objects.filter(parent=instance, cover__isnull=True).update(cover=instance.cover)
|
||||
Article.objects.filter(parent=instance, cover__isnull=True).update(cover=instance.cover)
|
||||
|
||||
|
||||
# ---- program
|
||||
@receiver(signals.post_save, sender=Program)
|
||||
def program_post_save__clean_later_episodes(sender, instance, created, *args, **kwargs):
|
||||
if not instance.active:
|
||||
Diffusion.objects.program(instance).after(tz.now()).delete()
|
||||
Episode.objects.parent(instance).filter(diffusion__isnull=True).delete()
|
||||
|
||||
|
||||
@receiver(signals.post_save, sender=Program)
|
||||
def program_post_save__mv_sounds(sender, instance, created, *args, **kwargs):
|
||||
path_ = getattr(instance, "__initial_path", None)
|
||||
if path_ in (None, instance.path):
|
||||
return
|
||||
|
||||
abspath = path_ and os.path.join(conf.MEDIA_ROOT, path_)
|
||||
if os.path.exists(abspath) and not os.path.exists(instance.abspath):
|
||||
logger.info(
|
||||
f"program #{instance.pk}'s dir changed to {instance.title} - update it.", instance.id, instance.title
|
||||
def page_post_save(sender, instance, created, *args, **kwargs):
|
||||
if not created and instance.cover:
|
||||
Page.objects.filter(parent=instance, cover__isnull=True).update(
|
||||
cover=instance.cover
|
||||
)
|
||||
|
||||
shutil.move(abspath, instance.abspath)
|
||||
Sound.objects.filter(path__startswith=path_).update(file=Concat("file", Substr(F("file"), len(path_))))
|
||||
|
||||
@receiver(signals.post_save, sender=Program)
|
||||
def program_post_save(sender, instance, created, *args, **kwargs):
|
||||
"""Clean-up later diffusions when a program becomes inactive."""
|
||||
if not instance.active:
|
||||
Diffusion.object.program(instance).after(tz.now()).delete()
|
||||
Episode.object.parent(instance).filter(diffusion__isnull=True).delete()
|
||||
|
||||
cover = getattr(instance, "__initial_cover", None)
|
||||
if cover is None and instance.cover is not None:
|
||||
Episode.objects.parent(instance).filter(cover__isnull=True).update(
|
||||
cover=instance.cover
|
||||
)
|
||||
|
||||
|
||||
# ---- schedule
|
||||
@receiver(signals.pre_save, sender=Schedule)
|
||||
def schedule_pre_save(sender, instance, *args, **kwargs):
|
||||
if getattr(instance, "pk") is not None and "raw" not in kwargs:
|
||||
if getattr(instance, "pk") is not None:
|
||||
instance._initial = Schedule.objects.get(pk=instance.pk)
|
||||
|
||||
|
||||
@ -96,7 +75,8 @@ def schedule_post_save(sender, instance, created, *args, **kwargs):
|
||||
corresponding diffusions accordingly."""
|
||||
initial = getattr(instance, "_initial", None)
|
||||
if not initial or (
|
||||
(instance.time, instance.duration, instance.timezone) == (initial.time, initial.duration, initial.timezone)
|
||||
(instance.time, instance.duration, instance.timezone)
|
||||
== (initial.time, initial.duration, initial.timezone)
|
||||
):
|
||||
return
|
||||
|
||||
@ -115,23 +95,13 @@ def schedule_post_save(sender, instance, created, *args, **kwargs):
|
||||
def schedule_pre_delete(sender, instance, *args, **kwargs):
|
||||
"""Delete later corresponding diffusion to a changed schedule."""
|
||||
Diffusion.objects.filter(schedule=instance).after(tz.now()).delete()
|
||||
Episode.objects.filter(diffusion__isnull=True, content__isnull=True, episodesound__isnull=True).delete()
|
||||
Episode.objects.filter(
|
||||
diffusion__isnull=True, content__isnull=True, sound__isnull=True
|
||||
).delete()
|
||||
|
||||
|
||||
# ---- diffusion
|
||||
@receiver(signals.post_delete, sender=Diffusion)
|
||||
def diffusion_post_delete(sender, instance, *args, **kwargs):
|
||||
Episode.objects.filter(diffusion__isnull=True, content__isnull=True, episodesound__isnull=True).delete()
|
||||
|
||||
|
||||
# ---- files
|
||||
@receiver(signals.post_delete, sender=Sound)
|
||||
def delete_file(sender, instance, *args, **kwargs):
|
||||
"""Deletes file on `post_delete`"""
|
||||
if not instance.file:
|
||||
return
|
||||
|
||||
path = instance.file.path
|
||||
qs = sender.objects.filter(file=path)
|
||||
if not qs.exists() and os.path.exists(path):
|
||||
os.remove(path)
|
||||
Episode.objects.filter(
|
||||
diffusion__isnull=True, content__isnull=True, sound__isnull=True
|
||||
).delete()
|
||||
|
||||
@ -1,195 +1,322 @@
|
||||
from datetime import date
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
|
||||
from django.conf import settings as conf
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.utils import timezone as tz
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from taggit.managers import TaggableManager
|
||||
|
||||
from aircox import utils
|
||||
from aircox.conf import settings
|
||||
|
||||
from .episode import Episode
|
||||
from .program import Program
|
||||
from .file import File, FileQuerySet
|
||||
|
||||
logger = logging.getLogger("aircox")
|
||||
|
||||
|
||||
__all__ = ("Sound", "SoundQuerySet")
|
||||
__all__ = ("Sound", "SoundQuerySet", "Track")
|
||||
|
||||
|
||||
class SoundQuerySet(FileQuerySet):
|
||||
class SoundQuerySet(models.QuerySet):
|
||||
def station(self, station=None, id=None):
|
||||
id = station.pk if id is None else id
|
||||
return self.filter(program__station__id=id)
|
||||
|
||||
def episode(self, episode=None, id=None):
|
||||
id = episode.pk if id is None else id
|
||||
return self.filter(episode__id=id)
|
||||
|
||||
def diffusion(self, diffusion=None, id=None):
|
||||
id = diffusion.pk if id is None else id
|
||||
return self.filter(episode__diffusion__id=id)
|
||||
|
||||
def available(self):
|
||||
return self.exclude(type=Sound.TYPE_REMOVED)
|
||||
|
||||
def public(self):
|
||||
"""Return sounds available as podcasts."""
|
||||
return self.filter(is_public=True)
|
||||
|
||||
def downloadable(self):
|
||||
"""Return sounds available as podcasts."""
|
||||
return self.filter(is_downloadable=True)
|
||||
|
||||
def broadcast(self):
|
||||
def archive(self):
|
||||
"""Return sounds that are archives."""
|
||||
return self.filter(broadcast=True, is_removed=False)
|
||||
return self.filter(type=Sound.TYPE_ARCHIVE)
|
||||
|
||||
def playlist(self, order_by="file"):
|
||||
def path(self, paths):
|
||||
if isinstance(paths, str):
|
||||
return self.filter(file=paths.replace(conf.MEDIA_ROOT + "/", ""))
|
||||
return self.filter(
|
||||
file__in=(p.replace(conf.MEDIA_ROOT + "/", "") for p in paths)
|
||||
)
|
||||
|
||||
def playlist(self, archive=True, order_by=True):
|
||||
"""Return files absolute paths as a flat list (exclude sound without
|
||||
path)."""
|
||||
path).
|
||||
|
||||
If `order_by` is True, order by path.
|
||||
"""
|
||||
if archive:
|
||||
self = self.archive()
|
||||
if order_by:
|
||||
self = self.order_by(order_by)
|
||||
self = self.order_by("file")
|
||||
return [
|
||||
os.path.join(conf.MEDIA_ROOT, file)
|
||||
for file in self.filter(file__isnull=False).values_list("file", flat=True)
|
||||
for file in self.filter(file__isnull=False).values_list(
|
||||
"file", flat=True
|
||||
)
|
||||
]
|
||||
|
||||
def search(self, query):
|
||||
return self.filter(
|
||||
Q(name__icontains=query)
|
||||
| Q(file__icontains=query)
|
||||
| Q(program__title__icontains=query)
|
||||
| Q(episode__title__icontains=query)
|
||||
)
|
||||
|
||||
class Sound(File):
|
||||
|
||||
# TODO:
|
||||
# - provide a default name based on program and episode
|
||||
class Sound(models.Model):
|
||||
"""A Sound is the representation of a sound file that can be either an
|
||||
excerpt or a complete archive of the related diffusion."""
|
||||
|
||||
TYPE_OTHER = 0x00
|
||||
TYPE_ARCHIVE = 0x01
|
||||
TYPE_EXCERPT = 0x02
|
||||
TYPE_REMOVED = 0x03
|
||||
TYPE_CHOICES = (
|
||||
(TYPE_OTHER, _("other")),
|
||||
(TYPE_ARCHIVE, _("archive")),
|
||||
(TYPE_EXCERPT, _("excerpt")),
|
||||
(TYPE_REMOVED, _("removed")),
|
||||
)
|
||||
|
||||
name = models.CharField(_("name"), max_length=64)
|
||||
program = models.ForeignKey(
|
||||
Program,
|
||||
models.CASCADE,
|
||||
blank=True, # NOT NULL
|
||||
verbose_name=_("program"),
|
||||
help_text=_("program related to it"),
|
||||
db_index=True,
|
||||
)
|
||||
episode = models.ForeignKey(
|
||||
Episode,
|
||||
models.SET_NULL,
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name=_("episode"),
|
||||
db_index=True,
|
||||
)
|
||||
type = models.SmallIntegerField(_("type"), choices=TYPE_CHOICES)
|
||||
position = models.PositiveSmallIntegerField(
|
||||
_("order"),
|
||||
default=0,
|
||||
help_text=_("position in the playlist"),
|
||||
)
|
||||
|
||||
def _upload_to(self, filename):
|
||||
subdir = (
|
||||
settings.SOUND_ARCHIVES_SUBDIR
|
||||
if self.type == self.TYPE_ARCHIVE
|
||||
else settings.SOUND_EXCERPTS_SUBDIR
|
||||
)
|
||||
return os.path.join(self.program.path, subdir, filename)
|
||||
|
||||
file = models.FileField(
|
||||
_("file"),
|
||||
upload_to=_upload_to,
|
||||
max_length=256,
|
||||
db_index=True,
|
||||
unique=True,
|
||||
)
|
||||
duration = models.TimeField(
|
||||
_("duration"),
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text=_("duration of the sound"),
|
||||
)
|
||||
mtime = models.DateTimeField(
|
||||
_("modification time"),
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text=_("last modification date and time"),
|
||||
)
|
||||
is_good_quality = models.BooleanField(
|
||||
_("good quality"),
|
||||
help_text=_("sound meets quality requirements"),
|
||||
blank=True,
|
||||
null=True,
|
||||
)
|
||||
is_downloadable = models.BooleanField(
|
||||
_("downloadable"),
|
||||
help_text=_("Sound can be downloaded by website visitors."),
|
||||
is_public = models.BooleanField(
|
||||
_("public"),
|
||||
help_text=_("whether it is publicly available as podcast"),
|
||||
default=False,
|
||||
)
|
||||
broadcast = models.BooleanField(
|
||||
_("Broadcast"),
|
||||
is_downloadable = models.BooleanField(
|
||||
_("downloadable"),
|
||||
help_text=_(
|
||||
"whether it can be publicly downloaded by visitors (sound must be "
|
||||
"public)"
|
||||
),
|
||||
default=False,
|
||||
help_text=_("The sound is broadcasted on air"),
|
||||
)
|
||||
|
||||
objects = SoundQuerySet.as_manager()
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Sound file")
|
||||
verbose_name_plural = _("Sound files")
|
||||
verbose_name = _("Sound")
|
||||
verbose_name_plural = _("Sounds")
|
||||
|
||||
_path_re = re.compile(
|
||||
"^(?P<year>[0-9]{4})(?P<month>[0-9]{2})(?P<day>[0-9]{2})"
|
||||
"(_(?P<hour>[0-9]{2})h(?P<minute>[0-9]{2}))?"
|
||||
"(_(?P<n>[0-9]+))?"
|
||||
"_?[ -]*(?P<name>.*)$"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def read_path(cls, path):
|
||||
"""Parse path name returning dictionary of extracted info. It can
|
||||
contain:
|
||||
|
||||
- `year`, `month`, `day`: diffusion date
|
||||
- `hour`, `minute`: diffusion time
|
||||
- `n`: sound arbitrary number (used for sound ordering)
|
||||
- `name`: cleaned name extracted or file name (without extension)
|
||||
"""
|
||||
basename = os.path.basename(path)
|
||||
basename = os.path.splitext(basename)[0]
|
||||
reg_match = cls._path_re.search(basename)
|
||||
if reg_match:
|
||||
info = reg_match.groupdict()
|
||||
for k in ("year", "month", "day", "hour", "minute", "n"):
|
||||
if info.get(k) is not None:
|
||||
info[k] = int(info[k])
|
||||
|
||||
name = info.get("name")
|
||||
info["name"] = name and cls._as_name(name) or basename
|
||||
else:
|
||||
info = {"name": basename}
|
||||
return info
|
||||
|
||||
@classmethod
|
||||
def _as_name(cls, name):
|
||||
name = name.replace("_", " ")
|
||||
return " ".join(r.capitalize() for r in name.split(" "))
|
||||
|
||||
def find_episode(self, path_info=None):
|
||||
"""Base on self's file name, match date to an initial diffusion and
|
||||
return corresponding episode or ``None``."""
|
||||
pi = path_info or self.read_path(self.file.path)
|
||||
if "year" not in pi:
|
||||
return None
|
||||
|
||||
year, month, day = pi.get("year"), pi.get("month"), pi.get("day")
|
||||
if pi.get("hour") is not None:
|
||||
at = tz.datetime(year, month, day, pi.get("hour", 0), pi.get("minute", 0))
|
||||
at = tz.make_aware(at)
|
||||
else:
|
||||
at = date(year, month, day)
|
||||
|
||||
diffusion = self.program.diffusion_set.at(at).first()
|
||||
return diffusion and diffusion.episode or None
|
||||
|
||||
def find_playlist(self, meta=None):
|
||||
"""Find a playlist file corresponding to the sound path, such as:
|
||||
my_sound.ogg => my_sound.csv.
|
||||
|
||||
Use provided sound's metadata if any and no csv file has been
|
||||
found.
|
||||
"""
|
||||
from aircox.controllers.playlist_import import PlaylistImport
|
||||
from .track import Track
|
||||
|
||||
if self.track_set.count() > 1:
|
||||
return
|
||||
|
||||
# import playlist
|
||||
path_noext, ext = os.path.splitext(self.file.path)
|
||||
path = path_noext + ".csv"
|
||||
if os.path.exists(path):
|
||||
PlaylistImport(path, sound=self).run()
|
||||
# use metadata
|
||||
elif meta and meta.tags:
|
||||
title, artist, album, year = tuple(
|
||||
t and ", ".join(t) for t in (meta.tags.get(k) for k in ("title", "artist", "album", "year"))
|
||||
)
|
||||
title = title or path_noext
|
||||
info = "{} ({})".format(album, year) if album and year else album or year or ""
|
||||
track = Track(
|
||||
sound=self,
|
||||
position=int(meta.tags.get("tracknumber", 0)),
|
||||
title=title,
|
||||
artist=artist or _("unknown"),
|
||||
info=info,
|
||||
)
|
||||
track.save()
|
||||
|
||||
def get_upload_dir(self):
|
||||
if self.broadcast:
|
||||
return settings.SOUND_BROADCASTS_SUBDIR
|
||||
return settings.SOUND_EXCERPTS_SUBDIR
|
||||
|
||||
meta = None
|
||||
"""Provided by read_metadata: Mutagen's metadata."""
|
||||
|
||||
def sync_fs(self, *args, find_playlist=False, **kwargs):
|
||||
changed = super().sync_fs(*args, **kwargs)
|
||||
if changed and not self.is_removed:
|
||||
if not self.program:
|
||||
self.program = Program.get_from_path(self.file.path)
|
||||
changed = True
|
||||
if find_playlist and self.meta:
|
||||
not self.pk and self.save(sync=False)
|
||||
self.find_playlist(self.meta)
|
||||
return changed
|
||||
|
||||
def read_metadata(self):
|
||||
import mutagen
|
||||
|
||||
meta = mutagen.File(self.file.path)
|
||||
|
||||
metadata = {"duration": utils.seconds_to_time(meta.info.length), "meta": meta}
|
||||
|
||||
path_info = self.read_path(self.file.path)
|
||||
if name := path_info.get("name"):
|
||||
metadata["name"] = name
|
||||
return metadata
|
||||
@property
|
||||
def url(self):
|
||||
return self.file and self.file.url
|
||||
|
||||
def __str__(self):
|
||||
infos = ""
|
||||
if self.is_removed:
|
||||
infos += _("removed")
|
||||
if infos:
|
||||
return f"{self.file.name} [{infos}]"
|
||||
return f"{self.file.name}"
|
||||
return "/".join(self.file.path.split("/")[-3:])
|
||||
|
||||
def save(self, check=True, *args, **kwargs):
|
||||
if self.episode is not None and self.program is None:
|
||||
self.program = self.episode.program
|
||||
if check:
|
||||
self.check_on_file()
|
||||
if not self.is_public:
|
||||
self.is_downloadable = False
|
||||
self.__check_name()
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
# TODO: rename get_file_mtime(self)
|
||||
def get_mtime(self):
|
||||
"""Get the last modification date from file."""
|
||||
mtime = os.stat(self.file.path).st_mtime
|
||||
mtime = tz.datetime.fromtimestamp(mtime)
|
||||
mtime = mtime.replace(microsecond=0)
|
||||
return tz.make_aware(mtime, tz.get_current_timezone())
|
||||
|
||||
def file_exists(self):
|
||||
"""Return true if the file still exists."""
|
||||
|
||||
return os.path.exists(self.file.path)
|
||||
|
||||
# TODO: rename to sync_fs()
|
||||
def check_on_file(self):
|
||||
"""Check sound file info again'st self, and update informations if
|
||||
needed (do not save).
|
||||
|
||||
Return True if there was changes.
|
||||
"""
|
||||
if not self.file_exists():
|
||||
if self.type == self.TYPE_REMOVED:
|
||||
return
|
||||
logger.debug("sound %s: has been removed", self.file.name)
|
||||
self.type = self.TYPE_REMOVED
|
||||
return True
|
||||
|
||||
# not anymore removed
|
||||
changed = False
|
||||
|
||||
if self.type == self.TYPE_REMOVED and self.program:
|
||||
changed = True
|
||||
self.type = (
|
||||
self.TYPE_ARCHIVE
|
||||
if self.file.name.startswith(self.program.archives_path)
|
||||
else self.TYPE_EXCERPT
|
||||
)
|
||||
|
||||
# check mtime -> reset quality if changed (assume file changed)
|
||||
mtime = self.get_mtime()
|
||||
|
||||
if self.mtime != mtime:
|
||||
self.mtime = mtime
|
||||
self.is_good_quality = None
|
||||
logger.debug(
|
||||
"sound %s: m_time has changed. Reset quality info",
|
||||
self.file.name,
|
||||
)
|
||||
return True
|
||||
|
||||
return changed
|
||||
|
||||
def __check_name(self):
|
||||
if not self.name and self.file and self.file.name:
|
||||
# FIXME: later, remove date?
|
||||
name = os.path.basename(self.file.name)
|
||||
name = os.path.splitext(name)[0]
|
||||
self.name = name.replace("_", " ").strip()
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.__check_name()
|
||||
|
||||
|
||||
class Track(models.Model):
|
||||
"""Track of a playlist of an object.
|
||||
|
||||
The position can either be expressed as the position in the playlist
|
||||
or as the moment in seconds it started.
|
||||
"""
|
||||
|
||||
episode = models.ForeignKey(
|
||||
Episode,
|
||||
models.CASCADE,
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name=_("episode"),
|
||||
)
|
||||
sound = models.ForeignKey(
|
||||
Sound,
|
||||
models.CASCADE,
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name=_("sound"),
|
||||
)
|
||||
position = models.PositiveSmallIntegerField(
|
||||
_("order"),
|
||||
default=0,
|
||||
help_text=_("position in the playlist"),
|
||||
)
|
||||
timestamp = models.PositiveSmallIntegerField(
|
||||
_("timestamp"),
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text=_("position (in seconds)"),
|
||||
)
|
||||
title = models.CharField(_("title"), max_length=128)
|
||||
artist = models.CharField(_("artist"), max_length=128)
|
||||
album = models.CharField(_("album"), max_length=128, null=True, blank=True)
|
||||
tags = TaggableManager(verbose_name=_("tags"), blank=True)
|
||||
year = models.IntegerField(_("year"), blank=True, null=True)
|
||||
# FIXME: remove?
|
||||
info = models.CharField(
|
||||
_("information"),
|
||||
max_length=128,
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text=_(
|
||||
"additional informations about this track, such as "
|
||||
"the version, if is it a remix, features, etc."
|
||||
),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Track")
|
||||
verbose_name_plural = _("Tracks")
|
||||
ordering = ("position",)
|
||||
|
||||
def __str__(self):
|
||||
return "{self.artist} -- {self.title} -- {self.position}".format(
|
||||
self=self
|
||||
)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if (self.sound is None and self.episode is None) or (
|
||||
self.sound is not None and self.episode is not None
|
||||
):
|
||||
raise ValueError("sound XOR episode is required")
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
@ -1,8 +1,11 @@
|
||||
import os
|
||||
|
||||
from django.db import models
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from filer.fields.image import FilerImageField
|
||||
|
||||
from aircox.conf import settings
|
||||
|
||||
__all__ = ("Station", "StationQuerySet", "Port")
|
||||
|
||||
@ -29,6 +32,13 @@ class Station(models.Model):
|
||||
|
||||
name = models.CharField(_("name"), max_length=64)
|
||||
slug = models.SlugField(_("slug"), max_length=64, unique=True)
|
||||
# FIXME: remove - should be decided only by Streamer controller + settings
|
||||
path = models.CharField(
|
||||
_("path"),
|
||||
help_text=_("path to the working directory"),
|
||||
max_length=256,
|
||||
blank=True,
|
||||
)
|
||||
default = models.BooleanField(
|
||||
_("default station"),
|
||||
default=False,
|
||||
@ -50,14 +60,16 @@ class Station(models.Model):
|
||||
max_length=512,
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text=_("specify one domain per line, without 'http://' prefix"),
|
||||
help_text=_("specify one url per line"),
|
||||
)
|
||||
audio_streams = models.TextField(
|
||||
_("audio streams"),
|
||||
max_length=2048,
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text=_("Audio streams urls used by station's player. One url a line."),
|
||||
help_text=_(
|
||||
"Audio streams urls used by station's player. One url " "a line."
|
||||
),
|
||||
)
|
||||
default_cover = FilerImageField(
|
||||
on_delete=models.SET_NULL,
|
||||
@ -66,14 +78,6 @@ class Station(models.Model):
|
||||
blank=True,
|
||||
related_name="+",
|
||||
)
|
||||
music_stream_title = models.CharField(
|
||||
_("Music stream's title"),
|
||||
max_length=64,
|
||||
default=_("Music stream"),
|
||||
)
|
||||
legal_label = models.CharField(
|
||||
_("Legal label"), max_length=64, blank=True, default="", help_text=_("Displayed at the bottom of pages.")
|
||||
)
|
||||
|
||||
objects = StationQuerySet.as_manager()
|
||||
|
||||
@ -86,6 +90,12 @@ class Station(models.Model):
|
||||
return self.name
|
||||
|
||||
def save(self, make_sources=True, *args, **kwargs):
|
||||
if not self.path:
|
||||
self.path = os.path.join(
|
||||
settings.CONTROLLERS_WORKING_DIR,
|
||||
self.slug.replace("-", "_"),
|
||||
)
|
||||
|
||||
if self.default:
|
||||
qs = Station.objects.filter(default=True)
|
||||
if self.pk is not None:
|
||||
@ -143,10 +153,16 @@ class Port(models.Model):
|
||||
(TYPE_FILE, _("file")),
|
||||
)
|
||||
|
||||
station = models.ForeignKey(Station, models.CASCADE, verbose_name=_("station"))
|
||||
direction = models.SmallIntegerField(_("direction"), choices=DIRECTION_CHOICES)
|
||||
station = models.ForeignKey(
|
||||
Station, models.CASCADE, verbose_name=_("station")
|
||||
)
|
||||
direction = models.SmallIntegerField(
|
||||
_("direction"), choices=DIRECTION_CHOICES
|
||||
)
|
||||
type = models.SmallIntegerField(_("type"), choices=TYPE_CHOICES)
|
||||
active = models.BooleanField(_("active"), default=True, help_text=_("this port is active"))
|
||||
active = models.BooleanField(
|
||||
_("active"), default=True, help_text=_("this port is active")
|
||||
)
|
||||
settings = models.TextField(
|
||||
_("port settings"),
|
||||
help_text=_(
|
||||
@ -177,6 +193,8 @@ class Port(models.Model):
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.is_valid_type():
|
||||
raise ValueError("port type is not allowed with the given port direction")
|
||||
raise ValueError(
|
||||
"port type is not allowed with the given port direction"
|
||||
)
|
||||
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
@ -1,72 +0,0 @@
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from taggit.managers import TaggableManager
|
||||
|
||||
|
||||
from .episode import Episode
|
||||
from .sound import Sound
|
||||
|
||||
|
||||
__all__ = ("Track",)
|
||||
|
||||
|
||||
class Track(models.Model):
|
||||
"""Track of a playlist of an object.
|
||||
|
||||
The position can either be expressed as the position in the playlist
|
||||
or as the moment in seconds it started.
|
||||
"""
|
||||
|
||||
episode = models.ForeignKey(
|
||||
Episode,
|
||||
models.CASCADE,
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name=_("episode"),
|
||||
)
|
||||
sound = models.ForeignKey(
|
||||
Sound,
|
||||
models.CASCADE,
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name=_("sound"),
|
||||
)
|
||||
position = models.PositiveSmallIntegerField(
|
||||
_("order"),
|
||||
default=0,
|
||||
help_text=_("position in the playlist"),
|
||||
)
|
||||
timestamp = models.PositiveSmallIntegerField(
|
||||
_("timestamp"),
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text=_("position (in seconds)"),
|
||||
)
|
||||
title = models.CharField(_("title"), max_length=128)
|
||||
artist = models.CharField(_("artist"), max_length=128)
|
||||
album = models.CharField(_("album"), max_length=128, null=True, blank=True)
|
||||
tags = TaggableManager(verbose_name=_("tags"), blank=True)
|
||||
year = models.IntegerField(_("year"), blank=True, null=True)
|
||||
# FIXME: remove?
|
||||
info = models.CharField(
|
||||
_("information"),
|
||||
max_length=128,
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text=_(
|
||||
"additional informations about this track, such as " "the version, if is it a remix, features, etc."
|
||||
),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Track")
|
||||
verbose_name_plural = _("Tracks")
|
||||
ordering = ("position",)
|
||||
|
||||
def __str__(self):
|
||||
return "{self.artist} -- {self.title} -- {self.position}".format(self=self)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if (self.sound is None and self.episode is None) or (self.sound is not None and self.episode is not None):
|
||||
raise ValueError("sound XOR episode is required")
|
||||
super().save(*args, **kwargs)
|
||||
@ -14,5 +14,7 @@ class UserSettings(models.Model):
|
||||
verbose_name=_("User"),
|
||||
related_name="aircox_settings",
|
||||
)
|
||||
tracklist_editor_columns = models.JSONField(_("Playlist Editor Columns"))
|
||||
tracklist_editor_sep = models.CharField(_("Playlist Editor Separator"), max_length=16)
|
||||
playlist_editor_columns = models.JSONField(_("Playlist Editor Columns"))
|
||||
playlist_editor_sep = models.CharField(
|
||||
_("Playlist Editor Separator"), max_length=16
|
||||
)
|
||||
|
||||
@ -1,89 +0,0 @@
|
||||
# Provide permissions handling
|
||||
# we don't import models at module level in order to avoid migration problems
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.contrib.auth.models import Group, Permission
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
from .models import Program
|
||||
|
||||
|
||||
__all__ = ("PagePermissions", "program")
|
||||
|
||||
|
||||
class PagePermissions:
|
||||
"""Handles obj permissions initialization of page subclass."""
|
||||
|
||||
model = None
|
||||
# TODO: move values to subclass
|
||||
groups = ({"label": _("editors"), "field": "editors_group_id", "perms": ["update"]},)
|
||||
"""Groups informations initialized."""
|
||||
groups_name_format = "{obj.title}: {group_label}"
|
||||
"""Format used for groups name."""
|
||||
perms_name_format = "{obj.title}: can {perm}"
|
||||
"""Format used for permission name (displayed to humans)."""
|
||||
perms_codename_format = "{obj._meta.label_lower}_{obj.pk}_{perm}"
|
||||
"""Format used for permissions codename."""
|
||||
|
||||
def __init__(self, model):
|
||||
self.model = model
|
||||
|
||||
def can(self, user, perm, obj):
|
||||
"""Return True wether if user can edit Program or its children."""
|
||||
from .models.page import ChildPage
|
||||
|
||||
if isinstance(obj, ChildPage):
|
||||
obj = obj.parent_subclass
|
||||
|
||||
if not isinstance(obj, self.model):
|
||||
return False
|
||||
|
||||
if user.is_superuser:
|
||||
return True
|
||||
|
||||
perm = self.perms_codename_format.format(self=self, perm=perm, obj=obj)
|
||||
return user.has_perm(perm)
|
||||
|
||||
def init(self, obj, model=None):
|
||||
"""Initialize permissions for the provided obj.
|
||||
|
||||
Return True if group or permission have been created (`obj` has
|
||||
thus been saved).
|
||||
"""
|
||||
updated = False
|
||||
created_groups = []
|
||||
|
||||
# init groups
|
||||
for infos in self.groups:
|
||||
group = getattr(obj, infos["field"])
|
||||
if not group:
|
||||
group, created = self.init_group(obj, infos)
|
||||
setattr(obj, infos["field"], group.pk)
|
||||
updated = True
|
||||
created and created_groups.append((group, infos))
|
||||
|
||||
if updated:
|
||||
obj.save()
|
||||
|
||||
# init perms
|
||||
for group, infos in created_groups:
|
||||
self.init_perms(obj, group, infos)
|
||||
|
||||
return updated
|
||||
|
||||
def init_group(self, obj, infos):
|
||||
name = self.groups_name_format.format(obj=obj, group_label=infos["label"])
|
||||
return Group.objects.get_or_create(name=name)
|
||||
|
||||
def init_perms(self, obj, group, infos):
|
||||
# TODO: avoid multiple database hits
|
||||
for name in infos["perms"]:
|
||||
perm, _ = Permission.objects.get_or_create(
|
||||
codename=self.perms_codename_format.format(obj=obj, perm=name),
|
||||
content_type=ContentType.objects.get_for_model(obj),
|
||||
defaults={"name": self.perms_name_format.format(obj=obj, perm=name)},
|
||||
)
|
||||
if perm not in group.permissions.all():
|
||||
group.permissions.add(perm)
|
||||
|
||||
|
||||
program = PagePermissions(Program)
|
||||
@ -1,18 +1,12 @@
|
||||
from . import auth
|
||||
from .admin import TrackSerializer, UserSettingsSerializer
|
||||
from .episode import EpisodeSoundSerializer, EpisodeSerializer
|
||||
from .log import LogInfo, LogInfoSerializer
|
||||
from .page import CommentSerializer
|
||||
from .sound import SoundSerializer
|
||||
from .sound import PodcastSerializer, SoundSerializer
|
||||
|
||||
__all__ = (
|
||||
"auth",
|
||||
"CommentSerializer",
|
||||
"LogInfo",
|
||||
"LogInfoSerializer",
|
||||
"EpisodeSoundSerializer",
|
||||
"EpisodeSerializer",
|
||||
"SoundSerializer",
|
||||
"TrackSerializer",
|
||||
"UserSettingsSerializer",
|
||||
"LogInfo",
|
||||
"LogInfoSerializer",
|
||||
"SoundSerializer",
|
||||
"PodcastSerializer",
|
||||
)
|
||||
|
||||
@ -1,17 +1,9 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from filer.models.imagemodels import Image
|
||||
from taggit.serializers import TaggitSerializer, TagListSerializerField
|
||||
|
||||
from ..models import Track, UserSettings
|
||||
|
||||
__all__ = ("ImageSerializer", "TrackSerializer", "UserSettingsSerializer")
|
||||
|
||||
|
||||
class ImageSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Image
|
||||
fields = "__all__"
|
||||
__all__ = ("TrackSerializer", "UserSettingsSerializer")
|
||||
|
||||
|
||||
class TrackSerializer(TaggitSerializer, serializers.ModelSerializer):
|
||||
@ -35,10 +27,10 @@ class TrackSerializer(TaggitSerializer, serializers.ModelSerializer):
|
||||
|
||||
|
||||
class UserSettingsSerializer(serializers.ModelSerializer):
|
||||
# TODO: validate fields values (tracklist_editor_columns at least)
|
||||
# TODO: validate fields values (playlist_editor_columns at least)
|
||||
class Meta:
|
||||
model = UserSettings
|
||||
fields = ("tracklist_editor_columns", "tracklist_editor_sep")
|
||||
fields = ("playlist_editor_columns", "playlist_editor_sep")
|
||||
|
||||
def create(self, validated_data):
|
||||
user = self.context.get("user")
|
||||
|
||||
@ -1,28 +0,0 @@
|
||||
from django.contrib.auth.models import User, Group
|
||||
from rest_framework import serializers
|
||||
|
||||
|
||||
__all__ = ("UserSerializer", "GroupSerializer", "UserGroupSerializer")
|
||||
|
||||
|
||||
class UserSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
exclude = ("password",)
|
||||
model = User
|
||||
|
||||
|
||||
class GroupSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
exclude = ("permissions",)
|
||||
model = Group
|
||||
|
||||
|
||||
class UserGroupSerializer(serializers.ModelSerializer):
|
||||
group = GroupSerializer(read_only=True)
|
||||
user = UserSerializer(read_only=True)
|
||||
user_id = serializers.IntegerField()
|
||||
group_id = serializers.IntegerField()
|
||||
|
||||
class Meta:
|
||||
model = User.groups.through
|
||||
fields = ("id", "group_id", "user_id", "group", "user")
|
||||
@ -1,36 +0,0 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from .. import models
|
||||
from .sound import SoundSerializer
|
||||
from .admin import TrackSerializer
|
||||
|
||||
|
||||
class EpisodeSoundSerializer(serializers.ModelSerializer):
|
||||
sound = SoundSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.EpisodeSound
|
||||
fields = [
|
||||
"id",
|
||||
"position",
|
||||
"episode",
|
||||
"broadcast",
|
||||
"sound",
|
||||
"sound_id",
|
||||
]
|
||||
|
||||
|
||||
class EpisodeSerializer(serializers.ModelSerializer):
|
||||
playlist = EpisodeSoundSerializer(source="episodesound_set", many=True, read_only=True)
|
||||
tracks = TrackSerializer(source="track_set", many=True, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.Episode
|
||||
fields = [
|
||||
"id",
|
||||
"title",
|
||||
"content",
|
||||
"pub_date",
|
||||
"playlist",
|
||||
"tracks",
|
||||
]
|
||||
@ -1,12 +0,0 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from aircox import models
|
||||
|
||||
|
||||
__all__ = ("CommentSerializer",)
|
||||
|
||||
|
||||
class CommentSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = models.Comment
|
||||
fields = ["page", "nickname", "email", "date", "content"]
|
||||
@ -1,24 +1,43 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from .. import models
|
||||
from ..models import Sound
|
||||
|
||||
__all__ = ("SoundSerializer",)
|
||||
__all__ = ("SoundSerializer", "PodcastSerializer")
|
||||
|
||||
|
||||
class SoundSerializer(serializers.ModelSerializer):
|
||||
file = serializers.FileField(use_url=False)
|
||||
|
||||
class Meta:
|
||||
model = models.Sound
|
||||
model = Sound
|
||||
fields = [
|
||||
"id",
|
||||
"pk",
|
||||
"name",
|
||||
"program",
|
||||
"episode",
|
||||
"type",
|
||||
"file",
|
||||
"duration",
|
||||
"mtime",
|
||||
"is_good_quality",
|
||||
"is_public",
|
||||
"is_downloadable",
|
||||
"url",
|
||||
]
|
||||
|
||||
|
||||
class PodcastSerializer(serializers.ModelSerializer):
|
||||
# serializers.HyperlinkedIdentityField(view_name='sound', format='html')
|
||||
|
||||
class Meta:
|
||||
model = Sound
|
||||
fields = [
|
||||
"pk",
|
||||
"name",
|
||||
"program",
|
||||
"episode",
|
||||
"type",
|
||||
"duration",
|
||||
"mtime",
|
||||
"url",
|
||||
"is_downloadable",
|
||||
]
|
||||
|
||||
File diff suppressed because one or more lines are too long
12
aircox/static/aircox/admin.html
Normal file
12
aircox/static/aircox/admin.html
Normal file
@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<title>Vue App</title>
|
||||
<script defer src="js/chunk-vendors.js"></script><script defer src="js/chunk-common.js"></script><script defer src="js/admin.js"></script><link href="css/chunk-vendors.css" rel="stylesheet"><link href="css/chunk-common.css" rel="stylesheet"><link href="css/admin.css" rel="stylesheet"></head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user