diff --git a/.gitignore b/.gitignore index 42b0cdc3e..892a00645 100644 --- a/.gitignore +++ b/.gitignore @@ -63,3 +63,4 @@ ingest/static/ingest/css/admin.css ingest/static/ingest/css/dashboard.css neoexchange/neox/local_settings.py *.db +docker-compose.yml diff --git a/docker/neoexchange.dockerfile b/Dockerfile similarity index 52% rename from docker/neoexchange.dockerfile rename to Dockerfile index 0f78292ad..6b9301d71 100644 --- a/docker/neoexchange.dockerfile +++ b/Dockerfile @@ -10,57 +10,61 @@ # just default to using the nginx port only (recommended). There is no # requirement to map all exposed container ports onto host ports. # -# To run with nginx only: -# docker run -d -p 8200:8200 --name=neoexchange lcogtwebmaster/lcogt:neoexchange_$BRANCH +# Build with +# docker build -t docker.lcogt.net/neoexchange:latest . # -# To run with nginx + uwsgi both exposed: -# docker run -d -p 8200:8200 -p 8201:8201 --name=neox lcogtwebmaster/lcogt:neoexchange_$BRANCH +# Push to docker registry with +# docker push docker.lcogt.net/neoexchange:latest # -# See the notes in the code below about NFS mounts. +# To run with nginx + uwsgi both exposed: +# docker run -d -p 8200:80 --name=neox docker.lcogt.net/neoexchange:latest +# Or use the docker-compose.yml from github.com/LCOGT/docker/compose/neoexchange/ # ################################################################################ FROM centos:centos7 MAINTAINER LCOGT -# Install package repositories -RUN yum -y install epel-release - # Install packages and update base system -RUN yum -y install nginx python-pip mysql-devel python-devel supervisor -RUN yum -y groupinstall "Development Tools" -RUN yum -y update - -# Copy the LCOGT Mezzanine webapp files -COPY neoexchange /var/www/apps/neoexchange +RUN yum -y install epel-release \ + && yum -y install cronie libjpeg-devel nginx python-pip mysql-devel python-devel supervisor \ + && yum -y groupinstall "Development Tools" \ + && yum -y update +# Setup our python env now so it can be cached +COPY neoexchange/requirements.txt /var/www/apps/neoexchange/requirements.txt # Install the LCOGT NEO exchange Python required packages -RUN pip install pip==1.3 && pip install uwsgi==2.0.8 -RUN pip install -r /var/www/apps/neoexchange/pip_requirements.txt +RUN pip install pip==1.3 && pip install uwsgi==2.0.8 \ + && pip install -r /var/www/apps/neoexchange/requirements.txt + # LCOGT packages which have to be installed after the normal pip install -RUN pip install pyslalib --extra-index-url=http://buildsba.lco.gtn/python/ -RUN pip install rise_set --extra-index-url=http://buildsba.lco.gtn/python/ +RUN pip install pyslalib --extra-index-url=http://buildsba.lco.gtn/python/ \ + && pip install rise_set --extra-index-url=http://buildsba.lco.gtn/python/ +# Ensure crond will run on all host operating systems +RUN sed -i -e 's/\(session\s*required\s*pam_loginuid.so\)/#\1/' /etc/pam.d/crond # Setup the Python Django environment ENV PYTHONPATH /var/www/apps ENV DJANGO_SETTINGS_MODULE neox.settings -ENV BRANCH ${BRANCH} -#ENV BUILDDATE ${BUILDDATE} - -# Setup the LCOGT Mezzanine webapp -RUN python /var/www/apps/neoexchange/manage.py validate -RUN python /var/www/apps/neoexchange/manage.py collectstatic --noinput -RUN python /var/www/apps/neoexchange/manage.py syncdb --noinput -RUN python /var/www/apps/neoexchange/manage.py migrate --noinput # Copy configuration files COPY config/uwsgi.ini /etc/uwsgi.ini COPY config/nginx/* /etc/nginx/ -COPY config/neoexchange.ini /etc/supervisord.d/neoexchange.ini +COPY config/processes.ini /etc/supervisord.d/processes.ini COPY config/crontab.root /var/spool/cron/root -# nginx runs on port 8200, uwsgi runs on port 8201 -EXPOSE 8200 8201 +# nginx runs on port 80, uwsgi is linked in the nginx conf +EXPOSE 80 + +# The entry point is our init script, which runs startup tasks, then +# execs the supervisord daemon +ENTRYPOINT [ "/init" ] + +# Copy configuration files +COPY config/init /init + +# Copy the LCOGT Mezzanine webapp files +COPY neoexchange /var/www/apps/neoexchange -# Entry point is the supervisord daemon -ENTRYPOINT [ "/usr/bin/supervisord", "-n" ] +# Setup the LCOGT NEOx webapp +RUN python /var/www/apps/neoexchange/manage.py collectstatic --noinput diff --git a/Makefile b/Makefile deleted file mode 100644 index fa0cd6989..000000000 --- a/Makefile +++ /dev/null @@ -1,56 +0,0 @@ -#----------------------------------------------------------------------------------------------------------------------- -# neoexchange docker image makefile -# -# 'make' will create the docker image needed to run the neoexchange app: -# lcogtwebmaster/lcogt:neoexchange_$BRANCH -# -# where $BRANCH is the git branch name presently in use. -# -# Once built, this image can be pushed up the docker hub repository via 'make install', -# and can then be run via something like: -# -# docker run -d -p 8200:8200 -p 8201:8201 --name=neoexchange lcogtwebmaster/lcogt:neoexchange_$BRANCH -# -# at which point nginx will be exposed on the host at port 8100 -# and uwsgi will be exposed on the host at port 8101 (optional, leave out the -p 8101:8101 argument if you don't need it) -# -# In order to get this work on Fedora (after installing Docker), I needed -# to do (as root or sudo): -# $ systemctl start docker -# $ systemctl enable docker (to get it to restart at boot) -# $ groupadd docker -# $ chown root:docker /var/run/docker.sock -# $ usermod -a -G docker -# (and then log out and back in again as that user) -# -# Ira W. Snyder -# Doug Thomas -# Tim Lister -# LCOGT -# -#----------------------------------------------------------------------------------------------------------------------- - -NAME := lcogtwebmaster/lcogt -BRANCH := $(shell git rev-parse --abbrev-ref HEAD) -BUILDDATE := $(shell date +%Y%m%d%H%M) -TAG1 := neoexchange_$(BRANCH) -ENVSUBST := $(shell command -v envsubst) -$(info $$ENVSUBST is [${ENVSUBST}]) - -.PHONY: all neoexchange login install - -all: neoexchange - -login: - docker login --username="lcogtwebmaster" --password="lc0GT!" --email="webmaster@lcogt.net" - -neoexchange: - export BUILDDATE=$(BUILDDATE) && \ - export BRANCH=$(BRANCH) && \ - cat docker/neoexchange.dockerfile | $(ENVSUBST) '$$BRANCH $$BUILDDATE' > Dockerfile - docker build -t $(NAME):$(TAG1) --rm . - rm -f Dockerfile - -install: login - @if ! docker images $(NAME) | awk '{ print $$2 }' | grep -q -F $(TAG1); then echo "$(NAME):$(TAG1) is not yet built. Please run 'make'"; false; fi - docker push $(NAME):$(TAG1) diff --git a/README.md b/README.md index 82b519e43..44270efbe 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ NEO Exchange ============ -Portal for scheduling observations of NEOs using LCOGT (Version 0.1.0) +Portal for scheduling observations of NEOs using LCOGT (Version 1.0.1) Setup ----- @@ -14,6 +14,71 @@ Construct a Python Virtual Enviroment (virtualenv) by executing: or: `source /bin/activate.csh # for (t)csh-shells` -`pip install -r pip-requirements.txt` +then: +`pip install -r neox/requirements.txt` + +Deployment +---------- + +You will need to set up 3 environment variables before deploying (if you are just locally testing see instructions below). + +If you are using BASH or ZSH add the following to your .profile or .zshrc files: +``` +export NEOX_DB_USER='' +export NEOX_DB_PASSWORD='' +export NEOX_DB_HOST='' +``` + +Docker +------ +If you are building a Docker container use the following syntax: +``` +docker build -t docker.lcogt.net/neoexchange:latest . +``` +This will build a Docker image which will need to be pushed into a Docker registry with: +``` +docker push docker.lcogt.net/neoexchange:latest +``` +Starting a Docker container from this image can be done with a `docker run` command or using `docker-compose`. + + +Local Testing +------------- + +For local testing you will probably want to create a +`neoexchange/neox/local_settings.py` file to point at a local test database and +to switch on `DEBUG` for easier testing. An example file would look like: +``` +import sys, os + +CURRENT_PATH = os.path.dirname(os.path.realpath(__file__)) +BASE_DIR = os.path.dirname(CURRENT_PATH) + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': 'neox.db', # Or path to database file if using sqlite3. + 'USER': '', # Not used with sqlite3. + 'PASSWORD': '', # Not used with sqlite3. + 'HOST': '', # Set to empty string for localhost. Not used with sqlite3. + 'PORT': '', # Set to empty string for default. Not used with sqlite3. + } +} + +DEBUG = True + +# Use a different database file when testing or exploring in the shell. +if 'test' in sys.argv or 'test_coverage' in sys.argv or 'shell' in sys.argv: + DATABASES['default']['ENGINE'] = 'django.db.backends.sqlite3' + DATABASES['default']['NAME'] = 'test.db' + DATABASES['default']['USER'] = '' + DATABASES['default']['PASSWORD'] = '' + +STATIC_ROOT = os.path.abspath(os.path.join(BASE_DIR, '../../static/')) +``` + +To prepare the local SQLite DB for use, you should follow these steps: +1. `cd neoexchange\neoexchange` +2. Run `python manage.py syncdb`. This will perform migrations as necessary. diff --git a/config/crontab.root b/config/crontab.root index 3bcfc0f26..56c00bd3f 100644 --- a/config/crontab.root +++ b/config/crontab.root @@ -1,4 +1,4 @@ # Put your cron jobs here 10 5 * * * python /var/www/apps/neoexchange/manage.py fetch_goldstone_targets -10 5 * * * python /var/www/apps/neoexchange/manage.py update_neocp_data -10 5 * * * python /var/www/apps/neoexchange/manage.py update_crossids \ No newline at end of file +20 5 * * * python /var/www/apps/neoexchange/manage.py update_neocp_data +30 5 * * * python /var/www/apps/neoexchange/manage.py update_crossids \ No newline at end of file diff --git a/config/init b/config/init new file mode 100755 index 000000000..ac98c421d --- /dev/null +++ b/config/init @@ -0,0 +1,8 @@ +#!/bin/bash + +# Substitute the prefix into the nginx configuration +# If any schema changed have happened but not been applied +python /var/www/apps/neoexchange/manage.py migrate --noinput + +# Run under supervisord +exec /usr/bin/supervisord -n -c /etc/supervisord.conf \ No newline at end of file diff --git a/config/nginx/nginx.conf b/config/nginx/nginx.conf index 92bd49ce4..71745a346 100644 --- a/config/nginx/nginx.conf +++ b/config/nginx/nginx.conf @@ -34,7 +34,7 @@ http { } server { - listen 8200; + listen 80; server_name dockerhost; charset utf-8; diff --git a/config/neoexchange.ini b/config/processes.ini similarity index 100% rename from config/neoexchange.ini rename to config/processes.ini diff --git a/docker/bin/loaddata.sh b/docker/bin/loaddata.sh deleted file mode 100755 index 8d83e41ed..000000000 --- a/docker/bin/loaddata.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash -cd /var/www/apps/neoexchange -python manage.py loaddata - diff --git a/docker/bin/nginx.sh b/docker/bin/nginx.sh deleted file mode 100755 index 6aab4bbed..000000000 --- a/docker/bin/nginx.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash -touch /var/log/nginx/error.log -touch /var/log/nginx/access.log -mkdir -p /run -tail --pid $$ -f /var/log/nginx/access.log & -tail --pid $$ -f /var/log/nginx/error.log & -cat /var/www/apps/neoexchange/docker/config/nginx.conf | envsubst '$PREFIX $NEOEXCHANGE_UWSGI_PORT_8001_TCP_ADDR' > /etc/nginx/nginx.conf -/usr/sbin/nginx -c /etc/nginx/nginx.conf diff --git a/docker/bin/run.sh b/docker/bin/run.sh deleted file mode 100755 index 2273843b8..000000000 --- a/docker/bin/run.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/bash -BRANCH=`git name-rev --name-only HEAD` -docker stop_uwsgi 2>&1 > /dev/null -docker rm_uwsgi 2>&1 > /dev/null -docker stop_nginx 2>&1 > /dev/null -docker rm_nginx 2>&1 > /dev/null -docker login --username="lcogtwebmaster" --password="lc0GT!" --email="webmaster@lcogt.net" -if [ "$DEBUG" != "" ]; then - DEBUGENV="-e DEBUG=True" -fi -if [ "$PREFIX" == "" ]; then - PREFIX="" -fi -docker run -d --name_uwsgi -e PREFIX=$PREFIX $DEBUGENV lcogtwebmaster/lcogt_$BRANCH /var/www/apps/docker/bin/uwsgi.sh -docker run -d --name_nginx -p 8000:8000 -e PREFIX=$PREFIX $DEBUGENV --link_uwsgi_uwsgi lcogtwebmaster/lcogt_$BRANCH /var/www/apps/docker/bin/nginx.sh -if [ "$DEBUG" != "" ]; then - docker logs -f_nginx & - docker logs -f_uwsgi & -fi \ No newline at end of file diff --git a/docker/bin/stop.sh b/docker/bin/stop.sh deleted file mode 100755 index fa7de3c36..000000000 --- a/docker/bin/stop.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash -docker stop observations_uwsgi 2>&1 > /dev/null -docker rm observations_uwsgi 2>&1 > /dev/null -docker stop observations_nginx 2>&1 > /dev/null -docker rm observations_nginx 2>&1 > /dev/null \ No newline at end of file diff --git a/docker/bin/uwsgi.sh b/docker/bin/uwsgi.sh deleted file mode 100755 index e3ca8194b..000000000 --- a/docker/bin/uwsgi.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash -touch /var/log/uwsgi.log -tail --pid $$ -f /var/log/uwsgi.log & -/usr/bin/uwsgi --ini /var/www/apps/neoexchange/docker/config/uwsgi.ini \ No newline at end of file diff --git a/neoexchange/ingest/__init__.py b/neoexchange/astrometrics/__init__.py similarity index 100% rename from neoexchange/ingest/__init__.py rename to neoexchange/astrometrics/__init__.py diff --git a/neoexchange/ingest/ast_subs.py b/neoexchange/astrometrics/ast_subs.py similarity index 77% rename from neoexchange/ingest/ast_subs.py rename to neoexchange/astrometrics/ast_subs.py index dc147426c..015f77374 100644 --- a/neoexchange/ingest/ast_subs.py +++ b/neoexchange/astrometrics/ast_subs.py @@ -134,3 +134,37 @@ def normal_to_packed(obj_name, dbg=False): packed_desig = ' ' * 12 rval = -1 return ( packed_desig, rval) + +def determine_asteroid_type(perihdist, eccentricity): + '''Determines the object class from the perihelion distance and + the eccentricity and returns it as a single character code + as defined in core/models.py. + + Currently the checked types are: + 1) NEO (perihelion < 1.3 AU) + 2) Centaur (perihelion > 5.5 AU (orbit of Jupiter) & semi-major axis < 30.1 AU + (orbit of Neptune) + 3) KBO (perihelion > 30.1 AU (orbit of Neptune)''' + + jupiter_semidist = 5.5 + neptune_semidist = 30.1 + juptrojan_lowlimit = 5.05 + juptrojan_hilimit = 5.35 + + obj_type = 'A' + if perihdist <= 1.3 and eccentricity < 0.999: + obj_type = 'N' # NEO + else: + # Test for eccentricity close to or greater than 1.0 + if abs(eccentricity-1.0) >= 1e-3 and eccentricity < 1.0: + semi_axis = perihdist / (1.0 - eccentricity ) + if perihdist >= jupiter_semidist and semi_axis >= jupiter_semidist \ + and semi_axis <= neptune_semidist: + obj_type = 'E' # Centaur + elif perihdist > neptune_semidist: + obj_type = 'K' + elif semi_axis >= juptrojan_lowlimit and semi_axis <= juptrojan_hilimit: + obj_type = 'T' + else: + obj_type = 'C' # Comet + return obj_type diff --git a/neoexchange/ingest/ephem_subs.py b/neoexchange/astrometrics/ephem_subs.py similarity index 82% rename from neoexchange/ingest/ephem_subs.py rename to neoexchange/astrometrics/ephem_subs.py index 9965c3695..289f80392 100644 --- a/neoexchange/ingest/ephem_subs.py +++ b/neoexchange/astrometrics/ephem_subs.py @@ -2,7 +2,7 @@ NEO exchange: NEO observing portal for Las Cumbres Observatory Global Telescope Network Copyright (C) 2014-2015 LCOGT -ast_subs.py -- Asteroid desigination related routines. +ephem_subs.py -- Asteroid ephemeris related routines. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -20,7 +20,7 @@ from numpy import array, concatenate, zeros # Local imports -from ingest.time_subs import datetime2mjd_utc, datetime2mjd_tdb, mjd_utc2mjd_tt, ut1_minus_utc, round_datetime +from astrometrics.time_subs import datetime2mjd_utc, datetime2mjd_tdb, mjd_utc2mjd_tt, ut1_minus_utc, round_datetime #from astsubs import mpc_8lineformat import logging @@ -31,9 +31,9 @@ def compute_phase_angle(r, delta, es_Rsq, dbg=False): '''Method to compute the phase angle (beta), trapping bad values''' # Compute phase angle, beta (Sun-Target-Earth angle) - if dbg: print "r,r^2,delta,delta^2,es_Rsq=",r,r*r,delta,delta*delta,es_Rsq + logger.debug("r(%s), r^2 (%s),delta (%s),delta^2 (%s), es_Rsq (%s)" % (r,r*r,delta,delta*delta,es_Rsq)) arg = (r*r+delta*delta-es_Rsq)/(2.0*r*delta) - if dbg: print "arg=", arg + logger.debug("arg=%s" % arg) if arg >= 1.0: beta = 0.0 @@ -42,8 +42,7 @@ def compute_phase_angle(r, delta, es_Rsq, dbg=False): else: beta = acos(arg) - if dbg: print - if dbg: print "Phase angle, beta (deg)=", beta, degrees(beta) + logger.debug("Phase angle, beta (deg)=%s %s" % (beta, degrees(beta))) return beta def compute_ephem(d, orbelems, sitecode, dbg=False, perturb=True, display=False): @@ -76,33 +75,32 @@ def compute_ephem(d, orbelems, sitecode, dbg=False, perturb=True, display=False) else: epoch_mjd = orbelems['epoch'] - if dbg: print 'Element Epoch=', epoch_mjd - if dbg: print 'MJD(UTC) = ', mjd_utc - if dbg: print ' JD(UTC) =', mjd_utc + 2400000.5 + logger.debug('Element Epoch=', epoch_mjd) + logger.debug('MJD(UTC) = ', mjd_utc) + logger.debug(' JD(UTC) =', mjd_utc + 2400000.5) # Convert MJD(UTC) to MJD(TT) mjd_tt = mjd_utc2mjd_tt(mjd_utc) - if dbg: print 'MJD(TT) = %.15f' % (mjd_tt) + logger.debug('MJD(TT) = %.15f' % (mjd_tt)) # Compute UT1-UTC dut = ut1_minus_utc(mjd_utc) - if dbg: print "UT1-UTC =", dut + logger.debug("UT1-UTC =%s" % dut) # Obtain precession-nutation 3x3 rotation matrix # Should really be TDB but "TT will do" says The Wallace... rmat = S.sla_prenut(2000.0, mjd_tt) - if dbg: print rmat + logger.debug(rmat) # Obtain latitude, longitude of the observing site. # Reverse longitude to get the more normal East-positive convention # (site_num, site_name, site_long, site_lat, site_hgt) = S.sla_obs(0, 'SAAO74') # site_long = -site_long (site_name, site_long, site_lat, site_hgt) = get_sitepos(sitecode) - if dbg: print - if dbg: print sitecode, site_name, site_long, site_lat, site_hgt + logger.debug(sitecode, site_name, site_long, site_lat, site_hgt) # Compute local apparent sidereal time # Do GMST first which takes UT1 and then add East longitiude and the equation of the equinoxes @@ -115,10 +113,10 @@ def compute_ephem(d, orbelems, sitecode, dbg=False, perturb=True, display=False) pvobs = S.sla_pvobs(site_lat, site_hgt, stl) if site_name == '?': - if dbg: print "WARN: No site co-ordinates found, computing for geocenter" + logger.debug("WARN: No site co-ordinates found, computing for geocenter") pvobs = pvobs * 0.0 - if dbg: print "PVobs(orig)=", pvobs[0:3], "\n ", pvobs[3:6]*86400.0 + logger.debug("PVobs(orig)=%s\n %s" % (pvobs[0:3],pvobs[3:6]*86400.0)) # Apply transpose of precession/nutation matrix to pv vector to go from # true equator and equinox of date to J2000.0 mean equator and equinox (to @@ -127,7 +125,7 @@ def compute_ephem(d, orbelems, sitecode, dbg=False, perturb=True, display=False) pos_new = S.sla_dimxv(rmat, pvobs[0:3]) vel_new = S.sla_dimxv(rmat, pvobs[3:6]) pvobs_new = concatenate([pos_new, vel_new]) - if dbg: print "PVobs(new)=", pvobs_new[0:3], "\n ", pvobs_new[3:6]*86400.0 + logger.debug("PVobs(new)=%s\n %s" % (pvobs_new[0:3],pvobs_new[3:6]*86400.0)) # Earth position and velocity @@ -146,18 +144,16 @@ def compute_ephem(d, orbelems, sitecode, dbg=False, perturb=True, display=False) # (e_pos_hel, e_vel_hel, e_pos_bar, e_vel_bar ) = ephem.epv(mjd_tt) e_vel_hel = e_vel_hel/86400.0 - if dbg: print - if dbg: print "Sun->Earth [X, Y, Z]=", e_pos_hel - if dbg: print "Sun->Earth [X, Y, Z]= %20.15E %20.15E %20.15E" % (e_pos_hel[0], e_pos_hel[1], e_pos_hel[2]) - if dbg: print "Sun->Earth [Xdot, Ydot, Zdot]=", e_vel_hel - if dbg: print "Sun->Earth [Xdot, Ydot, Zdot]= %20.15E %20.15E %20.15E" % (e_vel_hel[0]*86400.0, e_vel_hel[1]*86400.0, e_vel_hel[2]*86400.0) + logger.debug("Sun->Earth [X, Y, Z]=%s" % e_pos_hel) + logger.debug("Sun->Earth [X, Y, Z]= %20.15E %20.15E %20.15E" % (e_pos_hel[0], e_pos_hel[1], e_pos_hel[2])) + logger.debug("Sun->Earth [Xdot, Ydot, Zdot]=%s" % e_vel_hel) + logger.debug("Sun->Earth [Xdot, Ydot, Zdot]= %20.15E %20.15E %20.15E" % (e_vel_hel[0]*86400.0, e_vel_hel[1]*86400.0, e_vel_hel[2]*86400.0)) # Add topocentric offset in position and velocity e_pos_hel = e_pos_hel + pvobs_new[0:3] e_vel_hel = e_vel_hel + pvobs_new[3:6] - if dbg: print - if dbg: print "Sun->Obsvr [X, Y, Z]=", e_pos_hel - if dbg: print "Sun->Obsvr [Xdot, Ydot, Zdot]=", e_vel_hel + logger.debug("Sun->Obsvr [X, Y, Z]=%s" % e_pos_hel) + logger.debug("Sun->Obsvr [Xdot, Ydot, Zdot]=%s" % e_vel_hel) # Asteroid position (and velocity) @@ -199,7 +195,7 @@ def compute_ephem(d, orbelems, sitecode, dbg=False, perturb=True, display=False) orbelems['arg_perihelion'].in_radians(), orbelems['semi_axis'], orbelems['eccentricity'], orbelems['mean_anomaly'].in_radians()) else: - if dbg: print "Not perturbing" + logger.debug("Not perturbing") p_epoch_mjd = epoch_mjd j = 0 else: @@ -232,7 +228,7 @@ def compute_ephem(d, orbelems, sitecode, dbg=False, perturb=True, display=False) j = 0 if j != 0: - print "Perturbing error=", j + print "Perturbing error=%s" % j r3 = -100. @@ -256,8 +252,8 @@ def compute_ephem(d, orbelems, sitecode, dbg=False, perturb=True, display=False) p_orbelems['MeanAnom'], 0.0) - if dbg: print "Sun->Asteroid [x,y,z]=", pv[0:3], status - if dbg: print "Sun->Asteroid [xdot,ydot,zdot]=", pv[3:6], status + logger.debug("Sun->Asteroid [x,y,z]=%s %s" % (pv[0:3], status)) + logger.debug("Sun->Asteroid [xdot,ydot,zdot]=%s %s" % (pv[3:6], status)) for i, e_pos in enumerate(e_pos_hel): pos[i] = pv[i] - e_pos @@ -265,32 +261,31 @@ def compute_ephem(d, orbelems, sitecode, dbg=False, perturb=True, display=False) for i, e_vel in enumerate(e_vel_hel): vel[i] = pv[i+3] - e_vel - if dbg: print "Earth->Asteroid [x,y,z]=", pos - if dbg: print "Earth->Asteroid [xdot,ydot,zdot]=", vel + logger.debug("Earth->Asteroid [x,y,z]=%s" % pos) + logger.debug("Earth->Asteroid [xdot,ydot,zdot]=%s" % vel) # geometric distance, delta (AU) delta = sqrt(pos[0]*pos[0] + pos[1]*pos[1] + pos[2]*pos[2]) - if dbg: print - if dbg: print "Geometric distance, delta (AU)=", delta + + logger.debug("Geometric distance, delta (AU)=%s" % delta) # Light travel time to asteroid ltt = tau * delta - if dbg: print "Light travel time (sec, min, days)=", ltt, ltt/60.0, ltt/86400.0 + logger.debug("Light travel time (sec, min, days)=%s %s %s" % (ltt, ltt/60.0, ltt/86400.0)) # Correct position for planetary aberration for i, a_pos in enumerate(pos): pos[i] = a_pos - (ltt * vel[i]) - if dbg: print - if dbg: print "Earth->Asteroid [x,y,z]=", pos - if dbg: print "Earth->Asteroid [x,y,z]= %20.15E %20.15E %20.15E" % (pos[0], pos[1], pos[2]) - if dbg: print "Earth->Asteroid [xdot,ydot,zdot]=", vel*86400.0 + logger.debug("Earth->Asteroid [x,y,z]=%s" % pos) + logger.debug("Earth->Asteroid [x,y,z]= %20.15E %20.15E %20.15E" % (pos[0], pos[1], pos[2])) + logger.debug("Earth->Asteroid [xdot,ydot,zdot]=%s %s %s" % (vel[0]*86400.0,vel[1]*86400.0,vel[2]*86400.0)) # Convert Cartesian to RA, Dec (ra, dec) = S.sla_dcc2s(pos) - if dbg: print "ra,dec=", ra, dec + logger.debug("ra,dec=%s %s" % (ra, dec)) ra = S.sla_dranrm(ra) - if dbg: print "ra,dec=", ra, dec + logger.debug("ra,dec=%s %s" % (ra, dec)) (rsign, ra_geo_deg) = S.sla_dr2tf(2, ra) (dsign, dec_geo_deg) = S.sla_dr2af(1, dec) @@ -300,19 +295,19 @@ def compute_ephem(d, orbelems, sitecode, dbg=False, perturb=True, display=False) cposz = pv[2] - (ltt * pv[5]) r = sqrt(cposx*cposx + cposy*cposy + cposz*cposz) - if dbg: print "r (AU) =", r + logger.debug("r (AU) =%s" % r) # Compute R, the Earth-Sun distance. (Only actually need R^2 for the mag. formula) es_Rsq = (e_pos_hel[0]*e_pos_hel[0] + e_pos_hel[1]*e_pos_hel[1] + e_pos_hel[2]*e_pos_hel[2]) - if dbg: print "R (AU) =", sqrt(es_Rsq) - if dbg: print "delta (AU)=", delta + logger.debug("R (AU) =%s" % sqrt(es_Rsq)) + logger.debug("delta (AU)=%s" % delta) # Compute sky motion sky_vel = compute_relative_velocity_vectors(e_pos_hel, e_vel_hel, pos, vel, delta, dbg) - if dbg: print "vel1, vel2, r= %15.10lf %15.10lf %15.10lf" % (sky_vel[1], sky_vel[2], delta) - if dbg: print "vel1, vel2, r= %15.10e %15.10e %15.10lf\n" % (sky_vel[1], sky_vel[2], delta) + logger.debug("vel1, vel2, r= %15.10lf %15.10lf %15.10lf" % (sky_vel[1], sky_vel[2], delta)) + logger.debug("vel1, vel2, r= %15.10e %15.10e %15.10lf\n" % (sky_vel[1], sky_vel[2], delta)) total_motion, sky_pa, ra_motion, dec_motion = compute_sky_motion(sky_vel, delta, dbg) @@ -329,7 +324,7 @@ def compute_ephem(d, orbelems, sitecode, dbg=False, perturb=True, display=False) phi1 = exp(-3.33 * (tan(beta/2.0))**0.63) phi2 = exp(-1.87 * (tan(beta/2.0))**1.22) - # if dbg: print "Phi1, phi2=", phi1,phi2 + # logger.debug("Phi1, phi2=%s" % phi1,phi2) # Calculate magnitude of object mag = p_orbelems['H'] + 5.0 * log10(r * delta) - \ @@ -365,12 +360,12 @@ def compute_relative_velocity_vectors(obs_pos_hel, obs_vel_hel, obj_pos, obj_vel j2000_vel[i] = obj_vel[i] - obs_vel_hel[i] matrix[i] = obj_pos[i] / delta i += 1 - if dbg: print " obj_vel= %15.10f %15.10f %15.10f" % (obj_vel[0], obj_vel[1], obj_vel[2]) - if dbg: print " obs_vel= %15.10f %15.10f %15.10f" % (obs_vel_hel[0], obs_vel_hel[1], obs_vel_hel[2]) - if dbg: print " obs_vel= %15.10e %15.10e %15.10e" % (obs_vel_hel[0], obs_vel_hel[1], obs_vel_hel[2]) + logger.debug(" obj_vel= %15.10f %15.10f %15.10f" % (obj_vel[0], obj_vel[1], obj_vel[2])) + logger.debug(" obs_vel= %15.10f %15.10f %15.10f" % (obs_vel_hel[0], obs_vel_hel[1], obs_vel_hel[2])) + logger.debug(" obs_vel= %15.10e %15.10e %15.10e" % (obs_vel_hel[0], obs_vel_hel[1], obs_vel_hel[2])) - if dbg: print " j2000_vel= %15.10e %15.10e %15.10e" % (j2000_vel[0], j2000_vel[1], j2000_vel[2]) - if dbg: print "matrix_vel= %15.10f %15.10f %15.10f" % (matrix[0], matrix[1], matrix[2] ) + logger.debug(" j2000_vel= %15.10e %15.10e %15.10e" % (j2000_vel[0], j2000_vel[1], j2000_vel[2])) + logger.debug("matrix_vel= %15.10f %15.10f %15.10f" % (matrix[0], matrix[1], matrix[2] )) length = sqrt( matrix[0] * matrix[0] + matrix[1] * matrix[1]) matrix[3] = matrix[1] / length @@ -401,10 +396,10 @@ def compute_sky_motion(sky_vel, delta, dbg=True): dec_motion = dec_motion * 60.0 / 24.0 sky_pa = 180.0 + degrees(atan2(-ra_motion, -dec_motion)) - if dbg: print "RA motion, Dec motion, PA=%10.7f %10.7f %6.1f" % (ra_motion, dec_motion, sky_pa ) + logger.debug( "RA motion, Dec motion, PA=%10.7f %10.7f %6.1f" % (ra_motion, dec_motion, sky_pa )) total_motion = sqrt(ra_motion * ra_motion + dec_motion * dec_motion) - if dbg: print "Total motion=%10.7f" % (total_motion) + logger.debug( "Total motion=%10.7f" % (total_motion)) return (total_motion, sky_pa, ra_motion, dec_motion) @@ -508,7 +503,7 @@ def determine_darkness_times(site_code, utc_date=datetime.utcnow(), debug=False) utc_date = datetime.combine(utc_date, time()) (start_of_darkness, end_of_darkness) = astro_darkness(site_code, utc_date) end_of_darkness = end_of_darkness+timedelta(hours=1) - logger.debug("Start,End of darkness=", start_of_darkness, end_of_darkness) + logger.debug("Start,End of darkness=%s %s", start_of_darkness, end_of_darkness) if utc_date > end_of_darkness: logger.debug("Planning for the next night") utc_date = utc_date + timedelta(days=1) @@ -517,10 +512,10 @@ def determine_darkness_times(site_code, utc_date=datetime.utcnow(), debug=False) utc_date = utc_date + timedelta(days=-1) utc_date = utc_date.replace(hour=0, minute=0, second=0, microsecond=0) - logger.debug("Planning observations for", utc_date, "for", site_code) + logger.debug("Planning observations for %s for %s", utc_date, site_code) # Get hours of darkness for site (dark_start, dark_end) = astro_darkness(site_code, utc_date) - logger.debug("Dark from ", dark_start, "to", dark_end) + logger.debug("Dark from %s to %s", dark_start, dark_end) return dark_start, dark_end @@ -659,44 +654,52 @@ def __init__(self, value): def __str__(self): return self.value -BRIGHTEST_ALLOWABLE_MAG = 5 +BRIGHTEST_ALLOWABLE_MAG = 10 -def get_mag_mapping(site): +def get_mag_mapping(site_code): '''Defines the site-specific mappings from target magnitude to desired - exposure time. A null dictionary is returned if the site name isn't - recognized''' + slot length (in minutes). A null dictionary is returned if the site name + isn't recognized''' + + twom_site_codes = ['F65', 'E10'] + good_onem_site_codes = ['V37', 'K91', 'K92', 'K93', 'W85', 'W86', 'W87'] + # COJ normally has bad seeing, allow more time + bad_onem_site_codes = ['Q63', 'Q64'] # Magnitudes represent upper bin limits - site = site.upper() - if site == 'FTX' or site == 'FTS': + site_code = site_code.upper() + if site_code in twom_site_codes: # Mappings for FTN/FTS. Assumes Spectral+Solar filter mag_mapping = { - 19 : 45, - 19.5 : 60, - 20 : 100, - 20.5 : 120, - 21 : 150, - 21.5 : 215, - 22 : 240, - 23.3 : 300 + 19 : 15, + 20 : 20, + 20.5 : 22.5, + 21 : 25, + 21.5 : 27.5, + 22 : 30, + 23.3 : 35 } - elif site == 'SQA': -# Mappings for Sedgwick. Assumes kb18+parfocal clear - mag_mapping = { - 18 : 45, - 19 : 100, - 20 : 150, - 21 : 240, - 21.6 : 300 - } - elif site == 'ELP' or site == 'LSC' or site == 'CPT': + elif site_code in good_onem_site_codes: # Mappings for McDonald. Assumes kb74+w mag_mapping = { - 18 : 45, - 19 : 100, - 20 : 150, - 21 : 240, - 22.0 : 300 + 18 : 15, + 20 : 20, + 20.5 : 22.5, + 21 : 25, + 21.5 : 30, + 22.0 : 40, + 22.5 : 45 + } + elif site_code in bad_onem_site_codes: +# COJ normally has bad seeing, allow more time + mag_mapping = { + 18 : 17.5, + 19.5 : 20, + 20 : 22.5, + 20.5 : 25, + 21 : 27.5, + 21.5 : 32.5, + 22.0 : 35 } else: mag_mapping = {} @@ -704,17 +707,17 @@ def get_mag_mapping(site): return mag_mapping -def mag_to_exptime(site, mag, debug=False): +def determine_slot_length(target_name, mag, site_code, debug=False): if mag < BRIGHTEST_ALLOWABLE_MAG: raise MagRangeError("Target too bright") -# Obtain magnitude->exp. time mapping dictionary - mag_mapping = get_mag_mapping(site) +# Obtain magnitude->slot length mapping dictionary + mag_mapping = get_mag_mapping(site_code) if debug: print mag_mapping - if mag_mapping == {}: return 9999 + if mag_mapping == {}: return 0 - # Derive your tuple from the magnitude-exposure mapping data structure + # Derive your tuple from the magnitude->slot length mapping data structure upper_mags = tuple(sorted(mag_mapping.keys())) for upper_mag in upper_mags: @@ -731,6 +734,43 @@ def estimate_exptime(rate, pixscale=0.304, roundtime=10.0): round_exptime = max(int(exptime/roundtime)*roundtime, 1.0) return (round_exptime, exptime) +def determine_exptime(speed, pixel_scale, max_exp_time=300.0): + (round_exptime, full_exptime) = estimate_exptime(speed, pixel_scale, 5.0) + + if ( round_exptime > max_exp_time ): + logger.debug("Capping exposure time at %.1f seconds (Was %1.f seconds" % \ + (round_exptime, max_exp_time)) + round_exptime = full_exptime = max_exp_time + if ( round_exptime < 10.0 ): +# If under 10 seconds, re-round to nearest half second + (round_exptime, full_exptime) = estimate_exptime(speed, pixel_scale, 0.5) + logger.debug("Estimated exptime=%.1f seconds (%.1f)" % (round_exptime ,full_exptime)) + + return round_exptime + +def determine_exp_time_count(speed, site_code, slot_length_in_mins): + exp_time = None + exp_count = None + + (chk_site_code, setup_overhead, exp_overhead, pixel_scale, ccd_fov, max_exp_time, alt_limit) = get_sitecam_params(site_code) + + exp_time = determine_exptime(speed, pixel_scale, max_exp_time) + + slot_length = slot_length_in_mins * 60.0 + exp_count = int((slot_length - setup_overhead)/(exp_time + exp_overhead)) + if exp_count < 4: + exp_count = 4 + exp_time = (slot_length - setup_overhead - (exp_overhead * float(exp_count))) / exp_count + logger.debug("Reducing exposure time to %.1f secs to allow %d exposures in group" % ( exp_time, exp_count )) + logger.debug("Slot length of %.1f mins (%.1f secs) allows %d x %.1f second exposures" % \ + ( slot_length/60.0, slot_length, exp_count, exp_time)) + if exp_time == None or exp_time <= 0.0 or exp_count < 1: + logger.debug("Invalid exposure count") + exp_time = None + exp_count = None + + return exp_time, exp_count + def compute_score(obj_alt, moon_alt, moon_sep, alt_limit=25.0): '''Simple noddy scoring calculation for choosing best slot''' @@ -875,8 +915,7 @@ def get_sitepos(site_code, dbg=False): site_name = site_name.rstrip() site_long = -site_long - if dbg: print - if dbg: print site_name, site_long, site_lat, site_hgt + logger.debug(site_name, site_long, site_lat, site_hgt) return (site_name, site_long, site_lat, site_hgt) def moon_ra_dec(date, obsvr_long, obsvr_lat, obsvr_hgt, dbg=False): @@ -893,7 +932,7 @@ def moon_ra_dec(date, obsvr_long, obsvr_lat, obsvr_hgt, dbg=False): # Compute Moon's apparent RA, Dec, diameter (all in radians) (moon_ra, moon_dec, diam) = S.sla_rdplan(mjd_tdb, body, obsvr_long, obsvr_lat) - if dbg: print "Moon RA, Dec, diam=", moon_ra, moon_dec, diam + logger.debug("Moon RA, Dec, diam=%s %s %s" % (moon_ra, moon_dec, diam)) return (moon_ra, moon_dec, diam) def atmos_params(airless): @@ -932,11 +971,11 @@ def moon_alt_az(date, moon_app_ra, moon_app_dec, obsvr_long, obsvr_lat,\ # Compute MJD_UTC mjd_utc = datetime2mjd_utc(date) - if dbg: print mjd_utc + logger.debug(mjd_utc) # Compute UT1-UTC dut = ut1_minus_utc(mjd_utc) - if dbg: print dut + logger.debug(dut) # Perform apparent->observed place transformation (obs_az, obs_zd, obs_ha, obs_dec, obs_ra) = S.sla_aop(moon_app_ra, moon_app_dec,\ mjd_utc, dut, obsvr_long, obsvr_lat, obsvr_hgt, xp, yp, \ @@ -948,7 +987,7 @@ def moon_alt_az(date, moon_app_ra, moon_app_dec, obsvr_long, obsvr_lat,\ # due to observers' elevation above sea level) obs_alt = (pi/2.0)-obs_zd - if dbg: print obs_az, obs_zd, obs_alt + logger.debug(obs_az, obs_zd, obs_alt) return (obs_az, obs_alt) def moonphase(date, obsvr_long, obsvr_lat, obsvr_hgt, dbg=False): @@ -960,7 +999,7 @@ def moonphase(date, obsvr_long, obsvr_lat, obsvr_hgt, dbg=False): cosphi = ( sin(sun_dec) * sin(moon_dec) + cos(sun_dec) \ * cos(moon_dec) * cos(sun_ra - moon_ra) ) - if dbg: print "cos(phi)=", cosphi + logger.debug("cos(phi)=%s" % cosphi) # Full formula for phase angle, i. Requires r (Earth-Sun distance) and del(ta) (the # Earth-Moon distance) neither of which we have with our methods. However Meeus @@ -969,7 +1008,7 @@ def moonphase(date, obsvr_long, obsvr_lat, obsvr_hgt, dbg=False): # i = atan2( r * sin(phi), del - r * cos(phi) ) cosi = -cosphi - if dbg: print "cos(i)=", cosi + logger.debug("cos(i)=%s" % cosi) mphase = (1.0 + cosi) / 2.0 return mphase @@ -994,11 +1033,11 @@ def compute_hourangle(date, obsvr_long, obsvr_lat, obsvr_hgt, mean_ra, mean_dec, print 'GMST, LAST, EQEQX, GAST, long=', gmst, stl, S.sla_eqeqx(mjd_tdb), gmst+S.sla_eqeqx(mjd_tdb), obsvr_long (app_ra, app_dec) = S.sla_map(mean_ra, mean_dec, 0.0, 0.0, 0.0, 0.0, 2000.0, mjd_tdb) - if dbg: print app_ra, app_dec, radec2strings(app_ra, app_dec) + logger.debug(app_ra, app_dec, radec2strings(app_ra, app_dec)) hour_angle = stl - app_ra - if dbg: print hour_angle + logger.debug(hour_angle) hour_angle = S.sla_drange(hour_angle) - if dbg: print hour_angle + logger.debug(hour_angle) return hour_angle @@ -1043,9 +1082,9 @@ def get_mountlimits(site_code_or_name): return (ha_neg_limit, ha_pos_limit, alt_limit) -def get_siteparams(site): +def get_sitecam_params(site): '''Translates (e.g. 'FTN') to MPC site code, pixel scale, maximum - exposure time and slot length. + exposure time, setup and exposure overheads. site_code is set to 'XXX' and the others are set to -1 in the event of an unrecognized site.''' @@ -1058,115 +1097,47 @@ def get_siteparams(site): normal_alt_limit = 30.0 twom_alt_limit = 20.0 + onem_exp_overhead = 15.5 + sinistro_exp_overhead = 48.0 + onem_setup_overhead = 120.0 + twom_setup_overhead = 180.0 + twom_exp_overhead = 22.5 + point4m_exp_overhead = 7.5 # for BPL + valid_site_codes = [ 'V37', 'W85', 'W86', 'W87', 'K91', 'K92', 'K93', 'Q63', 'Q64' ] site = site.upper() if site == 'FTN' or 'OGG-CLMA' in site or site == 'F65': site_code = 'F65' - slot_length = 30 + setup_overhead = twom_setup_overhead + exp_overhead = twom_exp_overhead pixel_scale = 0.304 fov = arcmins_to_radians(10.0) max_exp_length = 300.0 alt_limit = twom_alt_limit elif site == 'FTS' or 'COJ-CLMA' in site or site == 'E10': site_code = 'E10' - slot_length = 30 + setup_overhead = twom_setup_overhead + exp_overhead = twom_exp_overhead pixel_scale = 0.304 fov = arcmins_to_radians(10.0) max_exp_length = 300.0 alt_limit = twom_alt_limit - elif 'SQA' in site: - site_code = 'G51' - slot_length = 30 - pixel_scale = 0.57 - fov = arcmins_to_radians(12.0) - max_exp_length = 300.0 - alt_limit = normal_alt_limit - elif 'ELP-DOM' in site: - site_code = 'V37' - slot_length = 35 - pixel_scale = onem_pixscale - fov = arcmins_to_radians(onem_fov) - max_exp_length = 300.0 - alt_limit = normal_alt_limit - elif 'BPL-DOM' in site: - site_code = 'G51' - slot_length = 30 - pixel_scale = onem_sinistro_pixscale - fov = arcmins_to_radians(onem_sinistro_fov) - max_exp_length = 300.0 - alt_limit = normal_alt_limit - elif 'ELP-SITE' in site or 'LSC-SITE' in site or 'CPT-SITE' in site or 'COJ-SITE' in site: -# Overall metasite for Planck observations - pond_site_codes = { 'ELP-SITE' : 'V37', - 'LSC-SITE' : 'W85', - 'CPT-SITE' : 'K91', - 'ELP-SITE-1M0A' : 'V37', - 'LSC-SITE-1M0A' : 'W85', - 'CPT-SITE-1M0A' : 'K91', - 'COJ-SITE-1M0A' : 'Q63', - } - site_code = pond_site_codes.get(site, 'XXX') -# Lookup failed, set to unrecognized - if site_code == 'XXX': - slot_length = pixel_scale = fov = max_exp_length = alt_limit = -1 - else: -# Site code found, set parameters - slot_length = 35.0 - pixel_scale = onem_pixscale - fov = arcmins_to_radians(onem_fov) - if 'LSC-SITE' in site: - pixel_scale = onem_sinistro_pixscale - fov = arcmins_to_radians(onem_sinistro_fov) - max_exp_length = 300.0 - alt_limit = normal_alt_limit -# Used for GAIA -# max_exp_length = 90.0 -# alt_limit = 40.0 - elif 'LSC-DOM' in site or 'CPT-DOM' in site or 'COJ-DOM' in site: - pond_site_codes = { 'LSC-DOMA-1M0A' : 'W85', - 'LSC-DOMB-1M0A' : 'W86', - 'LSC-DOMC-1M0A' : 'W87', - 'CPT-DOMA-1M0A' : 'K91', - 'CPT-DOMB-1M0A' : 'K92', - 'CPT-DOMC-1M0A' : 'K93', - 'COJ-DOMA-1M0A' : 'Q63', - 'COJ-DOMB-1M0A' : 'Q64', - } - site_code = pond_site_codes.get(site, 'XXX') -# Lookup failed, set to unrecognized - if site_code == 'XXX': - slot_length = pixel_scale = fov = max_exp_length = alt_limit = -1 - else: -# Site code found, set parameters - slot_length = 35.0 - pixel_scale = onem_pixscale - fov = arcmins_to_radians(onem_fov) - if 'LSC-DOMB' in site or 'LSC-DOMC' in site: - pixel_scale = onem_sinistro_pixscale - fov = arcmins_to_radians(onem_sinistro_fov) - max_exp_length = 300.0 - alt_limit = normal_alt_limit - elif 'LSC-AQWA-0M4A' in site: - site_code = 'W85' # XXX Wrong - slot_length = 30 - pixel_scale = point4m_pixscale - fov = arcmins_to_radians(point4m_fov) - max_exp_length = 300.0 - alt_limit = 17.0 elif site in valid_site_codes: - slot_length = 25 + setup_overhead = onem_setup_overhead + exp_overhead = onem_exp_overhead pixel_scale = onem_pixscale fov = arcmins_to_radians(onem_fov) if 'W86' in site or 'W87' in site: pixel_scale = onem_sinistro_pixscale fov = arcmins_to_radians(onem_sinistro_fov) + exp_overhead = sinistro_exp_overhead max_exp_length = 300.0 alt_limit = normal_alt_limit site_code = site else: # Unrecognized site site_code = 'XXX' - slot_length = pixel_scale = fov = max_exp_length = alt_limit = -1 + setup_overhead = exp_overhead = pixel_scale = fov = max_exp_length = alt_limit = -1 - return (site_code, slot_length, pixel_scale, fov, max_exp_length, alt_limit) + return (site_code, setup_overhead, exp_overhead, pixel_scale, fov, max_exp_length, alt_limit) diff --git a/neoexchange/ingest/sources_subs.py b/neoexchange/astrometrics/sources_subs.py similarity index 74% rename from neoexchange/ingest/sources_subs.py rename to neoexchange/astrometrics/sources_subs.py index b7af1dc9d..6af86f927 100644 --- a/neoexchange/ingest/sources_subs.py +++ b/neoexchange/astrometrics/sources_subs.py @@ -17,6 +17,9 @@ import urllib2, os from bs4 import BeautifulSoup from datetime import datetime +from reqdb.requests import Request, UserRequest +from reqdb.client import SchedulerClient + from re import sub import logging logger = logging.getLogger(__name__) @@ -487,3 +490,174 @@ def fetch_goldstone_targets(dbg=False): radar_objects.append(obj_id) last_year_seen = year return radar_objects + +def make_location(params): + location = { + 'telescope_class' : params['pondtelescope'][0:3], + 'site' : params['site'], + 'observatory' : params['observatory'], + 'telescope' : '', + } + +# Check if the 'pondtelescope' is length 4 (1m0a) rather than length 3, and if +# so, update the null string set above with a proper telescope + if len(params['pondtelescope']) == 4: + location['telescope'] = params['pondtelescope'] + + return location + +def make_target(params): + '''Make a target dictionary for the request. RA and Dec need to be + decimal degrees''' + + ra_degs = math.degrees(params['ra_rad']) + dec_degs = math.degrees(params['dec_rad']) + target = { + 'name' : params['source_id'], + 'ra' : ra_degs, + 'dec' : dec_degs + } + return target + +def make_moving_target(elements): + '''Make a target dictionary for the request from an element set''' + + print elements + # Generate initial dictionary of things in common + target = { + 'name' : elements['provisional_name'], + 'type' : 'NON_SIDEREAL', + 'scheme' : elements['elements_type'], + # Moving object param + 'epochofel' : elements['epochofel_mjd'], + 'orbinc' : elements['orbinc'], + 'longascnode' : elements['longascnode'], + 'argofperih' : elements['argofperih'], + 'eccentricity' : elements['eccentricity'], + } + + if elements['elements_type'].upper() == 'MPC_COMET': + target['epochofperih'] = elements['epochofperih'] + target['perihdist'] = elements['perihdist'] + else: + target['meandist'] = elements['meandist'] + target['meananom'] = elements['meananom'] + + return target + +def make_window(params): + '''Make a window. This is simply set to the start and end time from + params (i.e. the picked time with the best score plus the block length), + formatted into a string. + Hopefully this will prevent rescheduling at a different time as the + co-ords will be wrong in that case...''' + window = { + 'start' : params['start_time'].strftime('%Y-%m-%dT%H:%M:%S'), + 'end' : params['end_time'].strftime('%Y-%m-%dT%H:%M:%S'), + } + + return window + +def make_molecule(params): + molecule = { + 'exposure_count' : params['exp_count'], + 'exposure_time' : params['exp_time'], + 'bin_x' : params['binning'], + 'bin_y' : params['binning'], + 'instrument_name' : params['instrument'], + 'filter' : params['filter'], + 'ag_mode' : 'Optional', # 0=On, 1=Off, 2=Optional. Default is 2. + 'ag_name' : '' + + } + return molecule + +def make_proposal(params): + '''Construct needed proposal info''' + + proposal = { + 'proposal_id' : params['proposal_code'], + 'user_id' : params['user_id'], + 'tag_id' : params['tag_id'], + 'priority' : params['priority'], + } + return proposal + +def make_constraints(params): + constraints = { +# 'max_airmass' : 2.0, # 30 deg altitude (The maximum airmass you are willing to accept) + 'max_airmass' : 1.74, # 35 deg altitude (The maximum airmass you are willing to accept) +# 'max_airmass' : 1.55, # 40 deg altitude (The maximum airmass you are willing to accept) +# 'max_airmass' : 2.37, # 25 deg altitude (The maximum airmass you are willing to accept) + } + return constraints + +def configure_defaults(params): + + site_list = { 'V37' : 'ELP' , 'K92' : 'CPT', 'COJ' : 'Q63', 'W86' : 'LSC', 'F65' : 'OGG', 'E10' : 'COJ' } + params['pondtelescope'] = '1m0' + params['observatory'] = '' + params['site'] = site_list[params['site_code']] + params['binning'] = 2 + params['instrument'] = '1M0-SCICAM-SBIG' + params['filter'] = 'w' + + if params['site_code'] == 'W86' or params['site_code'] == 'W87': + params['binning'] = 1 + params['instrument'] = '1M0-SCICAM-SINISTRO' + elif params['site_code'] == 'F65' or params['site_code'] == 'E10': + params['instrument'] = '2M0-SCICAM-SPECTRAL' + params['filter'] = 'solar' + params['user_id'] = 'tlister@lcogt.net' + params['tag_id'] = 'LCOGT' + params['priority'] = 15 + + return params + +def submit_block_to_scheduler(elements, params): + request = Request() + + params = configure_defaults(params) +# Create Location (site, observatory etc) and add to Request + location = make_location(params) + logger.debug("Location=%s" % location) + request.set_location(location) +# Create Target (pointing) and add to Request + if len(elements) > 0: + logger.debug("Making a moving object") + target = make_moving_target(elements) + else: + logger.debug("Making a static object") + target = make_target(params) + logger.debug("Target=%s" % target) + request.set_target(target) +# Create Window and add to Request + window = make_window(params) + logger.debug("Window=%s" % window) + request.add_window(window) +# Create Molecule and add to Request + molecule = make_molecule(params) + request.add_molecule(molecule) # add exposure to the request + request.set_note('Submitted by NEOexchange') + logger.debug("Request=%s" % request) + + constraints = make_constraints(params) + request.set_constraints(constraints) + +# Add the Request to the outer User Request + user_request = UserRequest(group_id=params['group_id']) + user_request.add_request(request) + user_request.operator = 'single' + proposal = make_proposal(params) + user_request.set_proposal(proposal) + +# Make an endpoint and submit the thing + client = SchedulerClient('http://scheduler1.lco.gtn/requestdb/') + response_data = client.submit(user_request) + client.print_submit_response() + request_numbers = response_data.get('request_numbers', '') +# request_numbers = (-42,) + request_number = request_numbers[0] + logger.debug("Req number=%s" % request_number) + + return request_number diff --git a/neoexchange/ingest/tests/__init__.py b/neoexchange/astrometrics/tests/__init__.py similarity index 78% rename from neoexchange/ingest/tests/__init__.py rename to neoexchange/astrometrics/tests/__init__.py index d8090a0f9..8c9056769 100644 --- a/neoexchange/ingest/tests/__init__.py +++ b/neoexchange/astrometrics/tests/__init__.py @@ -1,4 +1,3 @@ from test_ast_subs import * from test_ephem_subs import * from test_source_subs import * -from test_views import * diff --git a/neoexchange/ingest/tests/test_ast_subs.py b/neoexchange/astrometrics/tests/test_ast_subs.py similarity index 74% rename from neoexchange/ingest/tests/test_ast_subs.py rename to neoexchange/astrometrics/tests/test_ast_subs.py index d095f5da9..f009905ed 100644 --- a/neoexchange/ingest/tests/test_ast_subs.py +++ b/neoexchange/astrometrics/tests/test_ast_subs.py @@ -17,7 +17,7 @@ from django.test import TestCase #Import module to test -from ingest.ast_subs import * +from astrometrics.ast_subs import * class TestIntToMutantHexChar(TestCase): @@ -232,3 +232,90 @@ def test_baddesig_t1(self): packed_desig, ret_code = normal_to_packed('12345 A') self.assertEqual(packed_desig, expected_desig) self.assertEqual(ret_code, -1) + +class TestDetermineAsteroidType(TestCase): + + def test_neo1(self): + expected_type = 'N' + obj_type = determine_asteroid_type(1.0, 0.1) + self.assertEqual(obj_type, expected_type) + + def test_neo2(self): + expected_type = 'N' + obj_type = determine_asteroid_type(1.299, 0.4) + self.assertEqual(obj_type, expected_type) + + def test_neo3(self): + expected_type = 'N' + obj_type = determine_asteroid_type(1.3, 0.99) + self.assertEqual(obj_type, expected_type) + + def test_nonneo1(self): + expected_type = 'A' + obj_type = determine_asteroid_type(1.301, 0.1) + self.assertEqual(obj_type, expected_type) + + def test_nonneo2(self): + expected_type = 'A' + obj_type = determine_asteroid_type(1.301, 0.9) + self.assertEqual(obj_type, expected_type) + + def test_comet1(self): + expected_type = 'C' + obj_type = determine_asteroid_type(0.301, 0.9991) + self.assertEqual(obj_type, expected_type) + + def test_comet2(self): + expected_type = 'C' + obj_type = determine_asteroid_type(1.301, 1.0000) + self.assertEqual(obj_type, expected_type) + + def test_comet3(self): + expected_type = 'C' + obj_type = determine_asteroid_type(5.51, 1.00001) + self.assertEqual(obj_type, expected_type) + + def test_centaur1(self): + expected_type = 'E' + obj_type = determine_asteroid_type(5.51, 0.1) + self.assertEqual(obj_type, expected_type) + + def test_centaur2(self): + expected_type = 'E' + obj_type = determine_asteroid_type(5.5, 0.817275747508) + self.assertEqual(obj_type, expected_type) + + def test_centaur3(self): + expected_type = 'E' + obj_type = determine_asteroid_type(30.099, 0.0) + self.assertEqual(obj_type, expected_type) + + def test_centaur4(self): + expected_type = 'A' + obj_type = determine_asteroid_type(5.49, 0.82) + self.assertEqual(obj_type, expected_type) + + def test_kbo1(self): + expected_type = 'K' + obj_type = determine_asteroid_type(30.1001, 0.0) + self.assertEqual(obj_type, expected_type) + + def test_kbo2(self): + expected_type = 'K' + obj_type = determine_asteroid_type(42, 0.998) + self.assertEqual(obj_type, expected_type) + + def test_trojan1(self): + expected_type = 'T' + obj_type = determine_asteroid_type(5.05, 0.0) + self.assertEqual(obj_type, expected_type) + + def test_trojan2(self): + expected_type = 'T' + obj_type = determine_asteroid_type(5.05, 0.05607) + self.assertEqual(obj_type, expected_type) + + def test_trojan3(self): + expected_type = 'A' + obj_type = determine_asteroid_type(5.05, 0.0561) + self.assertEqual(obj_type, expected_type) diff --git a/neoexchange/astrometrics/tests/test_ephem_subs.py b/neoexchange/astrometrics/tests/test_ephem_subs.py new file mode 100644 index 000000000..1103c5883 --- /dev/null +++ b/neoexchange/astrometrics/tests/test_ephem_subs.py @@ -0,0 +1,579 @@ +''' +NEO exchange: NEO observing portal for Las Cumbres Observatory Global Telescope Network +Copyright (C) 2014-2015 LCOGT + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. +''' + +from datetime import datetime +from django.test import TestCase +from django.forms.models import model_to_dict +from rise_set.angle import Angle +from math import radians + +#Import module to test +from astrometrics.ephem_subs import * +from core.models import Body + + +class TestGetMountLimits(TestCase): + + def compare_limits(self, pos_limit, neg_limit, alt_limit, tel_class): + if tel_class.lower() == '2m': + ha_pos_limit = 12.0 * 15.0 + ha_neg_limit = -12.0 * 15.0 + altitude_limit = 25.0 + elif tel_class.lower() == '1m': + ha_pos_limit = 4.5 * 15.0 + ha_neg_limit = -4.5 * 15.0 + altitude_limit = 30.0 + else: + self.Fail("Unknown telescope class:", tel_class) + self.assertEqual(ha_pos_limit, pos_limit) + self.assertEqual(ha_neg_limit, neg_limit) + self.assertEqual(altitude_limit, alt_limit) + + def test_2m_by_site(self): + (neg_limit, pos_limit, alt_limit) = get_mountlimits('OGG-CLMA-2M0A') + self.compare_limits(pos_limit, neg_limit, alt_limit, '2m') + + def test_2m_by_site_code(self): + (neg_limit, pos_limit, alt_limit) = get_mountlimits('F65') + self.compare_limits(pos_limit, neg_limit, alt_limit, '2m') + + def test_2m_by_site_code_lowercase(self): + (neg_limit, pos_limit, alt_limit) = get_mountlimits('f65') + self.compare_limits(pos_limit, neg_limit, alt_limit, '2m') + + def test_1m_by_site(self): + (neg_limit, pos_limit, alt_limit) = get_mountlimits('ELP-DOMA-1m0A') + self.compare_limits(pos_limit, neg_limit, alt_limit, '1m') + + def test_1m_by_site_code(self): + (neg_limit, pos_limit, alt_limit) = get_mountlimits('K91') + self.compare_limits(pos_limit, neg_limit, alt_limit, '1m') + + def test_1m_by_site_code_lowercase(self): + (neg_limit, pos_limit, alt_limit) = get_mountlimits('q63') + self.compare_limits(pos_limit, neg_limit, alt_limit, '1m') + + +class TestComputeEphem(TestCase): + + def setUp(self): + params = { 'provisional_name' : 'N999r0q', + 'abs_mag' : 21.0, + 'slope' : 0.15, + 'epochofel' : '2015-03-19 00:00:00', + 'meananom' : 325.2636, + 'argofperih' : 85.19251, + 'longascnode' : 147.81325, + 'orbinc' : 8.34739, + 'eccentricity' : 0.1896865, + 'meandist' : 1.2176312, + 'source_type' : 'U', + 'elements_type' : 'MPC_MINOR_PLANET', + 'active' : True, + 'origin' : 'M', + } + self.body, created = Body.objects.get_or_create(**params) + + self.elements = {'G': 0.15, + 'H': 21.0, + 'MDM': Angle(degrees=0.74394528), + 'arg_perihelion': Angle(degrees=85.19251), + 'eccentricity': 0.1896865, + 'epoch': 57100.0, + 'inclination': Angle(degrees=8.34739), + 'long_node': Angle(degrees=147.81325), + 'mean_anomaly': Angle(degrees=325.2636), + 'n_nights': 3, + 'n_obs': 17, + 'n_oppos': 1, + 'name': 'N007r0q', + 'reference': '', + 'residual': 0.53, + 'semi_axis': 1.2176312, + 'type': 'MPC_MINOR_PLANET', + 'uncertainty': 'U'} + + def test_body_is_correct_class(self): + tbody = Body.objects.get(provisional_name='N999r0q') + self.assertIsInstance(tbody, Body) + + def test_save_and_retrieve_bodies(self): + first_body = Body.objects.get(provisional_name='N999r0q') + body_dict = model_to_dict(first_body) + + body_dict['provisional_name'] = 'N999z0z' + body_dict['eccentricity'] = 0.42 + body_dict['id'] += 1 + second_body = Body.objects.create(**body_dict) + second_body.save() + + saved_items = Body.objects.all() + self.assertEqual(saved_items.count(), 2) + + first_saved_item = saved_items[0] + second_saved_item = saved_items[1] + self.assertEqual(first_saved_item.provisional_name, 'N999r0q') + self.assertEqual(second_saved_item.provisional_name, 'N999z0z') + + def test_compute_ephem_with_elements(self): + d = datetime(2015, 4, 21, 17, 35, 00) + expected_ra = 5.28722753669144 + expected_dec = 0.522637696108887 + expected_mag = 20.408525362626005 + expected_motion = 2.4825093417658186 + expected_alt = -58.658929026981895 + emp_line = compute_ephem(d, self.elements, '?', dbg=False, perturb=True, display=False) + self.assertEqual(d, emp_line[0]) + precision = 11 + self.assertAlmostEqual(expected_ra, emp_line[1], precision) + self.assertAlmostEqual(expected_dec, emp_line[2], precision) + self.assertAlmostEqual(expected_mag, emp_line[3], precision) + self.assertAlmostEqual(expected_motion, emp_line[4], precision) + self.assertAlmostEqual(expected_alt, emp_line[5], precision) + + def test_compute_ephem_with_body(self): + d = datetime(2015, 4, 21, 17, 35, 00) + expected_ra = 5.28722753669144 + expected_dec = 0.522637696108887 + expected_mag = 20.408525362626005 + expected_motion = 2.4825093417658186 + expected_alt = -58.658929026981895 + body_elements = model_to_dict(self.body) + emp_line = compute_ephem(d, body_elements, '?', dbg=False, perturb=True, display=False) + self.assertEqual(d, emp_line[0]) + precision = 11 + self.assertAlmostEqual(expected_ra, emp_line[1], precision) + self.assertAlmostEqual(expected_dec, emp_line[2], precision) + self.assertAlmostEqual(expected_mag, emp_line[3], precision) + self.assertAlmostEqual(expected_motion, emp_line[4], precision) + self.assertAlmostEqual(expected_alt, emp_line[5], precision) + + def test_call_compute_ephem_with_body(self): + start = datetime(2015, 4, 21, 8, 45, 00) + end = datetime(2015, 4, 21, 8, 51, 00) + site_code = 'V37' + step_size = 300 + body_elements = model_to_dict(self.body) + expected_ephem_lines = [['2015 04 21 08:45', '20 10 05.99', '+29 56 57.5', '20.4', ' 2.43', '+33', '0.09', '107', '-42', '+047', '-04:25'], + ['2015 04 21 08:50', '20 10 06.92', '+29 56 57.7', '20.4', ' 2.42', '+34', '0.09', '107', '-42', '+048', '-04:20']] + ephem_lines = call_compute_ephem(body_elements, start, end, site_code, step_size) + line = 0 + self.assertEqual(len(expected_ephem_lines), len(ephem_lines)) + while line < len(expected_ephem_lines): + self.assertEqual(expected_ephem_lines[line], ephem_lines[line]) + line += 1 + + def test_call_compute_ephem_with_body_F65(self): + start = datetime(2015, 4, 21, 11, 30, 00) + end = datetime(2015, 4, 21, 11, 35, 01) + site_code = 'F65' + step_size = 300 + body_elements = model_to_dict(self.body) + expected_ephem_lines = [['2015 04 21 11:30', '20 10 38.15', '+29 56 52.1', '20.4', ' 2.45', '+20', '0.09', '108', '-47', '-999', '-05:09'], + ['2015 04 21 11:35', '20 10 39.09', '+29 56 52.4', '20.4', ' 2.45', '+21', '0.09', '108', '-48', '-999', '-05:04']] + + ephem_lines = call_compute_ephem(body_elements, start, end, site_code, step_size) + line = 0 + self.assertEqual(len(expected_ephem_lines), len(ephem_lines)) + while line < len(expected_ephem_lines): + self.assertEqual(expected_ephem_lines[line], ephem_lines[line]) + line += 1 + + def test_call_compute_ephem_with_date(self): + start = datetime(2015, 4, 28, 10, 20, 00) + end = datetime(2015, 4, 28, 10, 25, 01) + site_code = 'V37' + step_size = 300 + body_elements = model_to_dict(self.body) + expected_ephem_lines = [['2015 04 28 10:20', '20 40 36.53', '+29 36 33.1', '20.6', ' 2.08', '+52', '0.72', '136', '-15', '+058', '-02:53'], + ['2015 04 28 10:25', '20 40 37.32', '+29 36 32.5', '20.6', ' 2.08', '+54', '0.72', '136', '-16', '+059', '-02:48']] + + ephem_lines = call_compute_ephem(body_elements, start, end, site_code, step_size) + line = 0 + self.assertEqual(len(expected_ephem_lines), len(ephem_lines)) + while line < len(expected_ephem_lines): + self.assertEqual(expected_ephem_lines[line], ephem_lines[line]) + line += 1 + + def test_call_compute_ephem_with_altlimit(self): + start = datetime(2015, 9, 3, 17, 20, 00) + end = datetime(2015, 9, 3, 19, 40, 01) + site_code = 'K91' + step_size = 300 + alt_limit = 30 + body_elements = model_to_dict(self.body) + expected_ephem_lines = [['2015 09 03 19:35', '23 53 33.81', '-12 45 53.8', '19.3', ' 1.87', '+30', '0.67', ' 57', '-26', '+039', '-04:05'], + ['2015 09 03 19:40', '23 53 33.46', '-12 46 01.5', '19.3', ' 1.87', '+32', '0.67', ' 58', '-25', '+040', '-04:00']] + + ephem_lines = call_compute_ephem(body_elements, start, end, + site_code, step_size, alt_limit) + line = 0 + self.assertEqual(len(expected_ephem_lines), len(ephem_lines)) + while line < len(expected_ephem_lines): + self.assertEqual(expected_ephem_lines[line], ephem_lines[line]) + line += 1 + +class TestDetermineSlotLength(TestCase): + + def test_bad_site_code(self): + site_code = 'foo' + name = 'WH2845B' + mag = 17.58 + expected_length = 0 + slot_length = determine_slot_length(name, mag, site_code) + self.assertEqual(expected_length, slot_length) + + def test_slot_length_very_bright_nonNEOWISE_good1m_lc(self): + site_code = 'k91' + name = 'WH2845B' + mag = 17.58 + expected_length = 15 + slot_length = determine_slot_length(name, mag, site_code) + self.assertEqual(expected_length, slot_length) + + def test_slot_length_very_bright_nonNEOWISE_good1m(self): + site_code = 'K91' + name = 'WH2845B' + mag = 17.58 + expected_length = 15 + slot_length = determine_slot_length(name, mag, site_code) + self.assertEqual(expected_length, slot_length) + + def test_slot_length_bright_nonNEOWISE_good1m(self): + site_code = 'K92' + name = 'WH2845B' + mag = 19.9 + expected_length = 20 + slot_length = determine_slot_length(name, mag, site_code) + self.assertEqual(expected_length, slot_length) + + def test_slot_length_medium_nonNEOWISE_good1m(self): + site_code = 'K93' + name = 'WH2845B' + mag = 20.1 + expected_length = 22.5 + slot_length = determine_slot_length(name, mag, site_code) + self.assertEqual(expected_length, slot_length) + + def test_slot_length_mediumfaint_nonNEOWISE_good1m(self): + site_code = 'V37' + name = 'WH2845B' + mag = 20.6 + expected_length = 25 + slot_length = determine_slot_length(name, mag, site_code) + self.assertEqual(expected_length, slot_length) + + def test_slot_length_faint_nonNEOWISE_good1m(self): + site_code = 'W85' + name = 'WH2845B' + mag = 21.0 + expected_length = 30 + slot_length = determine_slot_length(name, mag, site_code) + self.assertEqual(expected_length, slot_length) + + def test_slot_length_veryfaint_nonNEOWISE_good1m(self): + site_code = 'W86' + name = 'WH2845B' + mag = 21.51 + expected_length = 40 + slot_length = determine_slot_length(name, mag, site_code) + self.assertEqual(expected_length, slot_length) + + def test_slot_length_reallyfaint_nonNEOWISE_good1m(self): + site_code = 'W87' + name = 'WH2845B' + mag = 22.1 + expected_length = 45 + slot_length = determine_slot_length(name, mag, site_code) + self.assertEqual(expected_length, slot_length) + + def test_slot_length_toofaint_nonNEOWISE_good1m(self): + site_code = 'W87' + name = 'WH2845B' + mag = 23.1 + with self.assertRaises(MagRangeError): + slot_length = determine_slot_length(name, mag, site_code) + + def test_slot_length_toobright_nonNEOWISE_good1m(self): + site_code = 'W87' + name = 'WH2845B' + mag = 3.1 + with self.assertRaises(MagRangeError): + slot_length = determine_slot_length(name, mag, site_code) + + def test_slot_length_very_bright_nonNEOWISE_bad1m(self): + site_code = 'Q63' + name = 'WH2845B' + mag = 17.58 + expected_length = 17.5 + slot_length = determine_slot_length(name, mag, site_code) + self.assertEqual(expected_length, slot_length) + + def test_slot_length_bright_nonNEOWISE_bad1m(self): + site_code = 'Q63' + name = 'WH2845B' + mag = 19.9 + expected_length = 22.5 + slot_length = determine_slot_length(name, mag, site_code) + self.assertEqual(expected_length, slot_length) + + def test_slot_length_medium_nonNEOWISE_bad1m(self): + site_code = 'Q64' + name = 'WH2845B' + mag = 20.1 + expected_length = 25 + slot_length = determine_slot_length(name, mag, site_code) + self.assertEqual(expected_length, slot_length) + + def test_slot_length_mediumfaint_nonNEOWISE_bad1m(self): + site_code = 'Q63' + name = 'WH2845B' + mag = 20.6 + expected_length = 27.5 + slot_length = determine_slot_length(name, mag, site_code) + self.assertEqual(expected_length, slot_length) + + def test_slot_length_faint_nonNEOWISE_bad1m(self): + site_code = 'Q63' + name = 'WH2845B' + mag = 21.0 + expected_length = 32.5 + slot_length = determine_slot_length(name, mag, site_code) + self.assertEqual(expected_length, slot_length) + + def test_slot_length_veryfaint_nonNEOWISE_bad1m(self): + site_code = 'Q64' + name = 'WH2845B' + mag = 21.51 + expected_length = 35 + slot_length = determine_slot_length(name, mag, site_code) + self.assertEqual(expected_length, slot_length) + + def test_slot_length_toofaint_for_coj_nonNEOWISE_bad1m(self): + site_code = 'Q64' + name = 'WH2845B' + mag = 22.1 + with self.assertRaises(MagRangeError): + slot_length = determine_slot_length(name, mag, site_code) + + def test_slot_length_toofaint_nonNEOWISE_bad1m(self): + site_code = 'Q64' + name = 'WH2845B' + mag = 23.1 + with self.assertRaises(MagRangeError): + slot_length = determine_slot_length(name, mag, site_code) + + def test_slot_length_toobright_nonNEOWISE_bad1m(self): + site_code = 'Q64' + name = 'WH2845B' + mag = 3.1 + with self.assertRaises(MagRangeError): + slot_length = determine_slot_length(name, mag, site_code) + + def test_slot_length_very_bright_nonNEOWISE_2m_lc(self): + site_code = 'f65' + name = 'WH2845B' + mag = 17.58 + expected_length = 15 + slot_length = determine_slot_length(name, mag, site_code) + self.assertEqual(expected_length, slot_length) + + def test_slot_length_very_bright_nonNEOWISE_2m(self): + site_code = 'E10' + name = 'WH2845B' + mag = 17.58 + expected_length = 15 + slot_length = determine_slot_length(name, mag, site_code) + self.assertEqual(expected_length, slot_length) + + def test_slot_length_bright_nonNEOWISE_2m(self): + site_code = 'E10' + name = 'WH2845B' + mag = 19.9 + expected_length = 20 + slot_length = determine_slot_length(name, mag, site_code) + self.assertEqual(expected_length, slot_length) + + def test_slot_length_medium_nonNEOWISE_2m(self): + site_code = 'E10' + name = 'WH2845B' + mag = 20.1 + expected_length = 22.5 + slot_length = determine_slot_length(name, mag, site_code) + self.assertEqual(expected_length, slot_length) + + def test_slot_length_mediumfaint_nonNEOWISE_2m(self): + site_code = 'F65' + name = 'WH2845B' + mag = 20.6 + expected_length = 25 + slot_length = determine_slot_length(name, mag, site_code) + self.assertEqual(expected_length, slot_length) + + def test_slot_length_faint_nonNEOWISE_2m(self): + site_code = 'F65' + name = 'WH2845B' + mag = 21.0 + expected_length = 27.5 + slot_length = determine_slot_length(name, mag, site_code) + self.assertEqual(expected_length, slot_length) + + def test_slot_length_veryfaint_nonNEOWISE_2m(self): + site_code = 'F65' + name = 'WH2845B' + mag = 21.51 + expected_length = 30 + slot_length = determine_slot_length(name, mag, site_code) + self.assertEqual(expected_length, slot_length) + + def test_slot_length_reallyfaint_nonNEOWISE_2m(self): + site_code = 'F65' + name = 'WH2845B' + mag = 23.2 + expected_length = 35 + slot_length = determine_slot_length(name, mag, site_code) + self.assertEqual(expected_length, slot_length) + + def test_slot_length_toofaint_nonNEOWISE_2m(self): + site_code = 'F65' + name = 'WH2845B' + mag = 23.4 + with self.assertRaises(MagRangeError): + slot_length = determine_slot_length(name, mag, site_code) + + def test_slot_length_toobright_nonNEOWISE_2m(self): + site_code = 'F65' + name = 'WH2845B' + mag = 3.1 + with self.assertRaises(MagRangeError): + slot_length = determine_slot_length(name, mag, site_code) + +class TestGetSiteCamParams(TestCase): + + twom_setup_overhead = 180.0 + twom_exp_overhead = 22.5 + twom_fov = radians(10.0/60.0) + onem_sbig_fov = radians(15.5/60.0) + onem_setup_overhead = 120.0 + onem_exp_overhead = 15.5 + sinistro_exp_overhead = 48.0 + onem_sinistro_fov = radians(26.4/60.0) + max_exp = 300.0 + + def test_bad_site(self): + site_code = 'wibble' + chk_site_code, setup_overhead, exp_overhead, pixel_scale, ccd_fov, max_exp_time, alt_limit = get_sitecam_params(site_code) + self.assertEqual('XXX', chk_site_code) + self.assertEqual(-1, pixel_scale) + self.assertEqual(-1, max_exp_time) + self.assertEqual(-1, setup_overhead) + self.assertEqual(-1, exp_overhead) + + def test_2m_site(self): + site_code = 'f65' + chk_site_code, setup_overhead, exp_overhead, pixel_scale, ccd_fov, max_exp_time, alt_limit = get_sitecam_params(site_code) + self.assertEqual(site_code.upper(), chk_site_code) + self.assertEqual(0.304, pixel_scale) + self.assertEqual(self.twom_fov, ccd_fov) + self.assertEqual(self.max_exp, max_exp_time) + self.assertEqual(self.twom_setup_overhead, setup_overhead) + self.assertEqual(self.twom_exp_overhead, exp_overhead) + + def test_1m_site_sbig(self): + site_code = 'V37' + chk_site_code, setup_overhead, exp_overhead, pixel_scale, ccd_fov, max_exp_time, alt_limit = get_sitecam_params(site_code) + self.assertEqual(site_code.upper(), chk_site_code) + self.assertEqual(0.464, pixel_scale) + self.assertEqual(self.onem_sbig_fov, ccd_fov) + self.assertEqual(self.onem_setup_overhead, setup_overhead) + self.assertEqual(self.onem_exp_overhead, exp_overhead) + self.assertEqual(self.max_exp, max_exp_time) + + def test_1m_site_sinistro(self): + site_code = 'W86' + chk_site_code, setup_overhead, exp_overhead, pixel_scale, ccd_fov, max_exp_time, alt_limit = get_sitecam_params(site_code) + self.assertEqual(site_code.upper(), chk_site_code) + self.assertEqual(0.389, pixel_scale) + self.assertEqual(self.onem_sinistro_fov, ccd_fov) + self.assertEqual(self.max_exp, max_exp_time) + self.assertEqual(self.onem_setup_overhead, setup_overhead) + self.assertEqual(self.sinistro_exp_overhead, exp_overhead) + +class TestDetermineExpTimeCount(TestCase): + + def test_slow_1m(self): + speed = 2.52 + site_code = 'V37' + slot_len = 22.5 + + expected_exptime = 50.0 + expected_expcount = 18 + + exp_time, exp_count = determine_exp_time_count(speed, site_code, slot_len) + + self.assertEqual(expected_exptime, exp_time) + self.assertEqual(expected_expcount, exp_count) + + def test_fast_1m(self): + speed = 23.5 + site_code = 'K91' + slot_len = 20 + + expected_exptime = 5.5 + expected_expcount = 51 + + exp_time, exp_count = determine_exp_time_count(speed, site_code, slot_len) + + self.assertEqual(expected_exptime, exp_time) + self.assertEqual(expected_expcount, exp_count) + + def test_superslow_1m(self): + speed = 0.235 + site_code = 'W85' + slot_len = 20 + + expected_exptime = 254.5 + expected_expcount = 4 + + exp_time, exp_count = determine_exp_time_count(speed, site_code, slot_len) + + self.assertEqual(expected_exptime, exp_time) + self.assertEqual(expected_expcount, exp_count) + + def test_superfast_2m(self): + speed = 1800.0 + site_code = 'E10' + slot_len = 15 + + expected_exptime = 1.0 + expected_expcount = 30 + + exp_time, exp_count = determine_exp_time_count(speed, site_code, slot_len) + + self.assertEqual(expected_exptime, exp_time) + self.assertEqual(expected_expcount, exp_count) + + def test_block_too_short(self): + speed = 0.18 + site_code = 'F65' + slot_len = 2 + + expected_exptime = None + expected_expcount = None + + exp_time, exp_count = determine_exp_time_count(speed, site_code, slot_len) + + self.assertEqual(expected_exptime, exp_time) + self.assertEqual(expected_expcount, exp_count) diff --git a/neoexchange/ingest/tests/test_source_subs.py b/neoexchange/astrometrics/tests/test_source_subs.py similarity index 54% rename from neoexchange/ingest/tests/test_source_subs.py rename to neoexchange/astrometrics/tests/test_source_subs.py index 1297dfc47..774de5372 100644 --- a/neoexchange/ingest/tests/test_source_subs.py +++ b/neoexchange/astrometrics/tests/test_source_subs.py @@ -14,9 +14,14 @@ ''' from django.test import TestCase +from django.forms.models import model_to_dict +from core.models import Body +from datetime import datetime, timedelta +from unittest import skipIf +from astrometrics.ephem_subs import determine_darkness_times #Import module to test -from ingest.sources_subs import parse_goldstone_chunks +from astrometrics.sources_subs import parse_goldstone_chunks, submit_block_to_scheduler class TestGoldstoneChunkParser(TestCase): @@ -57,3 +62,44 @@ def test_unspecficdate_named_ast(self): chunks = ['2016', 'Jan', '1685', 'Toro', 'No', 'No', 'R'] obj_id = parse_goldstone_chunks(chunks) self.assertEqual(expected_objid, obj_id) + +class TestSubmitBlockToScheduler(TestCase): + + def setUp(self): + params = { 'provisional_name' : 'N999r0q', + 'abs_mag' : 21.0, + 'slope' : 0.15, + 'epochofel' : datetime(2015,03,19,00,00,00), + 'meananom' : 325.2636, + 'argofperih' : 85.19251, + 'longascnode' : 147.81325, + 'orbinc' : 8.34739, + 'eccentricity' : 0.1896865, + 'meandist' : 1.2176312, + 'source_type' : 'U', + 'elements_type' : 'MPC_MINOR_PLANET', + 'active' : True, + 'origin' : 'M', + } + self.body, created = Body.objects.get_or_create(**params) + + @skipIf(True, "needs mocking, submits to real scheduler") + def test_submit_body_for_cpt(self): + + body_elements = model_to_dict(self.body) + body_elements['epochofel_mjd'] = self.body.epochofel_mjd() + body_elements['current_name'] = self.body.current_name() + site_code = 'K92' + utc_date = datetime.now()+timedelta(days=1) + dark_start, dark_end = determine_darkness_times(site_code, utc_date) + params = { 'proposal_code' : 'LCO2015A-009', + 'exp_count' : 18, + 'exp_time' : 50.0, + 'site_code' : site_code, + 'start_time' : dark_start, + 'end_time' : dark_end, + 'group_id' : body_elements['current_name'] + '_' + 'CPT' + '-' + datetime.strftime(utc_date, '%Y%m%d') + + } + + request_number = submit_block_to_scheduler(body_elements, params) diff --git a/neoexchange/ingest/time_subs.py b/neoexchange/astrometrics/time_subs.py similarity index 100% rename from neoexchange/ingest/time_subs.py rename to neoexchange/astrometrics/time_subs.py diff --git a/neoexchange/ingest/management/__init__.py b/neoexchange/core/__init__.py similarity index 100% rename from neoexchange/ingest/management/__init__.py rename to neoexchange/core/__init__.py diff --git a/neoexchange/ingest/admin.py b/neoexchange/core/admin.py similarity index 100% rename from neoexchange/ingest/admin.py rename to neoexchange/core/admin.py diff --git a/neoexchange/core/forms.py b/neoexchange/core/forms.py new file mode 100644 index 000000000..539d20fc6 --- /dev/null +++ b/neoexchange/core/forms.py @@ -0,0 +1,82 @@ +from datetime import datetime +from django import forms +from django.db.models import Q +from .models import Body, Proposal +from django.utils.translation import ugettext as _ + +class EphemQuery(forms.Form): + SITES = (('V37','ELP (V37)'),('F65','FTN (F65)'),('E10', 'FTS (E10)'),('W86','LSC (W85-87)'),('K92','CPT (K91-93)'),('Q63','COJ (Q63-64)')) + target = forms.CharField(label="Enter target name...", max_length=10, required=True, widget=forms.TextInput(attrs={'size':'10'}), error_messages={'required': _(u'Target name is required')}) + site_code = forms.ChoiceField(required=True, choices=SITES) + utc_date = forms.DateField(input_formats=['%Y-%m-%d',], initial=datetime.utcnow().date(), required=True, widget=forms.TextInput(attrs={'size':'10'}), error_messages={'required': _(u'UTC date is required')}) + alt_limit = forms.FloatField(initial=30.0, required=True, widget=forms.TextInput(attrs={'size':'4'})) + + def clean_target(self): + name = self.cleaned_data['target'] + body = Body.objects.filter(Q(provisional_name__icontains = name )|Q(provisional_packed__icontains = name)|Q(name__icontains = name)) + if body.count() == 1 : + return body[0] + elif body.count() == 0: + raise forms.ValidationError("Object not found.") + elif body.count() > 1: + raise forms.ValidationError("Multiple objects found.") + +class ScheduleForm(forms.Form): + SITES = (('V37','ELP (V37)'),('F65','FTN (F65)'),('E10', 'FTS (E10)'),('W86','LSC (W85-87)'),('K92','CPT (K91-93)'),('Q63','COJ (Q63-64)')) + proposals = Proposal.objects.all() + proposal_choices = [(proposal.code, proposal.title) for proposal in proposals] + + proposal_code = forms.ChoiceField(required=True, choices=proposal_choices) + site_code = forms.ChoiceField(required=True, choices=SITES) + utc_date = forms.DateField(input_formats=['%Y-%m-%d',], initial=datetime.utcnow().date(), required=True, widget=forms.TextInput(attrs={'size':'10'}), error_messages={'required': _(u'UTC date is required')}) + # body_id = forms.IntegerField(widget=forms.HiddenInput()) + # ok_to_schedule = forms.BooleanField(initial=False, required=False, widget=forms.HiddenInput()) + + # def clean_body_id(self): + # body = Body.objects.filter(pk=self.cleaned_data['body_id']) + # if body.count() == 1 : + # return body[0] + # elif body.count() == 0: + # raise forms.ValidationError("Object not found.") + def clean_utc_date(self): + start = self.cleaned_data['utc_date'] + if start < datetime.now().date(): + raise forms.ValidationError("Window cannot start in the past") + return start + + +class ScheduleBlockForm(forms.Form): + start_time = forms.DateTimeField(widget=forms.HiddenInput(), input_formats=['%Y-%m-%d %H:%M:%S', '%Y-%m-%dT%H:%M:%S']) + end_time = forms.DateTimeField(widget=forms.HiddenInput(), input_formats=['%Y-%m-%d %H:%M:%S', '%Y-%m-%dT%H:%M:%S']) + exp_count = forms.IntegerField(widget=forms.HiddenInput()) + exp_length = forms.FloatField(widget=forms.HiddenInput()) + proposal_code = forms.CharField(max_length=15,widget=forms.HiddenInput()) + site_code = forms.CharField(max_length=5,widget=forms.HiddenInput()) + group_id = forms.CharField(max_length=30,widget=forms.HiddenInput()) + + def clean_start_time(self): + start = self.cleaned_data['start_time'] + if start <= datetime.now(): + raise forms.ValidationError("Window cannot start in the past") + else: + return self.cleaned_data + + def clean_end_time(self): + end = self.cleaned_data['end_time'] + if end <= datetime.now(): + raise forms.ValidationError("Window cannot end in the past") + else: + return self.cleaned_data + + def clean_exp_length(self): + if self.cleaned_data['exp_length'] > 0.: + return self.cleaned_data + else: + raise forms.ValidationError("Exposure length is too short") + + def clean_exp_count(self): + if self.cleaned_data['exp_count'] > 1: + return self.cleaned_data + else: + raise forms.ValidationError("There must be more than 1 exposure") + diff --git a/neoexchange/ingest/management/commands/__init__.py b/neoexchange/core/management/__init__.py similarity index 100% rename from neoexchange/ingest/management/commands/__init__.py rename to neoexchange/core/management/__init__.py diff --git a/neoexchange/ingest/migrations/__init__.py b/neoexchange/core/management/commands/__init__.py similarity index 100% rename from neoexchange/ingest/migrations/__init__.py rename to neoexchange/core/management/commands/__init__.py diff --git a/neoexchange/ingest/management/commands/fetch_goldstone_targets.py b/neoexchange/core/management/commands/fetch_goldstone_targets.py similarity index 82% rename from neoexchange/ingest/management/commands/fetch_goldstone_targets.py rename to neoexchange/core/management/commands/fetch_goldstone_targets.py index cbae797af..41c6e886b 100644 --- a/neoexchange/ingest/management/commands/fetch_goldstone_targets.py +++ b/neoexchange/core/management/commands/fetch_goldstone_targets.py @@ -1,6 +1,5 @@ -from ingest.sources_subs import fetch_goldstone_targets -from ingest.views import update_MPC_orbit -from ingest.models import * +from astrometrics.sources_subs import fetch_goldstone_targets +from core.views import update_MPC_orbit from django.core.management.base import BaseCommand, CommandError from django.db.models import Q diff --git a/neoexchange/core/management/commands/runserver.py b/neoexchange/core/management/commands/runserver.py new file mode 100644 index 000000000..2ef3477ca --- /dev/null +++ b/neoexchange/core/management/commands/runserver.py @@ -0,0 +1,59 @@ +import os +import platform +import sys + +import django +from django.conf import settings +from django.contrib.staticfiles.management.commands import runserver +from django.core.management.commands.runserver import BaseRunserverCommand +from django.contrib.staticfiles.handlers import StaticFilesHandler +from django.db import connection +from django.http import Http404 +from django.utils.termcolors import colorize +from django.views.static import serve + +def banner(): + + # The raw banner split into lines. + lines = (""" + +.__ __. _______ ______ ___ ___ +| \ | | | ____| / __ \ \ \ / / +| \| | | |__ | | | | \ V / +| . ` | | __| | | | | > < +| |\ | | |____ | `--' | / . \ +|__| \__| |_______| \______/ /__/ \__\ + +* Django %(django_version)s +* Python %(python_version)s +* %(os_name)s %(os_version)s + +""" % { + "django_version": django.get_version(), + "python_version": sys.version.split(" ", 1)[0], + "os_name": platform.system(), + "os_version": platform.release(), + }).splitlines() + if django.VERSION >= (1, 7): + lines = lines[2:] + + return "\n".join(lines) + + +class Command(BaseRunserverCommand): + """ + Overrides runserver to display an ODIN banner + """ + + def inner_run(self, *args, **kwargs): + # Show the funky ODIN banner in the terminal. There + # aren't really any exceptions to catch here, but we do + # so blanketly since such a trivial thing like the banner + # shouldn't be able to crash the development server. + + try: + self.stdout.write(banner()) + except Exception, e: + print e + pass + super(Command, self).inner_run(*args, **kwargs) \ No newline at end of file diff --git a/neoexchange/ingest/management/commands/update_crossids.py b/neoexchange/core/management/commands/update_crossids.py similarity index 81% rename from neoexchange/ingest/management/commands/update_crossids.py rename to neoexchange/core/management/commands/update_crossids.py index 46b151945..91c1b1bec 100644 --- a/neoexchange/ingest/management/commands/update_crossids.py +++ b/neoexchange/core/management/commands/update_crossids.py @@ -1,6 +1,5 @@ -from ingest.sources_subs import fetch_previous_NEOCP_desigs -from ingest.views import update_crossids -from ingest.models import * +from astrometrics.sources_subs import fetch_previous_NEOCP_desigs +from core.views import update_crossids from django.core.management.base import BaseCommand, CommandError from django.db.models import Q diff --git a/neoexchange/ingest/management/commands/update_neocp_data.py b/neoexchange/core/management/commands/update_neocp_data.py similarity index 82% rename from neoexchange/ingest/management/commands/update_neocp_data.py rename to neoexchange/core/management/commands/update_neocp_data.py index 9c1ed431f..d26addc75 100644 --- a/neoexchange/ingest/management/commands/update_neocp_data.py +++ b/neoexchange/core/management/commands/update_neocp_data.py @@ -1,6 +1,5 @@ -from ingest.sources_subs import fetch_NEOCP -from ingest.views import update_NEOCP_orbit -from ingest.models import * +from astrometrics.sources_subs import fetch_NEOCP +from core.views import update_NEOCP_orbit from django.core.management.base import BaseCommand, CommandError from django.db.models import Q diff --git a/neoexchange/core/migrations/0001_initial.py b/neoexchange/core/migrations/0001_initial.py new file mode 100644 index 000000000..57211499c --- /dev/null +++ b/neoexchange/core/migrations/0001_initial.py @@ -0,0 +1,105 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Block', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('telclass', models.CharField(default=b'1m0', max_length=3, choices=[(b'1m0', b'1-meter'), (b'2m0', b'2-meter'), (b'0m4', b'0.4-meter')])), + ('site', models.CharField(max_length=3, choices=[(b'ogg', b'Haleakala'), (b'coj', b'Siding Spring'), (b'lsc', b'Cerro Tololo'), (b'elp', b'McDonald'), (b'cpt', b'Sutherland')])), + ('block_start', models.DateTimeField(null=True, blank=True)), + ('block_end', models.DateTimeField(null=True, blank=True)), + ('tracking_number', models.CharField(max_length=10, null=True, blank=True)), + ('when_observed', models.DateTimeField(null=True, blank=True)), + ('active', models.BooleanField(default=False)), + ], + options={ + 'db_table': 'ingest_block', + 'verbose_name': 'Observation Block', + 'verbose_name_plural': 'Observation Blocks', + }, + ), + migrations.CreateModel( + name='Body', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('provisional_name', models.CharField(max_length=15, null=True, verbose_name=b'Provisional MPC designation', blank=True)), + ('provisional_packed', models.CharField(max_length=7, null=True, verbose_name=b'MPC name in packed format', blank=True)), + ('name', models.CharField(max_length=15, null=True, verbose_name=b'Designation', blank=True)), + ('origin', models.CharField(default=b'M', choices=[(b'M', b'Minor Planet Center'), (b'N', b'NASA ARM'), (b'S', b'Spaceguard'), (b'D', b'NEODSYS'), (b'G', b'Goldstone'), (b'A', b'Arecibo')], max_length=1, blank=True, null=True, verbose_name=b'Where did this target come from?')), + ('source_type', models.CharField(blank=True, max_length=1, null=True, verbose_name=b'Type of object', choices=[(b'N', b'NEO'), (b'A', b'Asteroid'), (b'C', b'Comet'), (b'K', b'KBO'), (b'E', b'Centaur'), (b'T', b'Trojan'), (b'U', b'Unknown/NEO Candidate'), (b'X', b'Did not exist'), (b'W', b'Was not interesting')])), + ('elements_type', models.CharField(blank=True, max_length=16, null=True, verbose_name=b'Elements type', choices=[(b'MPC_MINOR_PLANET', b'MPC Minor Planet'), (b'MPC_COMET', b'MPC Comet')])), + ('active', models.BooleanField(default=False, verbose_name=b'Actively following?')), + ('fast_moving', models.BooleanField(default=False, verbose_name=b'Is this object fast?')), + ('urgency', models.IntegerField(help_text=b'how urgent is this?', null=True, blank=True)), + ('epochofel', models.DateTimeField(null=True, verbose_name=b'Epoch of elements', blank=True)), + ('orbinc', models.FloatField(null=True, verbose_name=b'Orbital inclination in deg', blank=True)), + ('longascnode', models.FloatField(null=True, verbose_name=b'Longitude of Ascending Node (deg)', blank=True)), + ('argofperih', models.FloatField(null=True, verbose_name=b'Arg of perihelion (deg)', blank=True)), + ('eccentricity', models.FloatField(null=True, verbose_name=b'Eccentricity', blank=True)), + ('meandist', models.FloatField(help_text=b'for asteroids', null=True, verbose_name=b'Mean distance (AU)', blank=True)), + ('meananom', models.FloatField(help_text=b'for asteroids', null=True, verbose_name=b'Mean Anomaly (deg)', blank=True)), + ('perihdist', models.FloatField(help_text=b'for comets', null=True, verbose_name=b'Perihelion distance (AU)', blank=True)), + ('epochofperih', models.DateTimeField(help_text=b'for comets', null=True, verbose_name=b'Epoch of perihelion', blank=True)), + ('abs_mag', models.FloatField(null=True, verbose_name=b'H - absolute magnitude', blank=True)), + ('slope', models.FloatField(null=True, verbose_name=b'G - slope parameter', blank=True)), + ('ingest', models.DateTimeField(default=django.utils.timezone.now)), + ], + options={ + 'db_table': 'ingest_body', + 'verbose_name': 'Minor Body', + 'verbose_name_plural': 'Minor Bodies', + }, + ), + migrations.CreateModel( + name='Proposal', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('code', models.CharField(max_length=20)), + ('title', models.CharField(max_length=255)), + ('pi', models.CharField(default=b'', max_length=50)), + ('tag', models.CharField(default=b'LCO', max_length=10)), + ], + options={ + 'db_table': 'ingest_proposal', + }, + ), + migrations.CreateModel( + name='Record', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('site', models.CharField(max_length=3, verbose_name=b'3-letter site code')), + ('instrument', models.CharField(max_length=4, verbose_name=b'instrument code')), + ('filter', models.CharField(max_length=15, verbose_name=b'filter class')), + ('filename', models.CharField(max_length=31)), + ('exp', models.FloatField(verbose_name=b'exposure time in seconds')), + ('whentaken', models.DateTimeField()), + ('block', models.ForeignKey(to='core.Block')), + ], + options={ + 'db_table': 'ingest_record', + 'verbose_name': 'Observation Record', + 'verbose_name_plural': 'Observation Records', + }, + ), + migrations.AddField( + model_name='block', + name='body', + field=models.ForeignKey(to='core.Body'), + ), + migrations.AddField( + model_name='block', + name='proposal', + field=models.ForeignKey(to='core.Proposal'), + ), + ] diff --git a/neoexchange/core/migrations/__init__.py b/neoexchange/core/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neoexchange/ingest/models.py b/neoexchange/core/models.py similarity index 86% rename from neoexchange/ingest/models.py rename to neoexchange/core/models.py index 0fcbcf070..7f15427e6 100644 --- a/neoexchange/ingest/models.py +++ b/neoexchange/core/models.py @@ -13,7 +13,7 @@ GNU General Public License for more details. ''' from django.db import models -from datetime import datetime +from django.utils.timezone import now from django.utils.translation import ugettext as _ from astropy.time import Time import reversion @@ -27,7 +27,8 @@ ('T','Trojan'), ('U','Unknown/NEO Candidate'), ('X','Did not exist'), - ('W','Was not interesting') + ('W','Was not interesting'), + ('D','Discovery, non NEO'), ) ELEMENTS_TYPES = (('MPC_MINOR_PLANET','MPC Minor Planet'),('MPC_COMET','MPC Comet')) @@ -38,7 +39,8 @@ ('S','Spaceguard'), ('D','NEODSYS'), ('G','Goldstone'), - ('A','Arecibo') + ('A','Arecibo'), + ('L','LCOGT') ) TELESCOPE_CHOICES = ( @@ -73,6 +75,11 @@ def check_object_exists(objname,dbg=False): class Proposal(models.Model): code = models.CharField(max_length=20) title = models.CharField(max_length=255) + pi = models.CharField(max_length=50, default='') + tag = models.CharField(max_length=10, default='LCOGT') + + class Meta: + db_table = 'ingest_proposal' def __unicode__(self): if len(self.title)>=10: @@ -103,15 +110,25 @@ class Body(models.Model): epochofperih = models.DateTimeField('Epoch of perihelion', blank=True, null=True, help_text='for comets') abs_mag = models.FloatField('H - absolute magnitude', blank=True, null=True) slope = models.FloatField('G - slope parameter', blank=True, null=True) - ingest = models.DateTimeField(default=datetime.now()) + ingest = models.DateTimeField(default=now) def epochofel_mjd(self): - t = Time(self.epochofel.isoformat(), format='isot', scale='tt') - return t.mjd + mjd = None + try: + t = Time(self.epochofel.isoformat(), format='isot', scale='tt') + mjd = t.mjd + except: + pass + return mjd def epochofperih_mjd(self): - t = Time(self.epochofperih.isoformat(), format='isot', scale='tt') - return t.mjd + mjd = None + try: + t = Time(self.epochofperih.isoformat(), format='isot', scale='tt') + mjd = t.mjd + except: + pass + return mjd def current_name(self): if self.name: @@ -121,9 +138,16 @@ def current_name(self): else: return "Unknown" + def old_name(self): + if self.provisional_name and self.name: + return self.provisional_name + else: + return False + class Meta: verbose_name = _('Minor Body') verbose_name_plural = _('Minor Bodies') + db_table = 'ingest_body' def __unicode__(self): if self.active: @@ -151,6 +175,7 @@ class Block(models.Model): class Meta: verbose_name = _('Observation Block') verbose_name_plural = _('Observation Blocks') + db_table = 'ingest_block' def __unicode__(self): pass @@ -170,6 +195,7 @@ class Record(models.Model): class Meta: verbose_name = _('Observation Record') verbose_name_plural = _('Observation Records') + db_table = 'ingest_record' def __unicode__(self): if self.active: diff --git a/neoexchange/ingest/static/ingest/css/admin.css b/neoexchange/core/static/core/css/admin.css similarity index 100% rename from neoexchange/ingest/static/ingest/css/admin.css rename to neoexchange/core/static/core/css/admin.css diff --git a/neoexchange/ingest/static/ingest/css/dashboard.css b/neoexchange/core/static/core/css/dashboard.css similarity index 100% rename from neoexchange/ingest/static/ingest/css/dashboard.css rename to neoexchange/core/static/core/css/dashboard.css diff --git a/neoexchange/ingest/static/ingest/css/forms.css b/neoexchange/core/static/core/css/forms.css similarity index 94% rename from neoexchange/ingest/static/ingest/css/forms.css rename to neoexchange/core/static/core/css/forms.css index 6592f4afc..a5513a876 100644 --- a/neoexchange/ingest/static/ingest/css/forms.css +++ b/neoexchange/core/static/core/css/forms.css @@ -175,6 +175,18 @@ form.proposalsubmit_admin .proposalsubmit_credit label { color:#555; } +.compact-field { + float:left; + padding-right: 16px; +} +.compact-field input, .compact-field label { + display: table-row; +} +.compact-field label { + font-size: 70%; + color:#aaa; +} + @media (max-width: 800px), (max-device-width: 800px) { .pform { padding: 10px; margin:0px 20px 20px 20px; width:auto; } } diff --git a/neoexchange/core/static/core/css/img/NEOx-banner.jpg b/neoexchange/core/static/core/css/img/NEOx-banner.jpg new file mode 100644 index 000000000..96d9af63f Binary files /dev/null and b/neoexchange/core/static/core/css/img/NEOx-banner.jpg differ diff --git a/neoexchange/ingest/static/ingest/css/styles.css b/neoexchange/core/static/core/css/styles.css similarity index 99% rename from neoexchange/ingest/static/ingest/css/styles.css rename to neoexchange/core/static/core/css/styles.css index 5fc9a3b87..e3eb11190 100644 --- a/neoexchange/ingest/static/ingest/css/styles.css +++ b/neoexchange/core/static/core/css/styles.css @@ -23,7 +23,7 @@ img { border: 0px; } .padded-horizontal { padding-top: 0px; padding-bottom: 0px; } #lcogt-bar { -o-background-size: 100% 100%;-moz-background-size: 100% 100%;-webkit-background-size: 100% 100%;background-size: 100% 100%;/* Internet Explorer */*background: #33363c;background: #33363c;filter: progid:DXImageTransform.Microsoft.gradient(gradientType=0, startColorstr=#FF222429, endColorstr=#FF33363c);/* Recent browsers */background-image: -webkit-gradient(linear,left top, left bottom,from(#222429),to(#33363c));background-image: -webkit-linear-gradient(top,#222429,#33363c);background-image: -moz-linear-gradient(top,#222429,#33363c);background-image: -o-linear-gradient(top,#222429,#33363c);background-image: linear-gradient(top,#222429,#33363c);color: white; } div.compactonly { display: none; } -#header-holder { background: url("http://lcogt.net/sites/default/themes/lcogt/images/new-header-bg.png") no-repeat scroll -100px top #d4d4d4; } +#header-holder { background: url("img/NEOx-banner.jpg") no-repeat scroll -100px top #ffffff; background-position: right 0px center; } #lcogt-bar { margin: 0; height: 32px; clear:both; line-height: 26px; color: #cccccc; font-size: 0.9em; overflow: hidden; } #lcogt-bar a { color: #e6e6e6; text-decoration: none; } #lcogt-bar ul { margin: 0; padding: 0; list-style: none; } @@ -322,9 +322,9 @@ a:focus { border: 1px dotted invert; } #header-holder { display: block; clear:both; } #header h1 { font-size: 1em; } div p:first-child { margin-top: 0px; } -#logo { text-align: center; display: inline-block; margin: 16px 0px 16px 32px; float: left; } +#logo { text-align: center; display: inline-block; margin: 0; float: left; } #logo a { display: block; height: 64px; } -#logo img { height: 64px; display: block; border:0px; } +#logo img { height: 96px; display: block; border:0px; } #site-name { display: inline-block; margin: 24px 0px 0px 16px; } #site-name strong { font-family: Century Gothic, sans-serif; font-size: 28px; line-height: 1em; color: #202020; display: block; } #site-name h1 { font-size: inherit; } diff --git a/neoexchange/core/static/core/images/NEO-logo.jpg b/neoexchange/core/static/core/images/NEO-logo.jpg new file mode 100644 index 000000000..a6acd7854 Binary files /dev/null and b/neoexchange/core/static/core/images/NEO-logo.jpg differ diff --git a/neoexchange/core/static/core/images/NEO-logo_sm.jpg b/neoexchange/core/static/core/images/NEO-logo_sm.jpg new file mode 100644 index 000000000..ea054de66 Binary files /dev/null and b/neoexchange/core/static/core/images/NEO-logo_sm.jpg differ diff --git a/neoexchange/core/static/core/images/NEOx-banner.png b/neoexchange/core/static/core/images/NEOx-banner.png new file mode 100644 index 000000000..942f02fc0 Binary files /dev/null and b/neoexchange/core/static/core/images/NEOx-banner.png differ diff --git a/neoexchange/core/static/core/images/favicon.ico b/neoexchange/core/static/core/images/favicon.ico new file mode 100644 index 000000000..7a851a5e7 Binary files /dev/null and b/neoexchange/core/static/core/images/favicon.ico differ diff --git a/neoexchange/ingest/static/ingest/images/icons.svg b/neoexchange/core/static/core/images/icons.svg similarity index 100% rename from neoexchange/ingest/static/ingest/images/icons.svg rename to neoexchange/core/static/core/images/icons.svg diff --git a/neoexchange/ingest/static/ingest/images/icons_16.png b/neoexchange/core/static/core/images/icons_16.png similarity index 100% rename from neoexchange/ingest/static/ingest/images/icons_16.png rename to neoexchange/core/static/core/images/icons_16.png diff --git a/neoexchange/ingest/static/ingest/images/icons_16_white.png b/neoexchange/core/static/core/images/icons_16_white.png similarity index 100% rename from neoexchange/ingest/static/ingest/images/icons_16_white.png rename to neoexchange/core/static/core/images/icons_16_white.png diff --git a/neoexchange/ingest/static/ingest/images/lcogt.png b/neoexchange/core/static/core/images/lcogt.png similarity index 100% rename from neoexchange/ingest/static/ingest/images/lcogt.png rename to neoexchange/core/static/core/images/lcogt.png diff --git a/neoexchange/ingest/static/ingest/js/d3.v3.min.js b/neoexchange/core/static/core/js/d3.v3.min.js similarity index 100% rename from neoexchange/ingest/static/ingest/js/d3.v3.min.js rename to neoexchange/core/static/core/js/d3.v3.min.js diff --git a/neoexchange/ingest/static/ingest/js/jquery-1.10.2.js b/neoexchange/core/static/core/js/jquery-1.10.2.js similarity index 100% rename from neoexchange/ingest/static/ingest/js/jquery-1.10.2.js rename to neoexchange/core/static/core/js/jquery-1.10.2.js diff --git a/neoexchange/ingest/static/ingest/js/jquery-ui-1.10.3.min.js b/neoexchange/core/static/core/js/jquery-ui-1.10.3.min.js similarity index 100% rename from neoexchange/ingest/static/ingest/js/jquery-ui-1.10.3.min.js rename to neoexchange/core/static/core/js/jquery-ui-1.10.3.min.js diff --git a/neoexchange/ingest/static/ingest/js/jquery.burn.js b/neoexchange/core/static/core/js/jquery.burn.js similarity index 100% rename from neoexchange/ingest/static/ingest/js/jquery.burn.js rename to neoexchange/core/static/core/js/jquery.burn.js diff --git a/neoexchange/ingest/static/ingest/js/jquery.cycle2.js b/neoexchange/core/static/core/js/jquery.cycle2.js similarity index 100% rename from neoexchange/ingest/static/ingest/js/jquery.cycle2.js rename to neoexchange/core/static/core/js/jquery.cycle2.js diff --git a/neoexchange/core/static/core/js/jquery.dj.selectable.js b/neoexchange/core/static/core/js/jquery.dj.selectable.js new file mode 100644 index 000000000..36c9707e3 --- /dev/null +++ b/neoexchange/core/static/core/js/jquery.dj.selectable.js @@ -0,0 +1,352 @@ +/*jshint trailing:true, indent:4*/ +/* + * django-selectable UI widget + * Source: https://bitbucket.org/mlavin/django-selectable + * Docs: http://django-selectable.readthedocs.org/ + * + * Depends: + * - jQuery 1.4.4+ + * - jQuery UI 1.8 widget factory + * + * Copyright 2010-2012, Mark Lavin + * BSD License + * +*/ +(function ($) { + + $.widget("ui.djselectable", { + + options: { + removeIcon: "ui-icon-close", + comboboxIcon: "ui-icon-triangle-1-s", + prepareQuery: null, + highlightMatch: true, + formatLabel: null + }, + + _initDeck: function () { + /* Create list display for currently selected items for multi-select */ + var self = this; + var data = $(this.element).data(); + var style = data.selectablePosition || data['selectable-position'] || 'bottom'; + this.deck = $('
    ').addClass('ui-widget selectable-deck selectable-deck-' + style); + if (style === 'bottom' || style === 'bottom-inline') { + $(this.element).after(this.deck); + } else { + $(this.element).before(this.deck); + } + $(self.hiddenMultipleSelector).each(function (i, input) { + self._addDeckItem(input); + }); + }, + + _addDeckItem: function (input) { + /* Add new deck list item from a given hidden input */ + var self = this; + var li = $('
  • ') + .text($(input).attr('title')) + .addClass('selectable-deck-item'); + var item = {element: self.element, input: input, wrapper: li, deck: self.deck}; + if (self._trigger("add", null, item) === false) { + input.remove(); + } else { + var button = $('
    ') + .addClass('selectable-deck-remove') + .append( + $('') + .attr('href', '#') + .button({ + icons: { + primary: self.options.removeIcon + }, + text: false + }) + .click(function (e) { + e.preventDefault(); + if (self._trigger("remove", e, item) !== false) { + $(input).remove(); + li.remove(); + } + }) + ); + li.append(button).appendTo(this.deck); + } + }, + + select: function (item, event) { + /* Trigger selection of a given item. + Item should contain two properties: id and value + Event is the original select event if there is one. + Event should not be passed if triggered manually. + */ + var self = this, + input = this.element; + $(input).removeClass('ui-state-error'); + if (item) { + if (self.allowMultiple) { + $(input).val(""); + $(input).data("autocomplete").term = ""; + if ($(self.hiddenMultipleSelector + '[value="' + item.id + '"]').length === 0) { + var newInput = $('', { + 'type': 'hidden', + 'name': self.hiddenName, + 'value': item.id, + 'title': item.value, + 'data-selectable-type': 'hidden-multiple' + }); + $(input).after(newInput); + self._addDeckItem(newInput); + } + return false; + } else { + $(input).val(item.value); + var ui = {item: item}; + if (typeof(event) === 'undefined' || event.type !== "autocompleteselect") { + $(input).trigger('autocompleteselect', [ui ]); + } + } + } + }, + + _create: function () { + /* Initialize a new selectable widget */ + var self = this, + input = this.element, + data = $(input).data(); + self.allowNew = data.selectableAllowNew || data['selectable-allow-new']; + self.allowMultiple = data.selectableMultiple || data['selectable-multiple']; + self.textName = $(input).attr('name'); + self.hiddenName = self.textName.replace('_0', '_1'); + self.hiddenSelector = ':input[data-selectable-type=hidden][name=' + self.hiddenName + ']'; + self.hiddenMultipleSelector = ':input[data-selectable-type=hidden-multiple][name=' + self.hiddenName + ']'; + if (self.allowMultiple) { + self.allowNew = false; + $(input).val(""); + this._initDeck(); + } + + function dataSource(request, response) { + /* Custom data source to uses the lookup url with pagination + Adds hook for adjusting query parameters. + Includes timestamp to prevent browser caching the lookup. */ + var url = data.selectableUrl || data['selectable-url']; + var now = new Date().getTime(); + var query = {term: request.term, timestamp: now}; + if (self.options.prepareQuery) { + self.options.prepareQuery(query); + } + var page = $(input).data("page"); + if (page) { + query.page = page; + } + function unwrapResponse(data) { + var results = data.data; + var meta = data.meta; + if (meta.next_page && meta.more) { + results.push({ + id: '', + value: '', + label: meta.more, + page: meta.next_page + }); + } + return response(results); + } + $.getJSON(url, query, unwrapResponse); + } + // Create base auto-complete lookup + $(input).autocomplete({ + source: dataSource, + change: function (event, ui) { + $(input).removeClass('ui-state-error'); + if ($(input).val() && !ui.item) { + if (!self.allowNew) { + $(input).addClass('ui-state-error'); + } + } + if (self.allowMultiple && !$(input).hasClass('ui-state-error')) { + $(input).val(""); + $(input).data("autocomplete").term = ""; + } + }, + select: function (event, ui) { + $(input).removeClass('ui-state-error'); + if (ui.item && ui.item.page) { + // Set current page value + $(input).data("page", ui.item.page); + $('.selectable-paginator', self.menu).remove(); + // Search for next page of results + $(input).autocomplete("search"); + return false; + } + return self.select(ui.item, event); + } + }).addClass("ui-widget ui-widget-content ui-corner-all"); + // Override the default auto-complete render. + $(input).data("autocomplete")._renderItem = function (ul, item) { + /* Adds hook for additional formatting, allows HTML in the label, + highlights term matches and handles pagination. */ + var label = item.label; + if (self.options.formatLabel) { + label = self.options.formatLabel(label, item); + } + if (self.options.highlightMatch && this.term) { + var re = new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + + $.ui.autocomplete.escapeRegex(this.term) + + ")(?![^<>]*>)(?![^&;]+;)", "gi"); + label = label.replace(re, "$1"); + } + var li = $("
  • ") + .data("item.autocomplete", item) + .append($("").append(label)) + .appendTo(ul); + if (item.page) { + li.addClass('selectable-paginator'); + } + return li; + }; + // Override the default auto-complete suggest. + $(input).data("autocomplete")._suggest = function (items) { + /* Needed for handling pagination links */ + var page = $(input).data('page'); + var ul = this.menu.element; + if (!page) { + ul.empty(); + } + $(input).data('page', null); + ul.zIndex(this.element.zIndex() + 1); + this._renderMenu(ul, items); + // jQuery UI menu does not define deactivate + if (this.menu.deactivate) this.menu.deactivate(); + this.menu.refresh(); + // size and position menu + ul.show(); + this._resizeMenu(); + ul.position($.extend({of: this.element}, this.options.position)); + if (this.options.autoFocus) { + this.menu.next(new $.Event("mouseover")); + } + }; + // Additional work for combobox widgets + var selectableType = data.selectableType || data['selectable-type']; + if (selectableType === 'combobox') { + // Change auto-complete options + $(input).autocomplete("option", { + delay: 0, + minLength: 0 + }) + .removeClass("ui-corner-all") + .addClass("ui-corner-left ui-combo-input"); + // Add show all items button + $("").attr("tabIndex", -1).attr("title", "Show All Items") + .insertAfter($(input)) + .button({ + icons: { + primary: self.options.comboboxIcon + }, + text: false + }) + .removeClass("ui-corner-all") + .addClass("ui-corner-right ui-button-icon ui-combo-button") + .click(function () { + // close if already visible + if ($(input).autocomplete("widget").is(":visible")) { + $(input).autocomplete("close"); + return false; + } + // pass empty string as value to search for, displaying all results + $(input).autocomplete("search", ""); + $(input).focus(); + return false; + }); + } + } + }); + + window.bindSelectables = function (context) { + /* Bind all selectable widgets in a given context. + Automatically called on document.ready. + Additional calls can be made for dynamically added widgets. + */ + $(":input[data-selectable-type=text]", context).djselectable(); + $(":input[data-selectable-type=combobox]", context).djselectable(); + $(":input[data-selectable-type=hidden]", context).each(function (i, elem) { + var hiddenName = $(elem).attr('name'); + var textName = hiddenName.replace('_1', '_0'); + $(":input[name=" + textName + "][data-selectable-url]").bind( + "autocompletechange autocompleteselect", + function (event, ui) { + if (ui.item && ui.item.id) { + $(elem).val(ui.item.id); + } else { + $(elem).val(""); + } + } + ); + }); + }; + + /* Monkey-patch Django's dynamic formset, if defined */ + if (typeof(django) !== "undefined" && typeof(django.jQuery) !== "undefined") { + if (django.jQuery.fn.formset) { + var oldformset = django.jQuery.fn.formset; + django.jQuery.fn.formset = function (opts) { + var options = $.extend({}, opts); + var addedevent = function (row) { + bindSelectables($(row)); + }; + var added = null; + if (options.added) { + // Wrap previous added function and include call to bindSelectables + var oldadded = options.added; + added = function (row) { oldadded(row); addedevent(row); }; + } + options.added = added || addedevent; + return oldformset.call(this, options); + }; + } + } + + /* Monkey-patch Django's dismissAddAnotherPopup(), if defined */ + if (typeof(dismissAddAnotherPopup) !== "undefined" && + typeof(windowname_to_id) !== "undefined" && + typeof(html_unescape) !== "undefined") { + var django_dismissAddAnotherPopup = dismissAddAnotherPopup; + dismissAddAnotherPopup = function (win, newId, newRepr) { + /* See if the popup came from a selectable field. + If not, pass control to Django's code. + If so, handle it. */ + var fieldName = windowname_to_id(win.name); /* e.g. "id_fieldname" */ + var field = $('#' + fieldName); + var multiField = $('#' + fieldName + '_0'); + /* Check for bound selectable */ + var singleWidget = field.data('djselectable'); + var multiWidget = multiField.data('djselectable'); + if (singleWidget || multiWidget) { + // newId and newRepr are expected to have previously been escaped by + // django.utils.html.escape. + var item = { + id: html_unescape(newId), + value: html_unescape(newRepr) + }; + if (singleWidget) { + field.djselectable('select', item); + } + if (multiWidget) { + multiField.djselectable('select', item); + } + win.close(); + } else { + /* Not ours, pass on to original function. */ + return django_dismissAddAnotherPopup(win, newId, newRepr); + } + }; + } + + $(document).ready(function () { + // Bind existing widgets on document ready + if (typeof(djselectableAutoLoad) === "undefined" || djselectableAutoLoad) { + bindSelectables('body'); + } + }); +})(jQuery); diff --git a/neoexchange/ingest/static/ingest/js/jquery.jeditable.js b/neoexchange/core/static/core/js/jquery.jeditable.js similarity index 100% rename from neoexchange/ingest/static/ingest/js/jquery.jeditable.js rename to neoexchange/core/static/core/js/jquery.jeditable.js diff --git a/neoexchange/ingest/static/ingest/js/jquery.jumble.js b/neoexchange/core/static/core/js/jquery.jumble.js similarity index 100% rename from neoexchange/ingest/static/ingest/js/jquery.jumble.js rename to neoexchange/core/static/core/js/jquery.jumble.js diff --git a/neoexchange/ingest/static/ingest/js/jquery.knob.js b/neoexchange/core/static/core/js/jquery.knob.js similarity index 100% rename from neoexchange/ingest/static/ingest/js/jquery.knob.js rename to neoexchange/core/static/core/js/jquery.knob.js diff --git a/neoexchange/ingest/static/ingest/js/jquery.min.js b/neoexchange/core/static/core/js/jquery.min.js similarity index 100% rename from neoexchange/ingest/static/ingest/js/jquery.min.js rename to neoexchange/core/static/core/js/jquery.min.js diff --git a/neoexchange/ingest/static/ingest/js/jquery.timepicker.js b/neoexchange/core/static/core/js/jquery.timepicker.js similarity index 100% rename from neoexchange/ingest/static/ingest/js/jquery.timepicker.js rename to neoexchange/core/static/core/js/jquery.timepicker.js diff --git a/neoexchange/ingest/static/ingest/js/jquery.typewriter.js b/neoexchange/core/static/core/js/jquery.typewriter.js similarity index 100% rename from neoexchange/ingest/static/ingest/js/jquery.typewriter.js rename to neoexchange/core/static/core/js/jquery.typewriter.js diff --git a/neoexchange/ingest/static/ingest/js/jquery.ui.nestedSortable.js b/neoexchange/core/static/core/js/jquery.ui.nestedSortable.js similarity index 100% rename from neoexchange/ingest/static/ingest/js/jquery.ui.nestedSortable.js rename to neoexchange/core/static/core/js/jquery.ui.nestedSortable.js diff --git a/neoexchange/ingest/static/ingest/js/jquery.ui.pack.js b/neoexchange/core/static/core/js/jquery.ui.pack.js similarity index 100% rename from neoexchange/ingest/static/ingest/js/jquery.ui.pack.js rename to neoexchange/core/static/core/js/jquery.ui.pack.js diff --git a/neoexchange/ingest/templates/ingest/403.html b/neoexchange/core/templates/403.html similarity index 100% rename from neoexchange/ingest/templates/ingest/403.html rename to neoexchange/core/templates/403.html diff --git a/neoexchange/ingest/templates/ingest/404.html b/neoexchange/core/templates/404.html similarity index 100% rename from neoexchange/ingest/templates/ingest/404.html rename to neoexchange/core/templates/404.html diff --git a/neoexchange/ingest/templates/ingest/500x.html b/neoexchange/core/templates/500.html similarity index 100% rename from neoexchange/ingest/templates/ingest/500x.html rename to neoexchange/core/templates/500.html diff --git a/neoexchange/ingest/templates/ingest/base.html b/neoexchange/core/templates/base.html similarity index 70% rename from neoexchange/ingest/templates/ingest/base.html rename to neoexchange/core/templates/base.html index 8264f4e00..9dfd459a1 100644 --- a/neoexchange/ingest/templates/ingest/base.html +++ b/neoexchange/core/templates/base.html @@ -1,28 +1,27 @@ -{% load url from future %} {% load staticfiles %} - {%block header %}{% endblock %} | LCOGT + {%block header %}{% endblock %} | LCOGT NEOx {% block meta %}{% endblock %} {% block favicon %} - + {% endblock %} {% block css-content %}{% endblock %} - + {% block last-css-content %}{% endblock %} - - - + + + {% block script-content %}{% endblock %} @@ -31,6 +30,24 @@
    + {% block login %} +
    +
      + {% if user.is_authenticated %} +
    • + {% if user.first_name %}{{ user.first_name }}{% else %}{{user.username}}{% endif %} +
    • +
    • + Logout +
    • + {% else %} +
    • + Login +
    • + {% endif %} +
    +
    + {% endblock %} @@ -38,10 +55,10 @@
    diff --git a/neoexchange/core/templates/core/block_list.html b/neoexchange/core/templates/core/block_list.html new file mode 100644 index 000000000..e69de29bb diff --git a/neoexchange/ingest/templates/ingest/body_detail.html b/neoexchange/core/templates/core/body_detail.html similarity index 64% rename from neoexchange/ingest/templates/ingest/body_detail.html rename to neoexchange/core/templates/core/body_detail.html index 93206206f..eba9abc02 100644 --- a/neoexchange/ingest/templates/ingest/body_detail.html +++ b/neoexchange/core/templates/core/body_detail.html @@ -1,6 +1,8 @@ -{% extends 'ingest/base.html' %} -{% load url from future %} -{% block header %}NEOx home{% endblock %} +{% extends 'base.html' %} +{% load staticfiles %} + +{% block css-content %}{% endblock %} +{% block header %}{{body.current_name}} details{% endblock %} {% block bodyclass %}page{% endblock %} @@ -14,12 +16,39 @@

    Object: {{body.current_name}}

    +
    + {% if body.old_name %} +

    Name{{body.old_name}} → {{body.name}}

    + {% endif %}

    Type{{body.get_source_type_display}}

    Status {% if body.active %}Actively Following{% else %}Not Following{% endif %}

    Source {{body.get_origin_display}}

    - Schedule Observations +
    +
    + +
    +
    + {{ form.utc_date }} +
    +
    + {{ form.site_code }} +
    +
    + {{ form.alt_limit }} +
    + +
    + {% for key, error in form.errors.items %} +
    + {{ error }} +
    + {% endfor %} +
    +

    Recent Observations

    diff --git a/neoexchange/ingest/templates/ingest/body_list.html b/neoexchange/core/templates/core/body_list.html similarity index 94% rename from neoexchange/ingest/templates/ingest/body_list.html rename to neoexchange/core/templates/core/body_list.html index 7606c2fff..3f0e5e4cb 100644 --- a/neoexchange/ingest/templates/ingest/body_list.html +++ b/neoexchange/core/templates/core/body_list.html @@ -1,6 +1,6 @@ -{% extends 'ingest/base.html' %} +{% extends 'base.html' %} {% load url from future %} -{% block header %}NEOx{% endblock %} +{% block header %}Targets{% endblock %} {% block bodyclass %}page{% endblock %} {% block extramenu %} diff --git a/neoexchange/ingest/templates/ingest/ephem.html b/neoexchange/core/templates/core/ephem.html similarity index 82% rename from neoexchange/ingest/templates/ingest/ephem.html rename to neoexchange/core/templates/core/ephem.html index f3c95717b..295ddd5a0 100644 --- a/neoexchange/ingest/templates/ingest/ephem.html +++ b/neoexchange/core/templates/core/ephem.html @@ -1,7 +1,7 @@ -{% extends 'ingest/base.html' %} +{% extends 'base.html' %} {% load url from future %} -{% block header %}NEOx{% endblock %} +{% block header %}Ephemeris for {{ target.current_name }} at {{ site_code }}{% endblock %} {% block bodyclass %}page{% endblock %} @@ -9,12 +9,18 @@ {% block extramenu %}
    -

    Emphemeris for {{ new_target_name }} at {{ site_code }}

    +

    Ephemeris for {{ target.current_name }} at {{ site_code }}

    {% endblock%} {% block main-content %}
    +
    diff --git a/neoexchange/ingest/templates/ingest/home.html b/neoexchange/core/templates/core/home.html similarity index 70% rename from neoexchange/ingest/templates/ingest/home.html rename to neoexchange/core/templates/core/home.html index e4315878c..81a35486a 100644 --- a/neoexchange/ingest/templates/ingest/home.html +++ b/neoexchange/core/templates/core/home.html @@ -1,10 +1,10 @@ -{% extends 'ingest/base.html' %} +{% extends 'base.html' %} {% load url from future %} {% load staticfiles %} -{% block css-content %}{% endblock %} +{% block css-content %}{% endblock %} -{% block header %}NEOx home{% endblock %} +{% block header %}Home{% endblock %} {% block bodyclass %}page{% endblock %} {% block extramenucss %}extramenu-home{% endblock %} @@ -17,7 +17,7 @@
    - +
    {{ form.utc_date }} @@ -65,20 +65,22 @@
    + {% for body in newest%} - - - - - - - - - - - + + + + + + {% empty%} + + {% endfor%}
    N999r0qUnknown/NEO CandidateMPCApril 8, 2015, 9:23 p.m.
    P10kfudUnknown/NEO CandidateMPCApril 8, 2015, 8:57 p.m.
    {{body.current_name}}{{body.get_source_type_display}}{{body.get_origin_display}} + +
    No new targets
    -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/neoexchange/ingest/templates/ingest/login.html b/neoexchange/core/templates/core/login.html similarity index 59% rename from neoexchange/ingest/templates/ingest/login.html rename to neoexchange/core/templates/core/login.html index 73df1282c..00f0f6147 100644 --- a/neoexchange/ingest/templates/ingest/login.html +++ b/neoexchange/core/templates/core/login.html @@ -1,17 +1,12 @@ {% extends 'base.html' %} -{% load url from future %} +{% load staticfiles %} + {% block header %}NEOx Login{% endblock %} -{% block css-content %} - {% stylesheet_link_tag "ingest/css/forms.css" %} -{% endblock %} -{% block script-content %} - +{% block last-css-content %} + {% endblock %} + {% block bodyclass %}page{% endblock %} {% block main-content %} @@ -22,32 +17,14 @@ {% endfor %}
    {% endif %} -
    - -
    -
    - NEO - x - - the - Near Earth Object - Exchange - -
    -
    -
    {% csrf_token %}
    - +
    {{form.username.errors.as_text}}
    @@ -73,4 +50,5 @@
    +
    {% endblock %} \ No newline at end of file diff --git a/neoexchange/core/templates/core/logout.html b/neoexchange/core/templates/core/logout.html new file mode 100644 index 000000000..8b3ddbee2 --- /dev/null +++ b/neoexchange/core/templates/core/logout.html @@ -0,0 +1,23 @@ +{% extends 'base.html' %} +{% load staticfiles %} + +{% block header %}NEOx Login{% endblock %} +{% block last-css-content %} + +{% endblock %} + + +{% block bodyclass %}page{% endblock %} +{% block main-content %} + +
    + +
    +
    +

    You have been logged out.

    +

    Log in again?

    +
    +
    +
    +
    +{% endblock %} \ No newline at end of file diff --git a/neoexchange/core/templates/core/schedule.html b/neoexchange/core/templates/core/schedule.html new file mode 100644 index 000000000..fe03d87e6 --- /dev/null +++ b/neoexchange/core/templates/core/schedule.html @@ -0,0 +1,54 @@ +{% extends 'base.html' %} +{% load url from future %} +{% block header %}NEOx scheduling{% endblock %} + +{% block bodyclass %}page{% endblock %} + + +{% block extramenu %} +
    +

    Scheduling

    +
    +{% endblock%} + +{% block main-content %} +
    +
    + {% if body.id and form %} +
    +
    + {% csrf_token %} +

    Parameters for: {{body.current_name}}

    + + + + + + + + + + + + + + + +
    Proposal{{ form.proposal_code }}
    Site code{{ form.site_code }}
    UTC Date{{ form.utc_date }}
    + {% for key, error in form.errors.items %} +
    + {{ error }} +
    + {% endfor %} +
    +
    + +
    +
    + + {% endif %} +
    +
    +
    + +{% endblock %} diff --git a/neoexchange/core/templates/core/schedule_confirm.html b/neoexchange/core/templates/core/schedule_confirm.html new file mode 100644 index 000000000..a8f9fa299 --- /dev/null +++ b/neoexchange/core/templates/core/schedule_confirm.html @@ -0,0 +1,83 @@ +{% extends 'base.html' %} +{% load url from future %} +{% block header %}NEOx scheduling{% endblock %} + +{% block bodyclass %}page{% endblock %} + + +{% block extramenu %} +
    +

    Confirm Scheduling for: {{body.current_name}}

    +
    +{% endblock%} + +{% block main-content %} +
    +
    +
    + + {% if data %} +

    Submitted Parameters

    + + + + + + + + + + + + + + + +
    Proposal{{ data.proposal_code }}
    Site{{data.site_code}}
    UTC date{{ data.utc_date}}
    +

    Calculated characteristics

    + + + + + + + + + + + + + + + + + + + + + + + +
    Magnitude{{ data.magnitude|floatformat:2 }}
    Speed{{ data.speed|floatformat:2 }} '/min
    Slot length{{ data.slot_length }} mins
    No. of exposures{{ data.exp_count }}
    Exposure length{{ data.exp_length|floatformat:1 }} secs
    + {% endif %} +
    +
    +
    + {% csrf_token %} + {% for key, error in form.errors.items %} +
    + {{ error }} +
    + + {% empty %} + + {% endfor %} + {% if form.errors%} + Return to schedule parameters page + {% endif%} + {{form}} +
    +
    +
    +
    +{% endblock %} \ No newline at end of file diff --git a/neoexchange/core/tests/__init__.py b/neoexchange/core/tests/__init__.py new file mode 100644 index 000000000..1bb7f2666 --- /dev/null +++ b/neoexchange/core/tests/__init__.py @@ -0,0 +1,2 @@ +from test_forms import * +from test_views import * diff --git a/neoexchange/core/tests/test_forms.py b/neoexchange/core/tests/test_forms.py new file mode 100644 index 000000000..bc13545c7 --- /dev/null +++ b/neoexchange/core/tests/test_forms.py @@ -0,0 +1,96 @@ +''' +NEO exchange: NEO observing portal for Las Cumbres Observatory Global Telescope Network +Copyright (C) 2014-2015 LCOGT + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. +''' + +from django.test import TestCase +from core.models import Body + +#Import module to test +from core.forms import EphemQuery, ScheduleForm + +class EphemQueryFormTest(TestCase): + + + def setUp(self): + params = { 'provisional_name' : 'N999r0q', + 'abs_mag' : 21.0, + 'slope' : 0.15, + 'epochofel' : '2015-03-19 00:00:00', + 'meananom' : 325.2636, + 'argofperih' : 85.19251, + 'longascnode' : 147.81325, + 'orbinc' : 8.34739, + 'eccentricity' : 0.1896865, + 'meandist' : 1.2176312, + 'source_type' : 'U', + 'elements_type' : 'MPC_MINOR_PLANET', + 'active' : True, + 'origin' : 'M', + } + self.body, created = Body.objects.get_or_create(**params) + + def test_form_has_label(self): + form = EphemQuery() + self.assertIn('Enter target name...', form.as_p()) + self.assertIn('Site code:', form.as_p()) + + def test_form_validation_for_blank_target(self): + form = EphemQuery(data = {'target' : ''}) + self.assertFalse(form.is_valid()) + self.assertEqual(form.errors['target'], + ['Target name is required'] + ) + + def test_form_validation_for_blank_date(self): + form = EphemQuery(data = {'utc_date' : ''}) + self.assertFalse(form.is_valid()) + self.assertEqual(form.errors['utc_date'], + ['UTC date is required'] + ) + + def test_form_handles_save(self): + form = EphemQuery(data = {'target' : 'N999r0q', + 'utc_date' : '2015-04-20', + 'site_code' : 'K92', + 'alt_limit' : 30.0 + }) + self.assertTrue(form.is_valid()) + + +class TestScheduleForm(TestCase): + + def setUp(self): + params = { 'provisional_name' : 'N999r0q', + 'abs_mag' : 21.0, + 'slope' : 0.15, + 'epochofel' : '2015-03-19 00:00:00', + 'meananom' : 325.2636, + 'argofperih' : 85.19251, + 'longascnode' : 147.81325, + 'orbinc' : 8.34739, + 'eccentricity' : 0.1896865, + 'meandist' : 1.2176312, + 'source_type' : 'U', + 'elements_type' : 'MPC_MINOR_PLANET', + 'active' : True, + 'origin' : 'M', + } + self.body, created = Body.objects.get_or_create(**params) + + def test_form_has_fields(self): + form = ScheduleForm() + self.assertIsInstance(form, ScheduleForm) + self.assertIn('Proposal', form.as_p()) + self.assertIn('Site code:', form.as_p()) + diff --git a/neoexchange/core/tests/test_views.py b/neoexchange/core/tests/test_views.py new file mode 100644 index 000000000..3441dde08 --- /dev/null +++ b/neoexchange/core/tests/test_views.py @@ -0,0 +1,352 @@ +''' +NEO exchange: NEO observing portal for Las Cumbres Observatory Global Telescope Network +Copyright (C) 2014-2015 LCOGT + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. +''' + +from datetime import datetime +from django.test import TestCase +from django.http import HttpRequest +from django.core.urlresolvers import resolve, reverse +from django.template.loader import render_to_string +from django.views.generic import ListView +from django.forms.models import model_to_dict +from django.utils.html import escape +from django.contrib.auth.models import User +from unittest import skipIf + +#Import module to test +from astrometrics.ephem_subs import call_compute_ephem, determine_darkness_times +from core.views import home, clean_NEOCP_object, save_and_make_revision, update_MPC_orbit +from core.models import Body, Proposal +from core.forms import EphemQuery + + +class TestClean_NEOCP_Object(TestCase): + + def test_X33656(self): + obs_page = [u'X33656 23.9 0.15 K1548 330.99052 282.94050 31.81272 13.02458 0.7021329 0.45261672 1.6800247 3 1 0 days 0.21 NEOCPNomin', + u'X33656 23.9 0.15 K1548 250.56430 257.29551 60.34849 2.58054 0.0797769 0.87078998 1.0860765 3 1 0 days 0.20 NEOCPV0001', + u'X33656 23.9 0.15 K1548 256.86580 263.73491 53.18662 3.17001 0.1297341 0.88070404 1.0779106 3 1 0 days 0.20 NEOCPV0002', + ] + expected_elements = { 'abs_mag' : 23.9, + 'slope' : 0.15, + 'epochofel' : datetime(2015, 4, 8, 0, 0, 0), + 'meananom' : 330.99052, + 'argofperih' : 282.94050, + 'longascnode' : 31.81272, + 'orbinc' : 13.02458, + 'eccentricity': 0.7021329, + # 'MDM': 0.45261672, + 'meandist' : 1.6800247, + 'elements_type': 'MPC_MINOR_PLANET', + 'origin' : 'M', + 'source_type' : 'U', + 'active' : True + } + elements = clean_NEOCP_object(obs_page) + for element in expected_elements: + self.assertEqual(expected_elements[element], elements[element]) + + def test_missing_absmag(self): + obs_page = ['Object H G Epoch M Peri. Node Incl. e n a NObs NOpp Arc r.m.s. Orbit ID', + 'N007riz 0.15 K153J 340.52798 59.01148 160.84695 10.51732 0.3080134 0.56802014 1.4439768 6 1 0 days 0.34 NEOCPNomin', + 'N007riz 0.15 K153J 293.77087 123.25671 129.78437 3.76739 0.0556350 0.93124537 1.0385481 6 1 0 days 0.57 NEOCPV0001' + ] + + expected_elements = { 'abs_mag' : 99.99, + 'slope' : 0.15, + 'epochofel' : datetime(2015, 3, 19, 0, 0, 0), + 'meananom' : 340.52798, + 'argofperih' : 59.01148, + 'longascnode' : 160.84695, + 'orbinc' : 10.51732, + 'eccentricity': 0.3080134, + # 'MDM': 0.56802014, + 'meandist' : 1.4439768, + 'elements_type': 'MPC_MINOR_PLANET', + 'origin' : 'M', + 'source_type' : 'U', + 'active' : True + } + elements = clean_NEOCP_object(obs_page) + for element in expected_elements: + self.assertEqual(expected_elements[element], elements[element]) + + def save_N007riz(self): + obj_id ='N007riz' + elements = { 'abs_mag' : 23.9, + 'slope' : 0.15, + 'epochofel' : datetime(2015, 3, 19, 0, 0, 0), + 'meananom' : 340.52798, + 'argofperih' : 59.01148, + 'longascnode' : 160.84695, + 'orbinc' : 10.51732, + 'eccentricity': 0.3080134, + 'meandist' : 1.4439768, + 'elements_type': 'MPC_MINOR_PLANET', + 'origin' : 'M', + 'source_type' : 'U', + 'active' : True + } + body, created = Body.objects.get_or_create(provisional_name=obj_id) + # We are creating this object + self.assertEqual(True,created) + resp = save_and_make_revision(body,elements) + # We are saving all the detailing elements + self.assertEqual(True,resp) + + def test_revise_N007riz(self): + self.save_N007riz() + obj_id ='N007riz' + elements = { 'abs_mag' : 23.9, + 'slope' : 0.15, + 'epochofel' : datetime(2015, 4, 19, 0, 0, 0), + 'meananom' : 340.52798, + 'argofperih' : 59.01148, + 'longascnode' : 160.84695, + 'orbinc' : 10.51732, + 'eccentricity': 0.4080134, + 'meandist' : 1.4439768, + 'elements_type': 'MPC_MINOR_PLANET', + 'origin' : 'M', + 'source_type' : 'U', + 'active' : False + } + body, created = Body.objects.get_or_create(provisional_name=obj_id) + # Created should now be false + self.assertEqual(False, created) + resp = save_and_make_revision(body,elements) + # Saving the new elements + self.assertEqual(True,resp) + + def test_update_MPC_duplicate(self): + self.save_N007riz() + obj_id ='N007riz' + update_MPC_orbit(obj_id) + + def test_create_discovered_object(self): + obj_id ='LSCTLF8' + elements = { 'abs_mag' : 16.2, + 'slope' : 0.15, + 'epochofel' : datetime(2015, 6, 23, 0, 0, 0), + 'meananom' : 333.70614, + 'argofperih' : 40.75306, + 'longascnode' : 287.97838, + 'orbinc' : 23.61657, + 'eccentricity': 0.1186953, + 'meandist' : 2.7874893, + 'elements_type': 'MPC_MINOR_PLANET', + 'origin' : 'L', + 'source_type' : 'D', + 'active' : True + } + body, created = Body.objects.get_or_create(provisional_name=obj_id) + # We are creating this object + self.assertEqual(True,created) + resp = save_and_make_revision(body,elements) + # Need to call full_clean() to validate the fields as this is not + # done on save() (called by get_or_create() or save_and_make_revision()) + body.full_clean() + # We are saving all the detailing elements + self.assertEqual(True,resp) + + # Test it came from LCOGT as a discovery + self.assertEqual('L', body.origin) + self.assertEqual('D', body.source_type) + + +class HomePageTest(TestCase): + + def setUp(self): + params = { 'provisional_name' : 'N999r0q', + 'abs_mag' : 21.0, + 'slope' : 0.15, + 'epochofel' : '2015-03-19 00:00:00', + 'meananom' : 325.2636, + 'argofperih' : 85.19251, + 'longascnode' : 147.81325, + 'orbinc' : 8.34739, + 'eccentricity' : 0.1896865, + 'meandist' : 1.2176312, + 'source_type' : 'U', + 'elements_type' : 'MPC_MINOR_PLANET', + 'active' : True, + 'origin' : 'M', + } + self.body, created = Body.objects.get_or_create(**params) + + def test_home_page_renders_home_template(self): + response = self.client.get('/') + self.assertTemplateUsed(response, 'core/home.html') + + def test_home_page_uses_ephemquery_form(self): + response = self.client.get('/') + self.assertIsInstance(response.context['form'], EphemQuery) + + +class EphemPageTest(TestCase): + maxDiff = None + + def setUp(self): + params = { 'provisional_name' : 'N999r0q', + 'abs_mag' : 21.0, + 'slope' : 0.15, + 'epochofel' : '2015-03-19 00:00:00', + 'meananom' : 325.2636, + 'argofperih' : 85.19251, + 'longascnode' : 147.81325, + 'orbinc' : 8.34739, + 'eccentricity' : 0.1896865, + 'meandist' : 1.2176312, + 'source_type' : 'U', + 'elements_type' : 'MPC_MINOR_PLANET', + 'active' : True, + 'origin' : 'M', + } + self.body, created = Body.objects.get_or_create(**params) + + def test_home_page_can_save_a_GET_request(self): + + site_code = 'V37' + utc_date = datetime(2015, 4, 21, 3,0,0) + dark_start, dark_end = determine_darkness_times(site_code, utc_date ) + + response = self.client.get(reverse('ephemeris'), + data={'target' : 'N999r0q', + 'site_code' : site_code, + 'utc_date' : '2015-04-21', + 'alt_limit' : 0} + ) + self.assertIn('N999r0q', response.content.decode()) + body_elements = model_to_dict(self.body) + ephem_lines = call_compute_ephem(body_elements, dark_start, dark_end, site_code, '5m' ) + expected_html = render_to_string( + 'core/ephem.html', + {'target' : self.body, + 'ephem_lines' : ephem_lines, + 'site_code' : site_code } + ) + self.assertMultiLineEqual(response.content.decode(), expected_html) + + def test_displays_ephem(self): + response = self.client.get(reverse('ephemeris'), + data ={'target' : 'N999r0q', + 'utc_date' : '2015-05-11', + 'site_code' : 'V37', + 'alt_limit' : 30.0 + } + ) + self.assertContains(response, 'Ephemeris for') + + def test_uses_ephem_template(self): + response = self.client.get('/ephemeris/', + data = {'target' : 'N999r0q', + 'site_code' : 'W86', + 'utc_date' : '2015-04-20', + 'alt_limit' : 40.0 + } + ) + self.assertTemplateUsed(response, 'core/ephem.html') + + def test_form_errors_are_sent_back_to_home_page(self): + response = self.client.get(reverse('ephemeris'), data={'target' : ''}) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'core/home.html') + expected_error = escape("Target name is required") + self.assertContains(response, expected_error) + + def test_ephem_page_displays_site_code(self): + response = self.client.get(reverse('ephemeris'), + data = {'target' : 'N999r0q', + 'site_code' : 'F65', + 'utc_date' : '2015-04-20', + 'alt_limit' : 30.0 + } + ) + self.assertContains(response, 'Ephemeris for N999r0q at F65') + +class TargetsPageTest(TestCase): + + def test_target_url_resolves_to_targets_view(self): + found = reverse('targetlist') + self.assertEqual(found, '/target/') + + @skipIf(True, "to be fixed") + def test_target_page_returns_correct_html(self): + request = HttpRequest() + targetlist = ListView.as_view(model=Body, queryset=Body.objects.filter(active=True)) + response = targetlist.render_to_response(targetlist) + expected_html = render_to_string('core/body_list.html') + self.assertEqual(response, expected_html) + +class ScheduleTargetsPageTest(TestCase): + maxDiff = None + + def setUp(self): + # Initialise with a test body and two test proposals + params = { 'provisional_name' : 'N999r0q', + 'abs_mag' : 21.0, + 'slope' : 0.15, + 'epochofel' : '2015-03-19 00:00:00', + 'meananom' : 325.2636, + 'argofperih' : 85.19251, + 'longascnode' : 147.81325, + 'orbinc' : 8.34739, + 'eccentricity' : 0.1896865, + 'meandist' : 1.2176312, + 'source_type' : 'U', + 'elements_type' : 'MPC_MINOR_PLANET', + 'active' : True, + 'origin' : 'M', + } + self.body, created = Body.objects.get_or_create(**params) + + neo_proposal_params = { 'code' : 'LCO2015A-009', + 'title' : 'LCOGT NEO Follow-up Network' + } + self.neo_proposal, created = Proposal.objects.get_or_create(**neo_proposal_params) + + test_proposal_params = { 'code' : 'LCOEngineering', + 'title' : 'Test Proposal' + } + self.test_proposal, created = Proposal.objects.get_or_create(**test_proposal_params) + # Create a user to test login + self.bart= User.objects.create_user(username='bart', password='simpson', email='bart@simpson.org') + self.bart.first_name= 'Bart' + self.bart.last_name = 'Simpson' + self.bart.is_active=1 + self.bart.save() + + def login(self): + self.assertTrue(self.client.login(username='bart', password='simpson')) + + def test_uses_schedule_template(self): + self.login() + response = self.client.get(reverse('schedule-body', kwargs={'pk':self.body.pk}), + data = {'body_id' : self.body.pk, + 'site_code' : 'F65', + 'utc_date' : '2015-04-20', + } + ) + self.assertTemplateUsed(response, 'core/schedule.html') + + def test_schedule_page_contains_object_name(self): + self.login() + response = self.client.get(reverse('schedule-body', kwargs={'pk':self.body.pk}), + data = {'body_id' : self.body.pk, + 'site_code' : 'F65', + 'utc_date' : '2015-04-20', + 'proposal_code' : self.neo_proposal.code + } + ) + self.assertContains(response, 'Parameters for: ' + self.body.current_name()) diff --git a/neoexchange/ingest/views.py b/neoexchange/core/views.py similarity index 60% rename from neoexchange/ingest/views.py rename to neoexchange/core/views.py index af2e12759..7e8632549 100644 --- a/neoexchange/ingest/views.py +++ b/neoexchange/core/views.py @@ -13,49 +13,66 @@ GNU General Public License for more details. ''' -from datetime import datetime +from datetime import datetime, timedelta from django.db.models import Q from django.forms.models import model_to_dict +from django.contrib.auth.decorators import login_required +from django.core.urlresolvers import reverse from django.shortcuts import render -from django.views.generic import DetailView, ListView -from ingest.ephem_subs import call_compute_ephem, determine_darkness_times -from ingest.forms import EphemQuery -from ingest.models import * -from ingest.sources_subs import fetchpage_and_make_soup, packed_to_normal, fetch_mpcorbit -from ingest.time_subs import extract_mpc_epoch, parse_neocp_date +from django.views.generic import DetailView, ListView, FormView, TemplateView, View +from django.views.generic.detail import SingleObjectMixin +from django.http import Http404 +from astrometrics.ephem_subs import call_compute_ephem, compute_ephem, \ + determine_darkness_times, determine_slot_length, determine_exp_time_count, MagRangeError +from .forms import EphemQuery, ScheduleForm, ScheduleBlockForm +from .models import * +from astrometrics.sources_subs import fetchpage_and_make_soup, packed_to_normal, \ + fetch_mpcorbit, submit_block_to_scheduler +from astrometrics.time_subs import extract_mpc_epoch, parse_neocp_date +from astrometrics.ast_subs import determine_asteroid_type import logging import reversion +import json logger = logging.getLogger(__name__) +class LoginRequiredMixin(object): + @classmethod + def as_view(cls, **initkwargs): + view = super(LoginRequiredMixin, cls).as_view(**initkwargs) + return login_required(view) + + def home(request): + latest = Body.objects.filter(active=True).latest('ingest') + max_dt = latest.ingest + min_dt = max_dt - timedelta(minutes=30) + newest = Body.objects.filter(ingest__range=(min_dt,max_dt) ,active=True) params = { 'targets' : Body.objects.filter(active=True).count(), 'blocks' : Block.objects.filter(active=True).count(), - 'latest' : Body.objects.latest('ingest'), + 'latest' : latest, + 'newest' : newest, 'form' : EphemQuery() } - return render(request,'ingest/home.html',params) + return render(request,'core/home.html',params) class BodyDetailView(DetailView): - context_object_name = "body" model = Body def get_context_data(self, **kwargs): - # Call the base implementation first to get a context context = super(BodyDetailView, self).get_context_data(**kwargs) - # Add in a QuerySet of all the books - context['body_list'] = Body.objects.filter(active=True) + context['form'] = EphemQuery() return context + class BodySearchView(ListView): - template_name = 'ingest/body_list.html' + template_name = 'core/body_list.html' model = Body def get_queryset(self): - print self.kwargs try: name = self.request.REQUEST.get("q") except: @@ -76,15 +93,123 @@ def ephemeris(request): dark_start, dark_end = determine_darkness_times(data['site_code'], data['utc_date']) ephem_lines = call_compute_ephem(body_elements, dark_start, dark_end, data['site_code'], 300, data['alt_limit'] ) else: - return render(request, 'ingest/home.html', {'form' : form}) - - return render(request, 'ingest/ephem.html', - {'new_target_name' : form['target'].value, - 'ephem_lines' : ephem_lines, - 'site_code' : form['site_code'].value(), + return render(request, 'core/home.html', {'form' : form}) + return render(request, 'core/ephem.html', + {'target' : data['target'], + 'ephem_lines' : ephem_lines, + 'site_code' : form['site_code'].value(), } ) +class LookUpBodyMixin(object): + def dispatch(self, request, *args, **kwargs): + try: + body = Body.objects.get(pk=kwargs['pk']) + self.body = body + return super(LookUpBodyMixin, self).dispatch(request, *args, **kwargs) + except Body.DoesNotExist: + raise Http404("Body does not exist") + +class ScheduleParameters(LoginRequiredMixin,LookUpBodyMixin, FormView): + template_name = 'core/schedule.html' + form_class = ScheduleForm + ok_to_schedule = False + + def get(self, request, *args, **kwargs): + form = self.get_form() + # logger.debug(self.body) + return self.render_to_response(self.get_context_data(form=form,body=self.body)) + + def form_valid(self, form, request): + data = schedule_check(form.cleaned_data, self.body, self.ok_to_schedule) + #logger.debug() + new_form = ScheduleBlockForm(data) + return render(request,'core/schedule_confirm.html', {'form':new_form, 'data': data,'body':self.body}) + + def post(self, request, *args, **kwargs): + form = self.get_form() + logger.debug(form) + if form.is_valid(): + return self.form_valid(form, request) + else: + return self.render_to_response(self.get_context_data(form=form,body=self.body)) + +class ScheduleSubmit(LoginRequiredMixin, SingleObjectMixin, FormView): + template_name = 'core/schedule_confirm.html' + form_class = ScheduleBlockForm + model = Body + + def post(self, request, *args, **kwargs): + self.object = self.get_object() + return super(ScheduleSubmit, self).post(request, *args, **kwargs) + + def form_valid(self, form): + response = schedule_submit(form.cleaned_data, self.object) + return super(ScheduleSubmit, self).form_valid(form) + + def get_success_url(self): + return reverse('home') + + +def schedule_check(data,body,ok_to_schedule): + body_elements = model_to_dict(body) + # Check for valid proposal + # validate_proposal_time(data['proposal_code']) + ok_to_schedule = True + + # Determine magnitude + dark_start, dark_end = determine_darkness_times(data['site_code'], data['utc_date']) + dark_midpoint = dark_start + (dark_end-dark_start)/2 + emp = compute_ephem(dark_midpoint, body_elements, data['site_code'], False, False, False) + magnitude = emp[3] + speed = emp[4] + + # Determine slot length + try: + slot_length = determine_slot_length(body_elements['provisional_name'], magnitude, data['site_code']) + except MagRangeError: + slot_length = 0. + ok_to_schedule = False + # Determine exposure length and count + exp_length, exp_count = determine_exp_time_count(speed, data['site_code'], slot_length) + if exp_length == None or exp_count == None: + ok_to_schedule = False + + resp = { + 'target_name' : body.current_name(), + 'magnitude' : magnitude, + 'speed' : speed, + 'slot_length' : slot_length, + 'exp_count' : exp_count, + 'exp_length' : exp_length, + 'schedule_ok' : ok_to_schedule, + 'site_code' : data['site_code'], + 'proposal_code' : data['proposal_code'], + 'group_id' : body.current_name() + '_' + data['site_code'].upper() + '-' + datetime.strftime(data['utc_date'], '%Y%m%d'), + 'utc_date' : data['utc_date'].isoformat(), + 'start_time' : dark_start.isoformat(), + 'end_time' : dark_end.isoformat() + } + return resp + +def schedule_submit(data,body): + # Assemble request + # Send to scheduler + body_elements = model_to_dict(body) + body_elements['epochofel_mjd'] = body.epochofel_mjd() + body_elements['current_name'] = body.current_name() + params = { 'proposal_code' : data['proposal_code'], + 'exp_count' : data['exp_count'], + 'exp_time' : data['exp_length'], + 'site_code' : data['site_code'], + 'start_time' : data['start_time'], + 'end_time' : data['end_time'], + 'group_id' : data['group_id'] + } + # Record block and submit to scheduler + request_number = submit_block_to_scheduler(body_elements, params) + return request_number + def save_and_make_revision(body,kwargs): ''' Make a revision if any of the parameters have changed, but only do it once per ingest not for each parameter ''' @@ -135,7 +260,7 @@ def update_NEOCP_orbit(obj_id, dbg=False): save_and_make_revision(body,kwargs) logger.info("Added %s" % obj_id) else: - save_and_make_revision(check_body,{'active':False}) + save_and_make_revision(body,{'active':False}) logger.info("Object %s no longer exists on the NEOCP." % obj_id) return True @@ -286,13 +411,15 @@ def clean_mpcorbit(elements, dbg=False, origin='M'): if elements != None: params = { 'epochofel' : datetime.strptime(elements['epoch'].replace('.0', ''), '%Y-%m-%d'), + 'abs_mag' : elements['absolute magnitude'], + 'slope' : elements['phase slope'], 'meananom' : elements['mean anomaly'], 'argofperih' : elements['argument of perihelion'], 'longascnode' : elements['ascending node'], 'orbinc' : elements['inclination'], 'eccentricity' : elements['eccentricity'], 'meandist' : elements['semimajor axis'], - 'source_type' : 'A', + 'source_type' : determine_asteroid_type(float(elements['perihelion distance']), float(elements['eccentricity'])), 'elements_type' : 'MPC_MINOR_PLANET', 'active' : True, 'origin' : origin, @@ -301,20 +428,27 @@ def clean_mpcorbit(elements, dbg=False, origin='M'): def update_MPC_orbit(obj_id, dbg=False, origin='M'): - + ''' + Performs remote look up of orbital elements for object with id obj_id, + Gets or creates corresponding Body instance and updates entry + ''' elements = fetch_mpcorbit(obj_id, dbg) - - body, created = Body.objects.get_or_create(name=obj_id) + try: + body, created = Body.objects.get_or_create(name=obj_id) + except Body.MultipleObjectsReturned: + # When the crossid happens we end up with multiple versions of the body. + # Need to pick the one has been most recently updated + bodies = Body.objects.filter(name=obj_id,provisional_name__isnull=False).order_by('-ingest') + created = False + if not bodies: + bodies = Body.objects.filter(name=obj_id).order_by('-ingest') + body = bodies[0] # Determine what type of new object it is and whether to keep it active kwargs = clean_mpcorbit(elements, dbg, origin) + # Save, make revision, or do not update depending on the what has happened to the object + save_and_make_revision(body,kwargs) if not created: - # Find out if the details have changed, if they have, save a revision - check_body = Body.objects.filter(name=obj_id, **kwargs) - if check_body.count() == 1 and check_body[0] == body: - if save_and_make_revision(check_body[0],kwargs): - logger.info("Updated elements for %s" % obj_id) + logger.info("Updated elements for %s" % obj_id) else: - # Didn't know about this object before so create - save_and_make_revision(body,kwargs) logger.info("Added new orbit for %s" % obj_id) return True diff --git a/neoexchange/ingest/forms.py b/neoexchange/ingest/forms.py deleted file mode 100644 index d0377de80..000000000 --- a/neoexchange/ingest/forms.py +++ /dev/null @@ -1,22 +0,0 @@ -from datetime import datetime -from django import forms -from django.db.models import Q -from ingest.models import Body -from django.utils.translation import ugettext as _ - -class EphemQuery(forms.Form): - SITES = (('V37','ELP (V37)'),('F65','FTN (F65)'),('E10', 'FTS (E10)'),('W86','LSC (W85-87)'),('K92','CPT (K91-93)'),('Q63','COJ (Q63-64)')) - target = forms.CharField(label="Enter target name...", max_length=10, required=True, widget=forms.TextInput(attrs={'size':'10'}), error_messages={'required': _(u'Target name is required')}) - site_code = forms.ChoiceField(required=True, choices=SITES) - utc_date = forms.DateField(input_formats=['%Y-%m-%d',], initial=datetime.utcnow().date(), required=True, widget=forms.TextInput(attrs={'size':'10'}), error_messages={'required': _(u'UTC date is required')}) - alt_limit = forms.FloatField(initial=30.0, required=True, widget=forms.TextInput(attrs={'size':'4'})) - - def clean_target(self): - name = self.cleaned_data['target'] - body = Body.objects.filter(Q(provisional_name__icontains = name )|Q(provisional_packed__icontains = name)|Q(name__icontains = name)) - if body.count() == 1 : - return body[0] - elif body.count() == 0: - raise forms.ValidationError("Object not found.") - elif body.count() > 1: - raise forms.ValidationError("Multiple objects found.") \ No newline at end of file diff --git a/neoexchange/ingest/migrations/0001_initial.py b/neoexchange/ingest/migrations/0001_initial.py deleted file mode 100644 index 8b09a31bc..000000000 --- a/neoexchange/ingest/migrations/0001_initial.py +++ /dev/null @@ -1,143 +0,0 @@ -# -*- coding: utf-8 -*- -import datetime -from south.db import db -from south.v2 import SchemaMigration -from django.db import models - - -class Migration(SchemaMigration): - - def forwards(self, orm): - # Adding model 'Proposal' - db.create_table('ingest_proposal', ( - ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - ('code', self.gf('django.db.models.fields.CharField')(max_length=20)), - ('title', self.gf('django.db.models.fields.CharField')(max_length=255)), - )) - db.send_create_signal('ingest', ['Proposal']) - - # Adding model 'Body' - db.create_table('ingest_body', ( - ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - ('provisional_name', self.gf('django.db.models.fields.CharField')(max_length=15, null=True, blank=True)), - ('provisional_packed', self.gf('django.db.models.fields.CharField')(max_length=7, null=True, blank=True)), - ('name', self.gf('django.db.models.fields.CharField')(max_length=15, null=True, blank=True)), - ('origin', self.gf('django.db.models.fields.CharField')(default='M', max_length=1)), - ('source_type', self.gf('django.db.models.fields.CharField')(max_length=1)), - ('elements_type', self.gf('django.db.models.fields.CharField')(max_length=16)), - ('active', self.gf('django.db.models.fields.BooleanField')(default=False)), - ('fast_moving', self.gf('django.db.models.fields.BooleanField')(default=False)), - ('urgency', self.gf('django.db.models.fields.IntegerField')(null=True, blank=True)), - ('epochofel', self.gf('django.db.models.fields.FloatField')()), - ('orbinc', self.gf('django.db.models.fields.FloatField')()), - ('longascnode', self.gf('django.db.models.fields.FloatField')()), - ('argofperih', self.gf('django.db.models.fields.FloatField')()), - ('eccentricity', self.gf('django.db.models.fields.FloatField')()), - ('meandist', self.gf('django.db.models.fields.FloatField')(null=True, blank=True)), - ('meananom', self.gf('django.db.models.fields.FloatField')(null=True, blank=True)), - ('perihdist', self.gf('django.db.models.fields.FloatField')(null=True, blank=True)), - ('epochofperih', self.gf('django.db.models.fields.FloatField')(null=True, blank=True)), - ('ingest', self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime(2015, 2, 28, 0, 0))), - )) - db.send_create_signal('ingest', ['Body']) - - # Adding model 'Block' - db.create_table('ingest_block', ( - ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - ('telclass', self.gf('django.db.models.fields.CharField')(default='1m0', max_length=3)), - ('site', self.gf('django.db.models.fields.CharField')(max_length=3)), - ('body', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['ingest.Body'])), - ('proposal', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['ingest.Proposal'])), - ('block_start', self.gf('django.db.models.fields.DateTimeField')(null=True, blank=True)), - ('block_end', self.gf('django.db.models.fields.DateTimeField')(null=True, blank=True)), - ('tracking_number', self.gf('django.db.models.fields.CharField')(max_length=10, null=True, blank=True)), - ('when_observed', self.gf('django.db.models.fields.DateTimeField')(null=True, blank=True)), - ('active', self.gf('django.db.models.fields.BooleanField')(default=False)), - )) - db.send_create_signal('ingest', ['Block']) - - # Adding model 'Record' - db.create_table('ingest_record', ( - ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - ('site', self.gf('django.db.models.fields.CharField')(max_length=3)), - ('instrument', self.gf('django.db.models.fields.CharField')(max_length=4)), - ('filter', self.gf('django.db.models.fields.CharField')(max_length=15)), - ('filename', self.gf('django.db.models.fields.CharField')(max_length=31)), - ('exp', self.gf('django.db.models.fields.FloatField')()), - ('whentaken', self.gf('django.db.models.fields.DateTimeField')()), - ('block', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['ingest.Block'])), - )) - db.send_create_signal('ingest', ['Record']) - - - def backwards(self, orm): - # Deleting model 'Proposal' - db.delete_table('ingest_proposal') - - # Deleting model 'Body' - db.delete_table('ingest_body') - - # Deleting model 'Block' - db.delete_table('ingest_block') - - # Deleting model 'Record' - db.delete_table('ingest_record') - - - models = { - 'ingest.block': { - 'Meta': {'object_name': 'Block'}, - 'active': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'block_end': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), - 'block_start': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), - 'body': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['ingest.Body']"}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'proposal': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['ingest.Proposal']"}), - 'site': ('django.db.models.fields.CharField', [], {'max_length': '3'}), - 'telclass': ('django.db.models.fields.CharField', [], {'default': "'1m0'", 'max_length': '3'}), - 'tracking_number': ('django.db.models.fields.CharField', [], {'max_length': '10', 'null': 'True', 'blank': 'True'}), - 'when_observed': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}) - }, - 'ingest.body': { - 'Meta': {'object_name': 'Body'}, - 'active': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'argofperih': ('django.db.models.fields.FloatField', [], {}), - 'eccentricity': ('django.db.models.fields.FloatField', [], {}), - 'elements_type': ('django.db.models.fields.CharField', [], {'max_length': '16'}), - 'epochofel': ('django.db.models.fields.FloatField', [], {}), - 'epochofperih': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}), - 'fast_moving': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'ingest': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2015, 2, 28, 0, 0)'}), - 'longascnode': ('django.db.models.fields.FloatField', [], {}), - 'meananom': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}), - 'meandist': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'max_length': '15', 'null': 'True', 'blank': 'True'}), - 'orbinc': ('django.db.models.fields.FloatField', [], {}), - 'origin': ('django.db.models.fields.CharField', [], {'default': "'M'", 'max_length': '1'}), - 'perihdist': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}), - 'provisional_name': ('django.db.models.fields.CharField', [], {'max_length': '15', 'null': 'True', 'blank': 'True'}), - 'provisional_packed': ('django.db.models.fields.CharField', [], {'max_length': '7', 'null': 'True', 'blank': 'True'}), - 'source_type': ('django.db.models.fields.CharField', [], {'max_length': '1'}), - 'urgency': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}) - }, - 'ingest.proposal': { - 'Meta': {'object_name': 'Proposal'}, - 'code': ('django.db.models.fields.CharField', [], {'max_length': '20'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'title': ('django.db.models.fields.CharField', [], {'max_length': '255'}) - }, - 'ingest.record': { - 'Meta': {'object_name': 'Record'}, - 'block': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['ingest.Block']"}), - 'exp': ('django.db.models.fields.FloatField', [], {}), - 'filename': ('django.db.models.fields.CharField', [], {'max_length': '31'}), - 'filter': ('django.db.models.fields.CharField', [], {'max_length': '15'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'instrument': ('django.db.models.fields.CharField', [], {'max_length': '4'}), - 'site': ('django.db.models.fields.CharField', [], {'max_length': '3'}), - 'whentaken': ('django.db.models.fields.DateTimeField', [], {}) - } - } - - complete_apps = ['ingest'] \ No newline at end of file diff --git a/neoexchange/ingest/migrations/0002_auto__chg_field_body_origin__chg_field_body_epochofel__chg_field_body_.py b/neoexchange/ingest/migrations/0002_auto__chg_field_body_origin__chg_field_body_epochofel__chg_field_body_.py deleted file mode 100644 index caa54bfe7..000000000 --- a/neoexchange/ingest/migrations/0002_auto__chg_field_body_origin__chg_field_body_epochofel__chg_field_body_.py +++ /dev/null @@ -1,124 +0,0 @@ -# -*- coding: utf-8 -*- -import datetime -from south.db import db -from south.v2 import SchemaMigration -from django.db import models - - -class Migration(SchemaMigration): - - def forwards(self, orm): - - # Changing field 'Body.origin' - db.alter_column('ingest_body', 'origin', self.gf('django.db.models.fields.CharField')(max_length=1, null=True)) - - # Changing field 'Body.epochofel' - db.alter_column('ingest_body', 'epochofel', self.gf('django.db.models.fields.DateTimeField')(null=True)) - - # Changing field 'Body.epochofperih' - db.alter_column('ingest_body', 'epochofperih', self.gf('django.db.models.fields.DateTimeField')(null=True)) - - # Changing field 'Body.elements_type' - db.alter_column('ingest_body', 'elements_type', self.gf('django.db.models.fields.CharField')(max_length=16, null=True)) - - # Changing field 'Body.source_type' - db.alter_column('ingest_body', 'source_type', self.gf('django.db.models.fields.CharField')(max_length=1, null=True)) - - # Changing field 'Body.longascnode' - db.alter_column('ingest_body', 'longascnode', self.gf('django.db.models.fields.FloatField')(null=True)) - - # Changing field 'Body.orbinc' - db.alter_column('ingest_body', 'orbinc', self.gf('django.db.models.fields.FloatField')(null=True)) - - # Changing field 'Body.eccentricity' - db.alter_column('ingest_body', 'eccentricity', self.gf('django.db.models.fields.FloatField')(null=True)) - - # Changing field 'Body.argofperih' - db.alter_column('ingest_body', 'argofperih', self.gf('django.db.models.fields.FloatField')(null=True)) - - def backwards(self, orm): - - # Changing field 'Body.origin' - db.alter_column('ingest_body', 'origin', self.gf('django.db.models.fields.CharField')(max_length=1)) - - # User chose to not deal with backwards NULL issues for 'Body.epochofel' - raise RuntimeError("Cannot reverse this migration. 'Body.epochofel' and its values cannot be restored.") - - # Changing field 'Body.epochofperih' - db.alter_column('ingest_body', 'epochofperih', self.gf('django.db.models.fields.FloatField')(null=True)) - - # User chose to not deal with backwards NULL issues for 'Body.elements_type' - raise RuntimeError("Cannot reverse this migration. 'Body.elements_type' and its values cannot be restored.") - - # User chose to not deal with backwards NULL issues for 'Body.source_type' - raise RuntimeError("Cannot reverse this migration. 'Body.source_type' and its values cannot be restored.") - - # User chose to not deal with backwards NULL issues for 'Body.longascnode' - raise RuntimeError("Cannot reverse this migration. 'Body.longascnode' and its values cannot be restored.") - - # User chose to not deal with backwards NULL issues for 'Body.orbinc' - raise RuntimeError("Cannot reverse this migration. 'Body.orbinc' and its values cannot be restored.") - - # User chose to not deal with backwards NULL issues for 'Body.eccentricity' - raise RuntimeError("Cannot reverse this migration. 'Body.eccentricity' and its values cannot be restored.") - - # User chose to not deal with backwards NULL issues for 'Body.argofperih' - raise RuntimeError("Cannot reverse this migration. 'Body.argofperih' and its values cannot be restored.") - - models = { - 'ingest.block': { - 'Meta': {'object_name': 'Block'}, - 'active': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'block_end': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), - 'block_start': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), - 'body': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['ingest.Body']"}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'proposal': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['ingest.Proposal']"}), - 'site': ('django.db.models.fields.CharField', [], {'max_length': '3'}), - 'telclass': ('django.db.models.fields.CharField', [], {'default': "'1m0'", 'max_length': '3'}), - 'tracking_number': ('django.db.models.fields.CharField', [], {'max_length': '10', 'null': 'True', 'blank': 'True'}), - 'when_observed': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}) - }, - 'ingest.body': { - 'Meta': {'object_name': 'Body'}, - 'active': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'argofperih': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}), - 'eccentricity': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}), - 'elements_type': ('django.db.models.fields.CharField', [], {'max_length': '16', 'null': 'True', 'blank': 'True'}), - 'epochofel': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), - 'epochofperih': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), - 'fast_moving': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'ingest': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2015, 3, 2, 0, 0)'}), - 'longascnode': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}), - 'meananom': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}), - 'meandist': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'max_length': '15', 'null': 'True', 'blank': 'True'}), - 'orbinc': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}), - 'origin': ('django.db.models.fields.CharField', [], {'default': "'M'", 'max_length': '1', 'null': 'True', 'blank': 'True'}), - 'perihdist': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}), - 'provisional_name': ('django.db.models.fields.CharField', [], {'max_length': '15', 'null': 'True', 'blank': 'True'}), - 'provisional_packed': ('django.db.models.fields.CharField', [], {'max_length': '7', 'null': 'True', 'blank': 'True'}), - 'source_type': ('django.db.models.fields.CharField', [], {'max_length': '1', 'null': 'True', 'blank': 'True'}), - 'urgency': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}) - }, - 'ingest.proposal': { - 'Meta': {'object_name': 'Proposal'}, - 'code': ('django.db.models.fields.CharField', [], {'max_length': '20'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'title': ('django.db.models.fields.CharField', [], {'max_length': '255'}) - }, - 'ingest.record': { - 'Meta': {'object_name': 'Record'}, - 'block': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['ingest.Block']"}), - 'exp': ('django.db.models.fields.FloatField', [], {}), - 'filename': ('django.db.models.fields.CharField', [], {'max_length': '31'}), - 'filter': ('django.db.models.fields.CharField', [], {'max_length': '15'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'instrument': ('django.db.models.fields.CharField', [], {'max_length': '4'}), - 'site': ('django.db.models.fields.CharField', [], {'max_length': '3'}), - 'whentaken': ('django.db.models.fields.DateTimeField', [], {}) - } - } - - complete_apps = ['ingest'] \ No newline at end of file diff --git a/neoexchange/ingest/migrations/0003_auto__add_field_body_abs_mag__add_field_body_slope.py b/neoexchange/ingest/migrations/0003_auto__add_field_body_abs_mag__add_field_body_slope.py deleted file mode 100644 index ad0520512..000000000 --- a/neoexchange/ingest/migrations/0003_auto__add_field_body_abs_mag__add_field_body_slope.py +++ /dev/null @@ -1,88 +0,0 @@ -# -*- coding: utf-8 -*- -import datetime -from south.db import db -from south.v2 import SchemaMigration -from django.db import models - - -class Migration(SchemaMigration): - - def forwards(self, orm): - # Adding field 'Body.abs_mag' - db.add_column('ingest_body', 'abs_mag', - self.gf('django.db.models.fields.FloatField')(null=True, blank=True), - keep_default=False) - - # Adding field 'Body.slope' - db.add_column('ingest_body', 'slope', - self.gf('django.db.models.fields.FloatField')(null=True, blank=True), - keep_default=False) - - - def backwards(self, orm): - # Deleting field 'Body.abs_mag' - db.delete_column('ingest_body', 'abs_mag') - - # Deleting field 'Body.slope' - db.delete_column('ingest_body', 'slope') - - - models = { - 'ingest.block': { - 'Meta': {'object_name': 'Block'}, - 'active': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'block_end': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), - 'block_start': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), - 'body': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['ingest.Body']"}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'proposal': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['ingest.Proposal']"}), - 'site': ('django.db.models.fields.CharField', [], {'max_length': '3'}), - 'telclass': ('django.db.models.fields.CharField', [], {'default': "'1m0'", 'max_length': '3'}), - 'tracking_number': ('django.db.models.fields.CharField', [], {'max_length': '10', 'null': 'True', 'blank': 'True'}), - 'when_observed': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}) - }, - 'ingest.body': { - 'Meta': {'object_name': 'Body'}, - 'abs_mag': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}), - 'active': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'argofperih': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}), - 'eccentricity': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}), - 'elements_type': ('django.db.models.fields.CharField', [], {'max_length': '16', 'null': 'True', 'blank': 'True'}), - 'epochofel': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), - 'epochofperih': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), - 'fast_moving': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'ingest': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2015, 4, 5, 0, 0)'}), - 'longascnode': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}), - 'meananom': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}), - 'meandist': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'max_length': '15', 'null': 'True', 'blank': 'True'}), - 'orbinc': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}), - 'origin': ('django.db.models.fields.CharField', [], {'default': "'M'", 'max_length': '1', 'null': 'True', 'blank': 'True'}), - 'perihdist': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}), - 'provisional_name': ('django.db.models.fields.CharField', [], {'max_length': '15', 'null': 'True', 'blank': 'True'}), - 'provisional_packed': ('django.db.models.fields.CharField', [], {'max_length': '7', 'null': 'True', 'blank': 'True'}), - 'slope': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}), - 'source_type': ('django.db.models.fields.CharField', [], {'max_length': '1', 'null': 'True', 'blank': 'True'}), - 'urgency': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}) - }, - 'ingest.proposal': { - 'Meta': {'object_name': 'Proposal'}, - 'code': ('django.db.models.fields.CharField', [], {'max_length': '20'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'title': ('django.db.models.fields.CharField', [], {'max_length': '255'}) - }, - 'ingest.record': { - 'Meta': {'object_name': 'Record'}, - 'block': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['ingest.Block']"}), - 'exp': ('django.db.models.fields.FloatField', [], {}), - 'filename': ('django.db.models.fields.CharField', [], {'max_length': '31'}), - 'filter': ('django.db.models.fields.CharField', [], {'max_length': '15'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'instrument': ('django.db.models.fields.CharField', [], {'max_length': '4'}), - 'site': ('django.db.models.fields.CharField', [], {'max_length': '3'}), - 'whentaken': ('django.db.models.fields.DateTimeField', [], {}) - } - } - - complete_apps = ['ingest'] \ No newline at end of file diff --git a/neoexchange/ingest/tests/test_ephem_subs.py b/neoexchange/ingest/tests/test_ephem_subs.py deleted file mode 100644 index 642804a1f..000000000 --- a/neoexchange/ingest/tests/test_ephem_subs.py +++ /dev/null @@ -1,226 +0,0 @@ -''' -NEO exchange: NEO observing portal for Las Cumbres Observatory Global Telescope Network -Copyright (C) 2014-2015 LCOGT - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. -''' - -from datetime import datetime -from django.test import TestCase -from django.forms.models import model_to_dict -from rise_set.angle import Angle - -#Import module to test -from ingest.ephem_subs import compute_ephem, call_compute_ephem, get_mountlimits, determine_darkness_times -from ingest.models import Body - - -class TestGetMountLimits(TestCase): - - def compare_limits(self, pos_limit, neg_limit, alt_limit, tel_class): - if tel_class.lower() == '2m': - ha_pos_limit = 12.0 * 15.0 - ha_neg_limit = -12.0 * 15.0 - altitude_limit = 25.0 - elif tel_class.lower() == '1m': - ha_pos_limit = 4.5 * 15.0 - ha_neg_limit = -4.5 * 15.0 - altitude_limit = 30.0 - else: - self.Fail("Unknown telescope class:", tel_class) - self.assertEqual(ha_pos_limit, pos_limit) - self.assertEqual(ha_neg_limit, neg_limit) - self.assertEqual(altitude_limit, alt_limit) - - def test_2m_by_site(self): - (neg_limit, pos_limit, alt_limit) = get_mountlimits('OGG-CLMA-2M0A') - self.compare_limits(pos_limit, neg_limit, alt_limit, '2m') - - def test_2m_by_site_code(self): - (neg_limit, pos_limit, alt_limit) = get_mountlimits('F65') - self.compare_limits(pos_limit, neg_limit, alt_limit, '2m') - - def test_2m_by_site_code_lowercase(self): - (neg_limit, pos_limit, alt_limit) = get_mountlimits('f65') - self.compare_limits(pos_limit, neg_limit, alt_limit, '2m') - - def test_1m_by_site(self): - (neg_limit, pos_limit, alt_limit) = get_mountlimits('ELP-DOMA-1m0A') - self.compare_limits(pos_limit, neg_limit, alt_limit, '1m') - - def test_1m_by_site_code(self): - (neg_limit, pos_limit, alt_limit) = get_mountlimits('K91') - self.compare_limits(pos_limit, neg_limit, alt_limit, '1m') - - def test_1m_by_site_code_lowercase(self): - (neg_limit, pos_limit, alt_limit) = get_mountlimits('q63') - self.compare_limits(pos_limit, neg_limit, alt_limit, '1m') - - -class TestComputeEphem(TestCase): - - def setUp(self): - params = { 'provisional_name' : 'N999r0q', - 'abs_mag' : 21.0, - 'slope' : 0.15, - 'epochofel' : '2015-03-19 00:00:00', - 'meananom' : 325.2636, - 'argofperih' : 85.19251, - 'longascnode' : 147.81325, - 'orbinc' : 8.34739, - 'eccentricity' : 0.1896865, - 'meandist' : 1.2176312, - 'source_type' : 'U', - 'elements_type' : 'MPC_MINOR_PLANET', - 'active' : True, - 'origin' : 'M', - } - self.body = Body.objects.create(**params) - self.body.save() - - self.elements = {'G': 0.15, - 'H': 21.0, - 'MDM': Angle(degrees=0.74394528), - 'arg_perihelion': Angle(degrees=85.19251), - 'eccentricity': 0.1896865, - 'epoch': 57100.0, - 'inclination': Angle(degrees=8.34739), - 'long_node': Angle(degrees=147.81325), - 'mean_anomaly': Angle(degrees=325.2636), - 'n_nights': 3, - 'n_obs': 17, - 'n_oppos': 1, - 'name': 'N007r0q', - 'reference': '', - 'residual': 0.53, - 'semi_axis': 1.2176312, - 'type': 'MPC_MINOR_PLANET', - 'uncertainty': 'U'} - - def test_body_is_correct_class(self): - tbody = Body.objects.get(provisional_name='N999r0q') - self.assertIsInstance(tbody, Body) - - def test_save_and_retrieve_bodies(self): - first_body = Body.objects.get(provisional_name='N999r0q') - body_dict = model_to_dict(first_body) - - body_dict['provisional_name'] = 'N999z0z' - body_dict['eccentricity'] = 0.42 - body_dict['id'] += 1 - second_body = Body.objects.create(**body_dict) - second_body.save() - - saved_items = Body.objects.all() - self.assertEqual(saved_items.count(), 2) - - first_saved_item = saved_items[0] - second_saved_item = saved_items[1] - self.assertEqual(first_saved_item.provisional_name, 'N999r0q') - self.assertEqual(second_saved_item.provisional_name, 'N999z0z') - - def test_compute_ephem_with_elements(self): - d = datetime(2015, 4, 21, 17, 35, 00) - expected_ra = 5.28722753669144 - expected_dec = 0.522637696108887 - expected_mag = 20.408525362626005 - expected_motion = 2.4825093417658186 - expected_alt = -58.658929026981895 - emp_line = compute_ephem(d, self.elements, '?', dbg=False, perturb=True, display=False) - self.assertEqual(d, emp_line[0]) - precision = 11 - self.assertAlmostEqual(expected_ra, emp_line[1], precision) - self.assertAlmostEqual(expected_dec, emp_line[2], precision) - self.assertAlmostEqual(expected_mag, emp_line[3], precision) - self.assertAlmostEqual(expected_motion, emp_line[4], precision) - self.assertAlmostEqual(expected_alt, emp_line[5], precision) - - def test_compute_ephem_with_body(self): - d = datetime(2015, 4, 21, 17, 35, 00) - expected_ra = 5.28722753669144 - expected_dec = 0.522637696108887 - expected_mag = 20.408525362626005 - expected_motion = 2.4825093417658186 - expected_alt = -58.658929026981895 - body_elements = model_to_dict(self.body) - emp_line = compute_ephem(d, body_elements, '?', dbg=False, perturb=True, display=False) - self.assertEqual(d, emp_line[0]) - precision = 11 - self.assertAlmostEqual(expected_ra, emp_line[1], precision) - self.assertAlmostEqual(expected_dec, emp_line[2], precision) - self.assertAlmostEqual(expected_mag, emp_line[3], precision) - self.assertAlmostEqual(expected_motion, emp_line[4], precision) - self.assertAlmostEqual(expected_alt, emp_line[5], precision) - - def test_call_compute_ephem_with_body(self): - start = datetime(2015, 4, 21, 8, 45, 00) - end = datetime(2015, 4, 21, 8, 51, 00) - site_code = 'V37' - step_size = 300 - body_elements = model_to_dict(self.body) - expected_ephem_lines = [['2015 04 21 08:45', '20 10 05.99', '+29 56 57.5', '20.4', ' 2.43', '+33', '0.09', '107', '-42', '+047', '-04:25'], - ['2015 04 21 08:50', '20 10 06.92', '+29 56 57.7', '20.4', ' 2.42', '+34', '0.09', '107', '-42', '+048', '-04:20']] - ephem_lines = call_compute_ephem(body_elements, start, end, site_code, step_size) - line = 0 - self.assertEqual(len(expected_ephem_lines), len(ephem_lines)) - while line < len(expected_ephem_lines): - self.assertEqual(expected_ephem_lines[line], ephem_lines[line]) - line += 1 - - def test_call_compute_ephem_with_body_F65(self): - start = datetime(2015, 4, 21, 11, 30, 00) - end = datetime(2015, 4, 21, 11, 35, 01) - site_code = 'F65' - step_size = 300 - body_elements = model_to_dict(self.body) - expected_ephem_lines = [['2015 04 21 11:30', '20 10 38.15', '+29 56 52.1', '20.4', ' 2.45', '+20', '0.09', '108', '-47', '-999', '-05:09'], - ['2015 04 21 11:35', '20 10 39.09', '+29 56 52.4', '20.4', ' 2.45', '+21', '0.09', '108', '-48', '-999', '-05:04']] - - ephem_lines = call_compute_ephem(body_elements, start, end, site_code, step_size) - line = 0 - self.assertEqual(len(expected_ephem_lines), len(ephem_lines)) - while line < len(expected_ephem_lines): - self.assertEqual(expected_ephem_lines[line], ephem_lines[line]) - line += 1 - - def test_call_compute_ephem_with_date(self): - start = datetime(2015, 4, 28, 10, 20, 00) - end = datetime(2015, 4, 28, 10, 25, 01) - site_code = 'V37' - step_size = 300 - body_elements = model_to_dict(self.body) - expected_ephem_lines = [['2015 04 28 10:20', '20 40 36.53', '+29 36 33.1', '20.6', ' 2.08', '+52', '0.72', '136', '-15', '+058', '-02:53'], - ['2015 04 28 10:25', '20 40 37.32', '+29 36 32.5', '20.6', ' 2.08', '+54', '0.72', '136', '-16', '+059', '-02:48']] - - ephem_lines = call_compute_ephem(body_elements, start, end, site_code, step_size) - line = 0 - self.assertEqual(len(expected_ephem_lines), len(ephem_lines)) - while line < len(expected_ephem_lines): - self.assertEqual(expected_ephem_lines[line], ephem_lines[line]) - line += 1 - - def test_call_compute_ephem_with_altlimit(self): - start = datetime(2015, 9, 3, 17, 20, 00) - end = datetime(2015, 9, 3, 19, 40, 01) - site_code = 'K91' - step_size = 300 - alt_limit = 30 - body_elements = model_to_dict(self.body) - expected_ephem_lines = [['2015 09 03 19:35', '23 53 33.81', '-12 45 53.8', '19.3', ' 1.87', '+30', '0.67', ' 57', '-26', '+039', '-04:05'], - ['2015 09 03 19:40', '23 53 33.46', '-12 46 01.5', '19.3', ' 1.87', '+32', '0.67', ' 58', '-25', '+040', '-04:00']] - - ephem_lines = call_compute_ephem(body_elements, start, end, - site_code, step_size, alt_limit) - line = 0 - self.assertEqual(len(expected_ephem_lines), len(ephem_lines)) - while line < len(expected_ephem_lines): - self.assertEqual(expected_ephem_lines[line], ephem_lines[line]) - line += 1 diff --git a/neoexchange/ingest/tests/test_views.py b/neoexchange/ingest/tests/test_views.py deleted file mode 100644 index 43caf867f..000000000 --- a/neoexchange/ingest/tests/test_views.py +++ /dev/null @@ -1,185 +0,0 @@ -''' -NEO exchange: NEO observing portal for Las Cumbres Observatory Global Telescope Network -Copyright (C) 2014-2015 LCOGT - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. -''' - -from datetime import datetime -from django.test import TestCase -from django.http import HttpRequest -from django.core.urlresolvers import resolve, reverse -from django.template.loader import render_to_string -from django.views.generic import ListView -from django.forms.models import model_to_dict -from django.utils.html import escape -from unittest import skipIf - -#Import module to test -from ingest.ephem_subs import call_compute_ephem, determine_darkness_times -from ingest.views import home, clean_NEOCP_object -from ingest.models import Body - - -class TestClean_NEOCP_Object(TestCase): - - def test_X33656(self): - obs_page = [u'X33656 23.9 0.15 K1548 330.99052 282.94050 31.81272 13.02458 0.7021329 0.45261672 1.6800247 3 1 0 days 0.21 NEOCPNomin', - u'X33656 23.9 0.15 K1548 250.56430 257.29551 60.34849 2.58054 0.0797769 0.87078998 1.0860765 3 1 0 days 0.20 NEOCPV0001', - u'X33656 23.9 0.15 K1548 256.86580 263.73491 53.18662 3.17001 0.1297341 0.88070404 1.0779106 3 1 0 days 0.20 NEOCPV0002', - ] - expected_elements = { 'abs_mag' : 23.9, - 'slope' : 0.15, - 'epochofel' : datetime(2015, 4, 8, 0, 0, 0), - 'meananom' : 330.99052, - 'argofperih' : 282.94050, - 'longascnode' : 31.81272, - 'orbinc' : 13.02458, - 'eccentricity': 0.7021329, - # 'MDM': 0.45261672, - 'meandist' : 1.6800247, - 'elements_type': 'MPC_MINOR_PLANET', - 'origin' : 'M', - 'source_type' : 'U', - 'active' : True - } - elements = clean_NEOCP_object(obs_page) - for element in expected_elements: - self.assertEqual(expected_elements[element], elements[element]) - - def test_missing_absmag(self): - obs_page = ['Object H G Epoch M Peri. Node Incl. e n a NObs NOpp Arc r.m.s. Orbit ID', - 'N007riz 0.15 K153J 340.52798 59.01148 160.84695 10.51732 0.3080134 0.56802014 1.4439768 6 1 0 days 0.34 NEOCPNomin', - 'N007riz 0.15 K153J 293.77087 123.25671 129.78437 3.76739 0.0556350 0.93124537 1.0385481 6 1 0 days 0.57 NEOCPV0001' - ] - - expected_elements = { 'abs_mag' : 99.99, - 'slope' : 0.15, - 'epochofel' : datetime(2015, 3, 19, 0, 0, 0), - 'meananom' : 340.52798, - 'argofperih' : 59.01148, - 'longascnode' : 160.84695, - 'orbinc' : 10.51732, - 'eccentricity': 0.3080134, - # 'MDM': 0.56802014, - 'meandist' : 1.4439768, - 'elements_type': 'MPC_MINOR_PLANET', - 'origin' : 'M', - 'source_type' : 'U', - 'active' : True - } - elements = clean_NEOCP_object(obs_page) - for element in expected_elements: - self.assertEqual(expected_elements[element], elements[element]) - -class HomePageTest(TestCase): - - def test_root_url_resolves_to_home_page_view(self): - found = resolve('/') - self.assertEqual(found.func, home) - - def test_home_page_returns_correct_html(self): - request = HttpRequest() - response = home(request) - expected_html = render_to_string('ingest/home.html') - self.assertEqual(response.content.decode(), expected_html) - - def test_home_page_redirects_after_GET(self): - request = HttpRequest() - request.method = 'GET' - request.GET['target_name'] = 'New target' - - response = home(request) - - self.assertEqual(response.status_code, 302) - self.assertEqual(response['location'], '/ephemeris/') - - def test_home_page_ephem_form_shows_current_date(self): - pass - -class EphemPageTest(TestCase): - maxDiff = None - - def setUp(self): - params = { 'provisional_name' : 'N999r0q', - 'abs_mag' : 21.0, - 'slope' : 0.15, - 'epochofel' : '2015-03-19 00:00:00', - 'meananom' : 325.2636, - 'argofperih' : 85.19251, - 'longascnode' : 147.81325, - 'orbinc' : 8.34739, - 'eccentricity' : 0.1896865, - 'meandist' : 1.2176312, - 'source_type' : 'U', - 'elements_type' : 'MPC_MINOR_PLANET', - 'active' : True, - 'origin' : 'M', - } - self.body = Body.objects.create(**params) - self.body.save() - - def test_home_page_can_save_a_GET_request(self): - - site_code = 'V37' - utc_date = datetime(2015, 4, 21, 3,0,0) - dark_start, dark_end = determine_darkness_times(site_code, utc_date ) - - response = self.client.get('/ephemeris/', - data={'target_name' : 'N999r0q', - 'site_code' : site_code, - 'utc_date' : '2015-04-21'} - ) - self.assertIn('N999r0q', response.content.decode()) - body_elements = model_to_dict(self.body) - ephem_lines = call_compute_ephem(body_elements, dark_start, dark_end, site_code, '5m' ) - expected_html = render_to_string( - 'ingest/ephem.html', - {'new_target_name' : 'N999r0q', - 'ephem_lines' : ephem_lines, - 'site_code' : site_code } - ) - self.assertMultiLineEqual(response.content.decode(), expected_html) - - def test_displays_ephem(self): - response = self.client.get('/ephemeris/', data={'target_name' : 'N999r0q'}) - self.assertContains(response, 'Computing ephemeris for') - - def test_uses_ephem_template(self): - response = self.client.get('/ephemeris/', data={'target_name' : 'N999r0q'}) - self.assertTemplateUsed(response, 'ingest/ephem.html') - - def test_form_errors_are_sent_back_to_home_page(self): - response = self.client.get('/ephemeris/', data={'target_name' : ''}) - self.assertEqual(response.status_code, 200) - self.assertTemplateUsed(response, 'ingest/home.html') - expected_error = escape("You didn't specify a target") - self.assertContains(response, expected_error) - - def test_ephem_page_displays_site_code(self): - response = self.client.get('/ephemeris/', - data={'target_name' : 'N999r0q', 'site_code' : 'F65'}) - self.assertContains(response, - 'Computing ephemeris for: N999r0q for F65') - -class TargetsPageTest(TestCase): - - def test_target_url_resolves_to_targets_view(self): - found = reverse('targetlist') - self.assertEqual(found, '/target/') - - @skipIf(True, "to be fixed") - def test_target_page_returns_correct_html(self): - request = HttpRequest() - targetlist = ListView.as_view(model=Body, queryset=Body.objects.filter(active=True)) - response = targetlist.render_to_response(targetlist) - expected_html = render_to_string('ingest/body_list.html') - self.assertEqual(response, expected_html) diff --git a/neoexchange/initial_data.json b/neoexchange/initial_data.json deleted file mode 100644 index 8e337e6b2..000000000 --- a/neoexchange/initial_data.json +++ /dev/null @@ -1,380 +0,0 @@ -[ - { - "pk": 19, - "model": "auth.permission", - "fields": { - "codename": "add_logentry", - "name": "Can add log entry", - "content_type": 7 - } - }, - { - "pk": 20, - "model": "auth.permission", - "fields": { - "codename": "change_logentry", - "name": "Can change log entry", - "content_type": 7 - } - }, - { - "pk": 21, - "model": "auth.permission", - "fields": { - "codename": "delete_logentry", - "name": "Can delete log entry", - "content_type": 7 - } - }, - { - "pk": 4, - "model": "auth.permission", - "fields": { - "codename": "add_group", - "name": "Can add group", - "content_type": 2 - } - }, - { - "pk": 5, - "model": "auth.permission", - "fields": { - "codename": "change_group", - "name": "Can change group", - "content_type": 2 - } - }, - { - "pk": 6, - "model": "auth.permission", - "fields": { - "codename": "delete_group", - "name": "Can delete group", - "content_type": 2 - } - }, - { - "pk": 1, - "model": "auth.permission", - "fields": { - "codename": "add_permission", - "name": "Can add permission", - "content_type": 1 - } - }, - { - "pk": 2, - "model": "auth.permission", - "fields": { - "codename": "change_permission", - "name": "Can change permission", - "content_type": 1 - } - }, - { - "pk": 3, - "model": "auth.permission", - "fields": { - "codename": "delete_permission", - "name": "Can delete permission", - "content_type": 1 - } - }, - { - "pk": 7, - "model": "auth.permission", - "fields": { - "codename": "add_user", - "name": "Can add user", - "content_type": 3 - } - }, - { - "pk": 8, - "model": "auth.permission", - "fields": { - "codename": "change_user", - "name": "Can change user", - "content_type": 3 - } - }, - { - "pk": 9, - "model": "auth.permission", - "fields": { - "codename": "delete_user", - "name": "Can delete user", - "content_type": 3 - } - }, - { - "pk": 10, - "model": "auth.permission", - "fields": { - "codename": "add_contenttype", - "name": "Can add content type", - "content_type": 4 - } - }, - { - "pk": 11, - "model": "auth.permission", - "fields": { - "codename": "change_contenttype", - "name": "Can change content type", - "content_type": 4 - } - }, - { - "pk": 12, - "model": "auth.permission", - "fields": { - "codename": "delete_contenttype", - "name": "Can delete content type", - "content_type": 4 - } - }, - { - "pk": 31, - "model": "auth.permission", - "fields": { - "codename": "add_block", - "name": "Can add Observation Block", - "content_type": 11 - } - }, - { - "pk": 32, - "model": "auth.permission", - "fields": { - "codename": "change_block", - "name": "Can change Observation Block", - "content_type": 11 - } - }, - { - "pk": 33, - "model": "auth.permission", - "fields": { - "codename": "delete_block", - "name": "Can delete Observation Block", - "content_type": 11 - } - }, - { - "pk": 28, - "model": "auth.permission", - "fields": { - "codename": "add_body", - "name": "Can add Minor Body", - "content_type": 10 - } - }, - { - "pk": 29, - "model": "auth.permission", - "fields": { - "codename": "change_body", - "name": "Can change Minor Body", - "content_type": 10 - } - }, - { - "pk": 30, - "model": "auth.permission", - "fields": { - "codename": "delete_body", - "name": "Can delete Minor Body", - "content_type": 10 - } - }, - { - "pk": 25, - "model": "auth.permission", - "fields": { - "codename": "add_proposal", - "name": "Can add proposal", - "content_type": 9 - } - }, - { - "pk": 26, - "model": "auth.permission", - "fields": { - "codename": "change_proposal", - "name": "Can change proposal", - "content_type": 9 - } - }, - { - "pk": 27, - "model": "auth.permission", - "fields": { - "codename": "delete_proposal", - "name": "Can delete proposal", - "content_type": 9 - } - }, - { - "pk": 34, - "model": "auth.permission", - "fields": { - "codename": "add_record", - "name": "Can add Observation Record", - "content_type": 12 - } - }, - { - "pk": 35, - "model": "auth.permission", - "fields": { - "codename": "change_record", - "name": "Can change Observation Record", - "content_type": 12 - } - }, - { - "pk": 36, - "model": "auth.permission", - "fields": { - "codename": "delete_record", - "name": "Can delete Observation Record", - "content_type": 12 - } - }, - { - "pk": 37, - "model": "auth.permission", - "fields": { - "codename": "add_revision", - "name": "Can add revision", - "content_type": 13 - } - }, - { - "pk": 38, - "model": "auth.permission", - "fields": { - "codename": "change_revision", - "name": "Can change revision", - "content_type": 13 - } - }, - { - "pk": 39, - "model": "auth.permission", - "fields": { - "codename": "delete_revision", - "name": "Can delete revision", - "content_type": 13 - } - }, - { - "pk": 40, - "model": "auth.permission", - "fields": { - "codename": "add_version", - "name": "Can add version", - "content_type": 14 - } - }, - { - "pk": 41, - "model": "auth.permission", - "fields": { - "codename": "change_version", - "name": "Can change version", - "content_type": 14 - } - }, - { - "pk": 42, - "model": "auth.permission", - "fields": { - "codename": "delete_version", - "name": "Can delete version", - "content_type": 14 - } - }, - { - "pk": 13, - "model": "auth.permission", - "fields": { - "codename": "add_session", - "name": "Can add session", - "content_type": 5 - } - }, - { - "pk": 14, - "model": "auth.permission", - "fields": { - "codename": "change_session", - "name": "Can change session", - "content_type": 5 - } - }, - { - "pk": 15, - "model": "auth.permission", - "fields": { - "codename": "delete_session", - "name": "Can delete session", - "content_type": 5 - } - }, - { - "pk": 16, - "model": "auth.permission", - "fields": { - "codename": "add_site", - "name": "Can add site", - "content_type": 6 - } - }, - { - "pk": 17, - "model": "auth.permission", - "fields": { - "codename": "change_site", - "name": "Can change site", - "content_type": 6 - } - }, - { - "pk": 18, - "model": "auth.permission", - "fields": { - "codename": "delete_site", - "name": "Can delete site", - "content_type": 6 - } - }, - { - "pk": 22, - "model": "auth.permission", - "fields": { - "codename": "add_migrationhistory", - "name": "Can add migration history", - "content_type": 8 - } - }, - { - "pk": 23, - "model": "auth.permission", - "fields": { - "codename": "change_migrationhistory", - "name": "Can change migration history", - "content_type": 8 - } - }, - { - "pk": 24, - "model": "auth.permission", - "fields": { - "codename": "delete_migrationhistory", - "name": "Can delete migration history", - "content_type": 8 - } - } -] \ No newline at end of file diff --git a/neoexchange/neox/admin.py b/neoexchange/neox/admin.py index f9f3fe5f8..aebe627e0 100644 --- a/neoexchange/neox/admin.py +++ b/neoexchange/neox/admin.py @@ -12,7 +12,7 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. ''' -from ingest.models import * +from core.models import * from django.contrib import admin import reversion diff --git a/neoexchange/neox/settings.py b/neoexchange/neox/settings.py index 067c6bdfe..d182497aa 100644 --- a/neoexchange/neox/settings.py +++ b/neoexchange/neox/settings.py @@ -2,10 +2,11 @@ # Django settings for neox project. import os, sys +from django.utils.crypto import get_random_string CURRENT_PATH = os.path.dirname(os.path.realpath(__file__)) PRODUCTION = True if CURRENT_PATH.startswith('/var/www') else False -DEBUG = False +DEBUG = True BRANCH = os.environ.get('BRANCH',None) if BRANCH: BRANCH = '-' + BRANCH @@ -15,8 +16,6 @@ PREFIX ="" BASE_DIR = os.path.dirname(CURRENT_PATH) -TEMPLATE_DEBUG = DEBUG - ADMINS = ( # ('Your Name', 'your_email@example.com'), ) @@ -63,37 +62,22 @@ STATIC_ROOT = '/var/www/html/static/' STATIC_URL = PREFIX + '/static/' -STATICFILES_DIRS = [os.path.join(BASE_DIR,'ingest'),] +STATICFILES_DIRS = [os.path.join(BASE_DIR,'core'),] # List of finder classes that know how to find static files in # various locations. STATICFILES_FINDERS = ( - 'django.contrib.staticfiles.finders.FileSystemFinder', - 'django.contrib.staticfiles.finders.AppDirectoriesFinder', -# 'django.contrib.staticfiles.finders.DefaultStorageFinder', -) - -# List of callables that know how to import templates from various sources. -TEMPLATE_LOADERS = ( - 'django.template.loaders.filesystem.Loader', - 'django.template.loaders.app_directories.Loader', -# 'django.template.loaders.eggs.Loader', -) - -TEMPLATE_CONTEXT_PROCESSORS = ( - "django.core.context_processors.request", - 'django.contrib.auth.context_processors.auth', -) + "django.contrib.staticfiles.finders.FileSystemFinder", + "django.contrib.staticfiles.finders.AppDirectoriesFinder" + ) MIDDLEWARE_CLASSES = ( + 'opbeat.contrib.django.middleware.OpbeatAPMMiddleware', 'django.middleware.common.CommonMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.middleware.transaction.TransactionMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', - # Uncomment the next line for simple clickjacking protection: - # 'django.middleware.clickjacking.XFrameOptionsMiddleware', ) ROOT_URLCONF = 'neox.urls' @@ -101,17 +85,30 @@ # Python dotted path to the WSGI application used by Django's runserver. WSGI_APPLICATION = 'neox.wsgi.application' -TEMPLATE_DIRS = ( - # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". - # Always use forward slashes, even on Windows. - # Don't forget to use absolute paths, not relative paths. - os.path.join(BASE_DIR,'ingest','templates'), -) +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +LOGIN_REDIRECT_URL = '/' # GRAPPELLI_INDEX_DASHBOARD = 'neox.dashboard.CustomIndexDashboard' INSTALLED_APPS = ( 'grappelli', + 'neox', + 'core', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', @@ -119,24 +116,16 @@ 'django.contrib.staticfiles', 'django.contrib.admin', 'django.contrib.messages', - 'neox', - 'ingest', 'reversion', - 'south' + 'opbeat.contrib.django', ) -################## -# LOCAL SETTINGS # -################## - -# Allow any settings to be defined in local_settings.py which should be -# ignored in your version control system allowing for settings to be -# defined per machine. -try: - from local_settings import * -except ImportError as e: - if "local_settings" not in str(e): - raise e +OPBEAT = { + 'ORGANIZATION_ID': os.environ.get('NEOX_OPBEAT_ORGID',''), + 'APP_ID': os.environ.get('NEOX_OPBEAT_APPID',''), + 'SECRET_TOKEN': os.environ.get('NEOX_OPBEAT_TOKEN',''), + 'DEBUG': False, +} LOGGING = { 'version': 1, @@ -169,7 +158,7 @@ 'filters': ['require_debug_false'] }, 'console': { - 'level': 'INFO', + 'level': 'DEBUG', 'class': 'logging.StreamHandler', } }, @@ -180,14 +169,67 @@ 'propagate': True, }, 'django': { - 'handlers':['file','console'], + 'handlers':['file'], 'propagate': True, - 'level':'DEBUG', + 'level':'ERROR', }, - 'ingest' : { + 'core' : { 'handlers' : ['file','console'], 'level' : 'DEBUG', } } } +chars = 'abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+)' +SECRET_KEY = get_random_string(50, chars) + +DATABASES = { + "default": { + # Live DB + "ENGINE": "django.db.backends.mysql", + "NAME": "neoexchange", + "USER": os.environ.get('NEOX_DB_USER',''), + "PASSWORD": os.environ.get('NEOX_DB_PASSWD',''), + "HOST": os.environ.get('NEOX_DB_HOST',''), + "OPTIONS" : {'init_command': 'SET storage_engine=INNODB'}, + + } +} + +####################### +# Test Database setup # +####################### + +if 'test' in sys.argv: + # If you also want to speed up password hashing in test cases. + PASSWORD_HASHERS = ( + 'django.contrib.auth.hashers.MD5PasswordHasher', + ) + # Use SQLite3 for the database engine during testing. + DATABASES = { 'default': + { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': 'test_db', # Add the name of your SQLite3 database file here. + }, + 'rbauth': + { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': 'test_rbauth', # Add the name of your SQLite3 database file here. + } + } + +################## +# LOCAL SETTINGS # +################## + +# Allow any settings to be defined in local_settings.py which should be +# ignored in your version control system allowing for settings to be +# defined per machine. +if not CURRENT_PATH.startswith('/var/www'): + try: + from local_settings import * + except ImportError as e: + if "local_settings" not in str(e): + raise e + + diff --git a/neoexchange/neox/tests/__init__.py b/neoexchange/neox/tests/__init__.py index 5d4332eea..138cec777 100644 --- a/neoexchange/neox/tests/__init__.py +++ b/neoexchange/neox/tests/__init__.py @@ -2,3 +2,4 @@ from test_ephemeris_validation import * from test_layout_and_styling import * from test_targets_validation import * +from test_schedule_observations import * diff --git a/neoexchange/neox/tests/base.py b/neoexchange/neox/tests/base.py index d716aa55e..292398f23 100644 --- a/neoexchange/neox/tests/base.py +++ b/neoexchange/neox/tests/base.py @@ -1,11 +1,45 @@ -from django.test import LiveServerTestCase +from django.contrib.staticfiles.testing import StaticLiveServerTestCase from selenium import webdriver +from core.models import Body, Proposal -class FunctionalTest(LiveServerTestCase): +class FunctionalTest(StaticLiveServerTestCase): + + + def insert_test_body(self): + params = { 'provisional_name' : 'N999r0q', + 'abs_mag' : 21.0, + 'slope' : 0.15, + 'epochofel' : '2015-03-19 00:00:00', + 'meananom' : 325.2636, + 'argofperih' : 85.19251, + 'longascnode' : 147.81325, + 'orbinc' : 8.34739, + 'eccentricity' : 0.1896865, + 'meandist' : 1.2176312, + 'source_type' : 'U', + 'elements_type' : 'MPC_MINOR_PLANET', + 'active' : True, + 'origin' : 'M', + } + self.body, created = Body.objects.get_or_create(**params) + + def insert_test_proposals(self): + + neo_proposal_params = { 'code' : 'LCO2015A-009', + 'title' : 'LCOGT NEO Follow-up Network' + } + self.neo_proposal, created = Proposal.objects.get_or_create(**neo_proposal_params) + + test_proposal_params = { 'code' : 'LCOEngineering', + 'title' : 'Test Proposal' + } + self.test_proposal, created = Proposal.objects.get_or_create(**test_proposal_params) def setUp(self): self.browser = webdriver.Firefox() self.browser.implicitly_wait(5) + self.insert_test_body() + self.insert_test_proposals() def tearDown(self): self.browser.refresh() @@ -25,3 +59,8 @@ def check_for_header_in_table(self, table_id, header_text): def get_item_input_box(self, element_id='id_target'): return self.browser.find_element_by_id(element_id) + + def get_item_input_box_and_clear(self, element_id='id_target'): + inputbox = self.browser.find_element_by_id(element_id) + inputbox.clear() + return inputbox diff --git a/neoexchange/neox/tests/test_ephemeris_creation.py b/neoexchange/neox/tests/test_ephemeris_creation.py index 7b651ab9f..04999cc68 100644 --- a/neoexchange/neox/tests/test_ephemeris_creation.py +++ b/neoexchange/neox/tests/test_ephemeris_creation.py @@ -3,7 +3,7 @@ from selenium.webdriver.common.keys import Keys from selenium.webdriver.support.ui import Select from datetime import datetime -from ingest.models import Body +from core.models import Body class NewVisitorTest(FunctionalTest): @@ -28,8 +28,6 @@ def insert_test_body(self): def test_can_compute_ephemeris(self): - ## Insert test body otherwise things will fail - self.insert_test_body() # Eduardo has heard about a new website for NEOs. He goes to the # homepage @@ -37,29 +35,28 @@ def test_can_compute_ephemeris(self): # He notices the page title has the name of the site and the header # mentions current targets - self.assertIn('NEOexchange', self.browser.title) - header_text = self.browser.find_element_by_tag_name('h1').text - self.assertIn('Current Targets', header_text) + self.assertIn('Home | LCOGT NEOx', self.browser.title) + header_text = self.browser.find_element_by_class_name('masthead').text + self.assertIn('active targets', header_text) # He notices there are several targets that could be followed up self.check_for_header_in_table('id_neo_targets', 'Target Name Type Origin Ingested') - self.check_for_row_in_table('id_neo_targets', - 'N999r0q Unknown/NEO Candidate MPC April 8, 2015, 9:23 p.m.') - self.check_for_row_in_table('id_neo_targets', - 'P10kfud Unknown/NEO Candidate MPC April 8, 2015, 8:57 p.m.') + testlines =[u'N999r0q Unknown/NEO Candidate Minor Planet Center %s' % self.body.ingest.strftime('%-d %B %Y, %H:%M'), + u'P10kfud Unknown/NEO Candidate Minor Planet Center %s' % self.body.ingest.strftime('%-d %B %Y, %H:%M')] + self.check_for_row_in_table('id_neo_targets', testlines[0]) # He is invited to enter a target to compute an ephemeris inputbox = self.get_item_input_box() self.assertEqual( inputbox.get_attribute('placeholder'), - 'Enter a target name' + 'Enter target name...' ) # He types N999r0q into the textbox (he is most interested in NEOWISE targets) inputbox.send_keys('N999r0q') - datebox = self.get_item_input_box('id_date') + datebox = self.get_item_input_box_and_clear('id_utc_date') datebox.send_keys('2015-04-21') # When he hits Enter, he is taken to a new page and now the page shows an ephemeris @@ -69,7 +66,8 @@ def test_can_compute_ephemeris(self): eduardo_ephem_url = self.browser.current_url self.assertRegexpMatches(eduardo_ephem_url, '/ephemeris/.+') - self.check_for_row_in_table('id_planning_table', 'Computing ephemeris for: N999r0q for V37') + menu = self.browser.find_element_by_id('extramenu').text + self.assertIn('Ephemeris for N999r0q at V37', menu) self.check_for_header_in_table('id_ephemeris_table', 'Date/Time (UTC) RA Dec Mag "/min Alt Moon Phase Moon Dist. Moon Alt. Score H.A.' @@ -78,17 +76,17 @@ def test_can_compute_ephemeris(self): '2015 04 21 08:45 20 10 05.99 +29 56 57.5 20.4 2.43 +33 0.09 107 -42 +047 -04:25' ) - # There is a button asking whether to schedule the target + # # There is a button asking whether to schedule the target + # link = self.browser.find_element_by_link_text('No') - # He clicks 'No' and is returned to the front page - self.assertIn('NEOexchange', self.browser.title) + # # He clicks 'No' and is returned to the front page + # link.click() + # self.assertIn('NEOx home | LCOGT', self.browser.title) # Satisfied, he goes back to sleep def test_can_compute_ephemeris_for_specific_site(self): - ## Insert test body otherwise things will fail - self.insert_test_body() # Eduardo has heard about a new website for NEOs. He goes to the # homepage @@ -98,7 +96,7 @@ def test_can_compute_ephemeris_for_specific_site(self): inputbox = self.get_item_input_box() self.assertEqual( inputbox.get_attribute('placeholder'), - 'Enter a target name' + 'Enter target name...' ) # He types N999r0q into the textbox (he is most interested in NEOWISE targets) @@ -106,14 +104,19 @@ def test_can_compute_ephemeris_for_specific_site(self): # He notices a new selection for the site code and chooses FTN (F65) # XXX Code smell: Too many static text constants - site_choices = Select(self.browser.find_element_by_id('id_sitecode')) + site_choices = Select(self.browser.find_element_by_id('id_site_code')) self.assertIn('FTN (F65)', [option.text for option in site_choices.options]) site_choices.select_by_visible_text('FTN (F65)') - datebox = self.get_item_input_box('id_date') + datebox = self.get_item_input_box('id_utc_date') + datebox.clear() datebox.send_keys('2015-04-21') + altlimitbox = self.get_item_input_box('id_alt_limit') + altlimitbox.clear() + altlimitbox.send_keys('20') + # When he hits Enter, he is taken to a new page and now the page shows an ephemeris # for the target with a column header and a series of rows for the position # as a function of time. @@ -122,7 +125,8 @@ def test_can_compute_ephemeris_for_specific_site(self): eduardo_ephem_url = self.browser.current_url self.assertRegexpMatches(eduardo_ephem_url, '/ephemeris/.+') - self.check_for_row_in_table('id_planning_table', 'Computing ephemeris for: N999r0q for F65') + menu = self.browser.find_element_by_id('extramenu').text + self.assertIn('Ephemeris for N999r0q at F65', menu) # Check the results for V37 are not in the table table = self.browser.find_element_by_id('id_ephemeris_table') @@ -140,8 +144,6 @@ def test_can_compute_ephemeris_for_specific_site(self): def test_can_compute_ephemeris_for_specific_date(self): - ## Insert test body otherwise things will fail - self.insert_test_body() # Eduardo has heard about a new website for NEOs. He goes to the # homepage @@ -151,7 +153,7 @@ def test_can_compute_ephemeris_for_specific_date(self): inputbox = self.get_item_input_box() self.assertEqual( inputbox.get_attribute('placeholder'), - 'Enter a target name' + 'Enter target name...' ) # He types N999r0q into the textbox (he is most interested in NEOWISE targets) @@ -159,22 +161,24 @@ def test_can_compute_ephemeris_for_specific_date(self): # He notices a new selection for the site code and chooses ELP (V37) # XXX Code smell: Too many static text constants - site_choices = Select(self.get_item_input_box('id_sitecode')) + site_choices = Select(self.get_item_input_box('id_site_code')) self.assertIn('ELP (V37)', [option.text for option in site_choices.options]) site_choices.select_by_visible_text('ELP (V37)') # He notices a new textbox for the date that is wanted which is filled # in with the current date - datebox = self.get_item_input_box('id_date') + datebox = self.get_item_input_box('id_utc_date') current_date = datetime.utcnow().date() current_date_str = current_date.strftime('%Y-%m-%d') self.assertEqual( - datebox.get_attribute('placeholder'), + datebox.get_attribute('value'), current_date_str ) # He decides to see where it will be on a specific date in a future + # so clears the box and put his new date in + datebox.clear() datebox.send_keys('2015-04-28') # When he hits Enter, he is taken to a new page and now the page shows an ephemeris @@ -185,7 +189,8 @@ def test_can_compute_ephemeris_for_specific_date(self): eduardo_ephem_url = self.browser.current_url self.assertRegexpMatches(eduardo_ephem_url, '/ephemeris/.+') - self.check_for_row_in_table('id_planning_table', 'Computing ephemeris for: N999r0q for V37') + menu = self.browser.find_element_by_id('extramenu').text + self.assertIn('Ephemeris for N999r0q at V37', menu) # Check the results for default date are not in the table table = self.browser.find_element_by_id('id_ephemeris_table') @@ -203,8 +208,6 @@ def test_can_compute_ephemeris_for_specific_date(self): def test_can_compute_ephemeris_for_specific_alt_limit(self): - ## Insert test body otherwise things will fail - self.insert_test_body() # Eduardo has heard about a new website for NEOs. He goes to the # homepage @@ -214,7 +217,7 @@ def test_can_compute_ephemeris_for_specific_alt_limit(self): inputbox = self.get_item_input_box() self.assertEqual( inputbox.get_attribute('placeholder'), - 'Enter a target name' + 'Enter target name...' ) # He types N999r0q into the textbox (he is most interested in NEOWISE targets) @@ -222,28 +225,29 @@ def test_can_compute_ephemeris_for_specific_alt_limit(self): # He notices a new selection for the site code and chooses CPT (K91) # XXX Code smell: Too many static text constants - site_choices = Select(self.get_item_input_box('id_sitecode')) + site_choices = Select(self.get_item_input_box('id_site_code')) self.assertIn('CPT (K91-93)', [option.text for option in site_choices.options]) site_choices.select_by_visible_text('CPT (K91-93)') # He notices a new textbox for the date that is wanted which is filled # in with the current date - datebox = self.get_item_input_box('id_date') + datebox = self.get_item_input_box('id_utc_date') current_date = datetime.utcnow().date() current_date_str = current_date.strftime('%Y-%m-%d') self.assertEqual( - datebox.get_attribute('placeholder'), + datebox.get_attribute('value'), current_date_str ) # He decides to see where it will be on a specific date in a future + datebox.clear() datebox.send_keys('2015-09-04') # He notices a new textbox for the altitude limit that is wanted, below # which he doesn't want to see ephemeris output. It is filled in with # the default value of 30.0 degrees - datebox = self.get_item_input_box('id_altlimit') + datebox = self.get_item_input_box('id_alt_limit') self.assertEqual(datebox.get_attribute('value'), str(30.0)) @@ -255,7 +259,8 @@ def test_can_compute_ephemeris_for_specific_alt_limit(self): eduardo_ephem_url = self.browser.current_url self.assertRegexpMatches(eduardo_ephem_url, '/ephemeris/.+') - self.check_for_row_in_table('id_planning_table', 'Computing ephemeris for: N999r0q for K92') + menu = self.browser.find_element_by_id('extramenu').text + self.assertIn('Ephemeris for N999r0q at K92', menu) # Check the results for default date are not in the table table = self.browser.find_element_by_id('id_ephemeris_table') diff --git a/neoexchange/neox/tests/test_ephemeris_validation.py b/neoexchange/neox/tests/test_ephemeris_validation.py index 425600925..56f3ed110 100644 --- a/neoexchange/neox/tests/test_ephemeris_validation.py +++ b/neoexchange/neox/tests/test_ephemeris_validation.py @@ -1,7 +1,7 @@ from .base import FunctionalTest from selenium import webdriver from selenium.webdriver.common.keys import Keys -from ingest.models import Body +from core.models import Body class EphemerisValidationTest(FunctionalTest): @@ -15,4 +15,4 @@ def test_cannot_get_ephem_for_bad_objects(self): # The page refreshes and there is an error message saying that targets' # can't be blank error = self.browser.find_element_by_css_selector('.error') - self.assertEqual(error.text, "You didn't specify a target") + self.assertEqual(error.text, "Target name is required") diff --git a/neoexchange/neox/tests/test_layout_and_styling.py b/neoexchange/neox/tests/test_layout_and_styling.py index d114f79b7..6ebfd865d 100644 --- a/neoexchange/neox/tests/test_layout_and_styling.py +++ b/neoexchange/neox/tests/test_layout_and_styling.py @@ -9,8 +9,8 @@ def test_layout_and_styling(self): self.browser.set_window_size(1280, 1024) # He notices the input box is nicely centered - inputbox = self.get_item_input_box('id_date') + link = self.browser.find_element_by_partial_link_text('active targets') self.assertAlmostEqual( - inputbox.location['x'] + inputbox.size['width'] / 2, - 640, delta=7 + link.location['x'] + link.size['width'] /2 , + 640, delta=50 ) diff --git a/neoexchange/neox/tests/test_schedule_observations.py b/neoexchange/neox/tests/test_schedule_observations.py new file mode 100644 index 000000000..f6426af3f --- /dev/null +++ b/neoexchange/neox/tests/test_schedule_observations.py @@ -0,0 +1,128 @@ +from .base import FunctionalTest +from selenium import webdriver +from selenium.webdriver.common.keys import Keys +from selenium.webdriver.support.ui import Select +from datetime import datetime +from django.core.urlresolvers import reverse +from django.contrib.auth.models import User +from core.models import Body, Proposal + +class ScheduleObservations(FunctionalTest): + + def setUp(self): + # Create a user to test login + self.bart= User.objects.create_user(username='bart', password='simpson', email='bart@simpson.org') + self.bart.first_name= 'Bart' + self.bart.last_name = 'Simpson' + self.bart.is_active=1 + self.bart.save() + super(ScheduleObservations,self).setUp() + + def login(self): + self.assertTrue(self.client.login(username='bart', password='simpson')) + + def insert_test_bodies(self): + params1 = { 'provisional_name' : 'N999r0q', + 'abs_mag' : 21.0, + 'slope' : 0.15, + 'epochofel' : '2015-03-19 00:00:00', + 'meananom' : 325.2636, + 'argofperih' : 85.19251, + 'longascnode' : 147.81325, + 'orbinc' : 8.34739, + 'eccentricity' : 0.1896865, + 'meandist' : 1.2176312, + 'source_type' : 'U', + 'elements_type' : 'MPC_MINOR_PLANET', + 'active' : True, + 'origin' : 'M', + } + self.body1, created = Body.objects.get_or_create(**params1) + + params2 = { 'provisional_name' : 'WH2845B', + 'abs_mag' : 18.2, + 'slope' : 0.15, + 'epochofel' : '2015-06-27 00:00:00', + 'meananom' : 25.57309, + 'argofperih' : 314.41870, + 'longascnode' : 224.52430, + 'orbinc' : 31.31052, + 'eccentricity' : 0.5356964, + 'meandist' : 2.6132962, + 'source_type' : 'N', + 'elements_type' : 'MPC_MINOR_PLANET', + 'active' : True, + 'origin' : 'M', + } + self.body2, created = Body.objects.get_or_create(**params2) + + def test_can_schedule_observations(self): + self.login() + + ## Insert test bodies and proposals otherwise things will fail + self.insert_test_bodies() + #self.insert_test_proposals() + + # Sharon has heard about a new website for NEOs. She goes to the + # page of the first target + # (XXX semi-hardwired but the targets link should be being tested in + # test_targets_validation.TargetsValidationTest + start_url = reverse('target',kwargs={'pk':1}) + self.browser.get(self.live_server_url + start_url) + + # She sees a Schedule Observations button + link = self.browser.find_element_by_link_text('Schedule Observations') + target_url = self.live_server_url + reverse('schedule-body',kwargs={'pk':1}) + self.assertEqual(link.get_attribute('href'), target_url) + + # She clicks the link to go to the Schedule Observations page + link.click() + self.browser.implicitly_wait(10) + new_url = self.browser.current_url + self.assertEqual(str(new_url), target_url) + + + # She notices a new selection for the proposal and site code and + # chooses the NEO Follow-up Network and ELP (V37) + proposal_choices = Select(self.browser.find_element_by_id('id_proposal_code')) + self.assertIn(self.neo_proposal.title, [option.text for option in proposal_choices.options]) + + proposal_choices.select_by_visible_text(self.neo_proposal.title) + + site_choices = Select(self.browser.find_element_by_id('id_site_code')) + self.assertIn('ELP (V37)', [option.text for option in site_choices.options]) + + site_choices.select_by_visible_text('ELP (V37)') + + datebox = self.get_item_input_box('id_utc_date') + datebox.clear() + datebox.send_keys('2015-04-21') + datebox.send_keys(Keys.ENTER) + + # The page refreshes and a series of values for magnitude, speed, slot + # length, number and length of exposures appear + magnitude = self.browser.find_element_by_id('id_magnitude').find_element_by_class_name('kv-value').text + self.assertIn('20.39', magnitude) + speed = self.browser.find_element_by_id('id_speed').find_element_by_class_name('kv-value').text + self.assertIn("2.52 '/min", speed) + slot_length = self.browser.find_element_by_id('id_slot_length').find_element_by_class_name('kv-value').text + self.assertIn('22.5 mins', slot_length) + num_exp = self.browser.find_element_by_id('id_no_of_exps').find_element_by_class_name('kv-value').text + self.assertIn('18', num_exp) + exp_length = self.browser.find_element_by_id('id_exp_length').find_element_by_class_name('kv-value').text + self.assertIn('50.0 secs', exp_length) + + # At this point, a 'Schedule this object' button appears + submit = self.browser.find_element_by_id('id_submit_button').get_attribute("value") + self.assertIn('Schedule this Object',submit) + self.fail("Finish the test!") + + def test_cannot_schedule_observations(self): + # Sharon tries the same as above but forgets to login + start_url = reverse('target',kwargs={'pk':1}) + self.browser.get(self.live_server_url + start_url) + link = self.browser.find_element_by_link_text('Schedule Observations') + link.click() + self.browser.implicitly_wait(10) + new_url = self.browser.current_url + self.assertContains(str(new_url), 'login') diff --git a/neoexchange/neox/urls.py b/neoexchange/neox/urls.py index cf4e6056c..e84a8f89b 100644 --- a/neoexchange/neox/urls.py +++ b/neoexchange/neox/urls.py @@ -12,23 +12,36 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. ''' -from django.conf.urls import patterns, include, url -from django.contrib import admin from django.conf import settings -from django.conf.urls.static import static +from django.conf.urls import include, url +from django.contrib.staticfiles import views +from django.contrib import admin from django.views.generic import ListView, DetailView -from ingest.views import BodySearchView -from ingest.models import Body, Block +from django.core.urlresolvers import reverse_lazy +from core.models import Body, Block +from core.views import BodySearchView,BodyDetailView, ScheduleParameters, ephemeris, home +from django.contrib.auth.views import login, logout admin.autodiscover() -urlpatterns = patterns('ingest.views', - url(r'^$', 'home', name='home'), +urlpatterns = [ + url(r'^$', home, name='home'), url(r'^block/list/$', ListView.as_view(model=Block, queryset=Block.objects.filter(active=True).order_by('-block_start'), context_object_name="block_list"), name='blocklist'), url(r'^target/$', ListView.as_view(model=Body, queryset=Body.objects.filter(active=True).order_by('-origin','-ingest'), context_object_name="target_list"), name='targetlist'), - url(r'^target/(?P\d+)/$',DetailView.as_view(model=Body, context_object_name='body'), name='target'), + url(r'^target/(?P\d+)/$',BodyDetailView.as_view(model=Body), name='target'), url(r'^search/$', BodySearchView.as_view(context_object_name="target_list"), name='search'), - url(r'^ephemeris/$', 'ephemeris', name='ephemeris'), + url(r'^ephemeris/$', ephemeris, name='ephemeris'), + # url(r'^schedule/(?P\d+)/confirm/$',ScheduleConfirm.as_view(), name='schedule-confirm'), + url(r'^schedule/(?P\d+)/$', ScheduleParameters.as_view(), name='schedule-body'), + # url(r'^schedule/success/$',ScheduleSuccess.as_view(), name='schedule-success'), + # url(r'^schedule/$', SchedFormDisplay.as_view(), name='schedule'), + url(r'^accounts/login/$', login, {'template_name': 'core/login.html'}, name='auth_login'), + url(r'^accounts/logout/$', logout, {'template_name': 'core/logout.html'}, name='auth_logout' ), url(r'^grappelli/', include('grappelli.urls')), url(r'^admin/', include(admin.site.urls)), -)+ static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) +] + +if settings.DEBUG: + urlpatterns += [ + url(r'^static/(?P.*)$', views.serve), + ] \ No newline at end of file diff --git a/neoexchange/pytest.ini b/neoexchange/pytest.ini new file mode 100644 index 000000000..ed43b9be6 --- /dev/null +++ b/neoexchange/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +DJANGO_SETTINGS_MODULE=neox.settings diff --git a/neoexchange/pip_requirements.txt b/neoexchange/requirements.txt similarity index 62% rename from neoexchange/pip_requirements.txt rename to neoexchange/requirements.txt index fb60fb785..6b81e4845 100644 --- a/neoexchange/pip_requirements.txt +++ b/neoexchange/requirements.txt @@ -1,16 +1,19 @@ --extra-index-url=http://buildsba.lco.gtn/python/ -Django==1.4.20 -MySQL-python==1.2.5 -South==0.7.6 +Django>1.8,<1.9 +MySQL-python astropy==1.0 beautifulsoup4==4.3.2 django-grappelli==2.4.12 -django-reversion==1.6.6 +django-reversion==1.8.7 html5lib==1.0b3 -ipython==3.0.0 +ipython nose==1.3.6 numpy==1.9.2 pytz==2015.2 six==1.9.0 -wsgiref==0.1.2 +wsgiref +requests selenium +reqdbclient +pytest-django +opbeat