Commit a9dc6d61 authored by Jérome Perrin's avatar Jérome Perrin

Support Request App: Dashboard and RSS fixes

Assorted fixes:

 * Fix performance problems on the dashboard graphs by optimizing 
 `SupportRequest_getSupportRequestStatisticsAsJson` 

 * Fix performance problems on RSS by applying a limit correctly
 
 * Fix rendering of RSS in some corner cases security configurations (with documents user cannot view)




/reviewed-on nexedi/erp5!795
parents bbbe264a 45a23b27
......@@ -112,7 +112,7 @@
<item>
<key> <string>text_content</string> </key>
<value> <string>CACHE MANIFEST\n
# v1.0.5\n
# v1.0.6\n
CACHE:\n
font-awesome/font-awesome-webfont.woff2\n
echarts-all.js\n
......@@ -360,7 +360,7 @@ NETWORK:\n
</tuple>
<state>
<tuple>
<float>1540870337.1</float>
<float>1542010989.1</float>
<string>GMT+9</string>
</tuple>
</state>
......
......@@ -177,7 +177,10 @@
return result;
})
.push(function (result_list) {
var sp_data = result_list[0], graph_gadget_1 = result_list[1], graph_gadget_2 = result_list[2];
var sp_data = result_list[0],
graph_gadget_1 = result_list[1],
graph_gadget_2 = result_list[2],
count_by_state_and_date_range = sp_data.count_by_state_and_date_range;
gadget.property_dict.graph_widget = graph_gadget_1;
return RSVP.all([graph_gadget_1.render(
{
......@@ -185,45 +188,30 @@
data: [
{
value_dict: {
0: ["< 2", "2-7", "7-30", "> 30"],
1: [
sp_data.le2.validated,
sp_data['2to7'].validated,
sp_data['7to30'].validated,
sp_data.gt30.validated
]
0: count_by_state_and_date_range.validated.date_range_list,
1: count_by_state_and_date_range.validated.count_list
},
colors: ['#d48265'],
type: "bar",
title: "Opened"
title: sp_data.state_title_by_state_id.validated
},
{
value_dict: {
0: ["< 2", "2-7", "7-30", "> 30"],
1: [
sp_data.le2.submitted,
sp_data['2to7'].submitted,
sp_data['7to30'].submitted,
sp_data.gt30.submitted
]
0: count_by_state_and_date_range.submitted.date_range_list,
1: count_by_state_and_date_range.submitted.count_list
},
colors: ['#61a0a8'],
type: "bar",
title: "Submitted"
title: sp_data.state_title_by_state_id.submitted
},
{
value_dict: {
0: ["< 2", "2-7", "7-30", "> 30"],
1: [
sp_data.le2.suspended,
sp_data['2to7'].suspended,
sp_data['7to30'].suspended,
sp_data.gt30.suspended
]
0: count_by_state_and_date_range.suspended.date_range_list,
1: count_by_state_and_date_range.suspended.count_list
},
colors: ['#c23531'],
type: "bar",
title: "Suspended"
title: sp_data.state_title_by_state_id.suspended
}
],
layout: {
......@@ -249,8 +237,18 @@
data: [
{
value_dict: {
0: ["Opened", "Submitted", "Suspended", "Closed"],
1: [sp_data.validated, sp_data.submitted, sp_data.suspended, sp_data.invalidated]
0: [
sp_data.state_title_by_state_id.validated,
sp_data.state_title_by_state_id.submitted,
sp_data.state_title_by_state_id.suspended,
sp_data.state_title_by_state_id.invalidated
],
1: [
sp_data.count_by_state.validated || 0,
sp_data.count_by_state.submitted || 0,
sp_data.count_by_state.suspended || 0,
sp_data.count_by_state.invalidated || 0
],
},
colors: ['#d48265', '#61a0a8', '#c23531', '#2f4554'],
type: "pie",
......
......@@ -258,8 +258,8 @@
</tuple>
<state>
<tuple>
<float>1506606769.12</float>
<string>UTC</string>
<float>1542008780.46</float>
<string>GMT+9</string>
</tuple>
</state>
</object>
......
"""Returns the workflow states titles for ticket workflow, keyed by state id.
This script has proxy role, as only manager can access workflow configuration.
"""
from Products.ERP5Type.Message import translateString
portal = context.getPortalObject()
info = {}
workflow = portal.portal_workflow.ticket_workflow
for state in workflow['states'].objectValues():
state_title = state.title_or_id()
if 0:
# We don't translate yet, it needs several other fixes
# see https://lab.nexedi.com/nexedi/erp5/merge_requests/778
state_title = unicode(translateString(
'%s [state in %s]' % (state_title, workflow.getId()),
default=unicode(translateString(state_title))))
info[state.getId()] = state_title
return info
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="PythonScript" module="Products.PythonScripts.PythonScript"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>Script_magic</string> </key>
<value> <int>3</int> </value>
</item>
<item>
<key> <string>_bind_names</string> </key>
<value>
<object>
<klass>
<global name="NameAssignments" module="Shared.DC.Scripts.Bindings"/>
</klass>
<tuple/>
<state>
<dictionary>
<item>
<key> <string>_asgns</string> </key>
<value>
<dictionary>
<item>
<key> <string>name_container</string> </key>
<value> <string>container</string> </value>
</item>
<item>
<key> <string>name_context</string> </key>
<value> <string>context</string> </value>
</item>
<item>
<key> <string>name_m_self</string> </key>
<value> <string>script</string> </value>
</item>
<item>
<key> <string>name_subpath</string> </key>
<value> <string>traverse_subpath</string> </value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</state>
</object>
</value>
</item>
<item>
<key> <string>_params</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>_proxy_roles</string> </key>
<value>
<tuple>
<string>Manager</string>
</tuple>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>ERP5Site_getTicketWorkflowStateInfoDict</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
......@@ -4,6 +4,9 @@ from Products.PythonScripts.standard import Object
portal = context.getPortalObject()
document_type_list = portal.getPortalDocumentTypeList()
# for safety, we limit at 100 lines
list_lines = min(list_lines, 100)
def makeLine(kw):
return Object(**kw)
......@@ -13,7 +16,11 @@ def getSupportRequestInfo(event):
try:
return getSupportRequest_memo[follow_up]
except KeyError:
support_request = portal.restrictedTraverse(follow_up)
support_request = portal.restrictedTraverse(follow_up, None)
if support_request is None:
# For corner cases where user has an event for which he cannot access the ticket,
# we don't raise error so that others events are visible.
return event.getTitle(), '', ''
getSupportRequest_memo[follow_up] = (
support_request.getTitle(),
support_request.getResourceTranslatedTitle() or '',
......@@ -31,7 +38,7 @@ for brain in portal.portal_simulation.getMovementHistoryList(
omit_input=True,
# XXX we still don't have getCurrentMovementHistoryList
simulation_state=('started', 'stopped', 'delivered'),
limit=limit,
limit=list_lines,
sort_on=(('stock.date', 'desc'),
('uid', 'desc')),):
event = brain.getObject()
......@@ -51,7 +58,7 @@ for brain in portal.portal_simulation.getMovementHistoryList(
'thumbnail': ( # XXX this is not really a thumbnail, but it's what RSS style uses for <enclosure/>
# Also, with this `thumbnail` it will look good for image, and most of the time
# users attach a screenshot of their problem.
event.getDefaultAggregate(portal_type=document_type_list)
event.getDefaultAggregate(portal_type=document_type_list, checked_permission="View")
and event.getDefaultAggregateValue(portal_type=document_type_list).File_getDownloadUrl()
or None)
}
......
......@@ -50,7 +50,7 @@
</item>
<item>
<key> <string>_params</string> </key>
<value> <string>limit=50, **kw</string> </value>
<value> <string>list_lines=30, **kw</string> </value>
</item>
<item>
<key> <string>id</string> </key>
......
from datetime import timedelta
from collections import defaultdict
from json import dumps
portal = context.getPortalObject()
......@@ -9,42 +10,59 @@ date_2_midnight = DateTime(now_date - timedelta(days=2)).earliestTime()
date_7_midnight = DateTime(now_date - timedelta(days=7)).earliestTime()
date_30_midnight = DateTime(now_date - timedelta(days=30)).earliestTime()
support_request_list = portal.portal_catalog(
portal_type="Support Request",
select_list=['simulation_state', 'start_date'],
**{"delivery.start_date": {"query": DateTime(now_date), "range": "ngt"}}
)
count_by_state = {}
count_by_date = {"le2": {}, "2to7": {}, "7to30": {}, "gt30": {}}
for sr in support_request_list:
sr = sr.getObject()
sr_date = sr.getStartDate()
sr_state = sr.getSimulationState()
if sr_state not in count_by_state:
count_by_state[sr_state] = 0
if sr_state not in count_by_date["le2"]:
for date_category in count_by_date:
count_by_date[date_category][sr_state] = 0
# Count the "pipe" of current support requests by state and date
# this catalog search is not limited in time, but it only selects the
# currently active support requests by state. Unless CRM agents are late
# in the processing of these support requests, they should not be too many.
count_by_state_and_date_range = defaultdict(lambda:defaultdict(int))
for brain in portal.portal_catalog(
portal_type="Support Request",
simulation_state=("submitted", "suspended", "validated",),
select_dict={"simulation_state": None, "start_date": "delivery.start_date"},):
sr_date = brain.start_date
sr_state = brain.simulation_state
if sr_date >= date_2_midnight:
count_by_date["le2"][sr_state] = count_by_date["le2"][sr_state] + 1
count_by_state_and_date_range[sr_state]["< 2"] = \
count_by_state_and_date_range[sr_state]["< 2"] + 1
elif sr_date >= date_7_midnight:
count_by_date["2to7"][sr_state] = count_by_date["2to7"][sr_state] + 1
count_by_state_and_date_range[sr_state]["2-7"] = \
count_by_state_and_date_range[sr_state]["2-7"] + 1
elif sr_date >= date_30_midnight:
count_by_date["7to30"][sr_state] = count_by_date["7to30"][sr_state] + 1
count_by_state_and_date_range[sr_state]["7-30"] = \
count_by_state_and_date_range[sr_state]["7-30"] + 1
else:
count_by_date["gt30"][sr_state] = count_by_date["gt30"][sr_state] + 1
if sr_date < date_30_midnight:
continue
count_by_state_and_date_range[sr_state]["> 30"] = \
count_by_state_and_date_range[sr_state]["> 30"] + 1
# We have
# { state: { date_range: count } }
# but we need to turn it into:
# { state : {date_range_list: [date_range], count_list: [count], }
# with the date range sorted as `date_step_list`
date_range_list = ("< 2", "2-7", "7-30", "> 30")
count_by_state_and_date_range = {
state: {
"date_range_list": date_range_list,
"count_list": [count_by_state_and_date_range[state][date_range]
for date_range in date_range_list ]
} for state in count_by_state_and_date_range }
# Count last month activity by state
# we only select support requests from last 30 days, so there should not be too many.
count_by_state = defaultdict(int)
for brain in portal.portal_catalog(
portal_type="Support Request",
select_dict={"simulation_state": None},
**{"delivery.start_date": {"query": date_30_midnight, "range": ">="}}):
sr_state = brain.simulation_state
count_by_state[sr_state] = count_by_state[sr_state] + 1
result = {}
result.update(count_by_state)
result.update(count_by_date)
return dumps(result)
return dumps({
"count_by_state": count_by_state,
"count_by_state_and_date_range": count_by_state_and_date_range,
"state_title_by_state_id": portal.ERP5Site_getTicketWorkflowStateInfoDict()
})
......@@ -379,7 +379,7 @@ class TestSupportRequestCommentOnExistingSupportRequest(SupportRequestTestCase):
self.assertEqual([], json.loads(support_request.SupportRequest_getCommentPostListAsJson()))
class TestSupportRequestRSS(SupportRequestTestCase):
class SupportRequestRSSTestCase(SupportRequestTestCase):
# XXX token PAS plugin is not set up automatically when installing erp5_access_token
# so we set it up the same way test.erp5.testERP5AccessTokenSkins is setting it up
def _setupAccessTokenExtraction(self):
......@@ -418,6 +418,21 @@ class TestSupportRequestRSS(SupportRequestTestCase):
self.event.stop()
self.tic()
class DefaultTestRSSMixin(object):
"""Mixin to request RSS and check the content with `_checkRSS`, that
should be provided by another class.
"""
def test_RSS(self):
response = self.publish(
"%s/support_request_module/SupportRequestModule_viewLastSupportRequestListAsRss" % self.getWebSite().getPath(),
basic='%s:%s' % (self.user.erp5_login.getReference(), self.user_password))
self._checkRSS(response)
class TestSupportRequestRSSSOneEvent(SupportRequestRSSTestCase, DefaultTestRSSMixin):
"""Tests for simple cases of RSS with only one event.
"""
def _checkRSS(self, response):
self.assertEqual(httplib.OK, response.getStatus())
rss = feedparser.parse(response.getBody())
......@@ -434,12 +449,6 @@ class TestSupportRequestRSS(SupportRequestTestCase):
# https://pythonhosted.org/feedparser/bozo.html#advanced-bozo
self.assertFalse(rss.bozo)
def test_RSS(self):
response = self.publish(
"%s/support_request_module/SupportRequestModule_viewLastSupportRequestListAsRss" % self.getWebSite().getPath(),
basic='%s:%s' % (self.user.erp5_login.getReference(), self.user_password))
self._checkRSS(response)
def test_RSS_with_non_accessible_events(self):
non_visible_event = self.portal.event_module.newContent(
portal_type='Web Message',
......@@ -472,6 +481,92 @@ class TestSupportRequestRSS(SupportRequestTestCase):
self._checkRSS(self.publish(restricted_access_url))
class TestSupportRequestRSSSMultipleEvents(SupportRequestRSSTestCase, DefaultTestRSSMixin):
"""Test that support request RSS only applies a limit and sort entries by date.
When Preferred Listbox List Mode Line Count is set to 3 and
there are 4 events, only the first 3 are displayed.
"""
def afterSetUp(self):
super(TestSupportRequestRSSSMultipleEvents, self).afterSetUp()
preference = self.portal.portal_preferences.getActivePreference()
self._preferred_listbox_list_mode_line_count = \
preference.getPreferredListboxListModeLineCount()
preference.setPreferredListboxListModeLineCount(3)
for i in range(1, 5):
self.portal.event_module.newContent(
portal_type='Web Message',
source_value=self.user,
follow_up_value=self.support_request,
resource_value=self.portal.service_module.erp5_officejs_support_request_ui_test_service_001,
text_content="<p>This is <b>Content %s</b></p>" % i,
start_date=DateTime(2001, 1, i)).stop()
self.tic()
def beforeTearDown(self):
super(TestSupportRequestRSSSMultipleEvents, self).beforeTearDown()
preference = self.portal.portal_preferences.getActivePreference()
preference.setPreferredListboxListModeLineCount(
self._preferred_listbox_list_mode_line_count)
self.tic()
def _checkRSS(self, response):
self.assertEqual(httplib.OK, response.getStatus())
rss = feedparser.parse(response.getBody())
self.assertEqual(rss['feed']['title'], "Support Requests")
self.assertEqual(len(rss.entries), 3)
self.assertEqual(
[item['published'] for item in rss.entries],
[DateTime(2001, 1, 4).rfc822(),
DateTime(2001, 1, 3).rfc822(),
DateTime(2001, 1, 2).rfc822()])
# https://pythonhosted.org/feedparser/bozo.html#advanced-bozo
self.assertFalse(rss.bozo)
class TestSupportRequestRSSSNonVisibleSupportRequest(SupportRequestRSSTestCase, DefaultTestRSSMixin):
"""Edge case test for support request RSS for an event (visible by user) on a support request not visible by user.
"""
def afterSetUp(self):
super(TestSupportRequestRSSSNonVisibleSupportRequest, self).afterSetUp()
support_request = self.support_request.Base_createCloneDocument(batch_mode=True)
support_request.manage_permission('View', ['Manager'], 0)
self.event.setFollowUpValue(support_request)
self.tic()
def _checkRSS(self, response):
self.assertEqual(httplib.OK, response.getStatus())
rss = feedparser.parse(response.getBody())
item, = rss.entries
self.assertEqual(item['author'], self.user.getTitle())
# there's no link to support request, as user cannot see it.
self.assertNotIn(self.support_request.getRelativeUrl(), item['link'])
self.assertEqual(item['published'], DateTime(2001, 1, 1).rfc822())
self.assertEqual(item['summary'], '<p>This is <b>Content</b></p>')
# https://pythonhosted.org/feedparser/bozo.html#advanced-bozo
self.assertFalse(rss.bozo)
class TestSupportRequestRSSSNonVisibleAttachment(SupportRequestRSSTestCase, DefaultTestRSSMixin):
"""Edge case test for support request RSS for an event (visible by user) with attachment not visible by user.
"""
def afterSetUp(self):
super(TestSupportRequestRSSSNonVisibleAttachment, self).afterSetUp()
self.attached_document.manage_permission('View', ['Manager'], 0)
self.attached_document.reindexObject()
self.tic()
def _checkRSS(self, response):
self.assertEqual(httplib.OK, response.getStatus())
rss = feedparser.parse(response.getBody())
item, = rss.entries
# no enclosure
self.assertEqual([], [link for link in item['links'] if link['rel'] == 'enclosure'])
# https://pythonhosted.org/feedparser/bozo.html#advanced-bozo
self.assertFalse(rss.bozo)
class TestIngestPostAsWebMessage(SupportRequestTestCase):
"""Tests ingesting HTML Post into web messages.
"""
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment