docassemble.InterviewStats 0.2__tar.gz → 0.5.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. docassemble_interviewstats-0.5.3/MANIFEST.in +18 -0
  2. {docassemble.InterviewStats-0.2/docassemble.InterviewStats.egg-info → docassemble_interviewstats-0.5.3}/PKG-INFO +15 -10
  3. {docassemble.InterviewStats-0.2 → docassemble_interviewstats-0.5.3}/README.md +2 -0
  4. docassemble_interviewstats-0.5.3/docassemble/InterviewStats/__init__.py +1 -0
  5. docassemble_interviewstats-0.5.3/docassemble/InterviewStats/data/questions/data_report.yml +405 -0
  6. docassemble_interviewstats-0.5.3/docassemble/InterviewStats/data/questions/stats.yml +450 -0
  7. {docassemble.InterviewStats-0.2 → docassemble_interviewstats-0.5.3}/docassemble/InterviewStats/data/questions/subscribe.yml +2 -2
  8. {docassemble.InterviewStats-0.2 → docassemble_interviewstats-0.5.3}/docassemble/InterviewStats/data/questions/test_create_snapshot_data.yml +2 -1
  9. docassemble_interviewstats-0.5.3/docassemble/InterviewStats/data/questions/test_survey_data_generator.yml +110 -0
  10. docassemble_interviewstats-0.5.3/docassemble/InterviewStats/data/static/sortable.js +86 -0
  11. docassemble_interviewstats-0.5.3/docassemble/InterviewStats/data/static/styles.css +18 -0
  12. docassemble_interviewstats-0.5.3/docassemble/InterviewStats/data/static/summary.css +8 -0
  13. docassemble_interviewstats-0.5.3/docassemble/InterviewStats/data_report.py +149 -0
  14. {docassemble.InterviewStats-0.2 → docassemble_interviewstats-0.5.3}/docassemble/InterviewStats/generate_test_data.py +9 -1
  15. {docassemble.InterviewStats-0.2 → docassemble_interviewstats-0.5.3}/docassemble/InterviewStats/snapshot_geography.py +5 -1
  16. docassemble_interviewstats-0.5.3/docassemble/InterviewStats/snapshot_statistics.py +282 -0
  17. {docassemble.InterviewStats-0.2 → docassemble_interviewstats-0.5.3/docassemble.InterviewStats.egg-info}/PKG-INFO +15 -10
  18. {docassemble.InterviewStats-0.2 → docassemble_interviewstats-0.5.3}/docassemble.InterviewStats.egg-info/SOURCES.txt +7 -5
  19. docassemble_interviewstats-0.5.3/docassemble.InterviewStats.egg-info/requires.txt +6 -0
  20. {docassemble.InterviewStats-0.2 → docassemble_interviewstats-0.5.3}/docassemble.InterviewStats.egg-info/top_level.txt +2 -0
  21. docassemble_interviewstats-0.5.3/pyproject.toml +58 -0
  22. docassemble_interviewstats-0.5.3/setup.cfg +4 -0
  23. docassemble.InterviewStats-0.2/MANIFEST.in +0 -1
  24. docassemble.InterviewStats-0.2/docassemble/InterviewStats/__init__.py +0 -1
  25. docassemble.InterviewStats-0.2/docassemble/InterviewStats/data/questions/stats.yml +0 -283
  26. docassemble.InterviewStats-0.2/docassemble/InterviewStats/snapshot_statistics.py +0 -59
  27. docassemble.InterviewStats-0.2/docassemble/__init__.py +0 -5
  28. docassemble.InterviewStats-0.2/docassemble.InterviewStats.egg-info/namespace_packages.txt +0 -1
  29. docassemble.InterviewStats-0.2/docassemble.InterviewStats.egg-info/not-zip-safe +0 -1
  30. docassemble.InterviewStats-0.2/docassemble.InterviewStats.egg-info/requires.txt +0 -6
  31. docassemble.InterviewStats-0.2/setup.cfg +0 -7
  32. docassemble.InterviewStats-0.2/setup.py +0 -60
  33. {docassemble.InterviewStats-0.2 → docassemble_interviewstats-0.5.3}/LICENSE +0 -0
  34. {docassemble.InterviewStats-0.2 → docassemble_interviewstats-0.5.3}/docassemble/InterviewStats/cache_geography.py +0 -0
  35. {docassemble.InterviewStats-0.2 → docassemble_interviewstats-0.5.3}/docassemble/InterviewStats/data/sources/README.md +0 -0
  36. {docassemble.InterviewStats-0.2 → docassemble_interviewstats-0.5.3}/docassemble/InterviewStats/data/static/README.md +0 -0
  37. {docassemble.InterviewStats-0.2 → docassemble_interviewstats-0.5.3}/docassemble/InterviewStats/data/templates/README.md +0 -0
  38. {docassemble.InterviewStats-0.2 → docassemble_interviewstats-0.5.3}/docassemble.InterviewStats.egg-info/dependency_links.txt +0 -0
@@ -0,0 +1,18 @@
1
+ include README.md
2
+ graft docassemble/InterviewStats/data
3
+ recursive-exclude * *.egg-info
4
+ recursive-exclude .git *
5
+ recursive-exclude venv *
6
+ recursive-exclude .github *
7
+ recursive-exclude .pytest_cache *
8
+ recursive-exclude .vscode *
9
+ recursive-exclude build *
10
+ recursive-exclude dist *
11
+ recursive-exclude * __pycache__
12
+ recursive-exclude * *.pyc
13
+ recursive-exclude * *.pyo
14
+ recursive-exclude * *.orig
15
+ recursive-exclude * *~
16
+ recursive-exclude * *.bak
17
+ recursive-exclude * *.swp
18
+
@@ -1,17 +1,24 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: docassemble.InterviewStats
3
- Version: 0.2
4
- Summary: A docassemble extension, to view stats from other interviews
5
- Home-page: https://courtformsonline.org/about/
6
- Author: Bryce Willey, Quinten Steenhuis
7
- Author-email: bwilley@suffolk.com
8
- License: The MIT License (MIT)
9
- Platform: UNKNOWN
3
+ Version: 0.5.3
4
+ Summary: A docassemble interview to view statistics from other saved interview responses.
5
+ Author-email: Suffolk Legal Innovation and Technology Lab <litlab@suffolk.edu>
6
+ License-Expression: MIT
7
+ Project-URL: GitHub, https://github.com/SuffolkLITLab/docassemble-InterviewStats
10
8
  Description-Content-Type: text/markdown
11
9
  License-File: LICENSE
10
+ Requires-Dist: bokeh>=2.4.2
11
+ Requires-Dist: cenpy>=1.0.0.post4
12
+ Requires-Dist: geopandas>=0.9.0
13
+ Requires-Dist: numpy>=1.0.4
14
+ Requires-Dist: pandas>=1.4.2
15
+ Requires-Dist: requests>=2.27.1
16
+ Dynamic: license-file
12
17
 
13
18
  # docassemble.InterviewStats
14
19
 
20
+ [![PyPI version](https://badge.fury.io/py/docassemble.InterviewStats.svg)](https://badge.fury.io/py/docassemble.InterviewStats)
21
+
15
22
  A docassemble interview that lets you view statistics from other saved interview responses.
16
23
 
17
24
  ## Getting Started
@@ -71,5 +78,3 @@ If you have specific feature requests, feel free to open an issue or make a PR!
71
78
  * Quinten Steenhuis
72
79
  * Bryce Willey
73
80
 
74
-
75
-
@@ -1,5 +1,7 @@
1
1
  # docassemble.InterviewStats
2
2
 
3
+ [![PyPI version](https://badge.fury.io/py/docassemble.InterviewStats.svg)](https://badge.fury.io/py/docassemble.InterviewStats)
4
+
3
5
  A docassemble interview that lets you view statistics from other saved interview responses.
4
6
 
5
7
  ## Getting Started
@@ -0,0 +1 @@
1
+ __version__ = '0.5.3'
@@ -0,0 +1,405 @@
1
+ ---
2
+ metadata:
3
+ title: Interview data reporting
4
+ short title: Data reporting
5
+ temporary session: True
6
+ required privileges:
7
+ - admin
8
+ ---
9
+ features:
10
+ question help button: True
11
+ #question back button: True
12
+ css:
13
+ - styles.css
14
+ ---
15
+ default screen parts:
16
+ footer: |
17
+ [:sync-alt: Restart](${ url_of('restart') } )
18
+ ---
19
+ modules:
20
+ - .snapshot_statistics
21
+ - .data_report
22
+ ---
23
+ objects:
24
+ - xlsx_output: DAFile.using(filename="stats.xlsx")
25
+ #--------------------------------------------------------------------------
26
+ # Main logic
27
+ #--------------------------------------------------------------------------
28
+ ---
29
+ id: interview order
30
+ mandatory: True
31
+ code: |
32
+ filename # Choose interview for data report
33
+ simple_summary # Allow date filtering
34
+ excel_formatted_data # Prepare data for graph and excel
35
+ final # Display download and filter options
36
+ #--------------------------------------------------------------------------
37
+ # Question screens
38
+ #--------------------------------------------------------------------------
39
+ ---
40
+ question: |
41
+ Data Reporting
42
+ subquestion: |
43
+ **Prerequisites**:
44
+
45
+ 1. You have a survey-type interview (i.e. interviews that only collect answers but don't have a download screen);
46
+ 1. you are using the `save_input_data()` function in ALToolbox to capture the answers for data reporting purposes. ([See a demo](https://apps-dev.suffolklitlab.org/interview?i=docassemble.ALToolbox%3Adata%2Fquestions%2Fsave_input_data_demo.yml#page1) if you haven't used it before);
47
+ 1. your interview has been run successfully at least once.
48
+
49
+ You can then use this `data reporting` feature to pull your data records and make some quick on-screen data reports. In addition, you can download a complete data dump of the saved data records into an Excel file, and use it to create charts, review free typed answers etc..
50
+
51
+ Select an interview to see data reports:
52
+ fields:
53
+ - Filename: filename
54
+ datatype: combobox
55
+ code: |
56
+ [{interview.get('filename'): interview.get('title')} for interview in interview_menu()] or get_filenames()
57
+ ---
58
+ question: |
59
+ Quick Summary for ${ interview_title }
60
+ subquestion: |
61
+ * Total sessions: ${ summary_stats[0] }
62
+ * First session date: ${ format_date(summary_stats[1], format='MM/dd/yyyy') }
63
+ * Latest session date: ${ format_date(summary_stats[2], format='MM/dd/yyyy') }
64
+
65
+ % if summary_stats[0] > 10000:
66
+ You have a lot of sessions; some operations might take a long time.
67
+ % endif
68
+ ---
69
+ Do you want to filter your data records by date?
70
+ fields:
71
+ - no label: filter_date
72
+ datatype: yesnoradio
73
+ default: False
74
+ required: False
75
+ - Find submissions that are: date_op
76
+ datatype: dropdown
77
+ show if: filter_date
78
+ code: |
79
+ [{'gt': 'after'},
80
+ {'lt': 'before'},
81
+ {'eq': 'on'},
82
+ {'between': 'between'}]
83
+ - this date: begin_date
84
+ datatype: date
85
+ show if: filter_date
86
+ - and this date: end_date
87
+ datatype: date
88
+ js show if: |
89
+ val('date_op') == 'between'
90
+ continue button field: simple_summary
91
+ ---
92
+ id: final screen
93
+ event: final
94
+ question: |
95
+ ${interview_title} - Data Reports
96
+ subquestion: |
97
+ Total submissions: ${ len(stats) }
98
+
99
+ % if filter_date:
100
+ Date filter: Records dated ${phrase(date_op)} ${begin_date}
101
+ % if phrase(date_op) == 'between':
102
+ and ${end_date}
103
+ % endif
104
+ <br>Filtered submissions: ${ record_count }
105
+ % endif
106
+
107
+ % if have_multi_vars:
108
+ #### Multi-valued questions
109
+ The following fields are from multi-valued questions. Select one to see its table:<br>
110
+ % for col in col_headings_multi:
111
+ [${col}](${ url_action('show_filtered_data_multi', filter_by = col) })
112
+ % endfor
113
+ % endif
114
+
115
+ % if have_single_vars:
116
+ #### Single-valued questions (excluding text/area/numberic fields)
117
+ The following fields are from single-valued questions, except those whose answer lack statistic value, such as user-typed answer or numeric input.
118
+
119
+ Select a field to see its table:<br>
120
+ % for col in col_headings_single:
121
+ [${col}](${ url_action('show_filtered_data_single', filter_by = col) })
122
+ % endfor
123
+ % endif
124
+
125
+ #### All questions (excluding text/area/number fields)
126
+ [Click here](${ url_action('display_filtered_data_all') }) to see all the question tables on one screen.
127
+
128
+ #### Download an excel file:
129
+ To extract complete data including text/area/numeric fields, make an excel file:<br>
130
+ [:file-excel: Make Excel Download](${ url_ask('generate_excel') })
131
+ ---
132
+ id: single-valued filtered screen
133
+ question: |
134
+ Data Report - Single value questions
135
+ subquestion: |
136
+ **Number of submissions**: ${ record_count }<br>
137
+ **Variable Name**: ${ my_col_single }<br>
138
+ **Table Content**: Answer count / % of total submission<br>
139
+ :bell: Total percentage should be about 100% (±0.5% due to rounding).
140
+
141
+ ${ df_single[my_col_single].rename_axis("Answer").to_frame().query('index=='+my_index_single).to_html(classes=('table','table-striped')) }
142
+ continue button field: display_filtered_data_single
143
+ continue button label: Back
144
+ ---
145
+ id: multi-valued filtered screen
146
+ question: |
147
+ Data Report - Multi-choices questions
148
+ subquestion: |
149
+ **Number of submissions**: ${ record_count }<br>
150
+ **Variable Name**: ${ my_col_multi }<br>
151
+ **Table Content**: Answer count / % of total submission<br>
152
+ :bell: Total percentage can be over or under 100%.
153
+
154
+ ${ df_multi[my_col_multi].rename_axis("Answer").to_frame().query('index=='+my_index_multi).to_html(classes=('table','table-striped')) }
155
+ continue button field: display_filtered_data_multi
156
+ continue button label: Back
157
+ ---
158
+ id: all_variables screen
159
+ question: |
160
+ Data Report - All questions
161
+ subquestion: |
162
+ Number of submissions: ${ record_count }<br>
163
+ **Table Content**: Answer count / % of total submission<br>
164
+ :bell: For multi-valued questions, the total percentage can be over or under 100%; for single-valued questions, the total percentage should be about 100% (±0.5% due to rounding).
165
+
166
+ #### Multi-value questions
167
+ % for fld in df_multi:
168
+ ${ df_multi[fld].rename_axis("Answer").to_frame().query('index=='+str(answer_list_multi[col_headings_multi.index(fld)])).to_html(classes=('table','table-striped')) }
169
+ % endfor
170
+
171
+ #### Single-value questions
172
+ % for fld in df_single:
173
+ ${ df_single[fld].rename_axis("Answer").to_frame().query('index=='+str((answer_list_single[col_headings_single.index(fld)]))).to_html(classes=('table','table-striped')) }
174
+ % endfor
175
+ continue button field: display_filtered_data_all
176
+ continue button label: Back
177
+ #--------------------------------------------------------------------------
178
+ # Data extract code blocks and screens
179
+ #--------------------------------------------------------------------------
180
+ ---
181
+ code: |
182
+ summary_stats = get_summary_stats(filename)
183
+ ---
184
+ # Extract full data from database into stats
185
+ code: |
186
+ # Get stats using filename
187
+ if filename is not None:
188
+ if 'data/questions' in filename or 'playground' in filename:
189
+ stats = get_stats(filename)
190
+ else:
191
+ stats = get_stats(filename.replace(':',':data/questions/'))
192
+ else:
193
+ no_interview_stats
194
+
195
+ # Check if stats has data
196
+ if not stats:
197
+ no_interview_stats
198
+ else:
199
+ # Check if stats is data-reporting ready - it must have a "title" column
200
+ if 'title' not in stats[0].keys():
201
+ no_reportable_data
202
+ ---
203
+ event: no_interview_stats
204
+ question: Interview has no data!
205
+ subquestion: |
206
+ The interview you selected is <br>
207
+ ${interview_title}.
208
+
209
+ This interview doesn't have any data. Try another one.
210
+ buttons:
211
+ - Restart: restart
212
+ ---
213
+ event: no_reportable_data
214
+ question: Interview has no reportable data, you should use Stats
215
+ subquestion: |
216
+ The interview you selected is <br>
217
+ ${interview_title}.
218
+
219
+ This interview contains data not suitable for Data Reporting. You should use Stats.
220
+ buttons:
221
+ - Restart: restart
222
+ ---
223
+ code: |
224
+ interview_title = next(iter([interview.get('title') for interview in interview_menu() if interview.get('filename') == filename]), '(interview)')
225
+ if stats and 'title' in stats[0].keys():
226
+ interview_title = stats[0]['title']
227
+ ---
228
+ # Filter records in stats according to the filter
229
+ code: |
230
+ filtered_stats = list()
231
+
232
+ # Build filtered_stats if filter_date is selected
233
+ if filter_date:
234
+ filter_begin = format_date(begin_date, format='MM/dd/yyyy')
235
+ if date_op == 'between':
236
+ filter_end = format_date(end_date, format='MM/dd/yyyy')
237
+ for record in stats:
238
+ record_date = format_date(record['modtime'], format='MM/dd/yyyy')
239
+ if (date_op == 'eq' and record_date == filter_begin) or (date_op == 'gt' and record_date > filter_begin) or (date_op == 'lt' and record_date < filter_begin) or (date_op == 'between' and record_date >= filter_begin and record_date < filter_end):
240
+ filtered_stats.append(record)
241
+
242
+ # In case a wrong date/date_op pair is selected
243
+ if filter_date and not filtered_stats:
244
+ no_filtered_stats
245
+ ---
246
+ event: no_filtered_stats
247
+ question: No records in this period
248
+ subquestion: |
249
+ % if date_op == 'between':
250
+ You selected records "${phrase(date_op)} ${begin_date} and ${end_date}".
251
+ % else:
252
+ You selected records "${phrase(date_op)} ${begin_date}".
253
+ % endif
254
+ There aren't any records fit your selection.
255
+
256
+ Try another date.
257
+ back button: simple_summary
258
+ ---
259
+ need: filtered_stats
260
+ code: |
261
+ # Process the filtered data records
262
+ columns = get_columns(stats)
263
+ if filtered_stats:
264
+ table_data = processing_data(raw_data = filtered_stats, headings = columns)
265
+ record_count = len(filtered_stats)
266
+ else:
267
+ record_count = len(stats)
268
+ table_data = processing_data(raw_data = stats, headings = columns)
269
+
270
+ # Extract data for multi-valued questions
271
+ have_multi_vars = False
272
+ if table_data[0]:
273
+ have_multi_vars = True
274
+ table_data_multi = table_data[0]
275
+ col_headings_multi = list(table_data[0].keys())
276
+ answer_list_multi = table_data[1]
277
+
278
+ # Extract data for single-valued questions
279
+ have_single_vars = False
280
+ if table_data[2]:
281
+ have_single_vars = True
282
+ table_data_single = table_data[2]
283
+ col_headings_single = list(table_data[2].keys())
284
+ # Extract possible answers for single-valued questions
285
+ answer_list_single = table_data[3]
286
+ #--------------------------------------------------------------------------
287
+ # Pandas related code blocks
288
+ #--------------------------------------------------------------------------
289
+ ---
290
+ code: |
291
+ import pandas as pd
292
+ ---
293
+ code: |
294
+ excel_formatted_data = pd.DataFrame()
295
+ df_single = pd.DataFrame()
296
+ df_multi = pd.DataFrame()
297
+ stats_size = 0
298
+ if stats is not None:
299
+ excel_formatted_data = pd.DataFrame(stats)
300
+ stats_size = excel_formatted_data.shape[0]
301
+ if have_single_vars:
302
+ df_single = pd.DataFrame(table_data_single)
303
+ if have_multi_vars:
304
+ df_multi = pd.DataFrame(table_data_multi)
305
+ ---
306
+ event: show_filtered_data_single
307
+ code: |
308
+ # Receive user chosen field
309
+ my_col_single = action_argument('filter_by')
310
+
311
+ # Find the index of my_col_single, and use it to set my_index_single
312
+ my_row_single = col_headings_single.index(my_col_single)
313
+ my_index_single = str(answer_list_single[my_row_single])
314
+
315
+ # Display single-valued fields table screen
316
+ force_ask('display_filtered_data_single')
317
+ ---
318
+ event: show_filtered_data_multi
319
+ code: |
320
+ # Receive user chosen field
321
+ my_col_multi = action_argument('filter_by')
322
+
323
+ # Find the index of my_col_multi, and use it to set my_index_multi
324
+ my_row_multi = col_headings_multi.index(my_col_multi)
325
+ my_index_multi = str(answer_list_multi[my_row_multi])
326
+
327
+ # Display multi-valued fields table screen
328
+ force_ask('display_filtered_data_multi')
329
+ ---
330
+ code: |
331
+ row_single = col_headings_single.index(fld)
332
+ index_single = str(answer_list_single[row_single])
333
+
334
+ row_multi = col_headings_multi.index(fld)
335
+ index_multi = str(answer_list_multi[row_multi])
336
+ #--------------------------------------------------------------------------
337
+ # Excel related code blocks and screens
338
+ #--------------------------------------------------------------------------
339
+ ---
340
+ event: generate_excel
341
+ code: |
342
+ if stats_size > 10000:
343
+ if make_excel_task.ready():
344
+ if make_excel_task.failed():
345
+ show_failed_screen
346
+ show_excel_download
347
+ else:
348
+ waiting_screen
349
+ else:
350
+ proper_name = space_to_underscore(filename) + "_stats"
351
+ xlsx_output.initialize(filename=proper_name + ".xlsx")
352
+ writer = pd.ExcelWriter(xlsx_output.path(),
353
+ engine='xlsxwriter',
354
+ engine_kwargs={'options': {'remove_timezone': True}})
355
+ excel_formatted_data.to_excel(writer, index=False)
356
+ writer.close()
357
+ xlsx_output.commit()
358
+ del writer
359
+ show_excel_download
360
+ ---
361
+ event: show_excel_download
362
+ question: |
363
+ Excel Download
364
+ subquestion: |
365
+ [:file-excel: Download](${ xlsx_output.url_for() })
366
+ ---
367
+ code: |
368
+ make_excel_task = background_action('excel_task', data=excel_formatted_data)
369
+ ---
370
+ event: excel_task
371
+ code: |
372
+ bg_data = action_argument('data')
373
+ proper_name = space_to_underscore(filename) + "_stats"
374
+ xlsx_output.initialize(filename=proper_name + ".xlsx")
375
+ writer = pd.ExcelWriter(xlsx_output.path(),
376
+ engine='xlsxwriter',
377
+ engine_kwargs={'options': {'remove_timezone': True}})
378
+ bg_data.to_excel(writer, index=False)
379
+ writer.close()
380
+ xlsx_output.commit()
381
+ del writer
382
+ background_response_action('excel_resp', the_excel=xlsx_output)
383
+ ---
384
+ event: waiting_screen
385
+ id: waiting screen
386
+ question: |
387
+ We're generating your excel sheet.
388
+ subquestion: |
389
+ This may take a few minutes, so leave this tab open. Thanks for waiting.
390
+ reload: True
391
+ ---
392
+ event: show_failed_screen
393
+ question: |
394
+ Sorry, something went wrong
395
+ subquestion: |
396
+ We had some trouble trying to assemble your excel sheet.
397
+
398
+ ${ make_excel_task.result() }
399
+ buttons:
400
+ - Restart: restart
401
+ ---
402
+ event: excel_resp
403
+ code: |
404
+ xlsx_output = action_argument('the_excel')
405
+ background_response()