diff --git a/designsafe/apps/workspace/cms_plugins.py b/designsafe/apps/workspace/cms_plugins.py index d5570627e6..04dada9cb6 100644 --- a/designsafe/apps/workspace/cms_plugins.py +++ b/designsafe/apps/workspace/cms_plugins.py @@ -1,6 +1,7 @@ """CMS plugins for Tools & Applications pages.""" import logging +from typing import Optional, Union from cms.plugin_base import CMSPluginBase from cms.plugin_pool import plugin_pool from designsafe.apps.workspace.models.app_entries import ( @@ -10,11 +11,30 @@ AppCategoryListingPlugin, RelatedAppsPlugin, AppVariantsPlugin, + AppUserGuideLinkPlugin, ) +from designsafe.apps.workspace.forms import AppUserGuideLinkPluginForm logger = logging.getLogger(__name__) +def get_entry_instance(url): + """Helper function to get an AppListingEntry instance based on URL.""" + + app_listing_entries = AppListingEntry.objects.filter(enabled=True) + for entry in app_listing_entries: + if entry.href in url: + return entry + + return None + +def is_editing_text(url): + """Helper function to determine if the current context is editing text.""" + + # e.g. /…/page/edit-plugin/123/, /…/page/plugin/text_plugin/render-plugin/ + return 'admin/cms/page' in url + + class AppCategoryListing(CMSPluginBase): """CMS plugin to render the list of apps for a given category.""" @@ -53,6 +73,7 @@ def render(self, context, instance, placeholder): class RelatedApps(CMSPluginBase): """CMS plugin to render related apps.""" + # IDEA: Use get_entry_instance if model field is empty (i.e. auto value) model = RelatedAppsPlugin name = "Related Apps" @@ -89,6 +110,7 @@ def render(self, context, instance: AppListingEntry, placeholder): class AppVariants(CMSPluginBase): """CMS plugin to render an apps versions/variants.""" + # IDEA: Use get_entry_instance if model field is empty (i.e. auto value) model = AppVariantsPlugin name = "App Version Selection" @@ -105,3 +127,37 @@ def render(self, context, instance: AppListingEntry, placeholder): plugin_pool.register_plugin(AppVariants) + + +class AppUserGuideLink(CMSPluginBase): + """CMS plugin to render the user guide link.""" + + model = AppUserGuideLinkPlugin + form = AppUserGuideLinkPluginForm + name = "App User Guide Link" + module = "Tools & Applications" + render_template = "designsafe/apps/workspace/app_user_guide_link_plugin.html" + text_enabled = True + cache = False + + def render( + self, + context, + instance: Optional[Union[AppUserGuideLinkPlugin, AppListingEntry]] = None, + placeholder=None, + ): + plugin_instance = instance if isinstance(instance, AppUserGuideLinkPlugin) else None + + instance_app = None + if isinstance(instance, AppUserGuideLinkPlugin): + instance_app = getattr(instance, "app", None) + if instance_app is None: + instance_app = get_entry_instance(context.get("request").path) + + context = super().render(context, plugin_instance, placeholder) + context["user_guide_link"] = getattr(instance_app, "user_guide_link", None) + context["is_editing_text"] = is_editing_text(context.get("request").path) + return context + + +plugin_pool.register_plugin(AppUserGuideLink) diff --git a/designsafe/apps/workspace/forms.py b/designsafe/apps/workspace/forms.py index 9e036d84ac..d496eb48ba 100644 --- a/designsafe/apps/workspace/forms.py +++ b/designsafe/apps/workspace/forms.py @@ -3,6 +3,7 @@ from django.forms import ModelForm from designsafe.apps.workspace.models.app_entries import AppVariant +from designsafe.apps.workspace.models.app_cms_plugins import AppUserGuideLinkPlugin class AppVariantForm(ModelForm): @@ -20,3 +21,18 @@ class Meta: " only after save." ), } + +class AppUserGuideLinkPluginForm(ModelForm): + """Customizes app user guide link plugin admin form""" + + class Meta: + """To add or change help text""" + + model = AppUserGuideLinkPlugin + fields = '__all__' + + help_texts = { + "app": ( + "If no app is selected, then app can be derived from URL path." + ), + } diff --git a/designsafe/apps/workspace/migrations/0019_appuserguidelinkplugin.py b/designsafe/apps/workspace/migrations/0019_appuserguidelinkplugin.py new file mode 100644 index 0000000000..e2c42b675b --- /dev/null +++ b/designsafe/apps/workspace/migrations/0019_appuserguidelinkplugin.py @@ -0,0 +1,44 @@ +# Generated by Django 4.2.20 on 2025-09-22 21:33 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + dependencies = [ + ("cms", "0022_auto_20180620_1551"), + ("workspace", "0018_appvariant_external_href"), + ] + + operations = [ + migrations.CreateModel( + name="AppUserGuideLinkPlugin", + fields=[ + ( + "cmsplugin_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + related_name="%(app_label)s_%(class)s", + serialize=False, + to="cms.cmsplugin", + ), + ), + ( + "app", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="workspace.applistingentry", + ), + ), + ], + options={ + "abstract": False, + }, + bases=("cms.cmsplugin",), + ), + ] diff --git a/designsafe/apps/workspace/models/app_cms_plugins.py b/designsafe/apps/workspace/models/app_cms_plugins.py index 2ba760254a..20337f8c67 100644 --- a/designsafe/apps/workspace/models/app_cms_plugins.py +++ b/designsafe/apps/workspace/models/app_cms_plugins.py @@ -35,3 +35,17 @@ class AppVariantsPlugin(CMSPlugin): def __str__(self): return self.app.label + + +class AppUserGuideLinkPlugin(CMSPlugin): + """Model for rendering an app user guide link.""" + + app = models.ForeignKey( + to=AppListingEntry, + on_delete=models.deletion.CASCADE, + null=True, + blank=True, + ) + + def __str__(self): + return self.app.label if self.app else "Based on URL" diff --git a/designsafe/apps/workspace/templates/designsafe/apps/workspace/app_user_guide_link_plugin.html b/designsafe/apps/workspace/templates/designsafe/apps/workspace/app_user_guide_link_plugin.html new file mode 100644 index 0000000000..44458d25b0 --- /dev/null +++ b/designsafe/apps/workspace/templates/designsafe/apps/workspace/app_user_guide_link_plugin.html @@ -0,0 +1,16 @@ +{# @var user_guide_link #} + +{% if user_guide_link or is_editing_text %} + User Guide +{% else %} + User guide not found. +{% endif %} diff --git a/designsafe/settings/common_settings.py b/designsafe/settings/common_settings.py index 1f41db5dd8..79f411b8e5 100644 --- a/designsafe/settings/common_settings.py +++ b/designsafe/settings/common_settings.py @@ -327,7 +327,8 @@ 'ResponsiveEmbedPlugin', 'AppCategoryListing', 'RelatedApps', - 'AppVariants' + 'AppVariants', + 'AppUserGuideLink', ) } CMSPLUGIN_CASCADE_PLUGINS = [