Skip to content
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
e2f7c53
feat: #999 only cms admin edit static placeholder
wesleyboar Feb 11, 2026
f52b8b2
feat: #999 add 'header-logo' placeholder
wesleyboar Feb 11, 2026
ead8569
refactor: simplify code and format docstring
wesleyboar Feb 11, 2026
53cfef6
refactor!: header-logo → header-content
wesleyboar Mar 5, 2026
e1d19d9
Merge branch 'main' into feat/GH-999-let-cms-admin-edit-header
wesleyboar Mar 5, 2026
9fef1c3
Merge branch 'main' into feat/GH-999-let-cms-admin-edit-header
wesleyboar Apr 2, 2026
7459528
docs: heaader_logo comments
wesleyboar Apr 2, 2026
d156af6
refactor: legible header content parsing logic
wesleyboar Apr 2, 2026
d5b2a36
fix: GH-999 CMS admin can change header logo (#1142)
wesleyboar Apr 21, 2026
c7f4454
Merge branch 'main' into feat/GH-999-let-cms-admin-edit-header
wesleyboar Jun 4, 2026
15a1c2d
docs(gh-999): add editable header implementation plan
wesleyboar Jun 4, 2026
053fdd7
feat: GH-999 CMS admin can change header logo - via plugin (#1171)
wesleyboar Jun 11, 2026
79f14b3
chore: delete cruft
wesleyboar Jun 11, 2026
c42f211
Merge branch 'main' into feat/GH-999-let-cms-admin-edit-header
wesleyboar Jun 11, 2026
15fc05c
feat(gh-999): default Header logo form values on add (#1183)
wesleyboar Jun 16, 2026
07c0c2a
fix(gh-999): align header logo markup with settings path (#1184)
wesleyboar Jun 17, 2026
27fa19d
feat(gh-999): default Header logo alt text on add
wesleyboar Jun 17, 2026
96ed5cd
fix(gh-999): restrict Header logo plugin to header-content
wesleyboar Jun 17, 2026
7070552
Merge branch 'main' into feat/GH-999-let-cms-admin-edit-header
wesleyboar Jun 17, 2026
3ee0438
refactor(gh-999): centralize header-content placeholder slot
wesleyboar Jun 17, 2026
fc6bb82
chore: whitespace
wesleyboar Jun 17, 2026
1937592
chore: plugin order (logo plugin not deprecated)
wesleyboar Jun 17, 2026
fc7eb38
fix: revert settings test change
wesleyboar Jun 17, 2026
0d4fd91
refactor: rename a constant
wesleyboar Jun 17, 2026
42a801a
docs: gh-999
wesleyboar Jun 17, 2026
c85cc71
feat(gh-999): name static placeholders in Structure sidebar
wesleyboar Jun 17, 2026
2cabc1c
chore: whitespace
wesleyboar Jun 17, 2026
e56e434
Merge branch 'main' into feat/GH-999-let-cms-admin-edit-header
wesleyboar Jun 17, 2026
b737a87
Merge branches 'feat/GH-999-let-cms-admin-edit-header' and 'feat/GH-9…
wesleyboar Jun 17, 2026
34988a7
deps: @tacc/core-styles v2.57.1
wesleyboar Jun 17, 2026
440ceba
deps: @tacc/core-styles v2.57.2
wesleyboar Jun 18, 2026
606a0eb
docs(GH-999): document img-only header logo CMS limitation
wesleyboar Jun 18, 2026
559dbf7
fix(GH-1185): apply portal-logo class for img-only header logos
wesleyboar Jun 18, 2026
9beaf61
docs: clean up "Known Limitations"
wesleyboar Jun 18, 2026
a4f1a43
Merge branch 'fix/GH-1185-header-logo-portal-logo-class' of github.co…
wesleyboar Jun 18, 2026
1d01d08
chore: reduce diff
wesleyboar Jun 18, 2026
9ff3bc2
fix: expected plugins in header logo placeholder
wesleyboar Jun 18, 2026
958e340
fix: lost `portal-logo` class
wesleyboar Jun 18, 2026
74241ec
chore: delete outdated tests
wesleyboar Jun 18, 2026
56931f1
refactor: rename plugin
wesleyboar Jun 18, 2026
f878b60
chore(test): revert unused _FakePicture attributes_str helper
wesleyboar Jun 18, 2026
97d3382
Merge branch 'main' into feat/GH-999-let-cms-admin-edit-header
wesleyboar Jun 18, 2026
d8d6114
refactor(gh-999): simplify header logo rendering (#1189)
wesleyboar Jun 19, 2026
7450f3f
refactor: rename a const
wesleyboar Jun 19, 2026
a6b9209
refactor: rename a const
wesleyboar Jun 19, 2026
4c79ddf
docs: clarity
wesleyboar Jun 19, 2026
cead748
docs: remove cruft
wesleyboar Jun 19, 2026
950ed99
fix(header-logo): always set id on Header logo plugin markup
wesleyboar Jun 19, 2026
d361e90
Merge branch 'main' into feat/GH-999-let-cms-admin-edit-header
wesleyboar Jul 1, 2026
1d96a54
Merge branch 'main' into feat/GH-999-let-cms-admin-edit-header
wesleyboar Jul 1, 2026
39a8386
Merge branch 'main' into feat/GH-999-let-cms-admin-edit-header
wesleyboar Jul 3, 2026
b41b1a6
Merge branch 'main' into feat/GH-999-let-cms-admin-edit-header
wesleyboar Jul 21, 2026
791d412
Merge branch 'main' into feat/GH-999-let-cms-admin-edit-header
wesleyboar Jul 27, 2026
0d31361
Merge branch 'main' into feat/GH-999-let-cms-admin-edit-header
wesleyboar Jul 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions docs/gh-999-editable-header-plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# GH-999: Editable header (`header-content`)

Plan for [#999 Let CMS Admin Edit Header](https://github.com/TACC/Core-CMS/issues/999) and stacked work on [`feat/GH-999-let-cms-admin-edit-header`](https://github.com/TACC/Core-CMS/pull/1083).

## Goals

| Priority | Item | This stack |
| --- | --- | --- |
| 1 | CMS-editable logo | [#1083](https://github.com/TACC/Core-CMS/pull/1083) (current) |
| 2 | Nav collapse breakpoint | Follow-up issue (not this stack) |
| 3 | Custom branding | PR 3 (TACC Header Branding plugin) |
| 4–6 | Portal nav, search, branding order | Later thin plugins / options |

**Backwards compatibility:** If `header-content` has no plugins, render the header exactly as today (settings-driven branding + logo + nav).

**Precedence:** CMS plugin wins when present; otherwise settings (`LOGO` / `PORTAL_LOGO`, `BRANDING` / `PORTAL_BRANDING`).

## Portal logo (HeaderLogoPlugin)

- **One plugin:** [`HeaderLogoPlugin`](taccsite_cms/contrib/taccsite_header_logo/cms_plugins.py) extends Picture (same model, no migration). Toolbar: **TACC Header → Header logo** — full Picture fields/templates; default `id="header-logo"` when Attributes omit `id`.
- **Render:** Standard Picture template at `{% static_placeholder "header-content" %}`.
- **Empty placeholder:** `or` fallback [`header_logo_via_settings.html`](taccsite_cms/templates/header_logo_via_settings.html) (`LOGO` / `PORTAL_LOGO`).
- **Branding images (PR 3):** nested Pictures under TACC Header Branding — not top-level.

## PR stack

1. **#1083 → `main`:** `header-content` static placeholder, superuser-only edit, settings fallback.
2. **PR 1.75** (base: feature branch): `HeaderLogoPlugin` — correct markup + CMS edit UX (targets #1083 branch).
3. **PR 2** (base: feature branch): Full-header orchestrator (`header_nav.html`, etc.).
4. **PR 3** (base: after PR 2): TACC Header Branding plugin; add to `CMS_PLACEHOLDER_CONF`.

After #1083 merges, rebase the feature branch onto `main` before the stack lands on `main`.

## Template files

Portal/Guide repos duplicate header markup ([Confluence](https://confluence.tacc.utexas.edu/x/LoCnCQ)). **`base.html` keeps `{% include "header.html" %}` only.**

| File | Role |
| --- | --- |
| [`header.html`](../taccsite_cms/templates/header.html) | Branding + nav; full `{% static_placeholder "header-content" or %}` + settings fallback. |
| [`header_logo.html`](../taccsite_cms/templates/header_logo.html) | Core-Portal `/cms/header/logo/markup/` — logo-only via [`render_header_logo`](../taccsite_cms/templatetags/header_tags.py) or settings. |
| [`header_logo_via_settings.html`](../taccsite_cms/templates/header_logo_via_settings.html) | Settings-only logo (`or` fallback). |
| [`render.py`](../taccsite_cms/contrib/taccsite_header_logo/render.py) | `render` — published logo plugin only (reuse in PR 2). |
| `header_nav.html` | **PR 2** — extract `<nav>…</nav>`. |

## Testing

### #1083 + PR 1.75

1. **Backwards compatibility:** Empty `header-content` → settings logo; branding/nav unchanged.
2. **Superuser — logo:** Add **Header logo** plugin (image + optional link). Publish. Nav shows custom logo; placeholder still editable in `?edit`.
3. **Superuser — footer:** `footer-content` unchanged.
4. **Non-superuser:** Cannot edit static placeholders.
5. **Limits:** One `HeaderLogoPlugin` in `header-content` (`global` 2 reserved for PR 3).
6. **Core-Portal markup URL:** `/cms/header/logo/markup/` uses published `header-content` logo plugin or settings (not draft/edit toolbar).
7. **Permissions after deploy:** Group perms; re-check step 4.

### PR 2 (orchestrator)

1. Empty placeholder → same as today.
2. Header logo plugin → full nav + settings branding.
3. Edit UX for `header-content` at header level.

### PR 3 (branding plugin)

1. Branding plugin + header logo → both slots; settings branding hidden when plugin present.
26 changes: 26 additions & 0 deletions taccsite_cms/contrib/taccsite_header_logo/cms_plugins.py

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Create "Header logo" plugin (extends "Picture" plugin) so we can set defaults1.

Footnotes

  1. Defaults are set in another file, forms.py.

Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from cms.plugin_pool import plugin_pool
from django.utils.translation import gettext_lazy as _

from djangocms_picture.cms_plugins import PicturePlugin
from djangocms_picture.models import Picture

HEADER_LOGO_ELEMENT_ID = 'header-logo'


@plugin_pool.register_plugin
class HeaderLogoPlugin(PicturePlugin):
"""
Header > "Header logo" plugin

Full Picture plugin; default id="header-logo" when not set in Attributes.
"""
model = Picture
module = _('TACC Header')
name = _('Header logo')

def render(self, context, instance, placeholder):
if not instance.attributes:
instance.attributes = {}
if 'id' not in instance.attributes:
instance.attributes['id'] = HEADER_LOGO_ELEMENT_ID
return super().render(context, instance, placeholder)
29 changes: 29 additions & 0 deletions taccsite_cms/contrib/taccsite_header_logo/render.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from cms.models import StaticPlaceholder
from cms.plugin_rendering import ContentRenderer

from taccsite_cms.contrib.taccsite_header_logo.cms_plugins import HeaderLogoPlugin


def render(request, context):
"""
Render published HeaderLogoPlugin from header-content (logo-only fragment).
"""
static_ph = StaticPlaceholder.objects.filter(code='header-content').first()
if not static_ph:
return ''

placeholder = static_ph.public
placeholder.is_static = True

renderer = ContentRenderer(request)
language = renderer.request_language

for plugin in renderer.get_plugins_to_render(placeholder, language, template=None):
if plugin.plugin_type == HeaderLogoPlugin.__name__:
return renderer.render_plugin(
plugin,
context,
placeholder,
editable=False,
)
return ''
31 changes: 28 additions & 3 deletions taccsite_cms/management/commands/util.py

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To no longer let basic CMS editor edit placeholders.1

Footnotes

  1. I auto-added this permission before I realized I should not.

Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,25 @@ def add_perm(group, app_label, model_name, perm_name):
else:
group.permissions.add( Permission.objects.get( name=perm_name ))

def del_perm(group, app_label, model_name, perm_name):
"""
Delete specific permission from a given group
"""
logger.debug(f'Removing permission ({app_label}.{model_name}) "{perm_name}"')
if app_label and model_name:
model = model_name.lower().replace(' ', '')
content_type = ContentType.objects.get(
app_label=app_label,
model=model
)
group.permissions.remove(
Permission.objects.get(
name=perm_name,
content_type=content_type
)
)
else:
group.permissions.remove(Permission.objects.get(name=perm_name))


# Page
Expand All @@ -44,9 +63,15 @@ def let_view_page_and_structure(group):
add_perm(group, 'cms', 'page', 'Can change page')

add_perm(group, 'cms', 'placeholder', 'Can use Structure mode')
# HELP: Not necessary on TACC (as of Core-CMS v4.17.1)
# Is necessary on WTCS (as of Core-CMS v4.20.2)
add_perm(group, 'cms', 'static placeholder', 'Can change static placeholder')
# To delete undesired permission from sites that still have it:
# ```py
# add_perm(group, 'cms', 'static placeholder', 'Can change static placeholder')
# ```
# HELP: Should "Sitewide Content Manager" keep this perm?
# SEE: https://weteachcs.org/admin/auth/group/9/change/
# FAQ: Only superuser may edit static placeholders (footer, header-content)
# TODO: After this is deployed on all sites once, delete this code
del_perm(group, 'cms', 'static placeholder', 'Can change static placeholder')



Expand Down
16 changes: 13 additions & 3 deletions taccsite_cms/settings/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -499,7 +499,8 @@ def gettext(s): return s
'taccsite_cms.contrib.taccsite_sample',
'taccsite_cms.contrib.taccsite_offset',
'taccsite_cms.contrib.taccsite_system_specs',
'taccsite_cms.contrib.taccsite_data_list'
'taccsite_cms.contrib.taccsite_data_list',
'taccsite_cms.contrib.taccsite_header_logo',
]

# Convert list of paths to list of dotted module names
Expand Down Expand Up @@ -564,7 +565,16 @@ def get_subdirs_as_module_names(path):
}

CMS_PERMISSION = True
CMS_PLACEHOLDER_CONF = {}
CMS_PLACEHOLDER_CONF = {
'header-content': {
# HeaderLogoPlugin (PR 1.75); room for TaccHeaderBrandingPlugin (PR 3)
Comment thread
wesleyboar marked this conversation as resolved.
Outdated
'plugins': ['HeaderLogoPlugin'],
'limits': {
'global': 2,
'HeaderLogoPlugin': 1,
},
},
Comment on lines +571 to +581

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Name the placeholder and limit its content to a header logo.

}

THUMBNAIL_HIGH_RESOLUTION = True
THUMBNAIL_PROCESSORS = (
Expand Down Expand Up @@ -673,7 +683,7 @@ def get_subdirs_as_module_names(path):
if 'BRANDING' not in locals():
BRANDING = False

# For header_logo.html
# For header_logo_via_settings.html (logo slot fallback)
Comment thread
wesleyboar marked this conversation as resolved.
Outdated
deprecated_SETTINGS_EXPORT += ['LOGO']
if 'LOGO' not in locals():
LOGO = False
Expand Down
6 changes: 4 additions & 2 deletions taccsite_cms/templates/header.html

@wesleyboar wesleyboar Jun 23, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The keystone code is in this file.

Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{# @var settings #}
{% load cms_tags %}

{# WARNING: Some markup is duplicated in other repositories #}
{# SEE: https://confluence.tacc.utexas.edu/x/LoCnCQ #}
Expand All @@ -22,7 +23,9 @@
{% endif %}
">
<!-- Portal Logo -->
{% include "header_logo.html" %}
{% static_placeholder "header-content" or %}
{% include "header_logo_via_settings.html" %}
{% endstatic_placeholder %}

<!-- Navbar Accordian Toggle on Small Screens -->
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarsExpandTarget" aria-controls="navbarsExpandTarget" aria-expanded="false" aria-label="Toggle navigation">
Expand All @@ -44,7 +47,6 @@
{% endif %}
</div>
</nav>

{% endwith %}
{% endwith %}
{% endwith %}
44 changes: 5 additions & 39 deletions taccsite_cms/templates/header_logo.html

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not as big a change a it looks. I basically moved it to ./header_logo_via_settings.html which this conditionally calls.

what file looks like, sans diff
{% load header_tags %}

{% render_header_logo as header_logo %}
{% if header_logo %}
  {{ header_logo }}
{% else %}
  {% include "header_logo_via_settings.html" %}
{% endif %}

Original file line number Diff line number Diff line change
@@ -1,41 +1,7 @@
{# @var logo, className #}
{% load static custom_portal_settings %}

{% if settings.LOGO %}

{% with settings.LOGO as logo %}
{% with filename=logo|index:1 selectors=logo|index:2 targeturl=logo|index:3 targettype=logo|index:4 accessibility=logo|index:5 corstype=logo|index:6 visibility=logo|index:7 %}
{% if visibility == "True" %}
<a id="header-logo" class="navbar-brand {{className}}" href="{{ targeturl }}" target="{{ targettype }}">
<img class="portal-logo {{ selectors }}" src="{% static filename %}" crossorigin="{{ corstype }}" alt="{{ accessibility }}" />
</a>
{% endif %}
{% endwith %}
{% endwith %}

{% load header_tags %}
{% render_header_logo as header_logo %}
{% if header_logo %}
{{ header_logo }}
{% else %}

{% with settings.PORTAL_LOGO as logo %}
<a
id="header-logo"
class="navbar-brand {{className}}"
href="{{ logo.link_href }}"
target="{{ logo.link_target }}"
{% if logo.link_name %}
aria-label="{{ logo.link_name }}"
{% endif %}
>
<img
class="portal-logo {{ logo.img_class }}"
src="{% if logo.is_remote %}{{ logo.img_file_src }}{% else %}{% static logo.img_file_src %}{% endif %}"
{% if logo.img_crossorigin %}
crossorigin="{{ logo.img_crossorigin }}"
{% endif %}
{% if logo.img_alt_text %}
alt="{{ logo.img_alt_text }}"
{% endif %}
/>
</a>
{% endwith %}

{% include "header_logo_via_settings.html" %}
{% endif %}
45 changes: 45 additions & 0 deletions taccsite_cms/templates/header_logo_via_settings.html

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just copied over taccsite_cms/templates/header_logo.html which itself conditionally calls this.

Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{# @var className #}
{% load static custom_portal_settings %}

{% if settings.LOGO %}

{# via deprecated CMS setting (e.g. CMS developer sets logo image) #}
{% with settings.LOGO as logo %}
{% with filename=logo|index:1 selectors=logo|index:2 targeturl=logo|index:3 targettype=logo|index:4 accessibility=logo|index:5 corstype=logo|index:6 visibility=logo|index:7 %}
{% if visibility == "True" %}
<a id="header-logo" class="navbar-brand {{className}}" href="{{ targeturl }}" target="{{ targettype }}">
<img class="portal-logo {{ selectors }}" src="{% static filename %}" crossorigin="{{ corstype }}" alt="{{ accessibility }}" />
</a>
{% endif %}
{% endwith %}
{% endwith %}

{% elif settings.PORTAL_LOGO %}

{# via CMS setting (e.g. CMS developer sets logo image) #}
{% with settings.PORTAL_LOGO as logo %}
<a
id="header-logo"
class="navbar-brand {{className}}"
href="{{ logo.link_href }}"
{% if logo.link_target %}
target="{{ logo.link_target }}"
{% endif %}
{% if logo.link_name %}
aria-label="{{ logo.link_name }}"
{% endif %}
>
<img
class="portal-logo {{ logo.img_class }}"
src="{% if logo.is_remote %}{{ logo.img_file_src }}{% else %}{% static logo.img_file_src %}{% endif %}"
{% if logo.img_crossorigin %}
crossorigin="{{ logo.img_crossorigin }}"
{% endif %}
{% if logo.img_alt_text %}
alt="{{ logo.img_alt_text }}"
{% endif %}
/>
</a>
{% endwith %}

{% endif %}
12 changes: 12 additions & 0 deletions taccsite_cms/templatetags/header_tags.py

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a tag lib with one tag that render header logo, if any.

Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from django import template
from django.utils.safestring import mark_safe

from taccsite_cms.contrib.taccsite_header_logo.render import render as render_header

register = template.Library()


@register.simple_tag(takes_context=True)
def render_header_logo(context):
html = render_header(context['request'], context)
return mark_safe(html) if html else ''