thadillo Claude commited on
Commit
f037336
·
1 Parent(s): df450f1

Enable sentence-level view for map and contributions

Browse files

Changes:
- Dashboard now changes map and contributions list based on view mode
- Sentence-level view shows individual sentences with inherited locations
- Map displays sentence-level markers when in sentence mode
- Contributions list shows sentences instead of submissions
- PDF export respects view mode and exports correct data

Features:
1. Sentence-level map: Shows each sentence as a marker (inherits parent location)
2. Sentence-level contributions: Lists individual sentences by category
3. Dynamic view switching: All dashboard sections update based on mode
4. PDF export: Generates PDF matching selected view mode

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

app/routes/admin.py CHANGED
@@ -118,17 +118,22 @@ def dashboard():
118
  # Get view mode from query param ('submissions' or 'sentences')
119
  view_mode = request.args.get('mode', 'submissions')
120
 
121
- submissions = Submission.query.filter(Submission.category != None).all()
122
-
123
  # Contributor stats (unchanged - always submission-based)
124
  contributor_stats = db.session.query(
125
  Submission.contributor_type,
126
  db.func.count(Submission.id)
127
  ).group_by(Submission.contributor_type).all()
128
 
129
- # Category stats - MODE DEPENDENT
130
  if view_mode == 'sentences':
131
- # Sentence-based aggregation
 
 
 
 
 
 
 
132
  category_stats = db.session.query(
133
  SubmissionSentence.category,
134
  db.func.count(SubmissionSentence.id)
@@ -146,8 +151,43 @@ def dashboard():
146
  Submission.contributor_type == ctype['value']
147
  ).scalar()
148
  breakdown[cat][ctype['value']] = count
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
149
  else:
150
- # Submission-based aggregation (backward compatible)
 
 
 
 
 
151
  category_stats = db.session.query(
152
  Submission.category,
153
  db.func.count(Submission.id)
@@ -164,18 +204,21 @@ def dashboard():
164
  ).count()
165
  breakdown[cat][ctype['value']] = count
166
 
167
- # Geotagged submissions
168
- geotagged_submissions = Submission.query.filter(
169
- Submission.latitude != None,
170
- Submission.longitude != None,
171
- Submission.category != None
172
- ).all()
 
 
 
173
 
174
  return render_template('admin/dashboard.html',
175
- submissions=submissions,
176
  contributor_stats=contributor_stats,
177
  category_stats=category_stats,
178
- geotagged_submissions=geotagged_submissions,
179
  categories=CATEGORIES,
180
  contributor_types=CONTRIBUTOR_TYPES,
181
  breakdown=breakdown,
@@ -184,22 +227,27 @@ def dashboard():
184
  @bp.route('/dashboard/export-pdf')
185
  @admin_required
186
  def export_dashboard_pdf():
187
- """Export dashboard data as PDF"""
188
  try:
189
  # Get view mode
190
  view_mode = request.args.get('mode', 'submissions')
191
 
192
- # Gather same data as dashboard
193
- submissions = Submission.query.filter(Submission.category != None).all()
194
-
195
  # Contributor stats
196
  contributor_stats = db.session.query(
197
  Submission.contributor_type,
198
  db.func.count(Submission.id)
199
  ).group_by(Submission.contributor_type).all()
200
 
201
- # Category stats - mode dependent
202
  if view_mode == 'sentences':
 
 
 
 
 
 
 
 
203
  category_stats = db.session.query(
204
  SubmissionSentence.category,
205
  db.func.count(SubmissionSentence.id)
@@ -217,12 +265,48 @@ def export_dashboard_pdf():
217
  Submission.contributor_type == ctype['value']
218
  ).scalar()
219
  breakdown[cat][ctype['value']] = count
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
220
  else:
 
 
 
 
 
 
221
  category_stats = db.session.query(
222
  Submission.category,
223
  db.func.count(Submission.id)
224
  ).filter(Submission.category != None).group_by(Submission.category).all()
225
 
 
226
  breakdown = {}
227
  for cat in CATEGORIES:
228
  breakdown[cat] = {}
@@ -233,20 +317,23 @@ def export_dashboard_pdf():
233
  ).count()
234
  breakdown[cat][ctype['value']] = count
235
 
236
- # Geotagged submissions
237
- geotagged_submissions = Submission.query.filter(
238
- Submission.latitude != None,
239
- Submission.longitude != None,
240
- Submission.category != None
241
- ).all()
 
 
 
242
 
243
  # Prepare data for PDF
244
  pdf_data = {
245
- 'submissions': submissions,
246
  'category_stats': category_stats,
247
  'contributor_stats': contributor_stats,
248
  'breakdown': breakdown,
249
- 'geotagged_submissions': geotagged_submissions,
250
  'view_mode': view_mode,
251
  'categories': CATEGORIES,
252
  'contributor_types': CONTRIBUTOR_TYPES
 
118
  # Get view mode from query param ('submissions' or 'sentences')
119
  view_mode = request.args.get('mode', 'submissions')
120
 
 
 
121
  # Contributor stats (unchanged - always submission-based)
122
  contributor_stats = db.session.query(
123
  Submission.contributor_type,
124
  db.func.count(Submission.id)
125
  ).group_by(Submission.contributor_type).all()
126
 
127
+ # MODE DEPENDENT: Data changes based on sentence vs submission view
128
  if view_mode == 'sentences':
129
+ # SENTENCE-LEVEL VIEW
130
+
131
+ # Get all sentences with categories
132
+ sentences = SubmissionSentence.query.filter(
133
+ SubmissionSentence.category != None
134
+ ).all()
135
+
136
+ # Category stats
137
  category_stats = db.session.query(
138
  SubmissionSentence.category,
139
  db.func.count(SubmissionSentence.id)
 
151
  Submission.contributor_type == ctype['value']
152
  ).scalar()
153
  breakdown[cat][ctype['value']] = count
154
+
155
+ # Geotagged sentences (inherit location from parent submission)
156
+ geotagged_items = db.session.query(SubmissionSentence, Submission).join(
157
+ Submission
158
+ ).filter(
159
+ Submission.latitude != None,
160
+ Submission.longitude != None,
161
+ SubmissionSentence.category != None
162
+ ).all()
163
+
164
+ # Create sentence objects with location data
165
+ geotagged_data = []
166
+ for sentence, submission in geotagged_items:
167
+ # Create a pseudo-object that has both sentence and location data
168
+ class SentenceWithLocation:
169
+ def __init__(self, sentence, submission):
170
+ self.id = sentence.id
171
+ self.text = sentence.text
172
+ self.category = sentence.category
173
+ self.latitude = submission.latitude
174
+ self.longitude = submission.longitude
175
+ self.contributor_type = submission.contributor_type
176
+ self.timestamp = submission.timestamp
177
+ self.message = sentence.text # For compatibility
178
+
179
+ geotagged_data.append(SentenceWithLocation(sentence, submission))
180
+
181
+ # Items for contributions list (sentences)
182
+ items_by_category = sentences
183
+
184
  else:
185
+ # SUBMISSION-LEVEL VIEW (default)
186
+
187
+ # Get all submissions with categories
188
+ submissions = Submission.query.filter(Submission.category != None).all()
189
+
190
+ # Category stats
191
  category_stats = db.session.query(
192
  Submission.category,
193
  db.func.count(Submission.id)
 
204
  ).count()
205
  breakdown[cat][ctype['value']] = count
206
 
207
+ # Geotagged submissions
208
+ geotagged_data = Submission.query.filter(
209
+ Submission.latitude != None,
210
+ Submission.longitude != None,
211
+ Submission.category != None
212
+ ).all()
213
+
214
+ # Items for contributions list (submissions)
215
+ items_by_category = submissions
216
 
217
  return render_template('admin/dashboard.html',
218
+ items=items_by_category,
219
  contributor_stats=contributor_stats,
220
  category_stats=category_stats,
221
+ geotagged_items=geotagged_data,
222
  categories=CATEGORIES,
223
  contributor_types=CONTRIBUTOR_TYPES,
224
  breakdown=breakdown,
 
227
  @bp.route('/dashboard/export-pdf')
228
  @admin_required
229
  def export_dashboard_pdf():
230
+ """Export dashboard data as PDF based on view mode"""
231
  try:
232
  # Get view mode
233
  view_mode = request.args.get('mode', 'submissions')
234
 
 
 
 
235
  # Contributor stats
236
  contributor_stats = db.session.query(
237
  Submission.contributor_type,
238
  db.func.count(Submission.id)
239
  ).group_by(Submission.contributor_type).all()
240
 
241
+ # MODE DEPENDENT: Same logic as dashboard
242
  if view_mode == 'sentences':
243
+ # SENTENCE-LEVEL VIEW
244
+
245
+ # Get all sentences with categories
246
+ sentences = SubmissionSentence.query.filter(
247
+ SubmissionSentence.category != None
248
+ ).all()
249
+
250
+ # Category stats
251
  category_stats = db.session.query(
252
  SubmissionSentence.category,
253
  db.func.count(SubmissionSentence.id)
 
265
  Submission.contributor_type == ctype['value']
266
  ).scalar()
267
  breakdown[cat][ctype['value']] = count
268
+
269
+ # Geotagged sentences (inherit location from parent submission)
270
+ geotagged_items = db.session.query(SubmissionSentence, Submission).join(
271
+ Submission
272
+ ).filter(
273
+ Submission.latitude != None,
274
+ Submission.longitude != None,
275
+ SubmissionSentence.category != None
276
+ ).all()
277
+
278
+ # Create sentence objects with location data
279
+ geotagged_data = []
280
+ for sentence, submission in geotagged_items:
281
+ class SentenceWithLocation:
282
+ def __init__(self, sentence, submission):
283
+ self.id = sentence.id
284
+ self.text = sentence.text
285
+ self.category = sentence.category
286
+ self.latitude = submission.latitude
287
+ self.longitude = submission.longitude
288
+ self.contributor_type = submission.contributor_type
289
+ self.timestamp = submission.timestamp
290
+ self.message = sentence.text
291
+
292
+ geotagged_data.append(SentenceWithLocation(sentence, submission))
293
+
294
+ # Items for contributions list
295
+ items_list = sentences
296
+
297
  else:
298
+ # SUBMISSION-LEVEL VIEW
299
+
300
+ # Get all submissions with categories
301
+ submissions = Submission.query.filter(Submission.category != None).all()
302
+
303
+ # Category stats
304
  category_stats = db.session.query(
305
  Submission.category,
306
  db.func.count(Submission.id)
307
  ).filter(Submission.category != None).group_by(Submission.category).all()
308
 
309
+ # Breakdown by contributor
310
  breakdown = {}
311
  for cat in CATEGORIES:
312
  breakdown[cat] = {}
 
317
  ).count()
318
  breakdown[cat][ctype['value']] = count
319
 
320
+ # Geotagged submissions
321
+ geotagged_data = Submission.query.filter(
322
+ Submission.latitude != None,
323
+ Submission.longitude != None,
324
+ Submission.category != None
325
+ ).all()
326
+
327
+ # Items for contributions list
328
+ items_list = submissions
329
 
330
  # Prepare data for PDF
331
  pdf_data = {
332
+ 'submissions': items_list, # Can be sentences or submissions
333
  'category_stats': category_stats,
334
  'contributor_stats': contributor_stats,
335
  'breakdown': breakdown,
336
+ 'geotagged_submissions': geotagged_data,
337
  'view_mode': view_mode,
338
  'categories': CATEGORIES,
339
  'contributor_types': CONTRIBUTOR_TYPES
app/templates/admin/dashboard.html CHANGED
@@ -61,11 +61,11 @@
61
  </div>
62
  </div>
63
 
64
- {% if geotagged_submissions %}
65
  <div class="card shadow-sm mb-4">
66
  <div class="card-body">
67
  <h5 class="card-title mb-3">
68
- Geographic Distribution ({{ geotagged_submissions|length }} geotagged)
69
  </h5>
70
  <div id="dashboardMap" class="dashboard-map-container border rounded"></div>
71
  </div>
@@ -76,14 +76,14 @@
76
  <div class="card-body">
77
  <h5 class="card-title mb-4">Contributions by Category</h5>
78
  {% for category in categories %}
79
- {% set category_submissions = submissions|selectattr('category', 'equalto', category)|list %}
80
- {% if category_submissions %}
81
  <div class="mb-4">
82
  <h6 class="border-bottom pb-2" style="border-color: {{ get_category_color(category) }}!important; border-width: 2px!important;">
83
  <span class="badge" style="background-color: {{ get_category_color(category) }};">{{ category }}</span>
84
- <small class="text-muted">({{ category_submissions|length }} contribution{{ 's' if category_submissions|length != 1 else '' }})</small>
85
  </h6>
86
- {% for sub in category_submissions %}
87
  <div class="border-start border-3 ps-3 mb-3" style="border-color: {{ get_category_color(category) }}!important;">
88
  <div class="d-flex justify-content-between align-items-start mb-1">
89
  <small class="text-muted text-capitalize">{{ sub.contributor_type }}</small>
@@ -199,7 +199,7 @@ document.addEventListener('DOMContentLoaded', function() {
199
  }
200
  });
201
 
202
- {% if geotagged_submissions %}
203
  // Dashboard Map
204
  const dashMap = L.map('dashboardMap').setView([0, 0], 2);
205
 
@@ -209,7 +209,7 @@ document.addEventListener('DOMContentLoaded', function() {
209
 
210
  const bounds = [];
211
 
212
- {% for sub in geotagged_submissions %}
213
  {
214
  const color = categoryColors['{{ sub.category }}'] || '#6b7280';
215
  const customIcon = L.divIcon({
 
61
  </div>
62
  </div>
63
 
64
+ {% if geotagged_items %}
65
  <div class="card shadow-sm mb-4">
66
  <div class="card-body">
67
  <h5 class="card-title mb-3">
68
+ Geographic Distribution ({{ geotagged_items|length }} geotagged)
69
  </h5>
70
  <div id="dashboardMap" class="dashboard-map-container border rounded"></div>
71
  </div>
 
76
  <div class="card-body">
77
  <h5 class="card-title mb-4">Contributions by Category</h5>
78
  {% for category in categories %}
79
+ {% set category_items = items|selectattr('category', 'equalto', category)|list %}
80
+ {% if category_items %}
81
  <div class="mb-4">
82
  <h6 class="border-bottom pb-2" style="border-color: {{ get_category_color(category) }}!important; border-width: 2px!important;">
83
  <span class="badge" style="background-color: {{ get_category_color(category) }};">{{ category }}</span>
84
+ <small class="text-muted">({{ category_items|length }} contribution{{ 's' if category_items|length != 1 else '' }})</small>
85
  </h6>
86
+ {% for sub in category_items %}
87
  <div class="border-start border-3 ps-3 mb-3" style="border-color: {{ get_category_color(category) }}!important;">
88
  <div class="d-flex justify-content-between align-items-start mb-1">
89
  <small class="text-muted text-capitalize">{{ sub.contributor_type }}</small>
 
199
  }
200
  });
201
 
202
+ {% if geotagged_items %}
203
  // Dashboard Map
204
  const dashMap = L.map('dashboardMap').setView([0, 0], 2);
205
 
 
209
 
210
  const bounds = [];
211
 
212
+ {% for sub in geotagged_items %}
213
  {
214
  const color = categoryColors['{{ sub.category }}'] || '#6b7280';
215
  const customIcon = L.divIcon({