Added dynamic settings and removed my own attempt.
This commit is contained in:
parent
6e4cccb5f6
commit
ccddf6c8bf
11 changed files with 122 additions and 145 deletions
|
@ -3,8 +3,10 @@ asgiref>=3.2.3
|
||||||
cffi>=1.13.2
|
cffi>=1.13.2
|
||||||
dj-database-url>=0.5.0
|
dj-database-url>=0.5.0
|
||||||
Django>=3.0.2
|
Django>=3.0.2
|
||||||
|
django-dynamic-preferences>=1.8.1
|
||||||
django-inline-actions>=2.3.0
|
django-inline-actions>=2.3.0
|
||||||
djangorestframework>=3.11.0
|
djangorestframework>=3.11.0
|
||||||
|
persisting-theory>=0.2.1
|
||||||
psycopg2-binary>=2.8.4
|
psycopg2-binary>=2.8.4
|
||||||
pycparser>=2.19
|
pycparser>=2.19
|
||||||
python-decouple>=3.3
|
python-decouple>=3.3
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from dynamic_preferences.registries import global_preferences_registry
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.authentication import (SessionAuthentication,
|
from rest_framework.authentication import (SessionAuthentication,
|
||||||
TokenAuthentication)
|
TokenAuthentication)
|
||||||
|
@ -10,7 +11,6 @@ from rest_framework.renderers import BrowsableAPIRenderer, JSONRenderer
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
|
|
||||||
from core.utils import get_setting
|
|
||||||
from profiles.exceptions import MakeRequestError
|
from profiles.exceptions import MakeRequestError
|
||||||
from profiles.models import RadioProfile, SongRequest
|
from profiles.models import RadioProfile, SongRequest
|
||||||
from radio.models import Song
|
from radio.models import Song
|
||||||
|
@ -22,6 +22,7 @@ from ..serializers.controls import (JustPlayedSerializer,
|
||||||
|
|
||||||
|
|
||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
|
radio_settings = global_preferences_registry.manager()
|
||||||
|
|
||||||
|
|
||||||
class JustPlayed(APIView):
|
class JustPlayed(APIView):
|
||||||
|
@ -63,7 +64,7 @@ class NextRequest(RetrieveAPIView):
|
||||||
def retrieve(self, request):
|
def retrieve(self, request):
|
||||||
dj_profile = RadioProfile.objects.get(user__is_dj=True)
|
dj_profile = RadioProfile.objects.get(user__is_dj=True)
|
||||||
|
|
||||||
limit = get_setting('songs_per_jingle')
|
limit = radio_settings['general__songs_per_jingle']
|
||||||
queued_songs = SongRequest.music.get_queued_requests(limit)
|
queued_songs = SongRequest.music.get_queued_requests(limit)
|
||||||
if [j for j in queued_songs if j.song.is_jingle]:
|
if [j for j in queued_songs if j.song.is_jingle]:
|
||||||
if not SongRequest.music.new_requests().exists():
|
if not SongRequest.music.new_requests().exists():
|
||||||
|
|
92
savepointradio/core/dynamic_preferences_registry.py
Normal file
92
savepointradio/core/dynamic_preferences_registry.py
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
from django.forms import ValidationError
|
||||||
|
|
||||||
|
from dynamic_preferences.types import FloatPreference, IntegerPreference
|
||||||
|
from dynamic_preferences.preferences import Section
|
||||||
|
from dynamic_preferences.registries import global_preferences_registry
|
||||||
|
|
||||||
|
|
||||||
|
general = Section('general')
|
||||||
|
replay = Section('replay')
|
||||||
|
|
||||||
|
|
||||||
|
@global_preferences_registry.register
|
||||||
|
class MaxSongRequests(IntegerPreference):
|
||||||
|
section = general
|
||||||
|
name = 'max_song_requests'
|
||||||
|
help_text = (
|
||||||
|
'The maximum amount of requests a user can have queued at any given '
|
||||||
|
'time. This restriction does not apply to users who are designated '
|
||||||
|
'as staff.'
|
||||||
|
)
|
||||||
|
default = 5
|
||||||
|
required = True
|
||||||
|
|
||||||
|
def validate(self, value):
|
||||||
|
if value < 0:
|
||||||
|
raise ValidationError('Must be greater than 0.')
|
||||||
|
|
||||||
|
|
||||||
|
@global_preferences_registry.register
|
||||||
|
class SongsPerJingle(IntegerPreference):
|
||||||
|
section = general
|
||||||
|
name = 'songs_per_jingle'
|
||||||
|
help_text = 'The amount of songs that will be played between jingles.'
|
||||||
|
default = 30
|
||||||
|
required = True
|
||||||
|
|
||||||
|
def validate(self, value):
|
||||||
|
if value < 0:
|
||||||
|
raise ValidationError('Must be greater than 0.')
|
||||||
|
|
||||||
|
|
||||||
|
@global_preferences_registry.register
|
||||||
|
class ReplayRatio(FloatPreference):
|
||||||
|
section = replay
|
||||||
|
name = 'replay_ratio'
|
||||||
|
help_text = (
|
||||||
|
'This defines how long before a song can be played/requested again '
|
||||||
|
'once it\'s been played. The ratio is based on the total song length '
|
||||||
|
'of all the enabled, requestable songs in the radio playlist.\n\n'
|
||||||
|
'Example: If the total song length of the radio playlist is 432000 '
|
||||||
|
'seconds (5 days), then a ratio of 0.75 will mean that a song cannot '
|
||||||
|
'be played again for 324000 seconds '
|
||||||
|
'(0.75 * 432000 = 324000 seconds = 3 days, 18 hours).'
|
||||||
|
)
|
||||||
|
default = float(0.75)
|
||||||
|
required = True
|
||||||
|
|
||||||
|
def validate(self, value):
|
||||||
|
if value < 0.0 or value > 1.0:
|
||||||
|
raise ValidationError('Must be between 0.0 and 1.0, inclusive.')
|
||||||
|
|
||||||
|
|
||||||
|
@global_preferences_registry.register
|
||||||
|
class MinRatingsForVariance(IntegerPreference):
|
||||||
|
section = replay
|
||||||
|
name = 'min_ratings_for_variance'
|
||||||
|
help_text = (
|
||||||
|
'The minimum amount of ratings for the rating variance to take effect '
|
||||||
|
'on the replay ratios.'
|
||||||
|
)
|
||||||
|
default = 5
|
||||||
|
required = True
|
||||||
|
|
||||||
|
def validate(self, value):
|
||||||
|
if value < 0:
|
||||||
|
raise ValidationError('Must be greater than 0.')
|
||||||
|
|
||||||
|
|
||||||
|
@global_preferences_registry.register
|
||||||
|
class RatingVarianceRatio(FloatPreference):
|
||||||
|
section = replay
|
||||||
|
name = 'rating_variance_ratio'
|
||||||
|
help_text = (
|
||||||
|
'The range in which the replay ratio can be adjusted due to profile '
|
||||||
|
'ratings.'
|
||||||
|
)
|
||||||
|
default = float(0.20)
|
||||||
|
required = True
|
||||||
|
|
||||||
|
def validate(self, value):
|
||||||
|
if value < 0.0 or value > 1.0:
|
||||||
|
raise ValidationError('Must be between 0.0 and 1.0, inclusive.')
|
|
@ -1,4 +1,4 @@
|
||||||
# Generated by Django 3.0.2 on 2020-01-21 16:49
|
# Generated by Django 3.0.3 on 2020-02-16 01:40
|
||||||
|
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
import django.utils.timezone
|
import django.utils.timezone
|
||||||
|
@ -13,16 +13,6 @@ class Migration(migrations.Migration):
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.CreateModel(
|
|
||||||
name='Setting',
|
|
||||||
fields=[
|
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('name', models.CharField(max_length=64, unique=True, verbose_name='name')),
|
|
||||||
('description', models.TextField(blank=True, verbose_name='description')),
|
|
||||||
('setting_type', models.PositiveIntegerField(choices=[(0, 'Integer'), (1, 'Float'), (2, 'String'), (3, 'Bool')], default=0, verbose_name='variable type')),
|
|
||||||
('data', models.TextField(verbose_name='data')),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='RadioUser',
|
name='RadioUser',
|
||||||
fields=[
|
fields=[
|
||||||
|
|
|
@ -1,56 +0,0 @@
|
||||||
# Generated by Django 2.0 on 2017-12-28 15:16
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
def default_settings(apps, schema_editor):
|
|
||||||
SETTING_TYPES = {'Integer': 0, 'Float': 1, 'String': 2, 'Bool': 3}
|
|
||||||
Setting = apps.get_model('core', 'Setting')
|
|
||||||
db_alias = schema_editor.connection.alias
|
|
||||||
Setting.objects.using(db_alias).bulk_create([
|
|
||||||
Setting(name='max_song_requests',
|
|
||||||
description='The maximum amount of requests a user can have '
|
|
||||||
'queued at any given time. This restriction does '
|
|
||||||
'not apply to users who are designated as staff.',
|
|
||||||
setting_type=SETTING_TYPES['Integer'],
|
|
||||||
data='5'),
|
|
||||||
Setting(name='min_ratings_for_variance',
|
|
||||||
description='The minimum amount of ratings for the rating '
|
|
||||||
'variance to take effect on the replay ratios.',
|
|
||||||
setting_type=SETTING_TYPES['Integer'],
|
|
||||||
data='5'),
|
|
||||||
Setting(name='rating_variance_ratio',
|
|
||||||
description='The range in which the replay ratio can be '
|
|
||||||
'adjusted due to profile ratings.',
|
|
||||||
setting_type=SETTING_TYPES['Float'],
|
|
||||||
data='0.20'),
|
|
||||||
Setting(name='replay_ratio',
|
|
||||||
description='This defines how long before a song can be '
|
|
||||||
'played/requested again once it\'s been played. '
|
|
||||||
'The ratio is based on the total song length of '
|
|
||||||
'all the enabled, requestable songs in the radio '
|
|
||||||
'playlist. Example: If the total song length of '
|
|
||||||
'the radio playlist is 432000 seconds (5 days), '
|
|
||||||
'then a ratio of 0.75 will mean that a song '
|
|
||||||
'cannot be played again for 324000 seconds '
|
|
||||||
'(0.75 * 432000 = 324000 seconds = 3 days, 18 '
|
|
||||||
'hours).',
|
|
||||||
setting_type=SETTING_TYPES['Float'],
|
|
||||||
data='0.75'),
|
|
||||||
Setting(name='songs_per_jingle',
|
|
||||||
description='The amount of songs that will be played between '
|
|
||||||
'jingles.',
|
|
||||||
setting_type=SETTING_TYPES['Integer'],
|
|
||||||
data='30'),
|
|
||||||
])
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('core', '0002_create_dj_user'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.RunPython(default_settings),
|
|
||||||
]
|
|
|
@ -56,39 +56,3 @@ class RadioUser(AbstractBaseUser, PermissionsMixin):
|
||||||
name=self.name,
|
name=self.name,
|
||||||
email=self.email,
|
email=self.email,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class Setting(models.Model):
|
|
||||||
"""
|
|
||||||
A model for keeping track of dynamic settings while the site is online and
|
|
||||||
the radio is running.
|
|
||||||
"""
|
|
||||||
INTEGER = 0
|
|
||||||
FLOAT = 1
|
|
||||||
STRING = 2
|
|
||||||
BOOL = 3
|
|
||||||
TYPE_CHOICES = (
|
|
||||||
(INTEGER, 'Integer'),
|
|
||||||
(FLOAT, 'Float'),
|
|
||||||
(STRING, 'String'),
|
|
||||||
(BOOL, 'Bool'),
|
|
||||||
)
|
|
||||||
name = models.CharField(_('name'), max_length=64, unique=True)
|
|
||||||
description = models.TextField(_('description'), blank=True)
|
|
||||||
setting_type = models.PositiveIntegerField(_('variable type'),
|
|
||||||
choices=TYPE_CHOICES,
|
|
||||||
default=INTEGER)
|
|
||||||
data = models.TextField(_('data'))
|
|
||||||
|
|
||||||
def get(self):
|
|
||||||
if self.setting_type == self.INTEGER:
|
|
||||||
return int(self.data)
|
|
||||||
elif self.setting_type == self.FLOAT:
|
|
||||||
return float(self.data)
|
|
||||||
elif self.setting_type == self.BOOL:
|
|
||||||
return self.data == 'True'
|
|
||||||
else:
|
|
||||||
return self.data
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return '{}: {}'.format(self.name, self.data)
|
|
||||||
|
|
|
@ -14,8 +14,6 @@ from urllib.request import pathname2url, url2pathname
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from django.utils.encoding import iri_to_uri, uri_to_iri
|
from django.utils.encoding import iri_to_uri, uri_to_iri
|
||||||
|
|
||||||
from .models import Setting
|
|
||||||
|
|
||||||
|
|
||||||
GROUP_NT_UNC = r'file://[A-Za-z0-9!@#$%^&\'\)\(\.\-_{}~]+/'
|
GROUP_NT_UNC = r'file://[A-Za-z0-9!@#$%^&\'\)\(\.\-_{}~]+/'
|
||||||
|
|
||||||
|
@ -34,32 +32,6 @@ FILE_IRI_PATTERN = (
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_setting(name):
|
|
||||||
'''Helper function to get dynamic settings from the database.'''
|
|
||||||
setting = Setting.objects.get(name=name)
|
|
||||||
return setting.get()
|
|
||||||
|
|
||||||
|
|
||||||
def set_setting(name, value, setting_type=None):
|
|
||||||
'''Helper function to set dynamic settings from the database.'''
|
|
||||||
setting_types = {'Integer': 0, 'Float': 1, 'String': 2, 'Bool': 3}
|
|
||||||
try:
|
|
||||||
setting = Setting.objects.get(name=name)
|
|
||||||
setting.data = str(value)
|
|
||||||
if setting_type in setting_types:
|
|
||||||
setting.setting_type = setting_types[setting_type]
|
|
||||||
setting.save()
|
|
||||||
except ObjectDoesNotExist:
|
|
||||||
if setting_type in setting_types:
|
|
||||||
Setting.objects.create(name=name,
|
|
||||||
setting_type=setting_types[setting_type],
|
|
||||||
data=str(value))
|
|
||||||
else:
|
|
||||||
error_msg = 'New settings need type (Integer, Float, String, Bool)'
|
|
||||||
raise TypeError(error_msg)
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
def naturalize(text):
|
def naturalize(text):
|
||||||
'''
|
'''
|
||||||
Return a normalized unicode string, with removed starting articles, for use
|
Return a normalized unicode string, with removed starting articles, for use
|
||||||
|
|
|
@ -4,13 +4,17 @@ from django.core.validators import (MaxLengthValidator, MinValueValidator,
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from dynamic_preferences.registries import global_preferences_registry
|
||||||
|
|
||||||
from core.behaviors import Disableable, Timestampable
|
from core.behaviors import Disableable, Timestampable
|
||||||
from core.utils import get_setting
|
|
||||||
from radio.models import Song
|
from radio.models import Song
|
||||||
from .exceptions import MakeRequestError
|
from .exceptions import MakeRequestError
|
||||||
from .managers import RequestManager
|
from .managers import RequestManager
|
||||||
|
|
||||||
|
|
||||||
|
radio_settings = global_preferences_registry.manager()
|
||||||
|
|
||||||
|
|
||||||
class RadioProfile(Disableable, Timestampable, models.Model):
|
class RadioProfile(Disableable, Timestampable, models.Model):
|
||||||
user = models.OneToOneField(settings.AUTH_USER_MODEL,
|
user = models.OneToOneField(settings.AUTH_USER_MODEL,
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
|
@ -38,7 +42,7 @@ class RadioProfile(Disableable, Timestampable, models.Model):
|
||||||
|
|
||||||
def has_reached_request_max(self):
|
def has_reached_request_max(self):
|
||||||
self_requests = SongRequest.music.unplayed().filter(profile=self)
|
self_requests = SongRequest.music.unplayed().filter(profile=self)
|
||||||
max_requests = get_setting('max_song_requests')
|
max_requests = radio_settings['general__max_song_requests']
|
||||||
return self_requests.count() >= max_requests
|
return self_requests.count() >= max_requests
|
||||||
|
|
||||||
def can_request(self):
|
def can_request(self):
|
||||||
|
@ -59,7 +63,7 @@ class RadioProfile(Disableable, Timestampable, models.Model):
|
||||||
raise MakeRequestError('User is currently disabled.')
|
raise MakeRequestError('User is currently disabled.')
|
||||||
|
|
||||||
if self.has_reached_request_max() and not self.user.is_staff:
|
if self.has_reached_request_max() and not self.user.is_staff:
|
||||||
max_requests = get_setting('max_song_requests')
|
max_requests = radio_settings['general__max_song_requests']
|
||||||
message = 'User has reached the maximum request limit ({}).'
|
message = 'User has reached the maximum request limit ({}).'
|
||||||
raise MakeRequestError(message.format(max_requests))
|
raise MakeRequestError(message.format(max_requests))
|
||||||
|
|
||||||
|
|
|
@ -10,13 +10,16 @@ from django.apps import apps
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
from core.utils import get_setting
|
from dynamic_preferences.registries import global_preferences_registry
|
||||||
|
|
||||||
from .querysets import RadioQuerySet, SongQuerySet
|
from .querysets import RadioQuerySet, SongQuerySet
|
||||||
|
|
||||||
|
|
||||||
# Set decimal precision
|
# Set decimal precision
|
||||||
getcontext().prec = 16
|
getcontext().prec = 16
|
||||||
|
|
||||||
|
radio_settings = global_preferences_registry.manager()
|
||||||
|
|
||||||
|
|
||||||
class RadioManager(models.Manager):
|
class RadioManager(models.Manager):
|
||||||
'''
|
'''
|
||||||
|
@ -97,7 +100,8 @@ class SongManager(RadioManager):
|
||||||
Default length in seconds before a song can be played again. This is
|
Default length in seconds before a song can be played again. This is
|
||||||
based on the replay ratio set in the application settings.
|
based on the replay ratio set in the application settings.
|
||||||
'''
|
'''
|
||||||
total_ratio = get_setting('replay_ratio') + adjusted_ratio
|
replay_ratio = radio_settings['replay__replay_ratio']
|
||||||
|
total_ratio = replay_ratio + adjusted_ratio
|
||||||
wait = self.playlist_length() * Decimal(total_ratio)
|
wait = self.playlist_length() * Decimal(total_ratio)
|
||||||
wait = wait.quantize(Decimal('.01'), rounding=ROUND_UP)
|
wait = wait.quantize(Decimal('.01'), rounding=ROUND_UP)
|
||||||
return timedelta(seconds=float(wait))
|
return timedelta(seconds=float(wait))
|
||||||
|
|
|
@ -11,8 +11,9 @@ from django.db import models
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from dynamic_preferences.registries import global_preferences_registry
|
||||||
|
|
||||||
from core.behaviors import Disableable, Publishable, Timestampable
|
from core.behaviors import Disableable, Publishable, Timestampable
|
||||||
from core.utils import get_setting
|
|
||||||
from .fields import RadioIRIField
|
from .fields import RadioIRIField
|
||||||
from .managers import RadioManager, SongManager
|
from .managers import RadioManager, SongManager
|
||||||
|
|
||||||
|
@ -20,6 +21,8 @@ from .managers import RadioManager, SongManager
|
||||||
# Set decimal precision
|
# Set decimal precision
|
||||||
getcontext().prec = 16
|
getcontext().prec = 16
|
||||||
|
|
||||||
|
radio_settings = global_preferences_registry.manager()
|
||||||
|
|
||||||
|
|
||||||
class Album(Disableable, Publishable, Timestampable, models.Model):
|
class Album(Disableable, Publishable, Timestampable, models.Model):
|
||||||
'''
|
'''
|
||||||
|
@ -265,13 +268,13 @@ class Song(Disableable, Publishable, Timestampable, models.Model):
|
||||||
if self._is_song() and self._is_available():
|
if self._is_song() and self._is_available():
|
||||||
if last:
|
if last:
|
||||||
# Check if we have enough ratings to change ratio
|
# Check if we have enough ratings to change ratio
|
||||||
min_ratings = get_setting('min_ratings_for_variance')
|
min_rate = radio_settings['replay__min_ratings_for_variance']
|
||||||
if self.rating_set.count() >= min_ratings:
|
if self.rating_set.count() >= min_rate:
|
||||||
rate_ratio = get_setting('rating_variance_ratio')
|
ratio = radio_settings['replay__rating_variance_ratio']
|
||||||
|
|
||||||
# -((average - 1)/(highest_rating - 1)) * rating_ratio
|
# -((average - 1)/(highest_rating - 1)) * ratio
|
||||||
base = -((self._average_rating() - 1) / 4) * rate_ratio
|
base = -((self._average_rating() - 1) / 4) * ratio
|
||||||
adjusted_ratio = float(base + (rate_ratio * 0.5))
|
adjusted_ratio = float(base + (ratio * 0.5))
|
||||||
else:
|
else:
|
||||||
adjusted_ratio = float(0.0)
|
adjusted_ratio = float(0.0)
|
||||||
|
|
||||||
|
|
|
@ -57,6 +57,7 @@ INSTALLED_APPS = [
|
||||||
'django.contrib.messages',
|
'django.contrib.messages',
|
||||||
'django.contrib.staticfiles',
|
'django.contrib.staticfiles',
|
||||||
|
|
||||||
|
'dynamic_preferences',
|
||||||
'rest_framework',
|
'rest_framework',
|
||||||
'rest_framework.authtoken',
|
'rest_framework.authtoken',
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue