diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000000000000000000000000000000000000..e46c3310bf31324d76871fe8234d9656473e13bb --- /dev/null +++ b/.flake8 @@ -0,0 +1,4 @@ +[flake8] +max-line-length = 99 +ignore = E121,E123,E126,E203,E226,E24,E704,W503,N802 + diff --git a/.isort.cfg b/.isort.cfg new file mode 100644 index 0000000000000000000000000000000000000000..c6548d6c0e353efe990fa510cd4d25a5a4ff7281 --- /dev/null +++ b/.isort.cfg @@ -0,0 +1,5 @@ +[settings] +profile = black +default_section = THIRDPARTY +known_first_party = django + diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000000000000000000000000000000000000..13fbf401edb7ff01725f38984a5a2736b829f5cd --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,13 @@ +repos: + - repo: https://github.com/psf/black + rev: 22.1.0 + hooks: + - id: black + - repo: https://github.com/PyCQA/isort + rev: 5.9.3 + hooks: + - id: isort + - repo: https://github.com/PyCQA/flake8 + rev: 4.0.1 + hooks: + - id: flake8 diff --git a/CHANGELOG b/CHANGELOG new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/CHANGELOG @@ -0,0 +1 @@ + diff --git a/profile/admin.py b/profile/admin.py index 909b824e8da36a26f49d9d33e7ba56dfa09f44cc..0a93c66103e1e3aec33629c013b9597d77cb5c86 100644 --- a/profile/admin.py +++ b/profile/admin.py @@ -27,8 +27,8 @@ from .models import Profile class ProfileInline(admin.StackedInline): model = Profile can_delete = False - verbose_name_plural = 'Profile' - fk_name = 'user' + verbose_name_plural = "Profile" + fk_name = "user" class ProfileUserAdmin(UserAdmin): @@ -37,7 +37,9 @@ class ProfileUserAdmin(UserAdmin): def get_queryset(self, request): """Let common users only edit their own profile""" 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) + ) return super(UserAdmin, self).get_queryset(request) @@ -45,7 +47,15 @@ class ProfileUserAdmin(UserAdmin): """Limit field access for common users""" if not request.user.is_superuser: return ( - 'username', 'is_staff', 'is_superuser', 'is_active', 'date_joined', 'last_login', 'groups', 'user_permissions') + "username", + "is_staff", + "is_superuser", + "is_active", + "date_joined", + "last_login", + "groups", + "user_permissions", + ) return list() def get_inline_instances(self, request, obj=None): diff --git a/profile/migrations/0001_initial.py b/profile/migrations/0001_initial.py index e55dd0d75a6ea3c9e628c1a0add066056d091880..ad7c45b8559e5f0f484406bfd52958667e21d2cc 100644 --- a/profile/migrations/0001_initial.py +++ b/profile/migrations/0001_initial.py @@ -2,10 +2,11 @@ # Generated by Django 1.11.3 on 2017-11-09 18:42 from __future__ import unicode_literals +import versatileimagefield.fields + +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion -import versatileimagefield.fields class Migration(migrations.Migration): @@ -18,28 +19,104 @@ class Migration(migrations.Migration): operations = [ migrations.CreateModel( - name='Profile', + name="Profile", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('biography', models.TextField(blank=True, null=True, verbose_name='Biography')), - ('website', models.URLField(blank=True, verbose_name='Website')), - ('googleplus_url', models.URLField(blank=True, verbose_name='Google+ URL')), - ('facebook_url', models.URLField(blank=True, verbose_name='Facebook URL')), - ('twitter_url', models.URLField(blank=True, verbose_name='Twitter URL')), - ('linkedin_url', models.URLField(blank=True, verbose_name='LinkedIn URL')), - ('youtube_url', models.URLField(blank=True, verbose_name='Youtube URL')), - ('dorftv_url', models.URLField(blank=True, verbose_name='DorfTV URL')), - ('cba_url', models.URLField(blank=True, verbose_name='CBA URL')), - ('cba_username', models.CharField(blank=True, max_length=60, verbose_name='CBA Username')), - ('cba_user_token', models.CharField(blank=True, max_length=255, verbose_name='CBA Token')), - ('ppoi', versatileimagefield.fields.PPOIField(default='0.5x0.5', editable=False, max_length=20, verbose_name='Image PPOI')), - ('height', models.PositiveIntegerField(blank=True, editable=False, null=True, verbose_name='Image Height')), - ('width', models.PositiveIntegerField(blank=True, editable=False, null=True, verbose_name='Image Width')), - ('image', versatileimagefield.fields.VersatileImageField(blank=True, height_field='height', null=True, upload_to='user_images', verbose_name='Profile picture', width_field='width')), - ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "biography", + models.TextField(blank=True, null=True, verbose_name="Biography"), + ), + ("website", models.URLField(blank=True, verbose_name="Website")), + ( + "googleplus_url", + models.URLField(blank=True, verbose_name="Google+ URL"), + ), + ( + "facebook_url", + models.URLField(blank=True, verbose_name="Facebook URL"), + ), + ( + "twitter_url", + models.URLField(blank=True, verbose_name="Twitter URL"), + ), + ( + "linkedin_url", + models.URLField(blank=True, verbose_name="LinkedIn URL"), + ), + ( + "youtube_url", + models.URLField(blank=True, verbose_name="Youtube URL"), + ), + ("dorftv_url", models.URLField(blank=True, verbose_name="DorfTV URL")), + ("cba_url", models.URLField(blank=True, verbose_name="CBA URL")), + ( + "cba_username", + models.CharField( + blank=True, max_length=60, verbose_name="CBA Username" + ), + ), + ( + "cba_user_token", + models.CharField( + blank=True, max_length=255, verbose_name="CBA Token" + ), + ), + ( + "ppoi", + versatileimagefield.fields.PPOIField( + default="0.5x0.5", + editable=False, + max_length=20, + verbose_name="Image PPOI", + ), + ), + ( + "height", + models.PositiveIntegerField( + blank=True, + editable=False, + null=True, + verbose_name="Image Height", + ), + ), + ( + "width", + models.PositiveIntegerField( + blank=True, + editable=False, + null=True, + verbose_name="Image Width", + ), + ), + ( + "image", + versatileimagefield.fields.VersatileImageField( + blank=True, + height_field="height", + null=True, + upload_to="user_images", + verbose_name="Profile picture", + width_field="width", + ), + ), + ( + "user", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), ], options={ - 'db_table': 'profile', + "db_table": "profile", }, ), ] diff --git a/profile/migrations/0001_squashed.py b/profile/migrations/0001_squashed.py index e0e3632cfc0b65106bc182cc368695102da9a7e7..1313caa100beedfb221a769bf81943e0f62e0541 100644 --- a/profile/migrations/0001_squashed.py +++ b/profile/migrations/0001_squashed.py @@ -1,13 +1,17 @@ # Generated by Django 2.2.12 on 2020-11-21 01:34 +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion class Migration(migrations.Migration): - replaces = [('profile', '0001_initial'), ('profile', '0002_auto_20171129_1828'), ('profile', '0003_auto_20171213_1737')] + replaces = [ + ("profile", "0001_initial"), + ("profile", "0002_auto_20171129_1828"), + ("profile", "0003_auto_20171213_1737"), + ] initial = True @@ -17,15 +21,49 @@ class Migration(migrations.Migration): operations = [ migrations.CreateModel( - name='Profile', + name="Profile", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('cba_username', models.CharField(blank=True, help_text='Your username in CBA. This is necessary for uploading files to your account.', max_length=60, verbose_name='CBA Username')), - ('cba_user_token', models.CharField(blank=True, help_text='The CBA upload token for your account. This is NOT your password which you use to log into CBA!', max_length=255, verbose_name='CBA Token')), - ('user', models.OneToOneField(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "cba_username", + models.CharField( + blank=True, + help_text="Your username in CBA. This is necessary for uploading files to" + " your account.", + max_length=60, + verbose_name="CBA Username", + ), + ), + ( + "cba_user_token", + models.CharField( + blank=True, + help_text="The CBA upload token for your account. This is NOT your" + " password which you use to log into CBA!", + max_length=255, + verbose_name="CBA Token", + ), + ), + ( + "user", + models.OneToOneField( + editable=False, + on_delete=django.db.models.deletion.CASCADE, + related_name="profile", + to=settings.AUTH_USER_MODEL, + ), + ), ], options={ - 'db_table': 'profile', + "db_table": "profile", }, ), ] diff --git a/profile/migrations/0002_auto_20171129_1828.py b/profile/migrations/0002_auto_20171129_1828.py index 77147dda7df704cf6c9854eb4473f0198e736b4a..389554246f612dd8ce7674c8f2e7722fac746b21 100644 --- a/profile/migrations/0002_auto_20171129_1828.py +++ b/profile/migrations/0002_auto_20171129_1828.py @@ -2,82 +2,145 @@ # Generated by Django 1.11.3 on 2017-11-29 18:28 from __future__ import unicode_literals +import versatileimagefield.fields + +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion -import versatileimagefield.fields class Migration(migrations.Migration): dependencies = [ - ('profile', '0001_initial'), + ("profile", "0001_initial"), ] operations = [ migrations.AlterField( - model_name='profile', - name='biography', - field=models.TextField(blank=True, help_text='Describe yourself and your fields of interest in a few sentences.', null=True, verbose_name='Biography'), + model_name="profile", + name="biography", + field=models.TextField( + blank=True, + help_text="Describe yourself and your fields of interest in a few sentences.", + null=True, + verbose_name="Biography", + ), ), migrations.AlterField( - model_name='profile', - name='cba_url', - field=models.URLField(blank=True, help_text='URL to your CBA profile.', verbose_name='CBA URL'), + model_name="profile", + name="cba_url", + field=models.URLField( + blank=True, help_text="URL to your CBA profile.", verbose_name="CBA URL" + ), ), migrations.AlterField( - model_name='profile', - name='cba_user_token', - field=models.CharField(blank=True, help_text='The CBA upload token for your account. This is NOT your password which you use to log into CBA!', max_length=255, verbose_name='CBA Token'), + model_name="profile", + name="cba_user_token", + field=models.CharField( + blank=True, + help_text="The CBA upload token for your account. This is NOT your password which" + " you use to log into CBA!", + max_length=255, + verbose_name="CBA Token", + ), ), migrations.AlterField( - model_name='profile', - name='cba_username', - field=models.CharField(blank=True, help_text='Your username in CBA. This is necessary for uploading files to your account.', max_length=60, verbose_name='CBA Username'), + model_name="profile", + name="cba_username", + field=models.CharField( + blank=True, + help_text="Your username in CBA. This is necessary for uploading files to your" + " account.", + max_length=60, + verbose_name="CBA Username", + ), ), migrations.AlterField( - model_name='profile', - name='dorftv_url', - field=models.URLField(blank=True, help_text='URL to your dorfTV channel.', verbose_name='DorfTV URL'), + model_name="profile", + name="dorftv_url", + field=models.URLField( + blank=True, + help_text="URL to your dorfTV channel.", + verbose_name="DorfTV URL", + ), ), migrations.AlterField( - model_name='profile', - name='facebook_url', - field=models.URLField(blank=True, help_text='URL to your Facebook profile.', verbose_name='Facebook URL'), + model_name="profile", + name="facebook_url", + field=models.URLField( + blank=True, + help_text="URL to your Facebook profile.", + verbose_name="Facebook URL", + ), ), migrations.AlterField( - model_name='profile', - name='googleplus_url', - field=models.URLField(blank=True, help_text='URL to your Google+ profile.', verbose_name='Google+ URL'), + model_name="profile", + name="googleplus_url", + field=models.URLField( + blank=True, + help_text="URL to your Google+ profile.", + verbose_name="Google+ URL", + ), ), migrations.AlterField( - model_name='profile', - name='image', - field=versatileimagefield.fields.VersatileImageField(blank=True, height_field='height', help_text="Upload a picture of yourself. Images are automatically cropped around the 'Primary Point of Interest'. Click in the image to change it and press Save.", null=True, upload_to='user_images', verbose_name='Profile picture', width_field='width'), + model_name="profile", + name="image", + field=versatileimagefield.fields.VersatileImageField( + blank=True, + height_field="height", + help_text="Upload a picture of yourself. Images are automatically cropped around" + " the 'Primary Point of Interest'. Click in the image to change it and" + " press Save.", + null=True, + upload_to="user_images", + verbose_name="Profile picture", + width_field="width", + ), ), migrations.AlterField( - model_name='profile', - name='linkedin_url', - field=models.URLField(blank=True, help_text='URL to your LinkedIn profile.', verbose_name='LinkedIn URL'), + model_name="profile", + name="linkedin_url", + field=models.URLField( + blank=True, + help_text="URL to your LinkedIn profile.", + verbose_name="LinkedIn URL", + ), ), migrations.AlterField( - model_name='profile', - name='twitter_url', - field=models.URLField(blank=True, help_text='URL to your Twitter profile.', verbose_name='Twitter URL'), + model_name="profile", + name="twitter_url", + field=models.URLField( + blank=True, + help_text="URL to your Twitter profile.", + verbose_name="Twitter URL", + ), ), migrations.AlterField( - model_name='profile', - name='user', - field=models.OneToOneField(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL), + model_name="profile", + name="user", + field=models.OneToOneField( + editable=False, + on_delete=django.db.models.deletion.CASCADE, + related_name="profile", + to=settings.AUTH_USER_MODEL, + ), ), migrations.AlterField( - model_name='profile', - name='website', - field=models.URLField(blank=True, help_text='URL to your personal website.', verbose_name='Website'), + model_name="profile", + name="website", + field=models.URLField( + blank=True, + help_text="URL to your personal website.", + verbose_name="Website", + ), ), migrations.AlterField( - model_name='profile', - name='youtube_url', - field=models.URLField(blank=True, help_text='URL to your Youtube channel.', verbose_name='Youtube URL'), + model_name="profile", + name="youtube_url", + field=models.URLField( + blank=True, + help_text="URL to your Youtube channel.", + verbose_name="Youtube URL", + ), ), ] diff --git a/profile/migrations/0002_auto_20220117_1721.py b/profile/migrations/0002_auto_20220117_1721.py index d4969d4ae45de9f045af5934a54640b9f7e9d28b..e209efdea852e921a271540fe56c4e686a9c7f75 100644 --- a/profile/migrations/0002_auto_20220117_1721.py +++ b/profile/migrations/0002_auto_20220117_1721.py @@ -6,18 +6,22 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('profile', '0001_squashed'), + ("profile", "0001_squashed"), ] operations = [ migrations.AlterField( - model_name='profile', - name='cba_user_token', - field=models.CharField(blank=True, max_length=255, verbose_name='CBA Token'), + model_name="profile", + name="cba_user_token", + field=models.CharField( + blank=True, max_length=255, verbose_name="CBA Token" + ), ), migrations.AlterField( - model_name='profile', - name='cba_username', - field=models.CharField(blank=True, max_length=60, verbose_name='CBA Username'), + model_name="profile", + name="cba_username", + field=models.CharField( + blank=True, max_length=60, verbose_name="CBA Username" + ), ), ] diff --git a/profile/migrations/0003_auto_20171213_1737.py b/profile/migrations/0003_auto_20171213_1737.py index 1d4de59c5312a4407eff36f2a9459bd550e2d008..8a4b11031e742c40aec3dd314efbf7e8018c19c0 100644 --- a/profile/migrations/0003_auto_20171213_1737.py +++ b/profile/migrations/0003_auto_20171213_1737.py @@ -8,60 +8,60 @@ from django.db import migrations class Migration(migrations.Migration): dependencies = [ - ('profile', '0002_auto_20171129_1828'), + ("profile", "0002_auto_20171129_1828"), ] operations = [ migrations.RemoveField( - model_name='profile', - name='biography', + model_name="profile", + name="biography", ), migrations.RemoveField( - model_name='profile', - name='cba_url', + model_name="profile", + name="cba_url", ), migrations.RemoveField( - model_name='profile', - name='dorftv_url', + model_name="profile", + name="dorftv_url", ), migrations.RemoveField( - model_name='profile', - name='facebook_url', + model_name="profile", + name="facebook_url", ), migrations.RemoveField( - model_name='profile', - name='googleplus_url', + model_name="profile", + name="googleplus_url", ), migrations.RemoveField( - model_name='profile', - name='height', + model_name="profile", + name="height", ), migrations.RemoveField( - model_name='profile', - name='image', + model_name="profile", + name="image", ), migrations.RemoveField( - model_name='profile', - name='linkedin_url', + model_name="profile", + name="linkedin_url", ), migrations.RemoveField( - model_name='profile', - name='ppoi', + model_name="profile", + name="ppoi", ), migrations.RemoveField( - model_name='profile', - name='twitter_url', + model_name="profile", + name="twitter_url", ), migrations.RemoveField( - model_name='profile', - name='website', + model_name="profile", + name="website", ), migrations.RemoveField( - model_name='profile', - name='width', + model_name="profile", + name="width", ), migrations.RemoveField( - model_name='profile', - name='youtube_url', + model_name="profile", + name="youtube_url", ), ] diff --git a/profile/models.py b/profile/models.py index cc3316110b7e35293a8f211e4997213c6bddd769..743639222894e49eaadaf7539e780d9497a3ade0 100644 --- a/profile/models.py +++ b/profile/models.py @@ -23,7 +23,9 @@ from django.utils.translation import gettext_lazy as _ class Profile(models.Model): - user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile', editable=False) + user = models.OneToOneField( + User, on_delete=models.CASCADE, related_name="profile", editable=False + ) cba_username = models.CharField(_("CBA Username"), blank=True, max_length=60) cba_user_token = models.CharField(_("CBA Token"), blank=True, max_length=255) @@ -31,7 +33,7 @@ class Profile(models.Model): return self.user.username class Meta: - db_table = 'profile' + db_table = "profile" def save(self, *args, **kwargs): super(Profile, self).save(*args, **kwargs) diff --git a/profile/serializers.py b/profile/serializers.py index 377894a8b9f88f3d0213a79e65917f512eb4cc2e..efff1c2f0ee98b5b086d3c5fa069cd8869a3b72c 100644 --- a/profile/serializers.py +++ b/profile/serializers.py @@ -17,12 +17,12 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. # -from rest_framework import serializers - from profile.models import Profile +from rest_framework import serializers + class ProfileSerializer(serializers.ModelSerializer): class Meta: model = Profile - fields = '__all__' + fields = "__all__" diff --git a/program/auth.py b/program/auth.py index 17221ae5e93441891efd235cd04a765450babb1e..19d2b99756e835716636361c96bcf9b7457556a1 100644 --- a/program/auth.py +++ b/program/auth.py @@ -20,8 +20,7 @@ from oidc_provider.lib.utils.oauth2 import extract_access_token from oidc_provider.models import Token -from rest_framework import authentication -from rest_framework import exceptions +from rest_framework import authentication, exceptions class OidcOauth2Auth(authentication.BaseAuthentication): diff --git a/program/filters.py b/program/filters.py new file mode 100644 index 0000000000000000000000000000000000000000..e05e58d70c58d55ae4171ceb22055487f03bfe8d --- /dev/null +++ b/program/filters.py @@ -0,0 +1,236 @@ +import datetime + +from django_filters import rest_framework as filters +from django_filters import widgets + +from django import forms +from django.contrib.auth.models import User +from django.db.models import Q, QuerySet +from django.utils import timezone +from program import models + + +class StaticFilterHelpTextMixin: + @classmethod + def filter_for_field(cls, field, field_name, lookup_expr=None): + _filter = super().filter_for_field(field, field_name, lookup_expr) + if "help_text" not in _filter.extra: + help_texts = getattr(cls.Meta, "help_texts", {}) + _filter.extra["help_text"] = help_texts.get(field_name, "") + return _filter + + +class ModelMultipleChoiceFilter(filters.ModelMultipleChoiceFilter): + def __init__(self, *args, **kwargs): + kwargs.setdefault("widget", widgets.CSVWidget()) + kwargs["lookup_expr"] = "in" + super().__init__(*args, **kwargs) + + def get_filter_predicate(self, v): + # There is something wrong with using ModelMultipleChoiceFilter + # along the CSVWidget that causes lookups to fail. + # May be related to: https://github.com/carltongibson/django-filter/issues/1103 + return super().get_filter_predicate([v.pk]) + + +class ShowFilterSet(StaticFilterHelpTextMixin, filters.FilterSet): + active = filters.BooleanFilter( + field_name="is_active", + method="filter_active", + help_text=( + "Return only currently running shows if true or past or upcoming shows if false.", + ), + ) + host = ModelMultipleChoiceFilter( + queryset=models.Host.objects.all(), + field_name="hosts", + help_text="Return only shows assigned to the given host(s).", + ) + # TODO: replace `musicfocus` with `music_focus` when dashboard is updated + musicfocus = ModelMultipleChoiceFilter( + queryset=models.MusicFocus.objects.all(), + field_name="music_focus", + help_text="Return only shows with given music focus(es).", + ) + owner = ModelMultipleChoiceFilter( + queryset=User.objects.all(), + field_name="owners", + help_text="Return only shows that belong to the given owner(s).", + ) + category = ModelMultipleChoiceFilter( + queryset=models.Category.objects.all(), + help_text="Return only shows of the given category or categories.", + ) + language = ModelMultipleChoiceFilter( + queryset=models.Language.objects.all(), + help_text="Return only shows of the given language(s).", + ) + topic = ModelMultipleChoiceFilter( + queryset=models.Topic.objects.all(), + help_text="Return only shows of the given topic(s).", + ) + public = filters.BooleanFilter( + field_name="is_public", + help_text="Return only shows that are public/non-public.", + ) + + def filter_active(self, queryset: QuerySet, name: str, value: bool): + # Filter currently running shows + # Get currently running schedules to filter by first + # For single dates we test if there'll be one in the future (and ignore the until date) + # TODO: Really consider first_date? (=currently active, not just upcoming ones) + # Add limit for future? + show_ids = ( + models.Schedule.objects.filter( + Q( + rrule_id__gt=1, + first_date__lte=timezone.now(), + last_date__gte=timezone.now(), + ) + | Q(rrule_id=1, first_date__gte=timezone.now()) + ) + .distinct() + .values_list("show_id", flat=True) + ) + if value: + # Filter active shows based on timeslots as well as on the is_active flag + # Even if there are future timeslots but is_active=True the show will be considered as + # inactive + return queryset.filter(id__in=show_ids, is_active=True) + else: + return queryset.exclude(id__in=show_ids, is_active=True) + + class Meta: + model = models.Show + help_texts = { + "type": "Return only shows of a given type.", + } + fields = [ + "active", + "category", + "host", + "language", + "musicfocus", + "owner", + "public", + "topic", + "type", + ] + + +class TimeSlotFilterSet(filters.FilterSet): + order = filters.OrderingFilter( + fields=[field.name for field in models.TimeSlot._meta.get_fields()] + ) + surrounding = filters.DateTimeFilter( + method="filter_surrounding", + label="Return surrounding timeslots", + help_text=( + "Returns the 10 nearest timeslots around the specified datetime. " + "If specified without a datetime value the current date and time is assumed." + ), + ) + # The start/end filters will always be applied even if no query parameter has been set. + # This is because we enforce a value in the clean_start and clean_end methods + # of the filterset form. + start = filters.DateFilter( + method="filter_start", + help_text=( + "Only returns timeslots after that start on or after the specified date. " + "By default, this is set to the current date." + ), + ) + end = filters.DateFilter( + method="filter_end", + help_text=( + "Only returns timeslots that end on or before the specified date. " + "By default, this is set to value of the start filter + 60 days." + ), + ) + + def filter_surrounding( + self, queryset: QuerySet, name: str, value: datetime.datetime + ): + nearest_timeslots_in_future = ( + models.TimeSlot.objects.filter(start__gte=value) + .order_by("start") + .values_list("id", flat=True)[:5] + ) + nearest_timeslots_in_past = ( + models.TimeSlot.objects.filter(start__lt=value) + .order_by("-start") + .values_list("id", flat=True)[:5] + ) + relevant_timeslot_ids = list(nearest_timeslots_in_future) + list( + nearest_timeslots_in_past + ) + return queryset.filter(id__in=relevant_timeslot_ids) + + def filter_start(self, queryset: QuerySet, name: str, value: datetime.date): + start = timezone.make_aware(datetime.datetime.combine(value, datetime.time.min)) + return queryset.filter(start__gte=start) + + def filter_end(self, queryset: QuerySet, name: str, value: datetime.date): + end = timezone.make_aware(datetime.datetime.combine(value, datetime.time.max)) + return queryset.filter(end__lte=end) + + def filter_queryset(self, queryset): + queryset = super().filter_queryset(queryset) + # This is for backwards compatibility as the surrounding-filter was formerly implemented + # by just checking for the existence of the query parameter. + if self.request.GET.get("surrounding", None) == "": + queryset = self.filter_surrounding(queryset, "surrounding", timezone.now()) + return queryset + + class Meta: + model = models.TimeSlot + fields = [ + "order", + "start", + "end", + "surrounding", + ] + + class form(forms.Form): + def clean_start(self): + start = self.cleaned_data.get("start", None) + return start or timezone.now().date() + + def clean_end(self): + end = self.cleaned_data.get("end", None) + return end or self.cleaned_data["start"] + datetime.timedelta(days=60) + + +class NoteFilterSet(StaticFilterHelpTextMixin, filters.FilterSet): + ids = ModelMultipleChoiceFilter( + field_name="id", + queryset=models.Note.objects.all(), + help_text="Return only notes matching the specified id(s).", + ) + owner = ModelMultipleChoiceFilter( + field_name="show__owners", + queryset=models.User.objects.all(), + help_text="Return only notes by show the specified owner(s): all notes the user may edit.", + ) + + class Meta: + model = models.Note + help_texts = { + "host": "Return only notes from the specified host.", + "user": "Return only notes created by the specified user.", + } + fields = [ + "host", + "ids", + "owner", + "user", + ] + + +class ActiveFilterSet(StaticFilterHelpTextMixin, filters.FilterSet): + active = filters.BooleanFilter(field_name="is_active") + + class Meta: + fields = [ + "active", + ] diff --git a/program/management/commands/addnote.py b/program/management/commands/addnote.py index 7b8aff6ada714fc526a156d175fc495e9d1aafa6..e19ede8d7bd530c66919cb3a3daf76c1f33a809e 100644 --- a/program/management/commands/addnote.py +++ b/program/management/commands/addnote.py @@ -1,15 +1,14 @@ -from django.core.management.base import BaseCommand, CommandError -from django.core.exceptions import ValidationError - import sys -from program.models import Show, TimeSlot, Note +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]' + help = "adds a note to a timeslot" + args = "<show_id> <start_date> <status> [index]" def handle(self, *args, **options): if len(args) == 3: @@ -22,7 +21,9 @@ class Command(BaseCommand): status = args[2] index = args[3] else: - raise CommandError('you must provide the show_id, start_date, status [index]') + raise CommandError( + "you must provide the show_id, start_date, status [index]" + ) try: show = Show.objects.get(id=show_id) @@ -37,16 +38,20 @@ class Command(BaseCommand): 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) + 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') + 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)] + 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) @@ -56,7 +61,9 @@ class Command(BaseCommand): except Exception as e: raise CommandError(e) - note = Note(timeslot=timeslot, title=title, content=''.join(lines), status=status) + note = Note( + timeslot=timeslot, title=title, content="".join(lines), status=status + ) try: note.validate_unique() @@ -64,4 +71,6 @@ class Command(BaseCommand): raise CommandError(ve.messages[0]) else: note.save() - self.stdout.write(self.style.SUCCESS, f'added note "{title}" to "{timeslot}"') + self.stdout.write( + self.style.SUCCESS, f'added note "{title}" to "{timeslot}"' + ) diff --git a/program/management/commands/create_oidc_client.py b/program/management/commands/create_oidc_client.py index dfb96241bc488f29b424af71bb8d0b624a9a2c5d..b7c247daa912708be70b471ba000315f7eb55031 100644 --- a/program/management/commands/create_oidc_client.py +++ b/program/management/commands/create_oidc_client.py @@ -2,47 +2,106 @@ import random import string import sys -from django.core.management.base import BaseCommand, CommandError from oidc_provider.models import Client, ResponseType +from django.core.management.base import BaseCommand, CommandError + class Command(BaseCommand): - help = 'Sets up an OIDC client / relaying party. For details check out the' + \ - 'section on Relying Parties at https://django-oidc-provider.readthedocs.io' + help = ( + "Sets up an OIDC client / relaying party. For details check out the" + + "section on Relying Parties at https://django-oidc-provider.readthedocs.io" + ) def add_arguments(self, parser): - parser.add_argument('name', type=str, - help='A label that you associate with this client') - parser.add_argument('client_type', type=str, choices=['public', 'confidential'], - help='The type of client can be either public or confidential') - parser.add_argument('--client-id', type=int, dest='client_id', action='store', help='The client ID ') + parser.add_argument( + "name", type=str, help="A label that you associate with this client" + ) + parser.add_argument( + "client_type", + type=str, + choices=["public", "confidential"], + help="The type of client can be either public or confidential", + ) + parser.add_argument( + "--client-id", + type=int, + dest="client_id", + action="store", + help="The client ID ", + ) parser.set_defaults(client_id=None) - parser.add_argument('--client-secret', type=str, dest='client_secret', action='store', help='The client secret') + parser.add_argument( + "--client-secret", + type=str, + dest="client_secret", + action="store", + help="The client secret", + ) parser.set_defaults(client_secret=None) - parser.add_argument('--no-require-consent', dest='require_consent', action='store_false', - help='By default user consent is required. Use this to skip user consent.') - parser.add_argument('--no-reuse-consent', dest='reuse_consent', action='store_false', - help='By default user consent will be reused. Use this if the user should provide consent on every login.') + parser.add_argument( + "--no-require-consent", + dest="require_consent", + action="store_false", + help="By default user consent is required. Use this to skip user consent.", + ) + parser.add_argument( + "--no-reuse-consent", + dest="reuse_consent", + action="store_false", + help="By default user consent will be reused. Use this if the user should provide" + " consent on every login.", + ) parser.set_defaults(require_consent=True, reuse_consent=True) - parser.add_argument('-u', '--redirect-uri', type=str, action='append', - help='Redirect URI after successful authentication. Can be used more than once.') - parser.add_argument('-p', '--post-logout-redirect', type=str, action='append', - help='Post logout redirect URI. Can be used more than once.') - parser.add_argument('-s', '--scope', type=str, action='append', - help='Authorized scope values for this client. Can be used more than once.') - parser.add_argument('-r', dest='response_types', action='append', - choices=['code', 'id_token', 'id_token token', 'code token', 'code id_token', - 'code id_token token'], - help='The type of response the client will get.') - parser.add_argument('-i', '--id-only', dest='id_only', action='store_true', - help='Do not print anything else then the ID of the newly created client ' + \ - '(and the client secret in case of confidential clients).') + parser.add_argument( + "-u", + "--redirect-uri", + type=str, + action="append", + help="Redirect URI after successful authentication. Can be used more than once.", + ) + parser.add_argument( + "-p", + "--post-logout-redirect", + type=str, + action="append", + help="Post logout redirect URI. Can be used more than once.", + ) + parser.add_argument( + "-s", + "--scope", + type=str, + action="append", + help="Authorized scope values for this client. Can be used more than once.", + ) + parser.add_argument( + "-r", + dest="response_types", + action="append", + choices=[ + "code", + "id_token", + "id_token token", + "code token", + "code id_token", + "code id_token token", + ], + help="The type of response the client will get.", + ) + parser.add_argument( + "-i", + "--id-only", + dest="id_only", + action="store_true", + help="Do not print anything else then the ID of the newly created client " + + "(and the client secret in case of confidential clients).", + ) parser.set_defaults(id_only=False) def handle(self, *args, **options): - if options['client_id'] and options['client_secret']: - client_id = options['client_id'] - client_secret = options['client_secret'] + if options["client_id"] and options["client_secret"]: + client_id = options["client_id"] + client_secret = options["client_secret"] else: # generate a new client ID and secret client_id = False @@ -51,9 +110,11 @@ class Command(BaseCommand): client_id = random.randint(100000, 999999) counter += 1 if counter > 10000: - raise CommandError('Could not find a free client_id. Already' + - ' tried 10000 times. There seems to be something seriously' + - ' wrong with your setup. Please inspect manually.') + raise CommandError( + "Could not find a free client_id. Already" + + " tried 10000 times. There seems to be something seriously" + + " wrong with your setup. Please inspect manually." + ) try: Client.objects.get(client_id=client_id) except Client.DoesNotExist: @@ -61,17 +122,20 @@ class Command(BaseCommand): else: client_id = False - client_secret = ''.join(random.SystemRandom().choice(string.ascii_letters + string.digits) for _ in range(32)) + client_secret = "".join( + random.SystemRandom().choice(string.ascii_letters + string.digits) + for _ in range(32) + ) - show_results = options['client_id'] is None and options['client_secret'] is None + show_results = options["client_id"] is None and options["client_secret"] is None # initialize lists if no option was provided - if options['redirect_uri'] is None: - options['redirect_uri'] = [] - if options['post_logout_redirect'] is None: - options['post_logout_redirect'] = [] - if options['scope'] is None: - options['scope'] = [] + if options["redirect_uri"] is None: + options["redirect_uri"] = [] + if options["post_logout_redirect"] is None: + options["post_logout_redirect"] = [] + if options["scope"] is None: + options["scope"] = [] if not options["id_only"] and show_results: self.stdout.write(f'Creating client with name {options["name"]}') @@ -79,34 +143,43 @@ class Command(BaseCommand): c = Client( client_id=client_id, client_secret=client_secret, - name=options['name'], client_type=options['client_type'], - redirect_uris=options['redirect_uri'], - require_consent=options['require_consent'], - reuse_consent=options['reuse_consent'], - post_logout_redirect_uris=options['post_logout_redirect'], - scope=options['scope'], + name=options["name"], + client_type=options["client_type"], + redirect_uris=options["redirect_uri"], + require_consent=options["require_consent"], + reuse_consent=options["reuse_consent"], + post_logout_redirect_uris=options["post_logout_redirect"], + scope=options["scope"], ) c.save() - except: - raise CommandError('Could not create an OpenID connect client' + - f' due to the following error: {sys.exc_info()}') + except Exception: + raise CommandError( + "Could not create an OpenID connect client" + + f" due to the following error: {sys.exc_info()}" + ) - if options['response_types']: + if options["response_types"]: try: - for r_value in options['response_types']: + for r_value in options["response_types"]: r = ResponseType.objects.get(value=r_value) c.response_types.add(r) - except: - raise CommandError('Client was stored, but could not set response_types' + - f' due to the following error: {sys.exc_info()}') + except Exception: + raise CommandError( + "Client was stored, but could not set response_types" + + f" due to the following error: {sys.exc_info()}" + ) if show_results: if options["id_only"]: - if options['client_type'] == 'confidential': - self.stdout.write(f'{c.client_id} {c.client_secret}') + if options["client_type"] == "confidential": + self.stdout.write(f"{c.client_id} {c.client_secret}") else: - self.stdout.write(f'{c.client_id}') + self.stdout.write(f"{c.client_id}") else: - self.stdout.write(f'Successfully created new OIDC client, with ID: {c.client_id}') - if options['client_type'] == 'confidential': - self.stdout.write(f'The secret for this confidential client is: {c.client_secret}') + self.stdout.write( + f"Successfully created new OIDC client, with ID: {c.client_id}" + ) + if options["client_type"] == "confidential": + self.stdout.write( + f"The secret for this confidential client is: {c.client_secret}" + ) diff --git a/program/management/commands/createuser.py b/program/management/commands/createuser.py index 25985c0171ea8e1302053d7c609fd317e71cf20c..3e816ccd90e599166bc767baed7240f3c43f19cb 100644 --- a/program/management/commands/createuser.py +++ b/program/management/commands/createuser.py @@ -3,15 +3,27 @@ from django.core.management.base import BaseCommand, CommandError class Command(BaseCommand): - help = 'creates an user' + help = "creates an user" def add_arguments(self, parser): - parser.add_argument('--username', action='store', help='Specifies the username.', required=True, type=str) - parser.add_argument('--email', action='store', help='Specifies the email address.', required=True, type=str) + parser.add_argument( + "--username", + action="store", + help="Specifies the username.", + required=True, + type=str, + ) + parser.add_argument( + "--email", + action="store", + help="Specifies the email address.", + required=True, + type=str, + ) def handle(self, *args, **options): - username = options.get('username', None) - email = options.get('email', None) + username = options.get("username", None) + email = options.get("email", None) if not username or not email: raise CommandError("You must use --username and --email.") @@ -19,6 +31,8 @@ class Command(BaseCommand): User.objects.get(username=username) except User.DoesNotExist: User.objects.create_user(username=username, email=email) - self.stdout.write(self.style.SUCCESS('user created successfully.')) + self.stdout.write(self.style.SUCCESS("user created successfully.")) else: - self.stdout.write(self.style.NOTICE('User already exists, no need to create.')) + self.stdout.write( + self.style.NOTICE("User already exists, no need to create.") + ) diff --git a/program/management/commands/deleteuser.py b/program/management/commands/deleteuser.py index 97c44641e5a9804b237d78ba58b4ee4658762a06..4cbbe833dbfd9429f60fbdcbedcdbc6b41d8480d 100644 --- a/program/management/commands/deleteuser.py +++ b/program/management/commands/deleteuser.py @@ -3,19 +3,25 @@ from django.core.management.base import BaseCommand, CommandError class Command(BaseCommand): - help = 'deletes an user' + help = "deletes an user" def add_arguments(self, parser): - parser.add_argument('--username', action='store', help='Specifies the username.', required=True, type=str), + parser.add_argument( + "--username", + action="store", + help="Specifies the username.", + required=True, + type=str, + ), def handle(self, *args, **options): - username = options.get('username', None) + username = options.get("username", None) if not username: raise CommandError("You must use --username.") try: User.objects.get(username=username).delete() except User.DoesNotExist: - raise 'user does not exist.' + raise "user does not exist." else: - self.stdout.write(self.style.SUCCESS('user deleted successfully.')) + self.stdout.write(self.style.SUCCESS("user deleted successfully.")) diff --git a/program/management/commands/export_showlog.py b/program/management/commands/export_showlog.py index e5c86a83d10dacc954f2ca00b042b94c24586639..df471d48bcfce5e3ee427552b60c3ce2f290d5c0 100644 --- a/program/management/commands/export_showlog.py +++ b/program/management/commands/export_showlog.py @@ -3,16 +3,17 @@ import codecs import sys from datetime import datetime + from django.core.management.base import BaseCommand, CommandError from program.models import TimeSlot class Command(BaseCommand): - help = 'export playlog for one year' - args = '<year>' + help = "export playlog for one year" + args = "<year>" def handle(self, *args, **options): - UTF8Writer = codecs.getwriter('utf8') + UTF8Writer = codecs.getwriter("utf8") sys.stdout = UTF8Writer(sys.stdout) if len(args) == 1: @@ -21,23 +22,32 @@ class Command(BaseCommand): except ValueError: raise CommandError("'%s' is not a valid year" % args[0]) else: - raise CommandError('you must provide the year') + raise CommandError("you must provide the year") self.stdout.write(self.style.NOTICE, f"# Radio Helsinki Sendungslog {year}") start = datetime(year, 1, 1, 0, 0) - end = datetime(year+1, 1, 1, 0, 0) + end = datetime(year + 1, 1, 1, 0, 0) currentDate = None - for ts in TimeSlot.objects.filter(end__gt=start, start__lt=end).select_related('schedule').select_related('show'): + for ts in ( + TimeSlot.objects.filter(end__gt=start, start__lt=end) + .select_related("schedule") + .select_related("show") + ): if currentDate is None or currentDate < ts.start.date(): if currentDate: - self.stdout.write('\n') + self.stdout.write("\n") currentDate = ts.start.date() - self.stdout.write(self.style.NOTICE, currentDate.strftime("## %a %d.%m.%Y:\n")) + self.stdout.write( + self.style.NOTICE, currentDate.strftime("## %a %d.%m.%Y:\n") + ) title = ts.show.name if ts.schedule.is_repetition: title += " (WH)" - self.stdout.write(self.style.NOTICE, f' * **{ts.start.strftime("%H:%M:%S")} - {ts.end.strftime("%H:%M:%S")}**: {title}') + self.stdout.write( + self.style.NOTICE, + f' * **{ts.start.strftime("%H:%M:%S")} - {ts.end.strftime("%H:%M:%S")}**: {title}', + ) diff --git a/program/management/commands/remove_automation_id.py b/program/management/commands/remove_automation_id.py index 75da30c56bc348ff05b82edf0199b439bec25ed4..519e768150099a1bb958a0b8762293d55e749f08 100644 --- a/program/management/commands/remove_automation_id.py +++ b/program/management/commands/remove_automation_id.py @@ -1,16 +1,15 @@ from django.core.management.base import BaseCommand, CommandError - from program.models import Schedule class Command(BaseCommand): - help = 'removes the automation_id from the program slots' - args = '<automation_id>' + help = "removes the automation_id from the program slots" + args = "<automation_id>" def handle(self, *args, **options): if len(args) == 1: automation_id = args[0] else: - raise CommandError('you must provide the automation_id') + raise CommandError("you must provide the automation_id") - Schedule.objects.filter(automation_id=automation_id).update(automation_id=None) \ No newline at end of file + Schedule.objects.filter(automation_id=automation_id).update(automation_id=None) diff --git a/program/migrations/0001_initial.py b/program/migrations/0001_initial.py index ebbe4293995e8a45754043208a42a84e7dd138d3..34b3079b0c61d6b690249c289cc87a381d623dac 100644 --- a/program/migrations/0001_initial.py +++ b/program/migrations/0001_initial.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import migrations, models from django.conf import settings +from django.db import migrations, models class Migration(migrations.Migration): @@ -13,215 +13,602 @@ class Migration(migrations.Migration): operations = [ migrations.CreateModel( - name='BroadcastFormat', + name="BroadcastFormat", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('format', models.CharField(max_length=32, verbose_name='Format')), - ('slug', models.SlugField(unique=True, max_length=32, verbose_name='Slug')), - ('color', models.CharField(default=b'#ffffff', max_length=7, verbose_name='Color')), - ('text_color', models.CharField(default=b'#000000', max_length=7, verbose_name='Text color')), - ('enabled', models.BooleanField(default=True, verbose_name='Enabled')), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ("format", models.CharField(max_length=32, verbose_name="Format")), + ( + "slug", + models.SlugField(unique=True, max_length=32, verbose_name="Slug"), + ), + ( + "color", + models.CharField( + default=b"#ffffff", max_length=7, verbose_name="Color" + ), + ), + ( + "text_color", + models.CharField( + default=b"#000000", max_length=7, verbose_name="Text color" + ), + ), + ("enabled", models.BooleanField(default=True, verbose_name="Enabled")), ], options={ - 'ordering': ('format',), - 'verbose_name': 'Broadcast format', - 'verbose_name_plural': 'Broadcast formats', + "ordering": ("format",), + "verbose_name": "Broadcast format", + "verbose_name_plural": "Broadcast formats", }, ), migrations.CreateModel( - name='Host', + name="Host", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('name', models.CharField(max_length=128, verbose_name='Name')), - ('email', models.EmailField(max_length=254, verbose_name='E-Mail', blank=True)), - ('website', models.URLField(verbose_name='Website', blank=True)), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ("name", models.CharField(max_length=128, verbose_name="Name")), + ( + "email", + models.EmailField( + max_length=254, verbose_name="E-Mail", blank=True + ), + ), + ("website", models.URLField(verbose_name="Website", blank=True)), ], options={ - 'ordering': ('name',), - 'verbose_name': 'Host', - 'verbose_name_plural': 'Hosts', + "ordering": ("name",), + "verbose_name": "Host", + "verbose_name_plural": "Hosts", }, ), migrations.CreateModel( - name='MusicFocus', + name="MusicFocus", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('focus', models.CharField(max_length=32, verbose_name='Focus')), - ('abbrev', models.CharField(unique=True, max_length=4, verbose_name='Abbreviation')), - ('slug', models.SlugField(unique=True, max_length=32, verbose_name='Slug')), - ('button', models.ImageField(upload_to=b'buttons', null=True, verbose_name='Button image', blank=True)), - ('button_hover', models.ImageField(upload_to=b'buttons', null=True, verbose_name='Button image (hover)', blank=True)), - ('big_button', models.ImageField(upload_to=b'buttons', null=True, verbose_name='Big button image', blank=True)), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ("focus", models.CharField(max_length=32, verbose_name="Focus")), + ( + "abbrev", + models.CharField( + unique=True, max_length=4, verbose_name="Abbreviation" + ), + ), + ( + "slug", + models.SlugField(unique=True, max_length=32, verbose_name="Slug"), + ), + ( + "button", + models.ImageField( + upload_to=b"buttons", + null=True, + verbose_name="Button image", + blank=True, + ), + ), + ( + "button_hover", + models.ImageField( + upload_to=b"buttons", + null=True, + verbose_name="Button image (hover)", + blank=True, + ), + ), + ( + "big_button", + models.ImageField( + upload_to=b"buttons", + null=True, + verbose_name="Big button image", + blank=True, + ), + ), ], options={ - 'ordering': ('focus',), - 'verbose_name': 'Music focus', - 'verbose_name_plural': 'Music focus', + "ordering": ("focus",), + "verbose_name": "Music focus", + "verbose_name_plural": "Music focus", }, ), migrations.CreateModel( - name='Note', + name="Note", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('title', models.CharField(max_length=128, verbose_name='Title')), - ('content', models.TextField(verbose_name='Content')), - ('status', models.IntegerField(default=1, verbose_name='Status', choices=[(0, 'Cancellation'), (1, 'Recommendation'), (2, 'Repetition')])), - ('cba_entry_id', models.IntegerField(null=True, verbose_name='CBA entry ID', blank=True)), - ('start', models.DateTimeField(editable=False)), - ('created', models.DateTimeField(auto_now_add=True)), - ('last_updated', models.DateTimeField(auto_now=True)), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ("title", models.CharField(max_length=128, verbose_name="Title")), + ("content", models.TextField(verbose_name="Content")), + ( + "status", + models.IntegerField( + default=1, + verbose_name="Status", + choices=[ + (0, "Cancellation"), + (1, "Recommendation"), + (2, "Repetition"), + ], + ), + ), + ( + "cba_entry_id", + models.IntegerField( + null=True, verbose_name="CBA entry ID", blank=True + ), + ), + ("start", models.DateTimeField(editable=False)), + ("created", models.DateTimeField(auto_now_add=True)), + ("last_updated", models.DateTimeField(auto_now=True)), ], options={ - 'ordering': ('timeslot',), - 'verbose_name': 'Note', - 'verbose_name_plural': 'Notes', + "ordering": ("timeslot",), + "verbose_name": "Note", + "verbose_name_plural": "Notes", }, ), migrations.CreateModel( - name='ProgramSlot', + name="ProgramSlot", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('byweekday', models.IntegerField(verbose_name='Weekday', choices=[(0, 'Monday'), (1, 'Tuesday'), (2, 'Wednesday'), (3, 'Thursday'), (4, 'Friday'), (5, 'Saturday'), (6, 'Sunday')])), - ('dstart', models.DateField(verbose_name='First date')), - ('tstart', models.TimeField(verbose_name='Start time')), - ('tend', models.TimeField(verbose_name='End time')), - ('until', models.DateField(verbose_name='Last date')), - ('is_repetition', models.BooleanField(default=False, verbose_name='Is repetition')), - ('automation_id', models.IntegerField(blank=True, null=True, verbose_name='Automation ID', choices=[])), - ('created', models.DateTimeField(auto_now_add=True)), - ('last_updated', models.DateTimeField(auto_now=True)), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ( + "byweekday", + models.IntegerField( + verbose_name="Weekday", + choices=[ + (0, "Monday"), + (1, "Tuesday"), + (2, "Wednesday"), + (3, "Thursday"), + (4, "Friday"), + (5, "Saturday"), + (6, "Sunday"), + ], + ), + ), + ("dstart", models.DateField(verbose_name="First date")), + ("tstart", models.TimeField(verbose_name="Start time")), + ("tend", models.TimeField(verbose_name="End time")), + ("until", models.DateField(verbose_name="Last date")), + ( + "is_repetition", + models.BooleanField(default=False, verbose_name="Is repetition"), + ), + ( + "automation_id", + models.IntegerField( + blank=True, null=True, verbose_name="Automation ID", choices=[] + ), + ), + ("created", models.DateTimeField(auto_now_add=True)), + ("last_updated", models.DateTimeField(auto_now=True)), ], options={ - 'ordering': ('dstart', 'tstart'), - 'verbose_name': 'Program slot', - 'verbose_name_plural': 'Program slots', + "ordering": ("dstart", "tstart"), + "verbose_name": "Program slot", + "verbose_name_plural": "Program slots", }, ), migrations.CreateModel( - name='RRule', + name="RRule", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('name', models.CharField(unique=True, max_length=32, verbose_name='Name')), - ('freq', models.IntegerField(verbose_name='Frequency', choices=[(1, 'Monthly'), (2, 'Weekly'), (3, 'Daily')])), - ('interval', models.IntegerField(default=1, verbose_name='Interval')), - ('bysetpos', models.IntegerField(blank=True, null=True, verbose_name='Set position', choices=[(1, 'First'), (2, 'Second'), (3, 'Third'), (4, 'Fourth'), (5, 'Fifth'), (-1, 'Last')])), - ('count', models.IntegerField(null=True, verbose_name='Count', blank=True)), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ( + "name", + models.CharField(unique=True, max_length=32, verbose_name="Name"), + ), + ( + "freq", + models.IntegerField( + verbose_name="Frequency", + choices=[(1, "Monthly"), (2, "Weekly"), (3, "Daily")], + ), + ), + ("interval", models.IntegerField(default=1, verbose_name="Interval")), + ( + "bysetpos", + models.IntegerField( + blank=True, + null=True, + verbose_name="Set position", + choices=[ + (1, "First"), + (2, "Second"), + (3, "Third"), + (4, "Fourth"), + (5, "Fifth"), + (-1, "Last"), + ], + ), + ), + ( + "count", + models.IntegerField(null=True, verbose_name="Count", blank=True), + ), ], options={ - 'ordering': ('-freq', 'interval', 'bysetpos'), - 'verbose_name': 'Recurrence rule', - 'verbose_name_plural': 'Recurrence rules', + "ordering": ("-freq", "interval", "bysetpos"), + "verbose_name": "Recurrence rule", + "verbose_name_plural": "Recurrence rules", }, ), migrations.CreateModel( - name='Show', + name="Show", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('name', models.CharField(max_length=255, verbose_name='Name')), - ('slug', models.CharField(unique=True, max_length=255, verbose_name='Slug')), - ('image', models.ImageField(upload_to=b'show_images', null=True, verbose_name='Image', blank=True)), - ('image_enabled', models.BooleanField(default=True, verbose_name='show Image')), - ('short_description', models.CharField(max_length=64, verbose_name='Short description')), - ('description', models.TextField(null=True, verbose_name='Description', blank=True)), - ('email', models.EmailField(max_length=254, null=True, verbose_name='E-Mail', blank=True)), - ('website', models.URLField(null=True, verbose_name='Website', blank=True)), - ('cba_series_id', models.IntegerField(null=True, verbose_name='CBA series ID', blank=True)), - ('automation_id', models.IntegerField(blank=True, null=True, verbose_name='Automation ID', choices=[])), - ('created', models.DateTimeField(auto_now_add=True)), - ('last_updated', models.DateTimeField(auto_now=True)), - ('broadcastformat', models.ForeignKey(related_name='shows', on_delete=models.CASCADE, verbose_name='Broadcast format', to='program.BroadcastFormat')), - ('hosts', models.ManyToManyField(related_name='shows', verbose_name='Hosts', to='program.Host', blank=True)), - ('musicfocus', models.ManyToManyField(related_name='shows', verbose_name='Music focus', to='program.MusicFocus', blank=True)), - ('owners', models.ManyToManyField(related_name='shows', verbose_name='Owners', to=settings.AUTH_USER_MODEL, blank=True)), - ('predecessor', models.ForeignKey(related_name='successors', on_delete=models.CASCADE, verbose_name='Predecessor', blank=True, to='program.Show', null=True)), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ("name", models.CharField(max_length=255, verbose_name="Name")), + ( + "slug", + models.CharField(unique=True, max_length=255, verbose_name="Slug"), + ), + ( + "image", + models.ImageField( + upload_to=b"show_images", + null=True, + verbose_name="Image", + blank=True, + ), + ), + ( + "image_enabled", + models.BooleanField(default=True, verbose_name="show Image"), + ), + ( + "short_description", + models.CharField(max_length=64, verbose_name="Short description"), + ), + ( + "description", + models.TextField(null=True, verbose_name="Description", blank=True), + ), + ( + "email", + models.EmailField( + max_length=254, null=True, verbose_name="E-Mail", blank=True + ), + ), + ( + "website", + models.URLField(null=True, verbose_name="Website", blank=True), + ), + ( + "cba_series_id", + models.IntegerField( + null=True, verbose_name="CBA series ID", blank=True + ), + ), + ( + "automation_id", + models.IntegerField( + blank=True, null=True, verbose_name="Automation ID", choices=[] + ), + ), + ("created", models.DateTimeField(auto_now_add=True)), + ("last_updated", models.DateTimeField(auto_now=True)), + ( + "broadcastformat", + models.ForeignKey( + related_name="shows", + on_delete=models.CASCADE, + verbose_name="Broadcast format", + to="program.BroadcastFormat", + ), + ), + ( + "hosts", + models.ManyToManyField( + related_name="shows", + verbose_name="Hosts", + to="program.Host", + blank=True, + ), + ), + ( + "musicfocus", + models.ManyToManyField( + related_name="shows", + verbose_name="Music focus", + to="program.MusicFocus", + blank=True, + ), + ), + ( + "owners", + models.ManyToManyField( + related_name="shows", + verbose_name="Owners", + to=settings.AUTH_USER_MODEL, + blank=True, + ), + ), + ( + "predecessor", + models.ForeignKey( + related_name="successors", + on_delete=models.CASCADE, + verbose_name="Predecessor", + blank=True, + to="program.Show", + null=True, + ), + ), ], options={ - 'ordering': ('slug',), - 'verbose_name': 'Show', - 'verbose_name_plural': 'Shows', + "ordering": ("slug",), + "verbose_name": "Show", + "verbose_name_plural": "Shows", }, ), migrations.CreateModel( - name='ShowInformation', + name="ShowInformation", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('information', models.CharField(max_length=32, verbose_name='Information')), - ('abbrev', models.CharField(unique=True, max_length=4, verbose_name='Abbreviation')), - ('slug', models.SlugField(unique=True, max_length=32, verbose_name='Slug')), - ('button', models.ImageField(upload_to=b'buttons', null=True, verbose_name='Button image', blank=True)), - ('button_hover', models.ImageField(upload_to=b'buttons', null=True, verbose_name='Button image (hover)', blank=True)), - ('big_button', models.ImageField(upload_to=b'buttons', null=True, verbose_name='Big button image', blank=True)), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ( + "information", + models.CharField(max_length=32, verbose_name="Information"), + ), + ( + "abbrev", + models.CharField( + unique=True, max_length=4, verbose_name="Abbreviation" + ), + ), + ( + "slug", + models.SlugField(unique=True, max_length=32, verbose_name="Slug"), + ), + ( + "button", + models.ImageField( + upload_to=b"buttons", + null=True, + verbose_name="Button image", + blank=True, + ), + ), + ( + "button_hover", + models.ImageField( + upload_to=b"buttons", + null=True, + verbose_name="Button image (hover)", + blank=True, + ), + ), + ( + "big_button", + models.ImageField( + upload_to=b"buttons", + null=True, + verbose_name="Big button image", + blank=True, + ), + ), ], options={ - 'ordering': ('information',), - 'verbose_name': 'Show information', - 'verbose_name_plural': 'Show information', + "ordering": ("information",), + "verbose_name": "Show information", + "verbose_name_plural": "Show information", }, ), migrations.CreateModel( - name='ShowTopic', + name="ShowTopic", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('topic', models.CharField(max_length=32, verbose_name='Show topic')), - ('abbrev', models.CharField(unique=True, max_length=4, verbose_name='Abbreviation')), - ('slug', models.SlugField(unique=True, max_length=32, verbose_name='Slug')), - ('button', models.ImageField(upload_to=b'buttons', null=True, verbose_name='Button image', blank=True)), - ('button_hover', models.ImageField(upload_to=b'buttons', null=True, verbose_name='Button image (hover)', blank=True)), - ('big_button', models.ImageField(upload_to=b'buttons', null=True, verbose_name='Big button image', blank=True)), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ("topic", models.CharField(max_length=32, verbose_name="Show topic")), + ( + "abbrev", + models.CharField( + unique=True, max_length=4, verbose_name="Abbreviation" + ), + ), + ( + "slug", + models.SlugField(unique=True, max_length=32, verbose_name="Slug"), + ), + ( + "button", + models.ImageField( + upload_to=b"buttons", + null=True, + verbose_name="Button image", + blank=True, + ), + ), + ( + "button_hover", + models.ImageField( + upload_to=b"buttons", + null=True, + verbose_name="Button image (hover)", + blank=True, + ), + ), + ( + "big_button", + models.ImageField( + upload_to=b"buttons", + null=True, + verbose_name="Big button image", + blank=True, + ), + ), ], options={ - 'ordering': ('topic',), - 'verbose_name': 'Show topic', - 'verbose_name_plural': 'Show topics', + "ordering": ("topic",), + "verbose_name": "Show topic", + "verbose_name_plural": "Show topics", }, ), migrations.CreateModel( - name='TimeSlot', + name="TimeSlot", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('start', models.DateTimeField(unique=True, verbose_name='Start time')), - ('end', models.DateTimeField(verbose_name='End time')), - ('programslot', models.ForeignKey(related_name='timeslots', on_delete=models.CASCADE, verbose_name='Program slot', to='program.ProgramSlot')), - ('show', models.ForeignKey(related_name='timeslots', on_delete=models.CASCADE, editable=False, to='program.Show')), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ("start", models.DateTimeField(unique=True, verbose_name="Start time")), + ("end", models.DateTimeField(verbose_name="End time")), + ( + "programslot", + models.ForeignKey( + related_name="timeslots", + on_delete=models.CASCADE, + verbose_name="Program slot", + to="program.ProgramSlot", + ), + ), + ( + "show", + models.ForeignKey( + related_name="timeslots", + on_delete=models.CASCADE, + editable=False, + to="program.Show", + ), + ), ], options={ - 'ordering': ('start', 'end'), - 'verbose_name': 'Time slot', - 'verbose_name_plural': 'Time slots', + "ordering": ("start", "end"), + "verbose_name": "Time slot", + "verbose_name_plural": "Time slots", }, ), migrations.AddField( - model_name='show', - name='showinformation', - field=models.ManyToManyField(related_name='shows', verbose_name='Show information', to='program.ShowInformation', blank=True), + model_name="show", + name="showinformation", + field=models.ManyToManyField( + related_name="shows", + verbose_name="Show information", + to="program.ShowInformation", + blank=True, + ), ), migrations.AddField( - model_name='show', - name='showtopic', - field=models.ManyToManyField(related_name='shows', verbose_name='Show topic', to='program.ShowTopic', blank=True), + model_name="show", + name="showtopic", + field=models.ManyToManyField( + related_name="shows", + verbose_name="Show topic", + to="program.ShowTopic", + blank=True, + ), ), migrations.AddField( - model_name='programslot', - name='rrule', - field=models.ForeignKey(related_name='programslots', on_delete=models.CASCADE, verbose_name='Recurrence rule', to='program.RRule'), + model_name="programslot", + name="rrule", + field=models.ForeignKey( + related_name="programslots", + on_delete=models.CASCADE, + verbose_name="Recurrence rule", + to="program.RRule", + ), ), migrations.AddField( - model_name='programslot', - name='show', - field=models.ForeignKey(related_name='programslots', on_delete=models.CASCADE, verbose_name='Show', to='program.Show'), + model_name="programslot", + name="show", + field=models.ForeignKey( + related_name="programslots", + on_delete=models.CASCADE, + verbose_name="Show", + to="program.Show", + ), ), migrations.AddField( - model_name='note', - name='show', - field=models.ForeignKey(related_name='notes', on_delete=models.CASCADE, editable=False, to='program.Show'), + model_name="note", + name="show", + field=models.ForeignKey( + related_name="notes", + on_delete=models.CASCADE, + editable=False, + to="program.Show", + ), ), migrations.AddField( - model_name='note', - name='timeslot', - field=models.OneToOneField(verbose_name='Time slot', on_delete=models.CASCADE, to='program.TimeSlot'), + model_name="note", + name="timeslot", + field=models.OneToOneField( + verbose_name="Time slot", + on_delete=models.CASCADE, + to="program.TimeSlot", + ), ), migrations.AlterUniqueTogether( - name='programslot', - unique_together=set([('rrule', 'byweekday', 'dstart', 'tstart')]), + name="programslot", + unique_together=set([("rrule", "byweekday", "dstart", "tstart")]), ), ] diff --git a/program/migrations/0001_squashed.py b/program/migrations/0001_squashed.py index 7db3fbd6f60df1bf944cafbecea8741e27a77b8c..4a3a39fee7a4045ce858a8fb567a09f20666491e 100644 --- a/program/migrations/0001_squashed.py +++ b/program/migrations/0001_squashed.py @@ -1,14 +1,38 @@ # Generated by Django 2.2.12 on 2020-11-21 01:34 +import versatileimagefield.fields + +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion -import versatileimagefield.fields class Migration(migrations.Migration): - replaces = [('program', '0001_initial'), ('program', '0002_host_is_always_visible'), ('program', '0003_host_is_active'), ('program', '0004_show_is_active'), ('program', '0005_programslot_is_active'), ('program', '0006_note_remove_cba_entry_id'), ('program', '0007_show_remove_cba_series_id'), ('program', '0008_show_remove_automation_id'), ('program', '0009_host_remove_is_active'), ('program', '0010_show_remove_is_active'), ('program', '0011_programslot_remove_is_active'), ('program', '0012_auto_20180104_0005'), ('program', '0013_auto_20180124_1748'), ('program', '0014_auto_20180216_2000'), ('program', '0015_auto_20180218_1111'), ('program', '0016_auto_20180222_1253'), ('program', '0017_auto_20180314_1409'), ('program', '0018_auto_20190810_1146'), ('program', '0019_auto_20190810_1340'), ('program', '0020_auto_20190810_1341'), ('program', '0021_show_is_active'), ('program', '0022_show_is_public')] + replaces = [ + ("program", "0001_initial"), + ("program", "0002_host_is_always_visible"), + ("program", "0003_host_is_active"), + ("program", "0004_show_is_active"), + ("program", "0005_programslot_is_active"), + ("program", "0006_note_remove_cba_entry_id"), + ("program", "0007_show_remove_cba_series_id"), + ("program", "0008_show_remove_automation_id"), + ("program", "0009_host_remove_is_active"), + ("program", "0010_show_remove_is_active"), + ("program", "0011_programslot_remove_is_active"), + ("program", "0012_auto_20180104_0005"), + ("program", "0013_auto_20180124_1748"), + ("program", "0014_auto_20180216_2000"), + ("program", "0015_auto_20180218_1111"), + ("program", "0016_auto_20180222_1253"), + ("program", "0017_auto_20180314_1409"), + ("program", "0018_auto_20190810_1146"), + ("program", "0019_auto_20190810_1340"), + ("program", "0020_auto_20190810_1341"), + ("program", "0021_show_is_active"), + ("program", "0022_show_is_public"), + ] initial = True @@ -18,324 +42,1104 @@ class Migration(migrations.Migration): operations = [ migrations.CreateModel( - name='Host', + name="Host", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=128, verbose_name='Name')), - ('email', models.EmailField(blank=True, max_length=254, verbose_name='E-Mail')), - ('website', models.URLField(blank=True, help_text='URL to your personal website.', verbose_name='Website')), - ('biography', models.TextField(blank=True, help_text='Describe yourself and your fields of interest in a few sentences.', null=True, verbose_name='Biography')), - ('cba_url', models.URLField(blank=True, help_text='URL to your CBA profile.', verbose_name='CBA URL')), - ('dorftv_url', models.URLField(blank=True, help_text='URL to your dorfTV channel.', verbose_name='DorfTV URL')), - ('facebook_url', models.URLField(blank=True, help_text='URL to your Facebook profile.', verbose_name='Facebook URL')), - ('googleplus_url', models.URLField(blank=True, help_text='URL to your Google+ profile.', verbose_name='Google+ URL')), - ('height', models.PositiveIntegerField(blank=True, editable=False, null=True, verbose_name='Image Height')), - ('image', versatileimagefield.fields.VersatileImageField(blank=True, height_field='height', help_text="Upload a picture of yourself. Images are automatically cropped around the 'Primary Point of Interest'. Click in the image to change it and press Save.", null=True, upload_to='host_images', verbose_name='Profile picture', width_field='width')), - ('linkedin_url', models.URLField(blank=True, help_text='URL to your LinkedIn profile.', verbose_name='LinkedIn URL')), - ('ppoi', versatileimagefield.fields.PPOIField(default='0.5x0.5', editable=False, max_length=20, verbose_name='Image PPOI')), - ('twitter_url', models.URLField(blank=True, help_text='URL to your Twitter profile.', verbose_name='Twitter URL')), - ('width', models.PositiveIntegerField(blank=True, editable=False, null=True, verbose_name='Image Width')), - ('youtube_url', models.URLField(blank=True, help_text='URL to your Youtube channel.', verbose_name='Youtube URL')), - ('is_active', models.BooleanField(default=True, verbose_name='Is active?')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=128, verbose_name="Name")), + ( + "email", + models.EmailField( + blank=True, max_length=254, verbose_name="E-Mail" + ), + ), + ( + "website", + models.URLField( + blank=True, + help_text="URL to your personal website.", + verbose_name="Website", + ), + ), + ( + "biography", + models.TextField( + blank=True, + help_text="Describe yourself and your fields of interest in a few" + " sentences.", + null=True, + verbose_name="Biography", + ), + ), + ( + "cba_url", + models.URLField( + blank=True, + help_text="URL to your CBA profile.", + verbose_name="CBA URL", + ), + ), + ( + "dorftv_url", + models.URLField( + blank=True, + help_text="URL to your dorfTV channel.", + verbose_name="DorfTV URL", + ), + ), + ( + "facebook_url", + models.URLField( + blank=True, + help_text="URL to your Facebook profile.", + verbose_name="Facebook URL", + ), + ), + ( + "googleplus_url", + models.URLField( + blank=True, + help_text="URL to your Google+ profile.", + verbose_name="Google+ URL", + ), + ), + ( + "height", + models.PositiveIntegerField( + blank=True, + editable=False, + null=True, + verbose_name="Image Height", + ), + ), + ( + "image", + versatileimagefield.fields.VersatileImageField( + blank=True, + height_field="height", + help_text="Upload a picture of yourself. Images are automatically cropped" + " around the 'Primary Point of Interest'. Click in the image to" + " change it and press Save.", + null=True, + upload_to="host_images", + verbose_name="Profile picture", + width_field="width", + ), + ), + ( + "linkedin_url", + models.URLField( + blank=True, + help_text="URL to your LinkedIn profile.", + verbose_name="LinkedIn URL", + ), + ), + ( + "ppoi", + versatileimagefield.fields.PPOIField( + default="0.5x0.5", + editable=False, + max_length=20, + verbose_name="Image PPOI", + ), + ), + ( + "twitter_url", + models.URLField( + blank=True, + help_text="URL to your Twitter profile.", + verbose_name="Twitter URL", + ), + ), + ( + "width", + models.PositiveIntegerField( + blank=True, + editable=False, + null=True, + verbose_name="Image Width", + ), + ), + ( + "youtube_url", + models.URLField( + blank=True, + help_text="URL to your Youtube channel.", + verbose_name="Youtube URL", + ), + ), + ( + "is_active", + models.BooleanField(default=True, verbose_name="Is active?"), + ), ], options={ - 'ordering': ('name',), - 'verbose_name': 'Host', - 'verbose_name_plural': 'Hosts', + "ordering": ("name",), + "verbose_name": "Host", + "verbose_name_plural": "Hosts", }, ), migrations.CreateModel( - name='MusicFocus', + name="MusicFocus", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('focus', models.CharField(max_length=32, verbose_name='Focus')), - ('abbrev', models.CharField(max_length=4, unique=True, verbose_name='Abbreviation')), - ('slug', models.SlugField(max_length=32, unique=True, verbose_name='Slug')), - ('button', models.ImageField(blank=True, null=True, upload_to='buttons', verbose_name='Button image')), - ('button_hover', models.ImageField(blank=True, null=True, upload_to='buttons', verbose_name='Button image (hover)')), - ('big_button', models.ImageField(blank=True, null=True, upload_to='buttons', verbose_name='Big button image')), - ('is_active', models.BooleanField(default=True, verbose_name='Is active?')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("focus", models.CharField(max_length=32, verbose_name="Focus")), + ( + "abbrev", + models.CharField( + max_length=4, unique=True, verbose_name="Abbreviation" + ), + ), + ( + "slug", + models.SlugField(max_length=32, unique=True, verbose_name="Slug"), + ), + ( + "button", + models.ImageField( + blank=True, + null=True, + upload_to="buttons", + verbose_name="Button image", + ), + ), + ( + "button_hover", + models.ImageField( + blank=True, + null=True, + upload_to="buttons", + verbose_name="Button image (hover)", + ), + ), + ( + "big_button", + models.ImageField( + blank=True, + null=True, + upload_to="buttons", + verbose_name="Big button image", + ), + ), + ( + "is_active", + models.BooleanField(default=True, verbose_name="Is active?"), + ), ], options={ - 'ordering': ('focus',), - 'verbose_name': 'Music focus', - 'verbose_name_plural': 'Music focus', + "ordering": ("focus",), + "verbose_name": "Music focus", + "verbose_name_plural": "Music focus", }, ), migrations.CreateModel( - name='RRule', + name="RRule", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=32, unique=True, verbose_name='Name')), - ('freq', models.IntegerField(choices=[(1, 'Monthly'), (2, 'Weekly'), (3, 'Daily')], verbose_name='Frequency')), - ('interval', models.IntegerField(default=1, verbose_name='Interval')), - ('bysetpos', models.IntegerField(blank=True, choices=[(1, 'First'), (2, 'Second'), (3, 'Third'), (4, 'Fourth'), (5, 'Fifth'), (-1, 'Last')], null=True, verbose_name='Set position')), - ('count', models.IntegerField(blank=True, null=True, verbose_name='Count')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "name", + models.CharField(max_length=32, unique=True, verbose_name="Name"), + ), + ( + "freq", + models.IntegerField( + choices=[(1, "Monthly"), (2, "Weekly"), (3, "Daily")], + verbose_name="Frequency", + ), + ), + ("interval", models.IntegerField(default=1, verbose_name="Interval")), + ( + "bysetpos", + models.IntegerField( + blank=True, + choices=[ + (1, "First"), + (2, "Second"), + (3, "Third"), + (4, "Fourth"), + (5, "Fifth"), + (-1, "Last"), + ], + null=True, + verbose_name="Set position", + ), + ), + ( + "count", + models.IntegerField(blank=True, null=True, verbose_name="Count"), + ), ], options={ - 'ordering': ('pk',), - 'verbose_name': 'Recurrence rule', - 'verbose_name_plural': 'Recurrence rules', + "ordering": ("pk",), + "verbose_name": "Recurrence rule", + "verbose_name_plural": "Recurrence rules", }, ), migrations.CreateModel( - name='Show', + name="Show", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(help_text="The show's name. Avoid a subtitle.", max_length=255, verbose_name='Name')), - ('slug', models.CharField(help_text='A simple to read URL for your show', max_length=255, unique=True, verbose_name='Slug')), - ('image', versatileimagefield.fields.VersatileImageField(blank=True, height_field='height', help_text="Upload an image to your show. Images are automatically cropped around the 'Primary Point of Interest'. Click in the image to change it and press Save.", null=True, upload_to='show_images', verbose_name='Image', width_field='width')), - ('short_description', models.TextField(help_text='Describe your show in some sentences. Avoid technical data like airing times and contact information. They will be added automatically.', verbose_name='Short description')), - ('description', models.TextField(blank=True, help_text='Describe your show in detail.', null=True, verbose_name='Description')), - ('email', models.EmailField(blank=True, help_text='The main contact email address for your show.', max_length=254, null=True, verbose_name='E-Mail')), - ('website', models.URLField(blank=True, help_text='Is there a website to your show? Type in its URL.', null=True, verbose_name='Website')), - ('created', models.DateTimeField(auto_now_add=True)), - ('last_updated', models.DateTimeField(auto_now=True)), - ('hosts', models.ManyToManyField(blank=True, related_name='shows', to='program.Host', verbose_name='Hosts')), - ('musicfocus', models.ManyToManyField(blank=True, related_name='shows', to='program.MusicFocus', verbose_name='Music focus')), - ('owners', models.ManyToManyField(blank=True, related_name='shows', to=settings.AUTH_USER_MODEL, verbose_name='Owners')), - ('predecessor', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='successors', to='program.Show', verbose_name='Predecessor')), - ('cba_series_id', models.IntegerField(blank=True, help_text='Link your show to a CBA series by giving its ID. This will enable CBA upload and will automatically link your show to your CBA archive. Find out your ID under https://cba.fro.at/series', null=True, verbose_name='CBA Series ID')), - ('fallback_id', models.IntegerField(blank=True, null=True, verbose_name='Fallback ID')), - ('height', models.PositiveIntegerField(blank=True, editable=False, null=True, verbose_name='Image Height')), - ('logo', models.ImageField(blank=True, null=True, upload_to='show_images', verbose_name='Logo')), - ('ppoi', versatileimagefield.fields.PPOIField(default='0.5x0.5', editable=False, max_length=20, verbose_name='Image PPOI')), - ('width', models.PositiveIntegerField(blank=True, editable=False, null=True, verbose_name='Image Width')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "name", + models.CharField( + help_text="The show's name. Avoid a subtitle.", + max_length=255, + verbose_name="Name", + ), + ), + ( + "slug", + models.CharField( + help_text="A simple to read URL for your show", + max_length=255, + unique=True, + verbose_name="Slug", + ), + ), + ( + "image", + versatileimagefield.fields.VersatileImageField( + blank=True, + height_field="height", + help_text="Upload an image to your show. Images are automatically cropped" + " around the 'Primary Point of Interest'. Click in the image to" + " change it and press Save.", + null=True, + upload_to="show_images", + verbose_name="Image", + width_field="width", + ), + ), + ( + "short_description", + models.TextField( + help_text="Describe your show in some sentences. Avoid technical data like" + " airing times and contact information. They will be added" + " automatically.", + verbose_name="Short description", + ), + ), + ( + "description", + models.TextField( + blank=True, + help_text="Describe your show in detail.", + null=True, + verbose_name="Description", + ), + ), + ( + "email", + models.EmailField( + blank=True, + help_text="The main contact email address for your show.", + max_length=254, + null=True, + verbose_name="E-Mail", + ), + ), + ( + "website", + models.URLField( + blank=True, + help_text="Is there a website to your show? Type in its URL.", + null=True, + verbose_name="Website", + ), + ), + ("created", models.DateTimeField(auto_now_add=True)), + ("last_updated", models.DateTimeField(auto_now=True)), + ( + "hosts", + models.ManyToManyField( + blank=True, + related_name="shows", + to="program.Host", + verbose_name="Hosts", + ), + ), + ( + "musicfocus", + models.ManyToManyField( + blank=True, + related_name="shows", + to="program.MusicFocus", + verbose_name="Music focus", + ), + ), + ( + "owners", + models.ManyToManyField( + blank=True, + related_name="shows", + to=settings.AUTH_USER_MODEL, + verbose_name="Owners", + ), + ), + ( + "predecessor", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="successors", + to="program.Show", + verbose_name="Predecessor", + ), + ), + ( + "cba_series_id", + models.IntegerField( + blank=True, + help_text="Link your show to a CBA series by giving its ID. This will" + " enable CBA upload and will automatically link your show to" + " your CBA archive. Find out your ID under" + " https://cba.fro.at/series", + null=True, + verbose_name="CBA Series ID", + ), + ), + ( + "fallback_id", + models.IntegerField( + blank=True, null=True, verbose_name="Fallback ID" + ), + ), + ( + "height", + models.PositiveIntegerField( + blank=True, + editable=False, + null=True, + verbose_name="Image Height", + ), + ), + ( + "logo", + models.ImageField( + blank=True, + null=True, + upload_to="show_images", + verbose_name="Logo", + ), + ), + ( + "ppoi", + versatileimagefield.fields.PPOIField( + default="0.5x0.5", + editable=False, + max_length=20, + verbose_name="Image PPOI", + ), + ), + ( + "width", + models.PositiveIntegerField( + blank=True, + editable=False, + null=True, + verbose_name="Image Width", + ), + ), ], options={ - 'ordering': ('slug',), - 'verbose_name': 'Show', - 'verbose_name_plural': 'Shows', + "ordering": ("slug",), + "verbose_name": "Show", + "verbose_name_plural": "Shows", }, ), migrations.CreateModel( - name='TimeSlot', + name="TimeSlot", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('start', models.DateTimeField(verbose_name='Start time')), - ('end', models.DateTimeField(verbose_name='End time')), - ('show', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='timeslots', to='program.Show')), - ('is_repetition', models.BooleanField(default=False, verbose_name='REP')), - ('memo', models.TextField(blank=True, verbose_name='Memo')), - ('playlist_id', models.IntegerField(null=True, verbose_name='Playlist ID')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("start", models.DateTimeField(verbose_name="Start time")), + ("end", models.DateTimeField(verbose_name="End time")), + ( + "show", + models.ForeignKey( + editable=False, + on_delete=django.db.models.deletion.CASCADE, + related_name="timeslots", + to="program.Show", + ), + ), + ( + "is_repetition", + models.BooleanField(default=False, verbose_name="REP"), + ), + ("memo", models.TextField(blank=True, verbose_name="Memo")), + ( + "playlist_id", + models.IntegerField(null=True, verbose_name="Playlist ID"), + ), ], options={ - 'ordering': ('start', 'end'), - 'verbose_name': 'Time slot', - 'verbose_name_plural': 'Time slots', + "ordering": ("start", "end"), + "verbose_name": "Time slot", + "verbose_name_plural": "Time slots", }, ), migrations.CreateModel( - name='Category', + name="Category", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('category', models.CharField(max_length=32, verbose_name='Category')), - ('abbrev', models.CharField(max_length=4, unique=True, verbose_name='Abbreviation')), - ('slug', models.SlugField(max_length=32, unique=True, verbose_name='Slug')), - ('color', models.TextField(blank=True, max_length=7, verbose_name='Color')), - ('description', models.TextField(blank=True, verbose_name='Description')), - ('button', models.ImageField(blank=True, null=True, upload_to='buttons', verbose_name='Button image')), - ('button_hover', models.ImageField(blank=True, null=True, upload_to='buttons', verbose_name='Button image (hover)')), - ('big_button', models.ImageField(blank=True, null=True, upload_to='buttons', verbose_name='Big button image')), - ('is_active', models.BooleanField(default=True, verbose_name='Is active?')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("category", models.CharField(max_length=32, verbose_name="Category")), + ( + "abbrev", + models.CharField( + max_length=4, unique=True, verbose_name="Abbreviation" + ), + ), + ( + "slug", + models.SlugField(max_length=32, unique=True, verbose_name="Slug"), + ), + ( + "color", + models.TextField(blank=True, max_length=7, verbose_name="Color"), + ), + ( + "description", + models.TextField(blank=True, verbose_name="Description"), + ), + ( + "button", + models.ImageField( + blank=True, + null=True, + upload_to="buttons", + verbose_name="Button image", + ), + ), + ( + "button_hover", + models.ImageField( + blank=True, + null=True, + upload_to="buttons", + verbose_name="Button image (hover)", + ), + ), + ( + "big_button", + models.ImageField( + blank=True, + null=True, + upload_to="buttons", + verbose_name="Big button image", + ), + ), + ( + "is_active", + models.BooleanField(default=True, verbose_name="Is active?"), + ), ], options={ - 'verbose_name': 'Category', - 'verbose_name_plural': 'Categories', - 'ordering': ('category',), + "verbose_name": "Category", + "verbose_name_plural": "Categories", + "ordering": ("category",), }, ), migrations.CreateModel( - name='Language', + name="Language", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=32, verbose_name='Language')), - ('is_active', models.BooleanField(default=True, verbose_name='Is active?')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=32, verbose_name="Language")), + ( + "is_active", + models.BooleanField(default=True, verbose_name="Is active?"), + ), ], options={ - 'verbose_name': 'Language', - 'verbose_name_plural': 'Languages', - 'ordering': ('language',), + "verbose_name": "Language", + "verbose_name_plural": "Languages", + "ordering": ("language",), }, ), migrations.CreateModel( - name='Schedule', + name="Schedule", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('byweekday', models.IntegerField(choices=[(0, 'Monday'), (1, 'Tuesday'), (2, 'Wednesday'), (3, 'Thursday'), (4, 'Friday'), (5, 'Saturday'), (6, 'Sunday')], verbose_name='Weekday')), - ('dstart', models.DateField(verbose_name='First date')), - ('tstart', models.TimeField(verbose_name='Start time')), - ('tend', models.TimeField(verbose_name='End time')), - ('until', models.DateField(verbose_name='Last date')), - ('is_repetition', models.BooleanField(default=False, verbose_name='Is repetition')), - ('fallback_id', models.IntegerField(blank=True, null=True, verbose_name='Fallback ID')), - ('automation_id', models.IntegerField(blank=True, null=True, verbose_name='Automation ID')), - ('created', models.DateTimeField(auto_now_add=True, null=True)), - ('last_updated', models.DateTimeField(auto_now=True, null=True)), - ('rrule', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='schedules', to='program.RRule', verbose_name='Recurrence rule')), - ('show', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='schedules', to='program.Show', verbose_name='Show')), - ('add_business_days_only', models.BooleanField(default=False, verbose_name='Only add business days?')), - ('add_days_no', models.IntegerField(blank=True, null=True, verbose_name='Add days')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "byweekday", + models.IntegerField( + choices=[ + (0, "Monday"), + (1, "Tuesday"), + (2, "Wednesday"), + (3, "Thursday"), + (4, "Friday"), + (5, "Saturday"), + (6, "Sunday"), + ], + verbose_name="Weekday", + ), + ), + ("dstart", models.DateField(verbose_name="First date")), + ("tstart", models.TimeField(verbose_name="Start time")), + ("tend", models.TimeField(verbose_name="End time")), + ("until", models.DateField(verbose_name="Last date")), + ( + "is_repetition", + models.BooleanField(default=False, verbose_name="Is repetition"), + ), + ( + "fallback_id", + models.IntegerField( + blank=True, null=True, verbose_name="Fallback ID" + ), + ), + ( + "automation_id", + models.IntegerField( + blank=True, null=True, verbose_name="Automation ID" + ), + ), + ("created", models.DateTimeField(auto_now_add=True, null=True)), + ("last_updated", models.DateTimeField(auto_now=True, null=True)), + ( + "rrule", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="schedules", + to="program.RRule", + verbose_name="Recurrence rule", + ), + ), + ( + "show", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="schedules", + to="program.Show", + verbose_name="Show", + ), + ), + ( + "add_business_days_only", + models.BooleanField( + default=False, verbose_name="Only add business days?" + ), + ), + ( + "add_days_no", + models.IntegerField(blank=True, null=True, verbose_name="Add days"), + ), ], options={ - 'verbose_name': 'Schedule', - 'verbose_name_plural': 'Schedules', - 'ordering': ('dstart', 'tstart'), + "verbose_name": "Schedule", + "verbose_name_plural": "Schedules", + "ordering": ("dstart", "tstart"), }, ), migrations.CreateModel( - name='Topic', + name="Topic", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('topic', models.CharField(max_length=32, verbose_name='Topic')), - ('abbrev', models.CharField(max_length=4, unique=True, verbose_name='Abbreviation')), - ('slug', models.SlugField(max_length=32, unique=True, verbose_name='Slug')), - ('button', models.ImageField(blank=True, null=True, upload_to='buttons', verbose_name='Button image')), - ('button_hover', models.ImageField(blank=True, null=True, upload_to='buttons', verbose_name='Button image (hover)')), - ('big_button', models.ImageField(blank=True, null=True, upload_to='buttons', verbose_name='Big button image')), - ('is_active', models.BooleanField(default=True, verbose_name='Is active?')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("topic", models.CharField(max_length=32, verbose_name="Topic")), + ( + "abbrev", + models.CharField( + max_length=4, unique=True, verbose_name="Abbreviation" + ), + ), + ( + "slug", + models.SlugField(max_length=32, unique=True, verbose_name="Slug"), + ), + ( + "button", + models.ImageField( + blank=True, + null=True, + upload_to="buttons", + verbose_name="Button image", + ), + ), + ( + "button_hover", + models.ImageField( + blank=True, + null=True, + upload_to="buttons", + verbose_name="Button image (hover)", + ), + ), + ( + "big_button", + models.ImageField( + blank=True, + null=True, + upload_to="buttons", + verbose_name="Big button image", + ), + ), + ( + "is_active", + models.BooleanField(default=True, verbose_name="Is active?"), + ), ], options={ - 'verbose_name': 'Topic', - 'verbose_name_plural': 'Topics', - 'ordering': ('topic',), + "verbose_name": "Topic", + "verbose_name_plural": "Topics", + "ordering": ("topic",), }, ), migrations.CreateModel( - name='Type', + name="Type", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('type', models.CharField(max_length=32, verbose_name='Type')), - ('slug', models.SlugField(max_length=32, unique=True, verbose_name='Slug')), - ('color', models.CharField(default='#ffffff', max_length=7, verbose_name='Color')), - ('text_color', models.CharField(default='#000000', max_length=7, verbose_name='Text color')), - ('is_active', models.BooleanField(default=True, verbose_name='Is active?')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("type", models.CharField(max_length=32, verbose_name="Type")), + ( + "slug", + models.SlugField(max_length=32, unique=True, verbose_name="Slug"), + ), + ( + "color", + models.CharField( + default="#ffffff", max_length=7, verbose_name="Color" + ), + ), + ( + "text_color", + models.CharField( + default="#000000", max_length=7, verbose_name="Text color" + ), + ), + ( + "is_active", + models.BooleanField(default=True, verbose_name="Is active?"), + ), ], options={ - 'verbose_name': 'Type', - 'verbose_name_plural': 'Types', - 'ordering': ('type',), + "verbose_name": "Type", + "verbose_name_plural": "Types", + "ordering": ("type",), }, ), migrations.CreateModel( - name='Note', + name="Note", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('title', models.CharField(help_text="Give your note a good headline. What will your upcoming show be about? Try to arouse interest to listen to it!<br>Avoid technical data like the show's name, its airing times or its episode number. These data are added automatically.", max_length=128, verbose_name='Title')), - ('content', models.TextField(help_text='Describe your upcoming show in detail.', verbose_name='Content')), - ('status', models.IntegerField(choices=[(0, 'Cancellation'), (1, 'Recommendation'), (2, 'Repetition')], default=1, verbose_name='Status')), - ('start', models.DateTimeField(editable=False)), - ('created', models.DateTimeField(auto_now_add=True)), - ('last_updated', models.DateTimeField(auto_now=True)), - ('show', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notes', to='program.Show')), - ('timeslot', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='program.TimeSlot', verbose_name='Time slot')), - ('audio_url', models.TextField(blank=True, editable=False, verbose_name='Direct URL to a linked audio file')), - ('cba_id', models.IntegerField(blank=True, help_text="Link the note to a certain CBA post by giving its ID. (E.g. if your post's CBA URL is https://cba.fro.at/1234, then your CBA ID is 1234)", null=True, verbose_name='CBA ID')), - ('height', models.PositiveIntegerField(blank=True, editable=False, null=True, verbose_name='Image Height')), - ('host', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='hosts', to='program.Host')), - ('image', versatileimagefield.fields.VersatileImageField(blank=True, height_field='height', help_text="Upload an image to your note. Images are automatically cropped around the 'Primary Point of Interest'. Click in the image to change it and press Save.", null=True, upload_to='note_images', verbose_name='Featured image', width_field='width')), - ('ppoi', versatileimagefield.fields.PPOIField(default='0.5x0.5', editable=False, max_length=20, verbose_name='Image PPOI')), - ('slug', models.SlugField(default=1, help_text='A simple to read URL for your show.', max_length=32, unique=True, verbose_name='Slug')), - ('summary', models.TextField(blank=True, help_text='Describe your upcoming show in some sentences. Avoid technical data like airing times and contact information. They will be added automatically.', verbose_name='Summary')), - ('user', models.ForeignKey(default=1, editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='users', to=settings.AUTH_USER_MODEL)), - ('width', models.PositiveIntegerField(blank=True, editable=False, null=True, verbose_name='Image Width')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "title", + models.CharField( + help_text="Give your note a good headline. What will your upcoming show be" + " about? Try to arouse interest to listen to it!<br>Avoid" + " technical data like the show's name, its airing times or its" + " episode number. These data are added automatically.", + max_length=128, + verbose_name="Title", + ), + ), + ( + "content", + models.TextField( + help_text="Describe your upcoming show in detail.", + verbose_name="Content", + ), + ), + ( + "status", + models.IntegerField( + choices=[ + (0, "Cancellation"), + (1, "Recommendation"), + (2, "Repetition"), + ], + default=1, + verbose_name="Status", + ), + ), + ("start", models.DateTimeField(editable=False)), + ("created", models.DateTimeField(auto_now_add=True)), + ("last_updated", models.DateTimeField(auto_now=True)), + ( + "show", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="notes", + to="program.Show", + ), + ), + ( + "timeslot", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + to="program.TimeSlot", + verbose_name="Time slot", + ), + ), + ( + "audio_url", + models.TextField( + blank=True, + editable=False, + verbose_name="Direct URL to a linked audio file", + ), + ), + ( + "cba_id", + models.IntegerField( + blank=True, + help_text="Link the note to a certain CBA post by giving its ID. (E.g. if" + " your post's CBA URL is https://cba.fro.at/1234, then your CBA" + " ID is 1234)", + null=True, + verbose_name="CBA ID", + ), + ), + ( + "height", + models.PositiveIntegerField( + blank=True, + editable=False, + null=True, + verbose_name="Image Height", + ), + ), + ( + "host", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="hosts", + to="program.Host", + ), + ), + ( + "image", + versatileimagefield.fields.VersatileImageField( + blank=True, + height_field="height", + help_text="Upload an image to your note. Images are automatically cropped" + " around the 'Primary Point of Interest'. Click in the image to" + " change it and press Save.", + null=True, + upload_to="note_images", + verbose_name="Featured image", + width_field="width", + ), + ), + ( + "ppoi", + versatileimagefield.fields.PPOIField( + default="0.5x0.5", + editable=False, + max_length=20, + verbose_name="Image PPOI", + ), + ), + ( + "slug", + models.SlugField( + default=1, + help_text="A simple to read URL for your show.", + max_length=32, + unique=True, + verbose_name="Slug", + ), + ), + ( + "summary", + models.TextField( + blank=True, + help_text="Describe your upcoming show in some sentences. Avoid technical" + " data like airing times and contact information. They will be" + " added automatically.", + verbose_name="Summary", + ), + ), + ( + "user", + models.ForeignKey( + default=1, + editable=False, + on_delete=django.db.models.deletion.CASCADE, + related_name="users", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "width", + models.PositiveIntegerField( + blank=True, + editable=False, + null=True, + verbose_name="Image Width", + ), + ), ], options={ - 'ordering': ('timeslot',), - 'verbose_name': 'Note', - 'verbose_name_plural': 'Notes', + "ordering": ("timeslot",), + "verbose_name": "Note", + "verbose_name_plural": "Notes", }, ), migrations.AddField( - model_name='show', - name='category', - field=models.ManyToManyField(blank=True, related_name='shows', to='program.Category', verbose_name='Category'), + model_name="show", + name="category", + field=models.ManyToManyField( + blank=True, + related_name="shows", + to="program.Category", + verbose_name="Category", + ), ), migrations.AddField( - model_name='show', - name='language', - field=models.ManyToManyField(blank=True, related_name='language', to='program.Language', verbose_name='Language'), + model_name="show", + name="language", + field=models.ManyToManyField( + blank=True, + related_name="language", + to="program.Language", + verbose_name="Language", + ), ), migrations.AddField( - model_name='show', - name='topic', - field=models.ManyToManyField(blank=True, related_name='shows', to='program.Topic', verbose_name='Topic'), + model_name="show", + name="topic", + field=models.ManyToManyField( + blank=True, + related_name="shows", + to="program.Topic", + verbose_name="Topic", + ), ), migrations.AddField( - model_name='show', - name='type', - field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='shows', to='program.Type', verbose_name='Type'), + model_name="show", + name="type", + field=models.ForeignKey( + default=1, + on_delete=django.db.models.deletion.CASCADE, + related_name="shows", + to="program.Type", + verbose_name="Type", + ), preserve_default=False, ), migrations.AddField( - model_name='timeslot', - name='schedule', - field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='timeslots', to='program.Schedule', verbose_name='Schedule'), + model_name="timeslot", + name="schedule", + field=models.ForeignKey( + default=1, + on_delete=django.db.models.deletion.CASCADE, + related_name="timeslots", + to="program.Schedule", + verbose_name="Schedule", + ), preserve_default=False, ), migrations.AlterField( - model_name='timeslot', - name='is_repetition', - field=models.BooleanField(default=False, verbose_name='(REP)'), + model_name="timeslot", + name="is_repetition", + field=models.BooleanField(default=False, verbose_name="(REP)"), ), migrations.CreateModel( - name='FundingCategory', + name="FundingCategory", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('fundingcategory', models.CharField(max_length=32, verbose_name='Funding Category')), - ('abbrev', models.CharField(max_length=4, unique=True, verbose_name='Abbreviation')), - ('slug', models.SlugField(max_length=32, unique=True, verbose_name='Slug')), - ('is_active', models.BooleanField(default=True, verbose_name='Is active?')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "fundingcategory", + models.CharField(max_length=32, verbose_name="Funding Category"), + ), + ( + "abbrev", + models.CharField( + max_length=4, unique=True, verbose_name="Abbreviation" + ), + ), + ( + "slug", + models.SlugField(max_length=32, unique=True, verbose_name="Slug"), + ), + ( + "is_active", + models.BooleanField(default=True, verbose_name="Is active?"), + ), ], options={ - 'verbose_name': 'Funding Category', - 'verbose_name_plural': 'Funding Categories', - 'ordering': ('fundingcategory',), + "verbose_name": "Funding Category", + "verbose_name_plural": "Funding Categories", + "ordering": ("fundingcategory",), }, ), migrations.AddField( - model_name='timeslot', - name='note_id', - field=models.IntegerField(editable=False, null=True, verbose_name='Note ID'), + model_name="timeslot", + name="note_id", + field=models.IntegerField( + editable=False, null=True, verbose_name="Note ID" + ), ), migrations.AlterField( - model_name='timeslot', - name='is_repetition', - field=models.BooleanField(default=False, verbose_name='Is repetition?'), + model_name="timeslot", + name="is_repetition", + field=models.BooleanField(default=False, verbose_name="Is repetition?"), ), migrations.AlterField( - model_name='show', - name='cba_series_id', - field=models.IntegerField(blank=True, help_text="Link your show to a CBA series by giving its ID. This will enable CBA upload and will automatically link your show to your CBA archive. Find out your show's ID under https://cba.fro.at/series", null=True, verbose_name='CBA Series ID'), + model_name="show", + name="cba_series_id", + field=models.IntegerField( + blank=True, + help_text="Link your show to a CBA series by giving its ID. This will enable CBA" + " upload and will automatically link your show to your CBA archive. Find" + " out your show's ID under https://cba.fro.at/series", + null=True, + verbose_name="CBA Series ID", + ), ), migrations.AlterField( - model_name='show', - name='fallback_id', - field=models.IntegerField(blank=True, help_text='If a timeslot of your show is empty, this playlist will be aired as a backup.', null=True, verbose_name='Fallback ID'), + model_name="show", + name="fallback_id", + field=models.IntegerField( + blank=True, + help_text="If a timeslot of your show is empty, this playlist will be aired as a" + " backup.", + null=True, + verbose_name="Fallback ID", + ), ), migrations.AlterField( - model_name='show', - name='logo', - field=models.ImageField(blank=True, help_text='Upload a logo of your show.', null=True, upload_to='show_images', verbose_name='Logo'), + model_name="show", + name="logo", + field=models.ImageField( + blank=True, + help_text="Upload a logo of your show.", + null=True, + upload_to="show_images", + verbose_name="Logo", + ), ), migrations.AlterField( - model_name='show', - name='short_description', - field=models.TextField(help_text='Describe your show for your listeners in some sentences. Avoid technical data like airing times and contact information. They will be added automatically.', verbose_name='Short description'), + model_name="show", + name="short_description", + field=models.TextField( + help_text="Describe your show for your listeners in some sentences. Avoid" + " technical data like airing times and contact information. They will be" + " added automatically.", + verbose_name="Short description", + ), ), migrations.AddField( - model_name='show', - name='fundingcategory', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='shows', to='program.FundingCategory', verbose_name='Funding Category'), + model_name="show", + name="fundingcategory", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="shows", + to="program.FundingCategory", + verbose_name="Funding Category", + ), ), migrations.AddField( - model_name='show', - name='is_active', - field=models.BooleanField(default=True, verbose_name='Is active?'), + model_name="show", + name="is_active", + field=models.BooleanField(default=True, verbose_name="Is active?"), ), migrations.AddField( - model_name='show', - name='is_public', - field=models.BooleanField(default=False, help_text='Files and Playlists of Public Shows can only be changed by owners but may be used by everyone.', verbose_name='Is Public?'), + model_name="show", + name="is_public", + field=models.BooleanField( + default=False, + help_text="Files and Playlists of Public Shows can only be changed by owners but" + " may be used by everyone.", + verbose_name="Is Public?", + ), ), ] diff --git a/program/migrations/0002_auto_20210426_2345.py b/program/migrations/0002_auto_20210426_2345.py index 4d67419dded6a1761da498a4a4869b48a8affb08..12d9e83f8d680ad8ed427180dd2db47a1a55367a 100644 --- a/program/migrations/0002_auto_20210426_2345.py +++ b/program/migrations/0002_auto_20210426_2345.py @@ -6,18 +6,14 @@ from django.db import migrations class Migration(migrations.Migration): dependencies = [ - ('program', '0001_squashed'), + ("program", "0001_squashed"), ] operations = [ migrations.RenameField( - model_name='schedule', - old_name='fallback_id', - new_name='default_id' + model_name="schedule", old_name="fallback_id", new_name="default_id" ), migrations.RenameField( - model_name='show', - old_name='fallback_id', - new_name='default_id' + model_name="show", old_name="fallback_id", new_name="default_id" ), ] diff --git a/program/migrations/0002_host_is_always_visible.py b/program/migrations/0002_host_is_always_visible.py index 9abf2bdc3474a1e7a4f72c8ce794d90af18bde69..c2cb3345ca92a17f28398bff490acfc906ecbd6e 100644 --- a/program/migrations/0002_host_is_always_visible.py +++ b/program/migrations/0002_host_is_always_visible.py @@ -7,13 +7,13 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('program', '0001_initial'), + ("program", "0001_initial"), ] operations = [ migrations.AddField( - model_name='host', - name='is_always_visible', - field=models.BooleanField(default=False, verbose_name='Is always visible'), + model_name="host", + name="is_always_visible", + field=models.BooleanField(default=False, verbose_name="Is always visible"), ), ] diff --git a/program/migrations/0003_auto_20210713_0420.py b/program/migrations/0003_auto_20210713_0420.py index 88314acb966c742fcfffba664e8b59a8a37e7064..ab1047726171190c774bd382992f253e110ed431 100644 --- a/program/migrations/0003_auto_20210713_0420.py +++ b/program/migrations/0003_auto_20210713_0420.py @@ -6,18 +6,14 @@ from django.db import migrations class Migration(migrations.Migration): dependencies = [ - ('program', '0002_auto_20210426_2345'), + ("program", "0002_auto_20210426_2345"), ] operations = [ migrations.RenameField( - model_name='schedule', - old_name='default_id', - new_name='default_playlist_id' + model_name="schedule", old_name="default_id", new_name="default_playlist_id" ), migrations.RenameField( - model_name='show', - old_name='default_id', - new_name='default_playlist_id' + model_name="show", old_name="default_id", new_name="default_playlist_id" ), ] diff --git a/program/migrations/0003_host_is_active.py b/program/migrations/0003_host_is_active.py index 28c10498165c454ea33dfa51df3bbd3e5c12dab6..ef977a8827de9b71e95f7a27a8116d043736e328 100644 --- a/program/migrations/0003_host_is_active.py +++ b/program/migrations/0003_host_is_active.py @@ -7,13 +7,15 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('program', '0002_host_is_always_visible'), + ("program", "0002_host_is_always_visible"), ] operations = [ migrations.AddField( - model_name='host', - name='is_active', - field=models.BooleanField(default=True, verbose_name='Is active', editable=False), + model_name="host", + name="is_active", + field=models.BooleanField( + default=True, verbose_name="Is active", editable=False + ), ), ] diff --git a/program/migrations/0004_auto_20220111_1806.py b/program/migrations/0004_auto_20220111_1806.py index 2a7706ec083650189d750ffe644cda877fe57dda..17cf8ad1f3d09ffad4f6c0821e67000b8e744b2c 100644 --- a/program/migrations/0004_auto_20220111_1806.py +++ b/program/migrations/0004_auto_20220111_1806.py @@ -6,59 +6,72 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('program', '0003_auto_20210713_0420'), + ("program", "0003_auto_20210713_0420"), ] operations = [ migrations.RemoveField( - model_name='category', - name='big_button', + model_name="category", + name="big_button", ), migrations.RemoveField( - model_name='category', - name='button', + model_name="category", + name="button", ), migrations.RemoveField( - model_name='category', - name='button_hover', + model_name="category", + name="button_hover", ), migrations.RemoveField( - model_name='musicfocus', - name='big_button', + model_name="musicfocus", + name="big_button", ), migrations.RemoveField( - model_name='musicfocus', - name='button', + model_name="musicfocus", + name="button", ), migrations.RemoveField( - model_name='musicfocus', - name='button_hover', + model_name="musicfocus", + name="button_hover", ), migrations.RemoveField( - model_name='topic', - name='big_button', + model_name="topic", + name="big_button", ), migrations.RemoveField( - model_name='topic', - name='button', + model_name="topic", + name="button", ), migrations.RemoveField( - model_name='topic', - name='button_hover', + model_name="topic", + name="button_hover", ), migrations.AlterField( - model_name='note', - name='slug', - field=models.SlugField(help_text='A simple to read URL for your show.', max_length=32, unique=True, verbose_name='Slug'), + model_name="note", + name="slug", + field=models.SlugField( + help_text="A simple to read URL for your show.", + max_length=32, + unique=True, + verbose_name="Slug", + ), ), migrations.AlterField( - model_name='schedule', - name='default_playlist_id', - field=models.IntegerField(blank=True, null=True, verbose_name='Default Playlist ID'), + model_name="schedule", + name="default_playlist_id", + field=models.IntegerField( + blank=True, null=True, verbose_name="Default Playlist ID" + ), ), migrations.AlterField( - model_name='show', - name='default_playlist_id', - field=models.IntegerField(blank=True, help_text='If a timeslot of your show is empty, this playlist will be aired as a backup.', null=True, verbose_name='Default Playlist ID'), + model_name="show", + name="default_playlist_id", + field=models.IntegerField( + blank=True, + help_text="If a timeslot of your show is empty, this playlist will be aired as a" + " backup.", + null=True, + verbose_name="Default Playlist ID", + ), ), ] diff --git a/program/migrations/0004_show_is_active.py b/program/migrations/0004_show_is_active.py index 26e32052e8c0d8e1851c41e891f69c4afbbfaef8..2d27dcae933a2fc1ec6e5e7186352677f84d4de5 100644 --- a/program/migrations/0004_show_is_active.py +++ b/program/migrations/0004_show_is_active.py @@ -7,13 +7,15 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('program', '0003_host_is_active'), + ("program", "0003_host_is_active"), ] operations = [ migrations.AddField( - model_name='show', - name='is_active', - field=models.BooleanField(default=True, verbose_name='Is active', editable=False), + model_name="show", + name="is_active", + field=models.BooleanField( + default=True, verbose_name="Is active", editable=False + ), ), ] diff --git a/program/migrations/0005_auto_20220111_2251.py b/program/migrations/0005_auto_20220111_2251.py index 7e8f05762640cf12de5002ed54faa0a33f8ae5b0..2d6a50f419e8ce0557fde4e82094ee9ea420e631 100644 --- a/program/migrations/0005_auto_20220111_2251.py +++ b/program/migrations/0005_auto_20220111_2251.py @@ -6,16 +6,16 @@ from django.db import migrations class Migration(migrations.Migration): dependencies = [ - ('program', '0004_auto_20220111_1806'), + ("program", "0004_auto_20220111_1806"), ] operations = [ migrations.RemoveField( - model_name='type', - name='color', + model_name="type", + name="color", ), migrations.RemoveField( - model_name='type', - name='text_color', + model_name="type", + name="text_color", ), ] diff --git a/program/migrations/0005_programslot_is_active.py b/program/migrations/0005_programslot_is_active.py index 31975c8034e4494d03a907f8bb70c7a48f3c60ac..04ca314a26ad6e4178b0dfdc0765bcb6e5c2323b 100644 --- a/program/migrations/0005_programslot_is_active.py +++ b/program/migrations/0005_programslot_is_active.py @@ -7,13 +7,15 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('program', '0004_show_is_active'), + ("program", "0004_show_is_active"), ] operations = [ migrations.AddField( - model_name='programslot', - name='is_active', - field=models.BooleanField(default=True, verbose_name='Is active', editable=False), + model_name="programslot", + name="is_active", + field=models.BooleanField( + default=True, verbose_name="Is active", editable=False + ), ), ] diff --git a/program/migrations/0006_note_remove_cba_entry_id.py b/program/migrations/0006_note_remove_cba_entry_id.py index d04be5732e78e534bbe00d36d88fa86c766959ad..3d478b2a4e307f0473ce627e68814c5ae1d82afa 100644 --- a/program/migrations/0006_note_remove_cba_entry_id.py +++ b/program/migrations/0006_note_remove_cba_entry_id.py @@ -7,12 +7,12 @@ from django.db import migrations class Migration(migrations.Migration): dependencies = [ - ('program', '0005_programslot_is_active'), + ("program", "0005_programslot_is_active"), ] operations = [ migrations.RemoveField( - model_name='note', - name='cba_entry_id', + model_name="note", + name="cba_entry_id", ), ] diff --git a/program/migrations/0006_remove_category_color.py b/program/migrations/0006_remove_category_color.py index e7293b83261bca335b20d472cc9ae7b376d5bee4..4d09474e8c5c776f16c0d5f6409f33d9fd3a0124 100644 --- a/program/migrations/0006_remove_category_color.py +++ b/program/migrations/0006_remove_category_color.py @@ -6,12 +6,12 @@ from django.db import migrations class Migration(migrations.Migration): dependencies = [ - ('program', '0005_auto_20220111_2251'), + ("program", "0005_auto_20220111_2251"), ] operations = [ migrations.RemoveField( - model_name='category', - name='color', + model_name="category", + name="color", ), ] diff --git a/program/migrations/0007_auto_20220112_2335.py b/program/migrations/0007_auto_20220112_2335.py index 778ac552cd8d6de792450f113c668f272d22ff62..f635a3be8f8e0f923b09cc52805db7f5b3b7736f 100644 --- a/program/migrations/0007_auto_20220112_2335.py +++ b/program/migrations/0007_auto_20220112_2335.py @@ -6,23 +6,36 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('program', '0006_remove_category_color'), + ("program", "0006_remove_category_color"), ] operations = [ migrations.AlterField( - model_name='host', - name='biography', - field=models.TextField(blank=True, help_text='Describe yourself and your fields of interest in a few sentences.', null=True, verbose_name='Biography'), + model_name="host", + name="biography", + field=models.TextField( + blank=True, + help_text="Describe yourself and your fields of interest in a few sentences.", + null=True, + verbose_name="Biography", + ), ), migrations.AlterField( - model_name='note', - name='content', - field=models.TextField(help_text='Describe your upcoming show in detail.', verbose_name='Content'), + model_name="note", + name="content", + field=models.TextField( + help_text="Describe your upcoming show in detail.", + verbose_name="Content", + ), ), migrations.AlterField( - model_name='show', - name='description', - field=models.TextField(blank=True, help_text='Describe your show in detail.', null=True, verbose_name='Description'), + model_name="show", + name="description", + field=models.TextField( + blank=True, + help_text="Describe your show in detail.", + null=True, + verbose_name="Description", + ), ), ] diff --git a/program/migrations/0007_show_remove_cba_series_id.py b/program/migrations/0007_show_remove_cba_series_id.py index 1173b51079fdb2197814b611b08a85a6a8d8a9a4..346acbb3b6b2ad25a45c05db459f21dda36bb06b 100644 --- a/program/migrations/0007_show_remove_cba_series_id.py +++ b/program/migrations/0007_show_remove_cba_series_id.py @@ -7,12 +7,12 @@ from django.db import migrations class Migration(migrations.Migration): dependencies = [ - ('program', '0006_note_remove_cba_entry_id'), + ("program", "0006_note_remove_cba_entry_id"), ] operations = [ migrations.RemoveField( - model_name='show', - name='cba_series_id', + model_name="show", + name="cba_series_id", ), ] diff --git a/program/migrations/0008_auto_20220117_1721.py b/program/migrations/0008_auto_20220117_1721.py index 90e986aea1a68741fd59d4c8e281616cdf7660b8..6009f0392ec5a17ccdf7e3d323361eb17b0c9fcd 100644 --- a/program/migrations/0008_auto_20220117_1721.py +++ b/program/migrations/0008_auto_20220117_1721.py @@ -1,149 +1,179 @@ # Generated by Django 2.2.25 on 2022-01-17 16:21 -from django.db import migrations, models import versatileimagefield.fields +from django.db import migrations, models + class Migration(migrations.Migration): dependencies = [ - ('program', '0007_auto_20220112_2335'), + ("program", "0007_auto_20220112_2335"), ] operations = [ migrations.AlterField( - model_name='host', - name='biography', - field=models.TextField(blank=True, null=True, verbose_name='Biography'), + model_name="host", + name="biography", + field=models.TextField(blank=True, null=True, verbose_name="Biography"), ), migrations.AlterField( - model_name='host', - name='cba_url', - field=models.URLField(blank=True, verbose_name='CBA URL'), + model_name="host", + name="cba_url", + field=models.URLField(blank=True, verbose_name="CBA URL"), ), migrations.AlterField( - model_name='host', - name='dorftv_url', - field=models.URLField(blank=True, verbose_name='DorfTV URL'), + model_name="host", + name="dorftv_url", + field=models.URLField(blank=True, verbose_name="DorfTV URL"), ), migrations.AlterField( - model_name='host', - name='facebook_url', - field=models.URLField(blank=True, verbose_name='Facebook URL'), + model_name="host", + name="facebook_url", + field=models.URLField(blank=True, verbose_name="Facebook URL"), ), migrations.AlterField( - model_name='host', - name='googleplus_url', - field=models.URLField(blank=True, verbose_name='Google+ URL'), + model_name="host", + name="googleplus_url", + field=models.URLField(blank=True, verbose_name="Google+ URL"), ), migrations.AlterField( - model_name='host', - name='image', - field=versatileimagefield.fields.VersatileImageField(blank=True, height_field='height', null=True, upload_to='host_images', verbose_name='Profile picture', width_field='width'), + model_name="host", + name="image", + field=versatileimagefield.fields.VersatileImageField( + blank=True, + height_field="height", + null=True, + upload_to="host_images", + verbose_name="Profile picture", + width_field="width", + ), ), migrations.AlterField( - model_name='host', - name='linkedin_url', - field=models.URLField(blank=True, verbose_name='LinkedIn URL'), + model_name="host", + name="linkedin_url", + field=models.URLField(blank=True, verbose_name="LinkedIn URL"), ), migrations.AlterField( - model_name='host', - name='twitter_url', - field=models.URLField(blank=True, verbose_name='Twitter URL'), + model_name="host", + name="twitter_url", + field=models.URLField(blank=True, verbose_name="Twitter URL"), ), migrations.AlterField( - model_name='host', - name='website', - field=models.URLField(blank=True, verbose_name='Website'), + model_name="host", + name="website", + field=models.URLField(blank=True, verbose_name="Website"), ), migrations.AlterField( - model_name='host', - name='youtube_url', - field=models.URLField(blank=True, verbose_name='Youtube URL'), + model_name="host", + name="youtube_url", + field=models.URLField(blank=True, verbose_name="Youtube URL"), ), migrations.AlterField( - model_name='note', - name='cba_id', - field=models.IntegerField(blank=True, null=True, verbose_name='CBA ID'), + model_name="note", + name="cba_id", + field=models.IntegerField(blank=True, null=True, verbose_name="CBA ID"), ), migrations.AlterField( - model_name='note', - name='content', - field=models.TextField(verbose_name='Content'), + model_name="note", + name="content", + field=models.TextField(verbose_name="Content"), ), migrations.AlterField( - model_name='note', - name='image', - field=versatileimagefield.fields.VersatileImageField(blank=True, height_field='height', null=True, upload_to='note_images', verbose_name='Featured image', width_field='width'), + model_name="note", + name="image", + field=versatileimagefield.fields.VersatileImageField( + blank=True, + height_field="height", + null=True, + upload_to="note_images", + verbose_name="Featured image", + width_field="width", + ), ), migrations.AlterField( - model_name='note', - name='slug', - field=models.SlugField(max_length=32, unique=True, verbose_name='Slug'), + model_name="note", + name="slug", + field=models.SlugField(max_length=32, unique=True, verbose_name="Slug"), ), migrations.AlterField( - model_name='note', - name='summary', - field=models.TextField(blank=True, verbose_name='Summary'), + model_name="note", + name="summary", + field=models.TextField(blank=True, verbose_name="Summary"), ), migrations.AlterField( - model_name='note', - name='title', - field=models.CharField(max_length=128, verbose_name='Title'), + model_name="note", + name="title", + field=models.CharField(max_length=128, verbose_name="Title"), ), migrations.AlterField( - model_name='show', - name='cba_series_id', - field=models.IntegerField(blank=True, null=True, verbose_name='CBA Series ID'), + model_name="show", + name="cba_series_id", + field=models.IntegerField( + blank=True, null=True, verbose_name="CBA Series ID" + ), ), migrations.AlterField( - model_name='show', - name='default_playlist_id', - field=models.IntegerField(blank=True, null=True, verbose_name='Default Playlist ID'), + model_name="show", + name="default_playlist_id", + field=models.IntegerField( + blank=True, null=True, verbose_name="Default Playlist ID" + ), ), migrations.AlterField( - model_name='show', - name='description', - field=models.TextField(blank=True, null=True, verbose_name='Description'), + model_name="show", + name="description", + field=models.TextField(blank=True, null=True, verbose_name="Description"), ), migrations.AlterField( - model_name='show', - name='email', - field=models.EmailField(blank=True, max_length=254, null=True, verbose_name='E-Mail'), + model_name="show", + name="email", + field=models.EmailField( + blank=True, max_length=254, null=True, verbose_name="E-Mail" + ), ), migrations.AlterField( - model_name='show', - name='image', - field=versatileimagefield.fields.VersatileImageField(blank=True, height_field='height', null=True, upload_to='show_images', verbose_name='Image', width_field='width'), + model_name="show", + name="image", + field=versatileimagefield.fields.VersatileImageField( + blank=True, + height_field="height", + null=True, + upload_to="show_images", + verbose_name="Image", + width_field="width", + ), ), migrations.AlterField( - model_name='show', - name='is_public', - field=models.BooleanField(default=False, verbose_name='Is Public?'), + model_name="show", + name="is_public", + field=models.BooleanField(default=False, verbose_name="Is Public?"), ), migrations.AlterField( - model_name='show', - name='logo', - field=models.ImageField(blank=True, null=True, upload_to='show_images', verbose_name='Logo'), + model_name="show", + name="logo", + field=models.ImageField( + blank=True, null=True, upload_to="show_images", verbose_name="Logo" + ), ), migrations.AlterField( - model_name='show', - name='name', - field=models.CharField(max_length=255, verbose_name='Name'), + model_name="show", + name="name", + field=models.CharField(max_length=255, verbose_name="Name"), ), migrations.AlterField( - model_name='show', - name='short_description', - field=models.TextField(verbose_name='Short description'), + model_name="show", + name="short_description", + field=models.TextField(verbose_name="Short description"), ), migrations.AlterField( - model_name='show', - name='slug', - field=models.CharField(max_length=255, unique=True, verbose_name='Slug'), + model_name="show", + name="slug", + field=models.CharField(max_length=255, unique=True, verbose_name="Slug"), ), migrations.AlterField( - model_name='show', - name='website', - field=models.URLField(blank=True, null=True, verbose_name='Website'), + model_name="show", + name="website", + field=models.URLField(blank=True, null=True, verbose_name="Website"), ), ] diff --git a/program/migrations/0008_show_remove_automation_id.py b/program/migrations/0008_show_remove_automation_id.py index eda14c51e6f3e6b5f0dfe32a44346a635305757d..db0a6f0fa362aea0de24036774113dc7d7667d3d 100644 --- a/program/migrations/0008_show_remove_automation_id.py +++ b/program/migrations/0008_show_remove_automation_id.py @@ -1,18 +1,18 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import migrations, models +from django.db import migrations class Migration(migrations.Migration): dependencies = [ - ('program', '0007_show_remove_cba_series_id'), + ("program", "0007_show_remove_cba_series_id"), ] operations = [ migrations.RemoveField( - model_name='show', - name='automation_id', + model_name="show", + name="automation_id", ), ] diff --git a/program/migrations/0009_auto_20220124_2211.py b/program/migrations/0009_auto_20220124_2211.py index 3ca882551f8a9749bdcd78e63a7ace77bb6c0193..2469d57e06d540f08251f3093478261759229f56 100644 --- a/program/migrations/0009_auto_20220124_2211.py +++ b/program/migrations/0009_auto_20220124_2211.py @@ -6,41 +6,43 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('program', '0008_auto_20220117_1721'), + ("program", "0008_auto_20220117_1721"), ] operations = [ migrations.RemoveField( - model_name='host', - name='cba_url', + model_name="host", + name="cba_url", ), migrations.RemoveField( - model_name='host', - name='dorftv_url', + model_name="host", + name="dorftv_url", ), migrations.RemoveField( - model_name='host', - name='facebook_url', + model_name="host", + name="facebook_url", ), migrations.RemoveField( - model_name='host', - name='googleplus_url', + model_name="host", + name="googleplus_url", ), migrations.RemoveField( - model_name='host', - name='linkedin_url', + model_name="host", + name="linkedin_url", ), migrations.RemoveField( - model_name='host', - name='twitter_url', + model_name="host", + name="twitter_url", ), migrations.RemoveField( - model_name='host', - name='youtube_url', + model_name="host", + name="youtube_url", ), migrations.AlterField( - model_name='schedule', - name='automation_id', - field=models.IntegerField(blank=True, choices=[], null=True, verbose_name='Automation ID'), + model_name="schedule", + name="automation_id", + field=models.IntegerField( + blank=True, choices=[], null=True, verbose_name="Automation ID" + ), ), ] diff --git a/program/migrations/0009_host_remove_is_active.py b/program/migrations/0009_host_remove_is_active.py index f6647f04428ae3bbf55a7373e4d3e2b1f47b78d7..589c3d9afb8e2dd21571d0852d84d52509944680 100644 --- a/program/migrations/0009_host_remove_is_active.py +++ b/program/migrations/0009_host_remove_is_active.py @@ -7,12 +7,12 @@ from django.db import migrations class Migration(migrations.Migration): dependencies = [ - ('program', '0008_show_remove_automation_id'), + ("program", "0008_show_remove_automation_id"), ] operations = [ migrations.RemoveField( - model_name='host', - name='is_active', + model_name="host", + name="is_active", ), ] diff --git a/program/migrations/0010_remove_schedule_automation_id.py b/program/migrations/0010_remove_schedule_automation_id.py index e90f4eee718979b4939bc1cb069ec0588082b439..0e5e24f9c70cbb9ed2458e7d34e58d5fbb0f7ecd 100644 --- a/program/migrations/0010_remove_schedule_automation_id.py +++ b/program/migrations/0010_remove_schedule_automation_id.py @@ -6,12 +6,12 @@ from django.db import migrations class Migration(migrations.Migration): dependencies = [ - ('program', '0009_auto_20220124_2211'), + ("program", "0009_auto_20220124_2211"), ] operations = [ migrations.RemoveField( - model_name='schedule', - name='automation_id', + model_name="schedule", + name="automation_id", ), ] diff --git a/program/migrations/0010_show_remove_is_active.py b/program/migrations/0010_show_remove_is_active.py index 543bdc1d54958b2a0aba648a40f9d5612f57e47e..9ee6ecf86c94c250bde28a22b867729d4d7fa67b 100644 --- a/program/migrations/0010_show_remove_is_active.py +++ b/program/migrations/0010_show_remove_is_active.py @@ -7,12 +7,12 @@ from django.db import migrations class Migration(migrations.Migration): dependencies = [ - ('program', '0009_host_remove_is_active'), + ("program", "0009_host_remove_is_active"), ] operations = [ migrations.RemoveField( - model_name='show', - name='is_active', + model_name="show", + name="is_active", ), ] diff --git a/program/migrations/0011_link.py b/program/migrations/0011_link.py index 7260948af89eed73dfcf104dd2047bcbd201fe4c..120dd78aa6955867de547938b060c20d0228fe25 100644 --- a/program/migrations/0011_link.py +++ b/program/migrations/0011_link.py @@ -1,23 +1,38 @@ # Generated by Django 3.2.11 on 2022-01-27 16:33 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('program', '0010_remove_schedule_automation_id'), + ("program", "0010_remove_schedule_automation_id"), ] operations = [ migrations.CreateModel( - name='Link', + name="Link", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('description', models.CharField(max_length=8)), - ('url', models.URLField()), - ('host', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='links', to='program.host')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("description", models.CharField(max_length=8)), + ("url", models.URLField()), + ( + "host", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="links", + to="program.host", + ), + ), ], ), ] diff --git a/program/migrations/0011_programslot_remove_is_active.py b/program/migrations/0011_programslot_remove_is_active.py index f73a01ee3e92eb4c9a4ebb9332a63d1d080785ab..a987de5447afdec584a4be0e34c11391793d7449 100644 --- a/program/migrations/0011_programslot_remove_is_active.py +++ b/program/migrations/0011_programslot_remove_is_active.py @@ -7,12 +7,12 @@ from django.db import migrations class Migration(migrations.Migration): dependencies = [ - ('program', '0010_show_remove_is_active'), + ("program", "0010_show_remove_is_active"), ] operations = [ migrations.RemoveField( - model_name='programslot', - name='is_active', + model_name="programslot", + name="is_active", ), ] diff --git a/program/migrations/0012_auto_20180104_0005.py b/program/migrations/0012_auto_20180104_0005.py index dac9db486934a247a407833cc4ce8114e6bde03b..a1b9dd1a75171bd2c996015e10316cf044d3a370 100644 --- a/program/migrations/0012_auto_20180104_0005.py +++ b/program/migrations/0012_auto_20180104_0005.py @@ -2,439 +2,847 @@ # Generated by Django 1.11.3 on 2018-01-03 23:05 from __future__ import unicode_literals +import versatileimagefield.fields + +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion -import versatileimagefield.fields class Migration(migrations.Migration): dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('program', '0011_programslot_remove_is_active'), + ("program", "0011_programslot_remove_is_active"), ] operations = [ migrations.CreateModel( - name='Category', + name="Category", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('category', models.CharField(max_length=32, verbose_name='Category')), - ('abbrev', models.CharField(max_length=4, unique=True, verbose_name='Abbreviation')), - ('slug', models.SlugField(max_length=32, unique=True, verbose_name='Slug')), - ('color', models.TextField(blank=True, max_length=7, verbose_name='Color')), - ('description', models.TextField(blank=True, verbose_name='Description')), - ('button', models.ImageField(blank=True, null=True, upload_to='buttons', verbose_name='Button image')), - ('button_hover', models.ImageField(blank=True, null=True, upload_to='buttons', verbose_name='Button image (hover)')), - ('big_button', models.ImageField(blank=True, null=True, upload_to='buttons', verbose_name='Big button image')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("category", models.CharField(max_length=32, verbose_name="Category")), + ( + "abbrev", + models.CharField( + max_length=4, unique=True, verbose_name="Abbreviation" + ), + ), + ( + "slug", + models.SlugField(max_length=32, unique=True, verbose_name="Slug"), + ), + ( + "color", + models.TextField(blank=True, max_length=7, verbose_name="Color"), + ), + ( + "description", + models.TextField(blank=True, verbose_name="Description"), + ), + ( + "button", + models.ImageField( + blank=True, + null=True, + upload_to="buttons", + verbose_name="Button image", + ), + ), + ( + "button_hover", + models.ImageField( + blank=True, + null=True, + upload_to="buttons", + verbose_name="Button image (hover)", + ), + ), + ( + "big_button", + models.ImageField( + blank=True, + null=True, + upload_to="buttons", + verbose_name="Big button image", + ), + ), ], options={ - 'verbose_name': 'Category', - 'verbose_name_plural': 'Categories', - 'ordering': ('category',), + "verbose_name": "Category", + "verbose_name_plural": "Categories", + "ordering": ("category",), }, ), migrations.CreateModel( - name='Language', + name="Language", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=32, verbose_name='Language')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=32, verbose_name="Language")), ], options={ - 'verbose_name': 'Language', - 'verbose_name_plural': 'Languages', - 'ordering': ('language',), + "verbose_name": "Language", + "verbose_name_plural": "Languages", + "ordering": ("language",), }, ), migrations.CreateModel( - name='RTRCategory', + name="RTRCategory", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('rtrcategory', models.CharField(max_length=32, verbose_name='RTR Category')), - ('abbrev', models.CharField(max_length=4, unique=True, verbose_name='Abbreviation')), - ('slug', models.SlugField(max_length=32, unique=True, verbose_name='Slug')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "rtrcategory", + models.CharField(max_length=32, verbose_name="RTR Category"), + ), + ( + "abbrev", + models.CharField( + max_length=4, unique=True, verbose_name="Abbreviation" + ), + ), + ( + "slug", + models.SlugField(max_length=32, unique=True, verbose_name="Slug"), + ), ], options={ - 'verbose_name': 'RTR Category', - 'verbose_name_plural': 'RTR Categories', - 'ordering': ('rtrcategory',), + "verbose_name": "RTR Category", + "verbose_name_plural": "RTR Categories", + "ordering": ("rtrcategory",), }, ), migrations.CreateModel( - name='Schedule', + name="Schedule", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('byweekday', models.IntegerField(choices=[(0, 'Monday'), (1, 'Tuesday'), (2, 'Wednesday'), (3, 'Thursday'), (4, 'Friday'), (5, 'Saturday'), (6, 'Sunday')], verbose_name='Weekday')), - ('dstart', models.DateField(verbose_name='First date')), - ('tstart', models.TimeField(verbose_name='Start time')), - ('tend', models.TimeField(verbose_name='End time')), - ('until', models.DateField(verbose_name='Last date')), - ('is_repetition', models.BooleanField(default=False, verbose_name='Is repetition')), - ('fallback_id', models.IntegerField(blank=True, null=True, verbose_name='Fallback ID')), - ('automation_id', models.IntegerField(blank=True, null=True, verbose_name='Automation ID')), - ('created', models.DateTimeField(auto_now_add=True, null=True)), - ('last_updated', models.DateTimeField(auto_now=True, null=True)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "byweekday", + models.IntegerField( + choices=[ + (0, "Monday"), + (1, "Tuesday"), + (2, "Wednesday"), + (3, "Thursday"), + (4, "Friday"), + (5, "Saturday"), + (6, "Sunday"), + ], + verbose_name="Weekday", + ), + ), + ("dstart", models.DateField(verbose_name="First date")), + ("tstart", models.TimeField(verbose_name="Start time")), + ("tend", models.TimeField(verbose_name="End time")), + ("until", models.DateField(verbose_name="Last date")), + ( + "is_repetition", + models.BooleanField(default=False, verbose_name="Is repetition"), + ), + ( + "fallback_id", + models.IntegerField( + blank=True, null=True, verbose_name="Fallback ID" + ), + ), + ( + "automation_id", + models.IntegerField( + blank=True, null=True, verbose_name="Automation ID" + ), + ), + ("created", models.DateTimeField(auto_now_add=True, null=True)), + ("last_updated", models.DateTimeField(auto_now=True, null=True)), ], options={ - 'verbose_name': 'Schedule', - 'verbose_name_plural': 'Schedules', - 'ordering': ('dstart', 'tstart'), + "verbose_name": "Schedule", + "verbose_name_plural": "Schedules", + "ordering": ("dstart", "tstart"), }, ), migrations.CreateModel( - name='Topic', + name="Topic", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('topic', models.CharField(max_length=32, verbose_name='Topic')), - ('abbrev', models.CharField(max_length=4, unique=True, verbose_name='Abbreviation')), - ('slug', models.SlugField(max_length=32, unique=True, verbose_name='Slug')), - ('button', models.ImageField(blank=True, null=True, upload_to='buttons', verbose_name='Button image')), - ('button_hover', models.ImageField(blank=True, null=True, upload_to='buttons', verbose_name='Button image (hover)')), - ('big_button', models.ImageField(blank=True, null=True, upload_to='buttons', verbose_name='Big button image')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("topic", models.CharField(max_length=32, verbose_name="Topic")), + ( + "abbrev", + models.CharField( + max_length=4, unique=True, verbose_name="Abbreviation" + ), + ), + ( + "slug", + models.SlugField(max_length=32, unique=True, verbose_name="Slug"), + ), + ( + "button", + models.ImageField( + blank=True, + null=True, + upload_to="buttons", + verbose_name="Button image", + ), + ), + ( + "button_hover", + models.ImageField( + blank=True, + null=True, + upload_to="buttons", + verbose_name="Button image (hover)", + ), + ), + ( + "big_button", + models.ImageField( + blank=True, + null=True, + upload_to="buttons", + verbose_name="Big button image", + ), + ), ], options={ - 'verbose_name': 'Topic', - 'verbose_name_plural': 'Topics', - 'ordering': ('topic',), + "verbose_name": "Topic", + "verbose_name_plural": "Topics", + "ordering": ("topic",), }, ), migrations.CreateModel( - name='Type', + name="Type", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('type', models.CharField(max_length=32, verbose_name='Type')), - ('slug', models.SlugField(max_length=32, unique=True, verbose_name='Slug')), - ('color', models.CharField(default='#ffffff', max_length=7, verbose_name='Color')), - ('text_color', models.CharField(default='#000000', max_length=7, verbose_name='Text color')), - ('enabled', models.BooleanField(default=True, verbose_name='Enabled')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("type", models.CharField(max_length=32, verbose_name="Type")), + ( + "slug", + models.SlugField(max_length=32, unique=True, verbose_name="Slug"), + ), + ( + "color", + models.CharField( + default="#ffffff", max_length=7, verbose_name="Color" + ), + ), + ( + "text_color", + models.CharField( + default="#000000", max_length=7, verbose_name="Text color" + ), + ), + ("enabled", models.BooleanField(default=True, verbose_name="Enabled")), ], options={ - 'verbose_name': 'Type', - 'verbose_name_plural': 'Types', - 'ordering': ('type',), + "verbose_name": "Type", + "verbose_name_plural": "Types", + "ordering": ("type",), }, ), migrations.AlterUniqueTogether( - name='programslot', + name="programslot", unique_together=set([]), ), migrations.RemoveField( - model_name='programslot', - name='rrule', + model_name="programslot", + name="rrule", ), migrations.RemoveField( - model_name='programslot', - name='show', + model_name="programslot", + name="show", ), migrations.AlterModelOptions( - name='rrule', - options={'ordering': ('pk',), 'verbose_name': 'Recurrence rule', 'verbose_name_plural': 'Recurrence rules'}, + name="rrule", + options={ + "ordering": ("pk",), + "verbose_name": "Recurrence rule", + "verbose_name_plural": "Recurrence rules", + }, ), migrations.RemoveField( - model_name='show', - name='broadcastformat', + model_name="show", + name="broadcastformat", ), migrations.RemoveField( - model_name='show', - name='image_enabled', + model_name="show", + name="image_enabled", ), migrations.RemoveField( - model_name='show', - name='showinformation', + model_name="show", + name="showinformation", ), migrations.RemoveField( - model_name='show', - name='showtopic', + model_name="show", + name="showtopic", ), migrations.RemoveField( - model_name='timeslot', - name='programslot', - ), - migrations.AddField( - model_name='host', - name='biography', - field=models.TextField(blank=True, help_text='Describe yourself and your fields of interest in a few sentences.', null=True, verbose_name='Biography'), - ), - migrations.AddField( - model_name='host', - name='cba_url', - field=models.URLField(blank=True, help_text='URL to your CBA profile.', verbose_name='CBA URL'), - ), - migrations.AddField( - model_name='host', - name='dorftv_url', - field=models.URLField(blank=True, help_text='URL to your dorfTV channel.', verbose_name='DorfTV URL'), - ), - migrations.AddField( - model_name='host', - name='facebook_url', - field=models.URLField(blank=True, help_text='URL to your Facebook profile.', verbose_name='Facebook URL'), - ), - migrations.AddField( - model_name='host', - name='googleplus_url', - field=models.URLField(blank=True, help_text='URL to your Google+ profile.', verbose_name='Google+ URL'), - ), - migrations.AddField( - model_name='host', - name='height', - field=models.PositiveIntegerField(blank=True, editable=False, null=True, verbose_name='Image Height'), - ), - migrations.AddField( - model_name='host', - name='image', - field=versatileimagefield.fields.VersatileImageField(blank=True, height_field='height', help_text="Upload a picture of yourself. Images are automatically cropped around the 'Primary Point of Interest'. Click in the image to change it and press Save.", null=True, upload_to='host_images', verbose_name='Profile picture', width_field='width'), - ), - migrations.AddField( - model_name='host', - name='linkedin_url', - field=models.URLField(blank=True, help_text='URL to your LinkedIn profile.', verbose_name='LinkedIn URL'), - ), - migrations.AddField( - model_name='host', - name='ppoi', - field=versatileimagefield.fields.PPOIField(default='0.5x0.5', editable=False, max_length=20, verbose_name='Image PPOI'), - ), - migrations.AddField( - model_name='host', - name='twitter_url', - field=models.URLField(blank=True, help_text='URL to your Twitter profile.', verbose_name='Twitter URL'), - ), - migrations.AddField( - model_name='host', - name='width', - field=models.PositiveIntegerField(blank=True, editable=False, null=True, verbose_name='Image Width'), - ), - migrations.AddField( - model_name='host', - name='youtube_url', - field=models.URLField(blank=True, help_text='URL to your Youtube channel.', verbose_name='Youtube URL'), - ), - migrations.AddField( - model_name='note', - name='audio_url', - field=models.TextField(blank=True, editable=False, verbose_name='Direct URL to a linked audio file'), - ), - migrations.AddField( - model_name='note', - name='cba_id', - field=models.IntegerField(blank=True, help_text="Link the note to a certain CBA post by giving its ID. (E.g. if your post's CBA URL is https://cba.fro.at/1234, then your CBA ID is 1234)", null=True, verbose_name='CBA ID'), - ), - migrations.AddField( - model_name='note', - name='height', - field=models.PositiveIntegerField(blank=True, editable=False, null=True, verbose_name='Image Height'), - ), - migrations.AddField( - model_name='note', - name='host', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='hosts', to='program.Host'), - ), - migrations.AddField( - model_name='note', - name='image', - field=versatileimagefield.fields.VersatileImageField(blank=True, height_field='height', help_text="Upload an image to your show. Images are automatically cropped around the 'Primary Point of Interest'. Click in the image to change it and press Save.", null=True, upload_to='note_images', verbose_name='Featured image', width_field='width'), - ), - migrations.AddField( - model_name='note', - name='ppoi', - field=versatileimagefield.fields.PPOIField(default='0.5x0.5', editable=False, max_length=20, verbose_name='Image PPOI'), - ), - migrations.AddField( - model_name='note', - name='slug', - field=models.SlugField(default=1, help_text='A simple to read URL for your show.', max_length=32, unique=True, verbose_name='Slug'), + model_name="timeslot", + name="programslot", + ), + migrations.AddField( + model_name="host", + name="biography", + field=models.TextField( + blank=True, + help_text="Describe yourself and your fields of interest in a few sentences.", + null=True, + verbose_name="Biography", + ), + ), + migrations.AddField( + model_name="host", + name="cba_url", + field=models.URLField( + blank=True, help_text="URL to your CBA profile.", verbose_name="CBA URL" + ), + ), + migrations.AddField( + model_name="host", + name="dorftv_url", + field=models.URLField( + blank=True, + help_text="URL to your dorfTV channel.", + verbose_name="DorfTV URL", + ), + ), + migrations.AddField( + model_name="host", + name="facebook_url", + field=models.URLField( + blank=True, + help_text="URL to your Facebook profile.", + verbose_name="Facebook URL", + ), + ), + migrations.AddField( + model_name="host", + name="googleplus_url", + field=models.URLField( + blank=True, + help_text="URL to your Google+ profile.", + verbose_name="Google+ URL", + ), + ), + migrations.AddField( + model_name="host", + name="height", + field=models.PositiveIntegerField( + blank=True, editable=False, null=True, verbose_name="Image Height" + ), + ), + migrations.AddField( + model_name="host", + name="image", + field=versatileimagefield.fields.VersatileImageField( + blank=True, + height_field="height", + help_text="Upload a picture of yourself. Images are automatically cropped around" + " the 'Primary Point of Interest'. Click in the image to change it and" + " press Save.", + null=True, + upload_to="host_images", + verbose_name="Profile picture", + width_field="width", + ), + ), + migrations.AddField( + model_name="host", + name="linkedin_url", + field=models.URLField( + blank=True, + help_text="URL to your LinkedIn profile.", + verbose_name="LinkedIn URL", + ), + ), + migrations.AddField( + model_name="host", + name="ppoi", + field=versatileimagefield.fields.PPOIField( + default="0.5x0.5", + editable=False, + max_length=20, + verbose_name="Image PPOI", + ), + ), + migrations.AddField( + model_name="host", + name="twitter_url", + field=models.URLField( + blank=True, + help_text="URL to your Twitter profile.", + verbose_name="Twitter URL", + ), + ), + migrations.AddField( + model_name="host", + name="width", + field=models.PositiveIntegerField( + blank=True, editable=False, null=True, verbose_name="Image Width" + ), + ), + migrations.AddField( + model_name="host", + name="youtube_url", + field=models.URLField( + blank=True, + help_text="URL to your Youtube channel.", + verbose_name="Youtube URL", + ), + ), + migrations.AddField( + model_name="note", + name="audio_url", + field=models.TextField( + blank=True, + editable=False, + verbose_name="Direct URL to a linked audio file", + ), + ), + migrations.AddField( + model_name="note", + name="cba_id", + field=models.IntegerField( + blank=True, + help_text="Link the note to a certain CBA post by giving its ID. (E.g. if your" + " post's CBA URL is https://cba.fro.at/1234, then your CBA ID is 1234)", + null=True, + verbose_name="CBA ID", + ), + ), + migrations.AddField( + model_name="note", + name="height", + field=models.PositiveIntegerField( + blank=True, editable=False, null=True, verbose_name="Image Height" + ), + ), + migrations.AddField( + model_name="note", + name="host", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="hosts", + to="program.Host", + ), + ), + migrations.AddField( + model_name="note", + name="image", + field=versatileimagefield.fields.VersatileImageField( + blank=True, + height_field="height", + help_text="Upload an image to your show. Images are automatically cropped around" + " the 'Primary Point of Interest'. Click in the image to change it and" + " press Save.", + null=True, + upload_to="note_images", + verbose_name="Featured image", + width_field="width", + ), + ), + migrations.AddField( + model_name="note", + name="ppoi", + field=versatileimagefield.fields.PPOIField( + default="0.5x0.5", + editable=False, + max_length=20, + verbose_name="Image PPOI", + ), + ), + migrations.AddField( + model_name="note", + name="slug", + field=models.SlugField( + default=1, + help_text="A simple to read URL for your show.", + max_length=32, + unique=True, + verbose_name="Slug", + ), preserve_default=False, ), migrations.AddField( - model_name='note', - name='summary', - field=models.TextField(blank=True, help_text='Describe your upcoming show in some sentences. Avoid technical data like airing times and contact information. They will be added automatically.', verbose_name='Summary'), + model_name="note", + name="summary", + field=models.TextField( + blank=True, + help_text="Describe your upcoming show in some sentences. Avoid technical data" + " like airing times and contact information. They will be added" + " automatically.", + verbose_name="Summary", + ), ), migrations.AddField( - model_name='note', - name='user', - field=models.ForeignKey(default=1, editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='users', to=settings.AUTH_USER_MODEL), + model_name="note", + name="user", + field=models.ForeignKey( + default=1, + editable=False, + on_delete=django.db.models.deletion.CASCADE, + related_name="users", + to=settings.AUTH_USER_MODEL, + ), ), migrations.AddField( - model_name='note', - name='width', - field=models.PositiveIntegerField(blank=True, editable=False, null=True, verbose_name='Image Width'), + model_name="note", + name="width", + field=models.PositiveIntegerField( + blank=True, editable=False, null=True, verbose_name="Image Width" + ), ), migrations.AddField( - model_name='show', - name='cba_series_id', - field=models.IntegerField(blank=True, help_text='Link your show to a CBA series by giving its ID. This will enable CBA upload and will automatically link your show to your CBA archive. Find out your ID under https://cba.fro.at/series', null=True, verbose_name='CBA Series ID'), + model_name="show", + name="cba_series_id", + field=models.IntegerField( + blank=True, + help_text="Link your show to a CBA series by giving its ID. This will enable CBA" + " upload and will automatically link your show to your CBA archive. Find" + " out your ID under https://cba.fro.at/series", + null=True, + verbose_name="CBA Series ID", + ), ), migrations.AddField( - model_name='show', - name='fallback_id', - field=models.IntegerField(blank=True, null=True, verbose_name='Fallback ID'), + model_name="show", + name="fallback_id", + field=models.IntegerField( + blank=True, null=True, verbose_name="Fallback ID" + ), ), migrations.AddField( - model_name='show', - name='height', - field=models.PositiveIntegerField(blank=True, editable=False, null=True, verbose_name='Image Height'), + model_name="show", + name="height", + field=models.PositiveIntegerField( + blank=True, editable=False, null=True, verbose_name="Image Height" + ), ), migrations.AddField( - model_name='show', - name='logo', - field=models.ImageField(blank=True, null=True, upload_to='show_images', verbose_name='Logo'), + model_name="show", + name="logo", + field=models.ImageField( + blank=True, null=True, upload_to="show_images", verbose_name="Logo" + ), ), migrations.AddField( - model_name='show', - name='ppoi', - field=versatileimagefield.fields.PPOIField(default='0.5x0.5', editable=False, max_length=20, verbose_name='Image PPOI'), + model_name="show", + name="ppoi", + field=versatileimagefield.fields.PPOIField( + default="0.5x0.5", + editable=False, + max_length=20, + verbose_name="Image PPOI", + ), ), migrations.AddField( - model_name='show', - name='width', - field=models.PositiveIntegerField(blank=True, editable=False, null=True, verbose_name='Image Width'), + model_name="show", + name="width", + field=models.PositiveIntegerField( + blank=True, editable=False, null=True, verbose_name="Image Width" + ), ), migrations.AddField( - model_name='timeslot', - name='is_repetition', - field=models.BooleanField(default=False, verbose_name='REP'), + model_name="timeslot", + name="is_repetition", + field=models.BooleanField(default=False, verbose_name="REP"), ), migrations.AddField( - model_name='timeslot', - name='memo', - field=models.TextField(blank=True, verbose_name='Memo'), + model_name="timeslot", + name="memo", + field=models.TextField(blank=True, verbose_name="Memo"), ), migrations.AddField( - model_name='timeslot', - name='playlist_id', - field=models.IntegerField(null=True, verbose_name='Playlist ID'), + model_name="timeslot", + name="playlist_id", + field=models.IntegerField(null=True, verbose_name="Playlist ID"), ), migrations.AlterField( - model_name='host', - name='website', - field=models.URLField(blank=True, help_text='URL to your personal website.', verbose_name='Website'), + model_name="host", + name="website", + field=models.URLField( + blank=True, + help_text="URL to your personal website.", + verbose_name="Website", + ), ), migrations.AlterField( - model_name='musicfocus', - name='big_button', - field=models.ImageField(blank=True, null=True, upload_to='buttons', verbose_name='Big button image'), + model_name="musicfocus", + name="big_button", + field=models.ImageField( + blank=True, + null=True, + upload_to="buttons", + verbose_name="Big button image", + ), ), migrations.AlterField( - model_name='musicfocus', - name='button', - field=models.ImageField(blank=True, null=True, upload_to='buttons', verbose_name='Button image'), + model_name="musicfocus", + name="button", + field=models.ImageField( + blank=True, null=True, upload_to="buttons", verbose_name="Button image" + ), ), migrations.AlterField( - model_name='musicfocus', - name='button_hover', - field=models.ImageField(blank=True, null=True, upload_to='buttons', verbose_name='Button image (hover)'), + model_name="musicfocus", + name="button_hover", + field=models.ImageField( + blank=True, + null=True, + upload_to="buttons", + verbose_name="Button image (hover)", + ), ), migrations.AlterField( - model_name='note', - name='content', - field=models.TextField(help_text='Describe your upcoming show in detail.', verbose_name='Content'), + model_name="note", + name="content", + field=models.TextField( + help_text="Describe your upcoming show in detail.", + verbose_name="Content", + ), ), migrations.AlterField( - model_name='note', - name='show', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notes', to='program.Show'), + model_name="note", + name="show", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="notes", + to="program.Show", + ), ), migrations.AlterField( - model_name='note', - name='title', - field=models.CharField(help_text="Give your note a good headline. What will your upcoming show be about? Try to arouse interest to listen to it!<br>Avoid technical data like the show's name, its airing times or its episode number. These data are added automatically.", max_length=128, verbose_name='Title'), + model_name="note", + name="title", + field=models.CharField( + help_text="Give your note a good headline. What will your upcoming show be about?" + " Try to arouse interest to listen to it!<br>Avoid technical data like" + " the show's name, its airing times or its episode number. These data" + " are added automatically.", + max_length=128, + verbose_name="Title", + ), ), migrations.AlterField( - model_name='show', - name='description', - field=models.TextField(blank=True, help_text='Describe your show in detail.', null=True, verbose_name='Description'), + model_name="show", + name="description", + field=models.TextField( + blank=True, + help_text="Describe your show in detail.", + null=True, + verbose_name="Description", + ), ), migrations.AlterField( - model_name='show', - name='email', - field=models.EmailField(blank=True, help_text='The main contact email address for your show.', max_length=254, null=True, verbose_name='E-Mail'), + model_name="show", + name="email", + field=models.EmailField( + blank=True, + help_text="The main contact email address for your show.", + max_length=254, + null=True, + verbose_name="E-Mail", + ), ), migrations.AlterField( - model_name='show', - name='image', - field=versatileimagefield.fields.VersatileImageField(blank=True, height_field='height', help_text="Upload an image to your show. Images are automatically cropped around the 'Primary Point of Interest'. Click in the image to change it and press Save.", null=True, upload_to='show_images', verbose_name='Image', width_field='width'), + model_name="show", + name="image", + field=versatileimagefield.fields.VersatileImageField( + blank=True, + height_field="height", + help_text="Upload an image to your show. Images are automatically cropped around" + " the 'Primary Point of Interest'. Click in the image to change it and" + " press Save.", + null=True, + upload_to="show_images", + verbose_name="Image", + width_field="width", + ), ), migrations.AlterField( - model_name='show', - name='name', - field=models.CharField(help_text="The show's name. Avoid a subtitle.", max_length=255, verbose_name='Name'), + model_name="show", + name="name", + field=models.CharField( + help_text="The show's name. Avoid a subtitle.", + max_length=255, + verbose_name="Name", + ), ), migrations.AlterField( - model_name='show', - name='short_description', - field=models.TextField(help_text='Describe your show in some sentences. Avoid technical data like airing times and contact information. They will be added automatically.', verbose_name='Short description'), + model_name="show", + name="short_description", + field=models.TextField( + help_text="Describe your show in some sentences. Avoid technical data like airing" + " times and contact information. They will be added automatically.", + verbose_name="Short description", + ), ), migrations.AlterField( - model_name='show', - name='slug', - field=models.CharField(help_text='A simple to read URL for your show', max_length=255, unique=True, verbose_name='Slug'), + model_name="show", + name="slug", + field=models.CharField( + help_text="A simple to read URL for your show", + max_length=255, + unique=True, + verbose_name="Slug", + ), ), migrations.AlterField( - model_name='show', - name='website', - field=models.URLField(blank=True, help_text='Is there a website to your show? Type in its URL.', null=True, verbose_name='Website'), + model_name="show", + name="website", + field=models.URLField( + blank=True, + help_text="Is there a website to your show? Type in its URL.", + null=True, + verbose_name="Website", + ), ), migrations.AlterField( - model_name='timeslot', - name='start', - field=models.DateTimeField(verbose_name='Start time'), + model_name="timeslot", + name="start", + field=models.DateTimeField(verbose_name="Start time"), ), migrations.DeleteModel( - name='BroadcastFormat', + name="BroadcastFormat", ), migrations.DeleteModel( - name='ProgramSlot', + name="ProgramSlot", ), migrations.DeleteModel( - name='ShowInformation', + name="ShowInformation", ), migrations.DeleteModel( - name='ShowTopic', - ), - migrations.AddField( - model_name='schedule', - name='rrule', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='schedules', to='program.RRule', verbose_name='Recurrence rule'), - ), - migrations.AddField( - model_name='schedule', - name='show', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='schedules', to='program.Show', verbose_name='Show'), - ), - migrations.AddField( - model_name='show', - name='category', - field=models.ManyToManyField(blank=True, related_name='shows', to='program.Category', verbose_name='Category'), - ), - migrations.AddField( - model_name='show', - name='language', - field=models.ManyToManyField(blank=True, related_name='language', to='program.Language', verbose_name='Language'), - ), - migrations.AddField( - model_name='show', - name='rtrcategory', - field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='shows', to='program.RTRCategory', verbose_name='RTR Category'), + name="ShowTopic", + ), + migrations.AddField( + model_name="schedule", + name="rrule", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="schedules", + to="program.RRule", + verbose_name="Recurrence rule", + ), + ), + migrations.AddField( + model_name="schedule", + name="show", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="schedules", + to="program.Show", + verbose_name="Show", + ), + ), + migrations.AddField( + model_name="show", + name="category", + field=models.ManyToManyField( + blank=True, + related_name="shows", + to="program.Category", + verbose_name="Category", + ), + ), + migrations.AddField( + model_name="show", + name="language", + field=models.ManyToManyField( + blank=True, + related_name="language", + to="program.Language", + verbose_name="Language", + ), + ), + migrations.AddField( + model_name="show", + name="rtrcategory", + field=models.ForeignKey( + default=1, + on_delete=django.db.models.deletion.CASCADE, + related_name="shows", + to="program.RTRCategory", + verbose_name="RTR Category", + ), preserve_default=False, ), migrations.AddField( - model_name='show', - name='topic', - field=models.ManyToManyField(blank=True, related_name='shows', to='program.Topic', verbose_name='Topic'), - ), - migrations.AddField( - model_name='show', - name='type', - field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='shows', to='program.Type', verbose_name='Type'), + model_name="show", + name="topic", + field=models.ManyToManyField( + blank=True, + related_name="shows", + to="program.Topic", + verbose_name="Topic", + ), + ), + migrations.AddField( + model_name="show", + name="type", + field=models.ForeignKey( + default=1, + on_delete=django.db.models.deletion.CASCADE, + related_name="shows", + to="program.Type", + verbose_name="Type", + ), preserve_default=False, ), migrations.AddField( - model_name='timeslot', - name='schedule', - field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='timeslots', to='program.Schedule', verbose_name='Schedule'), + model_name="timeslot", + name="schedule", + field=models.ForeignKey( + default=1, + on_delete=django.db.models.deletion.CASCADE, + related_name="timeslots", + to="program.Schedule", + verbose_name="Schedule", + ), preserve_default=False, ), ] diff --git a/program/migrations/0012_rename_fields.py b/program/migrations/0012_rename_fields.py index cf4d70ea2023aaf480fd8f505a1e81a4bcc59fdb..7eaebfd7354a140a0b0b1276b504ca2f8edfae6f 100644 --- a/program/migrations/0012_rename_fields.py +++ b/program/migrations/0012_rename_fields.py @@ -6,53 +6,73 @@ from django.db import migrations class Migration(migrations.Migration): dependencies = [ - ('program', '0011_link'), + ("program", "0011_link"), ] operations = [ migrations.AlterModelOptions( - name='category', - options={'ordering': ('name',), 'verbose_name': 'Category', 'verbose_name_plural': 'Categories'}, + name="category", + options={ + "ordering": ("name",), + "verbose_name": "Category", + "verbose_name_plural": "Categories", + }, ), migrations.AlterModelOptions( - name='fundingcategory', - options={'ordering': ('name',), 'verbose_name': 'Funding Category', 'verbose_name_plural': 'Funding Categories'}, + name="fundingcategory", + options={ + "ordering": ("name",), + "verbose_name": "Funding Category", + "verbose_name_plural": "Funding Categories", + }, ), migrations.AlterModelOptions( - name='musicfocus', - options={'ordering': ('name',), 'verbose_name': 'Music focus', 'verbose_name_plural': 'Music focus'}, + name="musicfocus", + options={ + "ordering": ("name",), + "verbose_name": "Music focus", + "verbose_name_plural": "Music focus", + }, ), migrations.AlterModelOptions( - name='topic', - options={'ordering': ('name',), 'verbose_name': 'Topic', 'verbose_name_plural': 'Topics'}, + name="topic", + options={ + "ordering": ("name",), + "verbose_name": "Topic", + "verbose_name_plural": "Topics", + }, ), migrations.AlterModelOptions( - name='type', - options={'ordering': ('name',), 'verbose_name': 'Type', 'verbose_name_plural': 'Types'}, + name="type", + options={ + "ordering": ("name",), + "verbose_name": "Type", + "verbose_name_plural": "Types", + }, ), migrations.RenameField( - model_name='category', - old_name='category', - new_name='name', + model_name="category", + old_name="category", + new_name="name", ), migrations.RenameField( - model_name='fundingcategory', - old_name='fundingcategory', - new_name='name', + model_name="fundingcategory", + old_name="fundingcategory", + new_name="name", ), migrations.RenameField( - model_name='musicfocus', - old_name='focus', - new_name='name', + model_name="musicfocus", + old_name="focus", + new_name="name", ), migrations.RenameField( - model_name='topic', - old_name='topic', - new_name='name', + model_name="topic", + old_name="topic", + new_name="name", ), migrations.RenameField( - model_name='type', - old_name='type', - new_name='name', + model_name="type", + old_name="type", + new_name="name", ), ] diff --git a/program/migrations/0013_auto_20180124_1748.py b/program/migrations/0013_auto_20180124_1748.py index 5a3ecdffdd22c0079f9b70e78241e8889d93fc49..4be026a3d6a144cf1931a0ed9c3a6c460ffd3127 100644 --- a/program/migrations/0013_auto_20180124_1748.py +++ b/program/migrations/0013_auto_20180124_1748.py @@ -8,56 +8,56 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('program', '0012_auto_20180104_0005'), + ("program", "0012_auto_20180104_0005"), ] operations = [ migrations.RemoveField( - model_name='host', - name='is_always_visible', + model_name="host", + name="is_always_visible", ), migrations.RemoveField( - model_name='type', - name='enabled', + model_name="type", + name="enabled", ), migrations.AddField( - model_name='category', - name='is_active', - field=models.BooleanField(default=True, verbose_name='Is active?'), + model_name="category", + name="is_active", + field=models.BooleanField(default=True, verbose_name="Is active?"), ), migrations.AddField( - model_name='host', - name='is_active', - field=models.BooleanField(default=True, verbose_name='Is active?'), + model_name="host", + name="is_active", + field=models.BooleanField(default=True, verbose_name="Is active?"), ), migrations.AddField( - model_name='language', - name='is_active', - field=models.BooleanField(default=True, verbose_name='Is active?'), + model_name="language", + name="is_active", + field=models.BooleanField(default=True, verbose_name="Is active?"), ), migrations.AddField( - model_name='musicfocus', - name='is_active', - field=models.BooleanField(default=True, verbose_name='Is active?'), + model_name="musicfocus", + name="is_active", + field=models.BooleanField(default=True, verbose_name="Is active?"), ), migrations.AddField( - model_name='rtrcategory', - name='is_active', - field=models.BooleanField(default=True, verbose_name='Is active?'), + model_name="rtrcategory", + name="is_active", + field=models.BooleanField(default=True, verbose_name="Is active?"), ), migrations.AddField( - model_name='topic', - name='is_active', - field=models.BooleanField(default=True, verbose_name='Is active?'), + model_name="topic", + name="is_active", + field=models.BooleanField(default=True, verbose_name="Is active?"), ), migrations.AddField( - model_name='type', - name='is_active', - field=models.BooleanField(default=True, verbose_name='Is active?'), + model_name="type", + name="is_active", + field=models.BooleanField(default=True, verbose_name="Is active?"), ), migrations.AlterField( - model_name='timeslot', - name='is_repetition', - field=models.BooleanField(default=False, verbose_name='(REP)'), + model_name="timeslot", + name="is_repetition", + field=models.BooleanField(default=False, verbose_name="(REP)"), ), ] diff --git a/program/migrations/0013_auto_20220221_1637.py b/program/migrations/0013_auto_20220221_1637.py index 73f259bed0321909a977ed1642eb5cc6d2dd9665..dbd6cb517e418444bcf479afa659d16c7d835d2d 100644 --- a/program/migrations/0013_auto_20220221_1637.py +++ b/program/migrations/0013_auto_20220221_1637.py @@ -1,515 +1,582 @@ # Generated by Django 3.2.11 on 2022-02-21 15:37 +import versatileimagefield.fields + +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion -import versatileimagefield.fields class Migration(migrations.Migration): dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('program', '0012_rename_fields'), + ("program", "0012_rename_fields"), ] operations = [ migrations.AlterModelOptions( - name='category', - options={'ordering': ('name',)}, + name="category", + options={"ordering": ("name",)}, ), migrations.AlterModelOptions( - name='fundingcategory', - options={'ordering': ('name',)}, + name="fundingcategory", + options={"ordering": ("name",)}, ), migrations.AlterModelOptions( - name='host', - options={'ordering': ('name',)}, + name="host", + options={"ordering": ("name",)}, ), migrations.AlterModelOptions( - name='language', - options={'ordering': ('language',)}, + name="language", + options={"ordering": ("language",)}, ), migrations.AlterModelOptions( - name='musicfocus', - options={'ordering': ('name',)}, + name="musicfocus", + options={"ordering": ("name",)}, ), migrations.AlterModelOptions( - name='note', - options={'ordering': ('timeslot',)}, + name="note", + options={"ordering": ("timeslot",)}, ), migrations.AlterModelOptions( - name='rrule', - options={'ordering': ('pk',)}, + name="rrule", + options={"ordering": ("pk",)}, ), migrations.AlterModelOptions( - name='schedule', - options={'ordering': ('dstart', 'tstart')}, + name="schedule", + options={"ordering": ("dstart", "tstart")}, ), migrations.AlterModelOptions( - name='show', - options={'ordering': ('slug',)}, + name="show", + options={"ordering": ("slug",)}, ), migrations.AlterModelOptions( - name='timeslot', - options={'ordering': ('start', 'end')}, + name="timeslot", + options={"ordering": ("start", "end")}, ), migrations.AlterModelOptions( - name='topic', - options={'ordering': ('name',)}, + name="topic", + options={"ordering": ("name",)}, ), migrations.AlterModelOptions( - name='type', - options={'ordering': ('name',)}, + name="type", + options={"ordering": ("name",)}, ), migrations.AlterField( - model_name='category', - name='abbrev', + model_name="category", + name="abbrev", field=models.CharField(max_length=4, unique=True), ), migrations.AlterField( - model_name='category', - name='description', + model_name="category", + name="description", field=models.TextField(blank=True), ), migrations.AlterField( - model_name='category', - name='is_active', + model_name="category", + name="is_active", field=models.BooleanField(default=True), ), migrations.AlterField( - model_name='category', - name='name', + model_name="category", + name="name", field=models.CharField(max_length=32), ), migrations.AlterField( - model_name='category', - name='slug', + model_name="category", + name="slug", field=models.SlugField(max_length=32, unique=True), ), migrations.AlterField( - model_name='fundingcategory', - name='abbrev', + model_name="fundingcategory", + name="abbrev", field=models.CharField(max_length=4, unique=True), ), migrations.AlterField( - model_name='fundingcategory', - name='is_active', + model_name="fundingcategory", + name="is_active", field=models.BooleanField(default=True), ), migrations.AlterField( - model_name='fundingcategory', - name='name', + model_name="fundingcategory", + name="name", field=models.CharField(max_length=32), ), migrations.AlterField( - model_name='fundingcategory', - name='slug', + model_name="fundingcategory", + name="slug", field=models.SlugField(max_length=32, unique=True), ), migrations.AlterField( - model_name='host', - name='biography', + model_name="host", + name="biography", field=models.TextField(blank=True, null=True), ), migrations.AlterField( - model_name='host', - name='email', + model_name="host", + name="email", field=models.EmailField(blank=True, max_length=254), ), migrations.AlterField( - model_name='host', - name='height', + model_name="host", + name="height", field=models.PositiveIntegerField(blank=True, editable=False, null=True), ), migrations.AlterField( - model_name='host', - name='image', - field=versatileimagefield.fields.VersatileImageField(blank=True, height_field='height', null=True, upload_to='host_images', width_field='width'), + model_name="host", + name="image", + field=versatileimagefield.fields.VersatileImageField( + blank=True, + height_field="height", + null=True, + upload_to="host_images", + width_field="width", + ), ), migrations.AlterField( - model_name='host', - name='is_active', + model_name="host", + name="is_active", field=models.BooleanField(default=True), ), migrations.AlterField( - model_name='host', - name='name', + model_name="host", + name="name", field=models.CharField(max_length=128), ), migrations.AlterField( - model_name='host', - name='ppoi', - field=versatileimagefield.fields.PPOIField(default='0.5x0.5', editable=False, max_length=20), + model_name="host", + name="ppoi", + field=versatileimagefield.fields.PPOIField( + default="0.5x0.5", editable=False, max_length=20 + ), ), migrations.AlterField( - model_name='host', - name='website', + model_name="host", + name="website", field=models.URLField(blank=True), ), migrations.AlterField( - model_name='host', - name='width', + model_name="host", + name="width", field=models.PositiveIntegerField(blank=True, editable=False, null=True), ), migrations.AlterField( - model_name='language', - name='is_active', + model_name="language", + name="is_active", field=models.BooleanField(default=True), ), migrations.AlterField( - model_name='language', - name='name', + model_name="language", + name="name", field=models.CharField(max_length=32), ), migrations.AlterField( - model_name='musicfocus', - name='abbrev', + model_name="musicfocus", + name="abbrev", field=models.CharField(max_length=4, unique=True), ), migrations.AlterField( - model_name='musicfocus', - name='is_active', + model_name="musicfocus", + name="is_active", field=models.BooleanField(default=True), ), migrations.AlterField( - model_name='musicfocus', - name='name', + model_name="musicfocus", + name="name", field=models.CharField(max_length=32), ), migrations.AlterField( - model_name='musicfocus', - name='slug', + model_name="musicfocus", + name="slug", field=models.SlugField(max_length=32, unique=True), ), migrations.AlterField( - model_name='note', - name='audio_url', + model_name="note", + name="audio_url", field=models.TextField(blank=True, editable=False), ), migrations.AlterField( - model_name='note', - name='cba_id', + model_name="note", + name="cba_id", field=models.IntegerField(blank=True, null=True), ), migrations.AlterField( - model_name='note', - name='content', + model_name="note", + name="content", field=models.TextField(), ), migrations.AlterField( - model_name='note', - name='height', + model_name="note", + name="height", field=models.PositiveIntegerField(blank=True, editable=False, null=True), ), migrations.AlterField( - model_name='note', - name='image', - field=versatileimagefield.fields.VersatileImageField(blank=True, height_field='height', null=True, upload_to='note_images', width_field='width'), + model_name="note", + name="image", + field=versatileimagefield.fields.VersatileImageField( + blank=True, + height_field="height", + null=True, + upload_to="note_images", + width_field="width", + ), ), migrations.AlterField( - model_name='note', - name='ppoi', - field=versatileimagefield.fields.PPOIField(default='0.5x0.5', editable=False, max_length=20), + model_name="note", + name="ppoi", + field=versatileimagefield.fields.PPOIField( + default="0.5x0.5", editable=False, max_length=20 + ), ), migrations.AlterField( - model_name='note', - name='slug', + model_name="note", + name="slug", field=models.SlugField(max_length=32, unique=True), ), migrations.AlterField( - model_name='note', - name='status', + model_name="note", + name="status", field=models.IntegerField(default=1), ), migrations.AlterField( - model_name='note', - name='summary', + model_name="note", + name="summary", field=models.TextField(blank=True), ), migrations.AlterField( - model_name='note', - name='timeslot', - field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='program.timeslot'), + model_name="note", + name="timeslot", + field=models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, to="program.timeslot" + ), ), migrations.AlterField( - model_name='note', - name='title', + model_name="note", + name="title", field=models.CharField(max_length=128), ), migrations.AlterField( - model_name='note', - name='width', + model_name="note", + name="width", field=models.PositiveIntegerField(blank=True, editable=False, null=True), ), migrations.AlterField( - model_name='rrule', - name='bysetpos', + model_name="rrule", + name="bysetpos", field=models.IntegerField(blank=True, null=True), ), migrations.AlterField( - model_name='rrule', - name='count', + model_name="rrule", + name="count", field=models.IntegerField(blank=True, null=True), ), migrations.AlterField( - model_name='rrule', - name='freq', + model_name="rrule", + name="freq", field=models.IntegerField(), ), migrations.AlterField( - model_name='rrule', - name='interval', + model_name="rrule", + name="interval", field=models.IntegerField(default=1), ), migrations.AlterField( - model_name='rrule', - name='name', + model_name="rrule", + name="name", field=models.CharField(max_length=32, unique=True), ), migrations.AlterField( - model_name='schedule', - name='add_business_days_only', + model_name="schedule", + name="add_business_days_only", field=models.BooleanField(default=False), ), migrations.AlterField( - model_name='schedule', - name='add_days_no', + model_name="schedule", + name="add_days_no", field=models.IntegerField(blank=True, null=True), ), migrations.AlterField( - model_name='schedule', - name='byweekday', + model_name="schedule", + name="byweekday", field=models.IntegerField(), ), migrations.AlterField( - model_name='schedule', - name='default_playlist_id', + model_name="schedule", + name="default_playlist_id", field=models.IntegerField(blank=True, null=True), ), migrations.AlterField( - model_name='schedule', - name='dstart', + model_name="schedule", + name="dstart", field=models.DateField(), ), migrations.AlterField( - model_name='schedule', - name='is_repetition', + model_name="schedule", + name="is_repetition", field=models.BooleanField(default=False), ), migrations.AlterField( - model_name='schedule', - name='rrule', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='schedules', to='program.rrule'), + model_name="schedule", + name="rrule", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="schedules", + to="program.rrule", + ), ), migrations.AlterField( - model_name='schedule', - name='show', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='schedules', to='program.show'), + model_name="schedule", + name="show", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="schedules", + to="program.show", + ), ), migrations.AlterField( - model_name='schedule', - name='tend', + model_name="schedule", + name="tend", field=models.TimeField(), ), migrations.AlterField( - model_name='schedule', - name='tstart', + model_name="schedule", + name="tstart", field=models.TimeField(), ), migrations.AlterField( - model_name='schedule', - name='until', + model_name="schedule", + name="until", field=models.DateField(), ), migrations.AlterField( - model_name='show', - name='category', - field=models.ManyToManyField(blank=True, related_name='shows', to='program.Category'), + model_name="show", + name="category", + field=models.ManyToManyField( + blank=True, related_name="shows", to="program.Category" + ), ), migrations.AlterField( - model_name='show', - name='cba_series_id', + model_name="show", + name="cba_series_id", field=models.IntegerField(blank=True, null=True), ), migrations.AlterField( - model_name='show', - name='default_playlist_id', + model_name="show", + name="default_playlist_id", field=models.IntegerField(blank=True, null=True), ), migrations.AlterField( - model_name='show', - name='description', + model_name="show", + name="description", field=models.TextField(blank=True, null=True), ), migrations.AlterField( - model_name='show', - name='email', + model_name="show", + name="email", field=models.EmailField(blank=True, max_length=254, null=True), ), migrations.AlterField( - model_name='show', - name='fundingcategory', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='shows', to='program.fundingcategory'), + model_name="show", + name="fundingcategory", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="shows", + to="program.fundingcategory", + ), ), migrations.AlterField( - model_name='show', - name='height', + model_name="show", + name="height", field=models.PositiveIntegerField(blank=True, editable=False, null=True), ), migrations.AlterField( - model_name='show', - name='hosts', - field=models.ManyToManyField(blank=True, related_name='shows', to='program.Host'), + model_name="show", + name="hosts", + field=models.ManyToManyField( + blank=True, related_name="shows", to="program.Host" + ), ), migrations.AlterField( - model_name='show', - name='image', - field=versatileimagefield.fields.VersatileImageField(blank=True, height_field='height', null=True, upload_to='show_images', width_field='width'), + model_name="show", + name="image", + field=versatileimagefield.fields.VersatileImageField( + blank=True, + height_field="height", + null=True, + upload_to="show_images", + width_field="width", + ), ), migrations.AlterField( - model_name='show', - name='is_active', + model_name="show", + name="is_active", field=models.BooleanField(default=True), ), migrations.AlterField( - model_name='show', - name='is_public', + model_name="show", + name="is_public", field=models.BooleanField(default=False), ), migrations.AlterField( - model_name='show', - name='language', - field=models.ManyToManyField(blank=True, related_name='language', to='program.Language'), + model_name="show", + name="language", + field=models.ManyToManyField( + blank=True, related_name="language", to="program.Language" + ), ), migrations.AlterField( - model_name='show', - name='logo', - field=models.ImageField(blank=True, null=True, upload_to='show_images'), + model_name="show", + name="logo", + field=models.ImageField(blank=True, null=True, upload_to="show_images"), ), migrations.AlterField( - model_name='show', - name='musicfocus', - field=models.ManyToManyField(blank=True, related_name='shows', to='program.MusicFocus'), + model_name="show", + name="musicfocus", + field=models.ManyToManyField( + blank=True, related_name="shows", to="program.MusicFocus" + ), ), migrations.AlterField( - model_name='show', - name='name', + model_name="show", + name="name", field=models.CharField(max_length=255), ), migrations.AlterField( - model_name='show', - name='owners', - field=models.ManyToManyField(blank=True, related_name='shows', to=settings.AUTH_USER_MODEL), + model_name="show", + name="owners", + field=models.ManyToManyField( + blank=True, related_name="shows", to=settings.AUTH_USER_MODEL + ), ), migrations.AlterField( - model_name='show', - name='ppoi', - field=versatileimagefield.fields.PPOIField(default='0.5x0.5', editable=False, max_length=20), + model_name="show", + name="ppoi", + field=versatileimagefield.fields.PPOIField( + default="0.5x0.5", editable=False, max_length=20 + ), ), migrations.AlterField( - model_name='show', - name='predecessor', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='successors', to='program.show'), + model_name="show", + name="predecessor", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="successors", + to="program.show", + ), ), migrations.AlterField( - model_name='show', - name='short_description', + model_name="show", + name="short_description", field=models.TextField(), ), migrations.AlterField( - model_name='show', - name='slug', + model_name="show", + name="slug", field=models.CharField(max_length=255, unique=True), ), migrations.AlterField( - model_name='show', - name='topic', - field=models.ManyToManyField(blank=True, related_name='shows', to='program.Topic'), + model_name="show", + name="topic", + field=models.ManyToManyField( + blank=True, related_name="shows", to="program.Topic" + ), ), migrations.AlterField( - model_name='show', - name='type', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='shows', to='program.type'), + model_name="show", + name="type", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="shows", + to="program.type", + ), ), migrations.AlterField( - model_name='show', - name='website', + model_name="show", + name="website", field=models.URLField(blank=True, null=True), ), migrations.AlterField( - model_name='show', - name='width', + model_name="show", + name="width", field=models.PositiveIntegerField(blank=True, editable=False, null=True), ), migrations.AlterField( - model_name='timeslot', - name='end', + model_name="timeslot", + name="end", field=models.DateTimeField(), ), migrations.AlterField( - model_name='timeslot', - name='is_repetition', + model_name="timeslot", + name="is_repetition", field=models.BooleanField(default=False), ), migrations.AlterField( - model_name='timeslot', - name='memo', + model_name="timeslot", + name="memo", field=models.TextField(blank=True), ), migrations.AlterField( - model_name='timeslot', - name='note_id', + model_name="timeslot", + name="note_id", field=models.IntegerField(editable=False, null=True), ), migrations.AlterField( - model_name='timeslot', - name='playlist_id', + model_name="timeslot", + name="playlist_id", field=models.IntegerField(null=True), ), migrations.AlterField( - model_name='timeslot', - name='schedule', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='timeslots', to='program.schedule'), + model_name="timeslot", + name="schedule", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="timeslots", + to="program.schedule", + ), ), migrations.AlterField( - model_name='timeslot', - name='start', + model_name="timeslot", + name="start", field=models.DateTimeField(), ), migrations.AlterField( - model_name='topic', - name='abbrev', + model_name="topic", + name="abbrev", field=models.CharField(max_length=4, unique=True), ), migrations.AlterField( - model_name='topic', - name='is_active', + model_name="topic", + name="is_active", field=models.BooleanField(default=True), ), migrations.AlterField( - model_name='topic', - name='name', + model_name="topic", + name="name", field=models.CharField(max_length=32), ), migrations.AlterField( - model_name='topic', - name='slug', + model_name="topic", + name="slug", field=models.SlugField(max_length=32, unique=True), ), migrations.AlterField( - model_name='type', - name='is_active', + model_name="type", + name="is_active", field=models.BooleanField(default=True), ), migrations.AlterField( - model_name='type', - name='name', + model_name="type", + name="name", field=models.CharField(max_length=32), ), migrations.AlterField( - model_name='type', - name='slug', + model_name="type", + name="slug", field=models.SlugField(max_length=32, unique=True), ), ] diff --git a/program/migrations/0014_auto_20180216_2000.py b/program/migrations/0014_auto_20180216_2000.py index 2f0fc980d018e2e77f24a335543eb7771b1e8be5..6329c3f14388f4c27d86efd0b1324a0be97b41d1 100644 --- a/program/migrations/0014_auto_20180216_2000.py +++ b/program/migrations/0014_auto_20180216_2000.py @@ -2,43 +2,71 @@ # Generated by Django 1.11.3 on 2018-02-16 20:00 from __future__ import unicode_literals -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('program', '0013_auto_20180124_1748'), + ("program", "0013_auto_20180124_1748"), ] operations = [ migrations.CreateModel( - name='FundingCategory', + name="FundingCategory", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('fundingcategory', models.CharField(max_length=32, verbose_name='Funding Category')), - ('abbrev', models.CharField(max_length=4, unique=True, verbose_name='Abbreviation')), - ('slug', models.SlugField(max_length=32, unique=True, verbose_name='Slug')), - ('is_active', models.BooleanField(default=True, verbose_name='Is active?')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "fundingcategory", + models.CharField(max_length=32, verbose_name="Funding Category"), + ), + ( + "abbrev", + models.CharField( + max_length=4, unique=True, verbose_name="Abbreviation" + ), + ), + ( + "slug", + models.SlugField(max_length=32, unique=True, verbose_name="Slug"), + ), + ( + "is_active", + models.BooleanField(default=True, verbose_name="Is active?"), + ), ], options={ - 'verbose_name': 'Funding Category', - 'verbose_name_plural': 'Funding Categories', - 'ordering': ('fundingcategory',), + "verbose_name": "Funding Category", + "verbose_name_plural": "Funding Categories", + "ordering": ("fundingcategory",), }, ), migrations.RemoveField( - model_name='show', - name='rtrcategory', + model_name="show", + name="rtrcategory", ), migrations.DeleteModel( - name='RTRCategory', + name="RTRCategory", ), migrations.AddField( - model_name='show', - name='fundingcategory', - field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='shows', to='program.FundingCategory', verbose_name='Funding Category'), + model_name="show", + name="fundingcategory", + field=models.ForeignKey( + default=1, + on_delete=django.db.models.deletion.CASCADE, + related_name="shows", + to="program.FundingCategory", + verbose_name="Funding Category", + ), preserve_default=False, ), ] diff --git a/program/migrations/0014_auto_20220221_1641.py b/program/migrations/0014_auto_20220221_1641.py index b110c96aedfda6dff2ccb5c4d202421f8b82f5d4..602cc4925c134a18614f5208d1366ad3cd62e565 100644 --- a/program/migrations/0014_auto_20220221_1641.py +++ b/program/migrations/0014_auto_20220221_1641.py @@ -6,32 +6,32 @@ from django.db import migrations class Migration(migrations.Migration): dependencies = [ - ('program', '0013_auto_20220221_1637'), + ("program", "0013_auto_20220221_1637"), ] operations = [ migrations.RemoveField( - model_name='note', - name='created', + model_name="note", + name="created", ), migrations.RemoveField( - model_name='note', - name='last_updated', + model_name="note", + name="last_updated", ), migrations.RemoveField( - model_name='schedule', - name='created', + model_name="schedule", + name="created", ), migrations.RemoveField( - model_name='schedule', - name='last_updated', + model_name="schedule", + name="last_updated", ), migrations.RemoveField( - model_name='show', - name='created', + model_name="show", + name="created", ), migrations.RemoveField( - model_name='show', - name='last_updated', + model_name="show", + name="last_updated", ), ] diff --git a/program/migrations/0015_auto_20180218_1111.py b/program/migrations/0015_auto_20180218_1111.py index 291a19edbbf34a7c51bd15f6dfc4e677f3ed99b9..e0dacaee2d4b3ad78dee05b3247c22933d7dacaa 100644 --- a/program/migrations/0015_auto_20180218_1111.py +++ b/program/migrations/0015_auto_20180218_1111.py @@ -8,18 +8,20 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('program', '0014_auto_20180216_2000'), + ("program", "0014_auto_20180216_2000"), ] operations = [ migrations.AddField( - model_name='timeslot', - name='note_id', - field=models.IntegerField(editable=False, null=True, verbose_name='Note ID'), + model_name="timeslot", + name="note_id", + field=models.IntegerField( + editable=False, null=True, verbose_name="Note ID" + ), ), migrations.AlterField( - model_name='timeslot', - name='is_repetition', - field=models.BooleanField(default=False, verbose_name='Is repetition?'), + model_name="timeslot", + name="is_repetition", + field=models.BooleanField(default=False, verbose_name="Is repetition?"), ), ] diff --git a/program/migrations/0015_rename_bysetpos_rrule_by_set_pos.py b/program/migrations/0015_rename_bysetpos_rrule_by_set_pos.py index cded49ca5127c7b826960012aa9c811a15eff0ab..135e8b7d7fe9c7cdff3f5d57dc30e5f884a6a47f 100644 --- a/program/migrations/0015_rename_bysetpos_rrule_by_set_pos.py +++ b/program/migrations/0015_rename_bysetpos_rrule_by_set_pos.py @@ -6,13 +6,13 @@ from django.db import migrations class Migration(migrations.Migration): dependencies = [ - ('program', '0014_auto_20220221_1641'), + ("program", "0014_auto_20220221_1641"), ] operations = [ migrations.RenameField( - model_name='rrule', - old_name='bysetpos', - new_name='by_set_pos', + model_name="rrule", + old_name="bysetpos", + new_name="by_set_pos", ), ] diff --git a/program/migrations/0016_auto_20180222_1253.py b/program/migrations/0016_auto_20180222_1253.py index d9cb5da4d6bfcffb80a0f49e3b09b0965c1d1234..0510a88142f5f8ae57839953541e158e15bb4e6a 100644 --- a/program/migrations/0016_auto_20180222_1253.py +++ b/program/migrations/0016_auto_20180222_1253.py @@ -2,20 +2,31 @@ # Generated by Django 1.11.3 on 2018-02-22 12:53 from __future__ import unicode_literals -from django.db import migrations import versatileimagefield.fields +from django.db import migrations + class Migration(migrations.Migration): dependencies = [ - ('program', '0015_auto_20180218_1111'), + ("program", "0015_auto_20180218_1111"), ] operations = [ migrations.AlterField( - model_name='note', - name='image', - field=versatileimagefield.fields.VersatileImageField(blank=True, height_field='height', help_text="Upload an image to your note. Images are automatically cropped around the 'Primary Point of Interest'. Click in the image to change it and press Save.", null=True, upload_to='note_images', verbose_name='Featured image', width_field='width'), + model_name="note", + name="image", + field=versatileimagefield.fields.VersatileImageField( + blank=True, + height_field="height", + help_text="Upload an image to your note. Images are automatically cropped around" + " the 'Primary Point of Interest'. Click in the image to change it and" + " press Save.", + null=True, + upload_to="note_images", + verbose_name="Featured image", + width_field="width", + ), ), ] diff --git a/program/migrations/0016_auto_20220222_0209.py b/program/migrations/0016_auto_20220222_0209.py index 84ca864ed749ed69c6bf735189e7aa904703e0f6..6fe057f20d27573602c8ae99b88116d79ab0a618 100644 --- a/program/migrations/0016_auto_20220222_0209.py +++ b/program/migrations/0016_auto_20220222_0209.py @@ -6,18 +6,18 @@ from django.db import migrations class Migration(migrations.Migration): dependencies = [ - ('program', '0015_rename_bysetpos_rrule_by_set_pos'), + ("program", "0015_rename_bysetpos_rrule_by_set_pos"), ] operations = [ migrations.RenameField( - model_name='show', - old_name='fundingcategory', - new_name='funding_category', + model_name="show", + old_name="fundingcategory", + new_name="funding_category", ), migrations.RenameField( - model_name='show', - old_name='musicfocus', - new_name='music_focus', + model_name="show", + old_name="musicfocus", + new_name="music_focus", ), ] diff --git a/program/migrations/0017_auto_20180314_1409.py b/program/migrations/0017_auto_20180314_1409.py index 2dc44cba1d9093d7ed457712e8ea6094a15e36b9..e8ca67d96858490ed938c66b71672f417c85fee6 100644 --- a/program/migrations/0017_auto_20180314_1409.py +++ b/program/migrations/0017_auto_20180314_1409.py @@ -8,18 +8,20 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('program', '0016_auto_20180222_1253'), + ("program", "0016_auto_20180222_1253"), ] operations = [ migrations.AddField( - model_name='schedule', - name='add_business_days_only', - field=models.BooleanField(default=False, verbose_name='Only add business days?'), + model_name="schedule", + name="add_business_days_only", + field=models.BooleanField( + default=False, verbose_name="Only add business days?" + ), ), migrations.AddField( - model_name='schedule', - name='add_days_no', - field=models.IntegerField(blank=True, null=True, verbose_name='Add days'), + model_name="schedule", + name="add_days_no", + field=models.IntegerField(blank=True, null=True, verbose_name="Add days"), ), ] diff --git a/program/migrations/0017_auto_20220302_1711.py b/program/migrations/0017_auto_20220302_1711.py index 3d49ba5f08416fbcd923a8a39abb84ec8f5d25f8..74634a6ccf93584807741b6ae4026688b3229bed 100644 --- a/program/migrations/0017_auto_20220302_1711.py +++ b/program/migrations/0017_auto_20220302_1711.py @@ -6,37 +6,37 @@ from django.db import migrations class Migration(migrations.Migration): dependencies = [ - ('program', '0016_auto_20220222_0209'), + ("program", "0016_auto_20220222_0209"), ] operations = [ migrations.AlterModelOptions( - name='schedule', - options={'ordering': ('first_date', 'start_time')}, + name="schedule", + options={"ordering": ("first_date", "start_time")}, ), migrations.RenameField( - model_name='schedule', - old_name='byweekday', - new_name='by_weekday', + model_name="schedule", + old_name="byweekday", + new_name="by_weekday", ), migrations.RenameField( - model_name='schedule', - old_name='tend', - new_name='end_time', + model_name="schedule", + old_name="tend", + new_name="end_time", ), migrations.RenameField( - model_name='schedule', - old_name='dstart', - new_name='first_date', + model_name="schedule", + old_name="dstart", + new_name="first_date", ), migrations.RenameField( - model_name='schedule', - old_name='until', - new_name='last_date', + model_name="schedule", + old_name="until", + new_name="last_date", ), migrations.RenameField( - model_name='schedule', - old_name='tstart', - new_name='start_time', + model_name="schedule", + old_name="tstart", + new_name="start_time", ), ] diff --git a/program/migrations/0018_auto_20190810_1146.py b/program/migrations/0018_auto_20190810_1146.py index 1ed33a09088bd7febb08de51589c1d280b1beef0..846fb56bd75dafc816464c4a2bc1d92a9ed30d3b 100644 --- a/program/migrations/0018_auto_20190810_1146.py +++ b/program/migrations/0018_auto_20190810_1146.py @@ -8,28 +8,52 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('program', '0017_auto_20180314_1409'), + ("program", "0017_auto_20180314_1409"), ] operations = [ migrations.AlterField( - model_name='show', - name='cba_series_id', - field=models.IntegerField(blank=True, help_text="Link your show to a CBA series by giving its ID. This will enable CBA upload and will automatically link your show to your CBA archive. Find out your show's ID under https://cba.fro.at/series", null=True, verbose_name='CBA Series ID'), + model_name="show", + name="cba_series_id", + field=models.IntegerField( + blank=True, + help_text="Link your show to a CBA series by giving its ID. This will enable CBA" + " upload and will automatically link your show to your CBA archive. Find" + " out your show's ID under https://cba.fro.at/series", + null=True, + verbose_name="CBA Series ID", + ), ), migrations.AlterField( - model_name='show', - name='fallback_id', - field=models.IntegerField(blank=True, help_text='If a timeslot of your show is empty, this playlist will be aired as a backup.', null=True, verbose_name='Fallback ID'), + model_name="show", + name="fallback_id", + field=models.IntegerField( + blank=True, + help_text="If a timeslot of your show is empty, this playlist will be aired as a" + " backup.", + null=True, + verbose_name="Fallback ID", + ), ), migrations.AlterField( - model_name='show', - name='logo', - field=models.ImageField(blank=True, help_text='Upload a logo of your show.', null=True, upload_to='show_images', verbose_name='Logo'), + model_name="show", + name="logo", + field=models.ImageField( + blank=True, + help_text="Upload a logo of your show.", + null=True, + upload_to="show_images", + verbose_name="Logo", + ), ), migrations.AlterField( - model_name='show', - name='short_description', - field=models.TextField(help_text='Describe your show for your listeners in some sentences. Avoid technical data like airing times and contact information. They will be added automatically.', verbose_name='Short description'), + model_name="show", + name="short_description", + field=models.TextField( + help_text="Describe your show for your listeners in some sentences. Avoid" + " technical data like airing times and contact information. They will be" + " added automatically.", + verbose_name="Short description", + ), ), ] diff --git a/program/migrations/0019_auto_20190810_1340.py b/program/migrations/0019_auto_20190810_1340.py index 39e014fd3d493cb8c3131a757d508bb198c2ae5b..85bf5ca52ff82ff577623b55d412f40044eec509 100644 --- a/program/migrations/0019_auto_20190810_1340.py +++ b/program/migrations/0019_auto_20190810_1340.py @@ -2,20 +2,26 @@ # Generated by Django 1.11.3 on 2019-08-10 13:40 from __future__ import unicode_literals -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('program', '0018_auto_20190810_1146'), + ("program", "0018_auto_20190810_1146"), ] operations = [ migrations.AlterField( - model_name='show', - name='fundingcategory', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='shows', to='program.FundingCategory', verbose_name='Funding Category'), + model_name="show", + name="fundingcategory", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="shows", + to="program.FundingCategory", + verbose_name="Funding Category", + ), ), ] diff --git a/program/migrations/0020_auto_20190810_1341.py b/program/migrations/0020_auto_20190810_1341.py index b22e98115a21422e1854b06562bf967478ef232c..d98c18890430567152fe11bc8558f2578aee339d 100644 --- a/program/migrations/0020_auto_20190810_1341.py +++ b/program/migrations/0020_auto_20190810_1341.py @@ -2,20 +2,27 @@ # Generated by Django 1.11.3 on 2019-08-10 13:41 from __future__ import unicode_literals -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('program', '0019_auto_20190810_1340'), + ("program", "0019_auto_20190810_1340"), ] operations = [ migrations.AlterField( - model_name='show', - name='fundingcategory', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='shows', to='program.FundingCategory', verbose_name='Funding Category'), + model_name="show", + name="fundingcategory", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="shows", + to="program.FundingCategory", + verbose_name="Funding Category", + ), ), ] diff --git a/program/migrations/0021_show_is_active.py b/program/migrations/0021_show_is_active.py index 8d9d13ad8c84e71c69d54accee09ddd6bc322092..85b7188891a7b4ae733a92051ea57472ae5f4f91 100644 --- a/program/migrations/0021_show_is_active.py +++ b/program/migrations/0021_show_is_active.py @@ -8,13 +8,13 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('program', '0020_auto_20190810_1341'), + ("program", "0020_auto_20190810_1341"), ] operations = [ migrations.AddField( - model_name='show', - name='is_active', - field=models.BooleanField(default=True, verbose_name='Is active?'), + model_name="show", + name="is_active", + field=models.BooleanField(default=True, verbose_name="Is active?"), ), ] diff --git a/program/migrations/0022_show_is_public.py b/program/migrations/0022_show_is_public.py index 51136dd18633f0b41edaf94c37f6f39862f31954..adce478b0c03e2c3733530f1c06256fea059f8f6 100644 --- a/program/migrations/0022_show_is_public.py +++ b/program/migrations/0022_show_is_public.py @@ -8,13 +8,18 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('program', '0021_show_is_active'), + ("program", "0021_show_is_active"), ] operations = [ migrations.AddField( - model_name='show', - name='is_public', - field=models.BooleanField(default=False, help_text='Files and Playlists of Public Shows can only be changed by owners but may be used by everyone.', verbose_name='Is Public?'), + model_name="show", + name="is_public", + field=models.BooleanField( + default=False, + help_text="Files and Playlists of Public Shows can only be changed by owners but" + " may be used by everyone.", + verbose_name="Is Public?", + ), ), ] diff --git a/program/models.py b/program/models.py index f0f0df2b5cf2237b144a9870f5f643e23ce2fcfc..6a4762df388c594a6d4534e0e6e966b12234588f 100644 --- a/program/models.py +++ b/program/models.py @@ -22,6 +22,8 @@ from datetime import datetime, time, timedelta from dateutil.relativedelta import relativedelta from dateutil.rrule import rrule +from versatileimagefield.fields import PPOIField, VersatileImageField + from django.contrib.auth.models import User from django.core.exceptions import ObjectDoesNotExist from django.db import models @@ -29,10 +31,12 @@ from django.db.models import Q from django.forms.models import model_to_dict from django.utils import timezone from django.utils.translation import gettext_lazy as _ -from versatileimagefield.fields import VersatileImageField, PPOIField - -from steering.settings import THUMBNAIL_SIZES, AUTO_SET_UNTIL_DATE_TO_END_OF_YEAR, AUTO_SET_UNTIL_DATE_TO_DAYS_IN_FUTURE -from program.utils import parse_datetime, parse_date, parse_time +from program.utils import parse_date, parse_datetime, parse_time +from steering.settings import ( + AUTO_SET_UNTIL_DATE_TO_DAYS_IN_FUTURE, + AUTO_SET_UNTIL_DATE_TO_END_OF_YEAR, + THUMBNAIL_SIZES, +) class Type(models.Model): @@ -41,7 +45,10 @@ class Type(models.Model): is_active = models.BooleanField(default=True) class Meta: - ordering = ('name',) + ordering = ("name",) + + def __str__(self): + return self.name class Category(models.Model): @@ -52,7 +59,10 @@ class Category(models.Model): description = models.TextField(blank=True) class Meta: - ordering = ('name',) + ordering = ("name",) + + def __str__(self): + return self.name class Topic(models.Model): @@ -62,7 +72,10 @@ class Topic(models.Model): is_active = models.BooleanField(default=True) class Meta: - ordering = ('name',) + ordering = ("name",) + + def __str__(self): + return self.name class MusicFocus(models.Model): @@ -72,7 +85,10 @@ class MusicFocus(models.Model): is_active = models.BooleanField(default=True) class Meta: - ordering = ('name',) + ordering = ("name",) + + def __str__(self): + return self.name class FundingCategory(models.Model): @@ -82,7 +98,10 @@ class FundingCategory(models.Model): is_active = models.BooleanField(default=True) class Meta: - ordering = ('name',) + ordering = ("name",) + + def __str__(self): + return self.name class Language(models.Model): @@ -90,7 +109,10 @@ class Language(models.Model): is_active = models.BooleanField(default=True) class Meta: - ordering = ('language',) + ordering = ("language",) + + def __str__(self): + return self.name class Host(models.Model): @@ -102,11 +124,20 @@ class Host(models.Model): ppoi = PPOIField() height = models.PositiveIntegerField(blank=True, null=True, editable=False) width = models.PositiveIntegerField(blank=True, null=True, editable=False) - image = VersatileImageField(blank=True, null=True, upload_to='host_images', width_field='width', - height_field='height', ppoi_field='ppoi') + image = VersatileImageField( + blank=True, + null=True, + upload_to="host_images", + width_field="width", + height_field="height", + ppoi_field="ppoi", + ) class Meta: - ordering = ('name',) + ordering = ("name",) + + def __str__(self): + return self.name def save(self, *args, **kwargs): super(Host, self).save(*args, **kwargs) @@ -116,44 +147,52 @@ class Host(models.Model): for size in THUMBNAIL_SIZES: self.image.thumbnail = self.image.crop[size].name - # FIXME: this does not belong here - @staticmethod - def is_editable(host_view_set, host_id): - """ - Whether the given host is assigned to a show the current user owns - @return boolean - """ - if host_view_set.request.user.is_superuser: - return True - - host_ids = Host.objects.filter(shows__in=host_view_set.request.user.shows.all()).distinct().values_list('id', flat=True) - return int(host_id) in host_ids - class Link(models.Model): - host = models.ForeignKey(Host, on_delete=models.CASCADE, related_name='links') + host = models.ForeignKey(Host, on_delete=models.CASCADE, related_name="links") description = models.CharField(max_length=8) url = models.URLField() + def __str__(self): + return self.url + class Show(models.Model): - predecessor = models.ForeignKey('self', blank=True, null=True, on_delete=models.CASCADE, related_name='successors') - hosts = models.ManyToManyField(Host, blank=True, related_name='shows') - owners = models.ManyToManyField(User, blank=True, related_name='shows') - language = models.ManyToManyField(Language, blank=True, related_name='language') - type = models.ForeignKey(Type, on_delete=models.CASCADE, related_name='shows') - category = models.ManyToManyField(Category, blank=True, related_name='shows') - funding_category = models.ForeignKey(FundingCategory, null=True, on_delete=models.CASCADE, blank=True, related_name='shows') - topic = models.ManyToManyField(Topic, blank=True, related_name='shows') - music_focus = models.ManyToManyField(MusicFocus, blank=True, related_name='shows') + predecessor = models.ForeignKey( + "self", + blank=True, + null=True, + on_delete=models.CASCADE, + related_name="successors", + ) + hosts = models.ManyToManyField(Host, blank=True, related_name="shows") + owners = models.ManyToManyField(User, blank=True, related_name="shows") + language = models.ManyToManyField(Language, blank=True, related_name="language") + type = models.ForeignKey(Type, on_delete=models.CASCADE, related_name="shows") + category = models.ManyToManyField(Category, blank=True, related_name="shows") + funding_category = models.ForeignKey( + FundingCategory, + null=True, + on_delete=models.CASCADE, + blank=True, + related_name="shows", + ) + topic = models.ManyToManyField(Topic, blank=True, related_name="shows") + music_focus = models.ManyToManyField(MusicFocus, blank=True, related_name="shows") name = models.CharField(max_length=255) slug = models.CharField(max_length=255, unique=True) ppoi = PPOIField() height = models.PositiveIntegerField(blank=True, null=True, editable=False) width = models.PositiveIntegerField(blank=True, null=True, editable=False) - image = VersatileImageField(blank=True, null=True, upload_to='show_images', width_field='width', height_field='height', - ppoi_field='ppoi') - logo = models.ImageField(blank=True, null=True, upload_to='show_images') + image = VersatileImageField( + blank=True, + null=True, + upload_to="show_images", + width_field="width", + height_field="height", + ppoi_field="ppoi", + ) + logo = models.ImageField(blank=True, null=True, upload_to="show_images") short_description = models.TextField() description = models.TextField(blank=True, null=True) email = models.EmailField(blank=True, null=True) @@ -164,20 +203,10 @@ class Show(models.Model): is_public = models.BooleanField(default=False) class Meta: - ordering = ('slug',) + ordering = ("slug",) - # FIXME: this does not belong here - @staticmethod - def is_editable(show_view_set, show_id): - """ - Whether the current user is owner of the given show - @return boolean - """ - if show_view_set.request.user.is_superuser: - return True - - show_ids = show_view_set.request.user.shows.all().values_list('id', flat=True) - return int(show_id) in show_ids + def __str__(self): + return self.name class RRule(models.Model): @@ -188,12 +217,15 @@ class RRule(models.Model): count = models.IntegerField(blank=True, null=True) class Meta: - ordering = ('pk',) + ordering = ("pk",) + + def __str__(self): + return self.name class Schedule(models.Model): - rrule = models.ForeignKey(RRule, on_delete=models.CASCADE, related_name='schedules') - show = models.ForeignKey(Show, on_delete=models.CASCADE, related_name='schedules') + rrule = models.ForeignKey(RRule, on_delete=models.CASCADE, related_name="schedules") + show = models.ForeignKey(Show, on_delete=models.CASCADE, related_name="schedules") by_weekday = models.IntegerField() first_date = models.DateField() start_time = models.TimeField() @@ -205,50 +237,67 @@ class Schedule(models.Model): default_playlist_id = models.IntegerField(blank=True, null=True) class Meta: - ordering = ('first_date', 'start_time') + ordering = ("first_date", "start_time") # FIXME: this does not belong here @staticmethod def instantiate_upcoming(sdl, show_pk, pk=None): """Returns an upcoming schedule instance for conflict resolution""" pk = int(pk) if pk is not None else None - rrule = RRule.objects.get(pk=int(sdl['rrule'])) + rrule = RRule.objects.get(pk=int(sdl["rrule"])) show = Show.objects.get(pk=int(show_pk)) - is_repetition = True if sdl.get('is_repetition') is True else False - default_playlist_id = int(sdl['default_playlist_id']) if sdl.get('default_playlist_id') else None - add_days_no = int(sdl['add_days_no']) if sdl.get('add_days_no') else None - add_business_days_only = True if sdl.get('add_business_days_only') is True else False + is_repetition = True if sdl.get("is_repetition") is True else False + default_playlist_id = ( + int(sdl["default_playlist_id"]) if sdl.get("default_playlist_id") else None + ) + add_days_no = int(sdl["add_days_no"]) if sdl.get("add_days_no") else None + add_business_days_only = ( + True if sdl.get("add_business_days_only") is True else False + ) # TODO: replace `dstart` with `first_date` when the dashboard is updated - first_date = parse_date(str(sdl['dstart'])) + first_date = parse_date(str(sdl["dstart"])) # TODO: replace `tstart` with `start_time` when the dashboard is updated - start_time = sdl['tstart'] + ':00' if len(str(sdl['tstart'])) == 5 else sdl['tstart'] + start_time = ( + sdl["tstart"] + ":00" if len(str(sdl["tstart"])) == 5 else sdl["tstart"] + ) # TODO: replace `tend` with `end_time` when the dashboard is updated - end_time = sdl['tend'] + ':00' if len(str(sdl['tend'])) == 5 else sdl['tend'] + end_time = sdl["tend"] + ":00" if len(str(sdl["tend"])) == 5 else sdl["tend"] start_time = parse_time(str(start_time)) end_time = parse_time(str(end_time)) # TODO: replace `until` with `last_date` when the dashboard is updated - if sdl['until']: - last_date = parse_date(str(sdl['until'])) + if sdl["until"]: + last_date = parse_date(str(sdl["until"])) else: # If no until date was set # Set it to the end of the year # Or add x days if AUTO_SET_UNTIL_DATE_TO_END_OF_YEAR: year = timezone.now().year - last_date = parse_date(f'{year}-12-31') + last_date = parse_date(f"{year}-12-31") else: - last_date = first_date + timedelta(days=+AUTO_SET_UNTIL_DATE_TO_DAYS_IN_FUTURE) + last_date = first_date + timedelta( + days=+AUTO_SET_UNTIL_DATE_TO_DAYS_IN_FUTURE + ) # TODO: replace `byweekday` with `by_weekday` when the dashboard is updated - schedule = Schedule(pk=pk, by_weekday=sdl['byweekday'], rrule=rrule, - first_date=first_date, start_time=start_time, end_time=end_time, - last_date=last_date, is_repetition=is_repetition, - default_playlist_id=default_playlist_id, show=show, - add_days_no=add_days_no, add_business_days_only=add_business_days_only) + schedule = Schedule( + pk=pk, + by_weekday=sdl["byweekday"], + rrule=rrule, + first_date=first_date, + start_time=start_time, + end_time=end_time, + last_date=last_date, + is_repetition=is_repetition, + default_playlist_id=default_playlist_id, + show=show, + add_days_no=add_days_no, + add_business_days_only=add_business_days_only, + ) return schedule @@ -285,7 +334,9 @@ class Schedule(models.Model): elif schedule.rrule.freq == 3 and schedule.rrule.pk == 2: # Daily timeslots by_weekday_start = (0, 1, 2, 3, 4, 5, 6) by_weekday_end = (0, 1, 2, 3, 4, 5, 6) - elif schedule.rrule.freq == 3 and schedule.rrule.pk == 3: # Business days MO - FR/SA + elif ( + schedule.rrule.freq == 3 and schedule.rrule.pk == 3 + ): # Business days MO - FR/SA by_weekday_start = (0, 1, 2, 3, 4) if schedule.end_time < schedule.start_time: # End days for over midnight @@ -315,21 +366,29 @@ class Schedule(models.Model): starts.append(datetime.combine(schedule.first_date, schedule.start_time)) ends.append(datetime.combine(last_date, schedule.end_time)) else: - starts = list(rrule(freq=schedule.rrule.freq, - dtstart=datetime.combine(schedule.first_date, schedule.start_time), - interval=schedule.rrule.interval, - until=schedule.last_date + relativedelta(days=+1), - bysetpos=schedule.rrule.by_set_pos, - byweekday=by_weekday_start, - byweekno=by_week_no)) - - ends = list(rrule(freq=schedule.rrule.freq, - dtstart=datetime.combine(last_date, schedule.end_time), - interval=schedule.rrule.interval, - until=schedule.last_date + relativedelta(days=+1), - bysetpos=schedule.rrule.by_set_pos, - byweekday=by_weekday_end, - byweekno=by_week_no_end)) + starts = list( + rrule( + freq=schedule.rrule.freq, + dtstart=datetime.combine(schedule.first_date, schedule.start_time), + interval=schedule.rrule.interval, + until=schedule.last_date + relativedelta(days=+1), + bysetpos=schedule.rrule.by_set_pos, + byweekday=by_weekday_start, + byweekno=by_week_no, + ) + ) + + ends = list( + rrule( + freq=schedule.rrule.freq, + dtstart=datetime.combine(last_date, schedule.end_time), + interval=schedule.rrule.interval, + until=schedule.last_date + relativedelta(days=+1), + bysetpos=schedule.rrule.by_set_pos, + byweekday=by_weekday_end, + byweekno=by_week_no_end, + ) + ) for k in range(min(len(starts), len(ends))): @@ -338,45 +397,59 @@ class Schedule(models.Model): # produces wrong end dates if the 1st Tuesday is before the 1st Monday # In this case we take the next day instead of rrule's calculated end if starts[k] > ends[k]: - ends[k] = datetime.combine(starts[k] + relativedelta(days=+1), schedule.end_time) + ends[k] = datetime.combine( + starts[k] + relativedelta(days=+1), schedule.end_time + ) - ''' + """ Add a number of days to the generated dates? This can be helpful for repetitions: Examples: - 1. If RRule is "Every 1st Monday" and we want its repetition alyways to be on the following day, - the repetition's RRule is the same but add_days_no is 1 + 1. If RRule is "Every 1st Monday" and we want its repetition alyways to be on the + following day, the repetition's RRule is the same but add_days_no is 1 If we would set the repetition to "Every 1st Tuesday" instead we will get unmeant results if the 1st Tuesday is before the 1st Monday (e.g. 1st Tue = May 1 2018, 1st Mon = May 7 2018) - 2. If RRule is "Every 1st Friday" and we want its repetition always to be on the following business day, - the repetition's RRule is the same but add_days_no is 1 and add_business_days_only is True - (e.g. original date = Fri, March 2 2018; generated date = Mon, March 5 2018) + 2. If RRule is "Every 1st Friday" and we want its repetition always to be on the + following business day, the repetition's RRule is the same but add_days_no is 1 + and add_business_days_only is True (e.g. original date = Fri, March 2 2018; + generated date = Mon, March 5 2018) In the UI these can be presets: "On the following day" (add_days_no=1,add_business_days_only=False) or "On the following business day" (add_days_no=1,add_business_days_only=True) - ''' + """ if schedule.add_days_no is not None and schedule.add_days_no > 0: - # If only business days and weekday is Fri, Sat or Sun: add add_days_no beginning from Sunday + # If only business days and weekday is Fri, Sat or Sun: add add_days_no beginning + # from Sunday weekday = datetime.date(starts[k]).weekday() if schedule.add_business_days_only and weekday > 3: days_until_sunday = 6 - weekday - starts[k] = starts[k] + relativedelta(days=+days_until_sunday + schedule.add_days_no) - ends[k] = ends[k] + relativedelta(days=+days_until_sunday + schedule.add_days_no) + starts[k] = starts[k] + relativedelta( + days=+days_until_sunday + schedule.add_days_no + ) + ends[k] = ends[k] + relativedelta( + days=+days_until_sunday + schedule.add_days_no + ) else: starts[k] = starts[k] + relativedelta(days=+schedule.add_days_no) ends[k] = ends[k] + relativedelta(days=+schedule.add_days_no) if ends[k].date() > schedule.last_date: schedule.last_date = ends[k].date() - timeslots.append(TimeSlot(schedule=schedule, start=timezone.make_aware(starts[k]), end=timezone.make_aware(ends[k]))) + timeslots.append( + TimeSlot( + schedule=schedule, + start=timezone.make_aware(starts[k]), + end=timezone.make_aware(ends[k]), + ) + ) return timeslots @@ -395,7 +468,9 @@ class Schedule(models.Model): collision = TimeSlot.objects.get_colliding_timeslots(ts) if collision: - collisions.append(collision[0]) # TODO: Do we really always retrieve one? + collisions.append( + collision[0] + ) # TODO: Do we really always retrieve one? else: collisions.append(None) @@ -406,7 +481,8 @@ class Schedule(models.Model): def generate_conflicts(timeslots): """ Tests a list of timeslot objects for colliding timeslots in the database - Returns a list of conflicts containing dicts of projected timeslots, collisions and solutions + Returns a list of conflicts containing dicts of projected timeslots, collisions and + solutions """ conflicts = {} @@ -415,10 +491,6 @@ class Schedule(models.Model): # Cycle each timeslot for ts in timeslots: - - # Contains one conflict: a projected timeslot, collisions and solutions - conflict = {} - # Contains collisions collisions = [] @@ -426,36 +498,52 @@ class Schedule(models.Model): solution_choices = set() # Get collisions for each timeslot - collision_list = list(TimeSlot.objects.get_colliding_timeslots(ts).order_by('start')) + collision_list = list( + TimeSlot.objects.get_colliding_timeslots(ts).order_by("start") + ) # Add the projected timeslot - projected_entry = {'hash': ts.hash, 'start': str(ts.start), 'end': str(ts.end)} + projected_entry = { + "hash": ts.hash, + "start": str(ts.start), + "end": str(ts.end), + } for c in collision_list: # Add the collision - collision = {'id': c.id, 'start': str(c.start), 'end': str(c.end), 'playlist_id': c.playlist_id, - 'show': c.show.id, 'show_name': c.show.name, 'is_repetition': c.is_repetition, - 'schedule': c.schedule_id, 'memo': c.memo} + collision = { + "id": c.id, + "start": str(c.start), + "end": str(c.end), + "playlist_id": c.playlist_id, + "show": c.show.id, + "show_name": c.show.name, + "is_repetition": c.is_repetition, + "schedule": c.schedule_id, + "memo": c.memo, + } # Get note try: - note = Note.objects.get(timeslot=c.id).values_list('id', flat=True) - collision['note_id'] = note + note = Note.objects.get(timeslot=c.id).values_list("id", flat=True) + collision["note_id"] = note except ObjectDoesNotExist: pass collisions.append(collision) - '''Determine acceptable solutions''' + """Determine acceptable solutions""" if len(collision_list) > 1: - # If there is more than one collision: Only these two are supported at the moment - solution_choices.add('theirs') - solution_choices.add('ours') + # If there is more than one collision: Only these two are supported at the + # moment + solution_choices.add("theirs") + solution_choices.add("ours") else: - # These two are always possible: Either keep theirs and remove ours or vice versa - solution_choices.add('theirs') - solution_choices.add('ours') + # These two are always possible: Either keep theirs and remove ours or vice + # versa + solution_choices.add("theirs") + solution_choices.add("ours") # Partly overlapping: projected starts earlier than existing and ends earlier # @@ -468,8 +556,8 @@ class Schedule(models.Model): # +--+ # if ts.end > c.start > ts.start <= c.end: - solution_choices.add('theirs-end') - solution_choices.add('ours-end') + solution_choices.add("theirs-end") + solution_choices.add("ours-end") # Partly overlapping: projected starts later than existing and ends later # @@ -482,8 +570,8 @@ class Schedule(models.Model): # +--+ # if c.start <= ts.start < c.end < ts.end: - solution_choices.add('theirs-start') - solution_choices.add('ours-start') + solution_choices.add("theirs-start") + solution_choices.add("ours-start") # Fully overlapping: projected starts earlier and ends later than existing # @@ -495,9 +583,9 @@ class Schedule(models.Model): # +--+ # if ts.start < c.start and ts.end > c.end: - solution_choices.add('theirs-end') - solution_choices.add('theirs-start') - solution_choices.add('theirs-both') + solution_choices.add("theirs-end") + solution_choices.add("theirs-start") + solution_choices.add("theirs-both") # Fully overlapping: projected starts later and ends earlier than existing # @@ -509,21 +597,21 @@ class Schedule(models.Model): # +--+ # if ts.start > c.start and ts.end < c.end: - solution_choices.add('ours-end') - solution_choices.add('ours-start') - solution_choices.add('ours-both') + solution_choices.add("ours-end") + solution_choices.add("ours-start") + solution_choices.add("ours-both") if len(collisions) > 0: - solutions[ts.hash] = '' + solutions[ts.hash] = "" - projected_entry['collisions'] = collisions - projected_entry['solution_choices'] = solution_choices + projected_entry["collisions"] = collisions + projected_entry["solution_choices"] = solution_choices projected.append(projected_entry) - conflicts['projected'] = projected - conflicts['solutions'] = solutions - conflicts['notes'] = {} - conflicts['playlists'] = {} + conflicts["projected"] = projected + conflicts["solutions"] = solutions + conflicts["notes"] = {} + conflicts["playlists"] = {} return conflicts @@ -550,14 +638,18 @@ class Schedule(models.Model): existing_schedule = Schedule.objects.get(pk=int(schedule_pk)) if schedule.last_date > existing_schedule.last_date: - last_timeslot = TimeSlot.objects.filter(schedule=existing_schedule).order_by('start').reverse()[0] + last_timeslot = ( + TimeSlot.objects.filter(schedule=existing_schedule) + .order_by("start") + .reverse()[0] + ) gen_schedule.first_date = last_timeslot.start.date() + timedelta(days=1) timeslots = Schedule.generate_timeslots(gen_schedule) # Generate conflicts and add schedule conflicts = Schedule.generate_conflicts(timeslots) - conflicts['schedule'] = model_to_dict(schedule) + conflicts["schedule"] = model_to_dict(schedule) return conflicts @@ -572,8 +664,8 @@ class Schedule(models.Model): Returns an empty list if resolution was successful """ - sdl = data['schedule'] - solutions = data['solutions'] + sdl = data["schedule"] + solutions = data["solutions"] # Regenerate conflicts schedule = Schedule.instantiate_upcoming(sdl, show_pk, schedule_pk) @@ -581,15 +673,17 @@ class Schedule(models.Model): conflicts = Schedule.make_conflicts(sdl, schedule_pk, show_pk) if schedule.rrule.freq > 0 and schedule.first_date == schedule.last_date: - return {'detail': _("Start and until dates mustn't be the same")} + return {"detail": _("Start and until dates mustn't be the same")} if schedule.last_date < schedule.first_date: - return {'detail': _("Until date mustn't be before start")} + return {"detail": _("Until date mustn't be before start")} - num_conflicts = len([pr for pr in conflicts['projected'] if len(pr['collisions']) > 0]) + num_conflicts = len( + [pr for pr in conflicts["projected"] if len(pr["collisions"]) > 0] + ) if len(solutions) != num_conflicts: - return {'detail': _("Numbers of conflicts and solutions don't match.")} + return {"detail": _("Numbers of conflicts and solutions don't match.")} # Projected timeslots to create create = [] @@ -603,48 +697,52 @@ class Schedule(models.Model): # Error messages errors = {} - for ts in conflicts['projected']: + for ts in conflicts["projected"]: # If no solution necessary # # - Create the projected timeslot and skip # - if 'solution_choices' not in ts or len(ts['collisions']) < 1: - projected_ts = TimeSlot.objects.instantiate(ts['start'], ts['end'], schedule, show) + if "solution_choices" not in ts or len(ts["collisions"]) < 1: + projected_ts = TimeSlot.objects.instantiate( + ts["start"], ts["end"], schedule, show + ) create.append(projected_ts) continue # Check hash (if start, end, rrule or byweekday changed) - if not ts['hash'] in solutions: - errors[ts['hash']] = _("This change on the timeslot is not allowed.") + if not ts["hash"] in solutions: + errors[ts["hash"]] = _("This change on the timeslot is not allowed.") continue # If no resolution given # # - Skip # - if solutions[ts['hash']] == '': - errors[ts['hash']] = _("No solution given.") + if solutions[ts["hash"]] == "": + errors[ts["hash"]] = _("No solution given.") continue # If resolution is not accepted for this conflict # # - Skip # - if not solutions[ts['hash']] in ts['solution_choices']: - errors[ts['hash']] = _("Given solution is not accepted for this conflict.") + if not solutions[ts["hash"]] in ts["solution_choices"]: + errors[ts["hash"]] = _( + "Given solution is not accepted for this conflict." + ) continue - '''Conflict resolution''' + """Conflict resolution""" - existing = ts['collisions'][0] - solution = solutions[ts['hash']] + existing = ts["collisions"][0] + solution = solutions[ts["hash"]] # theirs # # - Discard the projected timeslot # - Keep the existing collision(s) # - if solution == 'theirs': + if solution == "theirs": continue # ours @@ -652,14 +750,16 @@ class Schedule(models.Model): # - Create the projected timeslot # - Delete the existing collision(s) # - if solution == 'ours': - projected_ts = TimeSlot.objects.instantiate(ts['start'], ts['end'], schedule, show) + if solution == "ours": + projected_ts = TimeSlot.objects.instantiate( + ts["start"], ts["end"], schedule, show + ) create.append(projected_ts) # Delete collision(s) - for ex in ts['collisions']: + for ex in ts["collisions"]: try: - existing_ts = TimeSlot.objects.get(pk=ex['id']) + existing_ts = TimeSlot.objects.get(pk=ex["id"]) delete.append(existing_ts) except ObjectDoesNotExist: pass @@ -669,8 +769,10 @@ class Schedule(models.Model): # - Keep the existing timeslot # - Create projected with end of existing start # - if solution == 'theirs-end': - projected_ts = TimeSlot.objects.instantiate(ts['start'], existing['start'], schedule, show) + if solution == "theirs-end": + projected_ts = TimeSlot.objects.instantiate( + ts["start"], existing["start"], schedule, show + ) create.append(projected_ts) # ours-end @@ -678,12 +780,14 @@ class Schedule(models.Model): # - Create the projected timeslot # - Change the start of the existing collision to projected end # - if solution == 'ours-end': - projected_ts = TimeSlot.objects.instantiate(ts['start'], ts['end'], schedule, show) + if solution == "ours-end": + projected_ts = TimeSlot.objects.instantiate( + ts["start"], ts["end"], schedule, show + ) create.append(projected_ts) - existing_ts = TimeSlot.objects.get(pk=existing['id']) - existing_ts.start = parse_datetime(ts['end']) + existing_ts = TimeSlot.objects.get(pk=existing["id"]) + existing_ts.start = parse_datetime(ts["end"]) update.append(existing_ts) # theirs-start @@ -691,8 +795,10 @@ class Schedule(models.Model): # - Keep existing # - Create projected with start time of existing end # - if solution == 'theirs-start': - projected_ts = TimeSlot.objects.instantiate(existing['end'], ts['end'], schedule, show) + if solution == "theirs-start": + projected_ts = TimeSlot.objects.instantiate( + existing["end"], ts["end"], schedule, show + ) create.append(projected_ts) # ours-start @@ -700,24 +806,31 @@ class Schedule(models.Model): # - Create the projected timeslot # - Change end of existing to projected start # - if solution == 'ours-start': - projected_ts = TimeSlot.objects.instantiate(ts['start'], ts['end'], schedule, show) + if solution == "ours-start": + projected_ts = TimeSlot.objects.instantiate( + ts["start"], ts["end"], schedule, show + ) create.append(projected_ts) - existing_ts = TimeSlot.objects.get(pk=existing['id']) - existing_ts.end = parse_datetime(ts['start']) + existing_ts = TimeSlot.objects.get(pk=existing["id"]) + existing_ts.end = parse_datetime(ts["start"]) update.append(existing_ts) # theirs-both # # - Keep existing - # - Create two projected timeslots with end of existing start and start of existing end + # - Create two projected timeslots with end of existing start and start of existing + # end # - if solution == 'theirs-both': - projected_ts = TimeSlot.objects.instantiate(ts['start'], existing['start'], schedule, show) + if solution == "theirs-both": + projected_ts = TimeSlot.objects.instantiate( + ts["start"], existing["start"], schedule, show + ) create.append(projected_ts) - projected_ts = TimeSlot.objects.instantiate(existing['end'], ts['end'], schedule, show) + projected_ts = TimeSlot.objects.instantiate( + existing["end"], ts["end"], schedule, show + ) create.append(projected_ts) # ours-both @@ -727,58 +840,72 @@ class Schedule(models.Model): # - Set existing end time to projected start # - Create another one with start = projected end and end = existing end # - if solution == 'ours-both': - projected_ts = TimeSlot.objects.instantiate(ts['start'], ts['end'], schedule, show) + if solution == "ours-both": + projected_ts = TimeSlot.objects.instantiate( + ts["start"], ts["end"], schedule, show + ) create.append(projected_ts) - existing_ts = TimeSlot.objects.get(pk=existing['id']) - existing_ts.end = parse_datetime(ts['start']) + existing_ts = TimeSlot.objects.get(pk=existing["id"]) + existing_ts.end = parse_datetime(ts["start"]) update.append(existing_ts) - projected_ts = TimeSlot.objects.instantiate(ts['end'], existing['end'], schedule, show) + projected_ts = TimeSlot.objects.instantiate( + ts["end"], existing["end"], schedule, show + ) create.append(projected_ts) # If there were any errors, don't make any db changes yet # but add error messages and return already chosen solutions if len(errors) > 0: - conflicts = Schedule.make_conflicts(model_to_dict(schedule), schedule.pk, show.pk) + conflicts = Schedule.make_conflicts( + model_to_dict(schedule), schedule.pk, show.pk + ) - partly_resolved = conflicts['projected'] + partly_resolved = conflicts["projected"] saved_solutions = {} # Add already chosen resolutions and error message to conflict - for index, c in enumerate(conflicts['projected']): + for index, c in enumerate(conflicts["projected"]): # The element should only exist if there was a collision - if len(c['collisions']) > 0: - saved_solutions[c['hash']] = '' + if len(c["collisions"]) > 0: + saved_solutions[c["hash"]] = "" - if c['hash'] in solutions and solutions[c['hash']] in c['solution_choices']: - saved_solutions[c['hash']] = solutions[c['hash']] + if ( + c["hash"] in solutions + and solutions[c["hash"]] in c["solution_choices"] + ): + saved_solutions[c["hash"]] = solutions[c["hash"]] - if c['hash'] in errors: - partly_resolved[index]['error'] = errors[c['hash']] + if c["hash"] in errors: + partly_resolved[index]["error"] = errors[c["hash"]] # Re-insert post data - conflicts['projected'] = partly_resolved - conflicts['solutions'] = saved_solutions - conflicts['notes'] = data.get('notes') - conflicts['playlists'] = data.get('playlists') + conflicts["projected"] = partly_resolved + conflicts["solutions"] = saved_solutions + conflicts["notes"] = data.get("notes") + conflicts["playlists"] = data.get("playlists") return conflicts # Collect upcoming timeslots to delete which might still remain - del_timeslots = TimeSlot.objects.filter(schedule=schedule, start__gt=schedule.last_date) + del_timeslots = TimeSlot.objects.filter( + schedule=schedule, start__gt=schedule.last_date + ) for del_ts in del_timeslots: delete.append(del_ts) # If 'dryrun' is true, just return the projected changes instead of executing them - if 'dryrun' in sdl and sdl['dryrun']: - output = {'create': [model_to_dict(ts) for ts in create], 'update': [model_to_dict(ts) for ts in update], - 'delete': [model_to_dict(ts) for ts in delete]} + if "dryrun" in sdl and sdl["dryrun"]: + output = { + "create": [model_to_dict(ts) for ts in create], + "update": [model_to_dict(ts) for ts in update], + "delete": [model_to_dict(ts) for ts in delete], + } return output - '''Database changes if no errors found''' + """Database changes if no errors found""" # Only save schedule if timeslots were created if create: @@ -794,15 +921,15 @@ class Schedule(models.Model): ts.schedule = schedule # Reassign playlists - if 'playlists' in data and ts.hash in data['playlists']: - ts.playlist_id = int(data['playlists'][ts.hash]) + if "playlists" in data and ts.hash in data["playlists"]: + ts.playlist_id = int(data["playlists"][ts.hash]) ts.save() # Reassign notes - if 'notes' in data and ts.hash in data['notes']: + if "notes" in data and ts.hash in data["notes"]: try: - note = Note.objects.get(pk=int(data['notes'][ts.hash])) + note = Note.objects.get(pk=int(data["notes"][ts.hash])) note.timeslot_id = ts.id note.save(update_fields=["timeslot_id"]) @@ -822,43 +949,54 @@ class Schedule(models.Model): class TimeSlotManager(models.Manager): @staticmethod def instantiate(start, end, schedule, show): - return TimeSlot(start=parse_datetime(start), - end=parse_datetime(end), - show=show, is_repetition=schedule.is_repetition, schedule=schedule) + return TimeSlot( + start=parse_datetime(start), + end=parse_datetime(end), + show=show, + is_repetition=schedule.is_repetition, + schedule=schedule, + ) @staticmethod def get_24h_timeslots(start): end = timezone.make_aware(start + timedelta(hours=24)) - return TimeSlot.objects.filter(Q(start__lte=start, end__gte=start) | - Q(start__gt=start, start__lt=end)).exclude(end=start) + return TimeSlot.objects.filter( + Q(start__lte=start, end__gte=start) | Q(start__gt=start, start__lt=end) + ).exclude(end=start) @staticmethod def get_7d_timeslots(start): start = datetime.combine(start, time(0, 0)) end = timezone.make_aware(start + timedelta(days=7)) - return TimeSlot.objects.filter(Q(start__lte=start, end__gte=start) | - Q(start__gt=start, start__lt=end)).exclude(end=start) + return TimeSlot.objects.filter( + Q(start__lte=start, end__gte=start) | Q(start__gt=start, start__lt=end) + ).exclude(end=start) @staticmethod def get_timerange_timeslots(start, end): - return TimeSlot.objects.filter(Q(start__lte=start, end__gte=start) | - Q(start__gt=start, start__lt=end)).exclude(end=start) + return TimeSlot.objects.filter( + Q(start__lte=start, end__gte=start) | Q(start__gt=start, start__lt=end) + ).exclude(end=start) @staticmethod def get_colliding_timeslots(timeslot): return TimeSlot.objects.filter( - (Q(start__lt=timeslot.end) & Q(end__gte=timeslot.end)) | - (Q(end__gt=timeslot.start) & Q(end__lte=timeslot.end)) | - (Q(start__gte=timeslot.start) & Q(end__lte=timeslot.end)) | - (Q(start__lte=timeslot.start) & Q(end__gte=timeslot.end)) + (Q(start__lt=timeslot.end) & Q(end__gte=timeslot.end)) + | (Q(end__gt=timeslot.start) & Q(end__lte=timeslot.end)) + | (Q(start__gte=timeslot.start) & Q(end__lte=timeslot.end)) + | (Q(start__lte=timeslot.start) & Q(end__gte=timeslot.end)) ) class TimeSlot(models.Model): - schedule = models.ForeignKey(Schedule, on_delete=models.CASCADE, related_name='timeslots') - show = models.ForeignKey(Show, editable=False, on_delete=models.CASCADE, related_name='timeslots') + schedule = models.ForeignKey( + Schedule, on_delete=models.CASCADE, related_name="timeslots" + ) + show = models.ForeignKey( + Show, editable=False, on_delete=models.CASCADE, related_name="timeslots" + ) start = models.DateTimeField() end = models.DateTimeField() memo = models.TextField(blank=True) @@ -869,7 +1007,21 @@ class TimeSlot(models.Model): objects = TimeSlotManager() class Meta: - ordering = ('start', 'end') + ordering = ("start", "end") + + def __str__(self): + if self.start.date() == self.end.date(): + time_span = "{0}, {1} - {2}".format( + self.start.strftime("%x"), + self.start.strftime("%X"), + self.end.strftime("%X"), + ) + else: + time_span = "{0} - {1}".format( + self.start.strftime("%X %x"), + self.end.strftime("%X %x"), + ) + return f"{str(self.show)} ({time_span})" def save(self, *args, **kwargs): self.show = self.schedule.show @@ -878,8 +1030,13 @@ class TimeSlot(models.Model): @property def hash(self): - string = str(self.start) + str(self.end) + str(self.schedule.rrule.id) + str(self.schedule.by_weekday) - return str(''.join(s for s in string if s.isdigit())) + string = ( + str(self.start) + + str(self.end) + + str(self.schedule.rrule.id) + + str(self.schedule.by_weekday) + ) + return str("".join(s for s in string if s.isdigit())) class Note(models.Model): @@ -891,18 +1048,33 @@ class Note(models.Model): ppoi = PPOIField() height = models.PositiveIntegerField(blank=True, null=True, editable=False) width = models.PositiveIntegerField(blank=True, null=True, editable=False) - image = VersatileImageField(blank=True, null=True, upload_to='note_images', width_field='width', height_field='height', - ppoi_field='ppoi') + image = VersatileImageField( + blank=True, + null=True, + upload_to="note_images", + width_field="width", + height_field="height", + ppoi_field="ppoi", + ) status = models.IntegerField(default=1) start = models.DateTimeField(editable=False) - show = models.ForeignKey(Show, on_delete=models.CASCADE, related_name='notes', editable=True) + show = models.ForeignKey( + Show, on_delete=models.CASCADE, related_name="notes", editable=True + ) cba_id = models.IntegerField(blank=True, null=True) audio_url = models.TextField(blank=True, editable=False) - user = models.ForeignKey(User, editable=False, on_delete=models.CASCADE, related_name='users', default=1) - host = models.ForeignKey(Host, on_delete=models.CASCADE, related_name='hosts', null=True) + user = models.ForeignKey( + User, editable=False, on_delete=models.CASCADE, related_name="users", default=1 + ) + host = models.ForeignKey( + Host, on_delete=models.CASCADE, related_name="hosts", null=True + ) class Meta: - ordering = ('timeslot',) + ordering = ("timeslot",) + + def __str__(self): + return self.title def save(self, *args, **kwargs): self.start = self.timeslot.start @@ -918,18 +1090,3 @@ class Note(models.Model): if self.image.name and THUMBNAIL_SIZES: for size in THUMBNAIL_SIZES: self.image.thumbnail = self.image.crop[size].name - - # FIXME: this does not belong here - @staticmethod - def is_editable(note_view_set, note_id): - """ - Whether the given note is assigned to a show the current user owns - @return boolean - """ - - if note_view_set.request.user.is_superuser: - return True - - note = Note.objects.get(pk=note_id) - - return int(note.show_id) in note_view_set.request.user.shows.all().values_list('id', flat=True) diff --git a/program/serializers.py b/program/serializers.py index 911cc00d2884de36430c2052b0f4299c50d60d5f..7e9e8af40a3adee5c4a6bb2a12811d2c7fbe00d7 100644 --- a/program/serializers.py +++ b/program/serializers.py @@ -18,17 +18,31 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. # +from profile.models import Profile +from profile.serializers import ProfileSerializer + +from rest_framework import serializers + from django.contrib.auth.models import User from django.core.exceptions import ObjectDoesNotExist from django.utils import timezone -from rest_framework import serializers - -from profile.models import Profile -from profile.serializers import ProfileSerializer -from program.models import Show, Schedule, TimeSlot, Category, FundingCategory, Host, Topic, MusicFocus, Note, Type, Language, \ - RRule, Link -from steering.settings import THUMBNAIL_SIZES +from program.models import ( + Category, + FundingCategory, + Host, + Language, + Link, + MusicFocus, + Note, + RRule, + Schedule, + Show, + TimeSlot, + Topic, + Type, +) from program.utils import get_audio_url +from steering.settings import THUMBNAIL_SIZES class UserSerializer(serializers.ModelSerializer): @@ -37,25 +51,39 @@ class UserSerializer(serializers.ModelSerializer): class Meta: model = User - fields = ('id', 'username', 'first_name', 'last_name', 'email', 'is_staff', 'is_active', 'is_superuser', 'password', - 'profile') + fields = ( + "id", + "username", + "first_name", + "last_name", + "email", + "is_staff", + "is_active", + "is_superuser", + "password", + "profile", + ) def create(self, validated_data): """ Create and return a new User instance, given the validated data. """ - profile_data = validated_data.pop('profile') if 'profile' in validated_data else None + profile_data = ( + validated_data.pop("profile") if "profile" in validated_data else None + ) user = super(UserSerializer, self).create(validated_data) user.date_joined = timezone.now() - user.set_password(validated_data['password']) + user.set_password(validated_data["password"]) user.save() if profile_data: - profile = Profile(user=user, - cba_username=profile_data.get('cba_username').strip(), - cba_user_token=profile_data.get('cba_user_token').strip()) + profile = Profile( + user=user, + cba_username=profile_data.get("cba_username").strip(), + cba_user_token=profile_data.get("cba_user_token").strip(), + ) profile.save() return user @@ -65,14 +93,18 @@ class UserSerializer(serializers.ModelSerializer): Update and return an existing User instance, given the validated data. """ - instance.first_name = validated_data.get('first_name', instance.first_name) - instance.last_name = validated_data.get('last_name', instance.last_name) - instance.email = validated_data.get('email', instance.email) - instance.is_active = validated_data.get('is_active', instance.is_active) - instance.is_staff = validated_data.get('is_staff', instance.is_staff) - instance.is_superuser = validated_data.get('is_superuser', instance.is_superuser) + instance.first_name = validated_data.get("first_name", instance.first_name) + instance.last_name = validated_data.get("last_name", instance.last_name) + instance.email = validated_data.get("email", instance.email) + instance.is_active = validated_data.get("is_active", instance.is_active) + instance.is_staff = validated_data.get("is_staff", instance.is_staff) + instance.is_superuser = validated_data.get( + "is_superuser", instance.is_superuser + ) - profile_data = validated_data.pop('profile') if 'profile' in validated_data else None + profile_data = ( + validated_data.pop("profile") if "profile" in validated_data else None + ) if profile_data: # TODO: How to hook into this from ProfileSerializer without having to call it here? @@ -81,8 +113,8 @@ class UserSerializer(serializers.ModelSerializer): except ObjectDoesNotExist: profile = Profile.objects.create(user=instance, **profile_data) - profile.cba_username = profile_data.get('cba_username') - profile.cba_user_token = profile_data.get('cba_user_token') + profile.cba_username = profile_data.get("cba_username") + profile.cba_user_token = profile_data.get("cba_user_token") profile.save() instance.save() @@ -91,18 +123,18 @@ class UserSerializer(serializers.ModelSerializer): class CategorySerializer(serializers.ModelSerializer): # TODO: remove this when the dashboard is updated - category = serializers.CharField(source='name') + category = serializers.CharField(source="name") class Meta: model = Category # TODO: replace `category` with `name` when the dashboard is updated - fields = ('category', 'abbrev', 'slug', 'is_active', 'description') + fields = ("category", "abbrev", "slug", "is_active", "description") class LinkSerializer(serializers.ModelSerializer): class Meta: model = Link - fields = ('description', 'url') + fields = ("description", "url") class HostSerializer(serializers.ModelSerializer): @@ -122,10 +154,10 @@ class HostSerializer(serializers.ModelSerializer): class Meta: model = Host - fields = '__all__' + fields = "__all__" def create(self, validated_data): - links_data = validated_data.pop('links', []) + links_data = validated_data.pop("links", []) host = Host.objects.create(**validated_data) for link_data in links_data: @@ -138,19 +170,19 @@ class HostSerializer(serializers.ModelSerializer): Update and return an existing Host instance, given the validated data. """ - instance.name = validated_data.get('name', instance.name) - instance.is_active = validated_data.get('is_active', instance.is_active) - instance.email = validated_data.get('email', instance.email) - instance.website = validated_data.get('website', instance.website) - instance.biography = validated_data.get('biography', instance.biography) - instance.image = validated_data.get('image', instance.image) - instance.ppoi = validated_data.get('ppoi', instance.ppoi) + instance.name = validated_data.get("name", instance.name) + instance.is_active = validated_data.get("is_active", instance.is_active) + instance.email = validated_data.get("email", instance.email) + instance.website = validated_data.get("website", instance.website) + instance.biography = validated_data.get("biography", instance.biography) + instance.image = validated_data.get("image", instance.image) + instance.ppoi = validated_data.get("ppoi", instance.ppoi) if instance.links.count() > 0: for link in instance.links.all(): link.delete(keep_parents=True) - if links := validated_data.get('links'): + if links := validated_data.get("links"): for link_data in links: Link.objects.create(host=instance, **link_data) @@ -162,61 +194,73 @@ class HostSerializer(serializers.ModelSerializer): class LanguageSerializer(serializers.ModelSerializer): class Meta: model = Language - fields = ('name', 'is_active') + fields = ("name", "is_active") class TopicSerializer(serializers.ModelSerializer): # TODO: remove this when the dashboard is updated - topic = serializers.CharField(source='name') + topic = serializers.CharField(source="name") class Meta: model = Topic # TODO: replace `topic` with `name` when the dashboard is updated - fields = ('topic', 'abbrev', 'slug', 'is_active') + fields = ("topic", "abbrev", "slug", "is_active") class MusicFocusSerializer(serializers.ModelSerializer): # TODO: remove this when the dashboard is updated - focus = serializers.CharField(source='name') + focus = serializers.CharField(source="name") class Meta: model = MusicFocus # TODO: replace `focus` with `name` when the dashboard is updated - fields = ('focus', 'abbrev', 'slug', 'is_active') + fields = ("focus", "abbrev", "slug", "is_active") class TypeSerializer(serializers.ModelSerializer): # TODO: remove this when the dashboard is updated - type = serializers.CharField(source='name') + type = serializers.CharField(source="name") class Meta: model = Type # TODO: replace `type` with `name` when the dashboard is updated - fields = ('type', 'slug', 'is_active') + fields = ("type", "slug", "is_active") class FundingCategorySerializer(serializers.ModelSerializer): # TODO: remove this when the dashboard is updated - fundingcategory = serializers.CharField(source='name') + fundingcategory = serializers.CharField(source="name") class Meta: model = FundingCategory # TODO: replace `fundingcategory` with `name` when the dashboard is updated - fields = ('fundingcategory', 'abbrev', 'slug', 'is_active') + fields = ("fundingcategory", "abbrev", "slug", "is_active") class ShowSerializer(serializers.HyperlinkedModelSerializer): owners = serializers.PrimaryKeyRelatedField(queryset=User.objects.all(), many=True) - category = serializers.PrimaryKeyRelatedField(queryset=Category.objects.all(), many=True) + category = serializers.PrimaryKeyRelatedField( + queryset=Category.objects.all(), many=True + ) hosts = serializers.PrimaryKeyRelatedField(queryset=Host.objects.all(), many=True) - language = serializers.PrimaryKeyRelatedField(queryset=Language.objects.all(), many=True) + language = serializers.PrimaryKeyRelatedField( + queryset=Language.objects.all(), many=True + ) topic = serializers.PrimaryKeyRelatedField(queryset=Topic.objects.all(), many=True) - # TODO: replace `musicfocs` with `music_focus` and remove the source when the dashboard in updated - musicfocus = serializers.PrimaryKeyRelatedField(queryset=MusicFocus.objects.all(), source='music_focus', many=True) + # TODO: replace `musicfocs` with `music_focus` and remove the source when the dashboard is + # updated + musicfocus = serializers.PrimaryKeyRelatedField( + queryset=MusicFocus.objects.all(), source="music_focus", many=True + ) type = serializers.PrimaryKeyRelatedField(queryset=Type.objects.all()) - # TODO: replace `fundingcategory` with `funding_category` and remove the source when the dashboard is updated - fundingcategory = serializers.PrimaryKeyRelatedField(queryset=FundingCategory.objects.all(), source='funding_category') - predecessor = serializers.PrimaryKeyRelatedField(queryset=Show.objects.all(), required=False, allow_null=True) + # TODO: replace `fundingcategory` with `funding_category` and remove the source when the + # dashboard is updated + fundingcategory = serializers.PrimaryKeyRelatedField( + queryset=FundingCategory.objects.all(), source="funding_category" + ) + predecessor = serializers.PrimaryKeyRelatedField( + queryset=Show.objects.all(), required=False, allow_null=True + ) thumbnails = serializers.SerializerMethodField() # Read-only @staticmethod @@ -232,22 +276,44 @@ class ShowSerializer(serializers.HyperlinkedModelSerializer): class Meta: model = Show - fields = ('id', 'name', 'slug', 'image', 'ppoi', 'logo', 'short_description', 'description', - 'email', 'website', 'type', 'fundingcategory', - 'predecessor', 'cba_series_id', 'default_playlist_id', 'category', 'hosts', - 'owners', 'language', 'topic', 'musicfocus', 'thumbnails', 'is_active', 'is_public') + fields = ( + "id", + "name", + "slug", + "image", + "ppoi", + "logo", + "short_description", + "description", + "email", + "website", + "type", + "fundingcategory", + "predecessor", + "cba_series_id", + "default_playlist_id", + "category", + "hosts", + "owners", + "language", + "topic", + "musicfocus", + "thumbnails", + "is_active", + "is_public", + ) def create(self, validated_data): """ Create and return a new Show instance, given the validated data. """ - owners = validated_data.pop('owners') - category = validated_data.pop('category') - hosts = validated_data.pop('hosts') - language = validated_data.pop('language') - topic = validated_data.pop('topic') - music_focus = validated_data.pop('music_focus') + owners = validated_data.pop("owners") + category = validated_data.pop("category") + hosts = validated_data.pop("hosts") + language = validated_data.pop("language") + topic = validated_data.pop("topic") + music_focus = validated_data.pop("music_focus") show = Show.objects.create(**validated_data) @@ -267,29 +333,39 @@ class ShowSerializer(serializers.HyperlinkedModelSerializer): Update and return an existing Show instance, given the validated data. """ - instance.name = validated_data.get('name', instance.name) - instance.slug = validated_data.get('slug', instance.slug) - instance.image = validated_data.get('image', instance.image) - instance.ppoi = validated_data.get('ppoi', instance.ppoi) - instance.logo = validated_data.get('logo', instance.logo) - instance.short_description = validated_data.get('short_description', instance.short_description) - instance.description = validated_data.get('description', instance.description) - instance.email = validated_data.get('email', instance.email) - instance.website = validated_data.get('website', instance.website) - instance.cba_series_id = validated_data.get('cba_series_id', instance.cba_series_id) - instance.default_playlist_id = validated_data.get('default_playlist_id', instance.default_playlist_id) - instance.type = validated_data.get('type', instance.type) - instance.funding_category = validated_data.get('funding_category', instance.funding_category) - instance.predecessor = validated_data.get('predecessor', instance.predecessor) - instance.is_active = validated_data.get('is_active', instance.is_active) - instance.is_public = validated_data.get('is_public', instance.is_public) - - instance.owners.set(validated_data.get('owners', instance.owners)) - instance.category.set(validated_data.get('category', instance.category)) - instance.hosts.set(validated_data.get('hosts', instance.hosts)) - instance.language.set(validated_data.get('language', instance.language)) - instance.topic.set(validated_data.get('topic', instance.topic)) - instance.music_focus.set(validated_data.get('music_focus', instance.music_focus)) + instance.name = validated_data.get("name", instance.name) + instance.slug = validated_data.get("slug", instance.slug) + instance.image = validated_data.get("image", instance.image) + instance.ppoi = validated_data.get("ppoi", instance.ppoi) + instance.logo = validated_data.get("logo", instance.logo) + instance.short_description = validated_data.get( + "short_description", instance.short_description + ) + instance.description = validated_data.get("description", instance.description) + instance.email = validated_data.get("email", instance.email) + instance.website = validated_data.get("website", instance.website) + instance.cba_series_id = validated_data.get( + "cba_series_id", instance.cba_series_id + ) + instance.default_playlist_id = validated_data.get( + "default_playlist_id", instance.default_playlist_id + ) + instance.type = validated_data.get("type", instance.type) + instance.funding_category = validated_data.get( + "funding_category", instance.funding_category + ) + instance.predecessor = validated_data.get("predecessor", instance.predecessor) + instance.is_active = validated_data.get("is_active", instance.is_active) + instance.is_public = validated_data.get("is_public", instance.is_public) + + instance.owners.set(validated_data.get("owners", instance.owners)) + instance.category.set(validated_data.get("category", instance.category)) + instance.hosts.set(validated_data.get("hosts", instance.hosts)) + instance.language.set(validated_data.get("language", instance.language)) + instance.topic.set(validated_data.get("topic", instance.topic)) + instance.music_focus.set( + validated_data.get("music_focus", instance.music_focus) + ) instance.save() return instance @@ -299,21 +375,21 @@ class ScheduleSerializer(serializers.ModelSerializer): rrule = serializers.PrimaryKeyRelatedField(queryset=RRule.objects.all()) show = serializers.PrimaryKeyRelatedField(queryset=Show.objects.all()) # TODO: remove this when the dashboard is updated - byweekday = serializers.IntegerField(source='by_weekday') - dstart = serializers.DateField(source='first_date') - tstart = serializers.TimeField(source='start_time') - tend = serializers.TimeField(source='end_time') - until = serializers.DateField(source='last_date') + byweekday = serializers.IntegerField(source="by_weekday") + dstart = serializers.DateField(source="first_date") + tstart = serializers.TimeField(source="start_time") + tend = serializers.TimeField(source="end_time") + until = serializers.DateField(source="last_date") class Meta: model = Schedule - fields = '__all__' + fields = "__all__" def create(self, validated_data): """Create and return a new Schedule instance, given the validated data.""" - rrule = validated_data.pop('rrule') - show = validated_data.pop('show') + rrule = validated_data.pop("rrule") + show = validated_data.pop("show") schedule = Schedule.objects.create(**validated_data) schedule.rrule = rrule @@ -325,17 +401,23 @@ class ScheduleSerializer(serializers.ModelSerializer): def update(self, instance, validated_data): """Update and return an existing Schedule instance, given the validated data.""" - instance.by_weekday = validated_data.get('byweekday', instance.by_weekday) - instance.first_date = validated_data.get('dstart', instance.first_date) - instance.start_time = validated_data.get('tstart', instance.start_time) - instance.end_time = validated_data.get('tend', instance.end_time) - instance.last_date = validated_data.get('until', instance.last_date) - instance.is_repetition = validated_data.get('is_repetition', instance.is_repetition) - instance.default_playlist_id = validated_data.get('default_playlist_id', instance.default_playlist_id) - instance.rrule = validated_data.get('rrule', instance.rrule) - instance.show = validated_data.get('show', instance.show) - instance.add_days_no = validated_data.get('add_days_no', instance.add_days_no) - instance.add_business_days_only = validated_data.get('add_business_days_only', instance.add_business_days_only) + instance.by_weekday = validated_data.get("byweekday", instance.by_weekday) + instance.first_date = validated_data.get("dstart", instance.first_date) + instance.start_time = validated_data.get("tstart", instance.start_time) + instance.end_time = validated_data.get("tend", instance.end_time) + instance.last_date = validated_data.get("until", instance.last_date) + instance.is_repetition = validated_data.get( + "is_repetition", instance.is_repetition + ) + instance.default_playlist_id = validated_data.get( + "default_playlist_id", instance.default_playlist_id + ) + instance.rrule = validated_data.get("rrule", instance.rrule) + instance.show = validated_data.get("show", instance.show) + instance.add_days_no = validated_data.get("add_days_no", instance.add_days_no) + instance.add_business_days_only = validated_data.get( + "add_business_days_only", instance.add_business_days_only + ) instance.save() return instance @@ -347,7 +429,7 @@ class TimeSlotSerializer(serializers.ModelSerializer): class Meta: model = TimeSlot - fields = '__all__' + fields = "__all__" def create(self, validated_data): """Create and return a new TimeSlot instance, given the validated data.""" @@ -357,9 +439,11 @@ class TimeSlotSerializer(serializers.ModelSerializer): """Update and return an existing Show instance, given the validated data.""" # Only save certain fields - instance.memo = validated_data.get('memo', instance.memo) - instance.is_repetition = validated_data.get('is_repetition', instance.is_repetition) - instance.playlist_id = validated_data.get('playlist_id', instance.playlist_id) + instance.memo = validated_data.get("memo", instance.memo) + instance.is_repetition = validated_data.get( + "is_repetition", instance.is_repetition + ) + instance.playlist_id = validated_data.get("playlist_id", instance.playlist_id) instance.save() return instance @@ -369,6 +453,7 @@ class NoteSerializer(serializers.ModelSerializer): timeslot = serializers.PrimaryKeyRelatedField(queryset=TimeSlot.objects.all()) host = serializers.PrimaryKeyRelatedField(queryset=Host.objects.all()) thumbnails = serializers.SerializerMethodField() # Read-only + cba_id = serializers.IntegerField(required=False, write_only=True) @staticmethod def get_thumbnails(note): @@ -383,16 +468,16 @@ class NoteSerializer(serializers.ModelSerializer): class Meta: model = Note - fields = '__all__' + fields = "__all__" def create(self, validated_data): """Create and return a new Note instance, given the validated data.""" # Save the creator - validated_data['user_id'] = self.context['user_id'] + validated_data["user_id"] = self.context["user_id"] # Try to retrieve audio URL from CBA - validated_data['audio_url'] = get_audio_url(validated_data['cba_id']) + validated_data["audio_url"] = get_audio_url(validated_data.get("cba_id", None)) note = Note.objects.create(**validated_data) @@ -410,17 +495,17 @@ class NoteSerializer(serializers.ModelSerializer): def update(self, instance, validated_data): """Update and return an existing Note instance, given the validated data.""" - instance.show = validated_data.get('show', instance.show) - instance.timeslot = validated_data.get('timeslot', instance.timeslot) - instance.title = validated_data.get('title', instance.title) - instance.slug = validated_data.get('slug', instance.slug) - instance.summary = validated_data.get('summary', instance.summary) - instance.content = validated_data.get('content', instance.content) - instance.image = validated_data.get('image', instance.image) - instance.ppoi = validated_data.get('ppoi', instance.ppoi) - instance.status = validated_data.get('status', instance.status) - instance.host = validated_data.get('host', instance.host) - instance.cba_id = validated_data.get('cba_id', instance.cba_id) + instance.show = validated_data.get("show", instance.show) + instance.timeslot = validated_data.get("timeslot", instance.timeslot) + instance.title = validated_data.get("title", instance.title) + instance.slug = validated_data.get("slug", instance.slug) + instance.summary = validated_data.get("summary", instance.summary) + instance.content = validated_data.get("content", instance.content) + instance.image = validated_data.get("image", instance.image) + instance.ppoi = validated_data.get("ppoi", instance.ppoi) + instance.status = validated_data.get("status", instance.status) + instance.host = validated_data.get("host", instance.host) + instance.cba_id = validated_data.get("cba_id", instance.cba_id) instance.audio_url = get_audio_url(instance.cba_id) instance.save() diff --git a/program/utils.py b/program/utils.py index 78481e83b35cc7979074af6fbb14557f5c6fd34e..53b6420d1b95bc99e98df51ac8e905e0d073eeff 100644 --- a/program/utils.py +++ b/program/utils.py @@ -18,11 +18,13 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. # -from datetime import datetime, date, time +import json +from datetime import date, datetime, time +from typing import Dict, Optional, Tuple, Union import requests -from django.utils import timezone +from django.utils import timezone from steering.settings import CBA_AJAX_URL, CBA_API_KEY, DEBUG @@ -35,7 +37,11 @@ def parse_datetime(date_string: str) -> datetime: except ValueError: parsed_datetime = datetime.strptime(date_string, "%Y-%m-%d %H:%M:%S%z") - return timezone.make_aware(parsed_datetime) if timezone.is_naive(parsed_datetime) else parsed_datetime + return ( + timezone.make_aware(parsed_datetime) + if timezone.is_naive(parsed_datetime) + else parsed_datetime + ) def parse_date(date_string: str) -> date: @@ -52,7 +58,7 @@ def parse_time(date_string: str) -> time: return datetime.strptime(date_string, "%H:%M:%S").time() -def get_audio_url(cba_id): +def get_audio_url(cba_id: Optional[int]) -> str: """ Retrieve the direct URL to the mp3 in CBA In order to retrieve the URL, stations need @@ -62,14 +68,55 @@ def get_audio_url(cba_id): For these contact cba@fro.at """ - if cba_id is None or cba_id == '' or CBA_API_KEY == '': - return None + if not cba_id or CBA_API_KEY == "": + return "" else: if DEBUG: - url = 'https://cba.fro.at/wp-content/plugins/cba/ajax/cba-get-filename.php?post_id=' + str(cba_id) + '&c=Ml3fASkfwR8' + url = ( + "https://cba.fro.at/wp-content/plugins/cba/ajax/cba-get-filename.php?post_id=" + + str(cba_id) + + "&c=Ml3fASkfwR8" + ) else: - url = CBA_AJAX_URL + '?action=cba_ajax_get_filename&post_id=' + str(cba_id) + '&api_key=' + CBA_API_KEY + url = ( + CBA_AJAX_URL + + "?action=cba_ajax_get_filename&post_id=" + + str(cba_id) + + "&api_key=" + + CBA_API_KEY + ) + + try: + return requests.get(url).json() + except (requests.RequestException, json.JSONDecodeError): + # TODO: we might want to add some logging + return "" + + +def get_values( + kwargs: Dict[str, str], *keys: str +) -> Union[Tuple[Union[int, str, None], ...], int, str, None]: + """Get the values of the keys from the kwargs.""" + + def int_if_digit(value: Optional[str]) -> Optional[Union[int, str]]: + return int(value) if value and value.isdigit() else value + + values = [kwargs.get(key) for key in keys] - audio_url = requests.get(url).json() + if len(values) > 1: + return tuple(int_if_digit(value) for value in values) + else: + return int_if_digit(values[0]) + + +def get_pk_and_slug(kwargs: Dict[str, str]) -> Tuple[Optional[int], Optional[str]]: + """Get the pk and the slug from the kwargs.""" + + pk, slug = None, None + + try: + pk = int(kwargs["pk"]) + except ValueError: + slug = kwargs["pk"] - return audio_url + return pk, slug diff --git a/program/views.py b/program/views.py index 7a8cdd9af8da516e509617f9e49c2cf5d172fe7d..51f95b55d2c2e7426e0aecc4ee27cdbae12f93b8 100644 --- a/program/views.py +++ b/program/views.py @@ -20,23 +20,46 @@ import json import logging -from datetime import date, datetime, time, timedelta +from datetime import date, datetime, time + +from rest_framework import mixins, permissions, status, viewsets +from rest_framework.pagination import LimitOffsetPagination +from rest_framework.response import Response from django.contrib.auth.models import User -from django.db.models import Q from django.http import HttpResponse from django.shortcuts import get_object_or_404 from django.utils import timezone from django.utils.translation import gettext as _ -from rest_framework import permissions, status, viewsets -from rest_framework.pagination import LimitOffsetPagination -from rest_framework.response import Response - -from program.models import Type, MusicFocus, Language, Note, Show, Category, FundingCategory, Topic, TimeSlot, Host, Schedule -from program.serializers import TypeSerializer, LanguageSerializer, MusicFocusSerializer, NoteSerializer, ShowSerializer, \ - ScheduleSerializer, CategorySerializer, FundingCategorySerializer, TopicSerializer, TimeSlotSerializer, HostSerializer, \ - UserSerializer -from program.utils import parse_date +from program import filters +from program.models import ( + Category, + FundingCategory, + Host, + Language, + MusicFocus, + Note, + Schedule, + Show, + TimeSlot, + Topic, + Type, +) +from program.serializers import ( + CategorySerializer, + FundingCategorySerializer, + HostSerializer, + LanguageSerializer, + MusicFocusSerializer, + NoteSerializer, + ScheduleSerializer, + ShowSerializer, + TimeSlotSerializer, + TopicSerializer, + TypeSerializer, + UserSerializer, +) +from program.utils import get_pk_and_slug, get_values, parse_date logger = logging.getLogger(__name__) @@ -45,22 +68,30 @@ def json_day_schedule(request, year=None, month=None, day=None): if year is None and month is None and day is None: today = timezone.make_aware(datetime.combine(timezone.now(), time(0, 0))) else: - today = timezone.make_aware(datetime.combine(date(year, month, day), time(0, 0))) - - timeslots = TimeSlot.objects.get_24h_timeslots(today).select_related('schedule').select_related('show') + today = timezone.make_aware( + datetime.combine(date(year, month, day), time(0, 0)) + ) + + timeslots = ( + TimeSlot.objects.get_24h_timeslots(today) + .select_related("schedule") + .select_related("show") + ) schedule = [] for ts in timeslots: entry = { - 'start': ts.start.strftime('%Y-%m-%d_%H:%M:%S'), - 'end': ts.end.strftime('%Y-%m-%d_%H:%M:%S'), - 'title': ts.show.name, - 'id': ts.show.id, + "start": ts.start.strftime("%Y-%m-%d_%H:%M:%S"), + "end": ts.end.strftime("%Y-%m-%d_%H:%M:%S"), + "title": ts.show.name, + "id": ts.show.id, } schedule.append(entry) - return HttpResponse(json.dumps(schedule, ensure_ascii=False).encode('utf8'), - content_type="application/json; charset=utf-8") + return HttpResponse( + json.dumps(schedule, ensure_ascii=False).encode("utf8"), + content_type="application/json; charset=utf-8", + ) def json_playout(request): @@ -75,111 +106,116 @@ def json_playout(request): If end not given, it returns all timeslots of the next 7 days """ - if request.GET.get('start') is None: + if request.GET.get("start") is None: start = timezone.make_aware(datetime.combine(timezone.now(), time(0, 0))) else: - start = timezone.make_aware(datetime.combine(parse_date(request.GET.get('start')), time(0, 0))) + start = timezone.make_aware( + datetime.combine(parse_date(request.GET.get("start")), time(0, 0)) + ) - if request.GET.get('end') is None: + if request.GET.get("end") is None: # If no end was given, return the next week - timeslots = TimeSlot.objects.get_7d_timeslots(start).select_related('schedule').select_related('show') + timeslots = ( + TimeSlot.objects.get_7d_timeslots(start) + .select_related("schedule") + .select_related("show") + ) else: # Otherwise return the given timerange - end = timezone.make_aware(datetime.combine(parse_date(request.GET.get('end')), time(23, 59))) - timeslots = TimeSlot.objects.get_timerange_timeslots(start, end).select_related('schedule').select_related('show') + end = timezone.make_aware( + datetime.combine(parse_date(request.GET.get("end")), time(23, 59)) + ) + timeslots = ( + TimeSlot.objects.get_timerange_timeslots(start, end) + .select_related("schedule") + .select_related("show") + ) schedule = [] for ts in timeslots: - is_repetition = ' ' + _('REP') if ts.schedule.is_repetition is True else '' - - hosts = ', '.join(ts.show.hosts.values_list('name', flat=True)) - categories = ', '.join(ts.show.category.values_list('name', flat=True)) - topics = ', '.join(ts.show.topic.values_list('name', flat=True)) - music_focus = ', '.join(ts.show.music_focus.values_list('name', flat=True)) - languages = ', '.join(ts.show.language.values_list('name', flat=True)) - funding_category = FundingCategory.objects.get(pk=ts.show.funding_category_id) if ts.show.funding_category_id else None + is_repetition = " " + _("REP") if ts.schedule.is_repetition is True else "" + + hosts = ", ".join(ts.show.hosts.values_list("name", flat=True)) + categories = ", ".join(ts.show.category.values_list("name", flat=True)) + topics = ", ".join(ts.show.topic.values_list("name", flat=True)) + music_focus = ", ".join(ts.show.music_focus.values_list("name", flat=True)) + languages = ", ".join(ts.show.language.values_list("name", flat=True)) + funding_category = ( + FundingCategory.objects.get(pk=ts.show.funding_category_id) + if ts.show.funding_category_id + else None + ) type_ = Type.objects.get(pk=ts.show.type_id) - classname = 'default' + classname = "default" if ts.playlist_id is None or ts.playlist_id == 0: - classname = 'danger' + classname = "danger" entry = { - 'id': ts.id, - 'start': ts.start.strftime('%Y-%m-%dT%H:%M:%S'), - 'end': ts.end.strftime('%Y-%m-%dT%H:%M:%S'), - 'title': ts.show.name + is_repetition, # For JS Calendar - 'schedule_id': ts.schedule.id, - 'is_repetition': ts.is_repetition, - 'playlist_id': ts.playlist_id, - 'schedule_default_playlist_id': ts.schedule.default_playlist_id, - 'show_default_playlist_id': ts.show.default_playlist_id, - 'show_id': ts.show.id, - 'show_name': ts.show.name + is_repetition, - 'show_hosts': hosts, - 'show_type': type_.name, - 'show_categories': categories, - 'show_topics': topics, + "id": ts.id, + "start": ts.start.strftime("%Y-%m-%dT%H:%M:%S"), + "end": ts.end.strftime("%Y-%m-%dT%H:%M:%S"), + "title": ts.show.name + is_repetition, # For JS Calendar + "schedule_id": ts.schedule.id, + "is_repetition": ts.is_repetition, + "playlist_id": ts.playlist_id, + "schedule_default_playlist_id": ts.schedule.default_playlist_id, + "show_default_playlist_id": ts.show.default_playlist_id, + "show_id": ts.show.id, + "show_name": ts.show.name + is_repetition, + "show_hosts": hosts, + "show_type": type_.name, + "show_categories": categories, + "show_topics": topics, # TODO: replace `show_musicfocus` with `show_music_focus` when engine is updated - 'show_musicfocus': music_focus, - 'show_languages': languages, - # TODO: replace `show_fundingcategory` with `show_funding_category` when engine is updated - 'show_fundingcategory': funding_category.name, - 'memo': ts.memo, - 'className': classname, + "show_musicfocus": music_focus, + "show_languages": languages, + # TODO: replace `show_fundingcategory` with `show_funding_category` when engine is + # updated + "show_fundingcategory": funding_category.name, + "memo": ts.memo, + "className": classname, } schedule.append(entry) - return HttpResponse(json.dumps(schedule, ensure_ascii=False).encode('utf8'), - content_type="application/json; charset=utf-8") - + return HttpResponse( + json.dumps(schedule, ensure_ascii=False).encode("utf8"), + content_type="application/json; charset=utf-8", + ) -def int_or_none(key, kwargs): - return int(kwargs[key]) if key in kwargs else None - -def pk_and_slug(kwargs): - pk = None - slug = None - - try: - pk = int(kwargs['pk']) - except ValueError: - slug = kwargs['pk'] - - return pk, slug - - -class APIUserViewSet(viewsets.ModelViewSet): +class APIUserViewSet( + mixins.CreateModelMixin, + mixins.RetrieveModelMixin, + mixins.UpdateModelMixin, + mixins.ListModelMixin, + viewsets.GenericViewSet, +): """ - /users returns oneself. Superusers see all users. Only superusers may create a user (GET, POST) - /users/{pk} retrieves or updates a single user. Non-superusers may only update certain fields (GET, PUT) + Returns a list of users. - Superusers may access and update all users. + Only returns the user that is currently authenticated unless the user is a superuser. """ permission_classes = [permissions.DjangoModelPermissionsOrAnonReadOnly] serializer_class = UserSerializer - queryset = User.objects.none() + queryset = User.objects.all() def get_queryset(self): - """Constrain access to oneself except for superusers""" - if self.request.user.is_superuser: - return User.objects.all() + queryset = super().get_queryset() - return User.objects.filter(pk=self.request.user.id) + # Constrain access to oneself except for superusers. + if not self.request.user.is_superuser: + queryset = queryset.filter(pk=self.request.user.id) - def list(self, request, *args, **kwargs): - users = self.get_queryset() - serializer = UserSerializer(users, many=True) - return Response(serializer.data) + return queryset def retrieve(self, request, *args, **kwargs): - """Returns a single user""" - pk = int_or_none('pk', self.kwargs) + """Returns a single user.""" + pk = get_values(self.kwargs, "pk") # Common users only see themselves if not request.user.is_superuser and pk != request.user.id: @@ -191,8 +227,9 @@ class APIUserViewSet(viewsets.ModelViewSet): def create(self, request, *args, **kwargs): """ - Create a User - Only superusers may create a user + Create a User. + + Only superusers may create users. """ if not request.user.is_superuser: @@ -202,20 +239,29 @@ class APIUserViewSet(viewsets.ModelViewSet): if serializer.is_valid(): serializer.save() - return Response(serializer.data) + return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) def update(self, request, *args, **kwargs): - pk = int_or_none('pk', self.kwargs) + """ + Updates the user’s data. + + Non-superusers may not be able to edit all of the available data. + """ + pk = get_values(self.kwargs, "pk") serializer = UserSerializer(data=request.data) # Common users may only edit themselves if not request.user.is_superuser and pk != request.user.id: - return Response(serializer.initial_data, status=status.HTTP_401_UNAUTHORIZED) + return Response( + serializer.initial_data, status=status.HTTP_401_UNAUTHORIZED + ) user = get_object_or_404(User, pk=pk) - serializer = UserSerializer(user, data=request.data, context={'user': request.user}) + serializer = UserSerializer( + user, data=request.data, context={"user": request.user} + ) if serializer.is_valid(): serializer.save() @@ -223,103 +269,25 @@ class APIUserViewSet(viewsets.ModelViewSet): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - def destroy(self, request, *args, **kwargs): - """Deleting users is prohibited: Set 'is_active' to False instead""" - return Response(status=status.HTTP_401_UNAUTHORIZED) - class APIShowViewSet(viewsets.ModelViewSet): """ - /shows/ returns all shows (GET, POST) - /shows/?active=true returns all active shows (= currently running) (GET) - /shows/?active=false returns all inactive shows (= past or upcoming) (GET) - /shows/?public=true returns all public shows (GET) - /shows/?public=false returns all non-public shows (GET) - /shows/?host={host_pk} returns shows assigned to a given host (GET) - /shows/?owner={owner_pk} returns shows of a given owner (GET) - /shows/?language={language_pk} returns shows in a given language (GET) - /shows/?type={type_pk} returns shows of a given type (GET) - /shows/?category={category_pk} returns shows of a given category (GET) - /shows/?topic={topic_pk} returns shows of a given topic (GET) - /shows/?musicfocus={musicfocus_pk} returns shows of a given music focus (GET) - /shows/{pk|slug} retrieves or updates (if owned) a single show (GET, PUT). - - Only superusers may add and delete shows + Returns a list of available shows. + + Only superusers may add and delete shows. """ - queryset = Show.objects.none() + queryset = Show.objects.all() serializer_class = ShowSerializer permission_classes = [permissions.DjangoModelPermissionsOrAnonReadOnly] pagination_class = LimitOffsetPagination - - def get_queryset(self): - shows = Show.objects.all() - - # Filters - if self.request.query_params.get('active') == 'true' or self.request.query_params.get('active') == 'false': - # Filter currently running shows - # Get currently running schedules to filter by first - # For single dates we test if there'll be one in the future (and ignore the until date) - # TODO: Really consider first_date? (=currently active, not just upcoming ones) - # Add limit for future? - show_ids = Schedule.objects.filter(Q(rrule_id__gt=1, - first_date__lte=timezone.now(), - last_date__gte=timezone.now()) | - Q(rrule_id=1, - first_date__gte=timezone.now()) - ).distinct().values_list('show_id', flat=True) - - # Filter active shows based on timeslots as well as on the is_active flag - # Even if there are future timeslots but is_active=True the show will be considered as inactive - shows = Show.objects.filter(id__in=show_ids, is_active=True) - - if self.request.query_params.get('active') == 'false': - # Return all shows except those which are running - shows = Show.objects.exclude(id__in=show_ids, is_active=True) - - if self.request.query_params.get('public') == 'true': - # Return all public shows - shows = shows.filter(is_public=True) - - if self.request.query_params.get('public') == 'false': - # Return all public shows - shows = shows.filter(is_public=False) - - if owner := self.request.query_params.get('owner'): - if owner != 'undefined': - shows = shows.filter(owners__in=[int(owner)]) - - if host := self.request.query_params.get('host'): - if host != 'undefined': - shows = shows.filter(hosts__in=[int(host)]) - - if language := self.request.query_params.get('language'): - if language != 'undefined': - shows = shows.filter(language__in=[int(language)]) - - if type_ := self.request.query_params.get('type'): - if type_ != 'undefined': - shows = shows.filter(type__in=[int(type_)]) - - if category := self.request.query_params.get('category'): - if category != 'undefined': - shows = shows.filter(category__in=[int(category)]) - - if topic := self.request.query_params.get('topic'): - if topic != 'undefined': - shows = shows.filter(topic__in=[int(topic)]) - - # TODO: replace `musicfocus` with `music_focus` when dashboard is updated - if music_focus := self.request.query_params.get('musicfocus'): - if music_focus != 'undefined': - shows = shows.filter(music_focus__in=[int(music_focus)]) - - return shows + filterset_class = filters.ShowFilterSet def create(self, request, *args, **kwargs): """ - Create a show - Only superusers may create a show + Create a show. + + Only superusers may create a show. """ if not request.user.is_superuser: @@ -329,16 +297,22 @@ class APIShowViewSet(viewsets.ModelViewSet): if serializer.is_valid(): serializer.save() - return Response(serializer.data) + return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) def retrieve(self, request, *args, **kwargs): """Returns a single show""" - pk, slug = pk_and_slug(self.kwargs) + pk, slug = get_pk_and_slug(self.kwargs) - show = get_object_or_404(Show, pk=pk) if pk else get_object_or_404(Show, slug=slug) if slug else None + show = ( + get_object_or_404(Show, pk=pk) + if pk + else get_object_or_404(Show, slug=slug) + if slug + else None + ) serializer = ShowSerializer(show) @@ -346,22 +320,27 @@ class APIShowViewSet(viewsets.ModelViewSet): def update(self, request, *args, **kwargs): """ - Update a show - Common users may only update shows they own + Update a show. + + Common users may only update shows they own. """ - pk = int_or_none('pk', self.kwargs) + pk = get_values(self.kwargs, "pk") - if not Show.is_editable(self, pk): + if not request.user.is_superuser and pk not in request.user.shows.values_list( + "id", flat=True + ): return Response(status=status.HTTP_401_UNAUTHORIZED) show = get_object_or_404(Show, pk=pk) - serializer = ShowSerializer(show, data=request.data, context={'user': request.user}) + serializer = ShowSerializer( + show, data=request.data, context={"user": request.user} + ) if serializer.is_valid(): # Common users mustn't edit the show's name if not request.user.is_superuser: - serializer.validated_data['name'] = show.name + serializer.validated_data["name"] = show.name serializer.save() return Response(serializer.data) @@ -369,14 +348,16 @@ class APIShowViewSet(viewsets.ModelViewSet): def destroy(self, request, *args, **kwargs): """ - Delete a show - Only superusers may delete shows + Delete a show. + + Only superusers may delete shows. """ if not request.user.is_superuser: return Response(status=status.HTTP_401_UNAUTHORIZED) - pk = int_or_none('pk', self.kwargs) + pk = get_values(self.kwargs, "pk") + Show.objects.get(pk=pk).delete() return Response(status=status.HTTP_204_NO_CONTENT) @@ -384,98 +365,95 @@ class APIShowViewSet(viewsets.ModelViewSet): class APIScheduleViewSet(viewsets.ModelViewSet): """ - /schedules/ returns all schedules (GET) - /schedules/{pk} returns the given schedule (GET) - /shows/{show_pk}/schedules returns schedules of the show (GET, POST) - /shows/{show_pk}/schedules/{pk} returns schedules by its ID (GET, PUT, DELETE) + Returns a list of schedules. - Only superusers may create and update schedules + Only superusers may create and update schedules. """ - queryset = Schedule.objects.none() + queryset = Schedule.objects.all() serializer_class = ScheduleSerializer permission_classes = [permissions.DjangoModelPermissionsOrAnonReadOnly] def get_queryset(self): - show_pk = int_or_none('show_pk', self.kwargs) + queryset = super().get_queryset() + # subroute filters + show_pk = get_values(self.kwargs, "show_pk") if show_pk: - return Schedule.objects.filter(show=show_pk) - - return Schedule.objects.all() + queryset = queryset.filter(show=show_pk) - def list(self, request, *args, **kwargs): - """List Schedules of a show""" - - schedules = self.get_queryset() - serializer = ScheduleSerializer(schedules, many=True) - return Response(serializer.data) + return queryset def retrieve(self, request, *args, **kwargs): - pk = int_or_none('pk', self.kwargs) - show_pk = int_or_none('show_pk', self.kwargs) + pk, show_pk = get_values(self.kwargs, "pk", "show_pk") - if show_pk: - schedule = get_object_or_404(Schedule, pk=pk, show=show_pk) - else: - schedule = get_object_or_404(Schedule, pk=pk) + schedule = ( + get_object_or_404(Schedule, pk=pk, show=show_pk) + if show_pk + else get_object_or_404(Schedule, pk=pk) + ) serializer = ScheduleSerializer(schedule) + return Response(serializer.data) def create(self, request, *args, **kwargs): """ Create a schedule, generate timeslots, test for collisions and resolve them including notes - Only superusers may add schedules - TODO: Perhaps directly insert into database if no conflicts found + Only superusers may add schedules. """ - pk = int_or_none('pk', self.kwargs) - show_pk = int_or_none('show_pk', self.kwargs) - # Only allow creating when calling /shows/1/schedules/ - if show_pk is None or not request.user.is_superuser: + if not request.user.is_superuser: return Response(status=status.HTTP_401_UNAUTHORIZED) - # The schedule dict is mandatory - if 'schedule' not in request.data: + pk, show_pk = get_values(self.kwargs, "pk", "show_pk") + + # Only allow creating when calling /shows/{show_pk}/schedules/ and with ehe `schedule` JSON + # object + if show_pk is None or "schedule" not in request.data: return Response(status=status.HTTP_400_BAD_REQUEST) # First create submit -> return projected timeslots and collisions - if 'solutions' not in request.data: - return Response(Schedule.make_conflicts(request.data['schedule'], pk, show_pk)) + # TODO: Perhaps directly insert into database if no conflicts found + if "solutions" not in request.data: + return Response( + Schedule.make_conflicts(request.data["schedule"], pk, show_pk), + status=status.HTTP_409_CONFLICT, + ) # Otherwise try to resolve resolution = Schedule.resolve_conflicts(request.data, pk, show_pk) # If resolution went well - if 'projected' not in resolution: + if "projected" not in resolution: return Response(resolution, status=status.HTTP_201_CREATED) # Otherwise return conflicts - return Response(resolution) + return Response(resolution, status=status.HTTP_409_CONFLICT) def update(self, request, *args, **kwargs): """ - Update a schedule, generate timeslots, test for collisions and resolve them including notes + Update a schedule, generate timeslots, test for collisions and resolve + them including notes. - Only superusers may update schedules + Only superusers may update schedules. """ - pk = int_or_none('pk', self.kwargs) - show_pk = int_or_none('show_pk', self.kwargs) - # Only allow updating when calling /shows/1/schedules/1 - if show_pk is None or not request.user.is_superuser: + if not request.user.is_superuser: return Response(status=status.HTTP_401_UNAUTHORIZED) - # The schedule dict is mandatory - if 'schedule' not in request.data: + pk, show_pk = get_values(self.kwargs, "pk", "show_pk") + + # Only allow updating when calling /shows/{show_pk}/schedules/{pk}/ and with the `schedule` + # JSON object + if show_pk is None or "schedule" not in request.data: return Response(status=status.HTTP_400_BAD_REQUEST) - # If we're updating the default playlist id - # TODO: If nothing else than default_playlist_id, or is_repetition changed -> just save and don't do anything - new_schedule = request.data.get('schedule') - if default_playlist_id := new_schedule.get('default_playlist_id'): + # If default playlist id or repetition are given, just update + if default_playlist_id := request.data.get("schedule").get( + "default_playlist_id" + ): schedule = get_object_or_404(Schedule, pk=pk, show=show_pk) schedule.default_playlist_id = int(default_playlist_id) schedule.save() @@ -483,116 +461,91 @@ class APIScheduleViewSet(viewsets.ModelViewSet): serializer = ScheduleSerializer(schedule) return Response(serializer.data) + if is_repetition := request.data.get("schedule").get("is_repetition"): + schedule = get_object_or_404(Schedule, pk=pk, show=show_pk) + schedule.is_repetition = bool(is_repetition) + schedule.save() + + serializer = ScheduleSerializer(schedule) + return Response(serializer.data) + # First update submit -> return projected timeslots and collisions - if 'solutions' not in request.data: - return Response(Schedule.make_conflicts(request.data['schedule'], pk, show_pk)) + if "solutions" not in request.data: + return Response( + Schedule.make_conflicts(request.data["schedule"], pk, show_pk), + status=status.HTTP_409_CONFLICT, + ) # Otherwise try to resolve resolution = Schedule.resolve_conflicts(request.data, pk, show_pk) # If resolution went well - if 'projected' not in resolution: - return Response(resolution, status=status.HTTP_200_OK) + if "projected" not in resolution: + return Response(resolution) # Otherwise return conflicts - return Response(resolution) + return Response(resolution, status=status.HTTP_409_CONFLICT) def destroy(self, request, *args, **kwargs): """ - Delete a schedule - Only superusers may delete schedules + Delete a schedule. + + Only superusers may delete schedules. """ - pk = int_or_none('pk', self.kwargs) - show_pk = self.kwargs.get('show_pk') - # Only allow deleting when calling /shows/1/schedules/1 - if show_pk is None or not request.user.is_superuser: + if not request.user.is_superuser: return Response(status=status.HTTP_401_UNAUTHORIZED) + pk, show_pk = get_values(self.kwargs, "pk", "show_pk") + + # Only allow deleting when calling /shows/{show_pk}/schedules/{pk} + if show_pk is None: + return Response(status=status.HTTP_400_BAD_REQUEST) + Schedule.objects.get(pk=pk).delete() return Response(status=status.HTTP_204_NO_CONTENT) -class APITimeSlotViewSet(viewsets.ModelViewSet): +# TODO: Create is currently not implemented because timeslots are supposed to be inserted +# by creating or updating a schedule. +# There might be a use case for adding a single timeslot without any conflicts though. +class APITimeSlotViewSet( + mixins.RetrieveModelMixin, + mixins.UpdateModelMixin, + mixins.DestroyModelMixin, + mixins.ListModelMixin, + viewsets.GenericViewSet, +): """ - /timeslots returns timeslots of the next 60 days (GET). Timeslots may only be added by creating/updating a schedule - /timeslots/{pk} eturns the given timeslot (GET) - /timeslots/?start={start_date}&end={end_date} returns timeslots within the time range (GET) - /shows/{show_pk}/timeslots returns timeslots of the show (GET, POST) - /shows/{show_pk}/timeslots?surrounding returns the 10 nearest timeslots for the current date (GET) - /shows/{show_pk}/timeslots/{pk} returns a timeslots by its ID (GET, PUT, DELETE) - /shows/{show_pk}/timeslots/?start={start_date}&end={end_date} returns timeslots of the show within the time range - /shows/{show_pk}/schedules/{schedule_pk}/timeslots returns all timeslots of the schedule (GET, POST) - /shows/{show_pk}/schedules/{schedule_pk}/timeslots/{pk} returns a timeslot by its ID (GET, DELETE). If PUT, the next repetition is returned or nothing if the next timeslot isn't one - /shows/{show_pk}/schedules/{schedule_pk}/timeslots?start={start_date}&end={end_date} returns all timeslots of the schedule within the time range + Returns a list of timeslots. + + By default, only timeslots ranging from now + 60 days will be displayed. + You may override this default overriding start and/or end parameter. + + Timeslots may only be added by creating/updating a schedule. """ permission_classes = [permissions.DjangoModelPermissionsOrAnonReadOnly] serializer_class = TimeSlotSerializer pagination_class = LimitOffsetPagination - queryset = TimeSlot.objects.none() + queryset = TimeSlot.objects.all().order_by("-start") + filterset_class = filters.TimeSlotFilterSet def get_queryset(self): - show_pk = int_or_none('show_pk', self.kwargs) - schedule_pk = int_or_none('schedule_pk', self.kwargs) - # Filters - - # Return next 60 days by default - start = timezone.make_aware(datetime.combine(timezone.now(), time(0, 0))) - end = start + timedelta(days=60) - - if ('start' in self.request.query_params) and ('end' in self.request.query_params): - start = timezone.make_aware(datetime.combine(parse_date(self.request.query_params.get('start')), time(0, 0))) - end = timezone.make_aware(datetime.combine(parse_date(self.request.query_params.get('end')), time(23, 59))) + queryset = super().get_queryset() - default_order = '-start' - order = self.request.query_params.get('order', default_order) - - # If someone tries to sort by a field that isn't available on the model - # we silently ignore that and use the default sort order. - model_fields = [field.name for field in TimeSlot._meta.get_fields()] - if order.lstrip('-') not in model_fields: - order = default_order - - if 'surrounding' in self.request.query_params: - now = timezone.now() - - nearest_timeslots_in_future = TimeSlot.objects.filter(start__gte=now).order_by('start').values_list('id', flat=True)[:5] - nearest_timeslots_in_past = TimeSlot.objects.filter(start__lt=now).order_by('-start').values_list('id', flat=True)[:5] - relevant_timeslot_ids = list(nearest_timeslots_in_future) + list(nearest_timeslots_in_past) - - return TimeSlot.objects.filter(id__in=relevant_timeslot_ids).order_by(order) - - # Endpoints - - # - # /shows/1/schedules/1/timeslots/ - # - # Returns timeslots of the given show and schedule - # - if show_pk and schedule_pk: - return TimeSlot.objects.filter(show=show_pk, schedule=schedule_pk, start__gte=start, end__lte=end).order_by(order) + # subroute filters + show_pk, schedule_pk = get_values(self.kwargs, "show_pk", "schedule_pk") + if show_pk: + queryset = queryset.filter(show=show_pk) + if schedule_pk: + queryset = queryset.filter(schedule=schedule_pk) - # - # /shows/1/timeslots/ - # - # Returns timeslots of the show - # - elif show_pk and schedule_pk is None: - return TimeSlot.objects.filter(show=show_pk, start__gte=start, end__lte=end).order_by(order) - - # - # /timeslots/ - # - # Returns all timeslots - # - else: - return TimeSlot.objects.filter(start__gte=start, end__lte=end).order_by(order) + return queryset def retrieve(self, request, *args, **kwargs): - pk = int_or_none('pk', self.kwargs) - show_pk = int_or_none('show_pk', self.kwargs) + pk, show_pk = get_values(self.kwargs, "pk", "show_pk") if show_pk: timeslot = get_object_or_404(TimeSlot, pk=pk, show=show_pk) @@ -602,24 +555,27 @@ class APITimeSlotViewSet(viewsets.ModelViewSet): serializer = TimeSlotSerializer(timeslot) return Response(serializer.data) - def create(self, request, *args, **kwargs): - """ - Timeslots may only be created by adding/updating schedules - TODO: Adding single timeslot which fits to schedule? - """ - return Response(status=status.HTTP_401_UNAUTHORIZED) - def update(self, request, *args, **kwargs): """Link a playlist_id to a timeslot""" - pk = int_or_none('pk', self.kwargs) - show_pk = int_or_none('show_pk', self.kwargs) - schedule_pk = int_or_none('schedule_pk', self.kwargs) - # Update is only allowed when calling /shows/1/schedules/1/timeslots/1 and if user owns the show - if schedule_pk is None or show_pk is None or not Show.is_editable(self, show_pk): + pk, show_pk, schedule_pk = get_values( + self.kwargs, "pk", "show_pk", "schedule_pk" + ) + + if ( + not request.user.is_superuser + and show_pk not in request.user.shows.values_lis("id", flat=True) + ): return Response(status=status.HTTP_401_UNAUTHORIZED) - timeslot = get_object_or_404(TimeSlot, pk=pk, schedule=schedule_pk, show=show_pk) + # Update is only allowed when calling /shows/1/schedules/1/timeslots/1 and if user owns the + # show + if schedule_pk is None or show_pk is None: + return Response(status=status.HTTP_400_BAD_REQUEST) + + timeslot = get_object_or_404( + TimeSlot, pk=pk, schedule=schedule_pk, show=show_pk + ) serializer = TimeSlotSerializer(timeslot, data=request.data) if serializer.is_valid(): @@ -631,7 +587,7 @@ class APITimeSlotViewSet(viewsets.ModelViewSet): ts = TimeSlot.objects.filter(show=show_pk, start__gt=timeslot.start)[0] if ts.is_repetition: serializer = TimeSlotSerializer(ts) - return Response(serializer.data, status=status.HTTP_200_OK) + return Response(serializer.data) # ...or nothing if there isn't one return Response(status=status.HTTP_200_OK) @@ -640,19 +596,20 @@ class APITimeSlotViewSet(viewsets.ModelViewSet): def destroy(self, request, *args, **kwargs): """ - Delete a timeslot - Only superusers may delete timeslots + Deletes a timeslot. + + Only superusers may delete timeslots. """ - pk = int_or_none('pk', self.kwargs) - show_pk = int_or_none('show_pk', self.kwargs) + + if not request.user.is_superuser: + return Response(status=status.HTTP_401_UNAUTHORIZED) + + pk, show_pk = get_values(self.kwargs, "pk", "show_pk") # Only allow when calling endpoint starting with /shows/1/... if show_pk is None: return Response(status=status.HTTP_400_BAD_REQUEST) - if not request.user.is_superuser: - return Response(status=status.HTTP_401_UNAUTHORIZED) - TimeSlot.objects.get(pk=pk).delete() return Response(status=status.HTTP_204_NO_CONTENT) @@ -660,103 +617,60 @@ class APITimeSlotViewSet(viewsets.ModelViewSet): class APINoteViewSet(viewsets.ModelViewSet): """ - /notes/ returns all notes (GET) - /notes/{pk} returns a single note (if owned) (GET) - /notes/?ids={...} returns given notes (if owned) (GET) - /notes/?host={host} returns notes assigned to a given host (GET) - /notes/?owner={owner} returns notes editable by a given user (GET) - /notes/?user={user} returns notes created by a given user (GET) - /shows/{show_pk}/notes returns all notes of a show (GET) - /shows/{show_pk}/notes/{pk} returns a note by its ID (GET) - /shows/{show_pk}/timeslots/{timeslot_pk}/note/ returns a note of the timeslot (GET) - /shows/{show_pk}/timeslots/{timeslot_pk}/note/{pk} returns a note by its ID (GET) - /shows/{show_pk}/schedules/{schedule_pk}/timeslots/{timeslot_pk}/note returns a note to the timeslot (GET, POST). - /shows/{show_pk}/schedules/{schedule_pk}/timeslots/{timeslot_pk}/note/{pk} returns a note by its ID (GET, PUT, DELETE) - - Superusers may access and update all notes + Returns a list of notes. + + Superusers may access and update all notes. """ - queryset = Note.objects.none() + queryset = Note.objects.all() serializer_class = NoteSerializer permission_classes = [permissions.DjangoModelPermissionsOrAnonReadOnly] pagination_class = LimitOffsetPagination + filter_class = filters.NoteFilterSet def get_queryset(self): - timeslot_pk = int_or_none('timeslot_pk', self.kwargs) - show_pk = int_or_none('show_pk', self.kwargs) - - # Endpoints - - # - # /shows/1/schedules/1/timeslots/1/note - # /shows/1/timeslots/1/note - # - # Return a note to the timeslot - # - if show_pk and timeslot_pk: - notes = Note.objects.filter(show=show_pk, timeslot=timeslot_pk) + queryset = super().get_queryset() - # - # /shows/1/notes - # - # Returns notes to the show - # - elif show_pk and timeslot_pk is None: - notes = Note.objects.filter(show=show_pk) - - # - # /notes - # - # Returns all notes - # - else: - notes = Note.objects.all() - - # Filters - - if ids := self.request.query_params.get('ids'): - # Filter notes by their IDs - note_ids = list(map(int, ids.split(','))) - notes = notes.filter(id__in=note_ids) - - if host := self.request.query_params.get('host'): - # Filter notes by host - notes = notes.filter(host=int(host)) - - if owner := self.request.query_params.get('owner'): - # Filter notes by show owner: all notes the user may edit - shows = Show.objects.filter(owners=int(owner)) - notes = notes.filter(show__in=shows) - - if user := self.request.query_params.get('user'): - # Filter notes by their creator - notes = notes.filter(user=int(user)) + # subroute filters + show_pk, timeslot_pk = get_values(self.kwargs, "show_pk", "timeslot_pk") + if show_pk: + queryset = queryset.filter(show=show_pk) + if timeslot_pk: + queryset = queryset.filter(timeslot=timeslot_pk) - return notes + return queryset def create(self, request, *args, **kwargs): """Create a note""" - show_pk = int_or_none('show_pk', self.kwargs) - schedule_pk = int_or_none('schedule_pk', self.kwargs) - timeslot_pk = int_or_none('timeslot_pk', self.kwargs) + + show_pk, schedule_pk, timeslot_pk = get_values( + self.kwargs, "show_pk", "schedule_pk", "timeslot_pk" + ) + + if ( + not request.user.is_superuser + and show_pk not in request.user.shows.values_list("id", flat=True) + ): + return Response(status=status.HTTP_401_UNAUTHORIZED) # Only create a note if show_id, timeslot_id and schedule_id is given if show_pk is None or schedule_pk is None or timeslot_pk is None: return Response(status=status.HTTP_400_BAD_REQUEST) - if not Show.is_editable(self, show_pk): - return Response(status=status.HTTP_401_UNAUTHORIZED) - - serializer = NoteSerializer(data=request.data, context={'user_id': request.user.id}) + serializer = NoteSerializer( + data={"show": show_pk, "timeslot": timeslot_pk} | request.data, + context={"user_id": request.user.id}, + ) if serializer.is_valid(): - - # Don't assign a host the user mustn't edit - if not Host.is_editable(self, request.data['host']) or request.data['host'] is None: - serializer.validated_data['host'] = None + hosts = Host.objects.filter( + shows__in=request.user.shows.values_list("id", flat=True) + ) + if not request.user.is_superuser and request.data["host"] not in hosts: + serializer.validated_data["host"] = None serializer.save() - return Response(serializer.data) + return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) @@ -770,10 +684,9 @@ class APINoteViewSet(viewsets.ModelViewSet): /shows/1/timeslots/1/note/1 /shows/1/schedules/1/timeslots/1/note/1 """ - pk = int_or_none('pk', self.kwargs) - show_pk = int_or_none('show_pk', self.kwargs) - schedule_pk = int_or_none('schedule_pk', self.kwargs) - timeslot_pk = int_or_none('timeslot_pk', self.kwargs) + pk, show_pk, schedule_pk, timeslot_pk = get_values( + self.kwargs, "pk", "show_pk", "schedule_pk", "timeslot_pk" + ) # # /shows/1/notes/1 @@ -804,28 +717,34 @@ class APINoteViewSet(viewsets.ModelViewSet): return Response(serializer.data) def update(self, request, *args, **kwargs): - pk = int_or_none('pk', self.kwargs) - show_pk = int_or_none('show_pk', self.kwargs) - schedule_pk = int_or_none('schedule_pk', self.kwargs) - timeslot_pk = int_or_none('timeslot_pk', self.kwargs) + pk, show_pk, schedule_pk, timeslot_pk = get_values( + self.kwargs, "pk", "show_pk", "schedule_pk", "timeslot_pk" + ) + + if ( + not request.user.is_superuser + and show_pk not in request.user.shows.values_list("id", flat=True) + ): + return Response(status=status.HTTP_401_UNAUTHORIZED) - # Allow PUT only when calling /shows/1/schedules/1/timeslots/1/note/1 + # Allow PUT only when calling + # /shows/{show_pk}/schedules/{schedule_pk}/timeslots/{timeslot_pk}/note/{pk} if show_pk is None or schedule_pk is None or timeslot_pk is None: return Response(status=status.HTTP_400_BAD_REQUEST) note = get_object_or_404(Note, pk=pk, timeslot=timeslot_pk, show=show_pk) - # Commons users may only edit notes of shows they own - if not Note.is_editable(self, note.id): - return Response(status=status.HTTP_401_UNAUTHORIZED) - serializer = NoteSerializer(note, data=request.data) if serializer.is_valid(): - + hosts = Host.objects.filter( + shows__in=request.user.shows.values_list("id", flat=True) + ) # Don't assign a host the user mustn't edit. Reassign the original value instead - if not Host.is_editable(self, request.data['host']) and request.data['host']: - serializer.validated_data['host'] = Host.objects.filter(pk=note.host_id)[0] + if not request.user.is_superuser and int(request.data["host"]) not in hosts: + serializer.validated_data["host"] = Host.objects.filter( + pk=note.host_id + )[0] serializer.save() return Response(serializer.data) @@ -833,117 +752,85 @@ class APINoteViewSet(viewsets.ModelViewSet): return Response(status=status.HTTP_400_BAD_REQUEST) def destroy(self, request, *args, **kwargs): - # Allow DELETE only when calling /shows/1/schedules/1/timeslots/1/note/1 - pk = int_or_none('pk', self.kwargs) - show_pk = int_or_none('show_pk', self.kwargs) - schedule_pk = int_or_none('schedule_pk', self.kwargs) - timeslot_pk = int_or_none('timeslot_pk', self.kwargs) + pk, show_pk, schedule_pk, timeslot_pk = get_values( + self.kwargs, "pk", "show_pk", "schedule_pk", "timeslot_pk" + ) + + if ( + not request.user.is_superuser + and show_pk not in request.user.shows.values_list("id", flat=True) + ): + return Response(status=status.HTTP_401_UNAUTHORIZED) - if show_pk is None or schedule_pk is None or timeslot_pk is None: + if pk is None or show_pk is None or schedule_pk is None or timeslot_pk is None: return Response(status=status.HTTP_400_BAD_REQUEST) - note = get_object_or_404(Note, pk=pk) - - if Note.is_editable(self, note.id): - Note.objects.get(pk=pk).delete() - return Response(status=status.HTTP_204_NO_CONTENT) - - return Response(status=status.HTTP_401_UNAUTHORIZED) - + Note.objects.get(pk=pk).delete() -class ActiveInactiveViewSet(viewsets.ModelViewSet): - permission_classes = [permissions.DjangoModelPermissionsOrAnonReadOnly] - - def get_queryset(self: viewsets.ModelViewSet): - """Filters""" - - if self.request.query_params.get('active') == 'true': - return self.queryset.model.objects.filter(is_active=True) + return Response(status=status.HTTP_204_NO_CONTENT) - if self.request.query_params.get('active') == 'false': - return self.queryset.model.objects.filter(is_active=False) - return self.queryset.model.objects.all() +class ActiveFilterMixin: + filter_class = filters.ActiveFilterSet -class APICategoryViewSet(ActiveInactiveViewSet): +class APICategoryViewSet(ActiveFilterMixin, viewsets.ModelViewSet): """ - /categories/ returns all categories (GET, POST) - /categories/?active=true returns all active categories (GET) - /categories/?active=false returns all inactive categories (GET) - /categories/{pk} Returns a category by its ID (GET, PUT, DELETE) + Returns a list of categories. """ queryset = Category.objects.all() serializer_class = CategorySerializer -class APITypeViewSet(ActiveInactiveViewSet): +class APITypeViewSet(ActiveFilterMixin, viewsets.ModelViewSet): """ - /types/ returns all types (GET, POST) - /types/?active=true returns all active types (GET) - /types/?active=false returns all inactive types (GET) - /types/{pk} returns a type by its ID (GET, PUT, DELETE) + Returns a list of types. """ queryset = Type.objects.all() serializer_class = TypeSerializer -class APITopicViewSet(ActiveInactiveViewSet): +class APITopicViewSet(ActiveFilterMixin, viewsets.ModelViewSet): """ - /topics/: Returns all topics (GET, POST) - /topics/?active=true Returns all active topics (GET) - /topics/?active=false Returns all inactive topics (GET) - /topics/{pk}: Returns a topic by its ID (GET, PUT, DELETE) + Returns a list of topics. """ queryset = Topic.objects.all() serializer_class = TopicSerializer -class APIMusicFocusViewSet(ActiveInactiveViewSet): +class APIMusicFocusViewSet(ActiveFilterMixin, viewsets.ModelViewSet): """ - /musicfocus/ returns all music focuses (GET, POST) - /musicfocus/?active=true: returns all active music focuses (GET) - /musicfocus/?active=false: returns all inactive music focuses (GET) - /musicfocus/{pk}: returns a music focus by its ID (GET, PUT, DELETE) + Returns a list of music focuses. """ queryset = MusicFocus.objects.all() serializer_class = MusicFocusSerializer -class APIFundingCategoryViewSet(ActiveInactiveViewSet): +class APIFundingCategoryViewSet(ActiveFilterMixin, viewsets.ModelViewSet): """ - /fundingcategories/: returns all funding categories (GET, POST) - /fundingcategories/?active=true returns all active funding categories (GET) - /fundingcategories/?active=false returns all inactive funding categories (GET) - /fundingcategories/{pk} returns a funding category by its ID (GET, PUT, DELETE) + Returns a list of funding categories. """ queryset = FundingCategory.objects.all() serializer_class = FundingCategorySerializer -class APILanguageViewSet(ActiveInactiveViewSet): +class APILanguageViewSet(ActiveFilterMixin, viewsets.ModelViewSet): """ - /languages/ returns all languages (GET, POST) - /languages/?active=true returns all active languages (GET) - /languages/?active=false returns all inactive languages (GET) - /languages/{pk} returns a language by its ID (GET, PUT, DELETE) + Returns a list of languages. """ queryset = Language.objects.all() serializer_class = LanguageSerializer -class APIHostViewSet(ActiveInactiveViewSet): +class APIHostViewSet(ActiveFilterMixin, viewsets.ModelViewSet): """ - /hosts/ returns all hosts (GET, POST) - /hosts/?active=true returns all active hosts (GET) - /hosts/?active=false returns all inactive hosts (GET) - /hosts/{pk} returns a host by its ID (GET, PUT, DELETE) + Returns a list of hosts. """ queryset = Host.objects.all() diff --git a/requirements.txt b/requirements.txt index 9f5b4c0413316156f5c2f80de2f9c309fb729340..8ef0a542428224ef5041843867732511557ee527 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,10 +1,11 @@ asgiref==3.4.1 attrs==21.4.0 certifi==2021.10.8 -charset-normalizer==2.0.10 -Django==3.2.11 +charset-normalizer==2.0.12 +Django==3.2.12 django-cors-headers==3.11.0 django-environ==0.8.1 +django-filter==21.1 django-oidc-provider==0.7.0 django-versatileimagefield==2.2 djangorestframework==3.13.1 @@ -14,16 +15,18 @@ gunicorn==20.1.0 idna==3.3 inflection==0.5.1 iniconfig==1.1.1 +isort==5.10.1 olefile==0.46 packaging==21.3 -Pillow==9.0.0 +Pillow==9.0.1 pluggy==1.0.0 +pre-commit==2.17.0 psycopg2-binary==2.9.3 py==1.11.0 pycryptodomex==3.12.0 pyjwkest==1.4.2 pyparsing==3.0.6 -pytest==6.2.5 +pytest==7.1.0 pytest-django==4.5.2 python-dateutil==2.8.2 python-magic==0.4.24 diff --git a/steering/oidc_provider_settings.py b/steering/oidc_provider_settings.py index c2641cb8cfb94fb50de06824e4dbd2a261f20dbe..400fe29b34b0864c2fb8b0a764a4b678f545e217 100644 --- a/steering/oidc_provider_settings.py +++ b/steering/oidc_provider_settings.py @@ -18,39 +18,39 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. # -from django.utils.translation import gettext as _ from oidc_provider.lib.claims import ScopeClaims +from django.utils.translation import gettext as _ + class AuraScopeClaims(ScopeClaims): info_username = ( - _(u'username'), - _(u'Your username.'), + _("username"), + _("Your username."), ) def scope_username(self): dic = { - 'username': self.user.username, + "username": self.user.username, # 'privileged': (self.user.is_staff or self.user.is_superuser) - 'privileged': self.user.is_superuser + "privileged": self.user.is_superuser, } return dic info_aura_shows = ( - _(u'AURA Shows'), - _(u'AURA shows you have access to.'), + _("AURA Shows"), + _("AURA shows you have access to."), ) def scope_aura_shows(self): from program.models import Show # TODO: should add filter `is_active=True` ? - public_show_slugs = list(Show.objects.filter(is_public=True).values_list('slug', flat=True)) - show_slugs = list(self.user.shows.all().values_list('slug', flat=True)) - dic = { - 'shows': show_slugs, - 'public-shows': public_show_slugs - } + public_show_slugs = list( + Show.objects.filter(is_public=True).values_list("slug", flat=True) + ) + show_slugs = list(self.user.shows.all().values_list("slug", flat=True)) + dic = {"shows": show_slugs, "public-shows": public_show_slugs} return dic diff --git a/steering/settings.py b/steering/settings.py index 773bec676129b8edcf0dcffb5d62a3ab4f2432d3..16eb0590c4253180e16d823442f84b957627526e 100644 --- a/steering/settings.py +++ b/steering/settings.py @@ -9,134 +9,131 @@ from corsheaders.defaults import default_headers PROJECT_DIR = os.path.dirname(__file__) -LOCALE_PATHS = ( - os.path.join(PROJECT_DIR, 'locale'), -) +LOCALE_PATHS = (os.path.join(PROJECT_DIR, "locale"),) -MEDIA_ROOT = os.path.join(PROJECT_DIR, 'site_media') -MEDIA_URL = '/site_media/' +MEDIA_ROOT = os.path.join(PROJECT_DIR, "site_media") +MEDIA_URL = "/site_media/" -STATIC_ROOT = os.path.join(PROJECT_DIR, 'static') -STATIC_URL = '/static/' +STATIC_ROOT = os.path.join(PROJECT_DIR, "static") +STATIC_URL = "/static/" -ROOT_URLCONF = 'steering.urls' +ROOT_URLCONF = "steering.urls" env = environ.Env() -env.read_env(env_file=PROJECT_DIR + '/../.env') +env.read_env(env_file=PROJECT_DIR + "/../.env") -DEBUG = env.bool('DEBUG', default=False) +DEBUG = env.bool("DEBUG", default=False) SITE_ID = 1 ADMINS = () MANAGERS = ADMINS # Must be set if DEBUG is False -ALLOWED_HOSTS = env.list('ALLOWED_HOSTS', default=['127.0.0.1', 'localhost']) +ALLOWED_HOSTS = env.list("ALLOWED_HOSTS", default=["127.0.0.1", "localhost"]) # Whitelist IPs that access the API -CORS_ORIGIN_WHITELIST = env.list('CORS_ORIGIN_WHITELIST', default=( - 'http://localhost:8080', - 'http://localhost:8040' -)) +CORS_ORIGIN_WHITELIST = env.list( + "CORS_ORIGIN_WHITELIST", default=("http://localhost:8080", "http://localhost:8040") +) CORS_ALLOW_CREDENTIALS = True CORS_ALLOW_HEADERS = list(default_headers) + [ - 'content-disposition', + "content-disposition", ] # if we are in a virtual environment, we use SQLite and enable debug -if os.environ.get('VIRTUAL_ENV'): +if os.environ.get("VIRTUAL_ENV"): DEBUG = True DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': os.path.join(PROJECT_DIR, "db.sqlite3") + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": os.path.join(PROJECT_DIR, "db.sqlite3"), } } else: DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.postgresql', - 'NAME': env.str('POSTGRES_DB', default='steering'), - 'USER': env.str('POSTGRES_USER', default='steering'), - 'PASSWORD': env.str('POSTGRES_PASSWORD'), - 'HOST': env.str('POSTGRES_HOST', default='steering-postgres'), - 'PORT': env.str('POSTGRES_PORT', default='5432'), + "default": { + "ENGINE": "django.db.backends.postgresql", + "NAME": env.str("POSTGRES_DB", default="steering"), + "USER": env.str("POSTGRES_USER", default="steering"), + "PASSWORD": env.str("POSTGRES_PASSWORD"), + "HOST": env.str("POSTGRES_HOST", default="steering-postgres"), + "PORT": env.str("POSTGRES_PORT", default="5432"), }, } -CACHE_BACKEND = 'locmem://' +CACHE_BACKEND = "locmem://" # LOCALIZATION -TIME_ZONE = env.str('TIME_ZONE', default='Europe/Vienna') -LANGUAGE_CODE = env.str('LANGUAGE_CODE', default='de') +TIME_ZONE = env.str("TIME_ZONE", default="Europe/Vienna") +LANGUAGE_CODE = env.str("LANGUAGE_CODE", default="de") USE_I18N = True USE_L10N = True -SECRET_KEY = env.str('SECRET_KEY') +SECRET_KEY = env.str("SECRET_KEY") TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [ - os.path.join(PROJECT_DIR, 'templates') - ], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.contrib.auth.context_processors.auth', - 'django.template.context_processors.request', - 'django.template.context_processors.debug', - 'django.template.context_processors.i18n', - 'django.template.context_processors.media', - 'django.template.context_processors.static', - 'django.template.context_processors.tz', - 'django.contrib.messages.context_processors.messages', + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [os.path.join(PROJECT_DIR, "templates")], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.contrib.auth.context_processors.auth", + "django.template.context_processors.request", + "django.template.context_processors.debug", + "django.template.context_processors.i18n", + "django.template.context_processors.media", + "django.template.context_processors.static", + "django.template.context_processors.tz", + "django.contrib.messages.context_processors.messages", ], }, }, ] MIDDLEWARE = ( - 'corsheaders.middleware.CorsMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', + "corsheaders.middleware.CorsMiddleware", + "django.middleware.common.CommonMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", ) REST_FRAMEWORK = { # Use Django's standard `django.contrib.auth` permissions, # or allow read-only access for unauthenticated users. - 'DEFAULT_PERMISSION_CLASSES': [ - 'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly' + "DEFAULT_PERMISSION_CLASSES": [ + "rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly" ], - 'DEFAULT_AUTHENTICATION_CLASSES': [ - 'program.auth.OidcOauth2Auth', + "DEFAULT_AUTHENTICATION_CLASSES": [ + "program.auth.OidcOauth2Auth", ], + "DEFAULT_FILTER_BACKENDS": ["django_filters.rest_framework.DjangoFilterBackend"], } INSTALLED_APPS = ( - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.sites', - 'django.contrib.messages', - 'django.contrib.admin', - 'django.contrib.staticfiles', - 'program', - 'profile', - 'versatileimagefield', - 'rest_framework', - 'rest_framework_nested', - 'oidc_provider', - 'corsheaders', + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.sites", + "django.contrib.messages", + "django.contrib.admin", + "django.contrib.staticfiles", + "program", + "profile", + "versatileimagefield", + "rest_framework", + "rest_framework_nested", + "django_filters", + "oidc_provider", + "corsheaders", ) -DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' +DEFAULT_AUTO_FIELD = "django.db.models.AutoField" # Set the desired sizes for your thumbnails (px) # Will apply to all uploaded images -THUMBNAIL_SIZES = ['640x480', '200x200', '150x150'] +THUMBNAIL_SIZES = ["640x480", "200x200", "150x150"] # When generating schedules/timeslots: # If until date wasn't set manually, add x days to the start date @@ -147,21 +144,21 @@ AUTO_SET_UNTIL_DATE_TO_DAYS_IN_FUTURE = 365 AUTO_SET_UNTIL_DATE_TO_END_OF_YEAR = True # URL to CBA - Cultural Broadcasting Archive -CBA_URL = 'https://cba.fro.at' +CBA_URL = "https://cba.fro.at" # Contact cba@fro.at to get whitelisted and get an API KEY # Leave empty to disable requests to CBA -CBA_API_KEY = '' +CBA_API_KEY = "" # URL to CBA ajax handler (used for retrieving additional data or increasing stream counter) -CBA_AJAX_URL = CBA_URL + '/wp-admin/admin-ajax.php' +CBA_AJAX_URL = CBA_URL + "/wp-admin/admin-ajax.php" # URL to CBA's REST API with trailing slash -CBA_REST_API_URL = CBA_URL + '/wp-json/wp/v2/' +CBA_REST_API_URL = CBA_URL + "/wp-json/wp/v2/" # OIDC Provider Settings USE_TZ = True # django-oidc-provider needs timezones in database -LOGIN_URL = '/admin/login/' # Login page OIDC redirects to -OIDC_EXTRA_SCOPE_CLAIMS = 'steering.oidc_provider_settings.AuraScopeClaims' +LOGIN_URL = "/admin/login/" # Login page OIDC redirects to +OIDC_EXTRA_SCOPE_CLAIMS = "steering.oidc_provider_settings.AuraScopeClaims" # WSGI_APPLICATION = 'steering.wsgi.application'; diff --git a/steering/urls.py b/steering/urls.py index fbe50aece5771607b06f82a1139671c02bc5d7c4..fdad4b383cf77da35c0620733567525c9a9c14cb 100644 --- a/steering/urls.py +++ b/steering/urls.py @@ -18,66 +18,87 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. # -from django.urls import include, path -from django.contrib import admin from rest_framework_nested import routers -from program.views import APIUserViewSet, APIHostViewSet, APIShowViewSet, APIScheduleViewSet, APITimeSlotViewSet, \ - APINoteViewSet, APICategoryViewSet, APITypeViewSet, APITopicViewSet, APIMusicFocusViewSet, APIFundingCategoryViewSet, \ - APILanguageViewSet, json_day_schedule, json_playout +from django.contrib import admin +from django.urls import include, path +from program.views import ( + APICategoryViewSet, + APIFundingCategoryViewSet, + APIHostViewSet, + APILanguageViewSet, + APIMusicFocusViewSet, + APINoteViewSet, + APIScheduleViewSet, + APIShowViewSet, + APITimeSlotViewSet, + APITopicViewSet, + APITypeViewSet, + APIUserViewSet, + json_day_schedule, + json_playout, +) admin.autodiscover() router = routers.DefaultRouter() -router.register(r'users', APIUserViewSet) -router.register(r'hosts', APIHostViewSet) -router.register(r'shows', APIShowViewSet) -router.register(r'schedules', APIScheduleViewSet) -router.register(r'timeslots', APITimeSlotViewSet) -router.register(r'notes', APINoteViewSet) -router.register(r'categories', APICategoryViewSet) -router.register(r'topics', APITopicViewSet) -router.register(r'types', APITypeViewSet) -router.register(r'musicfocus', APIMusicFocusViewSet) -router.register(r'fundingcategories', APIFundingCategoryViewSet) -router.register(r'languages', APILanguageViewSet) +router.register(r"users", APIUserViewSet) +router.register(r"hosts", APIHostViewSet) +router.register(r"shows", APIShowViewSet) +router.register(r"schedules", APIScheduleViewSet) +router.register(r"timeslots", APITimeSlotViewSet) +router.register(r"notes", APINoteViewSet) +router.register(r"categories", APICategoryViewSet) +router.register(r"topics", APITopicViewSet) +router.register(r"types", APITypeViewSet) +router.register(r"musicfocus", APIMusicFocusViewSet) +router.register(r"fundingcategories", APIFundingCategoryViewSet) +router.register(r"languages", APILanguageViewSet) # Nested Routers -show_router = routers.NestedSimpleRouter(router, r'shows', lookup='show') +show_router = routers.NestedSimpleRouter(router, r"shows", lookup="show") # /shows/1/schedules -show_router.register(r'schedules', APIScheduleViewSet, basename='show-schedules') +show_router.register(r"schedules", APIScheduleViewSet, basename="show-schedules") # /shows/1/notes -show_router.register(r'notes', APINoteViewSet, basename='show-notes') +show_router.register(r"notes", APINoteViewSet, basename="show-notes") # /shows/1/timeslots -show_router.register(r'timeslots', APITimeSlotViewSet, basename='show-timeslots') -show_timeslot_router = routers.NestedSimpleRouter(show_router, r'timeslots', lookup='timeslot') +show_router.register(r"timeslots", APITimeSlotViewSet, basename="show-timeslots") +show_timeslot_router = routers.NestedSimpleRouter( + show_router, r"timeslots", lookup="timeslot" +) # /shows/1/timeslots/1/note/ -show_timeslot_router.register(r'note', APINoteViewSet, basename='show-timeslots-note') +show_timeslot_router.register(r"note", APINoteViewSet, basename="show-timeslots-note") # /shows/1/schedules -schedule_router = routers.NestedSimpleRouter(show_router, r'schedules', lookup='schedule') +schedule_router = routers.NestedSimpleRouter( + show_router, r"schedules", lookup="schedule" +) # /shows/1/schedules/1/timeslots -schedule_router.register(r'timeslots', APITimeSlotViewSet, basename='schedule-timeslots') -timeslot_router = routers.NestedSimpleRouter(schedule_router, r'timeslots', lookup='timeslot') +schedule_router.register( + r"timeslots", APITimeSlotViewSet, basename="schedule-timeslots" +) +timeslot_router = routers.NestedSimpleRouter( + schedule_router, r"timeslots", lookup="timeslot" +) # /shows/1/schedules/1/timeslots/1/note -timeslot_router.register(r'note', APINoteViewSet, basename='timeslots-note') +timeslot_router.register(r"note", APINoteViewSet, basename="timeslots-note") urlpatterns = [ - path('openid/', include('oidc_provider.urls', namespace='oidc_provider')), - path('api/v1/', include(router.urls)), - path('api/v1/', include(show_router.urls)), - path('api/v1/', include(show_timeslot_router.urls)), - path('api/v1/', include(schedule_router.urls)), - path('api/v1/', include(timeslot_router.urls)), - path('api/v1/playout', json_playout), - path('api/v1/program/week', json_playout), - path('api/v1/program/<int:year>/<int:month>/<int:day>)/', json_day_schedule), - path('admin/', admin.site.urls), + path("openid/", include("oidc_provider.urls", namespace="oidc_provider")), + path("api/v1/", include(router.urls)), + path("api/v1/", include(show_router.urls)), + path("api/v1/", include(show_timeslot_router.urls)), + path("api/v1/", include(schedule_router.urls)), + path("api/v1/", include(timeslot_router.urls)), + path("api/v1/playout", json_playout), + path("api/v1/program/week", json_playout), + path("api/v1/program/<int:year>/<int:month>/<int:day>)/", json_day_schedule), + path("admin/", admin.site.urls), ] diff --git a/steering/wsgi.py b/steering/wsgi.py index 7c83fe0c815e9c0dd96b815a90e35b328c2fe7fb..4ef8b8cc96bfe8eaeca46b0afe28e2009b88044d 100644 --- a/steering/wsgi.py +++ b/steering/wsgi.py @@ -1,33 +1,16 @@ -# -*- coding: utf-8 -*- - """ -WSGI config for pv project. - -This module contains the WSGI application used by Django's development server -and any production WSGI deployments. It should expose a module-level variable -named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover -this application via the ``WSGI_APPLICATION`` setting. +WSGI config for alias project. -Usually you will have the standard Django WSGI application here, but it also -might make sense to replace the whole Django WSGI application with a custom one -that later delegates to the Django one. For example, you could introduce WSGI -middleware here, or combine a Django application with an application of another -framework. +It exposes the WSGI callable as a module-level variable named ``application``. +For more information on this file, see +https://docs.djangoproject.com/en/3.2/howto/deployment/wsgi/ """ + import os -import sys -sys.path.append('/srv/pv/pv') +from django.core.wsgi import get_wsgi_application os.environ.setdefault("DJANGO_SETTINGS_MODULE", "steering.settings") -# This application object is used by any WSGI server configured to use this -# file. This includes Django's development server, if the WSGI_APPLICATION -# setting points here. -from django.core.wsgi import get_wsgi_application application = get_wsgi_application() - -# Apply WSGI middleware here. -# from helloworld.wsgi import HelloWorldApplication -# application = HelloWorldApplication(application)