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()
 | 
			
		||||
                ),
 | 
			
		||||
                    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 + \
 | 
			
		||||
                timestamp = int(line.get('minutes') or 0) * 60 + \
 | 
			
		||||
                            int(line.get('seconds') or 0) \
 | 
			
		||||
                    if in_seconds else index
 | 
			
		||||
                            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,
 | 
			
		||||
                    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,7 +100,6 @@ class Importer:
 | 
			
		||||
                )
 | 
			
		||||
                continue
 | 
			
		||||
 | 
			
		||||
            if save:
 | 
			
		||||
            track.save()
 | 
			
		||||
            tracks.append(track)
 | 
			
		||||
        self.tracks = tracks
 | 
			
		||||
@ -109,8 +111,6 @@ class Command (BaseCommand):
 | 
			
		||||
 | 
			
		||||
    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(
 | 
			
		||||
            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
 | 
			
		||||
@ -135,7 +136,7 @@ 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 \
 | 
			
		||||
@ -151,7 +152,7 @@ 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):
 | 
			
		||||
        """
 | 
			
		||||
@ -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)
 | 
			
		||||
@ -190,6 +191,7 @@ 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
 | 
			
		||||
@ -253,7 +255,8 @@ class Command(BaseCommand):
 | 
			
		||||
 | 
			
		||||
    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]))
 | 
			
		||||
@ -346,6 +349,7 @@ class Command(BaseCommand):
 | 
			
		||||
 | 
			
		||||
        # update stats
 | 
			
		||||
        logger.info('update stats in database')
 | 
			
		||||
 | 
			
		||||
        def update_stats(sound_info, sound):
 | 
			
		||||
            stats = sound_info.get_file_stats()
 | 
			
		||||
            if stats:
 | 
			
		||||
@ -393,7 +397,7 @@ class Command(BaseCommand):
 | 
			
		||||
        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(
 | 
			
		||||
@ -414,4 +418,3 @@ class Command(BaseCommand):
 | 
			
		||||
            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
 | 
			
		||||
@ -124,8 +121,7 @@ class Monitor:
 | 
			
		||||
        """
 | 
			
		||||
        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
 | 
			
		||||
@ -161,16 +157,12 @@ class Monitor:
 | 
			
		||||
 | 
			
		||||
        # 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,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    def trace_tracks(self, log):
 | 
			
		||||
        """
 | 
			
		||||
        Log tracks for the given sound log (for streamed programs only).
 | 
			
		||||
@ -178,13 +170,11 @@ 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)
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										330
									
								
								aircox/models.py
									
									
									
									
									
								
							
							
						
						
									
										330
									
								
								aircox/models.py
									
									
									
									
									
								
							@ -11,6 +11,7 @@ from django.contrib.contenttypes.fields import (GenericForeignKey,
 | 
			
		||||
                                                GenericRelation)
 | 
			
		||||
from django.contrib.contenttypes.models import ContentType
 | 
			
		||||
from django.db import models
 | 
			
		||||
from django.db.transaction import atomic
 | 
			
		||||
from django.template.defaultfilters import slugify
 | 
			
		||||
from django.utils import timezone as tz
 | 
			
		||||
from django.utils.functional import cached_property
 | 
			
		||||
@ -24,66 +25,6 @@ from taggit.managers import TaggableManager
 | 
			
		||||
logger = logging.getLogger('aircox.core')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
#
 | 
			
		||||
# Abstracts
 | 
			
		||||
#
 | 
			
		||||
class RelatedQuerySet(models.QuerySet):
 | 
			
		||||
    def related(self, object = None, model = None):
 | 
			
		||||
        """
 | 
			
		||||
        Return a queryset that filter on the given object or model(s)
 | 
			
		||||
 | 
			
		||||
        * object: if given, use its type and pk; match on models only.
 | 
			
		||||
        * model: one model or an iterable of models
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
        if not model and object:
 | 
			
		||||
            model = type(object)
 | 
			
		||||
 | 
			
		||||
        qs = self
 | 
			
		||||
 | 
			
		||||
        if hasattr(model, '__iter__'):
 | 
			
		||||
            model = [ ContentType.objects.get_for_model(m).id
 | 
			
		||||
                        for m in model ]
 | 
			
		||||
            self = self.filter(related_type__pk__in = model)
 | 
			
		||||
        else:
 | 
			
		||||
            model = ContentType.objects.get_for_model(model)
 | 
			
		||||
            self = self.filter(related_type__pk = model.id)
 | 
			
		||||
 | 
			
		||||
        if object:
 | 
			
		||||
            self = self.filter(related_id = object.pk)
 | 
			
		||||
 | 
			
		||||
        return self
 | 
			
		||||
 | 
			
		||||
class Related(models.Model):
 | 
			
		||||
    """
 | 
			
		||||
    Add a field "related" of type GenericForeignKey, plus utilities.
 | 
			
		||||
    """
 | 
			
		||||
    related_type = models.ForeignKey(
 | 
			
		||||
        ContentType,
 | 
			
		||||
        blank = True, null = True,
 | 
			
		||||
        on_delete=models.SET_NULL,
 | 
			
		||||
    )
 | 
			
		||||
    related_id = models.PositiveIntegerField(
 | 
			
		||||
        blank = True, null = True,
 | 
			
		||||
    )
 | 
			
		||||
    related = GenericForeignKey(
 | 
			
		||||
        'related_type', 'related_id',
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        abstract = True
 | 
			
		||||
 | 
			
		||||
    objects = RelatedQuerySet.as_manager()
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def ReverseField(cl):
 | 
			
		||||
        """
 | 
			
		||||
        Return a GenericRelation object that points to this class
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
        return GenericRelation(cl, 'related_id', 'related_type')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Nameable(models.Model):
 | 
			
		||||
    name = models.CharField(
 | 
			
		||||
        _('name'),
 | 
			
		||||
@ -98,63 +39,14 @@ class Nameable(models.Model):
 | 
			
		||||
        """
 | 
			
		||||
        Slug based on the name. We replace '-' by '_'
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
        return slugify(self.name).replace('-', '_')
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        # if self.pk:
 | 
			
		||||
        #    return '#{} {}'.format(self.pk, self.name)
 | 
			
		||||
 | 
			
		||||
        return '{}'.format(self.name)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
#
 | 
			
		||||
# Small common models
 | 
			
		||||
#
 | 
			
		||||
class Track(Related):
 | 
			
		||||
    """
 | 
			
		||||
    Track of a playlist of an object. The position can either be expressed
 | 
			
		||||
    as the position in the playlist or as the moment in seconds it started.
 | 
			
		||||
    """
 | 
			
		||||
    # There are no nice solution for M2M relations ship (even without
 | 
			
		||||
    # through) in django-admin. So we unfortunately need to make one-
 | 
			
		||||
    # to-one relations and add a position argument
 | 
			
		||||
    title = models.CharField (
 | 
			
		||||
        _('title'),
 | 
			
		||||
        max_length = 128,
 | 
			
		||||
    )
 | 
			
		||||
    artist = models.CharField(
 | 
			
		||||
        _('artist'),
 | 
			
		||||
        max_length = 128,
 | 
			
		||||
    )
 | 
			
		||||
    tags = TaggableManager(
 | 
			
		||||
        verbose_name=_('tags'),
 | 
			
		||||
        blank=True,
 | 
			
		||||
    )
 | 
			
		||||
    info = models.CharField(
 | 
			
		||||
        _('information'),
 | 
			
		||||
        max_length = 128,
 | 
			
		||||
        blank = True, null = True,
 | 
			
		||||
        help_text=_('additional informations about this track, such as '
 | 
			
		||||
                    'the version, if is it a remix, features, etc.'),
 | 
			
		||||
    )
 | 
			
		||||
    position = models.SmallIntegerField(
 | 
			
		||||
        default = 0,
 | 
			
		||||
        help_text=_('position in the playlist'),
 | 
			
		||||
    )
 | 
			
		||||
    in_seconds = models.BooleanField(
 | 
			
		||||
        _('in seconds'),
 | 
			
		||||
        default = False,
 | 
			
		||||
        help_text=_('position in the playlist is expressed in seconds')
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return '{self.artist} -- {self.title} -- {self.position}'.format(self=self)
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        verbose_name = _('Track')
 | 
			
		||||
        verbose_name_plural = _('Tracks')
 | 
			
		||||
 | 
			
		||||
#
 | 
			
		||||
# Station related classes
 | 
			
		||||
#
 | 
			
		||||
@ -164,15 +56,13 @@ class StationQuerySet(models.QuerySet):
 | 
			
		||||
        Return station model instance, using defaults or
 | 
			
		||||
        given one.
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
        if station is None:
 | 
			
		||||
            return self.order_by('-default', 'pk').first()
 | 
			
		||||
 | 
			
		||||
        return self.filter(pk=station).first()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def default_station():
 | 
			
		||||
    """ Return default station (used by model fields) """
 | 
			
		||||
 | 
			
		||||
    return Station.objects.default()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -208,7 +98,6 @@ class Station(Nameable):
 | 
			
		||||
 | 
			
		||||
    def __prepare_controls(self):
 | 
			
		||||
        import aircox.controllers as controllers
 | 
			
		||||
 | 
			
		||||
        if not self.__streamer:
 | 
			
		||||
            self.__streamer = controllers.Streamer(station=self)
 | 
			
		||||
            self.__dealer = controllers.Source(station=self)
 | 
			
		||||
@ -223,7 +112,6 @@ class Station(Nameable):
 | 
			
		||||
        """
 | 
			
		||||
        Return all active input ports of the station
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
        return self.port_set.filter(
 | 
			
		||||
            direction=Port.Direction.input,
 | 
			
		||||
            active=True
 | 
			
		||||
@ -234,7 +122,6 @@ class Station(Nameable):
 | 
			
		||||
        """
 | 
			
		||||
        Return all active output ports of the station
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
        return self.port_set.filter(
 | 
			
		||||
            direction=Port.Direction.output,
 | 
			
		||||
            active=True,
 | 
			
		||||
@ -246,13 +133,11 @@ class Station(Nameable):
 | 
			
		||||
        Audio sources, dealer included
 | 
			
		||||
        """
 | 
			
		||||
        self.__prepare_controls()
 | 
			
		||||
 | 
			
		||||
        return self.__sources
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def dealer(self):
 | 
			
		||||
        self.__prepare_controls()
 | 
			
		||||
 | 
			
		||||
        return self.__dealer
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
@ -261,10 +146,9 @@ class Station(Nameable):
 | 
			
		||||
        Audio controller for the station
 | 
			
		||||
        """
 | 
			
		||||
        self.__prepare_controls()
 | 
			
		||||
 | 
			
		||||
        return self.__streamer
 | 
			
		||||
 | 
			
		||||
    def on_air(self, date = None, count = 0, no_cache = False):
 | 
			
		||||
    def on_air(self, date=None, count=0):
 | 
			
		||||
        """
 | 
			
		||||
        Return a queryset of what happened on air, based on logs and
 | 
			
		||||
        diffusions informations. The queryset is sorted by -date.
 | 
			
		||||
@ -279,20 +163,16 @@ class Station(Nameable):
 | 
			
		||||
        that has been played when there was a live diffusion.
 | 
			
		||||
        """
 | 
			
		||||
        # TODO argument to get sound instead of tracks
 | 
			
		||||
 | 
			
		||||
        if not date and not count:
 | 
			
		||||
            raise ValueError('at least one argument must be set')
 | 
			
		||||
 | 
			
		||||
        # FIXME can be a potential source of bug
 | 
			
		||||
 | 
			
		||||
        if date:
 | 
			
		||||
            date = utils.cast_date(date, to_datetime=False)
 | 
			
		||||
 | 
			
		||||
        if date and date > datetime.date.today():
 | 
			
		||||
            return []
 | 
			
		||||
 | 
			
		||||
        now = tz.now()
 | 
			
		||||
 | 
			
		||||
        if date:
 | 
			
		||||
            logs = Log.objects.at(date)
 | 
			
		||||
            diffs = Diffusion.objects.station(self).at(date) \
 | 
			
		||||
@ -310,9 +190,7 @@ class Station(Nameable):
 | 
			
		||||
        logs = logs.station(self).on_air().filter(q).order_by('-date')
 | 
			
		||||
 | 
			
		||||
        # filter out tracks played when there was a diffusion
 | 
			
		||||
        n = 0
 | 
			
		||||
        q = models.Q()
 | 
			
		||||
 | 
			
		||||
        n, q = 0, models.Q()
 | 
			
		||||
        for diff in diffs:
 | 
			
		||||
            if count and n >= count:
 | 
			
		||||
                break
 | 
			
		||||
@ -321,10 +199,8 @@ class Station(Nameable):
 | 
			
		||||
            q = q | models.Q(date__gte=diff.start, date__lte=diff.end)
 | 
			
		||||
            n += 1
 | 
			
		||||
        logs = logs.exclude(q, diffusion__isnull=True)
 | 
			
		||||
 | 
			
		||||
        if count:
 | 
			
		||||
            logs = logs[:count]
 | 
			
		||||
 | 
			
		||||
        return logs
 | 
			
		||||
 | 
			
		||||
    def save(self, make_sources=True, *args, **kwargs):
 | 
			
		||||
@ -382,12 +258,12 @@ class Program(Nameable):
 | 
			
		||||
 | 
			
		||||
    objects = ProgramManager()
 | 
			
		||||
 | 
			
		||||
    # TODO: use unique slug
 | 
			
		||||
    @property
 | 
			
		||||
    def path(self):
 | 
			
		||||
        """
 | 
			
		||||
        Return the path to the programs directory
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
        return os.path.join(settings.AIRCOX_PROGRAMS_DIR,
 | 
			
		||||
                            self.slug + '_' + str(self.id))
 | 
			
		||||
 | 
			
		||||
@ -441,7 +317,8 @@ class Program(Nameable):
 | 
			
		||||
                        self.id, self.name)
 | 
			
		||||
            shutil.move(self.__original_path, self.path)
 | 
			
		||||
 | 
			
		||||
            sounds = Sounds.objects.filter(path__startswith = self.__original_path)
 | 
			
		||||
            sounds = Sound.objects.filter(
 | 
			
		||||
                path__startswith=self.__original_path)
 | 
			
		||||
 | 
			
		||||
            for sound in sounds:
 | 
			
		||||
                sound.path.replace(self.__original_path, self.path)
 | 
			
		||||
@ -455,9 +332,11 @@ class Program(Nameable):
 | 
			
		||||
        """
 | 
			
		||||
        path = path.replace(settings.AIRCOX_PROGRAMS_DIR, '')
 | 
			
		||||
 | 
			
		||||
        while path[0] == '/': path = path[1:]
 | 
			
		||||
        while path[0] == '/':
 | 
			
		||||
            path = path[1:]
 | 
			
		||||
 | 
			
		||||
        while path[-1] == '/': path = path[:-2]
 | 
			
		||||
        while path[-1] == '/':
 | 
			
		||||
            path = path[:-2]
 | 
			
		||||
 | 
			
		||||
        if '/' in path:
 | 
			
		||||
            path = path[:path.index('/')]
 | 
			
		||||
@ -529,9 +408,8 @@ class Schedule(models.Model):
 | 
			
		||||
        one_on_two = 0b100000
 | 
			
		||||
 | 
			
		||||
    program = models.ForeignKey(
 | 
			
		||||
        Program,
 | 
			
		||||
        Program, models.CASCADE,
 | 
			
		||||
        verbose_name=_('related program'),
 | 
			
		||||
        on_delete=models.CASCADE,
 | 
			
		||||
    )
 | 
			
		||||
    time = models.TimeField(
 | 
			
		||||
        _('time'),
 | 
			
		||||
@ -545,9 +423,8 @@ class Schedule(models.Model):
 | 
			
		||||
    )
 | 
			
		||||
    timezone = models.CharField(
 | 
			
		||||
        _('timezone'),
 | 
			
		||||
        default = tz.get_current_timezone,
 | 
			
		||||
        default=tz.get_current_timezone, max_length=100,
 | 
			
		||||
        choices=[(x, x) for x in pytz.all_timezones],
 | 
			
		||||
        max_length = 100,
 | 
			
		||||
        help_text=_('timezone used for the date')
 | 
			
		||||
    )
 | 
			
		||||
    duration = models.TimeField(
 | 
			
		||||
@ -556,8 +433,7 @@ class Schedule(models.Model):
 | 
			
		||||
    )
 | 
			
		||||
    frequency = models.SmallIntegerField(
 | 
			
		||||
        _('frequency'),
 | 
			
		||||
        choices = [
 | 
			
		||||
            (int(y), {
 | 
			
		||||
        choices=[(int(y), {
 | 
			
		||||
            'ponctual': _('ponctual'),
 | 
			
		||||
            'first': _('first week of the month'),
 | 
			
		||||
            'second': _('second week of the month'),
 | 
			
		||||
@ -568,18 +444,15 @@ class Schedule(models.Model):
 | 
			
		||||
            'second_and_fourth': _('second and fourth weeks of the month'),
 | 
			
		||||
            'every': _('every week'),
 | 
			
		||||
            'one_on_two': _('one week on two'),
 | 
			
		||||
            }[x]) for x,y in Frequency.__members__.items()
 | 
			
		||||
        ],
 | 
			
		||||
        }[x]) for x, y in Frequency.__members__.items()],
 | 
			
		||||
    )
 | 
			
		||||
    initial = models.ForeignKey(
 | 
			
		||||
        'self',
 | 
			
		||||
        'self', models.SET_NULL,
 | 
			
		||||
        verbose_name=_('initial schedule'),
 | 
			
		||||
        blank=True, null=True,
 | 
			
		||||
        on_delete=models.SET_NULL,
 | 
			
		||||
        help_text=_('this schedule is a rerun of this one'),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    @cached_property
 | 
			
		||||
    def tz(self):
 | 
			
		||||
        """
 | 
			
		||||
@ -622,9 +495,8 @@ class Schedule(models.Model):
 | 
			
		||||
 | 
			
		||||
        # we check against a normalized version (norm_date will have
 | 
			
		||||
        # schedule's date.
 | 
			
		||||
        norm_date = self.normalize(date)
 | 
			
		||||
 | 
			
		||||
        return date == norm_date
 | 
			
		||||
        return date == self.normalize(date)
 | 
			
		||||
 | 
			
		||||
    def match_week(self, date=None):
 | 
			
		||||
        """
 | 
			
		||||
@ -644,7 +516,8 @@ class Schedule(models.Model):
 | 
			
		||||
 | 
			
		||||
        if self.frequency == Schedule.Frequency.one_on_two:
 | 
			
		||||
            # cf notes in date_of_month
 | 
			
		||||
            diff = utils.cast_date(date, False) - utils.cast_date(self.date, False)
 | 
			
		||||
            diff = utils.cast_date(date, False) - \
 | 
			
		||||
                utils.cast_date(self.date, False)
 | 
			
		||||
 | 
			
		||||
            return not (diff.days % 14)
 | 
			
		||||
 | 
			
		||||
@ -690,7 +563,8 @@ class Schedule(models.Model):
 | 
			
		||||
        # last of the month
 | 
			
		||||
 | 
			
		||||
        if freq == Schedule.Frequency.last:
 | 
			
		||||
            date = date.replace(day=calendar.monthrange(date.year, date.month)[1])
 | 
			
		||||
            date = date.replace(
 | 
			
		||||
                day=calendar.monthrange(date.year, date.month)[1])
 | 
			
		||||
 | 
			
		||||
            # end of month before the wanted weekday: move one week back
 | 
			
		||||
 | 
			
		||||
@ -706,7 +580,7 @@ class Schedule(models.Model):
 | 
			
		||||
        # check on SO#3284452 for the formula
 | 
			
		||||
        first_weekday = date.weekday()
 | 
			
		||||
        sched_weekday = self.date.weekday()
 | 
			
		||||
        date += tz.timedelta(days = (7 if first_weekday > sched_weekday else 0) \
 | 
			
		||||
        date += tz.timedelta(days=(7 if first_weekday > sched_weekday else 0)
 | 
			
		||||
                             - first_weekday + sched_weekday)
 | 
			
		||||
        month = date.month
 | 
			
		||||
 | 
			
		||||
@ -714,7 +588,8 @@ class Schedule(models.Model):
 | 
			
		||||
 | 
			
		||||
        if freq == Schedule.Frequency.one_on_two:
 | 
			
		||||
            # check date base on a diff of dates base on a 14 days delta
 | 
			
		||||
            diff = utils.cast_date(date, False) - utils.cast_date(self.date, False)
 | 
			
		||||
            diff = utils.cast_date(date, False) - \
 | 
			
		||||
                utils.cast_date(self.date, False)
 | 
			
		||||
 | 
			
		||||
            if diff.days % 14:
 | 
			
		||||
                date += tz.timedelta(days=7)
 | 
			
		||||
@ -729,7 +604,7 @@ class Schedule(models.Model):
 | 
			
		||||
                if freq & (0b1 << week):
 | 
			
		||||
                    dates.append(date)
 | 
			
		||||
                date += tz.timedelta(days=7)
 | 
			
		||||
                week += 1;
 | 
			
		||||
                week += 1
 | 
			
		||||
 | 
			
		||||
        return [self.normalize(date) for date in dates]
 | 
			
		||||
 | 
			
		||||
@ -767,8 +642,7 @@ class Schedule(models.Model):
 | 
			
		||||
            Diffusion(
 | 
			
		||||
                program=self.program,
 | 
			
		||||
                type=Diffusion.Type.unconfirmed,
 | 
			
		||||
                initial = \
 | 
			
		||||
                    Diffusion.objects.filter(start = date - delta).first() \
 | 
			
		||||
                initial=Diffusion.objects.filter(start=date - delta).first()
 | 
			
		||||
                if self.initial else None,
 | 
			
		||||
                start=date,
 | 
			
		||||
                end=date + duration,
 | 
			
		||||
@ -933,8 +807,6 @@ class Diffusion(models.Model):
 | 
			
		||||
    start = models.DateTimeField(_('start of the diffusion'))
 | 
			
		||||
    end = models.DateTimeField(_('end of the diffusion'))
 | 
			
		||||
 | 
			
		||||
    tracks = Track.ReverseField()
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def duration(self):
 | 
			
		||||
        return self.end - self.start
 | 
			
		||||
@ -981,16 +853,15 @@ class Diffusion(models.Model):
 | 
			
		||||
        return self.type == self.Type.normal and \
 | 
			
		||||
            not self.get_sounds(archive=True).count()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    def get_playlist(self, **types):
 | 
			
		||||
        """
 | 
			
		||||
        Returns sounds as a playlist (list of *local* archive file path).
 | 
			
		||||
        The given arguments are passed to ``get_sounds``.
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
        return list(self.get_sounds(**types) \
 | 
			
		||||
        return list(self.get_sounds(**types)
 | 
			
		||||
                        .filter(path__isnull=False,
 | 
			
		||||
                                type=Sound.Type.archive) \
 | 
			
		||||
                                type=Sound.Type.archive)
 | 
			
		||||
                        .values_list('path', flat=True))
 | 
			
		||||
 | 
			
		||||
    def get_sounds(self, **types):
 | 
			
		||||
@ -1025,7 +896,7 @@ class Diffusion(models.Model):
 | 
			
		||||
                     end__gt=self.start) |
 | 
			
		||||
            models.Q(start__gt=self.start,
 | 
			
		||||
                     start__lt=self.end)
 | 
			
		||||
        )
 | 
			
		||||
        ).exclude(pk=self.pk).distinct()
 | 
			
		||||
 | 
			
		||||
    def check_conflicts(self):
 | 
			
		||||
        conflicts = self.get_conflicts()
 | 
			
		||||
@ -1056,7 +927,6 @@ class Diffusion(models.Model):
 | 
			
		||||
                    self.end != self.__initial['end']:
 | 
			
		||||
                self.check_conflicts()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return '{self.program.name} {date} #{self.pk}'.format(
 | 
			
		||||
            self=self, date=self.local_date.strftime('%Y/%m/%d %H:%M%z')
 | 
			
		||||
@ -1065,7 +935,6 @@ class Diffusion(models.Model):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        verbose_name = _('Diffusion')
 | 
			
		||||
        verbose_name_plural = _('Diffusions')
 | 
			
		||||
 | 
			
		||||
        permissions = (
 | 
			
		||||
            ('programming', _('edit the diffusion\'s planification')),
 | 
			
		||||
        )
 | 
			
		||||
@ -1139,8 +1008,6 @@ class Sound(Nameable):
 | 
			
		||||
        help_text=_('the sound is accessible to the public')
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    tracks = Track.ReverseField()
 | 
			
		||||
 | 
			
		||||
    def get_mtime(self):
 | 
			
		||||
        """
 | 
			
		||||
        Get the last modification date from file
 | 
			
		||||
@ -1174,7 +1041,6 @@ class Sound(Nameable):
 | 
			
		||||
        Get metadata from sound file and return a Track object if succeed,
 | 
			
		||||
        else None.
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
        if not self.file_exists():
 | 
			
		||||
            return None
 | 
			
		||||
 | 
			
		||||
@ -1189,7 +1055,6 @@ class Sound(Nameable):
 | 
			
		||||
 | 
			
		||||
        def get_meta(key, cast=str):
 | 
			
		||||
            value = meta.get(key)
 | 
			
		||||
 | 
			
		||||
            return cast(value[0]) if value else None
 | 
			
		||||
 | 
			
		||||
        info = '{} ({})'.format(get_meta('album'), get_meta('year')) \
 | 
			
		||||
@ -1198,15 +1063,11 @@ class Sound(Nameable):
 | 
			
		||||
            if 'album' else \
 | 
			
		||||
               ('year' in meta) and get_meta('year') or ''
 | 
			
		||||
 | 
			
		||||
        track = Track(
 | 
			
		||||
            related = self,
 | 
			
		||||
        return Track(sound=self,
 | 
			
		||||
                     position=get_meta('tracknumber', int) or 0,
 | 
			
		||||
                     title=get_meta('title') or self.name,
 | 
			
		||||
                     artist=get_meta('artist') or _('unknown'),
 | 
			
		||||
            info = info,
 | 
			
		||||
            position = get_meta('tracknumber', int) or 0,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        return track
 | 
			
		||||
                     info=info)
 | 
			
		||||
 | 
			
		||||
    def check_on_file(self):
 | 
			
		||||
        """
 | 
			
		||||
@ -1290,6 +1151,64 @@ class Sound(Nameable):
 | 
			
		||||
        verbose_name_plural = _('Sounds')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Track(models.Model):
 | 
			
		||||
    """
 | 
			
		||||
    Track of a playlist of an object. The position can either be expressed
 | 
			
		||||
    as the position in the playlist or as the moment in seconds it started.
 | 
			
		||||
    """
 | 
			
		||||
    diffusion = models.ForeignKey(
 | 
			
		||||
        Diffusion, models.CASCADE, blank=True, null=True,
 | 
			
		||||
        verbose_name=_('diffusion'),
 | 
			
		||||
    )
 | 
			
		||||
    sound = models.ForeignKey(
 | 
			
		||||
        Sound, models.CASCADE, blank=True, null=True,
 | 
			
		||||
        verbose_name=_('sound'),
 | 
			
		||||
    )
 | 
			
		||||
    position = models.PositiveSmallIntegerField(
 | 
			
		||||
        _('order'),
 | 
			
		||||
        default=0,
 | 
			
		||||
        help_text=_('position in the playlist'),
 | 
			
		||||
    )
 | 
			
		||||
    timestamp = models.PositiveSmallIntegerField(
 | 
			
		||||
        _('timestamp'),
 | 
			
		||||
        blank=True, null=True,
 | 
			
		||||
        help_text=_('position in seconds')
 | 
			
		||||
    )
 | 
			
		||||
    title = models.CharField(
 | 
			
		||||
        _('title'),
 | 
			
		||||
        max_length=128,
 | 
			
		||||
    )
 | 
			
		||||
    artist = models.CharField(
 | 
			
		||||
        _('artist'),
 | 
			
		||||
        max_length=128,
 | 
			
		||||
    )
 | 
			
		||||
    tags = TaggableManager(
 | 
			
		||||
        verbose_name=_('tags'),
 | 
			
		||||
        blank=True,
 | 
			
		||||
    )
 | 
			
		||||
    info = models.CharField(
 | 
			
		||||
        _('information'),
 | 
			
		||||
        max_length=128,
 | 
			
		||||
        blank=True, null=True,
 | 
			
		||||
        help_text=_('additional informations about this track, such as '
 | 
			
		||||
                    'the version, if is it a remix, features, etc.'),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        verbose_name = _('Track')
 | 
			
		||||
        verbose_name_plural = _('Tracks')
 | 
			
		||||
        ordering = ('position',)
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return '{self.artist} -- {self.title} -- {self.position}'.format(
 | 
			
		||||
               self=self)
 | 
			
		||||
 | 
			
		||||
    def save(self, *args, **kwargs):
 | 
			
		||||
        if (self.sound is None and self.diffusion is None) or \
 | 
			
		||||
                (self.sound is not None and self.diffusion is not None):
 | 
			
		||||
            raise ValueError('sound XOR diffusion is required')
 | 
			
		||||
        super().save(*args, **kwargs)
 | 
			
		||||
 | 
			
		||||
#
 | 
			
		||||
# Controls and audio input/output
 | 
			
		||||
#
 | 
			
		||||
@ -1485,7 +1404,7 @@ class LogQuerySet(models.QuerySet):
 | 
			
		||||
        import gzip
 | 
			
		||||
 | 
			
		||||
        os.makedirs(settings.AIRCOX_LOGS_ARCHIVES_DIR, exist_ok=True)
 | 
			
		||||
        path = self._get_archive_path(station, date);
 | 
			
		||||
        path = self._get_archive_path(station, date)
 | 
			
		||||
 | 
			
		||||
        if os.path.exists(path) and not force:
 | 
			
		||||
            return -1
 | 
			
		||||
@ -1496,15 +1415,8 @@ class LogQuerySet(models.QuerySet):
 | 
			
		||||
            return 0
 | 
			
		||||
 | 
			
		||||
        fields = Log._meta.get_fields()
 | 
			
		||||
        logs = [
 | 
			
		||||
            {
 | 
			
		||||
                i.attname: getattr(log, i.attname)
 | 
			
		||||
 | 
			
		||||
                for i in fields
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            for log in qs
 | 
			
		||||
        ]
 | 
			
		||||
        logs = [{i.attname: getattr(log, i.attname)
 | 
			
		||||
                 for i in fields} for log in qs]
 | 
			
		||||
 | 
			
		||||
        # Note: since we use Yaml, we can just append new logs when file
 | 
			
		||||
        # exists yet <3
 | 
			
		||||
@ -1554,55 +1466,46 @@ class Log(models.Model):
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
    type = models.SmallIntegerField(
 | 
			
		||||
        verbose_name = _('type'),
 | 
			
		||||
        choices = [ (int(y), _(x.replace('_',' '))) for x,y in Type.__members__.items() ],
 | 
			
		||||
        choices=[(int(y), _(x.replace('_', ' ')))
 | 
			
		||||
                 for x, y in Type.__members__.items()],
 | 
			
		||||
        blank=True, null=True,
 | 
			
		||||
        verbose_name=_('type'),
 | 
			
		||||
    )
 | 
			
		||||
    station = models.ForeignKey(
 | 
			
		||||
        Station,
 | 
			
		||||
        Station, on_delete=models.CASCADE,
 | 
			
		||||
        verbose_name=_('station'),
 | 
			
		||||
        on_delete=models.CASCADE,
 | 
			
		||||
        help_text=_('related station'),
 | 
			
		||||
    )
 | 
			
		||||
    source = models.CharField(
 | 
			
		||||
        # we use a CharField to avoid loosing logs information if the
 | 
			
		||||
        # source is removed
 | 
			
		||||
        _('source'),
 | 
			
		||||
        max_length=64,
 | 
			
		||||
        max_length=64, blank=True, null=True,
 | 
			
		||||
        verbose_name=_('source'),
 | 
			
		||||
        help_text=_('identifier of the source related to this log'),
 | 
			
		||||
        blank = True, null = True,
 | 
			
		||||
    )
 | 
			
		||||
    date = models.DateTimeField(
 | 
			
		||||
        _('date'),
 | 
			
		||||
        default=tz.now,
 | 
			
		||||
        db_index = True,
 | 
			
		||||
        default=tz.now, db_index=True,
 | 
			
		||||
        verbose_name=_('date'),
 | 
			
		||||
    )
 | 
			
		||||
    comment = models.CharField(
 | 
			
		||||
        _('comment'),
 | 
			
		||||
        max_length = 512,
 | 
			
		||||
        blank = True, null = True,
 | 
			
		||||
        max_length=512, blank=True, null=True,
 | 
			
		||||
        verbose_name=_('comment'),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    diffusion = models.ForeignKey(
 | 
			
		||||
        Diffusion,
 | 
			
		||||
        Diffusion, on_delete=models.SET_NULL,
 | 
			
		||||
        blank=True, null=True, db_index=True,
 | 
			
		||||
        verbose_name=_('Diffusion'),
 | 
			
		||||
        blank = True, null = True,
 | 
			
		||||
        db_index = True,
 | 
			
		||||
        on_delete=models.SET_NULL,
 | 
			
		||||
    )
 | 
			
		||||
    sound = models.ForeignKey(
 | 
			
		||||
        Sound,
 | 
			
		||||
        Sound, on_delete=models.SET_NULL,
 | 
			
		||||
        blank=True, null=True, db_index=True,
 | 
			
		||||
        verbose_name=_('Sound'),
 | 
			
		||||
        blank = True, null = True,
 | 
			
		||||
        db_index = True,
 | 
			
		||||
        on_delete=models.SET_NULL,
 | 
			
		||||
    )
 | 
			
		||||
    track = models.ForeignKey(
 | 
			
		||||
        Track,
 | 
			
		||||
        Track, on_delete=models.SET_NULL,
 | 
			
		||||
        blank=True, null=True, db_index=True,
 | 
			
		||||
        verbose_name=_('Track'),
 | 
			
		||||
        blank = True, null = True,
 | 
			
		||||
        db_index = True,
 | 
			
		||||
        on_delete=models.SET_NULL,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    objects = LogQuerySet.as_manager()
 | 
			
		||||
@ -1618,31 +1521,22 @@ class Log(models.Model):
 | 
			
		||||
        This is needed since datetime are stored as UTC date and we want
 | 
			
		||||
        to get it as local time.
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
        return tz.localtime(self.date, tz.get_current_timezone())
 | 
			
		||||
 | 
			
		||||
    def print(self):
 | 
			
		||||
        r = []
 | 
			
		||||
 | 
			
		||||
        if self.diffusion:
 | 
			
		||||
            r.append('diff: ' + str(self.diffusion_id))
 | 
			
		||||
 | 
			
		||||
        if self.sound:
 | 
			
		||||
            r.append('sound: ' + str(self.sound_id))
 | 
			
		||||
 | 
			
		||||
        if self.track:
 | 
			
		||||
            r.append('track: ' + str(self.track_id))
 | 
			
		||||
 | 
			
		||||
        logger.info('log %s: %s%s',
 | 
			
		||||
            str(self),
 | 
			
		||||
            self.comment or '',
 | 
			
		||||
            ' (' + ', '.join(r) + ')' if r else ''
 | 
			
		||||
        )
 | 
			
		||||
        logger.info('log %s: %s%s', str(self), self.comment or '',
 | 
			
		||||
                    ' (' + ', '.join(r) + ')' if r else '')
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return '#{} ({}, {}, {})'.format(
 | 
			
		||||
                self.pk,
 | 
			
		||||
                self.get_type_display(),
 | 
			
		||||
            self.pk, self.get_type_display(),
 | 
			
		||||
            self.source,
 | 
			
		||||
            self.local_date.strftime('%Y/%m/%d %H:%M%z'),
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										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 %}
 | 
			
		||||
 | 
			
		||||
@ -39,7 +39,7 @@ 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)
 | 
			
		||||
@ -55,16 +55,13 @@ def on_air(request):
 | 
			
		||||
 | 
			
		||||
    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(
 | 
			
		||||
@ -73,14 +70,9 @@ def on_air(request):
 | 
			
		||||
                        program=last.program).first()
 | 
			
		||||
        except:
 | 
			
		||||
            pass
 | 
			
		||||
 | 
			
		||||
        last = {
 | 
			
		||||
            'type': 'diffusion',
 | 
			
		||||
        last = {'date': diff.start, 'type': 'diffusion',
 | 
			
		||||
                'title': diff.program.name,
 | 
			
		||||
            'date': diff.start,
 | 
			
		||||
            'url': publication.specific.url if publication else None,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
                'url': publication.specific.url if publication else None}
 | 
			
		||||
    last['date'] = str(last['date'])
 | 
			
		||||
    return HttpResponse(json.dumps(last))
 | 
			
		||||
 | 
			
		||||
@ -145,7 +137,7 @@ 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:
 | 
			
		||||
@ -194,7 +186,6 @@ 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.
 | 
			
		||||
@ -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)
 | 
			
		||||
                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(
 | 
			
		||||
                rel, item = log.sound, self.Item(
 | 
			
		||||
                    name=rel.program.name + ': ' + os.path.basename(rel.path),
 | 
			
		||||
                    type = _('Stream'),
 | 
			
		||||
                    col = 1,
 | 
			
		||||
                    tracks = [],
 | 
			
		||||
                    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):
 | 
			
		||||
@ -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