Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • aura/steering
  • kmohrf/steering
2 results
Show changes
Commits on Source (207)
Showing
with 1310 additions and 629 deletions
......@@ -14,4 +14,6 @@ Hermann Schwärzler <hermann.schwaerzler@freirad.at> Hermann <hermann@laptop>
jackie / Andrea Ida Malkah Klaura <jackie@diebin.at> <jackie@o94.at>
jackie / Andrea Ida Malkah Klaura <jackie@diebin.at> Andrea Ida Malkah Klaura
Roman Brendler <roman@jointech.org> Roman <roman@jointech.org>
EorlBruder <david@jointech.org> <eorl@bruder.space>
\ No newline at end of file
EorlBruder <david@jointech.org> <eorl@bruder.space>
Konrad Mohrfeldt <km@roko.li> <km@roko.li>
Konrad Mohrfeldt <km@roko.li> <konrad.mohrfeldt@farbdev.org>
......@@ -7,9 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Added
- `basic` and `calendar` program routes. (steering#239)
### Changed
- Changed the aura user and group ID from 2872 to 872.
- `playout` is now part of the program routes. (steering#120)
## [1.0.0-alpha4] - 2024-04-17
......
......@@ -6,14 +6,16 @@ from rest_framework.test import APIClient
from django.contrib.auth.models import Permission, User
from django.core.files.uploadedfile import SimpleUploadedFile
from program.models import (
CBA,
Category,
FundingCategory,
Host,
Image,
Language,
License,
LinkType,
MusicFocus,
Profile,
RadioSettings,
RRule,
Schedule,
Show,
......@@ -23,15 +25,17 @@ from program.models import (
)
from program.tests.factories import (
CategoryFactory,
CBAFactory,
CommonUserFactory,
FundingCategoryFactory,
HostFactory,
ImageFactory,
LanguageFactory,
LicenseFactory,
LinkTypeFactory,
MusicFocusFactory,
OwnerFactory,
ProfileFactory,
RadioSettingsFactory,
RRuleFactory,
ScheduleFactory,
ShowFactory,
......@@ -55,9 +59,30 @@ def assert_data(response, data) -> None:
assert response.data[key] == value
def create_daily_schedule(admin_api_client, daily_rrule, show) -> None:
"""creates a schedule for a show that repeats daily using the REST API."""
now = datetime.now()
in_one_hour = now + timedelta(hours=1)
in_seven_days = now + timedelta(days=7)
data = {
"schedule": {
"end_time": in_one_hour.strftime("%H:%M:%S"),
"first_date": now.strftime("%Y-%m-%d"),
"last_date": in_seven_days.strftime("%Y-%m-%d"),
"rrule_id": daily_rrule.id,
"show_id": show.id,
"start_time": now.strftime("%H:%M:%S"),
}
}
admin_api_client.post("/api/v1/schedules/", data=data, format="json")
@pytest.fixture
def host() -> Host:
return HostFactory()
def profile() -> Profile:
return ProfileFactory()
@pytest.fixture
......@@ -154,11 +179,36 @@ def once_rrule() -> RRule:
return RRuleFactory(freq=0)
@pytest.fixture
def daily_rrule() -> RRule:
return RRuleFactory(freq=3)
@pytest.fixture
def show() -> Show:
return ShowFactory()
@pytest.fixture
def fallback_show() -> Show:
return ShowFactory(name="Musikpool")
@pytest.fixture
def radio_settings(fallback_show) -> RadioSettings:
return RadioSettingsFactory(
fallback_default_pool="fallback",
fallback_show=fallback_show,
pools={"fallback": "Station Fallback Pool"},
station_name="Radio AURA",
)
@pytest.fixture
def cba(common_user1) -> CBA:
return CBAFactory(user=common_user1)
@pytest.fixture
def owned_show(common_user1, show) -> Show:
"""Show owned by a common user"""
......
[
{
"model": "program.radiosettings",
"pk": 1,
"fields": {
"cba_api_key": "",
"cba_domains": [
"cba.media"
],
"fallback_default_pool": "",
"fallback_show": null,
"host_image_aspect_ratio": "1:1",
"host_image_shape": "round",
"line_in_channels": {
"0": "live",
"1": "preprod"
},
"micro_show": null,
"note_image_aspect_ratio": "16:9",
"note_image_shape": "rect",
"show_image_aspect_ratio": "16:9",
"show_image_shape": "rect",
"show_logo_aspect_ratio": "1:1",
"show_logo_shape": "rect",
"station_logo": null,
"station_name": "Radio AURA",
"station_website": "https://aura.radio"
{
"model": "program.radiosettings",
"pk": 1,
"fields": {
"cba_api_key": "",
"cba_domains": ["cba.media"],
"fallback_default_pool": "fallback",
"fallback_show": null,
"profile_image_aspect_ratio": "1:1",
"profile_image_shape": "round",
"line_in_channels": {"0": "live", "1": "preprod"},
"micro_show": null,
"note_image_aspect_ratio": "16:9",
"pools": {"fallback": "Station Fallback Pool"},
"note_image_shape": "rect",
"show_image_aspect_ratio": "16:9",
"show_image_shape": "rect",
"show_logo_aspect_ratio": "1:1",
"show_logo_shape": "rect",
"station_logo": null,
"station_name": "Radio AURA",
"station_website": "https://aura.radio"
}
}
}
]
[
{
"model": "program.host",
"model": "program.profile",
"pk": 1,
"fields": {
"name": "Musikredaktion",
......
This diff is collapsed.
from django_json_widget.widgets import JSONEditorWidget
from django.conf import settings
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.models import User
from django.db.models import JSONField
from django.utils.safestring import mark_safe
from program.models import (
CBA,
Category,
FundingCategory,
Host,
Language,
License,
LinkType,
MusicFocus,
Profile,
RadioSettings,
RRule,
Topic,
Type,
UserProfile,
)
......@@ -36,8 +38,8 @@ class LicenseAdmin(admin.ModelAdmin):
list_display = ("name", "identifier")
@admin.register(Host)
class HostAdmin(admin.ModelAdmin):
@admin.register(Profile)
class ProfileAdmin(admin.ModelAdmin):
fields = ("name", "email", "biography", "created_at", "created_by", "updated_at", "updated_by")
list_display = ("name",)
readonly_fields = ("created_at", "created_by", "updated_at", "updated_by")
......@@ -56,19 +58,20 @@ class RRuleAdmin(admin.ModelAdmin):
list_display = ("name", "freq", "interval", "by_set_pos", "by_weekdays", "count")
class UserProfileInline(admin.StackedInline):
model = UserProfile
fields = ("cba_username", "cba_user_token")
class CBAInline(admin.StackedInline):
model = CBA
fields = ("username", "user_token")
can_delete = False
verbose_name_plural = "Profile"
verbose_name = "CBA"
verbose_name_plural = "CBA"
fk_name = "user"
class UserProfileUserAdmin(UserAdmin):
inlines = (UserProfileInline,)
class UserCBAAdmin(UserAdmin):
inlines = (CBAInline,)
def get_queryset(self, request):
"""Let common users only edit their own profile"""
"""Let common users only edit their own CBA."""
if not request.user.is_superuser:
return super(UserAdmin, self).get_queryset(request).filter(pk=request.user.id)
......@@ -90,38 +93,45 @@ class UserProfileUserAdmin(UserAdmin):
return list()
def get_inline_instances(self, request, obj=None):
"""Append profile fields to UserAdmin"""
"""Append CBA fields to UserAdmin"""
if not obj:
return list()
return super(UserProfileUserAdmin, self).get_inline_instances(request, obj)
return super(UserCBAAdmin, self).get_inline_instances(request, obj)
admin.site.unregister(User)
admin.site.register(User, UserProfileUserAdmin)
admin.site.register(User, UserCBAAdmin)
@admin.register(RadioSettings)
class RadioSettingsAdmin(admin.ModelAdmin):
fieldsets = [
(None, {"fields": ["station_name", "station_website", "station_logo"]}),
(
None,
{
"fields": [
"station_name",
"station_website",
"station_logo",
"station_logo_preview",
]
},
),
(
"Image requirements",
{
"fields": [
("host_image_aspect_ratio", "host_image_shape"),
(
"note_image_aspect_ratio",
"note_image_shape",
),
("note_image_aspect_ratio", "note_image_shape"),
("profile_image_aspect_ratio", "profile_image_shape"),
("show_image_aspect_ratio", "show_image_shape"),
("show_logo_aspect_ratio", "show_logo_shape"),
]
},
),
("Programme", {"fields": [("fallback_show", "fallback_default_pool"), "micro_show"]}),
("Program", {"fields": [("fallback_show", "fallback_default_pool"), "micro_show"]}),
("CBA", {"fields": ["cba_api_key", "cba_domains"]}),
("Playout", {"fields": ["line_in_channels"]}),
("Playout", {"fields": ["line_in_channels", "pools"]}),
]
formfield_overrides = {
JSONField: {
......@@ -135,3 +145,14 @@ class RadioSettingsAdmin(admin.ModelAdmin):
)
},
}
readonly_fields = ["station_logo_preview"]
@staticmethod
def station_logo_preview(obj):
url = obj.station_logo.url
height = obj.station_logo.height
width = obj.station_logo.width
return mark_safe(
f'<img src="{settings.SITE_URL}/{url}" width="{width}" height="{height}"/>'
)
from rest_framework import status
from rest_framework.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
class ConfigurationError(ValidationError):
status_code = status.HTTP_500_INTERNAL_SERVER_ERROR
default_detail = _("Invalid or insufficient server configuration.")
default_code = "misconfigured"
......@@ -8,6 +8,7 @@ from django import forms
from django.db.models import Exists, OuterRef, QuerySet
from django.utils import timezone
from program import models
from program.services import generate_program_entries
class StaticFilterHelpTextMixin:
......@@ -56,6 +57,10 @@ class ShowOrderingFilter(filters.OrderingFilter):
class ShowFilterSet(StaticFilterHelpTextMixin, filters.FilterSet):
ids = IntegerInFilter(
field_name="id",
help_text="Return only shows matching the specified id(s).",
)
order = ShowOrderingFilter(
fields=["name", "slug", "id", "is_active", "is_owner", "updated_at", "updated_by"],
help_text="Order shows by the given field(s).",
......@@ -70,7 +75,7 @@ class ShowFilterSet(StaticFilterHelpTextMixin, filters.FilterSet):
)
host_ids = IntegerInFilter(
field_name="hosts",
help_text="Return only shows assigned to the given host(s).",
help_text="Return only shows hosted by the given profile ID(s).",
)
is_active = filters.BooleanFilter(
field_name="is_active",
......@@ -120,7 +125,7 @@ class ShowFilterSet(StaticFilterHelpTextMixin, filters.FilterSet):
def filter_writable(self, queryset: QuerySet, _: str, value: bool) -> QuerySet:
user = self.request.user if self.request.user.is_authenticated else None
if value and (user.is_superuser or user.has_perm("program.update_show")):
if value and user and (user.is_superuser or user.has_perm("program.update_show")):
return queryset
elif value and user:
return queryset.filter(owners=user)
......@@ -130,6 +135,7 @@ class ShowFilterSet(StaticFilterHelpTextMixin, filters.FilterSet):
class Meta:
model = models.Show
fields = [
"ids",
"order",
"category_ids",
"category_slug",
......@@ -149,6 +155,10 @@ class ShowFilterSet(StaticFilterHelpTextMixin, filters.FilterSet):
class ScheduleFilterSet(filters.FilterSet):
ids = IntegerInFilter(
field_name="id",
help_text="Return only schedules matching the specified id(s).",
)
show_ids = IntegerInFilter(
field_name="show",
help_text="Return only schedules that belong to the specified show(s).",
......@@ -170,6 +180,10 @@ class ScheduleFilterSet(filters.FilterSet):
class TimeSlotFilterSet(filters.FilterSet):
ids = IntegerInFilter(
field_name="id",
help_text="Return only timeslots matching the specified id(s).",
)
order = filters.OrderingFilter(
fields=[field.name for field in models.TimeSlot._meta.get_fields()]
)
......@@ -236,6 +250,7 @@ class TimeSlotFilterSet(filters.FilterSet):
class Meta:
model = models.TimeSlot
fields = [
"ids",
"order",
"start",
"end",
......@@ -282,23 +297,40 @@ class ActiveFilterSet(StaticFilterHelpTextMixin, filters.FilterSet):
]
class PlayoutFilterSet(filters.FilterSet):
start = filters.DateFilter(
field_name="start",
lookup_expr="gte",
help_text="Returns timeslots that start at or after the specified datetime "
"(default: today).",
)
end = filters.DateFilter(
field_name="end",
lookup_expr="lte",
help_text="Returns timeslots that end at or before the specified datetime "
"(default: one week after start date).",
class VirtualTimeslotFilterSet(filters.FilterSet):
start = filters.IsoDateTimeFilter(method="filter_noop")
end = filters.IsoDateTimeFilter(method="filter_noop")
cut_at_range_boundaries = filters.BooleanFilter(
help_text=(
"If true guarantees that the first and last program entry match the requested range "
"even if these entries earlier or end later."
),
method="filter_noop",
)
include_virtual = filters.BooleanFilter(
field_name="include_virtual", help_text="Include virtual timeslots (default: false)."
help_text="Include virtual timeslot entries (default: false).",
method="filter_noop",
)
# Filters using the noop are implemented in the generate_program_entries generator.
# We do this, so that we have all the bells and whistles of the automatic value conversion,
# but can still implement custom filter logic on top.
def filter_noop(self, queryset: QuerySet, _: str, value: bool) -> QuerySet:
return queryset
def filter_queryset(self, queryset: QuerySet):
queryset = super().filter_queryset(queryset)
filter_data = self.form.cleaned_data
return list(
generate_program_entries(
queryset,
start=filter_data["start"],
end=filter_data["end"],
include_virtual=bool(filter_data["include_virtual"]),
cut_at_range_boundaries=bool(filter_data["cut_at_range_boundaries"]),
)
)
class Meta:
model = models.TimeSlot
fields = ["start", "end", "include_virtual"]
import sys
from django.core.exceptions import ValidationError
from django.core.management.base import BaseCommand, CommandError
from program.models import Note, Show, TimeSlot
from program.utils import parse_date
class Command(BaseCommand):
help = "adds a note to a timeslot"
args = "<show_id> <start_date> <status> [index]"
def handle(self, *args, **options):
if len(args) == 3:
show_id = args[0]
start_date = args[1]
status = args[2]
elif len(args) == 4:
show_id = args[0]
start_date = args[1]
status = args[2]
index = args[3]
else:
raise CommandError("you must provide the show_id, start_date, status [index]")
try:
show = Show.objects.get(id=show_id)
except Show.DoesNotExist as dne:
raise CommandError(dne)
try:
start = parse_date(start_date)
except ValueError as ve:
raise CommandError(ve)
else:
year, month, day = start.year, start.month, start.day
try:
timeslot = TimeSlot.objects.get(
show=show, start__year=year, start__month=month, start__day=day
)
except TimeSlot.DoesNotExist as dne:
raise CommandError(dne)
except TimeSlot.MultipleObjectsReturned:
if not index:
raise CommandError("you must provide the show_id, start_date, status index")
try:
timeslot = TimeSlot.objects.filter(
show=show, start__year=year, start__month=month, start__day=day
).order_by("start")[int(index)]
except IndexError as ie:
raise CommandError(ie)
try:
title = sys.stdin.readline().rstrip()
lines = sys.stdin.readlines()
except Exception as e:
raise CommandError(e)
note = Note(timeslot=timeslot, title=title, content="".join(lines), status=status)
try:
note.validate_unique()
except ValidationError as ve:
raise CommandError(ve.messages[0])
else:
note.save()
self.stdout.write(self.style.SUCCESS, f'added note "{title}" to "{timeslot}"')
from django.conf import settings
from django.contrib.auth.models import Group, Permission
from django.core.management.base import BaseCommand
from django.db.models import QuerySet
from django.db.models import Q, QuerySet
PERMISSIONS = {
# Program Managers get all permissions, they also need the edit the permissions
settings.PRIVILEGED_GROUP: {
"all program": Permission.objects.filter(content_type__app_label="program"),
"change user": Permission.objects.filter(codename="change_user"),
},
# Host
settings.ENTITLED_GROUPS[0]: {
"default add/change note & notelink": Permission.objects.filter(
codename__in=[
"add_notelink",
"change_note",
"change_notelink",
],
),
"default change profile": Permission.objects.filter(codename="change_profile"),
"custom add media-source": Permission.objects.filter(
codename__in=[
"add__file",
"add__import",
]
),
"custom edit note": Permission.objects.filter(
~Q(codename="edit__note__topics"),
~Q(codename="edit__note__languages"),
codename__startswith="edit__note",
),
"custom edit profile": Permission.objects.filter(codename="edit__profile__name"),
},
# Host+
settings.ENTITLED_GROUPS[1]: {
"default add/change note & notelink": Permission.objects.filter(
codename__in=[
"add_notelink",
"change_note",
"change_notelink",
],
),
"default change profile, schedule & show": Permission.objects.filter(
codename__in=[
"change_profile",
"change_schedule",
"change_show",
],
),
"custom add media-source": Permission.objects.filter(
codename__in=[
"add__file",
"add__import",
"add__line",
"add__stream",
]
),
"custom edit note": Permission.objects.filter(
~Q(codename="edit__note__topics"),
codename__startswith="edit__note",
),
"custom edit profile": Permission.objects.filter(
codename__in=[
"edit__profile__biography",
"edit__profile__email",
"edit__profile__image",
"edit__profile__links",
"edit__profile__name",
]
),
"custom edit schedule": Permission.objects.filter(
codename="edit__schedule__default_playlist_id"
),
"custom edit show": Permission.objects.filter(
codename__in=[
"edit__show__default_playlist_id",
"edit__show__description",
"edit__show__email",
"edit__show__hosts",
"edit__show__image",
"edit__show__links",
"edit__show__logo",
"edit__show__short_description",
]
),
},
}
class Command(BaseCommand):
......@@ -14,46 +98,8 @@ class Command(BaseCommand):
self.stdout.write(self.style.SUCCESS(str(len(permissions))))
def handle(self, *args, **options):
privileged_group = Group.objects.get(name=settings.PRIVILEGED_GROUP)
host_group = Group.objects.get(name=settings.ENTITLED_GROUPS[0])
host_plus_group = Group.objects.get(name=settings.ENTITLED_GROUPS[1])
app_permissions = Permission.objects.filter(content_type__app_label="program").exclude(
codename__startswith="edit"
)
default_model_permissions = (
Permission.objects.filter(content_type__model__in=["note", "notelink"])
.exclude(codename__startswith="edit")
.exclude(codename__startswith="create")
.exclude(codename__startswith="update")
)
change_permissions = Permission.objects.filter(
codename__startswith="change", content_type__model__in=["host", "note", "show"]
)
edit_permissions = Permission.objects.filter(
codename__startswith="edit", content_type__model__in=["host", "note", "show"]
)
create_permissions = Permission.objects.filter(
codename__startswith="create", content_type__model__in=["note"]
)
update_permissions = Permission.objects.filter(
codename__startswith="update", content_type__model__in=["host", "note", "show"]
)
custom_add_permissions = Permission.objects.filter(
codename__startswith="add__", content_type__model="playlist"
)
self.add_permissions(privileged_group, app_permissions, "default app level")
self.add_permissions(privileged_group, edit_permissions, "custom edit field")
self.add_permissions(privileged_group, create_permissions, "custom create")
self.add_permissions(privileged_group, update_permissions, "custom update")
self.add_permissions(privileged_group, custom_add_permissions, "custom add")
self.add_permissions(host_group, default_model_permissions, "default model")
self.add_permissions(host_plus_group, change_permissions, "default change")
self.add_permissions(host_plus_group, edit_permissions, "custom edit field")
for group_name in PERMISSIONS:
group = Group.objects.get(name=group_name)
self.add_permissions(
host_plus_group, custom_add_permissions.exclude(codename="add__m3ufile"), "custom add"
)
for name, permissions in PERMISSIONS[group_name].items():
self.add_permissions(group, permissions, name)
......@@ -11,14 +11,13 @@ class Command(BaseCommand):
AURA_PROTO = os.getenv("AURA_PROTO")
AURA_HOST = os.getenv("AURA_HOST")
TANK_CALLBACK_BASE_URL = os.getenv(
"TANK_CALLBACK_BASE_URL",
default=f"{AURA_PROTO}://{AURA_HOST}/tank",
)
DASHBOARD_CALLBACK_BASE_URL = os.getenv(
"DASHBOARD_CALLBACK_BASE_URL",
default=f"{AURA_PROTO}://${AURA_HOST}",
)
TANK_CALLBACK_BASE_URL = os.getenv("TANK_CALLBACK_BASE_URL")
if TANK_CALLBACK_BASE_URL == "":
TANK_CALLBACK_BASE_URL = f"{AURA_PROTO}://{AURA_HOST}/tank"
DASHBOARD_CALLBACK_BASE_URL = os.getenv("DASHBOARD_CALLBACK_BASE_URL")
if DASHBOARD_CALLBACK_BASE_URL == "":
DASHBOARD_CALLBACK_BASE_URL = f"{AURA_PROTO}://{AURA_HOST}"
call_command("migrate", "--no-input")
......
import sys
from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist
from django.core.management.base import BaseCommand, CommandError
from program.models import Note, Show
from program.utils import parse_date
class Command(BaseCommand):
help = "updates title and content of a note for a timeslot by reading both from stdin."
def add_arguments(self, parser):
parser.add_argument("show_id", help="ID of the show", type=int)
parser.add_argument("date", help="date of the timeslot", type=str)
parser.add_argument(
"index",
default=None,
help="index of the timeslot within the date. (default: 0)",
nargs="?",
type=int,
)
def handle(self, *args, **options):
try:
show = Show.objects.get(pk=options["show_id"])
except ObjectDoesNotExist as e:
raise CommandError(e)
try:
date = parse_date(options["date"])
except ValueError as e:
raise CommandError(e)
else:
year, month, day = date.year, date.month, date.day
try:
note = Note.objects.get(
timeslot__schedule__show=show,
timeslot__start__day=day,
timeslot__start__month=month,
timeslot__start__year=year,
)
except ObjectDoesNotExist as e:
raise CommandError(e)
except MultipleObjectsReturned:
if not options["index"]:
raise CommandError(f"more than one note within {date}. Please provide an index.")
try:
note = Note.objects.filter(
timeslot__schedule__show=show,
timeslot__start__day=day,
timeslot__start__month=month,
timeslot__start__year=year,
).order_by("timeslot__start")[options["index"]]
except IndexError as e:
raise CommandError(e)
try:
title = sys.stdin.readline().strip()
content = sys.stdin.read()
except Exception as e:
raise CommandError(e)
note.title = title
note.content = content
note.save()
self.stdout.write(self.style.SUCCESS(f'updated note "{title}" for {note.timeslot}'))
# Generated by Django 4.2.13 on 2024-06-11 18:58
import versatileimagefield.fields
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("program", "0095_alter_radiosettings_cba_domains_and_more"),
]
operations = [
migrations.AlterField(
model_name="category",
name="description",
field=models.TextField(blank=True, help_text="Description of the category."),
),
migrations.AlterField(
model_name="category",
name="is_active",
field=models.BooleanField(default=True, help_text="True if category is active."),
),
migrations.AlterField(
model_name="category",
name="name",
field=models.CharField(help_text="Name of the category.", max_length=32),
),
migrations.AlterField(
model_name="category",
name="slug",
field=models.SlugField(help_text="Slug of the category.", max_length=32, unique=True),
),
migrations.AlterField(
model_name="category",
name="subtitle",
field=models.CharField(
blank=True, help_text="Subtitle of the category.", max_length=32
),
),
migrations.AlterField(
model_name="fundingcategory",
name="is_active",
field=models.BooleanField(
default=True, help_text="True if funding category is active."
),
),
migrations.AlterField(
model_name="fundingcategory",
name="name",
field=models.CharField(help_text="Name of the funding category.", max_length=32),
),
migrations.AlterField(
model_name="fundingcategory",
name="slug",
field=models.SlugField(
help_text="Slug of the funding category.", max_length=32, unique=True
),
),
migrations.AlterField(
model_name="host",
name="biography",
field=models.TextField(blank=True, help_text="Biography of the host."),
),
migrations.AlterField(
model_name="host",
name="email",
field=models.EmailField(
blank=True, help_text="Email address of the host.", max_length=254
),
),
migrations.AlterField(
model_name="host",
name="is_active",
field=models.BooleanField(default=True, help_text="True if host is active."),
),
migrations.AlterField(
model_name="host",
name="name",
field=models.CharField(help_text="Display name of the host.", max_length=128),
),
migrations.AlterField(
model_name="host",
name="owners",
field=models.ManyToManyField(
blank=True,
help_text="User ID(s) identifying this host.",
related_name="hosts",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AlterField(
model_name="image",
name="alt_text",
field=models.TextField(
blank=True, default="", help_text="Alternate text for the image."
),
),
migrations.AlterField(
model_name="image",
name="credits",
field=models.TextField(blank=True, default="", help_text="Credits of the image"),
),
migrations.AlterField(
model_name="image",
name="image",
field=versatileimagefield.fields.VersatileImageField(
blank=True,
height_field="height",
help_text="The URI of the image.",
null=True,
upload_to="images",
width_field="width",
),
),
migrations.AlterField(
model_name="image",
name="is_use_explicitly_granted_by_author",
field=models.BooleanField(
default=False, help_text="True if use is explicitly granted by author."
),
),
migrations.AlterField(
model_name="language",
name="is_active",
field=models.BooleanField(default=True, help_text="True if language is active."),
),
migrations.AlterField(
model_name="language",
name="name",
field=models.CharField(help_text="Name of the language.", max_length=32),
),
migrations.AlterField(
model_name="license",
name="needs_author",
field=models.BooleanField(default=True, help_text="True if license needs an author."),
),
migrations.AlterField(
model_name="license",
name="requires_express_permission_for_publication",
field=models.BooleanField(
default=True, help_text="True if express permission for publication required."
),
),
migrations.AlterField(
model_name="license",
name="url",
field=models.URLField(blank=True, default="", help_text="URL of the licence."),
),
migrations.AlterField(
model_name="linktype",
name="is_active",
field=models.BooleanField(default=True, help_text="True if link type is active."),
),
migrations.AlterField(
model_name="musicfocus",
name="is_active",
field=models.BooleanField(default=True, help_text="True if music focus is active."),
),
migrations.AlterField(
model_name="musicfocus",
name="name",
field=models.CharField(help_text="Name of the music focus.", max_length=32),
),
migrations.AlterField(
model_name="musicfocus",
name="slug",
field=models.SlugField(
help_text="Slug of the music focus.", max_length=32, unique=True
),
),
migrations.AlterField(
model_name="note",
name="cba_id",
field=models.IntegerField(blank=True, help_text="CBA entry ID.", null=True),
),
migrations.AlterField(
model_name="note",
name="content",
field=models.TextField(help_text="Textual content of the note."),
),
migrations.AlterField(
model_name="note",
name="contributors",
field=models.ManyToManyField(
help_text="`Host` IDs contributing to this note.",
related_name="notes",
to="program.host",
),
),
migrations.AlterField(
model_name="note",
name="summary",
field=models.TextField(blank=True, help_text="Summary of the Note."),
),
migrations.AlterField(
model_name="note",
name="title",
field=models.CharField(
blank=True, default="", help_text="Title of the note.", max_length=128
),
),
migrations.AlterField(
model_name="show",
name="description",
field=models.TextField(blank=True, help_text="Description of this show."),
),
migrations.AlterField(
model_name="show",
name="email",
field=models.EmailField(
blank=True, help_text="Email address of this show.", max_length=254, null=True
),
),
migrations.AlterField(
model_name="show",
name="internal_note",
field=models.TextField(blank=True, help_text="Internal note for this show."),
),
migrations.AlterField(
model_name="show",
name="is_active",
field=models.BooleanField(default=True, help_text="True if this show is active."),
),
migrations.AlterField(
model_name="show",
name="is_public",
field=models.BooleanField(default=False, help_text="True if this show is public."),
),
migrations.AlterField(
model_name="show",
name="name",
field=models.CharField(help_text="Name of this Show.", max_length=255),
),
migrations.AlterField(
model_name="show",
name="short_description",
field=models.TextField(help_text="Short description of this show."),
),
migrations.AlterField(
model_name="show",
name="slug",
field=models.SlugField(
blank=True, help_text="Slug of this show.", max_length=255, unique=True
),
),
migrations.AlterField(
model_name="timeslot",
name="memo",
field=models.TextField(blank=True, help_text="Memo for this timeslot."),
),
migrations.AlterField(
model_name="timeslot",
name="playlist_id",
field=models.IntegerField(help_text="`Playlist` ID of this timeslot.", null=True),
),
migrations.AlterField(
model_name="topic",
name="is_active",
field=models.BooleanField(default=True, help_text="True if topic is active."),
),
migrations.AlterField(
model_name="topic",
name="name",
field=models.CharField(help_text="Name of the topic.", max_length=32),
),
migrations.AlterField(
model_name="topic",
name="slug",
field=models.SlugField(help_text="Slug of the topic.", max_length=32, unique=True),
),
migrations.AlterField(
model_name="type",
name="is_active",
field=models.BooleanField(default=True, help_text="True if type is active."),
),
migrations.AlterField(
model_name="type",
name="name",
field=models.CharField(help_text="Name of the type.", max_length=32),
),
migrations.AlterField(
model_name="type",
name="slug",
field=models.SlugField(help_text="Slug of the type.", max_length=32, unique=True),
),
]
# Generated by Django 4.2.13 on 2024-06-11 19:02
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("program", "0096_alter_category_description_alter_category_is_active_and_more"),
]
operations = [
migrations.AlterModelOptions(
name="host",
options={
"ordering": ("name",),
"permissions": [
("edit__host__biography", "Can edit biography field"),
("edit__host__email", "Can edit email field"),
("edit__host__image", "Can edit image field"),
("edit__host__name", "Can edit name field"),
("edit__host__owners", "Can edit owners field"),
("update_host", "Can update host"),
],
},
),
]
# Generated by Django 4.2.13 on 2024-06-11 19:08
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("program", "0097_alter_host_options"),
]
operations = [
migrations.AlterModelOptions(
name="userprofile",
options={
"permissions": [
("edit__user_profile__cba_username", "Can edit CBA username field"),
("edit__user_profile__cba_user_token", "Can edit CBA user token field"),
]
},
),
]
# Generated by Django 4.2.13 on 2024-06-11 19:16
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("program", "0098_alter_userprofile_options"),
]
operations = [
migrations.AlterModelOptions(
name="show",
options={
"ordering": ("slug",),
"permissions": [
("display__show__internal_note", "Can display internal note field"),
("edit__show__categories", "Can edit category field"),
("edit__show__cba_series_id", "Can edit cba series id field"),
("edit__show__default_playlist", "Can edit default playlist field"),
("edit__show__description", "Can edit description field"),
("edit__show__email", "Can edit email field"),
("edit__show__funding_categories", "Can edit funding category field"),
("edit__show__hosts", "Can edit hosts field"),
("edit__show__image", "Can edit image field"),
("edit__show__internal_note", "Can edit internal note field"),
("edit__show__is_active", "Can edit is active field"),
("edit__show__languages", "Can edit language field"),
("edit__show__links", "Can edit links field"),
("edit__show__logo", "Can edit logo field"),
("edit__show__music_focuses", "Can edit music focus field"),
("edit__show__name", "Can edit name field"),
("edit__show__owners", "Can edit owners field"),
("edit__show__predecessor", "Can edit predecessor field"),
("edit__show__short_description", "Can edit short description field"),
("edit__show__slug", "Can edit slug field"),
("edit__show__topics", "Can edit topic field"),
("edit__show__type", "Can edit type field"),
("update_show", "Can update show"),
],
},
),
]
# Generated by Django 4.2.13 on 2024-06-19 23:44
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("program", "0099_alter_show_options"),
]
operations = [
migrations.AlterModelOptions(
name="userprofile",
options={
"permissions": [
("create_user_profile", "Can create user profile"),
("update_user_profile", "Can update user profile"),
]
},
),
]
# Generated by Django 4.2.13 on 2024-06-24 21:31
import program.models
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("program", "0100_alter_userprofile_options"),
]
operations = [
migrations.AlterField(
model_name="radiosettings",
name="host_image_aspect_ratio",
field=program.models.ImageAspectRadioField(default="1:1", max_length=11),
),
migrations.AlterField(
model_name="radiosettings",
name="note_image_aspect_ratio",
field=program.models.ImageAspectRadioField(default="16:9", max_length=11),
),
migrations.AlterField(
model_name="radiosettings",
name="show_image_aspect_ratio",
field=program.models.ImageAspectRadioField(default="16:9", max_length=11),
),
migrations.AlterField(
model_name="radiosettings",
name="show_logo_aspect_ratio",
field=program.models.ImageAspectRadioField(default="1:1", max_length=11),
),
]
# Generated by Django 4.2.13 on 2024-06-25 19:31
import program.models
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("program", "0101_alter_radiosettings_host_image_aspect_ratio_and_more"),
]
operations = [
migrations.AddField(
model_name="radiosettings",
name="fallback_pools",
field=models.JSONField(
blank=True,
default=dict,
help_text="JSON key/value pairs",
validators=[program.models.validate_fallback_pools],
),
),
]