Compare commits

...

19 Commits

Author SHA1 Message Date
07f3881dde docs: update user manual with simplified program management for animators 2024-02-28 16:57:04 +01:00
6469913f8a db: create program editors groups 2024-02-28 16:57:04 +01:00
75f1973d41 db: add missing migration on schedule timezone 2024-02-28 16:57:04 +01:00
b96cf59780 episode-form: add tracks inline formset 2024-02-28 16:57:04 +01:00
63c7fadd3f templatetags: display edit-links for admins 2024-02-28 16:57:04 +01:00
c9ddd38b9b templates: update after merging branch 118-design 2024-02-28 16:57:04 +01:00
72cd24083a templates: add in-context edition links 2024-02-28 16:57:04 +01:00
fd3411ed38 db: migrations merge 2024-02-28 16:57:04 +01:00
fddaaee169 templates: update container block names 2024-02-28 16:57:04 +01:00
42ae35a6ae signals: disable schedule_pre_save when using loaddata 2024-02-28 16:57:04 +01:00
1bfbdb304f templates: set document type to html, prevent quicks mode 2024-02-28 16:57:00 +01:00
27b0bec870 context_processors: prevent a null station error when no default station is defined 2024-02-28 16:55:14 +01:00
5986d86da3 views/program: allow changing program cover 2024-02-28 16:52:03 +01:00
b4539481e6 misc: add a profile view for authenticated users 2024-02-28 16:43:37 +01:00
25f6d91903 misc: use the django authentication system 2024-02-28 16:23:25 +01:00
8b3d3a2483 misc: move station and audio_streams to context_processors (in order to have them available in accounts views) 2024-02-28 16:23:25 +01:00
c8b0d1c5fb misc: edit programs in site 2024-02-28 16:23:19 +01:00
1674266890 templatetags: parametrize has_perm() in order to enable aircox namespace permissions 2024-02-28 15:17:38 +01:00
dd71f984ed models/program: link to editor groups 2024-02-28 15:17:31 +01:00
39 changed files with 1310 additions and 45 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 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): class CommentForm(ModelForm):
@ -16,3 +21,38 @@ class CommentForm(ModelForm):
class Meta: class Meta:
model = Comment model = Comment
fields = ["nickname", "email", "content"] 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,25 @@
# Generated by Django 4.2.5 on 2023-10-18 13:50
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("auth", "0012_alter_user_first_name_max_length"),
("aircox", "0014_alter_schedule_timezone"),
]
operations = [
migrations.AddField(
model_name="program",
name="editors",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
to="auth.group",
verbose_name="editors",
),
),
]

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

@ -3,6 +3,8 @@ import os
import shutil import shutil
from django.conf import settings as conf from django.conf import settings as conf
from django.contrib.auth.models import Group, Permission
from django.contrib.contenttypes.models import ContentType
from django.db import models from django.db import models
from django.db.models import F from django.db.models import F
from django.db.models.functions import Concat, Substr from django.db.models.functions import Concat, Substr
@ -58,6 +60,7 @@ class Program(Page):
default=True, default=True,
help_text=_("update later diffusions according to schedule changes"), help_text=_("update later diffusions according to schedule changes"),
) )
editors = models.ForeignKey(Group, models.CASCADE, blank=True, null=True, verbose_name=_("editors"))
objects = ProgramQuerySet.as_manager() objects = ProgramQuerySet.as_manager()
detail_url_name = "program-detail" detail_url_name = "program-detail"
@ -81,6 +84,14 @@ class Program(Page):
def excerpts_path(self): def excerpts_path(self):
return os.path.join(self.path, settings.SOUND_ARCHIVES_SUBDIR) return os.path.join(self.path, settings.SOUND_ARCHIVES_SUBDIR)
@property
def editors_group_name(self):
return f"{self.title} editors"
@property
def change_permission_codename(self):
return f"change_program_{self.slug}"
def __init__(self, *kargs, **kwargs): def __init__(self, *kargs, **kwargs):
super().__init__(*kargs, **kwargs) super().__init__(*kargs, **kwargs)
if self.slug: if self.slug:
@ -110,6 +121,19 @@ class Program(Page):
os.makedirs(path, exist_ok=True) os.makedirs(path, exist_ok=True)
return os.path.exists(path) return os.path.exists(path)
def set_group_ownership(self):
editors, created = Group.objects.get_or_create(name=self.editors_group_name)
if created:
self.editors = editors
super().save()
permission, _ = Permission.objects.get_or_create(
name=f"change program {self.title}",
codename=self.change_permission_codename,
content_type=ContentType.objects.get_for_model(self),
)
if permission not in editors.permissions.all():
editors.permissions.add(permission)
class Meta: class Meta:
verbose_name = _("Program") verbose_name = _("Program")
verbose_name_plural = _("Programs") verbose_name_plural = _("Programs")
@ -135,6 +159,8 @@ class Program(Page):
shutil.move(abspath, self.abspath) shutil.move(abspath, self.abspath)
Sound.objects.filter(path__startswith=path_).update(file=Concat("file", Substr(F("file"), len(path_)))) Sound.objects.filter(path__startswith=path_).update(file=Concat("file", Substr(F("file"), len(path_))))
self.set_group_ownership()
class ProgramChildQuerySet(PageQuerySet): class ProgramChildQuerySet(PageQuerySet):
def station(self, station=None, id=None): def station(self, station=None, id=None):

View File

@ -41,8 +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):
return if not created and instance.cover and "raw" not in kwargs:
if not created and instance.cover:
Page.objects.filter(parent=instance, cover__isnull=True).update(cover=instance.cover) 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) @receiver(signals.pre_save, sender=Schedule)
def schedule_pre_save(sender, instance, *args, **kwargs): def schedule_pre_save(sender, instance, *args, **kwargs):
return if getattr(instance, "pk") is not None and "raw" not in kwargs:
if getattr(instance, "pk") is not None:
instance._initial = Schedule.objects.get(pk=instance.pk) 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 %} {% comment %}
Base website template. It displays various elements depending on context Base website template. It displays various elements depending on context
variables. variables.
@ -10,8 +11,6 @@ Usefull context:
- sidebar_url_name: url name sidebar item complete list - sidebar_url_name: url name sidebar item complete list
- sidebar_url_parent: parent page for sidebar items complete list - sidebar_url_parent: parent page for sidebar items complete list
{% endcomment %} {% endcomment %}
{% load static i18n thumbnail aircox %}
<html> <html>
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
@ -71,12 +70,21 @@ Usefull context:
{% translate "Admin" %} {% translate "Admin" %}
</a> </a>
{% endif %} {% endif %}
{% if user.is_authenticated %}
<a class="nav-item" href="{% url "profile" %}" target="new">
{% translate "Profile" %}
</a>
<a class="nav-item" href="{% url 'logout' %}">
<i title="{% translate 'disconnect' %}" class="fa fa-power-off"></i>
</a>
{% endif %}
{% endblock %} {% endblock %}
</div> </div>
{% endblock %} {% endblock %}
</nav> </nav>
{% endblock %}
{% block secondary-nav %}{% endblock %} {% block secondary-nav %}{% endblock %}
{% endblock %}
</div> </div>
{% block main-container %} {% block main-container %}
@ -90,6 +98,8 @@ Usefull context:
{% endblock %} {% endblock %}
{% endspaceless %} {% endspaceless %}
{% block header-container %} {% block header-container %}
{% if page or cover or title %} {% if page or cover or title %}
<header class="container header preview preview-header {% if cover %}has-cover{% endif %}"> <header class="container header preview preview-header {% if cover %}has-cover{% endif %}">
@ -103,6 +113,7 @@ Usefull context:
{% block headings %} {% block headings %}
<div> <div>
<h1 class="title is-1 {% block title-class %}{% endblock %}">{% block title %}{{ title|default:"" }}{% endblock %}</h1> <h1 class="title is-1 {% block title-class %}{% endblock %}">{% block title %}{{ title|default:"" }}{% endblock %}</h1>
{% include "aircox/edit-link.html" %}
</div> </div>
<div> <div>
{% spaceless %} {% 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,37 @@
{% 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 %} {% 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

@ -10,21 +10,6 @@ Context:
- related_url: url to the full list of related_objects - related_url: url to the full list of related_objects
{% endcomment %} {% 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 %} {% block breadcrumbs %}
{% if parent %} {% if parent %}
{% include "./widgets/breadcrumbs.html" with page=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 %} {% comment %}Detail page of a show{% endcomment %}
{% load i18n aircox %} {% 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 %} {% block content-container %}
{% with schedules=program.schedule_set.all %} {% with schedules=program.schedule_set.all %}
{% if schedules %} {% if schedules %}

View File

@ -0,0 +1,28 @@
{% 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 %}
<table>
{{ form.as_table }}
{% render_honeypot_field "website" %}
</table>
<br/>
<input type="submit" value="Update" class="button is-success">
</form>
</div>
</section>
{% endblock %}

View File

@ -25,12 +25,31 @@
{% endblock %} {% endblock %}
{% block content %} {% block actions %}
{% if not object.content %} {% if object.sound_set.public.count %}
{% with object.parent.content as content %} <button class="button" @click="player.playButtonClick($event)"
{{ block.super }} data-sounds="{{ object.podcasts|json }}">
{% endwith %} <span class="icon is-small">
{% else %} <span class="fas fa-play"></span>
{{ block.super }} </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 %} {% endif %}
{% endblock %} {% 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

@ -57,10 +57,13 @@ 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): 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 user is None: if user is None:
user = context["request"].user user = context["request"].user
if simple:
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)) return user.has_perm("{}.{}_{}".format(obj._meta.app_label, perm, obj._meta.model_name))
@ -131,3 +134,8 @@ def do_verbose_name(obj, plural=False):
if isinstance(obj, str): if isinstance(obj, str):
return obj return obj
return obj._meta.verbose_name_plural if plural else obj._meta.verbose_name 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 from datetime import time, timedelta
import itertools import itertools
import logging import logging
import os
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
@ -162,3 +163,10 @@ def tracks(episode, sound):
@pytest.fixture @pytest.fixture
def user(): def user():
return User.objects.create_user(username="user1", password="bar") 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,4 +1,6 @@
import pytest import pytest
from django.contrib.auth.models import User, Group
from django.urls import reverse
@pytest.mark.django_db() @pytest.mark.django_db()
@ -12,3 +14,33 @@ def test_no_admin(user, client):
def test_user_cannot_change_program_or_episode(user, client, program): def test_user_cannot_change_program_or_episode(user, client, program):
assert not user.has_perm("aircox.change_program") assert not user.has_perm("aircox.change_program")
assert not user.has_perm("aircox.change_episode") assert not user.has_perm("aircox.change_episode")
@pytest.mark.django_db()
def test_group_can_change_program(user, client, program):
assert program.editors in Group.objects.all()
assert not user.has_perm("aircox.%s" % program.change_permission_codename)
user.groups.add(program.editors)
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(), views.EpisodeDetailView.as_view(),
name="episode-detail", 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/"), views.PodcastListView.as_view(), name="podcast-list"),
path(_("podcasts/c/<slug:category_slug>/"), views.PodcastListView.as_view(), name="podcast-list"), path(_("podcasts/c/<slug:category_slug>/"), views.PodcastListView.as_view(), name="podcast-list"),
# ---- others # ---- others
path(
_("program/<pk>/edit/"),
views.ProgramUpdateView.as_view(),
name="program-edit",
),
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("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 .article import ArticleDetailView, ArticleListView
from .base import BaseAPIView, BaseView from .base import BaseAPIView, BaseView
from .diffusion import DiffusionListView, TimeTableView from .diffusion import DiffusionListView, TimeTableView
from .episode import EpisodeDetailView, EpisodeListView, PodcastListView 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 (
@ -11,11 +11,13 @@ from .page import (
PageDetailView, PageDetailView,
PageListView, PageListView,
) )
from .profile import ProfileView
from .program import ( from .program import (
ProgramDetailView, ProgramDetailView,
ProgramListView, ProgramListView,
ProgramPageDetailView, ProgramPageDetailView,
ProgramPageListView, ProgramPageListView,
ProgramUpdateView,
) )
__all__ = ( __all__ = (
@ -30,6 +32,7 @@ __all__ = (
"EpisodeDetailView", "EpisodeDetailView",
"EpisodeListView", "EpisodeListView",
"PodcastListView", "PodcastListView",
"EpisodeUpdateView",
"HomeView", "HomeView",
"LogListAPIView", "LogListAPIView",
"LogListView", "LogListView",
@ -37,10 +40,12 @@ __all__ = (
"BasePageListView", "BasePageListView",
"PageDetailView", "PageDetailView",
"PageListView", "PageListView",
"ProfileView",
"ProgramDetailView", "ProgramDetailView",
"ProgramListView", "ProgramListView",
"ProgramPageDetailView", "ProgramPageDetailView",
"ProgramPageListView", "ProgramPageListView",
"ProgramUpdateView",
"attached", "attached",
) )

View File

@ -50,13 +50,9 @@ class BaseView(TemplateResponseMixin, ContextMixin):
return None return None
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
kwargs.setdefault("station", self.station)
kwargs.setdefault("page", self.get_page()) kwargs.setdefault("page", self.get_page())
kwargs.setdefault("header_template_name", self.header_template_name) 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: 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

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 ..filters import EpisodeFilters
from ..models import Episode, Program, StaticPage
from .page import PageListView from .page import PageListView
from .program import ProgramPageDetailView from .program import ProgramPageDetailView, BaseProgramMixin
from .page import PageUpdateView
__all__ = ( __all__ = (
"EpisodeDetailView", "EpisodeDetailView",
"EpisodeListView", "EpisodeListView",
"PodcastListView", "PodcastListView",
"EpisodeUpdateView",
) )
@ -39,3 +45,43 @@ class EpisodeListView(PageListView):
class PodcastListView(EpisodeListView): class PodcastListView(EpisodeListView):
attach_to_value = StaticPage.Target.PODCASTS attach_to_value = StaticPage.Target.PODCASTS
queryset = Episode.objects.published().with_podcasts().order_by("-pub_date") 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): def get_context_data(self, **kwargs):
parent = kwargs.setdefault("parent", self.parent) 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) kwargs.setdefault("cover", parent.cover.url)
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)

View File

@ -1,6 +1,7 @@
from django.http import HttpResponse from django.http import 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.urls import reverse from django.urls import reverse
from honeypot.decorators import check_honeypot from honeypot.decorators import check_honeypot
@ -15,6 +16,7 @@ __all__ = [
"BasePageDetailView", "BasePageDetailView",
"PageDetailView", "PageDetailView",
"PageListView", "PageListView",
"PageUpdateView",
] ]
@ -180,3 +182,10 @@ class PageDetailView(BasePageDetailView):
comment.page = self.object comment.page = self.object
comment.save() comment.save()
return self.get(request, *args, **kwargs) 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 import random
from django.contrib.auth.mixins import UserPassesTestMixin
from django.urls import reverse 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 .mixins import ParentMixin
from .page import PageDetailView, PageListView from .page import PageDetailView, PageListView, PageUpdateView
__all__ = ["ProgramPageDetailView", "ProgramDetailView", "ProgramPageListView"] __all__ = ["ProgramPageDetailView", "ProgramDetailView", "ProgramPageListView"]
@ -46,6 +48,24 @@ class ProgramDetailView(BaseProgramMixin, PageDetailView):
**kwargs, **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): class ProgramListView(PageListView):
model = Program model = Program

Binary file not shown.

View File

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

View File

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