Skip to content

Commit a85ae1c

Browse files
committed
Merge branch 'main' of https://github.com/tlambert03/FPbase
2 parents 28a1f3d + be3b7e2 commit a85ae1c

File tree

20 files changed

+959
-693
lines changed

20 files changed

+959
-693
lines changed

backend/favit/views.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22

33
from django.apps import apps
44
from django.contrib.auth.decorators import login_required
5-
from django.http import HttpResponseBadRequest, HttpResponseNotAllowed, JsonResponse
5+
from django.http import HttpResponseBadRequest, JsonResponse
66

7-
from fpbase.util import is_ajax, uncache_protein_page
7+
from fpbase.util import uncache_protein_page
88

99
from .models import Favorite
1010

@@ -13,8 +13,6 @@
1313

1414
@login_required
1515
def add_or_remove(request):
16-
if not is_ajax(request):
17-
return HttpResponseNotAllowed([])
1816
user = request.user
1917
try:
2018
app_model = request.POST["target_model"]
@@ -46,9 +44,6 @@ def add_or_remove(request):
4644

4745
@login_required
4846
def remove(request):
49-
if not is_ajax(request):
50-
return HttpResponseNotAllowed([])
51-
5247
user = request.user
5348

5449
try:

backend/proteins/templates/lineage.html

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -41,17 +41,27 @@ <h1>Fluorescent Protein Lineages</h1>
4141
.then(({ LineageChart }) => {
4242
$('.lineage').each(function(i, el){
4343
$(el).html('<p>loading lineage info... <img src="' + '{% static 'images/GFP_spinner.gif' %}' + '">');
44-
$.getJSON("/ajax" + window.location.pathname + window.location.search, function(data){
45-
$(el).empty();
46-
var chart = LineageChart({
47-
withSearch: true,
48-
withToolbar: true,
49-
withTopScroll: true,
50-
}).data(data);
51-
chart.heightScalar(52);
52-
d3.select(el).call(chart);
53-
chart.duration(100);
44+
window.fetchWithSentry("/ajax" + window.location.pathname + window.location.search, {
45+
headers: {
46+
"X-Requested-With": "XMLHttpRequest",
47+
},
5448
})
49+
.then((response) => response.json())
50+
.then((data) => {
51+
$(el).empty();
52+
var chart = LineageChart({
53+
withSearch: true,
54+
withToolbar: true,
55+
withTopScroll: true,
56+
}).data(data);
57+
chart.heightScalar(52);
58+
d3.select(el).call(chart);
59+
chart.duration(100);
60+
})
61+
.catch((error) => {
62+
console.error("Failed to load lineage data:", error);
63+
$(el).html('<div class="alert alert-danger">Failed to load lineage data. Please refresh the page.</div>');
64+
});
5565
});
5666
})
5767
.catch(function(error){

backend/proteins/templates/proteins/organism_detail.html

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -49,18 +49,28 @@ <h5 class='pt-3'><strong>Proteins derived from {{ object }}</strong></h5>
4949
window.FPBASE.loadD3Charts()
5050
.then(({ LineageChart }) => {
5151
$('.lineage').each(function(i, el){
52-
$.getJSON("{% url 'proteins:get-org-lineage' object.pk %}", function(data){
53-
if(!$.isEmptyObject(data)){
54-
$('<h5>', {class: 'pt-3'}).html('<strong>Lineages derived from {{ object }}</strong>').insertBefore(el)
55-
var chart = LineageChart({
56-
withToolbar: true,
57-
withSearch: true,
58-
withTopScroll: true,
59-
}).data(data);
60-
d3.select(el).call(chart);
61-
chart.duration(100);
62-
}
52+
window.fetchWithSentry("{% url 'proteins:get-org-lineage' object.pk %}", {
53+
headers: {
54+
"X-Requested-With": "XMLHttpRequest",
55+
},
6356
})
57+
.then((response) => response.json())
58+
.then((data) => {
59+
if(!$.isEmptyObject(data)){
60+
$('<h5>', {class: 'pt-3'}).html('<strong>Lineages derived from {{ object }}</strong>').insertBefore(el)
61+
var chart = LineageChart({
62+
withToolbar: true,
63+
withSearch: true,
64+
withTopScroll: true,
65+
}).data(data);
66+
d3.select(el).call(chart);
67+
chart.duration(100);
68+
}
69+
})
70+
.catch((error) => {
71+
console.error("Failed to load lineage data:", error);
72+
$(el).html('<div class="alert alert-danger">Failed to load lineage data. Please refresh the page.</div>');
73+
});
6474
});
6575
})
6676
.catch(function(error){

backend/proteins/templates/proteins/protein_detail.html

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -534,23 +534,33 @@ <h5 class="modal-title" id="spectraURLModalLongTitle"><strong>Spectrum Image URL
534534
function initLineage() {
535535
window.FPBASE.loadD3Charts()
536536
.then(({ LineageChart }) => {
537-
$.getJSON('/ajax/lineage/' + slug + '/', function(data) {
538-
window._lastLineageData = data;
539-
if (!data.children || data.children.length === 0){
540-
return
541-
}
542-
var linchart = LineageChart({slug: slug}).data(data);
543-
var lineage = d3.select(el);
544-
lineage.call(linchart);
545-
linchart.duration(200);
546-
if (slug && $('.lineage-wrapper')){
547-
var node = d3.select("#node_" + slug);
548-
if (!node.empty()) {
549-
var slugpos = node.datum().y;
550-
$('.lineage-wrapper').scrollLeft(slugpos - ((window.innerWidth - 30) / 3));
537+
window.fetchWithSentry('/ajax/lineage/' + slug + '/', {
538+
headers: {
539+
"X-Requested-With": "XMLHttpRequest",
540+
},
541+
})
542+
.then((response) => response.json())
543+
.then((data) => {
544+
window._lastLineageData = data;
545+
if (!data.children || data.children.length === 0){
546+
return
551547
}
552-
}
553-
});
548+
var linchart = LineageChart({slug: slug}).data(data);
549+
var lineage = d3.select(el);
550+
lineage.call(linchart);
551+
linchart.duration(200);
552+
if (slug && $('.lineage-wrapper')){
553+
var node = d3.select("#node_" + slug);
554+
if (!node.empty()) {
555+
var slugpos = node.datum().y;
556+
$('.lineage-wrapper').scrollLeft(slugpos - ((window.innerWidth - 30) / 3));
557+
}
558+
}
559+
})
560+
.catch((error) => {
561+
console.error("Failed to load lineage data:", error);
562+
$(el).html('<div class="alert alert-danger">Failed to load lineage data. Please refresh the page.</div>');
563+
});
554564
})
555565
.catch(function(error){
556566
console.error("Error loading D3 charts:", error);

backend/proteins/templates/proteins/spectrum_form.html

Lines changed: 38 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,10 @@ <h5 class="mb-0">
9999

100100
<script>
101101

102-
// Configure jQuery to include CSRF token in AJAX requests
102+
// fetchWithSentry is available globally from the ajax-sentry module loaded in the main bundle
103+
// It wraps native fetch() with Sentry error tracking
104+
105+
// Get CSRF token from cookie for fetch requests
103106
function getCookie(name) {
104107
let cookieValue = null;
105108
if (document.cookie && document.cookie !== '') {
@@ -117,14 +120,6 @@ <h5 class="mb-0">
117120

118121
const csrftoken = getCookie('csrftoken');
119122

120-
$.ajaxSetup({
121-
beforeSend: function(xhr, settings) {
122-
if (!(/^(GET|HEAD|OPTIONS|TRACE)$/.test(settings.type)) && !this.crossDomain) {
123-
xhr.setRequestHeader("X-CSRFToken", csrftoken);
124-
}
125-
}
126-
});
127-
128123
// Helper to wait for Bootstrap plugins to be available
129124
function waitForBootstrap(callback) {
130125
if (typeof $.fn.tab !== 'undefined') {
@@ -336,39 +331,49 @@ <h5 class="mb-0">
336331
formData.append('data_source', 'file');
337332
}
338333

339-
// Send AJAX request for preview
340-
$.ajax({
341-
url: "{% url 'proteins:spectrum_preview' %}",
342-
type: 'POST',
343-
data: formData,
344-
processData: false,
345-
contentType: false,
346-
success: function(response) {
334+
// Send fetch request for preview
335+
fetchWithSentry("{% url 'proteins:spectrum_preview' %}", {
336+
method: 'POST',
337+
headers: {
338+
'X-CSRFToken': csrftoken,
339+
// Legacy header required by Django is_ajax() check in dual-purpose endpoints
340+
'X-Requested-With': 'XMLHttpRequest',
341+
},
342+
body: formData,
343+
})
344+
.then(response => {
345+
if (response.ok) {
346+
return response.json();
347+
} else {
348+
return response.text().then(text => {
349+
let errorData = {};
350+
try {
351+
errorData = JSON.parse(text);
352+
} catch(e) {
353+
// Ignore parse errors
354+
}
355+
throw errorData;
356+
});
357+
}
358+
})
359+
.then(response => {
347360
if (response.success) {
348361
// Always update currentPreviewData with the latest preview
349362
currentPreviewData = response.preview;
350363
displaySpectrumPreview(response);
351364
} else {
352365
showError(response.error || 'Failed to generate preview', response.details, response.form_errors);
353366
}
354-
},
355-
error: function(xhr) {
356-
var response = {};
357-
try {
358-
response = JSON.parse(xhr.responseText || '{}');
359-
} catch(e) {
360-
// Ignore parse errors
361-
}
362-
var errorMsg = response.error || 'Failed to generate preview';
363-
var details = response.details || '';
364-
var formErrors = response.form_errors || {};
365-
367+
})
368+
.catch(error => {
369+
var errorMsg = error.error || 'Failed to generate preview';
370+
var details = error.details || '';
371+
var formErrors = error.form_errors || {};
366372
showError(errorMsg, details, formErrors);
367-
},
368-
complete: function() {
373+
})
374+
.finally(() => {
369375
submitBtn.prop('disabled', false).val(originalText);
370-
}
371-
});
376+
});
372377
}
373378

374379
function displaySpectrumPreview(response) {

backend/proteins/views/ajax.py

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@
88
from django.contrib.auth.decorators import login_required
99
from django.core.mail import mail_managers
1010
from django.db.models import Prefetch
11-
from django.http import HttpResponseNotAllowed, JsonResponse
11+
from django.http import JsonResponse
1212
from django.utils.text import slugify
1313
from django.views.decorators.cache import cache_page
1414
from django.views.generic import DetailView
1515

16-
from fpbase.util import is_ajax, uncache_protein_page
16+
from fpbase.util import uncache_protein_page
1717
from proteins.util.maintain import validate_node
1818

1919
from ..models import Dye, Fluorophore, Lineage, Organism, Protein, Spectrum, State
@@ -45,8 +45,6 @@ def serialize_comparison(request):
4545

4646

4747
def update_comparison(request):
48-
if not is_ajax(request):
49-
return HttpResponseNotAllowed([])
5048
current = set(request.session.get("comparison", []))
5149
if request.POST.get("operation") == "add":
5250
current.add(request.POST.get("object"))
@@ -63,8 +61,6 @@ def update_comparison(request):
6361

6462
@login_required
6563
def add_organism(request):
66-
if not is_ajax(request):
67-
return HttpResponseNotAllowed([])
6864
try:
6965
tax_id = request.POST.get("taxonomy_id", None)
7066
if not tax_id:
@@ -95,9 +91,6 @@ def add_organism(request):
9591

9692
@staff_member_required
9793
def approve_protein(request, slug=None):
98-
if not is_ajax(request):
99-
return HttpResponseNotAllowed([])
100-
10194
try:
10295
p = Protein.objects.get(slug=slug)
10396
if p.status != "pending":
@@ -120,9 +113,6 @@ def approve_protein(request, slug=None):
120113

121114

122115
def similar_spectrum_owners(request):
123-
if not is_ajax(request):
124-
return HttpResponseNotAllowed([])
125-
126116
name = request.POST.get("owner", None)
127117
similars = Spectrum.objects.find_similar_owners(name, 0.3)[:4]
128118

@@ -184,9 +174,6 @@ def similar_spectrum_owners(request):
184174

185175

186176
def validate_proteinname(request):
187-
if not is_ajax(request):
188-
return HttpResponseNotAllowed([])
189-
190177
name = request.POST.get("name", None)
191178
slug = request.POST.get("slug", None)
192179
try:
@@ -239,8 +226,6 @@ def recursive_node_to_dict(node, widths=None, rootseq=None, validate=False):
239226

240227
@cache_page(60 * 5)
241228
def get_lineage(request, slug=None, org=None):
242-
# if not is_ajax(request):
243-
# return HttpResponseNotAllowed([])
244229
if org:
245230
_ids = list(Lineage.objects.filter(protein__parent_organism=org, parent=None))
246231
ids = []

backend/tests/test_proteins/test_ajax_views.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -278,15 +278,17 @@ def test_similar_spectrum_owners_dye_uses_own_name(self):
278278
names = [s["name"] for s in data["similars"]]
279279
self.assertIn(self.dyes[0].name, names)
280280

281-
def test_similar_spectrum_owners_requires_ajax(self):
282-
"""Test that the endpoint requires an AJAX request."""
281+
def test_similar_spectrum_owners_works_without_ajax_header(self):
282+
"""Test that the endpoint works without the X-Requested-With header."""
283283
response = self.client.post(
284284
"/ajax/validate_spectrumownername/",
285285
{"owner": "Test"},
286286
)
287287

288-
# Should return 405 Method Not Allowed for non-AJAX requests
289-
self.assertEqual(response.status_code, 405)
288+
# Should work fine without AJAX header (JSON-only endpoint)
289+
self.assertEqual(response.status_code, 200)
290+
data = response.json()
291+
self.assertIn("similars", data)
290292

291293
def test_similar_spectrum_owners_limits_to_four_results(self):
292294
"""Test that the endpoint limits results to 4 items."""

frontend/src/fret.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import "vite/modulepreload-polyfill"
33

44
// Initialize Sentry first to catch errors during module loading
55
import "./js/sentry-init.js"
6-
import "./js/jquery-ajax-sentry.js" // Track jQuery AJAX errors
6+
import "./js/ajax-sentry.js" // Track AJAX and fetch errors
77

88
// FRET calculator functionality with Highcharts
99
import initFRET from "./js/fret.js"

frontend/src/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import "vite/modulepreload-polyfill"
1010

1111
// Initialize Sentry first to catch errors during module loading
1212
import "./js/sentry-init.js"
13-
import "./js/jquery-ajax-sentry.js" // Track jQuery AJAX errors
13+
import "./js/ajax-sentry.js" // Track jQuery AJAX errors
1414
import { icon } from "./js/icons.js" // Icon helper for dynamic HTML
1515

1616
import "select2/dist/css/select2.css"

0 commit comments

Comments
 (0)