diff --git a/Dockerfile b/Dockerfile index d74cfe9b..32d97b69 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,7 +6,9 @@ RUN apt-get update \ && apt-get install -y curl \ && curl -sL https://deb.nodesource.com/setup_10.x | bash - \ && apt-get update \ - && apt-get install -y nodejs + && apt-get install -y nodejs \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* COPY package.json package-lock.json vue.config.js / COPY static /static diff --git a/README.md b/README.md index c29bac7e..0020e401 100644 --- a/README.md +++ b/README.md @@ -3,19 +3,19 @@ [![Coverage Status](https://coveralls.io/repos/github/LCOGT/observation-portal/badge.svg?branch=master)](https://coveralls.io/github/LCOGT/observation-portal?branch=master) [![Codacy Badge](https://api.codacy.com/project/badge/Grade/510995ede421411f8a08d0cdb588cc75)](https://www.codacy.com/app/LCOGT/observation-portal?utm_source=github.com&utm_medium=referral&utm_content=LCOGT/observation-portal&utm_campaign=Badge_Grade) -_An Astronomical Observation Web Portal and Backend_ +## An Astronomical Observation Web Portal and Backend The portal manages observation requests and observations in the context of a larger system. Other parts of this system include: -- an information store containing the configuration of resources in the system -- an information store containing resource availability information -- an information store containing periods of maintenance or other downtimes for resources in the system -- a scheduler responsible for scheduling observations on resources and a mechanism by which to report back observation states +- an information store containing the configuration of resources in the system +- an information store containing resource availability information +- an information store containing periods of maintenance or other downtimes for resources in the system +- a scheduler responsible for scheduling observations on resources and a mechanism by which to report back observation states ## Prerequisites The portal can be run as a standalone application with reduced functionality. The basic requirements are: -* Python >= 3.6 -* PostgreSQL 11 +- Python >= 3.6 +- PostgreSQL 11 ## Set up a virtual environment From the base of this project: diff --git a/observation_portal/accounts/management/commands/init_e2e_credentials.py b/observation_portal/accounts/management/commands/init_e2e_credentials.py index fdf02a46..301a4bee 100644 --- a/observation_portal/accounts/management/commands/init_e2e_credentials.py +++ b/observation_portal/accounts/management/commands/init_e2e_credentials.py @@ -9,7 +9,6 @@ from observation_portal.proposals.models import Proposal, Semester, Membership, ScienceCollaborationAllocation import logging -import sys logger = logging.getLogger() @@ -33,7 +32,7 @@ def handle(self, *args, **options): end=datetime(2100, 1, 1, tzinfo=timezone.utc)) try: user = User.objects.create_superuser(user_str, 'fake_email@lco.global', 'password') - except IntegrityError as ie: + except IntegrityError: user = User.objects.get(username=user_str) logging.warning(f"user {user_str} already exists") Profile.objects.get_or_create(user=user) @@ -43,7 +42,7 @@ def handle(self, *args, **options): public=False, non_science=True, direct_submission=True, sca=sca) Membership.objects.create(proposal=proposal, user=user, role=Membership.PI) - except IntegrityError as ie: + except IntegrityError: logging.warning(f'proposal {proposal_str} already exists') # Need to set the api token to some expected value diff --git a/observation_portal/blocks/viewsets.py b/observation_portal/blocks/viewsets.py index d6d61cb0..2ef41044 100644 --- a/observation_portal/blocks/viewsets.py +++ b/observation_portal/blocks/viewsets.py @@ -7,7 +7,6 @@ import logging from observation_portal.observations.models import Observation -from observation_portal.requestgroups.models import RequestGroup from observation_portal.blocks.filters import PondBlockFilter from observation_portal.blocks.conversion import (convert_pond_blocks_to_observations, convert_observations_to_pond_blocks) diff --git a/observation_portal/common/test_data/configdb.json b/observation_portal/common/test_data/configdb.json index 15b9563f..5f70ba87 100644 --- a/observation_portal/common/test_data/configdb.json +++ b/observation_portal/common/test_data/configdb.json @@ -559,7 +559,6 @@ "horizon": 15.0, "ha_limit_pos": 4.6, "ha_limit_neg": -4.6, - "ha_limit_neg": -4.6, "slew_rate": 0.0, "minimum_slew_overhead": 2.0, "maximum_slew_overhead": 2.0, diff --git a/observation_portal/common/test_helpers.py b/observation_portal/common/test_helpers.py index fb8317b4..6aa93b47 100644 --- a/observation_portal/common/test_helpers.py +++ b/observation_portal/common/test_helpers.py @@ -86,7 +86,7 @@ def create_simple_many_requestgroup(user, proposal, n_requests, state='PENDING') operator = 'SINGLE' if n_requests == 1 else 'MANY' rg = mixer.blend(RequestGroup, state=state, submitter=user, proposal=proposal, operator=operator, observation_type=RequestGroup.NORMAL) - for i in range(n_requests): + for _ in range(n_requests): request = mixer.blend(Request, request_group=rg, state=state) mixer.blend(Window, request=request) mixer.blend(Location, request=request) diff --git a/observation_portal/common/test_telescope_states.py b/observation_portal/common/test_telescope_states.py index 75e39064..9627da73 100644 --- a/observation_portal/common/test_telescope_states.py +++ b/observation_portal/common/test_telescope_states.py @@ -364,14 +364,14 @@ def test_states_no_enclosure_interlock(self): def test_states_end_time_after_start(self): telescope_states = TelescopeStates(self.start, self.end).get() - for tk, events in telescope_states.items(): + for _, events in telescope_states.items(): for event in events: self.assertTrue(event['start'] <= event['end']) def test_states_no_duplicate_consecutive_states(self): telescope_states = TelescopeStates(self.start, self.end).get() - for tk, events in telescope_states.items(): + for _, events in telescope_states.items(): previous_event = None for event in events: if previous_event: diff --git a/observation_portal/observations/models.py b/observation_portal/observations/models.py index fc265496..c4f82682 100644 --- a/observation_portal/observations/models.py +++ b/observation_portal/observations/models.py @@ -52,8 +52,8 @@ class Observation(models.Model): help_text='Current State of this Observation' ) - @classmethod - def cancel(self, observations): + @staticmethod + def cancel(observations): now = timezone.now() _, deleted_observations = observations.filter(start__gte=now + timedelta(hours=72)).delete() diff --git a/observation_portal/observations/tests.py b/observation_portal/observations/tests.py index c702ec01..55e184e7 100644 --- a/observation_portal/observations/tests.py +++ b/observation_portal/observations/tests.py @@ -7,7 +7,6 @@ from dateutil.parser import parse from django.urls import reverse from django.core import cache -from dateutil.parser import parse as datetime_parser from datetime import timedelta from io import StringIO from django.core.management import call_command @@ -351,7 +350,8 @@ def setUp(self): configuration.instrument_type = '1M0-SCICAM-SBIG' configuration.save() - def _generate_observation_data(self, request_id, configuration_id_list, guide_camera_name='xx03'): + @staticmethod + def _generate_observation_data(request_id, configuration_id_list, guide_camera_name='xx03'): observation = { "request": request_id, "site": "tst", @@ -916,7 +916,7 @@ def test_last_schedule_date_is_7_days_out_if_no_cached_value(self): response = self.client.get(reverse('api:last_scheduled')) last_schedule = response.json()['last_schedule_time'] - self.assertAlmostEqual(datetime_parser(last_schedule), timezone.now() - timedelta(days=7), + self.assertAlmostEqual(parse(last_schedule), timezone.now() - timedelta(days=7), delta=timedelta(minutes=1)) def test_last_schedule_date_is_updated_when_single_observation_is_submitted(self): @@ -929,12 +929,12 @@ def test_last_schedule_date_is_updated_when_single_observation_is_submitted(self response = self.client.get(reverse('api:last_scheduled') + "?site=tst") last_schedule = response.json()['last_schedule_time'] - self.assertAlmostEqual(datetime_parser(last_schedule), timezone.now(), delta=timedelta(minutes=1)) + self.assertAlmostEqual(parse(last_schedule), timezone.now(), delta=timedelta(minutes=1)) # Verify that the last scheduled time for a different site isn't updated response = self.client.get(reverse('api:last_scheduled') + "?site=non") last_schedule = response.json()['last_schedule_time'] - self.assertAlmostEqual(datetime_parser(last_schedule), timezone.now() - timedelta(days=7), + self.assertAlmostEqual(parse(last_schedule), timezone.now() - timedelta(days=7), delta=timedelta(minutes=1)) def test_last_schedule_date_is_updated_when_multiple_observations_are_submitted(self): @@ -948,7 +948,7 @@ def test_last_schedule_date_is_updated_when_multiple_observations_are_submitted( response = self.client.get(reverse('api:last_scheduled')) last_schedule = response.json()['last_schedule_time'] - self.assertAlmostEqual(datetime_parser(last_schedule), timezone.now(), delta=timedelta(minutes=1)) + self.assertAlmostEqual(parse(last_schedule), timezone.now(), delta=timedelta(minutes=1)) def test_last_schedule_date_is_not_updated_when_observation_is_mixed(self): mixer.blend(Observation, request=self.requestgroup.requests.first()) @@ -956,7 +956,7 @@ def test_last_schedule_date_is_not_updated_when_observation_is_mixed(self): self.assertIsNone(last_schedule_cached) response = self.client.get(reverse('api:last_scheduled')) last_schedule = response.json()['last_schedule_time'] - self.assertAlmostEqual(datetime_parser(last_schedule), timezone.now() - timedelta(days=7), + self.assertAlmostEqual(parse(last_schedule), timezone.now() - timedelta(days=7), delta=timedelta(minutes=1)) @@ -967,7 +967,8 @@ def setUp(self): proposal=self.proposal, std_allocation=100, rr_allocation=100, tc_allocation=100, ipp_time_available=100) - def _create_observation_and_config_status(self, requestgroup, start, end, config_state='PENDING'): + @staticmethod + def _create_observation_and_config_status(requestgroup, start, end, config_state='PENDING'): observation = mixer.blend(Observation, request=requestgroup.requests.first(), site='tst', enclosure='domb', telescope='1m0a', start=start, end=end, state='PENDING') diff --git a/observation_portal/observations/viewsets.py b/observation_portal/observations/viewsets.py index cc004884..935b44fa 100644 --- a/observation_portal/observations/viewsets.py +++ b/observation_portal/observations/viewsets.py @@ -59,11 +59,11 @@ def get_queryset(self): @action(detail=False, methods=['post']) def cancel(self, request): """ - Filters a set of observations based on the parameters provided, and then either deletes them if they are - scheduled >72 hours in the future, cancels them if they are in the future, or aborts them if they are currently - in progress. - :param request: - :return: + Filters a set of observations based on the parameters provided, and then either deletes them if they are + scheduled >72 hours in the future, cancels them if they are in the future, or aborts them if they are currently + in progress. + :param request: + :return: """ cancel_serializer = CancelObservationsSerializer(data=request.data) if cancel_serializer.is_valid(): diff --git a/observation_portal/proposals/tests.py b/observation_portal/proposals/tests.py index 420b3ee7..f9ac5b47 100644 --- a/observation_portal/proposals/tests.py +++ b/observation_portal/proposals/tests.py @@ -3,16 +3,14 @@ from django.contrib.auth.models import User from django.db.utils import IntegrityError from django.urls import reverse -from django.conf import settings from django.utils import timezone from mixer.backend.django import mixer -from unittest.mock import patch from requests import HTTPError import datetime from django_dramatiq.test import DramatiqTestCase from observation_portal.proposals.models import ProposalInvite, Proposal, Membership, ProposalNotification, TimeAllocation, Semester -from observation_portal.requestgroups.models import RequestGroup, Configuration, InstrumentConfig, AcquisitionConfig, GuidingConfig, Target +from observation_portal.requestgroups.models import RequestGroup, Configuration, InstrumentConfig from observation_portal.accounts.models import Profile from observation_portal.common.test_helpers import create_simple_requestgroup from observation_portal.requestgroups.signals import handlers # DO NOT DELETE, needed to active signals diff --git a/observation_portal/requestgroups/contention.py b/observation_portal/requestgroups/contention.py index 2c05e3cb..6043e0eb 100644 --- a/observation_portal/requestgroups/contention.py +++ b/observation_portal/requestgroups/contention.py @@ -42,7 +42,8 @@ def _binned_durations_by_proposal_and_ra(self): ra_bins[ra][proposal_id] += conf.duration return ra_bins - def _anonymize(self, data): + @staticmethod + def _anonymize(data): for index, ra in enumerate(data): data[index] = {'All Proposals': sum(ra.values())} return data diff --git a/observation_portal/requestgroups/duration_utils.py b/observation_portal/requestgroups/duration_utils.py index 56364a43..e1f8cfee 100644 --- a/observation_portal/requestgroups/duration_utils.py +++ b/observation_portal/requestgroups/duration_utils.py @@ -1,4 +1,3 @@ -import itertools from django.utils.translation import ugettext as _ from math import ceil, floor from django.utils import timezone diff --git a/observation_portal/requestgroups/filters.py b/observation_portal/requestgroups/filters.py index aefcc958..65ff3da4 100644 --- a/observation_portal/requestgroups/filters.py +++ b/observation_portal/requestgroups/filters.py @@ -1,5 +1,5 @@ import django_filters -from observation_portal.requestgroups.models import RequestGroup, Request, Location +from observation_portal.requestgroups.models import RequestGroup, Request class RequestGroupFilter(django_filters.FilterSet): diff --git a/observation_portal/requestgroups/serializers.py b/observation_portal/requestgroups/serializers.py index a535536e..c8222986 100644 --- a/observation_portal/requestgroups/serializers.py +++ b/observation_portal/requestgroups/serializers.py @@ -272,7 +272,7 @@ def validate(self, data): guiding_config['optional'] = False if data['type'] in ['LAMP_FLAT', 'ARC', 'AUTO_FOCUS', 'NRES_BIAS', 'NRES_DARK', 'BIAS', 'DARK', 'SCRIPT']: - # These types of observations should only ever be set to guiding mode OFF, but the acquisition modes for + # These types of observations should only ever be set to guiding mode OFF, but the acquisition modes for # spectrographs won't necessarily have that mode. Force OFF here. data['acquisition_config']['mode'] = AcquisitionConfig.OFF else: diff --git a/observation_portal/requestgroups/test/test.py b/observation_portal/requestgroups/test/test.py index 10ebd274..fa76cc7c 100644 --- a/observation_portal/requestgroups/test/test.py +++ b/observation_portal/requestgroups/test/test.py @@ -400,5 +400,5 @@ def test_get_duration_from_non_existent_camera(self): self.instrument_config_expose.save() with self.assertRaises(ConfigDBException) as context: - duration = self.configuration_expose.duration + _ = self.configuration_expose.duration self.assertTrue('not found in configdb' in context.exception) diff --git a/observation_portal/requestgroups/test/test_api.py b/observation_portal/requestgroups/test/test_api.py index 9e297d71..6a0792eb 100644 --- a/observation_portal/requestgroups/test/test_api.py +++ b/observation_portal/requestgroups/test/test_api.py @@ -19,15 +19,12 @@ from django.contrib.auth.models import User from django.core import cache from dateutil.parser import parse as datetime_parser -from datetime import timedelta from rest_framework.test import APITestCase from mixer.backend.django import mixer -from mixer.main import mixer as basic_mixer from django.utils import timezone from datetime import datetime, timedelta import copy import random -from urllib import parse from unittest import skip from unittest.mock import patch @@ -2572,7 +2569,7 @@ def test_last_change_date_is_updated_when_request_is_submitted(self): self.assertAlmostEqual(datetime_parser(last_change), timezone.now(), delta=timedelta(minutes=1)) def test_last_change_date_is_not_updated_when_request_is_mixed(self): - requestgroup = create_simple_requestgroup( + create_simple_requestgroup( user=self.user, proposal=self.proposal, instrument_type='1M0-SCICAM-SBIG', window=self.window ) last_change_cached = self.locmem_cache.get('observation_portal_last_change_time') diff --git a/observation_portal/sciapplications/test_admin.py b/observation_portal/sciapplications/test_admin.py index f1bfe1db..5e4d7f49 100644 --- a/observation_portal/sciapplications/test_admin.py +++ b/observation_portal/sciapplications/test_admin.py @@ -1,4 +1,3 @@ -from django.test import TestCase from django.core import mail from django.urls import reverse from django.contrib.auth.models import User diff --git a/observation_portal/userrequests/viewsets.py b/observation_portal/userrequests/viewsets.py index c3b799c1..34d393a0 100644 --- a/observation_portal/userrequests/viewsets.py +++ b/observation_portal/userrequests/viewsets.py @@ -10,11 +10,8 @@ from observation_portal.userrequests.filters import UserRequestFilter from observation_portal.userrequests.conversion import (validate_userrequest, convert_userrequests_to_requestgroups, convert_requestgroups_to_userrequests, expand_cadence, - convert_userrequest_to_requestgroup, - convert_requestgroup_to_userrequest) -from observation_portal.requestgroups.cadence import expand_cadence_request + convert_userrequest_to_requestgroup) from observation_portal.requestgroups.serializers import RequestGroupSerializer -from observation_portal.requestgroups.serializers import CadenceRequestSerializer from observation_portal.requestgroups.duration_utils import ( get_max_ipp_for_requestgroup ) diff --git a/static/js/components/instrumentconfig.vue b/static/js/components/instrumentconfig.vue index 6012306b..77daa107 100644 --- a/static/js/components/instrumentconfig.vue +++ b/static/js/components/instrumentconfig.vue @@ -305,7 +305,7 @@ export default { } } }, - rotatorModeOptions: function(newValue, oldValue) { + rotatorModeOptions: function(newValue) { if (this.instrumentHasRotatorModes) { this.instrumentconfig.rotator_mode = this.available_instruments[this.selectedinstrument].modes.rotator.default; } else { diff --git a/static/js/components/pressure.vue b/static/js/components/pressure.vue index c3e8709a..e271346d 100644 --- a/static/js/components/pressure.vue +++ b/static/js/components/pressure.vue @@ -137,6 +137,21 @@ } return maxPressure; }, + dataAvailable: function() { + for (let bin in this.rawData) { + for (let proposal in this.rawData[bin]) { + if (this.rawData[bin][proposal] > 0) { + return true; + } + } + } + return false; + } + }, + created: function() { + this.fetchData(); + }, + methods: { toSiteNightData: function() { let nights = []; let siteSpacing = 0.6; @@ -160,21 +175,6 @@ this.maxY = Math.ceil(height); return nights; }, - dataAvailable: function() { - for (let bin in this.rawData) { - for (let proposal in this.rawData[bin]) { - if (this.rawData[bin][proposal] > 0) { - return true; - } - } - } - return false; - } - }, - created: function() { - this.fetchData(); - }, - methods: { fetchData: function() { this.isLoading = true; this.rawData = []; @@ -187,7 +187,7 @@ that.rawData = data.pressure_data; that.rawSiteData = data.site_nights; that.maxY = that.maxPressureInGraph; - that.siteNights = that.toSiteNightData; + that.siteNights = that.toSiteNightData(); that.data.datasets = that.toChartData; }).done(function() { that.loadingDataFailed = false; diff --git a/static/js/request_row.js b/static/js/request_row.js index 1d00131f..e48be413 100644 --- a/static/js/request_row.js +++ b/static/js/request_row.js @@ -1,7 +1,6 @@ import $ from 'jquery'; import moment from 'moment'; import {datetimeFormat} from './utils.js'; -import tooltip from 'bootstrap'; import { getThumbnail, getLatestFrame, downloadAll } from './archive.js'; diff --git a/static/js/utils.js b/static/js/utils.js index 3b8c1bcc..c5217fe1 100644 --- a/static/js/utils.js +++ b/static/js/utils.js @@ -75,16 +75,15 @@ function sexagesimalDecToDecimal(dec){ } function decimalRaToSexigesimal(deg){ - var rs = 1; + let rs = 1; + let ra = deg; if(deg < 0){ rs = -1; - var ra = Math.abs(deg); - } else { - var ra = deg + ra = Math.abs(deg); } - var raH = Math.floor(ra / 15) - var raM = Math.floor(((ra / 15) - raH) * 60) - var raS = ((((ra / 15 ) - raH ) * 60) - raM) * 60 + let raH = Math.floor(ra / 15) + let raM = Math.floor(((ra / 15) - raH) * 60) + let raS = ((((ra / 15 ) - raH ) * 60) - raM) * 60 return { 'h': raH * rs, 'm': raM, @@ -94,21 +93,20 @@ function decimalRaToSexigesimal(deg){ } function decimalDecToSexigesimal(deg){ - var ds = 1; + let ds = 1; + let dec = deg; if(deg < 0){ ds = -1; - var dec = Math.abs(deg); - } else { - var dec = deg; + dec = Math.abs(deg); } - var deg = Math.floor(dec) - var decM = Math.abs(Math.floor((dec - deg) * 60)); - var decS = (Math.abs((dec - deg) * 60) - decM) * 60 + let decf = Math.floor(dec) + let decM = Math.abs(Math.floor((dec - decf) * 60)); + let decS = (Math.abs((dec - decf) * 60) - decM) * 60 return { - 'deg': deg * ds, + 'deg': decf * ds, 'm': decM, 's': decS, - 'str': (ds > 0 ? '' : '-') + zPadFloat(deg) + ':' + zPadFloat(decM) + ':' + zPadFloat(decS) + 'str': (ds > 0 ? '' : '-') + zPadFloat(decf) + ':' + zPadFloat(decM) + ':' + zPadFloat(decS) } } diff --git a/test_settings.py b/test_settings.py index f75f9ebb..6c273117 100644 --- a/test_settings.py +++ b/test_settings.py @@ -1,11 +1,11 @@ from observation_portal.settings import * # noqa import logging -""" -Settings specific to running tests. Using sqlite will run tests 100% in memory. -https://docs.djangoproject.com/en/dev/internals/contributing/writing-code/unit-tests/#using-another-settings-module -This file should be automatically used during tests, but you can manually specify as well: -./manage.py --settings=valhalla.test_settings -""" + +# Settings specific to running tests. Using sqlite will run tests 100% in memory. +# https://docs.djangoproject.com/en/dev/internals/contributing/writing-code/unit-tests/#using-another-settings-module +# This file should be automatically used during tests, but you can manually specify as well: +# ./manage.py --settings=valhalla.test_settings + logging.disable(logging.CRITICAL) PASSWORD_HASHERS = ( 'django.contrib.auth.hashers.MD5PasswordHasher',