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.
- docassemble_interviewstats-0.5.3/MANIFEST.in +18 -0
- {docassemble.InterviewStats-0.5.0/docassemble.InterviewStats.egg-info → docassemble_interviewstats-0.5.3}/PKG-INFO +15 -7
- {docassemble.InterviewStats-0.5.0 → docassemble_interviewstats-0.5.3}/README.md +2 -0
- docassemble_interviewstats-0.5.3/docassemble/InterviewStats/__init__.py +1 -0
- {docassemble.InterviewStats-0.5.0 → docassemble_interviewstats-0.5.3}/docassemble/InterviewStats/data/questions/data_report.yml +4 -4
- docassemble_interviewstats-0.5.3/docassemble/InterviewStats/data/questions/stats.yml +450 -0
- {docassemble.InterviewStats-0.5.0 → docassemble_interviewstats-0.5.3}/docassemble/InterviewStats/data/questions/subscribe.yml +2 -2
- docassemble_interviewstats-0.5.3/docassemble/InterviewStats/data/static/sortable.js +86 -0
- docassemble_interviewstats-0.5.3/docassemble/InterviewStats/data/static/summary.css +8 -0
- {docassemble.InterviewStats-0.5.0 → docassemble_interviewstats-0.5.3}/docassemble/InterviewStats/data_report.py +13 -4
- {docassemble.InterviewStats-0.5.0 → docassemble_interviewstats-0.5.3}/docassemble/InterviewStats/generate_test_data.py +9 -1
- {docassemble.InterviewStats-0.5.0 → docassemble_interviewstats-0.5.3}/docassemble/InterviewStats/snapshot_geography.py +5 -1
- docassemble_interviewstats-0.5.3/docassemble/InterviewStats/snapshot_statistics.py +282 -0
- {docassemble.InterviewStats-0.5.0 → docassemble_interviewstats-0.5.3/docassemble.InterviewStats.egg-info}/PKG-INFO +15 -7
- {docassemble.InterviewStats-0.5.0 → docassemble_interviewstats-0.5.3}/docassemble.InterviewStats.egg-info/SOURCES.txt +3 -5
- {docassemble.InterviewStats-0.5.0 → docassemble_interviewstats-0.5.3}/docassemble.InterviewStats.egg-info/requires.txt +1 -1
- {docassemble.InterviewStats-0.5.0 → docassemble_interviewstats-0.5.3}/docassemble.InterviewStats.egg-info/top_level.txt +2 -0
- docassemble_interviewstats-0.5.3/pyproject.toml +58 -0
- docassemble_interviewstats-0.5.3/setup.cfg +4 -0
- docassemble.InterviewStats-0.5.0/MANIFEST.in +0 -1
- docassemble.InterviewStats-0.5.0/docassemble/InterviewStats/__init__.py +0 -1
- docassemble.InterviewStats-0.5.0/docassemble/InterviewStats/data/questions/stats.yml +0 -318
- docassemble.InterviewStats-0.5.0/docassemble/InterviewStats/snapshot_statistics.py +0 -87
- docassemble.InterviewStats-0.5.0/docassemble/__init__.py +0 -5
- docassemble.InterviewStats-0.5.0/docassemble.InterviewStats.egg-info/namespace_packages.txt +0 -1
- docassemble.InterviewStats-0.5.0/docassemble.InterviewStats.egg-info/not-zip-safe +0 -1
- docassemble.InterviewStats-0.5.0/setup.cfg +0 -7
- docassemble.InterviewStats-0.5.0/setup.py +0 -60
- {docassemble.InterviewStats-0.5.0 → docassemble_interviewstats-0.5.3}/LICENSE +0 -0
- {docassemble.InterviewStats-0.5.0 → docassemble_interviewstats-0.5.3}/docassemble/InterviewStats/cache_geography.py +0 -0
- {docassemble.InterviewStats-0.5.0 → docassemble_interviewstats-0.5.3}/docassemble/InterviewStats/data/questions/test_create_snapshot_data.yml +0 -0
- {docassemble.InterviewStats-0.5.0 → docassemble_interviewstats-0.5.3}/docassemble/InterviewStats/data/questions/test_survey_data_generator.yml +0 -0
- {docassemble.InterviewStats-0.5.0 → docassemble_interviewstats-0.5.3}/docassemble/InterviewStats/data/sources/README.md +0 -0
- {docassemble.InterviewStats-0.5.0 → docassemble_interviewstats-0.5.3}/docassemble/InterviewStats/data/static/README.md +0 -0
- {docassemble.InterviewStats-0.5.0 → docassemble_interviewstats-0.5.3}/docassemble/InterviewStats/data/static/styles.css +0 -0
- {docassemble.InterviewStats-0.5.0 → docassemble_interviewstats-0.5.3}/docassemble/InterviewStats/data/templates/README.md +0 -0
- {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
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: docassemble.InterviewStats
|
|
3
|
-
Version: 0.5.
|
|
4
|
-
Summary: A docassemble
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
+
[](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
|
+
[](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
|
-
|
|
354
|
+
engine_kwargs={'options': {'remove_timezone': True}})
|
|
355
355
|
excel_formatted_data.to_excel(writer, index=False)
|
|
356
|
-
writer.
|
|
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
|
-
|
|
377
|
+
engine_kwargs={'options': {'remove_timezone': True}})
|
|
378
378
|
bg_data.to_excel(writer, index=False)
|
|
379
|
-
writer.
|
|
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
|
-
|
|
88
|
+
engine_kwargs={'options': {'remove_timezone': True}})
|
|
89
89
|
formatted_data.to_excel(writer, index=False)
|
|
90
|
-
writer.
|
|
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
|
-
|
|
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.
|
|
5
|
-
|
|
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
|
|
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
|
|