Skip to content

Commit fab6264

Browse files
committed
feat: integrate activity logging with admin candidate views
- Update get_all_candidates() to JOIN with candidate_activity_log table - Add activity statistics (query_executions, successful_queries, query_success_rate) to candidate detail summary - Replace challenge attempt history with comprehensive activity history display - Show all candidate queries including syntax errors with timestamps and status - Update admin candidate detail template to display activity_history data - Remove challenge-focused UI elements in favor of query-focused analytics
1 parent f619689 commit fab6264

File tree

2 files changed

+87
-58
lines changed

2 files changed

+87
-58
lines changed

models/users.py

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -170,19 +170,22 @@ def get_all_candidates():
170170
conn = get_user_db_connection()
171171
try:
172172
candidates = conn.execute('''
173-
SELECT DISTINCT u.username, u.created_at as registration_date,
173+
SELECT DISTINCT u.id, u.username, u.email, u.created_at as registration_date,
174174
COUNT(DISTINCT ca.challenge_id) as challenges_attempted,
175175
COUNT(DISTINCT CASE WHEN ca.is_correct = 1 THEN ca.challenge_id END) as challenges_completed,
176176
MAX(ca.score) as best_score,
177177
SUM(ca.score) as total_score,
178178
COUNT(ca.id) as total_attempts,
179179
AVG(ca.execution_time_ms) as avg_execution_time,
180180
SUM(ca.hints_used) as total_hints_used,
181-
MAX(ca.created_at) as last_activity
181+
MAX(COALESCE(cal.timestamp, ca.created_at)) as last_activity,
182+
COUNT(cal.id) as total_activity_events,
183+
COUNT(CASE WHEN cal.activity_type = 'query_executed' THEN 1 END) as query_attempts
182184
FROM users u
183185
LEFT JOIN challenge_attempts ca ON u.id = ca.user_id
184-
WHERE u.username != 'admin'
185-
GROUP BY u.id, u.username, u.created_at
186+
LEFT JOIN candidate_activity_log cal ON u.id = cal.user_id
187+
WHERE u.username != 'admin' AND u.is_admin != 1
188+
GROUP BY u.id, u.username, u.email, u.created_at
186189
ORDER BY last_activity DESC NULLS LAST
187190
''').fetchall()
188191

@@ -256,16 +259,31 @@ def get_candidate_detail(username):
256259
ORDER BY ca.created_at DESC
257260
''', (user_id,)).fetchall()
258261

262+
# Get candidate activity log (new system)
263+
activity_history = conn.execute('''
264+
SELECT id, activity_type, details, query_text, execution_time_ms,
265+
success, error_message, timestamp, ip_address
266+
FROM candidate_activity_log
267+
WHERE user_id = ?
268+
ORDER BY timestamp DESC
269+
LIMIT 100
270+
''', (user_id,)).fetchall()
271+
259272
# Calculate overall statistics
260273
total_challenges = len([p for p in challenge_progress if p['attempts'] > 0])
261274
completed_challenges = len([p for p in challenge_progress if p['completed']])
262275
total_score = sum(p['best_score'] or 0 for p in challenge_progress)
263276
max_possible_score = sum(p['max_score'] for p in challenge_progress)
264277

278+
# Calculate activity statistics
279+
query_activities = [a for a in activity_history if a['activity_type'] == 'query_executed']
280+
successful_queries = [a for a in query_activities if a['success']]
281+
265282
return {
266283
'username': username,
267284
'challenge_progress': [dict(p) for p in challenge_progress],
268285
'attempt_history': [dict(a) for a in attempt_history],
286+
'activity_history': [dict(a) for a in activity_history], # NEW: Include activity log
269287
'summary': {
270288
'total_challenges_attempted': total_challenges,
271289
'completed_challenges': completed_challenges,
@@ -275,7 +293,12 @@ def get_candidate_detail(username):
275293
'score_percentage': round(total_score / max_possible_score * 100, 1) if max_possible_score > 0 else 0,
276294
'total_attempts': len(attempt_history),
277295
'avg_execution_time': round(sum(a['execution_time_ms'] for a in attempt_history) / len(attempt_history), 1) if attempt_history else 0,
278-
'total_hints_used': sum(a['hints_used'] for a in attempt_history)
296+
'total_hints_used': sum(a['hints_used'] for a in attempt_history),
297+
# NEW: Activity-based statistics
298+
'total_activities': len(activity_history),
299+
'query_executions': len(query_activities),
300+
'successful_queries': len(successful_queries),
301+
'query_success_rate': round(len(successful_queries) / len(query_activities) * 100, 1) if query_activities else 0
279302
}
280303
}
281304
finally:

templates/admin/candidate_detail.html

Lines changed: 59 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -54,29 +54,30 @@ <h5 class="mb-0">
5454
</div>
5555
</div>
5656

57-
<!-- Attempt History -->
58-
<div class="card border-0 shadow-sm">
57+
<!-- Activity History -->
58+
<div class="card border-0 shadow-sm mb-4">
5959
<div class="card-header bg-light d-flex justify-content-between align-items-center">
6060
<h5 class="mb-0">
61-
<i class="fas fa-history me-2"></i>Query Attempt History
61+
<i class="fas fa-history me-2"></i>Query History & Activity Log
6262
</h5>
6363
<div>
64-
<button class="btn btn-sm btn-outline-primary" onclick="toggleQueryDetails()">
64+
<button class="btn btn-sm btn-outline-primary" onclick="toggleActivityDetails()">
6565
<i class="fas fa-code me-1"></i>Show/Hide Queries
6666
</button>
6767
</div>
6868
</div>
6969
<div class="card-body">
70-
<div id="attemptHistory">
70+
<div id="activityHistory">
7171
<div class="text-center">
7272
<div class="spinner-border text-primary" role="status">
7373
<span class="visually-hidden">Loading...</span>
7474
</div>
75-
<p class="mt-2 text-muted">Loading attempt history...</p>
75+
<p class="mt-2 text-muted">Loading activity history...</p>
7676
</div>
7777
</div>
7878
</div>
7979
</div>
80+
8081
</div>
8182
</div>
8283
{% endblock %}
@@ -99,7 +100,7 @@ <h5 class="mb-0">
99100
candidateData = data;
100101
displaySummary(data.summary);
101102
displayChallengeProgress(data.challenge_progress);
102-
displayAttemptHistory(data.attempt_history);
103+
displayActivityHistory(data.activity_history);
103104
})
104105
.catch(error => {
105106
console.error('Error loading candidate details:', error);
@@ -137,8 +138,8 @@ <h3 class="mb-1">${summary.total_score}</h3>
137138
<div class="col-md-3">
138139
<div class="card border-0 bg-warning text-white">
139140
<div class="card-body text-center">
140-
<h3 class="mb-1">${summary.total_attempts}</h3>
141-
<small>Total Attempts</small>
141+
<h3 class="mb-1">${summary.query_executions || 0}</h3>
142+
<small>Query Executions</small>
142143
<div class="mt-2">
143144
<small>${summary.avg_execution_time}ms avg time</small>
144145
</div>
@@ -148,10 +149,10 @@ <h3 class="mb-1">${summary.total_attempts}</h3>
148149
<div class="col-md-3">
149150
<div class="card border-0 bg-info text-white">
150151
<div class="card-body text-center">
151-
<h3 class="mb-1">${summary.total_hints_used}</h3>
152-
<small>Hints Used</small>
152+
<h3 class="mb-1">${summary.query_success_rate || 0}%</h3>
153+
<small>Query Success Rate</small>
153154
<div class="mt-2">
154-
<small>Help-seeking behavior</small>
155+
<small>${summary.successful_queries || 0}/${summary.query_executions || 0} successful</small>
155156
</div>
156157
</div>
157158
</div>
@@ -247,11 +248,11 @@ <h6 class="mb-0 text-${levelColor}">
247248
container.innerHTML = html;
248249
}
249250

250-
function displayAttemptHistory(attempts) {
251-
const container = document.getElementById('attemptHistory');
251+
function displayActivityHistory(activities) {
252+
const container = document.getElementById('activityHistory');
252253

253-
if (attempts.length === 0) {
254-
container.innerHTML = '<p class="text-muted text-center">No attempt history available</p>';
254+
if (!activities || activities.length === 0) {
255+
container.innerHTML = '<p class="text-muted text-center">No activity history available</p>';
255256
return;
256257
}
257258

@@ -261,23 +262,33 @@ <h6 class="mb-0 text-${levelColor}">
261262
<thead>
262263
<tr>
263264
<th>Timestamp</th>
264-
<th>Challenge</th>
265-
<th>Result</th>
265+
<th>Activity</th>
266+
<th>Status</th>
266267
<th>Performance</th>
267-
<th>Query</th>
268+
<th>Details</th>
268269
</tr>
269270
</thead>
270271
<tbody>
271272
`;
272273

273-
attempts.forEach((attempt, index) => {
274-
const timestamp = new Date(attempt.created_at).toLocaleString();
275-
const resultIcon = attempt.is_correct ?
274+
activities.forEach((activity, index) => {
275+
const timestamp = new Date(activity.timestamp).toLocaleString();
276+
const activityTypeIcons = {
277+
'query_executed': 'fas fa-play-circle',
278+
'candidate_login': 'fas fa-sign-in-alt',
279+
'page_view': 'fas fa-eye',
280+
'impersonation_started': 'fas fa-user-secret',
281+
'impersonation_ended': 'fas fa-user-times'
282+
};
283+
const icon = activityTypeIcons[activity.activity_type] || 'fas fa-circle';
284+
285+
const statusIcon = activity.success === 1 ?
276286
'<i class="fas fa-check-circle text-success"></i>' :
277-
'<i class="fas fa-times-circle text-danger"></i>';
287+
activity.success === 0 ?
288+
'<i class="fas fa-times-circle text-danger"></i>' :
289+
'<i class="fas fa-info-circle text-info"></i>';
278290

279-
const levelColors = {1: 'success', 2: 'warning', 3: 'danger', 4: 'dark'};
280-
const levelColor = levelColors[attempt.difficulty_level] || 'primary';
291+
const hasQuery = activity.query_text && activity.query_text.trim();
281292

282293
html += `
283294
<tr>
@@ -286,39 +297,34 @@ <h6 class="mb-0 text-${levelColor}">
286297
</td>
287298
<td>
288299
<div>
289-
${attempt.challenge_title}
290-
<br><span class="badge bg-${levelColor}">Level ${attempt.difficulty_level}</span>
300+
<i class="${icon} me-2"></i>
301+
${activity.activity_type.replace('_', ' ').replace(/\b\w/g, l => l.toUpperCase())}
291302
</div>
292303
</td>
293304
<td>
294-
<div>
295-
${resultIcon}
296-
<strong>${attempt.score || 0} pts</strong>
297-
<br><small class="text-muted">
298-
${attempt.result_count || 0} rows returned
299-
${attempt.expected_result_count ? ` (expected ~${attempt.expected_result_count})` : ''}
300-
</small>
301-
</div>
305+
${statusIcon}
306+
${activity.error_message ? `<br><small class="text-danger">${activity.error_message}</small>` : ''}
302307
</td>
303308
<td>
304-
<div>
305-
<small class="text-muted">
306-
${Math.round(attempt.execution_time_ms || 0)}ms
307-
${attempt.hints_used > 0 ? `<br>${attempt.hints_used} hints used` : ''}
308-
</small>
309-
</div>
309+
${activity.execution_time_ms ? `<small class="text-muted">${Math.round(activity.execution_time_ms)}ms</small>` : ''}
310310
</td>
311311
<td>
312-
<button class="btn btn-sm btn-outline-info"
313-
type="button"
314-
data-bs-toggle="collapse"
315-
data-bs-target="#query-${index}">
316-
<i class="fas fa-code me-1"></i>View Query
317-
</button>
318-
<div class="collapse mt-2" id="query-${index}">
319-
<div class="card card-body bg-light">
320-
<pre class="mb-0" style="font-size: 0.8rem;"><code>${attempt.query_text}</code></pre>
321-
</div>
312+
<div>
313+
${activity.details ? `<small>${activity.details}</small>` : ''}
314+
${hasQuery ? `
315+
<br>
316+
<button class="btn btn-sm btn-outline-info mt-1"
317+
type="button"
318+
data-bs-toggle="collapse"
319+
data-bs-target="#activity-query-${index}">
320+
<i class="fas fa-code me-1"></i>View Query
321+
</button>
322+
<div class="collapse mt-2" id="activity-query-${index}">
323+
<div class="card card-body bg-light">
324+
<pre class="mb-0" style="font-size: 0.8rem;"><code>${activity.query_text}</code></pre>
325+
</div>
326+
</div>
327+
` : ''}
322328
</div>
323329
</td>
324330
</tr>
@@ -329,9 +335,9 @@ <h6 class="mb-0 text-${levelColor}">
329335
container.innerHTML = html;
330336
}
331337

332-
function toggleQueryDetails() {
338+
function toggleActivityDetails() {
333339
showQueries = !showQueries;
334-
const collapses = document.querySelectorAll('[id^="query-"]');
340+
const collapses = document.querySelectorAll('[id^="activity-query-"]');
335341

336342
collapses.forEach(collapse => {
337343
if (showQueries) {

0 commit comments

Comments
 (0)