Skip to content

Commit 398ab00

Browse files
authored
Merge pull request #1426 from TOMToolkit/1396-update-reduced-datums-for-existing-target-via-data-service
1396 update reduced datums for existing target via data service
2 parents aa52509 + 18b7fb4 commit 398ab00

16 files changed

Lines changed: 618 additions & 102 deletions

File tree

35.2 KB
Loading
23.1 KB
Loading

docs/brokers/create_dataservice.rst

Lines changed: 355 additions & 34 deletions
Large diffs are not rendered by default.

docs/customization/customize_templates.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ Editing this file will allow you to overwrite any of the css used at the base le
181181
styling established by TOMToolkit themes such as "Dark Mode".
182182

183183
The TOMToolkit has several built in variables that allow you to establish a theme for your TOM. You can see these in
184-
`root` dictionay in yoyr ``custom.css``. Altering these will change the appearance of large portions of the website.
184+
`root` dictionary in your ``custom.css``. Altering these will change the appearance of large portions of the website.
185185

186186
As an example, let's change the background color from white (#ffffff) to an off-white (#efead6).
187187
Add the following in your ``custom.css`` after the comments:

tom_common/static/tom_common/css/main.css

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,11 @@ td, th {
141141
float: left;
142142
}
143143

144+
.bottom {
145+
position: absolute;
146+
bottom: 20%;
147+
}
148+
144149
/* Create copy button class */
145150
.copy-button {
146151
opacity: 0.75; /* Make it slightly transparent */

tom_dataservices/data_services/tns.py

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,10 @@ def __init__(self, *args, **kwargs):
7474
),
7575
)
7676

77+
def get_simple_form_partial(self):
78+
"""Returns a path to a simplified bare-minimum partial form that can be used to access the DataService."""
79+
return 'tom_dataservices/tns/partials/tns_simple_form.html'
80+
7781

7882
class TNSDataService(DataService):
7983
"""
@@ -98,14 +102,6 @@ class TNSDataService(DataService):
98102
info_url = 'https://tom-toolkit.readthedocs.io/en/latest/api/tom_alerts/brokers.html#module-tom_alerts.brokers.tns'
99103
query_results_table = 'tom_dataservices/tns/partials/tns_query_results_table.html'
100104

101-
def get_simple_form_partial(self):
102-
"""Returns a path to a simplified bare-minimum partial form that can be used to access the DataService."""
103-
return 'tom_dataservices/tns/partials/tns_simple_form.html'
104-
105-
# def get_advanced_form_partial(self):
106-
# """Returns a path to a full or advanced partial form that can be used to access the DataService."""
107-
# return 'tom_dataservices/tns/partials/tns_advanced_form.html'
108-
109105
@classmethod
110106
def urls(cls, **kwargs) -> dict:
111107
"""Dictionary of URLS for the TNS API."""

tom_dataservices/dataservices.py

Lines changed: 83 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
11
from abc import ABC, abstractmethod
22
import logging
3-
from typing import List, Tuple
3+
from typing import List
4+
from urllib.parse import urlencode
45

56
from django.conf import settings
67
from django.apps import apps
78
from django.utils.module_loading import import_string
9+
from django.db import IntegrityError
10+
from django.utils.safestring import mark_safe
11+
from django.contrib import messages
12+
from django.urls import reverse
813

9-
from tom_targets.models import TargetName
14+
from guardian.shortcuts import assign_perm
15+
16+
from tom_targets.models import TargetName, Target
1017

1118
logger = logging.getLogger(__name__)
1219

@@ -119,6 +126,22 @@ def build_query_parameters(self, parameters, **kwargs):
119126
"""Builds the query parameters from the form data"""
120127
raise NotImplementedError(f'build_query_parameters method has not been implemented for {self.name}')
121128

129+
# Include this method if you wish for the TOM to be able to query data for an individual Target.
130+
# def build_query_parameters_from_target(self, target, **kwargs):
131+
# """
132+
# This is a method that builds query parameters based on an existing target object that will be recognized by
133+
# `query_service()`.
134+
# This can be done by either by re-creating the form fields set by the Data Service Form and then calling
135+
# `self.build_query_parameters()` with the results, or we can reproduce a limited set of parameters uniquely for
136+
# a target query.
137+
138+
# :param target: A target object to be queried
139+
# :return: query_parameters (usually a dict) that can be understood by `query_service()`
140+
# """
141+
# raise NotImplementedError('build_query_parameters_from_target method has not been implemented' +
142+
# f'for {self.name}.'
143+
# )
144+
122145
def build_headers(self, *args, **kwargs):
123146
"""Builds the headers for the query"""
124147
return {}
@@ -193,31 +216,33 @@ def get_success_message(self, **kwargs):
193216
"""Returns a success message to display in the UI after making the query."""
194217
return "Query completed successfully."
195218

196-
def get_simple_form_partial(self):
197-
"""Returns a path to a simplified bare-minimum partial form that can be used to access the DataService."""
198-
return None
199-
200-
def get_advanced_form_partial(self):
201-
"""Returns a path to a full or advanced partial form that can be used to access the DataService."""
202-
return None
203-
204219
def query_forced_photometry(self, query_parameters, **kwargs):
205220
"""Set up and run a specialized query for a DataService’s forced photometry service."""
206-
return self.query_service(query_parameters, **kwargs)
221+
raise NotImplementedError(f'query_forced_photometry method has not been implemented for {self.name}')
207222

208223
def query_photometry(self, query_parameters, **kwargs):
209224
"""Set up and run a specialized query for a DataService’s photometry service."""
210-
return self.query_service(query_parameters, **kwargs)
225+
raise NotImplementedError(f'query_photometry method has not been implemented for {self.name}')
211226

212227
def query_spectroscopy(self, query_parameters, **kwargs):
213228
"""Set up and run a specialized query for a DataService’s spectroscopy service."""
214-
return self.query_service(query_parameters, **kwargs)
229+
raise NotImplementedError(f'query_spectroscopy method has not been implemented for {self.name}')
215230

216-
def query_reduced_data(self, query_parameters, **kwargs):
231+
def query_reduced_data(self, target, **kwargs):
217232
"""Set up and run a specialized query to retrieve Reduced Datums from a Data Service"""
218-
phot_results = self.query_photometry(query_parameters, **kwargs)
219-
spec_results = self.query_spectroscopy(query_parameters, **kwargs)
220-
forced_phot_results = self.query_forced_photometry(query_parameters, **kwargs)
233+
query_parameters = self.build_query_parameters_from_target(target)
234+
try:
235+
phot_results = self.query_photometry(query_parameters, **kwargs)
236+
except NotImplementedError:
237+
phot_results = []
238+
try:
239+
spec_results = self.query_spectroscopy(query_parameters, **kwargs)
240+
except NotImplementedError:
241+
spec_results = []
242+
try:
243+
forced_phot_results = self.query_forced_photometry(query_parameters, **kwargs)
244+
except NotImplementedError:
245+
forced_phot_results = []
221246
return {'photometry': phot_results,
222247
'spectroscopy': spec_results,
223248
'forced_photometry': forced_phot_results}
@@ -242,7 +267,7 @@ def query_targets(self, query_parameters, **kwargs) -> List[dict]:
242267
243268
:param query_parameters: This is the output from build_query_parameters()
244269
:return: A list of dictionaries describing the resulting targets. Include 'reduced_datums' and/or 'aliases' as
245-
keys in this dictionary to add associated data and alternate names without perfoming additional queries.
270+
keys in this dictionary to add associated data and alternate names without performing additional queries.
246271
:rtype: List[dict]
247272
"""
248273
return [{}]
@@ -271,19 +296,28 @@ def to_reduced_datums(self, target, data_results=None, **kwargs):
271296
of query_reduced_data() to create_reduced_datums_from_query()
272297
:param target: Target object to associate with the ReducedDatum
273298
:param data_results: Query results from the DataService storing observation data. This should be a dictionary
274-
with each key being a data_type (i.e. Photometry, Spectroscopy, etc.)
299+
with each key being a data_type (i.e. Photometry, Spectroscopy, etc.)
275300
"""
276301
if not data_results:
277302
raise MissingDataException('No Reduced Data dictionary found.')
303+
reduced_datum_list = []
278304
for key in data_results.keys():
279-
self.create_reduced_datums_from_query(target, data_results[key], key, **kwargs)
280-
return
305+
reduced_datum_list += self.create_reduced_datums_from_query(target, data_results[key], key, **kwargs)
306+
return reduced_datum_list
307+
308+
def create_reduced_datums_from_query(self, target, data: List, data_type=None, **kwargs) -> List:
309+
"""
310+
Create and save new reduced_datums of the appropriate data_type from the query results
311+
Be sure to use `ReducedDatum.objects.get_or_create()` when creating new objects.
281312
282-
def create_reduced_datums_from_query(self, target, data=None, data_type=None, **kwargs):
283-
"""Create and save new reduced_datums of the appropriate data_type from the query results"""
313+
:param target: Target Object to be associated with the reduced data
314+
:param data: List of data dictionaries of the appropriate `data_type`
315+
:param data_type: An appropriate data type as listed in tom_dataproducts.models.DATA_TYPE_CHOICES
316+
:return: List of Reduced datums (either retrieved or created)
317+
"""
284318
raise NotImplementedError
285319

286-
def to_target(self, target_result=None, **kwargs) -> Tuple[dict, dict, dict]:
320+
def to_target(self, target_result=None, **kwargs):
287321
"""
288322
Upper level function to create a new target from the query results
289323
This method is not intended to be extended. This method passes a single instance of the output
@@ -292,15 +326,38 @@ def to_target(self, target_result=None, **kwargs) -> Tuple[dict, dict, dict]:
292326
Intended usage: Call to_target on each element of the target_data list of dictionaries from query_target.
293327
(see views.py::CreateTargetFromQueryView)
294328
:param target_results: Dictionary containing target information.
295-
:returns: Target object, dictionary of target_extras, and list of aliases
329+
:returns: Target object
296330
"""
297331
if not target_result:
298332
raise MissingDataException('No query results. Did you call query_service()?')
299333
else:
300334
target = self.create_target_from_query(target_result, **kwargs)
301335
extras = self.create_target_extras_from_query(target_result, **kwargs)
302336
aliases = self.create_aliases_from_query(target_result.get('aliases', []), **kwargs)
303-
return target, extras, aliases
337+
338+
request = kwargs.get('request')
339+
try:
340+
target.save(extras=extras, names=aliases)
341+
# Give the user access to the target they created
342+
if request:
343+
target.give_user_access(request.user)
344+
for group in request.user.groups.all():
345+
assign_perm('tom_targets.view_target', group, target)
346+
assign_perm('tom_targets.change_target', group, target)
347+
assign_perm('tom_targets.delete_target', group, target)
348+
except IntegrityError:
349+
# errors.append(target.name)
350+
target = Target.objects.get(name=target.name)
351+
messages.warning(request,
352+
mark_safe(
353+
f"""The target,
354+
<a href="{reverse('targets:detail', kwargs={'pk': target.id})}">
355+
{target.name}</a> already exists, any new data has been ingested.
356+
You can <a href="{reverse('targets:create') + '?' +
357+
urlencode(target.as_dict())}">create</a> a new target anyway.
358+
""")
359+
)
360+
return target
304361

305362
def create_target_from_query(self, target_result, **kwargs):
306363
"""Create a new target from a single instance of the target results.

tom_dataservices/forms.py

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
1-
from crispy_forms.layout import Layout
1+
from typing import List
2+
23
from django import forms
4+
from django.urls import reverse
35
from crispy_forms.helper import FormHelper
6+
from crispy_forms.layout import ButtonHolder, Column, Layout, Row, Submit
47

58
from tom_dataservices.models import DataServiceQuery
9+
from tom_dataservices.dataservices import get_data_service_classes
10+
from tom_targets.models import Target
611

712

813
class BaseQueryForm(forms.Form):
@@ -27,11 +32,23 @@ def __init__(self, *args, **kwargs):
2732
self.helper.form_tag = False
2833
self.helper.layout = self.get_layout()
2934

35+
def simple_fields(self) -> List[str]:
36+
"""Return List of fields to be included in the simple form."""
37+
return []
38+
3039
def get_layout(self):
31-
exclude = ["query_save", "query_name"]
40+
exclude = ["query_save", "query_name"] + self.simple_fields()
3241
field_keys = [f for f in self.fields.keys() if f not in exclude]
3342
return Layout(*field_keys)
3443

44+
def get_simple_form_partial(self):
45+
"""Returns a path to a simplified bare-minimum partial form that can be used to access the DataService."""
46+
return None
47+
48+
def get_advanced_form_partial(self):
49+
"""Returns a path to a full or advanced partial form that can be used to access the DataService."""
50+
return None
51+
3552
def save(self, query_id=None):
3653
"""
3754
Saves the form data as a DataServiceQuery object
@@ -48,3 +65,36 @@ def save(self, query_id=None):
4865
query.parameters = self.cleaned_data
4966
query.save()
5067
return query
68+
69+
70+
class UpdateDataFromDataServiceForm(forms.Form):
71+
target = forms.ModelChoiceField(
72+
Target.objects.all(),
73+
widget=forms.HiddenInput(),
74+
required=False
75+
)
76+
data_service = forms.ChoiceField(required=True, choices=[])
77+
78+
def __init__(self, *args, **kwargs):
79+
super().__init__(*args, **kwargs)
80+
data_service_list = get_data_service_classes()
81+
data_service_choices = []
82+
for name, data_service in data_service_list.items():
83+
if hasattr(data_service, 'build_query_parameters_from_target'):
84+
data_service_choices.append((name, name))
85+
self.fields['data_service'].choices = data_service_choices
86+
self.helper = FormHelper()
87+
self.helper.form_action = reverse('tom_dataservices:update-data')
88+
self.helper.layout = Layout(
89+
'target',
90+
Row(
91+
Column(
92+
'data_service'
93+
),
94+
Column(
95+
ButtonHolder(
96+
Submit('Update', 'Update Reduced Data'), css_class="bottom"
97+
)
98+
),
99+
)
100+
)
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{% load bootstrap4 %}
2+
3+
4+
{% for field in simple_fields %}
5+
{% bootstrap_field field %}
6+
{% endfor %}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{% load bootstrap4 crispy_forms_tags static %}
2+
3+
<h4>Get Reduced Data from Data Service</h4>
4+
{% csrf_token %}
5+
{% if update_from_dataservice_form.fields.data_service.choices %}
6+
{% crispy update_from_dataservice_form %}
7+
{% else %}
8+
You don't have any Data Services installed capable of retrieving data for this target.
9+
{% endif %}

0 commit comments

Comments
 (0)