- various __all__
- serializer: track search, reorder module files
- autocomplete: allow simple string value selection
- playlist editor:
    - ui & flow improve
    - init data
    - save user settings
    - autocomplete
    - fix bugs
    - discard changes
			
			
This commit is contained in:
		@ -1,10 +1,11 @@
 | 
			
		||||
from .article import Article
 | 
			
		||||
from .page import Category, Page, StaticPage, Comment, NavItem
 | 
			
		||||
from .program import Program, Stream, Schedule
 | 
			
		||||
from .episode import Episode, Diffusion
 | 
			
		||||
from .log import Log
 | 
			
		||||
from .sound import Sound, Track
 | 
			
		||||
from .station import Station, Port
 | 
			
		||||
from .article import *
 | 
			
		||||
from .page import *
 | 
			
		||||
from .program import *
 | 
			
		||||
from .episode import *
 | 
			
		||||
from .log import *
 | 
			
		||||
from .sound import *
 | 
			
		||||
from .station import *
 | 
			
		||||
from .user_settings import *
 | 
			
		||||
 | 
			
		||||
from . import signals
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,8 +1,10 @@
 | 
			
		||||
from django.db import models
 | 
			
		||||
from django.utils.translation import gettext_lazy as _
 | 
			
		||||
 | 
			
		||||
from .page import Page, PageQuerySet
 | 
			
		||||
from .program import Program, ProgramChildQuerySet
 | 
			
		||||
from .page import Page
 | 
			
		||||
from .program import ProgramChildQuerySet
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
__all__ = ('Article',)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Article(Page):
 | 
			
		||||
 | 
			
		||||
@ -9,12 +9,12 @@ from django.utils.functional import cached_property
 | 
			
		||||
from easy_thumbnails.files import get_thumbnailer
 | 
			
		||||
 | 
			
		||||
from aircox import settings, utils
 | 
			
		||||
from .program import Program, ProgramChildQuerySet, \
 | 
			
		||||
from .program import ProgramChildQuerySet, \
 | 
			
		||||
        BaseRerun, BaseRerunQuerySet, Schedule
 | 
			
		||||
from .page import Page, PageQuerySet
 | 
			
		||||
from .page import Page
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
__all__ = ['Episode', 'Diffusion', 'DiffusionQuerySet']
 | 
			
		||||
__all__ = ('Episode', 'Diffusion', 'DiffusionQuerySet')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Episode(Page):
 | 
			
		||||
@ -31,9 +31,9 @@ class Episode(Page):
 | 
			
		||||
        """ Return serialized data about podcasts. """
 | 
			
		||||
        from ..serializers import PodcastSerializer
 | 
			
		||||
        podcasts = [PodcastSerializer(s).data
 | 
			
		||||
                    for s in self.sound_set.public().order_by('type') ]
 | 
			
		||||
                    for s in self.sound_set.public().order_by('type')]
 | 
			
		||||
        if self.cover:
 | 
			
		||||
            options = {'size': (128,128), 'crop':'scale'}
 | 
			
		||||
            options = {'size': (128, 128), 'crop': 'scale'}
 | 
			
		||||
            cover = get_thumbnailer(self.cover).get_thumbnail(options).url
 | 
			
		||||
        else:
 | 
			
		||||
            cover = None
 | 
			
		||||
@ -84,7 +84,7 @@ class DiffusionQuerySet(BaseRerunQuerySet):
 | 
			
		||||
    def episode(self, episode=None, id=None):
 | 
			
		||||
        """ Diffusions for this episode """
 | 
			
		||||
        return self.filter(episode=episode) if id is None else \
 | 
			
		||||
               self.filter(episode__id=id)
 | 
			
		||||
            self.filter(episode__id=id)
 | 
			
		||||
 | 
			
		||||
    def on_air(self):
 | 
			
		||||
        """ On air diffusions """
 | 
			
		||||
@ -104,13 +104,13 @@ class DiffusionQuerySet(BaseRerunQuerySet):
 | 
			
		||||
        end = tz.datetime.combine(date, datetime.time(23, 59, 59, 999))
 | 
			
		||||
        # start = tz.get_current_timezone().localize(start)
 | 
			
		||||
        # end = tz.get_current_timezone().localize(end)
 | 
			
		||||
        qs = self.filter(start__range = (start, end))
 | 
			
		||||
        qs = self.filter(start__range=(start, end))
 | 
			
		||||
        return qs.order_by('start') if order else qs
 | 
			
		||||
 | 
			
		||||
    def at(self, date, order=True):
 | 
			
		||||
        """ Return diffusions at specified date or datetime """
 | 
			
		||||
        return self.now(date, order) if isinstance(date, tz.datetime) else \
 | 
			
		||||
               self.date(date, order)
 | 
			
		||||
            self.date(date, order)
 | 
			
		||||
 | 
			
		||||
    def after(self, date=None):
 | 
			
		||||
        """
 | 
			
		||||
@ -201,7 +201,7 @@ class Diffusion(BaseRerun):
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        str_ = '{episode} - {date}'.format(
 | 
			
		||||
            self=self, episode=self.episode and self.episode.title,
 | 
			
		||||
            episode=self.episode and self.episode.title,
 | 
			
		||||
            date=self.local_start.strftime('%Y/%m/%d %H:%M%z'),
 | 
			
		||||
        )
 | 
			
		||||
        if self.initial:
 | 
			
		||||
@ -324,5 +324,3 @@ class Diffusion(BaseRerun):
 | 
			
		||||
            'end': self.end,
 | 
			
		||||
            'episode': getattr(self, 'episode', None),
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -20,7 +20,7 @@ from .station import Station
 | 
			
		||||
logger = logging.getLogger('aircox')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
__all__ = ['Log', 'LogQuerySet', 'LogArchiver']
 | 
			
		||||
__all__ = ('Log', 'LogQuerySet', 'LogArchiver')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class LogQuerySet(models.QuerySet):
 | 
			
		||||
@ -31,7 +31,7 @@ class LogQuerySet(models.QuerySet):
 | 
			
		||||
    def date(self, date):
 | 
			
		||||
        start = tz.datetime.combine(date, datetime.time())
 | 
			
		||||
        end = tz.datetime.combine(date, datetime.time(23, 59, 59, 999))
 | 
			
		||||
        return self.filter(date__range = (start, end))
 | 
			
		||||
        return self.filter(date__range=(start, end))
 | 
			
		||||
        # this filter does not work with mysql
 | 
			
		||||
        # return self.filter(date__date=date)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,3 @@
 | 
			
		||||
from enum import IntEnum
 | 
			
		||||
import re
 | 
			
		||||
 | 
			
		||||
from django.db import models
 | 
			
		||||
@ -18,7 +17,8 @@ from model_utils.managers import InheritanceQuerySet
 | 
			
		||||
from .station import Station
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
__all__ = ['Category', 'PageQuerySet', 'Page', 'Comment', 'NavItem']
 | 
			
		||||
__all__ = ('Category', 'PageQuerySet',
 | 
			
		||||
           'Page', 'StaticPage', 'Comment', 'NavItem')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
headline_re = re.compile(r'(<p>)?'
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,5 @@
 | 
			
		||||
import calendar
 | 
			
		||||
from collections import OrderedDict
 | 
			
		||||
import datetime
 | 
			
		||||
from enum import IntEnum
 | 
			
		||||
import logging
 | 
			
		||||
import os
 | 
			
		||||
@ -10,7 +9,7 @@ import pytz
 | 
			
		||||
from django.conf import settings as conf
 | 
			
		||||
from django.core.exceptions import ValidationError
 | 
			
		||||
from django.db import models
 | 
			
		||||
from django.db.models import F, Q
 | 
			
		||||
from django.db.models import F
 | 
			
		||||
from django.db.models.functions import Concat, Substr
 | 
			
		||||
from django.utils import timezone as tz
 | 
			
		||||
from django.utils.translation import gettext_lazy as _
 | 
			
		||||
@ -24,8 +23,8 @@ from .station import Station
 | 
			
		||||
logger = logging.getLogger('aircox')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
__all__ = ['Program', 'ProgramQuerySet', 'Stream', 'Schedule',
 | 
			
		||||
           'ProgramChildQuerySet', 'BaseRerun', 'BaseRerunQuerySet']
 | 
			
		||||
__all__ = ('Program', 'ProgramQuerySet', 'Stream', 'Schedule',
 | 
			
		||||
           'ProgramChildQuerySet', 'BaseRerun', 'BaseRerunQuerySet')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ProgramQuerySet(PageQuerySet):
 | 
			
		||||
 | 
			
		||||
@ -2,7 +2,7 @@ import pytz
 | 
			
		||||
 | 
			
		||||
from django.contrib.auth.models import User, Group, Permission
 | 
			
		||||
from django.db import transaction
 | 
			
		||||
from django.db.models import F, signals
 | 
			
		||||
from django.db.models import signals
 | 
			
		||||
from django.dispatch import receiver
 | 
			
		||||
from django.utils import timezone as tz
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,17 +1,14 @@
 | 
			
		||||
from enum import IntEnum
 | 
			
		||||
import logging
 | 
			
		||||
import os
 | 
			
		||||
 | 
			
		||||
from django.conf import settings as conf
 | 
			
		||||
from django.db import models
 | 
			
		||||
from django.db.models import Q, Value as V
 | 
			
		||||
from django.db.models.functions import Concat
 | 
			
		||||
from django.db.models import Q
 | 
			
		||||
from django.utils import timezone as tz
 | 
			
		||||
from django.utils.translation import gettext_lazy as _
 | 
			
		||||
 | 
			
		||||
from taggit.managers import TaggableManager
 | 
			
		||||
 | 
			
		||||
from aircox import settings
 | 
			
		||||
from .program import Program
 | 
			
		||||
from .episode import Episode
 | 
			
		||||
 | 
			
		||||
@ -19,7 +16,7 @@ from .episode import Episode
 | 
			
		||||
logger = logging.getLogger('aircox')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
__all__ = ['Sound', 'SoundQuerySet', 'Track']
 | 
			
		||||
__all__ = ('Sound', 'SoundQuerySet', 'Track')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SoundQuerySet(models.QuerySet):
 | 
			
		||||
 | 
			
		||||
@ -8,7 +8,7 @@ from filer.fields.image import FilerImageField
 | 
			
		||||
from .. import settings
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
__all__ = ['Station', 'StationQuerySet', 'Port']
 | 
			
		||||
__all__ = ('Station', 'StationQuerySet', 'Port')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class StationQuerySet(models.QuerySet):
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										16
									
								
								aircox/models/user_settings.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								aircox/models/user_settings.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,16 @@
 | 
			
		||||
from django.db import models
 | 
			
		||||
from django.contrib.auth.models import User
 | 
			
		||||
from django.utils.translation import gettext_lazy as _
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UserSettings(models.Model):
 | 
			
		||||
    """
 | 
			
		||||
    Store user's settings.
 | 
			
		||||
    """
 | 
			
		||||
    user = models.OneToOneField(
 | 
			
		||||
        User, models.CASCADE, verbose_name=_('User'),
 | 
			
		||||
        related_name='aircox_settings')
 | 
			
		||||
    playlist_editor_columns = models.JSONField(
 | 
			
		||||
        _('Playlist Editor Columns'))
 | 
			
		||||
    playlist_editor_sep = models.CharField(
 | 
			
		||||
        _('Playlist Editor Separator'), max_length=16)
 | 
			
		||||
							
								
								
									
										3
									
								
								aircox/serializers/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								aircox/serializers/__init__.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,3 @@
 | 
			
		||||
from .log import *
 | 
			
		||||
from .sound import *
 | 
			
		||||
from .admin import *
 | 
			
		||||
							
								
								
									
										30
									
								
								aircox/serializers/admin.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								aircox/serializers/admin.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,30 @@
 | 
			
		||||
from rest_framework import serializers
 | 
			
		||||
from taggit.serializers import TagListSerializerField, TaggitSerializer
 | 
			
		||||
 | 
			
		||||
from ..models import Track, UserSettings
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
__all__ = ('TrackSerializer', 'UserSettingsSerializer')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TrackSerializer(TaggitSerializer, serializers.ModelSerializer):
 | 
			
		||||
    tags = TagListSerializerField()
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Track
 | 
			
		||||
        fields = ('pk', 'artist', 'title', 'album', 'year', 'position',
 | 
			
		||||
                  'info', 'tags', 'episode', 'sound')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UserSettingsSerializer(serializers.ModelSerializer):
 | 
			
		||||
    # TODO: validate fields values (playlist_editor_columns at least)
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = UserSettings
 | 
			
		||||
        fields = ('playlist_editor_columns', 'playlist_editor_sep')
 | 
			
		||||
 | 
			
		||||
    def create(self, validated_data):
 | 
			
		||||
        user = self.context.get('user')
 | 
			
		||||
        if user:
 | 
			
		||||
            validated_data['user_id'] = user.id
 | 
			
		||||
        return super().create(validated_data)
 | 
			
		||||
        
 | 
			
		||||
@ -1,12 +1,9 @@
 | 
			
		||||
from rest_framework import serializers
 | 
			
		||||
from taggit.serializers import TagListSerializerField, TaggitSerializer
 | 
			
		||||
 | 
			
		||||
from .models import Diffusion, Log, Sound, Track
 | 
			
		||||
from ..models import Diffusion, Log
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
__all__ = ['LogInfo', 'LogInfoSerializer', 'SoundSerializer',
 | 
			
		||||
           'PodcastSerializer',
 | 
			
		||||
           'AdminTrackSerializer']
 | 
			
		||||
__all__ = ('LogInfo', 'LogInfoSerializer')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class LogInfo:
 | 
			
		||||
@ -54,30 +51,3 @@ class LogInfoSerializer(serializers.Serializer):
 | 
			
		||||
    info = serializers.CharField(max_length=200, required=False)
 | 
			
		||||
    url = serializers.URLField(required=False)
 | 
			
		||||
    cover = serializers.URLField(required=False)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SoundSerializer(serializers.ModelSerializer):
 | 
			
		||||
    file = serializers.FileField(use_url=False)
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Sound
 | 
			
		||||
        fields = ['pk', 'name', 'program', 'episode', 'type', 'file',
 | 
			
		||||
                  'duration', 'mtime', 'is_good_quality', 'is_public', 'url']
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PodcastSerializer(serializers.ModelSerializer):
 | 
			
		||||
    # serializers.HyperlinkedIdentityField(view_name='sound', format='html')
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Sound
 | 
			
		||||
        fields = ['pk', 'name', 'program', 'episode', 'type',
 | 
			
		||||
                  'duration', 'mtime', 'url', 'is_downloadable']
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AdminTrackSerializer(TaggitSerializer, serializers.ModelSerializer):
 | 
			
		||||
    tags = TagListSerializerField()
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Track
 | 
			
		||||
        fields = ('pk', 'artist', 'title', 'album', 'year', 'position',
 | 
			
		||||
                  'info', 'tags', 'episode', 'sound')
 | 
			
		||||
							
								
								
									
										21
									
								
								aircox/serializers/sound.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								aircox/serializers/sound.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,21 @@
 | 
			
		||||
from rest_framework import serializers
 | 
			
		||||
 | 
			
		||||
from ..models import Sound
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SoundSerializer(serializers.ModelSerializer):
 | 
			
		||||
    file = serializers.FileField(use_url=False)
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Sound
 | 
			
		||||
        fields = ['pk', 'name', 'program', 'episode', 'type', 'file',
 | 
			
		||||
                  'duration', 'mtime', 'is_good_quality', 'is_public', 'url']
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PodcastSerializer(serializers.ModelSerializer):
 | 
			
		||||
    # serializers.HyperlinkedIdentityField(view_name='sound', format='html')
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Sound
 | 
			
		||||
        fields = ['pk', 'name', 'program', 'episode', 'type',
 | 
			
		||||
                  'duration', 'mtime', 'url', 'is_downloadable']
 | 
			
		||||
@ -16,7 +16,7 @@
 | 
			
		||||
  \**********************/
 | 
			
		||||
/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
 | 
			
		||||
 | 
			
		||||
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _assets_styles_scss__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./assets/styles.scss */ \"./src/assets/styles.scss\");\n/* harmony import */ var _assets_admin_scss__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./assets/admin.scss */ \"./src/assets/admin.scss\");\n/* harmony import */ var _index_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./index.js */ \"./src/index.js\");\n/* harmony import */ var _app__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./app */ \"./src/app.js\");\n/* harmony import */ var _components__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./components */ \"./src/components/index.js\");\n\n\n\n\n\nconst AdminApp = {\n  ..._app__WEBPACK_IMPORTED_MODULE_3__[\"default\"],\n  components: {\n    ..._app__WEBPACK_IMPORTED_MODULE_3__[\"default\"].components,\n    ..._components__WEBPACK_IMPORTED_MODULE_4__.admin\n  }\n};\n/* harmony default export */ __webpack_exports__[\"default\"] = (AdminApp);\nwindow.App = AdminApp;\n\n//# sourceURL=webpack://aircox-assets/./src/admin.js?");
 | 
			
		||||
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _assets_styles_scss__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./assets/styles.scss */ \"./src/assets/styles.scss\");\n/* harmony import */ var _assets_admin_scss__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./assets/admin.scss */ \"./src/assets/admin.scss\");\n/* harmony import */ var _index_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./index.js */ \"./src/index.js\");\n/* harmony import */ var _app__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./app */ \"./src/app.js\");\n/* harmony import */ var _components__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./components */ \"./src/components/index.js\");\n/* harmony import */ var _track__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ./track */ \"./src/track.js\");\n\n\n\n\n\n\nconst AdminApp = {\n  ..._app__WEBPACK_IMPORTED_MODULE_3__[\"default\"],\n  components: {\n    ..._app__WEBPACK_IMPORTED_MODULE_3__[\"default\"].components,\n    ..._components__WEBPACK_IMPORTED_MODULE_4__.admin\n  },\n  data() {\n    return {\n      ...super.data,\n      Track: _track__WEBPACK_IMPORTED_MODULE_5__[\"default\"]\n    };\n  }\n};\n/* harmony default export */ __webpack_exports__[\"default\"] = (AdminApp);\nwindow.App = AdminApp;\n\n//# sourceURL=webpack://aircox-assets/./src/admin.js?");
 | 
			
		||||
 | 
			
		||||
/***/ }),
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							@ -1,17 +1,16 @@
 | 
			
		||||
{% comment %}Inline block to edit playlists{% endcomment %}
 | 
			
		||||
{% load aircox aircox_admin static i18n %}
 | 
			
		||||
 | 
			
		||||
{# include "adminsortable2/edit_inline/tabular-django-4.1.html" #}
 | 
			
		||||
{% with inline_admin_formset as admin_formset %}
 | 
			
		||||
{% with admin_formset.formset as formset %}
 | 
			
		||||
 | 
			
		||||
<script id="{{ formset.prefix }}-init-data">
 | 
			
		||||
{{ formset|inline_data|json }}
 | 
			
		||||
</script>
 | 
			
		||||
<div id="inline-tracks" class="box mb-5">
 | 
			
		||||
    {{ admin_formset.non_form_errors }}
 | 
			
		||||
 | 
			
		||||
    <a-playlist-editor data-el="{{ formset.prefix }}-init-data"
 | 
			
		||||
    <a-playlist-editor
 | 
			
		||||
            :labels="{% track_inline_labels %}"
 | 
			
		||||
            :init-data="{% track_inline_data formset=formset %}"
 | 
			
		||||
            settings-url="{% url "api:user-settings" %}"
 | 
			
		||||
            data-prefix="{{ formset.prefix }}-">
 | 
			
		||||
        <template #title>
 | 
			
		||||
            <h5 class="title is-4">{% trans "Playlist" %}</h5>
 | 
			
		||||
@ -57,13 +56,21 @@
 | 
			
		||||
        {% if not field.widget.is_hidden and not field.is_readonly %}
 | 
			
		||||
        <template v-slot:row-{{ field.name }}="{item,col,row,value,attr,emit}">
 | 
			
		||||
            <div class="field">
 | 
			
		||||
                {% if field.name in 'artist,title,album' %}
 | 
			
		||||
                <a-autocomplete
 | 
			
		||||
                        :input-class="['input', item.error(attr) ? 'is-danger' : 'half-field']"
 | 
			
		||||
                        url="{% url 'api:track-autocomplete' %}?{{ field.name }}=${query}&field={{ field.name }}"
 | 
			
		||||
                {% else %}
 | 
			
		||||
                <div class="control">
 | 
			
		||||
                    <input type="{{ widget.type }}"
 | 
			
		||||
                        :class="['input', item.error(attr) ? 'is-danger' : 'half-field']"
 | 
			
		||||
                {% endif %}
 | 
			
		||||
                        :name="'{{ formset.prefix }}-' + row + '-{{ field.name }}'"
 | 
			
		||||
                        v-model="item.data[attr]"
 | 
			
		||||
                        @change="emit('change', col)"/>
 | 
			
		||||
                {% if field.name not in 'artist,title,album' %}
 | 
			
		||||
                </div>
 | 
			
		||||
                {% endif %}
 | 
			
		||||
                <p v-for="error in item.error(attr)" class="help is-danger">
 | 
			
		||||
                    [[ error ]] !
 | 
			
		||||
                </p>
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,14 @@
 | 
			
		||||
import json
 | 
			
		||||
from django import template
 | 
			
		||||
from django.contrib import admin
 | 
			
		||||
from django.utils.safestring import mark_safe
 | 
			
		||||
from django.utils.translation import gettext_lazy as _, gettext as __
 | 
			
		||||
 | 
			
		||||
from aircox.serializers.admin import UserSettingsSerializer
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
__all__ = ('register', 'do_get_admin_tools', 'do_track_inline_data',
 | 
			
		||||
           'do_track_inline_column_labels')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
register = template.Library()
 | 
			
		||||
@ -10,8 +19,14 @@ def do_get_admin_tools():
 | 
			
		||||
    return admin.site.get_tools()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register.filter(name='inline_data')
 | 
			
		||||
def do_inline_data(formset):
 | 
			
		||||
@register.simple_tag(name='track_inline_data', takes_context=True)
 | 
			
		||||
def do_track_inline_data(context, formset, safe_string=False):
 | 
			
		||||
    """
 | 
			
		||||
    Return initial data for playlist editor as dict. Keys are:
 | 
			
		||||
    - ``items``: list of items. Extra keys:
 | 
			
		||||
        - ``__error__``: dict of form fields errors
 | 
			
		||||
    - ``settings``: user's settings
 | 
			
		||||
    """
 | 
			
		||||
    items = []
 | 
			
		||||
    for form in formset.forms:
 | 
			
		||||
        item = {name: form[name].value()
 | 
			
		||||
@ -23,5 +38,23 @@ def do_inline_data(formset):
 | 
			
		||||
        if tags and not isinstance(tags, str):
 | 
			
		||||
            item['tags'] = ', '.join(tag.name for tag in tags)
 | 
			
		||||
        items.append(item)
 | 
			
		||||
    return {"items": items}
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    data = {"items": items}
 | 
			
		||||
    user = context['request'].user
 | 
			
		||||
    settings = getattr(user, 'aircox_settings', None)
 | 
			
		||||
    data['settings'] = settings and UserSettingsSerializer(settings).data
 | 
			
		||||
    source = json.dumps(data)
 | 
			
		||||
    return safe_string and mark_safe(source) or source
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register.simple_tag(name='track_inline_labels')
 | 
			
		||||
def do_track_inline_labels():
 | 
			
		||||
    """ Return labels for columns in playlist editor as dict """
 | 
			
		||||
    return json.dumps({
 | 
			
		||||
        'artist': __('Artist'), 'album': __('Album'), 'title': __('Title'),
 | 
			
		||||
        'tags': __('Tags'), 'year': __('Year'),
 | 
			
		||||
        'save_settings': __('Save Settings'),
 | 
			
		||||
        'discard_changes': __('Discard changes'),
 | 
			
		||||
        'columns': __('Columns'),
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -24,10 +24,14 @@ register_converter(WeekConverter, 'week')
 | 
			
		||||
 | 
			
		||||
router = DefaultRouter()
 | 
			
		||||
router.register('sound', viewsets.SoundViewSet, basename='sound')
 | 
			
		||||
router.register('track', viewsets.TrackROViewSet, basename='track')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
api = [
 | 
			
		||||
    path('logs/', views.LogListAPIView.as_view(), name='live'),
 | 
			
		||||
    path('user/settings/', viewsets.UserSettingsViewSet.as_view(
 | 
			
		||||
         {'get': 'retrieve', 'post': 'update', 'put': 'update'}),
 | 
			
		||||
         name='user-settings'),
 | 
			
		||||
] + router.urls
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -62,6 +62,7 @@ class BaseView(TemplateResponseMixin, ContextMixin):
 | 
			
		||||
        return super().get_context_data(**kwargs)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# FIXME: rename to sth like [Base]?StationAPIView
 | 
			
		||||
class BaseAPIView:
 | 
			
		||||
    @property
 | 
			
		||||
    def station(self):
 | 
			
		||||
 | 
			
		||||
@ -1,13 +1,18 @@
 | 
			
		||||
from django.db.models import Q
 | 
			
		||||
 | 
			
		||||
from rest_framework import viewsets
 | 
			
		||||
from rest_framework import status, viewsets
 | 
			
		||||
from rest_framework.decorators import action
 | 
			
		||||
from rest_framework.permissions import IsAuthenticated
 | 
			
		||||
from rest_framework.response import Response
 | 
			
		||||
from django_filters import rest_framework as filters
 | 
			
		||||
 | 
			
		||||
from .models import Sound
 | 
			
		||||
from .serializers import SoundSerializer
 | 
			
		||||
from .models import Sound, Track
 | 
			
		||||
from .serializers import SoundSerializer, admin
 | 
			
		||||
from .views import BaseAPIView
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
__all__ = ('SoundFilter', 'SoundViewSet', 'TrackFilter', 'TrackROViewSet',
 | 
			
		||||
           'UserSettingsViewSet')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SoundFilter(filters.FilterSet):
 | 
			
		||||
    station = filters.NumberFilter(field_name='program__station__id')
 | 
			
		||||
    program = filters.NumberFilter(field_name='program_id')
 | 
			
		||||
@ -24,3 +29,63 @@ class SoundViewSet(BaseAPIView, viewsets.ModelViewSet):
 | 
			
		||||
    filter_backends = (filters.DjangoFilterBackend,)
 | 
			
		||||
    filterset_class = SoundFilter
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# --- admin
 | 
			
		||||
class TrackFilter(filters.FilterSet):
 | 
			
		||||
    artist = filters.CharFilter(field_name='artist', lookup_expr='icontains')
 | 
			
		||||
    album = filters.CharFilter(field_name='album', lookup_expr='icontains')
 | 
			
		||||
    title = filters.CharFilter(field_name='title', lookup_expr='icontains')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TrackROViewSet(viewsets.ReadOnlyModelViewSet):
 | 
			
		||||
    """ Track viewset used for auto completion """
 | 
			
		||||
    serializer_class = admin.TrackSerializer
 | 
			
		||||
    permission_classes = [IsAuthenticated]
 | 
			
		||||
    filter_backends = (filters.DjangoFilterBackend,)
 | 
			
		||||
    filterset_class = TrackFilter
 | 
			
		||||
    queryset = Track.objects.all()
 | 
			
		||||
 | 
			
		||||
    @action(name='autocomplete', detail=False)
 | 
			
		||||
    def autocomplete(self, request):
 | 
			
		||||
        field = request.GET.get('field', None)
 | 
			
		||||
        if field:
 | 
			
		||||
            queryset = self.filter_queryset(self.get_queryset())
 | 
			
		||||
            values = queryset.values_list(field, flat=True).distinct()
 | 
			
		||||
            return Response(values)
 | 
			
		||||
        return self.list(request)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UserSettingsViewSet(viewsets.ViewSet):
 | 
			
		||||
    """
 | 
			
		||||
    User's settings specific to aircox. Allow only to create and edit
 | 
			
		||||
    user's own settings.
 | 
			
		||||
    """
 | 
			
		||||
    serializer_class = admin.UserSettingsSerializer
 | 
			
		||||
    permission_classes = [IsAuthenticated]
 | 
			
		||||
 | 
			
		||||
    def get_serializer(self, instance=None, **kwargs):
 | 
			
		||||
        return self.serializer_class(
 | 
			
		||||
            instance=instance, context={'user': self.request.user},
 | 
			
		||||
            **kwargs)
 | 
			
		||||
 | 
			
		||||
    @action(detail=False, methods=['GET'])
 | 
			
		||||
    def retrieve(self, request):
 | 
			
		||||
        user = self.request.user
 | 
			
		||||
        settings = getattr(user, 'aircox_settings', None)
 | 
			
		||||
        data = settings and self.get_serializer(settings) or None
 | 
			
		||||
        return Response(data)
 | 
			
		||||
 | 
			
		||||
    @action(detail=False, methods=['POST', 'PUT'])
 | 
			
		||||
    def update(self, request):
 | 
			
		||||
        user = self.request.user
 | 
			
		||||
        settings = getattr(user, 'aircox_settings', None)
 | 
			
		||||
        data = dict(request.data)
 | 
			
		||||
        data['user_id'] = self.request.user
 | 
			
		||||
        serializer = self.get_serializer(instance=settings, data=request.data)
 | 
			
		||||
        if serializer.is_valid():
 | 
			
		||||
            serializer.save()
 | 
			
		||||
            return Response({'status': 'ok'})
 | 
			
		||||
        else:
 | 
			
		||||
            return Response({'errors': serializer.errors},
 | 
			
		||||
                            status=status.HTTP_400_BAD_REQUEST)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user