work hard on this

This commit is contained in:
bkfox 2019-06-29 18:13:25 +02:00
parent a951d7a319
commit 74dbc620ed
31 changed files with 1191 additions and 833 deletions

View File

@ -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
View 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
View 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
View 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
View 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
View 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
View 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]

View File

@ -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 has_timestamp else None
if in_seconds else index
track, created = Track.objects.get_or_create( track, created = Track.objects.get_or_create(
related_type = ContentType.objects.get_for_model(related), title=line.get('title'),
related_id = related.pk, artist=line.get('artist'),
title = line.get('title'), position=index,
artist = line.get('artist'), **self.track_kwargs
position = position,
) )
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,8 +100,7 @@ class Importer:
) )
continue continue
if save: track.save()
track.save()
tracks.append(track) tracks.append(track)
self.tracks = tracks self.tracks = tracks
return tracks return tracks
@ -107,10 +109,8 @@ class Importer:
class Command (BaseCommand): class Command (BaseCommand):
help= __doc__ help= __doc__
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
)
)

View File

@ -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
@ -76,7 +77,7 @@ class SoundInfo:
file_name) file_name)
if not (r and r.groupdict()): if not (r and r.groupdict()):
r = { 'name': file_name } r = {'name': file_name}
logger.info('file name can not be parsed -> %s', value) logger.info('file name can not be parsed -> %s', value)
else: else:
r = r.groupdict() r = r.groupdict()
@ -93,7 +94,7 @@ class SoundInfo:
self.n = r.get('n') self.n = r.get('n')
return r return r
def __init__(self, path = '', sound = None): def __init__(self, path='', sound=None):
self.path = path self.path = path
self.sound = sound self.sound = sound
@ -107,7 +108,7 @@ class SoundInfo:
self.duration = duration self.duration = duration
return duration return duration
def get_sound(self, save = True, **kwargs): def get_sound(self, save=True, **kwargs):
""" """
Get or create a sound using self info. Get or create a sound using self info.
@ -115,8 +116,8 @@ class SoundInfo:
(if save is True, sync to DB), and check for a playlist file. (if save is True, sync to DB), and check for a playlist file.
""" """
sound, created = Sound.objects.get_or_create( sound, created = Sound.objects.get_or_create(
path = self.path, path=self.path,
defaults = kwargs defaults=kwargs
) )
if created or sound.check_on_file(): if created or sound.check_on_file():
logger.info('sound is new or have been modified -> %s', self.path) logger.info('sound is new or have been modified -> %s', self.path)
@ -127,7 +128,7 @@ class SoundInfo:
self.sound = sound self.sound = sound
return sound return sound
def find_playlist(self, sound, use_default = True): def find_playlist(self, sound, use_default=True):
""" """
Find a playlist file corresponding to the sound path, such as: Find a playlist file corresponding to the sound path, such as:
my_sound.ogg => my_sound.csv my_sound.ogg => my_sound.csv
@ -135,11 +136,11 @@ 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 \
as import_playlist as import_playlist
# no playlist, try to retrieve metadata # no playlist, try to retrieve metadata
path = os.path.splitext(self.sound.path)[0] + '.csv' path = os.path.splitext(self.sound.path)[0] + '.csv'
@ -151,9 +152,9 @@ 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):
""" """
For a given program, check if there is an initial diffusion For a given program, check if there is an initial diffusion
to associate to, using the date info we have. Update self.sound to associate to, using the date info we have. Update self.sound
@ -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)
@ -173,7 +174,7 @@ class SoundInfo:
date = tz.get_current_timezone().localize(date) date = tz.get_current_timezone().localize(date)
qs = Diffusion.objects.station(program.station).after(date) \ qs = Diffusion.objects.station(program.station).after(date) \
.filter(program = program, initial__isnull = True) .filter(program=program, initial__isnull=True)
diffusion = qs.first() diffusion = qs.first()
if not diffusion: if not diffusion:
return return
@ -190,18 +191,19 @@ 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
""" """
self.subdir = subdir self.subdir = subdir
if self.subdir == settings.AIRCOX_SOUND_ARCHIVES_SUBDIR: if self.subdir == settings.AIRCOX_SOUND_ARCHIVES_SUBDIR:
self.sound_kwargs = { 'type': Sound.Type.archive } self.sound_kwargs = {'type': Sound.Type.archive}
else: else:
self.sound_kwargs = { 'type': Sound.Type.excerpt } self.sound_kwargs = {'type': Sound.Type.excerpt}
patterns = ['*/{}/*{}'.format(self.subdir, ext) patterns = ['*/{}/*{}'.format(self.subdir, ext)
for ext in settings.AIRCOX_SOUND_FILE_EXT ] for ext in settings.AIRCOX_SOUND_FILE_EXT]
super().__init__(patterns=patterns, ignore_directories=True) super().__init__(patterns=patterns, ignore_directories=True)
def on_created(self, event): def on_created(self, event):
@ -215,14 +217,14 @@ class MonitorHandler(PatternMatchingEventHandler):
si = SoundInfo(event.src_path) si = SoundInfo(event.src_path)
self.sound_kwargs['program'] = program self.sound_kwargs['program'] = program
si.get_sound(save = True, **self.sound_kwargs) si.get_sound(save=True, **self.sound_kwargs)
if si.year is not None: if si.year is not None:
si.find_diffusion(program) si.find_diffusion(program)
si.sound.save(True) si.sound.save(True)
def on_deleted(self, event): def on_deleted(self, event):
logger.info('sound deleted: %s', event.src_path) logger.info('sound deleted: %s', event.src_path)
sound = Sound.objects.filter(path = event.src_path) sound = Sound.objects.filter(path=event.src_path)
if sound: if sound:
sound = sound[0] sound = sound[0]
sound.type = sound.Type.removed sound.type = sound.Type.removed
@ -230,7 +232,7 @@ class MonitorHandler(PatternMatchingEventHandler):
def on_moved(self, event): def on_moved(self, event):
logger.info('sound moved: %s -> %s', event.src_path, event.dest_path) logger.info('sound moved: %s -> %s', event.src_path, event.dest_path)
sound = Sound.objects.filter(path = event.src_path) sound = Sound.objects.filter(path=event.src_path)
if not sound: if not sound:
self.on_modified( self.on_modified(
FileModifiedEvent(event.dest_path) FileModifiedEvent(event.dest_path)
@ -242,18 +244,19 @@ class MonitorHandler(PatternMatchingEventHandler):
if not sound.diffusion: if not sound.diffusion:
program = Program.get_from_path(event.src_path) program = Program.get_from_path(event.src_path)
if program: if program:
si = SoundInfo(sound.path, sound = sound) si = SoundInfo(sound.path, sound=sound)
if si.year is not None: if si.year is not None:
si.find_diffusion(program) si.find_diffusion(program)
sound.save() sound.save()
class Command(BaseCommand): class Command(BaseCommand):
help= __doc__ help = __doc__
def report(self, program = None, component = None, *content): def report(self, program=None, component=None, *content):
if not component: 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]))
@ -270,11 +273,11 @@ class Command(BaseCommand):
logger.info('#%d %s', program.id, program.name) logger.info('#%d %s', program.id, program.name)
self.scan_for_program( self.scan_for_program(
program, settings.AIRCOX_SOUND_ARCHIVES_SUBDIR, program, settings.AIRCOX_SOUND_ARCHIVES_SUBDIR,
type = Sound.Type.archive, type=Sound.Type.archive,
) )
self.scan_for_program( self.scan_for_program(
program, settings.AIRCOX_SOUND_EXCERPTS_SUBDIR, program, settings.AIRCOX_SOUND_EXCERPTS_SUBDIR,
type = Sound.Type.excerpt, type=Sound.Type.excerpt,
) )
dirs.append(os.path.join(program.path)) dirs.append(os.path.join(program.path))
@ -300,14 +303,14 @@ class Command(BaseCommand):
si = SoundInfo(path) si = SoundInfo(path)
sound_kwargs['program'] = program sound_kwargs['program'] = program
si.get_sound(save = True, **sound_kwargs) si.get_sound(save=True, **sound_kwargs)
si.find_diffusion(program, save = True) si.find_diffusion(program, save=True)
si.find_playlist(si.sound) si.find_playlist(si.sound)
sounds.append(si.sound.pk) sounds.append(si.sound.pk)
# sounds in db & unchecked # sounds in db & unchecked
sounds = Sound.objects.filter(path__startswith = subdir). \ sounds = Sound.objects.filter(path__startswith=subdir). \
exclude(pk__in = sounds) exclude(pk__in=sounds)
self.check_sounds(sounds) self.check_sounds(sounds)
@staticmethod @staticmethod
@ -318,18 +321,18 @@ class Command(BaseCommand):
# check files # check files
for sound in qs: for sound in qs:
if sound.check_on_file(): if sound.check_on_file():
sound.save(check = False) sound.save(check=False)
def check_quality(self, check = False): def check_quality(self, check=False):
""" """
Check all files where quality has been set to bad Check all files where quality has been set to bad
""" """
import aircox.management.commands.sounds_quality_check \ import aircox.management.commands.sounds_quality_check \
as quality_check as quality_check
# get available sound files # get available sound files
sounds = Sound.objects.filter(good_quality = False) \ sounds = Sound.objects.filter(good_quality=False) \
.exclude(type = Sound.Type.removed) .exclude(type=Sound.Type.removed)
if check: if check:
self.check_sounds(sounds) self.check_sounds(sounds)
@ -341,11 +344,12 @@ class Command(BaseCommand):
# check quality # check quality
logger.info('quality check...',) logger.info('quality check...',)
cmd = quality_check.Command() cmd = quality_check.Command()
cmd.handle( files = files, cmd.handle(files=files,
**settings.AIRCOX_SOUND_QUALITY ) **settings.AIRCOX_SOUND_QUALITY)
# 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:
@ -353,25 +357,25 @@ class Command(BaseCommand):
sound.duration = utils.seconds_to_time(duration) sound.duration = utils.seconds_to_time(duration)
for sound_info in cmd.good: for sound_info in cmd.good:
sound = Sound.objects.get(path = sound_info.path) sound = Sound.objects.get(path=sound_info.path)
sound.good_quality = True sound.good_quality = True
update_stats(sound_info, sound) update_stats(sound_info, sound)
sound.save(check = False) sound.save(check=False)
for sound_info in cmd.bad: for sound_info in cmd.bad:
sound = Sound.objects.get(path = sound_info.path) sound = Sound.objects.get(path=sound_info.path)
update_stats(sound_info, sound) update_stats(sound_info, sound)
sound.save(check = False) sound.save(check=False)
def monitor(self): def monitor(self):
""" """
Run in monitor mode Run in monitor mode
""" """
archives_handler = MonitorHandler( archives_handler = MonitorHandler(
subdir = settings.AIRCOX_SOUND_ARCHIVES_SUBDIR subdir=settings.AIRCOX_SOUND_ARCHIVES_SUBDIR
) )
excerpts_handler = MonitorHandler( excerpts_handler = MonitorHandler(
subdir = settings.AIRCOX_SOUND_EXCERPTS_SUBDIR subdir=settings.AIRCOX_SOUND_EXCERPTS_SUBDIR
) )
observer = Observer() observer = Observer()
@ -390,10 +394,10 @@ class Command(BaseCommand):
time.sleep(1) time.sleep(1)
def add_arguments(self, parser): def add_arguments(self, parser):
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(
@ -411,7 +415,6 @@ class Command(BaseCommand):
if options.get('scan'): if options.get('scan'):
self.scan() self.scan()
if options.get('quality_check'): if options.get('quality_check'):
self.check_quality(check = (not options.get('scan')) ) self.check_quality(check=(not options.get('scan')))
if options.get('monitor'): if options.get('monitor'):
self.monitor() self.monitor()

View File

@ -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
@ -120,12 +117,11 @@ class Monitor:
self.sync_playlists() self.sync_playlists()
self.handle() self.handle()
def log(self, date = None, **kwargs): def log(self, date=None, **kwargs):
""" """
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
@ -142,14 +138,14 @@ class Monitor:
air_times = (air_time - delta, air_time + delta) air_times = (air_time - delta, air_time + delta)
log = self.log_qs.on_air().filter( log = self.log_qs.on_air().filter(
source = source.id, sound__path = sound_path, source=source.id, sound__path=sound_path,
date__range = air_times, date__range=air_times,
).last() ).last()
if log: if log:
return log return log
# get sound # get sound
sound = Sound.objects.filter(path = sound_path) \ sound = Sound.objects.filter(path=sound_path) \
.select_related('diffusion').first() .select_related('diffusion').first()
diff = None diff = None
if sound and sound.diffusion: if sound and sound.diffusion:
@ -157,20 +153,16 @@ class Monitor:
# check for reruns # check for reruns
if not diff.is_date_in_range(air_time) and not diff.initial: if not diff.is_date_in_range(air_time) and not diff.initial:
diff = Diffusion.objects.at(air_time) \ diff = Diffusion.objects.at(air_time) \
.filter(initial = diff).first() .filter(initial=diff).first()
# 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,23 +170,21 @@ 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)
if pos > now: if pos > now:
break break
# log track on air # log track on air
self.log( self.log(
type = Log.Type.on_air, source = log.source, type=Log.Type.on_air, source=log.source,
date = pos, track = track, date=pos, track=track,
comment = track, comment=track,
) )
def sync_playlists(self): def sync_playlists(self):

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,6 @@
{% load static i18n %}
{% with inline_admin_formset.formset.instance as playlist %}
{% include "adminsortable2/tabular.html" %}
{% endwith %}

View File

@ -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 %}

View File

@ -25,7 +25,7 @@ class Stations:
if self.fetch_timeout and self.fetch_timeout > tz.now(): if self.fetch_timeout and self.fetch_timeout > tz.now():
return return
self.fetch_timeout = tz.now() + tz.timedelta(seconds = 5) self.fetch_timeout = tz.now() + tz.timedelta(seconds=5)
for station in self.stations: for station in self.stations:
station.streamer.fetch() station.streamer.fetch()
@ -39,62 +39,54 @@ 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)
if not station.count(): if not station.count():
return HttpResponse('{}') return HttpResponse('{}')
else: else:
station = stations.stations station = stations.stations
station = station.first() station = station.first()
on_air = station.on_air(count = 10).select_related('track','diffusion') on_air = station.on_air(count=10).select_related('track', 'diffusion')
if not on_air.count(): if not on_air.count():
return HttpResponse('') return HttpResponse('')
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(
diffusion = diff.initial or diff).first() or \ diffusion=diff.initial or diff).first() or \
cms.ProgramPage.objects.filter( cms.ProgramPage.objects.filter(
program = last.program).first() program=last.program).first()
except: except:
pass pass
last = {'date': diff.start, 'type': 'diffusion',
last = { 'title': diff.program.name,
'type': 'diffusion', 'url': publication.specific.url if publication else None}
'title': diff.program.name,
'date': diff.start,
'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))
# TODO: # TODO:
# - login url # - login url
class Monitor(View,TemplateResponseMixin,LoginRequiredMixin): class Monitor(View, TemplateResponseMixin, LoginRequiredMixin):
template_name = 'aircox/controllers/monitor.html' template_name = 'aircox/controllers/monitor.html'
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
stations.fetch() stations.fetch()
return { 'stations': stations.stations } return {'stations': stations.stations}
def get(self, request = None, **kwargs): def get(self, request=None, **kwargs):
if not request.user.is_active: if not request.user.is_active:
return Http404() return Http404()
@ -102,7 +94,7 @@ class Monitor(View,TemplateResponseMixin,LoginRequiredMixin):
context = self.get_context_data(**kwargs) context = self.get_context_data(**kwargs)
return render(request, self.template_name, context) return render(request, self.template_name, context)
def post(self, request = None, **kwargs): def post(self, request=None, **kwargs):
if not request.user.is_active: if not request.user.is_active:
return Http404() return Http404()
@ -113,15 +105,15 @@ class Monitor(View,TemplateResponseMixin,LoginRequiredMixin):
controller = POST.get('controller') controller = POST.get('controller')
action = POST.get('action') action = POST.get('action')
station = stations.stations.filter(name = POST.get('station')) \ station = stations.stations.filter(name=POST.get('station')) \
.first() .first()
if not station: if not station:
return Http404() return Http404()
source = None source = None
if 'source' in POST: if 'source' in POST:
source = [ s for s in station.sources source = [s for s in station.sources
if s.name == POST['source'] ] if s.name == POST['source']]
source = source[0] source = source[0]
if not source: if not source:
return Http404 return Http404
@ -141,11 +133,11 @@ class Monitor(View,TemplateResponseMixin,LoginRequiredMixin):
source.restart() source.restart()
class StatisticsView(View,TemplateResponseMixin,LoginRequiredMixin): 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:
@ -179,7 +171,7 @@ class StatisticsView(View,TemplateResponseMixin,LoginRequiredMixin):
self.__dict__.update(kwargs) self.__dict__.update(kwargs)
# Note: one row contains a column for diffusions and one for streams # Note: one row contains a column for diffusions and one for streams
#def append(self, log): # def append(self, log):
# if log.col == 0: # if log.col == 0:
# self.rows.append((log, [])) # self.rows.append((log, []))
# return # return
@ -194,13 +186,12 @@ 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.
""" """
stats = self.Stats(station = station, date = date, stats = self.Stats(station=station, date=date,
items = [], tags = {}) items=[], tags={})
qs = Log.objects.station(station).on_air() \ qs = Log.objects.station(station).on_air() \
.prefetch_related('diffusion', 'sound', 'track', 'track__tags') .prefetch_related('diffusion', 'sound', 'track', 'track__tags')
@ -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'), .prefetch_related('tags'),
col = 0,
tracks = models.Track.objects.related(object = rel)
.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'), col=1, tracks=[],
type = _('Stream'),
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):
@ -270,7 +249,7 @@ class StatisticsView(View,TemplateResponseMixin,LoginRequiredMixin):
return context return context
def get(self, request = None, **kwargs): def get(self, request=None, **kwargs):
if not request.user.is_active: if not request.user.is_active:
return Http404() return Http404()
@ -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
View File

68
aircox_web/admin.py Normal file
View 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
View File

@ -0,0 +1,5 @@
from django.apps import AppConfig
class AircoxWebConfig(AppConfig):
name = 'aircox_web'

View File

@ -0,0 +1 @@
import './js';

View 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
View 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
View 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
View 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,
),
)

View 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>

View 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
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

9
aircox_web/urls.py Normal file
View 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
View 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
),
})

View 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']
},
})

View File

@ -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',

View File

@ -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

View File

@ -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