diff --git a/taccsite_cms/contrib/constants.py b/taccsite_cms/contrib/constants.py
new file mode 100644
index 000000000..169450392
--- /dev/null
+++ b/taccsite_cms/contrib/constants.py
@@ -0,0 +1,7 @@
+TEXT_FOR_NESTED_PLUGIN_CONTENT_SWAP = '\
+
\
+ - To add {element},
\
+ - nest "{plugin_name}" plugin inside this plugin.
\
+ - To edit {element},
\
+ - edit existing nested "{plugin_name}" plugin.
\
+
'
diff --git a/taccsite_cms/contrib/helpers.py b/taccsite_cms/contrib/helpers.py
new file mode 100644
index 000000000..f26f86cf5
--- /dev/null
+++ b/taccsite_cms/contrib/helpers.py
@@ -0,0 +1,31 @@
+# Get Django `models.CharField` `choices`
+def get_choices(choice_dict):
+ """Get a sequence for a Django model field choices from a dictionary.
+ :param Dict[str, Dict[str, str]] dictionary: choice as key for dictionary of classnames and descriptions
+ :return: a sequence for django.db.models.CharField.choices
+ :rtype: List[Tuple[str, str], ...]
+ """
+ choices = []
+
+ for key, data in choice_dict.items():
+ choice = (key, data['description'])
+ choices.append(choice)
+
+ return choices
+
+
+
+# GH-93, GH-142, GH-133: Upcoming functions here (ease merge conflict, maybe)
+
+
+
+# Concatenate a list of CSS classes
+# SEE: https://github.com/django-cms/djangocms-bootstrap4/blob/master/djangocms_bootstrap4/helpers.py
+def concat_classnames(classes):
+ """Concatenate a list of classname strings (without failing on None)"""
+ # SEE: https://stackoverflow.com/a/20271297/11817077
+ return ' '.join(_class for _class in classes if _class)
+
+
+
+# GH-93, GH-142, GH-133: Upcoming functions here (ease merge conflict, maybe)
diff --git a/taccsite_cms/contrib/taccsite_data_list/__init__.py b/taccsite_cms/contrib/taccsite_data_list/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/taccsite_cms/contrib/taccsite_data_list/cms_plugins.py b/taccsite_cms/contrib/taccsite_data_list/cms_plugins.py
new file mode 100644
index 000000000..fece7a03f
--- /dev/null
+++ b/taccsite_cms/contrib/taccsite_data_list/cms_plugins.py
@@ -0,0 +1,136 @@
+from cms.plugin_base import CMSPluginBase
+from cms.plugin_pool import plugin_pool
+from django.utils.translation import gettext_lazy as _
+
+from taccsite_cms.contrib.constants import TEXT_FOR_NESTED_PLUGIN_CONTENT_SWAP
+from taccsite_cms.contrib.helpers import concat_classnames
+
+from .models import TaccsiteDataList, TaccsiteDataListItem
+from .constants import ORIENTATION_DICT, TYPE_STYLE_DICT, DENSITY_DICT
+
+
+
+# Helpers
+
+def get_classname(dict, value):
+ """Get layout class based on value."""
+ return dict.get(value, {}).get('classname')
+
+
+
+# Plugins
+
+@plugin_pool.register_plugin
+class TaccsiteDataListPlugin(CMSPluginBase):
+ """
+ Components > "Data List" Plugin
+ https://confluence.tacc.utexas.edu/x/EiIFDg
+ """
+ module = 'TACC Site'
+ model = TaccsiteDataList
+ name = _('Data List')
+ render_template = 'data_list.html'
+
+ cache = True
+ text_enabled = False
+ allow_children = True
+ child_classes = [
+ 'TaccsiteDataListItemPlugin'
+ ]
+
+ fieldsets = [
+ (_('Required configuration'), {
+ 'fields': (
+ 'type_style',
+ 'orientation',
+ 'density',
+ )
+ }),
+ (_('Optional configuration'), {
+ 'fields': (
+ 'truncate_values',
+ )
+ }),
+ (_('Advanced settings'), {
+ 'classes': ('collapse',),
+ 'fields': (
+ 'attributes',
+ )
+ }),
+ ]
+
+ # Render
+
+ def render(self, context, instance, placeholder):
+ context = super().render(context, instance, placeholder)
+ request = context['request']
+
+ classes = concat_classnames([
+ 'c-data-list',
+ get_classname(ORIENTATION_DICT, instance.orientation),
+ get_classname(TYPE_STYLE_DICT, instance.type_style),
+ get_classname(DENSITY_DICT, instance.density),
+ 'c-data-list--should-truncate-values'
+ if instance.truncate_values else '',
+ instance.attributes.get('class'),
+ ])
+ instance.attributes['class'] = classes
+
+ return context
+
+@plugin_pool.register_plugin
+class TaccsiteDataListItemPlugin(CMSPluginBase):
+ """
+ Components > "Data List Item" Plugin
+ https://confluence.tacc.utexas.edu/x/EiIFDg
+ """
+ module = 'TACC Site'
+ model = TaccsiteDataListItem
+ name = _('Data List Item')
+ render_template = 'data_list_item.html'
+
+ cache = True
+ text_enabled = False
+ allow_children = True
+ child_classes = [
+ 'LinkPlugin'
+ ]
+ max_children = 1 # Only a label until we know what value will need
+
+ fieldsets = [
+ (None, {
+ 'fields': (
+ ('key', 'value'),
+ ),
+ }),
+ (_('Link'), {
+ 'classes': ('collapse',),
+ 'description': TEXT_FOR_NESTED_PLUGIN_CONTENT_SWAP.format(
+ element='a link',
+ plugin_name='Link'
+ ) + '\
+
\
+ The "Link" plugin\'s "Display name" field takes precedence over this plugin\'s "Label" field. If "Link" plugin is not rendered, then check "Advanced settings" of this plugin.',
+ 'fields': (),
+ }),
+ (_('Advanced settings'), {
+ 'classes': ('collapse',),
+ 'fields': (
+ 'use_plugin_as_key',
+ ),
+ })
+ ]
+
+ # Render
+
+ def render(self, context, instance, placeholder):
+ context = super().render(context, instance, placeholder)
+ request = context['request']
+
+ parent_plugin_instance = instance.parent.get_plugin_instance()[0]
+
+ context.update({
+ 'parent_plugin_instance': parent_plugin_instance
+ })
+
+ return context
diff --git a/taccsite_cms/contrib/taccsite_data_list/constants.py b/taccsite_cms/contrib/taccsite_data_list/constants.py
new file mode 100644
index 000000000..8a27bd084
--- /dev/null
+++ b/taccsite_cms/contrib/taccsite_data_list/constants.py
@@ -0,0 +1,38 @@
+# TODO: Consider using an Enum (and an Abstract Enum with `get_choices` and `get_classname` methods)
+
+ORIENTATION_DICT = {
+ 'horizontal': {
+ 'classname': 'c-data-list--is-horz',
+ 'description': 'Horizontal (all data on one row)',
+ 'short_description': 'Horizontal',
+ },
+ 'vertical': {
+ 'classname': 'c-data-list--is-vert',
+ 'description': 'Vertical (every label and value has its own row)',
+ 'short_description': 'Vertical',
+ },
+}
+
+TYPE_STYLE_DICT = {
+ 'table': {
+ 'description': 'Table (e.g. Columns)',
+ 'short_description': 'Table',
+ },
+ 'dlist': {
+ 'description': 'List (e.g. Glossary, Metadata)',
+ 'short_description': 'List',
+ },
+}
+
+DENSITY_DICT = {
+ 'default': {
+ 'classname': 'c-data-list--is-wide',
+ 'description': 'Default (ample extra space)',
+ 'short_description': 'Default',
+ },
+ 'compact': {
+ 'classname': 'c-data-list--is-narrow',
+ 'description': 'Compact (minimal extra space)',
+ 'short_description': 'Compact',
+ },
+}
diff --git a/taccsite_cms/contrib/taccsite_data_list/migrations/0001_initial.py b/taccsite_cms/contrib/taccsite_data_list/migrations/0001_initial.py
new file mode 100644
index 000000000..2c82520b9
--- /dev/null
+++ b/taccsite_cms/contrib/taccsite_data_list/migrations/0001_initial.py
@@ -0,0 +1,46 @@
+# Generated by Django 2.2.16 on 2021-08-06 20:17
+
+from django.db import migrations, models
+import django.db.models.deletion
+import djangocms_attributes_field.fields
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ('cms', '0022_auto_20180620_1551'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='TaccsiteDataList',
+ fields=[
+ ('cmsplugin_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, related_name='taccsite_data_list_taccsitedatalist', serialize=False, to='cms.CMSPlugin')),
+ ('orientation', models.CharField(choices=[('horizontal', 'Horizontal (all data on one row)'), ('vertical', 'Vertical (every label and value has its own row)')], help_text='The direction in which to lay out the data. Hint: Choose based on the amount of space available in the layout for the data.', max_length=255, verbose_name='Orientation')),
+ ('type_style', models.CharField(choices=[('table', 'Table (e.g. Columns)'), ('dlist', 'List (e.g. Glossary, Metadata)')], help_text='The type of data to display, glossary/metadata or tabular. Notice: Each type of list has a slightly different style.', max_length=255, verbose_name='Type / Style')),
+ ('density', models.CharField(choices=[('default', 'Default (ample extra space)'), ('compact', 'Compact (minimal extra space)')], help_text='The amount of extra space in the layout. Hint: Choose based on the amount of space available in the layout for the data.', max_length=255, verbose_name='Density (Layout Spacing)')),
+ ('truncate_values', models.BooleanField(default=False, help_text='Truncate values if there is not enough space to show the label and the value. Notice: Labels are truncated as necessary.', verbose_name='Truncate the values (as necessary)')),
+ ('attributes', djangocms_attributes_field.fields.AttributesField(default=dict)),
+ ],
+ options={
+ 'abstract': False,
+ },
+ bases=('cms.cmsplugin',),
+ ),
+ migrations.CreateModel(
+ name='TaccsiteDataListItem',
+ fields=[
+ ('cmsplugin_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, related_name='taccsite_data_list_taccsitedatalistitem', serialize=False, to='cms.CMSPlugin')),
+ ('key', models.CharField(help_text='A label for the data value. To create a link, add a child plugin.', max_length=50, verbose_name='Label')),
+ ('value', models.CharField(help_text='The data value.', max_length=100, verbose_name='Value')),
+ ('use_plugin_as_key', models.BooleanField(default=True, help_text='If a child plugin is added, and this option is checked, then the child plugin will be used (not the Label field text).', verbose_name='Support child plugin for Label')),
+ ('attributes', djangocms_attributes_field.fields.AttributesField(default=dict)),
+ ],
+ options={
+ 'abstract': False,
+ },
+ bases=('cms.cmsplugin',),
+ ),
+ ]
diff --git a/taccsite_cms/contrib/taccsite_data_list/migrations/__init__.py b/taccsite_cms/contrib/taccsite_data_list/migrations/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/taccsite_cms/contrib/taccsite_data_list/models.py b/taccsite_cms/contrib/taccsite_data_list/models.py
new file mode 100644
index 000000000..750698739
--- /dev/null
+++ b/taccsite_cms/contrib/taccsite_data_list/models.py
@@ -0,0 +1,107 @@
+from cms.models.pluginmodel import CMSPlugin
+
+from django.db import models
+from django.utils.translation import gettext_lazy as _
+
+from djangocms_attributes_field import fields
+
+from taccsite_cms.contrib.helpers import get_choices
+
+from .constants import ORIENTATION_DICT, TYPE_STYLE_DICT, DENSITY_DICT
+
+
+
+# Helpers
+
+def get_short_description(dict, value):
+ """Get layout class based on value."""
+ return dict.get(value, {}).get('short_description')
+
+
+
+# Constants
+
+ORIENTATION_CHOICES = get_choices(ORIENTATION_DICT)
+TYPE_STYLE_CHOICES = get_choices(TYPE_STYLE_DICT)
+DENSITY_CHOICES = get_choices(DENSITY_DICT)
+
+
+
+# Models
+
+class TaccsiteDataList(CMSPlugin):
+ """
+ Components > "Data List" Model
+ """
+ orientation = models.CharField(
+ verbose_name=_('Orientation'),
+ help_text=_('The direction in which to lay out the data. Hint: Choose based on the amount of space available in the layout for the data.'),
+ choices=ORIENTATION_CHOICES,
+ blank=False,
+ max_length=255,
+ )
+ type_style = models.CharField(
+ verbose_name=_('Type / Style'),
+ help_text=_('The type of data to display, glossary/metadata or tabular. Notice: Each type of list has a slightly different style.'),
+ choices=TYPE_STYLE_CHOICES,
+ blank=False,
+ max_length=255,
+ )
+ density = models.CharField(
+ verbose_name=_('Density (Layout Spacing)'),
+ help_text=_('The amount of extra space in the layout. Hint: Choose based on the amount of space available in the layout for the data.'),
+ choices=DENSITY_CHOICES,
+ blank=False,
+ max_length=255,
+ )
+ truncate_values = models.BooleanField(
+ verbose_name=_('Truncate the values (as necessary)'),
+ help_text=_('Truncate values if there is not enough space to show the label and the value. Notice: Labels are truncated as necessary.'),
+ default=False,
+ )
+
+ attributes = fields.AttributesField()
+
+ def get_short_description(self):
+ orientation = get_short_description(ORIENTATION_DICT, self.orientation)
+ type_style = get_short_description(TYPE_STYLE_DICT, self.type_style)
+ density = get_short_description(DENSITY_DICT, self.density)
+
+ return density + ', ' + orientation + ' ' + type_style
+
+class TaccsiteDataListItem(CMSPlugin):
+ """
+ Components > "Data List Item" Model
+ """
+ key = models.CharField(
+ verbose_name=_('Label'),
+ help_text=_('A label for the data value.'),
+ blank=True,
+ max_length=50,
+ )
+ value = models.CharField(
+ verbose_name=_('Value'),
+ help_text=_('The data value.'),
+ blank=False,
+ max_length=100,
+ )
+ use_plugin_as_key = models.BooleanField(
+ verbose_name=_('Support child plugin for Label'),
+ help_text=_('If a child plugin is added, and this option is checked, then the child plugin will be used (not the Label field text).'),
+ default=True,
+ )
+
+ attributes = fields.AttributesField()
+
+ def get_short_description(self):
+ key = self.key
+ val = self.value
+ max_len = 4
+
+ should_truncate_key = len(key) > max_len
+ key_desc = key[0:max_len] + '…' if should_truncate_key else key
+
+ should_truncate_val = len(key) > max_len
+ val_desc = val[0:max_len] + '…' if should_truncate_val else val
+
+ return key_desc + ': ' + val_desc
diff --git a/taccsite_cms/contrib/taccsite_data_list/templates/data_list.html b/taccsite_cms/contrib/taccsite_data_list/templates/data_list.html
new file mode 100644
index 000000000..d6c38f17c
--- /dev/null
+++ b/taccsite_cms/contrib/taccsite_data_list/templates/data_list.html
@@ -0,0 +1,21 @@
+{% load cms_tags %}
+
+{% if instance.type_style == 'dlist' %}
+
+
+ {% for plugin_instance in instance.child_plugin_instances %}
+ {% render_plugin plugin_instance %}
+ {% endfor %}
+
+
+{% elif instance.type_style == 'table' %}
+
+
+
+ {% for plugin_instance in instance.child_plugin_instances %}
+ {% render_plugin plugin_instance %}
+ {% endfor %}
+
+
+
+{% endif %}
diff --git a/taccsite_cms/contrib/taccsite_data_list/templates/data_list_item.html b/taccsite_cms/contrib/taccsite_data_list/templates/data_list_item.html
new file mode 100644
index 000000000..0ed71f277
--- /dev/null
+++ b/taccsite_cms/contrib/taccsite_data_list/templates/data_list_item.html
@@ -0,0 +1,17 @@
+{% if parent_plugin_instance.type_style == 'dlist' %}
+
+
+ {% include "./data_list_item_key.html" %}
+
+ {{ instance.value }}
+
+{% elif parent_plugin_instance.type_style == 'table' %}
+
+
+ |
+ {% include "./data_list_item_key.html" %}
+ |
+ {{ instance.value }} |
+
+
+{% endif %}
diff --git a/taccsite_cms/contrib/taccsite_data_list/templates/data_list_item_key.html b/taccsite_cms/contrib/taccsite_data_list/templates/data_list_item_key.html
new file mode 100644
index 000000000..a6cc3ad86
--- /dev/null
+++ b/taccsite_cms/contrib/taccsite_data_list/templates/data_list_item_key.html
@@ -0,0 +1,9 @@
+{% load cms_tags %}
+
+{% if instance.use_plugin_as_key and instance.child_plugin_instances|length %}
+ {% for plugin_instance in instance.child_plugin_instances %}
+ {% render_plugin plugin_instance %}
+ {% endfor %}
+{% else %}
+ {{ instance.key }}
+{% endif %}
diff --git a/taccsite_cms/settings.py b/taccsite_cms/settings.py
index 530c2d3fd..5f07ba9bf 100644
--- a/taccsite_cms/settings.py
+++ b/taccsite_cms/settings.py
@@ -241,6 +241,7 @@ def getsecrets():
# TODO: Extract TACC CMS UI components into pip-installable plugins
# FAQ: The djangocms_bootstrap4 library can serve as an example
'taccsite_cms.contrib.taccsite_sample',
+ 'taccsite_cms.contrib.taccsite_data_list',
]
# Convert list of paths to list of dotted module names
diff --git a/taccsite_cms/static/site_cms/css/src/_imports/components/c-data-list.css b/taccsite_cms/static/site_cms/css/src/_imports/components/c-data-list.css
index 374ac3c97..9a197d2d2 100644
--- a/taccsite_cms/static/site_cms/css/src/_imports/components/c-data-list.css
+++ b/taccsite_cms/static/site_cms/css/src/_imports/components/c-data-list.css
@@ -58,6 +58,12 @@ table.c-data-list {
}
}
+/* To space out elements (tables) */
+th.c-data-list__key,
+td.c-data-list__value {
+ padding-block: 0.5em;
+}
+
/* To add colon (non-tables) */
.c-data-list__key:not(th)::after {
content: ':';