1
0
Fork 0
satnogs-db/db/base/views.py

593 lines
22 KiB
Python

"""Base django views for SatNOGS DB"""
import logging
from datetime import timedelta
from bootstrap_modal_forms.generic import BSModalCreateView, BSModalFormView, BSModalUpdateView
from django.conf import settings
from django.contrib import messages
from django.contrib.auth import get_user_model
from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.cache import cache
from django.core.exceptions import ObjectDoesNotExist
from django.core.paginator import Paginator
from django.db import OperationalError
from django.db.models import Count, Max, Prefetch, Q
from django.http import HttpResponse, HttpResponseServerError, JsonResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from django.utils.timezone import now
from django.views.decorators.http import require_POST
from db.base.forms import MergeSatellitesForm, SatelliteCreateForm, SatelliteUpdateForm, \
TransmitterCreateForm, TransmitterUpdateForm
from db.base.helpers import get_api_token
from db.base.models import DemodData, Satellite, SatelliteEntry, SatelliteIdentifier, \
SatelliteSuggestion, Transmitter, TransmitterEntry, TransmitterSuggestion
from db.base.tasks import export_frames, notify_suggestion
from db.base.utils import cache_statistics, millify, read_influx
LOGGER = logging.getLogger('db')
def home(request):
"""View to render home page.
:returns: base/home.html
"""
prefetch_approved = Prefetch(
'transmitter_entries', queryset=Transmitter.objects.all(), to_attr='approved_transmitters'
)
prefetch_suggested = Prefetch(
'transmitter_entries',
queryset=TransmitterSuggestion.objects.all(),
to_attr='suggested_transmitters'
)
newest_sats = Satellite.objects.filter(
associated_satellite__isnull=True
).order_by('-id')[:5].prefetch_related(prefetch_approved, prefetch_suggested)
# Calculate latest contributors
latest_data_satellites = []
found = False
date_from = now() - timedelta(days=1)
data_list = DemodData.objects.filter(timestamp__gte=date_from
).order_by('-pk').select_related('satellite')
paginator = Paginator(data_list, 50)
page = paginator.page(1)
while not found:
for data in page.object_list:
if data.satellite.id not in latest_data_satellites:
latest_data_satellites.append(data.satellite.id)
if len(latest_data_satellites) > 5:
found = True
break
if page.has_next():
page = paginator.page(page.next_page_number())
else:
break
# Check if satellite is merged and if it is then show its associated entry.
latest_data = Satellite.objects.filter(
associated_satellite__isnull=True
).filter(Q(pk__in=latest_data_satellites) | Q(associated_with__pk__in=latest_data_satellites)
).prefetch_related(prefetch_approved, prefetch_suggested)
# Calculate latest contributors
date_from = now() - timedelta(days=1)
latest_submitters = DemodData.objects.filter(timestamp__gte=date_from
).values('station').annotate(c=Count('station')
).order_by('-c')
return render(
request, 'base/home.html', {
'newest_sats': newest_sats,
'latest_data': latest_data,
'latest_submitters': latest_submitters
}
)
def transmitters_list(request):
"""View to render transmitters list page.
:returns: base/transmitters.html
"""
transmitters = Transmitter.objects.filter(
satellite__associated_satellite__isnull=True, satellite__satellite_entry__approved=True
).exclude(
status='invalid'
).select_related('satellite', 'satellite__satellite_entry', 'satellite__satellite_identifier')
return render(request, 'base/transmitters.html', {'transmitters': transmitters})
def robots(request):
"""robots.txt handler
:returns: robots.txt
"""
data = render(request, 'robots.txt', {'environment': settings.ENVIRONMENT})
response = HttpResponse(data, content_type='text/plain; charset=utf-8')
return response
def satellites(request):
"""View to render satellites page.
:returns: base/satellites.html
"""
satellite_objects = Satellite.objects.filter(
associated_satellite__isnull=True, satellite_entry__approved=True
).select_related('satellite_entry__operator').prefetch_related(
Prefetch(
'transmitter_entries',
queryset=Transmitter.objects.all(),
to_attr='approved_transmitters'
)
).annotate(
satellite_suggestions_count=Count(
'satellite_identifier__satellite_entries',
filter=Q(satellite_identifier__satellite_entries__reviewed__isnull=True)
)
)
return render(request, 'base/satellites.html', {'satellites': satellite_objects})
def satellite(request, norad=None, sat_id=None):
"""View to render satellite page.
:returns: base/satellite.html
"""
if norad:
satellite_obj = get_object_or_404(Satellite, satellite_entry__norad_cat_id=norad)
else:
satellite_obj = get_object_or_404(Satellite, satellite_identifier__sat_id=sat_id)
if satellite_obj.associated_satellite:
satellite_obj = satellite_obj.associated_satellite
latest_tle = None
latest_tle_set = None
if hasattr(satellite_obj, 'latest_tle_set'):
latest_tle_set = satellite_obj.latest_tle_set
if latest_tle_set:
if request.user.has_perm('base.access_all_tles'):
latest_tle = latest_tle_set.latest
else:
latest_tle = latest_tle_set.latest_distributable
transmitter_suggestions = TransmitterSuggestion.objects.filter(satellite=satellite_obj)
for suggestion in transmitter_suggestions:
try:
original_transmitter = satellite_obj.transmitters.get(uuid=suggestion.uuid)
suggestion.transmitter = original_transmitter
except Transmitter.DoesNotExist:
suggestion.transmitter = None
satellite_suggestions = SatelliteSuggestion.objects.filter(
satellite_identifier=satellite_obj.satellite_identifier
)
try:
# pull the last 5 observers and their submission timestamps for this
# satellite and for the satellite that are associated with it
recent_observers = DemodData.objects.filter(
Q(satellite=satellite_obj) | Q(satellite__associated_satellite=satellite_obj)
).values('observer').annotate(latest_payload=Max('timestamp')
).order_by('-latest_payload')[:5]
except (ObjectDoesNotExist, IndexError):
recent_observers = ''
# decide whether a map (and map link) will be visible or not (ie: re-entered)
showmap = False
if satellite_obj.satellite_entry.status not in ['re-entered', 'future'] and latest_tle:
showmap = True
return render(
request, 'base/satellite.html', {
'satellite': satellite_obj,
'latest_tle': latest_tle,
'transmitter_suggestions': transmitter_suggestions,
'satellite_suggestions': satellite_suggestions,
'mapbox_token': settings.MAPBOX_TOKEN,
'recent_observers': recent_observers,
'badge_telemetry_count': millify(satellite_obj.telemetry_data_count),
'showmap': showmap
}
)
@login_required
def request_export(request, sat_pk, period=None):
"""View to request frames export download.
This triggers a request to collect and zip up the requested data for
download, which the user is notified of via email when the celery task is
completed.
:returns: the originating satellite page
"""
satellite_obj = get_object_or_404(Satellite, id=sat_pk)
if satellite_obj.associated_satellite:
satellite_obj = satellite_obj.associated_satellite
export_frames.delay(satellite_obj.satellite_identifier.sat_id, request.user.id, period)
messages.success(
request, ('Your download request was received. '
'You will get an email when it\'s ready')
)
return redirect(
reverse('satellite', kwargs={'sat_id': satellite_obj.satellite_identifier.sat_id})
)
@login_required
@require_POST
def satellite_suggestion_handler(request):
"""Returns the Satellite page after approving or rejecting a suggestion if
user has approve permission.
:returns: Satellite page
"""
satellite_entry = get_object_or_404(SatelliteSuggestion, pk=request.POST['pk'])
satellite_obj = get_object_or_404(
Satellite, satellite_identifier=satellite_entry.satellite_identifier
)
if request.user.has_perm('base.approve_satellitesuggestion'):
if 'approve' in request.POST:
satellite_entry.approved = True
messages.success(request, ('Satellite approved.'))
elif 'reject' in request.POST:
satellite_entry.approved = False
messages.success(request, ('Satellite rejected.'))
satellite_entry.reviewed = now()
satellite_entry.reviewer = request.user
satellite_entry.save(update_fields=['approved', 'reviewed', 'reviewer'])
if satellite_entry.approved:
satellite_obj.satellite_entry = satellite_entry
satellite_obj.save(update_fields=['satellite_entry'])
redirect_page = redirect(
reverse('satellite', kwargs={'sat_id': satellite_obj.satellite_identifier.sat_id})
)
return redirect_page
@login_required
@require_POST
def transmitter_suggestion_handler(request):
"""Returns the Satellite page after approving or rejecting a suggestion if
user has approve permission.
:returns: Satellite page
"""
transmitter = get_object_or_404(TransmitterSuggestion, pk=request.POST['pk'])
if request.user.has_perm('base.approve_transmittersuggestion'):
if 'approve' in request.POST:
transmitter.approved = True
messages.success(request, ('Transmitter approved.'))
elif 'reject' in request.POST:
transmitter.approved = False
messages.success(request, ('Transmitter rejected.'))
transmitter.reviewed = now()
transmitter.reviewer = request.user
transmitter.save(update_fields=['approved', 'reviewed', 'reviewer'])
redirect_page = redirect(
reverse('satellite', kwargs={'sat_id': transmitter.satellite.satellite_identifier.sat_id})
)
return redirect_page
def about(request):
"""View to render about page.
:returns: base/about.html
"""
return render(request, 'base/about.html')
def satnogs_help(request):
"""View to render help modal. Have to avoid builtin 'help' name
:returns: base/modals/help.html
"""
return render(request, 'base/modals/satnogs_help.html')
def search(request):
"""View to render search page.
:returns: base/search.html
"""
query_string = ''
results = Satellite.objects.none()
if ('q' in request.GET) and request.GET['q'].strip():
query_string = request.GET['q']
if query_string:
results = Satellite.objects.filter(
associated_satellite__isnull=True, satellite_entry__approved=True
).filter(
Q(satellite_entry__name__icontains=query_string)
| Q(satellite_entry__names__icontains=query_string)
| Q(satellite_entry__norad_cat_id__icontains=query_string)
| Q(satellite_identifier__sat_id__icontains=query_string)
| Q(associated_with__satellite_identifier__sat_id__icontains=query_string)
).order_by('satellite_entry__name').prefetch_related(
Prefetch(
'transmitter_entries',
queryset=Transmitter.objects.all(),
to_attr='approved_transmitters'
)
)
if results.count() == 1:
return redirect(
reverse('satellite', kwargs={'sat_id': results[0].satellite_identifier.sat_id})
)
return render(request, 'base/search.html', {'results': results, 'q': query_string})
def stats(request):
"""View to render stats page.
:returns: base/stats.html
"""
cached_satellites = []
ids = cache.get('satellites_ids')
observers = cache.get('stats_observers')
if not ids or not observers:
try:
cache_statistics()
ids = cache.get('satellites_ids')
observers = cache.get('stats_observers')
except OperationalError:
pass
for sid in ids:
stat = cache.get(sid)
cached_satellites.append(stat)
return render(
request, 'base/stats.html', {
'satellites': cached_satellites,
'observers': observers
}
)
def statistics(request):
"""Triggers a refresh of cached statistics if the cache does not exist
:returns: JsonResponse of statistics
"""
cached_stats = cache.get('stats_transmitters')
if not cached_stats:
cache_statistics()
cached_stats = []
return JsonResponse(cached_stats, safe=False)
@login_required
def users_edit(request):
"""View to render user settings page.
:returns: base/users_edit.html
"""
token = get_api_token(request.user)
return render(request, 'base/modals/users_edit.html', {'token': token})
def recent_decoded_cnt(request, norad):
"""Returns a query of InfluxDB for a count of points across a given measurement
(norad) over the last 30 days, with a timestamp in unixtime.
:returns: JSON of point counts as JsonResponse
"""
if settings.USE_INFLUX:
results = read_influx(norad)
return JsonResponse(results, safe=False)
return HttpResponseServerError()
class TransmitterCreateView(LoginRequiredMixin, BSModalCreateView):
"""A django-bootstrap-modal-forms view for creating transmitter suggestions"""
template_name = 'base/modals/transmitter_create.html'
model = TransmitterEntry
form_class = TransmitterCreateForm
success_message = 'Your transmitter suggestion was stored successfully and will be \
reviewed by a moderator. Thanks for contibuting!'
satellite = Satellite()
user = get_user_model()
def dispatch(self, request, *args, **kwargs):
"""
Overridden so we can make sure the `Satellite` instance exists first
"""
self.satellite = get_object_or_404(Satellite, pk=kwargs['satellite_pk'])
self.user = request.user
return super().dispatch(request, *args, **kwargs)
def form_valid(self, form):
"""
Overridden to add the `Satellite` relation to the `Transmitter` instance.
"""
transmitter = form.instance
transmitter.satellite = self.satellite
transmitter.created = now()
transmitter.created_by = self.user
# Prevents sending notification twice as form_valid is triggered for validation and saving
# Check if request is an AJAX one
if not self.request.headers.get('x-requested-with') == 'XMLHttpRequest':
notify_suggestion.delay(
transmitter.satellite.satellite_entry.id, self.user.id, 'transmitter'
)
return super().form_valid(form)
def get_success_url(self):
return self.request.META.get('HTTP_REFERER')
class TransmitterUpdateView(LoginRequiredMixin, BSModalUpdateView):
"""A django-bootstrap-modal-forms view for updating transmitter entries"""
template_name = 'base/modals/transmitter_update.html'
model = TransmitterEntry
form_class = TransmitterUpdateForm
success_message = 'Your transmitter suggestion was stored successfully and will be \
reviewed by a moderator. Thanks for contributing!'
user = get_user_model()
def dispatch(self, request, *args, **kwargs):
self.user = request.user
return super().dispatch(request, *args, **kwargs)
def form_valid(self, form):
transmitter = form.instance
# Add update as a new TransmitterEntry object and change fields in order to be a suggestion
transmitter.pk = None
transmitter.reviewed = None
transmitter.reviewer = None
transmitter.approved = False
transmitter.created = now()
transmitter.created_by = self.user
# Prevents sending notification twice as form_valid is triggered for validation and saving
# Check if request is an AJAX one
if not self.request.headers.get('x-requested-with') == 'XMLHttpRequest':
notify_suggestion.delay(
transmitter.satellite.satellite_entry.id, self.user.id, 'transmitter'
)
return super().form_valid(form)
def get_success_url(self):
return self.request.META.get('HTTP_REFERER')
class MergeSatellitesView(LoginRequiredMixin, BSModalFormView):
"""Merges satellites if user has merge permission.
"""
template_name = 'base/modals/satellites_merge.html'
form_class = MergeSatellitesForm
user = get_user_model()
def dispatch(self, request, *args, **kwargs):
self.user = request.user
return super().dispatch(request, *args, **kwargs)
def form_valid(self, form):
response = super().form_valid(form)
if self.user.has_perm('base.merge_satellites'):
# Check if request is an AJAX one
if not self.request.headers.get('x-requested-with') == 'XMLHttpRequest':
primary_satellite = form.cleaned_data['primary_satellite']
associated_satellite = form.cleaned_data['associated_satellite']
associated_satellite.associated_satellite = primary_satellite
associated_satellite.save(update_fields=['associated_satellite'])
messages.success(self.request, ('Satellites have been merged!'))
else:
messages.error(self.request, ('No permission to merge satellites!'))
response = redirect(reverse('satellites'))
return response
def get_success_url(self):
return self.request.META.get('HTTP_REFERER')
class SatelliteCreateView(LoginRequiredMixin, BSModalCreateView):
"""A django-bootstrap-modal-forms view for creating satellite suggestions"""
template_name = 'base/modals/satellite_create.html'
model = SatelliteEntry
form_class = SatelliteCreateForm
success_message = 'Your satellite suggestion was stored successfully and will be \
reviewed by a moderator. Thanks for contributing!'
user = get_user_model()
def dispatch(self, request, *args, **kwargs):
self.user = request.user
return super().dispatch(request, *args, **kwargs)
def form_valid(self, form):
satellite_entry = form.instance
satellite_obj = None
# Create Satellite Identifier only when POST request is for saving and
# NORAD ID is not used by other Satellite.
# Check if request is an AJAX one
if not self.request.headers.get('x-requested-with') == 'XMLHttpRequest':
try:
# If the form doesn't contain NORAD ID, create a new satellite
if satellite_entry.norad_cat_id:
satellite_obj = Satellite.objects.get(
satellite_entry__norad_cat_id=satellite_entry.norad_cat_id
)
satellite_entry.satellite_identifier = satellite_obj.satellite_identifier
else:
satellite_entry.satellite_identifier = SatelliteIdentifier.objects.create()
except Satellite.DoesNotExist:
satellite_entry.satellite_identifier = SatelliteIdentifier.objects.create()
satellite_entry.created = now()
satellite_entry.created_by = self.user
# form_valid triggers also save() allowing us to use satellite_entry
# for creating Satellite object, see comment bellow.
response = super().form_valid(form)
# Prevents sending notification twice as form_valid is triggered for
# validation and saving. Also create and Satellite object only when POST
# request is for saving and NORAD ID is not used by other Satellite.
# Check if request is an AJAX one
if not self.request.headers.get('x-requested-with') == 'XMLHttpRequest':
if not satellite_obj:
satellite_obj = Satellite.objects.create(
satellite_identifier=satellite_entry.satellite_identifier,
satellite_entry=satellite_entry
)
notify_suggestion.delay(satellite_obj.satellite_entry.pk, self.user.id, 'satellite')
return response
def get_success_url(self):
return self.request.META.get('HTTP_REFERER')
class SatelliteUpdateView(LoginRequiredMixin, BSModalUpdateView):
"""A django-bootstrap-modal-forms view for updating satellite entries"""
template_name = 'base/modals/satellite_update.html'
model = SatelliteEntry
form_class = SatelliteUpdateForm
success_message = 'Your satellite suggestion was stored successfully and will be \
reviewed by a moderator. Thanks for contributing!'
user = get_user_model()
def dispatch(self, request, *args, **kwargs):
self.user = request.user
return super().dispatch(request, *args, **kwargs)
def form_valid(self, form):
satellite_entry = form.instance
initial_satellite_entry_pk = satellite_entry.pk
# Add update as a new SatelliteEntry object and change fields in order to be a suggestion
satellite_entry.pk = None
satellite_entry.reviewed = None
satellite_entry.reviewer = None
satellite_entry.approved = False
satellite_entry.created = now()
satellite_entry.created_by = self.user
# Prevents sending notification twice as form_valid is triggered for validation and saving
# Check if request is an AJAX one
if not self.request.headers.get('x-requested-with') == 'XMLHttpRequest':
notify_suggestion.delay(initial_satellite_entry_pk, self.user.id, 'satellite')
return super().form_valid(form)
def get_success_url(self):
return self.request.META.get('HTTP_REFERER')