forked from rc/aircox
		
	work hard on this
This commit is contained in:
		
							
								
								
									
										208
									
								
								aircox/admin.py
									
									
									
									
									
								
							
							
						
						
									
										208
									
								
								aircox/admin.py
									
									
									
									
									
								
							@ -1,208 +0,0 @@
 | 
			
		||||
import copy
 | 
			
		||||
 | 
			
		||||
from django import forms
 | 
			
		||||
from django.contrib import admin
 | 
			
		||||
from django.contrib.contenttypes.admin import GenericTabularInline
 | 
			
		||||
from django.db import models
 | 
			
		||||
from django.utils.translation import ugettext as _, ugettext_lazy
 | 
			
		||||
 | 
			
		||||
from aircox.models import *
 | 
			
		||||
 | 
			
		||||
#
 | 
			
		||||
# Inlines
 | 
			
		||||
#
 | 
			
		||||
class SoundInline(admin.TabularInline):
 | 
			
		||||
    model = Sound
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ScheduleInline(admin.TabularInline):
 | 
			
		||||
    model = Schedule
 | 
			
		||||
    extra = 1
 | 
			
		||||
 | 
			
		||||
class StreamInline(admin.TabularInline):
 | 
			
		||||
    fields = ['delay', 'begin', 'end']
 | 
			
		||||
    model = Stream
 | 
			
		||||
    extra = 1
 | 
			
		||||
 | 
			
		||||
class SoundInline(admin.TabularInline):
 | 
			
		||||
    fields = ['type', 'path', 'duration','public']
 | 
			
		||||
    # readonly_fields = fields
 | 
			
		||||
    model = Sound
 | 
			
		||||
    extra = 0
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DiffusionInline(admin.StackedInline):
 | 
			
		||||
    model = Diffusion
 | 
			
		||||
    extra = 0
 | 
			
		||||
    fields = ['type', 'start', 'end']
 | 
			
		||||
 | 
			
		||||
class NameableAdmin(admin.ModelAdmin):
 | 
			
		||||
    fields = [ 'name' ]
 | 
			
		||||
 | 
			
		||||
    list_display = ['id', 'name']
 | 
			
		||||
    list_filter = []
 | 
			
		||||
    search_fields = ['name',]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TrackInline(GenericTabularInline):
 | 
			
		||||
    ct_field = 'related_type'
 | 
			
		||||
    ct_fk_field = 'related_id'
 | 
			
		||||
    model = Track
 | 
			
		||||
    extra = 0
 | 
			
		||||
    fields = ('artist', 'title', 'info', 'position', 'in_seconds', 'tags')
 | 
			
		||||
 | 
			
		||||
    list_display = ['artist','title','tags','related']
 | 
			
		||||
    list_filter = ['artist','title','tags']
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@admin.register(Sound)
 | 
			
		||||
class SoundAdmin(NameableAdmin):
 | 
			
		||||
    fields = None
 | 
			
		||||
    list_display = ['id', 'name', 'program', 'type', 'duration', 'mtime',
 | 
			
		||||
                    'public', 'good_quality', 'path']
 | 
			
		||||
    list_filter = ('program', 'type', 'good_quality', 'public')
 | 
			
		||||
    fieldsets = [
 | 
			
		||||
        (None, { 'fields': NameableAdmin.fields +
 | 
			
		||||
                           ['path', 'type', 'program', 'diffusion'] } ),
 | 
			
		||||
        (None, { 'fields': ['embed', 'duration', 'public', 'mtime'] }),
 | 
			
		||||
        (None, { 'fields': ['good_quality' ] } )
 | 
			
		||||
    ]
 | 
			
		||||
    readonly_fields = ('path', 'duration',)
 | 
			
		||||
    inlines = [TrackInline]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@admin.register(Stream)
 | 
			
		||||
class StreamAdmin(admin.ModelAdmin):
 | 
			
		||||
    list_display = ('id', 'program', 'delay', 'begin', 'end')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@admin.register(Program)
 | 
			
		||||
class ProgramAdmin(NameableAdmin):
 | 
			
		||||
    def schedule(self, obj):
 | 
			
		||||
        return Schedule.objects.filter(program = obj).count() > 0
 | 
			
		||||
    schedule.boolean = True
 | 
			
		||||
    schedule.short_description = _("Schedule")
 | 
			
		||||
 | 
			
		||||
    list_display = ('id', 'name', 'active', 'schedule', 'sync', 'station')
 | 
			
		||||
    fields = NameableAdmin.fields + [ 'active', 'station','sync' ]
 | 
			
		||||
    inlines = [ ScheduleInline, StreamInline ]
 | 
			
		||||
 | 
			
		||||
    # SO#8074161
 | 
			
		||||
    #def get_form(self, request, obj=None, **kwargs):
 | 
			
		||||
        #if obj:
 | 
			
		||||
        #    if Schedule.objects.filter(program = obj).count():
 | 
			
		||||
        #        self.inlines.remove(StreamInline)
 | 
			
		||||
        #    elif Stream.objects.filter(program = obj).count():
 | 
			
		||||
        #        self.inlines.remove(ScheduleInline)
 | 
			
		||||
        #return super().get_form(request, obj, **kwargs)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@admin.register(Diffusion)
 | 
			
		||||
class DiffusionAdmin(admin.ModelAdmin):
 | 
			
		||||
    def archives(self, obj):
 | 
			
		||||
        sounds = [ str(s) for s in obj.get_sounds(archive=True)]
 | 
			
		||||
        return ', '.join(sounds) if sounds else ''
 | 
			
		||||
 | 
			
		||||
    def conflicts_(self, obj):
 | 
			
		||||
        if obj.conflicts.count():
 | 
			
		||||
            return obj.conflicts.count()
 | 
			
		||||
        return ''
 | 
			
		||||
 | 
			
		||||
    def start_date(self, obj):
 | 
			
		||||
        return obj.local_date.strftime('%Y/%m/%d %H:%M')
 | 
			
		||||
    start_date.short_description = _('start')
 | 
			
		||||
 | 
			
		||||
    def end_date(self, obj):
 | 
			
		||||
        return obj.local_end.strftime('%H:%M')
 | 
			
		||||
    end_date.short_description = _('end')
 | 
			
		||||
 | 
			
		||||
    def first(self, obj):
 | 
			
		||||
        return obj.initial.start if obj.initial else ''
 | 
			
		||||
 | 
			
		||||
    list_display = ('id', 'program', 'start_date', 'end_date', 'type', 'first', 'archives', 'conflicts_')
 | 
			
		||||
    list_filter = ('type', 'start', 'program')
 | 
			
		||||
    list_editable = ('type',)
 | 
			
		||||
    ordering = ('-start', 'id')
 | 
			
		||||
 | 
			
		||||
    fields = ['type', 'start', 'end', 'initial', 'program', 'conflicts']
 | 
			
		||||
    readonly_fields = ('conflicts',)
 | 
			
		||||
    inlines = [ DiffusionInline, SoundInline ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    def get_form(self, request, obj=None, **kwargs):
 | 
			
		||||
        if request.user.has_perm('aircox_program.programming'):
 | 
			
		||||
            self.readonly_fields = []
 | 
			
		||||
        else:
 | 
			
		||||
            self.readonly_fields = ['program', 'start', 'end']
 | 
			
		||||
        return super().get_form(request, obj, **kwargs)
 | 
			
		||||
 | 
			
		||||
    def get_object(self, *args, **kwargs):
 | 
			
		||||
        """
 | 
			
		||||
        We want rerun to redirect to the given object.
 | 
			
		||||
        """
 | 
			
		||||
        obj = super().get_object(*args, **kwargs)
 | 
			
		||||
        if obj and obj.initial:
 | 
			
		||||
            obj = obj.initial
 | 
			
		||||
        return obj
 | 
			
		||||
 | 
			
		||||
    def get_queryset(self, request):
 | 
			
		||||
        qs = super().get_queryset(request)
 | 
			
		||||
        if request.GET and len(request.GET):
 | 
			
		||||
            return qs
 | 
			
		||||
        return qs.exclude(type = Diffusion.Type.unconfirmed)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@admin.register(Schedule)
 | 
			
		||||
class ScheduleAdmin(admin.ModelAdmin):
 | 
			
		||||
    def program_name(self, obj):
 | 
			
		||||
        return obj.program.name
 | 
			
		||||
    program_name.short_description = _('Program')
 | 
			
		||||
 | 
			
		||||
    def day(self, obj):
 | 
			
		||||
        return '' # obj.date.strftime('%A')
 | 
			
		||||
    day.short_description = _('Day')
 | 
			
		||||
 | 
			
		||||
    def rerun(self, obj):
 | 
			
		||||
        return obj.initial != None
 | 
			
		||||
    rerun.short_description = _('Rerun')
 | 
			
		||||
    rerun.boolean = True
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    list_filter = ['frequency', 'program']
 | 
			
		||||
    list_display = ['id', 'program_name', 'frequency', 'day', 'date',
 | 
			
		||||
                    'time', 'duration', 'timezone', 'rerun']
 | 
			
		||||
    list_editable = ['time', 'timezone', 'duration']
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    def get_readonly_fields(self, request, obj=None):
 | 
			
		||||
        if obj:
 | 
			
		||||
            return ['program', 'date', 'frequency']
 | 
			
		||||
        else:
 | 
			
		||||
            return []
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@admin.register(Track)
 | 
			
		||||
class TrackAdmin(admin.ModelAdmin):
 | 
			
		||||
    list_display = ['id', 'title', 'artist', 'position', 'in_seconds', 'related']
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# TODO: sort & redo
 | 
			
		||||
class PortInline(admin.StackedInline):
 | 
			
		||||
    model = Port
 | 
			
		||||
    extra = 0
 | 
			
		||||
 | 
			
		||||
@admin.register(Station)
 | 
			
		||||
class StationAdmin(admin.ModelAdmin):
 | 
			
		||||
    inlines = [ PortInline ]
 | 
			
		||||
 | 
			
		||||
@admin.register(Log)
 | 
			
		||||
class LogAdmin(admin.ModelAdmin):
 | 
			
		||||
    list_display = ['id', 'date', 'station', 'source', 'type', 'comment', 'diffusion', 'sound', 'track']
 | 
			
		||||
    list_filter = ['date', 'source', 'diffusion', 'sound', 'track']
 | 
			
		||||
 | 
			
		||||
admin.site.register(Port)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										5
									
								
								aircox/admin/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								aircox/admin/__init__.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,5 @@
 | 
			
		||||
from .base import *
 | 
			
		||||
from .diffusion import DiffusionAdmin
 | 
			
		||||
# from .playlist import PlaylistAdmin
 | 
			
		||||
from .sound import SoundAdmin
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										94
									
								
								aircox/admin/base.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								aircox/admin/base.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,94 @@
 | 
			
		||||
from django import forms
 | 
			
		||||
from django.contrib import admin
 | 
			
		||||
from django.urls import reverse
 | 
			
		||||
from django.utils.translation import ugettext as _, ugettext_lazy
 | 
			
		||||
from django.utils.safestring import mark_safe
 | 
			
		||||
 | 
			
		||||
from adminsortable2.admin import SortableInlineAdminMixin
 | 
			
		||||
 | 
			
		||||
from aircox.models import *
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ScheduleInline(admin.TabularInline):
 | 
			
		||||
    model = Schedule
 | 
			
		||||
    extra = 1
 | 
			
		||||
 | 
			
		||||
class StreamInline(admin.TabularInline):
 | 
			
		||||
    fields = ['delay', 'begin', 'end']
 | 
			
		||||
    model = Stream
 | 
			
		||||
    extra = 1
 | 
			
		||||
 | 
			
		||||
class NameableAdmin(admin.ModelAdmin):
 | 
			
		||||
    fields = [ 'name' ]
 | 
			
		||||
 | 
			
		||||
    list_display = ['id', 'name']
 | 
			
		||||
    list_filter = []
 | 
			
		||||
    search_fields = ['name',]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@admin.register(Stream)
 | 
			
		||||
class StreamAdmin(admin.ModelAdmin):
 | 
			
		||||
    list_display = ('id', 'program', 'delay', 'begin', 'end')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@admin.register(Program)
 | 
			
		||||
class ProgramAdmin(NameableAdmin):
 | 
			
		||||
    def schedule(self, obj):
 | 
			
		||||
        return Schedule.objects.filter(program = obj).count() > 0
 | 
			
		||||
    schedule.boolean = True
 | 
			
		||||
    schedule.short_description = _("Schedule")
 | 
			
		||||
 | 
			
		||||
    list_display = ('id', 'name', 'active', 'schedule', 'sync', 'station')
 | 
			
		||||
    fields = NameableAdmin.fields + [ 'active', 'station','sync' ]
 | 
			
		||||
    inlines = [ ScheduleInline, StreamInline ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@admin.register(Schedule)
 | 
			
		||||
class ScheduleAdmin(admin.ModelAdmin):
 | 
			
		||||
    def program_name(self, obj):
 | 
			
		||||
        return obj.program.name
 | 
			
		||||
    program_name.short_description = _('Program')
 | 
			
		||||
 | 
			
		||||
    def day(self, obj):
 | 
			
		||||
        return '' # obj.date.strftime('%A')
 | 
			
		||||
    day.short_description = _('Day')
 | 
			
		||||
 | 
			
		||||
    def rerun(self, obj):
 | 
			
		||||
        return obj.initial is not None
 | 
			
		||||
    rerun.short_description = _('Rerun')
 | 
			
		||||
    rerun.boolean = True
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    list_filter = ['frequency', 'program']
 | 
			
		||||
    list_display = ['id', 'program_name', 'frequency', 'day', 'date',
 | 
			
		||||
                    'time', 'duration', 'timezone', 'rerun']
 | 
			
		||||
    list_editable = ['time', 'timezone', 'duration']
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    def get_readonly_fields(self, request, obj=None):
 | 
			
		||||
        if obj:
 | 
			
		||||
            return ['program', 'date', 'frequency']
 | 
			
		||||
        else:
 | 
			
		||||
            return []
 | 
			
		||||
 | 
			
		||||
# TODO: sort & redo
 | 
			
		||||
class PortInline(admin.StackedInline):
 | 
			
		||||
    model = Port
 | 
			
		||||
    extra = 0
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@admin.register(Station)
 | 
			
		||||
class StationAdmin(admin.ModelAdmin):
 | 
			
		||||
    inlines = [PortInline]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@admin.register(Log)
 | 
			
		||||
class LogAdmin(admin.ModelAdmin):
 | 
			
		||||
    list_display = ['id', 'date', 'station', 'source', 'type', 'comment', 'diffusion', 'sound', 'track']
 | 
			
		||||
    list_filter = ['date', 'source', 'diffusion', 'sound', 'track']
 | 
			
		||||
 | 
			
		||||
admin.site.register(Port)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										81
									
								
								aircox/admin/diffusion.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								aircox/admin/diffusion.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,81 @@
 | 
			
		||||
from django.contrib import admin
 | 
			
		||||
from django.utils.translation import ugettext as _, ugettext_lazy
 | 
			
		||||
 | 
			
		||||
from aircox.models import Diffusion, Sound, Track
 | 
			
		||||
 | 
			
		||||
from .playlist import TracksInline
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SoundInline(admin.TabularInline):
 | 
			
		||||
    model = Sound
 | 
			
		||||
    fk_name = 'diffusion'
 | 
			
		||||
    fields = ['type', 'path', 'duration','public']
 | 
			
		||||
    readonly_fields = ['type']
 | 
			
		||||
    extra = 0
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class RediffusionInline(admin.StackedInline):
 | 
			
		||||
    model = Diffusion
 | 
			
		||||
    fk_name = 'initial'
 | 
			
		||||
    extra = 0
 | 
			
		||||
    fields = ['type', 'start', 'end']
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@admin.register(Diffusion)
 | 
			
		||||
class DiffusionAdmin(admin.ModelAdmin):
 | 
			
		||||
    def archives(self, obj):
 | 
			
		||||
        sounds = [str(s) for s in obj.get_sounds(archive=True)]
 | 
			
		||||
        return ', '.join(sounds) if sounds else ''
 | 
			
		||||
 | 
			
		||||
    def conflicts_count(self, obj):
 | 
			
		||||
        if obj.conflicts.count():
 | 
			
		||||
            return obj.conflicts.count()
 | 
			
		||||
        return ''
 | 
			
		||||
    conflicts_count.short_description = _('Conflicts')
 | 
			
		||||
 | 
			
		||||
    def start_date(self, obj):
 | 
			
		||||
        return obj.local_date.strftime('%Y/%m/%d %H:%M')
 | 
			
		||||
    start_date.short_description = _('start')
 | 
			
		||||
 | 
			
		||||
    def end_date(self, obj):
 | 
			
		||||
        return obj.local_end.strftime('%H:%M')
 | 
			
		||||
    end_date.short_description = _('end')
 | 
			
		||||
 | 
			
		||||
    def first(self, obj):
 | 
			
		||||
        return obj.initial.start if obj.initial else ''
 | 
			
		||||
 | 
			
		||||
    list_display = ('id', 'program', 'start_date', 'end_date', 'type', 'first', 'archives', 'conflicts_count')
 | 
			
		||||
    list_filter = ('type', 'start', 'program')
 | 
			
		||||
    list_editable = ('type',)
 | 
			
		||||
    ordering = ('-start', 'id')
 | 
			
		||||
 | 
			
		||||
    fields = ['type', 'start', 'end', 'initial', 'program', 'conflicts']
 | 
			
		||||
    readonly_fields = ('conflicts',)
 | 
			
		||||
    inlines = [TracksInline, RediffusionInline, SoundInline]
 | 
			
		||||
 | 
			
		||||
    def get_playlist(self, request, obj=None):
 | 
			
		||||
        return obj and getattr(obj, 'playlist', None)
 | 
			
		||||
 | 
			
		||||
    def get_form(self, request, obj=None, **kwargs):
 | 
			
		||||
        if request.user.has_perm('aircox_program.programming'):
 | 
			
		||||
            self.readonly_fields = []
 | 
			
		||||
        else:
 | 
			
		||||
            self.readonly_fields = ['program', 'start', 'end']
 | 
			
		||||
        return super().get_form(request, obj, **kwargs)
 | 
			
		||||
 | 
			
		||||
    def get_object(self, *args, **kwargs):
 | 
			
		||||
        """
 | 
			
		||||
        We want rerun to redirect to the given object.
 | 
			
		||||
        """
 | 
			
		||||
        obj = super().get_object(*args, **kwargs)
 | 
			
		||||
        if obj and obj.initial:
 | 
			
		||||
            obj = obj.initial
 | 
			
		||||
        return obj
 | 
			
		||||
 | 
			
		||||
    def get_queryset(self, request):
 | 
			
		||||
        qs = super().get_queryset(request)
 | 
			
		||||
        if request.GET and len(request.GET):
 | 
			
		||||
            return qs
 | 
			
		||||
        return qs.exclude(type=Diffusion.Type.unconfirmed)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										42
									
								
								aircox/admin/mixins.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								aircox/admin/mixins.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,42 @@
 | 
			
		||||
class UnrelatedInlineMixin:
 | 
			
		||||
    """
 | 
			
		||||
    Inline class that can be included in an admin change view whose model
 | 
			
		||||
    is not directly related to inline's model.
 | 
			
		||||
    """
 | 
			
		||||
    view_model = None
 | 
			
		||||
    parent_model = None
 | 
			
		||||
    parent_fk = ''
 | 
			
		||||
 | 
			
		||||
    def __init__(self, parent_model, admin_site):
 | 
			
		||||
        self.view_model = parent_model
 | 
			
		||||
        super().__init__(self.parent_model, admin_site)
 | 
			
		||||
 | 
			
		||||
    def get_parent(self, view_obj):
 | 
			
		||||
        """ Get formset's instance from `obj` of AdminSite's change form. """
 | 
			
		||||
        field = self.parent_model._meta.get_field(self.parent_fk).remote_field
 | 
			
		||||
        return getattr(view_obj, field.name, None)
 | 
			
		||||
 | 
			
		||||
    def save_parent(self, parent, view_obj):
 | 
			
		||||
        """ Save formset's instance. """
 | 
			
		||||
        setattr(parent, self.parent_fk, view_obj)
 | 
			
		||||
        parent.save()
 | 
			
		||||
        return parent
 | 
			
		||||
 | 
			
		||||
    def get_formset(self, request, obj):
 | 
			
		||||
        ParentFormSet = super().get_formset(request, obj)
 | 
			
		||||
        inline = self
 | 
			
		||||
        class FormSet(ParentFormSet):
 | 
			
		||||
            view_obj = None
 | 
			
		||||
 | 
			
		||||
            def __init__(self, *args, instance=None, **kwargs):
 | 
			
		||||
                self.view_obj = instance
 | 
			
		||||
                instance = inline.get_parent(instance)
 | 
			
		||||
                self.instance = instance
 | 
			
		||||
                super().__init__(*args, instance=instance, **kwargs)
 | 
			
		||||
 | 
			
		||||
            def save(self):
 | 
			
		||||
                inline.save_parent(self.instance, self.view_obj)
 | 
			
		||||
                return super().save()
 | 
			
		||||
        return FormSet
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										41
									
								
								aircox/admin/playlist.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								aircox/admin/playlist.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,41 @@
 | 
			
		||||
from django.contrib import admin
 | 
			
		||||
from django.utils.translation import ugettext as _, ugettext_lazy
 | 
			
		||||
 | 
			
		||||
from adminsortable2.admin import SortableInlineAdminMixin
 | 
			
		||||
 | 
			
		||||
from aircox.models import Track
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TracksInline(SortableInlineAdminMixin, admin.TabularInline):
 | 
			
		||||
    template = 'admin/aircox/playlist_inline.html'
 | 
			
		||||
    model = Track
 | 
			
		||||
    extra = 0
 | 
			
		||||
    fields = ('position', 'artist', 'title', 'info', 'timestamp', 'tags')
 | 
			
		||||
 | 
			
		||||
    list_display = ['artist', 'title', 'tags', 'related']
 | 
			
		||||
    list_filter = ['artist', 'title', 'tags']
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@admin.register(Track)
 | 
			
		||||
class TrackAdmin(admin.ModelAdmin):
 | 
			
		||||
    # TODO: url to filter by tag
 | 
			
		||||
    def tag_list(self, obj):
 | 
			
		||||
        return u", ".join(o.name for o in obj.tags.all())
 | 
			
		||||
 | 
			
		||||
    list_display = ['pk', 'artist', 'title', 'tag_list', 'diffusion', 'sound']
 | 
			
		||||
    list_editable = ['artist', 'title']
 | 
			
		||||
    list_filter = ['sound', 'diffusion', 'artist', 'title', 'tags']
 | 
			
		||||
    fieldsets = [
 | 
			
		||||
        (_('Playlist'), {'fields': ['diffusion', 'sound', 'position', 'timestamp']}),
 | 
			
		||||
        (_('Info'), {'fields': ['artist', 'title', 'info', 'tags']}),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    # TODO on edit: readonly_fields = ['diffusion', 'sound']
 | 
			
		||||
 | 
			
		||||
#@admin.register(Playlist)
 | 
			
		||||
#class PlaylistAdmin(admin.ModelAdmin):
 | 
			
		||||
#    fields = ['diffusion', 'sound']
 | 
			
		||||
#    inlines = [TracksInline]
 | 
			
		||||
#    # TODO: dynamic read only fields
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										24
									
								
								aircox/admin/sound.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								aircox/admin/sound.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,24 @@
 | 
			
		||||
from django.contrib import admin
 | 
			
		||||
from django.utils.translation import ugettext as _, ugettext_lazy
 | 
			
		||||
 | 
			
		||||
from aircox.models import Sound
 | 
			
		||||
from .base import NameableAdmin
 | 
			
		||||
from .playlist import TracksInline
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@admin.register(Sound)
 | 
			
		||||
class SoundAdmin(NameableAdmin):
 | 
			
		||||
    fields = None
 | 
			
		||||
    list_display = ['id', 'name', 'program', 'type', 'duration', 'mtime',
 | 
			
		||||
                    'public', 'good_quality', 'path']
 | 
			
		||||
    list_filter = ('program', 'type', 'good_quality', 'public')
 | 
			
		||||
    fieldsets = [
 | 
			
		||||
        (None, {'fields': NameableAdmin.fields +
 | 
			
		||||
                          ['path', 'type', 'program', 'diffusion']}),
 | 
			
		||||
        (None, {'fields': ['embed', 'duration', 'public', 'mtime']}),
 | 
			
		||||
        (None, {'fields': ['good_quality']})
 | 
			
		||||
    ]
 | 
			
		||||
    readonly_fields = ('path', 'duration',)
 | 
			
		||||
    inlines = [TracksInline]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -29,62 +29,65 @@ class Importer:
 | 
			
		||||
    path = None
 | 
			
		||||
    data = None
 | 
			
		||||
    tracks = None
 | 
			
		||||
    track_kwargs = {}
 | 
			
		||||
 | 
			
		||||
    def __init__(self, related = None, path = None, save = False):
 | 
			
		||||
        if path:
 | 
			
		||||
            self.read(path)
 | 
			
		||||
            if related:
 | 
			
		||||
                self.make_playlist(related, save)
 | 
			
		||||
    def __init__(self, path=None, **track_kwargs):
 | 
			
		||||
        self.path = path
 | 
			
		||||
        self.track_kwargs = track_kwargs
 | 
			
		||||
 | 
			
		||||
    def reset(self):
 | 
			
		||||
        self.data = None
 | 
			
		||||
        self.tracks = None
 | 
			
		||||
 | 
			
		||||
    def read(self, path):
 | 
			
		||||
        if not os.path.exists(path):
 | 
			
		||||
    def run(self):
 | 
			
		||||
        self.read()
 | 
			
		||||
        if self.track_kwargs.get('sound') is not None:
 | 
			
		||||
            self.make_playlist()
 | 
			
		||||
 | 
			
		||||
    def read(self):
 | 
			
		||||
        if not os.path.exists(self.path):
 | 
			
		||||
            return True
 | 
			
		||||
        with open(path, 'r') as file:
 | 
			
		||||
            logger.info('start reading csv ' + path)
 | 
			
		||||
            self.path = path
 | 
			
		||||
        with open(self.path, 'r') as file:
 | 
			
		||||
            logger.info('start reading csv ' + self.path)
 | 
			
		||||
            self.data = list(csv.DictReader(
 | 
			
		||||
                (row for row in file
 | 
			
		||||
                    if not (row.startswith('#') or row.startswith('\ufeff#'))
 | 
			
		||||
                            and row.strip()
 | 
			
		||||
                ),
 | 
			
		||||
                fieldnames = settings.AIRCOX_IMPORT_PLAYLIST_CSV_COLS,
 | 
			
		||||
                delimiter = settings.AIRCOX_IMPORT_PLAYLIST_CSV_DELIMITER,
 | 
			
		||||
                quotechar = settings.AIRCOX_IMPORT_PLAYLIST_CSV_TEXT_QUOTE,
 | 
			
		||||
                    and row.strip()),
 | 
			
		||||
                fieldnames=settings.AIRCOX_IMPORT_PLAYLIST_CSV_COLS,
 | 
			
		||||
                delimiter=settings.AIRCOX_IMPORT_PLAYLIST_CSV_DELIMITER,
 | 
			
		||||
                quotechar=settings.AIRCOX_IMPORT_PLAYLIST_CSV_TEXT_QUOTE,
 | 
			
		||||
            ))
 | 
			
		||||
 | 
			
		||||
    def make_playlist(self, related, save = False):
 | 
			
		||||
    def make_playlist(self):
 | 
			
		||||
        """
 | 
			
		||||
        Make a playlist from the read data, and return it. If save is
 | 
			
		||||
        true, save it into the database
 | 
			
		||||
        """
 | 
			
		||||
        if self.track_kwargs.get('sound') is None:
 | 
			
		||||
            logger.error('related track\'s sound is missing. Skip import of ' +
 | 
			
		||||
                         self.path + '.')
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        maps = settings.AIRCOX_IMPORT_PLAYLIST_CSV_COLS
 | 
			
		||||
        tracks = []
 | 
			
		||||
 | 
			
		||||
        logger.info('parse csv file ' + self.path)
 | 
			
		||||
        in_seconds = ('minutes' or 'seconds') in maps
 | 
			
		||||
        has_timestamp = ('minutes' or 'seconds') in maps
 | 
			
		||||
        for index, line in enumerate(self.data):
 | 
			
		||||
            if ('title' or 'artist') not in line:
 | 
			
		||||
                return
 | 
			
		||||
 | 
			
		||||
            try:
 | 
			
		||||
                position = \
 | 
			
		||||
                    int(line.get('minutes') or 0) * 60 + \
 | 
			
		||||
                    int(line.get('seconds') or 0) \
 | 
			
		||||
                    if in_seconds else index
 | 
			
		||||
                timestamp = int(line.get('minutes') or 0) * 60 + \
 | 
			
		||||
                            int(line.get('seconds') or 0) \
 | 
			
		||||
                            if has_timestamp else None
 | 
			
		||||
 | 
			
		||||
                track, created = Track.objects.get_or_create(
 | 
			
		||||
                    related_type = ContentType.objects.get_for_model(related),
 | 
			
		||||
                    related_id = related.pk,
 | 
			
		||||
                    title = line.get('title'),
 | 
			
		||||
                    artist = line.get('artist'),
 | 
			
		||||
                    position = position,
 | 
			
		||||
                    title=line.get('title'),
 | 
			
		||||
                    artist=line.get('artist'),
 | 
			
		||||
                    position=index,
 | 
			
		||||
                    **self.track_kwargs
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
                track.in_seconds = in_seconds
 | 
			
		||||
                track.timestamp = timestamp
 | 
			
		||||
                track.info = line.get('info')
 | 
			
		||||
                tags = line.get('tags')
 | 
			
		||||
                if tags:
 | 
			
		||||
@ -97,8 +100,7 @@ class Importer:
 | 
			
		||||
                )
 | 
			
		||||
                continue
 | 
			
		||||
 | 
			
		||||
            if save:
 | 
			
		||||
                track.save()
 | 
			
		||||
            track.save()
 | 
			
		||||
            tracks.append(track)
 | 
			
		||||
        self.tracks = tracks
 | 
			
		||||
        return tracks
 | 
			
		||||
@ -107,10 +109,8 @@ class Importer:
 | 
			
		||||
class Command (BaseCommand):
 | 
			
		||||
    help= __doc__
 | 
			
		||||
 | 
			
		||||
    def add_arguments (self, parser):
 | 
			
		||||
    def add_arguments(self, parser):
 | 
			
		||||
        parser.formatter_class=RawTextHelpFormatter
 | 
			
		||||
        now = tz.datetime.today()
 | 
			
		||||
 | 
			
		||||
        parser.add_argument(
 | 
			
		||||
            'path', metavar='PATH', type=str,
 | 
			
		||||
            help='path of the input playlist to read'
 | 
			
		||||
@ -128,27 +128,24 @@ class Command (BaseCommand):
 | 
			
		||||
    def handle (self, path, *args, **options):
 | 
			
		||||
        # FIXME: absolute/relative path of sounds vs given path
 | 
			
		||||
        if options.get('sound'):
 | 
			
		||||
            related = Sound.objects.filter(
 | 
			
		||||
                path__icontains = options.get('sound')
 | 
			
		||||
            sound = Sound.objects.filter(
 | 
			
		||||
                path__icontains=options.get('sound')
 | 
			
		||||
            ).first()
 | 
			
		||||
        else:
 | 
			
		||||
            path_, ext = os.path.splitext(path)
 | 
			
		||||
            related = Sound.objects.filter(path__icontains = path_).first()
 | 
			
		||||
            sound = Sound.objects.filter(path__icontains=path_).first()
 | 
			
		||||
 | 
			
		||||
        if not related:
 | 
			
		||||
        if not sound:
 | 
			
		||||
            logger.error('no sound found in the database for the path ' \
 | 
			
		||||
                         '{path}'.format(path=path))
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        if options.get('diffusion') and related.diffusion:
 | 
			
		||||
            related = related.diffusion
 | 
			
		||||
        if options.get('diffusion') and sound.diffusion:
 | 
			
		||||
            sound = sound.diffusion
 | 
			
		||||
 | 
			
		||||
        importer = Importer(related = related, path = path, save = True)
 | 
			
		||||
        importer = Importer(path, sound=sound).run()
 | 
			
		||||
        for track in importer.tracks:
 | 
			
		||||
            logger.info('imported track at {pos}: {title}, by '
 | 
			
		||||
                        '{artist}'.format(
 | 
			
		||||
                    pos = track.position,
 | 
			
		||||
                    title = track.title, artist = track.artist
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
            logger.info('track #{pos} imported: {title}, by {artist}'.format(
 | 
			
		||||
                pos=track.position, title=track.title, artist=track.artist
 | 
			
		||||
            ))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -43,6 +43,7 @@ import aircox.utils as utils
 | 
			
		||||
 | 
			
		||||
logger = logging.getLogger('aircox.tools')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SoundInfo:
 | 
			
		||||
    name = ''
 | 
			
		||||
    sound = None
 | 
			
		||||
@ -76,7 +77,7 @@ class SoundInfo:
 | 
			
		||||
                      file_name)
 | 
			
		||||
 | 
			
		||||
        if not (r and r.groupdict()):
 | 
			
		||||
            r = { 'name': file_name }
 | 
			
		||||
            r = {'name': file_name}
 | 
			
		||||
            logger.info('file name can not be parsed -> %s', value)
 | 
			
		||||
        else:
 | 
			
		||||
            r = r.groupdict()
 | 
			
		||||
@ -93,7 +94,7 @@ class SoundInfo:
 | 
			
		||||
        self.n = r.get('n')
 | 
			
		||||
        return r
 | 
			
		||||
 | 
			
		||||
    def __init__(self, path = '', sound = None):
 | 
			
		||||
    def __init__(self, path='', sound=None):
 | 
			
		||||
        self.path = path
 | 
			
		||||
        self.sound = sound
 | 
			
		||||
 | 
			
		||||
@ -107,7 +108,7 @@ class SoundInfo:
 | 
			
		||||
            self.duration = duration
 | 
			
		||||
            return duration
 | 
			
		||||
 | 
			
		||||
    def get_sound(self, save = True, **kwargs):
 | 
			
		||||
    def get_sound(self, save=True, **kwargs):
 | 
			
		||||
        """
 | 
			
		||||
        Get or create a sound using self info.
 | 
			
		||||
 | 
			
		||||
@ -115,8 +116,8 @@ class SoundInfo:
 | 
			
		||||
        (if save is True, sync to DB), and check for a playlist file.
 | 
			
		||||
        """
 | 
			
		||||
        sound, created = Sound.objects.get_or_create(
 | 
			
		||||
            path = self.path,
 | 
			
		||||
            defaults = kwargs
 | 
			
		||||
            path=self.path,
 | 
			
		||||
            defaults=kwargs
 | 
			
		||||
        )
 | 
			
		||||
        if created or sound.check_on_file():
 | 
			
		||||
            logger.info('sound is new or have been modified -> %s', self.path)
 | 
			
		||||
@ -127,7 +128,7 @@ class SoundInfo:
 | 
			
		||||
        self.sound = sound
 | 
			
		||||
        return sound
 | 
			
		||||
 | 
			
		||||
    def find_playlist(self, sound, use_default = True):
 | 
			
		||||
    def find_playlist(self, sound, use_default=True):
 | 
			
		||||
        """
 | 
			
		||||
        Find a playlist file corresponding to the sound path, such as:
 | 
			
		||||
            my_sound.ogg => my_sound.csv
 | 
			
		||||
@ -135,11 +136,11 @@ class SoundInfo:
 | 
			
		||||
        If use_default is True and there is no playlist find found,
 | 
			
		||||
        use sound file's metadata.
 | 
			
		||||
        """
 | 
			
		||||
        if sound.tracks.count():
 | 
			
		||||
        if sound.track_set.count():
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        import aircox.management.commands.import_playlist \
 | 
			
		||||
                as import_playlist
 | 
			
		||||
            as import_playlist
 | 
			
		||||
 | 
			
		||||
        # no playlist, try to retrieve metadata
 | 
			
		||||
        path = os.path.splitext(self.sound.path)[0] + '.csv'
 | 
			
		||||
@ -151,9 +152,9 @@ class SoundInfo:
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        # else, import
 | 
			
		||||
        import_playlist.Importer(sound, path, save=True)
 | 
			
		||||
        import_playlist.Importer(path, sound=sound).run()
 | 
			
		||||
 | 
			
		||||
    def find_diffusion(self, program, save = True):
 | 
			
		||||
    def find_diffusion(self, program, save=True):
 | 
			
		||||
        """
 | 
			
		||||
        For a given program, check if there is an initial diffusion
 | 
			
		||||
        to associate to, using the date info we have. Update self.sound
 | 
			
		||||
@ -163,7 +164,7 @@ class SoundInfo:
 | 
			
		||||
        rerun.
 | 
			
		||||
        """
 | 
			
		||||
        if self.year == None or not self.sound or self.sound.diffusion:
 | 
			
		||||
            return;
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        if self.hour is None:
 | 
			
		||||
            date = datetime.date(self.year, self.month, self.day)
 | 
			
		||||
@ -173,7 +174,7 @@ class SoundInfo:
 | 
			
		||||
            date = tz.get_current_timezone().localize(date)
 | 
			
		||||
 | 
			
		||||
        qs = Diffusion.objects.station(program.station).after(date) \
 | 
			
		||||
                      .filter(program = program, initial__isnull = True)
 | 
			
		||||
                      .filter(program=program, initial__isnull=True)
 | 
			
		||||
        diffusion = qs.first()
 | 
			
		||||
        if not diffusion:
 | 
			
		||||
            return
 | 
			
		||||
@ -190,18 +191,19 @@ class MonitorHandler(PatternMatchingEventHandler):
 | 
			
		||||
    """
 | 
			
		||||
    Event handler for watchdog, in order to be used in monitoring.
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    def __init__(self, subdir):
 | 
			
		||||
        """
 | 
			
		||||
        subdir: AIRCOX_SOUND_ARCHIVES_SUBDIR or AIRCOX_SOUND_EXCERPTS_SUBDIR
 | 
			
		||||
        """
 | 
			
		||||
        self.subdir = subdir
 | 
			
		||||
        if self.subdir == settings.AIRCOX_SOUND_ARCHIVES_SUBDIR:
 | 
			
		||||
            self.sound_kwargs = { 'type': Sound.Type.archive }
 | 
			
		||||
            self.sound_kwargs = {'type': Sound.Type.archive}
 | 
			
		||||
        else:
 | 
			
		||||
            self.sound_kwargs = { 'type': Sound.Type.excerpt }
 | 
			
		||||
            self.sound_kwargs = {'type': Sound.Type.excerpt}
 | 
			
		||||
 | 
			
		||||
        patterns = ['*/{}/*{}'.format(self.subdir, ext)
 | 
			
		||||
                    for ext in settings.AIRCOX_SOUND_FILE_EXT ]
 | 
			
		||||
                    for ext in settings.AIRCOX_SOUND_FILE_EXT]
 | 
			
		||||
        super().__init__(patterns=patterns, ignore_directories=True)
 | 
			
		||||
 | 
			
		||||
    def on_created(self, event):
 | 
			
		||||
@ -215,14 +217,14 @@ class MonitorHandler(PatternMatchingEventHandler):
 | 
			
		||||
 | 
			
		||||
        si = SoundInfo(event.src_path)
 | 
			
		||||
        self.sound_kwargs['program'] = program
 | 
			
		||||
        si.get_sound(save = True, **self.sound_kwargs)
 | 
			
		||||
        si.get_sound(save=True, **self.sound_kwargs)
 | 
			
		||||
        if si.year is not None:
 | 
			
		||||
            si.find_diffusion(program)
 | 
			
		||||
        si.sound.save(True)
 | 
			
		||||
 | 
			
		||||
    def on_deleted(self, event):
 | 
			
		||||
        logger.info('sound deleted: %s', event.src_path)
 | 
			
		||||
        sound = Sound.objects.filter(path = event.src_path)
 | 
			
		||||
        sound = Sound.objects.filter(path=event.src_path)
 | 
			
		||||
        if sound:
 | 
			
		||||
            sound = sound[0]
 | 
			
		||||
            sound.type = sound.Type.removed
 | 
			
		||||
@ -230,7 +232,7 @@ class MonitorHandler(PatternMatchingEventHandler):
 | 
			
		||||
 | 
			
		||||
    def on_moved(self, event):
 | 
			
		||||
        logger.info('sound moved: %s -> %s', event.src_path, event.dest_path)
 | 
			
		||||
        sound = Sound.objects.filter(path = event.src_path)
 | 
			
		||||
        sound = Sound.objects.filter(path=event.src_path)
 | 
			
		||||
        if not sound:
 | 
			
		||||
            self.on_modified(
 | 
			
		||||
                FileModifiedEvent(event.dest_path)
 | 
			
		||||
@ -242,18 +244,19 @@ class MonitorHandler(PatternMatchingEventHandler):
 | 
			
		||||
        if not sound.diffusion:
 | 
			
		||||
            program = Program.get_from_path(event.src_path)
 | 
			
		||||
            if program:
 | 
			
		||||
                si = SoundInfo(sound.path, sound = sound)
 | 
			
		||||
                si = SoundInfo(sound.path, sound=sound)
 | 
			
		||||
                if si.year is not None:
 | 
			
		||||
                    si.find_diffusion(program)
 | 
			
		||||
        sound.save()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Command(BaseCommand):
 | 
			
		||||
    help= __doc__
 | 
			
		||||
    help = __doc__
 | 
			
		||||
 | 
			
		||||
    def report(self, program = None, component = None, *content):
 | 
			
		||||
    def report(self, program=None, component=None, *content):
 | 
			
		||||
        if not component:
 | 
			
		||||
            logger.info('%s: %s', str(program), ' '.join([str(c) for c in content]))
 | 
			
		||||
            logger.info('%s: %s', str(program),
 | 
			
		||||
                        ' '.join([str(c) for c in content]))
 | 
			
		||||
        else:
 | 
			
		||||
            logger.info('%s, %s: %s', str(program), str(component),
 | 
			
		||||
                        ' '.join([str(c) for c in content]))
 | 
			
		||||
@ -270,11 +273,11 @@ class Command(BaseCommand):
 | 
			
		||||
            logger.info('#%d %s', program.id, program.name)
 | 
			
		||||
            self.scan_for_program(
 | 
			
		||||
                program, settings.AIRCOX_SOUND_ARCHIVES_SUBDIR,
 | 
			
		||||
                type = Sound.Type.archive,
 | 
			
		||||
                type=Sound.Type.archive,
 | 
			
		||||
            )
 | 
			
		||||
            self.scan_for_program(
 | 
			
		||||
                program, settings.AIRCOX_SOUND_EXCERPTS_SUBDIR,
 | 
			
		||||
                type = Sound.Type.excerpt,
 | 
			
		||||
                type=Sound.Type.excerpt,
 | 
			
		||||
            )
 | 
			
		||||
            dirs.append(os.path.join(program.path))
 | 
			
		||||
 | 
			
		||||
@ -300,14 +303,14 @@ class Command(BaseCommand):
 | 
			
		||||
 | 
			
		||||
            si = SoundInfo(path)
 | 
			
		||||
            sound_kwargs['program'] = program
 | 
			
		||||
            si.get_sound(save = True, **sound_kwargs)
 | 
			
		||||
            si.find_diffusion(program, save = True)
 | 
			
		||||
            si.get_sound(save=True, **sound_kwargs)
 | 
			
		||||
            si.find_diffusion(program, save=True)
 | 
			
		||||
            si.find_playlist(si.sound)
 | 
			
		||||
            sounds.append(si.sound.pk)
 | 
			
		||||
 | 
			
		||||
        # sounds in db & unchecked
 | 
			
		||||
        sounds = Sound.objects.filter(path__startswith = subdir). \
 | 
			
		||||
                               exclude(pk__in = sounds)
 | 
			
		||||
        sounds = Sound.objects.filter(path__startswith=subdir). \
 | 
			
		||||
            exclude(pk__in=sounds)
 | 
			
		||||
        self.check_sounds(sounds)
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
@ -318,18 +321,18 @@ class Command(BaseCommand):
 | 
			
		||||
        # check files
 | 
			
		||||
        for sound in qs:
 | 
			
		||||
            if sound.check_on_file():
 | 
			
		||||
                sound.save(check = False)
 | 
			
		||||
                sound.save(check=False)
 | 
			
		||||
 | 
			
		||||
    def check_quality(self, check = False):
 | 
			
		||||
    def check_quality(self, check=False):
 | 
			
		||||
        """
 | 
			
		||||
        Check all files where quality has been set to bad
 | 
			
		||||
        """
 | 
			
		||||
        import aircox.management.commands.sounds_quality_check \
 | 
			
		||||
                as quality_check
 | 
			
		||||
            as quality_check
 | 
			
		||||
 | 
			
		||||
        # get available sound files
 | 
			
		||||
        sounds = Sound.objects.filter(good_quality = False) \
 | 
			
		||||
                      .exclude(type = Sound.Type.removed)
 | 
			
		||||
        sounds = Sound.objects.filter(good_quality=False) \
 | 
			
		||||
                      .exclude(type=Sound.Type.removed)
 | 
			
		||||
        if check:
 | 
			
		||||
            self.check_sounds(sounds)
 | 
			
		||||
 | 
			
		||||
@ -341,11 +344,12 @@ class Command(BaseCommand):
 | 
			
		||||
        # check quality
 | 
			
		||||
        logger.info('quality check...',)
 | 
			
		||||
        cmd = quality_check.Command()
 | 
			
		||||
        cmd.handle( files = files,
 | 
			
		||||
                    **settings.AIRCOX_SOUND_QUALITY )
 | 
			
		||||
        cmd.handle(files=files,
 | 
			
		||||
                   **settings.AIRCOX_SOUND_QUALITY)
 | 
			
		||||
 | 
			
		||||
        # update stats
 | 
			
		||||
        logger.info('update stats in database')
 | 
			
		||||
 | 
			
		||||
        def update_stats(sound_info, sound):
 | 
			
		||||
            stats = sound_info.get_file_stats()
 | 
			
		||||
            if stats:
 | 
			
		||||
@ -353,25 +357,25 @@ class Command(BaseCommand):
 | 
			
		||||
                sound.duration = utils.seconds_to_time(duration)
 | 
			
		||||
 | 
			
		||||
        for sound_info in cmd.good:
 | 
			
		||||
            sound = Sound.objects.get(path = sound_info.path)
 | 
			
		||||
            sound = Sound.objects.get(path=sound_info.path)
 | 
			
		||||
            sound.good_quality = True
 | 
			
		||||
            update_stats(sound_info, sound)
 | 
			
		||||
            sound.save(check = False)
 | 
			
		||||
            sound.save(check=False)
 | 
			
		||||
 | 
			
		||||
        for sound_info in cmd.bad:
 | 
			
		||||
            sound = Sound.objects.get(path = sound_info.path)
 | 
			
		||||
            sound = Sound.objects.get(path=sound_info.path)
 | 
			
		||||
            update_stats(sound_info, sound)
 | 
			
		||||
            sound.save(check = False)
 | 
			
		||||
            sound.save(check=False)
 | 
			
		||||
 | 
			
		||||
    def monitor(self):
 | 
			
		||||
        """
 | 
			
		||||
        Run in monitor mode
 | 
			
		||||
        """
 | 
			
		||||
        archives_handler = MonitorHandler(
 | 
			
		||||
            subdir = settings.AIRCOX_SOUND_ARCHIVES_SUBDIR
 | 
			
		||||
            subdir=settings.AIRCOX_SOUND_ARCHIVES_SUBDIR
 | 
			
		||||
        )
 | 
			
		||||
        excerpts_handler = MonitorHandler(
 | 
			
		||||
            subdir = settings.AIRCOX_SOUND_EXCERPTS_SUBDIR
 | 
			
		||||
            subdir=settings.AIRCOX_SOUND_EXCERPTS_SUBDIR
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        observer = Observer()
 | 
			
		||||
@ -390,10 +394,10 @@ class Command(BaseCommand):
 | 
			
		||||
            time.sleep(1)
 | 
			
		||||
 | 
			
		||||
    def add_arguments(self, parser):
 | 
			
		||||
        parser.formatter_class=RawTextHelpFormatter
 | 
			
		||||
        parser.formatter_class = RawTextHelpFormatter
 | 
			
		||||
        parser.add_argument(
 | 
			
		||||
            '-q', '--quality_check', action='store_true',
 | 
			
		||||
            help='Enable quality check using sound_quality_check on all ' \
 | 
			
		||||
            help='Enable quality check using sound_quality_check on all '
 | 
			
		||||
                 'sounds marqued as not good'
 | 
			
		||||
        )
 | 
			
		||||
        parser.add_argument(
 | 
			
		||||
@ -411,7 +415,6 @@ class Command(BaseCommand):
 | 
			
		||||
        if options.get('scan'):
 | 
			
		||||
            self.scan()
 | 
			
		||||
        if options.get('quality_check'):
 | 
			
		||||
            self.check_quality(check = (not options.get('scan')) )
 | 
			
		||||
            self.check_quality(check=(not options.get('scan')))
 | 
			
		||||
        if options.get('monitor'):
 | 
			
		||||
            self.monitor()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -82,17 +82,14 @@ class Monitor:
 | 
			
		||||
        """
 | 
			
		||||
        Last sound log of monitored station that occurred on_air
 | 
			
		||||
        """
 | 
			
		||||
        return self.get_last_log(type = Log.Type.on_air,
 | 
			
		||||
                                 sound__isnull = False)
 | 
			
		||||
        return self.get_last_log(type=Log.Type.on_air, sound__isnull=False)
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def last_diff_start(self):
 | 
			
		||||
        """
 | 
			
		||||
        Log of last triggered item (sound or diffusion)
 | 
			
		||||
        """
 | 
			
		||||
        return self.get_last_log(type = Log.Type.start,
 | 
			
		||||
                                 diffusion__isnull = False)
 | 
			
		||||
 | 
			
		||||
        return self.get_last_log(type=Log.Type.start, diffusion__isnull=False)
 | 
			
		||||
 | 
			
		||||
    def __init__(self, station, **kwargs):
 | 
			
		||||
        self.station = station
 | 
			
		||||
@ -120,12 +117,11 @@ class Monitor:
 | 
			
		||||
        self.sync_playlists()
 | 
			
		||||
        self.handle()
 | 
			
		||||
 | 
			
		||||
    def log(self, date = None, **kwargs):
 | 
			
		||||
    def log(self, date=None, **kwargs):
 | 
			
		||||
        """
 | 
			
		||||
        Create a log using **kwargs, and print info
 | 
			
		||||
        """
 | 
			
		||||
        log = Log(station = self.station, date = date or tz.now(),
 | 
			
		||||
                  **kwargs)
 | 
			
		||||
        log = Log(station=self.station, date=date or tz.now(), **kwargs)
 | 
			
		||||
        log.save()
 | 
			
		||||
        log.print()
 | 
			
		||||
        return log
 | 
			
		||||
@ -142,14 +138,14 @@ class Monitor:
 | 
			
		||||
        air_times = (air_time - delta, air_time + delta)
 | 
			
		||||
 | 
			
		||||
        log = self.log_qs.on_air().filter(
 | 
			
		||||
            source = source.id, sound__path = sound_path,
 | 
			
		||||
            date__range = air_times,
 | 
			
		||||
            source=source.id, sound__path=sound_path,
 | 
			
		||||
            date__range=air_times,
 | 
			
		||||
        ).last()
 | 
			
		||||
        if log:
 | 
			
		||||
            return log
 | 
			
		||||
 | 
			
		||||
        # get sound
 | 
			
		||||
        sound = Sound.objects.filter(path = sound_path) \
 | 
			
		||||
        sound = Sound.objects.filter(path=sound_path) \
 | 
			
		||||
                     .select_related('diffusion').first()
 | 
			
		||||
        diff = None
 | 
			
		||||
        if sound and sound.diffusion:
 | 
			
		||||
@ -157,20 +153,16 @@ class Monitor:
 | 
			
		||||
            # check for reruns
 | 
			
		||||
            if not diff.is_date_in_range(air_time) and not diff.initial:
 | 
			
		||||
                diff = Diffusion.objects.at(air_time) \
 | 
			
		||||
                                .filter(initial = diff).first()
 | 
			
		||||
                                .filter(initial=diff).first()
 | 
			
		||||
 | 
			
		||||
        # log sound on air
 | 
			
		||||
        return self.log(
 | 
			
		||||
            type = Log.Type.on_air,
 | 
			
		||||
            source = source.id,
 | 
			
		||||
            date = source.on_air,
 | 
			
		||||
            sound = sound,
 | 
			
		||||
            diffusion = diff,
 | 
			
		||||
            type=Log.Type.on_air, source=source.id, date=source.on_air,
 | 
			
		||||
            sound=sound, diffusion=diff,
 | 
			
		||||
            # if sound is removed, we keep sound path info
 | 
			
		||||
            comment = sound_path,
 | 
			
		||||
            comment=sound_path,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    def trace_tracks(self, log):
 | 
			
		||||
        """
 | 
			
		||||
        Log tracks for the given sound log (for streamed programs only).
 | 
			
		||||
@ -178,23 +170,21 @@ class Monitor:
 | 
			
		||||
        if log.diffusion:
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        tracks = Track.objects.related(object = log.sound) \
 | 
			
		||||
                              .filter(in_seconds = True)
 | 
			
		||||
        tracks = Track.objects.filter(sound=log.sound, timestamp_isnull=False)
 | 
			
		||||
        if not tracks.exists():
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        tracks = tracks.exclude(log__station = self.station,
 | 
			
		||||
                                log__pk__gt = log.pk)
 | 
			
		||||
        tracks = tracks.exclude(log__station=self.station, log__pk__gt=log.pk)
 | 
			
		||||
        now = tz.now()
 | 
			
		||||
        for track in tracks:
 | 
			
		||||
            pos = log.date + tz.timedelta(seconds = track.position)
 | 
			
		||||
            pos = log.date + tz.timedelta(seconds=track.position)
 | 
			
		||||
            if pos > now:
 | 
			
		||||
                break
 | 
			
		||||
            # log track on air
 | 
			
		||||
            self.log(
 | 
			
		||||
                type = Log.Type.on_air, source = log.source,
 | 
			
		||||
                date = pos, track = track,
 | 
			
		||||
                comment = track,
 | 
			
		||||
                type=Log.Type.on_air, source=log.source,
 | 
			
		||||
                date=pos, track=track,
 | 
			
		||||
                comment=track,
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
    def sync_playlists(self):
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										748
									
								
								aircox/models.py
									
									
									
									
									
								
							
							
						
						
									
										748
									
								
								aircox/models.py
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										6
									
								
								aircox/templates/admin/aircox/playlist_inline.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								aircox/templates/admin/aircox/playlist_inline.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,6 @@
 | 
			
		||||
{% load static i18n %}
 | 
			
		||||
 | 
			
		||||
{% with inline_admin_formset.formset.instance as playlist %}
 | 
			
		||||
{% include "adminsortable2/tabular.html" %}
 | 
			
		||||
{% endwith %}
 | 
			
		||||
 | 
			
		||||
@ -78,8 +78,7 @@ A stream is a source that:
 | 
			
		||||
- is interactive
 | 
			
		||||
{% endcomment %}
 | 
			
		||||
def stream (id, file) =
 | 
			
		||||
    s = playlist(id = '#{id}_playlist', mode = "random", reload_mode='watch',
 | 
			
		||||
                 file)
 | 
			
		||||
    s = playlist(id = '#{id}_playlist', mode = "random", reload_mode='watch', file)
 | 
			
		||||
    interactive_source(id, s)
 | 
			
		||||
end
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
@ -25,7 +25,7 @@ class Stations:
 | 
			
		||||
        if self.fetch_timeout and self.fetch_timeout > tz.now():
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        self.fetch_timeout = tz.now() + tz.timedelta(seconds = 5)
 | 
			
		||||
        self.fetch_timeout = tz.now() + tz.timedelta(seconds=5)
 | 
			
		||||
        for station in self.stations:
 | 
			
		||||
            station.streamer.fetch()
 | 
			
		||||
 | 
			
		||||
@ -39,62 +39,54 @@ def on_air(request):
 | 
			
		||||
    except:
 | 
			
		||||
        cms = None
 | 
			
		||||
 | 
			
		||||
    station = request.GET.get('station');
 | 
			
		||||
    station = request.GET.get('station')
 | 
			
		||||
    if station:
 | 
			
		||||
        # FIXME: by name???
 | 
			
		||||
        station = stations.stations.filter(name = station)
 | 
			
		||||
        station = stations.stations.filter(name=station)
 | 
			
		||||
        if not station.count():
 | 
			
		||||
            return HttpResponse('{}')
 | 
			
		||||
    else:
 | 
			
		||||
        station = stations.stations
 | 
			
		||||
 | 
			
		||||
    station = station.first()
 | 
			
		||||
    on_air = station.on_air(count = 10).select_related('track','diffusion')
 | 
			
		||||
    on_air = station.on_air(count=10).select_related('track', 'diffusion')
 | 
			
		||||
    if not on_air.count():
 | 
			
		||||
        return HttpResponse('')
 | 
			
		||||
 | 
			
		||||
    last = on_air.first()
 | 
			
		||||
    if last.track:
 | 
			
		||||
        last = {
 | 
			
		||||
            'type': 'track',
 | 
			
		||||
            'artist': last.related.artist,
 | 
			
		||||
            'title': last.related.title,
 | 
			
		||||
            'date': last.date,
 | 
			
		||||
        }
 | 
			
		||||
        last = {'date': last.date, 'type': 'track',
 | 
			
		||||
                'artist': last.track.artist, 'title': last.track.title}
 | 
			
		||||
    else:
 | 
			
		||||
        try:
 | 
			
		||||
            diff = last.diffusion
 | 
			
		||||
            publication = None
 | 
			
		||||
            # FIXME CMS
 | 
			
		||||
            if cms:
 | 
			
		||||
                publication = \
 | 
			
		||||
                    cms.DiffusionPage.objects.filter(
 | 
			
		||||
                        diffusion = diff.initial or diff).first() or \
 | 
			
		||||
                        diffusion=diff.initial or diff).first() or \
 | 
			
		||||
                    cms.ProgramPage.objects.filter(
 | 
			
		||||
                        program = last.program).first()
 | 
			
		||||
                        program=last.program).first()
 | 
			
		||||
        except:
 | 
			
		||||
            pass
 | 
			
		||||
 | 
			
		||||
        last = {
 | 
			
		||||
            'type': 'diffusion',
 | 
			
		||||
            'title': diff.program.name,
 | 
			
		||||
            'date': diff.start,
 | 
			
		||||
            'url': publication.specific.url if publication else None,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        last = {'date': diff.start, 'type': 'diffusion',
 | 
			
		||||
                'title': diff.program.name,
 | 
			
		||||
                'url': publication.specific.url if publication else None}
 | 
			
		||||
    last['date'] = str(last['date'])
 | 
			
		||||
    return HttpResponse(json.dumps(last))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# TODO:
 | 
			
		||||
#   - login url
 | 
			
		||||
class Monitor(View,TemplateResponseMixin,LoginRequiredMixin):
 | 
			
		||||
class Monitor(View, TemplateResponseMixin, LoginRequiredMixin):
 | 
			
		||||
    template_name = 'aircox/controllers/monitor.html'
 | 
			
		||||
 | 
			
		||||
    def get_context_data(self, **kwargs):
 | 
			
		||||
        stations.fetch()
 | 
			
		||||
        return { 'stations': stations.stations }
 | 
			
		||||
        return {'stations': stations.stations}
 | 
			
		||||
 | 
			
		||||
    def get(self, request = None, **kwargs):
 | 
			
		||||
    def get(self, request=None, **kwargs):
 | 
			
		||||
        if not request.user.is_active:
 | 
			
		||||
            return Http404()
 | 
			
		||||
 | 
			
		||||
@ -102,7 +94,7 @@ class Monitor(View,TemplateResponseMixin,LoginRequiredMixin):
 | 
			
		||||
        context = self.get_context_data(**kwargs)
 | 
			
		||||
        return render(request, self.template_name, context)
 | 
			
		||||
 | 
			
		||||
    def post(self, request = None, **kwargs):
 | 
			
		||||
    def post(self, request=None, **kwargs):
 | 
			
		||||
        if not request.user.is_active:
 | 
			
		||||
            return Http404()
 | 
			
		||||
 | 
			
		||||
@ -113,15 +105,15 @@ class Monitor(View,TemplateResponseMixin,LoginRequiredMixin):
 | 
			
		||||
        controller = POST.get('controller')
 | 
			
		||||
        action = POST.get('action')
 | 
			
		||||
 | 
			
		||||
        station = stations.stations.filter(name = POST.get('station')) \
 | 
			
		||||
        station = stations.stations.filter(name=POST.get('station')) \
 | 
			
		||||
                                   .first()
 | 
			
		||||
        if not station:
 | 
			
		||||
            return Http404()
 | 
			
		||||
 | 
			
		||||
        source = None
 | 
			
		||||
        if 'source' in POST:
 | 
			
		||||
            source = [ s for s in station.sources
 | 
			
		||||
                        if s.name == POST['source'] ]
 | 
			
		||||
            source = [s for s in station.sources
 | 
			
		||||
                      if s.name == POST['source']]
 | 
			
		||||
            source = source[0]
 | 
			
		||||
            if not source:
 | 
			
		||||
                return Http404
 | 
			
		||||
@ -141,11 +133,11 @@ class Monitor(View,TemplateResponseMixin,LoginRequiredMixin):
 | 
			
		||||
        source.restart()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class StatisticsView(View,TemplateResponseMixin,LoginRequiredMixin):
 | 
			
		||||
class StatisticsView(View, TemplateResponseMixin, LoginRequiredMixin):
 | 
			
		||||
    """
 | 
			
		||||
    View for statistics.
 | 
			
		||||
    """
 | 
			
		||||
    # we cannot manipulate queryset, since we have to be able to read from archives
 | 
			
		||||
    # we cannot manipulate queryset: we have to be able to read from archives
 | 
			
		||||
    template_name = 'aircox/controllers/stats.html'
 | 
			
		||||
 | 
			
		||||
    class Item:
 | 
			
		||||
@ -179,7 +171,7 @@ class StatisticsView(View,TemplateResponseMixin,LoginRequiredMixin):
 | 
			
		||||
            self.__dict__.update(kwargs)
 | 
			
		||||
 | 
			
		||||
        # Note: one row contains a column for diffusions and one for streams
 | 
			
		||||
        #def append(self, log):
 | 
			
		||||
        # def append(self, log):
 | 
			
		||||
        #    if log.col == 0:
 | 
			
		||||
        #        self.rows.append((log, []))
 | 
			
		||||
        #        return
 | 
			
		||||
@ -194,13 +186,12 @@ class StatisticsView(View,TemplateResponseMixin,LoginRequiredMixin):
 | 
			
		||||
        #    # all other cases: new row
 | 
			
		||||
        #    self.rows.append((None, [log]))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    def get_stats(self, station, date):
 | 
			
		||||
        """
 | 
			
		||||
        Return statistics for the given station and date.
 | 
			
		||||
        """
 | 
			
		||||
        stats = self.Stats(station = station, date = date,
 | 
			
		||||
                           items = [], tags = {})
 | 
			
		||||
        stats = self.Stats(station=station, date=date,
 | 
			
		||||
                           items=[], tags={})
 | 
			
		||||
 | 
			
		||||
        qs = Log.objects.station(station).on_air() \
 | 
			
		||||
                .prefetch_related('diffusion', 'sound', 'track', 'track__tags')
 | 
			
		||||
@ -209,37 +200,26 @@ class StatisticsView(View,TemplateResponseMixin,LoginRequiredMixin):
 | 
			
		||||
 | 
			
		||||
        sound_log = None
 | 
			
		||||
        for log in qs:
 | 
			
		||||
            rel = None
 | 
			
		||||
            item = None
 | 
			
		||||
 | 
			
		||||
            rel, item = None, None
 | 
			
		||||
            if log.diffusion:
 | 
			
		||||
                rel = log.diffusion
 | 
			
		||||
                item = self.Item(
 | 
			
		||||
                    name = rel.program.name,
 | 
			
		||||
                    type = _('Diffusion'),
 | 
			
		||||
                    col = 0,
 | 
			
		||||
                    tracks = models.Track.objects.related(object = rel)
 | 
			
		||||
                                         .prefetch_related('tags'),
 | 
			
		||||
                rel, item = log.diffusion, self.Item(
 | 
			
		||||
                    name=rel.program.name, type=_('Diffusion'), col=0,
 | 
			
		||||
                    tracks=models.Track.objects.filter(diffusion=log.diffusion)
 | 
			
		||||
                                       .prefetch_related('tags'),
 | 
			
		||||
                )
 | 
			
		||||
                sound_log = None
 | 
			
		||||
            elif log.sound:
 | 
			
		||||
                rel = log.sound
 | 
			
		||||
                item = self.Item(
 | 
			
		||||
                    name = rel.program.name + ': ' + os.path.basename(rel.path),
 | 
			
		||||
                    type = _('Stream'),
 | 
			
		||||
                    col = 1,
 | 
			
		||||
                    tracks = [],
 | 
			
		||||
                rel, item = log.sound, self.Item(
 | 
			
		||||
                    name=rel.program.name + ': ' + os.path.basename(rel.path),
 | 
			
		||||
                    type=_('Stream'), col=1, tracks=[],
 | 
			
		||||
                )
 | 
			
		||||
                sound_log = item
 | 
			
		||||
            elif log.track:
 | 
			
		||||
                # append to last sound log
 | 
			
		||||
                if not sound_log:
 | 
			
		||||
                    # TODO: create item ? should never happen
 | 
			
		||||
                    continue
 | 
			
		||||
 | 
			
		||||
                sound_log.tracks.append(log.track)
 | 
			
		||||
                sound_log.end = log.end
 | 
			
		||||
                sound_log
 | 
			
		||||
                continue
 | 
			
		||||
 | 
			
		||||
            item.date = log.date
 | 
			
		||||
@ -247,7 +227,6 @@ class StatisticsView(View,TemplateResponseMixin,LoginRequiredMixin):
 | 
			
		||||
            item.related = rel
 | 
			
		||||
            # stats.append(item)
 | 
			
		||||
            stats.items.append(item)
 | 
			
		||||
 | 
			
		||||
        return stats
 | 
			
		||||
 | 
			
		||||
    def get_context_data(self, **kwargs):
 | 
			
		||||
@ -270,7 +249,7 @@ class StatisticsView(View,TemplateResponseMixin,LoginRequiredMixin):
 | 
			
		||||
 | 
			
		||||
        return context
 | 
			
		||||
 | 
			
		||||
    def get(self, request = None, **kwargs):
 | 
			
		||||
    def get(self, request=None, **kwargs):
 | 
			
		||||
        if not request.user.is_active:
 | 
			
		||||
            return Http404()
 | 
			
		||||
 | 
			
		||||
@ -279,4 +258,3 @@ class StatisticsView(View,TemplateResponseMixin,LoginRequiredMixin):
 | 
			
		||||
        return render(request, self.template_name, context)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										0
									
								
								aircox_web/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								aircox_web/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										68
									
								
								aircox_web/admin.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								aircox_web/admin.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,68 @@
 | 
			
		||||
import copy
 | 
			
		||||
 | 
			
		||||
from django.contrib import admin
 | 
			
		||||
from django.utils.translation import ugettext_lazy as _
 | 
			
		||||
 | 
			
		||||
from content_editor.admin import ContentEditor
 | 
			
		||||
from feincms3 import plugins
 | 
			
		||||
from feincms3.admin import TreeAdmin
 | 
			
		||||
 | 
			
		||||
from aircox import models as aircox
 | 
			
		||||
from . import models
 | 
			
		||||
from aircox.admin.playlist import TracksInline
 | 
			
		||||
from aircox.admin.mixins import UnrelatedInlineMixin
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@admin.register(models.SiteSettings)
 | 
			
		||||
class SettingsAdmin(admin.ModelAdmin):
 | 
			
		||||
    pass
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PageDiffusionPlaylist(UnrelatedInlineMixin, TracksInline):
 | 
			
		||||
    parent_model = aircox.Diffusion
 | 
			
		||||
    fields = list(TracksInline.fields)
 | 
			
		||||
    fields.remove('timestamp')
 | 
			
		||||
 | 
			
		||||
    def get_parent(self, view_obj):
 | 
			
		||||
        return view_obj and view_obj.diffusion
 | 
			
		||||
 | 
			
		||||
    def save_parent(self, parent, view_obj):
 | 
			
		||||
        parent.save()
 | 
			
		||||
        view_obj.diffusion = parent
 | 
			
		||||
        view_obj.save()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@admin.register(models.Page)
 | 
			
		||||
class PageAdmin(ContentEditor, TreeAdmin):
 | 
			
		||||
    list_display = ["indented_title", "move_column", "is_active"]
 | 
			
		||||
    prepopulated_fields = {"slug": ("title",)}
 | 
			
		||||
    # readonly_fields = ('diffusion',)
 | 
			
		||||
 | 
			
		||||
    fieldsets = (
 | 
			
		||||
        (_('Main'), {
 | 
			
		||||
            'fields': ['title', 'slug', 'by_program', 'summary'],
 | 
			
		||||
            'classes': ('tabbed', 'uncollapse')
 | 
			
		||||
        }),
 | 
			
		||||
        (_('Settings'), {
 | 
			
		||||
            'fields': ['show_author', 'featured', 'allow_comments',
 | 
			
		||||
                       'status', 'static_path', 'path'],
 | 
			
		||||
            'classes': ('tabbed',)
 | 
			
		||||
        }),
 | 
			
		||||
        (_('Infos'), {
 | 
			
		||||
            'fields': ['diffusion'],
 | 
			
		||||
            'classes': ('tabbed',)
 | 
			
		||||
        }),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    inlines = [
 | 
			
		||||
        plugins.richtext.RichTextInline.create(models.RichText),
 | 
			
		||||
        plugins.image.ImageInline.create(models.Image),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    def get_inline_instances(self, request, obj=None):
 | 
			
		||||
        inlines = super().get_inline_instances(request, obj)
 | 
			
		||||
        if obj and obj.diffusion:
 | 
			
		||||
            inlines.insert(0, PageDiffusionPlaylist(self.model, self.admin_site))
 | 
			
		||||
        return inlines
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										5
									
								
								aircox_web/apps.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								aircox_web/apps.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,5 @@
 | 
			
		||||
from django.apps import AppConfig
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AircoxWebConfig(AppConfig):
 | 
			
		||||
    name = 'aircox_web'
 | 
			
		||||
							
								
								
									
										1
									
								
								aircox_web/assets/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								aircox_web/assets/index.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1 @@
 | 
			
		||||
import './js';
 | 
			
		||||
							
								
								
									
										12
									
								
								aircox_web/assets/js/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								aircox_web/assets/js/index.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,12 @@
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import Buefy from 'buefy';
 | 
			
		||||
import 'buefy/dist/buefy.css';
 | 
			
		||||
 | 
			
		||||
Vue.use(Buefy);
 | 
			
		||||
 | 
			
		||||
var app = new Vue({
 | 
			
		||||
  el: '#app',
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										117
									
								
								aircox_web/models.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										117
									
								
								aircox_web/models.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,117 @@
 | 
			
		||||
from django.db import models
 | 
			
		||||
from django.utils.translation import ugettext_lazy as _
 | 
			
		||||
from django.contrib.auth import models as auth
 | 
			
		||||
 | 
			
		||||
from content_editor.models import Region, create_plugin_base
 | 
			
		||||
from feincms3 import plugins
 | 
			
		||||
from feincms3.pages import AbstractPage
 | 
			
		||||
 | 
			
		||||
from model_utils.models import TimeStampedModel, StatusModel
 | 
			
		||||
from model_utils import Choices
 | 
			
		||||
from filer.fields.image import FilerImageField
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
from aircox import models as aircox
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SiteSettings(models.Model):
 | 
			
		||||
    station = models.ForeignKey(
 | 
			
		||||
        aircox.Station, on_delete=models.SET_NULL, null=True,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    # main settings
 | 
			
		||||
    title = models.CharField(
 | 
			
		||||
        _('Title'), max_length=32,
 | 
			
		||||
        help_text=_('Website title used at various places'),
 | 
			
		||||
    )
 | 
			
		||||
    logo = FilerImageField(
 | 
			
		||||
        on_delete=models.SET_NULL, null=True, blank=True,
 | 
			
		||||
        verbose_name=_('Logo'),
 | 
			
		||||
        related_name='+',
 | 
			
		||||
    )
 | 
			
		||||
    favicon = FilerImageField(
 | 
			
		||||
        on_delete=models.SET_NULL, null=True, blank=True,
 | 
			
		||||
        verbose_name=_('Favicon'),
 | 
			
		||||
        related_name='+',
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    # meta descriptors
 | 
			
		||||
    description = models.CharField(
 | 
			
		||||
        _('Description'), max_length=128,
 | 
			
		||||
        blank=True, null=True,
 | 
			
		||||
    )
 | 
			
		||||
    tags = models.CharField(
 | 
			
		||||
        _('Tags'), max_length=128,
 | 
			
		||||
        blank=True, null=True,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Page(AbstractPage, TimeStampedModel, StatusModel):
 | 
			
		||||
    STATUS = Choices('draft', 'published')
 | 
			
		||||
    regions = [
 | 
			
		||||
        Region(key="main", title=_("Content")),
 | 
			
		||||
        Region(key="sidebar", title=_("Sidebar")),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    # metadata
 | 
			
		||||
    by = models.ForeignKey(
 | 
			
		||||
        auth.User, models.SET_NULL, blank=True, null=True,
 | 
			
		||||
        verbose_name=_('Author'),
 | 
			
		||||
    )
 | 
			
		||||
    by_program = models.ForeignKey(
 | 
			
		||||
        aircox.Program, models.SET_NULL, blank=True, null=True,
 | 
			
		||||
        related_name='authored_pages',
 | 
			
		||||
        limit_choices_to={'schedule__isnull': False},
 | 
			
		||||
        verbose_name=_('Show program as author'),
 | 
			
		||||
        help_text=_("If nothing is selected, display user's name"),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    # options
 | 
			
		||||
    show_author = models.BooleanField(
 | 
			
		||||
        _('Show author'), default=True,
 | 
			
		||||
    )
 | 
			
		||||
    featured = models.BooleanField(
 | 
			
		||||
        _('featured'), default=False,
 | 
			
		||||
    )
 | 
			
		||||
    allow_comments = models.BooleanField(
 | 
			
		||||
        _('allow comments'), default=True,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    # content
 | 
			
		||||
    title = models.CharField(
 | 
			
		||||
        _('title'), max_length=64,
 | 
			
		||||
    )
 | 
			
		||||
    summary = models.TextField(
 | 
			
		||||
        _('Summary'),
 | 
			
		||||
        max_length=128, blank=True, null=True,
 | 
			
		||||
    )
 | 
			
		||||
    cover = FilerImageField(
 | 
			
		||||
        on_delete=models.SET_NULL, null=True, blank=True,
 | 
			
		||||
        verbose_name=_('Cover'),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    diffusion = models.OneToOneField(
 | 
			
		||||
        aircox.Diffusion, models.CASCADE,
 | 
			
		||||
        blank=True, null=True,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
PagePlugin = create_plugin_base(Page)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class RichText(plugins.richtext.RichText, PagePlugin):
 | 
			
		||||
    pass
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Image(plugins.image.Image, PagePlugin):
 | 
			
		||||
    caption = models.CharField(_("caption"), max_length=200, blank=True)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ProgramPage(Page):
 | 
			
		||||
    program = models.OneToOneField(
 | 
			
		||||
        aircox.Program, models.CASCADE,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										26
									
								
								aircox_web/package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								aircox_web/package.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,26 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "aircox-web-assets",
 | 
			
		||||
  "version": "0.0.0",
 | 
			
		||||
  "description": "Assets for Aircox Web",
 | 
			
		||||
  "main": "index.js",
 | 
			
		||||
  "author": "bkfox",
 | 
			
		||||
  "license": "AGPL",
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@fortawesome/fontawesome-free": "^5.8.2",
 | 
			
		||||
    "mini-css-extract-plugin": "^0.5.0",
 | 
			
		||||
    "css-loader": "^2.1.1",
 | 
			
		||||
    "file-loader": "^3.0.1",
 | 
			
		||||
    "ttf-loader": "^1.0.2",
 | 
			
		||||
    "vue-loader": "^15.7.0",
 | 
			
		||||
    "vue-style-loader": "^4.1.2",
 | 
			
		||||
    "webpack": "^4.32.2",
 | 
			
		||||
    "webpack-bundle-analyzer": "^3.3.2",
 | 
			
		||||
    "webpack-bundle-tracker": "^0.4.2-beta",
 | 
			
		||||
    "webpack-cli": "^3.3.2"
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "bootstrap": "^4.3.1",
 | 
			
		||||
    "buefy": "^0.7.8",
 | 
			
		||||
    "vue": "^2.6.10"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										20
									
								
								aircox_web/renderer.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								aircox_web/renderer.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,20 @@
 | 
			
		||||
from django.utils.html import format_html, mark_safe
 | 
			
		||||
from feincms3.renderer import TemplatePluginRenderer
 | 
			
		||||
 | 
			
		||||
from .models import Page, RichText, Image
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
renderer = TemplatePluginRenderer()
 | 
			
		||||
renderer.register_string_renderer(
 | 
			
		||||
    RichText,
 | 
			
		||||
    lambda plugin: mark_safe(plugin.text),
 | 
			
		||||
)
 | 
			
		||||
renderer.register_string_renderer(
 | 
			
		||||
    Image,
 | 
			
		||||
    lambda plugin: format_html(
 | 
			
		||||
        '<figure><img src="{}" alt=""/><figcaption>{}</figcaption></figure>',
 | 
			
		||||
        plugin.image.url,
 | 
			
		||||
        plugin.caption,
 | 
			
		||||
    ),
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										34
									
								
								aircox_web/templates/aircox_web/base.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								aircox_web/templates/aircox_web/base.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,34 @@
 | 
			
		||||
{% load static thumbnail %}
 | 
			
		||||
<html>
 | 
			
		||||
    <head>
 | 
			
		||||
        <meta charset="utf-8">
 | 
			
		||||
        <meta name="application-name" content="aircox">
 | 
			
		||||
        <meta name="description" content="{{ site_settings.description }}">
 | 
			
		||||
        <meta name="keywords" content="{{ site_settings.tags }}">
 | 
			
		||||
        <link rel="icon" href="{% thumbnail site_settings.favicon 32x32 crop %}" />
 | 
			
		||||
 | 
			
		||||
        {% block assets %}
 | 
			
		||||
        <link rel="stylesheet" type="text/css" href="{% static "aircox_web/assets/main.css" %}"/>
 | 
			
		||||
        <link rel="stylesheet" type="text/css" href="{% static "aircox_web/assets/vendor.css" %}"/>
 | 
			
		||||
 | 
			
		||||
        <script src="{% static "aircox_web/assets/main.js" %}"></script>
 | 
			
		||||
        <script src="{% static "aircox_web/assets/vendor.js" %}"></script>
 | 
			
		||||
        {% endblock %}
 | 
			
		||||
 | 
			
		||||
        <title>{% block title %}{{ site_settings.title }}{% endblock %}</title>
 | 
			
		||||
 | 
			
		||||
        {% block extra_head %}{% endblock %}
 | 
			
		||||
    </head>
 | 
			
		||||
    <body id="app">
 | 
			
		||||
        <nav class="navbar" role="navigation" aria-label="main navigation">
 | 
			
		||||
        </nav>
 | 
			
		||||
 | 
			
		||||
        <main>
 | 
			
		||||
            {% block main %}
 | 
			
		||||
 | 
			
		||||
            {% endblock main %}
 | 
			
		||||
        </main>
 | 
			
		||||
    </body>
 | 
			
		||||
</html>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										8
									
								
								aircox_web/templates/aircox_web/page.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								aircox_web/templates/aircox_web/page.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,8 @@
 | 
			
		||||
{% extends "aircox_web/base.html" %}
 | 
			
		||||
 | 
			
		||||
{% block title %}{{ page.title }} -- {{ block.super }}{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block main %}
 | 
			
		||||
<h1 class="title">{{ page.title }}</h1>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										3
									
								
								aircox_web/tests.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								aircox_web/tests.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,3 @@
 | 
			
		||||
from django.test import TestCase
 | 
			
		||||
 | 
			
		||||
# Create your tests here.
 | 
			
		||||
							
								
								
									
										9
									
								
								aircox_web/urls.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								aircox_web/urls.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,9 @@
 | 
			
		||||
from django.conf.urls import url
 | 
			
		||||
 | 
			
		||||
from . import views
 | 
			
		||||
 | 
			
		||||
urlpatterns = [
 | 
			
		||||
    url(r"^(?P<path>[-\w/]+)/$", views.page_detail, name="page"),
 | 
			
		||||
    url(r"^$", views.page_detail, name="root"),
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										22
									
								
								aircox_web/views.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								aircox_web/views.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,22 @@
 | 
			
		||||
from django.shortcuts import get_object_or_404, render
 | 
			
		||||
 | 
			
		||||
from feincms3.regions import Regions
 | 
			
		||||
 | 
			
		||||
from .models import SiteSettings, Page
 | 
			
		||||
from .renderer import renderer
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def page_detail(request, path=None):
 | 
			
		||||
    page = get_object_or_404(
 | 
			
		||||
        # TODO: published
 | 
			
		||||
        Page.objects.all(),
 | 
			
		||||
        path="/{}/".format(path) if path else "/",
 | 
			
		||||
    )
 | 
			
		||||
    return render(request, "aircox_web/page.html", {
 | 
			
		||||
        'site_settings': SiteSettings.objects.all().first(),
 | 
			
		||||
        "page": page,
 | 
			
		||||
        "regions": Regions.from_item(
 | 
			
		||||
            page, renderer=renderer, timeout=60
 | 
			
		||||
        ),
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										87
									
								
								aircox_web/webpack.config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								aircox_web/webpack.config.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,87 @@
 | 
			
		||||
const path = require('path');
 | 
			
		||||
const webpack = require('webpack');
 | 
			
		||||
 | 
			
		||||
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
 | 
			
		||||
// const { createLodashAliases } = require('lodash-loader');
 | 
			
		||||
const { VueLoaderPlugin } = require('vue-loader');
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
module.exports = (env, argv) => Object({
 | 
			
		||||
    context: __dirname,
 | 
			
		||||
    entry: './assets/index',
 | 
			
		||||
 | 
			
		||||
    output: {
 | 
			
		||||
        path: path.resolve('static/aircox_web/assets'),
 | 
			
		||||
        filename: '[name].js',
 | 
			
		||||
        chunkFilename: '[name].js',
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    optimization: {
 | 
			
		||||
        usedExports: true,
 | 
			
		||||
        concatenateModules: argv.mode == 'production' ? true : false,
 | 
			
		||||
 | 
			
		||||
        splitChunks: {
 | 
			
		||||
            cacheGroups: {
 | 
			
		||||
                vendor: {
 | 
			
		||||
                    name: 'vendor',
 | 
			
		||||
                    chunks: 'initial',
 | 
			
		||||
                    enforce: true,
 | 
			
		||||
 | 
			
		||||
                    test: /[\\/]node_modules[\\/]/,
 | 
			
		||||
                },
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    plugins: [
 | 
			
		||||
        new MiniCssExtractPlugin({
 | 
			
		||||
            filename: "[name].css",
 | 
			
		||||
            chunkFilename: "[id].css"
 | 
			
		||||
        }),
 | 
			
		||||
        new VueLoaderPlugin(),
 | 
			
		||||
    ],
 | 
			
		||||
 | 
			
		||||
    module: {
 | 
			
		||||
        rules: [
 | 
			
		||||
            {
 | 
			
		||||
                test: /\/node_modules\//,
 | 
			
		||||
                sideEffects: false
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
                test: /\.css$/,
 | 
			
		||||
                use: [ { loader: MiniCssExtractPlugin.loader },
 | 
			
		||||
                       'css-loader' ]
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
                // TODO: remove ttf eot svg
 | 
			
		||||
                test: /\.(ttf|eot|svg|woff2?)$/,
 | 
			
		||||
                use: [{
 | 
			
		||||
                    loader: 'file-loader',
 | 
			
		||||
                    options: {
 | 
			
		||||
                        name: '[name].[ext]',
 | 
			
		||||
                        outputPath: 'fonts/',
 | 
			
		||||
                    }
 | 
			
		||||
                }],
 | 
			
		||||
            },
 | 
			
		||||
            { test: /\.vue$/, use: 'vue-loader' },
 | 
			
		||||
        ],
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    resolve: {
 | 
			
		||||
        alias: {
 | 
			
		||||
            js: path.resolve(__dirname, 'assets/js'),
 | 
			
		||||
            vue: path.resolve(__dirname, 'assets/vue'),
 | 
			
		||||
            css: path.resolve(__dirname, 'assets/css'),
 | 
			
		||||
            vue: 'vue/dist/vue.esm.browser.js',
 | 
			
		||||
            // buefy: 'buefy/dist/buefy.js',
 | 
			
		||||
        },
 | 
			
		||||
        modules: [
 | 
			
		||||
            'assets/css',
 | 
			
		||||
            'assets/js',
 | 
			
		||||
            'assets/vue',
 | 
			
		||||
            './node_modules',
 | 
			
		||||
        ],
 | 
			
		||||
        extensions: ['.js', '.vue', '.css', '.styl', '.ttf']
 | 
			
		||||
    },
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
@ -152,7 +152,6 @@ MIDDLEWARE = (
 | 
			
		||||
    'django.middleware.common.CommonMiddleware',
 | 
			
		||||
    'django.middleware.csrf.CsrfViewMiddleware',
 | 
			
		||||
    'django.contrib.auth.middleware.AuthenticationMiddleware',
 | 
			
		||||
    'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
 | 
			
		||||
    'django.contrib.messages.middleware.MessageMiddleware',
 | 
			
		||||
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
 | 
			
		||||
    'django.middleware.security.SecurityMiddleware',
 | 
			
		||||
 | 
			
		||||
@ -17,24 +17,18 @@ from django.conf import settings
 | 
			
		||||
from django.urls import include, path, re_path
 | 
			
		||||
from django.contrib import admin
 | 
			
		||||
 | 
			
		||||
from wagtail.admin import urls as wagtailadmin_urls
 | 
			
		||||
from wagtail.documents import urls as wagtaildocs_urls
 | 
			
		||||
from wagtail.core import urls as wagtail_urls
 | 
			
		||||
from wagtail.images.views.serve import ServeView
 | 
			
		||||
#from wagtail.admin import urls as wagtailadmin_urls
 | 
			
		||||
#from wagtail.documents import urls as wagtaildocs_urls
 | 
			
		||||
#from wagtail.core import urls as wagtail_urls
 | 
			
		||||
#from wagtail.images.views.serve import ServeView
 | 
			
		||||
 | 
			
		||||
import aircox.urls
 | 
			
		||||
import aircox_web.urls
 | 
			
		||||
 | 
			
		||||
try:
 | 
			
		||||
    urlpatterns = [
 | 
			
		||||
        path('jet/', include('jet.urls', 'jet')),
 | 
			
		||||
        path('admin/', admin.site.urls),
 | 
			
		||||
        path('aircox/', include(aircox.urls.urls)),
 | 
			
		||||
 | 
			
		||||
        # cms
 | 
			
		||||
        path('cms/', include(wagtailadmin_urls)),
 | 
			
		||||
        path('documents/', include(wagtaildocs_urls)),
 | 
			
		||||
        re_path( r'^images/([^/]*)/(\d*)/([^/]*)/[^/]*$', ServeView.as_view(),
 | 
			
		||||
            name='wagtailimages_serve'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    if settings.DEBUG:
 | 
			
		||||
@ -45,7 +39,8 @@ try:
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    urlpatterns.append(re_path(r'', include(wagtail_urls)))
 | 
			
		||||
    urlpatterns.append(path('filer/', include('filer.urls')))
 | 
			
		||||
    urlpatterns += aircox_web.urls.urlpatterns
 | 
			
		||||
 | 
			
		||||
except Exception as e:
 | 
			
		||||
    import traceback
 | 
			
		||||
 | 
			
		||||
@ -1,14 +1,20 @@
 | 
			
		||||
gunicorn>=19.6.0
 | 
			
		||||
Django>=2.2.0
 | 
			
		||||
wagtail>=2.4
 | 
			
		||||
djangorestframework>=3.9.4
 | 
			
		||||
 | 
			
		||||
dateutils>=0.6.6
 | 
			
		||||
watchdog>=0.8.3
 | 
			
		||||
psutil>=5.0.1
 | 
			
		||||
pyyaml>=3.12
 | 
			
		||||
dateutils>=0.6.6
 | 
			
		||||
bleach>=1.4.3
 | 
			
		||||
django-auth-ldap>=1.7.0
 | 
			
		||||
django-honeypot>=0.5.0
 | 
			
		||||
django-jet>=1.0.3
 | 
			
		||||
mutagen>=1.37
 | 
			
		||||
tzlocal>=1.4
 | 
			
		||||
mutagen>=1.37
 | 
			
		||||
pyyaml>=3.12
 | 
			
		||||
 | 
			
		||||
django-filer>=1.5.0
 | 
			
		||||
django-admin-sortable2>=0.7.2
 | 
			
		||||
django-content-editor>=1.4.2
 | 
			
		||||
feincms3[all]>=0.31.0
 | 
			
		||||
 | 
			
		||||
bleach>=1.4.3
 | 
			
		||||
django-honeypot>=0.5.0
 | 
			
		||||
 | 
			
		||||
gunicorn>=19.6.0
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user