Skip to content

Commit 463ba03

Browse files
committed
Improves works list with pagination and stats
Enhances the works list page with pagination, allowing users to browse publications in manageable chunks. Adds user-selectable page size with configurable min/max limits. Displays cached publication statistics, including total works, published works, and metadata completeness. Updates the Optimap version to 0.8.0.
1 parent e4ebd26 commit 463ba03

File tree

9 files changed

+762
-32
lines changed

9 files changed

+762
-32
lines changed

optimap/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
__version__ = "0.7.0"
1+
__version__ = "0.8.0"
22
VERSION = __version__

optimap/settings.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,3 +384,26 @@
384384
GAZETTEER_PLACEHOLDER = env('OPTIMAP_GAZETTEER_PLACEHOLDER', default='Search for a location...')
385385
# Optional API key for commercial providers (not required for Nominatim)
386386
GAZETTEER_API_KEY = env('OPTIMAP_GAZETTEER_API_KEY', default='')
387+
388+
# Works List Pagination Settings
389+
# Default number of works to display per page
390+
WORKS_PAGE_SIZE_DEFAULT = int(env('OPTIMAP_WORKS_PAGE_SIZE_DEFAULT', default=50))
391+
# Minimum page size users can select
392+
WORKS_PAGE_SIZE_MIN = int(env('OPTIMAP_WORKS_PAGE_SIZE_MIN', default=10))
393+
# Maximum page size users can select
394+
WORKS_PAGE_SIZE_MAX = int(env('OPTIMAP_WORKS_PAGE_SIZE_MAX', default=200))
395+
396+
# Calculate available page size options by doubling from MIN to MAX
397+
# Always includes MIN and MAX values
398+
def _calculate_page_size_options(min_size, max_size):
399+
"""Calculate page size options by doubling from min to max"""
400+
options = [min_size]
401+
current = min_size
402+
while current * 2 < max_size:
403+
current = current * 2
404+
options.append(current)
405+
if options[-1] != max_size:
406+
options.append(max_size)
407+
return options
408+
409+
WORKS_PAGE_SIZE_OPTIONS = _calculate_page_size_options(WORKS_PAGE_SIZE_MIN, WORKS_PAGE_SIZE_MAX)
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# publications/management/commands/update_statistics.py
2+
"""
3+
Management command to update cached statistics.
4+
Run this command nightly via cron job:
5+
0 2 * * * /path/to/manage.py update_statistics
6+
"""
7+
8+
from django.core.management.base import BaseCommand
9+
from publications.utils.statistics import update_statistics_cache
10+
11+
12+
class Command(BaseCommand):
13+
help = 'Update cached publication statistics'
14+
15+
def handle(self, *args, **options):
16+
self.stdout.write('Updating publication statistics...')
17+
18+
try:
19+
stats = update_statistics_cache()
20+
21+
self.stdout.write(self.style.SUCCESS('✓ Statistics updated successfully'))
22+
self.stdout.write(f' Total works: {stats["total_works"]}')
23+
self.stdout.write(f' Published works: {stats["published_works"]}')
24+
self.stdout.write(f' With complete metadata: {stats["with_complete_metadata"]} ({stats["complete_percentage"]}%)')
25+
26+
except Exception as e:
27+
self.stdout.write(
28+
self.style.ERROR(f'✗ Failed to update statistics: {str(e)}')
29+
)
30+
raise

publications/templates/works.html

Lines changed: 250 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,30 +4,263 @@
44

55
{% block content %}
66
<div class="container py-5 works-page">
7-
<h1>All Article Links</h1>
7+
<h1>All Works</h1>
8+
89
{% if is_admin %}
910
<p class="alert alert-info">
1011
<strong>Admin view:</strong> You can see all publications regardless of status. Status labels are shown next to each entry.
1112
</p>
1213
{% endif %}
13-
<ul>
14-
{% for item in links %}
15-
<li>
16-
<a href="{{ item.href }}" target="_blank" rel="noopener" title="Open article: {{ item.title }} (opens in new tab)">{{ item.title }}</a>
17-
{% if is_admin and item.status %}
18-
<span class="badge
19-
{% if item.status_code == 'p' %}badge-success
20-
{% elif item.status_code == 'd' %}badge-secondary
21-
{% elif item.status_code == 't' %}badge-warning
22-
{% elif item.status_code == 'w' %}badge-danger
23-
{% elif item.status_code == 'h' %}badge-info
24-
{% elif item.status_code == 'c' %}badge-primary
25-
{% endif %}">{{ item.status }}</span>
14+
15+
<!-- Pagination controls (top) -->
16+
{% if page_obj %}
17+
<div class="pagination-controls mb-4">
18+
<div class="row align-items-center">
19+
<div class="col-md-6">
20+
<p class="mb-2">
21+
Showing {{ page_obj.start_index }} to {{ page_obj.end_index }} of {{ page_obj.paginator.count }} works
22+
</p>
23+
</div>
24+
<div class="col-md-6 text-md-right">
25+
<form method="get" class="form-inline justify-content-md-end">
26+
<label for="page-size" class="mr-2">Works per page:</label>
27+
<select name="size" id="page-size" class="form-control form-control-sm mr-2" onchange="this.form.submit()">
28+
{% for option in page_size_options %}
29+
<option value="{{ option }}" {% if option == page_size %}selected{% endif %}>{{ option }}</option>
30+
{% endfor %}
31+
</select>
32+
<input type="hidden" name="page" value="1">
33+
</form>
34+
</div>
35+
</div>
36+
37+
<!-- Pagination nav -->
38+
<nav aria-label="Works pagination" class="mt-3">
39+
<ul class="pagination justify-content-center">
40+
{% if page_obj.has_previous %}
41+
<li class="page-item">
42+
<a class="page-link" href="?page=1&size={{ page_size }}" aria-label="First">
43+
<span aria-hidden="true">&laquo;&laquo;</span>
44+
</a>
45+
</li>
46+
<li class="page-item">
47+
<a class="page-link" href="?page={{ page_obj.previous_page_number }}&size={{ page_size }}" aria-label="Previous">
48+
<span aria-hidden="true">&laquo;</span>
49+
</a>
50+
</li>
51+
{% else %}
52+
<li class="page-item disabled">
53+
<span class="page-link">&laquo;&laquo;</span>
54+
</li>
55+
<li class="page-item disabled">
56+
<span class="page-link">&laquo;</span>
57+
</li>
2658
{% endif %}
27-
</li>
59+
60+
{% for num in page_obj.paginator.page_range %}
61+
{% if page_obj.number == num %}
62+
<li class="page-item active"><span class="page-link">{{ num }} <span class="sr-only">(current)</span></span></li>
63+
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
64+
<li class="page-item"><a class="page-link" href="?page={{ num }}&size={{ page_size }}">{{ num }}</a></li>
65+
{% endif %}
66+
{% endfor %}
67+
68+
{% if page_obj.has_next %}
69+
<li class="page-item">
70+
<a class="page-link" href="?page={{ page_obj.next_page_number }}&size={{ page_size }}" aria-label="Next">
71+
<span aria-hidden="true">&raquo;</span>
72+
</a>
73+
</li>
74+
<li class="page-item">
75+
<a class="page-link" href="?page={{ page_obj.paginator.num_pages }}&size={{ page_size }}" aria-label="Last">
76+
<span aria-hidden="true">&raquo;&raquo;</span>
77+
</a>
78+
</li>
79+
{% else %}
80+
<li class="page-item disabled">
81+
<span class="page-link">&raquo;</span>
82+
</li>
83+
<li class="page-item disabled">
84+
<span class="page-link">&raquo;&raquo;</span>
85+
</li>
86+
{% endif %}
87+
</ul>
88+
</nav>
89+
</div>
90+
{% endif %}
91+
92+
<!-- Works list -->
93+
<div class="works-list">
94+
{% for work in works %}
95+
<div class="work-item mb-3 pb-3 border-bottom">
96+
<h5 class="work-title mb-2">
97+
<a href="{{ work.href }}" {% if not work.href|slice:":1" == "/" %}target="_blank" rel="noopener"{% endif %} title="{% if work.doi %}View details for: {% endif %}{{ work.title }}">
98+
{{ work.title }}
99+
</a>
100+
{% if is_admin and work.status %}
101+
<span class="badge ml-2
102+
{% if work.status_code == 'p' %}badge-success
103+
{% elif work.status_code == 'd' %}badge-secondary
104+
{% elif work.status_code == 't' %}badge-warning
105+
{% elif work.status_code == 'w' %}badge-danger
106+
{% elif work.status_code == 'h' %}badge-info
107+
{% elif work.status_code == 'c' %}badge-primary
108+
{% endif %}">{{ work.status }}</span>
109+
{% endif %}
110+
</h5>
111+
<div class="work-metadata text-muted">
112+
{% if work.authors %}
113+
<span class="work-authors">
114+
<i class="fas fa-users" aria-hidden="true"></i>
115+
{% if work.authors|length > 3 %}
116+
{{ work.authors.0 }}, {{ work.authors.1 }}, {{ work.authors.2 }}, et al. ({{ work.authors|length }} authors)
117+
{% else %}
118+
{{ work.authors|join:", " }}
119+
{% endif %}
120+
</span>
121+
<span class="mx-2">·</span>
122+
{% endif %}
123+
{% if work.source %}
124+
<span class="work-source">
125+
<i class="fas fa-book" aria-hidden="true"></i> {{ work.source }}
126+
</span>
127+
<span class="mx-2">·</span>
128+
{% endif %}
129+
{% if work.doi %}
130+
<span class="work-doi">
131+
<i class="fas fa-link" aria-hidden="true"></i>
132+
<a href="https://doi.org/{{ work.doi }}" target="_blank" rel="noopener" title="View DOI: {{ work.doi }}">
133+
DOI: {{ work.doi }}
134+
</a>
135+
</span>
136+
{% endif %}
137+
</div>
138+
</div>
28139
{% empty %}
29-
<li>No publications found.</li>
140+
<p class="alert alert-warning">No publications found.</p>
30141
{% endfor %}
31-
</ul>
142+
</div>
143+
144+
<!-- Pagination controls (bottom) -->
145+
{% if page_obj and page_obj.paginator.num_pages > 1 %}
146+
<div class="pagination-controls mt-4">
147+
<nav aria-label="Works pagination" class="mt-3">
148+
<ul class="pagination justify-content-center">
149+
{% if page_obj.has_previous %}
150+
<li class="page-item">
151+
<a class="page-link" href="?page=1&size={{ page_size }}" aria-label="First">
152+
<span aria-hidden="true">&laquo;&laquo;</span>
153+
</a>
154+
</li>
155+
<li class="page-item">
156+
<a class="page-link" href="?page={{ page_obj.previous_page_number }}&size={{ page_size }}" aria-label="Previous">
157+
<span aria-hidden="true">&laquo;</span>
158+
</a>
159+
</li>
160+
{% else %}
161+
<li class="page-item disabled">
162+
<span class="page-link">&laquo;&laquo;</span>
163+
</li>
164+
<li class="page-item disabled">
165+
<span class="page-link">&laquo;</span>
166+
</li>
167+
{% endif %}
168+
169+
{% for num in page_obj.paginator.page_range %}
170+
{% if page_obj.number == num %}
171+
<li class="page-item active"><span class="page-link">{{ num }} <span class="sr-only">(current)</span></span></li>
172+
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
173+
<li class="page-item"><a class="page-link" href="?page={{ num }}&size={{ page_size }}">{{ num }}</a></li>
174+
{% endif %}
175+
{% endfor %}
176+
177+
{% if page_obj.has_next %}
178+
<li class="page-item">
179+
<a class="page-link" href="?page={{ page_obj.next_page_number }}&size={{ page_size }}" aria-label="Next">
180+
<span aria-hidden="true">&raquo;</span>
181+
</a>
182+
</li>
183+
<li class="page-item">
184+
<a class="page-link" href="?page={{ page_obj.paginator.num_pages }}&size={{ page_size }}" aria-label="Last">
185+
<span aria-hidden="true">&raquo;&raquo;</span>
186+
</a>
187+
</li>
188+
{% else %}
189+
<li class="page-item disabled">
190+
<span class="page-link">&raquo;</span>
191+
</li>
192+
<li class="page-item disabled">
193+
<span class="page-link">&raquo;&raquo;</span>
194+
</li>
195+
{% endif %}
196+
</ul>
197+
</nav>
198+
</div>
199+
{% endif %}
200+
201+
<!-- Statistics -->
202+
{% if statistics %}
203+
<div class="works-statistics mt-5 pt-4 border-top">
204+
<h2>Statistics</h2>
205+
<div class="row">
206+
<div class="col-md-6">
207+
<dl class="row">
208+
<dt class="col-sm-8">Total works in database:</dt>
209+
<dd class="col-sm-4 text-right">{{ statistics.total_works|default:"0" }}</dd>
210+
211+
<dt class="col-sm-8">Published works:</dt>
212+
<dd class="col-sm-4 text-right">{{ statistics.published_works|default:"0" }}</dd>
213+
214+
<dt class="col-sm-8">With geographic data:</dt>
215+
<dd class="col-sm-4 text-right">{{ statistics.with_geometry|default:"0" }}</dd>
216+
217+
<dt class="col-sm-8">With temporal extent:</dt>
218+
<dd class="col-sm-4 text-right">{{ statistics.with_temporal|default:"0" }}</dd>
219+
</dl>
220+
</div>
221+
<div class="col-md-6">
222+
<dl class="row">
223+
<dt class="col-sm-8">With author information:</dt>
224+
<dd class="col-sm-4 text-right">{{ statistics.with_authors|default:"0" }}</dd>
225+
226+
<dt class="col-sm-8">With DOI:</dt>
227+
<dd class="col-sm-4 text-right">{{ statistics.with_doi|default:"0" }}</dd>
228+
229+
<dt class="col-sm-8">With abstract:</dt>
230+
<dd class="col-sm-4 text-right">{{ statistics.with_abstract|default:"0" }}</dd>
231+
232+
<dt class="col-sm-8">Open access:</dt>
233+
<dd class="col-sm-4 text-right">{{ statistics.open_access|default:"0" }}</dd>
234+
</dl>
235+
</div>
236+
</div>
237+
<div class="row mt-3">
238+
<div class="col-12">
239+
<div class="alert alert-info">
240+
<strong>Complete metadata coverage:</strong>
241+
{{ statistics.with_complete_metadata|default:"0" }} works ({{ statistics.complete_percentage|default:"0" }}%)
242+
have geographic data, temporal extent, and author information.
243+
</div>
244+
</div>
245+
</div>
246+
</div>
247+
{% endif %}
248+
249+
<!-- API Link -->
250+
{% if api_url %}
251+
<div class="api-link mt-4 pt-3 border-top">
252+
<p>
253+
<i class="fas fa-code" aria-hidden="true"></i>
254+
<strong>API Access:</strong>
255+
<a href="{{ api_url }}" target="_blank" rel="noopener" title="Access this data via API">
256+
View this page as JSON (API)
257+
</a>
258+
</p>
259+
<p class="text-muted small">
260+
The API returns the same works displayed on this page with full metadata in JSON format.
261+
</p>
262+
</div>
263+
{% endif %}
264+
32265
</div>
33266
{% endblock %}

publications/utils/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)