Skip to content

Commit 25a57c2

Browse files
committed
Add to enable working copy support also on trainings
1 parent 6403e88 commit 25a57c2

File tree

8 files changed

+245
-1
lines changed

8 files changed

+245
-1
lines changed

backend/news/17.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Enable working copy support also for Trainings. @datakurre

backend/src/ploneconf/core/profiles/default/metadata.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<metadata>
3-
<version>20250905001</version>
3+
<version>20250919001</version>
44
<dependencies>
55
<dependency>profile-plone.volto:default</dependency>
66
<dependency>profile-collective.volto.formsupport:default</dependency>

backend/src/ploneconf/core/profiles/default/types.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,8 @@
1212
<object meta_type="Dexterity FTI"
1313
name="Talk"
1414
/>
15+
<object meta_type="Dexterity FTI"
16+
name="Training"
17+
/>
1518

1619
</object>
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<object xmlns:i18n="http://xml.zope.org/namespaces/i18n"
3+
meta_type="Dexterity FTI"
4+
name="Training"
5+
i18n:domain="collective.techevent"
6+
>
7+
8+
<!-- Enabled behaviors -->
9+
<property name="behaviors"
10+
purge="false"
11+
>
12+
<element value="plone.locking" />
13+
<element value="collective.techevent.presenter_roles" />
14+
</property>
15+
16+
</object>

backend/src/ploneconf/core/profiles/default/workflows.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,5 +35,8 @@
3535
<type type_id="Presenter">
3636
<bound-workflow workflow_id="iterated_publication_workflow" />
3737
</type>
38+
<type type_id="Training">
39+
<bound-workflow workflow_id="iterated_publication_workflow" />
40+
</type>
3841
</bindings>
3942
</object>
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
from Acquisition import aq_base
2+
from plone import api
3+
from Products.CMFCore.utils import getToolByName
4+
5+
6+
def move_workflow_history(context):
7+
"""Move workflow history from old to new workflow."""
8+
pc = api.portal.get_tool("portal_catalog")
9+
from_id = "simple_publication_workflow"
10+
to_id = "iterated_publication_workflow"
11+
portal_type = ["Training"]
12+
brains = pc.unrestrictedSearchResults(portal_type=portal_type)
13+
for brain in brains:
14+
obj = brain._unrestrictedGetObject()
15+
history = getattr(aq_base(obj), "workflow_history", {}) or {}
16+
if from_id in history:
17+
history[to_id] = history.pop(from_id)
18+
19+
20+
def update_role_mappings(context):
21+
"""Update permissions after workflow changes."""
22+
portal = api.portal.get()
23+
wf_tool = api.portal.get_tool("portal_workflow")
24+
wf_tool._recursiveUpdateRoleMappings(
25+
portal,
26+
{
27+
"iterated_publication_workflow": wf_tool.getWorkflowById(
28+
"iterated_publication_workflow"
29+
)
30+
},
31+
)
32+
33+
34+
def update_attendees_permissions(context):
35+
"""Update attendees container permission to allow working copies."""
36+
pc = api.portal.get_tool("portal_catalog")
37+
brains = pc.unrestrictedSearchResults(portal_type="Attendees")
38+
for brain in brains:
39+
obj = brain._unrestrictedGetObject()
40+
default_roles = ["Manager", "Site Administrator", "Owner", "Contributor"]
41+
permissions = [
42+
"collective.techevent: Add Talk",
43+
"collective.techevent: Add Keynote",
44+
"collective.techevent: Add Training",
45+
]
46+
for permission in permissions:
47+
obj.manage_permission(
48+
permission,
49+
roles=default_roles,
50+
acquire=False,
51+
)
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<configure
2+
xmlns="http://namespaces.zope.org/zope"
3+
xmlns:genericsetup="http://namespaces.zope.org/genericsetup"
4+
>
5+
6+
<genericsetup:upgradeSteps
7+
profile="ploneconf.core:default"
8+
source="20250905001"
9+
destination="20250919001"
10+
/>
11+
<genericsetup:upgradeDepends
12+
title="Apply presenter_roles behavior to trainings; Allow attendees to draft iterate content"
13+
import_steps="typeinfo"
14+
/>
15+
<genericsetup:upgradeDepends
16+
title="Restrict check in permission to Site Administrator"
17+
import_steps="rolemap"
18+
/>
19+
<genericsetup:upgradeDepends
20+
title="Requires Editor role to use check out on published content"
21+
import_steps="workflow"
22+
/>
23+
<genericsetup:upgradeStep
24+
title="Move simple_publication_workflow history to iterated_publication_workflow"
25+
handler=".move_workflow_history"
26+
/>
27+
<genericsetup:upgradeStep
28+
title="Reindex content permissions after workflow update"
29+
handler=".update_role_mappings"
30+
/>
31+
<genericsetup:upgradeStep
32+
title="Update attendees container permission to allow working copies"
33+
handler=".update_attendees_permissions"
34+
/>
35+
</configure>
36+
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import os
2+
import uuid
3+
import requests
4+
PLONE_URL = "https://2025.ploneconf.org/++api++"
5+
AUTH_HEADER = os.environ.get("AUTHORIZATION")
6+
7+
if not AUTH_HEADER:
8+
raise RuntimeError("Please set AUTHORIZATION environment variable")
9+
10+
HEADERS = {
11+
"Accept": "application/json",
12+
"Authorization": AUTH_HEADER,
13+
"Content-Type": "application/json"
14+
}
15+
16+
17+
def get_all_items_by_type(content_type):
18+
"""Fetch all content items of a given type using @search with fullobjects=1"""
19+
items = []
20+
start = 0
21+
batch_size = 100
22+
23+
while True:
24+
params = {
25+
"portal_type": content_type,
26+
"fullobjects": 1,
27+
"b_size": batch_size,
28+
"b_start": start,
29+
}
30+
resp = requests.get(f"{PLONE_URL}/@search", headers=HEADERS, params=params)
31+
resp.raise_for_status()
32+
data = resp.json()
33+
batch = data.get("items", [])
34+
if not batch:
35+
break
36+
37+
items.extend(batch)
38+
start += len(batch)
39+
if len(batch) < batch_size:
40+
break
41+
42+
return items
43+
44+
45+
def normalize_people(items):
46+
"""Extract (email, first_name, last_name, @id, id) from search items"""
47+
people = []
48+
for it in items:
49+
email = it.get("email")
50+
title = it.get("title", "")
51+
if email:
52+
parts = title.strip().split()
53+
first_name = parts[0] if parts else ""
54+
last_name = " ".join(parts[1:]) if len(parts) > 1 else ""
55+
people.append({
56+
"email": email.lower(),
57+
"first_name": first_name,
58+
"last_name": last_name,
59+
"url": it.get("@id"),
60+
"id": it.get("id"),
61+
})
62+
return people
63+
64+
65+
def create_attendee(container_url, first_name, last_name, email):
66+
"""POST a new Attendee object with a UUID4 as id and return its id"""
67+
new_id = str(uuid.uuid4())
68+
payload = {
69+
"@type": "Attendee",
70+
"id": new_id,
71+
"first_name": first_name,
72+
"last_name": last_name,
73+
"email": email
74+
}
75+
resp = requests.post(container_url, headers=HEADERS, json=payload)
76+
resp.raise_for_status()
77+
data = resp.json()
78+
return data.get("id", new_id)
79+
80+
81+
def grant_editor_role_on_presenter(presenter_url, attendee_id):
82+
"""POST to @sharing endpoint to give Attendee Editor role"""
83+
presenter_url = presenter_url.replace(PLONE_URL.rsplit("/", 1)[0], PLONE_URL)
84+
payload = {
85+
"entries": [
86+
{
87+
"id": attendee_id,
88+
"roles": {
89+
"Contributor": False,
90+
"Editor": True,
91+
"Reader": False,
92+
"Reviewer": False
93+
},
94+
"type": "user"
95+
}
96+
],
97+
"inherit": True
98+
}
99+
resp = requests.post(f"{presenter_url}/@sharing", headers=HEADERS, json=payload)
100+
resp.raise_for_status()
101+
102+
103+
def main():
104+
print("Fetching all Presenters...")
105+
presenters = normalize_people(get_all_items_by_type("Presenter"))
106+
print(f"Found {len(presenters)} Presenters")
107+
108+
print("Fetching all Attendees...")
109+
attendees = normalize_people(get_all_items_by_type("Attendee"))
110+
print(f"Found {len(attendees)} Attendees")
111+
112+
attendee_emails = {a["email"] for a in attendees}
113+
114+
missing = [p for p in presenters if p["email"] not in attendee_emails]
115+
116+
print(f"\nFound {len(missing)} missing attendees to create...")
117+
118+
for m in missing:
119+
print(f"Creating attendee: {m['first_name']} {m['last_name']} <{m['email']}>")
120+
attendee_id = create_attendee(
121+
f"{PLONE_URL}/attendees",
122+
m["first_name"],
123+
m["last_name"],
124+
m["email"]
125+
)
126+
127+
print(f"Granting Editor to {attendee_id} on {m['url']}")
128+
grant_editor_role_on_presenter(m["url"], attendee_id)
129+
130+
print("\nAll missing attendees created and given Editor on their Presenter page.")
131+
132+
133+
if __name__ == "__main__":
134+
main()

0 commit comments

Comments
 (0)