Source code for tom_dataproducts.views

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_form_class(self): """ Gets the single target data service form class """ return self.get_service_class()().get_form()
[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] def get_form(self, *args, **kwargs): form = super().get_form(*args, **kwargs) if not settings.TARGET_PERMISSIONS_ONLY: if self.request.user.is_superuser: form.fields['groups'].queryset = Group.objects.all() else: form.fields['groups'].queryset = self.request.user.groups.all() return form
[docs] def form_valid(self, form): """ Runs after ``DataProductUploadForm`` is validated. Saves each ``DataProduct`` and calls ``run_data_processor`` on each saved file. Redirects to the previous page. """ target = form.cleaned_data['target'] if not target: observation_record = form.cleaned_data['observation_record'] target = observation_record.target else: observation_record = None dp_type = form.cleaned_data['data_product_type'] data_product_files = self.request.FILES.getlist('files') successful_uploads = [] for f in data_product_files: dp = DataProduct( target=target, observation_record=observation_record, data=f, product_id=None, data_product_type=dp_type ) dp.save() try: run_hook('data_product_post_upload', dp) reduced_data = run_data_processor(dp) if not settings.TARGET_PERMISSIONS_ONLY: for group in form.cleaned_data['groups']: assign_perm('tom_dataproducts.view_dataproduct', group, dp) assign_perm('tom_dataproducts.delete_dataproduct', group, dp) for datum in reduced_data: perm = f'tom_dataproducts.view_{type(datum).__name__.lower()}' assign_perm(perm, group, datum) successful_uploads.append(str(dp)) except InvalidFileFormatException as iffe: _delete_reduced_datums_for_product(dp) dp.delete() messages.error( self.request, f'File format invalid for file {str(dp)} -- error was {iffe}' ) except Exception as e: _delete_reduced_datums_for_product(dp) dp.delete() messages.error(self.request, f'There was a problem processing your file: {str(dp)} -- Error: {e}') if successful_uploads: messages.success( self.request, 'Successfully uploaded: {0}'.format('\n'.join([p for p in successful_uploads])) ) return redirect(form.cleaned_data.get('referrer', '/'))
[docs] def form_invalid(self, form): """ Adds errors to Django messaging framework in the case of an invalid form and redirects to the previous page. """ # TODO: Format error messages in a more human-readable way messages.error(self.request, 'There was a problem uploading your file: {}'.format(form.errors.as_json())) return redirect(form.cleaned_data.get('referrer', '/'))
[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 form_valid(self, form): """ Method that handles DELETE requests for this view. It performs the following actions in order: 1. Deletes all ``ReducedDatum`` objects associated with the ``DataProduct``. 2. Deletes the file referenced by the ``DataProduct``. 3. Deletes the ``DataProduct`` object from the database. :param form: Django form instance containing the data for the DELETE request. :type form: django.forms.Form :return: HttpResponseRedirect to the success URL. :rtype: HttpResponseRedirect """ # Fetch the DataProduct object data_product = self.get_object() # Delete associated ReducedDatum objects _delete_reduced_datums_for_product(data_product) # Delete the file reference. data_product.data.delete() # Delete the `DataProduct` object from the database. data_product.delete() return HttpResponseRedirect(self.get_success_url())
[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 get_form(self, *args, **kwargs): # TODO: Add permissions form = super().get_form(*args, **kwargs) return form
[docs] def form_invalid(self, form): """ Adds errors to Django messaging framework in the case of an invalid form and redirects to the previous page. """ # TODO: Format error messages in a more human-readable way messages.error(self.request, 'There was a problem sharing your Data: {}'.format(form.errors.as_json())) return redirect(form.cleaned_data.get('referrer', '/'))
[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] def form_valid(self, form): """ Runs after form validation. Adds the specified ``DataProduct`` objects to the group. :param form: Form with data products and group information :type form: AddProductToGroupForm """ group = form.cleaned_data['group'] group.dataproduct_set.add(*form.cleaned_data['products']) group.save() return redirect(reverse( 'tom_dataproducts:group-detail', kwargs={'pk': group.id}) )
[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