forked from rc/aircox
Compare commits
28 Commits
5ea092dba6
...
995a6c87d9
Author | SHA1 | Date | |
---|---|---|---|
995a6c87d9 | |||
83548e432c | |||
185fb57fd6 | |||
![]() |
df9e3a9f61 | ||
![]() |
378a2fca46 | ||
![]() |
fbeae81abb | ||
![]() |
b40cca8a95 | ||
![]() |
7cccb6182e | ||
![]() |
6b7cfbdadf | ||
![]() |
202f31d169 | ||
![]() |
31e22beaab | ||
![]() |
1344e2dce1 | ||
![]() |
0d5dd43a60 | ||
![]() |
5ed1a3e241 | ||
![]() |
5a074f6b94 | ||
![]() |
607468c052 | ||
![]() |
4b01a5217b | ||
![]() |
53512d9dcf | ||
![]() |
849551d092 | ||
![]() |
a24d5da72d | ||
![]() |
93fba2d46d | ||
55123c386d | |||
![]() |
1e17a1334a | ||
9097bced4a | |||
61e6732b19 | |||
fd4c765dc4 | |||
![]() |
e690953b82 | ||
![]() |
f7a61fe6c0 |
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -5,3 +5,6 @@ venv/
|
||||||
node_modules/
|
node_modules/
|
||||||
*.egg-info/
|
*.egg-info/
|
||||||
*.egg
|
*.egg
|
||||||
|
|
||||||
|
db.sqlite3
|
||||||
|
instance/settings/settings.py
|
||||||
|
|
|
@ -9,14 +9,11 @@ repos:
|
||||||
rev: 23.1.0
|
rev: 23.1.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: black
|
- id: black
|
||||||
args:
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
- --line-length=79
|
rev: v0.0.292
|
||||||
- --exclude="""\.git|\.__pycache__|venv|_build|buck-out|build|dist"""
|
|
||||||
- repo: https://github.com/PyCQA/flake8.git
|
|
||||||
rev: 6.0.0
|
|
||||||
hooks:
|
hooks:
|
||||||
- id: flake8
|
- id: ruff
|
||||||
exclude: ^instance/settings/|migrations/
|
args: [--fix, --exit-non-zero-on-fix]
|
||||||
- repo: https://github.com/PyCQA/docformatter.git
|
- repo: https://github.com/PyCQA/docformatter.git
|
||||||
rev: v1.5.1
|
rev: v1.5.1
|
||||||
hooks:
|
hooks:
|
||||||
|
|
54
README.md
Executable file → Normal file
54
README.md
Executable file → Normal file
|
@ -1,10 +1,9 @@
|
||||||

|

|
||||||
|
|
||||||
Platform to manage a radio, schedules, website, and so on. We use the power of great tools like Django or Liquidsoap.
|
A platform to manage radio schedules, website content, and more. It uses 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.
|
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
|
## Features
|
||||||
* **streams**: multiple random music streams when no program is played. We also can specify a time range and frequency for each;
|
* **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.
|
* **diffusions**: generate diffusions time slot for programs that have schedule informations. Check for conflicts and rerun.
|
||||||
|
@ -15,7 +14,51 @@ This project is distributed under GPL version 3. More information in the LICENSE
|
||||||
* **cms**: content management system.
|
* **cms**: content management system.
|
||||||
|
|
||||||
|
|
||||||
## Scripts
|
## 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
|
||||||
Are included various configuration scripts that can be used to ease setup. They
|
Are included various configuration scripts that can be used to ease setup. They
|
||||||
assume that the project is present in `/srv/apps/aircox`:
|
assume that the project is present in `/srv/apps/aircox`:
|
||||||
|
|
||||||
|
@ -27,7 +70,6 @@ The scripts are written with a combination of `cron`, `supervisord`, `nginx`
|
||||||
and `gunicorn` in mind.
|
and `gunicorn` in mind.
|
||||||
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
### Dependencies
|
### Dependencies
|
||||||
For python dependencies take a peek at the `requirements.txt` file, plus
|
For python dependencies take a peek at the `requirements.txt` file, plus
|
||||||
dependencies specific to Django (e.g. for database: `mysqlclient` for MySql
|
dependencies specific to Django (e.g. for database: `mysqlclient` for MySql
|
||||||
|
@ -62,8 +104,8 @@ pip install -r requirements.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
### Configuration
|
### Configuration
|
||||||
You must write a settings.py file in the `instance` directory (you can just
|
You must write a settings.py file in the `instance/settings` directory (you can just
|
||||||
copy and paste `instance/sample_settings.py`. There still is configuration
|
copy and paste `instance/settings/sample.py`. There still is configuration
|
||||||
required in this file, check it in for more info.
|
required in this file, check it in for more info.
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ from .diffusion import DiffusionAdmin
|
||||||
from .episode import EpisodeAdmin
|
from .episode import EpisodeAdmin
|
||||||
from .log import LogAdmin
|
from .log import LogAdmin
|
||||||
from .page import PageAdmin, StaticPageAdmin
|
from .page import PageAdmin, StaticPageAdmin
|
||||||
from .program import ProgramAdmin, StreamAdmin
|
from .program import ProgramAdmin
|
||||||
from .schedule import ScheduleAdmin
|
from .schedule import ScheduleAdmin
|
||||||
from .sound import SoundAdmin, TrackAdmin
|
from .sound import SoundAdmin, TrackAdmin
|
||||||
from .station import StationAdmin
|
from .station import StationAdmin
|
||||||
|
@ -19,7 +19,6 @@ __all__ = (
|
||||||
"StaticPageAdmin",
|
"StaticPageAdmin",
|
||||||
"ProgramAdmin",
|
"ProgramAdmin",
|
||||||
"ScheduleAdmin",
|
"ScheduleAdmin",
|
||||||
"StreamAdmin",
|
|
||||||
"SoundAdmin",
|
"SoundAdmin",
|
||||||
"TrackAdmin",
|
"TrackAdmin",
|
||||||
"StationAdmin",
|
"StationAdmin",
|
||||||
|
|
|
@ -30,12 +30,14 @@ class DiffusionAdmin(DiffusionBaseAdmin, admin.ModelAdmin):
|
||||||
|
|
||||||
end_date.short_description = _("end")
|
end_date.short_description = _("end")
|
||||||
|
|
||||||
list_display = ("episode", "start_date", "end_date", "type", "initial")
|
list_display = ("episode", "start", "end", "type", "initial")
|
||||||
list_filter = ("type", "start", "program")
|
list_filter = ("type", "start", "program")
|
||||||
list_editable = ("type",)
|
list_editable = ("type", "start", "end")
|
||||||
ordering = ("-start", "id")
|
ordering = ("-start", "id")
|
||||||
|
search_fields = ("program__title", "episode__title")
|
||||||
|
|
||||||
fields = ("type", "start", "end", "initial", "program", "schedule")
|
fields = ("type", "start", "end", "initial", "program", "schedule")
|
||||||
|
autocomplete_fields = ("episode", "program", "initial")
|
||||||
readonly_fields = ("schedule",)
|
readonly_fields = ("schedule",)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -2,12 +2,23 @@ from adminsortable2.admin import SortableAdminBase
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.forms import ModelForm
|
from django.forms import ModelForm
|
||||||
|
|
||||||
from aircox.models import Episode
|
from aircox.models import Episode, EpisodeSound
|
||||||
from .page import PageAdmin
|
from .page import ChildPageAdmin
|
||||||
from .sound import SoundInline, TrackInline
|
from .sound import TrackInline
|
||||||
from .diffusion import DiffusionInline
|
from .diffusion import DiffusionInline
|
||||||
|
|
||||||
|
|
||||||
|
class EpisodeSoundInline(admin.TabularInline):
|
||||||
|
model = EpisodeSound
|
||||||
|
extra = 0
|
||||||
|
fields = (
|
||||||
|
"sound",
|
||||||
|
"position",
|
||||||
|
"broadcast",
|
||||||
|
)
|
||||||
|
autocomplete_fields = ("sound",)
|
||||||
|
|
||||||
|
|
||||||
class EpisodeAdminForm(ModelForm):
|
class EpisodeAdminForm(ModelForm):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
@ -15,28 +26,14 @@ class EpisodeAdminForm(ModelForm):
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Episode)
|
@admin.register(Episode)
|
||||||
class EpisodeAdmin(SortableAdminBase, PageAdmin):
|
class EpisodeAdmin(SortableAdminBase, ChildPageAdmin):
|
||||||
form = EpisodeAdminForm
|
form = EpisodeAdminForm
|
||||||
list_display = PageAdmin.list_display
|
list_display = ChildPageAdmin.list_display
|
||||||
list_filter = tuple(
|
list_filter = tuple(f for f in ChildPageAdmin.list_filter if f != "pub_date") + (
|
||||||
f for f in PageAdmin.list_filter if f != "pub_date"
|
|
||||||
) + (
|
|
||||||
"diffusion__start",
|
"diffusion__start",
|
||||||
"pub_date",
|
"pub_date",
|
||||||
)
|
)
|
||||||
search_fields = PageAdmin.search_fields + ("parent__title",)
|
search_fields = ChildPageAdmin.search_fields + ("parent__title",)
|
||||||
# readonly_fields = ('parent',)
|
# readonly_fields = ('parent',)
|
||||||
|
|
||||||
inlines = [TrackInline, SoundInline, DiffusionInline]
|
inlines = (TrackInline, EpisodeSoundInline, 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)
|
|
||||||
|
|
|
@ -14,13 +14,9 @@ class DateFieldFilter(filters.FieldListFilter):
|
||||||
|
|
||||||
def __init__(self, field, request, params, model, model_admin, field_path):
|
def __init__(self, field, request, params, model, model_admin, field_path):
|
||||||
self.field_generic = f"{field_path}__"
|
self.field_generic = f"{field_path}__"
|
||||||
self.date_params = {
|
self.date_params = {k: v for k, v in params.items() if k.startswith(self.field_generic)}
|
||||||
k: v for k, v in params.items() if k.startswith(self.field_generic)
|
|
||||||
}
|
|
||||||
|
|
||||||
exact_lookup = (
|
exact_lookup = "date" if isinstance(field, models.DateTimeField) else "exact"
|
||||||
"date" if isinstance(field, models.DateTimeField) else "exact"
|
|
||||||
)
|
|
||||||
|
|
||||||
# links as: (label, param, input_type|None, value)
|
# links as: (label, param, input_type|None, value)
|
||||||
self.links = [
|
self.links = [
|
||||||
|
@ -29,17 +25,11 @@ class DateFieldFilter(filters.FieldListFilter):
|
||||||
(_("Until"), self.field_generic + "lte", self.input_type),
|
(_("Until"), self.field_generic + "lte", self.input_type),
|
||||||
]
|
]
|
||||||
if field.null:
|
if field.null:
|
||||||
self.links.insert(
|
self.links.insert(0, (_("None"), self.field_generic + "isnull", None, "1"))
|
||||||
0, (_("None"), self.field_generic + "isnull", None, "1")
|
|
||||||
)
|
|
||||||
|
|
||||||
self.query_attrs = {
|
self.query_attrs = {k: v for k, v in request.GET.items() if k not in self.date_params}
|
||||||
k: v for k, v in request.GET.items() if k not in self.date_params
|
|
||||||
}
|
|
||||||
self.query_string = urlencode(self.query_attrs)
|
self.query_string = urlencode(self.query_attrs)
|
||||||
super().__init__(
|
super().__init__(field, request, params, model, model_admin, field_path)
|
||||||
field, request, params, model, model_admin, field_path
|
|
||||||
)
|
|
||||||
|
|
||||||
def expected_parameters(self):
|
def expected_parameters(self):
|
||||||
return [link[1] for link in self.links]
|
return [link[1] for link in self.links]
|
||||||
|
@ -59,11 +49,7 @@ class DateFieldFilter(filters.FieldListFilter):
|
||||||
"value": value,
|
"value": value,
|
||||||
"type": link[2],
|
"type": link[2],
|
||||||
"query_attrs": self.query_attrs,
|
"query_attrs": self.query_attrs,
|
||||||
"query_string": urlencode({link[1]: value})
|
"query_string": urlencode({link[1]: value}) + "&" + self.query_string if value else self.query_string,
|
||||||
+ "&"
|
|
||||||
+ self.query_string
|
|
||||||
if value
|
|
||||||
else self.query_string,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -18,10 +18,11 @@ class CategoryAdmin(admin.ModelAdmin):
|
||||||
search_fields = ["title"]
|
search_fields = ["title"]
|
||||||
fields = ["title", "slug"]
|
fields = ["title", "slug"]
|
||||||
prepopulated_fields = {"slug": ("title",)}
|
prepopulated_fields = {"slug": ("title",)}
|
||||||
|
ordering = ("title",)
|
||||||
|
|
||||||
|
|
||||||
class BasePageAdmin(admin.ModelAdmin):
|
class BasePageAdmin(admin.ModelAdmin):
|
||||||
list_display = ("cover_thumb", "title", "status", "parent")
|
list_display = ("cover_thumb", "title", "status")
|
||||||
list_display_links = ("cover_thumb", "title")
|
list_display_links = ("cover_thumb", "title")
|
||||||
list_editable = ("status",)
|
list_editable = ("status",)
|
||||||
list_filter = ("status",)
|
list_filter = ("status",)
|
||||||
|
@ -42,19 +43,49 @@ class BasePageAdmin(admin.ModelAdmin):
|
||||||
(
|
(
|
||||||
_("Publication Settings"),
|
_("Publication Settings"),
|
||||||
{
|
{
|
||||||
"fields": ["status", "parent"],
|
"fields": [
|
||||||
|
"status",
|
||||||
|
],
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
change_form_template = "admin/aircox/page_change_form.html"
|
|
||||||
|
|
||||||
def cover_thumb(self, obj):
|
def cover_thumb(self, obj):
|
||||||
return (
|
if obj.cover and obj.cover.thumbnails:
|
||||||
mark_safe('<img src="{}"/>'.format(obj.cover.icons["64"]))
|
return mark_safe('<img src="{}"/>'.format(obj.cover.icons["64"]))
|
||||||
if obj.cover
|
return ""
|
||||||
else ""
|
|
||||||
)
|
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",)
|
||||||
|
|
||||||
def get_changeform_initial_data(self, request):
|
def get_changeform_initial_data(self, request):
|
||||||
data = super().get_changeform_initial_data(request)
|
data = super().get_changeform_initial_data(request)
|
||||||
|
@ -62,49 +93,22 @@ class BasePageAdmin(admin.ModelAdmin):
|
||||||
data["parent"] = filters.get("parent", None)
|
data["parent"] = filters.get("parent", None)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def _get_common_context(self, query, extra_context=None):
|
def _get_extra_context(self, query, **extra_context):
|
||||||
extra_context = extra_context or {}
|
|
||||||
parent = query.get("parent", None)
|
parent = query.get("parent", None)
|
||||||
extra_context["parent"] = (
|
extra_context["parent"] = None if parent is None else Page.objects.get_subclass(id=parent)
|
||||||
None if parent is None else Page.objects.get_subclass(id=parent)
|
return super()._get_extra_context(query, **extra_context)
|
||||||
)
|
|
||||||
return extra_context
|
|
||||||
|
|
||||||
def render_change_form(self, request, context, *args, **kwargs):
|
def render_change_form(self, request, context, *args, **kwargs):
|
||||||
if context["original"] and "parent" not in context:
|
if context["original"] and "parent" not in context:
|
||||||
context["parent"] = context["original"].parent
|
context["parent"] = context["original"].parent
|
||||||
return super().render_change_form(request, context, *args, **kwargs)
|
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)
|
@admin.register(StaticPage)
|
||||||
class StaticPageAdmin(BasePageAdmin):
|
class StaticPageAdmin(BasePageAdmin):
|
||||||
list_display = BasePageAdmin.list_display + ("attach_to",)
|
list_display = BasePageAdmin.list_display + ("attach_to",)
|
||||||
|
list_editable = BasePageAdmin.list_editable + ("attach_to",)
|
||||||
fieldsets = deepcopy(BasePageAdmin.fieldsets)
|
fieldsets = deepcopy(BasePageAdmin.fieldsets)
|
||||||
|
|
||||||
fieldsets[1][1]["fields"] += ("attach_to",)
|
fieldsets[1][1]["fields"] += ("attach_to",)
|
||||||
|
|
||||||
|
|
||||||
|
@ -113,6 +117,7 @@ class CommentAdmin(admin.ModelAdmin):
|
||||||
list_display = ("page_title", "date", "nickname")
|
list_display = ("page_title", "date", "nickname")
|
||||||
list_filter = ("date",)
|
list_filter = ("date",)
|
||||||
search_fields = ("page__title", "nickname")
|
search_fields = ("page__title", "nickname")
|
||||||
|
readonly_fields = ("page",)
|
||||||
|
|
||||||
def page_title(self, obj):
|
def page_title(self, obj):
|
||||||
return obj.page.title
|
return obj.page.title
|
||||||
|
|
|
@ -6,7 +6,10 @@ from .page import PageAdmin
|
||||||
from .schedule import ScheduleInline
|
from .schedule import ScheduleInline
|
||||||
|
|
||||||
|
|
||||||
__all__ = ("ProgramAdmin", "StreamInline", "StreamAdmin")
|
__all__ = (
|
||||||
|
"ProgramAdmin",
|
||||||
|
"StreamInline",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class StreamInline(admin.TabularInline):
|
class StreamInline(admin.TabularInline):
|
||||||
|
@ -27,6 +30,7 @@ class ProgramAdmin(PageAdmin):
|
||||||
list_filter = PageAdmin.list_filter + ("station", "active")
|
list_filter = PageAdmin.list_filter + ("station", "active")
|
||||||
prepopulated_fields = {"slug": ("title",)}
|
prepopulated_fields = {"slug": ("title",)}
|
||||||
search_fields = ("title",)
|
search_fields = ("title",)
|
||||||
|
ordering = ("title",)
|
||||||
|
|
||||||
inlines = [ScheduleInline, StreamInline]
|
inlines = [ScheduleInline, StreamInline]
|
||||||
|
|
||||||
|
@ -42,8 +46,3 @@ class ProgramAdmin(PageAdmin):
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
return fields
|
return fields
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Stream)
|
|
||||||
class StreamAdmin(admin.ModelAdmin):
|
|
||||||
list_display = ("id", "program", "delay", "begin", "end")
|
|
||||||
|
|
|
@ -22,6 +22,7 @@ class ScheduleInline(admin.TabularInline):
|
||||||
model = Schedule
|
model = Schedule
|
||||||
form = ScheduleInlineForm
|
form = ScheduleInlineForm
|
||||||
readonly_fields = ("timezone",)
|
readonly_fields = ("timezone",)
|
||||||
|
autocomplete_fields = ("initial",)
|
||||||
extra = 1
|
extra = 1
|
||||||
|
|
||||||
|
|
||||||
|
@ -46,7 +47,10 @@ class ScheduleAdmin(admin.ModelAdmin):
|
||||||
"duration",
|
"duration",
|
||||||
"initial",
|
"initial",
|
||||||
]
|
]
|
||||||
list_editable = ["time", "duration", "initial"]
|
list_editable = ("time", "duration", "initial")
|
||||||
|
autocomplete_fields = ("initial",)
|
||||||
|
search_fields = ("program__title",)
|
||||||
|
ordering = ("program__title", "initial", "date")
|
||||||
|
|
||||||
def get_readonly_fields(self, request, obj=None):
|
def get_readonly_fields(self, request, obj=None):
|
||||||
if obj:
|
if obj:
|
||||||
|
|
|
@ -9,7 +9,6 @@ from ..models import Sound, Track
|
||||||
|
|
||||||
|
|
||||||
class TrackInline(admin.TabularInline):
|
class TrackInline(admin.TabularInline):
|
||||||
template = "admin/aircox/playlist_inline.html"
|
|
||||||
model = Track
|
model = Track
|
||||||
extra = 0
|
extra = 0
|
||||||
fields = ("position", "artist", "title", "tags", "album", "year", "info")
|
fields = ("position", "artist", "title", "tags", "album", "year", "info")
|
||||||
|
@ -25,28 +24,24 @@ class SoundTrackInline(TrackInline):
|
||||||
class SoundInline(admin.TabularInline):
|
class SoundInline(admin.TabularInline):
|
||||||
model = Sound
|
model = Sound
|
||||||
fields = [
|
fields = [
|
||||||
"type",
|
|
||||||
"name",
|
"name",
|
||||||
"audio",
|
"audio",
|
||||||
"duration",
|
"duration",
|
||||||
|
"broadcast",
|
||||||
"is_good_quality",
|
"is_good_quality",
|
||||||
"is_public",
|
"is_public",
|
||||||
"is_downloadable",
|
"is_downloadable",
|
||||||
|
"is_removed",
|
||||||
]
|
]
|
||||||
readonly_fields = ["type", "audio", "duration", "is_good_quality"]
|
readonly_fields = ["broadcast", "audio", "duration", "is_good_quality"]
|
||||||
extra = 0
|
extra = 0
|
||||||
max_num = 0
|
max_num = 0
|
||||||
|
|
||||||
def audio(self, obj):
|
def audio(self, obj):
|
||||||
return mark_safe(
|
return mark_safe('<audio src="{}" controls></audio>'.format(obj.file.url))
|
||||||
'<audio src="{}" controls></audio>'.format(obj.file.url)
|
|
||||||
)
|
|
||||||
|
|
||||||
audio.short_description = _("Audio")
|
audio.short_description = _("Audio")
|
||||||
|
|
||||||
def get_queryset(self, request):
|
|
||||||
return super().get_queryset(request).available()
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Sound)
|
@admin.register(Sound)
|
||||||
class SoundAdmin(SortableAdminBase, admin.ModelAdmin):
|
class SoundAdmin(SortableAdminBase, admin.ModelAdmin):
|
||||||
|
@ -54,20 +49,31 @@ class SoundAdmin(SortableAdminBase, admin.ModelAdmin):
|
||||||
list_display = [
|
list_display = [
|
||||||
"id",
|
"id",
|
||||||
"name",
|
"name",
|
||||||
"related",
|
# "related",
|
||||||
"type",
|
"broadcast",
|
||||||
"duration",
|
"duration",
|
||||||
"is_public",
|
"is_public",
|
||||||
"is_good_quality",
|
"is_good_quality",
|
||||||
"is_downloadable",
|
"is_downloadable",
|
||||||
"audio",
|
"audio",
|
||||||
]
|
]
|
||||||
list_filter = ("type", "is_good_quality", "is_public")
|
list_filter = ("broadcast", "is_good_quality", "is_public")
|
||||||
list_editable = ["name", "is_public", "is_downloadable"]
|
list_editable = ["name", "is_public", "is_downloadable"]
|
||||||
|
|
||||||
search_fields = ["name", "program__title"]
|
search_fields = ["name", "program__title", "file"]
|
||||||
|
autocomplete_fields = ("program",)
|
||||||
fieldsets = [
|
fieldsets = [
|
||||||
(None, {"fields": ["name", "file", "type", "program", "episode"]}),
|
(
|
||||||
|
None,
|
||||||
|
{
|
||||||
|
"fields": [
|
||||||
|
"name",
|
||||||
|
"file",
|
||||||
|
"broadcast",
|
||||||
|
"program",
|
||||||
|
]
|
||||||
|
},
|
||||||
|
),
|
||||||
(
|
(
|
||||||
None,
|
None,
|
||||||
{
|
{
|
||||||
|
@ -81,27 +87,19 @@ class SoundAdmin(SortableAdminBase, admin.ModelAdmin):
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
readonly_fields = ("file", "duration", "type")
|
readonly_fields = ("file", "duration", "is_removed")
|
||||||
inlines = [SoundTrackInline]
|
inlines = [SoundTrackInline]
|
||||||
|
|
||||||
def related(self, obj):
|
def related(self, obj):
|
||||||
# TODO: link to episode or program edit
|
# # TODO: link to episode or program edit
|
||||||
return (
|
return obj.program.title if obj.program else ""
|
||||||
obj.episode.title
|
|
||||||
if obj.episode
|
|
||||||
else obj.program.title
|
|
||||||
if obj.program
|
|
||||||
else ""
|
|
||||||
)
|
|
||||||
|
|
||||||
related.short_description = _("Program / Episode")
|
# return obj.episode.title if obj.episode else obj.program.title if obj.program else ""
|
||||||
|
|
||||||
|
related.short_description = _("Program")
|
||||||
|
|
||||||
def audio(self, obj):
|
def audio(self, obj):
|
||||||
return (
|
return mark_safe('<audio src="{}" controls></audio>'.format(obj.file.url)) if not obj.is_removed else ""
|
||||||
mark_safe('<audio src="{}" controls></audio>'.format(obj.file.url))
|
|
||||||
if obj.type != Sound.TYPE_REMOVED
|
|
||||||
else ""
|
|
||||||
)
|
|
||||||
|
|
||||||
audio.short_description = _("Audio")
|
audio.short_description = _("Audio")
|
||||||
|
|
||||||
|
|
|
@ -1,77 +0,0 @@
|
||||||
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 . import models
|
|
||||||
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": models.Program.objects.active()
|
|
||||||
.values("pk", "title")
|
|
||||||
.order_by("title"),
|
|
||||||
# today's diffusions
|
|
||||||
"diffusions": models.Diffusion.objects.date()
|
|
||||||
.order_by("start")
|
|
||||||
.select_related("episode"),
|
|
||||||
# TODO: only for dashboard
|
|
||||||
# last comments
|
|
||||||
"comments": models.Comment.objects.order_by(
|
|
||||||
"-date"
|
|
||||||
).select_related("page")[0:10],
|
|
||||||
"latests": models.Page.objects.select_subclasses().order_by(
|
|
||||||
"-pub_date"
|
|
||||||
)[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,11 +1,6 @@
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
from django.contrib.admin.apps import AdminConfig
|
|
||||||
|
|
||||||
|
|
||||||
class AircoxConfig(AppConfig):
|
class AircoxConfig(AppConfig):
|
||||||
name = "aircox"
|
name = "aircox"
|
||||||
verbose_name = "Aircox"
|
verbose_name = "Aircox"
|
||||||
|
|
||||||
|
|
||||||
class AircoxAdminConfig(AdminConfig):
|
|
||||||
default_site = "aircox.admin_site.AdminSite"
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ import os
|
||||||
|
|
||||||
import inspect
|
import inspect
|
||||||
|
|
||||||
|
from bleach import sanitizer
|
||||||
from django.conf import settings as d_settings
|
from django.conf import settings as d_settings
|
||||||
|
|
||||||
|
|
||||||
|
@ -86,8 +87,8 @@ class Settings(BaseSettings):
|
||||||
# TODO include content_type in order to avoid clash with potential
|
# TODO include content_type in order to avoid clash with potential
|
||||||
# extra applications
|
# extra applications
|
||||||
# aircox
|
# aircox
|
||||||
"change_program",
|
"view_program",
|
||||||
"change_episode",
|
"view_episode",
|
||||||
"change_diffusion",
|
"change_diffusion",
|
||||||
"add_comment",
|
"add_comment",
|
||||||
"change_comment",
|
"change_comment",
|
||||||
|
@ -140,7 +141,7 @@ class Settings(BaseSettings):
|
||||||
"""In days, minimal age of a log before it is archived."""
|
"""In days, minimal age of a log before it is archived."""
|
||||||
|
|
||||||
# --- Sounds
|
# --- Sounds
|
||||||
SOUND_ARCHIVES_SUBDIR = "archives"
|
SOUND_BROADCASTS_SUBDIR = "archives"
|
||||||
"""Sub directory used for the complete episode sounds."""
|
"""Sub directory used for the complete episode sounds."""
|
||||||
SOUND_EXCERPTS_SUBDIR = "excerpts"
|
SOUND_EXCERPTS_SUBDIR = "excerpts"
|
||||||
"""Sub directory used for the excerpts of the episode."""
|
"""Sub directory used for the excerpts of the episode."""
|
||||||
|
@ -176,5 +177,13 @@ class Settings(BaseSettings):
|
||||||
IMPORT_PLAYLIST_CSV_TEXT_QUOTE = '"'
|
IMPORT_PLAYLIST_CSV_TEXT_QUOTE = '"'
|
||||||
"""Text delimiter of csv text files."""
|
"""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")
|
settings = Settings("AIRCOX")
|
||||||
|
|
4
aircox/context_processors/__init__.py
Normal file
4
aircox/context_processors/__init__.py
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
def station(request):
|
||||||
|
station = request.station
|
||||||
|
audio_streams = station.streams if station else None
|
||||||
|
return {"station": station, "audio_streams": audio_streams}
|
|
@ -22,9 +22,7 @@ class DiffusionMonitor:
|
||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
episodes, diffusions = [], []
|
episodes, diffusions = [], []
|
||||||
for schedule in Schedule.objects.filter(
|
for schedule in Schedule.objects.filter(program__active=True, initial__isnull=True):
|
||||||
program__active=True, initial__isnull=True
|
|
||||||
):
|
|
||||||
eps, diffs = schedule.diffusions_of_month(self.date)
|
eps, diffs = schedule.diffusions_of_month(self.date)
|
||||||
if eps:
|
if eps:
|
||||||
episodes += eps
|
episodes += eps
|
||||||
|
|
|
@ -44,9 +44,7 @@ class LogArchiver:
|
||||||
path = self.get_path(station, date)
|
path = self.get_path(station, date)
|
||||||
# FIXME: remove binary mode
|
# FIXME: remove binary mode
|
||||||
with gzip.open(path, "ab") as archive:
|
with gzip.open(path, "ab") as archive:
|
||||||
data = yaml.dump(
|
data = yaml.dump([self.serialize(line) for line in logs]).encode("utf8")
|
||||||
[self.serialize(line) for line in logs]
|
|
||||||
).encode("utf8")
|
|
||||||
archive.write(data)
|
archive.write(data)
|
||||||
|
|
||||||
if not keep:
|
if not keep:
|
||||||
|
@ -95,10 +93,7 @@ class LogArchiver:
|
||||||
|
|
||||||
return [
|
return [
|
||||||
Log(
|
Log(
|
||||||
diffusion=rel_obj(log, "diffusion"),
|
diffusion=rel_obj(log, "diffusion"), sound=rel_obj(log, "sound"), track=rel_obj(log, "track"), **log
|
||||||
sound=rel_obj(log, "sound"),
|
|
||||||
track=rel_obj(log, "track"),
|
|
||||||
**log
|
|
||||||
)
|
)
|
||||||
for log in logs
|
for log in logs
|
||||||
]
|
]
|
||||||
|
|
|
@ -50,14 +50,7 @@ class PlaylistImport:
|
||||||
logger.info("start reading csv " + self.path)
|
logger.info("start reading csv " + self.path)
|
||||||
self.data = list(
|
self.data = list(
|
||||||
csv.DictReader(
|
csv.DictReader(
|
||||||
(
|
(row for row in file if not (row.startswith("#") or row.startswith("\ufeff#")) and row.strip()),
|
||||||
row
|
|
||||||
for row in file
|
|
||||||
if not (
|
|
||||||
row.startswith("#") or row.startswith("\ufeff#")
|
|
||||||
)
|
|
||||||
and row.strip()
|
|
||||||
),
|
|
||||||
fieldnames=settings.IMPORT_PLAYLIST_CSV_COLS,
|
fieldnames=settings.IMPORT_PLAYLIST_CSV_COLS,
|
||||||
delimiter=settings.IMPORT_PLAYLIST_CSV_DELIMITER,
|
delimiter=settings.IMPORT_PLAYLIST_CSV_DELIMITER,
|
||||||
quotechar=settings.IMPORT_PLAYLIST_CSV_TEXT_QUOTE,
|
quotechar=settings.IMPORT_PLAYLIST_CSV_TEXT_QUOTE,
|
||||||
|
@ -70,11 +63,7 @@ class PlaylistImport:
|
||||||
If save is true, save it into the database
|
If save is true, save it into the database
|
||||||
"""
|
"""
|
||||||
if self.track_kwargs.get("sound") is None:
|
if self.track_kwargs.get("sound") is None:
|
||||||
logger.error(
|
logger.error("related track's sound is missing. Skip import of " + self.path + ".")
|
||||||
"related track's sound is missing. Skip import of "
|
|
||||||
+ self.path
|
|
||||||
+ "."
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
maps = settings.IMPORT_PLAYLIST_CSV_COLS
|
maps = settings.IMPORT_PLAYLIST_CSV_COLS
|
||||||
|
@ -87,17 +76,11 @@ class PlaylistImport:
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
timestamp = (
|
timestamp = (
|
||||||
int(line.get("minutes") or 0) * 60
|
int(line.get("minutes") or 0) * 60 + int(line.get("seconds") or 0) if has_timestamp else None
|
||||||
+ int(line.get("seconds") or 0)
|
|
||||||
if has_timestamp
|
|
||||||
else None
|
|
||||||
)
|
)
|
||||||
|
|
||||||
track, created = Track.objects.get_or_create(
|
track, created = Track.objects.get_or_create(
|
||||||
title=line.get("title"),
|
title=line.get("title"), artist=line.get("artist"), position=index, **self.track_kwargs
|
||||||
artist=line.get("artist"),
|
|
||||||
position=index,
|
|
||||||
**self.track_kwargs
|
|
||||||
)
|
)
|
||||||
track.timestamp = timestamp
|
track.timestamp = timestamp
|
||||||
track.info = line.get("info")
|
track.info = line.get("info")
|
||||||
|
|
|
@ -21,23 +21,18 @@ parameters given by the setting SOUND_QUALITY. This script requires
|
||||||
Sox (and soxi).
|
Sox (and soxi).
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
import os
|
|
||||||
import re
|
|
||||||
from datetime import date
|
|
||||||
|
|
||||||
import mutagen
|
|
||||||
from django.conf import settings as conf
|
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, EpisodeSound
|
||||||
from aircox.models import Program, Sound, Track
|
|
||||||
|
|
||||||
from .playlist_import import PlaylistImport
|
|
||||||
|
|
||||||
logger = logging.getLogger("aircox.commands")
|
logger = logging.getLogger("aircox.commands")
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ("SoundFile",)
|
||||||
|
|
||||||
|
|
||||||
class SoundFile:
|
class SoundFile:
|
||||||
"""Handle synchronisation between sounds on files and database."""
|
"""Handle synchronisation between sounds on files and database."""
|
||||||
|
|
||||||
|
@ -58,179 +53,43 @@ class SoundFile:
|
||||||
def episode(self):
|
def episode(self):
|
||||||
return self.sound and self.sound.episode
|
return self.sound and self.sound.episode
|
||||||
|
|
||||||
def sync(
|
def sync(self, sound=None, program=None, deleted=False, keep_deleted=False, **kwargs):
|
||||||
self,
|
|
||||||
sound=None,
|
|
||||||
program=None,
|
|
||||||
deleted=False,
|
|
||||||
keep_deleted=False,
|
|
||||||
**kwargs
|
|
||||||
):
|
|
||||||
"""Update related sound model and save it."""
|
"""Update related sound model and save it."""
|
||||||
if deleted:
|
if deleted:
|
||||||
return self._on_delete(self.path, keep_deleted)
|
self.sound = self._on_delete(self.path, keep_deleted)
|
||||||
|
return self.sound
|
||||||
|
|
||||||
# FIXME: sound.program as not null
|
program = sound and sound.program or Program.get_from_path(self.path)
|
||||||
if not program:
|
if program:
|
||||||
program = Program.get_from_path(self.path)
|
kwargs["program_id"] = program.pk
|
||||||
logger.debug('program from path "%s" -> %s', self.path, program)
|
|
||||||
kwargs["program_id"] = program.pk
|
|
||||||
|
|
||||||
if sound:
|
created = False
|
||||||
created = False
|
if not sound:
|
||||||
else:
|
sound, created = Sound.objects.get_or_create(file=self.sound_path, defaults=kwargs)
|
||||||
sound, created = Sound.objects.get_or_create(
|
|
||||||
file=self.sound_path, defaults=kwargs
|
|
||||||
)
|
|
||||||
|
|
||||||
self.sound = sound
|
self.sound = sound
|
||||||
self.path_info = self.read_path(self.path)
|
sound.sync_fs(on_update=True, find_playlist=True)
|
||||||
|
|
||||||
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()
|
sound.save()
|
||||||
|
|
||||||
# check for playlist
|
if not sound.episodesound_set.all().exists():
|
||||||
self.find_playlist(sound)
|
self.create_episode_sound(sound)
|
||||||
return sound
|
return sound
|
||||||
|
|
||||||
def _on_delete(self, path, keep_deleted):
|
def create_episode_sound(self, sound):
|
||||||
# TODO: remove from db on delete
|
episode = sound.find_episode()
|
||||||
if keep_deleted:
|
if episode:
|
||||||
sound = Sound.objects.path(self.path).first()
|
# FIXME: position from name
|
||||||
if sound:
|
item = EpisodeSound(
|
||||||
if keep_deleted:
|
episode=episode, sound=sound, position=episode.episodesound_set.all().count(), broadcast=sound.broadcast
|
||||||
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)
|
item.save()
|
||||||
else:
|
|
||||||
at = date(year, month, day)
|
|
||||||
|
|
||||||
diffusion = program.diffusion_set.at(at).first()
|
def _on_delete(self, path, keep_deleted):
|
||||||
if not diffusion:
|
sound = None
|
||||||
return None
|
if keep_deleted:
|
||||||
|
if sound := Sound.objects.path(self.path).first():
|
||||||
logger.debug("%s <--> %s", sound.file.name, str(diffusion.episode))
|
sound.is_removed = True
|
||||||
return diffusion.episode
|
sound.save(sync=False)
|
||||||
|
elif sound := Sound.objects.path(self.path):
|
||||||
def find_playlist(self, sound=None, use_meta=True):
|
sound.delete()
|
||||||
"""Find a playlist file corresponding to the sound path, such as:
|
return sound
|
||||||
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()
|
|
||||||
|
|
|
@ -105,8 +105,7 @@ class MoveTask(Task):
|
||||||
def __call__(self, event, **kw):
|
def __call__(self, event, **kw):
|
||||||
sound = Sound.objects.filter(file=event.src_path).first()
|
sound = Sound.objects.filter(file=event.src_path).first()
|
||||||
if sound:
|
if sound:
|
||||||
kw["sound"] = sound
|
kw = {**kw, "sound": sound, "path": event.src_path}
|
||||||
kw["path"] = event.src_path
|
|
||||||
else:
|
else:
|
||||||
kw["path"] = event.dest_path
|
kw["path"] = event.dest_path
|
||||||
return super().__call__(event, **kw)
|
return super().__call__(event, **kw)
|
||||||
|
@ -155,10 +154,7 @@ class MonitorHandler(PatternMatchingEventHandler):
|
||||||
self.jobs = jobs or {}
|
self.jobs = jobs or {}
|
||||||
self.sync_kw = sync_kw
|
self.sync_kw = sync_kw
|
||||||
|
|
||||||
patterns = [
|
patterns = ["*/{}/*{}".format(self.subdir, ext) for ext in settings.SOUND_FILE_EXT]
|
||||||
"*/{}/*{}".format(self.subdir, ext)
|
|
||||||
for ext in settings.SOUND_FILE_EXT
|
|
||||||
]
|
|
||||||
super().__init__(patterns=patterns, ignore_directories=True)
|
super().__init__(patterns=patterns, ignore_directories=True)
|
||||||
|
|
||||||
def on_created(self, event):
|
def on_created(self, event):
|
||||||
|
@ -202,11 +198,7 @@ class SoundMonitor:
|
||||||
|
|
||||||
def report(self, program=None, component=None, *content, logger=logging):
|
def report(self, program=None, component=None, *content, logger=logging):
|
||||||
content = " ".join([str(c) for c in content])
|
content = " ".join([str(c) for c in content])
|
||||||
logger.info(
|
logger.info(f"{program}: {content}" if not component else f"{program}, {component}: {content}")
|
||||||
f"{program}: {content}"
|
|
||||||
if not component
|
|
||||||
else f"{program}, {component}: {content}"
|
|
||||||
)
|
|
||||||
|
|
||||||
def scan(self, logger=logging):
|
def scan(self, logger=logging):
|
||||||
"""For all programs, scan dirs.
|
"""For all programs, scan dirs.
|
||||||
|
@ -221,34 +213,32 @@ class SoundMonitor:
|
||||||
logger.info(f"#{program.id} {program.title}")
|
logger.info(f"#{program.id} {program.title}")
|
||||||
self.scan_for_program(
|
self.scan_for_program(
|
||||||
program,
|
program,
|
||||||
settings.SOUND_ARCHIVES_SUBDIR,
|
settings.SOUND_BROADCASTS_SUBDIR,
|
||||||
logger=logger,
|
logger=logger,
|
||||||
type=Sound.TYPE_ARCHIVE,
|
broadcast=True,
|
||||||
)
|
)
|
||||||
self.scan_for_program(
|
self.scan_for_program(
|
||||||
program,
|
program,
|
||||||
settings.SOUND_EXCERPTS_SUBDIR,
|
settings.SOUND_EXCERPTS_SUBDIR,
|
||||||
logger=logger,
|
logger=logger,
|
||||||
type=Sound.TYPE_EXCERPT,
|
broadcast=False,
|
||||||
)
|
)
|
||||||
dirs.append(program.abspath)
|
dirs.append(program.abspath)
|
||||||
return dirs
|
return dirs
|
||||||
|
|
||||||
def scan_for_program(
|
def scan_for_program(self, program, subdir, logger=logging, **sound_kwargs):
|
||||||
self, program, subdir, logger=logging, **sound_kwargs
|
|
||||||
):
|
|
||||||
"""Scan a given directory that is associated to the given program, and
|
"""Scan a given directory that is associated to the given program, and
|
||||||
update sounds information."""
|
update sounds information."""
|
||||||
logger.info("- %s/", subdir)
|
logger.info("- %s/", subdir)
|
||||||
if not program.ensure_dir(subdir):
|
if not program.ensure_dir(subdir):
|
||||||
return
|
return
|
||||||
|
|
||||||
subdir = os.path.join(program.abspath, subdir)
|
abs_subdir = os.path.join(program.abspath, subdir)
|
||||||
sounds = []
|
sounds = []
|
||||||
|
|
||||||
# sounds in directory
|
# sounds in directory
|
||||||
for path in os.listdir(subdir):
|
for path in os.listdir(abs_subdir):
|
||||||
path = os.path.join(subdir, path)
|
path = os.path.join(abs_subdir, path)
|
||||||
if not path.endswith(settings.SOUND_FILE_EXT):
|
if not path.endswith(settings.SOUND_FILE_EXT):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
@ -257,16 +247,14 @@ class SoundMonitor:
|
||||||
sounds.append(sound_file.sound.pk)
|
sounds.append(sound_file.sound.pk)
|
||||||
|
|
||||||
# sounds in db & unchecked
|
# sounds in db & unchecked
|
||||||
sounds = Sound.objects.filter(file__startswith=subdir).exclude(
|
sounds = Sound.objects.filter(file__startswith=program.path).exclude(pk__in=sounds)
|
||||||
pk__in=sounds
|
|
||||||
)
|
|
||||||
self.check_sounds(sounds, program=program)
|
self.check_sounds(sounds, program=program)
|
||||||
|
|
||||||
def check_sounds(self, qs, **sync_kwargs):
|
def check_sounds(self, qs, **sync_kwargs):
|
||||||
"""Only check for the sound existence or update."""
|
"""Only check for the sound existence or update."""
|
||||||
# check files
|
# check files
|
||||||
for sound in qs:
|
for sound in qs:
|
||||||
if sound.check_on_file():
|
if sound.sync_fs(on_update=True):
|
||||||
SoundFile(sound.file.path).sync(sound=sound, **sync_kwargs)
|
SoundFile(sound.file.path).sync(sound=sound, **sync_kwargs)
|
||||||
|
|
||||||
_running = False
|
_running = False
|
||||||
|
@ -278,15 +266,15 @@ class SoundMonitor:
|
||||||
"""Run in monitor mode."""
|
"""Run in monitor mode."""
|
||||||
with futures.ThreadPoolExecutor() as pool:
|
with futures.ThreadPoolExecutor() as pool:
|
||||||
archives_handler = MonitorHandler(
|
archives_handler = MonitorHandler(
|
||||||
settings.SOUND_ARCHIVES_SUBDIR,
|
settings.SOUND_BROADCASTS_SUBDIR,
|
||||||
pool,
|
pool,
|
||||||
type=Sound.TYPE_ARCHIVE,
|
broadcast=True,
|
||||||
logger=logger,
|
logger=logger,
|
||||||
)
|
)
|
||||||
excerpts_handler = MonitorHandler(
|
excerpts_handler = MonitorHandler(
|
||||||
settings.SOUND_EXCERPTS_SUBDIR,
|
settings.SOUND_EXCERPTS_SUBDIR,
|
||||||
pool,
|
pool,
|
||||||
type=Sound.TYPE_EXCERPT,
|
broadcast=False,
|
||||||
logger=logger,
|
logger=logger,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -38,9 +38,7 @@ class SoxStats:
|
||||||
args += ["trim", str(at), str(length)]
|
args += ["trim", str(at), str(length)]
|
||||||
args.append("stats")
|
args.append("stats")
|
||||||
|
|
||||||
p = subprocess.Popen(
|
p = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||||
args, stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
|
||||||
)
|
|
||||||
# sox outputs to stderr (my god WHYYYY)
|
# sox outputs to stderr (my god WHYYYY)
|
||||||
out_, out = p.communicate()
|
out_, out = p.communicate()
|
||||||
self.values = self.parse(str(out, encoding="utf-8"))
|
self.values = self.parse(str(out, encoding="utf-8"))
|
||||||
|
@ -94,16 +92,8 @@ class SoundStats:
|
||||||
position += self.sample_length
|
position += self.sample_length
|
||||||
|
|
||||||
def check(self, name, min_val, max_val):
|
def check(self, name, min_val, max_val):
|
||||||
self.good = [
|
self.good = [index for index, stats in enumerate(self.stats) if min_val <= stats.get(name) <= max_val]
|
||||||
index
|
self.bad = [index for index, stats in enumerate(self.stats) if index not in self.good]
|
||||||
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()
|
self.resume()
|
||||||
|
|
||||||
def resume(self):
|
def resume(self):
|
||||||
|
@ -120,10 +110,6 @@ class SoundStats:
|
||||||
|
|
||||||
def _view(self, array):
|
def _view(self, array):
|
||||||
return [
|
return [
|
||||||
"file"
|
"file" if index == 0 else "sample {} (at {} seconds)".format(index, (index - 1) * self.sample_length)
|
||||||
if index == 0
|
|
||||||
else "sample {} (at {} seconds)".format(
|
|
||||||
index, (index - 1) * self.sample_length
|
|
||||||
)
|
|
||||||
for index in array
|
for index in array
|
||||||
]
|
]
|
||||||
|
|
|
@ -35,11 +35,7 @@ class WeekConverter:
|
||||||
return datetime.datetime.strptime(value + "/1", "%G/%V/%u").date()
|
return datetime.datetime.strptime(value + "/1", "%G/%V/%u").date()
|
||||||
|
|
||||||
def to_url(self, value):
|
def to_url(self, value):
|
||||||
return (
|
return value if isinstance(value, str) else "{:04d}/{:02d}".format(*value.isocalendar())
|
||||||
value
|
|
||||||
if isinstance(value, str)
|
|
||||||
else "{:04d}/{:02d}".format(*value.isocalendar())
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class DateConverter:
|
class DateConverter:
|
||||||
|
@ -52,10 +48,4 @@ class DateConverter:
|
||||||
return datetime.date(int(value[0]), int(value[1]), int(value[2]))
|
return datetime.date(int(value[0]), int(value[1]), int(value[2]))
|
||||||
|
|
||||||
def to_url(self, value):
|
def to_url(self, value):
|
||||||
return (
|
return value if isinstance(value, str) else "{:04d}/{:02d}/{:02d}".format(value.year, value.month, value.day)
|
||||||
value
|
|
||||||
if isinstance(value, str)
|
|
||||||
else "{:04d}/{:02d}/{:02d}".format(
|
|
||||||
value.year, value.month, value.day
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
|
@ -1,16 +1,30 @@
|
||||||
import django_filters as filters
|
from django.contrib.auth.models import User
|
||||||
|
from django.db.models import Q
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
import django_filters as filters
|
||||||
|
|
||||||
from .models import Episode, Page
|
from . import models
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
"PageFilters",
|
||||||
|
"EpisodeFilters",
|
||||||
|
"ImageFilterSet",
|
||||||
|
"SoundFilterSet",
|
||||||
|
"TrackFilterSet",
|
||||||
|
"UserFilterSet",
|
||||||
|
"GroupFilterSet",
|
||||||
|
"UserGroupFilterSet",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class PageFilters(filters.FilterSet):
|
class PageFilters(filters.FilterSet):
|
||||||
q = filters.CharFilter(method="search_filter", label=_("Search"))
|
q = filters.CharFilter(method="search_filter", label=_("Search"))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Page
|
model = models.Page
|
||||||
fields = {
|
fields = {
|
||||||
"category__id": ["in"],
|
"category__id": ["in", "exact"],
|
||||||
"pub_date": ["exact", "gte", "lte"],
|
"pub_date": ["exact", "gte", "lte"],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,15 +33,84 @@ class PageFilters(filters.FilterSet):
|
||||||
|
|
||||||
|
|
||||||
class EpisodeFilters(PageFilters):
|
class EpisodeFilters(PageFilters):
|
||||||
podcast = filters.BooleanFilter(
|
podcast = filters.BooleanFilter(method="podcast_filter", label=_("Podcast"))
|
||||||
method="podcast_filter", label=_("Podcast")
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Episode
|
model = models.Episode
|
||||||
fields = PageFilters.Meta.fields.copy()
|
fields = PageFilters.Meta.fields.copy()
|
||||||
|
|
||||||
def podcast_filter(self, queryset, name, value):
|
def podcast_filter(self, queryset, name, value):
|
||||||
if value:
|
if value:
|
||||||
return queryset.filter(sound__is_public=True).distinct()
|
return queryset.filter(sound__is_public=True).distinct()
|
||||||
return queryset.filter(sound__isnull=True)
|
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
|
||||||
|
|
|
@ -1,18 +0,0 @@
|
||||||
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"]
|
|
23
aircox/forms/__init__.py
Normal file
23
aircox/forms/__init__.py
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
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,
|
||||||
|
)
|
34
aircox/forms/episode.py
Normal file
34
aircox/forms/episode.py
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
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."""
|
37
aircox/forms/page.py
Normal file
37
aircox/forms/page.py
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
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
|
11
aircox/forms/program.py
Normal file
11
aircox/forms/program.py
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
from aircox import models
|
||||||
|
from .page import PageForm
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ("ProgramForm",)
|
||||||
|
|
||||||
|
|
||||||
|
class ProgramForm(PageForm):
|
||||||
|
class Meta:
|
||||||
|
fields = PageForm.Meta.fields
|
||||||
|
model = models.Program
|
26
aircox/forms/sound.py
Normal file
26
aircox/forms/sound.py
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
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()}
|
23
aircox/forms/track.py
Normal file
23
aircox/forms/track.py
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
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."""
|
89
aircox/forms/widgets.py
Normal file
89
aircox/forms/widgets.py
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
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,8 +30,7 @@ class Command(BaseCommand):
|
||||||
"--age",
|
"--age",
|
||||||
type=int,
|
type=int,
|
||||||
default=settings.LOGS_ARCHIVES_AGE,
|
default=settings.LOGS_ARCHIVES_AGE,
|
||||||
help="minimal age in days of logs to archive. Default is "
|
help="minimal age in days of logs to archive. Default is " "settings.LOGS_ARCHIVES_AGE",
|
||||||
"settings.LOGS_ARCHIVES_AGE",
|
|
||||||
)
|
)
|
||||||
group.add_argument(
|
group.add_argument(
|
||||||
"-k",
|
"-k",
|
||||||
|
|
|
@ -55,14 +55,11 @@ class Command(BaseCommand):
|
||||||
group.add_argument(
|
group.add_argument(
|
||||||
"--next-month",
|
"--next-month",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="set the date to the next month of given date"
|
help="set the date to the next month of given date" " (if next month from today",
|
||||||
" (if next month from today",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
date = datetime.date(
|
date = datetime.date(year=options["year"], month=options["month"], day=1)
|
||||||
year=options["year"], month=options["month"], day=1
|
|
||||||
)
|
|
||||||
if options.get("next_month"):
|
if options.get("next_month"):
|
||||||
month = options.get("month")
|
month = options.get("month")
|
||||||
date += tz.timedelta(days=28)
|
date += tz.timedelta(days=28)
|
||||||
|
|
|
@ -51,18 +51,13 @@ class Command(BaseCommand):
|
||||||
def handle(self, path, *args, **options):
|
def handle(self, path, *args, **options):
|
||||||
# FIXME: absolute/relative path of sounds vs given path
|
# FIXME: absolute/relative path of sounds vs given path
|
||||||
if options.get("sound"):
|
if options.get("sound"):
|
||||||
sound = Sound.objects.filter(
|
sound = Sound.objects.filter(file__icontains=options.get("sound")).first()
|
||||||
file__icontains=options.get("sound")
|
|
||||||
).first()
|
|
||||||
else:
|
else:
|
||||||
path_, ext = os.path.splitext(path)
|
path_, ext = os.path.splitext(path)
|
||||||
sound = Sound.objects.filter(path__icontains=path_).first()
|
sound = Sound.objects.filter(path__icontains=path_).first()
|
||||||
|
|
||||||
if not sound:
|
if not sound:
|
||||||
logger.error(
|
logger.error("no sound found in the database for the path " "{path}".format(path=path))
|
||||||
"no sound found in the database for the path "
|
|
||||||
"{path}".format(path=path)
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# FIXME: auto get sound.episode if any
|
# FIXME: auto get sound.episode if any
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
#! /usr/bin/env python3
|
#! /usr/bin/env python3
|
||||||
# TODO: SoundMonitor class
|
|
||||||
|
|
||||||
"""Monitor sound files; For each program, check for:
|
"""Monitor sound files; For each program, check for:
|
||||||
|
|
||||||
|
@ -43,8 +42,7 @@ class Command(BaseCommand):
|
||||||
"-q",
|
"-q",
|
||||||
"--quality_check",
|
"--quality_check",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="Enable quality check using sound_quality_check on all "
|
help="Enable quality check using sound_quality_check on all " "sounds marqued as not good",
|
||||||
"sounds marqued as not good",
|
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-s",
|
"-s",
|
||||||
|
@ -57,15 +55,12 @@ class Command(BaseCommand):
|
||||||
"-m",
|
"-m",
|
||||||
"--monitor",
|
"--monitor",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="Run in monitor mode, watch for modification in the "
|
help="Run in monitor mode, watch for modification in the " "filesystem and react in consequence",
|
||||||
"filesystem and react in consequence",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
SoundMonitor()
|
monitor = SoundMonitor()
|
||||||
if options.get("scan"):
|
if options.get("scan"):
|
||||||
self.scan()
|
monitor.scan()
|
||||||
# if options.get('quality_check'):
|
|
||||||
# self.check_quality(check=(not options.get('scan')))
|
|
||||||
if options.get("monitor"):
|
if options.get("monitor"):
|
||||||
self.monitor()
|
monitor.monitor()
|
||||||
|
|
|
@ -28,8 +28,7 @@ class Command(BaseCommand):
|
||||||
"--sample_length",
|
"--sample_length",
|
||||||
type=int,
|
type=int,
|
||||||
default=120,
|
default=120,
|
||||||
help="size of sample to analyse in seconds. If not set (or 0), "
|
help="size of sample to analyse in seconds. If not set (or 0), " "does not analyse by sample",
|
||||||
"does not analyse by sample",
|
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-a",
|
"-a",
|
||||||
|
@ -43,8 +42,7 @@ class Command(BaseCommand):
|
||||||
"--range",
|
"--range",
|
||||||
type=float,
|
type=float,
|
||||||
nargs=2,
|
nargs=2,
|
||||||
help="range of minimal and maximal accepted value such as: "
|
help="range of minimal and maximal accepted value such as: " "--range min max",
|
||||||
"--range min max",
|
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-i",
|
"-i",
|
||||||
|
@ -64,10 +62,7 @@ class Command(BaseCommand):
|
||||||
raise CommandError("no attribute specified")
|
raise CommandError("no attribute specified")
|
||||||
|
|
||||||
# sound analyse and checks
|
# sound analyse and checks
|
||||||
self.sounds = [
|
self.sounds = [SoundStats(path, options.get("sample_length")) for path in options.get("files")]
|
||||||
SoundStats(path, options.get("sample_length"))
|
|
||||||
for path in options.get("files")
|
|
||||||
]
|
|
||||||
self.bad = []
|
self.bad = []
|
||||||
self.good = []
|
self.good = []
|
||||||
for sound in self.sounds:
|
for sound in self.sounds:
|
||||||
|
|
|
@ -13,6 +13,7 @@ class AircoxMiddleware(object):
|
||||||
"""Middleware used to get default info for the given website.
|
"""Middleware used to get default info for the given website.
|
||||||
|
|
||||||
It provide following request attributes:
|
It provide following request attributes:
|
||||||
|
- ``mobile``: set to True if mobile device is detected
|
||||||
- ``station``: current Station
|
- ``station``: current Station
|
||||||
|
|
||||||
This middleware must be set after the middleware
|
This middleware must be set after the middleware
|
||||||
|
@ -24,6 +25,11 @@ class AircoxMiddleware(object):
|
||||||
def __init__(self, get_response):
|
def __init__(self, get_response):
|
||||||
self.get_response = 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):
|
def get_station(self, request):
|
||||||
"""Return station for the provided request."""
|
"""Return station for the provided request."""
|
||||||
host = request.get_host()
|
host = request.get_host()
|
||||||
|
@ -45,6 +51,7 @@ class AircoxMiddleware(object):
|
||||||
def __call__(self, request):
|
def __call__(self, request):
|
||||||
self.init_timezone(request)
|
self.init_timezone(request)
|
||||||
request.station = self.get_station(request)
|
request.station = self.get_station(request)
|
||||||
|
request.is_mobile = self.is_mobile(request)
|
||||||
try:
|
try:
|
||||||
return self.get_response(request)
|
return self.get_response(request)
|
||||||
except Redirect:
|
except Redirect:
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
# Generated by Django 3.0.6 on 2020-05-26 12:57
|
# Generated by Django 3.0.6 on 2020-05-26 12:57
|
||||||
|
|
||||||
import ckeditor.fields
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
|
@ -84,9 +83,7 @@ class Migration(migrations.Migration):
|
||||||
options={
|
options={
|
||||||
"verbose_name": "Diffusion",
|
"verbose_name": "Diffusion",
|
||||||
"verbose_name_plural": "Diffusions",
|
"verbose_name_plural": "Diffusions",
|
||||||
"permissions": (
|
"permissions": (("programming", "edit the diffusion's planification"),),
|
||||||
("programming", "edit the diffusion's planification"),
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
|
@ -125,22 +122,16 @@ class Migration(migrations.Migration):
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"content",
|
"content",
|
||||||
ckeditor.fields.RichTextField(
|
models.TextField(blank=True, null=True, verbose_name="content"),
|
||||||
blank=True, null=True, verbose_name="content"
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
("pub_date", models.DateTimeField(blank=True, null=True)),
|
("pub_date", models.DateTimeField(blank=True, null=True)),
|
||||||
(
|
(
|
||||||
"featured",
|
"featured",
|
||||||
models.BooleanField(
|
models.BooleanField(default=False, verbose_name="featured"),
|
||||||
default=False, verbose_name="featured"
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"allow_comments",
|
"allow_comments",
|
||||||
models.BooleanField(
|
models.BooleanField(default=True, verbose_name="allow comments"),
|
||||||
default=True, verbose_name="allow comments"
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"category",
|
"category",
|
||||||
|
@ -341,7 +332,7 @@ class Migration(migrations.Migration):
|
||||||
"active",
|
"active",
|
||||||
models.BooleanField(
|
models.BooleanField(
|
||||||
default=True,
|
default=True,
|
||||||
help_text="if not checked this program is no longer active",
|
help_text="if not chemodels.onger active",
|
||||||
verbose_name="active",
|
verbose_name="active",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -458,9 +449,7 @@ class Migration(migrations.Migration):
|
||||||
("name", models.CharField(max_length=64, verbose_name="name")),
|
("name", models.CharField(max_length=64, verbose_name="name")),
|
||||||
(
|
(
|
||||||
"slug",
|
"slug",
|
||||||
models.SlugField(
|
models.SlugField(max_length=64, unique=True, verbose_name="slug"),
|
||||||
max_length=64, unique=True, verbose_name="slug"
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"path",
|
"path",
|
||||||
|
@ -566,9 +555,7 @@ class Migration(migrations.Migration):
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"content",
|
"content",
|
||||||
ckeditor.fields.RichTextField(
|
models.TextField(blank=True, null=True, verbose_name="content"),
|
||||||
blank=True, null=True, verbose_name="content"
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"view",
|
"view",
|
||||||
|
@ -949,9 +936,7 @@ class Migration(migrations.Migration):
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"time",
|
"time",
|
||||||
models.TimeField(
|
models.TimeField(help_text="start time", verbose_name="time"),
|
||||||
help_text="start time", verbose_name="time"
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"timezone",
|
"timezone",
|
||||||
|
@ -1643,9 +1628,7 @@ class Migration(migrations.Migration):
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"duration",
|
"duration",
|
||||||
models.TimeField(
|
models.TimeField(help_text="regular duration", verbose_name="duration"),
|
||||||
help_text="regular duration", verbose_name="duration"
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"frequency",
|
"frequency",
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
# Generated by Django 3.1.1 on 2020-09-21 23:56
|
# Generated by Django 3.1.1 on 2020-09-21 23:56
|
||||||
|
|
||||||
import ckeditor_uploader.fields
|
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
@ -30,13 +29,6 @@ class Migration(migrations.Migration):
|
||||||
model_name="sound",
|
model_name="sound",
|
||||||
name="embed",
|
name="embed",
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
|
||||||
model_name="page",
|
|
||||||
name="content",
|
|
||||||
field=ckeditor_uploader.fields.RichTextUploadingField(
|
|
||||||
blank=True, null=True, verbose_name="content"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name="sound",
|
model_name="sound",
|
||||||
name="program",
|
name="program",
|
||||||
|
@ -49,11 +41,4 @@ class Migration(migrations.Migration):
|
||||||
),
|
),
|
||||||
preserve_default=False,
|
preserve_default=False,
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
|
||||||
model_name="staticpage",
|
|
||||||
name="content",
|
|
||||||
field=ckeditor_uploader.fields.RichTextUploadingField(
|
|
||||||
blank=True, null=True, verbose_name="content"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
|
|
|
@ -12,9 +12,7 @@ class Migration(migrations.Migration):
|
||||||
migrations.AlterModelOptions(
|
migrations.AlterModelOptions(
|
||||||
name="diffusion",
|
name="diffusion",
|
||||||
options={
|
options={
|
||||||
"permissions": (
|
"permissions": (("programming", "edit the diffusions' planification"),),
|
||||||
("programming", "edit the diffusions' planification"),
|
|
||||||
),
|
|
||||||
"verbose_name": "Diffusion",
|
"verbose_name": "Diffusion",
|
||||||
"verbose_name_plural": "Diffusions",
|
"verbose_name_plural": "Diffusions",
|
||||||
},
|
},
|
||||||
|
@ -22,9 +20,7 @@ class Migration(migrations.Migration):
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name="track",
|
model_name="track",
|
||||||
name="album",
|
name="album",
|
||||||
field=models.CharField(
|
field=models.CharField(default="", max_length=128, verbose_name="album"),
|
||||||
default="", max_length=128, verbose_name="album"
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name="schedule",
|
model_name="schedule",
|
||||||
|
|
|
@ -12,8 +12,6 @@ class Migration(migrations.Migration):
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name="track",
|
model_name="track",
|
||||||
name="year",
|
name="year",
|
||||||
field=models.IntegerField(
|
field=models.IntegerField(blank=True, null=True, verbose_name="year"),
|
||||||
blank=True, null=True, verbose_name="year"
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -12,8 +12,6 @@ class Migration(migrations.Migration):
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name="track",
|
model_name="track",
|
||||||
name="album",
|
name="album",
|
||||||
field=models.CharField(
|
field=models.CharField(blank=True, max_length=128, null=True, verbose_name="album"),
|
||||||
blank=True, max_length=128, null=True, verbose_name="album"
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -30,9 +30,7 @@ class Migration(migrations.Migration):
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"playlist_editor_sep",
|
"playlist_editor_sep",
|
||||||
models.CharField(
|
models.CharField(max_length=16, verbose_name="Playlist Editor Separator"),
|
||||||
max_length=16, verbose_name="Playlist Editor Separator"
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"user",
|
"user",
|
||||||
|
|
623
aircox/migrations/0014_alter_schedule_timezone.py
Normal file
623
aircox/migrations/0014_alter_schedule_timezone.py
Normal file
|
@ -0,0 +1,623 @@
|
||||||
|
# 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",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,641 @@
|
||||||
|
# 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",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
25
aircox/migrations/0015_program_editors.py
Normal file
25
aircox/migrations/0015_program_editors.py
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
# 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",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
32
aircox/migrations/0016_alter_staticpage_attach_to.py
Normal file
32
aircox/migrations/0016_alter_staticpage_attach_to.py
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
# 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",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,36 @@
|
||||||
|
# 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",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
32
aircox/migrations/0018_alter_staticpage_attach_to.py
Normal file
32
aircox/migrations/0018_alter_staticpage_attach_to.py
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
# 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",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
12
aircox/migrations/0019_merge_20240119_1022.py
Normal file
12
aircox/migrations/0019_merge_20240119_1022.py
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
# 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 = []
|
|
@ -0,0 +1,42 @@
|
||||||
|
# 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",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
12
aircox/migrations/0020_merge_20240205_1027.py
Normal file
12
aircox/migrations/0020_merge_20240205_1027.py
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
# 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 = []
|
623
aircox/migrations/0021_alter_schedule_timezone.py
Normal file
623
aircox/migrations/0021_alter_schedule_timezone.py
Normal file
|
@ -0,0 +1,623 @@
|
||||||
|
# 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",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
31
aircox/migrations/0022_set_group_ownership.py
Normal file
31
aircox/migrations/0022_set_group_ownership.py
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
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",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,634 @@
|
||||||
|
# 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",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,22 @@
|
||||||
|
# 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",
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,36 @@
|
||||||
|
# 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"),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,162 @@
|
||||||
|
# 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),
|
||||||
|
]
|
|
@ -0,0 +1,78 @@
|
||||||
|
# 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),
|
||||||
|
]
|
|
@ -0,0 +1,64 @@
|
||||||
|
# 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,28 +1,30 @@
|
||||||
from . import signals
|
from . import signals
|
||||||
from .article import Article
|
from .article import Article
|
||||||
from .diffusion import Diffusion, DiffusionQuerySet
|
from .diffusion import Diffusion, DiffusionQuerySet
|
||||||
from .episode import Episode
|
from .episode import Episode, EpisodeSound
|
||||||
from .log import Log, LogQuerySet
|
from .log import Log, LogQuerySet
|
||||||
from .page import Category, Comment, NavItem, Page, PageQuerySet, StaticPage
|
from .page import Category, Comment, NavItem, Page, PageQuerySet, StaticPage
|
||||||
from .program import Program, ProgramChildQuerySet, ProgramQuerySet, Stream
|
from .program import Program, ProgramChildQuerySet, ProgramQuerySet, Stream
|
||||||
from .schedule import Schedule
|
from .schedule import Schedule
|
||||||
from .sound import Sound, SoundQuerySet, Track
|
from .sound import Sound, SoundQuerySet
|
||||||
from .station import Port, Station, StationQuerySet
|
from .station import Port, Station, StationQuerySet
|
||||||
|
from .track import Track
|
||||||
from .user_settings import UserSettings
|
from .user_settings import UserSettings
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
"signals",
|
"signals",
|
||||||
"Article",
|
"Article",
|
||||||
"Episode",
|
"Category",
|
||||||
|
"Comment",
|
||||||
"Diffusion",
|
"Diffusion",
|
||||||
"DiffusionQuerySet",
|
"DiffusionQuerySet",
|
||||||
|
"Episode",
|
||||||
|
"EpisodeSound",
|
||||||
"Log",
|
"Log",
|
||||||
"LogQuerySet",
|
"LogQuerySet",
|
||||||
"Category",
|
|
||||||
"PageQuerySet",
|
"PageQuerySet",
|
||||||
"Page",
|
"Page",
|
||||||
"StaticPage",
|
"StaticPage",
|
||||||
"Comment",
|
|
||||||
"NavItem",
|
"NavItem",
|
||||||
"Program",
|
"Program",
|
||||||
"ProgramQuerySet",
|
"ProgramQuerySet",
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from .page import Page
|
from .page import ChildPage
|
||||||
from .program import ProgramChildQuerySet
|
from .program import ProgramChildQuerySet
|
||||||
|
|
||||||
__all__ = ("Article",)
|
__all__ = ("Article",)
|
||||||
|
|
||||||
|
|
||||||
class Article(Page):
|
class Article(ChildPage):
|
||||||
detail_url_name = "article-detail"
|
detail_url_name = "article-detail"
|
||||||
|
template_prefix = "article"
|
||||||
|
|
||||||
objects = ProgramChildQuerySet.as_manager()
|
objects = ProgramChildQuerySet.as_manager()
|
||||||
|
|
||||||
|
|
|
@ -17,13 +17,13 @@ __all__ = ("Diffusion", "DiffusionQuerySet")
|
||||||
|
|
||||||
|
|
||||||
class DiffusionQuerySet(RerunQuerySet):
|
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):
|
def episode(self, episode=None, id=None):
|
||||||
"""Diffusions for this episode."""
|
"""Diffusions for this episode."""
|
||||||
return (
|
return self.filter(episode=episode) if id is None else self.filter(episode__id=id)
|
||||||
self.filter(episode=episode)
|
|
||||||
if id is None
|
|
||||||
else self.filter(episode__id=id)
|
|
||||||
)
|
|
||||||
|
|
||||||
def on_air(self):
|
def on_air(self):
|
||||||
"""On air diffusions."""
|
"""On air diffusions."""
|
||||||
|
@ -40,9 +40,7 @@ class DiffusionQuerySet(RerunQuerySet):
|
||||||
"""Diffusions occuring date."""
|
"""Diffusions occuring date."""
|
||||||
date = date or datetime.date.today()
|
date = date or datetime.date.today()
|
||||||
start = tz.make_aware(tz.datetime.combine(date, datetime.time()))
|
start = tz.make_aware(tz.datetime.combine(date, datetime.time()))
|
||||||
end = tz.make_aware(
|
end = tz.make_aware(tz.datetime.combine(date, datetime.time(23, 59, 59, 999)))
|
||||||
tz.datetime.combine(date, datetime.time(23, 59, 59, 999))
|
|
||||||
)
|
|
||||||
# start = tz.get_current_timezone().localize(start)
|
# start = tz.get_current_timezone().localize(start)
|
||||||
# end = tz.get_current_timezone().localize(end)
|
# end = tz.get_current_timezone().localize(end)
|
||||||
qs = self.filter(start__range=(start, end))
|
qs = self.filter(start__range=(start, end))
|
||||||
|
@ -50,11 +48,7 @@ class DiffusionQuerySet(RerunQuerySet):
|
||||||
|
|
||||||
def at(self, date, order=True):
|
def at(self, date, order=True):
|
||||||
"""Return diffusions at specified date or datetime."""
|
"""Return diffusions at specified date or datetime."""
|
||||||
return (
|
return self.now(date, order) if isinstance(date, tz.datetime) else self.date(date, order)
|
||||||
self.now(date, order)
|
|
||||||
if isinstance(date, tz.datetime)
|
|
||||||
else self.date(date, order)
|
|
||||||
)
|
|
||||||
|
|
||||||
def after(self, date=None):
|
def after(self, date=None):
|
||||||
"""Return a queryset of diffusions that happen after the given date
|
"""Return a queryset of diffusions that happen after the given date
|
||||||
|
@ -99,6 +93,8 @@ class Diffusion(Rerun):
|
||||||
- stop: the diffusion has been manually stopped
|
- stop: the diffusion has been manually stopped
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
list_url_name = "timetable-list"
|
||||||
|
|
||||||
objects = DiffusionQuerySet.as_manager()
|
objects = DiffusionQuerySet.as_manager()
|
||||||
|
|
||||||
TYPE_ON_AIR = 0x00
|
TYPE_ON_AIR = 0x00
|
||||||
|
@ -137,14 +133,10 @@ class Diffusion(Rerun):
|
||||||
# help_text = _('use this input port'),
|
# help_text = _('use this input port'),
|
||||||
# )
|
# )
|
||||||
|
|
||||||
item_template_name = "aircox/widgets/diffusion_item.html"
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("Diffusion")
|
verbose_name = _("Diffusion")
|
||||||
verbose_name_plural = _("Diffusions")
|
verbose_name_plural = _("Diffusions")
|
||||||
permissions = (
|
permissions = (("programming", _("edit the diffusions' planification")),)
|
||||||
("programming", _("edit the diffusions' planification")),
|
|
||||||
)
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
str_ = "{episode} - {date}".format(
|
str_ = "{episode} - {date}".format(
|
||||||
|
@ -202,47 +194,17 @@ class Diffusion(Rerun):
|
||||||
def is_now(self):
|
def is_now(self):
|
||||||
"""True if diffusion is currently running."""
|
"""True if diffusion is currently running."""
|
||||||
now = tz.now()
|
now = tz.now()
|
||||||
return (
|
return self.type == self.TYPE_ON_AIR and self.start <= now and self.end >= now
|
||||||
self.type == self.TYPE_ON_AIR
|
|
||||||
and self.start <= now
|
@property
|
||||||
and self.end >= now
|
def is_today(self):
|
||||||
)
|
"""True if diffusion is currently today."""
|
||||||
|
return self.start.date() == datetime.date.today()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_live(self):
|
def is_live(self):
|
||||||
"""True if Diffusion is live (False if there are sounds files)."""
|
"""True if Diffusion is live (False if there are sounds files)."""
|
||||||
return (
|
return self.type == self.TYPE_ON_AIR and not self.episode.episodesound_set.all().broadcast()
|
||||||
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):
|
def is_date_in_range(self, date=None):
|
||||||
"""Return true if the given date is in the diffusion's start-end
|
"""Return true if the given date is in the diffusion's start-end
|
||||||
|
@ -262,8 +224,7 @@ class Diffusion(Rerun):
|
||||||
# .filter(conflict_with=True)
|
# .filter(conflict_with=True)
|
||||||
return (
|
return (
|
||||||
Diffusion.objects.filter(
|
Diffusion.objects.filter(
|
||||||
Q(start__lt=self.start, end__gt=self.start)
|
Q(start__lt=self.start, end__gt=self.start) | Q(start__gt=self.start, start__lt=self.end)
|
||||||
| Q(start__gt=self.start, start__lt=self.end)
|
|
||||||
)
|
)
|
||||||
.exclude(pk=self.pk)
|
.exclude(pk=self.pk)
|
||||||
.distinct()
|
.distinct()
|
||||||
|
|
|
@ -1,58 +1,65 @@
|
||||||
|
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.functional import cached_property
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from easy_thumbnails.files import get_thumbnailer
|
|
||||||
|
|
||||||
from aircox.conf import settings
|
from aircox.conf import settings
|
||||||
|
|
||||||
from .page import Page
|
from .page import ChildPage
|
||||||
from .program import ProgramChildQuerySet
|
from .program import ProgramChildQuerySet
|
||||||
|
from .sound import Sound
|
||||||
|
|
||||||
__all__ = ("Episode",)
|
__all__ = ("Episode",)
|
||||||
|
|
||||||
|
|
||||||
class Episode(Page):
|
class EpisodeQuerySet(ProgramChildQuerySet):
|
||||||
objects = ProgramChildQuerySet.as_manager()
|
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()
|
||||||
detail_url_name = "episode-detail"
|
detail_url_name = "episode-detail"
|
||||||
item_template_name = "aircox/widgets/episode_item.html"
|
list_url_name = "episode-list"
|
||||||
|
edit_url_name = "episode-edit"
|
||||||
|
template_prefix = "episode"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def program(self):
|
def program(self):
|
||||||
return getattr(self.parent, "program", None)
|
return self.parent_subclass
|
||||||
|
|
||||||
@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
|
@program.setter
|
||||||
def program(self, value):
|
def program(self, value):
|
||||||
self.parent = 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:
|
class Meta:
|
||||||
verbose_name = _("Episode")
|
verbose_name = _("Episode")
|
||||||
verbose_name_plural = _("Episodes")
|
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):
|
def save(self, *args, **kwargs):
|
||||||
if self.parent is None:
|
if self.parent is None:
|
||||||
raise ValueError("missing parent program")
|
raise ValueError("missing parent program")
|
||||||
|
@ -76,6 +83,55 @@ class Episode(Page):
|
||||||
if title is None
|
if title is None
|
||||||
else title
|
else title
|
||||||
)
|
)
|
||||||
return super().get_init_kwargs_from(
|
return super().get_init_kwargs_from(page, title=title, program=page, **kwargs)
|
||||||
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)
|
||||||
|
|
153
aircox/models/file.py
Normal file
153
aircox/models/file.py
Normal file
|
@ -0,0 +1,153 @@
|
||||||
|
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,5 +1,6 @@
|
||||||
import datetime
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
|
import operator
|
||||||
from collections import deque
|
from collections import deque
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
@ -7,8 +8,10 @@ from django.utils import timezone as tz
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from .diffusion import Diffusion
|
from .diffusion import Diffusion
|
||||||
from .sound import Sound, Track
|
from .sound import Sound
|
||||||
from .station import Station
|
from .station import Station
|
||||||
|
from .track import Track
|
||||||
|
from .page import Renderable
|
||||||
|
|
||||||
logger = logging.getLogger("aircox")
|
logger = logging.getLogger("aircox")
|
||||||
|
|
||||||
|
@ -18,11 +21,7 @@ __all__ = ("Log", "LogQuerySet")
|
||||||
|
|
||||||
class LogQuerySet(models.QuerySet):
|
class LogQuerySet(models.QuerySet):
|
||||||
def station(self, station=None, id=None):
|
def station(self, station=None, id=None):
|
||||||
return (
|
return self.filter(station=station) if id is None else self.filter(station_id=id)
|
||||||
self.filter(station=station)
|
|
||||||
if id is None
|
|
||||||
else self.filter(station_id=id)
|
|
||||||
)
|
|
||||||
|
|
||||||
def date(self, date):
|
def date(self, date):
|
||||||
start = tz.datetime.combine(date, datetime.time())
|
start = tz.datetime.combine(date, datetime.time())
|
||||||
|
@ -32,11 +31,10 @@ class LogQuerySet(models.QuerySet):
|
||||||
# return self.filter(date__date=date)
|
# return self.filter(date__date=date)
|
||||||
|
|
||||||
def after(self, date):
|
def after(self, date):
|
||||||
return (
|
return self.filter(date__gte=date) if isinstance(date, tz.datetime) else self.filter(date__date__gte=date)
|
||||||
self.filter(date__gte=date)
|
|
||||||
if isinstance(date, tz.datetime)
|
def before(self, date):
|
||||||
else self.filter(date__date__gte=date)
|
return self.filter(date__lte=date) if isinstance(date, tz.datetime) else self.filter(date__date__lte=date)
|
||||||
)
|
|
||||||
|
|
||||||
def on_air(self):
|
def on_air(self):
|
||||||
return self.filter(type=Log.TYPE_ON_AIR)
|
return self.filter(type=Log.TYPE_ON_AIR)
|
||||||
|
@ -54,13 +52,15 @@ class LogQuerySet(models.QuerySet):
|
||||||
return self.filter(track__isnull=not with_it)
|
return self.filter(track__isnull=not with_it)
|
||||||
|
|
||||||
|
|
||||||
class Log(models.Model):
|
class Log(Renderable, models.Model):
|
||||||
"""Log sounds and diffusions that are played on the station.
|
"""Log sounds and diffusions that are played on the station.
|
||||||
|
|
||||||
This only remember what has been played on the outputs, not on each
|
This only remember what has been played on the outputs, not on each
|
||||||
source; Source designate here which source is responsible of that.
|
source; Source designate here which source is responsible of that.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
template_prefix = "log"
|
||||||
|
|
||||||
TYPE_STOP = 0x00
|
TYPE_STOP = 0x00
|
||||||
"""Source has been stopped, e.g. manually."""
|
"""Source has been stopped, e.g. manually."""
|
||||||
# Rule: \/ diffusion != null \/ sound != null
|
# Rule: \/ diffusion != null \/ sound != null
|
||||||
|
@ -98,7 +98,7 @@ class Log(models.Model):
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
verbose_name=_("source"),
|
verbose_name=_("source"),
|
||||||
help_text=_("identifier of the source related to this log"),
|
help_text=_("Identifier of the log's source."),
|
||||||
)
|
)
|
||||||
comment = models.CharField(
|
comment = models.CharField(
|
||||||
max_length=512,
|
max_length=512,
|
||||||
|
@ -168,21 +168,22 @@ class Log(models.Model):
|
||||||
object_list += [cls(obj) for obj in items]
|
object_list += [cls(obj) for obj in items]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def merge_diffusions(cls, logs, diffs, count=None):
|
def merge_diffusions(cls, logs, diffs, count=None, diff_count=None, group_logs=False):
|
||||||
"""Merge logs and diffusions together.
|
"""Merge logs and diffusions together.
|
||||||
|
|
||||||
`logs` can either be a queryset or a list ordered by `Log.date`.
|
`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):
|
if isinstance(logs, models.QuerySet):
|
||||||
logs = list(logs.order_by("-date"))
|
logs = list(logs.order_by("-date"))
|
||||||
diffs = deque(diffs.on_air().before().order_by("-start"))
|
diffs = diffs.on_air().order_by("-start")
|
||||||
|
if diff_count:
|
||||||
|
diffs = diffs[:diff_count]
|
||||||
|
diffs = deque(diffs)
|
||||||
object_list = []
|
object_list = []
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
if not len(diffs):
|
if not len(diffs):
|
||||||
object_list += logs
|
cls._append_logs(object_list, logs, len(logs), group=group_logs)
|
||||||
break
|
break
|
||||||
|
|
||||||
if not len(logs):
|
if not len(logs):
|
||||||
|
@ -192,13 +193,8 @@ class Log(models.Model):
|
||||||
diff = diffs.popleft()
|
diff = diffs.popleft()
|
||||||
|
|
||||||
# - takes all logs after diff start
|
# - takes all logs after diff start
|
||||||
index = next(
|
index = cls._next_index(logs, diff.end, len(logs), pred=operator.le)
|
||||||
(i for i, v in enumerate(logs) if v.date <= diff.end),
|
cls._append_logs(object_list, logs, index, group=group_logs)
|
||||||
len(logs),
|
|
||||||
)
|
|
||||||
if index is not None and index > 0:
|
|
||||||
object_list += logs[:index]
|
|
||||||
logs = logs[index:]
|
|
||||||
|
|
||||||
if len(logs):
|
if len(logs):
|
||||||
# FIXME
|
# FIXME
|
||||||
|
@ -207,10 +203,7 @@ class Log(models.Model):
|
||||||
# object_list.append(logs[0])
|
# object_list.append(logs[0])
|
||||||
|
|
||||||
# - skips logs while diff is running
|
# - skips logs while diff is running
|
||||||
index = next(
|
index = cls._next_index(logs, diff.start, len(logs))
|
||||||
(i for i, v in enumerate(logs) if v.date < diff.start),
|
|
||||||
len(logs),
|
|
||||||
)
|
|
||||||
if index is not None and index > 0:
|
if index is not None and index > 0:
|
||||||
logs = logs[index:]
|
logs = logs[index:]
|
||||||
|
|
||||||
|
@ -219,6 +212,40 @@ class Log(models.Model):
|
||||||
|
|
||||||
return object_list if count is None else object_list[:count]
|
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):
|
def print(self):
|
||||||
r = []
|
r = []
|
||||||
if self.diffusion:
|
if self.diffusion:
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import re
|
import re
|
||||||
|
|
||||||
import bleach
|
import bleach
|
||||||
from ckeditor_uploader.fields import RichTextUploadingField
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils import timezone as tz
|
from django.utils import timezone as tz
|
||||||
|
@ -13,9 +12,11 @@ from django.utils.translation import gettext_lazy as _
|
||||||
from filer.fields.image import FilerImageField
|
from filer.fields.image import FilerImageField
|
||||||
from model_utils.managers import InheritanceQuerySet
|
from model_utils.managers import InheritanceQuerySet
|
||||||
|
|
||||||
|
from ..conf import settings
|
||||||
from .station import Station
|
from .station import Station
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
|
"Renderable",
|
||||||
"Category",
|
"Category",
|
||||||
"PageQuerySet",
|
"PageQuerySet",
|
||||||
"Page",
|
"Page",
|
||||||
|
@ -25,9 +26,17 @@ __all__ = (
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
headline_re = re.compile(
|
headline_clean_re = re.compile(r"\n(\s| )+", re.MULTILINE)
|
||||||
r"(<p>)?" r"(?P<headline>[^\n]{1,140}(\n|[^\.]*?\.))" r"(</p>)?"
|
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)
|
||||||
|
|
||||||
|
|
||||||
class Category(models.Model):
|
class Category(models.Model):
|
||||||
|
@ -52,23 +61,20 @@ class BasePageQuerySet(InheritanceQuerySet):
|
||||||
def trash(self):
|
def trash(self):
|
||||||
return self.filter(status=Page.STATUS_TRASH)
|
return self.filter(status=Page.STATUS_TRASH)
|
||||||
|
|
||||||
|
def by_last(self):
|
||||||
|
return self.order_by("-pub_date")
|
||||||
|
|
||||||
def parent(self, parent=None, id=None):
|
def parent(self, parent=None, id=None):
|
||||||
"""Return pages having this parent."""
|
"""Return pages having this parent."""
|
||||||
return (
|
return self.filter(parent=parent) if id is None else self.filter(parent__id=id)
|
||||||
self.filter(parent=parent)
|
|
||||||
if id is None
|
|
||||||
else self.filter(parent__id=id)
|
|
||||||
)
|
|
||||||
|
|
||||||
def search(self, q, search_content=True):
|
def search(self, q, search_content=True):
|
||||||
if search_content:
|
if search_content:
|
||||||
return self.filter(
|
return self.filter(models.Q(title__icontains=q) | models.Q(content__icontains=q))
|
||||||
models.Q(title__icontains=q) | models.Q(content__icontains=q)
|
|
||||||
)
|
|
||||||
return self.filter(title__icontains=q)
|
return self.filter(title__icontains=q)
|
||||||
|
|
||||||
|
|
||||||
class BasePage(models.Model):
|
class BasePage(Renderable, models.Model):
|
||||||
"""Base class for publishable content."""
|
"""Base class for publishable content."""
|
||||||
|
|
||||||
STATUS_DRAFT = 0x00
|
STATUS_DRAFT = 0x00
|
||||||
|
@ -80,18 +86,8 @@ class BasePage(models.Model):
|
||||||
(STATUS_TRASH, _("trash")),
|
(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)
|
title = models.CharField(max_length=100)
|
||||||
slug = models.SlugField(
|
slug = models.SlugField(_("slug"), max_length=120, blank=True, unique=True, db_index=True)
|
||||||
_("slug"), max_length=120, blank=True, unique=True, db_index=True
|
|
||||||
)
|
|
||||||
status = models.PositiveSmallIntegerField(
|
status = models.PositiveSmallIntegerField(
|
||||||
_("status"),
|
_("status"),
|
||||||
default=STATUS_DRAFT,
|
default=STATUS_DRAFT,
|
||||||
|
@ -103,40 +99,42 @@ class BasePage(models.Model):
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
)
|
)
|
||||||
content = RichTextUploadingField(
|
content = models.TextField(_("content"), blank=True, null=True)
|
||||||
_("content"),
|
|
||||||
blank=True,
|
|
||||||
null=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
objects = BasePageQuerySet.as_manager()
|
objects = BasePageQuerySet.as_manager()
|
||||||
|
|
||||||
detail_url_name = None
|
detail_url_name = None
|
||||||
item_template_name = "aircox/widgets/page_item.html"
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
abstract = True
|
abstract = True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cover_url(self):
|
||||||
|
return self.cover_id and self.cover.url
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "{}".format(self.title or self.pk)
|
return "{}".format(self.title or self.pk)
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
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:
|
if not self.slug:
|
||||||
self.slug = slugify(self.title)[:100]
|
self.slug = slugify(self.title)[:100]
|
||||||
count = Page.objects.filter(slug__startswith=self.slug).count()
|
count = Page.objects.filter(slug__startswith=self.slug).count()
|
||||||
if count:
|
if count:
|
||||||
self.slug += "-" + str(count)
|
self.slug += "-" + str(count)
|
||||||
|
|
||||||
if self.parent and not self.cover:
|
|
||||||
self.cover = self.parent.cover
|
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return (
|
if self.is_published:
|
||||||
reverse(self.detail_url_name, kwargs={"slug": self.slug})
|
return reverse(self.detail_url_name, kwargs={"slug": self.slug})
|
||||||
if self.is_published
|
return ""
|
||||||
else "#"
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_draft(self):
|
def is_draft(self):
|
||||||
|
@ -152,17 +150,24 @@ class BasePage(models.Model):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def display_title(self):
|
def display_title(self):
|
||||||
if self.is_published():
|
return self.is_published and self.title or ""
|
||||||
return self.title
|
|
||||||
return self.parent.display_title()
|
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def headline(self):
|
def display_headline(self):
|
||||||
if not self.content:
|
|
||||||
return ""
|
|
||||||
content = bleach.clean(self.content, tags=[], strip=True)
|
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)
|
headline = headline_re.search(content)
|
||||||
return mark_safe(headline.groupdict()["headline"]) if headline else ""
|
if not headline:
|
||||||
|
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))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_init_kwargs_from(cls, page, **kwargs):
|
def get_init_kwargs_from(cls, page, **kwargs):
|
||||||
|
@ -175,11 +180,10 @@ class BasePage(models.Model):
|
||||||
return cls(**cls.get_init_kwargs_from(page, **kwargs))
|
return cls(**cls.get_init_kwargs_from(page, **kwargs))
|
||||||
|
|
||||||
|
|
||||||
|
# FIXME: rename
|
||||||
class PageQuerySet(BasePageQuerySet):
|
class PageQuerySet(BasePageQuerySet):
|
||||||
def published(self):
|
def published(self):
|
||||||
return self.filter(
|
return self.filter(status=Page.STATUS_PUBLISHED, pub_date__lte=tz.now())
|
||||||
status=Page.STATUS_PUBLISHED, pub_date__lte=tz.now()
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class Page(BasePage):
|
class Page(BasePage):
|
||||||
|
@ -193,9 +197,7 @@ class Page(BasePage):
|
||||||
null=True,
|
null=True,
|
||||||
db_index=True,
|
db_index=True,
|
||||||
)
|
)
|
||||||
pub_date = models.DateTimeField(
|
pub_date = models.DateTimeField(_("publication date"), blank=True, null=True, db_index=True)
|
||||||
_("publication date"), blank=True, null=True, db_index=True
|
|
||||||
)
|
|
||||||
featured = models.BooleanField(
|
featured = models.BooleanField(
|
||||||
_("featured"),
|
_("featured"),
|
||||||
default=False,
|
default=False,
|
||||||
|
@ -207,18 +209,67 @@ class Page(BasePage):
|
||||||
|
|
||||||
objects = PageQuerySet.as_manager()
|
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:
|
class Meta:
|
||||||
verbose_name = _("Publication")
|
verbose_name = _("Publication")
|
||||||
verbose_name_plural = _("Publications")
|
verbose_name_plural = _("Publications")
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.__initial_cover = self.cover
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
if self.is_published and self.pub_date is None:
|
if self.is_published and self.pub_date is None:
|
||||||
self.pub_date = tz.now()
|
self.pub_date = tz.now()
|
||||||
elif not self.is_published:
|
elif not self.is_published:
|
||||||
self.pub_date = None
|
self.pub_date = None
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
if self.parent and not self.category:
|
|
||||||
self.category = self.parent.category
|
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
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
@ -227,45 +278,37 @@ class StaticPage(BasePage):
|
||||||
|
|
||||||
detail_url_name = "static-page-detail"
|
detail_url_name = "static-page-detail"
|
||||||
|
|
||||||
ATTACH_TO_HOME = 0x00
|
class Target(models.TextChoices):
|
||||||
ATTACH_TO_DIFFUSIONS = 0x01
|
NONE = "", _("None")
|
||||||
ATTACH_TO_LOGS = 0x02
|
HOME = "home", _("Home Page")
|
||||||
ATTACH_TO_PROGRAMS = 0x03
|
TIMETABLE = "timetable-list", _("Timetable")
|
||||||
ATTACH_TO_EPISODES = 0x04
|
PROGRAMS = "program-list", _("Programs list")
|
||||||
ATTACH_TO_ARTICLES = 0x05
|
EPISODES = "episode-list", _("Episodes list")
|
||||||
|
ARTICLES = "article-list", _("Articles list")
|
||||||
|
PAGES = "page-list", _("Publications list")
|
||||||
|
PODCASTS = "podcast-list", _("Podcasts list")
|
||||||
|
|
||||||
ATTACH_TO_CHOICES = (
|
attach_to = models.CharField(
|
||||||
(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"),
|
_("attach to"),
|
||||||
choices=ATTACH_TO_CHOICES,
|
choices=Target.choices,
|
||||||
|
max_length=32,
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
help_text=_("display this page content to related element"),
|
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):
|
def get_absolute_url(self):
|
||||||
if self.attach_to:
|
if self.attach_to:
|
||||||
return reverse(self.VIEWS[self.attach_to])
|
return reverse(self.attach_to)
|
||||||
return super().get_absolute_url()
|
return super().get_absolute_url()
|
||||||
|
|
||||||
|
|
||||||
class Comment(models.Model):
|
class Comment(Renderable, models.Model):
|
||||||
page = models.ForeignKey(
|
page = models.ForeignKey(
|
||||||
Page,
|
Page,
|
||||||
models.CASCADE,
|
models.CASCADE,
|
||||||
|
@ -278,7 +321,7 @@ class Comment(models.Model):
|
||||||
date = models.DateTimeField(auto_now_add=True)
|
date = models.DateTimeField(auto_now_add=True)
|
||||||
content = models.TextField(_("content"), max_length=1024)
|
content = models.TextField(_("content"), max_length=1024)
|
||||||
|
|
||||||
item_template_name = "aircox/widgets/comment_item.html"
|
template_prefix = "comment"
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def parent(self):
|
def parent(self):
|
||||||
|
@ -286,7 +329,7 @@ class Comment(models.Model):
|
||||||
return Page.objects.select_subclasses().filter(id=self.page_id).first()
|
return Page.objects.select_subclasses().filter(id=self.page_id).first()
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return self.parent.get_absolute_url()
|
return self.parent.get_absolute_url() + f"#{self._meta.label_lower}-{self.pk}"
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("Comment")
|
verbose_name = _("Comment")
|
||||||
|
@ -296,12 +339,10 @@ class Comment(models.Model):
|
||||||
class NavItem(models.Model):
|
class NavItem(models.Model):
|
||||||
"""Navigation menu items."""
|
"""Navigation menu items."""
|
||||||
|
|
||||||
station = models.ForeignKey(
|
station = models.ForeignKey(Station, models.CASCADE, verbose_name=_("station"))
|
||||||
Station, models.CASCADE, verbose_name=_("station")
|
|
||||||
)
|
|
||||||
menu = models.SlugField(_("menu"), max_length=24)
|
menu = models.SlugField(_("menu"), max_length=24)
|
||||||
order = models.PositiveSmallIntegerField(_("order"))
|
order = models.PositiveSmallIntegerField(_("order"))
|
||||||
text = models.CharField(_("title"), max_length=64)
|
text = models.CharField(_("title"), max_length=64, blank=True, null=True)
|
||||||
url = models.CharField(_("url"), max_length=256, blank=True, null=True)
|
url = models.CharField(_("url"), max_length=256, blank=True, null=True)
|
||||||
page = models.ForeignKey(
|
page = models.ForeignKey(
|
||||||
StaticPage,
|
StaticPage,
|
||||||
|
@ -318,24 +359,23 @@ class NavItem(models.Model):
|
||||||
ordering = ("order", "pk")
|
ordering = ("order", "pk")
|
||||||
|
|
||||||
def get_url(self):
|
def get_url(self):
|
||||||
return (
|
return self.url if self.url else self.page.get_absolute_url() if self.page else None
|
||||||
self.url
|
|
||||||
if self.url
|
def get_label(self):
|
||||||
else self.page.get_absolute_url()
|
if self.text:
|
||||||
if self.page
|
return self.text
|
||||||
else None
|
elif self.page:
|
||||||
)
|
return self.page.title
|
||||||
|
|
||||||
def render(self, request, css_class="", active_class=""):
|
def render(self, request, css_class="", active_class=""):
|
||||||
url = self.get_url()
|
url = self.get_url()
|
||||||
|
label = self.get_label()
|
||||||
if active_class and request.path.startswith(url):
|
if active_class and request.path.startswith(url):
|
||||||
css_class += " " + active_class
|
css_class += " " + active_class
|
||||||
|
|
||||||
if not url:
|
if not url:
|
||||||
return self.text
|
return label
|
||||||
elif not css_class:
|
elif not css_class:
|
||||||
return format_html('<a href="{}">{}</a>', url, self.text)
|
return format_html('<a href="{}">{}</a>', url, label)
|
||||||
else:
|
else:
|
||||||
return format_html(
|
return format_html('<a href="{}" class="{}">{}</a>', url, css_class, label)
|
||||||
'<a href="{}" class="{}">{}</a>', url, css_class, self.text
|
|
||||||
)
|
|
||||||
|
|
|
@ -1,11 +1,8 @@
|
||||||
import logging
|
|
||||||
import os
|
import os
|
||||||
import shutil
|
|
||||||
|
|
||||||
from django.conf import settings as conf
|
from django.conf import settings as conf
|
||||||
|
from django.contrib.auth.models import Group
|
||||||
from django.db import models
|
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 django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from aircox.conf import settings
|
from aircox.conf import settings
|
||||||
|
@ -13,13 +10,11 @@ from aircox.conf import settings
|
||||||
from .page import Page, PageQuerySet
|
from .page import Page, PageQuerySet
|
||||||
from .station import Station
|
from .station import Station
|
||||||
|
|
||||||
logger = logging.getLogger("aircox")
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
|
"ProgramQuerySet",
|
||||||
"Program",
|
"Program",
|
||||||
"ProgramChildQuerySet",
|
"ProgramChildQuerySet",
|
||||||
"ProgramQuerySet",
|
|
||||||
"Stream",
|
"Stream",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -32,6 +27,16 @@ class ProgramQuerySet(PageQuerySet):
|
||||||
def active(self):
|
def active(self):
|
||||||
return self.filter(active=True)
|
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 = user.groups.all()
|
||||||
|
return self.filter(editors_group__in=groups)
|
||||||
|
|
||||||
|
|
||||||
class Program(Page):
|
class Program(Page):
|
||||||
"""A Program can either be a Streamed or a Scheduled program.
|
"""A Program can either be a Streamed or a Scheduled program.
|
||||||
|
@ -47,9 +52,7 @@ class Program(Page):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# explicit foreign key in order to avoid related name clashes
|
# explicit foreign key in order to avoid related name clashes
|
||||||
station = models.ForeignKey(
|
station = models.ForeignKey(Station, models.CASCADE, verbose_name=_("station"))
|
||||||
Station, models.CASCADE, verbose_name=_("station")
|
|
||||||
)
|
|
||||||
active = models.BooleanField(
|
active = models.BooleanField(
|
||||||
_("active"),
|
_("active"),
|
||||||
default=True,
|
default=True,
|
||||||
|
@ -60,9 +63,12 @@ class Program(Page):
|
||||||
default=True,
|
default=True,
|
||||||
help_text=_("update later diffusions according to schedule changes"),
|
help_text=_("update later diffusions according to schedule changes"),
|
||||||
)
|
)
|
||||||
|
editors_group = models.ForeignKey(Group, models.CASCADE, verbose_name=_("editors"))
|
||||||
|
|
||||||
objects = ProgramQuerySet.as_manager()
|
objects = ProgramQuerySet.as_manager()
|
||||||
detail_url_name = "program-detail"
|
detail_url_name = "program-detail"
|
||||||
|
list_url_name = "program-list"
|
||||||
|
edit_url_name = "program-edit"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def path(self):
|
def path(self):
|
||||||
|
@ -82,11 +88,10 @@ class Program(Page):
|
||||||
def excerpts_path(self):
|
def excerpts_path(self):
|
||||||
return os.path.join(self.path, settings.SOUND_ARCHIVES_SUBDIR)
|
return os.path.join(self.path, settings.SOUND_ARCHIVES_SUBDIR)
|
||||||
|
|
||||||
def __init__(self, *kargs, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*kargs, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
if self.slug:
|
if self.slug:
|
||||||
self.__initial_path = self.path
|
self.__initial_path = self.path
|
||||||
self.__initial_cover = self.cover
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_from_path(cl, path):
|
def get_from_path(cl, path):
|
||||||
|
@ -118,34 +123,21 @@ class Program(Page):
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.title
|
return self.title
|
||||||
|
|
||||||
def save(self, *kargs, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
from .sound import Sound
|
if not self.editors_group_id:
|
||||||
|
from aircox import permissions
|
||||||
|
|
||||||
super().save(*kargs, **kwargs)
|
saved = permissions.program.init(self)
|
||||||
|
if saved:
|
||||||
|
return
|
||||||
|
|
||||||
# TODO: move in signals
|
super().save()
|
||||||
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):
|
class ProgramChildQuerySet(PageQuerySet):
|
||||||
def station(self, station=None, id=None):
|
def station(self, station=None, id=None):
|
||||||
|
# lookup `__program` is due to parent being a page subclass (page is
|
||||||
|
# concrete).
|
||||||
return (
|
return (
|
||||||
self.filter(parent__program__station=station)
|
self.filter(parent__program__station=station)
|
||||||
if id is None
|
if id is None
|
||||||
|
@ -155,6 +147,10 @@ class ProgramChildQuerySet(PageQuerySet):
|
||||||
def program(self, program=None, id=None):
|
def program(self, program=None, id=None):
|
||||||
return self.parent(program, id)
|
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):
|
class Stream(models.Model):
|
||||||
"""When there are no program scheduled, it is possible to play sounds in
|
"""When there are no program scheduled, it is possible to play sounds in
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.db.models import F, Q
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from .program import Program
|
from .program import Program
|
||||||
|
@ -15,18 +16,10 @@ class RerunQuerySet(models.QuerySet):
|
||||||
"""Queryset for Rerun (sub)classes."""
|
"""Queryset for Rerun (sub)classes."""
|
||||||
|
|
||||||
def station(self, station=None, id=None):
|
def station(self, station=None, id=None):
|
||||||
return (
|
return self.filter(program__station=station) if id is None else self.filter(program__station__id=id)
|
||||||
self.filter(program__station=station)
|
|
||||||
if id is None
|
|
||||||
else self.filter(program__station__id=id)
|
|
||||||
)
|
|
||||||
|
|
||||||
def program(self, program=None, id=None):
|
def program(self, program=None, id=None):
|
||||||
return (
|
return self.filter(program=program) if id is None else self.filter(program__id=id)
|
||||||
self.filter(program=program)
|
|
||||||
if id is None
|
|
||||||
else self.filter(program__id=id)
|
|
||||||
)
|
|
||||||
|
|
||||||
def rerun(self):
|
def rerun(self):
|
||||||
return self.filter(initial__isnull=False)
|
return self.filter(initial__isnull=False)
|
||||||
|
@ -53,7 +46,7 @@ class Rerun(models.Model):
|
||||||
models.SET_NULL,
|
models.SET_NULL,
|
||||||
related_name="rerun_set",
|
related_name="rerun_set",
|
||||||
verbose_name=_("rerun of"),
|
verbose_name=_("rerun of"),
|
||||||
limit_choices_to={"initial__isnull": True},
|
limit_choices_to=Q(initial__isnull=True) & Q(program=F("program")),
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
db_index=True,
|
db_index=True,
|
||||||
|
@ -78,17 +71,14 @@ class Rerun(models.Model):
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
super().clean()
|
super().clean()
|
||||||
if (
|
if hasattr(self, "start") and self.initial is not None and self.initial.start >= self.start:
|
||||||
hasattr(self, "start")
|
raise ValidationError({"initial": _("rerun must happen after original")})
|
||||||
and self.initial is not None
|
|
||||||
and self.initial.start >= self.start
|
|
||||||
):
|
|
||||||
raise ValidationError(
|
|
||||||
{"initial": _("rerun must happen after original")}
|
|
||||||
)
|
|
||||||
|
|
||||||
def save_rerun(self):
|
def save_rerun(self):
|
||||||
self.program = self.initial.program
|
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")
|
||||||
|
|
||||||
def save_initial(self):
|
def save_initial(self):
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -42,6 +42,7 @@ class Schedule(Rerun):
|
||||||
second_and_fourth = 0b001010, _("2nd and 4th {day} of the month")
|
second_and_fourth = 0b001010, _("2nd and 4th {day} of the month")
|
||||||
every = 0b011111, _("{day}")
|
every = 0b011111, _("{day}")
|
||||||
one_on_two = 0b100000, _("one {day} on two")
|
one_on_two = 0b100000, _("one {day} on two")
|
||||||
|
# every_weekday = 0b10000000 _("from Monday to Friday")
|
||||||
|
|
||||||
date = models.DateField(
|
date = models.DateField(
|
||||||
_("date"),
|
_("date"),
|
||||||
|
@ -55,7 +56,7 @@ class Schedule(Rerun):
|
||||||
_("timezone"),
|
_("timezone"),
|
||||||
default=current_timezone_key,
|
default=current_timezone_key,
|
||||||
max_length=100,
|
max_length=100,
|
||||||
choices=[(x, x) for x in zoneinfo.available_timezones()],
|
choices=sorted([(x, x) for x in zoneinfo.available_timezones()]),
|
||||||
help_text=_("timezone used for the date"),
|
help_text=_("timezone used for the date"),
|
||||||
)
|
)
|
||||||
duration = models.TimeField(
|
duration = models.TimeField(
|
||||||
|
@ -71,6 +72,10 @@ class Schedule(Rerun):
|
||||||
verbose_name = _("Schedule")
|
verbose_name = _("Schedule")
|
||||||
verbose_name_plural = _("Schedules")
|
verbose_name_plural = _("Schedules")
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self._initial = kwargs
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "{} - {}, {}".format(
|
return "{} - {}, {}".format(
|
||||||
self.program.title,
|
self.program.title,
|
||||||
|
@ -102,11 +107,7 @@ class Schedule(Rerun):
|
||||||
"""Return frequency formated for display."""
|
"""Return frequency formated for display."""
|
||||||
from django.template.defaultfilters import date
|
from django.template.defaultfilters import date
|
||||||
|
|
||||||
return (
|
return self._get_FIELD_display(self._meta.get_field("frequency")).format(day=date(self.date, "l")).capitalize()
|
||||||
self._get_FIELD_display(self._meta.get_field("frequency"))
|
|
||||||
.format(day=date(self.date, "l"))
|
|
||||||
.capitalize()
|
|
||||||
)
|
|
||||||
|
|
||||||
def normalize(self, date):
|
def normalize(self, date):
|
||||||
"""Return a datetime set to schedule's time for the provided date,
|
"""Return a datetime set to schedule's time for the provided date,
|
||||||
|
@ -114,19 +115,29 @@ class Schedule(Rerun):
|
||||||
date = tz.datetime.combine(date, self.time)
|
date = tz.datetime.combine(date, self.time)
|
||||||
return date.replace(tzinfo=self.tz)
|
return date.replace(tzinfo=self.tz)
|
||||||
|
|
||||||
def dates_of_month(self, date):
|
def dates_of_month(self, date, frequency=None, sched_date=None):
|
||||||
"""Return normalized diffusion dates of provided date's month."""
|
"""Return normalized diffusion dates of provided date's month.
|
||||||
if self.frequency == Schedule.Frequency.ponctual:
|
|
||||||
|
: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:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
sched_wday, freq = self.date.weekday(), self.frequency
|
sched_wday = sched_date.weekday()
|
||||||
date = date.replace(day=1)
|
date = date.replace(day=1)
|
||||||
|
|
||||||
# last of the month
|
# last of the month
|
||||||
if freq == Schedule.Frequency.last:
|
if frequency == Schedule.Frequency.last:
|
||||||
date = date.replace(
|
date = date.replace(day=calendar.monthrange(date.year, date.month)[1])
|
||||||
day=calendar.monthrange(date.year, date.month)[1]
|
|
||||||
)
|
|
||||||
date_wday = date.weekday()
|
date_wday = date.weekday()
|
||||||
|
|
||||||
# end of month before the wanted weekday: move one week back
|
# end of month before the wanted weekday: move one week back
|
||||||
|
@ -138,57 +149,53 @@ class Schedule(Rerun):
|
||||||
# move to the first day of the month that matches the schedule's
|
# move to the first day of the month that matches the schedule's
|
||||||
# weekday. Check on SO#3284452 for the formula
|
# weekday. Check on SO#3284452 for the formula
|
||||||
date_wday, month = date.weekday(), date.month
|
date_wday, month = date.weekday(), date.month
|
||||||
date += tz.timedelta(
|
date += tz.timedelta(days=(7 if date_wday > sched_wday else 0) - date_wday + sched_wday)
|
||||||
days=(7 if date_wday > sched_wday else 0) - date_wday + sched_wday
|
|
||||||
)
|
|
||||||
|
|
||||||
if freq == Schedule.Frequency.one_on_two:
|
if frequency == Schedule.Frequency.one_on_two:
|
||||||
# - adjust date with modulo 14 (= 2 weeks in days)
|
# - adjust date with modulo 14 (= 2 weeks in days)
|
||||||
# - there are max 3 "weeks on two" per month
|
# - there are max 3 "weeks on two" per month
|
||||||
if (date - self.date).days % 14:
|
if (date - sched_date).days % 14:
|
||||||
date += tz.timedelta(days=7)
|
date += tz.timedelta(days=7)
|
||||||
dates = (date + tz.timedelta(days=14 * i) for i in range(0, 3))
|
dates = (date + tz.timedelta(days=14 * i) for i in range(0, 3))
|
||||||
else:
|
else:
|
||||||
dates = (
|
dates = (date + tz.timedelta(days=7 * week) for week in range(0, 5) if frequency & (0b1 << week))
|
||||||
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]
|
return [self.normalize(date) for date in dates if date.month == month]
|
||||||
|
|
||||||
def diffusions_of_month(self, date):
|
def diffusions_of_month(self, date, frequency=None, sched_date=None):
|
||||||
"""Get episodes and diffusions for month of provided date, including
|
"""Get episodes and diffusions for month of provided date, including
|
||||||
reruns.
|
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])
|
:returns: tuple([Episode], [Diffusion])
|
||||||
"""
|
"""
|
||||||
from .diffusion import Diffusion
|
from .diffusion import Diffusion
|
||||||
from .episode import Episode
|
from .episode import Episode
|
||||||
|
|
||||||
if (
|
if frequency is None:
|
||||||
self.initial is not None
|
frequency = self.frequency
|
||||||
or self.frequency == Schedule.Frequency.ponctual
|
|
||||||
):
|
if sched_date is None:
|
||||||
|
sched_date = self.date
|
||||||
|
|
||||||
|
if self.initial is not None or frequency == Schedule.Frequency.ponctual:
|
||||||
return [], []
|
return [], []
|
||||||
|
|
||||||
# dates for self and reruns as (date, initial)
|
# dates for self and reruns as (date, initial)
|
||||||
reruns = [
|
reruns = [(rerun, rerun.date - sched_date) for rerun in self.rerun_set.all()]
|
||||||
(rerun, rerun.date - self.date) for rerun in self.rerun_set.all()
|
|
||||||
]
|
|
||||||
|
|
||||||
dates = {date: None for date in self.dates_of_month(date)}
|
dates = {date: None for date in self.dates_of_month(date, frequency, sched_date)}
|
||||||
dates.update(
|
dates.update(
|
||||||
(rerun.normalize(date.date() + delta), date)
|
(rerun.normalize(date.date() + delta), date) for date in list(dates.keys()) for rerun, delta in reruns
|
||||||
for date in list(dates.keys())
|
|
||||||
for rerun, delta in reruns
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# remove dates corresponding to existing diffusions
|
# remove dates corresponding to existing diffusions
|
||||||
saved = set(
|
saved = set(
|
||||||
Diffusion.objects.filter(
|
Diffusion.objects.filter(start__in=dates.keys(), program=self.program, schedule=self).values_list(
|
||||||
start__in=dates.keys(), program=self.program, schedule=self
|
"start", flat=True
|
||||||
).values_list("start", flat=True)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# make diffs
|
# make diffs
|
||||||
|
|
|
@ -1,16 +1,27 @@
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
from django.conf import settings as conf
|
||||||
from django.contrib.auth.models import Group, Permission, User
|
from django.contrib.auth.models import Group, Permission, User
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.db.models import signals
|
from django.db.models import signals, F
|
||||||
|
from django.db.models.functions import Concat, Substr
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.utils import timezone as tz
|
from django.utils import timezone as tz
|
||||||
|
|
||||||
from aircox import utils
|
from aircox import utils
|
||||||
from aircox.conf import settings
|
from aircox.conf import settings
|
||||||
|
from .article import Article
|
||||||
from .diffusion import Diffusion
|
from .diffusion import Diffusion
|
||||||
from .episode import Episode
|
from .episode import Episode
|
||||||
from .page import Page
|
from .page import Page
|
||||||
from .program import Program
|
from .program import Program
|
||||||
from .schedule import Schedule
|
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
|
# Add a default group to a user when it is created. It also assigns a list
|
||||||
|
@ -32,42 +43,50 @@ def user_default_groups(sender, instance, created, *args, **kwargs):
|
||||||
group, created = Group.objects.get_or_create(name=group_name)
|
group, created = Group.objects.get_or_create(name=group_name)
|
||||||
if created and permissions:
|
if created and permissions:
|
||||||
for codename in permissions:
|
for codename in permissions:
|
||||||
permission = Permission.objects.filter(
|
permission = Permission.objects.filter(codename=codename).first()
|
||||||
codename=codename
|
|
||||||
).first()
|
|
||||||
if permission:
|
if permission:
|
||||||
group.permissions.add(permission)
|
group.permissions.add(permission)
|
||||||
group.save()
|
group.save()
|
||||||
instance.groups.add(group)
|
instance.groups.add(group)
|
||||||
|
|
||||||
|
|
||||||
|
# ---- page
|
||||||
@receiver(signals.post_save, sender=Page)
|
@receiver(signals.post_save, sender=Page)
|
||||||
def page_post_save(sender, instance, created, *args, **kwargs):
|
def page_post_save__child_page_defaults(sender, instance, created, *args, **kwargs):
|
||||||
if not created and instance.cover:
|
initial_cover = getattr(instance, "__initial_cover", None)
|
||||||
Page.objects.filter(parent=instance, cover__isnull=True).update(
|
if initial_cover is None and instance.cover is not None:
|
||||||
cover=instance.cover
|
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)
|
@receiver(signals.post_save, sender=Program)
|
||||||
def program_post_save(sender, instance, created, *args, **kwargs):
|
def program_post_save__mv_sounds(sender, instance, created, *args, **kwargs):
|
||||||
"""Clean-up later diffusions when a program becomes inactive."""
|
path_ = getattr(instance, "__initial_path", None)
|
||||||
if not instance.active:
|
if path_ in (None, instance.path):
|
||||||
Diffusion.objects.program(instance).after(tz.now()).delete()
|
return
|
||||||
Episode.objects.parent(instance).filter(
|
|
||||||
diffusion__isnull=True
|
|
||||||
).delete()
|
|
||||||
|
|
||||||
cover = getattr(instance, "__initial_cover", None)
|
abspath = path_ and os.path.join(conf.MEDIA_ROOT, path_)
|
||||||
if cover is None and instance.cover is not None:
|
if os.path.exists(abspath) and not os.path.exists(instance.abspath):
|
||||||
Episode.objects.parent(instance).filter(cover__isnull=True).update(
|
logger.info(
|
||||||
cover=instance.cover
|
f"program #{instance.pk}'s dir changed to {instance.title} - update it.", instance.id, instance.title
|
||||||
)
|
)
|
||||||
|
|
||||||
|
shutil.move(abspath, instance.abspath)
|
||||||
|
Sound.objects.filter(path__startswith=path_).update(file=Concat("file", Substr(F("file"), len(path_))))
|
||||||
|
|
||||||
|
|
||||||
|
# ---- schedule
|
||||||
@receiver(signals.pre_save, sender=Schedule)
|
@receiver(signals.pre_save, sender=Schedule)
|
||||||
def schedule_pre_save(sender, instance, *args, **kwargs):
|
def schedule_pre_save(sender, instance, *args, **kwargs):
|
||||||
if getattr(instance, "pk") is not None:
|
if getattr(instance, "pk") is not None and "raw" not in kwargs:
|
||||||
instance._initial = Schedule.objects.get(pk=instance.pk)
|
instance._initial = Schedule.objects.get(pk=instance.pk)
|
||||||
|
|
||||||
|
|
||||||
|
@ -77,8 +96,7 @@ def schedule_post_save(sender, instance, created, *args, **kwargs):
|
||||||
corresponding diffusions accordingly."""
|
corresponding diffusions accordingly."""
|
||||||
initial = getattr(instance, "_initial", None)
|
initial = getattr(instance, "_initial", None)
|
||||||
if not initial or (
|
if not initial or (
|
||||||
(instance.time, instance.duration, instance.timezone)
|
(instance.time, instance.duration, instance.timezone) == (initial.time, initial.duration, initial.timezone)
|
||||||
== (initial.time, initial.duration, initial.timezone)
|
|
||||||
):
|
):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -97,13 +115,23 @@ def schedule_post_save(sender, instance, created, *args, **kwargs):
|
||||||
def schedule_pre_delete(sender, instance, *args, **kwargs):
|
def schedule_pre_delete(sender, instance, *args, **kwargs):
|
||||||
"""Delete later corresponding diffusion to a changed schedule."""
|
"""Delete later corresponding diffusion to a changed schedule."""
|
||||||
Diffusion.objects.filter(schedule=instance).after(tz.now()).delete()
|
Diffusion.objects.filter(schedule=instance).after(tz.now()).delete()
|
||||||
Episode.objects.filter(
|
Episode.objects.filter(diffusion__isnull=True, content__isnull=True, episodesound__isnull=True).delete()
|
||||||
diffusion__isnull=True, content__isnull=True, sound__isnull=True
|
|
||||||
).delete()
|
|
||||||
|
|
||||||
|
|
||||||
|
# ---- diffusion
|
||||||
@receiver(signals.post_delete, sender=Diffusion)
|
@receiver(signals.post_delete, sender=Diffusion)
|
||||||
def diffusion_post_delete(sender, instance, *args, **kwargs):
|
def diffusion_post_delete(sender, instance, *args, **kwargs):
|
||||||
Episode.objects.filter(
|
Episode.objects.filter(diffusion__isnull=True, content__isnull=True, episodesound__isnull=True).delete()
|
||||||
diffusion__isnull=True, content__isnull=True, sound__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)
|
||||||
|
|
|
@ -1,322 +1,195 @@
|
||||||
import logging
|
from datetime import date
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
from django.conf import settings as conf
|
from django.conf import settings as conf
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import Q
|
|
||||||
from django.utils import timezone as tz
|
from django.utils import timezone as tz
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from taggit.managers import TaggableManager
|
|
||||||
|
|
||||||
|
from aircox import utils
|
||||||
from aircox.conf import settings
|
from aircox.conf import settings
|
||||||
|
|
||||||
from .episode import Episode
|
|
||||||
from .program import Program
|
from .program import Program
|
||||||
|
from .file import File, FileQuerySet
|
||||||
logger = logging.getLogger("aircox")
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = ("Sound", "SoundQuerySet", "Track")
|
__all__ = ("Sound", "SoundQuerySet")
|
||||||
|
|
||||||
|
|
||||||
class SoundQuerySet(models.QuerySet):
|
class SoundQuerySet(FileQuerySet):
|
||||||
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):
|
def downloadable(self):
|
||||||
"""Return sounds available as podcasts."""
|
"""Return sounds available as podcasts."""
|
||||||
return self.filter(is_downloadable=True)
|
return self.filter(is_downloadable=True)
|
||||||
|
|
||||||
def archive(self):
|
def broadcast(self):
|
||||||
"""Return sounds that are archives."""
|
"""Return sounds that are archives."""
|
||||||
return self.filter(type=Sound.TYPE_ARCHIVE)
|
return self.filter(broadcast=True, is_removed=False)
|
||||||
|
|
||||||
def path(self, paths):
|
def playlist(self, order_by="file"):
|
||||||
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
|
"""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:
|
if order_by:
|
||||||
self = self.order_by("file")
|
self = self.order_by(order_by)
|
||||||
return [
|
return [
|
||||||
os.path.join(conf.MEDIA_ROOT, file)
|
os.path.join(conf.MEDIA_ROOT, file)
|
||||||
for file in self.filter(file__isnull=False).values_list(
|
for file in self.filter(file__isnull=False).values_list("file", flat=True)
|
||||||
"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 = models.TimeField(
|
||||||
_("duration"),
|
_("duration"),
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
help_text=_("duration of the sound"),
|
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(
|
is_good_quality = models.BooleanField(
|
||||||
_("good quality"),
|
_("good quality"),
|
||||||
help_text=_("sound meets quality requirements"),
|
help_text=_("sound meets quality requirements"),
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
)
|
)
|
||||||
is_public = models.BooleanField(
|
|
||||||
_("public"),
|
|
||||||
help_text=_("whether it is publicly available as podcast"),
|
|
||||||
default=False,
|
|
||||||
)
|
|
||||||
is_downloadable = models.BooleanField(
|
is_downloadable = models.BooleanField(
|
||||||
_("downloadable"),
|
_("downloadable"),
|
||||||
help_text=_(
|
help_text=_("Sound can be downloaded by website visitors."),
|
||||||
"whether it can be publicly downloaded by visitors (sound must be "
|
|
||||||
"public)"
|
|
||||||
),
|
|
||||||
default=False,
|
default=False,
|
||||||
)
|
)
|
||||||
|
broadcast = models.BooleanField(
|
||||||
|
_("Broadcast"),
|
||||||
|
default=False,
|
||||||
|
help_text=_("The sound is broadcasted on air"),
|
||||||
|
)
|
||||||
|
|
||||||
objects = SoundQuerySet.as_manager()
|
objects = SoundQuerySet.as_manager()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("Sound")
|
verbose_name = _("Sound file")
|
||||||
verbose_name_plural = _("Sounds")
|
verbose_name_plural = _("Sound files")
|
||||||
|
|
||||||
@property
|
_path_re = re.compile(
|
||||||
def url(self):
|
"^(?P<year>[0-9]{4})(?P<month>[0-9]{2})(?P<day>[0-9]{2})"
|
||||||
return self.file and self.file.url
|
"(_(?P<hour>[0-9]{2})h(?P<minute>[0-9]{2}))?"
|
||||||
|
"(_(?P<n>[0-9]+))?"
|
||||||
|
"_?[ -]*(?P<name>.*)$"
|
||||||
|
)
|
||||||
|
|
||||||
def __str__(self):
|
@classmethod
|
||||||
return "/".join(self.file.path.split("/")[-3:])
|
def read_path(cls, path):
|
||||||
|
"""Parse path name returning dictionary of extracted info. It can
|
||||||
|
contain:
|
||||||
|
|
||||||
def save(self, check=True, *args, **kwargs):
|
- `year`, `month`, `day`: diffusion date
|
||||||
if self.episode is not None and self.program is None:
|
- `hour`, `minute`: diffusion time
|
||||||
self.program = self.episode.program
|
- `n`: sound arbitrary number (used for sound ordering)
|
||||||
if check:
|
- `name`: cleaned name extracted or file name (without extension)
|
||||||
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():
|
basename = os.path.basename(path)
|
||||||
if self.type == self.TYPE_REMOVED:
|
basename = os.path.splitext(basename)[0]
|
||||||
return
|
reg_match = cls._path_re.search(basename)
|
||||||
logger.debug("sound %s: has been removed", self.file.name)
|
if reg_match:
|
||||||
self.type = self.TYPE_REMOVED
|
info = reg_match.groupdict()
|
||||||
return True
|
for k in ("year", "month", "day", "hour", "minute", "n"):
|
||||||
|
if info.get(k) is not None:
|
||||||
|
info[k] = int(info[k])
|
||||||
|
|
||||||
# not anymore removed
|
name = info.get("name")
|
||||||
changed = False
|
info["name"] = name and cls._as_name(name) or basename
|
||||||
|
else:
|
||||||
|
info = {"name": basename}
|
||||||
|
return info
|
||||||
|
|
||||||
if self.type == self.TYPE_REMOVED and self.program:
|
@classmethod
|
||||||
changed = True
|
def _as_name(cls, name):
|
||||||
self.type = (
|
name = name.replace("_", " ")
|
||||||
self.TYPE_ARCHIVE
|
return " ".join(r.capitalize() for r in name.split(" "))
|
||||||
if self.file.name.startswith(self.program.archives_path)
|
|
||||||
else self.TYPE_EXCERPT
|
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
|
||||||
# check mtime -> reset quality if changed (assume file changed)
|
info = "{} ({})".format(album, year) if album and year else album or year or ""
|
||||||
mtime = self.get_mtime()
|
track = Track(
|
||||||
|
sound=self,
|
||||||
if self.mtime != mtime:
|
position=int(meta.tags.get("tracknumber", 0)),
|
||||||
self.mtime = mtime
|
title=title,
|
||||||
self.is_good_quality = None
|
artist=artist or _("unknown"),
|
||||||
logger.debug(
|
info=info,
|
||||||
"sound %s: m_time has changed. Reset quality info",
|
|
||||||
self.file.name,
|
|
||||||
)
|
)
|
||||||
return True
|
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
|
return changed
|
||||||
|
|
||||||
def __check_name(self):
|
def read_metadata(self):
|
||||||
if not self.name and self.file and self.file.name:
|
import mutagen
|
||||||
# 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):
|
meta = mutagen.File(self.file.path)
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
self.__check_name()
|
|
||||||
|
|
||||||
|
metadata = {"duration": utils.seconds_to_time(meta.info.length), "meta": meta}
|
||||||
|
|
||||||
class Track(models.Model):
|
path_info = self.read_path(self.file.path)
|
||||||
"""Track of a playlist of an object.
|
if name := path_info.get("name"):
|
||||||
|
metadata["name"] = name
|
||||||
The position can either be expressed as the position in the playlist
|
return metadata
|
||||||
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):
|
def __str__(self):
|
||||||
return "{self.artist} -- {self.title} -- {self.position}".format(
|
infos = ""
|
||||||
self=self
|
if self.is_removed:
|
||||||
)
|
infos += _("removed")
|
||||||
|
if infos:
|
||||||
def save(self, *args, **kwargs):
|
return f"{self.file.name} [{infos}]"
|
||||||
if (self.sound is None and self.episode is None) or (
|
return f"{self.file.name}"
|
||||||
self.sound is not None and self.episode is not None
|
|
||||||
):
|
|
||||||
raise ValueError("sound XOR episode is required")
|
|
||||||
super().save(*args, **kwargs)
|
|
||||||
|
|
|
@ -1,11 +1,8 @@
|
||||||
import os
|
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from filer.fields.image import FilerImageField
|
from filer.fields.image import FilerImageField
|
||||||
|
|
||||||
from aircox.conf import settings
|
|
||||||
|
|
||||||
__all__ = ("Station", "StationQuerySet", "Port")
|
__all__ = ("Station", "StationQuerySet", "Port")
|
||||||
|
|
||||||
|
@ -32,13 +29,6 @@ class Station(models.Model):
|
||||||
|
|
||||||
name = models.CharField(_("name"), max_length=64)
|
name = models.CharField(_("name"), max_length=64)
|
||||||
slug = models.SlugField(_("slug"), max_length=64, unique=True)
|
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 = models.BooleanField(
|
||||||
_("default station"),
|
_("default station"),
|
||||||
default=False,
|
default=False,
|
||||||
|
@ -67,9 +57,7 @@ class Station(models.Model):
|
||||||
max_length=2048,
|
max_length=2048,
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text=_(
|
help_text=_("Audio streams urls used by station's player. One url a line."),
|
||||||
"Audio streams urls used by station's player. One url " "a line."
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
default_cover = FilerImageField(
|
default_cover = FilerImageField(
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
|
@ -78,6 +66,14 @@ class Station(models.Model):
|
||||||
blank=True,
|
blank=True,
|
||||||
related_name="+",
|
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()
|
objects = StationQuerySet.as_manager()
|
||||||
|
|
||||||
|
@ -90,12 +86,6 @@ class Station(models.Model):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
def save(self, make_sources=True, *args, **kwargs):
|
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:
|
if self.default:
|
||||||
qs = Station.objects.filter(default=True)
|
qs = Station.objects.filter(default=True)
|
||||||
if self.pk is not None:
|
if self.pk is not None:
|
||||||
|
@ -153,16 +143,10 @@ class Port(models.Model):
|
||||||
(TYPE_FILE, _("file")),
|
(TYPE_FILE, _("file")),
|
||||||
)
|
)
|
||||||
|
|
||||||
station = models.ForeignKey(
|
station = models.ForeignKey(Station, models.CASCADE, verbose_name=_("station"))
|
||||||
Station, models.CASCADE, verbose_name=_("station")
|
direction = models.SmallIntegerField(_("direction"), choices=DIRECTION_CHOICES)
|
||||||
)
|
|
||||||
direction = models.SmallIntegerField(
|
|
||||||
_("direction"), choices=DIRECTION_CHOICES
|
|
||||||
)
|
|
||||||
type = models.SmallIntegerField(_("type"), choices=TYPE_CHOICES)
|
type = models.SmallIntegerField(_("type"), choices=TYPE_CHOICES)
|
||||||
active = models.BooleanField(
|
active = models.BooleanField(_("active"), default=True, help_text=_("this port is active"))
|
||||||
_("active"), default=True, help_text=_("this port is active")
|
|
||||||
)
|
|
||||||
settings = models.TextField(
|
settings = models.TextField(
|
||||||
_("port settings"),
|
_("port settings"),
|
||||||
help_text=_(
|
help_text=_(
|
||||||
|
@ -193,8 +177,6 @@ class Port(models.Model):
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
if not self.is_valid_type():
|
if not self.is_valid_type():
|
||||||
raise ValueError(
|
raise ValueError("port type is not allowed with the given port direction")
|
||||||
"port type is not allowed with the given port direction"
|
|
||||||
)
|
|
||||||
|
|
||||||
return super().save(*args, **kwargs)
|
return super().save(*args, **kwargs)
|
||||||
|
|
72
aircox/models/track.py
Normal file
72
aircox/models/track.py
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
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,7 +14,5 @@ class UserSettings(models.Model):
|
||||||
verbose_name=_("User"),
|
verbose_name=_("User"),
|
||||||
related_name="aircox_settings",
|
related_name="aircox_settings",
|
||||||
)
|
)
|
||||||
playlist_editor_columns = models.JSONField(_("Playlist Editor Columns"))
|
tracklist_editor_columns = models.JSONField(_("Playlist Editor Columns"))
|
||||||
playlist_editor_sep = models.CharField(
|
tracklist_editor_sep = models.CharField(_("Playlist Editor Separator"), max_length=16)
|
||||||
_("Playlist Editor Separator"), max_length=16
|
|
||||||
)
|
|
||||||
|
|
89
aircox/permissions.py
Normal file
89
aircox/permissions.py
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
# 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,12 +1,18 @@
|
||||||
|
from . import auth
|
||||||
from .admin import TrackSerializer, UserSettingsSerializer
|
from .admin import TrackSerializer, UserSettingsSerializer
|
||||||
|
from .episode import EpisodeSoundSerializer, EpisodeSerializer
|
||||||
from .log import LogInfo, LogInfoSerializer
|
from .log import LogInfo, LogInfoSerializer
|
||||||
from .sound import PodcastSerializer, SoundSerializer
|
from .page import CommentSerializer
|
||||||
|
from .sound import SoundSerializer
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
"TrackSerializer",
|
"auth",
|
||||||
"UserSettingsSerializer",
|
"CommentSerializer",
|
||||||
"LogInfo",
|
"LogInfo",
|
||||||
"LogInfoSerializer",
|
"LogInfoSerializer",
|
||||||
|
"EpisodeSoundSerializer",
|
||||||
|
"EpisodeSerializer",
|
||||||
"SoundSerializer",
|
"SoundSerializer",
|
||||||
"PodcastSerializer",
|
"TrackSerializer",
|
||||||
|
"UserSettingsSerializer",
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,9 +1,17 @@
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from filer.models.imagemodels import Image
|
||||||
from taggit.serializers import TaggitSerializer, TagListSerializerField
|
from taggit.serializers import TaggitSerializer, TagListSerializerField
|
||||||
|
|
||||||
from ..models import Track, UserSettings
|
from ..models import Track, UserSettings
|
||||||
|
|
||||||
__all__ = ("TrackSerializer", "UserSettingsSerializer")
|
__all__ = ("ImageSerializer", "TrackSerializer", "UserSettingsSerializer")
|
||||||
|
|
||||||
|
|
||||||
|
class ImageSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Image
|
||||||
|
fields = "__all__"
|
||||||
|
|
||||||
|
|
||||||
class TrackSerializer(TaggitSerializer, serializers.ModelSerializer):
|
class TrackSerializer(TaggitSerializer, serializers.ModelSerializer):
|
||||||
|
@ -27,10 +35,10 @@ class TrackSerializer(TaggitSerializer, serializers.ModelSerializer):
|
||||||
|
|
||||||
|
|
||||||
class UserSettingsSerializer(serializers.ModelSerializer):
|
class UserSettingsSerializer(serializers.ModelSerializer):
|
||||||
# TODO: validate fields values (playlist_editor_columns at least)
|
# TODO: validate fields values (tracklist_editor_columns at least)
|
||||||
class Meta:
|
class Meta:
|
||||||
model = UserSettings
|
model = UserSettings
|
||||||
fields = ("playlist_editor_columns", "playlist_editor_sep")
|
fields = ("tracklist_editor_columns", "tracklist_editor_sep")
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
user = self.context.get("user")
|
user = self.context.get("user")
|
||||||
|
|
28
aircox/serializers/auth.py
Normal file
28
aircox/serializers/auth.py
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
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")
|
36
aircox/serializers/episode.py
Normal file
36
aircox/serializers/episode.py
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
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",
|
||||||
|
]
|
12
aircox/serializers/page.py
Normal file
12
aircox/serializers/page.py
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
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,43 +1,24 @@
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from ..models import Sound
|
from .. import models
|
||||||
|
|
||||||
__all__ = ("SoundSerializer", "PodcastSerializer")
|
__all__ = ("SoundSerializer",)
|
||||||
|
|
||||||
|
|
||||||
class SoundSerializer(serializers.ModelSerializer):
|
class SoundSerializer(serializers.ModelSerializer):
|
||||||
file = serializers.FileField(use_url=False)
|
file = serializers.FileField(use_url=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Sound
|
model = models.Sound
|
||||||
fields = [
|
fields = [
|
||||||
"pk",
|
"id",
|
||||||
"name",
|
"name",
|
||||||
"program",
|
"program",
|
||||||
"episode",
|
|
||||||
"type",
|
|
||||||
"file",
|
"file",
|
||||||
"duration",
|
"duration",
|
||||||
"mtime",
|
"mtime",
|
||||||
"is_good_quality",
|
"is_good_quality",
|
||||||
"is_public",
|
"is_public",
|
||||||
"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",
|
"is_downloadable",
|
||||||
|
"url",
|
||||||
]
|
]
|
||||||
|
|
1
aircox/static/aircox/admin.css
Normal file
1
aircox/static/aircox/admin.css
Normal file
File diff suppressed because one or more lines are too long
|
@ -1,12 +0,0 @@
|
||||||
<!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>
|
|
126
aircox/static/aircox/admin.js
Normal file
126
aircox/static/aircox/admin.js
Normal file
File diff suppressed because one or more lines are too long
1
aircox/static/aircox/admin.js.map
Normal file
1
aircox/static/aircox/admin.js.map
Normal file
File diff suppressed because one or more lines are too long
1
aircox/static/aircox/assets/admin-BkECowH5.css
Normal file
1
aircox/static/aircox/assets/admin-BkECowH5.css
Normal file
File diff suppressed because one or more lines are too long
1
aircox/static/aircox/assets/admin-BoR3j_Hw.css
Normal file
1
aircox/static/aircox/assets/admin-BoR3j_Hw.css
Normal file
File diff suppressed because one or more lines are too long
46
aircox/static/aircox/assets/index-BHp9xxGn.js
Normal file
46
aircox/static/aircox/assets/index-BHp9xxGn.js
Normal file
File diff suppressed because one or more lines are too long
1
aircox/static/aircox/assets/index-BHp9xxGn.js.map
Normal file
1
aircox/static/aircox/assets/index-BHp9xxGn.js.map
Normal file
File diff suppressed because one or more lines are too long
30
aircox/static/aircox/assets/index-BZUnmcIM.js
Normal file
30
aircox/static/aircox/assets/index-BZUnmcIM.js
Normal file
File diff suppressed because one or more lines are too long
1
aircox/static/aircox/assets/index-BZUnmcIM.js.map
Normal file
1
aircox/static/aircox/assets/index-BZUnmcIM.js.map
Normal file
File diff suppressed because one or more lines are too long
18
aircox/static/aircox/assets/index-BlOTjzEl.js
Normal file
18
aircox/static/aircox/assets/index-BlOTjzEl.js
Normal file
File diff suppressed because one or more lines are too long
1
aircox/static/aircox/assets/index-BlOTjzEl.js.map
Normal file
1
aircox/static/aircox/assets/index-BlOTjzEl.js.map
Normal file
File diff suppressed because one or more lines are too long
46
aircox/static/aircox/assets/index-CKSgqJ6k.js
Normal file
46
aircox/static/aircox/assets/index-CKSgqJ6k.js
Normal file
File diff suppressed because one or more lines are too long
1
aircox/static/aircox/assets/index-CKSgqJ6k.js.map
Normal file
1
aircox/static/aircox/assets/index-CKSgqJ6k.js.map
Normal file
File diff suppressed because one or more lines are too long
1
aircox/static/aircox/assets/index-CQSUXIlM.css
Normal file
1
aircox/static/aircox/assets/index-CQSUXIlM.css
Normal file
File diff suppressed because one or more lines are too long
46
aircox/static/aircox/assets/index-DYrJKfQw.js
Normal file
46
aircox/static/aircox/assets/index-DYrJKfQw.js
Normal file
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
Loading…
Reference in New Issue
Block a user