diff --git a/MANIFEST.in b/MANIFEST.in index de889dd..fe6bce5 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,6 +2,7 @@ graft src/kitconcept graft docs graft news graft tests +graft solr include .coveragerc include .dockerignore include .editorconfig diff --git a/Makefile b/Makefile index 77522a8..366c2e8 100644 --- a/Makefile +++ b/Makefile @@ -161,6 +161,11 @@ solr-start: ## Start solr @echo "Start solr" @COMPOSE_PROJECT_NAME=${COMPOSE_PROJECT_NAME} docker compose -f ${SOLR_ONLY_COMPOSE} up -d +.PHONY: solr-start-and-rebuild +solr-start-and-rebuild: ## Start solr, force rebuild + @echo "Start solr, force rebuild, erases data" + @COMPOSE_PROJECT_NAME=${COMPOSE_PROJECT_NAME} docker compose -f ${SOLR_ONLY_COMPOSE} up -d --build + .PHONY: solr-start-fg solr-start-fg: ## Start solr in foreground @echo "Start solr in foreground" diff --git a/setup.py b/setup.py index 5470e69..bfbfe63 100644 --- a/setup.py +++ b/setup.py @@ -54,6 +54,8 @@ "plone.distribution", # "plone.api", "kitconcept.solr", + "python-dateutil", + "collective.person", ], extras_require={ "test": [ diff --git a/solr/Dockerfile b/solr/Dockerfile new file mode 100644 index 0000000..1b8257f --- /dev/null +++ b/solr/Dockerfile @@ -0,0 +1,11 @@ +# syntax=docker/dockerfile:1 +FROM solr:8 + +LABEL maintainer="kitconcept, GmbH " \ + org.label-schema.name="ghcr.io/kitconcept/solr" \ + org.label-schema.description="Solr 8 image with Plone default settings" \ + org.label-schema.vendor="kitconcept, GmbH" + +# Copy default plone configuration for this image +COPY etc /plone-config +COPY bin/solr-update-core /opt/docker-solr/scripts diff --git a/src/kitconcept/intranet/behaviors/__init__.py b/src/kitconcept/intranet/behaviors/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/kitconcept/intranet/behaviors/additional_contact_info.py b/src/kitconcept/intranet/behaviors/additional_contact_info.py new file mode 100644 index 0000000..e817570 --- /dev/null +++ b/src/kitconcept/intranet/behaviors/additional_contact_info.py @@ -0,0 +1,33 @@ +from kitconcept.intranet import _ +from plone.autoform.directives import read_permission +from plone.autoform.interfaces import IFormFieldProvider +from plone.supermodel import directives +from plone.supermodel import model +from zope import schema +from zope.interface import provider + + +PERMISSION = "kitconcept.intranet.behaviors.additional_contact_info.view" + + +@provider(IFormFieldProvider) +class IAdditionalContactInfo(model.Schema): + directives.fieldset( + "additional_contact_info", + label=_( + "label_additional_contact_info", + default="Additional Contact Information", + ), + fields=("contact_building", "contact_room"), + ) + + read_permission(contact_building=PERMISSION, contact_room=PERMISSION) + + contact_building = schema.TextLine( + title=_("label_contact_building", default="Building"), + required=False, + ) + + contact_room = schema.TextLine( + title=_("label_contact_room", default="Room"), required=False + ) diff --git a/src/kitconcept/intranet/behaviors/configure.zcml b/src/kitconcept/intranet/behaviors/configure.zcml new file mode 100644 index 0000000..6a8e53c --- /dev/null +++ b/src/kitconcept/intranet/behaviors/configure.zcml @@ -0,0 +1,22 @@ + + + + + + + + diff --git a/src/kitconcept/intranet/configure.zcml b/src/kitconcept/intranet/configure.zcml index 8692da1..f7e7ce2 100644 --- a/src/kitconcept/intranet/configure.zcml +++ b/src/kitconcept/intranet/configure.zcml @@ -10,8 +10,17 @@ package="Products.CMFCore" file="permissions.zcml" /> + + + + + + diff --git a/src/kitconcept/intranet/dependencies.zcml b/src/kitconcept/intranet/dependencies.zcml index 6b7699b..1dc6a12 100644 --- a/src/kitconcept/intranet/dependencies.zcml +++ b/src/kitconcept/intranet/dependencies.zcml @@ -5,5 +5,6 @@ + diff --git a/src/kitconcept/intranet/distributions.zcml b/src/kitconcept/intranet/distributions.zcml index efb1264..94305c6 100644 --- a/src/kitconcept/intranet/distributions.zcml +++ b/src/kitconcept/intranet/distributions.zcml @@ -2,7 +2,7 @@ xmlns="http://namespaces.zope.org/zope" xmlns:i18n="http://namespaces.zope.org/i18n" xmlns:plone="http://namespaces.plone.org/plone" - i18n_domain="plone" + i18n_domain="kitconcept.intranet" > + + + + + + + diff --git a/src/kitconcept/intranet/profiles.zcml b/src/kitconcept/intranet/profiles.zcml new file mode 100644 index 0000000..986a706 --- /dev/null +++ b/src/kitconcept/intranet/profiles.zcml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/src/kitconcept/intranet/profiles/default/actions.xml b/src/kitconcept/intranet/profiles/default/actions.xml new file mode 100644 index 0000000..44a0283 --- /dev/null +++ b/src/kitconcept/intranet/profiles/default/actions.xml @@ -0,0 +1,45 @@ + + + + + + False + + + + + True + + + False + + + False + + + False + + + diff --git a/src/kitconcept/intranet/profiles/default/browserlayer.xml b/src/kitconcept/intranet/profiles/default/browserlayer.xml new file mode 100644 index 0000000..a6605f2 --- /dev/null +++ b/src/kitconcept/intranet/profiles/default/browserlayer.xml @@ -0,0 +1,6 @@ + + + + diff --git a/src/kitconcept/intranet/profiles/default/catalog.xml b/src/kitconcept/intranet/profiles/default/catalog.xml new file mode 100644 index 0000000..4ee3083 --- /dev/null +++ b/src/kitconcept/intranet/profiles/default/catalog.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + diff --git a/src/kitconcept/intranet/profiles/default/metadata.xml b/src/kitconcept/intranet/profiles/default/metadata.xml new file mode 100644 index 0000000..45ab0de --- /dev/null +++ b/src/kitconcept/intranet/profiles/default/metadata.xml @@ -0,0 +1,9 @@ + + + 20231122001 + + profile-plone.volto:default + kitconcept.solr:default + collective.person:default + + diff --git a/src/kitconcept/intranet/profiles/default/registry/kitconcept.solr.interfaces.IKitconceptSolrSettings.xml b/src/kitconcept/intranet/profiles/default/registry/kitconcept.solr.interfaces.IKitconceptSolrSettings.xml new file mode 100644 index 0000000..cf6c565 --- /dev/null +++ b/src/kitconcept/intranet/profiles/default/registry/kitconcept.solr.interfaces.IKitconceptSolrSettings.xml @@ -0,0 +1,70 @@ + + + + + { + "fieldList": [ + "UID", + "Title", + "Description", + "Type", + "effective", + "start", + "created", + "end", + "path_string", + "mime_type", + "location", + "contact_email", + "contact_phone", + "contact_building", + "contact_room", + "image_scales", + "image_field" + ], + "searchTabs": [ + { + "label": "All", + "filter": "Type(*)" + }, + { + "label": "Pages", + "filter": "Type:(Page)" + }, + { + "label": "Events", + "filter": "Type:(Event)" + }, + { + "label": "Images", + "filter": "Type:(Image)" + }, + { + "label": "Files", + "filter": "Type:(File)" + }, + { + "label": "Persons", + "filter": "Type:(Person)", + "layouts": ["list", "grid"], + "facetFields": [ + { + "name": "contact_building", + "label": "Building", + }, + { + "name":"contact_room", + "label": "Room" + } + ] + } + ] + } + + diff --git a/src/kitconcept/intranet/profiles/default/registry/plone.base.interfaces.controlpanel.IMailSchema.xml b/src/kitconcept/intranet/profiles/default/registry/plone.base.interfaces.controlpanel.IMailSchema.xml new file mode 100644 index 0000000..69001b3 --- /dev/null +++ b/src/kitconcept/intranet/profiles/default/registry/plone.base.interfaces.controlpanel.IMailSchema.xml @@ -0,0 +1,9 @@ + + + + kitconcept.intranet.demo + + diff --git a/src/kitconcept/intranet/profiles/default/registry/plone.base.interfaces.controlpanel.ISiteSchema.xml b/src/kitconcept/intranet/profiles/default/registry/plone.base.interfaces.controlpanel.ISiteSchema.xml new file mode 100644 index 0000000..d8345f1 --- /dev/null +++ b/src/kitconcept/intranet/profiles/default/registry/plone.base.interfaces.controlpanel.ISiteSchema.xml @@ -0,0 +1,9 @@ + + + + kitconcept.intranet.demo + + diff --git a/src/kitconcept/intranet/profiles/default/registry/plone.i18n.interfaces.ILanguageSchema.xml b/src/kitconcept/intranet/profiles/default/registry/plone.i18n.interfaces.ILanguageSchema.xml new file mode 100644 index 0000000..cd2730f --- /dev/null +++ b/src/kitconcept/intranet/profiles/default/registry/plone.i18n.interfaces.ILanguageSchema.xml @@ -0,0 +1,12 @@ + + + + + en + + en + + diff --git a/src/kitconcept/intranet/profiles/default/theme.xml b/src/kitconcept/intranet/profiles/default/theme.xml new file mode 100644 index 0000000..7f916aa --- /dev/null +++ b/src/kitconcept/intranet/profiles/default/theme.xml @@ -0,0 +1,5 @@ + + + barceloneta + true + diff --git a/src/kitconcept/intranet/profiles/default/types.xml b/src/kitconcept/intranet/profiles/default/types.xml new file mode 100644 index 0000000..bed2b0d --- /dev/null +++ b/src/kitconcept/intranet/profiles/default/types.xml @@ -0,0 +1,10 @@ + + + + diff --git a/src/kitconcept/intranet/profiles/default/types/Person.xml b/src/kitconcept/intranet/profiles/default/types/Person.xml new file mode 100644 index 0000000..1e65fc3 --- /dev/null +++ b/src/kitconcept/intranet/profiles/default/types/Person.xml @@ -0,0 +1,12 @@ + + + + + + diff --git a/src/kitconcept/intranet/profiles/initial/metadata.xml b/src/kitconcept/intranet/profiles/initial/metadata.xml new file mode 100644 index 0000000..a0ae0e7 --- /dev/null +++ b/src/kitconcept/intranet/profiles/initial/metadata.xml @@ -0,0 +1,4 @@ + + + 20231122001 + diff --git a/src/kitconcept/intranet/profiles/uninstall/browserlayer.xml b/src/kitconcept/intranet/profiles/uninstall/browserlayer.xml new file mode 100644 index 0000000..3628ad2 --- /dev/null +++ b/src/kitconcept/intranet/profiles/uninstall/browserlayer.xml @@ -0,0 +1,6 @@ + + + + diff --git a/src/kitconcept/intranet/setuphandlers/__init__.py b/src/kitconcept/intranet/setuphandlers/__init__.py new file mode 100644 index 0000000..f9a2bf6 --- /dev/null +++ b/src/kitconcept/intranet/setuphandlers/__init__.py @@ -0,0 +1,34 @@ +from kitconcept.intranet import logger +from kitconcept.intranet.setuphandlers import content +from kitconcept.intranet.setuphandlers import users +from plone import api +from Products.CMFPlone.interfaces import INonInstallable +from zope.interface import implementer + + +@implementer(INonInstallable) +class HiddenProfiles: + def getNonInstallableProfiles(self): + """Hide uninstall profile from site-creation and quickinstaller.""" + return [ + "kitconcept.intranet:uninstall", + ] + + +def populate_portal(context): + """Post install script""" + portal = api.portal.get() + # Delete content + content.delete_content(portal) + logger.info("Deleted default portal content") + user = users.create_default_user() + creators = [user.id] + logger.info("Created default user") + # Create other users + users.create_team_accounts() + logger.info("Created team accounts") + # Create Initial content + content.populate_portal(portal, creators) + logger.info("Created initial content") + # Update cover content + content.update_home(portal, creators) diff --git a/src/kitconcept/intranet/setuphandlers/content.py b/src/kitconcept/intranet/setuphandlers/content.py new file mode 100644 index 0000000..7581a7b --- /dev/null +++ b/src/kitconcept/intranet/setuphandlers/content.py @@ -0,0 +1,107 @@ +from DateTime import DateTime +from dateutil.parser import parse +from plone import api +from plone.app.dexterity.behaviors import constrains +from plone.namedfile.file import NamedBlobImage +from Products.CMFPlone.interfaces.constrains import ISelectableConstrainTypes + +import json +import os + + +TO_DELETE = ("front-page", "news", "events", "Members") + +__location__ = os.path.realpath(os.path.join(os.getcwd(), os.path.dirname(__file__))) + + +def delete_content(portal): + """Delete default content.""" + o_ids = [o_id for o_id in TO_DELETE if o_id in portal.objectIds()] + for o_id in o_ids: + api.content.delete(obj=portal[o_id]) + + +def _get_image(image: str) -> NamedBlobImage: + filepath = os.path.join(__location__, f"{image}") + with open(filepath, "rb") as f_in: + data = f_in.read() + return NamedBlobImage(data) + + +def date_from_string(value: str) -> DateTime: + return DateTime(parse(value)) + + +def _create_content(portal, item: dict, creators: list): + """Create a content.""" + container = portal.restrictedTraverse(item.get("_parent")) + o_id = item["id"] + + if o_id in container.objectIds(): + return container[o_id] + + item["creators"] = creators + permissions = item.get("_permissions", {}) + allowed_types = item.get("_allowed_types", []) + transitions = item.get("_transitions", []) + attributes = item.get("_attributes", {}) + payload = {k: v for k, v in item.items() if not k.startswith("_")} + payload["container"] = container + image_path = item.get("_image", None) + if image_path: + payload["image"] = _get_image(image_path) + date_keys = [k for k in payload.keys() if k.endswith("_date")] + for key in date_keys: + payload[key] = date_from_string(payload[key]) + + modified = payload.get("modification_date", None) + content = api.content.create(**payload) + # Apply attributes + if attributes: + for key, value in attributes.items(): + setattr(content, key, value) + for transition in transitions: + api.content.transition(obj=content, transition=transition) + # Set permissions + for permission_id, roles in permissions.items(): + content.manage_permission(permission_id, roles=roles) + if allowed_types: + behavior = ISelectableConstrainTypes(content) + behavior.setConstrainTypesMode(constrains.ENABLED) + behavior.setImmediatelyAddableTypes(allowed_types) + if modified: + content.modification_date = modified + content.reindexObject(idxs=["modified"]) + return content + + +def populate_portal(portal, creators): + """Create content structure.""" + with open(os.path.join(__location__, "contents.json")) as f_in: + contents = json.load(f_in) + + # Contents are created by Editors + with api.env.adopt_roles(["Editor", "Manager"]): + for item in contents: + _create_content(portal, item, creators) + + # Update workflow security + wf_tool = api.portal.get_tool("portal_workflow") + wf_tool.updateRoleMappings() + + +def _update_home(portal, item: dict): + """Update front page.""" + for key, value in item.items(): + setattr(portal, key, value) + return portal + + +def update_home(portal, creators): + """Create content structure.""" + with open(os.path.join(__location__, "home.json")) as f_in: + content = json.load(f_in) + + # Contents are created by Editors + with api.env.adopt_roles(["Editor", "Manager"]): + _update_home(portal, content) diff --git a/src/kitconcept/intranet/setuphandlers/contents.json b/src/kitconcept/intranet/setuphandlers/contents.json new file mode 100644 index 0000000..4aa958b --- /dev/null +++ b/src/kitconcept/intranet/setuphandlers/contents.json @@ -0,0 +1,26 @@ +[ + { + "_parent": "", + "type": "Document", + "id": "images", + "title": "Images", + "description": "Image bank.", + "_transitions": [ + "publish" + ], + "_allowed_types": [ + "Image" + ], + "exclude_from_nav": true + }, + { + "_parent": "images", + "type": "Image", + "id": "plone-foundation.png", + "title": "Plone Foundation", + "description": "", + "_image": "images/plone-foundation.png", + "_transitions": [], + "exclude_from_nav": false + } +] diff --git a/src/kitconcept/intranet/setuphandlers/home.json b/src/kitconcept/intranet/setuphandlers/home.json new file mode 100644 index 0000000..613cc1a --- /dev/null +++ b/src/kitconcept/intranet/setuphandlers/home.json @@ -0,0 +1,72 @@ +{ + "blocks": { + "94b9fb26-041d-438c-981a-671a89854cbe": { + "@type": "slate", + "plaintext": "Welcome to kitconcept.intranet", + "value": [ + { + "children": [ + { + "text": "" + }, + { + "children": [ + { + "text": "Welcome to kitconcept.intranet" + } + ], + "type": "b" + } + ], + "type": "p" + } + ] + }, + "1006adfe-d9b1-4e70-9b32-a9158310723d": { + "@type": "slate", + "plaintext": "Plone is an enterprise CMS built with Python.", + "value": [ + { + "children": [ + { + "text": "" + }, + { + "children": [ + { + "text": "Plone" + } + ], + "type": "b" + }, + { + "text": " is an enterprise CMS built with " + }, + { + "children": [ + { + "text": "Python" + } + ], + "data": { + "url": "https://python.org" + }, + "type": "link" + }, + { + "text": "." + } + ], + "type": "p" + } + ] + } + }, + "blocks_layout": { + "items": [ + "94b9fb26-041d-438c-981a-671a89854cbe", + "1006adfe-d9b1-4e70-9b32-a9158310723d" + ] + }, + "title": "kitconcept.intranet.demo" +} diff --git a/src/kitconcept/intranet/setuphandlers/images/plone-foundation.png b/src/kitconcept/intranet/setuphandlers/images/plone-foundation.png new file mode 100644 index 0000000..9516c4c Binary files /dev/null and b/src/kitconcept/intranet/setuphandlers/images/plone-foundation.png differ diff --git a/src/kitconcept/intranet/setuphandlers/users.json b/src/kitconcept/intranet/setuphandlers/users.json new file mode 100644 index 0000000..10da619 --- /dev/null +++ b/src/kitconcept/intranet/setuphandlers/users.json @@ -0,0 +1,19 @@ +{ + "portal": [ + { + "email": "collective@plone.org", + "properties": { + "fullname": "kitconcept.intranet.demo", + "location": "World" + }, + "groups": [ + ], + "roles": [ + "Member" + ], + "username": "kitconcept-intranet-demo" + } + ], + "team": [ + ] +} diff --git a/src/kitconcept/intranet/setuphandlers/users.py b/src/kitconcept/intranet/setuphandlers/users.py new file mode 100644 index 0000000..15aed51 --- /dev/null +++ b/src/kitconcept/intranet/setuphandlers/users.py @@ -0,0 +1,47 @@ +from plone import api + +import json +import os + + +__location__ = os.path.realpath(os.path.join(os.getcwd(), os.path.dirname(__file__))) + + +def _users_info() -> dict: + """Return users info.""" + with open(os.path.join(__location__, "users.json")) as f_in: + users = json.load(f_in) + return users + + +def create_default_user(): + """Create a default user to organize content.""" + all_users = _users_info() + user_info = all_users["portal"][0] + groups = user_info.pop("groups") + user = api.user.create(**user_info) + for group_name in groups: + api.group.add_user(groupname=group_name, user=user) + return user + + +def create_accounts(accounts: list) -> list: + """Create user accounts.""" + new_users = [] + for user_info in accounts: + username = user_info.get("username", "") + if api.user.get(username=username): + # User already exists, skip it + continue + groups = user_info.pop("groups") + user = api.user.create(**user_info) + for group_name in groups: + api.group.add_user(groupname=group_name, user=user) + new_users.append(user) + return new_users + + +def create_team_accounts(): + """Create team accounts.""" + all_users = _users_info() + return create_accounts(all_users["team"]) diff --git a/src/kitconcept/intranet/upgrades/__init__.py b/src/kitconcept/intranet/upgrades/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/kitconcept/intranet/upgrades/configure.zcml b/src/kitconcept/intranet/upgrades/configure.zcml new file mode 100644 index 0000000..fc98bb3 --- /dev/null +++ b/src/kitconcept/intranet/upgrades/configure.zcml @@ -0,0 +1,19 @@ + + + + +