diff --git a/MANIFEST.in b/MANIFEST.in index 4fe04bee4..edb1da67a 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -18,6 +18,7 @@ include babel.ini include docs/requirements.txt include LICENSE include pytest.ini +include invenio_communities/templates/semantic-ui/invenio_communities/community_theme_template.css recursive-include docs *.bat recursive-include docs *.py recursive-include docs *.rst @@ -30,6 +31,7 @@ recursive-include invenio_communities *.png recursive-include invenio_communities *.po *.pot *.mo recursive-include invenio_communities *.py recursive-include invenio_communities *.tpl +recursive-include tests *.html recursive-include tests *.json recursive-include tests *.py include .git-blame-ignore-revs diff --git a/invenio_communities/communities/dumpers/community_theme.py b/invenio_communities/communities/dumpers/community_theme.py new file mode 100644 index 000000000..712cc76c6 --- /dev/null +++ b/invenio_communities/communities/dumpers/community_theme.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Invenio. +# Copyright (C) 2024 CERN. +# +# Invenio is free software; you can redistribute it and/or modify it +# under the terms of the MIT License; see LICENSE file for more details. + +"""Community theme dumper. + +Dumper used to remove the theme field on dump of a community from a search body. +""" + +from invenio_records.dumpers import SearchDumperExt + + +class CommunityThemeDumperExt(SearchDumperExt): + """Dumper to remove community theme field from indexing.""" + + def __init__(self, key="theme"): + """Initialize the dumper.""" + self.key = key + + def dump(self, record, data): + """Remove theme information from indexing.""" + data.pop("theme", None) diff --git a/invenio_communities/communities/records/api.py b/invenio_communities/communities/records/api.py index fc0d62d3d..3a38dedb9 100644 --- a/invenio_communities/communities/records/api.py +++ b/invenio_communities/communities/records/api.py @@ -30,6 +30,7 @@ IsVerifiedField, ) +from ..dumpers.community_theme import CommunityThemeDumperExt from ..dumpers.featured import FeaturedDumperExt from . import models from .systemfields.access import CommunityAccessField @@ -58,6 +59,7 @@ class Community(Record): dumper = SearchDumper( extensions=[ + CommunityThemeDumperExt("theme"), FeaturedDumperExt("featured"), RelationDumperExt("relations"), CalculatedFieldDumperExt("is_verified"), diff --git a/invenio_communities/communities/records/jsonschemas/communities/communities-v1.0.0.json b/invenio_communities/communities/records/jsonschemas/communities/communities-v1.0.0.json index e2ffb017f..402f69084 100644 --- a/invenio_communities/communities/records/jsonschemas/communities/communities-v1.0.0.json +++ b/invenio_communities/communities/records/jsonschemas/communities/communities-v1.0.0.json @@ -145,6 +145,21 @@ } } }, + "theme": { + "type": "object", + "description": "Community theme settings.", + "additionalProperties": false, + "properties": { + "config": { + "description": "Theme config.", + "type": "object" + }, + "brand": { + "description": "Theme brand name.", + "type": "string" + } + } + }, "tombstone": { "type": "object", "description": "Tombstone information for the community.", diff --git a/invenio_communities/communities/records/jsonschemas/communities/definitions-v2.0.0.json b/invenio_communities/communities/records/jsonschemas/communities/definitions-v2.0.0.json index ca76f1a6e..38da5ecd4 100644 --- a/invenio_communities/communities/records/jsonschemas/communities/definitions-v2.0.0.json +++ b/invenio_communities/communities/records/jsonschemas/communities/definitions-v2.0.0.json @@ -8,7 +8,9 @@ }, "ids": { "type": "array", - "items": {"type": "string"}, + "items": { + "type": "string" + }, "uniqueItems": true } } @@ -27,7 +29,9 @@ }, "affiliations": { "type": "array", - "items": {"$ref": "#/affiliation"}, + "items": { + "$ref": "#/affiliation" + }, "uniqueItems": true }, "agent": { @@ -455,7 +459,9 @@ }, "subjects": { "type": "array", - "items": {"$ref": "#/subject"}, + "items": { + "$ref": "#/subject" + }, "uniqueItems": true }, "title_type": { @@ -474,7 +480,10 @@ "additionalProperties": false, "properties": { "user": { - "type": ["integer", "string"] + "type": [ + "integer", + "string" + ] } } }, diff --git a/invenio_communities/communities/resources/ui_schema.py b/invenio_communities/communities/resources/ui_schema.py index 8f8b4de48..fa8c963dc 100644 --- a/invenio_communities/communities/resources/ui_schema.py +++ b/invenio_communities/communities/resources/ui_schema.py @@ -22,6 +22,7 @@ from marshmallow import Schema, fields, post_dump from marshmallow_utils.fields import FormatEDTF as FormatEDTF_ +from invenio_communities.communities.schema import CommunityThemeSchema from invenio_communities.proxies import current_communities @@ -91,6 +92,8 @@ class UICommunitySchema(BaseObjectSchema): tombstone = fields.Nested(TombstoneSchema, attribute="tombstone") + theme = fields.Nested(CommunityThemeSchema, dump_only=True, load_default={}) + # Custom fields custom_fields = fields.Nested( partial(CustomFieldsSchemaUI, fields_var="COMMUNITIES_CUSTOM_FIELDS") @@ -109,12 +112,17 @@ def get_permissions(self, obj): return {"can_include_directly": can_include_directly, "can_update": can_update} @post_dump - def hide_tombstone(self, obj, **kwargs): - """Hide the tombstone information if it's not visible.""" - if not obj.get("tombstone", {}).get("is_visible", False): - obj.pop("tombstone", None) + def post_dump(self, data, many, **kwargs): + """Pop theme field if None.""" + is_deleted = (data.get("deletion_status") or {}).get("is_deleted", False) + tombstone_visible = (data.get("tombstone") or {}).get("is_visible", True) + + if not is_deleted or not tombstone_visible: + data.pop("tombstone", None) - return obj + if data.get("theme") is None: + data.pop("theme", None) + return data class TypesSchema(Schema): diff --git a/invenio_communities/communities/schema.py b/invenio_communities/communities/schema.py index 33a502269..d647c8fd2 100644 --- a/invenio_communities/communities/schema.py +++ b/invenio_communities/communities/schema.py @@ -161,6 +161,13 @@ class DeletionStatusSchema(Schema): status = fields.String(dump_only=True) +class CommunityThemeSchema(Schema): + """Community theme schema.""" + + config = fields.Dict() + brand = fields.Str() + + class CommunitySchema(BaseRecordSchema, FieldPermissionsMixin): """Schema for the community metadata.""" @@ -196,7 +203,10 @@ class Meta: is_verified = fields.Boolean(dump_only=True) + theme = fields.Nested(CommunityThemeSchema, allow_none=True) + tombstone = fields.Nested(TombstoneSchema, dump_only=True) + deletion_status = fields.Nested(DeletionStatusSchema, dump_only=True) @post_dump @@ -206,7 +216,10 @@ def post_dump(self, data, many, **kwargs): tombstone_visible = (data.get("tombstone") or {}).get("is_visible", True) if data.get("custom_fields") is None: - data.pop("custom_fields") + data.pop("custom_fields", None) + + if data.get("theme") is None: + data.pop("theme", None) if not is_deleted or not tombstone_visible: data.pop("tombstone", None) diff --git a/invenio_communities/communities/services/components.py b/invenio_communities/communities/services/components.py index 14e9e7d6e..ad9276eea 100644 --- a/invenio_communities/communities/services/components.py +++ b/invenio_communities/communities/services/components.py @@ -268,8 +268,35 @@ def unmark(self, identity, data=None, record=None, **kwargs): record.tombstone = record.tombstone +class CommunityThemeComponent(ServiceComponent): + """Service component for Community theme.""" + + def update(self, identity, data=None, record=None, errors=None, **kwargs): + """Inject parsed theme to the record.""" + stored_record_theme = record.get("theme") + if "theme" in data: + # if theme set to None then it is a delete operation + if data["theme"] is None: + if stored_record_theme is not None: + self.service.require_permission( + identity, "delete_theme", record=record + ) + # delete theme from record and data + record.pop("theme", None) + # We always pop the {theme: None} from the data so we don't store None + # value in the record + data.pop("theme", None) + elif data["theme"] != stored_record_theme: + # check update permissions for theme only if incoming theme is + # different from stored. Check is needed, so we don't apply the theme + # permission check when other community information is updated + self.service.require_permission(identity, "set_theme", record=record) + record["theme"] = data["theme"] + + DefaultCommunityComponents = [ MetadataComponent, + CommunityThemeComponent, CustomFieldsComponent, PIDComponent, RelationsComponent, diff --git a/invenio_communities/communities/services/config.py b/invenio_communities/communities/services/config.py index 08b361858..9e4b42038 100644 --- a/invenio_communities/communities/services/config.py +++ b/invenio_communities/communities/services/config.py @@ -40,7 +40,12 @@ ) from ...permissions import CommunityPermissionPolicy, can_perform_action -from ..schema import CommunityFeaturedSchema, CommunitySchema, TombstoneSchema +from ..schema import ( + CommunityFeaturedSchema, + CommunitySchema, + CommunityThemeSchema, + TombstoneSchema, +) from .components import DefaultCommunityComponents from .links import CommunityLink from .search_params import IncludeDeletedCommunitiesParam, StatusParam diff --git a/invenio_communities/permissions.py b/invenio_communities/permissions.py index 6bf3059f7..69dd182f1 100644 --- a/invenio_communities/permissions.py +++ b/invenio_communities/permissions.py @@ -162,6 +162,10 @@ class CommunityPermissionPolicy(BasePermissionPolicy): # correct permissions based on which the field will be exposed only to moderators can_moderate = [Disable()] + # Permissions to crud community theming + can_set_theme = [SystemProcess()] + can_delete_theme = can_set_theme + def can_perform_action(community, context): """Check if the given action is available on the request.""" diff --git a/invenio_communities/templates/semantic-ui/invenio_communities/base.html b/invenio_communities/templates/semantic-ui/invenio_communities/base.html index 06b054405..0c210a792 100644 --- a/invenio_communities/templates/semantic-ui/invenio_communities/base.html +++ b/invenio_communities/templates/semantic-ui/invenio_communities/base.html @@ -8,3 +8,10 @@ #} {% extends config.BASE_TEMPLATE %} + +{%- block css %} +{{ super() }} +{% if community %} + +{% endif %} +{%- endblock css %} diff --git a/invenio_communities/templates/semantic-ui/invenio_communities/community_theme_template.css b/invenio_communities/templates/semantic-ui/invenio_communities/community_theme_template.css new file mode 100644 index 000000000..7a94f9eb6 --- /dev/null +++ b/invenio_communities/templates/semantic-ui/invenio_communities/community_theme_template.css @@ -0,0 +1,45 @@ +.page-subheader-outer { + background-color: {{ theme.mainHeaderBackgroundColor }} !important; +} + +.theme-secondary, .ui.secondary.button { + background-color: {{ theme.secondaryColor }} !important; + color: {{ theme.secondaryTextColor }} !important; +} + +.invenio-page-body .ui.search.button { + background-color: {{ theme.secondaryColor }} !important; + color: {{ theme.secondaryTextColor }} !important; +} + +.theme-primary a, .theme-primary h1, .theme-primary h2 { + color: {{ theme.primaryTextColor }} !important; +} + +.theme-primary.pointing.menu .item.active { + border-color: {{ theme.secondaryColor }} !important; +} + +.theme-primary-menu .item.active { + background-color: {{ theme.primaryColor }} !important; +} + +.theme-primary .item, .theme-primary-menu, .page-subheader-outer{ + font-family: {{ theme.font.family }} !important; + font-weight: {{ theme.font.weight }} !important; + font-size: {{ theme.font.size }}; +} + +.theme-primary { + background-color: {{ theme.primaryColor }} !important; +} + +.theme-tertiary, .ui.tertiary.button { + background-color: {{ theme.tertiaryColor }} !important; + color: {{ theme.tertiaryTextColor }}; +} + +.invenio-accordion-field .title, .ui.primary.button{ + background-color: {{ theme.primaryColor }} !important; + color: {{ theme.primaryTextColor }} !important; +} diff --git a/invenio_communities/templates/semantic-ui/invenio_communities/details/base.html b/invenio_communities/templates/semantic-ui/invenio_communities/details/base.html index fe797505a..55ac48129 100644 --- a/invenio_communities/templates/semantic-ui/invenio_communities/details/base.html +++ b/invenio_communities/templates/semantic-ui/invenio_communities/details/base.html @@ -10,5 +10,6 @@ {% extends "invenio_communities/base.html" %} {%- block page_body %} + {% set community_menu_active = True %} {% include "invenio_communities/details/header.html" %} {%- endblock page_body %} diff --git a/invenio_communities/templates/semantic-ui/invenio_communities/details/header.html b/invenio_communities/templates/semantic-ui/invenio_communities/details/header.html index d5aff4afc..ec1d9c6d9 100644 --- a/invenio_communities/templates/semantic-ui/invenio_communities/details/header.html +++ b/invenio_communities/templates/semantic-ui/invenio_communities/details/header.html @@ -16,7 +16,8 @@
-
+ diff --git a/invenio_communities/templates/semantic-ui/invenio_communities/details/members/base.html b/invenio_communities/templates/semantic-ui/invenio_communities/details/members/base.html index ccb5cffa8..1af3c3ef6 100644 --- a/invenio_communities/templates/semantic-ui/invenio_communities/details/members/base.html +++ b/invenio_communities/templates/semantic-ui/invenio_communities/details/members/base.html @@ -20,7 +20,7 @@ 'invitations': (_('Invitations'), url_for('invenio_communities.invitations', pid_value=community.slug), permissions.can_search_invites), } %}
-