1
0
Fork 0

Merge branch 'remove-data-model' into 'dev'

Refactor models to remove Data table

See merge request !401
environments/stage/deployments/6
Nikos Roussos 2017-09-13 20:09:49 +00:00
commit b6c0132576
30 changed files with 826 additions and 756 deletions

View File

@ -1,14 +1,14 @@
import django_filters
from network.base.models import Data
from network.base.models import Observation
class DataViewFilter(django_filters.FilterSet):
class ObservationViewFilter(django_filters.FilterSet):
start = django_filters.IsoDateTimeFilter(name='start', lookup_expr='gte')
end = django_filters.IsoDateTimeFilter(name='end', lookup_expr='lte')
norad = django_filters.NumberFilter(name='observation__satellite__norad_cat_id',
lookup_expr='iexact')
class Meta:
model = Data
model = Observation
fields = ['ground_station']

View File

@ -1,6 +1,6 @@
from rest_framework import serializers
from network.base.models import Data, Station, DemodData
from network.base.models import Observation, Station, DemodData
class DemodDataSerializer(serializers.ModelSerializer):
@ -9,7 +9,7 @@ class DemodDataSerializer(serializers.ModelSerializer):
fields = ('payload_demod', )
class DataSerializer(serializers.ModelSerializer):
class ObservationSerializer(serializers.ModelSerializer):
transmitter = serializers.SerializerMethodField()
norad_cat_id = serializers.SerializerMethodField()
station_name = serializers.SerializerMethodField()
@ -18,8 +18,8 @@ class DataSerializer(serializers.ModelSerializer):
demoddata = DemodDataSerializer(many=True)
class Meta:
model = Data
fields = ('id', 'start', 'end', 'observation', 'ground_station', 'transmitter',
model = Observation
fields = ('id', 'start', 'end', 'ground_station', 'transmitter',
'norad_cat_id', 'payload', 'waterfall', 'demoddata', 'station_name',
'station_lat', 'station_lng')
read_only_fields = ['id', 'start', 'end', 'observation', 'ground_station',
@ -28,17 +28,17 @@ class DataSerializer(serializers.ModelSerializer):
def update(self, instance, validated_data):
validated_data.pop('demoddata')
super(DataSerializer, self).update(instance, validated_data)
super(ObservationSerializer, self).update(instance, validated_data)
return instance
def get_transmitter(self, obj):
try:
return obj.observation.transmitter.uuid
return obj.transmitter.uuid
except AttributeError:
return ''
def get_norad_cat_id(self, obj):
return obj.observation.satellite.norad_cat_id
return obj.satellite.norad_cat_id
def get_station_name(self, obj):
return obj.ground_station.name
@ -59,28 +59,28 @@ class JobSerializer(serializers.ModelSerializer):
transmitter = serializers.SerializerMethodField()
class Meta:
model = Data
model = Observation
fields = ('id', 'start', 'end', 'ground_station', 'tle0', 'tle1', 'tle2',
'frequency', 'mode', 'transmitter')
def get_frequency(self, obj):
return obj.observation.transmitter.downlink_low
return obj.transmitter.downlink_low
def get_transmitter(self, obj):
return obj.observation.transmitter.uuid
return obj.transmitter.uuid
def get_tle0(self, obj):
return obj.observation.tle.tle0
return obj.tle.tle0
def get_tle1(self, obj):
return obj.observation.tle.tle1
return obj.tle.tle1
def get_tle2(self, obj):
return obj.observation.tle.tle2
return obj.tle.tle2
def get_mode(self, obj):
try:
return obj.observation.transmitter.mode.name
return obj.transmitter.mode.name
except:
return ''

View File

@ -4,7 +4,7 @@ import pytest
from django.test import TestCase
from network.base.tests import (
DataFactory,
ObservationFactory,
SatelliteFactory,
TransmitterFactory,
StationFactory
@ -16,7 +16,7 @@ class JobViewApiTest(TestCase):
"""
Tests the Job View API
"""
data = None
observation = None
satellites = []
transmitters = []
stations = []
@ -28,7 +28,7 @@ class JobViewApiTest(TestCase):
self.transmitters.append(TransmitterFactory())
for x in xrange(1, 10):
self.stations.append(StationFactory())
self.data = DataFactory()
self.observation = ObservationFactory()
def test_job_view_api(self):
response = self.client.get('/api/jobs/')

View File

@ -5,7 +5,7 @@ from network.api import views
router = routers.DefaultRouter()
router.register(r'jobs', views.JobView, base_name='jobs')
router.register(r'data', views.DataView, base_name='data')
router.register(r'data', views.ObservationView, base_name='data')
router.register(r'settings', views.SettingsView, base_name='settings')
api_urlpatterns = router.urls

View File

@ -7,13 +7,13 @@ from rest_framework.response import Response
from network.api.perms import StationOwnerCanEditPermission
from network.api import serializers, filters
from network.base.models import Data, Station
from network.base.models import Observation, Station
class DataView(viewsets.ModelViewSet, mixins.UpdateModelMixin):
queryset = Data.objects.all()
serializer_class = serializers.DataSerializer
filter_class = filters.DataViewFilter
class ObservationView(viewsets.ModelViewSet, mixins.UpdateModelMixin):
queryset = Observation.objects.all()
serializer_class = serializers.ObservationSerializer
filter_class = filters.ObservationViewFilter
permission_classes = [
StationOwnerCanEditPermission
]
@ -23,14 +23,14 @@ class DataView(viewsets.ModelViewSet, mixins.UpdateModelMixin):
instance = self.get_object()
instance.demoddata.create(payload_demod=request.data.get('demoddata'))
super(DataView, self).update(request, *args, **kwargs)
super(ObservationView, self).update(request, *args, **kwargs)
return Response(status=status.HTTP_200_OK)
class JobView(viewsets.ReadOnlyModelViewSet):
queryset = Data.objects.filter(payload='')
queryset = Observation.objects.filter(payload='')
serializer_class = serializers.JobSerializer
filter_class = filters.DataViewFilter
filter_class = filters.ObservationViewFilter
filter_fields = ('ground_station')
def get_queryset(self):

View File

@ -1,7 +1,7 @@
from django.contrib import admin
from network.base.models import (Antenna, Satellite, Station, Transmitter,
Observation, Data, Mode, Tle, Rig, DemodData)
Observation, Mode, Tle, Rig, DemodData)
@admin.register(Rig)
@ -71,18 +71,3 @@ class ObservationAdmin(admin.ModelAdmin):
class DataDemodInline(admin.TabularInline):
model = DemodData
@admin.register(Data)
class DataAdmin(admin.ModelAdmin):
list_display = ('id', 'start_date', 'end_date', 'observation', 'ground_station')
readonly_fields = ('observation', 'ground_station')
inlines = [
DataDemodInline,
]
def start_date(self, obj):
return obj.start.strftime('%d.%m.%Y, %H:%M')
def end_date(self, obj):
return obj.end.strftime('%d.%m.%Y, %H:%M')

View File

@ -2,8 +2,8 @@ from django.core.management.base import BaseCommand
from django.core.management import call_command
from network.base.models import Antenna
from network.base.tests import (generate_payload, generate_payload_name, DemodDataFactory,
DataFactory, StationFactory)
from network.base.tests import (generate_payload, generate_payload_name,
DemodDataFactory, StationFactory, ObservationFactory)
class Command(BaseCommand):
@ -22,7 +22,7 @@ class Command(BaseCommand):
self.stdout.write("Creating fixtures...")
StationFactory.create_batch(10,
antennas=(Antenna.objects.all().values_list('id', flat=True)))
DataFactory.create_batch(20)
ObservationFactory.create_batch(20)
for _ in range(40):
DemodDataFactory.create(payload_demod__data=generate_payload(),
payload_demod__filename=generate_payload_name())

View File

@ -18,8 +18,8 @@ class Command(BaseCommand):
try:
sat = satellite(obj.norad_cat_id)
except:
self.stdout.write(('Satellite {} with Identifier {} does '
'not exist').format(obj.name, obj.norad_cat_id))
self.stdout.write(('{0} - {1}: TLE not found [error]')
.format(obj.name, obj.norad_cat_id))
continue
obj.name = sat.name()
@ -30,13 +30,13 @@ class Command(BaseCommand):
try:
latest_tle = obj.latest_tle.tle1
if latest_tle == tle[1]:
self.stdout.write(('Satellite {} with Identifier {} '
'found [defer]').format(obj.name, obj.norad_cat_id))
self.stdout.write(('{0} - {1}: TLE already exists [defer]')
.format(obj.name, obj.norad_cat_id))
continue
except:
pass
Tle.objects.create(tle0=tle[0], tle1=tle[1], tle2=tle[2], satellite=obj)
self.stdout.write(('Satellite {} with Identifier {} '
'found [updated]').format(obj.name, obj.norad_cat_id))
self.stdout.write(('{0} - {1}: new TLE found [updated]')
.format(obj.name, obj.norad_cat_id))

View File

@ -0,0 +1,78 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.4 on 2017-09-09 21:03
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
def disable_db_checks(apps, schema_editor):
from django.db import connections, DEFAULT_DB_ALIAS
connection = connections[DEFAULT_DB_ALIAS]
if 'mysql' in connection.settings_dict['ENGINE']:
cursor = connection.cursor()
cursor.execute('SET foreign_key_checks = 0')
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('base', '0021_auto_20170813_1258'),
]
operations = [
migrations.RunPython(disable_db_checks),
migrations.AddField(
model_name='demoddata',
name='observation',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='demoddata', to='base.Observation'),
),
migrations.AddField(
model_name='observation',
name='ground_station',
field=models.ForeignKey(default=None, on_delete=django.db.models.deletion.CASCADE, to='base.Station'),
preserve_default=False,
),
migrations.AddField(
model_name='observation',
name='max_altitude',
field=models.FloatField(blank=True, null=True),
),
migrations.AddField(
model_name='observation',
name='payload',
field=models.FileField(blank=True, null=True, upload_to=b'data_payloads'),
),
migrations.AddField(
model_name='observation',
name='rise_azimuth',
field=models.FloatField(blank=True, null=True),
),
migrations.AddField(
model_name='observation',
name='set_azimuth',
field=models.FloatField(blank=True, null=True),
),
migrations.AddField(
model_name='observation',
name='vetted_datetime',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name='observation',
name='vetted_status',
field=models.CharField(choices=[(b'unknown', b'Unknown'), (b'verified', b'Verified'), (b'data_not_verified', b'Has Data, Not Verified'), (b'no_data', b'No Data')], default=b'unknown', max_length=20),
),
migrations.AddField(
model_name='observation',
name='vetted_user',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='observations_vetted', to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='observation',
name='waterfall',
field=models.ImageField(blank=True, null=True, upload_to=b'data_waterfalls'),
),
]

View File

@ -0,0 +1,52 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.4 on 2017-09-09 13:26
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
def move_data(apps, schema_editor):
Data = apps.get_model('base', 'Data')
Observation = apps.get_model('base', 'Observation')
DemodData = apps.get_model('base', 'DemodData')
for observation in Observation.objects.all():
obs = Observation.objects.filter(pk=observation.pk)
data = Data.objects.filter(observation=observation)
for counter, datum in enumerate(data):
demod = DemodData.objects.filter(data=datum)
obj = {
'satellite': datum.observation.satellite,
'transmitter': datum.observation.transmitter,
'tle': datum.observation.tle,
'author': datum.observation.author,
'start': datum.start,
'end': datum.end,
'ground_station': datum.ground_station,
'payload': datum.payload,
'waterfall': datum.waterfall,
'vetted_datetime': datum.vetted_datetime,
'vetted_user': datum.vetted_user,
'vetted_status': datum.vetted_status,
'rise_azimuth': datum.rise_azimuth,
'max_altitude': datum.max_altitude,
'set_azimuth': datum.set_azimuth
}
# this observation becomes its first data object
if not counter:
obs.update(**obj)
obs_new = obs.get()
# new observation objects for all the next data objects
else:
obs_new = Observation.objects.create(**obj)
demod.update(observation=obs_new)
dependencies = [
('base', '0022_auto_20170909_2103'),
]
operations = [
migrations.RunPython(move_data),
]

View File

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.4 on 2017-09-09 21:07
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('base', '0023_auto_20170909_2103'),
]
operations = [
migrations.RemoveField(
model_name='demoddata',
name='data',
),
]

View File

@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.4 on 2017-09-09 21:11
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('base', '0024_remove_demoddata_data'),
]
operations = [
migrations.RemoveField(
model_name='data',
name='ground_station',
),
migrations.RemoveField(
model_name='data',
name='observation',
),
migrations.RemoveField(
model_name='data',
name='vetted_user',
),
migrations.DeleteModel(
name='Data',
),
]

View File

@ -0,0 +1,67 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.4 on 2017-09-09 21:16
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('base', '0025_auto_20170909_2111'),
]
operations = [
migrations.AlterField(
model_name='observation',
name='author',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='observations', to=settings.AUTH_USER_MODEL),
),
migrations.AlterField(
model_name='observation',
name='ground_station',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='observations', to='base.Station'),
),
migrations.AlterField(
model_name='observation',
name='satellite',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='observations', to='base.Satellite'),
),
migrations.AlterField(
model_name='observation',
name='tle',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='observations', to='base.Tle'),
),
migrations.AlterField(
model_name='observation',
name='transmitter',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='observations', to='base.Transmitter'),
),
migrations.AlterField(
model_name='observation',
name='vetted_user',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='observations_vetted', to=settings.AUTH_USER_MODEL),
),
migrations.AlterField(
model_name='station',
name='owner',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='ground_stations', to=settings.AUTH_USER_MODEL),
),
migrations.AlterField(
model_name='station',
name='rig',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='ground_stations', to='base.Rig'),
),
migrations.AlterField(
model_name='tle',
name='satellite',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='tles', to='base.Satellite'),
),
migrations.AlterField(
model_name='transmitter',
name='satellite',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='transmitters', to='base.Satellite'),
),
]

View File

@ -7,6 +7,7 @@ from django.conf import settings
from django.core.validators import MaxValueValidator, MinValueValidator
from django.dispatch import receiver
from django.db import models
from django.urls import reverse
from django.utils.html import format_html
from django.utils.timezone import now
@ -37,6 +38,7 @@ SATELLITE_STATUS = ['alive', 'dead', 're-entered']
class Rig(models.Model):
"""Model for Rig types."""
name = models.CharField(choices=zip(RIG_TYPES, RIG_TYPES), max_length=10)
rictld_number = models.PositiveIntegerField(blank=True, null=True)
@ -67,7 +69,8 @@ class Antenna(models.Model):
class Station(models.Model):
"""Model for SatNOGS ground stations."""
owner = models.ForeignKey(User)
owner = models.ForeignKey(User, related_name="ground_stations",
on_delete=models.SET_NULL, null=True, blank=True)
name = models.CharField(max_length=45)
image = models.ImageField(upload_to='ground_stations', blank=True)
alt = models.PositiveIntegerField(help_text='In meters above ground')
@ -87,7 +90,8 @@ class Station(models.Model):
last_seen = models.DateTimeField(null=True, blank=True)
horizon = models.PositiveIntegerField(help_text='In degrees above 0', default=10)
uuid = models.CharField(db_index=True, max_length=100, blank=True)
rig = models.ForeignKey(Rig, blank=True, null=True, on_delete=models.SET_NULL)
rig = models.ForeignKey(Rig, related_name='ground_stations',
on_delete=models.SET_NULL, null=True, blank=True)
description = models.TextField(max_length=500, blank=True)
class Meta:
@ -115,8 +119,8 @@ class Station(models.Model):
@property
def success_rate(self):
observations = self.data_set.all().count()
success = self.data_set.exclude(payload='').count()
observations = self.observations.all().count()
success = self.observations.exclude(payload='').count()
if observations:
return int(100 * (float(success) / float(observations)))
else:
@ -178,21 +182,21 @@ class Satellite(models.Model):
@property
def data_count(self):
return Data.objects.filter(observation__satellite=self).count()
return Observation.objects.filter(satellite=self).count()
@property
def verified_count(self):
data = Data.objects.filter(observation__satellite=self)
data = Observation.objects.filter(satellite=self)
return data.filter(vetted_status='verified').count()
@property
def empty_count(self):
data = Data.objects.filter(observation__satellite=self)
data = Observation.objects.filter(satellite=self)
return data.filter(vetted_status='no_data').count()
@property
def unknown_count(self):
data = Data.objects.filter(observation__satellite=self)
data = Observation.objects.filter(satellite=self)
return data.filter(vetted_status='unknown').count()
@property
@ -225,7 +229,8 @@ class Tle(models.Model):
tle1 = models.CharField(max_length=200, blank=True)
tle2 = models.CharField(max_length=200, blank=True)
updated = models.DateTimeField(auto_now=True, blank=True)
satellite = models.ForeignKey(Satellite, related_name='tles', null=True)
satellite = models.ForeignKey(Satellite, related_name='tles',
on_delete=models.CASCADE, null=True, blank=True)
class Meta:
ordering = ['tle0']
@ -247,7 +252,8 @@ class Transmitter(models.Model):
null=True, on_delete=models.SET_NULL)
invert = models.BooleanField(default=False)
baud = models.FloatField(validators=[MinValueValidator(0)], null=True, blank=True)
satellite = models.ForeignKey(Satellite, related_name='transmitters', null=True)
satellite = models.ForeignKey(Satellite, related_name='transmitters',
on_delete=models.CASCADE, null=True, blank=True)
def __unicode__(self):
return self.description
@ -255,15 +261,28 @@ class Transmitter(models.Model):
class Observation(models.Model):
"""Model for SatNOGS observations."""
satellite = models.ForeignKey(Satellite)
transmitter = models.ForeignKey(Transmitter, null=True, related_name='observations')
tle = models.ForeignKey(Tle, null=True)
author = models.ForeignKey(User)
satellite = models.ForeignKey(Satellite, related_name='observations',
on_delete=models.SET_NULL, null=True, blank=True)
transmitter = models.ForeignKey(Transmitter, related_name='observations',
on_delete=models.SET_NULL, null=True, blank=True)
tle = models.ForeignKey(Tle, related_name='observations',
on_delete=models.SET_NULL, null=True, blank=True)
author = models.ForeignKey(User, related_name='observations',
on_delete=models.SET_NULL, null=True, blank=True)
start = models.DateTimeField()
end = models.DateTimeField()
class Meta:
ordering = ['-start', '-end']
ground_station = models.ForeignKey(Station, related_name='observations',
on_delete=models.SET_NULL, null=True, blank=True)
payload = models.FileField(upload_to='data_payloads', blank=True, null=True)
waterfall = models.ImageField(upload_to='data_waterfalls', blank=True, null=True)
vetted_datetime = models.DateTimeField(null=True, blank=True)
vetted_user = models.ForeignKey(User, related_name='observations_vetted',
on_delete=models.SET_NULL, null=True, blank=True)
vetted_status = models.CharField(choices=OBSERVATION_STATUSES,
max_length=20, default='unknown')
rise_azimuth = models.FloatField(blank=True, null=True)
max_altitude = models.FloatField(blank=True, null=True)
set_azimuth = models.FloatField(blank=True, null=True)
@property
def is_past(self):
@ -283,51 +302,6 @@ class Observation(models.Model):
deletion = self.end + timedelta(minutes=int(settings.OBSERVATION_MIN_DELETION_RANGE))
return deletion < now()
# observation has at least 1 payload submitted, no verification taken into account
@property
def has_submitted_data(self):
return self.data_set.exclude(payload='').count()
# observaton has at least 1 payload that has been verified good
@property
def has_verified_data(self):
return self.data_set.filter(vetted_status='verified').count()
# observation is vetted to be all bad data
@property
def has_no_data(self):
return self.data_set.filter(
vetted_status='no_data').count() == self.data_set.count()
# observation has at least 1 payload left unvetted
@property
def has_unvetted_data(self):
return self.data_set.filter(vetted_status='unknown').count()
def __unicode__(self):
return '{0}'.format(self.id)
class Data(models.Model):
"""Model for observation data."""
start = models.DateTimeField()
end = models.DateTimeField()
observation = models.ForeignKey(Observation)
ground_station = models.ForeignKey(Station)
payload = models.FileField(upload_to='data_payloads', blank=True, null=True)
waterfall = models.ImageField(upload_to='data_waterfalls', blank=True, null=True)
vetted_datetime = models.DateTimeField(null=True, blank=True)
vetted_user = models.ForeignKey(User, related_name="vetted_user_set", null=True, blank=True)
vetted_status = models.CharField(choices=OBSERVATION_STATUSES,
max_length=20, default='unknown')
rise_azimuth = models.FloatField(blank=True, null=True)
max_altitude = models.FloatField(blank=True, null=True)
set_azimuth = models.FloatField(blank=True, null=True)
@property
def is_past(self):
return self.end < now()
# this payload has been vetted good/bad by someone
@property
def is_vetted(self):
@ -357,9 +331,15 @@ class Data(models.Model):
class Meta:
ordering = ['-start', '-end']
def __unicode__(self):
return str(self.id)
@receiver(models.signals.post_delete, sender=Data)
def data_remove_files(sender, instance, **kwargs):
def get_absolute_url(self):
return reverse('base:observation_view', kwargs={'id': self.id})
@receiver(models.signals.post_delete, sender=Observation)
def observation_remove_files(sender, instance, **kwargs):
if instance.payload:
if os.path.isfile(instance.payload.path):
os.remove(instance.payload.path)
@ -369,7 +349,8 @@ def data_remove_files(sender, instance, **kwargs):
class DemodData(models.Model):
data = models.ForeignKey(Data, related_name='demoddata')
observation = models.ForeignKey(Observation, related_name='demoddata',
on_delete=models.CASCADE, blank=True, null=True)
payload_demod = models.FileField(upload_to='data_payloads', blank=True, null=True)
def is_image(self):

View File

@ -14,7 +14,7 @@ from django.contrib.auth.models import Permission
from network.base.models import (ANTENNA_BANDS, ANTENNA_TYPES, RIG_TYPES, OBSERVATION_STATUSES,
Rig, Mode, Antenna, Satellite, Tle, Station, Transmitter,
Observation, Data, DemodData)
Observation, DemodData)
from network.users.tests import UserFactory
@ -142,12 +142,20 @@ class TransmitterFactory(factory.django.DjangoModelFactory):
class ObservationFactory(factory.django.DjangoModelFactory):
"""Observation model factory."""
satellite = factory.Iterator(get_valid_satellites())
tle = factory.SubFactory(TleFactory)
author = factory.SubFactory(UserFactory)
start = fuzzy.FuzzyDateTime(now() - timedelta(days=3),
now() + timedelta(days=3))
end = factory.LazyAttribute(
lambda x: x.start + timedelta(hours=random.randint(1, 8))
)
ground_station = factory.Iterator(Station.objects.all())
payload = factory.django.FileField(filename='data.ogg')
vetted_datetime = factory.LazyAttribute(
lambda x: x.end + timedelta(hours=random.randint(1, 20))
)
vetted_user = factory.SubFactory(UserFactory)
vetted_status = fuzzy.FuzzyChoice(choices=OBSERVATION_STATUSES)
@factory.lazy_attribute
def transmitter(self):
@ -157,27 +165,8 @@ class ObservationFactory(factory.django.DjangoModelFactory):
model = Observation
class DataFactory(factory.django.DjangoModelFactory):
start = fuzzy.FuzzyDateTime(now() - timedelta(days=3),
now() + timedelta(days=3))
end = factory.LazyAttribute(
lambda x: x.start + timedelta(minutes=random.randint(1, 20))
)
observation = factory.SubFactory(ObservationFactory)
ground_station = factory.Iterator(Station.objects.all())
payload = factory.django.FileField(filename='data.ogg')
vetted_datetime = factory.LazyAttribute(
lambda x: x.end + timedelta(hours=random.randint(1, 20))
)
vetted_user = factory.SubFactory(UserFactory)
vetted_status = fuzzy.FuzzyChoice(choices=OBSERVATION_STATUS_IDS)
class Meta:
model = Data
class DemodDataFactory(factory.django.DjangoModelFactory):
data = factory.Iterator(Data.objects.all())
observation = factory.Iterator(Observation.objects.all())
payload_demod = factory.django.FileField()
class Meta:
@ -255,6 +244,7 @@ class ObservationsListViewTest(TestCase):
observations = []
satellites = []
transmitters = []
stations = []
def setUp(self):
# Clear the data and create some new random data
@ -269,9 +259,11 @@ class ObservationsListViewTest(TestCase):
for x in xrange(1, 10):
self.satellites.append(SatelliteFactory())
for x in xrange(1, 10):
self.transmitters.append(TransmitterFactory(satellite=self.satellites[0]))
self.transmitters.append(TransmitterFactory())
for x in xrange(1, 10):
self.observations.append(ObservationFactory(satellite=self.satellites[0]))
self.stations.append(StationFactory())
for x in xrange(1, 20):
self.observations.append(ObservationFactory())
def test_observations_list(self):
response = self.client.get('/observations/')
@ -281,10 +273,9 @@ class ObservationsListViewTest(TestCase):
def test_observations_list_deselect_bad(self):
response = self.client.get('/observations/?bad=0')
print response
for x in self.observations:
self.assertNotContains(response, x.transmitter.mode.name)
self.assertContains(response, x.transmitter.mode.name)
def test_observations_list_deselect_good(self):
response = self.client.get('/observations/?good=0')
@ -330,6 +321,7 @@ class ObservationViewTest(TestCase):
observation = None
satellites = []
transmitters = []
stations = []
user = None
def setUp(self):
@ -340,6 +332,8 @@ class ObservationViewTest(TestCase):
self.satellites.append(SatelliteFactory())
for x in xrange(1, 10):
self.transmitters.append(TransmitterFactory())
for x in xrange(1, 10):
self.stations.append(StationFactory())
self.observation = ObservationFactory()
def test_observation(self):
@ -471,13 +465,12 @@ class SettingsSiteViewTest(TestCase):
@pytest.mark.django_db(transaction=True)
class DataVerifyViewtest(TestCase):
class ObservationVerifyViewtest(TestCase):
"""
Test marking data as vetted
"""
client = Client()
user = None
data = None
satellites = []
stations = []
transmitters = []
@ -490,30 +483,26 @@ class DataVerifyViewtest(TestCase):
self.satellites.append(SatelliteFactory())
for x in xrange(1, 10):
self.transmitters.append(TransmitterFactory())
for x in xrange(1, 10):
self.observations.append(ObservationFactory())
for x in xrange(1, 10):
self.stations.append(StationFactory())
self.data = DataFactory()
self.observation = ObservationFactory()
def test_get_data_verify(self):
response = self.client.get('/data_verify/%d/' % self.data.id)
self.assertRedirects(response, '/observations/%d/' % self.data.observation.id)
data = Data.objects.get(id=self.data.id)
self.assertEqual(data.vetted_user.username, self.user.username)
self.assertEqual(data.vetted_status, 'verified')
def test_get_observation_verify(self):
response = self.client.get('/observation_verify/%d/' % self.observation.id)
self.assertRedirects(response, '/observations/%d/' % self.observation.id)
observation = Observation.objects.get(id=self.observation.id)
self.assertEqual(observation.vetted_user.username, self.user.username)
self.assertEqual(observation.vetted_status, 'verified')
@pytest.mark.django_db(transaction=True)
class DataMarkBadViewtest(TestCase):
class ObservationMarkBadViewtest(TestCase):
"""
Test marking data as vetted
"""
client = Client()
user = None
data = None
satellites = []
stations = []
transmitters = []
@ -528,14 +517,14 @@ class DataMarkBadViewtest(TestCase):
for x in xrange(1, 10):
self.stations.append(StationFactory())
self.data = DataFactory()
self.observation = ObservationFactory()
def test_get_data_mark_bad(self):
response = self.client.get('/data_mark_bad/%d/' % self.data.id)
self.assertRedirects(response, '/observations/%d/' % self.data.observation.id)
data = Data.objects.get(id=self.data.id)
self.assertEqual(data.vetted_user.username, self.user.username)
self.assertEqual(data.vetted_status, 'no_data')
def test_get_observation_mark_bad(self):
response = self.client.get('/observation_mark_bad/%d/' % self.observation.id)
self.assertRedirects(response, '/observations/%d/' % self.observation.id)
observation = Observation.objects.get(id=self.observation.id)
self.assertEqual(observation.vetted_user.username, self.user.username)
self.assertEqual(observation.vetted_status, 'no_data')
@pytest.mark.django_db(transaction=True)
@ -578,9 +567,6 @@ class ObservationModelTest(TestCase):
self.observation.end = now()
self.observation.save()
def test_has_submitted_data(self):
self.assertEqual(0, self.observation.has_submitted_data)
def test_is_passed(self):
self.assertTrue(self.observation.is_past)
@ -599,34 +585,3 @@ class ObservationModelTest(TestCase):
self.observation.end = now() - timedelta(minutes=200)
self.observation.save()
self.assertTrue(self.observation.is_deletable_after_end)
@pytest.mark.django_db(transaction=True)
class DataModelTest(TestCase):
"""
Test various properties of the Observation Model
"""
data = None
data2 = None
satellites = []
transmitters = []
def setUp(self):
for x in xrange(1, 10):
self.satellites.append(SatelliteFactory())
for x in xrange(1, 10):
self.transmitters.append(TransmitterFactory())
self.data = DataFactory()
self.data.end = now()
self.data.vetted_status = 'no_data'
self.data.save()
self.data2 = DataFactory(payload=None)
def test_is_no_data(self):
self.assertTrue(self.data.is_no_data)
def test_is_passed(self):
self.assertTrue(self.data.is_past)
def test_payload_exists(self):
self.assertFalse(self.data.payload_exists)

View File

@ -11,7 +11,8 @@ base_urlpatterns = ([
# Observations
url(r'^observations/$', views.ObservationListView.as_view(), name='observations_list'),
url(r'^observations/(?P<id>[0-9]+)/$', views.observation_view, name='observation_view'),
url(r'^observations/(?P<id>[0-9]+)/$', views.observation_view,
name='observation_view'),
url(r'^observations/(?P<id>[0-9]+)/delete/$', views.observation_delete,
name='observation_delete'),
url(r'^observations/new/$', views.observation_new, name='observation_new'),
@ -21,10 +22,10 @@ base_urlpatterns = ([
url(r'^prediction_windows/(?P<sat_id>[\w.@+-]+)/(?P<transmitter>[\w.@+-]+)/'
'(?P<start_date>.+)/(?P<end_date>.+)/$',
views.prediction_windows, name='prediction_windows'),
url(r'^data_verify/(?P<id>[0-9]+)/$', views.data_verify, name='data_verify'),
url(r'^data_mark_bad/(?P<id>[0-9]+)/$', views.data_mark_bad, name='data_mark_bad'),
url(r'^observations/data/(?P<id>[0-9]+)/$', views.observation_data_view,
name='observation_data_view'),
url(r'^observation_verify/(?P<id>[0-9]+)/$', views.observation_verify,
name='observation_verify'),
url(r'^observation_mark_bad/(?P<id>[0-9]+)/$', views.observation_mark_bad,
name='observation_mark_bad'),
# Stations
url(r'^stations/$', views.stations_list, name='stations_list'),

View File

@ -5,7 +5,6 @@ from operator import itemgetter
from datetime import datetime, timedelta
from StringIO import StringIO
from django.db.models import Count, Case, When, F
from django.conf import settings
from django.contrib import messages
from django.contrib.auth.decorators import login_required
@ -21,7 +20,7 @@ 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)
Satellite, Antenna, Tle, Rig)
from network.base.forms import StationForm, SatelliteFilterForm
from network.base.decorators import admin_required
from network.base.helpers import calculate_polar_data, resolve_overlaps
@ -148,40 +147,18 @@ class ObservationListView(ListView):
unvetted = True
if norad_cat_id == '':
observations = Observation.objects.all().order_by('-id')
observations = Observation.objects.all()
else:
observations = Observation.objects.filter(
satellite__norad_cat_id=norad_cat_id).order_by('-id')
satellite__norad_cat_id=norad_cat_id)
# Add the data subqueries as annotations
observations = observations.annotate(
data_count=Count('data', distinct=True),
nodata_count=Count(
Case(
When(data__vetted_status='no_data', then=1)
), distinct=True
),
unknown_count=Count(
Case(
When(data__vetted_status='unknown', then=1)
), distinct=True
),
vetted_count=Count(
Case(
When(data__vetted_status='verified', then=1)
), distinct=True
)
)
# Start with an empty queryset and add each filter as an or/union
resultset = Observation.objects.none()
if bad:
resultset |= observations.filter(nodata_count=F('data_count'))
if good:
resultset |= observations.filter(vetted_count__gt=0)
if unvetted:
resultset |= observations.filter(unknown_count__gt=0)
return resultset
if not bad:
observations = observations.exclude(vetted_status='no_data')
if not good:
observations = observations.exclude(vetted_status='verified')
if not unvetted:
observations = observations.exclude(vetted_status='unknown')
return observations
def get_context_data(self, **kwargs):
"""
@ -221,9 +198,6 @@ def observation_new(request):
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()
sat_ephem = ephem.readtle(str(sat.latest_tle.tle0),
str(sat.latest_tle.tle1),
@ -247,15 +221,18 @@ def observation_new(request):
observer.elevation = ground_station.alt
tr, azr, tt, altt, ts, azs = observer.next_pass(sat_ephem)
Data.objects.create(start=make_aware(start, utc), end=make_aware(end, utc),
ground_station=ground_station, observation=obs,
rise_azimuth=format(math.degrees(azr), '.0f'),
max_altitude=format(math.degrees(altt), '.0f'),
set_azimuth=format(math.degrees(azs), '.0f'))
Observation.objects.create(satellite=sat, transmitter=trans, tle=tle, author=me,
start=make_aware(start, utc), end=make_aware(end, utc),
ground_station=ground_station,
rise_azimuth=format(math.degrees(azr), '.0f'),
max_altitude=format(math.degrees(altt), '.0f'),
set_azimuth=format(math.degrees(azs), '.0f'))
time_start_new = ephem.Date(ts).datetime() + timedelta(minutes=1)
observer.date = time_start_new.strftime("%Y-%m-%d %H:%M:%S.%f")
return redirect(reverse('base:observation_view', kwargs={'id': obs.id}))
messages.success(request, 'Your selected Observations were scheduled successfully.')
return redirect(reverse('base:observations_list'))
satellites = Satellite.objects.filter(transmitters__alive=True) \
.filter(status='alive').distinct()
@ -377,7 +354,7 @@ def prediction_windows(request, sat_id, transmitter, start_date, end_date,
window_end = make_aware(ephem.Date(ts).datetime(), utc)
# Check if overlaps with existing scheduled observations
gs_data = Data.objects.filter(ground_station=station)
gs_data = Observation.objects.filter(ground_station=station)
window = resolve_overlaps(station, gs_data, window_start, window_end)
if window:
@ -424,15 +401,13 @@ def prediction_windows(request, sat_id, transmitter, start_date, end_date,
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 \
Station.objects.filter(owner=request.user).filter(id=observation.id).count() or \
request.user.is_staff:
is_vetting_user = True
@ -444,8 +419,8 @@ def observation_view(request, id):
observation.is_deletable_after_end:
is_deletable = True
if settings.ENVIRONMENT == 'production':
discuss_slug = 'https://community.satnogs.org/t/observation-{0}-{1}-{2}' \
if settings.ENVIRONMENT == 'dev':
discuss_slug = 'https://community.libre.space/t/observation-{0}-{1}-{2}' \
.format(observation.id, slugify(observation.satellite.name),
observation.satellite.norad_cat_id)
discuss_url = ('https://community.libre.space/new-topic?title=Observation {0}: {1}'
@ -462,14 +437,13 @@ def observation_view(request, id):
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})
{'observation': observation, '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})
{'observation': observation, 'is_vetting_user': is_vetting_user,
'is_deletable': is_deletable})
@login_required
@ -489,25 +463,25 @@ def observation_delete(request, id):
@login_required
def data_verify(request, id):
def observation_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}))
observation = get_object_or_404(Observation, id=id)
observation.vetted_status = 'verified'
observation.vetted_user = me
observation.vetted_datetime = datetime.today()
observation.save(update_fields=['vetted_status', 'vetted_user', 'vetted_datetime'])
return redirect(reverse('base:observation_view', kwargs={'id': observation.id}))
@login_required
def data_mark_bad(request, id):
def observation_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}))
observation = get_object_or_404(Observation, id=id)
observation.vetted_status = 'no_data'
observation.vetted_user = me
observation.vetted_datetime = datetime.today()
observation.save(update_fields=['vetted_status', 'vetted_user', 'vetted_datetime'])
return redirect(reverse('base:observation_view', kwargs={'id': observation.id}))
def stations_list(request):
@ -698,9 +672,3 @@ def satellite_view(request, id):
}
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))

View File

@ -259,6 +259,13 @@ CSP_STYLE_SRC = (
# Database
DATABASE_URL = getenv('DATABASE_URL', 'sqlite:///db.sqlite3')
DATABASES = {'default': db_url(DATABASE_URL)}
DATABASES_EXTRAS = {
'OPTIONS': {
'init_command': 'SET sql_mode="STRICT_TRANS_TABLES"'
},
}
if DATABASES['default']['ENGINE'].split('.')[-1] == 'mysql':
DATABASES['default'].update(DATABASES_EXTRAS)
# Mapbox API
MAPBOX_GEOCODE_URL = 'https://api.tiles.mapbox.com/v4/geocode/mapbox.places/'

View File

@ -154,14 +154,23 @@ footer {
border-radius: 5px;
}
.gs-front-data {
.front-data {
float: right;
width: 200px;
display: flow-root;
}
.gs-front-line {
.front-line {
margin-top: 10px;
margin-bottom: 10px;
clear: both;
}
@media screen and (min-width: 992px) {
.front-border {
border-right: 1px solid lightgray;
}
}
#navbar-logo {
@ -347,32 +356,6 @@ span.datetime-time {
margin-bottom: 15px;
}
.observation-data .panel-title {
display: inline-block;
}
.panel {
margin-bottom: 40px;
}
.panel-title > a {
color: #337ab7;
}
.verified {
color: #009933;
padding-right: 5px;
}
.no_data {
color: #cc3300;
padding-right: 5px;
}
.vetting {
float: right;
}
.waterfall {
margin-top: 5px;
}

View File

@ -1,51 +1,18 @@
/* global d3 WaveSurfer URI */
/* global WaveSurfer URI */
$(document).ready(function() {
'use strict';
var observation_start = 1000 * $('#observation-info').data('start');
var observation_end = 1000 * $('#observation-info').data('end');
var observation_data = [];
var formatTime = function(timeSeconds) {
var minute = Math.floor(timeSeconds / 60); // get minute(integer) from timeSeconds
var tmp = Math.round(timeSeconds - (minute * 60)); // get second(integer) from timeSeconds
var second = (tmp < 10 ? '0' : '') + tmp; // make two-figured integer if less than 10
return String(minute + ':' + second); // combine minute and second in string
};
$('.observation-data').each(function(){
var $this = $(this);
var data_groundstation = $this.data('groundstation');
var data_time_start = 1000 * $this.data('start');
var data_time_end = 1000 * $this.data('end');
observation_data.push({label : data_groundstation, times : [{starting_time: data_time_start, ending_time: data_time_end}]});
});
var chart = d3.timeline()
.stack()
.beginning(observation_start)
.ending(observation_end)
.hover(function (d, i, datum) {
var div = $('#hoverRes');
var colors = chart.colors();
div.find('.coloredDiv').css('background-color', colors(i));
div.find('#name').text(datum.label);
})
.margin({left:140, right:10, top:0, bottom:50})
.tickFormat({format: d3.time.format.utc('%H:%M'), tickTime: d3.time.minutes, tickInterval: 30, tickSize: 6});
var svg_width = 1140;
if (screen.width < 1200) { svg_width = 940; }
if (screen.width < 992) { svg_width = 720; }
if (screen.width < 768) { svg_width = screen.width - 30; }
d3.select('#timeline').append('svg').attr('width', svg_width)
.datum(observation_data).call(chart);
// Format time for the player
function formatTime(timeSeconds) {
var minute = Math.floor(timeSeconds / 60);
var tmp = Math.round(timeSeconds - (minute * 60));
var second = (tmp < 10 ? '0' : '') + tmp;
return String(minute + ':' + second);
}
// Set width for not selected tabs
var panelWidth = $('.panel-body').first().width();
var panelWidth = $('.tab-content').first().width();
$('.tab-pane').css('width', panelWidth);
// Waveform loading
@ -93,9 +60,9 @@ $(document).ready(function() {
wavesurfer.playPause();
});
$('a[href="#tab-audio-' + wid + '"]').on('shown.bs.tab', function () {
$('a[href="#tab-audio"]').on('shown.bs.tab', function () {
wavesurfer.load(data_payload_url);
$('a[href="#tab-audio-' + wid + '"]').off('shown.bs.tab');
$('a[href="#tab-audio"]').off('shown.bs.tab');
});
wavesurfer.on('ready', function() {
@ -103,11 +70,12 @@ $(document).ready(function() {
var spectrogram = Object.create(WaveSurfer.Spectrogram);
spectrogram.init({
wavesurfer: wavesurfer,
container: '#wave-spectrogram-' + wid,
container: '#wave-spectrogram',
fftSamples: 256,
windowFunc: 'hann'
});
//$playbackTime.text(formatTime(wavesurfer.getCurrentTime()));
$playbackTime.text(formatTime(wavesurfer.getCurrentTime()));
wavesurfer.on('audioprocess', function(evt) {

View File

@ -0,0 +1,92 @@
$(document).ready(function() {
'use strict';
function drawPolarPlot(canvas, data) {
var ctx = canvas.getContext('2d');
var centerΧ = ctx.canvas.width / 2;
var centerY = ctx.canvas.height / 2;
var canvasSize = Math.min(ctx.canvas.width, ctx.canvas.height);
var altUnit = canvasSize/(2.5 * 90);
var fontRatio = 0.07;
var radius;
var radians;
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
//Draw altitude circles
ctx.beginPath();
var altCircles = [90, 60, 30];
for (var i=0; i < altCircles.length; i++) {
radius = altCircles[i] * altUnit;
ctx.moveTo(centerΧ + radius,centerY);
for(var th=1;th<=360;th+=1) {
radians = (Math.PI/180) * th;
ctx.lineTo(centerΧ + radius * Math.cos(radians),centerY + radius * Math.sin(radians));
}
}
ctx.strokeStyle = '#444444';
ctx.lineWidth = 1;
ctx.stroke();
//Draw axis and letters
radius = 96 * altUnit;
ctx.moveTo(centerΧ, centerY);
ctx.lineTo(centerΧ, centerY + radius);
ctx.moveTo(centerΧ, centerY);
ctx.lineTo(centerΧ, centerY-radius);
ctx.moveTo(centerΧ, centerY);
ctx.lineTo(centerΧ + radius, centerY);
ctx.moveTo(centerΧ, centerY);
ctx.lineTo(centerΧ-radius, centerY);
radius = 98 * altUnit;
ctx.font = (canvasSize * fontRatio) + 'px serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'bottom';
ctx.fillText('N', centerΧ, centerY - radius);
ctx.textBaseline = 'top';
ctx.fillText('S', centerΧ, centerY + radius);
ctx.strokeStyle = '#000000';
ctx.textAlign = 'right';
ctx.textBaseline = 'middle';
ctx.fillText('W', centerΧ - radius, centerY);
ctx.textAlign = 'left';
ctx.fillText('E', centerΧ + radius, centerY);
ctx.strokeStyle = '#000000';
ctx.lineWidth = 1;
ctx.stroke();
//Draw data
ctx.beginPath();
radians = (Math.PI/180) * (data[0][1] - 90);
radius = (90 - data[0][0]) * altUnit;
ctx.moveTo(centerΧ + radius * Math.cos(radians),centerY + radius * Math.sin(radians));
var dataLength = data.length;
for (var j=1; j< dataLength; j++) {
radians = (Math.PI/180) * (data[j][1] - 90);
radius = (90 - data[j][0] ) * altUnit;
ctx.lineTo(centerΧ + radius * Math.cos(radians),centerY + radius * Math.sin(radians));
}
ctx.strokeStyle = 'rgb(0, 0, 255)';
ctx.lineWidth = 2;
ctx.stroke();
//Draw start and end
radians = (Math.PI/180) * (data[0][1] - 90);
ctx.beginPath();
ctx.arc(centerΧ + radius * Math.cos(radians),centerY + radius * Math.sin(radians), 3, 0, 2 * Math.PI, false);
ctx.fillStyle = 'lightgreen';
ctx.fill();
radians = (Math.PI/180) * (data[dataLength-1][1] - 90);
ctx.beginPath();
ctx.arc(centerΧ + radius * Math.cos(radians),centerY + radius * Math.sin(radians), 3, 0, 2 * Math.PI, false);
ctx.fillStyle = 'red';
ctx.fill();
}
$('canvas').each(function(){
var $this = $(this);
drawPolarPlot($this.get(0), $this.data().points);
});
});

View File

@ -23,95 +23,6 @@ $(document).ready(function() {
});
}
function drawPolarPlot (canvas, data) {
var ctx = canvas.getContext('2d');
var centerΧ = ctx.canvas.width / 2;
var centerY = ctx.canvas.height / 2;
var canvasSize = Math.min(ctx.canvas.width, ctx.canvas.height);
var altUnit = canvasSize/(2.5 * 90);
var fontRatio = 0.07;
var radius;
var radians;
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
//Draw altitude circles
ctx.beginPath();
var altCircles = [90, 60, 30];
for (var i=0; i < altCircles.length; i++) {
radius = altCircles[i] * altUnit;
ctx.moveTo(centerΧ + radius,centerY);
for(var th=1;th<=360;th+=1) {
radians = (Math.PI/180) * th;
ctx.lineTo(centerΧ + radius * Math.cos(radians),centerY + radius * Math.sin(radians));
}
}
ctx.strokeStyle = '#444444';
ctx.lineWidth = 1;
ctx.stroke();
//Draw axis and letters
radius = 96 * altUnit;
ctx.moveTo(centerΧ, centerY);
ctx.lineTo(centerΧ, centerY + radius);
ctx.moveTo(centerΧ, centerY);
ctx.lineTo(centerΧ, centerY-radius);
ctx.moveTo(centerΧ, centerY);
ctx.lineTo(centerΧ + radius, centerY);
ctx.moveTo(centerΧ, centerY);
ctx.lineTo(centerΧ-radius, centerY);
radius = 98 * altUnit;
ctx.font = (canvasSize * fontRatio) + 'px serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'bottom';
ctx.fillText('N', centerΧ, centerY - radius);
ctx.textBaseline = 'top';
ctx.fillText('S', centerΧ, centerY + radius);
ctx.strokeStyle = '#000000';
ctx.textAlign = 'right';
ctx.textBaseline = 'middle';
ctx.fillText('W', centerΧ - radius, centerY);
ctx.textAlign = 'left';
ctx.fillText('E', centerΧ + radius, centerY);
ctx.strokeStyle = '#000000';
ctx.lineWidth = 1;
ctx.stroke();
//Draw data
ctx.beginPath();
radians = (Math.PI/180) * (data[0][1] - 90);
radius = (90 - data[0][0]) * altUnit;
ctx.moveTo(centerΧ + radius * Math.cos(radians),centerY + radius * Math.sin(radians));
var dataLength = data.length;
for (var j=1; j< dataLength; j++) {
radians = (Math.PI/180) * (data[j][1] - 90);
radius = (90 - data[j][0] ) * altUnit;
ctx.lineTo(centerΧ + radius * Math.cos(radians),centerY + radius * Math.sin(radians));
}
ctx.strokeStyle = 'rgb(0, 0, 255)';
ctx.lineWidth = 2;
ctx.stroke();
//Draw start and end
radians = (Math.PI/180) * (data[0][1] - 90);
ctx.beginPath();
ctx.arc(centerΧ + radius * Math.cos(radians),centerY + radius * Math.sin(radians), 3, 0, 2 * Math.PI, false);
ctx.fillStyle = 'lightgreen';
ctx.fill();
radians = (Math.PI/180) * (data[dataLength-1][1] - 90);
ctx.beginPath();
ctx.arc(centerΧ + radius * Math.cos(radians),centerY + radius * Math.sin(radians), 3, 0, 2 * Math.PI, false);
ctx.fillStyle = 'red';
ctx.fill();
}
$('canvas').each(function(){
var $this = $(this);
drawPolarPlot($this.get(0), $this.data().points);
});
// Init the map
var mapboxid = $('div#map-station').data('mapboxid');
var mapboxtoken = $('div#map-station').data('mapboxtoken');

View File

@ -110,18 +110,18 @@
<th>Frequency</th>
<th>Encoding</th>
<th>Timeframe</th>
<th>Stations</th>
<th>Station</th>
</thead>
<tbody>
{% for observation in latest_observations.all %}
<tr>
<td>
<a href="{% url 'base:observation_view' id=observation.id %}">
{% if observation.has_verified_data %}
{% if observation.is_verified %}
<span class="label label-success" title="There is known good data in this observation"
{% elif observation.is_future %}
<span class="label label-info" title="This observation is in the future"
{% elif observation.has_unvetted_data %}
{% elif not observation.is_vetted %}
<span class="label label-warning" title="There is data that needs vetting in this observation"
{% else %}
<span class="label label-danger" title="No good data in this observation"
@ -144,12 +144,9 @@
<span class="datetime-time">{{ observation.end|date:"H:i:s" }}</span>
</td>
<td>
{% regroup observation.data_set.all by ground_station as station_list %}
{% for station in station_list %}
<a href="{% url 'base:station_view' id=station.grouper.id %}">
<span class="label label-primary" title="{{ station.grouper.name }}" data-toggle="tooltip">{{ station.grouper.id }}</span>
</a>
{% endfor %}
<a href="{% url 'base:station_view' id=observation.ground_station.id %}">
{{ observation.ground_station.name }}
</a>
</td>
</tr>
{% endfor %}
@ -190,12 +187,9 @@
<span class="datetime-time">{{ observation.end|date:"H:i:s" }}</span>
</td>
<td>
{% regroup observation.data_set.all by ground_station as station_list %}
{% for station in station_list %}
<a href="{% url 'base:station_view' id=station.grouper.id %}">
<span class="label label-primary" title="{{ station.grouper.name }}" data-toggle="tooltip">{{ station.grouper.id }}</span>
</a>
{% endfor %}
<a href="{% url 'base:station_view' id=observation.ground_station.id %}">
<span class="label label-primary">{{ observation.ground_station.name }}</span>
</a>
</td>
</tr>
{% endfor %}

View File

@ -126,7 +126,7 @@
<div class="row">
<div class="col-md-12">
<button type="button" id="calculate-observation" class="btn btn-primary pull-right">
Calculate Observation
Calculate
</button>
</div>
</div>
@ -148,7 +148,7 @@
<div class="row calculation-result">
<div class="col-md-12">
<button type="submit" id="schedule-observation" class="btn btn-success pull-right" disabled="True">
Schedule Observation
Schedule
</button>
</div>
</div>

View File

@ -20,6 +20,27 @@
</div>
<div class="col-md-6 text-right">
<h2>
{% if discuss_slug %}
<a id="obs-discuss"
data-slug="{{ discuss_slug }}"
href="{% if has_comments %}{{ discuss_slug }}{% else %}{{ discuss_url }}{% endif %}"
class="btn btn-primary" target="_blank">
<span class="glyphicon glyphicon-comment" aria-hidden="true"></span>
Discuss
</a>
{% endif %}
{% if not observation.is_vetted and is_vetting_user %}
<a href="{% url 'base:observation_verify' id=observation.id %}">
<button id="verify-data" type="button" class="btn btn-default">
<span class="glyphicon glyphicon-thumbs-up" aria-hidden="true"></span> Verify
</button>
</a>
<a href="{% url 'base:observation_mark_bad' id=observation.id %}">
<button id="bad-data" type="button" class="btn btn-default">
<span class="glyphicon glyphicon-thumbs-down" aria-hidden="true"></span> Bad
</button>
</a>
{% endif %}
{% if is_deletable %}
<a href="{% url 'base:observation_delete' id=observation.id %}" id="obs-delete" class="btn btn-danger">
<span class="glyphicon glyphicon-trash" aria-hidden="true" title="Delete Observation"></span>
@ -30,243 +51,209 @@
</div>
<div class="row">
<div class="col-md-12">
<table class="table table-hover">
<thead>
<th>Satellite</th>
<th>Frequency</th>
<th>Encoding</th>
<th>Timeframe</th>
<th>Observer</th>
</thead>
<tbody>
<tr>
<td>
<a href="#" data-toggle="modal" data-target="#SatelliteModal" data-id="{{ observation.satellite.norad_cat_id }}">
{{ observation.satellite.norad_cat_id }} - {{ observation.satellite.name }}
</a>
</td>
<td>{{ observation.transmitter.downlink_low|frq }}</td>
<td>{{ observation.transmitter.mode|default:"-" }}</td>
<td>{{ observation.start|date:"Y-m-d H:i:s" }}</br>{{ observation.end|date:"Y-m-d H:i:s" }}</td>
<td>
<a href="{% url 'users:view_user' username=observation.author.username %}">
{{ observation.author.displayname }}
</a>
</td>
</tr>
</tbody>
</table>
<div class="col-md-4 front-border">
<div class="front-line">
<span class="label label-default">Satellite</span>
<span class="front-data">
<a href="#" data-toggle="modal" data-target="#SatelliteModal" data-id="{{ observation.satellite.norad_cat_id }}">
{{ observation.satellite.norad_cat_id }} - {{ observation.satellite.name }}
</a>
</span>
</div>
<div class="front-line">
<span class="label label-default">Station</span>
<span class="front-data">
<a href="{% url 'base:station_view' id=observation.ground_station.id %}">
{{ observation.ground_station.name }}
</a>
</span>
</div>
<div class="front-line">
<span class="label label-default">Observer</span>
<span class="front-data">
<a href="{% url 'users:view_user' username=observation.author.username %}">
{{ observation.author.displayname }}
</a>
</span>
</div>
<div class="front-line">
<span class="label label-default">Status</span>
<span class="front-data">
{% if observation.is_verified %}
<span class="label label-xs label-success" aria-hidden="true"
data-toggle="tooltip" data-placement="right"
title="Verified good on {{ observation.vetted_datetime|date:"Y-m-d H:i:s" }} by {{ observation.vetted_user.displayname }}">Verified</span>
{% elif observation.is_no_data %}
<span class="label label-xs label-danger" aria-hidden="true"
data-toggle="tooltip" data-placement="right"
title="Verified bad on {{ observation.vetted_datetime|date:"Y-m-d H:i:s" }} by {{ observation.vetted_user.displayname }}">Bad</span>
{% elif observation.is_future %}
<span class="label label-xs label-info" aria-hidden="true"
data-toggle="tooltip" data-placement="right"
title="This observation is in the future">Pending</span>
{% else %}
<span class="label label-xs label-warning" aria-hidden="true"
data-toggle="tooltip" data-placement="right"
title="This observation needs vetting">Unvetted</span>
{% endif %}
</span>
</div>
</div>
<div class="col-md-4 front-border">
<div class="front-line">
<span class="label label-default">Frequency</span>
<span class="front-data">
{{ observation.transmitter.downlink_low|frq }}
</span>
</div>
<div class="front-line">
<span class="label label-default">Encoding</span>
<span class="front-data">
{{ observation.transmitter.mode|default:"-" }}
</span>
</div>
<div class="front-line">
<span class="label label-default">Timeframe</span>
<span class="front-data">
{{ observation.start|date:"Y-m-d H:i:s" }}<br>{{ observation.end|date:"Y-m-d H:i:s" }}
</span>
</div>
</div>
<div class="col-md-4">
<div class="front-line">
<span class="label label-default">Rise</span>
<span class="front-data">
{{ observation.rise_azimuth }}°
</span>
</div>
<div class="front-line">
<span class="label label-default">Max</span>
<span class="front-data">
{{ observation.max_altitude }}°
</span>
</div>
<div class="front-line">
<span class="label label-default">Set</span>
<span class="front-data">
{{ observation.set_azimuth }}°
</span>
</div>
</div>
</div>
{% if not observation.data_set.all %}
<div class="row">
<div class="col-md-12">
<p class="notice">
No data associated with this observation.
</p>
</div>
</div>
{% else %}
<div class="row">
<div class="col-md-12">
<h3>Timeline</h3>
<div id="timeline"></div>
<div id="hoverRes">
<div class="coloredDiv"></div>
<div id="name"></div>
<div id="scrolled_date"></div>
<hr>
<div class="row">
<div class="col-md-12 observation-data" id="{{ observation.id }}"
data-start="{{ observation.start|date:"U" }}"
data-end="{{ observation.end|date:"U" }}"
data-groundstation="{{ observation.ground_station }}">
<h3>Payload</h3>
<ul class="nav nav-tabs data-tabs" role="tablist">
<li role="presentation" class="active">
<a href="#tab-waterfall"
aria-controls="tab-waterfall"
role="tab"
data-toggle="tab">Waterfall
</a>
</li>
<li role="presentation">
<a href="#tab-audio"
aria-controls="tab-audio"
role="tab"
data-toggle="tab">Audio</a>
</li>
<li role="presentation">
<a href="#tab-data"
aria-controls="tab-data"
role="tab"
data-toggle="tab">Data</a>
</li>
<div class="pull-right">
{% if observation.payload %}
<a href="{{ MEDIA_URL }}{{ observation.payload }}"
target="_blank"
download="">
<button type="button" class="btn btn-default btn-xs">
<span class="glyphicon glyphicon-download"></span> Audio
</button>
</a>
{% endif %}
{% if observation.waterfall %}
<a href="{{ MEDIA_URL }}{{ observation.waterfall }}"
target="_blank"
download="">
<button type="button" class="btn btn-default btn-xs">
<span class="glyphicon glyphicon-download"></span> Waterfall
</button>
</a>
{% endif %}
</div>
</ul>
<div class="tab-content">
<div role="tabpanel" class="tab-pane active" id="tab-waterfall">
{% if observation.waterfall %}
<div id="waterfall-{{ observation.id }}" class="waterfall">
<img class="img-responsive waterfall" src="{{ MEDIA_URL }}{{ observation.waterfall }}" alt="waterfall">
</div>
{% else %}
<div class="notice">
Waiting for waterfall
</div>
{% endif %}
</div>
<div role="tabpanel" class="tab-pane" id="tab-audio">
{% if observation.payload %}
<div id="loading-{{ observation.id }}" class="notice">Loading audio...</div>
<div class="progress progress-striped active" id="progress-bar-{{ observation.id }}">
<div class="progress-bar progress-bar-info"></div>
</div>
<div class="wave tab-data" id="data-{{ observation.id }}"
data-id="{{ observation.id }}"
data-payload="{{ MEDIA_URL }}{{ observation.payload }}"></div>
<div id="wave-spectrogram"></div>
<button type="button" class="btn btn-primary btn-xs playpause">
<span class="glyphicon glyphicon-play"></span>
<span class="glyphicon glyphicon-pause"></span>
</button>
<span id="playback-time-{{ observation.id }}" class="label label-info playback-time"></span>
{% else %}
<div class="notice">
Waiting for audio
</div>
{% endif %}
</div>
<div role="tabpanel" class="tab-pane tab-data" id="tab-data">
{% if observation.demoddata.all %}
{% for demoddata in observation.demoddata.all %}
<span class="label label-default">{{ demoddata.payload_demod }}</span>
<div class="well well-sm data-well">
{% if demoddata.is_image %}
<img src="{{ demoddata.payload_demod.url }}" alt="data-{{ data.pk }}" class="img-responsive">
{% else %}
{{ demoddata.display_payload }}
{% endif %}
</div>
{% endfor %}
{% else %}
<div class="notice">
Waiting for demoded data
</div>
{% endif %}
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12">
<div class="row">
<div class="col-md-6">
<h3>Data</h3>
</div>
<div class="col-md-6 text-right">
{% if discuss_slug %}
<h3>
<a id="obs-discuss"
data-slug="{{ discuss_slug }}"
href="{% if has_comments %}{{ discuss_slug }}{% else %}{{ discuss_url }}{% endif %}"
class="btn btn-primary" target="_blank">
<span class="glyphicon glyphicon-comment" aria-hidden="true"></span>
Discuss Observation
</a>
</h3>
{% endif %}
</div>
</div>
{% for data in dataset %}
<div class="panel panel-default observation-data" id="{{ data.id }}"
data-start="{{ data.start|date:"U" }}"
data-end="{{ data.end|date:"U" }}"
data-groundstation="{{ data.ground_station }}">
<div class="panel-heading">
<h3 class="panel-title">
{% if data.is_vetted %}
{% if data.is_verified %}
<span class="glyphicon glyphicon-ok-sign {{ data.vetted_status }}" aria-hidden="true"
data-toggle="tooltip" data-placement="right"
title="Verified good on {{ data.vetted_datetime|date:"Y-m-d H:i:s" }} by {{ data.vetted_user.displayname }}"></span>
{% else %}
<span class="glyphicon glyphicon-remove-sign {{ data.vetted_status }}" aria-hidden="true"
data-toggle="tooltip" data-placement="right"
title="Verified bad on {{ data.vetted_datetime|date:"Y-m-d H:i:s" }} by {{ data.vetted_user.displayname }}"></span>
{% endif %}
{% endif %}
Data payload <a href="{% url 'base:observation_data_view' id=data.id %}">#{{ data.id }}</a> ~
Ground Station:
<a href="{% url 'base:station_view' id=data.ground_station.id %}">
{{ data.ground_station }}
</a>
</h3>
<div class="vetting">
{% if not data.is_vetted and is_vetting_user %}
<a href="{% url 'base:data_verify' id=data.id %}">
<button id="verify-data" type="button" class="btn btn-default btn-xs">
<span class="glyphicon glyphicon-thumbs-up" aria-hidden="true"></span> Verify
</button>
</a>
<a href="{% url 'base:data_mark_bad' id=data.id %}">
<button id="bad-data" type="button" class="btn btn-default btn-xs">
<span class="glyphicon glyphicon-thumbs-down" aria-hidden="true"></span> No Data
</button>
</a>
{% endif %}
</div>
</div>
<div class="panel-body">
<ul class="nav nav-tabs data-tabs" role="tablist">
<li role="presentation" class="active">
<a href="#tab-waterfall-{{ data.id }}"
aria-controls="tab-waterfall-{{ data.id }}"
role="tab"
data-toggle="tab">Waterfall</a>
</li>
<li role="presentation">
<a href="#tab-audio-{{ data.id }}"
aria-controls="tab-audio-{{ data.id }}"
role="tab"
data-toggle="tab">Audio</a>
</li>
<li role="presentation">
<a href="#tab-data-{{ data.id }}"
aria-controls="tab-data-{{ data.id }}"
role="tab"
data-toggle="tab">Data</a>
</li>
</ul>
<div class="tab-content">
<div role="tabpanel" class="tab-pane active" id="tab-waterfall-{{ data.id }}">
{% if data.waterfall %}
<div id="waterfall-{{ data.id }}" class="waterfall">
<img class="img-responsive waterfall" src="{{ MEDIA_URL }}{{ data.waterfall }}" alt="waterfall">
</div>
{% else %}
<div class="notice">
Waiting for waterfall
</div>
{% endif %}
</div>
<div role="tabpanel" class="tab-pane" id="tab-audio-{{ data.id }}">
{% if data.payload %}
<div id="loading-{{ data.id }}" class="notice">Loading data...</div>
<div class="progress progress-striped active" id="progress-bar-{{ data.id }}">
<div class="progress-bar progress-bar-info"></div>
</div>
<div class="wave tab-data" id="data-{{ data.id }}"
data-id="{{ data.id }}"
data-payload="{{ MEDIA_URL }}{{ data.payload }}"></div>
<div id="wave-spectrogram-{{ data.id }}"></div>
<button type="button" class="btn btn-primary btn-xs playpause">
<span class="glyphicon glyphicon-play"></span>
<span class="glyphicon glyphicon-pause"></span>
</button>
<span id="playback-time-{{ data.id }}" class="label label-info playback-time"></span>
{% else %}
<div class="notice">
Waiting for audio
</div>
{% endif %}
</div>
<div role="tabpanel" class="tab-pane tab-data" id="tab-data-{{ data.id }}">
{% if data.demoddata.all %}
{% for demoddata in data.demoddata.all %}
<span class="label label-default">{{ demoddata.payload_demod }}</span>
<div class="well well-sm data-well">
{% if demoddata.is_image %}
<img src="{{ demoddata.payload_demod.url }}" alt="data-{{ data.pk }}">
{% else %}
{{ demoddata.display_payload }}
{% endif %}
</div>
{% endfor %}
{% else %}
<div class="notice">
Waiting for demoded data
</div>
{% endif %}
</div>
</div>
</div>
<div class="panel-footer">
<span class="hidden-xs">
{% if data.rise_azimuth %}
Rise Az: {{ data.rise_azimuth }}°
{% endif %}
{% if data.max_altitude %}
Max Alt: {{ data.max_altitude }}°
{% endif %}
{% if data.set_azimuth %}
Set Az: {{ data.set_azimuth }}°
{% endif %}
</span>
&nbsp;
{% if data.payload %}
<a href="{{ MEDIA_URL }}{{ data.payload }}"
target="_blank"
download="">
<button type="button" class="btn btn-default btn-xs">
<span class="glyphicon glyphicon-download"></span> Audio
</button>
</a>
{% endif %}
{% if data.waterfall %}
<a href="{{ MEDIA_URL }}{{ data.waterfall }}"
target="_blank"
download="">
<button type="button" class="btn btn-default btn-xs">
<span class="glyphicon glyphicon-download"></span> Waterfall
</button>
</a>
{% endif %}
<span class="pull-right hidden-xs">
<span class="label label-default">Timeframe</span>
{{ data.start|date:"Y-m-d H:i:s" }} - {{ data.end|date:"Y-m-d H:i:s" }}
</span>
</div>
</div>
{% endfor %}
</div>
</div>
{% endif %}
<hr>
<div class="row tle-data">
<div class="col-md-12">
<h3>TLE used <small>by Celestrak</small></h3>
<pre>{{ observation.tle.tle1 }}<br>{{ observation.tle.tle2 }}</pre>
<pre>{{ observation.tle.tle1 }}<br>{{ observation.tle.tle2 }}</pre>
</div>
</div>
@ -279,8 +266,6 @@
{% endblock content %}
{% block javascript %}
<script src="{% static 'lib/d3/d3.min.js' %}"></script>
<script src="{% static 'lib/d3-timeline/src/d3-timeline.js' %}"></script>
<script src="{% static 'lib/wavesurfer.js/dist/wavesurfer.min.js' %}"></script>
<script src="{% static 'lib/wavesurfer.js/dist/plugin/wavesurfer.spectrogram.min.js' %}"></script>
<script src="{% static 'lib/moment/min/moment.min.js' %}"></script>

View File

@ -61,18 +61,18 @@
<th>Encoding</th>
<th>Timeframe</th>
<th>Observer</th>
<th>Stations</th>
<th>Station</th>
</thead>
<tbody>
{% for observation in observations %}
<tr data-norad="{{ observation.satellite.norad_cat_id }}">
<td>
<a href="{% url 'base:observation_view' id=observation.id %}">
{% if observation.has_verified_data %}
{% if observation.is_verified %}
<span class="label label-success" title="There is known good data in this observation"
{% elif observation.is_future %}
<span class="label label-info" title="This observation is in the future"
{% elif observation.has_unvetted_data %}
{% elif not observation.is_vetted %}
<span class="label label-warning" title="There is data that needs vetting in this observation"
{% else %}
<span class="label label-danger" title="No good data in this observation"
@ -100,12 +100,9 @@
</a>
</td>
<td>
{% regroup observation.data_set.all by ground_station as station_list %}
{% for station in station_list %}
<a href="{% url 'base:station_view' id=station.grouper.id %}">
<span class="label label-primary" title="{{ station.grouper.name }}" data-toggle="tooltip">{{ station.grouper.id }}</span>
</a>
{% endfor %}
<a href="{% url 'base:station_view' id=observation.ground_station.id %}">
{{ observation.ground_station.name }}
</a>
</td>
</tr>
{% endfor %}

View File

@ -37,65 +37,65 @@
</div>
<div class="row">
<div class="col-md-4">
<div class="gs-front-line">
<div class="front-line">
<span class="label label-default">Owner</span>
<span class="gs-front-data">
<span class="front-data">
<a href="{% url 'users:view_user' username=station.owner.username %}">
{{ station.owner.displayname }}
</a>
</span>
</div>
{% if station.qthlocator %}
<div class="gs-front-line">
<div class="front-line">
<span class="label label-default">QTH Locator</span>
<span class="gs-front-data">
<span class="front-data">
{{ station.qthlocator }}
</span>
</div>
{% endif %}
{% if station.location %}
<div class="gs-front-line">
<div class="front-line">
<span class="label label-default">Location</span>
<span class="gs-front-data">
<span class="front-data">
{{ station.location }}
</span>
</div>
{% endif %}
<div class="gs-front-line">
<div class="front-line">
<span class="label label-default">Coordinates</span>
<span class="gs-front-data">
<span class="front-data">
{{ station.lat|floatformat:-3 }}°, {{ station.lng|floatformat:-3 }}°
</span>
</div>
<div class="gs-front-line">
<div class="front-line">
<span class="label label-default">Altitude</span>
<span class="gs-front-data">
<span class="front-data">
{{ station.alt }} m
</span>
</div>
<div class="gs-front-line">
<div class="front-line">
<span class="label label-default">Min Horizon</span>
<span class="gs-front-data">
<span class="front-data">
{{ station.horizon }}°
</span>
</div>
<div class="gs-front-line">
<div class="front-line">
<span class="label label-default">Rig</span>
<span class="gs-front-data">
<span class="front-data">
{{ station.rig|default:"-" }}
</span>
</div>
<div class="gs-front-line">
<div class="front-line">
<span class="label label-default">Creation Date</span>
<span class="gs-front-data"
<span class="front-data"
title="{{ station.created|date:"c" }}">
{{ station.created|timesince }} ago
</span>
</div>
{% if station.success_rate %}
<div class="gs-front-line">
<div class="front-line">
<span class="label label-default">Success Rate</span>
<span class="gs-front-data">
<span class="front-data">
<div class="progress" title="{{ station.success_rate }}%">
<div class="gs progress-bar progress-bar-success" data-success-rate="{{ station.success_rate }}">
<span class="sr-only">{{ station.success_rate }}% Complete (success)</span>
@ -107,13 +107,13 @@
</span>
</div>
{% endif %}
<div class="gs-front-line">
<div class="front-line">
{% if station.online %}
<span class="label label-success">Online</span>
{% else %}
<span class="label label-danger">Offline</span>
{% endif %}
<span class="gs-front-data"
<span class="front-data"
title="{{ station.last_seen|date:"c" }}">
{% if station.last_seen %}
Last seen {{ station.last_seen|timesince }} ago
@ -125,7 +125,7 @@
</div>
<div class="col-md-4">
{% for antenna in station.antenna.all %}
<div class="gs-front-line">
<div class="front-line">
<span class="label label-default">{{ antenna.get_antenna_type_display }} {{ antenna.band }} Antenna</span>
</div>
<div class="panel panel-default">
@ -184,53 +184,43 @@
<th>Encoding</th>
<th>Timeframe</th>
<th>Observer</th>
<th>Stations</th>
</thead>
<tbody>
{% regroup station.data_set.all by observation as observations %}
{% for observation in observations|slice:":30" %}
{% for observation in station.observations.all|slice:":30" %}
<tr>
<td>
<a href="{% url 'base:observation_view' id=observation.grouper.id %}">
{% if observation.grouper.has_verified_data %}
<a href="{% url 'base:observation_view' id=observation.id %}">
{% if observation.is_verified %}
<span class="label label-success" title="There is known good data in this observation"
{% elif observation.grouper.is_future %}
{% elif observation.is_future %}
<span class="label label-info" title="This observation is in the future"
{% elif observation.grouper.has_unvetted_data %}
{% elif not observation.is_vetted %}
<span class="label label-warning" title="There is data that needs vetting in this observation"
{% else %}
<span class="label label-danger" title="No good data in this observation"
{% endif %}>
{{ observation.grouper.id }}
{{ observation.id }}
</span>
</a>
</td>
<td>
<a href="#" data-toggle="modal" data-target="#SatelliteModal" data-id="{{ observation.grouper.satellite.norad_cat_id }}">
{{ observation.grouper.satellite.name }}
<a href="#" data-toggle="modal" data-target="#SatelliteModal" data-id="{{ observation.satellite.norad_cat_id }}">
{{ observation.satellite.name }}
</a>
</td>
<td>{{ observation.grouper.transmitter.downlink_low|frq }}</td>
<td>{{ observation.grouper.transmitter.mode|default:"-" }}</td>
<td>{{ observation.transmitter.downlink_low|frq }}</td>
<td>{{ observation.transmitter.mode|default:"-" }}</td>
<td>
<span class="datetime-date">{{ observation.grouper.start|date:"Y-m-d" }}</span>
<span class="datetime-time">{{ observation.grouper.start|date:"H:i:s" }}</span><br>
<span class="datetime-date">{{ observation.grouper.end|date:"Y-m-d" }}</span>
<span class="datetime-time">{{ observation.grouper.end|date:"H:i:s" }}</span>
<span class="datetime-date">{{ observation.start|date:"Y-m-d" }}</span>
<span class="datetime-time">{{ observation.start|date:"H:i:s" }}</span><br>
<span class="datetime-date">{{ observation.end|date:"Y-m-d" }}</span>
<span class="datetime-time">{{ observation.end|date:"H:i:s" }}</span>
</td>
<td>
<a href="{% url 'users:view_user' username=observation.grouper.author.username %}">
{{ observation.grouper.author.displayname }}
<a href="{% url 'users:view_user' username=observation.author.username %}">
{{ observation.author.displayname }}
</a>
</td>
<td>
{% regroup observation.grouper.data_set.all by ground_station as station_list %}
{% for station in station_list %}
<a href="{% url 'base:station_view' id=station.grouper.id %}">
<span class="label label-primary" title="{{ station.grouper.name }}" data-toggle="tooltip">{{ station.grouper.id }}</span>
</a>
{% endfor %}
</td>
</tr>
{% endfor %}
</tbody>
@ -303,10 +293,10 @@
</td>
<td>
{% if nextpass.valid and station.online and station.active %}
<a href="{% url 'base:observation_new' %}?norad={{ nextpass.norad_cat_id }}&ground_station={{ station.id }}&start_date={{ nextpass.tr|date:"Y/m/d H:i" }}&end_date={{ nextpass.ts|date:"Y/m/d H:i" }}"
class="btn btn-default">schedule</a>
<a href="{% url 'base:observation_new' %}?norad={{ nextpass.norad_cat_id }}&ground_station={{ station.id }}&start_date={{ nextpass.tr|date:"Y/m/d H:i" }}&end_date={{ nextpass.ts|date:"Y/m/d H:i" }}"
class="btn btn-default">schedule</a>
{% else %}
<a href="#" class="btn btn-default" disabled>schedule</a>
<a class="btn btn-default" disabled>schedule</a>
{% endif %}
</td>
</tr>
@ -396,4 +386,5 @@
<script src="{% static 'js/station_view.js' %}"></script>
<script src="{% static 'js/gridsquare.js' %}"></script>
<script src="{% static 'js/satellite.js' %}"></script>
<script src="{% static 'js/polar.js' %}"></script>
{% endblock javascript %}

View File

@ -111,6 +111,7 @@
<th>Frequency</th>
<th>Encoding</th>
<th>Timeframe</th>
<th>Station</th>
</thead>
<tbody>
{% for observation in observations.all %}
@ -118,14 +119,14 @@
<td>
<a href="{% url 'base:observation_view' id=observation.id %}">
<span class="label
{% if observation.has_verified_data %}
label-success" title="There is known good data in this observation"
{% if observation.is_verified %}
label-success" title="There is known good data in this observation"
{% elif observation.is_future %}
label-info" title="This observation is in the future"
{% elif observation.has_unvetted_data %}
label-warning" title="There is data that needs vetting in this observation"
label-info" title="This observation is in the future"
{% elif not observation.is_vetted %}
label-warning" title="There is data that needs vetting in this observation"
{% else %}
label-danger" title="No good data in this observation"
label-danger" title="No good data in this observation"
{% endif %}>
{{ observation.id }}
</span>
@ -144,6 +145,11 @@
<span class="datetime-date">{{ observation.end|date:"Y-m-d" }}</span>
<span class="datetime-time">{{ observation.end|date:"H:i:s" }}</span>
</td>
<td>
<a href="{% url 'base:station_view' id=observation.ground_station.id %}">
{{ observation.ground_station.name }}
</a>
</td>
</tr>
{% endfor %}
</tbody>

View File

@ -64,9 +64,9 @@ coverage==4.4.1 \
--hash=sha256:36aa6c8db83bc27346ddcd8c2a60846a7178ecd702672689d3ea1828eb1a4d11 \
--hash=sha256:9824e15b387d331c0fc0fef905a539ab69784368a1d6ac3db864b4182e520948 \
--hash=sha256:4a678e1b9619a29c51301af61ab84122e2f8cc7a0a6b40854b808ac6be604300
Faker==0.8.0 \
--hash=sha256:42c54a8563b671717ae153e069acf0b1c7a12de4576d4916e6958a438ef0e46d \
--hash=sha256:f45909af78ec9ea3f67a56030d0cdadf28de75784e87c02ac5da8bf81f5c41ca
Faker==0.8.3 \
--hash=sha256:499fc2c238501b9846b7e5c771c54f73177f300049aa61e829a46f47e6cd7af2 \
--hash=sha256:79b0682d59c4c6961f10edcc9453563a278640e9ffcfee21427fea7a19972bf8
docopts==0.6.1 \
--hash=sha256:ea8d6b03a0931c75a0e4919a0b0856f1c187c38a96174a750c86b41b903a693a
mock==2.0.0 \

View File

@ -1,7 +1,7 @@
# Basic
Django==1.11.4 \
--hash=sha256:6fd30e05dc9af265f7d7d10cfb0efa013e6236db0853c9f47c74c585587c5a57 \
--hash=sha256:abe86e67dda9897a1536a727ed57dbefb5a42b41943be3b116fe3edab4c07bb2
Django==1.11.5 \
--hash=sha256:89162f70a74aac62a53f975128faba6099a7ef2c9d8140a41ae9d6210bda05cd \
--hash=sha256:1836878162dfdf865492bacfdff0321e4ee8f1e7d51d93192546000b54982b29
django-shortuuidfield==0.1.3 \
--hash=sha256:a292c0fe5538abe947b131e2b914edd9ac44afcc6a40eaec71448e6231a3ef00