docassemble.InterviewStats 0.5.0__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 (37) hide show
  1. docassemble_interviewstats-0.5.3/MANIFEST.in +18 -0
  2. {docassemble.InterviewStats-0.5.0/docassemble.InterviewStats.egg-info → docassemble_interviewstats-0.5.3}/PKG-INFO +15 -7
  3. {docassemble.InterviewStats-0.5.0 → 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.0 → docassemble_interviewstats-0.5.3}/docassemble/InterviewStats/data/questions/data_report.yml +4 -4
  6. docassemble_interviewstats-0.5.3/docassemble/InterviewStats/data/questions/stats.yml +450 -0
  7. {docassemble.InterviewStats-0.5.0 → docassemble_interviewstats-0.5.3}/docassemble/InterviewStats/data/questions/subscribe.yml +2 -2
  8. docassemble_interviewstats-0.5.3/docassemble/InterviewStats/data/static/sortable.js +86 -0
  9. docassemble_interviewstats-0.5.3/docassemble/InterviewStats/data/static/summary.css +8 -0
  10. {docassemble.InterviewStats-0.5.0 → docassemble_interviewstats-0.5.3}/docassemble/InterviewStats/data_report.py +13 -4
  11. {docassemble.InterviewStats-0.5.0 → docassemble_interviewstats-0.5.3}/docassemble/InterviewStats/generate_test_data.py +9 -1
  12. {docassemble.InterviewStats-0.5.0 → docassemble_interviewstats-0.5.3}/docassemble/InterviewStats/snapshot_geography.py +5 -1
  13. docassemble_interviewstats-0.5.3/docassemble/InterviewStats/snapshot_statistics.py +282 -0
  14. {docassemble.InterviewStats-0.5.0 → docassemble_interviewstats-0.5.3/docassemble.InterviewStats.egg-info}/PKG-INFO +15 -7
  15. {docassemble.InterviewStats-0.5.0 → docassemble_interviewstats-0.5.3}/docassemble.InterviewStats.egg-info/SOURCES.txt +3 -5
  16. {docassemble.InterviewStats-0.5.0 → docassemble_interviewstats-0.5.3}/docassemble.InterviewStats.egg-info/requires.txt +1 -1
  17. {docassemble.InterviewStats-0.5.0 → docassemble_interviewstats-0.5.3}/docassemble.InterviewStats.egg-info/top_level.txt +2 -0
  18. docassemble_interviewstats-0.5.3/pyproject.toml +58 -0
  19. docassemble_interviewstats-0.5.3/setup.cfg +4 -0
  20. docassemble.InterviewStats-0.5.0/MANIFEST.in +0 -1
  21. docassemble.InterviewStats-0.5.0/docassemble/InterviewStats/__init__.py +0 -1
  22. docassemble.InterviewStats-0.5.0/docassemble/InterviewStats/data/questions/stats.yml +0 -318
  23. docassemble.InterviewStats-0.5.0/docassemble/InterviewStats/snapshot_statistics.py +0 -87
  24. docassemble.InterviewStats-0.5.0/docassemble/__init__.py +0 -5
  25. docassemble.InterviewStats-0.5.0/docassemble.InterviewStats.egg-info/namespace_packages.txt +0 -1
  26. docassemble.InterviewStats-0.5.0/docassemble.InterviewStats.egg-info/not-zip-safe +0 -1
  27. docassemble.InterviewStats-0.5.0/setup.cfg +0 -7
  28. docassemble.InterviewStats-0.5.0/setup.py +0 -60
  29. {docassemble.InterviewStats-0.5.0 → docassemble_interviewstats-0.5.3}/LICENSE +0 -0
  30. {docassemble.InterviewStats-0.5.0 → docassemble_interviewstats-0.5.3}/docassemble/InterviewStats/cache_geography.py +0 -0
  31. {docassemble.InterviewStats-0.5.0 → docassemble_interviewstats-0.5.3}/docassemble/InterviewStats/data/questions/test_create_snapshot_data.yml +0 -0
  32. {docassemble.InterviewStats-0.5.0 → docassemble_interviewstats-0.5.3}/docassemble/InterviewStats/data/questions/test_survey_data_generator.yml +0 -0
  33. {docassemble.InterviewStats-0.5.0 → docassemble_interviewstats-0.5.3}/docassemble/InterviewStats/data/sources/README.md +0 -0
  34. {docassemble.InterviewStats-0.5.0 → docassemble_interviewstats-0.5.3}/docassemble/InterviewStats/data/static/README.md +0 -0
  35. {docassemble.InterviewStats-0.5.0 → docassemble_interviewstats-0.5.3}/docassemble/InterviewStats/data/static/styles.css +0 -0
  36. {docassemble.InterviewStats-0.5.0 → docassemble_interviewstats-0.5.3}/docassemble/InterviewStats/data/templates/README.md +0 -0
  37. {docassemble.InterviewStats-0.5.0 → 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,16 +1,24 @@
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.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
9
8
  Description-Content-Type: text/markdown
10
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
11
17
 
12
18
  # docassemble.InterviewStats
13
19
 
20
+ [![PyPI version](https://badge.fury.io/py/docassemble.InterviewStats.svg)](https://badge.fury.io/py/docassemble.InterviewStats)
21
+
14
22
  A docassemble interview that lets you view statistics from other saved interview responses.
15
23
 
16
24
  ## Getting Started
@@ -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'
@@ -351,9 +351,9 @@ code: |
351
351
  xlsx_output.initialize(filename=proper_name + ".xlsx")
352
352
  writer = pd.ExcelWriter(xlsx_output.path(),
353
353
  engine='xlsxwriter',
354
- options={'remove_timezone': True})
354
+ engine_kwargs={'options': {'remove_timezone': True}})
355
355
  excel_formatted_data.to_excel(writer, index=False)
356
- writer.save()
356
+ writer.close()
357
357
  xlsx_output.commit()
358
358
  del writer
359
359
  show_excel_download
@@ -374,9 +374,9 @@ code: |
374
374
  xlsx_output.initialize(filename=proper_name + ".xlsx")
375
375
  writer = pd.ExcelWriter(xlsx_output.path(),
376
376
  engine='xlsxwriter',
377
- options={'remove_timezone': True})
377
+ engine_kwargs={'options': {'remove_timezone': True}})
378
378
  bg_data.to_excel(writer, index=False)
379
- writer.save()
379
+ writer.close()
380
380
  xlsx_output.commit()
381
381
  del writer
382
382
  background_response_action('excel_resp', the_excel=xlsx_output)
@@ -0,0 +1,450 @@
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
+ # - .snapshot_geography
26
+ ---
27
+ objects:
28
+ - xlsx_output: DAFile.using(filename="stats.xlsx")
29
+ ---
30
+ #mandatory: True
31
+ code: |
32
+ message(get_filenames())
33
+ ---
34
+ id: interview order
35
+ mandatory: True
36
+ code: |
37
+ # intro
38
+ filename
39
+ simple_data
40
+ formatted_data
41
+ display_grouped_data
42
+ # viewed_filtered
43
+ ---
44
+ event: generate_excel
45
+ code: |
46
+ if stats_size > 10000:
47
+ if make_excel_task.ready():
48
+ if make_excel_task.failed():
49
+ show_failed_screen
50
+ show_excel_download
51
+ else:
52
+ waiting_screen
53
+ else:
54
+ proper_name = space_to_underscore(filename) + "_stats"
55
+ xlsx_output.initialize(filename=proper_name + ".xlsx")
56
+ writer = pd.ExcelWriter(xlsx_output.path(),
57
+ engine='xlsxwriter',
58
+ engine_kwargs={'options': {'remove_timezone': True}})
59
+ formatted_data.to_excel(writer, index=False)
60
+ writer.close()
61
+ xlsx_output.commit()
62
+ del writer
63
+ show_excel_download
64
+ generate_excel = True
65
+ ---
66
+ event: 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
+ question: |
129
+ Sorry, something went wrong
130
+ subquestion: |
131
+ We had some trouble trying to assemble your excel sheet.
132
+
133
+ ${ make_excel_task.result() }
134
+ buttons:
135
+ - Restart: restart
136
+ ---
137
+ event: excel_resp
138
+ code: |
139
+ xlsx_output = action_argument('the_excel')
140
+ background_response()
141
+ ---
142
+ code: |
143
+ summary_stats = get_summary_stats(filename)
144
+ ---
145
+ reconsider: summary_stats
146
+ question: |
147
+ Summary data for ${ interview_title }
148
+ subquestion: |
149
+ * Total sessions: ${ summary_stats[0] }
150
+ * First session: ${ format_date(summary_stats[1], format='MM/dd/yyyy') }
151
+ * Latest session: ${ format_date(summary_stats[2], format='MM/dd/yyyy') }
152
+
153
+ % if summary_stats[0] > 10000:
154
+ You have a lot of sessions; some operations might
155
+ take a long time.
156
+ % endif
157
+ continue button field: simple_data
158
+ # ---
159
+ # code: |
160
+ # geo_layout = make_usage_map(formatted_data)
161
+ # map_script, map_div, map_inline = get_embedable_usage_map(geo_layout)
162
+ # del geo_layout
163
+ ---
164
+ # question: Interview Statistics
165
+ # subquestion: |
166
+ # We have two features in this repo:
167
+
168
+ # 1. Stats - it generates statistics, maps, charts and a downadable excel file.
169
+ # - If you have any inteview that uses `store_variables_snapshot(data={'zip': users[0].address.zip})` to save zip data, you should use Stats.
170
+ # - You can filter your data by date or by state.
171
+ # - Click the **Continue button** below to use this feature.
172
+ # 1. Data Reporting - it generates statistics tables and a downloadable excel file.
173
+ # - If you have any interview that uses `save_input_data()` to save reportable data, you should use Data Reporting.
174
+ # - You can filter your data by date.
175
+ # - [Click here to use Data Reporting](${interview_url(i=user_info().package + ":data_report.yml")})
176
+
177
+ # field: intro
178
+ ---
179
+ code: |
180
+ overall_stats = get_overall_stats()
181
+ summary_by_filename = get_summary_stats_by_filename()
182
+ session_overall_stats = get_session_overall_stats()
183
+ session_summary_by_filename = get_session_summary_stats_by_filename()
184
+ ---
185
+ question: |
186
+ What interview do you want to view statistics for?
187
+ under: |
188
+ #### Summary statistics
189
+
190
+ Keep in mind:
191
+
192
+ % if isinstance(get_config("interview delete days"), int):
193
+ Docassemble sessions on this server are kept for only ${ get_config("interview delete days") } days.
194
+ % else:
195
+ Docassemble sessions typically are kept for only 90 days.
196
+ % endif
197
+ Snapshotted sessions
198
+ may or may not be set to be kept longer. Statistics below will reflect the settings
199
+ on your server and interviews.
200
+
201
+ Total sessions with saved statistics across all interviews: ${ overall_stats[0] }
202
+
203
+ <div class="stats-tabs">
204
+ <ul class="nav nav-tabs" id="stats-tabs">
205
+ <li class="nav-item">
206
+ <a href="#" class="nav-link active" id="snapshots-tab">Snapshots</a>
207
+ </li>
208
+ <li class="nav-item">
209
+ <a href="#" class="nav-link" id="sessions-tab">All sessions</a>
210
+ </li>
211
+ </ul>
212
+ </div>
213
+
214
+ % if summary_by_filename:
215
+ <div id="snapshots-panel">
216
+ <div style="margin-bottom:0.5rem">
217
+ <a href="${ url_ask('generate_summary_excel') }" class="btn btn-link">
218
+ <i class="fa fa-file-excel" aria-hidden="true"></i>
219
+ Download snapshot summary
220
+ </a>
221
+ </div>
222
+ <h2 class="h4">Snapshotted sessions by interview title</h2>
223
+ <table id="summary-table" class="table table-striped table-bordered sortable">
224
+ <thead>
225
+ <tr>
226
+ <th>Interview (click to sort)</th>
227
+ <th data-type="number">30 days</th>
228
+ <th data-type="number">90 days</th>
229
+ <th data-type="number">365 days</th>
230
+ <th data-type="number">All time</th>
231
+ <th data-type="date">Last (30d)</th>
232
+ <th data-type="date">Latest</th>
233
+ </tr>
234
+ </thead>
235
+ <tbody>
236
+ % for row in summary_by_filename:
237
+ <tr>
238
+ <td title="${ row['filename'] }"><span class="short-name">${ shorten_filename(row['filename']) }</span></td>
239
+ <td>${ row.get('count_30d', 0) }</td>
240
+ <td>${ row.get('count_90d', 0) }</td>
241
+ <td>${ row.get('count_365d', 0) }</td>
242
+ <td>${ row.get('count_all', 0) }</td>
243
+ <td>${ format_date(row.get('max_30d'), format='MM/dd/yyyy') if row.get('max_30d') else '' }</td>
244
+ <td>${ format_date(row.get('max_all'), format='MM/dd/yyyy') if row.get('max_all') else '' }</td>
245
+ </tr>
246
+ % endfor
247
+ </tbody>
248
+ </table>
249
+ </div>
250
+
251
+
252
+ % endif
253
+ % if session_summary_by_filename:
254
+ <div id="sessions-panel" style="display:none;">
255
+ <div style="margin-bottom:0.5rem">
256
+ <a href="${ url_ask('generate_sessions_excel') }" class="btn btn-link">
257
+ <i class="fa fa-file-excel" aria-hidden="true"></i>
258
+ Download session summary
259
+ </a>
260
+ </div>
261
+ <h2 class="h4">All sessions by interview title</h2>
262
+ <table id="sessions-table" class="table table-striped table-bordered sortable">
263
+ <thead>
264
+ <tr>
265
+ <th>Interview</th>
266
+ <th data-type="number">30 days</th>
267
+ <th data-type="number">90 days</th>
268
+ <th data-type="number">365 days</th>
269
+ <th data-type="number">All time</th>
270
+ <th data-type="date">Latest</th>
271
+ </tr>
272
+ </thead>
273
+ <tbody>
274
+ % for row in session_summary_by_filename:
275
+ <tr>
276
+ <td title="${ row['filename'] }"><span class="short-name">${ shorten_filename(row['filename']) }</span></td>
277
+ <td>${ row.get('count_30d', 0) }</td>
278
+ <td>${ row.get('count_90d', 0) }</td>
279
+ <td>${ row.get('count_365d', 0) }</td>
280
+ <td>${ row.get('count_all', 0) }</td>
281
+ <td>${ format_date(row.get('max_all'), format='MM/dd/yyyy') if row.get('max_all') else '' }</td>
282
+ </tr>
283
+ % endfor
284
+ </tbody>
285
+ </table>
286
+ </div>
287
+ % endif
288
+ fields:
289
+ - Filename: filename
290
+ datatype: combobox
291
+ code: |
292
+ get_combined_filename_list()
293
+ ---
294
+ code: |
295
+ if filename is not None:
296
+ if 'data/questions' in filename or 'playground' in filename:
297
+ stats = get_stats(filename)
298
+ else:
299
+ stats = get_stats(filename.replace(':',':data/questions/'))
300
+ else:
301
+ no_interview_stats
302
+ ---
303
+ code: |
304
+ try:
305
+ interview_title = next(iter([interview.get('title') for interview in interview_menu() if interview.get('filename') == filename]), filename)
306
+ except:
307
+ interview_title = filename
308
+ # 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.
309
+ if stats and 'title' in stats[0].keys():
310
+ interview_title = stats[0]['title']
311
+ ---
312
+ event: no_interview_stats
313
+ question: There are no interviews to look for data in!
314
+ subquestion: |
315
+ There aren't any interviews that we can look for data in. There should be at least one interview
316
+ listed in the 'dispatch' section of the configuration. See
317
+ [the docassemble documentation](https://docassemble.org/docs/config.html#dispatch) for more information
318
+ buttons:
319
+ - Exit: exit
320
+ ---
321
+ need: stats
322
+ code: |
323
+ columns = get_columns(stats)
324
+ my_col = next(iter(columns), None)
325
+ groupable_columns = list(set(columns).difference(set(['modtime'])))
326
+ possible_states = get_column_values(stats, 'state')
327
+ ---
328
+ code: |
329
+ import pandas as pd
330
+ ---
331
+ code: |
332
+ formatted_data = pd.DataFrame()
333
+ stats_size = 0
334
+ if stats is not None:
335
+ formatted_data = pd.DataFrame(stats)
336
+ stats_size = formatted_data.shape[0]
337
+ ---
338
+ # mandatory: True
339
+ id: view_data
340
+ question: |
341
+ Stats for ${interview_title}
342
+ subquestion: |
343
+ Total submissions: ${ len(stats) }
344
+
345
+ % if len(stats):
346
+ Group by:
347
+ % for col in groupable_columns:
348
+ [${col}](${ url_action('show_grouped_data', group_by = col ) }) |
349
+ % endfor
350
+
351
+ [:file-excel: Make Excel Download](${ url_ask('generate_excel') })
352
+ % endif
353
+ # under: |
354
+ # ${ map_div }
355
+ fields:
356
+ - Filter State: filter_state
357
+ datatype: yesno
358
+ disable if:
359
+ code: |
360
+ 'state' not in formatted_data.columns or not possible_states
361
+ - Find States that: state_op
362
+ datatype: dropdown
363
+ show if: filter_state
364
+ code: |
365
+ [{'eq': "equal"},
366
+ {'ne': "don't equal"}]
367
+ - this state: state_to_filter
368
+ datatype: dropdown
369
+ show if: filter_state
370
+ code: |
371
+ possible_states
372
+ - Filter by Date: filter_date
373
+ datatype: yesno
374
+ disable if:
375
+ code: |
376
+ 'modtime' not in formatted_data.columns
377
+ - Find submissions that are: date_op
378
+ datatype: dropdown
379
+ show if: filter_date
380
+ code: |
381
+ [{'gt': 'after'},
382
+ {'lt': 'before'},
383
+ {'eq': 'the same day as'}]
384
+ - this date: date_to_filter
385
+ datatype: date
386
+ show if: filter_date
387
+ # script: |
388
+ # ${ map_script}
389
+ continue button field: start_filter
390
+ continue button label: Filter
391
+ #help: |
392
+ # ${ formatted_data.to_html() }
393
+ # ---
394
+ # mandatory: True
395
+ # event: viewed_filtered
396
+ # depends on: filt_map_script, filt_map_div
397
+ # need: filt_map_script, filt_map_div
398
+ # id: view_filtered_data
399
+ # question: |
400
+ # Filtered: ${ filter_description }
401
+ # under: |
402
+ # ${ filt_map_div }
403
+ # script: |
404
+ # ${ filt_map_script }
405
+ # back button: True
406
+ ---
407
+ event: show_grouped_data
408
+ code: |
409
+ my_col = action_argument('group_by')
410
+ force_ask('display_grouped_data')
411
+ ---
412
+ question: |
413
+ Stats for ${interview_title}
414
+ subquestion: |
415
+ Total submissions: ${ len(stats) }
416
+
417
+ Group by:
418
+ % for col in groupable_columns:
419
+ [${col}](${ url_action('show_grouped_data', group_by = col) })
420
+ % endfor
421
+
422
+ ${ formatted_data.groupby(by=my_col).count().to_html(classes=('table','table-striped')) }
423
+
424
+ [:file-excel: Download](${ url_ask('generate_excel') })
425
+ # under: |
426
+ # ${ map_div }
427
+ # script: |
428
+ # ${ map_script }
429
+ event: display_grouped_data
430
+ # ---
431
+ # need: start_filter
432
+ # depends on: state_op, state_to_filter
433
+ # code: |
434
+ # filter_list = []
435
+ # readable = {'state': 'State', 'modtime': 'Date',
436
+ # 'eq': 'is', 'ne': 'is not', 'lt': '<', 'gt': '>'}
437
+ # if defined('filter_state') and filter_state:
438
+ # filter_list.append(('state', state_op, state_to_filter))
439
+ # if defined('filter_date') and filter_date:
440
+ # date_to_filter = date_to_filter.replace(tzinfo=None)
441
+ # filter_list.append(('modtime', date_op, date_to_filter))
442
+
443
+ # filter_description = ' and '.join(
444
+ # ['{} {} {}'.format(readable[elem[0]], readable[elem[1]], elem[2])
445
+ # for elem in filter_list])
446
+ # filters = get_filters_from_strings(filter_list)
447
+ # # filt_geo_layout = make_usage_map(formatted_data, geo_col='zip',
448
+ # # time_col='modtime', filters=filters)
449
+ # # filt_map_script, filt_map_div, _ = get_embedable_usage_map(filt_geo_layout)
450
+ # # del filt_geo_layout
@@ -85,9 +85,9 @@ code: |
85
85
  xlsx_output.initialize(filename=proper_name + ".xlsx")
86
86
  writer = pd.ExcelWriter(xlsx_output.path(),
87
87
  engine='xlsxwriter',
88
- options={'remove_timezone': True})
88
+ engine_kwargs={'options': {'remove_timezone': True}})
89
89
  formatted_data.to_excel(writer, index=False)
90
- writer.save()
90
+ writer.close()
91
91
  xlsx_output.commit()
92
92
  del writer
93
93
  saved_data = True
@@ -0,0 +1,86 @@
1
+ // Sortable + summary helpers for Stats table
2
+ (function(){
3
+ // --- existing sorter logic ---
4
+ function parseValue(value, type){
5
+ if(type === 'number'){
6
+ var n = parseFloat(String(value).replace(/[^0-9.+-eE]/g, ''));
7
+ return isNaN(n) ? 0 : n;
8
+ }
9
+ if(type === 'date'){
10
+ var d = Date.parse(value);
11
+ return isNaN(d) ? 0 : d;
12
+ }
13
+ return String(value).toLowerCase();
14
+ }
15
+
16
+ function sortTable(table, colIndex, type, asc){
17
+ var tbody = table.tBodies[0];
18
+ if(!tbody) return;
19
+ var rows = Array.prototype.slice.call(tbody.querySelectorAll('tr'));
20
+ rows.sort(function(a,b){
21
+ var aText = (a.children[colIndex] && a.children[colIndex].textContent) || '';
22
+ var bText = (b.children[colIndex] && b.children[colIndex].textContent) || '';
23
+ var va = parseValue(aText.trim(), type);
24
+ var vb = parseValue(bText.trim(), type);
25
+ if(va < vb) return asc ? -1 : 1;
26
+ if(va > vb) return asc ? 1 : -1;
27
+ return 0;
28
+ });
29
+ var frag = document.createDocumentFragment();
30
+ rows.forEach(function(r){ frag.appendChild(r); });
31
+ tbody.appendChild(frag);
32
+ }
33
+
34
+ function makeSortable(table){
35
+ var ths = table.tHead ? table.tHead.querySelectorAll('th') : [];
36
+ if(!ths.length) return;
37
+ Array.prototype.forEach.call(ths, function(th, i){
38
+ var type = th.getAttribute('data-type') || 'string';
39
+ var asc = true;
40
+ th.style.cursor = 'pointer';
41
+ th.addEventListener('click', function(){
42
+ Array.prototype.forEach.call(th.parentNode.children, function(s){
43
+ if(s !== th) s.removeAttribute('data-sort');
44
+ });
45
+ asc = ! (th.getAttribute('data-sort') === 'asc');
46
+ th.setAttribute('data-sort', asc ? 'asc' : 'desc');
47
+ sortTable(table, i, type, asc);
48
+ });
49
+ });
50
+ }
51
+
52
+
53
+ function setupTabs(){
54
+ var snapTab = document.getElementById('snapshots-tab');
55
+ var sessTab = document.getElementById('sessions-tab');
56
+ var snapPanel = document.getElementById('snapshots-panel');
57
+ var sessPanel = document.getElementById('sessions-panel');
58
+ if(!snapTab || !sessTab || !snapPanel || !sessPanel) return;
59
+ if(!snapTab._daTabAttached){
60
+ snapTab._daTabAttached = true;
61
+ snapTab.addEventListener('click', function(e){
62
+ e.preventDefault();
63
+ snapTab.classList.add('active');
64
+ sessTab.classList.remove('active');
65
+ snapPanel.style.display = '';
66
+ sessPanel.style.display = 'none';
67
+ });
68
+ }
69
+ if(!sessTab._daTabAttached){
70
+ sessTab._daTabAttached = true;
71
+ sessTab.addEventListener('click', function(e){
72
+ e.preventDefault();
73
+ sessTab.classList.add('active');
74
+ snapTab.classList.remove('active');
75
+ sessPanel.style.display = '';
76
+ snapPanel.style.display = 'none';
77
+ });
78
+ }
79
+ }
80
+
81
+ $(document).on('daPageLoad', function(){
82
+ var tables = document.querySelectorAll('table.sortable');
83
+ Array.prototype.forEach.call(tables, makeSortable);
84
+ setupTabs();
85
+ });
86
+ })();
@@ -0,0 +1,8 @@
1
+ /* Summary table styles moved out of the template */
2
+ #summary-table { width: 100%; border-collapse: collapse; }
3
+ #summary-table th { background: #f8f9fa; padding: 8px; text-align: left; }
4
+ #summary-table td { padding: 8px; vertical-align: top; }
5
+ #summary-table th[data-sort="asc"]::after { content: " \25B2"; }
6
+ #summary-table th[data-sort="desc"]::after { content: " \25BC"; }
7
+ #summary-table th { cursor: pointer; }
8
+ .short-name { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; display: inline-block; max-width: 20ch; }
@@ -1,8 +1,17 @@
1
- from docassemble.webapp.jsonstore import read_answer_json, write_answer_json, JsonStorage, JsonDb
1
+ import random
2
2
  from docassemble.base.generate_key import random_alphanumeric
3
3
  from docassemble.base.functions import get_current_info
4
- from docassemble.base.util import variables_snapshot_connection
5
- import random
4
+ from docassemble.webapp.jsonstore import JsonStorage
5
+
6
+ try:
7
+ from docassemble.webapp.jsonstore import JsonDb
8
+ except ImportError:
9
+ # Will happen on 1.9.8 and up
10
+ from docassemble.webapp.jsonstore import db
11
+
12
+ JsonDb = db.session
13
+
14
+
6
15
  from .snapshot_statistics import *
7
16
 
8
17
  __all__ = ['get_column_values_list',
@@ -135,6 +144,6 @@ def save_random_records(number_of_records, title = '', seed_data_list = None, da
135
144
  JsonDb.add(new_entry)
136
145
  JsonDb.commit()
137
146
 
138
- def phrase(input:str = None) -> str:
147
+ def phrase(input:str) -> str:
139
148
  base = {'eq': 'on', 'gt': 'after', 'lt': 'before', 'between': 'between'}
140
149
  return base[input]
@@ -1,7 +1,15 @@
1
- from docassemble.webapp.jsonstore import read_answer_json, write_answer_json, JsonStorage, JsonDb
2
1
  from docassemble.base.generate_key import random_alphanumeric
3
2
  from docassemble.base.functions import get_current_info
4
3
  from docassemble.base.util import variables_snapshot_connection
4
+ from docassemble.webapp.jsonstore import JsonStorage
5
+
6
+ try:
7
+ from docassemble.webapp.jsonstore import JsonDb
8
+ except ImportError:
9
+ # Will happen on 1.9.8 and up
10
+ from docassemble.webapp.jsonstore import db
11
+
12
+ JsonDb = db.session
5
13
 
6
14
  __all__ = ['write_random_user', 'delete_data_from_interview']
7
15