From a6af6efe636bd878180d70dea3bdafcdd9cda498 Mon Sep 17 00:00:00 2001 From: Ernesto Rico Schmidt <ernesto@helsinki.at> Date: Fri, 20 Nov 2020 19:49:25 -0400 Subject: [PATCH] Clean-up code - Make `Host.is_editable`, `Show.is_editable`, `Note.is_editable`, and `Note.get_audio_url` static methods, - Make `Schedule.instantiate_upcoming`, `Schedule.generate_timeslots`, `Schedule.get_collisions`, `Schedule.generate_conflicts`, `Schedule.make_conflicts` and `Schedule.resolve_conflicts` static methods. Most of these methods do not belong here. - Fix signatures of methods for subclasses of `viewsets.ModelViewSet` - Fix doctrins for subclasses of `viewsets.ModelViewSet` - Fix comparisons with None (replace '==' and '!=' with 'is' and 'is not') --- program/models.py | 230 +++++++++++++---------- program/serializers.py | 54 ++---- program/views.py | 404 ++++++++++++++++++++++------------------- 3 files changed, 368 insertions(+), 320 deletions(-) diff --git a/program/models.py b/program/models.py index b9a0b3b5..322d796b 100644 --- a/program/models.py +++ b/program/models.py @@ -1,4 +1,6 @@ +import json from datetime import date, datetime, time, timedelta +from urllib.request import urlopen from dateutil.relativedelta import relativedelta from dateutil.rrule import rrule @@ -249,7 +251,8 @@ class Host(models.Model): is_active = models.BooleanField(_("Is active?"), default=True) email = models.EmailField(_("E-Mail"), blank=True) website = models.URLField(_("Website"), blank=True, help_text=_("URL to your personal website.")) - biography = tinymce_models.HTMLField(_("Biography"), blank=True, null=True, help_text=_("Describe yourself and your fields of interest in a few sentences.")) + biography = tinymce_models.HTMLField(_("Biography"), blank=True, null=True, + help_text=_("Describe yourself and your fields of interest in a few sentences.")) googleplus_url = models.URLField(_("Google+ URL"), blank=True, help_text=_("URL to your Google+ profile.")) facebook_url = models.URLField(_("Facebook URL"), blank=True, help_text=_("URL to your Facebook profile.")) twitter_url = models.URLField(_("Twitter URL"), blank=True, help_text=_("URL to your Twitter profile.")) @@ -260,7 +263,10 @@ class Host(models.Model): ppoi = PPOIField('Image PPOI') height = models.PositiveIntegerField('Image Height', blank=True, null=True, editable=False) width = models.PositiveIntegerField('Image Width', blank=True, null=True, editable=False) - image = VersatileImageField(_("Profile picture"), blank=True, null=True, upload_to='host_images', width_field='width', height_field='height', ppoi_field='ppoi', 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.")) + image = VersatileImageField(_("Profile picture"), blank=True, null=True, upload_to='host_images', width_field='width', + height_field='height', ppoi_field='ppoi', 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.")) class Meta: ordering = ('name',) @@ -276,34 +282,38 @@ class Host(models.Model): def active_shows(self): return self.shows.filter(schedules__until__gt=timezone.now()) - def is_editable(self, host_id): + def save(self, *args, **kwargs): + super(Host, self).save(*args, **kwargs) + + # Generate thumbnails + 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(host_view_set, host_id): """ Whether the given host is assigned to a show the current user owns @return boolean """ - if self.request.user.is_superuser: + if host_view_set.request.user.is_superuser: return True - host_ids = Host.objects.filter(shows__in=self.request.user.shows.all()).distinct().values_list('id', flat=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 - def save(self, *args, **kwargs): - super(Host, self).save(*args, **kwargs) - - # Generate thumbnails - if self.image.name and THUMBNAIL_SIZES: - for size in THUMBNAIL_SIZES: - thumbnail = self.image.crop[size].name - class Show(models.Model): - predecessor = models.ForeignKey('self', blank=True, null=True, on_delete=models.CASCADE, related_name='successors', verbose_name=_("Predecessor")) + predecessor = models.ForeignKey('self', blank=True, null=True, on_delete=models.CASCADE, related_name='successors', + verbose_name=_("Predecessor")) hosts = models.ManyToManyField(Host, blank=True, related_name='shows', verbose_name=_("Hosts")) owners = models.ManyToManyField(User, blank=True, related_name='shows', verbose_name=_("Owners")) language = models.ManyToManyField(Language, blank=True, related_name='language', verbose_name=_("Language")) type = models.ForeignKey(Type, on_delete=models.CASCADE, related_name='shows', verbose_name=_("Type")) category = models.ManyToManyField(Category, blank=True, related_name='shows', verbose_name=_("Category")) - fundingcategory = models.ForeignKey(FundingCategory, null=True, on_delete=models.CASCADE, blank=True, related_name='shows', verbose_name=_("Funding Category")) + fundingcategory = models.ForeignKey(FundingCategory, null=True, on_delete=models.CASCADE, blank=True, related_name='shows', + verbose_name=_("Funding Category")) topic = models.ManyToManyField(Topic, blank=True, related_name='shows', verbose_name=_("Topic")) musicfocus = models.ManyToManyField(MusicFocus, blank=True, related_name='shows', verbose_name=_("Music focus")) name = models.CharField(_("Name"), max_length=255, help_text=_("The show's name. Avoid a subtitle.")) @@ -311,18 +321,30 @@ class Show(models.Model): ppoi = PPOIField('Image PPOI') height = models.PositiveIntegerField('Image Height', blank=True, null=True, editable=False) width = models.PositiveIntegerField('Image Width', blank=True, null=True, editable=False) - image = VersatileImageField(_("Image"), blank=True, null=True, upload_to='show_images', width_field='width', height_field='height', ppoi_field='ppoi', 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.")) - logo = models.ImageField(_("Logo"), blank=True, null=True, upload_to='show_images', help_text=_("Upload a logo of your show.")) - short_description = models.TextField(_("Short description"), 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.")) - description = tinymce_models.HTMLField(_("Description"), blank=True, null=True, help_text=_("Describe your show in detail.")) + image = VersatileImageField(_("Image"), blank=True, null=True, upload_to='show_images', width_field='width', + height_field='height', ppoi_field='ppoi', 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.")) + logo = models.ImageField(_("Logo"), blank=True, null=True, upload_to='show_images', + help_text=_("Upload a logo of your show.")) + short_description = models.TextField(_("Short description"), 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.")) + description = tinymce_models.HTMLField(_("Description"), blank=True, null=True, + help_text=_("Describe your show in detail.")) 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 show's ID under https://cba.fro.at/series")) - fallback_id = models.IntegerField(_("Fallback ID"), blank=True, null=True, help_text=_("If a timeslot of your show is empty, this playlist will be aired as a backup.")) + 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 show's ID under https://cba.fro.at/series")) + fallback_id = models.IntegerField(_("Fallback ID"), blank=True, null=True, help_text=_( + "If a timeslot of your show is empty, this playlist will be aired as a backup.")) created = models.DateTimeField(auto_now_add=True, editable=False) last_updated = models.DateTimeField(auto_now=True, editable=False) is_active = models.BooleanField(_("Is active?"), default=True) - is_public = models.BooleanField(_("Is Public?"), default=False, help_text=_("Files and Playlists of Public Shows can only be changed by owners but may be used by everyone.")) + is_public = models.BooleanField(_("Is Public?"), default=False, help_text=_( + "Files and Playlists of Public Shows can only be changed by owners but may be used by everyone.")) class Meta: ordering = ('slug',) @@ -331,7 +353,7 @@ class Show(models.Model): def __str__(self): if self.id is None: - return '%s' % (self.name) + return '%s' % self.name return '%04d | %s' % (self.id, self.name) @@ -342,20 +364,21 @@ class Show(models.Model): def active_schedules(self): return self.schedules.filter(until__gt=date.today()) - def is_editable(self, show_id): + # 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 self.request.user.is_superuser: + if show_view_set.request.user.is_superuser: return True - show_ids = self.request.user.shows.all().values_list('id', flat=True) + show_ids = show_view_set.request.user.shows.all().values_list('id', flat=True) return int(show_id) in show_ids class RRule(models.Model): - FREQ_CHOICES = ( (1, _("Monthly")), (2, _("Weekly")), @@ -373,8 +396,7 @@ class RRule(models.Model): name = models.CharField(_("Name"), max_length=32, unique=True) freq = models.IntegerField(_("Frequency"), choices=FREQ_CHOICES) interval = models.IntegerField(_("Interval"), default=1) - bysetpos = models.IntegerField(_("Set position"), blank=True, - choices=BYSETPOS_CHOICES, null=True) + bysetpos = models.IntegerField(_("Set position"), blank=True, choices=BYSETPOS_CHOICES, null=True) count = models.IntegerField(_("Count"), blank=True, null=True) class Meta: @@ -408,8 +430,11 @@ class Schedule(models.Model): add_days_no = models.IntegerField(_("Add days"), blank=True, null=True) add_business_days_only = models.BooleanField(_("Only add business days?"), default=False) fallback_id = models.IntegerField(_("Fallback ID"), blank=True, null=True) - automation_id = models.IntegerField(_("Automation ID"), blank=True, null=True, choices=get_automation_id_choices()) # Deprecated - created = models.DateTimeField(auto_now_add=True, editable=False, null=True) # -> both see https://stackoverflow.com/questions/1737017/django-auto-now-and-auto-now-add + automation_id = models.IntegerField(_("Automation ID"), blank=True, null=True, + choices=get_automation_id_choices()) # Deprecated + # -> both see https://stackoverflow.com/questions/1737017/django-auto-now-and-auto-now-add + created = models.DateTimeField(auto_now_add=True, editable=False, + null=True) last_updated = models.DateTimeField(auto_now=True, editable=False, null=True) class Meta: @@ -430,6 +455,11 @@ class Schedule(models.Model): else: return '%s, %s, %s - %s' % (weekday, self.rrule, tstart, tend) + def save(self, *args, **kwargs): + super(Schedule, self).save(*args, **kwargs) + + # FIXME: this does not belong here + @staticmethod def instantiate_upcoming(sdl, show_pk, pk=None): """Returns an upcoming schedule instance for conflict resolution""" @@ -475,6 +505,8 @@ class Schedule(models.Model): return schedule + # FIXME: this does not belong here + @staticmethod def generate_timeslots(schedule): """ Returns a list of timeslot objects based on a schedule and its rrule @@ -591,8 +623,8 @@ class Schedule(models.Model): 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) @@ -604,6 +636,8 @@ class Schedule(models.Model): return timeslots + # FIXME: this does not belong here + @staticmethod def get_collisions(timeslots): """ Tests a list of timeslot objects for colliding timeslots in the database @@ -616,11 +650,11 @@ class Schedule(models.Model): for ts in timeslots: collision = TimeSlot.objects.filter( - (Q(start__lt=ts.end) & Q(end__gte=ts.end)) | - (Q(end__gt=ts.start) & Q(end__lte=ts.end)) | - (Q(start__gte=ts.start) & Q(end__lte=ts.end)) | - (Q(start__lte=ts.start) & Q(end__gte=ts.end)) - ) + (Q(start__lt=ts.end) & Q(end__gte=ts.end)) | + (Q(end__gt=ts.start) & Q(end__lte=ts.end)) | + (Q(start__gte=ts.start) & Q(end__lte=ts.end)) | + (Q(start__lte=ts.start) & Q(end__gte=ts.end)) + ) if collision: collisions.append(collision[0]) # TODO: Do we really always retrieve one? @@ -629,6 +663,8 @@ class Schedule(models.Model): return collisions + # FIXME: this does not belong here + @staticmethod def generate_conflicts(timeslots): """ Tests a list of timeslot objects for colliding timeslots in the database @@ -653,31 +689,20 @@ class Schedule(models.Model): # Get collisions for each timeslot collision_list = list(TimeSlot.objects.filter( - (Q(start__lt=ts.end) & Q(end__gte=ts.end)) | - (Q(end__gt=ts.start) & Q(end__lte=ts.end)) | - (Q(start__gte=ts.start) & Q(end__lte=ts.end)) | - (Q(start__lte=ts.start) & Q(end__gte=ts.end)) - ).order_by('start')) + (Q(start__lt=ts.end) & Q(end__gte=ts.end)) | + (Q(end__gt=ts.start) & Q(end__lte=ts.end)) | + (Q(start__gte=ts.start) & Q(end__lte=ts.end)) | + (Q(start__lte=ts.start) & Q(end__gte=ts.end)) + ).order_by('start')) # Add the projected timeslot - projected_entry = {} - projected_entry['hash'] = ts.hash - projected_entry['start'] = str(ts.start) - projected_entry['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 = {} - collision['id'] = c.id - collision['start'] = str(c.start) - collision['end'] = str(c.end) - collision['playlist_id'] = c.playlist_id - collision['show'] = c.show.id - collision['show_name'] = c.show.name - collision['is_repetition'] = c.is_repetition - collision['schedule'] = c.schedule_id - collision['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: @@ -709,7 +734,7 @@ class Schedule(models.Model): # | | # +--+ # - if ts.start < c.start and ts.end > c.start and ts.end <= c.end: + if ts.end > c.start > ts.start <= c.end: solution_choices.add('theirs-end') solution_choices.add('ours-end') @@ -723,7 +748,7 @@ class Schedule(models.Model): # | | # +--+ # - if ts.start >= c.start and ts.start < c.end and ts.end > c.end: + if c.start <= ts.start < c.end < ts.end: solution_choices.add('theirs-start') solution_choices.add('ours-start') @@ -769,6 +794,8 @@ class Schedule(models.Model): return conflicts + # FIXME: this does not belong here + @staticmethod def make_conflicts(sdl, schedule_pk, show_pk): """ Retrieves POST vars @@ -801,6 +828,8 @@ class Schedule(models.Model): return conflicts + # FIXME: this does not belong here + @staticmethod def resolve_conflicts(data, schedule_pk, show_pk): """ Resolves conflicts @@ -1017,10 +1046,8 @@ class Schedule(models.Model): # If 'dryrun' is true, just return the projected changes instead of executing them if 'dryrun' in sdl and sdl['dryrun']: - output = {} - output['create'] = [model_to_dict(ts) for ts in create] - output['update'] = [model_to_dict(ts) for ts in update] - output['delete'] = [model_to_dict(ts) for ts in delete] + 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''' @@ -1063,9 +1090,6 @@ class Schedule(models.Model): return model_to_dict(schedule) - def save(self, *args, **kwargs): - super(Schedule, self).save(*args, **kwargs) - class TimeSlotManager(models.Manager): @staticmethod @@ -1138,7 +1162,9 @@ class TimeSlotManager(models.Manager): class TimeSlot(models.Model): schedule = models.ForeignKey(Schedule, on_delete=models.CASCADE, 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) + 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) end = models.DateTimeField(_("End time")) show = models.ForeignKey(Show, editable=False, on_delete=models.CASCADE, related_name='timeslots') memo = models.TextField(_("Memo"), blank=True) @@ -1165,7 +1191,7 @@ class TimeSlot(models.Model): super(TimeSlot, self).save(*args, **kwargs) return self - def generate(self, **kwargs): + def generate(self): """Returns the object instance without saving""" self.show = self.schedule.show @@ -1187,18 +1213,28 @@ class Note(models.Model): (2, _("Repetition")), ) timeslot = models.OneToOneField(TimeSlot, on_delete=models.CASCADE, verbose_name=_("Time slot"), unique=True) - title = models.CharField(_("Title"), max_length=128, 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.")) + title = models.CharField(_("Title"), max_length=128, 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.")) slug = models.SlugField(_("Slug"), max_length=32, unique=True, help_text=_("A simple to read URL for your show.")) - summary = models.TextField(_("Summary"), 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.")) + summary = models.TextField(_("Summary"), 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.")) content = tinymce_models.HTMLField(_("Content"), help_text=_("Describe your upcoming show in detail.")) ppoi = PPOIField('Image PPOI') height = models.PositiveIntegerField('Image Height', blank=True, null=True, editable=False) width = models.PositiveIntegerField('Image Width', blank=True, null=True, editable=False) - image = VersatileImageField(_("Featured image"), blank=True, null=True, upload_to='note_images', width_field='width', height_field='height', ppoi_field='ppoi', 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.")) + image = VersatileImageField(_("Featured image"), blank=True, null=True, upload_to='note_images', width_field='width', + height_field='height', ppoi_field='ppoi', 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.")) status = models.IntegerField(_("Status"), choices=STATUS_CHOICES, default=1) start = models.DateTimeField(editable=False) show = models.ForeignKey(Show, on_delete=models.CASCADE, related_name='notes', editable=True) - cba_id = models.IntegerField(_("CBA ID"), blank=True, null=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)")) + cba_id = models.IntegerField(_("CBA ID"), blank=True, null=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)")) audio_url = models.TextField(_("Direct URL to a linked audio file"), blank=True, editable=False) created = models.DateTimeField(auto_now_add=True, editable=False) last_updated = models.DateTimeField(auto_now=True, editable=False) @@ -1213,19 +1249,38 @@ class Note(models.Model): def __str__(self): return '%s - %s' % (self.title, self.timeslot) - def is_editable(self, note_id): + def save(self, *args, **kwargs): + self.start = self.timeslot.start + self.show = self.timeslot.schedule.show + + timeslot = TimeSlot.objects.get(pk=self.timeslot.id) + timeslot.note_id = self.id + timeslot.save() + + super(Note, self).save(*args, **kwargs) + + # Generate thumbnails + 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 self.request.user.is_superuser: + if note_view_set.request.user.is_superuser: return True note = Note.objects.get(pk=note_id) - return int(note.show_id) in self.request.user.shows.all().values_list('id', flat=True) + return int(note.show_id) in note_view_set.request.user.shows.all().values_list('id', flat=True) + # FIXME: this does not belong here + @staticmethod def get_audio_url(cba_id): """ Retrieve the direct URL to the mp3 in CBA @@ -1241,31 +1296,14 @@ class Note(models.Model): audio_url = '' if cba_id is not None and cba_id != '' and CBA_API_KEY != '': - from urllib.request import urlopen - import json - url = CBA_AJAX_URL + '?action=cba_ajax_get_filename&post_id=' + str(cba_id) + '&api_key=' + CBA_API_KEY # For momentary testing without being whitelisted - TODO: delete the line - 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' with urlopen(url) as conn: audio_url_json = conn.read().decode('utf-8-sig') audio_url = json.loads(audio_url_json) return audio_url - - def save(self, *args, **kwargs): - self.start = self.timeslot.start - self.show = self.timeslot.schedule.show - - timeslot = TimeSlot.objects.get(pk=self.timeslot.id) - timeslot.note_id = self.id - timeslot.save() - - super(Note, self).save(*args, **kwargs) - - # Generate thumbnails - if self.image.name and THUMBNAIL_SIZES: - for size in THUMBNAIL_SIZES: - thumbnail = self.image.crop[size].name diff --git a/program/serializers.py b/program/serializers.py index 897b5427..1eb947bc 100644 --- a/program/serializers.py +++ b/program/serializers.py @@ -18,7 +18,6 @@ class UserSerializer(serializers.ModelSerializer): model = User fields = '__all__' - def create(self, validated_data): """ Create and return a new User instance, given the validated data. @@ -31,12 +30,12 @@ class UserSerializer(serializers.ModelSerializer): user.set_password(validated_data['password']) user.save() - 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 - def update(self, instance, validated_data): """ Update and return an existing User instance, given the validated data. @@ -69,13 +68,11 @@ class UserSerializer(serializers.ModelSerializer): return instance - class CategorySerializer(serializers.ModelSerializer): class Meta: model = Category fields = '__all__' - def update(self, instance, validated_data): """ Update and return an existing Category instance, given the validated data. @@ -92,9 +89,10 @@ class CategorySerializer(serializers.ModelSerializer): class HostSerializer(serializers.ModelSerializer): - thumbnails = serializers.SerializerMethodField() # Read-only + thumbnails = serializers.SerializerMethodField() # Read-only - def get_thumbnails(self, host): + @staticmethod + def get_thumbnails(host): """Returns thumbnails""" thumbnails = [] @@ -108,7 +106,6 @@ class HostSerializer(serializers.ModelSerializer): model = Host fields = '__all__' - def update(self, instance, validated_data): """ Update and return an existing Host instance, given the validated data. @@ -138,7 +135,6 @@ class LanguageSerializer(serializers.ModelSerializer): model = Language fields = '__all__' - def update(self, instance, validated_data): """ Update and return an existing Language instance, given the validated data. @@ -155,7 +151,6 @@ class TopicSerializer(serializers.ModelSerializer): model = Topic fields = '__all__' - def update(self, instance, validated_data): """ Update and return an existing Topic instance, given the validated data. @@ -174,7 +169,6 @@ class MusicFocusSerializer(serializers.ModelSerializer): model = MusicFocus fields = '__all__' - def update(self, instance, validated_data): """ Update and return an existing MusicFocus instance, given the validated data. @@ -193,7 +187,6 @@ class TypeSerializer(serializers.ModelSerializer): model = Type fields = '__all__' - def update(self, instance, validated_data): """ Update and return an existing Type instance, given the validated data. @@ -213,7 +206,6 @@ class FundingCategorySerializer(serializers.ModelSerializer): model = FundingCategory fields = '__all__' - def update(self, instance, validated_data): """ Update and return an existing FundingCategory instance, given the validated data. @@ -228,18 +220,19 @@ class FundingCategorySerializer(serializers.ModelSerializer): class ShowSerializer(serializers.HyperlinkedModelSerializer): - owners = serializers.PrimaryKeyRelatedField(queryset=User.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) - topic = serializers.PrimaryKeyRelatedField(queryset=Topic.objects.all(),many=True) - musicfocus = serializers.PrimaryKeyRelatedField(queryset=MusicFocus.objects.all(),many=True) + owners = serializers.PrimaryKeyRelatedField(queryset=User.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) + topic = serializers.PrimaryKeyRelatedField(queryset=Topic.objects.all(), many=True) + musicfocus = serializers.PrimaryKeyRelatedField(queryset=MusicFocus.objects.all(), many=True) type = serializers.PrimaryKeyRelatedField(queryset=Type.objects.all()) fundingcategory = serializers.PrimaryKeyRelatedField(queryset=FundingCategory.objects.all()) - predecessor = serializers.PrimaryKeyRelatedField(queryset=Show.objects.all(),required=False,allow_null=True) - thumbnails = serializers.SerializerMethodField() # Read-only + predecessor = serializers.PrimaryKeyRelatedField(queryset=Show.objects.all(), required=False, allow_null=True) + thumbnails = serializers.SerializerMethodField() # Read-only - def get_thumbnails(self, show): + @staticmethod + def get_thumbnails(show): """Returns thumbnails""" thumbnails = [] @@ -249,7 +242,6 @@ class ShowSerializer(serializers.HyperlinkedModelSerializer): return thumbnails - class Meta: model = Show fields = ('id', 'name', 'slug', 'image', 'ppoi', 'logo', 'short_description', 'description', @@ -257,7 +249,6 @@ class ShowSerializer(serializers.HyperlinkedModelSerializer): 'predecessor', 'cba_series_id', 'fallback_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. @@ -283,7 +274,6 @@ class ShowSerializer(serializers.HyperlinkedModelSerializer): show.save() return show - def update(self, instance, validated_data): """ Update and return an existing Show instance, given the validated data. @@ -342,7 +332,6 @@ class ScheduleSerializer(serializers.ModelSerializer): schedule.save() return schedule - def update(self, instance, validated_data): """Update and return an existing Schedule instance, given the validated data.""" @@ -375,7 +364,6 @@ class TimeSlotSerializer(serializers.ModelSerializer): """Create and return a new TimeSlot instance, given the validated data.""" return TimeSlot.objects.create(**validated_data) - def update(self, instance, validated_data): """Update and return an existing Show instance, given the validated data.""" @@ -391,9 +379,10 @@ 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()) - thumbnails = serializers.SerializerMethodField() # Read-only + thumbnails = serializers.SerializerMethodField() # Read-only - def get_thumbnails(self, note): + @staticmethod + def get_thumbnails(note): """Returns thumbnails""" thumbnails = [] @@ -403,12 +392,10 @@ class NoteSerializer(serializers.ModelSerializer): return thumbnails - class Meta: model = Note fields = '__all__' - def create(self, validated_data): """Create and return a new Note instance, given the validated data.""" @@ -421,7 +408,7 @@ class NoteSerializer(serializers.ModelSerializer): note = Note.objects.create(**validated_data) # Assign note to timeslot - if note.timeslot_id != None: + if note.timeslot_id is not None: try: timeslot = TimeSlot.objects.get(pk=note.timeslot_id) timeslot.note_id = note.id @@ -431,7 +418,6 @@ class NoteSerializer(serializers.ModelSerializer): return note - def update(self, instance, validated_data): """Update and return an existing Note instance, given the validated data.""" @@ -457,7 +443,7 @@ class NoteSerializer(serializers.ModelSerializer): ts.save(update_fields=["note_id"]) # Assign note to timeslot - if instance.timeslot.id != None: + if instance.timeslot.id is not None: try: timeslot = TimeSlot.objects.get(pk=instance.timeslot.id) timeslot.note_id = instance.id diff --git a/program/views.py b/program/views.py index ad24aad2..ea9158ce 100644 --- a/program/views.py +++ b/program/views.py @@ -16,9 +16,9 @@ from program.serializers import TypeSerializer, LanguageSerializer, MusicFocusSe ScheduleSerializer, CategorySerializer, FundingCategorySerializer, TopicSerializer, TimeSlotSerializer, HostSerializer, \ UserSerializer from program.utils import get_cached_shows +from pv.settings import STATION_FALLBACK_ID -# Deprecated def json_day_schedule(request, year=None, month=None, day=None): if year is None and month is None and day is None: today = datetime.combine(date.today(), time(0, 0)) @@ -57,14 +57,12 @@ def json_playout(request): If end not given, it returns all timeslots of the next 7 days """ - from pv.settings import STATION_FALLBACK_ID - - if request.GET.get('start') == None: + if request.GET.get('start') is None: start = datetime.combine(date.today(), time(0, 0)) else: start = datetime.combine(datetime.strptime(request.GET.get('start'), '%Y-%m-%d').date(), time(0, 0)) - if request.GET.get('end') == 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') else: @@ -87,7 +85,7 @@ def json_playout(request): fundingcategory = FundingCategory.objects.get(pk=ts.show.fundingcategory_id) fdcategory = fundingcategory.fundingcategory - type = Type.objects.get(pk=ts.show.type_id) + type_ = Type.objects.get(pk=ts.show.type_id) classname = 'default' @@ -108,7 +106,7 @@ def json_playout(request): 'show_id': ts.show.id, 'show_name': ts.show.name + is_repetition, 'show_hosts': hosts, - 'show_type': type.type, + 'show_type': type_.type, 'show_categories': categories, 'show_topics': topics, 'show_musicfocus': musicfocus, @@ -128,7 +126,6 @@ def json_playout(request): content_type="application/json; charset=utf-8") -# Deprecated def json_timeslots_specials(request): specials = {} shows = get_cached_shows()['shows'] @@ -138,7 +135,7 @@ def json_timeslots_specials(request): specials[show['id']] = show for ts in TimeSlot.objects.filter(end__gt=timezone.now(), - schedule__automation_id__in=specials.iterkeys()).select_related('show'): + schedule__automation_id__in=specials.keys()).select_related('show'): automation_id = ts.schedule.automation_id start = ts.start.strftime('%Y-%m-%d_%H:%M:%S') end = ts.end.strftime('%Y-%m-%d_%H:%M:%S') @@ -155,17 +152,12 @@ def json_timeslots_specials(request): content_type="application/json; charset=utf-8") -#################################################################### -# REST API View Sets -#################################################################### - - class APIUserViewSet(viewsets.ModelViewSet): """ - /api/v1/users Returns oneself - Superusers see all users. Only superusers may create a user (GET, POST) - /api/v1/users/1 Used for retrieving or updating a single user. Non-superusers may only update certain fields. (GET, PUT) - DELETE is prohibited for everyone + `/users` returns oneself. Superusers see all users. Only superusers may create a user (GET, POST) + `/users/{user_id}` retrieves or updates a single user. Non-superusers may only update certain fields (GET, PUT) - Superusers may access and update all users + Superusers may access and update all users. """ permission_classes = [permissions.DjangoModelPermissionsOrAnonReadOnly] @@ -179,13 +171,14 @@ class APIUserViewSet(viewsets.ModelViewSet): return User.objects.filter(pk=self.request.user.id) - def list(self, request): + def list(self, request, *args, **kwargs): users = self.get_queryset() serializer = UserSerializer(users, many=True) return Response(serializer.data) - def retrieve(self, request, pk=None): + def retrieve(self, request, *args, **kwargs): """Returns a single user""" + pk = kwargs.get('pk') # Common users only see themselves if not request.user.is_superuser and int(pk) != request.user.id: @@ -195,7 +188,7 @@ class APIUserViewSet(viewsets.ModelViewSet): serializer = UserSerializer(user) return Response(serializer.data) - def create(self, request, pk=None): + def create(self, request, *args, **kwargs): """ Create a User Only superusers may create a user @@ -212,7 +205,8 @@ class APIUserViewSet(viewsets.ModelViewSet): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - def update(self, request, pk=None): + def update(self, request, *args, **kwargs): + pk = kwargs.get('pk') serializer = UserSerializer(data=request.data) # Common users may only edit themselves @@ -223,35 +217,35 @@ class APIUserViewSet(viewsets.ModelViewSet): serializer = UserSerializer(user, data=request.data, context={'user': request.user}) if serializer.is_valid(): - serializer.save(); + serializer.save() return Response(serializer.data) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - def destroy(self, request, pk=None): + 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): """ - /api/v1/shows/ Returns all shows (GET, POST) - /api/v1/shows/?active=true Returns all active shows (= currently running) (GET) - /api/v1/shows/?active=false Returns all inactive shows (= past or upcoming) (GET) - /api/v1/shows/?public=true Returns all public shows (GET) - /api/v1/shows/?public=false Returns all non-public 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 is not allowed via API. Set is_active to False instead. - /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 - /api/v1/shows/1/schedules Returns all schedules of the show (GET, POST) - /api/v1/shows/1/schedules/1 Returns the schedule of the show by its ID (GET) - POST not allowed at this level, use /shows/1/schedules/ instead - /api/v1/shows/1/timeslots Returns all timeslots of the show (GET) - Timeslots may only be added by creating/updating a schedule - /api/v1/shows/1/timeslots/1 Returns the timeslot of the show (GET) - Timeslots may only be added by creating/updating a schedule - /api/v1/shows/1/timeslots?start=2017-01-01&end=2017-12-31 Returns all timeslots of the show within the given timerange (GET) - /api/v1/shows/1/timeslots/1/note Returns a note to the timeslot (one at max) (GET) - POST not allowed at this level, use /shows/1/schedules/1/timelots/1/note/ instead - /api/v1/shows/1/timeslots/1/note/1 Returns the note of the show's timeslot by its ID (GET) - PUT/DELETE not allowed at this level, use /shows/1/schedules/1/timeslots/1/note/1/ instead + `/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_id}` returns shows assigned to a given host (GET) + `/shows/?owner={owner_id}` returns shows of a given owner (GET) + `/shows/{show_id}` retrieves or updates (if owned) a single show (GET, PUT). + `/shows/{show_id}/notes` returns all notes to the show (GET) + `/shows/{show_id}/notes/{note_id}` returns the note of the show by its ID (GET) + `/shows/{show_id}/schedules` returns all schedules of the show (GET, POST) + `/shows/{show_id}/schedules/{schedule_id}` returns the schedule of the show by its ID (GET) + `/shows/{show_id}/timeslots` returns all timeslots of the show (GET) + `/shows/{show_id}/timeslots/{timeslot_id}` returns the timeslot of the show (GET) + `/shows/{show_id}/timeslots?start={start_date}&end={end_date}` returns all timeslots of the show within the time range (GET) + `/shows/{show_id}/timeslots/{timeslot_id}/note` returns a note to the timeslot (one at max) (GET) + `/shows/{show_id}/timeslots/{timeslot_id}/note/1` returns the note of the show's timeslot by its ID (GET) Only superusers may add and delete shows """ @@ -262,20 +256,20 @@ class APIShowViewSet(viewsets.ModelViewSet): pagination_class = LimitOffsetPagination def get_queryset(self): - shows = Show.objects.all() - '''Filters''' - + # Filters if self.request.GET.get('active') == 'true' or self.request.GET.get('active') == 'false': - '''Filter currently running shows''' - + # 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 dstart? (=currently active, not just upcoming ones) # Add limit for future? - show_ids = Schedule.objects.filter(Q(rrule_id__gt=1, dstart__lte=date.today(), until__gte=date.today()) | - Q(rrule_id=1, dstart__gte=date.today()) + show_ids = Schedule.objects.filter(Q(rrule_id__gt=1, + dstart__lte=date.today(), + until__gte=date.today()) | + Q(rrule_id=1, + dstart__gte=date.today()) ).distinct().values_list('show_id', flat=True) # Filter active shows based on timeslots as well as on the is_active flag @@ -283,28 +277,28 @@ class APIShowViewSet(viewsets.ModelViewSet): shows = Show.objects.filter(id__in=show_ids, is_active=True) if self.request.GET.get('active') == 'false': - '''Return all shows except those which are running''' + # Return all shows except those which are running shows = Show.objects.exclude(id__in=show_ids, is_active=True) if self.request.GET.get('public') == 'true': - '''Return all public shows''' + # Return all public shows shows = shows.filter(is_public=True) if self.request.GET.get('public') == 'false': - '''Return all public shows''' + # Return all public shows shows = shows.filter(is_public=False) - if self.request.GET.get('owner') != None: - '''Filter shows by owner''' + if self.request.GET.get('owner') is not 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''' + if self.request.GET.get('host') is not None: + # Filter shows by host shows = shows.filter(hosts__in=[int(self.request.GET.get('host'))]) return shows - def create(self, request, pk=None): + def create(self, request, *args, **kwargs): """ Create a show Only superusers may create a show @@ -321,19 +315,24 @@ class APIShowViewSet(viewsets.ModelViewSet): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - def retrieve(self, request, pk=None): + def retrieve(self, request, *args, **kwargs): """Returns a single show""" + + pk = kwargs.get('pk') + show = get_object_or_404(Show, pk=pk) serializer = ShowSerializer(show) return Response(serializer.data) - def update(self, request, pk=None): + def update(self, request, *args, **kwargs): """ Update a show Common users may only update shows they own """ + pk = kwargs.get('pk') + if not Show.is_editable(self, pk): return Response(status=status.HTTP_401_UNAUTHORIZED) @@ -344,36 +343,35 @@ class APIShowViewSet(viewsets.ModelViewSet): # Common users mustn't edit the show's name if not request.user.is_superuser: serializer.validated_data['name'] = show.name - serializer.save(); + serializer.save() return Response(serializer.data) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - def destroy(self, request, pk=None): + def destroy(self, request, *args, **kwargs): """ Delete a show Only superusers may delete shows """ - """ - if not request.user.is_superuser: - return Response(status=status.HTTP_401_UNAUTHORIZED) - - show = get_object_or_404(Show, pk=pk) - Show.objects.get(pk=pk).delete() - - return Response(status=status.HTTP_204_NO_CONTENT) - """ + # if not request.user.is_superuser: + # return Response(status=status.HTTP_401_UNAUTHORIZED) + # + # show = get_object_or_404(Show, pk=pk) + # Show.objects.get(pk=pk).delete() + # + # return Response(status=status.HTTP_204_NO_CONTENT) + # return Response(status=status.HTTP_401_UNAUTHORIZED) class APIScheduleViewSet(viewsets.ModelViewSet): """ - /api/v1/schedules/ Returns schedules (GET) - POST not allowed at this level - /api/v1/schedules/1 Returns the given schedule (GET) - POST not allowed at this level - /api/v1/shows/1/schedules Returns schedules of the show (GET, POST) - /api/v1/shows/1/schedules/1 Returns schedules by its ID (GET, PUT, DELETE) + `/schedules/` returns all schedules (GET) + `/schedules/{schedule_id}` returns the given schedule (GET) + `/shows/{show_id}/schedules` returns schedules of the show (GET, POST) + `/shows/{show_id}/schedules/{schedule_id}` returns schedules by its ID (GET, PUT, DELETE) Only superusers may create and update schedules """ @@ -383,22 +381,25 @@ class APIScheduleViewSet(viewsets.ModelViewSet): permission_classes = [permissions.DjangoModelPermissionsOrAnonReadOnly] def get_queryset(self): - show_pk = self.kwargs['show_pk'] if 'show_pk' in self.kwargs else None + show_pk = self.kwargs.get('show_pk') - if show_pk != None: + if show_pk is not None: return Schedule.objects.filter(show=show_pk) return Schedule.objects.all() - def list(self, request, show_pk=None, pk=None): + def list(self, request, *args, **kwargs): """List Schedules of a show""" + schedules = self.get_queryset() serializer = ScheduleSerializer(schedules, many=True) return Response(serializer.data) - def retrieve(self, request, pk=None, show_pk=None): + def retrieve(self, request, *args, **kwargs): + pk = kwargs.get('pk') + show_pk = kwargs.get('show_pk') - if show_pk != None: + if show_pk is not None: schedule = get_object_or_404(Schedule, pk=pk, show=show_pk) else: schedule = get_object_or_404(Schedule, pk=pk) @@ -406,55 +407,57 @@ class APIScheduleViewSet(viewsets.ModelViewSet): serializer = ScheduleSerializer(schedule) return Response(serializer.data) - def create(self, request, pk=None, show_pk=None): + 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 """ + pk = kwargs.get('pk') + show_pk = kwargs.get('show_pk') # Only allow creating when calling /shows/1/schedules/ - if show_pk == None or not request.user.is_superuser: + if show_pk is None or not request.user.is_superuser: return Response(status=status.HTTP_401_UNAUTHORIZED) # The schedule dict is mandatory - if not 'schedule' in request.data: + if 'schedule' not in request.data: return Response(status=status.HTTP_400_BAD_REQUEST) # First create submit -> return projected timeslots and collisions - if not 'solutions' in request.data: + if 'solutions' not in request.data: return Response(Schedule.make_conflicts(request.data['schedule'], pk, show_pk)) # Otherwise try to resolve resolution = Schedule.resolve_conflicts(request.data, pk, show_pk) # If resolution went well - if not 'projected' in resolution: + if 'projected' not in resolution: return Response(resolution, status=status.HTTP_201_CREATED) # Otherwise return conflicts return Response(resolution) - def update(self, request, pk=None, show_pk=None): + def update(self, request, *args, **kwargs): """ Update a schedule, generate timeslots, test for collisions and resolve them including notes Only superusers may update schedules """ + pk = kwargs.get('pk') + show_pk = kwargs.get('show_pk') # Only allow updating when calling /shows/1/schedules/1 - if show_pk == None or not request.user.is_superuser: + if show_pk is None or not request.user.is_superuser: return Response(status=status.HTTP_401_UNAUTHORIZED) - schedule = get_object_or_404(Schedule, pk=pk, show=show_pk) - # The schedule dict is mandatory - if not 'schedule' in request.data: + if 'schedule' not in request.data: return Response(status=status.HTTP_400_BAD_REQUEST) # First update submit -> return projected timeslots and collisions - if not 'solutions' in request.data: + if 'solutions' not in request.data: # TODO: If nothing else than fallback_id, automation_id or is_repetition changed -> just save and don't do anything return Response(Schedule.make_conflicts(request.data['schedule'], pk, show_pk)) @@ -462,23 +465,24 @@ class APIScheduleViewSet(viewsets.ModelViewSet): resolution = Schedule.resolve_conflicts(request.data, pk, show_pk) # If resolution went well - if not 'projected' in resolution: + if 'projected' not in resolution: return Response(resolution, status=status.HTTP_200_OK) # Otherwise return conflicts return Response(resolution) - def destroy(self, request, pk=None, show_pk=None): + def destroy(self, request, *args, **kwargs): """ Delete a schedule Only superusers may delete schedules """ + pk = kwargs.get('pk') + show_pk = kwargs.get('show_pk') # Only allow deleting when calling /shows/1/schedules/1 - if show_pk == None or not request.user.is_superuser: + if show_pk is None or not request.user.is_superuser: return Response(status=status.HTTP_401_UNAUTHORIZED) - schedule = get_object_or_404(Schedule, pk=pk) Schedule.objects.get(pk=pk).delete() return Response(status=status.HTTP_204_NO_CONTENT) @@ -486,15 +490,15 @@ class APIScheduleViewSet(viewsets.ModelViewSet): class APITimeSlotViewSet(viewsets.ModelViewSet): """ - /api/v1/timeslots Returns timeslots of the next 60 days (GET) - Timeslots may only be added by creating/updating a schedule - /api/v1/timeslots/1 Returns the given timeslot (GET) - PUT/DELETE not allowed at this level - /api/v1/timeslots/?start=2017-01-01&end=2017-02-01 Returns timeslots within the given timerange (GET) - /api/v1/shows/1/timeslots Returns timeslots of the show (GET, POST) - /api/v1/shows/1/timeslots/1 Returns a timeslots by its ID (GET, PUT, DELETE) - /api/v1/shows/1/timeslots?start=2017-01-01&end=2017-02-01 Returns timeslots of the show within the given timerange - /api/v1/shows/1/schedules/1/timeslots Returns all timeslots of the schedule (GET, POST) - /api/v1/shows/1/schedules/1/timeslots/1 Returns a timeslot by its ID (GET, DELETE). If PUT, the next repetition is returned or nothing if the next timeslot isn't one - /api/v1/shows/1/schedules/1/timeslots?start=2017-01-01&end=2017-02-01 Returns all timeslots of the schedule within the given timerange + `/timeslots` returns timeslots of the next 60 days (GET). Timeslots may only be added by creating/updating a schedule + `/timeslots/{timeslot_id}` returns the given timeslot (GET) + `/timeslots/?start={start_date}&end={end_date}` returns timeslots within the time range (GET) + `/shows/{show_id}/timeslots` returns timeslots of the show (GET, POST) + `/shows/{show_id}/timeslots/{timeslot_id}` returns a timeslots by its ID (GET, PUT, DELETE) + `/shows/{show_id}/timeslots?start={start_date}&end={end_date}` returns timeslots of the show within the time range + `/shows/{show_id}/schedules/{schedule_id}/timeslots` returns all timeslots of the schedule (GET, POST) + `/shows/{show_id}/schedules/{schedule_id}/timeslots/{timeslot_id}` 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_id}/schedules/{schedule_id}/timeslots?start={start_date}&end={end_date}` returns all timeslots of the schedule within the time range """ permission_classes = [permissions.DjangoModelPermissionsOrAnonReadOnly] @@ -503,11 +507,10 @@ class APITimeSlotViewSet(viewsets.ModelViewSet): queryset = TimeSlot.objects.none() def get_queryset(self): + show_pk = self.kwargs.get('show_pk') + schedule_pk = self.kwargs.get('schedule_pk') - show_pk = self.kwargs['show_pk'] if 'show_pk' in self.kwargs else None - schedule_pk = self.kwargs['schedule_pk'] if 'schedule_pk' in self.kwargs else None - - '''Filters''' + # Filters # Return next 60 days by default start = datetime.combine(date.today(), time(0, 0)) @@ -517,14 +520,14 @@ class APITimeSlotViewSet(viewsets.ModelViewSet): start = datetime.combine(datetime.strptime(self.request.GET.get('start'), '%Y-%m-%d').date(), time(0, 0)) end = datetime.combine(datetime.strptime(self.request.GET.get('end'), '%Y-%m-%d').date(), time(23, 59)) - '''Endpoints''' + # Endpoints # # /shows/1/schedules/1/timeslots/ # # Returns timeslots of the given show and schedule # - if show_pk != None and schedule_pk != None: + if show_pk is not None and schedule_pk is not None: return TimeSlot.objects.filter(show=show_pk, schedule=schedule_pk, start__gte=start, end__lte=end).order_by('start') # @@ -532,7 +535,7 @@ class APITimeSlotViewSet(viewsets.ModelViewSet): # # Returns timeslots of the show # - elif show_pk != None and schedule_pk == None: + elif show_pk is not None and schedule_pk is None: return TimeSlot.objects.filter(show=show_pk, start__gte=start, end__lte=end).order_by('start') # @@ -543,9 +546,11 @@ class APITimeSlotViewSet(viewsets.ModelViewSet): else: return TimeSlot.objects.filter(start__gte=start, end__lte=end).order_by('start') - def retrieve(self, request, pk=None, schedule_pk=None, show_pk=None): + def retrieve(self, request, *args, **kwargs): + pk = kwargs.get('pk') + show_pk = kwargs.get('show_pk') - if show_pk != None: + if show_pk is not None: timeslot = get_object_or_404(TimeSlot, pk=pk, show=show_pk) else: timeslot = get_object_or_404(TimeSlot, pk=pk) @@ -553,18 +558,21 @@ class APITimeSlotViewSet(viewsets.ModelViewSet): serializer = TimeSlotSerializer(timeslot) return Response(serializer.data) - def create(self, request): + 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, pk=None, schedule_pk=None, show_pk=None): + def update(self, request, *args, **kwargs): """Link a playlist_id to a timeslot""" + pk = kwargs.get('pk') + show_pk = kwargs.get('show_pk') + schedule_pk = kwargs.get('schedule_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, show_pk): + if schedule_pk is None or show_pk is None or not Show.is_editable(self, show_pk): return Response(status=status.HTTP_401_UNAUTHORIZED) timeslot = get_object_or_404(TimeSlot, pk=pk, schedule=schedule_pk, show=show_pk) @@ -586,20 +594,21 @@ class APITimeSlotViewSet(viewsets.ModelViewSet): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - def destroy(self, request, pk=None, schedule_pk=None, show_pk=None): + def destroy(self, request, *args, **kwargs): """ Delete a timeslot Only superusers may delete timeslots """ + pk = kwargs.get('pk') + show_pk = kwargs.get('show_pk') # Only allow when calling endpoint starting with /shows/1/... - if show_pk == None: + 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 = get_object_or_404(TimeSlot, pk=pk) TimeSlot.objects.get(pk=pk).delete() return Response(status=status.HTTP_204_NO_CONTENT) @@ -607,18 +616,18 @@ class APITimeSlotViewSet(viewsets.ModelViewSet): 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 editable by a given user (GET) - /api/v1/notes/?user=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 - /api/v1/shows/1/timeslots/1/note/1 Returns a note by its ID (GET) - PUT/DELETE not allowed at this level - /api/v1/shows/1/schedules/1/timeslots/1/note Returns a note to the timeslot (GET, POST) - Only one note allowed per timeslot - /api/v1/shows/1/schedules/1/timeslots/1/note/1 Returns a note by its ID (GET, PUT, DELETE) + `/notes/` returns all notes (GET) + `/notes/{note_id}` returns a single note (if owned) (GET) + `/notes/?ids={note_id,note_id2}` returns given notes (if owned) (GET) + `/notes/?host={host_id}` returns notes assigned to a given host (GET) + `/notes/?owner={owner_id}` returns notes editable by a given user (GET) + `/notes/?user={user_id}` returns notes created by a given user (GET) + `/shows/{show_id}/notes` returns all notes of a show (GET) + `/shows/{show_id}/notes/{note_id}` returns a note by its ID (GET) + `/shows/{show_id}/timeslots/{timeslot_id}/note/` returns a note of the timeslot (GET) + `/shows/{show_id}/timeslots/{timeslot_id}/note/{note_id}` returns a note by its ID (GET) + `/shows/{show_id}/schedules/{schedule_id}/timeslots/{timeslot_id}/note` returns a note to the timeslot (GET, POST). + `/shows/{show_id}/schedules/{schedule_id}/timeslots/{timeslot_id}/note/{note_id}` returns a note by its ID (GET, PUT, DELETE) Superusers may access and update all notes """ @@ -629,12 +638,10 @@ class APINoteViewSet(viewsets.ModelViewSet): pagination_class = LimitOffsetPagination def get_queryset(self): + timeslot_pk = self.kwargs.get('timeslot_pk') + show_pk = self.kwargs.get('show_pk') - pk = self.kwargs['pk'] if 'pk' in self.kwargs else None - 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 - - '''Endpoints''' + # Endpoints # # /shows/1/schedules/1/timeslots/1/note @@ -642,7 +649,7 @@ class APINoteViewSet(viewsets.ModelViewSet): # # Return a note to the timeslot # - if show_pk != None and timeslot_pk != None: + if show_pk is not None and timeslot_pk is not None: notes = Note.objects.filter(show=show_pk, timeslot=timeslot_pk) # @@ -650,7 +657,7 @@ class APINoteViewSet(viewsets.ModelViewSet): # # Returns notes to the show # - elif show_pk != None and timeslot_pk == None: + elif show_pk is not None and timeslot_pk is None: notes = Note.objects.filter(show=show_pk) # @@ -661,33 +668,36 @@ class APINoteViewSet(viewsets.ModelViewSet): else: notes = Note.objects.all() - '''Filters''' + # Filters - if self.request.GET.get('ids') != None: - '''Filter notes by their IDs''' + if self.request.GET.get('ids') is not None: + # Filter notes by their IDs note_ids = self.request.GET.get('ids').split(',') notes = notes.filter(id__in=note_ids) - if self.request.GET.get('host') != None: - '''Filter notes by host''' + if self.request.GET.get('host') is not None: + # Filter notes by host notes = notes.filter(host=int(self.request.GET.get('host'))) - if self.request.GET.get('owner') != None: - '''Filter notes by show owner: all notes the user may edit''' + if self.request.GET.get('owner') is not None: + # Filter notes by show owner: all notes the user may edit shows = Show.objects.filter(owners=int(self.request.GET.get('owner'))) notes = notes.filter(show__in=shows) - if self.request.GET.get('user') != None: - '''Filter notes by their creator''' + if self.request.GET.get('user') is not None: + # Filter notes by their creator notes = notes.filter(user=int(self.request.GET.get('user'))) return notes - def create(self, request, pk=None, timeslot_pk=None, schedule_pk=None, show_pk=None): + def create(self, request, *args, **kwargs): """Create a note""" + show_pk = kwargs.get('show_pk') + schedule_pk = kwargs.get('schedule_pk') + timeslot_pk = kwargs.get('timeslot_pk') # Only create a note if show_id, timeslot_id and schedule_id is given - if show_pk == None or schedule_pk == None or timeslot_pk == None: + 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): @@ -698,7 +708,7 @@ class APINoteViewSet(viewsets.ModelViewSet): 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: + if not Host.is_editable(self, request.data['host']) or request.data['host'] is None: serializer.validated_data['host'] = None serializer.save() @@ -706,7 +716,7 @@ class APINoteViewSet(viewsets.ModelViewSet): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - def retrieve(self, request, pk=None, timeslot_pk=None, schedule_pk=None, show_pk=None): + def retrieve(self, request, *args, **kwargs): """ Returns a single note @@ -716,13 +726,17 @@ class APINoteViewSet(viewsets.ModelViewSet): /shows/1/timeslots/1/note/1 /shows/1/schedules/1/timeslots/1/note/1 """ + pk = kwargs.get('pk') + show_pk = kwargs.get('show_pk') + schedule_pk = kwargs.get('schedule_pk') + timeslot_pk = kwargs.get('timeslot_pk') # # /shows/1/notes/1 # # Returns a note to a show # - if show_pk != None and timeslot_pk == None and schedule_pk == None: + if show_pk is not None and timeslot_pk is None and schedule_pk is None: note = get_object_or_404(Note, pk=pk, show=show_pk) # @@ -731,7 +745,7 @@ class APINoteViewSet(viewsets.ModelViewSet): # # Return a note to a timeslot # - elif show_pk != None and timeslot_pk != None: + elif show_pk is not None and timeslot_pk is not None: note = get_object_or_404(Note, pk=pk, show=show_pk, timeslot=timeslot_pk) # @@ -745,10 +759,14 @@ class APINoteViewSet(viewsets.ModelViewSet): serializer = NoteSerializer(note) return Response(serializer.data) - def update(self, request, pk=None, show_pk=None, schedule_pk=None, timeslot_pk=None): + def update(self, request, *args, **kwargs): + pk = kwargs.get('pk') + show_pk = kwargs.get('show_pk') + schedule_pk = kwargs.get('schedule_pk') + timeslot_pk = kwargs.get('timeslot_pk') # 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: + 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) @@ -762,17 +780,22 @@ class APINoteViewSet(viewsets.ModelViewSet): 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: + if not Host.is_editable(self, request.data['host']) and request.data['host'] is not None: serializer.validated_data['host'] = Host.objects.filter(pk=note.host_id)[0] - serializer.save(); + serializer.save() return Response(serializer.data) return Response(status=status.HTTP_400_BAD_REQUEST) - def destroy(self, request, pk=None, show_pk=None, schedule_pk=None, timeslot_pk=None): + def destroy(self, request, *args, **kwargs): # Allow DELETE only when calling /shows/1/schedules/1/timeslots/1/note/1 - if show_pk == None or schedule_pk == None or timeslot_pk == None: + pk = kwargs.get('pk') + show_pk = kwargs.get('show_pk') + schedule_pk = kwargs.get('schedule_pk') + timeslot_pk = kwargs.get('timeslot_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) @@ -786,17 +809,18 @@ class APINoteViewSet(viewsets.ModelViewSet): class APICategoryViewSet(viewsets.ModelViewSet): """ - /api/v1/categories/ Returns all categories (GET, POST) - /api/v1/categories/?active=true Returns all active categories (GET) - /api/v1/categories/?active=false Returns all inactive categories (GET) - /api/v1/categories/1 Returns a category by its ID (GET, PUT, DELETE) + `/categories/` returns all categories (GET, POST) + `/categories/?active=true` returns all active categories (GET) + `/categories/?active=false` returns all inactive categories (GET) + `/categories/{category_id}` Returns a category by its ID (GET, PUT, DELETE) """ + queryset = Category.objects.all() serializer_class = CategorySerializer permission_classes = [permissions.DjangoModelPermissionsOrAnonReadOnly] def get_queryset(self): - '''Filters''' + """Filters""" if self.request.GET.get('active') == 'true': return Category.objects.filter(is_active=True) @@ -809,10 +833,10 @@ class APICategoryViewSet(viewsets.ModelViewSet): class APITypeViewSet(viewsets.ModelViewSet): """ - /api/v1/types/ Returns all types (GET, POST) - /api/v1/types/?active=true Returns all active types (GET) - /api/v1/types/?active=false Returns all active types (GET) - /api/v1/types/1 Returns a type by its ID (GET, PUT, DELETE) + `/types/` returns all types (GET, POST) + `/types/?active=true` returns all active types (GET) + `/types/?active=false` returns all inactive types (GET) + `/types/{type_id}` returns a type by its ID (GET, PUT, DELETE) """ queryset = Type.objects.all() @@ -820,7 +844,7 @@ class APITypeViewSet(viewsets.ModelViewSet): permission_classes = [permissions.DjangoModelPermissionsOrAnonReadOnly] def get_queryset(self): - '''Filters''' + """Filters""" if self.request.GET.get('active') == 'true': return Type.objects.filter(is_active=True) @@ -833,9 +857,9 @@ class APITypeViewSet(viewsets.ModelViewSet): class APITopicViewSet(viewsets.ModelViewSet): """ - /api/v1/topics/ Returns all topics (GET, POST) - /api/v1/topics/?active=true Returns all active topics (GET) - /api/v1/topics/1 Returns a topic by its ID (GET, PUT, DELETE) + /topics/: Returns all topics (GET, POST) + /topics/?active=true Returns all active topics (GET) + /topics/1: Returns a topic by its ID (GET, PUT, DELETE) """ queryset = Topic.objects.all() @@ -843,7 +867,7 @@ class APITopicViewSet(viewsets.ModelViewSet): permission_classes = [permissions.DjangoModelPermissionsOrAnonReadOnly] def get_queryset(self): - '''Filters''' + """Filters""" if self.request.GET.get('active') == 'true': return Topic.objects.filter(is_active=True) @@ -856,10 +880,10 @@ class APITopicViewSet(viewsets.ModelViewSet): class APIMusicFocusViewSet(viewsets.ModelViewSet): """ - /api/v1/musicfocus/ Returns all musicfocuses (GET, POST) - /api/v1/musicfocus/?active=true Returns all active musicfocuses (GET) - /api/v1/musicfocus/?active=false Returns all inactive musicfocuses (GET) - /api/v1/musicfocus/1 Returns a musicfocus by its ID (GET, PUT, DELETE) + `/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/{music_focus_id}`: returns a music focus by its ID (GET, PUT, DELETE) """ queryset = MusicFocus.objects.all() @@ -867,7 +891,7 @@ class APIMusicFocusViewSet(viewsets.ModelViewSet): permission_classes = [permissions.DjangoModelPermissionsOrAnonReadOnly] def get_queryset(self): - '''Filters''' + """Filters""" if self.request.GET.get('active') == 'true': return MusicFocus.objects.filter(is_active=True) @@ -880,10 +904,10 @@ class APIMusicFocusViewSet(viewsets.ModelViewSet): class APIFundingCategoryViewSet(viewsets.ModelViewSet): """ - /api/v1/fundingcategories/ Returns all fundingcategories (GET, POST) - /api/v1/fundingcategories/?active=true Returns all active fundingcategories (GET) - /api/v1/fundingcategories/?active=false Returns all active fundingcategories (GET) - /api/v1/fundingcategories/1 Returns a fundingcategory by its ID (GET, PUT, DELETE) + `/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/{funding_category_id}` returns a funding category by its ID (GET, PUT, DELETE) """ queryset = FundingCategory.objects.all() @@ -891,7 +915,7 @@ class APIFundingCategoryViewSet(viewsets.ModelViewSet): permission_classes = [permissions.DjangoModelPermissionsOrAnonReadOnly] def get_queryset(self): - '''Filters''' + """Filters""" if self.request.GET.get('active') == 'true': return FundingCategory.objects.filter(is_active=True) @@ -904,10 +928,10 @@ class APIFundingCategoryViewSet(viewsets.ModelViewSet): class APILanguageViewSet(viewsets.ModelViewSet): """ - /api/v1/languages/ Returns all languages (GET, POST) - /api/v1/languages/?active=true Returns all active languages (GET) - /api/v1/languages/?active=false Returns all active languages (GET) - /api/v1/languages/1 Returns a language by its ID (GET, PUT, DELETE) + `/languages/` returns all languages (GET, POST) + `/languages/?active=true' returns all active languages (GET) + `/languages/?active=false' returns all inactive languages (GET) + `/languages/{language_id}` returns a language by its ID (GET, PUT, DELETE) """ queryset = Language.objects.all() @@ -915,7 +939,7 @@ class APILanguageViewSet(viewsets.ModelViewSet): permission_classes = [permissions.DjangoModelPermissionsOrAnonReadOnly] def get_queryset(self): - '''Filters''' + """Filters""" if self.request.GET.get('active') == 'true': return Language.objects.filter(is_active=True) @@ -928,10 +952,10 @@ class APILanguageViewSet(viewsets.ModelViewSet): class APIHostViewSet(viewsets.ModelViewSet): """ - /api/v1/hosts/ Returns all hosts (GET, POST) - /api/v1/hosts/?active=true Returns all active hosts (GET) - /api/v1/hosts/?active=false Returns all active hosts (GET) - /api/v1/hosts/1 Returns a host by its ID (GET, PUT, DELETE) + `/hosts/` returns all hosts (GET, POST) + `/hosts/?active=true` returns all active hosts (GET) + `/hosts/?active=false` returns all inactive hosts (GET) + `/hosts/1` returns a host by its ID (GET, PUT, DELETE) """ queryset = Host.objects.all() @@ -940,7 +964,7 @@ class APIHostViewSet(viewsets.ModelViewSet): pagination_class = LimitOffsetPagination def get_queryset(self): - '''Filters''' + """Filters""" if self.request.GET.get('active') == 'true': return Host.objects.filter(is_active=True) -- GitLab