diff --git a/aircox_web/admin.py b/aircox_web/admin.py index bc47fcd..a9f3405 100644 --- a/aircox_web/admin.py +++ b/aircox_web/admin.py @@ -3,7 +3,7 @@ import copy from django.contrib import admin from django.utils.translation import ugettext_lazy as _ -from content_editor.admin import ContentEditor +from content_editor.admin import ContentEditor, ContentEditorInline from feincms3 import plugins from feincms3.admin import TreeAdmin @@ -16,8 +16,9 @@ from aircox.admin.mixins import UnrelatedInlineMixin @admin.register(models.Site) class SiteAdmin(ContentEditor): inlines = [ - plugins.richtext.RichTextInline.create(models.SiteRichText), - plugins.image.ImageInline.create(models.SiteImage), + ContentEditorInline.create(models.SiteRichText), + ContentEditorInline.create(models.SiteImage), + ContentEditorInline.create(models.SiteLink), ] @@ -36,36 +37,58 @@ class PageDiffusionPlaylist(UnrelatedInlineMixin, TracksInline): @admin.register(models.Page) -class PageAdmin(ContentEditor, TreeAdmin): - list_display = ["indented_title", "move_column", "is_active"] +class PageAdmin(ContentEditor): + list_display = ["title", "parent", "status"] prepopulated_fields = {"slug": ("title",)} # readonly_fields = ('diffusion',) fieldsets = ( (_('Main'), { - 'fields': ['title', 'slug', 'by_program', 'summary'], + 'fields': ['title', 'slug', 'as_program', 'headline'], 'classes': ('tabbed', 'uncollapse') }), (_('Settings'), { - 'fields': ['show_author', 'featured', 'allow_comments', + 'fields': ['featured', 'allow_comments', 'status', 'static_path', 'path'], 'classes': ('tabbed',) }), - (_('Infos'), { - 'fields': ['diffusion'], - 'classes': ('tabbed',) - }), + #(_('Infos'), { + # 'fields': ['diffusion'], + # 'classes': ('tabbed',) + #}), ) inlines = [ - plugins.richtext.RichTextInline.create(models.PageRichText), - plugins.image.ImageInline.create(models.PageImage), + ContentEditorInline.create(models.PageRichText), + ContentEditorInline.create(models.PageImage), + ] + + +@admin.register(models.DiffusionPage) +class DiffusionPageAdmin(PageAdmin): + fieldsets = copy.deepcopy(PageAdmin.fieldsets) + fieldsets[1][1]['fields'].insert(0, 'diffusion') + + inlines = PageAdmin.inlines + [ + PageDiffusionPlaylist + ] + + # TODO: permissions + #def get_inline_instances(self, request, obj=None): + # inlines = super().get_inline_instances(request, obj) + # if obj and obj.diffusion: + # inlines.insert(0, PageDiffusionPlaylist(self.model, self.admin_site)) + # return inlines + + +@admin.register(models.ProgramPage) +class DiffusionPageAdmin(PageAdmin): + fieldsets = copy.deepcopy(PageAdmin.fieldsets) + fieldsets[1][1]['fields'].insert(0, 'program') + + inlines = PageAdmin.inlines + [ + PageDiffusionPlaylist ] - def get_inline_instances(self, request, obj=None): - inlines = super().get_inline_instances(request, obj) - if obj and obj.diffusion: - inlines.insert(0, PageDiffusionPlaylist(self.model, self.admin_site)) - return inlines diff --git a/aircox_web/assets/index.js b/aircox_web/assets/index.js index 089d9c1..17c2705 100644 --- a/aircox_web/assets/index.js +++ b/aircox_web/assets/index.js @@ -1,2 +1,3 @@ import './js'; +import './styles.scss'; diff --git a/aircox_web/assets/js/index.js b/aircox_web/assets/js/index.js index a4233d5..206ef10 100644 --- a/aircox_web/assets/js/index.js +++ b/aircox_web/assets/js/index.js @@ -1,6 +1,5 @@ import Vue from 'vue'; import Buefy from 'buefy'; -import 'buefy/dist/buefy.css'; Vue.use(Buefy); diff --git a/aircox_web/models.py b/aircox_web/models.py index 376f969..0b8abe3 100644 --- a/aircox_web/models.py +++ b/aircox_web/models.py @@ -1,16 +1,18 @@ +from django.core.validators import RegexValidator from django.db import models +from django.db.models import F +from django.db.models.functions import Concat, Substr from django.utils.translation import ugettext_lazy as _ -from django.contrib.auth import models as auth from content_editor.models import Region, create_plugin_base -from feincms3 import plugins -from feincms3.pages import AbstractPage from model_utils.models import TimeStampedModel, StatusModel +from model_utils.managers import InheritanceManager from model_utils import Choices from filer.fields.image import FilerImageField from aircox import models as aircox +from . import plugins class Site(models.Model): @@ -34,6 +36,11 @@ class Site(models.Model): related_name='+', ) + default = models.BooleanField(_('default site'), + default=False, + help_text=_('Use as default site'), + ) + # meta descriptors description = models.CharField( _('Description'), max_length=128, @@ -52,38 +59,119 @@ class Site(models.Model): SitePlugin = create_plugin_base(Site) -class SiteRichText(plugins.richtext.RichText, SitePlugin): +class SiteRichText(plugins.RichText, SitePlugin): pass +class SiteImage(plugins.Image, SitePlugin): + pass -class SiteImage(plugins.image.Image, SitePlugin): - caption = models.CharField(_("caption"), max_length=200, blank=True) +class SiteLink(plugins.Link, SitePlugin): + css_class="navbar-item" +#----------------------------------------------------------------------- +class BasePage(StatusModel): + """ + Base abstract class for views whose url path is defined by users. + Page parenting is based on foreignkey to parent and page path. -class Page(AbstractPage, TimeStampedModel, StatusModel): - STATUS = Choices('draft', 'published') + Inspired by Feincms3. + """ + STATUS = Choices('draft', 'announced', 'published') + + parent = models.ForeignKey( + 'self', models.CASCADE, + verbose_name=_('parent page'), + blank=True, null=True, + ) + title = models.CharField(max_length=128) + slug = models.SlugField(_('slug')) + path = models.CharField( + _("path"), max_length=1000, + blank=True, db_index=True, unique=True, + validators=[ + RegexValidator( + regex=r"^/(|.+/)$", + message=_("Path must start and end with a slash (/)."), + ) + ], + ) + static_path = models.BooleanField( + _('static path'), default=False, + help_text=_('Update path using parent\'s page path and page title') + ) + + objects = InheritanceManager() + + class Meta: + abstract = True + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._initial_path = self.path + self._initial_parent = self.parent + self._initial_slug = self.slug + + def view(self, request, *args, **kwargs): + """ Page view function """ + from django.http import HttpResponse + return HttpResponse('Not implemented') + + def update_descendants(self): + """ Update descendants pages' path if required. """ + if self.path == self._initial_path: + return + + # FIXME: draft -> draft children? + expr = Concat(self.path, Substr(F('path'), len(self._initial_path))) + BasePage.objects.filter(path__startswith=self._initial_path) \ + .update(path=expr) + + def sync_generations(self, update_descendants=True): + """ + Update fields (path, ...) based on parent. Update childrens if + ``update_descendants`` is True. + """ + # TODO: set parent based on path (when static path) + # TODO: ensure unique path fallback + if self.path == self._initial_path and \ + self.slug == self._initial_slug and \ + self.parent == self._initial_parent: + return + + if not self.title or not self.path or self.static_path and \ + self.slug != self._initial_slug: + self.path = self.parent.path + '/' + self.slug \ + if self.parent is not None else '/' + self.slug + + if self.path[-1] != '/': + self.path += '/' + if self.path[0] != '/': + self.path = '/' + self.path + if update_descendants: + self.update_descendants() + + def save(self, *args, update_descendants=True, **kwargs): + self.sync_generations(update_descendants) + super().save(*args, **kwargs) + + +class Page(BasePage, TimeStampedModel): + """ User's pages """ regions = [ Region(key="main", title=_("Content")), ] # metadata - by = models.ForeignKey( - auth.User, models.SET_NULL, blank=True, null=True, - verbose_name=_('Author'), - ) - by_program = models.ForeignKey( + as_program = models.ForeignKey( aircox.Program, models.SET_NULL, blank=True, null=True, - related_name='authored_pages', + related_name='published_pages', limit_choices_to={'schedule__isnull': False}, verbose_name=_('Show program as author'), - help_text=_("If nothing is selected, display user's name"), + help_text=_("Show program as author"), ) # options - show_author = models.BooleanField( - _('Show author'), default=True, - ) featured = models.BooleanField( _('featured'), default=False, ) @@ -92,36 +180,46 @@ class Page(AbstractPage, TimeStampedModel, StatusModel): ) # content - title = models.CharField( - _('title'), max_length=64, - ) - summary = models.TextField( - _('Summary'), - max_length=128, blank=True, null=True, + headline = models.TextField( + _('headline'), max_length=128, blank=True, null=True, ) cover = FilerImageField( on_delete=models.SET_NULL, null=True, blank=True, verbose_name=_('Cover'), ) + def get_view_class(self): + from .views import PageView + return PageView + + def view(self, request, *args, **kwargs): + """ Page view function """ + view = self.get_view_class().as_view() + return view(request, *args, **kwargs) + + +class DiffusionPage(Page): diffusion = models.OneToOneField( aircox.Diffusion, models.CASCADE, blank=True, null=True, ) + + +class ProgramPage(Page): program = models.OneToOneField( aircox.Program, models.CASCADE, blank=True, null=True, ) +#----------------------------------------------------------------------- PagePlugin = create_plugin_base(Page) -class PageRichText(plugins.richtext.RichText, PagePlugin): +class PageRichText(plugins.RichText, PagePlugin): + pass + +class PageImage(plugins.Image, PagePlugin): pass -class PageImage(plugins.image.Image, PagePlugin): - caption = models.CharField(_("caption"), max_length=200, blank=True) - - diff --git a/aircox_web/package.json b/aircox_web/package.json index f987767..ae265b3 100644 --- a/aircox_web/package.json +++ b/aircox_web/package.json @@ -7,19 +7,21 @@ "license": "AGPL", "devDependencies": { "@fortawesome/fontawesome-free": "^5.8.2", - "mini-css-extract-plugin": "^0.5.0", + "bulma": "^0.7.5", "css-loader": "^2.1.1", + "extract-text-webpack-plugin": "^4.0.0-beta.0", "file-loader": "^3.0.1", + "mini-css-extract-plugin": "^0.5.0", + "node-sass": "^4.12.0", + "sass-loader": "^7.1.0", + "style-loader": "^0.23.1", "ttf-loader": "^1.0.2", "vue-loader": "^15.7.0", "vue-style-loader": "^4.1.2", "webpack": "^4.32.2", - "webpack-bundle-analyzer": "^3.3.2", - "webpack-bundle-tracker": "^0.4.2-beta", "webpack-cli": "^3.3.2" }, "dependencies": { - "bootstrap": "^4.3.1", "buefy": "^0.7.8", "vue": "^2.6.10" } diff --git a/aircox_web/plugins/richtext.py b/aircox_web/plugins/richtext.py new file mode 100644 index 0000000..3e70308 --- /dev/null +++ b/aircox_web/plugins/richtext.py @@ -0,0 +1,12 @@ +from django.db import models +from django.utils.translation import ugettext_lazy as _ + +from ckeditor.fields import RichTextField + + +class RichText(models.Model): + text = RichTextField(_('text')) + + class Meta: + abstract = True + diff --git a/aircox_web/renderer.py b/aircox_web/renderer.py index 235a9c2..0deae15 100644 --- a/aircox_web/renderer.py +++ b/aircox_web/renderer.py @@ -1,35 +1,18 @@ from django.utils.html import format_html, mark_safe -from feincms3.renderer import TemplatePluginRenderer +from content_editor.renderer import PluginRenderer from .models import * -site_renderer = TemplatePluginRenderer() -site_renderer.register_string_renderer( - SiteRichText, - lambda plugin: mark_safe(plugin.text), -) -site_renderer.register_string_renderer( - SiteImage, - lambda plugin: format_html( - '
{}
', - plugin.image.url, - plugin.caption, - ), -) +site_renderer = PluginRenderer() +site_renderer._renderers.clear() +site_renderer.register(SiteRichText, lambda plugin: mark_safe(plugin.text)) +site_renderer.register(SiteImage, lambda plugin: plugin.render()) +site_renderer.register(SiteLink, lambda plugin: plugin.render()) -page_renderer = TemplatePluginRenderer() -page_renderer.register_string_renderer( - PageRichText, - lambda plugin: mark_safe(plugin.text), -) -page_renderer.register_string_renderer( - PageImage, - lambda plugin: format_html( - '
{}
', - plugin.image.url, - plugin.caption, - ), -) +page_renderer = PluginRenderer() +page_renderer._renderers.clear() +page_renderer.register(PageRichText, lambda plugin: mark_safe(plugin.text)) +page_renderer.register(PageImage, lambda plugin: plugin.render()) diff --git a/aircox_web/templates/aircox_web/base.html b/aircox_web/templates/aircox_web/base.html index 38c13bf..ec373e3 100644 --- a/aircox_web/templates/aircox_web/base.html +++ b/aircox_web/templates/aircox_web/base.html @@ -1,4 +1,4 @@ -{% load static thumbnail feincms3 %} +{% load static i18n thumbnail %} @@ -9,7 +9,7 @@ {% block assets %} - + @@ -20,18 +20,27 @@ {% block extra_head %}{% endblock %} -