1
0
Fork 0

Merge pull request #104 from satnogs/test-job

[Fixes #102] Test client connectivity
merge-requests/118/head
Nikos Roussos 2015-05-06 12:52:11 +03:00
commit 4143a73c71
22 changed files with 833 additions and 500 deletions

1022
LICENSE

File diff suppressed because it is too large Load Diff

View File

@ -12,4 +12,4 @@ See the [documentation](http://docs.satnogs.org/network/).
© 2014-2015 [Libre Space Foundation](http://librespacefoundation.org).
Licensed under the [MPL-2.0](LICENSE).
Licensed under the [AGPLv3](LICENSE).

View File

@ -0,0 +1,9 @@
import django_filters
from network.base.models import Data
class DataViewFilter(django_filters.FilterSet):
class Meta:
model = Data
fields = ['start', 'end', 'ground_station']

View File

@ -10,13 +10,23 @@ class SafeMethodsOnlyPermission(permissions.BasePermission):
return request.method in permissions.SAFE_METHODS
class StationOwnerCanEditPermission(SafeMethodsOnlyPermission):
"""Only the owner can push new data"""
class StationOwnerCanViewPermission(permissions.BasePermission):
"""Only the owner can view station jobs"""
def has_object_permission(self, request, view, obj=None):
if obj is None:
can_edit = True
else:
can_edit = request.user == obj.observation.author
return (can_edit or
super(StationOwnerCanEditPermission,
self).has_object_permission(request, view, obj))
can_edit = request.user == obj.ground_station.owner
return can_edit
class StationOwnerCanEditPermission(permissions.BasePermission):
"""Only the owner can edit station jobs"""
def has_object_permission(self, request, view, obj=None):
if request.method in permissions.SAFE_METHODS:
return True
if obj is None:
can_edit = True
else:
can_edit = request.user == obj.ground_station.owner
return can_edit

View File

@ -49,6 +49,7 @@ class DataSerializer(serializers.ModelSerializer):
class Meta:
model = Data
fields = ('id', 'start', 'end', 'observation', 'ground_station', 'payload')
read_only_fields = ['id', 'start', 'end', 'observation', 'ground_station']
class JobSerializer(serializers.ModelSerializer):

View File

@ -7,13 +7,14 @@ from network.api import views
router = routers.DefaultRouter()
router.register(r'antennas', views.AntennaView)
router.register(r'data', views.DataView)
router.register(r'observations', views.ObservationView)
router.register(r'satellites', views.SatelliteView)
router.register(r'stations', views.StationView)
router.register(r'transponders', views.TransponderView)
router.register(r'data', views.DataView)
router.register(r'jobs', views.JobView)
urlpatterns = patterns(
'',
url(r'^', include(router.urls))

View File

@ -1,65 +1,55 @@
from django.utils.timezone import now
import django_filters
from rest_framework import viewsets, mixins
from network.api.perms import StationOwnerCanEditPermission
from network.api import serializers
from network.api.perms import StationOwnerCanViewPermission, StationOwnerCanEditPermission
from network.api import serializers, filters
from network.base.models import (Antenna, Data, Observation, Satellite,
Station, Transponder)
class AntennaView(viewsets.ModelViewSet):
class AntennaView(viewsets.ReadOnlyModelViewSet):
queryset = Antenna.objects.all()
serializer_class = serializers.AntennaSerializer
class StationView(viewsets.ModelViewSet):
class StationView(viewsets.ReadOnlyModelViewSet):
queryset = Station.objects.all()
serializer_class = serializers.StationSerializer
class SatelliteView(viewsets.ModelViewSet):
class SatelliteView(viewsets.ReadOnlyModelViewSet):
queryset = Satellite.objects.all()
serializer_class = serializers.SatelliteSerializer
#permission_classes = [SafeMethodsOnlyPermission]
class TransponderView(viewsets.ModelViewSet):
class TransponderView(viewsets.ReadOnlyModelViewSet):
queryset = Transponder.objects.all()
serializer_class = serializers.TransponderSerializer
class ObservationView(viewsets.ModelViewSet):
class ObservationView(viewsets.ReadOnlyModelViewSet):
queryset = Observation.objects.all()
serializer_class = serializers.ObservationSerializer
class DataFilter(django_filters.FilterSet):
class Meta:
model = Data
fields = ['start', 'end', 'ground_station']
class DataView(viewsets.ReadOnlyModelViewSet, mixins.UpdateModelMixin):
class DataView(viewsets.ModelViewSet, mixins.UpdateModelMixin):
queryset = Data.objects.all()
serializer_class = serializers.DataSerializer
filter_class = DataFilter
permission_classes = [
StationOwnerCanEditPermission
]
def get_queryset(self):
payload = self.request.query_params.get('payload', None)
if payload == '':
return self.queryset.filter(payload='')
return super(DataView, self).get_queryset()
class JobView(viewsets.ReadOnlyModelViewSet):
queryset = Data.objects.filter(payload='')
serializer_class = serializers.JobSerializer
filter_class = DataFilter
filter_class = filters.DataViewFilter
filter_fields = ('ground_station')
permission_classes = [
StationOwnerCanViewPermission
]
def get_queryset(self):
return self.queryset.filter(start__gte=now())

View File

@ -7,5 +7,5 @@ class StationForm(forms.ModelForm):
class Meta:
model = Station
fields = ['name', 'image', 'alt',
'lat', 'lng', 'qthlocator', 'antenna', 'online']
'lat', 'lng', 'qthlocator', 'antenna', 'active']
image = forms.ImageField(required=False)

View File

@ -1,64 +0,0 @@
import signal
import sys
from optparse import make_option
from orbit import satellite
from django.core.management.base import BaseCommand, CommandError
from network.base.models import Satellite
def signal_handler(signal, frame):
print('You pressed Ctrl+C!')
sys.exit(0)
signal.signal(signal.SIGINT, signal_handler)
class Command(BaseCommand):
option_list = BaseCommand.option_list + (
make_option('--delete',
action='store_true',
dest='delete',
default=False,
help='Delete Satellites'),
)
args = '<Satellite Range>'
help = 'Updates/Inserts TLEs for a range of Satellites (eg. xxxx:xxxx)'
def handle(self, *args, **options):
for arg in args:
try:
start, end = arg.split(':')
except ValueError:
raise CommandError('You need to spacify the range in the form: xxxx:xxxx')
r = range(int(start), int(end))
for item in r:
if options['delete']:
try:
Satellite.objects.get(norad_cat_id=item).delete()
self.stdout.write('Satellite {}: deleted'.format(item))
continue
except:
self.stdout.write('Satellite with Identifier {} does not exist'.format(item))
continue
try:
sat = satellite(item)
except:
self.stdout.write('Satellite with Identifier {} does not exist'.format(item))
continue
try:
obj = Satellite.objects.get(norad_cat_id=item)
except:
obj = Satellite(norad_cat_id=item)
obj.name = sat.name()
tle = sat.tle()
obj.tle0 = tle[0]
obj.tle1 = tle[1]
obj.tle2 = tle[2]
obj.save()
self.stdout.write('fetched data for {}: {}'.format(obj.norad_cat_id, obj.name))

View File

@ -10,7 +10,7 @@ class Command(BaseCommand):
def handle(self, *args, **options):
satellites = Satellite.objets.all()
satellites = Satellite.objetcs.all()
for obj in satellites:
try:
@ -27,4 +27,4 @@ class Command(BaseCommand):
obj.tle2 = tle[2]
obj.save()
self.stdout.write(('Satellite {} with Identifier {} '
'found [updated]').format(obj.norad_cat_id, obj.name))
'found [updated]').format(obj.norad_cat_id, obj.name))

View File

@ -0,0 +1,36 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('base', '0015_station_qthlocator'),
]
operations = [
migrations.RemoveField(
model_name='station',
name='online',
),
migrations.AddField(
model_name='station',
name='active',
field=models.BooleanField(default=False),
preserve_default=True,
),
migrations.AddField(
model_name='station',
name='last_seen',
field=models.DateTimeField(null=True, blank=True),
preserve_default=True,
),
migrations.AlterField(
model_name='satellite',
name='updated',
field=models.DateTimeField(auto_now=True),
preserve_default=True,
),
]

View File

@ -2,12 +2,13 @@
from __future__ import unicode_literals
from django.db import models, migrations
import django.core.validators
class Migration(migrations.Migration):
dependencies = [
('base', '0015_station_qthlocator'),
('base', '0016_auto_20150416_0758'),
]
operations = [
@ -46,4 +47,14 @@ class Migration(migrations.Migration):
field=models.CharField(default='', max_length=255, blank=True),
preserve_default=False,
),
migrations.AlterField(
model_name='transponder',
name='baud',
field=models.FloatField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0)]),
),
migrations.AlterField(
model_name='transponder',
name='mode',
field=models.CharField(blank=True, max_length=10, choices=[(b'FM', b'FM'), (b'AFSK', b'AFSK'), (b'BFSK', b'BFSK'), (b'APRS', b'APRS'), (b'SSTV', b'SSTV'), (b'CW', b'CW'), (b'FMN', b'FMN')]),
),
]

View File

@ -1,3 +1,5 @@
from datetime import timedelta
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from django.utils.timezone import now
@ -44,8 +46,8 @@ class Station(models.Model):
'contact SatNOGS Team'))
featured_date = models.DateField(null=True, blank=True)
created = models.DateTimeField(auto_now_add=True)
online = models.BooleanField(default=False,
help_text='Is your Ground Station functional?')
active = models.BooleanField(default=False)
last_seen = models.DateTimeField(null=True, blank=True)
def get_image(self):
if self.image and hasattr(self.image, 'url'):
@ -53,6 +55,14 @@ class Station(models.Model):
else:
return settings.STATION_DEFAULT_IMAGE
@property
def online(self):
try:
heartbeat = self.last_seen + timedelta(minutes=settings.STATION_HEARTBEAT_TIME)
return self.active and heartbeat > now()
except:
return False
def __unicode__(self):
return "%d - %s" % (self.pk, self.name)
@ -64,7 +74,7 @@ class Satellite(models.Model):
tle0 = models.CharField(max_length=100, blank=True)
tle1 = models.CharField(max_length=200, blank=True)
tle2 = models.CharField(max_length=200, blank=True)
updated = models.DateTimeField(auto_now_add=True, blank=True)
updated = models.DateTimeField(auto_now=True, blank=True)
def __unicode__(self):
return self.name
@ -79,9 +89,9 @@ class Transponder(models.Model):
downlink_low = models.PositiveIntegerField(blank=True, null=True)
downlink_high = models.PositiveIntegerField(blank=True, null=True)
mode = models.CharField(choices=zip(MODE_CHOICES, MODE_CHOICES),
max_length=10)
max_length=10, blank=True)
invert = models.BooleanField(default=False)
baud = models.FloatField(validators=[MinValueValidator(0)])
baud = models.FloatField(validators=[MinValueValidator(0)], null=True, blank=True)
satellite = models.ForeignKey(Satellite, related_name='transponder',
null=True)

View File

@ -29,7 +29,8 @@ class StationFactory(factory.django.DjangoModelFactory):
lat = fuzzy.FuzzyFloat(-20, 70)
lng = fuzzy.FuzzyFloat(-180, 180)
featured_date = fuzzy.FuzzyDateTime(now() - timedelta(days=10), now())
online = fuzzy.FuzzyChoice(choices=[True, False])
active = fuzzy.FuzzyChoice(choices=[True, False])
last_seen = fuzzy.FuzzyDateTime(now() - timedelta(days=10), now())
@factory.post_generation
def antennas(self, create, extracted, **kwargs):

View File

@ -18,7 +18,7 @@ from network.base.forms import StationForm
def index(request):
"""View to render index page."""
observations = Observation.objects.all()
featured_station = Station.objects.filter(online=True).latest('featured_date')
featured_station = Station.objects.filter(active=True).latest('featured_date')
ctx = {
'latest_observations': observations.filter(end__lt=now()),
@ -74,7 +74,6 @@ def observation_new(request):
satellites = Satellite.objects.filter(transponder__alive=True)
transponders = Transponder.objects.filter(alive=True)
return render(request, 'base/observation_new.html',
{'satellites': satellites,
'transponders': transponders,
@ -98,6 +97,8 @@ def prediction_windows(request, sat_id, start_date, end_date):
stations = Station.objects.all()
for station in stations:
if not station.online:
continue
observer = ephem.Observer()
observer.lon = str(station.lng)
observer.lat = str(station.lat)
@ -192,7 +193,12 @@ def station_edit(request):
f.owner = request.user
f.save()
form.save_m2m()
messages.success(request, 'Successfully saved Ground Station')
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, 'Some fields missing on the form')

View File

@ -151,6 +151,9 @@ REST_FRAMEWORK = {
),
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.TokenAuthentication',
),
'DEFAULT_FILTER_BACKENDS': (
'rest_framework.filters.DjangoFilterBackend',
)
}
@ -162,10 +165,6 @@ import dj_database_url
DATABASE_URL = getenv('DATABASE_URL', 'sqlite:///db.sqlite3')
DATABASES = {'default': dj_database_url.parse(DATABASE_URL)}
REST_FRAMEWORK = {
'DEFAULT_FILTER_BACKENDS': ('rest_framework.filters.DjangoFilterBackend',)
}
# Mapbox API
MAPBOX_GEOCODE_URL = 'https://api.tiles.mapbox.com/v4/geocode/mapbox.places/'
MAPBOX_MAP_ID = getenv('MAPBOX_MAP_ID', '')
@ -174,3 +173,6 @@ MAPBOX_TOKEN = getenv('MAPBOX_TOKEN', '')
# Observations datetimes in minutes
DATE_MIN_START = '60'
DATE_MAX_RANGE = '480'
# Station heartbeat in minutes
STATION_HEARTBEAT_TIME = 60

View File

@ -94,18 +94,18 @@
<div class="container">
<hr>
<div class="row">
<div class="col-md-6">
<span class="glyphicon glyphicon-copyright-mark" aria-hidden="true"></span> 2014<script>document.write("-"+new Date().getFullYear());</script>
<a href="http://librespacefoundation.org/" target="_blank">Libre Space Foundation</a>.<br>
<span class="glyphicon glyphicon-cloud" aria-hidden="true"></span>
Observation data are freely distributed under the
<a href="https://creativecommons.org/licenses/by-sa/4.0/" target="_blank">CC BY-SA</a> license.
<div class="col-md-6">
<span class="glyphicon glyphicon-copyright-mark" aria-hidden="true"></span> 2014<script>document.write("-"+new Date().getFullYear());</script>
<a href="http://librespacefoundation.org/" target="_blank">Libre Space Foundation</a>.<br>
<span class="glyphicon glyphicon-cloud" aria-hidden="true"></span>
Observation data are freely distributed under the
<a href="https://creativecommons.org/licenses/by-sa/4.0/" target="_blank">CC BY-SA</a> license.
</div>
<div class="col-md-6 text-right footer-options">
<a href="https://github.com/satnogs/satnogs-network">Contribute</a> |
<a href="#top">Back to top</a>
</div>
</div>
<div class="col-md-6 text-right footer-options">
<a href="https://github.com/satnogs/satnogs-network">Contribute</a> |
<a href="#top">Back to top</a>
</div>
</div>
</div>
</footer>

View File

@ -10,16 +10,29 @@
{% endblock css %}
{% block content %}
<h2 id="station-info"
data-name="{{ station.name }}"
data-id="{{ station.id }}"
data-lng="{{ station.lng }}"
data-lat="{{ station.lat }}">
{{ station.id }} - {{ station.name }}
{% if request.user == station.owner %}
<button class="btn btn-primary pull-right" data-toggle="modal" data-target="#StationModal">Edit Ground Station</button>
{% endif %}
</h2>
<div class="row">
<div class="col-md-6">
<h2 id="station-info"
data-name="{{ station.name }}"
data-id="{{ station.id }}"
data-lng="{{ station.lng }}"
data-lat="{{ station.lat }}">
{{ station.id }} - {{ station.name }}
{% if station.online %}
<span class="label label-success">Online</span>
{% else %}
<span class="label label-danger">Offline</button>
{% endif %}
</h2>
</div>
<div class="col-md-6 text-right">
<h2>
{% if request.user == station.owner %}
<button class="btn btn-primary" data-toggle="modal" data-target="#StationModal">Edit Ground Station</button>
{% endif %}
</h2>
</div>
</div>
<div class="row">
<div class="col-md-4">
@ -66,6 +79,13 @@
{{ station.created|timesince }} ago
</span>
</div>
<div class="gs-front-line">
<span class="label label-default">Last Seen</span>
<span class="gs-front-data"
title="{{ station.last_seen|date:"c" }}">
{{ station.last_seen|timesince }} ago
</span>
</div>
</div>
<div class="col-md-4">
{% for antenna in station.antenna.all %}

View File

@ -73,8 +73,8 @@
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<div class="checkbox">
<label>
<input type="checkbox" name="online" {% if form.online.value %}checked="True"{% endif %}> Online Ground Station
<input type="checkbox" name="active" {% if form.active.value %}checked="True"{% endif %}">
Is it operational?
</label>
</div>
</div>

View File

@ -4,6 +4,6 @@
{% block branding %}
<a class="navbar-brand" rel="nofollow" href="#">
SatNOGS Network API <span class="version"></span>
SatNOGS Network API <span class="version">1</span>
</a>
{% endblock %}
{% endblock %}

View File

@ -169,4 +169,4 @@
{% block javascript %}
<script src="{% static 'js/gridsquare.js' %}"></script>
{% endblock javascript %}
{% endblock javascript %}

View File

@ -2,5 +2,5 @@ from allauth.account.adapter import DefaultAccountAdapter
class NoSignupsAdapter(DefaultAccountAdapter):
def is_open_for_signup(self, request):
def is_open_for_signup(self, request):
return False