diff --git a/taccsite_cms/contrib/_docs/how-to-conditionally-render-child-plugins.md b/taccsite_cms/contrib/_docs/how-to-conditionally-render-child-plugins.md
new file mode 100644
index 000000000..db314ed32
--- /dev/null
+++ b/taccsite_cms/contrib/_docs/how-to-conditionally-render-child-plugins.md
@@ -0,0 +1,11 @@
+# How to Conditionally Render Child Plugins
+
+```handlebars
+ {% for plugin_instance in instance.child_plugin_instances %}
+ {% if plugin_instance.plugin_type == 'LinkPlugin' %}
+ >
+
+
+ {% endif %}
+ {% endfor %}
+```
diff --git a/taccsite_cms/contrib/_docs/how-to-handle-non-nullable-default-value.md b/taccsite_cms/contrib/_docs/how-to-handle-non-nullable-default-value.md
new file mode 100644
index 000000000..fb558ce69
--- /dev/null
+++ b/taccsite_cms/contrib/_docs/how-to-handle-non-nullable-default-value.md
@@ -0,0 +1,62 @@
+# How to Handle "Non-Nullable" "Default Value"
+
+## Sample Error
+
+```text
+You are trying to add a non-nullable field '...'
+to choice without a default; we can't do that
+(the database needs something to populate existing rows).
+Please select a fix:
+ 1) Provide a one-off default now (will be set on all existing rows)
+ 2) Quit, and let me add a default in models.py
+Select an option:
+```
+
+## Explanations
+
+- (blog post) [What do you do when 'makemigrations' is telling you About your Lack of Default Value](https://chrisbartos.com/articles/what-do-you-do-when-makemigrations-is-telling-you-about-your-lack-of-default-value/)
+- (video) [You are trying to add a non-nullable field ' ' to ' ' without a default; we can't do that](https://www.youtube.com/watch?v=NgaTUEijQSQ)
+
+## Solutions
+
+### For `cmsplugin_ptr`
+
+1. ☑ Select option 1), then see:
+ - [Follow-Up Error](#follow-up-error)
+ - [Notes ▸ `cmsplugin_ptr`](#cmsplugin_ptr)
+
+### For Other Fields
+
+1. ⚠ Select option 1) and hope for the best.
+2. ☑ Select option 2) and provide a sensible default (_not_ `None` a.k.a. null).
+3. ⚠ (blog post) (hack) [Add A Migration For A Non-Null Foreignkey Field In Django](https://jaketrent.com/post/add-migration-nonnull-foreignkey-field-django)
+
+## Follow-Up Error
+
+If you allowed Null to be set as default, then you may have this new error:
+
+```text
+django.db.utils.IntegrityError: column "..." contains null values
+```
+
+Solutions:
+
+1. [delete _relevant_ migration files and rebuild migrations](https://stackoverflow.com/a/37244199/11817077)
+2. [delete _all_ migration files and rebuild migrations](https://stackoverflow.com/a/37242930/11817077)
+
+## Notes
+
+### `cmsplugin_ptr`
+
+ If the field is `cmsplugin_ptr` then know that
+
+ - [it is a database relationship field managed automatically by Django](https://github.com/nephila/djangocms-blog/issues/316#issuecomment-242292787),
+ - you may see it in workarounds for other plugins ([source a](https://github.com/django-cms/djangocms-link/blob/3.0.0/djangocms_link/models.py#L125), [source b](https://github.com/django-cms/djangocms-picture/blob/3.0.0/djangocms_picture/models.py#L208)),
+ - you should __not__ add or overwrite it unless you know what you are doing.
+
+ _W. Bomar learned everything in the intitial version of this document after trying to overwrite `cmsplugin_ptr` while extending its model from [source a](https://github.com/django-cms/djangocms-link/blob/3.0.0/djangocms_link/models.py#L125). His solution was [delete _all_ migration files and rebuild migrations](https://stackoverflow.com/a/37242930/11817077)._
+
+## Appendix
+
+- [Django CMS ▸ How to create Plugins ▸ Handling Relations](https://docs.django-cms.org/en/release-3.7.x/how_to/custom_plugins.html#handling-relations)
+- [[BUG] Plugins with models that don't directly inherit from CMSPlugin or an abstract model cannot be copied](https://github.com/django-cms/django-cms/issues/6987)
diff --git a/taccsite_cms/contrib/_docs/taccsite_static_article.md b/taccsite_cms/contrib/_docs/taccsite_static_article.md
new file mode 100644
index 000000000..e69d9e300
--- /dev/null
+++ b/taccsite_cms/contrib/_docs/taccsite_static_article.md
@@ -0,0 +1,66 @@
+# Static Article Plugins
+
+## Intention
+
+Support static addition of news articles that originate from a Core news site.
+
+A [dynamic solution that pulls form the Core news site](https://github.com/TACC/Core-CMS/issues/69) is preferable.
+
+But this is not available due to constrainst of architecture, time, or ability.
+
+## Architecture
+
+### (Currently) Add Image via Child Plugin Instead of Via Fields
+
+Instead, the image fields should be in the plugin, __not__ via a child plugin, but a solution has not yet been implemented.
+
+#### Hope for the Future
+
+The `AbstractLink` model was successfully extended.
+
+See:
+ - [./how-to-extend-django-cms-plugin.md](./how-to-extend-django-cms-plugin.md)
+ - [../taccsite_static_article_preview](../taccsite_static_article_preview)
+ - [../taccsite_static_article_list](../taccsite_static_article_list)
+
+#### Failed Attempt
+
+1. Build model so it extends `AbstractPicture` from `djangocms-picture`.
+2. Tweak model to sweep bugs under the rug.
+3. Quit when he was unable to resolve the error,
+ `TaccsiteStaticNewsArticlePreview has no field named 'cmsplugin_ptr_id'`
+ upon saving a plugin instance.
+4. Learn:
+ - [one should not try to reduce `AbstractPicture`](https://stackoverflow.com/a/3674714/11817077)
+ - [one should not subclass a subclass of `CMSPlugin`](https://github.com/django-cms/django-cms/blob/3.7.4/cms/models/pluginmodel.py#L104)
+
+#### Abandoned Code
+
+```python
+from djangocms_picture.models import AbstractPicture
+
+# To allow user to not set image
+# FAQ: Emptying the clean() method avoids picture validation
+# SEE: https://github.com/django-cms/djangocms-picture/blob/3.0.0/djangocms_picture/models.py#L278
+def skip_image_validation():
+ pass
+
+class TaccsiteStaticNewsArticlePreview(AbstractPicture):
+ #
+ # …
+ #
+
+ # Remove error-prone attribute from parent class
+ # FAQ: Avoid error when running `makemigrations`:
+ # "You are trying to add a non-nullable field 'cmsplugin_ptr' […]"
+ # SEE: https://github.com/django-cms/djangocms-picture/blob/3.0.0/djangocms_picture/models.py#L212
+ # SEE: https://github.com/django-cms/djangocms-picture/blob/3.0.0/djangocms_picture/models.py#L234
+ cmsplugin_ptr = None
+
+ class Meta:
+ abstract = False
+
+ # Validate
+ def clean(self):
+ skip_image_validation()
+```
diff --git a/taccsite_cms/contrib/helpers.py b/taccsite_cms/contrib/helpers.py
index 0e6fa468f..9ae8af700 100644
--- a/taccsite_cms/contrib/helpers.py
+++ b/taccsite_cms/contrib/helpers.py
@@ -15,7 +15,23 @@ def get_choices(choice_dict):
-# GH-93, GH-142, GH-133: Upcoming functions here (ease merge conflict, maybe)
+# Filter Django `models.CharField` `choices`
+# SEE: get_choices
+def filter_choices_by_prefix(choices, prefix):
+ """Reduce sequence of choices to items whose values begin with given string
+ :param List[Tuple[str, str], ...] choices: the sequence to filter
+ :param str prefix: the starting text required of an item value to retain it
+ :returns: a sequence for django.db.models.CharField.choices
+ :rtype: List[Tuple[str, str], ...]
+ """
+ new_choices = []
+
+ for choice in choices:
+ should_keep = choice[0].startswith(prefix)
+ if should_keep:
+ new_choices.append(choice)
+
+ return new_choices
@@ -28,7 +44,26 @@ def concat_classnames(classes):
-# GH-93, GH-142, GH-133: Upcoming functions here (ease merge conflict, maybe)
+# Create a list clone that has another list shoved into it
+# SEE: https://newbedev.com/how-to-insert-multiple-elements-into-a-list
+def insert_at_position(position, list, list_to_insert):
+ """Insert list at position within another list
+ :returns: New list
+ """
+ return list[:position] + list_to_insert + list[position:]
+
+
+
+# Get the date from a list that is nearest
+# SEE: https://stackoverflow.com/a/32237949/11817077
+def get_nearest(items, pivot):
+ """Get nearest date (or other arithmatic value)
+ :returns: The item value nearest the given "pivot" value
+ """
+ return min(items, key=lambda x: abs(x - pivot))
+
+
+
# Get list of indicies of items that start with text
# SEE: https://stackoverflow.com/a/67393343/11817077
def get_indices_that_start_with(text, list):
@@ -39,6 +74,129 @@ def get_indices_that_start_with(text, list):
return [i for i in range(len(list)) if list[i].startswith(text)]
+
+# Populate class attribute of plugin instances
+def add_classname_to_instances(classname, plugin_instances):
+ """Add class names to class attribute of plugin instances"""
+ for instance in plugin_instances:
+ # A plugin must not have any class set
+ if not hasattr(instance.attributes, 'class'):
+ instance.attributes['class'] = ''
+
+ # The class should occur before any CMS or user classes
+ # FAQ: This keeps plugin author classes together
+ instance.attributes['class'] = instance.attributes['class'] + classname
+
+
+
+# Get date nearest today
+
+from datetime import date
+
+# HELP: Can this logic be less verbose?
+# HELP: Is the `preferred_time_period` parameter effectual?
+def which_date_is_nearest_today(date_a, date_b, preferred_time_period):
+ """
+ Returns whether each date is today or nearest today, and whether nearest date is past or today or future.
+ Only two dates are supported. You may prefer 'future' or 'past' date(s).
+ If both dates are the same date, then both are reported as True.
+ :param datetime date_a: a date "A" to compare
+ :param datetime date_b: a date "B" to compare
+ :param str preferred_time_period: whether to prefer 'future' or 'past' dates
+ :returns:
+ A tuple of tuples:
+ ((
+ ``boolean`` of whether ``date_a`` is nearest,
+ ``string`` of ``date_a`` time period ``past``/``today``/``future``
+ ),
+ (
+ ``boolean`` of whether ``date_b`` is nearest,
+ ``string`` of ``date_b`` time period ``past``/``today``/``future``
+ )),
+ :rtype: tuple
+ """
+ today = date.today()
+ is_a = False
+ is_b = False
+ a_time_period = 'today'
+ b_time_period = 'today'
+
+ # Match preferred time
+
+ if today in {date_a, date_b}:
+ is_a = True
+ is_b = True
+ a_time_period = 'today'
+ b_time_period = 'today'
+
+ elif preferred_time_period == 'future':
+ is_a = date_a and date_a >= today
+ is_b = date_b and date_b >= today
+ if is_a: a_time_period = 'future'
+ if is_b: b_time_period = 'future'
+ if not is_a and not is_b:
+ is_a = date_a and date_a < today
+ is_b = date_b and date_b < today
+ if is_a: a_time_period = 'past'
+ if is_b: b_time_period = 'past'
+
+ elif preferred_time_period == 'past':
+ is_a = date_a and date_a < today
+ is_b = date_b and date_b < today
+ if is_a: a_time_period = 'past'
+ if is_b: b_time_period = 'past'
+ if not is_a and not is_b:
+ is_a = date_a and date_a >= today
+ is_b = date_b and date_b >= today
+ if is_a: a_time_period = 'future'
+ if is_b: b_time_period = 'future'
+
+ # Show nearest date
+ if is_a and is_b and date_a != date_b:
+ nearest_date = get_nearest((date_a, date_b), today)
+
+ if date_a == nearest_date:
+ is_b = False
+ if date_b == nearest_date:
+ is_a = False
+
+ return ((is_a, a_time_period), (is_b, b_time_period))
+
+
+
+# Allow plugins to set max number of nested children
+
+from django.shortcuts import render
+
+# SEE: https://github.com/django-cms/django-cms/issues/5102#issuecomment-597150141
+class AbstractMaxChildrenPlugin():
+ """
+ Abstract extension of `CMSPluginBase` that allows setting maximum amount of nested/child plugins.
+ Usage:
+ 1. Extend this class,
+ after extending `CMSPluginBase` or a class that extends `CMSPluginBase`.
+ 2. Set `max_children` to desired limit.
+ """
+
+ max_children = None
+
+ def add_view(self,request, form_url='', extra_context=None):
+
+ if self.max_children:
+ # FAQ: Placeholders do not have a parent, only plugins do
+ if self._cms_initial_attributes['parent']:
+ num_allowed = len([v for v in self._cms_initial_attributes['parent'].get_children() if v.get_plugin_instance()[0] is not None])
+ else:
+ num_allowed = len([v for v in self.placeholder.get_plugins() if v.get_plugin_instance()[0] is not None and v.get_plugin_name() == self.name])
+
+ if num_allowed >= self.max_children:
+ return render(request , "path/to/your/max_reached_template.html", {
+ 'max_children': self.max_children,
+ })
+ return super(AbstractMaxChildrenPlugin, self).add_view(request, form_url, extra_context)
+
+
+
# Tweak validation on Django CMS `AbstractLink` for TACC
from cms.models.pluginmodel import CMSPlugin
@@ -82,8 +240,9 @@ def clean(self):
if len(err.messages):
raise err
-# Get name of field from a given model
+
+# Get name of field from a given model
# SEE: https://stackoverflow.com/a/14498938/11817077
def get_model_field_name(model, field_name):
model_field_name = model._meta.get_field(field_name).verbose_name.title()
diff --git a/taccsite_cms/contrib/taccsite_static_article_list/README.md b/taccsite_cms/contrib/taccsite_static_article_list/README.md
new file mode 100644
index 000000000..81f610412
--- /dev/null
+++ b/taccsite_cms/contrib/taccsite_static_article_list/README.md
@@ -0,0 +1,3 @@
+# Static Article List
+
+See [./_docs/taccsite_static_article.md](./_docs/taccsite_static_article.md).
diff --git a/taccsite_cms/contrib/taccsite_static_article_list/__init__.py b/taccsite_cms/contrib/taccsite_static_article_list/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/taccsite_cms/contrib/taccsite_static_article_list/cms_plugins.py b/taccsite_cms/contrib/taccsite_static_article_list/cms_plugins.py
new file mode 100644
index 000000000..65e916d9e
--- /dev/null
+++ b/taccsite_cms/contrib/taccsite_static_article_list/cms_plugins.py
@@ -0,0 +1,142 @@
+from cms.plugin_pool import plugin_pool
+from django.utils.translation import gettext_lazy as _
+
+from djangocms_link.cms_plugins import LinkPlugin
+
+from taccsite_cms.contrib.helpers import (
+ concat_classnames,
+ add_classname_to_instances
+)
+
+from .models import TaccsiteArticleList
+from .constants import LAYOUT_DICT, STYLE_DICT
+
+
+
+# Helpers
+
+def get_layout_classname(value):
+ """Get layout class based on value."""
+ return LAYOUT_DICT.get(value, {}).get('classname')
+
+def get_style_classname(value):
+ """Get style class based on value."""
+ return STYLE_DICT.get(value, {}).get('classname')
+
+
+
+# Abstracts
+
+class AbstractArticleListPlugin(LinkPlugin):
+ """
+ Components > "Article List" Plugin
+ https://confluence.tacc.utexas.edu/x/OIAjCQ
+ """
+ module = 'TACC Site'
+ model = TaccsiteArticleList
+ # name = _('______ Article List (Static)') # abstract
+ render_template = 'article_list.html'
+ def get_render_template(self, context, instance, placeholder):
+ return self.render_template
+
+ cache = True
+ text_enabled = False
+ allow_children = True
+
+ fieldsets = [
+ (None, {
+ 'fields': (
+ 'title_text',
+ ('layout_type', 'style_type')
+ )
+ }),
+ (_('Footer link'), {
+ 'classes': ('collapse',),
+ 'description': 'The "See All" link at the bottom of the list. "Display name" is the text.',
+ 'fields': (
+ 'name',
+ ('external_link', 'internal_link'),
+ ('anchor', 'target'),
+ )
+ }),
+ (_('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([
+ 's-article-list c-article-list',
+ get_layout_classname(instance.layout_type),
+ get_style_classname(instance.style_type),
+ instance.attributes.get('class'),
+ ])
+ instance.attributes['class'] = classes
+
+ add_classname_to_instances('c-article-list__item', instance.child_plugin_instances)
+
+ context.update({
+ 'link_url': instance.get_link(),
+ 'link_text': instance.name,
+ 'link_target': instance.target
+ })
+ return context
+
+
+
+# Plugins
+
+@plugin_pool.register_plugin
+class TaccsiteNewsArticleListPlugin(AbstractArticleListPlugin):
+ """
+ Components > "Article List" Plugin
+ https://confluence.tacc.utexas.edu/x/OIAjCQ
+ """
+ name = _('News Article List (Static)')
+
+ child_classes = [
+ 'TaccsiteStaticNewsArticlePreviewPlugin'
+ ]
+
+@plugin_pool.register_plugin
+class TaccsiteAllocsArticleListPlugin(AbstractArticleListPlugin):
+ """
+ Components > "Article List" Plugin
+ https://confluence.tacc.utexas.edu/x/OIAjCQ
+ """
+ name = _('Allocations Article List (Static)')
+
+ child_classes = [
+ 'TaccsiteStaticAllocsArticlePreviewPlugin'
+ ]
+
+@plugin_pool.register_plugin
+class TaccsiteDocsArticleListPlugin(AbstractArticleListPlugin):
+ """
+ Components > "Article List" Plugin
+ https://confluence.tacc.utexas.edu/x/OIAjCQ
+ """
+ name = _('Document Article List (Static)')
+
+ child_classes = [
+ 'TaccsiteStaticDocsArticlePreviewPlugin'
+ ]
+
+@plugin_pool.register_plugin
+class TaccsiteEventsArticleListPlugin(AbstractArticleListPlugin):
+ """
+ Components > "Article List" Plugin
+ https://confluence.tacc.utexas.edu/x/OIAjCQ
+ """
+ name = _('Event Article List (Static)')
+
+ child_classes = [
+ 'TaccsiteStaticEventsArticlePreviewPlugin'
+ ]
diff --git a/taccsite_cms/contrib/taccsite_static_article_list/constants.py b/taccsite_cms/contrib/taccsite_static_article_list/constants.py
new file mode 100644
index 000000000..63505cb50
--- /dev/null
+++ b/taccsite_cms/contrib/taccsite_static_article_list/constants.py
@@ -0,0 +1,35 @@
+# TODO: Consider using an Enum (and an Abstract Enum with `get_choices` method)
+LAYOUT_DICT = {
+ 'cols-widest-2-even_width': {
+ 'classname': 'c-article-list--layout-a',
+ 'description': '2 Equal-Width Columns',
+ },
+ 'cols-widest-2-wide_narrow': {
+ 'classname': 'c-article-list--layout-b',
+ 'description': '2 Columns: 1 Wide, 1 Narrow',
+ },
+ 'cols-widest-2-narrow_wide': {
+ 'classname': 'c-article-list--layout-c',
+ 'description': '2 Columns: 1 Narrow, 1 Wide',
+ },
+ 'cols-widest-3-even_width': {
+ 'classname': 'c-article-list--layout-d',
+ 'description': '3 Equal-Width Columns',
+ },
+ 'rows-always-N-even_height': {
+ 'classname': 'c-article-list--layout-e'
+ ' ' + 'c-article-list--style-gapless',
+ 'description': 'Multiple Rows',
+ },
+}
+
+STYLE_DICT = {
+ 'rows-divided': {
+ 'classname': 'c-article-list--style-divided',
+ 'description': 'Dividers Between Articles',
+ },
+ 'cols-gapless': {
+ 'classname': 'c-article-list--style-gapless',
+ 'description': 'Remove Gaps Between Articles',
+ },
+}
diff --git a/taccsite_cms/contrib/taccsite_static_article_list/migrations/0001_initial.py b/taccsite_cms/contrib/taccsite_static_article_list/migrations/0001_initial.py
new file mode 100644
index 000000000..521bbd070
--- /dev/null
+++ b/taccsite_cms/contrib/taccsite_static_article_list/migrations/0001_initial.py
@@ -0,0 +1,43 @@
+# Generated by Django 2.2.16 on 2021-07-02 19:13
+
+from django.db import migrations, models
+import django.db.models.deletion
+import djangocms_attributes_field.fields
+import djangocms_link.validators
+import filer.fields.file
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ('filer', '0012_file_mime_type'),
+ ('cms', '0022_auto_20180620_1551'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='TaccsiteArticleList',
+ fields=[
+ ('template', models.CharField(choices=[('default', 'Default')], default='default', max_length=255, verbose_name='Template')),
+ ('name', models.CharField(blank=True, max_length=255, verbose_name='Display name')),
+ ('external_link', models.CharField(blank=True, help_text='Provide a link to an external source.', max_length=2040, validators=[djangocms_link.validators.IntranetURLValidator(intranet_host_re=None)], verbose_name='External link')),
+ ('anchor', models.CharField(blank=True, help_text='Appends the value only after the internal or external link. Do not include a preceding "#" symbol.', max_length=255, verbose_name='Anchor')),
+ ('mailto', models.EmailField(blank=True, max_length=255, verbose_name='Email address')),
+ ('phone', models.CharField(blank=True, max_length=255, verbose_name='Phone')),
+ ('target', models.CharField(blank=True, choices=[('_blank', 'Open in new window'), ('_self', 'Open in same window'), ('_parent', 'Delegate to parent'), ('_top', 'Delegate to top')], max_length=255, verbose_name='Target')),
+ ('attributes', djangocms_attributes_field.fields.AttributesField(blank=True, default=dict, verbose_name='Attributes')),
+ ('cmsplugin_ptr', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, related_name='taccsite_static_article_list_taccsitearticlelist', serialize=False, to='cms.CMSPlugin')),
+ ('title_text', models.CharField(blank=True, help_text='The title at the top of the list.', max_length=100, verbose_name='Title Text')),
+ ('layout_type', models.CharField(choices=[('Row Layouts', [('rows-always-N-even_height', 'Multiple Rows')]), ('Column Layouts', [('cols-widest-2-even_width', '2 Equal-Width Columns'), ('cols-widest-2-wide_narrow', '2 Columns: 1 Wide, 1 Narrow'), ('cols-widest-2-narrow_wide', '2 Columns: 1 Narrow, 1 Wide'), ('cols-widest-3-even_width', '3 Equal-Width Columns')])], default='Row Layouts', help_text='Layout of the articles within. Notice: All Column Layouts become multiple rows when screen width is narrow.', max_length=255, verbose_name='Layout Option')),
+ ('style_type', models.CharField(blank=True, choices=[('Row Layouts', [('rows-divided', 'Dividers Between Articles')]), ('Column Layouts', [('cols-gapless', 'Remove Gaps Between Articles')])], help_text='Optional styles for the list itself.', max_length=255, verbose_name='Style Option')),
+ ('file_link', filer.fields.file.FilerFileField(blank=True, help_text='If provided links a file from the filer app.', null=True, on_delete=django.db.models.deletion.SET_NULL, to='filer.File', verbose_name='File link')),
+ ('internal_link', models.ForeignKey(blank=True, help_text='If provided, overrides the external link.', null=True, on_delete=django.db.models.deletion.SET_NULL, to='cms.Page', verbose_name='Internal link')),
+ ],
+ options={
+ 'abstract': False,
+ },
+ bases=('cms.cmsplugin',),
+ ),
+ ]
diff --git a/taccsite_cms/contrib/taccsite_static_article_list/migrations/__init__.py b/taccsite_cms/contrib/taccsite_static_article_list/migrations/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/taccsite_cms/contrib/taccsite_static_article_list/models.py b/taccsite_cms/contrib/taccsite_static_article_list/models.py
new file mode 100644
index 000000000..0cd92ccf9
--- /dev/null
+++ b/taccsite_cms/contrib/taccsite_static_article_list/models.py
@@ -0,0 +1,115 @@
+from django.core.exceptions import ValidationError
+from django.utils.translation import gettext_lazy as _
+from django.utils.encoding import force_text
+from django.db import models
+
+from djangocms_link.models import AbstractLink
+
+from taccsite_cms.contrib.helpers import (
+ get_choices,
+ filter_choices_by_prefix,
+ clean_for_abstract_link,
+)
+
+from .constants import LAYOUT_DICT, STYLE_DICT
+
+
+
+# Constants
+
+ANY_CHOICES_NAME = _('Any Layouts')
+ROWS_CHOICES_NAME = _('Row Layouts')
+COLS_CHOICES_NAME = _('Column Layouts')
+
+LAYOUT_CHOICES = (
+ ( ROWS_CHOICES_NAME, filter_choices_by_prefix(
+ get_choices(LAYOUT_DICT), 'row'
+ ) ),
+ ( COLS_CHOICES_NAME, filter_choices_by_prefix(
+ get_choices(LAYOUT_DICT), 'cols'
+ ) ),
+)
+STYLE_CHOICES = (
+ ( ROWS_CHOICES_NAME, filter_choices_by_prefix(
+ get_choices(STYLE_DICT), 'rows'
+ ) ),
+ ( COLS_CHOICES_NAME, filter_choices_by_prefix(
+ get_choices(STYLE_DICT), 'cols'
+ ) ),
+)
+
+
+
+# Models
+
+class TaccsiteArticleList(AbstractLink):
+ """
+ Components > "Article List" Model
+ https://confluence.tacc.utexas.edu/x/OIAjCQ
+ """
+ title_text = models.CharField(
+ verbose_name=_('Title Text'),
+ help_text=_('The title at the top of the list.'),
+ blank=True,
+ max_length=100,
+ )
+
+ layout_type = models.CharField(
+ verbose_name=_('Layout Option'),
+ help_text=_('Layout of the articles within. Notice: All %(col_layouts)s become multiple rows when screen width is narrow.') % { 'col_layouts': COLS_CHOICES_NAME },
+ choices=LAYOUT_CHOICES,
+ default=LAYOUT_CHOICES[0][0],
+ blank=False,
+ max_length=255,
+ )
+ style_type = models.CharField(
+ verbose_name=_('Style Option'),
+ help_text=_('Optional styles for the list itself.'),
+ choices=STYLE_CHOICES,
+ blank=True,
+ max_length=255,
+ )
+
+ def get_short_description(self):
+ return self.title_text
+
+
+
+ # Parent
+
+ link_is_optional = True
+
+ class Meta:
+ abstract = False
+
+ # Validate
+ def clean(self):
+ clean_for_abstract_link(__class__, self)
+
+ # If user provided link text, then require link
+ if self.name and not self.get_link():
+ raise ValidationError(
+ _('Please provide a footer link or delete its display name.'), code='invalid'
+ )
+
+ # If user mix-and-matched layout and styles, then explain their mistake
+ layout_name = force_text(
+ self._meta.get_field('layout_type').verbose_name )
+ style_name = force_text(
+ self._meta.get_field('style_type').verbose_name )
+ if 'cols' in self.layout_type and 'rows' in self.style_type:
+ raise ValidationError(
+ _('If you choose a %(layout)s from %(row_layouts)s, then choose a %(style)s from %(row_layouts)s (or no %(style)s).') % {
+ 'style': style_name, 'layout': layout_name,
+ 'row_layouts': ROWS_CHOICES_NAME
+ },
+ code='invalid'
+ )
+ if 'rows' in self.layout_type and 'cols' in self.style_type:
+ raise ValidationError(
+ _('If you choose a %(layout)s from %(col_layouts)s, then choose a %(style)s from %(col_layouts)s (or no %(style)s).') % {
+ 'style': style_name, 'layout': layout_name,
+ 'col_layouts': COLS_CHOICES_NAME
+ },
+ code='invalid'
+ )
diff --git a/taccsite_cms/contrib/taccsite_static_article_list/templates/article_list.html b/taccsite_cms/contrib/taccsite_static_article_list/templates/article_list.html
new file mode 100644
index 000000000..1f0f98ef9
--- /dev/null
+++ b/taccsite_cms/contrib/taccsite_static_article_list/templates/article_list.html
@@ -0,0 +1,29 @@
+{% load cms_tags %}
+
+
+ {# Title #}
+ {% if instance.title_text %}
+
+ {{ instance.title_text }}
+
+ {% endif %}
+
+ {# Articles #}
+ {% for plugin_instance in instance.child_plugin_instances %}
+ {# … #}
+ {% render_plugin plugin_instance %}
+ {% endfor %}
+
+ {# Footer #}
+ {% if link_url %}
+
+ {% endif %}
+
diff --git a/taccsite_cms/contrib/taccsite_static_article_preview/README.md b/taccsite_cms/contrib/taccsite_static_article_preview/README.md
new file mode 100644
index 000000000..943596354
--- /dev/null
+++ b/taccsite_cms/contrib/taccsite_static_article_preview/README.md
@@ -0,0 +1,3 @@
+# Static Article Preview
+
+See [./_docs/taccsite_static_article.md](./_docs/taccsite_static_article.md).
diff --git a/taccsite_cms/contrib/taccsite_static_article_preview/__init__.py b/taccsite_cms/contrib/taccsite_static_article_preview/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/taccsite_cms/contrib/taccsite_static_article_preview/cms_plugins.py b/taccsite_cms/contrib/taccsite_static_article_preview/cms_plugins.py
new file mode 100644
index 000000000..eb6d7f0dd
--- /dev/null
+++ b/taccsite_cms/contrib/taccsite_static_article_preview/cms_plugins.py
@@ -0,0 +1,289 @@
+from django.core.exceptions import ValidationError
+
+from cms.plugin_pool import plugin_pool
+from django.utils.translation import gettext_lazy as _
+
+from djangocms_link.cms_plugins import LinkPlugin
+
+from taccsite_cms.contrib.helpers import (
+ concat_classnames,
+ insert_at_position,
+ which_date_is_nearest_today,
+ AbstractMaxChildrenPlugin,
+)
+
+from .models import (
+ MEDIA_SUPPORT_CHOICES,
+ TaccsiteStaticNewsArticlePreview,
+ TaccsiteStaticAllocsArticlePreview,
+ TaccsiteStaticDocsArticlePreview,
+ TaccsiteStaticEventsArticlePreview,
+)
+
+
+
+# Constants
+
+KIND_DICT = {
+ 'news': 'c-article-preview--news',
+ 'docs': 'c-article-preview--docs',
+ 'allocs': 'c-article-preview--allocs',
+ 'events': 'c-article-preview--events',
+}
+
+
+
+# Helpers
+
+# FAQ: This exists to retireve classnames via consistently-named functions
+# SEE: taccsite_cms.contrib.taccsite_static_article_list.cms_plugins
+def get_kind_classname(value):
+ """Get kind class based on value."""
+ return KIND_DICT[value]
+
+
+
+# Abstracts
+
+class AbstractArticlePreviewPlugin(LinkPlugin, AbstractMaxChildrenPlugin):
+ module = 'TACC Site'
+ # model = TaccsiteStatic___ArticlePreview # abstract
+ # name = _('______ Article Preview (Static)') # abstract
+ render_template = 'static_article_preview.html'
+ def get_render_template(self, context, instance, placeholder):
+ return self.render_template
+
+ cache = True
+ text_enabled = False
+ # NOTE: Should article previews be allowed to exist in isolation?
+ # Consider [hero banner](https://github.com/TACC/Core-CMS/issues/134).
+ # require_parent = True
+
+ fieldsets = [
+ (_('Link'), {
+ 'fields': (
+ ('external_link', 'internal_link'),
+ ('anchor', 'target'),
+ )
+ }),
+ (_('Advanced settings'), {
+ 'classes': ('collapse',),
+ 'fields': (
+ 'attributes',
+ )
+ }),
+ ]
+
+
+
+ # Helpers
+
+ # kind = '______' # abstract
+
+
+
+ # Render
+ def render(self, context, instance, placeholder):
+ context = super().render(context, instance, placeholder)
+ request = context['request']
+
+ classes = concat_classnames([
+ 'c-article-preview',
+ get_kind_classname(self.kind),
+ instance.attributes.get('class'),
+ ])
+ instance.attributes['class'] = classes
+
+ context.update({
+ 'kind': self.kind,
+ 'link_url': instance.get_link(),
+ 'link_text': instance.name,
+ 'link_target': instance.target
+ })
+ return context
+
+class AbstractArticlePreviewWithMediaPlugin(AbstractArticlePreviewPlugin):
+ allow_children = True
+ child_classes = [
+ 'PicturePlugin', # HELP: Why does this not show up in plugin list?
+ 'Bootstrap4PicturePlugin'
+ ]
+
+ fieldsets = insert_at_position(0, AbstractArticlePreviewPlugin.fieldsets, [
+ (_('Image'), {
+ # To enable these fields, see `./README.md`
+ # 'fields': ('picture', 'external_picture')
+ 'fields': ('media_support',)
+ }),
+ ])
+
+ # Set `readonly_fields` that can be populated upon instance creation
+ # SEE: https://stackoverflow.com/a/17614057/11817077
+ # HELP: Instead, how can we disable a field with minimal effort?
+ def get_readonly_fields(self, request, obj=None):
+ if obj: # i.e. user is editing instance
+ return ['media_support'] if len(MEDIA_SUPPORT_CHOICES) == 1 else []
+ else: # i.e. user is creating instance
+ return []
+
+
+
+# Plugins
+
+@plugin_pool.register_plugin
+class TaccsiteStaticNewsArticlePreviewPlugin(AbstractArticlePreviewWithMediaPlugin):
+ """
+ Components > "(Static) News Article Preview" Plugin
+ https://confluence.tacc.utexas.edu/x/OYAjCQ
+ """
+ model = TaccsiteStaticNewsArticlePreview
+ name = _('News Article Preview (Static)')
+
+ parent_classes = [
+ 'TaccsiteNewsArticleListPlugin'
+ ]
+
+ fieldsets = insert_at_position(0, AbstractArticlePreviewWithMediaPlugin.fieldsets, [
+ (None, {
+ # To enable these fields, see `./README.md`
+ # 'fields': (..., 'picture', 'external_picture')
+ 'fields': ('title_text', 'abstract_text')
+ }),
+ ])
+ fieldsets = insert_at_position(len(fieldsets) - 1, fieldsets, [
+ (_('Metadata'), {
+ 'fields': ('publish_date', 'type_text', 'author_text')
+ }),
+ ])
+
+
+
+ # Helpers
+
+ kind = 'news'
+
+@plugin_pool.register_plugin
+class TaccsiteStaticAllocsArticlePreviewPlugin(AbstractArticlePreviewWithMediaPlugin):
+ """
+ Components > "(Static) Allocations Article Preview" Plugin
+ https://confluence.tacc.utexas.edu/x/OYAjCQ
+ """
+ model = TaccsiteStaticAllocsArticlePreview
+ name = _('Allocations Article Preview (Static)')
+
+ parent_classes = [
+ 'TaccsiteAllocsArticleListPlugin'
+ ]
+
+ fieldsets = insert_at_position(0, AbstractArticlePreviewWithMediaPlugin.fieldsets, [
+ (None, {
+ # To enable these fields, see `./README.md`
+ # 'fields': ('picture', 'external_picture')
+ 'fields': ('title_text',)
+ }),
+ ])
+ fieldsets = insert_at_position(len(fieldsets) - 1, fieldsets, [
+ (_('Dates'), {
+ 'description': 'Two dates will show a range. If given one date, the nearest future date is shown. Otherwise, the nearest past date is shown.',
+ 'fields': (('publish_date', 'expiry_date'),)
+ }),
+ ])
+
+
+
+ # Helper
+
+ kind = 'allocs'
+
+
+
+ # Render
+ def render(self, context, instance, placeholder):
+ context = super().render(context, instance, placeholder)
+ request = context['request']
+
+ dates = which_date_is_nearest_today(
+ instance.publish_date,
+ instance.expiry_date,
+ 'future'
+ )
+ (should_show_open_date, open_date_time_period) = dates[0]
+ (should_show_close_date, close_date_time_period) = dates[1]
+
+ context.update({
+ 'open_date': instance.publish_date,
+ 'should_show_open_date': should_show_open_date,
+ 'open_date_time_period': open_date_time_period,
+
+ 'close_date': instance.expiry_date,
+ 'should_show_close_date': should_show_close_date,
+ 'close_date_time_period': close_date_time_period,
+ })
+ return context
+
+@plugin_pool.register_plugin
+class TaccsiteStaticDocsArticlePreviewPlugin(AbstractArticlePreviewPlugin):
+ """
+ Components > "(Static) Document Article Preview" Plugin
+ https://confluence.tacc.utexas.edu/x/OYAjCQ
+ """
+ model = TaccsiteStaticDocsArticlePreview
+ name = _('Document Article Preview (Static)')
+
+ parent_classes = [
+ 'TaccsiteDocsArticleListPlugin'
+ ]
+
+ fieldsets = insert_at_position(0, AbstractArticlePreviewPlugin.fieldsets, [
+ (None, {
+ 'fields': ('title_text', 'abstract_text')
+ }),
+ ])
+
+
+
+ # Helpers
+
+ kind = 'docs'
+
+@plugin_pool.register_plugin
+class TaccsiteStaticEventsArticlePreviewPlugin(AbstractArticlePreviewPlugin):
+ """
+ Components > "(Static) Event Article Preview" Plugin
+ https://confluence.tacc.utexas.edu/x/OYAjCQ
+ """
+ model = TaccsiteStaticEventsArticlePreview
+ name = _('Event Article Preview (Static)')
+
+ parent_classes = [
+ 'TaccsiteEventsArticleListPlugin'
+ ]
+
+ fieldsets = insert_at_position(0, AbstractArticlePreviewPlugin.fieldsets, [
+ (None, {
+ 'fields': (
+ ('publish_date', 'expiry_date'),
+ 'title_text',
+ 'abstract_text'
+ )
+ }),
+ ])
+
+
+
+ # Helpers
+
+ kind = 'events'
+
+
+
+ # Render
+ def render(self, context, instance, placeholder):
+ context = super().render(context, instance, placeholder)
+ request = context['request']
+
+ context.update({
+ 'open_date': instance.publish_date,
+ 'close_date': instance.expiry_date,
+ })
+ return context
diff --git a/taccsite_cms/contrib/taccsite_static_article_preview/migrations/0001_initial.py b/taccsite_cms/contrib/taccsite_static_article_preview/migrations/0001_initial.py
new file mode 100644
index 000000000..6cb252d59
--- /dev/null
+++ b/taccsite_cms/contrib/taccsite_static_article_preview/migrations/0001_initial.py
@@ -0,0 +1,116 @@
+# Generated by Django 2.2.16 on 2021-07-02 19:13
+
+from django.db import migrations, models
+import django.db.models.deletion
+import djangocms_attributes_field.fields
+import djangocms_link.validators
+import filer.fields.file
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ('filer', '0012_file_mime_type'),
+ ('cms', '0022_auto_20180620_1551'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='TaccsiteStaticNewsArticlePreview',
+ fields=[
+ ('template', models.CharField(choices=[('default', 'Default')], default='default', max_length=255, verbose_name='Template')),
+ ('name', models.CharField(blank=True, max_length=255, verbose_name='Display name')),
+ ('external_link', models.CharField(blank=True, help_text='Provide a link to an external source.', max_length=2040, validators=[djangocms_link.validators.IntranetURLValidator(intranet_host_re=None)], verbose_name='External link')),
+ ('anchor', models.CharField(blank=True, help_text='Appends the value only after the internal or external link. Do not include a preceding "#" symbol.', max_length=255, verbose_name='Anchor')),
+ ('mailto', models.EmailField(blank=True, max_length=255, verbose_name='Email address')),
+ ('phone', models.CharField(blank=True, max_length=255, verbose_name='Phone')),
+ ('target', models.CharField(blank=True, choices=[('_blank', 'Open in new window'), ('_self', 'Open in same window'), ('_parent', 'Delegate to parent'), ('_top', 'Delegate to top')], max_length=255, verbose_name='Target')),
+ ('attributes', djangocms_attributes_field.fields.AttributesField(blank=True, default=dict, verbose_name='Attributes')),
+ ('cmsplugin_ptr', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, related_name='taccsite_static_article_preview_taccsitestaticnewsarticlepreview', serialize=False, to='cms.CMSPlugin')),
+ ('media_support', models.CharField(choices=[('nested', 'Nest a single Picture / Image plugin inside this plugin.')], default='nested', max_length=255, verbose_name='How to Add an Image')),
+ ('title_text', models.CharField(default='', help_text='The title for the article.', max_length=50, verbose_name='Title')),
+ ('abstract_text', models.TextField(default='', help_text='A summary of the article', verbose_name='Abstract')),
+ ('type_text', models.CharField(blank=True, help_text='The type of the article, ex: "Science News", "Press Release" (manual entry).', max_length=50, verbose_name='Type')),
+ ('author_text', models.CharField(blank=True, help_text='The author of the article (manual entry).', max_length=50, verbose_name='Author')),
+ ('publish_date', models.DateField(blank=True, help_text='The date the article was published (manual entry).', null=True, verbose_name='Date Published')),
+ ('file_link', filer.fields.file.FilerFileField(blank=True, help_text='If provided links a file from the filer app.', null=True, on_delete=django.db.models.deletion.SET_NULL, to='filer.File', verbose_name='File link')),
+ ('internal_link', models.ForeignKey(blank=True, help_text='If provided, overrides the external link.', null=True, on_delete=django.db.models.deletion.SET_NULL, to='cms.Page', verbose_name='Internal link')),
+ ],
+ options={
+ 'abstract': False,
+ },
+ bases=('cms.cmsplugin',),
+ ),
+ migrations.CreateModel(
+ name='TaccsiteStaticEventsArticlePreview',
+ fields=[
+ ('template', models.CharField(choices=[('default', 'Default')], default='default', max_length=255, verbose_name='Template')),
+ ('name', models.CharField(blank=True, max_length=255, verbose_name='Display name')),
+ ('external_link', models.CharField(blank=True, help_text='Provide a link to an external source.', max_length=2040, validators=[djangocms_link.validators.IntranetURLValidator(intranet_host_re=None)], verbose_name='External link')),
+ ('anchor', models.CharField(blank=True, help_text='Appends the value only after the internal or external link. Do not include a preceding "#" symbol.', max_length=255, verbose_name='Anchor')),
+ ('mailto', models.EmailField(blank=True, max_length=255, verbose_name='Email address')),
+ ('phone', models.CharField(blank=True, max_length=255, verbose_name='Phone')),
+ ('target', models.CharField(blank=True, choices=[('_blank', 'Open in new window'), ('_self', 'Open in same window'), ('_parent', 'Delegate to parent'), ('_top', 'Delegate to top')], max_length=255, verbose_name='Target')),
+ ('attributes', djangocms_attributes_field.fields.AttributesField(blank=True, default=dict, verbose_name='Attributes')),
+ ('cmsplugin_ptr', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, related_name='taccsite_static_article_preview_taccsitestaticeventsarticlepreview', serialize=False, to='cms.CMSPlugin')),
+ ('title_text', models.CharField(default='', help_text='The title for the article.', max_length=50, verbose_name='Title')),
+ ('abstract_text', models.TextField(default='', help_text='A summary of the article', verbose_name='Abstract')),
+ ('expiry_date', models.DateField(blank=True, help_text='The date upon which the event starts (manual entry). Format: YYYY-MM-DD', null=True, verbose_name='Event End Date')),
+ ('publish_date', models.DateField(blank=True, help_text='The date after which the event ends (manual entry). Format: YYYY-MM-DD', null=True, verbose_name='Event Start Date')),
+ ('file_link', filer.fields.file.FilerFileField(blank=True, help_text='If provided links a file from the filer app.', null=True, on_delete=django.db.models.deletion.SET_NULL, to='filer.File', verbose_name='File link')),
+ ('internal_link', models.ForeignKey(blank=True, help_text='If provided, overrides the external link.', null=True, on_delete=django.db.models.deletion.SET_NULL, to='cms.Page', verbose_name='Internal link')),
+ ],
+ options={
+ 'abstract': False,
+ },
+ bases=('cms.cmsplugin',),
+ ),
+ migrations.CreateModel(
+ name='TaccsiteStaticDocsArticlePreview',
+ fields=[
+ ('template', models.CharField(choices=[('default', 'Default')], default='default', max_length=255, verbose_name='Template')),
+ ('name', models.CharField(blank=True, max_length=255, verbose_name='Display name')),
+ ('external_link', models.CharField(blank=True, help_text='Provide a link to an external source.', max_length=2040, validators=[djangocms_link.validators.IntranetURLValidator(intranet_host_re=None)], verbose_name='External link')),
+ ('anchor', models.CharField(blank=True, help_text='Appends the value only after the internal or external link. Do not include a preceding "#" symbol.', max_length=255, verbose_name='Anchor')),
+ ('mailto', models.EmailField(blank=True, max_length=255, verbose_name='Email address')),
+ ('phone', models.CharField(blank=True, max_length=255, verbose_name='Phone')),
+ ('target', models.CharField(blank=True, choices=[('_blank', 'Open in new window'), ('_self', 'Open in same window'), ('_parent', 'Delegate to parent'), ('_top', 'Delegate to top')], max_length=255, verbose_name='Target')),
+ ('attributes', djangocms_attributes_field.fields.AttributesField(blank=True, default=dict, verbose_name='Attributes')),
+ ('cmsplugin_ptr', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, related_name='taccsite_static_article_preview_taccsitestaticdocsarticlepreview', serialize=False, to='cms.CMSPlugin')),
+ ('title_text', models.CharField(default='', help_text='The title for the article.', max_length=50, verbose_name='Title')),
+ ('abstract_text', models.TextField(default='', help_text='A summary of the article', verbose_name='Abstract')),
+ ('file_link', filer.fields.file.FilerFileField(blank=True, help_text='If provided links a file from the filer app.', null=True, on_delete=django.db.models.deletion.SET_NULL, to='filer.File', verbose_name='File link')),
+ ('internal_link', models.ForeignKey(blank=True, help_text='If provided, overrides the external link.', null=True, on_delete=django.db.models.deletion.SET_NULL, to='cms.Page', verbose_name='Internal link')),
+ ],
+ options={
+ 'abstract': False,
+ },
+ bases=('cms.cmsplugin',),
+ ),
+ migrations.CreateModel(
+ name='TaccsiteStaticAllocsArticlePreview',
+ fields=[
+ ('template', models.CharField(choices=[('default', 'Default')], default='default', max_length=255, verbose_name='Template')),
+ ('name', models.CharField(blank=True, max_length=255, verbose_name='Display name')),
+ ('external_link', models.CharField(blank=True, help_text='Provide a link to an external source.', max_length=2040, validators=[djangocms_link.validators.IntranetURLValidator(intranet_host_re=None)], verbose_name='External link')),
+ ('anchor', models.CharField(blank=True, help_text='Appends the value only after the internal or external link. Do not include a preceding "#" symbol.', max_length=255, verbose_name='Anchor')),
+ ('mailto', models.EmailField(blank=True, max_length=255, verbose_name='Email address')),
+ ('phone', models.CharField(blank=True, max_length=255, verbose_name='Phone')),
+ ('target', models.CharField(blank=True, choices=[('_blank', 'Open in new window'), ('_self', 'Open in same window'), ('_parent', 'Delegate to parent'), ('_top', 'Delegate to top')], max_length=255, verbose_name='Target')),
+ ('attributes', djangocms_attributes_field.fields.AttributesField(blank=True, default=dict, verbose_name='Attributes')),
+ ('cmsplugin_ptr', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, related_name='taccsite_static_article_preview_taccsitestaticallocsarticlepreview', serialize=False, to='cms.CMSPlugin')),
+ ('media_support', models.CharField(choices=[('nested', 'Nest a single Picture / Image plugin inside this plugin.')], default='nested', max_length=255, verbose_name='How to Add an Image')),
+ ('title_text', models.CharField(default='', help_text='The title for the article.', max_length=50, verbose_name='Title')),
+ ('expiry_date', models.DateField(blank=True, help_text='The date after which submissions are not accepted (manual entry). Format: YYYY-MM-DD', null=True, verbose_name='Submission End Date')),
+ ('publish_date', models.DateField(blank=True, help_text='The date after which submissions are accepted (manual entry). Format: YYYY-MM-DD', null=True, verbose_name='Submission Start Date')),
+ ('file_link', filer.fields.file.FilerFileField(blank=True, help_text='If provided links a file from the filer app.', null=True, on_delete=django.db.models.deletion.SET_NULL, to='filer.File', verbose_name='File link')),
+ ('internal_link', models.ForeignKey(blank=True, help_text='If provided, overrides the external link.', null=True, on_delete=django.db.models.deletion.SET_NULL, to='cms.Page', verbose_name='Internal link')),
+ ],
+ options={
+ 'abstract': False,
+ },
+ bases=('cms.cmsplugin',),
+ ),
+ ]
diff --git a/taccsite_cms/contrib/taccsite_static_article_preview/migrations/__init__.py b/taccsite_cms/contrib/taccsite_static_article_preview/migrations/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/taccsite_cms/contrib/taccsite_static_article_preview/models.py b/taccsite_cms/contrib/taccsite_static_article_preview/models.py
new file mode 100644
index 000000000..fb3381a0f
--- /dev/null
+++ b/taccsite_cms/contrib/taccsite_static_article_preview/models.py
@@ -0,0 +1,206 @@
+from cms.models.pluginmodel import CMSPlugin
+
+from django.core.exceptions import ValidationError
+from django.utils.translation import gettext_lazy as _
+from django.utils.encoding import force_text
+from django.db import models
+
+from djangocms_link.models import AbstractLink
+
+from taccsite_cms.contrib.helpers import clean_for_abstract_link
+
+
+
+# Constants
+
+MEDIA_SUPPORT_CHOICES = (
+ ('nested', _('Nest a single Picture / Image plugin inside this plugin.')),
+ # ('direct', _('Choose / Define an image directly within this plugin.')),
+)
+
+
+
+# Helpers
+
+# This field lets us:
+# - (for user) describe how to add media
+# - (for code) identify instances added before media could be directly added
+def create_media_support_field(blank=False):
+ return models.CharField(
+ choices=MEDIA_SUPPORT_CHOICES,
+ verbose_name=_('How to Add an Image'),
+ default=MEDIA_SUPPORT_CHOICES[0][0],
+ blank=blank,
+ max_length=255,
+ )
+
+# Helpers: Field Creation
+# FAQ: Allow fields to be shared between models without creating abstract model
+# NOTE: What every model has could change depending on new page designs…
+
+def create_title_text_field(blank=True):
+ return models.CharField(
+ verbose_name=_('Title'),
+ help_text='The title for the article.',
+ blank=blank,
+ max_length=50,
+ default=''
+ )
+
+def create_abstract_text_field(blank=True):
+ return models.TextField(
+ verbose_name=_('Abstract'),
+ help_text='A summary of the article',
+ blank=blank,
+ default=''
+ )
+
+def create_type_text_field(blank=True):
+ return models.CharField(
+ verbose_name=_('Type'),
+ help_text='The type of the article, ex: "Science News", "Press Release" (manual entry).',
+ blank=blank,
+ max_length=50
+ )
+
+def create_author_text_field(blank=True):
+ return models.CharField(
+ verbose_name=_('Author'),
+ help_text='The author of the article (manual entry).',
+ blank=blank,
+ max_length=50,
+ )
+
+def create_publish_date_field(blank=True, help_text=None, verbose_name=None):
+ return models.DateField(
+ verbose_name=verbose_name
+ if verbose_name
+ else _('Date Published'),
+ # Allocations repurposes this as date when submissions open
+ help_text=help_text + ' Format: YYYY-MM-DD'
+ if help_text
+ else 'The date the article was published (manual entry).',
+ blank=blank,
+ null=True,
+ )
+
+def create_expiry_date_field(blank=True, help_text=None, verbose_name=None):
+ return models.DateField(
+ verbose_name=verbose_name
+ if verbose_name
+ else _('Date to Expire'),
+ # Allocations repurposes this as date when submissions close
+ help_text=help_text + ' Format: YYYY-MM-DD'
+ if help_text
+ else 'The date the article should no longer appear show (manual entry).',
+ blank=blank,
+ null=True,
+ )
+
+
+
+# Models
+
+class TaccsiteStaticNewsArticlePreview(AbstractLink):
+ media_support = create_media_support_field(blank=False)
+ title_text = create_title_text_field(blank=False)
+ abstract_text = create_abstract_text_field(blank=False)
+
+ type_text = create_type_text_field()
+ author_text = create_author_text_field()
+ publish_date = create_publish_date_field()
+
+
+
+ # Parent
+
+ link_is_optional = True
+
+ class Meta:
+ abstract = False
+
+ # Validate
+ def clean(self):
+ clean_for_abstract_link(__class__, self)
+
+class TaccsiteStaticAllocsArticlePreview(AbstractLink):
+ media_support = create_media_support_field(blank=False)
+ title_text = create_title_text_field(blank=False)
+
+ expiry_date = create_expiry_date_field(
+ verbose_name='Submission End Date',
+ help_text='The date after which submissions are not accepted (manual entry).'
+ )
+ publish_date = create_publish_date_field(
+ verbose_name='Submission Start Date',
+ help_text='The date after which submissions are accepted (manual entry).'
+ )
+
+
+
+ # Parent
+
+ link_is_optional = True
+
+ class Meta:
+ abstract = False
+
+ # Validate
+ def clean(self):
+ clean_for_abstract_link(__class__, self)
+
+class TaccsiteStaticDocsArticlePreview(AbstractLink):
+ title_text = create_title_text_field(blank=False)
+ abstract_text = create_abstract_text_field(blank=False)
+
+
+
+ # Parent
+
+ link_is_optional = True
+
+ class Meta:
+ abstract = False
+
+ # Validate
+ def clean(self):
+ clean_for_abstract_link(__class__, self)
+
+class TaccsiteStaticEventsArticlePreview(AbstractLink):
+ title_text = create_title_text_field(blank=False)
+ abstract_text = create_abstract_text_field(blank=False)
+
+ expiry_date = create_expiry_date_field(
+ verbose_name='Event End Date',
+ help_text='The date upon which the event starts (manual entry).'
+ )
+ publish_date = create_publish_date_field(
+ verbose_name='Event Start Date',
+ help_text='The date after which the event ends (manual entry).'
+ )
+
+
+
+ # Parent
+
+ link_is_optional = True
+
+ class Meta:
+ abstract = False
+
+ # Validate
+ def clean(self):
+ clean_for_abstract_link(__class__, self)
+
+ # If user provided link text, then require link
+ if not self.publish_date and not self.expiry_date:
+ end_date_name = force_text(
+ self._meta.get_field('expiry_date').verbose_name )
+ start_date_name = force_text(
+ self._meta.get_field('publish_date').verbose_name )
+ raise ValidationError(
+ _('Provide either a %(start_date)s or an %(end_date)s.') % {
+ 'start_date': start_date_name, 'end_date': end_date_name
+ },
+ code='invalid'
+ )
diff --git a/taccsite_cms/contrib/taccsite_static_article_preview/templates/static_article_preview.html b/taccsite_cms/contrib/taccsite_static_article_preview/templates/static_article_preview.html
new file mode 100644
index 000000000..c0cd6d56c
--- /dev/null
+++ b/taccsite_cms/contrib/taccsite_static_article_preview/templates/static_article_preview.html
@@ -0,0 +1,133 @@
+{% load cms_tags %}
+
+
+ {# Media e.g. image thumbnail #}
+ {% if kind == 'news' or kind == 'allocs' %}
+ {# HACK: Forced to use a wrapper because we cannot control markup #}
+
+ {% for plugin_instance in instance.child_plugin_instances %}
+ {% render_plugin plugin_instance %}
+ {% endfor %}
+
+ {% endif %}
+
+ {# Title #}
+
+
+ {# Abstract #}
+ {% if kind != 'allocs' %}
+
+ {{ instance.abstract_text }}
+
+ {% endif %}
+
+
+
+ {# Metadata #}
+
+
+ {# Metadata: Date #}
+
+ {% if kind == 'news' %}
+
+ Published
+
+ {{ instance.publish_date|date:"F d, Y" }}
+
+
+ {% endif %}
+
+ {% if kind == 'allocs' %}
+
+ {% if open_date and close_date and open_date <= close_date %}
+
+
+ Submission Dates
+
+ {{ open_date|date:"M d, Y" }}
+
+ —
+
+ {{ close_date|date:"M d, Y" }}
+
+
+
+ {% elif should_show_open_date %}
+
+
+
+ {# FAQ: Odd spacing avoids whitespace rendered after text #}
+ {% if open_date_time_period == 'future' %}
+ Submissions Next Open{% else %}Submissions Open Since{% endif %}
+
+ {{ open_date|date:"F d, Y" }}
+
+
+
+ {% elif should_show_close_date %}
+
+
+
+ {# FAQ: Odd spacing avoids whitespace rendered after text #}
+ {% if open_date_time_period == 'future' %}
+ Submissions Deadline{% else %}Submissions Closed{% endif %}
+
+ {{ close_date|date:"F d, Y" }}
+
+
+
+ {% endif %}
+
+ {% endif %}
+
+ {% if kind == 'events' %}
+
+ {# WARNING: Dev Idea: A single day range may show as just a single date #}
+ {% if open_date or open_date == close_date %}
+
+ {{ open_date|date:"m/d/y" }}
+
+ {% endif %}
+ {# WARNING: Dev Idea: A lone close date may start with trailing dash #}
+ {% if close_date and close_date != open_date %}
+ —
+
+ {{ close_date|date:"m/d/y" }}
+
+ {% endif %}
+
+ {% endif %}
+
+
+
+ {# Metadata: Type #}
+ {% if kind == 'news' %}
+ {{ instance.type_text }}
+ {% endif %}
+
+ {# Metadata: Author #}
+ {# FAQ: Not shown in design #}
+ {% comment %}
+ {{ instance.author_text }}
+ {% endcomment %}
+
+
+
+
+
+
diff --git a/taccsite_cms/settings.py b/taccsite_cms/settings.py
index 06a5ee7a1..ce9f3b638 100644
--- a/taccsite_cms/settings.py
+++ b/taccsite_cms/settings.py
@@ -303,8 +303,10 @@ def gettext(s): return s
# FP-1231: Convert our CMS plugins to stand-alone apps
'taccsite_cms.contrib.taccsite_blockquote',
'taccsite_cms.contrib.taccsite_callout',
- 'taccsite_cms.contrib.taccsite_sample',
'taccsite_cms.contrib.taccsite_offset',
+ 'taccsite_cms.contrib.taccsite_sample',
+ 'taccsite_cms.contrib.taccsite_static_article_list',
+ 'taccsite_cms.contrib.taccsite_static_article_preview',
'taccsite_cms.contrib.taccsite_system_specs',
'taccsite_cms.contrib.taccsite_system_monitor',
'taccsite_cms.contrib.taccsite_data_list'
diff --git a/taccsite_cms/static/site_cms/css/src/_imports/components/c-article-list.css b/taccsite_cms/static/site_cms/css/src/_imports/components/c-article-list.css
new file mode 100644
index 000000000..13fc0f125
--- /dev/null
+++ b/taccsite_cms/static/site_cms/css/src/_imports/components/c-article-list.css
@@ -0,0 +1,205 @@
+/*
+Article List
+
+A list of article previews.
+
+Markup:
+
+
+ News
+
+ ...
+ ...
+ ...
+
+
+
+Styleguide Components.ArticleList
+*/
+@import url("_imports/tools/x-truncate.css");
+@import url("_imports/tools/x-layout.css");
+@import url("_imports/tools/x-article-link.css");
+
+
+
+
+
+/* Children */
+
+
+
+.c-article-list--layout-e .c-article-list__item {
+ /* To shrink heading */
+ flex-grow: 1;
+}
+
+
+
+/* Children: Title */
+
+.c-article-list--layout-a .c-article-list__title,
+.c-article-list--layout-b .c-article-list__title,
+.c-article-list--layout-c .c-article-list__title,
+.c-article-list--layout-d .c-article-list__title {
+ /* To span all columns */
+ grid-column-start: 1;
+ grid-column-end: -1;
+}
+
+.c-article-list__title {
+ margin-top: 0; /* overwrite Bootstrap */
+ margin-bottom: 3.0rem; /* overwrite Bootstrap */
+
+ color: var(--global-color-accent--normal);
+
+ font-size: 1.6rem;
+ font-weight: var(--bold);
+ text-transform: uppercase;
+
+ @extend %x-truncate--one-line;
+}
+/* Add a fake short border above title */
+.c-article-list__title {
+ position: relative;
+ padding-top: 1em;
+}
+.c-article-list__title::before {
+ content: '';
+ display: block;
+
+ position: absolute;
+ top: 0;
+ height: 0.5em;
+ width: 2.5em;
+
+ background-color: var(--global-color-accent--normal);
+}
+
+
+
+/* Children: "See More" */
+
+/* Anchor */
+
+.c-article-list--layout-a .c-article-list__footer,
+.c-article-list--layout-b .c-article-list__footer,
+.c-article-list--layout-c .c-article-list__footer,
+.c-article-list--layout-d .c-article-list__footer {
+ /* To span all columns */
+ grid-column-start: 1;
+ grid-column-end: -1;
+}
+
+.c-article-list__footer {
+ border-top-width: var(--global-border-width--thick);
+ border-top-style: solid;
+
+ margin-bottom: -1.0rem; /* to "undo" space added from `padding-bottom` */
+
+ font-size: 1.2rem;
+ font-weight: var(--bold);
+}
+.c-article-list__link {
+ display: inline-block;
+
+ padding-top: 1.0rem;
+ padding-bottom: 1.0rem;
+ padding-right: 1.0rem;
+
+ @extend %x-truncate--one-line;
+ max-width: 100%; /* SEE: https://stackoverflow.com/a/44521595 */
+}
+/* Dark section */
+.o-section--style-dark .c-article-list__footer {
+ border-color: var(--global-color-primary--xx-light);
+}
+.o-section--style-dark .c-article-list__link {
+ color: var(--global-color-primary--xx-light);
+}
+/* Light section */
+.o-section--style-light .c-article-list__footer {
+ border-color: var(--global-color-primary--xx-dark);
+}
+.o-section--style-light .c-article-list__link {
+ color: var(--global-color-primary--xx-dark);
+}
+
+/* Icon */
+
+.c-article-list__link-icon {
+ margin-right: 0.75em;
+
+ font-size: 1.4rem;
+ vertical-align: text-bottom;
+
+ /* To hide the `text-decoration: underline` of the anchor */
+ /* SEE: https://stackoverflow.com/a/15688237/11817077 */
+ display: inline-block;
+}
+
+
+
+
+
+/* Modifiers */
+
+
+
+/* Modifiers: Layout */
+
+.c-article-list--layout-a { @extend %x-layout--a; }
+.c-article-list--layout-b { @extend %x-layout--b; }
+.c-article-list--layout-c { @extend %x-layout--c; }
+.c-article-list--layout-d { @extend %x-layout--d; }
+.c-article-list--layout-e { @extend %x-layout--e; }
+
+/* Modifiers: Layout: Column-Based */
+
+.c-article-list--layout-a,
+.c-article-list--layout-b,
+.c-article-list--layout-c,
+.c-article-list--layout-d {
+ column-gap: 3.0rem; /* GH-99: Use standard spacing value */
+}
+
+/* Modifiers: Layout: Row-Based */
+
+.c-article-list--layout-e {
+ row-gap: 3.0rem; /* GH-99: Use standard spacing value */
+}
+
+
+
+/* Modifiers: Style */
+
+/* Modifiers: Style: Divided */
+
+/* Vertical layout */
+.c-article-list--layout-e.c-article-list--style-divided .c-article-list__item {
+ padding-top: 0.8rem;
+ padding-bottom: 0.8rem;
+
+ border-width: var(--global-border-width--normal) 0 0 0;
+ border-style: solid;
+}
+/* Dark section */
+.o-section--style-dark.c-article-list--style-divided .c-article-list__item,
+.o-section--style-dark .c-article-list--style-divided .c-article-list__item {
+ border-color: var(--global-color-primary--light);
+}
+/* Light section */
+.o-section--style-light.c-article-list--style-divided .c-article-list__item,
+.o-section--style-light .c-article-list--style-divided .c-article-list__item {
+ border-color: var(--global-color-primary--dark);
+}
+
+/* Modifiers: Style: Gapless */
+
+.c-article-list--style-gapless {
+ gap: 0; /* overwrite `column-gap` or `row-gap` */
+}
diff --git a/taccsite_cms/static/site_cms/css/src/_imports/components/c-article-preview.css b/taccsite_cms/static/site_cms/css/src/_imports/components/c-article-preview.css
new file mode 100644
index 000000000..f25b3f52a
--- /dev/null
+++ b/taccsite_cms/static/site_cms/css/src/_imports/components/c-article-preview.css
@@ -0,0 +1,248 @@
+/*
+Article Preview
+
+A preview of an article (to be used in a `c-article-list`). Content __should__ come in the order defined by the example markup.
+
+Markup:
+
+
+
+
+
+ A Long or Short Title of Article
+
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+
+
+
+ July 7
+
+ Science News
+ Wesley B.
+
+
+
+Styleguide Components.ArticlePreview
+*/
+@import url("_imports/tools/x-truncate.css");
+@import url("_imports/tools/x-article-link.css");
+
+
+
+
+
+/* Block */
+
+.c-article-preview {
+ position: relative; /* for absolutely positioned "Children: Link" */
+
+ display: flex;
+ flex-direction: column;
+}
+
+
+
+
+
+/* Children */
+
+
+
+/* Children: Media */
+/* HACK: Forced to style directly because we do not contorl markup */
+
+.c-article-preview__media {
+ order: 1;
+
+ overflow: hidden;
+
+ margin-bottom: 0.8rem; /* overwrite Bootstrap */
+}
+.c-article-preview__media img {
+ /* To center image within container */
+ position: relative;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+
+ /* To ensure super wide or tall image do not have negative space / gaps */
+ width: 100%;
+ object-fit: cover;
+ height: 100%;
+}
+/* News */
+.c-article-preview--news .c-article-preview__media {
+ height: 180px;
+}
+/* Allocations */
+.c-article-preview--allocs .c-article-preview__media {
+ height: 10rem;
+}
+/* Events */
+.c-article-preview--events .c-article-preview__media {
+ display: none;
+}
+
+
+/* Children: Title */
+
+.c-article-preview__title {
+ order: 3;
+
+ margin-top: 0; /* overwrite Bootstrap and browser */
+ margin-bottom: 0; /* overwrite Bootstrap and browser */
+
+ color: inherit;
+ font-weight: var(--bold);
+ line-height: 2.4rem;
+}
+.c-article-preview__title a,
+.c-article-preview__title a:hover,
+.c-article-preview__title a:focus {
+ color: inherit;
+}
+/* News */
+.c-article-preview--news .c-article-preview__title {
+ font-size: 1.8rem;
+}
+/* Allocations */
+.c-article-preview--allocs .c-article-preview__title {
+ font-size: 1.6rem;
+}
+/* Events */
+.c-article-preview--events .c-article-preview__title {
+ font-size: 1.4rem;
+ color: var(--global-color-primary--xx-dark);
+}
+/* Docs */
+.c-article-preview--docs .c-article-preview__title {
+ font-size: 1.4rem;
+ color: var(--global-color-primary--xx-dark);
+}
+
+
+
+/* Children: Abstract */
+
+.c-article-preview__abstract {
+ order: 4;
+
+ margin-bottom: 0; /* overwrite Bootstrap and browser */
+
+ line-height: 2.4rem;
+}
+/* News */
+.c-article-preview--news .c-article-preview__abstract {
+ font-size: 1.6rem;
+}
+/* Allocations */
+.c-article-preview--allocs .c-article-preview__abstract {
+ display: none;
+}
+/* Events */
+.c-article-preview--events .c-article-preview__abstract {
+ font-size: 1.4rem;
+ color: var(--global-color-primary--xx-dark);
+}
+/* Docs */
+.c-article-preview--docs .c-article-preview__abstract {
+ font-size: 1.4rem;
+ color: var(--global-color-primary--xx-dark);
+}
+
+
+
+/* Children: Metadata */
+
+.c-article-preview__metadata {
+ order: 2;
+
+ display: flex;
+ flex-direction: column;
+
+ list-style: none;
+ padding-left: 0; /* overwrite `site.css` and browser */
+
+ margin-bottom: 0; /* overwrite Bootstrap and browser */
+}
+/* Allocations */
+.c-article-preview--allocs .c-article-preview__metadata {
+ order: 5;
+}
+
+/* Children: Metadata: Date */
+
+.c-article-preview__date {
+ order: 2;
+
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+
+ font-weight: var(--medium);
+
+ white-space: pre;
+}
+/* News */
+.c-article-preview--news .c-article-preview__date {
+ margin-bottom: 0.8rem; /* overwrite Bootstrap */
+ font-size: 1.0rem;
+}
+/* Events */
+.c-article-preview--events .c-article-preview__date {
+ font-size: 1.4rem;
+ color: var(--global-color-accent--normal);
+}
+/* Allocations */
+.c-article-preview--allocs .c-article-preview__date {
+ font-size: 1.6rem;
+}
+
+/* Children: Metadata: Type */
+
+.c-article-preview__type {
+ order: 1;
+
+ font-size: 1.2rem;
+ font-weight: var(--bold);
+ text-transform: uppercase;
+}
+/* Events */
+.c-article-preview--events .c-article-preview__type,
+/* Allocations */
+.c-article-preview--allocs .c-article-preview__type {
+ display: none;
+}
+
+/* Children: Metadata: Author */
+
+.c-article-preview__author {
+ order: 3;
+}
+/* News */
+.c-article-preview--news .c-article-preview__author,
+/* Events */
+.c-article-preview--events .c-article-preview__author,
+/* Allocations */
+.c-article-preview--allocs .c-article-preview__author {
+ display: none;
+}
+
+
+
+/* Children: Link */
+
+/* Expand link to cover its container */
+.c-article-preview__link::before {
+ content: '';
+ z-index: 1; /* ensure Link appears over Media */
+
+ color: transparent; /* ensure Link _text_ is invisible (allow decoration) */
+
+ @extend %x-article-link-stretch;
+}
+/* Give link state (pseudo-class) feedback */
+.c-article-preview__link:hover::before {
+ @extend %x-article-link-hover;
+}
diff --git a/taccsite_cms/static/site_cms/css/src/_imports/components/c-date.css b/taccsite_cms/static/site_cms/css/src/_imports/components/c-date.css
new file mode 100644
index 000000000..1f8381e51
--- /dev/null
+++ b/taccsite_cms/static/site_cms/css/src/_imports/components/c-date.css
@@ -0,0 +1,44 @@
+/*
+Date
+
+A date with a label.
+
+Markup:
+
+ Submission Deadline
+
+ June 06, 2020
+
+
+
+Styleguide Components.Date
+*/
+@import url("_imports/tools/x-truncate.css");
+
+/* Container */
+
+dl.c-date {
+ margin: 0; /* overwrite Bootstrap's `_reboot.scss` */
+}
+
+/* Children */
+
+.c-date__label {
+ @extend %x-truncate--one-line;
+}
+.c-date__label::after {
+ content: ':';
+ display: inline;
+ padding-right: 0.25em;
+}
+dt.c-date__label {
+ font-weight: inherit; /* overwrite Bootstrap's `_reboot.scss` */
+}
+
+.c-date__value {
+ white-space: nowrap;
+}
+dd.c-date__value {
+ font-weight: inherit;
+ margin: 0; /* overwrite Bootstrap's `_reboot.scss` */
+}
diff --git a/taccsite_cms/static/site_cms/css/src/_imports/tools/x-article-link.css b/taccsite_cms/static/site_cms/css/src/_imports/tools/x-article-link.css
index 3720f2113..3a1bcd201 100644
--- a/taccsite_cms/static/site_cms/css/src/_imports/tools/x-article-link.css
+++ b/taccsite_cms/static/site_cms/css/src/_imports/tools/x-article-link.css
@@ -4,18 +4,16 @@ Article Link
Styles that allow visible link hover for article lists.
%x-article-link-stretch - Stretch link to cover container
-%x-article-link-stretch--gapless - Make link box fix gapless layout
%x-article-link-hover - Give link a hover state
-%x-article-link-hover--gapless - Make link hover state fix gapless layout
%x-article-link-active - Give link an active (click, enter) state
Styleguide Tools.ExtendsAndMixins.ArticleLink
*/
-/* WARNING: A link ancestor must have its `position` set (not to static) */
+
/* To expand link to cover container */
-.x-article-link-stretch,
+/* CAVEAT: A link ancestor must have its `position` set (not to static) */
%x-article-link-stretch {
position: absolute;
height: 100%;
@@ -28,24 +26,16 @@ Styleguide Tools.ExtendsAndMixins.ArticleLink
/* SEE: http://johndoesdesign.com/blog/2012/css/firefox-and-its-css-focus-outline-bug/ */
overflow: hidden;
}
-.x-article-link-stretch--gapless,
-%x-article-link-stretch--gapless {
- width: calc(100% + 30px); /* GH-99: Use standard spacing value */
- left: -15px;
-}
+
+
/* To give link state (pseudo-class) feedback */
-.x-article-link-hover,
%x-article-link-hover {
outline: 1px solid var(--global-color-accent--normal);
-
- outline-offset: 1em;
-}
-.x-article-link-hover--gapless,
-%x-article-link-hover--gapless {
- outline-offset: 0;
}
+
+
/* To give link active state feedback */
%x-article-link-active {
outline: 1px dotted var(--global-color-accent--normal);
diff --git a/taccsite_cms/static/site_cms/css/src/_imports/tools/x-overlay.css b/taccsite_cms/static/site_cms/css/src/_imports/tools/x-overlay.css
index 8b8df14be..57209b900 100644
--- a/taccsite_cms/static/site_cms/css/src/_imports/tools/x-overlay.css
+++ b/taccsite_cms/static/site_cms/css/src/_imports/tools/x-overlay.css
@@ -9,7 +9,7 @@ Colored boxes that appear atop a large image (like a banner).
Styleguide Tools.ExtendsAndMixins.Overlay
*/
-.x-overlay--curtain {
+%x-overlay--curtain {
--color-text: inherit;
--color-bkgd-rgb: var(--global-color-primary--normal);
@@ -19,7 +19,7 @@ Styleguide Tools.ExtendsAndMixins.Overlay
-webkit-backdrop-filter: blur(6px);
}
-.x-overlay--callout {
+%x-overlay--callout {
--color-text: inherit;
--color-bkgd-rgb: var(--global-color-primary--normal);
diff --git a/taccsite_cms/static/site_cms/css/src/_imports/trumps/s-article-list.css b/taccsite_cms/static/site_cms/css/src/_imports/trumps/s-article-list.css
index e07f7bdda..f9b18482b 100644
--- a/taccsite_cms/static/site_cms/css/src/_imports/trumps/s-article-list.css
+++ b/taccsite_cms/static/site_cms/css/src/_imports/trumps/s-article-list.css
@@ -3,239 +3,52 @@ Article List
A list of article previews. Content __must__ use the tags defined by the example markup.
-Markup: s-article-list.html
+Markup:
+
+ Articles
+ ...
+ ...
+ ...
+
+
Styleguide Trumps.Scopes.ArticleList
*/
-@import url("_imports/tools/x-truncate.css");
-@import url("_imports/tools/x-layout.css");
-@import url("_imports/tools/x-article-link.css");
-
-
-
-
-
-/* Block */
-
-[class*="s-article-list--"] {
- /* … */
-}
-
-
/* Children */
-
-
-/* Children: All */
-
-/* Not "Title" & Not "See More" */
-.s-article-list--layout-e > :not(h2):not(p:last-child) {
- /* To shrink heading */
- flex-grow: 1;
-}
-
-
-
/* Children: Title */
-.s-article-list--layout-a > h2,
-.s-article-list--layout-b > h2,
-.s-article-list--layout-c > h2,
-.s-article-list--layout-d > h2 {
- /* To span all columns */
- grid-column-start: 1;
- grid-column-end: -1;
+.s-article-list .c-article-preview__title {
+ /* FAQ: Article preview truncation differs for Hero Banner */
+ /* SEE: https://github.com/TACC/Core-CMS-Resources/blob/main/frontera-cms/static/frontera-cms/css/src/_imports/trumps/s-home.css#L158-L161 */
+ @extend %x-truncate--one-line;
}
-[class*="s-article-list--"] > h2 {
- margin-top: 0; /* overwrite Bootstrap */
- margin-bottom: 3.0rem; /* overwrite Bootstrap */
-
- color: var(--global-color-accent--normal);
+/* Children: Abstract */
- font-size: 1.6rem;
- font-weight: var(--bold);
- text-transform: uppercase;
-
- @extend .x-truncate--one-line;
-}
-/* Add a fake short border above title */
-[class*="s-article-list--"] > h2 {
- position: relative;
- padding-top: 1em;
+.s-article-list .c-article-preview__abstract {
+ /* FAQ: Article previews may not always truncate many lines */
+ @extend %x-truncate--many-lines;
+ --lines: 3;
}
-[class*="s-article-list--"] > h2::before {
- content: '';
- display: block;
- position: absolute;
- top: 0;
- height: 0.5em;
- width: 2.5em;
+/* Children: Link */
- background-color: var(--global-color-accent--normal);
+.s-article-list:not(.c-article-list--style-gapless)
+ .c-article-preview__link:hover::before {
+ outline-offset: 1rem;
}
-/* Children: "See More" */
-
-/* Anchor */
-
-.s-article-list--layout-a > p:last-child,
-.s-article-list--layout-b > p:last-child,
-.s-article-list--layout-c > p:last-child,
-.s-article-list--layout-d > p:last-child {
- /* To span all columns */
- grid-column-start: 1;
- grid-column-end: -1;
-}
-
-[class*="s-article-list--"] > p:last-child {
- border-top-width: var(--global-border-width--thick);
- border-top-style: solid;
-
- margin-top: 3.0rem; /* GH-99: Use standard spacing value */
- margin-bottom: -1.0rem; /* to "undo" space added from `padding-bottom` */
-
- font-size: 1.2rem;
- font-weight: var(--bold);
-}
-[class*="s-article-list--"] > p:last-child a {
- display: inline-block;
-
- padding-top: 1.0rem;
- padding-bottom: 1.0rem;
- padding-right: 1.0rem;
-
- @extend .x-truncate--one-line;
- max-width: 100%; /* SEE: https://stackoverflow.com/a/44521595 */
-}
-/* Dark section */
-.o-section--style-dark[class*="s-article-list--"] > p:last-child,
-.o-section--style-dark [class*="s-article-list--"] > p:last-child {
- border-color: var(--global-color-primary--xx-light);
-}
-.o-section--style-dark[class*="s-article-list--"] > p:last-child a,
-.o-section--style-dark [class*="s-article-list--"] > p:last-child a {
- color: var(--global-color-primary--xx-light);
-}
-/* Light section */
-.o-section--style-light[class*="s-article-list--"] > p:last-child,
-.o-section--style-light [class*="s-article-list--"] > p:last-child {
- border-color: var(--global-color-primary--xx-dark);
-}
-.o-section--style-light[class*="s-article-list--"] > p:last-child a,
-.o-section--style-light [class*="s-article-list--"] > p:last-child a {
- color: var(--global-color-primary--xx-dark);
-}
-
-/* Icon */
-
-[class*="s-article-list--"] > p:last-child a::before {
- font-family: "Font Awesome 5 Free";
- content: "\f35a";
- margin-right: 10px;
-
- font-size: 1.4rem;
- vertical-align: middle;
-
- /* To hide the `text-decoration: underline` of the anchor */
- /* SEE: https://stackoverflow.com/a/15688237/11817077 */
- display: inline-block;
-}
-
-
-
-
-
/* Modifiers */
+/* Modifiers: Docs */
-
-/* Modifiers: Links */
-
-.s-article-list--links {
- font-size: 1.4rem;
- color: var(--global-color-primary--xx-dark);
-}
-.s-article-list--links p:not(:last-child) {
- margin: 0; /* Overwrite Bootstrap and browser */
-}
-.s-article-list--links p:not(:last-child) a {
- font-weight: var(--bold);
- color: var(--global-color-primary--xx-dark);
-}
-
-/* Expand link to cover its container */
-.s-article-list--links p:not(:last-child) { position: relative; }
-.s-article-list--links p:not(:last-child) a::before {
- content: '';
-
- @extend .x-article-link-stretch;
-}
-.s-article-list--layout-gapless.s-article-list--links p:not(:last-child) a::before {
- @extend .x-article-link-stretch--gapless;
-}
-/* Give link state (pseudo-class) feedback */
-.s-article-list--links p:not(:last-child) a:hover::before {
- @extend .x-article-link-hover;
-}
-.s-article-list--layout-gapless.s-article-list--links p:not(:last-child) a:hover::before {
- @extend .x-article-link-hover--gapless;
-}
-
-
-
-/* Modifiers: Layout */
-
-.s-article-list--layout-a { @extend .x-layout--a; }
-.s-article-list--layout-b { @extend .x-layout--b; }
-.s-article-list--layout-c { @extend .x-layout--c; }
-.s-article-list--layout-d { @extend .x-layout--d; }
-.s-article-list--layout-e { @extend .x-layout--e; }
-
-/* Modifiers: Layout: Column-Based */
-
-.s-article-list--layout-a,
-.s-article-list--layout-b,
-.s-article-list--layout-c,
-.s-article-list--layout-d {
- column-gap: 3.0rem; /* GH-99: Use standard spacing value */
-}
-
-/* Modifiers: Layout: Row-Based */
-
-.s-article-list--layout-e {
- /* … */
-}
-
-/* Modifiers: Layout: Options */
-
-.s-article-list--layout-gapless {
- gap: 0;
-}
-
-.s-article-list--layout-compact > p:last-child {
- margin-top: 0;
-}
-
-.s-article-list--layout-divided > :not(h2):not(p:last-child) {
- padding-top: 0.8rem;
-
- border-width: var(--global-border-width--normal) 0 0;
- border-style: solid;
-}
-/* Dark section */
-.o-section--style-dark.s-article-list--layout-divided > :not(h2):not(p:last-child),
-.o-section--style-dark .s-article-list--layout-divided > :not(h2):not(p:last-child) {
- border-color: var(--global-color-primary--light);
-}
-/* Light section */
-.o-section--style-light.s-article-list--layout-divided > :not(h2):not(p:last-child),
-.o-section--style-light .s-article-list--layout-divided > :not(h2):not(p:last-child) {
- border-color: var(--global-color-primary--dark);
+.s-article-list .c-article-preview--docs:last-of-type {
+ /* HACK: Force links to be a little closer together (match design) */
+ margin-bottom: 3.0rem;
}
diff --git a/taccsite_cms/static/site_cms/css/src/_imports/trumps/s-article-list.html b/taccsite_cms/static/site_cms/css/src/_imports/trumps/s-article-list.html
deleted file mode 100644
index 50608ad90..000000000
--- a/taccsite_cms/static/site_cms/css/src/_imports/trumps/s-article-list.html
+++ /dev/null
@@ -1,20 +0,0 @@
-
-
- News & Stuff
-
-
- Article Placeholder
-
-
-
-
-
diff --git a/taccsite_cms/static/site_cms/css/src/_imports/trumps/s-article-preview.css b/taccsite_cms/static/site_cms/css/src/_imports/trumps/s-article-preview.css
deleted file mode 100644
index 7fa66a883..000000000
--- a/taccsite_cms/static/site_cms/css/src/_imports/trumps/s-article-preview.css
+++ /dev/null
@@ -1,253 +0,0 @@
-/*
-Article Preview
-
-A preview of an article (to be used in a `s-article-list`). Content __must__ come in the order and use the tags defined by the example markup.
-
-Markup: s-article-preview.html
-
-Styleguide Trumps.Scopes.ArticlePreview
-*/
-@import url("_imports/tools/x-truncate.css");
-@import url("_imports/tools/x-article-link.css");
-
-
-
-
-
-/* Block */
-
-.s-article-preview {
- position: relative; /* for absolutely positioned "Children: Link" */
-
- display: flex;
- flex-direction: column;
-}
-
-
-
-
-
-/* Children */
-
-
-
-/* Children: Media */
-
-.s-article-preview p:first-child {
- order: 1;
-
- overflow: hidden;
-
- margin-bottom: 0.8rem; /* overwrite Bootstrap */
-}
-.s-article-preview p:first-child > img {
- /* To center image within container */
- position: relative;
- top: 50%;
- left: 50%;
- transform: translate(-50%, -50%);
-}
-.s-article-preview p:first-child > img.img-fluid {
- /* To ensure super wide or tall image do not have negative space / gaps */
- width: 100%;
- object-fit: cover;
- height: 100%; /* overwrite `.img-fluid` *//* NOTE: Sould this be standard? */
-}
-/* (List) News */
-.s-article-list--news .s-article-preview p:first-child {
- height: 180px;
-}
-/* (List) Allocations */
-.s-article-list--allocations .s-article-preview p:first-child {
- height: 10.0rem;
-}
-/* (List) Events */
-.s-article-list--events .s-article-preview p:first-child {
- display: none;
-}
-
-
-/* Children: Title */
-
-.s-article-preview h3 {
- order: 3;
-
- margin-top: 0; /* overwrite Bootstrap and browser */
- margin-bottom: 0.8rem; /* overwrite Bootstrap and browser */
-
- font-size: 1.8rem;
- font-weight: var(--bold);
- line-height: 2.4rem;
-}
-/* (List) */
-[class*="s-article-list--"] .s-article-preview h3 {
- @extend %x-truncate--one-line;
-}
-/* (List) Allocations */
-.s-article-list--allocations .s-article-preview h3 {
- font-size: 1.6rem;
- font-weight: var(--bold);
-}
-/* (List) Events */
-.s-article-list--events .s-article-preview h3 {
- font-size: 1.4rem;
- color: var(--global-color-primary--xx-dark);
-}
-
-
-
-/* Children: Abstract */
-
-.s-article-preview p:not(:first-child):not(:last-child) {
- order: 4;
-
- margin-bottom: 0; /* overwrite Bootstrap and browser */
-
- font-size: 1.6rem;
- line-height: 2.4rem;
-}
-/* (List) */
-[class*="s-article-list--"] .s-article-preview p:not(:first-child):not(:last-child) {
- @extend %x-truncate--many-lines;
- --lines: 3;
-}
-/* (List) Allocations */
-.s-article-list--allocations .s-article-preview p:not(:first-child):not(:last-child) {
- display: none;
-}
-/* (List) Events */
-.s-article-list--events .s-article-preview p:not(:first-child):not(:last-child) {
- font-size: 1.4rem;
- color: var(--global-color-primary--xx-dark);
-}
-
-
-
-/* Children: Metadata */
-
-.s-article-preview ul {
- order: 2;
-
- display: flex;
- flex-direction: column;
-
- list-style: none;
- padding-left: 0; /* overwrite `site.css` and browser */
-
- margin-bottom: 0.8rem; /* overwrite Bootstrap */
-}
-/* (List) Allocations */
-.s-article-list--allocations .s-article-preview ul {
- order: 5;
-}
-
-/* Children: Metadata: Date */
-
-.s-article-preview ul > li:nth-child(1) {
- order: 2;
-
- font-weight: var(--medium);
-
- display: flex;
- flex-direction: row;
- flex-wrap: wrap;
-}
-/* (List) News */
-.s-article-list--news .s-article-preview ul > li:nth-child(1) {
- margin-bottom: 0.8rem; /* overwrite Bootstrap */
- font-size: 1.0rem;
-}
-.s-article-list--news .s-article-preview ul > li:nth-child(1)::before {
- content: 'Published: ';
- white-space: pre;
-}
-/* (List) Events */
-.s-article-list--events .s-article-preview ul > li:nth-child(1) {
- font-size: 1.4rem;
- color: var(--global-color-accent--normal);
-}
-/* (List) Allocations */
-.s-article-list--allocations .s-article-preview ul > li:nth-child(1) {
- font-size: 1.6rem;
-}
-.s-article-list--allocations .s-article-preview ul > li:nth-child(1)::before {
- content: 'Submission Deadlines: ';
- white-space: pre;
-}
-
-/* Children: Metadata: Type */
-
-.s-article-preview ul > li:nth-child(2) {
- order: 1;
-
- font-size: 1.2rem;
- font-weight: var(--bold);
- text-transform: uppercase;
-}
-/* (List) Events */
-.s-article-list--events .s-article-preview ul > li:nth-child(2),
-/* (List) Allocations */
-.s-article-list--allocations .s-article-preview ul > li:nth-child(2) {
- display: none;
-}
-
-/* Children: Metadata: Author */
-
-.s-article-preview ul > li:nth-child(3) {
- order: 3;
-}
-/* (List) News */
-.s-article-list--news .s-article-preview ul > li:nth-child(3),
-/* (List) Events */
-.s-article-list--events .s-article-preview ul > li:nth-child(3),
-/* (List) Allocations */
-.s-article-list--allocations .s-article-preview ul > li:nth-child(3) {
- display: none;
-}
-
-
-
-/* Children: Link */
-
-.s-article-preview p:last-child {
- margin-bottom: 0; /* overwite Bootstrap and browser */
-}
-
-/* Expand link to cover its container */
-.s-article-preview p:last-child {
- z-index: 1; /* ensure Link appears over Media */
-}
-.s-article-preview p:last-child > a {
- color: transparent; /* ensure Link _text_ is invisible (allow decoration) */
-
- @extend .x-article-link-stretch;
-}
-.s-article-list--layout-gapless .s-article-preview p:last-child > a {
- @extend .x-article-link-stretch--gapless;
-}
-/* Give link state (pseudo-class) feedback */
-.s-article-preview p:last-child > a:hover {
- @extend .x-article-link-hover;
-}
-.s-article-list--layout-gapless .s-article-preview p:last-child > a:hover {
- @extend .x-article-link-hover--gapless;
-}
-
-
-
-
-
-/* Modifiers */
-
-
-
-/* Modifiers: (List) News, Allocations, Evetns, etc. */
-/* SEE: All "Children" styles */
-
-
-
-/* Modifiers: (List) Layout: Options */
-
-.s-article-list--layout-compact .s-article-preview > * {
- margin-bottom: 0; /* overwrite `.s-article-preview > …` */
-}
diff --git a/taccsite_cms/static/site_cms/css/src/_imports/trumps/s-article-preview.html b/taccsite_cms/static/site_cms/css/src/_imports/trumps/s-article-preview.html
deleted file mode 100644
index dcea7f35f..000000000
--- a/taccsite_cms/static/site_cms/css/src/_imports/trumps/s-article-preview.html
+++ /dev/null
@@ -1,30 +0,0 @@
-
-
-
- A Long or Short Title of Article
-
-
- Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
-
-
- July 7
- Science News
- Wesley Bomar
-
-
-
-
-
diff --git a/taccsite_cms/static/site_cms/css/src/site.css b/taccsite_cms/static/site_cms/css/src/site.css
index a19f3d008..d4f407b82 100644
--- a/taccsite_cms/static/site_cms/css/src/site.css
+++ b/taccsite_cms/static/site_cms/css/src/site.css
@@ -28,7 +28,10 @@
/* COMPONENTS */
/* GH-302: HELP: How should all of these become individually built files? */
/* GH-302: FAQ: Individually built stylesheets could be loaded explicitely. */
+@import url("_imports/components/c-article-list.css");
+@import url("_imports/components/c-article-preview.css");
@import url("_imports/components/c-callout.css");
+@import url("_imports/components/c-date.css");
@import url("_imports/components/c-data-list.css");
@import url("_imports/components/c-footer.css");
@import url("_imports/components/c-see-all-link.css");
@@ -38,6 +41,7 @@
@import url("_imports/components/bootstrap.container.css");
/* TRUMPS */
+@import url("_imports/trumps/s-article-list.css");
@import url("_imports/trumps/s-breadcrumbs.css");
@import url("_imports/trumps/s-footer.css");
@import url("_imports/trumps/s-blockquote.css");
diff --git a/taccsite_custom b/taccsite_custom
index 1cf4c8a0d..1f19f64a1 160000
--- a/taccsite_custom
+++ b/taccsite_custom
@@ -1 +1 @@
-Subproject commit 1cf4c8a0d26910e64730a3edb533d07a85ea7e3d
+Subproject commit 1f19f64a166b08d96915c9440b6d6e0285541192