Skip to content

Commit

Permalink
Merge branch 'master' into feature/enable-multiple-instrument-configs
Browse files Browse the repository at this point in the history
  • Loading branch information
eheinrich committed Oct 31, 2019
2 parents b12812e + f7bcf08 commit ce9896f
Show file tree
Hide file tree
Showing 31 changed files with 1,155 additions and 272 deletions.
10 changes: 8 additions & 2 deletions observation_portal/accounts/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,14 @@ def save(self, commit=True):
view_authored_requests_only=self.cleaned_data['education_user'],
simple_interface=self.cleaned_data['education_user']
)
for invite in ProposalInvite.objects.filter(email__iexact=new_user_instance.email):
invite.accept(new_user_instance)
# There may be more than one proposal invite for the same proposal for the same user. Use the latest invite
# that was sent if this is the case.
proposal_invites = {}
for proposal_invite in ProposalInvite.objects.filter(email__iexact=new_user_instance.email).order_by('sent'):
proposal_invites[proposal_invite.proposal.id] = proposal_invite

for proposal_id in proposal_invites:
proposal_invites[proposal_id].accept(new_user_instance)

return new_user_instance

Expand Down
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
20 changes: 20 additions & 0 deletions observation_portal/accounts/test_views.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
from datetime import datetime
from django.test import TestCase
from django.urls import reverse
from rest_framework.authtoken.models import Token
from django.contrib.auth.models import User
from django.contrib import auth
from mixer.backend.django import mixer
from django.utils import timezone
from django.core import mail
from django_dramatiq.test import DramatiqTestCase

Expand Down Expand Up @@ -122,6 +124,24 @@ def test_registration_with_multiple_invites(self):
self.assertTrue(invitation.used)
self.assertTrue(Membership.objects.filter(user__username=self.reg_data['username']).exists())

def test_reqistration_with_multiple_invites_for_same_proposal(self):
proposal = mixer.blend(Proposal)
first_invitation = mixer.blend(
ProposalInvite, email=self.reg_data['email'], proposal=proposal,
sent=datetime(year=2018, month=10, day=10, tzinfo=timezone.utc), used=None
)
second_invitation = mixer.blend(
ProposalInvite, email=self.reg_data['email'].upper(), proposal=proposal,
sent=datetime(year=2019, month=10, day=10, tzinfo=timezone.utc), used=None
)
self.assertEqual(ProposalInvite.objects.all().count(), 2)
self.client.post(reverse('registration_register'), self.reg_data, follow=True)
first_invitation.refresh_from_db()
second_invitation.refresh_from_db()
self.assertFalse(first_invitation.used)
self.assertTrue(second_invitation.used)
self.assertTrue(Membership.objects.filter(user__username=self.reg_data['username']).exists())

def test_education_register(self):
reg_data = self.reg_data.copy()
reg_data['education_user'] = True
Expand Down
178 changes: 107 additions & 71 deletions observation_portal/common/state_changes.py

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions observation_portal/common/test_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ def create_simple_requestgroup(user, proposal, state='PENDING', request=None, wi
location.save()

if not configuration:
configuration = mixer.blend(Configuration, request=request)
configuration = mixer.blend(Configuration, request=request, priority=1)
else:
configuration.request = request
configuration.save()
Expand All @@ -94,8 +94,8 @@ def create_simple_many_requestgroup(user, proposal, n_requests, state='PENDING')
return rg


def create_simple_configuration(request, instrument_type='1M0-SCICAM-SBIG', instrument_config=None):
configuration = mixer.blend(Configuration, request=request, instrument_type=instrument_type)
def create_simple_configuration(request, instrument_type='1M0-SCICAM-SBIG', instrument_config=None, priority=1):
configuration = mixer.blend(Configuration, request=request, instrument_type=instrument_type, priority=priority)
fill_in_configuration_structures(configuration, instrument_config=instrument_config)
return configuration

Expand Down Expand Up @@ -127,7 +127,7 @@ def fill_in_configuration_structures(configuration, instrument_config=None, cons
acquisition_config.save()

if not target:
mixer.blend(Target, configuration=configuration)
mixer.blend(Target, configuration=configuration, ra=11.1, dec=11.1)
else:
target.configuration = configuration
target.save()
45 changes: 42 additions & 3 deletions observation_portal/common/test_state_changes.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
from unittest.mock import patch
from django.contrib.auth.models import User
from django.db.models.signals import post_save
from django.core import mail
from django_dramatiq.test import DramatiqTestCase

from observation_portal.observations.signals.handlers import cb_configurationstatus_post_save
import observation_portal.observations.signals.handlers # noqa
Expand All @@ -13,7 +15,7 @@
create_simple_requestgroup, create_simple_many_requestgroup, create_simple_configuration, SetTimeMixin,
disconnect_signal
)
from observation_portal.proposals.models import Proposal
from observation_portal.proposals.models import Proposal, Membership
from observation_portal.accounts.models import Profile
from observation_portal.observations.models import Observation, ConfigurationStatus, Summary
from observation_portal.requestgroups.models import Request, RequestGroup, Window
Expand Down Expand Up @@ -135,6 +137,26 @@ def test_request_state_completed_if_a_config_status_failed_but_acceptability_thr
request.refresh_from_db()
self.assertEqual(request.state, request_states[i])

def test_request_state_completed_if_a_config_status_completed_but_acceptability_threshold_not_reached(self):
request = self.requestgroup.requests.first()
request.acceptability_threshold = 90
request.save()
observation = request.observation_set.first()
for cs in observation.configuration_statuses.all():
summary = cs.summary
summary.time_completed = 0.0
summary.save()
cs.state = 'COMPLETED'
cs.save()
observation.refresh_from_db()
self.requestgroup.refresh_from_db()
self.assertEqual(observation.state, 'COMPLETED')
self.assertEqual(self.requestgroup.state, 'PENDING')
request_states = ['COMPLETED', 'PENDING', 'PENDING']
for i, request in enumerate(self.requestgroup.requests.all()):
request.refresh_from_db()
self.assertEqual(request.state, request_states[i])

def test_request_state_complete_if_was_expired_but_config_statuses_complete(self):
request = self.requestgroup.requests.first()
request.state = 'WINDOW_EXPIRED'
Expand Down Expand Up @@ -276,12 +298,13 @@ def test_many_requests_canceled_to_completed(self):
self.assertEqual(request.state, request_states[i])


class TestStateFromConfigurationStatuses(SetTimeMixin, TestCase):
class TestStateFromConfigurationStatuses(SetTimeMixin, DramatiqTestCase):
def setUp(self):
super().setUp()
self.proposal = dmixer.blend(Proposal)
self.user = dmixer.blend(User)
dmixer.blend(Profile, user=self.user)
dmixer.blend(Membership, user=self.user, proposal=self.proposal)
dmixer.blend(Profile, user=self.user, notifications_enabled=True)
self.client.force_login(self.user)
self.now = timezone.now()
self.window = dmixer.blend(Window, start=self.now - timedelta(days=1), end=self.now + timedelta(days=1))
Expand Down Expand Up @@ -329,6 +352,8 @@ def test_configuration_statuses_not_complete_or_failed_use_initial(self):
initial_state, 100, observation.configuration_statuses.all()
)
self.assertEqual(request_state, initial_state)
# requestgroup state is not completed, no email sent
self.assertEqual(len(mail.outbox), 0)

def test_ongoinging_configuration_statuses_in_use_initial(self):
observation = dmixer.blend(
Expand All @@ -351,6 +376,8 @@ def test_ongoinging_configuration_statuses_in_use_initial(self):
initial_state, 100, observation.configuration_statuses.all()
)
self.assertEqual(request_state, initial_state)
# requestgroup state is not completed, no email sent
self.assertEqual(len(mail.outbox), 0)

def test_configuration_statuses_failed_but_threshold_complete(self):
observation = dmixer.blend(
Expand All @@ -369,6 +396,12 @@ def test_configuration_statuses_failed_but_threshold_complete(self):
initial_state, 90, observation.configuration_statuses.all()
)
self.assertEqual(request_state, 'COMPLETED')
# The requestgroup state changed to complete, so an email should be sent
self.requestgroup.refresh_from_db()
self.assertEqual(self.requestgroup.state, 'COMPLETED')
self.assertEqual(len(mail.outbox), 1)
self.assertIn(self.requestgroup.name, str(mail.outbox[0].message()))
self.assertEqual(mail.outbox[0].to, [self.user.email])

def test_configuration_statuses_failed_but_threshold_complete_multi(self):
observation = dmixer.blend(
Expand Down Expand Up @@ -397,6 +430,12 @@ def test_configuration_statuses_failed_but_threshold_complete_multi(self):
initial_state, 95, observation.configuration_statuses.all()
)
self.assertEqual(request_state, 'COMPLETED')
# The requestgroup state changed to complete, so an email should be sent
self.requestgroup.refresh_from_db()
self.assertEqual(self.requestgroup.state, 'COMPLETED')
self.assertEqual(len(mail.outbox), 1)
self.assertIn(self.requestgroup.name, str(mail.outbox[0].message()))
self.assertEqual(mail.outbox[0].to, [self.user.email])


class TestRequestState(SetTimeMixin, TestCase):
Expand Down
24 changes: 14 additions & 10 deletions observation_portal/observations/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,37 +8,41 @@


class ObservationFilter(mixins.CustomIsoDateTimeFilterMixin, django_filters.FilterSet):
site = django_filters.MultipleChoiceFilter(choices=configdb.get_site_tuples())
enclosure = django_filters.MultipleChoiceFilter(choices=configdb.get_enclosure_tuples())
telescope = django_filters.MultipleChoiceFilter(choices=configdb.get_telescope_tuples())
site = django_filters.MultipleChoiceFilter(choices=sorted(configdb.get_site_tuples()))
enclosure = django_filters.MultipleChoiceFilter(choices=sorted(configdb.get_enclosure_tuples()))
telescope = django_filters.MultipleChoiceFilter(choices=sorted(configdb.get_telescope_tuples()))
time_span = django_filters.DateRangeFilter(
field_name='start',
label='Time Span'
)
start_after = django_filters.IsoDateTimeFilter(
field_name='start',
lookup_expr='gte',
label='Start after',
label='Start After (Inclusive)',
widget=forms.TextInput(attrs={'class': 'input', 'type': 'date'})
)
start_before = django_filters.IsoDateTimeFilter(
field_name='start',
lookup_expr='lt',
label='Start before',
label='Start Before',
widget=forms.TextInput(attrs={'class': 'input', 'type': 'date'})
)
end_after = django_filters.IsoDateTimeFilter(
field_name='end',
lookup_expr='gte',
label='End after',
label='End After (Inclusive)',
widget=forms.TextInput(attrs={'class': 'input', 'type': 'date'})
)
end_before = django_filters.IsoDateTimeFilter(
field_name='end',
lookup_expr='lt',
label='End before',
label='End Before',
widget=forms.TextInput(attrs={'class': 'input', 'type': 'date'})
)
modified_after = django_filters.IsoDateTimeFilter(
field_name='modified',
lookup_expr='gte',
label='Modified After',
label='Modified After (Inclusive)',
widget=forms.TextInput(attrs={'class': 'input', 'type': 'date'})
)
request_id = django_filters.CharFilter(field_name='request__id')
Expand All @@ -56,12 +60,12 @@ class ObservationFilter(mixins.CustomIsoDateTimeFilterMixin, django_filters.Filt
)
proposal = django_filters.CharFilter(field_name='request__request_group__proposal__id', label='Proposal')
instrument_type = django_filters.MultipleChoiceFilter(
choices=configdb.get_instrument_type_tuples(),
choices=sorted(configdb.get_instrument_type_tuples()),
label='Instrument Type',
field_name='configuration_statuses__configuration__instrument_type'
)
configuration_type = django_filters.MultipleChoiceFilter(
choices=configdb.get_configuration_type_tuples(),
choices=sorted(configdb.get_configuration_type_tuples()),
label='Configuration Type',
field_name='configuration_statuses__configuration__type'
)
Expand Down
32 changes: 31 additions & 1 deletion observation_portal/observations/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@
from django.contrib.postgres.fields import JSONField
from django.forms.models import model_to_dict
from django.utils import timezone
from django.core.cache import cache
from datetime import timedelta

from observation_portal.requestgroups.models import Request, Configuration
from observation_portal.requestgroups.models import Request, RequestGroup, Configuration
import logging

logger = logging.getLogger()


class Observation(models.Model):
STATE_CHOICES = (
('PENDING', 'PENDING'),
Expand Down Expand Up @@ -74,6 +76,34 @@ def cancel(observations):

return deleted_observations.get('observations.Observation', 0) + canceled + aborted

def update_end_time(self, new_end_time):
if new_end_time > self.start:
# Only update the end time if it is > start time
old_end_time = self.end
self.end = new_end_time
self.save()
# Cancel observations that used to be under this observation
if new_end_time > old_end_time:
observations = Observation.objects.filter(
site=self.site,
enclosure=self.enclosure,
telescope=self.telescope,
start__lte=self.end,
start__gte=old_end_time,
state='PENDING'
)
if self.request.request_group.observation_type != RequestGroup.RAPID_RESPONSE:
observations = observations.exclude(
request__request_group__observation_type=RequestGroup.RAPID_RESPONSE
)
num_canceled = Observation.cancel(observations)
logger.info(
f"updated end time for observation {self.id} to {self.end}. "
f"Canceled {num_canceled} overlapping observations."
)
cache.set('observation_portal_last_change_time', timezone.now(), None)
return self

@staticmethod
def delete_old_observations(cutoff):
observations = Observation.objects.filter(start__lt=cutoff, end__lt=cutoff, state='CANCELED').exclude(
Expand Down

0 comments on commit ce9896f

Please sign in to comment.