Skip to content
Open
Show file tree
Hide file tree
Changes from 75 commits
Commits
Show all changes
112 commits
Select commit Hold shift + click to select a range
707d106
Add content_review behavior
jnptk Feb 13, 2026
0b36d0c
Add vocabularies
jnptk Feb 13, 2026
a991d9b
Fix fieldset not showing
jnptk Feb 17, 2026
c77642d
Wording & stuff
jnptk Feb 17, 2026
848b12f
Add behavior to all default content types
jnptk Feb 17, 2026
94e4902
Add upgrade step
jnptk Feb 17, 2026
d70117f
make format
jnptk Feb 17, 2026
6d47850
Add content_review_default_interval setting to intranet controlpanel
jnptk Feb 18, 2026
417568c
Directly access vocabularies without directives.widget
jnptk Feb 18, 2026
1137d82
Add basic review endpoint
jnptk Feb 18, 2026
8a5b166
Update service permission & allowed context
jnptk Feb 19, 2026
7341f10
Add plone.app.registry to upgrade step
jnptk Feb 19, 2026
d382f08
Remove unused import
jnptk Feb 19, 2026
ca04888
Fix context & update error messages
jnptk Feb 19, 2026
8d7dbb4
Remove unused import
jnptk Feb 19, 2026
1ac5bb6
Add review_status and review_due_date indexes
jnptk Feb 19, 2026
4d939ea
Update upgrade step
jnptk Feb 19, 2026
4696fef
Add querystring criterion
jnptk Feb 19, 2026
c22de8b
Merge branch 'main' into content-review
jnptk Feb 20, 2026
d04b534
Fix upgrade step after merging main
jnptk Feb 20, 2026
e831fd1
Add import step for review_due_date indexer
jnptk Feb 20, 2026
bd7bf93
Add invariant & review_enabled, title & description in fields
jnptk Feb 24, 2026
cb4ab7f
Fix querystring thingie
jnptk Feb 24, 2026
2df48b4
Merge branch 'main' into content-review
jnptk Feb 24, 2026
4d84607
Fix querystring again
jnptk Feb 24, 2026
636ff24
Add default review intervals
jnptk Feb 25, 2026
e615c82
Set default review intervals
jnptk Feb 25, 2026
62fd42e
Implement "approve", "delegate" and "postpone" action in review endpo…
jnptk Feb 25, 2026
18f51ae
First batch of tests
jnptk Feb 25, 2026
c9ee28f
Merge branch 'main' into content-review
jnptk Feb 26, 2026
7cfd169
Set review_comment to readonly
jnptk Feb 26, 2026
3a5ec4a
Require review_interval, set default & default to setting
jnptk Feb 26, 2026
120b14d
Bind to behavior & get vocab from field
jnptk Feb 26, 2026
c2cdb6f
Fix fti test
jnptk Feb 26, 2026
44a4f6a
Add test for content_review_intervals vocabulary
jnptk Feb 26, 2026
d7e99c5
How-To guide (wip)
jnptk Feb 27, 2026
57a27e7
Merge branch 'main' into content-review
jnptk Mar 5, 2026
e574255
Fix upgrade step after merge
jnptk Mar 5, 2026
92747f9
Fix profile version
jnptk Mar 5, 2026
179da61
Combine upgrade steps & move calc_due_date to utils
jnptk Mar 6, 2026
6dbdd79
Formatting
jnptk Mar 6, 2026
c5ea73c
Add script to send a mail if a content objects is due to review
jnptk Mar 6, 2026
49b789a
Merge branch 'main' into content-review
jnptk Mar 17, 2026
5aa11df
Fix version after merge
jnptk Mar 17, 2026
95a9861
Fix upgrade step
jnptk Mar 17, 2026
7da59e8
Send mail in review_reminder script
jnptk Mar 17, 2026
30792fc
Set default review_due_date to today
jnptk Mar 17, 2026
96837ec
Add changelog entries
jnptk Mar 17, 2026
e40467d
Fix [at]review endpoint tests
jnptk Mar 17, 2026
eedb6f2
Properly calculate review_due_date
jnptk Mar 18, 2026
884c7e3
Fix [at]review endpoint to set values persistently
jnptk Mar 18, 2026
e0c31db
More test for [at]review endpoint
jnptk Mar 18, 2026
67575b6
Merge remote-tracking branch 'origin/main' into content-review
davisagli Apr 1, 2026
6287e39
Remove .python-version (duplicates requires-python in pyproject.toml)
davisagli Apr 1, 2026
ab526b9
fix bad merge of upgrade steps
davisagli Apr 1, 2026
1bc3cf3
add review querystring criteria
jnptk Apr 2, 2026
624e311
return record directly in default_review_interval
jnptk Apr 2, 2026
7b2443d
off source reminder email for better testability
jnptk Apr 2, 2026
5f84081
update default review intervals based of lo-fi design
jnptk Apr 2, 2026
2df56db
shorten [at]review reply
jnptk Apr 2, 2026
bf140a6
remove content_review_users vocab test
jnptk Apr 2, 2026
b306b25
assert full vocab in test
jnptk Apr 2, 2026
808c8f4
make format
jnptk Apr 2, 2026
25c20c6
fixes for notify_reviewer
jnptk Apr 7, 2026
881f23b
add path to warning if no user was found
jnptk Apr 7, 2026
2741b54
test calc_due_date
jnptk Apr 7, 2026
231d72b
remove context aware factory (no context needed = no factory needed)
jnptk Apr 7, 2026
d1a6400
fix defaultFactory
davisagli Apr 7, 2026
58a3bd5
Hacky override to get the UI working, we can make it better later
davisagli Apr 7, 2026
f1fdfac
Merge remote-tracking branch 'origin/main' into content-review
davisagli Apr 7, 2026
36bfd5f
Work around bug in plone.restapi that calls defaultFactory with a con…
davisagli Apr 7, 2026
08df685
fix acceptance test
davisagli Apr 7, 2026
2d9a4ba
Remove unneeded explicit commits, disable CSRF protection
davisagli Apr 8, 2026
1d68403
rename doc in service test
jnptk Apr 8, 2026
b93b2c6
add docs for [at]review endpoint
jnptk Apr 9, 2026
c6d63a9
i18n
Tishasoumya-02 Apr 21, 2026
be24f80
Merge branch 'content-review' of github.com:kitconcept/kitconcept.int…
Tishasoumya-02 Apr 21, 2026
6da18b8
don't allow pastDates
Tishasoumya-02 Apr 22, 2026
01553e3
fix lint
Tishasoumya-02 Apr 22, 2026
8759f1b
fix scss lint
Tishasoumya-02 Apr 22, 2026
2bda500
replace semantic-ui with plone-components and other fixes
Tishasoumya-02 Apr 28, 2026
8c1cd21
fix lint
Tishasoumya-02 Apr 28, 2026
a21dcd1
make i18n
Tishasoumya-02 Apr 28, 2026
d99cc6b
lint
Tishasoumya-02 Apr 28, 2026
c76f880
update with main
Tishasoumya-02 Apr 28, 2026
ea751fe
fix styleint
Tishasoumya-02 Apr 28, 2026
a115359
Merge branch 'main' into content-review
Tishasoumya-02 Apr 29, 2026
1d1b3be
update with main
Tishasoumya-02 May 4, 2026
6d9acc4
update accordion export
Tishasoumya-02 May 4, 2026
42d58c8
update form customization to updated form code from volto with autosa…
Tishasoumya-02 May 4, 2026
ddd8604
merge with 'main' into content-review
Tishasoumya-02 Jun 2, 2026
7614b2b
update lockfile
Tishasoumya-02 Jun 2, 2026
12d6904
fix merge conflict in locales
Tishasoumya-02 Jun 3, 2026
4eafc8c
remove the customised components and add the styling
Tishasoumya-02 Jun 4, 2026
42d025d
fix lint and stylelint
Tishasoumya-02 Jun 4, 2026
0754d92
fix
Tishasoumya-02 Jun 4, 2026
405981a
fix
Tishasoumya-02 Jun 4, 2026
6873b9a
Merge branch 'main' into content-review
Tishasoumya-02 Jun 4, 2026
762f6eb
remove onClose prop from DocumentReview component
Tishasoumya-02 Jun 4, 2026
c2d5267
Merge branch 'content-review' of github.com:kitconcept/kitconcept.int…
Tishasoumya-02 Jun 4, 2026
c83a0b7
improve code
iFlameing Jun 8, 2026
41f2f77
add feature flag to toggle the feature on and off
iFlameing Jun 11, 2026
9ad860c
Merge branch 'main' into content-review
iFlameing Jun 11, 2026
1b367f7
update pnpm-lock.yaml
jnptk Jun 15, 2026
b7d8924
integrate review_reminder script into docker using swarm-cronjob
jnptk Jun 15, 2026
07d285c
Merge branch 'main' into content-review
Tishasoumya-02 Jun 16, 2026
6f23570
integrate review_reminder script in demo & persistent stacks
jnptk Jun 17, 2026
9197d57
add docs for configuring the reminder script
jnptk Jun 17, 2026
d12a17e
Merge branch 'main' into content-review
iFlameing Jun 18, 2026
9156591
remove swarm-cronjob from demo.yml
jnptk Jun 19, 2026
9b7a8b4
make sure backend-cron-worker service connects to the same db as back…
jnptk Jun 19, 2026
78bb36d
update docs
jnptk Jun 19, 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
1 change: 1 addition & 0 deletions backend/news/330.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Implemented content review & reminders features. @jntpk
10 changes: 10 additions & 0 deletions backend/scripts/review_reminder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from kitconcept.intranet.utils.review_due_notifier import nofity_reviewer
from plone import api
from zope.component.hooks import setSite


portal = app.Plone
setSite(portal)

with api.env.adopt_roles(["Manager"]):
nofity_reviewer(portal)
7 changes: 7 additions & 0 deletions backend/src/kitconcept/intranet/behaviors/configure.zcml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,13 @@
provides=".person.IPersonBehavior"
/>

<plone:behavior
name="kitconcept.intranet.content_review"
title="CLM: Reminders for potentially outdated content"
description="Fields for reviewing content (part of CLM)"
provides=".content_review.IContentReview"
/>

<utility
factory=".person.AcademicTitleFirstLastName"
name="academic_title_first_last"
Expand Down
96 changes: 96 additions & 0 deletions backend/src/kitconcept/intranet/behaviors/content_review.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
from kitconcept.intranet import _
from kitconcept.intranet.utils.calc_due_date import calc_due_date
from plone import api
from plone.autoform.interfaces import IFormFieldProvider
from plone.supermodel import model
from zope import schema
from zope.interface import Invalid
from zope.interface import invariant
from zope.interface import provider
from zope.schema.interfaces import IContextAwareDefaultFactory


@provider(IContextAwareDefaultFactory)
def default_review_interval(context) -> str:
return api.portal.get_registry_record(
"kitconcept.intranet.content_review_default_interval"
)


@provider(IFormFieldProvider)
class IContentReview(model.Schema):
"""Content Review behavior"""

model.fieldset(
"Content Review & Reminders",
label=_("label_review_fieldset", "Content Review & Reminders"),
fields=[
"review_timeless",
"review_status",
"review_interval",
"review_assignee",
"review_due_date",
"review_completed_date",
],
)

review_timeless = schema.Bool(
title=_(
"label_review_timeless",
default="Timeless content - exclude from review reminders",
),
required=False,
default=False,
)

review_status = schema.Choice(
title=_("label_review_status", default="Status"),
values=("Up-to-date", "Due", "Changes requested"),
default="Up-to-date",
required=False,
readonly=True,
Comment thread
davisagli marked this conversation as resolved.
)

review_interval = schema.Choice(
title=_("label_review_interval", default="Review Interval"),
vocabulary="kitconcept.intranet.vocabularies.content_review_intervals",
required=False,
defaultFactory=default_review_interval,
)

review_assignee = schema.Choice(
title=_("label_review_assignee", default="Reviewer"),
description=_(
"help_review_assignee",
default="... will be notified as soon as the review is due. Unless otherwise specified, the content owner is considered the responsible person.",
),
vocabulary="plone.app.vocabularies.Users",
required=False,
)

review_due_date = schema.Date(
title=_("label_review_due_date", default="Next review on"),
required=False,
defaultFactory=calc_due_date,
)

review_completed_date = schema.Date(
title=_("label_review_completed_date", default="Last review on"),
required=False,
readonly=True,
)

review_comment = schema.Text(
title=_("label_review_comment", default="Comment"),
required=False,
Comment thread
jnptk marked this conversation as resolved.
readonly=True,
)

@invariant
def validate_due_date_field(data):
is_timeless = getattr(data, "review_timeless", False)
has_due_date = getattr(data, "review_due_date", None)
if not is_timeless and not has_due_date:
raise Invalid("You have to set a due date if the content is not timeless")
elif is_timeless and has_due_date:
raise Invalid("Cannot set due date if content is timeless")
2 changes: 1 addition & 1 deletion backend/src/kitconcept/intranet/behaviors/location.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
from plone.autoform.directives import order_after
from plone.autoform.interfaces import IFormFieldProvider
from plone.supermodel import model
from zope.interface import provider
from zope import schema
from zope.interface import provider


@provider(IFormFieldProvider)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
from plone.autoform.directives import order_after
from plone.autoform.interfaces import IFormFieldProvider
from plone.supermodel import model
from zope.interface import provider
from zope import schema
from zope.interface import provider


@provider(IFormFieldProvider)
Expand Down
7 changes: 7 additions & 0 deletions backend/src/kitconcept/intranet/controlpanels/intranet.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,13 @@ class IIntranetSettings(Interface):
defaultFactory=list,
)

content_review_default_interval = schema.Choice(
title=_("Default review interval"),
vocabulary="kitconcept.intranet.vocabularies.content_review_intervals",
required=True,
default="6m",
)
Comment thread
jnptk marked this conversation as resolved.


class IntranetSettingsEditForm(RegistryEditForm):
schema = IIntranetSettings
Expand Down
13 changes: 13 additions & 0 deletions backend/src/kitconcept/intranet/profiles/default/catalog.xml
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,17 @@
<indexed_attr value="responsibilities" />
</index>

<!-- Field indexes for IContentReview behavior -->
<index meta_type="FieldIndex"
name="review_status"
>
<indexed_attr value="review_status" />
</index>

<index meta_type="FieldIndex"
name="review_due_date"
>
<indexed_attr value="review_due_date" />
</index>

</object>
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<metadata>
<version>20260318001</version>
<version>20260401001</version>
<dependencies>
<dependency>plone.app.discussion:default</dependency>
<dependency>profile-kitconcept.contactblock:default</dependency>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,4 +82,24 @@
<value key="widget" />
</records>

<!-- Content Review -->
<records interface="plone.app.querystring.interfaces.IQueryField"
prefix="plone.app.querystring.field.review_status"
>
<value key="title"
i18n:translate="label_review_status"
>Review status</value>
<value key="description"
i18n:translate="help_review_status_filter"
>Filter by review status</value>
<value key="enabled">True</value>
<value key="sortable">False</value>
<value key="operations">
<element>plone.app.querystring.operation.selection.any</element>
</value>
<value key="group"
i18n:translate=""
>Review</value>
</records>

</registry>
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
/>
<element value="kitconcept.intranet.clm" />
<element value="kitconcept.intranet.votes" />
<element value="kitconcept.intranet.content_review" />
</property>

</object>
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
<element value="kitconcept.intranet.location" />
<element value="kitconcept.intranet.organisational_unit" />
<element value="kitconcept.intranet.votes" />
<element value="kitconcept.intranet.content_review" />

</property>
</object>
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
<element value="kitconcept.intranet.location" />
<element value="kitconcept.intranet.organisational_unit" />
<element value="kitconcept.intranet.votes" />
<element value="kitconcept.intranet.content_review" />

</property>
</object>
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
<element value="kitconcept.intranet.location" />
<element value="kitconcept.intranet.organisational_unit" />
<element value="kitconcept.intranet.votes" />
<element value="kitconcept.intranet.content_review" />


</property>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@
>
<element value="kitconcept.intranet.location" />
<element value="kitconcept.intranet.organisational_unit" />
<element value="kitconcept.intranet.content_review" />
</property>
</object>
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
<element value="plone.versioning" />
<element value="plone.locking" />
<element value="plone.translatable" />
<element value="kitconcept.intranet.content_review" />
</property>


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
<element value="kitconcept.intranet.location" />
<element value="kitconcept.intranet.organisational_unit" />
<element value="kitconcept.intranet.votes" />
<element value="kitconcept.intranet.content_review" />

</property>
</object>
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
<element value="plone.versioning" />
<element value="plone.locking" />
<element value="plone.translatable" />
<element value="kitconcept.intranet.content_review" />

</property>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,6 @@
<element value="plone.translatable" />
<element value="kitconcept.intranet.location" />
<element value="kitconcept.intranet.organisational_unit" />
<element value="kitconcept.intranet.content_review" />
</property>
</object>
4 changes: 2 additions & 2 deletions backend/src/kitconcept/intranet/serializers/person.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
from collective.person.content.person import IPerson
from kitconcept.intranet.interfaces import IBrowserLayer
from plone import api
from plone.restapi.interfaces import ISerializeToJson
from plone.restapi.serializer.dxcontent import SerializeToJson
from zope.component import adapter
from zope.interface import implementer
from plone.restapi.serializer.dxcontent import SerializeToJson
from plone import api


@implementer(ISerializeToJson)
Expand Down
8 changes: 8 additions & 0 deletions backend/src/kitconcept/intranet/services/configure.zcml
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,12 @@
name="@vote"
/>

<plone:service
method="POST"
factory=".review.ReviewPost"
for="kitconcept.intranet.behaviors.content_review.IContentReview"
permission="cmf.ModifyPortalContent"
name="@review"
/>

</configure>
65 changes: 65 additions & 0 deletions backend/src/kitconcept/intranet/services/review.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
from datetime import date
from kitconcept.intranet.behaviors.content_review import IContentReview
from kitconcept.intranet.utils.calc_due_date import calc_due_date
from plone import api
from plone.protect.interfaces import IDisableCSRFProtection
from plone.restapi.deserializer import json_body
from plone.restapi.services import Service
from zExceptions import BadRequest
from zope.interface import alsoProvides
from zope.interface import implementer
from zope.publisher.interfaces import IPublishTraverse


@implementer(IPublishTraverse)
class ReviewPost(Service):
"""@review endpoint."""

def __init__(self, context, request):
super().__init__(context, request)
self.params = []

def publishTraverse(self, request, name):
# Treat any path segments after /@review as parameters
self.params.append(name)
return self

def reply(self):
# Disable CSRF protection
alsoProvides(self.request, IDisableCSRFProtection)

match self.params:
case ["approve"]:
# update review_status
Comment thread
jnptk marked this conversation as resolved.
self.context.review_status = "Up-to-date"
# update review_due_date
default_interval = api.portal.get_registry_record(
"kitconcept.intranet.content_review_default_interval"
)
interval = self.context.review_interval or default_interval
self.context.review_due_date = calc_due_date(interval=interval)
# update review_completed_date
self.context.review_completed_date = date.today()
case ["delegate"]:
field = IContentReview["review_assignee"].bind(self.context)
vocabulary = field.vocabulary
data = json_body(self.request)
if comment := data.get("comment"):
self.context.review_comment = comment
assignee = data.get("assignee", None)
if assignee not in vocabulary:
raise BadRequest(f"Assignee not found in vocabulary: {vocabulary}")
self.context.review_assignee = assignee
case ["postpone"]:
self.context.review_status = "Up-to-date"
data = json_body(self.request)
Comment thread
jnptk marked this conversation as resolved.
if comment := data.get("comment"):
self.context.review_comment = comment
due_date = data.get("due_date", None)
if due_date:
self.context.review_due_date = date.fromisoformat(due_date)
case _:
raise BadRequest(
"Unknown action: expected /@review/approve, "
"/@review/delegate, or /@review/postpone"
)
2 changes: 1 addition & 1 deletion backend/src/kitconcept/intranet/upgrades/configure.zcml
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,6 @@
<include package=".v20260217001" />
<include package=".v20260304001" />
<include package=".v20260318001" />

<include package=".v20260401001" />

</configure>
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from BTrees.OIBTree import OIBTree
from kitconcept.intranet import logger
from kitconcept.intranet.behaviors.location import ILocationBehavior
from kitconcept.intranet.behaviors.organisational_unit import (
IOrganisationalUnitBehavior,
Expand All @@ -7,8 +8,6 @@
from zc.relation.interfaces import ICatalog
from zope.component import getUtility

from kitconcept.intranet import logger


def init_vocabulary_cache(context):
"""Initialize the vocabulary cache BTree on the portal."""
Expand Down
Empty file.
Loading
Loading