• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

TOMToolkit / tom_base / 6713509976

31 Oct 2023 11:13PM UTC coverage: 86.773% (+0.7%) from 86.072%
6713509976

push

github-actions

web-flow
Merge pull request #699 from TOMToolkit/dev

Multi-Feature Merge. Please Review Carefully.

795 of 795 new or added lines in 39 files covered. (100.0%)

8253 of 9511 relevant lines covered (86.77%)

0.87 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

64.31
/tom_dataproducts/views.py
1
from io import StringIO
1✔
2
import logging
1✔
3
from urllib.parse import urlencode, urlparse
1✔
4

5
from django.conf import settings
1✔
6
from django.contrib import messages
1✔
7
from django.contrib.auth.mixins import LoginRequiredMixin
1✔
8
from django.contrib.auth.models import Group
1✔
9
from django.core.cache import cache
1✔
10
from django.core.cache.utils import make_template_fragment_key
1✔
11
from django.core.management import call_command
1✔
12
from django.http import HttpResponseRedirect
1✔
13
from django.shortcuts import redirect
1✔
14
from django.urls import reverse, reverse_lazy
1✔
15
from django.utils.safestring import mark_safe
1✔
16
from django.views.generic import View, ListView
1✔
17
from django.views.generic.base import RedirectView
1✔
18
from django.views.generic.detail import DetailView
1✔
19
from django.views.generic.edit import CreateView, DeleteView, FormView
1✔
20
from django_filters.views import FilterView
1✔
21
from guardian.shortcuts import assign_perm, get_objects_for_user
1✔
22

23
from tom_common.hooks import run_hook
1✔
24
from tom_common.hints import add_hint
1✔
25
from tom_common.mixins import Raise403PermissionRequiredMixin
1✔
26
from tom_dataproducts.models import DataProduct, DataProductGroup, ReducedDatum
1✔
27
from tom_dataproducts.exceptions import InvalidFileFormatException
1✔
28
from tom_dataproducts.forms import AddProductToGroupForm, DataProductUploadForm, DataShareForm
1✔
29
from tom_dataproducts.filters import DataProductFilter
1✔
30
from tom_dataproducts.data_processor import run_data_processor
1✔
31
from tom_observations.models import ObservationRecord
1✔
32
from tom_observations.facility import get_service_class
1✔
33
from tom_dataproducts.sharing import share_data_with_hermes, share_data_with_tom, sharing_feedback_handler
1✔
34
import tom_dataproducts.forced_photometry.forced_photometry_service as fps
1✔
35
from tom_targets.models import Target
1✔
36

37
logger = logging.getLogger(__name__)
1✔
38
logger.setLevel(logging.DEBUG)
1✔
39

40

41
class DataProductSaveView(LoginRequiredMixin, View):
1✔
42
    """
43
    View that handles saving a ``DataProduct`` generated by an observation. Requires authentication.
44
    """
45
    def post(self, request, *args, **kwargs):
1✔
46
        """
47
        Method that handles POST requests for the ``DataProductSaveView``. Gets the observation facility that created
48
        the data and saves the selected data products as ``DataProduct`` objects. Redirects to the
49
        ``ObservationDetailView`` for the specific ``ObservationRecord``.
50

51
        :param request: Django POST request object
52
        :type request: HttpRequest
53
        """
54
        service_class = get_service_class(request.POST['facility'])
1✔
55
        observation_record = ObservationRecord.objects.get(pk=kwargs['pk'])
1✔
56
        products = request.POST.getlist('products')
1✔
57
        if not products:
1✔
58
            messages.warning(request, 'No products were saved, please select at least one dataproduct')
×
59
        elif products[0] == 'ALL':
1✔
60
            products = service_class().save_data_products(observation_record)
×
61
            messages.success(request, 'Saved all available data products')
×
62
        else:
63
            total_saved_products = []
1✔
64
            for product in products:
1✔
65
                saved_products = service_class().save_data_products(
1✔
66
                    observation_record,
67
                    product
68
                )
69
                total_saved_products += saved_products
1✔
70
                run_hook('data_product_post_save', saved_products)
1✔
71
                messages.success(
1✔
72
                    request,
73
                    'Successfully saved: {0}'.format('\n'.join(
74
                        [str(p) for p in saved_products]
75
                    ))
76
                )
77
            run_hook('multiple_data_products_post_save', total_saved_products)
1✔
78
        return redirect(reverse(
1✔
79
            'tom_observations:detail',
80
            kwargs={'pk': observation_record.id})
81
        )
82

83

84
class ForcedPhotometryQueryView(LoginRequiredMixin, FormView):
1✔
85
    """
86
    View that handles queries for forced photometry services
87
    """
88
    template_name = 'tom_dataproducts/forced_photometry_form.html'
1✔
89

90
    def get_target_id(self):
1✔
91
        """
92
        Parses the target id from the query parameters.
93
        """
94
        if self.request.method == 'GET':
×
95
            return self.request.GET.get('target_id')
×
96
        elif self.request.method == 'POST':
×
97
            return self.request.POST.get('target_id')
×
98

99
    def get_target(self):
1✔
100
        """
101
        Gets the target for observing from the database
102

103
        :returns: target for observing
104
        :rtype: Target
105
        """
106
        return Target.objects.get(pk=self.get_target_id())
×
107

108
    def get_service(self):
1✔
109
        """
110
        Gets the forced photometry service that you want to query
111
        """
112
        return self.kwargs['service']
×
113

114
    def get_service_class(self):
1✔
115
        """
116
        Gets the forced photometry service class
117
        """
118
        return fps.get_service_class(self.get_service())
×
119

120
    def get_form_class(self):
1✔
121
        """
122
        Gets the forced photometry service form class
123
        """
124
        return self.get_service_class()().get_form()
×
125

126
    def get_context_data(self, *args, **kwargs):
1✔
127
        """
128
        Adds the target to the context object.
129
        """
130
        context = super().get_context_data(*args, **kwargs)
×
131
        context['target'] = self.get_target()
×
132
        context['query_form'] = self.get_form_class()(initial=self.get_initial())
×
133
        return context
×
134

135
    def get_initial(self):
1✔
136
        """
137
        Populates the form with initial data including service name and target id
138
        """
139
        initial = super().get_initial()
×
140
        if not self.get_target_id():
×
141
            raise Exception('Must provide target_id')
×
142
        initial['target_id'] = self.get_target_id()
×
143
        initial['service'] = self.get_service()
×
144
        initial.update(self.request.GET.dict())
×
145
        return initial
×
146

147
    def post(self, request, *args, **kwargs):
1✔
148
        form = self.get_form()
×
149
        if form.is_valid():
×
150
            service = self.get_service_class()()
×
151
            try:
×
152
                service.query_service(form.cleaned_data)
×
153
            except fps.ForcedPhotometryServiceException as e:
×
154
                form.add_error(None, f"Problem querying forced photometry service: {repr(e)}")
×
155
                return self.form_invalid(form)
×
156
            messages.info(self.request, service.get_success_message())
×
157
            return redirect(
×
158
                reverse('tom_targets:detail', kwargs={'pk': self.get_target_id()})
159
            )
160
        else:
161
            return self.form_invalid(form)
×
162

163

164
class DataProductUploadView(LoginRequiredMixin, FormView):
1✔
165
    """
166
    View that handles manual upload of DataProducts. Requires authentication.
167
    """
168
    form_class = DataProductUploadForm
1✔
169

170
    def get_form(self, *args, **kwargs):
1✔
171
        form = super().get_form(*args, **kwargs)
1✔
172
        if not settings.TARGET_PERMISSIONS_ONLY:
1✔
173
            if self.request.user.is_superuser:
1✔
174
                form.fields['groups'].queryset = Group.objects.all()
×
175
            else:
176
                form.fields['groups'].queryset = self.request.user.groups.all()
1✔
177
        return form
1✔
178

179
    def form_valid(self, form):
1✔
180
        """
181
        Runs after ``DataProductUploadForm`` is validated. Saves each ``DataProduct`` and calls ``run_data_processor``
182
        on each saved file. Redirects to the previous page.
183
        """
184
        target = form.cleaned_data['target']
1✔
185
        if not target:
1✔
186
            observation_record = form.cleaned_data['observation_record']
1✔
187
            target = observation_record.target
1✔
188
        else:
189
            observation_record = None
1✔
190
        dp_type = form.cleaned_data['data_product_type']
1✔
191
        data_product_files = self.request.FILES.getlist('files')
1✔
192
        successful_uploads = []
1✔
193
        for f in data_product_files:
1✔
194
            dp = DataProduct(
1✔
195
                target=target,
196
                observation_record=observation_record,
197
                data=f,
198
                product_id=None,
199
                data_product_type=dp_type
200
            )
201
            dp.save()
1✔
202
            try:
1✔
203
                run_hook('data_product_post_upload', dp)
1✔
204
                reduced_data = run_data_processor(dp)
1✔
205
                if not settings.TARGET_PERMISSIONS_ONLY:
1✔
206
                    for group in form.cleaned_data['groups']:
×
207
                        assign_perm('tom_dataproducts.view_dataproduct', group, dp)
×
208
                        assign_perm('tom_dataproducts.delete_dataproduct', group, dp)
×
209
                        assign_perm('tom_dataproducts.view_reduceddatum', group, reduced_data)
×
210
                successful_uploads.append(str(dp))
1✔
211
            except InvalidFileFormatException as iffe:
×
212
                ReducedDatum.objects.filter(data_product=dp).delete()
×
213
                dp.delete()
×
214
                messages.error(
×
215
                    self.request,
216
                    'File format invalid for file {0} -- error was {1}'.format(str(dp), iffe)
217
                )
218
            except Exception:
×
219
                ReducedDatum.objects.filter(data_product=dp).delete()
×
220
                dp.delete()
×
221
                messages.error(self.request, 'There was a problem processing your file: {0}'.format(str(dp)))
×
222
        if successful_uploads:
1✔
223
            messages.success(
1✔
224
                self.request,
225
                'Successfully uploaded: {0}'.format('\n'.join([p for p in successful_uploads]))
226
            )
227

228
        return redirect(form.cleaned_data.get('referrer', '/'))
1✔
229

230
    def form_invalid(self, form):
1✔
231
        """
232
        Adds errors to Django messaging framework in the case of an invalid form and redirects to the previous page.
233
        """
234
        # TODO: Format error messages in a more human-readable way
235
        messages.error(self.request, 'There was a problem uploading your file: {}'.format(form.errors.as_json()))
1✔
236
        return redirect(form.cleaned_data.get('referrer', '/'))
1✔
237

238

239
class DataProductDeleteView(Raise403PermissionRequiredMixin, DeleteView):
1✔
240
    """
241
    View that handles the deletion of a ``DataProduct``. Requires authentication.
242
    """
243
    model = DataProduct
1✔
244
    permission_required = 'tom_dataproducts.delete_dataproduct'
1✔
245
    success_url = reverse_lazy('home')
1✔
246

247
    def get_required_permissions(self, request=None):
1✔
248
        if settings.TARGET_PERMISSIONS_ONLY:
1✔
249
            return None
×
250
        return super(Raise403PermissionRequiredMixin, self).get_required_permissions(request)
1✔
251

252
    def check_permissions(self, request):
1✔
253
        if settings.TARGET_PERMISSIONS_ONLY:
1✔
254
            return False
1✔
255
        return super(Raise403PermissionRequiredMixin, self).check_permissions(request)
1✔
256

257
    def get_success_url(self):
1✔
258
        """
259
        Gets the URL specified in the query params by "next" if it exists, otherwise returns the URL for home.
260

261
        :returns: referer or the index URL
262
        :rtype: str
263
        """
264
        referer = self.request.GET.get('next', None)
1✔
265
        referer = urlparse(referer).path if referer else '/'
1✔
266
        return referer
1✔
267

268
    def form_valid(self, form):
1✔
269
        """
270
        Method that handles DELETE requests for this view. It performs the following actions in order:
271
        1. Deletes all ``ReducedDatum`` objects associated with the ``DataProduct``.
272
        2. Deletes the file referenced by the ``DataProduct``.
273
        3. Deletes the ``DataProduct`` object from the database.
274

275
        :param form: Django form instance containing the data for the DELETE request.
276
        :type form: django.forms.Form
277
        :return: HttpResponseRedirect to the success URL.
278
        :rtype: HttpResponseRedirect
279
        """
280
        # Fetch the DataProduct object
281
        data_product = self.get_object()
1✔
282

283
        # Delete associated ReducedDatum objects
284
        ReducedDatum.objects.filter(data_product=data_product).delete()
1✔
285

286
        # Delete the file reference.
287
        data_product.data.delete()
1✔
288
        # Delete the `DataProduct` object from the database.
289
        data_product.delete()
1✔
290

291
        return HttpResponseRedirect(self.get_success_url())
1✔
292

293
    def get_context_data(self, *args, **kwargs):
1✔
294
        """
295
        Adds the referer to the query parameters as "next" and returns the context dictionary.
296

297
        :returns: context dictionary
298
        :rtype: dict
299
        """
300
        context = super().get_context_data(*args, **kwargs)
×
301
        context['next'] = self.request.META.get('HTTP_REFERER', '/')
×
302
        return context
×
303

304

305
class DataProductListView(FilterView):
1✔
306
    """
307
    View that handles the list of ``DataProduct`` objects.
308
    """
309

310
    model = DataProduct
1✔
311
    template_name = 'tom_dataproducts/dataproduct_list.html'
1✔
312
    paginate_by = 25
1✔
313
    filterset_class = DataProductFilter
1✔
314
    strict = False
1✔
315

316
    def get_queryset(self):
1✔
317
        """
318
        Gets the set of ``DataProduct`` objects that the user has permission to view.
319

320
        :returns: Set of ``DataProduct`` objects
321
        :rtype: QuerySet
322
        """
323
        if settings.TARGET_PERMISSIONS_ONLY:
1✔
324
            return super().get_queryset().filter(
1✔
325
                target__in=get_objects_for_user(self.request.user, 'tom_targets.view_target')
326
            )
327
        else:
328
            return get_objects_for_user(self.request.user, 'tom_dataproducts.view_dataproduct')
1✔
329

330
    def get_context_data(self, *args, **kwargs):
1✔
331
        """
332
        Adds the set of ``DataProductGroup`` objects to the context dictionary.
333

334
        :returns: context dictionary
335
        :rtype: dict
336
        """
337
        context = super().get_context_data(*args, **kwargs)
1✔
338
        context['product_groups'] = DataProductGroup.objects.all()
1✔
339
        return context
1✔
340

341

342
class DataProductFeatureView(View):
1✔
343
    """
344
    View that handles the featuring of ``DataProduct``s. A featured ``DataProduct`` is displayed on the
345
    ``TargetDetailView``.
346
    """
347
    def get(self, request, *args, **kwargs):
1✔
348
        """
349
        Method that handles the GET requests for this view. Sets all other ``DataProduct``s to unfeatured in the
350
        database, and sets the specified ``DataProduct`` to featured. Caches the featured image. Deletes previously
351
        featured images from the cache.
352
        """
353
        product_id = kwargs.get('pk', None)
×
354
        product = DataProduct.objects.get(pk=product_id)
×
355
        try:
×
356
            current_featured = DataProduct.objects.filter(
×
357
                featured=True,
358
                data_product_type=product.data_product_type,
359
                target=product.target
360
            )
361
            for featured_image in current_featured:
×
362
                featured_image.featured = False
×
363
                featured_image.save()
×
364
                featured_image_cache_key = make_template_fragment_key(
×
365
                    'featured_image',
366
                    str(featured_image.target.id)
367
                )
368
                cache.delete(featured_image_cache_key)
×
369
        except DataProduct.DoesNotExist:
×
370
            pass
×
371
        product.featured = True
×
372
        product.save()
×
373
        return redirect(reverse(
×
374
            'tom_targets:detail',
375
            kwargs={'pk': request.GET.get('target_id')})
376
        )
377

378

379
class DataShareView(FormView):
1✔
380
    """
381
    View that handles the sharing of data either through HERMES or with another TOM.
382
    """
383

384
    form_class = DataShareForm
1✔
385

386
    def get_form(self, *args, **kwargs):
1✔
387
        # TODO: Add permissions
388
        form = super().get_form(*args, **kwargs)
×
389
        return form
×
390

391
    def form_invalid(self, form):
1✔
392
        """
393
        Adds errors to Django messaging framework in the case of an invalid form and redirects to the previous page.
394
        """
395
        # TODO: Format error messages in a more human-readable way
396
        messages.error(self.request, 'There was a problem sharing your Data: {}'.format(form.errors.as_json()))
×
397
        return redirect(form.cleaned_data.get('referrer', '/'))
×
398

399
    def post(self, request, *args, **kwargs):
1✔
400
        """
401
        Method that handles the POST requests for sharing data.
402
        Handles Data Products and All the data of a type for a target as well as individual Reduced Datums.
403
        Submit to Hermes, or Share with TOM (soon).
404
        """
405
        data_share_form = DataShareForm(request.POST, request.FILES)
1✔
406

407
        if data_share_form.is_valid():
1✔
408
            form_data = data_share_form.cleaned_data
1✔
409
            share_destination = form_data['share_destination']
1✔
410
            product_id = kwargs.get('dp_pk', None)
1✔
411
            target_id = kwargs.get('tg_pk', None)
1✔
412

413
            # Check if data points have been selected.
414
            selected_data = request.POST.getlist("share-box")
1✔
415

416
            # Check Destination
417
            if 'HERMES' in share_destination.upper():
1✔
418
                response = share_data_with_hermes(share_destination, form_data, product_id, target_id, selected_data)
×
419
            else:
420
                response = share_data_with_tom(share_destination, form_data, product_id, target_id, selected_data)
1✔
421
            sharing_feedback_handler(response, self.request)
1✔
422
        return redirect(reverse('tom_targets:detail', kwargs={'pk': request.POST.get('target')}))
1✔
423

424

425
class DataProductGroupDetailView(DetailView):
1✔
426
    """
427
    View that handles the viewing of a specific ``DataProductGroup``.
428
    """
429
    model = DataProductGroup
1✔
430

431
    def post(self, request, *args, **kwargs):
1✔
432
        """
433
        Handles the POST request for this view.
434
        """
435
        group = self.get_object()
×
436
        for product in request.POST.getlist('products'):
×
437
            group.dataproduct_set.remove(DataProduct.objects.get(pk=product))
×
438
        group.save()
×
439
        return redirect(reverse(
×
440
            'tom_dataproducts:group-detail',
441
            kwargs={'pk': group.id})
442
        )
443

444

445
class DataProductGroupListView(ListView):
1✔
446
    """
447
    View that handles the display of all ``DataProductGroup`` objects.
448
    """
449
    model = DataProductGroup
1✔
450

451

452
class DataProductGroupCreateView(LoginRequiredMixin, CreateView):
1✔
453
    """
454
    View that handles the creation of a new ``DataProductGroup``.
455
    """
456
    model = DataProductGroup
1✔
457
    success_url = reverse_lazy('tom_dataproducts:group-list')
1✔
458
    fields = ['name']
1✔
459

460

461
class DataProductGroupDeleteView(LoginRequiredMixin, DeleteView):
1✔
462
    """
463
    View that handles the deletion of a ``DataProductGroup``. Requires authentication.
464
    """
465
    success_url = reverse_lazy('tom_dataproducts:group-list')
1✔
466
    model = DataProductGroup
1✔
467

468

469
class DataProductGroupDataView(LoginRequiredMixin, FormView):
1✔
470
    """
471
    View that handles the addition of ``DataProduct``s to a ``DataProductGroup``. Requires authentication.
472
    """
473
    form_class = AddProductToGroupForm
1✔
474
    template_name = 'tom_dataproducts/add_product_to_group.html'
1✔
475

476
    def form_valid(self, form):
1✔
477
        """
478
        Runs after form validation. Adds the specified ``DataProduct`` objects to the group.
479

480
        :param form: Form with data products and group information
481
        :type form: AddProductToGroupForm
482
        """
483
        group = form.cleaned_data['group']
×
484
        group.dataproduct_set.add(*form.cleaned_data['products'])
×
485
        group.save()
×
486
        return redirect(reverse(
×
487
            'tom_dataproducts:group-detail',
488
            kwargs={'pk': group.id})
489
        )
490

491

492
class UpdateReducedDataView(LoginRequiredMixin, RedirectView):
1✔
493
    """
494
    View that handles the updating of reduced data tied to a ``DataProduct`` that was automatically ingested from a
495
    broker. Requires authentication.
496
    """
497
    def get(self, request, *args, **kwargs):
1✔
498
        """
499
        Method that handles the GET requests for this view. Calls the management command to update the reduced data and
500
        adds a hint using the messages framework about automation.
501
        """
502
        # QueryDict is immutable, and we want to append the remaining params to the redirect URL
503
        query_params = request.GET.copy()
×
504
        target_id = query_params.pop('target_id', None)
×
505
        out = StringIO()
×
506
        if target_id:
×
507
            if isinstance(target_id, list):
×
508
                target_id = target_id[-1]
×
509
            call_command('updatereduceddata', target_id=target_id, stdout=out)
×
510
        else:
511
            call_command('updatereduceddata', stdout=out)
×
512
        messages.info(request, out.getvalue())
×
513
        add_hint(request, mark_safe(
×
514
                          'Did you know updating observation statuses can be automated? Learn how in '
515
                          '<a href=https://tom-toolkit.readthedocs.io/en/stable/customization/automation.html>'
516
                          'the docs.</a>'))
517
        return HttpResponseRedirect(f'{self.get_redirect_url(*args, **kwargs)}?{urlencode(query_params)}')
×
518

519
    def get_redirect_url(self, *args, **kwargs):
1✔
520
        """
521
        Returns redirect URL as specified in the HTTP_REFERER field of the request.
522

523
        :returns: referer
524
        :rtype: str
525
        """
526
        referer = self.request.META.get('HTTP_REFERER', '/')
×
527
        return referer
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc