diff --git a/program/admin.py b/program/admin.py index bf09bea59495a50a9a71eb099fc550050e3bc6d9..152fb677dd5b017ee5f48470bf00e9cea9c202ca 100644 --- a/program/admin.py +++ b/program/admin.py @@ -84,11 +84,18 @@ class HostAdmin(admin.ModelAdmin): list_display = ('name','email',) list_filter = (ActiveHostsFilter, 'is_always_visible',) + def get_queryset(self, request): + if request.user.is_superuser: + return Host.objects.all() + + # Common users only see hosts of shows they own + return Host.objects.filter(shows__in=request.user.shows.all()).distinct() + class NoteAdmin(admin.ModelAdmin): date_hierarchy = 'start' list_display = ('title', 'show', 'start', 'status', 'user') - fields = (( 'show', 'timeslot'), 'title', 'slug', 'summary', 'content', 'image', 'status', 'cba_id') + fields = (( 'show', 'timeslot'), 'title', 'slug', 'summary', 'content', 'image', 'host', 'status', 'cba_id') prepopulated_fields = {'slug': ('title',)} list_filter = ('status',) ordering = ('timeslot',) @@ -98,16 +105,18 @@ class NoteAdmin(admin.ModelAdmin): js = [ settings.MEDIA_URL + 'js/calendar/lib/moment.min.js', settings.MEDIA_URL + 'js/note_change.js', ] + def get_queryset(self, request): if request.user.is_superuser: - # Superusers see notes of all shows shows = Show.objects.all() else: - # Users only see notes of shows they own + # Commons users only see notes of shows they own shows = request.user.shows.all() return super(NoteAdmin, self).get_queryset(request).filter(show__in=shows) + + def formfield_for_foreignkey(self, db_field, request=None, **kwargs): four_weeks_ago = datetime.now() - timedelta(weeks=4) in_twelve_weeks = datetime.now() + timedelta(weeks=12) @@ -134,12 +143,19 @@ class NoteAdmin(admin.ModelAdmin): if db_field.name == 'show': # Adding/Editing a note: load user's shows into the dropdown - # Superusers see all shows + # Common users only see shows they own if not request.user.is_superuser: kwargs['queryset'] = Show.objects.filter(pk__in=request.user.shows.all()) + + if db_field.name == 'host': + # Common users only see hosts of shows they own + if not request.user.is_superuser: + kwargs['queryset'] = Host.objects.filter(shows__in=request.user.shows.all()).distinct() + return super(NoteAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs) + def save_model(self, request, obj, form, change): # Save the creator when adding a note @@ -227,8 +243,11 @@ class ShowAdmin(admin.ModelAdmin): def get_readonly_fields(self, request, obj=None): '''Limit field access for common users''' + if not request.user.is_superuser: + # TODO: how to set field 'name' readonly although it's required? return ('predecessor', 'type', 'hosts', 'owners', 'language', 'category', 'topic', 'musicfocus', 'rtrcategory') + return list() diff --git a/program/fixtures/group_permissions.yaml b/program/fixtures/group_permissions.yaml index 20fa7567d6c35ae28963b3eb5cedc31593bf69a2..95191a376e7c0a0ae4d164f3f7a9feb4c6b0f5f5 100644 --- a/program/fixtures/group_permissions.yaml +++ b/program/fixtures/group_permissions.yaml @@ -33,4 +33,10 @@ fields: id: 6 group_id: 1 - permission_id: 68 \ No newline at end of file + permission_id: 68 +- model: auth.group_permissions + pk: 7 + fields: + id: 6 + group_id: 1 + permission_id: 23 \ No newline at end of file diff --git a/program/migrations/0012_auto_20171129_1828.py b/program/migrations/0012_auto_20180103_0054.py similarity index 80% rename from program/migrations/0012_auto_20171129_1828.py rename to program/migrations/0012_auto_20180103_0054.py index b9e2d992eb496711873a14b358dde40e4cabd354..8ba4be8ef317ef9547fe4d3bc8f06b74d2361dc9 100644 --- a/program/migrations/0012_auto_20171129_1828.py +++ b/program/migrations/0012_auto_20180103_0054.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Generated by Django 1.11.3 on 2017-11-29 18:28 +# Generated by Django 1.11.3 on 2018-01-02 23:54 from __future__ import unicode_literals from django.conf import settings @@ -24,6 +24,8 @@ class Migration(migrations.Migration): ('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')), @@ -70,7 +72,7 @@ class Migration(migrations.Migration): ('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_playlist_id', models.IntegerField(blank=True, null=True, verbose_name='Fallback Playlist ID')), + ('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)), @@ -150,6 +152,71 @@ class Migration(migrations.Migration): model_name='timeslot', name='programslot', ), + migrations.AddField( + model_name='host', + name='biography', + field=tinymce.models.HTMLField(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', @@ -160,6 +227,11 @@ class Migration(migrations.Migration): 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', @@ -198,8 +270,8 @@ class Migration(migrations.Migration): ), migrations.AddField( model_name='show', - name='fallback_pool', - field=models.CharField(blank=True, max_length=255, verbose_name='Fallback Pool'), + name='fallback_id', + field=models.IntegerField(blank=True, null=True, verbose_name='Fallback ID'), ), migrations.AddField( model_name='show', @@ -224,7 +296,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='timeslot', name='is_repetition', - field=models.BooleanField(default=False, verbose_name='WH'), + field=models.BooleanField(default=False, verbose_name='REP'), ), migrations.AddField( model_name='timeslot', @@ -236,6 +308,11 @@ class Migration(migrations.Migration): 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'), + ), migrations.AlterField( model_name='musicfocus', name='big_button', diff --git a/program/migrations/0013_category_color.py b/program/migrations/0013_category_color.py deleted file mode 100644 index 1dcdab40ab9dcb37db14ee34a41a89c006817e82..0000000000000000000000000000000000000000 --- a/program/migrations/0013_category_color.py +++ /dev/null @@ -1,20 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11.3 on 2017-12-12 15:14 -from __future__ import unicode_literals - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('program', '0012_auto_20171129_1828'), - ] - - operations = [ - migrations.AddField( - model_name='category', - name='color', - field=models.TextField(blank=True, max_length=7, verbose_name='Color'), - ), - ] diff --git a/program/migrations/0014_category_description.py b/program/migrations/0014_category_description.py deleted file mode 100644 index 31bf87bc7ecf1ab5358a5101f39a55f15d229b50..0000000000000000000000000000000000000000 --- a/program/migrations/0014_category_description.py +++ /dev/null @@ -1,20 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11.3 on 2017-12-12 15:18 -from __future__ import unicode_literals - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('program', '0013_category_color'), - ] - - operations = [ - migrations.AddField( - model_name='category', - name='description', - field=models.TextField(blank=True, verbose_name='Description'), - ), - ] diff --git a/program/migrations/0015_note_audio_url.py b/program/migrations/0015_note_audio_url.py deleted file mode 100644 index 5a107364a0b1dcbcabbb36f01940cd0a33b54990..0000000000000000000000000000000000000000 --- a/program/migrations/0015_note_audio_url.py +++ /dev/null @@ -1,20 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11.3 on 2017-12-12 19:18 -from __future__ import unicode_literals - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('program', '0014_category_description'), - ] - - operations = [ - migrations.AddField( - model_name='note', - name='audio_url', - field=models.TextField(blank=True, editable=False, verbose_name='Direct URL to a linked audio file'), - ), - ] diff --git a/program/migrations/0016_auto_20171213_1737.py b/program/migrations/0016_auto_20171213_1737.py deleted file mode 100644 index c87b746ced70f2a609271de34e125fd891ebd6d5..0000000000000000000000000000000000000000 --- a/program/migrations/0016_auto_20171213_1737.py +++ /dev/null @@ -1,87 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11.3 on 2017-12-13 17:37 -from __future__ import unicode_literals - -from django.db import migrations, models -import tinymce.models -import versatileimagefield.fields - - -class Migration(migrations.Migration): - - dependencies = [ - ('program', '0015_note_audio_url'), - ] - - operations = [ - migrations.AddField( - model_name='host', - name='biography', - field=tinymce.models.HTMLField(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='user_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.AlterField( - model_name='host', - name='website', - field=models.URLField(blank=True, help_text='URL to your personal website.', verbose_name='Website'), - ), - migrations.AlterField( - model_name='timeslot', - name='is_repetition', - field=models.BooleanField(default=False, verbose_name='REP'), - ), - ] diff --git a/program/migrations/0017_auto_20180102_1535.py b/program/migrations/0017_auto_20180102_1535.py deleted file mode 100644 index bc5a82cbb6c6b7754a3e5f87317d1515fb507f03..0000000000000000000000000000000000000000 --- a/program/migrations/0017_auto_20180102_1535.py +++ /dev/null @@ -1,39 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11.3 on 2018-01-02 15:35 -from __future__ import unicode_literals - -from django.db import migrations, models -import versatileimagefield.fields - - -class Migration(migrations.Migration): - - dependencies = [ - ('program', '0016_auto_20171213_1737'), - ] - - operations = [ - migrations.RemoveField( - model_name='schedule', - name='fallback_playlist_id', - ), - migrations.RemoveField( - model_name='show', - name='fallback_pool', - ), - migrations.AddField( - model_name='schedule', - name='fallback_id', - field=models.IntegerField(blank=True, null=True, verbose_name='Fallback ID'), - ), - migrations.AddField( - model_name='show', - name='fallback_id', - field=models.CharField(blank=True, max_length=255, verbose_name='Fallback ID'), - ), - migrations.AlterField( - 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'), - ), - ] diff --git a/program/models.py b/program/models.py index 21ec31c55507ff858bd316afcaeedd35da2586da..468f88cb7d70880d23cb8796bc860cc72cf10ce4 100644 --- a/program/models.py +++ b/program/models.py @@ -271,6 +271,17 @@ class Host(models.Model): def active_shows(self): return self.shows.filter(schedules__until__gt=datetime.today()) + def is_editable(self, host_id): + """ + Whether the given host is assigned to a show the current user owns + @return boolean + """ + if self.request.user.is_superuser: + return True + + host_ids = Host.objects.filter(shows__in=self.request.user.shows.all()).distinct().values_list('id', flat=True) + return int(host_id) in host_ids + def save(self, *args, **kwargs): super(Host, self).save(*args, **kwargs) @@ -304,7 +315,7 @@ class Show(models.Model): email = models.EmailField(_("E-Mail"), blank=True, null=True, help_text=_("The main contact email address for your show.")) website = models.URLField(_("Website"), blank=True, null=True, help_text=_("Is there a website to your show? Type in its URL.")) cba_series_id = models.IntegerField(_("CBA Series ID"), blank=True, null=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")) - fallback_id = models.CharField(_("Fallback ID"), max_length=255, blank=True) + fallback_id = models.IntegerField(_("Fallback ID"), blank=True, null=True) created = models.DateTimeField(auto_now_add=True, editable=False) last_updated = models.DateTimeField(auto_now=True, editable=False) @@ -328,14 +339,14 @@ class Show(models.Model): def is_editable(self, show_id): """ - Whether the current user can edit the given show + Whether the current user is owner of the given show @return boolean """ if self.request.user.is_superuser: return True - else: - show_ids = self.request.user.shows.all().values_list('id', flat=True) - return int(show_id) in show_ids + + show_ids = self.request.user.shows.all().values_list('id', flat=True) + return int(show_id) in show_ids class RRule(models.Model): @@ -596,6 +607,12 @@ class TimeSlotManager(models.Manager): 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) + + class TimeSlot(models.Model): schedule = models.ForeignKey(Schedule, related_name='timeslots', verbose_name=_("Schedule")) start = models.DateTimeField(_("Start time")) # Removed 'unique=True' because new Timeslots need to be created before deleting the old ones (otherwise linked notes get deleted first) @@ -656,6 +673,7 @@ class Note(models.Model): created = models.DateTimeField(auto_now_add=True, editable=False) last_updated = models.DateTimeField(auto_now=True, editable=False) user = models.ForeignKey(User, editable=False, related_name='users', default=1) + host = models.ForeignKey(Host, related_name='hosts', null=True) class Meta: ordering = ('timeslot',) @@ -665,6 +683,16 @@ class Note(models.Model): def __str__(self): return '%s - %s' % (self.title, self.timeslot) + def is_editable(self, note_id): + """ + Whether the given note is assigned to a show the current user owns + @return boolean + """ + if self.request.user.is_superuser: + return True + + return int(note_id) in self.request.user.shows.all().values_list('id', flat=True) + def get_audio_url(cba_id): """ Retrieve the direct URL to the mp3 in CBA @@ -679,7 +707,7 @@ class Note(models.Model): audio_url = '' - if cba_id != '' and CBA_API_KEY != '': + if cba_id != None and cba_id != '' and CBA_API_KEY != '': from urllib.request import urlopen import json diff --git a/program/serializers.py b/program/serializers.py index fb950785f455103f1d79b3818e48a62e42951650..a1b6936b18455fada66d5018a27eacb4d472fb65 100644 --- a/program/serializers.py +++ b/program/serializers.py @@ -28,7 +28,7 @@ class UserSerializer(serializers.ModelSerializer): user.set_password(validated_data['password']) user.save() - profile = Profile(user=user, cba_username=profile_data.get('cba_username'), cba_user_token=profile_data.get('cba_user_token')) + 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 @@ -98,7 +98,6 @@ class HostSerializer(serializers.ModelSerializer): Update and return an existing Host instance, given the validated data. """ - # TODO: Still put this into a sub app? instance.name = validated_data.get('name', instance.name) instance.is_always_visible = validated_data.get('is_always_visible', instance.is_always_visible) instance.email = validated_data.get('email', instance.email) @@ -207,14 +206,6 @@ class RTRCategorySerializer(serializers.ModelSerializer): return instance -''' -class OwnersSerializer(serializers.ModelSerializer): - class Meta: - model = Owners - fields = '__all__' -''' - - class ShowSerializer(serializers.HyperlinkedModelSerializer): owners = serializers.PrimaryKeyRelatedField(queryset=User.objects.all(),many=True) category = serializers.PrimaryKeyRelatedField(queryset=Category.objects.all(),many=True) @@ -360,11 +351,13 @@ class TimeSlotSerializer(serializers.ModelSerializer): class NoteSerializer(serializers.ModelSerializer): show = serializers.PrimaryKeyRelatedField(queryset=Show.objects.all()) timeslot = serializers.PrimaryKeyRelatedField(queryset=TimeSlot.objects.all()) + host = serializers.PrimaryKeyRelatedField(queryset=Host.objects.all()) class Meta: model = Note fields = '__all__' + def create(self, validated_data): """Create and return a new Note instance, given the validated data.""" @@ -388,6 +381,7 @@ class NoteSerializer(serializers.ModelSerializer): instance.content = validated_data.get('content', instance.content) instance.image = validated_data.get('image', instance.image) 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 = Note.get_audio_url(instance.cba_id) diff --git a/program/views.py b/program/views.py index 63754bc70f3bc552acb995808a13e905436d50aa..dfe7ee74bf1268a8e9f4f96d60113666164a9d30 100644 --- a/program/views.py +++ b/program/views.py @@ -228,11 +228,16 @@ def json_day_schedule(request, year=None, month=None, day=None): content_type="application/json; charset=utf-8") -def json_week_schedule(request): +def json_playout(request): """ - Called by calendar to get all timeslots for a week. - Expects GET variable 'start' (date), otherwise start will be today - Returns all timeslots of the next 7 days + Called by + - engine (playout) to retrieve timeslots within a given timerange + Expects GET variables 'start' (date) and 'end' (date). + If start not given, it will be today + + - internal calendar to retrieve all timeslots for a week + Expects GET variable 'start' (date), otherwise start will be today + If end not given, it returns all timeslots of the next 7 days """ if request.GET.get('start') == None: @@ -240,7 +245,14 @@ def json_week_schedule(request): else: start = datetime.combine( datetime.strptime(request.GET.get('start'), '%Y-%m-%d').date(), time(0, 0)) - timeslots = TimeSlot.objects.get_7d_timeslots(start).select_related('schedule').select_related('show') + if request.GET.get('end') == None: + # If no end was given, return the next week + timeslots = TimeSlot.objects.get_7d_timeslots(start).select_related('schedule').select_related('show') + else: + # Otherwise return the given timerange + end = datetime.combine( datetime.strptime(request.GET.get('end'), '%Y-%m-%d').date(), time(23, 59)) + timeslots = TimeSlot.objects.get_timerange_timeslots(start, end).select_related('schedule').select_related('show') + schedule = [] for ts in timeslots: @@ -268,8 +280,8 @@ def json_week_schedule(request): 'schedule_id': ts.schedule.id, 'is_repetition': ts.is_repetition, 'playlist_id': ts.playlist_id, - 'schedule_fallback_id': ts.schedule.fallback_playlist_id, # The schedule's fallback - 'show_fallback_id': ts.show.fallback_pool, # The show's fallback + 'schedule_fallback_id': ts.schedule.fallback_id, # The schedule's fallback + 'show_fallback_id': ts.show.fallback_id, # The show's fallback 'show_id': ts.show.id, 'show_name': ts.show.name + is_repetition, 'show_hosts': hosts, @@ -344,8 +356,8 @@ class APIUserViewSet(viewsets.ModelViewSet): """Constrain access to oneself except for superusers""" if self.request.user.is_superuser: return User.objects.all() - else: - return User.objects.filter(pk=self.request.user.id) + + return User.objects.filter(pk=self.request.user.id) def list(self, request): @@ -409,7 +421,9 @@ class APIUserViewSet(viewsets.ModelViewSet): class APIShowViewSet(viewsets.ModelViewSet): """ /api/v1/shows/ Returns shows a user owns (GET, POST) - /api/v1/shows/?active=true Returns all active shows (no matter if owned by user) (GET) + /api/v1/shows/?active=true Returns all active shows (GET) + /api/v1/shows/?host=1 Returns shows assigned to a given host (GET) + /api/v1/shows/?owner=1 Returns shows of a given owner (GET) /api/v1/shows/1 Used for retrieving a single show or update (if owned) (GET, PUT, DELETE) /api/v1/shows/1/notes Returns all notes to the show (GET) - POST not allowed at this level, use /shows/1/schedules/1/timeslots/1/note instead /api/v1/shows/1/notes/1 Returns the note of the show by its ID (GET) - PUT/DELETE not allowed at this level, use /shows/1/schedules/1/timeslots/1/note/1/ instead @@ -433,10 +447,12 @@ class APIShowViewSet(viewsets.ModelViewSet): def get_queryset(self): + shows = Show.objects.all() + if self.request.GET.get('active') == 'true': - '''Filter for retrieving currently running shows''' + '''Filter currently running shows''' - # Get currently running schedules to filter by + # 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 dstart? (=currently active, not just upcoming ones) # Add limit for future? @@ -444,17 +460,18 @@ class APIShowViewSet(viewsets.ModelViewSet): Q(rrule_id=1,dstart__gte=date.today()) ).distinct().values_list('show_id', flat=True) - return Show.objects.filter(id__in=schedules) + shows = Show.objects.filter(id__in=schedules) - return Show.objects.all() + if self.request.GET.get('owner') != None: + '''Filter shows by owner''' + shows = shows.filter(owners__in=[int(self.request.GET.get('owner'))]) + + if self.request.GET.get('host') != None: + '''Filter shows by host''' + shows = shows.filter(hosts__in=[int(self.request.GET.get('host'))]) + + return shows - ''' - def list(self, request): - """List shows""" - shows = self.get_queryset() - serializer = ShowSerializer(shows, many=True) - return Response(serializer.data) - ''' def create(self, request, pk=None): """ @@ -494,6 +511,9 @@ class APIShowViewSet(viewsets.ModelViewSet): 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.save(); return Response(serializer.data) @@ -567,7 +587,7 @@ class APIScheduleViewSet(viewsets.ModelViewSet): # Only allow creating when calling /shows/1/schedules/1 if show_pk == None or not request.user.is_superuser: - return Response(status=HTTP_401_UNAUTHORIZED) + return Response(status=status.HTTP_401_UNAUTHORIZED) return Response(status=HTTP_401_UNAUTHORIZED) @@ -642,24 +662,30 @@ class APITimeSlotViewSet(viewsets.ModelViewSet): # If show is given: Return corresponding timeslots if show_pk != None and schedule_pk != None: - # /shows/1/schedules/1/timeslots/ returns timeslots of the given schedule and show + # + # /shows/1/schedules/1/timeslots/ + # + # Returns timeslots of the given show and schedule + # return TimeSlot.objects.filter(show=show_pk, schedule=schedule_pk, start__gte=start, end__lte=end).order_by('start') + elif show_pk != None and schedule_pk == None: - # /shows/1/timeslots returns timeslots of the show + # + # /shows/1/timeslots/ + # + # Returns timeslots of the show + # return TimeSlot.objects.filter(show=show_pk, start__gte=start, end__lte=end).order_by('start') + else: - # Otherwise return all timeslots + # + # /timeslots/ + # + # Returns all timeslots + # return TimeSlot.objects.filter(start__gte=start, end__lte=end).order_by('start') - ''' - def list(self, request, show_pk=None, schedule_pk=None): - """Lists timeslots of a show""" - timeslots = self.get_queryset() - serializer = TimeSlotSerializer(timeslots, many=True) - return Response(serializer.data) - ''' - def retrieve(self, request, pk=None, schedule_pk=None, show_pk=None): if show_pk != None: @@ -685,7 +711,7 @@ class APITimeSlotViewSet(viewsets.ModelViewSet): 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 == None or show_pk==None or not Show.is_editable(self, timeslot.show_id): + if schedule_pk == None or show_pk == None or not Show.is_editable(self, timeslot.show_id): return Response(status=status.HTTP_401_UNAUTHORIZED) serializer = TimeSlotSerializer(timeslot, data=request.data) @@ -717,6 +743,8 @@ class APINoteViewSet(viewsets.ModelViewSet): /api/v1/notes/ Returns all notes (GET) - POST not allowed at this level /ap1/v1/notes/1 Returns a single note (if owned) (GET) - PUT/DELETE not allowed at this level /api/v1/notes/?ids=1,2,3,4,5 Returns given notes (if owned) (GET) + /api/v1/notes/?host=1 Returns notes assigned to a given host (GET) + /api/v1/notes/?owner=1 Returns notes created by a given user (GET) /api/v1/shows/1/notes Returns all notes of a show (GET) - POST not allowed at this level /api/v1/shows/1/notes/1 Returns a note by its ID (GET) - PUT/DELETE not allowed at this level /api/v1/shows/1/timeslots/1/note/ Returns a note of the timeslot (GET) - POST not allowed at this level @@ -740,28 +768,43 @@ class APINoteViewSet(viewsets.ModelViewSet): timeslot_pk = self.kwargs['timeslot_pk'] if 'timeslot_pk' in self.kwargs else None show_pk = self.kwargs['show_pk'] if 'show_pk' in self.kwargs else None - # TODO: Should users be able to edit other's notes if they're part of a show they own? + if show_pk != None and timeslot_pk != None: + # + # /shows/1/schedules/1/timeslots/1/note + # /shows/1/timeslots/1/note + # + # Return a note to the timeslot + # + notes = Note.objects.filter(show=show_pk, timeslot=timeslot_pk) + + elif show_pk != None and timeslot_pk == None: + # + # /shows/1/notes + # + # Returns notes to the show + # + notes = Note.objects.filter(show=show_pk) + + else: + # + # /notes + # + # Returns all notes + # + notes = Note.objects.all() + if self.request.GET.get('ids') != None: + '''Filter notes by their IDs''' note_ids = self.request.GET.get('ids').split(',') - if self.request.user.is_superuser: - notes = Note.objects.filter(id__in=note_ids) - else: - # Common users only retrieve notes they own - notes = Note.objects.filter(id__in=note_ids,user=self.request.user.id) - else: + notes = notes.filter(id__in=note_ids) + + if self.request.GET.get('host') != None: + '''Filter notes by host''' + notes = notes.filter(host=int(self.request.GET.get('host'))) - if show_pk != None and timeslot_pk != None: - # /shows/1/schedules/1/timeslots/1/note - # /shows/1/timeslots/1/note/ - # ...return notes to the timeslot - notes = Note.objects.filter(show=show_pk, timeslot=timeslot_pk) - elif show_pk != None and timeslot_pk == None: - # /shows/1/notes returns notes to the show - notes = Note.objects.filter(show=show_pk) - else: - # /notes returns all notes - notes = Note.objects.all() - #notes = Note.objects.filter(user=self.request.user.id) + if self.request.GET.get('owner') != None: + '''Filter notes by their creator''' + notes = notes.filter(user=int(self.request.GET.get('owner'))) return notes @@ -787,6 +830,11 @@ class APINoteViewSet(viewsets.ModelViewSet): serializer = NoteSerializer(data=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'] == None: + serializer.validated_data['host'] = None + serializer.save() return Response(serializer.data) @@ -805,14 +853,28 @@ class APINoteViewSet(viewsets.ModelViewSet): """ if show_pk != None and timeslot_pk == None and schedule_pk == None: - # /shows/1/notes/1 + # + # /shows/1/notes/1 + # + # Returns a note to a show + # note = get_object_or_404(Note, pk=pk, show=show_pk) + elif show_pk != None and timeslot_pk != None: - # /shows/1/timeslots/1/note/1 - # /shows/1/schedules/1/timeslots/1/note/1 + # + # /shows/1/timeslots/1/note/1 + # /shows/1/schedules/1/timeslots/1/note/1 + # + # Return a note to a timeslot + # note = get_object_or_404(Note, pk=pk, show=show_pk, timeslot=timeslot_pk) + else: - # /notes/1 + # + # /notes/1 + # + # Returns the given note + # note = get_object_or_404(Note, pk=pk) serializer = NoteSerializer(note) @@ -823,17 +885,22 @@ class APINoteViewSet(viewsets.ModelViewSet): # Allow PUT only when calling /shows/1/schedules/1/timeslots/1/note/1 if show_pk == None or schedule_pk == None or timeslot_pk == None: - return Response(status=status.HTTP_401_UNAUTHORIZED) + 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 their own notes - if not request.user.is_superuser and note.user_id != request.user.id: + # 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(): + + # 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'] != None: + serializer.validated_data['host'] = Host.objects.filter(pk=note.host_id)[0] + serializer.save(); return Response(serializer.data) @@ -843,13 +910,11 @@ class APINoteViewSet(viewsets.ModelViewSet): def destroy(self, request, pk=None): note = get_object_or_404(Note, pk=pk) - # Commons users may only delete their own notes - if not request.user.is_superuser and note.user_id != request.user.id: - return Response(status=status.HTTP_401_UNAUTHORIZED) - - Note.objects.delete(pk=pk) - return Response(status=status.HTTP_204_NO_CONTENT) + if Note.is_editable(self, note.id): + Note.objects.delete(pk=pk) + return Response(status=status.HTTP_204_NO_CONTENT) + return Response(status=status.HTTP_401_UNAUTHORIZED) class APICategoryViewSet(viewsets.ModelViewSet): @@ -937,16 +1002,4 @@ class APIHostViewSet(viewsets.ModelViewSet): queryset = Host.objects.all() serializer_class = HostSerializer permission_classes = [permissions.DjangoModelPermissionsOrAnonReadOnly] - required_scopes = ['hosts'] - - -''' -class APIOwnersViewSet(viewsets.ModelViewSet): - """ - """ - - queryset = Owners.objects.all() - serializer_class = OwnersSerializer - permission_classes = [permissions.DjangoModelPermissionsOrAnonReadOnly] - required_scopes = ['owners'] -''' \ No newline at end of file + required_scopes = ['hosts'] \ No newline at end of file diff --git a/pv/settings.py b/pv/settings.py index e9edf97ed45f8c453bc2b02426015d7a21895f5f..ec41cf7c08ecdbecda1d63f5dd0c57ea50ec5afe 100644 --- a/pv/settings.py +++ b/pv/settings.py @@ -8,6 +8,9 @@ PROJECT_DIR = os.path.dirname(__file__) DEBUG = True +# Must be set if DEBUG is False +ALLOWED_HOSTS = ['127.0.0.1', 'localhost'] + ADMINS = () MANAGERS = ADMINS @@ -27,6 +30,9 @@ DATABASE_ROUTERS = ['nop.dbrouter.NopRouter'] TIME_ZONE = 'Europe/Vienna' +# django-oidc-provider needs timezones in database +USE_TZ = True + LANGUAGE_CODE = 'de' SITE_ID = 1 @@ -86,8 +92,6 @@ REST_FRAMEWORK = { ], 'DEFAULT AUTHENTICATION_CLASSES': [ ], - #'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination', - #'PAGE_SIZE': 100 } INSTALLED_APPS = ( @@ -106,6 +110,7 @@ INSTALLED_APPS = ( 'rest_framework', 'rest_framework_nested', 'frapp', + 'oidc_provider', ) THUMBNAIL_SIZES = ['640x480', '200x200', '150x150'] diff --git a/pv/urls.py b/pv/urls.py index 0844b7df0d63f0ee84a4130d71cac03ae012e4aa..83285c0098dbf4e419a56bd7f397ab87fe0f9b5f 100644 --- a/pv/urls.py +++ b/pv/urls.py @@ -4,8 +4,9 @@ from django.contrib import admin from django.views.static import serve from rest_framework_nested import routers from rest_framework.authtoken import views +from oidc_provider import urls -from program.views import APIUserViewSet, APIHostViewSet, APIShowViewSet, APIScheduleViewSet, APITimeSlotViewSet, APINoteViewSet, APICategoryViewSet, APITypeViewSet, APITopicViewSet, APIMusicFocusViewSet, APIRTRCategoryViewSet, APILanguageViewSet, json_day_schedule, json_week_schedule, json_timeslots_specials +from program.views import APIUserViewSet, APIHostViewSet, APIShowViewSet, APIScheduleViewSet, APITimeSlotViewSet, APINoteViewSet, APICategoryViewSet, APITypeViewSet, APITopicViewSet, APIMusicFocusViewSet, APIRTRCategoryViewSet, APILanguageViewSet, json_day_schedule, json_playout, json_timeslots_specials admin.autodiscover() @@ -54,13 +55,14 @@ timeslot_router.register(r'note', APINoteViewSet, base_name='timeslots-note') urlpatterns = [ + url(r'^openid/', include('oidc_provider.urls', namespace='oidc_provider')), url(r'^api/v1/', include(router.urls) ), url(r'^api/v1/', include(show_router.urls)), url(r'^api/v1/', include(show_timeslot_router.urls)), url(r'^api/v1/', include(schedule_router.urls)), url(r'^api/v1/', include(timeslot_router.urls)), - url(r'^api/v1/playout', json_week_schedule), - url(r'^api/v1/program/week', json_week_schedule), + url(r'^api/v1/playout', json_playout), + url(r'^api/v1/program/week', json_playout), url(r'^api/v1/program/(?P<year>\d{4})/(?P<month>\d{1,2})/(?P<day>\d{1,2})/$', json_day_schedule), url(r'^admin/', admin.site.urls), url(r'^program/', include('program.urls')), diff --git a/requirements.txt b/requirements.txt index aa962397cfdb466b2608f6983d33b9a6e331c333..207448256a970634a1cc194abd45f825af692cc8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,4 +7,4 @@ python-dateutil==2.6.0 django-versatileimagefield==1.8.1 djangorestframework drf-nested-routers==0.90.0 -django-oauth-toolkit \ No newline at end of file +django-oidc-provider==0.5.2 \ No newline at end of file