Compare commits

...

17 Commits

Author SHA1 Message Date
306eb20257 docs: update user manual with simplified program management for animators 2024-02-28 17:19:21 +01:00
5ae85083a5 db: create program editors groups 2024-02-28 17:19:21 +01:00
1ac83f1066 db: add missing migration on schedule timezone 2024-02-28 17:19:21 +01:00
7e0e6e9652 episode-form: add tracks inline formset 2024-02-28 17:19:21 +01:00
ab1b152a46 templatetags: display edit-links for admins 2024-02-28 17:19:21 +01:00
8821cd86c6 templates: update after merging branch 118-design 2024-02-28 17:19:21 +01:00
4ea93c9eff templates: add in-context edition links 2024-02-28 17:19:21 +01:00
40ca2064d9 db: migrations merge 2024-02-28 17:19:21 +01:00
e840fbabac templates: update container block names 2024-02-28 17:19:21 +01:00
ff2a8ff6d4 signals: disable schedule_pre_save when using loaddata 2024-02-28 17:19:21 +01:00
d33256edb8 templates: set document type to html, prevent quicks mode 2024-02-28 17:19:21 +01:00
10b9e9280f context_processors: prevent a null station error when no default station is defined 2024-02-28 17:19:21 +01:00
ee7f301f44 views/program: allow changing program cover 2024-02-28 17:19:21 +01:00
64984d69d5 misc: add a profile view for authenticated users 2024-02-28 17:19:21 +01:00
4f856c0705 misc: use the django authentication system 2024-02-28 17:19:21 +01:00
8b4da52760 misc: move station and audio_streams to context_processors (in order to have them available in accounts views) 2024-02-28 17:19:21 +01:00
aa171375e5 misc: edit programs in site 2024-02-28 17:19:18 +01:00
37 changed files with 1255 additions and 44 deletions

View File

@ -0,0 +1,4 @@
def station(request):
station = request.station
audio_streams = station.streams if station else None
return {"station": station, "audio_streams": audio_streams}

View File

@ -1,7 +1,12 @@
from django import forms
from django.forms import ModelForm
from django.forms import ModelForm, ImageField, FileField
from .models import Comment
from ckeditor.fields import RichTextField
from filer.models.imagemodels import Image
from filer.models.filemodels import File
from aircox.models import Comment, Episode, Program
from aircox.controllers.sound_file import SoundFile
class CommentForm(ModelForm):
@ -16,3 +21,38 @@ class CommentForm(ModelForm):
class Meta:
model = Comment
fields = ["nickname", "email", "content"]
class ProgramForm(ModelForm):
content = RichTextField()
new_cover = ImageField(required=False)
class Meta:
model = Program
fields = ["content"]
def save(self, commit=True):
file_obj = self.cleaned_data["new_cover"]
if file_obj:
obj, _ = Image.objects.get_or_create(original_filename=file_obj.name, file=file_obj)
self.instance.cover = obj
super().save(commit=commit)
class EpisodeForm(ModelForm):
content = RichTextField()
new_podcast = FileField(required=False)
class Meta:
model = Episode
fields = ["content"]
def save(self, commit=True):
file_obj = self.cleaned_data["new_podcast"]
if file_obj:
obj, _ = File.objects.get_or_create(original_filename=file_obj.name, file=file_obj)
sound_file = SoundFile(obj.path)
sound_file.sync(
program=self.instance.program, episode=self.instance, type=0, is_public=True, is_downloadable=True
)
super().save(commit=commit)

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

@ -0,0 +1,12 @@
# Generated by Django 4.2.7 on 2024-02-05 09:27
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("aircox", "0019_merge_20240119_1022"),
("aircox", "0019_station_program_streams_title_and_more"),
]
operations = []

View File

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

View File

@ -0,0 +1,18 @@
from django.db import migrations
from aircox.models import Program
def set_group_ownership(*args):
for program in Program.objects.all():
program.set_group_ownership()
class Migration(migrations.Migration):
dependencies = [
("aircox", "0021_alter_schedule_timezone"),
]
operations = [
migrations.RunPython(set_group_ownership),
]

View File

@ -41,8 +41,7 @@ def user_default_groups(sender, instance, created, *args, **kwargs):
@receiver(signals.post_save, sender=Page)
def page_post_save(sender, instance, created, *args, **kwargs):
return
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)
@ -60,8 +59,7 @@ def program_post_save(sender, instance, created, *args, **kwargs):
@receiver(signals.pre_save, sender=Schedule)
def schedule_pre_save(sender, instance, *args, **kwargs):
return
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)

View File

@ -0,0 +1,29 @@
/* global CKEDITOR, django */
/* Modified in order to be manually loaded after vue.js */
function initialiseCKEditor() {
var textareas = Array.prototype.slice.call(
document.querySelectorAll("textarea[data-type=ckeditortype]"),
)
for (var i = 0; i < textareas.length; ++i) {
var t = textareas[i]
if (
t.getAttribute("data-processed") == "0" &&
t.id.indexOf("__prefix__") == -1
) {
t.setAttribute("data-processed", "1")
var ext = JSON.parse(t.getAttribute("data-external-plugin-resources"))
for (var j = 0; j < ext.length; ++j) {
CKEDITOR.plugins.addExternal(ext[j][0], ext[j][1], ext[j][2])
}
CKEDITOR.replace(t.id, JSON.parse(t.getAttribute("data-config")))
}
}
}
function initialiseCKEditorInInlinedForms() {
if (typeof django === "object" && django.jQuery) {
django.jQuery(document).on("formset:added", initialiseCKEditor)
}
}
//})()

View File

@ -0,0 +1,34 @@
{% extends "aircox/base.html" %}
{% load i18n aircox %}
{% block head_title %}
{% block title %}{{ user.username }}{% endblock %}
{% endblock %}
{% block content-container %}
<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 %}
<ul>
{% for p in programs %}
<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 %}
</ul>
{% else %}
{% trans 'You are not listed as a program editor yet' %}
{% endif %}
</div>
{% endblock %}

View File

@ -1,3 +1,4 @@
{% load static i18n thumbnail aircox %}<!doctype html>
{% comment %}
Base website template. It displays various elements depending on context
variables.
@ -10,8 +11,6 @@ Usefull context:
- sidebar_url_name: url name sidebar item complete list
- sidebar_url_parent: parent page for sidebar items complete list
{% endcomment %}
{% load static i18n thumbnail aircox %}
<html>
<head>
<meta charset="utf-8" />
@ -71,12 +70,22 @@ Usefull context:
{% translate "Admin" %}
</a>
{% endif %}
{% if user.is_authenticated %}
<a class="nav-item" href="{% url "profile" %}" target="new">
{% translate "Profile" %}
</a>
<a class="nav-item" href="{% url "logout" %}" title="{% translate "Disconnect" %}"
aria-label="{% translate "Disconnect" %}">
<i class="fa fa-power-off"></i>
</a>
{% endif %}
{% endblock %}
</div>
{% endblock %}
</nav>
{% endblock %}
{% block secondary-nav %}{% endblock %}
{% endblock %}
</div>
{% block main-container %}
@ -90,6 +99,8 @@ Usefull context:
{% endblock %}
{% endspaceless %}
{% block header-container %}
{% if page or cover or title %}
<header class="container header preview preview-header {% if cover %}has-cover{% endif %}">
@ -103,6 +114,7 @@ Usefull context:
{% block headings %}
<div>
<h1 class="title is-1 {% block title-class %}{% endblock %}">{% block title %}{{ title|default:"" }}{% endblock %}</h1>
{% include "aircox/edit-link.html" %}
</div>
<div>
{% spaceless %}

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

@ -0,0 +1,36 @@
{% extends "aircox/basepage_detail.html" %}
{% load static i18n humanize honeypot aircox %}
{% block head_extra %}
<script type="text/javascript" src="{% static "aircox/js/admin.js" %}"></script>
<script type="text/javascript" src="{% static "aircox/js/ckeditor-init.js" %}"></script>
<!-- <script type="text/javascript" src="{% static "ckeditor/ckeditor-init.js" %}"></script> -->
<script type="text/javascript" src="{% static "ckeditor/ckeditor/ckeditor.js" %}"></script>
{% endblock %}
{% block init-scripts %}
aircox.init(null, {hotReload:false, initPlayer:false, initApp:true})
initialiseCKEditor()
initialiseCKEditorInInlinedForms()
{% endblock %}
{% block comments %}
{% endblock %}
{% block content-container %}
<section class="container">
<form method="post" enctype="multipart/form-data">{% csrf_token %}
<table>
{{ form.as_table }}
{% render_honeypot_field "website" %}
</table>
<br/>
<input type="submit" value="Update" class="button is-success">
{% include "aircox/playlist_inline.html" %}
<input type="submit" value="Update" class="button is-success">
</form>
</section>
{% endblock %}

View File

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

View File

@ -10,21 +10,6 @@ Context:
- related_url: url to the full list of related_objects
{% endcomment %}
{% 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 breadcrumbs %}
{% if parent %}
{% include "./widgets/breadcrumbs.html" with page=parent %}

View File

@ -0,0 +1,70 @@
{% comment %}Inline block to edit playlists{% endcomment %}
{% load aircox aircox_admin static i18n %}
<div id="inline-tracks" class="box mb-5">
{{ formset.non_form_errors }}
<!-- formset.management_form -->
<a-playlist-editor
:labels="{% track_inline_labels %}"
:init-data="{% track_inline_data formset=formset %}"
settings-url="{% url "api:user-settings" %}"
data-prefix="{{ formset.prefix }}-">
<template #title>
<h5 class="title is-4">{% trans "Playlist" %}</h5>
</template>
<template #top="{items}">
<input type="hidden" name="{{ formset.prefix }}-TOTAL_FORMS"
:value="items.length || 0"/>
<input type="hidden" name="{{ formset.prefix }}-INITIAL_FORMS"
value="{{ formset.initial_form_count }}"/>
<input type="hidden" name="{{ formset.prefix }}-MIN_NUM_FORMS"
value="{{ formset.min_num }}"/>
<input type="hidden" name="{{ formset.prefix }}-MAX_NUM_FORMS"
value="{{ formset.max_num }}"/>
</template>
<template #rows-header-head>
<th style="max-width:2em" title="{% trans "Track Position" %}"
aria-description="{% trans "Track Position" %}">
<span class="icon">
<i class="fa fa-arrow-down-1-9"></i>
</span>
</th>
</template>
<template v-slot:row-head="{item,row}">
<td>
[[ row+1 ]]
<input type="hidden"
:name="'{{ formset.prefix }}-' + row + '-position'"
:value="row"/>
<input t-if="item.data.id" type="hidden"
:name="'{{ formset.prefix }}-' + row + '-id'"
:value="item.data.id || item.id"/>
{% for field in fields %}
{% if field != 'position' %}
<input type="hidden"
:name="'{{ formset.prefix }}-' + row + '-{{ field.name }}'"
v-model="item.data[attr]"/>
{% endif %}
{% endfor %}
</td>
</template>
{% for field in fields %}
<template v-slot:row-{{ field }}="{item,cell,value,attr,emit}">
<div class="field">
<a-autocomplete
:input-class="['input', item.error(attr) ? 'is-danger' : 'half-field']"
url="{% url 'api:track-autocomplete' %}?{{ field }}=${query}&field={{ field }}"
:name="'{{ formset.prefix }}-' + cell.row + '-{{ field }}'"
v-model="item.data[attr]"
title="{{ field }}"
@change="emit('change', col)"/>
<p v-for="error in item.error(attr)" class="help is-danger">
[[ error ]] !
</p>
</div>
</template>
{% endfor %}
</a-playlist-editor>
</div>

View File

@ -2,6 +2,18 @@
{% comment %}Detail page of a show{% endcomment %}
{% load i18n aircox %}
{% block top-nav-tools %}
{% has_perm page page.change_permission_codename simple=True as can_edit %}
{% if can_edit %}
<a class="navbar-item" href="{% url 'program-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-container %}
{% with schedules=program.schedule_set.all %}
{% if schedules %}

View File

@ -0,0 +1,39 @@
{% extends "aircox/page_detail.html" %}
{% load static i18n humanize honeypot aircox %}
{% block head_extra %}
{{ form.media }}
{% endblock %}
{% block init-scripts %}
{% endblock %}
{% block comments %}
{% endblock %}
{% block content-container %}
<section class="container">
<div>
<form method="post" enctype="multipart/form-data">{% csrf_token %}
{% csrf_token %}
{% for field in form %}
<div class="field is-horizontal">
<label class="label">{{ field.label }}</label>
<div class="control">{{ field }}</div>
</div>
{% 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 %}
{% endfor %}
<div class="has-text-right">
<button type="submit" class="button">{% translate "Update" %}</button>
</div>
</form>
</div>
</section>
{% endblock %}

View File

@ -25,12 +25,31 @@
{% endblock %}
{% block content %}
{% if not object.content %}
{% with object.parent.content as content %}
{{ block.super }}
{% endwith %}
{% else %}
{{ block.super }}
{% 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 %}
{% has_perm page object.program.change_permission_codename simple=True as can_edit %}
{% if can_edit %}
<a class="button" href="{% url 'episode-edit' object.pk %}" target="_self">
<span class="icon is-small"><i class="fas fa-pen" alt="{% trans 'edit' %}"></i></span>
</a>
{% endif %}
{% 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 %}

View File

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

View File

@ -62,7 +62,7 @@ def do_has_perm(context, obj, perm, user=None, simple=False):
if user is None:
user = context["request"].user
if simple:
return user.has_perm("aircox.{}".format(perm))
return user.has_perm("aircox.{}".format(perm)) or user.is_superuser
else:
return user.has_perm("{}.{}_{}".format(obj._meta.app_label, perm, obj._meta.model_name))
@ -134,3 +134,8 @@ def do_verbose_name(obj, plural=False):
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

@ -1,6 +1,7 @@
from datetime import time, timedelta
import itertools
import logging
import os
from django.conf import settings
from django.contrib.auth.models import User
@ -162,3 +163,10 @@ def tracks(episode, sound):
@pytest.fixture
def user():
return User.objects.create_user(username="user1", password="bar")
@pytest.fixture
def png_content():
image_file = "{}/image.png".format(os.path.dirname(__file__))
with open(image_file, "rb") as fh:
return fh.read()

BIN
aircox/tests/image.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 B

View File

@ -1,5 +1,6 @@
import pytest
from django.contrib.auth.models import User, Group
from django.urls import reverse
@pytest.mark.django_db()
@ -23,3 +24,23 @@ def test_group_can_change_program(user, client, program):
user = User.objects.get(pk=user.pk) # reload user in order to have permissions set
assert program.editors in user.groups.all()
assert user.has_perm("aircox.%s" % program.change_permission_codename)
@pytest.mark.django_db()
def test_group_change_program(user, client, program):
client.force_login(user)
response = client.get(reverse("program-edit", kwargs={"pk": program.pk}))
assert response.status_code == 403
user.groups.add(program.editors)
response = client.get(reverse("program-edit", kwargs={"pk": program.pk}))
assert response.status_code == 200
@pytest.mark.django_db()
def test_group_change_episode(user, client, program, episode):
client.force_login(user)
response = client.get(reverse("episode-edit", kwargs={"pk": episode.pk}))
assert response.status_code == 403
user.groups.add(program.editors)
response = client.get(reverse("episode-edit", kwargs={"pk": episode.pk}))
assert response.status_code == 200

View File

@ -0,0 +1,22 @@
import pytest
from django.urls import reverse
@pytest.mark.django_db()
def test_authenticate(user, client, program):
r = client.get(reverse("login"))
assert r.status_code == 200
assert b"id_username" in r.content
r = client.post(reverse("login"), kwargs={"username": "foo", "password": "bar"})
assert b"errorlist" in r.content
assert client.login(username="user1", password="bar")
@pytest.mark.django_db()
def test_profile_programs(user, client, program):
client.force_login(user)
r = client.get(reverse("profile"))
assert program.title not in r.content.decode("utf-8")
user.groups.add(program.editors)
r = client.get(reverse("profile"))
assert program.title in r.content.decode("utf-8")

View File

@ -0,0 +1,67 @@
from itertools import chain
import json
import pytest
from django.urls import reverse
from django.core.files.uploadedfile import SimpleUploadedFile
from aircox.models import Program
@pytest.mark.django_db()
def test_edit_program(user, client, program):
client.force_login(user)
response = client.get(reverse("program-detail", kwargs={"slug": program.slug}))
assert response.status_code == 200
assert "🖉 ".encode() not in response.content
user.groups.add(program.editors)
response = client.get(reverse("program-detail", kwargs={"slug": program.slug}))
assert "🖉 ".encode() in response.content
assert b"foobar" not in response.content
response = client.post(reverse("program-edit", kwargs={"pk": program.pk}), {"content": "foobar"})
response = client.get(reverse("program-detail", kwargs={"slug": program.slug}))
assert b"foobar" in response.content
@pytest.mark.django_db()
def test_add_cover(user, client, program, png_content):
assert program.cover is None
user.groups.add(program.editors)
client.force_login(user)
cover = SimpleUploadedFile("cover1.png", png_content, content_type="image/png")
r = client.post(
reverse("program-edit", kwargs={"pk": program.pk}), {"content": "foobar", "new_cover": cover}, follow=True
)
assert r.status_code == 200
p = Program.objects.get(pk=program.pk)
assert "cover1.png" in p.cover.url
@pytest.mark.django_db()
def test_edit_tracklist(user, client, program, episode, tracks):
user.groups.add(program.editors)
client.force_login(user)
episode.status = 0x10 # published
episode.save()
r = client.get(reverse("program-detail", kwargs={"slug": episode.program.slug}))
assert r.status_code == 200
r = client.get(reverse("episode-detail", kwargs={"slug": episode.slug}))
assert r.status_code == 200
r2 = client.get(reverse("episode-edit", kwargs={"pk": episode.pk}))
assert r2.status_code == 200
tracklist = [t.id for t in episode.track_set.all().order_by("position")]
tracklist_details_reversed = [(t.id, t.artist, t.title) for t in episode.track_set.all().order_by("-position")]
tracklist_details_reversed = list(chain(*tracklist_details_reversed))
data = """{{"website": [""], "content": ["foobar"], "new_podcast": [""], "form-TOTAL_FORMS": ["3"],
"form-INITIAL_FORMS": ["3"], "form-MIN_NUM_FORMS": ["0"], "form-MAX_NUM_FORMS": ["1000"], "form-0-position": ["0"],
"form-0-id": ["{}"], "form-0-": ["", "", "", "", "", ""], "form-0-artist": ["{}"], "form-0-title": ["{}"],
"form-0-tags": [""], "form-0-album": [""], "form-0-year": [""], "form-1-position": ["1"], "form-1-id": ["{}"],
"form-1-": ["", "", "", "", "", ""], "form-1-artist": ["{}"], "form-1-title": ["{}"], "form-1-tags": [""],
"form-1-album": [""], "form-1-year": [""], "form-2-position": ["2"], "form-2-id": ["{}"], "form-2-": ["", "", "",
"", "", ""], "form-2-artist": ["{}"], "form-2-title": ["{}"], "form-2-tags": [""], "form-2-album": [""],
"form-2-year": [""]}}""".format(
*tracklist_details_reversed
)
r = client.post(reverse("episode-edit", kwargs={"pk": episode.pk}), json.loads(data), follow=True)
assert r.status_code == 200
assert set(episode.track_set.all().values_list("id", flat=True)) == set(tracklist)

View File

@ -114,12 +114,24 @@ urls = [
views.EpisodeDetailView.as_view(),
name="episode-detail",
),
path(
_("programs/episodes/<pk>/edit/"),
views.EpisodeUpdateView.as_view(),
name="episode-edit",
),
path(_("podcasts/"), views.PodcastListView.as_view(), name="podcast-list"),
path(_("podcasts/c/<slug:category_slug>/"), views.PodcastListView.as_view(), name="podcast-list"),
# ---- others
path(
_("program/<pk>/edit/"),
views.ProgramUpdateView.as_view(),
name="program-edit",
),
path(
"errors/no-station",
views.errors.NoStationErrorView.as_view(),
name="errors-no-station",
),
path("gestion/", views.ProfileView.as_view(), name="profile"),
path("accounts/profile/", views.ProfileView.as_view(), name="profile"),
]

View File

@ -2,7 +2,7 @@ from . import admin, errors
from .article import ArticleDetailView, ArticleListView
from .base import BaseAPIView, BaseView
from .diffusion import DiffusionListView, TimeTableView
from .episode import EpisodeDetailView, EpisodeListView, PodcastListView
from .episode import EpisodeDetailView, EpisodeListView, PodcastListView, EpisodeUpdateView
from .home import HomeView
from .log import LogListAPIView, LogListView
from .page import (
@ -11,11 +11,13 @@ from .page import (
PageDetailView,
PageListView,
)
from .profile import ProfileView
from .program import (
ProgramDetailView,
ProgramListView,
ProgramPageDetailView,
ProgramPageListView,
ProgramUpdateView,
)
__all__ = (
@ -30,6 +32,7 @@ __all__ = (
"EpisodeDetailView",
"EpisodeListView",
"PodcastListView",
"EpisodeUpdateView",
"HomeView",
"LogListAPIView",
"LogListView",
@ -37,10 +40,12 @@ __all__ = (
"BasePageListView",
"PageDetailView",
"PageListView",
"ProfileView",
"ProgramDetailView",
"ProgramListView",
"ProgramPageDetailView",
"ProgramPageListView",
"ProgramUpdateView",
"attached",
)

View File

@ -50,13 +50,9 @@ class BaseView(TemplateResponseMixin, ContextMixin):
return None
def get_context_data(self, **kwargs):
kwargs.setdefault("station", self.station)
kwargs.setdefault("page", self.get_page())
kwargs.setdefault("header_template_name", self.header_template_name)
if "audio_streams" not in kwargs:
kwargs["audio_streams"] = self.station.streams
if "model" not in kwargs:
model = getattr(self, "model", None) or hasattr(self, "object") and type(self.object)
kwargs["model"] = model

View File

@ -1,14 +1,20 @@
from django.shortcuts import reverse
from django.contrib.auth.mixins import UserPassesTestMixin
from django.forms.models import modelformset_factory
from django.urls import reverse
from aircox.forms import EpisodeForm
from aircox.models import Episode, Program, StaticPage, Track
from ..filters import EpisodeFilters
from ..models import Episode, Program, StaticPage
from .page import PageListView
from .program import ProgramPageDetailView
from .program import ProgramPageDetailView, BaseProgramMixin
from .page import PageUpdateView
__all__ = (
"EpisodeDetailView",
"EpisodeListView",
"PodcastListView",
"EpisodeUpdateView",
)
@ -39,3 +45,43 @@ class EpisodeListView(PageListView):
class PodcastListView(EpisodeListView):
attach_to_value = StaticPage.Target.PODCASTS
queryset = Episode.objects.published().with_podcasts().order_by("-pub_date")
class EpisodeUpdateView(UserPassesTestMixin, BaseProgramMixin, PageUpdateView):
model = Episode
form_class = EpisodeForm
template_name = "aircox/episode_form.html"
def get_sidebar_queryset(self):
return super().get_sidebar_queryset().filter(parent=self.program)
def test_func(self):
program = self.get_object().program
return self.request.user.has_perm("aircox.%s" % program.change_permission_codename)
def get_success_url(self):
return reverse("episode-detail", kwargs={"slug": self.get_object().slug})
def get_object(self, queryset=None):
obj = Episode.objects.get(pk=self.kwargs["pk"])
return obj
def get_formset(self, *args, **kwargs):
fields = ("position", "artist", "title", "tags", "album", "year", "info")
TrackFormSet = modelformset_factory(Track, fields=fields, extra=0)
return TrackFormSet(*args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["fields"] = ("position", "artist", "title", "tags", "album", "year", "info")
context["formset"] = self.get_formset(queryset=Track.objects.filter(episode=self.object))
return context
def post(self, request, *args, **kwargs):
super().post(request, *args, **kwargs)
formset = self.get_formset(request.POST)
if formset.is_valid():
formset.save()
return super().form_valid(formset)
else:
return super().form_valid(formset) # form_invalid(formset)

View File

@ -63,7 +63,7 @@ class ParentMixin:
def get_context_data(self, **kwargs):
parent = kwargs.setdefault("parent", self.parent)
if parent is not None:
if parent is not None and parent.cover:
kwargs.setdefault("cover", parent.cover.url)
return super().get_context_data(**kwargs)

View File

@ -1,6 +1,7 @@
from django.http import HttpResponse
from django.utils.translation import gettext_lazy as _
from django.views.generic import DetailView, ListView
from django.views.generic.edit import UpdateView
from django.urls import reverse
from honeypot.decorators import check_honeypot
@ -15,6 +16,7 @@ __all__ = [
"BasePageDetailView",
"PageDetailView",
"PageListView",
"PageUpdateView",
]
@ -180,3 +182,10 @@ class PageDetailView(BasePageDetailView):
comment.page = self.object
comment.save()
return self.get(request, *args, **kwargs)
class PageUpdateView(BaseView, UpdateView):
context_object_name = "page"
def get_page(self):
return self.object

15
aircox/views/profile.py Normal file
View File

@ -0,0 +1,15 @@
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic.base import TemplateView
from aircox.models import Program
from aircox.views import BaseView
class ProfileView(LoginRequiredMixin, BaseView, TemplateView):
template_name = "accounts/profile.html"
def get_context_data(self, **kwargs):
groups = self.request.user.groups.all()
programs = Program.objects.filter(editors__in=groups)
kwargs.update({"user": self.request.user, "programs": programs})
return super().get_context_data(**kwargs)

View File

@ -1,10 +1,12 @@
import random
from django.contrib.auth.mixins import UserPassesTestMixin
from django.urls import reverse
from ..models import Article, Page, Program, StaticPage, Episode
from aircox.forms import ProgramForm
from aircox.models import Article, Episode, Page, Program, StaticPage
from .mixins import ParentMixin
from .page import PageDetailView, PageListView
from .page import PageDetailView, PageListView, PageUpdateView
__all__ = ["ProgramPageDetailView", "ProgramDetailView", "ProgramPageListView"]
@ -46,6 +48,24 @@ class ProgramDetailView(BaseProgramMixin, PageDetailView):
**kwargs,
)
def get_template_names(self):
return super().get_template_names() + ["aircox/program_detail.html"]
class ProgramUpdateView(UserPassesTestMixin, BaseProgramMixin, PageUpdateView):
model = Program
form_class = ProgramForm
def get_sidebar_queryset(self):
return super().get_sidebar_queryset().filter(parent=self.program)
def test_func(self):
program = self.get_object()
return self.request.user.has_perm("aircox.%s" % program.change_permission_codename)
def get_success_url(self):
return reverse("program-detail", kwargs={"slug": self.get_object().slug})
class ProgramListView(PageListView):
model = Program

Binary file not shown.

View File

@ -238,6 +238,7 @@ TEMPLATES = [
"django.template.context_processors.static",
"django.template.context_processors.tz",
"django.contrib.messages.context_processors.messages",
"aircox.context_processors.station",
),
"loaders": (
"django.template.loaders.filesystem.Loader",
@ -249,3 +250,5 @@ TEMPLATES = [
WSGI_APPLICATION = "instance.wsgi.application"
LOGOUT_REDIRECT_URL = "/"

View File

@ -23,6 +23,7 @@ import aircox.urls
urlpatterns = aircox.urls.urls + [
path("admin/", admin.site.urls),
path("accounts/", include("django.contrib.auth.urls")),
path("ckeditor/", include("ckeditor_uploader.urls")),
path("filer/", include("filer.urls")),
]