from django.contrib.auth.decorators import login_required
import logging
from datetime import datetime, timedelta
from io import StringIO
from urllib.parse import urlencode
import numpy as np
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.exceptions import PermissionDenied
from django.core.management import call_command
from django.core.paginator import Paginator
from django.db import transaction
from django_filters.views import FilterView
from django.http import HttpResponse
from django.http import HttpResponseRedirect, QueryDict, StreamingHttpResponse
from django.forms import HiddenInput
from django.shortcuts import redirect, render
from django.template.loader import render_to_string
from django.urls import reverse_lazy, reverse
from django.utils.text import slugify
from django.utils.safestring import mark_safe
from django.views.generic.edit import CreateView, UpdateView, DeleteView, FormView
from django.views.generic.detail import DetailView
from django.views.generic import RedirectView, TemplateView, View
from rest_framework.views import APIView
from rest_framework.renderers import TemplateHTMLRenderer
from rest_framework.response import Response
from guardian.mixins import PermissionListMixin
from guardian.shortcuts import get_groups_with_perms, assign_perm
from tom_common.hints import add_hint
from tom_common.htmx_table import HTMXTableViewMixin
from tom_common.hooks import run_hook
from tom_common.mixins import Raise403PermissionRequiredMixin
from tom_observations.observation_template import ApplyObservationTemplateForm
from tom_observations.models import ObservationTemplate
from tom_targets.filters import TargetFilterSet, TargetGroupFilterSet
from tom_targets.forms import SiderealTargetCreateForm, NonSiderealTargetCreateForm, TargetExtraFormset
from tom_targets.forms import TargetNamesFormset, TargetShareForm, TargetListShareForm, TargetMergeForm, \
UnknownTypeTargetCreateForm, TargetSelectionForm
from tom_targets.sharing import share_target_with_tom
from tom_targets.merge import target_merge
from tom_dataproducts.sharing import (share_data_with_tom, sharing_feedback_handler)
from tom_targets.groups import (
add_all_to_grouping, add_selected_to_grouping, remove_all_from_grouping, remove_selected_from_grouping,
move_all_to_grouping, move_selected_to_grouping
)
from tom_targets.merge import (merge_error_message)
from tom_targets.models import Target, TargetList
from tom_targets.persistent_sharing_serializers import PersistentShareSerializer
from tom_targets.permissions import targets_for_user
from tom_targets.templatetags.targets_extras import target_merge_fields, persistent_share_table
from tom_targets.utils import import_targets, export_targets
from tom_observations.utils import get_sidereal_visibility
from tom_targets.seed import seed_messier_targets
from tom_targets.tables import TargetTable, TargetGroupTable
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
[docs]
class TargetListView(HTMXTableViewMixin, FilterView):
"""
View for listing targets in the TOM. Only shows targets that the user is authorized to view. Requires authorization.
"""
template_name = 'tom_targets/target_list.html'
paginate_by = 20
strict = False
model = Target
filterset_class = TargetFilterSet
table_class = TargetTable
ordering = ['-created']
[docs]
def get_context_data(self, *args, **kwargs):
"""
Adds the number of targets visible, the available ``TargetList`` objects if the user is authenticated, and
the query string to the context object.
:returns: context dictionary
:rtype: dict
"""
context = super().get_context_data(*args, **kwargs)
context['target_count'] = context['record_count']
# hide target grouping list if user not logged in
context['groupings'] = (TargetList.objects.all()
if self.request.user.is_authenticated
else TargetList.objects.none())
context['query_string'] = self.request.META['QUERY_STRING']
# Prepare list of targets for Aladin skymap (avoiding BoundRow wrappers)
table = context['table']
if hasattr(table, 'page') and table.page:
context['skymap_objects'] = [row.record for row in table.page.object_list]
else:
context['skymap_objects'] = context['object_list']
return context
[docs]
def get_queryset(self, *args, **kwargs):
qs = super().get_queryset(*args, **kwargs)
return targets_for_user(self.request.user, qs, 'view_target')
[docs]
class TargetNameSearchView(RedirectView):
"""
View for searching by target name. If the search returns one result, the view redirects to the corresponding
TargetDetailView. Otherwise, the view redirects to the TargetListView.
"""
def get(self, request, *args, **kwargs):
target_name = self.kwargs['name'].strip()
# The Django query planner shows different results between in practice and unit tests
# django-guardian related querying is present in the test planner, but not in practice
all_targets = targets_for_user(request.user, Target.objects.all(), 'view_target')
targets_main = all_targets.filter(name__icontains=target_name)
targets_alias = all_targets.filter(aliases__name__icontains=target_name)
targets = targets_main.union(targets_alias)
try:
target = targets.get()
except (Target.DoesNotExist, Target.MultipleObjectsReturned):
return HttpResponseRedirect(reverse('targets:list') + f'?name={target_name}')
else:
return HttpResponseRedirect(reverse('targets:detail', kwargs={'pk': target.id}))
[docs]
class TargetCreateView(LoginRequiredMixin, CreateView):
"""
View for creating a Target. Requires authentication.
"""
# Target Views require explicit template names since the Model Class names are variable.
template_name = 'tom_targets/target_form.html'
model = Target
fields = '__all__'
[docs]
def get_default_target_type(self):
"""
Returns the user-configured target type specified in ``settings.py``, if it exists, otherwise returns sidereal
:returns: User-configured target type or global default
:rtype: str
"""
try:
return settings.TARGET_TYPE
except AttributeError:
return Target.SIDEREAL
[docs]
def get_target_type(self):
"""
Gets the type of the target to be created from the query parameters. If none exists, use the default target
type specified in ``settings.py``.
:returns: target type
:rtype: str
"""
obj = self.request.GET or self.request.POST
target_type = obj.get('type')
# If None or some invalid value, use default target type
if target_type not in (Target.SIDEREAL, Target.NON_SIDEREAL):
target_type = self.get_default_target_type()
return target_type
[docs]
def get_initial(self):
"""
Returns the initial data to use for forms on this view.
:returns: Dictionary with the following keys:
`type`: ``str``: Type of the target to be created
`groups`: ``QuerySet<Group>`` Groups available to the current user
:rtype: dict
"""
return {
'type': self.get_target_type(),
'groups': self.request.user.groups.all(),
**dict(self.request.GET.items())
}
[docs]
def get_context_data(self, **kwargs):
"""
Inserts certain form data into the context dict.
:returns: Dictionary with the following keys:
`type_choices`: ``tuple``: Tuple of 2-tuples of strings containing available target types in the TOM
`extra_form`: ``FormSet``: Django formset with fields for arbitrary key/value pairs
:rtype: dict
"""
context = super(TargetCreateView, self).get_context_data(**kwargs)
context['type_choices'] = Target.TARGET_TYPES
context['names_form'] = TargetNamesFormset(initial=[{'name': new_name}
for new_name
in self.request.GET.get('names', '').split(',')])
context['extra_form'] = TargetExtraFormset()
return context
[docs]
class TargetUpdateView(LoginRequiredMixin, UpdateView):
"""
View that handles updating a target. Requires authorization.
"""
template_name = 'tom_targets/target_form.html'
model = Target
fields = '__all__'
[docs]
def get_context_data(self, **kwargs):
"""
Adds formset for ``TargetName`` and ``TargetExtra`` to the context.
:returns: context object
:rtype: dict
"""
extra_field_names = [extra['name'] for extra in settings.EXTRA_FIELDS]
context = super().get_context_data(**kwargs)
context['names_form'] = TargetNamesFormset(instance=self.object)
context['extra_form'] = TargetExtraFormset(
instance=self.object,
queryset=self.object.targetextra_set.exclude(key__in=extra_field_names)
)
return context
[docs]
def get_queryset(self, *args, **kwargs):
"""
Returns the queryset that will be used to look up the Target by limiting the result to targets that the user is
authorized to modify.
:returns: Set of targets
:rtype: QuerySet
"""
qs = super().get_queryset(*args, **kwargs)
return targets_for_user(self.request.user, qs, 'change_target')
[docs]
def get_initial(self):
"""
Returns the initial data to use for forms on this view. For the ``TargetUpdateView``, adds the groups that the
target is a member of.
:returns:
:rtype: dict
"""
initial = super().get_initial()
initial['groups'] = get_groups_with_perms(self.get_object())
return initial
[docs]
class TargetDeleteView(LoginRequiredMixin, DeleteView):
"""
View for deleting a target. Requires authorization.
"""
template_name = 'tom_targets/target_confirm_delete.html'
# Set app_name for Django-Guardian Permissions in case of Custom Target Model
success_url = reverse_lazy('targets:list')
model = Target
[docs]
def get_queryset(self, *args, **kwargs):
qs = super().get_queryset(*args, **kwargs)
return targets_for_user(self.request.user, qs, 'delete_target')
[docs]
class TargetShareView(FormView):
"""
View for sharing a target. Requires authorization.
"""
template_name = 'tom_targets/target_share.html'
# Set app_name for Django-Guardian Permissions in case of Custom Target Model
permission_required = f'{Target._meta.app_label}.change_target'
form_class = TargetShareForm
[docs]
def get_context_data(self, *args, **kwargs):
"""
Adds the target information to the context.
:returns: context object
:rtype: dict
"""
context = super().get_context_data(*args, **kwargs)
target_id = self.kwargs.get('pk', None)
target = Target.objects.get(id=target_id)
context['target'] = target
initial = {
'submitter': self.request.user,
'share_title': f'Updated data for {target.name}'
}
form = TargetShareForm(initial=initial)
context['form'] = form
return context
[docs]
def get_success_url(self):
"""
Redirect to target detail page for shared target
"""
return reverse_lazy('targets:detail', kwargs={'pk': self.kwargs.get('pk', None)})
[docs]
class TargetDetailView(DetailView):
"""
View that handles the display of the target details.
"""
template_name = 'tom_targets/target_detail.html'
model = Target
[docs]
def get_queryset(self, *args, **kwargs):
qs = super().get_queryset(*args, **kwargs)
qs = targets_for_user(self.request.user, qs, 'view_target')
if not qs.exists() and Target.objects.filter(pk=self.kwargs.get("pk")).exists():
# Not great hack in order to permission denied instead of 404
raise PermissionDenied('You do not have permission to view this target')
else:
return qs
[docs]
def get_context_data(self, *args, **kwargs):
"""
Adds the ``DataProductUploadForm`` to the context and prepopulates the hidden fields.
:returns: context object
:rtype: dict
"""
context = super().get_context_data(*args, **kwargs)
observation_template_form = ApplyObservationTemplateForm(initial={'target': self.get_object()})
if any(self.request.GET.get(x) for x in ['observation_template', 'cadence_strategy', 'cadence_frequency']):
initial = {'target': self.object}
initial.update(self.request.GET)
observation_template_form = ApplyObservationTemplateForm(
initial=initial
)
observation_template_form.fields['target'].widget = HiddenInput()
context['observation_template_form'] = observation_template_form
context['target'] = self.object
return context
[docs]
def get(self, request, *args, **kwargs):
"""
Handles the GET requests to this view. If update_status is passed into the query parameters, calls the
updatestatus management command to query for new statuses for ``ObservationRecord`` objects associated with this
target.
:param request: the request object passed to this view
:type request: HTTPRequest
"""
update_status = request.GET.get('update_status', False)
if update_status:
if not request.user.is_authenticated:
return redirect(reverse('login'))
target_id = kwargs.get('pk', None)
out = StringIO()
call_command('updatestatus', target_id=target_id, 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 redirect(reverse('tom_targets:detail', args=(target_id,)) + '?tab=observations')
obs_template_form = ApplyObservationTemplateForm(request.GET)
if obs_template_form.is_valid():
obs_template = ObservationTemplate.objects.get(pk=obs_template_form.cleaned_data['observation_template'].id)
obs_template_params = obs_template.parameters
obs_template_params['cadence_strategy'] = request.GET.get('cadence_strategy', '')
obs_template_params['cadence_frequency'] = request.GET.get('cadence_frequency', '')
params = urlencode(obs_template_params)
return redirect(
reverse('tom_observations:create',
args=(obs_template.facility,)) + f'?target_id={self.get_object().id}&' + params)
return super().get(request, *args, **kwargs)
@login_required
def render_observation_table(request, pk):
out = StringIO()
call_command('updatestatus', target_id=pk, stdout=out)
messages.info(request, out.getvalue())
return render(request, 'tom_targets/partials/observation_table.html', context={'object': Target.objects.get(id=pk)})
[docs]
class TargetImportView(LoginRequiredMixin, TemplateView):
"""
View that handles the import of targets from a CSV. Requires authentication.
"""
template_name = 'tom_targets/target_import.html'
[docs]
def post(self, request):
"""
Handles the POST requests to this view. Creates a StringIO object and passes it to ``import_targets``.
:param request: the request object passed to this view
:type request: HTTPRequest
"""
csv_file = request.FILES['target_csv']
csv_stream = StringIO(csv_file.read().decode('utf-8'), newline=None)
result = import_targets(csv_stream)
for target in result['targets']:
target.give_user_access(request.user)
messages.success(
request,
'Targets created: {}'.format(len(result['targets']))
)
for error in result['errors']:
messages.warning(request, error)
return redirect(reverse('tom_targets:list'))
[docs]
class TargetExportView(TargetListView):
"""
View that handles the export of targets to a CSV. Only exports selected targets.
"""
[docs]
def render_to_response(self, context, **response_kwargs):
"""
Returns a response containing the exported CSV of selected targets.
:param context: Context object for this view
:type context: dict
:returns: response class with CSV
:rtype: StreamingHttpResponse
"""
qs = context['filter'].qs.values()
file_buffer = export_targets(qs)
file_buffer.seek(0) # goto the beginning of the buffer
response = StreamingHttpResponse(file_buffer, content_type="text/csv")
filename = "targets-{}.csv".format(slugify(datetime.utcnow()))
response['Content-Disposition'] = 'attachment; filename="{}"'.format(filename)
return response
[docs]
class TargetMergeView(FormView):
"""
View that handles choosing the primary target in the process of merging targets
"""
template_name = 'tom_targets/target_merge.html'
form_class = TargetMergeForm
[docs]
def get_name_select_choices(self, pk1, pk2):
"""
Puts user selected targets in a choice field on the target_merge.html page,
using the target pk's.
"""
first_target = Target.objects.get(id=pk1)
second_target = Target.objects.get(id=pk2)
choices = [
(first_target.id, first_target.name),
(second_target.id, second_target.name)
]
return choices
[docs]
def get_context_data(self, *args, **kwargs):
"""
Adds the target information to the context.
:returns: context object
:rtype: dict
"""
context = super().get_context_data(*args, **kwargs)
first_target_id = self.kwargs.get('pk1', None)
first_target = Target.objects.get(id=first_target_id)
context['target1'] = first_target
second_target_id = self.kwargs.get('pk2', None)
second_target = Target.objects.get(id=second_target_id)
context['target2'] = second_target
initial = {'target1': first_target,
'target2': second_target}
form = TargetMergeForm(initial=initial)
form.fields['name_select'].choices = self.get_name_select_choices(first_target_id, second_target_id)
context['form'] = form
return context
[docs]
def post(self, request, *args, **kwargs):
form = TargetMergeForm(request.POST)
first_target_id = int(self.kwargs.get('pk1', None))
second_target_id = int(self.kwargs.get('pk2', None))
# let the form name_select field know what it's choices are
# these were determined at run time
form.fields['name_select'].choices = self.get_name_select_choices(
first_target_id, second_target_id)
if form.is_valid():
primary_target_id = int(form.cleaned_data['name_select'])
if primary_target_id == first_target_id:
secondary_target_id = second_target_id
else:
secondary_target_id = first_target_id
primary_target = Target.objects.get(id=primary_target_id)
secondary_target = Target.objects.get(id=secondary_target_id)
if 'confirm' in request.POST: # redirects user to an updated target detail page with merged targets
target_merge(primary_target, secondary_target)
return redirect('tom_targets:detail', pk=primary_target_id)
return redirect('tom_targets:merge', pk1=primary_target_id, pk2=secondary_target_id)
else:
messages.warning(request, form.errors)
return redirect('tom_targets:merge',
pk1=first_target_id, pk2=second_target_id)
[docs]
def get(self, request, *args, **kwargs):
"""When called as a result of the Primary Target name_select field being
changed, request.htmx will be True and this should update the
target_field inclusiontag/partial (via render_to_string) according to
the selected target.
If this is not an HTMX request, just call super().get.
"""
if request.htmx:
pk1 = int(self.kwargs.get('pk1', None))
pk2 = int(self.kwargs.get('pk2', None))
# get the target_id of the selected target: it's the primary
primary_target_id = int(request.GET.get('name_select', None))
# decide which of pk1 or pk2 is primary (i.e. it matches name_select)
if pk1 == primary_target_id: # first is primary, so
secondary_target_id = pk2
else: # second is primary, so
secondary_target_id = pk1
# get the actual Target instances for these target_ids
primary_target = Target.objects.get(id=primary_target_id)
secondary_target = Target.objects.get(id=secondary_target_id)
# render the table with those targets via the inclusiontag
target_table_html = render_to_string(
'tom_targets/partials/target_merge_fields.html',
context=target_merge_fields(primary_target, secondary_target))
# replace the old target_field table with the newly rendered one
return HttpResponse(target_table_html)
else:
# not an HTMX request
return super().get(request, *args, **kwargs)
[docs]
class TargetAddRemoveGroupingView(LoginRequiredMixin, View):
"""
View that handles addition and removal of targets to target groups. Requires authentication.
"""
[docs]
def post(self, request, *args, **kwargs):
"""
Handles the POST requests to this view. Routes the information from the request and query parameters to the
appropriate utility method in ``groups.py``.
:param request: the request object passed to this view
:type request: HTTPRequest
"""
query_string = request.POST.get('query_string', '')
grouping_id = request.POST.get('grouping')
filter_data = QueryDict(query_string)
if 'merge' in request.POST:
target_ids = request.POST.getlist('selected-target')
if len(target_ids) == 2:
return redirect('tom_targets:merge', pk1=target_ids[0], pk2=target_ids[1])
else:
merge_error_message(request)
else:
try:
grouping_object = TargetList.objects.get(pk=grouping_id)
except Exception as e:
messages.error(request, 'Cannot find the target group with id={}; {}'.format(grouping_id, e))
return redirect(reverse('tom_targets:list') + '?' + query_string)
if not request.user.has_perm('tom_targets.view_targetlist', grouping_object):
messages.error(request, 'Permission denied.')
return redirect(reverse('tom_targets:list') + '?' + query_string)
if 'add' in request.POST:
if request.POST.get('isSelectAll') == 'True':
add_all_to_grouping(filter_data, grouping_object, request)
else:
targets_ids = request.POST.getlist('selected-target')
add_selected_to_grouping(targets_ids, grouping_object, request)
if 'remove' in request.POST:
if request.POST.get('isSelectAll') == 'True':
remove_all_from_grouping(filter_data, grouping_object, request)
else:
targets_ids = request.POST.getlist('selected-target')
remove_selected_from_grouping(targets_ids, grouping_object, request)
if 'move' in request.POST:
if request.POST.get('isSelectAll') == 'True':
move_all_to_grouping(filter_data, grouping_object, request)
else:
target_ids = request.POST.getlist('selected-target')
move_selected_to_grouping(target_ids, grouping_object, request)
return redirect(reverse('tom_targets:list') + '?' + query_string)
[docs]
class TargetGroupingView(PermissionListMixin, HTMXTableViewMixin, FilterView):
"""
View that handles the display of ``TargetList`` objects, also known as target groups. Requires authorization.
"""
permission_required = 'tom_targets.view_targetlist'
template_name = 'tom_targets/target_grouping.html'
model = TargetList
table_class = TargetGroupTable
filterset_class = TargetGroupFilterSet
paginate_by = 5
[docs]
def get_context_data(self, *args, **kwargs):
"""
Adds ``settings.DATA_SHARING`` to the context to see if sharing has been configured.
:returns: context object
:rtype: dict
"""
context = super().get_context_data(*args, **kwargs)
context['sharing'] = getattr(settings, "DATA_SHARING", None)
return context
[docs]
class TargetGroupingDeleteView(Raise403PermissionRequiredMixin, DeleteView):
"""
View that handles the deletion of ``TargetList`` objects, also known as target groups. Requires authorization.
"""
permission_required = 'tom_targets.delete_targetlist'
model = TargetList
success_url = reverse_lazy('targets:targetgrouping')
[docs]
class TargetGroupingCreateView(LoginRequiredMixin, CreateView):
"""
View that handles the creation of ``TargetList`` objects, also known as target groups. Requires authentication.
"""
model = TargetList
fields = ['name']
success_url = reverse_lazy('targets:targetgrouping')
[docs]
class TargetGroupingShareView(FormView):
"""
View for sharing a TargetList. Requires authorization.
"""
template_name = 'tom_targets/target_group_share.html'
# Set app_name for Django-Guardian Permissions in case of Custom Target Model
permission_required = f'{Target._meta.app_label}.change_target'
form_class = TargetListShareForm
[docs]
def get_context_data(self, *args, **kwargs):
"""
Adds the ``TargetListShareForm`` to the context and prepopulates the hidden fields.
:returns: context object
:rtype: dict
"""
context = super().get_context_data(*args, **kwargs)
target_list_id = self.kwargs.get('pk', None)
target_list = TargetList.objects.get(id=target_list_id)
context['target_list'] = target_list
initial = {'submitter': self.request.user,
'target_list': target_list,
'share_title': f"Updated targets for group {target_list.name}."}
form = TargetListShareForm(initial=initial)
context['form'] = form
return context
[docs]
def get_success_url(self):
"""
Redirects to the target list page with the target list name as a query parameter.
"""
return reverse_lazy('targets:list') + f'?targetlist__name={self.kwargs.get("pk", None)}'
[docs]
class TargetFacilitySelectionView(Raise403PermissionRequiredMixin, FormView):
"""
View to select targets suitable to observe from a specific facility/location, taking into account target visibility
from that site, as well as other user-defined constraints.
"""
template_name = 'tom_targets/target_facility_selection.html'
strict = False
model = Target
paginate_by = 5
permission_required = 'tom_targets.view_target'
form_class = TargetSelectionForm
observable_targets = []
[docs]
def get_context_data(self, *args, **kwargs):
"""
Adds the ``TargetListShareForm`` to the context and prepopulates the hidden fields.
:returns: context object
:rtype: dict
"""
context = super().get_context_data(*args, **kwargs)
request = kwargs['request']
# Surely this needs to verify that the user has permission?
context['form'] = TargetSelectionForm(request.POST or None)
# The displayed table can be extended to include selected extra_fields for each target,
# if configured in the TOM's settings.py. So we set the list of table columns accordingly.
context['table_columns'] = [
'Target', 'Site', 'Min airmass', 'Rise time',
'Time of min airmass', 'Set time', 'Duration [hrs]'
] + getattr(settings, 'SELECTION_EXTRA_FIELDS', [])
return context
[docs]
def post(self, request, *args, **kwargs):
# Populate the form with data from the request
form = self.form_class(request.POST)
if form.is_valid():
targetlist = form.cleaned_data['target_list']
window_start = form.cleaned_data['window_start']
observatory = form.cleaned_data['observatory']
# Retrieve the full list of targets for this targetlist.
# This is paginated so that the visibility calculations
# are performed on a limited number of targets rather than
# all of them at once, which can be time consuming.
targets = targetlist.targets.all().order_by('name')
paginator = Paginator(targets, self.paginate_by)
targets_page = paginator.get_page(1)
# This list is stored in the session so that future GETs
# can retrieve different pages. The Target query set and date have to be
# in a format that can serialize to JSON
request.session['targets'] = [t.pk for t in targets]
request.session['window_start'] = window_start.strftime('%Y-%m-%d %H:%M:%S')
request.session['observatory'] = observatory
# Now calculate the visibilities of the restricted list of objects
target_visibilities = self.get_observable_targets(
targets_page,
window_start,
observatory
)
context = self.get_context_data(request=request, *args, **kwargs)
context['target_visibilities'] = target_visibilities
context['targets_page'] = targets_page
context['use_table'] = True
return render(request, self.template_name, context)
[docs]
def get(self, request, *args, **kwargs):
context = self.get_context_data(request=request, *args, **kwargs)
page = request.GET.get('page')
if page and 'targets' in request.session.keys():
targets = Target.objects.filter(pk__in=request.session['targets'])
window_start = datetime.strptime(request.session['window_start'], "%Y-%m-%d %H:%M:%S")
paginator = Paginator(targets, self.paginate_by)
targets_page = paginator.get_page(page)
target_visibilities = self.get_observable_targets(
targets_page,
window_start,
request.session['observatory']
)
context['target_visibilities'] = target_visibilities
context['targets_page'] = targets_page
context['use_table'] = True
else:
context['target_visibilities'] = None
context['targets_page'] = None
context['use_table'] = False
return render(request, self.template_name, context)
[docs]
def get_observable_targets(self, targets_page, window_start, observatory):
"""
Handles POST requests to select targets suitable for observation from this facility
:param targets_page: int Index of the page of targets to compute visbilities for
:param window_start: datetime object for the start of the time window to calculate for
:param observatory: str Full name of the observatory to compute for. This can refer to either
a facility module or general facilty entry
:return: HTTPRequest
"""
# Configuration:
# Maximum airmass limit to consider a target visible
# Intervals in minutes for which to calculate visibility throughout a single night
airmass_max = 2
visibiliy_intervals = 10
# Calculate the visibility of all selected targets for a 24hr window begining on the datetime given
# Since some observatories include multiple sites, the visibility_data returned is always
# a dictionary indexed by site code. Our purpose here is to verify whether each target is ever
# visible at lower airmass than the limit from any site - if so the target is considered to be visible
observable_targets = []
for target in targets_page:
window_end = window_start + timedelta(days=1)
visibility_data = get_sidereal_visibility(
target, window_start, window_end,
visibiliy_intervals, airmass_max,
facility_name=observatory
)
for site, vis_data in visibility_data.items():
airmass_data = np.array([[vis_data[0][i], x] for i, x in enumerate(vis_data[1]) if x])
if len(airmass_data) > 0:
# Find the timestamp of the minimum airmass
imin = np.where(airmass_data[:, 1] == airmass_data[:, 1].min())[0][0]
duration = airmass_data[:, 0][-1] - airmass_data[:, 0][0]
# Target entry includes:
# PK, name, site, minimum airmass, time of minimum airmass, rise time, set time
target_data = [
target.id,
target.name,
site,
round(airmass_data[:, 1].min(), 1),
airmass_data[:, 0][0].time().strftime("%H:%M:%S"),
airmass_data[:, 0][imin].time().strftime("%H:%M:%S"),
airmass_data[:, 0][-1].time().strftime("%H:%M:%S"),
str(duration).split('.')[0]
]
# Populate the results table with null entries for any targets
# that are not visible
else:
target_data = [
target.id,
target.name,
site,
'>'+str(airmass_max),
'-',
'-',
'-',
0
]
# Extract any requested extra parameters for this object, if available
for param in getattr(settings, 'SELECTION_EXTRA_FIELDS', []):
target_data.append(getattr(target, param, None))
observable_targets.append(target_data)
return observable_targets
[docs]
class PersistentShareManageTable(View):
def get(self, request):
context = {'request': request}
return render(request,
'tom_targets/partials/persistent_share_table.html',
context=persistent_share_table(context, None))
[docs]
class TargetPersistentShareManageTable(View):
def get(self, request, target_pk):
context = {'request': request}
target = Target.objects.get(pk=target_pk)
return render(request,
'tom_targets/partials/persistent_share_table.html',
context=persistent_share_table(context, target))
[docs]
class TargetSeedView(LoginRequiredMixin, View):
def post(self, request, *args, **kwargs):
seed_messier_targets()
return redirect(reverse('targets:list'))
def get(self, request, *args, **kwargs):
return redirect(reverse('targets:list'))