593 lines
22 KiB
Python
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')
|