1
0
Fork 0

Add reviewer and reviewed fields in TransmitterEntry

Signed-off-by: Alfredos-Panagiotis Damkalis <fredy@fredy.gr>
spacecruft
Alfredos-Panagiotis Damkalis 2021-03-10 22:54:44 +02:00
parent 2805acde27
commit a97f9666db
11 changed files with 1174 additions and 96 deletions

View File

@ -132,7 +132,7 @@ class TransmitterSerializer(serializers.ModelSerializer):
mode_id = serializers.SerializerMethodField()
uplink_mode = serializers.SerializerMethodField()
alive = serializers.SerializerMethodField()
updated = serializers.DateTimeField(source='created')
updated = serializers.DateTimeField(source='reviewed')
class Meta:
model = Transmitter

View File

@ -1,5 +1,4 @@
"""Defines functions and settings for the django admin interface"""
from datetime import datetime
from socket import error as socket_error
from django.conf.urls import url
@ -7,6 +6,7 @@ from django.contrib import admin, messages
from django.http import HttpResponseRedirect
from django.shortcuts import redirect
from django.urls import reverse
from django.utils.timezone import now
from db.base.models import Artifact, DemodData, ExportedFrameset, LatestTleSet, Mode, Operator, \
Satellite, Telemetry, Tle, Transmitter, TransmitterEntry, TransmitterSuggestion
@ -95,9 +95,10 @@ class SatelliteAdmin(admin.ModelAdmin):
class TransmitterEntryAdmin(admin.ModelAdmin):
"""Defines TransmitterEntry view in django admin UI"""
list_display = (
'uuid', 'description', 'satellite', 'service', 'type', 'downlink_mode', 'uplink_mode',
'baud', 'downlink_low', 'downlink_high', 'downlink_drift', 'uplink_low', 'uplink_high',
'uplink_drift', 'is_reviewed', 'approved', 'status', 'created', 'citation', 'created_by'
'id', 'uuid', 'description', 'satellite', 'service', 'type', 'downlink_mode',
'uplink_mode', 'baud', 'downlink_low', 'downlink_high', 'downlink_drift', 'uplink_low',
'uplink_high', 'uplink_drift', 'citation', 'is_reviewed', 'approved', 'status', 'created',
'created_by', 'reviewed', 'reviewer'
)
search_fields = ('satellite__id', 'uuid', 'satellite__name', 'satellite__norad_cat_id')
list_filter = (
@ -110,7 +111,6 @@ class TransmitterEntryAdmin(admin.ModelAdmin):
'uplink_mode',
'baud',
)
readonly_fields = ('uuid', 'satellite')
@admin.register(TransmitterSuggestion)
@ -119,7 +119,7 @@ class TransmitterSuggestionAdmin(admin.ModelAdmin):
list_display = (
'uuid', 'description', 'satellite', 'service', 'type', 'downlink_mode', 'uplink_mode',
'baud', 'downlink_low', 'downlink_high', 'downlink_drift', 'uplink_low', 'uplink_high',
'uplink_drift', 'status', 'created', 'citation', 'created_by'
'uplink_drift', 'citation', 'status', 'created', 'created_by'
)
search_fields = ('satellite__id', 'uuid', 'satellite__name', 'satellite__norad_cat_id')
list_filter = (
@ -132,8 +132,8 @@ class TransmitterSuggestionAdmin(admin.ModelAdmin):
readonly_fields = (
'uuid', 'description', 'status', 'type', 'uplink_low', 'uplink_high', 'uplink_drift',
'downlink_low', 'downlink_high', 'downlink_drift', 'downlink_mode', 'uplink_mode',
'invert', 'baud', 'satellite', 'is_reviewed', 'approved', 'created', 'citation',
'created_by', 'service'
'invert', 'baud', 'satellite', 'is_reviewed', 'reviewer', 'reviewed', 'approved',
'created', 'created_by', 'citation', 'service', 'coordination', 'coordination_url'
)
actions = ['approve_suggestion', 'reject_suggestion']
@ -160,12 +160,9 @@ class TransmitterSuggestionAdmin(admin.ModelAdmin):
for entry in queryset:
entry.approved = True
entry.is_reviewed = True
entry.created = datetime.utcnow()
entry.created_by = request.user # change to reviewer
entry.reviewed = now()
entry.reviewer = request.user
entry.save()
# After creating the new approved entries, we update the suggestion entries as reviewed
# Note that queryset.update doesn't use model's save() that creates new entries
queryset.update(is_reviewed=True, approved=True)
if queryset_size == 1:
self.message_user(request, "Transmitter suggestion was successfully approved")
else:
@ -181,14 +178,11 @@ class TransmitterSuggestionAdmin(admin.ModelAdmin):
"""
queryset_size = len(queryset)
for entry in queryset:
entry.created = datetime.utcnow()
entry.created_by = request.user # change to reviewer
entry.approved = False
entry.is_reviewed = True
entry.reviewed = now()
entry.reviewer = request.user
entry.save()
# After creating the new approved entries, we update the suggestion entries as reviewed
# Note that queryset.update doesn't use model's save() that creates new entries
queryset.update(is_reviewed=True, approved=False)
if queryset_size == 1:
self.message_user(request, "Transmitter suggestion was successfully rejected")
else:
@ -201,14 +195,15 @@ class TransmitterSuggestionAdmin(admin.ModelAdmin):
class TransmitterAdmin(admin.ModelAdmin):
"""Defines Transmitter view in django admin UI"""
list_display = (
'uuid', 'description', 'satellite', 'service', 'type', 'downlink_mode', 'uplink_mode',
'baud', 'downlink_low', 'downlink_high', 'downlink_drift', 'uplink_low', 'uplink_high',
'uplink_drift', 'status', 'created', 'citation', 'created_by'
)
search_fields = (
'satellite__id', 'uuid', 'description', 'satellite__name', 'satellite__norad_cat_id'
'id', 'uuid', 'description', 'satellite', 'service', 'type', 'downlink_mode',
'uplink_mode', 'baud', 'downlink_low', 'downlink_high', 'downlink_drift', 'uplink_low',
'uplink_high', 'uplink_drift', 'citation', 'is_reviewed', 'approved', 'status', 'created',
'created_by', 'reviewed', 'reviewer'
)
search_fields = ('satellite__id', 'uuid', 'satellite__name', 'satellite__norad_cat_id')
list_filter = (
'is_reviewed',
'approved',
'type',
'status',
'service',
@ -216,7 +211,12 @@ class TransmitterAdmin(admin.ModelAdmin):
'uplink_mode',
'baud',
)
readonly_fields = ('uuid', 'satellite')
readonly_fields = (
'uuid', 'description', 'status', 'type', 'uplink_low', 'uplink_high', 'uplink_drift',
'downlink_low', 'downlink_high', 'downlink_drift', 'downlink_mode', 'uplink_mode',
'invert', 'baud', 'satellite', 'is_reviewed', 'reviewer', 'reviewed', 'approved',
'created', 'created_by', 'citation', 'service', 'coordination', 'coordination_url'
)
@admin.register(Tle)

File diff suppressed because it is too large Load Diff

View File

@ -45,17 +45,15 @@ class TransmitterUpdateForm(BSModalModelForm): # pylint: disable=too-many-ances
fields = [
'description', 'type', 'status', 'uplink_low', 'uplink_high', 'uplink_drift',
'uplink_mode', 'downlink_low', 'downlink_high', 'downlink_drift', 'downlink_mode',
'invert', 'baud', 'citation', 'service', 'coordination', 'coordination_url', 'created'
'invert', 'baud', 'citation', 'service', 'coordination', 'coordination_url'
]
labels = {
'downlink_low': _('Downlink freq.'),
'uplink_low': _('Uplink freq.'),
'invert': _('Inverted Transponder?'),
'created': _('Updated'),
}
widgets = {
'description': TextInput(),
'created': TextInput(attrs={'readonly': True}),
}

View File

@ -0,0 +1,86 @@
# Generated by Django 3.1.5 on 2021-03-08 22:59
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
def add_review_details(apps, schema_editor):
TransmitterEntry = apps.get_model('base', 'TransmitterEntry')
orphans = TransmitterEntry.objects.filter(created_by__isnull=True, is_reviewed=True)
for entry in orphans:
entry.reviewed = entry.created
entry.save()
ids = []
non_orphans = TransmitterEntry.objects.filter(created_by__isnull=False, is_reviewed=True).order_by('created')
for entry in non_orphans:
if entry.id in ids:
entry.delete()
continue
next_entries = TransmitterEntry.objects.filter(created_by__isnull=False, is_reviewed=True).filter(uuid=entry.uuid).filter(created__gt=entry.created).order_by('created')
for next_entry in next_entries:
if (entry.uuid == next_entry.uuid and entry.description == next_entry.description and
entry.status == next_entry.status and entry.type == next_entry.type and entry.uplink_low == next_entry.uplink_low and
entry.uplink_high == next_entry.uplink_high and entry.uplink_drift == next_entry.uplink_drift and
entry.downlink_low == next_entry.downlink_low and entry.downlink_high == next_entry.downlink_high and
entry.downlink_drift == next_entry.downlink_drift and entry.downlink_mode == next_entry.downlink_mode and
entry.uplink_mode == next_entry.uplink_mode and entry.invert == next_entry.invert and entry.baud == next_entry.baud and
entry.satellite == next_entry.satellite and entry.reviewed == next_entry.reviewed and entry.approved == next_entry.approved and
entry.citation == next_entry.citation and entry.service == next_entry.service and entry.coordination == next_entry.coordination and
entry.coordination_url == next_entry.coordination_url):
ids.append(next_entry.id)
entry.reviewed = next_entry.created
entry.reviewer = next_entry.created_by
entry.save()
break
else:
entry.reviewed = entry.created
entry.reviewer = entry.created_by
entry.save()
def remove_review_details(apps, schema_editor):
TransmitterEntry = apps.get_model('base', 'TransmitterEntry')
non_orphans = TransmitterEntry.objects.filter(created_by__isnull=False, is_reviewed=True).order_by('created')
for entry in non_orphans:
entry.pk = None
entry.created = entry.reviewed
entry.created_by = entry.reviewer
entry.save()
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('base', '0036_rename_reviewed_field_on_transmitter_entry_model'),
]
operations = [
migrations.AddField(
model_name='transmitterentry',
name='reviewed',
field=models.DateTimeField(blank=True, help_text='Timestamp of review', null=True),
),
migrations.AddField(
model_name='transmitterentry',
name='reviewer',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='reviewed_transmitters', to=settings.AUTH_USER_MODEL),
),
migrations.AlterField(
model_name='transmitterentry',
name='created',
field=models.DateTimeField(default=django.utils.timezone.now, help_text='Timestamp of creation/edit'),
),
migrations.AlterField(
model_name='transmitterentry',
name='created_by',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_transmitters', to=settings.AUTH_USER_MODEL),
),
migrations.AlterUniqueTogether(
name='transmitterentry',
unique_together={('uuid', 'reviewed')},
),
migrations.RunPython(add_review_details, remove_review_details),
]

View File

@ -341,15 +341,11 @@ class TransmitterEntry(models.Model):
satellite = models.ForeignKey(
Satellite, null=True, related_name='transmitter_entries', on_delete=models.SET_NULL
)
is_reviewed = models.BooleanField(default=False)
approved = models.BooleanField(default=False)
created = models.DateTimeField(default=now, help_text='Timestamp for this entry or edit')
citation = models.CharField(
max_length=512,
default='CITATION NEEDED - https://xkcd.com/285/',
help_text='A reference (preferrably URL) for this entry or edit'
)
created_by = models.ForeignKey(get_user_model(), null=True, on_delete=models.SET_NULL)
service = models.CharField(
choices=zip(SERVICE_TYPE, SERVICE_TYPE),
max_length=34,
@ -368,6 +364,23 @@ class TransmitterEntry(models.Model):
help_text='URL for more details on this frequency coordination',
validators=[URLValidator(schemes=['http', 'https'], regex=URL_REGEX)]
)
is_reviewed = models.BooleanField(default=False)
reviewer = models.ForeignKey(
get_user_model(),
related_name='reviewed_transmitters',
blank=True,
null=True,
on_delete=models.SET_NULL
)
reviewed = models.DateTimeField(blank=True, null=True, help_text='Timestamp of review')
approved = models.BooleanField(default=False)
created = models.DateTimeField(default=now, help_text='Timestamp of creation/edit')
created_by = models.ForeignKey(
get_user_model(),
related_name='created_transmitters',
null=True,
on_delete=models.SET_NULL
)
# NOTE: future fields will need to be added to forms.py and to
# api/serializers.py
@ -384,19 +397,12 @@ class TransmitterEntry(models.Model):
return False
class Meta:
unique_together = ("uuid", "created")
unique_together = ("uuid", "reviewed")
verbose_name_plural = 'Transmitter entries'
def __str__(self):
return self.description
# see https://github.com/PyCQA/pylint-django/issues/94
def save(self, *args, **kwargs): # pylint: disable=W0222
# this assignment is needed to preserve changes made to a Transmitter
# through the admin UI
self.id = None # pylint: disable=C0103, W0201
super().save()
def clean(self):
if self.type == TRANSMITTER_TYPE[0]:
if self.uplink_low is not None or self.uplink_high is not None \
@ -462,10 +468,10 @@ class TransmitterManager(models.Manager): # pylint: disable=R0903
"""
subquery = TransmitterEntry.objects.filter(
is_reviewed=True, approved=True
).filter(uuid=OuterRef('uuid')).order_by('-created')
).filter(uuid=OuterRef('uuid')).order_by('-reviewed')
return super().get_queryset().filter(
is_reviewed=True, approved=True
).filter(created=Subquery(subquery.values('created')[:1]))
).filter(reviewed=Subquery(subquery.values('reviewed')[:1]))
class Transmitter(TransmitterEntry):

View File

@ -83,9 +83,11 @@ class TransmitterFactory(factory.django.DjangoModelFactory):
satellite = factory.SubFactory(SatelliteFactory)
is_reviewed = True
approved = True
created = fuzzy.FuzzyDateTime(now() - timedelta(days=30), now())
created = fuzzy.FuzzyDateTime(now() - timedelta(days=30), now() - timedelta(hours=10))
reviewed = fuzzy.FuzzyDateTime(now() - timedelta(hours=10), now())
citation = fuzzy.FuzzyText()
created_by = factory.SubFactory(UserFactory)
reviewer = factory.SubFactory(UserFactory)
class Meta:
model = Transmitter

View File

@ -6,7 +6,7 @@ from bootstrap_modal_forms.generic import BSModalCreateView, BSModalUpdateView
from django.conf import settings
from django.contrib import messages
from django.contrib.auth import get_user_model
from django.contrib.auth.decorators import login_required, user_passes_test
from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
from django.core.cache import cache
from django.core.exceptions import ObjectDoesNotExist
@ -16,7 +16,7 @@ from django.db.models import Count, Max, Prefetch, Q
from django.http import HttpResponse, HttpResponseServerError, JsonResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from django.utils import timezone
from django.utils.timezone import now
from django.views.decorators.http import require_POST
from db.base.forms import SatelliteModelForm, TransmitterModelForm, TransmitterUpdateForm
@ -29,12 +29,6 @@ from db.base.utils import cache_statistics, millify, read_influx
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.
@ -55,7 +49,7 @@ def home(request):
# Calculate latest contributors
latest_data_satellites = []
found = False
date_from = timezone.now() - timedelta(days=1)
date_from = now() - timedelta(days=1)
data_list = DemodData.objects.filter(timestamp__gte=date_from).order_by('-pk')
paginator = Paginator(data_list, 50)
page = paginator.page(1)
@ -77,7 +71,7 @@ def home(request):
).prefetch_related(prefetch_approved, prefetch_suggested)
# Calculate latest contributors
date_from = timezone.now() - timedelta(days=1)
date_from = now() - timedelta(days=1)
latest_submitters = DemodData.objects.filter(timestamp__gte=date_from
).values('station').annotate(c=Count('station')
).order_by('-c')
@ -132,7 +126,7 @@ def satellite(request, norad):
:returns: base/satellite.html
"""
satellite_obj = get_object_or_404(Satellite.objects, norad_cat_id=norad)
satellite_obj = get_object_or_404(Satellite, norad_cat_id=norad)
latest_tle = None
latest_tle_set = None
@ -199,31 +193,26 @@ def request_export(request, norad, period=None):
@login_required
@require_POST
@user_passes_test(superuser_check)
def transmitter_suggestion_handler(request):
"""Returns the Satellite page after approving or rejecting a suggestion
"""Returns the Satellite page after approving or rejecting a suggestion if
user has approve permission.
:returns: Satellite page
"""
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
messages.success(request, ('Transmitter rejected.'))
transmitter.is_reviewed = True
transmitter.created = timezone.now()
transmitter.created_by = request.user # change to reviewer
transmitter = get_object_or_404(TransmitterSuggestion, pk=request.POST['pk'])
if request.user.has_perm('base.approve'):
if 'approve' in request.POST:
transmitter.approved = True
messages.success(request, ('Transmitter approved.'))
elif 'reject' in request.POST:
transmitter.approved = False
messages.success(request, ('Transmitter rejected.'))
transmitter.is_reviewed = True
transmitter.reviewed = now()
transmitter.reviewer = request.user
transmitter.save()
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(
is_reviewed=True, approved=transmitter.approved
)
redirect_page = redirect(
reverse('satellite', kwargs={'norad': transmitter.satellite.norad_cat_id})
)
@ -361,6 +350,7 @@ class TransmitterCreateView(LoginRequiredMixin, BSModalCreateView):
"""
transmitter = form.instance
transmitter.satellite = self.satellite
transmitter.created = now()
transmitter.created_by = self.user
if not self.request.is_ajax():
notify_transmitter_suggestion.delay(transmitter.satellite.id, self.user.id)
@ -380,20 +370,20 @@ class TransmitterUpdateView(LoginRequiredMixin, BSModalUpdateView):
user = get_user_model()
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):
transmitter = form.instance
transmitter.created_by = self.user
# Add update as a new TransmitterEntry object and change fields in order to be a suggestion
transmitter.pk = None
transmitter.reviewed = None
transmitter.reviewer = None
transmitter.is_reviewed = False
transmitter.approved = False
transmitter.created = now()
transmitter.created_by = self.user
if not self.request.is_ajax():
notify_transmitter_suggestion.delay(transmitter.satellite.id, self.user.id)
return super().form_valid(form)

View File

@ -46,11 +46,10 @@
<a class="nav-link" id="transmitters-tab" data-toggle="tab" href="#transmitters" role="tab"
aria-controls="transmitters" aria-selected="false" aria-label="Transmitters">
<i class="nav-icon fas fa-satellite-dish"></i>
{% if satellite.transmitters %}
<span class="badge badge-satnogs-primary navbar-badge">{{ satellite.transmitters.count }}</span>
{% endif %}
{% if request.user.is_superuser and satellite.transmitter_suggestion_count %}
<!-- <span class="badge badge-warning navbar-badge">{{ satellite.transmitter_suggestion_count }}</span> -->
{% if perms.base.approve and satellite.transmitter_suggestion_count %}
<span class="badge badge-warning navbar-badge">{{ satellite.transmitter_suggestion_count }}</span>
{% elif satellite.transmitters %}
<span class="badge badge-satnogs-primary navbar-badge">{{ satellite.transmitters.count }}</span>
{% endif %}
<p class="d-none d-lg-inline-block text-sm">Transmitters</p>
</a>
@ -363,14 +362,11 @@
</div>
</div>
{% endfor %}
<!-- show the pending transmitter suggestions for super users -->
{% if request.user.is_superuser and satellite.transmitter_suggestion_count %}
{% for transmitter in transmitter_suggestions %}
<div class="col-sm-12 col-md-6 col-xl-4 my-2">
{% include 'includes/cards/transmitter_card.html' with satellite=satellite transmitter=transmitter suggestion_card=True %}
</div>
<div class="col-sm-12 col-md-6 col-xl-4 my-2">
{% include 'includes/cards/transmitter_card.html' with satellite=satellite transmitter=transmitter suggestion_card=True %}
</div>
{% endfor %}
{% endif %}
</div>
</div>

View File

@ -9,7 +9,7 @@
</div>
<div class="d-flex ml-auto">
{% if request.user.is_superuser and satellite.suggested_transmitters|length > 0 %}
{% if perms.base.approve and satellite.suggested_transmitters|length > 0 %}
<div class="mx-1">
<span class="badge badge-warning" data-toggle="tooltip"
title="Transmitter Suggestions">{{ satellite.suggested_transmitters|length }}</span>

View File

@ -29,11 +29,11 @@
<button class="btn btn-satnogs dropdown-toggle ml-3" data-toggle="dropdown" aria-expanded="false"
id="dropdown-{{ transmitter.id }}"><i class="fas fa-bars"></i></button>
<div class="dropdown-menu" aria-labelledby="dropdown-{{ transmitter.id }}">
{% if request.user.is_superuser and suggestion_card %}
{% if perms.base.approve and suggestion_card %}
<form action="{% url 'transmitter_suggestion_handler' %}" method="post"
id="transmitter_suggestion_handler-form">
{% csrf_token %}
<input type="hidden" name="uuid" value="{{ transmitter.uuid }}">
<input type="hidden" name="pk" value="{{ transmitter.pk }}">
<button type="submit" name="approve" class="btn btn-satnogs btn-outline-success dropdown-item">
<i class="fas fa-check-square mr-2"></i>
Approve Suggestion
@ -44,7 +44,7 @@
</button>
</form>
{% endif %}
{% if request.user.is_authenticated %}
{% if request.user.is_authenticated and not suggestion_card %}
<a class="dropdown-item update-transmitter-link" data-toggle="modal" href="#"
data-form-url="{% url 'update_transmitter' transmitter.id %}">
<i class="fas fa-edit mr-2"></i>
@ -129,7 +129,17 @@
</dl>
</div>
<div class="card-footer align-items-baseline justify-content-between transmitter-card-footer">
Updated on {{ transmitter.created|date:'Y-m-d H:i' }} by {{ transmitter.created_by }}
{% if suggestion_card %}
Suggested on {{ transmitter.created|date:'Y-m-d H:i' }} by {{ transmitter.created_by }}
{% else %}
Updated on {{ transmitter.reviewed|date:'Y-m-d H:i' }}
{% if transmitter.created_by %}
by {{ transmitter.created_by }}
{% endif %}
{% if transmitter.reviewer %}
and reviewed by {{ transmitter.reviewer }}
{% endif %}
{% endif %}
</div>
</div>
</div>