diff --git a/designsafe/apps/workspace/admin.py b/designsafe/apps/workspace/admin.py index 4c48a5386c..125fdbbed3 100644 --- a/designsafe/apps/workspace/admin.py +++ b/designsafe/apps/workspace/admin.py @@ -41,6 +41,7 @@ def get_fieldsets(self, request, obj=None): { "fields": ( "label", + "description", "enabled", ) }, diff --git a/designsafe/apps/workspace/cms_plugins.py b/designsafe/apps/workspace/cms_plugins.py new file mode 100644 index 0000000000..9d344e56fd --- /dev/null +++ b/designsafe/apps/workspace/cms_plugins.py @@ -0,0 +1,105 @@ +"""CMS plugins for Tools & Applications pages.""" + +import logging +from cms.plugin_base import CMSPluginBase +from cms.plugin_pool import plugin_pool +from designsafe.apps.workspace.models.app_entries import ( + AppListingEntry, +) +from designsafe.apps.workspace.models.app_cms_plugins import ( + AppCategoryListingPlugin, + RelatedAppsPlugin, + AppVariantsPlugin, +) + +logger = logging.getLogger(__name__) + + +class AppCategoryListing(CMSPluginBase): + """CMS plugin to render the list of apps for a given category.""" + + model = AppCategoryListingPlugin + name = "App Category Listing" + module = "Tools & Applications" + render_template = "designsafe/apps/workspace/app_listing_plugin.html" + cache = False + + def render(self, context, instance, placeholder): + context = super().render(context, instance, placeholder) + listing_entries = AppListingEntry.objects.filter( + category=instance.app_category, enabled=True + ) + serialized_listing = [ + { + "label": entry.label, + "icon": entry.icon, + "description": entry.description, + "tags": [tag.name for tag in entry.tags.all()], + "is_popular": entry.is_popular, + "is_simcenter": entry.is_simcenter, + "license_type": ( + "Open Source" if entry.license_type == "OS" else "Licensed" + ), + "href": entry.href, + } + for entry in listing_entries + ] + context["listing"] = serialized_listing + return context + + +plugin_pool.register_plugin(AppCategoryListing) + + +class RelatedApps(CMSPluginBase): + """CMS plugin to render related apps.""" + + model = RelatedAppsPlugin + name = "Related Apps" + module = "Tools & Applications" + render_template = "designsafe/apps/workspace/related_apps_plugin.html" + cache = False + + def render(self, context, instance: AppListingEntry, placeholder): + context = super().render(context, instance, placeholder) + listing_entries = instance.app.related_apps.filter(enabled=True) + serialized_listing = [ + { + "label": entry.label, + "icon": entry.icon, + "description": entry.description, + "tags": [tag.name for tag in entry.tags.all()], + "is_popular": entry.is_popular, + "is_simcenter": entry.is_simcenter, + "license_type": ( + "Open Source" if entry.license_type == "OS" else "Licensed" + ), + "href": entry.href, + } + for entry in listing_entries + ] + context["listing"] = serialized_listing + return context + + +plugin_pool.register_plugin(RelatedApps) + + +class AppVariants(CMSPluginBase): + """CMS plugin to render an apps versions/variants.""" + + model = AppVariantsPlugin + name = "App Version Selection" + module = "Tools & Applications" + render_template = "designsafe/apps/workspace/app_variant_plugin.html" + cache = False + + def render(self, context, instance: AppListingEntry, placeholder): + context = super().render(context, instance, placeholder) + app_variants = instance.app.appvariant_set.filter(enabled=True) + context["listing"] = app_variants + + return context + + +plugin_pool.register_plugin(AppVariants) diff --git a/designsafe/apps/workspace/migrations/0005_appcategorylistingplugin.py b/designsafe/apps/workspace/migrations/0005_appcategorylistingplugin.py new file mode 100644 index 0000000000..26fd527f5c --- /dev/null +++ b/designsafe/apps/workspace/migrations/0005_appcategorylistingplugin.py @@ -0,0 +1,42 @@ +# Generated by Django 4.2.6 on 2024-04-02 17:54 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + dependencies = [ + ("cms", "0022_auto_20180620_1551"), + ("workspace", "0004_initial_app_categories_and_tags"), + ] + + operations = [ + migrations.CreateModel( + name="AppCategoryListingPlugin", + 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_category", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="workspace.apptraycategory", + ), + ), + ], + options={ + "abstract": False, + }, + bases=("cms.cmsplugin",), + ), + ] diff --git a/designsafe/apps/workspace/migrations/0006_appvariant_description.py b/designsafe/apps/workspace/migrations/0006_appvariant_description.py new file mode 100644 index 0000000000..65e36af7b8 --- /dev/null +++ b/designsafe/apps/workspace/migrations/0006_appvariant_description.py @@ -0,0 +1,21 @@ +# Generated by Django 4.2.6 on 2024-04-02 22:16 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("workspace", "0005_appcategorylistingplugin"), + ] + + operations = [ + migrations.AddField( + model_name="appvariant", + name="description", + field=models.TextField( + blank=True, + help_text="App variant description text for version overview.", + null=True, + ), + ), + ] diff --git a/designsafe/apps/workspace/migrations/0007_relatedappsplugin.py b/designsafe/apps/workspace/migrations/0007_relatedappsplugin.py new file mode 100644 index 0000000000..97a3607c3f --- /dev/null +++ b/designsafe/apps/workspace/migrations/0007_relatedappsplugin.py @@ -0,0 +1,42 @@ +# Generated by Django 4.2.6 on 2024-04-03 15:14 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + dependencies = [ + ("cms", "0022_auto_20180620_1551"), + ("workspace", "0006_appvariant_description"), + ] + + operations = [ + migrations.CreateModel( + name="RelatedAppsPlugin", + 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( + on_delete=django.db.models.deletion.CASCADE, + to="workspace.applistingentry", + ), + ), + ], + options={ + "abstract": False, + }, + bases=("cms.cmsplugin",), + ), + ] diff --git a/designsafe/apps/workspace/migrations/0008_appvariantsplugin.py b/designsafe/apps/workspace/migrations/0008_appvariantsplugin.py new file mode 100644 index 0000000000..36954953ae --- /dev/null +++ b/designsafe/apps/workspace/migrations/0008_appvariantsplugin.py @@ -0,0 +1,42 @@ +# Generated by Django 4.2.6 on 2024-04-03 18:02 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + dependencies = [ + ("cms", "0022_auto_20180620_1551"), + ("workspace", "0007_relatedappsplugin"), + ] + + operations = [ + migrations.CreateModel( + name="AppVariantsPlugin", + 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( + 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 new file mode 100644 index 0000000000..2ba760254a --- /dev/null +++ b/designsafe/apps/workspace/models/app_cms_plugins.py @@ -0,0 +1,37 @@ +"""Models associated with CMS plugins for Tools & Applications""" + +from cms.models.pluginmodel import CMSPlugin +from django.db import models +from designsafe.apps.workspace.models.app_entries import ( + AppTrayCategory, + AppListingEntry, +) + + +class AppCategoryListingPlugin(CMSPlugin): + """Model for listing apps by category.""" + + app_category = models.ForeignKey( + to=AppTrayCategory, on_delete=models.deletion.CASCADE + ) + + def __str__(self): + return self.app_category.category + + +class RelatedAppsPlugin(CMSPlugin): + """Model for listing related apps.""" + + app = models.ForeignKey(to=AppListingEntry, on_delete=models.deletion.CASCADE) + + def __str__(self): + return self.app.label + + +class AppVariantsPlugin(CMSPlugin): + """Model for listing related apps.""" + + app = models.ForeignKey(to=AppListingEntry, on_delete=models.deletion.CASCADE) + + def __str__(self): + return self.app.label diff --git a/designsafe/apps/workspace/models/app_entries.py b/designsafe/apps/workspace/models/app_entries.py index 2c2cd2ddf7..21f4f06ee4 100644 --- a/designsafe/apps/workspace/models/app_entries.py +++ b/designsafe/apps/workspace/models/app_entries.py @@ -178,6 +178,12 @@ class AppVariant(models.Model): blank=True, ) + description = models.TextField( + help_text="App variant description text for version overview.", + blank=True, + null=True, + ) + # Tapis Apps version = models.CharField( help_text="The version number of the app. The app id + version denotes a unique app.", @@ -190,6 +196,14 @@ class AppVariant(models.Model): help_text="App variant visibility in app tray.", default=True ) + @property + def href(self): + """Retrieve the app's URL in the Tools & Applications space""" + app_href = f"/rw/workspace/applications/{self.app_id}" + if self.version: + app_href += f"?version={self.version}" + return app_href + def __str__(self): return f"{self.bundle.label} {self.app_id} {self.version} ({'ENABLED' if self.enabled else 'DISABLED'})" diff --git a/designsafe/apps/workspace/templates/designsafe/apps/workspace/app_card.html b/designsafe/apps/workspace/templates/designsafe/apps/workspace/app_card.html new file mode 100644 index 0000000000..1e1e6f3bde --- /dev/null +++ b/designsafe/apps/workspace/templates/designsafe/apps/workspace/app_card.html @@ -0,0 +1,24 @@ + +

{{app.label}}

+ +

{{app.description}}

+ + + + +
diff --git a/designsafe/apps/workspace/templates/designsafe/apps/workspace/app_listing_plugin.html b/designsafe/apps/workspace/templates/designsafe/apps/workspace/app_listing_plugin.html new file mode 100644 index 0000000000..6d0a96afb7 --- /dev/null +++ b/designsafe/apps/workspace/templates/designsafe/apps/workspace/app_listing_plugin.html @@ -0,0 +1,6 @@ +

{{instance.app_category}}

+
+ {% for app in listing %} + {% include "designsafe/apps/workspace/app_card.html" with app=app %} + {% endfor %} +
diff --git a/designsafe/apps/workspace/templates/designsafe/apps/workspace/app_variant_plugin.html b/designsafe/apps/workspace/templates/designsafe/apps/workspace/app_variant_plugin.html new file mode 100644 index 0000000000..ac7a2bf29d --- /dev/null +++ b/designsafe/apps/workspace/templates/designsafe/apps/workspace/app_variant_plugin.html @@ -0,0 +1,15 @@ +
+

Select a Version

+ {% for variant in listing %} +
+
+

{{variant.label}}

+ +
+

{{variant.description}}

+ {% if listing.count > forloop.counter %} +
+ {% endif %} +
+ {% endfor %} +
diff --git a/designsafe/apps/workspace/templates/designsafe/apps/workspace/related_apps_plugin.html b/designsafe/apps/workspace/templates/designsafe/apps/workspace/related_apps_plugin.html new file mode 100644 index 0000000000..8776b428b8 --- /dev/null +++ b/designsafe/apps/workspace/templates/designsafe/apps/workspace/related_apps_plugin.html @@ -0,0 +1,6 @@ +

Related Applications

+
+ {% for app in listing %} + {% include "designsafe/apps/workspace/app_card.html" with app=app %} + {% endfor %} +
diff --git a/designsafe/settings/common_settings.py b/designsafe/settings/common_settings.py index c09ae20ff6..66c2ee58cc 100644 --- a/designsafe/settings/common_settings.py +++ b/designsafe/settings/common_settings.py @@ -319,6 +319,9 @@ 'FormPlugin', 'MeetingFormPlugin', 'ResponsiveEmbedPlugin', + 'AppCategoryListing', + 'RelatedApps', + 'AppVariants' ) } CMSPLUGIN_CASCADE_PLUGINS = [ diff --git a/designsafe/templates/base.j2 b/designsafe/templates/base.j2 index c61398b765..76c42c8291 100644 --- a/designsafe/templates/base.j2 +++ b/designsafe/templates/base.j2 @@ -35,6 +35,7 @@ + {% block styles %}{% endblock %} {% render_block "css" %}