Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
6f00bb4
WIP: choices
reinhardt Apr 4, 2025
1bc4165
Fix saving and navigation of choices/options in client
reinhardt Apr 7, 2025
9be064d
Fix is_custom_risk
reinhardt Apr 7, 2025
3f76540
A choice is answered if there are any selected options
reinhardt Apr 7, 2025
656fe7e
Choice: add field allow_multiple_options
reinhardt Apr 7, 2025
5a8c05a
getSurveyTree: Include euphorie.choice
reinhardt Apr 7, 2025
2b08ec3
Implement conditions in admin backend
reinhardt Apr 9, 2025
6c0174b
Choice: Fix class inheritance
reinhardt Apr 10, 2025
d1f0a56
Export/import of choices and options
reinhardt Apr 10, 2025
9713aab
Black/flake8/zpretty
reinhardt Apr 28, 2025
79f970d
Fix test
reinhardt Apr 28, 2025
199f46b
Fix navigation
reinhardt Apr 30, 2025
59f56fe
Fix updating session with choices
reinhardt Apr 30, 2025
1350476
isort
reinhardt Apr 30, 2025
683c71b
Fix import and update of allow_multiple_options
reinhardt May 5, 2025
64e083e
Copy options when session is updated
reinhardt May 5, 2025
c39053b
Fix tests
reinhardt May 5, 2025
f1c3850
Add tool type `inventory` and allow Choice only in this type
reinhardt May 6, 2025
b44bb4f
Guard against missing `allow_choice` key
reinhardt May 7, 2025
fc78f17
Do some checks and housekeeping when choice identification view is ca…
reinhardt May 7, 2025
f7e472c
Implement some PR review suggestions
reinhardt May 8, 2025
9e49d38
Choice conditions: Limit to current tool (survey)
reinhardt May 8, 2025
781195c
Don't iterate over the list that you're modifying
reinhardt May 8, 2025
a0ba265
Choices: Selecting no option counts as an answer if multiple answers …
reinhardt May 8, 2025
7d1ab6a
Custom navigation legend for inventory tools
reinhardt May 8, 2025
89c525b
WIP: inventory tool report / recommendations
reinhardt May 13, 2025
abbb94d
Merge branch 'main' into scrum-2858-report-recommendations
reinhardt May 13, 2025
dab3922
Option view: List recommendations; show parent (choice) title
reinhardt May 13, 2025
c577128
flake8, zpretty
reinhardt May 13, 2025
ff453bc
report_inventory: limit to selected options
reinhardt May 13, 2025
b863b50
export/import recommendations
reinhardt May 13, 2025
d3c2b96
Extend inventory report structure
reinhardt May 14, 2025
bbad2e6
Inventory report PDF preparations
reinhardt May 14, 2025
92b9b12
Inventory report: PDF version
reinhardt May 14, 2025
a804a0d
Hook up inventory report
reinhardt May 14, 2025
14e6e81
Recommendation view
reinhardt May 15, 2025
fac3044
Recommendation report header image
reinhardt May 15, 2025
eff50b4
Inventory report: Don't repeat option headings
reinhardt May 15, 2025
ea4b8cf
Extend inventory report footer
reinhardt May 15, 2025
ab9e8a2
Consider recommendations when checking for tool updates
reinhardt May 15, 2025
675e4d9
Inventory report: Specify order of options
reinhardt May 15, 2025
811e685
Inventory report: Add cover image
reinhardt May 16, 2025
75a1aed
Use `with` for opening files
reinhardt May 16, 2025
09913bf
Inventory report: Order choices and options as in container
reinhardt May 16, 2025
25e2436
Simplify recommendation view
reinhardt May 19, 2025
da26ce0
Choice identification: Show recommendations as immediate feedback
reinhardt Jun 17, 2025
61a3425
Inventory tool: Allow turning immediate feedback on and off
reinhardt Jun 17, 2025
1e32659
flake8
reinhardt Jun 17, 2025
d35cf77
Choice identification: Show choice title on feedback page
reinhardt Jun 23, 2025
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
151 changes: 151 additions & 0 deletions src/euphorie/client/browser/choice.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
from euphorie.client import utils
from euphorie.client.navigation import getTreeData
from plone import api
from plone.memoize.instance import memoize
from Products.Five import BrowserView


class IdentificationView(BrowserView):
"""A view for displaying a choice in the identification phase."""

variation_class = "variation-risk-assessment"

@property
@memoize
def webhelpers(self):
return api.content.get_view("webhelpers", self.context.aq_parent, self.request)

def check_render_condition(self):
# Render the page only if the user can inspection rights,
# otherwise redirect to the start page of the session.
if not self.webhelpers.can_inspect_session:
return self.request.response.redirect(
"{session_url}/@@start".format(
session_url=self.webhelpers.traversed_session.absolute_url()
)
)
if self.webhelpers.redirectOnSurveyUpdate():
return

@property
@memoize
def navigation(self):
return api.content.get_view("navigation", self.context, self.request)

def _get_next(self, reply):
_next = reply.get("next", None)
# In Safari browser we get a list
if isinstance(_next, list):
_next = _next.pop()
return _next

@property
def tree(self):
return getTreeData(self.request, self.context)

@property
@memoize
def session(self):
return self.webhelpers.traversed_session.session

@property
@memoize
def survey(self):
"""This is the survey dexterity object."""
return self.webhelpers._survey

@property
@memoize
def choice(self):
return self.webhelpers.traversed_session.restrictedTraverse(
self.context.zodb_path.split("/")
)

@property
@memoize
def selected(self):
return [option.zodb_path for option in self.context.options]

def set_answer_data(self, reply):
"""Save the selected options as indicated by the paths in the `answer`
field of `reply` (i.e. the request form).
If the choice allows multiple options, then selecting none of them counts as
a valid answer. In this case the `postponed` attribute is set to True.

Note that this use of the `postponed` attribute does not exactly match
the use for risks in that we don't expect the user to come back and
answer later, but it is similar in that we record that the user has
been here and clicked “Save” rather than “Skip”.
"""
answer = reply.get("answer", [])
if self.choice.allow_multiple_options:
self.context.postponed = answer == "postponed"
if answer == "postponed":
answer = []
if not isinstance(answer, (list, tuple)):
answer = [answer]
# XXX Check if paths are valid?
# for path in answer[:]:
# try:
# self.webhelpers.traversed_session.restrictedTraverse(path)
# except KeyError:
# answer.remove(path)
return self.context.set_options_by_zodb_path(answer)

def __call__(self):
# Render the page only if the user has inspection rights,
# otherwise redirect to the start page of the session.
if not self.webhelpers.can_inspect_session:
return self.request.response.redirect(
self.context.aq_parent.absolute_url() + "/@@start"
)
self.check_render_condition()

utils.setLanguage(self.request, self.survey, self.survey.language)

if self.request.method == "POST":
reply = self.request.form
if not self.webhelpers.can_edit_session:
return self.navigation.proceed_to_next(reply)
_next = self._get_next(reply)
# Don't persist anything if the user skipped the question
if _next == "skip":
return self.navigation.proceed_to_next(reply)

changed = self.set_answer_data(reply)

if changed:
self.session.touch()

return self.navigation.proceed_to_next(reply)
return self.index()


class IdentificationFeedbackView(IdentificationView):
"""A view for displaying feedback on a selected option in the identification
phase."""

variation_class = "variation-risk-assessment"

def set_answer_data(self, reply):
# has already been done in @@identification
return False

@property
@memoize
def recommendations(self):
zodb_options = [
self.webhelpers.traversed_session.restrictedTraverse(option.zodb_path)
for option in self.context.options
]
recommendation_texts = []
for zodb_option in zodb_options:
for recommendation in zodb_option.values():
if recommendation.portal_type == "euphorie.recommendation":
recommendation_texts.append(recommendation.text)
return recommendation_texts

def __call__(self):
if not self.recommendations:
return self.navigation.proceed_to_next({"next": "next"})
return super().__call__()
45 changes: 45 additions & 0 deletions src/euphorie/client/browser/configure.zcml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,14 @@
layer="euphorie.client.interfaces.IClientSkinLayer"
/>

<browser:page
name="navigation"
for="*"
class="euphorie.client.browser.navigation.NavigationView"
permission="zope.Public"
layer="euphorie.client.interfaces.IClientSkinLayer"
/>

<browser:page
name="risk_macros"
for="*"
Expand Down Expand Up @@ -702,6 +710,25 @@
layer="euphorie.client.interfaces.IClientSkinLayer"
/>

<!-- Choice -->
<browser:page
name="identification"
for="euphorie.client.model.Choice"
class=".choice.IdentificationView"
template="templates/choice_identification.pt"
permission="euphorie.client.ViewSurvey"
layer="euphorie.client.interfaces.IClientSkinLayer"
/>

<browser:page
name="identification_feedback"
for="euphorie.client.model.Choice"
class=".choice.IdentificationFeedbackView"
template="templates/choice_identification_feedback.pt"
permission="euphorie.client.ViewSurvey"
layer="euphorie.client.interfaces.IClientSkinLayer"
/>

<browser:page
name="tool-more-info"
for="euphorie.client.adapters.session_traversal.ITraversedSurveySession"
Expand Down Expand Up @@ -1035,6 +1062,24 @@
layer="euphorie.client.interfaces.IClientSkinLayer"
/>

<browser:page
name="report_inventory"
for="euphorie.client.adapters.session_traversal.ITraversedSurveySession"
class=".report.ReportInventory"
template="templates/report_inventory.pt"
permission="euphorie.client.ViewSurvey"
layer="euphorie.client.interfaces.IClientSkinLayer"
/>

<browser:page
name="report_inventory_pdf"
for="euphorie.client.adapters.session_traversal.ITraversedSurveySession"
class=".report.ReportInventory"
template="templates/report_inventory_pdf.pt"
permission="euphorie.client.ViewSurvey"
layer="euphorie.client.interfaces.IClientSkinLayer"
/>

<!-- Sector -->
<browser:page
name="view"
Expand Down
108 changes: 108 additions & 0 deletions src/euphorie/client/browser/navigation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
from euphorie.client import model
from euphorie.client.interfaces import CustomRisksModifiedEvent
from euphorie.client.navigation import FindNextQuestion
from euphorie.client.navigation import FindPreviousQuestion
from plone import api
from plone.memoize.instance import memoize
from Products.Five import BrowserView
from sqlalchemy import and_
from z3c.saconfig import Session
from zope.event import notify


class NavigationView(BrowserView):
question_filter = None

@property
@memoize
def webhelpers(self):
return api.content.get_view("webhelpers", self.context, self.request)

@property
@memoize
def session(self):
return self.webhelpers.traversed_session.session

@property
@memoize
def previous_question(self):
return FindPreviousQuestion(
self.context, dbsession=self.session, filter=self.question_filter
)

@property
@memoize
def next_question(self):
return FindNextQuestion(
self.context, dbsession=self.session, filter=self.question_filter
)

def proceed_to_next(self, reply):
_next = reply.get("next", None)
# In Safari browser we get a list
if isinstance(_next, list):
_next = _next.pop()
if _next == "previous":
target = self.previous_question
if target is None:
# We ran out of questions, step back to intro page
url = "{session_url}/@@identification".format(
session_url=self.webhelpers.traversed_session.absolute_url()
)
return self.request.response.redirect(url)
elif _next == "feedback":
url = self.context.absolute_url() + "/@@identification_feedback"
return self.request.response.redirect(url)
elif _next in ("next", "skip"):
target = self.next_question
if target is None:
# We ran out of questions, proceed to the action plan
if self.webhelpers.use_action_plan_phase:
next_view_name = "@@actionplan"
elif self.webhelpers.use_consultancy_phase:
next_view_name = "@@consultancy"
else:
next_view_name = "@@report"
base_url = self.webhelpers.traversed_session.absolute_url()
url = f"{base_url}/{next_view_name}"
return self.request.response.redirect(url)

elif _next == "add_custom_risk" and self.webhelpers.can_edit_session:
sql_module = (
Session.query(model.Module)
.filter(
and_(
model.SurveyTreeItem.session == self.session,
model.Module.zodb_path == "custom-risks",
)
)
.first()
)
if not sql_module:
url = self.context.absolute_url() + "/@@identification"
return self.request.response.redirect(url)

view = api.content.get_view("identification", sql_module, self.request)
view.add_custom_risk()
notify(CustomRisksModifiedEvent(self.context.aq_parent))
risk_id = self.context.aq_parent.children().count()
# Construct the path to the newly added risk: We know that there
# is only one custom module, so we can take its id directly. And
# to that we can append the risk id.
url = "{session_url}/{module}/{risk}/@@identification".format(
session_url=self.webhelpers.traversed_session.absolute_url(),
module=sql_module.getId(),
risk=risk_id,
)
return self.request.response.redirect(url)
elif _next == "actionplan":
url = self.webhelpers.traversed_session.absolute_url() + "/@@actionplan"
return self.request.response.redirect(url)
# stay on current risk
else:
target = self.context
url = ("{session_url}/{path}/@@identification").format(
session_url=self.webhelpers.traversed_session.absolute_url(),
path="/".join(target.short_path),
)
return self.request.response.redirect(url)
Loading
Loading