formset component

This commit is contained in:
bkfox 2024-03-28 05:21:25 +01:00
parent 6bfcdd06c8
commit b82f8f4527
19 changed files with 1246 additions and 312 deletions

View File

@ -63,8 +63,8 @@ class SoundCreateForm(forms.ModelForm):
TrackFormSet = modelformset_factory(
models.Track,
fields=[
"episode",
"position",
"episode",
"artist",
"title",
"tags",
@ -79,15 +79,15 @@ TrackFormSet = modelformset_factory(
EpisodeSoundFormSet = modelformset_factory(
models.EpisodeSound,
fields=(
"position",
"episode",
"sound",
"position",
"broadcast",
),
widgets={
"broadcast": forms.CheckboxInput(),
"episode": forms.HiddenInput(),
"sound": forms.HiddenInput(),
# "sound": forms.HiddenInput(),
"position": forms.HiddenInput(),
},
can_delete=True,

View File

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

View File

@ -0,0 +1,22 @@
# Generated by Django 4.2.9 on 2024-03-19 22:38
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("aircox", "0023_station_legal_label_alter_schedule_timezone"),
]
operations = [
migrations.RenameField(
model_name="usersettings",
old_name="playlist_editor_columns",
new_name="tracklist_editor_columns",
),
migrations.RenameField(
model_name="usersettings",
old_name="playlist_editor_sep",
new_name="tracklist_editor_sep",
),
]

View File

@ -0,0 +1,36 @@
# Generated by Django 4.2.9 on 2024-03-25 20:23
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("aircox", "0024_rename_playlist_editor_columns_usersettings_tracklist_editor_columns_and_more"),
]
operations = [
migrations.AddField(
model_name="sound",
name="is_removed",
field=models.BooleanField(default=False, help_text="file has been removed", verbose_name="removed"),
),
migrations.AlterField(
model_name="sound",
name="is_downloadable",
field=models.BooleanField(
default=False,
help_text="sound can be downloaded by visitors (sound must be public)",
verbose_name="downloadable",
),
),
migrations.AlterField(
model_name="sound",
name="is_public",
field=models.BooleanField(default=False, help_text="sound is available as podcast", verbose_name="public"),
),
migrations.AlterField(
model_name="sound",
name="type",
field=models.SmallIntegerField(choices=[(0, "other"), (1, "archive"), (2, "excerpt")], verbose_name="type"),
),
]

View File

@ -0,0 +1,108 @@
# Generated by Django 4.2.9 on 2024-03-26 02:53
import aircox.models.file
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("aircox", "0025_sound_is_removed_alter_sound_is_downloadable_and_more"),
]
operations = [
migrations.AlterModelOptions(
name="sound",
options={"verbose_name": "Sound file", "verbose_name_plural": "Sound files"},
),
migrations.RemoveField(
model_name="sound",
name="episode",
),
migrations.RemoveField(
model_name="sound",
name="position",
),
migrations.RemoveField(
model_name="sound",
name="type",
),
migrations.AddField(
model_name="sound",
name="broadcast",
field=models.BooleanField(
default=False, help_text="The sound is broadcasted on air", verbose_name="Broadcast"
),
),
migrations.AddField(
model_name="sound",
name="description",
field=models.TextField(blank=True, default="", max_length=256, verbose_name="description"),
),
migrations.AlterField(
model_name="sound",
name="file",
field=models.FileField(
db_index=True, max_length=256, upload_to=aircox.models.file.File._upload_to, verbose_name="file"
),
),
migrations.AlterField(
model_name="sound",
name="is_downloadable",
field=models.BooleanField(
default=False, help_text="sound can be downloaded by visitors", verbose_name="downloadable"
),
),
migrations.AlterField(
model_name="sound",
name="is_public",
field=models.BooleanField(default=False, help_text="file is publicly accessible", verbose_name="public"),
),
migrations.AlterField(
model_name="sound",
name="is_removed",
field=models.BooleanField(
db_index=True, default=False, help_text="file has been removed from server", verbose_name="removed"
),
),
migrations.AlterField(
model_name="sound",
name="name",
field=models.CharField(db_index=True, max_length=64, verbose_name="name"),
),
migrations.AlterField(
model_name="sound",
name="program",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="aircox.program",
verbose_name="Program",
),
),
migrations.CreateModel(
name="EpisodeSound",
fields=[
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
(
"position",
models.PositiveSmallIntegerField(
default=0, help_text="position in the playlist", verbose_name="order"
),
),
(
"broadcast",
models.BooleanField(
blank=None, help_text="The sound is broadcasted on air", verbose_name="Broadcast"
),
),
("episode", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="aircox.episode")),
("sound", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="aircox.sound")),
],
options={
"verbose_name": "Episode Sound",
"verbose_name_plural": "Episode Sounds",
},
),
]

File diff suppressed because one or more lines are too long

View File

@ -7,6 +7,7 @@ Context:
- related_field: field name that target object
- object: related object
- formset: formset used to render the list editor
- formset_data: formset data
{% endcomment %}
{% load aircox aircox_admin static i18n %}
@ -19,6 +20,7 @@ Context:
<{{ tag }}
{% block tag-attrs %}
:form-data="{{ formset_data|json }}"
:labels="window.aircox.labels"
:init-data="{% formset_inline_data formset=formset %}"
:columns="[{% for n, f in fields.items %}{% if not f.widget.is_hidden %}'{{ n }}',{% endif %}{% endfor %} ]"
@ -26,19 +28,6 @@ Context:
data-prefix="{{ formset.prefix }}-"
{% endblock %}>
{% block inner %}
<template #top="{items}">
{% block top %}
<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 }}"/>
{% endblock %}
</template>
<template #rows-header-head>
{% block rows-header-head %}
<th style="max-width:2em" title="{{ fields.position.help_text }}"
@ -49,46 +38,12 @@ Context:
</th>
{% endblock %}
</template>
<template v-slot:row-head="{item,row,attr}">
<td>
{% block row-head %}
[[ row+1 ]]
<input type="hidden"
:name="'{{ formset.prefix }}-' + row + '-position'"
:value="row"/>
<input type="hidden"
:name="'{{ formset.prefix }}-' + row + '-id'"
:value="item.data.id || item.id "/>
{% for name, field in fields.items %}
{% if name == related_field %}
<input type="hidden"
:name="'{{ formset.prefix }}-' + row + '-{{ name }}'"
value="{{ object.id }}"/>
{% elif name != 'position' and field.widget.is_hidden %}
<input type="hidden"
:name="'{{ formset.prefix }}-' + row + '-{{ name }}'"
v-model="item.data['{{ name }}']"/>
{% endif %}
{% endfor %}
{% endblock %}
</td>
</template>
{% for name, field in fields.items %}
{% if name != related_field and not field.widget.is_hidden and not field.is_readonly %}
<template v-slot:row-{{ name }}="{item,cell,value,attr,emit}">
<div class="field">
{% with full_name="'"|add:formset.prefix|add:"-' + cell.row + '-"|add:name|add:"'" %}
{% block row-field %}
<div class="control">
{% include "./v_form_field.html" with value="item.data."|add:name name=full_name %}
</div>
{% endblock %}
{% endwith %}
<p v-for="error in item.error(attr)" class="help is-danger">
[[ error ]] !
</p>
</div>
{% if not field.widget.is_hidden and not field.is_readonly %}
<template v-slot:control-{{ name }}="{item,cell,value,attr,emit,inputName}">
{% block row-control %}
{% include "./v_form_field.html" with value="item.data."|add:name name="inputName" %}
{% endblock %}
</template>
{% endif %}
{% endfor %}

View File

@ -20,7 +20,6 @@ Context:
sound-list-url="{% url "api:sound-list" %}?program={{ object.parent_id }}"
sound-upload-url="{% url "api:sound-list" %}"
sound-delete-url="{% url "api:sound-detail" pk=123 %}"
:item-defaults="{episode: {{ object.pk }}}"
{% endblock %}
{% block inner %}
@ -29,19 +28,15 @@ sound-delete-url="{% url "api:sound-detail" pk=123 %}"
<template #upload-form>
{% for field in sound_form %}
{% with field.name as name %}
{% with field.initial as value %}
{% with field.field as field %}
{% if name in "episode,program,sound" %}
{% include "./form_field.html" with value=value hidden=True %}
{% if name in "episode" %}
{% include "./form_field.html" with value=field.initial field=field.field hidden=True %}
{% elif name != "file" %}
<div class="field is-horizontal">
<label class="label mr-3">{{ field.label }}</label>
{% include "./form_field.html" with value=value %}
{% include "./form_field.html" with value=field.initial field=field.field %}
</div>
{% endif %}
{% endwith %}
{% endwith %}
{% endwith %}
{% endfor %}
</template>
<template #row-delete="{cell}">

View File

@ -16,12 +16,19 @@
{{ block.super }}
{% endblock %}
{% block row-field %}
{% block row-control %}
{% if name == "tags" %}
<input type="text" class="input"
:name="inputName"
v-model="item.data[attr]"
@change="emit('change', cell.col)"
>
{% else %}
<a-autocomplete
:input-class="['input', item.error(attr) ? 'is-danger' : 'half-field']"
url="{% url 'api:track-autocomplete' %}?{{ name }}=${query}&field={{ name }}"
:name="'{{ formset.prefix }}-' + cell.row + '-{{ name }}'"
:name="inputName"
v-model="item.data[attr]"
title="{{ name }}"
@change="emit('change', col)"/>
@change="emit('change', cell.col)"/>
{% endif %}
{% endblock %}

View File

@ -16,7 +16,7 @@
<a-playlist v-if="page" :set="podcasts"
name="{{ page.title }}"
list-class="menu-list" item-class="menu-item"
:player="player" :actions="['play']"
:player="player" :actions="['play', 'pin']"
@select="player.playItems('queue', $event.item)">
</a-playlist>
</section>

View File

@ -6,10 +6,10 @@
<template v-slot="{podcasts,page}">
{{ block.super }}
<hr/>
{% include "./dashboard/tracklist_editor.html" with formset=tracklist_formset %}
{% include "./dashboard/tracklist_editor.html" with formset=tracklist_formset formset_data=tracklist_formset_data %}
<hr/>
<h3 class="title">{% translate "Podcasts" %}</h3>
{% include "./dashboard/soundlist_editor.html" with formset=soundlist_formset %}
{% include "./dashboard/soundlist_editor.html" with formset=soundlist_formset formset_data=soundlist_formset_data %}
</template>
</a-episode>
{% endblock %}

View File

@ -2,8 +2,9 @@ from django.contrib.auth.mixins import UserPassesTestMixin
from django.urls import reverse
from aircox.models import Episode, Program, StaticPage, Track
from aircox import forms
from ..filters import EpisodeFilters
from aircox import forms, filters
from .mixins import VueFormDataMixin
from .page import PageListView
from .program import ProgramPageDetailView, BaseProgramMixin
from .page import PageUpdateView
@ -36,7 +37,7 @@ class EpisodeDetailView(ProgramPageDetailView):
class EpisodeListView(PageListView):
model = Episode
filterset_class = EpisodeFilters
filterset_class = filters.EpisodeFilters
parent_model = Program
attach_to_value = StaticPage.Target.EPISODES
@ -46,7 +47,7 @@ class PodcastListView(EpisodeListView):
queryset = Episode.objects.published().with_podcasts().order_by("-pub_date")
class EpisodeUpdateView(UserPassesTestMixin, BaseProgramMixin, PageUpdateView):
class EpisodeUpdateView(UserPassesTestMixin, VueFormDataMixin, BaseProgramMixin, PageUpdateView):
model = Episode
form_class = forms.EpisodeForm
template_name = "aircox/episode_form.html"
@ -110,6 +111,10 @@ class EpisodeUpdateView(UserPassesTestMixin, BaseProgramMixin, PageUpdateView):
for key, func in forms:
if key not in kwargs:
kwargs[key] = func(self.object)
for key in ("soundlist_formset", "tracklist_formset"):
formset = kwargs[key]
kwargs[f"{key}_data"] = self.get_formset_data(formset, {"episode": self.object.id})
return super().get_context_data(**kwargs)
def post(self, request, *args, **kwargs):
@ -123,7 +128,6 @@ class EpisodeUpdateView(UserPassesTestMixin, BaseProgramMixin, PageUpdateView):
for formset in formsets.values():
if not formset.is_valid():
invalid = True
breakpoint()
else:
formset.save()
if invalid:

View File

@ -108,3 +108,37 @@ class FiltersMixin:
params = self.request.GET.copy()
kwargs["get_params"] = params.pop("page", True) and params
return super().get_context_data(**kwargs)
class VueFormDataMixin:
"""Provide form information as data to be used with vue components."""
# Note: values corresponds to AFormSet expected one
def get_form_field_data(self, form, values=None):
"""Return form fields as data."""
model = form.Meta.model
fields = ((name, field, model._meta.get_field(name)) for name, field in form.base_fields.items())
return [
{
"name": name,
"label": str(m_field.verbose_name).capitalize(),
"help": str(m_field.help_text).capitalize(),
"hidden": field.widget.is_hidden,
"value": values and values.get(name),
}
for name, field, m_field in fields
]
def get_formset_data(self, formset, field_values=None, **kwargs):
"""Return formset as data object."""
return {
"prefix": formset.prefix,
"management": {
"initial_forms": formset.initial_form_count(),
"min_num_forms": formset.min_num,
"max_num_forms": formset.max_num,
},
"fields": self.get_form_field_data(formset.form, field_values),
**kwargs,
}

View File

@ -0,0 +1,195 @@
<template>
<div>
<input type="hidden" :name="_prefix + 'TOTAL_FORMS'" :value="items.length || 0"/>
<template v-for="(value,name) in formData.management" v-bind:key="name">
<input type="hidden" :name="_prefix + name.toUpperCase()"
:value="value"/>
</template>
<a-rows ref="rows" :set="set"
:columns="visibleFields" :columnsOrderable="columnsOrderable"
:orderable="orderable" @move="moveItem" @colmove="onMoveColumn"
@cell="e => $emit('cell', e)">
<template #header-head>
<template v-if="orderable">
<th style="max-width:2em" :title="orderField.label"
:aria-label="orderField.label"
:aria-description="orderField.help || ''">
<span class="icon">
<i class="fa fa-arrow-down-1-9"></i>
</span>
</th>
<slot name="rows-header-head"></slot>
</template>
</template>
<template #row-head="data">
<input v-if="orderable" type="hidden"
:name="_prefix + data.row + '-' + orderBy"
:value="data.row"/>
<input type="hidden" :name="_prefix + data.row + '-id'"
:value="data.item ? data.item.id : ''"/>
<template v-for="field of hiddenFields" v-bind:key="field.name">
<input type="hidden"
v-if="!(field.name in ['id', orderBy])"
:name="_prefix + data.row + '-' + field.name"
:value="field.value in [null, undefined] ? data.item.data[name] : field.value"/>
</template>
<slot name="row-head" v-bind="data">
<td v-if="orderable">{{ data.row+1 }}</td>
</slot>
</template>
<template v-for="(field,slot) of fieldSlots" v-bind:key="field.name"
v-slot:[slot]="data">
<slot :name="slot" v-bind="data" :field="field" :input-name="_prefix + data.cell.row + '-' + field.name">
<div class="field">
<div class="control">
<slot :name="'control-' + field.name" v-bind="data" :field="field" :input-name="_prefix + data.cell.row + '-' + field.name"/>
</div>
<p v-for="[error,index] in data.item.error(field.name)" class="help is-danger" v-bind:key="index">
{{ error }}
</p>
</div>
</slot>
</template>
<template #row-tail="data">
<slot v-if="$slots['row-tail']" name="row-tail" v-bind="data"/>
<td class="align-right pr-0">
<button type="button" class="button square"
@click.stop="removeItem(data.row, data.item)"
:title="labels.remove_item"
:aria-label="labels.remove_item">
<span class="icon"><i class="fa fa-trash" /></span>
</button>
</td>
</template>
</a-rows>
<div class="a-formset-footer flex-row">
<div class="flex-grow-1 flex-row">
<slot name="footer"/>
</div>
<div class="flex-grow-1 align-right">
<button type="button" class="button square is-warning p-2"
@click="reset()"
:title="labels.discard_changes"
:aria-label="labels.discard_changes"
>
<span class="icon"><i class="fa fa-rotate" /></span>
</button>
<button type="button" class="button square is-primary p-2"
@click="onActionAdd"
:title="labels.add_item"
:aria-label="labels.add_item"
>
<span class="icon">
<i class="fa fa-plus"/></span>
</button>
</div>
</div>
</div>
</template>
<script>
import {cloneDeep} from 'lodash'
import Model, {Set} from '../model'
import ARows from './ARows'
export default {
emit: ['cell', 'move', 'colmove', 'load'],
components: {ARows},
props: {
labels: Object,
//! If provided call this function instead of adding an item to rows on "+" button click.
actionAdd: Function,
//! If True, columns can be reordered
columnsOrderable: Boolean,
//! Field name used for ordering
orderBy: String,
//! Formset data as returned by get_formset_data
formData: Object,
//! Model class used for item's set
model: {type: Function, default: Model},
//! initial data set load at mount
initials: Array,
},
data() {
return {
set: new Set(Model),
}
},
computed: {
// ---- fields
_prefix() { return this.formData.prefix ? this.formData.prefix + '-' : '' },
fields() { return this.formData.fields },
orderField() { return this.orderBy && this.fields.find(f => f.name == this.orderBy) },
orderable() { return !!this.orderField },
hiddenFields() { return this.fields.filter(f => f.hidden && !(this.orderable && f == this.orderField)) },
visibleFields() { return this.fields.filter(f => !f.hidden) },
fieldSlots() { return this.visibleFields.reduce(
(slots, f) => ({...slots, ['row-' + f.name]: f}),
{}
)},
items() { return this.set.items },
rows() { return this.$refs.rows },
},
methods: {
onCellEvent(event) { this.$emit('cell', event) },
onMoveColumn(event) { this.$emit('colmove', event) },
onActionAdd() {
if(this.actionAdd)
return this.actionAdd(this)
this.set.push()
},
moveItem(event) {
const {from, to} = event
const set_ = event.set || this.set
set_.move(from, to);
this.$emit('move', {...event, seŧ: set_})
},
removeItem(row) {
const item = this.items[row]
if(item.id) {
// TODO
}
else {
this.items.splice(row,1)
}
},
//! Load items into set
load(items=[], reset=false) {
if(reset)
this.set.items = []
for(var item of items)
this.set.push(cloneDeep(item))
this.$emit('load', items)
},
//! Reset forms to initials
reset() {
this.load(this.initials || [], true)
},
},
mounted() {
this.reset()
}
}
</script>

View File

@ -27,12 +27,15 @@ import {isReactive, toRefs} from 'vue'
import Model from '../model'
export default {
emit: ['move', 'cell'],
emits: ['move', 'cell'],
props: {
//! Item to display in row
item: Object,
item: {type: Object, default: () => ({})},
//! Columns to display, as items' attributes
//! - name: field name / item attribute value
//! - label: display label
//! - help: help text
columns: Array,
//! Default cell's info
cell: {type: Object, default() { return {row: 0}}},

View File

@ -1,34 +1,38 @@
<template>
<table class="table is-stripped is-fullwidth">
<thead>
<a-row :item="labels" :columns="columns" :orderable="orderable"
@move="$emit('colmove', $event)">
<a-row :columns="columnNames"
:orderable="columnsOrderable" cellTag="th"
@move="moveColumn">
<template v-if="$slots['header-head']" v-slot:head="data">
<slot name="header-head" v-bind="data"/>
</template>
<template v-if="$slots['header-tail']" v-slot:tail="data">
<slot name="header-tail" v-bind="data"/>
</template>
<template v-for="column of columns" v-bind:key="column.name"
v-slot:[column.name]="data">
<slot :name="'header-' + column.name" v-bind="data">
{{ column.label }}
<span v-if="column.help" class="icon small"
:title="column.help">
<i class="fa fa-circle-question"/>
</span>
</slot>
</template>
</a-row>
</thead>
<tbody>
<slot name="head"/>
<template v-for="(item,row) in items" :key="row">
<!-- data-index comes from AList component drag & drop -->
<a-row :item="item" :cell="{row}" :columns="columns" :data-index="row"
<a-row :item="item" :cell="{row}" :columns="columnNames" :data-index="row"
:data-row="row"
:draggable="orderable"
@dragstart="onDragStart" @dragover="onDragOver" @drop="onDrop"
@cell="onCellEvent(row, $event)">
<template v-for="[name,slot] of rowSlots" :key="slot" v-slot:[slot]="data">
<template v-if="slot == 'head' || slot == 'tail'">
<slot :name="name" v-bind="data"/>
</template>
<template v-else>
<div>
<slot :name="name" v-bind="data"/>
</div>
</template>
<slot :name="name" v-bind="data"/>
</template>
</a-row>
</template>
@ -43,28 +47,32 @@ import ARow from './ARow.vue'
const Component = {
extends: AList,
components: { ARow },
emit: ['cell', 'colmove'],
emits: ['cell', 'colmove'],
props: {
...AList.props,
//! Ordered list of columns, as objects with:
//! - name: item attribute value
//! - label: display label
//! - help: help text
//! - hidden: if true, field is hidden
columns: Array,
labels: Object,
//! If True, columns are orderable
columnsOrderable: Boolean,
},
data() {
return {
...super.data,
// TODO: add observer
columns_: [...this.columns],
extraItem: new this.set.model(),
}
},
computed: {
rowCells() {
const cells = []
for(var row in this.items)
cells.push({row})
},
columnNames() { return this.columns_.map(c => c.name) },
columnLabels() { return this.columns_.map(c => c.label) },
rowSlots() {
return Object.keys(this.$slots).filter(x => x.startsWith('row-'))
.map(x => [x, x.slice(4)])
@ -72,6 +80,17 @@ const Component = {
},
methods: {
/**
* Move column using provided event object (as `{from, to}`)
*/
moveColumn(event) {
const {from, to} = event
const value = this.columns_[from]
this.columns_.splice(from, 1)
this.columns_.splice(to, 0, value)
this.$emit('colmove', event)
},
/**
* React on 'cell' event, re-emitting it with additional values:
* - `set`: data set

View File

@ -21,141 +21,63 @@
</template>
</a-select-file>
<slot name="top" :set="set" :items="set.items"></slot>
<a-rows :set="set" :columns="allColumns"
:labels="allColumnsLabels" :allow-create="true" :orderable="true"
@move="listItemMove">
<a-form-set ref="formset" :form-data="formData" :labels="labels"
:initials="initData.items"
order-by="position"
:action-add="actionAdd">
<template v-for="[name,slot] of rowsSlots" :key="slot"
v-slot:[slot]="data">
<slot v-if="name != 'row-tail'" :name="name" v-bind="data"/>
</template>
<template #row-sound="{item}">
<template #row-sound="{item,inputName}">
<label>{{ item.data.name }}</label><br>
<audio controls :src="item.data.url"/>
<input type="hidden" :name="inputName" :value="item.data.sound"/>
</template>
</a-rows>
<div class="flex-row">
<div class="flex-grow-1 flex-row">
</div>
<div class="flex-grow-1 align-right">
<button type="button" class="button square is-warning p-2"
@click="loadData({items: this.initData.items},true)"
:title="labels.discard_changes"
:aria-label="labels.discard_changes"
>
<span class="icon"><i class="fa fa-rotate" /></span>
</button>
<button type="button" class="button square is-primary p-2"
@click="$refs['select-file'].open()"
:title="labels.add_sound"
:aria-label="labels.add_sound"
>
<span class="icon">
<i class="fa fa-plus"/></span>
</button>
</div>
</div>
</a-form-set>
</div>
</template>
<script>
// import {dropRightWhile, cloneDeep, isEqual} from 'lodash'
import {cloneDeep} from 'lodash'
import Model, {Set} from '../model'
import ARows from './ARows'
//import AFileUpload from "./AFileUpload"
import AFormSet from './AFormSet'
import ASelectFile from "./ASelectFile"
export default {
components: {ARows, ASelectFile},
components: {AFormSet, ASelectFile},
props: {
// default values of items
itemDefaults: Object,
formData: Object,
labels: Object,
// initial datas
initData: Object,
labels: Object,
soundListUrl: String,
soundUploadUrl: String,
soundDeleteUrl: String,
columns: {
type: Array,
default: () => ['name', "broadcast"]
},
},
data() {
return {
set: new Set(Model),
}
},
computed: {
player_() {
return this.player || window.aircox.player
},
allColumns() {
return ["sound", ...this.columns, "delete"]
},
allColumnsLabels() {
return {...this.labels, ...this.initData.fields}
},
items() {
return this.set.items
},
rowsSlots() {
return Object.keys(this.$slots)
.filter(x => x.startsWith('row-') || x.startsWith('rows-'))
.filter(x => x.startsWith('row-') || x.startsWith('rows-') || x.startsWith('control-'))
.map(x => [x, x.startsWith('rows-') ? x.slice(5) : x])
},
},
methods: {
listItemMove({from, to, set}) {
set.move(from, to);
actionAdd() {
this.$refs['select-file'].open()
},
/**
* Load initial data
*/
loadData({items=[] /*, settings=null*/}, reset=false) {
if(reset) {
this.set.items = []
}
for(var index in items)
this.set.push(cloneDeep(items[index]))
// if(settings)
// this.settingsSaved(settings)
},
selected(item) {
selected(item) {
const data = {
...this.itemDefaults,
"sound": item.id,
"name": item.name,
"url": item.url,
"broadcast": item.broadcast,
}
this.set.push(data)
this.$refs.formset.set.push(data)
},
},
watch: {
initData(val) {
this.loadData(val)
},
},
mounted() {
this.initData && this.loadData(this.initData)
},
}
</script>

View File

@ -27,7 +27,6 @@
</div>
</div>
</div>
<slot name="top" :set="set" :columns="allColumns" :items="items"/>
<section v-show="page == Page.Text" class="panel">
<textarea ref="textarea" class="is-fullwidth is-size-6" rows="20"
@change="updateList"
@ -35,60 +34,33 @@
</section>
<section v-show="page == Page.List" class="panel">
<a-rows :set="set" :columns="allColumns" :labels="initData.fields"
:orderable="true" @move="listItemMove" @colmove="columnMove"
@cell="onCellEvent">
<a-form-set ref="formset"
:form-data="formData" :initials="initData.items"
:columnsOrderable="true" :labels="labels"
order-by="position"
@load="updateInput" @colmove="updateInput" @move="updateInput"
@cell="onCellEvent">
<template v-for="[name,slot] of rowsSlots" :key="slot"
v-slot:[slot]="data">
<slot v-if="name != 'row-tail'" :name="name" v-bind="data"/>
</template>
<template v-slot:row-tail="data">
<slot v-if="$slots['row-tail']" :name="row-tail" v-bind="data"/>
<td class="align-right pr-0">
<button type="button" class="button square"
@click.stop="items.splice(data.row,1)"
:title="labels.remove_item"
:aria-label="labels.remove_item">
<span class="icon"><i class="fa fa-trash" /></span>
</button>
</td>
<template #footer>
<div class="field">
<p class="control">
<button type="button" class="button is-info"
@click="$refs.settings.open()">
<span class="icon is-small">
<i class="fa fa-cog"></i>
</span>
<span>Options</span>
</button>
</p>
</div>
</template>
</a-rows>
</a-form-set>
</section>
<div class="flex-row">
<div class="flex-grow-1 flex-row">
<div class="field">
<p class="control">
<button type="button" class="button is-info"
@click="$refs.settings.open()">
<span class="icon is-small">
<i class="fa fa-cog"></i>
</span>
<span>Options</span>
</button>
</p>
</div>
</div>
<div class="flex-grow-1 align-right">
<button type="button" class="button square is-warning p-2"
@click="loadData({items: this.initData.items},true)"
:title="labels.discard_changes"
:aria-label="labels.discard_changes"
>
<span class="icon"><i class="fa fa-rotate" /></span>
</button>
<button type="button" class="button square is-primary p-2" v-if="page == Page.List"
@click="this.set.push(new this.set.model())"
:title="labels.add_item"
:aria-label="labels.add_item"
>
<span class="icon"><i class="fa fa-plus"/></span>
</button>
</div>
</div>
<a-modal ref="settings" title="Options">
<template #default>
<div class="field">
@ -97,12 +69,14 @@
</label>
<table class="table is-bordered"
style="vertical-align: middle">
<tr>
<a-row :columns="allColumns" :item="initData.fields"
@move="formatMove" :orderable="true">
<tr v-if="$refs.formset">
<a-row :columns="$refs.formset.rows.columnNames"
:item="$refs.formset.rows.columnLabels"
@move="$refs.formset.rows.moveColumn"
>
<template v-slot:cell-after="{cell}">
<td style="cursor:pointer;" v-if="cell.col < allColumns.length-1">
<span class="icon" @click="formatMove({from: cell.col, to: cell.col+1})"
<td style="cursor:pointer;" v-if="cell.col < $refs.formset.rows.columns_.length-1">
<span class="icon" @click="$refs.formset.rows.moveColumn({from: cell.col, to: cell.col+1})"
><i class="fa fa-left-right"/>
</span>
</td>
@ -143,16 +117,14 @@
</div>
</template>
</a-modal>
<slot name="bottom" :set="set" :columns="allColumns" :items="items"/>
</div>
</template>
<script>
import {dropRightWhile, cloneDeep, isEqual} from 'lodash'
import Model, {Set} from '../model'
import AActionButton from './AActionButton'
import AFormSet from './AFormSet'
import ARow from './ARow'
import ARows from './ARows'
import AModal from "./AModal"
/// Page display
@ -161,12 +133,14 @@ export const Page = {
}
export default {
components: { AActionButton, ARow, ARows, AModal },
components: { AActionButton, AFormSet, ARow, AModal },
props: {
formData: Object,
labels: Object,
///! initial data as: {items: [], fields: {column_name: label, settings: {}}
initData: Object,
dataPrefix: String,
labels: Object,
settingsUrl: String,
defaultColumns: {
type: Array,
@ -175,13 +149,12 @@ export default {
data() {
const settings = {
tracklist_editor_columns: this.columns,
// tracklist_editor_columns: this.columns,
tracklist_editor_sep: ' -- ',
}
return {
Page: Page,
page: Page.Text,
set: new Set(Model),
extraData: {},
settings,
savedSettings: cloneDeep(settings),
@ -189,6 +162,9 @@ export default {
},
computed: {
rows() { return this.$refs.formset && this.$refs.formset.rows },
columns() { return this.rows && this.rows.columns_ || [] },
settingsChanged() {
var k = Object.keys(this.savedSettings)
.findIndex(k => !isEqual(this.settings[k], this.savedSettings[k]))
@ -204,25 +180,9 @@ export default {
get() { return this.settings.tracklist_editor_sep }
},
allColumns: {
set(value) {
var cols = value.filter(x => x in this.defaultColumns)
var left = this.defaultColumns.filter(x => !(x in cols))
value = cols.concat(left)
this.settings.tracklist_editor_columns = value
},
get() {
return this.settings.tracklist_editor_columns
}
},
items() {
return this.set.items
},
rowsSlots() {
return Object.keys(this.$slots)
.filter(x => x.startsWith('row-') || x.startsWith('rows-'))
.filter(x => x.startsWith('row-') || x.startsWith('rows-') || x.startsWith('control-'))
.map(x => [x, x.startsWith('rows-') ? x.slice(5) : x])
},
},
@ -236,34 +196,30 @@ export default {
},
formatMove({from, to}) {
const value = this.allColumns[from]
this.settings.tracklist_editor_columns.splice(from, 1)
this.settings.tracklist_editor_columns.splice(to, 0, value)
if(this.page == Page.Text)
this.updateList()
else
this.updateInput()
this.$refs.formset.rows.columnMove({from, to})
/*this.settings.tracklist_editor_columns.splice(from, 1)
this.settings.tracklist_editor_columns.splice(to, 0, value)*/
},
columnMove({from, to}) {
const value = this.allColumns[from]
this.allColumns.splice(from, 1)
this.allColumns.splice(to, 0, value)
this.updateInput()
},
listItemMove({from, to, set}) {
moveItem({from, to, set}) {
set.move(from, to);
this.updateInput()
},
onColumnMove() {
if(this.page == this.Page.List)
this.updateInput()
else
this.updateList()
},
updateList() {
const items = this.toList(this.$refs.textarea.value)
this.set.reset(items)
this.$refs.formset.set.reset(items)
},
updateInput() {
const input = this.toText(this.items)
const input = this.toText(this.$refs.formset.items)
this.$refs.textarea.value = input
},
@ -271,6 +227,7 @@ export default {
* From input and separator, return list of items.
*/
toList(input) {
const columns = this.$refs.formset.rows.columns_
var lines = input.split('\n')
var items = []
@ -281,11 +238,11 @@ export default {
var lineBits = line.split(this.separator)
var item = {}
for(var col in this.allColumns) {
for(var col in columns) {
if(col >= lineBits.length)
break
const attr = this.allColumns[col]
item[attr] = lineBits[col].trim()
const column = columns[col]
item[column.name] = lineBits[col].trim()
}
item && items.push(item)
}
@ -296,14 +253,15 @@ export default {
* From items and separator return a string
*/
toText(items) {
const columns = this.$refs.formset.rows.columns_
const sep = ` ${this.separator.trim()} `
const lines = []
for(let item of items) {
if(!item)
continue
var line = []
for(var col of this.allColumns)
line.push(item.data[col] || '')
for(var col of columns)
line.push(item.data[col.name] || '')
line = dropRightWhile(line, x => !x || !('' + x).trim())
line = line.join(sep).trimRight()
lines.push(line)
@ -335,27 +293,17 @@ export default {
/**
* Load initial data
*/
loadData({items=[], settings=null}, reset=false) {
if(reset) {
this.set.items = []
}
for(var index in items)
this.set.push(cloneDeep(items[index]))
/*loadData({items=[], settings=null}, reset=false) {
if(settings)
this.settingsSaved(settings)
this.updateInput()
},
},
watch: {
initData(val) {
this.loadData(val)
},
},*/
},
mounted() {
this.initData && this.loadData(this.initData)
this.page = this.items.length ? Page.List : Page.Text
//this.initData && this.initData.settings &&
//this.initData && this.loadData(this.initData)
this.page = this.initData.items.length ? Page.List : Page.Text
},
}
</script>

View File

@ -16,6 +16,8 @@ import AFileUpload from "./AFileUpload"
import ASelectFile from "./ASelectFile"
import AStatistics from './AStatistics'
import AStreamer from './AStreamer'
import AFormSet from './AFormSet'
import ATrackListEditor from './ATrackListEditor'
import ASoundListEditor from './ASoundListEditor'
@ -37,5 +39,6 @@ export const admin = {
export const dashboard = {
...base,
AActionButton, AFileUpload, ASelectFile, AModal, ATrackListEditor, ASoundListEditor
AActionButton, AFileUpload, ASelectFile, AModal,
AFormSet, ATrackListEditor, ASoundListEditor
}