Skip to content

Commit

Permalink
Merge branch 'main' into add_dithering
Browse files Browse the repository at this point in the history
  • Loading branch information
jnation3406 committed Jul 14, 2021
2 parents 1df266f + fd66e4f commit 175dcf4
Show file tree
Hide file tree
Showing 14 changed files with 296 additions and 7 deletions.
12 changes: 12 additions & 0 deletions observation_portal/common/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
"""
utils.py - Common utility functions
"""

def get_queryset_field_values(queryset, field):
"""Get all the values for a field in a given queryset"""
all_values = queryset.values_list(field, flat=True)
values_set = set()
for values in all_values:
if values:
values_set.update(values)
return values_set
22 changes: 20 additions & 2 deletions observation_portal/proposals/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from django.contrib import admin

from observation_portal.proposals.forms import TimeAllocationForm, TimeAllocationFormSet, CollaborationAllocationForm

from observation_portal.common.utils import get_queryset_field_values
from observation_portal.proposals.models import (
Semester,
ScienceCollaborationAllocation,
Expand Down Expand Up @@ -45,6 +45,23 @@ class TimeAllocationAdminInline(admin.TabularInline):
extra = 0


class ProposalTagListFilter(admin.SimpleListFilter):
"""Filter proposals given a proposal tag"""
title = 'Tag'
parameter_name = 'tag'

def lookups(self, request, model_admin):
proposal_tags = get_queryset_field_values(Proposal.objects.all(), 'tags')
return ((tag, tag) for tag in proposal_tags)

def queryset(self, request, queryset):
value = self.value()
if value:
return queryset.filter(tags__contains=[value])
else:
return queryset


class ProposalAdmin(admin.ModelAdmin):
list_display = (
'id',
Expand All @@ -55,10 +72,11 @@ class ProposalAdmin(admin.ModelAdmin):
'public',
'semesters',
'pi',
'tags',
'created',
'modified'
)
list_filter = ('active', 'sca', 'public')
list_filter = ('active', ProposalTagListFilter, 'sca', 'public')
raw_id_fields = ('users',)
inlines = [TimeAllocationAdminInline]
search_fields = ['id', 'title', 'abstract']
Expand Down
5 changes: 5 additions & 0 deletions observation_portal/proposals/filters.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import django_filters
from django.utils import timezone
from dateutil.parser import parse

from observation_portal.proposals.models import Semester, Proposal, Membership, ProposalInvite


Expand All @@ -20,11 +21,15 @@ class ProposalFilter(django_filters.FilterSet):
label="Semester", distinct=True, queryset=Semester.objects.all().order_by('-start')
)
active = django_filters.ChoiceFilter(choices=((False, 'Inactive'), (True, 'Active')), empty_label='All')
tags = django_filters.BaseInFilter(method='filter_has_tag', label='Comma separated list of tags')

class Meta:
model = Proposal
fields = ('active', 'semester', 'id', 'tac_rank', 'tac_priority', 'public', 'title')

def filter_has_tag(self, queryset, name, value):
return queryset.filter(tags__overlap=[value])


class SemesterFilter(django_filters.FilterSet):
semester_contains = django_filters.CharFilter(method='semester_contains_filter', label='Contains Date')
Expand Down
24 changes: 24 additions & 0 deletions observation_portal/proposals/migrations/0008_auto_20210708_1623.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Generated by Django 2.2.23 on 2021-07-08 16:23

import django.contrib.postgres.fields
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('proposals', '0007_remove_timeallocation_instrument_type'),
]

operations = [
migrations.AddField(
model_name='proposal',
name='tags',
field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=255), blank=True, default=list, help_text='List of strings tagging this proposal', size=None),
),
migrations.AlterField(
model_name='timeallocation',
name='instrument_types',
field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=200), default=list, help_text='One or more instrument_types to share this time allocation', size=None),
),
]
3 changes: 2 additions & 1 deletion observation_portal/proposals/models.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from django.contrib.postgres.fields import ArrayField
from django.contrib.auth.models import User
from django.contrib.postgres.fields import ArrayField
from django.utils.functional import cached_property
from django.forms import model_to_dict
from django.db import models
Expand Down Expand Up @@ -133,6 +133,7 @@ class Proposal(models.Model):
non_science = models.BooleanField(default=False)
direct_submission = models.BooleanField(default=False)
users = models.ManyToManyField(User, through='Membership')
tags = ArrayField(models.CharField(max_length=255), default=list, blank=True, help_text='List of strings tagging this proposal')

# Admin only notes
notes = models.TextField(blank=True, default='', help_text='Add notes here. Not visible to users.')
Expand Down
48 changes: 47 additions & 1 deletion observation_portal/proposals/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
class TestProposalApiList(APITestCase):
def setUp(self):
self.user = blend_user()
self.proposals = mixer.cycle(3).blend(Proposal)
self.proposals = mixer.cycle(3).blend(Proposal, tags=(t for t in [[], ['planets'], ['student', 'supernovae']]))
mixer.cycle(3).blend(Membership, user=self.user, proposal=(p for p in self.proposals))

def test_no_auth(self):
Expand Down Expand Up @@ -49,6 +49,52 @@ def test_staff_with_staff_view_set_can_view_all_proposals(self):
for p in self.proposals:
self.assertContains(response, p.id)

def test_filter_for_tags(self):
self.client.force_login(self.user)
# Filter for proposals with the 'planets' tag
response = self.client.get(reverse('api:proposals-list') + '?tags=planets')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()['count'], 1)
self.assertEqual(response.json()['results'][0]['tags'], ['planets'])
# Filter for proposals with either the 'planets' tag or the 'student' tag
response = self.client.get(reverse('api:proposals-list') + '?tags=planets,student')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()['count'], 2)
response_tags = []
for result in response.json()['results']:
response_tags.extend(result['tags'])
self.assertTrue('planets' in response_tags)
self.assertTrue('student' in response_tags)
# Filter for two tags but where both come from a single proposal
response = self.client.get(reverse('api:proposals-list') + '?tags=supernovae,student')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()['count'], 1)
self.assertTrue('supernovae' in response.json()['results'][0]['tags'])
self.assertTrue('student' in response.json()['results'][0]['tags'])
# Get all tags
response = self.client.get(reverse('api:proposals-list'))
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()['count'], 3)

def test_normal_user_sees_only_their_tags(self):
mixer.blend(Proposal, tags=['secret'])
self.client.force_login(self.user)
response = self.client.get(reverse('api:proposals-tags'))
expected_tags = ['student', 'planets', 'supernovae']
self.assertEqual(len(response.json()), len(expected_tags))
for expected_tag in expected_tags:
self.assertTrue(expected_tag in response.json())

def test_staff_user_with_staff_view_sees_all_tags(self):
mixer.blend(Proposal, tags=['secret'])
admin_user = blend_user(user_params={'is_staff': True}, profile_params={'staff_view': True})
self.client.force_login(admin_user)
response = self.client.get(reverse('api:proposals-tags'))
expected_tags = ['student', 'planets', 'supernovae', 'secret']
self.assertEqual(len(response.json()), len(expected_tags))
for expected_tag in expected_tags:
self.assertTrue(expected_tag in response.json())


class TestProposalApiDetail(APITestCase):
def setUp(self):
Expand Down
6 changes: 6 additions & 0 deletions observation_portal/proposals/viewsets.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

from observation_portal.accounts.permissions import IsPrincipleInvestigator
from observation_portal.common.mixins import ListAsDictMixin, DetailAsDictMixin
from observation_portal.common.utils import get_queryset_field_values
from observation_portal.proposals.filters import SemesterFilter, ProposalFilter, MembershipFilter, ProposalInviteFilter
from observation_portal.proposals.models import Proposal, Semester, ProposalNotification, Membership, ProposalInvite

Expand Down Expand Up @@ -67,6 +68,11 @@ def globallimit(self, request, pk=None):
else:
return Response({'errors': serializer.errors}, status=400)

@action(detail=False, methods=['get'])
def tags(self, request, pk=None):
proposal_tags = get_queryset_field_values(self.get_queryset(), 'tags')
return Response(list(proposal_tags))


class SemesterViewSet(viewsets.ReadOnlyModelViewSet):
permission_classes = (AllowAny,)
Expand Down
21 changes: 20 additions & 1 deletion observation_portal/sciapplications/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
NoTimeAllocatedError, MultipleTimesAllocatedError
)
from observation_portal.proposals.models import Proposal
from observation_portal.common.utils import get_queryset_field_values


class InstrumentAdmin(admin.ModelAdmin):
Expand Down Expand Up @@ -39,6 +40,23 @@ class CoInvestigatorInline(admin.TabularInline):
model = CoInvestigator


class ScienceApplicationTagListFilter(admin.SimpleListFilter):
"""Filter science applications given a tag"""
title = 'Tag'
parameter_name = 'tag'

def lookups(self, request, model_admin):
sciapp_tags = get_queryset_field_values(ScienceApplication.objects.all(), 'tags')
return ((tag, tag) for tag in sciapp_tags)

def queryset(self, request, queryset):
value = self.value()
if value:
return queryset.filter(tags__contains=[value])
else:
return queryset


class ScienceApplicationAdmin(admin.ModelAdmin):
inlines = [CoInvestigatorInline, TimeRequestAdminInline]
list_display = (
Expand All @@ -47,9 +65,10 @@ class ScienceApplicationAdmin(admin.ModelAdmin):
'status',
'submitter',
'tac_rank',
'tags',
'preview_link',
)
list_filter = ('call', 'status', 'call__proposal_type')
list_filter = (ScienceApplicationTagListFilter, 'call', 'status', 'call__proposal_type')
actions = ['accept', 'reject', 'port']
search_fields = ['title', 'abstract', 'submitter__first_name', 'submitter__last_name', 'submitter__username']

Expand Down
4 changes: 4 additions & 0 deletions observation_portal/sciapplications/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ class ScienceApplicationFilter(django_filters.FilterSet):
only_authored = django_filters.BooleanFilter(
method='filter_only_authored'
)
tags = django_filters.BaseInFilter(method='filter_has_tag', label='Comma separated list of tags')

class Meta:
model = ScienceApplication
Expand All @@ -31,6 +32,9 @@ def filter_only_authored(self, queryset, name, value):
else:
return queryset

def filter_has_tag(self, queryset, name, value):
return queryset.filter(tags__overlap=[value])


class CallFilter(django_filters.FilterSet):
only_open = django_filters.BooleanFilter(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 2.2.23 on 2021-07-08 16:23

import django.contrib.postgres.fields
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('sciapplications', '0004_remove_timerequest_instrument'),
]

operations = [
migrations.AlterModelOptions(
name='timerequest',
options={'ordering': ('semester',)},
),
migrations.AddField(
model_name='scienceapplication',
name='tags',
field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=255), blank=True, default=list, help_text='List of strings tagging this application', size=None),
),
]
5 changes: 4 additions & 1 deletion observation_portal/sciapplications/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from django.contrib.auth.models import User
from django.urls import reverse
from django.utils.translation import ugettext as _
from django.contrib.postgres.fields import ArrayField
from django.template.loader import render_to_string
from django.utils.functional import cached_property
from django.conf import settings
Expand Down Expand Up @@ -120,6 +121,7 @@ class ScienceApplication(models.Model):
tac_rank = models.PositiveIntegerField(default=0)
tac_priority = models.PositiveIntegerField(default=0)
pdf = models.FileField(upload_to=pdf_upload_path, blank=True, null=True)
tags = ArrayField(models.CharField(max_length=255), default=list, blank=True, help_text='List of strings tagging this application')

# Admin only Notes
notes = models.TextField(blank=True, default='', help_text='Add notes here. Not visible to users.')
Expand Down Expand Up @@ -191,7 +193,8 @@ def convert_to_proposal(self):
tac_priority=self.tac_priority,
tac_rank=self.tac_rank,
active=False,
sca=self.sca
sca=self.sca,
tags=self.tags
)

for tr in self.timerequest_set.filter(approved=True):
Expand Down
5 changes: 4 additions & 1 deletion observation_portal/sciapplications/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ class ScienceApplicationSerializer(serializers.ModelSerializer):
timerequest_set = TimeRequestSerializer(many=True, required=False)
pdf = serializers.FileField(required=False)
clear_pdf = serializers.BooleanField(required=False, default=False, write_only=True)
tags = serializers.ListField(child=serializers.CharField(max_length=255), allow_empty=True, required=False)
call = serializers.SerializerMethodField()
sca = serializers.SerializerMethodField()
submitter = serializers.SerializerMethodField()
Expand All @@ -108,7 +109,7 @@ class Meta:
model = ScienceApplication
fields = (
'id', 'title', 'abstract', 'status', 'tac_rank', 'call', 'call_id', 'pi',
'pi_first_name', 'pi_last_name', 'pi_institution', 'timerequest_set',
'pi_first_name', 'pi_last_name', 'pi_institution', 'timerequest_set', 'tags',
'coinvestigator_set', 'pdf', 'clear_pdf', 'sca', 'submitter', 'submitted'
)
read_only_fields = ('submitted', )
Expand Down Expand Up @@ -262,8 +263,10 @@ def update(self, instance, validated_data):
clear_pdf = validated_data.pop('clear_pdf', False)
timerequest_set = validated_data.pop('timerequest_set', [])
coinvestigator_set = validated_data.pop('coinvestigator_set', [])
tags = validated_data.pop('tags', [])

with transaction.atomic():
instance.tags = tags
for field, value in validated_data.items():
setattr(instance, field, value)

Expand Down
Loading

0 comments on commit 175dcf4

Please sign in to comment.