Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 2 additions & 7 deletions backend/favit/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

from django.apps import apps
from django.contrib.auth.decorators import login_required
from django.http import HttpResponseBadRequest, HttpResponseNotAllowed, JsonResponse
from django.http import HttpResponseBadRequest, JsonResponse

from fpbase.util import is_ajax, uncache_protein_page
from fpbase.util import uncache_protein_page

from .models import Favorite

Expand All @@ -13,8 +13,6 @@

@login_required
def add_or_remove(request):
if not is_ajax(request):
return HttpResponseNotAllowed([])
user = request.user
try:
app_model = request.POST["target_model"]
Expand Down Expand Up @@ -46,9 +44,6 @@ def add_or_remove(request):

@login_required
def remove(request):
if not is_ajax(request):
return HttpResponseNotAllowed([])

user = request.user

try:
Expand Down
30 changes: 20 additions & 10 deletions backend/proteins/templates/lineage.html
Original file line number Diff line number Diff line change
Expand Up @@ -41,17 +41,27 @@ <h1>Fluorescent Protein Lineages</h1>
.then(({ LineageChart }) => {
$('.lineage').each(function(i, el){
$(el).html('<p>loading lineage info... <img src="' + '{% static 'images/GFP_spinner.gif' %}' + '">');
$.getJSON("/ajax" + window.location.pathname + window.location.search, function(data){
$(el).empty();
var chart = LineageChart({
withSearch: true,
withToolbar: true,
withTopScroll: true,
}).data(data);
chart.heightScalar(52);
d3.select(el).call(chart);
chart.duration(100);
window.fetchWithSentry("/ajax" + window.location.pathname + window.location.search, {
headers: {
"X-Requested-With": "XMLHttpRequest",
},
})
.then((response) => response.json())
.then((data) => {
$(el).empty();
var chart = LineageChart({
withSearch: true,
withToolbar: true,
withTopScroll: true,
}).data(data);
chart.heightScalar(52);
d3.select(el).call(chart);
chart.duration(100);
})
.catch((error) => {
console.error("Failed to load lineage data:", error);
$(el).html('<div class="alert alert-danger">Failed to load lineage data. Please refresh the page.</div>');
});
});
})
.catch(function(error){
Expand Down
32 changes: 21 additions & 11 deletions backend/proteins/templates/proteins/organism_detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -49,18 +49,28 @@ <h5 class='pt-3'><strong>Proteins derived from {{ object }}</strong></h5>
window.FPBASE.loadD3Charts()
.then(({ LineageChart }) => {
$('.lineage').each(function(i, el){
$.getJSON("{% url 'proteins:get-org-lineage' object.pk %}", function(data){
if(!$.isEmptyObject(data)){
$('<h5>', {class: 'pt-3'}).html('<strong>Lineages derived from {{ object }}</strong>').insertBefore(el)
var chart = LineageChart({
withToolbar: true,
withSearch: true,
withTopScroll: true,
}).data(data);
d3.select(el).call(chart);
chart.duration(100);
}
window.fetchWithSentry("{% url 'proteins:get-org-lineage' object.pk %}", {
headers: {
"X-Requested-With": "XMLHttpRequest",
},
})
.then((response) => response.json())
.then((data) => {
if(!$.isEmptyObject(data)){
$('<h5>', {class: 'pt-3'}).html('<strong>Lineages derived from {{ object }}</strong>').insertBefore(el)
var chart = LineageChart({
withToolbar: true,
withSearch: true,
withTopScroll: true,
}).data(data);
d3.select(el).call(chart);
chart.duration(100);
}
})
.catch((error) => {
console.error("Failed to load lineage data:", error);
$(el).html('<div class="alert alert-danger">Failed to load lineage data. Please refresh the page.</div>');
});
});
})
.catch(function(error){
Expand Down
42 changes: 26 additions & 16 deletions backend/proteins/templates/proteins/protein_detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -534,23 +534,33 @@ <h5 class="modal-title" id="spectraURLModalLongTitle"><strong>Spectrum Image URL
function initLineage() {
window.FPBASE.loadD3Charts()
.then(({ LineageChart }) => {
$.getJSON('/ajax/lineage/' + slug + '/', function(data) {
window._lastLineageData = data;
if (!data.children || data.children.length === 0){
return
}
var linchart = LineageChart({slug: slug}).data(data);
var lineage = d3.select(el);
lineage.call(linchart);
linchart.duration(200);
if (slug && $('.lineage-wrapper')){
var node = d3.select("#node_" + slug);
if (!node.empty()) {
var slugpos = node.datum().y;
$('.lineage-wrapper').scrollLeft(slugpos - ((window.innerWidth - 30) / 3));
window.fetchWithSentry('/ajax/lineage/' + slug + '/', {
headers: {
"X-Requested-With": "XMLHttpRequest",
},
})
.then((response) => response.json())
.then((data) => {
window._lastLineageData = data;
if (!data.children || data.children.length === 0){
return
}
}
});
var linchart = LineageChart({slug: slug}).data(data);
var lineage = d3.select(el);
lineage.call(linchart);
linchart.duration(200);
if (slug && $('.lineage-wrapper')){
var node = d3.select("#node_" + slug);
if (!node.empty()) {
var slugpos = node.datum().y;
$('.lineage-wrapper').scrollLeft(slugpos - ((window.innerWidth - 30) / 3));
}
}
})
.catch((error) => {
console.error("Failed to load lineage data:", error);
$(el).html('<div class="alert alert-danger">Failed to load lineage data. Please refresh the page.</div>');
});
})
.catch(function(error){
console.error("Error loading D3 charts:", error);
Expand Down
71 changes: 38 additions & 33 deletions backend/proteins/templates/proteins/spectrum_form.html
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,10 @@ <h5 class="mb-0">

<script>

// Configure jQuery to include CSRF token in AJAX requests
// fetchWithSentry is available globally from the ajax-sentry module loaded in the main bundle
// It wraps native fetch() with Sentry error tracking

// Get CSRF token from cookie for fetch requests
function getCookie(name) {
let cookieValue = null;
if (document.cookie && document.cookie !== '') {
Expand All @@ -117,14 +120,6 @@ <h5 class="mb-0">

const csrftoken = getCookie('csrftoken');

$.ajaxSetup({
beforeSend: function(xhr, settings) {
if (!(/^(GET|HEAD|OPTIONS|TRACE)$/.test(settings.type)) && !this.crossDomain) {
xhr.setRequestHeader("X-CSRFToken", csrftoken);
}
}
});

// Helper to wait for Bootstrap plugins to be available
function waitForBootstrap(callback) {
if (typeof $.fn.tab !== 'undefined') {
Expand Down Expand Up @@ -336,39 +331,49 @@ <h5 class="mb-0">
formData.append('data_source', 'file');
}

// Send AJAX request for preview
$.ajax({
url: "{% url 'proteins:spectrum_preview' %}",
type: 'POST',
data: formData,
processData: false,
contentType: false,
success: function(response) {
// Send fetch request for preview
fetchWithSentry("{% url 'proteins:spectrum_preview' %}", {
method: 'POST',
headers: {
'X-CSRFToken': csrftoken,
// Legacy header required by Django is_ajax() check in dual-purpose endpoints
'X-Requested-With': 'XMLHttpRequest',
},
body: formData,
})
.then(response => {
if (response.ok) {
return response.json();
} else {
return response.text().then(text => {
let errorData = {};
try {
errorData = JSON.parse(text);
} catch(e) {
// Ignore parse errors
}
throw errorData;
});
}
})
.then(response => {
if (response.success) {
// Always update currentPreviewData with the latest preview
currentPreviewData = response.preview;
displaySpectrumPreview(response);
} else {
showError(response.error || 'Failed to generate preview', response.details, response.form_errors);
}
},
error: function(xhr) {
var response = {};
try {
response = JSON.parse(xhr.responseText || '{}');
} catch(e) {
// Ignore parse errors
}
var errorMsg = response.error || 'Failed to generate preview';
var details = response.details || '';
var formErrors = response.form_errors || {};

})
.catch(error => {
var errorMsg = error.error || 'Failed to generate preview';
var details = error.details || '';
var formErrors = error.form_errors || {};
showError(errorMsg, details, formErrors);
},
complete: function() {
})
.finally(() => {
submitBtn.prop('disabled', false).val(originalText);
}
});
});
}

function displaySpectrumPreview(response) {
Expand Down
19 changes: 2 additions & 17 deletions backend/proteins/views/ajax.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@
from django.contrib.auth.decorators import login_required
from django.core.mail import mail_managers
from django.db.models import Prefetch
from django.http import HttpResponseNotAllowed, JsonResponse
from django.http import JsonResponse
from django.utils.text import slugify
from django.views.decorators.cache import cache_page
from django.views.generic import DetailView

from fpbase.util import is_ajax, uncache_protein_page
from fpbase.util import uncache_protein_page
from proteins.util.maintain import validate_node

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


def update_comparison(request):
if not is_ajax(request):
return HttpResponseNotAllowed([])
current = set(request.session.get("comparison", []))
if request.POST.get("operation") == "add":
current.add(request.POST.get("object"))
Expand All @@ -63,8 +61,6 @@ def update_comparison(request):

@login_required
def add_organism(request):
if not is_ajax(request):
return HttpResponseNotAllowed([])
try:
tax_id = request.POST.get("taxonomy_id", None)
if not tax_id:
Expand Down Expand Up @@ -95,9 +91,6 @@ def add_organism(request):

@staff_member_required
def approve_protein(request, slug=None):
if not is_ajax(request):
return HttpResponseNotAllowed([])

try:
p = Protein.objects.get(slug=slug)
if p.status != "pending":
Expand All @@ -120,9 +113,6 @@ def approve_protein(request, slug=None):


def similar_spectrum_owners(request):
if not is_ajax(request):
return HttpResponseNotAllowed([])

name = request.POST.get("owner", None)
similars = Spectrum.objects.find_similar_owners(name, 0.3)[:4]

Expand Down Expand Up @@ -184,9 +174,6 @@ def similar_spectrum_owners(request):


def validate_proteinname(request):
if not is_ajax(request):
return HttpResponseNotAllowed([])

name = request.POST.get("name", None)
slug = request.POST.get("slug", None)
try:
Expand Down Expand Up @@ -239,8 +226,6 @@ def recursive_node_to_dict(node, widths=None, rootseq=None, validate=False):

@cache_page(60 * 5)
def get_lineage(request, slug=None, org=None):
# if not is_ajax(request):
# return HttpResponseNotAllowed([])
if org:
_ids = list(Lineage.objects.filter(protein__parent_organism=org, parent=None))
ids = []
Expand Down
10 changes: 6 additions & 4 deletions backend/tests/test_proteins/test_ajax_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -278,15 +278,17 @@ def test_similar_spectrum_owners_dye_uses_own_name(self):
names = [s["name"] for s in data["similars"]]
self.assertIn(self.dyes[0].name, names)

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

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

def test_similar_spectrum_owners_limits_to_four_results(self):
"""Test that the endpoint limits results to 4 items."""
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/fret.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import "vite/modulepreload-polyfill"

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

// FRET calculator functionality with Highcharts
import initFRET from "./js/fret.js"
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import "vite/modulepreload-polyfill"

// Initialize Sentry first to catch errors during module loading
import "./js/sentry-init.js"
import "./js/jquery-ajax-sentry.js" // Track jQuery AJAX errors
import "./js/ajax-sentry.js" // Track jQuery AJAX errors
Copy link

Copilot AI Nov 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incorrect comment: This comment refers to "jQuery AJAX errors" but the file now tracks both jQuery AJAX and fetch API errors. Update the comment to reflect the current functionality, e.g., "Track AJAX and fetch errors".

Suggested change
import "./js/ajax-sentry.js" // Track jQuery AJAX errors
import "./js/ajax-sentry.js" // Track jQuery AJAX and fetch errors

Copilot uses AI. Check for mistakes.
import { icon } from "./js/icons.js" // Icon helper for dynamic HTML

import "select2/dist/css/select2.css"
Expand Down
Loading
Loading