Merge lp:~graeme-acm/sahana-eden/RMS into lp:sahana-eden

Proposed by Graeme Foster
Status: Merged
Merged at revision: 2776
Proposed branch: lp:~graeme-acm/sahana-eden/RMS
Merge into: lp:sahana-eden
Diff against target: 536 lines (+352/-35)
4 files modified
controllers/survey.py (+162/-0)
models/survey.py (+112/-28)
modules/s3/s3survey.py (+68/-7)
views/survey/series_analysis.html (+10/-0)
To merge this branch: bzr merge lp:~graeme-acm/sahana-eden/RMS
Reviewer Review Type Date Requested Status
Fran Boon Pending
Review via email: mp+76952@code.launchpad.net

This proposal supersedes a proposal from 2011-09-24.

Description of the change

Completed the basic code for the series analysis of two questions
Added basic excel generation for a set of questions

Tidied up the try excepts around the matplotlib import.

To post a comment you must log in.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'controllers/survey.py'
2--- controllers/survey.py 2011-09-23 08:45:47 +0000
3+++ controllers/survey.py 2011-09-26 09:46:25 +0000
4@@ -23,6 +23,9 @@
5 except:
6 from StringIO import StringIO
7
8+import base64
9+
10+from gluon.contenttype import contenttype
11 from s3survey import survey_question_type, \
12 survey_analysis_type
13
14@@ -252,6 +255,56 @@
15 rheader=response.s3.survey_series_rheader)
16 return output
17
18+def series_export():
19+ prefix = "survey"
20+ resourcename = "series"
21+ tablename = "%s_%s" % (prefix, resourcename)
22+ s3mgr.load(tablename)
23+ crud_strings = response.s3.crud_strings[tablename]
24+
25+ if len(request.args) == 1:
26+ series_id = request.args[0]
27+ questions = response.s3.survey_getAllQuestionsForSeries(series_id)
28+ try:
29+ import xlwt
30+ except ImportError:
31+ output = s3_rest_controller(prefix,
32+ resourcename,
33+ rheader=response.s3.survey_series_rheader)
34+ return output
35+
36+ COL_WIDTH_MULTIPLIER = 864
37+ book = xlwt.Workbook(encoding="utf-8")
38+ sheet1 = book.add_sheet(T("Assignment"))
39+ output = StringIO()
40+ row = 0
41+ section = ""
42+ styleHeader = xlwt.XFStyle()
43+ styleHeader.font.bold = True
44+ styleHeader.alignment.horz = styleHeader.alignment.HORZ_CENTER
45+ for question in questions:
46+ if question["section"] != section:
47+ section = question["section"]
48+ sheet1.write_merge(row, row, 0, 1, section, style=styleHeader)
49+ row += 1
50+ sheet1.write(row, 0, question["code"])
51+ sheet1.write(row, 1, question["name"])
52+ width=len(unicode(question["name"]))*COL_WIDTH_MULTIPLIER
53+ sheet1.col(1).width = width
54+ row += 1
55+ book.save(output)
56+ output.seek(0)
57+ response.headers["Content-Type"] = contenttype(".xls")
58+ seriesName = response.s3.survey_getSeriesFromID(series_id)
59+ filename = "%s.xls" % seriesName
60+ response.headers["Content-disposition"] = "attachment; filename=\"%s\"" % filename
61+ return output.read()
62+ else:
63+ output = s3_rest_controller(prefix,
64+ resourcename,
65+ rheader=response.s3.survey_series_rheader)
66+ return output
67+
68 def series_summary():
69 # Load Model
70 if request.env.request_method == "POST":
71@@ -292,6 +345,115 @@
72 response.s3.actions = None
73 return output
74
75+def series_analysis():
76+ # Load Model
77+ prefix = "survey"
78+ resourcename = "series"
79+ tablename = "%s_%s" % (prefix, resourcename)
80+ s3mgr.load(tablename)
81+ crud_strings = response.s3.crud_strings[tablename]
82+
83+ def postp(r, output):
84+ if r.interactive:
85+ # Draw the chart
86+ if "vars" in current.request and len(current.request.vars) > 0:
87+ dummy, series_id = current.request.vars.viewing.split(".")
88+ else:
89+ series_id = r.id
90+ numericQuestion = None
91+ if "post_vars" in current.request and len(current.request.post_vars) > 0:
92+ labelQuestion = None
93+ if "labelQuestion" in current.request.post_vars:
94+ labelQuestion = current.request.post_vars.labelQuestion
95+ numericQuestion = None
96+ if "numericQuestion" in current.request.post_vars:
97+ numericQuestion = current.request.post_vars.numericQuestion
98+ if (numericQuestion != None) and (labelQuestion != None):
99+ getAnswers = response.s3.survey_getAllAnswersForQuestionInSeries
100+ qstn = response.s3.survey_getQuestionFromCode(numericQuestion, series_id)
101+ qstn_id = qstn["qstn_id"]
102+ qstn_type = qstn["type"]
103+ answers = getAnswers(qstn_id, series_id)
104+ gqstn = response.s3.survey_getQuestionFromName(labelQuestion, series_id)
105+ gqstn_id = gqstn["qstn_id"]
106+ ganswers = getAnswers(gqstn_id, series_id)
107+ analysisTool = survey_analysis_type[qstn_type](qstn_id, answers)
108+ grouped = analysisTool.groupData(ganswers)
109+ aggregate = "Sum"
110+ filtered = analysisTool.filter(aggregate, grouped)
111+ (label, data) = analysisTool.splitGroupedData(filtered)
112+ chart = Storage()
113+ chart["body"] = StringIO()
114+ chart["headers"] = Storage()
115+ chart["headers"]['Content-Type']="image/png"
116+ analysisTool.drawChart(chart, data, label)
117+ base64Img = base64.b64encode(chart.body.getvalue())
118+ image = IMG(_src="data:image/png;base64,%s" % base64Img)
119+ output["chart"] = image
120+
121+ # Build the form
122+ if series_id == None:
123+ return output
124+ allQuestions = response.s3.survey_get_series_questions(series_id)
125+ labelTypeList = ("String", "Option", "YesNo", "YesNoDontKnow")
126+ labelQuestions = response.s3.survey_get_series_questions_of_type (allQuestions, labelTypeList)
127+ lblQstns = []
128+ for question in labelQuestions:
129+ lblQstns.append(question["name"])
130+ numericTypeList = ("Numeric")
131+ numericQuestions = response.s3.survey_get_series_questions_of_type (allQuestions, numericTypeList)
132+
133+ form = FORM()
134+ table = TABLE()
135+
136+ labelQstn = SELECT(lblQstns, _name="labelQuestion")
137+ table.append(TR(TH(T("Label Question:")), _class="survey_question"))
138+ table.append(labelQstn)
139+
140+ table.append(TR(TH(T("Numeric Question:")), _class="survey_question"))
141+ for qstn in numericQuestions:
142+ innerTable = TABLE()
143+ tr = TR()
144+ if numericQuestion != None and numericQuestion == qstn["code"]:
145+ tr.append(INPUT(_type='radio',
146+ _name='numericQuestion',
147+ _value=qstn["code"],
148+ value=qstn["code"],
149+ )
150+ )
151+ else:
152+ tr.append(INPUT(_type='radio',
153+ _name='numericQuestion',
154+ _value=qstn["code"],
155+ )
156+ )
157+ tr.append(LABEL(qstn["name"]))
158+ innerTable.append(tr)
159+ table.append(innerTable)
160+ form.append(table)
161+
162+ button = INPUT(_type="submit", _name="Chart", _value="Draw Chart")
163+ form.append(button)
164+
165+ output["form"] = form
166+ return output
167+
168+ # remove CRUD generated buttons in the tabs
169+ s3mgr.configure(tablename,
170+ listadd=False,
171+ deletable=False,
172+ )
173+
174+ response.s3.postp = postp
175+ output = s3_rest_controller(prefix,
176+ resourcename,
177+ method = "create",
178+ rheader=response.s3.survey_series_rheader
179+ )
180+ response.s3.has_required = False
181+ response.view = "survey/series_analysis.html"
182+ return output
183+
184 def completed_chart():
185 """ RESTful CRUD controller """
186 # Load Model
187
188=== modified file 'models/survey.py'
189--- models/survey.py 2011-09-23 07:33:12 +0000
190+++ models/survey.py 2011-09-26 09:46:25 +0000
191@@ -529,6 +529,70 @@
192 questions = getAllQuestionsForSeries(series_id)
193 return (questions, series_id)
194
195+ def survey_getQuestionFromCode(code, series_id):
196+ """
197+ function to return the question for the given series
198+ with the code that matches the one passed in
199+ """
200+ sertable = db.survey_series
201+ q_ltable = db.survey_question_list
202+ qsntable = db.survey_question
203+ query = db((sertable.id == series_id) & \
204+ (q_ltable.template_id == sertable.template_id) & \
205+ (q_ltable.question_id == qsntable.id) & \
206+ (qsntable.code == code)
207+ )
208+ record = query.select(qsntable.id,
209+ qsntable.code,
210+ qsntable.name,
211+ qsntable.type,
212+ q_ltable.posn,
213+ limitby=(0, 1)).first()
214+ question = {}
215+ question["qstn_id"] = record.survey_question.id
216+ question["code"] = record.survey_question.code
217+ question["name"] = record.survey_question.name
218+ question["type"] = record.survey_question.type
219+ question["posn"] = record.survey_question_list.posn
220+ return question
221+
222+ def survey_getSeriesFromID(series_id):
223+ """
224+ function to return the series from a series id
225+ """
226+ sertable = db.survey_series
227+ query = db((sertable.id == series_id))
228+ record = query.select(sertable.name,
229+ limitby=(0, 1)).first()
230+ return record.name
231+
232+ def survey_getQuestionFromName(name, series_id):
233+ """
234+ function to return the question for the given series
235+ with the name that matches the one passed in
236+ """
237+ sertable = db.survey_series
238+ q_ltable = db.survey_question_list
239+ qsntable = db.survey_question
240+ query = db((sertable.id == series_id) & \
241+ (q_ltable.template_id == sertable.template_id) & \
242+ (q_ltable.question_id == qsntable.id) & \
243+ (qsntable.name == name)
244+ )
245+ record = query.select(qsntable.id,
246+ qsntable.code,
247+ qsntable.name,
248+ qsntable.type,
249+ q_ltable.posn,
250+ limitby=(0, 1)).first()
251+ question = {}
252+ question["qstn_id"] = record.survey_question.id
253+ question["code"] = record.survey_question.code
254+ question["name"] = record.survey_question.name
255+ question["type"] = record.survey_question.type
256+ question["posn"] = record.survey_question_list.posn
257+ return question
258+
259 def survey_save_answers_for_complete(complete_id, vars):
260 # Get all the questions
261 (questions, series_id) = getAllQuestionsForComplete(complete_id)
262@@ -570,6 +634,24 @@
263 questions = getAllQuestionsForSeries(series_id)
264 return build_questions_form(questions, complete_id)
265
266+ def survey_get_series_questions(series_id):
267+ return getAllQuestionsForSeries(series_id)
268+
269+ def survey_get_series_questions_of_type(questionList, type):
270+ if isinstance(type, (list, tuple)):
271+ types = type
272+ else:
273+ types = (type)
274+ questions = []
275+ for question in questionList:
276+ if question["type"] in types:
277+ questions.append(question)
278+ if question["type"] == "Link":
279+ widgetObj = survey_question_type["Link"](question_id = question["qstn_id"])
280+ if widgetObj.getParentType() in types:
281+ questions.append(question)
282+ return questions
283+
284 def survey_build_template_questions(template_id):
285 # Get all the questions
286 questions = getAllQuestionsForTemplate(template_id)
287@@ -672,10 +754,12 @@
288 (T("Completed Assessments"), "complete"),
289 (T("Series Details"), "read"),
290 (T("Series Summary"),"series_summary/"),
291+ (T("Series Analysis"),"series_analysis/"),
292 ]
293 else:
294 tabs = [(T("Series Details"), "read"),
295 (T("Series Summary"),"series_summary/"),
296+ (T("Series Analysis"),"series_analysis/"),
297 ]
298
299 rheader_tabs = s3_rheader_tabs(r, tabs)
300@@ -832,35 +916,29 @@
301 def survey_serieslist_dataTable_post(r):
302 s3_action_buttons(r)
303 if auth.s3_has_permission("create", tablename):
304- response.s3.actions = [
305- dict(label=str(T("Open")),
306- _class="action-btn",
307- url=URL(c=module,
308- f="newAssessment",
309- vars={"viewing":"survey_series.[id]"})
310- ),
311- dict(label=str(T("Analysis")),
312- _class="action-btn",
313- url=URL(c=module,
314- f="series_summary",
315- args=["[id]"])
316- ),
317- ]
318+ openFunction = "newAssessment"
319 else:
320- response.s3.actions = [
321- dict(label=str(T("Open")),
322- _class="action-btn",
323- url=URL(c=module,
324- f="series",
325- vars={"viewing":"survey_series.[id]"})
326- ),
327- dict(label=str(T("Analysis")),
328- _class="action-btn",
329- url=URL(c=module,
330- f="series_summary",
331- vars={"viewing":"survey_series.[id]"})
332- ),
333- ]
334+ openFunction = "series"
335+ response.s3.actions = [
336+ dict(label=str(T("Open")),
337+ _class="action-btn",
338+ url=URL(c=module,
339+ f=openFunction,
340+ vars={"viewing":"survey_series.[id]"})
341+ ),
342+ dict(label=str(T("Analysis")),
343+ _class="action-btn",
344+ url=URL(c=module,
345+ f="series_summary",
346+ args=["[id]"])
347+ ),
348+ dict(label=str(T("Excel")),
349+ _class="action-btn",
350+ url=URL(c=module,
351+ f="series_export",
352+ args=["[id]"])
353+ ),
354+ ]
355
356 def survey_answerlist_dataTable_post(r):
357 s3_action_buttons(r)
358@@ -1167,8 +1245,14 @@
359 survey_build_template_questions = survey_build_template_questions,
360 survey_build_complete_questions = survey_build_complete_questions,
361 survey_build_completed_list = survey_build_completed_list,
362+ survey_getSeriesFromID = survey_getSeriesFromID,
363 survey_save_answers_for_complete = survey_save_answers_for_complete,
364 survey_getAllAnswersForQuestionInSeries = getAllAnswersForQuestionInSeries,
365+ survey_get_series_questions = survey_get_series_questions,
366+ survey_get_series_questions_of_type = survey_get_series_questions_of_type,
367+ survey_getAllQuestionsForSeries = getAllQuestionsForSeries,
368+ survey_getQuestionFromCode = survey_getQuestionFromCode,
369+ survey_getQuestionFromName = survey_getQuestionFromName,
370 )
371
372 return return_dict
373
374=== modified file 'modules/s3/s3survey.py'
375--- modules/s3/s3survey.py 2011-09-24 08:31:16 +0000
376+++ modules/s3/s3survey.py 2011-09-26 09:46:25 +0000
377@@ -41,6 +41,9 @@
378 _debug = lambda m: None
379
380 try:
381+ # NOTE matplotlib requires a directory to write temp files to.
382+ # If it is unable to create or write to this directory
383+ # then it will raise a RunTimeError.
384 import matplotlib
385 # need to set up the back end before importing pyplot
386 try:
387@@ -50,7 +53,7 @@
388 matplotlib.use('Agg')
389 import matplotlib.pyplot as plt
390 from pylab import savefig
391-except:
392+except ImportError:
393 print >> sys.stderr, "S3 Debug: S3Survey: matplotlib Library not installed"
394
395 try:
396@@ -68,10 +71,14 @@
397 output = StringIO()
398 output.write(msg)
399 response.body = output
400+ return
401 # draw the histogram
402 ax.hist(data, bins=bins, range=(min,max))
403 # Set up the response headers and body
404- response.headers['Content-Type']="image/png"
405+ try:
406+ response.headers['Content-Type']="image/png"
407+ except:
408+ pass
409 savefig(response.body)
410
411 def survey_pie(response, data, label):
412@@ -84,6 +91,7 @@
413 output = StringIO()
414 output.write(msg)
415 response.body = output
416+ return
417 # draw a pie chart
418 ax.pie(data, labels=label)
419 # Set up the response headers and body
420@@ -723,6 +731,10 @@
421 except:
422 return DIV("Link", _class="surveyLinkWidget")
423
424+ def getParentType(self):
425+ self._store_metadata()
426+ return self.get("Type")
427+
428 def getParentQstnID(self):
429 db = current.db
430 parent = self.get("Parent")
431@@ -819,7 +831,7 @@
432 link = A(current.T("Chart"), _href=src, _target="blank", _class="action-btn")
433 return DIV(link, _class="surveyChart%sWidget" % self.type)
434
435- def drawChart(self, response):
436+ def drawChart(self, response, data = None, label = None):
437 msg = "Programming Error: No chart for %sWidget" % self.type
438 output = StringIO()
439 output.write(msg)
440@@ -838,7 +850,35 @@
441 for (key, value) in self.result:
442 table.append(TR(TD(B(key)),TD(value)))
443 return table
444-
445+
446+ def groupData(self, groupAnswer):
447+ grouped = {}
448+ answers = {}
449+ for answer in self.answerList:
450+ answers[answer["complete_id"]] = answer["value"]
451+ for ganswer in groupAnswer:
452+ gcode = ganswer["complete_id"]
453+ greply = ganswer["value"]
454+ if gcode in answers:
455+ value = answers[gcode]
456+ else:
457+ continue
458+ if greply in grouped:
459+ grouped[greply].append(value)
460+ else:
461+ grouped[greply] = [value]
462+ return grouped
463+
464+ def filter(self, filterType, groupedData):
465+ return groupedData
466+
467+ def splitGroupedData(self, groupedData):
468+ keys = []
469+ values = []
470+ for (key, value) in groupedData.items():
471+ keys.append(key)
472+ values.append(value)
473+ return (keys, values)
474
475 class S3StringAnalysis(S3AbstractAnalysis):
476
477@@ -936,8 +976,23 @@
478 return None
479 return S3AbstractAnalysis.chartButton(self)
480
481- def drawChart(self, response):
482- survey_hist(response, self.valueList, 10, 0, self.max)
483+ def drawChart(self, response, data = None, label = None):
484+ if data == None:
485+ survey_hist(response, self.valueList, 10, 0, self.max)
486+ else:
487+ survey_pie(response, data, label)
488+
489+ def filter(self, filterType, groupedData):
490+ filteredData = {}
491+ if filterType == "Sum":
492+ sum = 0
493+ for (key, valueList) in groupedData.items():
494+ for value in valueList:
495+ sum += self.castRawAnswer(value)
496+ filteredData[key] = sum
497+ return filteredData
498+ return groupedData
499+
500
501 class S3OptionAnalysis(S3AbstractAnalysis):
502
503@@ -970,7 +1025,7 @@
504 for (key, value) in self.list.items():
505 self.listp[key] = "%s%%" %((100 * value) / self.cnt)
506
507- def drawChart(self, response):
508+ def drawChart(self, response, data = None, label = None):
509 data = []
510 label = []
511 for (key, value) in self.list.items():
512@@ -1149,3 +1204,9 @@
513
514 def chartButton(self):
515 return self.widget.chartButton()
516+
517+ def filter(self, filterType, groupedData):
518+ return self.widget.filter(filterType, groupedData)
519+
520+ def drawChart(self, response, data = None, label = None):
521+ return self.widget.drawChart(response, data, label)
522
523=== added file 'views/survey/series_analysis.html'
524--- views/survey/series_analysis.html 1970-01-01 00:00:00 +0000
525+++ views/survey/series_analysis.html 2011-09-26 09:46:25 +0000
526@@ -0,0 +1,10 @@
527+{{extend "layout.html"}}
528+{{try:}}
529+ {{=chart}}
530+{{except:}}
531+{{pass}}
532+{{try:}}
533+ {{=sform}}
534+{{except:}}
535+{{pass}}
536+{{include "_create.html"}}