from io import StringIO
import logging
from urllib.parse import urlencode, urlparse
from django.conf import settings
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.models import Group
from django.core.cache import cache
from django.core.cache.utils import make_template_fragment_key
from django.core.management import call_command
from django.http import HttpResponseRedirect
from django.shortcuts import redirect
from django.urls import reverse, reverse_lazy
from django.utils.safestring import mark_safe
from django.views.generic import View, ListView
from django.views.generic.base import RedirectView
from django.views.generic.detail import DetailView
from django.views.generic.edit import CreateView, DeleteView, FormView
from django_filters.views import FilterView
from guardian.shortcuts import assign_perm, get_objects_for_user
from tom_common.hooks import run_hook
from tom_common.hints import add_hint
from tom_common.mixins import Raise403PermissionRequiredMixin
from tom_dataproducts.models import DataProduct, DataProductGroup, REDUCED_DATUM_MODELS
from tom_dataproducts.exceptions import InvalidFileFormatException
from tom_dataproducts.forms import AddProductToGroupForm, DataProductUploadForm, DataShareForm
from tom_dataproducts.filters import DataProductFilter
from tom_dataproducts.data_processor import run_data_processor
from tom_observations.models import ObservationRecord
from tom_observations.facility import get_service_class
from tom_dataproducts.sharing import (share_data_with_tom, sharing_feedback_handler,
download_data)
import tom_dataproducts.single_target_data_service.single_target_data_service as stds
from tom_targets.models import Target
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
def _delete_reduced_datums_for_product(dp):
for model in REDUCED_DATUM_MODELS:
model.objects.filter(data_product=dp).delete()
[docs]
class DataProductSaveView(LoginRequiredMixin, View):
"""
View that handles saving a ``DataProduct`` generated by an observation. Requires authentication.
"""
[docs]
def post(self, request, *args, **kwargs):
"""
Method that handles POST requests for the ``DataProductSaveView``. Gets the observation facility that created
the data and saves the selected data products as ``DataProduct`` objects. Redirects to the
``ObservationDetailView`` for the specific ``ObservationRecord``.
:param request: Django POST request object
:type request: HttpRequest
"""
service_class = get_service_class(request.POST['facility'])
observation_record = ObservationRecord.objects.get(pk=kwargs['pk'])
products = request.POST.getlist('products')
if not products:
messages.warning(request, 'No products were saved, please select at least one dataproduct')
elif products[0] == 'ALL':
products = service_class().save_data_products(observation_record)
messages.success(request, 'Saved all available data products')
else:
total_saved_products = []
for product in products:
saved_products = service_class().save_data_products(
observation_record,
product
)
total_saved_products += saved_products
run_hook('data_product_post_save', saved_products)
messages.success(
request,
'Successfully saved: {0}'.format('\n'.join(
[str(p) for p in saved_products]
))
)
run_hook('multiple_data_products_post_save', total_saved_products)
return redirect(reverse(
'tom_observations:detail',
kwargs={'pk': observation_record.id})
)
[docs]
class SingleTargetDataServiceQueryView(LoginRequiredMixin, FormView):
"""
View that handles queries for single target data services
"""
template_name = 'tom_dataproducts/single_target_data_service_form.html'
[docs]
def get_target_id(self):
"""
Parses the target id from the query parameters.
"""
if self.request.method == 'GET':
return self.request.GET.get('target_id')
elif self.request.method == 'POST':
return self.request.POST.get('target_id')
[docs]
def get_target(self):
"""
Gets the target for observing from the database
:returns: target for observing
:rtype: Target
"""
return Target.objects.get(pk=self.get_target_id())
[docs]
def get_service(self):
"""
Gets the single target data service that you want to query
"""
return self.kwargs['service']
[docs]
def get_service_class(self):
"""
Gets the single target data service class
"""
return stds.get_service_class(self.get_service())
[docs]
def get_context_data(self, *args, **kwargs):
"""
Adds the target to the context object.
"""
context = super().get_context_data(*args, **kwargs)
# give the service class a chance to add to the context data
service_class = self.get_service_class()
service_class_context_data = service_class().get_context_data()
context.update(service_class_context_data)
context['target'] = self.get_target()
context['query_form'] = self.get_form_class()(initial=self.get_initial())
return context
[docs]
def get_initial(self):
"""
Populates the form with initial data including service name and target id
"""
initial = super().get_initial()
if not self.get_target_id():
raise Exception('Must provide target_id')
initial['target_id'] = self.get_target_id()
initial['service'] = self.get_service()
initial.update(self.request.GET.dict())
return initial
[docs]
def post(self, request, *args, **kwargs):
form = self.get_form()
if form.is_valid():
service = self.get_service_class()()
try:
service.query_service(form.cleaned_data)
except stds.SingleTargetDataServiceException as e:
form.add_error(None, f"Problem querying single target data service: {repr(e)}")
return self.form_invalid(form)
messages.info(self.request, service.get_success_message())
return redirect(
reverse('tom_targets:detail', kwargs={'pk': self.get_target_id()}) + '?tab=photometry'
)
else:
return self.form_invalid(form)
[docs]
class DataProductUploadView(LoginRequiredMixin, FormView):
"""
View that handles manual upload of DataProducts. Requires authentication.
"""
form_class = DataProductUploadForm
[docs]
class DataProductDeleteView(Raise403PermissionRequiredMixin, DeleteView):
"""
View that handles the deletion of a ``DataProduct``. Requires authentication.
"""
model = DataProduct
permission_required = 'tom_dataproducts.delete_dataproduct'
success_url = reverse_lazy('home')
[docs]
def get_required_permissions(self, request=None):
if settings.TARGET_PERMISSIONS_ONLY:
return None
return super(Raise403PermissionRequiredMixin, self).get_required_permissions(request)
[docs]
def check_permissions(self, request):
if settings.TARGET_PERMISSIONS_ONLY:
return False
return super(Raise403PermissionRequiredMixin, self).check_permissions(request)
[docs]
def get_success_url(self):
"""
Gets the URL specified in the query params by "next" if it exists, otherwise returns the URL for home.
:returns: referer or the index URL
:rtype: str
"""
referer = self.request.GET.get('next', None)
referer = urlparse(referer).path if referer else '/'
return referer
[docs]
def get_context_data(self, *args, **kwargs):
"""
Adds the referer to the query parameters as "next" and returns the context dictionary.
:returns: context dictionary
:rtype: dict
"""
context = super().get_context_data(*args, **kwargs)
context['next'] = self.request.META.get('HTTP_REFERER', '/')
return context
[docs]
class DataProductListView(FilterView):
"""
View that handles the list of ``DataProduct`` objects.
"""
model = DataProduct
template_name = 'tom_dataproducts/dataproduct_list.html'
paginate_by = 25
filterset_class = DataProductFilter
strict = False
[docs]
def get_queryset(self):
"""
Gets the set of ``DataProduct`` objects that the user has permission to view.
:returns: Set of ``DataProduct`` objects
:rtype: QuerySet
"""
if settings.TARGET_PERMISSIONS_ONLY:
return super().get_queryset().filter(
target__in=get_objects_for_user(self.request.user, f'{Target._meta.app_label}.view_target')
)
else:
return get_objects_for_user(self.request.user, 'tom_dataproducts.view_dataproduct')
[docs]
def get_context_data(self, *args, **kwargs):
"""
Adds the set of ``DataProductGroup`` objects to the context dictionary.
:returns: context dictionary
:rtype: dict
"""
context = super().get_context_data(*args, **kwargs)
context['product_groups'] = DataProductGroup.objects.all()
return context
[docs]
class DataProductFeatureView(View):
"""
View that handles the featuring of ``DataProduct``s. A featured ``DataProduct`` is displayed on the
``TargetDetailView``.
"""
[docs]
def get(self, request, *args, **kwargs):
"""
Method that handles the GET requests for this view. Sets all other ``DataProduct``s to unfeatured in the
database, and sets the specified ``DataProduct`` to featured. Caches the featured image. Deletes previously
featured images from the cache.
"""
product_id = kwargs.get('pk', None)
product = DataProduct.objects.get(pk=product_id)
try:
current_featured = DataProduct.objects.filter(
featured=True,
data_product_type=product.data_product_type,
target=product.target
)
for featured_image in current_featured:
featured_image.featured = False
featured_image.save()
featured_image_cache_key = make_template_fragment_key(
'featured_image',
str(featured_image.target.id)
)
cache.delete(featured_image_cache_key)
except DataProduct.DoesNotExist:
pass
product.featured = True
product.save()
return redirect(reverse(
'tom_targets:detail',
kwargs={'pk': request.GET.get('target_id')})
)
[docs]
class DataShareView(FormView):
"""
View that handles the sharing of data with another TOM.
"""
form_class = DataShareForm
[docs]
def post(self, request, *args, **kwargs):
"""
Method that handles the POST requests for sharing data.
Handles Data Products and All the data of a type for a target as well as individual Reduced Datums.
Share with TOM.
"""
data_share_form = DataShareForm(request.POST, request.FILES)
if data_share_form.is_valid():
form_data = data_share_form.cleaned_data
share_destination = form_data['share_destination']
product_id = kwargs.get('dp_pk', None)
target_id = kwargs.get('tg_pk', None)
# Check if data points have been selected.
selected_data = request.POST.getlist("share-box")
# Check Destination
if share_destination == 'download':
return download_data(form_data, selected_data=selected_data)
else:
response = share_data_with_tom(share_destination, form_data, product_id, target_id, selected_data)
sharing_feedback_handler(response, self.request)
return redirect(reverse('tom_targets:detail', kwargs={'pk': request.POST.get('target')}))
[docs]
class DataProductGroupDetailView(DetailView):
"""
View that handles the viewing of a specific ``DataProductGroup``.
"""
model = DataProductGroup
[docs]
def post(self, request, *args, **kwargs):
"""
Handles the POST request for this view.
"""
group = self.get_object()
for product in request.POST.getlist('products'):
group.dataproduct_set.remove(DataProduct.objects.get(pk=product))
group.save()
return redirect(reverse(
'tom_dataproducts:group-detail',
kwargs={'pk': group.id})
)
[docs]
class DataProductGroupListView(ListView):
"""
View that handles the display of all ``DataProductGroup`` objects.
"""
model = DataProductGroup
[docs]
class DataProductGroupCreateView(LoginRequiredMixin, CreateView):
"""
View that handles the creation of a new ``DataProductGroup``.
"""
model = DataProductGroup
success_url = reverse_lazy('tom_dataproducts:group-list')
fields = ['name']
[docs]
class DataProductGroupDeleteView(LoginRequiredMixin, DeleteView):
"""
View that handles the deletion of a ``DataProductGroup``. Requires authentication.
"""
success_url = reverse_lazy('tom_dataproducts:group-list')
model = DataProductGroup
[docs]
class DataProductGroupDataView(LoginRequiredMixin, FormView):
"""
View that handles the addition of ``DataProduct``s to a ``DataProductGroup``. Requires authentication.
"""
form_class = AddProductToGroupForm
template_name = 'tom_dataproducts/add_product_to_group.html'
[docs]
class UpdateReducedDataView(LoginRequiredMixin, RedirectView):
"""
View that handles the updating of reduced data tied to a ``DataProduct`` that was ingested from a
dataservice. The ReducedDatum.source must match the DataService.name Requires authentication.
"""
[docs]
def get(self, request, *args, **kwargs):
"""
Method that handles the GET requests for this view. Calls the management command to update the reduced data and
adds a hint using the messages framework about automation.
"""
# QueryDict is immutable, and we want to append the remaining params to the redirect URL
query_params = request.GET.copy()
target_id = query_params.pop('target_id', None)
out = StringIO()
if target_id:
if isinstance(target_id, list):
target_id = target_id[-1]
call_command('updatereduceddata', target_id=target_id, stdout=out)
else:
call_command('updatereduceddata', stdout=out)
messages.info(request, out.getvalue())
add_hint(request, mark_safe(
'Did you know updating observation statuses can be automated? Learn how in '
'<a href=https://tom-toolkit.readthedocs.io/en/stable/customization/automation.html>'
'the docs.</a>'))
return HttpResponseRedirect(f'{self.get_redirect_url(*args, **kwargs)}?{urlencode(query_params)}')
[docs]
def get_redirect_url(self, *args, **kwargs):
"""
Returns redirect URL as specified in the HTTP_REFERER field of the request.
:returns: referer
:rtype: str
"""
referer = self.request.META.get('HTTP_REFERER', '/')
return referer