diff --git a/aircox/admin/episode.py b/aircox/admin/episode.py index a8570a7..3a6fa67 100644 --- a/aircox/admin/episode.py +++ b/aircox/admin/episode.py @@ -3,7 +3,7 @@ from django.contrib import admin from django.forms import ModelForm from aircox.models import Episode, EpisodeSound -from .page import PageAdmin +from .page import ChildPageAdmin from .sound import TrackInline from .diffusion import DiffusionInline @@ -15,14 +15,14 @@ class EpisodeAdminForm(ModelForm): @admin.register(Episode) -class EpisodeAdmin(SortableAdminBase, PageAdmin): +class EpisodeAdmin(SortableAdminBase, ChildPageAdmin): form = EpisodeAdminForm - list_display = PageAdmin.list_display - list_filter = tuple(f for f in PageAdmin.list_filter if f != "pub_date") + ( + list_display = ChildPageAdmin.list_display + list_filter = tuple(f for f in ChildPageAdmin.list_filter if f != "pub_date") + ( "diffusion__start", "pub_date", ) - search_fields = PageAdmin.search_fields + ("parent__title",) + search_fields = ChildPageAdmin.search_fields + ("parent__title",) # readonly_fields = ('parent',) inlines = [TrackInline, DiffusionInline] diff --git a/aircox/admin/page.py b/aircox/admin/page.py index eb8c075..3a278bc 100644 --- a/aircox/admin/page.py +++ b/aircox/admin/page.py @@ -21,7 +21,7 @@ class CategoryAdmin(admin.ModelAdmin): class BasePageAdmin(admin.ModelAdmin): - list_display = ("cover_thumb", "title", "status", "parent") + list_display = ("cover_thumb", "title", "status") list_display_links = ("cover_thumb", "title") list_editable = ("status",) list_filter = ("status",) @@ -42,7 +42,9 @@ class BasePageAdmin(admin.ModelAdmin): ( _("Publication Settings"), { - "fields": ["status", "parent"], + "fields": [ + "status", + ], }, ), ] @@ -54,23 +56,6 @@ class BasePageAdmin(admin.ModelAdmin): return mark_safe(''.format(obj.cover.icons["64"])) return "" - def get_changeform_initial_data(self, request): - data = super().get_changeform_initial_data(request) - filters = QueryDict(request.GET.get("_changelist_filters", "")) - data["parent"] = filters.get("parent", None) - return data - - def _get_common_context(self, query, extra_context=None): - extra_context = extra_context or {} - parent = query.get("parent", None) - extra_context["parent"] = None if parent is None else Page.objects.get_subclass(id=parent) - return extra_context - - def render_change_form(self, request, context, *args, **kwargs): - if context["original"] and "parent" not in context: - context["parent"] = context["original"].parent - return super().render_change_form(request, context, *args, **kwargs) - def add_view(self, request, form_url="", extra_context=None): filters = QueryDict(request.GET.get("_changelist_filters", "")) extra_context = self._get_common_context(filters, extra_context) @@ -88,12 +73,36 @@ class PageAdmin(BasePageAdmin): list_editable = BasePageAdmin.list_editable + ("category",) list_filter = BasePageAdmin.list_filter + ("category", "pub_date") search_fields = BasePageAdmin.search_fields + ("category__title",) - fieldsets = deepcopy(BasePageAdmin.fieldsets) + fieldsets = deepcopy(BasePageAdmin.fieldsets) fieldsets[0][1]["fields"].insert(fieldsets[0][1]["fields"].index("slug") + 1, "category") fieldsets[1][1]["fields"] += ("featured", "allow_comments") +class ChildPageAdmin(PageAdmin): + list_display = PageAdmin.list_display + ("parent",) + + fieldsets = deepcopy(PageAdmin.fieldsets) + fieldsets[1][1]["fields"] += ("parent",) + + def get_changeform_initial_data(self, request): + data = super().get_changeform_initial_data(request) + filters = QueryDict(request.GET.get("_changelist_filters", "")) + data["parent"] = filters.get("parent", None) + return data + + def _get_common_context(self, query, extra_context=None): + extra_context = extra_context or {} + parent = query.get("parent", None) + extra_context["parent"] = None if parent is None else Page.objects.get_subclass(id=parent) + return extra_context + + def render_change_form(self, request, context, *args, **kwargs): + if context["original"] and "parent" not in context: + context["parent"] = context["original"].parent + return super().render_change_form(request, context, *args, **kwargs) + + @admin.register(StaticPage) class StaticPageAdmin(BasePageAdmin): list_display = BasePageAdmin.list_display + ("attach_to",) diff --git a/aircox/forms.py b/aircox/forms.py index a939e5d..ac6ae04 100644 --- a/aircox/forms.py +++ b/aircox/forms.py @@ -27,6 +27,12 @@ class ImageForm(forms.Form): class PageForm(forms.ModelForm): + class Meta: + fields = ("title", "category", "status", "cover", "content") + model = models.Page + + +class ChildPageForm(forms.ModelForm): class Meta: fields = ("title", "status", "cover", "content") model = models.Page @@ -41,7 +47,7 @@ class ProgramForm(PageForm): class EpisodeForm(PageForm): class Meta: model = models.Episode - fields = PageForm.Meta.fields + fields = ChildPageForm.Meta.fields class SoundForm(forms.ModelForm): diff --git a/aircox/migrations/0015_program_editors.py b/aircox/migrations/0015_program_editors.py index 9a3964f..2c921b0 100644 --- a/aircox/migrations/0015_program_editors.py +++ b/aircox/migrations/0015_program_editors.py @@ -13,7 +13,7 @@ class Migration(migrations.Migration): operations = [ migrations.AddField( model_name="program", - name="editors", + name="editors_group", field=models.ForeignKey( blank=True, null=True, diff --git a/aircox/migrations/0022_set_group_ownership.py b/aircox/migrations/0022_set_group_ownership.py deleted file mode 100644 index 7e0d3bd..0000000 --- a/aircox/migrations/0022_set_group_ownership.py +++ /dev/null @@ -1,18 +0,0 @@ -from django.db import migrations - -from aircox.models import Program - - -def set_group_ownership(*args): - for program in Program.objects.all(): - program.set_group_ownership() - - -class Migration(migrations.Migration): - dependencies = [ - ("aircox", "0021_alter_schedule_timezone"), - ] - - operations = [ - migrations.RunPython(set_group_ownership), - ] diff --git a/aircox/migrations/0023_station_legal_label_alter_schedule_timezone.py b/aircox/migrations/0023_station_legal_label_alter_schedule_timezone.py index 6a9b7ad..dba7c62 100644 --- a/aircox/migrations/0023_station_legal_label_alter_schedule_timezone.py +++ b/aircox/migrations/0023_station_legal_label_alter_schedule_timezone.py @@ -6,7 +6,8 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ("aircox", "0022_set_group_ownership"), + ("filer", "0017_image__transparent"), + ("aircox", "0021_alter_schedule_timezone"), ] operations = [ diff --git a/aircox/migrations/0027_remove_page_parent_remove_staticpage_parent_and_more.py b/aircox/migrations/0027_remove_page_parent_remove_staticpage_parent_and_more.py new file mode 100644 index 0000000..a927021 --- /dev/null +++ b/aircox/migrations/0027_remove_page_parent_remove_staticpage_parent_and_more.py @@ -0,0 +1,83 @@ +# Generated by Django 5.0 on 2024-04-10 08:38 + +import django.db.models.deletion +from django.db import migrations, models + + +children_infos = {} + + +def get_children_infos(apps, schema_editor): + Page = apps.get_model("aircox", "page") + query = Page.objects.filter(parent__isnull=False).values("pk", "parent_id", "category_id", "parent__category_id") + children_infos.update((r["pk"], r) for r in query) + + +def restore_children_infos(apps, schema_editor): + Episode = apps.get_model("aircox", "Episode") + + pks = set(children_infos.keys()) + eps = _restore_for_objs(Episode.objects.filter(pk__in=pks)) + Episode.objects.bulk_update(eps, ("parent_id", "category_id")) + print(f">> {len(eps)} episodes restored") + + +def _restore_for_objs(objs): + updated = [] + for obj in objs: + info = children_infos.get(obj.pk) + if info: + obj.parent_id = info["parent_id"] + obj.category_id = info["category_id"] or info["parent__category_id"] + updated.append(obj) + return updated + + +def set_group_ownership(*args): + for program in Program.objects.all(): + program.set_group_ownership() + + +class Migration(migrations.Migration): + dependencies = [ + ("aircox", "0026_alter_sound_options_remove_sound_episode_and_more"), + ] + + operations = [ + migrations.RunPython(get_children_infos), + migrations.RemoveField( + model_name="page", + name="parent", + ), + migrations.RemoveField( + model_name="staticpage", + name="parent", + ), + migrations.RemoveField( + model_name="station", + name="path", + ), + migrations.AddField( + model_name="article", + name="parent", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="%(class)s_set", + to="aircox.page", + ), + ), + migrations.AddField( + model_name="episode", + name="parent", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="%(class)s_set", + to="aircox.page", + ), + ), + migrations.RunPython(restore_children_infos), + ] diff --git a/aircox/models/article.py b/aircox/models/article.py index 0c86d77..c75387c 100644 --- a/aircox/models/article.py +++ b/aircox/models/article.py @@ -1,12 +1,12 @@ from django.utils.translation import gettext_lazy as _ -from .page import Page +from .page import ChildPage from .program import ProgramChildQuerySet __all__ = ("Article",) -class Article(Page): +class Article(ChildPage): detail_url_name = "article-detail" template_prefix = "article" diff --git a/aircox/models/diffusion.py b/aircox/models/diffusion.py index c04d1c0..d9beaff 100644 --- a/aircox/models/diffusion.py +++ b/aircox/models/diffusion.py @@ -17,6 +17,10 @@ __all__ = ("Diffusion", "DiffusionQuerySet") class DiffusionQuerySet(RerunQuerySet): + def editor(self, user): + episodes = Episode.objects.editor(user) + return self.filter(episode__in=episodes) + def episode(self, episode=None, id=None): """Diffusions for this episode.""" return self.filter(episode=episode) if id is None else self.filter(episode__id=id) @@ -200,7 +204,7 @@ class Diffusion(Rerun): @property def is_live(self): """True if Diffusion is live (False if there are sounds files).""" - return self.type == self.TYPE_ON_AIR and self.episode.episodesound_set.all().broadcast().empty() + return self.type == self.TYPE_ON_AIR and not self.episode.episodesound_set.all().broadcast() def is_date_in_range(self, date=None): """Return true if the given date is in the diffusion's start-end diff --git a/aircox/models/episode.py b/aircox/models/episode.py index b7e666b..59e2caf 100644 --- a/aircox/models/episode.py +++ b/aircox/models/episode.py @@ -7,7 +7,7 @@ from django.utils.translation import gettext_lazy as _ from aircox.conf import settings -from .page import Page +from .page import ChildPage from .program import ProgramChildQuerySet from .sound import Sound @@ -19,10 +19,11 @@ class EpisodeQuerySet(ProgramChildQuerySet): return self.filter(episodesound__sound__is_public=True).distinct() -class Episode(Page): +class Episode(ChildPage): objects = EpisodeQuerySet.as_manager() detail_url_name = "episode-detail" list_url_name = "episode-list" + edit_url_name = "episode-edit" template_prefix = "episode" @property @@ -59,11 +60,6 @@ class Episode(Page): verbose_name = _("Episode") verbose_name_plural = _("Episodes") - def get_absolute_url(self): - if not self.is_published: - return self.program.get_absolute_url() - return super().get_absolute_url() - def save(self, *args, **kwargs): if self.parent is None: raise ValueError("missing parent program") diff --git a/aircox/models/page.py b/aircox/models/page.py index 0581fd1..84b1ade 100644 --- a/aircox/models/page.py +++ b/aircox/models/page.py @@ -86,14 +86,6 @@ class BasePage(Renderable, models.Model): (STATUS_TRASH, _("trash")), ) - parent = models.ForeignKey( - "self", - models.CASCADE, - blank=True, - null=True, - db_index=True, - related_name="child_set", - ) title = models.CharField(max_length=100) slug = models.SlugField(_("slug"), max_length=120, blank=True, unique=True, db_index=True) status = models.PositiveSmallIntegerField( @@ -133,12 +125,6 @@ class BasePage(Renderable, models.Model): count = Page.objects.filter(slug__startswith=self.slug).count() if count: self.slug += "-" + str(count) - - if self.parent: - if self.parent == self: - self.parent = None - if not self.cover: - self.cover = self.parent.cover super().save(*args, **kwargs) def get_absolute_url(self): @@ -160,14 +146,10 @@ class BasePage(Renderable, models.Model): @property def display_title(self): - if self.is_published: - return self.title - return self.parent and self.parent.title or "" + return self.is_published and self.title or "" @cached_property def display_headline(self): - if not self.content or not self.is_published: - return self.parent and self.parent.display_headline or "" content = bleach.clean(self.content, tags=[], strip=True) content = headline_clean_re.sub("\n", content) if content.startswith("\n"): @@ -205,6 +187,7 @@ class BasePage(Renderable, models.Model): return cls(**cls.get_init_kwargs_from(page, **kwargs)) +# FIXME: rename class PageQuerySet(BasePageQuerySet): def published(self): return self.filter(status=Page.STATUS_PUBLISHED, pub_date__lte=tz.now()) @@ -232,8 +215,48 @@ class Page(BasePage): ) objects = PageQuerySet.as_manager() + detail_url_name = "" list_url_name = "page-list" + edit_url_name = "" + + @classmethod + def get_list_url(cls, kwargs={}): + return reverse(cls.list_url_name, kwargs=kwargs) + + class Meta: + verbose_name = _("Publication") + verbose_name_plural = _("Publications") + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.__initial_cover = self.cover + + def save(self, *args, **kwargs): + if self.is_published and self.pub_date is None: + self.pub_date = tz.now() + elif not self.is_published: + self.pub_date = None + super().save(*args, **kwargs) + + +class ChildPage(Page): + parent = models.ForeignKey(Page, models.CASCADE, blank=True, null=True, db_index=True, related_name="%(class)s_set") + + class Meta: + abstract = True + + @property + def display_title(self): + if self.is_published: + return self.title + return self.parent and self.parent.title or "" + + @property + def display_headline(self): + if not self.content or not self.is_published: + return self.parent and self.parent.display_headline or "" + return super().display_headline @cached_property def parent_subclass(self): @@ -246,22 +269,14 @@ class Page(BasePage): return self.parent_subclass.get_absolute_url() return super().get_absolute_url() - @classmethod - def get_list_url(cls, kwargs={}): - return reverse(cls.list_url_name, kwargs=kwargs) - - class Meta: - verbose_name = _("Publication") - verbose_name_plural = _("Publications") - def save(self, *args, **kwargs): - if self.is_published and self.pub_date is None: - self.pub_date = tz.now() - elif not self.is_published: - self.pub_date = None - - if self.parent and not self.category: - self.category = self.parent.category + if self.parent: + if self.parent == self: + self.parent = None + if not self.cover: + self.cover = self.parent.cover + if not self.category: + self.category = self.parent.category super().save(*args, **kwargs) diff --git a/aircox/models/program.py b/aircox/models/program.py index 85c1f06..f9b8ec4 100644 --- a/aircox/models/program.py +++ b/aircox/models/program.py @@ -1,13 +1,9 @@ -import logging import os -import shutil from django.conf import settings as conf from django.contrib.auth.models import Group, Permission from django.contrib.contenttypes.models import ContentType from django.db import models -from django.db.models import F -from django.db.models.functions import Concat, Substr from django.utils.translation import gettext_lazy as _ from aircox.conf import settings @@ -15,13 +11,11 @@ from aircox.conf import settings from .page import Page, PageQuerySet from .station import Station -logger = logging.getLogger("aircox") - __all__ = ( + "ProgramQuerySet", "Program", "ProgramChildQuerySet", - "ProgramQuerySet", "Stream", ) @@ -34,6 +28,16 @@ class ProgramQuerySet(PageQuerySet): def active(self): return self.filter(active=True) + def editor(self, user): + """Return programs for which user is an editor. + + Superuser is considered as editor of all groups. + """ + if user.is_superuser: + return self + groups = self.request.user.groups.all() + return self.filter(editors_group__in=groups) + class Program(Page): """A Program can either be a Streamed or a Scheduled program. @@ -60,11 +64,12 @@ class Program(Page): default=True, help_text=_("update later diffusions according to schedule changes"), ) - editors = models.ForeignKey(Group, models.CASCADE, blank=True, null=True, verbose_name=_("editors")) + editors_group = models.ForeignKey(Group, models.CASCADE, blank=True, null=True, verbose_name=_("editors")) objects = ProgramQuerySet.as_manager() detail_url_name = "program-detail" list_url_name = "program-list" + edit_url_name = "program-edit" @property def path(self): @@ -84,19 +89,10 @@ class Program(Page): def excerpts_path(self): return os.path.join(self.path, settings.SOUND_ARCHIVES_SUBDIR) - @property - def editors_group_name(self): - return f"{self.title} editors" - - @property - def change_permission_codename(self): - return f"change_program_{self.slug}" - - def __init__(self, *kargs, **kwargs): - super().__init__(*kargs, **kwargs) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) if self.slug: self.__initial_path = self.path - self.__initial_cover = self.cover @classmethod def get_from_path(cl, path): @@ -121,18 +117,36 @@ class Program(Page): os.makedirs(path, exist_ok=True) return os.path.exists(path) - def set_group_ownership(self): - editors, created = Group.objects.get_or_create(name=self.editors_group_name) + 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: - self.editors = editors - super().save() - permission, _ = Permission.objects.get_or_create( - codename=self.change_permission_codename, - content_type=ContentType.objects.get_for_model(self), - defaults={"name": f"change program {self.title}"}, - ) - if permission not in editors.permissions.all(): - editors.permissions.add(permission) + 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") @@ -141,29 +155,11 @@ class Program(Page): def __str__(self): return self.title - def save(self, *kargs, **kwargs): - from .sound import Sound - - super().save(*kargs, **kwargs) - - # TODO: move in signals - path_ = getattr(self, "__initial_path", None) - abspath = path_ and os.path.join(conf.MEDIA_ROOT, path_) - if path_ is not None and path_ != self.path and os.path.exists(abspath) and not os.path.exists(self.abspath): - logger.info( - "program #%s's dir changed to %s - update it.", - self.id, - self.title, - ) - - shutil.move(abspath, self.abspath) - Sound.objects.filter(path__startswith=path_).update(file=Concat("file", Substr(F("file"), len(path_)))) - - self.set_group_ownership() - class ProgramChildQuerySet(PageQuerySet): def station(self, station=None, id=None): + # lookup `__program` is due to parent being a page subclass (page is + # concrete). return ( self.filter(parent__program__station=station) if id is None @@ -173,6 +169,10 @@ class ProgramChildQuerySet(PageQuerySet): def program(self, program=None, id=None): return self.parent(program, id) + def editor(self, user): + programs = Program.objects.editor(user) + return self.filter(parent__program__in=programs) + class Stream(models.Model): """When there are no program scheduled, it is possible to play sounds in diff --git a/aircox/models/signals.py b/aircox/models/signals.py index b9d5dd4..b465310 100755 --- a/aircox/models/signals.py +++ b/aircox/models/signals.py @@ -1,13 +1,18 @@ +import logging import os +import shutil +from django.conf import settings as conf from django.contrib.auth.models import Group, Permission, User from django.db import transaction -from django.db.models import signals +from django.db.models import signals, F +from django.db.models.functions import Concat, Substr from django.dispatch import receiver from django.utils import timezone as tz from aircox import utils from aircox.conf import settings +from .article import Article from .diffusion import Diffusion from .episode import Episode from .page import Page @@ -16,6 +21,9 @@ from .schedule import Schedule from .sound import Sound +logger = logging.getLogger("aircox") + + # Add a default group to a user when it is created. It also assigns a list # of permissions to the group if it is created. # @@ -42,24 +50,40 @@ def user_default_groups(sender, instance, created, *args, **kwargs): instance.groups.add(group) +# ---- page @receiver(signals.post_save, sender=Page) -def page_post_save(sender, instance, created, *args, **kwargs): - if not created and instance.cover and "raw" not in kwargs: - Page.objects.filter(parent=instance, cover__isnull=True).update(cover=instance.cover) +def page_post_save__child_page_defaults(sender, instance, created, *args, **kwargs): + initial_cover = getattr(instance, "__initial_cover", None) + if initial_cover is None and instance.cover is not None: + Episode.objects.filter(parent=instance, cover__isnull=True).update(cover=instance.cover) + Article.objects.filter(parent=instance, cover__isnull=True).update(cover=instance.cover) +# ---- program @receiver(signals.post_save, sender=Program) -def program_post_save(sender, instance, created, *args, **kwargs): - """Clean-up later diffusions when a program becomes inactive.""" +def program_post_save__clean_later_episodes(sender, instance, created, *args, **kwargs): if not instance.active: Diffusion.objects.program(instance).after(tz.now()).delete() Episode.objects.parent(instance).filter(diffusion__isnull=True).delete() - cover = getattr(instance, "__initial_cover", None) - if cover is None and instance.cover is not None: - Episode.objects.parent(instance).filter(cover__isnull=True).update(cover=instance.cover) + +@receiver(signals.post_save, sender=Program) +def program_post_save__mv_sounds(sender, instance, created, *args, **kwargs): + path_ = getattr(instance, "__initial_path", None) + if path_ in (None, instance.path): + return + + abspath = path_ and os.path.join(conf.MEDIA_ROOT, path_) + if os.path.exists(abspath) and not os.path.exists(instance.abspath): + logger.info( + f"program #{instance.pk}'s dir changed to {instance.title} - update it.", instance.id, instance.title + ) + + shutil.move(abspath, instance.abspath) + Sound.objects.filter(path__startswith=path_).update(file=Concat("file", Substr(F("file"), len(path_)))) +# ---- schedule @receiver(signals.pre_save, sender=Schedule) def schedule_pre_save(sender, instance, *args, **kwargs): if getattr(instance, "pk") is not None and "raw" not in kwargs: @@ -94,11 +118,13 @@ def schedule_pre_delete(sender, instance, *args, **kwargs): Episode.objects.filter(diffusion__isnull=True, content__isnull=True, episodesound__isnull=True).delete() +# ---- diffusion @receiver(signals.post_delete, sender=Diffusion) def diffusion_post_delete(sender, instance, *args, **kwargs): Episode.objects.filter(diffusion__isnull=True, content__isnull=True, episodesound__isnull=True).delete() +# ---- files @receiver(signals.post_delete, sender=Sound) def delete_file(sender, instance, *args, **kwargs): """Deletes file on `post_delete`""" diff --git a/aircox/static/aircox/css/chunk-common.css b/aircox/static/aircox/css/chunk-common.css index ae6fbdf..a12a3f9 100644 --- a/aircox/static/aircox/css/chunk-common.css +++ b/aircox/static/aircox/css/chunk-common.css @@ -169,6 +169,9 @@ .preview .headings a:hover { color: var(--heading-link-hv-fg) !important; } +.preview.tiny .title { + font-size: calc(var(--preview-title-sz) * 0.8); +} .preview.tiny .content { font-size: 1rem; } @@ -256,6 +259,7 @@ } .list-item .actions { text-align: right; + align-items: center; } .list-item:not(.wide) .media { padding: 0.6rem; @@ -9689,6 +9693,26 @@ a.tag:hover { height: 25em; } +.gap-1 { + gap: 0.2rem !important; +} + +.gap-2 { + gap: 0.4rem !important; +} + +.gap-3 { + gap: 0.6rem !important; +} + +.gap-4 { + gap: 1.2rem !important; +} + +.gap-5 { + gap: 1.6rem !important; +} + .grid { display: grid; grid-template-columns: 1fr 1fr; @@ -9819,6 +9843,13 @@ a.tag:hover { border-color: #b00 !important; } +.box-shadow:hover { + box-shadow: 0em 0em 1em rgba(0, 0, 0, 0.2); +} +.box-shadow.active { + box-shadow: 0em 0em 1em rgba(0, 0, 0, 0.4); +} + input.half-field:not(:active):not(:hover) { border: none; background-color: rgba(0, 0, 0, 0); diff --git a/aircox/static/aircox/css/public.css b/aircox/static/aircox/css/public.css index 76f16c6..d54bbf6 100644 --- a/aircox/static/aircox/css/public.css +++ b/aircox/static/aircox/css/public.css @@ -169,6 +169,9 @@ .preview .headings a:hover { color: var(--heading-link-hv-fg) !important; } +.preview.tiny .title { + font-size: calc(var(--preview-title-sz) * 0.8); +} .preview.tiny .content { font-size: 1rem; } @@ -256,6 +259,7 @@ } .list-item .actions { text-align: right; + align-items: center; } .list-item:not(.wide) .media { padding: 0.6rem; diff --git a/aircox/templates/aircox/base.html b/aircox/templates/aircox/base.html index 5e42e48..9416902 100644 --- a/aircox/templates/aircox/base.html +++ b/aircox/templates/aircox/base.html @@ -86,9 +86,9 @@ Usefull context: {% endspaceless %} {% block header-container %} - {% if page or cover or title %}
{% block header %} + {% spaceless %}
{% block header-cover %} {% if cover %} @@ -96,6 +96,7 @@ Usefull context: {% endif %} {% endblock %}
+ {% endspaceless %}
{% block headings %}
@@ -119,7 +120,6 @@ Usefull context:
{% endblock %}
- {% endif %} {% endblock %} {% block content-container %} @@ -144,15 +144,6 @@ Usefull context: {{ render }} {% endfor %} {% endcomment %} - - {% if not request.user.is_authenticated %} - - - - - - {% endif %} {% endblock %} {% if request.station and request.station.legal_label %} diff --git a/aircox/templates/aircox/dashboard/base.html b/aircox/templates/aircox/dashboard/base.html new file mode 100644 index 0000000..120291e --- /dev/null +++ b/aircox/templates/aircox/dashboard/base.html @@ -0,0 +1,8 @@ +{% extends "../base.html" %} +{% load i18n %} + +{% block head-title %} + {% block title %}{{ block.super }}{% endblock %} + — + {{ block.super }} +{% endblock %} diff --git a/aircox/templates/aircox/dashboard/dashboard.html b/aircox/templates/aircox/dashboard/dashboard.html new file mode 100644 index 0000000..05c422a --- /dev/null +++ b/aircox/templates/aircox/dashboard/dashboard.html @@ -0,0 +1,50 @@ +{% extends "./base.html" %} +{% load i18n aircox %} + +{% block subtitle %} + + + +{{ block.super }} +{% if user.is_superuser %} + — {% translate "administrator" %} +{% endif %} +{% endblock %} + + +{% block content-container %} +
+
+

{% translate "Programs" %}

+
+ {% for object in programs %} + {% page_widget "item" object admin=True is_tiny=True %} + {% empty %} +
{% translate "No diffusion to come" %}
+ {% endfor %} +
+ +
+ +
+

{% translate "Next diffusions" %}

+
+ {% for object in next_diffs|slice:"0:25" %} + {% page_widget "item" object.episode diffusion=object timetable=True admin=True is_tiny=True %} + {% empty %} +
{% translate "No diffusion to come" %}
+ {% endfor %} +
+
+ +
+

{% translate "Last Comments" %}

+
+ {% for object in comments|slice:"0:25" %} + {% page_widget "item" object admin=True is_tiny=True %} + {% empty %} +
{% translate "No diffusion to come" %}
+ {% endfor %} +
+
+{% endblock %} diff --git a/aircox/templates/aircox/dashboard/nav.html b/aircox/templates/aircox/dashboard/nav.html index 82ffd71..507730d 100644 --- a/aircox/templates/aircox/dashboard/nav.html +++ b/aircox/templates/aircox/dashboard/nav.html @@ -10,10 +10,13 @@