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

640 lines
25 KiB
Python

import urllib2
import ephem
import math
from operator import itemgetter
from datetime import datetime, timedelta
from StringIO import StringIO
from django.conf import settings
from django.contrib import messages
from django.views.decorators.http import require_POST
from django.shortcuts import get_object_or_404, render, redirect
from django.core.urlresolvers import reverse
from django.utils.timezone import now, make_aware, utc
from django.utils.text import slugify
from django.http import JsonResponse, HttpResponseNotFound, HttpResponseServerError, HttpResponse
from django.contrib.auth.decorators import login_required
from django.core.management import call_command
from django.views.generic import ListView
from rest_framework import serializers, viewsets
from network.base.models import (Station, Transmitter, Observation,
Data, Satellite, Antenna, Tle, Rig)
from network.base.forms import StationForm, SatelliteFilterForm
from network.base.decorators import admin_required
class StationSerializer(serializers.ModelSerializer):
class Meta:
model = Station
fields = ('name', 'lat', 'lng')
class StationAllView(viewsets.ReadOnlyModelViewSet):
queryset = Station.objects.filter(active=True)
serializer_class = StationSerializer
def satellite_position(request, sat_id):
sat = get_object_or_404(Satellite, norad_cat_id=sat_id)
satellite = ephem.readtle(
str(sat.latest_tle.tle0),
str(sat.latest_tle.tle1),
str(sat.latest_tle.tle2)
)
now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
satellite.compute(now)
data = {
'lon': '{0}'.format(satellite.sublong),
'lat': '{0}'.format(satellite.sublat)
}
return JsonResponse(data, safe=False)
def _resolve_overlaps(station, start, end):
data = Data.objects.filter(ground_station=station)
if data:
for datum in data:
if datum.is_past:
continue
if datum.start <= end and start <= datum.end:
if datum.start <= start and datum.end >= end:
return False
if start < datum.start and end > datum.end:
start1 = start
end1 = datum.start
start2 = datum.end
end2 = end
return start1, end1, start2, end2
if datum.start <= start:
start = datum.end
if datum.end >= end:
end = datum.start
return start, end
def index(request):
"""View to render index page."""
observations = Observation.objects.all()
try:
featured_station = Station.objects.filter(active=True).latest('featured_date')
except Station.DoesNotExist:
featured_station = None
ctx = {
'latest_observations': observations.filter(end__lt=now()).order_by('-id')[:10],
'scheduled_observations': observations.filter(end__gte=now()),
'featured_station': featured_station,
'mapbox_id': settings.MAPBOX_MAP_ID,
'mapbox_token': settings.MAPBOX_TOKEN
}
return render(request, 'base/home.html', ctx)
def custom_404(request):
"""Custom 404 error handler."""
return HttpResponseNotFound(render(request, '404.html'))
def custom_500(request):
"""Custom 500 error handler."""
return HttpResponseServerError(render(request, '500.html'))
def robots(request):
data = render(request, 'robots.txt', {'environment': settings.ENVIRONMENT})
response = HttpResponse(data,
content_type='text/plain; charset=utf-8')
return response
@admin_required
def settings_site(request):
"""View to render settings page."""
if request.method == 'POST':
if request.POST['fetch']:
try:
data_out = StringIO()
tle_out = StringIO()
call_command('fetch_data', stdout=data_out)
call_command('update_all_tle', stdout=tle_out)
request.session['settings_out'] = data_out.getvalue() + tle_out.getvalue()
except:
messages.error(request, 'fetch command failed.')
return redirect(reverse('base:settings_site'))
fetch_out = request.session.get('settings_out', False)
if fetch_out:
del request.session['settings_out']
return render(request, 'base/settings_site.html', {'fetch_data': fetch_out})
return render(request, 'base/settings_site.html')
class ObservationListView(ListView):
"""
Displays a list of observations with pagination
"""
model = Observation
context_object_name = "observations"
paginate_by = settings.ITEMS_PER_PAGE
template_name = 'base/observations.html'
def get_queryset(self):
"""
Optionally filter based on norad get argument
Optionally filter based on good/bad/unvetted
"""
norad_cat_id = self.request.GET.get('norad', '')
bad = self.request.GET.get('bad', '1')
if bad == '0':
bad = False
else:
bad = True
good = self.request.GET.get('good', '1')
if good == '0':
good = False
else:
good = True
unvetted = self.request.GET.get('unvetted', '1')
if unvetted == '0':
unvetted = False
else:
unvetted = True
if norad_cat_id == '':
observations = Observation.objects.all().order_by('-id')
else:
observations = Observation.objects.filter(
satellite__norad_cat_id=norad_cat_id).order_by('-id')
# Good/Bad/Unvetted are properties of the model not fields
# so cannot use queryset filtering
resultset = []
for ob in observations:
if bad and ob.has_no_data:
resultset.append(ob)
elif good and ob.has_verified_data:
resultset.append(ob)
elif unvetted and ob.has_unvetted_data:
resultset.append(ob)
return resultset
def get_context_data(self, **kwargs):
"""
Need to add a list of satellites to the context for the template
"""
context = super(ObservationListView, self).get_context_data(**kwargs)
context['satellites'] = Satellite.objects.all()
norad_cat_id = self.request.GET.get('norad', None)
context['bad'] = self.request.GET.get('bad', '1')
context['good'] = self.request.GET.get('good', '1')
context['unvetted'] = self.request.GET.get('unvetted', '1')
if norad_cat_id is not None and norad_cat_id != '':
context['norad'] = int(norad_cat_id)
return context
@login_required
def observation_new(request):
"""View for new observation"""
me = request.user
if request.method == 'POST':
sat_id = request.POST.get('satellite')
trans_id = request.POST.get('transmitter')
try:
start_time = datetime.strptime(request.POST.get('start-time'), '%Y-%m-%d %H:%M')
end_time = datetime.strptime(request.POST.get('end-time'), '%Y-%m-%d %H:%M')
except ValueError:
messages.error(request, 'Please use the datetime dialogs to submit valid values.')
return redirect(reverse('base:observation_new'))
if (end_time - start_time) > timedelta(minutes=int(settings.DATE_MAX_RANGE)):
messages.error(request, 'Please use the datetime dialogs to submit valid timeframe.')
return redirect(reverse('base:observation_new'))
start = make_aware(start_time, utc)
end = make_aware(end_time, utc)
sat = Satellite.objects.get(norad_cat_id=sat_id)
trans = Transmitter.objects.get(id=trans_id)
tle = Tle.objects.get(id=sat.latest_tle.id)
obs = Observation(satellite=sat, transmitter=trans, tle=tle,
author=me, start=start, end=end)
obs.save()
total = int(request.POST.get('total'))
for item in range(total):
start = datetime.strptime(
request.POST.get('{0}-starting_time'.format(item)), '%Y-%m-%d %H:%M:%S.%f'
)
end = datetime.strptime(
request.POST.get('{}-ending_time'.format(item)), '%Y-%m-%d %H:%M:%S.%f'
)
station_id = request.POST.get('{}-station'.format(item))
ground_station = Station.objects.get(id=station_id)
Data.objects.create(start=make_aware(start, utc), end=make_aware(end, utc),
ground_station=ground_station, observation=obs)
return redirect(reverse('base:observation_view', kwargs={'id': obs.id}))
satellites = Satellite.objects.filter(transmitters__alive=True).distinct()
transmitters = Transmitter.objects.filter(alive=True)
obs_filter = {}
if request.method == 'GET':
filter_form = SatelliteFilterForm(request.GET)
if filter_form.is_valid():
start_date = filter_form.cleaned_data['start_date']
end_date = filter_form.cleaned_data['end_date']
ground_station = filter_form.cleaned_data['ground_station']
norad = filter_form.cleaned_data['norad']
if start_date:
start_date = datetime.strptime(start_date,
'%Y/%m/%d %H:%M').strftime('%Y-%m-%d %H:%M')
if end_date:
end_date = (datetime.strptime(end_date, '%Y/%m/%d %H:%M') +
timedelta(minutes=1)).strftime('%Y-%m-%d %H:%M')
obs_filter['exists'] = True
obs_filter['norad'] = norad
obs_filter['start_date'] = start_date
obs_filter['end_date'] = end_date
obs_filter['ground_station'] = ground_station
else:
obs_filter['exists'] = False
return render(request, 'base/observation_new.html',
{'satellites': satellites,
'transmitters': transmitters, 'obs_filter': obs_filter,
'date_min_start': settings.DATE_MIN_START,
'date_min_end': settings.DATE_MIN_END,
'date_max_range': settings.DATE_MAX_RANGE})
def prediction_windows(request, sat_id, start_date, end_date, station_id=None):
try:
sat = Satellite.objects.filter(transmitters__alive=True). \
distinct().get(norad_cat_id=sat_id)
except:
data = {
'error': 'You should select a Satellite first.'
}
return JsonResponse(data, safe=False)
try:
satellite = ephem.readtle(
str(sat.latest_tle.tle0),
str(sat.latest_tle.tle1),
str(sat.latest_tle.tle2)
)
except:
data = {
'error': 'No TLEs for this satellite yet.'
}
return JsonResponse(data, safe=False)
end_date = datetime.strptime(end_date, '%Y-%m-%d %H:%M')
data = []
stations = Station.objects.all()
if station_id:
stations = stations.filter(id=station_id)
for station in stations:
if not station.online:
continue
observer = ephem.Observer()
observer.lon = str(station.lng)
observer.lat = str(station.lat)
observer.elevation = station.alt
observer.date = str(start_date)
station_match = False
keep_digging = True
while keep_digging:
try:
tr, azr, tt, altt, ts, azs = observer.next_pass(satellite)
except ValueError:
data = {
'error': 'That satellite seems to stay always below your horizon.'
}
break
# no match if the sat will not rise above the configured min horizon
elevation = format(math.degrees(altt), '.0f')
if float(elevation) >= station.horizon:
if ephem.Date(tr).datetime() < end_date:
if ephem.Date(ts).datetime() > end_date:
ts = end_date
keep_digging = False
else:
time_start_new = ephem.Date(ts).datetime() + timedelta(minutes=1)
observer.date = time_start_new.strftime("%Y-%m-%d %H:%M:%S.%f")
# Adjust or discard window if overlaps exist
window_start = make_aware(ephem.Date(tr).datetime(), utc)
window_end = make_aware(ephem.Date(ts).datetime(), utc)
window = _resolve_overlaps(station, window_start, window_end)
if window:
if not station_match:
station_windows = {
'id': station.id,
'name': station.name,
'window': []
}
station_match = True
window_start = window[0]
window_end = window[1]
station_windows['window'].append(
{
'start': window_start.strftime("%Y-%m-%d %H:%M:%S.%f"),
'end': window_end.strftime("%Y-%m-%d %H:%M:%S.%f"),
'az_start': azr
})
# In case our window was split in two
try:
window_start = window[2]
window_end = window[3]
station_windows['window'].append(
{
'start': window_start.strftime("%Y-%m-%d %H:%M:%S.%f"),
'end': window_end.strftime("%Y-%m-%d %H:%M:%S.%f"),
'az_start': azr
})
except:
pass
else:
# window start outside of window bounds
break
else:
# did not rise above user configured horizon
break
if station_match:
data.append(station_windows)
return JsonResponse(data, safe=False)
def observation_view(request, id):
"""View for single observation page."""
observation = get_object_or_404(Observation, id=id)
dataset = Data.objects.filter(observation=observation)
# not all users will be able to vet data within an observation, allow
# staff, observation requestors, and station owners
is_vetting_user = False
if request.user.is_authenticated():
if request.user == observation.author or \
dataset.filter(
ground_station__in=Station.objects.filter(owner=request.user)).count or \
request.user.is_staff:
is_vetting_user = True
# Determine if there is no valid payload file in the observation dataset
if request.user.has_perm('base.delete_observation'):
data_payload_exists = False
for data in dataset:
if data.payload_exists:
data_payload_exists = True
# This context flag will determine if a delete button appears for the observation.
is_deletable = False
if observation.author == request.user and observation.is_deletable_before_start:
is_deletable = True
if request.user.has_perm('base.delete_observation') and not data_payload_exists and \
observation.is_deletable_after_end:
is_deletable = True
if settings.ENVIRONMENT == 'production':
discuss_slug = 'https://community.satnogs.org/t/observation-{0}-{1}-{2}' \
.format(observation.id, slugify(observation.satellite.name),
observation.satellite.norad_cat_id)
discuss_url = ('https://community.satnogs.org/new-topic?title=Observation {0}: {1}'
' ({2})&body=Regarding [Observation {3}](http://{4}{5}) ...'
'&category=observations') \
.format(observation.id, observation.satellite.name,
observation.satellite.norad_cat_id, observation.id,
request.get_host(), request.path)
try:
apiurl = '{0}.json'.format(discuss_slug)
urllib2.urlopen(apiurl).read()
has_comments = True
except:
has_comments = False
return render(request, 'base/observation_view.html',
{'observation': observation, 'dataset': dataset,
'has_comments': has_comments, 'discuss_url': discuss_url,
'discuss_slug': discuss_slug, 'is_vetting_user': is_vetting_user,
'is_deletable': is_deletable})
return render(request, 'base/observation_view.html',
{'observation': observation, 'dataset': dataset,
'is_vetting_user': is_vetting_user, 'is_deletable': is_deletable})
@login_required
def observation_delete(request, id):
"""View for deleting observation."""
me = request.user
observation = get_object_or_404(Observation, id=id)
# Having non-existent data is also grounds for deletion if user is staff
data_payload_exists = False
for data in observation.data_set.all():
if data.payload_exists:
data_payload_exists = True
if (observation.author == me and observation.is_deletable_before_start) or \
(request.user.has_perm('base.delete_observation') and
not data_payload_exists and observation.is_deletable_after_end):
observation.delete()
messages.success(request, 'Observation deleted successfully.')
else:
messages.error(request, 'Permission denied.')
return redirect(reverse('base:observations_list'))
@login_required
def data_verify(request, id):
me = request.user
data = get_object_or_404(Data, id=id)
data.vetted_status = 'verified'
data.vetted_user = me
data.vetted_datetime = datetime.today()
data.save(update_fields=['vetted_status', 'vetted_user', 'vetted_datetime'])
return redirect(reverse('base:observation_view', kwargs={'id': data.observation}))
@login_required
def data_mark_bad(request, id):
me = request.user
data = get_object_or_404(Data, id=id)
data.vetted_status = 'no_data'
data.vetted_user = me
data.vetted_datetime = datetime.today()
data.save(update_fields=['vetted_status', 'vetted_user', 'vetted_datetime'])
return redirect(reverse('base:observation_view', kwargs={'id': data.observation}))
def stations_list(request):
"""View to render Stations page."""
stations = Station.objects.all()
form = StationForm()
antennas = Antenna.objects.all()
return render(request, 'base/stations.html',
{'stations': stations, 'form': form, 'antennas': antennas})
def station_view(request, id):
"""View for single station page."""
station = get_object_or_404(Station, id=id)
form = StationForm(instance=station)
antennas = Antenna.objects.all()
rigs = Rig.objects.all()
try:
satellites = Satellite.objects.filter(transmitters__alive=True).distinct()
except:
pass # we won't have any next passes to display
# Load the station information and invoke ephem so we can
# calculate upcoming passes for the station
observer = ephem.Observer()
observer.lon = str(station.lng)
observer.lat = str(station.lat)
observer.elevation = station.alt
nextpasses = []
passid = 0
for satellite in satellites:
observer.date = ephem.date(datetime.today())
try:
sat_ephem = ephem.readtle(str(satellite.latest_tle.tle0),
str(satellite.latest_tle.tle1),
str(satellite.latest_tle.tle2))
# Here we are going to iterate over each satellite to
# find its appropriate passes within a given time constraint
keep_digging = True
while keep_digging:
try:
tr, azr, tt, altt, ts, azs = observer.next_pass(sat_ephem)
if tr is None:
break
# using the angles module convert the sexagesimal degree into
# something more easily read by a human
elevation = format(math.degrees(altt), '.0f')
azimuth = format(math.degrees(azr), '.0f')
passid += 1
# show only if >= configured horizon and in next 6 hours,
# and not directly overhead (tr < ts see issue 199)
if tr < ephem.date(datetime.today() + timedelta(hours=6)):
if (float(elevation) >= station.horizon and tr < ts):
valid = True
if tr < ephem.Date(datetime.now() +
timedelta(minutes=int(settings.DATE_MIN_START))):
valid = False
sat_pass = {'passid': passid,
'mytime': str(observer.date),
'debug': observer.next_pass(sat_ephem),
'name': str(satellite.name),
'id': str(satellite.id),
'norad_cat_id': str(satellite.norad_cat_id),
'tr': str(tr), # Rise time
'azr': azimuth, # Rise Azimuth
'tt': tt, # Max altitude time
'altt': elevation, # Max altitude
'ts': str(ts), # Set time
'azs': azs, # Set azimuth
'valid': valid}
nextpasses.append(sat_pass)
observer.date = ephem.Date(ts).datetime() + timedelta(minutes=1)
continue
else:
keep_digging = False
continue
except ValueError:
break # there will be sats in our list that fall below horizon, skip
except TypeError:
break # if there happens to be a non-EarthSatellite object in the list
except Exception:
break
except (ValueError, AttributeError):
pass # TODO: if something does not have a proper TLE line we need to know/fix
return render(request, 'base/station_view.html',
{'station': station, 'form': form, 'antennas': antennas,
'mapbox_id': settings.MAPBOX_MAP_ID,
'mapbox_token': settings.MAPBOX_TOKEN,
'nextpasses': sorted(nextpasses, key=itemgetter('tr')),
'rigs': rigs})
@require_POST
def station_edit(request):
"""Edit or add a single station."""
if request.POST['id']:
pk = request.POST.get('id')
station = get_object_or_404(Station, id=pk, owner=request.user)
form = StationForm(request.POST, request.FILES, instance=station)
else:
form = StationForm(request.POST, request.FILES)
if form.is_valid():
f = form.save(commit=False)
f.owner = request.user
f.save()
form.save_m2m()
if f.online:
messages.success(request, 'Successfully saved Ground Station.')
else:
messages.success(request, ('Successfully saved Ground Station. It will appear online '
'as soon as it connects with our API.'))
return redirect(reverse('base:station_view', kwargs={'id': f.id}))
else:
messages.error(request, 'Your Station submission had some errors.{0}'.format(form.errors))
return redirect(reverse('users:view_user', kwargs={'username': request.user.username}))
@login_required
def station_delete(request, id):
"""View for deleting a station."""
me = request.user
station = get_object_or_404(Station, id=id, owner=request.user)
station.delete()
messages.success(request, 'Ground Station deleted successfully.')
return redirect(reverse('users:view_user', kwargs={'username': me}))
def satellite_view(request, id):
try:
sat = get_object_or_404(Satellite, norad_cat_id=id)
except:
data = {
'error': 'Unable to find that satellite.'
}
return JsonResponse(data, safe=False)
data = {
'id': id,
'name': sat.name,
'names': sat.names,
'image': sat.image,
}
return JsonResponse(data, safe=False)
def observation_data_view(request, id):
observation = get_object_or_404(Observation, data__id=id)
return redirect(reverse('base:observation_view',
kwargs={'id': observation.id}) + '#{0}'.format(id))