Initial changes to Song model using Stores.
This commit is contained in:
parent
7e0b2a5a45
commit
9fa3a408b1
6 changed files with 325 additions and 216 deletions
39
savepointradio/radio/fields.py
Normal file
39
savepointradio/radio/fields.py
Normal file
|
@ -0,0 +1,39 @@
|
|||
'''
|
||||
Custom model fields for the Save Point Radio project.
|
||||
'''
|
||||
|
||||
from django.core import validators
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from .forms import ALLOWED_SCHEMES, RadioIRIFormField
|
||||
|
||||
|
||||
class RadioIRIField(models.TextField):
|
||||
'''
|
||||
A custom URL model field that allows schemes that match those from
|
||||
Liquidsoap. This is necessary due to a bug in how Django currently
|
||||
handles custom URLFields:
|
||||
|
||||
https://code.djangoproject.com/ticket/25594
|
||||
https://stackoverflow.com/questions/41756572/
|
||||
'''
|
||||
default_validators = [validators.URLValidator(schemes=ALLOWED_SCHEMES)]
|
||||
description = _("Long IRI")
|
||||
|
||||
def __init__(self, verbose_name=None, name=None, **kwargs):
|
||||
# This is a limit for Internet Explorer URLs
|
||||
kwargs.setdefault('max_length', 2000)
|
||||
super().__init__(verbose_name, name, **kwargs)
|
||||
|
||||
def deconstruct(self):
|
||||
name, path, args, kwargs = super().deconstruct()
|
||||
if kwargs.get("max_length") == 2000:
|
||||
del kwargs['max_length']
|
||||
return name, path, args, kwargs
|
||||
|
||||
def formfield(self, **kwargs):
|
||||
return super().formfield(**{
|
||||
'form_class': RadioIRIFormField,
|
||||
**kwargs,
|
||||
})
|
21
savepointradio/radio/forms.py
Normal file
21
savepointradio/radio/forms.py
Normal file
|
@ -0,0 +1,21 @@
|
|||
'''
|
||||
Custom forms/formfields for the Save Point Radio project.
|
||||
'''
|
||||
|
||||
from django.core import validators
|
||||
from django.forms.fields import URLField
|
||||
|
||||
|
||||
ALLOWED_SCHEMES = ['http', 'https', 'file', 'ftp', 'ftps', 's3']
|
||||
|
||||
|
||||
class RadioIRIFormField(URLField):
|
||||
'''
|
||||
A custom URL form field that allows schemes that match those from
|
||||
Liquidsoap. This is necessary due to a bug in how Django currently
|
||||
handles custom URLFields:
|
||||
|
||||
https://code.djangoproject.com/ticket/25594
|
||||
https://stackoverflow.com/questions/41756572/
|
||||
'''
|
||||
default_validators = [validators.URLValidator(schemes=ALLOWED_SCHEMES)]
|
|
@ -1,160 +1,129 @@
|
|||
from decimal import *
|
||||
'''
|
||||
Django management command to import old playlist data. This should only be used
|
||||
for seeding a newly created database.
|
||||
'''
|
||||
|
||||
import decimal
|
||||
import json
|
||||
import os
|
||||
import sqlite3
|
||||
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
|
||||
from radio.models import Album, Artist, Game, Song
|
||||
from core.utils import path_to_iri
|
||||
from radio.models import Album, Artist, Game, Store, Song
|
||||
|
||||
getcontext().prec = 8
|
||||
|
||||
|
||||
def adapt_decimal(d):
|
||||
return str(d)
|
||||
|
||||
|
||||
def convert_decimal(s):
|
||||
return Decimal(s.decode('utf8'))
|
||||
|
||||
|
||||
sqlite3.register_adapter(Decimal, adapt_decimal)
|
||||
sqlite3.register_converter("decimal", convert_decimal)
|
||||
decimal.getcontext().prec = 8
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Imports the old radio data from the original sqlite3 database'
|
||||
'''Main "importoldreadio" command class'''
|
||||
help = 'Imports the old radio data from an exported playlist'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('sqlite3_db_file', nargs=1)
|
||||
parser.add_argument('playlist_file', nargs=1)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
if not os.path.isfile(options['sqlite3_db_file'][0]):
|
||||
playlist_file = options['playlist_file'][0]
|
||||
if not os.path.isfile(playlist_file):
|
||||
raise CommandError('File does not exist')
|
||||
else:
|
||||
total_albums = 0
|
||||
total_artists = 0
|
||||
total_games = 0
|
||||
total_songs = 0
|
||||
total_jingles = 0
|
||||
|
||||
detect_types = sqlite3.PARSE_DECLTYPES | sqlite3.PARSE_COLNAMES
|
||||
con = sqlite3.connect(options['sqlite3_db_file'][0],
|
||||
detect_types=detect_types)
|
||||
cur = con.cursor()
|
||||
with open(playlist_file, 'r', encoding='utf8') as pfile:
|
||||
playlist = json.load(pfile, parse_float=decimal.Decimal)
|
||||
|
||||
# Fetching albums first
|
||||
for album in con.execute('SELECT title, enabled FROM albums'):
|
||||
album_disabled = not bool(album[1])
|
||||
Album.objects.create(title=album[0], disabled=album_disabled)
|
||||
total_albums += 1
|
||||
totals = {
|
||||
'albums': 0,
|
||||
'artists': 0,
|
||||
'games': 0,
|
||||
'songs': 0,
|
||||
'jingles': 0
|
||||
}
|
||||
|
||||
self.stdout.write('Imported {} albums'.format(str(total_albums)))
|
||||
# Fetching albums first
|
||||
for album in playlist['albums']:
|
||||
Album.objects.create(title=album['title'],
|
||||
disabled=album['disabled'])
|
||||
total_albums += 1
|
||||
|
||||
# Next up, artists
|
||||
cur.execute('''SELECT
|
||||
artists_id,
|
||||
alias,
|
||||
firstname,
|
||||
lastname,
|
||||
enabled
|
||||
FROM artists''')
|
||||
artists = cur.fetchall()
|
||||
self.stdout.write('Imported {} albums'.format(str(totals['albums'])))
|
||||
|
||||
for artist in artists:
|
||||
artist_disabled = not bool(artist[4])
|
||||
Artist.objects.create(alias=artist[1] or '',
|
||||
first_name=artist[2] or '',
|
||||
last_name=artist[3] or '',
|
||||
disabled=artist_disabled)
|
||||
total_artists += 1
|
||||
# Next up, artists
|
||||
for artist in playlist['artists']:
|
||||
Artist.objects.create(alias=artist['alias'] or '',
|
||||
first_name=artist['first_name'] or '',
|
||||
last_name=artist['last_name'] or '',
|
||||
disabled=artist['disabled'])
|
||||
total_artists += 1
|
||||
|
||||
self.stdout.write('Imported {} artists'.format(str(total_artists)))
|
||||
self.stdout.write('Imported {} artists'.format(str(totals['artists'])))
|
||||
|
||||
# On to games
|
||||
for game in con.execute('SELECT title, enabled FROM games'):
|
||||
game_disabled = not bool(game[1])
|
||||
Game.objects.create(title=game[0], disabled=game_disabled)
|
||||
total_games += 1
|
||||
# On to games
|
||||
for game in playlist['games']:
|
||||
Game.objects.create(title=game['title'],
|
||||
disabled=game['disabled'])
|
||||
total_games += 1
|
||||
|
||||
self.stdout.write('Imported {} games'.format(str(total_games)))
|
||||
self.stdout.write('Imported {} games'.format(str(totals['games'])))
|
||||
|
||||
# Followed by the songs
|
||||
cur.execute('''SELECT
|
||||
songs.songs_id AS id,
|
||||
games.title AS game,
|
||||
albums.title AS album,
|
||||
songs.enabled AS enabled,
|
||||
songs.type AS type,
|
||||
songs.title AS title,
|
||||
songs.length AS length,
|
||||
songs.path AS path
|
||||
FROM songs
|
||||
LEFT JOIN games
|
||||
ON (songs.game = games.games_id)
|
||||
LEFT JOIN albums
|
||||
ON (songs.album = albums.albums_id)''')
|
||||
songs = cur.fetchall()
|
||||
# Followed by the songs
|
||||
for song in playlist['songs']:
|
||||
try:
|
||||
album = Album.objects.get(title__exact=song['album'])
|
||||
except Album.DoesNotExist:
|
||||
album = None
|
||||
|
||||
for song in songs:
|
||||
try:
|
||||
album = Album.objects.get(title__exact=song[2])
|
||||
except Album.DoesNotExist:
|
||||
album = None
|
||||
try:
|
||||
game = Game.objects.get(title__exact=song['game'])
|
||||
except Game.DoesNotExist:
|
||||
game = None
|
||||
|
||||
try:
|
||||
game = Game.objects.get(title__exact=song[1])
|
||||
except Game.DoesNotExist:
|
||||
game = None
|
||||
new_song = Song.objects.create(album=album,
|
||||
game=game,
|
||||
disabled=song['disabled'],
|
||||
song_type=song['type'],
|
||||
title=song['title'])
|
||||
|
||||
song_disabled = not bool(song[3])
|
||||
new_song = Song.objects.create(album=album,
|
||||
game=game,
|
||||
disabled=song_disabled,
|
||||
song_type=song[4],
|
||||
title=song[5],
|
||||
length=song[6],
|
||||
path=song[7])
|
||||
if song[4] == 'S':
|
||||
total_songs += 1
|
||||
else:
|
||||
total_jingles += 1
|
||||
|
||||
cur.execute('''SELECT
|
||||
ifnull(alias, "") AS alias,
|
||||
ifnull(firstname, "") AS firstname,
|
||||
ifnull(lastname, "") AS lastname
|
||||
FROM artists
|
||||
WHERE artists_id
|
||||
IN (SELECT artists_artists_id
|
||||
FROM songs_have_artists
|
||||
WHERE songs_songs_id = ?)''', [song[0]])
|
||||
old_artists = cur.fetchall()
|
||||
for old_artist in old_artists:
|
||||
new_artist = Artist.objects.get(alias__exact=old_artist[0],
|
||||
first_name__exact=old_artist[1],
|
||||
last_name__exact=old_artist[2])
|
||||
new_song.artists.add(new_artist)
|
||||
|
||||
self.stdout.write(
|
||||
'Imported {} requestables ({} songs, {} jingles)'.format(
|
||||
str(total_songs + total_jingles),
|
||||
str(total_songs),
|
||||
str(total_jingles)
|
||||
for artist in song['artists']:
|
||||
new_artist = Artist.objects.get(
|
||||
alias__exact=artist['alias'] or '',
|
||||
first_name__exact=artist['first_name'] or '',
|
||||
last_name__exact=artist['last_name'] or ''
|
||||
)
|
||||
new_song.artists.add(new_artist)
|
||||
|
||||
new_store = Store.objects.create(
|
||||
iri=path_to_iri(song['store']['path']),
|
||||
mime_type=song['store']['mime'],
|
||||
file_size=song['store']['filesize'],
|
||||
length=song['store']['length']
|
||||
)
|
||||
|
||||
pub = input('Do you want to publish all imported objects as well? '
|
||||
'[Y/N] ')
|
||||
|
||||
if pub == 'Y' or pub == 'y':
|
||||
for al in Album.objects.all():
|
||||
al.publish()
|
||||
for ar in Artist.objects.all():
|
||||
ar.publish()
|
||||
for g in Game.objects.all():
|
||||
g.publish()
|
||||
for s in Song.objects.all():
|
||||
s.publish()
|
||||
self.stdout.write('Published imported objects successfully')
|
||||
new_song.stores.add(new_store)
|
||||
new_song.current_store = new_store
|
||||
new_song.save()
|
||||
if song['type'] == 'S':
|
||||
totals['songs'] += 1
|
||||
else:
|
||||
self.stdout.write('Skipped publishing songs')
|
||||
totals['jingles'] += 1
|
||||
|
||||
self.stdout.write(
|
||||
'Imported {} requestables ({} songs, {} jingles)'.format(
|
||||
str(totals['songs'] + totals['jingles']),
|
||||
str(totals['songs']),
|
||||
str(totals['jingles'])
|
||||
)
|
||||
)
|
||||
|
||||
pub = input('Do you want to publish all imported objects as well? '
|
||||
'[Y/N] ')
|
||||
|
||||
if pub in ('Y', 'y'):
|
||||
for album in Album.objects.all():
|
||||
album.publish()
|
||||
for artist in Artist.objects.all():
|
||||
artist.publish()
|
||||
for game in Game.objects.all():
|
||||
game.publish()
|
||||
for song in Song.objects.all():
|
||||
song.publish()
|
||||
self.stdout.write('Published imported objects successfully')
|
||||
else:
|
||||
self.stdout.write('Skipped publishing songs')
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
'''
|
||||
Django Model Managers for the Radio application.
|
||||
'''
|
||||
|
||||
from datetime import timedelta
|
||||
from decimal import getcontext, Decimal, ROUND_UP
|
||||
from random import randint
|
||||
|
@ -15,123 +19,126 @@ getcontext().prec = 16
|
|||
|
||||
|
||||
class RadioManager(models.Manager):
|
||||
"""
|
||||
'''
|
||||
Custom object manager for filtering out common behaviors for radio
|
||||
objects.
|
||||
"""
|
||||
'''
|
||||
def get_queryset(self):
|
||||
"""
|
||||
'''
|
||||
Return customized default QuerySet.
|
||||
"""
|
||||
'''
|
||||
return RadioQuerySet(self.model, using=self._db)
|
||||
|
||||
def disabled(self):
|
||||
"""
|
||||
'''
|
||||
Radio objects that are marked as disabled.
|
||||
"""
|
||||
'''
|
||||
return self.get_queryset().disabled()
|
||||
|
||||
def enabled(self):
|
||||
"""
|
||||
'''
|
||||
Radio objects that are marked as enabled.
|
||||
"""
|
||||
'''
|
||||
return self.get_queryset().enabled()
|
||||
|
||||
def published(self):
|
||||
"""
|
||||
'''
|
||||
Radio objects that are marked as published.
|
||||
"""
|
||||
'''
|
||||
return self.get_queryset().published()
|
||||
|
||||
def unpublished(self):
|
||||
"""
|
||||
'''
|
||||
Radio objects that are marked as unpublished.
|
||||
"""
|
||||
'''
|
||||
return self.get_queryset().unpublished()
|
||||
|
||||
def available(self):
|
||||
"""
|
||||
'''
|
||||
Radio objects that are enabled and published.
|
||||
"""
|
||||
'''
|
||||
return self.enabled().published()
|
||||
|
||||
|
||||
class SongManager(RadioManager):
|
||||
"""
|
||||
'''
|
||||
Custom object manager for filtering out common behaviors for Song objects.
|
||||
"""
|
||||
'''
|
||||
def get_queryset(self):
|
||||
"""
|
||||
'''
|
||||
Return customized default QuerySet for Songs.
|
||||
"""
|
||||
'''
|
||||
return SongQuerySet(self.model, using=self._db)
|
||||
|
||||
def available_jingles(self):
|
||||
"""
|
||||
'''
|
||||
Jingles that are currently published and are enabled.
|
||||
"""
|
||||
'''
|
||||
return self.available().jingles()
|
||||
|
||||
def available_songs(self):
|
||||
"""
|
||||
'''
|
||||
Songs that are currently published and are enabled.
|
||||
"""
|
||||
'''
|
||||
return self.available().songs()
|
||||
|
||||
def playlist_length(self):
|
||||
"""
|
||||
'''
|
||||
Total length of available songs in the playlist (in seconds).
|
||||
"""
|
||||
length = self.available_songs().aggregate(models.Sum('length'))
|
||||
return length['length__sum']
|
||||
'''
|
||||
a_songs = self.available_songs()
|
||||
length = a_songs.aggregate(
|
||||
total_time=models.Sum('current_store__length')
|
||||
)
|
||||
return length['total_time']
|
||||
|
||||
def wait_total(self, adjusted_ratio=0.0):
|
||||
"""
|
||||
'''
|
||||
Default length in seconds before a song can be played again. This is
|
||||
based on the replay ratio set in the application settings.
|
||||
"""
|
||||
'''
|
||||
total_ratio = get_setting('replay_ratio') + adjusted_ratio
|
||||
wait = self.playlist_length() * Decimal(total_ratio)
|
||||
wait = wait.quantize(Decimal('.01'), rounding=ROUND_UP)
|
||||
return timedelta(seconds=float(wait))
|
||||
|
||||
def datetime_from_wait(self):
|
||||
"""
|
||||
'''
|
||||
Datetime of now minus the default wait time for played songs.
|
||||
"""
|
||||
'''
|
||||
return timezone.now() - self.wait_total()
|
||||
|
||||
def playable(self):
|
||||
"""
|
||||
'''
|
||||
Songs that are playable because they are available (enabled &
|
||||
published) and they have not been played within the default wait time
|
||||
(or at all).
|
||||
"""
|
||||
'''
|
||||
return self.available_songs().filter(
|
||||
models.Q(next_play__lt=timezone.now()) |
|
||||
models.Q(next_play__isnull=True)
|
||||
)
|
||||
models.Q(next_play__lt=timezone.now()) |
|
||||
models.Q(next_play__isnull=True)
|
||||
)
|
||||
|
||||
def requestable(self):
|
||||
"""
|
||||
'''
|
||||
Songs that can be placed in the request queue for playback.
|
||||
"""
|
||||
'''
|
||||
# Import SongRequest here to get rid of circular dependencies
|
||||
SongRequest = apps.get_model(app_label='profiles',
|
||||
model_name='SongRequest')
|
||||
requests = SongRequest.music.unplayed().values_list('song__id',
|
||||
flat=True)
|
||||
song_request = apps.get_model(app_label='profiles',
|
||||
model_name='SongRequest')
|
||||
requests = song_request.music.unplayed().values_list('song__id',
|
||||
flat=True)
|
||||
return self.playable().exclude(id__in=requests)
|
||||
|
||||
def get_random_requestable_song(self):
|
||||
"""
|
||||
'''
|
||||
Pick a random requestable song and return it.
|
||||
"""
|
||||
'''
|
||||
return self.requestable()[randint(0, self.requestable().count() - 1)]
|
||||
|
||||
def get_random_jingle(self):
|
||||
"""
|
||||
'''
|
||||
Pick a random jingle and return it.
|
||||
"""
|
||||
'''
|
||||
random_index = randint(0, self.available_jingles().count() - 1)
|
||||
return self.available_jingles()[random_index]
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
# Generated by Django 2.2.1 on 2019-05-31 03:00
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import radio.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('radio', '0003_song_next_play'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Store',
|
||||
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')),
|
||||
('iri', radio.fields.RadioIRIField()),
|
||||
('mime_type', models.CharField(blank=True, max_length=64, verbose_name='file MIME type')),
|
||||
('file_size', models.BigIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0)], verbose_name='file size')),
|
||||
('length', models.DecimalField(blank=True, decimal_places=2, max_digits=8, null=True, verbose_name='song length (in seconds)')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='song',
|
||||
name='length',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='song',
|
||||
name='path',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='song',
|
||||
name='current_store',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='current_of', to='radio.Store'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='song',
|
||||
name='stores',
|
||||
field=models.ManyToManyField(blank=True, related_name='song', to='radio.Store'),
|
||||
),
|
||||
]
|
|
@ -1,13 +1,19 @@
|
|||
'''
|
||||
Django Models for the Radio application.
|
||||
'''
|
||||
|
||||
from datetime import timedelta
|
||||
from decimal import getcontext, Decimal, ROUND_UP
|
||||
|
||||
from django.apps import apps
|
||||
from django.core.validators import MinValueValidator
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from core.behaviors import Disableable, Publishable, Timestampable
|
||||
from core.utils import get_setting
|
||||
from .fields import RadioIRIField
|
||||
from .managers import RadioManager, SongManager
|
||||
|
||||
|
||||
|
@ -16,9 +22,9 @@ getcontext().prec = 16
|
|||
|
||||
|
||||
class Album(Disableable, Publishable, Timestampable, models.Model):
|
||||
"""
|
||||
'''
|
||||
A model for a music album.
|
||||
"""
|
||||
'''
|
||||
title = models.CharField(_('title'), max_length=255, unique=True)
|
||||
|
||||
sorted_title = models.CharField(_('naturalized title'),
|
||||
|
@ -37,9 +43,9 @@ class Album(Disableable, Publishable, Timestampable, models.Model):
|
|||
|
||||
|
||||
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)
|
||||
|
@ -57,10 +63,10 @@ class Artist(Disableable, Publishable, Timestampable, models.Model):
|
|||
|
||||
@property
|
||||
def full_name(self):
|
||||
"""
|
||||
'''
|
||||
String representing the artist's full name including an alias, if
|
||||
available.
|
||||
"""
|
||||
'''
|
||||
if self.alias:
|
||||
if self.first_name or self.last_name:
|
||||
return '{} "{}" {}'.format(self.first_name,
|
||||
|
@ -74,9 +80,9 @@ class Artist(Disableable, Publishable, Timestampable, models.Model):
|
|||
|
||||
|
||||
class Game(Disableable, Publishable, Timestampable, models.Model):
|
||||
"""
|
||||
'''
|
||||
A model for a game.
|
||||
"""
|
||||
'''
|
||||
title = models.CharField(_('title'), max_length=255, unique=True)
|
||||
|
||||
sorted_title = models.CharField(_('naturalized title'),
|
||||
|
@ -94,10 +100,29 @@ class Game(Disableable, Publishable, Timestampable, models.Model):
|
|||
return self.title
|
||||
|
||||
|
||||
class Store(Timestampable, models.Model):
|
||||
'''
|
||||
A model to represent various data locations (stores) for the song.
|
||||
'''
|
||||
iri = RadioIRIField()
|
||||
mime_type = models.CharField(_('file MIME type'),
|
||||
max_length=64,
|
||||
blank=True)
|
||||
file_size = models.BigIntegerField(_('file size'),
|
||||
validators=[MinValueValidator(0)],
|
||||
blank=True,
|
||||
null=True)
|
||||
length = models.DecimalField(_('song length (in seconds)'),
|
||||
max_digits=8,
|
||||
decimal_places=2,
|
||||
null=True,
|
||||
blank=True)
|
||||
|
||||
|
||||
class Song(Disableable, Publishable, Timestampable, models.Model):
|
||||
"""
|
||||
'''
|
||||
A model for a song.
|
||||
"""
|
||||
'''
|
||||
JINGLE = 'J'
|
||||
SONG = 'S'
|
||||
TYPE_CHOICES = (
|
||||
|
@ -128,13 +153,12 @@ class Song(Disableable, Publishable, Timestampable, models.Model):
|
|||
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'))
|
||||
|
||||
stores = models.ManyToManyField(Store, blank=True, related_name='song')
|
||||
current_store = models.ForeignKey(Store,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='current_of')
|
||||
sorted_title = models.CharField(_('naturalized title'),
|
||||
db_index=True,
|
||||
editable=False,
|
||||
|
@ -147,34 +171,34 @@ class Song(Disableable, Publishable, Timestampable, models.Model):
|
|||
ordering = ['sorted_title', ]
|
||||
|
||||
def _is_jingle(self):
|
||||
"""
|
||||
'''
|
||||
Is the object a jingle?
|
||||
"""
|
||||
'''
|
||||
return self.song_type == 'J'
|
||||
_is_jingle.boolean = True
|
||||
is_jingle = property(_is_jingle)
|
||||
|
||||
def _is_song(self):
|
||||
"""
|
||||
'''
|
||||
Is the object a song?
|
||||
"""
|
||||
'''
|
||||
return self.song_type == 'S'
|
||||
_is_song.boolean = True
|
||||
is_song = property(_is_song)
|
||||
|
||||
def _is_available(self):
|
||||
"""
|
||||
'''
|
||||
Is the object both enabled and published?
|
||||
"""
|
||||
'''
|
||||
return self._is_enabled() and self._is_published()
|
||||
_is_available.boolean = True
|
||||
is_available = property(_is_available)
|
||||
|
||||
def _full_title(self):
|
||||
"""
|
||||
'''
|
||||
String representing the entire song title, including the game and
|
||||
artists involved.
|
||||
"""
|
||||
'''
|
||||
if self._is_song():
|
||||
enabled_artists = self.artists.all().filter(disabled=False)
|
||||
all_artists = ', '.join([a.full_name for a in enabled_artists])
|
||||
|
@ -185,9 +209,9 @@ class Song(Disableable, Publishable, Timestampable, models.Model):
|
|||
full_title = property(_full_title)
|
||||
|
||||
def _average_rating(self):
|
||||
"""
|
||||
'''
|
||||
Decimal number of the average rating of a song from 1 - 5.
|
||||
"""
|
||||
'''
|
||||
ratings = self.rating_set.all()
|
||||
if ratings:
|
||||
avg = Decimal(ratings.aggregate(avg=models.Avg('value'))['avg'])
|
||||
|
@ -196,9 +220,9 @@ class Song(Disableable, Publishable, Timestampable, models.Model):
|
|||
average_rating = property(_average_rating)
|
||||
|
||||
def get_time_until_requestable(self):
|
||||
"""
|
||||
'''
|
||||
Length of time before a song can be requested again.
|
||||
"""
|
||||
'''
|
||||
if self._is_song() and self._is_available():
|
||||
if self.last_played:
|
||||
allowed_datetime = Song.music.datetime_from_wait()
|
||||
|
@ -209,9 +233,9 @@ class Song(Disableable, Publishable, Timestampable, models.Model):
|
|||
return None
|
||||
|
||||
def get_date_when_requestable(self, last_play=None):
|
||||
"""
|
||||
'''
|
||||
Datetime when a song can be requested again.
|
||||
"""
|
||||
'''
|
||||
last = self.last_played if last_play is None else last_play
|
||||
|
||||
if self._is_song() and self._is_available():
|
||||
|
@ -232,10 +256,10 @@ class Song(Disableable, Publishable, Timestampable, models.Model):
|
|||
return None
|
||||
|
||||
def _is_playable(self):
|
||||
"""
|
||||
'''
|
||||
Is the song available and not been played within the default waiting
|
||||
period (or at all)?
|
||||
"""
|
||||
'''
|
||||
if self._is_song() and self._is_available():
|
||||
return self.get_date_when_requestable() <= timezone.now()
|
||||
return False
|
||||
|
@ -243,14 +267,14 @@ class Song(Disableable, Publishable, Timestampable, models.Model):
|
|||
is_playable = property(_is_playable)
|
||||
|
||||
def _is_requestable(self):
|
||||
"""
|
||||
'''
|
||||
Is the song playable and has it not already been requested?
|
||||
"""
|
||||
'''
|
||||
if self._is_playable():
|
||||
SongRequest = apps.get_model(app_label='profiles',
|
||||
model_name='SongRequest')
|
||||
requests = SongRequest.music.unplayed().values_list('song__id',
|
||||
flat=True)
|
||||
song_request = apps.get_model(app_label='profiles',
|
||||
model_name='SongRequest')
|
||||
requests = song_request.music.unplayed().values_list('song__id',
|
||||
flat=True)
|
||||
return self.pk not in requests
|
||||
return False
|
||||
_is_requestable.boolean = True
|
||||
|
|
Loading…
Reference in a new issue