Skip to content

Commit 3766108

Browse files
authored
spectrum form no longer directly adds to public site (#215)
1 parent 77730c9 commit 3766108

File tree

10 files changed

+153
-30
lines changed

10 files changed

+153
-30
lines changed

backend/fpbase/templates/pages/contact.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ <h2>What's on your mind?</h2>
99

1010
<p class="small text-muted">A common reason people contact us is to request that new spectra be added to the database.
1111
While we will eventually try to accomodate these requests, please note that much of FPbase is publicly editable, and
12-
you can <a href="{% url "proteins:submit-spectra" %}">add spectra to the database here</a>.</p>
12+
you can <a href="{% url "proteins:submit-spectra" %}">add spectra to the database here</a>, and
13+
<a href="{% url "proteins:submit" %}">submit new proteins here</a>.</p>
1314
<form action="{% url 'contact' %}" method="post">
1415
{% csrf_token %}
1516
{% crispy form %}

backend/proteins/admin.py

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from django import forms
22
from django.contrib import admin
3-
from django.db.models import Count
3+
from django.db.models import Count, Prefetch
44
from django.forms import TextInput
55
from django.urls import reverse
66
from django.utils.safestring import mark_safe
@@ -56,7 +56,8 @@ def __init__(self, *args, **kwargs):
5656
def spectra(self, obj):
5757
def _makelink(sp):
5858
url = reverse("admin:proteins_spectrum_change", args=(sp.pk,))
59-
return f'<a href="{url}">{sp.get_subtype_display()}</a>'
59+
pending = " (pending)" if sp.status == Spectrum.STATUS.pending else ""
60+
return f'<a href="{url}">{sp.get_subtype_display()}{pending}</a>'
6061

6162
links = []
6263
if isinstance(obj, Fluorophore):
@@ -67,13 +68,13 @@ def _makelink(sp):
6768

6869
def get_queryset(self, request):
6970
qs = super().get_queryset(request)
70-
return qs.prefetch_related("spectrum")
71+
return qs.prefetch_related(Prefetch("spectrum", queryset=Spectrum.objects.all_objects()))
7172

7273

7374
class MultipleSpectraOwner(SpectrumOwner):
7475
def get_queryset(self, request):
7576
qs = super(SpectrumOwner, self).get_queryset(request)
76-
return qs.prefetch_related("spectra")
77+
return qs.prefetch_related(Prefetch("spectra", queryset=Spectrum.objects.all_objects()))
7778

7879

7980
class BleachInline(admin.TabularInline):
@@ -219,7 +220,7 @@ class SpectrumAdmin(VersionAdmin):
219220
"created_by",
220221
)
221222
list_display = ("__str__", "category", "subtype", "owner", "created_by")
222-
list_filter = ("created", "category", "subtype")
223+
list_filter = ("status", "created", "category", "subtype")
223224
readonly_fields = ("owner", "name", "created", "modified")
224225
search_fields = (
225226
"owner_state__protein__name",
@@ -253,6 +254,7 @@ def get_fields(self, request, obj=None):
253254
"solvent",
254255
"source",
255256
"reference",
257+
"status",
256258
("created", "created_by"),
257259
("modified", "updated_by"),
258260
]
@@ -267,9 +269,16 @@ def owner(self, obj):
267269
link = f'<a href="{url}">{obj.owner}</a>'
268270
return mark_safe(link)
269271

270-
# def get_queryset(self, request):
271-
# qs = super().get_queryset(request)
272-
# return qs.prefetch_related('owner_state__protein')
272+
def get_queryset(self, request):
273+
"""
274+
Return a QuerySet of all model instances that can be edited by the
275+
admin site. This is used by changelist_view.
276+
"""
277+
qs = Spectrum.objects.all_objects()
278+
ordering = self.get_ordering(request)
279+
if ordering:
280+
qs = qs.order_by(*ordering)
281+
return qs
273282

274283

275284
@admin.register(OSERMeasurement)
@@ -458,7 +467,7 @@ def save_model(self, request, obj, form, change):
458467

459468

460469
@admin.action(description="Mark selected proteins as approved")
461-
def make_approved(modeladmin, request, queryset):
470+
def approve_protein(modeladmin, request, queryset):
462471
# note, this will fail if the list is ordered by numproteins
463472
queryset.update(status=Protein.STATUS.approved)
464473

@@ -487,7 +496,7 @@ class ProteinAdmin(CompareVersionAdmin):
487496
)
488497
prepopulated_fields = {"slug": ("name",)}
489498
inlines = (StateInline, StateTransitionInline, OSERInline, LineageInline)
490-
actions = [make_approved]
499+
actions = [approve_protein]
491500
fieldsets = [
492501
(
493502
None,

backend/proteins/forms/spectrum.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -160,14 +160,18 @@ def clean_owner_state(self):
160160
owner_state = self.cleaned_data.get("owner_state")
161161
stype = self.cleaned_data.get("subtype")
162162
if self.cleaned_data.get("category") == Spectrum.PROTEIN:
163-
if owner_state.spectra.filter(subtype=stype).exists():
163+
spectra = Spectrum.objects.all_objects().filter(owner_state=owner_state, subtype=stype)
164+
if spectra.exists():
165+
first = spectra.first()
164166
self.add_error(
165167
"owner_state",
166168
forms.ValidationError(
167-
"%(owner)s already has a{} %(stype)s spectrum".format("n" if stype != Spectrum.TWOP else ""),
169+
"%(owner)s already has a%(n)s %(stype)s spectrum %(status)s",
168170
params={
169171
"owner": owner_state,
170-
"stype": owner_state.spectra.filter(subtype=stype).first().get_subtype_display().lower(),
172+
"stype": first.get_subtype_display().lower(),
173+
"n": "n" if stype != Spectrum.TWOP else "",
174+
"status": " (pending)" if first.status == Spectrum.STATUS.pending else "",
171175
},
172176
code="owner_exists",
173177
),
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Generated by Django 4.2.1 on 2024-09-20 14:26
2+
3+
from django.db import migrations
4+
import django.utils.timezone
5+
import model_utils.fields
6+
7+
8+
class Migration(migrations.Migration):
9+
10+
dependencies = [
11+
("proteins", "0054_microscope_cfg_calc_efficiency_and_more"),
12+
]
13+
14+
operations = [
15+
migrations.AddField(
16+
model_name="spectrum",
17+
name="status",
18+
field=model_utils.fields.StatusField(
19+
choices=[
20+
("pending", "pending"),
21+
("approved", "approved"),
22+
("rejected", "rejected"),
23+
],
24+
default="pending",
25+
max_length=100,
26+
no_check_for_status=True,
27+
verbose_name="status",
28+
),
29+
),
30+
migrations.AddField(
31+
model_name="spectrum",
32+
name="status_changed",
33+
field=model_utils.fields.MonitorField(
34+
default=django.utils.timezone.now,
35+
monitor="status",
36+
verbose_name="status changed",
37+
),
38+
),
39+
]
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Generated by Django 4.2.1 on 2024-09-20 14:26
2+
3+
from contextlib import suppress
4+
from django.db import migrations
5+
from proteins.models import Spectrum
6+
7+
def set_default_status(apps, schema_editor):
8+
for spectrum in Spectrum.objects.all():
9+
try:
10+
spectrum.status = Spectrum.STATUS.approved
11+
spectrum.save()
12+
except Exception:
13+
print(f"Failed to resave spectrum id: {spectrum.id}")
14+
15+
class Migration(migrations.Migration):
16+
17+
dependencies = [
18+
("proteins", "0055_spectrum_status_spectrum_status_changed"),
19+
]
20+
21+
operations = [migrations.RunPython(set_default_status)]

backend/proteins/models/spectrum.py

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import ast
22
import json
3+
import logging
34

45
import numpy as np
56
from django.contrib.postgres.fields import ArrayField
@@ -11,14 +12,17 @@
1112
from django.forms import CharField, Textarea
1213
from django.urls import reverse
1314
from django.utils.text import slugify
15+
from model_utils import Choices
1416
from model_utils.managers import QueryManager
15-
from model_utils.models import TimeStampedModel
17+
from model_utils.models import StatusModel, TimeStampedModel
1618
from references.models import Reference
1719

1820
from ..util.helpers import wave_to_hex
1921
from ..util.spectra import interp_linear, interp_univar, norm2one, norm2P, step_size
2022
from .mixins import AdminURLMixin, Authorable, Product
2123

24+
logger = logging.getLogger(__name__)
25+
2226

2327
class SpectrumOwner(Authorable, TimeStampedModel):
2428
name = models.CharField(max_length=100) # required
@@ -74,6 +78,13 @@ def get_cached_spectra_info(timeout=60 * 60):
7478

7579

7680
class SpectrumManager(models.Manager):
81+
def get_queryset(self):
82+
# by default, only include approved spectra
83+
return super().get_queryset().filter(status=Spectrum.STATUS.approved)
84+
85+
def all_objects(self):
86+
return super().get_queryset()
87+
7788
def state_slugs(self):
7889
L = (
7990
self.get_queryset()
@@ -294,7 +305,9 @@ def validate(self, value, model_instance):
294305
raise ValidationError("All items in Spectrum list elements must be numbers")
295306

296307

297-
class Spectrum(Authorable, TimeStampedModel, AdminURLMixin):
308+
class Spectrum(Authorable, StatusModel, TimeStampedModel, AdminURLMixin):
309+
STATUS = Choices("approved", "pending", "rejected")
310+
298311
DYE = "d"
299312
PROTEIN = "p"
300313
LIGHT = "l"
@@ -419,12 +432,15 @@ def save(self, *args, **kwargs):
419432
super().save(*args, **kwargs)
420433

421434
def _norm2one(self):
422-
if self.subtype == self.TWOP:
423-
y, self._peakval2p, maxi = norm2P(self.y)
424-
self._peakwave2p = self.x[maxi]
425-
self.change_y(y)
426-
else:
427-
self.change_y(norm2one(self.y))
435+
try:
436+
if self.subtype == self.TWOP:
437+
y, self._peakval2p, maxi = norm2P(self.y)
438+
self._peakwave2p = self.x[maxi]
439+
self.change_y(y)
440+
else:
441+
self.change_y(norm2one(self.y))
442+
except Exception:
443+
logger.exception("Error normalizing spectrum data")
428444

429445
def _interpolated_data(self, method=None, **kwargs):
430446
if not method or method.lower() == "linear":
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{% extends "base.html" %} {% block content %}
2+
<div class="container">
3+
<div class="row">
4+
<div class="col-md-12">
5+
<h1>Thank you!</h1>
6+
<p>The {{spectrum_name}} spectrum has been submitted successfully. A moderator will verify it soon.</p>
7+
<p>If you don't see it within a couple days, please <a href="{% url 'contact' %}">contact us</a>.</p>
8+
<p>If you like, you may <a href="{% url 'proteins:submit-spectra' %}">add another spectrum.</a></p>
9+
</div>
10+
</div>
11+
</div>
12+
{% endblock content %}

backend/proteins/urls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
),
6666
name="submit-spectra",
6767
),
68+
path("spectra/submitted/", views.spectrum_submitted, name="spectrum_submitted"),
6869
re_path(
6970
r"^spectra/submit/(?P<slug>[-\w]+)/$",
7071
(

backend/proteins/util/spectra.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ def norm2P(y):
6363
localmax = argrelextrema(y, np.greater, order=100)
6464
# can't be within first 10 points
6565
localmax = [i for i in localmax[0] if i > 10]
66+
if not localmax:
67+
return y, 0, 0
6668
maxind = localmax[np.argmax(y[localmax])]
6769
maxy = y[maxind]
6870
return [round(max(yy / maxy, 0), 4) for yy in y], maxy, maxind

backend/proteins/views/spectra.py

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import contextlib
22
import json
3+
from textwrap import dedent
34

45
# from django.views.decorators.cache import cache_page
56
# from django.views.decorators.vary import vary_on_cookie
@@ -9,6 +10,7 @@
910
from django.http import Http404, HttpResponse, JsonResponse
1011
from django.shortcuts import render
1112
from django.template.defaultfilters import slugify
13+
from django.urls import reverse_lazy
1214
from django.views.decorators.clickjacking import xframe_options_exempt
1315
from django.views.generic import CreateView
1416
from fpbase.util import is_ajax, uncache_protein_page
@@ -48,12 +50,17 @@ def protein_spectra_graph(request, slug=None):
4850
return render(request, "spectra_graph.html")
4951

5052

53+
def spectrum_submitted(request):
54+
context = {"spectrum_name": request.session.get("spectrum_name", "")}
55+
return render(request, "spectrum_submitted.html", context)
56+
57+
5158
class SpectrumCreateView(CreateView):
5259
model = Spectrum
5360
form_class = SpectrumForm
5461

5562
def get_success_url(self, **kwargs):
56-
return self.object.owner.get_absolute_url()
63+
return reverse_lazy("proteins:spectrum_submitted")
5764

5865
def get_initial(self):
5966
init = super().get_initial()
@@ -74,33 +81,44 @@ def get_form(self, form_class=None):
7481
form = super().get_form()
7582

7683
if self.kwargs.get("slug", False):
77-
try:
84+
with contextlib.suppress(Exception):
7885
form.fields["owner_state"] = forms.ModelChoiceField(
7986
required=True,
8087
label="Protein (state)",
8188
empty_label=None,
8289
queryset=State.objects.filter(protein=self.protein).select_related("protein"),
8390
)
8491
form.fields["category"].disabled = True
85-
except Exception:
86-
pass
8792
return form
8893

8994
def form_valid(self, form):
95+
# Set the status to "pending" before saving the form
96+
if not self.request.user.is_staff:
97+
form.instance.status = Spectrum.STATUS.pending
9098
# This method is called when valid form data has been POSTed.
9199
# It should return an HttpResponse.
92-
i = super().form_valid(form)
100+
response = super().form_valid(form)
93101
with contextlib.suppress(Exception):
94102
uncache_protein_page(self.object.owner_state.protein.slug, self.request)
95103

96104
if not self.request.user.is_staff:
105+
body = f"""
106+
A new spectrum has been submitted to FPbase.
107+
108+
Admin URL: {self.request.build_absolute_uri(form.instance.get_admin_url())}
109+
User: {self.request.user}
110+
Data:
111+
112+
{form.cleaned_data}
113+
"""
97114
EmailMessage(
98-
f'[FPbase] Spectrum submitted: {form.cleaned_data["owner"]}',
99-
self.request.build_absolute_uri(form.instance.get_admin_url()),
115+
subject=f'[FPbase] Spectrum needs validation: {form.cleaned_data["owner"]}',
116+
body=dedent(body),
100117
to=[a[1] for a in settings.ADMINS],
101118
headers={"X-Mailgun-Track": "no"},
102119
).send()
103-
return i
120+
self.request.session["spectrum_name"] = self.object.name
121+
return response
104122

105123
def get_context_data(self, **kwargs):
106124
# Call the base implementation first to get a context

0 commit comments

Comments
 (0)