Compare commits

..

2 Commits

10 changed files with 209 additions and 401 deletions
+8 -32
View File
@@ -9,12 +9,16 @@ from grapple.models import (
) )
from wagtail.admin.panels import FieldPanel from wagtail.admin.panels import FieldPanel
from wagtail.fields import RichTextField from wagtail.fields import RichTextField
from wagtail.models import Page from wagtail.models import Page, PageManager
from wagtail.search import index from wagtail.search import index
from wagtail_headless_preview.models import HeadlessMixin from wagtail_headless_preview.models import HeadlessMixin
from dnscms.fields import CommonStreamField from dnscms.fields import CommonStreamField
from dnscms.wordpress.models import WPImportedPageMixin from dnscms.wordpress.models import DeferWPFieldsManagerMixin, WPImportedPageMixin
class AssociationPageManager(DeferWPFieldsManagerMixin, PageManager):
pass
@register_singular_query_field("associationIndex") @register_singular_query_field("associationIndex")
@@ -48,6 +52,8 @@ class AssociationPage(HeadlessMixin, WPImportedPageMixin, Page):
parent_page_types = ["associations.AssociationIndex"] parent_page_types = ["associations.AssociationIndex"]
show_in_menus = False show_in_menus = False
objects = AssociationPageManager()
class AssociationType(models.TextChoices): class AssociationType(models.TextChoices):
FORENING = "forening", _("Association") FORENING = "forening", _("Association")
UTVALG = "utvalg", _("Committee") UTVALG = "utvalg", _("Committee")
@@ -97,33 +103,3 @@ class AssociationPage(HeadlessMixin, WPImportedPageMixin, Page):
class Meta: class Meta:
verbose_name = _("association") verbose_name = _("association")
verbose_name_plural = _("associations") verbose_name_plural = _("associations")
def import_wordpress_data(self, data):
import html
# Wagtail page model fields
self.title = html.unescape(data["title"])
self.slug = data["slug"]
self.first_published_at = data["first_published_at"]
self.last_published_at = data["last_published_at"]
self.latest_revision_created_at = data["latest_revision_created_at"]
self.search_description = data["search_description"]
# debug fields
self.wp_post_id = data["wp_post_id"]
self.wp_post_type = data["wp_post_type"]
self.wp_link = data["wp_link"]
self.wp_raw_content = data["wp_raw_content"]
self.wp_block_json = data["wp_block_json"]
self.wp_processed_content = data["wp_processed_content"]
self.wp_normalized_styles = data["wp_normalized_styles"]
self.wp_post_meta = data["wp_post_meta"]
# own model fields
self.body = data["body"] or ""
meta = data["wp_post_meta"]
self.association_type = meta.get("neuf_associations_type").lower()
self.website_url = meta.get("neuf_associations_homepage") or ""
self.excerpt = meta.get("excerpt_encoded") or "TODO"
-72
View File
@@ -12,7 +12,6 @@ https://docs.djangoproject.com/en/4.2/ref/settings/
# Build paths inside the project like this: os.path.join(BASE_DIR, ...) # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
import os import os
import re
PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
BASE_DIR = os.path.dirname(PROJECT_DIR) BASE_DIR = os.path.dirname(PROJECT_DIR)
@@ -228,74 +227,3 @@ GRAPPLE = {
"PAGE_SIZE": 100, "PAGE_SIZE": 100,
"MAX_PAGE_SIZE": 5000, "MAX_PAGE_SIZE": 5000,
} }
# Wgtail WordPress import
WAGTAIL_WORDPRESS_IMPORTER_SOURCE_DOMAIN = "https://studentersamfundet.no/"
WAGTAIL_WORDPRESS_IMPORTER_CONVERT_HTML_TAGS_TO_BLOCKS = {
# "h1": "wagtail_wordpress_import.block_builder_defaults.build_heading_block",
"table": "wagtail_wordpress_import.block_builder_defaults.build_table_block",
# "iframe": "wagtail_wordpress_import.block_builder_defaults.build_iframe_block",
# "form": "wagtail_wordpress_import.block_builder_defaults.build_form_block",
# "img": "wagtail_wordpress_import.block_builder_defaults.build_image_block",
# "blockquote": "wagtail_wordpress_import.block_builder_defaults.build_block_quote_block",
}
WAGTAIL_WORDPRESS_IMPORTER_FALLBACK_BLOCK = (
"dnscms.wordpress.block_builder.build_richtext_block_content"
)
WORDPRESS_IMPORT_HOOKS_ITEMS_TO_CACHE = {
"attachment": {
"DATA_TAG": "thumbnail_id",
"FUNCTION": "dnscms.wordpress.import_hooks.header_image_processor",
}
}
# WORDPRESS_IMPORT_HOOKS_TAGS_TO_CACHE = {
# "wp:term": {
# "DATA_TAG": "category",
# "FUNCTION": "dnscms.wordpress.import_hooks.categories_processor",
# }
# }
WAGTAIL_WORDPRESS_IMPORTER_PROMOTE_CHILD_TAGS = {
"TAGS_TO_PROMOTE": [],
"PARENTS_TO_REMOVE": ["p", "div", "span"],
}
WAGTAIL_WORDPRESS_IMPORT_PREFILTERS = [
{
"FUNCTION": "wagtail_wordpress_import.prefilters.linebreaks_wp",
},
{
"FUNCTION": "wagtail_wordpress_import.prefilters.transform_shortcodes",
},
{
"FUNCTION": "wagtail_wordpress_import.prefilters.transform_inline_styles",
"OPTIONS": {
"TRANSFORM_STYLES_MAPPING": [
(
re.compile(r"font-style:italic;font-weight:bold;", re.IGNORECASE),
"wagtail_wordpress_import.prefilters.transform_styles_defaults.transform_style_bold_italic",
),
(
re.compile(r"font-weight:bold;", re.IGNORECASE),
"wagtail_wordpress_import.prefilters.transform_styles_defaults.transform_style_bold",
),
(
re.compile(r"font-style:italic;", re.IGNORECASE),
"wagtail_wordpress_import.prefilters.transform_styles_defaults.transform_style_italic",
),
# (
# re.compile(
# r"text-align:center;",
# re.IGNORECASE,
# ),
# transform_style_center,
# ),
# (re.compile(r"text-align:left;", re.IGNORECASE), transform_style_left),
# (re.compile(r"text-align:right;", re.IGNORECASE), transform_style_right),
# (re.compile(r"float:left;", re.IGNORECASE), transform_float_left),
# (re.compile(r"float:right;", re.IGNORECASE), transform_float_right),
],
},
},
{
"FUNCTION": "wagtail_wordpress_import.prefilters.bleach_clean",
},
]
-24
View File
@@ -1,24 +0,0 @@
from django.conf import settings
from wagtail_wordpress_import.block_builder_defaults import (
document_linker,
image_linker,
import_string,
)
def build_richtext_block_content(html, blocks):
"""
image_linker is called to link up and retrive the remote image
document_linker is called to link up and retrive the remote documents
filters are called to replace inline shortcodes
"""
html = image_linker(html)
html = document_linker(html)
for inline_shortcode_handler in getattr(
settings, "WAGTAIL_WORDPRESS_IMPORTER_INLINE_SHORTCODE_HANDLERS", []
):
function = import_string(inline_shortcode_handler).construct_html_tag
html = function(html)
blocks.append({"type": "paragraph", "value": html})
html = ""
return html
-51
View File
@@ -1,51 +0,0 @@
from wagtail_wordpress_import.block_builder_defaults import get_or_save_image
def header_image_processor(imported_pages, data_tag, items_cache):
"""
imported_pages:
Is a specific() page model queryset of all imported pages.
data_tag:
Is the value of the `DATA_TAG` key from the configuration above.
items_cache:
Is a list of dictionaries, one for each item in the XML file.
"""
# See note above about leading _ and : characters in the XML value
lookup = f"wp_post_meta__{data_tag}"
for attachment in items_cache:
# The id of the cached item used in the filter
thumbnail_id = attachment.get("wp:post_id")
# Filter the imported_pages for only pages that include the
# matching thumbnail_id in the wp_post_meta field
pages = imported_pages.filter(**{lookup: thumbnail_id})
if pages.exists():
# guid is the url of the image to fetch, the get_or_save_image()
# function will fetch the image if it doesn't exist
image_url = attachment.get("guid")
# fix cases where the /wp prefix is missing from the image url
if image_url.startswith("https://studentersamfundet.no/wp-content/uploads/"):
image_url = image_url.replace(
"https://studentersamfundet.no/wp-content/uploads/",
"https://studentersamfundet.no/wp/wp-content/uploads/",
)
try:
image = get_or_save_image(image_url)
except Exception as e:
print("Error with image", image_url, "associated with pages:", pages)
print(e)
continue
print("Attaching header images to pages:", pages)
try:
pages.update(featured_image=image)
except Exception:
pass
try:
pages.update(logo=image)
except Exception:
pass
+73 -6
View File
@@ -1,17 +1,84 @@
from django.core.exceptions import FieldDoesNotExist
from django.db import models from django.db import models
from wagtail.models import Page from wagtail.models import Page
# Field names declared by WPImportedPageMixin. Concrete models that mix it in
# get a manager that .defer()s these so they're never loaded by default — the
# columns stay in the database (no migration), and any code path that
# explicitly reads them still works via Django's lazy-load.
WP_IMPORT_FIELDS = (
"wp_post_id",
"wp_post_type",
"wp_link",
"wp_raw_content",
"wp_processed_content",
"wp_block_json",
"wp_normalized_styles",
"wp_post_meta",
)
# https://github.com/wagtail/wagtail-wordpress-import/blob/main/wagtail_wordpress_import/models.py # https://github.com/wagtail/wagtail-wordpress-import/blob/main/wagtail_wordpress_import/models.py
# DJ001 (null=True on string fields) is suppressed: the schema mirrors the
# upstream mixin and changing nullability would force a migration we avoid.
class WPImportedPageMixin(Page): class WPImportedPageMixin(Page):
wp_post_id = models.IntegerField(blank=True, null=True) wp_post_id = models.IntegerField(blank=True, null=True)
wp_post_type = models.CharField(max_length=255, blank=True, null=True) wp_post_type = models.CharField(max_length=255, blank=True, null=True) # noqa: DJ001
wp_link = models.TextField(blank=True, null=True) wp_link = models.TextField(blank=True, null=True) # noqa: DJ001
wp_raw_content = models.TextField(blank=True, null=True) wp_raw_content = models.TextField(blank=True, null=True) # noqa: DJ001
wp_processed_content = models.TextField(blank=True, null=True) wp_processed_content = models.TextField(blank=True, null=True) # noqa: DJ001
wp_block_json = models.TextField(blank=True, null=True) wp_block_json = models.TextField(blank=True, null=True) # noqa: DJ001
wp_normalized_styles = models.TextField(blank=True, null=True) wp_normalized_styles = models.TextField(blank=True, null=True) # noqa: DJ001
wp_post_meta = models.JSONField(blank=True, null=True) wp_post_meta = models.JSONField(blank=True, null=True)
class Meta: class Meta:
abstract = True abstract = True
class DeferWPFieldsManagerMixin:
"""
Manager mixin that always .defer()s the wp_* import columns, so they are
never SELECTed by default queries. The columns remain in the database;
accessing one on an instance still works (Django lazy-loads it).
"""
def get_queryset(self):
return super().get_queryset().defer(*WP_IMPORT_FIELDS)
def _resolve_related_model(model, lookup_path):
"""Walk a ``foo__bar`` lookup path and return the final related model, or None."""
current = model
for part in lookup_path.split("__"):
try:
field = current._meta.get_field(part)
except FieldDoesNotExist:
return None
current = getattr(field, "related_model", None)
if current is None:
return None
return current
class WPAwareQuerySet(models.QuerySet):
"""
QuerySet whose ``select_related()`` auto-defers wp_* columns when a join
targets a WPImportedPageMixin model. Without this, ``select_related``
builds a JOIN that ignores the related model's manager and SELECTs every
column including the wp_* blobs. Apply via ``objects = WPAwareManager()``
on any model that has a ForeignKey into a WPImported page.
"""
def select_related(self, *fields):
qs = super().select_related(*fields)
defers = [
f"{path}__{name}"
for path in fields
if (related := _resolve_related_model(qs.model, path)) is not None
and issubclass(related, WPImportedPageMixin)
for name in WP_IMPORT_FIELDS
]
return qs.defer(*defers) if defers else qs
WPAwareManager = models.Manager.from_queryset(WPAwareQuerySet)
@@ -0,0 +1,82 @@
# Generated by Django 6.0.5 on 2026-05-22 23:27
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('associations', '0026_alter_association_options'),
('events', '0054_alter_eventpage_options'),
('images', '0005_customimage_description'),
]
operations = [
migrations.AlterModelOptions(
name='eventcategory',
options={'ordering': ['name'], 'verbose_name': 'event category', 'verbose_name_plural': 'event categories'},
),
migrations.AlterModelOptions(
name='eventoccurrence',
options={'verbose_name': 'occurrence', 'verbose_name_plural': 'occurrences'},
),
migrations.AlterModelOptions(
name='eventorganizer',
options={'ordering': ['name'], 'verbose_name': 'event organizer', 'verbose_name_plural': 'event organizers'},
),
migrations.AlterModelOptions(
name='eventorganizerlink',
options={'verbose_name': 'organizer', 'verbose_name_plural': 'organizers'},
),
migrations.AlterField(
model_name='eventcategory',
name='pig',
field=models.CharField(blank=True, choices=[('', 'None'), ('logo', 'Logogrisen'), ('music', 'Musikergrisen'), ('drink', 'Drikkegrisen'), ('dance', 'Dansegrisen'), ('point', 'Pekegrisen'), ('student', 'Studentgrisen'), ('listen', 'Lyttegrisen'), ('guard', 'Vaktgrisen'), ('key', 'Nøkkelgrisen'), ('chill', 'Liggegrisen'), ('peek', 'Tittegrisen')], default='', help_text='Default pig for events of this kind.', max_length=32),
),
migrations.AlterField(
model_name='eventcategory',
name='show_in_filters',
field=models.BooleanField(default=False, help_text='Should this category be available as a filter in the event programme?'),
),
migrations.AlterField(
model_name='eventoccurrence',
name='venue_custom',
field=models.CharField(blank=True, help_text='Use this <em>if none of the venues that can be selected on the left</em> fit. E.g. <em>Frederikkeplassen</em> or <em>Sirkusteltet</em>.', max_length=128),
),
migrations.AlterField(
model_name='eventorganizer',
name='association',
field=models.ForeignKey(blank=True, help_text='If a DNS association or committee is behind it, choose it here.', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='organizers', to='associations.associationpage'),
),
migrations.AlterField(
model_name='eventorganizer',
name='external_url',
field=models.URLField(blank=True, help_text="Link to the external organizer's website", max_length=512),
),
migrations.AlterField(
model_name='eventpage',
name='facebook_url',
field=models.URLField(blank=True, help_text='Direct link to the event on Facebook', max_length=1024),
),
migrations.AlterField(
model_name='eventpage',
name='featured_image',
field=models.ForeignKey(blank=True, help_text="Choose an image for use in the programme and other surfaces. Should be a photo or an illustration without too much text don't reuse a Facebook cover uncritically!", null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='images.customimage'),
),
migrations.AlterField(
model_name='eventpage',
name='pig',
field=models.CharField(blank=True, choices=[('', 'None'), ('automatic', 'Automatic'), ('logo', 'Logogrisen'), ('music', 'Musikergrisen'), ('drink', 'Drikkegrisen'), ('dance', 'Dansegrisen'), ('point', 'Pekegrisen'), ('student', 'Studentgrisen'), ('listen', 'Lyttegrisen'), ('guard', 'Vaktgrisen'), ('key', 'Nøkkelgrisen'), ('chill', 'Liggegrisen'), ('peek', 'Tittegrisen')], default='automatic', help_text="The pig that hangs out on the event page. Automatic causes one to be chosen based on the event's category.", max_length=32),
),
migrations.AlterField(
model_name='eventpage',
name='subtitle',
field=models.CharField(blank=True, help_text='A short text that appears right below the title. Feel free to leave it empty if you fit most of it in the main title.', max_length=128),
),
migrations.AlterField(
model_name='eventpage',
name='ticket_url',
field=models.URLField(blank=True, help_text='Direct link to ticket purchase, e.g. TicketCo, Billetto or Ticketmaster', max_length=1024),
),
]
+14 -124
View File
@@ -35,7 +35,11 @@ from wagtail_headless_preview.models import HeadlessMixin
from associations.widgets import AssociationChooserWidget from associations.widgets import AssociationChooserWidget
from dnscms.fields import CommonStreamField from dnscms.fields import CommonStreamField
from dnscms.options import ALL_PIGS from dnscms.options import ALL_PIGS
from dnscms.wordpress.models import WPImportedPageMixin from dnscms.wordpress.models import (
DeferWPFieldsManagerMixin,
WPAwareManager,
WPImportedPageMixin,
)
from venues.models import VenuePage from venues.models import VenuePage
@@ -163,6 +167,8 @@ class EventOrganizerLink(Orderable):
@register_snippet @register_snippet
@register_query_field("eventOrganizer", "eventOrganizers") @register_query_field("eventOrganizer", "eventOrganizers")
class EventOrganizer(ClusterableModel): class EventOrganizer(ClusterableModel):
objects = WPAwareManager()
name = models.CharField( name = models.CharField(
max_length=100, max_length=100,
null=False, null=False,
@@ -235,7 +241,11 @@ class EventPageQuerySet(PageQuerySet):
) )
EventPageManager = PageManager.from_queryset(EventPageQuerySet) class EventPageManager(
DeferWPFieldsManagerMixin,
PageManager.from_queryset(EventPageQuerySet),
):
pass
class EventPage(HeadlessMixin, WPImportedPageMixin, Page): class EventPage(HeadlessMixin, WPImportedPageMixin, Page):
@@ -434,130 +444,10 @@ class EventPage(HeadlessMixin, WPImportedPageMixin, Page):
self.price_student = "" self.price_student = ""
self.price_member = "" self.price_member = ""
def import_wordpress_data(self, data):
import datetime
import html
from zoneinfo import ZoneInfo
from django.core.validators import URLValidator
validate_url = URLValidator(schemes=["http", "https"])
def fix_url(url):
if not url:
return None
url = url.strip()
try:
validate_url(url)
except Exception:
print(f"Bogus URL for {self.wp_post_id}: {url}")
return None
return url
# Wagtail page model fields
self.title = html.unescape(data["title"])
self.slug = data["slug"]
self.first_published_at = data["first_published_at"]
self.last_published_at = data["last_published_at"]
self.latest_revision_created_at = data["latest_revision_created_at"]
self.search_description = data["search_description"]
# debug fields
self.wp_post_id = data["wp_post_id"]
self.wp_post_type = data["wp_post_type"]
self.wp_link = data["wp_link"]
self.wp_raw_content = data["wp_raw_content"]
self.wp_block_json = data["wp_block_json"]
self.wp_processed_content = data["wp_processed_content"]
self.wp_normalized_styles = data["wp_normalized_styles"]
self.wp_post_meta = data["wp_post_meta"]
# own model fields
self.body = data["body"] or ""
# categories (organizers and event types)
wp_categories = data["wp_categories"]
# organizers
organizer_cats = [x for x in wp_categories if x["domain"] == "event_organizer"]
organizers = []
for x in organizer_cats:
try:
organizer = EventOrganizer.objects.get(slug=x["nicename"])
except EventOrganizer.DoesNotExist:
organizer = EventOrganizer.objects.create(name=x["name"], slug=x["nicename"])
organizers.append(organizer)
self.organizer_links.set(
[EventOrganizerLink(event=self, organizer=organizer) for organizer in organizers]
)
## event types
# type_cats = [x for x in wp_categories if x["domain"] == "event_type"]
# event_categories = []
# for x in type_cats:
# try:
# event_category = EventCategory.objects.get(slug=x["nicename"])
# except EventCategory.DoesNotExist:
# event_category = EventCategory.objects.create(
# name=x["name"], slug=x["nicename"], show_in_filters=False
# )
# event_categories.append(event_category)
# self.categories.set(event_categories)
meta = data["wp_post_meta"]
start_ts = meta.get("neuf_events_starttime") or 1337
end_ts = meta.get("neuf_events_endtime")
tz = ZoneInfo("Europe/Oslo")
start = start_ts and datetime.datetime.fromtimestamp(start_ts, datetime.UTC).replace(
tzinfo=tz
)
end = end_ts and datetime.datetime.fromtimestamp(end_ts, datetime.UTC).replace(tzinfo=tz)
venue_id = meta.get("neuf_events_venue_id")
venue_custom = meta.get("neuf_events_venue")
venue = None
if venue_id:
venue = VenuePage.objects.get(wp_post_id=venue_id)
venue_custom = ""
else:
venue_custom = venue_custom or ""
occurrence = EventOccurrence(
event=self, start=start, end=end, venue=venue, venue_custom=venue_custom
)
self.occurrences.set([occurrence])
self.ticket_url = fix_url(meta.get("neuf_events_bs_url")) or ""
self.facebook_url = fix_url(meta.get("neuf_events_fb_url")) or ""
def parse_price(price):
if price is None:
return ""
if type(price) is int:
return price
p = price.strip()
if p == "":
return ""
try:
return int(p)
except ValueError:
pass
free = ["gratis", "free", "gratis/free", "free/gratis"]
if p.lower() in free:
return 0
return price
price_regular = parse_price(meta.get("neuf_events_price_regular"))
price_member = parse_price(meta.get("neuf_events_price_member"))
if not price_regular and not price_member:
self.free = True
else:
self.price_regular = parse_price(meta.get("neuf_events_price_regular"))
self.price_member = parse_price(meta.get("neuf_events_price_member"))
class EventOccurrence(Orderable): class EventOccurrence(Orderable):
objects = WPAwareManager()
event = ParentalKey(EventPage, on_delete=models.CASCADE, related_name="occurrences") event = ParentalKey(EventPage, on_delete=models.CASCADE, related_name="occurrences")
start = models.DateTimeField() start = models.DateTimeField()
end = models.DateTimeField(null=True, blank=True) end = models.DateTimeField(null=True, blank=True)
+8 -51
View File
@@ -4,12 +4,16 @@ from grapple.helpers import register_singular_query_field
from grapple.models import GraphQLImage, GraphQLRichText, GraphQLStreamfield, GraphQLString from grapple.models import GraphQLImage, GraphQLRichText, GraphQLStreamfield, GraphQLString
from wagtail.admin.panels import FieldPanel from wagtail.admin.panels import FieldPanel
from wagtail.fields import RichTextField from wagtail.fields import RichTextField
from wagtail.models import Page from wagtail.models import Page, PageManager
from wagtail.search import index from wagtail.search import index
from wagtail_headless_preview.models import HeadlessMixin from wagtail_headless_preview.models import HeadlessMixin
from dnscms.fields import CommonStreamField from dnscms.fields import CommonStreamField
from dnscms.wordpress.models import WPImportedPageMixin from dnscms.wordpress.models import DeferWPFieldsManagerMixin, WPImportedPageMixin
class NewsPageManager(DeferWPFieldsManagerMixin, PageManager):
pass
@register_singular_query_field("newsIndex") @register_singular_query_field("newsIndex")
@@ -39,6 +43,8 @@ class NewsPage(HeadlessMixin, WPImportedPageMixin, Page):
parent_page_types = ["news.NewsIndex"] parent_page_types = ["news.NewsIndex"]
show_in_menus = False show_in_menus = False
objects = NewsPageManager()
excerpt = models.TextField(max_length=512, blank=False) excerpt = models.TextField(max_length=512, blank=False)
lead = RichTextField(features=["italic", "link"], blank=True) lead = RichTextField(features=["italic", "link"], blank=True)
body = CommonStreamField body = CommonStreamField
@@ -90,52 +96,3 @@ class NewsPage(HeadlessMixin, WPImportedPageMixin, Page):
class Meta: class Meta:
verbose_name = _("news article") verbose_name = _("news article")
verbose_name_plural = _("news articles") verbose_name_plural = _("news articles")
def import_wordpress_data(self, data):
import html
from bs4 import BeautifulSoup
def generate_excerpt(html_content):
soup = BeautifulSoup(html_content, features="lxml")
VALID_TAGS = ["div", "p"]
for tag in soup.findAll("p"):
if tag.name not in VALID_TAGS:
tag.remove()
text = soup.get_text().strip()
words = text.split(" ")
if len(words) < 26:
return text
return " ".join(words[:25]) + " [...]"
# Wagtail page model fields
self.title = html.unescape(data["title"])
self.slug = data["slug"]
self.first_published_at = data["first_published_at"]
self.last_published_at = data["last_published_at"]
self.latest_revision_created_at = data["latest_revision_created_at"]
self.search_description = data["search_description"]
# debug fields
self.wp_post_id = data["wp_post_id"]
self.wp_post_type = data["wp_post_type"]
self.wp_link = data["wp_link"]
self.wp_raw_content = data["wp_raw_content"]
self.wp_block_json = data["wp_block_json"]
self.wp_processed_content = data["wp_processed_content"]
self.wp_normalized_styles = data["wp_normalized_styles"]
self.wp_post_meta = data["wp_post_meta"]
# own model fields
self.body = data["body"] or ""
meta = data["wp_post_meta"]
written_excerpt = meta.get("excerpt_encoded")
generated_excerpt = ""
if not written_excerpt:
generated_excerpt = generate_excerpt(self.wp_processed_content)
self.excerpt = written_excerpt or generated_excerpt or "[...]"
+16
View File
@@ -248,6 +248,22 @@ def test_future_events_does_not_have_n_plus_one_queries(
) )
def test_future_events_does_not_load_wp_import_fields(event_index, graphql_post):
"""wp_* columns must stay deferred and lazy-load on explicit access."""
event = EventPageFactory(parent=event_index, wp_raw_content="marker")
EventOccurrence.objects.create(
event=event, start=timezone.now() + timedelta(days=1), venue_custom="X"
)
with CaptureQueriesContext(connection) as ctx:
response, body = graphql_post("{ eventIndex { futureEvents { id } } }")
assert response.status_code == 200 and "errors" not in body, body
sql = "\n".join(q["sql"] for q in ctx.captured_queries)
assert "wp_raw_content" not in sql, f"wp_* must be deferred. SQL:\n{sql}"
assert EventPage.objects.get(pk=event.pk).wp_raw_content == "marker"
def test_graphql_event_index_future_events_ordered_by_next_occurrence(event_index, graphql_post): def test_graphql_event_index_future_events_ordered_by_next_occurrence(event_index, graphql_post):
now = timezone.now() now = timezone.now()
+8 -41
View File
@@ -9,13 +9,17 @@ from grapple.models import (
) )
from wagtail.admin.panels import FieldPanel, FieldRowPanel, MultiFieldPanel from wagtail.admin.panels import FieldPanel, FieldRowPanel, MultiFieldPanel
from wagtail.fields import RichTextField, StreamField from wagtail.fields import RichTextField, StreamField
from wagtail.models import Page from wagtail.models import Page, PageManager
from wagtail.search import index from wagtail.search import index
from wagtail_headless_preview.models import HeadlessMixin from wagtail_headless_preview.models import HeadlessMixin
from dnscms.blocks import ImageSliderBlock from dnscms.blocks import ImageSliderBlock
from dnscms.fields import CommonStreamField from dnscms.fields import CommonStreamField
from dnscms.wordpress.models import WPImportedPageMixin from dnscms.wordpress.models import DeferWPFieldsManagerMixin, WPImportedPageMixin
class VenuePageManager(DeferWPFieldsManagerMixin, PageManager):
pass
@register_singular_query_field("venueIndex") @register_singular_query_field("venueIndex")
@@ -59,6 +63,8 @@ class VenuePage(HeadlessMixin, WPImportedPageMixin, Page):
# should not be able to be shown in menus # should not be able to be shown in menus
show_in_menus = False show_in_menus = False
objects = VenuePageManager()
featured_image = models.ForeignKey( featured_image = models.ForeignKey(
"images.CustomImage", "images.CustomImage",
null=True, null=True,
@@ -178,42 +184,3 @@ class VenuePage(HeadlessMixin, WPImportedPageMixin, Page):
search_fields = Page.search_fields + [ search_fields = Page.search_fields + [
index.SearchField("body"), index.SearchField("body"),
] ]
def import_wordpress_data(self, data):
import html
# Wagtail page model fields
self.title = html.unescape(data["title"])
self.slug = data["slug"]
self.first_published_at = data["first_published_at"]
self.last_published_at = data["last_published_at"]
self.latest_revision_created_at = data["latest_revision_created_at"]
self.search_description = data["search_description"]
# debug fields
self.wp_post_id = data["wp_post_id"]
self.wp_post_type = data["wp_post_type"]
self.wp_link = data["wp_link"]
self.wp_raw_content = data["wp_raw_content"]
self.wp_block_json = data["wp_block_json"]
self.wp_processed_content = data["wp_processed_content"]
self.wp_normalized_styles = data["wp_normalized_styles"]
self.wp_post_meta = data["wp_post_meta"]
# own model fields
self.body = data["body"] or ""
meta = data["wp_post_meta"]
self.show_as_bookable = meta.get("neuf_venues_show_on_booking_page", False)
self.preposition = meta.get("neuf_venues_preposition") or ""
self.floor = meta.get("neuf_venues_floor") or ""
self.used_for = meta.get("neuf_venues_used_for") or ""
self.capability_bar = meta.get("neuf_venues_bar") or ""
self.capability_audio = meta.get("neuf_venues_audio") or ""
self.capability_lighting = meta.get("neuf_venues_lighting") or ""
self.capability_audio_video = meta.get("neuf_venues_audio_video") or ""
self.capacity_legal = meta.get("neuf_venues_capacity_legal") or ""
self.capacity_standing = meta.get("neuf_venues_capacity_standing") or ""
self.capacity_sitting = meta.get("neuf_venues_capacity_sitting") or ""