Skip to content

Commit

Permalink
Merge pull request #306 from jamesvandyne/content_plugins
Browse files Browse the repository at this point in the history
Extract exercise detail rendering from core
  • Loading branch information
jamesvandyne committed Nov 21, 2023
2 parents 3b1572d + 6cfc056 commit a3e5b14
Show file tree
Hide file tree
Showing 12 changed files with 175 additions and 105 deletions.
50 changes: 43 additions & 7 deletions apps/data/plugins/plugin.py
@@ -1,11 +1,11 @@
import abc
from typing import TYPE_CHECKING, Optional, Protocol
from typing import Protocol

from django import http, urls
from django.conf import settings
from django.db.models import TextChoices

if TYPE_CHECKING:
from data.post import models as post_models
from data.post import models as post_models


class NavigationProtocol(Protocol):
Expand All @@ -28,24 +28,60 @@ class FeedHook(Protocol):
@property
def has_feed_hooks(self) -> bool:
"""
Plugins which use the the feed hook should return True
Plugins which use the feed hook should return True
"""
return False

def feed_before_content(self, request: http.HttpRequest, post: Optional["post_models.TPost"] = None) -> str:
def feed_before_content(self, request: http.HttpRequest, post: post_models.TPost | None = None) -> str:
"""
Returns any content that should be displayed before the post.
"""
return ""

def feed_after_content(self, request: http.HttpRequest, post: Optional["post_models.TPost"] = None) -> str:
def feed_after_content(self, request: http.HttpRequest, post: post_models.TPost | None = None) -> str:
"""
Returns any content that should be displayed after the post.
"""
return ""


class Plugin(abc.ABC, NavigationProtocol, FeedHook):
class RequestContentType(TextChoices):
HTML = "html"
RSS = "rss"


class ContentHook(Protocol):
@property
def has_content_hooks(self) -> bool:
"""
Plugins which use the content hook should return True
"""
return False

def render_before_content(
self,
request: http.HttpRequest,
request_content_type: str = RequestContentType.HTML,
post: post_models.TPost | None = None,
) -> str:
"""
Returns any content that should be displayed before the post.
"""
return ""

def render_after_content(
self,
request: http.HttpRequest,
request_content_type: str = RequestContentType.HTML,
post: post_models.TPost | None = None,
) -> str:
"""
Returns any content that should be displayed after the post.
"""
return ""


class Plugin(abc.ABC, NavigationProtocol, FeedHook, ContentHook):
name: str
description: str
# A unique namespaced identifier for the plugin
Expand Down
53 changes: 48 additions & 5 deletions apps/interfaces/common/templatetags/plugins.py
Expand Up @@ -2,26 +2,43 @@

from django import template

from data.plugins import plugin as plugin_base
from data.plugins import pool

register = template.Library()
logger = logging.getLogger(__name__)


class RenderPlugin(template.Node):
class _RenderPlugin(template.Node):
def __init__(self, *, plugin: template.Variable, render_location: str):
self.plugin = plugin
self.render_location = render_location[1:-1] # trim quotes

def render(self, context) -> str:
def _get_plugin(self, context) -> plugin_base.Plugin:
plugin_variable = self.plugin.resolve(context=context)
plugin_ = pool.plugin_pool.get_plugin(plugin_variable)
if not plugin_:
raise template.TemplateSyntaxError(f"Plugin with identifier {plugin_variable} does not exist.")
if not plugin_.is_enabled():
raise template.TemplateSyntaxError(f"Plugin {plugin_variable} is not enabled.")

return plugin_

def _get_context(self, context) -> dict:
context["render_location"] = self.render_location
return context

def _render(self, plugin_: plugin_base.Plugin, context: dict) -> str:
return ""

def render(self, context) -> str:
plugin_ = self._get_plugin(context)
context = self._get_context(context)
return self._render(plugin_, context)


class RenderNavigationPlugin(_RenderPlugin):
def _render(self, plugin_: plugin_base.Plugin, context: dict) -> str:
try:
return plugin_.render_navigation(context=context, render_location=self.render_location)
except Exception:
Expand All @@ -30,13 +47,39 @@ def render(self, context) -> str:
return ""


@register.tag(name="render_plugin")
def do_render_plugin(parser, token):
class RenderAfterContent(_RenderPlugin):
def _render(self, plugin_: plugin_base.Plugin, context: dict) -> str:
try:
return plugin_.render_after_content(
request=context["request"],
request_content_type=plugin_base.RequestContentType.HTML,
post=context.get("t_post"),
)
except Exception:
# Use a broad exception to prevent a template error in a plugin from breaking Tanzawa
logger.exception("Error rendering %s, %s", plugin_, self.render_location)
return ""


@register.tag(name="render_navigation")
def do_render_navigation(parser, token):
try:
# split_contents() knows not to split quoted strings.
tag_name, plugin_identifier, render_location = token.split_contents()
except ValueError:
raise template.TemplateSyntaxError(
"%r tag requires a plugin identifier and location arguments" % token.contents.split()[0]
)
return RenderNavigationPlugin(plugin=template.Variable(plugin_identifier), render_location=render_location)


@register.tag(name="render_after_content")
def do_render_after_content(parser, token):
try:
# split_contents() knows not to split quoted strings.
tag_name, plugin_identifier, render_location = token.split_contents()
except ValueError:
raise template.TemplateSyntaxError(
"%r tag requires a plugin identifier and location arguments" % token.contents.split()[0]
)
return RenderPlugin(plugin=template.Variable(plugin_identifier), render_location=render_location)
return RenderAfterContent(plugin=template.Variable(plugin_identifier), render_location=render_location)
41 changes: 0 additions & 41 deletions apps/interfaces/public/entry/serializers.py

This file was deleted.

12 changes: 1 addition & 11 deletions apps/interfaces/public/entry/views.py
Expand Up @@ -8,11 +8,10 @@
from application import entry as entry_application
from data.entry import models as entry_models
from data.indieweb.constants import MPostKinds, MPostStatuses
from data.plugins import pool
from data.post import models as post_models
from data.streams.models import MStream

from . import forms, serializers
from . import forms


def status_detail(request, uuid):
Expand All @@ -34,8 +33,6 @@ def status_detail(request, uuid):
webmentions = t_post.ref_t_webmention.filter(approval_status=True).select_related("t_post", "t_webmention_response")
detail_template = f"public/entry/{t_post.m_post_kind.key}_item.html"

activities = _get_activities(t_entry)

context = {
"t_post": t_post,
"detail_template": detail_template,
Expand All @@ -47,7 +44,6 @@ def status_detail(request, uuid):
"title": t_entry.p_name if t_entry.p_name else t_entry.p_summary[:140],
"streams": MStream.objects.visible(request.user),
"public": True,
"activities": activities,
"meta": entry_application.get_open_graph_meta_for_entry(request, t_entry),
"open_interactions": request.GET.get("o"),
}
Expand Down Expand Up @@ -100,9 +96,3 @@ def get_context_data(self, *, object_list=None, **kwargs):
}
)
return context


def _get_activities(t_entry: entry_models.TEntry) -> list[dict]:
if "blog.tanzawa.plugins.exercise" in [p.identifier for p in pool.plugin_pool.enabled_plugins()]:
return serializers.Activity(t_entry.activities.all(), many=True).data
return []
8 changes: 2 additions & 6 deletions apps/tanzawa_plugin/comment_by_email/plugin.py
@@ -1,12 +1,8 @@
from typing import TYPE_CHECKING, Type

from django import http
from django.template import loader

from data.plugins import plugin, pool

if TYPE_CHECKING:
from data.post import models as post_models
from data.post import models as post_models

__identifier__ = "blog.tanzawa.plugins.comment-by-email"

Expand Down Expand Up @@ -35,7 +31,7 @@ def urls(self) -> str | None:
def admin_urls(self) -> str | None:
return None

def feed_after_content(self, request: http.HttpRequest, post: None | Type["post_models.TPost"] = None) -> str:
def feed_after_content(self, request: http.HttpRequest, post: post_models.TPost | None = None) -> str:
template = loader.get_template("comment_by_email/feed.html")
return template.render(context={"post": post})

Expand Down
42 changes: 39 additions & 3 deletions apps/tanzawa_plugin/exercise/interfaces/public/serializers.py
Expand Up @@ -8,7 +8,7 @@
from tanzawa_plugin.exercise.domain.exercise import queries


class ActivitySerializer(serializers.Serializer):
class _RunsTopActivity(serializers.Serializer):
route_svg = serializers.SerializerMethodField()

def get_route_svg(self, obj: models.Activity) -> str:
Expand Down Expand Up @@ -43,10 +43,46 @@ def get_total_kms(self, obj) -> float:
def get_total_time(self, obj) -> float:
return (queries.total_elapsed_time(self.start_at, self.end_at, self.activity_types) / 60) / 60

def get_activities(self, obj) -> ActivitySerializer:
def get_activities(self, obj) -> _RunsTopActivity:
activities = (
queries.get_activties(self.start_at, self.end_at, self.activity_types)
.select_related("map")
.order_by("-started_at")
)
return ActivitySerializer(instance=activities, many=True).data
return _RunsTopActivity(instance=activities, many=True).data


class ActivityPhoto(serializers.Serializer):
url = serializers.SerializerMethodField()

def get_url(self, obj: models.ActivityPhoto) -> str:
return obj.t_file.get_absolute_url()


class Activity(serializers.Serializer):
distance_km = serializers.SerializerMethodField()
elapsed_time_minutes = serializers.SerializerMethodField()
average_heartrate = serializers.SerializerMethodField()
total_elevation_gain = serializers.SerializerMethodField()
photos = serializers.SerializerMethodField()
route_svg = serializers.SerializerMethodField()

def get_distance_km(self, obj: models.Activity) -> float:
return obj.distance_km

def get_elapsed_time_minutes(self, obj: models.Activity) -> float:
return obj.elapsed_time_minutes

def get_average_heartrate(self, obj: models.Activity) -> float:
return obj.average_heartrate

def get_total_elevation_gain(self, obj: models.Activity) -> float:
return obj.total_elevation_gain

def get_photos(self, obj: models.Activity) -> dict:
return ActivityPhoto(obj.photos, many=True).data

def get_route_svg(self, obj: models.Activity) -> str:
return mark_safe(
tanzawa_plugin.exercise.domain.exercise.operations.maybe_create_and_get_svg(obj, 256, 256, css_class="h-80")
)
32 changes: 26 additions & 6 deletions apps/tanzawa_plugin/exercise/plugin.py
@@ -1,14 +1,12 @@
from typing import TYPE_CHECKING, Type

from django import http, template
from django.template import loader

from data.plugins import plugin, pool
from data.plugins.plugin import RequestContentType
from data.post import models as post_models

from .data import exercise_models

if TYPE_CHECKING:
from data.post import models as post_models
from .interfaces.public import serializers

__identifier__ = "blog.tanzawa.plugins.exercise"

Expand All @@ -35,6 +33,10 @@ def has_admin_left_nav(self) -> bool:
def has_feed_hooks(self):
return True

@property
def has_content_hooks(self):
return True

def render_navigation(
self,
*,
Expand All @@ -47,7 +49,7 @@ def render_navigation(
t = context.template.engine.get_template("exercise/navigation.html")
return t.render(context=context)

def feed_after_content(self, request: http.HttpRequest, post: None | Type["post_models.TPost"] = None) -> str:
def feed_after_content(self, request: http.HttpRequest, post: None | post_models.TPost = None) -> str:
from .interfaces.public.feeds import serializers

if post is None:
Expand All @@ -62,6 +64,24 @@ def feed_after_content(self, request: http.HttpRequest, post: None | Type["post_
activity_detail = serializers.Activity(activity).data
return template.render(context={"activity": activity_detail}, request=request)

def render_after_content(
self,
request: http.HttpRequest,
request_content_type: RequestContentType = RequestContentType.HTML,
post: post_models.TPost | None = None,
) -> str:
if post is None:
return ""

template = loader.get_template("exercise/public/fragments/activity_detail.html")
try:
activity = exercise_models.Activity.objects.get(entry_id=post.ref_t_entry.id)
except Exception:
return ""
else:
activity_detail = serializers.Activity(activity).data
return template.render(context={"activity": activity_detail}, request=request)


def get_plugin() -> plugin.Plugin:
return ExercisePlugin()
Expand Down

0 comments on commit a3e5b14

Please sign in to comment.