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))