docassemble.InterviewStats 0.5.0__tar.gz → 0.6.0__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 (41) hide show
  1. docassemble_interviewstats-0.6.0/MANIFEST.in +18 -0
  2. {docassemble.InterviewStats-0.5.0/docassemble.InterviewStats.egg-info → docassemble_interviewstats-0.6.0}/PKG-INFO +13 -18
  3. {docassemble.InterviewStats-0.5.0 → docassemble_interviewstats-0.6.0}/README.md +3 -11
  4. docassemble_interviewstats-0.6.0/docassemble/InterviewStats/__init__.py +1 -0
  5. {docassemble.InterviewStats-0.5.0 → docassemble_interviewstats-0.6.0}/docassemble/InterviewStats/data/questions/data_report.yml +17 -10
  6. docassemble_interviewstats-0.6.0/docassemble/InterviewStats/data/questions/stats.yml +373 -0
  7. {docassemble.InterviewStats-0.5.0 → docassemble_interviewstats-0.6.0}/docassemble/InterviewStats/data/questions/subscribe.yml +3 -2
  8. {docassemble.InterviewStats-0.5.0 → docassemble_interviewstats-0.6.0}/docassemble/InterviewStats/data/questions/test_create_snapshot_data.yml +11 -3
  9. {docassemble.InterviewStats-0.5.0 → docassemble_interviewstats-0.6.0}/docassemble/InterviewStats/data/questions/test_survey_data_generator.yml +4 -0
  10. docassemble_interviewstats-0.6.0/docassemble/InterviewStats/data/static/sortable.js +56 -0
  11. docassemble_interviewstats-0.6.0/docassemble/InterviewStats/data/static/summary.css +8 -0
  12. docassemble_interviewstats-0.6.0/docassemble/InterviewStats/data_report.py +179 -0
  13. docassemble_interviewstats-0.6.0/docassemble/InterviewStats/generate_test_data.py +52 -0
  14. docassemble_interviewstats-0.6.0/docassemble/InterviewStats/snapshot_statistics.py +319 -0
  15. docassemble_interviewstats-0.6.0/docassemble/InterviewStats/test_generate_data.py +9 -0
  16. {docassemble.InterviewStats-0.5.0 → docassemble_interviewstats-0.6.0/docassemble.InterviewStats.egg-info}/PKG-INFO +13 -18
  17. {docassemble.InterviewStats-0.5.0 → docassemble_interviewstats-0.6.0}/docassemble.InterviewStats.egg-info/SOURCES.txt +4 -7
  18. docassemble_interviewstats-0.6.0/docassemble.InterviewStats.egg-info/requires.txt +3 -0
  19. {docassemble.InterviewStats-0.5.0 → docassemble_interviewstats-0.6.0}/docassemble.InterviewStats.egg-info/top_level.txt +1 -0
  20. docassemble_interviewstats-0.6.0/pyproject.toml +45 -0
  21. docassemble_interviewstats-0.6.0/setup.cfg +4 -0
  22. docassemble.InterviewStats-0.5.0/MANIFEST.in +0 -1
  23. docassemble.InterviewStats-0.5.0/docassemble/InterviewStats/__init__.py +0 -1
  24. docassemble.InterviewStats-0.5.0/docassemble/InterviewStats/cache_geography.py +0 -89
  25. docassemble.InterviewStats-0.5.0/docassemble/InterviewStats/data/questions/stats.yml +0 -318
  26. docassemble.InterviewStats-0.5.0/docassemble/InterviewStats/data_report.py +0 -140
  27. docassemble.InterviewStats-0.5.0/docassemble/InterviewStats/generate_test_data.py +0 -26
  28. docassemble.InterviewStats-0.5.0/docassemble/InterviewStats/snapshot_geography.py +0 -239
  29. docassemble.InterviewStats-0.5.0/docassemble/InterviewStats/snapshot_statistics.py +0 -87
  30. docassemble.InterviewStats-0.5.0/docassemble/__init__.py +0 -5
  31. docassemble.InterviewStats-0.5.0/docassemble.InterviewStats.egg-info/namespace_packages.txt +0 -1
  32. docassemble.InterviewStats-0.5.0/docassemble.InterviewStats.egg-info/not-zip-safe +0 -1
  33. docassemble.InterviewStats-0.5.0/docassemble.InterviewStats.egg-info/requires.txt +0 -6
  34. docassemble.InterviewStats-0.5.0/setup.cfg +0 -7
  35. docassemble.InterviewStats-0.5.0/setup.py +0 -60
  36. {docassemble.InterviewStats-0.5.0 → docassemble_interviewstats-0.6.0}/LICENSE +0 -0
  37. {docassemble.InterviewStats-0.5.0 → docassemble_interviewstats-0.6.0}/docassemble/InterviewStats/data/sources/README.md +0 -0
  38. {docassemble.InterviewStats-0.5.0 → docassemble_interviewstats-0.6.0}/docassemble/InterviewStats/data/static/README.md +0 -0
  39. {docassemble.InterviewStats-0.5.0 → docassemble_interviewstats-0.6.0}/docassemble/InterviewStats/data/static/styles.css +0 -0
  40. {docassemble.InterviewStats-0.5.0 → docassemble_interviewstats-0.6.0}/docassemble/InterviewStats/data/templates/README.md +0 -0
  41. {docassemble.InterviewStats-0.5.0 → docassemble_interviewstats-0.6.0}/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,16 +1,21 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: docassemble.InterviewStats
3
- Version: 0.5.0
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)
3
+ Version: 0.6.0
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
9
8
  Description-Content-Type: text/markdown
10
9
  License-File: LICENSE
10
+ Requires-Dist: numpy>=1.0.4
11
+ Requires-Dist: pandas>=1.4.2
12
+ Requires-Dist: requests>=2.27.1
13
+ Dynamic: license-file
11
14
 
12
15
  # docassemble.InterviewStats
13
16
 
17
+ [![PyPI version](https://badge.fury.io/py/docassemble.InterviewStats.svg)](https://badge.fury.io/py/docassemble.InterviewStats)
18
+
14
19
  A docassemble interview that lets you view statistics from other saved interview responses.
15
20
 
16
21
  ## Getting Started
@@ -51,20 +56,10 @@ visiting this url:
51
56
  They will be directed immediately to download an XlSX file containing the
52
57
  statistics.
53
58
 
54
- ## Example
55
-
56
- ![Example Pic](static/example_pic.png)
57
-
58
-
59
- ## Roadmap
60
-
61
- Currently, we can show simple grouping over all the data points in the interview stats. However, the
62
- special visualization are only shown on based on the `zip` attribute. We're working to expand the
63
- features available here in conjunction with our [EFiling Integration](https://github.com/SuffolkLITLab/EfileProxyServer).
59
+ ## Contributing
64
60
 
65
61
  If you have specific feature requests, feel free to open an issue or make a PR!
66
62
 
67
-
68
63
  ## Authors
69
64
 
70
65
  * Quinten Steenhuis
@@ -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
@@ -40,20 +42,10 @@ visiting this url:
40
42
  They will be directed immediately to download an XlSX file containing the
41
43
  statistics.
42
44
 
43
- ## Example
44
-
45
- ![Example Pic](static/example_pic.png)
46
-
47
-
48
- ## Roadmap
49
-
50
- Currently, we can show simple grouping over all the data points in the interview stats. However, the
51
- special visualization are only shown on based on the `zip` attribute. We're working to expand the
52
- features available here in conjunction with our [EFiling Integration](https://github.com/SuffolkLITLab/EfileProxyServer).
45
+ ## Contributing
53
46
 
54
47
  If you have specific feature requests, feel free to open an issue or make a PR!
55
48
 
56
-
57
49
  ## Authors
58
50
 
59
51
  * Quinten Steenhuis
@@ -0,0 +1 @@
1
+ __version__ = "0.6.0"
@@ -37,6 +37,7 @@ code: |
37
37
  # Question screens
38
38
  #--------------------------------------------------------------------------
39
39
  ---
40
+ id: data-reporting
40
41
  question: |
41
42
  Data Reporting
42
43
  subquestion: |
@@ -55,6 +56,7 @@ fields:
55
56
  code: |
56
57
  [{interview.get('filename'): interview.get('title')} for interview in interview_menu()] or get_filenames()
57
58
  ---
59
+ id: quick-summary-screen
58
60
  question: |
59
61
  Quick Summary for ${ interview_title }
60
62
  subquestion: |
@@ -65,10 +67,8 @@ subquestion: |
65
67
  % if summary_stats[0] > 10000:
66
68
  You have a lot of sessions; some operations might take a long time.
67
69
  % endif
68
- ---
69
- Do you want to filter your data records by date?
70
70
  fields:
71
- - no label: filter_date
71
+ - Do you want to filter your data records by date?: filter_date
72
72
  datatype: yesnoradio
73
73
  default: False
74
74
  required: False
@@ -117,15 +117,17 @@ subquestion: |
117
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
118
 
119
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) })
120
+ % for col_single in col_headings_single:
121
+ [${col_single}](${ url_action('show_filtered_data_single', filter_by = col_single) })
122
122
  % endfor
123
123
  % endif
124
124
 
125
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.
126
+
127
+ Click this link to see [all the question tables on one screen](${ url_action('display_filtered_data_all') }).
127
128
 
128
129
  #### Download an excel file:
130
+
129
131
  To extract complete data including text/area/numeric fields, make an excel file:<br>
130
132
  [:file-excel: Make Excel Download](${ url_ask('generate_excel') })
131
133
  ---
@@ -201,6 +203,7 @@ code: |
201
203
  no_reportable_data
202
204
  ---
203
205
  event: no_interview_stats
206
+ id: interview-has-no-data
204
207
  question: Interview has no data!
205
208
  subquestion: |
206
209
  The interview you selected is <br>
@@ -211,6 +214,7 @@ buttons:
211
214
  - Restart: restart
212
215
  ---
213
216
  event: no_reportable_data
217
+ id: interview-has-no-reportable-data
214
218
  question: Interview has no reportable data, you should use Stats
215
219
  subquestion: |
216
220
  The interview you selected is <br>
@@ -244,6 +248,7 @@ code: |
244
248
  no_filtered_stats
245
249
  ---
246
250
  event: no_filtered_stats
251
+ id: no-records-in-this-period
247
252
  question: No records in this period
248
253
  subquestion: |
249
254
  % if date_op == 'between':
@@ -351,14 +356,15 @@ code: |
351
356
  xlsx_output.initialize(filename=proper_name + ".xlsx")
352
357
  writer = pd.ExcelWriter(xlsx_output.path(),
353
358
  engine='xlsxwriter',
354
- options={'remove_timezone': True})
359
+ engine_kwargs={'options': {'remove_timezone': True}})
355
360
  excel_formatted_data.to_excel(writer, index=False)
356
- writer.save()
361
+ writer.close()
357
362
  xlsx_output.commit()
358
363
  del writer
359
364
  show_excel_download
360
365
  ---
361
366
  event: show_excel_download
367
+ id: show-excel-download
362
368
  question: |
363
369
  Excel Download
364
370
  subquestion: |
@@ -374,9 +380,9 @@ code: |
374
380
  xlsx_output.initialize(filename=proper_name + ".xlsx")
375
381
  writer = pd.ExcelWriter(xlsx_output.path(),
376
382
  engine='xlsxwriter',
377
- options={'remove_timezone': True})
383
+ engine_kwargs={'options': {'remove_timezone': True}})
378
384
  bg_data.to_excel(writer, index=False)
379
- writer.save()
385
+ writer.close()
380
386
  xlsx_output.commit()
381
387
  del writer
382
388
  background_response_action('excel_resp', the_excel=xlsx_output)
@@ -390,6 +396,7 @@ subquestion: |
390
396
  reload: True
391
397
  ---
392
398
  event: show_failed_screen
399
+ id: failed-screen
393
400
  question: |
394
401
  Sorry, something went wrong
395
402
  subquestion: |
@@ -0,0 +1,373 @@
1
+ ---
2
+ metadata:
3
+ title: All Interview Statistics
4
+ short title: View Stats
5
+ temporary session: True
6
+ required privileges:
7
+ - admin
8
+ - developer
9
+ - stats_viewer
10
+ ---
11
+ features:
12
+ javascript:
13
+ - sortable.js
14
+ css:
15
+ - summary.css
16
+ question help button: True
17
+ question back button: True
18
+ ---
19
+ default screen parts:
20
+ footer: |
21
+ [:sync-alt: Restart](${ url_of('restart') } )
22
+ ---
23
+ modules:
24
+ - .snapshot_statistics
25
+ ---
26
+ objects:
27
+ - xlsx_output: DAFile.using(filename="stats.xlsx")
28
+ ---
29
+ #mandatory: True
30
+ code: |
31
+ message(get_filenames())
32
+ ---
33
+ id: interview order
34
+ mandatory: True
35
+ code: |
36
+ # intro
37
+ filename
38
+ simple_data
39
+ formatted_data
40
+ display_grouped_data
41
+ # viewed_filtered
42
+ ---
43
+ event: generate_excel
44
+ code: |
45
+ if stats_size > 10000:
46
+ if make_excel_task.ready():
47
+ if make_excel_task.failed():
48
+ show_failed_screen
49
+ show_excel_download
50
+ else:
51
+ waiting_screen
52
+ else:
53
+ proper_name = space_to_underscore(filename) + "_stats"
54
+ xlsx_output.initialize(filename=proper_name + ".xlsx")
55
+ writer = pd.ExcelWriter(xlsx_output.path(),
56
+ engine='xlsxwriter',
57
+ engine_kwargs={'options': {'remove_timezone': True}})
58
+ formatted_data.to_excel(writer, index=False)
59
+ writer.close()
60
+ xlsx_output.commit()
61
+ del writer
62
+ show_excel_download
63
+ generate_excel = True
64
+ ---
65
+ event: show_excel_download
66
+ id: show-excel-download
67
+ question: |
68
+ Excel Download
69
+ subquestion: |
70
+ [:file-excel: Download](${ xlsx_output.url_for() })
71
+ ---
72
+ code: |
73
+ make_excel_task = background_action('excel_task', data=formatted_data)
74
+ ---
75
+ event: excel_task
76
+ code: |
77
+ bg_data = action_argument('data')
78
+ proper_name = space_to_underscore(filename) + "_stats"
79
+ xlsx_output.initialize(filename=proper_name + ".xlsx")
80
+ writer = pd.ExcelWriter(xlsx_output.path(),
81
+ engine='xlsxwriter',
82
+ engine_kwargs={'options': {'remove_timezone': True}})
83
+ bg_data.to_excel(writer, index=False)
84
+ writer.close()
85
+ xlsx_output.commit()
86
+ del writer
87
+ background_response_action('excel_resp', the_excel=xlsx_output)
88
+ ---
89
+ event: generate_summary_excel
90
+ code: |
91
+ import pandas as pd
92
+ proper_name = 'snapshot_summary'
93
+ xlsx_output.initialize(filename=proper_name + ".xlsx")
94
+ df = pd.DataFrame(summary_by_filename)
95
+ writer = pd.ExcelWriter(xlsx_output.path(),
96
+ engine='xlsxwriter',
97
+ engine_kwargs={'options': {'remove_timezone': True}})
98
+ df.to_excel(writer, index=False)
99
+ writer.close()
100
+ xlsx_output.commit()
101
+ del writer
102
+ show_excel_download
103
+ ---
104
+ event: generate_sessions_excel
105
+ code: |
106
+ import pandas as pd
107
+ proper_name = 'sessions_summary'
108
+ xlsx_output.initialize(filename=proper_name + ".xlsx")
109
+ df = pd.DataFrame(session_summary_by_filename)
110
+ writer = pd.ExcelWriter(xlsx_output.path(),
111
+ engine='xlsxwriter',
112
+ engine_kwargs={'options': {'remove_timezone': True}})
113
+ df.to_excel(writer, index=False)
114
+ writer.close()
115
+ xlsx_output.commit()
116
+ del writer
117
+ show_excel_download
118
+ ---
119
+ event: waiting_screen
120
+ id: waiting screen
121
+ question: |
122
+ We're generating your excel sheet.
123
+ subquestion: |
124
+ This may take a few minutes, so leave this tab open. Thanks for waiting.
125
+ reload: True
126
+ ---
127
+ event: show_failed_screen
128
+ id: failed-screen
129
+ question: |
130
+ Sorry, something went wrong
131
+ subquestion: |
132
+ We had some trouble trying to assemble your excel sheet.
133
+
134
+ ${ make_excel_task.result() }
135
+ buttons:
136
+ - Restart: restart
137
+ ---
138
+ event: excel_resp
139
+ code: |
140
+ xlsx_output = action_argument('the_excel')
141
+ background_response()
142
+ ---
143
+ code: |
144
+ summary_stats = get_summary_stats(filename, date_from=stats_date_from if filter_by_date else None, date_to=stats_date_to if filter_by_date else None)
145
+ ---
146
+ reconsider: summary_stats
147
+ id: summary-data-screen
148
+ question: |
149
+ Summary data for ${ interview_title }
150
+ subquestion: |
151
+ * Total sessions: ${ summary_stats[0] }
152
+ * First session: ${ format_date(summary_stats[1], format='MM/dd/yyyy') }
153
+ * Latest session: ${ format_date(summary_stats[2], format='MM/dd/yyyy') }
154
+
155
+ % if summary_stats[0] > 10000:
156
+ You have a lot of sessions; some operations might
157
+ take a long time.
158
+ % endif
159
+ continue button field: simple_data
160
+ ---
161
+ # question: Interview Statistics
162
+ # subquestion: |
163
+ # We have two features in this repo:
164
+
165
+ # 1. Stats - it generates statistics, maps, charts and a downadable excel file.
166
+ # - If you have any inteview that uses `store_variables_snapshot(data={'zip': users[0].address.zip})` to save zip data, you should use Stats.
167
+ # - You can filter your data by date or by state.
168
+ # - Click the **Continue button** below to use this feature.
169
+ # 1. Data Reporting - it generates statistics tables and a downloadable excel file.
170
+ # - If you have any interview that uses `save_input_data()` to save reportable data, you should use Data Reporting.
171
+ # - You can filter your data by date.
172
+ # - [Click here to use Data Reporting](${interview_url(i=user_info().package + ":data_report.yml")})
173
+
174
+ # field: intro
175
+ ---
176
+ code: |
177
+ overall_stats = get_overall_stats()
178
+ summary_by_filename = get_summary_stats_by_filename()
179
+ session_summary_by_filename = get_session_summary_stats_by_filename()
180
+ ---
181
+ id: what-interview-do-you-want-to-view-statistics-for
182
+ question: |
183
+ What interview do you want to view statistics for?
184
+ under: |
185
+ #### Summary statistics
186
+
187
+ Keep in mind:
188
+
189
+ % if isinstance(get_config("interview delete days"), int):
190
+ Docassemble sessions on this server are kept for only ${ get_config("interview delete days") } days.
191
+ % else:
192
+ Docassemble sessions typically are kept for only 90 days.
193
+ % endif
194
+ Snapshotted sessions
195
+ may or may not be set to be kept longer. Statistics below will reflect the settings
196
+ on your server and interviews.
197
+
198
+ Total sessions with saved statistics across all interviews: ${ overall_stats[0] }
199
+
200
+ % if summary_by_filename:
201
+ <div style="margin-bottom:0.5rem">
202
+ <a href="${ url_ask('generate_summary_excel') }" class="btn btn-link">
203
+ <i class="fa fa-file-excel" aria-hidden="true"></i>
204
+ Download snapshot summary
205
+ </a>
206
+ </div>
207
+ <h2 class="h4">Snapshotted sessions by interview title</h2>
208
+ <table id="summary-table" class="table table-striped table-bordered sortable">
209
+ <thead>
210
+ <tr>
211
+ <th>Interview (click to sort)</th>
212
+ <th data-type="number">30 days</th>
213
+ <th data-type="number">90 days</th>
214
+ <th data-type="number">365 days</th>
215
+ <th data-type="number">All time</th>
216
+ <th data-type="date">Last (30d)</th>
217
+ <th data-type="date">Latest</th>
218
+ </tr>
219
+ </thead>
220
+ <tbody>
221
+ % for row in summary_by_filename:
222
+ <tr>
223
+ <td title="${ row['filename'] }"><span class="short-name">${ shorten_filename(row['filename']) }</span></td>
224
+ <td>${ row.get('count_30d', 0) }</td>
225
+ <td>${ row.get('count_90d', 0) }</td>
226
+ <td>${ row.get('count_365d', 0) }</td>
227
+ <td>${ row.get('count_all', 0) }</td>
228
+ <td>${ format_date(row.get('max_30d'), format='MM/dd/yyyy') if row.get('max_30d') else '' }</td>
229
+ <td>${ format_date(row.get('max_all'), format='MM/dd/yyyy') if row.get('max_all') else '' }</td>
230
+ </tr>
231
+ % endfor
232
+ </tbody>
233
+ </table>
234
+ % endif
235
+
236
+ <p><a href="${ url_ask('session_data_screen') }">View all sessions by interview title</a></p>
237
+ fields:
238
+ - Filename: filename
239
+ datatype: combobox
240
+ code: |
241
+ get_combined_filename_list()
242
+ - Filter by date range?: filter_by_date
243
+ datatype: yesno
244
+ default: False
245
+ - From date: stats_date_from
246
+ datatype: date
247
+ show if: filter_by_date
248
+ - To date: stats_date_to
249
+ datatype: date
250
+ show if: filter_by_date
251
+ ---
252
+ code: |
253
+ session_summary_by_filename = get_session_summary_stats_by_filename()
254
+ ---
255
+ event: session_data_screen
256
+ id: session-data-screen
257
+ question: |
258
+ All sessions by interview title
259
+ subquestion: |
260
+ % if session_summary_by_filename:
261
+ <div style="margin-bottom:0.5rem">
262
+ <a href="${ url_ask('generate_sessions_excel') }" class="btn btn-link">
263
+ <i class="fa fa-file-excel" aria-hidden="true"></i>
264
+ Download session summary
265
+ </a>
266
+ </div>
267
+ <table id="sessions-table" class="table table-striped table-bordered sortable">
268
+ <thead>
269
+ <tr>
270
+ <th>Interview</th>
271
+ <th data-type="number">30 days</th>
272
+ <th data-type="number">90 days</th>
273
+ <th data-type="number">365 days</th>
274
+ <th data-type="number">All time</th>
275
+ <th data-type="date">Latest</th>
276
+ </tr>
277
+ </thead>
278
+ <tbody>
279
+ % for row in session_summary_by_filename:
280
+ <tr>
281
+ <td title="${ row['filename'] }"><span class="short-name">${ shorten_filename(row['filename']) }</span></td>
282
+ <td>${ row.get('count_30d', 0) }</td>
283
+ <td>${ row.get('count_90d', 0) }</td>
284
+ <td>${ row.get('count_365d', 0) }</td>
285
+ <td>${ row.get('count_all', 0) }</td>
286
+ <td>${ format_date(row.get('max_all'), format='MM/dd/yyyy') if row.get('max_all') else '' }</td>
287
+ </tr>
288
+ % endfor
289
+ </tbody>
290
+ </table>
291
+ % else:
292
+ No session data found.
293
+ % endif
294
+
295
+ [Back to summary](${ url_ask('filename') })
296
+ ---
297
+ code: |
298
+ if filename is not None:
299
+ if 'data/questions' in filename or 'playground' in filename:
300
+ stats = get_stats(filename, date_from=stats_date_from if filter_by_date else None, date_to=stats_date_to if filter_by_date else None)
301
+ else:
302
+ stats = get_stats(filename.replace(':',':data/questions/'), date_from=stats_date_from if filter_by_date else None, date_to=stats_date_to if filter_by_date else None)
303
+ else:
304
+ no_interview_stats
305
+ ---
306
+ code: |
307
+ try:
308
+ interview_title = next(iter([interview.get('title') for interview in interview_menu() if interview.get('filename') == filename]), filename)
309
+ except:
310
+ interview_title = filename
311
+ # A survey interview's title is saved in Stats, yet interview_title is used on Summary page which comes before we kick out survey interviews, so we set the value here.
312
+ if stats and 'title' in stats[0].keys():
313
+ interview_title = stats[0]['title']
314
+ ---
315
+ id: there-are-no-interviews-to-look-for-data-in
316
+ event: no_interview_stats
317
+ question: There are no interviews to look for data in!
318
+ subquestion: |
319
+ There aren't any interviews that we can look for data in. There should be at least one interview
320
+ listed in the 'dispatch' section of the configuration. See
321
+ [the docassemble documentation](https://docassemble.org/docs/config.html#dispatch) for more information
322
+ buttons:
323
+ - Exit: exit
324
+ ---
325
+ need: stats
326
+ code: |
327
+ columns = get_columns(stats)
328
+ my_col = next(iter(columns), None)
329
+ groupable_columns = list(set(columns).difference(set(['modtime'])))
330
+ possible_states = get_column_values(stats, 'state')
331
+ ---
332
+ code: |
333
+ import pandas as pd
334
+ ---
335
+ code: |
336
+ formatted_data = pd.DataFrame()
337
+ stats_size = 0
338
+ if stats is not None:
339
+ formatted_data = pd.DataFrame(stats)
340
+ stats_size = formatted_data.shape[0]
341
+ ---
342
+ event: show_grouped_data
343
+ code: |
344
+ my_col = action_argument('group_by')
345
+ force_ask('display_grouped_data')
346
+ ---
347
+ id: stats-for
348
+ question: |
349
+ Stats for ${interview_title}
350
+ subquestion: |
351
+ Total submissions: ${ len(stats) }
352
+
353
+ % if len(stats):
354
+ Group by:
355
+ % for col in groupable_columns:
356
+ [${col}](${ url_action('show_grouped_data', group_by = col) })
357
+ % endfor
358
+
359
+ <%
360
+ grouped = formatted_data.groupby(by=my_col).count()
361
+ grouped = grouped.sort_values(grouped.columns[0], ascending=False)
362
+ total_groups = len(grouped)
363
+ %>
364
+ % if total_groups > 50:
365
+ Showing the top 50 of ${ total_groups } groups by count. Download the Excel file below for the full breakdown.
366
+ % endif
367
+ ${ grouped.head(50).to_html(classes=('table','table-striped')) }
368
+
369
+ [:file-excel: Download](${ url_ask('generate_excel') })
370
+ % else:
371
+ No records found for this date range. Try a wider range or clear the date filter.
372
+ % endif
373
+ event: display_grouped_data
@@ -72,6 +72,7 @@ code: |
72
72
  formatted_data = pd.DataFrame(stats)
73
73
  ---
74
74
  event: no_interview_stats
75
+ id: there-are-no-interviews-to-look-for-data-in
75
76
  question: There are no interviews to look for data in!
76
77
  subquestion: |
77
78
  There aren't any interviews that we can look for data in. There should be at least one interview
@@ -85,9 +86,9 @@ code: |
85
86
  xlsx_output.initialize(filename=proper_name + ".xlsx")
86
87
  writer = pd.ExcelWriter(xlsx_output.path(),
87
88
  engine='xlsxwriter',
88
- options={'remove_timezone': True})
89
+ engine_kwargs={'options': {'remove_timezone': True}})
89
90
  formatted_data.to_excel(writer, index=False)
90
- writer.save()
91
+ writer.close()
91
92
  xlsx_output.commit()
92
93
  del writer
93
94
  saved_data = True
@@ -20,12 +20,16 @@ code: |
20
20
  if what_to_do == 'generate':
21
21
  number_of_data_points
22
22
  # Edit your data here
23
- ma_zips = ['01001', '01002', '02330', '02332', '02771', '027771',
24
- '02125', '02122', '02124', '02123', '02115', '02116']
23
+ ma_zips = list(set([f'{random.randint(75001, 79999):05}' for _ in range(300)])) \
24
+ + list(set([f'{random.randint(1001,2799):05}' for _ in range(300)]))
25
+ import datetime
26
+ datetimes = [datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None) - datetime.timedelta(days=i) for i in range(30)]
25
27
  def my_generator():
26
28
  for _ in range(number_of_data_points):
27
29
  yield {'zip': random.choice(ma_zips), 'state': 'MA'}
28
- write_random_user(my_generator())
30
+ def modtime_generator():
31
+ return random.choice(datetimes)
32
+ write_random_user(my_generator(), modtime_generator=modtime_generator)
29
33
  elif what_to_do == 'delete':
30
34
  which_to_delete
31
35
  delete_data_from_interview(which_to_delete)
@@ -36,6 +40,7 @@ code: |
36
40
  users[0].name.last = 'Brown'
37
41
  users[0].address
38
42
  ---
43
+ id: snapshot-generator
39
44
  question: |
40
45
  Snapshot Generator
41
46
  subquestion: |
@@ -49,6 +54,7 @@ fields:
49
54
  - Generate: generate
50
55
  - Delete: delete
51
56
  ---
57
+ id: how-many-data-points-do-you-want-to-generate
52
58
  question: |
53
59
  How many data points do you want to generate?
54
60
  fields:
@@ -58,6 +64,7 @@ fields:
58
64
  min: 1
59
65
  max: 400000
60
66
  ---
67
+ id: what-interview-do-you-want-to-delete-all-statistics-from
61
68
  question: |
62
69
  What interview do you want to delete all statistics from?
63
70
  fields:
@@ -73,6 +80,7 @@ validation code: |
73
80
  if which_to_delete_confirm != which_to_delete:
74
81
  validation_error("Are you sure you are deleting the right one?")
75
82
  ---
83
+ id: snapshot-ending-screen
76
84
  event: ending_screen
77
85
  question: |
78
86
  % if what_to_do == 'generate':
@@ -29,6 +29,7 @@ code: |
29
29
  # Question screens
30
30
  #--------------------------------------------------------------------------
31
31
  ---
32
+ id: survey data generator
32
33
  question: |
33
34
  Survey Data Generator
34
35
  subquestion: |
@@ -42,6 +43,7 @@ fields:
42
43
  - Generate: generate
43
44
  - Delete: delete
44
45
  ---
46
+ id: how-many-data-points-do-you-want-to-generate
45
47
  question: |
46
48
  How many data points do you want to generate?
47
49
  fields:
@@ -52,6 +54,7 @@ fields:
52
54
  min: 1
53
55
  max: 4000
54
56
  ---
57
+ id: which-survey-interviews-do-you-want-to-delete?
55
58
  question: |
56
59
  Which survey interviews do you want to delete?
57
60
  fields:
@@ -67,6 +70,7 @@ validation code: |
67
70
  if which_to_delete_confirm != which_to_delete:
68
71
  validation_error("Are you sure you are deleting the right one?")
69
72
  ---
73
+ id: ending-screen
70
74
  event: ending_screen
71
75
  question: |
72
76
  % if what_to_do == 'generate':