Skip to content

Commit d676973

Browse files
committed
Improves publication status visibility
Enhances the user experience by restricting access to publications based on their status. - Introduces admin-only visibility for non-published publications on the works list and landing pages, allowing administrators to preview and manage content effectively. - Adds an "Edit in Admin" button for admins on the work landing page for quick access. - Implements permission checks in the API viewset to filter publications based on user roles. - Includes comprehensive tests to ensure the correct behavior for different user types and publication statuses.
1 parent fb490c4 commit d676973

File tree

7 files changed

+332
-10
lines changed

7 files changed

+332
-10
lines changed

.claude/settings.local.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
"Bash(gh issue view:*)",
88
"Bash(pytest:*)",
99
"Bash(pip search:*)",
10-
"Bash(psql:*)"
10+
"Bash(psql:*)",
11+
"Bash(OPTIMAP_LOGGING_LEVEL=WARNING python manage.py test tests.test_work_landing_page.PublicationStatusVisibilityTest)"
1112
],
1213
"deny": [],
1314
"ask": []

.claude/temp.md

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,21 @@
1-
# OPTIMAP
1+
add a button to the work landing page for the logged in admin that takes the user directly to the editing view in the Django backend.
22

3+
for the article http://127.0.0.1:8000/work/10.1007/s11368-020-02742-9/ with the internal ID 949
34

5+
the editing page is http://127.0.0.1:8000/admin/publications/publication/949/change/
46

5-
# geoextent
7+
--
68

9+
10+
expand all harvesting to identify an existing OpenAlex record based on the available unique identifier and store the OpenAlex ID together with the record; if there is no perfet match then the property of the record should be set to None and a seperate field should indicate which partial match(es) were found and what kind of match it was (e.g. DOI match, title+author match, etc);
11+
12+
expand all harvesting to include the messages that led to a warning log also in the email that is sent after the harvesting run, so that the user can see what went wrong without having to check the logs;
13+
14+
--
15+
16+
17+
add feed-based harvesting support (RSS/Atom) for EarthArxiv;
18+
19+
all articles from EarthArxiv are available via https://eartharxiv.org/repository/list/
20+
21+
there is a feed at https://eartharxiv.org/feed/ but it is unclear how many articles it contains

publications/templates/work_landing_page.html

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,24 @@
1818

1919
<h1 class="page-title">{{ pub.title }}</h1>
2020

21+
{% if is_admin and status_display %}
22+
<div class="alert alert-warning">
23+
<strong>Admin view:</strong> This publication has status <span class="badge
24+
{% if pub.status == 'p' %}badge-success
25+
{% elif pub.status == 'd' %}badge-secondary
26+
{% elif pub.status == 't' %}badge-warning
27+
{% elif pub.status == 'w' %}badge-danger
28+
{% elif pub.status == 'h' %}badge-info
29+
{% endif %}">{{ status_display }}</span>
30+
{% if pub.status != 'p' %}
31+
and is not visible to the public.
32+
{% endif %}
33+
<a href="/admin/publications/publication/{{ pub.id }}/change/" class="btn btn-sm btn-primary float-right" target="_blank" rel="noopener">
34+
Edit in Admin
35+
</a>
36+
</div>
37+
{% endif %}
38+
2139
<div class="meta muted">
2240
{% if authors_list %}
2341
<strong>Authors:</strong> {{ authors_list|join:", " }} ·

publications/templates/works.html

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,25 @@
1212
{% block content %}
1313
<div class="container py-5 works-page">
1414
<h2>All Article Links</h2>
15+
{% if is_admin %}
16+
<p class="alert alert-info">
17+
<strong>Admin view:</strong> You can see all publications regardless of status. Status labels are shown next to each entry.
18+
</p>
19+
{% endif %}
1520
<ul>
1621
{% for item in links %}
17-
<li><a href="{{ item.href }}" target="_blank" rel="noopener">{{ item.title }}</a></li>
22+
<li>
23+
<a href="{{ item.href }}" target="_blank" rel="noopener">{{ item.title }}</a>
24+
{% if is_admin and item.status %}
25+
<span class="badge
26+
{% if item.status_code == 'p' %}badge-success
27+
{% elif item.status_code == 'd' %}badge-secondary
28+
{% elif item.status_code == 't' %}badge-warning
29+
{% elif item.status_code == 'w' %}badge-danger
30+
{% elif item.status_code == 'h' %}badge-info
31+
{% endif %}">{{ item.status }}</span>
32+
{% endif %}
33+
</li>
1834
{% empty %}
1935
<li>No publications found.</li>
2036
{% endfor %}

publications/views.py

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -613,23 +613,55 @@ def works_list(request):
613613
Public page that lists a link for every work:
614614
- DOI present -> /work/<doi> (site-local landing page)
615615
- no DOI -> fall back to Publication.url (external/original)
616+
617+
Only published works (status='p') are shown to non-admin users.
618+
Admin users see all works with status labels.
616619
"""
617-
pubs = Publication.objects.all().order_by("-creationDate", "-id")
620+
is_admin = request.user.is_authenticated and request.user.is_staff
621+
622+
if is_admin:
623+
pubs = Publication.objects.all().order_by("-creationDate", "-id")
624+
else:
625+
pubs = Publication.objects.filter(status='p').order_by("-creationDate", "-id")
626+
618627
links = []
619628
for pub in pubs:
629+
link_data = {"title": pub.title}
630+
620631
if pub.doi:
621-
links.append({"title": pub.title, "href": reverse("optimap:article-landing", args=[pub.doi])})
632+
link_data["href"] = reverse("optimap:article-landing", args=[pub.doi])
622633
elif pub.url:
623-
links.append({"title": pub.title, "href": pub.url})
624-
return render(request, "works.html", {"links": links})
634+
link_data["href"] = pub.url
635+
636+
# Add status info for admin users
637+
if is_admin:
638+
link_data["status"] = pub.get_status_display()
639+
link_data["status_code"] = pub.status
640+
641+
links.append(link_data)
642+
643+
return render(request, "works.html", {"links": links, "is_admin": is_admin})
625644

626645

627646
def work_landing(request, doi):
628647
"""
629648
Landing page for a publication with a DOI.
630649
Embeds a small Leaflet map when geometry is available.
650+
651+
Only published works (status='p') are accessible to non-admin users.
652+
Admin users can view all works with a status label.
631653
"""
632-
pub = get_object_or_404(Publication, doi=doi)
654+
is_admin = request.user.is_authenticated and request.user.is_staff
655+
656+
# Get the publication
657+
try:
658+
pub = Publication.objects.get(doi=doi)
659+
except Publication.DoesNotExist:
660+
raise Http404("Publication not found.")
661+
662+
# Check access permissions
663+
if not is_admin and pub.status != 'p':
664+
raise Http404("Publication not found.")
633665

634666
feature_json = None
635667
if pub.geometry and not pub.geometry.empty:
@@ -645,5 +677,7 @@ def work_landing(request, doi):
645677
"feature_json": feature_json,
646678
"timeperiod_label": _format_timeperiod(pub),
647679
"authors_list": _normalize_authors(pub),
680+
"is_admin": is_admin,
681+
"status_display": pub.get_status_display() if is_admin else None,
648682
}
649683
return render(request, "work_landing_page.html", context)

publications/viewsets.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,14 @@ class PublicationViewSet(viewsets.ReadOnlyModelViewSet):
2020
filter_backends = (filters.InBBoxFilter,)
2121
serializer_class = PublicationSerializer
2222
permission_classes = [IsAuthenticatedOrReadOnly]
23-
queryset = Publication.objects.filter(status="p").distinct()
23+
24+
def get_queryset(self):
25+
"""
26+
Return all publications for admin users, only published ones for others.
27+
"""
28+
if self.request.user.is_authenticated and self.request.user.is_staff:
29+
return Publication.objects.all().distinct()
30+
return Publication.objects.filter(status="p").distinct()
2431

2532
class SubscriptionViewSet(viewsets.ModelViewSet):
2633
"""

0 commit comments

Comments
 (0)