diff --git a/aircox/migrations/0015_program_editors.py b/aircox/migrations/0015_program_editors.py index 2c921b0..23737cc 100644 --- a/aircox/migrations/0015_program_editors.py +++ b/aircox/migrations/0015_program_editors.py @@ -4,6 +4,14 @@ from django.db import migrations, models import django.db.models.deletion +def init_groups_and_permissions(app, schema_editor): + from aircox.permissions import program_permissions + + Program = app.get_model("aircox", "Program") + for program in Program.objects.all(): + program_permissions.init(program) + + class Migration(migrations.Migration): dependencies = [ ("auth", "0012_alter_user_first_name_max_length"), @@ -22,4 +30,5 @@ class Migration(migrations.Migration): verbose_name="editors", ), ), + migrations.RunPython(init_groups_and_permissions), ] diff --git a/aircox/models/page.py b/aircox/models/page.py index 84b1ade..9511c97 100644 --- a/aircox/models/page.py +++ b/aircox/models/page.py @@ -336,7 +336,7 @@ class Comment(Renderable, models.Model): return Page.objects.select_subclasses().filter(id=self.page_id).first() def get_absolute_url(self): - return self.parent.get_absolute_url() + f"#comment-{self.pk}" + return self.parent.get_absolute_url() + f"#{self._meta.label_lower}-{self.pk}" class Meta: verbose_name = _("Comment") diff --git a/aircox/models/program.py b/aircox/models/program.py index f9b8ec4..c204971 100644 --- a/aircox/models/program.py +++ b/aircox/models/program.py @@ -1,8 +1,7 @@ import os from django.conf import settings as conf -from django.contrib.auth.models import Group, Permission -from django.contrib.contenttypes.models import ContentType +from django.contrib.auth.models import Group from django.db import models from django.utils.translation import gettext_lazy as _ @@ -117,37 +116,6 @@ class Program(Page): os.makedirs(path, exist_ok=True) return os.path.exists(path) - def can_update(self, user): - """Return True if user can update program.""" - if user.is_superuser: - return True - perm = self._perm_update_codename.format(self=self) - return user.has_perm("aircox." + perm) - - # permissions and editor group format. Use of pk in codename makes it - # consistent in case program title changes. - _editor_group_name = "{self.title}: editors" - _perm_update_codename = "program_{self.pk}_update" - _perm_update_name = "{self.title}: update" - - def init_editor_group(self): - if not self.editors_group: - name = self._editor_group_name.format(self=self) - self.editors_group, created = Group.objects.get_or_create(name=name) - else: - created = False - - if created: - if not self.pk: - self.save(check_groups=False) - permission, _ = Permission.objects.get_or_create( - codename=self._perm_update_codename.format(self=self), - content_type=ContentType.objects.get_for_model(self), - defaults={"name": self._perm_update_name.format(self=self)}, - ) - if permission not in self.editors_group.permissions.all(): - self.editors_group.permissions.add(permission) - class Meta: verbose_name = _("Program") verbose_name_plural = _("Programs") diff --git a/aircox/permissions.py b/aircox/permissions.py new file mode 100644 index 0000000..40d6df1 --- /dev/null +++ b/aircox/permissions.py @@ -0,0 +1,82 @@ +# Provide permissions handling +# we don't import models at module level in order to avoid migration problems +from django.utils.translation import gettext_lazy as _ +from django.contrib.auth.models import Group, Permission +from django.contrib.contenttypes.models import ContentType + +from .models import Program + + +__all__ = ("PagePermissions", "program_permissions") + + +class PagePermissions: + """Handles obj permissions initialization of page subclass.""" + + model = None + groups = ({"label": _("editors"), "field": "editors_group_id", "perms": ["update"]},) + """Groups informations initialized.""" + groups_name_format = "{obj.title}: {group_label}" + """Format used for groups name.""" + perms_name_format = "{obj.title}: can {perm}" + """Format used for permission name (displayed to humans).""" + perms_codename_format = "{obj._meta.label_lower}_{obj.pk}_{perm}" + """Format used for permissions codename.""" + + def __init__(self, model): + self.model = model + + def can(self, user, perm, obj): + """Return True wether if user can edit Program or its children.""" + from .models.page import ChildPage + + breakpoint() + if isinstance(obj, ChildPage): + obj = obj.parent_subclass + + if not isinstance(obj, self.model): + return False + + if user.is_superuser: + return True + + perm = self.perms_codename_format.format(self=self, perm=perm) + return user.has_perm(perm) + + # TODO: bulk init + def init(self, obj): + """Initialize permissions for the provided obj.""" + created_groups = [] + + # init groups + for infos in self.groups: + group = getattr(obj, infos["field"]) + if not group: + group, created = self.init_group(obj, infos) + setattr(obj, infos["field"], group.pk) + created and created_groups.append((group, infos)) + + if created_groups: + obj.save() + + # init perms + for group, infos in created_groups: + self.init_perms(obj, group, infos) + + def init_group(self, obj, infos): + name = self.groups_name_format.format(obj=obj, group_label=infos["label"]) + return Group.objects.get_or_create(name=name) + + def init_perms(self, obj, group, infos): + # TODO: avoid multiple database hits + for name in infos["perms"]: + perm, _ = Permission.objects.get_or_create( + codename=self.perms_codename_format.format(obj=obj, perm=name), + content_type=ContentType.objects.get_for_model(obj), + defaults={"name": self.perms_name_format.format(obj=obj, perm=name)}, + ) + if perm not in group.permissions.all(): + group.permissions.add(perm) + + +program_permissions = PagePermissions(Program) diff --git a/aircox/serializers/__init__.py b/aircox/serializers/__init__.py index 4518172..1558f66 100644 --- a/aircox/serializers/__init__.py +++ b/aircox/serializers/__init__.py @@ -1,9 +1,11 @@ from .admin import TrackSerializer, UserSettingsSerializer -from .log import LogInfo, LogInfoSerializer -from .sound import SoundSerializer from .episode import EpisodeSoundSerializer, EpisodeSerializer +from .log import LogInfo, LogInfoSerializer +from .page import CommentSerializer +from .sound import SoundSerializer __all__ = ( + "CommentSerializer", "LogInfo", "LogInfoSerializer", "EpisodeSoundSerializer", diff --git a/aircox/serializers/page.py b/aircox/serializers/page.py new file mode 100644 index 0000000..3f52fff --- /dev/null +++ b/aircox/serializers/page.py @@ -0,0 +1,12 @@ +from rest_framework import serializers + +from aircox import models + + +__all__ = ("CommentSerializer",) + + +class CommentSerializer(serializers.ModelSerializer): + class Meta: + model = models.Comment + fields = ["page", "nickname", "email", "date", "content"] diff --git a/aircox/static/aircox/css/chunk-common.css b/aircox/static/aircox/css/chunk-common.css index b4f3540..d5a7156 100644 --- a/aircox/static/aircox/css/chunk-common.css +++ b/aircox/static/aircox/css/chunk-common.css @@ -9834,10 +9834,34 @@ a.tag:hover { color: var(--secondary-color); } +.bg-main { + background-color: var(--main-color); +} + +.bg-main-light { + background-color: var(--main-color-light); +} + +.bg-secondary { + background-color: var(--secondary-color); +} + +.bg-secondary-light { + background-color: var(--secondary-color-light); +} + .bg-transparent { background-color: transparent; } +.border-bottom-main { + border-bottom: 1px solid var(--main-color); +} + +.border-bottom-secondary { + border-bottom: 1px solid var(--secondary-color); +} + .is-success { background-color: #0e0 !important; border-color: #0b0 !important; diff --git a/aircox/static/aircox/js/chunk-common.js b/aircox/static/aircox/js/chunk-common.js index 091a1fe..6060c77 100644 --- a/aircox/static/aircox/js/chunk-common.js +++ b/aircox/static/aircox/js/chunk-common.js @@ -15,7 +15,7 @@ \***********************************************************************************************************************************************************************************************/ /***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { -eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _model__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../model */ \"./src/model.js\");\n\n\n/**\n * Button that can be used to call API requests on provided url\n */\n/* harmony default export */ __webpack_exports__[\"default\"] = ({\n emit: ['start', 'done'],\n props: {\n //! Component tag, by default, `button`\n tag: {\n type: String,\n default: 'a'\n },\n //! Button icon\n icon: String,\n //! Data or model instance to send\n data: Object,\n //! Action method, by default, `POST`\n method: {\n type: String,\n default: 'POST'\n },\n //! If provided open confirmation box before proceeding\n confirm: {\n type: String,\n default: ''\n },\n //! Action url\n url: String,\n //! Extra request options\n fetchOptions: {\n type: Object,\n default: () => {\n return {};\n }\n },\n //! Component class while action is running\n runClass: String,\n //! Icon class while action is running\n runIcon: String\n },\n computed: {\n //! Input data as model instance\n item() {\n return this.data instanceof _model__WEBPACK_IMPORTED_MODULE_0__[\"default\"] ? this.data : new _model__WEBPACK_IMPORTED_MODULE_0__[\"default\"](this.data);\n },\n //! Computed button class\n buttonClass() {\n return this.promise ? this.runClass : '';\n }\n },\n data() {\n return {\n promise: false\n };\n },\n methods: {\n call() {\n if (this.promise || !this.url) return;\n if (this.confirm && !confirm(this.confirm)) return;\n const options = _model__WEBPACK_IMPORTED_MODULE_0__[\"default\"].getOptions({\n ...this.fetchOptions,\n method: this.method,\n body: JSON.stringify(this.item.data)\n });\n this.promise = fetch(this.url, options).then(data => {\n const response = data.json();\n this.promise = null;\n this.$emit('done', response);\n return response;\n }, data => {\n this.promise = null;\n return data;\n });\n return this.promise;\n }\n }\n});\n\n//# sourceURL=webpack://aircox-assets/./src/components/AActionButton.vue?./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use%5B0%5D!./node_modules/vue-loader/dist/index.js??ruleSet%5B0%5D.use%5B0%5D"); +eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _model__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../model */ \"./src/model.js\");\n\n\n/**\n * Button that can be used to call API requests on provided url\n */\n/* harmony default export */ __webpack_exports__[\"default\"] = ({\n emit: ['start', 'done'],\n props: {\n //! Component tag, by default, `button`\n tag: {\n type: String,\n default: 'a'\n },\n //! Button icon\n icon: String,\n //! Data or model instance to send\n data: Object,\n //! Action method, by default, `POST`\n method: {\n type: String,\n default: 'POST'\n },\n //! If provided open confirmation box before proceeding\n confirm: {\n type: String,\n default: ''\n },\n //! Action url\n url: String,\n //! Extra request options\n fetchOptions: {\n type: Object,\n default: () => {\n return {};\n }\n },\n //! Component class while action is running\n runClass: String,\n //! Icon class while action is running\n runIcon: String\n },\n computed: {\n //! Input data as model instance\n item() {\n return this.data instanceof _model__WEBPACK_IMPORTED_MODULE_0__[\"default\"] ? this.data : new _model__WEBPACK_IMPORTED_MODULE_0__[\"default\"](this.data);\n },\n //! Computed button class\n buttonClass() {\n return this.promise ? this.runClass : '';\n }\n },\n data() {\n return {\n promise: false\n };\n },\n methods: {\n call() {\n if (this.promise || !this.url) return;\n if (this.confirm && !confirm(this.confirm)) return;\n const options = _model__WEBPACK_IMPORTED_MODULE_0__[\"default\"].getOptions({\n ...this.fetchOptions,\n method: this.method,\n body: JSON.stringify(this.item.data)\n });\n this.promise = fetch(this.url, options).then(data => data.text()).then(data => {\n data = data && JSON.parse(data) || null;\n this.promise = null;\n this.$emit('done', data);\n return data;\n }, data => {\n this.promise = null;\n return data;\n });\n return this.promise;\n }\n }\n});\n\n//# sourceURL=webpack://aircox-assets/./src/components/AActionButton.vue?./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use%5B0%5D!./node_modules/vue-loader/dist/index.js??ruleSet%5B0%5D.use%5B0%5D"); /***/ }), @@ -195,7 +195,7 @@ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var core \*********************************************************************************************************************************************************************************************/ /***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { -eval("__webpack_require__.r(__webpack_exports__);\nconst splitReg = new RegExp(',\\\\s*', 'g');\n/* harmony default export */ __webpack_exports__[\"default\"] = ({\n data() {\n return {\n counts: {}\n };\n },\n methods: {\n update() {\n const items = this.$el.querySelectorAll('input[name=\"data\"]:checked');\n const counts = {};\n for (var item of items) if (item.value) for (var tag of item.value.split(splitReg)) counts[tag.trim()] = (counts[tag.trim()] || 0) + 1;\n this.counts = counts;\n },\n onclick() {\n // TODO: row click => check checkbox\n }\n },\n mounted() {\n console.log(this.counts);\n this.$refs.form.addEventListener('change', () => this.update());\n this.update();\n }\n});\n\n//# sourceURL=webpack://aircox-assets/./src/components/AStatistics.vue?./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use%5B0%5D!./node_modules/vue-loader/dist/index.js??ruleSet%5B0%5D.use%5B0%5D"); +eval("__webpack_require__.r(__webpack_exports__);\nconst splitReg = new RegExp(',\\\\s*|\\\\s+', 'g');\n/* harmony default export */ __webpack_exports__[\"default\"] = ({\n data() {\n return {\n counts: {}\n };\n },\n methods: {\n update() {\n const items = this.$el.querySelectorAll('input[name=\"data\"]:checked');\n const counts = {};\n for (var item of items) if (item.value) for (var tag of item.value.split(splitReg)) if (tag.trim()) counts[tag.trim()] = (counts[tag.trim()] || 0) + 1;\n this.counts = counts;\n },\n onclick() {\n // TODO: row click => check checkbox\n }\n },\n mounted() {\n console.log(this.counts);\n this.$refs.form.addEventListener('change', () => this.update());\n this.update();\n }\n});\n\n//# sourceURL=webpack://aircox-assets/./src/components/AStatistics.vue?./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use%5B0%5D!./node_modules/vue-loader/dist/index.js??ruleSet%5B0%5D.use%5B0%5D"); /***/ }), @@ -455,7 +455,7 @@ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpac \********************/ /***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { -eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ PlayerApp: function() { return /* binding */ PlayerApp; }\n/* harmony export */ });\n/* harmony import */ var v_calendar__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! v-calendar */ \"./node_modules/v-calendar/dist/es/index.js\");\n/* harmony import */ var _components__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./components */ \"./src/components/index.js\");\n\n\nconst App = {\n el: '#app',\n delimiters: ['[[', ']]'],\n components: {\n ..._components__WEBPACK_IMPORTED_MODULE_1__[\"default\"],\n ...{\n VCalendar: v_calendar__WEBPACK_IMPORTED_MODULE_0__.Calendar,\n VDatepicker: v_calendar__WEBPACK_IMPORTED_MODULE_0__.DatePicker\n }\n },\n computed: {\n player() {\n return window.aircox.player;\n }\n }\n};\nconst PlayerApp = {\n el: '#player',\n delimiters: ['[[', ']]'],\n components: {\n ..._components__WEBPACK_IMPORTED_MODULE_1__[\"default\"]\n }\n};\n/* harmony default export */ __webpack_exports__[\"default\"] = (App);\n\n//# sourceURL=webpack://aircox-assets/./src/app.js?"); +eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ PlayerApp: function() { return /* binding */ PlayerApp; }\n/* harmony export */ });\n/* harmony import */ var v_calendar__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! v-calendar */ \"./node_modules/v-calendar/dist/es/index.js\");\n/* harmony import */ var _components__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./components */ \"./src/components/index.js\");\n\n\nconst App = {\n el: '#app',\n delimiters: ['[[', ']]'],\n components: {\n ..._components__WEBPACK_IMPORTED_MODULE_1__[\"default\"],\n ...{\n VCalendar: v_calendar__WEBPACK_IMPORTED_MODULE_0__.Calendar,\n VDatepicker: v_calendar__WEBPACK_IMPORTED_MODULE_0__.DatePicker\n }\n },\n computed: {\n player() {\n return window.aircox.player;\n }\n },\n methods: {\n deleteElements(sel) {\n for (var el of document.querySelectorAll(sel)) el.parentNode.removeChild(el);\n }\n }\n};\nconst PlayerApp = {\n el: '#player',\n delimiters: ['[[', ']]'],\n components: {\n ..._components__WEBPACK_IMPORTED_MODULE_1__[\"default\"]\n }\n};\n/* harmony default export */ __webpack_exports__[\"default\"] = (App);\n\n//# sourceURL=webpack://aircox-assets/./src/app.js?"); /***/ }), @@ -465,7 +465,7 @@ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpac \*********************************/ /***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { -eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ admin: function() { return /* binding */ admin; },\n/* harmony export */ base: function() { return /* binding */ base; },\n/* harmony export */ dashboard: function() { return /* binding */ dashboard; }\n/* harmony export */ });\n/* harmony import */ var _AActionButton_vue__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./AActionButton.vue */ \"./src/components/AActionButton.vue\");\n/* harmony import */ var _AAutocomplete__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./AAutocomplete */ \"./src/components/AAutocomplete.vue\");\n/* harmony import */ var _ACarousel__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./ACarousel */ \"./src/components/ACarousel.vue\");\n/* harmony import */ var _ADropdown__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./ADropdown */ \"./src/components/ADropdown.vue\");\n/* harmony import */ var _AEpisode__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./AEpisode */ \"./src/components/AEpisode.vue\");\n/* harmony import */ var _AList__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ./AList */ \"./src/components/AList.vue\");\n/* harmony import */ var _APage__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ./APage */ \"./src/components/APage.vue\");\n/* harmony import */ var _APlayer__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! ./APlayer */ \"./src/components/APlayer.vue\");\n/* harmony import */ var _APlaylist__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(/*! ./APlaylist */ \"./src/components/APlaylist.vue\");\n/* harmony import */ var _AProgress__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(/*! ./AProgress */ \"./src/components/AProgress.vue\");\n/* harmony import */ var _ASoundItem__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(/*! ./ASoundItem */ \"./src/components/ASoundItem.vue\");\n/* harmony import */ var _ASwitch__WEBPACK_IMPORTED_MODULE_11__ = __webpack_require__(/*! ./ASwitch */ \"./src/components/ASwitch.vue\");\n/* harmony import */ var _AModal__WEBPACK_IMPORTED_MODULE_12__ = __webpack_require__(/*! ./AModal */ \"./src/components/AModal.vue\");\n/* harmony import */ var _AFileUpload__WEBPACK_IMPORTED_MODULE_13__ = __webpack_require__(/*! ./AFileUpload */ \"./src/components/AFileUpload.vue\");\n/* harmony import */ var _ASelectFile__WEBPACK_IMPORTED_MODULE_14__ = __webpack_require__(/*! ./ASelectFile */ \"./src/components/ASelectFile.vue\");\n/* harmony import */ var _AStatistics__WEBPACK_IMPORTED_MODULE_15__ = __webpack_require__(/*! ./AStatistics */ \"./src/components/AStatistics.vue\");\n/* harmony import */ var _AStreamer__WEBPACK_IMPORTED_MODULE_16__ = __webpack_require__(/*! ./AStreamer */ \"./src/components/AStreamer.vue\");\n/* harmony import */ var _AFormSet__WEBPACK_IMPORTED_MODULE_17__ = __webpack_require__(/*! ./AFormSet */ \"./src/components/AFormSet.vue\");\n/* harmony import */ var _ATrackListEditor__WEBPACK_IMPORTED_MODULE_18__ = __webpack_require__(/*! ./ATrackListEditor */ \"./src/components/ATrackListEditor.vue\");\n/* harmony import */ var _ASoundListEditor__WEBPACK_IMPORTED_MODULE_19__ = __webpack_require__(/*! ./ASoundListEditor */ \"./src/components/ASoundListEditor.vue\");\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n/**\n * Core components\n */\nconst base = {\n AAutocomplete: _AAutocomplete__WEBPACK_IMPORTED_MODULE_1__[\"default\"],\n ACarousel: _ACarousel__WEBPACK_IMPORTED_MODULE_2__[\"default\"],\n ADropdown: _ADropdown__WEBPACK_IMPORTED_MODULE_3__[\"default\"],\n AEpisode: _AEpisode__WEBPACK_IMPORTED_MODULE_4__[\"default\"],\n AList: _AList__WEBPACK_IMPORTED_MODULE_5__[\"default\"],\n APage: _APage__WEBPACK_IMPORTED_MODULE_6__[\"default\"],\n APlayer: _APlayer__WEBPACK_IMPORTED_MODULE_7__[\"default\"],\n APlaylist: _APlaylist__WEBPACK_IMPORTED_MODULE_8__[\"default\"],\n AProgress: _AProgress__WEBPACK_IMPORTED_MODULE_9__[\"default\"],\n ASoundItem: _ASoundItem__WEBPACK_IMPORTED_MODULE_10__[\"default\"],\n ASwitch: _ASwitch__WEBPACK_IMPORTED_MODULE_11__[\"default\"]\n};\n/* harmony default export */ __webpack_exports__[\"default\"] = (base);\nconst admin = {\n ...base,\n AStatistics: _AStatistics__WEBPACK_IMPORTED_MODULE_15__[\"default\"],\n AStreamer: _AStreamer__WEBPACK_IMPORTED_MODULE_16__[\"default\"],\n ATrackListEditor: _ATrackListEditor__WEBPACK_IMPORTED_MODULE_18__[\"default\"]\n};\nconst dashboard = {\n ...base,\n AActionButton: _AActionButton_vue__WEBPACK_IMPORTED_MODULE_0__[\"default\"],\n AFileUpload: _AFileUpload__WEBPACK_IMPORTED_MODULE_13__[\"default\"],\n ASelectFile: _ASelectFile__WEBPACK_IMPORTED_MODULE_14__[\"default\"],\n AModal: _AModal__WEBPACK_IMPORTED_MODULE_12__[\"default\"],\n AFormSet: _AFormSet__WEBPACK_IMPORTED_MODULE_17__[\"default\"],\n ATrackListEditor: _ATrackListEditor__WEBPACK_IMPORTED_MODULE_18__[\"default\"],\n ASoundListEditor: _ASoundListEditor__WEBPACK_IMPORTED_MODULE_19__[\"default\"]\n};\n\n//# sourceURL=webpack://aircox-assets/./src/components/index.js?"); +eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ admin: function() { return /* binding */ admin; },\n/* harmony export */ base: function() { return /* binding */ base; },\n/* harmony export */ dashboard: function() { return /* binding */ dashboard; }\n/* harmony export */ });\n/* harmony import */ var _AActionButton_vue__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./AActionButton.vue */ \"./src/components/AActionButton.vue\");\n/* harmony import */ var _AAutocomplete__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./AAutocomplete */ \"./src/components/AAutocomplete.vue\");\n/* harmony import */ var _ACarousel__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./ACarousel */ \"./src/components/ACarousel.vue\");\n/* harmony import */ var _ADropdown__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./ADropdown */ \"./src/components/ADropdown.vue\");\n/* harmony import */ var _AEpisode__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./AEpisode */ \"./src/components/AEpisode.vue\");\n/* harmony import */ var _AList__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ./AList */ \"./src/components/AList.vue\");\n/* harmony import */ var _APage__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ./APage */ \"./src/components/APage.vue\");\n/* harmony import */ var _APlayer__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! ./APlayer */ \"./src/components/APlayer.vue\");\n/* harmony import */ var _APlaylist__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(/*! ./APlaylist */ \"./src/components/APlaylist.vue\");\n/* harmony import */ var _AProgress__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(/*! ./AProgress */ \"./src/components/AProgress.vue\");\n/* harmony import */ var _ASoundItem__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(/*! ./ASoundItem */ \"./src/components/ASoundItem.vue\");\n/* harmony import */ var _ASwitch__WEBPACK_IMPORTED_MODULE_11__ = __webpack_require__(/*! ./ASwitch */ \"./src/components/ASwitch.vue\");\n/* harmony import */ var _AModal__WEBPACK_IMPORTED_MODULE_12__ = __webpack_require__(/*! ./AModal */ \"./src/components/AModal.vue\");\n/* harmony import */ var _AFileUpload__WEBPACK_IMPORTED_MODULE_13__ = __webpack_require__(/*! ./AFileUpload */ \"./src/components/AFileUpload.vue\");\n/* harmony import */ var _ASelectFile__WEBPACK_IMPORTED_MODULE_14__ = __webpack_require__(/*! ./ASelectFile */ \"./src/components/ASelectFile.vue\");\n/* harmony import */ var _AStatistics__WEBPACK_IMPORTED_MODULE_15__ = __webpack_require__(/*! ./AStatistics */ \"./src/components/AStatistics.vue\");\n/* harmony import */ var _AStreamer__WEBPACK_IMPORTED_MODULE_16__ = __webpack_require__(/*! ./AStreamer */ \"./src/components/AStreamer.vue\");\n/* harmony import */ var _AFormSet__WEBPACK_IMPORTED_MODULE_17__ = __webpack_require__(/*! ./AFormSet */ \"./src/components/AFormSet.vue\");\n/* harmony import */ var _ATrackListEditor__WEBPACK_IMPORTED_MODULE_18__ = __webpack_require__(/*! ./ATrackListEditor */ \"./src/components/ATrackListEditor.vue\");\n/* harmony import */ var _ASoundListEditor__WEBPACK_IMPORTED_MODULE_19__ = __webpack_require__(/*! ./ASoundListEditor */ \"./src/components/ASoundListEditor.vue\");\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n/**\n * Core components\n */\nconst base = {\n AAutocomplete: _AAutocomplete__WEBPACK_IMPORTED_MODULE_1__[\"default\"],\n ACarousel: _ACarousel__WEBPACK_IMPORTED_MODULE_2__[\"default\"],\n ADropdown: _ADropdown__WEBPACK_IMPORTED_MODULE_3__[\"default\"],\n AEpisode: _AEpisode__WEBPACK_IMPORTED_MODULE_4__[\"default\"],\n AList: _AList__WEBPACK_IMPORTED_MODULE_5__[\"default\"],\n APage: _APage__WEBPACK_IMPORTED_MODULE_6__[\"default\"],\n APlayer: _APlayer__WEBPACK_IMPORTED_MODULE_7__[\"default\"],\n APlaylist: _APlaylist__WEBPACK_IMPORTED_MODULE_8__[\"default\"],\n AProgress: _AProgress__WEBPACK_IMPORTED_MODULE_9__[\"default\"],\n ASoundItem: _ASoundItem__WEBPACK_IMPORTED_MODULE_10__[\"default\"],\n ASwitch: _ASwitch__WEBPACK_IMPORTED_MODULE_11__[\"default\"]\n};\n/* harmony default export */ __webpack_exports__[\"default\"] = (base);\nconst admin = {\n ...base,\n ATrackListEditor: _ATrackListEditor__WEBPACK_IMPORTED_MODULE_18__[\"default\"]\n};\nconst dashboard = {\n ...base,\n AActionButton: _AActionButton_vue__WEBPACK_IMPORTED_MODULE_0__[\"default\"],\n AFileUpload: _AFileUpload__WEBPACK_IMPORTED_MODULE_13__[\"default\"],\n ASelectFile: _ASelectFile__WEBPACK_IMPORTED_MODULE_14__[\"default\"],\n AModal: _AModal__WEBPACK_IMPORTED_MODULE_12__[\"default\"],\n AFormSet: _AFormSet__WEBPACK_IMPORTED_MODULE_17__[\"default\"],\n ATrackListEditor: _ATrackListEditor__WEBPACK_IMPORTED_MODULE_18__[\"default\"],\n ASoundListEditor: _ASoundListEditor__WEBPACK_IMPORTED_MODULE_19__[\"default\"],\n AStatistics: _AStatistics__WEBPACK_IMPORTED_MODULE_15__[\"default\"],\n AStreamer: _AStreamer__WEBPACK_IMPORTED_MODULE_16__[\"default\"]\n};\n\n//# sourceURL=webpack://aircox-assets/./src/components/index.js?"); /***/ }), @@ -505,7 +505,7 @@ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpac \*************************/ /***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { -eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": function() { return /* binding */ PageLoad; }\n/* harmony export */ });\n/* harmony import */ var core_js_modules_es_array_push_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! core-js/modules/es.array.push.js */ \"./node_modules/core-js/modules/es.array.push.js\");\n/* harmony import */ var core_js_modules_es_array_push_js__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_array_push_js__WEBPACK_IMPORTED_MODULE_0__);\n\n/**\n * Load page without leaving current one (hot-reload).\n */\nclass PageLoad {\n constructor(el, {\n loadingClass = \"loading\",\n append = false\n } = {}) {\n this.el = el;\n this.append = append;\n this.loadingClass = loadingClass;\n }\n get target() {\n if (!this._target) this._target = document.querySelector(this.el);\n return this._target;\n }\n reset() {\n this._target = null;\n }\n\n /**\n * Enable hot reload: catch page change in order to fetch them and\n * load page without actually leaving current one.\n */\n enable(target = null) {\n if (this._pageChanged) throw \"Already enabled, please disable me\";\n if (!target) target = this.target || document.body;\n this.historySave(document.location, true);\n this._pageChanged = event => this.pageChanged(event);\n this._statePopped = event => this.statePopped(event);\n target.addEventListener('click', this._pageChanged, true);\n target.addEventListener('submit', this._pageChanged, true);\n window.addEventListener('popstate', this._statePopped, true);\n }\n\n /**\n * Disable hot reload, remove listeners.\n */\n disable() {\n this.target.removeEventListener('click', this._pageChanged, true);\n this.target.removeEventListener('submit', this._pageChanged, true);\n window.removeEventListener('popstate', this._statePopped, true);\n this._pageChanged = null;\n this._statePopped = null;\n }\n\n /**\n * Fetch url, return promise, similar to standard Fetch API.\n * Default implementation just forward argument to it.\n */\n fetch(url, options) {\n return fetch(url, options);\n }\n\n /**\n * Fetch app from remote and mount application.\n */\n load(url, {\n mount = true,\n scroll = [0, 0],\n ...options\n } = {}) {\n if (this.loadingClass) this.target.classList.add(this.loadingClass);\n if (this.onLoad) this.onLoad({\n url,\n el: this.el,\n options\n });\n if (scroll) window.scroll(...scroll);\n return this.fetch(url, options).then(response => response.text()).then(content => {\n if (this.loadingClass) this.target.classList.remove(this.loadingClass);\n var doc = new DOMParser().parseFromString(content, 'text/html');\n var dom = doc.querySelectorAll(this.el);\n var result = {\n url,\n content: dom || [document.createTextNode(content)],\n title: doc.title,\n append: this.append\n };\n mount && this.mount(result);\n return result;\n });\n }\n\n /**\n * Mount the page on provided target element\n */\n mount({\n content,\n title = null,\n ...options\n } = {}) {\n if (this.onPreMount) this.onPreMount({\n target: this.target,\n content,\n items,\n title\n });\n var items = null;\n if (content) items = this.mountContent(content, options);\n if (title) document.title = title;\n if (this.onMount) this.onMount({\n target: this.target,\n content,\n items,\n title\n });\n }\n\n /**\n * Mount page content\n */\n mountContent(content, {\n append = false\n } = {}) {\n if (typeof content == \"string\") {\n this.target.innerHTML = append ? this.target.innerHTML + content : content;\n // TODO\n return [];\n }\n if (!append) this.target.innerHTML = \"\";\n var fragment = document.createDocumentFragment();\n var items = [];\n for (var node of content) while (node.firstChild) {\n items.push(node.firstChild);\n fragment.appendChild(node.firstChild);\n }\n this.target.append(fragment);\n return items;\n }\n\n /// Save application state into browser history\n historySave(url, replace = false) {\n const state = {\n content: this.target.innerHTML,\n title: document.title\n };\n if (replace) history.replaceState(state, '', url);else history.pushState(state, '', url);\n }\n\n // --- events\n pageChanged(event) {\n let submit = event.type == 'submit';\n let target = submit || event.target.tagName == 'A' ? event.target : event.target.closest('a');\n if (!target || target.hasAttribute('target')) return;\n let url = submit ? target.getAttribute('action') || '' : target.getAttribute('href');\n let domain = window.location.protocol + '//' + window.location.hostname;\n let stay = (url === '' || url.startsWith('/') || url.startsWith('?') || url.startsWith(domain)) && url.indexOf('wp-admin') == -1;\n if (url === null || !stay) {\n return;\n }\n let options = {};\n if (submit) {\n let formData = new FormData(event.target);\n if (target.method == 'get') url += '?' + new URLSearchParams(formData).toString();else options = {\n ...options,\n method: target.method,\n body: formData\n };\n }\n this.load(url, options).then(() => this.historySave(url));\n event.preventDefault();\n event.stopPropagation();\n }\n statePopped(event) {\n const state = event.state;\n if (state && state.content) this.mount({\n content: state.content,\n title: state.title\n });\n }\n}\n\n//# sourceURL=webpack://aircox-assets/./src/pageLoad.js?"); +eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": function() { return /* binding */ PageLoad; }\n/* harmony export */ });\n/* harmony import */ var core_js_modules_es_array_push_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! core-js/modules/es.array.push.js */ \"./node_modules/core-js/modules/es.array.push.js\");\n/* harmony import */ var core_js_modules_es_array_push_js__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_array_push_js__WEBPACK_IMPORTED_MODULE_0__);\n\n/**\n * Load page without leaving current one (hot-reload).\n */\nclass PageLoad {\n constructor(el, {\n loadingClass = \"loading\",\n append = false\n } = {}) {\n this.el = el;\n this.append = append;\n this.loadingClass = loadingClass;\n }\n get target() {\n if (!this._target) this._target = document.querySelector(this.el);\n return this._target;\n }\n reset() {\n this._target = null;\n }\n\n /**\n * Enable hot reload: catch page change in order to fetch them and\n * load page without actually leaving current one.\n */\n enable(target = null) {\n if (this._pageChanged) throw \"Already enabled, please disable me\";\n if (!target) target = this.target || document.body;\n this.historySave(document.location, true);\n this._pageChanged = event => this.pageChanged(event);\n this._statePopped = event => this.statePopped(event);\n target.addEventListener('click', this._pageChanged, true);\n target.addEventListener('submit', this._pageChanged, true);\n window.addEventListener('popstate', this._statePopped, true);\n }\n\n /**\n * Disable hot reload, remove listeners.\n */\n disable() {\n this.target.removeEventListener('click', this._pageChanged, true);\n this.target.removeEventListener('submit', this._pageChanged, true);\n window.removeEventListener('popstate', this._statePopped, true);\n this._pageChanged = null;\n this._statePopped = null;\n }\n\n /**\n * Fetch url, return promise, similar to standard Fetch API.\n * Default implementation just forward argument to it.\n */\n fetch(url, options) {\n return fetch(url, options);\n }\n\n /**\n * Fetch app from remote and mount application.\n */\n load(url, {\n mount = true,\n scroll = [0, 0],\n ...options\n } = {}) {\n if (this.loadingClass) this.target.classList.add(this.loadingClass);\n if (this.onLoad) this.onLoad({\n url,\n el: this.el,\n options\n });\n if (scroll) window.scroll(...scroll);\n return this.fetch(url, options).then(response => response.text()).then(content => {\n if (this.loadingClass) this.target.classList.remove(this.loadingClass);\n var doc = new DOMParser().parseFromString(content, 'text/html');\n var dom = doc.querySelectorAll(this.el);\n var result = {\n url,\n content: dom || [document.createTextNode(content)],\n title: doc.title,\n append: this.append\n };\n mount && this.mount(result);\n return result;\n });\n }\n\n /**\n * Mount the page on provided target element\n */\n mount({\n content,\n title = null,\n ...options\n } = {}) {\n if (this.onPreMount) this.onPreMount({\n target: this.target,\n content,\n items,\n title\n });\n var items = null;\n if (content) items = this.mountContent(content, options);\n if (title) document.title = title;\n if (this.onMount) this.onMount({\n target: this.target,\n content,\n items,\n title\n });\n }\n\n /**\n * Mount page content\n */\n mountContent(content, {\n append = false\n } = {}) {\n if (typeof content == \"string\") {\n this.target.innerHTML = append ? this.target.innerHTML + content : content;\n // TODO\n return [];\n }\n if (!append) this.target.innerHTML = \"\";\n var fragment = document.createDocumentFragment();\n var items = [];\n for (var node of content) while (node.firstChild) {\n items.push(node.firstChild);\n fragment.appendChild(node.firstChild);\n }\n this.target.append(fragment);\n return items;\n }\n\n /// Save application state into browser history\n historySave(url, replace = false) {\n const state = {\n content: this.target.innerHTML,\n title: document.title\n };\n if (replace) history.replaceState(state, '', url);else history.pushState(state, '', url);\n }\n\n // --- events\n pageChanged(event) {\n let submit = event.type == 'submit';\n let target = submit || event.target.tagName == 'A' ? event.target : event.target.closest('a');\n if (!target || target.hasAttribute('target') || target.data.forceReload) return;\n let url = submit ? target.getAttribute('action') || '' : target.getAttribute('href');\n let domain = window.location.protocol + '//' + window.location.hostname;\n let stay = (url === '' || url.startsWith('/') || url.startsWith('?') || url.startsWith(domain)) && url.indexOf('wp-admin') == -1;\n if (url === null || !stay) {\n return;\n }\n let options = {};\n if (submit) {\n let formData = new FormData(event.target);\n if (target.method == 'get') url += '?' + new URLSearchParams(formData).toString();else options = {\n ...options,\n method: target.method,\n body: formData\n };\n }\n this.load(url, options).then(() => this.historySave(url));\n event.preventDefault();\n event.stopPropagation();\n }\n statePopped(event) {\n const state = event.state;\n if (state && state.content) this.mount({\n content: state.content,\n title: state.title\n });\n }\n}\n\n//# sourceURL=webpack://aircox-assets/./src/pageLoad.js?"); /***/ }), diff --git a/aircox/templates/aircox/base.html b/aircox/templates/aircox/base.html index 9416902..f100b13 100644 --- a/aircox/templates/aircox/base.html +++ b/aircox/templates/aircox/base.html @@ -37,6 +37,7 @@ Usefull context: {% block head_extra %}{% endblock %}
+ {% block body-head %}{% endblock %} + +{% endblock %} {% block head-title %} {% block title %}{{ block.super }}{% endblock %} diff --git a/aircox/templates/aircox/dashboard/statistics.html b/aircox/templates/aircox/dashboard/statistics.html new file mode 100644 index 0000000..785de35 --- /dev/null +++ b/aircox/templates/aircox/dashboard/statistics.html @@ -0,0 +1,92 @@ +{% extends "./base.html" %} +{% load i18n aircox %} + +{% block title %}{% translate "Statistics" %}{% endblock %} + +{% block content-container %} +{% translate "Time" %} | +{% translate "Episode" %} / {% translate "Track" %} | +{% translate "Tags" %} | +||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
{{ object.start|time:"H:i" }} - {{ object.end|time:"H:i" }} | ++ {{ object.episode|default:"" }} + | +|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
{{ object.start|time:"H:i" }} {% if object|is_diffusion %} - {{ object.end|time:"H:i" }}{% endif %} | + {% endif %} + ++ {% if object.source %}{{ object.source }} / {% endif %} + {% include "aircox/widgets/track_item.html" with object=track %} + | + {% with track.tags.all|join:', ' as tags %} ++ {% if tags and tags.strip %} + + {% endif %} + | + {% endwith %} +||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
{% translate "No tracks" %} | +||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
{% translate "Totals" %} | +
+
+
+ [[ tag ]]
+ [[ count ]]
+
+
+ |
+
{{ field.help_text }}
diff --git a/aircox/templates/aircox/widgets/comment.html b/aircox/templates/aircox/widgets/comment.html index 7595708..3289b77 100644 --- a/aircox/templates/aircox/widgets/comment.html +++ b/aircox/templates/aircox/widgets/comment.html @@ -2,7 +2,6 @@ {% load i18n humanize aircox %} {% block tag-class %}{{ block.super }} comment{% endblock %} -{% block tag-extra %} id="comment-{{ object.pk }}"{% endblock %} {% block outer %} {% with url=object.get_absolute_url %} @@ -33,18 +32,23 @@ {% block actions %} {{ block.super }} -{% if request.user.is_staff %} +{% if admin %} +{% if user.is_staff %} - - - +{% endif %} +