diff --git a/savepointradio/radio/__init__.py b/savepointradio/radio/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/savepointradio/radio/admin.py b/savepointradio/radio/admin.py new file mode 100644 index 0000000..5f0b5d4 --- /dev/null +++ b/savepointradio/radio/admin.py @@ -0,0 +1,203 @@ +from django.contrib import admin +from django.db import models +from django.forms import TextInput +from django.utils import timezone + +from .models import Album, Artist, Game, Song + + +class ArtistInline(admin.TabularInline): + model = Song.artists.through + verbose_name = 'artist' + verbose_name_plural = 'artists' + extra = 0 + + +@admin.register(Album) +class AlbumAdmin(admin.ModelAdmin): + # Detail List display + list_display = ('title', 'is_enabled', 'is_published') + search_fields = ['title'] + actions = ['publish_items'] + + # Edit Form display + readonly_fields = ('created_date', 'modified_date') + fieldsets = ( + ('Album Disabling', { + 'classes': ('collapse',), + 'fields': ('disabled', 'disabled_date', 'disabled_reason') + }), + ('Main', { + 'fields': ('title', 'published_date') + }), + ('Stats', { + 'classes': ('collapse',), + 'fields': ('created_date', 'modified_date') + }) + ) + + def is_enabled(self, obj): + return not obj.disabled + + def is_published(self, obj): + return obj.is_published + + def publish_items(self, request, queryset): + rows_updated = queryset.update(published_date=timezone.now()) + if rows_updated == 1: + msg = '1 album was' + else: + msg = '{} albums were'.format(str(rows_updated)) + self.message_user(request, '{} successfully published.'.format(msg)) + publish_items.short_description = "Publish selected items" + + +@admin.register(Artist) +class ArtistAdmin(admin.ModelAdmin): + # Detail List display + list_display = ('first_name', + 'alias', + 'last_name', + 'is_enabled', + 'is_published') + search_fields = ['first_name', 'alias', 'last_name'] + actions = ['publish_items'] + + # Edit Form display + readonly_fields = ('created_date', 'modified_date') + fieldsets = ( + ('Artist Disabling', { + 'classes': ('collapse',), + 'fields': ('disabled', 'disabled_date', 'disabled_reason') + }), + ('Main', { + 'fields': ('first_name', 'alias', 'last_name', 'published_date') + }), + ('Stats', { + 'classes': ('collapse',), + 'fields': ('created_date', 'modified_date') + }) + ) + + def is_enabled(self, obj): + return not obj.disabled + + def is_published(self, obj): + return obj.is_published + + def publish_items(self, request, queryset): + rows_updated = queryset.update(published_date=timezone.now()) + if rows_updated == 1: + msg = '1 artist was' + else: + msg = '{} artists were'.format(str(rows_updated)) + self.message_user(request, '{} successfully published.'.format(msg)) + publish_items.short_description = "Publish selected items" + + +@admin.register(Game) +class GameAdmin(admin.ModelAdmin): + # Detail List display + list_display = ('title', 'is_enabled', 'is_published') + search_fields = ['title'] + actions = ['publish_items'] + + # Edit Form display + readonly_fields = ('created_date', 'modified_date') + fieldsets = ( + ('Game Disabling', { + 'classes': ('collapse',), + 'fields': ('disabled', 'disabled_date', 'disabled_reason') + }), + ('Main', { + 'fields': ('title', 'published_date') + }), + ('Stats', { + 'classes': ('collapse',), + 'fields': ('created_date', 'modified_date') + }) + ) + + def is_enabled(self, obj): + return not obj.disabled + + def is_published(self, obj): + return obj.is_published + + def publish_items(self, request, queryset): + rows_updated = queryset.update(published_date=timezone.now()) + if rows_updated == 1: + msg = '1 game was' + else: + msg = '{} games were'.format(str(rows_updated)) + self.message_user(request, '{} successfully published.'.format(msg)) + publish_items.short_description = "Publish selected items" + + +@admin.register(Song) +class SongAdmin(admin.ModelAdmin): + formfield_overrides = { + models.TextField: {'widget': TextInput(attrs={'size': 160, })}, + } + + # Detail List display + list_display = ('title', + 'game', + 'artist_list', + 'is_enabled', + 'is_published') + search_fields = ['title'] + actions = ['publish_items'] + + # Edit Form display + exclude = ('artists',) + readonly_fields = ('length', + 'last_played', + 'num_played', + 'created_date', + 'modified_date') + fieldsets = ( + ('Song Disabling', { + 'classes': ('collapse',), + 'fields': ('disabled', 'disabled_date', 'disabled_reason') + }), + ('Main', { + 'fields': ('song_type', + 'title', + 'path', + 'published_date') + }), + ('Stats', { + 'classes': ('collapse',), + 'fields': ('created_date', + 'modified_date', + 'last_played', + 'num_played', + 'length') + }), + ('Album', { + 'fields': ('album',) + }), + ('Game', { + 'fields': ('game',) + }) + ) + inlines = [ArtistInline] + + def artist_list(self, obj): + return ', '.join([a.full_name for a in obj.artists.all()]) + + def is_enabled(self, obj): + return not obj.disabled + + def is_published(self, obj): + return obj.is_published + + def publish_items(self, request, queryset): + rows_updated = queryset.update(published_date=timezone.now()) + if rows_updated == 1: + msg = '1 song was' + else: + msg = '{} songs were'.format(str(rows_updated)) + self.message_user(request, '{} successfully published.'.format(msg)) + publish_items.short_description = "Publish selected items" diff --git a/savepointradio/radio/apps.py b/savepointradio/radio/apps.py new file mode 100644 index 0000000..bd336cf --- /dev/null +++ b/savepointradio/radio/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class RadioConfig(AppConfig): + name = 'radio' diff --git a/savepointradio/radio/behaviors.py b/savepointradio/radio/behaviors.py new file mode 100644 index 0000000..ec51944 --- /dev/null +++ b/savepointradio/radio/behaviors.py @@ -0,0 +1,56 @@ +from django.db import models +from django.utils import timezone +from django.utils.translation import ugettext_lazy as _ + + +class Disableable(models.Model): + """ + Mixin for models that can be disabled with a specified reason. + """ + disabled = models.BooleanField(_('disabled state'), default=False) + disabled_date = models.DateTimeField(_('disabled on'), + default=None, + blank=True, + null=True) + disabled_reason = models.TextField(_('reason for disabling'), blank=True) + + class Meta: + abstract = True + + def disable(self, reason=''): + self.disabled = True + self.disabled_date = timezone.now() + self.disabled_reason = reason + self.save() + + def enable(self): + self.disabled = False + self.disabled_date = None + self.disabled_reason = '' + self.save() + + +class Publishable(models.Model): + """ + Mixin for models that can be published to restrict accessibility before an + appointed date/time. + """ + published_date = models.DateTimeField(_('published for listening'), + default=None, + blank=True, + null=True) + + class Meta: + abstract = True + + def publish(self, date=None): + if date is None: + date = timezone.now() + self.published_date = date + self.save() + + @property + def is_published(self): + if self.published_date is not None: + return self.published_date < timezone.now() + return False diff --git a/savepointradio/radio/managers.py b/savepointradio/radio/managers.py new file mode 100644 index 0000000..f56c810 --- /dev/null +++ b/savepointradio/radio/managers.py @@ -0,0 +1,14 @@ +from django.db import models + +from .querysets import SongQuerySet + + +class SongManager(models.Manager): + """ + Custom object manager for filtering out common behaviors for a playlist. + """ + def get_queryset(self): + return SongQuerySet(self.model, using=self._db) + + def available(self): + return self.get_queryset().songs().enabled().published() diff --git a/savepointradio/radio/migrations/0001_initial.py b/savepointradio/radio/migrations/0001_initial.py new file mode 100644 index 0000000..b5f2d50 --- /dev/null +++ b/savepointradio/radio/migrations/0001_initial.py @@ -0,0 +1,89 @@ +# Generated by Django 2.0 on 2017-12-29 14:36 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Album', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_date', models.DateTimeField(auto_now_add=True, verbose_name='added on')), + ('modified_date', models.DateTimeField(auto_now=True, verbose_name='last modified')), + ('disabled', models.BooleanField(default=False, verbose_name='disabled state')), + ('disabled_date', models.DateTimeField(blank=True, default=None, null=True, verbose_name='disabled on')), + ('disabled_reason', models.TextField(blank=True, verbose_name='reason for disabling')), + ('published_date', models.DateTimeField(blank=True, default=None, null=True, verbose_name='published for listening')), + ('title', models.CharField(max_length=255, unique=True, verbose_name='title')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='Artist', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_date', models.DateTimeField(auto_now_add=True, verbose_name='added on')), + ('modified_date', models.DateTimeField(auto_now=True, verbose_name='last modified')), + ('disabled', models.BooleanField(default=False, verbose_name='disabled state')), + ('disabled_date', models.DateTimeField(blank=True, default=None, null=True, verbose_name='disabled on')), + ('disabled_reason', models.TextField(blank=True, verbose_name='reason for disabling')), + ('published_date', models.DateTimeField(blank=True, default=None, null=True, verbose_name='published for listening')), + ('alias', models.CharField(blank=True, max_length=127, verbose_name='alias')), + ('first_name', models.CharField(blank=True, max_length=127, verbose_name='first name')), + ('last_name', models.CharField(blank=True, max_length=127, verbose_name='last name')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='Game', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_date', models.DateTimeField(auto_now_add=True, verbose_name='added on')), + ('modified_date', models.DateTimeField(auto_now=True, verbose_name='last modified')), + ('disabled', models.BooleanField(default=False, verbose_name='disabled state')), + ('disabled_date', models.DateTimeField(blank=True, default=None, null=True, verbose_name='disabled on')), + ('disabled_reason', models.TextField(blank=True, verbose_name='reason for disabling')), + ('published_date', models.DateTimeField(blank=True, default=None, null=True, verbose_name='published for listening')), + ('title', models.CharField(max_length=255, unique=True, verbose_name='title')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='Song', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_date', models.DateTimeField(auto_now_add=True, verbose_name='added on')), + ('modified_date', models.DateTimeField(auto_now=True, verbose_name='last modified')), + ('disabled', models.BooleanField(default=False, verbose_name='disabled state')), + ('disabled_date', models.DateTimeField(blank=True, default=None, null=True, verbose_name='disabled on')), + ('disabled_reason', models.TextField(blank=True, verbose_name='reason for disabling')), + ('published_date', models.DateTimeField(blank=True, default=None, null=True, verbose_name='published for listening')), + ('song_type', models.CharField(choices=[('J', 'Jingle'), ('S', 'Song')], default='S', max_length=1, verbose_name='song type')), + ('title', models.CharField(max_length=255, verbose_name='title')), + ('num_played', models.PositiveIntegerField(default=0, verbose_name='number of times played')), + ('last_played', models.DateTimeField(blank=True, editable=False, null=True, verbose_name='was last played')), + ('length', models.DecimalField(blank=True, decimal_places=2, max_digits=8, null=True, verbose_name='song length (in seconds)')), + ('path', models.TextField(verbose_name='absolute path to song file')), + ('album', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='radio.Album')), + ('artists', models.ManyToManyField(to='radio.Artist')), + ('game', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='radio.Game')), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/savepointradio/radio/migrations/__init__.py b/savepointradio/radio/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/savepointradio/radio/models.py b/savepointradio/radio/models.py new file mode 100644 index 0000000..97998ad --- /dev/null +++ b/savepointradio/radio/models.py @@ -0,0 +1,100 @@ +from django.db import models +from django.utils.translation import ugettext_lazy as _ + +from core.behaviors import Timestampable +from .behaviors import Disableable, Publishable +from .managers import SongManager + + +class Album(Disableable, Publishable, Timestampable, models.Model): + """ + A model for a music album. + """ + title = models.CharField(_('title'), max_length=255, unique=True) + + def __str__(self): + return self.title + + +class Artist(Disableable, Publishable, Timestampable, models.Model): + """ + A model for a music artist. + """ + alias = models.CharField(_('alias'), max_length=127, blank=True) + first_name = models.CharField(_('first name'), max_length=127, blank=True) + last_name = models.CharField(_('last name'), max_length=127, blank=True) + + @property + def full_name(self): + if not self.alias: + return '{} {}'.format(self.first_name, self.last_name) + else: + if not self.first_name or not self.last_name: + return self.alias + else: + return '{} "{}" {}'.format(self.first_name, + self.alias, + self.last_name) + + def __str__(self): + return self.full_name + + +class Game(Disableable, Publishable, Timestampable, models.Model): + """ + A model for a game. + """ + title = models.CharField(_('title'), max_length=255, unique=True) + + def __str__(self): + return self.title + + +class Song(Disableable, Publishable, Timestampable, models.Model): + """ + A model for a song. + """ + JINGLE = 'J' + SONG = 'S' + TYPE_CHOICES = ( + (JINGLE, 'Jingle'), + (SONG, 'Song'), + ) + album = models.ForeignKey(Album, + on_delete=models.SET_NULL, + null=True, + blank=True) + artists = models.ManyToManyField(Artist) + game = models.ForeignKey(Game, + on_delete=models.SET_NULL, + null=True, + blank=True) + song_type = models.CharField(_('song type'), + max_length=1, + choices=TYPE_CHOICES, + default=SONG) + title = models.CharField(_('title'), max_length=255) + num_played = models.PositiveIntegerField(_('number of times played'), + default=0) + last_played = models.DateTimeField(_('was last played'), + null=True, + blank=True, + editable=False) + length = models.DecimalField(_('song length (in seconds)'), + max_digits=8, + decimal_places=2, + null=True, + blank=True) + path = models.TextField(_('absolute path to song file')) + + objects = models.Manager() + music = SongManager() + + def __str__(self): + if self.song_type == 'J': + return self.title + else: + all_artists = ', '.join([a.full_name for a in self.artists.all()]) + return '{} - {} ({})'.format(self.game.title, + self.title, + all_artists) diff --git a/savepointradio/radio/querysets.py b/savepointradio/radio/querysets.py new file mode 100644 index 0000000..6552c81 --- /dev/null +++ b/savepointradio/radio/querysets.py @@ -0,0 +1,45 @@ +from datetime import timedelta + +from django.db import models +from django.utils import timezone + + +class EnabledQuerySet(models.QuerySet): + """ + Queryset to select all objects that are not disabled. + """ + def enabled(self): + return self.filter(disabled=False) + + +class PublishedQuerySet(models.QuerySet): + """ + Queryset to select all objects that have been published. + """ + def published(self): + results = self.filter( + models.Q(published_date__isnull=False) & + models.Q(published_date__lte=timezone.now()) + ) + return results + + +class TypeQuerySet(models.QuerySet): + """ + Queryset to select all objects that are either songs or jingles. + """ + def songs(self): + return self.filter(song_type='S') + + def jingles(self): + return self.filter(song_type='J') + + +class SongQuerySet(EnabledQuerySet, + PublishedQuerySet, + TypeQuerySet): + """ + Queryset combination that can easily select enabled objects, published + objects, and objects of a certain song type. + """ + pass diff --git a/savepointradio/radio/tests.py b/savepointradio/radio/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/savepointradio/radio/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/savepointradio/radio/views.py b/savepointradio/radio/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/savepointradio/radio/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/savepointradio/savepointradio/settings/base.py b/savepointradio/savepointradio/settings/base.py index 0a8d1dd..ac3137e 100644 --- a/savepointradio/savepointradio/settings/base.py +++ b/savepointradio/savepointradio/settings/base.py @@ -40,6 +40,7 @@ INSTALLED_APPS = [ 'authtools', 'core.apps.CoreConfig', + 'radio.apps.RadioConfig', ] LANGUAGE_CODE = 'en-us'