Compare commits

..

63 Commits

Author SHA1 Message Date
9378435345 (wip) episode_form: add inline track formset 2024-01-22 14:24:20 +01:00
07075d3b90 templatetags: display edit-links for admins 2024-01-22 14:24:20 +01:00
b46ae584a2 (wip): templates: update after merging branch 118 2024-01-22 14:24:20 +01:00
85cfe43cb1 templatetags: return on none type object 2024-01-22 14:24:20 +01:00
042d3d65aa templates: add in-context edition links 2024-01-22 14:24:16 +01:00
49f1aee9fc db: migrations merge 2024-01-22 11:23:57 +01:00
6417ecc628 templates: update container block names 2024-01-22 11:23:57 +01:00
9e1c4277a6 templatetags: avoid failing on nav_items when no station is defined 2024-01-22 11:23:57 +01:00
7b5a37894a signals: disable schedule_pre_save when using loaddata 2024-01-22 11:23:57 +01:00
6e6eb25c96 misc: add in-site episode management for animators 2024-01-22 11:23:57 +01:00
51db7ba5ee templates: set document type to html, prevent quicks mode 2024-01-22 11:23:57 +01:00
fff73235cd ProgramUpdateView: use ckeditor RichTextField 2024-01-22 11:23:57 +01:00
d68ba9a59e context_processors: prevent a null station error when no default station is defined 2024-01-22 11:23:57 +01:00
ac05d1b09a views/program: allow changing program cover 2024-01-22 11:23:57 +01:00
ac6b6b4f79 misc: add a profile view for authenticated users 2024-01-22 11:23:54 +01:00
f2f493cac5 misc: use the django authentication system 2024-01-22 11:23:54 +01:00
1dcdb382b0 misc: move station and audio_streams to context_processors (in order to have them available in accounts views) 2024-01-22 11:23:54 +01:00
affe4cee02 misc: edit programs in site 2024-01-22 11:23:50 +01:00
972b574299 templatetags: parametrize has_perm() in order to enable aircox namespace permissions 2024-01-19 07:43:26 +01:00
08f3a9db07 models/program: link to editor groups 2024-01-19 07:43:26 +01:00
12a9beecfd aircox/conf: user cannot edit all programs/episode 2024-01-19 07:43:26 +01:00
bkfox
ac9b3c8ede rendering styles 2024-01-16 15:37:04 +01:00
bkfox
825ed03dbd rendering styles; view order; i18n 2024-01-16 14:36:09 +01:00
bkfox
561914ee78 bkp 2024-01-09 20:05:39 +01:00
bkfox
ccea2a5ea6 player link; page rendering 2024-01-05 19:17:10 +01:00
bkfox
c52e87acd2 home page fixes; various issues fix 2024-01-05 16:23:23 +01:00
bkfox
294c848415 player button & playlist header fix; timetable order 2024-01-03 19:51:39 +01:00
bkfox
1f6381bf07 migration files 2023-12-13 17:27:42 +01:00
bkfox
73d8ff32d5 fix bug & remove dynamic 2023-12-12 21:24:21 +01:00
bkfox
46a9008cda navigation & breadcrumbs 2023-12-12 20:07:58 +01:00
bkfox
eaa1e2412a page headers, various fixes, responsive 2023-12-11 23:29:49 +01:00
bkfox
a3c21c64ed fix integration into admin interface 2023-12-10 15:47:04 +01:00
bkfox
0e444f0502 fix integration into admin interface 2023-12-10 15:21:30 +01:00
bkfox
4778803ee0 page headers 2023-12-01 20:50:28 +01:00
bkfox
9c3eaf05c7 page headers 2023-12-01 20:49:34 +01:00
bkfox
f05e47af1c page headers 2023-12-01 20:43:12 +01:00
bkfox
1de9548111 player shadow 2023-11-29 15:45:22 +01:00
bkfox
8202a9324c responsive menus 2023-11-29 15:41:15 +01:00
bkfox
f5ce00795e page loader 2023-11-29 02:05:14 +01:00
bkfox
4e04cfae7e add pdocasts 2023-11-28 02:36:24 +01:00
bkfox
d2ed8df2ac add pdocasts 2023-11-28 02:22:58 +01:00
bkfox
712ab223ba add pdocasts 2023-11-28 02:16:40 +01:00
bkfox
ed9affbef6 carousel, display logs 2023-11-28 01:23:56 +01:00
bkfox
cb5a6a3ee8 carousel, display logs 2023-11-28 01:04:39 +01:00
bkfox
bc697bd4bd clean-up css; related publications; pagination 2023-11-26 21:35:37 +01:00
bkfox
d075fecbce attach static page to page-list 2023-11-24 22:11:55 +01:00
bkfox
0c07586787 player: progress bar position 2023-11-24 21:56:58 +01:00
bkfox
9661e98a70 player: progress bar position 2023-11-24 21:39:20 +01:00
bkfox
69d77e1d0c player: progress bar position 2023-11-24 21:27:59 +01:00
bkfox
62ada47352 podcasts & player 2023-11-24 20:46:56 +01:00
bkfox
474016f776 merge develop-1.0 2023-11-22 21:17:37 +01:00
bkfox
6a21a9d094 design 2023-11-22 21:09:59 +01:00
bkfox
b4c12def13 work on templates 2023-11-22 17:33:51 +01:00
bkfox
36ae12af3d fix: static 2023-11-02 22:10:41 +01:00
bkfox
0a86d4e0a3 add statics 2023-11-02 22:09:49 +01:00
bkfox
a53aebb5b8 rm static 2023-11-02 22:07:27 +01:00
bkfox
1af0348c89 add vue files 2023-11-02 22:03:55 +01:00
bkfox
8ab8ef5b1c add missing files 2023-11-02 21:59:30 +01:00
bkfox
bf9da835b2 add missing files 2023-11-02 21:58:13 +01:00
bkfox
7b28149d7e add radiocampus app 2023-11-02 21:56:22 +01:00
bkfox
87a2ee5a45 feat: work on date menu 2023-11-02 21:54:15 +01:00
bkfox
ab231e9a89 work on design 2023-10-27 21:09:58 +02:00
bkfox
1661601caf work on design: items, components 2023-10-24 18:29:34 +02:00
121 changed files with 34777 additions and 7014 deletions

View File

@ -30,9 +30,9 @@ 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")
fields = ("type", "start", "end", "initial", "program", "schedule") fields = ("type", "start", "end", "initial", "program", "schedule")

View File

@ -50,7 +50,9 @@ class BasePageAdmin(admin.ModelAdmin):
change_form_template = "admin/aircox/page_change_form.html" 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_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)
@ -95,6 +97,7 @@ class PageAdmin(BasePageAdmin):
@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",)

View File

@ -10,7 +10,7 @@ class PageFilters(filters.FilterSet):
class Meta: class Meta:
model = Page model = Page
fields = { fields = {
"category__id": ["in"], "category__id": ["in", "exact"],
"pub_date": ["exact", "gte", "lte"], "pub_date": ["exact", "gte", "lte"],
} }

File diff suppressed because it is too large Load Diff

View File

@ -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",
),
),
]

View 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",
),
),
]

View File

@ -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",
),
),
]

View 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",
),
),
]

View 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 = []

View File

@ -8,6 +8,7 @@ __all__ = ("Article",)
class Article(Page): class Article(Page):
detail_url_name = "article-detail" detail_url_name = "article-detail"
template_prefix = "article"
objects = ProgramChildQuerySet.as_manager() objects = ProgramChildQuerySet.as_manager()

View File

@ -89,6 +89,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 +129,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,6 +192,11 @@ 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)."""

View File

@ -10,18 +10,29 @@ from .program import ProgramChildQuerySet
__all__ = ("Episode",) __all__ = ("Episode",)
class EpisodeQuerySet(ProgramChildQuerySet):
def with_podcasts(self):
return self.filter(sound__is_public=True).distinct()
class Episode(Page): class Episode(Page):
objects = ProgramChildQuerySet.as_manager() 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"
template_prefix = "episode"
@property @property
def program(self): def program(self):
return getattr(self.parent, "program", None) return self.parent_subclass
@program.setter
def program(self, value):
self.parent = value
@cached_property @cached_property
def podcasts(self): def podcasts(self):
"""Return serialized data about podcasts.""" """Return serialized data about podcasts."""
from .sound import Sound
from ..serializers import PodcastSerializer from ..serializers import PodcastSerializer
podcasts = [PodcastSerializer(s).data for s in self.sound_set.public().order_by("type")] podcasts = [PodcastSerializer(s).data for s in self.sound_set.public().order_by("type")]
@ -31,16 +42,19 @@ class Episode(Page):
else: else:
cover = None cover = None
archive_index = 1
for index, podcast in enumerate(podcasts): for index, podcast in enumerate(podcasts):
if podcast["type"] == Sound.TYPE_ARCHIVE:
if archive_index > 1:
podcast["name"] = f"{self.title} - {archive_index}"
else:
podcast["name"] = self.title
podcasts[index]["cover"] = cover podcasts[index]["cover"] = cover
podcasts[index]["page_url"] = self.get_absolute_url() podcasts[index]["page_url"] = self.get_absolute_url()
podcasts[index]["page_title"] = self.title podcasts[index]["page_title"] = self.title
return podcasts return podcasts
@program.setter
def program(self, value):
self.parent = value
class Meta: class Meta:
verbose_name = _("Episode") verbose_name = _("Episode")
verbose_name_plural = _("Episodes") verbose_name_plural = _("Episodes")

View File

@ -9,6 +9,7 @@ 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, Track
from .station import Station from .station import Station
from .page import Renderable
logger = logging.getLogger("aircox") logger = logging.getLogger("aircox")
@ -46,13 +47,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
@ -160,16 +163,17 @@ 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, log_slice=None):
"""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:
@ -189,6 +193,9 @@ class Log(models.Model):
len(logs), len(logs),
) )
if index is not None and index > 0: if index is not None and index > 0:
if log_slice:
object_list += logs[: min(log_slice, index)]
else:
object_list += logs[:index] object_list += logs[:index]
logs = logs[index:] logs = logs[index:]

View File

@ -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|&nbsp;)+", 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
@ -102,11 +116,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)
@ -117,12 +134,17 @@ class BasePage(models.Model):
if count: if count:
self.slug += "-" + str(count) self.slug += "-" + str(count)
if self.parent and not self.cover: if self.parent:
if self.parent == self:
self.parent = None
if not self.cover:
self.cover = self.parent.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 +160,28 @@ class BasePage(models.Model):
@property @property
def display_title(self): def display_title(self):
if self.is_published(): if self.is_published:
return self.title return self.title
return self.parent.display_title() return self.parent and self.parent.title or ""
@cached_property @cached_property
def headline(self): def display_headline(self):
if not self.content: if not self.content or not self.is_published:
return "" return self.parent and self.parent.display_headline or ""
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("<br>".join(headline))
@classmethod @classmethod
def get_init_kwargs_from(cls, page, **kwargs): def get_init_kwargs_from(cls, page, **kwargs):
@ -188,6 +221,23 @@ class Page(BasePage):
) )
objects = PageQuerySet.as_manager() objects = PageQuerySet.as_manager()
detail_url_name = ""
list_url_name = "page-list"
@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()
@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")
@ -209,45 +259,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 import attached
return self.attach_to and attached.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 +302,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 +310,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"#comment-{self.pk}"
class Meta: class Meta:
verbose_name = _("Comment") verbose_name = _("Comment")
@ -281,7 +323,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 +342,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)

View File

@ -64,6 +64,7 @@ class Program(Page):
objects = ProgramQuerySet.as_manager() objects = ProgramQuerySet.as_manager()
detail_url_name = "program-detail" detail_url_name = "program-detail"
list_url_name = "program-list"
@property @property
def path(self): def path(self):

View File

@ -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
) )

View File

@ -41,7 +41,7 @@ def user_default_groups(sender, instance, created, *args, **kwargs):
@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(sender, instance, created, *args, **kwargs):
if not created and instance.cover: if not created and instance.cover and "raw" not in kwargs:
Page.objects.filter(parent=instance, cover__isnull=True).update(cover=instance.cover) Page.objects.filter(parent=instance, cover__isnull=True).update(cover=instance.cover)
@ -59,7 +59,7 @@ def program_post_save(sender, instance, created, *args, **kwargs):
@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)

View File

@ -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>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -16,7 +16,7 @@
\**********************/ \**********************/
/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { /***/ (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?"); eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _assets_admin_scss__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./assets/admin.scss */ \"./src/assets/admin.scss\");\n/* harmony import */ var _index_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./index.js */ \"./src/index.js\");\n/* harmony import */ var _app__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./app */ \"./src/app.js\");\n/* harmony import */ var _components__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./components */ \"./src/components/index.js\");\n/* harmony import */ var _track__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./track */ \"./src/track.js\");\n\n\n\n\n\nconst AdminApp = {\n ..._app__WEBPACK_IMPORTED_MODULE_2__[\"default\"],\n components: {\n ..._app__WEBPACK_IMPORTED_MODULE_2__[\"default\"].components,\n ..._components__WEBPACK_IMPORTED_MODULE_3__.admin\n },\n data() {\n return {\n ...super.data,\n Track: _track__WEBPACK_IMPORTED_MODULE_4__[\"default\"]\n };\n }\n};\n/* harmony default export */ __webpack_exports__[\"default\"] = (AdminApp);\nwindow.App = AdminApp;\n\n//# sourceURL=webpack://aircox-assets/./src/admin.js?");
/***/ }), /***/ }),

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -10,13 +10,23 @@
/******/ "use strict"; /******/ "use strict";
/******/ var __webpack_modules__ = ({ /******/ var __webpack_modules__ = ({
/***/ "./src/core.js": /***/ "./src/public.js":
/*!*********************!*\ /*!***********************!*\
!*** ./src/core.js ***! !*** ./src/public.js ***!
\*********************/ \***********************/
/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { /***/ (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?"); eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _assets_public_scss__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./assets/public.scss */ \"./src/assets/public.scss\");\n/* harmony import */ var _index_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./index.js */ \"./src/index.js\");\n/* harmony import */ var _app_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./app.js */ \"./src/app.js\");\n\n\n\n/* harmony default export */ __webpack_exports__[\"default\"] = (_app_js__WEBPACK_IMPORTED_MODULE_2__[\"default\"]);\nwindow.App = _app_js__WEBPACK_IMPORTED_MODULE_2__[\"default\"];\n\n//# sourceURL=webpack://aircox-assets/./src/public.js?");
/***/ }),
/***/ "./src/assets/public.scss":
/*!********************************!*\
!*** ./src/assets/public.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/public.scss?");
/***/ }) /***/ })
@ -156,7 +166,7 @@ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _ind
/******/ // undefined = chunk not loaded, null = chunk preloaded/prefetched /******/ // undefined = chunk not loaded, null = chunk preloaded/prefetched
/******/ // [resolve, reject, Promise] = chunk loading, 0 = chunk loaded /******/ // [resolve, reject, Promise] = chunk loading, 0 = chunk loaded
/******/ var installedChunks = { /******/ var installedChunks = {
/******/ "core": 0 /******/ "public": 0
/******/ }; /******/ };
/******/ /******/
/******/ // no chunk on demand loading /******/ // no chunk on demand loading
@ -208,7 +218,7 @@ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _ind
/******/ // startup /******/ // startup
/******/ // Load entry module and return exports /******/ // Load entry module and return exports
/******/ // This entry module depends on other loaded chunks and execution need to be delayed /******/ // 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"); }) /******/ var __webpack_exports__ = __webpack_require__.O(undefined, ["chunk-vendors","chunk-common"], function() { return __webpack_require__("./src/public.js"); })
/******/ __webpack_exports__ = __webpack_require__.O(__webpack_exports__); /******/ __webpack_exports__ = __webpack_require__.O(__webpack_exports__);
/******/ /******/
/******/ })() /******/ })()

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -5,15 +5,30 @@
{% block title %}{{ user.username }}{% endblock %} {% block title %}{{ user.username }}{% endblock %}
{% endblock %} {% endblock %}
{% block main %} {% block content-container %}
<h2 class="subtitle is-3">Mes émissions</h2> <div class="container content page-content">
<h2 class="subtitle">Mon Profil</h2>
{% translate "Username" %} : {{ user.username|title }}<br/>
<!-- Connexion: {{ user.last_login }} -->
<h2 class="subtitle is-1">Mes émissions</h2>
{% if programs|length %} {% if programs|length %}
<ul> <ul>
{% for p in programs %} {% for p in programs %}
<li><a href="{% url 'program-detail' slug=p.slug %}">{{ p.title }}</a></li> <li>{{ p.title }} :
&nbsp;
<a href="{% url 'program-detail' slug=p.slug %}">
<span title="{% translate 'View' %} {{ page }}">{% translate 'View' %}</span>
</a>
&nbsp;
<a href="{% url 'program-edit' pk=p.pk %}">
<span title="{% translate 'Edit' %} {{ page }}">{% translate 'Edit' %} </span>
</a>
</li>
{% endfor %} {% endfor %}
</ul> </ul>
{% else %} {% else %}
{% trans 'You are not listed as a program editor yet' %} {% trans 'You are not listed as a program editor yet' %}
{% endif %} {% endif %}
</div>
{% endblock %} {% endblock %}

View File

@ -7,7 +7,7 @@
<li> <li>
{% if choice.type %} {% if choice.type %}
<form method="GET" action="?{{ choice.query_string }}" <form method="GET" action="?{{ choice.query_string }}"
onsubmit="return this.{{ choice.name }}.value ? true : false""> onsubmit="return this.{{ choice.name }}.value ? true : false">
<label for="filter-{{ choice.name }}">{{ choice.label }}: </label> <label for="filter-{{ choice.name }}">{{ choice.label }}: </label>
<input id="filter-{{ choice.name }}" type="{{ choice.type }}" name="{{ choice.name }}" <input id="filter-{{ choice.name }}" type="{{ choice.type }}" name="{{ choice.name }}"
value="{{ choice.value }}" {{ choice.extra }}/> value="{{ choice.value }}" {{ choice.extra }}/>

View File

@ -12,6 +12,7 @@
:init-data="{% track_inline_data formset=formset %}" :init-data="{% track_inline_data formset=formset %}"
settings-url="{% url "api:user-settings" %}" settings-url="{% url "api:user-settings" %}"
data-prefix="{{ formset.prefix }}-"> data-prefix="{{ formset.prefix }}-">
{% comment %}
<template #title> <template #title>
<h5 class="title is-4">{% trans "Playlist" %}</h5> <h5 class="title is-4">{% trans "Playlist" %}</h5>
</template> </template>
@ -79,6 +80,7 @@
</template> </template>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% endcomment %}
</a-playlist-editor> </a-playlist-editor>
</div> </div>
{% endwith %} {% endwith %}

View File

@ -4,7 +4,6 @@
<head> <head>
<title>{% block title %}{% endblock %}</title> <title>{% block title %}{% endblock %}</title>
<!-- <link rel="stylesheet" type="text/css" href="{% static "aircox/vendor.css" %}"> -->
<link rel="stylesheet" type="text/css" href="{% static "admin/css/base.css" %}"> <link rel="stylesheet" type="text/css" href="{% static "admin/css/base.css" %}">
<link rel="stylesheet" type="text/css" href="{% static "aircox/css/chunk-common.css" %}"/> <link rel="stylesheet" type="text/css" href="{% static "aircox/css/chunk-common.css" %}"/>
<link rel="stylesheet" type="text/css" href="{% static "aircox/css/chunk-vendors.css" %}"/> <link rel="stylesheet" type="text/css" href="{% static "aircox/css/chunk-vendors.css" %}"/>
@ -33,23 +32,26 @@
function vuePre(selector) { function vuePre(selector) {
const elms = document.querySelectorAll(selector) const elms = document.querySelectorAll(selector)
for(const elm of elms) { for(const elm of elms) {
elm.setAttribute('v-pre', true) // elm.setAttribute('v-pre', "")
elm.parentNode.setAttribute('v-pre', '')
} }
} }
window.addEventListener('load', function() { window.addEventListener('load', function() {
{% block init-scripts %} {% block init-scripts %}
vuePre(".django-ckeditor-widget")
vuePre("fieldset")
window.source_ = document.body.innerHTML
aircox.init(null, { aircox.init(null, {
hotReload: false,
{% if not init_app %} {% if not init_app %}
initBuilder: false, initApp: false,
{% endif %} {% endif %}
{% if init_el %} {% if init_el %}
el: "{{ init_el }}", el: "{{ init_el }}",
{% endif %} {% endif %}
}) })
{% endblock %} {% endblock %}
}) }, true)
</script> </script>
<!-- Container --> <!-- Container -->
@ -71,7 +73,7 @@
</span> </span>
<div class="navbar-dropdown is-boxed"> <div class="navbar-dropdown is-boxed">
{% for diffusion in diffusions %} {% for diffusion in diffusions %}
<a class="navbar-item {% if diffusion.is_now %}has-background-primary{% endif %}" href="{% url "admin:aircox_episode_change" diffusion.episode.pk %}"> <a class="navbar-item {% if diffusion.is_now %}active{% endif %}" href="{% url "admin:aircox_episode_change" diffusion.episode.pk %}">
{{ diffusion.start|time }} | {{ diffusion.start|time }} |
{{ diffusion.episode.title }} {{ diffusion.episode.title }}
</a> </a>

View File

@ -1,5 +1,5 @@
{% extends "admin/index.html" %} {% extends "admin/index.html" %}
{% load i18n thumbnail %} {% load i18n thumbnail aircox %}
{% block app %} {% block app %}
@ -12,43 +12,11 @@
<span>{% translate "Today" %}</span> <span>{% translate "Today" %}</span>
</h1> </h1>
{% if diffusions %} {% if diffusions %}
<table class="table is-fullwidth is-striped"> <div class="card-grid">
<tbody> {% for obj in diffusions %}
{% for diffusion in diffusions %} {% page_widget "card" obj.episode diffusion=obj timetable=True admin=True tag_class="" %}
{% with episode=diffusion.episode %}
<tr {% if diffusion.is_now %}class="is-selected"{% endif %}>
<td>{{ diffusion.start|time }} - {{ diffusion.end|time }}</td>
<td><img src="{% thumbnail episode.cover 64x64 crop %}"/></td>
<td>
<a href="{% url "admin:aircox_episode_change" episode.pk %}">{{ episode.title }}</a>
&nbsp;
{% if diffusion.type == diffusion.TYPE_ON_AIR %}
<span class="tag is-info">
<span class="icon is-small">
{% if diffusion.is_live %}
<i class="fa fa-microphone"
title="{% translate "Live diffusion" %}"></i>
{% else %}
<i class="fa fa-music"
title="{% translate "Differed diffusion" %}"></i>
{% endif %}
</span>
&nbsp;
{{ diffusion.get_type_display }}
</span>
{% elif diffusion.type == diffusion.TYPE_CANCEL %}
<span class="tag is-danger">
{{ diffusion.get_type_display }}</span>
{% elif diffusion.type == diffusion.TYPE_UNCONFIRMED %}
<span class="tag is-warning">
{{ diffusion.get_type_display }}</span>
{% endif %}
</td>
</tr>
{% endwith %}
{% endfor %} {% endfor %}
</tbody> </div>
</table>
{% else %} {% else %}
<div class="block has-text-centered"> <div class="block has-text-centered">
{% trans "No diffusion is scheduled for today." %} {% trans "No diffusion is scheduled for today." %}
@ -62,7 +30,9 @@
<span>{% translate "Latest comments" %}</span> <span>{% translate "Latest comments" %}</span>
</h1> </h1>
{% if comments %} {% if comments %}
{% include "aircox/widgets/page_list.html" with object_list=comments with_title=True %} {% for object in comments|slice:":5" %}
{% page_widget "item" object with_title=True %}
{% endfor %}
<div class="has-text-centered"> <div class="has-text-centered">
<a href="{% url "admin:aircox_comment_changelist" %}" class="float-center">{% translate "All comments" %}</a> <a href="{% url "admin:aircox_comment_changelist" %}" class="float-center">{% translate "All comments" %}</a>
</div> </div>

View File

@ -23,9 +23,10 @@ Usefull context:
{% block assets %} {% block assets %}
<link rel="stylesheet" type="text/css" href="{% static "aircox/css/chunk-common.css" %}"/> <link rel="stylesheet" type="text/css" href="{% static "aircox/css/chunk-common.css" %}"/>
<link rel="stylesheet" type="text/css" href="{% static "aircox/css/chunk-vendors.css" %}"/> <link rel="stylesheet" type="text/css" href="{% static "aircox/css/chunk-vendors.css" %}"/>
<link rel="stylesheet" type="text/css" href="{% static "aircox/css/public.css" %}"/>
<script src="{% static "aircox/js/chunk-common.js" %}"></script> <script src="{% static "aircox/js/chunk-common.js" %}"></script>
<script src="{% static "aircox/js/chunk-vendors.js" %}"></script> <script src="{% static "aircox/js/chunk-vendors.js" %}"></script>
<script src="{% static "aircox/js/core.js" %}"></script> <script src="{% static "aircox/js/public.js" %}"></script>
{% endblock %} {% endblock %}
<title> <title>
@ -47,127 +48,102 @@ Usefull context:
}) })
</script> </script>
<div id="app"> <div id="app">
{% block top-nav-container %} <div class="container navs">
<nav class="navbar has-shadow" role="navigation" aria-label="main navigation"> {% block nav %}
<div class="container"> <nav class="nav primary" role="navigation" aria-label="main navigation">
<div class="navbar-brand"> {% block nav-primary %}
<a href="/" title="{% translate "Home" %}" class="navbar-item"> <a class="nav-brand" href="{% url "home" %}">
<img src="{{ station.logo.url }}" class="logo"/> <img src="{{ station.logo.url }}">
</a> </a>
</div> <a-switch class="button burger"
<div class="navbar-menu"> el=".nav.primary .nav-menu" group="nav"
<div class="navbar-start"> aria-label="{% translate "Main menu" %}">
{% block top-nav %} </a-switch>
{% nav_items "top" css_class="navbar-item" active_class="is-active" as items %} <div class="nav-menu">
{% block nav-primary-menu %}
{% nav_items "top" css_class="nav-item" active_class="active" as items %}
{% for item, render in items %} {% for item, render in items %}
{{ render }} {{ render }}
{% endfor %} {% endfor %}
{% endblock %} {% if user.is_staff %}
</div> <a class="nav-item" href="{% url "admin:index" %}" target="new">
<div class="navbar-end"> {% translate "Admin" %}
{% block top-nav-tools %} </a>
{% endblock %} {% endif %}
{% block top-nav-end %}
<div class="navbar-item">
<form action="{% url 'page-list' %}" method="GET">
<div class="control has-icons-left">
<span class="icon is-small is-left">
<i class="fa fa-search"></i>
</span>
<input type="text" name="q" class="input"
placeholder="{% translate "Search" %}" />
</div>
</form>
</div>
{% endblock %}
{% if user.is_authenticated %} {% if user.is_authenticated %}
<div class="navbar-item"> <a class="nav-item" href="{% url "profile" %}" target="new">
<a href="{% url 'profile' %}">{{ user.username }}</a> &nbsp; <a href="{% url 'logout' %}"> <i class="fa fa-power-off"></i></a> {% translate "Profile" %}
</div> </a>
<a class="nav-item" href="{% url 'logout' %}">
<i title="{% translate 'disconnect' %}" class="fa fa-power-off"></i>
</a>
{% endif %} {% endif %}
{% endblock %}
</div> </div>
</div> {% endblock %}
</div>
</nav> </nav>
{% endblock %}
<div class="container"> {% block secondary-nav %}{% endblock %}
<div class="columns is-desktop"> {% endblock %}
<main class="column page"> </div>
<header class="header">
{% block main-container %}
<main class="page">
{% block main %}
{% spaceless %}
{% block breadcrumbs-container %}
<div class="breadcrumbs container">
{% block breadcrumbs %}{% endblock %}
</div>
{% endblock %}
{% endspaceless %}
{% block header-container %}
{% if page or cover or title %}
<header class="container header preview preview-header {% if cover %}has-cover{% endif %}">
{% block header %} {% block header %}
<h1 class="title is-1"> {% if cover %}
{% block title %} <img src="{{ cover }}" class="header-cover">
{% if page and page.title %}
{{ page.title }}
{% endif %} {% endif %}
{% endblock %} <div class="headings preview-card-headings">
</h1> {% block headings %}
<div>
<h3 class="subtitle is-3"> <h1 class="heading title is-1 {% block title-class %}{% endblock %}">{% block title %}{{ title|default:"" }}{% endblock %}</h1>
{% block subtitle %}{% endblock %} {% include "aircox/edit-link.html" %}
</h3> </div>
<div>
<div class="columns is-size-4"> {% spaceless %}
{% block header_nav %} <span class="heading subtitle is-2">
<span class="column"> {% block subtitle %}
{% block header_crumbs %} {% if subtitle %}
{% if parent %} {{ subtitle }}
<a href="{{ parent.get_absolute_url }}">
{{ parent.title }}</a></li>
{% endif %} {% endif %}
{% endblock %} {% endblock %}
</span> </span>
{% endspaceless %}
</span>
</div>
{% endblock %} {% endblock %}
</div> </div>
{% endblock %} {% endblock %}
</header> </header>
{% endif %}
{% endblock %}
{% block main %} {% block content-container %}
{% block content %}
{% if page and page.content %} {% if page and page.content %}
<section class="page-content mb-2">{{ page.content|safe }}</section> <div class="container content page-content">
{% block content %}
{{ page.content|safe }}
{% endblock %}
</div>
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% endblock main %} {% endblock %}
</main> </main>
{% if has_sidebar %}
{% comment %}Translators: main sidebar {% endcomment %}
<aside class="column is-one-third-desktop">
{# FIXME: block cover into sidebar one #}
{% block cover %}
{% if page and page.cover %}
<img class="cover mb-4" src="{{ page.cover.url }}" class="cover"/>
{% endif %}
{% endblock %} {% endblock %}
{% with is_thin=True %}
{% block sidebar %}
{% if sidebar_object_list %}
{% with object_list=sidebar_object_list %}
{% with list_url=sidebar_list_url %}
{% with has_headline=False %}
<section>
<h4 class="title is-4">
{% block sidebar_title %}{% translate "Recently" %}{% endblock %}
</h4>
{% include "aircox/widgets/page_list.html" %}
{% endwith %}
{% endwith %}
{% endwith %}
{% endif %}
</section>
{% endblock %}
{% endwith %}
</aside>
{% endif %}
</div>
</div>
<hr>
</div> </div>
{% block player-container %} {% block player-container %}
<div id="player">{% include "aircox/widgets/player.html" %}</div> <div id="player">{% include "aircox/widgets/player.html" %}</div>

View File

@ -6,3 +6,5 @@
&mdash; &mdash;
{{ station.name }} {{ station.name }}
{% endblock %} {% endblock %}
{% block header %}{% if page %}{{ block.super }}{% endif %}{% endblock %}

View File

@ -1,86 +1,68 @@
{% extends "aircox/base.html" %} {% extends "./base.html" %}
{% comment %}Display a list of BasePages{% endcomment %} {% comment %}Display a list of BasePages{% endcomment %}
{% load i18n aircox %} {% load i18n aircox %}
{% block head_title %} {% block head_title %}
{% block title %} {% block title %}
{% if not page or not page.title %} {{ block.super }}
{% if not parent %}{{ view.model|verbose_name:True|title }}
{% else %}
{% with parent.title as title %}
{% with model|default:"Publications"|verbose_name:True|capfirst as model %}
{% comment %}Translators: title when pages are filtered for a specific parent page, e.g.: Articles of My Incredible Show{% endcomment %}
{% blocktranslate %}{{ model }} of {{ title }}{% endblocktranslate %}
{% endwith %}
{% endwith %}
{% endif %}
{% else %}{{ block.super }}
{% endif %}
{% endblock %} {% endblock %}
&mdash; &mdash;
{{ station.name }} {{ station.name }}
{% endblock %} {% endblock %}
{% block main %}{{ block.super }} {% block main %}
{{ block.super }}
{% block before_list %}{% endblock %} {% block list-container %}
<section class="container clear-both" role="list">
<section role="list"> {% block list %}
{% block pages_list %}
{% with has_headline=True %} {% with has_headline=True %}
{% for object in object_list %} {% for object in object_list %}
{% block list_object %} {% block list_object %}
{% include object.item_template_name|default:item_template_name %} {% page_widget item_widget|default:"item" object %}
{% endblock %} {% endblock %}
{% empty %} {% empty %}
{% blocktranslate %}There is nothing published here...{% endblocktranslate %} {% blocktranslate %}There is nothing published here...{% endblocktranslate %}
{% endfor %} {% endfor %}
{% endwith %} {% endwith %}
{% endblock %} {% endblock %}
</section> </section>
{% block list-pagination %}
{% if is_paginated %} {% if is_paginated %}
<hr/> <hr/>
{% update_query request.GET.copy page=None as GET %} {% update_query request.GET.copy page=None as GET %}
{% with GET.urlencode as GET %} {% with GET.urlencode as GET %}
<nav class="pagination is-centered" role="pagination" aria-label="{% translate "pagination" %}"> <nav class="nav-urls is-centered" role="pagination" aria-label="{% translate "pagination" %}">
{% block pagination %} <ul class="urls">
{% if page_obj.has_previous %} {% if page_obj.has_previous %}
<a href="?{{ GET }}&page={{ page_obj.previous_page_number }}" class="pagination-previous">
{% else %}
<a class="pagination-previous" disabled>
{% endif %}
{% comment %}Translators: Bottom of the list, "previous page"{% endcomment %} {% comment %}Translators: Bottom of the list, "previous page"{% endcomment %}
{% translate "Previous" %}</a> <a href="?{{ GET }}&page={{ page_obj.previous_page_number }}" class="left button"
title="{% translate "Previous" %}"
aria-label="{% translate "Previous" %}">
<span class="icon"><i class="fa fa-chevron-left"></i></span>
</a>
{% endif %}
{% if page_obj.has_next %} {% if page_obj.has_next %}
<a href="?{{ GET }}&page={{ page_obj.next_page_number }}" class="pagination-next">
{% else %}
<a class="pagination-next" disabled>
{% endif %}
{% comment %}Translators: Bottom of the list, "Nextpage"{% endcomment %} {% comment %}Translators: Bottom of the list, "Nextpage"{% endcomment %}
{% translate "Next" %}</a> <a href="?{{ GET }}&page={{ page_obj.next_page_number }}" class="right button"
title="{% translate "Next" %}"
<ul class="pagination-list"> aria-label="{% translate "Next" %}">
{% for i in paginator.page_range %} <span class="icon"><i class="fa fa-chevron-right"></i></span>
<li> </a>
{% comment %} {% endif %}
<form action="?{{ GET }}">
{% for get in GET %}
<input type="hidden" name="{{ get.0 }}" value="{{ get.1 }}" />
{% endfor %}
<input type="number" name="page" value="{{ page_obj.number }}" />
</form>
{% endcomment %}
<a class="pagination-link {% if page_obj.number == i %}is-current{% endif %}"
href="?{{ GET }}&page={{ i }}">{{ i }}</a>
</li>
{% endfor %}
</ul> </ul>
{% endblock %}
</nav> </nav>
{% endwith %} {% endwith %}
{% endif %} {% endif %}
{% endblock %}
{% endblock %} {% endblock %}
</div>
{% endblock %}

View File

@ -14,16 +14,18 @@
{% block subtitle %}{{ date|date:"l d F Y" }}{% endblock %} {% block subtitle %}{{ date|date:"l d F Y" }}{% endblock %}
{% block before_list %} {% block secondary-nav %}
{% with "diffusion-list" as url_name %} <nav class="nav secondary">
{% include "aircox/widgets/dates_menu.html" %} {% include "./widgets/dates_menu.html" with url_name="diffusion-list" %}
{% endwith %} </nav>
{% endblock %} {% endblock %}
{% block pages_list %} {% block pages_list %}
{% with hide_schedule=True %} {% with hide_schedule=True %}
<section role="list"> <section role="list" class="list">
{% include 'aircox/widgets/diffusion_list.html' %} {% for object in object_list %}
{% page_widget "item" object.episode diffusion=object timetable=True %}
{% endfor %}
</section> </section>
{% endwith %} {% endwith %}
{% endblock %} {% endblock %}

View File

@ -0,0 +1,20 @@
{% load aircox i18n %}
{% block user-actions-container %}
{% has_perm page page.program.change_permission_codename simple=True as can_edit %}
{% if user.is_authenticated and can_edit %}
{% with request.resolver_match.view_name as view_name %}
&nbsp;
{% if view_name in 'program-edit,bla' %}
<!--
<a href="{% url 'program-detail' page.slug %}" target="_self">
<span title="{% translate 'View' %} {{ page }}">{% translate 'View' %} 👁 </span>
</a>
-->
{% else %}
<a href="{% url view_name|edit_view page.pk %}" target="_self">
<span title="{% translate 'Edit' %} {{ page }}">{% translate 'Edit' %} 🖉 </span>
</a>
{% endif %}
{% endwith %}
{% endif %}
{% endblock %}

View File

@ -2,92 +2,54 @@
{% comment %}List of a show's episodes for a specific{% endcomment %} {% comment %}List of a show's episodes for a specific{% endcomment %}
{% load i18n aircox %} {% load i18n aircox %}
{% include "aircox/program_sidebar.html" %}
{% block content-container %}
{{ block.container }}
{% block top-nav-tools %}
{% has_perm page page.program.change_permission_codename simple=True as can_edit %}
{% if can_edit %}
<a class="navbar-item" href="{% url 'episode-edit' page.pk %}" target="_self">
<span class="icon is-small">
<i class="fa fa-pen"></i>
</span>&nbsp;
<span>{% translate "Edit" %}</span>
</a>
{% endif %}
{% endblock %}
{% block content %}
<a-episode :page="{title: &quot;{{ page.title }}&quot;, podcasts: {{ object.podcasts|json }}}"> <a-episode :page="{title: &quot;{{ page.title }}&quot;, podcasts: {{ object.podcasts|json }}}">
<template v-slot="{podcasts,page}"> <template v-slot="{podcasts,page}">
{{ block.super }} {{ block.super }}
{% if object.podcasts %} {% if object.podcasts %}
<section> {% spaceless %}
<section class="container no-border">
<h3 class="title is-3">{% translate "Podcasts" %}</h3>
<a-playlist v-if="page" :set="podcasts" <a-playlist v-if="page" :set="podcasts"
name="{{ page.title }}" name="{{ page.title }}"
list-class="menu-list" item-class="menu-item" list-class="menu-list" item-class="menu-item"
:player="player" :actions="['play']" :player="player" :actions="['play']"
@select="player.playItems('queue', $event.item)"> @select="player.playItems('queue', $event.item)">
<template v-slot:header>
<h4 class="title is-4">{% translate "Podcasts" %}</h4>
</template>
</a-playlist> </a-playlist>
{% comment %} </section>
{% for object in podcasts %} {% endspaceless %}
{% include "aircox/widgets/podcast_item.html" %}
{% endfor %}
{% endcomment %}
</section>
{% endif %}
{% if tracks %}
<section>
<h4 class="title is-4">{% translate "Playlist" %}</h4>
<ol>
{% for track in tracks %}
<li><span>{{ track.title }}</span>
<span class="has-text-grey-dark has-text-weight-light">
&mdash; {{ track.artist }}
{% if track.info %}(<i>{{ track.info }}</i>){% endif %}
</span>
</li>
{% endfor %}
</ol>
</section>
{% endif %}
</template></a-episode>
{% endblock %}
{% block sidebar %}
<section>
<h4 class="title is-4">{% translate "Diffusions" %}</h4>
<ul>
{% for diffusion in object.diffusion_set.all %}
<li>
{% with diffusion.start as start %}
{% with diffusion.end as end %}
<time datetime="{{ start }}">{{ start|date:"D. d F Y, H:i" }}</time>
&mdash;
<time datetime="{{ end }}">{{ end|date:"H:i" }}</time>
{% endwith %}
{% endwith %}
<small>
{% if diffusion.initial %}
{% with diffusion.initial.date as date %}
<span title="{% blocktranslate %}Rerun of {{ date }}{% endblocktranslate %}">
({% translate "rerun" %})
</span>
{% endwith %}
{% endif %} {% endif %}
</small>
<br> {% if tracks %}
</li> <section class="container">
<h3 class="title is-3">{% translate "Playlist" %}</h3>
<table class="table is-hoverable is-fullwidth">
<thead>
<tr>
<th></th>
<th>{% translate "Artist" %}</th>
<th>{% translate "Title" %}</th>
<th></th>
</thead>
<tbody>
{% for track in tracks %}
<tr>
<td>{{ forloop.counter }}</td>
<td>{{ track.artist }}</td>
<td>{{ track.title }}</td>
<td>{{ track.info|default:"" }}</td>
</tr>
{% endfor %} {% endfor %}
</ul> </tbody>
</section> </table>
{{ block.super }} </section>
{% endif %}
</template>
</a-episode>
{% endblock %} {% endblock %}

View File

@ -9,22 +9,22 @@
{% block init-scripts %} {% block init-scripts %}
{% endblock %} {% endblock %}
{% block top-nav-tools %} {% block comments %}
<a class="navbar-item" href="{% url 'episode-detail' object.slug %}" target="_self">
<span class="icon is-small">
<i class="fa fa-eye"></i>
</span>&nbsp;
<span>{% translate "View" %}</span>
</a>
{% endblock %} {% endblock %}
{% block main %} {% block content-container %}
<section class="container">
<form method="post" enctype="multipart/form-data">{% csrf_token %} <form method="post" enctype="multipart/form-data">{% csrf_token %}
<table> <table>
{{ form.as_table }} {{ form.as_table }}
{% render_honeypot_field "website" %} {% render_honeypot_field "website" %}
</table> </table>
<br/> <br/>
{{ forms }}
<br/>
<input type="submit" value="Update" class="button is-success"> <input type="submit" value="Update" class="button is-success">
</form> </form>
</section>
{% endblock %} {% endblock %}

View File

@ -18,7 +18,7 @@
{% endblock %} {% endblock %}
{% block content %} {% block content-container %}
<article class="message is-danger"> <article class="message is-danger">
<div class="message-header"> <div class="message-header">
<p>{% block error_title %}{% trans "An error occurred" %}{% endblock %}</p> <p>{% block error_title %}{% trans "An error occurred" %}{% endblock %}</p>

View File

@ -1,85 +1,70 @@
{% extends "aircox/page_list.html" %} {% extends "aircox/base.html" %}
{% comment %}Home page{% endcomment %} {% load i18n aircox %}
{% load i18n %}
{% block head_title %}{{ station.name }}{% endblock %} {% block head_title %}{{ station.name }}{% endblock %}
{% block title %} {% block title %}{% if page %}{{ block.super }}{% endif %}{% endblock %}
{% if not page or not page.title %}{{ station.name }}
{% else %}{{ block.super }}
{% endif %}
{% endblock %}
{% block before_list %}{% endblock %}
{% block pages_list %} {% block breadcrumbs-container %}{% endblock %}
{% if page and page.content %}<hr/>{% endif %}
{% block content-container %}
{{ block.super }}
{% if next_diffs %} {% if next_diffs %}
<div class="columns"> <section class="container">
{% with render_card=True %} <h2 class="title is-3 p-2">
{% for object in next_diffs %} {% with station.name as station %}
{% with is_primary=object.is_now %} {% blocktrans %}Today on {{ station }}{% endblocktrans %}
<div class="column is-relative"> {% endwith %}
<h4 class="card-super-title" title="{{ object.start }}"> </h2>
{% if is_primary %}
<span class="fas fa-play"></span>
<time datetime="{{ object.start }}">
{% translate "Currently" %}
</time>
{% else %}
{{ object.start|date:"H:i" }}
{% endif %}
{% if object.episode.category %} <div class="mb-3">
// {{ object.episode.category.title }} {% with next_diffs.0 as obj %}
{% endif %} {% page_widget "wide" obj.episode diffusion=obj timetable=True %}
</h4> {% endwith %}
{% include object.item_template_name %}
</div> </div>
{% endwith %}
{% endfor %}
{% endwith %}
</div>
{% endif %}
{% if object_list %} <a-carousel section-class="card-grid">
<h4 class="title is-4">{% translate "Today" %}</h4> {% for obj in next_diffs|slice:"1:" %}
<section role="list"> {% if object != diffusion %}
{% include 'aircox/widgets/diffusion_list.html' %} {% page_widget "card" obj.episode diffusion=obj timetable=True %}
{% endif %}
{% endfor %}
</a-carousel>
</section> </section>
{% endif %} {% endif %}
{% endblock %}
{% block pagination %} {% if logs %}
<ul class="pagination-list"> <section class="container">
<li> <h2 class="title is-3 p-2">{% translate "It just happened" %}</h2>
<a href="{% url "page-list" %}" class="pagination-link" {% for object in logs %}
aria-label="{% translate "Show all publication" %}"> {% include "./widgets/log.html" with widget="item" %}
{% translate "More publications..." %} {% endfor %}
<nav class="nav-urls">
<a href="{% url "timetable-list" %}"
aria-label="{% translate "Show all program's for today" %}">
{% translate "Today" %}
</a> </a>
</li> </nav>
</ul> </section>
{% endif %}
{% if podcasts %}
<section class="container">
<h2 class="title is-3 p-2">{% translate "Last podcasts" %}</h2>
{% include "./widgets/carousel.html" with objects=podcasts url_name="podcast-list" url_label=_("All podcasts") %}
</section>
{% endif %}
{% if publications %}
<section class="container">
<h2 class="title is-3 p-2">{% translate "Last publications" %}</h2>
{% include "./widgets/carousel.html" with objects=publications url_name="page-list" url_label=_("All publications") %}
</section>
{% endif %}
{% endblock %} {% endblock %}
{% block pages_list %}{% endblock %}
{% block sidebar %}
<section>
<h4 class="title is-4">{% translate "Previously on air" %}</h4>
{% with has_cover=False %}
{% with logs as object_list %}
{% include "aircox/widgets/log_list.html" %}
{% endwith %}
{% endwith %}
</section>
<section>
<h4 class="title is-4">{% translate "Last publications" %}</h4>
{% with hide_schedule=True %}
{% with has_headline=False %}
{% for object in last_publications %}
{% include object.item_template_name|default:'aircox/widgets/page_item.html' %}
{% endfor %}
{% endwith %}
{% endwith %}
</section>
{% endblock %}

View File

@ -15,15 +15,31 @@
{% block subtitle %}{{ date|date:"l d F Y" }}{% endblock %} {% block subtitle %}{{ date|date:"l d F Y" }}{% endblock %}
{% block before_list %}
{% with "log-list" as url_name %} {% block secondary-nav %}
{% include "aircox/widgets/dates_menu.html" %} <nav class="nav secondary">
{% endwith %} {% include "./widgets/dates_menu.html" with url_name="log-list" %}
</nav>
{% endblock %} {% endblock %}
{% block pages_list %}
{% block pages_list_ %}
<section> <section>
{# <h4 class="subtitle size-4">{{ date }}</h4> #} {# <h4 class="subtitle size-4">{{ date }}</h4> #}
{% include "aircox/widgets/log_list.html" %} {% include "aircox/widgets/log_list.html" %}
</section> </section>
{% endblock %} {% endblock %}
{% block pages_list %}
{% with hide_schedule=True %}
<section role="list" class="list">
{% for object in object_list %}
{% if object.episode %}
{% page_widget "item" object.episode diffusion=object timetable=True %}
{% else %}
{% page_widget "item" object timetable=True %}
{% endif %}
{% endfor %}
</section>
{% endwith %}
{% endblock %}

View File

@ -6,83 +6,83 @@ Base template used to display a Page
Context: Context:
- page: page - page: page
- parent: parent page - parent: parent page
- related_objects: list of object to display as related publications
- related_url: url to the full list of related_objects
{% endcomment %} {% endcomment %}
{% block header_crumbs %} {% block breadcrumbs %}
{{ block.super }} {% if parent %}
{% if page.category %} {% include "./widgets/breadcrumbs.html" with page=parent %}
{% if parent %} / {% endif %} {{ page.category.title }} {% if page %}
<a href="{% url page.list_url_name parent_slug=parent.slug %}">
{{ page|verbose_name:True }}
</a>
{% endif %}
{% elif page %}
{% include "./widgets/breadcrumbs.html" with page=page no_title=True %}
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% block top-nav-tools %}
{% has_perm page "change" as can_edit %}
{% if can_edit %}
<a class="navbar-item" href="{{ page|admin_url:'change' }}"
target="new">
<span class="icon is-small">
<i class="fa fa-pen"></i>
</span>&nbsp;
<span>{% translate "Edit" %}</span>
</a>
{% endif %}
{% endblock %}
{% block main %} {% block main %}
{{ block.super }} {{ block.super }}
{% block comments %} {% block related %}
{% if comments or comment_form %} {% if related_objects %}
<section class="mt-6"> <section class="container">
<h4 class="title is-4">{% translate "Comments" %}</h4> {% with models=object|verbose_name:True %}
<h3 class="title is-3">{% blocktranslate %}Related {{models}}{% endblocktranslate %}</h3>
{% for comment in comments %} {% include "./widgets/carousel.html" with objects=related_objects url_name=object.list_url_name url_category=object.category %}
<div class="media box"> {% endwith %}
<div class="media-content"> {% endif %}
<p> {% endblock %}
<strong class="mr-2">{{ comment.nickname }}</strong>
<time datetime="{{ comment.date }}" title="{{ comment.date }}"> {% block comments %}
<small>{{ comment.date|naturaltime }}</small> {% if comments %}
</time> <section class="container">
<br> <h2 class="title">{% translate "Comments" %}</h2>
{{ comment.content }}
</p> {% for object in comments %}
</div> <div id="comment-{{ object.pk }}">
{% page_widget "item" object %}
</div> </div>
{% endfor %} {% endfor %}
</section>
{% endif %}
{% if comment_form %} {% if comment_form %}
<section class="container">
<h2 class="title">{% translate "Post a comment" %}</h2>
<form method="POST"> <form method="POST">
<h5 class="title is-5">{% translate "Post a comment" %}</h5>
{% csrf_token %} {% csrf_token %}
{% render_honeypot_field "website" %} {% render_honeypot_field "website" %}
{% for field in comment_form %}
<div class="field is-horizontal">
<div class="field-label is-normal">
<label class="label">
{{ field.label_tag }}
</label>
</div>
<div class="field-body">
<div class="field"> <div class="field">
<p class="control is-expanded">{{ field }}</p> <div class="control">
{{ comment_form.content }}
</div>
</div>
{% for field in comment_form %}
{% if field.name != "content" %}
<div class="field is-horizontal">
<label class="label">{{ field.label }}</label>
<div class="control">{{ field }}</div>
</div>
{% if field.errors %} {% if field.errors %}
<p class="help is-danger">{{ field.errors }}</p> <p class="help is-danger">{{ field.errors }}</p>
{% endif %} {% endif %}
{% if field.help_text %} {% if field.help_text %}
<p class="help">{{ field.help_text|safe }}</p> <p class="help">{{ field.help_text|safe }}</p>
{% endif %} {% endif %}
</div> {% endif %}
</div>
</div>
{% endfor %} {% endfor %}
<div class="has-text-right"> <div class="has-text-right">
<button type="reset" class="button is-danger">{% translate "Reset" %}</button> <button type="submit" class="button">{% translate "Post comment" %}</button>
<button type="submit" class="button is-success">{% translate "Post comment" %}</button>
</div> </div>
</form> </form>
{% endif %}
</section> </section>
{% endif %} {% endif %}

View File

@ -2,61 +2,60 @@
{% comment %}Display a list of Pages{% endcomment %} {% comment %}Display a list of Pages{% endcomment %}
{% load i18n aircox %} {% load i18n aircox %}
{% block before_list %} {% block secondary-nav %}
{{ block.super }} {% if not parent and categories %}
<nav class="nav secondary">
{% if view.has_filters and object_list %} <div class="nav-menu nav-categories">
<form method="GET" action="" class="media"> {% for cat in categories %}
<div class="media-content"> <a class="nav-item{% if cat == category %} active{% endif %}"
{% block filters %} href="{% url request.resolver_match.url_name category_slug=cat.slug %}">
<div class="field is-horizontal"> {{ cat.title }}
<div class="field-label"> </a>
<label class="label">{% translate "Search" %}</label>
</div>
<div class="field-body">
<div class="field">
<div class="control has-icons-left">
<span class="icon is-small is-left">
<i class="fa fa-search"></i>
</span>
<input class="input" type="text" name="q"
value="{{ filterset_data.q }}"
placeholder="{% translate "Search content" %}">
</div>
</div>
</div>
</div>
<div class="field is-horizontal">
<div class="field-label">
<label class="label">{% translate "Categories" %}</label>
</div>
<div class="field-body">
<div class="field is-narrow">
<div class="control">
{% for label, value in categories %}
<label class="checkbox">
<input type="checkbox" class="checkbox" name="category__id__in"
value="{{ value }}"
{% if value in filterset_data.category__id__in %}checked{% endif %} />
{{ label }}
</label>
{% endfor %} {% endfor %}
</div> </div>
</div> <a-switch class="button burger"
</div> el=".nav-categories" group="nav" icon="fas fa-tags"
</div> aria-label="{% translate "Categories" %}">
{% endblock %} </a-switch>
</div> </nav>
<div class="media-right">
<div class="field is-grouped is-grouped-right">
<div class="control">
<button class="button is-primary"/>{% translate "Apply" %}</button>
</div>
<div class="control">
<a href="?" class="button is-secondary">{% translate "Reset" %}</a>
</div>
</div>
</div>
</form>
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% block title %}
{% if parent %}{{ parent.title }}
{% else %}{{ block.super }}
{% endif %}
{% endblock %}
{% block header %}
{% if page and not object %}
{% with page as object %}
{{ block.super }}
{% endwith %}
{% else %}
{{ block.super }}
{% endif %}
{% endblock %}
{% block breadcrumbs %}
{% if parent and model.list_url_name %}
{% include "./widgets/breadcrumbs.html" with page=parent %}
<a href="{% url model.list_url_name %}">{{ model|verbose_name:True }}</a>
{% elif page and model.list_url_name %}
<a href="{% url model.list_url_name %}">{{ page.title }}</a>
{% if category %}
<a href="{% url request.resolver_match.url_name category_slug=category.slug %}">
{{ category.title }}
</a>
{% endif %}
{% else %}
<a href="{% url request.resolver_match.url_name %}">{{ model|verbose_name:True }}</a>
{% if category %}
<a href="{% url request.resolver_match.url_name category_slug=category.slug %}">
{{ category.title }}
</a>
{% endif %}
{% endif %}
{% endblock %}
{% block content-container %}{% endblock %}

View File

@ -1,19 +1,6 @@
{% extends "aircox/basepage_detail.html" %} {% extends "aircox/page_detail.html" %}
{% load static i18n humanize honeypot aircox %} {% comment %}Detail page of a show{% endcomment %}
{% comment %} {% load i18n aircox %}
Base template used to display a Page
Context:
- page: page
- parent: parent page
{% endcomment %}
{% block header_crumbs %}
{{ block.super }}
{% if page.category %}
{% if parent %} / {% endif %} {{ page.category.title }}
{% endif %}
{% endblock %}
{% block top-nav-tools %} {% block top-nav-tools %}
{% has_perm page page.change_permission_codename simple=True as can_edit %} {% has_perm page page.change_permission_codename simple=True as can_edit %}
@ -27,63 +14,52 @@ Context:
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% block main %} {% block content-container %}
{% with schedules=program.schedule_set.all %}
{% if schedules %}
<section class="container schedules">
{% for schedule in schedules %}
<div class="schedule">
<div class="heading">
<span class="day">{{ schedule.get_frequency_display }}</span>
{% with schedule.start|date:"H:i" as start %}
{% with schedule.end|date:"H:i" as end %}
<time datetime="{{ start }}">{{ start }}</time>
&mdash;
<time datetime="{{ end }}">{{ end }}</time>
{% endwith %}
{% endwith %}
<small>
{% if schedule.is_rerun %}
{% with schedule.initial.date as date %}
<span title="{% blocktranslate %}Rerun of {{ date }}{% endblocktranslate %}">
({% translate "Rerun" %})
</span>
{% endwith %}
{% endif %}
</small>
</div>
</div>
{% endfor %}
</section>
{% endif %}
{% endwith %}
{{ block.super }} {{ block.super }}
{% block comments %} {% if episodes %}
{% if comments or comment_form %} <section class="container">
<section class="mt-6"> <h3 class="title is-3">{% translate "Last Episodes" %}</h3>
<h4 class="title is-4">{% translate "Comments" %}</h4> {% include "./widgets/carousel.html" with objects=episodes url_name="episode-list" url_parent=object url_label=_("All episodes") %}
</section>
{% endif %}
{% for comment in comments %}
<div class="media box">
<div class="media-content">
<p>
<strong class="mr-2">{{ comment.nickname }}</strong>
<time datetime="{{ comment.date }}" title="{{ comment.date }}">
<small>{{ comment.date|naturaltime }}</small>
</time>
<br>
{{ comment.content }}
</p>
</div>
</div>
{% endfor %}
{% if comment_form %} {% if articles %}
<form method="POST"> <section class="container">
<h5 class="title is-5">{% translate "Post a comment" %}</h5> <h3 class="title is-3">{% translate "Last Articles" %}</h3>
{% csrf_token %} {% include "./widgets/carousel.html" with objects=articles url_name="article-list" url_parent=object url_label=_("All articles") %}
{% render_honeypot_field "website" %}
{% for field in comment_form %}
<div class="field is-horizontal">
<div class="field-label is-normal">
<label class="label">
{{ field.label_tag }}
</label>
</div>
<div class="field-body">
<div class="field">
<p class="control is-expanded">{{ field }}</p>
{% if field.errors %}
<p class="help is-danger">{{ field.errors }}</p>
{% endif %}
{% if field.help_text %}
<p class="help">{{ field.help_text|safe }}</p>
{% endif %}
</div>
</div>
</div>
{% endfor %}
<div class="has-text-right">
<button type="reset" class="button is-danger">{% translate "Reset" %}</button>
<button type="submit" class="button is-success">{% translate "Post comment" %}</button>
</div>
</form>
{% endif %}
</section> </section>
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% endblock %}

View File

@ -1,4 +1,4 @@
{% extends "aircox/basepage_detail.html" %} {% extends "aircox/page_detail.html" %}
{% load static i18n humanize honeypot aircox %} {% load static i18n humanize honeypot aircox %}
@ -9,16 +9,12 @@
{% block init-scripts %} {% block init-scripts %}
{% endblock %} {% endblock %}
{% block top-nav-tools %} {% block comments %}
<a class="navbar-item" href="{% url 'program-detail' object.slug %}" target="_self">
<span class="icon is-small">
<i class="fa fa-eye"></i>
</span>&nbsp;
<span>{% translate "View" %}</span>
</a>
{% endblock %} {% endblock %}
{% block main %} {% block content-container %}
<section class="container">
<div>
<form method="post" enctype="multipart/form-data">{% csrf_token %} <form method="post" enctype="multipart/form-data">{% csrf_token %}
<table> <table>
{{ form.as_table }} {{ form.as_table }}
@ -27,4 +23,6 @@
<br/> <br/>
<input type="submit" value="Update" class="button is-success"> <input type="submit" value="Update" class="button is-success">
</form> </form>
</div>
</section>
{% endblock %} {% endblock %}

View File

@ -1,6 +0,0 @@
{% block sidebar_title %}
{% with program.title as program %}
{% blocktranslate %}Recently on {{ program }}{% endblocktranslate %}
{% endwith %}
{% endblock %}

View File

@ -0,0 +1,27 @@
{% extends "aircox/page_list.html" %}
{% comment %}List of diffusions as a timetable{% endcomment %}
{% load i18n aircox humanize %}
{% block subtitle %}{{ date|date:"l d F Y" }}{% endblock %}
{% block secondary-nav %}
<nav class="nav secondary">
{% include "./widgets/dates_menu.html" with url_name=view.redirect_date_url %}
</nav>
{% endblock %}
{% block breadcrumbs %}
{{ block.super }}
<a href="{% url "timetable-list" date=date %}">{{ date|date:"l d F Y" }}</a>
{% endblock %}
{% block list %}
{% for object in object_list %}
{% if object.episode %}
{% page_widget "item" object.episode diffusion=object timetable=True %}
{% else %}
{% page_widget "item" object timetable=True %}
{% endif %}
{% endfor %}
{% endblock %}

View File

@ -0,0 +1,4 @@
{% extends "./page.html" %}
{% load humanize %}
{% block subtitle %}{{ object.pub_date.date }}{% endblock %}

View File

@ -11,63 +11,48 @@ Context variables:
- is_thin (=False): if True, smaller cover and display less info - is_thin (=False): if True, smaller cover and display less info
{% endcomment %} {% endcomment %}
{% if render_card %} {% block outer %}
<article class="card {% if is_primary %}is-primary{% endif %}"> <article class="preview preview-item{% if is_primary %}is-primary{% endif %}{% block card_class %}{% endblock %}">
<header class="card-image"> {% block inner %}
<a href="{{ object.get_absolute_url }}"> <header class="headings"
<figure class="image is-4by3"> style="background-image: url({{ object.cover.url }})">
<img src="{% thumbnail object.cover|default:station.default_cover 480x480 %}"> {% block headings %}
</figure> <div>
</a> <span class="heading subtitle">{% block subtitle %}{% endblock %}</span>
</div>
{% endblock %}
</header> </header>
<div class="card-header">
<h4 class="title"> <div class="">
<a href="{{ object.get_absolute_url }}"> <div>
{% block card_title %}{{ object.title }}{% endblock %} <h2 class="heading title">{% block title %}{% endblock %}</h2>
</div>
<summary class="heading-container">
{% block content %}
{% if content and with_content %}
{% autoescape off %}
{{ content|striptags|truncatewords:64|linebreaks }}
{% endautoescape %}
{% endif %}
{% endblock %}
</summary>
<div class="actions">
{% block actions %}
<a class="button float-right" href="{{ object.get_absolute_url|escape }}">
<span class="icon">
<i class="fas fa-external-link"></i>
</span>
<label>{% translate "More infos" %}</label>
</a> </a>
</h4>
</div>
</article>
{% else %}
<article class="media item {% block css %}{% endblock%}">
{% if has_cover|default_if_none:True %}
<div class="media-left">
{% if is_thin %}
<img src="{% thumbnail object.cover|default:station.default_cover 64x64 crop=scale %}"
class="cover is-tiny">
{% else %}
<img src="{% thumbnail object.cover|default:station.default_cover 128x128 crop=scale %}"
class="cover is-small">
{% endif %}
</div>
{% endif %}
<div class="media-content">
<h5 class="title is-5 has-text-weight-normal">
{% block title %}
{% if object.is_published %}
<a href="{{ object.get_absolute_url }}">{{ object.title }}</a>
{% else %}
{{ object.title }}
{% endif %}
{% endblock %}
</h5>
<div class="subtitle is-6 has-text-weight-light">
{% block subtitle %}
{% if object.category %}{{ object.category.title }}{% endif %}
{% endblock %} {% endblock %}
</div> </div>
{% if has_headline|default_if_none:True %}
<div class="headline">
{% block headline %}{{ object.headline }}{% endblock %}
</div>
{% endif %}
</div> </div>
{% endblock %}
{% if not no_actions %} {% if with_container %}
{% block actions %}{% endblock %} </div>
{% endif %} {% endif %}
</article> </article>
{% endif %} {% endblock %}

View File

@ -0,0 +1,15 @@
{% load aircox %}
<a href="{% url page.list_url_name %}">
{{ page|verbose_name:True }}
</a>
{% if page.category and not no_cat %}
<a href="{% url page.list_url_name category_slug=page.category.slug %}">
{{ page.category.title }}
</a>
{% endif %}
{% if not no_title %}
<a href="{{ page.get_absolute_url }}">
{{ page.title|truncatechars:24 }}
</a>
{% endif %}

View File

@ -0,0 +1,19 @@
{% extends "./preview.html" %}
{% load i18n %}
{% block tag-class %}{{ block.super }} preview-card{% endblock %}
{% block tag-extra %}
{{ block.super }}
{% if cover %}
style="background-image: url({{ cover }});"
{% endif %}
{% endblock %}
{% block headings-class %}{{ block.super }} preview-card-headings{% endblock %}
{% block inner %}
{% block headings-container %}
{{ block.super }}
{% block actions-container %}{{ block.super }}{% endblock %}
{% endblock %}
{% endblock %}

View File

@ -0,0 +1,28 @@
{% load aircox %}
{% comment %}
Context:
- objects: list of objects to display
- url_name: url name to show the full list
- url_parent: parent page for the full list
- url_label: label of url button
{% endcomment %}
<a-carousel section-class="card-grid">
{% for object in objects %}
{% page_widget "card" object %}
{% endfor %}
</a-carousel>
{% if url_name %}
<nav class="nav-urls">
{% if url_parent %}
<a href="{% url url_name parent_slug=url_parent.slug %}">
{% elif url_category %}
<a href="{% url url_name category_slug=url_category.slug %}">
{% else %}
<a href="{% url url_name %}">
{% endif %}
{{ url_label|default:_("Show all") }}
</a>
</nav>
{% endif %}

View File

@ -0,0 +1,50 @@
{% extends "./page.html" %}
{% load i18n humanize aircox %}
{% block tag-class %}{{ block.super }} comment{% endblock %}
{% block outer %}
{% if with_title %}
{% with url=object.get_absolute_url %}
{{ block.super }}
{{ block.super }}
{% endwith %}
{% else %}
{{ block.super }}
{{ block.super }}
{% endif %}
{% endblock %}
{% block title %}
{{ object.nickname }} &mdash; {{ object.date }}
{% endblock %}
{% block subtitle %}
{% if with_title %}
{{ object.parent.title }}
{% endif %}
{% endblock %}
{% block content %}{{ object.content }}{% endblock %}
{% block actions %}
{{ block.super }}
{% if request.user.is_staff %}
<a href="{% url "admin:aircox_comment_change" object.pk %}" class="button"
title="{% trans "Edit comment" %}"
aria-label="{% trans "Edit comment" %}">
<span class="fa fa-edit"></span>
</a>
<a class="button is-danger"
title="{% trans "Delete comment" %}"
aria-label="{% trans "Delete comment" %}"
href="{% url "admin:aircox_comment_delete" object.pk %}">
<span class="fa fa-trash-alt"></span>
</a>
{# <a href="mailto:{{ object.email }}">{{ object.nickname }}</a> #}
{% endif %}
{% endblock %}

View File

@ -11,36 +11,33 @@ An empty date results to a title or a separator
{% endcomment %} {% endcomment %}
{% load i18n %} {% load i18n %}
<div class="media" role="menu" <a-switch class="button burger"
aria-label="{% translate "pick a date" %}"> el=".nav-dates" icon="far fa-calendar" group="nav"
<div class="media-content"> aria-label="{% translate "Dates" %}">
<div class="tabs is-toggle"> </a-switch>
<ul>
{% for day in dates %}
<li class="{% if day == date %}is-active{% endif %}">
<a href="{% url url_name date=day %}">
{{ day|date:"D. d" }}
</a>
</li>
{% endfor %}
</ul>
</div>
</div>
<div class="media-right"> <div class="nav-menu nav-dates">
<form action="{% url url_name %}" method="GET" class="navbar-body" {% for day in dates %}
aria-label="{% translate "Jump to date" %}"> <a href="{% url url_name date=day %}" class="nav-item {% if day == date %}active{% endif %}">
<div class="field has-addons"> {{ day|date:"l d" }}
<div class="control has-icons-left"> </a>
<span class="icon is-small is-left"><span class="far fa-calendar"></span></span> {% endfor %}
<input type="{{ date_input|default:"date" }}" class="input date"
name="date" value="{{ date|date:"Y-m-d" }}"> <a-dropdown class="nav-item align-right flex-grow-0 dropdown is-right"
</div> content-class="dropdown-menu"
<div class="control"> button-tag="span" button-class="dropdown-trigger"
{% comment %}Translators: form button to select a date{% endcomment %} button-icon-open="fa-solid fa-plus" button-icon-close="fa-solid fa-minus">
<button class="button is-primary">{% translate "Go" %}</button> <template #default>
<div class="dropdown-content">
<div class="dropdown-item">
<h4>{% translate "Pick a date" %}</h4>
<v-calendar mode="date" borderless
:initial-page="{month: {{date.month}}, year: {{date.year}}}"
@dayclick="(event) => window.aircox.pickDate({% url url_name %}, event)"
color="yellow"
/>
</div> </div>
</div> </div>
</form> </template>
</div> </a-dropdown>
</div> </div>

View File

@ -3,19 +3,4 @@ Context:
- object_list: object list - object_list: object list
- date: date for list - date: date for list
{% endcomment %} {% endcomment %}
<table id="timetable{% if date %}-{{ date|date:"Y-m-d" }}{% endif %}" class="timetable"> {% load aircox %}
{% for diffusion in object_list %}
<tr class="{% if diffusion.is_now %}has-background-primary{% endif %}">
<td class="pr-2 pb-2">
<time datetime="{{ diffusion.start|date:"c" }}">
{{ diffusion.start|date:"H:i" }} - {{ diffusion.end|date:"H:i" }}
</time>
</td>
<td class="pb-2">
{% with diffusion.episode as object %}
{% include "aircox/widgets/episode_item.html" %}
{% endwith %}
</td>
</tr>
{% endfor %}
</table>

View File

@ -0,0 +1,68 @@
{% extends "./page.html" %}
{% load i18n humanize aircox %}
{% block outer %}
{% with diffusion.is_now as is_active %}
{% if admin %}
{% with object|admin_url:"change" as url %}
{{ block.super }}
{% endwith %}
{% else %}
{{ block.super }}
{% endif %}
{% endwith %}
{% endblock %}
{% block subtitle %}
{% if diffusion %}
{% if timetable %}
{{ diffusion.start|date:"H:i" }}
&mdash;
{{ diffusion.end|date:"H:i" }}
{% else %}
{{ diffusion.start|naturalday }},
{{ diffusion.start|date:"H:i" }}
{% endif %}
{% else %}
{{ block.super }}
{% endif %}
{% endblock %}
{% block actions %}
{{ block.super }}
{% if admin and diffusion %}
{% if diffusion.type == diffusion.TYPE_ON_AIR %}
<span class="tag is-info">
<span class="icon is-small">
{% if diffusion.is_live %}
<i class="fa fa-microphone"
title="{% translate "Live diffusion" %}"></i>
{% else %}
<i class="fa fa-music"
title="{% translate "Differed diffusion" %}"></i>
{% endif %}
</span>
&nbsp;
{{ diffusion.get_type_display }}
</span>
{% elif diffusion.type == diffusion.TYPE_CANCEL %}
<span class="tag is-danger">
{{ diffusion.get_type_display }}</span>
{% elif diffusion.type == diffusion.TYPE_UNCONFIRMED %}
<span class="tag is-warning">
{{ diffusion.get_type_display }}</span>
{% endif %}
{% endif %}
{% if object.sound_set.count %}
<button class="button action" @click="player.playButtonClick($event)"
data-sounds="{{ object.podcasts|json }}">
<span class="icon is-small">
<span class="fas fa-play"></span>
</span>
<label>{% translate "Listen" %}</label>
</button>
{% endif %}
{% endblock %}

View File

@ -1,51 +1,41 @@
{% extends "aircox/widgets/page_item.html" %} {% extends "./basepage_item.html" %}
{% comment %} {% load i18n humanize %}
List item for an episode.
Context variables:
- object: episode
- diffusion: episode's diffusion
- hide_schedule: if True, do not display start time
{% endcomment %}
{% load i18n easy_thumbnails_tags aircox %}
{% block title %} {% block title %}
{% if not object.is_published and object.program.is_published %} {% if not object.is_published and object.program.is_published %}
<a href="{{ object.program.get_absolute_url }}"> <a href="{{ object.program.get_absolute_url }}">
{{ object.program.title }} {{ object.program.title }}
{% if diffusion %} </a>
&mdash;
{{ diffusion.start|date:"d F" }}
{% endif %}
</a>
{% else %} {% else %}
{{ block.super }} {{ block.super }}
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% block class %}
{% if object.is_now %}is-active{% endif %}
{% endblock %}
{% block subtitle %} {% block subtitle %}
{{ block.super }}
{% if diffusion %} {% if diffusion %}
{% if not hide_schedule %} {{ diffusion.start|naturalday }},
{% if object.category %}&mdash;{% endif %} {{ diffusion.start|date:"g:i" }}
<time datetime="{{ diffusion.start|date:"c" }}" title="{{ diffusion.start }}"> {% else %}
{{ diffusion.start|date:"d M, H:i" }} {{ block.super }}
</time>
{% endif %}
{% if diffusion.initial %}
{% with diffusion.initial.date as date %}
<span title="{% blocktranslate %}Rerun of {{ date }}{% endblocktranslate %}">
{% translate "(rerun)" %}
</span>
{% endwith %}
{% endif %}
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% block actions %}
{% if object.sound_set.public.count %}
<button class="button" @click="player.playButtonClick($event)"
data-sounds="{{ object.podcasts|json }}">
<span class="icon is-small">
<span class="fas fa-play"></span>
</span>
</button>
{% endif %}
{% endblock %}
{% block actions %} {% block actions %}
{% has_perm page object.program.change_permission_codename simple=True as can_edit %} {% has_perm page object.program.change_permission_codename simple=True as can_edit %}
{% if can_edit %} {% if can_edit %}

View File

@ -0,0 +1,42 @@
{% extends "./preview.html" %}
{% load i18n aircox %}
{% block tag-class %}{{ block.super }} list-item is-fullwidth{% endblock %}
{% block headings %}
<a href="{{ url|escape }}" class="heading title {% block title-class %}{% endblock %}">
{% block title %}{{ title|default:"" }}{% endblock %}
</a>
<span class="heading subtitle {% block subtitle-class %}{% endblock %}">
{% block subtitle %}{{ subtitle|default:"" }}{% endblock %}
</span>
{% endblock %}
{% block inner %}
{% block headings-container %}{{ block.super }}{% endblock %}
{% block content-container %}
<div class="media">
{% if object.cover %}
<a href="{{ object.get_absolute_url }}"
class="media-left preview-cover small"
style="background-image: url({{ object.cover.url }})">
</a>
{% endif %}
<div class="media-content">
<section class="content">
{% block content %}
{% if content and with_content %}
{% autoescape off %}
{{ content|striptags|linebreaks }}
{% endautoescape %}
{% endif %}
{% endblock %}
</section>
{% block actions-container %}
{{ block.super }}
{% endblock %}
</div>
{% endblock %}
{% endblock %}

View File

@ -11,12 +11,10 @@ for design review.
{% endcomment %} {% endcomment %}
{% block outer %}
{% if object|is_diffusion %} {% if object|is_diffusion %}
{% with object as diffusion %} {% page_widget widget object.episode diffusion=object %}
{% include "aircox/widgets/diffusion_item.html" %}
{% endwith %}
{% else %} {% else %}
{% with object.track as object %} {% include "./track_item.html" with object=object.track log=object %}
{% include "aircox/widgets/track_item.html" %}
{% endwith %}
{% endif %} {% endif %}
{% endblock %}

View File

@ -1,30 +0,0 @@
{% comment %}
Render list of logs (as widget).
Context:
- object_list: list of logs to display
- is_thin: if True, hide some information in order to fit in a thin container
{% endcomment %}
{% load aircox %}
{% with True as hide_schedule %}
<table class="table is-striped is-hoverable is-fullwidth" role="list">
{% for object in object_list %}
<tr {% if object|is_diffusion and object.is_now %}class="is-selected"{% endif %}>
<td>
{% if object|is_diffusion %}
<time datetime="{{ object.start }}" title="{{ object.start }}">
{{ object.start|date:"H:i" }}
{% if not is_thin %} - {{ object.end|date:"H:i" }}{% endif %}
</time>
{% else %}
<time datetime="{{ object.date }}" title="{{ object.date }}">
{{ object.date|date:"H:i" }}
</time>
{% endif %}
</td>
<td>{% include "aircox/widgets/log_item.html" %}</td>
</tr>
{% endfor %}
</table>
{% endwith %}

View File

@ -0,0 +1,36 @@
{% extends widget_template %}
{% load i18n aircox %}
{% block outer %}
{% with cover|default:object.cover_url as cover %}
{% if admin %}
{% with object|admin_url:"change" as url %}
{{ block.super }}
{% endwith %}
{% else %}
{% with url|default:object.get_absolute_url as url %}
{{ block.super }}
{% endwith %}
{% endif %}
{% endwith %}
{% endblock %}
{% block title %}
{% if title %}
{{ block.super }}
{% elif object %}
{{ object.display_title }}
{% endif %}
{% endblock %}
{% block content %}
{% if content %}
{{ content }}
{% elif object %}
{{ block.super }}
{{ object.display_headline }}
{% endif %}
{% endblock %}

View File

@ -0,0 +1,13 @@
{% extends widget|default:"./card.html" %}
{% block outer %}
{% if object %}
{% with content=object.get_display_excerpt() %}
{% with title=object.get_display_title() %}
{{ block.super }}
{% endwith %}
{% endwith %}
{% else %}
{{ block.super }}
{% endif %}
{% endblock %}

View File

@ -3,3 +3,11 @@
{% block card_title %} {% block card_title %}
{% block title %}{{ block.super }}{% endblock %} {% block title %}{{ block.super }}{% endblock %}
{% endblock %} {% endblock %}
{% block card_subtitle %}
{% block subtitle %}{{ block.super }}{% endblock %}
{% endblock %}
{% block card_class %}
{% block class %}{{ block.super }}{% endblock %}
{% endblock %}

View File

@ -5,10 +5,10 @@ Context:
- object_list: object list - object_list: object list
- list_url: url to complete list page - list_url: url to complete list page
{% endcomment %} {% endcomment %}
{% load i18n %} {% load i18n aircox %}
{% for object in object_list %} {% for object in object_list %}
{% include object.item_template_name %} {% page_widget "item" object %}
{% endfor %} {% endfor %}
{% if list_url %} {% if list_url %}

View File

@ -5,7 +5,7 @@ The audio player
<br> <br>
<div class="box is-fullwidth is-fixed-bottom is-paddingless player" <div class="is-fullwidth is-fixed-bottom is-paddingless player-container"
role="{% translate "player" %}" role="{% translate "player" %}"
aria-description="{% translate "Audio player used to listen to the radio and podcasts" %}"> aria-description="{% translate "Audio player used to listen to the radio and podcasts" %}">
<noscript> <noscript>
@ -22,24 +22,27 @@ The audio player
:live-args="{% player_live_attr %}" :live-args="{% player_live_attr %}"
button-title="{% translate "Play or pause audio" %}"> button-title="{% translate "Play or pause audio" %}">
<template v-slot:content="{ loaded, live, current }"> <template v-slot:content="{ loaded, live, current }">
<h4 v-if="loaded" class="title is-4"> <h4 v-if="loaded" class="title">
<a v-if="current?.data?.page_url" :href="current.data.page_url">
[[ loaded.name ]] [[ loaded.name ]]
</a>
<template v-else>[[ loaded.name ]]</template>
</h4> </h4>
<h4 v-else-if="current && current.data.type == 'track'" <h4 v-else-if="current && current.data.type == 'track'"
class="title is-4" aria-description="{% translate "Track currently on air" %}"> class="title" aria-description="{% translate "Track currently on air" %}">
<span class="has-text-info is-size-3">&#9836;</span> <span class="has-text-info is-size-3 mr-3">&#9836;</span>
<span>[[ current.data.title ]]</span> <span>[[ current.data.title ]]</span>
<span class="has-text-grey-dark has-text-weight-light"> <span class="has-text-grey-dark has-text-weight-light">
&mdash; [[ current.data.artist ]] &mdash; [[ current.data.artist ]]
<i v-if="current.data.info">([[ current.data.info ]])</i> <i v-if="current.data.info">([[ current.data.info ]])</i>
</span> </span>
</h4> </h4>
<div v-else-if="live && current && current.data.type == 'diffusion'"> <h4 v-else-if="live && current && current.data.type == 'diffusion'"
<h4 class="title is-4" aria-description="{% translate "Diffusion currently on air" %}"> class="title"
<a :href="current.data.url">[[ current.data.title ]]</a> aria-description="{% translate "Diffusion currently on air" %}">
<a :href="current.data.url" v-if="current.data.url">[[ current.data.title ]]</a>
<template v-else>[[ current.data.title ]]</template>
</h4> </h4>
<div class="">[[ current.data.info ]]</div>
</div>
<h4 v-else class="title is-4" aria-description="{% translate "Currently playing" %}"> <h4 v-else class="title is-4" aria-description="{% translate "Currently playing" %}">
{{ request.station.name }} {{ request.station.name }}
</h4> </h4>

View File

@ -0,0 +1,55 @@
{% load i18n %}
{% comment %}
Content related context:
- object: object to display
- cover: cover
- title: title
- subtitle: subtitle
- content: content to display
Styling related context:
- is_active: add "active" css class
- is_small: add "small" css class
- is_tiny: add "tiny" css class
- tag
- tag_class: css class to set to main tag
- tag_extra: extra tag attributes
{% endcomment %}
{% block outer %}
<{{ tag|default:"article" }} class="preview {% if not cover %}no-cover {% endif %}{% if is_active %}active {% endif %}{% block tag-class %}{{ tag_class|default:"" }} {% endblock %}" {% block tag-extra %}{% endblock %}>
{% block inner %}
{% block headings-container %}
<header class="headings{% block headings-class %}{% endblock %}"{% block headings-tag-extra %}{% endblock %}>
{% block headings %}
<div>
<a href="{{ url|escape }}" class="heading title {% block title-class %}{% endblock %}"{% if title %} title="{{ title|escape }}"{% endif %}>{% block title %}{{ title|default:"" }}{% endblock %}</a>
</div>
<div>
<span class="heading subtitle {% block subtitle-class %}{% endblock %}">{% block subtitle %}{{ subtitle|default:"" }}{% endblock %}</span>
</div>
{% endblock %}
</header>
{% endblock %}
{% block content-container %}
<section class="content headings-container">
{% block content %}
{% if content and with_content %}
{% autoescape off %}
{{ content|striptags|linebreaks }}
{% endautoescape %}
{% endif %}
{% endblock %}
</section>
{% endblock %}
{% block actions-container %}
<div class="actions">
{% block actions %}{% endblock %}
</div>
{% endblock %}
{% endblock %}
</{{ tag|default:"article" }}>
{% endblock %}

View File

@ -5,9 +5,18 @@ Context:
- object: track to render - object: track to render
{% endcomment %} {% endcomment %}
<span class="has-text-info is-size-5">&#9836;</span> <span class="content mr-2">
<span>{{ object.title }}</span> <span class="icon highlight-color-2 m-2">
<span class="has-text-grey-dark has-text-weight-light"> <i class="fas fa-music"></i>
&mdash; {{ object.artist }} </span>
{% if object.info %}(<i>{{ object.info }}</i>){% endif %} {% if log %}
<span>{{ log.date|date:"H:i" }} &mdash; </span>
{% endif %}
<span class="has-text-weight-bold">{{ object.title }}</span>
{% if object.artist and object.artist != object.title %}
<span>
&mdash; {{ object.artist }}
{% if object.info %}(<i>{{ object.info }}</i>){% endif %}
</span>
{% endif %}
</span> </span>

View File

@ -0,0 +1,20 @@
{% extends "./preview.html" %}
{% load i18n %}
{% block tag-class %}{{ block.super }} preview-wide{% endblock %}
{% block headings-class %}{{ block.super }} preview-card-headings{% endblock %}
{% block headings-tag-extra %}
{{ block.super }}
{% if cover %}
style="background-image: url({{ cover }});"
{% endif %}
{% endblock %}
{% block inner %}
{% block headings-container %}{{ block.super }}{% endblock %}
<div class="is-flex-direction-column">
{% block content-container %}{{ block.super }}{% endblock %}
{% block actions-container %}{{ block.super }}{% endblock %}
</div>
{% endblock %}

View File

@ -1,8 +1,8 @@
{% extends "aircox/base.html" %} {% extends "aircox/base.html" %}
{% load i18n aircox %} {% load i18n aircox %}
{% block main %} {% block content-container %}
<div class="container content page-content">
<h2>{% trans "Log in" %}</h2> <h2>{% trans "Log in" %}</h2>
<br/> <br/>
<form method="post" action="{% url 'login' %}"> <form method="post" action="{% url 'login' %}">
@ -11,10 +11,8 @@
{{ form.as_table }} {{ form.as_table }}
</table> </table>
<br/> <br/>
<button type="submit">{% trans "Log in" %}</button> <button class="button" type="submit">{% trans "Log in" %}</button>
<input type="hidden" name="next" value="{{ next }}"> <input type="hidden" name="next" value="{{ next }}">
</form> </form>
</div>
{{ block.super }}
{% endblock %} {% endblock %}

View File

@ -3,14 +3,41 @@ import random
from django import template from django import template
from django.contrib.admin.templatetags.admin_urls import admin_urlname from django.contrib.admin.templatetags.admin_urls import admin_urlname
from django.template.loader import render_to_string
from django.urls import reverse from django.urls import reverse
from aircox.models import Diffusion, Log from aircox.models import Diffusion, Log
random.seed() random.seed()
register = template.Library() register = template.Library()
@register.filter(name="admin_url")
def admin_url(obj, action):
meta = obj._meta
return reverse(f"admin:{meta.app_label}_{meta.model_name}_{action}", args=[obj.id])
@register.simple_tag(name="page_widget", takes_context=True)
def do_page_widget(context, widget, object, dir="aircox/widgets", **ctx):
"""Render widget for the provided page and context."""
ctx["request"] = context["request"]
ctx["object"] = object
ctx["widget"] = widget
if object.pk and not ctx.get("tag_id"):
model = type(object)._meta.model_name
ctx["tag_id"] = f"{widget}_{model}_{object.pk}"
ctx["widget_template"] = f"{dir}/{widget}.html"
return render_to_string(object.get_template_name(widget), ctx)
@register.filter(name="page_template")
def do_page_template(self, page, component):
"""For a provided page object and component name, return template name."""
return page.get_template(component)
@register.filter(name="admin_url") @register.filter(name="admin_url")
def do_admin_url(obj, arg, pass_id=True): def do_admin_url(obj, arg, pass_id=True):
"""Reverse admin url for object.""" """Reverse admin url for object."""
@ -32,10 +59,12 @@ def do_get_tracks(obj):
@register.simple_tag(name="has_perm", takes_context=True) @register.simple_tag(name="has_perm", takes_context=True)
def do_has_perm(context, obj, perm, user=None, simple=False): def do_has_perm(context, obj, perm, user=None, simple=False):
"""Return True if ``user.has_perm('[APP].[perm]_[MODEL]')``""" """Return True if ``user.has_perm('[APP].[perm]_[MODEL]')``"""
if not obj:
return
if user is None: if user is None:
user = context["request"].user user = context["request"].user
if simple: if simple:
return user.has_perm("aircox.{}".format(perm)) return user.has_perm("aircox.{}".format(perm)) or user.is_superuser
else: else:
return user.has_perm("{}.{}_{}".format(obj._meta.app_label, perm, obj._meta.model_name)) return user.has_perm("{}.{}_{}".format(obj._meta.app_label, perm, obj._meta.model_name))
@ -69,13 +98,25 @@ def do_player_live_attr(context):
@register.simple_tag(name="nav_items", takes_context=True) @register.simple_tag(name="nav_items", takes_context=True)
def do_nav_items(context, menu, **kwargs): def do_nav_items(context, menu, **kwargs):
"""Render navigation items for the provided menu name.""" """Render navigation items for the provided menu name."""
if not getattr(context["request"], "station"):
return []
station, request = context["station"], context["request"] station, request = context["station"], context["request"]
return [(item, item.render(request, **kwargs)) for item in station.navitem_set.filter(menu=menu)] return [(item, item.render(request, **kwargs)) for item in station.navitem_set.filter(menu=menu)]
@register.filter(name="nav_active")
def do_nav_active(obj, request):
if request.path.startswith(obj.get_url()):
return True
return False
@register.simple_tag(name="update_query") @register.simple_tag(name="update_query")
def do_update_query(obj, **kwargs): def do_update_query(obj, **kwargs):
"""Replace provided querydict's values with **kwargs.""" """Replace provided querydict's values with **kwargs.
Values set to ``None`` will be dropped.
"""
for k, v in kwargs.items(): for k, v in kwargs.items():
if v is not None: if v is not None:
obj[k] = list(v) if hasattr(v, "__iter__") else [v] obj[k] = list(v) if hasattr(v, "__iter__") else [v]
@ -88,4 +129,11 @@ def do_update_query(obj, **kwargs):
def do_verbose_name(obj, plural=False): def do_verbose_name(obj, plural=False):
"""Return model's verbose name (singular or plural) or `obj` if it is a """Return model's verbose name (singular or plural) or `obj` if it is a
string (can act for default values).""" string (can act for default values)."""
return obj if isinstance(obj, str) else obj._meta.verbose_name_plural if plural else obj._meta.verbose_name if isinstance(obj, str):
return obj
return obj._meta.verbose_name_plural if plural else obj._meta.verbose_name
@register.filter(name="edit_view")
def do_edit_view(obj):
return "%s-edit" % obj.split("-")[0]

View File

@ -16,10 +16,10 @@ def test_edit_program(user, client, program):
client.force_login(user) client.force_login(user)
response = client.get(reverse("program-detail", kwargs={"slug": program.slug})) response = client.get(reverse("program-detail", kwargs={"slug": program.slug}))
assert response.status_code == 200 assert response.status_code == 200
assert b"fa-pen" not in response.content assert "🖉 ".encode() not in response.content
user.groups.add(program.editors) user.groups.add(program.editors)
response = client.get(reverse("program-detail", kwargs={"slug": program.slug})) response = client.get(reverse("program-detail", kwargs={"slug": program.slug}))
assert b"fa-pen" in response.content assert "🖉 ".encode() in response.content
assert b"foobar" not in response.content assert b"foobar" not in response.content
response = client.post(reverse("program-edit", kwargs={"pk": program.pk}), {"content": "foobar"}) response = client.post(reverse("program-edit", kwargs={"pk": program.pk}), {"content": "foobar"})
response = client.get(reverse("program-detail", kwargs={"slug": program.slug})) response = client.get(reverse("program-detail", kwargs={"slug": program.slug}))

View File

@ -1,6 +1,5 @@
import pytest import pytest
from django.urls import reverse
from aircox import models from aircox import models
from aircox.test import Interface from aircox.test import Interface
@ -37,28 +36,14 @@ class TestBaseView:
def test_station(self, base_view, station): def test_station(self, base_view, station):
assert base_view.station == station assert base_view.station == station
@pytest.mark.django_db
def test_get_sidebar_queryset(self, base_view, pages, published_pages):
query = base_view.get_sidebar_queryset().values_list("id", flat=True)
page_ids = {r.id for r in published_pages}
assert set(query) == page_ids
@pytest.mark.django_db
def test_get_sidebar_url(self, base_view):
assert base_view.get_sidebar_url() == reverse("page-list")
@pytest.mark.django_db @pytest.mark.django_db
def test_get_context_data(self, base_view, station, published_pages): def test_get_context_data(self, base_view, station, published_pages):
base_view.has_sidebar = True
base_view.get_sidebar_queryset = lambda: published_pages
context = base_view.get_context_data() context = base_view.get_context_data()
assert context == { assert context == {
"view": base_view, "view": base_view,
"station": station,
"page": None, # get_page() returns None "page": None, # get_page() returns None
"has_sidebar": base_view.has_sidebar, "audio_streams": station.streams,
"has_filters": False,
"sidebar_object_list": published_pages[: base_view.list_count],
"sidebar_list_url": base_view.get_sidebar_url(),
"model": base_view.model, "model": base_view.model,
} }

View File

@ -38,36 +38,52 @@ api = [
urls = [ urls = [
path("", views.HomeView.as_view(), name="home"), path("", views.HomeView.as_view(), name="home"),
path("api/", include((api, "aircox"), namespace="api")), path("api/", include((api, "aircox"), namespace="api")),
# path('', views.PageDetailView.as_view(model=models.Article), # ---- ---- objects views
# name='home'), # ---- articles
path( path(
_("articles/"), _("articles/"),
views.ArticleListView.as_view(model=models.Article), views.ArticleListView.as_view(model=models.Article),
name="article-list", name="article-list",
), ),
path(
_("articles/c/<slug:category_slug>/"), views.ArticleListView.as_view(model=models.Article), name="article-list"
),
path( path(
_("articles/<slug:slug>/"), _("articles/<slug:slug>/"),
views.ArticleDetailView.as_view(), views.ArticleDetailView.as_view(),
name="article-detail", name="article-detail",
), ),
# ---- episodes
path(_("episodes/"), views.EpisodeListView.as_view(), name="episode-list"), path(_("episodes/"), views.EpisodeListView.as_view(), name="episode-list"),
path(_("episodes/c/<slug:category_slug>/"), views.EpisodeListView.as_view(), name="episode-list"),
path( path(
_("episodes/<slug:slug>/"), _("episodes/<slug:slug>/"),
views.EpisodeDetailView.as_view(), views.EpisodeDetailView.as_view(),
name="episode-detail", name="episode-detail",
), ),
path(_("week/"), views.DiffusionListView.as_view(), name="diffusion-list"),
path( path(
_("week/<date:date>/"), _("episode/<pk>/edit/"),
views.DiffusionListView.as_view(), views.EpisodeUpdateView.as_view(),
name="diffusion-list", name="episode-edit",
), ),
path(_("logs/"), views.LogListView.as_view(), name="log-list"), path(_("podcasts/"), views.PodcastListView.as_view(), name="podcast-list"),
path(_("logs/<date:date>/"), views.LogListView.as_view(), name="log-list"), path(_("podcasts/c/<slug:category_slug>/"), views.PodcastListView.as_view(), name="podcast-list"),
# path('<page_path:path>', views.route_page, name='page'), # ---- timetable
path(_("timetable/"), views.TimeTableView.as_view(), name="timetable-list"),
path(
_("timetable/<date:date>/"),
views.TimeTableView.as_view(),
name="timetable-list",
),
# ---- pages
path( path(
_("publications/"), _("publications/"),
views.PageListView.as_view(model=models.Page), views.PageListView.as_view(model=models.Page, attach_to_value=models.StaticPage.Target.PAGES),
name="page-list",
),
path(
_("publications/c/<slug:category_slug>"),
views.PageListView.as_view(model=models.Page, attach_to_value=models.StaticPage.Target.PAGES),
name="page-list", name="page-list",
), ),
path( path(
@ -86,42 +102,33 @@ urls = [
), ),
name="static-page-detail", name="static-page-detail",
), ),
# ---- programs
path(_("programs/"), views.ProgramListView.as_view(), name="program-list"), path(_("programs/"), views.ProgramListView.as_view(), name="program-list"),
path(_("programs/c/<slug:category_slug>/"), views.ProgramListView.as_view(), name="program-list"),
path( path(
_("programs/<slug:slug>/"), _("programs/<slug:slug>/"),
views.ProgramDetailView.as_view(), views.ProgramDetailView.as_view(),
name="program-detail", name="program-detail",
), ),
path(_("programs/<slug:parent_slug>/articles/"), views.ArticleListView.as_view(), name="article-list"),
path(_("programs/<slug:parent_slug>/podcasts/"), views.PodcastListView.as_view(), name="podcast-list"),
path(_("programs/<slug:parent_slug>/episodes/"), views.EpisodeListView.as_view(), name="episode-list"),
path(_("programs/<slug:parent_slug>/diffusions/"), views.DiffusionListView.as_view(), name="diffusion-list"),
path( path(
_("program/<pk>/edit/"), _("program/<pk>/edit/"),
views.ProgramUpdateView.as_view(), views.ProgramUpdateView.as_view(),
name="program-edit", name="program-edit",
), ),
path(
_("episode/<pk>/edit/"),
views.EpisodeUpdateView.as_view(),
name="episode-edit",
),
path(
_("programs/<slug:parent_slug>/episodes/"),
views.EpisodeListView.as_view(),
name="episode-list",
),
path(
_("programs/<slug:parent_slug>/articles/"),
views.ArticleListView.as_view(),
name="article-list",
),
path(
_("programs/<slug:parent_slug>/publications/"),
views.ProgramPageListView.as_view(),
name="program-page-list",
),
path( path(
"errors/no-station", "errors/no-station",
views.errors.NoStationErrorView.as_view(), views.errors.NoStationErrorView.as_view(),
name="errors-no-station", name="errors-no-station",
), ),
path(
_("program/<slug:parent_slug>/publications"),
views.PageListView.as_view(model=models.Page, attach_to_value=models.StaticPage.Target.PAGES),
name="page-list",
),
path("gestion/", views.profile, name="profile"), path("gestion/", views.profile, name="profile"),
path("accounts/profile/", views.profile, name="profile"), path("accounts/profile/", views.profile, name="profile"),
] ]

View File

@ -1,8 +1,8 @@
from . import admin, errors from . import admin, errors
from .article import ArticleDetailView, ArticleListView from .article import ArticleDetailView, ArticleListView
from .base import BaseAPIView, BaseView from .base import BaseAPIView, BaseView
from .diffusion import DiffusionListView from .diffusion import DiffusionListView, TimeTableView
from .episode import EpisodeDetailView, EpisodeListView, EpisodeUpdateView from .episode import EpisodeDetailView, EpisodeListView, PodcastListView, EpisodeUpdateView
from .home import HomeView from .home import HomeView
from .log import LogListAPIView, LogListView from .log import LogListAPIView, LogListView
from .page import ( from .page import (
@ -28,8 +28,10 @@ __all__ = (
"BaseAPIView", "BaseAPIView",
"BaseView", "BaseView",
"DiffusionListView", "DiffusionListView",
"TimeTableView",
"EpisodeDetailView", "EpisodeDetailView",
"EpisodeListView", "EpisodeListView",
"PodcastListView",
"EpisodeUpdateView", "EpisodeUpdateView",
"HomeView", "HomeView",
"LogListAPIView", "LogListAPIView",
@ -44,4 +46,15 @@ __all__ = (
"ProgramPageDetailView", "ProgramPageDetailView",
"ProgramPageListView", "ProgramPageListView",
"ProgramUpdateView", "ProgramUpdateView",
"attached",
) )
attached = {}
for key in __all__:
view = globals().get(key)
if key == "attached":
continue
if attach := getattr(view, "attach_to_value", None):
attached[attach] = view

View File

@ -5,7 +5,6 @@ __all__ = ["ArticleDetailView", "ArticleListView"]
class ArticleDetailView(PageDetailView): class ArticleDetailView(PageDetailView):
has_sidebar = True
model = Article model = Article
def get_sidebar_queryset(self): def get_sidebar_queryset(self):
@ -17,4 +16,4 @@ class ArticleListView(PageListView):
model = Article model = Article
has_headline = True has_headline = True
parent_model = Program parent_model = Program
attach_to_value = StaticPage.ATTACH_TO_ARTICLES attach_to_value = StaticPage.Target.ARTICLES

View File

@ -2,18 +2,14 @@ from django.http import HttpResponseRedirect
from django.urls import reverse from django.urls import reverse
from django.views.generic.base import ContextMixin, TemplateResponseMixin from django.views.generic.base import ContextMixin, TemplateResponseMixin
from ..models import Page
__all__ = ("BaseView", "BaseAPIView") __all__ = ("BaseView", "BaseAPIView")
class BaseView(TemplateResponseMixin, ContextMixin): class BaseView(TemplateResponseMixin, ContextMixin):
has_sidebar = True header_template_name = "aircox/widgets/header.html"
"""Show side navigation.""" related_count = 4
has_filters = False related_carousel_count = 8
"""Show filters nav."""
list_count = 5
"""Item count for small lists displayed on page."""
@property @property
def station(self): def station(self):
@ -22,31 +18,52 @@ class BaseView(TemplateResponseMixin, ContextMixin):
# def get_queryset(self): # def get_queryset(self):
# return super().get_queryset().station(self.station) # return super().get_queryset().station(self.station)
def get_sidebar_queryset(self): def get_nav_menu(self):
"""Return a queryset of items to render on the side nav.""" menu = []
return Page.objects.select_subclasses().published().order_by("-pub_date") for item in self.station.navitem_set.all():
try:
if item.page:
view = item.page.get_related_view()
secondary = view and view.get_secondary_nav()
else:
secondary = None
menu.append((item, secondary))
except:
import traceback
def get_sidebar_url(self): traceback.print_exc()
return reverse("page-list") raise
return menu
def get_secondary_nav(self):
return None
def get_related_queryset(self):
"""Return a queryset of related pages or None."""
return None
def get_related_url(self):
"""Return an url to the list of related pages."""
return None
def get_page(self): def get_page(self):
return None return None
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
kwargs.setdefault("page", self.get_page()) kwargs.setdefault("page", self.get_page())
kwargs.setdefault("has_filters", self.has_filters) kwargs.setdefault("header_template_name", self.header_template_name)
has_sidebar = kwargs.setdefault("has_sidebar", self.has_sidebar)
if has_sidebar and "sidebar_object_list" not in kwargs:
sidebar_object_list = self.get_sidebar_queryset()
if sidebar_object_list is not None:
kwargs["sidebar_object_list"] = sidebar_object_list[: self.list_count]
kwargs["sidebar_list_url"] = self.get_sidebar_url()
if "model" not in kwargs: if "model" not in kwargs:
model = getattr(self, "model", None) or hasattr(self, "object") and type(self.object) model = getattr(self, "model", None) or hasattr(self, "object") and type(self.object)
kwargs["model"] = model kwargs["model"] = model
page = kwargs.get("page")
if page:
kwargs["title"] = page.display_title
kwargs["cover"] = page.cover and page.cover.url
if "nav_menu" not in kwargs:
kwargs["nav_menu"] = self.get_nav_menu()
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)
def dispatch(self, *args, **kwargs): def dispatch(self, *args, **kwargs):

View File

@ -1,30 +1,55 @@
import datetime import datetime
from django.urls import reverse
from django.views.generic import ListView from django.views.generic import ListView
from aircox.models import Diffusion, StaticPage from aircox.models import Diffusion, Log, StaticPage
from .base import BaseView from .base import BaseView
from .mixins import AttachedToMixin, GetDateMixin from .mixins import AttachedToMixin, GetDateMixin
__all__ = ("DiffusionListView",) __all__ = ("DiffusionListView", "TimeTableView")
class DiffusionListView(GetDateMixin, AttachedToMixin, BaseView, ListView): class BaseDiffusionListView(AttachedToMixin, BaseView, ListView):
model = Diffusion
queryset = Diffusion.objects.on_air().order_by("-start")
class DiffusionListView(BaseDiffusionListView):
"""View for timetables.""" """View for timetables."""
model = Diffusion model = Diffusion
has_filters = True
redirect_date_url = "diffusion-list"
attach_to_value = StaticPage.ATTACH_TO_DIFFUSIONS class TimeTableView(GetDateMixin, BaseDiffusionListView):
model = Diffusion
redirect_date_url = "timetable-list"
attach_to_value = StaticPage.Target.TIMETABLE
template_name = "aircox/timetable_list.html"
def get_date(self): def get_date(self):
date = super().get_date() date = super().get_date()
return date if date is not None else datetime.date.today() return date if date is not None else datetime.date.today()
def get_queryset(self): def get_logs(self, date):
return super().get_queryset().date(self.date).order_by("start") return Log.objects.on_air().date(self.date).filter(track__isnull=False)
def get_context_data(self, **kwargs): def get_queryset(self):
return super().get_queryset().date(self.date)
@classmethod
def get_secondary_nav(cls):
date = datetime.date.today()
start = date - datetime.timedelta(days=date.weekday())
dates = [start + datetime.timedelta(days=i) for i in range(0, 7)]
return tuple((date.strftime("%A %d"), reverse("timetable-list", kwargs={"date": date})) for date in dates)
def get_context_data(self, object_list=None, **kwargs):
start = self.date - datetime.timedelta(days=self.date.weekday()) start = self.date - datetime.timedelta(days=self.date.weekday())
dates = [start + datetime.timedelta(days=i) for i in range(0, 7)] dates = [start + datetime.timedelta(days=i) for i in range(0, 7)]
return super().get_context_data(date=self.date, dates=dates, **kwargs)
if object_list is None:
logs = self.get_logs(self.date)
object_list = Log.merge_diffusions(logs, self.object_list)
object_list = list(reversed(object_list))
return super().get_context_data(date=self.date, dates=dates, object_list=object_list, **kwargs)

View File

@ -1,11 +1,13 @@
from django.contrib.auth.mixins import UserPassesTestMixin from django.contrib.auth.mixins import UserPassesTestMixin
from django.forms import ModelForm, FileField from django.forms import ModelForm, FileField
from django.forms.models import modelformset_factory
from django.urls import reverse from django.urls import reverse
from ckeditor.fields import RichTextField from ckeditor.fields import RichTextField
from filer.models.filemodels import File from filer.models.filemodels import File
from aircox.controllers.sound_file import SoundFile from aircox.controllers.sound_file import SoundFile
from aircox.models import Track
from ..filters import EpisodeFilters from ..filters import EpisodeFilters
from ..models import Episode, Program, StaticPage from ..models import Episode, Program, StaticPage
@ -13,9 +15,12 @@ from .page import PageListView
from .program import ProgramPageDetailView, BaseProgramMixin from .program import ProgramPageDetailView, BaseProgramMixin
from .page import PageUpdateView from .page import PageUpdateView
__all__ = ( __all__ = (
"EpisodeDetailView", "EpisodeDetailView",
"EpisodeListView", "EpisodeListView",
"PodcastListView",
"EpisodeUpdateView",
) )
@ -27,14 +32,25 @@ class EpisodeDetailView(ProgramPageDetailView):
kwargs["tracks"] = self.object.track_set.order_by("position") kwargs["tracks"] = self.object.track_set.order_by("position")
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)
def get_related_queryset(self):
return (
self.get_queryset().parent(self.object.parent).exclude(pk=self.object.pk).published().order_by("-pub_date")
)
def get_related_url(self):
return reverse("episode-list", kwargs={"parent_slug": self.object.parent.slug})
class EpisodeListView(PageListView): class EpisodeListView(PageListView):
model = Episode model = Episode
filterset_class = EpisodeFilters filterset_class = EpisodeFilters
item_template_name = "aircox/widgets/episode_item.html"
has_headline = True
parent_model = Program parent_model = Program
attach_to_value = StaticPage.ATTACH_TO_EPISODES attach_to_value = StaticPage.Target.EPISODES
class PodcastListView(EpisodeListView):
attach_to_value = StaticPage.Target.PODCASTS
queryset = Episode.objects.published().with_podcasts().order_by("-pub_date")
class EpisodeForm(ModelForm): class EpisodeForm(ModelForm):
@ -68,3 +84,12 @@ class EpisodeUpdateView(UserPassesTestMixin, BaseProgramMixin, PageUpdateView):
def get_success_url(self): def get_success_url(self):
return reverse("episode-detail", kwargs={"slug": self.get_object().slug}) return reverse("episode-detail", kwargs={"slug": self.get_object().slug})
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
formset = modelformset_factory(Track, fields=["title", "artist"])
context["forms"] = formset(queryset=Track.objects.filter(episode=self.object))
return context
# def post(self, request, *args, **kwargs):
# def form_valid(self, formset,day_form):

View File

@ -1,43 +1,53 @@
from datetime import date from datetime import date, datetime, timedelta
from django.utils import timezone as tz from django.utils import timezone as tz
from django.views.generic import ListView from django.views.generic import ListView
from ..models import Diffusion, Log, Page, StaticPage from ..models import Diffusion, Episode, Log, Page, StaticPage
from .base import BaseView from .base import BaseView
from .mixins import AttachedToMixin
class HomeView(BaseView, ListView): class HomeView(AttachedToMixin, BaseView, ListView):
template_name = "aircox/home.html" template_name = "aircox/home.html"
attach_to_value = StaticPage.Target.HOME
model = Diffusion model = Diffusion
attach_to_value = StaticPage.ATTACH_TO_HOME queryset = Diffusion.objects.on_air().select_related("episode").order_by("-start")
queryset = Diffusion.objects.on_air().select_related("episode")
logs_count = 5 publications_queryset = Page.objects.select_subclasses().published().order_by("-pub_date")
publications_count = 5 podcasts_queryset = Episode.objects.published().with_podcasts().order_by("-pub_date")
has_filters = False
def get_queryset(self): def get_queryset(self):
return super().get_queryset().date(date.today()) now = datetime.now()
return super().get_queryset().after(now - timedelta(hours=24)).before(now).order_by("-start")
def get_logs(self, diffusions): def get_logs(self, diffusions):
today = date.today() today = date.today()
logs = Log.objects.on_air().date(today).filter(track__isnull=False)
# diffs = Diffusion.objects.on_air().date(today) # diffs = Diffusion.objects.on_air().date(today)
return Log.merge_diffusions(logs, diffusions, self.logs_count) object_list = self.object_list
diffs = list(object_list[: self.related_count])
logs = Log.objects.on_air().filter(track__isnull=False)
if diffs:
min_date = diffs[-1].start - timedelta(hours=1)
logs = logs.after(min_date)
else:
logs = logs.date(today)
return Log.merge_diffusions(logs, object_list, diff_count=self.related_count)
def get_next_diffs(self): def get_next_diffs(self):
now = tz.now() now = tz.now()
current_diff = Diffusion.objects.on_air().now(now).first() query = Diffusion.objects.on_air().select_related("episode")
next_diffs = Diffusion.objects.on_air().after(now) current_diff = query.now(now).first()
next_diffs = query.after(now)
if current_diff: if current_diff:
diffs = [current_diff] + list(next_diffs.exclude(pk=current_diff.pk)[:2]) diffs = [current_diff] + list(next_diffs.exclude(pk=current_diff.pk)[:9])
else: else:
diffs = next_diffs[:3] diffs = next_diffs[: self.related_carousel_count]
return diffs return diffs
def get_last_publications(self): def get_publications(self):
# note: with postgres db, possible to use distinct() # note: with postgres db, possible to use distinct()
qs = Page.objects.select_subclasses().published().order_by("-pub_date") qs = self.publications_queryset.all()
parents = set() parents = set()
items = [] items = []
for publication in qs: for publication in qs:
@ -45,13 +55,25 @@ class HomeView(BaseView, ListView):
if parent_id is not None and parent_id in parents: if parent_id is not None and parent_id in parents:
continue continue
items.append(publication) items.append(publication)
if len(items) == self.publications_count: if len(items) == self.related_count:
break break
return items return items
def get_podcasts(self):
return self.podcasts_queryset.all()[: self.related_carousel_count]
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) next_diffs = self.get_next_diffs()
context["logs"] = self.get_logs(context["object_list"]) current_diff = next_diffs and next_diffs[0]
context["next_diffs"] = self.get_next_diffs()
context["last_publications"] = self.get_last_publications()[:5] kwargs.update(
return context {
"object": current_diff.episode,
"diffusion": current_diff,
"logs": self.get_logs(self.object_list),
"next_diffs": next_diffs,
"publications": self.get_publications(),
"podcasts": self.get_podcasts(),
}
)
return super().get_context_data(**kwargs)

View File

@ -6,12 +6,12 @@ from django.views.decorators.cache import cache_page
from django.views.generic import ListView from django.views.generic import ListView
from rest_framework.generics import ListAPIView from rest_framework.generics import ListAPIView
from ..models import Diffusion, Log, StaticPage from ..models import Diffusion, Log
from ..serializers import LogInfo, LogInfoSerializer from ..serializers import LogInfo, LogInfoSerializer
from .base import BaseAPIView, BaseView from .base import BaseAPIView, BaseView
from .mixins import AttachedToMixin, GetDateMixin from .mixins import AttachedToMixin, GetDateMixin
__all__ = ["LogListMixin", "LogListView"] __all__ = ("LogListMixin", "LogListView", "LogListAPIView")
class LogListMixin(GetDateMixin): class LogListMixin(GetDateMixin):
@ -37,7 +37,8 @@ class LogListMixin(GetDateMixin):
) )
def get_diffusions_queryset(self): def get_diffusions_queryset(self):
qs = Diffusion.objects.station(self.station).on_air().filter(start__lte=tz.now()) qs = Diffusion.objects.station(self.station).on_air().filter(start__lte=tz.now()).before()
return ( return (
qs.date(self.date) qs.date(self.date)
if self.date is not None if self.date is not None
@ -62,8 +63,6 @@ class LogListView(AttachedToMixin, BaseView, LogListMixin, ListView):
`request.GET`, defaults to today).""" `request.GET`, defaults to today)."""
redirect_date_url = "log-list" redirect_date_url = "log-list"
has_filters = True
attach_to_value = StaticPage.ATTACH_TO_LOGS
def get_date(self): def get_date(self):
date = super().get_date() date = super().get_date()

View File

@ -44,16 +44,16 @@ class ParentMixin:
parent = None parent = None
"""Parent page object.""" """Parent page object."""
def get_parent(self, request, *args, **kwargs): def get_parent(self, request, **kwargs):
if self.parent_model is None or self.parent_url_kwarg not in kwargs: if self.parent_model is None or self.parent_url_kwarg not in kwargs:
return return
lookup = {self.parent_field: kwargs[self.parent_url_kwarg]} lookup = {self.parent_field: kwargs[self.parent_url_kwarg]}
return get_object_or_404(self.parent_model.objects.select_related("cover"), **lookup) return get_object_or_404(self.parent_model.objects.select_related("cover"), **lookup)
def get(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
self.parent = self.get_parent(request, *args, **kwargs) self.parent = self.get_parent(request, **kwargs)
return super().get(request, *args, **kwargs) return super().dispatch(request, *args, **kwargs)
def get_queryset(self): def get_queryset(self):
if self.parent is not None: if self.parent is not None:
@ -61,9 +61,10 @@ class ParentMixin:
return super().get_queryset() return super().get_queryset()
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
self.parent = kwargs.setdefault("parent", self.parent) parent = kwargs.setdefault("parent", self.parent)
if self.parent is not None:
kwargs.setdefault("cover", self.parent.cover) if parent is not None:
kwargs.setdefault("cover", parent.cover.url)
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)

View File

@ -2,11 +2,12 @@ from django.http import Http404, HttpResponse
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views.generic import DetailView, ListView from django.views.generic import DetailView, ListView
from django.views.generic.edit import UpdateView from django.views.generic.edit import UpdateView
from django.urls import reverse
from honeypot.decorators import check_honeypot from honeypot.decorators import check_honeypot
from ..filters import PageFilters from ..filters import PageFilters
from ..forms import CommentForm from ..forms import CommentForm
from ..models import Comment from ..models import Comment, Category
from ..utils import Redirect from ..utils import Redirect
from .base import BaseView from .base import BaseView
from .mixins import AttachedToMixin, FiltersMixin, ParentMixin from .mixins import AttachedToMixin, FiltersMixin, ParentMixin
@ -19,34 +20,76 @@ __all__ = [
] ]
class BasePageListView(AttachedToMixin, ParentMixin, BaseView, ListView): class BasePageMixin:
category = None
def get_category(self, page, **kwargs):
if page:
if getattr(page, "category_id", None):
return page.category
if page.parent_id:
return self.get_category(page.parent_subclass)
if slug := self.kwargs.get("category_slug"):
return Category.objects.get(slug=slug)
return None
def get_context_data(self, *args, **kwargs):
kwargs.setdefault("category", self.category)
return super().get_context_data(*args, **kwargs)
class BasePageListView(AttachedToMixin, BasePageMixin, ParentMixin, BaseView, ListView):
"""Base view class for BasePage list.""" """Base view class for BasePage list."""
template_name = "aircox/basepage_list.html" template_name = "aircox/basepage_list.html"
item_template_name = "aircox/widgets/page_item.html"
has_sidebar = True
paginate_by = 30 paginate_by = 30
has_headline = True has_headline = True
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
def get(self, *args, **kwargs): def get(self, *args, **kwargs):
self.category = self.get_category(self.parent)
return super().get(*args, **kwargs) return super().get(*args, **kwargs)
def get_queryset(self): def get_queryset(self):
return super().get_queryset().select_subclasses().published().select_related("cover") query = super().get_queryset().select_subclasses().published().select_related("cover")
if self.category:
query = query.filter(category=self.category)
return query
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
kwargs.setdefault("item_template_name", self.item_template_name)
kwargs.setdefault("has_headline", self.has_headline) kwargs.setdefault("has_headline", self.has_headline)
return super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
parent = context.get("parent")
if not context.get("page"):
if not context.get("title"):
model = self.model._meta.verbose_name_plural
title = _("{model}")
context["title"] = title.format(model=model, parent=parent)
if not context.get("cover") and parent and parent.cover:
context["cover"] = parent.cover.url
return context
class BasePageDetailView(BaseView, DetailView): class BasePageDetailView(BasePageMixin, BaseView, DetailView):
"""Base view class for BasePage.""" """Base view class for BasePage."""
template_name = "aircox/basepage_detail.html" template_name = "aircox/basepage_detail.html"
context_object_name = "page" context_object_name = "page"
has_filters = False
def get(self, *args, **kwargs):
return super().get(*args, **kwargs)
def get_context_data(self, **kwargs):
if self.object.cover:
kwargs.setdefault("cover", self.object.cover.url)
if self.object.title:
kwargs.setdefault("title", self.object.display_title)
return super().get_context_data(**kwargs)
def get_queryset(self): def get_queryset(self):
return super().get_queryset().select_related("cover") return super().get_queryset().select_related("cover")
@ -68,6 +111,8 @@ class BasePageDetailView(BaseView, DetailView):
if redirect_url: if redirect_url:
raise Redirect(redirect_url) raise Redirect(redirect_url)
raise Http404("%s not found" % self.model._meta.verbose_name) raise Http404("%s not found" % self.model._meta.verbose_name)
self.category = self.get_category(obj)
return obj return obj
def get_page(self): def get_page(self):
@ -79,7 +124,6 @@ class PageListView(FiltersMixin, BasePageListView):
filterset_class = PageFilters filterset_class = PageFilters
template_name = None template_name = None
has_filters = True
categories = None categories = None
filters = None filters = None
@ -93,15 +137,21 @@ class PageListView(FiltersMixin, BasePageListView):
def get_queryset(self): def get_queryset(self):
qs = super().get_queryset().select_related("category").order_by("-pub_date") qs = super().get_queryset().select_related("category").order_by("-pub_date")
cat_ids = self.model.objects.published().values_list("category_id", flat=True)
self.categories = Category.objects.filter(id__in=cat_ids)
return qs return qs
def get_context_data(self, **kwargs): @classmethod
kwargs["categories"] = ( def get_secondary_nav(cls):
self.model.objects.published() cat_ids = cls.model.objects.published().values_list("category_id", flat=True)
.filter(category__isnull=False) categories = Category.objects.filter(id__in=cat_ids)
.values_list("category__title", "category__id") return tuple(
.distinct() (category.title, reverse(cls.model.list_url_name, kwargs={"category_slug": category.slug}))
for category in categories
) )
def get_context_data(self, **kwargs):
kwargs["categories"] = self.categories
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)
@ -110,7 +160,6 @@ class PageDetailView(BasePageDetailView):
template_name = None template_name = None
context_object_name = "page" context_object_name = "page"
has_filters = False
def get_template_names(self): def get_template_names(self):
return super().get_template_names() + ["aircox/page_detail.html"] return super().get_template_names() + ["aircox/page_detail.html"]
@ -122,6 +171,15 @@ class PageDetailView(BasePageDetailView):
if self.object.allow_comments and "comment_form" not in kwargs: if self.object.allow_comments and "comment_form" not in kwargs:
kwargs["comment_form"] = CommentForm() kwargs["comment_form"] = CommentForm()
kwargs["comments"] = Comment.objects.filter(page=self.object).order_by("-date") kwargs["comments"] = Comment.objects.filter(page=self.object).order_by("-date")
if self.object.parent_subclass:
kwargs["parent"] = self.object.parent_subclass
if "related_objects" not in kwargs:
related = self.get_related_queryset()
if related:
related = related[: self.related_count]
kwargs["related_objects"] = related
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)
@classmethod @classmethod

View File

@ -1,3 +1,5 @@
import random
from django.contrib.auth.mixins import UserPassesTestMixin from django.contrib.auth.mixins import UserPassesTestMixin
from django.forms import ModelForm, ImageField from django.forms import ModelForm, ImageField
from django.urls import reverse from django.urls import reverse
@ -5,7 +7,8 @@ from django.urls import reverse
from ckeditor.fields import RichTextField from ckeditor.fields import RichTextField
from filer.models.imagemodels import Image from filer.models.imagemodels import Image
from ..models import Page, Program, StaticPage
from ..models import Article, Episode, Page, Program, StaticPage
from .mixins import ParentMixin from .mixins import ParentMixin
from .page import PageDetailView, PageListView, PageUpdateView from .page import PageDetailView, PageListView, PageUpdateView
@ -28,12 +31,33 @@ class BaseProgramMixin:
class ProgramDetailView(BaseProgramMixin, PageDetailView): class ProgramDetailView(BaseProgramMixin, PageDetailView):
model = Program model = Program
def get_related_queryset(self):
queryset = (
self.get_queryset()
.filter(category_id=self.object.category_id)
.exclude(pk=self.object.pk)
.published()
.order_by("-pub_date")[:50]
)
return random.sample(list(queryset), min(len(queryset), self.related_count))
def get_related_url(self):
return reverse("program-list") + f"?category__id={self.object.category_id}"
def get_context_data(self, **kwargs):
episodes = Episode.objects.program(self.object).published().order_by("-pub_date")
podcasts = episodes.with_podcasts()
articles = Article.objects.parent(self.object).published().order_by("-pub_date")
return super().get_context_data(
articles=articles[: self.related_count],
episodes=episodes[: self.related_count],
podcasts=podcasts[: self.related_count],
**kwargs,
)
def get_template_names(self): def get_template_names(self):
return super().get_template_names() + ["aircox/program_detail.html"] return super().get_template_names() + ["aircox/program_detail.html"]
def get_sidebar_queryset(self):
return super().get_sidebar_queryset().filter(parent=self.program)
class ProgramForm(ModelForm): class ProgramForm(ModelForm):
content = RichTextField() content = RichTextField()
@ -68,7 +92,10 @@ class ProgramUpdateView(UserPassesTestMixin, BaseProgramMixin, PageUpdateView):
class ProgramListView(PageListView): class ProgramListView(PageListView):
model = Program model = Program
attach_to_value = StaticPage.ATTACH_TO_PROGRAMS attach_to_value = StaticPage.Target.PROGRAMS
def get_queryset(self):
return super().get_queryset().order_by("title")
# FIXME: not used # FIXME: not used

View File

@ -91,9 +91,9 @@
<div class="column is-two-fifths"> <div class="column is-two-fifths">
<h6 class="subtitle is-6 is-marginless">Metadata</h6> <h6 class="subtitle is-6 is-marginless">Metadata</h6>
<table class="table has-background-transparent"> <table class="table bg-transparent">
<tbody> <tbody>
<tr><th class="has-text-right has-text-nowrap"> <tr><th class="has-text-right ws-nowrap">
{% translate "Status" %} {% translate "Status" %}
</th> </th>
<td :class="{'has-text-danger': source.isPlaying, 'has-text-warning': source.isPaused}"> <td :class="{'has-text-danger': source.isPlaying, 'has-text-warning': source.isPaused}">
@ -103,7 +103,7 @@
</td> </td>
</tr> </tr>
<tr v-if="source.data.air_time"> <tr v-if="source.data.air_time">
<th class="has-text-right has-text-nowrap"> <th class="has-text-right ws-nowrap">
{% translate "Air time" %} {% translate "Air time" %}
</th><td> </th><td>
<span class="far fa-clock"></span> <span class="far fa-clock"></span>
@ -113,7 +113,7 @@
</time> </time>
</td> </td>
<tr v-if="source.remaining"> <tr v-if="source.remaining">
<th class="has-text-right has-text-nowrap"> <th class="has-text-right ws-nowrap">
{% translate "Time left" %} {% translate "Time left" %}
</th><td> </th><td>
<span class="far fa-hourglass"></span> <span class="far fa-hourglass"></span>
@ -121,7 +121,7 @@
</td> </td>
</tr> </tr>
<tr v-if="source.data.uri"> <tr v-if="source.data.uri">
<th class="has-text-right has-text-nowrap"> <th class="has-text-right ws-nowrap">
{% translate "Data source" %} {% translate "Data source" %}
</th><td> </th><td>
<span class="far fa-play-circle"></span> <span class="far fa-play-circle"></span>

View File

@ -10,8 +10,10 @@
}, },
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-free": "^6.0.0", "@fortawesome/fontawesome-free": "^6.0.0",
"@popperjs/core": "^2.11.8",
"core-js": "^3.8.3", "core-js": "^3.8.3",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"v-calendar": "^3.1.2",
"vue": "^3.2.13" "vue": "^3.2.13"
}, },
"devDependencies": { "devDependencies": {
@ -20,7 +22,7 @@
"@vue/cli-plugin-babel": "~5.0.0", "@vue/cli-plugin-babel": "~5.0.0",
"@vue/cli-plugin-eslint": "~5.0.0", "@vue/cli-plugin-eslint": "~5.0.0",
"@vue/cli-service": "~5.0.0", "@vue/cli-service": "~5.0.0",
"bulma": "^0.9.3", "bulma": "^0.9.4",
"eslint": "^7.32.0", "eslint": "^7.32.0",
"eslint-plugin-vue": "^8.0.3", "eslint-plugin-vue": "^8.0.3",
"sass": "^1.49.9", "sass": "^1.49.9",

View File

@ -1,4 +1,3 @@
import './assets/styles.scss'
import './assets/admin.scss' import './assets/admin.scss'
import './index.js' import './index.js'

View File

@ -1,9 +1,16 @@
import {Calendar, DatePicker} from 'v-calendar';
import components from './components' import components from './components'
const App = { const App = {
el: '#app', el: '#app',
delimiters: ['[[', ']]'], delimiters: ['[[', ']]'],
components: {...components}, components: {
...components,
...{
VCalendar: Calendar,
VDatepicker: DatePicker
},
},
computed: { computed: {
player() { return window.aircox.player; }, player() { return window.aircox.player; },

View File

@ -1,139 +0,0 @@
import {createApp} from 'vue'
/**
* Utility class used to handle Vue applications. It provides way to load
* remote application and update history.
*/
export default class Builder {
constructor(config={}) {
this.config = config
this.title = null
this.app = null
this.vm = null
}
/**
* Fetch app from remote and mount application.
*/
fetch(url, {el='#app', ...options}={}) {
return fetch(url, options).then(response => response.text())
.then(content => {
let doc = new DOMParser().parseFromString(content, 'text/html')
let app = doc.querySelector(el)
content = app ? app.innerHTML : content
return this.mount({content, title: doc.title, reset:true, url })
})
}
/**
* Mount application, using `create_app` if required.
*
* @param {String} options.content: replace app container content with it
* @param {String} options.title: set DOM document title.
* @param {String} [options.el=this.config.el]: mount application on this element (querySelector argument)
* @param {Boolean} [reset=False]: if True, force application recreation.
* @return `app.mount`'s result.
*/
mount({content=null, title=null, el=null, reset=false, props=null}={}) {
try {
this.unmount()
let config = this.config
if(el === null)
el = config.el
if(reset || !this.app)
this.app = this.createApp({title,content,el,...config}, props)
this.vm = this.app.mount(el)
window.scroll(0, 0)
return this.vm
} catch(error) {
this.unmount()
throw error
}
}
createApp({el, title=null, content=null, ...config}, props) {
const container = document.querySelector(el)
if(!container)
return
if(content)
container.innerHTML = content
if(title)
document.title = title
return createApp(config, props)
}
unmount() {
this.app && this.app.unmount()
this.app = null
this.vm = null
}
/**
* Enable hot reload: catch page change in order to fetch them and
* load page without actually leaving current one.
*/
enableHotReload(node=null, historySave=true) {
if(historySave)
this.historySave(document.location, true)
node.addEventListener('click', event => this.pageChanged(event), true)
node.addEventListener('submit', event => this.pageChanged(event), true)
node.addEventListener('popstate', event => this.statePopped(event), true)
}
pageChanged(event) {
let submit = event.type == 'submit';
let target = submit || event.target.tagName == 'A'
? event.target : event.target.closest('a');
if(!target || target.hasAttribute('target'))
return;
let url = submit ? target.getAttribute('action') || ''
: target.getAttribute('href');
let domain = window.location.protocol + '//' + window.location.hostname
let stay = (url === '' || url.startsWith('/') || url.startsWith('?') ||
url.startsWith(domain)) && url.indexOf('wp-admin') == -1
if(url===null || !stay) {
return;
}
let options = {};
if(submit) {
let formData = new FormData(event.target);
if(target.method == 'get')
url += '?' + (new URLSearchParams(formData)).toString();
else
options = {...options, method: target.method, body: formData}
}
this.fetch(url, options).then(() => this.historySave(url))
event.preventDefault();
event.stopPropagation();
}
statePopped(event) {
const state = event.state
if(state && state.content)
// document.title = this.title;
this.historyLoad(state);
}
/// Save application state into browser history
historySave(url,replace=false) {
const el = document.querySelector(this.config.el)
const state = {
content: el.innerHTML,
title: document.title,
}
if(replace)
history.replaceState(state, '', url)
else
history.pushState(state, '', url)
}
/// Load application from browser history's state
historyLoad(state) {
return this.mount({ content: state.content, title: state.title })
}
}

View File

@ -1,5 +1,77 @@
@use "./vars";
@use "./components";
@import "~bulma/sass/utilities/_all.sass";
@import "~bulma/sass/elements/button";
@import "~bulma/sass/components/navbar";
// enforce button usage inside custom application
#player, .ax {
@include components.button;
}
.admin { .admin {
.navbar.has-shadow, .navbar.is-fixed-bottom.has-shadow {
box-shadow: 0em 0em 1em rgba(0,0,0,0.1);
}
a.navbar-item.is-active {
border-bottom: 1px grey solid;
}
.navbar {
& + .container {
margin-top: 1em;
}
.navbar-dropdown {
z-index: 2000;
}
.navbar-split {
margin: 0.2em 0em;
margin-right: 1em;
padding-right: 1em;
border-right: 1px vars.$grey-light solid;
display: inline-block;
}
form {
margin: 0em;
padding: 0em;
}
&.toolbar {
margin: 1em 0em;
background-color: transparent;
margin-bottom: 1em;
.title {
padding-right: 2em;
margin-right: 1em;
border-right: 1px vars.$grey-light solid;
font-size: vars.$text-size;
font-weight: vars.$weight-light;
}
}
.navbar-dropdown {
max-height: 40rem;
overflow-y: auto;
input {
z-index: 10000;
position: sticky;
top: 0;
}
}
}
.navbar .navbar-brand { .navbar .navbar-brand {
padding-right: 1em; padding-right: 1em;
} }

View File

@ -0,0 +1,78 @@
@use "./vars" as v;
@import "./vendor";
@import "./helpers";
//-- helpers/modifiers
//-- forms
input.half-field:not(:active):not(:hover) {
border: none;
background-color: rgba(0,0,0,0);
cursor: pointer;
}
//-- general
:root {
--body-bg: #fff;
--text-color: black;
--disabled-color: #aaa;
--disabled-bg: #eee;
--highlight-color: rgba(255, 255, 0, 1);
--highlight-color-alpha: rgba(255, 255, 0, 0.7);
--highlight-color-grey: rgba(230, 230, 60, 1);
--highlight-color-2: rgb(0, 0, 254);
--highlight-color-2-alpha: rgb(0, 0, 254, 0.7);
--highlight-color-2-grey: rgba(50, 200, 200, 1);
--nav-primary-height: 3rem;
--nav-secondary-height: 2.5rem;
--nav-bg: var(--highlight-color);
--nav-fg: var(--highlight-color-2);
--nav-active-bg: var(--highlight-color-2);
--nav-active-fg: var(--highlight-color);
--nav-fs: 1rem;
--nav-2-fs: 0.8rem;
--button-fg: var(--text-color);
--button-bg: var(--highlight-color);
--button-hg-fg: var(--highlight-color-2);
--button-hg-bg: var(--highlight-color);
--button-active-fg: var(--highlight-color);
--button-active-bg: var(--highlight-color-2);
}
body {
font-size: 1.4em;
background-color: var(--body-bg);
}
@media screen and (max-width: v.$screen-wider) {
body { font-size: 1.2em; }
}
@media screen and (max-width: v.$screen-normal) {
body { font-size: 1em; }
:root {
--header-height: 20rem;
}
}
section > .toolbar {
background-color: rgba(0,0,0,0.05);
padding: 1em;
margin-bottom: 1.5em;
}
h1, h2, h3, h4, h5, h6, .heading, .title, .subtitle {
font-family: var(--heading-font-family);
}
.container:empty {
display: none;
}

View File

@ -0,0 +1,669 @@
@use "vars" as v;
:root {
--title-1-sz: 1.6rem;
--title-2-sz: 1.4rem;
--title-3-sz: 1.2rem;
--subtitle-1-sz: 1.6rem;
--subtitle-2-sz: 1.4rem;
--subtitle-3-sz: 1.2rem;
--heading-title-bg-color: rgba(255, 255, 0, 1);
--heading-bg-color: var(--highlight-color);
--heading-bg-highlight-color: var(--highlight-color-2);
--heading-font-family: default;
--preview-title-sz: #{v.$text-size-medium};
--preview-subtitle-sz: #{v.$text-size};
--preview-cover-size: 14rem;
--preview-cover-small-size: 10rem;
--preview-cover-tiny-size: 4rem;
--preview-wide-content-sz: #{v.$text-size-bigger};
--header-height: var(--preview-cover-size);
--a-carousel-p: #{v.$text-size-medium};
--a-carousel-ml: calc(#{v.$mp-4} - 0.5rem);
--a-carousel-gap: #{v.$mp-4};
--a-carousel-nav-x: -#{v.$mp-3e};
--a-progress-bg: transparent;
--a-progress-bar-bg: var(--highlight-color-2);
--a-progress-bar-color: var(--highlight-color);
--a-progress-bar-pd: #{v.$mp-2};
--a-playlist-header-bg: var(--highlight-color-2-alpha);
--a-playlist-header-fg: var(--highlight-color);
--a-playlist-title-sz: #{v.$text-size};
--a-playlist-title-pd: #{v.$mp-3};
--a-playlist-item-border: 1px var(--highlight-color-2) solid;
--a-sound-bg: var(--highlight-color-alpha);
--a-sound-hv-bg: var(--highlight-color);
--a-sound-playing-fg: var(--highlight-color-alpha);
--a-sound-hv-fg: var(--highlight-color-2);
--a-sound-text-sz: #{v.$text-size};
--a-player-url-fg: var(--highlight-color-2);
--a-player-panel-bg: var(--highlight-color);
--a-player-bar-height: var(--nav-primary-height);
--a-player-bar-bg: var(--highlight-color);
--a-player-bar-title-alone-sz: #{v.$text-size-medium};
--button-fg: var(--highlight-color-2);
--button-bg: var(--highlight-color);
--button-sec-bg: var(--highlight-color-alpha);
--button-hg-fg: var(--text-color);
--button-hg-bg: var(--highlight-color);
--button-active-fg: var(--highlight-color);
--button-active-bg: var(--highlight-color-2);
}
@media screen and (max-width: v.$screen-wide) {
:root {
--section-content-sz: 1rem;
--preview-title-sz: #{v.$text-size};
--preview-subtitle-sz: #{v.$text-size-smaller};
--preview-cover-size: 10rem;
--preview-cover-small-size: 6rem;
--preview-cover-tiny-size: 4rem;
--preview-wide-content-sz: #{v.$text-size};
}
}
// ---- headings
.title, .header.preview .title {
&.is-1 { font-size: var(--title-1-sz); }
&.is-2 { font-size: var(--title-2-sz); }
&.is-3 { font-size: var(--title-3-sz); }
}
.subtitle, .header.preview .subtitle {
&.is-1 { font-size: var(--subtitle-1-sz); }
&.is-2 { font-size: var(--subtitle-2-sz); }
&.is-3 { font-size: var(--subtitle-3-sz); }
}
.heading {
display: inline-block;
&:not(:empty) {
background-color: var(--heading-bg-color);
padding: v.$mp-2;
margin-top: 0em !important;
vertical-align: top;
&.highlight, &.active,
.preview.active &,
{
background-color: var(--heading-bg-highlight-color);
color: var(--highlight-color);
}
}
&.title {
background-color: var(--heading-title-bg-color);
}
}
// ---- button
@mixin button {
.button, a.button, button.button, .nav-urls a {
display: inline-block;
padding: v.$mp-2;
border: 1px var(--highlight-color-2-alpha) solid;
justify-content: center;
text-align: center;
// font-size: v.$text-size-medium;
color: var(--button-fg);
background-color: var(--button-bg);
&.secondary {
background-color: var(--button-sec-bg);
}
.icon {
vertical-align: middle;
&:not(:only-child) {
&:first-child { margin-right: v.$mp-3; }
&:last-child { margin-left: v.$mp-3 }
}
}
&:hover {
color: var(--button-hg-fg);
opacity: 1 !important;
}
&.active {
border-color: var(--highlight-color-alpha);
color: var(--button-active-fg);
background-color: var(--button-active-bg);
&:hover {
border-color: var(--highlight-color);
background-color: var(--highlight-color-2-alpha);
opacity: 1 !important;
}
}
&:not([disabled]), &:not(.disabled) {
cursor: pointer;
}
&[disabled], &.disabled {
background-color: var(--highlight-color-grey);
color: var(--highlight-color-2);
border-color: var(--highlight-color-2-alpha);
}
.dropdown-trigger {
border-radius: 1.5em;
}
}
.button-group, .nav {
.button {
border-radius: 0px;
background-color: transparent;
border-top: 0px;
border-bottom: 0px;
height: 100%;
&:not(:first-child) { border-left: 0px; }
&:last-child { border-right: 0px; }
}
}
}
// ---- preview
.preview {
position: relative;
background-size: cover;
&.preview-item {
width: 100%;
}
// FIXME: remove
&.columns, .headings.columns {
margin-left: 0em;
margin-right: 0em;
.column { padding: 0em; }
}
.title, .title:not(:last-child) {
// second is bulma reset
font-weight: v.$weight-bold;
font-size: var(--preview-title-sz);
margin-bottom: unset;
}
.subtitle {
font-weight: v.$weight-bolder;
font-size: var(--preview-subtitle-sz);
margin-bottom: unset;
}
//.content, .actions {
// font-size: v.$text-size-bigger;
//}
.headings {
background-size: cover;
> * { margin: 0em; }
.column { padding: 0em; }
a {
color: var(--text-color);
}
}
&.tiny {
.content {
font-size: v.$text-size;
}
}
}
@media screen and (max-width: v.$screen-small) {
.preview .content {
display: none;
}
}
.preview-cover {
background-size: cover;
background-color: transparent !important;
height: var(--preview-cover-size);
width: var(--preview-cover-size);
min-width: var(--preview-cover-size);
&.small, .preview.small & {
min-width: unset;
height: var(--preview-cover-small-size);
width: var(--preview-cover-small-size) !important;
min-width: var(--preview-cover-small-size);
}
&.tiny, .preview.tiny & {
min-width: unset;
height: var(--preview-cover-tiny-size);
width: var(--preview-cover-tiny-size) !important;
min-width: var(--preview-cover-tiny-size);
}
}
.preview-header {
width: 100%;
&:not(.no-cover) {
min-height: var(--header-height);
}
&.no-cover {
height: unset;
}
.headings {
padding-top: v.$mp-6;
}
.headings, > .container {
width: 100%;
}
> .container, {
height: 100%;
}
}
// ---- list
.list-item {
width: 100%;
&:not(:first-child) {
margin-top: calc(v.$mp-4 / 2);
}
.headings {
display: flex;
flex-direction: row;
padding-top: 0em;
margin-bottom: v.$mp-2 !important;
.title {
flex-grow: 1;
}
}
.subtitle {
font-size: var(--preview-title-sz);
text-align: right;
}
.media-content {
display: flex;
flex-direction: column;
.list-item:not(.no-cover) & {
min-height: var(--preview-cover-small-size);
}
.content {
flex-grow: 1;
margin-bottom: auto;
}
.actions {
flex-grow: unset;
text-align: right;
margin-top: auto;
}
}
}
// ---- wide
.preview-wide {
height: var(--preview-cover-size);
display: flex;
.headings {
height: var(--preview-cover-size)
}
&:not(.header) .headings {
box-shadow: 0em 0em 1em rgba(0,0,0,0.2);
}
& .headings {
width: var(--preview-cover-size);
min-width: var(--preview-cover-size);
flex-grow: 0;
margin-right: v.$mp-4;
}
& .content {
font-size: var(--preview-wide-content-sz);
flex-grow: 1;
}
}
// ---- card
.preview-card {
padding: 0rem !important;
height: var(--preview-cover-size);
width: var(--preview-cover-size);
&.small {
height: var(--preview-cover-small-size);
width: var(--preview-cover-small-size);
}
&.tiny {
height: var(--preview-cover-tiny-size);
width: var(--preview-cover-tiny-size);
}
&:not(.header) {
box-shadow: 0em 0em 1em rgba(0,0,0,0.2);
}
.title {
max-height: calc( var(--preview-cover-size) / 2 );
overflow: hidden;
}
.card-grid & {
min-width: unset;
}
.actions {
position: absolute;
bottom: v.$mp-3;
right: v.$mp-3;
label {
display: none;
}
}
}
.card-headings, .preview-card-headings {
padding-top: v.$mp-3;
& > div:not(:last-child),
& .column > div {
margin-bottom: v.$mp-3;
}
preview-header:not(.no-cover) & .heading {
margin-bottom: v.$mp-3;
}
}
// ---- card grid
.card-grid {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: v.$mp-4;
}
// ---- ---- Carousel
.a-carousel {
margin-left: calc( 0rem - var(--a-carousel-ml));
.a-carousel-viewport {
padding: var(--a-carousel-p) 0;
padding-left: var(--a-carousel-ml);
}
}
.a-carousel-container {
width: 100%;
gap: var(--a-carousel-gap);
transition: margin-left 1s;
> * {
flex-shrink: 0;
}
}
.a-carousel-button-container {
button, .button {
z-index:1000;
position: absolute;
display: flex;
flex-direction: column;
top: 50%;
&.prev { left: var(--a-carousel-nav-x); }
&.next { right: var(--a-carousel-nav-x); }
}
}
// ---- ---- progress bar
.a-progress {
display: flex;
flex-direction: row;
margin: 0em;
padding: 0em;
&:hover {
background-color: var(--a-progress-bg);
}
.a-progress-bar-container {
flex-grow: 1;
margin: 0em;
}
> time, .a-progress-bar {
height: 100%;
padding: var(--a-progress-bar-pd);
}
.a-progress-bar {
background-color: var(--a-progress-bar-bg);
color: var(--a-progress-bar-color)
}
}
// ---- ---- player
// ---- playlist
.playlist, .a-playlist {
.header {
display: flex;
flex-direction: row;
.title, .button {
background-color: var(--a-playlist-header-bg);
color: var(--a-playlist-header-fg);
}
.title {
font-size: var(--a-playlist-title-sz);
margin: 0;
padding: var(--a-playlist-title-pd);
}
}
li {
list-style: none;
border-bottom: var(--a-playlist-item-border);
&:last-child {
border-bottom: 0px;
}
}
}
// ---- sound item
.a-sound-item {
display: flex;
align-items: center;
flex-direction: row;
height: 3rem;
background-color: var(--a-sound-bg);
&.playing, &.playing .label {
color: var(--a-sound-playing-fg) !important;
}
&:hover {
background-color: var(--a-sound-hv-bg);
.label {
color: var(--a-sound-hv-fg) !important;
}
}
.label:hover::before, &.playing .label::before {
content: "\f04b";
font-family: "Font Awesome 6 Free";
margin-right: v.$mp-3e;
}
&.playing .label:hover::before {
content: '';
margin: 0;
}
.headings > * {
}
.label {
cursor: pointer;
.icon {
padding: 0em v.$mp-3;
}
margin: 0em !important;
padding: v.$mp-3e;
font-size: var(--a-sound-text-sz);
font-family: var(--heading-font-family);
}
.button {
width: 3em;
font-size: var(--a-sound-text-sz);
}
}
// ---- player
.player-container {
z-index: 1000000;
}
.a-player {
box-shadow: 0em -0.5em 0.5em rgba(0, 0, 0, 0.05);
a { color: var(--a-player-url-fg); }
.button {
color: var(--text-black);
&:hover { color: var(--button-fg); }
}
}
.a-player-panels {
background: var(--a-player-panel-bg);
height: 0%;
transition: height 1s;
}
.a-player-panels.is-open {
height: auto;
}
.a-player-panel {
padding-bottom: v.$mp-3;
max-height: 80%;
overflow-y: auto;
.a-sound-item:not(:hover) {
background-color: transparent;
}
}
.a-player-progress {
height: 0.4em;
overflow: hidden;
time { display: none; }
&:hover, .a-player-panels.is-open + & {
background: var(--a-player-bar-bg);
height: 2em;
time { display: unset; }
}
}
.a-player-bar {
display: flex;
flex-direction: row;
justify-content: center;
height: var(--a-player-bar-height);
border-top: 1px v.$grey-light solid;
background: var(--a-player-bar-bg);
> * { height: 100%; }
.cover { height: 100%; }
.title {
font-size: v.$text-size;
margin: 0em;
&:last-child {
font-size: var(--a-player-bar-title-alone-sz);
}
}
.button {
font-size: v.$text-size-medium;
height: 100%;
padding: v.$mp-2 !important;
min-width: calc(var(--a-player-bar-height) + v.$mp-2 * 2);
border-radius: 0px;
transition: background-color 0.5s;
&.open {
background-color: var(--highlight-color-2-alpha);
color: var(--highlight-color);
}
}
}
.a-player-bar-content {
display: flex;
flex-direction: vertical;
align-items: center;
flex-grow: 1;
padding: 0 v.$mp-3;
border-right: 1px black solid;
.title {
max-height: calc( var(--a-player-bar-height) - v.$mp-3 );
overflow: hidden;
}
}

View File

@ -0,0 +1,74 @@
@use "./vars";
.align-left { text-align: left; justify-content: left; }
.align-right { text-align: right; justify-content: right; }
.clear-left { clear: left !important }
.clear-right { clear: right !important }
.clear-both { clear: both !important }
.d-inline { display: inline !important; }
.d-block { display: block !important; }
.d-inline-block { display: inline-block !important; }
.push-right, .flex-push-right { margin-left: auto !important; }
.push-bottom { margin-top: auto !important; }
.flex-grow-0 { flex-grow: 0 !important; }
.float-right { float: right }
.float-left { float: left }
.is-fullwidth { width: 100%; }
.is-fullheight { height: 100%; }
.is-fixed-bottom {
position: fixed;
bottom: 0;
margin-bottom: 0px;
border-radius: 0;
}
.is-borderless { border: none; }
.overflow-hidden { overflow: hidden }
.overflow-hidden.is-fullwidth { max-width: 100%; }
.p-relative { position: relative !important }
.p-absolute { position: absolute !important }
.p-fixed { position: fixed !important }
.p-sticky { position: sticky !important }
.p-static { position: static !important }
.height-full { height: 100%; }
.ws-nowrap { white-space: nowrap; }
.no-border { border: 0px !important; }
*[draggable="true"] {
cursor: move;
}
// ---- animations
@keyframes blink {
from { opacity: 1; }
to { opacity: 0.4; }
}
.blink { animation: 1s ease-in-out 3s infinite alternate blink; }
.loading { animation: 1s ease-in-out 1s infinite alternate blink; }
// -- colors
.highlight-color { color: var(--highlight-color); }
.highlight-color-2 { color: var(--highlight-color-2); }
.bg-transparent { background-color: transparent; }
.is-success {
background-color: vars.$green !important;
border-color: vars.$green-dark !important;
}
.is-danger {
background-color: vars.$red !important;
border-color: vars.$red-dark !important;
}

View File

@ -0,0 +1,441 @@
@use "./vars" as v;
@use "./components";
@import "./vendor";
// ---- main theme & layout
.page {
padding-bottom: 5rem;
a {
color: var(--highlight-color-2);
text-decoration: none;
&:hover {
color: var(--text-color);
}
}
section.container {
margin-top: v.$mp-3;
> .title {
margin-top: unset;
padding-top: unset !important;
margin-bottom: v.$mp-4;
border-bottom: 1px solid black;
}
&:not(:last-child) {
margin-bottom: v.$mp-3;
}
}
}
// ---- components
.dropdown-item {
font-size: unset !important
}
.vc-weekday-1, .vc-weekday-7 {
color: var(--highlight-color-2) !important;
}
.schedules {
padding-top: 0;
margin-bottom: calc(0rem - v.$mp-3) !important;
}
.schedule {
display: inline-block;
margin: v.$mp-3;
margin-left: 0rem;
.day {
font-weight: v.$weight-bold;
margin-right: v.$mp-3;
}
}
// -- buttons, forms
@include components.button;
.actions {
display: flex;
flex-direction: row;
gap: v.$mp-3;
justify-content: right;
&.no-label label {
display: none;
}
button, .action {
justify-content: center;
min-width: 2rem;
.not-selected { opacity: 0.6; }
.icon { margin: 0em !important; }
label { margin-left: v.$mp-2; }
}
}
.label, .textarea, .input, .select {
font-size: v.$text-size;
}
.field.is-horizontal {
display: flex;
flex-direction: horizontal;
.label { min-width: 7rem }
.control {
flex: 1;
> * {
width: 100%;
}
}
}
@media screen and (min-width: v.$screen-small) {
textarea {
height: calc( v.$text-size * 7 ) !important;
}
}
.navbar-item.active, .table tr.is-selected {
color: var(--highlight-color-2);
background-color: var(--highlight-color);
}
// -- headings
.title {
text-transform: uppercase;
&.is-3 {
margin-top: v.$mp-3;
}
}
// ---- main navigation
.navs {
position: relative;
}
.nav {
display: flex;
background-color: var(--nav-bg);
&:empty {
display: none;
}
.burger {
display: none;
background-color: var(--nav-bg);
}
.nav-item {
padding: v.$mp-2;
flex-grow: 1;
flex-shrink: 1;
text-align: center;
font-family: var(--heading-font-family);
text-transform: uppercase;
a, .button {
display: block;
width: 100%;
}
.icon:first-child, .icon + span {
text-align: center;
vertical-align: top;
display: inline-block;
}
&.active {
background-color: var(--nav-active-bg);
color: var(--nav-active-fg);
}
}
.nav-menu {
display: flex;
flex-grow: 1;
background-color: var(--nav-bg);
.dropdown-content {
font-size: v.$text-size;
min-width: 15rem;
}
}
&.primary {
height: var(--nav-primary-height);
.nav-menu {
flex-grow: 1;
}
.nav-brand {
display: inline-block;
padding: v.$mp-3;
flex-grow: 0;
flex-shrink: 1;
img {
height: 100%;
}
}
.nav-item {
font-size: var(--nav-fs);
font-weight: v.$weight-bold;
white-space: nowrap;
}
}
&.secondary {
background-color: var(--nav-bg);
//position: absolute;
//width: 100%;
//box-shadow: 0em 0.5em 0.5em rgba(0, 0, 0, 0.05);
justify-content: right;
//display: none;
.nav.primary:hover + &,
&:hover {
display: flex;
top: var(--nav-primary-height);
left: 0rem;
}
.nav-item {
font-size: var(--nav-2-fs);
}
}
}
// ---- breadcrumbs
.breadcrumbs {
text-align: right;
padding: v.$mp-3 0rem;
padding-bottom: 0;
margin-bottom: 0;
&:empty { display: none; }
a + a {
padding-left: 0;
&:before {
content: "/";
margin: 0 v.$mp-2;
}
}
}
@media screen and (max-width: v.$screen-normal) {
.page {
margin-top: var(--nav-primary-height);
}
.navs {
z-index: 100000;
position: fixed;
display: flex;
left: 0;
right: 0;
top: 0;
.nav:first-child {
flex-grow: 1;
}
.nav + .nav {
flew-grow: 0 !important;
}
}
.nav {
justify-content: space-between;
.burger {
display: unset;
margin-left: auto;
}
.nav-menu {
display: block;
position: absolute;
left: 0;
top: 100%;
width: 100%;
box-shadow: 0em 0.5em 0.5em rgba(0,0,0,0.05);
.nav-item {
display: block;
font-weight: v.$weight-normal;
&:hover {
background-color: var(--highlight-color-2-alpha);
color: var(--highlight-color);
}
}
}
.nav-menu:not(.active) {
display: none !important
}
}
}
nav li {
list-style: none;
a, .button {
font-size: v.$text-size-medium;
}
}
.nav-urls {
display: flex;
flex-direction: row;
margin-top: v.$mp-3;
text-align: right;
> a:only-child {
margin-left: auto;
}
li {
list-style: none;
}
.urls {
flex-grow: 1;
display: flex;
flex-direction: row;
gap: v.$mp-3;
justify-content: center;
a:not(:last-child) {
margin-right: v.$mp-3;
}
}
.left {
flex-grow: 0;
text-align: left;
}
.right {
flex-grow: 0;
text-align: right;
}
}
// ---- page header
.header {
&.preview-header {
//display: flex;
align-items: start;
gap: v.$mp-3;
min-height: unset;
padding-top: v.$mp-3 !important;
}
.headings {
width: unset;
flex-grow: 1;
padding-top: 0 !important;
display: flex;
flex-direction: column;
}
&.has-cover {
min-height: calc( var(--header-height) / 3 );
}
}
.header-cover:not(:only-child) {
float: right;
height: var(--header-height);
max-width: calc(var(--header-height) * 2);
margin: 0 0 v.$mp-4 v.$mp-4;
}
.header-cover:only-child {
with: 100%;
}
// ---- ---- detail
.page-content {
margin-top: v.$mp-6;
&:not(:last-child) {
margin-bottom: v.$mp-6;
}
}
// ---- responsive
body { font-size: 1.4em; }
@media screen and (max-width: v.$screen-wide) {
body { font-size: 1em; }
}
@media screen and (max-width: v.$screen-normal) {
.page .container {
margin-left: v.$mp-4;
margin-right: v.$mp-4;
}
}
@media screen and (max-width: v.$screen-small) {
.page .container {
margin-left: v.$mp-2;
margin-right: v.$mp-2;
}
.container.header {
width: calc( 100% - v.$mp-2 );
.headings {
width: 100%;
clear: both;
}
.header-cover {
float: none;
width: 100%;
max-width: unset;
height: unset;
margin-left: 0rem;
margin-right: 0rem;
}
}
}

View File

@ -1,327 +0,0 @@
@charset "utf-8";
@import "~bulma/sass/utilities/_all.sass";
@import "~bulma/sass/components/dropdown.sass";
$body-background-color: $light;
$menu-item-hover-background-color: #dfdfdf;
$menu-item-active-background-color: #d2d2d2;
@import "~bulma";
//-- helpers/modifiers
.is-fullwidth { width: 100%; }
.is-fullheight { height: 100%; }
.is-fixed-bottom {
position: fixed;
bottom: 0;
margin-bottom: 0px;
border-radius: 0;
}
.is-borderless { border: none; }
.has-text-nowrap {
white-space: nowrap;
}
.has-background-transparent {
background-color: transparent;
}
.is-opacity-light {
opacity: 0.7;
&:hover {
opacity: 1;
}
}
.float-right { float: right }
.float-left { float: left }
.overflow-hidden { overflow: hidden }
.overflow-hidden.is-fullwidth { max-width: 100%; }
*[draggable="true"] {
cursor: move;
}
//-- forms
input.half-field:not(:active):not(:hover) {
border: none;
background-color: rgba(0,0,0,0);
cursor: pointer;
}
//-- animations
@keyframes blink {
from { opacity: 1; }
to { opacity: 0.4; }
}
.blink {
animation: 1s ease-in-out 3s infinite alternate blink;
}
//-- navbar
.navbar + .container {
margin-top: 1em;
}
.navbar.has-shadow, .navbar.is-fixed-bottom.has-shadow {
box-shadow: 0em 0em 1em rgba(0,0,0,0.1);
}
a.navbar-item.is-active {
border-bottom: 1px grey solid;
}
.navbar {
.navbar-dropdown {
z-index: 2000;
}
.navbar-split {
margin: 0.2em 0em;
margin-right: 1em;
padding-right: 1em;
border-right: 1px $grey-light solid;
display: inline-block;
}
form {
margin: 0em;
padding: 0em;
}
&.toolbar {
margin: 1em 0em;
background-color: transparent;
margin-bottom: 1em;
.title {
padding-right: 2em;
margin-right: 1em;
border-right: 1px $grey-light solid;
font-size: $size-5;
color: $text-light;
font-weight: $weight-light;
}
}
}
//-- cards
.card {
.title {
a {
color: $dark;
}
padding: 0.2em;
font-size: $size-5;
font-weight: $weight-medium;
}
&.is-primary {
box-shadow: 0em 0em 0.5em $black
}
}
.card-super-title {
position: absolute;
z-index: 1000;
font-size: $size-6;
font-weight: $weight-bold;
padding: 0.2em;
top: 1em;
background-color: #ffffffc7;
max-width: 90%;
.fas {
padding: 0.1em;
font-size: 0.8em;
}
}
//-- page
.page {
& > .cover {
float: right;
max-width: 45%;
}
.header {
margin-bottom: 1.5em;
}
.headline {
font-size: 1.4em;
padding: 0.2em 0em;
}
p { padding: 0.4em 0em; }
hr { background-color: $grey-light; }
.page-content {
h1 { font-size: $size-1; font-weight: bolder; margin-top:0.4em; margin-bottom:0.2em; }
h2 { font-size: $size-3; font-weight: bolder; margin-top:0.4em; margin-bottom:0.2em; }
h3 { font-size: $size-4; font-weight: bolder; margin-top:0.4em; margin-bottom:0.2em; }
h4 { font-size: $size-5; font-weight: bolder; margin-top:0.4em; margin-bottom:0.2em; }
h5 { font-size: $size-6; font-weight: bolder; margin-top:0.4em; margin-bottom:0.2em; }
h6 { font-size: $size-6; margin-top:0.4em; margin-bottom:0.2em; }
}
}
.media.item .headline {
line-height: 1.2em;
max-height: calc(1.2em * 3);
overflow: hidden;
& + .headline-overflow {
position: relative;
width: 100%;
height: 2em;
margin-top: -2em;
}
& + .headline-overflow:before {
content:'';
width:100%;
height:100%;
position:absolute;
left:0;
bottom:0;
background:linear-gradient(transparent 1em, $body-background-color);
}
}
//-- player
.player {
z-index: 10000;
box-shadow: 0em 1.5em 2.5em rgba(0, 0, 0, 0.6);
.player-panels {
height: 0%;
transition: height 3s;
}
.player-panels.is-open {
height: auto;
}
.player-panel {
margin: 0.4em;
max-height: 80%;
overflow-y: auto;
}
.progress {
margin: 0em;
padding: 0em;
border-color: $info;
border-style: 'solid';
}
.player-bar {
border-top: 1px $grey-light solid;
> div {
height: 3.75em !important;
}
> .media-left:not(:last-child) {
margin-right: 0em;
}
> .media-cover {
border-left: 1px black solid;
}
.cover {
font-size: 1.5rem !important;
height: 2.5em !important;
}
> .media-content {
padding-top: 0.4em;
padding-left: 0.4em;
}
.button {
font-size: 1.5rem !important;
height: 100%;
padding: auto 0.2em !important;
min-width: 2.5em;
border-radius: 0px;
transition: background-color 1s;
}
.title {
margin: 0em;
}
}
}
//-- media
.media {
.subtitle {
margin-bottom: 0.4em;
}
.media-content .headline {
font-size: 1em;
font-weight: 400;
}
}
//-- general
body {
background-color: $body-background-color;
}
section > .toolbar {
background-color: rgba(0,0,0,0.05);
padding: 1em;
margin-bottom: 1.5em;
}
main {
.cover.is-small { width: 10em; }
.cover.is-tiny { height: 2em; }
}
aside {
& > section {
margin-bottom: 2em;
}
.cover.is-small { width: 10em; }
.cover.is-tiny { height: 2em; }
.media .subtitle {
font-size: 1em;
}
}
.sound-item {
.cover { height: 5em; }
.media-content a { padding: 0em; }
margin-bottom: 0.2em;
}
.sound-item .media-right .button {
margin-right: 0.2em;
min-width: 2.5em;
display: inline-block;
}
.timetable {
width: 100%;
border: none;
}

View File

@ -0,0 +1,53 @@
@charset "utf-8";
$black: #000;
$white: #fff;
$red: #e00;
$red-dark: #b00;
$green: #0e0;
$green-dark: #0b0;
$grey-light: #ddd;
$mp-1: 0.2rem;
$mp-1e: 0.2em;
$mp-2: 0.4rem;
$mp-2e: 0.4em;
$mp-3: 0.6rem;
$mp-3e: 0.6em;
$mp-4: 1.2rem;
$mp-4e: 1.2em;
$mp-5: 1.6rem;
$mp-5e: 1.6em;
$mp-6: 2rem;
$mp-6e: 2em;
$mp-7: 4rem;
$mp-7e: 4em;
$text-size-small: 0.6rem;
$text-size-smaller: 0.8rem;
$text-size: 1rem;
$text-size-2: 1.2rem;
$text-size-medium: 1.4rem;
$text-size-bigger: 1.6rem;
$text-size-big: 2rem;
$h1-size: 40px;
$h2-size: 32px;
$h3-size: 28px;
$h4-size: 24px;
$h5-size: 20px;
$h6-size: 14px;
$weight-light: 100;
$weight-lighter: 300;
$weight-normal: 400;
$weight-bolder: 500;
$weight-bold: 700;
$screen-very-small: 400px;
//TODO: switch small & smaller
$screen-small: 600px;
$screen-smaller: 800px;
$screen-normal: 1024px;
$screen-wider: 1280px;
$screen-wide: 1380px;

View File

@ -0,0 +1,32 @@
@import 'v-calendar/style.css';
// ---- bulma
$body-color: #000;
$title-color: #000;
@import "~bulma/sass/utilities/_all.sass";
@import "~bulma/sass/base/_all";
@import "~bulma/sass/components/dropdown";
@import "~bulma/sass/components/media";
@import "~bulma/sass/components/message";
@import "~bulma/sass/components/modal";
//@import "~bulma/sass/components/pagination";
@import "~bulma/sass/form/_all";
@import "~bulma/sass/grid/_all";
@import "~bulma/sass/helpers/_all";
@import "~bulma/sass/layout/_all";
@import "~bulma/sass/elements/box";
// @import "~bulma/sass/elements/button";
@import "~bulma/sass/elements/container";
@import "~bulma/sass/elements/content";
@import "~bulma/sass/elements/icon";
// @import "~bulma/sass/elements/image";
// @import "~bulma/sass/elements/notification";
// @import "~bulma/sass/elements/progress";
@import "~bulma/sass/elements/table";
@import "~bulma/sass/elements/tag";
//@import "~bulma/sass/elements/title";

View File

@ -0,0 +1,198 @@
<template>
<section class="a-carousel">
<nav class="a-carousel-button-container" v-if="showPrevButton">
<button :class="[buttonClass, 'prev']" aria-label="Go left" @click="prev">
<span class="icon">
<i :class="leftButtonIcon"></i>
</span>
</button>
</nav>
<div class="a-carousel-button-container" v-if="showNextButton">
<button :class="[buttonClass, 'next']" aria-label="Go left" @click="next">
<span class="icon">
<i :class="rightButtonIcon"></i>
</span>
</button>
</div>
<nav ref="viewport" class="a-carousel-viewport">
<section ref="container" :class="['a-carousel-container', containerClass]">
<slot name="default"></slot>
</section>
</nav>
</section>
</template>
<style scoped>
.a-carousel {
width: 100%;
position: relative;
}
.a-carousel-viewport {
width: 100%;
overflow: hidden;
}
.a-carousel-container {
display: flex;
flex-direction: row;
align-items: left;
}
.a-carousel-container > * {
flex-shrink: 0;
}
</style>
<script>
import {ref} from 'vue'
export default {
setup() {
return {
viewport: ref(null),
container: ref(null),
}
},
data() {
return {
cards: [],
index: 0,
refresh_: 0,
}
},
props: {
cardSelector: {type: String, default: ''},
containerClass: {type: String, default: ''},
buttonClass: {type: String, default: 'button'},
leftButtonIcon: {type: String, default: "fas fa-chevron-left"},
rightButtonIcon: {type: String, default: "fas fa-chevron-right"},
},
computed: {
card() { return this.cards()[this.index] },
showPrevButton() {
return this.index > 0
},
showNextButton() {
if(!this.cards || this.cards.length <= 1)
return false
let { count } = this.visibility
return (this.index + count) < this.cards.length
},
visibility() {
// force refresh on index
[this.index, this.refresh_]
if(!this.cards)
return {min: -1, max: -1, count: 0};
const vOff = this.offset(this.$refs.viewport)
var [min, max] = [-1, -1]
for(let at=0; at < this.cards.length; at++) {
const card = this.cards[at]
const cOff = this.offset(card)
const visible = vOff.min <= cOff.min && vOff.max >= cOff.max
if(visible) {
if(min === -1)
min = parseInt(at)
max = parseInt(at)
}
}
if(max !== -1)
max++
return {
min, max,
count: (min !== -1) ? max-min : 0
}
},
},
methods: {
offset(el, parent=null) {
const rect = el.getBoundingClientRect()
const off = {min: rect.left, max: rect.right }
if(parent === null)
return off
const pOff = this.offset(parent)
return {
min: off.min - pOff.min,
max: off.max - pOff.max,
}
},
getCards() {
if(!this.$refs.container)
return []
if(!this.cardSelector)
return this.$refs.container.children
return this.$refs.container.querySelectorAll(this.cardSelector)
},
selectIndex(index, relative=false) {
if(relative)
index = this.index + index
index = Math.min(this.cards.length, index)
const el = this.cards[index]
if(!el)
return null;
const elOff = this.offset(el, this.$refs.container)
this.$refs.container.style.marginLeft = `-${elOff.min}px`
this.index = index;
return el
},
next() {
this.refresh_++
if(!this.visibility.count)
return
let {count} = this.visibility
let at = Math.min(
count === 1 ? this.index+count : this.index+count-1,
this.cards.length-count
)
this.selectIndex(at)
},
prev() {
this.refresh_++
if(!this.visibility.count)
return
const {min, count} = this.visibility
let at = Math.max(0, min-count)
if(min < 0 || count <= 0)
return
this.selectIndex(at)
},
refresh() {
this.cards = this.getCards()
this.selectIndex(this.index)
this.refresh_++
}
},
mounted() {
this.observers = [
new MutationObserver(() => this.refresh()),
new ResizeObserver(() => this.refresh())
]
this.observers[0].observe(this.$refs.container, {"childList": true})
this.observers[1].observe(this.$refs.container)
this.refresh()
},
unmounted() {
for(var observer of this.observers)
observer.disconnect()
}
}
</script>

Some files were not shown because too many files have changed in this diff Show More