• 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

84.52
/tom_targets/views.py
1
import logging
1✔
2

3
from datetime import datetime
1✔
4
from io import StringIO
1✔
5
from urllib.parse import urlencode
1✔
6

7
from django.conf import settings
1✔
8
from django.contrib import messages
1✔
9
from django.contrib.auth.mixins import LoginRequiredMixin
1✔
10
from django.contrib.auth.models import Group
1✔
11
from django.core.management import call_command
1✔
12
from django.db import transaction
1✔
13
from django.db.models import Q
1✔
14
from django.http import HttpResponseRedirect, QueryDict, StreamingHttpResponse
1✔
15
from django.forms import HiddenInput
1✔
16
from django.shortcuts import redirect
1✔
17
from django.urls import reverse_lazy, reverse
1✔
18
from django.utils.text import slugify
1✔
19
from django.utils.safestring import mark_safe
1✔
20
from django.views.generic.edit import CreateView, UpdateView, DeleteView, FormView
1✔
21
from django.views.generic.detail import DetailView
1✔
22
from django.views.generic.list import ListView
1✔
23
from django.views.generic import RedirectView, TemplateView, View
1✔
24
from django_filters.views import FilterView
1✔
25

26
from guardian.mixins import PermissionListMixin
1✔
27
from guardian.shortcuts import get_objects_for_user, get_groups_with_perms, assign_perm
1✔
28

29
from tom_common.hints import add_hint
1✔
30
from tom_common.hooks import run_hook
1✔
31
from tom_common.mixins import Raise403PermissionRequiredMixin
1✔
32
from tom_observations.observation_template import ApplyObservationTemplateForm
1✔
33
from tom_observations.models import ObservationTemplate
1✔
34
from tom_targets.filters import TargetFilter
1✔
35
from tom_targets.forms import SiderealTargetCreateForm, NonSiderealTargetCreateForm, TargetExtraFormset
1✔
36
from tom_targets.forms import TargetNamesFormset, TargetShareForm, TargetListShareForm
1✔
37
from tom_targets.sharing import share_target_with_tom
1✔
38
from tom_dataproducts.sharing import share_data_with_hermes, share_data_with_tom, sharing_feedback_handler
1✔
39

40
from tom_targets.groups import (
1✔
41
    add_all_to_grouping, add_selected_to_grouping, remove_all_from_grouping, remove_selected_from_grouping,
42
    move_all_to_grouping, move_selected_to_grouping
43
)
44
from tom_targets.models import Target, TargetList
1✔
45
from tom_targets.utils import import_targets, export_targets
1✔
46

47
logger = logging.getLogger(__name__)
1✔
48

49

50
class TargetListView(PermissionListMixin, FilterView):
1✔
51
    """
52
    View for listing targets in the TOM. Only shows targets that the user is authorized to view. Requires authorization.
53
    """
54
    template_name = 'tom_targets/target_list.html'
1✔
55
    paginate_by = 25
1✔
56
    strict = False
1✔
57
    model = Target
1✔
58
    filterset_class = TargetFilter
1✔
59
    permission_required = 'tom_targets.view_target'
1✔
60
    ordering = ['-created']
1✔
61

62
    def get_context_data(self, *args, **kwargs):
1✔
63
        """
64
        Adds the number of targets visible, the available ``TargetList`` objects if the user is authenticated, and
65
        the query string to the context object.
66

67
        :returns: context dictionary
68
        :rtype: dict
69
        """
70
        context = super().get_context_data(*args, **kwargs)
1✔
71
        context['target_count'] = context['paginator'].count
1✔
72
        # hide target grouping list if user not logged in
73
        context['groupings'] = (TargetList.objects.all()
1✔
74
                                if self.request.user.is_authenticated
75
                                else TargetList.objects.none())
76
        context['query_string'] = self.request.META['QUERY_STRING']
1✔
77
        return context
1✔
78

79

80
class TargetNameSearchView(RedirectView):
1✔
81
    """
82
    View for searching by target name. If the search returns one result, the view redirects to the corresponding
83
    TargetDetailView. Otherwise, the view redirects to the TargetListView.
84
    """
85

86
    def get(self, request, *args, **kwargs):
1✔
87
        target_name = self.kwargs['name']
1✔
88
        # Tests fail without distinct but it works in practice, it is unclear as to why
89
        # The Django query planner shows different results between in practice and unit tests
90
        # django-guardian related querying is present in the test planner, but not in practice
91
        targets = get_objects_for_user(request.user, 'tom_targets.view_target').filter(
1✔
92
            Q(name__icontains=target_name) | Q(aliases__name__icontains=target_name)
93
        ).distinct()
94
        if targets.count() == 1:
1✔
95
            return HttpResponseRedirect(reverse('targets:detail', kwargs={'pk': targets.first().id}))
1✔
96
        else:
97
            return HttpResponseRedirect(reverse('targets:list') + f'?name={target_name}')
1✔
98

99

100
class TargetCreateView(LoginRequiredMixin, CreateView):
1✔
101
    """
102
    View for creating a Target. Requires authentication.
103
    """
104

105
    model = Target
1✔
106
    fields = '__all__'
1✔
107

108
    def get_default_target_type(self):
1✔
109
        """
110
        Returns the user-configured target type specified in ``settings.py``, if it exists, otherwise returns sidereal
111

112
        :returns: User-configured target type or global default
113
        :rtype: str
114
        """
115
        try:
1✔
116
            return settings.TARGET_TYPE
1✔
117
        except AttributeError:
×
118
            return Target.SIDEREAL
×
119

120
    def get_target_type(self):
1✔
121
        """
122
        Gets the type of the target to be created from the query parameters. If none exists, use the default target
123
        type specified in ``settings.py``.
124

125
        :returns: target type
126
        :rtype: str
127
        """
128
        obj = self.request.GET or self.request.POST
1✔
129
        target_type = obj.get('type')
1✔
130
        # If None or some invalid value, use default target type
131
        if target_type not in (Target.SIDEREAL, Target.NON_SIDEREAL):
1✔
132
            target_type = self.get_default_target_type()
1✔
133
        return target_type
1✔
134

135
    def get_initial(self):
1✔
136
        """
137
        Returns the initial data to use for forms on this view.
138

139
        :returns: Dictionary with the following keys:
140

141
                  `type`: ``str``: Type of the target to be created
142

143
                  `groups`: ``QuerySet<Group>`` Groups available to the current user
144

145
        :rtype: dict
146
        """
147
        return {
1✔
148
            'type': self.get_target_type(),
149
            'groups': self.request.user.groups.all(),
150
            **dict(self.request.GET.items())
151
        }
152

153
    def get_context_data(self, **kwargs):
1✔
154
        """
155
        Inserts certain form data into the context dict.
156

157
        :returns: Dictionary with the following keys:
158

159
                  `type_choices`: ``tuple``: Tuple of 2-tuples of strings containing available target types in the TOM
160

161
                  `extra_form`: ``FormSet``: Django formset with fields for arbitrary key/value pairs
162
        :rtype: dict
163
        """
164
        context = super(TargetCreateView, self).get_context_data(**kwargs)
1✔
165
        context['type_choices'] = Target.TARGET_TYPES
1✔
166
        context['names_form'] = TargetNamesFormset(initial=[{'name': new_name}
1✔
167
                                                            for new_name
168
                                                            in self.request.GET.get('names', '').split(',')])
169
        context['extra_form'] = TargetExtraFormset()
1✔
170
        return context
1✔
171

172
    def get_form_class(self):
1✔
173
        """
174
        Return the form class to use in this view.
175

176
        :returns: form class for target creation
177
        :rtype: subclass of TargetCreateForm
178
        """
179
        target_type = self.get_target_type()
1✔
180
        self.initial['type'] = target_type
1✔
181
        if target_type == Target.SIDEREAL:
1✔
182
            return SiderealTargetCreateForm
1✔
183
        else:
184
            return NonSiderealTargetCreateForm
1✔
185

186
    def form_valid(self, form):
1✔
187
        """
188
        Runs after form validation. Creates the ``Target``, and creates any ``TargetName`` or ``TargetExtra`` objects,
189
        then runs the ``target_post_save`` hook and redirects to the success URL.
190

191
        :param form: Form data for target creation
192
        :type form: subclass of TargetCreateForm
193
        """
194
        super().form_valid(form)
1✔
195

196
        extra = TargetExtraFormset(self.request.POST, instance=self.object)
1✔
197
        names = TargetNamesFormset(self.request.POST, instance=self.object)
1✔
198

199
        if extra.is_valid() and names.is_valid():
1✔
200
            extra.save()
1✔
201
            names.save()
1✔
202
        else:
203
            form.add_error(None, extra.errors)
1✔
204
            form.add_error(None, extra.non_form_errors())
1✔
205
            form.add_error(None, names.errors)
1✔
206
            form.add_error(None, names.non_form_errors())
1✔
207
            return super().form_invalid(form)
1✔
208
        logger.info('Target post save hook: %s created: %s', self.object, True)
1✔
209
        run_hook('target_post_save', target=self.object, created=True)
1✔
210
        return redirect(self.get_success_url())
1✔
211

212
    def get_form(self, *args, **kwargs):
1✔
213
        """
214
        Gets an instance of the ``TargetCreateForm`` and populates it with the groups available to the current user.
215

216
        :returns: instance of creation form
217
        :rtype: subclass of TargetCreateForm
218
        """
219
        form = super().get_form(*args, **kwargs)
1✔
220
        if self.request.user.is_superuser:
1✔
221
            form.fields['groups'].queryset = Group.objects.all()
×
222
        else:
223
            form.fields['groups'].queryset = self.request.user.groups.all()
1✔
224
        return form
1✔
225

226

227
class TargetUpdateView(Raise403PermissionRequiredMixin, UpdateView):
1✔
228
    """
229
    View that handles updating a target. Requires authorization.
230
    """
231
    permission_required = 'tom_targets.change_target'
1✔
232
    model = Target
1✔
233
    fields = '__all__'
1✔
234

235
    def get_context_data(self, **kwargs):
1✔
236
        """
237
        Adds formset for ``TargetName`` and ``TargetExtra`` to the context.
238

239
        :returns: context object
240
        :rtype: dict
241
        """
242
        extra_field_names = [extra['name'] for extra in settings.EXTRA_FIELDS]
1✔
243
        context = super().get_context_data(**kwargs)
1✔
244
        context['names_form'] = TargetNamesFormset(instance=self.object)
1✔
245
        context['extra_form'] = TargetExtraFormset(
1✔
246
            instance=self.object,
247
            queryset=self.object.targetextra_set.exclude(key__in=extra_field_names)
248
        )
249
        return context
1✔
250

251
    @transaction.atomic
1✔
252
    def form_valid(self, form):
1✔
253
        """
254
        Runs after form validation. Validates and saves the ``TargetExtra`` and ``TargetName`` formsets, then calls the
255
        superclass implementation of ``form_valid``, which saves the ``Target``. If any forms are invalid, rolls back
256
        the changes.
257

258
        Saving is done in this order to ensure that new names/extras are available in the ``target_post_save`` hook.
259

260
        :param form: Form data for target update
261
        :type form: subclass of TargetCreateForm
262
        """
263
        super().form_valid(form)
1✔
264
        extra = TargetExtraFormset(self.request.POST, instance=self.object)
1✔
265
        names = TargetNamesFormset(self.request.POST, instance=self.object)
1✔
266
        if extra.is_valid() and names.is_valid():
1✔
267
            extra.save()
1✔
268
            names.save()
1✔
269
        else:
270
            form.add_error(None, extra.errors)
1✔
271
            form.add_error(None, extra.non_form_errors())
1✔
272
            form.add_error(None, names.errors)
1✔
273
            form.add_error(None, names.non_form_errors())
1✔
274
            return super().form_invalid(form)
1✔
275
        return redirect(self.get_success_url())
1✔
276

277
    def get_queryset(self, *args, **kwargs):
1✔
278
        """
279
        Returns the queryset that will be used to look up the Target by limiting the result to targets that the user is
280
        authorized to modify.
281

282
        :returns: Set of targets
283
        :rtype: QuerySet
284
        """
285
        return get_objects_for_user(self.request.user, 'tom_targets.change_target')
1✔
286

287
    def get_form_class(self):
1✔
288
        """
289
        Return the form class to use in this view.
290

291
        :returns: form class for target update
292
        :rtype: subclass of TargetCreateForm
293
        """
294
        if self.object.type == Target.SIDEREAL:
1✔
295
            return SiderealTargetCreateForm
1✔
296
        elif self.object.type == Target.NON_SIDEREAL:
×
297
            return NonSiderealTargetCreateForm
×
298

299
    def get_initial(self):
1✔
300
        """
301
        Returns the initial data to use for forms on this view. For the ``TargetUpdateView``, adds the groups that the
302
        target is a member of.
303

304
        :returns:
305
        :rtype: dict
306
        """
307
        initial = super().get_initial()
1✔
308
        initial['groups'] = get_groups_with_perms(self.get_object())
1✔
309
        return initial
1✔
310

311
    def get_form(self, *args, **kwargs):
1✔
312
        """
313
        Gets an instance of the ``TargetCreateForm`` and populates it with the groups available to the current user.
314

315
        :returns: instance of creation form
316
        :rtype: subclass of TargetCreateForm
317
        """
318
        form = super().get_form(*args, **kwargs)
1✔
319
        if self.request.user.is_superuser:
1✔
320
            form.fields['groups'].queryset = Group.objects.all()
×
321
        else:
322
            form.fields['groups'].queryset = self.request.user.groups.all()
1✔
323
        return form
1✔
324

325

326
class TargetDeleteView(Raise403PermissionRequiredMixin, DeleteView):
1✔
327
    """
328
    View for deleting a target. Requires authorization.
329
    """
330
    permission_required = 'tom_targets.delete_target'
1✔
331
    success_url = reverse_lazy('targets:list')
1✔
332
    model = Target
1✔
333

334

335
class TargetShareView(FormView):
1✔
336
    """
337
    View for sharing a target. Requires authorization.
338
    """
339
    template_name = 'tom_targets/target_share.html'
1✔
340
    permission_required = 'tom_targets.share_target'
1✔
341
    form_class = TargetShareForm
1✔
342

343
    def get_context_data(self, *args, **kwargs):
1✔
344
        """
345
        Adds the target information to the context.
346
        :returns: context object
347
        :rtype: dict
348
        """
349
        context = super().get_context_data(*args, **kwargs)
×
350
        target_id = self.kwargs.get('pk', None)
×
351
        context['target'] = Target.objects.get(id=target_id)
×
352

353
        return context
×
354

355
    def get_success_url(self):
1✔
356
        """
357
        Redirect to target detail page for shared target
358
        """
359
        return reverse_lazy('targets:detail', kwargs={'pk': self.kwargs.get('pk', None)})
1✔
360

361
    def form_invalid(self, form):
1✔
362
        """
363
        Adds errors to Django messaging framework in the case of an invalid form and redirects to the previous page.
364
        """
365
        # TODO: Format error messages in a more human-readable way
366
        messages.error(self.request, 'There was a problem sharing your Data: {}'.format(form.errors.as_json()))
×
367
        return redirect(self.get_success_url())
×
368

369
    def form_valid(self, form):
1✔
370
        """
371
        Shares the target with the selected destination(s) and redirects to the target detail page.
372
        """
373
        form_data = form.cleaned_data
1✔
374
        share_destination = form_data['share_destination']
1✔
375
        target_id = self.kwargs.get('pk', None)
1✔
376
        selected_data = self.request.POST.getlist("share-box")
1✔
377
        if 'HERMES' in share_destination.upper():
1✔
378
            response = share_data_with_hermes(share_destination, form_data, None, target_id, selected_data)
×
379
            sharing_feedback_handler(response, self.request)
×
380
        else:
381
            # Share Target with Destination TOM
382
            response = share_target_with_tom(share_destination, form_data)
1✔
383
            sharing_feedback_handler(response, self.request)
1✔
384
            if selected_data:
1✔
385
                # Share Data with Destination TOM
386
                response = share_data_with_tom(share_destination, form_data, selected_data=selected_data)
1✔
387
                sharing_feedback_handler(response, self.request)
1✔
388
        return redirect(self.get_success_url())
1✔
389

390

391
class TargetDetailView(Raise403PermissionRequiredMixin, DetailView):
1✔
392
    """
393
    View that handles the display of the target details. Requires authorization.
394
    """
395
    permission_required = 'tom_targets.view_target'
1✔
396
    model = Target
1✔
397

398
    def get_context_data(self, *args, **kwargs):
1✔
399
        """
400
        Adds the ``DataProductUploadForm`` to the context and prepopulates the hidden fields.
401

402
        :returns: context object
403
        :rtype: dict
404
        """
405
        context = super().get_context_data(*args, **kwargs)
1✔
406
        observation_template_form = ApplyObservationTemplateForm(initial={'target': self.get_object()})
1✔
407
        if any(self.request.GET.get(x) for x in ['observation_template', 'cadence_strategy', 'cadence_frequency']):
1✔
408
            initial = {'target': self.object}
×
409
            initial.update(self.request.GET)
×
410
            observation_template_form = ApplyObservationTemplateForm(
×
411
                initial=initial
412
            )
413
        observation_template_form.fields['target'].widget = HiddenInput()
1✔
414
        context['observation_template_form'] = observation_template_form
1✔
415
        return context
1✔
416

417
    def get(self, request, *args, **kwargs):
1✔
418
        """
419
        Handles the GET requests to this view. If update_status is passed into the query parameters, calls the
420
        updatestatus management command to query for new statuses for ``ObservationRecord`` objects associated with this
421
        target.
422

423
        :param request: the request object passed to this view
424
        :type request: HTTPRequest
425
        """
426
        update_status = request.GET.get('update_status', False)
1✔
427
        if update_status:
1✔
428
            if not request.user.is_authenticated:
×
429
                return redirect(reverse('login'))
×
430
            target_id = kwargs.get('pk', None)
×
431
            out = StringIO()
×
432
            call_command('updatestatus', target_id=target_id, stdout=out)
×
433
            messages.info(request, out.getvalue())
×
434
            add_hint(request, mark_safe(
×
435
                              'Did you know updating observation statuses can be automated? Learn how in'
436
                              '<a href=https://tom-toolkit.readthedocs.io/en/stable/customization/automation.html>'
437
                              ' the docs.</a>'))
438
            return redirect(reverse('tom_targets:detail', args=(target_id,)))
×
439

440
        obs_template_form = ApplyObservationTemplateForm(request.GET)
1✔
441
        if obs_template_form.is_valid():
1✔
442
            obs_template = ObservationTemplate.objects.get(pk=obs_template_form.cleaned_data['observation_template'].id)
×
443
            obs_template_params = obs_template.parameters
×
444
            obs_template_params['cadence_strategy'] = request.GET.get('cadence_strategy', '')
×
445
            obs_template_params['cadence_frequency'] = request.GET.get('cadence_frequency', '')
×
446
            params = urlencode(obs_template_params)
×
447
            return redirect(
×
448
                reverse('tom_observations:create',
449
                        args=(obs_template.facility,)) + f'?target_id={self.get_object().id}&' + params)
450

451
        return super().get(request, *args, **kwargs)
1✔
452

453

454
class TargetImportView(LoginRequiredMixin, TemplateView):
1✔
455
    """
456
    View that handles the import of targets from a CSV. Requires authentication.
457
    """
458
    template_name = 'tom_targets/target_import.html'
1✔
459

460
    def post(self, request):
1✔
461
        """
462
        Handles the POST requests to this view. Creates a StringIO object and passes it to ``import_targets``.
463

464
        :param request: the request object passed to this view
465
        :type request: HTTPRequest
466
        """
467
        csv_file = request.FILES['target_csv']
×
468
        csv_stream = StringIO(csv_file.read().decode('utf-8'), newline=None)
×
469
        result = import_targets(csv_stream)
×
470
        messages.success(
×
471
            request,
472
            'Targets created: {}'.format(len(result['targets']))
473
        )
474
        for error in result['errors']:
×
475
            messages.warning(request, error)
×
476
        return redirect(reverse('tom_targets:list'))
×
477

478

479
class TargetExportView(TargetListView):
1✔
480
    """
481
    View that handles the export of targets to a CSV. Only exports selected targets.
482
    """
483
    def render_to_response(self, context, **response_kwargs):
1✔
484
        """
485
        Returns a response containing the exported CSV of selected targets.
486

487
        :param context: Context object for this view
488
        :type context: dict
489

490
        :returns: response class with CSV
491
        :rtype: StreamingHttpResponse
492
        """
493
        qs = context['filter'].qs.values()
1✔
494
        file_buffer = export_targets(qs)
1✔
495
        file_buffer.seek(0)  # goto the beginning of the buffer
1✔
496
        response = StreamingHttpResponse(file_buffer, content_type="text/csv")
1✔
497
        filename = "targets-{}.csv".format(slugify(datetime.utcnow()))
1✔
498
        response['Content-Disposition'] = 'attachment; filename="{}"'.format(filename)
1✔
499
        return response
1✔
500

501

502
class TargetAddRemoveGroupingView(LoginRequiredMixin, View):
1✔
503
    """
504
    View that handles addition and removal of targets to target groups. Requires authentication.
505
    """
506

507
    def post(self, request, *args, **kwargs):
1✔
508
        """
509
        Handles the POST requests to this view. Routes the information from the request and query parameters to the
510
        appropriate utility method in ``groups.py``.
511

512
        :param request: the request object passed to this view
513
        :type request: HTTPRequest
514
        """
515
        query_string = request.POST.get('query_string', '')
1✔
516
        grouping_id = request.POST.get('grouping')
1✔
517
        filter_data = QueryDict(query_string)
1✔
518
        try:
1✔
519
            grouping_object = TargetList.objects.get(pk=grouping_id)
1✔
520
        except Exception as e:
1✔
521
            messages.error(request, 'Cannot find the target group with id={}; {}'.format(grouping_id, e))
1✔
522
            return redirect(reverse('tom_targets:list') + '?' + query_string)
1✔
523
        if not request.user.has_perm('tom_targets.view_targetlist', grouping_object):
1✔
524
            messages.error(request, 'Permission denied.')
1✔
525
            return redirect(reverse('tom_targets:list') + '?' + query_string)
1✔
526

527
        if 'add' in request.POST:
1✔
528
            if request.POST.get('isSelectAll') == 'True':
1✔
529
                add_all_to_grouping(filter_data, grouping_object, request)
1✔
530
            else:
531
                targets_ids = request.POST.getlist('selected-target')
1✔
532
                add_selected_to_grouping(targets_ids, grouping_object, request)
1✔
533
        if 'remove' in request.POST:
1✔
534
            if request.POST.get('isSelectAll') == 'True':
1✔
535
                remove_all_from_grouping(filter_data, grouping_object, request)
1✔
536
            else:
537
                targets_ids = request.POST.getlist('selected-target')
1✔
538
                remove_selected_from_grouping(targets_ids, grouping_object, request)
1✔
539
        if 'move' in request.POST:
1✔
540
            if request.POST.get('isSelectAll') == 'True':
1✔
541
                move_all_to_grouping(filter_data, grouping_object, request)
1✔
542
            else:
543
                target_ids = request.POST.getlist('selected-target')
1✔
544
                move_selected_to_grouping(target_ids, grouping_object, request)
1✔
545

546
        return redirect(reverse('tom_targets:list') + '?' + query_string)
1✔
547

548

549
class TargetGroupingView(PermissionListMixin, ListView):
1✔
550
    """
551
    View that handles the display of ``TargetList`` objects, also known as target groups. Requires authorization.
552
    """
553
    permission_required = 'tom_targets.view_targetlist'
1✔
554
    template_name = 'tom_targets/target_grouping.html'
1✔
555
    model = TargetList
1✔
556
    paginate_by = 25
1✔
557

558
    def get_context_data(self, *args, **kwargs):
1✔
559
        """
560
        Adds ``settings.DATA_SHARING`` to the context to see if sharing has been configured.
561
        :returns: context object
562
        :rtype: dict
563
        """
564
        context = super().get_context_data(*args, **kwargs)
1✔
565
        context['sharing'] = getattr(settings, "DATA_SHARING", None)
1✔
566
        return context
1✔
567

568

569
class TargetGroupingDeleteView(Raise403PermissionRequiredMixin, DeleteView):
1✔
570
    """
571
    View that handles the deletion of ``TargetList`` objects, also known as target groups. Requires authorization.
572
    """
573
    permission_required = 'tom_targets.delete_targetlist'
1✔
574
    model = TargetList
1✔
575
    success_url = reverse_lazy('targets:targetgrouping')
1✔
576

577

578
class TargetGroupingCreateView(LoginRequiredMixin, CreateView):
1✔
579
    """
580
    View that handles the creation of ``TargetList`` objects, also known as target groups. Requires authentication.
581
    """
582
    model = TargetList
1✔
583
    fields = ['name']
1✔
584
    success_url = reverse_lazy('targets:targetgrouping')
1✔
585

586
    def form_valid(self, form):
1✔
587
        """
588
        Runs after form validation. Saves the target group and assigns the user's permissions to the group.
589

590
        :param form: Form data for target creation
591
        :type form: django.forms.ModelForm
592
        """
593
        obj = form.save(commit=False)
1✔
594
        obj.save()
1✔
595
        assign_perm('tom_targets.view_targetlist', self.request.user, obj)
1✔
596
        assign_perm('tom_targets.change_targetlist', self.request.user, obj)
1✔
597
        assign_perm('tom_targets.delete_targetlist', self.request.user, obj)
1✔
598
        return super().form_valid(form)
1✔
599

600

601
class TargetGroupingShareView(FormView):
1✔
602
    """
603
    View for sharing a TargetList. Requires authorization.
604
    """
605
    template_name = 'tom_targets/target_group_share.html'
1✔
606
    permission_required = 'tom_targets.share_target'
1✔
607
    form_class = TargetListShareForm
1✔
608

609
    def get_context_data(self, *args, **kwargs):
1✔
610
        """
611
        Adds the ``TargetListShareForm`` to the context and prepopulates the hidden fields.
612
        :returns: context object
613
        :rtype: dict
614
        """
615
        context = super().get_context_data(*args, **kwargs)
×
616
        target_list_id = self.kwargs.get('pk', None)
×
617
        target_list = TargetList.objects.get(id=target_list_id)
×
618
        context['target_list'] = target_list
×
619
        initial = {'submitter': self.request.user,
×
620
                   'target_list': target_list,
621
                   'share_title': f"Updated targets for group {target_list.name}."}
622
        form = TargetListShareForm(initial=initial)
×
623
        context['form'] = form
×
624
        return context
×
625

626
    def get_success_url(self):
1✔
627
        """
628
        Redirects to the target list page with the target list name as a query parameter.
629
        """
630
        return reverse_lazy('targets:list')+f'?targetlist__name={self.kwargs.get("pk", None)}'
1✔
631

632
    def form_invalid(self, form):
1✔
633
        """
634
        Adds errors to Django messaging framework in the case of an invalid form and redirects to the previous page.
635
        """
636
        # TODO: Format error messages in a more human-readable way
637
        messages.error(self.request, 'There was a problem sharing your Target List: {}'.format(form.errors.as_json()))
×
638
        return redirect(self.get_success_url())
×
639

640
    def form_valid(self, form):
1✔
641
        form_data = form.cleaned_data
1✔
642
        share_destination = form_data['share_destination']
1✔
643
        selected_targets = self.request.POST.getlist('selected-target')
1✔
644
        data_switch = self.request.POST.get('dataSwitch', False)
1✔
645
        if 'HERMES' in share_destination.upper():
1✔
646
            # TODO: Implement Hermes sharing
647
            # response = share_data_with_hermes(share_destination, form_data, None, target_id, selected_data)
648
            messages.error(self.request, "Publishing Groups to Hermes is not yet supported.")
×
649
            return redirect(self.get_success_url())
×
650
        else:
651
            for target in selected_targets:
1✔
652
                # Share each target individually
653
                form_data['target'] = Target.objects.get(id=target)
1✔
654
                response = share_target_with_tom(share_destination, form_data, target_lists=[form_data['target_list']])
1✔
655
                sharing_feedback_handler(response, self.request)
1✔
656
                if data_switch:
1✔
657
                    # If Data sharing request, share all data associated with the target
658
                    response = share_data_with_tom(share_destination, form_data, target_id=target)
1✔
659
                    sharing_feedback_handler(response, self.request)
1✔
660
            if not selected_targets:
1✔
661
                messages.error(self.request, f'No targets shared. {form.errors.as_json()}')
1✔
662
                return redirect(self.get_success_url())
1✔
663
        return redirect(self.get_success_url())
1✔
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