Skip to content

Commit e7b6c10

Browse files
committed
Allow SPV users to return protocol excerpt to proposal dossier
1 parent ad3b174 commit e7b6c10

9 files changed

Lines changed: 216 additions & 0 deletions

File tree

docs/public/dev-manual/api/api_changelog.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ Breaking Changes
1313
Other Changes
1414
^^^^^^^^^^^^^
1515
- ``@membership-notes``: Add endpoint to update a note on a membership.
16+
- ``@ris-return-excerpt``: Add endpoint to allow users from spv to create a proposal excerpt in a dossier they do not have view permission in.
1617

1718

1819
2025.8.0 (2025-08-22)

docs/public/dev-manual/api/proposals.rst

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,3 +159,42 @@ Die Response ist eine Liste die für jede Beilage die folgenden Informationen zu
159159
"source": "dossier-1/proposal-1/document-76"
160160
}
161161
]
162+
163+
164+
Protokollauszug im Antragsdossier ablegen
165+
=========================================
166+
167+
Mit dem ``@ris-return-excerpt`` Endpoint können Protokollauszüge aus der SPV
168+
ins Antragsdossier eingereichtwerden. Der Endpoint erwartet als Pfad Parameter:
169+
170+
- Mandant ID
171+
- relative Dossierpfad
172+
- Vermerk als String
173+
174+
175+
**Beispiel-Request**:
176+
177+
.. sourcecode:: http
178+
179+
POST ordnungssystem/dossier-1/document-1/@ris-return-excerpt HTTP/1.1
180+
Accept: application/json
181+
182+
{
183+
"target_admin_unit_id": "fd",
184+
"target_dossier_relative_path": "ordnungssystem/dossier-1"
185+
}
186+
187+
**Beispiel-Response**:
188+
189+
190+
.. sourcecode:: http
191+
192+
HTTP/1.1 200 OK
193+
Content-Type: application/json
194+
195+
{
196+
"path": "ordnungssystem/dossier-2/document-2",
197+
"intid": 3,
198+
"url": "http://gever.onegovgever.ch/fd/ordnungssystem/dossier-2/document-2",
199+
"current_version_id": 1,
200+
}

opengever/core/profiles/default/rolemap.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,12 @@
284284
<role name="Administrator" />
285285
</permission>
286286

287+
<!-- RIS -->
288+
<permission name="opengever.ris: Return Excerpt" acquire="False">
289+
<role name="Manager" />
290+
<role name="Member" />
291+
</permission>
292+
287293
<!-- SHARING -->
288294
<permission name="Sharing page: Delegate Administrator role" acquire="True">
289295
<role name="Administrator" />

opengever/core/upgrades/20250924150922_add_ris_return_excerpt_permission/__init__ .py

Whitespace-only changes.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<rolemap>
2+
<permissions>
3+
4+
<permission name="opengever.ris: Return Excerpt" acquire="True">
5+
<role name="Manager" />
6+
<role name="Member" />
7+
</permission>
8+
9+
</permissions>
10+
</rolemap>
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from ftw.upgrade import UpgradeStep
2+
3+
4+
class AddRisReturnExcerptPermission(UpgradeStep):
5+
"""Add ris return excerpt permission.
6+
"""
7+
8+
def __call__(self):
9+
self.install_upgrade_profile()

opengever/ris/browser/configure.zcml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
<configure
22
xmlns="http://namespaces.zope.org/zope"
33
xmlns:browser="http://namespaces.zope.org/browser"
4+
xmlns:plone="http://namespaces.plone.org/plone"
45
i18n_domain="opengever.ris">
56

7+
<include package="plone.rest" file="meta.zcml" />
8+
69
<browser:page
710
name="proposal_transition_controller"
811
for="opengever.ris.proposal.IProposal"
@@ -38,4 +41,21 @@
3841
permission="cmf.ModifyPortalContent"
3942
/>
4043

44+
<plone:service
45+
name="@ris-return-excerpt"
46+
for="opengever.document.document.IDocumentSchema"
47+
method="POST"
48+
factory="opengever.ris.browser.ris_excerpt.RISReturnExcerptService"
49+
permission="opengever.ris.ReturnExcerpt"
50+
layer="opengever.base.interfaces.IOpengeverBaseLayer"
51+
/>
52+
53+
<browser:page
54+
name="receive-ris-return-excerpt"
55+
for="opengever.dossier.behaviors.dossier.IDossierMarker"
56+
class="opengever.ris.browser.ris_excerpt.RISReturnExcerptReceive"
57+
permission="zope.Public"
58+
layer="opengever.ogds.base.interfaces.IInternalOpengeverRequestLayer"
59+
/>
60+
4161
</configure>
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
from Acquisition import aq_inContextOf
2+
from Acquisition import aq_inner
3+
from opengever.base.json_response import JSONResponse
4+
from opengever.base.security import elevated_privileges
5+
from opengever.base.transport import PrivilegedReceiveObject
6+
from opengever.base.transport import Transporter
7+
from plone import api
8+
from plone.protect.interfaces import IDisableCSRFProtection
9+
from plone.restapi.deserializer import json_body
10+
from plone.restapi.services import Service
11+
from z3c.relationfield.relation import RelationValue
12+
from zExceptions import BadRequest
13+
from zope.component import getUtility
14+
from zope.interface import alsoProvides
15+
from zope.intid.interfaces import IIntIds
16+
import json
17+
18+
19+
class RISReturnExcerptService(Service):
20+
21+
def reply(self):
22+
alsoProvides(self.request, IDisableCSRFProtection)
23+
24+
if not api.user.has_permission("View", obj=self.context):
25+
return JSONResponse(self.request).error("Forbidden", status=403).dump()
26+
27+
data = json_body(self.request) or {}
28+
target_cid = data.get("target_admin_unit_id")
29+
container_path = data.get("target_dossier_relative_path")
30+
proposal_relative_path = data.get("proposal_relative_path")
31+
32+
if not target_cid or not container_path:
33+
return (
34+
JSONResponse(self.request)
35+
.error(
36+
"Target admin_unit_id and dossier_url are required.",
37+
status=400,
38+
)
39+
.dump()
40+
)
41+
42+
container_path = container_path.lstrip("/")
43+
44+
result = Transporter().transport_to(
45+
obj=self.context,
46+
target_cid=target_cid,
47+
container_path=container_path,
48+
view="receive-ris-return-excerpt",
49+
proposal_relative_path=proposal_relative_path,
50+
finalize=data.get("finalize", True),
51+
)
52+
return result
53+
54+
55+
class RISReturnExcerptReceive(PrivilegedReceiveObject):
56+
"""Receiver on the target dossier. Runs with elevated privileges."""
57+
58+
def __call__(self):
59+
obj = self.receive()
60+
portal = self.context.portal_url.getPortalObject()
61+
portal_path = "/".join(portal.getPhysicalPath())
62+
63+
intids = getUtility(IIntIds)
64+
65+
data = {
66+
"path": "/".join(obj.getPhysicalPath())[len(portal_path) + 1:],
67+
"intid": intids.queryId(obj),
68+
"url": obj.absolute_url(),
69+
"current_version_id": obj.get_current_version_id(missing_as_zero=True),
70+
}
71+
72+
self.request.response.setHeader("Content-type", "application/json")
73+
return json.dumps(data)
74+
75+
@property
76+
def container(self):
77+
return self.context
78+
79+
def _traverse_relpath(self, relpath):
80+
portal = api.portal.get()
81+
82+
rel = (relpath or "").lstrip("/")
83+
84+
if isinstance(rel, unicode):
85+
rel = rel.encode("utf-8")
86+
try:
87+
return portal.unrestrictedTraverse(rel, default=None)
88+
except Exception:
89+
return None
90+
91+
def _is_within(self, container, obj):
92+
return aq_inContextOf(aq_inner(obj), aq_inner(container))
93+
94+
def _link_as_excerpt(self, proposal, document):
95+
96+
if not hasattr(proposal, "excerpts"):
97+
raise BadRequest("Proposal has no 'excerpts' field.")
98+
99+
proposal.excerpts = [RelationValue(getUtility(IIntIds).getId(document))]
100+
proposal.reindexObject(idxs=["excerpts"])
101+
102+
def receive(self):
103+
data = self.request.form or {}
104+
proposal_path = data.get("proposal_relative_path")
105+
finalize = data.get("finalize", True)
106+
107+
document = super(RISReturnExcerptReceive, self).receive()
108+
109+
if proposal_path:
110+
proposal = self._traverse_relpath(proposal_path)
111+
112+
if proposal is None:
113+
raise BadRequest("Invalid 'proposal_relative_path' (object not found).")
114+
115+
if not self._is_within(self.container, proposal):
116+
raise BadRequest("The proposal is not located in the target dossier.")
117+
118+
self._link_as_excerpt(proposal, document)
119+
120+
if finalize:
121+
try:
122+
with elevated_privileges():
123+
api.content.transition(
124+
obj=document, transition="document-transition-finalize"
125+
)
126+
except Exception:
127+
pass
128+
129+
return document

opengever/ris/permissions.zcml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,6 @@
44

55
<permission id="opengever.ris.AddProposal" title="opengever.ris: Add Proposal" />
66

7+
<permission id="opengever.ris.ReturnExcerpt" title="opengever.ris: Return Excerpt" />
8+
79
</configure>

0 commit comments

Comments
 (0)