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> ...@@ -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> <jackie@o94.at>
jackie / Andrea Ida Malkah Klaura <jackie@diebin.at> Andrea Ida Malkah Klaura jackie / Andrea Ida Malkah Klaura <jackie@diebin.at> Andrea Ida Malkah Klaura
Roman Brendler <roman@jointech.org> Roman <roman@jointech.org> Roman Brendler <roman@jointech.org> Roman <roman@jointech.org>
EorlBruder <david@jointech.org> <eorl@bruder.space> EorlBruder <david@jointech.org> <eorl@bruder.space>
\ No newline at end of file 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 ...@@ -7,9 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
### Added
- `basic` and `calendar` program routes. (steering#239)
### Changed ### Changed
- Changed the aura user and group ID from 2872 to 872. - 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 ## [1.0.0-alpha4] - 2024-04-17
......
...@@ -6,14 +6,16 @@ from rest_framework.test import APIClient ...@@ -6,14 +6,16 @@ from rest_framework.test import APIClient
from django.contrib.auth.models import Permission, User from django.contrib.auth.models import Permission, User
from django.core.files.uploadedfile import SimpleUploadedFile from django.core.files.uploadedfile import SimpleUploadedFile
from program.models import ( from program.models import (
CBA,
Category, Category,
FundingCategory, FundingCategory,
Host,
Image, Image,
Language, Language,
License, License,
LinkType, LinkType,
MusicFocus, MusicFocus,
Profile,
RadioSettings,
RRule, RRule,
Schedule, Schedule,
Show, Show,
...@@ -23,15 +25,17 @@ from program.models import ( ...@@ -23,15 +25,17 @@ from program.models import (
) )
from program.tests.factories import ( from program.tests.factories import (
CategoryFactory, CategoryFactory,
CBAFactory,
CommonUserFactory, CommonUserFactory,
FundingCategoryFactory, FundingCategoryFactory,
HostFactory,
ImageFactory, ImageFactory,
LanguageFactory, LanguageFactory,
LicenseFactory, LicenseFactory,
LinkTypeFactory, LinkTypeFactory,
MusicFocusFactory, MusicFocusFactory,
OwnerFactory, OwnerFactory,
ProfileFactory,
RadioSettingsFactory,
RRuleFactory, RRuleFactory,
ScheduleFactory, ScheduleFactory,
ShowFactory, ShowFactory,
...@@ -55,9 +59,30 @@ def assert_data(response, data) -> None: ...@@ -55,9 +59,30 @@ def assert_data(response, data) -> None:
assert response.data[key] == value 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 @pytest.fixture
def host() -> Host: def profile() -> Profile:
return HostFactory() return ProfileFactory()
@pytest.fixture @pytest.fixture
...@@ -154,11 +179,36 @@ def once_rrule() -> RRule: ...@@ -154,11 +179,36 @@ def once_rrule() -> RRule:
return RRuleFactory(freq=0) return RRuleFactory(freq=0)
@pytest.fixture
def daily_rrule() -> RRule:
return RRuleFactory(freq=3)
@pytest.fixture @pytest.fixture
def show() -> Show: def show() -> Show:
return ShowFactory() 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 @pytest.fixture
def owned_show(common_user1, show) -> Show: def owned_show(common_user1, show) -> Show:
"""Show owned by a common user""" """Show owned by a common user"""
......
[ [
{ {
"model": "program.radiosettings", "model": "program.radiosettings",
"pk": 1, "pk": 1,
"fields": { "fields": {
"cba_api_key": "", "cba_api_key": "",
"cba_domains": [ "cba_domains": ["cba.media"],
"cba.media" "fallback_default_pool": "fallback",
], "fallback_show": null,
"fallback_default_pool": "", "profile_image_aspect_ratio": "1:1",
"fallback_show": null, "profile_image_shape": "round",
"host_image_aspect_ratio": "1:1", "line_in_channels": {"0": "live", "1": "preprod"},
"host_image_shape": "round", "micro_show": null,
"line_in_channels": { "note_image_aspect_ratio": "16:9",
"0": "live", "pools": {"fallback": "Station Fallback Pool"},
"1": "preprod" "note_image_shape": "rect",
}, "show_image_aspect_ratio": "16:9",
"micro_show": null, "show_image_shape": "rect",
"note_image_aspect_ratio": "16:9", "show_logo_aspect_ratio": "1:1",
"note_image_shape": "rect", "show_logo_shape": "rect",
"show_image_aspect_ratio": "16:9", "station_logo": null,
"show_image_shape": "rect", "station_name": "Radio AURA",
"show_logo_aspect_ratio": "1:1", "station_website": "https://aura.radio"
"show_logo_shape": "rect", }
"station_logo": null,
"station_name": "Radio AURA",
"station_website": "https://aura.radio"
} }
}
] ]
[ [
{ {
"model": "program.host", "model": "program.profile",
"pk": 1, "pk": 1,
"fields": { "fields": {
"name": "Musikredaktion", "name": "Musikredaktion",
......
This diff is collapsed.
from django_json_widget.widgets import JSONEditorWidget from django_json_widget.widgets import JSONEditorWidget
from django.conf import settings
from django.contrib import admin from django.contrib import admin
from django.contrib.auth.admin import UserAdmin from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.db.models import JSONField from django.db.models import JSONField
from django.utils.safestring import mark_safe
from program.models import ( from program.models import (
CBA,
Category, Category,
FundingCategory, FundingCategory,
Host,
Language, Language,
License, License,
LinkType, LinkType,
MusicFocus, MusicFocus,
Profile,
RadioSettings, RadioSettings,
RRule, RRule,
Topic, Topic,
Type, Type,
UserProfile,
) )
...@@ -36,8 +38,8 @@ class LicenseAdmin(admin.ModelAdmin): ...@@ -36,8 +38,8 @@ class LicenseAdmin(admin.ModelAdmin):
list_display = ("name", "identifier") list_display = ("name", "identifier")
@admin.register(Host) @admin.register(Profile)
class HostAdmin(admin.ModelAdmin): class ProfileAdmin(admin.ModelAdmin):
fields = ("name", "email", "biography", "created_at", "created_by", "updated_at", "updated_by") fields = ("name", "email", "biography", "created_at", "created_by", "updated_at", "updated_by")
list_display = ("name",) list_display = ("name",)
readonly_fields = ("created_at", "created_by", "updated_at", "updated_by") readonly_fields = ("created_at", "created_by", "updated_at", "updated_by")
...@@ -56,19 +58,20 @@ class RRuleAdmin(admin.ModelAdmin): ...@@ -56,19 +58,20 @@ class RRuleAdmin(admin.ModelAdmin):
list_display = ("name", "freq", "interval", "by_set_pos", "by_weekdays", "count") list_display = ("name", "freq", "interval", "by_set_pos", "by_weekdays", "count")
class UserProfileInline(admin.StackedInline): class CBAInline(admin.StackedInline):
model = UserProfile model = CBA
fields = ("cba_username", "cba_user_token") fields = ("username", "user_token")
can_delete = False can_delete = False
verbose_name_plural = "Profile" verbose_name = "CBA"
verbose_name_plural = "CBA"
fk_name = "user" fk_name = "user"
class UserProfileUserAdmin(UserAdmin): class UserCBAAdmin(UserAdmin):
inlines = (UserProfileInline,) inlines = (CBAInline,)
def get_queryset(self, request): 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: if not request.user.is_superuser:
return super(UserAdmin, self).get_queryset(request).filter(pk=request.user.id) return super(UserAdmin, self).get_queryset(request).filter(pk=request.user.id)
...@@ -90,38 +93,45 @@ class UserProfileUserAdmin(UserAdmin): ...@@ -90,38 +93,45 @@ class UserProfileUserAdmin(UserAdmin):
return list() return list()
def get_inline_instances(self, request, obj=None): def get_inline_instances(self, request, obj=None):
"""Append profile fields to UserAdmin""" """Append CBA fields to UserAdmin"""
if not obj: if not obj:
return list() 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.unregister(User)
admin.site.register(User, UserProfileUserAdmin) admin.site.register(User, UserCBAAdmin)
@admin.register(RadioSettings) @admin.register(RadioSettings)
class RadioSettingsAdmin(admin.ModelAdmin): class RadioSettingsAdmin(admin.ModelAdmin):
fieldsets = [ fieldsets = [
(None, {"fields": ["station_name", "station_website", "station_logo"]}), (
None,
{
"fields": [
"station_name",
"station_website",
"station_logo",
"station_logo_preview",
]
},
),
( (
"Image requirements", "Image requirements",
{ {
"fields": [ "fields": [
("host_image_aspect_ratio", "host_image_shape"), ("note_image_aspect_ratio", "note_image_shape"),
( ("profile_image_aspect_ratio", "profile_image_shape"),
"note_image_aspect_ratio",
"note_image_shape",
),
("show_image_aspect_ratio", "show_image_shape"), ("show_image_aspect_ratio", "show_image_shape"),
("show_logo_aspect_ratio", "show_logo_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"]}), ("CBA", {"fields": ["cba_api_key", "cba_domains"]}),
("Playout", {"fields": ["line_in_channels"]}), ("Playout", {"fields": ["line_in_channels", "pools"]}),
] ]
formfield_overrides = { formfield_overrides = {
JSONField: { JSONField: {
...@@ -135,3 +145,14 @@ class RadioSettingsAdmin(admin.ModelAdmin): ...@@ -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 ...@@ -8,6 +8,7 @@ from django import forms
from django.db.models import Exists, OuterRef, QuerySet from django.db.models import Exists, OuterRef, QuerySet
from django.utils import timezone from django.utils import timezone
from program import models from program import models
from program.services import generate_program_entries
class StaticFilterHelpTextMixin: class StaticFilterHelpTextMixin:
...@@ -56,6 +57,10 @@ class ShowOrderingFilter(filters.OrderingFilter): ...@@ -56,6 +57,10 @@ class ShowOrderingFilter(filters.OrderingFilter):
class ShowFilterSet(StaticFilterHelpTextMixin, filters.FilterSet): class ShowFilterSet(StaticFilterHelpTextMixin, filters.FilterSet):
ids = IntegerInFilter(
field_name="id",
help_text="Return only shows matching the specified id(s).",
)
order = ShowOrderingFilter( order = ShowOrderingFilter(
fields=["name", "slug", "id", "is_active", "is_owner", "updated_at", "updated_by"], fields=["name", "slug", "id", "is_active", "is_owner", "updated_at", "updated_by"],
help_text="Order shows by the given field(s).", help_text="Order shows by the given field(s).",
...@@ -70,7 +75,7 @@ class ShowFilterSet(StaticFilterHelpTextMixin, filters.FilterSet): ...@@ -70,7 +75,7 @@ class ShowFilterSet(StaticFilterHelpTextMixin, filters.FilterSet):
) )
host_ids = IntegerInFilter( host_ids = IntegerInFilter(
field_name="hosts", 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( is_active = filters.BooleanFilter(
field_name="is_active", field_name="is_active",
...@@ -120,7 +125,7 @@ class ShowFilterSet(StaticFilterHelpTextMixin, filters.FilterSet): ...@@ -120,7 +125,7 @@ class ShowFilterSet(StaticFilterHelpTextMixin, filters.FilterSet):
def filter_writable(self, queryset: QuerySet, _: str, value: bool) -> QuerySet: def filter_writable(self, queryset: QuerySet, _: str, value: bool) -> QuerySet:
user = self.request.user if self.request.user.is_authenticated else None 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 return queryset
elif value and user: elif value and user:
return queryset.filter(owners=user) return queryset.filter(owners=user)
...@@ -130,6 +135,7 @@ class ShowFilterSet(StaticFilterHelpTextMixin, filters.FilterSet): ...@@ -130,6 +135,7 @@ class ShowFilterSet(StaticFilterHelpTextMixin, filters.FilterSet):
class Meta: class Meta:
model = models.Show model = models.Show
fields = [ fields = [
"ids",
"order", "order",
"category_ids", "category_ids",
"category_slug", "category_slug",
...@@ -149,6 +155,10 @@ class ShowFilterSet(StaticFilterHelpTextMixin, filters.FilterSet): ...@@ -149,6 +155,10 @@ class ShowFilterSet(StaticFilterHelpTextMixin, filters.FilterSet):
class ScheduleFilterSet(filters.FilterSet): class ScheduleFilterSet(filters.FilterSet):
ids = IntegerInFilter(
field_name="id",
help_text="Return only schedules matching the specified id(s).",
)
show_ids = IntegerInFilter( show_ids = IntegerInFilter(
field_name="show", field_name="show",
help_text="Return only schedules that belong to the specified show(s).", help_text="Return only schedules that belong to the specified show(s).",
...@@ -170,6 +180,10 @@ class ScheduleFilterSet(filters.FilterSet): ...@@ -170,6 +180,10 @@ class ScheduleFilterSet(filters.FilterSet):
class TimeSlotFilterSet(filters.FilterSet): class TimeSlotFilterSet(filters.FilterSet):
ids = IntegerInFilter(
field_name="id",
help_text="Return only timeslots matching the specified id(s).",
)
order = filters.OrderingFilter( order = filters.OrderingFilter(
fields=[field.name for field in models.TimeSlot._meta.get_fields()] fields=[field.name for field in models.TimeSlot._meta.get_fields()]
) )
...@@ -236,6 +250,7 @@ class TimeSlotFilterSet(filters.FilterSet): ...@@ -236,6 +250,7 @@ class TimeSlotFilterSet(filters.FilterSet):
class Meta: class Meta:
model = models.TimeSlot model = models.TimeSlot
fields = [ fields = [
"ids",
"order", "order",
"start", "start",
"end", "end",
...@@ -282,23 +297,40 @@ class ActiveFilterSet(StaticFilterHelpTextMixin, filters.FilterSet): ...@@ -282,23 +297,40 @@ class ActiveFilterSet(StaticFilterHelpTextMixin, filters.FilterSet):
] ]
class PlayoutFilterSet(filters.FilterSet): class VirtualTimeslotFilterSet(filters.FilterSet):
start = filters.DateFilter( start = filters.IsoDateTimeFilter(method="filter_noop")
field_name="start", end = filters.IsoDateTimeFilter(method="filter_noop")
lookup_expr="gte", cut_at_range_boundaries = filters.BooleanFilter(
help_text="Returns timeslots that start at or after the specified datetime " help_text=(
"(default: today).", "If true guarantees that the first and last program entry match the requested range "
) "even if these entries earlier or end later."
end = filters.DateFilter( ),
field_name="end", method="filter_noop",
lookup_expr="lte",
help_text="Returns timeslots that end at or before the specified datetime "
"(default: one week after start date).",
) )
include_virtual = filters.BooleanFilter( 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: class Meta:
model = models.TimeSlot model = models.TimeSlot
fields = ["start", "end", "include_virtual"] 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.conf import settings
from django.contrib.auth.models import Group, Permission from django.contrib.auth.models import Group, Permission
from django.core.management.base import BaseCommand 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): class Command(BaseCommand):
...@@ -14,46 +98,8 @@ class Command(BaseCommand): ...@@ -14,46 +98,8 @@ class Command(BaseCommand):
self.stdout.write(self.style.SUCCESS(str(len(permissions)))) self.stdout.write(self.style.SUCCESS(str(len(permissions))))
def handle(self, *args, **options): def handle(self, *args, **options):
privileged_group = Group.objects.get(name=settings.PRIVILEGED_GROUP) for group_name in PERMISSIONS:
host_group = Group.objects.get(name=settings.ENTITLED_GROUPS[0]) group = Group.objects.get(name=group_name)
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")
self.add_permissions( for name, permissions in PERMISSIONS[group_name].items():
host_plus_group, custom_add_permissions.exclude(codename="add__m3ufile"), "custom add" self.add_permissions(group, permissions, name)
)
...@@ -11,14 +11,13 @@ class Command(BaseCommand): ...@@ -11,14 +11,13 @@ class Command(BaseCommand):
AURA_PROTO = os.getenv("AURA_PROTO") AURA_PROTO = os.getenv("AURA_PROTO")
AURA_HOST = os.getenv("AURA_HOST") AURA_HOST = os.getenv("AURA_HOST")
TANK_CALLBACK_BASE_URL = os.getenv( TANK_CALLBACK_BASE_URL = os.getenv("TANK_CALLBACK_BASE_URL")
"TANK_CALLBACK_BASE_URL", if TANK_CALLBACK_BASE_URL == "":
default=f"{AURA_PROTO}://{AURA_HOST}/tank", TANK_CALLBACK_BASE_URL = f"{AURA_PROTO}://{AURA_HOST}/tank"
)
DASHBOARD_CALLBACK_BASE_URL = os.getenv( DASHBOARD_CALLBACK_BASE_URL = os.getenv("DASHBOARD_CALLBACK_BASE_URL")
"DASHBOARD_CALLBACK_BASE_URL", if DASHBOARD_CALLBACK_BASE_URL == "":
default=f"{AURA_PROTO}://${AURA_HOST}", DASHBOARD_CALLBACK_BASE_URL = f"{AURA_PROTO}://{AURA_HOST}"
)
call_command("migrate", "--no-input") 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],
),
),
]