From c46f0063792822ac8453358ab5f42c5ba2025016 Mon Sep 17 00:00:00 2001 From: bkfox Date: Thu, 5 Sep 2019 14:12:12 +0200 Subject: [PATCH] work on website + page becomes concrete --- aircox/README.md | 22 +- .../admin/__pycache__/__init__.cpython-37.pyc | Bin 491 -> 491 bytes .../admin/__pycache__/episode.cpython-37.pyc | Bin 2691 -> 2691 bytes aircox/admin/__pycache__/page.cpython-37.pyc | Bin 1619 -> 1683 bytes .../admin/__pycache__/program.cpython-37.pyc | Bin 2677 -> 2677 bytes aircox/admin/__pycache__/sound.cpython-37.pyc | Bin 2368 -> 2368 bytes aircox/admin/page.py | 2 + aircox/controllers.py | 12 +- aircox/management/commands/diffusions.py | 4 +- aircox/management/commands/sounds_monitor.py | 12 +- aircox/management/commands/streamer.py | 12 +- .../__pycache__/__init__.cpython-37.pyc | Bin 559 -> 559 bytes .../models/__pycache__/episode.cpython-37.pyc | Bin 10433 -> 10052 bytes aircox/models/__pycache__/log.cpython-37.pyc | Bin 9158 -> 8851 bytes aircox/models/__pycache__/page.cpython-37.pyc | Bin 6200 -> 6175 bytes .../models/__pycache__/program.cpython-37.pyc | Bin 16598 -> 16680 bytes .../models/__pycache__/sound.cpython-37.pyc | Bin 9237 -> 8981 bytes .../models/__pycache__/station.cpython-37.pyc | Bin 5120 -> 4691 bytes aircox/models/article.py | 10 +- aircox/models/episode.py | 30 +- aircox/models/log.py | 64 +- aircox/models/page.py | 40 +- aircox/models/program.py | 7 +- aircox/models/sound.py | 31 +- aircox/models/station.py | 53 +- aircox/static/aircox/main.css | 6 + aircox/static/aircox/main.js | 2 +- .../admin/aircox/page_change_form.html | 32 + aircox/templates/aircox/article_detail.html | 29 + aircox/templates/aircox/article_list.html | 12 + aircox/templates/aircox/base.html | 1 + .../templates/aircox/diffusion_timetable.html | 25 +- aircox/templates/aircox/episode_detail.html | 14 +- aircox/templates/aircox/episode_list.html | 10 +- aircox/templates/aircox/log_list.html | 4 +- aircox/templates/aircox/page.html | 2 +- aircox/templates/aircox/page_list.html | 8 +- aircox/templates/aircox/player.html | 2 +- aircox/templates/aircox/program_base.html | 11 +- aircox/templates/aircox/program_detail.html | 36 + aircox/urls.py | 14 +- aircox/views.py | 257 - aircox/views/__init__.py | 2 +- aircox/views/article.py | 32 +- aircox/views/base.py | 25 +- aircox/views/episode.py | 20 +- aircox/views/page.py | 97 +- aircox/views/program.py | 50 +- aircox_web/__init__.py | 0 aircox_web/admin.py | 81 - aircox_web/apps.py | 5 - aircox_web/assets/index.js | 5 - aircox_web/assets/js/index.js | 14 - aircox_web/assets/styles.scss | 75 - aircox_web/assets/vue/index.js | 11 - aircox_web/assets/vue/tab.vue | 31 - aircox_web/assets/vue/tabs.vue | 45 - aircox_web/fields.py | 32 - aircox_web/models.py | 214 - aircox_web/package.json | 29 - aircox_web/plugins/__init__.py | 51 - aircox_web/plugins/image.py | 47 - aircox_web/plugins/richtext.py | 12 - aircox_web/plugins/timetable.py | 37 - aircox_web/renderer.py | 18 - aircox_web/static/aircox_web/assets/main.css | 7201 ----------------- aircox_web/static/aircox_web/assets/main.js | 347 - aircox_web/static/aircox_web/assets/vendor.js | 89 - aircox_web/templates/aircox_web/base.html | 79 - .../templates/aircox_web/diffusion_item.html | 66 - .../templates/aircox_web/diffusion_page.html | 14 - .../templates/aircox_web/diffusions.html | 51 - aircox_web/templates/aircox_web/log_item.html | 19 - aircox_web/templates/aircox_web/logs.html | 53 - aircox_web/templates/aircox_web/page.html | 30 - .../templates/aircox_web/podcast_item.html | 8 - .../templates/aircox_web/program_base.html | 29 - .../templates/aircox_web/program_header.html | 25 - .../templates/aircox_web/program_page.html | 8 - .../templates/aircox_web/timetable.html | 58 - aircox_web/templatetags/aircox_web.py | 32 - aircox_web/tests.py | 3 - aircox_web/urls.py | 29 - aircox_web/views.py | 296 - aircox_web/webpack.config.js | 93 - assets/styles.scss | 7 + assets/vue/tabs.vue | 2 +- instance/sample_settings.py | 63 +- 88 files changed, 476 insertions(+), 9823 deletions(-) create mode 100644 aircox/templates/admin/aircox/page_change_form.html create mode 100644 aircox/templates/aircox/article_detail.html create mode 100644 aircox/templates/aircox/article_list.html delete mode 100755 aircox/views.py delete mode 100644 aircox_web/__init__.py delete mode 100644 aircox_web/admin.py delete mode 100644 aircox_web/apps.py delete mode 100644 aircox_web/assets/index.js delete mode 100644 aircox_web/assets/js/index.js delete mode 100644 aircox_web/assets/styles.scss delete mode 100644 aircox_web/assets/vue/index.js delete mode 100644 aircox_web/assets/vue/tab.vue delete mode 100644 aircox_web/assets/vue/tabs.vue delete mode 100644 aircox_web/fields.py delete mode 100644 aircox_web/models.py delete mode 100644 aircox_web/package.json delete mode 100644 aircox_web/plugins/__init__.py delete mode 100644 aircox_web/plugins/image.py delete mode 100644 aircox_web/plugins/richtext.py delete mode 100644 aircox_web/plugins/timetable.py delete mode 100644 aircox_web/renderer.py delete mode 100644 aircox_web/static/aircox_web/assets/main.css delete mode 100644 aircox_web/static/aircox_web/assets/main.js delete mode 100644 aircox_web/static/aircox_web/assets/vendor.js delete mode 100644 aircox_web/templates/aircox_web/base.html delete mode 100644 aircox_web/templates/aircox_web/diffusion_item.html delete mode 100644 aircox_web/templates/aircox_web/diffusion_page.html delete mode 100644 aircox_web/templates/aircox_web/diffusions.html delete mode 100644 aircox_web/templates/aircox_web/log_item.html delete mode 100644 aircox_web/templates/aircox_web/logs.html delete mode 100644 aircox_web/templates/aircox_web/page.html delete mode 100644 aircox_web/templates/aircox_web/podcast_item.html delete mode 100644 aircox_web/templates/aircox_web/program_base.html delete mode 100644 aircox_web/templates/aircox_web/program_header.html delete mode 100644 aircox_web/templates/aircox_web/program_page.html delete mode 100644 aircox_web/templates/aircox_web/timetable.html delete mode 100644 aircox_web/templatetags/aircox_web.py delete mode 100644 aircox_web/tests.py delete mode 100644 aircox_web/urls.py delete mode 100644 aircox_web/views.py delete mode 100644 aircox_web/webpack.config.js diff --git a/aircox/README.md b/aircox/README.md index 657dcdd..6aeecf5 100755 --- a/aircox/README.md +++ b/aircox/README.md @@ -1,24 +1,10 @@ -# Aircox Programs - -This application defines all base models and basic control of them. We have: -* **Nameable**: generic class used in any class needing to be named. Includes some utility functions; -* **Station**: a station -* **Program**: the program itself; -* **Diffusion**: occurrence of a program planified in the timetable. For rerun, informations are bound to the initial diffusion; -* **Schedule**: describes diffusions frequencies for each program; -* **Track**: track informations in a playlist of a diffusion; -* **Sound**: information about a sound that can be used for podcast or rerun; -* **Log**: logs - +# Aircox +Aircox application aims to provide basis of a radio management system. ## Architecture -A Station is basically an object that represent a radio station. On each station, we use the Program object, that is declined in two different types: -* **Scheduled**: the diffusion is based on a timetable and planified through one Schedule or more; Diffusion object represent the occurrence of these programs; -* **Streamed**: the diffusion is based on random playlist, used to fill gaps between the programs; +A Station contains programs that can be scheduled or streamed. A *Scheduled Program* is a regular show that has planified diffusions of its occurences (episodes). A *Streamed Program* is a program used to play randoms musics between the shows. -Each program has a directory in **AIRCOX_PROGRAMS_DIR**; For each, subdir: -* **archives**: complete episode record, can be used for diffusions or as a podcast -* **excerpts**: excerpt of an episode, or other elements, can be used as a podcast +Each program has a directory on the server where user puts its podcasts (in **AIRCOX_PROGRAM_DIR**). It contains the directories **archives** (complete show's podcasts) and **excerpts** (partial or whatever podcasts). ## manage.py's commands diff --git a/aircox/admin/__pycache__/__init__.cpython-37.pyc b/aircox/admin/__pycache__/__init__.cpython-37.pyc index 8a13be3e971486e225e6080a40c369e401bb98ee..2e1c2039f5d5357db09ebef2d87cba2d51159d46 100644 GIT binary patch delta 20 acmaFO{F<5DiI4aEeF^LljqvXfT7O*yLAC{wh^Ui7C06 zdHRW&MalUU`UQ#Usqx7fiFrUOEx#yNFQX(kXR{?UDM uQgaJ(5=&AiPh~OW7XvzzgOLRcIha@`UuG$0)Sc|kn!uPV}iIPV}iIgbU=vPiIgbU=vPiI now: break # log track on air - self.log(type=Log.Type.on_air, date=pos, source=log.source, + self.log(type=Log.TYPE_ON_AIR, date=pos, source=log.source, track=track, comment=track) def handle_diffusions(self): @@ -208,7 +208,7 @@ class Monitor: # now = tz.now() diff = Diffusion.objects.station(self.station).on_air().now(now) \ - .filter(episode__sound__type=Sound.Type.archive) \ + .filter(episode__sound__type=Sound.TYPE_ARCHIVE) \ .first() # Can't use delay: diffusion may start later than its assigned start. log = None if not diff else self.logs.start().filter(diffusion=diff) @@ -228,13 +228,13 @@ class Monitor: def start_diff(self, source, diff): playlist = Sound.objects.episode(id=diff.episode_id).paths() source.append(*playlist) - self.log(type=Log.Type.start, source=source.id, diffusion=diff, + self.log(type=Log.TYPE_START, source=source.id, diffusion=diff, comment=str(diff)) def cancel_diff(self, source, diff): - diff.type = Diffusion.Type.cancel + diff.type = Diffusion.TYPE_CANCEL diff.save() - self.log(type=Log.Type.cancel, source=source.id, diffusion=diff, + self.log(type=Log.TYPE_CANCEL, source=source.id, diffusion=diff, comment=str(diff)) def sync(self): diff --git a/aircox/models/__pycache__/__init__.cpython-37.pyc b/aircox/models/__pycache__/__init__.cpython-37.pyc index 2b2f32353c72ad6e0cf360507758c4b49586fb64..82b22fac2dfcc986b26e834236aa1b2a2e9621c5 100644 GIT binary patch delta 20 acmZ3_vYv(8iIO diff --git a/aircox/models/__pycache__/episode.cpython-37.pyc b/aircox/models/__pycache__/episode.cpython-37.pyc index c2f04696213cd3680121efcc3ec605cd68b6f3ca..5c71c93fec2304f9e02f86b8258ab3bcbea8e175 100644 GIT binary patch delta 3744 zcmbVPO>7)V74GWp>G}8kIUbK=JN}J5*+czG>OG^){G8q{}x#D_p(!9b#cL=z^1dPlm)@Yhru?^(T=W#L53K_zFm;#4oI4uf2F)jxmy{ z^KH(&#nvUZ$}u@)yf%HkHa#=T_rpl-2T`1iAG$PvV>}&47iGHcyFt)o;hi?+xQEO- zj*``x?>kO(Bs|xaOBhFvw@?<}jXx$uaX9g41Vh4)1ChE%cXYT~jPjG>Pl+>`F5uqN zmb8#8>0&f_@VLQGpk@Iz(+gku;;eIZ-kF}e2Ae69Dxiv}>$BP|$MyK@;v31I%%G-B zP%j9*Ry~x7Hm8g`w^yrXr29I~$COm+cZ1zt;dD)`rar!T2DqvPrr?f3ugN&x1zc5| zx~q|Y1_<~xF59#{i-L1VYDivmljH21ct8E46K8RD1_^GbCN>jDZ4#MyQCsS6cy*(Z zNs_mfV{x7URhdKYoHlnTYq%Xh+MOIS#@*H;^Tp3|r^a?=_Hi_F8p(F> z_ytkOmnu6EQyk_7F9LZ@T*!~F#el=VV3KGW(`-UDTc^Y}nE533RI@Fr+i_~x32NF& z7H26MTPF>jC4jROmjo`3T*I)_EVGnlT|jdnO{^1|{J^mDFfPD2wNAi7nx;WkWSJ)= zmRh5kb@G(h-O-R{)o9-ihOX;&51?n^p=Or>Bunt^rQLKNd@HPv_R#V}ZJpTrf$yai z#rLxT+DG>*eh}pSbU^V3=pa1+dP8)G4#Rhtj?h!1z>LsW=r}$2kl3Sif=+^Dj8^F( z_>R*t``{z3LFh~L<%bD-;*n-*_9US56fbOiSonv2n?u|*o&@q4qRRt#c0=&&9%NhX zaH!sHH9X#A)K1pj7NP{B>gmIq?L)njK;YVum+{SPmGSzWwpV8X$3)yJ1 zEbO{;g;&Z>KN?7{Fy8cnz-zajf5AJ~J^-sef?r?(K|oLF0O@A=(b_Nwlw;~CJctcV zzWenmGNbHldi_)J?aIQT%c!Q__}*J5XotHY1oxMsx36_XAq`w>5IP7q|3g#7 z^w%37_oYYyK}aT5{1kki5r5dMxL26>@zI-BUg~!2K7{k1RWY z9e)&;2VU(Tgr!(S;76AAc0YXeCF6#?+(_wMeEGS?DNfpo)i`QFU$lTJ7T8%~_eg=lUInTLyd*7_AzdW1@#%rXq=*j{VyD#6OWqI%htHnJ*h9+^2McZ(a%nEW zE>%P7Q%9uPa+^%XlrLMV+lllnbs9ITx0@?x|0TXZTwAzt^QA@g>&5($dAKuG2fdb~ z>c%s1Y4nF{U%)NnkRXn;7*=&Vsa`P-I8MQJ=_||Cq-rzx=RopYRo~uM#@YS4-+>YWbsZA27Rv{1_lv#$8JsEhxIbKEtN*Vz?DdU6%Fn>O0Ir~)t7h7WJPiO z?(O5atFZ}^sHFr})PE)%h^7}pQ?~6iZ{eaZM_MY&$5F6%3;7;QmBgpxlWW!8O*0@E zZP&pvR#AQoiHZ}xAv>=U_{Mzg>iqe+YgcAx?5I_%P0!b6FDrQ5O!C^rt8=y4>#8T^ zN6-p3iu@=NJc%q^Z@Rv}HrEQ-BID;h=2P`pWp)PeW&xV7NVapp0o1*LB5W@7BzfNJt&k}g3v)1IQYqqaQ1J)VmGzr(SU zl$lL&C-nS)n;_z6#a|`{Rp+AODa+3a4fMBVD3xJC*p-A11x^g@#e`7liQA98bGI1JQ@A&L2fl+{!IiY9B T=077(diA`KGKyw?B!A-HrPd%a delta 4075 zcmbVPU2Ggl5uTo%o&EoFy#8JP|HSJ=PI4sNab5i;!Y^)7o-!&5f3L2@PGwKh=)TuA;AOU0ST#JPM|z+61tNR2|wL`hd^SEX^%Ptp3Ydljdn*L!$-O$GX-HYv8g*+h_(P8CF`4 zScAYHqB-DmtOxiZ;M-{y_$=!NewdOQTDkZEPHetRWb8^Un5#8BAWg4Mnd_HzNiIop z9e&M=^;*RVq5TZdfa2ZEgImT1s&(V1@Do&V($BtnCqX?7O_z8n2Kzdv}G@m>D z?0k6y8f5ZPowMq4?J8U2Sro)B&fK^-GdsrzfK%h~PVpCW^6C)sI6ICm%2dU5eBWci ztvcm6hRoQul2w@N+IBc4oY#&^7)Oq)&@FDp9+M97QS8w$hJ;T8NWDs1eYjFa`5E!; z_-C?tpdM%|T0mCxm5BILeEgKb&tP9%b!jZDtukrWYqnE`4Wa_Q-`IIjoDpXeKcB^Z zGES?0P_0!08LxB7xV^YmHo}eCg$vmFSp*!8W5f0aQiFshp6xAl zC%C>fo=%XP%CQ(vLob;_@9a9aDO++HZoof>23z)36qSqN1`X{W;GK**O9A6AZ@rl* zkT32Tf`1+@{@-TJMYaT+a8Z1eedWY8H1!35o!R0zGQw*6H;0UJr?$*$;z4furF}aa z_YFUfup2yn71>?u{5rC&__dax0~)49EI-yXfxOhVr7BP0dKAo7GKHJ7>#deKovIIo@lcgXeB`F z5KC;fgI02#JRw#QxhAkF#dg3z=`g#l>sBYG;SJ5|3Ujd44Vm{4@I9;-a%^z}Oj;$N zJ7}k(`&d8iqTPxf0C^AXRrDY&(LU%mMEmIg{0`GWIt0HX^avfHqZ`MFHA=_mIEcpR zVLAc7<8;_M{773Ov`i;A;?~3?&Csker1GP@xWx*8)5Y&dM-x|LZ@j2(88l)=sR5eN z``Ov*(o(~()@xIEY&k?%;~9VtFnND6F{_odQjJ=rUR$bik5Oq>oEoH=0~hdI6RbRf z&&LIf69fMFIUrSQtlT32;mLWBm@!MptR_V=`Ed$VjyoAlrl_%Gs*xR3PU|ed@R*_as@{<1C)_kCSIy z$Uv34w|Z9KxHx%}+;4A1r=>r*5bkAY(T4 z& z?i>kgl8VA^C}djM+4lMu;zHNLBt~7u|KNfHWud{H0D}9jc(pcdq=0!^CRF@Xg_#j+J*93H{%1YpHfa8K>&HC>atb@~6$ItuZJ_>(kD+B7 z((50@-+NE>4}vPzS@@vR-q!2=Kt)zm!rL^(TPCQZ+Zr=>}9QsnwZ;wvUK-qKfM;)TBNl8pFcUw;#ir8HKZ;MM}%^Nc0e z3CeL9_3I5N0C!X)o*J24W`V7wepqPFp(iy|NZobZ2E&2 zmPXC-n2ai4cDnBFCT@YsKgG4diwgnTREA$g%0rj~kkM+uyl`?mcZYi*x6E+XR>ZE> z)LeF~s|&=)Ma8< zC?h@#cAWe=+S}tKs^%)USmk!hN>2Q8j8_K!X#^D}9VxXRv99ow2zc$~rw|4Z z&}CV;;W@4gFCVtd_@ydysj74`W!oO}78&=!_Cr+1Q_+-}Sx6fTaLo*LTk{!09R~r6 z0AB`>@l{@j^d7A7TPRbHgd|cnf{U<!J;jvJu-{UEiV|ossJNOLZIfOhy0pU{!Z3u?|uqeUO!SdicGhAG?2{0UcHm%ye e7$+G&19NJ)>?{G!@5jO75Ti87le{=~xcDDY7h^&I diff --git a/aircox/models/__pycache__/log.cpython-37.pyc b/aircox/models/__pycache__/log.cpython-37.pyc index 1d10ae4809c08ad0ba14a59d2280f6f099b68050..2b34c89f6b7413f919067e4f49e410449ec1c3c3 100644 GIT binary patch delta 3459 zcma(TOKcR$b*euzJu^LH&tUw=7-PVL&36|z0*l}^1}uop+6HNxkWMpI9=GY99;(}b z(FQAmNV_XJN>n-RY9%(eT#|!xixer+CWjoNxWvWLAS?^PSyu&fkY z^ZM2M)qAgAy;py@B&N!5mP&;T{FT0J-TGwcr{(UoU&4xJC-9~PXV!n1)LmYfpcIcf#vu+3>xcs^(kD^4Y>I#snU1iQix zr-Nje_H;KbetdyAoz$kKhURo#%Fr?`J;~4#Gncy`>rM|)+Ncc_8z{X%>7&|wrq=!p zW2g~nQg(NR%?O)}6o|B9mLz_&A*RS@q%3M$Xt#l!)O`Aj4DS#>*Up(p=o0_XZjMyY zgvP5VVNfq&yaUh_>_(!QMATdD6HEGshjs|_&Mh&ekji?CU|>KTHcnkH1OJ`OawZ|m z8YSxl9M)#QStD6(v6{}&7BArr#f7)8O}Vo(?!@$5jVM=Mb|M=!UC-zH#jlOm=f1%* z?+0Tq7%nKo^9vJm3*TVZj1!M12gMoFJGLV@nY-^NORk^1>_=YFslT{b6Mrk{NAaUc?dWl-a~8bh-&cik>erKc6t>P?6mI^(38kj-qo5}BAZoyY z<-8=}u1`~4Jm;_S;|Qbqvx#C8YsQoCF=N3Zz}R3y!$a z?PXR^;_;226>gB?8I(ASU|jsCIC=I6ps=+L!NG~1K*k#YoGhyP6w3&(c-tkJ+cfHn z*naog4tr=x=Pwb6sywKB3{=?%H@}VeRxv|v0zXc3(-c=qmx^P^KY-w%_^33n_ANv$ z1bGD5QEK_Le7DbQ0$Fb&I0yienI4t8YaTz1jLn>@?av`WrOFN>k3K)yIUvVa-xq!M&-h5Y8RpchV*3@eHPI8xdQIU z&!a5-Vhuo+JRB2WRJ+B^N{5h@()H~?Fa^>Iy*rHhJmy(g@(~0oK^P{VM4*z;g_yeL zrW>-vlLai;ba#G1j8{i)Ved(Wv8PCLG2(UR7r0wkLfkHh^Fd!4QJ|2!;Vj8`?qQ*TK)Fh^FDFL!L&7?;=o{-3BUe zWniG^aW?>q6bwtZO!(;q^=}u%Z+G=-O(MEG2iF{n87xn8kIACOOu#IKWdX}8EC*OY zVHRLT#1>7bz>3Q@D*-G6)qYF>mL3yYUN@XJpti%h4X6d$zOFeHpj3fU0m`oB4qBzV z)^%q$P}e#sE|Ixg8R-NgUF)XP1Gc7xe?9B;DPI2z zygi$|J&HH*0&f6$H$ZVO8+^L&x$LE?vucZx51`UhX7SWrSP zPX51((cVYd8tyzQe%0G~G(>a-!C?T2ltuyckd6!w%pq6tx|_ZkDa`} zvsC7M%{7LY8USf z441Ka$n0$fU5fFy#cu`%Zf+Tp+0?6AHg)(k%9`jvEX_K+0uugEkyQOWZ(V9D)wgg_YQkn(@CjUf)|IM^p*?2sL%cwPSAy;8@17=~(KGfy;66{$T&%yFlcJ06c%& zk#rhwha}Aw_nS$d`14?GZIs_ah3_G70Z2Gi8r5Cb$#1@)X0P$R$VeY+lM^$OQ&;&O zpg8F(Yj)xC)SM))w3Wi-<=N@UsdfFxH>3ONPRjh`lD8?X3RQ)SS4vv*Qw#3Q^{ZFY zCxyC8NGRJl$bMSg< zz`I>NF=U=Dd(B34l-^ErXm74p5+807b~WB3J|7xbGZ7UCYy>!sWsbG{IHD{KSiP$- zG3tquPFi&gc^Ji4VN-~HU8%V)oP+C1>#Y^WSLa#6Ri4c6fUQ6U`L*azqM|h(Msy`$ d=dc5-cZyBaJB8T9*5OCm;ah-_SY69l{{#Kb4VwS} delta 3866 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 YeQ{U4+-lQpi(Mfg+G8S-8TaYhUuXxMVE_OC delta 1936 zcmb7ENo*WN6z!_+>DfGsXY<&O?XkyO+re=nve=*mZ#W6D0~&{*hGZB|mE)O=msGa_ zM~-~3g)gy$Rd7KF9L^w+ECdo4ZrniP*e4DM$+>Vth!g*pMZkBR`7gGlonjM-;nW%epry} zzSE=sX-)J1#WF-Me3Tv0hsJ`dYxA{=v&h+zAqx!!&5<+z*O1Q@C1>%-$N(Do$UJMJ zH8CKsX0r03X>Hyln#MJoFwLfn*!r}_DAOS@IyWFJK5kb1%wWDU=R`&o2%)3j4OU0;aR z7vj5yo#9PDZLDh&U?dByX5Lb5Wl3=67|hy$kpe~rwI`KhAntpOT;BL#&@!N^WZ@u8#A61eFQE8s8(Fl(6tcZ+6(76hy}ag<$@+8+n`cE+E^;(8v2 zV*&(-0k7zj-v=kn)J=HPxXPsq6ZIy6rvWiq^%kHQLG z88G0|xQ&+ynYDD);;OAhzZ?s=>*cNbTfz@v%RWly~0;uMNlqUsht1%iWiu}9**29koW2_sAKo1hf+fN>fH zDGJ)C9-sz|$`9i02f2igm{_vnoE%IR%qof(EM&4|N7gbOBror0 srpQhCOXi0*W%%fa#bT*egM>o2F0EE-T+GU^np)&fO?{+W{@K*`4{YU^&j0`b diff --git a/aircox/models/__pycache__/program.cpython-37.pyc b/aircox/models/__pycache__/program.cpython-37.pyc index 90e1f0bba3eb843b23a6e6158566462699cdd9fa..d14001528d30b53db0f5b709ab8a834457808c7b 100644 GIT binary patch delta 3573 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 G`tV<2L8S2j delta 2260 zcmZ`)OOMn>5O%lU9?x@!eFkb#-<3SJlQc6;Kse z4pcdzlAS(A8&a*fjznuD)6aC0>CRI4;K?ImrI*QX#7-4@m{iga=vh)u@6toD)?nL^R_VXw%2ZlB@Gqn=mjTSVDGo>B*9d4vg^zr>8VjA}r zgJ#R)f`BPh(OlbWB{45TkAs88I4PwyYwgHqke<|0Hnd9mg{mE zd*xXBUoDKDUm{toIMdmf!FNx^n#3*{$sp%7NeF~&Daf_o#uka z`9YjBj({Yxyz91tW*oP?Ab@Q*qXOO^Mk7HN{TfMcnA2SiBz)qh0G{L!CP9V~M~0)P zOopSEJq5Bug}%a6=&Pcyd3xLMOwVH43IRFC^flSBLCS;9SRt5ctCFX-3*Q%ikftQi zuUyU95jq za{BK}^Mnns!BxWTm3yI`o? zF3d(esZaSF&fSkX9F5iyF4k1O>vXta}sS1Ae;UMQyh({j?Og4V7g;61NfP@O~LQVtwm2z)brk`R z_hQZ3&N)43k90HLZ+Wd|oMg(iBw5TP`a0V*m@%vUGLuGkS;Ip^3CiS4cvZ$qJdL59kf0(Yw|) zn#q1A5qG90XS9AOBVT6Z);6XX=jYb3z>kww6fU0NxB&&wq}xRit*x;d0smXqA_xJv zoPc955ya>Tey@Qb_*J*_Ti(Hq@Uvi7lleJl_<4j22%9&rcjYdE?hE(?DXK~7H}>9* zD)*~>H#drN#t`JY?3ROPo(aR^|e{)n)MB827YVOlTV)CAet^rzy{F|5$9<|As{ zcd1$$?GA6xXWZaj1U&7Y%feC2wal7{xaTi)&Uv>u7Cn(dnLc@{F?nigTI6h2ZiLMs zfIpcB-4F42XS ng*3N3=t;;wGmeE3TME1rc&njoN|H?sY7=-Vyg;;)s~q_c(x1ae diff --git a/aircox/models/article.py b/aircox/models/article.py index e99f30a..c631632 100644 --- a/aircox/models/article.py +++ b/aircox/models/article.py @@ -1,11 +1,17 @@ from django.db import models from django.utils.translation import ugettext_lazy as _ -from .page import Page +from .page import Page, PageQuerySet from .program import Program, InProgramQuerySet +class ArticleQuerySet(InProgramQuerySet, PageQuerySet): + pass + + class Article(Page): + detail_url_name = 'article-detail' + program = models.ForeignKey( Program, models.SET_NULL, verbose_name=_('program'), blank=True, null=True, @@ -17,7 +23,7 @@ class Article(Page): 'instead of a blog article'), ) - objects = InProgramQuerySet.as_manager() + objects = ArticleQuerySet.as_manager() class Meta: verbose_name = _('Article') diff --git a/aircox/models/episode.py b/aircox/models/episode.py index 0182282..8826611 100644 --- a/aircox/models/episode.py +++ b/aircox/models/episode.py @@ -1,9 +1,7 @@ import datetime -from enum import IntEnum from django.db import models -from django.db.models import F, Q -from django.db.models.functions import Concat, Substr +from django.db.models import Q from django.utils import timezone as tz from django.utils.translation import ugettext_lazy as _ from django.utils.functional import cached_property @@ -64,7 +62,7 @@ class DiffusionQuerySet(BaseRerunQuerySet): def on_air(self): """ On air diffusions """ - return self.filter(type=Diffusion.Type.on_air) + return self.filter(type=Diffusion.TYPE_ON_AIR) def now(self, now=None, order=True): """ Diffusions occuring now """ @@ -132,20 +130,20 @@ class Diffusion(BaseRerun): """ objects = DiffusionQuerySet.as_manager() - class Type(IntEnum): - on_air = 0x00 - unconfirmed = 0x01 - cancel = 0x02 + TYPE_ON_AIR = 0x00 + TYPE_UNCONFIRMED = 0x01 + TYPE_CANCEL = 0x02 + TYPE_CHOICES = ( + (TYPE_ON_AIR, _('on air')), + (TYPE_UNCONFIRMED, _('not confirmed')), + (TYPE_CANCEL, _('cancelled')), + ) episode = models.ForeignKey( - Episode, models.CASCADE, - verbose_name=_('episode'), + Episode, models.CASCADE, verbose_name=_('episode'), ) type = models.SmallIntegerField( - verbose_name=_('type'), - default=Type.on_air, - choices=[(int(y), _(x.replace('_', ' '))) - for x, y in Type.__members__.items()], + verbose_name=_('type'), default=TYPE_ON_AIR, choices=TYPE_CHOICES, ) start = models.DateTimeField(_('start')) end = models.DateTimeField(_('end')) @@ -222,7 +220,7 @@ class Diffusion(BaseRerun): # TODO: property? def is_live(self): """ True if Diffusion is live (False if there are sounds files). """ - return self.type == self.Type.on_air and \ + return self.type == self.TYPE_ON_AIR and \ not self.episode.sound_set.archive().count() def get_playlist(self, **types): @@ -232,7 +230,7 @@ class Diffusion(BaseRerun): """ from .sound import Sound return list(self.get_sounds(**types) - .filter(path__isnull=False, type=Sound.Type.archive) + .filter(path__isnull=False, type=Sound.TYPE_ARCHIVE) .values_list('path', flat=True)) def get_sounds(self, **types): diff --git a/aircox/models/log.py b/aircox/models/log.py index d4c8897..53097bf 100644 --- a/aircox/models/log.py +++ b/aircox/models/log.py @@ -1,6 +1,4 @@ from collections import deque -import datetime -from enum import IntEnum import logging import os @@ -9,7 +7,7 @@ from django.utils import timezone as tz from django.utils.translation import ugettext_lazy as _ -from aircox import settings, utils +from aircox import settings from .episode import Diffusion from .sound import Sound, Track from .station import Station @@ -35,10 +33,10 @@ class LogQuerySet(models.QuerySet): self.filter(date__date__gte=date) def on_air(self): - return self.filter(type=Log.Type.on_air) + return self.filter(type=Log.TYPE_ON_AIR) def start(self): - return self.filter(type=Log.Type.start) + return self.filter(type=Log.TYPE_START) def with_diff(self, with_it=True): return self.filter(diffusion__isnull=not with_it) @@ -163,43 +161,33 @@ class Log(models.Model): This only remember what has been played on the outputs, not on each source; Source designate here which source is responsible of that. """ - class Type(IntEnum): - stop = 0x00 - """ - Source has been stopped, e.g. manually - """ - # Rule: \/ diffusion != null \/ sound != null - start = 0x01 - """ Diffusion or sound has been request to be played. """ - cancel = 0x02 - """ Diffusion has been canceled. """ - # Rule: \/ sound != null /\ track == null - # \/ sound == null /\ track != null - # \/ sound == null /\ track == null /\ comment = sound_path - on_air = 0x03 - """ - The sound or diffusion has been detected occurring on air. Can - also designate live diffusion, although Liquidsoap did not play - them since they don't have an attached sound archive. - """ - other = 0x04 - """ Other log """ + + TYPE_STOP = 0x00 + """ Source has been stopped, e.g. manually """ + # Rule: \/ diffusion != null \/ sound != null + TYPE_START = 0x01 + """ Diffusion or sound has been request to be played. """ + TYPE_CANCEL = 0x02 + """ Diffusion has been canceled. """ + # Rule: \/ sound != null /\ track == null + # \/ sound == null /\ track != null + # \/ sound == null /\ track == null /\ comment = sound_path + TYPE_ON_AIR = 0x03 + """ Sound or diffusion occured on air """ + TYPE_OTHER = 0x04 + """ Other log """ + TYPE_CHOICES = ( + (TYPE_STOP, _('stop')), (TYPE_START, _('start')), + (TYPE_CANCEL, _('cancelled')), (TYPE_ON_AIR, _('on air')), + (TYPE_OTHER, _('other')) + ) station = models.ForeignKey( Station, models.CASCADE, - verbose_name=_('station'), - help_text=_('related station'), - ) - type = models.SmallIntegerField( - choices=[(int(y), _(x.replace('_', ' '))) - for x, y in Type.__members__.items()], - blank=True, null=True, - verbose_name=_('type'), - ) - date = models.DateTimeField( - default=tz.now, db_index=True, - verbose_name=_('date'), + verbose_name=_('station'), help_text=_('related station'), ) + type = models.SmallIntegerField(_('type'), choices=TYPE_CHOICES) + date = models.DateTimeField(_('date'), default=tz.now, db_index=True) source = models.CharField( # we use a CharField to avoid loosing logs information if the # source is removed diff --git a/aircox/models/page.py b/aircox/models/page.py index 8fc61e7..990fdeb 100644 --- a/aircox/models/page.py +++ b/aircox/models/page.py @@ -38,28 +38,30 @@ class Category(models.Model): class PageQuerySet(InheritanceQuerySet): def draft(self): - return self.filter(status=Page.STATUS.draft) + return self.filter(status=Page.STATUS_DRAFT) def published(self): - return self.filter(status=Page.STATUS.published) + return self.filter(status=Page.STATUS_PUBLISHED) def trash(self): - return self.filter(status=Page.STATUS.trash) + return self.filter(status=Page.STATUS_TRASH) class Page(models.Model): """ Base class for publishable content """ - class STATUS(IntEnum): - draft = 0x00 - published = 0x10 - trash = 0x20 + STATUS_DRAFT = 0x00 + STATUS_PUBLISHED = 0x10 + STATUS_TRASH = 0x20 + STATUS_CHOICES = ( + (STATUS_DRAFT, _('draft')), + (STATUS_PUBLISHED, _('published')), + (STATUS_TRASH, _('trash')), + ) title = models.CharField(max_length=128) slug = models.SlugField(_('slug'), blank=True, unique=True) status = models.PositiveSmallIntegerField( - _('status'), - default=STATUS.draft, - choices=[(int(y), _(x)) for x, y in STATUS.__members__.items()], + _('status'), default=STATUS_DRAFT, choices=STATUS_CHOICES, ) category = models.ForeignKey( Category, models.SET_NULL, @@ -84,8 +86,6 @@ class Page(models.Model): detail_url_name = None - class Meta: - abstract = True def __str__(self): return '{}: {}'.format(self._meta.verbose_name, @@ -104,15 +104,15 @@ class Page(models.Model): @property def is_draft(self): - return self.status == self.STATUS.draft + return self.status == self.STATUS_DRAFT @property def is_published(self): - return self.status == self.STATUS.published + return self.status == self.STATUS_PUBLISHED @property def is_trash(self): - return self.status == self.STATUS.trash + return self.status == self.STATUS_TRASH @cached_property def headline(self): @@ -132,6 +132,16 @@ class Page(models.Model): return cls(**cls.get_init_kwargs_from(page, **kwargs)) +class Comment(models.Model): + page = models.ForeignKey( + Page, models.CASCADE, verbose_name=_('related page'), + ) + nickname = models.CharField(_('nickname'), max_length=32) + email = models.EmailField(_('email'), max_length=32) + date = models.DateTimeField(auto_now_add=True) + content = models.TextField(_('content'), max_length=1024) + + class NavItem(models.Model): """ Navigation menu items """ station = models.ForeignKey( diff --git a/aircox/models/program.py b/aircox/models/program.py index dba587d..3e3b777 100644 --- a/aircox/models/program.py +++ b/aircox/models/program.py @@ -45,6 +45,11 @@ class Program(Page): Renaming a Program rename the corresponding directory to matches the new name if it does not exists. """ + # explicit foreign key in order to avoid related name clashes + page = models.OneToOneField( + Page, models.CASCADE, + parent_link=True, related_name='program_page' + ) station = models.ForeignKey( Station, verbose_name=_('station'), @@ -478,7 +483,7 @@ class Schedule(BaseRerun): initial = diffusions[initial] diffusions[date] = Diffusion( - episode=episode, type=Diffusion.Type.on_air, + episode=episode, type=Diffusion.TYPE_ON_AIR, initial=initial, start=date, end=date+duration ) return episodes.values(), diffusions.values() diff --git a/aircox/models/sound.py b/aircox/models/sound.py index f262dcb..a7c9178 100644 --- a/aircox/models/sound.py +++ b/aircox/models/sound.py @@ -36,7 +36,7 @@ class SoundQuerySet(models.QuerySet): def archive(self): """ Return sounds that are archives """ - return self.filter(type=Sound.Type.archive) + return self.filter(type=Sound.TYPE_ARCHIVE) def paths(self, archive=True, order_by=True): """ @@ -55,11 +55,14 @@ class Sound(models.Model): A Sound is the representation of a sound file that can be either an excerpt or a complete archive of the related diffusion. """ - class Type(IntEnum): - other = 0x00, - archive = 0x01, - excerpt = 0x02, - removed = 0x03, + TYPE_OTHER = 0x00 + TYPE_ARCHIVE = 0x01 + TYPE_EXCERPT = 0x02 + TYPE_REMOVED = 0x03 + TYPE_CHOICES = ( + (TYPE_OTHER, _('other')), (TYPE_ARCHIVE, _('archive')), + (TYPE_EXCERPT, _('excerpt')), (TYPE_REMOVED, _('removed')) + ) name = models.CharField(_('name'), max_length=64) program = models.ForeignKey( @@ -72,11 +75,7 @@ class Sound(models.Model): Episode, models.SET_NULL, blank=True, null=True, verbose_name=_('episode'), ) - type = models.SmallIntegerField( - verbose_name=_('type'), - choices=[(int(y), _(x)) for x, y in Type.__members__.items()], - blank=True, null=True - ) + type = models.SmallIntegerField(_('type'), choices=TYPE_CHOICES) # FIXME: url() does not use the same directory than here # should we use FileField for more reliability? path = models.FilePathField( @@ -196,21 +195,21 @@ class Sound(models.Model): """ if not self.file_exists(): - if self.type == self.Type.removed: + if self.type == self.TYPE_REMOVED: return logger.info('sound %s: has been removed', self.path) - self.type = self.Type.removed + self.type = self.TYPE_REMOVED return True # not anymore removed changed = False - if self.type == self.Type.removed and self.program: + if self.type == self.TYPE_REMOVED and self.program: changed = True - self.type = self.Type.archive \ + self.type = self.TYPE_ARCHIVE \ if self.path.startswith(self.program.archives_path) else \ - self.Type.excerpt + self.TYPE_EXCERPT # check mtime -> reset quality if changed (assume file changed) mtime = self.get_mtime() diff --git a/aircox/models/station.py b/aircox/models/station.py index 751cdf1..691a439 100644 --- a/aircox/models/station.py +++ b/aircox/models/station.py @@ -1,4 +1,3 @@ -from enum import IntEnum import os from django.db import models @@ -91,36 +90,32 @@ class Port(models.Model): Some port types may be not available depending on the direction of the port. """ - class Direction(IntEnum): - input = 0x00 - output = 0x01 + DIRECTION_INPUT = 0x00 + DIRECTION_OUTPUT = 0x01 + DIRECTION_CHOICES = ((DIRECTION_INPUT, _('input')), + (DIRECTION_OUTPUT, _('output'))) - class Type(IntEnum): - jack = 0x00 - alsa = 0x01 - pulseaudio = 0x02 - icecast = 0x03 - http = 0x04 - https = 0x05 - file = 0x06 + TYPE_JACK = 0x00 + TYPE_ALSA = 0x01 + TYPE_PULSEAUDIO = 0x02 + TYPE_ICECAST = 0x03 + TYPE_HTTP = 0x04 + TYPE_HTTPS = 0x05 + TYPE_FILE = 0x06 + TYPE_CHOICES = ( + (TYPE_JACK, 'jack'), (TYPE_ALSA, 'alsa'), + (TYPE_PULSEAUDIO, 'pulseaudio'), (TYPE_ICECAST, 'icecast'), + (TYPE_HTTP, 'http'), (TYPE_HTTPS, 'https'), + (TYPE_FILE, _('file')) + ) station = models.ForeignKey( - Station, - verbose_name=_('station'), - on_delete=models.CASCADE, - ) + Station, models.CASCADE, verbose_name=_('station')) direction = models.SmallIntegerField( - _('direction'), - choices=[(int(y), _(x)) for x, y in Direction.__members__.items()], - ) - type = models.SmallIntegerField( - _('type'), - # we don't translate the names since it is project names. - choices=[(int(y), x) for x, y in Type.__members__.items()], - ) + _('direction'), choices=DIRECTION_CHOICES) + type = models.SmallIntegerField(_('type'), choices=TYPE_CHOICES) active = models.BooleanField( - _('active'), - default=True, + _('active'), default=True, help_text=_('this port is active') ) settings = models.TextField( @@ -136,13 +131,13 @@ class Port(models.Model): Return True if the type is available for the given direction. """ - if self.direction == self.Direction.input: + if self.direction == self.DIRECTION_INPUT: return self.type not in ( - self.Type.icecast, self.Type.file + self.TYPE_ICECAST, self.TYPE_FILE ) return self.type not in ( - self.Type.http, self.Type.https + self.TYPE_HTTP, self.TYPE_HTTPS ) def save(self, *args, **kwargs): diff --git a/aircox/static/aircox/main.css b/aircox/static/aircox/main.css index 7d8416f..75b6975 100644 --- a/aircox/static/aircox/main.css +++ b/aircox/static/aircox/main.css @@ -7159,12 +7159,18 @@ label.panel-block { .is-borderless { border: none; } +.has-background-transparent { + background-color: transparent; } + .navbar + .container { margin-top: 1em; } .navbar.has-shadow, .navbar.is-fixed-bottom.has-shadow { box-shadow: 0em 0em 1em rgba(0, 0, 0, 0.1); } +a.navbar-item.is-active { + border-bottom: 1px grey solid; } + /* .navbar-brand img { min-height: 6em; diff --git a/aircox/static/aircox/main.js b/aircox/static/aircox/main.js index eef5775..f2ca7fc 100644 --- a/aircox/static/aircox/main.js +++ b/aircox/static/aircox/main.js @@ -419,7 +419,7 @@ eval("/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; -eval("/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"a\", function() { return render; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"b\", function() { return staticRenderFns; });\nvar render = function() {\n var _vm = this\n var _h = _vm.$createElement\n var _c = _vm._self._c || _h\n return _c(\n \"div\",\n [\n _c(\"div\", { staticClass: \"tabs is-centered\" }, [\n _c(\"ul\", [_vm._t(\"tabs\", null, { value: _vm.value })], 2)\n ]),\n _vm._v(\" \"),\n _vm._t(\"default\", null, { value: _vm.value })\n ],\n 2\n )\n}\nvar staticRenderFns = []\nrender._withStripped = true\n\n\n\n//# sourceURL=webpack:///./assets/vue/tabs.vue?./node_modules/vue-loader/lib/loaders/templateLoader.js??vue-loader-options!./node_modules/vue-loader/lib??vue-loader-options"); +eval("/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"a\", function() { return render; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"b\", function() { return staticRenderFns; });\nvar render = function() {\n var _vm = this\n var _h = _vm.$createElement\n var _c = _vm._self._c || _h\n return _c(\n \"div\",\n [\n _c(\"div\", { staticClass: \"tabs is-centered is-medium\" }, [\n _c(\"ul\", [_vm._t(\"tabs\", null, { value: _vm.value })], 2)\n ]),\n _vm._v(\" \"),\n _vm._t(\"default\", null, { value: _vm.value })\n ],\n 2\n )\n}\nvar staticRenderFns = []\nrender._withStripped = true\n\n\n\n//# sourceURL=webpack:///./assets/vue/tabs.vue?./node_modules/vue-loader/lib/loaders/templateLoader.js??vue-loader-options!./node_modules/vue-loader/lib??vue-loader-options"); /***/ }) diff --git a/aircox/templates/admin/aircox/page_change_form.html b/aircox/templates/admin/aircox/page_change_form.html new file mode 100644 index 0000000..9aac820 --- /dev/null +++ b/aircox/templates/admin/aircox/page_change_form.html @@ -0,0 +1,32 @@ +{% extends "admin/change_form.html" %} +{% load i18n static %} + +{% block extrahead %}{{ block.super }} + + +{% endblock %} + +{% block submit_buttons_bottom %} +{% if has_change_permission %} +
+
+ {% if original and not original.is_trash %} + + {% endif %} + {% if original and not original.is_draft %} + + {% endif %} +
+ +
+ + + {% if not original.is_published %} + + {% endif %} +
+ +{% endif %} +
+{% endblock %} + diff --git a/aircox/templates/aircox/article_detail.html b/aircox/templates/aircox/article_detail.html new file mode 100644 index 0000000..72a5630 --- /dev/null +++ b/aircox/templates/aircox/article_detail.html @@ -0,0 +1,29 @@ +{% extends "aircox/page.html" %} +{% load i18n %} + +{% block side_nav %} +{{ block.super }} + +{% if side_items %} +
+

{% trans "Latest news" %}

+ + {% for object in side_items %} + {% include "aircox/page_item.html" %} + {% endfor %} + +
+ +
+{% endif %} +{% endblock %} + diff --git a/aircox/templates/aircox/article_list.html b/aircox/templates/aircox/article_list.html new file mode 100644 index 0000000..cc4776e --- /dev/null +++ b/aircox/templates/aircox/article_list.html @@ -0,0 +1,12 @@ +{% extends "aircox/page_list.html" %} +{% load i18n aircox %} + +{% block title %} +{% if parent %} + {% with parent.title as parent %} + {% blocktrans %}Articles of {{ parent }}{% endblocktrans %} + {% endwith %} +{% else %}{{ block.super }}{% endif %} +{% endblock %} + + diff --git a/aircox/templates/aircox/base.html b/aircox/templates/aircox/base.html index 1812fdb..ea38221 100644 --- a/aircox/templates/aircox/base.html +++ b/aircox/templates/aircox/base.html @@ -67,6 +67,7 @@ Context: {% block main %}{% endblock main %} + {% if show_side_nav %}