cfr #121 Co-authored-by: Christophe Siraut <d@tobald.eu.org> Co-authored-by: bkfox <thomas bkfox net> Co-authored-by: Thomas Kairos <thomas@bkfox.net> Reviewed-on: #131 Co-authored-by: Chris Tactic <ctactic@noreply.git.radiocampus.be> Co-committed-by: Chris Tactic <ctactic@noreply.git.radiocampus.be>
This commit is contained in:
parent
1e17a1334a
commit
55123c386d
|
@ -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,26 +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(f for f in PageAdmin.list_filter if f != "pub_date") + (
|
list_filter = tuple(f for f in ChildPageAdmin.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)
|
|
||||||
|
|
|
@ -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,15 +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 mark_safe('<img src="{}"/>'.format(obj.cover.icons["64"])) if obj.cover else ""
|
if obj.cover and obj.cover.thumbnails:
|
||||||
|
return mark_safe('<img src="{}"/>'.format(obj.cover.icons["64"]))
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def _get_extra_context(self, query, **extra_context):
|
||||||
|
return extra_context
|
||||||
|
|
||||||
|
def add_view(self, request, form_url="", extra_context=None):
|
||||||
|
filters = QueryDict(request.GET.get("_changelist_filters", ""))
|
||||||
|
extra_context = self._get_extra_context(filters, **(extra_context or {}))
|
||||||
|
return super().add_view(request, form_url, extra_context)
|
||||||
|
|
||||||
|
def changelist_view(self, request, extra_context=None):
|
||||||
|
extra_context = self._get_extra_context(request.GET, **(extra_context or {}))
|
||||||
|
return super().changelist_view(request, extra_context)
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Page)
|
||||||
|
class PageAdmin(BasePageAdmin):
|
||||||
|
list_display = BasePageAdmin.list_display + ("category",)
|
||||||
|
list_editable = BasePageAdmin.list_editable + ("category",)
|
||||||
|
list_filter = BasePageAdmin.list_filter + ("category", "pub_date")
|
||||||
|
search_fields = BasePageAdmin.search_fields + ("category__title",)
|
||||||
|
|
||||||
|
fieldsets = deepcopy(BasePageAdmin.fieldsets)
|
||||||
|
fieldsets[0][1]["fields"].insert(fieldsets[0][1]["fields"].index("slug") + 1, "category")
|
||||||
|
fieldsets[1][1]["fields"] += ("featured", "allow_comments")
|
||||||
|
|
||||||
|
|
||||||
|
class ChildPageAdmin(PageAdmin):
|
||||||
|
list_display = PageAdmin.list_display + ("parent",)
|
||||||
|
autocomplete_fields = ("parent",)
|
||||||
|
|
||||||
|
fieldsets = deepcopy(PageAdmin.fieldsets)
|
||||||
|
fieldsets[1][1]["fields"] += ("parent",)
|
||||||
|
|
||||||
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)
|
||||||
|
@ -58,45 +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"] = None if parent is None else Page.objects.get_subclass(id=parent)
|
extra_context["parent"] = None if parent is None else Page.objects.get_subclass(id=parent)
|
||||||
return extra_context
|
return super()._get_extra_context(query, **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",)
|
||||||
|
|
||||||
|
|
||||||
|
@ -105,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,15 +24,16 @@ 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
|
||||||
|
|
||||||
|
@ -42,9 +42,6 @@ class SoundInline(admin.TabularInline):
|
||||||
|
|
||||||
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):
|
||||||
|
@ -52,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,
|
||||||
{
|
{
|
||||||
|
@ -79,21 +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 obj.episode.title if obj.episode else obj.program.title if obj.program else ""
|
return 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,67 +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"
|
|
||||||
|
|
|
@ -86,8 +86,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 +140,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 +176,8 @@ 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."""
|
||||||
|
|
||||||
|
|
||||||
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}
|
|
@ -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."""
|
||||||
|
|
||||||
|
@ -61,153 +56,40 @@ class SoundFile:
|
||||||
def sync(self, sound=None, program=None, deleted=False, keep_deleted=False, **kwargs):
|
def sync(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)
|
|
||||||
logger.debug('program from path "%s" -> %s', self.path, program)
|
|
||||||
kwargs["program_id"] = program.pk
|
kwargs["program_id"] = program.pk
|
||||||
|
|
||||||
if sound:
|
|
||||||
created = False
|
created = False
|
||||||
else:
|
if not sound:
|
||||||
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 create_episode_sound(self, sound):
|
||||||
|
episode = sound.find_episode()
|
||||||
|
if episode:
|
||||||
|
# FIXME: position from name
|
||||||
|
item = EpisodeSound(
|
||||||
|
episode=episode, sound=sound, position=episode.episodesound_set.all().count(), broadcast=sound.broadcast
|
||||||
|
)
|
||||||
|
item.save()
|
||||||
|
|
||||||
def _on_delete(self, path, keep_deleted):
|
def _on_delete(self, path, keep_deleted):
|
||||||
# TODO: remove from db on delete
|
sound = None
|
||||||
if keep_deleted:
|
if keep_deleted:
|
||||||
sound = Sound.objects.path(self.path).first()
|
if sound := Sound.objects.path(self.path).first():
|
||||||
if sound:
|
sound.is_removed = True
|
||||||
if keep_deleted:
|
sound.save(sync=False)
|
||||||
sound.type = sound.TYPE_REMOVED
|
elif sound := Sound.objects.path(self.path):
|
||||||
sound.check_on_file()
|
sound.delete()
|
||||||
sound.save()
|
|
||||||
return sound
|
return sound
|
||||||
else:
|
|
||||||
Sound.objects.path(self.path).delete()
|
|
||||||
|
|
||||||
def read_path(self, path):
|
|
||||||
"""Parse path name returning dictionary of extracted info. It can
|
|
||||||
contain:
|
|
||||||
|
|
||||||
- `year`, `month`, `day`: diffusion date
|
|
||||||
- `hour`, `minute`: diffusion time
|
|
||||||
- `n`: sound arbitrary number (used for sound ordering)
|
|
||||||
- `name`: cleaned name extracted or file name (without extension)
|
|
||||||
"""
|
|
||||||
basename = os.path.basename(path)
|
|
||||||
basename = os.path.splitext(basename)[0]
|
|
||||||
reg_match = self._path_re.search(basename)
|
|
||||||
if reg_match:
|
|
||||||
info = reg_match.groupdict()
|
|
||||||
for k in ("year", "month", "day", "hour", "minute", "n"):
|
|
||||||
if info.get(k) is not None:
|
|
||||||
info[k] = int(info[k])
|
|
||||||
|
|
||||||
name = info.get("name")
|
|
||||||
info["name"] = name and self._into_name(name) or basename
|
|
||||||
else:
|
|
||||||
info = {"name": basename}
|
|
||||||
return info
|
|
||||||
|
|
||||||
_path_re = re.compile(
|
|
||||||
"^(?P<year>[0-9]{4})(?P<month>[0-9]{2})(?P<day>[0-9]{2})"
|
|
||||||
"(_(?P<hour>[0-9]{2})h(?P<minute>[0-9]{2}))?"
|
|
||||||
"(_(?P<n>[0-9]+))?"
|
|
||||||
"_?[ -]*(?P<name>.*)$"
|
|
||||||
)
|
|
||||||
|
|
||||||
def _into_name(self, name):
|
|
||||||
name = name.replace("_", " ")
|
|
||||||
return " ".join(r.capitalize() for r in name.split(" "))
|
|
||||||
|
|
||||||
def read_file_info(self):
|
|
||||||
"""Read file information and metadata."""
|
|
||||||
try:
|
|
||||||
if os.path.exists(self.path):
|
|
||||||
return mutagen.File(self.path)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return None
|
|
||||||
|
|
||||||
def find_episode(self, sound, path_info):
|
|
||||||
"""For a given program, check if there is an initial diffusion to
|
|
||||||
associate to, using the date info we have. Update self.sound and save
|
|
||||||
it consequently.
|
|
||||||
|
|
||||||
We only allow initial diffusion since there should be no rerun.
|
|
||||||
"""
|
|
||||||
program, pi = sound.program, path_info
|
|
||||||
if "year" not in pi or not sound or sound.episode:
|
|
||||||
return None
|
|
||||||
|
|
||||||
year, month, day = pi.get("year"), pi.get("month"), pi.get("day")
|
|
||||||
if pi.get("hour") is not None:
|
|
||||||
at = tz.datetime(year, month, day, pi.get("hour", 0), pi.get("minute", 0))
|
|
||||||
at = tz.make_aware(at)
|
|
||||||
else:
|
|
||||||
at = date(year, month, day)
|
|
||||||
|
|
||||||
diffusion = program.diffusion_set.at(at).first()
|
|
||||||
if not diffusion:
|
|
||||||
return None
|
|
||||||
|
|
||||||
logger.debug("%s <--> %s", sound.file.name, str(diffusion.episode))
|
|
||||||
return diffusion.episode
|
|
||||||
|
|
||||||
def find_playlist(self, sound=None, use_meta=True):
|
|
||||||
"""Find a playlist file corresponding to the sound path, such as:
|
|
||||||
my_sound.ogg => my_sound.csv.
|
|
||||||
|
|
||||||
Use sound's file metadata if no corresponding playlist has been
|
|
||||||
found and `use_meta` is True.
|
|
||||||
"""
|
|
||||||
if sound is None:
|
|
||||||
sound = self.sound
|
|
||||||
if sound.track_set.count() > 1:
|
|
||||||
return
|
|
||||||
|
|
||||||
# import playlist
|
|
||||||
path_noext, ext = os.path.splitext(self.sound.file.path)
|
|
||||||
path = path_noext + ".csv"
|
|
||||||
if os.path.exists(path):
|
|
||||||
PlaylistImport(path, sound=sound).run()
|
|
||||||
# use metadata
|
|
||||||
elif use_meta:
|
|
||||||
if self.info is None:
|
|
||||||
self.read_file_info()
|
|
||||||
if self.info and self.info.tags:
|
|
||||||
tags = self.info.tags
|
|
||||||
title, artist, album, year = tuple(
|
|
||||||
t and ", ".join(t) for t in (tags.get(k) for k in ("title", "artist", "album", "year"))
|
|
||||||
)
|
|
||||||
title = title or (self.path_info and self.path_info.get("name")) or os.path.basename(path_noext)
|
|
||||||
info = "{} ({})".format(album, year) if album and year else album or year or ""
|
|
||||||
track = Track(
|
|
||||||
sound=sound,
|
|
||||||
position=int(tags.get("tracknumber", 0)),
|
|
||||||
title=title,
|
|
||||||
artist=artist or _("unknown"),
|
|
||||||
info=info,
|
|
||||||
)
|
|
||||||
track.save()
|
|
||||||
|
|
|
@ -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)
|
||||||
|
@ -214,15 +213,15 @@ 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
|
||||||
|
@ -234,12 +233,12 @@ class SoundMonitor:
|
||||||
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
|
||||||
|
|
||||||
|
@ -248,14 +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(pk__in=sounds)
|
sounds = Sound.objects.filter(file__startswith=program.path).exclude(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
|
||||||
|
@ -267,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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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"],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,10 +36,81 @@ class EpisodeFilters(PageFilters):
|
||||||
podcast = filters.BooleanFilter(method="podcast_filter", label=_("Podcast"))
|
podcast = filters.BooleanFilter(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
|
@ -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:
|
||||||
|
|
|
@ -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),
|
||||||
|
]
|
|
@ -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,6 +17,10 @@ __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 self.filter(episode=episode) if id is None else self.filter(episode__id=id)
|
return self.filter(episode=episode) if id is None else self.filter(episode__id=id)
|
||||||
|
@ -89,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
|
||||||
|
@ -127,8 +133,6 @@ 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")
|
||||||
|
@ -192,34 +196,15 @@ class Diffusion(Rerun):
|
||||||
now = tz.now()
|
now = tz.now()
|
||||||
return self.type == self.TYPE_ON_AIR and self.start <= now and self.end >= now
|
return self.type == self.TYPE_ON_AIR and self.start <= now and self.end >= now
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_today(self):
|
||||||
|
"""True if diffusion is currently today."""
|
||||||
|
return self.start.date() == datetime.date.today()
|
||||||
|
|
||||||
@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 self.type == self.TYPE_ON_AIR and not self.episode.sound_set.archive().count()
|
return self.type == self.TYPE_ON_AIR and not self.episode.episodesound_set.all().broadcast()
|
||||||
|
|
||||||
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
|
||||||
|
|
|
@ -1,55 +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).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.all().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")
|
||||||
|
@ -74,3 +84,54 @@ class Episode(Page):
|
||||||
else title
|
else title
|
||||||
)
|
)
|
||||||
return super().get_init_kwargs_from(page, title=title, program=page, **kwargs)
|
return super().get_init_kwargs_from(page, title=title, program=page, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class EpisodeSoundQuerySet(models.QuerySet):
|
||||||
|
def episode(self, episode):
|
||||||
|
if isinstance(episode, int):
|
||||||
|
return self.filter(episode_id=episode)
|
||||||
|
return self.filter(episode=episode)
|
||||||
|
|
||||||
|
def available(self):
|
||||||
|
return self.filter(sound__is_removed=False)
|
||||||
|
|
||||||
|
def public(self):
|
||||||
|
return self.filter(sound__is_public=True)
|
||||||
|
|
||||||
|
def broadcast(self):
|
||||||
|
return self.available().filter(broadcast=True)
|
||||||
|
|
||||||
|
def playlist(self, order="position"):
|
||||||
|
# TODO: subquery expression
|
||||||
|
if order:
|
||||||
|
self = self.order_by(order)
|
||||||
|
query = self.filter(sound__file__isnull=False, sound__is_removed=False).values_list("sound__file", flat=True)
|
||||||
|
return [os.path.join(d_settings.MEDIA_ROOT, file) for file in query]
|
||||||
|
|
||||||
|
|
||||||
|
class EpisodeSound(models.Model):
|
||||||
|
"""Element of an episode playlist."""
|
||||||
|
|
||||||
|
episode = models.ForeignKey(Episode, on_delete=models.CASCADE)
|
||||||
|
sound = models.ForeignKey(Sound, on_delete=models.CASCADE)
|
||||||
|
position = models.PositiveSmallIntegerField(
|
||||||
|
_("order"),
|
||||||
|
default=0,
|
||||||
|
help_text=_("position in the playlist"),
|
||||||
|
)
|
||||||
|
broadcast = models.BooleanField(
|
||||||
|
_("Broadcast"),
|
||||||
|
blank=None,
|
||||||
|
help_text=_("The sound is broadcasted on air"),
|
||||||
|
)
|
||||||
|
|
||||||
|
objects = EpisodeSoundQuerySet.as_manager()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("Podcast")
|
||||||
|
verbose_name_plural = _("Podcasts")
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
if self.broadcast is None:
|
||||||
|
self.broadcast = self.sound.broadcast
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
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")
|
||||||
|
|
||||||
|
@ -30,6 +33,9 @@ class LogQuerySet(models.QuerySet):
|
||||||
def after(self, date):
|
def after(self, date):
|
||||||
return self.filter(date__gte=date) if isinstance(date, tz.datetime) else self.filter(date__date__gte=date)
|
return self.filter(date__gte=date) if isinstance(date, tz.datetime) else self.filter(date__date__gte=date)
|
||||||
|
|
||||||
|
def before(self, date):
|
||||||
|
return self.filter(date__lte=date) if isinstance(date, tz.datetime) else self.filter(date__date__lte=date)
|
||||||
|
|
||||||
def on_air(self):
|
def on_air(self):
|
||||||
return self.filter(type=Log.TYPE_ON_AIR)
|
return self.filter(type=Log.TYPE_ON_AIR)
|
||||||
|
|
||||||
|
@ -46,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
|
||||||
|
@ -90,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,
|
||||||
|
@ -160,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):
|
||||||
|
@ -184,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
|
||||||
|
@ -199,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:]
|
||||||
|
|
||||||
|
@ -211,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:
|
||||||
|
|
|
@ -16,6 +16,7 @@ from model_utils.managers import InheritanceQuerySet
|
||||||
from .station import Station
|
from .station import Station
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
|
"Renderable",
|
||||||
"Category",
|
"Category",
|
||||||
"PageQuerySet",
|
"PageQuerySet",
|
||||||
"Page",
|
"Page",
|
||||||
|
@ -25,7 +26,17 @@ __all__ = (
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
headline_re = re.compile(r"(<p>)?" r"(?P<headline>[^\n]{1,140}(\n|[^\.]*?\.))" r"(</p>)?")
|
headline_clean_re = re.compile(r"\n(\s| )+", re.MULTILINE)
|
||||||
|
headline_re = re.compile(r"(?P<headline>([\S+]|\s+){1,240}\S+)", re.MULTILINE)
|
||||||
|
|
||||||
|
|
||||||
|
class Renderable:
|
||||||
|
template_prefix = "page"
|
||||||
|
template_name = "aircox/widgets/{prefix}.html"
|
||||||
|
|
||||||
|
def get_template_name(self, widget):
|
||||||
|
"""Return template name for the provided widget."""
|
||||||
|
return self.template_name.format(prefix=self.template_prefix, widget=widget)
|
||||||
|
|
||||||
|
|
||||||
class Category(models.Model):
|
class Category(models.Model):
|
||||||
|
@ -50,6 +61,9 @@ 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 self.filter(parent=parent) if id is None else self.filter(parent__id=id)
|
return self.filter(parent=parent) if id is None else self.filter(parent__id=id)
|
||||||
|
@ -60,7 +74,7 @@ class BasePageQuerySet(InheritanceQuerySet):
|
||||||
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
|
||||||
|
@ -72,14 +86,6 @@ 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"), max_length=120, blank=True, unique=True, db_index=True)
|
slug = models.SlugField(_("slug"), max_length=120, blank=True, unique=True, db_index=True)
|
||||||
status = models.PositiveSmallIntegerField(
|
status = models.PositiveSmallIntegerField(
|
||||||
|
@ -102,11 +108,14 @@ class BasePage(models.Model):
|
||||||
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)
|
||||||
|
|
||||||
|
@ -116,13 +125,12 @@ class BasePage(models.Model):
|
||||||
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 reverse(self.detail_url_name, kwargs={"slug": self.slug}) if self.is_published else "#"
|
if self.is_published:
|
||||||
|
return reverse(self.detail_url_name, kwargs={"slug": self.slug})
|
||||||
|
return ""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_draft(self):
|
def is_draft(self):
|
||||||
|
@ -138,17 +146,35 @@ 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))
|
||||||
|
|
||||||
|
_url_re = re.compile(
|
||||||
|
"((http|https)\:\/\/)?[a-zA-Z0-9\.\/\?\:@\-_=#]+\.([a-zA-Z]){2,6}([a-zA-Z0-9\.\&\/\?\:@\-_=#])*"
|
||||||
|
)
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def display_content(self):
|
||||||
|
if "<p>" in self.content:
|
||||||
|
return self.content
|
||||||
|
content = self._url_re.sub(r'<a href="\1" target="new">\1</a>', self.content)
|
||||||
|
return content.replace("\n\n", "\n").replace("\n", "<br>")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_init_kwargs_from(cls, page, **kwargs):
|
def get_init_kwargs_from(cls, page, **kwargs):
|
||||||
|
@ -161,6 +187,7 @@ 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(status=Page.STATUS_PUBLISHED, pub_date__lte=tz.now())
|
return self.filter(status=Page.STATUS_PUBLISHED, pub_date__lte=tz.now())
|
||||||
|
@ -189,17 +216,66 @@ 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:
|
|
||||||
|
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
|
self.category = self.parent.category
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
@ -209,45 +285,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,
|
||||||
|
@ -260,7 +328,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):
|
||||||
|
@ -268,7 +336,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")
|
||||||
|
@ -281,7 +349,7 @@ class NavItem(models.Model):
|
||||||
station = models.ForeignKey(Station, models.CASCADE, verbose_name=_("station"))
|
station = models.ForeignKey(Station, models.CASCADE, verbose_name=_("station"))
|
||||||
menu = models.SlugField(_("menu"), max_length=24)
|
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,
|
||||||
|
@ -300,14 +368,21 @@ class NavItem(models.Model):
|
||||||
def get_url(self):
|
def get_url(self):
|
||||||
return self.url if self.url else self.page.get_absolute_url() if self.page else None
|
return self.url if self.url else self.page.get_absolute_url() if self.page else None
|
||||||
|
|
||||||
|
def get_label(self):
|
||||||
|
if self.text:
|
||||||
|
return self.text
|
||||||
|
elif self.page:
|
||||||
|
return self.page.title
|
||||||
|
|
||||||
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('<a href="{}" class="{}">{}</a>', url, css_class, self.text)
|
return format_html('<a href="{}" class="{}">{}</a>', url, css_class, label)
|
||||||
|
|
|
@ -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 = self.request.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.
|
||||||
|
@ -58,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):
|
||||||
|
@ -80,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):
|
||||||
|
@ -116,27 +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
|
||||||
|
@ -146,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
|
||||||
|
@ -45,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,
|
||||||
|
@ -74,7 +75,10 @@ class Rerun(models.Model):
|
||||||
raise ValidationError({"initial": _("rerun must happen after original")})
|
raise ValidationError({"initial": _("rerun must happen after original")})
|
||||||
|
|
||||||
def save_rerun(self):
|
def save_rerun(self):
|
||||||
|
if not self.program_id:
|
||||||
self.program = self.initial.program
|
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"),
|
||||||
|
@ -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,
|
||||||
|
@ -110,16 +115,28 @@ 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(day=calendar.monthrange(date.year, date.month)[1])
|
date = date.replace(day=calendar.monthrange(date.year, date.month)[1])
|
||||||
date_wday = date.weekday()
|
date_wday = date.weekday()
|
||||||
|
|
||||||
|
@ -134,33 +151,42 @@ class Schedule(Rerun):
|
||||||
date_wday, month = date.weekday(), date.month
|
date_wday, month = date.weekday(), date.month
|
||||||
date += tz.timedelta(days=(7 if date_wday > sched_wday else 0) - date_wday + sched_wday)
|
date += tz.timedelta(days=(7 if date_wday > sched_wday else 0) - date_wday + sched_wday)
|
||||||
|
|
||||||
if 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 = (date + tz.timedelta(days=7 * week) for week in range(0, 5) if freq & (0b1 << week))
|
dates = (date + tz.timedelta(days=7 * week) for week in range(0, 5) if frequency & (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 self.initial is not None or self.frequency == Schedule.Frequency.ponctual:
|
if frequency is None:
|
||||||
|
frequency = self.frequency
|
||||||
|
|
||||||
|
if sched_date is None:
|
||||||
|
sched_date = self.date
|
||||||
|
|
||||||
|
if self.initial is not None or frequency == Schedule.Frequency.ponctual:
|
||||||
return [], []
|
return [], []
|
||||||
|
|
||||||
# dates for self and reruns as (date, initial)
|
# dates for self and reruns as (date, initial)
|
||||||
reruns = [(rerun, rerun.date - self.date) for rerun in self.rerun_set.all()]
|
reruns = [(rerun, rerun.date - sched_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) for date in list(dates.keys()) for rerun, delta in reruns
|
(rerun.normalize(date.date() + delta), date) for date in list(dates.keys()) for rerun, delta in reruns
|
||||||
)
|
)
|
||||||
|
|
|
@ -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
|
||||||
|
@ -39,27 +50,43 @@ def user_default_groups(sender, instance, created, *args, **kwargs):
|
||||||
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(cover=instance.cover)
|
if initial_cover is None and instance.cover is not None:
|
||||||
|
Episode.objects.filter(parent=instance, cover__isnull=True).update(cover=instance.cover)
|
||||||
|
Article.objects.filter(parent=instance, cover__isnull=True).update(cover=instance.cover)
|
||||||
|
|
||||||
|
|
||||||
|
# ---- program
|
||||||
@receiver(signals.post_save, sender=Program)
|
@receiver(signals.post_save, sender=Program)
|
||||||
def program_post_save(sender, instance, created, *args, **kwargs):
|
def program_post_save__clean_later_episodes(sender, instance, created, *args, **kwargs):
|
||||||
"""Clean-up later diffusions when a program becomes inactive."""
|
|
||||||
if not instance.active:
|
if not instance.active:
|
||||||
Diffusion.objects.program(instance).after(tz.now()).delete()
|
Diffusion.objects.program(instance).after(tz.now()).delete()
|
||||||
Episode.objects.parent(instance).filter(diffusion__isnull=True).delete()
|
Episode.objects.parent(instance).filter(diffusion__isnull=True).delete()
|
||||||
|
|
||||||
cover = getattr(instance, "__initial_cover", None)
|
|
||||||
if cover is None and instance.cover is not None:
|
@receiver(signals.post_save, sender=Program)
|
||||||
Episode.objects.parent(instance).filter(cover__isnull=True).update(cover=instance.cover)
|
def program_post_save__mv_sounds(sender, instance, created, *args, **kwargs):
|
||||||
|
path_ = getattr(instance, "__initial_path", None)
|
||||||
|
if path_ in (None, instance.path):
|
||||||
|
return
|
||||||
|
|
||||||
|
abspath = path_ and os.path.join(conf.MEDIA_ROOT, path_)
|
||||||
|
if os.path.exists(abspath) and not os.path.exists(instance.abspath):
|
||||||
|
logger.info(
|
||||||
|
f"program #{instance.pk}'s dir changed to {instance.title} - update it.", instance.id, instance.title
|
||||||
|
)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
@ -88,9 +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(diffusion__isnull=True, content__isnull=True, sound__isnull=True).delete()
|
Episode.objects.filter(diffusion__isnull=True, content__isnull=True, episodesound__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(diffusion__isnull=True, content__isnull=True, sound__isnull=True).delete()
|
Episode.objects.filter(diffusion__isnull=True, content__isnull=True, episodesound__isnull=True).delete()
|
||||||
|
|
||||||
|
|
||||||
|
# ---- files
|
||||||
|
@receiver(signals.post_delete, sender=Sound)
|
||||||
|
def delete_file(sender, instance, *args, **kwargs):
|
||||||
|
"""Deletes file on `post_delete`"""
|
||||||
|
if not instance.file:
|
||||||
|
return
|
||||||
|
|
||||||
|
path = instance.file.path
|
||||||
|
qs = sender.objects.filter(file=path)
|
||||||
|
if not qs.exists() and os.path.exists(path):
|
||||||
|
os.remove(path)
|
||||||
|
|
|
@ -1,304 +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("file", flat=True)
|
for file in self.filter(file__isnull=False).values_list("file", flat=True)
|
||||||
]
|
]
|
||||||
|
|
||||||
def search(self, query):
|
|
||||||
return self.filter(
|
|
||||||
Q(name__icontains=query)
|
|
||||||
| Q(file__icontains=query)
|
|
||||||
| Q(program__title__icontains=query)
|
|
||||||
| Q(episode__title__icontains=query)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
class Sound(File):
|
||||||
# TODO:
|
|
||||||
# - provide a default name based on program and episode
|
|
||||||
class Sound(models.Model):
|
|
||||||
"""A Sound is the representation of a sound file that can be either an
|
|
||||||
excerpt or a complete archive of the related diffusion."""
|
|
||||||
|
|
||||||
TYPE_OTHER = 0x00
|
|
||||||
TYPE_ARCHIVE = 0x01
|
|
||||||
TYPE_EXCERPT = 0x02
|
|
||||||
TYPE_REMOVED = 0x03
|
|
||||||
TYPE_CHOICES = (
|
|
||||||
(TYPE_OTHER, _("other")),
|
|
||||||
(TYPE_ARCHIVE, _("archive")),
|
|
||||||
(TYPE_EXCERPT, _("excerpt")),
|
|
||||||
(TYPE_REMOVED, _("removed")),
|
|
||||||
)
|
|
||||||
|
|
||||||
name = models.CharField(_("name"), max_length=64)
|
|
||||||
program = models.ForeignKey(
|
|
||||||
Program,
|
|
||||||
models.CASCADE,
|
|
||||||
blank=True, # NOT NULL
|
|
||||||
verbose_name=_("program"),
|
|
||||||
help_text=_("program related to it"),
|
|
||||||
db_index=True,
|
|
||||||
)
|
|
||||||
episode = models.ForeignKey(
|
|
||||||
Episode,
|
|
||||||
models.SET_NULL,
|
|
||||||
blank=True,
|
|
||||||
null=True,
|
|
||||||
verbose_name=_("episode"),
|
|
||||||
db_index=True,
|
|
||||||
)
|
|
||||||
type = models.SmallIntegerField(_("type"), choices=TYPE_CHOICES)
|
|
||||||
position = models.PositiveSmallIntegerField(
|
|
||||||
_("order"),
|
|
||||||
default=0,
|
|
||||||
help_text=_("position in the playlist"),
|
|
||||||
)
|
|
||||||
|
|
||||||
def _upload_to(self, filename):
|
|
||||||
subdir = settings.SOUND_ARCHIVES_SUBDIR if self.type == self.TYPE_ARCHIVE else settings.SOUND_EXCERPTS_SUBDIR
|
|
||||||
return os.path.join(self.program.path, subdir, filename)
|
|
||||||
|
|
||||||
file = models.FileField(
|
|
||||||
_("file"),
|
|
||||||
upload_to=_upload_to,
|
|
||||||
max_length=256,
|
|
||||||
db_index=True,
|
|
||||||
unique=True,
|
|
||||||
)
|
|
||||||
duration = models.TimeField(
|
duration = 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=_("whether it can be publicly downloaded by visitors (sound must be " "public)"),
|
help_text=_("Sound can be downloaded by website visitors."),
|
||||||
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]
|
||||||
|
reg_match = cls._path_re.search(basename)
|
||||||
|
if reg_match:
|
||||||
|
info = reg_match.groupdict()
|
||||||
|
for k in ("year", "month", "day", "hour", "minute", "n"):
|
||||||
|
if info.get(k) is not None:
|
||||||
|
info[k] = int(info[k])
|
||||||
|
|
||||||
|
name = info.get("name")
|
||||||
|
info["name"] = name and cls._as_name(name) or basename
|
||||||
|
else:
|
||||||
|
info = {"name": basename}
|
||||||
|
return info
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _as_name(cls, name):
|
||||||
|
name = name.replace("_", " ")
|
||||||
|
return " ".join(r.capitalize() for r in name.split(" "))
|
||||||
|
|
||||||
|
def find_episode(self, path_info=None):
|
||||||
|
"""Base on self's file name, match date to an initial diffusion and
|
||||||
|
return corresponding episode or ``None``."""
|
||||||
|
pi = path_info or self.read_path(self.file.path)
|
||||||
|
if "year" not in pi:
|
||||||
|
return None
|
||||||
|
|
||||||
|
year, month, day = pi.get("year"), pi.get("month"), pi.get("day")
|
||||||
|
if pi.get("hour") is not None:
|
||||||
|
at = tz.datetime(year, month, day, pi.get("hour", 0), pi.get("minute", 0))
|
||||||
|
at = tz.make_aware(at)
|
||||||
|
else:
|
||||||
|
at = date(year, month, day)
|
||||||
|
|
||||||
|
diffusion = self.program.diffusion_set.at(at).first()
|
||||||
|
return diffusion and diffusion.episode or None
|
||||||
|
|
||||||
|
def find_playlist(self, meta=None):
|
||||||
|
"""Find a playlist file corresponding to the sound path, such as:
|
||||||
|
my_sound.ogg => my_sound.csv.
|
||||||
|
|
||||||
|
Use provided sound's metadata if any and no csv file has been
|
||||||
|
found.
|
||||||
|
"""
|
||||||
|
from aircox.controllers.playlist_import import PlaylistImport
|
||||||
|
from .track import Track
|
||||||
|
|
||||||
|
if self.track_set.count() > 1:
|
||||||
return
|
return
|
||||||
logger.debug("sound %s: has been removed", self.file.name)
|
|
||||||
self.type = self.TYPE_REMOVED
|
|
||||||
return True
|
|
||||||
|
|
||||||
# not anymore removed
|
# import playlist
|
||||||
changed = False
|
path_noext, ext = os.path.splitext(self.file.path)
|
||||||
|
path = path_noext + ".csv"
|
||||||
|
if os.path.exists(path):
|
||||||
|
PlaylistImport(path, sound=self).run()
|
||||||
|
# use metadata
|
||||||
|
elif meta and meta.tags:
|
||||||
|
title, artist, album, year = tuple(
|
||||||
|
t and ", ".join(t) for t in (meta.tags.get(k) for k in ("title", "artist", "album", "year"))
|
||||||
|
)
|
||||||
|
title = title or path_noext
|
||||||
|
info = "{} ({})".format(album, year) if album and year else album or year or ""
|
||||||
|
track = Track(
|
||||||
|
sound=self,
|
||||||
|
position=int(meta.tags.get("tracknumber", 0)),
|
||||||
|
title=title,
|
||||||
|
artist=artist or _("unknown"),
|
||||||
|
info=info,
|
||||||
|
)
|
||||||
|
track.save()
|
||||||
|
|
||||||
if self.type == self.TYPE_REMOVED and self.program:
|
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
|
changed = True
|
||||||
self.type = (
|
if find_playlist and self.meta:
|
||||||
self.TYPE_ARCHIVE if self.file.name.startswith(self.program.archives_path) else self.TYPE_EXCERPT
|
not self.pk and self.save(sync=False)
|
||||||
)
|
self.find_playlist(self.meta)
|
||||||
|
|
||||||
# check mtime -> reset quality if changed (assume file changed)
|
|
||||||
mtime = self.get_mtime()
|
|
||||||
|
|
||||||
if self.mtime != mtime:
|
|
||||||
self.mtime = mtime
|
|
||||||
self.is_good_quality = None
|
|
||||||
logger.debug(
|
|
||||||
"sound %s: m_time has changed. Reset quality info",
|
|
||||||
self.file.name,
|
|
||||||
)
|
|
||||||
return True
|
|
||||||
|
|
||||||
return changed
|
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(self=self)
|
infos = ""
|
||||||
|
if self.is_removed:
|
||||||
def save(self, *args, **kwargs):
|
infos += _("removed")
|
||||||
if (self.sound is None and self.episode is None) or (self.sound is not None and self.episode is not None):
|
if infos:
|
||||||
raise ValueError("sound XOR episode is required")
|
return f"{self.file.name} [{infos}]"
|
||||||
super().save(*args, **kwargs)
|
return f"{self.file.name}"
|
||||||
|
|
|
@ -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,7 +57,7 @@ class Station(models.Model):
|
||||||
max_length=2048,
|
max_length=2048,
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text=_("Audio streams urls used by station's player. One url " "a line."),
|
help_text=_("Audio streams urls used by station's player. One url a line."),
|
||||||
)
|
)
|
||||||
default_cover = FilerImageField(
|
default_cover = FilerImageField(
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
|
@ -76,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()
|
||||||
|
|
||||||
|
@ -88,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:
|
||||||
|
|
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,5 +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(_("Playlist Editor Separator"), max_length=16)
|
tracklist_editor_sep = models.CharField(_("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)
|
||||||
|
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>
|
|
30
aircox/static/aircox/admin.js
Normal file
30
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
1
aircox/static/aircox/assets/index-DYrJKfQw.js.map
Normal file
1
aircox/static/aircox/assets/index-DYrJKfQw.js.map
Normal file
File diff suppressed because one or more lines are too long
1
aircox/static/aircox/assets/index-QB2VsSsQ.css
Normal file
1
aircox/static/aircox/assets/index-QB2VsSsQ.css
Normal file
File diff suppressed because one or more lines are too long
1
aircox/static/aircox/assets/public-D1AeeZhP.css
Normal file
1
aircox/static/aircox/assets/public-D1AeeZhP.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/core.js"></script><link href="css/chunk-vendors.css" rel="stylesheet"><link href="css/chunk-common.css" rel="stylesheet"></head>
|
|
||||||
<body>
|
|
||||||
<div id="app"></div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -1,24 +0,0 @@
|
||||||
/*!*************************************************************************************************************************************************************************************************************************************!*\
|
|
||||||
!*** css ./node_modules/css-loader/dist/cjs.js??clonedRuleSet-24.use[1]!./node_modules/postcss-loader/dist/cjs.js??clonedRuleSet-24.use[2]!./node_modules/sass-loader/dist/cjs.js??clonedRuleSet-24.use[3]!./src/assets/admin.scss ***!
|
|
||||||
\*************************************************************************************************************************************************************************************************************************************/
|
|
||||||
.admin .navbar .navbar-brand {
|
|
||||||
padding-right: 1em;
|
|
||||||
}
|
|
||||||
.admin .navbar .navbar-brand img {
|
|
||||||
margin: 0em 0.4em;
|
|
||||||
margin-top: 0.3em;
|
|
||||||
max-height: 3em;
|
|
||||||
}
|
|
||||||
.admin .breadcrumbs {
|
|
||||||
margin-bottom: 1em;
|
|
||||||
}
|
|
||||||
.admin .results > #result_list {
|
|
||||||
width: 100%;
|
|
||||||
margin: 1em 0em;
|
|
||||||
}
|
|
||||||
.admin ul.menu-list li {
|
|
||||||
list-style-type: none;
|
|
||||||
}
|
|
||||||
.admin .submit-row a.deletelink {
|
|
||||||
height: 35px;
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
29
aircox/static/aircox/index-BtiUbLPj.cjs
Normal file
29
aircox/static/aircox/index-BtiUbLPj.cjs
Normal file
File diff suppressed because one or more lines are too long
13537
aircox/static/aircox/index-ByevNFTS.js
Normal file
13537
aircox/static/aircox/index-ByevNFTS.js
Normal file
File diff suppressed because one or more lines are too long
1
aircox/static/aircox/index.css
Normal file
1
aircox/static/aircox/index.css
Normal file
File diff suppressed because one or more lines are too long
2
aircox/static/aircox/index.js
Normal file
2
aircox/static/aircox/index.js
Normal file
File diff suppressed because one or more lines are too long
1
aircox/static/aircox/index.js.map
Normal file
1
aircox/static/aircox/index.js.map
Normal file
File diff suppressed because one or more lines are too long
|
@ -1,225 +0,0 @@
|
||||||
/*
|
|
||||||
* ATTENTION: The "eval" devtool has been used (maybe by default in mode: "development").
|
|
||||||
* This devtool is neither made for production nor for readable output files.
|
|
||||||
* It uses "eval()" calls to create a separate source file in the browser devtools.
|
|
||||||
* If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/)
|
|
||||||
* or disable the default devtool with "devtool: false".
|
|
||||||
* If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/).
|
|
||||||
*/
|
|
||||||
/******/ (function() { // webpackBootstrap
|
|
||||||
/******/ "use strict";
|
|
||||||
/******/ var __webpack_modules__ = ({
|
|
||||||
|
|
||||||
/***/ "./src/admin.js":
|
|
||||||
/*!**********************!*\
|
|
||||||
!*** ./src/admin.js ***!
|
|
||||||
\**********************/
|
|
||||||
/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
|
|
||||||
|
|
||||||
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _assets_styles_scss__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./assets/styles.scss */ \"./src/assets/styles.scss\");\n/* harmony import */ var _assets_admin_scss__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./assets/admin.scss */ \"./src/assets/admin.scss\");\n/* harmony import */ var _index_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./index.js */ \"./src/index.js\");\n/* harmony import */ var _app__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./app */ \"./src/app.js\");\n/* harmony import */ var _components__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./components */ \"./src/components/index.js\");\n/* harmony import */ var _track__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ./track */ \"./src/track.js\");\n\n\n\n\n\n\nconst AdminApp = {\n ..._app__WEBPACK_IMPORTED_MODULE_3__[\"default\"],\n components: {\n ..._app__WEBPACK_IMPORTED_MODULE_3__[\"default\"].components,\n ..._components__WEBPACK_IMPORTED_MODULE_4__.admin\n },\n data() {\n return {\n ...super.data,\n Track: _track__WEBPACK_IMPORTED_MODULE_5__[\"default\"]\n };\n }\n};\n/* harmony default export */ __webpack_exports__[\"default\"] = (AdminApp);\nwindow.App = AdminApp;\n\n//# sourceURL=webpack://aircox-assets/./src/admin.js?");
|
|
||||||
|
|
||||||
/***/ }),
|
|
||||||
|
|
||||||
/***/ "./src/assets/admin.scss":
|
|
||||||
/*!*******************************!*\
|
|
||||||
!*** ./src/assets/admin.scss ***!
|
|
||||||
\*******************************/
|
|
||||||
/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
|
|
||||||
|
|
||||||
eval("__webpack_require__.r(__webpack_exports__);\n// extracted by mini-css-extract-plugin\n\n\n//# sourceURL=webpack://aircox-assets/./src/assets/admin.scss?");
|
|
||||||
|
|
||||||
/***/ })
|
|
||||||
|
|
||||||
/******/ });
|
|
||||||
/************************************************************************/
|
|
||||||
/******/ // The module cache
|
|
||||||
/******/ var __webpack_module_cache__ = {};
|
|
||||||
/******/
|
|
||||||
/******/ // The require function
|
|
||||||
/******/ function __webpack_require__(moduleId) {
|
|
||||||
/******/ // Check if module is in cache
|
|
||||||
/******/ var cachedModule = __webpack_module_cache__[moduleId];
|
|
||||||
/******/ if (cachedModule !== undefined) {
|
|
||||||
/******/ return cachedModule.exports;
|
|
||||||
/******/ }
|
|
||||||
/******/ // Create a new module (and put it into the cache)
|
|
||||||
/******/ var module = __webpack_module_cache__[moduleId] = {
|
|
||||||
/******/ id: moduleId,
|
|
||||||
/******/ loaded: false,
|
|
||||||
/******/ exports: {}
|
|
||||||
/******/ };
|
|
||||||
/******/
|
|
||||||
/******/ // Execute the module function
|
|
||||||
/******/ __webpack_modules__[moduleId].call(module.exports, module, module.exports, __webpack_require__);
|
|
||||||
/******/
|
|
||||||
/******/ // Flag the module as loaded
|
|
||||||
/******/ module.loaded = true;
|
|
||||||
/******/
|
|
||||||
/******/ // Return the exports of the module
|
|
||||||
/******/ return module.exports;
|
|
||||||
/******/ }
|
|
||||||
/******/
|
|
||||||
/******/ // expose the modules object (__webpack_modules__)
|
|
||||||
/******/ __webpack_require__.m = __webpack_modules__;
|
|
||||||
/******/
|
|
||||||
/************************************************************************/
|
|
||||||
/******/ /* webpack/runtime/chunk loaded */
|
|
||||||
/******/ !function() {
|
|
||||||
/******/ var deferred = [];
|
|
||||||
/******/ __webpack_require__.O = function(result, chunkIds, fn, priority) {
|
|
||||||
/******/ if(chunkIds) {
|
|
||||||
/******/ priority = priority || 0;
|
|
||||||
/******/ for(var i = deferred.length; i > 0 && deferred[i - 1][2] > priority; i--) deferred[i] = deferred[i - 1];
|
|
||||||
/******/ deferred[i] = [chunkIds, fn, priority];
|
|
||||||
/******/ return;
|
|
||||||
/******/ }
|
|
||||||
/******/ var notFulfilled = Infinity;
|
|
||||||
/******/ for (var i = 0; i < deferred.length; i++) {
|
|
||||||
/******/ var chunkIds = deferred[i][0];
|
|
||||||
/******/ var fn = deferred[i][1];
|
|
||||||
/******/ var priority = deferred[i][2];
|
|
||||||
/******/ var fulfilled = true;
|
|
||||||
/******/ for (var j = 0; j < chunkIds.length; j++) {
|
|
||||||
/******/ if ((priority & 1 === 0 || notFulfilled >= priority) && Object.keys(__webpack_require__.O).every(function(key) { return __webpack_require__.O[key](chunkIds[j]); })) {
|
|
||||||
/******/ chunkIds.splice(j--, 1);
|
|
||||||
/******/ } else {
|
|
||||||
/******/ fulfilled = false;
|
|
||||||
/******/ if(priority < notFulfilled) notFulfilled = priority;
|
|
||||||
/******/ }
|
|
||||||
/******/ }
|
|
||||||
/******/ if(fulfilled) {
|
|
||||||
/******/ deferred.splice(i--, 1)
|
|
||||||
/******/ var r = fn();
|
|
||||||
/******/ if (r !== undefined) result = r;
|
|
||||||
/******/ }
|
|
||||||
/******/ }
|
|
||||||
/******/ return result;
|
|
||||||
/******/ };
|
|
||||||
/******/ }();
|
|
||||||
/******/
|
|
||||||
/******/ /* webpack/runtime/compat get default export */
|
|
||||||
/******/ !function() {
|
|
||||||
/******/ // getDefaultExport function for compatibility with non-harmony modules
|
|
||||||
/******/ __webpack_require__.n = function(module) {
|
|
||||||
/******/ var getter = module && module.__esModule ?
|
|
||||||
/******/ function() { return module['default']; } :
|
|
||||||
/******/ function() { return module; };
|
|
||||||
/******/ __webpack_require__.d(getter, { a: getter });
|
|
||||||
/******/ return getter;
|
|
||||||
/******/ };
|
|
||||||
/******/ }();
|
|
||||||
/******/
|
|
||||||
/******/ /* webpack/runtime/define property getters */
|
|
||||||
/******/ !function() {
|
|
||||||
/******/ // define getter functions for harmony exports
|
|
||||||
/******/ __webpack_require__.d = function(exports, definition) {
|
|
||||||
/******/ for(var key in definition) {
|
|
||||||
/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
|
|
||||||
/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
|
|
||||||
/******/ }
|
|
||||||
/******/ }
|
|
||||||
/******/ };
|
|
||||||
/******/ }();
|
|
||||||
/******/
|
|
||||||
/******/ /* webpack/runtime/global */
|
|
||||||
/******/ !function() {
|
|
||||||
/******/ __webpack_require__.g = (function() {
|
|
||||||
/******/ if (typeof globalThis === 'object') return globalThis;
|
|
||||||
/******/ try {
|
|
||||||
/******/ return this || new Function('return this')();
|
|
||||||
/******/ } catch (e) {
|
|
||||||
/******/ if (typeof window === 'object') return window;
|
|
||||||
/******/ }
|
|
||||||
/******/ })();
|
|
||||||
/******/ }();
|
|
||||||
/******/
|
|
||||||
/******/ /* webpack/runtime/hasOwnProperty shorthand */
|
|
||||||
/******/ !function() {
|
|
||||||
/******/ __webpack_require__.o = function(obj, prop) { return Object.prototype.hasOwnProperty.call(obj, prop); }
|
|
||||||
/******/ }();
|
|
||||||
/******/
|
|
||||||
/******/ /* webpack/runtime/make namespace object */
|
|
||||||
/******/ !function() {
|
|
||||||
/******/ // define __esModule on exports
|
|
||||||
/******/ __webpack_require__.r = function(exports) {
|
|
||||||
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
|
|
||||||
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
||||||
/******/ }
|
|
||||||
/******/ Object.defineProperty(exports, '__esModule', { value: true });
|
|
||||||
/******/ };
|
|
||||||
/******/ }();
|
|
||||||
/******/
|
|
||||||
/******/ /* webpack/runtime/node module decorator */
|
|
||||||
/******/ !function() {
|
|
||||||
/******/ __webpack_require__.nmd = function(module) {
|
|
||||||
/******/ module.paths = [];
|
|
||||||
/******/ if (!module.children) module.children = [];
|
|
||||||
/******/ return module;
|
|
||||||
/******/ };
|
|
||||||
/******/ }();
|
|
||||||
/******/
|
|
||||||
/******/ /* webpack/runtime/jsonp chunk loading */
|
|
||||||
/******/ !function() {
|
|
||||||
/******/ // no baseURI
|
|
||||||
/******/
|
|
||||||
/******/ // object to store loaded and loading chunks
|
|
||||||
/******/ // undefined = chunk not loaded, null = chunk preloaded/prefetched
|
|
||||||
/******/ // [resolve, reject, Promise] = chunk loading, 0 = chunk loaded
|
|
||||||
/******/ var installedChunks = {
|
|
||||||
/******/ "admin": 0
|
|
||||||
/******/ };
|
|
||||||
/******/
|
|
||||||
/******/ // no chunk on demand loading
|
|
||||||
/******/
|
|
||||||
/******/ // no prefetching
|
|
||||||
/******/
|
|
||||||
/******/ // no preloaded
|
|
||||||
/******/
|
|
||||||
/******/ // no HMR
|
|
||||||
/******/
|
|
||||||
/******/ // no HMR manifest
|
|
||||||
/******/
|
|
||||||
/******/ __webpack_require__.O.j = function(chunkId) { return installedChunks[chunkId] === 0; };
|
|
||||||
/******/
|
|
||||||
/******/ // install a JSONP callback for chunk loading
|
|
||||||
/******/ var webpackJsonpCallback = function(parentChunkLoadingFunction, data) {
|
|
||||||
/******/ var chunkIds = data[0];
|
|
||||||
/******/ var moreModules = data[1];
|
|
||||||
/******/ var runtime = data[2];
|
|
||||||
/******/ // add "moreModules" to the modules object,
|
|
||||||
/******/ // then flag all "chunkIds" as loaded and fire callback
|
|
||||||
/******/ var moduleId, chunkId, i = 0;
|
|
||||||
/******/ if(chunkIds.some(function(id) { return installedChunks[id] !== 0; })) {
|
|
||||||
/******/ for(moduleId in moreModules) {
|
|
||||||
/******/ if(__webpack_require__.o(moreModules, moduleId)) {
|
|
||||||
/******/ __webpack_require__.m[moduleId] = moreModules[moduleId];
|
|
||||||
/******/ }
|
|
||||||
/******/ }
|
|
||||||
/******/ if(runtime) var result = runtime(__webpack_require__);
|
|
||||||
/******/ }
|
|
||||||
/******/ if(parentChunkLoadingFunction) parentChunkLoadingFunction(data);
|
|
||||||
/******/ for(;i < chunkIds.length; i++) {
|
|
||||||
/******/ chunkId = chunkIds[i];
|
|
||||||
/******/ if(__webpack_require__.o(installedChunks, chunkId) && installedChunks[chunkId]) {
|
|
||||||
/******/ installedChunks[chunkId][0]();
|
|
||||||
/******/ }
|
|
||||||
/******/ installedChunks[chunkId] = 0;
|
|
||||||
/******/ }
|
|
||||||
/******/ return __webpack_require__.O(result);
|
|
||||||
/******/ }
|
|
||||||
/******/
|
|
||||||
/******/ var chunkLoadingGlobal = self["webpackChunkaircox_assets"] = self["webpackChunkaircox_assets"] || [];
|
|
||||||
/******/ chunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0));
|
|
||||||
/******/ chunkLoadingGlobal.push = webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal));
|
|
||||||
/******/ }();
|
|
||||||
/******/
|
|
||||||
/************************************************************************/
|
|
||||||
/******/
|
|
||||||
/******/ // startup
|
|
||||||
/******/ // Load entry module and return exports
|
|
||||||
/******/ // This entry module depends on other loaded chunks and execution need to be delayed
|
|
||||||
/******/ var __webpack_exports__ = __webpack_require__.O(undefined, ["chunk-vendors","chunk-common"], function() { return __webpack_require__("./src/admin.js"); })
|
|
||||||
/******/ __webpack_exports__ = __webpack_require__.O(__webpack_exports__);
|
|
||||||
/******/
|
|
||||||
/******/ })()
|
|
||||||
;
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -1,215 +0,0 @@
|
||||||
/*
|
|
||||||
* ATTENTION: The "eval" devtool has been used (maybe by default in mode: "development").
|
|
||||||
* This devtool is neither made for production nor for readable output files.
|
|
||||||
* It uses "eval()" calls to create a separate source file in the browser devtools.
|
|
||||||
* If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/)
|
|
||||||
* or disable the default devtool with "devtool: false".
|
|
||||||
* If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/).
|
|
||||||
*/
|
|
||||||
/******/ (function() { // webpackBootstrap
|
|
||||||
/******/ "use strict";
|
|
||||||
/******/ var __webpack_modules__ = ({
|
|
||||||
|
|
||||||
/***/ "./src/core.js":
|
|
||||||
/*!*********************!*\
|
|
||||||
!*** ./src/core.js ***!
|
|
||||||
\*********************/
|
|
||||||
/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
|
|
||||||
|
|
||||||
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _index_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./index.js */ \"./src/index.js\");\n/* harmony import */ var _app_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./app.js */ \"./src/app.js\");\n\n\n/* harmony default export */ __webpack_exports__[\"default\"] = (_app_js__WEBPACK_IMPORTED_MODULE_1__[\"default\"]);\nwindow.App = _app_js__WEBPACK_IMPORTED_MODULE_1__[\"default\"];\n\n//# sourceURL=webpack://aircox-assets/./src/core.js?");
|
|
||||||
|
|
||||||
/***/ })
|
|
||||||
|
|
||||||
/******/ });
|
|
||||||
/************************************************************************/
|
|
||||||
/******/ // The module cache
|
|
||||||
/******/ var __webpack_module_cache__ = {};
|
|
||||||
/******/
|
|
||||||
/******/ // The require function
|
|
||||||
/******/ function __webpack_require__(moduleId) {
|
|
||||||
/******/ // Check if module is in cache
|
|
||||||
/******/ var cachedModule = __webpack_module_cache__[moduleId];
|
|
||||||
/******/ if (cachedModule !== undefined) {
|
|
||||||
/******/ return cachedModule.exports;
|
|
||||||
/******/ }
|
|
||||||
/******/ // Create a new module (and put it into the cache)
|
|
||||||
/******/ var module = __webpack_module_cache__[moduleId] = {
|
|
||||||
/******/ id: moduleId,
|
|
||||||
/******/ loaded: false,
|
|
||||||
/******/ exports: {}
|
|
||||||
/******/ };
|
|
||||||
/******/
|
|
||||||
/******/ // Execute the module function
|
|
||||||
/******/ __webpack_modules__[moduleId].call(module.exports, module, module.exports, __webpack_require__);
|
|
||||||
/******/
|
|
||||||
/******/ // Flag the module as loaded
|
|
||||||
/******/ module.loaded = true;
|
|
||||||
/******/
|
|
||||||
/******/ // Return the exports of the module
|
|
||||||
/******/ return module.exports;
|
|
||||||
/******/ }
|
|
||||||
/******/
|
|
||||||
/******/ // expose the modules object (__webpack_modules__)
|
|
||||||
/******/ __webpack_require__.m = __webpack_modules__;
|
|
||||||
/******/
|
|
||||||
/************************************************************************/
|
|
||||||
/******/ /* webpack/runtime/chunk loaded */
|
|
||||||
/******/ !function() {
|
|
||||||
/******/ var deferred = [];
|
|
||||||
/******/ __webpack_require__.O = function(result, chunkIds, fn, priority) {
|
|
||||||
/******/ if(chunkIds) {
|
|
||||||
/******/ priority = priority || 0;
|
|
||||||
/******/ for(var i = deferred.length; i > 0 && deferred[i - 1][2] > priority; i--) deferred[i] = deferred[i - 1];
|
|
||||||
/******/ deferred[i] = [chunkIds, fn, priority];
|
|
||||||
/******/ return;
|
|
||||||
/******/ }
|
|
||||||
/******/ var notFulfilled = Infinity;
|
|
||||||
/******/ for (var i = 0; i < deferred.length; i++) {
|
|
||||||
/******/ var chunkIds = deferred[i][0];
|
|
||||||
/******/ var fn = deferred[i][1];
|
|
||||||
/******/ var priority = deferred[i][2];
|
|
||||||
/******/ var fulfilled = true;
|
|
||||||
/******/ for (var j = 0; j < chunkIds.length; j++) {
|
|
||||||
/******/ if ((priority & 1 === 0 || notFulfilled >= priority) && Object.keys(__webpack_require__.O).every(function(key) { return __webpack_require__.O[key](chunkIds[j]); })) {
|
|
||||||
/******/ chunkIds.splice(j--, 1);
|
|
||||||
/******/ } else {
|
|
||||||
/******/ fulfilled = false;
|
|
||||||
/******/ if(priority < notFulfilled) notFulfilled = priority;
|
|
||||||
/******/ }
|
|
||||||
/******/ }
|
|
||||||
/******/ if(fulfilled) {
|
|
||||||
/******/ deferred.splice(i--, 1)
|
|
||||||
/******/ var r = fn();
|
|
||||||
/******/ if (r !== undefined) result = r;
|
|
||||||
/******/ }
|
|
||||||
/******/ }
|
|
||||||
/******/ return result;
|
|
||||||
/******/ };
|
|
||||||
/******/ }();
|
|
||||||
/******/
|
|
||||||
/******/ /* webpack/runtime/compat get default export */
|
|
||||||
/******/ !function() {
|
|
||||||
/******/ // getDefaultExport function for compatibility with non-harmony modules
|
|
||||||
/******/ __webpack_require__.n = function(module) {
|
|
||||||
/******/ var getter = module && module.__esModule ?
|
|
||||||
/******/ function() { return module['default']; } :
|
|
||||||
/******/ function() { return module; };
|
|
||||||
/******/ __webpack_require__.d(getter, { a: getter });
|
|
||||||
/******/ return getter;
|
|
||||||
/******/ };
|
|
||||||
/******/ }();
|
|
||||||
/******/
|
|
||||||
/******/ /* webpack/runtime/define property getters */
|
|
||||||
/******/ !function() {
|
|
||||||
/******/ // define getter functions for harmony exports
|
|
||||||
/******/ __webpack_require__.d = function(exports, definition) {
|
|
||||||
/******/ for(var key in definition) {
|
|
||||||
/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
|
|
||||||
/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
|
|
||||||
/******/ }
|
|
||||||
/******/ }
|
|
||||||
/******/ };
|
|
||||||
/******/ }();
|
|
||||||
/******/
|
|
||||||
/******/ /* webpack/runtime/global */
|
|
||||||
/******/ !function() {
|
|
||||||
/******/ __webpack_require__.g = (function() {
|
|
||||||
/******/ if (typeof globalThis === 'object') return globalThis;
|
|
||||||
/******/ try {
|
|
||||||
/******/ return this || new Function('return this')();
|
|
||||||
/******/ } catch (e) {
|
|
||||||
/******/ if (typeof window === 'object') return window;
|
|
||||||
/******/ }
|
|
||||||
/******/ })();
|
|
||||||
/******/ }();
|
|
||||||
/******/
|
|
||||||
/******/ /* webpack/runtime/hasOwnProperty shorthand */
|
|
||||||
/******/ !function() {
|
|
||||||
/******/ __webpack_require__.o = function(obj, prop) { return Object.prototype.hasOwnProperty.call(obj, prop); }
|
|
||||||
/******/ }();
|
|
||||||
/******/
|
|
||||||
/******/ /* webpack/runtime/make namespace object */
|
|
||||||
/******/ !function() {
|
|
||||||
/******/ // define __esModule on exports
|
|
||||||
/******/ __webpack_require__.r = function(exports) {
|
|
||||||
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
|
|
||||||
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
||||||
/******/ }
|
|
||||||
/******/ Object.defineProperty(exports, '__esModule', { value: true });
|
|
||||||
/******/ };
|
|
||||||
/******/ }();
|
|
||||||
/******/
|
|
||||||
/******/ /* webpack/runtime/node module decorator */
|
|
||||||
/******/ !function() {
|
|
||||||
/******/ __webpack_require__.nmd = function(module) {
|
|
||||||
/******/ module.paths = [];
|
|
||||||
/******/ if (!module.children) module.children = [];
|
|
||||||
/******/ return module;
|
|
||||||
/******/ };
|
|
||||||
/******/ }();
|
|
||||||
/******/
|
|
||||||
/******/ /* webpack/runtime/jsonp chunk loading */
|
|
||||||
/******/ !function() {
|
|
||||||
/******/ // no baseURI
|
|
||||||
/******/
|
|
||||||
/******/ // object to store loaded and loading chunks
|
|
||||||
/******/ // undefined = chunk not loaded, null = chunk preloaded/prefetched
|
|
||||||
/******/ // [resolve, reject, Promise] = chunk loading, 0 = chunk loaded
|
|
||||||
/******/ var installedChunks = {
|
|
||||||
/******/ "core": 0
|
|
||||||
/******/ };
|
|
||||||
/******/
|
|
||||||
/******/ // no chunk on demand loading
|
|
||||||
/******/
|
|
||||||
/******/ // no prefetching
|
|
||||||
/******/
|
|
||||||
/******/ // no preloaded
|
|
||||||
/******/
|
|
||||||
/******/ // no HMR
|
|
||||||
/******/
|
|
||||||
/******/ // no HMR manifest
|
|
||||||
/******/
|
|
||||||
/******/ __webpack_require__.O.j = function(chunkId) { return installedChunks[chunkId] === 0; };
|
|
||||||
/******/
|
|
||||||
/******/ // install a JSONP callback for chunk loading
|
|
||||||
/******/ var webpackJsonpCallback = function(parentChunkLoadingFunction, data) {
|
|
||||||
/******/ var chunkIds = data[0];
|
|
||||||
/******/ var moreModules = data[1];
|
|
||||||
/******/ var runtime = data[2];
|
|
||||||
/******/ // add "moreModules" to the modules object,
|
|
||||||
/******/ // then flag all "chunkIds" as loaded and fire callback
|
|
||||||
/******/ var moduleId, chunkId, i = 0;
|
|
||||||
/******/ if(chunkIds.some(function(id) { return installedChunks[id] !== 0; })) {
|
|
||||||
/******/ for(moduleId in moreModules) {
|
|
||||||
/******/ if(__webpack_require__.o(moreModules, moduleId)) {
|
|
||||||
/******/ __webpack_require__.m[moduleId] = moreModules[moduleId];
|
|
||||||
/******/ }
|
|
||||||
/******/ }
|
|
||||||
/******/ if(runtime) var result = runtime(__webpack_require__);
|
|
||||||
/******/ }
|
|
||||||
/******/ if(parentChunkLoadingFunction) parentChunkLoadingFunction(data);
|
|
||||||
/******/ for(;i < chunkIds.length; i++) {
|
|
||||||
/******/ chunkId = chunkIds[i];
|
|
||||||
/******/ if(__webpack_require__.o(installedChunks, chunkId) && installedChunks[chunkId]) {
|
|
||||||
/******/ installedChunks[chunkId][0]();
|
|
||||||
/******/ }
|
|
||||||
/******/ installedChunks[chunkId] = 0;
|
|
||||||
/******/ }
|
|
||||||
/******/ return __webpack_require__.O(result);
|
|
||||||
/******/ }
|
|
||||||
/******/
|
|
||||||
/******/ var chunkLoadingGlobal = self["webpackChunkaircox_assets"] = self["webpackChunkaircox_assets"] || [];
|
|
||||||
/******/ chunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0));
|
|
||||||
/******/ chunkLoadingGlobal.push = webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal));
|
|
||||||
/******/ }();
|
|
||||||
/******/
|
|
||||||
/************************************************************************/
|
|
||||||
/******/
|
|
||||||
/******/ // startup
|
|
||||||
/******/ // Load entry module and return exports
|
|
||||||
/******/ // This entry module depends on other loaded chunks and execution need to be delayed
|
|
||||||
/******/ var __webpack_exports__ = __webpack_require__.O(undefined, ["chunk-vendors","chunk-common"], function() { return __webpack_require__("./src/core.js"); })
|
|
||||||
/******/ __webpack_exports__ = __webpack_require__.O(__webpack_exports__);
|
|
||||||
/******/
|
|
||||||
/******/ })()
|
|
||||||
;
|
|
1
aircox/static/aircox/public.css
Normal file
1
aircox/static/aircox/public.css
Normal file
File diff suppressed because one or more lines are too long
2
aircox/static/aircox/public.js
Normal file
2
aircox/static/aircox/public.js
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
import{A as p}from"./index.js";import"vue";window.App=p;
|
||||||
|
//# sourceMappingURL=public.js.map
|
1
aircox/static/aircox/public.js.map
Normal file
1
aircox/static/aircox/public.js.map
Normal file
|
@ -0,0 +1 @@
|
||||||
|
{"version":3,"file":"public.js","sources":["../../../assets/src/public.js"],"sourcesContent":["import \"./styles/public.scss\"\nimport './index.js'\nimport App from './app.js'\n\nwindow.App = App\n"],"names":["App"],"mappings":"2CAIA,OAAO,IAAMA"}
|
BIN
aircox/static/aircox/webfonts/fa-brands-400.eot
Normal file
BIN
aircox/static/aircox/webfonts/fa-brands-400.eot
Normal file
Binary file not shown.
3717
aircox/static/aircox/webfonts/fa-brands-400.svg
Normal file
3717
aircox/static/aircox/webfonts/fa-brands-400.svg
Normal file
File diff suppressed because it is too large
Load Diff
After Width: | Height: | Size: 730 KiB |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user