From 248b77fca4da174ec40442eb33a30eb03b44fc28 Mon Sep 17 00:00:00 2001 From: bkfox Date: Wed, 7 Aug 2019 01:45:27 +0200 Subject: [PATCH] website --- aircox/__init__.py | 1 - aircox/admin/__init__.py | 4 +- .../admin/__pycache__/__init__.cpython-37.pyc | Bin 426 -> 491 bytes .../admin/__pycache__/episode.cpython-37.pyc | Bin 2713 -> 2691 bytes aircox/admin/__pycache__/page.cpython-37.pyc | Bin 1297 -> 1619 bytes aircox/admin/__pycache__/sound.cpython-37.pyc | Bin 1327 -> 2357 bytes aircox/admin/article.py | 20 ++++ aircox/admin/episode.py | 3 +- aircox/admin/page.py | 22 +++- aircox/admin/playlist.py | 40 ------- aircox/admin/sound.py | 31 +++++- aircox/apps.py | 3 +- aircox/connector.py | 3 +- aircox/controllers.py | 13 +-- aircox/converters.py | 1 - aircox/middleware.py | 16 ++- aircox/models/__init__.py | 5 +- .../__pycache__/__init__.cpython-37.pyc | Bin 469 -> 559 bytes .../models/__pycache__/episode.cpython-37.pyc | Bin 10198 -> 10245 bytes aircox/models/__pycache__/page.cpython-37.pyc | Bin 4575 -> 6200 bytes .../models/__pycache__/program.cpython-37.pyc | Bin 16618 -> 16598 bytes aircox/models/article.py | 27 +++++ aircox/models/episode.py | 21 ++-- aircox/models/page.py | 57 ++++++++-- aircox/models/program.py | 7 +- aircox/models/signals.py | 100 ++++++++++++++++++ aircox/settings.py | 21 ++-- aircox/signals.py | 92 ---------------- aircox/static/aircox/main.css | 7 +- aircox/templates/aircox/base.html | 21 ++-- aircox/templates/aircox/diffusion_item.html | 59 ----------- aircox/templates/aircox/diffusion_list.html | 51 --------- .../templates/aircox/diffusion_timetable.html | 10 +- aircox/templates/aircox/episode_detail.html | 27 +++++ aircox/templates/aircox/log_list.html | 6 +- aircox/templates/aircox/page.html | 11 +- aircox/templates/aircox/podcast_item.html | 2 +- aircox/templates/aircox/program_base.html | 10 +- aircox/templates/aircox/program_header.html | 37 ++++--- aircox/templatetags/aircox.py | 24 ++++- aircox/tests.py | 21 ++-- aircox/urls.py | 23 ++-- aircox/utils.py | 2 +- aircox/views.py | 11 +- aircox/views/__init__.py | 7 ++ aircox/views/article.py | 14 +++ aircox/views/base.py | 32 ++++++ aircox/views/episode.py | 99 +++++++++++++++++ aircox/views/log.py | 96 +++++++++++++++++ aircox/views/page.py | 84 +++++++++++++++ aircox/views/program.py | 29 +++++ assets/styles.scss | 8 +- 52 files changed, 794 insertions(+), 384 deletions(-) create mode 100644 aircox/admin/article.py delete mode 100644 aircox/admin/playlist.py create mode 100644 aircox/models/article.py create mode 100755 aircox/models/signals.py delete mode 100755 aircox/signals.py delete mode 100644 aircox/templates/aircox/diffusion_item.html delete mode 100644 aircox/templates/aircox/diffusion_list.html create mode 100644 aircox/views/__init__.py create mode 100644 aircox/views/article.py create mode 100644 aircox/views/base.py create mode 100644 aircox/views/episode.py create mode 100644 aircox/views/log.py create mode 100644 aircox/views/page.py create mode 100644 aircox/views/program.py diff --git a/aircox/__init__.py b/aircox/__init__.py index 877e873..0b70342 100755 --- a/aircox/__init__.py +++ b/aircox/__init__.py @@ -1,3 +1,2 @@ default_app_config = 'aircox.apps.AircoxConfig' - diff --git a/aircox/admin/__init__.py b/aircox/admin/__init__.py index 52f64e1..21436bb 100644 --- a/aircox/admin/__init__.py +++ b/aircox/admin/__init__.py @@ -1,7 +1,7 @@ +from .article import ArticleAdmin from .episode import DiffusionAdmin, EpisodeAdmin from .log import LogAdmin -# from .playlist import PlaylistAdmin from .program import ProgramAdmin, ScheduleAdmin, StreamAdmin -from .sound import SoundAdmin +from .sound import SoundAdmin, TrackAdmin from .station import StationAdmin diff --git a/aircox/admin/__pycache__/__init__.cpython-37.pyc b/aircox/admin/__pycache__/__init__.cpython-37.pyc index c11ecc4f2a8ece1c7ae83f1fafbde63eaf1d8b43..8a13be3e971486e225e6080a40c369e401bb98ee 100644 GIT binary patch delta 304 zcmXAkF;2rU6o&0Mi4!+YtEv*>2)O}k5eo|fmC1r3s#9d)msBrdX5s)5r+_1QU}8a> zfUUOi@brKGCqK)d?3c+)DFyN8;`{pXZDKq?tIw>2!)(r3OSP4Pqh6Ag9M7ALc}-ByF?o5a;u=5Uzb z?ly@NyWyqRQ8;mr?f!ZGI7t(L*@z7P%%3^VHV{PeQW{qAbA)? n=(91b#xfM)=Z=q4JDH)!g{KdK>tNRJH}tj+zM?By(T$%c^yNqa delta 257 zcmXAhv2MaJ5Qcqr5{F<5hAw@COnreaWuj8Wk^y0f0m-O}RmL}gUqtYW3SK&adu7r)RKRr}M}4~unELa(arYFoEW^s`d;a=od$ z)~-DdA6^9Mg=@ckLL@gn;0k?8$BabMKJBQ}=P}bg8~q@-f96M&BDuf6;r;-hieh6> I`~?&|03Mk;&Hw-a diff --git a/aircox/admin/__pycache__/episode.cpython-37.pyc b/aircox/admin/__pycache__/episode.cpython-37.pyc index 8ef6f1cb18bc23a4fe1d7cf04cb1992c38a3bc8a..efe1b5b8f21cfe9e6244deef174daa20b9c720b8 100644 GIT binary patch delta 290 zcmbO!+APZF#LLUY00bqCp0QuqCh|!zZkwncnwBdNCBVqw&XB^9!r8)*!r9CeC78;z zKq!@SAtO+fD}@^<%AE=n%Mxy8X=aQP0gCdZ@B&47!J?wgEKy=$9UNdCd@1}uIsR1M zW@eyj@f5~j22Fv9uM)+WZgB_am*%B-=H+DOrQYHRDN0PvF5aBXIFpf4c=BJSHH`d| zS20gz#RVI*yKBGUm0~LUt+If)R^qc@rR8E=#?Vw$^M*L zjE0k|IHlMnimZS_QIltL+A>B>zR9V}bBnbYWM2_CP<*lsSE7^{kio&k!zjQc0V0|C NxP%ya1VuQ77y;%~MsolF delta 329 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 delta 651 zcmZuvv5wO~5cN2Aymst__yW2Uq5vUImQHlnAOr#mpx`8QG(}B zP$Na=1E8e33Q9^oV*daMiC2j1~z`7^XXgjv{bSb&3AX%{Dzx{+DF#Ih%%wp~`Wn)ir>|3K+jQe!lOEEEHfMcNycQgXsAVBC zw@tR{S)voF>JtnINYBFA#174yO4k!E%v8&HEvvE>w6T%~)6&dUzOubGLhrTEHry^l z)JCVI6_8dS+kKjg%}~A_ou<}T5a?P%2j((2ay^J3-T}`YK?;{JY>SSvLGzp)?%$#= z55Xq6)YEEtyL}k5eI-Vo$Wily?HyUB#R&%JEP6)I_kb6&Y~}uE# zxC5SG%g^(^HzQg8R$p#hRk8jphqLN0S1Sz?8jDrdky5OP*-prKo zQG><+0t_LXnQJ0kzyf24XYZK=Q%L99HyP%T;}Vv$*Jg!lSmOpZvk%M`cd(O}q6+Ut z5&cN}jzUiQipKWQ)PmTJ7YG2PWkch@-0r)_0dwY!b&Glg2V_S$vJW{Do~g}f+?h{ zi=D;{GRz@gvvM0Opjhj~m1w|V1=X5UcY!t3*g!*BKZ`c|m9(mjf_m!qebC$P?Q!W( z=iPDJUIsOh^bvLVm=N^j$Gz{+D^>|3C4layVIFPoHLHveVn>}hKPL0|da#~<=nEEl kUbV|Z?Lz+`&r&sJX6anS<8~{Kahz(*Z=?k diff --git a/aircox/models/__pycache__/episode.cpython-37.pyc b/aircox/models/__pycache__/episode.cpython-37.pyc index ea46f1c803c65b839261c0077efb0df6e2ba0909..1adab9a0ab2c1e7382f91c0bd878120489f18347 100644 GIT binary patch delta 3204 zcmbVOO>7&-73S0zXj@5OXxUQYnaaEa)e-b;2Y{zmD5Ovlo?oeK9$)(?} z6f0$_HcDflO%tS3peRzbX)Zw!AV?s%Jrrn9&9%uT0p`*}4?zoG+8hd`m%cYEij*K< zy2O5bGyCS7H}Ac9GoM}h@#X0!Iyy2jd~Uyg;d>ig z5!R&5p}=D$=EO>xQqW}Axw;$p4!^R(VeKjp=S^K``hoO&G1!mVuwTsSrw_vb%6<3- z8bXSxb{WO(hfJp@cwX$&F7{>b#wyH7JDFF~8?2PS8#7|1ZgEHZ$u$^6Ef^vgMv%;c zQTpGv%3;wDxK*>O+^TykV0&98kghUjgcOZucxX?e6c`98hSp4ocl67wAimU(uWu^? zdN+2E^b!a$mciePxvk|s8rZ9RzDx}CkoELt@eO7X8oI-lQ!pOJZn0ZhgEh3bb(D?Y zF-QBER?`~#OL`dJh}}vwScNt826OaZYYpRVZUF0I29eeYf3q2<=x&{}@MhNZx&fL}G^QZIXfH0t&e2NU(+5WzHp$>3`w zw^n-un-Aj~tu}Fh^{^L5nycNDp3R#*x%O~~fy7jHjA+Sd7!cFXE&Onfo< z5Gg;7AT`%%ae*SC{%?sdlfki*q(-SIjfJ&3mkGaS*)HIeiXci_(oy3mmNVj4sXwrj z;!1khR63_=bleArgVb&ZyXT_MMe&RD@4tHrB{LyY6ur=`MnS+J`i{K@$`DE#I74bi zH2;&-ahBj5!7g*?>v{2Z=Dia|`g)##rUUw5>Jf#Cw4&HKT55JUxA~)Nnz2*jr@4{h zQ#7(jpoo+~%9eJbJn?0)*nFOj0aFO8urcEsJiVys{xY+r@vqr?a{AGc1NT)-C6?{NDT|*SpVIuN`Bf&V2Y3WdlwFqsM=Il zWY+V`wr2&Q4Iw;2JxMA_F4keP0c#l%l#C#Rw!NOA>s1Hs>HKG+ba;VHHo-Ir7iF<temaz4-9?%#t$d_v!6|Wncw|WVbf-md9X0>5 zIkCT+Jw}7zCV{faMd<-HMa&lNkH3ngk>BKJXpqW1mH&^6zQROdkJ&Itf#tcYTy9=1 zjIvQ0M@L3@bHQZNsCag0rer+uS8(U~;*HV%IaMeWwMWX?Roh$PL=uEsn-yyu*46-g zGGgcIWR4CKO!GC;|J&xjM$fUV(o+`K#p&_cQ#1sRyCkaGVF+p+NPSJ&MhCo%t4Yxp zttlv)@EpRw#h=E{EC4mtSCvl7)THVr<8H{SfvUgFs=JE)rG#2zP}1=vRYo`26>*3v z{}9(EK6$u^QdCbU@G3A{xg8y{ZR-`6C;QlcL}l__HYYl#hMhSafo|36%Vt`gLhu-I zD38WT+NP9Aey8lM;2HB0^*^Bo?sZ1V4svMOuT?y^90oBaUYojcdL8v;db?NZ0S}v2 z{*VaUtR&U0@=ab|Zqa1KBL{9Tsx76rjEd@5BD!>>tmw*NwSB)s)Qbqss!bQD2i!O| z@KgTh4;@@OX}Nw<7>l3Be+;2{?jG)xpOK8m>Ww z`qWuV7jm^};TB0Xf)@xVXP`kq;Q%@rlyiWV4krjO41-ZHFx%9XOl2EgP->!h@QgkM z=f$fBLsk_3Jvhb2#Mt!lLV-qh5_A#d3Azc6B8c15z0c8oJ93V4BGI))T*aQ|k*S%g k@2q%ymSHQP<0-{*T8`!Mm!qGaXM zCE@kS(UhRecawBN4R~&6JJSG4{Qw!TjWz6w9r$3vo0RW zdbw8;FN<1Pmb|=IFna%MiDaU-U!AwVtu9(qDJ>itRv+2Jx%U$^&i*v6UbM~)>f~h^ zx#$Q%Ei1BuDrzEmRSc;xdD1bahhfhvNld^l%_1T4{9iB5ulZr%$tOeiwyfJ2;2++g zc%Gkdm_?LW6|2^U*s$KSSx?>myxq|6{q$6ieP_I?9cW#Cr+w z+`MnywKt&J8)6l7ZP2|^wPUI&Gb0^g7iRw?zhXT}Pe8|VV%DF67RMW!p`Kf&py^<- z7&UQESVghk-zB_EgB^qF0UCkdL5M?YK0R3g&3J;63e(4B1#N7OF4P-t7&c{eBk*EQ zpGJ3|!u>3=A}P|MX!G0Jmn^1=S@m)H}d&(E<|P3tTI7x_(T0<2R};JJ6`DIO$jB|QzZBpK`r=s+6MGZ4GzV02#nF8}__ z8H~~zXqjwD^(D);q^FYlOwz6iws8#aAiE(au+RJd>H#6>`TiA8s{m`B^PwR?MYirm(U zyzzEv_Uev=ggZ9g>})S~w1B>CaJqtKFMLMBNubAAoZ9+M>78VK7b;SvR%o~CL2KEk zru1~W?zT9E_ry8P*5BmE9DihQQV{wKInHwwA(~aM4=j{%d>)7j>WklsXjFYPFmkBJ z4YVHQPdEH9st3)rxi{3{;N%$mb*58obf$hI@ar-pM2jw~uMYljaTk4aNX@Kca4oD! zYWiX52d$k6-U~#9t$x5yX8~M>EiS_gkG-~OlF-B$nYf;+JpR(%DsP9RcB`79+vKgY#dKR>fxa2JnjI;@cKagbLhL{Ak7mR?#3@oP7g?Ze|R#kD7$cq2B*XN zr|QGu#p9RZhG0Kp>a5o$H{#&_QZ0?#t32A$l#E|*Js8Q03pkTHXX0h53B2?G?uvSw zNVagRH065Gl6%ieOYHF5t&4~H#VKIX3WTA0gSEY#Iukm4!v3h<9DTPu3#Zc%$atDj zb7M!w_6l#t51od8TmEHhV{BZAS@q8NXuZWgI*DY}c`y%ycFPOnVrW2b!k}>5ZL}ra z!>Du6xoqB9;|vDCjuVCK*F_+$HQYO}|8VQz;dxOoc7|)9Rx5L7uxC|EXV}z@A~Gvr z=tcM^TW(Y8l$n&H(3=A2^fea$Qh%?UU%Z8;88!K}iY6+fPWh2+#;fQS@;3L^((&?Y zw&S-9=`Okf4%y@%>fVt*oin>}mYbae%33G!_xzG5b7W1`_FImra{pG}oA}Y@C3dT2 zc%vL=C8}DroHWETo&DX;3ztdp`-JWU?wtg{ucL^RQ2fbOApsP&Go|z z>Wqu3PM?J^VX>f@Y_y@BfEa-I48$nJjC%Lz&?ZiRE;vq8Hm^$x8EGAZNITv44BxAZ zd>}9Qvf=HZUV+KiAzpy+Sm^8;1zd5XJ0#I(uJAmxB?up)32_?&c|$L;I7RoM;vO-v zAgq)=4`Egh(G7=*1a`iu{A1_s37S_Q9E-#mH8njcM%BXfN&bGmJ3Vw$C)eB+N%*Rw z11u0F*L;VW5SUPz4BDQevz@Tbo^A$SyCLTUw?afJD~kN3Smu8bf4eM(t+JiB%a!uX Fe*uuClA{0s diff --git a/aircox/models/__pycache__/page.cpython-37.pyc b/aircox/models/__pycache__/page.cpython-37.pyc index b11411de909f393c143ca185083dd17f83a16001..5bc095791018d6f55087273f492c464bc8ba479a 100644 GIT binary patch literal 6200 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 RS;c^icblKvF)=X;001?)O&$OM delta 295 zcmccC$oQ&}k=Kcrmx}=it~YqZPTR=qWx}X8Il^R%VNMOp0a%XZ!c>N zdkv!vL*cG)h8m6qQVSVs*g<3(Q!s-j-{#4t6PYG!TgWg1(ikffXvC3R_a1e85tOg!JcH~V-{oNU}oE#XC=kRcy4m1bqS;1 g=6}{Jn7A(kRTtd^5w|99v{PjKxcRaj6BDB_0Fhl(;{X5v diff --git a/aircox/models/article.py b/aircox/models/article.py new file mode 100644 index 0000000..8986eb6 --- /dev/null +++ b/aircox/models/article.py @@ -0,0 +1,27 @@ +from django.db import models +from django.utils.translation import ugettext_lazy as _ + +from .page import Page +from .program import Program, InProgramQuerySet + + +class Article(Page): + program = models.ForeignKey( + Program, models.SET_NULL, + verbose_name=_('program'), blank=True, null=True, + help_text=_("publish as this program's article"), + ) + is_static = models.BooleanField( + _('is static'), default=False, + help_text=_('Should this article be considered as a page ' + 'instead of a blog article'), + ) + + objects = InProgramQuerySet.as_manager() + + class Meta: + verbose_name = _('Article') + verbose_name_plural = _('Articles') + + + diff --git a/aircox/models/episode.py b/aircox/models/episode.py index 37af2b1..d145db4 100644 --- a/aircox/models/episode.py +++ b/aircox/models/episode.py @@ -18,13 +18,17 @@ from .page import Page, PageQuerySet __all__ = ['Episode', 'Diffusion', 'DiffusionQuerySet'] +class EpisodeQuerySet(PageQuerySet, InProgramQuerySet): + pass + + class Episode(Page): program = models.ForeignKey( Program, models.CASCADE, verbose_name=_('program'), ) - objects = InProgramQuerySet.as_manager() + objects = EpisodeQuerySet.as_manager() detail_url_name = 'episode-detail' class Meta: @@ -37,17 +41,14 @@ class Episode(Page): super().save(*args, **kwargs) @classmethod - def get_default_title(cls, program, date): + def get_init_kwargs_from(cls, page, date, title=None, **kwargs): """ Get default Episode's title """ - return settings.AIRCOX_EPISODE_TITLE.format( - program=program, + title = settings.AIRCOX_EPISODE_TITLE.format( + program=page, date=date.strftime(settings.AIRCOX_EPISODE_TITLE_DATE_FORMAT), - ) - - @classmethod - def from_date(cls, program, date): - title = cls.get_default_title(program, date) - return cls(program=program, title=title, cover=program.cover) + ) if title is None else title + return super().get_init_kwargs_from(page, title=title, program=page, + **kwargs) class DiffusionQuerySet(BaseRerunQuerySet): diff --git a/aircox/models/page.py b/aircox/models/page.py index 959ef7d..8fc61e7 100644 --- a/aircox/models/page.py +++ b/aircox/models/page.py @@ -1,13 +1,13 @@ from enum import IntEnum +import re from django.db import models from django.urls import reverse +from django.utils import timezone as tz from django.utils.text import slugify from django.utils.html import format_html from django.utils.translation import ugettext_lazy as _ - -from django.contrib.contenttypes.fields import GenericForeignKey -from django.contrib.contenttypes.models import ContentType +from django.utils.functional import cached_property from ckeditor.fields import RichTextField from filer.fields.image import FilerImageField @@ -16,13 +16,36 @@ from model_utils.managers import InheritanceQuerySet from .station import Station -__all__ = ['PageQuerySet', 'Page', 'NavItem'] +__all__ = ['Category', 'PageQuerySet', 'Page', 'NavItem'] + + +headline_re = re.compile(r'(

)?' + r'(?P[^\n]{1,140}(\n|[^\.]*?\.))' + r'(

)?') + + +class Category(models.Model): + title = models.CharField(_('title'), max_length=64) + slug = models.SlugField(_('slug'), max_length=64, db_index=True) + + class Meta: + verbose_name = _('Category') + verbose_name_plural = _('Categories') + + def __str__(self): + return self.title class PageQuerySet(InheritanceQuerySet): + def draft(self): + return self.filter(status=Page.STATUS.draft) + def published(self): return self.filter(status=Page.STATUS.published) + def trash(self): + return self.filter(status=Page.STATUS.trash) + class Page(models.Model): """ Base class for publishable content """ @@ -38,13 +61,18 @@ class Page(models.Model): default=STATUS.draft, choices=[(int(y), _(x)) for x, y in STATUS.__members__.items()], ) + category = models.ForeignKey( + Category, models.SET_NULL, + verbose_name=_('category'), blank=True, null=True, db_index=True + ) cover = FilerImageField( - on_delete=models.SET_NULL, null=True, blank=True, - verbose_name=_('Cover'), + on_delete=models.SET_NULL, + verbose_name=_('Cover'), null=True, blank=True, ) content = RichTextField( _('content'), blank=True, null=True, ) + date = models.DateTimeField(default=tz.now) featured = models.BooleanField( _('featured'), default=False, ) @@ -86,6 +114,23 @@ class Page(models.Model): def is_trash(self): return self.status == self.STATUS.trash + @cached_property + def headline(self): + if not self.content: + return '' + headline = headline_re.search(self.content) + return headline.groupdict()['headline'] if headline else '' + + @classmethod + def get_init_kwargs_from(cls, page, **kwargs): + kwargs.setdefault('cover', page.cover) + kwargs.setdefault('category', page.category) + return kwargs + + @classmethod + def from_page(cls, page, **kwargs): + return cls(**cls.get_init_kwargs_from(page, **kwargs)) + class NavItem(models.Model): """ Navigation menu items """ diff --git a/aircox/models/program.py b/aircox/models/program.py index 9852f1d..dba587d 100644 --- a/aircox/models/program.py +++ b/aircox/models/program.py @@ -470,7 +470,8 @@ class Schedule(BaseRerun): continue if initial is None: - episode = Episode.from_date(self.program, date) + episode = Episode.from_page(self.program, date=date) + episode.date = date episodes[date] = episode else: episode = episodes[initial] @@ -489,10 +490,6 @@ class Schedule(BaseRerun): if self.initial is not None and self.date > self.date: raise ValueError('initial must be later') - # initial only if it has been yet saved - if self.pk: - self.__initial = self.__dict__.copy() - class Stream(models.Model): """ diff --git a/aircox/models/signals.py b/aircox/models/signals.py new file mode 100755 index 0000000..7b2ad76 --- /dev/null +++ b/aircox/models/signals.py @@ -0,0 +1,100 @@ +import pytz + +from django.contrib.auth.models import User, Group, Permission +from django.db.models import F, signals +from django.dispatch import receiver +from django.utils import timezone as tz + +from .. import settings, utils +from . import Diffusion, Episode, Program, Schedule + + +# Add a default group to a user when it is created. It also assigns a list +# of permissions to the group if it is created. +# +# - group name: settings.AIRCOX_DEFAULT_USER_GROUP +# - group permissions: settings.AIRCOX_DEFAULT_USER_GROUP_PERMS +# +@receiver(signals.post_save, sender=User) +def user_default_groups(sender, instance, created, *args, **kwargs): + """ + Set users to different default groups + """ + if not created or instance.is_superuser: + return + + for groupName, permissions in settings.AIRCOX_DEFAULT_USER_GROUPS.items(): + if instance.groups.filter(name=groupName).count(): + continue + + group, created = Group.objects.get_or_create(name=groupName) + if created and permissions: + for codename in permissions: + permission = Permission.objects.filter( + codename=codename).first() + if permission: + group.permissions.add(permission) + group.save() + instance.groups.add(group) + + +@receiver(signals.post_save, sender=Program) +def program_post_save(sender, instance, created, *args, **kwargs): + """ + Clean-up later diffusions when a program becomes inactive + """ + if not instance.active: + Diffusion.objects.program(instance).after().delete() + Episode.object.program(instance).filter(diffusion__isnull=True) \ + .delete() + + +@receiver(signals.pre_save, sender=Schedule) +def schedule_pre_save(sender, instance, *args, **kwargs): + if getattr(instance, 'pk') is not None: + instance._initial = Schedule.objects.get(pk=instance.pk) + + +# TODO +@receiver(signals.post_save, sender=Schedule) +def schedule_post_save(sender, instance, created, *args, **kwargs): + """ + Handles Schedule's time, duration and timezone changes and update + corresponding diffusions accordingly. + """ + initial = getattr(instance, '_initial', None) + if not initial or ((instance.time, instance.duration, instance.timezone) == + (initial.time, initial.duration, initial.timezone)): + return + + today = tz.datetime.today() + delta = instance.normalize(today) - initial.normalize(today) + + qs = Diffusion.objects.program(instance.program).after() + pks = [d.pk for d in qs if initial.match(d.date)] + qs.filter(pk__in=pks).update( + start=F('start') + delta, + end=F('start') + delta + utils.to_timedelta(instance.duration) + ) + + +@receiver(signals.pre_delete, sender=Schedule) +def schedule_pre_delete(sender, instance, *args, **kwargs): + """ + Delete later corresponding diffusion to a changed schedule. + """ + if not instance.program.sync: + return + + qs = Diffusion.objects.program(instance.program).after() + pks = [d.pk for d in qs if instance.match(d.date)] + qs.filter(pk__in=pks).delete() + + +@receiver(signals.post_delete, sender=Diffusion) +def diffusion_post_delete(sender, instance, *args, **kwargs): + Episode.objects.filter(diffusion__isnull=True, content_isnull=True, + sound__isnull=True) \ + .delete() + + diff --git a/aircox/settings.py b/aircox/settings.py index 682d776..43ffbca 100755 --- a/aircox/settings.py +++ b/aircox/settings.py @@ -3,9 +3,11 @@ import stat from django.conf import settings -def ensure (key, default): + +def ensure(key, default): globals()[key] = getattr(settings, key, default) + ######################################################################## # Global & misc ######################################################################## @@ -48,7 +50,7 @@ ensure('AIRCOX_EPISODE_TITLE_DATE_FORMAT', '%-d %B %Y') # Directory where to save logs' archives ensure('AIRCOX_LOGS_ARCHIVES_DIR', os.path.join(AIRCOX_DATA_DIR, 'archives') -) + ) # In days, minimal age of a log before it is archived ensure('AIRCOX_LOGS_ARCHIVES_MIN_AGE', 60) @@ -70,21 +72,21 @@ ensure('AIRCOX_SOUND_AUTO_CHMOD', True) # and stat.* ensure( 'AIRCOX_SOUND_CHMOD_FLAGS', - (stat.S_IRWXU, stat.S_IRWXU | stat.S_IRWXG | stat.S_IROTH ) + (stat.S_IRWXU, stat.S_IRWXU | stat.S_IRWXG | stat.S_IROTH) ) # Quality attributes passed to sound_quality_check from sounds_monitor ensure('AIRCOX_SOUND_QUALITY', { - 'attribute': 'RMS lev dB', - 'range': (-18.0, -8.0), - 'sample_length': 120, - } + 'attribute': 'RMS lev dB', + 'range': (-18.0, -8.0), + 'sample_length': 120, +} ) # Extension of sound files ensure( 'AIRCOX_SOUND_FILE_EXT', - ('.ogg','.flac','.wav','.mp3','.opus') + ('.ogg', '.flac', '.wav', '.mp3', '.opus') ) @@ -107,6 +109,3 @@ ensure( ensure('AIRCOX_IMPORT_PLAYLIST_CSV_DELIMITER', ';') # Text delimiter of csv text files ensure('AIRCOX_IMPORT_PLAYLIST_CSV_TEXT_QUOTE', '"') - - - diff --git a/aircox/signals.py b/aircox/signals.py deleted file mode 100755 index 8844968..0000000 --- a/aircox/signals.py +++ /dev/null @@ -1,92 +0,0 @@ -import pytz - -from django.contrib.auth.models import User, Group, Permission -from django.contrib.contenttypes.models import ContentType -from django.db.models import F -from django.db.models.signals import post_save, pre_save, pre_delete, m2m_changed -from django.dispatch import receiver, Signal -from django.utils import timezone as tz -from django.utils.translation import ugettext as _, ugettext_lazy - -import aircox.models as models -import aircox.utils as utils -import aircox.settings as settings - - - -# Add a default group to a user when it is created. It also assigns a list -# of permissions to the group if it is created. -# -# - group name: settings.AIRCOX_DEFAULT_USER_GROUP -# - group permissions: settings.AIRCOX_DEFAULT_USER_GROUP_PERMS -# -@receiver(post_save, sender=User) -def user_default_groups(sender, instance, created, *args, **kwargs): - """ - Set users to different default groups - """ - if not created or instance.is_superuser: - return - - for groupName, permissions in settings.AIRCOX_DEFAULT_USER_GROUPS.items(): - if instance.groups.filter(name = groupName).count(): - continue - - group, created = Group.objects.get_or_create(name = groupName) - if created and permissions: - for codename in permissions: - permission = Permission.objects.filter(codename = codename).first() - if permission: - group.permissions.add(permission) - group.save() - instance.groups.add(group) - -@receiver(post_save, sender=models.Program) -def program_post_save(sender, instance, created, *args, **kwargs): - """ - Clean-up later diffusions when a program becomes inactive - """ - if not instance.active: - instance.diffusion_set.after().delete() - -@receiver(post_save, sender=models.Schedule) -def schedule_post_save(sender, instance, created, *args, **kwargs): - """ - Handles Schedule's time, duration and timezone changes and update - corresponding diffusions accordingly. - """ - if created or not instance.program.sync or \ - not instance.changed(['time','duration','timezone']): - return - - initial = instance._Schedule__initial - initial = models.Schedule(**{ k: v - for k, v in instance._Schedule__initial.items() - if not k.startswith('_') - }) - - today = tz.datetime.today() - delta = instance.normalize(today) - \ - initial.normalize(today) - - qs = models.Diffusion.objects.program(instance.program).after() - pks = [ d.pk for d in qs if initial.match(d.date) ] - qs.filter(pk__in = pks).update( - start = F('start') + delta, - end = F('start') + delta + utils.to_timedelta(instance.duration) - ) - - -@receiver(pre_delete, sender=models.Schedule) -def schedule_pre_delete(sender, instance, *args, **kwargs): - """ - Delete later corresponding diffusion to a changed schedule. - """ - if not instance.program.sync: - return - - qs = models.Diffusion.objects.program(instance.program).after() - pks = [ d.pk for d in qs if instance.match(d.date) ] - qs.filter(pk__in = pks).delete() - - diff --git a/aircox/static/aircox/main.css b/aircox/static/aircox/main.css index b988d70..35c8116 100644 --- a/aircox/static/aircox/main.css +++ b/aircox/static/aircox/main.css @@ -7169,7 +7169,7 @@ label.panel-block { float: right; max-width: 45%; } -.page > .header { +.page .header { margin-bottom: 1.5em; } .page .headline { @@ -7179,6 +7179,11 @@ label.panel-block { .page p { padding: 0.4em 0em; } +section > .toolbar { + background-color: rgba(0, 0, 0, 0.05); + padding: 1em; + margin-bottom: 1.5em; } + .cover { margin: 1em 0em; border: 0.2em black solid; } diff --git a/aircox/templates/aircox/base.html b/aircox/templates/aircox/base.html index 2349978..4c5d6b3 100644 --- a/aircox/templates/aircox/base.html +++ b/aircox/templates/aircox/base.html @@ -5,10 +5,11 @@ Context: {% endcomment %} - - - - + + + + + {% block assets %} @@ -18,7 +19,7 @@ Context: {% endblock %} - {% block head_title %}{{ site.title }}{% endblock %} + {% block head_title %}{{ station.name }}{% endblock %} {% block head_extra %}{% endblock %} @@ -52,12 +53,14 @@ Context: {% block header %}

{% block title %}{% endblock %}

- {% if parent %} -

- +

+ {% block subtitle %} + {% if parent %} + ❬ {{ parent.title }} + {% endif %} + {% endblock %}

- {% endif %} {% endblock %} diff --git a/aircox/templates/aircox/diffusion_item.html b/aircox/templates/aircox/diffusion_item.html deleted file mode 100644 index b907276..0000000 --- a/aircox/templates/aircox/diffusion_item.html +++ /dev/null @@ -1,59 +0,0 @@ -{% load i18n easy_thumbnails_tags aircox %} -{% comment %} -Context variables: -- object: the actual diffusion -- page: current parent page in which item is rendered -- hide_schedule: if True, do not display start time -- hide_headline: if True, do not display headline -{% endcomment %} - -{% with object.episode as episode %} -{% with episode.program as program %} -
-
- -
-
-
- {% if episode.is_published %} - {{ episode.title }} - {% endif %} -
- -
- {% if not page or program != page %} - {% if program.is_published %} - - {{ program.title }} - {% else %}{{ program.title }} - {% endif %} - {% if not hide_schedule %} — {% endif %} - {% endif %} - - {% if not hide_schedule %} - - {% endif %} - - {% if object.initial %} - {% with object.initial.date as date %} - - {% trans "rerun" %} - - {% endwith %} - {% endif %} - -
- {% if not hide_headline %} -
- {{ episode.headline }} -
- {% endif %} -
-
-{% endwith %} -{% endwith %} - diff --git a/aircox/templates/aircox/diffusion_list.html b/aircox/templates/aircox/diffusion_list.html deleted file mode 100644 index 57850b1..0000000 --- a/aircox/templates/aircox/diffusion_list.html +++ /dev/null @@ -1,51 +0,0 @@ -{% extends "aircox/page.html" %} -{% load i18n aircox %} - -{% block title %} -{% if program %} - {% with program.name as program %} - {% blocktrans %}Diffusions of {{ program }}{% endblocktrans %} - {% endwith %} -{% else %} - {% trans "All diffusions" %} - {% endif %} -{% endblock %} - - -{% block content %} -
- {% for object in object_list %} - {% include "aircox/diffusion_item.html" %} - {% endfor %} -
- - -{% if is_paginated %} - -{% endif %} - -{% endblock %} - diff --git a/aircox/templates/aircox/diffusion_timetable.html b/aircox/templates/aircox/diffusion_timetable.html index 24fc2cf..4a30fd5 100644 --- a/aircox/templates/aircox/diffusion_timetable.html +++ b/aircox/templates/aircox/diffusion_timetable.html @@ -34,15 +34,17 @@ {% for day, diffusions in by_date.items %}
- {% for object in diffusions %} + {% for diffusion in diffusions %}
-
- {% include "aircox/diffusion_item.html" %} + {% with diffusion.episode as object %} + {% include "aircox/episode_item.html" %} + {% endwith %}
{% endfor %} diff --git a/aircox/templates/aircox/episode_detail.html b/aircox/templates/aircox/episode_detail.html index c0ee957..3f1fda9 100644 --- a/aircox/templates/aircox/episode_detail.html +++ b/aircox/templates/aircox/episode_detail.html @@ -1,6 +1,33 @@ {% extends "aircox/program_base.html" %} {% load i18n %} +{% block header %} +{{ block.super }} + +
+ {% for diffusion in object.diffusion_set.all %} + {% with diffusion.start as start %} + {% with diffusion.end as end %} + + — + + {% endwith %} + {% endwith %} + + + {% if diffusion.initial %} + {% with diffusion.initial.date as date %} + + ({% trans "rerun" %}) + + {% endwith %} + {% endif %} + +
+ {% endfor %} +
+{% endblock %} + {% block main %} {{ block.super }} diff --git a/aircox/templates/aircox/log_list.html b/aircox/templates/aircox/log_list.html index e73198f..4fa4830 100644 --- a/aircox/templates/aircox/log_list.html +++ b/aircox/templates/aircox/log_list.html @@ -39,7 +39,11 @@ {{ object.start|date:"H:i" }} - {{ object.end|date:"H:i" }} - {% include "aircox/diffusion_item.html" %} + {% with object as diffusion %} + {% with diffusion.episode as object %} + {% include "aircox/episode_item.html" %} + {% endwith %} + {% endwith %} {% else %}