Skip to content

Commit

Permalink
Merge 502c8c8 into d79dd6d
Browse files Browse the repository at this point in the history
  • Loading branch information
eheinrich committed Sep 20, 2019
2 parents d79dd6d + 502c8c8 commit da256bb
Show file tree
Hide file tree
Showing 10 changed files with 488 additions and 43 deletions.
23 changes: 23 additions & 0 deletions observation_portal/accounts/permissions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from rest_framework.permissions import BasePermission, SAFE_METHODS


class IsAdminOrReadOnly(BasePermission):
"""The request is either read-only, or the user is staff"""
def has_permission(self, request, view):
return bool(
request.method in SAFE_METHODS
or request.user and request.user.is_staff
)


class IsDirectUser(BasePermission):
"""
The user is a member of a proposal that allows direct submission. Users on
proposals that allow direct submission have certain privileges.
"""
def has_permission(self, request, view):
if request.user and request.user.is_authenticated:
direct_proposals = request.user.proposal_set.filter(direct_submission=True)
return len(direct_proposals) > 0
else:
return False
1 change: 1 addition & 0 deletions observation_portal/observations/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

logger = logging.getLogger()


class Observation(models.Model):
STATE_CHOICES = (
('PENDING', 'PENDING'),
Expand Down
23 changes: 20 additions & 3 deletions observation_portal/observations/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,20 @@ class Meta:
fields = ('site', 'enclosure', 'telescope', 'start', 'end', 'priority', 'configuration_statuses', 'request')

def validate(self, data):
user = self.context['request'].user
if self.instance is not None:
# An observation already exists, must be a patch and data won't have the request, so get request like this
proposal = self.instance.request.request_group.proposal
else:
proposal = data['request'].request_group.proposal

# If the user is not staff, check that they are allowed to perform the action
if not user.is_staff and proposal not in user.proposal_set.filter(direct_submission=True):
raise serializers.ValidationError(_(
'Non staff users can only create or update observations on proposals they belong to that '
'allow direct submission'
))

if self.context.get('request').method == 'PATCH':
# For a partial update, only validate that the 'end' field is set, and that it is > now
if 'end' not in data:
Expand All @@ -311,7 +325,8 @@ def validate(self, data):
if not in_a_window:
raise serializers.ValidationError(_(
'The start {} and end {} times do not fall within any window of the request'.format(
data['start'].isoformat(), data['end'].isoformat())
data['start'].isoformat(), data['end'].isoformat()
)
))

# Validate that the site, enclosure, and telescope match the location of the request
Expand Down Expand Up @@ -365,7 +380,9 @@ def update(self, instance, validated_data):
)
num_canceled = Observation.cancel(observations)
logger.info(
f"updated end time for observation {instance.id} to {instance.end}. Canceled {num_canceled} overlapping observations.")
f"updated end time for observation {instance.id} to {instance.end}. "
f"Canceled {num_canceled} overlapping observations."
)
cache.set('observation_portal_last_change_time', timezone.now(), None)

return instance
Expand All @@ -392,6 +409,6 @@ class CancelObservationsSerializer(serializers.Serializer):

def validate(self, data):
if 'ids' not in data and ('start' not in data or 'end' not in data):
raise serializers.ValidationError("Must include either a observation id list or a start and end time")
raise serializers.ValidationError("Must include either an observation id list or a start and end time")

return data
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ <h1>Observation {{ object.id }}</h1>
<tbody>
{% for c_status in object.configuration_statuses.all %}
<tr>
<td><a href="{% url 'api:configurationstatus-detail' c_status.id %}">{{ c_status.id }}</a></td>
<td>{{ c_status.id }}</td>
<td>{{ c_status.instrument_name }}</td>
<td>{{ c_status.guide_camera_name }}</td>
<td style="border-right: 2px solid black">{{ c_status.state }}</td>
Expand Down
Empty file.
130 changes: 130 additions & 0 deletions observation_portal/observations/test/test_views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
from django.test import TestCase
from mixer.backend.django import mixer
from django.urls import reverse

from observation_portal.proposals.models import Proposal, Membership
from observation_portal.observations.models import Observation
from observation_portal.common.test_helpers import create_simple_requestgroup
from observation_portal.accounts.test_utils import blend_user


class TestObservationsDetailView(TestCase):
def setUp(self):
super().setUp()
self.user = blend_user()
self.proposal = mixer.blend(Proposal)
mixer.blend(Membership, proposal=self.proposal, user=self.user)
self.rg = create_simple_requestgroup(self.user, self.proposal)
self.observation = mixer.blend(Observation, request=self.rg.requests.first())

self.staff_user = blend_user(user_params={'is_staff': True, 'is_superuser': True}, profile_params={'staff_view': True})
self.staff_proposal = mixer.blend(Proposal)
mixer.blend(Membership, proposal=self.staff_proposal, user=self.staff_user)
self.staff_rg = create_simple_requestgroup(self.staff_user, self.staff_proposal)
self.staff_observation = mixer.blend(Observation, request=self.staff_rg.requests.first())

self.public_proposal = mixer.blend(Proposal, public=True)
mixer.blend(Membership, proposal=self.public_proposal, user=self.user)
self.public_requestgroup = create_simple_requestgroup(self.user, self.public_proposal)
self.public_observation = mixer.blend(Observation, request=self.public_requestgroup.requests.first())

def test_unauthenticated_user_sees_only_public_observation(self):
public_response = self.client.get(reverse('observations:observation-detail', kwargs={'pk': self.public_observation.id}))
self.assertEqual(public_response.status_code, 200)
non_public_response = self.client.get(reverse('observations:observation-detail', args=[self.observation.id]))
self.assertEqual(non_public_response.status_code, 404)

def test_authenticated_user_sees_their_observation_but_not_others(self):
self.client.force_login(self.user)
response = self.client.get(reverse('observations:observation-detail', kwargs={'pk': self.observation.id}))
self.assertEqual(response.status_code, 200)
staff_response = self.client.get(reverse('observations:observation-detail', kwargs={'pk': self.staff_observation.id}))
self.assertEqual(staff_response.status_code, 404)

def test_staff_user_with_staff_view_sees_others_observation(self):
self.client.force_login(self.staff_user)
response = self.client.get(reverse('observations:observation-detail', kwargs={'pk': self.observation.id}))
self.assertEqual(response.status_code, 200)

def test_staff_user_without_staff_view_doesnt_see_others_observation(self):
self.staff_user.profile.staff_view = False
self.staff_user.profile.save()
response = self.client.get(reverse('observations:observation-detail', kwargs={'pk': self.observation.id}))
self.assertEqual(response.status_code, 404)

def test_user_authored_only_enabled(self):
user = blend_user(profile_params={'view_authored_requests_only': True})
mixer.blend(Membership, proposal=self.public_proposal, user=user)
requestgroup = create_simple_requestgroup(user, self.public_proposal)
observation = mixer.blend(Observation, request=requestgroup.requests.first())
self.client.force_login(user)
response = self.client.get(reverse('observations:observation-detail', kwargs={'pk': self.public_observation.id}))
self.assertEqual(response.status_code, 404)
response = self.client.get(reverse('observations:observation-detail', kwargs={'pk': observation.id}))
self.assertEqual(response.status_code, 200)


class TestObservationsListView(TestCase):
def setUp(self):
self.user = blend_user()
self.proposal = mixer.blend(Proposal)
mixer.blend(Membership, proposal=self.proposal, user=self.user)
self.requestgroups = [create_simple_requestgroup(self.user, self.proposal) for _ in range(3)]
self.observations = mixer.cycle(3).blend(
Observation,
request=(rg.requests.first() for rg in self.requestgroups),
state='PENDING'
)
self.public_proposal = mixer.blend(Proposal, public=True)
mixer.blend(Membership, proposal=self.public_proposal, user=self.user)
self.public_requestgroups = [create_simple_requestgroup(self.user, self.public_proposal) for _ in range(3)]
self.public_observations = mixer.cycle(3).blend(
Observation,
request=(rg.requests.first() for rg in self.public_requestgroups),
state='PENDING'
)
self.other_user = blend_user()
self.other_proposal = mixer.blend(Proposal)
mixer.blend(Membership, proposal=self.other_proposal, user=self.other_user)
self.other_requestgroups = [create_simple_requestgroup(self.other_user, self.other_proposal) for _ in range(3)]
self.other_observations = mixer.cycle(3).blend(
Observation,
request=(rg.requests.first() for rg in self.other_requestgroups),
state='PENDING'
)

def test_unauthenticated_user_only_sees_public_observations(self):
response = self.client.get(reverse('observations:observation-list'))
self.assertIn(self.public_proposal.id, str(response.content))
self.assertNotIn(self.proposal.id, str(response.content))
self.assertNotIn(self.other_proposal.id, str(response.content))

def test_authenticated_user_sees_their_observations(self):
self.client.force_login(self.user)
response = self.client.get(reverse('observations:observation-list'))
self.assertIn(self.proposal.id, str(response.content))
self.assertIn(self.public_proposal.id, str(response.content))
self.assertNotIn(self.other_proposal.id, str(response.content))

def test_staff_user_with_staff_view_sees_everything(self):
staff_user = blend_user(user_params={'is_staff': True, 'is_superuser': True}, profile_params={'staff_view': True})
self.client.force_login(staff_user)
response = self.client.get(reverse('observations:observation-list'))
self.assertIn(self.proposal.id, str(response.content))
self.assertIn(self.public_proposal.id, str(response.content))
self.assertIn(self.other_proposal.id, str(response.content))

def test_staff_user_without_staff_view_sees_only_their_observations(self):
self.user.is_staff = True
self.user.save()
self.client.force_login(self.user)
response = self.client.get(reverse('observations:observation-list'))
self.assertIn(self.proposal.id, str(response.content))
self.assertIn(self.public_proposal.id, str(response.content))
self.assertNotIn(self.other_proposal.id, str(response.content))

def test_user_with_authored_only(self):
user = blend_user(profile_params={'view_authored_requests_only': True})
mixer.blend(Membership, proposal=self.proposal, user=user)
response = self.client.get(reverse('observations:observation-list'))
self.assertNotIn(self.proposal.id, str(response.content))

0 comments on commit da256bb

Please sign in to comment.