Merge lp:~graeme-acm/sahana-eden/RMS into lp:sahana-eden
- RMS
- Merge into trunk
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 |
Related bugs: |
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.
Commit message
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"}} |