diff --git a/requirements.txt b/requirements.txt index 0593b0a..c054422 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,8 +3,10 @@ asgiref>=3.2.3 cffi>=1.13.2 dj-database-url>=0.5.0 Django>=3.0.2 +django-dynamic-preferences>=1.8.1 django-inline-actions>=2.3.0 djangorestframework>=3.11.0 +persisting-theory>=0.2.1 psycopg2-binary>=2.8.4 pycparser>=2.19 python-decouple>=3.3 diff --git a/savepointradio/api/views/controls.py b/savepointradio/api/views/controls.py index 08a5fd7..5937997 100644 --- a/savepointradio/api/views/controls.py +++ b/savepointradio/api/views/controls.py @@ -1,6 +1,7 @@ from django.contrib.auth import get_user_model from django.utils import timezone +from dynamic_preferences.registries import global_preferences_registry from rest_framework import status from rest_framework.authentication import (SessionAuthentication, TokenAuthentication) @@ -10,7 +11,6 @@ from rest_framework.renderers import BrowsableAPIRenderer, JSONRenderer from rest_framework.response import Response from rest_framework.views import APIView -from core.utils import get_setting from profiles.exceptions import MakeRequestError from profiles.models import RadioProfile, SongRequest from radio.models import Song @@ -22,6 +22,7 @@ from ..serializers.controls import (JustPlayedSerializer, User = get_user_model() +radio_settings = global_preferences_registry.manager() class JustPlayed(APIView): @@ -63,7 +64,7 @@ class NextRequest(RetrieveAPIView): def retrieve(self, request): 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) if [j for j in queued_songs if j.song.is_jingle]: if not SongRequest.music.new_requests().exists(): diff --git a/savepointradio/core/dynamic_preferences_registry.py b/savepointradio/core/dynamic_preferences_registry.py new file mode 100644 index 0000000..a731960 --- /dev/null +++ b/savepointradio/core/dynamic_preferences_registry.py @@ -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.') diff --git a/savepointradio/core/migrations/0001_initial.py b/savepointradio/core/migrations/0001_initial.py index cf5d06f..c51c25c 100644 --- a/savepointradio/core/migrations/0001_initial.py +++ b/savepointradio/core/migrations/0001_initial.py @@ -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 import django.utils.timezone @@ -13,16 +13,6 @@ class Migration(migrations.Migration): ] 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( name='RadioUser', fields=[ diff --git a/savepointradio/core/migrations/0003_default_settings.py b/savepointradio/core/migrations/0003_default_settings.py deleted file mode 100644 index c0aa8c3..0000000 --- a/savepointradio/core/migrations/0003_default_settings.py +++ /dev/null @@ -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), - ] diff --git a/savepointradio/core/models.py b/savepointradio/core/models.py index 494bd8f..0f30f2d 100644 --- a/savepointradio/core/models.py +++ b/savepointradio/core/models.py @@ -56,39 +56,3 @@ class RadioUser(AbstractBaseUser, PermissionsMixin): name=self.name, 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) diff --git a/savepointradio/core/utils.py b/savepointradio/core/utils.py index 26a4d8d..a459dc9 100644 --- a/savepointradio/core/utils.py +++ b/savepointradio/core/utils.py @@ -14,8 +14,6 @@ from urllib.request import pathname2url, url2pathname from django.core.exceptions import ObjectDoesNotExist from django.utils.encoding import iri_to_uri, uri_to_iri -from .models import Setting - 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): ''' Return a normalized unicode string, with removed starting articles, for use diff --git a/savepointradio/profiles/models.py b/savepointradio/profiles/models.py index d1e7971..99e48dd 100644 --- a/savepointradio/profiles/models.py +++ b/savepointradio/profiles/models.py @@ -4,13 +4,17 @@ from django.core.validators import (MaxLengthValidator, MinValueValidator, from django.db import models from django.utils.translation import gettext_lazy as _ +from dynamic_preferences.registries import global_preferences_registry + from core.behaviors import Disableable, Timestampable -from core.utils import get_setting from radio.models import Song from .exceptions import MakeRequestError from .managers import RequestManager +radio_settings = global_preferences_registry.manager() + + class RadioProfile(Disableable, Timestampable, models.Model): user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, @@ -38,7 +42,7 @@ class RadioProfile(Disableable, Timestampable, models.Model): def has_reached_request_max(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 def can_request(self): @@ -59,7 +63,7 @@ class RadioProfile(Disableable, Timestampable, models.Model): raise MakeRequestError('User is currently disabled.') 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 ({}).' raise MakeRequestError(message.format(max_requests)) diff --git a/savepointradio/radio/managers.py b/savepointradio/radio/managers.py index 41ae5e8..6c3af9b 100644 --- a/savepointradio/radio/managers.py +++ b/savepointradio/radio/managers.py @@ -10,13 +10,16 @@ from django.apps import apps from django.db import models from django.utils import timezone -from core.utils import get_setting +from dynamic_preferences.registries import global_preferences_registry + from .querysets import RadioQuerySet, SongQuerySet # Set decimal precision getcontext().prec = 16 +radio_settings = global_preferences_registry.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 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 = wait.quantize(Decimal('.01'), rounding=ROUND_UP) return timedelta(seconds=float(wait)) diff --git a/savepointradio/radio/models.py b/savepointradio/radio/models.py index 78899a9..63d7f9b 100644 --- a/savepointradio/radio/models.py +++ b/savepointradio/radio/models.py @@ -11,8 +11,9 @@ from django.db import models from django.utils import timezone 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.utils import get_setting from .fields import RadioIRIField from .managers import RadioManager, SongManager @@ -20,6 +21,8 @@ from .managers import RadioManager, SongManager # Set decimal precision getcontext().prec = 16 +radio_settings = global_preferences_registry.manager() + 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 last: # Check if we have enough ratings to change ratio - min_ratings = get_setting('min_ratings_for_variance') - if self.rating_set.count() >= min_ratings: - rate_ratio = get_setting('rating_variance_ratio') + min_rate = radio_settings['replay__min_ratings_for_variance'] + if self.rating_set.count() >= min_rate: + ratio = radio_settings['replay__rating_variance_ratio'] - # -((average - 1)/(highest_rating - 1)) * rating_ratio - base = -((self._average_rating() - 1) / 4) * rate_ratio - adjusted_ratio = float(base + (rate_ratio * 0.5)) + # -((average - 1)/(highest_rating - 1)) * ratio + base = -((self._average_rating() - 1) / 4) * ratio + adjusted_ratio = float(base + (ratio * 0.5)) else: adjusted_ratio = float(0.0) diff --git a/savepointradio/savepointradio/settings.py b/savepointradio/savepointradio/settings.py index e2f0151..e998a23 100644 --- a/savepointradio/savepointradio/settings.py +++ b/savepointradio/savepointradio/settings.py @@ -57,6 +57,7 @@ INSTALLED_APPS = [ 'django.contrib.messages', 'django.contrib.staticfiles', + 'dynamic_preferences', 'rest_framework', 'rest_framework.authtoken',