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
 | 
					    path = None
 | 
				
			||||||
    data = None
 | 
					    data = None
 | 
				
			||||||
    tracks = None
 | 
					    tracks = None
 | 
				
			||||||
 | 
					    track_kwargs = {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __init__(self, related = None, path = None, save = False):
 | 
					    def __init__(self, path=None, **track_kwargs):
 | 
				
			||||||
        if path:
 | 
					        self.path = path
 | 
				
			||||||
            self.read(path)
 | 
					        self.track_kwargs = track_kwargs
 | 
				
			||||||
            if related:
 | 
					 | 
				
			||||||
                self.make_playlist(related, save)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def reset(self):
 | 
					    def reset(self):
 | 
				
			||||||
        self.data = None
 | 
					        self.data = None
 | 
				
			||||||
        self.tracks = None
 | 
					        self.tracks = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def read(self, path):
 | 
					    def run(self):
 | 
				
			||||||
        if not os.path.exists(path):
 | 
					        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
 | 
					            return True
 | 
				
			||||||
        with open(path, 'r') as file:
 | 
					        with open(self.path, 'r') as file:
 | 
				
			||||||
            logger.info('start reading csv ' + path)
 | 
					            logger.info('start reading csv ' + self.path)
 | 
				
			||||||
            self.path = path
 | 
					 | 
				
			||||||
            self.data = list(csv.DictReader(
 | 
					            self.data = list(csv.DictReader(
 | 
				
			||||||
                (row for row in file
 | 
					                (row for row in file
 | 
				
			||||||
                    if not (row.startswith('#') or row.startswith('\ufeff#'))
 | 
					                    if not (row.startswith('#') or row.startswith('\ufeff#'))
 | 
				
			||||||
                            and row.strip()
 | 
					                    and row.strip()),
 | 
				
			||||||
                ),
 | 
					 | 
				
			||||||
                fieldnames=settings.AIRCOX_IMPORT_PLAYLIST_CSV_COLS,
 | 
					                fieldnames=settings.AIRCOX_IMPORT_PLAYLIST_CSV_COLS,
 | 
				
			||||||
                delimiter=settings.AIRCOX_IMPORT_PLAYLIST_CSV_DELIMITER,
 | 
					                delimiter=settings.AIRCOX_IMPORT_PLAYLIST_CSV_DELIMITER,
 | 
				
			||||||
                quotechar=settings.AIRCOX_IMPORT_PLAYLIST_CSV_TEXT_QUOTE,
 | 
					                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
 | 
					        Make a playlist from the read data, and return it. If save is
 | 
				
			||||||
        true, save it into the database
 | 
					        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
 | 
					        maps = settings.AIRCOX_IMPORT_PLAYLIST_CSV_COLS
 | 
				
			||||||
        tracks = []
 | 
					        tracks = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        logger.info('parse csv file ' + self.path)
 | 
					        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):
 | 
					        for index, line in enumerate(self.data):
 | 
				
			||||||
            if ('title' or 'artist') not in line:
 | 
					            if ('title' or 'artist') not in line:
 | 
				
			||||||
                return
 | 
					                return
 | 
				
			||||||
 | 
					 | 
				
			||||||
            try:
 | 
					            try:
 | 
				
			||||||
                position = \
 | 
					                timestamp = int(line.get('minutes') or 0) * 60 + \
 | 
				
			||||||
                    int(line.get('minutes') or 0) * 60 + \
 | 
					 | 
				
			||||||
                            int(line.get('seconds') or 0) \
 | 
					                            int(line.get('seconds') or 0) \
 | 
				
			||||||
                    if in_seconds else index
 | 
					                            if has_timestamp else None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                track, created = Track.objects.get_or_create(
 | 
					                track, created = Track.objects.get_or_create(
 | 
				
			||||||
                    related_type = ContentType.objects.get_for_model(related),
 | 
					 | 
				
			||||||
                    related_id = related.pk,
 | 
					 | 
				
			||||||
                    title=line.get('title'),
 | 
					                    title=line.get('title'),
 | 
				
			||||||
                    artist=line.get('artist'),
 | 
					                    artist=line.get('artist'),
 | 
				
			||||||
                    position = position,
 | 
					                    position=index,
 | 
				
			||||||
 | 
					                    **self.track_kwargs
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
 | 
					                track.timestamp = timestamp
 | 
				
			||||||
                track.in_seconds = in_seconds
 | 
					 | 
				
			||||||
                track.info = line.get('info')
 | 
					                track.info = line.get('info')
 | 
				
			||||||
                tags = line.get('tags')
 | 
					                tags = line.get('tags')
 | 
				
			||||||
                if tags:
 | 
					                if tags:
 | 
				
			||||||
@ -97,7 +100,6 @@ class Importer:
 | 
				
			|||||||
                )
 | 
					                )
 | 
				
			||||||
                continue
 | 
					                continue
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if save:
 | 
					 | 
				
			||||||
            track.save()
 | 
					            track.save()
 | 
				
			||||||
            tracks.append(track)
 | 
					            tracks.append(track)
 | 
				
			||||||
        self.tracks = tracks
 | 
					        self.tracks = tracks
 | 
				
			||||||
@ -109,8 +111,6 @@ class Command (BaseCommand):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    def add_arguments(self, parser):
 | 
					    def add_arguments(self, parser):
 | 
				
			||||||
        parser.formatter_class=RawTextHelpFormatter
 | 
					        parser.formatter_class=RawTextHelpFormatter
 | 
				
			||||||
        now = tz.datetime.today()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        parser.add_argument(
 | 
					        parser.add_argument(
 | 
				
			||||||
            'path', metavar='PATH', type=str,
 | 
					            'path', metavar='PATH', type=str,
 | 
				
			||||||
            help='path of the input playlist to read'
 | 
					            help='path of the input playlist to read'
 | 
				
			||||||
@ -128,27 +128,24 @@ class Command (BaseCommand):
 | 
				
			|||||||
    def handle (self, path, *args, **options):
 | 
					    def handle (self, path, *args, **options):
 | 
				
			||||||
        # FIXME: absolute/relative path of sounds vs given path
 | 
					        # FIXME: absolute/relative path of sounds vs given path
 | 
				
			||||||
        if options.get('sound'):
 | 
					        if options.get('sound'):
 | 
				
			||||||
            related = Sound.objects.filter(
 | 
					            sound = Sound.objects.filter(
 | 
				
			||||||
                path__icontains=options.get('sound')
 | 
					                path__icontains=options.get('sound')
 | 
				
			||||||
            ).first()
 | 
					            ).first()
 | 
				
			||||||
        else:
 | 
					        else:
 | 
				
			||||||
            path_, ext = os.path.splitext(path)
 | 
					            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 ' \
 | 
					            logger.error('no sound found in the database for the path ' \
 | 
				
			||||||
                         '{path}'.format(path=path))
 | 
					                         '{path}'.format(path=path))
 | 
				
			||||||
            return
 | 
					            return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if options.get('diffusion') and related.diffusion:
 | 
					        if options.get('diffusion') and sound.diffusion:
 | 
				
			||||||
            related = related.diffusion
 | 
					            sound = sound.diffusion
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        importer = Importer(related = related, path = path, save = True)
 | 
					        importer = Importer(path, sound=sound).run()
 | 
				
			||||||
        for track in importer.tracks:
 | 
					        for track in importer.tracks:
 | 
				
			||||||
            logger.info('imported track at {pos}: {title}, by '
 | 
					            logger.info('track #{pos} imported: {title}, by {artist}'.format(
 | 
				
			||||||
                        '{artist}'.format(
 | 
					                pos=track.position, title=track.title, artist=track.artist
 | 
				
			||||||
                    pos = track.position,
 | 
					            ))
 | 
				
			||||||
                    title = track.title, artist = track.artist
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -43,6 +43,7 @@ import aircox.utils as utils
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
logger = logging.getLogger('aircox.tools')
 | 
					logger = logging.getLogger('aircox.tools')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class SoundInfo:
 | 
					class SoundInfo:
 | 
				
			||||||
    name = ''
 | 
					    name = ''
 | 
				
			||||||
    sound = None
 | 
					    sound = None
 | 
				
			||||||
@ -135,7 +136,7 @@ class SoundInfo:
 | 
				
			|||||||
        If use_default is True and there is no playlist find found,
 | 
					        If use_default is True and there is no playlist find found,
 | 
				
			||||||
        use sound file's metadata.
 | 
					        use sound file's metadata.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        if sound.tracks.count():
 | 
					        if sound.track_set.count():
 | 
				
			||||||
            return
 | 
					            return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        import aircox.management.commands.import_playlist \
 | 
					        import aircox.management.commands.import_playlist \
 | 
				
			||||||
@ -151,7 +152,7 @@ class SoundInfo:
 | 
				
			|||||||
            return
 | 
					            return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # else, import
 | 
					        # else, import
 | 
				
			||||||
        import_playlist.Importer(sound, path, save=True)
 | 
					        import_playlist.Importer(path, sound=sound).run()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def find_diffusion(self, program, save=True):
 | 
					    def find_diffusion(self, program, save=True):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
@ -163,7 +164,7 @@ class SoundInfo:
 | 
				
			|||||||
        rerun.
 | 
					        rerun.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        if self.year == None or not self.sound or self.sound.diffusion:
 | 
					        if self.year == None or not self.sound or self.sound.diffusion:
 | 
				
			||||||
            return;
 | 
					            return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if self.hour is None:
 | 
					        if self.hour is None:
 | 
				
			||||||
            date = datetime.date(self.year, self.month, self.day)
 | 
					            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.
 | 
					    Event handler for watchdog, in order to be used in monitoring.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __init__(self, subdir):
 | 
					    def __init__(self, subdir):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        subdir: AIRCOX_SOUND_ARCHIVES_SUBDIR or AIRCOX_SOUND_EXCERPTS_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):
 | 
					    def report(self, program=None, component=None, *content):
 | 
				
			||||||
        if not component:
 | 
					        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:
 | 
					        else:
 | 
				
			||||||
            logger.info('%s, %s: %s', str(program), str(component),
 | 
					            logger.info('%s, %s: %s', str(program), str(component),
 | 
				
			||||||
                        ' '.join([str(c) for c in content]))
 | 
					                        ' '.join([str(c) for c in content]))
 | 
				
			||||||
@ -346,6 +349,7 @@ class Command(BaseCommand):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        # update stats
 | 
					        # update stats
 | 
				
			||||||
        logger.info('update stats in database')
 | 
					        logger.info('update stats in database')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        def update_stats(sound_info, sound):
 | 
					        def update_stats(sound_info, sound):
 | 
				
			||||||
            stats = sound_info.get_file_stats()
 | 
					            stats = sound_info.get_file_stats()
 | 
				
			||||||
            if stats:
 | 
					            if stats:
 | 
				
			||||||
@ -393,7 +397,7 @@ class Command(BaseCommand):
 | 
				
			|||||||
        parser.formatter_class = RawTextHelpFormatter
 | 
					        parser.formatter_class = RawTextHelpFormatter
 | 
				
			||||||
        parser.add_argument(
 | 
					        parser.add_argument(
 | 
				
			||||||
            '-q', '--quality_check', action='store_true',
 | 
					            '-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'
 | 
					                 'sounds marqued as not good'
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        parser.add_argument(
 | 
					        parser.add_argument(
 | 
				
			||||||
@ -414,4 +418,3 @@ class Command(BaseCommand):
 | 
				
			|||||||
            self.check_quality(check=(not options.get('scan')))
 | 
					            self.check_quality(check=(not options.get('scan')))
 | 
				
			||||||
        if options.get('monitor'):
 | 
					        if options.get('monitor'):
 | 
				
			||||||
            self.monitor()
 | 
					            self.monitor()
 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
				
			|||||||
@ -82,17 +82,14 @@ class Monitor:
 | 
				
			|||||||
        """
 | 
					        """
 | 
				
			||||||
        Last sound log of monitored station that occurred on_air
 | 
					        Last sound log of monitored station that occurred on_air
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        return self.get_last_log(type = Log.Type.on_air,
 | 
					        return self.get_last_log(type=Log.Type.on_air, sound__isnull=False)
 | 
				
			||||||
                                 sound__isnull = False)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
    def last_diff_start(self):
 | 
					    def last_diff_start(self):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Log of last triggered item (sound or diffusion)
 | 
					        Log of last triggered item (sound or diffusion)
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        return self.get_last_log(type = Log.Type.start,
 | 
					        return self.get_last_log(type=Log.Type.start, diffusion__isnull=False)
 | 
				
			||||||
                                 diffusion__isnull = False)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __init__(self, station, **kwargs):
 | 
					    def __init__(self, station, **kwargs):
 | 
				
			||||||
        self.station = station
 | 
					        self.station = station
 | 
				
			||||||
@ -124,8 +121,7 @@ class Monitor:
 | 
				
			|||||||
        """
 | 
					        """
 | 
				
			||||||
        Create a log using **kwargs, and print info
 | 
					        Create a log using **kwargs, and print info
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        log = Log(station = self.station, date = date or tz.now(),
 | 
					        log = Log(station=self.station, date=date or tz.now(), **kwargs)
 | 
				
			||||||
                  **kwargs)
 | 
					 | 
				
			||||||
        log.save()
 | 
					        log.save()
 | 
				
			||||||
        log.print()
 | 
					        log.print()
 | 
				
			||||||
        return log
 | 
					        return log
 | 
				
			||||||
@ -161,16 +157,12 @@ class Monitor:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        # log sound on air
 | 
					        # log sound on air
 | 
				
			||||||
        return self.log(
 | 
					        return self.log(
 | 
				
			||||||
            type = Log.Type.on_air,
 | 
					            type=Log.Type.on_air, source=source.id, date=source.on_air,
 | 
				
			||||||
            source = source.id,
 | 
					            sound=sound, diffusion=diff,
 | 
				
			||||||
            date = source.on_air,
 | 
					 | 
				
			||||||
            sound = sound,
 | 
					 | 
				
			||||||
            diffusion = diff,
 | 
					 | 
				
			||||||
            # if sound is removed, we keep sound path info
 | 
					            # if sound is removed, we keep sound path info
 | 
				
			||||||
            comment=sound_path,
 | 
					            comment=sound_path,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
    def trace_tracks(self, log):
 | 
					    def trace_tracks(self, log):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Log tracks for the given sound log (for streamed programs only).
 | 
					        Log tracks for the given sound log (for streamed programs only).
 | 
				
			||||||
@ -178,13 +170,11 @@ class Monitor:
 | 
				
			|||||||
        if log.diffusion:
 | 
					        if log.diffusion:
 | 
				
			||||||
            return
 | 
					            return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        tracks = Track.objects.related(object = log.sound) \
 | 
					        tracks = Track.objects.filter(sound=log.sound, timestamp_isnull=False)
 | 
				
			||||||
                              .filter(in_seconds = True)
 | 
					 | 
				
			||||||
        if not tracks.exists():
 | 
					        if not tracks.exists():
 | 
				
			||||||
            return
 | 
					            return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        tracks = tracks.exclude(log__station = self.station,
 | 
					        tracks = tracks.exclude(log__station=self.station, log__pk__gt=log.pk)
 | 
				
			||||||
                                log__pk__gt = log.pk)
 | 
					 | 
				
			||||||
        now = tz.now()
 | 
					        now = tz.now()
 | 
				
			||||||
        for track in tracks:
 | 
					        for track in tracks:
 | 
				
			||||||
            pos = log.date + tz.timedelta(seconds=track.position)
 | 
					            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)
 | 
					                                                GenericRelation)
 | 
				
			||||||
from django.contrib.contenttypes.models import ContentType
 | 
					from django.contrib.contenttypes.models import ContentType
 | 
				
			||||||
from django.db import models
 | 
					from django.db import models
 | 
				
			||||||
 | 
					from django.db.transaction import atomic
 | 
				
			||||||
from django.template.defaultfilters import slugify
 | 
					from django.template.defaultfilters import slugify
 | 
				
			||||||
from django.utils import timezone as tz
 | 
					from django.utils import timezone as tz
 | 
				
			||||||
from django.utils.functional import cached_property
 | 
					from django.utils.functional import cached_property
 | 
				
			||||||
@ -24,66 +25,6 @@ from taggit.managers import TaggableManager
 | 
				
			|||||||
logger = logging.getLogger('aircox.core')
 | 
					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):
 | 
					class Nameable(models.Model):
 | 
				
			||||||
    name = models.CharField(
 | 
					    name = models.CharField(
 | 
				
			||||||
        _('name'),
 | 
					        _('name'),
 | 
				
			||||||
@ -98,63 +39,14 @@ class Nameable(models.Model):
 | 
				
			|||||||
        """
 | 
					        """
 | 
				
			||||||
        Slug based on the name. We replace '-' by '_'
 | 
					        Slug based on the name. We replace '-' by '_'
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
 | 
					 | 
				
			||||||
        return slugify(self.name).replace('-', '_')
 | 
					        return slugify(self.name).replace('-', '_')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __str__(self):
 | 
					    def __str__(self):
 | 
				
			||||||
        # if self.pk:
 | 
					        # if self.pk:
 | 
				
			||||||
        #    return '#{} {}'.format(self.pk, self.name)
 | 
					        #    return '#{} {}'.format(self.pk, self.name)
 | 
				
			||||||
 | 
					 | 
				
			||||||
        return '{}'.format(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
 | 
					# Station related classes
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
@ -164,15 +56,13 @@ class StationQuerySet(models.QuerySet):
 | 
				
			|||||||
        Return station model instance, using defaults or
 | 
					        Return station model instance, using defaults or
 | 
				
			||||||
        given one.
 | 
					        given one.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
 | 
					 | 
				
			||||||
        if station is None:
 | 
					        if station is None:
 | 
				
			||||||
            return self.order_by('-default', 'pk').first()
 | 
					            return self.order_by('-default', 'pk').first()
 | 
				
			||||||
 | 
					 | 
				
			||||||
        return self.filter(pk=station).first()
 | 
					        return self.filter(pk=station).first()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def default_station():
 | 
					def default_station():
 | 
				
			||||||
    """ Return default station (used by model fields) """
 | 
					    """ Return default station (used by model fields) """
 | 
				
			||||||
 | 
					 | 
				
			||||||
    return Station.objects.default()
 | 
					    return Station.objects.default()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -208,7 +98,6 @@ class Station(Nameable):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    def __prepare_controls(self):
 | 
					    def __prepare_controls(self):
 | 
				
			||||||
        import aircox.controllers as controllers
 | 
					        import aircox.controllers as controllers
 | 
				
			||||||
 | 
					 | 
				
			||||||
        if not self.__streamer:
 | 
					        if not self.__streamer:
 | 
				
			||||||
            self.__streamer = controllers.Streamer(station=self)
 | 
					            self.__streamer = controllers.Streamer(station=self)
 | 
				
			||||||
            self.__dealer = controllers.Source(station=self)
 | 
					            self.__dealer = controllers.Source(station=self)
 | 
				
			||||||
@ -223,7 +112,6 @@ class Station(Nameable):
 | 
				
			|||||||
        """
 | 
					        """
 | 
				
			||||||
        Return all active input ports of the station
 | 
					        Return all active input ports of the station
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
 | 
					 | 
				
			||||||
        return self.port_set.filter(
 | 
					        return self.port_set.filter(
 | 
				
			||||||
            direction=Port.Direction.input,
 | 
					            direction=Port.Direction.input,
 | 
				
			||||||
            active=True
 | 
					            active=True
 | 
				
			||||||
@ -234,7 +122,6 @@ class Station(Nameable):
 | 
				
			|||||||
        """
 | 
					        """
 | 
				
			||||||
        Return all active output ports of the station
 | 
					        Return all active output ports of the station
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
 | 
					 | 
				
			||||||
        return self.port_set.filter(
 | 
					        return self.port_set.filter(
 | 
				
			||||||
            direction=Port.Direction.output,
 | 
					            direction=Port.Direction.output,
 | 
				
			||||||
            active=True,
 | 
					            active=True,
 | 
				
			||||||
@ -246,13 +133,11 @@ class Station(Nameable):
 | 
				
			|||||||
        Audio sources, dealer included
 | 
					        Audio sources, dealer included
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        self.__prepare_controls()
 | 
					        self.__prepare_controls()
 | 
				
			||||||
 | 
					 | 
				
			||||||
        return self.__sources
 | 
					        return self.__sources
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
    def dealer(self):
 | 
					    def dealer(self):
 | 
				
			||||||
        self.__prepare_controls()
 | 
					        self.__prepare_controls()
 | 
				
			||||||
 | 
					 | 
				
			||||||
        return self.__dealer
 | 
					        return self.__dealer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
@ -261,10 +146,9 @@ class Station(Nameable):
 | 
				
			|||||||
        Audio controller for the station
 | 
					        Audio controller for the station
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        self.__prepare_controls()
 | 
					        self.__prepare_controls()
 | 
				
			||||||
 | 
					 | 
				
			||||||
        return self.__streamer
 | 
					        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
 | 
					        Return a queryset of what happened on air, based on logs and
 | 
				
			||||||
        diffusions informations. The queryset is sorted by -date.
 | 
					        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.
 | 
					        that has been played when there was a live diffusion.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        # TODO argument to get sound instead of tracks
 | 
					        # TODO argument to get sound instead of tracks
 | 
				
			||||||
 | 
					 | 
				
			||||||
        if not date and not count:
 | 
					        if not date and not count:
 | 
				
			||||||
            raise ValueError('at least one argument must be set')
 | 
					            raise ValueError('at least one argument must be set')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # FIXME can be a potential source of bug
 | 
					        # FIXME can be a potential source of bug
 | 
				
			||||||
 | 
					 | 
				
			||||||
        if date:
 | 
					        if date:
 | 
				
			||||||
            date = utils.cast_date(date, to_datetime=False)
 | 
					            date = utils.cast_date(date, to_datetime=False)
 | 
				
			||||||
 | 
					 | 
				
			||||||
        if date and date > datetime.date.today():
 | 
					        if date and date > datetime.date.today():
 | 
				
			||||||
            return []
 | 
					            return []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        now = tz.now()
 | 
					        now = tz.now()
 | 
				
			||||||
 | 
					 | 
				
			||||||
        if date:
 | 
					        if date:
 | 
				
			||||||
            logs = Log.objects.at(date)
 | 
					            logs = Log.objects.at(date)
 | 
				
			||||||
            diffs = Diffusion.objects.station(self).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')
 | 
					        logs = logs.station(self).on_air().filter(q).order_by('-date')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # filter out tracks played when there was a diffusion
 | 
					        # filter out tracks played when there was a diffusion
 | 
				
			||||||
        n = 0
 | 
					        n, q = 0, models.Q()
 | 
				
			||||||
        q = models.Q()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        for diff in diffs:
 | 
					        for diff in diffs:
 | 
				
			||||||
            if count and n >= count:
 | 
					            if count and n >= count:
 | 
				
			||||||
                break
 | 
					                break
 | 
				
			||||||
@ -321,10 +199,8 @@ class Station(Nameable):
 | 
				
			|||||||
            q = q | models.Q(date__gte=diff.start, date__lte=diff.end)
 | 
					            q = q | models.Q(date__gte=diff.start, date__lte=diff.end)
 | 
				
			||||||
            n += 1
 | 
					            n += 1
 | 
				
			||||||
        logs = logs.exclude(q, diffusion__isnull=True)
 | 
					        logs = logs.exclude(q, diffusion__isnull=True)
 | 
				
			||||||
 | 
					 | 
				
			||||||
        if count:
 | 
					        if count:
 | 
				
			||||||
            logs = logs[:count]
 | 
					            logs = logs[:count]
 | 
				
			||||||
 | 
					 | 
				
			||||||
        return logs
 | 
					        return logs
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def save(self, make_sources=True, *args, **kwargs):
 | 
					    def save(self, make_sources=True, *args, **kwargs):
 | 
				
			||||||
@ -382,12 +258,12 @@ class Program(Nameable):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    objects = ProgramManager()
 | 
					    objects = ProgramManager()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # TODO: use unique slug
 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
    def path(self):
 | 
					    def path(self):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Return the path to the programs directory
 | 
					        Return the path to the programs directory
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
 | 
					 | 
				
			||||||
        return os.path.join(settings.AIRCOX_PROGRAMS_DIR,
 | 
					        return os.path.join(settings.AIRCOX_PROGRAMS_DIR,
 | 
				
			||||||
                            self.slug + '_' + str(self.id))
 | 
					                            self.slug + '_' + str(self.id))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -441,7 +317,8 @@ class Program(Nameable):
 | 
				
			|||||||
                        self.id, self.name)
 | 
					                        self.id, self.name)
 | 
				
			||||||
            shutil.move(self.__original_path, self.path)
 | 
					            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:
 | 
					            for sound in sounds:
 | 
				
			||||||
                sound.path.replace(self.__original_path, self.path)
 | 
					                sound.path.replace(self.__original_path, self.path)
 | 
				
			||||||
@ -455,9 +332,11 @@ class Program(Nameable):
 | 
				
			|||||||
        """
 | 
					        """
 | 
				
			||||||
        path = path.replace(settings.AIRCOX_PROGRAMS_DIR, '')
 | 
					        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:
 | 
					        if '/' in path:
 | 
				
			||||||
            path = path[:path.index('/')]
 | 
					            path = path[:path.index('/')]
 | 
				
			||||||
@ -529,9 +408,8 @@ class Schedule(models.Model):
 | 
				
			|||||||
        one_on_two = 0b100000
 | 
					        one_on_two = 0b100000
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    program = models.ForeignKey(
 | 
					    program = models.ForeignKey(
 | 
				
			||||||
        Program,
 | 
					        Program, models.CASCADE,
 | 
				
			||||||
        verbose_name=_('related program'),
 | 
					        verbose_name=_('related program'),
 | 
				
			||||||
        on_delete=models.CASCADE,
 | 
					 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    time = models.TimeField(
 | 
					    time = models.TimeField(
 | 
				
			||||||
        _('time'),
 | 
					        _('time'),
 | 
				
			||||||
@ -545,9 +423,8 @@ class Schedule(models.Model):
 | 
				
			|||||||
    )
 | 
					    )
 | 
				
			||||||
    timezone = models.CharField(
 | 
					    timezone = models.CharField(
 | 
				
			||||||
        _('timezone'),
 | 
					        _('timezone'),
 | 
				
			||||||
        default = tz.get_current_timezone,
 | 
					        default=tz.get_current_timezone, max_length=100,
 | 
				
			||||||
        choices=[(x, x) for x in pytz.all_timezones],
 | 
					        choices=[(x, x) for x in pytz.all_timezones],
 | 
				
			||||||
        max_length = 100,
 | 
					 | 
				
			||||||
        help_text=_('timezone used for the date')
 | 
					        help_text=_('timezone used for the date')
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    duration = models.TimeField(
 | 
					    duration = models.TimeField(
 | 
				
			||||||
@ -556,8 +433,7 @@ class Schedule(models.Model):
 | 
				
			|||||||
    )
 | 
					    )
 | 
				
			||||||
    frequency = models.SmallIntegerField(
 | 
					    frequency = models.SmallIntegerField(
 | 
				
			||||||
        _('frequency'),
 | 
					        _('frequency'),
 | 
				
			||||||
        choices = [
 | 
					        choices=[(int(y), {
 | 
				
			||||||
            (int(y), {
 | 
					 | 
				
			||||||
            'ponctual': _('ponctual'),
 | 
					            'ponctual': _('ponctual'),
 | 
				
			||||||
            'first': _('first week of the month'),
 | 
					            'first': _('first week of the month'),
 | 
				
			||||||
            'second': _('second 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'),
 | 
					            'second_and_fourth': _('second and fourth weeks of the month'),
 | 
				
			||||||
            'every': _('every week'),
 | 
					            'every': _('every week'),
 | 
				
			||||||
            'one_on_two': _('one week on two'),
 | 
					            '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(
 | 
					    initial = models.ForeignKey(
 | 
				
			||||||
        'self',
 | 
					        'self', models.SET_NULL,
 | 
				
			||||||
        verbose_name=_('initial schedule'),
 | 
					        verbose_name=_('initial schedule'),
 | 
				
			||||||
        blank=True, null=True,
 | 
					        blank=True, null=True,
 | 
				
			||||||
        on_delete=models.SET_NULL,
 | 
					 | 
				
			||||||
        help_text=_('this schedule is a rerun of this one'),
 | 
					        help_text=_('this schedule is a rerun of this one'),
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
    @cached_property
 | 
					    @cached_property
 | 
				
			||||||
    def tz(self):
 | 
					    def tz(self):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
@ -622,9 +495,8 @@ class Schedule(models.Model):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        # we check against a normalized version (norm_date will have
 | 
					        # we check against a normalized version (norm_date will have
 | 
				
			||||||
        # schedule's date.
 | 
					        # schedule's date.
 | 
				
			||||||
        norm_date = self.normalize(date)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return date == norm_date
 | 
					        return date == self.normalize(date)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def match_week(self, date=None):
 | 
					    def match_week(self, date=None):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
@ -644,7 +516,8 @@ class Schedule(models.Model):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        if self.frequency == Schedule.Frequency.one_on_two:
 | 
					        if self.frequency == Schedule.Frequency.one_on_two:
 | 
				
			||||||
            # cf notes in date_of_month
 | 
					            # 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)
 | 
					            return not (diff.days % 14)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -690,7 +563,8 @@ class Schedule(models.Model):
 | 
				
			|||||||
        # last of the month
 | 
					        # last of the month
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if freq == Schedule.Frequency.last:
 | 
					        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
 | 
					            # 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
 | 
					        # check on SO#3284452 for the formula
 | 
				
			||||||
        first_weekday = date.weekday()
 | 
					        first_weekday = date.weekday()
 | 
				
			||||||
        sched_weekday = self.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)
 | 
					                             - first_weekday + sched_weekday)
 | 
				
			||||||
        month = date.month
 | 
					        month = date.month
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -714,7 +588,8 @@ class Schedule(models.Model):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        if freq == Schedule.Frequency.one_on_two:
 | 
					        if freq == Schedule.Frequency.one_on_two:
 | 
				
			||||||
            # check date base on a diff of dates base on a 14 days delta
 | 
					            # 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:
 | 
					            if diff.days % 14:
 | 
				
			||||||
                date += tz.timedelta(days=7)
 | 
					                date += tz.timedelta(days=7)
 | 
				
			||||||
@ -729,7 +604,7 @@ class Schedule(models.Model):
 | 
				
			|||||||
                if freq & (0b1 << week):
 | 
					                if freq & (0b1 << week):
 | 
				
			||||||
                    dates.append(date)
 | 
					                    dates.append(date)
 | 
				
			||||||
                date += tz.timedelta(days=7)
 | 
					                date += tz.timedelta(days=7)
 | 
				
			||||||
                week += 1;
 | 
					                week += 1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return [self.normalize(date) for date in dates]
 | 
					        return [self.normalize(date) for date in dates]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -767,8 +642,7 @@ class Schedule(models.Model):
 | 
				
			|||||||
            Diffusion(
 | 
					            Diffusion(
 | 
				
			||||||
                program=self.program,
 | 
					                program=self.program,
 | 
				
			||||||
                type=Diffusion.Type.unconfirmed,
 | 
					                type=Diffusion.Type.unconfirmed,
 | 
				
			||||||
                initial = \
 | 
					                initial=Diffusion.objects.filter(start=date - delta).first()
 | 
				
			||||||
                    Diffusion.objects.filter(start = date - delta).first() \
 | 
					 | 
				
			||||||
                if self.initial else None,
 | 
					                if self.initial else None,
 | 
				
			||||||
                start=date,
 | 
					                start=date,
 | 
				
			||||||
                end=date + duration,
 | 
					                end=date + duration,
 | 
				
			||||||
@ -933,8 +807,6 @@ class Diffusion(models.Model):
 | 
				
			|||||||
    start = models.DateTimeField(_('start of the diffusion'))
 | 
					    start = models.DateTimeField(_('start of the diffusion'))
 | 
				
			||||||
    end = models.DateTimeField(_('end of the diffusion'))
 | 
					    end = models.DateTimeField(_('end of the diffusion'))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    tracks = Track.ReverseField()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
    def duration(self):
 | 
					    def duration(self):
 | 
				
			||||||
        return self.end - self.start
 | 
					        return self.end - self.start
 | 
				
			||||||
@ -981,16 +853,15 @@ class Diffusion(models.Model):
 | 
				
			|||||||
        return self.type == self.Type.normal and \
 | 
					        return self.type == self.Type.normal and \
 | 
				
			||||||
            not self.get_sounds(archive=True).count()
 | 
					            not self.get_sounds(archive=True).count()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
    def get_playlist(self, **types):
 | 
					    def get_playlist(self, **types):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Returns sounds as a playlist (list of *local* archive file path).
 | 
					        Returns sounds as a playlist (list of *local* archive file path).
 | 
				
			||||||
        The given arguments are passed to ``get_sounds``.
 | 
					        The given arguments are passed to ``get_sounds``.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return list(self.get_sounds(**types) \
 | 
					        return list(self.get_sounds(**types)
 | 
				
			||||||
                        .filter(path__isnull=False,
 | 
					                        .filter(path__isnull=False,
 | 
				
			||||||
                                type=Sound.Type.archive) \
 | 
					                                type=Sound.Type.archive)
 | 
				
			||||||
                        .values_list('path', flat=True))
 | 
					                        .values_list('path', flat=True))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_sounds(self, **types):
 | 
					    def get_sounds(self, **types):
 | 
				
			||||||
@ -1025,7 +896,7 @@ class Diffusion(models.Model):
 | 
				
			|||||||
                     end__gt=self.start) |
 | 
					                     end__gt=self.start) |
 | 
				
			||||||
            models.Q(start__gt=self.start,
 | 
					            models.Q(start__gt=self.start,
 | 
				
			||||||
                     start__lt=self.end)
 | 
					                     start__lt=self.end)
 | 
				
			||||||
        )
 | 
					        ).exclude(pk=self.pk).distinct()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def check_conflicts(self):
 | 
					    def check_conflicts(self):
 | 
				
			||||||
        conflicts = self.get_conflicts()
 | 
					        conflicts = self.get_conflicts()
 | 
				
			||||||
@ -1056,7 +927,6 @@ class Diffusion(models.Model):
 | 
				
			|||||||
                    self.end != self.__initial['end']:
 | 
					                    self.end != self.__initial['end']:
 | 
				
			||||||
                self.check_conflicts()
 | 
					                self.check_conflicts()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
    def __str__(self):
 | 
					    def __str__(self):
 | 
				
			||||||
        return '{self.program.name} {date} #{self.pk}'.format(
 | 
					        return '{self.program.name} {date} #{self.pk}'.format(
 | 
				
			||||||
            self=self, date=self.local_date.strftime('%Y/%m/%d %H:%M%z')
 | 
					            self=self, date=self.local_date.strftime('%Y/%m/%d %H:%M%z')
 | 
				
			||||||
@ -1065,7 +935,6 @@ class Diffusion(models.Model):
 | 
				
			|||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
        verbose_name = _('Diffusion')
 | 
					        verbose_name = _('Diffusion')
 | 
				
			||||||
        verbose_name_plural = _('Diffusions')
 | 
					        verbose_name_plural = _('Diffusions')
 | 
				
			||||||
 | 
					 | 
				
			||||||
        permissions = (
 | 
					        permissions = (
 | 
				
			||||||
            ('programming', _('edit the diffusion\'s planification')),
 | 
					            ('programming', _('edit the diffusion\'s planification')),
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
@ -1139,8 +1008,6 @@ class Sound(Nameable):
 | 
				
			|||||||
        help_text=_('the sound is accessible to the public')
 | 
					        help_text=_('the sound is accessible to the public')
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    tracks = Track.ReverseField()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def get_mtime(self):
 | 
					    def get_mtime(self):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Get the last modification date from file
 | 
					        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,
 | 
					        Get metadata from sound file and return a Track object if succeed,
 | 
				
			||||||
        else None.
 | 
					        else None.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
 | 
					 | 
				
			||||||
        if not self.file_exists():
 | 
					        if not self.file_exists():
 | 
				
			||||||
            return None
 | 
					            return None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -1189,7 +1055,6 @@ class Sound(Nameable):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        def get_meta(key, cast=str):
 | 
					        def get_meta(key, cast=str):
 | 
				
			||||||
            value = meta.get(key)
 | 
					            value = meta.get(key)
 | 
				
			||||||
 | 
					 | 
				
			||||||
            return cast(value[0]) if value else None
 | 
					            return cast(value[0]) if value else None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        info = '{} ({})'.format(get_meta('album'), get_meta('year')) \
 | 
					        info = '{} ({})'.format(get_meta('album'), get_meta('year')) \
 | 
				
			||||||
@ -1198,15 +1063,11 @@ class Sound(Nameable):
 | 
				
			|||||||
            if 'album' else \
 | 
					            if 'album' else \
 | 
				
			||||||
               ('year' in meta) and get_meta('year') or ''
 | 
					               ('year' in meta) and get_meta('year') or ''
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        track = Track(
 | 
					        return Track(sound=self,
 | 
				
			||||||
            related = self,
 | 
					                     position=get_meta('tracknumber', int) or 0,
 | 
				
			||||||
                     title=get_meta('title') or self.name,
 | 
					                     title=get_meta('title') or self.name,
 | 
				
			||||||
                     artist=get_meta('artist') or _('unknown'),
 | 
					                     artist=get_meta('artist') or _('unknown'),
 | 
				
			||||||
            info = info,
 | 
					                     info=info)
 | 
				
			||||||
            position = get_meta('tracknumber', int) or 0,
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return track
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def check_on_file(self):
 | 
					    def check_on_file(self):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
@ -1290,6 +1151,64 @@ class Sound(Nameable):
 | 
				
			|||||||
        verbose_name_plural = _('Sounds')
 | 
					        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
 | 
					# Controls and audio input/output
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
@ -1485,7 +1404,7 @@ class LogQuerySet(models.QuerySet):
 | 
				
			|||||||
        import gzip
 | 
					        import gzip
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        os.makedirs(settings.AIRCOX_LOGS_ARCHIVES_DIR, exist_ok=True)
 | 
					        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:
 | 
					        if os.path.exists(path) and not force:
 | 
				
			||||||
            return -1
 | 
					            return -1
 | 
				
			||||||
@ -1496,15 +1415,8 @@ class LogQuerySet(models.QuerySet):
 | 
				
			|||||||
            return 0
 | 
					            return 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        fields = Log._meta.get_fields()
 | 
					        fields = Log._meta.get_fields()
 | 
				
			||||||
        logs = [
 | 
					        logs = [{i.attname: getattr(log, i.attname)
 | 
				
			||||||
            {
 | 
					                 for i in fields} for log in qs]
 | 
				
			||||||
                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
 | 
					        # Note: since we use Yaml, we can just append new logs when file
 | 
				
			||||||
        # exists yet <3
 | 
					        # exists yet <3
 | 
				
			||||||
@ -1554,55 +1466,46 @@ class Log(models.Model):
 | 
				
			|||||||
        """
 | 
					        """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    type = models.SmallIntegerField(
 | 
					    type = models.SmallIntegerField(
 | 
				
			||||||
        verbose_name = _('type'),
 | 
					        choices=[(int(y), _(x.replace('_', ' ')))
 | 
				
			||||||
        choices = [ (int(y), _(x.replace('_',' '))) for x,y in Type.__members__.items() ],
 | 
					                 for x, y in Type.__members__.items()],
 | 
				
			||||||
        blank=True, null=True,
 | 
					        blank=True, null=True,
 | 
				
			||||||
 | 
					        verbose_name=_('type'),
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    station = models.ForeignKey(
 | 
					    station = models.ForeignKey(
 | 
				
			||||||
        Station,
 | 
					        Station, on_delete=models.CASCADE,
 | 
				
			||||||
        verbose_name=_('station'),
 | 
					        verbose_name=_('station'),
 | 
				
			||||||
        on_delete=models.CASCADE,
 | 
					 | 
				
			||||||
        help_text=_('related station'),
 | 
					        help_text=_('related station'),
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    source = models.CharField(
 | 
					    source = models.CharField(
 | 
				
			||||||
        # we use a CharField to avoid loosing logs information if the
 | 
					        # we use a CharField to avoid loosing logs information if the
 | 
				
			||||||
        # source is removed
 | 
					        # source is removed
 | 
				
			||||||
        _('source'),
 | 
					        max_length=64, blank=True, null=True,
 | 
				
			||||||
        max_length=64,
 | 
					        verbose_name=_('source'),
 | 
				
			||||||
        help_text=_('identifier of the source related to this log'),
 | 
					        help_text=_('identifier of the source related to this log'),
 | 
				
			||||||
        blank = True, null = True,
 | 
					 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    date = models.DateTimeField(
 | 
					    date = models.DateTimeField(
 | 
				
			||||||
        _('date'),
 | 
					        default=tz.now, db_index=True,
 | 
				
			||||||
        default=tz.now,
 | 
					        verbose_name=_('date'),
 | 
				
			||||||
        db_index = True,
 | 
					 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    comment = models.CharField(
 | 
					    comment = models.CharField(
 | 
				
			||||||
        _('comment'),
 | 
					        max_length=512, blank=True, null=True,
 | 
				
			||||||
        max_length = 512,
 | 
					        verbose_name=_('comment'),
 | 
				
			||||||
        blank = True, null = True,
 | 
					 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    diffusion = models.ForeignKey(
 | 
					    diffusion = models.ForeignKey(
 | 
				
			||||||
        Diffusion,
 | 
					        Diffusion, on_delete=models.SET_NULL,
 | 
				
			||||||
 | 
					        blank=True, null=True, db_index=True,
 | 
				
			||||||
        verbose_name=_('Diffusion'),
 | 
					        verbose_name=_('Diffusion'),
 | 
				
			||||||
        blank = True, null = True,
 | 
					 | 
				
			||||||
        db_index = True,
 | 
					 | 
				
			||||||
        on_delete=models.SET_NULL,
 | 
					 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    sound = models.ForeignKey(
 | 
					    sound = models.ForeignKey(
 | 
				
			||||||
        Sound,
 | 
					        Sound, on_delete=models.SET_NULL,
 | 
				
			||||||
 | 
					        blank=True, null=True, db_index=True,
 | 
				
			||||||
        verbose_name=_('Sound'),
 | 
					        verbose_name=_('Sound'),
 | 
				
			||||||
        blank = True, null = True,
 | 
					 | 
				
			||||||
        db_index = True,
 | 
					 | 
				
			||||||
        on_delete=models.SET_NULL,
 | 
					 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    track = models.ForeignKey(
 | 
					    track = models.ForeignKey(
 | 
				
			||||||
        Track,
 | 
					        Track, on_delete=models.SET_NULL,
 | 
				
			||||||
 | 
					        blank=True, null=True, db_index=True,
 | 
				
			||||||
        verbose_name=_('Track'),
 | 
					        verbose_name=_('Track'),
 | 
				
			||||||
        blank = True, null = True,
 | 
					 | 
				
			||||||
        db_index = True,
 | 
					 | 
				
			||||||
        on_delete=models.SET_NULL,
 | 
					 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    objects = LogQuerySet.as_manager()
 | 
					    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
 | 
					        This is needed since datetime are stored as UTC date and we want
 | 
				
			||||||
        to get it as local time.
 | 
					        to get it as local time.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
 | 
					 | 
				
			||||||
        return tz.localtime(self.date, tz.get_current_timezone())
 | 
					        return tz.localtime(self.date, tz.get_current_timezone())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def print(self):
 | 
					    def print(self):
 | 
				
			||||||
        r = []
 | 
					        r = []
 | 
				
			||||||
 | 
					 | 
				
			||||||
        if self.diffusion:
 | 
					        if self.diffusion:
 | 
				
			||||||
            r.append('diff: ' + str(self.diffusion_id))
 | 
					            r.append('diff: ' + str(self.diffusion_id))
 | 
				
			||||||
 | 
					 | 
				
			||||||
        if self.sound:
 | 
					        if self.sound:
 | 
				
			||||||
            r.append('sound: ' + str(self.sound_id))
 | 
					            r.append('sound: ' + str(self.sound_id))
 | 
				
			||||||
 | 
					 | 
				
			||||||
        if self.track:
 | 
					        if self.track:
 | 
				
			||||||
            r.append('track: ' + str(self.track_id))
 | 
					            r.append('track: ' + str(self.track_id))
 | 
				
			||||||
 | 
					        logger.info('log %s: %s%s', str(self), self.comment or '',
 | 
				
			||||||
        logger.info('log %s: %s%s',
 | 
					                    ' (' + ', '.join(r) + ')' if r else '')
 | 
				
			||||||
            str(self),
 | 
					 | 
				
			||||||
            self.comment or '',
 | 
					 | 
				
			||||||
            ' (' + ', '.join(r) + ')' if r else ''
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __str__(self):
 | 
					    def __str__(self):
 | 
				
			||||||
        return '#{} ({}, {}, {})'.format(
 | 
					        return '#{} ({}, {}, {})'.format(
 | 
				
			||||||
                self.pk,
 | 
					            self.pk, self.get_type_display(),
 | 
				
			||||||
                self.get_type_display(),
 | 
					 | 
				
			||||||
            self.source,
 | 
					            self.source,
 | 
				
			||||||
            self.local_date.strftime('%Y/%m/%d %H:%M%z'),
 | 
					            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
 | 
					- is interactive
 | 
				
			||||||
{% endcomment %}
 | 
					{% endcomment %}
 | 
				
			||||||
def stream (id, file) =
 | 
					def stream (id, file) =
 | 
				
			||||||
    s = playlist(id = '#{id}_playlist', mode = "random", reload_mode='watch',
 | 
					    s = playlist(id = '#{id}_playlist', mode = "random", reload_mode='watch', file)
 | 
				
			||||||
                 file)
 | 
					 | 
				
			||||||
    interactive_source(id, s)
 | 
					    interactive_source(id, s)
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
{% endblock %}
 | 
					{% endblock %}
 | 
				
			||||||
 | 
				
			|||||||
@ -39,7 +39,7 @@ def on_air(request):
 | 
				
			|||||||
    except:
 | 
					    except:
 | 
				
			||||||
        cms = None
 | 
					        cms = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    station = request.GET.get('station');
 | 
					    station = request.GET.get('station')
 | 
				
			||||||
    if station:
 | 
					    if station:
 | 
				
			||||||
        # FIXME: by name???
 | 
					        # FIXME: by name???
 | 
				
			||||||
        station = stations.stations.filter(name=station)
 | 
					        station = stations.stations.filter(name=station)
 | 
				
			||||||
@ -55,16 +55,13 @@ def on_air(request):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    last = on_air.first()
 | 
					    last = on_air.first()
 | 
				
			||||||
    if last.track:
 | 
					    if last.track:
 | 
				
			||||||
        last = {
 | 
					        last = {'date': last.date, 'type': 'track',
 | 
				
			||||||
            'type': 'track',
 | 
					                'artist': last.track.artist, 'title': last.track.title}
 | 
				
			||||||
            'artist': last.related.artist,
 | 
					 | 
				
			||||||
            'title': last.related.title,
 | 
					 | 
				
			||||||
            'date': last.date,
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    else:
 | 
					    else:
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
            diff = last.diffusion
 | 
					            diff = last.diffusion
 | 
				
			||||||
            publication = None
 | 
					            publication = None
 | 
				
			||||||
 | 
					            # FIXME CMS
 | 
				
			||||||
            if cms:
 | 
					            if cms:
 | 
				
			||||||
                publication = \
 | 
					                publication = \
 | 
				
			||||||
                    cms.DiffusionPage.objects.filter(
 | 
					                    cms.DiffusionPage.objects.filter(
 | 
				
			||||||
@ -73,14 +70,9 @@ def on_air(request):
 | 
				
			|||||||
                        program=last.program).first()
 | 
					                        program=last.program).first()
 | 
				
			||||||
        except:
 | 
					        except:
 | 
				
			||||||
            pass
 | 
					            pass
 | 
				
			||||||
 | 
					        last = {'date': diff.start, 'type': 'diffusion',
 | 
				
			||||||
        last = {
 | 
					 | 
				
			||||||
            'type': 'diffusion',
 | 
					 | 
				
			||||||
                'title': diff.program.name,
 | 
					                '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'])
 | 
					    last['date'] = str(last['date'])
 | 
				
			||||||
    return HttpResponse(json.dumps(last))
 | 
					    return HttpResponse(json.dumps(last))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -145,7 +137,7 @@ class StatisticsView(View,TemplateResponseMixin,LoginRequiredMixin):
 | 
				
			|||||||
    """
 | 
					    """
 | 
				
			||||||
    View for statistics.
 | 
					    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'
 | 
					    template_name = 'aircox/controllers/stats.html'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    class Item:
 | 
					    class Item:
 | 
				
			||||||
@ -194,7 +186,6 @@ class StatisticsView(View,TemplateResponseMixin,LoginRequiredMixin):
 | 
				
			|||||||
        #    # all other cases: new row
 | 
					        #    # all other cases: new row
 | 
				
			||||||
        #    self.rows.append((None, [log]))
 | 
					        #    self.rows.append((None, [log]))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
    def get_stats(self, station, date):
 | 
					    def get_stats(self, station, date):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Return statistics for the given station and date.
 | 
					        Return statistics for the given station and date.
 | 
				
			||||||
@ -209,37 +200,26 @@ class StatisticsView(View,TemplateResponseMixin,LoginRequiredMixin):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        sound_log = None
 | 
					        sound_log = None
 | 
				
			||||||
        for log in qs:
 | 
					        for log in qs:
 | 
				
			||||||
            rel = None
 | 
					            rel, item = None, None
 | 
				
			||||||
            item = None
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if log.diffusion:
 | 
					            if log.diffusion:
 | 
				
			||||||
                rel = log.diffusion
 | 
					                rel, item = log.diffusion, self.Item(
 | 
				
			||||||
                item = self.Item(
 | 
					                    name=rel.program.name, type=_('Diffusion'), col=0,
 | 
				
			||||||
                    name = rel.program.name,
 | 
					                    tracks=models.Track.objects.filter(diffusion=log.diffusion)
 | 
				
			||||||
                    type = _('Diffusion'),
 | 
					 | 
				
			||||||
                    col = 0,
 | 
					 | 
				
			||||||
                    tracks = models.Track.objects.related(object = rel)
 | 
					 | 
				
			||||||
                                       .prefetch_related('tags'),
 | 
					                                       .prefetch_related('tags'),
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
                sound_log = None
 | 
					                sound_log = None
 | 
				
			||||||
            elif log.sound:
 | 
					            elif log.sound:
 | 
				
			||||||
                rel = log.sound
 | 
					                rel, item = log.sound, self.Item(
 | 
				
			||||||
                item = self.Item(
 | 
					 | 
				
			||||||
                    name=rel.program.name + ': ' + os.path.basename(rel.path),
 | 
					                    name=rel.program.name + ': ' + os.path.basename(rel.path),
 | 
				
			||||||
                    type = _('Stream'),
 | 
					                    type=_('Stream'), col=1, tracks=[],
 | 
				
			||||||
                    col = 1,
 | 
					 | 
				
			||||||
                    tracks = [],
 | 
					 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
                sound_log = item
 | 
					                sound_log = item
 | 
				
			||||||
            elif log.track:
 | 
					            elif log.track:
 | 
				
			||||||
                # append to last sound log
 | 
					                # append to last sound log
 | 
				
			||||||
                if not sound_log:
 | 
					                if not sound_log:
 | 
				
			||||||
                    # TODO: create item ? should never happen
 | 
					 | 
				
			||||||
                    continue
 | 
					                    continue
 | 
				
			||||||
 | 
					 | 
				
			||||||
                sound_log.tracks.append(log.track)
 | 
					                sound_log.tracks.append(log.track)
 | 
				
			||||||
                sound_log.end = log.end
 | 
					                sound_log.end = log.end
 | 
				
			||||||
                sound_log
 | 
					 | 
				
			||||||
                continue
 | 
					                continue
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            item.date = log.date
 | 
					            item.date = log.date
 | 
				
			||||||
@ -247,7 +227,6 @@ class StatisticsView(View,TemplateResponseMixin,LoginRequiredMixin):
 | 
				
			|||||||
            item.related = rel
 | 
					            item.related = rel
 | 
				
			||||||
            # stats.append(item)
 | 
					            # stats.append(item)
 | 
				
			||||||
            stats.items.append(item)
 | 
					            stats.items.append(item)
 | 
				
			||||||
 | 
					 | 
				
			||||||
        return stats
 | 
					        return stats
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_context_data(self, **kwargs):
 | 
					    def get_context_data(self, **kwargs):
 | 
				
			||||||
@ -279,4 +258,3 @@ class StatisticsView(View,TemplateResponseMixin,LoginRequiredMixin):
 | 
				
			|||||||
        return render(request, self.template_name, context)
 | 
					        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.common.CommonMiddleware',
 | 
				
			||||||
    'django.middleware.csrf.CsrfViewMiddleware',
 | 
					    'django.middleware.csrf.CsrfViewMiddleware',
 | 
				
			||||||
    'django.contrib.auth.middleware.AuthenticationMiddleware',
 | 
					    'django.contrib.auth.middleware.AuthenticationMiddleware',
 | 
				
			||||||
    'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
 | 
					 | 
				
			||||||
    'django.contrib.messages.middleware.MessageMiddleware',
 | 
					    'django.contrib.messages.middleware.MessageMiddleware',
 | 
				
			||||||
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
 | 
					    'django.middleware.clickjacking.XFrameOptionsMiddleware',
 | 
				
			||||||
    'django.middleware.security.SecurityMiddleware',
 | 
					    'django.middleware.security.SecurityMiddleware',
 | 
				
			||||||
 | 
				
			|||||||
@ -17,24 +17,18 @@ from django.conf import settings
 | 
				
			|||||||
from django.urls import include, path, re_path
 | 
					from django.urls import include, path, re_path
 | 
				
			||||||
from django.contrib import admin
 | 
					from django.contrib import admin
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from wagtail.admin import urls as wagtailadmin_urls
 | 
					#from wagtail.admin import urls as wagtailadmin_urls
 | 
				
			||||||
from wagtail.documents import urls as wagtaildocs_urls
 | 
					#from wagtail.documents import urls as wagtaildocs_urls
 | 
				
			||||||
from wagtail.core import urls as wagtail_urls
 | 
					#from wagtail.core import urls as wagtail_urls
 | 
				
			||||||
from wagtail.images.views.serve import ServeView
 | 
					#from wagtail.images.views.serve import ServeView
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import aircox.urls
 | 
					import aircox.urls
 | 
				
			||||||
 | 
					import aircox_web.urls
 | 
				
			||||||
 | 
					
 | 
				
			||||||
try:
 | 
					try:
 | 
				
			||||||
    urlpatterns = [
 | 
					    urlpatterns = [
 | 
				
			||||||
        path('jet/', include('jet.urls', 'jet')),
 | 
					 | 
				
			||||||
        path('admin/', admin.site.urls),
 | 
					        path('admin/', admin.site.urls),
 | 
				
			||||||
        path('aircox/', include(aircox.urls.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:
 | 
					    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:
 | 
					except Exception as e:
 | 
				
			||||||
    import traceback
 | 
					    import traceback
 | 
				
			||||||
 | 
				
			|||||||
@ -1,14 +1,20 @@
 | 
				
			|||||||
gunicorn>=19.6.0
 | 
					 | 
				
			||||||
Django>=2.2.0
 | 
					Django>=2.2.0
 | 
				
			||||||
wagtail>=2.4
 | 
					djangorestframework>=3.9.4
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					dateutils>=0.6.6
 | 
				
			||||||
watchdog>=0.8.3
 | 
					watchdog>=0.8.3
 | 
				
			||||||
psutil>=5.0.1
 | 
					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
 | 
					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