diff --git a/.gitignore b/.gitignore index bc9ac17..34fc210 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ env .env .cache *.egg-info +*.DS_Store # Logs *.log diff --git a/db/base/admin.py b/db/base/admin.py index bcc286f..31982d4 100644 --- a/db/base/admin.py +++ b/db/base/admin.py @@ -9,7 +9,7 @@ from django.shortcuts import redirect from django.urls import reverse from db.base.models import Artifact, DemodData, ExportedFrameset, Mode, \ - Satellite, Telemetry, Transmitter, TransmitterEntry, \ + Operator, Satellite, Telemetry, Transmitter, TransmitterEntry, \ TransmitterSuggestion from db.base.tasks import check_celery, decode_all_data @@ -20,6 +20,13 @@ class ModeAdmin(admin.ModelAdmin): list_display = ('name', ) +@admin.register(Operator) +class OperatorAdmin(admin.ModelAdmin): + """Defines Operator view in django admin UI""" + list_display = ('name', 'names', 'website') + search_fields = ('name', 'names') + + @admin.register(Satellite) class SatelliteAdmin(admin.ModelAdmin): """Defines Satellite view in django admin UI""" diff --git a/db/base/fixtures/operators.json b/db/base/fixtures/operators.json new file mode 100644 index 0000000..e54bbd1 --- /dev/null +++ b/db/base/fixtures/operators.json @@ -0,0 +1,22 @@ +[ +{ + "model": "base.operator", + "pk": 1, + "fields": { + "name": "Libre Space Foundation", + "names": "LSF", + "description": "The Libre Space Foundation promotes open source space technologies.", + "website": "https://libre.space" + } +}, +{ + "model": "base.operator", + "pk": 2, + "fields": { + "name": "Radio Amateur Satellite Corporation", + "names": "AMSAT", + "description": "The goal of AMSAT is to foster Amateur Radio’s participation in space research and communication.", + "website": "https://www.amsat.org" + } +} +] diff --git a/db/base/forms.py b/db/base/forms.py index d85e538..b578058 100644 --- a/db/base/forms.py +++ b/db/base/forms.py @@ -1,9 +1,10 @@ """SatNOGS DB django base Forms class""" -from django import forms +from bootstrap_modal_forms.forms import BSModalModelForm from django.core.exceptions import ValidationError +from django.forms import NumberInput, TextInput from django.utils.translation import ugettext_lazy as _ -from db.base.models import Transmitter, TransmitterEntry +from db.base.models import Satellite, Transmitter, TransmitterEntry def existing_uuid(value): @@ -18,15 +19,52 @@ def existing_uuid(value): ) -class TransmitterEntryForm(forms.ModelForm): +class TransmitterModelForm(BSModalModelForm): # pylint: disable=too-many-ancestors """Model Form class for TransmitterEntry objects""" - - uuid = forms.CharField(required=False, validators=[existing_uuid]) - class Meta: model = TransmitterEntry fields = [ 'description', 'status', 'type', 'uplink_low', 'uplink_high', 'downlink_low', 'downlink_high', 'uplink_drift', 'downlink_drift', 'downlink_mode', 'uplink_mode', - 'invert', 'baud', 'satellite', 'citation', 'service' + 'invert', 'baud', 'citation', 'service' ] + widgets = { + 'description': TextInput(), + } + + +class TransmitterUpdateForm(BSModalModelForm): # pylint: disable=too-many-ancestors + """Model Form class for TransmitterEntry objects""" + class Meta: + model = TransmitterEntry + fields = [ + 'description', 'status', 'type', 'service', 'uplink_low', 'uplink_drift', + 'uplink_high', 'downlink_low', 'uplink_mode', 'downlink_drift', 'downlink_high', + 'downlink_mode', 'invert', 'baud', 'created', 'citation' + ] + widgets = { + 'description': TextInput(), + 'created': TextInput(attrs={'readonly': True}), + } + + +class SatelliteModelForm(BSModalModelForm): + """Form that uses django-bootstrap-modal-forms for satellite editing""" + class Meta: + model = Satellite + fields = [ + 'norad_cat_id', 'name', 'names', 'operator', 'status', 'description', 'countries', + 'website', 'dashboard_url', 'launched', 'deployed', 'decayed', 'image' + ] + labels = { + 'norad_cat_id': _('Norad ID'), + 'names': _('Other names'), + 'countries': _('Countries of Origin'), + 'launched': _('Launch Date'), + 'deployed': _('Deploy Date'), + 'decayed': _('Re-entry Date'), + 'description': _('Description'), + 'dashboard_url': _('Dashboard URL'), + 'operator': _('Owner/Operator'), + } + widgets = {'norad_cat_id': NumberInput(attrs={'readonly': True}), 'names': TextInput()} diff --git a/db/base/migrations/0019_satellite_details.py b/db/base/migrations/0019_satellite_details.py new file mode 100644 index 0000000..b543711 --- /dev/null +++ b/db/base/migrations/0019_satellite_details.py @@ -0,0 +1,50 @@ +# Generated by Django 2.2.14 on 2020-07-15 12:02 + +from django.db import migrations, models +import django.db.models.deletion +import django_countries.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('base', '0018_artifact'), + ] + + operations = [ + migrations.CreateModel( + name='Operator', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255, unique=True)), + ('names', models.TextField(blank=True)), + ('description', models.TextField(blank=True)), + ('website', models.URLField(blank=True)), + ], + ), + migrations.AddField( + model_name='satellite', + name='countries', + field=django_countries.fields.CountryField(blank=True, max_length=746, multiple=True), + ), + migrations.AddField( + model_name='satellite', + name='deployed', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name='satellite', + name='launched', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name='satellite', + name='website', + field=models.URLField(blank=True), + ), + migrations.AddField( + model_name='satellite', + name='operator', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='satellite_operator', to='base.Operator'), + ), + ] diff --git a/db/base/models.py b/db/base/models.py index 7b19e3c..806bb2c 100644 --- a/db/base/models.py +++ b/db/base/models.py @@ -12,6 +12,7 @@ from django.db.models import OuterRef, Subquery from django.db.models.signals import post_save, pre_save from django.utils.encoding import python_2_unicode_compatible from django.utils.timezone import now +from django_countries.fields import CountryField from markdown import markdown from shortuuidfield import ShortUUIDField @@ -86,6 +87,18 @@ class Mode(models.Model): return self.name +@python_2_unicode_compatible +class Operator(models.Model): + """Satellite Owner/Operator""" + name = models.CharField(max_length=255, unique=True) + names = models.TextField(blank=True) + description = models.TextField(blank=True) + website = models.URLField(blank=True) + + def __str__(self): + return self.name + + @python_2_unicode_compatible class Satellite(models.Model): """Model for all the satellites.""" @@ -103,6 +116,20 @@ class Satellite(models.Model): ) decayed = models.DateTimeField(null=True, blank=True) + # new fields below, metasat etc + # countries is multiple for edge cases like ISS/Zarya + countries = CountryField(blank=True, multiple=True, blank_label='(select countries)') + website = models.URLField(blank=True) + launched = models.DateTimeField(null=True, blank=True) + deployed = models.DateTimeField(null=True, blank=True) + operator = models.ForeignKey( + Operator, + blank=True, + null=True, + on_delete=models.SET_NULL, + related_name='satellite_operator' + ) + class Meta: ordering = ['norad_cat_id'] @@ -182,6 +209,47 @@ class Satellite(models.Model): 'redistributable': self.tle_redistributable } + @property + def latest_data(self): + """Returns the latest DemodData for this Satellite + + :returns: dict with most recent DemodData for this Satellite + """ + data = DemodData.objects.filter(satellite=self.id).order_by('-id')[:1] + return { + 'data_id': data[0].data_id, + 'payload_frame': data[0].payload_frame, + 'timestamp': data[0].timestamp, + 'is_decoded': data[0].is_decoded, + 'station': data[0].station, + 'observer': data[0].observer, + } + + @property + def needs_help(self): + """Returns a boolean based on whether or not this Satellite could + use some editorial help based on a configurable threshold + + :returns: bool + """ + score = 0 + if self.description and self.description != '': + score += 1 + if self.countries and self.countries != '': + score += 1 + if self.website and self.website != '': + score += 1 + if self.names and self.names != '': + score += 1 + if self.launched and self.launched != '': + score += 1 + if self.operator and self.operator != '': + score += 1 + if self.image and self.image != '': + score += 1 + + return score <= 2 + def __str__(self): return '{0} - {1}'.format(self.norad_cat_id, self.name) diff --git a/db/base/urls.py b/db/base/urls.py index f8bd837..050bdee 100644 --- a/db/base/urls.py +++ b/db/base/urls.py @@ -1,5 +1,7 @@ """Django base URL routings for SatNOGS DB""" from django.conf.urls import url +from django.contrib.auth.decorators import permission_required +from django.urls import path from db.base import views @@ -7,8 +9,8 @@ BASE_URLPATTERNS = ( [ url(r'^$', views.home, name='home'), url(r'^about/$', views.about, name='about'), - url(r'^transmitters/$', views.transmitters_list, name='transmitters_list'), url(r'^faq/$', views.faq, name='faq'), + url(r'^satellites/$', views.satellites, name='satellites'), url(r'^satellite/(?P[0-9]+)/$', views.satellite, name='satellite'), url(r'^frames/(?P[0-9]+)/$', views.request_export, name='request_export_all'), url( @@ -16,14 +18,32 @@ BASE_URLPATTERNS = ( views.request_export, name='request_export' ), + url(r'^help/$', views.satnogs_help, name='help'), url( - r'^transmitter_suggestion/$', - views.transmitter_suggestion, - name='transmitter_suggestion' + r'^transmitter_suggestion_handler/$', + views.transmitter_suggestion_handler, + name='transmitter_suggestion_handler' ), + url(r'^transmitters/$', views.transmitters_list, name='transmitters_list'), url(r'^statistics/$', views.statistics, name='statistics'), url(r'^stats/$', views.stats, name='stats'), url(r'^users/edit/$', views.users_edit, name='users_edit'), url(r'^robots\.txt$', views.robots, name='robots'), + url(r'^search/$', views.search, name='search_results'), + url( + r'^update_satellite/(?P[0-9]+)/$', + permission_required('base.change_satellite')(views.SatelliteUpdateView.as_view()), + name='update_satellite' + ), + path( + 'create_transmitter/', + views.TransmitterCreateView.as_view(), + name='create_transmitter' + ), + path( + 'update_transmitter/', + views.TransmitterUpdateView.as_view(), + name='update_transmitter' + ), ] ) diff --git a/db/base/views.py b/db/base/views.py index bc6c27b..d6b89b9 100644 --- a/db/base/views.py +++ b/db/base/views.py @@ -1,40 +1,62 @@ """Base django views for SatNOGS DB""" import logging +from datetime import timedelta +from bootstrap_modal_forms.generic import BSModalCreateView, BSModalUpdateView from django.conf import settings from django.contrib import messages -from django.contrib.auth.decorators import login_required +from django.contrib.auth.decorators import login_required, user_passes_test +from django.contrib.auth.mixins import LoginRequiredMixin, \ + PermissionRequiredMixin from django.contrib.auth.models import User -from django.contrib.sites.shortcuts import get_current_site from django.core.cache import cache from django.core.exceptions import ObjectDoesNotExist from django.db import OperationalError -from django.db.models import Count +from django.db.models import Count, Max, Q from django.http import HttpResponse, JsonResponse from django.shortcuts import get_object_or_404, redirect, render -from django.template.loader import render_to_string from django.urls import reverse +from django.utils import timezone from django.views.decorators.http import require_POST -from db.base.forms import TransmitterEntryForm +from db.base.forms import SatelliteModelForm, TransmitterModelForm, \ + TransmitterUpdateForm from db.base.helpers import get_apikey from db.base.models import SERVICE_TYPE, TRANSMITTER_STATUS, \ TRANSMITTER_TYPE, DemodData, Mode, Satellite, Transmitter, \ - TransmitterSuggestion + TransmitterEntry, TransmitterSuggestion from db.base.tasks import export_frames from db.base.utils import cache_statistics LOGGER = logging.getLogger('db') +def superuser_check(user): + """Returns True if user is a superuser, for use with @user_passes_test + """ + return user.is_superuser + + def home(request): """View to render home page. :returns: base/home.html """ - satellites = Satellite.objects.annotate(transmitters_count=Count('transmitter_entries')) - transmitter_suggestions = TransmitterSuggestion.objects.count() - contributors = User.objects.filter(is_active=1).count() + newest_sats = Satellite.objects.all().order_by('-id')[:5].annotate( + transmitters_count=Count('transmitter_entries') + ) + latest_data = Satellite.objects.annotate( + latest=Max('telemetry_data__timestamp'), transmitters_count=Count('transmitter_entries') + ).order_by('-latest')[:5] + + # Calculate latest contributors + date_from = timezone.now() - timedelta(days=1) + latest_submitters = DemodData.objects.filter(timestamp__gte=date_from + ).values('station').annotate(c=Count('station') + ).order_by('-c') + + suggestion_count = TransmitterSuggestion.objects.count() + contributor_count = User.objects.filter(is_active=1).count() cached_stats = cache.get('stats_transmitters') if not cached_stats: try: @@ -44,10 +66,12 @@ def home(request): pass return render( request, 'base/home.html', { - 'satellites': satellites, + 'newest_sats': newest_sats, + 'latest_data': latest_data, + 'latest_submitters': latest_submitters, 'statistics': cached_stats, - 'contributors': contributors, - 'transmitter_suggestions': transmitter_suggestions + 'contributor_count': contributor_count, + 'suggestion_count': suggestion_count } ) @@ -72,6 +96,33 @@ def robots(request): return response +def satellites(request): + """View to render satellites page. + + :returns: base/satellites.html + """ + satellite_objects = Satellite.objects.annotate( + transmitters_count=Count('transmitter_entries') + ).prefetch_related('operator') + suggestion_count = TransmitterSuggestion.objects.count() + contributor_count = User.objects.filter(is_active=1).count() + cached_stats = cache.get('stats_transmitters') + if not cached_stats: + try: + cache_statistics() + cached_stats = cache.get('stats_transmitters') + except OperationalError: + pass + return render( + request, 'base/satellites.html', { + 'satellites': satellite_objects, + 'statistics': cached_stats, + 'contributor_count': contributor_count, + 'suggestion_count': suggestion_count + } + ) + + def satellite(request, norad): """View to render satellite page. @@ -132,68 +183,106 @@ def request_export(request, norad, period=None): return redirect(reverse('satellite', kwargs={'norad': norad})) +# leaving this in place for reference while the New UI is fixed up +# and the functionality below is moved into new modals accordingly. +# @login_required +# @require_POST +# def transmitter_suggestion(request): +# """View to process transmitter suggestion form + +# :returns: the originating satellite page unless an error occurs +# """ +# transmitter_form = TransmitterEntryForm(request.POST) +# if transmitter_form.is_valid(): +# transmitter = transmitter_form.save(commit=False) +# transmitter.user = request.user +# transmitter.reviewed = False +# transmitter.approved = False +# uuid = transmitter_form.cleaned_data['uuid'] +# if uuid: +# transmitter.uuid = uuid +# transmitter.save() + +# # Notify admins +# admins = User.objects.filter(is_superuser=True) +# site = get_current_site(request) +# subject = '[{0}] A new suggestion for {1} was submitted'.format( +# site.name, transmitter.satellite.name +# ) +# template = 'emails/new_transmitter_suggestion.txt' +# saturl = '{0}{1}'.format( +# site.domain, +# reverse('satellite', kwargs={'norad': transmitter.satellite.norad_cat_id}) +# ) +# data = { +# 'satname': transmitter.satellite.name, +# 'saturl': saturl, +# 'sitedomain': site.domain, +# 'contributor': transmitter.user +# } +# message = render_to_string(template, {'data': data}) +# for user in admins: +# try: +# user.email_user(subject, message, from_email=settings.DEFAULT_FROM_EMAIL) +# except Exception: # pylint: disable=W0703 +# LOGGER.error('Could not send email to user', exc_info=True) + +# messages.success( +# request, +# ('Your transmitter suggestion was stored successfully. ' +# 'Thanks for contibuting!') +# ) +# redirect_page = redirect( +# reverse('satellite', kwargs={'norad': transmitter.satellite.norad_cat_id}) +# ) +# else: +# LOGGER.error( +# 'Suggestion form was not valid %s', +# format(transmitter_form.errors), +# exc_info=True, +# extra={ +# 'form': transmitter_form.errors, +# } +# ) +# messages.error(request, 'We are sorry, but some error occured :(') +# redirect_page = redirect(reverse('home')) + +# return redirect_page + + @login_required @require_POST -def transmitter_suggestion(request): - """View to process transmitter suggestion form +@user_passes_test(superuser_check) +def transmitter_suggestion_handler(request): + """Returns the Satellite page after approving or rejecting a suggestion - :returns: the originating satellite page unless an error occurs + :returns: Satellite page """ - transmitter_form = TransmitterEntryForm(request.POST) - if transmitter_form.is_valid(): - transmitter = transmitter_form.save(commit=False) - transmitter.user = request.user - transmitter.reviewed = False + transmitter = TransmitterSuggestion.objects.get(uuid=request.POST['uuid']) + if 'approve' in request.POST: + transmitter.approved = True + messages.success(request, ('Transmitter approved.')) + elif 'reject' in request.POST: transmitter.approved = False - uuid = transmitter_form.cleaned_data['uuid'] - if uuid: - transmitter.uuid = uuid - transmitter.save() + messages.success(request, ('Transmitter rejected.')) + transmitter.reviewed = True + transmitter.created = timezone.now() + transmitter.user = request.user - # Notify admins - admins = User.objects.filter(is_superuser=True) - site = get_current_site(request) - subject = '[{0}] A new suggestion for {1} was submitted'.format( - site.name, transmitter.satellite.name - ) - template = 'emails/new_transmitter_suggestion.txt' - saturl = '{0}{1}'.format( - site.domain, - reverse('satellite', kwargs={'norad': transmitter.satellite.norad_cat_id}) - ) - data = { - 'satname': transmitter.satellite.name, - 'saturl': saturl, - 'sitedomain': site.domain, - 'contributor': transmitter.user - } - message = render_to_string(template, {'data': data}) - for user in admins: - try: - user.email_user(subject, message, from_email=settings.DEFAULT_FROM_EMAIL) - except Exception: # pylint: disable=W0703 - LOGGER.error('Could not send email to user', exc_info=True) + # Need to determine if we will attribute the suggestion or the approval + # transmitter.user = request.user - messages.success( - request, - ('Your transmitter suggestion was stored successfully. ' - 'Thanks for contibuting!') - ) - redirect_page = redirect( - reverse('satellite', kwargs={'norad': transmitter.satellite.norad_cat_id}) - ) - else: - LOGGER.error( - 'Suggestion form was not valid %s', - format(transmitter_form.errors), - exc_info=True, - extra={ - 'form': transmitter_form.errors, - } - ) - messages.error(request, 'We are sorry, but some error occured :(') - redirect_page = redirect(reverse('home')) + transmitter.save() + # the way we handle suggestions in admin is to update the suggestion as + # reviewed and save a new object. This feels hacky but preserves the + # admin workflow + TransmitterSuggestion.objects.filter(uuid=request.POST['uuid']).update( + reviewed=True, approved=transmitter.approved + ) + redirect_page = redirect( + reverse('satellite', kwargs={'norad': transmitter.satellite.norad_cat_id}) + ) return redirect_page @@ -213,25 +302,68 @@ def faq(request): return render(request, 'base/faq.html') +def satnogs_help(request): + """View to render help modal. Have to avoid builtin 'help' name + + :returns: base/modals/help.html + """ + return render(request, 'base/modals/satnogs_help.html') + + +def search(request): + """View to render search page. + + :returns: base/search.html + """ + if ('q' in request.GET) and request.GET['q'].strip(): + query_string = request.GET['q'] + + results = Satellite.objects.filter( + Q(name__icontains=query_string) | Q(names__icontains=query_string) + | Q(norad_cat_id__icontains=query_string) # noqa: W503 google W503 it is evil + ).order_by('name').annotate(transmitters_count=Count('transmitter_entries')) + + if results.count() == 1: + return redirect(reverse('satellite', kwargs={'norad': results[0].norad_cat_id})) + + # else (no-else-return) + return render(request, 'base/search.html', { + 'results': results, + }) + + def stats(request): """View to render stats page. :returns: base/stats.html """ - satellites = [] + cached_satellites = [] ids = cache.get('satellites_ids') observers = cache.get('stats_observers') + suggestion_count = TransmitterSuggestion.objects.count() + contributor_count = User.objects.filter(is_active=1).count() + cached_stats = cache.get('stats_transmitters') if not ids or not observers: try: cache_statistics() + cached_stats = cache.get('stats_transmitters') + ids = cache.get('satellites_ids') + observers = cache.get('stats_observers') except OperationalError: pass - else: - for sid in ids: - stat = cache.get(sid['id']) - satellites.append(stat) + for sid in ids: + stat = cache.get(sid['id']) + cached_satellites.append(stat) - return render(request, 'base/stats.html', {'satellites': satellites, 'observers': observers}) + return render( + request, 'base/stats.html', { + 'satellites': cached_satellites, + 'observers': observers, + 'statistics': cached_stats, + 'contributor_count': contributor_count, + 'suggestion_count': suggestion_count, + } + ) def statistics(request): @@ -253,4 +385,74 @@ def users_edit(request): :returns: base/users_edit.html """ token = get_apikey(request.user) - return render(request, 'base/users_edit.html', {'token': token}) + return render(request, 'base/modals/users_edit.html', {'token': token}) + + +class TransmitterCreateView(LoginRequiredMixin, BSModalCreateView): + """A django-bootstrap-modal-forms view for creating transmitter suggestions""" + template_name = 'base/modals/transmitter_create.html' + model = TransmitterEntry + form_class = TransmitterModelForm + success_message = 'Your transmitter suggestion was stored successfully and will be \ + reviewed by a moderator. Thanks for contibuting!' + + satellite = Satellite() + user = User() + + def dispatch(self, request, *args, **kwargs): + """ + Overridden so we can make sure the `Satellite` instance exists first + """ + self.satellite = get_object_or_404(Satellite, pk=kwargs['satellite_pk']) + self.user = request.user + return super().dispatch(request, *args, **kwargs) + + def form_valid(self, form): + """ + Overridden to add the `Satellite` relation to the `Transmitter` instance. + """ + form.instance.satellite = self.satellite + form.instance.user = self.user + return super().form_valid(form) + + def get_success_url(self): + return self.request.META.get('HTTP_REFERER') + + +class TransmitterUpdateView(LoginRequiredMixin, BSModalUpdateView): + """A django-bootstrap-modal-forms view for updating transmitter entries""" + template_name = 'base/modals/transmitter_update.html' + model = TransmitterEntry + form_class = TransmitterUpdateForm + success_message = 'Your transmitter suggestion was stored successfully and will be \ + reviewed by a moderator. Thanks for contibuting!' + + user = User() + + def get_initial(self): + initial = {} + initial['created'] = timezone.now() + return initial + + def dispatch(self, request, *args, **kwargs): + self.user = request.user + return super().dispatch(request, *args, **kwargs) + + def form_valid(self, form): + form.instance.user = self.user + return super().form_valid(form) + + def get_success_url(self): + return self.request.META.get('HTTP_REFERER') + + +class SatelliteUpdateView(PermissionRequiredMixin, BSModalUpdateView): + """A django-bootstrap-modal-forms view for updating satellite fields""" + permission_required = 'base.change_satellite' + model = Satellite + template_name = 'base/modals/satellite_update.html' + form_class = SatelliteModelForm + success_message = 'Satellite was updated.' + + def get_success_url(self): + return self.request.META.get('HTTP_REFERER') diff --git a/db/settings.py b/db/settings.py index 0b6e76e..e13aa10 100644 --- a/db/settings.py +++ b/db/settings.py @@ -30,9 +30,13 @@ DJANGO_APPS = ( ) THIRD_PARTY_APPS = ( 'avatar', + 'bootstrap_modal_forms', 'rest_framework', 'rest_framework.authtoken', + 'django_countries', 'django_filters', + 'fontawesome_5', + 'widget_tweaks', 'allauth', 'allauth.account', 'allauth.socialaccount', @@ -159,8 +163,8 @@ STATICFILES_FINDERS = ( MEDIA_ROOT = config('MEDIA_ROOT', default=Path('media').resolve()) FILE_UPLOAD_TEMP_DIR = config('FILE_UPLOAD_TEMP_DIR', default=Path('/tmp').resolve()) MEDIA_URL = config('MEDIA_URL', default='/media/') -CRISPY_TEMPLATE_PACK = 'bootstrap3' -SATELLITE_DEFAULT_IMAGE = '/static/img/sat.png' +CRISPY_TEMPLATE_PACK = 'bootstrap4' +SATELLITE_DEFAULT_IMAGE = '/static/img/sat_purple.png' COMPRESS_ENABLED = config('COMPRESS_ENABLED', default=False, cast=bool) COMPRESS_OFFLINE = config('COMPRESS_OFFLINE', default=False, cast=bool) COMPRESS_CACHE_BACKEND = config('COMPRESS_CACHE_BACKEND', default='default') @@ -279,6 +283,9 @@ CSP_DEFAULT_SRC = config( cast=lambda v: tuple(s.strip() for s in v.split(',')), default="'self'," 'https://*.mapbox.com,' + 'https://kit-free.fontawesome.com,' + 'https://ka-f.fontawesome.com,' + 'https://fonts.gstatic.com,' "'unsafe-inline'" ) CSP_SCRIPT_SRC = config( @@ -286,8 +293,8 @@ CSP_SCRIPT_SRC = config( cast=lambda v: tuple(s.strip() for s in v.split(',')), default="'self'," 'https://*.google-analytics.com,' - "'unsafe-eval'," - "'unsafe-inline'" + 'https://kit-free.fontawesome.com,' + 'https://kit.fontawesome.com,' ) CSP_IMG_SRC = config( 'CSP_IMG_SRC', @@ -302,6 +309,12 @@ CSP_IMG_SRC = config( CSP_FRAME_SRC = config( 'CSP_FRAME_SRC', cast=lambda v: tuple(s.strip() for s in v.split(',')), default='blob:' ) +CSP_WORKER_SRC = config( + 'CSP_WORKER_SRC', + cast=lambda v: tuple(s.strip() for s in v.split(',')), + default="'self'," + 'blob:' +) SECURE_HSTS_INCLUDE_SUBDOMAINS = True SECURE_CONTENT_TYPE_NOSNIFF = True SECURE_BROWSER_XSS_FILTER = True diff --git a/db/static/css/app.css b/db/static/css/app.css index 638cc8c..e987770 100644 --- a/db/static/css/app.css +++ b/db/static/css/app.css @@ -4,7 +4,8 @@ @font-face { font-family: ClearSans; src: url('../fonts/ClearSans-Regular.eot'); - src: url('../fonts/ClearSans-Regular.eot?#iefix') format('embedded-opentype'), + src: url('../fonts/ClearSans-Regular.eot?#iefix') + format('embedded-opentype'), url('../fonts/ClearSans-Regular.woff') format('woff'), url('../fonts/ClearSans-Regular.ttf') format('truetype'), url('../fonts/ClearSans-Regular.svg#open_sans') format('svg'); @@ -26,10 +27,12 @@ @font-face { font-family: ClearSans; src: url('../fonts/ClearSans-BoldItalic.eot'); - src: url('../fonts/ClearSans-BoldItalic.eot?#iefix') format('embedded-opentype'), + src: url('../fonts/ClearSans-BoldItalic.eot?#iefix') + format('embedded-opentype'), url('../fonts/ClearSans-BoldItalic.woff') format('woff'), url('../fonts/ClearSans-BoldItalic.ttf') format('truetype'), - url('../fonts/ClearSans-BoldItalic.svg#open_sansbold_italic') format('svg'); + url('../fonts/ClearSans-BoldItalic.svg#open_sansbold_italic') + format('svg'); font-weight: bold; font-style: italic; } @@ -48,12 +51,6 @@ /* Generic ==================== */ -body { - font-size: 14px; - line-height: 1.3; - font-family: ClearSans; -} - .alert-debug { color: black; background-color: white; @@ -112,13 +109,13 @@ body { } #search { - margin: auto; + margin: 15px auto; } .statistics { text-align: center; text-shadow: 1px 1px 2px rgba(150, 150, 150, 0.77); - margin-top: 12px; + margin: 12px auto auto auto; display: inline-block; padding: 0 15px; } @@ -166,6 +163,7 @@ a.satellite-item:hover { min-height: 30px; padding-top: 5px; display: inline-block; + color: var(--satnogs-color-dark); } .satellite-title .badge { @@ -192,7 +190,7 @@ a.satellite-item:hover { } .satellite-status { - font-size: .9em; + font-size: 0.9em; } .satellite-status[data-status='alive'] { @@ -218,7 +216,9 @@ a.satellite-item:hover { #map { width: 100%; - height: 250px; + /* height: 250px; */ + height: 100%; + min-height: 250px; border-radius: 5px; } @@ -232,13 +232,13 @@ a.satellite-item:hover { } .satellite-img-full { - width: 100%; + /* width: 100%; */ max-height: 250px; border-radius: 5px; } .panel-transmitter { - margin: 20px 0; + margin: 10px 0; } .panel-tle { @@ -250,18 +250,14 @@ a.satellite-item:hover { } .suggest-transmitter { - margin-top: -6px; + margin-right: 6px; } -.transmitter-uuid { - margin-top: -6px; - margin-left: 6px; -} - -.transmitter-citation { +/* .transmitter-citation { margin-top: -8px; margin-left: 6px; -} + margin: 6px 10px 6px 10px; +} */ .transmitter-suggestions-counter { display: inline-block; @@ -277,12 +273,19 @@ a.satellite-item:hover { margin: 10px; } +.transmitter-badge { + font-size: 1em; +} + .transmitter-element-suggest { margin-bottom: 10px; } -.transmitter-citation-footer { - font-size: .8em; +.transmitter-card-footer { + font-size: 0.8em; + height: 100%; + padding: 6px 10px 6px 10px; + background-color: var(--satnogs-color-white); } .tle-element { @@ -364,7 +367,7 @@ footer { ============================= */ .chart text { - color: #333; + color: var(--satnogs-color-dark); font-size: 12px; } @@ -401,7 +404,8 @@ svg.chart { border-radius: 3px; } -.tick line, .domain { +.tick line, +.domain { fill: none; stroke: #ddd; } @@ -411,3 +415,583 @@ svg.chart { stroke: #286090; stroke-width: 2; } + +/* New UI CSS +============= */ + +/* Theme */ + +.bg-gradient-primary { + background-color: #4e73df; + background-image: linear-gradient(180deg, #4e73df 10%, #224abe 100%); + background-size: cover; +} + +.bg-gradient-secondary { + background-color: #858796; + background-image: linear-gradient(180deg, #858796 10%, #60616f 100%); + background-size: cover; +} + +.bg-gradient-success { + background-color: #1cc88a; + background-image: linear-gradient(180deg, #1cc88a 10%, #13855c 100%); + background-size: cover; +} + +.bg-gradient-info { + background-color: #36b9cc; + background-image: linear-gradient(180deg, #36b9cc 10%, #258391 100%); + background-size: cover; +} + +.bg-gradient-warning { + background-color: #f6c23e; + background-image: linear-gradient(180deg, #f6c23e 10%, #dda20a 100%); + background-size: cover; +} + +.bg-gradient-danger { + background-color: #e74a3b; + background-image: linear-gradient(180deg, #e74a3b 10%, #be2617 100%); + background-size: cover; +} + +.bg-gradient-light { + background-color: #f8f9fc; + background-image: linear-gradient(180deg, #f8f9fc 10%, #c2cbe5 100%); + background-size: cover; +} + +.bg-gradient-dark { + background-color: #5a5c69; + background-image: linear-gradient(180deg, #5a5c69 10%, #373840 100%); + background-size: cover; +} + +.bg-gray-100 { + background-color: #f8f9fc; +} + +.bg-gray-200 { + background-color: #eaecf4; +} + +.bg-gray-300 { + background-color: #dddfeb; +} + +.bg-gray-400 { + background-color: #d1d3e2; +} + +.bg-gray-500 { + background-color: #b7b9cc; +} + +.bg-gray-600 { + background-color: #858796; +} + +.bg-gray-700 { + background-color: #6e707e; +} + +.bg-gray-800 { + background-color: #5a5c69; +} + +.bg-gray-900 { + background-color: #3a3b45; +} + +.o-hidden { + overflow: hidden; +} + +.text-xs { + font-size: 0.7rem; +} + +.text-lg { + font-size: 1.2rem; +} + +.text-gray-100 { + color: #f8f9fc; +} + +.text-gray-200 { + color: #eaecf4; +} + +.text-gray-300 { + color: #dddfeb; +} + +.text-gray-400 { + color: #d1d3e2; +} + +.text-gray-500 { + color: #b7b9cc; +} + +.text-gray-600 { + color: #858796; +} + +.text-gray-700 { + color: #6e707e; +} + +.text-gray-800 { + color: #5a5c69; +} + +.text-gray-900 { + color: #3a3b45; +} + +.icon-circle { + height: 2.5rem; + width: 2.5rem; + border-radius: 100%; + display: flex; + align-items: center; + justify-content: center; +} + +.border-left-primary { + border-left: 0.25rem solid var(--satnogs-color-primary); +} + +.border-bottom-primary { + border-bottom: 0.25rem solid var(--satnogs-color-primary); +} + +.border-left-active { + border-left: 0.25rem solid #dff0d8; + /* color: #3c763d; + background-color: #dff0d8; + border-color: #d6e9c6; */ +} + +.border-left-secondary { + border-left: 0.25rem solid #858796; +} + +.border-bottom-secondary { + border-bottom: 0.25rem solid #858796; +} + +.border-left-success { + border-left: 0.25rem solid #1cc88a; +} + +.border-bottom-success { + border-bottom: 0.25rem solid #1cc88a; +} + +.border-left-info { + border-left: 0.25rem solid #36b9cc; +} + +.border-bottom-info { + border-bottom: 0.25rem solid #36b9cc; +} + +.border-left-warning { + border-left: 0.25rem solid #f6c23e; +} + +.border-bottom-warning { + border-bottom: 0.25rem solid #f6c23e; +} + +.border-left-danger { + border-left: 0.25rem solid #e74a3b; +} + +.border-bottom-danger { + border-bottom: 0.25rem solid #e74a3b; +} + +.border-left-light { + border-left: 0.25rem solid #f8f9fc; +} + +.border-bottom-light { + border-bottom: 0.25rem solid #f8f9fc; +} + +.border-left-dark { + border-left: 0.25rem solid #5a5c69; +} + +.border-bottom-dark { + border-bottom: 0.25rem solid #5a5c69; +} + +@-webkit-keyframes grow_in { + + 0% { + transform: scale(0.9); + opacity: 0; + } + + 100% { + transform: scale(1); + opacity: 1; + } +} + +@keyframes grow_in { + + 0% { + transform: scale(0.9); + opacity: 0; + } + + 100% { + transform: scale(1); + opacity: 1; + } +} + +.animated--grow-in, +.sidebar .nav-item .collapse { + -webkit-animation-name: grow_in; + animation-name: grow_in; + -webkit-animation-duration: 200ms; + animation-duration: 200ms; + -webkit-animation-timing-function: transform + cubic-bezier(0.18, 1.25, 0.4, 1), + opacity cubic-bezier(0, 1, 0.4, 1); + animation-timing-function: transform cubic-bezier(0.18, 1.25, 0.4, 1), + opacity cubic-bezier(0, 1, 0.4, 1); +} + +@-webkit-keyframes fade_in { + + 0% { + opacity: 0; + } + + 100% { + opacity: 1; + } +} + +@keyframes fade_in { + + 0% { + opacity: 0; + } + + 100% { + opacity: 1; + } +} + +.animated--fade-in { + -webkit-animation-name: fade_in; + animation-name: fade_in; + -webkit-animation-duration: 200ms; + animation-duration: 200ms; + -webkit-animation-timing-function: opacity cubic-bezier(0, 1, 0.4, 1); + animation-timing-function: opacity cubic-bezier(0, 1, 0.4, 1); +} + +#satnogs-menu { + background-color: var(--satnogs-color-primary); + height: 30px; + padding-top: 5px; + padding-bottom: 5px; + padding-left: 0; + margin-top: 0; + margin-bottom: 0; +} + +#satnogs-navbar { + margin-right: 15px; +} + +.satnogs-navbar-link { + margin-right: 5px; + margin-left: 5px; + font-family: Roboto, Helvetica, Arial, sans-serif; + font-weight: 700; + letter-spacing: 1.6px; + font-size: 13px; + color: var(--satnogs-color-white); +} + +.satnogs-navbar-link:hover { + text-decoration: none; + color: var(--satnogs-color-white); +} + +#db-menu { + background-color: var(--satnogs-color-menu-background); + height: 30px; +} + +#db-menu-col { + height: 30px; + /* border-color: var(--satnogs-color-primary); */ + padding-right: 0; + padding-left: 0; +} + +#db-menu-navbar { + /* border-color: var(--satnogs-color-primary); */ + padding-right: 0; + padding-left: 0; + height: 30px; +} + +.db-menu-link { + font-family: Roboto, Helvetica, Arial, sans-serif; + font-weight: 700; + font-size: 13px; + letter-spacing: 1.6px; + height: 30px; + padding-left: 0; + /* color: rgba(0,0,0,0.5); */ + color: var(--satnogs-color-menu-text); +} + +.db-menu-link:hover { + text-decoration: none; + color: var(--satnogs-color-menu-hover); +} + +.satnogs-card-header { + padding: 5px 15px 5px 15px; + height: 100%; + background-color: var(--satnogs-color-header); + border-color: var(--satnogs-color-border); +} + +.satnogs-card-body { + padding: 5px 15px 5px 15px; + background-color: var(--satnogs-color-background); + border-color: var(--satnogs-color-border); +} + +.satnogs-card { + background-color: var(--satnogs-color-card-background); + border-color: var(--satnogs-color-border); +} + +.transmitter-card-header { + padding: 6px 10px 6px 10px; + margin-bottom: 0; + background-color: var(--satnogs-color-white); + /* height: 34px; */ +} + +.transmitter-title { + font-size: 16px; +} + +.panel-title { + /* width: 100%; */ + padding-right: 0; + padding-left: 0; +} + +.satellite-panels, +.stats-panel { + margin-top: 15px; +} + +.transmitter-inactive { + color: #a94442; + background-color: #f2dede; + border-color: #ebccd1; +} + +.transmitter-active { + color: #3c763d; + background-color: #dff0d8; + border-color: #d6e9c6; +} + +.transmitter-card-body { + padding: 10px 10px; +} + +/* Color pallete */ + +.text-satnogs-white { + color: var(--satnogs-color-white); +} + +.text-satnogs-dark { + color: var(--satnogs-color-dark); +} + +.text-satnogs-text { + color: var(--satnogs-color-text); +} + +.text-satnogs-background { + color: var(--satnogs-color-background); +} + +.text-satnogs-primary { + color: var(--satnogs-color-primary); +} + +.text-satnogs-active { + color: #3c763d; +} + +.text-satnogs-inactive { + color: #a94442; +} + +.background-satnogs { + background-color: var(--satnogs-color-background); +} + +.background-satnogs-light { + background-color: var(--satnogs-color-white); +} + +.background-satnogs-primary { + background-color: var(--satnogs-color-primary); +} + +.background-satnogs-header { + background-color: var(--satnogs-color-header); +} + +.text-satnogs-header { + color: var(--satnogs-color-text); +} + +.text-satnogs { + color: var(--satnogs-color-text); +} + +.text-satnogs-headline { + color: var(--satnogs-color-headline); +} + +.satnogs-icon { + color: var(--satnogs-color-icon); +} + +body { + background: var(--color-scheme-background); + color: var(--color-scheme-text-color); + font-size: 14px; + line-height: 1.3; + font-family: ClearSans; +} + +.nav-link.active { + color: var(--satnogs-color-menu-hover); +} + +:root { + --satnogs-color-primary: #656fdb; + --satnogs-color-dark: #4d4e59; + --satnogs-color-text: #7c81b2; + --satnogs-color-background: #f2f2f7; + --satnogs-color-header: #f2f2f7; + --satnogs-color-border: #f2f2f7; + --satnogs-color-white: #f2f2f7; + --satnogs-color-card-background: #f2f2f7; + --satnogs-color-icon: #656fdb; + --satnogs-color-headline: #656fdb; + --satnogs-color-menu-background: #f2f2f7; + --satnogs-color-menu-text: #4d4e59; + --satnogs-color-menu-hover: #656fdb; + --color-scheme-background: white; + --color-scheme-text-color: #4d4e59; +} + +a { + color: var(--satnogs-color-primary); +} + +.satellite-card-body a:link, +.satellite-card-body a:visited, +.satellite-card-body a:hover { + color: var(--color-scheme-text-color); + text-decoration: none; +} + +.satellite-card-body-row { + width: 40px; +} + +td.details-control { + text-align: center; + color: forestgreen; + cursor: pointer; +} + +tr.shown td.details-control { + text-align: center; + color: red; +} + +.table { + color: var(--satnogs-color-dark); +} + +.page-item.active .page-link { + background-color: var(--satnogs-color-primary); + border-color: var(--satnogs-color-primary); +} + +.page-link { + color: var(--satnogs-color-primary); +} + +/* Dark mode */ +/* @media (prefers-color-scheme: dark) { + :root { + --satnogs-color-primary:#656FDB; + --satnogs-color-dark: #4d4e59; + --satnogs-color-text: #F2F2F7; + --satnogs-color-background: #4D4E59; + --satnogs-color-header: #656fdb; + --satnogs-color-border: #656fdb; + --satnogs-color-white: #F2F2F7; + --satnogs-color-card-background: #7c81b2; + --satnogs-color-icon:#4d4e59; + --satnogs-color-headline:#f2f2f7; + --satnogs-color-menu-background: #7c81b2; + --satnogs-color-menu-text: #4d4e59; + --satnogs-color-menu-hover: #F2F2F7; + --color-scheme-background: #4d4e59; + --color-scheme-text-color: #F2F2F7; + } +} */ + +/* Light mode */ +/* @media (prefers-color-scheme: light) { + :root { + --satnogs-color-primary:#656FDB; + --satnogs-color-dark: #4d4e59; + --satnogs-color-text: #7c81b2; + --satnogs-color-background: #F2F2F7; + --satnogs-color-header: #F2F2F7; + --satnogs-color-border: #F2F2F7; + --satnogs-color-white: #F2F2F7; + --satnogs-color-card-background: #f2f2f7; + --satnogs-color-icon: #656FDB; + --satnogs-color-headline:#656FDB; + --satnogs-color-menu-background: #F2F2F7; + --satnogs-color-menu-text: #4d4e59; + --satnogs-color-menu-hover: #656FDB; + --color-scheme-background: white; + --color-scheme-text-color: #4d4e59; + } +} */ diff --git a/db/static/css/newapp.css b/db/static/css/newapp.css new file mode 100644 index 0000000..5a68b6f --- /dev/null +++ b/db/static/css/newapp.css @@ -0,0 +1,376 @@ +/* transitional / new CSS file for the new SatNOGS-DB UI */ + +:root { + --satnogs-color-primary: #656fdb; + --satnogs-color-dark: #4d4e59; + --satnogs-color-text: #7c81b2; + --satnogs-color-background: #f2f2f7; + --satnogs-color-header: #f2f2f7; + --satnogs-color-border: #f2f2f7; + --satnogs-color-white: #f2f2f7; + --satnogs-color-card-background: #f2f2f7; + --satnogs-color-icon: #656fdb; + --satnogs-color-headline: #656fdb; + --satnogs-color-menu-background: #f2f2f7; + --satnogs-color-menu-text: #4d4e59; + --satnogs-color-menu-hover: #656fdb; + --color-scheme-background: white; + --color-scheme-text-color: #4d4e59; +} + +.satellite-title { + font-weight: bold; + font-size: 1.2em; + color: var(--satnogs-color-dark); +} + +.text-satnogs-white { + color: var(--satnogs-color-white); +} + +.text-satnogs-dark { + color: var(--satnogs-color-dark); +} + +.text-satnogs-text { + color: var(--satnogs-color-text); +} + +.text-satnogs-background { + color: var(--satnogs-color-background); +} + +.text-satnogs-primary { + color: var(--satnogs-color-primary); +} + +.text-satnogs-active { + color: #3c763d; +} + +.text-satnogs-inactive { + color: #a94442; +} + +.background-satnogs { + background-color: var(--satnogs-color-background); +} + +.background-satnogs-light { + background-color: var(--satnogs-color-white); +} + +.background-satnogs-primary { + background-color: var(--satnogs-color-primary); +} + +.background-satnogs-header { + background-color: var(--satnogs-color-header); +} + +.text-satnogs-header { + color: var(--satnogs-color-text); +} + +.text-satnogs { + color: var(--satnogs-color-text); +} + +.text-satnogs-headline { + color: var(--satnogs-color-headline); +} + +.satnogs-icon { + color: var(--satnogs-color-icon); +} + +.c-app:not(.c-legacy-theme):not(.c-dark-theme) .c-header.c-header-fixed { + border: 0; + box-shadow: 0 2px 2px 0 rgba(60, 75, 100, 0.14), + 0 3px 1px -2px rgba(60, 75, 100, 0.12), + 0 1px 5px 0 rgba(60, 75, 100, 0.2); +} + +#map { + min-height: 250px; + position: relative; + top: 0; + bottom: 0; + width: 100%; + border-radius: 5px; +} + +.mapbox-improve-map, +.mapboxgl-ctrl-bottom-left { + display: none; +} + +.mapboxgl-canvas-container { + height: 60vh; +} + +.satellite-card-body a:link, +.satellite-card-body a:visited, +.satellite-card-body a:hover { + color: var(--color-scheme-text-color); + text-decoration: none; +} + +.satellite-card-body-row { + width: 40px; +} + +.satellite-img-full { + max-width: 100%; + max-height: 250px; + border-radius: 5px; +} + +.c-avatar { + position: relative; + display: inline-flex; + -ms-flex-align: center; + align-items: center; + -ms-flex-pack: center; + justify-content: center; + border-radius: 50em; + width: 36px; + height: 36px; + font-size: 14.4px; +} + +.c-avatar .c-avatar-status { + width: 10px; + height: 10px; +} + +.c-avatar-img { + width: 100%; + height: auto; + border-radius: 50em; +} + +.c-avatar-status { + position: absolute; + bottom: 0; + display: block; + border: 1px solid #fff; + border-radius: 50em; +} + +.card-satnogs, +.card-satnogs > a, +.jumbotron-satnogs { + color: var(--satnogs-color-dark); +} + +.card-satnogs.card-outline { + border-top: 3px solid var(--satnogs-color-primary); +} + +.card-satnogs.card-outline-tabs > .card-header a:hover { + border-top: 3px solid #dee2e6; +} + +.card-satnogs.card-outline-tabs > .card-header a.active { + border-top: 3px solid var(--satnogs-color-primary); +} + +.sat-menu-panel { + position: relative; +} + +[class*='sidebar-dark'] .sat-menu-panel { + border-top: 1px solid #4f5962; +} + +[class*='sidebar-light'] .sat-menu-panel { + border-top: 1px solid #dee2e6; +} + +.sat-menu-panel, +.sat-menu-panel .info { + overflow: hidden; + white-space: nowrap; +} + +.sat-menu-panel .image { + display: inline-block; + padding-left: 0.8rem; +} + +.sat-menu-panel img { + height: auto; + width: 2.1rem; +} + +.sat-menu-panel .info { + display: inline-block; + padding: 5px 5px 5px 10px; +} + +.sat-menu-panel .status, +.sat-menu-panel .dropdown-menu { + font-size: 0.875rem; +} + +.sat-menu-panel .sat-name { + color: #c2c7d0; + font-weight: bold; +} + +.navbar-nav > .nav-item > .nav-link.active, +.nav > .nav-item > .nav-link.active, +.sidebar-dark-primary .nav-sidebar > .nav-item > .nav-link.active, +.sidebar-light-primary .nav-sidebar > .nav-item > .nav-link.active { + background-color: var(--satnogs-color-primary); + color: #fff; +} + +.nav-pills .nav-link:not(.active):hover { + color: var(--satnogs-color-primary); +} + +.dropdown-item.active, .dropdown-item:active { + background-color: var(--satnogs-color-primary); +} + +.brand-text { + /* color: var(--satnogs-color-primary); */ + color: #b9b9b9; + font-weight: bold; +} + +a { + color: var(--satnogs-color-primary); +} + +.page-item.active .page-link { + z-index: 3; + color: #fff; + background-color: var(--satnogs-color-primary); + border-color: var(--satnogs-color-primary); +} + +.page-link { + color: var(--satnogs-color-primary); + background-color: #fff; + border: 1px solid #dee2e6; +} + +.satnogs-nav-sat { + font-size: small; +} + +.satnogs-nav-sat > .nav-link { + padding-bottom: 0; +} + +.satnogs-nav-sat .nav-link { + padding: 0; + line-height: 1; +} + +.satnogs-nav-sat > .nav-pills .nav-link.active, .nav-pills .show > .nav-link { + color: #fff; + background-color: var(--satnogs-color-primary); +} + +.satnogs-nav-sat > .nav-pills .nav-link:not(.active):hover { + color: var(--satnogs-color-primary); +} + +.satnogs-nav-sat > .nav-tabs .nav-link.active, .nav-tabs .show > .nav-link { + color: #fff; + background-color: var(--satnogs-color-primary); + padding-top: 2px; +} + +.satnogs-nav-sat > .nav-tabs .nav-link:not(.active):hover { + color: var(--satnogs-color-primary); +} + +.navbar-nav > .nav-item > .brand-link { + padding: 0 0 0 0; +} + +.nav-sidebar > .nav-item .nav-icon { + font-size: 24px; +} + +.small-box-satnogs { + background-color: var(--satnogs-color-text); + color: rgba(255, 255, 255, .8); +} + +.sat-menu-panel > .image > .fas { + color: var(--satnogs-color-primary); +} + +.timeline > .time-label > .background-satnogs-primary, +.timeline > div > .background-satnogs-primary { + background-color: var(--satnogs-color-primary); + color: rgba(255, 255, 255, .8); +} + +.nav-sidebar .nav-item > .nav-link.disabled { + color: #6c757d; + pointer-events: none; + cursor: default; +} + +.freqlabel { + display: inline-block; + width: 75px; + padding: 3px; +} + +.btn-satnogs-primary, +.badge-satnogs-primary { + background-color: var(--satnogs-color-primary); + color: rgba(255, 255, 255, .8); +} + +.nav-link.active > .badge-satnogs-primary { + color: rgba(255, 255, 255, .8); + background-color: var(--satnogs-color-text); +} + +.btn-satnogs { + color: var(--satnogs-color-dark); +} + +.btn-navbar-satnogs { + color: #939ba2; + opacity: 1; + border: 1px solid #56606a; +} + +a.dropdown-item { + color: var(--satnogs-color-dark); +} + +.transmitter-element-suggest { + margin-bottom: 10px; +} + +[class*='sidebar-dark'] .user-panel { + border-bottom: none; +} + +[class*='sidebar-dark'] .satnogs-sidebar-footer, +[class*='sidebar-dark'] .satnogs-sidebar-footer a { + color: rgba(255, 255, 255, .5); + font-size: .7rem; +} + +.toasts-top-right { + position: absolute; + right: 1rem; + top: 4.25rem; +} + +.toast.alert-warning > .toast-header { + background-color: rgba(255, 255, 255, .2); + color: #000; +} diff --git a/db/static/fonts/ClearSans-Bold.woff b/db/static/fonts/ClearSans-Bold.woff deleted file mode 100644 index bda6eb2..0000000 Binary files a/db/static/fonts/ClearSans-Bold.woff and /dev/null differ diff --git a/db/static/fonts/ClearSans-BoldItalic.woff b/db/static/fonts/ClearSans-BoldItalic.woff deleted file mode 100644 index 4dee91c..0000000 Binary files a/db/static/fonts/ClearSans-BoldItalic.woff and /dev/null differ diff --git a/db/static/fonts/ClearSans-Italic.woff b/db/static/fonts/ClearSans-Italic.woff deleted file mode 100644 index 56573d2..0000000 Binary files a/db/static/fonts/ClearSans-Italic.woff and /dev/null differ diff --git a/db/static/fonts/ClearSans-Regular.woff b/db/static/fonts/ClearSans-Regular.woff deleted file mode 100644 index f4aacf7..0000000 Binary files a/db/static/fonts/ClearSans-Regular.woff and /dev/null differ diff --git a/db/static/img/sat_purple.png b/db/static/img/sat_purple.png new file mode 100644 index 0000000..cadc2b8 Binary files /dev/null and b/db/static/img/sat_purple.png differ diff --git a/db/static/img/satnogs-logo-only-light.png b/db/static/img/satnogs-logo-only-light.png new file mode 100644 index 0000000..4379a06 Binary files /dev/null and b/db/static/img/satnogs-logo-only-light.png differ diff --git a/db/static/img/satnogs-logo-only-purple.png b/db/static/img/satnogs-logo-only-purple.png new file mode 100644 index 0000000..c4b6b67 Binary files /dev/null and b/db/static/img/satnogs-logo-only-purple.png differ diff --git a/db/static/img/satnogs-logo-only.png b/db/static/img/satnogs-logo-only.png new file mode 100644 index 0000000..9c656de Binary files /dev/null and b/db/static/img/satnogs-logo-only.png differ diff --git a/db/static/img/satnogs-logo-trans.png b/db/static/img/satnogs-logo-trans.png new file mode 100644 index 0000000..7447ccd Binary files /dev/null and b/db/static/img/satnogs-logo-trans.png differ diff --git a/db/static/img/status_alive.png b/db/static/img/status_alive.png new file mode 100644 index 0000000..a8249ad Binary files /dev/null and b/db/static/img/status_alive.png differ diff --git a/db/static/img/status_dead.png b/db/static/img/status_dead.png new file mode 100644 index 0000000..88c06b3 Binary files /dev/null and b/db/static/img/status_dead.png differ diff --git a/db/static/img/status_decayed.png b/db/static/img/status_decayed.png new file mode 100644 index 0000000..b27c92b Binary files /dev/null and b/db/static/img/status_decayed.png differ diff --git a/db/static/img/status_unknown.png b/db/static/img/status_unknown.png new file mode 100644 index 0000000..6d3f78b Binary files /dev/null and b/db/static/img/status_unknown.png differ diff --git a/db/static/js/app.js b/db/static/js/app.js index 8c07721..689a089 100644 --- a/db/static/js/app.js +++ b/db/static/js/app.js @@ -1,3 +1,4 @@ +/* eslint new-cap: "off" */ $(document).ready(function() { 'use strict'; @@ -6,5 +7,39 @@ $(document).ready(function() { $('#copy').text(current_year); // Enable tooltips - $('[data-toggle="tooltip"]').tooltip(); + // $('[data-toggle="tooltip"]').tooltip(); + // $('[data-toggle="popover"]').popover(); + + // User Settings / API Modal Form Link + $('.basemodal-link').each(function () { + $(this).modalForm({ + formURL: $(this).data('form-url'), + modalID: '#basemodal' + }); + $(this).click(function() { + $('#control-sidebar-toggle').ControlSidebar('toggle'); + }); + }); + + // Transitional from inline alerts to Toasts + $('.alert').each(function() { + var alerticon = 'fas fa-question'; + var alerttitle = 'Unknown'; + if ($(this).data('alertclass') == 'alert-success') { + alerticon = 'far fa-thumbs-up'; + alerttitle = 'Success'; + } + if ($(this).data('alertclass') == 'alert-warning') { + alerticon = 'fas fa-exclamation'; + alerttitle = 'Alert'; + } + $(document).Toasts('create', { + class: $(this).data('alertclass'), + title: alerttitle, + autohide: true, + delay: 6000, + icon: alerticon, + body: $(this).data('alertmessage') + }); + }); }); diff --git a/db/static/js/map.js b/db/static/js/map.js index 87459d7..d96885d 100644 --- a/db/static/js/map.js +++ b/db/static/js/map.js @@ -119,7 +119,8 @@ $(document).ready(function() { var map = new mapboxgl.Map({ container: 'map', - style: 'mapbox://styles/pierros/cj8kftshl4zll2slbelhkndwo', + // style: 'mapbox://styles/pierros/cj8kftshl4zll2slbelhkndwo', + style: 'mapbox://styles/cshields/ckc1a24y45smb1ht9bbhrcrk6', zoom: 2, center: sat_location }); @@ -223,4 +224,10 @@ $(document).ready(function() { } setInterval(update_map, 5000); }); + + // couldn't get this to work with shown.bs.tab, have to go with click + // timeout is necessary for the first click for some reason + document.getElementById('mapcontent-tab').addEventListener('click', function() { + setTimeout( function() { map.resize();}, 200); + }); }); diff --git a/db/static/js/satellite.js b/db/static/js/satellite.js index 9c44bf5..b5db463 100644 --- a/db/static/js/satellite.js +++ b/db/static/js/satellite.js @@ -1,3 +1,4 @@ +/* eslint new-cap: "off" */ function copyToClipboard(text, el) { var copyTest = document.queryCommandSupported('copy'); var elOriginalText = el.attr('data-original-title'); @@ -41,7 +42,7 @@ function transmitter_suggestion_type(selection) { $('.input-group').has('input[name=\'invert\']').hide(); $('.input-group').has('select[name=\'uplink_mode\']').hide(); - $('.input-group-addon:contains(\'Downlink Low\')').html('Downlink'); + $('.input-group-prepend:contains(\'Downlink Low\')').html('Downlink'); break; case 'Transceiver': $('.input-group').show(); @@ -54,15 +55,15 @@ function transmitter_suggestion_type(selection) { $('.input-group').has('input[name=\'downlink_high\']').hide(); $('.input-group').has('input[name=\'invert\']').hide(); - $('input[name=\'downlink_low\']').prev().html('Downlink'); - $('input[name=\'uplink_low\']').prev().html('Uplink'); + $('input[name=\'downlink_low\']').prev().html('Downlink'); + $('input[name=\'uplink_low\']').prev().html('Uplink'); break; case 'Transponder': $('.input-group').show(); $('input').prop( 'disabled', false ); $('select').prop( 'disabled', false ); - $('input[name=\'downlink_low\']').prev().html('Downlink Low'); - $('input[name=\'uplink_low\']').prev().html('Uplink Low'); + $('input[name=\'downlink_low\']').prev().html('Downlink Low'); + $('input[name=\'uplink_low\']').prev().html('Uplink Low'); break; } } @@ -97,7 +98,7 @@ $(document).ready(function() { transmitter_suggestion_type(selection); }); - $('.transmitter_suggestion-edit-modal').on('show.bs.modal', function(){ + $('.transmitter_suggestion-modal').on('show.bs.modal', function(){ var selection = $(this).find('.transmitter_suggestion-type').val(); transmitter_suggestion_type(selection); @@ -114,10 +115,6 @@ $(document).ready(function() { } }); - $('#NewSuggestionModal').on('show.bs.modal', function(){ - transmitter_suggestion_type('Transmitter'); - }); - // Calculate the drifted frequencies $('.drifted').each(function() { var drifted = ppb_to_freq($(this).data('freq_or'),$(this).data('drift')); @@ -126,13 +123,13 @@ $(document).ready(function() { $('.uplink-drifted-sugedit').on('change click', function(){ var freq_obs = parseInt($(this).val()); - var freq = parseInt($('.in input[name=\'uplink_low\']').val()); + var freq = parseInt($('input[name=\'uplink_low\']:visible').val()); $('.uplink-ppb-sugedit').val(freq_to_ppb(freq_obs,freq)); }); $('.downlink-drifted-sugedit').on('change click', function(){ var freq_obs = parseInt($(this).val()); - var freq = parseInt($('.in input[name=\'downlink_low\']').val()); + var freq = parseInt($('input[name=\'downlink_low\']:visible').val()); $('.downlink-ppb-sugedit').val(freq_to_ppb(freq_obs,freq)); }); @@ -148,4 +145,39 @@ $(document).ready(function() { var el = $(this); copyToClipboard(text, el); }); + + // Update Satellite + $('.bs-modal').each(function () { + $(this).modalForm({ + formURL: $(this).data('form-url') + }); + }); + + // Update Transmitter + $('.update-transmitter-link').each(function () { + $(this).modalForm({ + formURL: $(this).data('form-url'), + modalID: '#update-transmitter-modal' + }); + }); + + // New transmitter links + $('.create-transmitter-link').each(function () { + $(this).modalForm({ + formURL: $(this).data('form-url'), + modalID: '#create-transmitter-modal' + }); + }); + + // Ask for help in a toast if this Satellite object is flagged as in need + if ($('#satellite_name').data('needshelp') == 'True') { + $(document).Toasts('create', { + title: 'Please Help!', + class: 'alert-warning', + autohide: true, + delay: 6000, + icon: 'fas fa-hand-holding-medical', + body: 'This Satellite needs editing. Contact us to become an editor.' + }); + } }); diff --git a/db/static/js/satellites.js b/db/static/js/satellites.js new file mode 100644 index 0000000..f1d2d87 --- /dev/null +++ b/db/static/js/satellites.js @@ -0,0 +1,42 @@ +/* eslint new-cap: "off" */ +$(document).ready(function() { + $('#sats').DataTable( { + // the dom field controls the layout and visibility of datatable items + // and is not intuitive at all. Without layout we have dom: 'Bftrilp' + // https://datatables.net/reference/option/dom + dom: '<"row"<"d-none d-md-block col-md-6"B><"col-sm-12 col-md-6"f>>' + + '<"row"<"col-sm-12"tr>>' + + '<"row"<"col-sm-12 col-xl-3"i><"col-sm-12 col-md-6 col-xl-3"l><"col-sm-12 col-md-6 col-xl-6"p>>', + buttons: [ + 'colvis' + ], + responsive: { + details: { + display: $.fn.dataTable.Responsive.display.childRow, + type: 'column' + } + }, + columnDefs: [ + { + className: 'control', + orderable: false, + targets: 0 + }, + ], + language: { + search: 'Filter:', + buttons: { + colvis: 'Columns', + } + }, + order: [ 1, 'asc' ], + pageLength: 25 + } ); + + // Update Satellite + $('.bs-modal').each(function () { + $(this).modalForm({ + formURL: $(this).data('form-url') + }); + }); +} ); \ No newline at end of file diff --git a/db/static/js/stats.js b/db/static/js/stats.js index 9a45750..fd0873c 100644 --- a/db/static/js/stats.js +++ b/db/static/js/stats.js @@ -75,30 +75,40 @@ $(document).ready(function() { $('#transmitters-numbers').hide(); } else { var i; - var r; - var g; - var b; - var a; + var h; + var s; + var l; var color; + var mode_total = 0; + var band_total = 0; // Create colors for Mode Chart var mode_colors = []; for (i = 0; i < data.mode_label.length; i++) { - r = Math.floor(data.mode_data[i]* 10); - b = Math.floor(0.3 * 255); - g = Math.floor(data.mode_data[i]* 10); - a = 0.5; - color = 'rgba(' + r + ',' + g + ',' + b + ',' + a + ')'; + mode_total += data.mode_data[i]; + } + for (i = 0; i < data.band_label.length; i++) { + band_total += data.band_data[i]; + } + for (i = 0; i < data.mode_label.length; i++) { + // Switching to HSL to stick with hue of LSF logo + h = 235; + l = data.mode_data[i]/mode_total*100; + l *= 3; // adjust for better visibility + s = data.mode_data[i]/mode_total*100; + s *= 3; // adjust for better visibility + color = 'hsl(' + h + ',' + Math.floor(s) + '%,' + Math.floor(l) + '%)'; mode_colors.push(color); } // Create colors for Band Chart var band_colors = []; for (i = 0; i < data.band_label.length; i++) { - b = Math.floor(0.1 * 255); - g = Math.floor(data.band_data[i]); - r = Math.floor(data.band_data[i]); - a = 0.5; - color = 'rgba(' + r + ',' + g + ',' + b + ',' + a + ')'; + h = 235; + l = data.band_data[i]/band_total*100; + l *= 1.25; // adjust for better visibility + s = data.band_data[i]/band_total*100; + s *= 1.25; // adjust for better visibility + color = 'hsl(' + h + ',' + s + '%,' + l + '%)'; band_colors.push(color); } diff --git a/db/static/js/transmitters.js b/db/static/js/transmitters.js index e20bc10..5973707 100644 --- a/db/static/js/transmitters.js +++ b/db/static/js/transmitters.js @@ -3,23 +3,65 @@ function ppb_to_freq(freq, drift) { return Math.round(freq_obs); } -/* eslint-disable no-unused-vars */ -/* Disabling eslint for the following function since it is called by the Bootstrap Table library */ -function freqSorter(a, b) { - var aa = a.split(' ', 1); - var bb = b.split(' ', 1); - return aa - bb; +function format_freq(frequency) { + if (isNaN(frequency) || frequency == ''){ + return 'None'; + } else if (frequency < 1000) { + // Frequency is in Hz range + return frequency.toFixed(3) + ' Hz'; + } else if (frequency < 1000000) { + return (frequency/1000).toFixed(3) + ' kHz'; + } else { + return (frequency/1000000).toFixed(3) + ' MHz'; + } } /* eslint-enable no-unused-vars */ +/* eslint new-cap: "off" */ $(document).ready(function() { - $('#transmitters-table').bootstrapTable(); - $('#transmitters-table').css('opacity','1'); - // Calculate the drifted frequencies $('.drifted').each(function() { var drifted = ppb_to_freq($(this).data('freq_or'),$(this).data('drift')); $(this).html(drifted); }); -}); + + // Format all frequencies + $('.frequency').each(function() { + var to_format = $(this).html(); + $(this).html(format_freq(to_format)); + }); + + $('#transmitters').DataTable( { + // the dom field controls the layout and visibility of datatable items + // and is not intuitive at all. Without layout we have dom: 'Bftrilp' + // https://datatables.net/reference/option/dom + dom: '<"row"<"d-none d-md-block col-md-6"B><"col-sm-12 col-md-6"f>>' + + '<"row"<"col-sm-12"tr>>' + + '<"row"<"col-sm-12 col-xl-3"i><"col-sm-12 col-md-6 col-xl-3"l><"col-sm-12 col-md-6 col-xl-6"p>>', + buttons: [ + 'colvis' + ], + responsive: { + details: { + display: $.fn.dataTable.Responsive.display.childRow, + type: 'column' + } + }, + columnDefs: [ + { + className: 'control', + orderable: false, + targets: 0 + }, + ], + language: { + search: 'Filter:', + buttons: { + colvis: 'Columns', + } + }, + order: [ 1, 'asc' ], + pageLength: 25 + } ); +} ); diff --git a/db/templates/404.html b/db/templates/404.html index 27c509a..e227b1a 100644 --- a/db/templates/404.html +++ b/db/templates/404.html @@ -1,5 +1,5 @@ {% extends "base.html" %} -{% load staticfiles %} +{% load static %} {% block title %} - Page Not found{% endblock %} diff --git a/db/templates/500.html b/db/templates/500.html index b39b28b..a8c4abb 100644 --- a/db/templates/500.html +++ b/db/templates/500.html @@ -1,5 +1,5 @@ {% extends "base.html" %} -{% load staticfiles %} +{% load static %} {% block title %} - Server Error{% endblock %} diff --git a/db/templates/account/login.html b/db/templates/account/login.html index 09b72e3..f18e6de 100644 --- a/db/templates/account/login.html +++ b/db/templates/account/login.html @@ -33,7 +33,7 @@ {% endif %} Forgot Password? - + diff --git a/db/templates/avatar/avatar_tag.html b/db/templates/avatar/avatar_tag.html index cd7efde..ff9f672 100644 --- a/db/templates/avatar/avatar_tag.html +++ b/db/templates/avatar/avatar_tag.html @@ -1 +1,2 @@ - \ No newline at end of file + +User Image diff --git a/db/templates/base.html b/db/templates/base.html index 0875d75..dc6b1e0 100644 --- a/db/templates/base.html +++ b/db/templates/base.html @@ -1,129 +1,248 @@ -{% load staticfiles %} +{% load static %} {% load avatar_tags %} {% load tags %} {% load compress %} +{% load fontawesome_5 %} - - - SatNOGS DB{% block title %}{% endblock title %} - - {% compress css %} - - - {% block css %}{% endblock %} - {% endcompress %} - + + + SatNOGS DB{% block title %}{% endblock title %} + + {% compress css %} + - + + {% block css %}{% endblock %} + {% endcompress %} + {% fontawesome_5_static %} - - {{ stage_notice }} + -
- + + +
+ + + -
+ + + + +
{% if messages %} -
-
- {% for notification in messages %} - - {% endfor %} -
+ + {% for notification in messages %} + + + {% endfor %} {% endif %} {% block top %}{% endblock %} {% block content %}{% endblock content %} -
- - +
+
+ - {% compress js %} - - - - - {% block javascript %}{% endblock javascript %} - {{ analytics_code }} - {% endcompress %} + {% compress js %} + + + + + + + + {% block javascript %}{% endblock javascript %} + {{ analytics_code }} + {% endcompress %} - + + \ No newline at end of file diff --git a/db/templates/base/about.html b/db/templates/base/about.html index db8f19b..8b3dc44 100644 --- a/db/templates/base/about.html +++ b/db/templates/base/about.html @@ -2,24 +2,75 @@ {% block title %} - About{% endblock %} +{% block top-menu-left %} +
About
+{% endblock %} + +{% block top-menu-right %} + +