Skip to content

Commit f2feb3e

Browse files
Merge pull request #789 from geekygirlsarah/main
Add statistics page
2 parents 782559a + 3a6d09a commit f2feb3e

File tree

9 files changed

+622
-31
lines changed

9 files changed

+622
-31
lines changed
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Generated by Django 4.2.27 on 2025-12-28 00:20
2+
3+
from django.db import migrations, models
4+
import django.utils.timezone
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
('web', '0002_lookupdata_version1_lookupdata_version2'),
11+
]
12+
13+
operations = [
14+
migrations.AddField(
15+
model_name='lookupdata',
16+
name='date_time',
17+
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
18+
preserve_default=False,
19+
),
20+
]
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Generated by Django 4.2.27 on 2025-12-28 00:33
2+
3+
from django.db import migrations, models
4+
import django.db.models.deletion
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
('web', '0003_lookupdata_date_time'),
11+
]
12+
13+
operations = [
14+
migrations.CreateModel(
15+
name='MissingLookup',
16+
fields=[
17+
('id', models.BigAutoField(primary_key=True, serialize=False)),
18+
('date_time', models.DateTimeField(auto_now_add=True)),
19+
('item_type', models.CharField(max_length=20)),
20+
('item_value', models.CharField(max_length=100)),
21+
('language_context', models.CharField(blank=True, max_length=50, null=True)),
22+
('site_visit', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='web.sitevisit')),
23+
],
24+
),
25+
]

web/models.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -377,10 +377,19 @@ class SiteVisit(models.Model):
377377

378378
class LookupData(models.Model):
379379
id = models.BigAutoField(primary_key=True)
380-
date_time = models.DateTimeField
380+
date_time = models.DateTimeField(auto_now_add=True)
381381
language1 = models.CharField(max_length=50)
382382
version1 = models.CharField(max_length=20, default='')
383383
language2 = models.CharField(max_length=50)
384384
version2 = models.CharField(max_length=20, default='')
385385
structure = models.CharField(max_length=50)
386386
site_visit = models.ForeignKey(SiteVisit, on_delete=models.CASCADE)
387+
388+
389+
class MissingLookup(models.Model):
390+
id = models.BigAutoField(primary_key=True)
391+
date_time = models.DateTimeField(auto_now_add=True)
392+
item_type = models.CharField(max_length=20) # 'language', 'structure', 'concept'
393+
item_value = models.CharField(max_length=100)
394+
language_context = models.CharField(max_length=50, blank=True, null=True)
395+
site_visit = models.ForeignKey(SiteVisit, on_delete=models.CASCADE)

web/static/js/contributors.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,20 @@ document.addEventListener("DOMContentLoaded", function () {
2626
contributorsRequest.onerror = function () {
2727
document.querySelector("#contributors").innerHTML = "multiple";
2828
};
29+
30+
var repoRequest = new XMLHttpRequest();
31+
repoRequest.open(
32+
"GET",
33+
"https://api.github.com/repos/codethesaurus/codethesaur.us"
34+
);
35+
repoRequest.send();
36+
37+
repoRequest.onload = function () {
38+
if (repoRequest.status === 200) {
39+
let repoData = JSON.parse(repoRequest.responseText);
40+
let lastUpdate = new Date(repoData.pushed_at);
41+
let options = { year: 'numeric', month: 'long', day: 'numeric' };
42+
document.querySelector("#last-update").innerHTML = lastUpdate.toLocaleDateString(undefined, options);
43+
}
44+
};
2945
});

web/templates/base.html

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@
5555
<li class="nav-item">
5656
<a class="nav-link" href="/about">About</a>
5757
</li>
58+
<li class="nav-item">
59+
<a class="nav-link" href="/statistics">Statistics</a>
60+
</li>
5861
<li class="nav-item">
5962
<a class="nav-link" href="https://docs.codethesaur.us">Docs</a>
6063
</li>
@@ -79,6 +82,7 @@
7982
<div class="row">
8083
<div class="col-8 justify-content-start">
8184
<p>Made with &#x2764; by Sarah Withee and <a href="https://github.com/codethesaurus/codethesaur.us/graphs/contributors" target="_blank" rel="noreferrer"><span id="contributors"></span> contributors</a>.</p> <!-- x2764 the heart emoji code -->
85+
<p>Last GitHub update: <span id="last-update"></span></p>
8286
<p>Want to help out? Check the project out on <a href="http://github.com/codethesaurus/" target="_blank" rel="noopener">GitHub</a>.</p>
8387
</div>
8488
<div class="col-4 order-sm-2 d-flex justify-content-end align-self-center">

web/templates/statistics.html

Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
{% extends 'base.html' %}
2+
3+
{% block content %}
4+
<div class="container mt-4">
5+
<div class="row">
6+
<div class="col-12">
7+
<h1>Site Statistics</h1>
8+
<p class="lead">Insights into how developers are using Code Thesaurus.</p>
9+
</div>
10+
</div>
11+
12+
<div class="row mt-4">
13+
<div class="col-md-4">
14+
<div class="card mb-4 shadow-sm h-100">
15+
<div class="card-header bg-primary text-white">
16+
<h5 class="mb-0"><i class="fas fa-users"></i> Overall Activity</h5>
17+
</div>
18+
<div class="card-body">
19+
<div class="row text-center mb-3">
20+
<div class="col-6">
21+
<h2 class="display-4">{{ total_visits }}</h2>
22+
<p class="text-muted">Total Visits</p>
23+
</div>
24+
<div class="col-6">
25+
<h2 class="display-4">{{ total_lookups }}</h2>
26+
<p class="text-muted">Total Lookups</p>
27+
</div>
28+
</div>
29+
<hr>
30+
<div class="row text-center">
31+
<div class="col-6 border-end">
32+
<h4 class="mb-0">{{ unique_comparisons_count }}</h4>
33+
<small class="text-muted">Unique Lang Comparisons</small>
34+
</div>
35+
<div class="col-6">
36+
<h4 class="mb-0">{{ unique_structures_count }}</h4>
37+
<small class="text-muted">Unique Category Lookups</small>
38+
</div>
39+
</div>
40+
</div>
41+
</div>
42+
</div>
43+
<div class="col-md-8">
44+
<div class="card mb-4 shadow-sm h-100">
45+
<div class="card-header bg-success text-white">
46+
<h5 class="mb-0"><i class="fas fa-chart-bar"></i> Language Popularity</h5>
47+
</div>
48+
<div class="card-body">
49+
<canvas id="languagesChart" height="100"></canvas>
50+
</div>
51+
</div>
52+
</div>
53+
</div>
54+
55+
<div class="row">
56+
<div class="col-12">
57+
<div class="card mb-4 shadow-sm">
58+
<div class="card-header bg-dark text-white">
59+
<h5 class="mb-0"><i class="fas fa-chart-line"></i> Most Popular Specific Concept Lookups</h5>
60+
</div>
61+
<div class="card-body">
62+
<p class="text-muted small">Comparing how often specific concepts are looked up for each language (e.g., "Javascript functions").</p>
63+
<canvas id="conceptLangsChart" height="80"></canvas>
64+
</div>
65+
</div>
66+
</div>
67+
</div>
68+
69+
<div class="row">
70+
<div class="col-12">
71+
<div class="card mb-4 shadow-sm">
72+
<div class="card-header bg-secondary text-white">
73+
<h5 class="mb-0"><i class="fas fa-history"></i> Recent Lookups</h5>
74+
</div>
75+
<div class="card-body">
76+
{% if recent_lookups %}
77+
<table class="table table-hover table-sm">
78+
<thead>
79+
<tr>
80+
<th>Time</th>
81+
<th>Language 1</th>
82+
<th>Language 2</th>
83+
<th>Concept</th>
84+
</tr>
85+
</thead>
86+
<tbody>
87+
{% for lookup in recent_lookups %}
88+
<tr>
89+
<td>{{ lookup.date_time|date:"Y-m-d H:i" }}</td>
90+
<td>{{ lookup.lang1 }}</td>
91+
<td>{% if lookup.lang2 %}{{ lookup.lang2 }}{% else %}<span class="text-muted">-</span>{% endif %}</td>
92+
<td>{{ lookup.structure }}</td>
93+
</tr>
94+
{% endfor %}
95+
</tbody>
96+
</table>
97+
{% else %}
98+
<p class="text-center">No recent lookups recorded.</p>
99+
{% endif %}
100+
</div>
101+
</div>
102+
</div>
103+
</div>
104+
105+
<div class="row">
106+
<div class="col-12">
107+
<div class="card mb-4 shadow-sm">
108+
<div class="card-header bg-warning text-dark">
109+
<h5 class="mb-0"><i class="fas fa-exchange-alt"></i> Top Comparisons</h5>
110+
</div>
111+
<div class="card-body">
112+
{% if popular_comparisons %}
113+
<table class="table table-hover table-sm">
114+
<thead>
115+
<tr>
116+
<th>Language 1</th>
117+
<th>Language 2</th>
118+
<th class="text-end">Count</th>
119+
</tr>
120+
</thead>
121+
<tbody>
122+
{% for comp in popular_comparisons %}
123+
<tr>
124+
<td>{{ comp.lang1 }}</td>
125+
<td>{{ comp.lang2 }}</td>
126+
<td class="text-end">{{ comp.count }}</td>
127+
</tr>
128+
{% endfor %}
129+
</tbody>
130+
</table>
131+
{% else %}
132+
<p class="text-center">No comparisons made yet.</p>
133+
{% endif %}
134+
</div>
135+
</div>
136+
</div>
137+
</div>
138+
139+
<div class="row">
140+
<div class="col-12">
141+
<div class="card mb-4 shadow-sm border-danger">
142+
<div class="card-header bg-danger text-white">
143+
<h5 class="mb-0"><i class="fas fa-exclamation-triangle"></i> Most Requested Missing Items</h5>
144+
</div>
145+
<div class="card-body">
146+
<p class="text-muted small">Tracking languages, structures, or concepts that were requested but are currently missing from the site.</p>
147+
{% if missing_items %}
148+
<table class="table table-hover table-sm">
149+
<thead>
150+
<tr>
151+
<th>Item</th>
152+
<th class="text-end">Requests</th>
153+
</tr>
154+
</thead>
155+
<tbody>
156+
{% for item in missing_items %}
157+
<tr>
158+
<td>
159+
{% if item.type == 'language' %}
160+
<span class="badge bg-primary">Language</span>
161+
{% elif item.type == 'structure' %}
162+
<span class="badge bg-info">Structure</span>
163+
{% else %}
164+
<span class="badge bg-secondary">Concept</span>
165+
{% endif %}
166+
{{ item.label }}
167+
</td>
168+
<td class="text-end">{{ item.count }}</td>
169+
</tr>
170+
{% endfor %}
171+
</tbody>
172+
</table>
173+
{% else %}
174+
<p class="text-center">No missing items requested yet.</p>
175+
{% endif %}
176+
</div>
177+
</div>
178+
</div>
179+
</div>
180+
181+
<div class="row">
182+
<div class="col-md-6">
183+
<div class="card mb-4 shadow-sm">
184+
<div class="card-header bg-info text-white">
185+
<h5 class="mb-0"><i class="fas fa-chart-pie"></i> Category Popularity</h5>
186+
</div>
187+
<div class="card-body">
188+
<canvas id="structuresChart"></canvas>
189+
</div>
190+
</div>
191+
</div>
192+
</div>
193+
</div>
194+
195+
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
196+
<script>
197+
document.addEventListener('DOMContentLoaded', function() {
198+
const langData = {{ popular_languages_json|safe }};
199+
const structData = {{ popular_structures_json|safe }};
200+
const conceptLangData = {{ popular_concept_langs_json|safe }};
201+
202+
if (langData.length > 0) {
203+
new Chart(document.getElementById('languagesChart'), {
204+
type: 'bar',
205+
data: {
206+
labels: langData.map(item => item.name),
207+
datasets: [{
208+
label: 'Lookups',
209+
data: langData.map(item => item.count),
210+
backgroundColor: 'rgba(40, 167, 69, 0.7)',
211+
borderColor: 'rgba(40, 167, 69, 1)',
212+
borderWidth: 1
213+
}]
214+
},
215+
options: {
216+
indexAxis: 'y',
217+
scales: {
218+
x: { beginAtZero: true }
219+
},
220+
plugins: {
221+
legend: { display: false }
222+
}
223+
}
224+
});
225+
}
226+
227+
if (structData.length > 0) {
228+
new Chart(document.getElementById('structuresChart'), {
229+
type: 'doughnut',
230+
data: {
231+
labels: structData.map(item => item.name),
232+
datasets: [{
233+
data: structData.map(item => item.count),
234+
backgroundColor: [
235+
'#17a2b8', '#28a745', '#ffc107', '#dc3545', '#007bff',
236+
'#6610f2', '#e83e8c', '#fd7e14', '#20c997', '#6c757d'
237+
]
238+
}]
239+
},
240+
options: {
241+
plugins: {
242+
legend: { position: 'right' }
243+
}
244+
}
245+
});
246+
}
247+
248+
if (conceptLangData.length > 0) {
249+
new Chart(document.getElementById('conceptLangsChart'), {
250+
type: 'bar',
251+
data: {
252+
labels: conceptLangData.map(item => item.label),
253+
datasets: [{
254+
label: 'Lookups',
255+
data: conceptLangData.map(item => item.count),
256+
backgroundColor: 'rgba(52, 58, 64, 0.7)',
257+
borderColor: 'rgba(52, 58, 64, 1)',
258+
borderWidth: 1
259+
}]
260+
},
261+
options: {
262+
scales: {
263+
y: { beginAtZero: true }
264+
},
265+
plugins: {
266+
legend: { display: false }
267+
}
268+
}
269+
});
270+
}
271+
});
272+
</script>
273+
{% endblock %}

0 commit comments

Comments
 (0)