Adding Forms to the YSE-PZ Front End¶
At Charlie’s request, I wanted to document a brief example of how to add front-end utilities to the YSE-PZ pages. In this case, I’m going to write a web form that sets up an automated way to trigger LCOGT or SOAR spectroscopic followup via their respective APIs.
This procedure can be generalized to other forms, functionality, user input, etc. The main toolset in this tutorial is how to use Django forms models. Usually Django forms are tied to a YSE-PZ object itself, though we won’t really be saving any data to SQL during this procedure.
Writing a YSE-PZ Form¶
With some small exceptions, YSE-PZ forms are rendered using the
forms.py script in the YSE_App/ directory. In this
case we’ll tie the form to the TransientFollowup model.
Starting with the minimum:
from django.forms import ModelForm
from django import forms
class LCOGTSpectrumRequest(ModelForm):
class Meta:
model = TransientFollowup
fields =()
In this case we won’t really be using the fields from the
TransientFollowup model directly, but we’ll add our
own fields to the form. For LCOGT, we need to ask the user for
the desired exposure time, whether they’d like a FLOYDS or Goodman
spectrum, and what the desired date range would be. We can do this
using the django forms module, which will set the fields that get
automatically rendered in the form:
class AutomatedSpectrumRequest(ModelForm):
spec_instruments = ['FLOYDS-N','FLOYDS-S','Goodman']
instrument = forms.ModelChoiceField(Instrument.objects.filter(Q(name__in=spec_instruments)))
exp_time = forms.IntegerField(initial=1800) # 1800s seems like a reasonable default
spectrum_valid_start = forms.DateTimeField()
spectrum_valid_stop = forms.DateTimeField()
class Meta:
model = TransientFollowup
fields =('transient',)
spectrum_valid_start and spectrum_valid_stop will be written to the valid_start
and valid_stop fields in in the TransientFollowup model, and transient is already part of
the TransientFollowup model, so we can add a few necessary fields
and then use this form to build a TransientFollowup entry.
Starting a YSE-PZ Form View¶
Now for the form “view”, which I will primarily use to save the form data from the frontend and perform actions on the YSE-PZ backend. First, the basics:
class AddAutomatedSpectrumRequestFormView(FormView):
form_class = AutomatedSpectrumRequest
template_name = 'YSE_App/form_snippets/spectrum_request_form.html'
success_url = '/form-success/'
def form_invalid(self, form):
response = super(AddAutomatedSpectrumRequestFormView, self).form_invalid(form)
if self.request.is_ajax():
return JsonResponse(form.errors, status=400)
else:
return response
def form_valid(self, form):
response = super(AddTransientFollowupFormView, self).form_valid(form)
if self.request.is_ajax():
pass
This sets up the basic form defaults, including the form class object, the name of the template form we’re about to create, and the error function for if the form is invalid. If the form is valid, and verifying that the request is passed by AJAX code on the frontend, then we will execut the necessary code.
Finally, we need to add the form view to urls.py. You can add the following
to the urlpatterns list.:
url(r'^automated_spectrum_request/', AddAutomatedSpectrumRequestFormView.as_view(), name='automated_spectrum_request'),
Rendering the Form on the YSE-PZ FrontEnd¶
Now let’s write the Django template code to render the form. I usually split this
into some basic HTML code that gets passed to the main page, and then a separate
HTML script in the form_snippets directory to render the form itself.
But first, we need to make sure the function that is generating the frontend view
includes an instance of the form. In view.py, I’m going to edit the
transient detail page to include our new form, which is the function
def transient_detail(request, slug) (you can see this in the urls.py
script).
At the top, I’m going to add a line:
automated_spectrum_form = AutomatedSpectrumRequest()
and at the bottom, I’ll include this variable in the context that gets rendered in the view.:
context['automated_spectrum_form'] = automated_spectrum_form
Writing the Django HTML¶
Now, to edit the HTML itself. You can see in the transient_detail
function that the template being rendered is YSE_App/transient_detail.html.
These paths are relative to the YSE_App/templates directory. As we are
creating a new TransientFollowup object, this form should be in the followup_tab,
where I will create a new row to contain it:
<div class="row">
<div class="col-xs-6">
<input type="hidden" id="transient_pk" value="{{ transient.id }}"/>
{% include "YSE_App/form_snippets/automated_spectrum_form.html" with form=automated_spectrum_form %}
</div>
</div>
Without going into a full-on Django tutorial, the curly brackets
and percent signs indicate Django instructions, while the rest is
HTML. This will pass our form variable (automated_spectrum_form)
to the form snippet that we’re about to write.
In this form snippet, we just need to enable django-widget-tweaks <https://github.com/jazzband/django-widget-tweaks>_ to allow
the form to easily render (slightly easier than just doing it in HTML),
and we need to add all the fields in our original forms.py class.:
{% load widget_tweaks %}
{% block content %}
<div class="box box-primary box-solid collapsed-box"> <!--collapsed-box-->
<div class="box-header with-border">
<button id="automated_spectrum_request_btn" type="button" class="btn btn-box-tool" data-widget="collapse" style="height: 30px;"><h3 class="box-title">Automated Spectrum Request</h3></button>
<div class="box-tools pull-right">
<button id="automated_spectrum_request_btn" type="button" class="btn btn-box-tool" data-widget="collapse"><i class="fa fa-plus"></i></button>
</div>
</div>
<div class="box-body">
<form id="automated_spectrum_request" action="{% url 'automated_spectrum_request' %}" method="post">
{% csrf_token %}
{% for hidden_field in form.hidden_fields %}
{{ hidden_field }}
{% endfor %}
<div class="col-xs-6">
<div class="form-group">
<label>Instrument</label>
{% render_field form.instrument class+="form-control select2" %}
</div>
</div>
<div class="col-xs-6">
<div class="form-group">
<label>Exposure Time</label>
{% render_field form.exp_time class+="form-control select2" %}
</div>
</div>
<div class="col-xs-12">
<div class="form-group">
<label>Date Range</label>
<div class="input-group">
<div class="input-group-addon">
<i class="fa fa-clock-o"></i>
</div>
<input type="text" class="form-control pull-right" id="automated_spectrum_date_range">
</div>
<input type="hidden" id="spectrum_valid_start" name="spectrum_valid_start">
<input type="hidden" id="spectrum_valid_stop" name="spectrum_valid_stop">
</div>
</div>
<div class="col-xs-12">
<div class="form-group">
<br>
<button type="submit" class="btn btn-block btn-primary btn-lg">Submit</button>
</div>
</div>
</form>
</div>
</div>
{% endblock %}
Without going into the details here, this uses a combination of HTML and django-widget-tweaks to render your form field.
Writing the JS instructions¶
Back on the transient_detail.html page, we need to add some scripting in the
{% block scripts %} section so that the form gets sent to our view. This does require a
few dependencies that have already been added to the detail page.
Here’s the simple function that will do most of the work, pulling from the labeled form ID and adding in the transient primary key so that the view will automatically know which transient to associate the TransientFollowup entry with. We could be fancier here, but for now I’m just going to see if the JSON data contains any errors that we need to alert the user about. If not, we will reload the page and our new followup request will be added:
<script src="{% static 'YSE_App/bower_components/bootstrap-timepicker/js/bootstrap-timepicker.js' %}"></script>
<script>
$('#automated_spectrum_request').on('submit', function(event){
event.preventDefault();
automated_spectrum_request();
});
function automated_spectrum_request() {
// Grab the form, and associate it with the current transient detail page
var data = $('#automated_spectrum_request').serialize()
var transient_id = $('#transient_pk').val()
data = (data + "&transient=" + transient_id)
//alert(data)
$.ajax({
url : "{% url 'automated_spectrum_request' %}", // the endpoint
type : "POST", // http method
data : data, // data sent with the post request
// handle a successful response
success : function(json) {
var errors = json.data["errors"]
// Construct HTML to append container
if (errors){
alert(errors)
} else {
location.reload();
}
},
// handle a non-successful response
error : function(xhr,errmsg,err) {
alert(xhr.status + ": " + xhr.responseText);
}
});
}
</script>
Then, between the scripts tags we need to render the calendars so that the user can select the dates in which the request is valid.:
$('#automated_spectrum_date_range').daterangepicker({
timePicker24Hour: true,
timePicker: true,
timePickerIncrement: 1,
format: 'MM/DD/YYYY HH:mm',
locale: {
format: 'MM/DD/YYYY HH:mm'
}
});
fdr_spec_picker = $('#automated_spectrum_date_range').data('daterangepicker')
$('#spectrum_valid_start').val(fdr_spec_picker.startDate.format("YYYY-MM-DD HH:mm:00"))
$('#spectrum_valid_stop').val(fdr_spec_picker.endDate.format("YYYY-MM-DD HH:mm:00"))
$('#automated_spectrum_date_range').on('apply.daterangepicker', function(ev, picker) {
$('#spectrum_valid_start').val(picker.startDate.format("YYYY-MM-DD HH:mm:00"))
$('#spectrum_valid_stop').val(picker.endDate.format("YYYY-MM-DD HH:mm:00"))
});
Last but not least, we need to make sure the form has a valid CSRF token:
var csrftoken = getCookie('csrftoken');
/*
The functions below will create a header with csrftoken
*/
function csrfSafeMethod(method) {
// these HTTP methods do not require CSRF protection
return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
}
function sameOrigin(url) {
// test that a given url is a same-origin URL
// url could be relative or scheme relative or absolute
var host = document.location.host; // host + port
var protocol = document.location.protocol;
var sr_origin = '//' + host;
var origin = protocol + sr_origin;
// Allow absolute or scheme relative URLs to same origin
return (url == origin || url.slice(0, origin.length + 1) == origin + '/') ||
(url == sr_origin || url.slice(0, sr_origin.length + 1) == sr_origin + '/') ||
// or any other URL that isn't scheme relative or absolute i.e relative.
!(/^(\/\/|http:|https:).*/.test(url));
}
$.ajaxSetup({
beforeSend: function(xhr, settings) {
if (!csrfSafeMethod(settings.type) && sameOrigin(settings.url)) {
// Send the token to same-origin, relative URLs only.
// Send the token only if the method warrants CSRF protection
// Using the CSRFToken value acquired earlier
xhr.setRequestHeader("X-CSRFToken", csrftoken);
}
}
});
});
Finishing the YSE-PZ Form View¶
Okay! We’re almost there. Now we need to use the data returned by the user to create a TransientFollowup object, and then use Charlie’s code to ping the LCOGT or SOAR API.
Here’s the full view:
class AddAutomatedSpectrumRequestFormView(FormView):
form_class = AutomatedSpectrumRequest
template_name = 'YSE_App/form_snippets/spectrum_request_form.html'
success_url = '/form-success/'
def form_invalid(self, form):
response = super(AddAutomatedSpectrumRequestFormView, self).form_invalid(form)
if self.request.is_ajax():
return JsonResponse(form.errors, status=400)
else:
return response
def form_valid(self, form):
response = super(AddAutomatedSpectrumRequestFormView, self).form_valid(form)
if self.request.is_ajax():
tfdict = {}
# some hard-coded logic
if 'goodman' in form.cleaned_data['instrument'].name.lower():
resource = ClassicalResource.objects.filter(telescope__name=form.cleaned_data['instrument'].telescope).\
filter(principal_investigator__name='Dimitriadis')
else:
resource = ToOResource.objects.filter(telescope__name=form.cleaned_data['instrument'].telescope) #.\
#filter(principal_investigator__name='Kilpatrick')
# make sure the dates line up, with a +/-1 day window to make life easier
resource = resource.filter(Q(begin_date_valid__lt=form.cleaned_data['spectrum_valid_start']+datetime.timedelta(1)) &
Q(end_date_valid__gt=form.cleaned_data['spectrum_valid_stop']-datetime.timedelta(1)))
if not len(resource):
data_dict = {'errors':'could not find a matching resource, make sure the dates are valid and the program is still active!',
'errorflag':1}
data = {
'data':data_dict,
'message': "Successfully submitted form data.",
}
return JsonResponse(data)
else:
resource = resource[0]
status = FollowupStatus.objects.get(name='Requested')
if 'goodman' in form.cleaned_data['instrument'].name.lower():
tf = TransientFollowup(status=status,valid_start=form.cleaned_data['spectrum_valid_start'],
valid_stop=form.cleaned_data['spectrum_valid_stop'],classical_resource=resource,
transient=form.cleaned_data['transient'],created_by=self.request.user,modified_by=self.request.user)
else:
tf = TransientFollowup(status=status,valid_start=form.cleaned_data['spectrum_valid_start'],
valid_stop=form.cleaned_data['spectrum_valid_stop'],too_resource=resource,
transient=form.cleaned_data['transient'],created_by=self.request.user,modified_by=self.request.user)
tf.save()
# now charlie's code
lcogt.main(
tf.transient.name,tf.transient.ra,tf.transient.dec,form.cleaned_data['exp_time'],
form.cleaned_data['instrument'].telescope.name)
data_dict = {'errors':'',
'errorflag':0}
data = {
'data':data_dict,
'message': "Successfully submitted form data.",
}
return JsonResponse(data)
else:
return response
Final Thoughts¶
Okay! I didn’t really explain everything, but this basic procedure can be used to build any form you want and trigger some action on the backend. There are simpler ways depending on what you want to do, but this template is a good way to proceed especially if you need to add new entries to the database.