Skip to content
Open
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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@ Types of changes:
- `Fixed`: for any bug fixes.
- `Security`: in case of vulnerabilities.

## [x.y.z]

### Added

- Added a new form to update properties of existing transients
- Form prompts retriggering of updated transients from the relevant stages
- Added a new migration to record comments on any user-updated properties

## [1.12.0]

### Added
Expand Down
10 changes: 10 additions & 0 deletions app/host/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,14 @@ class TransientUploadForm(forms.Form):
label='Transient definition table',
required=False,
)
update_info = forms.CharField(
widget=forms.Textarea(attrs={
'style': 'min-width: 30rem;',
'cols': 100,
'placeholder': 'Identifier, RA, Dec, Redshift, Classification, HostName, HostRA, HostDec, HostRedshift, Global_aperture_a, Global_aperture_b, Global_aperture_theta, Comment'
}),
label='Transient update table',
required=False,
)

file = forms.FileField(required=False)
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 5.1.14 on 2026-06-23 00:59

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('host', '0050_alter_host_name_alias'),
]

operations = [
migrations.AddField(
model_name='transient',
name='update_comment',
field=models.CharField(blank=True, max_length=500, null=True),
),
migrations.AddField(
model_name='transient',
name='update_fields',
field=models.CharField(blank=True, max_length=500, null=True),
),
]
4 changes: 3 additions & 1 deletion app/host/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,9 @@ def validate_name(name):
added_by = models.ForeignKey(User, null=True, blank=True, on_delete=models.SET_NULL)
progress = models.IntegerField(default=0)
software_version = models.CharField(max_length=50, blank=True, null=True)

update_comment = models.CharField(max_length=500, blank=True, null=True)
update_fields = models.CharField(max_length=500, blank=True, null=True)

@property
def best_redshift(self):
"""get the best redshift for a transient"""
Expand Down
71 changes: 70 additions & 1 deletion app/host/templates/host/add_transient.html
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
<div class="pt-5">
<h1 class="mb-3"><i class="bi bi-database-add"></i> Add Transients</h1>
<p class="mb-3">
<b>There are three ways to add transients to the Blast database and trigger their data processing.</b>
<b>There are four ways to add or update transients to the Blast database and trigger their data processing.</b>
Click a method below to enter information:
</p>
</div>
Expand All @@ -71,6 +71,10 @@ <h1 class="mb-3"><i class="bi bi-database-add"></i> Add Transients</h1>
<span style="font-size: larger;"><i class="bi bi-filetype-csv"></i></span>
&nbsp;&nbsp;Define transients by specifying properties.
</a>
<a href="#" type="button" class="list-group-item list-group-item-action" id="update-button" onclick="showUpdateForm();">
<span style="font-size: larger;"><i class="bi bi-filetype-csv"></i></span>
&nbsp;&nbsp;Update properties of existing transients.
</a>
<a href="#" type="button" class="list-group-item list-group-item-action" id="upload-archive-button" onclick="showImportArchiveForm();">
<span style="font-size: larger;"><i class="bi bi-file-earmark-zip"></i></span>
&nbsp;&nbsp;Import transient from an exported archive file.
Expand Down Expand Up @@ -111,6 +115,28 @@ <h1 class="mb-3"><i class="bi bi-database-add"></i> Add Transients</h1>
</div>
{% endif %}

{% if updated_transient_names|length %}
<div class="py-3">
<p>The following transients were updated and re-triggered:</p>
<ul>
{% for transient_name in updated_transient_names %}
<li><a href="/transients/{{ transient_name }}">{{ transient_name }}</a></li>
{% endfor %}
</ul>
</div>
{% endif %}

{% if not_existing_transient_names|length %}
<div class="py-3">
<p>The following transients were not found in the database:</p>
<ul>
{% for transient_name in not_existing_transient_names %}
<li><a href="/transients/{{ transient_name }}">{{ transient_name }}</a></li>
{% endfor %}
</ul>
</div>
{% endif %}

{% if defined_transient_names|length %}
<div class="py-3">
<p>The following transients were successfully added to the Blast database:</p>
Expand Down Expand Up @@ -170,6 +196,28 @@ <h1 class="mb-3"><i class="bi bi-database-add"></i> Add Transients</h1>
{{ form.full_info | as_crispy_field }}
</div>
</div>
<div id="update-form" class="transient-form">
<p>Update transients by supplying a comma-separated value (CSV) table,
where each row defines a transient with the following columns of information:<br>
<code>Identifier, RA, Dec, Redshift, Classification, HostName, HostRA, HostDec, HostRedshift, Global_aperture_a, Global_aperture_b, Global_aperture_theta, Comment</code>
<ul>
<li>The identifier must match the name of an existing transient in the database.</li>
<li>RA/Dec must be decimal degrees.</li>
<li>If Blast has not successfully identified a host galaxy and you wish to update any host information, you must provide HostRA and HostDec.</li>
<li>If Blast has not successfully identified a host galaxy, you must provide HostRA and HostDec if you wish to input aperture parameters..</li>
<li>Global_aperture_a and Global_aperture_b must be in arcseconds.</li>
<li>Global_aperture_theta must be in degrees. For a right-handed world coordinate system, theta increases counterclockwise from North (PA=0).</li>
<li>The comment field is a brief explanation (500-character max) for why the transient data were updated.</li>
<li>Use "None" to indicate data that should not be updated.</li>
<li>For a given transient, the "Cutout download" and "Transient MWEBV" tasks must have completed successfully for these updates to be recorded.</li>
</ul>
<p>A transient will not be added if the name already exists in the database or if the coordinates are within one arcsecond of an existing transient.</p>
</p>
<div class="input-group-text">
{{ form.update_info | as_crispy_field }}
</div>
</div>

</div>
<div class="input-group" style="justify-content: center; margin-bottom: 5rem;">
<button type="submit" id="submit-btn" class="btn btn-primary" type="button" style="min-width: 10rem; width: 20rem;">Submit</button>
Expand All @@ -184,9 +232,11 @@ <h1 class="mb-3"><i class="bi bi-database-add"></i> Add Transients</h1>
function showImportForm() {
document.getElementById("import-form").style.display = "block";
document.getElementById("define-form").style.display = "none";
document.getElementById("update-form").style.display = "none";
document.getElementById("import-archive-form").style.display = "none";
document.getElementById("import-button").classList.add("active");
document.getElementById("define-button").classList.remove("active");
document.getElementById("update-button").classList.remove("active");
document.getElementById("upload-archive-button").classList.remove("active");
document.getElementById("submit-btn").style.display = "block";
document.getElementById("submit-btn").innerHTML = "Submit";
Expand All @@ -196,9 +246,11 @@ <h1 class="mb-3"><i class="bi bi-database-add"></i> Add Transients</h1>
function showCreateForm() {
document.getElementById("import-form").style.display = "none";
document.getElementById("define-form").style.display = "block";
document.getElementById("update-form").style.display = "none";
document.getElementById("import-archive-form").style.display = "none";
document.getElementById("import-button").classList.remove("active");
document.getElementById("define-button").classList.add("active");
document.getElementById("update-button").classList.remove("active");
document.getElementById("upload-archive-button").classList.remove("active");
document.getElementById("submit-btn").style.display = "block";
document.getElementById("submit-btn").innerHTML = "Submit";
Expand All @@ -208,15 +260,32 @@ <h1 class="mb-3"><i class="bi bi-database-add"></i> Add Transients</h1>
function showImportArchiveForm() {
document.getElementById("import-form").style.display = "none";
document.getElementById("define-form").style.display = "none";
document.getElementById("update-form").style.display = "none";
document.getElementById("import-archive-form").style.display = "block";
document.getElementById("import-button").classList.remove("active");
document.getElementById("define-button").classList.remove("active");
document.getElementById("update-button").classList.remove("active");
document.getElementById("upload-archive-button").classList.add("active");
document.getElementById("submit-btn").style.display = "block";
document.getElementById("submit-btn").innerHTML = "Upload";
document.getElementById("id_tns_names").value = "";
document.getElementById("id_full_info").value = "";
document.getElementById("add-form").enctype = "multipart/form-data";
}
function showUpdateForm() {
document.getElementById("import-form").style.display = "none";
document.getElementById("define-form").style.display = "none";
document.getElementById("update-form").style.display = "block";
document.getElementById("import-archive-form").style.display = "none";
document.getElementById("import-button").classList.remove("active");
document.getElementById("define-button").classList.remove("active");
document.getElementById("update-button").classList.add("active");
document.getElementById("upload-archive-button").classList.remove("active");
document.getElementById("submit-btn").style.display = "block";
document.getElementById("submit-btn").innerHTML = "Submit";
document.getElementById("id_tns_names").value = "";
document.getElementById("add-form").removeAttribute("enctype");
}

</script>
{% endblock %}
90 changes: 90 additions & 0 deletions app/host/tests/test_views.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
from django.test import TestCase
from textwrap import dedent
from django.contrib.auth.models import User
from django.db.models import Q
from ..host_utils import import_transient_info
from host.models import Transient
from host.models import Aperture
from host.models import TaskRegister


class ViewTest(TestCase):
Expand All @@ -19,6 +23,92 @@ def test_transient_page(self):
self.assertEqual(response.status_code, 200)


class ModifyTransientTest(TestCase):
def setUp(self):
import base64
username = 'testuser'
b64username = base64.urlsafe_b64encode(username.encode('utf-8')).decode('utf-8').strip('=')
self.credentials = {"username": b64username, "password": "secret"}
User.objects.create_user(**self.credentials, is_superuser=True)
self.client.login(**self.credentials)
with open('''/data/transient_datasets/2026dix.tar.gz''', 'rb') as dataset_fileobj:
import_transient_info(dataset_fileobj)
with open('''/data/transient_datasets/2026dgt.tar.gz''', 'rb') as dataset_fileobj:
import_transient_info(dataset_fileobj)

def test_add_tansients_by_definition(self):
# TODO: This test is fragile due to the explicit HTML string search.
response = self.client.post("/add/", data={
'update_info': dedent("""
2026dix,None,None,0.05,SN Ia,None,None,None,None,None,None,None,z and class test
2026dgt,None,None,None,None,None,132.3561625,29.5105694,None,5,5,0,host pos test
""")})
self.assertEqual(response.status_code, 200)
# Check that transients were updated
self.assertContains(
response,
text=(""" <p>The following transients were updated and re-triggered:</p>\n <ul>"""
"""\n \n <li><a href="/transients/2026dix">2026dix</a></li>\n \n <li>"""
"""<a href="/transients/2026dgt">2026dgt</a></li>"""))

# Check that 2026dix z and class were updated
transient_26dix = Transient.objects.get(name='2026dix')
self.assertEqual(transient_26dix.redshift, 0.05)
self.assertEqual(transient_26dix.spectroscopic_class, 'SN Ia')

# Check the 2026dgt host coord and aperture were updated
transient_26dgt = Transient.objects.get(name='2026dgt')
self.assertEqual(transient_26dgt.host.ra_deg, 132.3561625)
self.assertEqual(transient_26dgt.host.dec_deg, 29.5105694)

aperture = Aperture.objects.get(transient__name='2026dgt', cutout__filter__name='PanSTARRS_g')
self.assertEqual(aperture.semi_major_axis_arcsec, 5)
self.assertEqual(aperture.semi_minor_axis_arcsec, 5)
self.assertEqual(aperture.orientation_deg, 0)

# check that 2026dix tasks set to unprocessed
tasks_not_processed = TaskRegister.objects.filter(transient__name='2026dix', task__name__in=[
'Global host SED inference', 'Local aperture photometry',
'Validate local photometry', 'Local host SED inference'
]
)
for t in tasks_not_processed:
self.assertEqual(t.status.message, 'not processed')

tasks_processed = TaskRegister.objects.filter(transient__name='2026dix').filter(
~Q(
task__name__in=[
'Global host SED inference', 'Local aperture photometry',
'Validate local photometry', 'Local host SED inference'
]
)
)
for t in tasks_processed:
self.assertEqual(t.status.message, 'processed')

# check that 2026dgt tasks set to unprocessed
tasks_not_processed = TaskRegister.objects.filter(transient__name='2026dgt', task__name__in=[
'Global aperture photometry',
'Validate global photometry',
'Global host SED inference'
]
)
for t in tasks_not_processed:
self.assertEqual(t.status.message, 'not processed')

tasks_processed = TaskRegister.objects.filter(transient__name='2026dgt').filter(
~Q(
task__name__in=[
'Global aperture photometry',
'Validate global photometry',
'Global host SED inference'
]
)
)
for t in tasks_processed:
self.assertEqual(t.status.message, 'processed')


class AddTransientTest(TestCase):
def setUp(self):
import base64
Expand Down
Loading