From 8581743d13f24810c602015d81fb1dc3d81602ec Mon Sep 17 00:00:00 2001 From: bkfox Date: Mon, 29 Jul 2019 18:58:45 +0200 Subject: [PATCH] add episode: models, admin, diffusion gen, sounds_monitor, playlist import --- aircox/admin/__init__.py | 27 +- .../admin/__pycache__/__init__.cpython-37.pyc | Bin 0 -> 1173 bytes aircox/admin/__pycache__/base.cpython-37.pyc | Bin 0 -> 1252 bytes .../__pycache__/diffusion.cpython-37.pyc | Bin 0 -> 3468 bytes .../admin/__pycache__/episode.cpython-37.pyc | Bin 0 -> 3443 bytes .../admin/__pycache__/mixins.cpython-37.pyc | Bin 0 -> 2124 bytes aircox/admin/__pycache__/page.cpython-37.pyc | Bin 0 -> 1013 bytes .../admin/__pycache__/playlist.cpython-37.pyc | Bin 0 -> 1477 bytes .../admin/__pycache__/program.cpython-37.pyc | Bin 0 -> 2677 bytes aircox/admin/__pycache__/sound.cpython-37.pyc | Bin 0 -> 1089 bytes aircox/admin/base.py | 91 - aircox/admin/diffusion.py | 2 +- aircox/admin/episode.py | 82 + aircox/admin/page.py | 28 + aircox/admin/playlist.py | 10 +- aircox/admin/program.py | 75 + aircox/admin/sound.py | 10 +- aircox/management/commands/diffusions.py | 98 +- aircox/management/commands/import_playlist.py | 25 +- aircox/management/commands/sounds_monitor.py | 96 +- aircox/management/commands/streamer.py | 2 +- aircox/models.py | 8 +- aircox/models/__init__.py | 8 + .../__pycache__/__init__.cpython-37.pyc | Bin 0 -> 451 bytes .../__pycache__/diffusion.cpython-37.pyc | Bin 0 -> 9706 bytes .../models/__pycache__/episode.cpython-37.pyc | Bin 0 -> 10455 bytes aircox/models/__pycache__/log.cpython-37.pyc | Bin 0 -> 7703 bytes .../models/__pycache__/mixins.cpython-37.pyc | Bin 0 -> 2516 bytes aircox/models/__pycache__/page.cpython-37.pyc | Bin 0 -> 2862 bytes .../models/__pycache__/program.cpython-37.pyc | Bin 0 -> 15818 bytes .../models/__pycache__/sound.cpython-37.pyc | Bin 0 -> 8580 bytes .../models/__pycache__/station.cpython-37.pyc | Bin 0 -> 6334 bytes aircox/models/episode.py | 296 + aircox/models/log.py | 264 + aircox/models/page.py | 73 + aircox/models/program.py | 508 + aircox/models/sound.py | 288 + aircox/models/station.py | 206 + aircox/settings.py | 9 + aircox_cms/__init__.py | 3 - aircox_cms/admin.py | 6 - aircox_cms/apps.py | 9 - aircox_cms/forms.py | 44 - aircox_cms/locale/fr/LC_MESSAGES/django.mo | Bin 19109 -> 0 bytes aircox_cms/locale/fr/LC_MESSAGES/django.po | 1069 -- .../management/commands/programs_to_cms.py | 92 - aircox_cms/models/__init__.py | 823 -- aircox_cms/models/lists.py | 534 - aircox_cms/models/sections.py | 666 - aircox_cms/settings.py | 22 - aircox_cms/signals.py | 196 - aircox_cms/static/aircox_cms/css/layout.css | 627 - aircox_cms/static/aircox_cms/css/theme.css | 50 - aircox_cms/static/aircox_cms/js/bootstrap.js | 41 - aircox_cms/static/aircox_cms/js/player.js | 336 - aircox_cms/static/aircox_cms/js/utils.js | 68 - aircox_cms/static/lib/vue.js | 10798 ---------------- aircox_cms/static/lib/vue.min.js | 6 - aircox_cms/template.py | 73 - .../templates/aircox_cms/base_site.html | 146 - .../templates/aircox_cms/category_page.html | 3 - .../templates/aircox_cms/dated_list_page.html | 15 - .../templates/aircox_cms/diffusion_page.html | 41 - .../aircox_cms/dynamic_list_page.html | 52 - .../templates/aircox_cms/event_page.html | 24 - .../templates/aircox_cms/program_page.html | 41 - .../templates/aircox_cms/publication.html | 27 - .../templates/aircox_cms/sections/item.html | 10 - .../aircox_cms/sections/link_list.html | 11 - .../templates/aircox_cms/sections/list.html | 9 - .../aircox_cms/sections/logs_list.html | 10 - .../aircox_cms/sections/playlist.html | 59 - .../aircox_cms/sections/publication_info.html | 94 - .../aircox_cms/sections/search_field.html | 16 - .../aircox_cms/sections/timetable.html | 6 - .../aircox_cms/snippets/comments.html | 55 - .../aircox_cms/snippets/date_list.html | 48 - .../aircox_cms/snippets/date_list_item.html | 57 - .../templates/aircox_cms/snippets/link.html | 13 - .../templates/aircox_cms/snippets/list.html | 70 - .../aircox_cms/snippets/list_item.html | 53 - .../aircox_cms/snippets/sound_list_item.html | 42 - aircox_cms/templates/aircox_cms/tags | 6 - .../templates/aircox_cms/vues/player.html | 94 - .../templates/wagtailadmin/admin_base.html | 7 - aircox_cms/templates/wagtailadmin/base.html | 6 - aircox_cms/templatetags/aircox_cms.py | 76 - aircox_cms/tests.py | 3 - aircox_cms/utils.py | 55 - aircox_cms/views.py | 1 - aircox_cms/views/__init__.py | 0 aircox_cms/views/components.py | 111 - aircox_cms/wagtail_hooks.py | 414 - aircox_web/models.py | 1 + instance/urls.py | 4 +- 95 files changed, 1976 insertions(+), 17373 deletions(-) create mode 100644 aircox/admin/__pycache__/__init__.cpython-37.pyc create mode 100644 aircox/admin/__pycache__/base.cpython-37.pyc create mode 100644 aircox/admin/__pycache__/diffusion.cpython-37.pyc create mode 100644 aircox/admin/__pycache__/episode.cpython-37.pyc create mode 100644 aircox/admin/__pycache__/mixins.cpython-37.pyc create mode 100644 aircox/admin/__pycache__/page.cpython-37.pyc create mode 100644 aircox/admin/__pycache__/playlist.cpython-37.pyc create mode 100644 aircox/admin/__pycache__/program.cpython-37.pyc create mode 100644 aircox/admin/__pycache__/sound.cpython-37.pyc delete mode 100644 aircox/admin/base.py create mode 100644 aircox/admin/episode.py create mode 100644 aircox/admin/page.py create mode 100644 aircox/admin/program.py create mode 100644 aircox/models/__init__.py create mode 100644 aircox/models/__pycache__/__init__.cpython-37.pyc create mode 100644 aircox/models/__pycache__/diffusion.cpython-37.pyc create mode 100644 aircox/models/__pycache__/episode.cpython-37.pyc create mode 100644 aircox/models/__pycache__/log.cpython-37.pyc create mode 100644 aircox/models/__pycache__/mixins.cpython-37.pyc create mode 100644 aircox/models/__pycache__/page.cpython-37.pyc create mode 100644 aircox/models/__pycache__/program.cpython-37.pyc create mode 100644 aircox/models/__pycache__/sound.cpython-37.pyc create mode 100644 aircox/models/__pycache__/station.cpython-37.pyc create mode 100644 aircox/models/episode.py create mode 100644 aircox/models/log.py create mode 100644 aircox/models/page.py create mode 100644 aircox/models/program.py create mode 100644 aircox/models/sound.py create mode 100644 aircox/models/station.py delete mode 100755 aircox_cms/__init__.py delete mode 100755 aircox_cms/admin.py delete mode 100755 aircox_cms/apps.py delete mode 100755 aircox_cms/forms.py delete mode 100644 aircox_cms/locale/fr/LC_MESSAGES/django.mo delete mode 100755 aircox_cms/locale/fr/LC_MESSAGES/django.po delete mode 100755 aircox_cms/management/commands/programs_to_cms.py delete mode 100755 aircox_cms/models/__init__.py delete mode 100644 aircox_cms/models/lists.py delete mode 100644 aircox_cms/models/sections.py delete mode 100755 aircox_cms/settings.py delete mode 100755 aircox_cms/signals.py delete mode 100755 aircox_cms/static/aircox_cms/css/layout.css delete mode 100755 aircox_cms/static/aircox_cms/css/theme.css delete mode 100644 aircox_cms/static/aircox_cms/js/bootstrap.js delete mode 100644 aircox_cms/static/aircox_cms/js/player.js delete mode 100755 aircox_cms/static/aircox_cms/js/utils.js delete mode 100644 aircox_cms/static/lib/vue.js delete mode 100644 aircox_cms/static/lib/vue.min.js delete mode 100644 aircox_cms/template.py delete mode 100755 aircox_cms/templates/aircox_cms/base_site.html delete mode 100644 aircox_cms/templates/aircox_cms/category_page.html delete mode 100755 aircox_cms/templates/aircox_cms/dated_list_page.html delete mode 100755 aircox_cms/templates/aircox_cms/diffusion_page.html delete mode 100755 aircox_cms/templates/aircox_cms/dynamic_list_page.html delete mode 100755 aircox_cms/templates/aircox_cms/event_page.html delete mode 100755 aircox_cms/templates/aircox_cms/program_page.html delete mode 100755 aircox_cms/templates/aircox_cms/publication.html delete mode 100755 aircox_cms/templates/aircox_cms/sections/item.html delete mode 100755 aircox_cms/templates/aircox_cms/sections/link_list.html delete mode 100755 aircox_cms/templates/aircox_cms/sections/list.html delete mode 100755 aircox_cms/templates/aircox_cms/sections/logs_list.html delete mode 100755 aircox_cms/templates/aircox_cms/sections/playlist.html delete mode 100755 aircox_cms/templates/aircox_cms/sections/publication_info.html delete mode 100755 aircox_cms/templates/aircox_cms/sections/search_field.html delete mode 100755 aircox_cms/templates/aircox_cms/sections/timetable.html delete mode 100755 aircox_cms/templates/aircox_cms/snippets/comments.html delete mode 100755 aircox_cms/templates/aircox_cms/snippets/date_list.html delete mode 100755 aircox_cms/templates/aircox_cms/snippets/date_list_item.html delete mode 100755 aircox_cms/templates/aircox_cms/snippets/link.html delete mode 100755 aircox_cms/templates/aircox_cms/snippets/list.html delete mode 100755 aircox_cms/templates/aircox_cms/snippets/list_item.html delete mode 100755 aircox_cms/templates/aircox_cms/snippets/sound_list_item.html delete mode 100755 aircox_cms/templates/aircox_cms/tags delete mode 100644 aircox_cms/templates/aircox_cms/vues/player.html delete mode 100644 aircox_cms/templates/wagtailadmin/admin_base.html delete mode 100644 aircox_cms/templates/wagtailadmin/base.html delete mode 100755 aircox_cms/templatetags/aircox_cms.py delete mode 100755 aircox_cms/tests.py delete mode 100755 aircox_cms/utils.py delete mode 100755 aircox_cms/views.py delete mode 100644 aircox_cms/views/__init__.py delete mode 100644 aircox_cms/views/components.py delete mode 100755 aircox_cms/wagtail_hooks.py diff --git a/aircox/admin/__init__.py b/aircox/admin/__init__.py index 4a0752a..1954e33 100644 --- a/aircox/admin/__init__.py +++ b/aircox/admin/__init__.py @@ -1,5 +1,28 @@ -from .base import * -from .diffusion import DiffusionAdmin +from django.contrib import admin + + +from .episode import DiffusionAdmin, EpisodeAdmin # from .playlist import PlaylistAdmin +from .program import ProgramAdmin, ScheduleAdmin, StreamAdmin from .sound import SoundAdmin +from aircox.models import Log, Port, Station + + +class PortInline(admin.StackedInline): + model = Port + extra = 0 + + +@admin.register(Station) +class StationAdmin(admin.ModelAdmin): + prepopulated_fields = {'slug': ('name',)} + inlines = [PortInline] + + +@admin.register(Log) +class LogAdmin(admin.ModelAdmin): + list_display = ['id', 'date', 'station', 'source', 'type', 'diffusion', 'sound', 'track'] + list_filter = ['date', 'source', 'station'] + + diff --git a/aircox/admin/__pycache__/__init__.cpython-37.pyc b/aircox/admin/__pycache__/__init__.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..70c1708baad6ef144c63d5390c7644c333977b55 GIT binary patch literal 1173 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 V=*&_^^xCCmT- literal 0 HcmV?d00001 diff --git a/aircox/admin/__pycache__/episode.cpython-37.pyc b/aircox/admin/__pycache__/episode.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5748b4a95326d14ac3ef0161dd84f8752b40507a GIT binary patch literal 3443 zcma)8+in}j8Q$45kFsbdmYOzY9ViWpmdaJrx~`)fT&F-I1h9dk3j~YR&X8JqcS+AI z8;hW?Xdu05fxdxs+4tyE%x$mo3c2d{&ytiasYQmEpELi=&VN39+UYbcjMlx6zn(QL z>p!&F3?GXlO!aR7ZgG}d5&hdaE9}T-w03f*a3inqBj4=ZJSf5_EE-XxXhzMV6}5o( zxSzL+PSi2$Anz7C(GIg7TRh~AR~B!q?C6%T&v!-Rf&ts)Enr&$xJhs2u2^&%*f#F~ z+cDTXD>r%zxGvuTZU^`7teog=^G0aixW#vIvMV|(2jAc2?1?q#eFsUb;{nrN!i%gN z*gBk#MOBFxmF{d-ah@#B2Cfbs%`!FRLN`9oMx(jPrlt0tOy?!n-cy;R&%R@L4UaXx zO2*<7^B~i0bEN)Q=2X@rK2Nle_vR6hb3TTAQf-b=9YH)~=&^QF6F|FoaGPpp5D3jI+ga;{(yd-$RD z%noQ*iF_n?@%{I|J1hjxl0%+U$zeL3%cQK-Avl-_{MSrw|Hu}HqBh@fcJ_VqL7u59 zoffl)h9hFnw-)!WYjy~Ch^0vQw9L=qnycZ1e3~Y?dWgm58Tf~%<})Gn?Hj((?p)z0 zJWW)LJH^12Z-Jx^qnTjiK&Ov%hN=%2#jGfYMNA_x!?6a@Ir zK3;SW{&ILw93Jrg!58-rzJwdLjLG9Vz(Q5YQI!>9VAlrv3EsF=kkO28n!7RlQ|u8P zL!2%4-{fg{dCP>&59iU_u-2$qL(t0u5c+>Sg}7um`tfTV-Mzxm=8lQrfv1CY;K=t$ zN=jC;1E7Pf%&IKO2aeg-QGW|3a1gpMuowQXYK7SuukZYVzNS0d%&{nAPcqTk3iYdP9P2DwC@ zDS>qI_|a4CmyHBZ1wB}_xEk~I|28A9d6G?IiZ9iAnr98w5@wm=A zZFY;fZ1K(&r+JFq!i`9MM7%2<^~rn(0Cg z{OqpXZb-V5Vr~b8>k+2v18lOEa3Ytp$m4e8Q=vq@3OH3|U+-)cQkQx;z;}}B-`4E( zB$@p)%p23r|E{eFYO0#@9e_=MlgTb6u9HrWDonz;sWO?O9_0a1BhnUI+`Gc$@cQ-R zL3q`A#&tZ@3z6$!^vu){?V(4KiTwSWOk4&Bk^KnMxJ|fLwIu8tYNqf%qH7d<*VRl) z=HA$ay6rK8vVD|;016Lv%d}pvE8Aoxca~JAI^<{w$iH+WL&=?=N!lxlg7H#GHH0mSV)pL3MtP}VyWez7C?tmjDbPQX?{trZggq# zg6=#`PUd+c*Um;xT+2@U-z2gQAJpyDLrMUODoto3bJ}}=ZKvDpv3CDXqZjtre*sB& B70Cbq literal 0 HcmV?d00001 diff --git a/aircox/admin/__pycache__/mixins.cpython-37.pyc b/aircox/admin/__pycache__/mixins.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fc084d98c6e65ac7c69a1059159e6774c0365400 GIT binary patch literal 2124 zcmb7E&2Jnv6t_K}yV*ol8zHDDHAtvh4jX#up(^D|qHthUDdI8`a;IZ>Ygi)b=** zYQrUw0vxz<;gCJ@FZs$Jz=0bl-t){Rfrdjp^2gZE@4esi@4YWJH~R$I<3B(7{^y90 zKk#SW0$6+o-CT#^5s^gF6X?e*Ba-csVNk&Sc|&Pz=4%)f$*3e5lQat?%R=ap46^u? zOsEWHbdqL?jAe2{vQ(zB2XoJ#_2mYf^-oDivJJ2?aNFP2T2;AK@|(IUYxQmUvaAeoORWl9z2cooZW~a6X789*dgR5&judOaIET=2HaB5d(j^hZ(qnSQmSh?%*@{sJ zAHbpGVd(nkf$+9nWabA-yHtp>F0Bx5aFAI zLi|qtzP~e5vdnj6Zu6a@nd`i^X6HaRQw4Kg>Y{mhK4CC+W*9~@I(Vhw*$I-sAP7QZ zy0~>!$VY1z3x;yg*1*4nwc#)TSTdz8=oOuYN6bbL3^t8{H37h9i%3o=%_eX2L&5A9n#Lh;+|}cSd|XOxj>XVy0~FdV9)>NvSI7dR^ou z&)h((Sz}dqkbq2Xt#)CZ&(tu`3Ha(#{|aExMgG3V7_`F%p7&96t8xqe+vu?CbaDI2 z=v+v@MhqVxB!>0Wm_kI30}AUiwjwwqF%~!ZZvFz|@LFg_9B7b0xDbOzUw{-Kx&{)r zPHN23|AN>+!4Aa@M1e#iY>TFJ!LP(|>B2b>WXL(gKY|}ifZG(GcEyf@CHV=;gHK;7 z*-0wdGB^&7!et1+f^mT+*UfVbe#|`>{#7O5>TmRvoYD!CfecUjS=jwWGKQKE`I-=e z%4Bi(O4mIV+VG7YR85gr=Hcio*w|He3>lNI$yQJ>jvc+hejB>^7=|`=f20U*MUN07 z5zGe1WEw7^nk{Kbm%*>>7uqE>8|!!A-Y{yzzYS}@p;4_*tzqc-=679~>pkP*XNNcs zneKswS1*l^t&kLGYFGMqr`{MyP9&1F7Ca#^Pc1h)<(=*sEvJrWpQ5N zdLYE}d0w?AZSaw%-m=^5YGa|QN(k>4_=`TmgfZ7X#gmR4c;GW5r6Jpn_4_Eu{1dv3 zORR!d_N61y>iJw5>jEer+2&(ku{&Mu<?NdBN9e+V5{{Skx{mlRX literal 0 HcmV?d00001 diff --git a/aircox/admin/__pycache__/page.cpython-37.pyc b/aircox/admin/__pycache__/page.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a85256e0c3a1f859688526fffebe7bb3e54a4787 GIT binary patch literal 1013 zcmY*YyKWRQ6t(BIvzy%{B!L8pB1*SSwm}G?5Ctfxh$4k*BxHH#dIv9_2et>21*+sL z5CxR{l3OZ%fdX+on?%7Q--qq7&pFqAJ{k=Owy)cVZ~g{^{PM|li!eCCZl0h40#uQb zW|aCkP(c}HVHbxgD&s7nZ=mMOkC&;8o#mSAv%-MmI4NJfEV0njXjAd4W(Vt<~bSpo_4AjPZ?gCdf2 z7Kk3KTu>Oo2v&;_)?ocxFbj$RG2MZUOOo|r6R~Z%{s}L{vkqA3R`U9T(9FJV%c+a4 zv`V8z35*BLa!g@OYN%S@@`SW;E3XpsP-113Yx0)mPNu%qzK+mPl z>$4l@Rh~4c_kMHUsd*2#FddO#ardSW`?soaJRZ1|v5J?1F*jhWtf5st9y0d6qq;OZ zKJ{|@%V4(9q%`WjGz{o^kkFJ4qi)6VxbIeB&a0wEMwQiasy$b?eKpuxsm#8A1_mdq zVk!F0f3p?^HHZ;!jrI>x@A@4EuWAsgyS3d>w*Yo{JxZ4zEVVkMm>G|ENbw8*0iTx* A)Bpeg literal 0 HcmV?d00001 diff --git a/aircox/admin/__pycache__/playlist.cpython-37.pyc b/aircox/admin/__pycache__/playlist.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..03e431bfebf4404f71819c0fa232e85ee1db698d GIT binary patch literal 1477 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 O$Q?2NV8nu#M(iIJgH-1L literal 0 HcmV?d00001 diff --git a/aircox/admin/__pycache__/sound.cpython-37.pyc b/aircox/admin/__pycache__/sound.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bbc5043941e71ed16fba7652950b247608efb653 GIT binary patch literal 1089 zcmYjQJ8u**5VrSm`^a7%mk1I>1=TfoH4=gV1)?BLQmuqmdpEv~5AOqePs&B<68-{p zQu3GFQspnCAk6ILLM(Ya&RW}FYnjT=mvrX1UQ}?)S2vOqoln1;FO`y&m{OSah)_Ai zX)aI*mU3XJ100+ymb$_NOfTHb2QTe`Prx9A2x8cP1o}mk2C$iiNMj3q+vp1*DSXLi zPPezyogEb3FwFL!~B8BO&x?ogH|f-C!FR53gFl5g3tLi zqP#(zzzhW&77OMXp=Q>tC-dJ-Z)3E( zH{()(%*T+o`FL6{bY8V)+~|5PaLuKj)@P3kAsZVi?5J7Va3+F zwDwz%v?J?CupV8o0VXJPURtkRHo}I0d0SamHj*Z57850>b_>;_t|9xe$PxY02BMLs z2B8O-Z|Lb~xaj93s4n^dh3(CxP+)XIhA+yA0PB~nEXC02HuXMwhJ71m86n9s8)sQr z!$OgsWL>tEr^{n`W_QeKt=kNQnQGY(PW_VNCrTRJmRQ|6HV`6_a8PFtyE^z;-ebX6TW$ mBo%+9Q(b>{>7T%+Uebi!hHqEZd`Dl;P-`RzIN^yq;Qs)v6C{xU literal 0 HcmV?d00001 diff --git a/aircox/admin/base.py b/aircox/admin/base.py deleted file mode 100644 index f5680eb..0000000 --- a/aircox/admin/base.py +++ /dev/null @@ -1,91 +0,0 @@ -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 - - -@admin.register(Stream) -class StreamAdmin(admin.ModelAdmin): - list_display = ('id', 'program', 'delay', 'begin', 'end') - - -@admin.register(Program) -class ProgramAdmin(admin.ModelAdmin): - def schedule(self, obj): - return Schedule.objects.filter(program=obj).count() > 0 - - schedule.boolean = True - schedule.short_description = _("Schedule") - - list_display = ('name', 'id', 'active', 'schedule', 'sync', 'station') - fields = ['name', 'slug', 'active', 'station', 'sync'] - prepopulated_fields = {'slug': ('name',)} - search_fields = ['name'] - - 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): - prepopulated_fields = {'slug': ('name',)} - inlines = [PortInline] - - -@admin.register(Log) -class LogAdmin(admin.ModelAdmin): - list_display = ['id', 'date', 'station', 'source', 'type', 'diffusion', 'sound', 'track'] - list_filter = ['date', 'source', 'station'] - -admin.site.register(Port) - - - - diff --git a/aircox/admin/diffusion.py b/aircox/admin/diffusion.py index 977905f..168bbfb 100644 --- a/aircox/admin/diffusion.py +++ b/aircox/admin/diffusion.py @@ -9,7 +9,7 @@ from .playlist import TracksInline class SoundInline(admin.TabularInline): model = Sound fk_name = 'diffusion' - fields = ['type', 'path', 'duration','public'] + fields = ['type', 'path', 'duration', 'is_public'] readonly_fields = ['type'] extra = 0 diff --git a/aircox/admin/episode.py b/aircox/admin/episode.py new file mode 100644 index 0000000..794221e --- /dev/null +++ b/aircox/admin/episode.py @@ -0,0 +1,82 @@ +import copy + +from django.contrib import admin +from django.utils.translation import ugettext as _, ugettext_lazy + +from aircox.models import Episode, Diffusion, Sound, Track + +from .page import PageAdmin +from .playlist import TracksInline + + +class DiffusionBaseAdmin: + fields = ['type', 'start', 'end'] + + def get_readonly_fields(self, request, obj=None): + fields = super().get_readonly_fields(request, obj) + if not request.user.has_perm('aircox_program.scheduling'): + fields += ['program', 'start', 'end'] + return [field for field in fields if field in self.fields] + + +@admin.register(Diffusion) +class DiffusionAdmin(DiffusionBaseAdmin, admin.ModelAdmin): + def start_date(self, obj): + return obj.local_start.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') + + list_display = ('episode', 'start_date', 'end_date', 'type', 'initial') + list_filter = ('type', 'start', 'program') + list_editable = ('type',) + ordering = ('-start', 'id') + + fields = ['type', 'start', 'end', 'initial', 'program'] + + 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) + + +class DiffusionInline(DiffusionBaseAdmin, admin.TabularInline): + model = Diffusion + fk_name = 'episode' + extra = 0 + + def has_add_permission(self, request): + return request.user.has_perm('aircox_program.scheduling') + + +class SoundInline(admin.TabularInline): + model = Sound + fk_name = 'episode' + fields = ['type', 'path', 'duration', 'is_public'] + readonly_fields = ['type'] + extra = 0 + + +@admin.register(Episode) +class EpisodeAdmin(PageAdmin): + list_display = PageAdmin.list_display + ('program',) + list_filter = ('program',) + readonly_fields = ('program',) + + fieldsets = copy.deepcopy(PageAdmin.fieldsets) + fieldsets[1][1]['fields'].insert(0, 'program') + inlines = [TracksInline, SoundInline, DiffusionInline] + + diff --git a/aircox/admin/page.py b/aircox/admin/page.py new file mode 100644 index 0000000..ae2fb3d --- /dev/null +++ b/aircox/admin/page.py @@ -0,0 +1,28 @@ +from django.contrib import admin +from django.utils.safestring import mark_safe +from django.utils.translation import ugettext_lazy as _ + + +class PageAdmin(admin.ModelAdmin): + list_display = ('cover_thumb', 'title', 'status') + list_display_links = ('cover_thumb', 'title') + list_editable = ('status',) + prepopulated_fields = {"slug": ("title",)} + + fieldsets = [ + ('', { + 'fields': ['title', 'slug', 'cover', 'content'], + }), + (_('Publication Settings'), { + 'fields': ['featured', 'allow_comments', 'status'], + 'classes': ('collapse',), + }), + ] + + def cover_thumb(self, obj): + return mark_safe(''.format(obj.cover.icons['64'])) \ + if obj.cover else '' + + + + diff --git a/aircox/admin/playlist.py b/aircox/admin/playlist.py index 0d760b6..1879c1e 100644 --- a/aircox/admin/playlist.py +++ b/aircox/admin/playlist.py @@ -21,19 +21,19 @@ class TrackAdmin(admin.ModelAdmin): 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', 'timestamp'] + list_display = ['pk', 'artist', 'title', 'tag_list', 'episode', 'sound', 'timestamp'] list_editable = ['artist', 'title'] - list_filter = ['sound', 'diffusion', 'artist', 'title', 'tags'] + list_filter = ['sound', 'episode', 'artist', 'title', 'tags'] fieldsets = [ - (_('Playlist'), {'fields': ['diffusion', 'sound', 'position', 'timestamp']}), + (_('Playlist'), {'fields': ['episode', 'sound', 'position', 'timestamp']}), (_('Info'), {'fields': ['artist', 'title', 'info', 'tags']}), ] - # TODO on edit: readonly_fields = ['diffusion', 'sound'] + # TODO on edit: readonly_fields = ['episode', 'sound'] #@admin.register(Playlist) #class PlaylistAdmin(admin.ModelAdmin): -# fields = ['diffusion', 'sound'] +# fields = ['episode', 'sound'] # inlines = [TracksInline] # # TODO: dynamic read only fields diff --git a/aircox/admin/program.py b/aircox/admin/program.py new file mode 100644 index 0000000..92345bc --- /dev/null +++ b/aircox/admin/program.py @@ -0,0 +1,75 @@ +from copy import deepcopy + +from django.contrib import admin +from django.utils.translation import ugettext_lazy as _ + +from aircox.models import Program, Schedule, Stream +from .page import PageAdmin + + +class ScheduleInline(admin.TabularInline): + model = Schedule + extra = 1 + + +class StreamInline(admin.TabularInline): + fields = ['delay', 'begin', 'end'] + model = Stream + extra = 1 + + +@admin.register(Program) +class ProgramAdmin(PageAdmin): + def schedule(self, obj): + return Schedule.objects.filter(program=obj).count() > 0 + + schedule.boolean = True + schedule.short_description = _("Schedule") + + list_display = PageAdmin.list_display + ('schedule', 'station') + fieldsets = deepcopy(PageAdmin.fieldsets) + [ + (_('Program Settings'), { + 'fields': ['active', 'station', 'sync'], + 'classes': ('collapse',), + }) + ] + + prepopulated_fields = {'slug': ('title',)} + search_fields = ['title'] + + inlines = [ScheduleInline, StreamInline] + + +@admin.register(Schedule) +class ScheduleAdmin(admin.ModelAdmin): + def program_title(self, obj): + return obj.program.title + program_title.short_description = _('Program') + + def freq(self, obj): + return obj.get_frequency_verbose() + freq.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 = ['program_title', 'freq', 'time', 'timezone', 'duration', + 'rerun'] + list_editable = ['time', 'duration'] + + def get_readonly_fields(self, request, obj=None): + if obj: + return ['program', 'date', 'frequency'] + else: + return [] + + +@admin.register(Stream) +class StreamAdmin(admin.ModelAdmin): + list_display = ('id', 'program', 'delay', 'begin', 'end') + + + diff --git a/aircox/admin/sound.py b/aircox/admin/sound.py index 3203e16..3f2655c 100644 --- a/aircox/admin/sound.py +++ b/aircox/admin/sound.py @@ -7,12 +7,16 @@ from .playlist import TracksInline @admin.register(Sound) class SoundAdmin(admin.ModelAdmin): + def filename(self, obj): + return '/'.join(obj.path.split('/')[-2:]) + filename.short_description=_('file') + fields = None - list_display = ['id', 'name', 'program', 'type', 'duration', 'mtime', - 'is_public', 'is_good_quality', 'path'] + list_display = ['id', 'name', 'program', 'type', 'duration', + 'is_public', 'is_good_quality', 'episode', 'filename'] list_filter = ('program', 'type', 'is_good_quality', 'is_public') fieldsets = [ - (None, {'fields': ['name', 'path', 'type', 'program', 'diffusion']}), + (None, {'fields': ['name', 'path', 'type', 'program', 'episode']}), (None, {'fields': ['embed', 'duration', 'is_public', 'mtime']}), (None, {'fields': ['is_good_quality']}) ] diff --git a/aircox/management/commands/diffusions.py b/aircox/management/commands/diffusions.py index 53baa62..f75fbd3 100755 --- a/aircox/management/commands/diffusions.py +++ b/aircox/management/commands/diffusions.py @@ -15,60 +15,57 @@ planified before the (given) month. - "check" will remove all diffusions that are unconfirmed and have been planified from the (given) month and later. """ -import time +import datetime import logging from argparse import RawTextHelpFormatter from django.core.management.base import BaseCommand, CommandError +from django.db import transaction from django.utils import timezone as tz -from aircox.models import * +from aircox.models import Schedule, Diffusion logger = logging.getLogger('aircox.tools') class Actions: - @classmethod - def update(cl, date, mode): - manual = (mode == 'manual') + date = None - count = [0, 0] - for schedule in Schedule.objects.filter(program__active=True) \ - .order_by('initial'): - # in order to allow rerun links between diffusions, we save items - # by schedule; - items = schedule.diffusions_of_month(date, exclude_saved=True) - count[0] += len(items) + def __init__(self, date): + self.date = date or datetime.date.today() - # we can't bulk create because we need signal processing - for item in items: - conflicts = item.get_conflicts() - item.type = Diffusion.Type.unconfirmed \ - if manual or conflicts.count() else \ - Diffusion.Type.normal - item.save(no_check=True) - if conflicts.count(): - item.conflicts.set(conflicts.all()) + def update(self): + episodes, diffusions = [], [] + for schedule in Schedule.objects.filter(program__active=True, + initial__isnull=True): + eps, diffs = schedule.diffusions_of_month(self.date) - logger.info('[update] schedule %s: %d new diffusions', - str(schedule), len(items), - ) + episodes += eps + diffusions += diffs - logger.info('[update] %d diffusions have been created, %s', count[0], - 'do not forget manual approval' if manual else - '{} conflicts found'.format(count[1])) + logger.info('[update] %s: %d episodes, %d diffusions and reruns', + str(schedule), len(eps), len(diffs)) - @staticmethod - def clean(date): + with transaction.atomic(): + logger.info('[update] save %d episodes and %d diffusions', + len(episodes), len(diffusions)) + for episode in episodes: + episode.save() + for diffusion in diffusions: + # force episode id's update + diffusion.episode = diffusion.episode + diffusion.save() + + def clean(self): qs = Diffusion.objects.filter(type=Diffusion.Type.unconfirmed, - start__lt=date) + start__lt=self.date) logger.info('[clean] %d diffusions will be removed', qs.count()) qs.delete() - @staticmethod - def check(date): + def check(self): + # TODO: redo qs = Diffusion.objects.filter(type=Diffusion.Type.unconfirmed, - start__gt=date) + start__gt=self.date) items = [] for diffusion in qs: schedules = Schedule.objects.filter(program=diffusion.program) @@ -88,21 +85,21 @@ class Command(BaseCommand): def add_arguments(self, parser): parser.formatter_class = RawTextHelpFormatter - now = tz.datetime.today() + today = datetime.date.today() group = parser.add_argument_group('action') group.add_argument( - '--update', action='store_true', + '-u', '--update', action='store_true', help='generate (unconfirmed) diffusions for the given month. ' 'These diffusions must be confirmed manually by changing ' 'their type to "normal"' ) group.add_argument( - '--clean', action='store_true', + '-l', '--clean', action='store_true', help='remove unconfirmed diffusions older than the given month' ) group.add_argument( - '--check', action='store_true', + '-c', '--check', action='store_true', help='check unconfirmed later diffusions from the given ' 'date agains\'t schedule. If no schedule is found, remove ' 'it.' @@ -110,10 +107,10 @@ class Command(BaseCommand): group = parser.add_argument_group('date') group.add_argument( - '--year', type=int, default=now.year, + '--year', type=int, default=today.year, help='used by update, default is today\'s year') group.add_argument( - '--month', type=int, default=now.month, + '--month', type=int, default=today.month, help='used by update, default is today\'s month') group.add_argument( '--next-month', action='store_true', @@ -121,31 +118,20 @@ class Command(BaseCommand): ' (if next month from today' ) - group = parser.add_argument_group('options') - group.add_argument( - '--mode', type=str, choices=['manual', 'auto'], - default='auto', - help='manual means that all generated diffusions are unconfirmed, ' - 'thus must be approved manually; auto confirmes all ' - 'diffusions except those that conflicts with others' - ) - def handle(self, *args, **options): - date = tz.datetime(year=options.get('year'), - month=options.get('month'), - day=1) - date = tz.make_aware(date) + date = datetime.date(year=options['year'], month=options['month'], + day=1) if options.get('next_month'): month = options.get('month') date += tz.timedelta(days=28) if date.month == month: date += tz.timedelta(days=28) - date = date.replace(day=1) + actions = Actions(date) if options.get('update'): - Actions.update(date, mode=options.get('mode')) + actions.update() if options.get('clean'): - Actions.clean(date) + actions.clean() if options.get('check'): - Actions.check(date) + actions.check() diff --git a/aircox/management/commands/import_playlist.py b/aircox/management/commands/import_playlist.py index 91bf382..011e221 100755 --- a/aircox/management/commands/import_playlist.py +++ b/aircox/management/commands/import_playlist.py @@ -1,6 +1,6 @@ """ -Import one or more playlist for the given sound. Attach it to the sound -or to the related Diffusion if wanted. +Import one or more playlist for the given sound. Attach it to the provided +sound. Playlists are in CSV format, where columns are separated with a '{settings.AIRCOX_IMPORT_PLAYLIST_CSV_DELIMITER}'. Text quote is @@ -18,14 +18,15 @@ from argparse import RawTextHelpFormatter from django.core.management.base import BaseCommand, CommandError from django.contrib.contenttypes.models import ContentType +from aircox import settings from aircox.models import * -import aircox.settings as settings + __doc__ = __doc__.format(settings=settings) logger = logging.getLogger('aircox.tools') -class Importer: +class PlaylistImport: path = None data = None tracks = None @@ -121,17 +122,12 @@ class Command (BaseCommand): help='generate a playlist for the sound of the given path. ' 'If not given, try to match a sound with the same path.' ) - parser.add_argument( - '--diffusion', '-d', action='store_true', - help='try to get the diffusion relative to the sound if it exists' - ) def handle(self, path, *args, **options): # FIXME: absolute/relative path of sounds vs given path if options.get('sound'): - sound = Sound.objects.filter( - path__icontains=options.get('sound') - ).first() + sound = Sound.objects.filter(path__icontains=options.get('sound'))\ + .first() else: path_, ext = os.path.splitext(path) sound = Sound.objects.filter(path__icontains=path_).first() @@ -141,11 +137,10 @@ class Command (BaseCommand): '{path}'.format(path=path)) return - if options.get('diffusion') and sound.diffusion: - sound = sound.diffusion - - importer = Importer(path, sound=sound).run() + # FIXME: auto get sound.episode if any + importer = PlaylistImport(path, sound=sound).run() for track in importer.tracks: logger.info('track #{pos} imported: {title}, by {artist}'.format( pos=track.position, title=track.title, artist=track.artist )) + diff --git a/aircox/management/commands/sounds_monitor.py b/aircox/management/commands/sounds_monitor.py index c034470..422d08d 100755 --- a/aircox/management/commands/sounds_monitor.py +++ b/aircox/management/commands/sounds_monitor.py @@ -23,6 +23,7 @@ parameters given by the setting AIRCOX_SOUND_QUALITY. This script requires Sox (and soxi). """ from argparse import RawTextHelpFormatter +import datetime import atexit import logging import os @@ -37,13 +38,21 @@ from django.conf import settings as main_settings from django.core.management.base import BaseCommand, CommandError from django.utils import timezone as tz -from aircox.models import * -import aircox.settings as settings -import aircox.utils as utils +from aircox import settings, utils +from aircox.models import Diffusion, Program, Sound +from .import_playlist import PlaylistImport logger = logging.getLogger('aircox.tools') +sound_path_re = re.compile( + '^(?P[0-9]{4})(?P[0-9]{2})(?P[0-9]{2})' + '(_(?P[0-9]{2})h(?P[0-9]{2}))?' + '(_(?P[0-9]+))?' + '_?(?P.*)$' +) + + class SoundInfo: name = '' sound = None @@ -66,33 +75,19 @@ class SoundInfo: Parse file name to get info on the assumption it has the correct format (given in Command.help) """ - file_name = os.path.basename(value) - file_name = os.path.splitext(file_name)[0] - r = re.search('^(?P[0-9]{4})' - '(?P[0-9]{2})' - '(?P[0-9]{2})' - '(_(?P[0-9]{2})h(?P[0-9]{2}))?' - '(_(?P[0-9]+))?' - '_?(?P.*)$', - file_name) - - if not (r and r.groupdict()): - r = {'name': file_name} - logger.info('file name can not be parsed -> %s', value) - else: - r = r.groupdict() + name = os.path.splitext(os.path.basename(value))[0] + match = sound_path_re.search(name) + match = match.groupdict() if match and match.groupdict() else \ + {'name': name} self._path = value - self.name = r['name'].replace('_', ' ').capitalize() + self.name = match['name'].replace('_', ' ').capitalize() for key in ('year', 'month', 'day', 'hour', 'minute'): - value = r.get(key) - if value is not None: - value = int(value) - setattr(self, key, value) + value = match.get(key) + setattr(self, key, int(value) if value is not None else None) - self.n = r.get('n') - return r + self.n = match.get('n') def __init__(self, path='', sound=None): self.path = path @@ -116,9 +111,8 @@ class SoundInfo: (if save is True, sync to DB), and check for a playlist file. """ sound, created = Sound.objects.get_or_create( - path=self.path, - defaults=kwargs - ) + path=self.path, defaults=kwargs) + if created or sound.check_on_file(): logger.info('sound is new or have been modified -> %s', self.path) sound.duration = self.get_duration() @@ -139,22 +133,17 @@ class SoundInfo: if sound.track_set.count(): return - import aircox.management.commands.import_playlist \ - as import_playlist - - # no playlist, try to retrieve metadata + # import playlist path = os.path.splitext(self.sound.path)[0] + '.csv' - if not os.path.exists(path): - if use_default: - track = sound.file_metadata() - if track: - track.save() - return + if os.path.exists(path): + PlaylistImport(path, sound=sound).run() + # try metadata + elif use_default: + track = sound.file_metadata() + if track: + track.save() - # else, import - import_playlist.Importer(path, sound=sound).run() - - def find_diffusion(self, program, save=True): + def find_episode(self, program, save=True): """ For a given program, check if there is an initial diffusion to associate to, using the date info we have. Update self.sound @@ -163,25 +152,22 @@ class SoundInfo: We only allow initial diffusion since there should be no rerun. """ - if self.year == None or not self.sound or self.sound.diffusion: + if self.year is None or not self.sound or self.sound.episode: return if self.hour is None: date = datetime.date(self.year, self.month, self.day) else: - date = datetime.datetime(self.year, self.month, self.day, - self.hour or 0, self.minute or 0) + date = tz.datetime(self.year, self.month, self.day, + self.hour or 0, self.minute or 0) date = tz.get_current_timezone().localize(date) - qs = Diffusion.objects.station(program.station).after(date) \ - .filter(program=program, initial__isnull=True) - diffusion = qs.first() + diffusion = program.diffusion_set.initial().at(date).first() if not diffusion: return - logger.info('diffusion %s mathes to sound -> %s', str(diffusion), - self.sound.path) - self.sound.diffusion = diffusion + logger.info('%s <--> %s', self.sound.path, str(diffusion.episode)) + self.sound.episode = diffusion.episode if save: self.sound.save() return diffusion @@ -219,7 +205,7 @@ class MonitorHandler(PatternMatchingEventHandler): self.sound_kwargs['program'] = program si.get_sound(save=True, **self.sound_kwargs) if si.year is not None: - si.find_diffusion(program) + si.find_episode(program) si.sound.save(True) def on_deleted(self, event): @@ -246,7 +232,7 @@ class MonitorHandler(PatternMatchingEventHandler): if program: si = SoundInfo(sound.path, sound=sound) if si.year is not None: - si.find_diffusion(program) + si.find_episode(program) sound.save() @@ -270,7 +256,7 @@ class Command(BaseCommand): dirs = [] for program in programs: - logger.info('#%d %s', program.id, program.name) + logger.info('#%d %s', program.id, program.title) self.scan_for_program( program, settings.AIRCOX_SOUND_ARCHIVES_SUBDIR, type=Sound.Type.archive, @@ -304,7 +290,7 @@ class Command(BaseCommand): si = SoundInfo(path) sound_kwargs['program'] = program si.get_sound(save=True, **sound_kwargs) - si.find_diffusion(program, save=True) + si.find_episode(program, save=True) si.find_playlist(si.sound) sounds.append(si.sound.pk) diff --git a/aircox/management/commands/streamer.py b/aircox/management/commands/streamer.py index 4017132..dc1f986 100755 --- a/aircox/management/commands/streamer.py +++ b/aircox/management/commands/streamer.py @@ -216,7 +216,7 @@ class Monitor: return qs = Diffusions.objects.station(self.station).at().filter( - type=Diffusion.Type.normal, + type=Diffusion.Type.on_air, sound__type=Sound.Type.archive, ) logs = Log.objects.station(station).on_air().with_diff() diff --git a/aircox/models.py b/aircox/models.py index da689fb..94ccf59 100755 --- a/aircox/models.py +++ b/aircox/models.py @@ -153,12 +153,12 @@ class Station(models.Model): if date: logs = Log.objects.at(date) diffs = Diffusion.objects.station(self).at(date) \ - .filter(start__lte=now, type=Diffusion.Type.normal) \ + .filter(start__lte=now, type=Diffusion.Type.on_air) \ .order_by('-start') else: logs = Log.objects diffs = Diffusion.objects \ - .filter(type=Diffusion.Type.normal, + .filter(type=Diffusion.Type.on_air, start__lte=now) \ .order_by('-start')[:count] @@ -653,7 +653,7 @@ class DiffusionQuerySet(models.QuerySet): return self.filter(program=program) def on_air(self): - return self.filter(type=Diffusion.Type.normal) + return self.filter(type=Diffusion.Type.on_air) def at(self, date=None): """ @@ -811,7 +811,7 @@ class Diffusion(models.Model): True if Diffusion is live (False if there are sounds files) """ - return self.type == self.Type.normal and \ + return self.type == self.Type.on_air and \ not self.get_sounds(archive=True).count() def get_playlist(self, **types): diff --git a/aircox/models/__init__.py b/aircox/models/__init__.py new file mode 100644 index 0000000..1fabee8 --- /dev/null +++ b/aircox/models/__init__.py @@ -0,0 +1,8 @@ +from .page import Page +from .program import Program, Stream, Schedule +from .episode import Episode, Diffusion +from .log import Log +from .sound import Sound, Track +from .station import Station, Port + + diff --git a/aircox/models/__pycache__/__init__.cpython-37.pyc b/aircox/models/__pycache__/__init__.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3e43f002f76a49801ed0b54637feb3fb80d5fc59 GIT binary patch literal 451 zcmXv~OHRWu5RLP5+9oteTp}BC0jLn49YU4D$`T4z;+B9NM|L8?k+=zG@Rk)4Ct$@) zs#=;i^Jd0=p1IDlr3dHcck}cG{u{~h_z+xy*=K;(Yl-v}VT54pOJ4;nP$3Id#G*MD z$XHQERl<^)hjO8oY&pk~T&a|$Dr4EqV|k+1Y^`#Z1E-Z&BtHaX3gW^o--{w}bZhk9 zaOL7^Gy;55H7}x_q$qqx?}pCmR=CA&x7$s&(>+`b?)4suQKhH8bV9UTrbCIDtagb099aZoTZrYL|5Bz!2rVq{UlEU<(NWYiuc%nDN} zz^7+&j1m4WOC{Qlmn|Q8+33meezYYhzY6fYGmU;fIt61%Th?{gcVk_j4<9bMfFumQ P!4+8)OvxF^NecE4yNq&- literal 0 HcmV?d00001 diff --git a/aircox/models/__pycache__/diffusion.cpython-37.pyc b/aircox/models/__pycache__/diffusion.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d64fe3c3e246352ef3b8d261e4326d4baa1efe9c GIT binary patch literal 9706 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 J>~FQ|{{ilsB254Q literal 0 HcmV?d00001 diff --git a/aircox/models/__pycache__/mixins.cpython-37.pyc b/aircox/models/__pycache__/mixins.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a182ce6e8440391b89f83b5960ea92adb94a703f GIT binary patch literal 2516 zcmbtWPj4GV6rb6D9y{wKO=%hmg@vjpDDt`p4$NQ*sPgpI6Co$rCrYY{Pf} zL-{~vqz{gA>p6wJcIOe=m}E(w#Ay^IDx0J!+IXC#xj<_jCDP;HiLArkzdvmc1yABO zI25;gqlt{OT(!q?)E6+viR_Ke%4y+cdx&PK*7#hz63wikA?O{U)17}C-ZDb}+5E$h zO2{fiTH?VlF_?_83xdhlwZcJsDRtjkp}P?4gsx^$l*L04MY<71;L0RL-jAZ6CULs> zBG*tA#*D%P`y)uVL6=M8OCxD#40dS z!RR>1PZJf~-8Cg(;uu0meJRp77u+mp|EZ8iBPAAZ?ok?N1MOh>>H4Wi#}VS$P4==pfO{>ImqSiobPFOPqrPrV_L`9@EOH^b+s~akQDhl2=d@19q$yi80D+q-4B5O`8Pn7ls zKjREgu18TXjg^X`B{t+c;K-Zk5k{5J2K8ZhmPZ>FqgP^zM_&d5lY!#_3{&Bp7zfPB zt6?bL0@=GLU%>xHC1Fd){+9~umb(=dH(ymz#OsO@G{^XXxGj>3O&roeO*&TlqZ^Qy z;=B(pbz0M^TT6-8YOKFrqr3%bKB}m?UU9E1lGTc^k{7Liz~uT2l2z6$sI{i;;EBj5 zG6Up_pz!Gu;lVE60l`R?U&5dhT8qwyrL#Ib5PC+U=zx&K9_H;ARsN3qsSV@C&B*V4;GUJMj7m4272sV8+^^7M*TZ{4+;} zTd%YqiLU9!qmdNJN%oC6*X-coVbuA4e_tXn79qt{8CV*?NGxg1;En}+=jZZ${EBs8 z;w#Y<-7J9J6DeHc%hpy}Ztf$*H#S0rn;CT|qaIM-UblUp+060b#~g`rop4QW^M0J2 zj9NXgwk6JbVvK)c)FL?b;v+v=__R~JPx2&HE$FbBf?f~u5;JO?)2#e+VlG1MKE}U6 bM%~ilWso)dY;o;=j%~(7+K1lc+gt1(bT1{# literal 0 HcmV?d00001 diff --git a/aircox/models/__pycache__/page.cpython-37.pyc b/aircox/models/__pycache__/page.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1c1631fafa2b6193e59ca3ade48b3ece8a567460 GIT binary patch literal 2862 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 E0U=h;vj6}9 literal 0 HcmV?d00001 diff --git a/aircox/models/__pycache__/sound.cpython-37.pyc b/aircox/models/__pycache__/sound.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..29f67d3e9a5b64d9326eb9c4f3030e28caa0bbac GIT binary patch literal 8580 zcmb7JTaOz@cJAtKl1&ad_t9J|%WbV}i?%e<-n@xd5k?+KUS*|`ERC}cvreanRWs9^ zCY!EqN}4ujH}Pn*b`T|4APDjhBtsw%Nq#||f&lpoK^{5}dB{VcV1c{~2%PU!H8~tv ziy$Prs=DrV&R6Hw@7HP-1Ha8*Uj5|Dvxe~>^fLU}D6HX${}l;0I17z7YcNJ-Gc+5f z%B_Zl+zRb>p;1(Ac35gV4M&v=VYyvtR8+YbPPD6ynktvV$@WxZT9ut}rajx3Rb?xj z!#o9)%e)fKw-*`%YA-exnIX)hC0-q{#xk$*Nj|k>HC8?__%xq}fv|c&1 z8mCb+!)vIiq2^W8oI%YjpF+)4Uh^6^9~kwyXE>WtXIbf1C%Mt-wKq_9Vv!_4XD3Fv z*p9desl+~{rX*;KH0lVHYrP%x6OR)w^wWJ*Ol|r*JN{NE?)V*lN62R^pZlJScBJ1% zm(q=H5M#!y=m)YHJ6}C=ygr=o>Ag4-!13Lz~3=lF$sQ zGdY6}byLnFlcy-5P3vTSSobOldUcjAoLoJh=>M^JV>Tb-c|6$O?!`gWIk}IlJZgAx zH;dSbJGPs}le-y>+o%^a$MZUVTXZZ}NhIfdECx$4rW{zeD#&rWVn2S$8 zwHtHjHIH61n029b3Zh}|Xc48Q0Xtge^L$}oE*kt4o)TYFqe^_~snb}&xK}W4Sg{{{*PfIcm-w&q_t4`qc=LUJ1-!X(Xg0q2bHg?o-@@g% zDr?|m`ent%@0zQ?+g;6G$W3wnj)hjbt2Y+=NgB z!noZi@&e9LOe<>dtDE~>2ysaxk(E{Uj08lTJ+qK))Ju}UElI#gl9yQ_qRnTee1nh` z`V3h~igxr+@R!|^%vLL5eC!}`n63Wub(+zp%l2#yMTnkC+kfnZqO+6iZe}HjLDCuN zLS6usH64=34U)Q@6}CdZvxg}lcC+e3A-AGfsC%82cSYFsNHwV&T({(#SVF!<$$2E1 zo#6B%%|OQ``Mi!Nc9HBrFn+T1WAlI=n2FIh51}9W?2v7nzc4?wA82U+;cf+;Bx7FP z%FNZv8GD?u{V@`djCfwT5(aTXyuJE;nw}_JG}39!gyY7#t#?*x?nXgV#K$|%Y(mj< zcK$i*XKXb!e~hPo?%7wEsb0$LuAl5?g|?qGceAn-&7O>b9!Y9)Y?5NH6MWheSwX3* z^t(UUymK3nuUtjZi4Ajvu;RBK&qF+b0NF#{!wdp|uG0>B6uy#BEH9 zGo68W4Yq@(z8cDUcrAqTbWhYn0|NKZv#LXNCyKaBvN%Zg(|lKL0i`&s1QN$D}7I0S`r=~h(xXZE2yCM zMjRgv$nR)2(GHpt7;i=$Uaw?k6vLc=%#q(yoQM;zP2|hWBn4kUQT0}%38{px?Jiwa zu(K)Xw(SYef8P;c3+mXi?1zxX#KnM73;Tb#=83dy-Yns+mq{ zaF2StoSZJkk^OPR60>Cy=>|knvju^*2&-h#0O9QP;)o8pK?d2`lOe%YaWIj>Z;$$9 zh7x7Z*`<0x(M8@Q;?&xH(D8<5ZC1H+@-u=6GH?y)Xe{}!$(AFVxzIa>gtoHGP8wy|1kK{Hb4f=2gsR@!C^Ta+B%Kh<0?{+9L7E$krEM2_{W#5;;=a z`M#@s4ma9riDp7PiF?ha5d6YuD)>Rd-N4n*8kjEghPs?18<$8(4>2V^gX9a4vJao) z0Na);eL6B*h9w5V61+`49?H+cZGj%K_QZZ>lXsC79{ORA{4ATcuK`IaC3DopLfxOz zDhiOS!qPX!I6OS?>Xk5R`eA%^bvUL`@WZWMJG1wNFVoUz2k!aL4(jrUn5r?6kk~*3?oq8cFC<2lP5(gyuWdnod@6gulptBv-EAl?Ql-j)n zZeS;~ZvvAVyjGk@_(aepkXutU?j1_bQgQ(a)SyE3?@`6alz5a7JpuS1QCZQTrhq&= z=1)+s{6eDu`@(dX$);J6RZz0c>4J-V-n3a2|4Ek4y@XJs%Rt;ClCIN~e~To)4DZq{ z#sg8c2Tsz}fP60ijz+6EFd2z5wDk=So+P+yLnDljNbpvY35STI5zr5KZ{;%owd&-`un0-}F)TY8)-zq*}LtHnlFT)+gywOn)&#>iVu|?hVyB zi0=|#Tz>~r@GaNXs{^x{RUN6pz~xBKZ4Pn)pDy9zBvpgNtaW;M#*nd za*Mi8!EqHUepY=Cehl9IvzptuhBmyW78-)z?zMxV%nB% z&I5@{tYSH41<0hoW7_5rizntdfkpBm6&627f%FNa2yhawBvuRfL)jj%_>96M5P|p7 zvH@{Dn^+2CHmnn%69Rk9Hj?sQMfMqx#fa)`uD%Fd6}s47v3a zh;}XDX$@F1cSh*YD5GP$T_M{+Ob&Z|0#{0qDm|^jC~%mxX*QZ;BRvn-NP!Ei9-juH z2|8ZQ{@KBME~M0?q}!P-Ub}UF{qD!!gS#JXT=%Yhw0YNC|G}NR*A*0w6{Y-;Ixn5* ztonI3Z(sZ1ft;r1LUR{}Xf}0kwEr7YMlgg_BM^mhQiJaqfjLIV^$%TW+9mzHdM!By+>RpD`F&puHywH^Vn>iNWKlpC z80y`o9s_V|8GSv~pCl8v1|&7Krm?lAQ9|}PbL&MdbJbm0!y=ip<->)AJBILI7lt=E zPh>m2+|FnqSfV#iL;eN*vfrC5l2ASgv`f=-YiQZwtR>Bnau-c{R|J8@RrgI%lQmt}NRNe1Crl3at5+B)bm}nL>yR1T*elC3!~~QaRigcq;AZme;r@|ZbdJQv>0t5n zEjr!sR7D+IA~7Xflsu;7Arct%BxbcDB!EJ^$bD)}k$@eaQJFNNQiZaQ(!b-0DWGS7 z_!Wp9M5EZy)2Hc1#3ytW@T}ue#OI7G6tYt}Fa^{YKQotg6x7;~x6$p{|9th;S73wP zih_<-;{^qgmqQ~fc^fZoAqzMFg+cTM@^uLwy!l24a&;vyS}^`h>+_YoTbXj%}9!1r}IOxFG-+h&Vw79 z-o{6_Z%YM$37UgKtl7+iwjYLw#S5r2HBB~w5G6V25*W@3xR~u&slTjt6O#8Hl$lLg9CGw_S8oVpRp+vx*;kZT%%6D`~Sj*4p+SbtD^6a!M5b zC6&}ApxdcXE&qT@#Dc7rU*o0-!{JpX#tp9n8HEZF=TL}gR>!v4ba57UffQ~P=3hmn zMTp=E+KN!fD`=$_lwX=xNqNI)bfC7K38&(eoY$Rm&Wuy4+LO);(Oz<)=okMI$#@J- zAW9&LNK3d~CgPnI;+ZyLwPn>(8Eb)4q*^9WS4B^_N5kkZFC+e0%j@9)<#Xw{uicnL z3qI`({IkhMMM!7Ri4$Vy9r-+ z=sT#`Odb@a@aSWTq{f1)sbDXX=`iXWboBdZT7*q?mv%&vAXvFDZ7DngBNo;w8GnLn zYjpGFkwEzgn1#Tmnh{nX2DV};h=gP%G5N@mNsG9%`P21R^6uPe*Tc^yiLNgDBU zmRK++VAX45DUTwgug1#0{`l^FH~)r4oX?}A9I-IMxmGci-=esJHgn0|P@;Bd*!~}O z#tI$L!633T=9P`1kEu+SJv`RCOAXOxU$4wAMb|AG>al0!;}9a@F_8I_cZ|C+iW7W_A!_&Z2QT?|#k zP*Ip;N?}%6?xZSAQWo}P!LjRZW($N%lns!T$Ok3du3@@tg16u^MyrrU+sbW`cc{9Y zSMx1Jmu!0Uz6w7C0VegKIIHH>JwyXzNm!TbG<<2iel@`d;~24diouZfQ6z0lL8j~k z$!e~8V@)734z;mC28cF36+bADds}b)@tw(7nJ6>8_h27;>tE1g|ASsIz4fG1?>Y7N79dDD_95h8v3R>!ytnV`_x9@S zY+b|e_WwTm`roHD?Z5Oe{#oc;#w8!4VH(pTt*?8!PJJUXJOg(#GW(WiDVh~k`nG4Q zekH2*9j{i*t@mfV8T4&dWlqQR8Xs${#;OlBR^?W2_JQfmfl_BSC^jhbpe%qg!yHhY zg0jerEv?l!1~*zu&#E`$^m;t(Z=i4Y6UHNnp8gq{*BZD|Ku&%l^OsDZ7PnK_AI zT=+ZtS!FjAGR^GWFiN>-8JQ({w3}6n@BdvB^LXX^i)($(!eEUBX|UE#h9Zblxi%0< zkK-PMqMh6y-*P3a6)UU`u=52Iw3M{bXcb3a&_`#taw*4(SS{wY^o^F4IldnUeeU~N z-S@HRFrt3L_wNjYC?64vSW9s@)hrN=R5OQ5l2#o3gmM)JG?t+$l?%1P!YbTm#si(1 zP>YqXX3_Gj(XVG6~=Tv_g{T23->YrmT zv-4PEg}INkU7fwcUVT{eUU~#8^j?O6z9vZ0;4NL#lu2yzf#8xub8g^@fQ8A}%r3ZT z;@<0q?XKI0U4?^)yHIXN1bxgE+zrw+Xm>eN>jW`#({7Nup@cnh<^~ea9BT~RWT(g5 zsk@s9m*0hZrCr$TYF)i|#Vz#hc9V!nx1GePNTP@fsa~Kpf?>{<;6dN*@R$qbvsgRH z?RvWl5pt#^Pu<}Fe8cWz&KWp)2VBuI%BtO<-R2U!B}9|lg##f2?8m;c>Mtj|M&kqkN6ZP87s@qxr(($BG!e7w)>)==L6GP)Zj&T=>zmo81pW z~jsUKN=1sBCJxr>G&f>Gf93;3Z}6d8T<5JCL0-m~P+W35+ttRm85 z-M8>$mrqs9sCM*6+9Q3}U=;*s=Rof{Jp^v%9O>6!xM#77UuTGHt=fhl7LKXG9#(JX zMuf;i&Uk`7vdvSt_w(1e@SH8R z5EtS#nzAsN!gv6m$f~(7NztJ3#mTr-!b|Lnl9IllL8S5Zl9ulYl{eha#!K}~Pq zZ|igT+j_$ot-N?MO2v_0%N)0a2jnuXr>92G>{+le8Il81gj8a;W&w?m|JEq7{ zNM?g!>JE@{k-v78yB0QHz64jxYE1bMC2Tu?vsK9)dd-L9t4$FOILK<(%83=VHmq%; ze#$w5>rTX%SODMFSA=GwPFyl{Df4 zdYPRs@B%@q`KY{wsktPkl4Oi)R*UfyqEJLwrx(bV2$4KRB!Kn^$><~3y^;ss+_WwL zVUP_%SM6v>ffWi_nONYn&LRlzyolSvT)&`?lkI4xqmN2ni_ss!>yrC10)N3^;dJ0n z{2WV*pP+dGBX<7E*O)Qkqtt#r_#@bOz1Xse+m}hKFz1q~w_ePYGmZ!C6KpMvjj|jj z&!ZpR=~B9+7)l;UIs^a~!0s=SzN$hG!WO2e@TW~Fp%^QnETs-ol>%&kLWe#gS5m#F zJuyum= z;YlTq%EmVMr%Z+$q0}3#c|n;WGZL9uy(Ek?2Y#O-yUOg9D>paSZ++>n-`d#Ty!Gj) z*EhHPFK%sqa&zNj|Juz>v5Xh1)B zM^f=HNT|h zSJeEP8WJy?Ed+1#C>je-mIBRPp-B{N74$rZj-<65$I_en61IXi#LyjsdWPOGor(pj zWjKvSz2P)$`a32$L-`{W(og;e%`{v`X-M@L%1>^gRIK3HVii1B)U(ZDl)T0P&jHn| zy%`1H8|ck~2KbKRv;I)ymEPQw`KKBo8NhhMTPS7_jGt9A>}O^yVn!2d%wfhn?hCjt z0_dLteF@J^e6P+<(J1~)C=E`t)9ehO`WXe(&$8u*j(3)wV=JI7E5LpZKKznO0m19= zZS}XDkp=?6C)1X?^L7I_N|HT# z{1k{bWLrrvcw4%#z99i+1-;dR<1Zxxs-OYC?*?HM?4V3gNFN~D1@2{(&!;&ozP^?8 zQBNRqyXpP_;JqL0yF1*ClXPk!#s@rR1fG+aq*C84F~Arp&?-4vKVc&j5igUCkONKI z2s1p3nb=!vGVshQ^JynnMEvoi=M&@B;({S zwm?^y9H^jyDsLx|T-EAXEs1>`!EicDz@##r67$6NIxgv=QGkE>E8|cEd(DEBFxvYs8n-Of043=_%+1PYUjBk4~wYj;xpp zx&qfum2jh3H8#$4qdh7<$oNsa^aEE?k%E-PT)IntEzLZoLW^>C#M2!-cr6jRUK@LA?kv>79jPgy%|eZ!m9j)s|*Q~ zbFNk`N7Uq(x$An=Q)YULdYjbTrsgv=$Wza&4`xH@-wmRW`NYCMFj1;g(aIcbziC?f zsHx2A#I$mUXm{u$E07dba*Qn(Qk()l(Ep4Y382S3&<~972y+`7qxY4n$MULZ7)8lF zDED4SM+#72F0z_e%H6WWS6Dx*e;!0beqD$}h>IYo`cWA~*xpZwc?VlKt z4jN5&kcVxv2t0&)l8z(_IQu_@NNuf$H zIn26Ak7Ya=t5Ag%QraP|99PvXeZg9&S)@a~u29u7wT5a5VJVtz)=g)w;k25W#c^1k zi~K&8L7CkP;!d*4c5=A6P(H8b2f2d6W42t7hiMqe)fDHy5-|f= self.start: + raise ValidationError({ + 'initial': _('rerun must happen after initial') + }) + + +# BIG FIXME: self.date is still used as datetime +class Schedule(BaseRerun): + """ + A Schedule defines time slots of programs' diffusions. It can be an initial + run or a rerun (in such case it is linked to the related schedule). + """ + # Frequency for schedules. Basically, it is a mask of bits where each bit is + # a week. Bits > rank 5 are used for special schedules. + # Important: the first week is always the first week where the weekday of + # the schedule is present. + # For ponctual programs, there is no need for a schedule, only a diffusion + class Frequency(IntEnum): + ponctual = 0b000000 + first = 0b000001 + second = 0b000010 + third = 0b000100 + fourth = 0b001000 + last = 0b010000 + first_and_third = 0b000101 + second_and_fourth = 0b001010 + every = 0b011111 + one_on_two = 0b100000 + + date = models.DateField( + _('date'), help_text=_('date of the first diffusion'), + ) + time = models.TimeField( + _('time'), help_text=_('start time'), + ) + timezone = models.CharField( + _('timezone'), + default=tz.get_current_timezone, max_length=100, + choices=[(x, x) for x in pytz.all_timezones], + help_text=_('timezone used for the date') + ) + duration = models.TimeField( + _('duration'), + help_text=_('regular duration'), + ) + frequency = models.SmallIntegerField( + _('frequency'), + choices=[(int(y), { + 'ponctual': _('ponctual'), + 'first': _('1st {day} of the month'), + 'second': _('2nd {day} of the month'), + 'third': _('3rd {day} of the month'), + 'fourth': _('4th {day} of the month'), + 'last': _('last {day} of the month'), + 'first_and_third': _('1st and 3rd {day}s of the month'), + 'second_and_fourth': _('2nd and 4th {day}s of the month'), + 'every': _('every {day}'), + 'one_on_two': _('one {day} on two'), + }[x]) for x, y in Frequency.__members__.items()], + ) + + + class Meta: + verbose_name = _('Schedule') + verbose_name_plural = _('Schedules') + + def __str__(self): + return '{} - {}, {}'.format( + self.program.title, self.get_frequency_verbose(), + self.time.strftime('%H:%M') + ) + + def save_rerun(self, *args, **kwargs): + self.program = self.initial.program + self.duration = self.initial.duration + self.frequency = self.initial.frequency + + @cached_property + def tz(self): + """ Pytz timezone of the schedule. """ + import pytz + return pytz.timezone(self.timezone) + + @cached_property + def start(self): + """ Datetime of the start (timezone unaware) """ + return tz.datetime.combine(self.date, self.time) + + @cached_property + def end(self): + """ Datetime of the end """ + return self.start + utils.to_timedelta(self.duration) + + def get_frequency_verbose(self): + """ Return frequency formated for display """ + from django.template.defaultfilters import date + return self.get_frequency_display().format( + day=date(self.date, 'l') + ) + + # initial cached data + __initial = None + + def changed(self, fields=['date', 'duration', 'frequency', 'timezone']): + initial = self._Schedule__initial + + if not initial: + return + + this = self.__dict__ + + for field in fields: + if initial.get(field) != this.get(field): + return True + + return False + + def match(self, date=None, check_time=True): + """ + Return True if the given date(time) matches the schedule. + """ + date = utils.date_or_default( + date, tz.datetime if check_time else datetime.date) + + if self.date.weekday() != date.weekday() or \ + not self.match_week(date): + return False + + # we check against a normalized version (norm_date will have + # schedule's date. + return date == self.normalize(date) if check_time else True + + def match_week(self, date=None): + """ + Return True if the given week number matches the schedule, False + otherwise. + If the schedule is ponctual, return None. + """ + + if self.frequency == Schedule.Frequency.ponctual: + return False + + # since we care only about the week, go to the same day of the week + date = utils.date_or_default(date, datetime.date) + date += tz.timedelta(days=self.date.weekday() - date.weekday()) + + # FIXME this case + + if self.frequency == Schedule.Frequency.one_on_two: + # cf notes in date_of_month + diff = date - utils.cast_date(self.date, datetime.date) + + return not (diff.days % 14) + + first_of_month = date.replace(day=1) + week = date.isocalendar()[1] - first_of_month.isocalendar()[1] + + # weeks of month + + if week == 4: + # fifth week: return if for every week + + return self.frequency == self.Frequency.every + + return (self.frequency & (0b0001 << week) > 0) + + def normalize(self, date): + """ + Return a new datetime with schedule time. Timezone is handled + using `schedule.timezone`. + """ + date = tz.datetime.combine(date, self.time) + return self.tz.normalize(self.tz.localize(date)) + + def dates_of_month(self, date): + """ Return normalized diffusion dates of provided date's month. """ + if self.frequency == Schedule.Frequency.ponctual: + return [] + + sched_wday, freq = self.date.weekday(), self.frequency + date = date.replace(day=1) + + # last of the month + if freq == Schedule.Frequency.last: + date = date.replace( + day=calendar.monthrange(date.year, date.month)[1]) + date_wday = date.weekday() + + # end of month before the wanted weekday: move one week back + + if date_wday < sched_wday: + date -= tz.timedelta(days=7) + date += tz.timedelta(days=sched_wday - date_wday) + + return [self.normalize(date)] + + # move to the first day of the month that matches the schedule's weekday + # check on SO#3284452 for the formula + date_wday, month = date.weekday(), date.month + date += tz.timedelta(days=(7 if date_wday > sched_wday else 0) - + date_wday + sched_wday) + + if freq == Schedule.Frequency.one_on_two: + # - adjust date with modulo 14 (= 2 weeks in days) + # - there are max 3 "weeks on two" per month + if (date - self.date).days % 14: + date += tz.timedelta(days=7) + dates = (date + tz.timedelta(days=14*i) for i in range(0, 3)) + else: + dates = (date + tz.timedelta(days=7*week) for week in range(0, 5) + if freq & (0b1 << week)) + + return [self.normalize(date) for date in dates if date.month == month] + + + def _exclude_existing_date(self, dates): + from .episode import Diffusion + saved = set(Diffusion.objects.filter(start__in=dates) + .values_list('start', flat=True)) + return [date for date in dates if date not in saved] + + + def diffusions_of_month(self, date): + """ + Get episodes and diffusions for month of provided date, including + reruns. + :returns: tuple([Episode], [Diffusion]) + """ + from .episode import Diffusion, Episode + if self.initial is not None or \ + self.frequency == Schedule.Frequency.ponctual: + return [] + + # dates for self and reruns as (date, initial) + reruns = [(rerun, rerun.date - self.date) + for rerun in self.rerun_set.all()] + + dates = OrderedDict((date, None) for date in self.dates_of_month(date)) + dates.update([(rerun.normalize(date.date() + delta), date) + for date in dates.keys() for rerun, delta in reruns]) + + # remove dates corresponding to existing diffusions + saved = set(Diffusion.objects.filter(start__in=dates.keys(), + program=self.program) + .values_list('start', flat=True)) + + # make diffs + duration = utils.to_timedelta(self.duration) + diffusions = {} + episodes = {} + + for date, initial in dates.items(): + if date in saved: + continue + + if initial is None: + episode = Episode.from_date(self.program, date) + episodes[date] = episode + else: + episode = episodes[initial] + initial = diffusions[initial] + + diffusions[date] = Diffusion( + episode=episode, type=Diffusion.Type.on_air, + initial=initial, start=date, end=date+duration + ) + return episodes.values(), diffusions.values() + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # TODO/FIXME: use validators? + 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): + """ + When there are no program scheduled, it is possible to play sounds + in order to avoid blanks. A Stream is a Program that plays this role, + and whose linked to a Stream. + + All sounds that are marked as good and that are under the related + program's archive dir are elligible for the sound's selection. + """ + program = models.ForeignKey( + Program, models.CASCADE, + verbose_name=_('related program'), + ) + delay = models.TimeField( + _('delay'), blank=True, null=True, + help_text=_('minimal delay between two sound plays') + ) + begin = models.TimeField( + _('begin'), blank=True, null=True, + help_text=_('used to define a time range this stream is' + 'played') + ) + end = models.TimeField( + _('end'), + blank=True, null=True, + help_text=_('used to define a time range this stream is' + 'played') + ) + + diff --git a/aircox/models/sound.py b/aircox/models/sound.py new file mode 100644 index 0000000..0238d26 --- /dev/null +++ b/aircox/models/sound.py @@ -0,0 +1,288 @@ +from enum import IntEnum +import logging +import os + +from django.conf import settings as main_settings +from django.db import models +from django.db.models import Q +from django.utils import timezone as tz +from django.utils.translation import ugettext_lazy as _ + +from taggit.managers import TaggableManager + +from aircox import settings +from .program import Program +from .episode import Episode + + +logger = logging.getLogger('aircox') + + +__all__ = ['Sound', 'SoundQuerySet', 'Track'] + + +class SoundQuerySet(models.QuerySet): + def podcasts(self): + """ Return sound available as podcasts """ + return self.filter(Q(embed__isnull=False) | Q(is_public=True)) + + def episode(self, episode): + return self.filter(episode=episode) + + def diffusion(self, diffusion): + return self.filter(episode__diffusion=diffusion) + + +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, + + name = models.CharField(_('name'), max_length=64) + program = models.ForeignKey( + Program, models.SET_NULL, blank=True, null=True, + verbose_name=_('program'), + help_text=_('program related to it'), + ) + episode = models.ForeignKey( + 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 + ) + # FIXME: url() does not use the same directory than here + # should we use FileField for more reliability? + path = models.FilePathField( + _('file'), + path=settings.AIRCOX_PROGRAMS_DIR, + match=r'(' + '|'.join(settings.AIRCOX_SOUND_FILE_EXT) + .replace('.', r'\.') + ')$', + recursive=True, max_length=255, + blank=True, null=True, unique=True, + ) + embed = models.TextField( + _('embed'), + blank=True, null=True, + help_text=_('HTML code to embed a sound from an external plateform'), + ) + duration = models.TimeField( + _('duration'), + blank=True, null=True, + help_text=_('duration of the sound'), + ) + mtime = models.DateTimeField( + _('modification time'), + blank=True, null=True, + help_text=_('last modification date and time'), + ) + is_good_quality = models.BooleanField( + _('good quality'), help_text=_('sound meets quality requirements'), + blank=True, null=True + ) + is_public = models.BooleanField( + _('public'), help_text=_('if it can be podcasted from the server'), + default=False, + ) + + objects = SoundQuerySet.as_manager() + + def get_mtime(self): + """ + Get the last modification date from file + """ + mtime = os.stat(self.path).st_mtime + mtime = tz.datetime.fromtimestamp(mtime) + # db does not store microseconds + mtime = mtime.replace(microsecond=0) + + return tz.make_aware(mtime, tz.get_current_timezone()) + + def url(self): + """ + Return an url to the stream + """ + # path = self._meta.get_field('path').path + path = self.path.replace(main_settings.MEDIA_ROOT, '', 1) + #path = self.path.replace(path, '', 1) + + return main_settings.MEDIA_URL + '/' + path + + def file_exists(self): + """ + Return true if the file still exists + """ + + return os.path.exists(self.path) + + def file_metadata(self): + """ + Get metadata from sound file and return a Track object if succeed, + else None. + """ + if not self.file_exists(): + return None + + import mutagen + try: + meta = mutagen.File(self.path) + except: + meta = {} + + if meta is None: + meta = {} + + def get_meta(key, cast=str): + value = meta.get(key) + return cast(value[0]) if value else None + + info = '{} ({})'.format(get_meta('album'), get_meta('year')) \ + if meta and ('album' and 'year' in meta) else \ + get_meta('album') \ + if 'album' else \ + ('year' in meta) and get_meta('year') or '' + + return Track(sound=self, + position=get_meta('tracknumber', int) or 0, + title=get_meta('title') or self.name, + artist=get_meta('artist') or _('unknown'), + info=info) + + def check_on_file(self): + """ + Check sound file info again'st self, and update informations if + needed (do not save). Return True if there was changes. + """ + + if not self.file_exists(): + if self.type == self.Type.removed: + return + logger.info('sound %s: has been removed', self.path) + self.type = self.Type.removed + + return True + + # not anymore removed + changed = False + + if self.type == self.Type.removed and self.program: + changed = True + self.type = self.Type.archive \ + if self.path.startswith(self.program.archives_path) else \ + self.Type.excerpt + + # check mtime -> reset quality if changed (assume file changed) + mtime = self.get_mtime() + + if self.mtime != mtime: + self.mtime = mtime + self.is_good_quality = None + logger.info('sound %s: m_time has changed. Reset quality info', + self.path) + + return True + + return changed + + def check_perms(self): + """ + Check file permissions and update it if the sound is public + """ + + if not settings.AIRCOX_SOUND_AUTO_CHMOD or \ + self.removed or not os.path.exists(self.path): + + return + + flags = settings.AIRCOX_SOUND_CHMOD_FLAGS[self.is_public] + try: + os.chmod(self.path, flags) + except PermissionError as err: + logger.error('cannot set permissions {} to file {}: {}'.format( + self.flags[self.is_public], self.path, err)) + + def __check_name(self): + if not self.name and self.path: + # FIXME: later, remove date? + self.name = os.path.basename(self.path) + self.name = os.path.splitext(self.name)[0] + self.name = self.name.replace('_', ' ') + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.__check_name() + + def save(self, check=True, *args, **kwargs): + if self.episode is not None and self.program is None: + self.program = self.episode.program + if check: + self.check_on_file() + self.__check_name() + super().save(*args, **kwargs) + + def __str__(self): + return '/'.join(self.path.split('/')[-3:]) + + class Meta: + verbose_name = _('Sound') + verbose_name_plural = _('Sounds') + + +class Track(models.Model): + """ + Track of a playlist of an object. The position can either be expressed + as the position in the playlist or as the moment in seconds it started. + """ + episode = models.ForeignKey( + Episode, models.CASCADE, blank=True, null=True, + verbose_name=_('episode'), + ) + sound = models.ForeignKey( + Sound, models.CASCADE, blank=True, null=True, + verbose_name=_('sound'), + ) + position = models.PositiveSmallIntegerField( + _('order'), + default=0, + help_text=_('position in the playlist'), + ) + timestamp = models.PositiveSmallIntegerField( + _('timestamp'), + blank=True, null=True, + help_text=_('position in seconds') + ) + title = models.CharField(_('title'), max_length=128) + artist = models.CharField(_('artist'), max_length=128) + tags = TaggableManager(verbose_name=_('tags'), blank=True,) + info = models.CharField( + _('information'), + max_length=128, + blank=True, null=True, + help_text=_('additional informations about this track, such as ' + 'the version, if is it a remix, features, etc.'), + ) + + class Meta: + verbose_name = _('Track') + verbose_name_plural = _('Tracks') + ordering = ('position',) + + def __str__(self): + return '{self.artist} -- {self.title} -- {self.position}'.format( + self=self) + + def save(self, *args, **kwargs): + if (self.sound is None and self.episode is None) or \ + (self.sound is not None and self.episode is not None): + raise ValueError('sound XOR episode is required') + super().save(*args, **kwargs) + + diff --git a/aircox/models/station.py b/aircox/models/station.py new file mode 100644 index 0000000..808ea62 --- /dev/null +++ b/aircox/models/station.py @@ -0,0 +1,206 @@ +from enum import IntEnum +import os + +from django.db import models +from django.db.models import Q +from django.utils.translation import ugettext_lazy as _ + +import aircox.settings as settings + + +__all__ = ['Station', 'StationQuerySet', 'Port'] + + +class StationQuerySet(models.QuerySet): + def default(self, station=None): + """ + Return station model instance, using defaults or + given one. + """ + if station is None: + return self.order_by('-default', 'pk').first() + return self.filter(pk=station).first() + + +class Station(models.Model): + """ + Represents a radio station, to which multiple programs are attached + and that is used as the top object for everything. + + A Station holds controllers for the audio stream generation too. + Theses are set up when needed (at the first access to these elements) + then cached. + """ + name = models.CharField(_('name'), max_length=64) + slug = models.SlugField(_('slug'), max_length=64, unique=True) + path = models.CharField( + _('path'), + help_text=_('path to the working directory'), + max_length=256, + blank=True, + ) + default = models.BooleanField( + _('default station'), + default=True, + help_text=_('if checked, this station is used as the main one') + ) + + objects = StationQuerySet.as_manager() + + # + # Controllers + # + __sources = None + __dealer = None + __streamer = None + + def __prepare_controls(self): + import aircox.controllers as controllers + from .program import Program + if not self.__streamer: + self.__streamer = controllers.Streamer(station=self) + self.__dealer = controllers.Source(station=self) + self.__sources = [self.__dealer] + [ + controllers.Source(station=self, program=program) + + for program in Program.objects.filter(stream__isnull=False) + ] + + @property + def inputs(self): + """ + Return all active input ports of the station + """ + return self.port_set.filter( + direction=Port.Direction.input, + active=True + ) + + @property + def outputs(self): + """ Return all active output ports of the station """ + return self.port_set.filter( + direction=Port.Direction.output, + active=True, + ) + + @property + def sources(self): + """ Audio sources, dealer included """ + self.__prepare_controls() + return self.__sources + + @property + def dealer(self): + """ Get dealer control """ + self.__prepare_controls() + return self.__dealer + + @property + def streamer(self): + """ Audio controller for the station """ + self.__prepare_controls() + return self.__streamer + + def __str__(self): + return self.name + + def save(self, make_sources=True, *args, **kwargs): + if not self.path: + self.path = os.path.join( + settings.AIRCOX_CONTROLLERS_WORKING_DIR, + self.slug + ) + + if self.default: + qs = Station.objects.filter(default=True) + + if self.pk: + qs = qs.exclude(pk=self.pk) + qs.update(default=False) + + super().save(*args, **kwargs) + + +class Port (models.Model): + """ + Represent an audio input/output for the audio stream + generation. + + You might want to take a look to LiquidSoap's documentation + for the options available for each kind of input/output. + + Some port types may be not available depending on the + direction of the port. + """ + class Direction(IntEnum): + input = 0x00 + output = 0x01 + + class Type(IntEnum): + jack = 0x00 + alsa = 0x01 + pulseaudio = 0x02 + icecast = 0x03 + http = 0x04 + https = 0x05 + file = 0x06 + + station = models.ForeignKey( + Station, + verbose_name=_('station'), + on_delete=models.CASCADE, + ) + 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()], + ) + active = models.BooleanField( + _('active'), + default=True, + help_text=_('this port is active') + ) + settings = models.TextField( + _('port settings'), + help_text=_('list of comma separated params available; ' + 'this is put in the output config file as raw code; ' + 'plugin related'), + blank=True, null=True + ) + + def is_valid_type(self): + """ + Return True if the type is available for the given direction. + """ + + if self.direction == self.Direction.input: + return self.type not in ( + self.Type.icecast, self.Type.file + ) + + return self.type not in ( + self.Type.http, self.Type.https + ) + + def save(self, *args, **kwargs): + if not self.is_valid_type(): + raise ValueError( + "port type is not allowed with the given port direction" + ) + + return super().save(*args, **kwargs) + + def __str__(self): + return "{direction}: {type} #{id}".format( + direction=self.get_direction_display(), + type=self.get_type_display(), + id=self.pk or '' + ) + + + diff --git a/aircox/settings.py b/aircox/settings.py index 1382f60..682d776 100755 --- a/aircox/settings.py +++ b/aircox/settings.py @@ -33,6 +33,15 @@ ensure('AIRCOX_PROGRAMS_DIR', ensure('AIRCOX_DATA_DIR', os.path.join(settings.PROJECT_ROOT, 'data')) + +######################################################################## +# Programs & Episodes +######################################################################## +# default title for episodes +ensure('AIRCOX_EPISODE_TITLE', '{program.title} - {date}') +# date format in episode title (python's strftime) +ensure('AIRCOX_EPISODE_TITLE_DATE_FORMAT', '%-d %B %Y') + ######################################################################## # Logs & Archives ######################################################################## diff --git a/aircox_cms/__init__.py b/aircox_cms/__init__.py deleted file mode 100755 index 3ab7718..0000000 --- a/aircox_cms/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ - -default_app_config = 'aircox_cms.apps.AircoxCMSConfig' - diff --git a/aircox_cms/admin.py b/aircox_cms/admin.py deleted file mode 100755 index 3c21844..0000000 --- a/aircox_cms/admin.py +++ /dev/null @@ -1,6 +0,0 @@ -from django.contrib import admin - -# Register your models here. - - - diff --git a/aircox_cms/apps.py b/aircox_cms/apps.py deleted file mode 100755 index 03bc796..0000000 --- a/aircox_cms/apps.py +++ /dev/null @@ -1,9 +0,0 @@ -from django.apps import AppConfig - -class AircoxCMSConfig(AppConfig): - name = 'aircox_cms' - verbose_name = 'Aircox CMS' - - def ready(self): - import aircox_cms.signals - diff --git a/aircox_cms/forms.py b/aircox_cms/forms.py deleted file mode 100755 index e5cbdbf..0000000 --- a/aircox_cms/forms.py +++ /dev/null @@ -1,44 +0,0 @@ -import django.forms as forms -from django.utils.translation import ugettext as _, ugettext_lazy -from django.core.exceptions import ValidationError - -from honeypot.decorators import verify_honeypot_value - -import aircox_cms.models as models - - -class CommentForm(forms.ModelForm): - class Meta: - model = models.Comment - fields = ['author', 'email', 'url', 'content'] - localized_fields = '__all__' - widgets = { - 'author': forms.TextInput(attrs={ - 'placeholder': _('your name'), - }), - 'email': forms.TextInput(attrs={ - 'placeholder': _('your email (optional)'), - }), - 'url': forms.URLInput(attrs={ - 'placeholder': _('your website (optional)'), - }), - 'comment': forms.TextInput(attrs={ - 'placeholder': _('your comment'), - }) - } - - def __init__(self, *args, **kwargs): - self.request = kwargs.pop('request', None) - self.page = kwargs.pop('object', None) - super().__init__(*args, **kwargs) - - def clean(self): - super().clean() - if self.request: - if verify_honeypot_value(self.request, 'hp_website'): - raise ValidationError(_('You are a bot, that is not cool')) - - if not self.object: - raise ValidationError(_('No publication found for this comment')) - - diff --git a/aircox_cms/locale/fr/LC_MESSAGES/django.mo b/aircox_cms/locale/fr/LC_MESSAGES/django.mo deleted file mode 100644 index 35f8867dc8b5c01e9c4b0776acfcd007c5e80728..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 19109 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 diff --git a/aircox_cms/locale/fr/LC_MESSAGES/django.po b/aircox_cms/locale/fr/LC_MESSAGES/django.po deleted file mode 100755 index dddc174..0000000 --- a/aircox_cms/locale/fr/LC_MESSAGES/django.po +++ /dev/null @@ -1,1069 +0,0 @@ -# French translation of Aircox -# Copyright (C) Aarys -# Copyright (C) Bkfox -# This file is distributed under the same license as the Aircox package. -# Aarys, 2016. -# -msgid "" -msgstr "" -"Project-Id-Version: Aircox 0.1\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-02-12 00:34+0100\n" -"PO-Revision-Date: 2016-10-10 16:00+02\n" -"Last-Translator: Aarys\n" -"Language-Team: Aircox's translators team\n" -"Language: \n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=(n > 1);\n" - -#: aircox_cms/forms.py:17 -msgid "your name" -msgstr "nom" - -#: aircox_cms/forms.py:20 -msgid "your email (optional)" -msgstr "email (optionnel)" - -#: aircox_cms/forms.py:23 -msgid "your website (optional)" -msgstr "site internet (optionnel)" - -#: aircox_cms/forms.py:26 -msgid "your comment" -msgstr "commentaire" - -#: aircox_cms/forms.py:39 -msgid "You are a bot, that is not cool" -msgstr "Vous êtes un robot. Pas cool !" - -#: aircox_cms/forms.py:42 -msgid "No publication found for this comment" -msgstr "Aucune publication n'a été trouvée pour ce commentaire" - -#: aircox_cms/models/__init__.py:43 -msgid "aircox station" -msgstr "station aircox" - -#: aircox_cms/models/__init__.py:48 -msgid "" -"refers to an Aircox's station; it is used to make the link between the " -"website and Aircox" -msgstr "" -"fait référence à une station Aircox; utilisé pour faire le lien entre le " -"site internet et Aircox" - -#: aircox_cms/models/__init__.py:55 -msgid "favicon" -msgstr "favicon" - -#: aircox_cms/models/__init__.py:57 -msgid "small logo for the website displayed in the browser" -msgstr "petit logo pour le site affiché dans le navigateur" - -#: aircox_cms/models/__init__.py:60 aircox_cms/models/__init__.py:378 -msgid "tags" -msgstr "tags" - -#: aircox_cms/models/__init__.py:63 -msgid "tags describing the website; used for referencing" -msgstr "tags pour décrire le site internet; utilisés pour le référencement" - -#: aircox_cms/models/__init__.py:66 -msgid "public description" -msgstr "description publique" - -#: aircox_cms/models/__init__.py:69 -msgid "public description of the website; used for referencing" -msgstr "description publique du site internet; utilisée pour le référencement" - -#: aircox_cms/models/__init__.py:73 -msgid "page for lists" -msgstr "page pour les listes" - -#: aircox_cms/models/__init__.py:74 -msgid "page used to display the results of a search and other lists" -msgstr "" -"page utilisée pour afficher les résultats d'une recherche et d'autres listes" - -#: aircox_cms/models/__init__.py:82 aircox_cms/models/__init__.py:86 -msgid "publish comments automatically without verifying" -msgstr "publier les commentaires automatiquement sans vérification" - -#: aircox_cms/models/__init__.py:89 -msgid "success message" -msgstr "message de réussite" - -#: aircox_cms/models/__init__.py:90 -msgid "Your comment has been successfully posted!" -msgstr "Votre commentaire a bien été posté !" - -#: aircox_cms/models/__init__.py:92 -msgid "message displayed when a comment has been successfully posted" -msgstr "message à afficher quand un commentaire a bien été posté" - -#: aircox_cms/models/__init__.py:96 -msgid "waiting message" -msgstr "message d'attente" - -#: aircox_cms/models/__init__.py:97 -msgid "Your comment is awaiting for approval." -msgstr "Votre message est en attente d'approbation" - -#: aircox_cms/models/__init__.py:99 -msgid "" -"message displayed when a comment has been sent, but waits for website " -"administrators' approval." -msgstr "" -"message affiché quand un commentaire a été envoyé, mais est en attente de " -"l'approbation des administrateurs du site" - -#: aircox_cms/models/__init__.py:104 -msgid "error message" -msgstr "message d'erreur" - -#: aircox_cms/models/__init__.py:105 -msgid "We could not save your message. Please correct the error(s) below." -msgstr "" -"Votre message n'a pas pu être sauvegardé. Veuillez corriger l'erreur suivante" - -#: aircox_cms/models/__init__.py:107 -msgid "" -"message displayed when the form of the comment has been submitted but there " -"is an error, such as an incomplete field" -msgstr "" -"message affiché quand le formulaire a été envoyé mais qu'il y a une erreur " -"telle qu'un champ incomplet" - -#: aircox_cms/models/__init__.py:113 -msgid "synchronize with Aircox" -msgstr "synchroniser avec Aircox" - -#: aircox_cms/models/__init__.py:116 -msgid "" -"create publication for each object added to an Aircox's station; for example " -"when there is a new program, or when a diffusion has been added to the " -"timetable. Note: it does not concern the Station themselves." -msgstr "" -"créer une publication pour chaque objet ajouté à une station Aircox; par " -"exemple quand il y a un nouveau programme, ou quand une diffusion a été " -"ajoutée au calendrier. Note: cela ne concerne pas les Stations elles-mêmes" - -#: aircox_cms/models/__init__.py:126 -#, fuzzy -#| msgid "default program parent page" -msgid "default programs page" -msgstr "Page des programmes par défault" - -#: aircox_cms/models/__init__.py:129 -msgid "" -"when a new program is saved and a publication is created, put this " -"publication as a child of this page. If no page has been specified, try to " -"put it as the child of the website's root page (otherwise, do not create the " -"page)." -msgstr "" -"Quand un nouveau programme est sauvegardé et qu'une publication est créée, " -"placez cette publication en tant qu'enfant de cette page. Si aucune page n'a " -"été spécifiée, placez cette publication en tant qu'enfant de la page racine " -"du site (sinon, ne créez pas la page)." - -#: aircox_cms/models/__init__.py:148 -msgid "Promotion" -msgstr "Promotion" - -#: aircox_cms/models/__init__.py:155 -#: aircox_cms/templates/aircox_cms/snippets/comments.html:6 -#: aircox_cms/wagtail_hooks.py:163 -msgid "Comments" -msgstr "Commentaires" - -#: aircox_cms/models/__init__.py:159 -msgid "Programs and controls" -msgstr "Programmes et contrôles" - -#: aircox_cms/models/__init__.py:163 -msgid "website settings" -msgstr "paramètres du site internet" - -#: aircox_cms/models/__init__.py:170 aircox_cms/models/sections.py:63 -msgid "page" -msgstr "page" - -#: aircox_cms/models/__init__.py:173 -msgid "published" -msgstr "publié" - -#: aircox_cms/models/__init__.py:177 -msgid "author" -msgstr "auteur" - -#: aircox_cms/models/__init__.py:181 aircox_cms/models/__init__.py:443 -msgid "email" -msgstr "email" - -#: aircox_cms/models/__init__.py:185 -msgid "website" -msgstr "site internet" - -#: aircox_cms/models/__init__.py:189 aircox_cms/models/__init__.py:350 -msgid "date" -msgstr "date" - -#: aircox_cms/models/__init__.py:193 aircox_cms/models/__init__.py:197 -msgid "comment" -msgstr "commentaire" - -#: aircox_cms/models/__init__.py:198 -#, fuzzy -#| msgid "comment" -msgid "comments" -msgstr "commentaire" - -#. Translators: text shown in the comments list (in admin) -#: aircox_cms/models/__init__.py:202 -#, python-brace-format -msgid "{date}, {author}: {content}..." -msgstr "{date}, {author}: {content}..." - -#: aircox_cms/models/__init__.py:230 -msgid "body" -msgstr "corps de texte" - -#: aircox_cms/models/__init__.py:232 -msgid "the publication itself" -msgstr "contenu de la publication elle-même" - -#: aircox_cms/models/__init__.py:236 -msgid "cover" -msgstr "couverture" - -#: aircox_cms/models/__init__.py:240 -msgid "image to use as cover of the publication" -msgstr "image à utiliser comme couverture de la publication" - -#: aircox_cms/models/__init__.py:243 aircox_cms/models/__init__.py:245 -#: aircox_cms/models/__init__.py:367 aircox_cms/models/__init__.py:369 -msgid "allow comments" -msgstr "autoriser les commentaires" - -#: aircox_cms/models/__init__.py:254 aircox_cms/models/__init__.py:393 -#: aircox_cms/models/__init__.py:399 aircox_cms/models/__init__.py:556 -msgid "Content" -msgstr "Contenu" - -#: aircox_cms/models/__init__.py:356 -msgid "publish as program" -msgstr "publier en tant que programme" - -#: aircox_cms/models/__init__.py:359 -msgid "use this program as the author of the publication" -msgstr "utiliser ce programme en tant qu'auteur de la publication" - -#: aircox_cms/models/__init__.py:362 -msgid "focus" -msgstr "focus" - -#: aircox_cms/models/__init__.py:364 -msgid "the publication is highlighted;" -msgstr "la publication est mise en avant;" - -#: aircox_cms/models/__init__.py:373 -msgid "headline" -msgstr "entête" - -#: aircox_cms/models/__init__.py:375 -msgid "headline of the publication, use it as an introduction" -msgstr "entête de la publication, utiliée comme introduction" - -#: aircox_cms/models/__init__.py:384 aircox_cms/models/__init__.py:385 -msgid "Publication" -msgstr "Publication" - -#: aircox_cms/models/__init__.py:436 -msgid "program" -msgstr "programme" - -#: aircox_cms/models/__init__.py:446 -msgid "email is public" -msgstr "l'email est public" - -#: aircox_cms/models/__init__.py:448 -msgid "the email addess is accessible to the public" -msgstr "l'adresse email est accessible au public" - -#: aircox_cms/models/__init__.py:452 aircox_cms/wagtail_hooks.py:49 -msgid "Program" -msgstr "Programme" - -#: aircox_cms/models/__init__.py:453 aircox_cms/signals.py:87 -#: aircox_cms/signals.py:99 aircox_cms/wagtail_hooks.py:38 -#: aircox_cms/wagtail_hooks.py:377 -msgid "Programs" -msgstr "Programmes" - -#: aircox_cms/models/__init__.py:528 -msgid "diffusion" -msgstr "diffusion" - -#: aircox_cms/models/__init__.py:539 -msgid "publish archive" -msgstr "publier l'archive" - -#: aircox_cms/models/__init__.py:541 -msgid "publish the podcast of the complete diffusion" -msgstr "publier le podcast de la diffusion complète" - -#: aircox_cms/models/__init__.py:545 -#: aircox_cms/templates/aircox_cms/snippets/list_item.html:38 -#: aircox_cms/wagtail_hooks.py:80 -msgid "Diffusion" -msgstr "Diffusion" - -#: aircox_cms/models/__init__.py:546 aircox_cms/wagtail_hooks.py:55 -msgid "Diffusions" -msgstr "Diffusions" - -#: aircox_cms/models/__init__.py:549 -msgid "Tracks" -msgstr "Pistes" - -#: aircox_cms/models/__init__.py:594 -#, python-format -msgid "Rerun of %(date)s" -msgstr "Rediffusion du %(date)s" - -#: aircox_cms/models/__init__.py:598 -msgid "Cancelled" -msgstr "Annulé" - -#: aircox_cms/models/__init__.py:614 -msgid "Podcasts" -msgstr "Podcasts" - -#: aircox_cms/models/__init__.py:682 -msgid "Dynamic List Page" -msgstr "Page avec liste dynamique" - -#: aircox_cms/models/__init__.py:683 -msgid "Dynamic List Pages" -msgstr "Pages avec liste dynamique" - -#: aircox_cms/models/__init__.py:727 aircox_cms/models/__init__.py:792 -#: aircox_cms/models/sections.py:404 aircox_cms/models/sections.py:464 -msgid "station" -msgstr "station" - -#: aircox_cms/models/__init__.py:730 aircox_cms/models/__init__.py:795 -#: aircox_cms/models/sections.py:465 -msgid "(required) related station" -msgstr "(requis) station" - -#: aircox_cms/models/__init__.py:733 -msgid "maximum age" -msgstr "âge maximum" - -#: aircox_cms/models/__init__.py:735 -msgid "maximum days in the past allowed to be shown. 0 means no limit" -msgstr "" -"nombre de jours maximum dans le passé autorisés à être affichés. 0 signifie " -"qu'il n'y a pas de limite" - -#: aircox_cms/models/__init__.py:739 -msgid "reverse list" -msgstr "inverser la liste" - -#: aircox_cms/models/__init__.py:741 -msgid "print logs in ascending order by date" -msgstr "afficher les logs de manière ascendante par date" - -#: aircox_cms/models/__init__.py:745 aircox_cms/models/__init__.py:746 -#: aircox_cms/wagtail_hooks.py:127 -msgid "Logs" -msgstr "Logs" - -#: aircox_cms/models/__init__.py:753 aircox_cms/models/__init__.py:801 -msgid "Configuration" -msgstr "Configuration" - -#: aircox_cms/models/__init__.py:805 aircox_cms/models/__init__.py:806 -#: aircox_cms/models/sections.py:485 aircox_cms/signals.py:73 -msgid "Timetable" -msgstr "Grille horaire" - -#: aircox_cms/models/lists.py:49 -msgid "url" -msgstr "url" - -#: aircox_cms/models/lists.py:51 -msgid "URL of the link" -msgstr "URL du lien" - -#: aircox_cms/models/lists.py:59 -msgid "Use a page instead of a URL" -msgstr "Utiliser une page au lieu d'une URL" - -#: aircox_cms/models/lists.py:63 aircox_cms/models/sections.py:631 -msgid "icon" -msgstr "icône" - -#: aircox_cms/models/lists.py:68 -msgid "icon from the gallery" -msgstr "icône de la gallerie" - -#: aircox_cms/models/lists.py:72 -msgid "icon path" -msgstr "chemin de l'icône" - -#: aircox_cms/models/lists.py:76 -msgid "icon from a given URL or path in the directory of static files" -msgstr "icône d'une URL donnée ou chemin dans le des fichiers statiques" - -#: aircox_cms/models/lists.py:80 -msgid "text" -msgstr "texte" - -#: aircox_cms/models/lists.py:83 -msgid "text of the link" -msgstr "texte du lien" - -#: aircox_cms/models/lists.py:86 -msgid "info" -msgstr "info" - -#: aircox_cms/models/lists.py:90 -msgid "description displayed in a popup when the mouse hovers the link" -msgstr "" -"description affichée dans une fenêtre popup quand la souris passe au dessus " -"du lien" - -#: aircox_cms/models/lists.py:106 -msgid "link" -msgstr "lien" - -#: aircox_cms/models/lists.py:156 -msgid "focus available" -msgstr "focus disponible" - -#: aircox_cms/models/lists.py:158 -msgid "if true, highlight the first focused article found" -msgstr "si vrai, surligner le premier article en focus trouvé" - -#: aircox_cms/models/lists.py:161 aircox_cms/models/sections.py:410 -msgid "count" -msgstr "compte" - -#: aircox_cms/models/lists.py:163 -msgid "number of items to display in the list" -msgstr "nombre d'objets à afficher dans la liste" - -#: aircox_cms/models/lists.py:166 -msgid "ascending order" -msgstr "ordre ascendant" - -#: aircox_cms/models/lists.py:168 -msgid "if selected sort list in the ascending order by date" -msgstr "si selectionné, affiche la liste dans l'ordre ascendant par date" - -#: aircox_cms/models/lists.py:173 -msgid "filter on date" -msgstr "filtrer par date" - -#: aircox_cms/models/lists.py:178 -msgid "filter pages on their date" -msgstr "filtrer les pages par leur date" - -#: aircox_cms/models/lists.py:183 -msgid "keep only elements of this type" -msgstr "garder seulement les éléments de ce type" - -#: aircox_cms/models/lists.py:186 -msgid "if set, select only elements that are of this type" -msgstr "si actif, séléctionne seulement des éléments de ce type" - -#: aircox_cms/models/lists.py:191 -msgid "related page" -msgstr "page apparentée" - -#: aircox_cms/models/lists.py:195 -msgid "if set, select children or siblings of this page" -msgstr "" -"si actif, selectionne les enfants ou les sœurs apparentées à cette page" - -#: aircox_cms/models/lists.py:200 -msgid "relation" -msgstr "relation" - -#: aircox_cms/models/lists.py:205 -msgid "" -"when the list is related to a page, only select pages that correspond to " -"this relationship" -msgstr "" -"quand une liste est relative à une page, sélectionner uniquement les pages " -"correspondant à cette relation" - -#: aircox_cms/models/lists.py:210 -msgid "filter on search" -msgstr "filtre sur cette recherche" - -#: aircox_cms/models/lists.py:214 -msgid "keep only pages that matches the given search" -msgstr "garder seulement les pages qui correspondent à cette recherche" - -#: aircox_cms/models/lists.py:218 -msgid "filter on tag" -msgstr "filtrer par date" - -#: aircox_cms/models/lists.py:222 -msgid "keep only pages with the given tags (separated by a colon)" -msgstr "" -"garder seulement les pages avec les tags suivants (séparés par une virgule" - -#: aircox_cms/models/lists.py:231 -msgid "rendering" -msgstr "rendu" - -#: aircox_cms/models/lists.py:239 -msgid "filters" -msgstr "filtres" - -#: aircox_cms/models/lists.py:452 -msgid "navigation days count" -msgstr "compte des jours de navigation" - -#: aircox_cms/models/lists.py:454 -msgid "number of days to display in the navigation header when we use dates" -msgstr "" -"nombre de jours à afficher dans l'entête de la navigation quand des dates " -"sont utilisées" - -#: aircox_cms/models/lists.py:458 -msgid "navigation per week" -msgstr "navigation par semaine" - -#: aircox_cms/models/lists.py:460 -msgid "" -"if selected, show dates navigation per weeks instead of show days equally " -"around the current date" -msgstr "" -"si sélectionné, montre les dates de navigation par semaine au lieu de " -"montrer les date ségalement autour de la date actuelle" - -#: aircox_cms/models/lists.py:464 -msgid "hide icons" -msgstr "cacher les icones" - -#: aircox_cms/models/lists.py:466 -msgid "if selected, images of publications will not be displayed in the list" -msgstr "si sélectionné, la liste n'affiche pas les images des publications" - -#: aircox_cms/models/lists.py:478 -msgid "Navigation" -msgstr "Navigation" - -#: aircox_cms/models/sections.py:36 -msgid "name" -msgstr "nom" - -#: aircox_cms/models/sections.py:39 -msgid "name of this section (not displayed)" -msgstr "nom de cette section (pas affiché)" - -#: aircox_cms/models/sections.py:42 -msgid "position" -msgstr "position" - -#: aircox_cms/models/sections.py:45 -msgid "name of the template block in which the section must be set" -msgstr "nom du bloc modèle dans lequel la section doit être activée" - -#: aircox_cms/models/sections.py:49 aircox_cms/models/sections.py:125 -msgid "order" -msgstr "ordre" - -#: aircox_cms/models/sections.py:51 aircox_cms/models/sections.py:127 -msgid "order of rendering, the higher the latest" -msgstr "ordre d'affichage, le plus élevé étant affiché en dernier" - -#: aircox_cms/models/sections.py:55 -msgid "model" -msgstr "modèle" - -#: aircox_cms/models/sections.py:57 -msgid "" -"this section is displayed only when the current page or publication is of " -"this type" -msgstr "" -"cette section n'est affichée que quand la page ou publication courante est " -"de ce type" - -#: aircox_cms/models/sections.py:65 -msgid "this section is displayed only on this page" -msgstr "cette section n'est affichée que sur cette page" - -#: aircox_cms/models/sections.py:74 aircox_cms/models/sections.py:159 -msgid "General" -msgstr "Général" - -#: aircox_cms/models/sections.py:134 -msgid "title" -msgstr "titre" - -#: aircox_cms/models/sections.py:139 -msgid "show title" -msgstr "montrer le titre" - -#: aircox_cms/models/sections.py:141 -msgid "if set show a title at the head of the section" -msgstr "si actif, montre un titre dans l'entête de cette section" - -#: aircox_cms/models/sections.py:144 -msgid "CSS class" -msgstr "classe CSS" - -#: aircox_cms/models/sections.py:147 -msgid "section container's \"class\" attribute" -msgstr "attribut \"class\" de la balise HTML de la section" - -#: aircox_cms/models/sections.py:186 -msgid "is related" -msgstr "est apparenté" - -#: aircox_cms/models/sections.py:189 -msgid "" -"if set, section is related to the page being processed e.g rendering a list " -"of links will use thoses of the publication instead of an assigned one." -msgstr "" -"si actif, la section est apparentée à la page traitée e.g. fournir une liste " -"de liens utilisera ceux de la publication au lieu de celle assignée" - -#: aircox_cms/models/sections.py:235 -msgid "image" -msgstr "image" - -#: aircox_cms/models/sections.py:239 -msgid "" -"If this item is related to the current page, this image will be used only " -"when the page has not a cover" -msgstr "" -"Si cet objet est apparenté à la page actuelle, cette page sera utilisée " -"seulement quand la page n'a pas de couverture" - -#: aircox_cms/models/sections.py:244 -msgid "width" -msgstr "largeur" - -#: aircox_cms/models/sections.py:246 -msgid "if set and > 0, sets a maximum width for the image" -msgstr "si actif et > 0, fixe une largeur maximum pour l'image" - -#: aircox_cms/models/sections.py:249 -msgid "height" -msgstr "hauteur" - -#: aircox_cms/models/sections.py:251 -msgid "if set 0 and > 0, sets a maximum height for the image" -msgstr "si actif et > 0, fixe une hauteur maximum pour l'image" - -#: aircox_cms/models/sections.py:254 -msgid "resize mode" -msgstr "mode de redimensionnement" - -#: aircox_cms/models/sections.py:257 -msgid "if the image is resized, set the resizing mode" -msgstr "si l'image est redimensionnée, fixer le mode de redimensionnement" - -#: aircox_cms/models/sections.py:266 -msgid "Resizing" -msgstr "Redimensionner" - -#: aircox_cms/models/sections.py:325 -msgid "Links" -msgstr "Liens" - -#: aircox_cms/models/sections.py:361 -msgid "text of the url" -msgstr "texte de l'url" - -#: aircox_cms/models/sections.py:364 -msgid "" -"use this text to display an URL to the complete list. If empty, no link is " -"displayed" -msgstr "" -"utiliser ce texte pour afficher un URL dans la liste complète. Si vide " -"aucune adresse est affichée." - -#: aircox_cms/models/sections.py:407 -msgid "(required) the station on which the logs happened" -msgstr "(requis) la station sur laquelle les logs se sont produits" - -#: aircox_cms/models/sections.py:412 -msgid "number of items to display in the list (max 100)" -msgstr "nombre d'objets à afficher dans la liste (max 100)" - -#: aircox_cms/models/sections.py:416 -msgid "list of logs" -msgstr "liste des logs" - -#: aircox_cms/models/sections.py:417 -msgid "lists of logs" -msgstr "listes des logs" - -#: aircox_cms/models/sections.py:459 -msgid "Section: Timetable" -msgstr "Section: Grille horaire" - -#: aircox_cms/models/sections.py:460 -msgid "Sections: Timetable" -msgstr "Sections: Grilles horaire" - -#: aircox_cms/models/sections.py:469 -msgid "timetable page" -msgstr "Page de Grille horaire" - -#: aircox_cms/models/sections.py:471 -msgid "select a timetable page used to show complete timetable" -msgstr "" -"sélectionner une page de grille horaire pour afficher l'horaire complet" - -#: aircox_cms/models/sections.py:474 -msgid "show date navigation" -msgstr "afficher les dates de navigation" - -#: aircox_cms/models/sections.py:476 -msgid "if checked, navigation dates will be shown" -msgstr "si coché, les dates de navigation sont affichées" - -#: aircox_cms/models/sections.py:511 -msgid "Section: publication's info" -msgstr "Section: info de la publication" - -#: aircox_cms/models/sections.py:512 -msgid "Sections: publication's info" -msgstr "Sections: info de la publication" - -#: aircox_cms/models/sections.py:518 -msgid "default text" -msgstr "texte par défaut" - -#: aircox_cms/models/sections.py:520 -msgid "search" -msgstr "recherche" - -#: aircox_cms/models/sections.py:521 -msgid "text to display when the search field is empty" -msgstr "texte à afficher quand le champ de recherche est vide" - -#: aircox_cms/models/sections.py:525 -msgid "Section: search field" -msgstr "Section: champ de recherche" - -#: aircox_cms/models/sections.py:526 -msgid "Sections: search field" -msgstr "Sections: champ de recherche" - -#: aircox_cms/models/sections.py:567 -msgid "user playlist" -msgstr "playlist utilisateur/ice" - -#: aircox_cms/models/sections.py:570 -msgid "" -"this is a user playlist, it can be edited and saved by the " -"users (the modifications will NOT be registered on the server)" -msgstr "" -"il s'agit d'une playlist utilisateur/ices qu'ils/elles peuvent éditer " -" et enregistrer (les modifications ne seront PAS enregistrées sur le serveur)" - -#: aircox_cms/models/sections.py:574 -msgid "read all" -msgstr "tout lire" - -#: aircox_cms/models/sections.py:577 -msgid "by default at the end of the sound play the next one" -msgstr "par défault à la fin d'un son, jouer le suivant" - -#: aircox_cms/models/sections.py:622 -msgid "live title" -msgstr "titre du direct" - -#: aircox_cms/models/sections.py:624 -msgid "text to display when it plays live" -msgstr "texte à afficher quand le direct est activé" - -#: aircox_cms/models/sections.py:627 -msgid "audio streams" -msgstr "streams audio" - -#: aircox_cms/models/sections.py:628 -msgid "one audio stream per line" -msgstr "un stream audio par ligne" - -#: aircox_cms/models/sections.py:633 -#, fuzzy -#| msgid "text to display when it plays live" -msgid "icon to display in the player" -msgstr "texte à afficher quand le direct est activé" - -#: aircox_cms/models/sections.py:637 -msgid "Section: Player" -msgstr "Section: Player" - -#: aircox_cms/signals.py:33 -#, python-brace-format -msgid "" -"If you see this page, then Aircox is running for the station {station.name}. " -"You might want to change it to a better one. " -msgstr "" -"Si vous voyez cette page, Aircox fonctionne pour la station {station.name}. " -"Vous devriez peut-être la change pour une meilleure station. " - -#: aircox_cms/signals.py:64 -#, python-brace-format -msgid "The website of the {name} radio" -msgstr "Site internet de la radio {name}" - -#. Translators: tags set by default in description of the website -#: aircox_cms/signals.py:68 -#, python-brace-format -msgid "radio,{station.name}" -msgstr "radio,{station.name}" - -#: aircox_cms/signals.py:80 -msgid "Search" -msgstr "Recherche" - -#: aircox_cms/signals.py:92 -msgid "programs" -msgstr "programmes" - -#: aircox_cms/signals.py:100 -msgid "All programs" -msgstr "Tous les programmes" - -#: aircox_cms/signals.py:110 -msgid "Previously on air" -msgstr "Précédemment on air" - -#. Translators: default content of a page for program -#: aircox_cms/signals.py:139 -#, python-brace-format -msgid "{program.name} is a program on {station.name}." -msgstr "{program.name} est un programme sur {station.name}" - -#: aircox_cms/templates/aircox_cms/diffusion_page.html:9 -msgid "Playlist" -msgstr "Playlist" - -#: aircox_cms/templates/aircox_cms/diffusion_page.html:24 -msgid "Dates of diffusion" -msgstr "Dates de diffusion" - -#: aircox_cms/templates/aircox_cms/dynamic_list_page.html:14 -#, python-format -msgid "Search in publications for %(terms)s" -msgstr "Chercher %(terms)s dans les publications" - -#: aircox_cms/templates/aircox_cms/dynamic_list_page.html:19 -#, python-format -msgid "Related to %(title)s" -msgstr "Relatif à %(title)s" - -#: aircox_cms/templates/aircox_cms/dynamic_list_page.html:23 -msgid "All the publications" -msgstr "Toutes les publications" - -#: aircox_cms/templates/aircox_cms/dynamic_list_page.html:36 -msgid "More about it" -msgstr "Plus d'informations" - -#: aircox_cms/templates/aircox_cms/event_page.html:6 -msgid "Practical information" -msgstr "Informations pratiques" - -#: aircox_cms/templates/aircox_cms/event_page.html:10 -msgid "Date" -msgstr "Date" - -#: aircox_cms/templates/aircox_cms/event_page.html:13 -msgid "Place" -msgstr "Lieu" - -#: aircox_cms/templates/aircox_cms/event_page.html:15 -msgid "Price" -msgstr "Prix" - -#: aircox_cms/templates/aircox_cms/program_page.html:12 -#: aircox_cms/wagtail_hooks.py:101 -msgid "Schedule" -msgstr "Horaire" - -#: aircox_cms/templates/aircox_cms/program_page.html:19 -#, python-format -msgid "" -"Diffusion on %(day)s at %(start_hour)s hours %(start_minute)s, " -"%(frequency)s, and last for %(duration_hour)s hours and %(duration_minute)s " -"minutes" -msgstr "" -"Diffusion le %(day)s à %(start_hour)s heures %(start_minute)s, " -"%(frequency)s, et dure %(duration_hour)s heures et %(duration_minute)s " - -#: aircox_cms/templates/aircox_cms/program_page.html:24 -msgid "" -"%(day)s at %(start)s (%(duration)s), %(frequency)s" -msgstr "" -"%(day)s à %(start)s (%(duration)s), %(frequency)s" - -#: aircox_cms/templates/aircox_cms/program_page.html:28 -msgid "Rerun" -msgstr "Rediffusion" - -#: aircox_cms/templates/aircox_cms/program_page.html:37 -msgid "This program is no longer active" -msgstr "Ce programme n'est plus actif" - -#: aircox_cms/templates/aircox_cms/sections/publication_info.html:14 -#: aircox_cms/templates/aircox_cms/sections/publication_info.html:15 -msgid "Parent pages" -msgstr "Pages parentes" - -#: aircox_cms/templates/aircox_cms/sections/publication_info.html:31 -#: aircox_cms/templates/aircox_cms/sections/publication_info.html:32 -msgid "Tags" -msgstr "Tags" - -#: aircox_cms/templates/aircox_cms/sections/publication_info.html:49 -#: aircox_cms/templates/aircox_cms/sections/publication_info.html:50 -msgid "Author" -msgstr "Auteur" - -#: aircox_cms/templates/aircox_cms/sections/publication_info.html:52 -#, python-format -msgid "Published by %(author)s" -msgstr "Publié par %(author)s" - -#: aircox_cms/templates/aircox_cms/sections/publication_info.html:62 -#: aircox_cms/templates/aircox_cms/sections/publication_info.html:63 -msgid "Date of publication" -msgstr "Date de publication" - -#: aircox_cms/templates/aircox_cms/sections/publication_info.html:72 -#: aircox_cms/templates/aircox_cms/sections/publication_info.html:73 -msgid "Share" -msgstr "Partager" - -#: aircox_cms/templates/aircox_cms/snippets/comments.html:21 -msgid "show more options" -msgstr "montrer plus d'options" - -#: aircox_cms/templates/aircox_cms/snippets/comments.html:34 -msgid "Post!" -msgstr "Poster!" - -#: aircox_cms/templates/aircox_cms/snippets/date_list.html:8 -msgid "go to today" -msgstr "aujourd'hui" - -#: aircox_cms/templates/aircox_cms/snippets/date_list.html:11 -msgid "previous days" -msgstr "jours précédents" - -#: aircox_cms/templates/aircox_cms/snippets/date_list.html:27 -msgid "next days" -msgstr "jours suivants" - -#: aircox_cms/templates/aircox_cms/snippets/date_list_item.html:21 -#: aircox_cms/templates/aircox_cms/snippets/date_list_item.html:22 -msgid "on air" -msgstr "en onde" - -#: aircox_cms/templates/aircox_cms/snippets/list.html:29 -msgid "previous page" -msgstr "page précédente" - -#: aircox_cms/templates/aircox_cms/snippets/list.html:59 -msgid "next page" -msgstr "page suivante" - -#: aircox_cms/templates/aircox_cms/snippets/sound_list_item.html:39 -msgid "add this sound to the playlist" -msgstr "ajouter ce son à la playlist" - -#: aircox_cms/templates/aircox_cms/vues/player.html:21 -msgid "Click to pause" -msgstr "Cliquer pour mettre sur pause" - -#: aircox_cms/templates/aircox_cms/vues/player.html:25 -msgid "Loading... Click to pause" -msgstr "Chargement... Cliquer pour mettre sur pause" - -#: aircox_cms/templates/aircox_cms/vues/player.html:29 -msgid "Click to play" -msgstr "Cliquer pour jouer" - -#: aircox_cms/templates/aircox_cms/vues/player.html:54 -msgid "Remove from playlist" -msgstr "Retirer de la playlist" - -#: aircox_cms/templates/aircox_cms/vues/player.html:59 -msgid "Add to my playlist" -msgstr "Ajouter à ma playlist" - -#: aircox_cms/templates/aircox_cms/vues/player.html:82 -#| msgid "add to the player" -msgid "Read all the playlist" -msgstr "Lire toute la playlist" - -#: aircox_cms/wagtail_hooks.py:86 -msgid "Schedules" -msgstr "Horaires" - -#: aircox_cms/wagtail_hooks.py:107 -msgid "Streams" -msgstr "Streams" - -#: aircox_cms/wagtail_hooks.py:121 -msgid "Stream" -msgstr "Stream" - -#: aircox_cms/wagtail_hooks.py:142 -msgid "Log" -msgstr "Logs" - -#: aircox_cms/wagtail_hooks.py:147 -msgid "Related objects" -msgstr "Objects relatifs" - -#: aircox_cms/wagtail_hooks.py:154 -msgid "Advanced" -msgstr "Avancé" - -#: aircox_cms/wagtail_hooks.py:174 -msgid "Sounds" -msgstr "Sons" - -#: aircox_cms/wagtail_hooks.py:338 -msgid "Today's Diffusions" -msgstr "Diffusions du jour" - -#: aircox_cms/wagtail_hooks.py:410 -msgid "Current Station" -msgstr "Station courante" - -#~ msgid "play" -#~ msgstr "jouer" - -#~ msgid "pause" -#~ msgstr "pause" - -#~ msgid "loading..." -#~ msgstr "chargement..." - -#~ msgid "more informations" -#~ msgstr "plus d'informations" - -#~ msgid "remove this sound" -#~ msgstr "enlever ce son" - -#~ msgid "enable and disable single mode" -#~ msgstr "activer et désactiver le mode solo" diff --git a/aircox_cms/management/commands/programs_to_cms.py b/aircox_cms/management/commands/programs_to_cms.py deleted file mode 100755 index 171fbfe..0000000 --- a/aircox_cms/management/commands/programs_to_cms.py +++ /dev/null @@ -1,92 +0,0 @@ -""" -Create missing publications for diffusions and programs already existing. - -We limit the creation of diffusion to the elements to those that start at least -in the last 15 days, and to the future ones. - -The new publications are not published automatically. -""" -import logging -from argparse import RawTextHelpFormatter - -from django.core.management.base import BaseCommand, CommandError -from django.contrib.contenttypes.models import ContentType -from django.utils import timezone as tz - -from aircox.models import Program, Diffusion -from aircox_cms.models import WebsiteSettings, ProgramPage, DiffusionPage - -logger = logging.getLogger('aircox.tools') - - -class Command (BaseCommand): - help= __doc__ - - def add_arguments (self, parser): - parser.formatter_class=RawTextHelpFormatter - - def handle (self, *args, **options): - for settings in WebsiteSettings.objects.all(): - logger.info('start sync for website {}'.format( - str(settings.site) - )) - - if not settings.auto_create: - logger.warning('auto_create disabled: skip') - continue - - if not settings.default_program_parent_page: - logger.warning('no default program page for this website: skip') - continue - - # programs - logger.info('Programs...') - parent = settings.default_programs_page - qs = Program.objects.filter( - active = True, - stream__isnull = True, - page__isnull = True, - ) - for program in qs: - logger.info('- ' + program.name) - page = ProgramPage( - program = program, - title = program.name, - live = False, - ) - parent.add_child(instance = page) - - # diffusions - logger.info('Diffusions...') - qs = Diffusion.objects.filter( - start__gt = tz.now().date() - tz.timedelta(days = 20), - page__isnull = True, - initial__isnull = True - ).exclude(type = Diffusion.Type.unconfirmed) - for diffusion in qs: - if not diffusion.program.page: - if not hasattr(diffusion.program, '__logged_diff_error'): - logger.warning( - 'the program {} has no page; skip the creation of ' - 'page for its diffusions'.format( - diffusion.program.name - ) - ) - diffusion.program.__logged_diff_error = True - continue - - logger.info('- ' + str(diffusion)) - try: - page = DiffusionPage.from_diffusion( - diffusion, live = False - ) - diffusion.program.page.add_child(instance = page) - except: - import sys - e = sys.exc_info()[0] - logger.error('Error saving', str(diffusion) + ':', e) - - logger.info('done') - - - diff --git a/aircox_cms/models/__init__.py b/aircox_cms/models/__init__.py deleted file mode 100755 index 2a7f53b..0000000 --- a/aircox_cms/models/__init__.py +++ /dev/null @@ -1,823 +0,0 @@ -import datetime - -from django.db import models -from django.contrib.auth.models import User -from django.contrib import messages -from django.utils import timezone as tz -from django.utils.translation import ugettext as _, ugettext_lazy - -# pages and panels -from wagtail.contrib.settings.models import BaseSetting, register_setting -from wagtail.core.models import Page, Orderable, \ - PageManager, PageQuerySet -from wagtail.core.fields import RichTextField -from wagtail.images.edit_handlers import ImageChooserPanel -from wagtail.admin.edit_handlers import FieldPanel, FieldRowPanel, \ - MultiFieldPanel, InlinePanel, PageChooserPanel, StreamFieldPanel -from wagtail.search import index - -# snippets -from wagtail.snippets.models import register_snippet - -# tags -from modelcluster.fields import ParentalKey -from modelcluster.tags import ClusterTaggableManager -from taggit.models import TaggedItemBase - -# comment clean-up -import bleach - -import aircox.models -import aircox_cms.settings as settings - -from aircox_cms.models.lists import * -from aircox_cms.models.sections import * -from aircox_cms.template import TemplateMixin -from aircox_cms.utils import image_url - - -@register_setting -class WebsiteSettings(BaseSetting): - station = models.OneToOneField( - aircox.models.Station, - models.SET_NULL, - verbose_name = _('aircox station'), - related_name = 'website_settings', - unique = True, - blank = True, null = True, - help_text = _( - 'refers to an Aircox\'s station; it is used to make the link ' - 'between the website and Aircox' - ), - ) - - # general website information - favicon = models.ImageField( - verbose_name = _('favicon'), - null=True, blank=True, - help_text = _('small logo for the website displayed in the browser'), - ) - tags = models.CharField( - _('tags'), - max_length=256, - null=True, blank=True, - help_text = _('tags describing the website; used for referencing'), - ) - description = models.CharField( - _('public description'), - max_length=256, - null=True, blank=True, - help_text = _('public description of the website; used for referencing'), - ) - list_page = models.ForeignKey( - 'aircox_cms.DynamicListPage', - on_delete=models.CASCADE, - verbose_name = _('page for lists'), - help_text=_('page used to display the results of a search and other ' - 'lists'), - related_name= 'list_page', - blank = True, null = True, - ) - # comments - accept_comments = models.BooleanField( - default = True, - help_text = _('publish comments automatically without verifying'), - ) - allow_comments = models.BooleanField( - default = True, - help_text = _('publish comments automatically without verifying'), - ) - comment_success_message = models.TextField( - _('success message'), - default = _('Your comment has been successfully posted!'), - help_text = _( - 'message displayed when a comment has been successfully posted' - ), - ) - comment_wait_message = models.TextField( - _('waiting message'), - default = _('Your comment is awaiting for approval.'), - help_text = _( - 'message displayed when a comment has been sent, but waits for ' - ' website administrators\' approval.' - ), - ) - comment_error_message = models.TextField( - _('error message'), - default = _('We could not save your message. Please correct the error(s) below.'), - help_text = _( - 'message displayed when the form of the comment has been ' - ' submitted but there is an error, such as an incomplete field' - ), - ) - - sync = models.BooleanField( - _('synchronize with Aircox'), - default = False, - help_text = _( - 'create publication for each object added to an Aircox\'s ' - 'station; for example when there is a new program, or ' - 'when a diffusion has been added to the timetable. Note: ' - 'it does not concern the Station themselves.' - # /doc/ the page is saved but not pubished -- this must be - # done manually, when the user edit it. - ) - ) - - default_programs_page = ParentalKey( - Page, - verbose_name = _('default programs page'), - blank = True, null = True, - help_text = _( - 'when a new program is saved and a publication is created, ' - 'put this publication as a child of this page. If no page ' - 'has been specified, try to put it as the child of the ' - 'website\'s root page (otherwise, do not create the page).' - # /doc/ (technicians, admin): if the page has not been created, - # it still can be created using the `programs_to_cms` command. - ), - limit_choices_to = { - 'show_in_menus': True, - 'publication__isnull': False, - }, - ) - - panels = [ - MultiFieldPanel([ - FieldPanel('favicon'), - FieldPanel('tags'), - FieldPanel('description'), - FieldPanel('list_page'), - ], heading=_('Promotion')), - MultiFieldPanel([ - FieldPanel('allow_comments'), - FieldPanel('accept_comments'), - FieldPanel('comment_success_message'), - FieldPanel('comment_wait_message'), - FieldPanel('comment_error_message'), - ], heading = _('Comments')), - MultiFieldPanel([ - FieldPanel('sync'), - FieldPanel('default_programs_page'), - ], heading = _('Programs and controls')), - ] - - class Meta: - verbose_name = _('website settings') - - -@register_snippet -class Comment(models.Model): - publication = models.ForeignKey( - Page, - on_delete=models.CASCADE, - verbose_name = _('page') - ) - published = models.BooleanField( - verbose_name = _('published'), - default = False - ) - author = models.CharField( - verbose_name = _('author'), - max_length = 32, - ) - email = models.EmailField( - verbose_name = _('email'), - blank = True, null = True, - ) - url = models.URLField( - verbose_name = _('website'), - blank = True, null = True, - ) - date = models.DateTimeField( - _('date'), - auto_now_add = True, - ) - content = models.TextField ( - _('comment'), - ) - - class Meta: - verbose_name = _('comment') - verbose_name_plural = _('comments') - - def __str__(self): - # Translators: text shown in the comments list (in admin) - return _('{date}, {author}: {content}...').format( - author = self.author, - date = self.date.strftime('%d %A %Y, %H:%M'), - content = self.content[:128] - ) - - def make_safe(self): - self.author = bleach.clean(self.author, tags=[]) - if self.email: - self.email = bleach.clean(self.email, tags=[]) - self.email = self.email.replace('"', '%22') - if self.url: - self.url = bleach.clean(self.url, tags=[]) - self.url = self.url.replace('"', '%22') - self.content = bleach.clean( - self.content, - tags=settings.AIRCOX_CMS_BLEACH_COMMENT_TAGS, - attributes=settings.AIRCOX_CMS_BLEACH_COMMENT_ATTRS - ) - - def save(self, make_safe = True, *args, **kwargs): - if make_safe: - self.make_safe() - return super().save(*args, **kwargs) - - -class BasePage(Page): - body = RichTextField( - _('body'), - null = True, blank = True, - help_text = _('the publication itself') - ) - cover = models.ForeignKey( - 'wagtailimages.Image', - verbose_name = _('cover'), - null=True, blank=True, - on_delete=models.SET_NULL, - related_name='+', - help_text = _('image to use as cover of the publication'), - ) - allow_comments = models.BooleanField( - _('allow comments'), - default = True, - help_text = _('allow comments') - ) - - # panels - content_panels = [ - MultiFieldPanel([ - FieldPanel('title'), - ImageChooserPanel('cover'), - FieldPanel('body', classname='full'), - ], heading=_('Content')) - ] - settings_panels = Page.settings_panels + [ - FieldPanel('allow_comments'), - ] - search_fields = [ - index.SearchField('title', partial_match=True), - index.SearchField('body', partial_match=True), - index.FilterField('live'), - index.FilterField('show_in_menus'), - ] - - # properties - @property - def url(self): - if not self.live: - parent = self.get_parent().specific - return parent and parent.url - return super().url - - @property - def icon(self): - return image_url(self.cover, 'fill-64x64') - - @property - def small_icon(self): - return image_url(self.cover, 'fill-32x32') - - @property - def comments(self): - return Comment.objects.filter( - publication = self, - published = True, - ).order_by('-date') - - # methods - def get_list_page(self): - """ - Return the page that should be used for lists related to this - page. If None is returned, use a default one. - """ - return None - - def get_context(self, request, *args, **kwargs): - from aircox_cms.forms import CommentForm - - context = super().get_context(request, *args, **kwargs) - if self.allow_comments and \ - WebsiteSettings.for_site(request.site).allow_comments: - context['comment_form'] = CommentForm() - - context['settings'] = { - 'debug': settings.DEBUG - } - return context - - def serve(self, request): - from aircox_cms.forms import CommentForm - if request.POST and 'comment' in request.POST['type']: - settings = WebsiteSettings.for_site(request.site) - comment_form = CommentForm(request.POST) - if comment_form.is_valid(): - comment = comment_form.save(commit=False) - comment.publication = self - comment.published = settings.accept_comments - comment.save() - messages.success(request, - settings.comment_success_message - if comment.published else - settings.comment_wait_message, - fail_silently=True, - ) - else: - messages.error( - request, settings.comment_error_message, fail_silently=True - ) - return super().serve(request) - - class Meta: - abstract = True - - -# -# Publications -# -class PublicationRelatedLink(RelatedLinkBase,Component): - template = 'aircox_cms/snippets/link.html' - parent = ParentalKey('Publication', related_name='links') - -class PublicationTag(TaggedItemBase): - content_object = ParentalKey('Publication', related_name='tagged_items') - -class Publication(BasePage): - order_field = 'date' - - date = models.DateTimeField( - _('date'), - blank = True, null = True, - auto_now_add = True, - ) - publish_as = models.ForeignKey( - 'ProgramPage', - verbose_name = _('publish as program'), - on_delete=models.SET_NULL, - blank = True, null = True, - help_text = _('use this program as the author of the publication'), - ) - focus = models.BooleanField( - _('focus'), - default = False, - help_text = _('the publication is highlighted;'), - ) - allow_comments = models.BooleanField( - _('allow comments'), - default = True, - help_text = _('allow comments') - ) - - headline = models.TextField( - _('headline'), - blank = True, null = True, - help_text = _('headline of the publication, use it as an introduction'), - ) - tags = ClusterTaggableManager( - verbose_name = _('tags'), - through=PublicationTag, - blank=True - ) - - class Meta: - verbose_name = _('Publication') - verbose_name_plural = _('Publication') - - content_panels = [ - MultiFieldPanel([ - FieldPanel('title'), - ImageChooserPanel('cover'), - FieldPanel('headline'), - FieldPanel('body', classname='full'), - ], heading=_('Content')) - ] - promote_panels = [ - MultiFieldPanel([ - FieldPanel('tags'), - FieldPanel('focus'), - ], heading=_('Content')), - ] + Page.promote_panels - settings_panels = Page.settings_panels + [ - FieldPanel('publish_as'), - FieldPanel('allow_comments'), - ] - search_fields = BasePage.search_fields + [ - index.SearchField('headline', partial_match=True), - ] - - - @property - def recents(self): - return self.get_children().type(Publication).not_in_menu().live() \ - .order_by('-publication__date') - - def get_context(self, request, *args, **kwargs): - context = super().get_context(request, *args, **kwargs) - view = request.GET.get('view') - context.update({ - 'view': view, - 'page': self, - }) - if view == 'list': - context.update(BaseList.from_request(request, related = self)) - context['list_url_args'] += '&view=list' - return context - - def save(self, *args, **kwargs): - if not self.date and self.first_published_at: - self.date = self.first_published_at - return super().save(*args, **kwargs) - - -class ProgramPage(Publication): - program = models.OneToOneField( - aircox.models.Program, - verbose_name = _('program'), - related_name = 'page', - on_delete=models.SET_NULL, - blank=True, null=True, - ) - # rss = models.URLField() - email = models.EmailField( - _('email'), blank=True, null=True, - ) - email_is_public = models.BooleanField( - _('email is public'), - default = False, - help_text = _('the email addess is accessible to the public'), - ) - - class Meta: - verbose_name = _('Program') - verbose_name_plural = _('Programs') - - content_panels = [ - # FieldPanel('program'), - ] + Publication.content_panels - - settings_panels = Publication.settings_panels + [ - FieldPanel('email'), - FieldPanel('email_is_public'), - ] - - def diffs_to_page(self, diffs): - for diff in diffs: - if not diff.page: - diff.page = ListItem( - title = '{}, {}'.format( - self.program.name, diff.date.strftime('%d %B %Y') - ), - cover = self.cover, - live = True, - date = diff.start, - ) - return [ - diff.page for diff in diffs if diff.page.live - ] - - @property - def next(self): - now = tz.now() - diffs = aircox.models.Diffusion.objects \ - .filter(end__gte = now, program = self.program) \ - .order_by('start').prefetch_related('page') - return self.diffs_to_page(diffs) - - @property - def prev(self): - now = tz.now() - diffs = aircox.models.Diffusion.objects \ - .filter(end__lte = now, program = self.program) \ - .order_by('-start').prefetch_related('page') - return self.diffs_to_page(diffs) - - def save(self, *args, **kwargs): - # set publish_as - if self.program and not self.pk: - super().save() - self.publish_as = self - super().save(*args, **kwargs) - - -class Track(aircox.models.Track,Orderable): - diffusion = ParentalKey( - 'DiffusionPage', related_name='tracks', - null = True, blank = True, - on_delete = models.SET_NULL - ) - - sort_order_field = 'position' - panels = [ - FieldPanel('artist'), - FieldPanel('title'), - FieldPanel('tags'), - FieldPanel('info'), - ] - - def save(self, *args, **kwargs): - if self.diffusion.diffusion: - self.related = self.diffusion.diffusion - self.in_seconds = False - super().save(*args, **kwargs) - - -class DiffusionPage(Publication): - diffusion = models.OneToOneField( - aircox.models.Diffusion, - verbose_name = _('diffusion'), - related_name = 'page', - null=True, blank = True, - # not blank because we enforce the connection to a diffusion - # (still users always tend to break sth) - on_delete=models.SET_NULL, - limit_choices_to = { - 'initial__isnull': True, - }, - ) - publish_archive = models.BooleanField( - _('publish archive'), - default = False, - help_text = _('publish the podcast of the complete diffusion'), - ) - - class Meta: - verbose_name = _('Diffusion') - verbose_name_plural = _('Diffusions') - - content_panels = Publication.content_panels + [ - InlinePanel('tracks', label=_('Tracks')), - ] - promote_panels = [ - MultiFieldPanel([ - FieldPanel('publish_archive'), - FieldPanel('tags'), - FieldPanel('focus'), - ], heading=_('Content')), - ] + Page.promote_panels - settings_panels = Publication.settings_panels + [ - FieldPanel('diffusion') - ] - - @classmethod - def from_diffusion(cl, diff, model = None, **kwargs): - model = model or cl - model_kwargs = { - 'diffusion': diff, - 'title': '{}, {}'.format( - diff.program.name, tz.localtime(diff.date).strftime('%d %B %Y') - ), - 'cover': (diff.program.page and \ - diff.program.page.cover) or None, - 'date': diff.start, - } - model_kwargs.update(kwargs) - r = model(**model_kwargs) - return r - - @classmethod - def as_item(cl, diff): - """ - Return a DiffusionPage or ListItem from a Diffusion. - """ - initial = diff.initial or diff - - if hasattr(initial, 'page'): - item = initial.page - else: - item = cl.from_diffusion(diff, ListItem) - item.live = True - - item.info = [] - # Translators: informations about a diffusion - if diff.initial: - item.info.append(_('Rerun of %(date)s') % { - 'date': diff.initial.start.strftime('%A %d') - }) - if diff.type == diff.Type.canceled: - item.info.append(_('Cancelled')) - item.info = '; '.join(item.info) - - item.date = diff.start - item.css_class = 'diffusion' - - now = tz.now() - if diff.start <= now <= diff.end: - item.css_class = ' now' - item.now = True - - return item - - def get_context(self, request, *args, **kwargs): - context = super().get_context(request, *args, **kwargs) - context['podcasts'] = self.diffusion and SectionPlaylist( - title=_('Podcasts'), - page = self, - sounds = self.diffusion.get_sounds( - archive = self.publish_archive, excerpt = True - ) - ) - return context - - def save(self, *args, **kwargs): - if self.diffusion: - # force to sort by diffusion date in wagtail explorer - self.latest_revision_created_at = self.diffusion.start - - # set publish_as - if not self.pk: - self.publish_as = self.diffusion.program.page - - # sync date - self.date = self.diffusion.start - - # update podcasts' attributes - for podcast in self.diffusion.sound_set \ - .exclude(type = aircox.models.Sound.Type.removed): - publish = self.live and self.publish_archive \ - if podcast.type == podcast.Type.archive else self.live - - if podcast.public != publish: - podcast.public = publish - podcast.save() - - super().save(*args, **kwargs) - - -# -# Others types of pages -# - -class CategoryPage(BasePage, BaseList): - # TODO: hide related in panels? - content_panels = BasePage.content_panels + BaseList.panels - - def get_list_page(self): - return self - - def get_context(self, request, *args, **kwargs): - context = super().get_context(request, *args, **kwargs) - context.update(BaseList.get_context(self, request, paginate = True)) - context['view'] = 'list' - return context - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - # we force related attribute - if not self.related: - self.related = self - - -class DynamicListPage(BasePage): - """ - Displays a list of publications using query passed by the url. - This can be used for search/tags page, and generally only one - page is used per website. - - If a title is given, use it instead of the generated one. - """ - # FIXME/TODO: title in template - # TODO: personnalized titles depending on request - class Meta: - verbose_name = _('Dynamic List Page') - verbose_name_plural = _('Dynamic List Pages') - - def get_context(self, request, *args, **kwargs): - context = super().get_context(request, *args, **kwargs) - context.update(BaseList.from_request(request)) - return context - - -class DatedListPage(DatedBaseList,BasePage): - class Meta: - abstract = True - - def get_queryset(self, request, context): - """ - Must be implemented by the child - """ - return [] - - def get_context(self, request, *args, **kwargs): - """ - note: context is updated using self.get_date_context - """ - context = super().get_context(request, *args, **kwargs) - - # date navigation - if 'date' in request.GET: - date = request.GET.get('date') - date = self.str_to_date(date) - else: - date = tz.now().date() - context.update(self.get_date_context(date)) - - # queryset - context['object_list'] = self.get_queryset(request, context) - context['target'] = self - return context - - -class LogsPage(DatedListPage): - template = 'aircox_cms/dated_list_page.html' - - # TODO: make it a property that automatically select the station - station = models.ForeignKey( - aircox.models.Station, - verbose_name = _('station'), - null = True, blank = True, - on_delete = models.SET_NULL, - help_text = _('(required) related station') - ) - max_age = models.IntegerField( - _('maximum age'), - default=15, - help_text = _('maximum days in the past allowed to be shown. ' - '0 means no limit') - ) - reverse = models.BooleanField( - _('reverse list'), - default=False, - help_text = _('print logs in ascending order by date'), - ) - - class Meta: - verbose_name = _('Logs') - verbose_name_plural = _('Logs') - - content_panels = DatedListPage.content_panels + [ - MultiFieldPanel([ - FieldPanel('station'), - FieldPanel('max_age'), - FieldPanel('reverse'), - ], heading=_('Configuration')), - ] - - def get_nav_dates(self, date): - """ - Return a list of dates availables for the navigation - """ - # there might be a bug if max_age < nav_days - today = tz.now().date() - first = min(date, today) - first = first - tz.timedelta(days = self.nav_days-1) - if self.max_age: - first = max(first, today - tz.timedelta(days = self.max_age)) - return [ first + tz.timedelta(days=i) - for i in range(0, self.nav_days) ] - - def get_queryset(self, request, context): - today = tz.now().date() - if self.max_age and context['nav_dates']['next'] > today: - context['nav_dates']['next'] = None - if self.max_age and context['nav_dates']['prev'] < \ - today - tz.timedelta(days = self.max_age): - context['nav_dates']['prev'] = None - - logs = [] - for date in context['nav_dates']['dates']: - items = self.station.on_air(date = date) \ - .select_related('track','diffusion') - items = [ SectionLogsList.as_item(item) for item in items ] - logs.append( - (date, reversed(items) if self.reverse else items) - ) - return logs - - -class TimetablePage(DatedListPage): - template = 'aircox_cms/dated_list_page.html' - station = models.ForeignKey( - aircox.models.Station, - verbose_name=_('station'), - on_delete=models.SET_NULL, - null=True, blank=True, - help_text=_('(required) related station') - ) - - content_panels = DatedListPage.content_panels + [ - MultiFieldPanel([ - FieldPanel('station'), - ], heading=_('Configuration')), - ] - - class Meta: - verbose_name = _('Timetable') - verbose_name_plural = _('Timetable') - - def get_queryset(self, request, context): - diffs = [] - for date in context['nav_dates']['dates']: - items = [ - DiffusionPage.as_item(item) - for item in aircox.models.Diffusion.objects \ - .station(self.station).at(date) - ] - diffs.append((date, items)) - return diffs - - diff --git a/aircox_cms/models/lists.py b/aircox_cms/models/lists.py deleted file mode 100644 index 4bea799..0000000 --- a/aircox_cms/models/lists.py +++ /dev/null @@ -1,534 +0,0 @@ -""" -Generic list manipulation used to render list of items - -Includes various usefull class and abstract models to make lists and -list items. -""" -import datetime -import re -from enum import IntEnum - -from django.db import models -from django.contrib.contenttypes.models import ContentType -from django.contrib.staticfiles.templatetags.staticfiles import static -from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger -from django.utils.translation import ugettext as _, ugettext_lazy -from django.utils import timezone as tz -from django.utils.functional import cached_property - -from wagtail.admin.edit_handlers import * -from wagtail.core.models import Page, Orderable -from wagtail.images.models import Image -from wagtail.images.edit_handlers import ImageChooserPanel - -from aircox_cms.utils import related_pages_filter - - -class ListItem: - """ - Generic normalized element to add item in lists that are not based - on Publication. - """ - title = '' - headline = '' - url = '' - cover = None - date = None - - def __init__(self, **kwargs): - self.__dict__.update(kwargs) - self.specific = self - - -class RelatedLinkBase(Orderable): - """ - Base model to make a link item. It can link to an url, or a page and - includes some common fields. - """ - url = models.URLField( - _('url'), - null=True, blank=True, - help_text = _('URL of the link'), - ) - page = models.ForeignKey( - Page, - null=True, - blank=True, - on_delete=models.SET_NULL, - related_name='+', - help_text = _('Use a page instead of a URL') - ) - icon = models.ForeignKey( - Image, - verbose_name = _('icon'), - null=True, blank=True, - on_delete=models.SET_NULL, - related_name='+', - help_text = _( - 'icon from the gallery' - ), - ) - icon_path = models.CharField( - _('icon path'), - null=True, blank=True, - max_length=128, - help_text = _( - 'icon from a given URL or path in the directory of static files' - ) - ) - text = models.CharField( - _('text'), - max_length = 64, - null = True, blank=True, - help_text = _('text of the link'), - ) - info = models.CharField( - _('info'), - max_length = 128, - null=True, blank=True, - help_text = _( - 'description displayed in a popup when the mouse hovers ' - 'the link' - ) - ) - - class Meta: - abstract = True - - panels = [ - MultiFieldPanel([ - FieldPanel('text'), - FieldPanel('info'), - ImageChooserPanel('icon'), - FieldPanel('icon_path'), - FieldPanel('url'), - PageChooserPanel('page'), - ], heading=_('link')) - ] - - def icon_url(self): - """ - Return icon_path as a complete url, since it can either be an - url or a path to static file. - """ - if self.icon_path.startswith('http://') or \ - self.icon_path.startswith('https://'): - return self.icon_path - return static(self.icon_path) - - def as_dict(self): - """ - Return compiled values from parameters as dict with - 'url', 'icon', 'text' - """ - if self.page: - url, text = self.page.url, self.text or self.page.title - else: - url, text = self.url, self.text or self.url - return { - 'url': url, - 'text': text, - 'info': self.info, - 'icon': self.icon, - 'icon_path': self.icon_path and self.icon_url(), - } - - -class BaseList(models.Model): - """ - Generic list - """ - class DateFilter(IntEnum): - none = 0x00 - previous = 0x01 - next = 0x02 - before_related = 0x03 - after_related = 0x04 - - class RelationFilter(IntEnum): - none = 0x00 - subpages = 0x01 - siblings = 0x02 - subpages_or_siblings = 0x03 - - # rendering - use_focus = models.BooleanField( - _('focus available'), - default = False, - help_text = _('if true, highlight the first focused article found') - ) - count = models.SmallIntegerField( - _('count'), - default = 30, - help_text = _('number of items to display in the list'), - ) - asc = models.BooleanField( - verbose_name = _('ascending order'), - default = True, - help_text = _('if selected sort list in the ascending order by date') - ) - - # selectors - date_filter = models.SmallIntegerField( - verbose_name = _('filter on date'), - choices = [ (int(y), _(x.replace('_', ' '))) - for x,y in DateFilter.__members__.items() ], - blank = True, null = True, - help_text = _('filter pages on their date') - ) - model = models.ForeignKey( - ContentType, - verbose_name = _('filter on page type'), - blank = True, null = True, - on_delete=models.SET_NULL, - help_text = _('keep only elements of this type'), - limit_choices_to = related_pages_filter, - ) - related = models.ForeignKey( - Page, - verbose_name = _('related page'), - blank = True, null = True, - on_delete=models.SET_NULL, - help_text = _( - 'if set, select children or siblings of this page' - ), - related_name = '+' - ) - relation = models.SmallIntegerField( - verbose_name = _('relation'), - choices = [ (int(y), _(x.replace('_', ' '))) - for x,y in RelationFilter.__members__.items() ], - default = 1, - help_text = _( - 'when the list is related to a page, only select pages that ' - 'correspond to this relationship' - ), - ) - search = models.CharField( - verbose_name = _('filter on search'), - blank = True, null = True, - max_length = 128, - help_text = _( - 'keep only pages that matches the given search' - ) - ) - tags = models.CharField( - verbose_name = _('filter on tag'), - blank = True, null = True, - max_length = 128, - help_text = _( - 'keep only pages with the given tags (separated by a colon)' - ) - ) - - panels = [ - MultiFieldPanel([ - FieldPanel('count'), - FieldPanel('use_focus'), - FieldPanel('asc'), - ], heading=_('rendering')), - MultiFieldPanel([ - FieldPanel('date_filter'), - FieldPanel('model'), - PageChooserPanel('related'), - FieldPanel('relation'), - FieldPanel('search'), - FieldPanel('tags'), - ], heading=_('filters')) - ] - - class Meta: - abstract = True - - def __get_related(self, qs): - related = self.related and self.related.specific - filter = self.RelationFilter - - if self.relation in (filter.subpages, filter.subpages_or_siblings): - qs_ = qs.descendant_of(related) - if self.relation == filter.subpages_or_siblings and \ - not qs.count(): - qs_ = qs.sibling_of(related) - qs = qs_ - else: - qs = qs.sibling_of(related) - - date = related.date if hasattr(related, 'date') else \ - related.first_published_at - if self.date_filter == self.DateFilter.before_related: - qs = qs.filter(date__lt = date) - elif self.date_filter == self.DateFilter.after_related: - qs = qs.filter(date__gte = date) - return qs - - def get_queryset(self): - """ - Get queryset based on the arguments. This class is intended to be - reusable by other classes if needed. - """ - # FIXME: check if related is published - from aircox_cms.models import Publication - # model - if self.model: - qs = self.model.model_class().objects.all() - else: - qs = Publication.objects.all() - qs = qs.live().not_in_menu() - - # related - if self.related: - qs = self.__get_related(qs) - - # date_filter - date = tz.now() - if self.date_filter == self.DateFilter.previous: - qs = qs.filter(date__lt = date) - elif self.date_filter == self.DateFilter.next: - qs = qs.filter(date__gte = date) - - # sort - qs = qs.order_by('date', 'pk') \ - if self.asc else qs.order_by('-date', '-pk') - - # tags - if self.tags: - qs = qs.filter(tags__name__in = ','.split(self.tags)) - - # search - if self.search: - # this qs.search does not return a queryset - qs = qs.search(self.search) - - return qs - - def get_context(self, request, qs = None, paginate = True): - """ - Return a context object using the given request and arguments. - @param paginate: paginate and include paginator into context - - Context arguments: - - object_list: queryset of the list's objects - - paginator: [if paginate] paginator object for this list - - list_url_args: GET arguments of the url as string - - ! Note: BaseList does not inherit from Wagtail.Page, and calling - this method won't call other super() get_context. - """ - qs = qs or self.get_queryset() - paginator = None - context = {} - if qs.count(): - if paginate: - context.update(self.paginate(request, qs)) - else: - context['object_list'] = qs[:self.count] - else: - # keep empty queryset - context['object_list'] = qs - context['list_url_args'] = self.to_url(full_url = False) - context['list_selector'] = self - return context - - def paginate(self, request, qs): - # paginator - paginator = Paginator(qs, self.count) - try: - qs = paginator.page(request.GET.get('page') or 1) - except PageNotAnInteger: - qs = paginator.page(1) - except EmptyPage: - qs = paginator.page(paginator.num_pages) - return { - 'paginator': paginator, - 'object_list': qs - } - - def to_url(self, page = None, **kwargs): - """ - Return a url to a given page with GET corresponding to this - list's parameters. - @param page: if given use it to prepend url with page's url instead of giving only - GET parameters - @param **kwargs: override list parameters - - If there is related field use it to get the page, otherwise use - the given list_page or the first BaseListPage it finds. - """ - params = { - 'asc': self.asc, - 'date_filter': self.get_date_filter_display(), - 'model': self.model and self.model.model, - 'relation': self.relation, - 'search': self.search, - 'tags': self.tags - } - params.update(kwargs) - - if self.related: - params['related'] = self.related.pk - - params = '&'.join([ - key if value == True else '{}={}'.format(key, value) - for key, value in params.items() if value - ]) - if not page: - return params - return page.url + '?' + params - - @classmethod - def from_request(cl, request, related = None): - """ - Return a context from the request's GET parameters. Context - can be used to update relative informations, more information - on this object from BaseList.get_context() - - @param request: get params from this request - @param related: reference page for a related list - @return context object from BaseList.get_context() - - This function can be used by other views if needed - - Parameters: - * asc: if present, sort ascending instead of descending - * date_filter: one of DateFilter attribute's key. - * model: ['program','diffusion','event'] type of the publication - * relation: one of RelationFilter attribute's key - * related: list is related to the method's argument `related`. - It can be a page id. - - * tag: tag to search for - * search: query to search in the publications - * page: page number - """ - date_filter = request.GET.get('date_filter') - model = request.GET.get('model') - - relation = request.GET.get('relation') - if relation is not None: - try: - relation = int(relation) - except: - relation = None - - related_= request.GET.get('related') - if related_: - try: - related_ = int(related_) - related_ = Page.objects.filter(pk = related_).first() - related_ = related_ and related_.specific - except: - related_ = None - - kwargs = { - 'asc': 'asc' in request.GET, - 'date_filter': - int(getattr(cl.DateFilter, date_filter)) - if date_filter and hasattr(cl.DateFilter, date_filter) - else None, - 'model': - ProgramPage if model == 'program' else - DiffusionPage if model == 'diffusion' else - EventPage if model == 'event' else None, - 'related': related_, - 'relation': relation, - 'tags': request.GET.get('tags'), - 'search': request.GET.get('search'), - } - - base_list = cl( - count = 30, **{ k:v for k,v in kwargs.items() if v } - ) - return base_list.get_context(request) - - -class DatedBaseList(models.Model): - """ - List that display items per days. Renders a navigation section on the - top. - """ - nav_days = models.SmallIntegerField( - _('navigation days count'), - default = 7, - help_text = _('number of days to display in the navigation header ' - 'when we use dates') - ) - nav_per_week = models.BooleanField( - _('navigation per week'), - default = False, - help_text = _('if selected, show dates navigation per weeks instead ' - 'of show days equally around the current date') - ) - hide_icons = models.BooleanField( - _('hide icons'), - default = False, - help_text = _('if selected, images of publications will not be ' - 'displayed in the list') - ) - - class Meta: - abstract = True - - panels = [ - MultiFieldPanel([ - FieldPanel('nav_days'), - FieldPanel('nav_per_week'), - FieldPanel('hide_icons'), - ], heading=_('Navigation')), - ] - - @staticmethod - def str_to_date(date): - """ - Parse a string and return a regular date or None. - Format is either "YYYY/MM/DD" or "YYYY-MM-DD" or "YYYYMMDD" - """ - try: - exp = r'(?P[0-9]{4})(-|\/)?(?P[0-9]{1,2})(-|\/)?' \ - r'(?P[0-9]{1,2})' - date = re.match(exp, date).groupdict() - return datetime.date( - year = int(date['year']), month = int(date['month']), - day = int(date['day']) - ) - except: - return None - - def get_nav_dates(self, date): - """ - Return a list of dates availables for the navigation - """ - if self.nav_per_week: - first = date.weekday() - else: - first = int((self.nav_days - 1) / 2) - first = date - tz.timedelta(days = first) - return [ first + tz.timedelta(days=i) - for i in range(0, self.nav_days) ] - - def get_date_context(self, date = None): - """ - Return a dict that can be added to the context to be used by - a date_list. - """ - today = tz.now().date() - if not date: - date = today - - # next/prev weeks/date bunch - dates = self.get_nav_dates(date) - next = date + tz.timedelta(days=self.nav_days) - prev = date - tz.timedelta(days=self.nav_days) - - # context dict - return { - 'nav_dates': { - 'today': today, - 'date': date, - 'next': next, - 'prev': prev, - 'dates': dates, - } - } - - - diff --git a/aircox_cms/models/sections.py b/aircox_cms/models/sections.py deleted file mode 100644 index c5d8b4c..0000000 --- a/aircox_cms/models/sections.py +++ /dev/null @@ -1,666 +0,0 @@ -from enum import IntEnum - -from django.db import models -from django.contrib.contenttypes.models import ContentType -from django.template import Template, Context -from django.utils.translation import ugettext as _, ugettext_lazy -from django.utils.functional import cached_property -from django.urls import reverse - -from modelcluster.models import ClusterableModel -from modelcluster.fields import ParentalKey - -from wagtail.admin.edit_handlers import * -from wagtail.images.edit_handlers import ImageChooserPanel -from wagtail.core.models import Page -from wagtail.core.fields import RichTextField -from wagtail.snippets.models import register_snippet - -import aircox.models -from aircox_cms.models.lists import * -from aircox_cms.views.components import Component, ExposedData -from aircox_cms.utils import related_pages_filter - - -@register_snippet -class Region(ClusterableModel): - """ - Region is a container of multiple items of different types - that are used to render extra content related or not the current - page. - - A section has an assigned position in the page, and can be restrained - to a given type of page. - """ - name = models.CharField( - _('name'), - max_length=32, - blank = True, null = True, - help_text=_('name of this section (not displayed)'), - ) - position = models.CharField( - _('position'), - max_length=16, - blank = True, null = True, - help_text = _('name of the template block in which the section must ' - 'be set'), - ) - order = models.IntegerField( - _('order'), - default = 100, - help_text = _('order of rendering, the higher the latest') - ) - model = models.ForeignKey( - ContentType, - on_delete=models.CASCADE, - verbose_name = _('model'), - blank = True, null = True, - help_text=_('this section is displayed only when the current ' - 'page or publication is of this type'), - limit_choices_to = related_pages_filter, - ) - page = models.ForeignKey( - Page, - on_delete=models.CASCADE, - verbose_name = _('page'), - blank = True, null = True, - help_text=_('this section is displayed only on this page'), - ) - - panels = [ - MultiFieldPanel([ - FieldPanel('name'), - FieldPanel('position'), - FieldPanel('model'), - FieldPanel('page'), - ], heading=_('General')), - # InlinePanel('items', label=_('Region Items')), - ] - - @classmethod - def get_sections_at (cl, position, page = None): - """ - Return a queryset of sections that are at the given position. - Filter out Region that are not for the given page. - """ - qs = Region.objects.filter(position = position) - if page: - qs = qs.filter( - models.Q(page__isnull = True) | - models.Q(page = page) - ) - qs = qs.filter( - models.Q(model__isnull = True) | - models.Q( - model = ContentType.objects.get_for_model(page).pk - ) - ) - return qs.order_by('order','pk') - - def add_item(self, item): - """ - Add an item to the section. Automatically save the item and - create the corresponding SectionPlace. - """ - item.section = self - item.save() - - def render(self, request, page = None, context = None, *args, **kwargs): - return ''.join([ - item.specific.render(request, page, context, *args, **kwargs) - for item in self.items.all().order_by('order','pk') - ]) - - def __str__(self): - return '{}: {}'.format(self.__class__.__name__, self.name or self.pk) - - -@register_snippet -class Section(Component, models.Model): - """ - Section is a widget configurable by user that can be rendered inside - Regions. - """ - template_name = 'aircox_cms/sections/section.html' - section = ParentalKey(Region, related_name='items') - order = models.IntegerField( - _('order'), - default = 100, - help_text = _('order of rendering, the higher the latest') - ) - real_type = models.CharField( - max_length=32, - blank = True, null = True, - ) - title = models.CharField( - _('title'), - max_length=32, - blank = True, null = True, - ) - show_title = models.BooleanField( - _('show title'), - default = False, - help_text=_('if set show a title at the head of the section'), - ) - css_class = models.CharField( - _('CSS class'), - max_length=64, - blank = True, null = True, - help_text=_('section container\'s "class" attribute') - ) - - template_name = 'aircox_cms/sections/item.html' - - panels = [ - MultiFieldPanel([ - FieldPanel('section'), - FieldPanel('title'), - FieldPanel('show_title'), - FieldPanel('order'), - FieldPanel('css_class'), - ], heading=_('General')), - ] - - # TODO make it reusable - @cached_property - def specific(self): - """ - Return a downcasted version of the model if it is from another - model, or itself - """ - if not self.real_type or type(self) != Section: - return self - return getattr(self, self.real_type) - - def save(self, *args, **kwargs): - if type(self) != Section and not self.real_type: - self.real_type = type(self).__name__.lower() - return super().save(*args, **kwargs) - - def __str__(self): - return '{}: {}'.format( - (self.real_type or 'section item').replace('section','section '), - self.title or self.pk - ) - -class SectionRelativeItem(Section): - is_related = models.BooleanField( - _('is related'), - default = False, - help_text=_( - 'if set, section is related to the page being processed ' - 'e.g rendering a list of links will use thoses of the ' - 'publication instead of an assigned one.' - ) - ) - - class Meta: - abstract=True - - panels = Section.panels.copy() - panels[-1] = MultiFieldPanel( - panels[-1].children + [ FieldPanel('is_related') ], - heading = panels[-1].heading - ) - - def related_attr(self, page, attr): - """ - Return an attribute from the given page if self.is_related, - otherwise retrieve the attribute from self. - """ - return self.is_related and hasattr(page, attr) \ - and getattr(page, attr) - -@register_snippet -class SectionText(Section): - template_name = 'aircox_cms/sections/text.html' - body = RichTextField() - panels = Section.panels + [ - FieldPanel('body'), - ] - - def get_context(self, request, page): - from wagtail.core.rich_text import expand_db_html - context = super().get_context(request, page) - context['content'] = expand_db_html(self.body) - return context - -@register_snippet -class SectionImage(SectionRelativeItem): - class ResizeMode(IntEnum): - max = 0x00 - min = 0x01 - crop = 0x02 - - image = models.ForeignKey( - 'wagtailimages.Image', - on_delete=models.CASCADE, - verbose_name = _('image'), - related_name='+', - blank=True, null=True, - help_text=_( - 'If this item is related to the current page, this image will ' - 'be used only when the page has not a cover' - ) - ) - width = models.SmallIntegerField( - _('width'), - blank=True, null=True, - help_text=_('if set and > 0, sets a maximum width for the image'), - ) - height = models.SmallIntegerField( - _('height'), - blank=True, null=True, - help_text=_('if set 0 and > 0, sets a maximum height for the image'), - ) - resize_mode = models.SmallIntegerField( - verbose_name = _('resize mode'), - choices = [ (int(y), _(x)) for x,y in ResizeMode.__members__.items() ], - default = int(ResizeMode.max), - help_text=_('if the image is resized, set the resizing mode'), - ) - - panels = Section.panels + [ - ImageChooserPanel('image'), - MultiFieldPanel([ - FieldPanel('width'), - FieldPanel('height'), - FieldPanel('resize_mode'), - ], heading=_('Resizing')) - ] - - cache = "" - - - def get_filter(self): - return \ - 'original' if not (self.height or self.width) else \ - 'width-{}'.format(self.width) if not self.height else \ - 'height-{}'.format(self.height) if not self.width else \ - '{}-{}x{}'.format( - self.get_resize_mode_display(), - self.width, self.height - ) - - def ensure_cache(self, image): - """ - Ensure that we have a generated image and that it is put in cache. - We use this method since generating dynamic signatures don't generate - static images (and we need it). - """ - # Note: in order to put the generated image in db, we first need a way - # to get save events from related page or image. - if self.cache: - return self.cache - - if self.width or self.height: - template = Template( - '{% load wagtailimages_tags %}\n' + - '{{% image source {filter} as img %}}'.format( - filter = self.get_filter() - ) + - '' - ) - context = Context({ - "source": image - }) - self.cache = template.render(context) - else: - self.cache = ''.format(image.file.url) - return self.cache - - def get_context(self, request, page): - from wagtail.images.views.serve import generate_signature - context = super().get_context(request, page) - - image = self.related_attr(page, 'cover') or self.image - if not image: - return context - - context['content'] = self.ensure_cache(image) - return context - - -@register_snippet -class SectionLinkList(ClusterableModel, Section): - template_name = 'aircox_cms/sections/link_list.html' - panels = Section.panels + [ - InlinePanel('links', label=_('Links')), - ] - - -@register_snippet -class SectionLink(RelatedLinkBase, Component): - """ - Render a link to a page or a given url. - Can either be used standalone or in a SectionLinkList - """ - template_name = 'aircox_cms/snippets/link.html' - parent = ParentalKey( - 'SectionLinkList', related_name = 'links', - null = True - ) - - def __str__(self): - return 'link: {} #{}'.format( - self.text or (self.page and self.page.title) or self.title, - self.pk - ) - - -@register_snippet -class SectionList(BaseList, SectionRelativeItem): - """ - This one is quite badass, but needed: render a list of pages - using given parameters (cf. BaseList). - - If focus_available, the first article in the list will be the last - article with a focus, and will be rendered in a bigger size. - """ - template_name = 'aircox_cms/sections/list.html' - # TODO/FIXME: focus, quid? - # TODO: logs in menu show headline??? - url_text = models.CharField( - _('text of the url'), - max_length=32, - blank = True, null = True, - help_text = _('use this text to display an URL to the complete ' - 'list. If empty, no link is displayed'), - ) - - panels = SectionRelativeItem.panels + [ - FieldPanel('url_text'), - ] + BaseList.panels - - def get_context(self, request, page): - import aircox_cms.models as cms - if self.is_related and not self.related: - # set current page if there is not yet a related page only - self.related = page - - context = BaseList.get_context(self, request, paginate = False) - if not context['object_list'].count(): - self.hide = True - return {} - - context.update(SectionRelativeItem.get_context(self, request, page)) - if self.url_text: - self.related = self.related and self.related.specific - target = None - if self.related and hasattr(self.related, 'get_list_page'): - target = self.related.get_list_page() - - if not target: - settings = cms.WebsiteSettings.for_site(request.site) - target = settings.list_page - context['url'] = self.to_url(page = target) + '&view=list' - return context - -SectionList._meta.get_field('count').default = 5 - - -@register_snippet -class SectionLogsList(Section): - template_name = 'aircox_cms/sections/logs_list.html' - station = models.ForeignKey( - aircox.models.Station, - verbose_name = _('station'), - null = True, - on_delete=models.SET_NULL, - help_text = _('(required) the station on which the logs happened') - ) - count = models.SmallIntegerField( - _('count'), - default = 5, - help_text = _('number of items to display in the list (max 100)'), - ) - - class Meta: - verbose_name = _('list of logs') - verbose_name_plural = _('lists of logs') - - panels = Section.panels + [ - FieldPanel('station'), - FieldPanel('count'), - ] - - @staticmethod - def as_item(log): - """ - Return a log object as a DiffusionPage or ListItem. - Supports: Log/Track, Diffusion - """ - from aircox_cms.models import DiffusionPage - if log.diffusion: - return DiffusionPage.as_item(log.diffusion) - - track = log.track - return ListItem( - title = '{artist} -- {title}'.format( - artist = track.artist, - title = track.title, - ), - headline = track.info, - date = log.date, - info = '♫', - css_class = 'track' - ) - - def get_context(self, request, page): - context = super().get_context(request, page) - context['object_list'] = [ - self.as_item(item) - for item in self.station.on_air(count = min(self.count, 100)) - ] - return context - - -@register_snippet -class SectionTimetable(Section,DatedBaseList): - template_name = 'aircox_cms/sections/timetable.html' - class Meta: - verbose_name = _('Section: Timetable') - verbose_name_plural = _('Sections: Timetable') - - station = models.ForeignKey( - aircox.models.Station, - on_delete=models.CASCADE, - verbose_name = _('station'), - help_text = _('(required) related station') - ) - target = models.ForeignKey( - 'aircox_cms.TimetablePage', - on_delete=models.CASCADE, - verbose_name = _('timetable page'), - blank = True, null = True, - help_text = _('select a timetable page used to show complete timetable'), - ) - nav_visible = models.BooleanField( - _('show date navigation'), - default = True, - help_text = _('if checked, navigation dates will be shown') - ) - - # TODO: put in multi-field panel of DatedBaseList - panels = Section.panels + DatedBaseList.panels + [ - MultiFieldPanel([ - FieldPanel('nav_visible'), - FieldPanel('station'), - FieldPanel('target'), - ], heading=_('Timetable')), - ] - - def get_queryset(self, context): - from aircox_cms.models import DiffusionPage - diffs = [] - for date in context['nav_dates']['dates']: - items = [ - DiffusionPage.as_item(item) - for item in aircox.models.Diffusion.objects \ - .station(self.station).at(date) - ] - diffs.append((date, items)) - return diffs - - def get_context(self, request, page): - context = super().get_context(request, page) - context.update(self.get_date_context()) - context['object_list'] = self.get_queryset(context) - context['target'] = self.target - if not self.nav_visible: - del context['nav_dates']['dates']; - return context - - -@register_snippet -class SectionPublicationInfo(Section): - template_name = 'aircox_cms/sections/publication_info.html' - class Meta: - verbose_name = _('Section: publication\'s info') - verbose_name_plural = _('Sections: publication\'s info') - -@register_snippet -class SectionSearchField(Section): - template_name = 'aircox_cms/sections/search_field.html' - default_text = models.CharField( - _('default text'), - max_length=32, - default=_('search'), - help_text=_('text to display when the search field is empty'), - ) - - class Meta: - verbose_name = _('Section: search field') - verbose_name_plural = _('Sections: search field') - - panels = Section.panels + [ - FieldPanel('default_text'), - ] - - - -@register_snippet -class SectionPlaylist(Section): - """ - User playlist. Can be used to add sounds in it -- there should - only be one for the moment. - """ - class Track(ExposedData): - """ - Class exposed to Javascript playlist manager as Track. - """ - fields = { - 'name': 'name', - 'embed': 'embed', - 'duration': lambda e, o: - o.duration.hour * 3600 + o.duration.minute * 60 + - o.duration.second - , - 'duration_str': lambda e, o: - (str(o.duration.hour) + '"' if o.duration.hour else '') + - str(o.duration.minute) + "'" + str(o.duration.second) - , - 'sources': lambda e, o: [ o.url() ], - 'detail_url': - lambda e, o: o.diffusion and hasattr(o.diffusion, 'page') \ - and o.diffusion.page.url - , - 'cover': - lambda e, o: o.diffusion and hasattr(o.diffusion, 'page') \ - and o.diffusion.page.icon - , - } - - user_playlist = models.BooleanField( - _('user playlist'), - default = False, - help_text = _( - 'this is a user playlist, it can be edited and saved by the ' - 'users (the modifications will NOT be registered on the server)' - ) - ) - read_all = models.BooleanField( - _('read all'), - default = True, - help_text = _( - 'by default at the end of the sound play the next one' - ) - ) - - tracks = None - - template_name = 'aircox_cms/sections/playlist.html' - panels = Section.panels + [ - FieldPanel('user_playlist'), - FieldPanel('read_all'), - ] - - def __init__(self, *args, sounds = None, tracks = None, page = None, **kwargs): - """ - Init playlist section. If ``sounds`` is given initialize playlist - tracks with it. If ``page`` is given use it for Track infos - related to a page (cover, detail_url, ...) - """ - self.tracks = (tracks or []) + [ - self.Track(object = sound, detail_url = page and page.url, - cover = page and page.icon) - for sound in sounds or [] - ] - super().__init__(*args, **kwargs) - - def get_context(self, request, page): - context = super().get_context(request, page) - context.update({ - 'is_default': self.user_playlist, - 'modifiable': self.user_playlist, - 'storage_key': self.user_playlist and str(self.pk), - 'read_all': self.read_all, - 'tracks': self.tracks - }) - if not self.user_playlist and not self.tracks: - self.hide = True - return context - - -@register_snippet -class SectionPlayer(Section): - """ - Radio stream player. - """ - template_name = 'aircox_cms/sections/playlist.html' - live_title = models.CharField( - _('live title'), - max_length = 32, - help_text = _('text to display when it plays live'), - ) - streams = models.TextField( - _('audio streams'), - help_text = _('one audio stream per line'), - ) - icon = models.ImageField( - _('icon'), - blank = True, null = True, - help_text = _('icon to display in the player') - ) - - class Meta: - verbose_name = _('Section: Player') - - panels = Section.panels + [ - FieldPanel('live_title'), - FieldPanel('icon'), - FieldPanel('streams'), - ] - - def get_context(self, request, page): - context = super().get_context(request, page) - context['tracks'] = [SectionPlaylist.Track( - name = self.live_title, - sources = self.streams.split('\r\n'), - data_url = reverse('aircox.on_air'), - interval = 10, - run = True, - )] - return context - - diff --git a/aircox_cms/settings.py b/aircox_cms/settings.py deleted file mode 100755 index 88908ce..0000000 --- a/aircox_cms/settings.py +++ /dev/null @@ -1,22 +0,0 @@ -import os - -from django.conf import settings - -AIRCOX_CMS_BLEACH_COMMENT_TAGS = [ - 'i', 'emph', 'b', 'strong', 'strike', 's', - 'p', 'span', 'quote','blockquote','code', - 'sup', 'sub', 'a', -] - -AIRCOX_CMS_BLEACH_COMMENT_ATTRS = { - '*': ['title'], - 'a': ['href', 'rel'], -} - - -# import settings -for k, v in settings.__dict__.items(): - if not k.startswith('__') and k not in globals(): - globals()[k] = v - - diff --git a/aircox_cms/signals.py b/aircox_cms/signals.py deleted file mode 100755 index cdd01cd..0000000 --- a/aircox_cms/signals.py +++ /dev/null @@ -1,196 +0,0 @@ -from django.contrib.contenttypes.models import ContentType -from django.db.models import Q -from django.db.models.signals import post_save, pre_delete -from django.dispatch import receiver -from django.utils import timezone as tz -from django.utils.translation import ugettext as _, ugettext_lazy - -from wagtail.core.models import Page, Site, PageRevision - -import aircox.models as aircox -import aircox_cms.models as models -import aircox_cms.models.sections as sections -import aircox_cms.utils as utils - -# on a new diffusion - -@receiver(post_save, sender=aircox.Station) -def station_post_saved(sender, instance, created, *args, **kwargs): - """ - Create the basis for the website: set up settings and pages - that are common. - """ - if not created: - return - - # root pages - root_page = Page.objects.get(id=1) - - homepage = models.Publication( - title = instance.name, - slug = instance.slug, - body = _( - 'If you see this page, then Aircox is running for the station ' - '{station.name}. You might want to change it to a better one. ' - ).format(station = instance), - ) - root_page.add_child(instance=homepage) - - # Site - default_site = Site.objects.filter(is_default_site = True).first() - is_default_site = False - if default_site and default_site.pk == 1: - # default website generated by wagtail: disable is_default_site so - # we can use it for us - default_site.is_default_site = False - default_site.save() - is_default_site = True - - site = Site( - # /doc/ when a Station is created, a wagtail Site is generated with - # default options. User must set the correct localhost afterwards - hostname = instance.slug + ".local", - port = 80, - site_name = instance.name.capitalize(), - root_page = homepage, - is_default_site = is_default_site, - ) - site.save() - - # settings - website_settings = models.WebsiteSettings( - site = site, - station = instance, - description = _("The website of the {name} radio").format( - name = instance.name - ), - # Translators: tags set by default in description of the website - tags = _('radio,{station.name}').format(station = instance) - ) - - # timetable - timetable = models.TimetablePage( - title = _('Timetable'), - ) - homepage.add_child(instance = timetable) - - # list page (search, terms) - list_page = models.DynamicListPage( - # title is dynamic: no need to specify - title = _('Search'), - ) - homepage.add_child(instance = list_page) - website_settings.list_page = list_page - - # programs' page: list of programs in a section - programs = models.Publication( - title = _('Programs'), - ) - homepage.add_child(instance = programs) - - section = sections.Region( - name = _('programs'), - position = 'post_content', - page = programs, - ) - section.save(); - section.add_item(sections.SectionList( - count = 15, - title = _('Programs'), - url_text = _('All programs'), - model = ContentType.objects.get_for_model(models.ProgramPage), - related = programs, - )) - - website_settings.default_programs_page = programs - website_settings.sync = True - - # logs (because it is a cool feature) - logs = models.LogsPage( - title = _('Previously on air'), - station = instance, - ) - homepage.add_child(instance = logs) - - # save - site.save() - website_settings.save() - - -@receiver(post_save, sender=aircox.Program) -def program_post_saved(sender, instance, created, *args, **kwargs): - if not created or hasattr(instance, 'page'): - return - - settings = utils.get_station_settings(instance.station) - if not settings or not settings.sync: - return - - parent = settings.default_programs_page or \ - settings.site.root_page - if not parent: - return - - page = models.ProgramPage( - program = instance, - title = instance.name, - live = False, - # Translators: default content of a page for program - body = _('{program.name} is a program on {station.name}.').format( - program = instance, - station = instance.station - ) - ) - parent.add_child(instance = page) - - -def clean_page_of(instance): - """ - Delete empty pages for the given instance object; we assume instance - has a One-To-One relationship with a page. - - Empty is defined on theses parameters: - - `numchild = 0` => no children - - no headline - - no body - """ - if not hasattr(instance, 'page'): - return - - page = instance.page - if page.numchild > 0 or page.headline or page.body: - return - - page.delete() - - -@receiver(pre_delete, sender=aircox.Program) -def program_post_deleted(sender, instance, *args, **kwargs): - clean_page_of(instance) - - -@receiver(post_save, sender=aircox.Diffusion) -def diffusion_post_saved(sender, instance, created, *args, **kwargs): - initial = instance.initial - if initial: - if not created and hasattr(instance, 'page'): - # fuck it - page = instance.page - page.diffusion = None - page.save() - return - - if hasattr(instance, 'page'): - return - - page = models.DiffusionPage.from_diffusion( - instance, live = False - ) - page = instance.program.page.add_child( - instance = page - ) - -@receiver(pre_delete, sender=aircox.Diffusion) -def diffusion_pre_deleted(sender, instance, *args, **kwargs): - clean_page_of(instance) - diff --git a/aircox_cms/static/aircox_cms/css/layout.css b/aircox_cms/static/aircox_cms/css/layout.css deleted file mode 100755 index ff6f184..0000000 --- a/aircox_cms/static/aircox_cms/css/layout.css +++ /dev/null @@ -1,627 +0,0 @@ -/** - * Define rules for the default layouts, and some useful classes - */ - -/** general **/ -body { - background-color: #F2F2F2; - font-family: "Myriad Pro",Calibri,Helvetica,Arial,sans-serif; -} - -h1, h2, h3, h4, h5 { - font-family: "Myriad Pro",Calibri,Helvetica,Arial,sans-serif; - margin: 0.4em 0em; -} - -h1:first-letter, h2:first-letter, h3:first-letter, h4:first-letter { - text-transform: capitalize; -} - -h1 { font-size: 1.4em; } -h2 { font-size: 1.2em; } -h3 { font-size: 0.9em; } -h4 { font-size: 0.8em; } - -h1 > *, h2 > *, h3 > *, h4 > * { vertical-align: middle; } - - -a { - cursor: pointer; - text-decoration: none; - color: #616161; -} - -a:hover { color: #007EDF; } -a:hover > .small_icon { box-shadow: 0em 0em 0.1em #007EDF; } - -ul { margin: 0em; } - - -/**** position & box ****/ -.float_right { float: right; } -.float_left { float: left; } - - -.flex_row { - display: -webkit-flex; - display: flex; - -webkit-flex-direction: row; - flex-direction: row; -} - -.flex_column { - display: -webkit-flex; - display: flex; - -webkit-flex-direction: column; - flex-direction: column; -} - -.flex_row > .flex_item, -.flex_column > .flex_item { - -webkit-flex: auto; - flex: auto; -} - - -.small { - font-size: 0.8em; -} - - - -/**** indicators & info ****/ -time, .tags { - font-size: 0.9em; - color: #616161; -} - -.info { - font-size: 0.9em; - padding: 0.1em; - color: #007EDF; -} - -.error { color: red; } -.warning { color: orange; } -.success { color: green; } - -.icon { - max-width: 2em; - max-height: 2em; - vertical-align: middle; -} - -.small_icon { - max-height: 1.5em; - vertical-align: middle; -} - - -/** main layout **/ -body > * { - max-width: 92em; - margin: 0em auto; - padding: 0em; -} - - -.menu { - padding: 0.4em; -} - -.menu:empty { - display: none; -} - - -.menu.row section { - display: inline-block; -} - -.menu.col > section { - margin-bottom: 1em; -} - - -/**** top + header layout ****/ -body > .top { - position: fixed; - z-index: 10000000; - top: 0; - left: 0; - width: 100%; - max-width: 100%; - - margin: 0em auto; - background-color: white; - border-bottom: 0.1em #dfdfdf solid; - box-shadow: 0em 0.1em 0.1em rgba(255,255,255,0.7); - box-shadow: 0em 0.1em 0.5em rgba(0,0,0,0.1); - - transition: opacity 1.5s; -} - - body > .top > .menu { - max-width: 92em; - height: 2.5em; - margin: 0em auto; - } - - body[scrollY] > .top { - opacity: 0.1; - transition: opacity 1.5s 1s; - } - - body > .top:hover { - opacity: 1.0; - transition: opacity 1.5s; - } - - -body > .header { - overflow: hidden; - margin-top: 3.3em; - margin-bottom: 1em; -} - - /** FIXME: remove this once image slides impled **/ - body > .header > div { - width: 15000%; - } - - body > .header > div > section { - margin: 0; - margin-right: -0.4em; - } - - - -/**** page layout ****/ -.page { - display: flex; -} - -.page > main { - flex: auto; - - overflow: hidden; - margin: 0em 0em; - border-radius: 0.4em; - border: 0.1em #dfdfdf solid; - - background-color: rgba(255,255,255,0.9); - box-shadow: inset 0.1em 0.1em 0.2em rgba(255, 255, 255, 0.8); -} - - -.page > nav { - flex: 1; - width: 50em; - overflow: hidden; - max-width: 16em; -} - - .page > .menu.col:first-child { margin-right: 2em; } - .page > main + .menu.col { margin-left: 2em; } - - - -/**** page main ****/ -main:not(.detail) h1 { - margin: 0em 0em 0.4em 0em; -} - -main .post_content { - display: block; -} - -main .post_content section { - display: inline-block; - width: calc(50% - 1em); - vertical-align: top; -} - - -main.detail { - padding: 0em; - margin: 0em; -} - - main > .content { - padding: 1em; - } - - main > header { - margin: 0em; - padding: 1em; - position: relative; - } - - main > header .foreground { - position: absolute; - left: 0em; - top: 0em; - width: calc(100% - 2em); - padding: 1em; - } - - main > header h1 { - width: calc(100% - 2em); - margin: 0em; - margin-bottom: 0.8em; - } - - main header .headline { - display: inline-block; - width: calc(60% - 0.8em); - min-height: 1.2em; - font-size: 1.2em; - font-weight: bold; - } - - main > header .background { - margin: -1em; - height: 17em; - overflow: hidden; - position: relative; - } - - main > header .background img { - position: absolute; - /*! top: -40%; */ - /*! left: -40%; */ - width: 100%; - min-height: 100%; - filter: blur(20px); - opacity: 0.3; - } - - main > header .cover { - right: 0em; - top: 1em; - width: auto; - max-height: calc(100% - 2em); - max-width: calc(40% - 2em); - margin: 1em; - position: absolute; - box-shadow: 0em 0em 4em rgba(0, 0, 0, 0.4); - } - - - -/** sections **/ -body section ul { - padding: 0em; - padding-left: 1em; -} - - -/**** link list ****/ -.menu.row .section_link_list > a { - display: inline-block; - margin: 0.2em 1em; -} - -.menu.col .section_link_list > a { - display: block; -} - - - - - -/** content: menus **/ -/** content: list & items **/ -.list { - width: 100%; -} - -ul.list, .list > ul { - padding: 0.4em; -} - -.list_item { - margin: 0.4em 0; -} - -.list_item > *:not(:last-child) { - margin-right: 0.4em; -} - -.list_item img.cover.big { - display: block; - max-width: 100%; - min-height: 15em; - margin: auto; -} - -.list_item img.cover.small { - margin-right: 0.4em; - border-radius: 0.4em; - float: left; - min-height: 64px; -} - -.list_item > * { - margin: 0em 0.2em; - vertical-align: middle; -} - - -.list nav { - text-align: center; - font-size: 0.9em; -} - - -/** content: list items in full page **/ -.content > .list:not(.date_list) .list_item { - min-width: 20em; - display: inline-block; - min-height: 2.5em; - margin: 0.4em; -} - -/** content: date list **/ -.date_list nav { - text-align:center; -} - - .date_list nav a { - display: inline-block; - width: 2em; - } - - .date_list nav a.date { - width: 4em; - } - - .date_list nav a[selected] { - color: #007EDF; - border-bottom: 0.2em #007EDF dotted; - } - -.date_list ul:not([selected]) { - display: none; -} - -.date_list ul:target { - display: block; -} - - .date_list h2 { - display: none; - } - -.date_list_item .cover.small { - width: 64px; - margin: 0.4em; -} - -.date_list_item h3 { - margin-top: 0em; -} - -.date_list_item time { - color: #007EDF; -} - - -.date_list_item.now { - padding: 0.4em; -} - - .date_list_item img.now { - width: 1.3em; - vertical-align: bottom; - } - - -/** content: date list in full page **/ -.content > .date_list .date_list_item time { - color: #007EDF; - font-size: 1.1em; - display: block; -} - - -.content > .date_list .date_list_item:nth-child(2n+1), -.date_list_item.now { - box-shadow: inset 0em 0em 3em rgba(0, 124, 226, 0.1); - background-color: rgba(0, 124, 226, 0.05); -} - -.content > .date_list { - padding: 0 10%; - margin: auto; - width: 80%; -} - - -/** content: comments **/ -.comments form input:not([type=checkbox]), -.comments form textarea { - display: inline-block; - width: 100%; - max-height: 6em; - margin: 0.2em 0em; - padding: 0.2em; -} - -.comments form input[type=checkbox], -.comments form button[type=submit] { - vertical-align:bottom; - margin: 0.2em 0em; - text-align: center; -} - -.comments form button[type=submit] { - float: right; -} - -.comments form #show_more:not(:checked) ~ .extra { - display: none; -} - -.comments label[for="show_more"] { - font-size: 0.8em; -} - -.comments ul { - margin-top: 2.5em; -} - -.comment { - list-style: none; - border: 1px #818181 dotted; - margin: 0.4em 0em; -} - - .comment .metadata { - font-size: 0.9em; - } - - .comment time { - float: right; - } - - -/** component: sound **/ -.component.sound { - display: flex; - flex-direction: row; - margin: 0.2em; - width: 100%; -} - - .component.sound[state="play"] button { - animation-name: sound-blink; - animation-duration: 4s; - animation-iteration-count: infinite; - animation-direction: alternate; - } - - @keyframes sound-blink { - from { background-color: rgba(255, 255, 255, 0); } - to { background-color: rgba(255, 255, 255, 0.6); } - } - - -.component.sound .button { - width: 4em; - height: 4em; - cursor: pointer; - position: relative; - margin-right: 0.4em; -} - - .component.sound .button > img { - width: 100%; - height: 100%; - } - - .component.sound button { - transition: background-color 0.5s; - background-color: rgba(255,255,255,0.1); - position: absolute; - cursor: pointer; - left: 0; - top: 0; - width: 100%; - height: 100%; - border: 0; - } - - .component.sound button:hover { - background-color: rgba(255,255,255,0.5); - } - - .component.sound button > img { - background-color: rgba(255,255,255,0.9); - border-radius: 50%; - } - -.component.sound .content { - position: relative; -} - - .component.sound .info { - text-align: right; - } - - .component.sound progress { - width: 100%; - position: absolute; - bottom: 0; - height: 0.4em; - } - - .component.sound progress:hover { - height: 1em; - } - - -/** component: playlist **/ -.component.playlist footer { - text-align: right; - display: block; -} - -.component.playlist .read_all { - display: none; -} - - .component.playlist .read_all + label { - display: inline-block; - padding: 0.1em; - margin-left: 0.2em; - cursor: pointer; - font-size: 1em; - box-shadow: inset 0em 0em 0.1em #818181; - } - - .component.playlist .read_all:not(:checked) + label { - border-left: 0.1em #818181 solid; - margin-right: 0em; - } - - .component.playlist .read_all:checked + label { - border-right: 0.1em #007EDF solid; - box-shadow: inset 0em 0em 0.1em #007EDF; - margin-right: 0em; - } - - -/** content: page **/ -main .body ~ section:not(.comments) { - width: calc(50% - 1em); - vertical-align: top; - display: inline-block; -} - -.meta .author .headline { - display: none; -} - - .meta .link_list > a { - font-size: 0.9em; - margin: 0em 0.1em; - padding: 0.2em; - line-height: 1.4em; - } - - .meta .link_list > a:hover { - border-radius: 0.2em; - background-color: rgba(0, 126, 223, 0.1); - } - - -/** content: others **/ -.list_item.track .title { - display: inline; - font-style: italic; - font-weight: normal; - font-size: 0.9em; -} - - diff --git a/aircox_cms/static/aircox_cms/css/theme.css b/aircox_cms/static/aircox_cms/css/theme.css deleted file mode 100755 index 4cad9a8..0000000 --- a/aircox_cms/static/aircox_cms/css/theme.css +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Define a default theme, that is the one for RadioCampus - * - * Colors: - * - light: - * - background: #F2F2F2 - * - color: #000 - * - * - dark: - * - background: #212121 - * - color: #007EDF - * - * - info: - * - generic (time,url,...): #616161 - * - additional: #007EDF - * - active: #007EDF - */ - - -/** detail view **/ - - - -@keyframes rotate { - from { - transform: rotate(0deg); - } - - to { - transform: rotate(360deg); - } -} - - - -/** section: playlist **/ -.playlist .title { - font-style: italic; - color: #616161; -} - -section.playlist .artist { - display: inline-block; - margin-right: 0.4em; -} -section.playlist .artist:after { - padding-left: 0.2em; - content: ':' -} - diff --git a/aircox_cms/static/aircox_cms/js/bootstrap.js b/aircox_cms/static/aircox_cms/js/bootstrap.js deleted file mode 100644 index e36eb3e..0000000 --- a/aircox_cms/static/aircox_cms/js/bootstrap.js +++ /dev/null @@ -1,41 +0,0 @@ - -scroll_margin = 0 -window.addEventListener('scroll', function(e) { - if(window.scrollX > scroll_margin) - document.body.setAttribute('scrollX', 1) - else - document.body.removeAttribute('scrollX') - - if(window.scrollY > scroll_margin) - document.body.setAttribute('scrollY', 1) - else - document.body.removeAttribute('scrollY') -}); - - - -/// TODO: later get rid of it in order to use Vue stuff -/// Helper to provide a tab+panel functionnality; the tab and the selected -/// element will have an attribute "selected". -/// We assume a common ancestor between tab and panel at a maximum level -/// of 2. -/// * tab: corresponding tab -/// * panel_selector is used to select the right panel object. -function select_tab(tab, panel_selector) { - var parent = tab.parentNode.parentNode; - var panel = parent.querySelector(panel_selector); - - // unselect - var qs = parent.querySelectorAll('*[selected]'); - for(var i = 0; i < qs.length; i++) - if(qs[i] != tab && qs[i] != panel) - qs[i].removeAttribute('selected'); - - panel.setAttribute('selected', 'true'); - tab.setAttribute('selected', 'true'); -} - - - - - diff --git a/aircox_cms/static/aircox_cms/js/player.js b/aircox_cms/static/aircox_cms/js/player.js deleted file mode 100644 index 8c731da..0000000 --- a/aircox_cms/static/aircox_cms/js/player.js +++ /dev/null @@ -1,336 +0,0 @@ -/* Implementation status: -- TODO - * - proper design - * - mini-button integration in lists (list of diffusion articles) - */ - - -var State = Object.freeze({ - Stop: 'stop', - Loading: 'loading', - Play: 'play', -}); - - -class Track { - // Create a track with the given data. - // If url and interval are given, use them to retrieve regularely - // the track informations - constructor(data) { - Object.assign(this, { - 'name': '', - 'detail_url': '', - }); - - Object.assign(this, data); - - if(this.data_url) { - if(!this.interval) - this.data_url = undefined; - if(this.run) { - this.run = false; - this.start(); - } - } - } - - start() { - if(this.run || !this.interval || !this.data_url) - return; - this.run = true; - this.fetch_data(); - } - - stop() { - this.run = false; - } - - fetch_data() { - if(!this.run || !this.interval || !this.data_url) - return; - - var self = this; - var req = new XMLHttpRequest(); - req.open('GET', this.data_url, true); - req.onreadystatechange = function() { - if(req.readyState != 4 || (req.status && req.status != 200)) - return; - if(!req.responseText.length) - return; - - // TODO: more consistent API - var data = JSON.parse(req.responseText); - if(data.type == 'track') - data = { - name: '♫ ' + (data.artist ? data.artist + ' — ' : '') + - data.title, - detail_url: '' - } - else - data = { - name: data.title, - detail_url: data.url - } - Object.assign(self, data); - }; - req.send(); - - if(this.run && this.interval) - this._trigger_fetch(); - } - - _trigger_fetch() { - if(!this.run || !this.data_url) - return; - - var self = this; - if(this.interval) - window.setTimeout(function() { - self.fetch_data(); - }, this.interval*1000); - else - this.fetch_data(); - } -} - - -/// Current selected sound (being played) -var CurrentSound = null; - -var Sound = Vue.extend({ - template: '#template-sound', - delimiters: ['[[', ']]'], - - data: function() { - return { - mounted: false, - // sound state, - state: State.Stop, - // current position in playing sound - position: 0, - // estimated position when user mouse over progress bar - user_seek: null, - }; - }, - - computed: { - // sound can be seeked - seekable() { - // seekable: for the moment only when we have a podcast file - // note: need mounted because $refs is not reactive - return this.mounted && this.duration && this.$refs.audio.seekable; - }, - - // sound duration in seconds - duration() { - return this.track.duration; - }, - - seek_position() { - return (this.user_seek === null && this.position) || - this.user_seek; - }, - }, - - props: { - track: { type: Object, required: true }, - }, - - mounted() { - this.mounted = true; - console.log(this.track, this.track.detail_url); - this.detail_url = this.track.detail_url; - this.storage_key = "sound." + this.track.sources[0]; - - var pos = localStorage.getItem(this.storage_key) - if(pos) try { - // go back of 5 seconds - pos = parseFloat(pos) - 5; - if(pos > 0) - this.$refs.audio.currentTime = pos; - } catch (e) {} - }, - - methods: { - // - // Common methods - // - stop() { - this.$refs.audio.pause(); - CurrentSound = null; - }, - - play(reset = false) { - if(CurrentSound && CurrentSound != this) - CurrentSound.stop(); - CurrentSound = this; - if(reset) - this.$refs.audio.currentTime = 0; - this.$refs.audio.play(); - }, - - play_stop() { - if(this.state == State.Stop) - this.play(); - else - this.stop(); - }, - - add_to_playlist() { - if(!DefaultPlaylist) - return; - var tracks = DefaultPlaylist.tracks; - if(tracks.indexOf(this.track) == -1) - DefaultPlaylist.tracks.push(this.track); - }, - - remove() { - this.stop(); - var tracks = this.$parent.tracks; - var i = tracks.indexOf(this.track); - if(i == -1) - return; - tracks.splice(i, 1); - }, - - // - // Utils functions - // - _as_progress_time(event) { - bounding = this.$refs.progress.getBoundingClientRect() - offset = (event.clientX - bounding.left); - return offset * this.$refs.audio.duration / bounding.width; - }, - - // format seconds into time string such as: [h"m]m'ss - format_time(seconds) { - seconds = Math.floor(seconds); - var hours = Math.floor(seconds / 3600); - seconds -= hours * 3600; - var minutes = Math.floor(seconds / 60); - seconds -= minutes * 60; - - return (hours ? ((hours < 10 ? '0' + hours : hours) + '"') : '') + - minutes + "'" + seconds - ; - }, - - - // - // Events - // - timeUpdate() { - this.position = this.$refs.audio.currentTime; - if(this.state == State.Play) - localStorage.setItem( - this.storage_key, this.$refs.audio.currentTime - ); - }, - - ended() { - this.state = State.Stop; - this.$refs.audio.currentTime = 0; - localStorage.removeItem(this.storage_key); - this.$emit('ended', this); - }, - - progress_mouse_out(event) { - this.user_seek = null; - }, - - progress_mouse_move(event) { - if(this.$refs.audio.duration == Infinity || - isNaN(this.$refs.audio.duration)) - return; - this.user_seek = this._as_progress_time(event); - }, - - progress_clicked(event) { - this.$refs.audio.currentTime = this._as_progress_time(event); - this.play(); - event.stopImmediatePropagation(); - }, - } -}); - - -/// User's default playlist -DefaultPlaylist = null; - -var Playlist = Vue.extend({ - template: '#template-playlist', - delimiters: ['[[', ']]'], - data() { - return { - // if true, use this playlist as user's default playlist - default: false, - // read all mode enabled - read_all: false, - // playlist can be modified by user - modifiable: false, - // if set, save items into localstorage using this root key - storage_key: null, - // sounds info - tracks: [], - }; - }, - - computed: { - // id of the read all mode checkbox switch - read_all_id() { - return this.id + "_read_all"; - } - }, - - mounted() { - // set default - if(this.default) { - if(DefaultPlaylist) - this.tracks = DefaultPlaylist.tracks; - else - DefaultPlaylist = this; - } - - // storage_key - if(this.storage_key) { - tracks = localStorage.getItem('playlist.' + this.storage_key); - if(tracks) - this.tracks = JSON.parse(tracks); - } - - console.log(this.tracks) - }, - - methods: { - sound_ended(sound) { - // ensure sound is stopped (beforeDestroy()) - sound.stop(); - - // next only when read all mode - if(!this.read_all) - return; - - var sounds = this.$refs.sounds; - var id = sounds.findIndex(s => s == sound); - if(id < 0 || id+1 >= sounds.length) - return - id++; - sounds[id].play(true); - }, - }, - - watch: { - tracks: { - handler() { - if(!this.storage_key) - return; - localStorage.setItem('playlist.' + this.storage_key, - JSON.stringify(this.tracks)); - }, - deep: true, - } - } -}); - -Vue.component('a-sound', Sound); -Vue.component('a-playlist', Playlist); - diff --git a/aircox_cms/static/aircox_cms/js/utils.js b/aircox_cms/static/aircox_cms/js/utils.js deleted file mode 100755 index b5f527e..0000000 --- a/aircox_cms/static/aircox_cms/js/utils.js +++ /dev/null @@ -1,68 +0,0 @@ - -/// Helper to provide a tab+panel functionnality; the tab and the selected -/// element will have an attribute "selected". -/// We assume a common ancestor between tab and panel at a maximum level -/// of 2. -/// * tab: corresponding tab -/// * panel_selector is used to select the right panel object. -function select_tab(tab, panel_selector) { - var parent = tab.parentNode.parentNode; - var panel = parent.querySelector(panel_selector); - - // unselect - var qs = parent.querySelectorAll('*[selected]'); - for(var i = 0; i < qs.length; i++) - if(qs[i] != tab && qs[i] != panel) - qs[i].removeAttribute('selected'); - - panel.setAttribute('selected', 'true'); - tab.setAttribute('selected', 'true'); -} - - -/// Utility to store objects in local storage. Data are stringified in JSON -/// format in order to keep type. -function Store(prefix) { - this.prefix = prefix; -} - -Store.prototype = { - // save data to localstorage, or remove it if data is null - set: function(key, data) { - key = this.prefix + '.' + key; - if(data == undefined) { - localStorage.removeItem(this.prefix); - return; - } - localStorage.setItem(key, JSON.stringify(data)) - }, - - // load data from localstorage - get: function(key) { - try { - key = this.prefix + '.' + key; - var data = localStorage.getItem(key); - if(data) - return JSON.parse(data); - } - catch(e) { console.log(e, data); } - }, - - // return true if the given item is stored - exists: function(key) { - key = this.prefix + '.' + key; - return (localStorage.getItem(key) != null); - }, - - // update a field in the stored data - update: function(key, field_key, value) { - data = this.get(key) || {}; - if(value) - data[field_key] = value; - else - delete data[field_key]; - this.set(key, data); - }, -} - - diff --git a/aircox_cms/static/lib/vue.js b/aircox_cms/static/lib/vue.js deleted file mode 100644 index 9d84c53..0000000 --- a/aircox_cms/static/lib/vue.js +++ /dev/null @@ -1,10798 +0,0 @@ -/*! - * Vue.js v2.5.13 - * (c) 2014-2017 Evan You - * Released under the MIT License. - */ -(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : - typeof define === 'function' && define.amd ? define(factory) : - (global.Vue = factory()); -}(this, (function () { 'use strict'; - -/* */ - -var emptyObject = Object.freeze({}); - -// these helpers produces better vm code in JS engines due to their -// explicitness and function inlining -function isUndef (v) { - return v === undefined || v === null -} - -function isDef (v) { - return v !== undefined && v !== null -} - -function isTrue (v) { - return v === true -} - -function isFalse (v) { - return v === false -} - -/** - * Check if value is primitive - */ -function isPrimitive (value) { - return ( - typeof value === 'string' || - typeof value === 'number' || - // $flow-disable-line - typeof value === 'symbol' || - typeof value === 'boolean' - ) -} - -/** - * Quick object check - this is primarily used to tell - * Objects from primitive values when we know the value - * is a JSON-compliant type. - */ -function isObject (obj) { - return obj !== null && typeof obj === 'object' -} - -/** - * Get the raw type string of a value e.g. [object Object] - */ -var _toString = Object.prototype.toString; - -function toRawType (value) { - return _toString.call(value).slice(8, -1) -} - -/** - * Strict object type check. Only returns true - * for plain JavaScript objects. - */ -function isPlainObject (obj) { - return _toString.call(obj) === '[object Object]' -} - -function isRegExp (v) { - return _toString.call(v) === '[object RegExp]' -} - -/** - * Check if val is a valid array index. - */ -function isValidArrayIndex (val) { - var n = parseFloat(String(val)); - return n >= 0 && Math.floor(n) === n && isFinite(val) -} - -/** - * Convert a value to a string that is actually rendered. - */ -function toString (val) { - return val == null - ? '' - : typeof val === 'object' - ? JSON.stringify(val, null, 2) - : String(val) -} - -/** - * Convert a input value to a number for persistence. - * If the conversion fails, return original string. - */ -function toNumber (val) { - var n = parseFloat(val); - return isNaN(n) ? val : n -} - -/** - * Make a map and return a function for checking if a key - * is in that map. - */ -function makeMap ( - str, - expectsLowerCase -) { - var map = Object.create(null); - var list = str.split(','); - for (var i = 0; i < list.length; i++) { - map[list[i]] = true; - } - return expectsLowerCase - ? function (val) { return map[val.toLowerCase()]; } - : function (val) { return map[val]; } -} - -/** - * Check if a tag is a built-in tag. - */ -var isBuiltInTag = makeMap('slot,component', true); - -/** - * Check if a attribute is a reserved attribute. - */ -var isReservedAttribute = makeMap('key,ref,slot,slot-scope,is'); - -/** - * Remove an item from an array - */ -function remove (arr, item) { - if (arr.length) { - var index = arr.indexOf(item); - if (index > -1) { - return arr.splice(index, 1) - } - } -} - -/** - * Check whether the object has the property. - */ -var hasOwnProperty = Object.prototype.hasOwnProperty; -function hasOwn (obj, key) { - return hasOwnProperty.call(obj, key) -} - -/** - * Create a cached version of a pure function. - */ -function cached (fn) { - var cache = Object.create(null); - return (function cachedFn (str) { - var hit = cache[str]; - return hit || (cache[str] = fn(str)) - }) -} - -/** - * Camelize a hyphen-delimited string. - */ -var camelizeRE = /-(\w)/g; -var camelize = cached(function (str) { - return str.replace(camelizeRE, function (_, c) { return c ? c.toUpperCase() : ''; }) -}); - -/** - * Capitalize a string. - */ -var capitalize = cached(function (str) { - return str.charAt(0).toUpperCase() + str.slice(1) -}); - -/** - * Hyphenate a camelCase string. - */ -var hyphenateRE = /\B([A-Z])/g; -var hyphenate = cached(function (str) { - return str.replace(hyphenateRE, '-$1').toLowerCase() -}); - -/** - * Simple bind, faster than native - */ -function bind (fn, ctx) { - function boundFn (a) { - var l = arguments.length; - return l - ? l > 1 - ? fn.apply(ctx, arguments) - : fn.call(ctx, a) - : fn.call(ctx) - } - // record original fn length - boundFn._length = fn.length; - return boundFn -} - -/** - * Convert an Array-like object to a real Array. - */ -function toArray (list, start) { - start = start || 0; - var i = list.length - start; - var ret = new Array(i); - while (i--) { - ret[i] = list[i + start]; - } - return ret -} - -/** - * Mix properties into target object. - */ -function extend (to, _from) { - for (var key in _from) { - to[key] = _from[key]; - } - return to -} - -/** - * Merge an Array of Objects into a single Object. - */ -function toObject (arr) { - var res = {}; - for (var i = 0; i < arr.length; i++) { - if (arr[i]) { - extend(res, arr[i]); - } - } - return res -} - -/** - * Perform no operation. - * Stubbing args to make Flow happy without leaving useless transpiled code - * with ...rest (https://flow.org/blog/2017/05/07/Strict-Function-Call-Arity/) - */ -function noop (a, b, c) {} - -/** - * Always return false. - */ -var no = function (a, b, c) { return false; }; - -/** - * Return same value - */ -var identity = function (_) { return _; }; - -/** - * Generate a static keys string from compiler modules. - */ -function genStaticKeys (modules) { - return modules.reduce(function (keys, m) { - return keys.concat(m.staticKeys || []) - }, []).join(',') -} - -/** - * Check if two values are loosely equal - that is, - * if they are plain objects, do they have the same shape? - */ -function looseEqual (a, b) { - if (a === b) { return true } - var isObjectA = isObject(a); - var isObjectB = isObject(b); - if (isObjectA && isObjectB) { - try { - var isArrayA = Array.isArray(a); - var isArrayB = Array.isArray(b); - if (isArrayA && isArrayB) { - return a.length === b.length && a.every(function (e, i) { - return looseEqual(e, b[i]) - }) - } else if (!isArrayA && !isArrayB) { - var keysA = Object.keys(a); - var keysB = Object.keys(b); - return keysA.length === keysB.length && keysA.every(function (key) { - return looseEqual(a[key], b[key]) - }) - } else { - /* istanbul ignore next */ - return false - } - } catch (e) { - /* istanbul ignore next */ - return false - } - } else if (!isObjectA && !isObjectB) { - return String(a) === String(b) - } else { - return false - } -} - -function looseIndexOf (arr, val) { - for (var i = 0; i < arr.length; i++) { - if (looseEqual(arr[i], val)) { return i } - } - return -1 -} - -/** - * Ensure a function is called only once. - */ -function once (fn) { - var called = false; - return function () { - if (!called) { - called = true; - fn.apply(this, arguments); - } - } -} - -var SSR_ATTR = 'data-server-rendered'; - -var ASSET_TYPES = [ - 'component', - 'directive', - 'filter' -]; - -var LIFECYCLE_HOOKS = [ - 'beforeCreate', - 'created', - 'beforeMount', - 'mounted', - 'beforeUpdate', - 'updated', - 'beforeDestroy', - 'destroyed', - 'activated', - 'deactivated', - 'errorCaptured' -]; - -/* */ - -var config = ({ - /** - * Option merge strategies (used in core/util/options) - */ - // $flow-disable-line - optionMergeStrategies: Object.create(null), - - /** - * Whether to suppress warnings. - */ - silent: false, - - /** - * Show production mode tip message on boot? - */ - productionTip: "development" !== 'production', - - /** - * Whether to enable devtools - */ - devtools: "development" !== 'production', - - /** - * Whether to record perf - */ - performance: false, - - /** - * Error handler for watcher errors - */ - errorHandler: null, - - /** - * Warn handler for watcher warns - */ - warnHandler: null, - - /** - * Ignore certain custom elements - */ - ignoredElements: [], - - /** - * Custom user key aliases for v-on - */ - // $flow-disable-line - keyCodes: Object.create(null), - - /** - * Check if a tag is reserved so that it cannot be registered as a - * component. This is platform-dependent and may be overwritten. - */ - isReservedTag: no, - - /** - * Check if an attribute is reserved so that it cannot be used as a component - * prop. This is platform-dependent and may be overwritten. - */ - isReservedAttr: no, - - /** - * Check if a tag is an unknown element. - * Platform-dependent. - */ - isUnknownElement: no, - - /** - * Get the namespace of an element - */ - getTagNamespace: noop, - - /** - * Parse the real tag name for the specific platform. - */ - parsePlatformTagName: identity, - - /** - * Check if an attribute must be bound using property, e.g. value - * Platform-dependent. - */ - mustUseProp: no, - - /** - * Exposed for legacy reasons - */ - _lifecycleHooks: LIFECYCLE_HOOKS -}); - -/* */ - -/** - * Check if a string starts with $ or _ - */ -function isReserved (str) { - var c = (str + '').charCodeAt(0); - return c === 0x24 || c === 0x5F -} - -/** - * Define a property. - */ -function def (obj, key, val, enumerable) { - Object.defineProperty(obj, key, { - value: val, - enumerable: !!enumerable, - writable: true, - configurable: true - }); -} - -/** - * Parse simple path. - */ -var bailRE = /[^\w.$]/; -function parsePath (path) { - if (bailRE.test(path)) { - return - } - var segments = path.split('.'); - return function (obj) { - for (var i = 0; i < segments.length; i++) { - if (!obj) { return } - obj = obj[segments[i]]; - } - return obj - } -} - -/* */ - - -// can we use __proto__? -var hasProto = '__proto__' in {}; - -// Browser environment sniffing -var inBrowser = typeof window !== 'undefined'; -var inWeex = typeof WXEnvironment !== 'undefined' && !!WXEnvironment.platform; -var weexPlatform = inWeex && WXEnvironment.platform.toLowerCase(); -var UA = inBrowser && window.navigator.userAgent.toLowerCase(); -var isIE = UA && /msie|trident/.test(UA); -var isIE9 = UA && UA.indexOf('msie 9.0') > 0; -var isEdge = UA && UA.indexOf('edge/') > 0; -var isAndroid = (UA && UA.indexOf('android') > 0) || (weexPlatform === 'android'); -var isIOS = (UA && /iphone|ipad|ipod|ios/.test(UA)) || (weexPlatform === 'ios'); -var isChrome = UA && /chrome\/\d+/.test(UA) && !isEdge; - -// Firefox has a "watch" function on Object.prototype... -var nativeWatch = ({}).watch; - -var supportsPassive = false; -if (inBrowser) { - try { - var opts = {}; - Object.defineProperty(opts, 'passive', ({ - get: function get () { - /* istanbul ignore next */ - supportsPassive = true; - } - })); // https://github.com/facebook/flow/issues/285 - window.addEventListener('test-passive', null, opts); - } catch (e) {} -} - -// this needs to be lazy-evaled because vue may be required before -// vue-server-renderer can set VUE_ENV -var _isServer; -var isServerRendering = function () { - if (_isServer === undefined) { - /* istanbul ignore if */ - if (!inBrowser && typeof global !== 'undefined') { - // detect presence of vue-server-renderer and avoid - // Webpack shimming the process - _isServer = global['process'].env.VUE_ENV === 'server'; - } else { - _isServer = false; - } - } - return _isServer -}; - -// detect devtools -var devtools = inBrowser && window.__VUE_DEVTOOLS_GLOBAL_HOOK__; - -/* istanbul ignore next */ -function isNative (Ctor) { - return typeof Ctor === 'function' && /native code/.test(Ctor.toString()) -} - -var hasSymbol = - typeof Symbol !== 'undefined' && isNative(Symbol) && - typeof Reflect !== 'undefined' && isNative(Reflect.ownKeys); - -var _Set; -/* istanbul ignore if */ // $flow-disable-line -if (typeof Set !== 'undefined' && isNative(Set)) { - // use native Set when available. - _Set = Set; -} else { - // a non-standard Set polyfill that only works with primitive keys. - _Set = (function () { - function Set () { - this.set = Object.create(null); - } - Set.prototype.has = function has (key) { - return this.set[key] === true - }; - Set.prototype.add = function add (key) { - this.set[key] = true; - }; - Set.prototype.clear = function clear () { - this.set = Object.create(null); - }; - - return Set; - }()); -} - -/* */ - -var warn = noop; -var tip = noop; -var generateComponentTrace = (noop); // work around flow check -var formatComponentName = (noop); - -{ - var hasConsole = typeof console !== 'undefined'; - var classifyRE = /(?:^|[-_])(\w)/g; - var classify = function (str) { return str - .replace(classifyRE, function (c) { return c.toUpperCase(); }) - .replace(/[-_]/g, ''); }; - - warn = function (msg, vm) { - var trace = vm ? generateComponentTrace(vm) : ''; - - if (config.warnHandler) { - config.warnHandler.call(null, msg, vm, trace); - } else if (hasConsole && (!config.silent)) { - console.error(("[Vue warn]: " + msg + trace)); - } - }; - - tip = function (msg, vm) { - if (hasConsole && (!config.silent)) { - console.warn("[Vue tip]: " + msg + ( - vm ? generateComponentTrace(vm) : '' - )); - } - }; - - formatComponentName = function (vm, includeFile) { - if (vm.$root === vm) { - return '' - } - var options = typeof vm === 'function' && vm.cid != null - ? vm.options - : vm._isVue - ? vm.$options || vm.constructor.options - : vm || {}; - var name = options.name || options._componentTag; - var file = options.__file; - if (!name && file) { - var match = file.match(/([^/\\]+)\.vue$/); - name = match && match[1]; - } - - return ( - (name ? ("<" + (classify(name)) + ">") : "") + - (file && includeFile !== false ? (" at " + file) : '') - ) - }; - - var repeat = function (str, n) { - var res = ''; - while (n) { - if (n % 2 === 1) { res += str; } - if (n > 1) { str += str; } - n >>= 1; - } - return res - }; - - generateComponentTrace = function (vm) { - if (vm._isVue && vm.$parent) { - var tree = []; - var currentRecursiveSequence = 0; - while (vm) { - if (tree.length > 0) { - var last = tree[tree.length - 1]; - if (last.constructor === vm.constructor) { - currentRecursiveSequence++; - vm = vm.$parent; - continue - } else if (currentRecursiveSequence > 0) { - tree[tree.length - 1] = [last, currentRecursiveSequence]; - currentRecursiveSequence = 0; - } - } - tree.push(vm); - vm = vm.$parent; - } - return '\n\nfound in\n\n' + tree - .map(function (vm, i) { return ("" + (i === 0 ? '---> ' : repeat(' ', 5 + i * 2)) + (Array.isArray(vm) - ? ((formatComponentName(vm[0])) + "... (" + (vm[1]) + " recursive calls)") - : formatComponentName(vm))); }) - .join('\n') - } else { - return ("\n\n(found in " + (formatComponentName(vm)) + ")") - } - }; -} - -/* */ - - -var uid = 0; - -/** - * A dep is an observable that can have multiple - * directives subscribing to it. - */ -var Dep = function Dep () { - this.id = uid++; - this.subs = []; -}; - -Dep.prototype.addSub = function addSub (sub) { - this.subs.push(sub); -}; - -Dep.prototype.removeSub = function removeSub (sub) { - remove(this.subs, sub); -}; - -Dep.prototype.depend = function depend () { - if (Dep.target) { - Dep.target.addDep(this); - } -}; - -Dep.prototype.notify = function notify () { - // stabilize the subscriber list first - var subs = this.subs.slice(); - for (var i = 0, l = subs.length; i < l; i++) { - subs[i].update(); - } -}; - -// the current target watcher being evaluated. -// this is globally unique because there could be only one -// watcher being evaluated at any time. -Dep.target = null; -var targetStack = []; - -function pushTarget (_target) { - if (Dep.target) { targetStack.push(Dep.target); } - Dep.target = _target; -} - -function popTarget () { - Dep.target = targetStack.pop(); -} - -/* */ - -var VNode = function VNode ( - tag, - data, - children, - text, - elm, - context, - componentOptions, - asyncFactory -) { - this.tag = tag; - this.data = data; - this.children = children; - this.text = text; - this.elm = elm; - this.ns = undefined; - this.context = context; - this.fnContext = undefined; - this.fnOptions = undefined; - this.fnScopeId = undefined; - this.key = data && data.key; - this.componentOptions = componentOptions; - this.componentInstance = undefined; - this.parent = undefined; - this.raw = false; - this.isStatic = false; - this.isRootInsert = true; - this.isComment = false; - this.isCloned = false; - this.isOnce = false; - this.asyncFactory = asyncFactory; - this.asyncMeta = undefined; - this.isAsyncPlaceholder = false; -}; - -var prototypeAccessors = { child: { configurable: true } }; - -// DEPRECATED: alias for componentInstance for backwards compat. -/* istanbul ignore next */ -prototypeAccessors.child.get = function () { - return this.componentInstance -}; - -Object.defineProperties( VNode.prototype, prototypeAccessors ); - -var createEmptyVNode = function (text) { - if ( text === void 0 ) text = ''; - - var node = new VNode(); - node.text = text; - node.isComment = true; - return node -}; - -function createTextVNode (val) { - return new VNode(undefined, undefined, undefined, String(val)) -} - -// optimized shallow clone -// used for static nodes and slot nodes because they may be reused across -// multiple renders, cloning them avoids errors when DOM manipulations rely -// on their elm reference. -function cloneVNode (vnode, deep) { - var componentOptions = vnode.componentOptions; - var cloned = new VNode( - vnode.tag, - vnode.data, - vnode.children, - vnode.text, - vnode.elm, - vnode.context, - componentOptions, - vnode.asyncFactory - ); - cloned.ns = vnode.ns; - cloned.isStatic = vnode.isStatic; - cloned.key = vnode.key; - cloned.isComment = vnode.isComment; - cloned.fnContext = vnode.fnContext; - cloned.fnOptions = vnode.fnOptions; - cloned.fnScopeId = vnode.fnScopeId; - cloned.isCloned = true; - if (deep) { - if (vnode.children) { - cloned.children = cloneVNodes(vnode.children, true); - } - if (componentOptions && componentOptions.children) { - componentOptions.children = cloneVNodes(componentOptions.children, true); - } - } - return cloned -} - -function cloneVNodes (vnodes, deep) { - var len = vnodes.length; - var res = new Array(len); - for (var i = 0; i < len; i++) { - res[i] = cloneVNode(vnodes[i], deep); - } - return res -} - -/* - * not type checking this file because flow doesn't play well with - * dynamically accessing methods on Array prototype - */ - -var arrayProto = Array.prototype; -var arrayMethods = Object.create(arrayProto);[ - 'push', - 'pop', - 'shift', - 'unshift', - 'splice', - 'sort', - 'reverse' -].forEach(function (method) { - // cache original method - var original = arrayProto[method]; - def(arrayMethods, method, function mutator () { - var args = [], len = arguments.length; - while ( len-- ) args[ len ] = arguments[ len ]; - - var result = original.apply(this, args); - var ob = this.__ob__; - var inserted; - switch (method) { - case 'push': - case 'unshift': - inserted = args; - break - case 'splice': - inserted = args.slice(2); - break - } - if (inserted) { ob.observeArray(inserted); } - // notify change - ob.dep.notify(); - return result - }); -}); - -/* */ - -var arrayKeys = Object.getOwnPropertyNames(arrayMethods); - -/** - * By default, when a reactive property is set, the new value is - * also converted to become reactive. However when passing down props, - * we don't want to force conversion because the value may be a nested value - * under a frozen data structure. Converting it would defeat the optimization. - */ -var observerState = { - shouldConvert: true -}; - -/** - * Observer class that are attached to each observed - * object. Once attached, the observer converts target - * object's property keys into getter/setters that - * collect dependencies and dispatches updates. - */ -var Observer = function Observer (value) { - this.value = value; - this.dep = new Dep(); - this.vmCount = 0; - def(value, '__ob__', this); - if (Array.isArray(value)) { - var augment = hasProto - ? protoAugment - : copyAugment; - augment(value, arrayMethods, arrayKeys); - this.observeArray(value); - } else { - this.walk(value); - } -}; - -/** - * Walk through each property and convert them into - * getter/setters. This method should only be called when - * value type is Object. - */ -Observer.prototype.walk = function walk (obj) { - var keys = Object.keys(obj); - for (var i = 0; i < keys.length; i++) { - defineReactive(obj, keys[i], obj[keys[i]]); - } -}; - -/** - * Observe a list of Array items. - */ -Observer.prototype.observeArray = function observeArray (items) { - for (var i = 0, l = items.length; i < l; i++) { - observe(items[i]); - } -}; - -// helpers - -/** - * Augment an target Object or Array by intercepting - * the prototype chain using __proto__ - */ -function protoAugment (target, src, keys) { - /* eslint-disable no-proto */ - target.__proto__ = src; - /* eslint-enable no-proto */ -} - -/** - * Augment an target Object or Array by defining - * hidden properties. - */ -/* istanbul ignore next */ -function copyAugment (target, src, keys) { - for (var i = 0, l = keys.length; i < l; i++) { - var key = keys[i]; - def(target, key, src[key]); - } -} - -/** - * Attempt to create an observer instance for a value, - * returns the new observer if successfully observed, - * or the existing observer if the value already has one. - */ -function observe (value, asRootData) { - if (!isObject(value) || value instanceof VNode) { - return - } - var ob; - if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) { - ob = value.__ob__; - } else if ( - observerState.shouldConvert && - !isServerRendering() && - (Array.isArray(value) || isPlainObject(value)) && - Object.isExtensible(value) && - !value._isVue - ) { - ob = new Observer(value); - } - if (asRootData && ob) { - ob.vmCount++; - } - return ob -} - -/** - * Define a reactive property on an Object. - */ -function defineReactive ( - obj, - key, - val, - customSetter, - shallow -) { - var dep = new Dep(); - - var property = Object.getOwnPropertyDescriptor(obj, key); - if (property && property.configurable === false) { - return - } - - // cater for pre-defined getter/setters - var getter = property && property.get; - var setter = property && property.set; - - var childOb = !shallow && observe(val); - Object.defineProperty(obj, key, { - enumerable: true, - configurable: true, - get: function reactiveGetter () { - var value = getter ? getter.call(obj) : val; - if (Dep.target) { - dep.depend(); - if (childOb) { - childOb.dep.depend(); - if (Array.isArray(value)) { - dependArray(value); - } - } - } - return value - }, - set: function reactiveSetter (newVal) { - var value = getter ? getter.call(obj) : val; - /* eslint-disable no-self-compare */ - if (newVal === value || (newVal !== newVal && value !== value)) { - return - } - /* eslint-enable no-self-compare */ - if ("development" !== 'production' && customSetter) { - customSetter(); - } - if (setter) { - setter.call(obj, newVal); - } else { - val = newVal; - } - childOb = !shallow && observe(newVal); - dep.notify(); - } - }); -} - -/** - * Set a property on an object. Adds the new property and - * triggers change notification if the property doesn't - * already exist. - */ -function set (target, key, val) { - if (Array.isArray(target) && isValidArrayIndex(key)) { - target.length = Math.max(target.length, key); - target.splice(key, 1, val); - return val - } - if (key in target && !(key in Object.prototype)) { - target[key] = val; - return val - } - var ob = (target).__ob__; - if (target._isVue || (ob && ob.vmCount)) { - "development" !== 'production' && warn( - 'Avoid adding reactive properties to a Vue instance or its root $data ' + - 'at runtime - declare it upfront in the data option.' - ); - return val - } - if (!ob) { - target[key] = val; - return val - } - defineReactive(ob.value, key, val); - ob.dep.notify(); - return val -} - -/** - * Delete a property and trigger change if necessary. - */ -function del (target, key) { - if (Array.isArray(target) && isValidArrayIndex(key)) { - target.splice(key, 1); - return - } - var ob = (target).__ob__; - if (target._isVue || (ob && ob.vmCount)) { - "development" !== 'production' && warn( - 'Avoid deleting properties on a Vue instance or its root $data ' + - '- just set it to null.' - ); - return - } - if (!hasOwn(target, key)) { - return - } - delete target[key]; - if (!ob) { - return - } - ob.dep.notify(); -} - -/** - * Collect dependencies on array elements when the array is touched, since - * we cannot intercept array element access like property getters. - */ -function dependArray (value) { - for (var e = (void 0), i = 0, l = value.length; i < l; i++) { - e = value[i]; - e && e.__ob__ && e.__ob__.dep.depend(); - if (Array.isArray(e)) { - dependArray(e); - } - } -} - -/* */ - -/** - * Option overwriting strategies are functions that handle - * how to merge a parent option value and a child option - * value into the final value. - */ -var strats = config.optionMergeStrategies; - -/** - * Options with restrictions - */ -{ - strats.el = strats.propsData = function (parent, child, vm, key) { - if (!vm) { - warn( - "option \"" + key + "\" can only be used during instance " + - 'creation with the `new` keyword.' - ); - } - return defaultStrat(parent, child) - }; -} - -/** - * Helper that recursively merges two data objects together. - */ -function mergeData (to, from) { - if (!from) { return to } - var key, toVal, fromVal; - var keys = Object.keys(from); - for (var i = 0; i < keys.length; i++) { - key = keys[i]; - toVal = to[key]; - fromVal = from[key]; - if (!hasOwn(to, key)) { - set(to, key, fromVal); - } else if (isPlainObject(toVal) && isPlainObject(fromVal)) { - mergeData(toVal, fromVal); - } - } - return to -} - -/** - * Data - */ -function mergeDataOrFn ( - parentVal, - childVal, - vm -) { - if (!vm) { - // in a Vue.extend merge, both should be functions - if (!childVal) { - return parentVal - } - if (!parentVal) { - return childVal - } - // when parentVal & childVal are both present, - // we need to return a function that returns the - // merged result of both functions... no need to - // check if parentVal is a function here because - // it has to be a function to pass previous merges. - return function mergedDataFn () { - return mergeData( - typeof childVal === 'function' ? childVal.call(this, this) : childVal, - typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal - ) - } - } else { - return function mergedInstanceDataFn () { - // instance merge - var instanceData = typeof childVal === 'function' - ? childVal.call(vm, vm) - : childVal; - var defaultData = typeof parentVal === 'function' - ? parentVal.call(vm, vm) - : parentVal; - if (instanceData) { - return mergeData(instanceData, defaultData) - } else { - return defaultData - } - } - } -} - -strats.data = function ( - parentVal, - childVal, - vm -) { - if (!vm) { - if (childVal && typeof childVal !== 'function') { - "development" !== 'production' && warn( - 'The "data" option should be a function ' + - 'that returns a per-instance value in component ' + - 'definitions.', - vm - ); - - return parentVal - } - return mergeDataOrFn(parentVal, childVal) - } - - return mergeDataOrFn(parentVal, childVal, vm) -}; - -/** - * Hooks and props are merged as arrays. - */ -function mergeHook ( - parentVal, - childVal -) { - return childVal - ? parentVal - ? parentVal.concat(childVal) - : Array.isArray(childVal) - ? childVal - : [childVal] - : parentVal -} - -LIFECYCLE_HOOKS.forEach(function (hook) { - strats[hook] = mergeHook; -}); - -/** - * Assets - * - * When a vm is present (instance creation), we need to do - * a three-way merge between constructor options, instance - * options and parent options. - */ -function mergeAssets ( - parentVal, - childVal, - vm, - key -) { - var res = Object.create(parentVal || null); - if (childVal) { - "development" !== 'production' && assertObjectType(key, childVal, vm); - return extend(res, childVal) - } else { - return res - } -} - -ASSET_TYPES.forEach(function (type) { - strats[type + 's'] = mergeAssets; -}); - -/** - * Watchers. - * - * Watchers hashes should not overwrite one - * another, so we merge them as arrays. - */ -strats.watch = function ( - parentVal, - childVal, - vm, - key -) { - // work around Firefox's Object.prototype.watch... - if (parentVal === nativeWatch) { parentVal = undefined; } - if (childVal === nativeWatch) { childVal = undefined; } - /* istanbul ignore if */ - if (!childVal) { return Object.create(parentVal || null) } - { - assertObjectType(key, childVal, vm); - } - if (!parentVal) { return childVal } - var ret = {}; - extend(ret, parentVal); - for (var key$1 in childVal) { - var parent = ret[key$1]; - var child = childVal[key$1]; - if (parent && !Array.isArray(parent)) { - parent = [parent]; - } - ret[key$1] = parent - ? parent.concat(child) - : Array.isArray(child) ? child : [child]; - } - return ret -}; - -/** - * Other object hashes. - */ -strats.props = -strats.methods = -strats.inject = -strats.computed = function ( - parentVal, - childVal, - vm, - key -) { - if (childVal && "development" !== 'production') { - assertObjectType(key, childVal, vm); - } - if (!parentVal) { return childVal } - var ret = Object.create(null); - extend(ret, parentVal); - if (childVal) { extend(ret, childVal); } - return ret -}; -strats.provide = mergeDataOrFn; - -/** - * Default strategy. - */ -var defaultStrat = function (parentVal, childVal) { - return childVal === undefined - ? parentVal - : childVal -}; - -/** - * Validate component names - */ -function checkComponents (options) { - for (var key in options.components) { - validateComponentName(key); - } -} - -function validateComponentName (name) { - if (!/^[a-zA-Z][\w-]*$/.test(name)) { - warn( - 'Invalid component name: "' + name + '". Component names ' + - 'can only contain alphanumeric characters and the hyphen, ' + - 'and must start with a letter.' - ); - } - if (isBuiltInTag(name) || config.isReservedTag(name)) { - warn( - 'Do not use built-in or reserved HTML elements as component ' + - 'id: ' + name - ); - } -} - -/** - * Ensure all props option syntax are normalized into the - * Object-based format. - */ -function normalizeProps (options, vm) { - var props = options.props; - if (!props) { return } - var res = {}; - var i, val, name; - if (Array.isArray(props)) { - i = props.length; - while (i--) { - val = props[i]; - if (typeof val === 'string') { - name = camelize(val); - res[name] = { type: null }; - } else { - warn('props must be strings when using array syntax.'); - } - } - } else if (isPlainObject(props)) { - for (var key in props) { - val = props[key]; - name = camelize(key); - res[name] = isPlainObject(val) - ? val - : { type: val }; - } - } else { - warn( - "Invalid value for option \"props\": expected an Array or an Object, " + - "but got " + (toRawType(props)) + ".", - vm - ); - } - options.props = res; -} - -/** - * Normalize all injections into Object-based format - */ -function normalizeInject (options, vm) { - var inject = options.inject; - if (!inject) { return } - var normalized = options.inject = {}; - if (Array.isArray(inject)) { - for (var i = 0; i < inject.length; i++) { - normalized[inject[i]] = { from: inject[i] }; - } - } else if (isPlainObject(inject)) { - for (var key in inject) { - var val = inject[key]; - normalized[key] = isPlainObject(val) - ? extend({ from: key }, val) - : { from: val }; - } - } else { - warn( - "Invalid value for option \"inject\": expected an Array or an Object, " + - "but got " + (toRawType(inject)) + ".", - vm - ); - } -} - -/** - * Normalize raw function directives into object format. - */ -function normalizeDirectives (options) { - var dirs = options.directives; - if (dirs) { - for (var key in dirs) { - var def = dirs[key]; - if (typeof def === 'function') { - dirs[key] = { bind: def, update: def }; - } - } - } -} - -function assertObjectType (name, value, vm) { - if (!isPlainObject(value)) { - warn( - "Invalid value for option \"" + name + "\": expected an Object, " + - "but got " + (toRawType(value)) + ".", - vm - ); - } -} - -/** - * Merge two option objects into a new one. - * Core utility used in both instantiation and inheritance. - */ -function mergeOptions ( - parent, - child, - vm -) { - { - checkComponents(child); - } - - if (typeof child === 'function') { - child = child.options; - } - - normalizeProps(child, vm); - normalizeInject(child, vm); - normalizeDirectives(child); - var extendsFrom = child.extends; - if (extendsFrom) { - parent = mergeOptions(parent, extendsFrom, vm); - } - if (child.mixins) { - for (var i = 0, l = child.mixins.length; i < l; i++) { - parent = mergeOptions(parent, child.mixins[i], vm); - } - } - var options = {}; - var key; - for (key in parent) { - mergeField(key); - } - for (key in child) { - if (!hasOwn(parent, key)) { - mergeField(key); - } - } - function mergeField (key) { - var strat = strats[key] || defaultStrat; - options[key] = strat(parent[key], child[key], vm, key); - } - return options -} - -/** - * Resolve an asset. - * This function is used because child instances need access - * to assets defined in its ancestor chain. - */ -function resolveAsset ( - options, - type, - id, - warnMissing -) { - /* istanbul ignore if */ - if (typeof id !== 'string') { - return - } - var assets = options[type]; - // check local registration variations first - if (hasOwn(assets, id)) { return assets[id] } - var camelizedId = camelize(id); - if (hasOwn(assets, camelizedId)) { return assets[camelizedId] } - var PascalCaseId = capitalize(camelizedId); - if (hasOwn(assets, PascalCaseId)) { return assets[PascalCaseId] } - // fallback to prototype chain - var res = assets[id] || assets[camelizedId] || assets[PascalCaseId]; - if ("development" !== 'production' && warnMissing && !res) { - warn( - 'Failed to resolve ' + type.slice(0, -1) + ': ' + id, - options - ); - } - return res -} - -/* */ - -function validateProp ( - key, - propOptions, - propsData, - vm -) { - var prop = propOptions[key]; - var absent = !hasOwn(propsData, key); - var value = propsData[key]; - // handle boolean props - if (isType(Boolean, prop.type)) { - if (absent && !hasOwn(prop, 'default')) { - value = false; - } else if (!isType(String, prop.type) && (value === '' || value === hyphenate(key))) { - value = true; - } - } - // check default value - if (value === undefined) { - value = getPropDefaultValue(vm, prop, key); - // since the default value is a fresh copy, - // make sure to observe it. - var prevShouldConvert = observerState.shouldConvert; - observerState.shouldConvert = true; - observe(value); - observerState.shouldConvert = prevShouldConvert; - } - { - assertProp(prop, key, value, vm, absent); - } - return value -} - -/** - * Get the default value of a prop. - */ -function getPropDefaultValue (vm, prop, key) { - // no default, return undefined - if (!hasOwn(prop, 'default')) { - return undefined - } - var def = prop.default; - // warn against non-factory defaults for Object & Array - if ("development" !== 'production' && isObject(def)) { - warn( - 'Invalid default value for prop "' + key + '": ' + - 'Props with type Object/Array must use a factory function ' + - 'to return the default value.', - vm - ); - } - // the raw prop value was also undefined from previous render, - // return previous default value to avoid unnecessary watcher trigger - if (vm && vm.$options.propsData && - vm.$options.propsData[key] === undefined && - vm._props[key] !== undefined - ) { - return vm._props[key] - } - // call factory function for non-Function types - // a value is Function if its prototype is function even across different execution context - return typeof def === 'function' && getType(prop.type) !== 'Function' - ? def.call(vm) - : def -} - -/** - * Assert whether a prop is valid. - */ -function assertProp ( - prop, - name, - value, - vm, - absent -) { - if (prop.required && absent) { - warn( - 'Missing required prop: "' + name + '"', - vm - ); - return - } - if (value == null && !prop.required) { - return - } - var type = prop.type; - var valid = !type || type === true; - var expectedTypes = []; - if (type) { - if (!Array.isArray(type)) { - type = [type]; - } - for (var i = 0; i < type.length && !valid; i++) { - var assertedType = assertType(value, type[i]); - expectedTypes.push(assertedType.expectedType || ''); - valid = assertedType.valid; - } - } - if (!valid) { - warn( - "Invalid prop: type check failed for prop \"" + name + "\"." + - " Expected " + (expectedTypes.map(capitalize).join(', ')) + - ", got " + (toRawType(value)) + ".", - vm - ); - return - } - var validator = prop.validator; - if (validator) { - if (!validator(value)) { - warn( - 'Invalid prop: custom validator check failed for prop "' + name + '".', - vm - ); - } - } -} - -var simpleCheckRE = /^(String|Number|Boolean|Function|Symbol)$/; - -function assertType (value, type) { - var valid; - var expectedType = getType(type); - if (simpleCheckRE.test(expectedType)) { - var t = typeof value; - valid = t === expectedType.toLowerCase(); - // for primitive wrapper objects - if (!valid && t === 'object') { - valid = value instanceof type; - } - } else if (expectedType === 'Object') { - valid = isPlainObject(value); - } else if (expectedType === 'Array') { - valid = Array.isArray(value); - } else { - valid = value instanceof type; - } - return { - valid: valid, - expectedType: expectedType - } -} - -/** - * Use function string name to check built-in types, - * because a simple equality check will fail when running - * across different vms / iframes. - */ -function getType (fn) { - var match = fn && fn.toString().match(/^\s*function (\w+)/); - return match ? match[1] : '' -} - -function isType (type, fn) { - if (!Array.isArray(fn)) { - return getType(fn) === getType(type) - } - for (var i = 0, len = fn.length; i < len; i++) { - if (getType(fn[i]) === getType(type)) { - return true - } - } - /* istanbul ignore next */ - return false -} - -/* */ - -function handleError (err, vm, info) { - if (vm) { - var cur = vm; - while ((cur = cur.$parent)) { - var hooks = cur.$options.errorCaptured; - if (hooks) { - for (var i = 0; i < hooks.length; i++) { - try { - var capture = hooks[i].call(cur, err, vm, info) === false; - if (capture) { return } - } catch (e) { - globalHandleError(e, cur, 'errorCaptured hook'); - } - } - } - } - } - globalHandleError(err, vm, info); -} - -function globalHandleError (err, vm, info) { - if (config.errorHandler) { - try { - return config.errorHandler.call(null, err, vm, info) - } catch (e) { - logError(e, null, 'config.errorHandler'); - } - } - logError(err, vm, info); -} - -function logError (err, vm, info) { - { - warn(("Error in " + info + ": \"" + (err.toString()) + "\""), vm); - } - /* istanbul ignore else */ - if ((inBrowser || inWeex) && typeof console !== 'undefined') { - console.error(err); - } else { - throw err - } -} - -/* */ -/* globals MessageChannel */ - -var callbacks = []; -var pending = false; - -function flushCallbacks () { - pending = false; - var copies = callbacks.slice(0); - callbacks.length = 0; - for (var i = 0; i < copies.length; i++) { - copies[i](); - } -} - -// Here we have async deferring wrappers using both micro and macro tasks. -// In < 2.4 we used micro tasks everywhere, but there are some scenarios where -// micro tasks have too high a priority and fires in between supposedly -// sequential events (e.g. #4521, #6690) or even between bubbling of the same -// event (#6566). However, using macro tasks everywhere also has subtle problems -// when state is changed right before repaint (e.g. #6813, out-in transitions). -// Here we use micro task by default, but expose a way to force macro task when -// needed (e.g. in event handlers attached by v-on). -var microTimerFunc; -var macroTimerFunc; -var useMacroTask = false; - -// Determine (macro) Task defer implementation. -// Technically setImmediate should be the ideal choice, but it's only available -// in IE. The only polyfill that consistently queues the callback after all DOM -// events triggered in the same loop is by using MessageChannel. -/* istanbul ignore if */ -if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) { - macroTimerFunc = function () { - setImmediate(flushCallbacks); - }; -} else if (typeof MessageChannel !== 'undefined' && ( - isNative(MessageChannel) || - // PhantomJS - MessageChannel.toString() === '[object MessageChannelConstructor]' -)) { - var channel = new MessageChannel(); - var port = channel.port2; - channel.port1.onmessage = flushCallbacks; - macroTimerFunc = function () { - port.postMessage(1); - }; -} else { - /* istanbul ignore next */ - macroTimerFunc = function () { - setTimeout(flushCallbacks, 0); - }; -} - -// Determine MicroTask defer implementation. -/* istanbul ignore next, $flow-disable-line */ -if (typeof Promise !== 'undefined' && isNative(Promise)) { - var p = Promise.resolve(); - microTimerFunc = function () { - p.then(flushCallbacks); - // in problematic UIWebViews, Promise.then doesn't completely break, but - // it can get stuck in a weird state where callbacks are pushed into the - // microtask queue but the queue isn't being flushed, until the browser - // needs to do some other work, e.g. handle a timer. Therefore we can - // "force" the microtask queue to be flushed by adding an empty timer. - if (isIOS) { setTimeout(noop); } - }; -} else { - // fallback to macro - microTimerFunc = macroTimerFunc; -} - -/** - * Wrap a function so that if any code inside triggers state change, - * the changes are queued using a Task instead of a MicroTask. - */ -function withMacroTask (fn) { - return fn._withTask || (fn._withTask = function () { - useMacroTask = true; - var res = fn.apply(null, arguments); - useMacroTask = false; - return res - }) -} - -function nextTick (cb, ctx) { - var _resolve; - callbacks.push(function () { - if (cb) { - try { - cb.call(ctx); - } catch (e) { - handleError(e, ctx, 'nextTick'); - } - } else if (_resolve) { - _resolve(ctx); - } - }); - if (!pending) { - pending = true; - if (useMacroTask) { - macroTimerFunc(); - } else { - microTimerFunc(); - } - } - // $flow-disable-line - if (!cb && typeof Promise !== 'undefined') { - return new Promise(function (resolve) { - _resolve = resolve; - }) - } -} - -/* */ - -var mark; -var measure; - -{ - var perf = inBrowser && window.performance; - /* istanbul ignore if */ - if ( - perf && - perf.mark && - perf.measure && - perf.clearMarks && - perf.clearMeasures - ) { - mark = function (tag) { return perf.mark(tag); }; - measure = function (name, startTag, endTag) { - perf.measure(name, startTag, endTag); - perf.clearMarks(startTag); - perf.clearMarks(endTag); - perf.clearMeasures(name); - }; - } -} - -/* not type checking this file because flow doesn't play well with Proxy */ - -var initProxy; - -{ - var allowedGlobals = makeMap( - 'Infinity,undefined,NaN,isFinite,isNaN,' + - 'parseFloat,parseInt,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent,' + - 'Math,Number,Date,Array,Object,Boolean,String,RegExp,Map,Set,JSON,Intl,' + - 'require' // for Webpack/Browserify - ); - - var warnNonPresent = function (target, key) { - warn( - "Property or method \"" + key + "\" is not defined on the instance but " + - 'referenced during render. Make sure that this property is reactive, ' + - 'either in the data option, or for class-based components, by ' + - 'initializing the property. ' + - 'See: https://vuejs.org/v2/guide/reactivity.html#Declaring-Reactive-Properties.', - target - ); - }; - - var hasProxy = - typeof Proxy !== 'undefined' && - Proxy.toString().match(/native code/); - - if (hasProxy) { - var isBuiltInModifier = makeMap('stop,prevent,self,ctrl,shift,alt,meta,exact'); - config.keyCodes = new Proxy(config.keyCodes, { - set: function set (target, key, value) { - if (isBuiltInModifier(key)) { - warn(("Avoid overwriting built-in modifier in config.keyCodes: ." + key)); - return false - } else { - target[key] = value; - return true - } - } - }); - } - - var hasHandler = { - has: function has (target, key) { - var has = key in target; - var isAllowed = allowedGlobals(key) || key.charAt(0) === '_'; - if (!has && !isAllowed) { - warnNonPresent(target, key); - } - return has || !isAllowed - } - }; - - var getHandler = { - get: function get (target, key) { - if (typeof key === 'string' && !(key in target)) { - warnNonPresent(target, key); - } - return target[key] - } - }; - - initProxy = function initProxy (vm) { - if (hasProxy) { - // determine which proxy handler to use - var options = vm.$options; - var handlers = options.render && options.render._withStripped - ? getHandler - : hasHandler; - vm._renderProxy = new Proxy(vm, handlers); - } else { - vm._renderProxy = vm; - } - }; -} - -/* */ - -var seenObjects = new _Set(); - -/** - * Recursively traverse an object to evoke all converted - * getters, so that every nested property inside the object - * is collected as a "deep" dependency. - */ -function traverse (val) { - _traverse(val, seenObjects); - seenObjects.clear(); -} - -function _traverse (val, seen) { - var i, keys; - var isA = Array.isArray(val); - if ((!isA && !isObject(val)) || Object.isFrozen(val)) { - return - } - if (val.__ob__) { - var depId = val.__ob__.dep.id; - if (seen.has(depId)) { - return - } - seen.add(depId); - } - if (isA) { - i = val.length; - while (i--) { _traverse(val[i], seen); } - } else { - keys = Object.keys(val); - i = keys.length; - while (i--) { _traverse(val[keys[i]], seen); } - } -} - -/* */ - -var normalizeEvent = cached(function (name) { - var passive = name.charAt(0) === '&'; - name = passive ? name.slice(1) : name; - var once$$1 = name.charAt(0) === '~'; // Prefixed last, checked first - name = once$$1 ? name.slice(1) : name; - var capture = name.charAt(0) === '!'; - name = capture ? name.slice(1) : name; - return { - name: name, - once: once$$1, - capture: capture, - passive: passive - } -}); - -function createFnInvoker (fns) { - function invoker () { - var arguments$1 = arguments; - - var fns = invoker.fns; - if (Array.isArray(fns)) { - var cloned = fns.slice(); - for (var i = 0; i < cloned.length; i++) { - cloned[i].apply(null, arguments$1); - } - } else { - // return handler return value for single handlers - return fns.apply(null, arguments) - } - } - invoker.fns = fns; - return invoker -} - -function updateListeners ( - on, - oldOn, - add, - remove$$1, - vm -) { - var name, def, cur, old, event; - for (name in on) { - def = cur = on[name]; - old = oldOn[name]; - event = normalizeEvent(name); - /* istanbul ignore if */ - if (isUndef(cur)) { - "development" !== 'production' && warn( - "Invalid handler for event \"" + (event.name) + "\": got " + String(cur), - vm - ); - } else if (isUndef(old)) { - if (isUndef(cur.fns)) { - cur = on[name] = createFnInvoker(cur); - } - add(event.name, cur, event.once, event.capture, event.passive, event.params); - } else if (cur !== old) { - old.fns = cur; - on[name] = old; - } - } - for (name in oldOn) { - if (isUndef(on[name])) { - event = normalizeEvent(name); - remove$$1(event.name, oldOn[name], event.capture); - } - } -} - -/* */ - -function mergeVNodeHook (def, hookKey, hook) { - if (def instanceof VNode) { - def = def.data.hook || (def.data.hook = {}); - } - var invoker; - var oldHook = def[hookKey]; - - function wrappedHook () { - hook.apply(this, arguments); - // important: remove merged hook to ensure it's called only once - // and prevent memory leak - remove(invoker.fns, wrappedHook); - } - - if (isUndef(oldHook)) { - // no existing hook - invoker = createFnInvoker([wrappedHook]); - } else { - /* istanbul ignore if */ - if (isDef(oldHook.fns) && isTrue(oldHook.merged)) { - // already a merged invoker - invoker = oldHook; - invoker.fns.push(wrappedHook); - } else { - // existing plain hook - invoker = createFnInvoker([oldHook, wrappedHook]); - } - } - - invoker.merged = true; - def[hookKey] = invoker; -} - -/* */ - -function extractPropsFromVNodeData ( - data, - Ctor, - tag -) { - // we are only extracting raw values here. - // validation and default values are handled in the child - // component itself. - var propOptions = Ctor.options.props; - if (isUndef(propOptions)) { - return - } - var res = {}; - var attrs = data.attrs; - var props = data.props; - if (isDef(attrs) || isDef(props)) { - for (var key in propOptions) { - var altKey = hyphenate(key); - { - var keyInLowerCase = key.toLowerCase(); - if ( - key !== keyInLowerCase && - attrs && hasOwn(attrs, keyInLowerCase) - ) { - tip( - "Prop \"" + keyInLowerCase + "\" is passed to component " + - (formatComponentName(tag || Ctor)) + ", but the declared prop name is" + - " \"" + key + "\". " + - "Note that HTML attributes are case-insensitive and camelCased " + - "props need to use their kebab-case equivalents when using in-DOM " + - "templates. You should probably use \"" + altKey + "\" instead of \"" + key + "\"." - ); - } - } - checkProp(res, props, key, altKey, true) || - checkProp(res, attrs, key, altKey, false); - } - } - return res -} - -function checkProp ( - res, - hash, - key, - altKey, - preserve -) { - if (isDef(hash)) { - if (hasOwn(hash, key)) { - res[key] = hash[key]; - if (!preserve) { - delete hash[key]; - } - return true - } else if (hasOwn(hash, altKey)) { - res[key] = hash[altKey]; - if (!preserve) { - delete hash[altKey]; - } - return true - } - } - return false -} - -/* */ - -// The template compiler attempts to minimize the need for normalization by -// statically analyzing the template at compile time. -// -// For plain HTML markup, normalization can be completely skipped because the -// generated render function is guaranteed to return Array. There are -// two cases where extra normalization is needed: - -// 1. When the children contains components - because a functional component -// may return an Array instead of a single root. In this case, just a simple -// normalization is needed - if any child is an Array, we flatten the whole -// thing with Array.prototype.concat. It is guaranteed to be only 1-level deep -// because functional components already normalize their own children. -function simpleNormalizeChildren (children) { - for (var i = 0; i < children.length; i++) { - if (Array.isArray(children[i])) { - return Array.prototype.concat.apply([], children) - } - } - return children -} - -// 2. When the children contains constructs that always generated nested Arrays, -// e.g.