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 #} +

+ + {{ instance.title_text }} + +

+ + {# Abstract #} + {% if kind != 'allocs' %} +

+ {{ instance.abstract_text }} +

+ {% endif %} + + + + {# Metadata #} + + + + +
diff --git a/taccsite_cms/settings.py b/taccsite_cms/settings.py index 91b2dea69..bfe802585 100644 --- a/taccsite_cms/settings.py +++ b/taccsite_cms/settings.py @@ -306,8 +306,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. +

+ +
+ +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
+
+ +
+
+ +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/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 cbe36e415..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); - color: inherit; -} -/* (List) Events */ -.s-article-list--events .s-article-preview h3 { - font-size: 1.4rem; -} - - - -/* 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. -

- -
- - - 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 28193dade..64e25a1df 160000 --- a/taccsite_custom +++ b/taccsite_custom @@ -1 +1 @@ -Subproject commit 28193dadeb55ca243b172a85621bc935d7a6ee38 +Subproject commit 64e25a1df27968abeeefee0774f4bdbd8f3d72a1