Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • to/majak
  • b1242/majak
2 results
Show changes
Showing
with 932 additions and 57 deletions
# Generated by Django 4.0.4 on 2022-05-05 10:28
from datetime import date, timedelta
import arrow
from django.db import migrations
from calendar_utils.icalevents import icalevents
from calendar_utils.parser import process_event_list
def update_event_lists(apps, schema):
Calendar = apps.get_model("calendar_utils", "Calendar")
for calendar in Calendar.objects.all():
try:
event_list = icalevents.events(
url=calendar.url,
start=date.today() - timedelta(days=30),
end=date.today() + timedelta(days=60),
)
except ValueError:
print("Could not parse calendar from {}".format(calendar.url))
event_list_hash = str(hash(str(event_list)))
past, future = process_event_list(event_list)
calendar.past_events = past
calendar.future_events = list(reversed(future))
calendar.event_hash = event_list_hash
calendar.last_update = arrow.utcnow().datetime
calendar.save()
class Migration(migrations.Migration):
dependencies = [
("calendar_utils", "0003_remove_calendar_source_calendar_event_hash"),
]
operations = [
migrations.RunPython(update_event_lists, reverse_code=migrations.RunPython.noop)
]
import json
import logging
from datetime import timedelta
from functools import partial
import arrow import arrow
import requests
from django.core.serializers.json import DjangoJSONEncoder from django.core.serializers.json import DjangoJSONEncoder
from django.db import models from django.core.validators import URLValidator, ValidationError
from django.db import models, transaction
from django.utils.html import escape
from django.utils.timezone import now
from .icalevents import icalevents
from .parser import process_event_list
from .tasks import update_calendar_source
from .parser import process_ical logger = logging.getLogger(__name__)
def _convert_arrow_to_datetime(event): def _convert_arrow_to_datetime(event):
event["begin"] = event["begin"].datetime event["start"] = event["start"].datetime
event["end"] = event["end"].datetime event["end"] = event["end"].datetime
return event return event
...@@ -20,36 +31,55 @@ class EventsJSONField(models.JSONField): ...@@ -20,36 +31,55 @@ class EventsJSONField(models.JSONField):
def from_db_value(self, value, expression, connection): def from_db_value(self, value, expression, connection):
value = super().from_db_value(value, expression, connection) value = super().from_db_value(value, expression, connection)
urlValidator = URLValidator()
if value: if value:
for event in value: for event in value:
event["begin"] = arrow.get(event["begin"]).datetime event["start"] = arrow.get(event.get("start")).datetime
event["end"] = arrow.get(event["end"]).datetime event["end"] = arrow.get(event["end"]).datetime
try:
urlValidator(event.get("location"))
event["url"] = event.get("location")
except ValidationError:
pass
return value return value
class Calendar(models.Model): class Calendar(models.Model):
ACTUAL_NUM = 6 CURRENT_NUM = 6
url = models.URLField() url = models.URLField()
source = models.TextField(null=True) event_hash = models.CharField(max_length=256, null=True)
last_update = models.DateTimeField(null=True) last_update = models.DateTimeField(null=True)
past_events = EventsJSONField(encoder=DjangoJSONEncoder, null=True) past_events = EventsJSONField(encoder=DjangoJSONEncoder, null=True)
future_events = EventsJSONField(encoder=DjangoJSONEncoder, null=True) future_events = EventsJSONField(encoder=DjangoJSONEncoder, null=True)
def update_source(self): def current_events(self):
source = requests.get(self.url).text if self.future_events is not None:
if self.source != source: return self.future_events[: self.CURRENT_NUM]
self.source = source else:
past, future = process_ical(source) return []
self.past_events = list(map(_convert_arrow_to_datetime, past))
self.future_events = list( def handle_event_list(self, event_list):
reversed(list(map(_convert_arrow_to_datetime, future))) event_list_hash = str(hash(str(event_list)))
)
if event_list_hash != self.event_hash:
past, future = process_event_list(event_list)
self.past_events = past
self.future_events = future
self.event_hash = event_list_hash
self.last_update = arrow.utcnow().datetime self.last_update = arrow.utcnow().datetime
self.save() self.save()
def actual_events(self): def update_source(self):
return self.future_events[-self.ACTUAL_NUM :] event_list = icalevents.events(
url=self.url,
start=now() - timedelta(days=30),
end=now()
+ timedelta(days=365), # Pull a year ahead due to "popular" demand.
)
self.handle_event_list(event_list)
class CalendarMixin(models.Model): class CalendarMixin(models.Model):
...@@ -59,29 +89,69 @@ class CalendarMixin(models.Model): ...@@ -59,29 +89,69 @@ class CalendarMixin(models.Model):
""" """
calendar_url = models.URLField( calendar_url = models.URLField(
"URL kalendáře ve formátu iCal", blank=True, null=True "URL kalendáře ve formátu iCal",
blank=True,
null=True,
help_text="Kalendář se po uložení stránky aktualizuje na pozadí. U plnějších kalendářů to může trvat i desítky sekund.",
) )
calendar = models.ForeignKey( calendar = models.ForeignKey(
Calendar, null=True, blank=True, on_delete=models.PROTECT Calendar, null=True, blank=True, on_delete=models.SET_NULL
) )
class Meta: class Meta:
abstract = True abstract = True
def get_fullcalendar_data(self) -> str:
calendar_format_events = []
if self.calendar is None:
return []
for event in (
self.calendar.past_events if self.calendar.past_events is not None else []
) + (
self.calendar.future_events
if self.calendar.future_events is not None
else []
):
parsed_event = {
"allDay": event["all_day"],
"start": event["start"].isoformat(),
"end": event["end"].isoformat(),
}
if event["summary"] not in ("", None):
parsed_event["title"] = event["summary"]
if event["url"] not in ("", None):
parsed_event["url"] = event["url"]
if event["location"] not in ("", None):
parsed_event["location"] = event["location"]
if event["description"] not in ("", None):
parsed_event["description"] = event["description"]
calendar_format_events.append(parsed_event)
return escape(json.dumps(calendar_format_events))
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
# create or update related Calendar # create or update related Calendar
if self.calendar_url: if self.calendar_url:
if self.calendar: if not self.calendar or self.calendar.url != self.calendar_url:
if self.calendar.url != self.calendar_url: calendar = Calendar.objects.filter(url=self.calendar_url).first()
self.calendar.url = self.calendar_url if calendar:
self.calendar.save() self.calendar = calendar
else: else:
self.calendar = Calendar.objects.create(url=self.calendar_url) self.calendar = Calendar.objects.create(url=self.calendar_url)
self.calendar.update_source()
transaction.on_commit(
partial(update_calendar_source.delay, self.calendar.id)
)
# delete related Calendar when URL is cleared # delete related Calendar when URL is cleared
if not self.calendar_url and self.calendar: if not self.calendar_url and self.calendar:
self.calendar.delete()
self.calendar = None self.calendar = None
super().save(*args, **kwargs) super().save(*args, **kwargs)
from operator import attrgetter from operator import itemgetter
from typing import TYPE_CHECKING
from zoneinfo import ZoneInfo
import arrow import arrow
import bleach
import nh3
from django.conf import settings from django.conf import settings
from ics import Calendar from django.utils.timezone import is_naive
EVENT_KEYS = ("begin", "end", "all_day", "name", "description", "location") if TYPE_CHECKING:
from .icalevents.icalparser import Event
EVENT_KEYS = ("start", "end", "all_day", "summary", "description", "location", "url")
def parse_ical(source):
"""Parses iCalendar source and returns events as list of dicts."""
cal = Calendar(source)
events = []
for event in sorted(cal.events, key=attrgetter("begin"), reverse=True):
events.append({key: getattr(event, key) for key in EVENT_KEYS})
return events
def split_event_dict_list(event_list: "list[dict]") -> tuple[list[dict], list[dict]]:
def split_events(events):
"""Splits events and returns list of past events and future events.""" """Splits events and returns list of past events and future events."""
now = arrow.utcnow() singularity = arrow.utcnow().shift(hours=-2)
past = [ev for ev in events if ev["begin"] < now]
future = [ev for ev in events if ev["begin"] > now] past = [ev for ev in event_list if ev["end"] < singularity]
future = list(reversed([ev for ev in event_list if ev["end"] > singularity]))
return past, future return past, future
def set_event_duration(event): def set_event_description(event: "Event") -> "Event":
"""Clears even description from unwanted tags."""
description: str = event.description or ""
event.description = bleach.clean(description, tags=["a", "br"], strip=True)
return event
def set_event_duration(event: "Event") -> "Event":
"""Sets duration for event.""" """Sets duration for event."""
if event["all_day"]: if event.all_day:
event["duration"] = "celý den" event.duration = "celý den"
return event return event
delta = event["end"] - event["begin"] delta = event.end - event.start
if delta.days < 1: if delta.days < 1:
begin = event["begin"].to(settings.TIME_ZONE).format("H:mm") begin = arrow.get(event.start).to(settings.TIME_ZONE).format("H:mm")
end = event["end"].to(settings.TIME_ZONE).format("H:mm") end = arrow.get(event.end).to(settings.TIME_ZONE).format("H:mm")
event["duration"] = f"{begin} - {end}" event.duration = f"{begin} - {end}"
else: else:
begin = event["begin"].to(settings.TIME_ZONE).format("H:mm") begin = arrow.get(event.start).to(settings.TIME_ZONE).format("H:mm")
end = event["end"].to(settings.TIME_ZONE).format("H:mm (D.M.)") end = arrow.get(event.end).to(settings.TIME_ZONE).format("H:mm (D.M.)")
event["duration"] = f"{begin} - {end}" event.duration = f"{begin} - {end}"
return event return event
def process_ical(source): def set_event_timezone(event: "Event") -> "Event":
"""Sets default project timezone for event if missing."""
if is_naive(event.start) or is_naive(event.end):
event.start = event.start.replace(tzinfo=ZoneInfo(settings.TIME_ZONE))
event.end = event.end.replace(tzinfo=ZoneInfo(settings.TIME_ZONE))
return event
def process_event(event: "Event") -> dict:
"""Processes single event for use in Majak"""
event = set_event_timezone(event)
event = set_event_duration(event)
event = set_event_description(event)
event.description = nh3.clean(
event.description,
tags={"h1", "h2", "h3", "h4", "h5", "h6", "a", "em", "p", "b", "strong", "br"},
)
# for event in sorted(cal.events, key=attrgetter("start"), reverse=True): TODO check
return {key: getattr(event, key) for key in EVENT_KEYS}
def process_event_list(event_list: "list[Event]") -> tuple[list[dict], list[dict]]:
"""Parses iCalendar source and returns events as list of dicts. Returns """Parses iCalendar source and returns events as list of dicts. Returns
tuple of past and future events. tuple of past and future events.
""" """
events = parse_ical(source) processed_event_list = list(map(process_event, event_list))
events = list(map(set_event_duration, events)) processed_event_list = sorted(
return split_events(events) processed_event_list, key=itemgetter("start"), reverse=True
)
return split_event_dict_list(processed_event_list)
from celery import shared_task
from celery.utils.log import get_task_logger
logger = get_task_logger(__name__)
@shared_task
def update_calendar_source(calendar_id):
from .models import Calendar # noqa circular import
cal = Calendar.objects.get(id=calendar_id)
cal.update_source()
from django import template
register = template.Library()
def event_list(calendar, full_list):
"""
Outputs a list of events, in case the calendar is on calendar page, it will print all future events,
otherwise will print only a fraction
"""
return calendar.future_events if full_list else calendar.current_events
register.filter("event_list", event_list)
from django.apps import AppConfig
class CzechInspirationalConfig(AppConfig):
name = "czech_inspirational"
# Generated by Django 3.1.7 on 2021-03-22 21:37
import django.db.models.deletion
import wagtailmetadata.models
from django.db import migrations, models
import shared.models
class Migration(migrations.Migration):
initial = True
dependencies = [
("wagtailcore", "0060_fix_workflow_unique_constraint"),
("wagtailimages", "0023_add_choose_permissions"),
]
operations = [
migrations.CreateModel(
name="CzechInspirationalHomePage",
fields=[
(
"page_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="wagtailcore.page",
),
),
(
"matomo_id",
models.IntegerField(
blank=True,
null=True,
verbose_name="Matomo ID pro sledování návštěvnosti",
),
),
(
"search_image",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="+",
to="wagtailimages.image",
verbose_name="Search image",
),
),
],
options={
"verbose_name": "Česko inspirativní",
},
bases=(
"wagtailcore.page",
wagtailmetadata.models.WagtailImageMetadataMixin,
models.Model,
),
),
migrations.CreateModel(
name="CzechInspirationalDownloadPage",
fields=[
(
"page_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="wagtailcore.page",
),
),
(
"search_image",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="+",
to="wagtailimages.image",
verbose_name="Search image",
),
),
],
options={
"verbose_name": "Download",
},
bases=(
"wagtailcore.page",
shared.models.SubpageMixin,
wagtailmetadata.models.WagtailImageMetadataMixin,
models.Model,
),
),
migrations.CreateModel(
name="CzechInspirationalChaptersPage",
fields=[
(
"page_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="wagtailcore.page",
),
),
(
"search_image",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="+",
to="wagtailimages.image",
verbose_name="Search image",
),
),
],
options={
"verbose_name": "Přehled kapitol",
},
bases=(
"wagtailcore.page",
shared.models.SubpageMixin,
wagtailmetadata.models.WagtailImageMetadataMixin,
models.Model,
),
),
migrations.CreateModel(
name="CzechInspirationalChapterPage",
fields=[
(
"page_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="wagtailcore.page",
),
),
(
"search_image",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="+",
to="wagtailimages.image",
verbose_name="Search image",
),
),
],
options={
"verbose_name": "Kapitola",
},
bases=(
"wagtailcore.page",
shared.models.SubpageMixin,
wagtailmetadata.models.WagtailImageMetadataMixin,
models.Model,
),
),
]
# Generated by Django 3.1.7 on 2021-03-23 01:03
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("czech_inspirational", "0001_initial"),
]
operations = [
migrations.AddField(
model_name="czechinspirationalchapterpage",
name="number",
field=models.IntegerField(default=0, verbose_name="číslo kapitoly"),
),
]
# Generated by Django 3.1.7 on 2021-03-23 02:12
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("wagtaildocs", "0012_uploadeddocument"),
("czech_inspirational", "0002_czechinspirationalchapterpage_number"),
]
operations = [
migrations.AddField(
model_name="czechinspirationaldownloadpage",
name="book_file",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="+",
to="wagtaildocs.document",
),
),
]
# Generated by Django 3.1.7 on 2021-03-23 02:52
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("wagtaildocs", "0012_uploadeddocument"),
("czech_inspirational", "0003_czechinspirationaldownloadpage_book_file"),
]
operations = [
migrations.AddField(
model_name="czechinspirationalhomepage",
name="buy_book_url",
field=models.URLField(
blank=True, null=True, verbose_name="URL pro nákup knihy"
),
),
migrations.AlterField(
model_name="czechinspirationaldownloadpage",
name="book_file",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="+",
to="wagtaildocs.document",
verbose_name="ebook",
),
),
]
# Generated by Django 3.1.7 on 2021-03-23 04:36
import django.db.models.deletion
import wagtail.fields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("wagtailimages", "0023_add_choose_permissions"),
("czech_inspirational", "0004_auto_20210323_0352"),
]
operations = [
migrations.AddField(
model_name="czechinspirationalchapterpage",
name="author",
field=models.CharField(
blank=True, max_length=250, null=True, verbose_name="autor"
),
),
migrations.AddField(
model_name="czechinspirationalchapterpage",
name="image",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
to="wagtailimages.image",
verbose_name="obrázek",
),
),
migrations.AddField(
model_name="czechinspirationalchapterpage",
name="text",
field=wagtail.fields.RichTextField(blank=True, verbose_name="text"),
),
]
# Generated by Django 3.1.7 on 2021-03-23 05:31
import wagtail.fields
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("czech_inspirational", "0005_auto_20210323_0536"),
]
operations = [
migrations.AddField(
model_name="czechinspirationalchapterpage",
name="extra_text",
field=wagtail.fields.RichTextField(
blank=True, verbose_name="extra modrý blok"
),
),
]
# Generated by Django 4.0.3 on 2022-04-27 13:27
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("czech_inspirational", "0006_czechinspirationalchapterpage_extra_text"),
]
operations = [
migrations.AddField(
model_name="czechinspirationalhomepage",
name="title_suffix",
field=models.CharField(
blank=True,
help_text="Umožňuje přidat příponu k základnímu titulku stránky. Pokud je např. titulek stránky pojmenovaný 'Kontakt' a do přípony vyplníte 'MS Pardubice | Piráti', výsledný titulek bude 'Kontakt | MS Pardubice | Piráti'. Pokud příponu nevyplníte, použije se název webu.",
max_length=100,
null=True,
verbose_name="Přípona titulku stránky",
),
),
]
# Generated by Django 5.0.6 on 2024-06-13 10:02
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("czech_inspirational", "0007_czechinspirationalhomepage_title_suffix"),
]
operations = [
migrations.AlterField(
model_name="czechinspirationalhomepage",
name="title_suffix",
field=models.CharField(
blank=True,
help_text="Umožňuje přidat příponu k základnímu titulku stránky. Pokud je např. titulek stránky pojmenovaný 'Kontakt' a do přípony vyplníte 'MS Pardubice', výsledný titulek bude 'Kontakt | Piráti MS Pardubice'. Pokud příponu nevyplníte, použije se název domovské stránky a text 'Piráti', např. 'Kontakt | Piráti Pardubice'.",
max_length=100,
null=True,
verbose_name="Přípona titulku stránky",
),
),
]
# Generated by Django 5.0.6 on 2024-07-02 06:13
from django.db import migrations, models
def prefill_title_suffix(apps, schema_editor):
CzechInspirationalHomePage = apps.get_model(
"czech_inspirational", "CzechInspirationalHomePage"
)
for page in CzechInspirationalHomePage.objects.all():
page.meta_title_suffix = page.title_suffix
page.save()
class Migration(migrations.Migration):
dependencies = [
("czech_inspirational", "0008_alter_czechinspirationalhomepage_title_suffix"),
]
operations = [
migrations.AddField(
model_name="czechinspirationalhomepage",
name="meta_title_suffix",
field=models.CharField(
blank=True,
help_text='Umožňuje přidat příponu k titulku stránky běžně zobrazovanému na záložce s touto stránkou. Pokud vyplníš například "Piráti Pardubicko", záložka s kontakty bude nadepsaná "Kontakty | Piráti Pardubicko".',
max_length=100,
null=True,
verbose_name="Přípona meta titulku stránky",
),
),
migrations.AlterField(
model_name="czechinspirationalhomepage",
name="title_suffix",
field=models.CharField(
blank=True,
help_text='Umožňuje přidat příponu k názvu stránky. Pokud vyplníš například "Pardubicko", v levém horním rohu bude logo Pirátské strany a text "| Pardubicko".',
max_length=100,
null=True,
verbose_name="Přípona názvu stránky",
),
),
migrations.RunPython(prefill_title_suffix),
]
from django.conf import settings
from django.core.exceptions import ValidationError
from django.core.validators import validate_email
from django.db import models
from django.utils.translation import gettext_lazy
from wagtail.admin.panels import CommentPanel, FieldPanel, HelpPanel, MultiFieldPanel
from wagtail.fields import RichTextField
from wagtail.models import Page
from wagtailmetadata.models import MetadataPageMixin
from shared.const import RICH_TEXT_DEFAULT_FEATURES
from shared.models import (
ExtendedMetadataHomePageMixin,
ExtendedMetadataPageMixin,
SubpageMixin,
)
from shared.utils import subscribe_to_newsletter
from tuning import admin_help
class CzechInspirationalHomePage(
Page, ExtendedMetadataHomePageMixin, MetadataPageMixin
):
### FIELDS
buy_book_url = models.URLField("URL pro nákup knihy", blank=True, null=True)
# settings
matomo_id = models.IntegerField(
"Matomo ID pro sledování návštěvnosti", blank=True, null=True
)
### PANELS
content_panels = Page.content_panels + [
FieldPanel("buy_book_url"),
]
promote_panels = [
MultiFieldPanel(
[
FieldPanel("seo_title"),
FieldPanel("search_description"),
FieldPanel("search_image"),
HelpPanel(admin_help.build(admin_help.IMPORTANT_TITLE)),
],
gettext_lazy("Common page configuration"),
),
]
settings_panels = [
MultiFieldPanel(
[
FieldPanel("matomo_id"),
FieldPanel("title_suffix"),
FieldPanel("meta_title_suffix"),
],
"nastavení webu",
),
CommentPanel(),
]
### RELATIONS
subpage_types = [
"czech_inspirational.CzechInspirationalChaptersPage",
"czech_inspirational.CzechInspirationalDownloadPage",
]
### OTHERS
class Meta:
verbose_name = "Česko inspirativní"
@property
def root_page(self):
return self
@property
def chapters_page_url(self):
try:
return (
self.get_descendants()
.type(CzechInspirationalChaptersPage)
.live()
.get()
.get_url()
)
except Page.DoesNotExist:
return "#"
@property
def download_page_url(self):
try:
return (
self.get_descendants()
.type(CzechInspirationalDownloadPage)
.live()
.get()
.get_url()
)
except Page.DoesNotExist:
return "#"
def get_context(self, request):
context = super().get_context(request)
context["chapters"] = (
self.get_descendants()
.type(CzechInspirationalChapterPage)
.live()
.specific()
.order_by("czechinspirationalchapterpage__number")
)
return context
class CzechInspirationalChaptersPage(
Page, ExtendedMetadataPageMixin, SubpageMixin, MetadataPageMixin
):
### FIELDS
### PANELS
promote_panels = [
MultiFieldPanel(
[
FieldPanel("slug"),
FieldPanel("seo_title"),
FieldPanel("search_description"),
FieldPanel("search_image"),
HelpPanel(
admin_help.build(
admin_help.NO_SEO_TITLE, admin_help.NO_SEARCH_IMAGE
)
),
],
gettext_lazy("Common page configuration"),
),
]
settings_panels = [CommentPanel()]
### RELATIONS
parent_page_types = ["czech_inspirational.CzechInspirationalHomePage"]
subpage_types = ["czech_inspirational.CzechInspirationalChapterPage"]
### OTHERS
class Meta:
verbose_name = "Přehled kapitol"
def get_context(self, request):
context = super().get_context(request)
context["chapters"] = (
self.get_children()
.live()
.specific()
.order_by("czechinspirationalchapterpage__number")
)
return context
class CzechInspirationalChapterPage(
Page, ExtendedMetadataPageMixin, SubpageMixin, MetadataPageMixin
):
### FIELDS
number = models.IntegerField("číslo kapitoly", default=0)
text = RichTextField("text", blank=True, features=RICH_TEXT_DEFAULT_FEATURES)
extra_text = RichTextField(
"extra modrý blok", blank=True, features=RICH_TEXT_DEFAULT_FEATURES
)
author = models.CharField("autor", max_length=250, blank=True, null=True)
image = models.ForeignKey(
"wagtailimages.Image",
on_delete=models.PROTECT,
blank=True,
null=True,
verbose_name="obrázek",
)
### PANELS
content_panels = Page.content_panels + [
FieldPanel("number"),
FieldPanel("author"),
FieldPanel("image"),
FieldPanel("text"),
FieldPanel("extra_text"),
]
promote_panels = [
MultiFieldPanel(
[
FieldPanel("slug"),
FieldPanel("seo_title"),
FieldPanel("search_description"),
FieldPanel("search_image"),
HelpPanel(
admin_help.build(
admin_help.NO_SEO_TITLE, admin_help.NO_SEARCH_IMAGE
)
),
],
gettext_lazy("Common page configuration"),
),
]
settings_panels = [CommentPanel()]
### RELATIONS
parent_page_types = ["czech_inspirational.CzechInspirationalChaptersPage"]
subpage_types = []
### OTHERS
class Meta:
verbose_name = "Kapitola"
def get_context(self, request):
context = super().get_context(request)
context["chapters"] = (
self.get_siblings()
.live()
.specific()
.order_by("czechinspirationalchapterpage__number")
)
return context
class CzechInspirationalDownloadPage(
Page, ExtendedMetadataPageMixin, SubpageMixin, MetadataPageMixin
):
### FIELDS
book_file = models.ForeignKey(
"wagtaildocs.Document",
verbose_name="ebook",
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="+",
)
### PANELS
content_panels = Page.content_panels + [
FieldPanel("book_file"),
]
promote_panels = [
MultiFieldPanel(
[
FieldPanel("slug"),
FieldPanel("seo_title"),
FieldPanel("search_description"),
FieldPanel("search_image"),
HelpPanel(
admin_help.build(
admin_help.NO_SEO_TITLE, admin_help.NO_SEARCH_IMAGE
)
),
],
gettext_lazy("Common page configuration"),
),
]
settings_panels = [CommentPanel()]
### RELATIONS
parent_page_types = ["czech_inspirational.CzechInspirationalHomePage"]
subpage_types = []
### OTHERS
class Meta:
verbose_name = "Download"
def get_context(self, request):
context = super().get_context(request)
if "stahnout" in request.GET:
context["show"] = "download"
elif "diky" in request.GET:
context["show"] = "thanks"
else:
context["show"] = "info"
if "email" in request.POST:
context["show"] = "download"
if "subscribe" in request.POST:
email = request.POST.get("email", "")
try:
validate_email(email)
except ValidationError:
pass
else:
subscribe_to_newsletter(
email, settings.CZECH_INSPIRATIONAL_NEWSLETTER_CID
)
return context
.tha_heroimage{max-width:683px}.tha_underground{overflow:hidden;height:415px}.tha_underground.tha_underground_higher{overflow:hidden;height:510px}#tha_bottomcenter{max-width:556px}.tha_underground .tha_heroimage{box-shadow:0 24px 21px 3px rgba(0,0,0,.75)}.tha_underground .tha_bottomcenter .tha_heroimage{width:556px}#tha_leftwing{position:absolute;top:51px;left:-264px;width:278px;z-index:-1;transition:all ease-in .5s}#tha_rightwing{position:absolute;top:51px;right:-264px;width:278px;z-index:-1;transition:all ease-in .5s}@media only screen and (max-width:670px){#tha_bottomcenter,.tha_underground .tha_heroimage{max-width:302px}#tha_leftwing{top:51px;left:-69px;width:139px}#tha_rightwing{top:51px;right:-69px;width:139px}.tha_underground{overflow:hidden;height:230px!important}}.tha_logo_illustrated{width:140px}.tha_videocont_activated{padding:0 6.92%}@media only screen and (max-width:670px){.tha_videocont_activated{padding:0}}@media only screen and (max-width:1440px){#chaptertable{max-width:768px}}.tha_animations_container{position:absolute;top:0;left:50%;transform:translateX(-50%);width:100%;height:100%;min-width:1920px;z-index:-1}#tha_leftfloat1{position:absolute;top:312px;left:0;width:561px;transition:all ease-in .5s}#tha_leftfloat2{position:absolute;top:954px;left:116px;width:447px;z-index:-1;transition:all ease-in .5s}#tha_leftfloat_arrow{position:absolute;top:1697px;left:469px;width:109px;z-index:-1;transition:all ease-in .5s}#tha_rightfloat1{position:absolute;top:640px;right:0;width:561px}#tha_rightfloat2{position:absolute;top:1334px;right:73px;width:310px;z-index:-1}#tha_rightfloat_arrow1{position:absolute;top:204px;right:500px;width:107px}#tha_rightfloat_arrow2{position:absolute;top:345px;right:490px;width:48px}#tha_leftparalax{position:absolute;top:56px;left:0;width:359px;transition:all ease-in .5s}#tha_rightparalax{position:absolute;top:472px;right:0;width:359px;transition:all ease-in .5s}#tha_bottom_arrow{position:absolute;bottom:150px;left:calc(50% + 103px);width:244px;transition:all ease-in .5s}@media only screen and (max-width:1440px){#tha_animations_container2{min-width:1618px}}@media only screen and (max-width:768px){.tha_animations_container{position:static!important;transform:none;min-width:0;width:100%;max-width:100%}}.tha_underground.tha_underground_higher #tha_leftfloat1{top:0}.tha_underground.tha_underground_higher #tha_rightfloat1{top:185px}.tha_chapterhead picture{width:470px;max-width:90vw}#tha_stickysharebox{position:-webkit-sticky;position:sticky;top:32px}#tha_carousel_cont{max-width:1224px}.VueCarousel-navigation{text-align:center}.VueCarousel-dot-button{border-radius:0!important;width:24px!important;height:4px!important;position:relative;top:-4px}.VueCarousel-slide{padding:0 12px 16px 12px}.VueCarousel-navigation-prev{position:static!important;transform:translateY(-100%) translateX(-500%)!important}.VueCarousel-navigation-next{position:static!important;transform:translateY(-100%) translateX(500%)!important}@media only screen and (max-width:1024px){.VueCarousel-pagination{display:none}.VueCarousel-navigation-prev{position:absolute!important;transform:translateY(-50%) translateX(-50%)!important;font-size:40px}.VueCarousel-navigation-next{position:absolute!important;transform:translateY(-50%) translateX(50%)!important;font-size:40px}}.tha_cardnumber{position:relative;top:2px;left:-2px}.label{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)}.VueCarousel-slide{background:0 0;color:inherit}