From f74028477a8aa3f491a6a11e44320ba8beeb5d34 Mon Sep 17 00:00:00 2001 From: Domenico DiNicola Date: Mon, 21 Jan 2019 11:21:13 -0500 Subject: [PATCH 01/72] pipenv --- .circleci/config.yml | 4 +- .gitignore | 1 - Dockerfile | 9 +- Dockerfile-dev | 9 +- Pipfile | 133 ++ Pipfile.lock | 1576 +++++++++++++++++ requirements.txt | 3 - setup.py | 30 +- src/etools/applications/permissions2/views.py | 2 +- .../applications/users/tests/factories.py | 1 + tasks.py | 30 - tox.ini | 1 + 12 files changed, 1723 insertions(+), 76 deletions(-) create mode 100644 Pipfile create mode 100644 Pipfile.lock delete mode 100644 requirements.txt delete mode 100644 tasks.py diff --git a/.circleci/config.yml b/.circleci/config.yml index ec2a4a191b..572f443aa3 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -18,11 +18,11 @@ jobs: steps: - checkout - restore_cache: - key: deps2-{{ .Branch }}--{{ checksum "src/requirements/test.txt" }}-{{ checksum ".circleci/config.yml" }} + key: deps2-{{ .Branch }}--{{ checksum "Pipfile.lock" }}-{{ checksum ".circleci/config.yml" }} - run: name: Run Tests command: | - pip install tox + pip install -q tox pipenv tox -re d20,report - save_cache: key: deps2-{{ .Branch }}--{{ checksum "src/requirements/test.txt" }}-{{ checksum ".circleci/config.yml" }} diff --git a/.gitignore b/.gitignore index afea542902..32c4b3cf62 100644 --- a/.gitignore +++ b/.gitignore @@ -119,4 +119,3 @@ src/etools/config/settings/custom.py .tox .pytest_cache coverage.xml -Pipfile diff --git a/Dockerfile b/Dockerfile index 54693d465f..22f1b7a652 100644 --- a/Dockerfile +++ b/Dockerfile @@ -34,16 +34,15 @@ RUN apt-get install -y --no-install-recommends \ RUN pip install --upgrade \ setuptools \ pip \ - wheel + wheel \ + pipenv # http://gis.stackexchange.com/a/74060 ENV CPLUS_INCLUDE_PATH /usr/include/gdal ENV C_INCLUDE_PATH /usr/include/gdal -ENV REQUIREMENTS_FILE base.txt -ADD src/requirements/*.txt /pip/ -ADD src/requirements/$REQUIREMENTS_FILE /pip/app_requirements.txt -RUN pip install -f /pip -r /pip/app_requirements.txt +ADD Pipfile.lock / +RUN pipenv install --system --deploy --ignore-pipfile ENV PYTHONUNBUFFERED 1 ADD src /code/ diff --git a/Dockerfile-dev b/Dockerfile-dev index a4c06e72c4..7ea635a4d9 100644 --- a/Dockerfile-dev +++ b/Dockerfile-dev @@ -34,17 +34,16 @@ RUN apt-get install -y --no-install-recommends \ RUN pip install --upgrade \ setuptools \ pip \ - wheel + wheel \ + pipenv # http://gis.stackexchange.com/a/74060 ENV CPLUS_INCLUDE_PATH /usr/include/gdal ENV C_INCLUDE_PATH /usr/include/gdal -ENV REQUIREMENTS_FILE=test.txt -ADD src/requirements/*.txt /pip/ -ADD src/requirements/$REQUIREMENTS_FILE /pip/app_requirements.txt -RUN pip install -f /pip -r /pip/app_requirements.txt +ADD Pipfile.lock / +RUN pipenv install --verbose --system --deploy --ignore-pipfile ENV PYTHONUNBUFFERED 1 VOLUME "./:/code/" diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000000..cd323b54b1 --- /dev/null +++ b/Pipfile @@ -0,0 +1,133 @@ +[[source]] +name = "pypi" +url = "https://pypi.org/simple" +verify_ssl = true + +[dev-packages] +flake8 = "*" +coverage = "*" +mock = "*" +freezegun = "*" +responses = "*" +isort = "*" +ipython = "*" +pdbpp = "*" +tox = "*" +drf-api-checker = "*" +factory_boy = "*" +django-extensions = "*" +sphinx = "*" + +[packages] +amqp = "==2.2.2" +asn1crypto = "==0.24.0" +azure-common = "==1.1.8" +azure-nspkg = "==2.0.0" +azure-storage = "==0.20.2" +billiard = "==3.5.0.3" +carto = "==1.3.1" +celery = "==4.2.1" +cffi = "==1.11.5" +coreapi = "==2.3.3" +coreschema = "==0.0.4" +cryptography = "==2.4.1" +defusedxml = "==0.5.0" +diff-match-patch = "==20121119" +dj-database-url = "==0.5" +dj-static = "==0.0.6" +django-appconf = "==1.0.2" +django-celery-email = "==2.0.1" +django-contrib-comments = "==1.9" +django-cors-headers = "==2.4" +django-debug-toolbar = "==1.11" +django-easy-pdf = "==0.1.1" +django-filter = "==2.0" +django-fsm = "==2.6" +django-import-export = "==1.1" +django-js-asset = "==1.1.0" +django-leaflet = "==0.24" +django-logentry-admin = "==1.0.4" +django-model-utils = "==3.1.2" +django-mptt = "==0.9.1" +django-ordered-model = "==1.5" +django-redis-cache = "==1.8" +django-rest-swagger = "==2.2" +django-storages = "==1.6.6" +django-tenants = "==2.1" +django-timezone-field = "==3.0" +django-waffle = "==0.14" +djangorestframework-csv = "==2.1.0" +djangorestframework-gis = "==0.14" +djangorestframework-jwt = "==1.11.0" +djangorestframework-recursive = "==0.1.2" +djangorestframework-xml = "==1.3" +djangorestframework = "==3.9.0" +drf-nested-routers = "==0.91" +drf-querystringfilter = "==1.0.0" +drfpasswordless = "==1.2" +etools-validator = "==0.3.2" +flower = "==0.9.2" +future = "==0.15.2" +gunicorn = "==19.9" +html5lib = "==1.0.1" +idna = "==2.6" +itypes = "==1.1.0" +jdcal = "==1.4" +jsonfield = "==2.0.2" +kombu = "==4.2.1" +newrelic = "==2.94.0.79" +oauthlib = "==2.0.7" +odfpy = "==1.3.6" +openapi-codec = "==1.3.2" +openpyxl = "==2.5.9" +psycopg2-binary = "==2.7.5" +psycopg2 = "==2.7.5" +pycparser = "==2.18" +pyrestcli = "==0.6.4" +python-crontab = "==2.3.5" +python-dateutil = "==2.5.3" +python3-openid = "==3.1.0" +pytz = "==2018.3" +raven = "==6.9" +redis = "==2.10.6" +reportlab = "==3.5.9" +requests-oauthlib = "==0.8.0" +requests = "==2.11.1" +simplejson = "==3.16.0" +six = "==1.11.0" +social-auth-app-django = "==2.1.0" +social-auth-core = {extras = ["azuread"], version = "==1.7.0"} +sqlparse = "==0.2.4" +static3 = "==0.7.0" +tablib = "==0.12.1" +tenant-schemas-celery = "==0.2.1" +tornado = "==5.1.1" +unicef-djangolib = "==0.5.2" +unicef-locations = "==1.5" +unicodecsv = "==0.14.1" +uritemplate = "==3.0.0" +vine = "==1.1.4" +webencodings = "==0.5.1" +xhtml2pdf = "==0.2.3" +xlrd = "==1.1.0" +xlwt = "==1.3.0" +Babel = "==2.6.0" +django_celery_beat = "==1.2" +django_celery_results = "==1.0.1" +django-post_office = "==3.1.0" +Django = "==2.0.9" +et_xmlfile = "==1.0.1" +GDAL = "==1.10.0" +Jinja2 = "==2.10" +MarkupSafe = "==1.1.0" +PyJWT = "==1.5.3" +PyPDF2 = "==1.26.0" +PyYAML = "==3.12" +unicef_attachments = "==0.4.2" +unicef_notification = "==0.2.0" +unicef_restlib = "==0.3.8" +unicef_snapshot = "==0.2.1" +Pillow = "==5.3.0" + +[requires] +python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000000..b1334746f1 --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,1576 @@ +{ + "_meta": { + "hash": { + "sha256": "3156cc80efc3673865bcd8f4fc4520f4e15e5297c53de108501c27f161aa371e" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.7" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "amqp": { + "hashes": [ + "sha256:4e28d3ea61a64ae61830000c909662cb053642efddbe96503db0e7783a6ee85b", + "sha256:cba1ace9d4ff6049b190d8b7991f9c1006b443a5238021aca96dd6ad2ac9da22" + ], + "index": "pypi", + "version": "==2.2.2" + }, + "asn1crypto": { + "hashes": [ + "sha256:2f1adbb7546ed199e3c90ef23ec95c5cf3585bac7d11fb7eb562a3fe89c64e87", + "sha256:9d5c20441baf0cb60a4ac34cc447c6c189024b6b4c6cd7877034f4965c464e49" + ], + "index": "pypi", + "version": "==0.24.0" + }, + "azure-common": { + "hashes": [ + "sha256:037a5df2ee68078c2e71ddf7cb9e45dfec6510292b040c84314cd08d1894d9fe", + "sha256:25559617b41fe0902f34b215a3440a3ffa4862fe442bf351415be0e1d2eb4289" + ], + "index": "pypi", + "version": "==1.1.8" + }, + "azure-nspkg": { + "hashes": [ + "sha256:4bd758e649f57cc188db4f3c64becaca16195e057e4362b6caad56fe1e7934e9", + "sha256:fe19ee5d8c66ee8ef62557fc7310f59cffb7230f0a94701eef79f6e3191fdc7b" + ], + "index": "pypi", + "version": "==2.0.0" + }, + "azure-storage": { + "hashes": [ + "sha256:3350eed1352c16bddc2429661c9fb4e50a5f1a998ce39d5706fdfab2a544833f", + "sha256:3df58122466ee613eeb2caef677d6ee4cc2c1907b0aaf43a926a30bb575d0927", + "sha256:64e5c309867e9348984258d4ccd7b9ef4684466dbfaa28096ccad60bbd016f97" + ], + "index": "pypi", + "version": "==0.20.2" + }, + "babel": { + "hashes": [ + "sha256:6778d85147d5d85345c14a26aada5e478ab04e39b078b0745ee6870c2b5cf669", + "sha256:8cba50f48c529ca3fa18cf81fa9403be176d374ac4d60738b839122dfaaa3d23" + ], + "index": "pypi", + "version": "==2.6.0" + }, + "billiard": { + "hashes": [ + "sha256:1d7b22bdc47aa52841120fcd22a74ae4fc8c13e9d3935643098184f5788c3ce6", + "sha256:abd9ce008c9a71ccde2c816f8daa36246e92a21e6a799831b887d88277187ecd" + ], + "index": "pypi", + "version": "==3.5.0.3" + }, + "carto": { + "hashes": [ + "sha256:ff183bc1a07c142ef707b6c5fc759452aa446db619e73afad086f8058db4beed" + ], + "index": "pypi", + "version": "==1.3.1" + }, + "celery": { + "hashes": [ + "sha256:77dab4677e24dc654d42dfbdfed65fa760455b6bb563a0877ecc35f4cfcfc678", + "sha256:ad7a7411772b80a4d6c64f2f7f723200e39fb66cf614a7fdfab76d345acc7b13" + ], + "index": "pypi", + "version": "==4.2.1" + }, + "certifi": { + "hashes": [ + "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", + "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" + ], + "version": "==2018.11.29" + }, + "cffi": { + "hashes": [ + "sha256:151b7eefd035c56b2b2e1eb9963c90c6302dc15fbd8c1c0a83a163ff2c7d7743", + "sha256:1553d1e99f035ace1c0544050622b7bc963374a00c467edafac50ad7bd276aef", + "sha256:1b0493c091a1898f1136e3f4f991a784437fac3673780ff9de3bcf46c80b6b50", + "sha256:2ba8a45822b7aee805ab49abfe7eec16b90587f7f26df20c71dd89e45a97076f", + "sha256:3bb6bd7266598f318063e584378b8e27c67de998a43362e8fce664c54ee52d30", + "sha256:3c85641778460581c42924384f5e68076d724ceac0f267d66c757f7535069c93", + "sha256:3eb6434197633b7748cea30bf0ba9f66727cdce45117a712b29a443943733257", + "sha256:495c5c2d43bf6cebe0178eb3e88f9c4aa48d8934aa6e3cddb865c058da76756b", + "sha256:4c91af6e967c2015729d3e69c2e51d92f9898c330d6a851bf8f121236f3defd3", + "sha256:57b2533356cb2d8fac1555815929f7f5f14d68ac77b085d2326b571310f34f6e", + "sha256:770f3782b31f50b68627e22f91cb182c48c47c02eb405fd689472aa7b7aa16dc", + "sha256:79f9b6f7c46ae1f8ded75f68cf8ad50e5729ed4d590c74840471fc2823457d04", + "sha256:7a33145e04d44ce95bcd71e522b478d282ad0eafaf34fe1ec5bbd73e662f22b6", + "sha256:857959354ae3a6fa3da6651b966d13b0a8bed6bbc87a0de7b38a549db1d2a359", + "sha256:87f37fe5130574ff76c17cab61e7d2538a16f843bb7bca8ebbc4b12de3078596", + "sha256:95d5251e4b5ca00061f9d9f3d6fe537247e145a8524ae9fd30a2f8fbce993b5b", + "sha256:9d1d3e63a4afdc29bd76ce6aa9d58c771cd1599fbba8cf5057e7860b203710dd", + "sha256:a36c5c154f9d42ec176e6e620cb0dd275744aa1d804786a71ac37dc3661a5e95", + "sha256:a6a5cb8809091ec9ac03edde9304b3ad82ad4466333432b16d78ef40e0cce0d5", + "sha256:ae5e35a2c189d397b91034642cb0eab0e346f776ec2eb44a49a459e6615d6e2e", + "sha256:b0f7d4a3df8f06cf49f9f121bead236e328074de6449866515cea4907bbc63d6", + "sha256:b75110fb114fa366b29a027d0c9be3709579602ae111ff61674d28c93606acca", + "sha256:ba5e697569f84b13640c9e193170e89c13c6244c24400fc57e88724ef610cd31", + "sha256:be2a9b390f77fd7676d80bc3cdc4f8edb940d8c198ed2d8c0be1319018c778e1", + "sha256:ca1bd81f40adc59011f58159e4aa6445fc585a32bb8ac9badf7a2c1aa23822f2", + "sha256:d5d8555d9bfc3f02385c1c37e9f998e2011f0db4f90e250e5bc0c0a85a813085", + "sha256:e55e22ac0a30023426564b1059b035973ec82186ddddbac867078435801c7801", + "sha256:e90f17980e6ab0f3c2f3730e56d1fe9bcba1891eeea58966e89d352492cc74f4", + "sha256:ecbb7b01409e9b782df5ded849c178a0aa7c906cf8c5a67368047daab282b184", + "sha256:ed01918d545a38998bfa5902c7c00e0fee90e957ce036a4000a88e3fe2264917", + "sha256:edabd457cd23a02965166026fd9bfd196f4324fe6032e866d0f3bd0301cd486f", + "sha256:fdf1c1dc5bafc32bc5d08b054f94d659422b05aba244d6be4ddc1c72d9aa70fb" + ], + "index": "pypi", + "version": "==1.11.5" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "coreapi": { + "hashes": [ + "sha256:46145fcc1f7017c076a2ef684969b641d18a2991051fddec9458ad3f78ffc1cb", + "sha256:bf39d118d6d3e171f10df9ede5666f63ad80bba9a29a8ec17726a66cf52ee6f3" + ], + "index": "pypi", + "version": "==2.3.3" + }, + "coreschema": { + "hashes": [ + "sha256:5e6ef7bf38c1525d5e55a895934ab4273548629f16aed5c0a6caa74ebf45551f", + "sha256:9503506007d482ab0867ba14724b93c18a33b22b6d19fb419ef2d239dd4a1607" + ], + "index": "pypi", + "version": "==0.0.4" + }, + "cryptography": { + "hashes": [ + "sha256:02915ee546b42ce513e8167140e9937fc4c81a06a82216e086ccce51f347948a", + "sha256:03cc8bc5a69ae3d44acf1a03facdb7c10a94c67907862c563e10efe72b737977", + "sha256:07f76bde6815c55195f3b3812d35769cc7c765144c0bb71ae45e02535d078591", + "sha256:13eac1c477b9af7e9a9024369468d08aead6ad78ed599d163ad046684474364b", + "sha256:179bfb585c5efc87ae0e665770e4896727b92dbc1f810c761b1ebf8363e2fec8", + "sha256:414af0ba308e74c1f8bc5b11befc86cb66b10be8959547786f64258830d2096f", + "sha256:41a1ca14f255df8c44dd22c6006441d631d1589104045ec7263cc47e9772f41a", + "sha256:54947eb98bc4eef99ddf49f45d2694ea5a3929ab3edc9806ad01967368594d82", + "sha256:5bac7a2abda07d0c3c8429210349bb54149ad8940dc7bcffedcd56519b410a3c", + "sha256:7f41af8c586bed9f59cfe8832d818b3b75c860d7025da9cd2db76875a72ff785", + "sha256:8004fae1b3cb2dbd90a011ad972e49a7e78a871b89c70cc7213cf4ebd2532bcb", + "sha256:8e0eccadc3b465e12c50a5b8fb4d39cf401b44d7bb9936c70fddb5e5aaf740d5", + "sha256:95b4741722269cfdc134fec23b7ae6503ee2aea83d0924cfee6d6ec54cd42d8e", + "sha256:a06f5aa6d7a94531dfe82eb2972e669258c452fe9cf88f76116610de4c789785", + "sha256:b0833d27c7eb536bc27323a1e8e22cb39ebac78c4ef3be0167ba40f447344808", + "sha256:b72dec675bc59a01edc96616cd48ec465b714481caa0938c8bbca5d18f17d5df", + "sha256:c800ddc23b5206ce025f23225fdde89cdc0e64016ad914d5be32d1f602ce9495", + "sha256:c980c8c313a5e014ae12e2245e89e7b30427e5a98cbb88afe478ecae85f3abaa", + "sha256:e85b410885addaeb31a867eabcefc9ef4a7e904ad45eac9e60a763a54b244626" + ], + "index": "pypi", + "version": "==2.4.1" + }, + "defusedxml": { + "hashes": [ + "sha256:24d7f2f94f7f3cb6061acb215685e5125fbcdc40a857eff9de22518820b0a4f4", + "sha256:702a91ade2968a82beb0db1e0766a6a273f33d4616a6ce8cde475d8e09853b20" + ], + "index": "pypi", + "version": "==0.5.0" + }, + "diff-match-patch": { + "hashes": [ + "sha256:9dba5611fbf27893347349fd51cc1911cb403682a7163373adacc565d11e2e4c" + ], + "index": "pypi", + "version": "==20121119" + }, + "dj-database-url": { + "hashes": [ + "sha256:4aeaeb1f573c74835b0686a2b46b85990571159ffc21aa57ecd4d1e1cb334163", + "sha256:851785365761ebe4994a921b433062309eb882fedd318e1b0fcecc607ed02da9" + ], + "index": "pypi", + "version": "==0.5" + }, + "dj-static": { + "hashes": [ + "sha256:032ec1c532617922e6e3e956d504a6fb1acce4fc1c7c94612d0fda21828ce8ef" + ], + "index": "pypi", + "version": "==0.0.6" + }, + "django": { + "hashes": [ + "sha256:25df265e1fdb74f7e7305a1de620a84681bcc9c05e84a3ed97e4a1a63024f18d", + "sha256:d6d94554abc82ca37e447c3d28958f5ac39bd7d4adaa285543ae97fb1129fd69" + ], + "index": "pypi", + "version": "==2.0.9" + }, + "django-appconf": { + "hashes": [ + "sha256:6a4d9aea683b4c224d97ab8ee11ad2d29a37072c0c6c509896dd9857466fb261", + "sha256:ddab987d14b26731352c01ee69c090a4ebfc9141ed223bef039d79587f22acd9" + ], + "index": "pypi", + "version": "==1.0.2" + }, + "django-celery-beat": { + "hashes": [ + "sha256:b25bc19a589e2851361408708a2aac43524608dd35b85d34a295f90f873a75e5", + "sha256:f3be14a02280d9977757acfcc8464d0bb5a24c3ed5fbd88b60ca63bb24fce632" + ], + "markers": "python_version != '3.3.*' and python_version != '3.0.*' and python_version != '3.2.*' and python_version >= '2.7' and python_version != '3.1.*'", + "version": "==1.2" + }, + "django-celery-email": { + "hashes": [ + "sha256:1b2e0e31c6266007463befdc23934696fc93dcf320dfc85b8bb6b063cfe9558a", + "sha256:e5f9122c02ec58d3e49653475ad1b8612fd752681ce2f006d9c0792c57046283" + ], + "index": "pypi", + "version": "==2.0.1" + }, + "django-celery-results": { + "hashes": [ + "sha256:8bca2605eeff4418be7ce428a6958d64bee0f5bdf1f8e563fbc09a9e2f3d990f", + "sha256:dfa240fb535a1a2d01c9e605ad71629909318eae6b893c5009eafd7265fde10b" + ], + "markers": "python_version != '3.3.*' and python_version != '3.0.*' and python_version != '3.2.*' and python_version >= '2.7' and python_version != '3.1.*'", + "version": "==1.0.1" + }, + "django-contrib-comments": { + "hashes": [ + "sha256:37fb0c60e0880c0978797925705ecbc1a153bf9cbc1253b579963e101a9acaac", + "sha256:689f3f80ff7ea8ab9f712ae5fe17ffa2ee8babbf8d75229ee8acc7bad461dfef" + ], + "index": "pypi", + "version": "==1.9" + }, + "django-cors-headers": { + "hashes": [ + "sha256:5545009c9b233ea7e70da7dbab7cb1c12afa01279895086f98ec243d7eab46fa", + "sha256:c4c2ee97139d18541a1be7d96fe337d1694623816d83f53cb7c00da9b94acae1" + ], + "index": "pypi", + "version": "==2.4" + }, + "django-debug-toolbar": { + "hashes": [ + "sha256:89d75b60c65db363fb24688d977e5fbf0e73386c67acf562d278402a10fc3736", + "sha256:c2b0134119a624f4ac9398b44f8e28a01c7686ac350a12a74793f3dd57a9eea0" + ], + "index": "pypi", + "version": "==1.11" + }, + "django-easy-pdf": { + "hashes": [ + "sha256:f7cb58e896791d28718219c54d2c8930e442fa1327817037e1c480bead77cddb" + ], + "index": "pypi", + "version": "==0.1.1" + }, + "django-filter": { + "hashes": [ + "sha256:6f4e4bc1a11151178520567b50320e5c32f8edb552139d93ea3e30613b886f56", + "sha256:86c3925020c27d072cdae7b828aaa5d165c2032a629abbe3c3a1be1edae61c58" + ], + "index": "pypi", + "version": "==2.0" + }, + "django-fsm": { + "hashes": [ + "sha256:13da2fbec4284dcf2235157c299306f67b788bd39126bed3cdbcc6a8f17575e9", + "sha256:c4790469e4f54b6cc95a3ac2c1bd1cdadeb04b0f3578e2472cdd857543a56e67" + ], + "index": "pypi", + "version": "==2.6" + }, + "django-import-export": { + "hashes": [ + "sha256:51823434e06721725e0e51b8da424b3c0e915c93cbb65b607464e3d9613f200e", + "sha256:54d0c9a0e0b0513c9db7ea47c41e6499ffe576e13f724d37003e18d974f5c3b5" + ], + "index": "pypi", + "version": "==1.1" + }, + "django-js-asset": { + "hashes": [ + "sha256:30149158206f693a5d027fe590096fc84495486bd11cd77d395b4f2ec27fc1d0", + "sha256:a395d8d19eb201ea8d2bd4f145b38f1717cd74c0f609f040141d8724c5a27f36" + ], + "index": "pypi", + "version": "==1.1.0" + }, + "django-leaflet": { + "hashes": [ + "sha256:efdf98ae0bb52f1c5e0fdceb20b870dc6a58378340366a9083f857ccea66117b" + ], + "index": "pypi", + "version": "==0.24" + }, + "django-logentry-admin": { + "hashes": [ + "sha256:0033fa146b5c3d1195a1d306e5b665a22b901450e55715f3dec50c2b4076f8f6" + ], + "index": "pypi", + "version": "==1.0.4" + }, + "django-model-utils": { + "hashes": [ + "sha256:2c057f3bf0859aba27f04389f0cedd2d48f8c9b3848acb86fd9970794e58f477", + "sha256:8cd377744aa45f9f131d652ec460c57d1aaa88d3e9b586c8e27eb709341b9084" + ], + "index": "pypi", + "version": "==3.1.2" + }, + "django-mptt": { + "hashes": [ + "sha256:18a41d1b56ca7c02a5b04d246e33ee2d18f6ee5459c02ed1d945f5abdef23a2e", + "sha256:689a04cce0981671d6061a9928c33a16b47abb0d4cd43cf7dec31ae284fdae9d" + ], + "index": "pypi", + "version": "==0.9.1" + }, + "django-ordered-model": { + "hashes": [ + "sha256:a58e18387641284288a9781f7946058fb9e0d8b08c16d0a9e571550212d86160" + ], + "index": "pypi", + "version": "==1.5" + }, + "django-post-office": { + "hashes": [ + "sha256:207b663a05d5d6a62765eb30081093837272a888cf00557d89d0e6f467928871", + "sha256:827937a944fe47cea393853069cd9315d080298c8ddb0faf787955d6aa51a030" + ], + "version": "==3.1.0" + }, + "django-redis-cache": { + "hashes": [ + "sha256:572c6b0a7c0f454a227082342aeb2ac49abd318ac651e9148484cc7185dd9f26" + ], + "index": "pypi", + "version": "==1.8" + }, + "django-rest-swagger": { + "hashes": [ + "sha256:48f6aded9937e90ae7cbe9e6c932b9744b8af80cc4e010088b3278c700e0685b", + "sha256:b039b0288bab4665cd45dc5d16f94b13911bc4ad0ed55f74ad3b90aa31c87c17" + ], + "index": "pypi", + "version": "==2.2" + }, + "django-storages": { + "hashes": [ + "sha256:7339070cf0c8042f5a885783a0a909175a8dbb68e7f5697d597571c830a460c4", + "sha256:f1dd5668a4df9a23aff56c8321ea3aac3fda23d9d17473158d308d1b13e5363e" + ], + "index": "pypi", + "version": "==1.6.6" + }, + "django-tenants": { + "hashes": [ + "sha256:43abc01d47f9b9ab62b8ff5ba09d995652b53c10d71fdd146afaec51f55d05af" + ], + "index": "pypi", + "version": "==2.1" + }, + "django-timezone-field": { + "hashes": [ + "sha256:7d7a37cfeacec5b1e81cd2f0aa334d46ebaa369cd516028579ed343cbc676c38", + "sha256:d9fdab77c443b78c362ffaeb50fe7d7b54692c89aaae8ca1cae67848139b82ac" + ], + "index": "pypi", + "version": "==3.0" + }, + "django-waffle": { + "hashes": [ + "sha256:f243a56db80bd28601222b1a8a0b1fa4e7e6ac1bbf809952c3725cb4cc0012d9", + "sha256:f3db39cc17d6e388a485230b6029095e5d6fba4ceaff8d4fcc21f95c47fe2e97" + ], + "index": "pypi", + "version": "==0.14" + }, + "djangorestframework": { + "hashes": [ + "sha256:607865b0bb1598b153793892101d881466bd5a991de12bd6229abb18b1c86136", + "sha256:63f76cbe1e7d12b94c357d7e54401103b2e52aef0f7c1650d6c820ad708776e5" + ], + "index": "pypi", + "version": "==3.9.0" + }, + "djangorestframework-csv": { + "hashes": [ + "sha256:2f008b20a44f2d3c37835ea5b5ddfe19f54394f07b9cb267c616a917a7f7e27c" + ], + "index": "pypi", + "version": "==2.1.0" + }, + "djangorestframework-gis": { + "hashes": [ + "sha256:35527c51e083ccc93f6e6d90a6515c132bbeb2c5648b166ac5b1a48c4ea8e2a4", + "sha256:e645c6c8aedee53ac0a4851abcdf8121fff66813eebae1b040b1ccb941cb248b" + ], + "index": "pypi", + "version": "==0.14" + }, + "djangorestframework-jwt": { + "hashes": [ + "sha256:5efe33032f3a4518a300dc51a51c92145ad95fb6f4b272e5aa24701db67936a7", + "sha256:ab15dfbbe535eede8e2e53adaf52ef0cf018ee27dbfad10cbc4cbec2ab63d38c" + ], + "index": "pypi", + "version": "==1.11.0" + }, + "djangorestframework-recursive": { + "hashes": [ + "sha256:e4e51b26b7ee3c9f9b838885d638b91293e7c66e85b5955f278a6e10eb34ce7c", + "sha256:f8fc2d677ccb32fe53ec4153a45f66c822d0ce444824cba56edc76ca89b704ae" + ], + "index": "pypi", + "version": "==0.1.2" + }, + "djangorestframework-xml": { + "hashes": [ + "sha256:caea8e446298b7fe1eb9a79306f35554db7531c2e637734d32de3cf99afbdc5a", + "sha256:f7d5efc26eabbca73db0ff0f0c15b59ca08e36660c02f96563a0d937321f519f" + ], + "index": "pypi", + "version": "==1.3" + }, + "drf-nested-routers": { + "hashes": [ + "sha256:46e5c3abc15c782cafafd7d75028e8f9121bbc6228e3599bbb48a3daa4585034", + "sha256:60c1e1f5cc801e757d26a8138e61c44419ef800c213c3640c5b6138e77d46762" + ], + "index": "pypi", + "version": "==0.91" + }, + "drf-querystringfilter": { + "hashes": [ + "sha256:feae3c659ae24cf393a35cf3161e87f01a71b8d30bb2cdf90e1eb549ba23af4c" + ], + "index": "pypi", + "version": "==1.0.0" + }, + "drfpasswordless": { + "hashes": [ + "sha256:ebed11b197a63fade5762cedfd5e1b7706bd4d3d875151584fdc79e7281452ea" + ], + "index": "pypi", + "version": "==1.2" + }, + "et-xmlfile": { + "hashes": [ + "sha256:614d9722d572f6246302c4491846d2c393c199cfa4edc9af593437691683335b" + ], + "version": "==1.0.1" + }, + "etools-validator": { + "hashes": [ + "sha256:057ae0fbd8405efe6ca1240c8ce60b5bff654525a39cb2f8234d5a560277db15" + ], + "index": "pypi", + "version": "==0.3.2" + }, + "flower": { + "hashes": [ + "sha256:a7a828c2dbea7e9cff1c86d63626f0eeb047b1b1e9a0ee5daad30771fb51e6d0" + ], + "index": "pypi", + "version": "==0.9.2" + }, + "future": { + "hashes": [ + "sha256:3d3b193f20ca62ba7d8782589922878820d0a023b885882deec830adbf639b97" + ], + "index": "pypi", + "version": "==0.15.2" + }, + "gdal": { + "hashes": [ + "sha256:1f95b3219616387f3da23c18bea030fa46b4b581091de3aa7c32466d62aade4c" + ], + "index": "pypi", + "version": "==1.10.0" + }, + "gunicorn": { + "hashes": [ + "sha256:aa8e0b40b4157b36a5df5e599f45c9c76d6af43845ba3b3b0efe2c70473c2471", + "sha256:fa2662097c66f920f53f70621c6c58ca4a3c4d3434205e608e121b5b3b71f4f3" + ], + "index": "pypi", + "version": "==19.9" + }, + "html5lib": { + "hashes": [ + "sha256:20b159aa3badc9d5ee8f5c647e5efd02ed2a66ab8d354930bd9ff139fc1dc0a3", + "sha256:66cb0dcfdbbc4f9c3ba1a63fdb511ffdbd4f513b2b6d81b80cd26ce6b3fb3736" + ], + "index": "pypi", + "version": "==1.0.1" + }, + "idna": { + "hashes": [ + "sha256:2c6a5de3089009e3da7c5dde64a141dbc8551d5b7f6cf4ed7c2568d0cc520a8f", + "sha256:8c7309c718f94b3a625cb648ace320157ad16ff131ae0af362c9f21b80ef6ec4" + ], + "index": "pypi", + "version": "==2.6" + }, + "itypes": { + "hashes": [ + "sha256:c6e77bb9fd68a4bfeb9d958fea421802282451a25bac4913ec94db82a899c073" + ], + "index": "pypi", + "version": "==1.1.0" + }, + "jdcal": { + "hashes": [ + "sha256:948fb8d079e63b4be7a69dd5f0cd618a0a57e80753de8248fd786a8a20658a07", + "sha256:ea0a5067c5f0f50ad4c7bdc80abad3d976604f6fb026b0b3a17a9d84bb9046c9" + ], + "index": "pypi", + "version": "==1.4" + }, + "jinja2": { + "hashes": [ + "sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd", + "sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4" + ], + "index": "pypi", + "markers": "python_version != '3.3.*' and python_version != '3.0.*' and python_version != '3.2.*' and python_version >= '2.7' and python_version != '3.1.*'", + "version": "==2.10" + }, + "jsonfield": { + "hashes": [ + "sha256:a0a7fdee736ff049059409752b045281a225610fecbda9b9bd588ba976493c12", + "sha256:beb1cd4850d6d6351c32daefcb826c01757744e9c863228a642f87a1a4acb834" + ], + "index": "pypi", + "version": "==2.0.2" + }, + "kombu": { + "hashes": [ + "sha256:86adec6c60f63124e2082ea8481bbe4ebe04fde8ebed32c177c7f0cd2c1c9082", + "sha256:b274db3a4eacc4789aeb24e1de3e460586db7c4fc8610f7adcc7a3a1709a60af" + ], + "index": "pypi", + "version": "==4.2.1" + }, + "markupsafe": { + "hashes": [ + "sha256:048ef924c1623740e70204aa7143ec592504045ae4429b59c30054cb31e3c432", + "sha256:130f844e7f5bdd8e9f3f42e7102ef1d49b2e6fdf0d7526df3f87281a532d8c8b", + "sha256:19f637c2ac5ae9da8bfd98cef74d64b7e1bb8a63038a3505cd182c3fac5eb4d9", + "sha256:1b8a7a87ad1b92bd887568ce54b23565f3fd7018c4180136e1cf412b405a47af", + "sha256:1c25694ca680b6919de53a4bb3bdd0602beafc63ff001fea2f2fc16ec3a11834", + "sha256:1f19ef5d3908110e1e891deefb5586aae1b49a7440db952454b4e281b41620cd", + "sha256:1fa6058938190ebe8290e5cae6c351e14e7bb44505c4a7624555ce57fbbeba0d", + "sha256:31cbb1359e8c25f9f48e156e59e2eaad51cd5242c05ed18a8de6dbe85184e4b7", + "sha256:3e835d8841ae7863f64e40e19477f7eb398674da6a47f09871673742531e6f4b", + "sha256:4e97332c9ce444b0c2c38dd22ddc61c743eb208d916e4265a2a3b575bdccb1d3", + "sha256:525396ee324ee2da82919f2ee9c9e73b012f23e7640131dd1b53a90206a0f09c", + "sha256:52b07fbc32032c21ad4ab060fec137b76eb804c4b9a1c7c7dc562549306afad2", + "sha256:52ccb45e77a1085ec5461cde794e1aa037df79f473cbc69b974e73940655c8d7", + "sha256:5c3fbebd7de20ce93103cb3183b47671f2885307df4a17a0ad56a1dd51273d36", + "sha256:5e5851969aea17660e55f6a3be00037a25b96a9b44d2083651812c99d53b14d1", + "sha256:5edfa27b2d3eefa2210fb2f5d539fbed81722b49f083b2c6566455eb7422fd7e", + "sha256:7d263e5770efddf465a9e31b78362d84d015cc894ca2c131901a4445eaa61ee1", + "sha256:83381342bfc22b3c8c06f2dd93a505413888694302de25add756254beee8449c", + "sha256:857eebb2c1dc60e4219ec8e98dfa19553dae33608237e107db9c6078b1167856", + "sha256:98e439297f78fca3a6169fd330fbe88d78b3bb72f967ad9961bcac0d7fdd1550", + "sha256:bf54103892a83c64db58125b3f2a43df6d2cb2d28889f14c78519394feb41492", + "sha256:d9ac82be533394d341b41d78aca7ed0e0f4ba5a2231602e2f05aa87f25c51672", + "sha256:e982fe07ede9fada6ff6705af70514a52beb1b2c3d25d4e873e82114cf3c5401", + "sha256:edce2ea7f3dfc981c4ddc97add8a61381d9642dc3273737e756517cc03e84dd6", + "sha256:efdc45ef1afc238db84cb4963aa689c0408912a0239b0721cb172b4016eb31d6", + "sha256:f137c02498f8b935892d5c0172560d7ab54bc45039de8805075e19079c639a9c", + "sha256:f82e347a72f955b7017a39708a3667f106e6ad4d10b25f237396a7115d8ed5fd", + "sha256:fb7c206e01ad85ce57feeaaa0bf784b97fa3cad0d4a5737bc5295785f5c613a1" + ], + "index": "pypi", + "markers": "python_version != '3.3.*' and python_version != '3.0.*' and python_version != '3.2.*' and python_version >= '2.7' and python_version != '3.1.*'", + "version": "==1.1.0" + }, + "newrelic": { + "hashes": [ + "sha256:051577a6a59c3d4b0b6e7cc03f8826a35f450ce66597d5832d237afa008f3dc9" + ], + "index": "pypi", + "version": "==2.94.0.79" + }, + "oauthlib": { + "hashes": [ + "sha256:09d438bcac8f004ae348e721e9d8a7792a9e23cd574634e973173344046287f5", + "sha256:909665297635fa11fe9914c146d875f2ed41c8c2d78e21a529dd71c0ba756508" + ], + "index": "pypi", + "version": "==2.0.7" + }, + "odfpy": { + "hashes": [ + "sha256:6bcaf3b23aa9e49ed8c8c177266539b211add4e02402748a994451482a10cb1b" + ], + "index": "pypi", + "version": "==1.3.6" + }, + "openapi-codec": { + "hashes": [ + "sha256:1bce63289edf53c601ea3683120641407ff6b708803b8954c8a876fe778d2145" + ], + "index": "pypi", + "version": "==1.3.2" + }, + "openpyxl": { + "hashes": [ + "sha256:022c0f3fa1e873cc0ba20651c54dd5e6276fc4ff150b4060723add4fc448645e" + ], + "index": "pypi", + "version": "==2.5.9" + }, + "pillow": { + "hashes": [ + "sha256:00203f406818c3f45d47bb8fe7e67d3feddb8dcbbd45a289a1de7dd789226360", + "sha256:0616f800f348664e694dddb0b0c88d26761dd5e9f34e1ed7b7a7d2da14b40cb7", + "sha256:1f7908aab90c92ad85af9d2fec5fc79456a89b3adcc26314d2cde0e238bd789e", + "sha256:2ea3517cd5779843de8a759c2349a3cd8d3893e03ab47053b66d5ec6f8bc4f93", + "sha256:48a9f0538c91fc136b3a576bee0e7cd174773dc9920b310c21dcb5519722e82c", + "sha256:5280ebc42641a1283b7b1f2c20e5b936692198b9dd9995527c18b794850be1a8", + "sha256:5e34e4b5764af65551647f5cc67cf5198c1d05621781d5173b342e5e55bf023b", + "sha256:63b120421ab85cad909792583f83b6ca3584610c2fe70751e23f606a3c2e87f0", + "sha256:696b5e0109fe368d0057f484e2e91717b49a03f1e310f857f133a4acec9f91dd", + "sha256:870ed021a42b1b02b5fe4a739ea735f671a84128c0a666c705db2cb9abd528eb", + "sha256:916da1c19e4012d06a372127d7140dae894806fad67ef44330e5600d77833581", + "sha256:9303a289fa0811e1c6abd9ddebfc770556d7c3311cb2b32eff72164ddc49bc64", + "sha256:9577888ecc0ad7d06c3746afaba339c94d62b59da16f7a5d1cff9e491f23dace", + "sha256:987e1c94a33c93d9b209315bfda9faa54b8edfce6438a1e93ae866ba20de5956", + "sha256:99a3bbdbb844f4fb5d6dd59fac836a40749781c1fa63c563bc216c27aef63f60", + "sha256:99db8dc3097ceafbcff9cb2bff384b974795edeb11d167d391a02c7bfeeb6e16", + "sha256:a5a96cf49eb580756a44ecf12949e52f211e20bffbf5a95760ac14b1e499cd37", + "sha256:aa6ca3eb56704cdc0d876fc6047ffd5ee960caad52452fbee0f99908a141a0ae", + "sha256:aade5e66795c94e4a2b2624affeea8979648d1b0ae3fcee17e74e2c647fc4a8a", + "sha256:b78905860336c1d292409e3df6ad39cc1f1c7f0964e66844bbc2ebfca434d073", + "sha256:b92f521cdc4e4a3041cc343625b699f20b0b5f976793fb45681aac1efda565f8", + "sha256:bfde84bbd6ae5f782206d454b67b7ee8f7f818c29b99fd02bf022fd33bab14cb", + "sha256:c2b62d3df80e694c0e4a0ed47754c9480521e25642251b3ab1dff050a4e60409", + "sha256:c5e2be6c263b64f6f7656e23e18a4a9980cffc671442795682e8c4e4f815dd9f", + "sha256:c99aa3c63104e0818ec566f8ff3942fb7c7a8f35f9912cb63fd8e12318b214b2", + "sha256:dae06620d3978da346375ebf88b9e2dd7d151335ba668c995aea9ed07af7add4", + "sha256:db5499d0710823fa4fb88206050d46544e8f0e0136a9a5f5570b026584c8fd74", + "sha256:f36baafd82119c4a114b9518202f2a983819101dcc14b26e43fc12cbefdce00e", + "sha256:f52b79c8796d81391ab295b04e520bda6feed54d54931708872e8f9ae9db0ea1", + "sha256:ff8cff01582fa1a7e533cb97f628531c4014af4b5f38e33cdcfe5eec29b6d888" + ], + "index": "pypi", + "markers": "python_version != '3.3.*' and python_version != '3.0.*' and python_version != '3.2.*' and python_version >= '2.7' and python_version != '3.1.*'", + "version": "==5.3.0" + }, + "psycopg2": { + "hashes": [ + "sha256:0b9e48a1c1505699a64ac58815ca99104aacace8321e455072cee4f7fe7b2698", + "sha256:0f4c784e1b5a320efb434c66a50b8dd7e30a7dc047e8f45c0a8d2694bfe72781", + "sha256:0fdbaa32c9eb09ef09d425dc154628fca6fa69d2f7c1a33f889abb7e0efb3909", + "sha256:11fbf688d5c953c0a5ba625cc42dea9aeb2321942c7c5ed9341a68f865dc8cb1", + "sha256:19eaac4eb25ab078bd0f28304a0cb08702d120caadfe76bb1e6846ed1f68635e", + "sha256:3232ec1a3bf4dba97fbf9b03ce12e4b6c1d01ea3c85773903a67ced725728232", + "sha256:36f8f9c216fcca048006f6dd60e4d3e6f406afde26cfb99e063f137070139eaf", + "sha256:59c1a0e4f9abe970062ed35d0720935197800a7ef7a62b3a9e3a70588d9ca40b", + "sha256:6506c5ff88750948c28d41852c09c5d2a49f51f28c6d90cbf1b6808e18c64e88", + "sha256:6bc3e68ee16f571681b8c0b6d5c0a77bef3c589012352b3f0cf5520e674e9d01", + "sha256:6dbbd7aabbc861eec6b910522534894d9dbb507d5819bc982032c3ea2e974f51", + "sha256:6e737915de826650d1a5f7ff4ac6cf888a26f021a647390ca7bafdba0e85462b", + "sha256:6ed9b2cfe85abc720e8943c1808eeffd41daa73e18b7c1e1a228b0b91f768ccc", + "sha256:711ec617ba453fdfc66616db2520db3a6d9a891e3bf62ef9aba4c95bb4e61230", + "sha256:844dacdf7530c5c612718cf12bc001f59b2d9329d35b495f1ff25045161aa6af", + "sha256:86b52e146da13c896e50c5a3341a9448151f1092b1a4153e425d1e8b62fec508", + "sha256:985c06c2a0f227131733ae58d6a541a5bc8b665e7305494782bebdb74202b793", + "sha256:a86dfe45f4f9c55b1a2312ff20a59b30da8d39c0e8821d00018372a2a177098f", + "sha256:aa3cd07f7f7e3183b63d48300666f920828a9dbd7d7ec53d450df2c4953687a9", + "sha256:b1964ed645ef8317806d615d9ff006c0dadc09dfc54b99ae67f9ba7a1ec9d5d2", + "sha256:b2abbff9e4141484bb89b96eb8eae186d77bc6d5ffbec6b01783ee5c3c467351", + "sha256:cc33c3a90492e21713260095f02b12bee02b8d1f2c03a221d763ce04fa90e2e9", + "sha256:d7de3bf0986d777807611c36e809b77a13bf1888f5c8db0ebf24b47a52d10726", + "sha256:db5e3c52576cc5b93a959a03ccc3b02cb8f0af1fbbdc80645f7a215f0b864f3a", + "sha256:e168aa795ffbb11379c942cf95bf813c7db9aa55538eb61de8c6815e092416f5", + "sha256:e9ca911f8e2d3117e5241d5fa9aaa991cb22fb0792627eeada47425d706b5ec8", + "sha256:eccf962d41ca46e6326b97c8fe0a6687b58dfc1a5f6540ed071ff1474cea749e", + "sha256:efa19deae6b9e504a74347fe5e25c2cb9343766c489c2ae921b05f37338b18d1", + "sha256:f4b0460a21f784abe17b496f66e74157a6c36116fa86da8bf6aa028b9e8ad5fe", + "sha256:f93d508ca64d924d478fb11e272e09524698f0c581d9032e68958cfbdd41faef" + ], + "index": "pypi", + "version": "==2.7.5" + }, + "psycopg2-binary": { + "hashes": [ + "sha256:04afb59bbbd2eab3148e6816beddc74348078b8c02a1113ea7f7822f5be4afe3", + "sha256:098b18f4d8857a8f9b206d1dc54db56c2255d5d26458917e7bcad61ebfe4338f", + "sha256:0bf855d4a7083e20ead961fda4923887094eaeace0ab2d76eb4aa300f4bbf5bd", + "sha256:197dda3ffd02057820be83fe4d84529ea70bf39a9a4daee1d20ffc74eb3d042e", + "sha256:278ef63afb4b3d842b4609f2c05ffbfb76795cf6a184deeb8707cd5ed3c981a5", + "sha256:3cbf8c4fc8f22f0817220891cf405831559f4d4c12c4f73913730a2ea6c47a47", + "sha256:4305aed922c4d9d6163ab3a41d80b5a1cfab54917467da8168552c42cad84d32", + "sha256:47ee296f704fb8b2a616dec691cdcfd5fa0f11943955e88faa98cbd1dc3b3e3d", + "sha256:4a0e38cb30457e70580903367161173d4a7d1381eb2f2cfe4e69b7806623f484", + "sha256:4d6c294c6638a71cafb82a37f182f24321f1163b08b5d5ca076e11fe838a3086", + "sha256:4f3233c366500730f839f92833194fd8f9a5c4529c8cd8040aa162c3740de8e5", + "sha256:5221f5a3f4ca2ddf0d58e8b8a32ca50948be9a43351fda797eb4e72d7a7aa34d", + "sha256:5c6ca0b507540a11eaf9e77dee4f07c131c2ec80ca0cffa146671bf690bc1c02", + "sha256:789bd89d71d704db2b3d5e67d6d518b158985d791d3b2dec5ab85457cfc9677b", + "sha256:7b94d29239efeaa6a967f3b5971bd0518d2a24edd1511edbf4a2c8b815220d07", + "sha256:89bc65ef3301c74cf32db25334421ea6adbe8f65601ea45dcaaf095abed910bb", + "sha256:89d6d3a549f405c20c9ae4dc94d7ed2de2fa77427a470674490a622070732e62", + "sha256:97521704ac7127d7d8ba22877da3c7bf4a40366587d238ec679ff38e33177498", + "sha256:a395b62d5f44ff6f633231abe568e2203b8fabf9797cd6386aa92497df912d9a", + "sha256:a6d32c37f714c3f34158f3fa659f3a8f2658d5f53c4297d45579b9677cc4d852", + "sha256:a89ee5c26f72f2d0d74b991ce49e42ddeb4ac0dc2d8c06a0f2770a1ab48f4fe0", + "sha256:b4c8b0ef3608e59317bfc501df84a61e48b5445d45f24d0391a24802de5f2d84", + "sha256:b5fcf07140219a1f71e18486b8dc28e2e1b76a441c19374805c617aa6d9a9d55", + "sha256:b86f527f00956ecebad6ab3bb30e3a75fedf1160a8716978dd8ce7adddedd86f", + "sha256:be4c4aa22ba22f70de36c98b06480e2f1697972d49eb20d525f400d204a6d272", + "sha256:c2ac7aa1a144d4e0e613ac7286dae85671e99fe7a1353954d4905629c36b811c", + "sha256:de26ef4787b5e778e8223913a3e50368b44e7480f83c76df1f51d23bd21cea16", + "sha256:e70ebcfc5372dc7b699c0110454fc4263967f30c55454397e5769eb72c0eb0ce", + "sha256:eadbd32b6bc48b67b0457fccc94c86f7ccc8178ab839f684eb285bb592dc143e", + "sha256:ecbc6dfff6db06b8b72ae8a2f25ff20fbdcb83cb543811a08f7cb555042aa729" + ], + "index": "pypi", + "version": "==2.7.5" + }, + "pycparser": { + "hashes": [ + "sha256:99a8ca03e29851d96616ad0404b4aad7d9ee16f25c9f9708a11faf2810f7b226" + ], + "index": "pypi", + "version": "==2.18" + }, + "pyjwt": { + "hashes": [ + "sha256:500be75b17a63f70072416843dc80c8821109030be824f4d14758f114978bae7", + "sha256:a4e5f1441e3ca7b382fd0c0b416777ced1f97c64ef0c33bfa39daf38505cfd2f" + ], + "index": "pypi", + "version": "==1.5.3" + }, + "pypdf2": { + "hashes": [ + "sha256:e28f902f2f0a1603ea95ebe21dff311ef09be3d0f0ef29a3e44a932729564385" + ], + "index": "pypi", + "version": "==1.26.0" + }, + "pyrestcli": { + "hashes": [ + "sha256:9c3b984f3b515618479ca3057072a9f150a42bdb46dc15e73710e8b44198dab9" + ], + "index": "pypi", + "version": "==0.6.4" + }, + "python-crontab": { + "hashes": [ + "sha256:d28583dc5b2f37f707ad37221f390933cc24ee4f350a641fdc3cf84ba22a9dec" + ], + "index": "pypi", + "version": "==2.3.5" + }, + "python-dateutil": { + "hashes": [ + "sha256:1408fdb07c6a1fa9997567ce3fcee6a337b39a503d80699e0f213de4aa4b32ed", + "sha256:598499a75be2e5e18a66f12c00dd47a069de24794effeda4228bfc760f44f527", + "sha256:9d94861f04ce14f9a3d835206067c889b8f1244f1415035dadcf9c10066adf04" + ], + "index": "pypi", + "version": "==2.5.3" + }, + "python3-openid": { + "hashes": [ + "sha256:0086da6b6ef3161cfe50fb1ee5cceaf2cda1700019fda03c2c5c440ca6abe4fa", + "sha256:628d365d687e12da12d02c6691170f4451db28d6d68d050007e4a40065868502" + ], + "index": "pypi", + "version": "==3.1.0" + }, + "pytz": { + "hashes": [ + "sha256:07edfc3d4d2705a20a6e99d97f0c4b61c800b8232dc1c04d87e8554f130148dd", + "sha256:410bcd1d6409026fbaa65d9ed33bf6dd8b1e94a499e32168acfc7b332e4095c0" + ], + "index": "pypi", + "version": "==2018.3" + }, + "pyyaml": { + "hashes": [ + "sha256:16b20e970597e051997d90dc2cddc713a2876c47e3d92d59ee198700c5427736", + "sha256:3262c96a1ca437e7e4763e2843746588a965426550f3797a79fca9c6199c431f", + "sha256:592766c6303207a20efc445587778322d7f73b161bd994f227adaa341ba212ab", + "sha256:5ac82e411044fb129bae5cfbeb3ba626acb2af31a8d17d175004b70862a741a7", + "sha256:827dc04b8fa7d07c44de11fabbc888e627fa8293b695e0f99cb544fdfa1bf0d1", + "sha256:bc6bced57f826ca7cb5125a10b23fd0f2fff3b7c4701d64c439a300ce665fff8", + "sha256:c01b880ec30b5a6e6aa67b09a2fe3fb30473008c85cd6a67359a1b15ed6d83a4", + "sha256:e863072cdf4c72eebf179342c94e6989c67185842d9997960b3e69290b2fa269" + ], + "index": "pypi", + "version": "==3.12" + }, + "raven": { + "hashes": [ + "sha256:3fd787d19ebb49919268f06f19310e8112d619ef364f7989246fc8753d469888", + "sha256:95f44f3ea2c1b176d5450df4becdb96c15bf2632888f9ab193e9dd22300ce46a" + ], + "index": "pypi", + "version": "==6.9" + }, + "redis": { + "hashes": [ + "sha256:8a1900a9f2a0a44ecf6e8b5eb3e967a9909dfed219ad66df094f27f7d6f330fb", + "sha256:a22ca993cea2962dbb588f9f30d0015ac4afcc45bee27d3978c0dbe9e97c6c0f" + ], + "index": "pypi", + "version": "==2.10.6" + }, + "reportlab": { + "hashes": [ + "sha256:04ca57bebe2c3e06550aae82a468ac80d97249178f3aca4496872a9ebf8ba391", + "sha256:1a97e6f06d21682476a12fee60506ea6e8e2f67c180f227a5272e14e2f2cdf5d", + "sha256:1c9d782cddada11b0c83bd5c3f124fed9ba189c718499f5baecae18c7574cf68", + "sha256:1ca461c2f4b4e8685fde083b4fca9189f5120881f87bc6ac7e923127cafbf70a", + "sha256:1cafbe667567d1a34febc00011f097665b705e3cb8bbea307de87a324b318fe8", + "sha256:22c1515321dad5a372109fdd3a68e25d59c2f06883e56d991670156fb8a675fc", + "sha256:2725bf2d8ff228a0d7914fcbd70e012fc0435d6489d3cd8e6331a3f545dfb8c7", + "sha256:30845e210cee64c0ac53f2fa6b8ce8878e5437514bc9527ce2fe71ab5e8c79bb", + "sha256:3bb05d06c33bf1dee166a375caa2a39f110d6b3504988c2a5591eae4a179eb01", + "sha256:4894b76d94b2a57696bcecc06606366584b06214af8c231d71499a52d451ccb4", + "sha256:495603088d81ca80d3a3f5e0587402522c1a2f9d10658e477d7601db73d59718", + "sha256:4c15800a83c4e7dc1924d8fb2ec1e46ad1a097601d53439e189cfb8999c3c889", + "sha256:552c4d18eee5037e396eef5132fb0233282d38be43d04cd1ae06dddce848f3b0", + "sha256:61f2a8efc021ec2f48aa7af854ce2e2d6f4620657dad147ac0dedd6e342abd3a", + "sha256:6f615ce97b0631450f227edb122cde35b2d597863766c15514d3b4e165109b09", + "sha256:7c42b12829f5958b60d4a8fc0664db2de98779a0646ef00a6ea39c10f47df374", + "sha256:7cb5349be994d276fe10425ab4f230fb6cbcdab027287bb6033a96f953cdf5e1", + "sha256:95914d90df4d72b3e4b0f97dca095bd83bdbbcf50b2e2e5132329c76ecb941c7", + "sha256:9b91208f4539ef75f4efcd62ef8ce5b476b18b6157bc8b0c7050ebd75eaf22b7", + "sha256:a7a8acf70d852771c14a5a3be5174233419e7ad17d73a3b9820646346ef62bcb", + "sha256:b78a465c993edc1a6e0f4184a49570ae8a42f048c9ef855bf65c0cee54b0b8bf", + "sha256:b93f1a1edef554fa9c0d71091c20797f9289b6ad048b379fc3f62920416e164e", + "sha256:ba7518c099417eaab0000238984e0f7010602793687891b9ac7f58f9adc1b3a4", + "sha256:baba0edec7f763143d04e82f3bc4c9fb99d4a43439969f0abaf4788a8a92e452", + "sha256:bbb807604d98d062275c59d53be431ca7f229aa209fcdf4a878b6a3ac5cabd98", + "sha256:c8410ce1e2504c083c84b8310f57813469a7efced2485456badcedb63ae1e8f2", + "sha256:d003f5414291b8112d80e2940b0e1de13f5eab2ca4db0b6a3f4597b95c451351", + "sha256:f92f81314807cd860f29fe07a1a4100b03910ae6bbfca20a07e02c3b460f4f20" + ], + "index": "pypi", + "version": "==3.5.9" + }, + "requests": { + "hashes": [ + "sha256:545c4855cd9d7c12671444326337013766f4eea6068c3f0307fb2dc2696d580e", + "sha256:5acf980358283faba0b897c73959cecf8b841205bb4b2ad3ef545f46eae1a133" + ], + "index": "pypi", + "version": "==2.11.1" + }, + "requests-oauthlib": { + "hashes": [ + "sha256:50a8ae2ce8273e384895972b56193c7409601a66d4975774c60c2aed869639ca", + "sha256:883ac416757eada6d3d07054ec7092ac21c7f35cb1d2cf82faf205637081f468" + ], + "index": "pypi", + "version": "==0.8.0" + }, + "simplejson": { + "hashes": [ + "sha256:067a7177ddfa32e1483ba5169ebea1bc2ea27f224853211ca669325648ca5642", + "sha256:2fc546e6af49fb45b93bbe878dea4c48edc34083729c0abd09981fe55bdf7f91", + "sha256:354fa32b02885e6dae925f1b5bbf842c333c1e11ea5453ddd67309dc31fdb40a", + "sha256:37e685986cf6f8144607f90340cff72d36acf654f3653a6c47b84c5c38d00df7", + "sha256:3af610ee72efbe644e19d5eaad575c73fb83026192114e5f6719f4901097fce2", + "sha256:3b919fc9cf508f13b929a9b274c40786036b31ad28657819b3b9ba44ba651f50", + "sha256:3dd289368bbd064974d9a5961101f080e939cbe051e6689a193c99fb6e9ac89b", + "sha256:6c3258ffff58712818a233b9737fe4be943d306c40cf63d14ddc82ba563f483a", + "sha256:75e3f0b12c28945c08f54350d91e624f8dd580ab74fd4f1bbea54bc6b0165610", + "sha256:b1f329139ba647a9548aa05fb95d046b4a677643070dc2afc05fa2e975d09ca5", + "sha256:ee9625fc8ee164902dfbb0ff932b26df112da9f871c32f0f9c1bcf20c350fe2a", + "sha256:fb2530b53c28f0d4d84990e945c2ebb470edb469d63e389bf02ff409012fe7c5" + ], + "index": "pypi", + "version": "==3.16.0" + }, + "six": { + "hashes": [ + "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9", + "sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb" + ], + "index": "pypi", + "version": "==1.11.0" + }, + "social-auth-app-django": { + "hashes": [ + "sha256:25295c94375d28062f65d0b3cd4b7e304c7e2fb7bac1ec51b40650a654c352f4", + "sha256:4e34d86709bfa51fd2938307e00f12f4e1dfef8a8ed6bfbfe9aeb4e69d57148f", + "sha256:b7c28bef8fbd11ff357ddd885cb219cdb55565e01109c709dd28569e0bfb0dea" + ], + "index": "pypi", + "version": "==2.1.0" + }, + "social-auth-core": { + "hashes": [ + "sha256:273eb5bbeded3cfc178ca7e14f0641165df03133a2f787a6e412f782489d56ba", + "sha256:7b393754ab75f6e5176568554f4f7b5cd9e4cb6dab23d9614e5c9e1425f3fcf9", + "sha256:eb0d0e29d0cfa729cd52437314d4aeb83806c4d6e7824cbe988195b6a4b85163" + ], + "index": "pypi", + "markers": null, + "version": "==1.7.0" + }, + "sqlparse": { + "hashes": [ + "sha256:ce028444cfab83be538752a2ffdb56bc417b7784ff35bb9a3062413717807dec", + "sha256:d9cf190f51cbb26da0412247dfe4fb5f4098edb73db84e02f9fc21fdca31fed4" + ], + "index": "pypi", + "version": "==0.2.4" + }, + "static3": { + "hashes": [ + "sha256:674641c64bc75507af2eb20bef7e7e3593dca993dec6674be108fa15b42f47c8" + ], + "index": "pypi", + "version": "==0.7.0" + }, + "tablib": { + "hashes": [ + "sha256:b8cf50a61d66655229993f2ee29220553fb2c80403479f8e6de77c0c24649d87" + ], + "index": "pypi", + "version": "==0.12.1" + }, + "tenant-schemas-celery": { + "hashes": [ + "sha256:67f90e32777b9b7a9b384a4a0415d03574b998ffbf400871825f528a321a9447" + ], + "index": "pypi", + "version": "==0.2.1" + }, + "tornado": { + "hashes": [ + "sha256:0662d28b1ca9f67108c7e3b77afabfb9c7e87bde174fbda78186ecedc2499a9d", + "sha256:4e5158d97583502a7e2739951553cbd88a72076f152b4b11b64b9a10c4c49409", + "sha256:732e836008c708de2e89a31cb2fa6c0e5a70cb60492bee6f1ea1047500feaf7f", + "sha256:8154ec22c450df4e06b35f131adc4f2f3a12ec85981a203301d310abf580500f", + "sha256:8e9d728c4579682e837c92fdd98036bd5cdefa1da2aaf6acf26947e6dd0c01c5", + "sha256:d4b3e5329f572f055b587efc57d29bd051589fb5a43ec8898c77a47ec2fa2bbb", + "sha256:e5f2585afccbff22390cddac29849df463b252b711aa2ce7c5f3f342a5b3b444" + ], + "index": "pypi", + "version": "==5.1.1" + }, + "unicef-attachments": { + "hashes": [ + "sha256:86712628bdbbd6c60b4261b720b267c3340a8bf96582f6b1e3a59039d2928953" + ], + "version": "==0.4.2" + }, + "unicef-djangolib": { + "hashes": [ + "sha256:2dfd6efc398b9dba62751a0c78d526fbc629bf92cd9d73534df03fd2dbb3da69", + "sha256:62924b27e859a54a61d2ed90d6c23b7b913a0e1a7b0164ea2c20801e2d5d79d3" + ], + "index": "pypi", + "version": "==0.5.2" + }, + "unicef-locations": { + "hashes": [ + "sha256:5dde31cb0959acb8bc868f89419caa5d8e736cfc8cd45248fe2b21a107a12883" + ], + "index": "pypi", + "version": "==1.5" + }, + "unicef-notification": { + "hashes": [ + "sha256:06e8971eecae5a86a67aed75be4925aaddc9fac1ea82de02190a9b7a5c59e233" + ], + "version": "==0.2.0" + }, + "unicef-restlib": { + "hashes": [ + "sha256:256f81a7f3ee4ab726af02c0c4b872788675676454d44c9006ce4bec596f7351" + ], + "version": "==0.3.8" + }, + "unicef-snapshot": { + "hashes": [ + "sha256:7083ea95b7794b716eb3e528dc01a4ace7a5f0ab5316f5e4cb2be2cf4ef67d72" + ], + "version": "==0.2.1" + }, + "unicodecsv": { + "hashes": [ + "sha256:018c08037d48649a0412063ff4eda26eaa81eff1546dbffa51fa5293276ff7fc" + ], + "index": "pypi", + "version": "==0.14.1" + }, + "uritemplate": { + "hashes": [ + "sha256:01c69f4fe8ed503b2951bef85d996a9d22434d2431584b5b107b2981ff416fbd", + "sha256:1b9c467a940ce9fb9f50df819e8ddd14696f89b9a8cc87ac77952ba416e0a8fd", + "sha256:c02643cebe23fc8adb5e6becffe201185bf06c40bda5c0b4028a93f1527d011d" + ], + "index": "pypi", + "version": "==3.0.0" + }, + "urllib3": { + "hashes": [ + "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", + "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" + ], + "markers": "python_version != '3.0.*' and python_version != '3.1.*' and python_version >= '2.7' and python_version < '4' and python_version != '3.3.*' and python_version != '3.2.*'", + "version": "==1.24.1" + }, + "vine": { + "hashes": [ + "sha256:52116d59bc45392af9fdd3b75ed98ae48a93e822cee21e5fda249105c59a7a72", + "sha256:6849544be74ec3638e84d90bc1cf2e1e9224cc10d96cd4383ec3f69e9bce077b" + ], + "index": "pypi", + "version": "==1.1.4" + }, + "webencodings": { + "hashes": [ + "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", + "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923" + ], + "index": "pypi", + "version": "==0.5.1" + }, + "xhtml2pdf": { + "hashes": [ + "sha256:86a37e78d7a8d8bb2761746c3d559e12284d92c4d531b3a8a0f8fd632b436f82" + ], + "index": "pypi", + "version": "==0.2.3" + }, + "xlrd": { + "hashes": [ + "sha256:83a1d2f1091078fb3f65876753b5302c5cfb6a41de64b9587b74cefa75157148", + "sha256:8a21885513e6d915fe33a8ee5fdfa675433b61405ba13e2a69e62ee36828d7e2" + ], + "index": "pypi", + "version": "==1.1.0" + }, + "xlwt": { + "hashes": [ + "sha256:a082260524678ba48a297d922cc385f58278b8aa68741596a87de01a9c628b2e", + "sha256:c59912717a9b28f1a3c2a98fd60741014b06b043936dcecbc113eaaada156c88" + ], + "index": "pypi", + "version": "==1.3.0" + } + }, + "develop": { + "alabaster": { + "hashes": [ + "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359", + "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02" + ], + "version": "==0.7.12" + }, + "babel": { + "hashes": [ + "sha256:6778d85147d5d85345c14a26aada5e478ab04e39b078b0745ee6870c2b5cf669", + "sha256:8cba50f48c529ca3fa18cf81fa9403be176d374ac4d60738b839122dfaaa3d23" + ], + "index": "pypi", + "version": "==2.6.0" + }, + "backcall": { + "hashes": [ + "sha256:38ecd85be2c1e78f77fd91700c76e14667dc21e2713b63876c0eb901196e01e4", + "sha256:bbbf4b1e5cd2bdb08f915895b51081c041bac22394fdfcfdfbe9f14b77c08bf2" + ], + "version": "==0.1.0" + }, + "certifi": { + "hashes": [ + "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", + "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" + ], + "version": "==2018.11.29" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "coverage": { + "hashes": [ + "sha256:09e47c529ff77bf042ecfe858fb55c3e3eb97aac2c87f0349ab5a7efd6b3939f", + "sha256:0a1f9b0eb3aa15c990c328535655847b3420231af299386cfe5efc98f9c250fe", + "sha256:0cc941b37b8c2ececfed341444a456912e740ecf515d560de58b9a76562d966d", + "sha256:10e8af18d1315de936d67775d3a814cc81d0747a1a0312d84e27ae5610e313b0", + "sha256:1b4276550b86caa60606bd3572b52769860a81a70754a54acc8ba789ce74d607", + "sha256:1e8a2627c48266c7b813975335cfdea58c706fe36f607c97d9392e61502dc79d", + "sha256:2b224052bfd801beb7478b03e8a66f3f25ea56ea488922e98903914ac9ac930b", + "sha256:447c450a093766744ab53bf1e7063ec82866f27bcb4f4c907da25ad293bba7e3", + "sha256:46101fc20c6f6568561cdd15a54018bb42980954b79aa46da8ae6f008066a30e", + "sha256:4710dc676bb4b779c4361b54eb308bc84d64a2fa3d78e5f7228921eccce5d815", + "sha256:510986f9a280cd05189b42eee2b69fecdf5bf9651d4cd315ea21d24a964a3c36", + "sha256:5535dda5739257effef56e49a1c51c71f1d37a6e5607bb25a5eee507c59580d1", + "sha256:5a7524042014642b39b1fcae85fb37556c200e64ec90824ae9ecf7b667ccfc14", + "sha256:5f55028169ef85e1fa8e4b8b1b91c0b3b0fa3297c4fb22990d46ff01d22c2d6c", + "sha256:6694d5573e7790a0e8d3d177d7a416ca5f5c150742ee703f3c18df76260de794", + "sha256:6831e1ac20ac52634da606b658b0b2712d26984999c9d93f0c6e59fe62ca741b", + "sha256:77f0d9fa5e10d03aa4528436e33423bfa3718b86c646615f04616294c935f840", + "sha256:828ad813c7cdc2e71dcf141912c685bfe4b548c0e6d9540db6418b807c345ddd", + "sha256:85a06c61598b14b015d4df233d249cd5abfa61084ef5b9f64a48e997fd829a82", + "sha256:8cb4febad0f0b26c6f62e1628f2053954ad2c555d67660f28dfb1b0496711952", + "sha256:a5c58664b23b248b16b96253880b2868fb34358911400a7ba39d7f6399935389", + "sha256:aaa0f296e503cda4bc07566f592cd7a28779d433f3a23c48082af425d6d5a78f", + "sha256:ab235d9fe64833f12d1334d29b558aacedfbca2356dfb9691f2d0d38a8a7bfb4", + "sha256:b3b0c8f660fae65eac74fbf003f3103769b90012ae7a460863010539bb7a80da", + "sha256:bab8e6d510d2ea0f1d14f12642e3f35cefa47a9b2e4c7cea1852b52bc9c49647", + "sha256:c45297bbdbc8bb79b02cf41417d63352b70bcb76f1bbb1ee7d47b3e89e42f95d", + "sha256:d19bca47c8a01b92640c614a9147b081a1974f69168ecd494687c827109e8f42", + "sha256:d64b4340a0c488a9e79b66ec9f9d77d02b99b772c8b8afd46c1294c1d39ca478", + "sha256:da969da069a82bbb5300b59161d8d7c8d423bc4ccd3b410a9b4d8932aeefc14b", + "sha256:ed02c7539705696ecb7dc9d476d861f3904a8d2b7e894bd418994920935d36bb", + "sha256:ee5b8abc35b549012e03a7b1e86c09491457dba6c94112a2482b18589cc2bdb9" + ], + "index": "pypi", + "version": "==4.5.2" + }, + "decorator": { + "hashes": [ + "sha256:2c51dff8ef3c447388fe5e4453d24a2bf128d3a4c32af3fabef1f01c6851ab82", + "sha256:c39efa13fbdeb4506c476c9b3babf6a718da943dab7811c206005a4a956c080c" + ], + "version": "==4.3.0" + }, + "django-extensions": { + "hashes": [ + "sha256:8317a3fe479b1ba3e3a04ecf33fb8d6ccf09bb18f30eab64e34c40a593741d26", + "sha256:a76a61566f1c8d96acc7bcf765080b8e91367a25a2c6f8c5bddd574493839180" + ], + "index": "pypi", + "version": "==2.1.4" + }, + "djangorestframework": { + "hashes": [ + "sha256:607865b0bb1598b153793892101d881466bd5a991de12bd6229abb18b1c86136", + "sha256:63f76cbe1e7d12b94c357d7e54401103b2e52aef0f7c1650d6c820ad708776e5" + ], + "index": "pypi", + "version": "==3.9.0" + }, + "docutils": { + "hashes": [ + "sha256:02aec4bd92ab067f6ff27a38a38a41173bf01bed8f89157768c1573f53e474a6", + "sha256:51e64ef2ebfb29cae1faa133b3710143496eca21c530f3f71424d77687764274", + "sha256:7a4bd47eaf6596e1295ecb11361139febe29b084a87bf005bf899f9a42edc3c6" + ], + "version": "==0.14" + }, + "drf-api-checker": { + "hashes": [ + "sha256:db4045afe4585120ab298464c37887d8567a0ecadbe8eaa6a0e348c364d745bb" + ], + "index": "pypi", + "version": "==0.4.1" + }, + "factory-boy": { + "hashes": [ + "sha256:6f25cc4761ac109efd503f096e2ad99421b1159f01a29dbb917359dcd68e08ca", + "sha256:d552cb872b310ae78bd7429bf318e42e1e903b1a109e899a523293dfa762ea4f" + ], + "markers": "python_version != '3.0.*' and python_version != '3.1.*' and python_version >= '2.7' and python_version != '3.2.*'", + "version": "==2.11.1" + }, + "faker": { + "hashes": [ + "sha256:228419b0a788a7ac867ebfafdd438461559ab1a0975edb607300852d9acaa78d", + "sha256:52a3dcc6a565b15fe1c95090321756d5a8a7c1caf5ab3df2f573ed70936ff518" + ], + "markers": "python_version != '3.0.*' and python_version != '3.1.*' and python_version >= '2.7' and python_version != '3.2.*'", + "version": "==1.0.1" + }, + "fancycompleter": { + "hashes": [ + "sha256:d2522f1f3512371f295379c4c0d1962de06762eb586c199620a2a5d423539b12" + ], + "version": "==0.8" + }, + "filelock": { + "hashes": [ + "sha256:b8d5ca5ca1c815e1574aee746650ea7301de63d87935b3463d26368b76e31633", + "sha256:d610c1bb404daf85976d7a82eb2ada120f04671007266b708606565dd03b5be6" + ], + "version": "==3.0.10" + }, + "flake8": { + "hashes": [ + "sha256:6a35f5b8761f45c5513e3405f110a86bea57982c3b75b766ce7b65217abe1670", + "sha256:c01f8a3963b3571a8e6bd7a4063359aff90749e160778e03817cd9b71c9e07d2" + ], + "index": "pypi", + "version": "==3.6.0" + }, + "freezegun": { + "hashes": [ + "sha256:6cb82b276f83f2acce67f121dc2656f4df26c71e32238334eb071170b892a278", + "sha256:e839b43bfbe8158b4d62bb97e6313d39f3586daf48e1314fb1083d2ef17700da" + ], + "index": "pypi", + "version": "==0.3.11" + }, + "idna": { + "hashes": [ + "sha256:2c6a5de3089009e3da7c5dde64a141dbc8551d5b7f6cf4ed7c2568d0cc520a8f", + "sha256:8c7309c718f94b3a625cb648ace320157ad16ff131ae0af362c9f21b80ef6ec4" + ], + "index": "pypi", + "version": "==2.6" + }, + "imagesize": { + "hashes": [ + "sha256:3f349de3eb99145973fefb7dbe38554414e5c30abd0c8e4b970a7c9d09f3a1d8", + "sha256:f3832918bc3c66617f92e35f5d70729187676313caa60c187eb0f28b8fe5e3b5" + ], + "markers": "python_version != '3.2.*' and python_version >= '2.7' and python_version != '3.0.*' and python_version != '3.3.*' and python_version != '3.1.*'", + "version": "==1.1.0" + }, + "ipython": { + "hashes": [ + "sha256:6a9496209b76463f1dec126ab928919aaf1f55b38beb9219af3fe202f6bbdd12", + "sha256:f69932b1e806b38a7818d9a1e918e5821b685715040b48e59c657b3c7961b742" + ], + "index": "pypi", + "version": "==7.2.0" + }, + "ipython-genutils": { + "hashes": [ + "sha256:72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8", + "sha256:eb2e116e75ecef9d4d228fdc66af54269afa26ab4463042e33785b887c628ba8" + ], + "version": "==0.2.0" + }, + "isort": { + "hashes": [ + "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", + "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", + "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" + ], + "index": "pypi", + "version": "==4.3.4" + }, + "jedi": { + "hashes": [ + "sha256:571702b5bd167911fe9036e5039ba67f820d6502832285cde8c881ab2b2149fd", + "sha256:c8481b5e59d34a5c7c42e98f6625e633f6ef59353abea6437472c7ec2093f191" + ], + "version": "==0.13.2" + }, + "jinja2": { + "hashes": [ + "sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd", + "sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4" + ], + "index": "pypi", + "markers": "python_version != '3.3.*' and python_version != '3.0.*' and python_version != '3.2.*' and python_version >= '2.7' and python_version != '3.1.*'", + "version": "==2.10" + }, + "markupsafe": { + "hashes": [ + "sha256:048ef924c1623740e70204aa7143ec592504045ae4429b59c30054cb31e3c432", + "sha256:130f844e7f5bdd8e9f3f42e7102ef1d49b2e6fdf0d7526df3f87281a532d8c8b", + "sha256:19f637c2ac5ae9da8bfd98cef74d64b7e1bb8a63038a3505cd182c3fac5eb4d9", + "sha256:1b8a7a87ad1b92bd887568ce54b23565f3fd7018c4180136e1cf412b405a47af", + "sha256:1c25694ca680b6919de53a4bb3bdd0602beafc63ff001fea2f2fc16ec3a11834", + "sha256:1f19ef5d3908110e1e891deefb5586aae1b49a7440db952454b4e281b41620cd", + "sha256:1fa6058938190ebe8290e5cae6c351e14e7bb44505c4a7624555ce57fbbeba0d", + "sha256:31cbb1359e8c25f9f48e156e59e2eaad51cd5242c05ed18a8de6dbe85184e4b7", + "sha256:3e835d8841ae7863f64e40e19477f7eb398674da6a47f09871673742531e6f4b", + "sha256:4e97332c9ce444b0c2c38dd22ddc61c743eb208d916e4265a2a3b575bdccb1d3", + "sha256:525396ee324ee2da82919f2ee9c9e73b012f23e7640131dd1b53a90206a0f09c", + "sha256:52b07fbc32032c21ad4ab060fec137b76eb804c4b9a1c7c7dc562549306afad2", + "sha256:52ccb45e77a1085ec5461cde794e1aa037df79f473cbc69b974e73940655c8d7", + "sha256:5c3fbebd7de20ce93103cb3183b47671f2885307df4a17a0ad56a1dd51273d36", + "sha256:5e5851969aea17660e55f6a3be00037a25b96a9b44d2083651812c99d53b14d1", + "sha256:5edfa27b2d3eefa2210fb2f5d539fbed81722b49f083b2c6566455eb7422fd7e", + "sha256:7d263e5770efddf465a9e31b78362d84d015cc894ca2c131901a4445eaa61ee1", + "sha256:83381342bfc22b3c8c06f2dd93a505413888694302de25add756254beee8449c", + "sha256:857eebb2c1dc60e4219ec8e98dfa19553dae33608237e107db9c6078b1167856", + "sha256:98e439297f78fca3a6169fd330fbe88d78b3bb72f967ad9961bcac0d7fdd1550", + "sha256:bf54103892a83c64db58125b3f2a43df6d2cb2d28889f14c78519394feb41492", + "sha256:d9ac82be533394d341b41d78aca7ed0e0f4ba5a2231602e2f05aa87f25c51672", + "sha256:e982fe07ede9fada6ff6705af70514a52beb1b2c3d25d4e873e82114cf3c5401", + "sha256:edce2ea7f3dfc981c4ddc97add8a61381d9642dc3273737e756517cc03e84dd6", + "sha256:efdc45ef1afc238db84cb4963aa689c0408912a0239b0721cb172b4016eb31d6", + "sha256:f137c02498f8b935892d5c0172560d7ab54bc45039de8805075e19079c639a9c", + "sha256:f82e347a72f955b7017a39708a3667f106e6ad4d10b25f237396a7115d8ed5fd", + "sha256:fb7c206e01ad85ce57feeaaa0bf784b97fa3cad0d4a5737bc5295785f5c613a1" + ], + "index": "pypi", + "markers": "python_version != '3.3.*' and python_version != '3.0.*' and python_version != '3.2.*' and python_version >= '2.7' and python_version != '3.1.*'", + "version": "==1.1.0" + }, + "mccabe": { + "hashes": [ + "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + ], + "version": "==0.6.1" + }, + "mock": { + "hashes": [ + "sha256:5ce3c71c5545b472da17b72268978914d0252980348636840bd34a00b5cc96c1", + "sha256:b158b6df76edd239b8208d481dc46b6afd45a846b7812ff0ce58971cf5bc8bba" + ], + "index": "pypi", + "version": "==2.0.0" + }, + "packaging": { + "hashes": [ + "sha256:0c98a5d0be38ed775798ece1b9727178c4469d9c3b4ada66e8e6b7849f8732af", + "sha256:9e1cbf8c12b1f1ce0bb5344b8d7ecf66a6f8a6e91bcb0c84593ed6d3ab5c4ab3" + ], + "markers": "python_version >= '2.6' and python_version != '3.0.*' and python_version != '3.1.*'", + "version": "==19.0" + }, + "parso": { + "hashes": [ + "sha256:35704a43a3c113cce4de228ddb39aab374b8004f4f2407d070b6a2ca784ce8a2", + "sha256:895c63e93b94ac1e1690f5fdd40b65f07c8171e3e53cbd7793b5b96c0e0a7f24" + ], + "version": "==0.3.1" + }, + "pbr": { + "hashes": [ + "sha256:f59d71442f9ece3dffc17bc36575768e1ee9967756e6b6535f0ee1f0054c3d68", + "sha256:f6d5b23f226a2ba58e14e49aa3b1bfaf814d0199144b95d78458212444de1387" + ], + "version": "==5.1.1" + }, + "pdbpp": { + "hashes": [ + "sha256:535085916fcfb768690ba0aeab2967c2a2163a0a60e5b703776846873e171399" + ], + "index": "pypi", + "version": "==0.9.3" + }, + "pexpect": { + "hashes": [ + "sha256:2a8e88259839571d1251d278476f3eec5db26deb73a70be5ed5dc5435e418aba", + "sha256:3fbd41d4caf27fa4a377bfd16fef87271099463e6fa73e92a52f92dfee5d425b" + ], + "markers": "sys_platform != 'win32'", + "version": "==4.6.0" + }, + "pickleshare": { + "hashes": [ + "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca", + "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56" + ], + "version": "==0.7.5" + }, + "pluggy": { + "hashes": [ + "sha256:8ddc32f03971bfdf900a81961a48ccf2fb677cf7715108f85295c67405798616", + "sha256:980710797ff6a041e9a73a5787804f848996ecaa6f8a1b1e08224a5894f2074a" + ], + "markers": "python_version != '3.0.*' and python_version != '3.1.*' and python_version >= '2.7' and python_version != '3.3.*' and python_version != '3.2.*'", + "version": "==0.8.1" + }, + "prompt-toolkit": { + "hashes": [ + "sha256:c1d6aff5252ab2ef391c2fe498ed8c088066f66bc64a8d5c095bbf795d9fec34", + "sha256:d4c47f79b635a0e70b84fdb97ebd9a274203706b1ee5ed44c10da62755cf3ec9", + "sha256:fd17048d8335c1e6d5ee403c3569953ba3eb8555d710bfc548faf0712666ea39" + ], + "markers": "python_version != '3.0.*' and python_version != '3.1.*' and python_version >= '2.6'", + "version": "==2.0.7" + }, + "ptyprocess": { + "hashes": [ + "sha256:923f299cc5ad920c68f2bc0bc98b75b9f838b93b599941a6b63ddbc2476394c0", + "sha256:d7cc528d76e76342423ca640335bd3633420dc1366f258cb31d05e865ef5ca1f" + ], + "version": "==0.6.0" + }, + "py": { + "hashes": [ + "sha256:bf92637198836372b520efcba9e020c330123be8ce527e535d185ed4b6f45694", + "sha256:e76826342cefe3c3d5f7e8ee4316b80d1dd8a300781612ddbc765c17ba25a6c6" + ], + "markers": "python_version != '3.0.*' and python_version != '3.1.*' and python_version >= '2.7' and python_version != '3.3.*' and python_version != '3.2.*'", + "version": "==1.7.0" + }, + "pycodestyle": { + "hashes": [ + "sha256:cbc619d09254895b0d12c2c691e237b2e91e9b2ecf5e84c26b35400f93dcfb83", + "sha256:cbfca99bd594a10f674d0cd97a3d802a1fdef635d4361e1a2658de47ed261e3a" + ], + "version": "==2.4.0" + }, + "pyflakes": { + "hashes": [ + "sha256:9a7662ec724d0120012f6e29d6248ae3727d821bba522a0e6b356eff19126a49", + "sha256:f661252913bc1dbe7fcfcbf0af0db3f42ab65aabd1a6ca68fe5d466bace94dae" + ], + "markers": "python_version != '3.0.*' and python_version != '3.1.*' and python_version >= '2.7' and python_version != '3.3.*' and python_version != '3.2.*'", + "version": "==2.0.0" + }, + "pygments": { + "hashes": [ + "sha256:5ffada19f6203563680669ee7f53b64dabbeb100eb51b61996085e99c03b284a", + "sha256:e8218dd399a61674745138520d0d4cf2621d7e032439341bc3f647bff125818d" + ], + "version": "==2.3.1" + }, + "pyparsing": { + "hashes": [ + "sha256:66c9268862641abcac4a96ba74506e594c884e3f57690a696d21ad8210ed667a", + "sha256:f6c5ef0d7480ad048c054c37632c67fca55299990fff127850181659eea33fc3" + ], + "version": "==2.3.1" + }, + "python-dateutil": { + "hashes": [ + "sha256:1408fdb07c6a1fa9997567ce3fcee6a337b39a503d80699e0f213de4aa4b32ed", + "sha256:598499a75be2e5e18a66f12c00dd47a069de24794effeda4228bfc760f44f527", + "sha256:9d94861f04ce14f9a3d835206067c889b8f1244f1415035dadcf9c10066adf04" + ], + "index": "pypi", + "version": "==2.5.3" + }, + "pytz": { + "hashes": [ + "sha256:07edfc3d4d2705a20a6e99d97f0c4b61c800b8232dc1c04d87e8554f130148dd", + "sha256:410bcd1d6409026fbaa65d9ed33bf6dd8b1e94a499e32168acfc7b332e4095c0" + ], + "index": "pypi", + "version": "==2018.3" + }, + "requests": { + "hashes": [ + "sha256:545c4855cd9d7c12671444326337013766f4eea6068c3f0307fb2dc2696d580e", + "sha256:5acf980358283faba0b897c73959cecf8b841205bb4b2ad3ef545f46eae1a133" + ], + "index": "pypi", + "version": "==2.11.1" + }, + "responses": { + "hashes": [ + "sha256:c85882d2dc608ce6b5713a4e1534120f4a0dc6ec79d1366570d2b0c909a50c87", + "sha256:ea5a14f9aea173e3b786ff04cf03133c2dabd4103dbaef1028742fd71a6c2ad3" + ], + "index": "pypi", + "version": "==0.10.5" + }, + "six": { + "hashes": [ + "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9", + "sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb" + ], + "index": "pypi", + "version": "==1.11.0" + }, + "snowballstemmer": { + "hashes": [ + "sha256:919f26a68b2c17a7634da993d91339e288964f93c274f1343e3bbbe2096e1128", + "sha256:9f3bcd3c401c3e862ec0ebe6d2c069ebc012ce142cce209c098ccb5b09136e89" + ], + "version": "==1.2.1" + }, + "sphinx": { + "hashes": [ + "sha256:429e3172466df289f0f742471d7e30ba3ee11f3b5aecd9a840480d03f14bcfe5", + "sha256:c4cb17ba44acffae3d3209646b6baec1e215cad3065e852c68cc569d4df1b9f8" + ], + "index": "pypi", + "version": "==1.8.3" + }, + "sphinxcontrib-websupport": { + "hashes": [ + "sha256:68ca7ff70785cbe1e7bccc71a48b5b6d965d79ca50629606c7861a21b206d9dd", + "sha256:9de47f375baf1ea07cdb3436ff39d7a9c76042c10a769c52353ec46e4e8fc3b9" + ], + "markers": "python_version != '3.2.*' and python_version >= '2.7' and python_version != '3.0.*' and python_version != '3.3.*' and python_version != '3.1.*'", + "version": "==1.1.0" + }, + "text-unidecode": { + "hashes": [ + "sha256:5a1375bb2ba7968740508ae38d92e1f889a0832913cb1c447d5e2046061a396d", + "sha256:801e38bd550b943563660a91de8d4b6fa5df60a542be9093f7abf819f86050cc" + ], + "version": "==1.2" + }, + "toml": { + "hashes": [ + "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", + "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e" + ], + "version": "==0.10.0" + }, + "tox": { + "hashes": [ + "sha256:04f8f1aa05de8e76d7a266ccd14e0d665d429977cd42123bc38efa9b59964e9e", + "sha256:25ef928babe88c71e3ed3af0c464d1160b01fca2dd1870a5bb26c2dea61a17fc" + ], + "index": "pypi", + "version": "==3.7.0" + }, + "traitlets": { + "hashes": [ + "sha256:9c4bd2d267b7153df9152698efb1050a5d84982d3384a37b2c1f7723ba3e7835", + "sha256:c6cb5e6f57c5a9bdaa40fa71ce7b4af30298fbab9ece9815b5d995ab6217c7d9" + ], + "version": "==4.3.2" + }, + "urllib3": { + "hashes": [ + "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", + "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" + ], + "markers": "python_version != '3.0.*' and python_version != '3.1.*' and python_version >= '2.7' and python_version < '4' and python_version != '3.3.*' and python_version != '3.2.*'", + "version": "==1.24.1" + }, + "virtualenv": { + "hashes": [ + "sha256:34b9ae3742abed2f95d3970acf4d80533261d6061b51160b197f84e5b4c98b4c", + "sha256:fa736831a7b18bd2bfeef746beb622a92509e9733d645952da136b0639cd40cd" + ], + "markers": "python_version != '3.0.*' and python_version != '3.1.*' and python_version >= '2.7' and python_version != '3.3.*' and python_version != '3.2.*'", + "version": "==16.2.0" + }, + "wcwidth": { + "hashes": [ + "sha256:3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e", + "sha256:f4ebe71925af7b40a864553f761ed559b43544f8f71746c2d756c7fe788ade7c" + ], + "version": "==0.1.7" + }, + "wmctrl": { + "hashes": [ + "sha256:d806f65ac1554366b6e31d29d7be2e8893996c0acbb2824bbf2b1f49cf628a13" + ], + "version": "==0.3" + } + } +} diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 5d2bb042af..0000000000 --- a/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -# This file is here because many Platforms as a Service look for -# requirements.txt in the root directory of a project. --r src/requirements/base.txt diff --git a/setup.py b/setup.py index fa83d3fc15..7df772b5a6 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ import sys from codecs import open -from setuptools import setup, find_packages +from setuptools import find_packages, setup ROOT = os.path.realpath(os.path.dirname(__file__)) init = os.path.join(ROOT, 'src', 'etools', '__init__.py') @@ -22,30 +22,6 @@ dependency_links = set() - -def get_requirements(env): - ret = [] - with open(f'src/requirements/{env}.txt') as fp: - for line in fp.readlines(): - if line.startswith('#'): - continue - line = line[:-1] - if line.startswith('-e git+'): - url = line.replace('-e ', '') - groups = re.match(".*@(?P.*)#egg=(?P.*)", line).groupdict() - version = groups['version'].replace('v', '') - egg = line.partition('egg=')[2] - ret.append(f"{egg}=={version}") - dependency_links.add(f"{url}-{version}") - else: - dep = line.partition('#')[0] - ret.append(dep.strip()) - return ret - - -install_requires = get_requirements('base') -test_requires = get_requirements('test') - setup( name=NAME, version=VERSION, @@ -57,13 +33,9 @@ def get_requirements(env): package_dir={'': 'src'}, packages=find_packages('src'), zip_safe=False, - install_requires=install_requires, dependency_links=list(dependency_links), license='BSD', include_package_data=True, - extras_require={ - 'test': test_requires, - }, classifiers=[ 'Framework :: Django', 'Operating System :: POSIX :: Linux', diff --git a/src/etools/applications/permissions2/views.py b/src/etools/applications/permissions2/views.py index a4c8c9cfbd..1869f90bb1 100644 --- a/src/etools/applications/permissions2/views.py +++ b/src/etools/applications/permissions2/views.py @@ -45,7 +45,7 @@ def post_transition(self, instance, action): """ pass - @action(detail=True, methods=['post'], url_path='(?P\D+)') + @action(detail=True, methods=['post'], url_path=r'(?P\D+)') def transition(self, request, *args, **kwargs): """ Change status of FSM controlled object diff --git a/src/etools/applications/users/tests/factories.py b/src/etools/applications/users/tests/factories.py index c3dfde9d52..d64fea4726 100644 --- a/src/etools/applications/users/tests/factories.py +++ b/src/etools/applications/users/tests/factories.py @@ -48,6 +48,7 @@ class Meta: class ProfileFactory(factory.django.DjangoModelFactory): class Meta: model = models.UserProfile + django_get_or_create = ('user',) country = factory.SubFactory(CountryFactory) office = factory.SubFactory(OfficeFactory) diff --git a/tasks.py b/tasks.py deleted file mode 100644 index 8e0ef4d547..0000000000 --- a/tasks.py +++ /dev/null @@ -1,30 +0,0 @@ -# -*- coding: utf-8 -*- -import os - -from invoke import task - -BASE_DIR = "{}/".format(os.path.dirname(os.path.abspath(__file__))) - - -@task -def update_requirements(ctx): - """Update requirements for all environments""" - ctx.run( - 'pip-compile {0}src/requirements/input/base.in ' - '--no-header ' - '--no-emit-trusted-host ' - '--no-index -o {0}src/requirements/base.txt'.format(BASE_DIR) - ) - ctx.run( - 'pip-compile {0}src/requirements/base.txt ' - '{0}src/requirements/input/test.in ' - '--no-header ' - '--no-emit-trusted-host ' - '--no-index -o {0}src/requirements/test.txt'.format(BASE_DIR) - ) - - -@task(help={"env": "Environment to install"}) -def install_requirements(ctx, env='production'): - """Install requirements for specified environment""" - ctx.run('pip-sync {0}src/requirements/{1}.txt'.format(BASE_DIR, env)) diff --git a/tox.ini b/tox.ini index 0e8a89b4e1..8ef301c9bd 100644 --- a/tox.ini +++ b/tox.ini @@ -20,6 +20,7 @@ setenv = ; RUNNING_UNDER_TOX=1 commands = + pipenv install -d --ignore-pipfile ./runtests.sh [testenv:d20] From b6fc99ca93b72a9458a7c696e74d596d32eff72f Mon Sep 17 00:00:00 2001 From: Domenico DiNicola Date: Wed, 23 Jan 2019 16:01:54 -0500 Subject: [PATCH 02/72] remove t2f models --- src/etools/applications/t2f/admin.py | 21 - .../applications/t2f/helpers/clone_travel.py | 2 +- .../t2f/helpers/cost_summary_calculator.py | 376 ----- .../t2f/migrations/0009_auto_20181227_0815.py | 1 + .../t2f/migrations/0010_auto_20181229_0249.py | 4 +- .../t2f/migrations/0014_auto_20190123_2101.py | 58 + src/etools/applications/t2f/models.py | 138 -- .../applications/t2f/serializers/export.py | 18 +- .../applications/t2f/serializers/mailing.py | 18 +- .../applications/t2f/serializers/travel.py | 65 +- .../applications/t2f/tests/factories.py | 38 - .../t2f/tests/test_cost_summary_calculator.py | 425 ----- .../applications/t2f/tests/test_dashboard.py | 25 +- .../t2f/tests/test_dsa_calculations.py | 1445 ----------------- .../applications/t2f/tests/test_exports.py | 84 - .../applications/t2f/tests/test_mailing.py | 2 - .../t2f/tests/test_overlapping_trips.py | 25 +- .../t2f/tests/test_permission_matrix.py | 8 - .../t2f/tests/test_state_machine.py | 64 +- .../applications/t2f/tests/test_travel.py | 68 - .../t2f/tests/test_travel_details.py | 326 +--- .../t2f/tests/test_travel_list.py | 21 +- 22 files changed, 113 insertions(+), 3119 deletions(-) delete mode 100644 src/etools/applications/t2f/helpers/cost_summary_calculator.py create mode 100644 src/etools/applications/t2f/migrations/0014_auto_20190123_2101.py delete mode 100644 src/etools/applications/t2f/tests/test_cost_summary_calculator.py delete mode 100644 src/etools/applications/t2f/tests/test_dsa_calculations.py delete mode 100644 src/etools/applications/t2f/tests/test_travel.py diff --git a/src/etools/applications/t2f/admin.py b/src/etools/applications/t2f/admin.py index e4c3bb8ea6..0bcc4411e9 100644 --- a/src/etools/applications/t2f/admin.py +++ b/src/etools/applications/t2f/admin.py @@ -76,27 +76,6 @@ class ItineraryItemAdmin(admin.ModelAdmin): ) -@admin.register(models.Expense) -class ExpenseAdmin(AdminListMixin, admin.ModelAdmin): - raw_id_fields = ( - 'travel', - ) - - -@admin.register(models.Deduction) -class DeductionAdmin(AdminListMixin, admin.ModelAdmin): - raw_id_fields = ( - 'travel', - ) - - -@admin.register(models.CostAssignment) -class CostAssignmentAdmin(AdminListMixin, admin.ModelAdmin): - raw_id_fields = ( - 'travel', - ) - - @admin.register(models.TravelAttachment) class TravelAttachmentAdmin(AdminListMixin, admin.ModelAdmin): pass diff --git a/src/etools/applications/t2f/helpers/clone_travel.py b/src/etools/applications/t2f/helpers/clone_travel.py index 635929d096..fab6820c09 100644 --- a/src/etools/applications/t2f/helpers/clone_travel.py +++ b/src/etools/applications/t2f/helpers/clone_travel.py @@ -7,7 +7,7 @@ def __init__(self, travel): self.travel = travel def clone_for_secondary_traveler(self, new_traveler): - fk_related = ['itinerary', 'expenses', 'deductions', 'cost_assignments'] + fk_related = ['itinerary'] o2o_related = [] new_travel = self._do_the_cloning(new_traveler, fk_related, o2o_related) new_travel.activities.set(self.travel.activities.all()) diff --git a/src/etools/applications/t2f/helpers/cost_summary_calculator.py b/src/etools/applications/t2f/helpers/cost_summary_calculator.py deleted file mode 100644 index dd4b29bad8..0000000000 --- a/src/etools/applications/t2f/helpers/cost_summary_calculator.py +++ /dev/null @@ -1,376 +0,0 @@ - -from collections import OrderedDict -from datetime import timedelta -from decimal import Decimal -from itertools import chain - - -class ExpenseDTO(object): - def __init__(self, vendor_number, expense): - self.vendor_number = vendor_number - self.label = expense.type.title - self.currency = expense.currency - self.amount = expense.amount - - -class CostSummaryCalculator(object): - def __init__(self, travel): - self.travel = travel - - def get_cost_summary(self): - expense_mapping = self.get_expenses() - - sorted_expense_mapping_values = chain.from_iterable(expense_mapping.values()) - local_expenses = [e.amount for e in sorted_expense_mapping_values - if e.currency == self.travel.currency] - total_expense_local = sum(local_expenses, Decimal(0)) - total_expense_local = total_expense_local.quantize(Decimal('1.0000')) - - usd_expenses = [e.usd_amount for e in sorted_expense_mapping_values - if e.currency != self.travel.currency and e.usd_amount] - total_expense_usd = sum(usd_expenses, Decimal(0)) - total_expense_usd = total_expense_usd.quantize(Decimal('1.0000')) - - # Order the expenses - expenses = [] - for expense in expense_mapping.pop('user', []): - expenses.append(ExpenseDTO('user', expense)) - - parking_money = expense_mapping.pop('', []) - - # Create data transfer object for each expense - travel_agent_expenses = [] - for key, values in expense_mapping.items(): - travel_agent_expenses.extend(ExpenseDTO(key, e) for e in values) - travel_agent_expenses = sorted(travel_agent_expenses, key=lambda o: o.vendor_number) - expenses.extend(travel_agent_expenses) - - for parking_money_expense in parking_money: - expenses.append(ExpenseDTO('', parking_money_expense)) - - if self.travel.preserved_expenses_local is not None: - expenses_delta_local = self.travel.preserved_expenses_local - total_expense_local - else: - expenses_delta_local = Decimal(0) - - if self.travel.preserved_expenses_usd is not None: - expenses_delta_usd = self.travel.preserved_expenses_usd - total_expense_usd - else: - expenses_delta_usd = Decimal(0) - - dsa_calculator = DSACalculator(self.travel) - dsa_calculator.calculate_dsa() - - expenses_total = OrderedDict() - paid_to_traveler = dsa_calculator.paid_to_traveler.quantize(Decimal('1.0000')) - for expense in expenses: - if expense.currency not in expenses_total: - expenses_total[expense.currency] = Decimal(0) - expenses_total[expense.currency] += expense.amount - - if expense.vendor_number == 'user': - paid_to_traveler += expense.amount - - expenses_total = [{'currency': k, 'amount': v} for k, v in expenses_total.items()] - - result = {'dsa_total': dsa_calculator.total_dsa, - 'dsa': dsa_calculator.detailed_dsa, - 'deductions_total': dsa_calculator.total_deductions.quantize(Decimal('1.0000')), - 'traveler_dsa': dsa_calculator.paid_to_traveler.quantize(Decimal('1.0000')), - 'expenses_total': expenses_total, - 'preserved_expenses': self.travel.preserved_expenses_local, - 'expenses_delta': expenses_delta_local, - 'expenses_delta_local': expenses_delta_local, - 'expenses_delta_usd': expenses_delta_usd, - 'expenses': expenses, - 'paid_to_traveler': paid_to_traveler} - return result - - def get_expenses(self): - expenses_mapping = OrderedDict() - expenses_qs = self.travel.expenses.exclude(amount=None).select_related('type').order_by('id') - - for expense in expenses_qs: - if expense.type.vendor_number not in expenses_mapping: - expenses_mapping[expense.type.vendor_number] = [] - - expenses_mapping[expense.type.vendor_number].append(expense) - return expenses_mapping - - -class DSAdto(object): - def __init__(self, d, itinerary_item): - self.date = d - self.set_itinerary_item(itinerary_item) - - self.dsa_amount = Decimal(0) - self.deduction_multiplier = Decimal(0) - self.last_day = False - - def set_itinerary_item(self, itinerary_item): - self.itinerary_item = itinerary_item - self.region = itinerary_item.dsa_region - - def __repr__(self): - return 'Date: {} | Region: {} | DSA amount: {:.2f} | Deduction: {:.2f} => Final: {:.2f}'.format( - self.date, - self.region, - self.dsa_amount, - self.deduction, - self.final_amount - ) - - @property - def corrected_dsa_amount(self): - if self.last_day: - return self.dsa_amount - (self.dsa_amount * DSACalculator.LAST_DAY_DEDUCTION) - return self.dsa_amount - - @property - def final_amount(self): - final_amount = self.dsa_amount - self._internal_deduction - final_amount -= self.deduction - return max(final_amount, Decimal(0)) - - @property - def deduction(self): - multiplier = self.deduction_multiplier - if self.last_day: - multiplier = min(multiplier, Decimal(1) - DSACalculator.LAST_DAY_DEDUCTION) - return self.dsa_amount * multiplier - - @property - def _internal_deduction(self): - if self.last_day: - return self.dsa_amount * DSACalculator.LAST_DAY_DEDUCTION - return Decimal(0) - - -class DSACalculator(object): - LAST_DAY_DEDUCTION = Decimal('0.6') - SAME_DAY_TRAVEL_MULTIPLIER = Decimal('0.4') - USD_CODE = 'USD' - - def __init__(self, travel): - self.travel = travel - self.total_dsa = None - self.total_deductions = None - self.paid_to_traveler = None - self.detailed_dsa = None - - def _cast_datetime(self, dt): - """ - :type dt: datetime.datetime - :rtype: datetime.datetime - """ - return dt - - def _cast_date(self, d): - """ - :type d: datetime.date - :rtype: datetime.date - """ - return d - - def calculate_dsa(self): - # If TA not required, dsa amounts should be zero - dsa_should_be_zero = False - if not self.travel.ta_required: - dsa_should_be_zero = True - - if self.travel.itinerary.filter(dsa_region=None).exists(): - dsa_should_be_zero = True - - if dsa_should_be_zero: - self.total_dsa = Decimal(0) - self.total_deductions = Decimal(0) - self.paid_to_traveler = Decimal(0) - self.detailed_dsa = [] - return - - dsa_dto_list = self.get_by_day_grouping() - dsa_dto_list = self.check_one_day_long_trip(dsa_dto_list) - dsa_dto_list = self.calculate_daily_dsa_rate(dsa_dto_list) - dsa_dto_list = self.calculate_daily_deduction(dsa_dto_list) - dsa_dto_list = self.check_last_day(dsa_dto_list) - - self.total_dsa = Decimal(0) - self.total_deductions = Decimal(0) - self.paid_to_traveler = Decimal(0) - for dto in dsa_dto_list: - self.paid_to_traveler += dto.final_amount - self.total_dsa += dto.corrected_dsa_amount - self.total_deductions += dto.deduction - self.detailed_dsa = self.aggregate_detailed_dsa(dsa_dto_list) - - def get_dsa_amount(self, dsa_region, over_60_days): - dsa_rate = dsa_region.get_rate_at(self.travel.submitted_at) - - if self.travel.currency: - currency = 'usd' if self.travel.currency.code == self.USD_CODE else 'local' - else: - currency = 'local' - over_60 = '60plus_' if over_60_days else '' - field_name = 'dsa_amount_{over_60}{currency}'.format(over_60=over_60, currency=currency) - return getattr(dsa_rate, field_name) - - def get_by_day_grouping(self): - """ - Returns a mapping where the key represents the day, the value represents the DSA Region applied - """ - mapping = {} - - itinerary_item_list = list(self.travel.itinerary.order_by('arrival_date')) - - # To few elements, cannot calculate properly - if len(itinerary_item_list) < 2: - return [] - - for itinerary_item in itinerary_item_list[:-1]: - arrival_date = self._cast_datetime(itinerary_item.arrival_date).date() - mapping[arrival_date] = itinerary_item - - start_date = self._cast_datetime(itinerary_item_list[0].arrival_date).date() - end_date = self._cast_datetime(itinerary_item_list[-1].departure_date).date() - - tmp_date = start_date - previous_itinerary = None - while tmp_date <= end_date: - if tmp_date in mapping: - previous_itinerary = mapping[tmp_date] - else: - mapping[tmp_date] = previous_itinerary - tmp_date += timedelta(days=1) - - dsa_dto_list = [] - counter = 1 - # Process these in date order so we know when we pass 60 days - for date in sorted(mapping.keys()): - itinerary = mapping[date] - dto = DSAdto(date, itinerary) - over_60 = counter > 60 - dto.daily_rate = self.get_dsa_amount(dto.region, over_60) - dsa_dto_list.append(dto) - counter += 1 - - return dsa_dto_list - - def check_one_day_long_trip(self, dsa_dto_list): - # If it's a day long trip and only one less than 8 hour travel was made, no dsa applied - if len(dsa_dto_list) == 1: - same_day_travels = list(self.travel.itinerary.all()) - for i, sdt in enumerate(same_day_travels[:-1], start=1): - # If it was less than 8 hours long, skip it - arrival = sdt.arrival_date - departure = same_day_travels[i].departure_date - if (departure - arrival) >= timedelta(hours=8): - break - else: - # No longer than 8 hours travel found, no dsa should be applied - return [] - - return dsa_dto_list - - def calculate_daily_dsa_rate(self, dsa_dto_list): - day_counter = 1 - - for dto in dsa_dto_list: - departure_date = self._cast_datetime(dto.itinerary_item.departure_date).date() - if departure_date != dto.date or not dto.itinerary_item.overnight_travel: - over_60 = day_counter > 60 - dto.dsa_amount += self.get_dsa_amount(dto.region, over_60) - - # Last day does not add same day travel - if dto != dsa_dto_list[-1]: - self.add_same_day_travels(dto, day_counter) - - day_counter += 1 - - return dsa_dto_list - - def add_same_day_travels(self, dto, day_counter): - # Check if there were any extra travel on that day - same_day_travels = self.travel.itinerary.exclude(dsa_region=dto.region) - same_day_travels = same_day_travels.filter(departure_date__year=dto.date.year, - departure_date__month=dto.date.month, - departure_date__day=dto.date.day) - - over_60 = day_counter > 60 - same_day_travels = list(same_day_travels) - same_day_travels.append(dto.itinerary_item) - for i, sdt in enumerate(same_day_travels[:-1], start=1): - # If it was less than 8 hours long, skip it - arrival = sdt.arrival_date - departure = same_day_travels[i].departure_date - if (departure - arrival) < timedelta(hours=8): - continue - - same_day_dsa = self.get_dsa_amount(sdt.dsa_region, over_60) - dto.dsa_amount += same_day_dsa * self.SAME_DAY_TRAVEL_MULTIPLIER - - def calculate_daily_deduction(self, dsa_dto_list): - if not dsa_dto_list: - return dsa_dto_list - - deduction_mapping = {d.date: d.multiplier for d in self.travel.deductions.all()} - - for dto in dsa_dto_list: - dto.deduction_multiplier = deduction_mapping.get(dto.date, Decimal(0)) - - return dsa_dto_list - - def check_last_day(self, dsa_dto_list): - if not dsa_dto_list: - return dsa_dto_list - - last_dto = dsa_dto_list[-1] - last_day_departure_count = self.travel.itinerary.filter(departure_date__year=last_dto.date.year, - departure_date__month=last_dto.date.month, - departure_date__day=last_dto.date.day).count() - - itinerary = self.travel.itinerary.order_by('-departure_date') - if last_day_departure_count and itinerary.count() > last_day_departure_count: - first_departure = itinerary[last_day_departure_count] - - last_dto.set_itinerary_item(first_departure) - over_60 = len(dsa_dto_list) > 60 - dsa_amount = self.get_dsa_amount(last_dto.region, over_60) - last_dto.dsa_amount = dsa_amount - - last_dto.last_day = True - return dsa_dto_list - - def aggregate_detailed_dsa(self, dsa_dto_list): - detailed_dsa = [] - previous_region = None - current_data = None - - for day_index, dto in enumerate(dsa_dto_list): - if previous_region != dto.region or day_index == 60: - # If there is data, put to the result list - if current_data: - detailed_dsa.append(current_data) - - # Create new data holder - over_60 = day_index >= 60 # bigger or equal (not just bigger) because index starts from zero - current_data = {'start_date': dto.date, - 'end_date': dto.date, - 'dsa_region': dto.region.id, - 'dsa_region_name': dto.region.label, - 'night_count': -1, # -1 because nights are always days-1 - 'daily_rate': self.get_dsa_amount(dto.region, over_60), - 'paid_to_traveler': Decimal(0), - 'total_amount': Decimal(0), - 'deduction': Decimal(0)} - previous_region = dto.region - - current_data['end_date'] = dto.date - current_data['night_count'] += 1 - current_data['paid_to_traveler'] += dto.final_amount - current_data['total_amount'] += dto.corrected_dsa_amount - current_data['deduction'] += dto.deduction - - if current_data: - detailed_dsa.append(current_data) - - return detailed_dsa diff --git a/src/etools/applications/t2f/migrations/0009_auto_20181227_0815.py b/src/etools/applications/t2f/migrations/0009_auto_20181227_0815.py index 1a4b36a5a9..e6ae2cffe3 100644 --- a/src/etools/applications/t2f/migrations/0009_auto_20181227_0815.py +++ b/src/etools/applications/t2f/migrations/0009_auto_20181227_0815.py @@ -1,6 +1,7 @@ # Generated by Django 2.0.9 on 2018-12-27 08:15 from django.db import migrations + import django_fsm diff --git a/src/etools/applications/t2f/migrations/0010_auto_20181229_0249.py b/src/etools/applications/t2f/migrations/0010_auto_20181229_0249.py index 110938ddb8..b57f332b75 100644 --- a/src/etools/applications/t2f/migrations/0010_auto_20181229_0249.py +++ b/src/etools/applications/t2f/migrations/0010_auto_20181229_0249.py @@ -1,9 +1,9 @@ # Generated by Django 2.0.9 on 2018-12-29 02:49 -from django.conf import settings import django.contrib.postgres.fields -from django.db import migrations, models import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models class Migration(migrations.Migration): diff --git a/src/etools/applications/t2f/migrations/0014_auto_20190123_2101.py b/src/etools/applications/t2f/migrations/0014_auto_20190123_2101.py new file mode 100644 index 0000000000..a8a7b477cd --- /dev/null +++ b/src/etools/applications/t2f/migrations/0014_auto_20190123_2101.py @@ -0,0 +1,58 @@ +# Generated by Django 2.0.9 on 2019-01-23 21:01 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('t2f', '0013_auto_20190103_1345'), + ] + + operations = [ + migrations.RemoveField( + model_name='costassignment', + name='business_area', + ), + migrations.RemoveField( + model_name='costassignment', + name='fund', + ), + migrations.RemoveField( + model_name='costassignment', + name='grant', + ), + migrations.RemoveField( + model_name='costassignment', + name='travel', + ), + migrations.RemoveField( + model_name='costassignment', + name='wbs', + ), + migrations.RemoveField( + model_name='deduction', + name='travel', + ), + migrations.RemoveField( + model_name='expense', + name='currency', + ), + migrations.RemoveField( + model_name='expense', + name='travel', + ), + migrations.RemoveField( + model_name='expense', + name='type', + ), + migrations.DeleteModel( + name='CostAssignment', + ), + migrations.DeleteModel( + name='Deduction', + ), + migrations.DeleteModel( + name='Expense', + ), + ] diff --git a/src/etools/applications/t2f/models.py b/src/etools/applications/t2f/models.py index eef4ca5920..5c30410cba 100644 --- a/src/etools/applications/t2f/models.py +++ b/src/etools/applications/t2f/models.py @@ -1,6 +1,5 @@ import logging from decimal import Decimal -from functools import wraps from django.conf import settings from django.contrib.postgres.fields.array import ArrayField @@ -16,8 +15,6 @@ from unicef_notification.utils import send_notification from etools.applications.action_points.models import ActionPoint -from etools.applications.publics.models import TravelExpenseType -from etools.applications.t2f.helpers.cost_summary_calculator import CostSummaryCalculator from etools.applications.t2f.serializers.mailing import TravelMailSerializer from etools.applications.users.models import WorkspaceCounter from etools.applications.utils.common.urlresolvers import build_frontend_url @@ -73,19 +70,6 @@ def make_travel_reference_number(): return '{}/{}'.format(year, numeric_part) -def mark_as_certified_or_completed_threshold_decorator(func): - @wraps(func) - def wrapper(self, *args, **kwargs): - # If invoicing is enabled, do the threshold check, otherwise it will result an infinite process loop - if self.check_threshold(): - self.submit_certificate(*args, **kwargs) - return - - func(self, *args, **kwargs) - - return wrapper - - class Travel(models.Model): PLANNED = 'planned' SUBMITTED = 'submitted' @@ -187,41 +171,6 @@ class Travel(models.Model): def __str__(self): return self.reference_number - @property - def cost_summary(self): - calculator = CostSummaryCalculator(self) - return calculator.get_cost_summary() - - def check_threshold(self): - expenses = {'user': Decimal(0), - 'travel_agent': Decimal(0)} - - for expense in self.expenses.all(): - if expense.type.vendor_number == TravelExpenseType.USER_VENDOR_NUMBER_PLACEHOLDER: - expenses['user'] += expense.amount - else: - expenses['travel_agent'] += expense.amount - - traveler_delta = 0 - travel_agent_delta = 0 - if self.approved_cost_traveler: - traveler_delta = expenses['user'] - self.approved_cost_traveler - if self.currency.code != 'USD': - exchange_rate = self.currency.exchange_rates.all().last() - traveler_delta *= exchange_rate.x_rate - - if self.approved_cost_travel_agencies: - travel_agent_delta = expenses['travel_agent'] - self.approved_cost_travel_agencies - - workspace = self.traveler.profile.country - if workspace.threshold_tre_usd and traveler_delta > workspace.threshold_tre_usd: - return True - - if workspace.threshold_tae_usd and travel_agent_delta > workspace.threshold_tae_usd: - return True - - return False - # Completion conditions def check_trip_report(self): if not self.report_note: @@ -268,12 +217,6 @@ def approve(self): expenses = {'user': Decimal(0), 'travel_agent': Decimal(0)} - for expense in self.expenses.all(): - if expense.type.vendor_number == TravelExpenseType.USER_VENDOR_NUMBER_PLACEHOLDER: - expenses['user'] += expense.amount - elif expense.type.vendor_number: - expenses['travel_agent'] += expense.amount - self.approved_cost_traveler = expenses['user'] self.approved_cost_travel_agencies = expenses['travel_agent'] @@ -301,7 +244,6 @@ def cancel(self): def plan(self): pass - @mark_as_certified_or_completed_threshold_decorator @transition(status, source=[SUBMITTED, APPROVED, PLANNED, CANCELLED], target=COMPLETED, conditions=[check_trip_report, check_state_flow]) def mark_as_completed(self): @@ -441,86 +383,6 @@ def __str__(self): return '{} {} - {}'.format(self.travel.reference_number, self.origin, self.destination) -class Expense(models.Model): - travel = models.ForeignKey( - 'Travel', related_name='expenses', verbose_name=_('Travel'), - on_delete=models.CASCADE, - ) - type = models.ForeignKey( - 'publics.TravelExpenseType', related_name='+', null=True, blank=True, - verbose_name=_('Type'), - on_delete=models.CASCADE, - ) - currency = models.ForeignKey( - 'publics.Currency', related_name='+', null=True, blank=True, - verbose_name=_('Currency'), - on_delete=models.CASCADE, - ) - amount = models.DecimalField(max_digits=10, decimal_places=4, null=True, blank=True, verbose_name=_('Amount')) - - @property - def usd_amount(self): - if self.currency is None or self.amount is None: - return None - xchange_rate = self.currency.exchange_rates.last() - return self.amount * xchange_rate.x_rate - - -class Deduction(models.Model): - travel = models.ForeignKey( - 'Travel', related_name='deductions', verbose_name=_('Deduction'), - on_delete=models.CASCADE, - ) - date = models.DateField(verbose_name=_('Date')) - breakfast = models.BooleanField(default=False, verbose_name=_('Breakfast')) - lunch = models.BooleanField(default=False, verbose_name=_('Lunch')) - dinner = models.BooleanField(default=False, verbose_name=_('Dinner')) - accomodation = models.BooleanField(default=False, verbose_name=_('Accomodation')) - no_dsa = models.BooleanField(default=False, verbose_name=_('No DSA')) - - @property - def day_of_the_week(self): - return self.date.strftime('%a') - - @property - def multiplier(self): - multiplier = Decimal(0) - - if self.no_dsa: - multiplier += Decimal(1) - if self.breakfast: - multiplier += Decimal('0.05') - if self.lunch: - multiplier += Decimal('0.1') - if self.dinner: - multiplier += Decimal('0.15') - if self.accomodation: - multiplier += Decimal('0.5') - - # Handle if it goes above 1 - return min(multiplier, Decimal(1)) - - -class CostAssignment(models.Model): - travel = models.ForeignKey( - 'Travel', related_name='cost_assignments', verbose_name=_('Travel'), - on_delete=models.CASCADE, - ) - share = models.PositiveIntegerField(verbose_name=_('Share')) - delegate = models.BooleanField(default=False, verbose_name=_('Delegate')) - business_area = models.ForeignKey( - 'publics.BusinessArea', related_name='+', null=True, blank=True, - verbose_name=_('Business Area'), - on_delete=models.CASCADE, - ) - wbs = models.ForeignKey('publics.WBS', related_name='+', null=True, blank=True, on_delete=models.DO_NOTHING, - verbose_name=_('WBS')) - grant = models.ForeignKey('publics.Grant', related_name='+', null=True, blank=True, on_delete=models.DO_NOTHING, - verbose_name=_('Grant')) - fund = models.ForeignKey('publics.Fund', related_name='+', null=True, blank=True, on_delete=models.DO_NOTHING, - verbose_name=_('Fund')) - - def determine_file_upload_path(instance, filename): # TODO: add business area in there country_name = connection.schema_name or 'Uncategorized' diff --git a/src/etools/applications/t2f/serializers/export.py b/src/etools/applications/t2f/serializers/export.py index b6bba128b0..320e8cc402 100644 --- a/src/etools/applications/t2f/serializers/export.py +++ b/src/etools/applications/t2f/serializers/export.py @@ -99,32 +99,16 @@ class FinanceExportSerializer(serializers.Serializer): mode_of_travel = serializers.SerializerMethodField() international_travel = YesOrNoField() require_ta = YesOrNoField(source='ta_required') - dsa_total = serializers.DecimalField(source='cost_summary.dsa_total', max_digits=20, decimal_places=2, - read_only=True) - expense_total = serializers.SerializerMethodField() - deductions_total = serializers.DecimalField( - source='cost_summary.deductions_total', max_digits=20, decimal_places=2, read_only=True) class Meta: fields = ('reference_number', 'traveler', 'office', 'section', 'status', 'supervisor', 'start_date', - 'end_date', 'purpose_of_travel', 'mode_of_travel', 'international_travel', 'require_ta', 'dsa_total', - 'expense_total', 'deductions_total') + 'end_date', 'purpose_of_travel', 'mode_of_travel', 'international_travel', 'require_ta') def get_mode_of_travel(self, obj): if obj.mode_of_travel: return ', '.join(obj.mode_of_travel) return '' - def get_expense_total(self, obj): - ret = [] - for expense in obj.cost_summary['expenses_total']: - if not expense['currency']: - continue - - ret.append('{amount:.{currency.decimal_places}f} {currency.code}'.format(amount=expense['amount'], - currency=expense['currency'])) - return '+'.join(ret) - class TravelAdminExportSerializer(serializers.Serializer): reference_number = serializers.CharField(source='travel.reference_number', read_only=True) diff --git a/src/etools/applications/t2f/serializers/mailing.py b/src/etools/applications/t2f/serializers/mailing.py index c2f34a96ea..2f4a8a24e5 100644 --- a/src/etools/applications/t2f/serializers/mailing.py +++ b/src/etools/applications/t2f/serializers/mailing.py @@ -1,18 +1,5 @@ - from rest_framework import serializers -from etools.applications.t2f.serializers import CostSummarySerializer - - -class CostAssignmentNameSerializer(serializers.Serializer): - name = serializers.SerializerMethodField() - - class Meta: - fields = ('name',) - - def get_name(self, obj): - return '{} | {} | {}: {}%'.format(obj.wbs.name, obj.grant.name, obj.fund.name, obj.share) - class TravelMailSerializer(serializers.Serializer): estimated_travel_cost = serializers.DecimalField(max_digits=18, decimal_places=2, required=False) @@ -21,14 +8,11 @@ class TravelMailSerializer(serializers.Serializer): start_date = serializers.DateTimeField(format='%m/%d/%Y') end_date = serializers.DateTimeField(format='%m/%d/%Y') currency = serializers.CharField(source='currency.code', read_only=True) - cost_summary = CostSummarySerializer(read_only=True) location = serializers.CharField(source='itinerary.first.destination', read_only=True) - cost_assignments = CostAssignmentNameSerializer(many=True) reference_number = serializers.CharField() purpose = serializers.CharField() rejection_note = serializers.CharField() class Meta: fields = ('traveler', 'supervisor', 'start_date', 'end_date', 'estimated_travel_cost', 'purpose', - 'reference_number', 'currency', 'cost_summary', 'rejection_note', 'location', 'cost_assignments', - 'reference_number', 'purpose', 'rejection_note') + 'reference_number', 'currency', 'rejection_note', 'location', 'rejection_note') diff --git a/src/etools/applications/t2f/serializers/travel.py b/src/etools/applications/t2f/serializers/travel.py index 01a9aab67f..2a035d7cb5 100644 --- a/src/etools/applications/t2f/serializers/travel.py +++ b/src/etools/applications/t2f/serializers/travel.py @@ -18,19 +18,15 @@ from etools.applications.action_points.models import ActionPoint from etools.applications.action_points.serializers import ActionPointBaseSerializer from etools.applications.partners.models import PartnerType -from etools.applications.publics.models import AirlineCompany, Currency +from etools.applications.publics.models import AirlineCompany from etools.applications.t2f.helpers.permission_matrix import PermissionMatrix from etools.applications.t2f.models import ( - CostAssignment, - Deduction, - Expense, ItineraryItem, Travel, TravelActivity, TravelAttachment, TravelType, ) -from etools.applications.t2f.serializers import CostSummarySerializer itineraryItemSortKey = operator.attrgetter('departure_date') @@ -91,33 +87,6 @@ class Meta: 'mode_of_travel', 'airlines') -class ExpenseSerializer(PermissionBasedModelSerializer): - id = serializers.IntegerField(required=False) - amount = serializers.DecimalField(max_digits=18, decimal_places=2, required=False) - document_currency = serializers.PrimaryKeyRelatedField(source='currency', queryset=Currency.objects.all()) - - class Meta: - model = Expense - fields = ('id', 'type', 'currency', 'document_currency', 'amount') - - -class DeductionSerializer(PermissionBasedModelSerializer): - id = serializers.IntegerField(required=False) - day_of_the_week = serializers.CharField(read_only=True) - - class Meta: - model = Deduction - fields = ('id', 'date', 'breakfast', 'lunch', 'dinner', 'accomodation', 'no_dsa', 'day_of_the_week') - - -class CostAssignmentSerializer(PermissionBasedModelSerializer): - id = serializers.IntegerField(required=False) - - class Meta: - model = CostAssignment - fields = ('id', 'wbs', 'share', 'grant', 'fund', 'business_area', 'delegate') - - class TravelActivitySerializer(PermissionBasedModelSerializer): id = serializers.IntegerField(required=False) locations = serializers.PrimaryKeyRelatedField(many=True, queryset=Location.objects.all(), required=False, @@ -178,12 +147,8 @@ def get_url(self, obj): class TravelDetailsSerializer(PermissionBasedModelSerializer): itinerary = ItineraryItemSerializer(many=True, required=False) - expenses = ExpenseSerializer(many=True, required=False) - deductions = DeductionSerializer(many=True, required=False) - cost_assignments = CostAssignmentSerializer(many=True, required=False) activities = TravelActivitySerializer(many=True, required=False) attachments = TravelAttachmentSerializer(many=True, read_only=True, required=False) - cost_summary = CostSummarySerializer(read_only=True) report = serializers.CharField(source='report_note', required=False, default='', allow_blank=True) mode_of_travel = serializers.ListField(child=LowerTitleField(), allow_null=True, required=False) @@ -193,10 +158,10 @@ class TravelDetailsSerializer(PermissionBasedModelSerializer): class Meta: model = Travel fields = ('reference_number', 'supervisor', 'office', 'end_date', 'international_travel', 'section', - 'traveler', 'start_date', 'ta_required', 'purpose', 'id', 'itinerary', 'expenses', 'deductions', - 'cost_assignments', 'status', 'activities', 'mode_of_travel', 'estimated_travel_cost', + 'traveler', 'start_date', 'ta_required', 'purpose', 'id', 'itinerary', + 'status', 'activities', 'mode_of_travel', 'estimated_travel_cost', 'currency', 'completed_at', 'canceled_at', 'rejection_note', 'cancellation_note', 'attachments', - 'cost_summary', 'certification_note', 'report', 'additional_note', 'misc_expenses', + 'certification_note', 'report', 'additional_note', 'misc_expenses', 'first_submission_date') # Review this, as a developer could be confusing why the status field is not saved during an update read_only_fields = ('status', 'reference_number') @@ -210,22 +175,6 @@ def __init__(self, *args, **kwargs): if self.instance and not is_iterable(self.instance): ta_required |= self.instance.ta_required - if not ta_required: - data.pop('deductions', None) - data.pop('expenses', None) - data.pop('cost_assignments', None) - - # -------- Validation method -------- - def validate_cost_assignments(self, value): - # If transition is None, it's a normal save (not an action) and we don't have to validate this - if not value or self.transition_name is None: - return value - - share_sum = sum([ca['share'] for ca in value]) - if share_sum != 100: - raise ValidationError('Shares should add up to 100%') - return value - def validate_itinerary(self, value): if not value: return value @@ -327,9 +276,6 @@ def align_dates_to_itinerary(self, data): # -------- Create and update methods -------- def create(self, validated_data): itinerary = validated_data.pop('itinerary', []) - expenses = validated_data.pop('expenses', []) - deductions = validated_data.pop('deductions', []) - cost_assignments = validated_data.pop('cost_assignments', []) activities = validated_data.pop('activities', []) action_points = validated_data.pop('action_points', []) @@ -339,9 +285,6 @@ def create(self, validated_data): itineraryitems = self.create_related_models(ItineraryItem, itinerary, travel=instance) # ensure itineraryitems are ordered by `departure_date` order_itineraryitems(instance, itineraryitems) - self.create_related_models(Expense, expenses, travel=instance) - self.create_related_models(Deduction, deductions, travel=instance) - self.create_related_models(CostAssignment, cost_assignments, travel=instance) self.create_related_models(ActionPoint, action_points, travel=instance) travel_activities = self.create_related_models(TravelActivity, activities) for activity in travel_activities: diff --git a/src/etools/applications/t2f/tests/factories.py b/src/etools/applications/t2f/tests/factories.py index bc39e8048d..54160ddd91 100644 --- a/src/etools/applications/t2f/tests/factories.py +++ b/src/etools/applications/t2f/tests/factories.py @@ -9,10 +9,6 @@ PublicsAirlineCompanyFactory, PublicsCurrencyFactory, PublicsDSARegionFactory, - PublicsFundFactory, - PublicsGrantFactory, - PublicsTravelExpenseTypeFactory, - PublicsWBSFactory, ) from etools.applications.reports.tests.factories import ResultFactory, SectionFactory from etools.applications.t2f import models @@ -85,37 +81,6 @@ class Meta: model = models.ItineraryItem -class ExpenseFactory(factory.DjangoModelFactory): - currency = factory.SubFactory(PublicsCurrencyFactory) - amount = fuzzy.FuzzyDecimal(1, 10000) - type = factory.SubFactory(PublicsTravelExpenseTypeFactory) - - class Meta: - model = models.Expense - - -class DeductionFactory(factory.DjangoModelFactory): - date = fuzzy.FuzzyDateTime(start_dt=_FUZZY_START_DATE, end_dt=_FUZZY_END_DATE) - breakfast = False - lunch = False - dinner = False - accomodation = False - no_dsa = False - - class Meta: - model = models.Deduction - - -class CostAssignmentFactory(factory.DjangoModelFactory): - wbs = factory.SubFactory(PublicsWBSFactory) - share = fuzzy.FuzzyInteger(1, 100) - grant = factory.SubFactory(PublicsGrantFactory) - fund = factory.SubFactory(PublicsFundFactory) - - class Meta: - model = models.CostAssignment - - class TravelFactory(factory.DjangoModelFactory): traveler = factory.SubFactory(UserFactory) supervisor = factory.SubFactory(UserFactory) @@ -137,9 +102,6 @@ class TravelFactory(factory.DjangoModelFactory): mode_of_travel = [] itinerary = factory.RelatedFactory(ItineraryItemFactory, 'travel') - expenses = factory.RelatedFactory(ExpenseFactory, 'travel') - deductions = factory.RelatedFactory(DeductionFactory, 'travel') - cost_assignments = factory.RelatedFactory(CostAssignmentFactory, 'travel') @factory.post_generation def populate_activities(self, create, extracted, **kwargs): diff --git a/src/etools/applications/t2f/tests/test_cost_summary_calculator.py b/src/etools/applications/t2f/tests/test_cost_summary_calculator.py deleted file mode 100644 index 761215eb1d..0000000000 --- a/src/etools/applications/t2f/tests/test_cost_summary_calculator.py +++ /dev/null @@ -1,425 +0,0 @@ -from datetime import date, datetime -from decimal import Decimal - -from pytz import UTC - -from etools.applications.EquiTrack.tests.cases import BaseTenantTestCase -from etools.applications.publics.models import TravelExpenseType -from etools.applications.publics.tests.factories import ( - PublicsCountryFactory, - PublicsCurrencyFactory, - PublicsDSARateFactory, - PublicsDSARegionFactory, - PublicsTravelExpenseTypeFactory, -) -from etools.applications.t2f.helpers.cost_summary_calculator import CostSummaryCalculator, DSACalculator, ExpenseDTO -from etools.applications.t2f.tests.factories import ExpenseFactory, ItineraryItemFactory, TravelFactory -from etools.applications.users.tests.factories import UserFactory - - -class TestExpenseDTO(BaseTenantTestCase): - def test_init(self): - currency_usd = PublicsCurrencyFactory(code='USD') - travel = TravelFactory(currency=currency_usd) - expense_type = PublicsTravelExpenseTypeFactory( - title='Train cost', - vendor_number=TravelExpenseType.USER_VENDOR_NUMBER_PLACEHOLDER - ) - expense = ExpenseFactory( - travel=travel, - type=expense_type, - currency=currency_usd, - amount=100 - ) - dto = ExpenseDTO("vendor_number", expense) - self.assertEqual(dto.vendor_number, "vendor_number") - self.assertEqual(dto.label, expense_type.title) - self.assertEqual(dto.currency, currency_usd) - self.assertEqual(dto.amount, 100) - - -class CostSummaryTest(BaseTenantTestCase): - @classmethod - def setUpTestData(cls): - cls.unicef_staff = UserFactory(is_staff=True) - - cls.currency_usd = PublicsCurrencyFactory(code='USD') - cls.currency_huf = PublicsCurrencyFactory(name='Hungarian Forint', code='HUF') - - cls.user_et_1 = PublicsTravelExpenseTypeFactory( - title='Train cost', - vendor_number=TravelExpenseType.USER_VENDOR_NUMBER_PLACEHOLDER - ) - cls.user_et_2 = PublicsTravelExpenseTypeFactory( - title='Other expenses', - vendor_number=TravelExpenseType.USER_VENDOR_NUMBER_PLACEHOLDER - ) - cls.ta_et = PublicsTravelExpenseTypeFactory(title='Travel agent') - - netherlands = PublicsCountryFactory(name='Netherlands', long_name='Netherlands') - hungary = PublicsCountryFactory(name='Hungary', long_name='Hungary') - denmark = PublicsCountryFactory(name='Denmark', long_name='Denmark') - germany = PublicsCountryFactory(name='Germany', long_name='Germany') - - cls.amsterdam = PublicsDSARegionFactory( - country=netherlands, - area_name='Amsterdam', - area_code='ds1' - ) - PublicsDSARateFactory( - region=cls.amsterdam, - dsa_amount_usd=100, - dsa_amount_60plus_usd=60 - ) - - cls.budapest = PublicsDSARegionFactory( - country=hungary, - area_name='Budapest', - area_code='ds2' - ) - PublicsDSARateFactory( - region=cls.budapest, - dsa_amount_usd=200, - dsa_amount_60plus_usd=120 - ) - - cls.copenhagen = PublicsDSARegionFactory( - country=denmark, - area_name='Copenhagen', - area_code='ds3' - ) - PublicsDSARateFactory( - region=cls.copenhagen, - dsa_amount_usd=300, - dsa_amount_60plus_usd=180 - ) - - cls.dusseldorf = PublicsDSARegionFactory( - country=germany, - area_name='Duesseldorf', - area_code='ds4' - ) - PublicsDSARateFactory( - region=cls.dusseldorf, - dsa_amount_usd=400, - dsa_amount_60plus_usd=240 - ) - - # Delete default items created by factory - cls.travel = TravelFactory(currency=cls.currency_huf) - cls.travel.itinerary.all().delete() - cls.travel.expenses.all().delete() - cls.travel.deductions.all().delete() - - def test_init(self): - calc = CostSummaryCalculator(self.travel) - self.assertEqual(calc.travel, self.travel) - - def test_get_expenses_empty(self): - """If no expenses then empty dictionary returned""" - calc = CostSummaryCalculator(self.travel) - self.assertEqual(calc.get_expenses(), {}) - - def test_get_expenses_amount_none(self): - """Ignore expenses where amount is None""" - ExpenseFactory( - travel=self.travel, - type=self.user_et_1, - currency=self.currency_usd, - amount=None - ) - calc = CostSummaryCalculator(self.travel) - self.assertEqual(calc.get_expenses(), {}) - - def test_get_expenses(self): - """Check that dictionary with expense type vendor number as key is - returned - """ - expense_1 = ExpenseFactory( - travel=self.travel, - type=self.user_et_1, - currency=self.currency_usd, - amount=100 - ) - expense_2 = ExpenseFactory( - travel=self.travel, - type=self.user_et_1, - currency=self.currency_usd, - amount=150 - ) - expense_3 = ExpenseFactory( - travel=self.travel, - currency=self.currency_usd, - amount=200 - ) - calc = CostSummaryCalculator(self.travel) - self.assertEqual(calc.get_expenses(), { - self.user_et_1.vendor_number: [expense_1, expense_2], - expense_3.type.vendor_number: [expense_3], - }) - - def test_calculations(self): - ItineraryItemFactory(travel=self.travel, - departure_date=datetime(2017, 1, 1, 1, 0, tzinfo=UTC), - arrival_date=datetime(2017, 1, 1, 2, 0, tzinfo=UTC), - dsa_region=self.budapest) - - ItineraryItemFactory(travel=self.travel, - departure_date=datetime(2017, 1, 1, 10, 0, tzinfo=UTC), - arrival_date=datetime(2017, 1, 1, 11, 0, tzinfo=UTC), - dsa_region=self.copenhagen) - - ItineraryItemFactory(travel=self.travel, - departure_date=datetime(2017, 1, 1, 22, 0, tzinfo=UTC), - arrival_date=datetime(2017, 1, 1, 23, 0, tzinfo=UTC), - dsa_region=self.dusseldorf) - - ItineraryItemFactory(travel=self.travel, - departure_date=datetime(2017, 1, 3, 10, 0, tzinfo=UTC), - arrival_date=datetime(2017, 1, 3, 13, 0, tzinfo=UTC), - dsa_region=self.amsterdam) - - ExpenseFactory(travel=self.travel, - type=self.user_et_1, - currency=self.currency_huf, - amount=100) - ExpenseFactory(travel=self.travel, - type=self.user_et_2, - currency=self.currency_huf, - amount=200) - - calculator = CostSummaryCalculator(self.travel) - cost_summary = calculator.get_cost_summary() - cost_summary.pop('expenses') - - self.assertEqual(cost_summary, - {'deductions_total': Decimal('0.0000'), - 'dsa': [{'daily_rate': Decimal('200.0000'), - 'deduction': Decimal('0.00000'), - 'dsa_region': self.dusseldorf.id, - 'dsa_region_name': 'Germany - Duesseldorf', - 'end_date': date(2017, 1, 3), - 'night_count': 2, - 'paid_to_traveler': Decimal('640.00000'), - 'start_date': date(2017, 1, 1), - 'total_amount': Decimal('640.00000')}], - 'dsa_total': Decimal('640.00000'), - 'expenses_delta': Decimal('0'), - 'expenses_delta_local': Decimal('0'), - 'expenses_delta_usd': Decimal('0'), - 'expenses_total': [{'amount': Decimal('300.0000'), - 'currency': self.currency_huf}], - 'paid_to_traveler': Decimal('940.0000'), - 'preserved_expenses': None, - 'traveler_dsa': Decimal('640.0000')}) - - def test_cost_summary_calculator(self): - ExpenseFactory(travel=self.travel, - currency=self.currency_huf, - amount=None) - ExpenseFactory(travel=self.travel, - currency=None, - amount=600) - ExpenseFactory(travel=self.travel, - currency=self.currency_usd, - amount=50) - - calculator = CostSummaryCalculator(self.travel) - # Should not raise TypeError - calculator.get_cost_summary() - - # TODO: confirm that this is correct - # should a single itinerary result in a zero dsa total? - def test_cost_calculation_single(self): - """Check calculations for a single itinerary""" - ItineraryItemFactory( - travel=self.travel, - arrival_date=datetime(2017, 1, 1, 1, 0, tzinfo=UTC), - departure_date=datetime(2017, 1, 2, 4, 0, tzinfo=UTC), - dsa_region=self.budapest - ) - - calculator = CostSummaryCalculator(self.travel) - cost_summary = calculator.get_cost_summary() - - self.assertEqual(cost_summary["dsa"], []) - self.assertEqual(cost_summary["expenses_total"], []) - self.assertEqual(cost_summary["preserved_expenses"], Decimal("150")) - self.assertEqual(cost_summary["dsa_total"], Decimal("0")) - self.assertEqual(cost_summary["paid_to_traveler"], Decimal("0")) - self.assertEqual(cost_summary["traveler_dsa"], Decimal("0")) - - def test_cost_calculation_60plus(self): - """Check calculations for itinerary over 60 days - takes into account 60 plus adjustment - """ - itinerary = ItineraryItemFactory( - travel=self.travel, - arrival_date=datetime(2017, 1, 1, 1, 0, tzinfo=UTC), - departure_date=datetime(2017, 1, 2, 4, 0, tzinfo=UTC), - dsa_region=self.budapest - ) - - ItineraryItemFactory( - travel=self.travel, - arrival_date=datetime(2017, 1, 3, 1, 0, tzinfo=UTC), - departure_date=datetime(2017, 5, 4, 4, 0, tzinfo=UTC), - dsa_region=self.budapest - ) - - daily_amt = self.budapest.dsa_amount_local - daily_60_amt = self.budapest.dsa_amount_60plus_local - last_day_amount = daily_60_amt * (1 - DSACalculator.LAST_DAY_DEDUCTION) - first_portion = daily_amt * 60 - second_portion = daily_60_amt * 63 + last_day_amount - - calculator = CostSummaryCalculator(self.travel) - cost_summary = calculator.get_cost_summary() - self.assertEqual(cost_summary["dsa"], [{ - "start_date": date(2017, 1, 1), - "end_date": date(2017, 3, 1), - "dsa_region": itinerary.dsa_region.pk, - "dsa_region_name": itinerary.dsa_region.label, - "night_count": 59, - "daily_rate": daily_amt, - "paid_to_traveler": first_portion, - "total_amount": first_portion, - "deduction": Decimal("0.0000"), - }, { - "start_date": date(2017, 3, 2), - "end_date": date(2017, 5, 4), - "dsa_region": itinerary.dsa_region.pk, - "dsa_region_name": itinerary.dsa_region.label, - "night_count": 63, - "daily_rate": daily_60_amt, - "paid_to_traveler": second_portion, - "total_amount": second_portion, - "deduction": Decimal(0), - }]) - self.assertEqual(cost_summary["expenses_total"], []) - self.assertEqual(cost_summary["preserved_expenses"], None) - self.assertEqual( - cost_summary["dsa_total"], - first_portion + second_portion - ) - self.assertEqual( - cost_summary["paid_to_traveler"], - first_portion + second_portion - ) - self.assertEqual( - cost_summary["traveler_dsa"], - first_portion + second_portion - ) - - def test_cost_calculation_parking_money(self): - """If expense mapping has empty key, allocate to parking money""" - itinerary = ItineraryItemFactory( - travel=self.travel, - arrival_date=datetime(2017, 1, 1, 1, 0, tzinfo=UTC), - departure_date=datetime(2017, 1, 2, 4, 0, tzinfo=UTC), - dsa_region=self.budapest - ) - - ItineraryItemFactory( - travel=self.travel, - arrival_date=datetime(2017, 1, 3, 1, 0, tzinfo=UTC), - departure_date=datetime(2017, 1, 4, 4, 0, tzinfo=UTC), - dsa_region=self.budapest - ) - parking_money_type = PublicsTravelExpenseTypeFactory( - title='Parking money', - vendor_number="", - ) - ExpenseFactory( - travel=self.travel, - type=parking_money_type, - currency=self.currency_usd, - amount=100 - ) - - daily_amt = self.budapest.dsa_amount_local - last_day_amount = daily_amt * (1 - DSACalculator.LAST_DAY_DEDUCTION) - total = daily_amt * 3 + last_day_amount - - calculator = CostSummaryCalculator(self.travel) - cost_summary = calculator.get_cost_summary() - self.assertEqual(cost_summary["dsa"], [{ - "start_date": date(2017, 1, 1), - "end_date": date(2017, 1, 4), - "dsa_region": itinerary.dsa_region.pk, - "dsa_region_name": itinerary.dsa_region.label, - "night_count": 3, - "daily_rate": daily_amt, - "paid_to_traveler": total, - "total_amount": total, - "deduction": Decimal("0.0000"), - }]) - self.assertEqual(cost_summary["expenses_total"], [ - {"currency": self.currency_usd, "amount": Decimal("100.0")} - ]) - self.assertEqual(cost_summary["preserved_expenses"], Decimal("150")) - self.assertEqual(cost_summary["dsa_total"], total) - self.assertEqual(cost_summary["paid_to_traveler"], total) - self.assertEqual(cost_summary["traveler_dsa"], total) - - def test_cost_calculation_expense_delta(self): - """If preserved expense set, ensure correct delta updated""" - self.travel.preserved_expenses_local = Decimal("150") - self.travel.preserved_expenses_usd = Decimal("275") - itinerary = ItineraryItemFactory( - travel=self.travel, - arrival_date=datetime(2017, 1, 1, 1, 0, tzinfo=UTC), - departure_date=datetime(2017, 1, 2, 4, 0, tzinfo=UTC), - dsa_region=self.budapest - ) - - ItineraryItemFactory( - travel=self.travel, - arrival_date=datetime(2017, 1, 3, 1, 0, tzinfo=UTC), - departure_date=datetime(2017, 1, 4, 4, 0, tzinfo=UTC), - dsa_region=self.budapest - ) - expense_huf = ExpenseFactory( - travel=self.travel, - type=self.user_et_1, - currency=self.currency_huf, - amount=100 - ) - expense_usd = ExpenseFactory( - travel=self.travel, - type=self.user_et_2, - currency=self.currency_usd, - amount=200 - ) - - daily_amt = self.budapest.dsa_amount_local - last_day_amount = daily_amt * (1 - DSACalculator.LAST_DAY_DEDUCTION) - total = daily_amt * 3 + last_day_amount - expense_total = expense_huf.amount + expense_usd.amount - - calculator = CostSummaryCalculator(self.travel) - cost_summary = calculator.get_cost_summary() - self.assertEqual(cost_summary["dsa"], [{ - "start_date": date(2017, 1, 1), - "end_date": date(2017, 1, 4), - "dsa_region": itinerary.dsa_region.pk, - "dsa_region_name": itinerary.dsa_region.label, - "night_count": 3, - "daily_rate": daily_amt, - "paid_to_traveler": total, - "total_amount": total, - "deduction": Decimal("0.0000"), - }]) - self.assertCountEqual(cost_summary["expenses_total"], [ - {"currency": self.currency_huf, "amount": expense_huf.amount}, - {"currency": self.currency_usd, "amount": expense_usd.amount}, - ]) - self.assertEqual(cost_summary["preserved_expenses"], Decimal("150")) - self.assertEqual(cost_summary["expenses_delta_local"], Decimal("50")) - self.assertEqual(cost_summary["expenses_delta_usd"], Decimal("275")) - self.assertEqual(cost_summary["dsa_total"], total) - self.assertEqual( - cost_summary["paid_to_traveler"], - total + expense_total - ) - self.assertEqual(cost_summary["traveler_dsa"], total) diff --git a/src/etools/applications/t2f/tests/test_dashboard.py b/src/etools/applications/t2f/tests/test_dashboard.py index d4805c4238..78ce670796 100644 --- a/src/etools/applications/t2f/tests/test_dashboard.py +++ b/src/etools/applications/t2f/tests/test_dashboard.py @@ -6,11 +6,8 @@ from etools.applications.EquiTrack.tests.cases import BaseTenantTestCase from etools.applications.partners.models import PartnerOrganization from etools.applications.publics.tests.factories import ( - PublicsBusinessAreaFactory, PublicsCurrencyFactory, PublicsDSARegionFactory, - PublicsTravelExpenseTypeFactory, - PublicsWBSFactory, ) from etools.applications.t2f.models import make_travel_reference_number, ModeOfTravel, Travel, TravelType from etools.applications.t2f.tests.factories import TravelActivityFactory, TravelFactory @@ -67,13 +64,8 @@ def test_list_view(self): def test_completed_counts(self): currency = PublicsCurrencyFactory() - expense_type = PublicsTravelExpenseTypeFactory() - business_area = PublicsBusinessAreaFactory() dsa_region = PublicsDSARegionFactory() - wbs = PublicsWBSFactory(business_area=business_area) - grant = wbs.grants.first() - fund = grant.funds.first() traveler = UserFactory(is_staff=True) traveler.profile.vendor_number = 'usrvend' traveler.profile.save() @@ -82,16 +74,7 @@ def test_completed_counts(self): traveler=traveler, status=Travel.APPROVED, supervisor=self.unicef_staff) - data = {'cost_assignments': [{'wbs': wbs.id, - 'grant': grant.id, - 'fund': fund.id, - 'share': 100}], - 'deductions': [{'date': '2016-11-03', - 'breakfast': True, - 'lunch': True, - 'dinner': False, - 'accomodation': True}], - 'itinerary': [{'origin': 'Berlin', + data = {'itinerary': [{'origin': 'Berlin', 'destination': 'Budapest', 'departure_date': '2017-04-14T17:06:55.821490', 'arrival_date': '2017-04-15T17:06:55.821490', @@ -111,11 +94,7 @@ def test_completed_counts(self): 'ta_required': True, 'report': 'Some report', 'currency': currency.id, - 'supervisor': self.unicef_staff.id, - 'expenses': [{'amount': '120', - 'type': expense_type.id, - 'currency': currency.id, - 'document_currency': currency.id}]} + 'supervisor': self.unicef_staff.id} act1 = TravelActivityFactory(travel_type=TravelType.PROGRAMME_MONITORING, primary_traveler=traveler) act2 = TravelActivityFactory(travel_type=TravelType.SPOT_CHECK, primary_traveler=traveler) act1.travels.add(travel) diff --git a/src/etools/applications/t2f/tests/test_dsa_calculations.py b/src/etools/applications/t2f/tests/test_dsa_calculations.py deleted file mode 100644 index 19bddb65a6..0000000000 --- a/src/etools/applications/t2f/tests/test_dsa_calculations.py +++ /dev/null @@ -1,1445 +0,0 @@ -from datetime import date, datetime, timedelta -from decimal import Decimal -from unittest import skip - -from pytz import UTC - -from etools.applications.EquiTrack.tests.cases import BaseTenantTestCase -from etools.applications.publics.tests.factories import ( - PublicsCountryFactory, - PublicsCurrencyFactory, - PublicsDSARateFactory, - PublicsDSARegionFactory, -) -from etools.applications.t2f.helpers.cost_summary_calculator import DSACalculator, DSAdto -from etools.applications.t2f.tests.factories import DeductionFactory, ItineraryItemFactory, TravelFactory -from etools.applications.users.tests.factories import UserFactory - - -class TestDASdto(BaseTenantTestCase): - @classmethod - def setUpTestData(cls): - netherlands = PublicsCountryFactory( - name='Netherlands', - long_name='Netherlands' - ) - cls.amsterdam = PublicsDSARegionFactory( - country=netherlands, - area_name='Amsterdam', - area_code='ds1' - ) - - def setUp(self): - super().setUp() - self.travel = TravelFactory() - self.itinerary_item = ItineraryItemFactory( - travel=self.travel, - dsa_region=self.amsterdam, - ) - self.dsa = DSAdto(date.today(), self.itinerary_item) - self.dsa.dsa_amount = Decimal(100.0) - - def test_init(self): - today = date.today() - dsa = DSAdto(today, self.itinerary_item) - self.assertEqual(dsa.date, today) - self.assertEqual(dsa.itinerary_item, self.itinerary_item) - self.assertEqual(dsa.region, self.amsterdam) - self.assertEqual(dsa.dsa_amount, 0) - self.assertEqual(dsa.deduction_multiplier, 0) - self.assertFalse(dsa.last_day) - - def test_corrected_dsa_amount(self): - """If NOT last day, then no change to dsa_amount""" - self.assertFalse(self.dsa.last_day) - self.assertEqual(self.dsa.corrected_dsa_amount, self.dsa.dsa_amount) - - def test_corrected_dsa_amount_last_day(self): - """If last day, then dsa amount is corrected""" - self.dsa.last_day = True - self.assertEqual(self.dsa.corrected_dsa_amount, 40.00) - - def test_internal_deduction(self): - """If NOT last day, then internal deduction is zero""" - self.assertFalse(self.dsa.last_day) - self.assertEqual(self.dsa._internal_deduction, 0) - - def test_internal_deduction_last_day(self): - """If last day, then internal deduction is calculated""" - self.dsa.last_day = True - self.assertEqual(self.dsa._internal_deduction, 60.00) - - def test_deduction(self): - """If NOT last day, then use deduction multiplier only""" - self.assertFalse(self.dsa.last_day) - self.dsa.deduction_multiplier = 2 - self.assertEqual(self.dsa.deduction, 200.0) - - def test_deduction_last_day_use_deduction(self): - """If last day, use the lesser of multiplier and last day deduction""" - self.dsa.last_day = True - self.dsa.deduction_multiplier = 0.5 - self.assertEqual(self.dsa.deduction, 40.0) - - def test_deduction_last_day_use_multiplier(self): - """If last day, use the lesser of multiplier and last day deduction""" - self.dsa.last_day = True - self.dsa.deduction_multiplier = Decimal(0.3) - self.assertEqual("{:.2f}".format(self.dsa.deduction), "30.00") - - def test_final_amount(self): - """If NOT last day, then just deduction should be substracted""" - self.assertFalse(self.dsa.last_day) - self.dsa.deduction_multiplier = Decimal(0.2) - self.assertEqual("{:.2f}".format(self.dsa.final_amount), "80.00") - - def test_final_amount_last_day(self): - """If last day, then both deduction and internal deduction - should be subtracted - """ - self.dsa.last_day = True - self.dsa.deduction_multiplier = Decimal(0.2) - self.assertEqual("{:.2f}".format(self.dsa.final_amount), "20.00") - - def test_str(self): - self.assertFalse(self.dsa.last_day) - self.dsa.deduction_multiplier = Decimal(0.2) - res = "Date: {} | Region: {} | DSA amount: 100.00 | Deduction: 20.00 => Final: 80.00".format( - date.today(), - self.amsterdam, - ) - self.assertEqual(str(self.dsa), res) - - -class TestDSACalculator(BaseTenantTestCase): - @classmethod - def setUpTestData(cls): - cls.unicef_staff = UserFactory(is_staff=True) - - netherlands = PublicsCountryFactory(name='Netherlands', long_name='Netherlands') - hungary = PublicsCountryFactory(name='Hungary', long_name='Hungary') - denmark = PublicsCountryFactory(name='Denmark', long_name='Denmark') - germany = PublicsCountryFactory(name='Germany', long_name='Germany') - - # For Amsterdam daylight saving occurred on March 26 (2am) in 2017 - cls.amsterdam = PublicsDSARegionFactory(country=netherlands, - area_name='Amsterdam', - area_code='ds1') - cls.amsterdam_rate = PublicsDSARateFactory(region=cls.amsterdam, - dsa_amount_usd=100, - dsa_amount_60plus_usd=60) - - cls.budapest = PublicsDSARegionFactory(country=hungary, - area_name='Budapest', - area_code='ds2') - PublicsDSARateFactory(region=cls.budapest, - dsa_amount_usd=200, - dsa_amount_60plus_usd=120) - - cls.copenhagen = PublicsDSARegionFactory(country=denmark, - area_name='Copenhagen', - area_code='ds3') - PublicsDSARateFactory(region=cls.copenhagen, - dsa_amount_usd=300, - dsa_amount_60plus_usd=180) - - cls.dusseldorf = PublicsDSARegionFactory(country=germany, - area_name='Duesseldorf', - area_code='ds4') - PublicsDSARateFactory(region=cls.dusseldorf, - dsa_amount_usd=400, - dsa_amount_60plus_usd=240) - - cls.essen = PublicsDSARegionFactory(country=germany, - area_name='Essen', - area_code='ds5') - PublicsDSARateFactory(region=cls.essen, - dsa_amount_usd=500, - dsa_amount_60plus_usd=300) - - cls.frankfurt = PublicsDSARegionFactory(country=germany, - area_name='Frankfurt', - area_code='ds6') - PublicsDSARateFactory(region=cls.frankfurt, - dsa_amount_usd=600, - dsa_amount_60plus_usd=360) - - def setUp(self): - super().setUp() - currency = PublicsCurrencyFactory(code="USD") - self.travel = TravelFactory(currency=currency) - - # Delete default items created by factory - self.travel.itinerary.all().delete() - self.travel.expenses.all().delete() - self.travel.deductions.all().delete() - - def test_init(self): - dsa = DSACalculator(self.travel) - self.assertEqual(dsa.travel, self.travel) - self.assertIsNone(dsa.total_dsa) - self.assertIsNone(dsa.total_deductions) - self.assertIsNone(dsa.paid_to_traveler) - self.assertIsNone(dsa.detailed_dsa) - - def test_cast_datetime(self): - """Nothing happens _cast_datetime""" - dsa = DSACalculator(self.travel) - today = date.today() - self.assertEqual(dsa._cast_datetime(today), today) - - def test_cast_date(self): - """Nothing happens _cast_date""" - dsa = DSACalculator(self.travel) - today = date.today() - self.assertEqual(dsa._cast_date(today), today) - - def test_get_dsa_amount_usd(self): - """If currency code is USD and NOT 60 plus - then get region rate for USD - """ - self.assertEqual( - self.amsterdam.get_rate_at(self.travel.submitted_at), - self.amsterdam_rate - ) - dsa = DSACalculator(self.travel) - self.assertEqual(dsa.travel.currency.code, dsa.USD_CODE) - self.assertEqual( - dsa.get_dsa_amount(self.amsterdam, False), - self.amsterdam_rate.dsa_amount_usd - ) - - def test_get_dsa_amount_usd_60plus(self): - """If currency code is USD and 60 plus then get region rate for USD - """ - self.assertEqual( - self.amsterdam.get_rate_at(self.travel.submitted_at), - self.amsterdam_rate - ) - dsa = DSACalculator(self.travel) - self.assertEqual(dsa.travel.currency.code, dsa.USD_CODE) - self.assertEqual( - dsa.get_dsa_amount(self.amsterdam, True), - self.amsterdam_rate.dsa_amount_60plus_usd - ) - - def test_get_dsa_amount_local(self): - """If currency code is NOT USD and NOT 60 plus - then get region rate for local - """ - self.assertEqual( - self.amsterdam.get_rate_at(self.travel.submitted_at), - self.amsterdam_rate - ) - dsa = DSACalculator(self.travel) - dsa.travel.currency.code = "EU" - self.assertNotEqual(dsa.travel.currency.code, dsa.USD_CODE) - self.assertEqual( - dsa.get_dsa_amount(self.amsterdam, False), - self.amsterdam_rate.dsa_amount_local - ) - - def test_get_dsa_amount_local_60plus(self): - """If currency code is NOT USD and 60 plus - then get region rate for local - """ - self.assertEqual( - self.amsterdam.get_rate_at(self.travel.submitted_at), - self.amsterdam_rate - ) - dsa = DSACalculator(self.travel) - dsa.travel.currency.code = "EU" - self.assertNotEqual(dsa.travel.currency.code, dsa.USD_CODE) - self.assertEqual( - dsa.get_dsa_amount(self.amsterdam, True), - self.amsterdam_rate.dsa_amount_60plus_local - ) - - def test_get_dsa_amount_no_currency(self): - """If no currency and NOT 60 plus then get region rate for local""" - self.assertEqual( - self.amsterdam.get_rate_at(self.travel.submitted_at), - self.amsterdam_rate - ) - dsa = DSACalculator(self.travel) - dsa.travel.currency = None - self.assertEqual( - dsa.get_dsa_amount(self.amsterdam, False), - self.amsterdam_rate.dsa_amount_local - ) - - def test_get_dsa_amount_no_currency_60plus(self): - """If no currency and 60 plus then get region rate for local 60plus""" - self.assertEqual( - self.amsterdam.get_rate_at(self.travel.submitted_at), - self.amsterdam_rate - ) - dsa = DSACalculator(self.travel) - dsa.travel.currency = None - self.assertEqual( - dsa.get_dsa_amount(self.amsterdam, True), - self.amsterdam_rate.dsa_amount_60plus_local - ) - - def test_get_by_day_grouping_no_itinerary(self): - """If less than 2 itineary items, then empty list. - Check handling with no itineraries - """ - self.assertEqual(self.travel.itinerary.count(), 0) - dsa = DSACalculator(self.travel) - self.assertEqual(dsa.get_by_day_grouping(), []) - - def test_get_by_day_grouping_single_itinerary(self): - """If less than 2 itineary items, then empty list - Check handling with one itinerary - """ - ItineraryItemFactory( - travel=self.travel, - arrival_date=datetime(2017, 1, 1, 1, 0, tzinfo=UTC), - departure_date=datetime(2017, 1, 1, 2, 0, tzinfo=UTC), - dsa_region=self.budapest - ) - self.assertEqual(self.travel.itinerary.count(), 1) - dsa = DSACalculator(self.travel) - self.assertEqual(dsa.get_by_day_grouping(), []) - - def test_get_by_day_grouping(self): - """If itinerary count greater than 2, - then collate list of dsa dto with amounts ordered by date - """ - ItineraryItemFactory( - travel=self.travel, - arrival_date=datetime(2017, 1, 1, 1, 0, tzinfo=UTC), - departure_date=datetime(2017, 1, 1, 2, 0, tzinfo=UTC), - dsa_region=self.amsterdam - ) - - ItineraryItemFactory( - travel=self.travel, - arrival_date=datetime(2017, 1, 2, 10, 0, tzinfo=UTC), - departure_date=datetime(2017, 1, 3, 15, 0, tzinfo=UTC), - dsa_region=self.amsterdam - ) - dsa = DSACalculator(self.travel) - dsa_dto_list = dsa.get_by_day_grouping() - self.assertEqual(len(dsa_dto_list), 3) - self.assertEqual(dsa_dto_list[0].date, date(2017, 1, 1)) - self.assertEqual(dsa_dto_list[-1].date, date(2017, 1, 3)) - first_day = dsa_dto_list[0] - last_day = dsa_dto_list[-1] - self.assertEqual(first_day.daily_rate, self.amsterdam.dsa_amount_usd) - self.assertEqual(last_day.daily_rate, self.amsterdam.dsa_amount_usd) - - def test_get_by_day_grouping_multiple_single_day(self): - """If itinerary count greater than 2, and all days are the same - then collated list of dsa dto should be the single day - """ - ItineraryItemFactory( - travel=self.travel, - arrival_date=datetime(2017, 1, 1, 1, 0, tzinfo=UTC), - departure_date=datetime(2017, 1, 1, 2, 0, tzinfo=UTC), - dsa_region=self.amsterdam - ) - ItineraryItemFactory( - travel=self.travel, - arrival_date=datetime(2017, 1, 1, 2, 0, tzinfo=UTC), - departure_date=datetime(2017, 1, 1, 3, 0, tzinfo=UTC), - dsa_region=self.amsterdam - ) - ItineraryItemFactory( - travel=self.travel, - arrival_date=datetime(2017, 1, 1, 6, 0, tzinfo=UTC), - departure_date=datetime(2017, 1, 1, 8, 0, tzinfo=UTC), - dsa_region=self.amsterdam - ) - dsa = DSACalculator(self.travel) - dsa_dto_list = dsa.get_by_day_grouping() - self.assertEqual(len(dsa_dto_list), 1) - self.assertEqual(dsa_dto_list[0].date, date(2017, 1, 1)) - day = dsa_dto_list[0] - self.assertEqual(day.daily_rate, self.amsterdam.dsa_amount_usd) - - def test_get_by_day_grouping_60plus(self): - """If itinerary count greater than 2, - then collate list of dsa dto with amounts ordered by date - if days grater than 60 then dsa daily amount changes - """ - ItineraryItemFactory( - travel=self.travel, - arrival_date=datetime(2017, 1, 1, 1, 0, tzinfo=UTC), - departure_date=datetime(2017, 1, 1, 2, 0, tzinfo=UTC), - dsa_region=self.amsterdam - ) - - ItineraryItemFactory( - travel=self.travel, - arrival_date=datetime(2017, 1, 2, 15, 0, tzinfo=UTC), - departure_date=datetime(2017, 4, 3, 10, 0, tzinfo=UTC), - dsa_region=self.amsterdam - ) - dsa = DSACalculator(self.travel) - dsa_dto_list = dsa.get_by_day_grouping() - self.assertEqual(len(dsa_dto_list), 93) - self.assertEqual(dsa_dto_list[0].date, date(2017, 1, 1)) - self.assertEqual(dsa_dto_list[-1].date, date(2017, 4, 3)) - first_day = dsa_dto_list[0] - last_day = dsa_dto_list[-1] - self.assertEqual(first_day.daily_rate, self.amsterdam.dsa_amount_usd) - self.assertEqual( - last_day.daily_rate, - self.amsterdam.dsa_amount_60plus_usd - ) - - def test_one_day_long_trip_empty(self): - """If dsa_dto_list provided is empty return list""" - dsa = DSACalculator(self.travel) - self.assertEqual(dsa.check_one_day_long_trip([]), []) - - def test_one_day_long_trip_many(self): - """If length of dsa_dto_list provided is greater than 1 - then return list - """ - dsa = DSACalculator(self.travel) - self.assertEqual(dsa.check_one_day_long_trip([1, 2]), [1, 2]) - - def test_one_day_long_trip_multiple_long(self): - """Multiple itineraies on the same day, - If subsequent itineraries are longer than 8 hours, then return dto list - """ - itinerary = ItineraryItemFactory( - travel=self.travel, - arrival_date=datetime(2017, 1, 1, 1, 0, tzinfo=UTC), - departure_date=datetime(2017, 1, 1, 3, 0, tzinfo=UTC), - dsa_region=self.amsterdam - ) - ItineraryItemFactory( - travel=self.travel, - arrival_date=datetime(2017, 1, 1, 4, 0, tzinfo=UTC), - departure_date=datetime(2017, 1, 1, 11, 0, tzinfo=UTC), - dsa_region=self.amsterdam - ) - dsa_dto_list = [DSAdto(date(2017, 1, 1), itinerary)] - dsa = DSACalculator(self.travel) - self.assertEqual( - dsa.check_one_day_long_trip(dsa_dto_list), - dsa_dto_list - ) - - @skip("DSA Calculations have been disabled") - # TODO: Confirm that this is correct, Seems counterintuitive - # shouldn't we add the hours of the itineraries up and if total >= 8 hours - # then considered as a valid day? - # At the moment, only checking itinerary against previous itinerary - def test_one_day_long_trip_multiple_short(self): - """Multiple itineraies on the same day, - If subsequent itineraries are shorter than 8 hours, - then return empty list - """ - itinerary = ItineraryItemFactory( - travel=self.travel, - arrival_date=datetime(2017, 1, 1, 1, 0, tzinfo=UTC), - departure_date=datetime(2017, 1, 1, 3, 0, tzinfo=UTC), - dsa_region=self.amsterdam - ) - ItineraryItemFactory( - travel=self.travel, - arrival_date=datetime(2017, 1, 1, 4, 0, tzinfo=UTC), - departure_date=datetime(2017, 1, 1, 8, 0, tzinfo=UTC), - dsa_region=self.amsterdam - ) - ItineraryItemFactory( - travel=self.travel, - arrival_date=datetime(2017, 1, 1, 9, 0, tzinfo=UTC), - departure_date=datetime(2017, 1, 1, 11, 0, tzinfo=UTC), - dsa_region=self.amsterdam - ) - dsa_dto_list = [DSAdto(date(2017, 1, 1), itinerary)] - dsa = DSACalculator(self.travel) - self.assertEqual(dsa.check_one_day_long_trip(dsa_dto_list), []) - - def test_one_day_long_trip_short(self): - """If single itineray on the day, and itinerary is less than 8 hours, - then return empty list - """ - today = datetime(2017, 1, 1, 1, 0, tzinfo=UTC) - itinerary = ItineraryItemFactory( - travel=self.travel, - arrival_date=today, - departure_date=today + timedelta(hours=6), - dsa_region=self.amsterdam - ) - dsa_dto_list = [DSAdto(today.date(), itinerary)] - dsa = DSACalculator(self.travel) - self.assertEqual(dsa.check_one_day_long_trip(dsa_dto_list), []) - - # TODO: Confirm that this is correct. Seems counterintuitive - # shouldn't a single day long trip >= 8 hrs be considered as valid? - # and not return empty. May be part of larger calculation? - def test_one_day_long_trip_long(self): - """If single itineray on the day, and itinerary is more than 8 hours, - then return empty list - """ - today = datetime(2017, 1, 1, 1, 0, tzinfo=UTC) - itinerary = ItineraryItemFactory( - travel=self.travel, - arrival_date=today, - departure_date=today + timedelta(hours=10), - dsa_region=self.amsterdam - ) - dsa_dto_list = [DSAdto(today.date(), itinerary)] - dsa = DSACalculator(self.travel) - self.assertEqual(dsa.check_one_day_long_trip(dsa_dto_list), []) - - def test_add_same_day_travel_same_region(self): - """If same region, then no change""" - itinerary = ItineraryItemFactory( - travel=self.travel, - arrival_date=datetime(2017, 1, 1, 1, 0, tzinfo=UTC), - departure_date=datetime(2017, 1, 1, 4, 0, tzinfo=UTC), - dsa_region=self.amsterdam - ) - ItineraryItemFactory( - travel=self.travel, - arrival_date=datetime(2017, 1, 1, 5, 0, tzinfo=UTC), - departure_date=datetime(2017, 1, 1, 12, 0, tzinfo=UTC), - dsa_region=self.amsterdam - ) - dsa = DSACalculator(self.travel) - dto = DSAdto(date(2017, 1, 1), itinerary) - dto.dsa_amount = 0 - dsa.add_same_day_travels(dto, 1) - self.assertEqual(dto.dsa_amount, 0) - - def test_add_same_day_travel_short(self): - """If different region, and time is less than 8 hrs, then no change""" - itinerary = ItineraryItemFactory( - travel=self.travel, - arrival_date=datetime(2017, 1, 1, 1, 0, tzinfo=UTC), - departure_date=datetime(2017, 1, 1, 4, 0, tzinfo=UTC), - dsa_region=self.amsterdam - ) - ItineraryItemFactory( - travel=self.travel, - arrival_date=datetime(2017, 1, 1, 5, 0, tzinfo=UTC), - departure_date=datetime(2017, 1, 1, 12, 0, tzinfo=UTC), - dsa_region=self.budapest - ) - dsa = DSACalculator(self.travel) - dto = DSAdto(date(2017, 1, 1), itinerary) - dto.dsa_amount = 0 - dsa.add_same_day_travels(dto, 1) - self.assertEqual(dto.dsa_amount, 0) - - @skip("DSA Calculations have been disabled") - # TODO: confirm that this is correct - # If only a single itinerary, but still >= 8 hrs, no change - # Also why does this only get calculated against different regions? - def test_add_same_day_travel_single(self): - """If different region, and time is >= than 8 hrs, - but only a single itinerary, then no change - """ - itinerary = ItineraryItemFactory( - travel=self.travel, - arrival_date=datetime(2017, 1, 1, 1, 0, tzinfo=UTC), - departure_date=datetime(2017, 1, 1, 4, 0, tzinfo=UTC), - dsa_region=self.budapest - ) - ItineraryItemFactory( - travel=self.travel, - arrival_date=datetime(2017, 1, 1, 5, 0, tzinfo=UTC), - departure_date=datetime(2017, 1, 1, 14, 0, tzinfo=UTC), - dsa_region=self.amsterdam - ) - dsa = DSACalculator(self.travel) - dto = DSAdto(date(2017, 1, 1), itinerary) - dto.dsa_amount = 0 - dsa.add_same_day_travels(dto, 1) - self.assertEqual(dto.dsa_amount, 0) - - def test_add_same_day_travel(self): - """If different region, and time is >= than 8 hrs, - then update dsa amount - """ - dsa = DSACalculator(self.travel) - itinerary = ItineraryItemFactory( - travel=self.travel, - arrival_date=datetime(2017, 1, 1, 1, 0, tzinfo=UTC), - departure_date=datetime(2017, 1, 1, 4, 0, tzinfo=UTC), - dsa_region=self.budapest - ) - ItineraryItemFactory( - travel=self.travel, - arrival_date=datetime(2017, 1, 1, 5, 0, tzinfo=UTC), - departure_date=datetime(2017, 1, 1, 8, 0, tzinfo=UTC), - dsa_region=self.amsterdam - ) - ItineraryItemFactory( - travel=self.travel, - arrival_date=datetime(2017, 1, 1, 9, 0, tzinfo=UTC), - departure_date=datetime(2017, 1, 1, 14, 0, tzinfo=UTC), - dsa_region=self.amsterdam - ) - dto = DSAdto(date(2017, 1, 1), itinerary) - dto.dsa_amount = 0 - dsa.add_same_day_travels(dto, 1) - extra_rate = ( - self.amsterdam.dsa_amount_usd * dsa.SAME_DAY_TRAVEL_MULTIPLIER - ) - self.assertEqual(dto.dsa_amount, extra_rate) - - def test_add_same_day_travel_60plus(self): - """If different region, and time is >= than 8 hrs, - then update dsa amount - """ - itinerary = ItineraryItemFactory( - travel=self.travel, - arrival_date=datetime(2017, 1, 1, 1, 0, tzinfo=UTC), - departure_date=datetime(2017, 1, 1, 4, 0, tzinfo=UTC), - dsa_region=self.budapest - ) - ItineraryItemFactory( - travel=self.travel, - arrival_date=datetime(2017, 1, 1, 5, 0, tzinfo=UTC), - departure_date=datetime(2017, 1, 1, 8, 0, tzinfo=UTC), - dsa_region=self.amsterdam - ) - ItineraryItemFactory( - travel=self.travel, - arrival_date=datetime(2017, 1, 1, 9, 0, tzinfo=UTC), - departure_date=datetime(2017, 1, 1, 14, 0, tzinfo=UTC), - dsa_region=self.amsterdam - ) - dsa = DSACalculator(self.travel) - dto = DSAdto(date(2017, 1, 1), itinerary) - dto.dsa_amount = 0 - dsa.add_same_day_travels(dto, 62) - extra_rate = ( - self.amsterdam.dsa_amount_60plus_usd * dsa.SAME_DAY_TRAVEL_MULTIPLIER - ) - self.assertEqual(dto.dsa_amount, extra_rate) - - def test_calculate_daily_dsa_rate_empty(self): - """If empty list provided, then return empty list""" - dsa = DSACalculator(self.travel) - self.assertEqual(dsa.calculate_daily_dsa_rate([]), []) - - def test_calculate_daily_dsa_rate_overnight(self): - """If departure date matches dto date and overnight, then no change""" - itinerary = ItineraryItemFactory( - travel=self.travel, - arrival_date=datetime(2017, 1, 1, 1, 0, tzinfo=UTC), - departure_date=datetime(2017, 1, 1, 4, 0, tzinfo=UTC), - dsa_region=self.amsterdam, - overnight_travel=True, - ) - dsa = DSACalculator(self.travel) - dto = DSAdto(date(2017, 1, 1), itinerary) - dto.dsa_amount = 0 - self.assertEqual(dsa.calculate_daily_dsa_rate([dto]), [dto]) - self.assertEqual(dto.dsa_amount, 0) - - def test_calculate_daily_dsa_rate_not_overnight(self): - """If departure date matches dto date and NOT overnight, - then update dsa amount - """ - itinerary = ItineraryItemFactory( - travel=self.travel, - arrival_date=datetime(2017, 1, 1, 1, 0, tzinfo=UTC), - departure_date=datetime(2017, 1, 1, 4, 0, tzinfo=UTC), - dsa_region=self.amsterdam, - overnight_travel=False, - ) - dsa = DSACalculator(self.travel) - dto = DSAdto(date(2017, 1, 1), itinerary) - dto.dsa_amount = 0 - self.assertEqual(dsa.calculate_daily_dsa_rate([dto]), [dto]) - self.assertEqual(dto.dsa_amount, self.amsterdam.dsa_amount_usd) - - def test_calculate_daily_dsa_rate_overnight_multi(self): - """If overnight travel and multiple days - Arrival date has dsa amount, while departure date is zero - """ - itinerary = ItineraryItemFactory( - travel=self.travel, - arrival_date=datetime(2017, 1, 1, 1, 0, tzinfo=UTC), - departure_date=datetime(2017, 1, 2, 1, 0, tzinfo=UTC), - dsa_region=self.amsterdam, - overnight_travel=True, - ) - dsa = DSACalculator(self.travel) - dto_arrival = DSAdto(date(2017, 1, 1), itinerary) - dto_departure = DSAdto(date(2017, 1, 2), itinerary) - dto_arrival.dsa_amount = 0 - dto_departure.dsa_amount = 0 - dsa_dto_list = [dto_arrival, dto_departure] - self.assertEqual( - dsa.calculate_daily_dsa_rate(dsa_dto_list), - dsa_dto_list - ) - self.assertEqual(dto_arrival.dsa_amount, self.amsterdam.dsa_amount_usd) - self.assertEqual(dto_departure.dsa_amount, 0) - - def test_calculate_daily_dsa_rate_60plus(self): - """If 60 plus days provided, then update dsa amount - and after 60 rate is different - """ - ItineraryItemFactory( - travel=self.travel, - arrival_date=datetime(2017, 1, 1, 1, 0, tzinfo=UTC), - departure_date=datetime(2017, 1, 2, 1, 0, tzinfo=UTC), - dsa_region=self.amsterdam, - overnight_travel=True, - ) - ItineraryItemFactory( - travel=self.travel, - arrival_date=datetime(2017, 1, 2, 1, 0, tzinfo=UTC), - departure_date=datetime(2017, 5, 5, 1, 0, tzinfo=UTC), - dsa_region=self.amsterdam, - overnight_travel=True, - ) - dsa = DSACalculator(self.travel) - dsa_dto_list = dsa.get_by_day_grouping() - self.assertEqual(len(dsa_dto_list), 125) - for dto in dsa_dto_list: - dto.dsa_amount = 0 - self.assertEqual( - dsa.calculate_daily_dsa_rate(dsa_dto_list), - dsa_dto_list - ) - self.assertEqual( - dsa_dto_list[0].dsa_amount, - self.amsterdam.dsa_amount_usd - ) - self.assertEqual(dsa_dto_list[1].dsa_amount, 0) - for dto in dsa_dto_list[2:59]: - self.assertEqual(dto.dsa_amount, self.amsterdam.dsa_amount_usd) - for dto in dsa_dto_list[60:]: - self.assertEqual( - dto.dsa_amount, - self.amsterdam.dsa_amount_60plus_usd - ) - - @skip("DSA Calculations have been disabled") - # TODO: Confirm this is correct. - # If travel on same day, falls on a date prior to last day of - # of dsa dto list, then the last is definitely longer than 8 hrs - # So we have the chance of adding same day travel multiplier twice - def test_calculate_daily_dsa_rate_same_day_travel(self): - """If departure date matches dto date, is overnight, - and same day travel, then update dsa amount""" - itinerary = ItineraryItemFactory( - travel=self.travel, - arrival_date=datetime(2017, 1, 1, 1, 0, tzinfo=UTC), - departure_date=datetime(2017, 1, 2, 4, 0, tzinfo=UTC), - dsa_region=self.budapest, - overnight_travel=False, - ) - ItineraryItemFactory( - travel=self.travel, - arrival_date=datetime(2017, 1, 1, 5, 0, tzinfo=UTC), - departure_date=datetime(2017, 1, 1, 8, 0, tzinfo=UTC), - dsa_region=self.amsterdam, - ) - ItineraryItemFactory( - travel=self.travel, - arrival_date=datetime(2017, 1, 1, 9, 0, tzinfo=UTC), - departure_date=datetime(2017, 1, 1, 14, 0, tzinfo=UTC), - dsa_region=self.amsterdam, - ) - dsa = DSACalculator(self.travel) - dto_arrival = DSAdto(date(2017, 1, 1), itinerary) - dto_departure = DSAdto(date(2017, 1, 2), itinerary) - dto_arrival.dsa_amount = 0 - dto_departure.dsa_amount = 0 - dsa_dto_list = [dto_arrival, dto_departure] - self.assertEqual( - dsa.calculate_daily_dsa_rate(dsa_dto_list), - dsa_dto_list - ) - extra = self.amsterdam.dsa_amount_usd * dsa.SAME_DAY_TRAVEL_MULTIPLIER - self.assertEqual( - dto_arrival.dsa_amount, - self.budapest.dsa_amount_usd + (extra * 2) - ) - self.assertEqual( - dto_departure.dsa_amount, - self.budapest.dsa_amount_usd - ) - - def test_calculate_daily_deductions_empty(self): - """If empty list provided, just return the empty list""" - dsa = DSACalculator(self.travel) - self.assertEqual(dsa.calculate_daily_deduction([]), []) - - def test_calculate_daily_deductions(self): - """If deduction set for dto date, then set deduction multiplier, - otherwise set to 0""" - itinerary = ItineraryItemFactory( - travel=self.travel, - arrival_date=datetime(2017, 1, 1, 1, 0, tzinfo=UTC), - departure_date=datetime(2017, 1, 2, 4, 0, tzinfo=UTC), - dsa_region=self.budapest, - ) - DeductionFactory(travel=self.travel, date=date(2017, 1, 1), lunch=True) - dsa = DSACalculator(self.travel) - dto_arrival = DSAdto(date(2017, 1, 1), itinerary) - dto_departure = DSAdto(date(2017, 1, 2), itinerary) - dsa_dto_list = [dto_arrival, dto_departure] - dsa_dto_list = dsa.calculate_daily_deduction(dsa_dto_list) - self.assertEqual(dsa_dto_list[0].deduction_multiplier, Decimal('0.1')) - self.assertEqual(dsa_dto_list[1].deduction_multiplier, Decimal('0')) - - def test_check_last_day_empty(self): - """If empty list given, expect empty list returned""" - dsa = DSACalculator(self.travel) - self.assertEqual(dsa.check_last_day([]), []) - - def test_check_last_day_single(self): - """If single dto, then expect that dto last day attribute to be True""" - itinerary = ItineraryItemFactory( - travel=self.travel, - arrival_date=datetime(2017, 1, 1, 1, 0, tzinfo=UTC), - departure_date=datetime(2017, 1, 2, 4, 0, tzinfo=UTC), - dsa_region=self.budapest, - ) - dsa = DSACalculator(self.travel) - dto = DSAdto(date(2017, 1, 1), itinerary) - dsa_dto_list = [dto] - self.assertEqual(dsa.check_last_day(dsa_dto_list), dsa_dto_list) - self.assertTrue(dto.last_day) - - def test_check_last_day(self): - """If multiple dto, then expect that dto last day attribute to be True - and first dto last day to attribute to be False - """ - itinerary = ItineraryItemFactory( - travel=self.travel, - arrival_date=datetime(2017, 1, 1, 1, 0, tzinfo=UTC), - departure_date=datetime(2017, 1, 2, 4, 0, tzinfo=UTC), - dsa_region=self.budapest, - ) - dsa = DSACalculator(self.travel) - dto_arrival = DSAdto(date(2017, 1, 1), itinerary) - dto_departure = DSAdto(date(2017, 1, 2), itinerary) - dsa_dto_list = [dto_arrival, dto_departure] - self.assertEqual(dsa.check_last_day(dsa_dto_list), dsa_dto_list) - self.assertFalse(dto_arrival.last_day) - self.assertTrue(dto_departure.last_day) - - def test_check_last_day_multiple_itinerary(self): - """If multiple dto, then expect that dto last day attribute to be True - and first dto last day to attribute to be False - Also the itinerary on last dto updated - """ - itinerary_1 = ItineraryItemFactory( - travel=self.travel, - arrival_date=datetime(2017, 1, 1, 1, 0, tzinfo=UTC), - departure_date=datetime(2017, 1, 4, 4, 0, tzinfo=UTC), - dsa_region=self.budapest, - ) - itinerary_2 = ItineraryItemFactory( - travel=self.travel, - arrival_date=datetime(2017, 1, 2, 1, 0, tzinfo=UTC), - departure_date=datetime(2017, 1, 2, 4, 0, tzinfo=UTC), - dsa_region=self.budapest, - ) - dsa = DSACalculator(self.travel) - dsa_dto_list = dsa.get_by_day_grouping() - - # confirm all dtos have itinerary_1 - for dto in dsa_dto_list: - self.assertEqual(dto.itinerary_item, itinerary_1) - - self.assertEqual(dsa.check_last_day(dsa_dto_list), dsa_dto_list) - - # confirm that all but last dto have itinerary_1 - for dto in dsa_dto_list[:-1]: - self.assertEqual(dto.itinerary_item, itinerary_1) - - # confirm last dto has itinerary_2 - self.assertEqual(dsa_dto_list[-1].itinerary_item, itinerary_2) - - # confirm that all but last dto has last_day set to False - for dto in dsa_dto_list[:-1]: - self.assertFalse(dto.last_day) - # confirm that last dto has last_day set to True - self.assertTrue(dsa_dto_list[-1].last_day) - - def tets_aggregate_detail_dsa_empty(self): - """If empty list provided, expect empty list returned""" - dsa = DSACalculator(self.travel) - self.assertEqual(dsa.aggregate_detailed_dsa([]), []) - - def test_aggregate_detailed_dsa(self): - """Check that detailed dsa data set - - Total amount and paid to traveller are amounts for a single day - """ - itinerary = ItineraryItemFactory( - travel=self.travel, - arrival_date=datetime(2017, 1, 1, 1, 0, tzinfo=UTC), - departure_date=datetime(2017, 1, 2, 4, 0, tzinfo=UTC), - dsa_region=self.amsterdam, - ) - ItineraryItemFactory( - travel=self.travel, - arrival_date=datetime(2017, 1, 3, 1, 0, tzinfo=UTC), - departure_date=datetime(2017, 1, 4, 4, 0, tzinfo=UTC), - dsa_region=self.amsterdam, - ) - dsa = DSACalculator(self.travel) - dsa_dto_list = dsa.get_by_day_grouping() - dsa.check_last_day(dsa_dto_list) - self.assertTrue(dsa_dto_list[-1].last_day) - self.assertEqual(len(dsa_dto_list), 4) - detailed_dsa = dsa.aggregate_detailed_dsa(dsa_dto_list) - self.assertEqual(len(detailed_dsa), 1) - data = detailed_dsa[0] - self.assertEqual(data, { - "start_date": date(2017, 1, 1), - "end_date": date(2017, 1, 4), - "dsa_region": itinerary.dsa_region.pk, - "dsa_region_name": itinerary.dsa_region.label, - "night_count": 3, - "daily_rate": self.amsterdam.dsa_amount_usd, - "paid_to_traveler": dsa_dto_list[-1].final_amount, - "total_amount": dsa_dto_list[-1].corrected_dsa_amount, - "deduction": Decimal(0), - }) - - def test_aggregate_detailed_dsa_no_last_day(self): - """Check that detailed dsa data set, if no last day set, - then zero for amounts - """ - itinerary = ItineraryItemFactory( - travel=self.travel, - arrival_date=datetime(2017, 1, 1, 1, 0, tzinfo=UTC), - departure_date=datetime(2017, 1, 2, 4, 0, tzinfo=UTC), - dsa_region=self.amsterdam, - ) - ItineraryItemFactory( - travel=self.travel, - arrival_date=datetime(2017, 1, 3, 1, 0, tzinfo=UTC), - departure_date=datetime(2017, 1, 4, 4, 0, tzinfo=UTC), - dsa_region=self.amsterdam, - ) - dsa = DSACalculator(self.travel) - dsa_dto_list = dsa.get_by_day_grouping() - self.assertEqual(len(dsa_dto_list), 4) - detailed_dsa = dsa.aggregate_detailed_dsa(dsa_dto_list) - self.assertEqual(len(detailed_dsa), 1) - data = detailed_dsa[0] - self.assertEqual(data, { - "start_date": date(2017, 1, 1), - "end_date": date(2017, 1, 4), - "dsa_region": itinerary.dsa_region.pk, - "dsa_region_name": itinerary.dsa_region.label, - "night_count": 3, - "daily_rate": self.amsterdam.dsa_amount_usd, - "paid_to_traveler": Decimal(0), - "total_amount": Decimal(0), - "deduction": Decimal(0), - }) - - def test_aggregate_detailed_dsa_60plus(self): - """Check that detailed dsa data set, if greater than 60 days - then daily rate changes and amount calculated is based on 60plus - - Total amount and paid to traveller are amounts for a single day - """ - itinerary = ItineraryItemFactory( - travel=self.travel, - arrival_date=datetime(2017, 1, 1, 1, 0, tzinfo=UTC), - departure_date=datetime(2017, 1, 2, 4, 0, tzinfo=UTC), - dsa_region=self.amsterdam, - ) - ItineraryItemFactory( - travel=self.travel, - arrival_date=datetime(2017, 1, 3, 1, 0, tzinfo=UTC), - departure_date=datetime(2017, 5, 4, 4, 0, tzinfo=UTC), - dsa_region=self.amsterdam, - ) - dsa = DSACalculator(self.travel) - dsa_dto_list = dsa.get_by_day_grouping() - dsa.check_last_day(dsa_dto_list) - self.assertTrue(dsa_dto_list[-1].last_day) - self.assertEqual(len(dsa_dto_list), 124) - detailed_dsa = dsa.aggregate_detailed_dsa(dsa_dto_list) - self.assertEqual(len(detailed_dsa), 2) - self.assertEqual(detailed_dsa[0], { - "start_date": date(2017, 1, 1), - "end_date": date(2017, 3, 1), - "dsa_region": itinerary.dsa_region.pk, - "dsa_region_name": itinerary.dsa_region.label, - "night_count": 59, - "daily_rate": self.amsterdam.dsa_amount_usd, - "paid_to_traveler": Decimal(0), - "total_amount": Decimal(0), - "deduction": Decimal(0), - }) - self.assertEqual(detailed_dsa[1], { - "start_date": date(2017, 3, 2), - "end_date": date(2017, 5, 4), - "dsa_region": itinerary.dsa_region.pk, - "dsa_region_name": itinerary.dsa_region.label, - "night_count": 63, - "daily_rate": self.amsterdam.dsa_amount_60plus_usd, - "paid_to_traveler": dsa_dto_list[-1].final_amount, - "total_amount": dsa_dto_list[-1].corrected_dsa_amount, - "deduction": Decimal(0), - }) - - def test_calculate_dsa_not_ta_required(self): - """If TA is not required, then should be zero""" - self.travel.ta_required = False - self.assertFalse(self.travel.ta_required) - dsa = DSACalculator(self.travel) - dsa.calculate_dsa() - self.assertEqual(dsa.total_dsa, Decimal(0)) - self.assertEqual(dsa.total_deductions, Decimal(0)) - self.assertEqual(dsa.paid_to_traveler, Decimal(0)) - self.assertEqual(dsa.detailed_dsa, []) - - def test_calculate_dsa_no_region(self): - """If region for one of the itineraries is None, then should be zero""" - self.assertTrue(self.travel.ta_required) - ItineraryItemFactory( - travel=self.travel, - arrival_date=datetime(2017, 1, 1, 1, 0, tzinfo=UTC), - departure_date=datetime(2017, 1, 2, 4, 0, tzinfo=UTC), - dsa_region=None, - ) - dsa = DSACalculator(self.travel) - dsa.calculate_dsa() - self.assertEqual(dsa.total_dsa, Decimal(0)) - self.assertEqual(dsa.total_deductions, Decimal(0)) - self.assertEqual(dsa.paid_to_traveler, Decimal(0)) - self.assertEqual(dsa.detailed_dsa, []) - - def test_calculate_dsa_single_itinerary(self): - """If single itinerary then should be zero and empty detailed dsa""" - ItineraryItemFactory( - travel=self.travel, - arrival_date=datetime(2017, 1, 1, 1, 0, tzinfo=UTC), - departure_date=datetime(2017, 1, 2, 4, 0, tzinfo=UTC), - dsa_region=self.amsterdam, - ) - dsa = DSACalculator(self.travel) - dsa.calculate_dsa() - self.assertEqual(dsa.total_dsa, Decimal(0)) - self.assertEqual(dsa.total_deductions, Decimal(0)) - self.assertEqual(dsa.paid_to_traveler, Decimal(0)) - self.assertEqual(dsa.detailed_dsa, []) - - def test_calculate_dsa(self): - """If itinerary less than 60 days totals should be multiples of days""" - itinerary = ItineraryItemFactory( - travel=self.travel, - arrival_date=datetime(2017, 1, 1, 1, 0, tzinfo=UTC), - departure_date=datetime(2017, 1, 2, 4, 0, tzinfo=UTC), - dsa_region=self.amsterdam, - ) - ItineraryItemFactory( - travel=self.travel, - arrival_date=datetime(2017, 1, 3, 1, 0, tzinfo=UTC), - departure_date=datetime(2017, 1, 4, 4, 0, tzinfo=UTC), - dsa_region=self.amsterdam, - ) - dsa = DSACalculator(self.travel) - dsa.calculate_dsa() - daily_amt = self.amsterdam.dsa_amount_usd - last_day_amount = daily_amt * (1 - dsa.LAST_DAY_DEDUCTION) - self.assertEqual(dsa.total_dsa, daily_amt * 3 + last_day_amount) - self.assertEqual(dsa.total_deductions, Decimal(0)) - self.assertEqual(dsa.paid_to_traveler, daily_amt * 3 + last_day_amount) - self.assertEqual(dsa.detailed_dsa, [{ - "start_date": date(2017, 1, 1), - "end_date": date(2017, 1, 4), - "dsa_region": itinerary.dsa_region.pk, - "dsa_region_name": itinerary.dsa_region.label, - "night_count": 3, - "daily_rate": daily_amt, - "paid_to_traveler": dsa.paid_to_traveler, - "total_amount": dsa.total_dsa, - "deduction": Decimal(0), - }]) - - def test_calculate_dsa_60plus(self): - """If itinerary greater than 60 days totals should be - multiples of days and change after 60 days""" - itinerary = ItineraryItemFactory( - travel=self.travel, - arrival_date=datetime(2017, 1, 1, 1, 0, tzinfo=UTC), - departure_date=datetime(2017, 1, 2, 4, 0, tzinfo=UTC), - dsa_region=self.amsterdam, - ) - ItineraryItemFactory( - travel=self.travel, - arrival_date=datetime(2017, 1, 3, 1, 0, tzinfo=UTC), - departure_date=datetime(2017, 5, 4, 4, 0, tzinfo=UTC), - dsa_region=self.amsterdam, - ) - dsa = DSACalculator(self.travel) - dsa.calculate_dsa() - daily_amt = self.amsterdam.dsa_amount_usd - daily_60plus_amt = self.amsterdam.dsa_amount_60plus_usd - last_day_amount = daily_60plus_amt * (1 - dsa.LAST_DAY_DEDUCTION) - first_portion = daily_amt * 60 - second_portion = daily_60plus_amt * 63 + last_day_amount - self.assertEqual(dsa.total_dsa, first_portion + second_portion) - self.assertEqual(dsa.total_deductions, Decimal(0)) - self.assertEqual(dsa.paid_to_traveler, first_portion + second_portion) - self.assertEqual(dsa.detailed_dsa, [{ - "start_date": date(2017, 1, 1), - "end_date": date(2017, 3, 1), - "dsa_region": itinerary.dsa_region.pk, - "dsa_region_name": itinerary.dsa_region.label, - "night_count": 59, - "daily_rate": daily_amt, - "paid_to_traveler": first_portion, - "total_amount": first_portion, - "deduction": Decimal(0), - }, { - "start_date": date(2017, 3, 2), - "end_date": date(2017, 5, 4), - "dsa_region": itinerary.dsa_region.pk, - "dsa_region_name": itinerary.dsa_region.label, - "night_count": 63, - "daily_rate": daily_60plus_amt, - "paid_to_traveler": second_portion, - "total_amount": second_portion, - "deduction": Decimal(0), - }]) - - def test_case_1(self): - ItineraryItemFactory(travel=self.travel, - departure_date=datetime(2017, 1, 1, 1, 0, tzinfo=UTC), - arrival_date=datetime(2017, 1, 1, 2, 0, tzinfo=UTC), - dsa_region=self.budapest) - - ItineraryItemFactory(travel=self.travel, - departure_date=datetime(2017, 1, 1, 10, 0, tzinfo=UTC), - arrival_date=datetime(2017, 1, 1, 15, 0, tzinfo=UTC), - dsa_region=self.amsterdam) - - DeductionFactory(travel=self.travel, - date=date(2017, 1, 1), - lunch=True) - - calculator = DSACalculator(self.travel) - calculator.calculate_dsa() - - self.assertEqual(calculator.total_dsa, 80) - self.assertEqual(calculator.total_deductions, 20) - self.assertEqual(calculator.paid_to_traveler, 60) - - self.assertEqual(calculator.detailed_dsa, - [{'daily_rate': Decimal('200'), - 'deduction': Decimal('20'), - 'dsa_region': self.budapest.id, - 'dsa_region_name': 'Hungary - Budapest', - 'end_date': date(2017, 1, 1), - 'night_count': 0, - 'paid_to_traveler': Decimal('60'), - 'start_date': date(2017, 1, 1), - 'total_amount': Decimal('80')}]) - - def test_case_2(self): - ItineraryItemFactory(travel=self.travel, - departure_date=datetime(2017, 1, 1, 1, 0, tzinfo=UTC), - arrival_date=datetime(2017, 1, 1, 2, 0, tzinfo=UTC), - dsa_region=self.budapest) - - ItineraryItemFactory(travel=self.travel, - departure_date=datetime(2017, 1, 1, 10, 0, tzinfo=UTC), - arrival_date=datetime(2017, 1, 1, 11, 0, tzinfo=UTC), - dsa_region=self.copenhagen) - - ItineraryItemFactory(travel=self.travel, - departure_date=datetime(2017, 1, 1, 22, 0, tzinfo=UTC), - arrival_date=datetime(2017, 1, 1, 23, 0, tzinfo=UTC), - dsa_region=self.dusseldorf) - - ItineraryItemFactory(travel=self.travel, - departure_date=datetime(2017, 1, 3, 10, 0, tzinfo=UTC), - arrival_date=datetime(2017, 1, 3, 13, 0, tzinfo=UTC), - dsa_region=self.amsterdam) - - DeductionFactory(travel=self.travel, - date=date(2017, 1, 1), - accomodation=True) - - DeductionFactory(travel=self.travel, - date=date(2017, 1, 2), - breakfast=True, - lunch=True, - dinner=True) - - DeductionFactory(travel=self.travel, - date=date(2017, 1, 3), - lunch=True) - - calculator = DSACalculator(self.travel) - calculator.calculate_dsa() - - self.assertEqual(calculator.total_dsa, 1160) - self.assertEqual(calculator.total_deductions, 460) - self.assertEqual(calculator.paid_to_traveler, 700) - - self.assertEqual(calculator.detailed_dsa, - [{'daily_rate': Decimal('400'), - 'deduction': Decimal('460'), - 'dsa_region': self.dusseldorf.id, - 'dsa_region_name': 'Germany - Duesseldorf', - 'end_date': date(2017, 1, 3), - 'night_count': 2, - 'paid_to_traveler': Decimal('700'), - 'start_date': date(2017, 1, 1), - 'total_amount': Decimal('1160')}]) - - def test_case_3(self): - ItineraryItemFactory(travel=self.travel, - departure_date=datetime(2017, 1, 1, 1, 0, tzinfo=UTC), - arrival_date=datetime(2017, 1, 1, 2, 0, tzinfo=UTC), - dsa_region=self.budapest) - - ItineraryItemFactory(travel=self.travel, - departure_date=datetime(2017, 1, 3, 1, 0, tzinfo=UTC), - arrival_date=datetime(2017, 1, 3, 2, 0, tzinfo=UTC), - dsa_region=self.copenhagen) - - ItineraryItemFactory(travel=self.travel, - departure_date=datetime(2017, 1, 3, 11, 0, tzinfo=UTC), - arrival_date=datetime(2017, 1, 3, 11, 30, tzinfo=UTC), - dsa_region=self.amsterdam) - - DeductionFactory(travel=self.travel, - date=date(2017, 1, 2), - breakfast=True) - - DeductionFactory(travel=self.travel, - date=date(2017, 1, 3), - breakfast=True) - - calculator = DSACalculator(self.travel) - calculator.calculate_dsa() - - self.assertEqual(calculator.total_dsa, 480) - self.assertEqual(calculator.total_deductions, 20) - self.assertEqual(calculator.paid_to_traveler, 460) - - self.assertEqual(calculator.detailed_dsa, - [{'daily_rate': Decimal('200'), - 'deduction': Decimal('20'), - 'dsa_region': self.budapest.id, - 'dsa_region_name': 'Hungary - Budapest', - 'end_date': date(2017, 1, 3), - 'night_count': 2, - 'paid_to_traveler': Decimal('460'), - 'start_date': date(2017, 1, 1), - 'total_amount': Decimal('480')}]) - - def test_case_4(self): - ItineraryItemFactory(travel=self.travel, - departure_date=datetime(2017, 1, 1, 10, 0, tzinfo=UTC), - arrival_date=datetime(2017, 1, 1, 11, 0, tzinfo=UTC), - dsa_region=self.budapest) - - ItineraryItemFactory(travel=self.travel, - departure_date=datetime(2017, 3, 4, 10, 0, tzinfo=UTC), - arrival_date=datetime(2017, 3, 4, 11, 0, tzinfo=UTC), - dsa_region=self.copenhagen) - - ItineraryItemFactory(travel=self.travel, - departure_date=datetime(2017, 3, 5, 10, 0, tzinfo=UTC), - arrival_date=datetime(2017, 3, 5, 10, 0, tzinfo=UTC), - dsa_region=self.budapest) - - ItineraryItemFactory(travel=self.travel, - departure_date=datetime(2017, 3, 7, 10, 0, tzinfo=UTC), - arrival_date=datetime(2017, 3, 7, 11, 0, tzinfo=UTC), - dsa_region=self.amsterdam) - - DeductionFactory(travel=self.travel, - date=date(2017, 2, 28), - dinner=True) - - DeductionFactory(travel=self.travel, - date=date(2017, 3, 2), - dinner=True) - - DeductionFactory(travel=self.travel, - date=date(2017, 3, 4), - dinner=True) - - DeductionFactory(travel=self.travel, - date=date(2017, 3, 6), - dinner=True) - - calculator = DSACalculator(self.travel) - calculator.calculate_dsa() - - self.assertEqual(calculator.total_dsa, 12708) - self.assertEqual(calculator.total_deductions, 93) - self.assertEqual(calculator.paid_to_traveler, 12615) - - self.assertEqual(calculator.detailed_dsa, - [{'daily_rate': Decimal('200'), - 'deduction': Decimal('30'), - 'dsa_region': self.budapest.id, - 'dsa_region_name': 'Hungary - Budapest', - 'end_date': date(2017, 3, 1), - 'night_count': 59, - 'paid_to_traveler': Decimal('11970'), - 'start_date': date(2017, 1, 1), - 'total_amount': Decimal('12000')}, - {'daily_rate': Decimal('120'), - 'deduction': Decimal('18'), - 'dsa_region': self.budapest.id, - 'dsa_region_name': 'Hungary - Budapest', - 'end_date': date(2017, 3, 3), - 'night_count': 1, - 'paid_to_traveler': Decimal('222'), - 'start_date': date(2017, 3, 2), - 'total_amount': Decimal('240')}, - {'daily_rate': Decimal('180'), - 'deduction': Decimal('27'), - 'dsa_region': self.copenhagen.id, - 'dsa_region_name': 'Denmark - Copenhagen', - 'end_date': date(2017, 3, 4), - 'night_count': 0, - 'paid_to_traveler': Decimal('153'), - 'start_date': date(2017, 3, 4), - 'total_amount': Decimal('180')}, - {'daily_rate': Decimal('120'), - 'deduction': Decimal('18'), - 'dsa_region': self.budapest.id, - 'dsa_region_name': 'Hungary - Budapest', - 'end_date': date(2017, 3, 7), - 'night_count': 2, - 'paid_to_traveler': Decimal('270'), - 'start_date': date(2017, 3, 5), - 'total_amount': Decimal('288')}]) - - def test_case_5(self): - ItineraryItemFactory(travel=self.travel, - departure_date=datetime(2016, 12, 31, 22, 0, tzinfo=UTC), - arrival_date=datetime(2017, 1, 1, 3, 0, tzinfo=UTC), - dsa_region=self.budapest, - overnight_travel=True) - - ItineraryItemFactory(travel=self.travel, - departure_date=datetime(2017, 1, 3, 23, 0, tzinfo=UTC), - arrival_date=datetime(2017, 1, 4, 4, 0, tzinfo=UTC), - dsa_region=self.amsterdam, - overnight_travel=True) - - DeductionFactory(travel=self.travel, - date=date(2017, 1, 2), - breakfast=True) - - DeductionFactory(travel=self.travel, - date=date(2017, 1, 3), - breakfast=True) - - calculator = DSACalculator(self.travel) - calculator.calculate_dsa() - - self.assertEqual(calculator.total_dsa, 480) - self.assertEqual(calculator.total_deductions, 20) - self.assertEqual(calculator.paid_to_traveler, 460) - - self.assertEqual(calculator.detailed_dsa, - [{'daily_rate': Decimal('200'), - 'deduction': Decimal('20'), - 'dsa_region': self.budapest.id, - 'dsa_region_name': 'Hungary - Budapest', - 'end_date': date(2017, 1, 3), - 'night_count': 2, - 'paid_to_traveler': Decimal('460'), - 'start_date': date(2017, 1, 1), - 'total_amount': Decimal('480')}]) - - def test_case_6(self): - ItineraryItemFactory(travel=self.travel, - departure_date=datetime(2017, 1, 1, 1, 0, tzinfo=UTC), - arrival_date=datetime(2017, 1, 1, 3, 0, tzinfo=UTC), - dsa_region=self.budapest) - - ItineraryItemFactory(travel=self.travel, - departure_date=datetime(2017, 1, 3, 12, 0, tzinfo=UTC), - arrival_date=datetime(2017, 1, 3, 14, 0, tzinfo=UTC), - dsa_region=self.amsterdam) - - DeductionFactory(travel=self.travel, - date=date(2017, 1, 3), - no_dsa=True) - - calculator = DSACalculator(self.travel) - calculator.calculate_dsa() - - self.assertEqual(calculator.total_dsa, 480) - self.assertEqual(calculator.total_deductions, 80) - self.assertEqual(calculator.paid_to_traveler, 400) - - self.assertEqual(calculator.detailed_dsa, - [{'daily_rate': Decimal('200'), - 'deduction': Decimal('80'), - 'dsa_region': self.budapest.id, - 'dsa_region_name': 'Hungary - Budapest', - 'end_date': date(2017, 1, 3), - 'night_count': 2, - 'paid_to_traveler': Decimal('400'), - 'start_date': date(2017, 1, 1), - 'total_amount': Decimal('480')}]) - - def test_case_7(self): - ItineraryItemFactory(travel=self.travel, - departure_date=datetime(2017, 1, 1, 1, 0, tzinfo=UTC), - arrival_date=datetime(2017, 1, 1, 3, 0, tzinfo=UTC), - dsa_region=self.budapest) - - ItineraryItemFactory(travel=self.travel, - departure_date=datetime(2017, 1, 1, 10, 0, tzinfo=UTC), - arrival_date=datetime(2017, 1, 1, 14, 0, tzinfo=UTC), - dsa_region=self.amsterdam) - - ItineraryItemFactory(travel=self.travel, - departure_date=datetime(2017, 1, 1, 21, 0, tzinfo=UTC), - arrival_date=datetime(2017, 1, 1, 22, 0, tzinfo=UTC), - dsa_region=self.budapest) - - calculator = DSACalculator(self.travel) - calculator.calculate_dsa() - - self.assertEqual(calculator.total_dsa, 0) - self.assertEqual(calculator.total_deductions, 0) - self.assertEqual(calculator.paid_to_traveler, 0) - - self.assertEqual(calculator.detailed_dsa, []) - - def test_ta_not_required(self): - self.travel.ta_required = False - self.travel.save() - - ItineraryItemFactory(travel=self.travel, - departure_date=datetime(2017, 1, 1, 1, 0, tzinfo=UTC), - arrival_date=datetime(2017, 1, 1, 2, 0, tzinfo=UTC), - dsa_region=self.budapest) - - ItineraryItemFactory(travel=self.travel, - departure_date=datetime(2017, 1, 1, 10, 0, tzinfo=UTC), - arrival_date=datetime(2017, 1, 1, 15, 0, tzinfo=UTC), - dsa_region=self.amsterdam) - - DeductionFactory(travel=self.travel, - date=date(2017, 1, 1), - lunch=True) - - calculator = DSACalculator(self.travel) - calculator.calculate_dsa() - - self.assertEqual(calculator.total_dsa, 0) - self.assertEqual(calculator.total_deductions, 0) - self.assertEqual(calculator.paid_to_traveler, 0) - self.assertEqual(calculator.detailed_dsa, []) diff --git a/src/etools/applications/t2f/tests/test_exports.py b/src/etools/applications/t2f/tests/test_exports.py index 26eee1cc51..c9247f7367 100644 --- a/src/etools/applications/t2f/tests/test_exports.py +++ b/src/etools/applications/t2f/tests/test_exports.py @@ -1,7 +1,6 @@ import csv import logging from datetime import datetime -from decimal import Decimal from django.urls import reverse from django.utils import timezone @@ -14,14 +13,12 @@ from etools.applications.partners.tests.factories import InterventionFactory from etools.applications.publics.tests.factories import ( PublicsAirlineCompanyFactory, - PublicsCurrencyFactory, PublicsDSARateFactory, PublicsDSARegionFactory, ) from etools.applications.reports.tests.factories import ResultFactory, SectionFactory from etools.applications.t2f.models import ModeOfTravel, TravelActivity, TravelType from etools.applications.t2f.tests.factories import ( - ExpenseFactory, ItineraryItemFactory, TravelActivityFactory, TravelAttachmentFactory, @@ -271,87 +268,6 @@ def test_activity_export(self): 'Yes', ]) - def test_finance_export(self): - currency_usd = PublicsCurrencyFactory(code="USD") - travel = TravelFactory(traveler=self.traveler, - supervisor=self.unicef_staff, - start_date=datetime(2016, 11, 20, tzinfo=UTC), - end_date=datetime(2016, 12, 5, tzinfo=UTC), - mode_of_travel=[ModeOfTravel.PLANE, ModeOfTravel.CAR, ModeOfTravel.RAIL]) - travel.expenses.all().delete() - ExpenseFactory(travel=travel, amount=Decimal( - '500'), currency=currency_usd) - - travel_2 = TravelFactory(traveler=self.traveler, - supervisor=self.unicef_staff, - start_date=datetime(2016, 11, 20, tzinfo=UTC), - end_date=datetime(2016, 12, 5, tzinfo=UTC), - mode_of_travel=None) - travel_2.expenses.all().delete() - ExpenseFactory(travel=travel_2, amount=Decimal( - '200'), currency=currency_usd) - ExpenseFactory(travel=travel_2, amount=Decimal('100'), currency=None) - - with self.assertNumQueries(27): - response = self.forced_auth_req('get', reverse('t2f:travels:list:finance_export'), - user=self.unicef_staff) - export_csv = csv.reader(StringIO(response.content.decode('utf-8'))) - rows = [r for r in export_csv] - - self.assertEqual(len(rows), 3) - - # check header - self.assertEqual(rows[0], - ['reference_number', - 'traveler', - 'office', - 'section', - 'status', - 'supervisor', - 'start_date', - 'end_date', - 'purpose_of_travel', - 'mode_of_travel', - 'international_travel', - 'require_ta', - 'dsa_total', - 'expense_total', - 'deductions_total']) - - self.assertEqual(rows[1], - ['{}/1'.format(datetime.now().year), - 'John Doe', - 'An Office', - travel.section.name, - 'planned', - 'Jakab Gipsz', - '20-Nov-2016', - '05-Dec-2016', - travel.purpose, - 'Plane, Car, Rail', - 'No', - 'Yes', - '0.00', - '500 USD', - '0.00']) - - self.assertEqual(rows[2], - ['{}/2'.format(datetime.now().year), - 'John Doe', - 'An Office', - travel_2.section.name, - 'planned', - 'Jakab Gipsz', - '20-Nov-2016', - '05-Dec-2016', - travel_2.purpose, - '', - 'No', - 'Yes', - '0.00', - '200 USD', - '0.00']) - def test_travel_admin_export(self): dsa_brd = PublicsDSARegionFactory(area_code='BRD') PublicsDSARateFactory(region=dsa_brd) diff --git a/src/etools/applications/t2f/tests/test_mailing.py b/src/etools/applications/t2f/tests/test_mailing.py index a8da7b52fd..1a424f1bbe 100644 --- a/src/etools/applications/t2f/tests/test_mailing.py +++ b/src/etools/applications/t2f/tests/test_mailing.py @@ -44,10 +44,8 @@ def test_mailing(self): def test_mailing_serializer(self): serializer = TravelMailSerializer(self.travel, context={}) self.assertKeysIn(['reference_number', - 'cost_summary', 'supervisor', 'end_date', - 'cost_assignments', 'rejection_note', 'currency', 'estimated_travel_cost', diff --git a/src/etools/applications/t2f/tests/test_overlapping_trips.py b/src/etools/applications/t2f/tests/test_overlapping_trips.py index 63a14d8c2b..0b0bdc7d7d 100644 --- a/src/etools/applications/t2f/tests/test_overlapping_trips.py +++ b/src/etools/applications/t2f/tests/test_overlapping_trips.py @@ -15,7 +15,6 @@ PublicsCurrencyFactory, PublicsDSARateFactory, PublicsDSARegionFactory, - PublicsTravelExpenseTypeFactory, ) from etools.applications.t2f.models import make_travel_reference_number, ModeOfTravel, Travel from etools.applications.t2f.tests.factories import ItineraryItemFactory, TravelFactory @@ -43,7 +42,6 @@ def setUpTestData(cls): supervisor=cls.unicef_staff, start_date=datetime(2017, 4, 4, 12, 00, tzinfo=UTC), end_date=datetime(2017, 4, 14, 16, 00, tzinfo=UTC)) - cls.travel.expenses.all().delete() ItineraryItemFactory(travel=cls.travel) ItineraryItemFactory(travel=cls.travel) cls.travel.submit_for_approval() @@ -54,8 +52,7 @@ def test_overlapping_trips(self): currency = PublicsCurrencyFactory() dsa_region = PublicsDSARegionFactory() - data = {'deductions': [], - 'itinerary': [{'origin': 'Berlin', + data = {'itinerary': [{'origin': 'Berlin', 'destination': 'Budapest', 'departure_date': '2017-04-07T17:06:55.821490', 'arrival_date': '2017-04-08T17:06:55.821490', @@ -72,7 +69,6 @@ def test_overlapping_trips(self): 'mode_of_travel': ModeOfTravel.RAIL, 'airlines': []}], 'activities': [], - 'cost_assignments': [], 'ta_required': True, 'international_travel': False, 'mode_of_travel': [ModeOfTravel.BOAT], @@ -102,12 +98,10 @@ def test_overlapping_trips(self): def test_almost_overlapping_trips(self): currency = PublicsCurrencyFactory() - expense_type = PublicsTravelExpenseTypeFactory() dsa_rate = PublicsDSARateFactory(effective_from_date=datetime(2017, 4, 10, 16, 00, tzinfo=UTC)) dsa_region = dsa_rate.region - data = {'deductions': [], - 'itinerary': [{'origin': 'Berlin', + data = {'itinerary': [{'origin': 'Berlin', 'destination': 'Budapest', 'departure_date': '2017-04-14T17:06:55.821490', 'arrival_date': '2017-04-15T17:06:55.821490', @@ -124,7 +118,6 @@ def test_almost_overlapping_trips(self): 'mode_of_travel': ModeOfTravel.RAIL, 'airlines': []}], 'activities': [], - 'cost_assignments': [], 'ta_required': True, 'international_travel': False, 'mode_of_travel': [ModeOfTravel.BOAT], @@ -134,11 +127,7 @@ def test_almost_overlapping_trips(self): 'end_date': '2017-05-22T15:02:13+00:00', 'currency': currency.id, 'purpose': 'Purpose', - 'additional_note': 'Notes', - 'expenses': [{'amount': '120', - 'type': expense_type.id, - 'currency': currency.id, - 'document_currency': currency.id}]} + 'additional_note': 'Notes'} response = self.forced_auth_req('post', reverse('t2f:travels:list:index'), data=data, user=self.traveler) @@ -157,8 +146,7 @@ def test_edit_to_overlap(self): currency = PublicsCurrencyFactory() dsa_region = PublicsDSARegionFactory() - data = {'deductions': [], - 'itinerary': [{'origin': 'Berlin', + data = {'itinerary': [{'origin': 'Berlin', 'destination': 'Budapest', 'departure_date': '2017-04-14T17:06:55.821490', 'arrival_date': '2017-04-15T17:06:55.821490', @@ -175,7 +163,6 @@ def test_edit_to_overlap(self): 'mode_of_travel': ModeOfTravel.RAIL, 'airlines': []}], 'activities': [], - 'cost_assignments': [], 'ta_required': True, 'international_travel': False, 'mode_of_travel': [ModeOfTravel.BOAT], @@ -248,8 +235,7 @@ def test_daylight_saving(self): currency = PublicsCurrencyFactory() dsa_region = PublicsDSARegionFactory() - data = {'deductions': [], - 'itinerary': [{'origin': 'Berlin', + data = {'itinerary': [{'origin': 'Berlin', 'destination': 'Budapest', 'departure_date': '2017-04-14T17:06:55.821490', 'arrival_date': '2017-04-15T17:06:55.821490', @@ -266,7 +252,6 @@ def test_daylight_saving(self): 'mode_of_travel': ModeOfTravel.RAIL, 'airlines': []}], 'activities': [], - 'cost_assignments': [], 'ta_required': True, 'international_travel': False, 'mode_of_travel': [ModeOfTravel.BOAT], diff --git a/src/etools/applications/t2f/tests/test_permission_matrix.py b/src/etools/applications/t2f/tests/test_permission_matrix.py index fb7cc9983d..a1b444aa97 100644 --- a/src/etools/applications/t2f/tests/test_permission_matrix.py +++ b/src/etools/applications/t2f/tests/test_permission_matrix.py @@ -12,7 +12,6 @@ from etools.applications.publics.tests.factories import ( PublicsCurrencyFactory, PublicsDSARegionFactory, - PublicsWBSFactory, ) from etools.applications.t2f import UserTypes from etools.applications.t2f.helpers.permission_matrix import ( @@ -157,9 +156,6 @@ def test_permission_aggregation(self, permission_matrix_getter): def test_travel_creation(self): dsa_region = PublicsDSARegionFactory() currency = PublicsCurrencyFactory() - wbs = PublicsWBSFactory() - grant = wbs.grants.first() - fund = grant.funds.first() location = LocationFactory() purpose = 'Some purpose to check later' @@ -196,10 +192,6 @@ def test_travel_creation(self): 'locations': [location.id], 'travel_type': TravelType.ADVOCACY, 'date': '2016-12-15T15:02:13+01:00'}], - 'cost_assignments': [{'wbs': wbs.id, - 'grant': grant.id, - 'fund': fund.id, - 'share': '100'}], 'ta_required': True, 'international_travel': False, 'mode_of_travel': [ModeOfTravel.BOAT], diff --git a/src/etools/applications/t2f/tests/test_state_machine.py b/src/etools/applications/t2f/tests/test_state_machine.py index 4ae7db0ca3..06f1d1d25b 100644 --- a/src/etools/applications/t2f/tests/test_state_machine.py +++ b/src/etools/applications/t2f/tests/test_state_machine.py @@ -10,8 +10,6 @@ PublicsBusinessAreaFactory, PublicsCurrencyFactory, PublicsDSARegionFactory, - PublicsTravelExpenseTypeFactory, - PublicsWBSFactory, ) from etools.applications.t2f.models import ModeOfTravel, Travel from etools.applications.t2f.tests.factories import TravelFactory @@ -57,28 +55,14 @@ def test_possible_transitions(self): def test_state_machine_flow(self): currency = PublicsCurrencyFactory() - expense_type = PublicsTravelExpenseTypeFactory() business_area = PublicsBusinessAreaFactory() dsa_region = PublicsDSARegionFactory() - wbs = PublicsWBSFactory(business_area=business_area) - grant = wbs.grants.first() - fund = grant.funds.first() - workspace = self.unicef_staff.profile.country workspace.business_area_code = business_area.code workspace.save() - data = {'cost_assignments': [{'wbs': wbs.id, - 'grant': grant.id, - 'fund': fund.id, - 'share': 100}], - 'deductions': [{'date': '2016-11-03', - 'breakfast': True, - 'lunch': True, - 'dinner': False, - 'accomodation': True}], - 'itinerary': [{'origin': 'Berlin', + data = {'itinerary': [{'origin': 'Berlin', 'destination': 'Budapest', 'departure_date': '2017-04-14T17:06:55.821490', 'arrival_date': '2017-04-15T17:06:55.821490', @@ -97,14 +81,9 @@ def test_state_machine_flow(self): 'traveler': self.traveler.id, 'ta_required': True, 'currency': currency.id, - 'supervisor': self.unicef_staff.id, - 'expenses': [{'amount': '120', - 'type': expense_type.id, - 'currency': currency.id, - 'document_currency': currency.id}]} + 'supervisor': self.unicef_staff.id} response = self.forced_auth_req('post', reverse('t2f:travels:list:index'), data=data, user=self.unicef_staff) response_json = json.loads(response.rendered_content) - self.assertEqual(response_json['cost_summary']['preserved_expenses'], None) travel_id = response_json['id'] @@ -154,28 +133,14 @@ def test_state_machine_flow(self): def test_state_machine_flow2(self): currency = PublicsCurrencyFactory() - expense_type = PublicsTravelExpenseTypeFactory() business_area = PublicsBusinessAreaFactory() dsa_region = PublicsDSARegionFactory() - wbs = PublicsWBSFactory(business_area=business_area) - grant = wbs.grants.first() - fund = grant.funds.first() - workspace = self.unicef_staff.profile.country workspace.business_area_code = business_area.code workspace.save() - data = {'cost_assignments': [{'wbs': wbs.id, - 'grant': grant.id, - 'fund': fund.id, - 'share': 100}], - 'deductions': [{'date': '2016-11-03', - 'breakfast': True, - 'lunch': True, - 'dinner': False, - 'accomodation': True}], - 'itinerary': [{'origin': 'Berlin', + data = {'itinerary': [{'origin': 'Berlin', 'destination': 'Budapest', 'departure_date': '2017-04-14T17:06:55.821490', 'arrival_date': '2017-04-15T17:06:55.821490', @@ -195,13 +160,9 @@ def test_state_machine_flow2(self): 'ta_required': True, 'currency': currency.id, 'supervisor': self.unicef_staff.id, - 'expenses': [{'amount': '120', - 'type': expense_type.id, - 'currency': currency.id, - 'document_currency': currency.id}]} + } response = self.forced_auth_req('post', reverse('t2f:travels:list:index'), data=data, user=self.unicef_staff) response_json = json.loads(response.rendered_content) - self.assertEqual(response_json['cost_summary']['preserved_expenses'], None) travel_id = response_json['id'] @@ -349,24 +310,11 @@ def test_expense_required_on_send_for_payment(self): dsa_region = PublicsDSARegionFactory() currency = PublicsCurrencyFactory() - wbs = PublicsWBSFactory(business_area=business_area) - grant = wbs.grants.first() - fund = grant.funds.first() - workspace = self.unicef_staff.profile.country workspace.business_area_code = business_area.code workspace.save() - data = {'cost_assignments': [{'wbs': wbs.id, - 'grant': grant.id, - 'fund': fund.id, - 'share': 100}], - 'deductions': [{'date': '2016-11-03', - 'breakfast': True, - 'lunch': True, - 'dinner': False, - 'accomodation': True}], - 'itinerary': [{'origin': 'Berlin', + data = {'itinerary': [{'origin': 'Berlin', 'destination': 'Budapest', 'departure_date': '2017-04-14T17:06:55.821490', 'arrival_date': '2017-04-15T17:06:55.821490', @@ -385,11 +333,9 @@ def test_expense_required_on_send_for_payment(self): 'traveler': self.traveler.id, 'ta_required': True, 'supervisor': self.unicef_staff.id, - 'expenses': [], 'currency': currency.id} response = self.forced_auth_req('post', reverse('t2f:travels:list:index'), data=data, user=self.unicef_staff) response_json = json.loads(response.rendered_content) - self.assertEqual(response_json['cost_summary']['preserved_expenses'], None) travel_id = response_json['id'] diff --git a/src/etools/applications/t2f/tests/test_travel.py b/src/etools/applications/t2f/tests/test_travel.py deleted file mode 100644 index 07a9bb77a8..0000000000 --- a/src/etools/applications/t2f/tests/test_travel.py +++ /dev/null @@ -1,68 +0,0 @@ - -from etools.applications.EquiTrack.tests.cases import BaseTenantTestCase -from etools.applications.publics.models import TravelExpenseType -from etools.applications.publics.tests.factories import PublicsCurrencyFactory, PublicsTravelExpenseTypeFactory -from etools.applications.t2f.models import Expense, Travel -from etools.applications.users.tests.factories import UserFactory - - -class TravelMethods(BaseTenantTestCase): - @classmethod - def setUpTestData(cls): - cls.unicef_staff = UserFactory(is_staff=True) - cls.traveler = UserFactory() - - profile = cls.traveler.profile - profile.vendor_number = 'user0001' - profile.save() - - country = profile.country - country.business_area_code = '0060' - country.save() - - def test_cost_summary(self): - # Currencies - huf = PublicsCurrencyFactory(name='HUF', code='huf') - - # Expense types - et_t_food = PublicsTravelExpenseTypeFactory( - title='Food', - vendor_number=TravelExpenseType.USER_VENDOR_NUMBER_PLACEHOLDER - ) - et_t_travel = PublicsTravelExpenseTypeFactory( - title='Travel', - vendor_number=TravelExpenseType.USER_VENDOR_NUMBER_PLACEHOLDER - ) - et_t_other = PublicsTravelExpenseTypeFactory( - title='Other', - vendor_number=TravelExpenseType.USER_VENDOR_NUMBER_PLACEHOLDER - ) - - # Make a travel - travel = Travel.objects.create(traveler=self.traveler, - supervisor=self.unicef_staff, - currency=huf) - - # Add expenses - Expense.objects.create(travel=travel, - type=et_t_food, - currency=huf, - amount=35) - - Expense.objects.create(travel=travel, - type=et_t_travel, - currency=huf, - amount=50) - - Expense.objects.create(travel=travel, - type=et_t_other, - currency=huf, - amount=15) - - Expense.objects.create(travel=travel, - type=et_t_travel, - currency=huf, - amount=None) - - # This should not raise 500 - travel.cost_summary diff --git a/src/etools/applications/t2f/tests/test_travel_details.py b/src/etools/applications/t2f/tests/test_travel_details.py index 540354907a..8f633e5c59 100644 --- a/src/etools/applications/t2f/tests/test_travel_details.py +++ b/src/etools/applications/t2f/tests/test_travel_details.py @@ -10,16 +10,9 @@ from etools.applications.EquiTrack.tests.cases import BaseTenantTestCase from etools.applications.EquiTrack.tests.mixins import URLAssertionMixin from etools.applications.partners.models import PartnerType -from etools.applications.partners.tests.factories import InterventionFactory, PartnerFactory +from etools.applications.partners.tests.factories import PartnerFactory from etools.applications.publics.models import DSARegion -from etools.applications.publics.tests.factories import ( - PublicsAirlineCompanyFactory, - PublicsBusinessAreaFactory, - PublicsCurrencyFactory, - PublicsDSARegionFactory, - PublicsTravelExpenseTypeFactory, - PublicsWBSFactory, -) +from etools.applications.publics.tests.factories import PublicsAirlineCompanyFactory, PublicsDSARegionFactory from etools.applications.t2f.models import ModeOfTravel, Travel, TravelAttachment, TravelType from etools.applications.t2f.tests.factories import TravelAttachmentFactory, TravelFactory from etools.applications.users.tests.factories import UserFactory @@ -58,18 +51,18 @@ def test_urls(self): self.assertIntParamRegexes(names_and_paths, 't2f:travels:details:') def test_details_view(self): - with self.assertNumQueries(22): + with self.assertNumQueries(14): response = self.forced_auth_req('get', reverse('t2f:travels:details:index', kwargs={'travel_pk': self.travel.id}), user=self.unicef_staff) response_json = json.loads(response.rendered_content) - self.assertKeysIn(['cancellation_note', 'supervisor', 'attachments', 'office', 'expenses', 'ta_required', + self.assertKeysIn(['cancellation_note', 'supervisor', 'attachments', 'office', 'ta_required', 'completed_at', 'certification_note', 'misc_expenses', 'traveler', 'id', 'additional_note', - 'section', 'cost_assignments', 'start_date', 'status', 'activities', + 'section', 'start_date', 'status', 'activities', 'rejection_note', 'end_date', 'mode_of_travel', 'international_travel', - 'first_submission_date', 'deductions', 'purpose', 'report', 'itinerary', - 'reference_number', 'cost_summary', 'currency', 'canceled_at', 'estimated_travel_cost'], + 'first_submission_date', 'purpose', 'report', 'itinerary', + 'reference_number', 'currency', 'canceled_at', 'estimated_travel_cost'], response_json, exact=True) @@ -79,7 +72,7 @@ def test_details_view_with_file(self): name=u'\u0628\u0631\u0646\u0627\u0645\u062c \u062a\u062f\u0631\u064a\u0628 \u0627\u0644\u0645\u062a\u0627\u0628\u0639\u064a\u0646.pdf', # noqa file=factory.django.FileField(filename=u'travels/lebanon/24800/\u0628\u0631\u0646\u0627\u0645\u062c_\u062a\u062f\u0631\u064a\u0628_\u0627\u0644\u0645\u062a\u0627\u0628\u0639\u064a\u0646.pdf') # noqa ) - with self.assertNumQueries(23): + with self.assertNumQueries(15): response = self.forced_auth_req( 'get', reverse('t2f:travels:details:index', args=[self.travel.pk]), @@ -88,12 +81,12 @@ def test_details_view_with_file(self): response_json = json.loads(response.rendered_content) self.assertKeysIn( - ['cancellation_note', 'supervisor', 'attachments', 'office', 'expenses', 'ta_required', + ['cancellation_note', 'supervisor', 'attachments', 'office', 'ta_required', 'completed_at', 'certification_note', 'misc_expenses', 'traveler', 'id', 'additional_note', - 'section', 'cost_assignments', 'start_date', 'status', 'activities', + 'section', 'start_date', 'status', 'activities', 'rejection_note', 'end_date', 'mode_of_travel', 'international_travel', 'itinerary', - 'first_submission_date', 'deductions', 'purpose', 'report', - 'reference_number', 'cost_summary', 'currency', 'canceled_at', 'estimated_travel_cost'], + 'first_submission_date', 'purpose', 'report', + 'reference_number', 'currency', 'canceled_at', 'estimated_travel_cost'], response_json, exact=True ) @@ -110,7 +103,7 @@ def test_details_view_with_attachment(self): code="t2f_travel_attachment", content_object=attachment, ) - with self.assertNumQueries(24): + with self.assertNumQueries(16): response = self.forced_auth_req( 'get', reverse('t2f:travels:details:index', args=[self.travel.pk]), @@ -222,40 +215,6 @@ def test_add_attachment(self): self.assertKeysIn(expected_keys, response_json) self.assertTrue(attachment_qs.exists()) - def test_patch_request(self): - currency = PublicsCurrencyFactory() - expense_type = PublicsTravelExpenseTypeFactory() - - data = {'cost_assignments': [], - 'deductions': [{'date': '2016-11-03', - 'breakfast': True, - 'lunch': True, - 'dinner': False, - 'accomodation': True}], - 'traveler': self.traveler.id, - 'ta_required': True, - 'supervisor': self.unicef_staff.id, - 'expenses': [{'amount': '120', - 'type': expense_type.id, - 'document_currency': currency.id}]} - response = self.forced_auth_req('post', reverse('t2f:travels:list:index'), data=data, user=self.unicef_staff) - response_json = json.loads(response.rendered_content) - self.assertEqual(response_json['expenses'][0]['currency'], response_json['expenses'][0]['document_currency']) - self.assertEqual(response_json['cost_summary']['preserved_expenses'], None) - - travel_id = response_json['id'] - - data = {'expenses': response_json['expenses']} - data['expenses'].append({'amount': '200', - 'type': expense_type.id, - 'currency': currency.id, - 'document_currency': currency.id}) - response = self.forced_auth_req('patch', reverse('t2f:travels:details:index', - kwargs={'travel_pk': travel_id}), - data=data, user=self.unicef_staff) - response_json = json.loads(response.rendered_content) - self.assertEqual(len(response_json['deductions']), 1) - def test_duplication(self): data = {'traveler': self.unicef_staff.id} response = self.forced_auth_req('post', reverse('t2f:travels:details:clone_for_driver', @@ -280,10 +239,7 @@ def test_airlines(self): airlines_2 = PublicsAirlineCompanyFactory() airlines_3 = PublicsAirlineCompanyFactory() - data = {'cost_assignments': [], - 'deductions': [], - 'expenses': [], - 'itinerary': [{'origin': 'Budapest', + data = {'itinerary': [{'origin': 'Budapest', 'destination': 'Berlin', 'departure_date': '2016-11-16T12:06:55.821490', 'arrival_date': '2016-11-17T12:06:55.821490', @@ -296,7 +252,6 @@ def test_airlines(self): 'supervisor': self.unicef_staff.id} response = self.forced_auth_req('post', reverse('t2f:travels:list:index'), data=data, user=self.traveler) response_json = json.loads(response.rendered_content) - self.assertEqual(response_json['cost_summary']['preserved_expenses'], None) travel_id = response_json['id'] @@ -308,171 +263,12 @@ def test_airlines(self): response_json = json.loads(response.rendered_content) self.assertEqual(sorted(response_json['itinerary'][0]['airlines']), sorted([airlines_1.id, airlines_3.id])) - def test_preserved_expenses(self): - currency = PublicsCurrencyFactory() - expense_type = PublicsTravelExpenseTypeFactory() - dsa_region = PublicsDSARegionFactory() - - data = {'cost_assignments': [], - 'deductions': [{'date': '2016-11-03', - 'breakfast': True, - 'lunch': True, - 'dinner': False, - 'accomodation': True}], - 'itinerary': [{'origin': 'Berlin', - 'destination': 'Budapest', - 'departure_date': '2017-04-14T17:06:55.821490', - 'arrival_date': '2017-04-15T17:06:55.821490', - 'dsa_region': dsa_region.id, - 'overnight_travel': False, - 'mode_of_travel': ModeOfTravel.RAIL, - 'airlines': []}, - {'origin': 'Budapest', - 'destination': 'Berlin', - 'departure_date': '2017-05-20T12:06:55.821490', - 'arrival_date': '2017-05-21T12:06:55.821490', - 'dsa_region': dsa_region.id, - 'overnight_travel': False, - 'mode_of_travel': ModeOfTravel.RAIL, - 'airlines': []}], - 'traveler': self.traveler.id, - 'ta_required': True, - 'supervisor': self.unicef_staff.id, - 'expenses': [{'amount': '120', - 'type': expense_type.id, - 'currency': currency.id, - 'document_currency': currency.id}]} - response = self.forced_auth_req('post', reverse('t2f:travels:list:index'), data=data, user=self.unicef_staff) - response_json = json.loads(response.rendered_content) - self.assertEqual(response_json['cost_summary']['preserved_expenses'], None) - - travel_id = response_json['id'] - - response = self.forced_auth_req('post', reverse('t2f:travels:details:state_change', - kwargs={'travel_pk': travel_id, - 'transition_name': Travel.SUBMIT_FOR_APPROVAL}), - data=data, user=self.unicef_staff) - response_json = json.loads(response.rendered_content) - self.assertEqual(response_json['cost_summary']['preserved_expenses'], None) - - response = self.forced_auth_req('post', reverse('t2f:travels:details:state_change', - kwargs={'travel_pk': travel_id, - 'transition_name': Travel.APPROVE}), - data=data, user=self.unicef_staff) - response_json = json.loads(response.rendered_content) - self.assertEqual(response_json['cost_summary']['preserved_expenses'], None) - - def test_detailed_expenses(self): - currency = PublicsCurrencyFactory() - user_et = PublicsTravelExpenseTypeFactory(vendor_number='user') - travel_agent_1_et = PublicsTravelExpenseTypeFactory(vendor_number='ta1') - travel_agent_2_et = PublicsTravelExpenseTypeFactory(vendor_number='ta2') - parking_money_et = PublicsTravelExpenseTypeFactory(vendor_number='') - - data = {'cost_assignments': [], - 'traveler': self.traveler.id, - 'supervisor': self.unicef_staff.id, - 'ta_required': True, - 'expenses': [{'amount': '120', - 'type': user_et.id, - 'currency': currency.id, - 'document_currency': currency.id}, - {'amount': '80', - 'type': user_et.id, - 'currency': currency.id, - 'document_currency': currency.id}, - {'amount': '100', - 'type': travel_agent_1_et.id, - 'currency': currency.id, - 'document_currency': currency.id}, - {'amount': '500', - 'type': travel_agent_2_et.id, - 'currency': currency.id, - 'document_currency': currency.id}, - {'amount': '1000', - 'type': parking_money_et.id, - 'currency': currency.id, - 'document_currency': currency.id}]} - response = self.forced_auth_req('post', reverse('t2f:travels:list:index'), data=data, user=self.unicef_staff) - response_json = json.loads(response.rendered_content) - self.assertEqual(response_json['cost_summary']['expenses'], - [{'amount': '120.00', - 'currency': currency.id, - 'label': user_et.title, - 'vendor_number': 'Traveler'}, - {'amount': '80.00', - 'currency': currency.id, - 'label': user_et.title, - 'vendor_number': 'Traveler'}, - {'amount': '100.00', - 'currency': currency.id, - 'label': travel_agent_1_et.title, - 'vendor_number': 'ta1'}, - {'amount': '500.00', - 'currency': currency.id, - 'label': travel_agent_2_et.title, - 'vendor_number': 'ta2'}, - {'amount': '1000.00', - 'currency': currency.id, - 'label': parking_money_et.title, - 'vendor_number': ''}]) - - def test_cost_assignments(self): - wbs = PublicsWBSFactory() - grant = wbs.grants.first() - fund = grant.funds.first() - business_area = PublicsBusinessAreaFactory() - dsa_region = PublicsDSARegionFactory() - - data = {'cost_assignments': [{'wbs': wbs.id, - 'fund': fund.id, - 'grant': grant.id, - 'share': 55}], - 'ta_required': True} - response = self.forced_auth_req('post', reverse('t2f:travels:list:state_change', - kwargs={'transition_name': 'save_and_submit'}), - data=data, user=self.traveler) - response_json = json.loads(response.rendered_content) - self.assertEqual(response_json, {'cost_assignments': ['Shares should add up to 100%']}) - - data = {'cost_assignments': [{'wbs': wbs.id, - 'fund': fund.id, - 'grant': grant.id, - 'share': 100, - 'business_area': business_area.id, - 'delegate': False}], - 'itinerary': [{'origin': 'Berlin', - 'destination': 'Budapest', - 'departure_date': '2017-04-14T17:06:55.821490', - 'arrival_date': '2017-04-15T17:06:55.821490', - 'dsa_region': dsa_region.id, - 'overnight_travel': False, - 'mode_of_travel': ModeOfTravel.RAIL, - 'airlines': []}, - {'origin': 'Budapest', - 'destination': 'Berlin', - 'departure_date': '2017-05-20T12:06:55.821490', - 'arrival_date': '2017-05-21T12:06:55.821490', - 'dsa_region': dsa_region.id, - 'overnight_travel': False, - 'mode_of_travel': ModeOfTravel.RAIL, - 'airlines': []}], - 'ta_required': True, - 'supervisor': self.unicef_staff.id} - response = self.forced_auth_req('post', reverse('t2f:travels:list:state_change', - kwargs={'transition_name': 'save_and_submit'}), - data=data, user=self.traveler) - response_json = json.loads(response.rendered_content) - self.assertKeysIn(['wbs', 'fund', 'grant', 'share', 'business_area', 'delegate'], - response_json['cost_assignments'][0]) - def test_activity_location(self): location = LocationFactory() location_2 = LocationFactory() location_3 = LocationFactory() - data = {'cost_assignments': [], - 'activities': [{'is_primary_traveler': True, + data = {'activities': [{'is_primary_traveler': True, 'locations': [location.id, location_2.id]}], 'traveler': self.traveler.id} response = self.forced_auth_req('post', reverse('t2f:travels:list:index'), data=data, @@ -494,14 +290,15 @@ def test_activity_results(self): location = LocationFactory() location_2 = LocationFactory() - data = {'cost_assignments': [], - 'activities': [{ - 'is_primary_traveler': True, - 'locations': [location.id, location_2.id], - 'partner': PartnerFactory(partner_type=PartnerType.GOVERNMENT).id, - 'travel_type': TravelType.PROGRAMME_MONITORING, - }], - 'traveler': self.traveler.id} + data = { + 'activities': [{ + 'is_primary_traveler': True, + 'locations': [location.id, location_2.id], + 'partner': PartnerFactory(partner_type=PartnerType.GOVERNMENT).id, + 'travel_type': TravelType.PROGRAMME_MONITORING + }], + 'traveler': self.traveler.id + } response = self.forced_auth_req('post', reverse('t2f:travels:list:index'), data=data, user=self.traveler) @@ -513,10 +310,7 @@ def test_itinerary_dates(self): dsaregion = DSARegion.objects.first() airlines = PublicsAirlineCompanyFactory() - data = {'cost_assignments': [], - 'deductions': [], - 'expenses': [], - 'itinerary': [{'origin': 'Budapest', + data = {'itinerary': [{'origin': 'Budapest', 'destination': 'Berlin', 'departure_date': '2016-11-16T12:06:55.821490', 'arrival_date': '2016-11-17T12:06:55.821490', @@ -540,10 +334,7 @@ def test_itinerary_dates(self): self.assertEqual(response_json, {'itinerary': ['Itinerary items have to be ordered by date']}) def test_itinerary_submit_fail(self): - data = {'cost_assignments': [], - 'deductions': [], - 'expenses': [], - 'itinerary': [], + data = {'itinerary': [], 'activities': []} response = self.forced_auth_req('post', reverse('t2f:travels:list:index'), data=data, user=self.traveler) @@ -560,10 +351,7 @@ def test_itinerary_origin_destination(self): dsaregion = DSARegion.objects.first() airlines = PublicsAirlineCompanyFactory() - data = {'cost_assignments': [], - 'deductions': [], - 'expenses': [], - 'itinerary': [{'origin': 'Berlin', + data = {'itinerary': [{'origin': 'Berlin', 'destination': 'Budapest', 'departure_date': '2016-11-15T12:06:55.821490', 'arrival_date': '2016-11-16T12:06:55.821490', @@ -591,10 +379,7 @@ def test_itinerary_dsa_regions(self): dsaregion = DSARegion.objects.first() airlines = PublicsAirlineCompanyFactory() - data = {'cost_assignments': [], - 'deductions': [], - 'expenses': [], - 'itinerary': [{'origin': 'Budapest', + data = {'itinerary': [{'origin': 'Budapest', 'destination': 'Berlin', 'departure_date': '2016-11-15T12:06:55.821490', 'arrival_date': '2016-11-16T12:06:55.821490', @@ -625,10 +410,7 @@ def test_itinerary_dsa_regions(self): self.assertEqual(response_json, {'non_field_errors': ['All itinerary items has to have DSA region assigned']}) # Non ta trip - data = {'cost_assignments': [], - 'deductions': [], - 'expenses': [], - 'itinerary': [{'origin': 'Budapest', + data = {'itinerary': [{'origin': 'Budapest', 'destination': 'Berlin', 'departure_date': '2016-11-15T12:06:55.821490', 'arrival_date': '2016-11-16T12:06:55.821490', @@ -659,10 +441,7 @@ def test_itinerary_dsa_regions(self): self.assertEqual(response.status_code, 200) def test_activity_locations(self): - data = {'cost_assignments': [], - 'deductions': [], - 'expenses': [], - 'itinerary': [], + data = {'itinerary': [], 'activities': [{}], 'traveler': self.traveler.id} response = self.forced_auth_req('post', reverse('t2f:travels:list:index'), data=data, @@ -690,8 +469,6 @@ def test_reversed_itinerary_order(self): 'mode_of_travel': 'car'}], 'activities': [{'is_primary_traveler': True, 'locations': []}], - 'cost_assignments': [], - 'expenses': [], 'action_points': [], 'ta_required': True, 'international_travel': False, @@ -735,8 +512,6 @@ def test_incorrect_itinerary_order(self): 'locations': [] } ], - 'cost_assignments': [], - 'expenses': [], 'action_points': [], 'ta_required': True, 'international_travel': False, @@ -754,8 +529,6 @@ def test_ta_not_required(self): data = {'itinerary': [], 'activities': [{'is_primary_traveler': True, 'locations': []}], - 'cost_assignments': [], - 'expenses': [{}], 'action_points': [], 'ta_required': False, 'international_travel': False, @@ -773,8 +546,6 @@ def test_not_primary_traveler(self): data = {'itinerary': [], 'activities': [{'is_primary_traveler': False, 'locations': []}], - 'cost_assignments': [], - 'expenses': [{}], 'action_points': [], 'ta_required': False, 'international_travel': False, @@ -790,8 +561,6 @@ def test_not_primary_traveler(self): 'activities': [{'is_primary_traveler': False, 'primary_traveler': primary_traveler.id, 'locations': []}], - 'cost_assignments': [], - 'expenses': [{}], 'action_points': [], 'ta_required': False, 'international_travel': False, @@ -802,39 +571,11 @@ def test_not_primary_traveler(self): data=data, user=self.unicef_staff) self.assertEqual(response.status_code, 201) - def test_travel_activity_partnership(self): - partnership = InterventionFactory() - - data = {'itinerary': [], - 'activities': [{'is_primary_traveler': True, - 'locations': [], - 'partnership': partnership.id}], - 'cost_assignments': [], - 'expenses': [], - 'action_points': [], - 'ta_required': True, - 'international_travel': False, - 'traveler': self.traveler.id, - 'mode_of_travel': []} - - # Check only if 200 - response = self.forced_auth_req('post', reverse('t2f:travels:list:index'), - data=data, user=self.unicef_staff) - self.assertEqual(response.status_code, 201) - - response_json = json.loads(response.rendered_content) - activity = response_json['activities'][0] - - self.assertEqual(activity['partnership'], partnership.id) - def test_ghost_data_existence(self): dsa_region = DSARegion.objects.first() airline = PublicsAirlineCompanyFactory() - data = {'cost_assignments': [], - 'deductions': [], - 'expenses': [], - 'itinerary': [{'origin': 'Budapest', + data = {'itinerary': [{'origin': 'Budapest', 'destination': 'Berlin', 'departure_date': '2016-11-16T12:06:55.821490', 'arrival_date': '2016-11-17T12:06:55.821490', @@ -861,10 +602,7 @@ def test_save_with_ghost_data(self): dsa_region = DSARegion.objects.first() airline = PublicsAirlineCompanyFactory() - data = {'cost_assignments': [], - 'deductions': [], - 'expenses': [], - 'itinerary': [{'origin': 'Budapest', + data = {'itinerary': [{'origin': 'Budapest', 'destination': 'Berlin', 'departure_date': '2016-11-16T12:06:55.821490', 'arrival_date': '2016-11-17T12:06:55.821490', diff --git a/src/etools/applications/t2f/tests/test_travel_list.py b/src/etools/applications/t2f/tests/test_travel_list.py index c52e5f2afb..1312ff2e66 100644 --- a/src/etools/applications/t2f/tests/test_travel_list.py +++ b/src/etools/applications/t2f/tests/test_travel_list.py @@ -11,7 +11,7 @@ from etools.applications.EquiTrack.tests.cases import BaseTenantTestCase from etools.applications.EquiTrack.tests.mixins import URLAssertionMixin from etools.applications.publics.models import DSARegion -from etools.applications.publics.tests.factories import PublicsCurrencyFactory, PublicsWBSFactory +from etools.applications.publics.tests.factories import PublicsCurrencyFactory from etools.applications.reports.tests.factories import ResultFactory from etools.applications.t2f.models import make_travel_reference_number, ModeOfTravel, Travel, TravelType from etools.applications.t2f.tests.factories import TravelActivityFactory, TravelFactory @@ -257,9 +257,6 @@ def test_show_hidden(self): def test_travel_creation(self): dsaregion = DSARegion.objects.first() currency = PublicsCurrencyFactory() - wbs = PublicsWBSFactory() - grant = wbs.grants.first() - fund = grant.funds.first() location = LocationFactory() data = {'0': {}, @@ -269,18 +266,6 @@ def test_travel_creation(self): 'dinner': False, 'accomodation': False, 'no_dsa': False}, - 'deductions': [{'date': '2016-12-15', - 'breakfast': False, - 'lunch': False, - 'dinner': False, - 'accomodation': False, - 'no_dsa': False}, - {'date': '2016-12-16', - 'breakfast': False, - 'lunch': False, - 'dinner': False, - 'accomodation': False, - 'no_dsa': False}], 'itinerary': [{'airlines': [], 'overnight_travel': False, 'origin': 'a', @@ -293,10 +278,6 @@ def test_travel_creation(self): 'locations': [location.id], 'travel_type': TravelType.ADVOCACY, 'date': '2016-12-15T15:02:13+01:00'}], - 'cost_assignments': [{'wbs': wbs.id, - 'grant': grant.id, - 'fund': fund.id, - 'share': '100'}], 'ta_required': True, 'international_travel': False, 'mode_of_travel': [ModeOfTravel.BOAT], From 4a9fc2061c286b275342396eba1d24ac3f715f7a Mon Sep 17 00:00:00 2001 From: robertavram Date: Mon, 28 Jan 2019 16:58:53 -0500 Subject: [PATCH 03/72] testing --- Dockerfile | 86 +++++++++++++++-------- Pipfile | 18 +---- requirements.txt | 109 +++++++++++++++++++++++++++++ src/etools/config/settings/base.py | 2 +- 4 files changed, 167 insertions(+), 48 deletions(-) create mode 100644 requirements.txt diff --git a/Dockerfile b/Dockerfile index 22f1b7a652..d5f0bcc6a6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,35 +1,20 @@ -FROM python:3.6.4-jessie +FROM python:3.6.4-alpine as builder # python:3.6.4-jessie has python 2.7 and 3.6 installed, and packages # available to install 3.4 # Install dependencies -RUN apt-get update -RUN apt-get install -y --no-install-recommends \ - build-essential \ - libcurl4-openssl-dev \ - libjpeg-dev \ - vim \ - ntp \ - libpq-dev -RUN apt-get install -y --no-install-recommends \ - git-core -RUN apt-get install -y --no-install-recommends \ - python3-dev \ - python-software-properties \ - python-setuptools -RUN apt-get install -y --no-install-recommends \ - postgresql-client \ - libpq-dev \ - python-psycopg2 -RUN apt-get install -y --no-install-recommends \ - python-gdal \ - gdal-bin \ - libgdal-dev \ - libgdal1h \ - libgdal1-dev \ +RUN apk update +RUN apk add \ + --update alpine-sdk +RUN apk add \ libxml2-dev \ libxslt-dev \ - xmlsec1 + xmlsec-dev + +RUN apk add postgresql-dev +RUN apk add libffi-dev +RUN apk add jpeg-dev + RUN pip install --upgrade \ setuptools \ @@ -37,19 +22,58 @@ RUN pip install --upgrade \ wheel \ pipenv +RUN echo "http://dl-3.alpinelinux.org/alpine/edge/main" >> /etc/apk/repositories +RUN apk update +RUN apk add --upgrade apk-tools +RUN apk add openssl +RUN apk add ca-certificates +RUN apk add libressl2.7-libcrypto +RUN apk add gdal --update-cache --repository http://dl-3.alpinelinux.org/alpine/edge/testing/ +RUN apk add gdal-dev --update-cache --repository http://dl-3.alpinelinux.org/alpine/edge/testing/ +RUN apk add py-gdal --update-cache --repository http://dl-3.alpinelinux.org/alpine/edge/testing/ +RUN apk add geos --update-cache --repository http://dl-3.alpinelinux.org/alpine/edge/testing/ +RUN apk add geos-dev --update-cache --repository http://dl-3.alpinelinux.org/alpine/edge/testing/ +RUN apk add gcc --update-cache --repository http://dl-3.alpinelinux.org/alpine/edge/testing/ +RUN apk add g++ --update-cache --repository http://dl-3.alpinelinux.org/alpine/edge/testing/ + +#RUN apk add g++ +#RUN apk add py-setuptools + + # http://gis.stackexchange.com/a/74060 ENV CPLUS_INCLUDE_PATH /usr/include/gdal ENV C_INCLUDE_PATH /usr/include/gdal -ADD Pipfile.lock / -RUN pipenv install --system --deploy --ignore-pipfile +WORKDIR /etools/ +ADD Pipfile.lock . +ADD Pipfile . +ADD requirements.txt . +#RUN pipenv lock -r > requirements.txt +#RUN cat requirements.txt +RUN pip wheel --wheel-dir=/tmp/etwheels -r requirements.txt + +FROM python:3.6.4-alpine + +RUN echo "http://dl-3.alpinelinux.org/alpine/edge/main" >> /etc/apk/repositories +RUN apk update +RUN apk add --upgrade apk-tools +RUN apk add postgresql-client +RUN apk add openssl +RUN apk add ca-certificates +RUN apk add libressl2.7-libcrypto +RUN apk add gdal --update-cache --repository http://dl-3.alpinelinux.org/alpine/edge/testing/ -ENV PYTHONUNBUFFERED 1 ADD src /code/ -ADD manage.py /code/manage.py +ADD manage.py /code/manage.py +ENV PYTHONUNBUFFERED 1 ENV PYTHONPATH /code - WORKDIR /code/ +COPY --from=builder /tmp/etwheels /tmp/etwheels +COPY --from=builder /etools/requirements.txt /code/requirements.txt +RUN pip install --no-index --find-links=/tmp/etwheels -r /code/requirements.txt + +RUN rm -rf /tmp/etwheels + ENV DJANGO_SETTINGS_MODULE etools.config.settings.production RUN SECRET_KEY=not-so-secret-key-just-for-collectstatic DISABLE_JWT_LOGIN=1 python manage.py collectstatic --noinput diff --git a/Pipfile b/Pipfile index cd323b54b1..571101d49c 100644 --- a/Pipfile +++ b/Pipfile @@ -3,20 +3,6 @@ name = "pypi" url = "https://pypi.org/simple" verify_ssl = true -[dev-packages] -flake8 = "*" -coverage = "*" -mock = "*" -freezegun = "*" -responses = "*" -isort = "*" -ipython = "*" -pdbpp = "*" -tox = "*" -drf-api-checker = "*" -factory_boy = "*" -django-extensions = "*" -sphinx = "*" [packages] amqp = "==2.2.2" @@ -107,6 +93,7 @@ unicef-locations = "==1.5" unicodecsv = "==0.14.1" uritemplate = "==3.0.0" vine = "==1.1.4" +GDAL = "==2.4.0" webencodings = "==0.5.1" xhtml2pdf = "==0.2.3" xlrd = "==1.1.0" @@ -117,7 +104,6 @@ django_celery_results = "==1.0.1" django-post_office = "==3.1.0" Django = "==2.0.9" et_xmlfile = "==1.0.1" -GDAL = "==1.10.0" Jinja2 = "==2.10" MarkupSafe = "==1.1.0" PyJWT = "==1.5.3" @@ -130,4 +116,4 @@ unicef_snapshot = "==0.2.1" Pillow = "==5.3.0" [requires] -python_version = "3.7" +python_version = "3.6.4" diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000..2e33f2b3ec --- /dev/null +++ b/requirements.txt @@ -0,0 +1,109 @@ +amqp==2.2.2 +asn1crypto==0.24.0 +azure-common==1.1.8 +azure-nspkg==2.0.0 +azure-storage==0.20.2 +babel==2.6.0 +billiard==3.5.0.3 +carto==1.3.1 +celery==4.2.1 +cffi==1.11.5 +coreapi==2.3.3 +coreschema==0.0.4 +cryptography==2.4.1 +defusedxml==0.5.0 +diff-match-patch==20121119 +dj-database-url==0.5 +dj-static==0.0.6 +django-appconf==1.0.2 +django-celery-beat==1.2 +django-celery-email==2.0.1 +django-celery-results==1.0.1 +django-contrib-comments==1.9 +django-cors-headers==2.4 +django-debug-toolbar==1.11 +django-easy-pdf==0.1.1 +django-filter==2.0 +django-fsm==2.6 +django-import-export==1.1 +django-js-asset==1.1.0 +django-leaflet==0.24 +django-logentry-admin==1.0.4 +django-model-utils==3.1.2 +django-mptt==0.9.1 +django-ordered-model==1.5 +django-post-office==3.1.0 +django-redis-cache==1.8 +django-rest-swagger==2.2 +django-storages==1.6.6 +django-tenants==2.1 +django-timezone-field==3.0 +django-waffle==0.14 +django==2.0.9 +djangorestframework-csv==2.1.0 +djangorestframework-gis==0.14 +djangorestframework-jwt==1.11.0 +djangorestframework-recursive==0.1.2 +djangorestframework-xml==1.3 +djangorestframework==3.9.0 +drf-nested-routers==0.91 +drf-querystringfilter==1.0.0 +drfpasswordless==1.2 +et-xmlfile==1.0.1 +etools-validator==0.3.2 +flower==0.9.2 +future==0.15.2 +gdal==2.4.0 +gunicorn==19.9 +html5lib==1.0.1 +idna==2.6 +itypes==1.1.0 +jdcal==1.4 +jinja2==2.10 +jsonfield==2.0.2 +kombu==4.2.1 +markupsafe==1.1.0 +newrelic==2.94.0.79 +oauthlib==2.0.7 +odfpy==1.3.6 +openapi-codec==1.3.2 +openpyxl==2.5.9 +pillow==5.3.0 +psycopg2-binary==2.7.5 +psycopg2==2.7.5 +pycparser==2.18 +pyjwt==1.5.3 +pypdf2==1.26.0 +pyrestcli==0.6.4 +python-crontab==2.3.5 +python-dateutil==2.5.3 +python3-openid==3.1.0 +pytz==2018.3 +pyyaml==3.12 +raven==6.9 +redis==2.10.6 +reportlab==3.5.9 +requests-oauthlib==0.8.0 +requests==2.11.1 +simplejson==3.16.0 +six==1.11.0 +social-auth-app-django==2.1.0 +social-auth-core[azuread]==1.7.0 +sqlparse==0.2.4 +static3==0.7.0 +tablib==0.12.1 +tenant-schemas-celery==0.2.1 +tornado==5.1.1 +unicef-attachments==0.4.2 +unicef-djangolib==0.5.2 +unicef-locations==1.5 +unicef-notification==0.2.0 +unicef-restlib==0.3.8 +unicef-snapshot==0.2.1 +unicodecsv==0.14.1 +uritemplate==3.0.0 +vine==1.1.4 +webencodings==0.5.1 +xhtml2pdf==0.2.3 +xlrd==1.1.0 +xlwt==1.3.0 diff --git a/src/etools/config/settings/base.py b/src/etools/config/settings/base.py index ec7da1490b..3dd92b068d 100644 --- a/src/etools/config/settings/base.py +++ b/src/etools/config/settings/base.py @@ -142,7 +142,7 @@ def get_from_secrets_or_env(var_name, default=None): 'level': 'INFO' }, } - +GDAL_LIBRARY_PATH = '/usr/lib/libgdal.so.20' # DJANGO: MODELS FIXTURE_DIRS = ( os.path.join(os.path.dirname(etools.__file__), 'applications', 'EquiTrack', 'data'), From 7142d1b40592604565eef78b17b73abd0f584264 Mon Sep 17 00:00:00 2001 From: Domenico DiNicola Date: Tue, 22 Jan 2019 14:58:39 -0500 Subject: [PATCH 04/72] remove tokens --- .../applications/action_points/models.py | 4 +- src/etools/applications/audit/models.py | 6 +- .../audit/purchase_order/models.py | 2 +- .../email_auth/notifications/token-login.py | 30 ---- src/etools/applications/tokens/__init__.py | 0 src/etools/applications/tokens/forms.py | 42 ----- src/etools/applications/tokens/middleware.py | 28 ---- .../tokens/templates/tokens/login.html | 151 ----------------- .../applications/tokens/tests/__init__.py | 0 .../tokens/tests/test_middleware.py | 39 ----- .../applications/tokens/tests/test_views.py | 153 ------------------ src/etools/applications/tokens/urls.py | 11 -- src/etools/applications/tokens/utils.py | 30 ---- src/etools/applications/tokens/views.py | 88 ---------- src/etools/applications/tpm/models.py | 21 ++- src/etools/config/settings/base.py | 2 - src/etools/config/urls.py | 1 - 17 files changed, 16 insertions(+), 592 deletions(-) delete mode 100644 src/etools/applications/email_auth/notifications/token-login.py delete mode 100644 src/etools/applications/tokens/__init__.py delete mode 100644 src/etools/applications/tokens/forms.py delete mode 100644 src/etools/applications/tokens/middleware.py delete mode 100644 src/etools/applications/tokens/templates/tokens/login.html delete mode 100644 src/etools/applications/tokens/tests/__init__.py delete mode 100644 src/etools/applications/tokens/tests/test_middleware.py delete mode 100644 src/etools/applications/tokens/tests/test_views.py delete mode 100644 src/etools/applications/tokens/urls.py delete mode 100644 src/etools/applications/tokens/utils.py delete mode 100644 src/etools/applications/tokens/views.py diff --git a/src/etools/applications/action_points/models.py b/src/etools/applications/action_points/models.py index 65dc5029f1..323626027c 100644 --- a/src/etools/applications/action_points/models.py +++ b/src/etools/applications/action_points/models.py @@ -191,7 +191,7 @@ def get_snapshot_action_display(cls, activity): return activity.get_action_display() - def get_mail_context(self, user=None, include_token=False): + def get_mail_context(self, user=None): return { 'person_responsible': self.assigned_to.get_full_name(), 'assigned_by': self.assigned_by.get_full_name(), @@ -200,7 +200,7 @@ def get_mail_context(self, user=None, include_token=False): 'description': self.description, 'due_date': self.due_date.strftime('%d %b %Y') if self.due_date else '', 'status': self.status, - 'object_url': self.get_object_url(user=user, include_token=include_token), + 'object_url': self.get_object_url(user=user), } def send_email(self, recipient, template_name, additional_context=None, cc=None): diff --git a/src/etools/applications/audit/models.py b/src/etools/applications/audit/models.py index ec0810390d..104b9e81f7 100644 --- a/src/etools/applications/audit/models.py +++ b/src/etools/applications/audit/models.py @@ -764,10 +764,10 @@ class Meta(ActionPoint.Meta): verbose_name_plural = _('Engagement Action Points') proxy = True - def get_mail_context(self, user=None, include_token=False): - context = super().get_mail_context(user=user, include_token=include_token) + def get_mail_context(self, user=None): + context = super().get_mail_context(user=user) if self.engagement: - context['engagement'] = self.engagement_subclass.get_mail_context(user=user, include_token=include_token) + context['engagement'] = self.engagement_subclass.get_mail_context(user=user) return context diff --git a/src/etools/applications/audit/purchase_order/models.py b/src/etools/applications/audit/purchase_order/models.py index 4c2d603cc8..a359673bd1 100644 --- a/src/etools/applications/audit/purchase_order/models.py +++ b/src/etools/applications/audit/purchase_order/models.py @@ -27,7 +27,7 @@ def __str__(self): def send_user_appointed_email(self, engagement): context = { 'environment': get_environment(), - 'engagement': engagement.get_mail_context(user=self.user, include_token=False), + 'engagement': engagement.get_mail_context(user=self.user), 'staff_member': self.user.get_full_name(), } diff --git a/src/etools/applications/email_auth/notifications/token-login.py b/src/etools/applications/email_auth/notifications/token-login.py deleted file mode 100644 index cc6be501e8..0000000000 --- a/src/etools/applications/email_auth/notifications/token-login.py +++ /dev/null @@ -1,30 +0,0 @@ -from unicef_notification.utils import strip_text - -name = 'email_auth/token/login' -defaults = { - 'description': 'The email that is sent to user to login without password.', - 'subject': 'eTools Access Token', - 'content': strip_text(""" - Dear {{ recipient }}, - - Please click on this link to sign in to eTools portal: - - {{ login_link }} - - Thank you. - """), - - 'html_content': """ - {% extends "email-templates/base" %} - - {% block title %}eTools Access Token{% endblock %} - - {% block content %} -

Dear {{ recipient }},

- -

Please click on this link to sign in to eTools portal.

- -

Thank you.

- {% endblock %} - """ -} diff --git a/src/etools/applications/tokens/__init__.py b/src/etools/applications/tokens/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/etools/applications/tokens/forms.py b/src/etools/applications/tokens/forms.py deleted file mode 100644 index 99e4d0e6b1..0000000000 --- a/src/etools/applications/tokens/forms.py +++ /dev/null @@ -1,42 +0,0 @@ - -from django import forms -from django.contrib.auth import get_user_model -from django.utils.translation import ugettext_lazy as _ - - -class EmailLoginForm(forms.Form): - email = forms.EmailField(label=_("Your Email")) - next = forms.CharField(widget=forms.HiddenInput(), required=False) - - error_messages = { - 'no_such_user': _("User with such email does not exists."), - 'inactive': _("This account is inactive."), - } - - def __init__(self, *args, **kwargs): - self.request = kwargs.pop('request', None) - self.user_cache = None - super().__init__(*args, **kwargs) - - def clean(self): - if self.errors: - return - - self.user_cache = get_user_model().objects.filter(email=self.cleaned_data['email']).first() - if not self.user_cache: - raise forms.ValidationError( - self.error_messages['no_such_user'], - code='no_such_user', - ) - else: - self.confirm_login_allowed(self.user_cache) - - def confirm_login_allowed(self, user): - if not user.is_active: - raise forms.ValidationError( - self.error_messages['inactive'], - code='inactive', - ) - - def get_user(self): - return self.user_cache diff --git a/src/etools/applications/tokens/middleware.py b/src/etools/applications/tokens/middleware.py deleted file mode 100644 index b4cdf0ea82..0000000000 --- a/src/etools/applications/tokens/middleware.py +++ /dev/null @@ -1,28 +0,0 @@ -from django.conf import settings -from django.contrib.auth import login -from django.utils.deprecation import MiddlewareMixin - -from drfpasswordless.utils import authenticate_by_token -from rest_framework.authentication import TokenAuthentication -from rest_framework.exceptions import AuthenticationFailed - - -class TokenAuthenticationMiddleware(MiddlewareMixin): - def process_request(self, request): - token = request.GET.get(settings.EMAIL_AUTH_TOKEN_NAME) - if token is None: - return - - try: - # attempt to auth via authtoken - token_auth = TokenAuthentication() - user, _ = token_auth.authenticate_credentials(token) - except AuthenticationFailed: - # attempt to auth by drfpasswordless - user = authenticate_by_token(token) - - if user is None: - return - - user.backend = 'django.contrib.auth.backends.ModelBackend' - login(request, user) diff --git a/src/etools/applications/tokens/templates/tokens/login.html b/src/etools/applications/tokens/templates/tokens/login.html deleted file mode 100644 index 52368b926a..0000000000 --- a/src/etools/applications/tokens/templates/tokens/login.html +++ /dev/null @@ -1,151 +0,0 @@ -{% extends "account/base.html" %} - -{% load i18n %} -{% load static %} - -{% block head_title %}{% trans "Sign In with Email" %}{% endblock %} - -{% block extra_head %} - - - -{% endblock %} - -{% block content %} - -
- -

eTools portal

-
-
-

Sign in with Email

-
- {% csrf_token %} - {% for error in form.non_field_errors %} - {% if error %} - - - {{ error }} - - {% endif %} - {% endfor %} - {{ form.NON_FIELD_ERRORS }} - {% for field in form %} - {% if field.field.widget.input_type %} - {% if field.field.widget.input_type != 'hidden' %} - - - {% else %} - - {% endif %} - {% else %} -
- {{ field.label }} - {% endif %} - {% if field.help_text %} -

{{ field.help_text|safe }}

- {% endif %} - {% endfor %} - - {% if form.is_valid %} - - {% blocktrans %}Email with login info was sent to {{ email }}.{% endblocktrans %} - - {% endif %} -
- - - - -
- {% trans "Submit" %} -
-
-
-
-{% endblock %} - -{% block extra_js %} - -{% endblock %} diff --git a/src/etools/applications/tokens/tests/__init__.py b/src/etools/applications/tokens/tests/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/etools/applications/tokens/tests/test_middleware.py b/src/etools/applications/tokens/tests/test_middleware.py deleted file mode 100644 index ebb6b43808..0000000000 --- a/src/etools/applications/tokens/tests/test_middleware.py +++ /dev/null @@ -1,39 +0,0 @@ -from django.conf import settings -from django.test import Client -from django.urls import reverse -from drfpasswordless.utils import create_callback_token_for_user -from rest_framework import status - -from etools.applications.EquiTrack.tests.cases import BaseTenantTestCase -from etools.applications.users.tests.factories import UserFactory - - -class TestTokenAuthenticationMiddleware(BaseTenantTestCase): - def setUp(self): - self.client = Client() - - def test_no_token(self): - response = self.client.get(reverse("tokens:login")) - self.assertEquals(response.status_code, status.HTTP_200_OK) - - def test_token(self): - user = UserFactory() - token = create_callback_token_for_user(user, "email") - response = self.client.get("{}?{}={}".format( - reverse("tokens:login"), - settings.EMAIL_AUTH_TOKEN_NAME, - token, - )) - self.assertEquals(response.status_code, status.HTTP_302_FOUND) - - def test_token_invalid(self): - response = self.client.get("{}?{}={}".format( - reverse("tokens:login"), - settings.EMAIL_AUTH_TOKEN_NAME, - "wrong", - )) - self.assertEquals(response.status_code, status.HTTP_200_OK) - self.assertEqual( - response.context["form"].errors, - {"__all__": ["Couldn't log you in. Invalid token."]} - ) diff --git a/src/etools/applications/tokens/tests/test_views.py b/src/etools/applications/tokens/tests/test_views.py deleted file mode 100644 index 11bc273249..0000000000 --- a/src/etools/applications/tokens/tests/test_views.py +++ /dev/null @@ -1,153 +0,0 @@ -from unittest.mock import Mock, patch - -from django.core.management import call_command -from django.test import Client -from django.urls import reverse -from rest_framework import status -from rest_framework.authtoken.models import Token - -from etools.applications.EquiTrack.tests.cases import BaseTenantTestCase -from etools.applications.users.tests.factories import UserFactory - -SEND_PATH = "etools.applications.tokens.views.send_notification_with_template" - - -class TestTokenEmailAuthView(BaseTenantTestCase): - @classmethod - def setUpTestData(cls): - call_command("update_notifications") - - def setUp(self): - self.client = Client() - - def test_get(self): - response = self.client.get(reverse("tokens:login")) - self.assertEquals(response.status_code, status.HTTP_200_OK) - - def test_post(self): - email = "test-email-auth@example.com" - UserFactory(email=email) - mock_send = Mock() - with patch(SEND_PATH, mock_send): - response = self.client.post( - reverse("tokens:login"), - data={"email": email} - ) - self.assertEquals(response.status_code, status.HTTP_200_OK) - self.assertEqual(mock_send.call_count, 1) - - def test_post_invalid_email(self): - email = "test-email-auth@example.com" - UserFactory() - mock_send = Mock() - with patch(SEND_PATH, mock_send): - response = self.client.post( - reverse("tokens:login"), - data={"email": email} - ) - self.assertEquals(response.status_code, status.HTTP_200_OK) - self.assertEqual( - response.context["form"].errors, - {"__all__": ["User with such email does not exists."]} - ) - self.assertEqual(mock_send.call_count, 0) - - def test_post_invalid_inactive_user(self): - email = "test-email-auth@example.com" - UserFactory(email=email, is_active=False) - mock_send = Mock() - with patch(SEND_PATH, mock_send): - response = self.client.post( - reverse("tokens:login"), - data={"email": email} - ) - self.assertEquals(response.status_code, status.HTTP_200_OK) - self.assertEqual( - response.context["form"].errors, - {"__all__": ["This account is inactive."]} - ) - self.assertEqual(mock_send.call_count, 0) - - -class TestTokenGetView(BaseTenantTestCase): - def setUp(self): - self.client = Client() - - def test_get_not_logged_in(self): - response = self.client.get(reverse("tokens:get")) - self.assertEquals(response.status_code, status.HTTP_302_FOUND) - - def test_get_new(self): - user = UserFactory() - token_qs = Token.objects.filter(user=user) - self.assertFalse(token_qs.exists()) - self.client.force_login(user) - response = self.client.get(reverse("tokens:get")) - self.assertEquals(response.status_code, status.HTTP_200_OK) - self.assertTrue(token_qs.exists()) - token = token_qs.first() - self.assertEqual(response.data, {"token": token.key}) - - def test_get_exists(self): - user = UserFactory() - token, _ = Token.objects.get_or_create(user=user) - self.client.force_login(user) - response = self.client.get(reverse("tokens:get")) - self.assertEquals(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data, {"token": token.key}) - - -class TestTokenResetView(BaseTenantTestCase): - def setUp(self): - self.client = Client() - - def test_get_not_logged_in(self): - response = self.client.get(reverse("tokens:reset")) - self.assertEquals(response.status_code, status.HTTP_302_FOUND) - - def test_get_none(self): - user = UserFactory() - token_qs = Token.objects.filter(user=user) - self.assertFalse(token_qs.exists()) - self.client.force_login(user) - response = self.client.get(reverse("tokens:reset")) - self.assertEquals(response.status_code, status.HTTP_200_OK) - self.assertTrue(token_qs.exists()) - token = token_qs.first() - self.assertEqual(response.data, {"token": token.key}) - - def test_get_exists(self): - user = UserFactory() - token, _ = Token.objects.get_or_create(user=user) - self.client.force_login(user) - response = self.client.get(reverse("tokens:reset")) - self.assertEquals(response.status_code, status.HTTP_200_OK) - self.assertNotEqual(response.data["token"], token.key) - - -class TestTokenDeleteView(BaseTenantTestCase): - def setUp(self): - self.client = Client() - - def test_get_not_logged_in(self): - response = self.client.get(reverse("tokens:delete")) - self.assertEquals(response.status_code, status.HTTP_302_FOUND) - - def test_get_none(self): - user = UserFactory() - token_qs = Token.objects.filter(user=user) - self.assertFalse(token_qs.exists()) - self.client.force_login(user) - response = self.client.get(reverse("tokens:delete")) - self.assertEquals(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data, {"message": "Token has been deleted."}) - self.assertFalse(token_qs.exists()) - - def test_get_exists(self): - user = UserFactory() - token, _ = Token.objects.get_or_create(user=user) - self.client.force_login(user) - response = self.client.get(reverse("tokens:delete")) - self.assertEquals(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data, {"message": "Token has been deleted."}) - self.assertFalse(Token.objects.filter(user=user).exists()) diff --git a/src/etools/applications/tokens/urls.py b/src/etools/applications/tokens/urls.py deleted file mode 100644 index d39d8bfca5..0000000000 --- a/src/etools/applications/tokens/urls.py +++ /dev/null @@ -1,11 +0,0 @@ -from django.conf.urls import url - -from etools.applications.tokens import views - -app_name = 'tokens' -urlpatterns = [ - url(r"^email/login/$", views.TokenEmailAuthView.as_view(), name="login"), - url(r"^$", views.TokenGetView.as_view(), name="get"), - url(r"^reset/$", views.TokenResetView.as_view(), name="reset"), - url(r"^delete/$", views.TokenDeleteView.as_view(), name="delete"), -] diff --git a/src/etools/applications/tokens/utils.py b/src/etools/applications/tokens/utils.py deleted file mode 100644 index 6f49fd9f5b..0000000000 --- a/src/etools/applications/tokens/utils.py +++ /dev/null @@ -1,30 +0,0 @@ -from urllib.parse import urlparse, parse_qsl, urlencode, urlunparse, urljoin - -from django.conf import settings -from django.urls import reverse - -from drfpasswordless.utils import create_callback_token_for_user - - -def update_url_with_kwargs(url, **kwargs): - if not url: - return - - url_parts = list(urlparse(url)) - query = dict(parse_qsl(url_parts[4])) - query.update(kwargs) - url_parts[4] = urlencode(query) - - return urlunparse(url_parts) - - -def update_url_with_auth_token(url, user): - token = create_callback_token_for_user(user, 'email') - return update_url_with_kwargs(url, **{settings.EMAIL_AUTH_TOKEN_NAME: token}) - - -def get_token_auth_link(user): - return update_url_with_auth_token( - urljoin(settings.HOST, reverse('tokens:login')), - user - ) diff --git a/src/etools/applications/tokens/views.py b/src/etools/applications/tokens/views.py deleted file mode 100644 index 6b3914cf49..0000000000 --- a/src/etools/applications/tokens/views.py +++ /dev/null @@ -1,88 +0,0 @@ -from django.conf import settings -from django.shortcuts import redirect -from django.utils.translation import ugettext_lazy as _ -from django.views.generic import FormView - -from rest_framework.authtoken.models import Token -from rest_framework.response import Response -from rest_framework.views import APIView -from unicef_notification.utils import send_notification_with_template - -from etools.applications.tokens.forms import EmailLoginForm -from etools.applications.tokens.utils import get_token_auth_link, update_url_with_kwargs - - -class TokenEmailAuthView(FormView): - form_class = EmailLoginForm - template_name = 'tokens/login.html' - - def get(self, request, *args, **kwargs): - if request.user.is_authenticated: - return redirect(request.GET.get('next', 'dashboard')) - - return super().get(request, *args, **kwargs) - - def get_initial(self): - initial = super().get_initial() - initial.update({'next': self.request.GET.get('next')}) - return initial - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - if settings.EMAIL_AUTH_TOKEN_NAME in self.request.GET: - context['form'].errors['__all__'] = [_('Couldn\'t log you in. Invalid token.')] - - return context - - def form_valid(self, form): - login_link = get_token_auth_link(form.get_user()) - - redirect_to = form.data.get('next', self.request.GET.get('next')) - if redirect_to: - login_link = update_url_with_kwargs(login_link, next=redirect_to) - - email_context = { - 'recipient': form.get_user().get_full_name(), - 'login_link': login_link, - } - - send_notification_with_template( - recipients=[form.get_user().email], - template_name='email_auth/token/login', - context=email_context - ) - - return self.render_to_response(self.get_context_data(email=form.get_user().email)) - - -class TokenGetView(APIView): - """Expects user to be logged in already""" - def get(self, request, *args, **kwargs): - if not request.user.is_authenticated: - return redirect(request.GET.get('next', 'dashboard')) - token, _ = Token.objects.get_or_create(user=request.user) - return Response({"token": token.key}) - - -class TokenResetView(APIView): - """Reset users token""" - def get(self, request, *args, **kwargs): - if not request.user.is_authenticated: - return redirect(request.GET.get('next', 'dashboard')) - token, created = Token.objects.get_or_create(user=request.user) - if not created: - token.delete() - token = Token.objects.create(user=request.user) - return Response({"token": token.key}) - - -class TokenDeleteView(APIView): - """Delete users token""" - def get(self, request, *args, **kwargs): - if not request.user.is_authenticated: - return redirect(request.GET.get('next', 'dashboard')) - try: - Token.objects.get(user=request.user).delete() - except Token.DoesNotExist: - pass - return Response({"message": "Token has been deleted."}) diff --git a/src/etools/applications/tpm/models.py b/src/etools/applications/tpm/models.py index 9932eaeb37..e9f87e85e3 100644 --- a/src/etools/applications/tpm/models.py +++ b/src/etools/applications/tpm/models.py @@ -165,8 +165,8 @@ def __str__(self): self.start_date, self.end_date ) - def get_mail_context(self, user=None, include_token=False, include_activities=True): - object_url = self.get_object_url(user=user, include_token=include_token) + def get_mail_context(self, user=None, include_activities=True): + object_url = self.get_object_url(user=user) activities = self.tpm_activities.all() interventions = set(a.intervention.title for a in activities if a.intervention) @@ -185,11 +185,11 @@ def get_mail_context(self, user=None, include_token=False, include_activities=Tr return context - def _send_email(self, recipients, template_name, context=None, user=None, include_token=False, **kwargs): + def _send_email(self, recipients, template_name, context=None, user=None, **kwargs): context = context or {} base_context = { - 'visit': self.get_mail_context(user=user, include_token=include_token), + 'visit': self.get_mail_context(user=user), 'environment': get_environment(), } base_context.update(context) @@ -246,7 +246,7 @@ def assign(self): self._send_email( staff_member.user.email, 'tpm/visit/assign_staff_member', context={'recipient': staff_member.user.get_full_name()}, - user=staff_member.user, include_token=False + user=staff_member.user ) @transition( @@ -442,7 +442,7 @@ def related_reports(self): def pv_applicable(self): return self.related_reports.exists() - def get_mail_context(self, user=None, include_token=False, include_visit=True): + def get_mail_context(self, user=None, include_visit=True): context = { 'locations': ', '.join(map(force_text, self.locations.all())), 'intervention': self.intervention.title if self.intervention else '-', @@ -451,8 +451,7 @@ def get_mail_context(self, user=None, include_token=False, include_visit=True): 'partner': self.partner.name if self.partner else '-', } if include_visit: - context['tpm_visit'] = self.tpm_visit.get_mail_context(user=user, include_token=include_token, - include_activities=False) + context['tpm_visit'] = self.tpm_visit.get_mail_context(user=user, include_activities=False) return context @@ -474,10 +473,10 @@ class Meta(ActionPoint.Meta): verbose_name_plural = _('Engagement Action Points') proxy = True - def get_mail_context(self, user=None, include_token=False): - context = super().get_mail_context(user=user, include_token=include_token) + def get_mail_context(self, user=None): + context = super().get_mail_context(user=user) if self.tpm_activity: - context['tpm_activity'] = self.tpm_activity.get_mail_context(user=user, include_token=include_token) + context['tpm_activity'] = self.tpm_activity.get_mail_context(user=user) return context diff --git a/src/etools/config/settings/base.py b/src/etools/config/settings/base.py index ec7da1490b..fe240a0de8 100644 --- a/src/etools/config/settings/base.py +++ b/src/etools/config/settings/base.py @@ -111,7 +111,6 @@ def get_from_secrets_or_env(var_name, default=None): 'django.contrib.sessions.middleware.SessionMiddleware', 'etools.applications.EquiTrack.auth.CustomSocialAuthExceptionMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'etools.applications.tokens.middleware.TokenAuthenticationMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', @@ -189,7 +188,6 @@ def get_from_secrets_or_env(var_name, default=None): 'etools.applications.tpm.tpmpartners', 'etools.applications.utils.common', 'waffle', - 'etools.applications.tokens', 'etools.applications.permissions2', 'unicef_notification', ) diff --git a/src/etools/config/urls.py b/src/etools/config/urls.py index 3b68737a82..0892d9bc58 100644 --- a/src/etools/config/urls.py +++ b/src/etools/config/urls.py @@ -72,7 +72,6 @@ url(r'^$', ModuleRedirectView.as_view(), name='dashboard'), url(r'^login/$', MainView.as_view(), name='main'), url(r'^logout/$', logout_view, name='logout'), - url(r'^tokens/', include('etools.applications.tokens.urls')), url(r'^api/static_data/$', StaticDataView.as_view({'get': 'list'}), name='public_static'), From 773fd092ff150bc4c09e844e2f14e275e3126e15 Mon Sep 17 00:00:00 2001 From: Domenico DiNicola Date: Fri, 18 Jan 2019 16:36:28 -0500 Subject: [PATCH 05/72] removed legacy u' --- .../commands/clear-migrations-table.py | 6 +- .../management/commands/init-celery.py | 4 +- .../applications/EquiTrack/util_scripts.py | 4 +- .../migrations/0003_fix_null_values.py | 2 +- .../attachments/tests/test_models.py | 2 +- .../audit/migrations/0003_fix_null_values.py | 2 +- .../migrations/0002_fix_null_values.py | 2 +- .../audit/purchase_order/tasks.py | 10 +-- .../applications/audit/tests/test_views.py | 4 +- .../funds/migrations/0004_fix_null_values.py | 2 +- src/etools/applications/funds/models.py | 10 +-- .../applications/funds/tests/test_models.py | 34 ++++----- src/etools/applications/management/tasks.py | 4 +- .../management/tests/test_models.py | 6 +- src/etools/applications/partners/forms.py | 8 +-- .../migrations/0004_fix_null_values.py | 2 +- src/etools/applications/partners/mixins.py | 2 +- src/etools/applications/partners/models.py | 2 +- .../partners/serializers/dashboards.py | 2 +- .../serializers/partner_organization_v2.py | 2 +- .../applications/partners/synchronizers.py | 20 +++--- .../partners/tests/test_api_interventions.py | 10 +-- .../partners/tests/test_export_agreement.py | 6 +- .../tests/test_export_intervention.py | 42 +++++------ .../partners/tests/test_export_partner.py | 16 ++--- .../partners/tests/test_exports.py | 72 +++++++++---------- .../partners/tests/test_models.py | 60 ++++++++-------- .../partners/tests/test_serializers.py | 4 +- .../applications/partners/tests/test_tasks.py | 2 +- .../applications/partners/tests/test_views.py | 8 +-- .../partners/views/interventions_v2.py | 8 +-- .../migrations/0002_fix_null_values.py | 2 +- .../applications/publics/tests/test_models.py | 58 +++++++-------- .../management/commands/init-result-type.py | 6 +- .../migrations/0002_fix_null_values.py | 2 +- src/etools/applications/reports/models.py | 26 +++---- .../applications/reports/tests/test_models.py | 56 +++++++-------- .../t2f/migrations/0002_fix_null_values.py | 2 +- .../applications/t2f/tests/test_models.py | 16 ++--- .../t2f/tests/test_travel_details.py | 12 ++-- .../t2f/tests/test_travel_list.py | 2 +- .../applications/tpm/tests/test_views.py | 12 ++-- .../applications/tpm/tpmpartners/tasks.py | 10 +-- src/etools/applications/users/admin.py | 34 ++++----- .../users/migrations/0003_fix_null_values.py | 2 +- src/etools/applications/users/models.py | 2 +- src/etools/applications/users/tasks.py | 16 ++--- .../applications/users/tests/test_models.py | 24 +++---- .../applications/users/tests/test_views.py | 2 +- .../applications/users/tests/test_views_v3.py | 2 +- .../vision/migrations/0002_fix_null_values.py | 2 +- src/etools/applications/vision/models.py | 2 +- src/etools/applications/vision/tasks.py | 10 +-- .../applications/vision/tests/test_models.py | 2 +- .../applications/vision/tests/test_tasks.py | 2 +- 55 files changed, 331 insertions(+), 331 deletions(-) diff --git a/src/etools/applications/EquiTrack/management/commands/clear-migrations-table.py b/src/etools/applications/EquiTrack/management/commands/clear-migrations-table.py index 52bc8477c9..3ae92f3476 100644 --- a/src/etools/applications/EquiTrack/management/commands/clear-migrations-table.py +++ b/src/etools/applications/EquiTrack/management/commands/clear-migrations-table.py @@ -17,7 +17,7 @@ def add_arguments(self, parser): @transaction.atomic def handle(self, *args, **options): - logger.info(u'Command started') + logger.info('Command started') countries = Country.objects.exclude(name__iexact='global') if options['schema']: @@ -31,8 +31,8 @@ def handle(self, *args, **options): ]) for country in countries: connection.set_tenant(country) - logger.info(u'Clear table for %s' % country.name) + logger.info('Clear table for %s' % country.name) with connection.cursor() as cursor: cursor.execute("DELETE FROM django_migrations WHERE app IN ({})".format(etools_apps)) - logger.info(u'Command finished') + logger.info('Command finished') diff --git a/src/etools/applications/EquiTrack/management/commands/init-celery.py b/src/etools/applications/EquiTrack/management/commands/init-celery.py index 83a4552714..08131e2159 100644 --- a/src/etools/applications/EquiTrack/management/commands/init-celery.py +++ b/src/etools/applications/EquiTrack/management/commands/init-celery.py @@ -11,7 +11,7 @@ class Command(BaseCommand): help = 'Init celery command' def handle(self, *args, **options): - logger.info(u'Init Celery command started') + logger.info('Init Celery command started') every_day, _ = IntervalSchedule.objects.get_or_create(every=1, period=IntervalSchedule.DAYS) every_two_weeks, _ = IntervalSchedule.objects.get_or_create(every=14, period=IntervalSchedule.DAYS) every_week, _ = IntervalSchedule.objects.get_or_create(every=7, period=IntervalSchedule.DAYS) @@ -73,4 +73,4 @@ def handle(self, *args, **options): 'enabled': False, 'crontab': first_day_of_the_month}) - logger.info(u'Init Celery command finished') + logger.info('Init Celery command finished') diff --git a/src/etools/applications/EquiTrack/util_scripts.py b/src/etools/applications/EquiTrack/util_scripts.py index 2ef105dc2c..88984ed7b7 100644 --- a/src/etools/applications/EquiTrack/util_scripts.py +++ b/src/etools/applications/EquiTrack/util_scripts.py @@ -21,7 +21,7 @@ def printtf(*args): def set_country(name): connection.set_tenant(Country.objects.get(name=name)) - logger.info(u'Set in {} workspace'.format(name)) + logger.info('Set in {} workspace'.format(name)) def local_country_keep(): @@ -56,4 +56,4 @@ def create_test_user(email, password): userp.country = country userp.country_override = country userp.save() - logger.info(u"user {} created".format(u.email)) + logger.info("user {} created".format(u.email)) diff --git a/src/etools/applications/attachments/migrations/0003_fix_null_values.py b/src/etools/applications/attachments/migrations/0003_fix_null_values.py index b291d54b9f..cf6a45c2a1 100644 --- a/src/etools/applications/attachments/migrations/0003_fix_null_values.py +++ b/src/etools/applications/attachments/migrations/0003_fix_null_values.py @@ -5,7 +5,7 @@ class Migration(migrations.Migration): dependencies = [ - (u'attachments', u'0002_attachmentflat_filename'), + ('attachments', '0002_attachmentflat_filename'), ] operations = [ diff --git a/src/etools/applications/attachments/tests/test_models.py b/src/etools/applications/attachments/tests/test_models.py index b089880b51..b6bbbcdff4 100644 --- a/src/etools/applications/attachments/tests/test_models.py +++ b/src/etools/applications/attachments/tests/test_models.py @@ -15,7 +15,7 @@ def setUpTestData(cls): def test_str(self): attachment = AttachmentFactory( file=SimpleUploadedFile( - 'simple_file.txt', u'R\xe4dda Barnen'.encode('utf-8') + 'simple_file.txt', 'R\xe4dda Barnen'.encode('utf-8') ), content_object=self.simple_object ) diff --git a/src/etools/applications/audit/migrations/0003_fix_null_values.py b/src/etools/applications/audit/migrations/0003_fix_null_values.py index 40183debde..156f59cd54 100644 --- a/src/etools/applications/audit/migrations/0003_fix_null_values.py +++ b/src/etools/applications/audit/migrations/0003_fix_null_values.py @@ -5,7 +5,7 @@ class Migration(migrations.Migration): dependencies = [ - (u'audit', u'0002_auto_20180326_1605'), + ('audit', '0002_auto_20180326_1605'), ] operations = [ diff --git a/src/etools/applications/audit/purchase_order/migrations/0002_fix_null_values.py b/src/etools/applications/audit/purchase_order/migrations/0002_fix_null_values.py index 19e3ba09ba..71bb8422eb 100644 --- a/src/etools/applications/audit/purchase_order/migrations/0002_fix_null_values.py +++ b/src/etools/applications/audit/purchase_order/migrations/0002_fix_null_values.py @@ -4,7 +4,7 @@ class Migration(migrations.Migration): dependencies = [ - (u'purchase_order', u'0001_initial'), + ('purchase_order', '0001_initial'), ] operations = [ diff --git a/src/etools/applications/audit/purchase_order/tasks.py b/src/etools/applications/audit/purchase_order/tasks.py index d1d3ee589b..27b9490e55 100644 --- a/src/etools/applications/audit/purchase_order/tasks.py +++ b/src/etools/applications/audit/purchase_order/tasks.py @@ -13,20 +13,20 @@ @app.task def update_purchase_orders(country_name=None): - logger.info(u'Starting update values for purchase order') + logger.info('Starting update values for purchase order') countries = Country.objects.filter(vision_sync_enabled=True) processed = [] if country_name is not None: countries = countries.filter(name=country_name) for country in countries: try: - logger.info(u'Starting purchase order update for country {}'.format( + logger.info('Starting purchase order update for country {}'.format( country.name )) POSynchronizer(country).sync() processed.append(country.name) - logger.info(u"Update finished successfully for {}".format(country.name)) + logger.info("Update finished successfully for {}".format(country.name)) except VisionException: - logger.exception(u"{} sync failed".format(POSynchronizer.__name__)) + logger.exception("{} sync failed".format(POSynchronizer.__name__)) # Keep going to the next country - logger.info(u'Purchase orders synced successfully for {}.'.format(u', '.join(processed))) + logger.info('Purchase orders synced successfully for {}.'.format(', '.join(processed))) diff --git a/src/etools/applications/audit/tests/test_views.py b/src/etools/applications/audit/tests/test_views.py index 8bd7d2c285..03ed2753c0 100644 --- a/src/etools/applications/audit/tests/test_views.py +++ b/src/etools/applications/audit/tests/test_views.py @@ -1180,7 +1180,7 @@ def test_list(self): request_format='multipart', data={ 'file_type': AttachmentFileTypeFactory(code='audit_engagement').id, - 'file': SimpleUploadedFile('hello_world.txt', u'hello world!'.encode('utf-8')), + 'file': SimpleUploadedFile('hello_world.txt', 'hello world!'.encode('utf-8')), } ) self.assertEqual(create_response.status_code, status.HTTP_201_CREATED) @@ -1225,7 +1225,7 @@ def test_list(self): request_format='multipart', data={ 'file_type': AttachmentFileTypeFactory(code='audit_report').id, - 'file': SimpleUploadedFile('hello_world.txt', u'hello world!'.encode('utf-8')), + 'file': SimpleUploadedFile('hello_world.txt', 'hello world!'.encode('utf-8')), } ) self.assertEqual(create_response.status_code, status.HTTP_201_CREATED) diff --git a/src/etools/applications/funds/migrations/0004_fix_null_values.py b/src/etools/applications/funds/migrations/0004_fix_null_values.py index 6f3b431dad..591a43411e 100644 --- a/src/etools/applications/funds/migrations/0004_fix_null_values.py +++ b/src/etools/applications/funds/migrations/0004_fix_null_values.py @@ -5,7 +5,7 @@ class Migration(migrations.Migration): dependencies = [ - (u'funds', u'0003_auto_20180329_1154'), + ('funds', '0003_auto_20180329_1154'), ] operations = [ diff --git a/src/etools/applications/funds/models.py b/src/etools/applications/funds/models.py index 7b36a2a16d..d6be3b3ba7 100644 --- a/src/etools/applications/funds/models.py +++ b/src/etools/applications/funds/models.py @@ -55,7 +55,7 @@ class Meta: ordering = ['donor'] def __str__(self): - return u"{}: {}".format( + return "{}: {}".format( self.donor.name, self.name ) @@ -168,7 +168,7 @@ class FundsReservationHeader(TimeStampedModel): ) def __str__(self): - return u'{}'.format( + return '{}'.format( self.fr_number ) @@ -252,7 +252,7 @@ class FundsReservationItem(TimeStampedModel): ) def __str__(self): - return u'{}'.format( + return '{}'.format( self.fr_ref_number ) @@ -313,7 +313,7 @@ class FundsCommitmentHeader(TimeStampedModel): ) def __str__(self): - return u'{}'.format( + return '{}'.format( self.fc_number ) @@ -393,7 +393,7 @@ class FundsCommitmentItem(TimeStampedModel): ) def __str__(self): - return u'{}'.format( + return '{}'.format( self.fc_ref_number ) diff --git a/src/etools/applications/funds/tests/test_models.py b/src/etools/applications/funds/tests/test_models.py index 0c4817ac7c..699138ba9e 100644 --- a/src/etools/applications/funds/tests/test_models.py +++ b/src/etools/applications/funds/tests/test_models.py @@ -11,37 +11,37 @@ class TestStrUnicode(BaseTenantTestCase): """Ensure calling str() on model instances returns the right text.""" def test_donor(self): - donor = DonorFactory.build(name=u'R\xe4dda Barnen') - self.assertEqual(str(donor), u'R\xe4dda Barnen') + donor = DonorFactory.build(name='R\xe4dda Barnen') + self.assertEqual(str(donor), 'R\xe4dda Barnen') def test_grant(self): donor = DonorFactory.build(name='xyz') - grant = GrantFactory.build(donor=donor, name=u'R\xe4dda Barnen') - self.assertEqual(str(grant), u'xyz: R\xe4dda Barnen') + grant = GrantFactory.build(donor=donor, name='R\xe4dda Barnen') + self.assertEqual(str(grant), 'xyz: R\xe4dda Barnen') - donor = DonorFactory.build(name=u'xyz') - grant = GrantFactory.build(donor=donor, name=u'R\xe4dda Barnen') - self.assertEqual(str(grant), u'xyz: R\xe4dda Barnen') + donor = DonorFactory.build(name='xyz') + grant = GrantFactory.build(donor=donor, name='R\xe4dda Barnen') + self.assertEqual(str(grant), 'xyz: R\xe4dda Barnen') - donor = DonorFactory.build(name=u'R\xe4dda Barnen') + donor = DonorFactory.build(name='R\xe4dda Barnen') grant = GrantFactory.build(donor=donor, name='xyz') - self.assertEqual(str(grant), u'R\xe4dda Barnen: xyz') + self.assertEqual(str(grant), 'R\xe4dda Barnen: xyz') def test_funds_reservation_header(self): - funds_reservation_header = FundsReservationHeaderFactory.build(fr_number=u'R\xe4dda Barnen') - self.assertEqual(str(funds_reservation_header), u'R\xe4dda Barnen') + funds_reservation_header = FundsReservationHeaderFactory.build(fr_number='R\xe4dda Barnen') + self.assertEqual(str(funds_reservation_header), 'R\xe4dda Barnen') def test_funds_reservation_item(self): - funds_reservation_item = FundsReservationItemFactory.build(fr_ref_number=u'R\xe4dda Barnen') - self.assertEqual(str(funds_reservation_item), u'R\xe4dda Barnen') + funds_reservation_item = FundsReservationItemFactory.build(fr_ref_number='R\xe4dda Barnen') + self.assertEqual(str(funds_reservation_item), 'R\xe4dda Barnen') def test_funds_commitment_header(self): - funds_commitment_header = FundsCommitmentHeaderFactory.build(fc_number=u'R\xe4dda Barnen') - self.assertEqual(str(funds_commitment_header), u'R\xe4dda Barnen') + funds_commitment_header = FundsCommitmentHeaderFactory.build(fc_number='R\xe4dda Barnen') + self.assertEqual(str(funds_commitment_header), 'R\xe4dda Barnen') def test_funds_commitment_item(self): - funds_commitment_item = FundsCommitmentItemFactory.build(fc_ref_number=u'R\xe4dda Barnen') - self.assertEqual(str(funds_commitment_item), u'R\xe4dda Barnen') + funds_commitment_item = FundsCommitmentItemFactory.build(fc_ref_number='R\xe4dda Barnen') + self.assertEqual(str(funds_commitment_item), 'R\xe4dda Barnen') class TestFundsReservationHeader(BaseTenantTestCase): diff --git a/src/etools/applications/management/tasks.py b/src/etools/applications/management/tasks.py index bbb65b927b..c2a03ed937 100644 --- a/src/etools/applications/management/tasks.py +++ b/src/etools/applications/management/tasks.py @@ -130,7 +130,7 @@ def pmp_indicator_report(writer, **kwargs): for country in qs: set_country(country.name) - logger.info(u'Running on %s' % country.name) + logger.info('Running on %s' % country.name) for partner in PartnerOrganization.objects.prefetch_related('core_values_assessments'): for intervention in Intervention.objects.filter( agreement__partner=partner).select_related('planned_budget'): @@ -155,7 +155,7 @@ def pmp_indicator_report(writer, **kwargs): 'Unicef Cash': intervention.planned_budget.unicef_cash if planned_budget else '-', 'In kind Amount': intervention.planned_budget.in_kind_amount if planned_budget else '-', 'Total': intervention.planned_budget.total if planned_budget else '-', - 'FR numbers against PD / SSFA': u' - '.join([fh.fr_number for fh in intervention.frs.all()]), + 'FR numbers against PD / SSFA': ' - '.join([fh.fr_number for fh in intervention.frs.all()]), 'FR currencies': ', '.join(fr for fr in fr_currencies), 'Sum of all FR planned amount': intervention.frs.aggregate( total=Coalesce(Sum('intervention_amt'), 0))['total'] if fr_currencies.count() <= 1 else '-', diff --git a/src/etools/applications/management/tests/test_models.py b/src/etools/applications/management/tests/test_models.py index b6de953ced..0fb45edcf0 100644 --- a/src/etools/applications/management/tests/test_models.py +++ b/src/etools/applications/management/tests/test_models.py @@ -14,14 +14,14 @@ def test_flagged_issue(self): issue_id="321", message='test message' ) - self.assertEqual(str(issue), u"test message") + self.assertEqual(str(issue), "test message") issue = FlaggedIssueFactory( content_object=partner, issue_id="321", - message=u"R\xe4dda Barnen" + message="R\xe4dda Barnen" ) - self.assertEqual(str(issue), u"R\xe4dda Barnen") + self.assertEqual(str(issue), "R\xe4dda Barnen") class FlaggedIssueTest(BaseTenantTestCase): diff --git a/src/etools/applications/partners/forms.py b/src/etools/applications/partners/forms.py index 31c68305d6..16e3a22a03 100644 --- a/src/etools/applications/partners/forms.py +++ b/src/etools/applications/partners/forms.py @@ -24,16 +24,16 @@ class Meta: def clean(self): cleaned_data = super().clean() - partner_type = cleaned_data.get(u'partner_type') - cso_type = cleaned_data.get(u'cso_type') + partner_type = cleaned_data.get('partner_type') + cso_type = cleaned_data.get('cso_type') if partner_type and partner_type == PartnerType.CIVIL_SOCIETY_ORGANIZATION and not cso_type: raise ValidationError( - _(u'You must select a type for this CSO') + _('You must select a type for this CSO') ) if partner_type and partner_type != PartnerType.CIVIL_SOCIETY_ORGANIZATION and cso_type: raise ValidationError( - _(u'"CSO Type" does not apply to non-CSO organizations, please remove type') + _('"CSO Type" does not apply to non-CSO organizations, please remove type') ) return cleaned_data diff --git a/src/etools/applications/partners/migrations/0004_fix_null_values.py b/src/etools/applications/partners/migrations/0004_fix_null_values.py index 4d7271b363..6f75c006fd 100644 --- a/src/etools/applications/partners/migrations/0004_fix_null_values.py +++ b/src/etools/applications/partners/migrations/0004_fix_null_values.py @@ -5,7 +5,7 @@ class Migration(migrations.Migration): dependencies = [ - (u'partners', u'0003_auto_20180329_1155'), + ('partners', '0003_auto_20180329_1155'), ] operations = [ diff --git a/src/etools/applications/partners/mixins.py b/src/etools/applications/partners/mixins.py index 7f12a3dc1b..8f6f2a93b5 100644 --- a/src/etools/applications/partners/mixins.py +++ b/src/etools/applications/partners/mixins.py @@ -7,7 +7,7 @@ class HiddenPartnerMixin(object): def formfield_for_foreignkey(self, db_field, request=None, **kwargs): - if db_field.name == u'partner': + if db_field.name == 'partner': kwargs['queryset'] = PartnerOrganization.objects.filter(hidden=False) return super().formfield_for_foreignkey( diff --git a/src/etools/applications/partners/models.py b/src/etools/applications/partners/models.py index c57bf9c959..efd115cc4b 100644 --- a/src/etools/applications/partners/models.py +++ b/src/etools/applications/partners/models.py @@ -1098,7 +1098,7 @@ class Agreement(TimeStampedModel): MOU = 'MOU' SSFA = 'SSFA' AGREEMENT_TYPES = ( - (PCA, u"Programme Cooperation Agreement"), + (PCA, "Programme Cooperation Agreement"), (SSFA, 'Small Scale Funding Agreement'), (MOU, 'Memorandum of Understanding'), ) diff --git a/src/etools/applications/partners/serializers/dashboards.py b/src/etools/applications/partners/serializers/dashboards.py index f28d475942..7207297e99 100644 --- a/src/etools/applications/partners/serializers/dashboards.py +++ b/src/etools/applications/partners/serializers/dashboards.py @@ -70,7 +70,7 @@ def get_disbursement_percent(self, obj): return None if not (self.fr_currencies_ok(obj) and obj.max_fr_currency == obj.planned_budget.currency): - return u"!Error! (currencies do not match)" + return "!Error! (currencies do not match)" percent = obj.frs__actual_amt_local__sum / obj.total_unicef_cash * 100 \ if obj.total_unicef_cash and obj.total_unicef_cash > 0 else 0 return "%.1f" % percent diff --git a/src/etools/applications/partners/serializers/partner_organization_v2.py b/src/etools/applications/partners/serializers/partner_organization_v2.py index f53b819d67..88ceb161f6 100644 --- a/src/etools/applications/partners/serializers/partner_organization_v2.py +++ b/src/etools/applications/partners/serializers/partner_organization_v2.py @@ -366,7 +366,7 @@ class Meta: extra_kwargs = { "partner_type": { "error_messages": { - "null": u'Vendor number must belong to PRG2 account group' + "null": 'Vendor number must belong to PRG2 account group' } } } diff --git a/src/etools/applications/partners/synchronizers.py b/src/etools/applications/partners/synchronizers.py index eb455cd24e..7dee4193cc 100644 --- a/src/etools/applications/partners/synchronizers.py +++ b/src/etools/applications/partners/synchronizers.py @@ -78,7 +78,7 @@ class PartnerSynchronizer(VisionDataSynchronizer): } def _convert_records(self, records): - return json.loads(records)[u'ROWSET'][u'ROW'] + return json.loads(records)['ROWSET']['ROW'] def _filter_records(self, records): records = super()._filter_records(records) @@ -229,7 +229,7 @@ def _partner_save(self, partner, full_sync=True): processed = 1 except Exception: - logger.exception(u'Exception occurred during Partner Sync') + logger.exception('Exception occurred during Partner Sync') return processed @@ -245,10 +245,10 @@ def _save_records(self, records): @staticmethod def get_cso_type(partner): cso_type_mapping = { - 'INTERNATIONAL NGO': u'International', - 'NATIONAL NGO': u'National', - 'COMMUNITY BASED ORGANIZATION': u'Community Based Organization', - 'ACADEMIC INSTITUTION': u'Academic Institution' + 'INTERNATIONAL NGO': 'International', + 'NATIONAL NGO': 'National', + 'COMMUNITY BASED ORGANIZATION': 'Community Based Organization', + 'ACADEMIC INSTITUTION': 'Academic Institution' } if 'CSO_TYPE' in partner and partner['CSO_TYPE'].upper() in cso_type_mapping: return cso_type_mapping[partner['CSO_TYPE'].upper()] @@ -256,10 +256,10 @@ def get_cso_type(partner): @staticmethod def get_partner_type(partner): type_mapping = { - 'BILATERAL / MULTILATERAL': u'Bilateral / Multilateral', - 'CIVIL SOCIETY ORGANIZATION': u'Civil Society Organization', - 'GOVERNMENT': u'Government', - 'UN AGENCY': u'UN Agency', + 'BILATERAL / MULTILATERAL': 'Bilateral / Multilateral', + 'CIVIL SOCIETY ORGANIZATION': 'Civil Society Organization', + 'GOVERNMENT': 'Government', + 'UN AGENCY': 'UN Agency', } return type_mapping.get(partner['PARTNER_TYPE_DESC'].upper(), None) diff --git a/src/etools/applications/partners/tests/test_api_interventions.py b/src/etools/applications/partners/tests/test_api_interventions.py index af2d31abb0..ba4bb7a97c 100644 --- a/src/etools/applications/partners/tests/test_api_interventions.py +++ b/src/etools/applications/partners/tests/test_api_interventions.py @@ -1573,7 +1573,7 @@ def setUp(self): kwargs={'intervention_pk': self.intervention.id} ) - self.uploaded_file = SimpleUploadedFile('hello_world.txt', u'hello world!'.encode('utf-8')) + self.uploaded_file = SimpleUploadedFile('hello_world.txt', 'hello world!'.encode('utf-8')) self.data = { "types": InterventionAmendment.DATES, "signed_date": datetime.date.today(), @@ -1632,7 +1632,7 @@ def test_create_amendment_invalid_file(self): ) self.assertEquals(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEquals(response.data['signed_amendment'], [u'No file was submitted.']) + self.assertEquals(response.data['signed_amendment'], ['No file was submitted.']) response = self._make_request( user=self.partnership_manager_user, @@ -1643,7 +1643,7 @@ def test_create_amendment_invalid_file(self): self.assertEquals(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEquals( response.data['signed_amendment'], - [u'The submitted data was not a file. Check the encoding type on the form.'] + ['The submitted data was not a file. Check the encoding type on the form.'] ) def test_create_amendment_invalid_date(self): @@ -1659,7 +1659,7 @@ def test_create_amendment_invalid_date(self): ) self.assertEquals(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEquals(next(iter(response.data.values())), [u'Date cannot be in the future!']) + self.assertEquals(next(iter(response.data.values())), ['Date cannot be in the future!']) def test_create_amendment_success(self): response = self._make_request( @@ -1768,7 +1768,7 @@ def test_create_amendment_when_already_in_amendment(self): self.assertEquals(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEquals( next(iter(response.data.values())), - [u'Cannot add a new amendment while another amendment is in progress.'] + ['Cannot add a new amendment while another amendment is in progress.'] ) def _make_request(self, user=None, data=None, request_format='json', **kwargs): diff --git a/src/etools/applications/partners/tests/test_export_agreement.py b/src/etools/applications/partners/tests/test_export_agreement.py index 294b9bf246..5aedbf847b 100644 --- a/src/etools/applications/partners/tests/test_export_agreement.py +++ b/src/etools/applications/partners/tests/test_export_agreement.py @@ -93,13 +93,13 @@ def test_csv_export_api(self): self.agreement.agreement_type, '{}'.format(self.agreement.start), '{}'.format(self.agreement.end), - u'', + '', '{}'.format(self.agreement.signed_by_partner_date), self.unicef_staff.get_full_name(), '{}'.format(self.agreement.signed_by_unicef_date), ', '.join([sm.get_full_name() for sm in self.agreement.authorized_officers.all()]), - u'', - u'https://testserver/pmp/agreements/{}/details/'.format(self.agreement.id), + '', + 'https://testserver/pmp/agreements/{}/details/'.format(self.agreement.id), 'No', )) diff --git a/src/etools/applications/partners/tests/test_export_intervention.py b/src/etools/applications/partners/tests/test_export_intervention.py index 6fa27485fe..469b3a1242 100644 --- a/src/etools/applications/partners/tests/test_export_intervention.py +++ b/src/etools/applications/partners/tests/test_export_intervention.py @@ -174,40 +174,40 @@ def test_csv_export_api(self): str(self.intervention.title), '{}'.format(self.intervention.start), '{}'.format(self.intervention.end), - u'', - u'', - u'', + '', + '', + '', str("Yes" if self.intervention.contingency_pd else "No"), - u'', - u'', - u'', + '', + '', + '', str(self.ib.currency), - u'{:.2f}'.format(self.intervention.total_partner_contribution), - u'{:.2f}'.format(self.intervention.total_unicef_cash), - u'{:.2f}'.format(self.intervention.total_in_kind_amount), - u'{:.2f}'.format(self.intervention.total_budget), - u', '.join([fr.fr_numbers for fr in self.intervention.frs.all()]), - u'', - u'', - u'', - u'', - u'', - u'N/A', + '{:.2f}'.format(self.intervention.total_partner_contribution), + '{:.2f}'.format(self.intervention.total_unicef_cash), + '{:.2f}'.format(self.intervention.total_in_kind_amount), + '{:.2f}'.format(self.intervention.total_budget), + ', '.join([fr.fr_numbers for fr in self.intervention.frs.all()]), + '', + '', + '', + '', + '', + 'N/A', '{}'.format(self.intervention.submission_date), '{}'.format(self.intervention.submission_date_prc), '{}'.format(self.intervention.review_date_prc), - u'{}'.format(self.intervention.partner_authorized_officer_signatory.get_full_name()), + '{}'.format(self.intervention.partner_authorized_officer_signatory.get_full_name()), '{}'.format(self.intervention.signed_by_unicef_date), self.unicef_staff.get_full_name(), '{}'.format(self.intervention.signed_by_partner_date), '{}'.format(self.intervention.days_from_submission_to_signed), '{}'.format(self.intervention.days_from_review_to_signed), str(self.intervention.amendments.count()), - u'', + '', str(', '.join(['{}'.format(att.type.name) for att in self.intervention.attachments.all()])), str(self.intervention.attachments.count()), - u'', - u'https://testserver/pmp/interventions/{}/details/'.format(self.intervention.id), + '', + 'https://testserver/pmp/interventions/{}/details/'.format(self.intervention.id), )) def test_csv_flat_export_api(self): diff --git a/src/etools/applications/partners/tests/test_export_partner.py b/src/etools/applications/partners/tests/test_export_partner.py index b3d56fe38a..5497b41eec 100644 --- a/src/etools/applications/partners/tests/test_export_partner.py +++ b/src/etools/applications/partners/tests/test_export_partner.py @@ -107,22 +107,22 @@ def test_csv_export_api(self): self.partner.short_name, self.partner.alternate_name, "{}".format(self.partner.partner_type), - u', '.join([x for x in self.partner.shared_with]), + ', '.join([x for x in self.partner.shared_with]), self.partner.address, self.partner.phone_number, self.partner.email, self.partner.rating, - u'{}'.format(self.partner.core_values_assessment_date), - u'{:.2f}'.format(self.partner.total_ct_cp), - u'{:.2f}'.format(self.partner.total_ct_ytd), + '{}'.format(self.partner.core_values_assessment_date), + '{:.2f}'.format(self.partner.total_ct_cp), + '{:.2f}'.format(self.partner.total_ct_ytd), deleted_flag, blocked, self.partner.type_of_assessment, - u'{}'.format(self.partner.last_assessment_date), - u'', + '{}'.format(self.partner.last_assessment_date), + '', test_option[18], - u'https://testserver/pmp/partners/{}/details/'.format(self.partner.id), - u'{} (Q1:{} Q2:{}, Q3:{}, Q4:{})'.format( + 'https://testserver/pmp/partners/{}/details/'.format(self.partner.id), + '{} (Q1:{} Q2:{}, Q3:{}, Q4:{})'.format( self.planned_visit.year, self.planned_visit.programmatic_q1, self.planned_visit.programmatic_q2, diff --git a/src/etools/applications/partners/tests/test_exports.py b/src/etools/applications/partners/tests/test_exports.py index dc5d6cc0e9..a855b7b6b3 100644 --- a/src/etools/applications/partners/tests/test_exports.py +++ b/src/etools/applications/partners/tests/test_exports.py @@ -162,44 +162,44 @@ def test_intervention_export_api(self): str(self.intervention.title), '{}'.format(self.intervention.start), '{}'.format(self.intervention.end), - u'', - u'', - u'', + '', + '', + '', str("Yes" if self.intervention.contingency_pd else "No"), - u'', - u'', - u'', + '', + '', + '', str(self.ib.currency), - u'{:.2f}'.format(self.intervention.total_partner_contribution), - u'{:.2f}'.format(self.intervention.total_unicef_cash), - u'{:.2f}'.format(self.intervention.total_in_kind_amount), - u'{:.2f}'.format(self.intervention.total_budget), - u', '.join([fr.fr_numbers for fr in self.intervention.frs.all()]), - u'', - u'', - u'', - u'', - u'', - u'{} (Q1:{} Q2:{}, Q3:{}, Q4:{})'.format(self.planned_visit.year, - self.planned_visit.programmatic_q1, - self.planned_visit.programmatic_q2, - self.planned_visit.programmatic_q3, - self.planned_visit.programmatic_q4, ), + '{:.2f}'.format(self.intervention.total_partner_contribution), + '{:.2f}'.format(self.intervention.total_unicef_cash), + '{:.2f}'.format(self.intervention.total_in_kind_amount), + '{:.2f}'.format(self.intervention.total_budget), + ', '.join([fr.fr_numbers for fr in self.intervention.frs.all()]), + '', + '', + '', + '', + '', + '{} (Q1:{} Q2:{}, Q3:{}, Q4:{})'.format(self.planned_visit.year, + self.planned_visit.programmatic_q1, + self.planned_visit.programmatic_q2, + self.planned_visit.programmatic_q3, + self.planned_visit.programmatic_q4), '{}'.format(self.intervention.submission_date), '{}'.format(self.intervention.submission_date_prc), '{}'.format(self.intervention.review_date_prc), - u'{}'.format(self.intervention.partner_authorized_officer_signatory.get_full_name()), + '{}'.format(self.intervention.partner_authorized_officer_signatory.get_full_name()), '{}'.format(self.intervention.signed_by_partner_date), self.unicef_staff.get_full_name(), '{}'.format(self.intervention.signed_by_unicef_date), '{}'.format(self.intervention.days_from_submission_to_signed), '{}'.format(self.intervention.days_from_review_to_signed), str(self.intervention.amendments.count()), - u'', + '', str(', '.join(['{}'.format(att.type.name) for att in self.intervention.attachments.all()])), str(self.intervention.attachments.count()), - u'', - u'https://testserver/pmp/interventions/{}/details/'.format(self.intervention.id), + '', + 'https://testserver/pmp/interventions/{}/details/'.format(self.intervention.id), )) def test_agreement_export_api(self): @@ -241,13 +241,13 @@ def test_agreement_export_api(self): self.agreement.agreement_type, '{}'.format(self.agreement.start), '{}'.format(self.agreement.end), - u'', + '', '{}'.format(self.agreement.signed_by_partner_date), self.unicef_staff.get_full_name(), '{}'.format(self.agreement.signed_by_unicef_date), ', '.join([sm.get_full_name() for sm in self.agreement.authorized_officers.all()]), - u'', - u'https://testserver/pmp/agreements/{}/details/'.format(self.agreement.id), + '', + 'https://testserver/pmp/agreements/{}/details/'.format(self.agreement.id), 'No', ) ) @@ -296,23 +296,23 @@ def test_partners_export_api(self): self.partner.short_name, self.partner.alternate_name, "{}".format(self.partner.partner_type), - u', '.join([x for x in self.partner.shared_with]), + ', '.join([x for x in self.partner.shared_with]), self.partner.address, self.partner.phone_number, self.partner.email, self.partner.rating, - u'{}'.format(self.partner.core_values_assessment_date), - u'{:.2f}'.format(self.partner.total_ct_cp), - u'{:.2f}'.format(self.partner.total_ct_ytd), + '{}'.format(self.partner.core_values_assessment_date), + '{:.2f}'.format(self.partner.total_ct_cp), + '{:.2f}'.format(self.partner.total_ct_ytd), deleted_flag, blocked, self.partner.type_of_assessment, - u'{}'.format(self.partner.last_assessment_date), - u'', + '{}'.format(self.partner.last_assessment_date), + '', ', '.join(["{} ({})".format(sm.get_full_name(), sm.email) for sm in self.partner.staff_members.filter(active=True).all()]), - u'https://testserver/pmp/partners/{}/details/'.format(self.partner.id), - u'{} (Q1:{} Q2:{}, Q3:{}, Q4:{})'.format( + 'https://testserver/pmp/partners/{}/details/'.format(self.partner.id), + '{} (Q1:{} Q2:{}, Q3:{}, Q4:{})'.format( self.planned_visit.year, self.planned_visit.programmatic_q1, self.planned_visit.programmatic_q2, diff --git a/src/etools/applications/partners/tests/test_models.py b/src/etools/applications/partners/tests/test_models.py index 23248ae49d..bda3305725 100644 --- a/src/etools/applications/partners/tests/test_models.py +++ b/src/etools/applications/partners/tests/test_models.py @@ -162,7 +162,7 @@ class TestHACTCalculations(BaseTenantTestCase): def setUpTestData(cls): year = datetime.date.today().year cls.intervention = InterventionFactory( - status=u'active' + status='active' ) current_cp = CountryProgrammeFactory( name='Current Country Programme', @@ -1454,11 +1454,11 @@ class TestStrUnicodeSlow(BaseTenantTestCase): def test_assessment(self): partner = PartnerFactory(name='xyz') instance = AssessmentFactory(partner=partner) - self.assertIn(u'xyz', str(instance)) + self.assertIn('xyz', str(instance)) - partner = PartnerFactory(name=u'R\xe4dda Barnen') + partner = PartnerFactory(name='R\xe4dda Barnen') instance = AssessmentFactory(partner=partner) - self.assertIn(u'R\xe4dda Barnen', str(instance)) + self.assertIn('R\xe4dda Barnen', str(instance)) def test_agreement_amendment(self): partner = PartnerFactory(name='xyz') @@ -1474,42 +1474,42 @@ class TestStrUnicode(SimpleTestCase): def test_workspace_file_type(self): instance = WorkspaceFileTypeFactory.build(name='xyz') - self.assertEqual(str(instance), u'xyz') + self.assertEqual(str(instance), 'xyz') - instance = WorkspaceFileTypeFactory.build(name=u'R\xe4dda Barnen') - self.assertEqual(str(instance), u'R\xe4dda Barnen') + instance = WorkspaceFileTypeFactory.build(name='R\xe4dda Barnen') + self.assertEqual(str(instance), 'R\xe4dda Barnen') def test_partner_organization(self): instance = PartnerFactory.build(name='xyz') - self.assertEqual(str(instance), u'xyz') + self.assertEqual(str(instance), 'xyz') - instance = PartnerFactory.build(name=u'R\xe4dda Barnen') - self.assertEqual(str(instance), u'R\xe4dda Barnen') + instance = PartnerFactory.build(name='R\xe4dda Barnen') + self.assertEqual(str(instance), 'R\xe4dda Barnen') def test_partner_staff_member(self): partner = PartnerFactory.build(name='partner') instance = PartnerStaffFactory.build(first_name='xyz', partner=partner) - self.assertTrue(str(instance).startswith(u'xyz')) + self.assertTrue(str(instance).startswith('xyz')) - instance = PartnerStaffFactory.build(first_name=u'R\xe4dda Barnen', partner=partner) - self.assertTrue(str(instance).startswith(u'R\xe4dda Barnen')) + instance = PartnerStaffFactory.build(first_name='R\xe4dda Barnen', partner=partner) + self.assertTrue(str(instance).startswith('R\xe4dda Barnen')) def test_agreement(self): partner = PartnerFactory.build(name='xyz') instance = AgreementFactory.build(partner=partner) - self.assertIn(u'xyz', str(instance)) + self.assertIn('xyz', str(instance)) - partner = PartnerFactory.build(name=u'R\xe4dda Barnen') + partner = PartnerFactory.build(name='R\xe4dda Barnen') instance = AgreementFactory.build(partner=partner) - self.assertIn(u'R\xe4dda Barnen', str(instance)) + self.assertIn('R\xe4dda Barnen', str(instance)) def test_intervention(self): instance = InterventionFactory.build(number='two') - self.assertEqual(u'two', str(instance)) + self.assertEqual('two', str(instance)) - instance = InterventionFactory.build(number=u'tv\xe5') - self.assertEqual(u'tv\xe5', str(instance)) + instance = InterventionFactory.build(number='tv\xe5') + self.assertEqual('tv\xe5', str(instance)) def test_intervention_amendment(self): instance = InterventionAmendmentFactory.build() @@ -1520,20 +1520,20 @@ def test_intervention_amendment(self): def test_intervention_result_link(self): intervention = InterventionFactory.build(number='two') instance = InterventionResultLinkFactory.build(intervention=intervention) - self.assertTrue(str(instance).startswith(u'two')) + self.assertTrue(str(instance).startswith('two')) - intervention = InterventionFactory.build(number=u'tv\xe5') + intervention = InterventionFactory.build(number='tv\xe5') instance = InterventionResultLinkFactory.build(intervention=intervention) - self.assertTrue(str(instance).startswith(u'tv\xe5')) + self.assertTrue(str(instance).startswith('tv\xe5')) def test_intervention_budget(self): intervention = InterventionFactory.build(number='two') instance = InterventionBudgetFactory.build(intervention=intervention) - self.assertTrue(str(instance).startswith(u'two')) + self.assertTrue(str(instance).startswith('two')) - intervention = InterventionFactory.build(number=u'tv\xe5') + intervention = InterventionFactory.build(number='tv\xe5') instance = InterventionBudgetFactory.build(intervention=intervention) - self.assertTrue(str(instance).startswith(u'tv\xe5')) + self.assertTrue(str(instance).startswith('tv\xe5')) def test_file_type(self): instance = FileTypeFactory.build() @@ -1544,11 +1544,11 @@ def test_file_type(self): def test_intervention_attachment(self): attachment = SimpleUploadedFile(name='two.txt', content='hello world!'.encode('utf-8')) instance = InterventionAttachmentFactory.build(attachment=attachment) - self.assertEqual(str(instance), u'two.txt') + self.assertEqual(str(instance), 'two.txt') - attachment = SimpleUploadedFile(u'tv\xe5.txt', u'hello world!'.encode('utf-8')) + attachment = SimpleUploadedFile('tv\xe5.txt', 'hello world!'.encode('utf-8')) instance = InterventionAttachmentFactory.build(attachment=attachment) - self.assertEqual(str(instance), u'tv\xe5.txt') + self.assertEqual(str(instance), 'tv\xe5.txt') def test_intervention_reporting_period(self): intervention = InterventionFactory.build(number='two') @@ -1556,9 +1556,9 @@ def test_intervention_reporting_period(self): instance = InterventionReportingPeriodFactory.build(intervention=intervention) self.assertTrue(str(instance).startswith('two')) - intervention = InterventionFactory.build(number=u'tv\xe5') + intervention = InterventionFactory.build(number='tv\xe5') instance = InterventionReportingPeriodFactory.build(intervention=intervention) - self.assertTrue(str(instance).startswith(u'tv\xe5')) + self.assertTrue(str(instance).startswith('tv\xe5')) class TestPlannedEngagement(BaseTenantTestCase): diff --git a/src/etools/applications/partners/tests/test_serializers.py b/src/etools/applications/partners/tests/test_serializers.py index 8fe53f37d0..078e1b2f1a 100644 --- a/src/etools/applications/partners/tests/test_serializers.py +++ b/src/etools/applications/partners/tests/test_serializers.py @@ -695,7 +695,7 @@ def test_retrieve(self): 'address', 'alternate_id', 'alternate_name', 'assessments', 'basis_for_risk_rating', 'blocked', 'city', 'core_values_assessment_date', 'country', 'core_values_assessments', 'created', 'cso_type', 'deleted_flag', 'description', 'email', 'hact_min_requirements', 'hact_values', - 'hidden', u'id', 'interventions', 'last_assessment_date', 'modified', 'name', 'net_ct_cy', 'partner_type', + 'hidden', 'id', 'interventions', 'last_assessment_date', 'modified', 'name', 'net_ct_cy', 'partner_type', 'phone_number', 'planned_engagement', 'postal_code', 'rating', 'reported_cy', 'shared_with', 'short_name', 'staff_members', 'street_address', 'total_ct_cp', 'total_ct_cy', 'total_ct_ytd', 'type_of_assessment', 'vendor_number', 'vision_synced', 'planned_visits', 'manually_blocked', 'flags', 'partner_type_slug', @@ -710,4 +710,4 @@ def test_retrieve(self): self.assertEquals(len(data['staff_members']), 1) self.assertCountEqual(data['staff_members'][0].keys(), [ - 'active', 'created', 'email', 'first_name', u'id', 'last_name', 'modified', 'partner', 'phone', 'title']) + 'active', 'created', 'email', 'first_name', 'id', 'last_name', 'modified', 'partner', 'phone', 'title']) diff --git a/src/etools/applications/partners/tests/test_tasks.py b/src/etools/applications/partners/tests/test_tasks.py index ff17d9ec26..0458fe425c 100644 --- a/src/etools/applications/partners/tests/test_tasks.py +++ b/src/etools/applications/partners/tests/test_tasks.py @@ -32,7 +32,7 @@ def _build_country(name): It exists only in memory. We must be careful not to save this because creating a new Country in the database complicates schemas. """ - country = CountryFactory.build(name=u'Country {}'.format(name.title()), schema_name=name) + country = CountryFactory.build(name='Country {}'.format(name.title()), schema_name=name) # Mock save() to prevent inadvertent database changes. country.save = mock.Mock() diff --git a/src/etools/applications/partners/tests/test_views.py b/src/etools/applications/partners/tests/test_views.py index 6e2675b089..3fbb7d70c8 100644 --- a/src/etools/applications/partners/tests/test_views.py +++ b/src/etools/applications/partners/tests/test_views.py @@ -559,7 +559,7 @@ def test_retrieve_attachment(self): # Now add an attachment. Note that in Python 2, the content must be str, in Python 3 the content must be # bytes. I think the existing code is compatible with both. - self.agreement.attached_agreement = SimpleUploadedFile('hello_world.txt', u'hello world!'.encode('utf-8')) + self.agreement.attached_agreement = SimpleUploadedFile('hello_world.txt', 'hello world!'.encode('utf-8')) self.agreement.save() response_json = self._get_and_assert_response() @@ -596,7 +596,7 @@ def test_retrieve_attachment(self): # Now add an amendment. amendment = AgreementAmendmentFactory(agreement=self.agreement, signed_amendment=None) - amendment.signed_amendment = SimpleUploadedFile('goodbye_world.txt', u'goodbye world!'.encode('utf-8')) + amendment.signed_amendment = SimpleUploadedFile('goodbye_world.txt', 'goodbye world!'.encode('utf-8')) amendment.save() response_json = self._get_and_assert_response() @@ -1246,7 +1246,7 @@ def test_intervention_create_unicef_user_fail(self): data=data ) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - self.assertEqual(response.data['detail'], u'Accessing this item is not allowed.') + self.assertEqual(response.data['detail'], 'Accessing this item is not allowed.') def test_intervention_retrieve_fr_numbers(self): self.fr_header_1.intervention = self.intervention_obj @@ -1398,7 +1398,7 @@ def test_intervention_validation_doctype_pca(self): ) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertIn(u'Agreement selected is not of type SSFA', response.data) + self.assertIn('Agreement selected is not of type SSFA', response.data) def test_intervention_validation_doctype_ssfa(self): self.agreement.agreement_type = Agreement.SSFA diff --git a/src/etools/applications/partners/views/interventions_v2.py b/src/etools/applications/partners/views/interventions_v2.py index 071eb006bc..9de8e27979 100644 --- a/src/etools/applications/partners/views/interventions_v2.py +++ b/src/etools/applications/partners/views/interventions_v2.py @@ -541,7 +541,7 @@ def delete(self, request, *args, **kwargs): # make sure there are no indicators added to this LLO obj = self.get_object() if obj.applied_indicators.exists(): - raise ValidationError(u'This PD Output has indicators related, please remove the indicators first') + raise ValidationError('This PD Output has indicators related, please remove the indicators first') return super().delete(request, *args, **kwargs) @@ -576,8 +576,8 @@ def delete(self, request, *args, **kwargs): # make sure there are no indicators added to this LLO obj = self.get_object() if obj.ll_results.exists(): - raise ValidationError(u'This CP Output cannot be removed from this Intervention because there are nested' - u' Results, please remove all Document Results to continue') + raise ValidationError('This CP Output cannot be removed from this Intervention because there are nested' + ' Results, please remove all Document Results to continue') return super().delete(request, *args, **kwargs) @@ -629,7 +629,7 @@ def delete(self, request, *args, **kwargs): ai = self.get_object() intervention = ai.lower_result.result_link.intervention if not intervention.status == Intervention.DRAFT: - raise ValidationError(u'Deleting an indicator is only possible in status Draft.') + raise ValidationError('Deleting an indicator is only possible in status Draft.') return super().delete(request, *args, **kwargs) diff --git a/src/etools/applications/publics/migrations/0002_fix_null_values.py b/src/etools/applications/publics/migrations/0002_fix_null_values.py index 1e273aa650..3026019ffa 100644 --- a/src/etools/applications/publics/migrations/0002_fix_null_values.py +++ b/src/etools/applications/publics/migrations/0002_fix_null_values.py @@ -5,7 +5,7 @@ class Migration(migrations.Migration): dependencies = [ - (u'publics', u'0001_initial'), + ('publics', '0001_initial'), ] operations = [ diff --git a/src/etools/applications/publics/tests/test_models.py b/src/etools/applications/publics/tests/test_models.py index 9023995f0d..53c351fdfb 100644 --- a/src/etools/applications/publics/tests/test_models.py +++ b/src/etools/applications/publics/tests/test_models.py @@ -21,14 +21,14 @@ class TestStrUnicode(SimpleTestCase): def test_travel_expense_type(self): instance = PublicsTravelExpenseTypeFactory.build(title='xyz') - self.assertEqual(str(instance), u'xyz') + self.assertEqual(str(instance), 'xyz') - instance = PublicsTravelExpenseTypeFactory.build(title=u'R\xe4dda Barnen') - self.assertEqual(str(instance), u'R\xe4dda Barnen') + instance = PublicsTravelExpenseTypeFactory.build(title='R\xe4dda Barnen') + self.assertEqual(str(instance), 'R\xe4dda Barnen') def test_currency(self): instance = PublicsCurrencyFactory.build(name='xyz') - self.assertEqual(str(instance), u'xyz') + self.assertEqual(str(instance), 'xyz') # Polish Zloty instance = PublicsCurrencyFactory.build(name='z\u0142oty') @@ -36,7 +36,7 @@ def test_currency(self): def test_airline(self): instance = PublicsAirlineCompanyFactory.build(name='xyz') - self.assertEqual(str(instance), u'xyz') + self.assertEqual(str(instance), 'xyz') # Myflug (Iceland) instance = PublicsAirlineCompanyFactory.build(name='M\xfdflug') @@ -44,69 +44,69 @@ def test_airline(self): def test_business_region(self): instance = PublicsBusinessRegionFactory.build(name='xyz') - self.assertEqual(str(instance), u'xyz') + self.assertEqual(str(instance), 'xyz') # Ost (East) - instance = PublicsBusinessRegionFactory.build(name=u'\xd6st') - self.assertEqual(str(instance), u'\xd6st') + instance = PublicsBusinessRegionFactory.build(name='\xd6st') + self.assertEqual(str(instance), '\xd6st') def test_business_area(self): instance = PublicsBusinessAreaFactory.build(name='xyz') - self.assertEqual(str(instance), u'xyz') + self.assertEqual(str(instance), 'xyz') # Ost (East) - instance = PublicsBusinessAreaFactory.build(name=u'\xd6st') - self.assertEqual(str(instance), u'\xd6st') + instance = PublicsBusinessAreaFactory.build(name='\xd6st') + self.assertEqual(str(instance), '\xd6st') def test_wbs(self): instance = PublicsWBSFactory.build(name='xyz') - self.assertEqual(str(instance), u'xyz') + self.assertEqual(str(instance), 'xyz') # Ost (East) - instance = PublicsWBSFactory.build(name=u'\xd6st') - self.assertEqual(str(instance), u'\xd6st') + instance = PublicsWBSFactory.build(name='\xd6st') + self.assertEqual(str(instance), '\xd6st') def test_fund(self): instance = PublicsFundFactory.build(name='xyz') - self.assertEqual(str(instance), u'xyz') + self.assertEqual(str(instance), 'xyz') # Ost (East) - instance = PublicsFundFactory.build(name=u'\xd6st') - self.assertEqual(str(instance), u'\xd6st') + instance = PublicsFundFactory.build(name='\xd6st') + self.assertEqual(str(instance), '\xd6st') def test_grant(self): instance = PublicsGrantFactory.build(name='xyz') - self.assertEqual(str(instance), u'xyz') + self.assertEqual(str(instance), 'xyz') # Ost (East) - instance = PublicsGrantFactory.build(name=u'\xd6st') - self.assertEqual(str(instance), u'\xd6st') + instance = PublicsGrantFactory.build(name='\xd6st') + self.assertEqual(str(instance), '\xd6st') def test_country(self): instance = PublicsCountryFactory.build(name='xyz') - self.assertEqual(str(instance), u'xyz') + self.assertEqual(str(instance), 'xyz') # Island (Iceland) - instance = PublicsCountryFactory.build(name=u'\xccsland') - self.assertEqual(str(instance), u'\xccsland') + instance = PublicsCountryFactory.build(name='\xccsland') + self.assertEqual(str(instance), '\xccsland') def test_dsa_region(self): country = PublicsCountryFactory.build(name='xyz') instance = PublicsDSARegionFactory.build(area_name='xyz', country=country) - self.assertEqual(str(instance), u'xyz - xyz') + self.assertEqual(str(instance), 'xyz - xyz') # Island (Iceland) - country = PublicsCountryFactory.build(name=u'\xccsland') + country = PublicsCountryFactory.build(name='\xccsland') instance = PublicsDSARegionFactory.build(area_name='xyz', country=country) - self.assertEqual(str(instance), u'\xccsland - xyz') + self.assertEqual(str(instance), '\xccsland - xyz') def test_dsa_rate(self): country = PublicsCountryFactory.build(name='xyz') region = PublicsDSARegionFactory.build(area_name='xyz', country=country) instance = PublicsDSARateFactory.build(region=region) - self.assertTrue(str(instance).startswith(u'xyz - xyz')) + self.assertTrue(str(instance).startswith('xyz - xyz')) - country = PublicsCountryFactory.build(name=u'\xccsland') + country = PublicsCountryFactory.build(name='\xccsland') region = PublicsDSARegionFactory.build(area_name='xyz', country=country) instance = PublicsDSARateFactory.build(region=region) - self.assertTrue(str(instance).startswith(u'\xccsland - xyz')) + self.assertTrue(str(instance).startswith('\xccsland - xyz')) diff --git a/src/etools/applications/reports/management/commands/init-result-type.py b/src/etools/applications/reports/management/commands/init-result-type.py index a864237ed6..e8353b0ccf 100644 --- a/src/etools/applications/reports/management/commands/init-result-type.py +++ b/src/etools/applications/reports/management/commands/init-result-type.py @@ -18,7 +18,7 @@ def add_arguments(self, parser): @transaction.atomic def handle(self, *args, **options): - logger.info(u'Command started') + logger.info('Command started') countries = Country.objects.exclude(name__iexact='global') if options['schema']: @@ -26,9 +26,9 @@ def handle(self, *args, **options): for country in countries: connection.set_tenant(country) - logger.info(u'Initialization for %s' % country.name) + logger.info('Initialization for %s' % country.name) ResultType.objects.get_or_create(name=ResultType.OUTPUT) ResultType.objects.get_or_create(name=ResultType.OUTCOME) ResultType.objects.get_or_create(name=ResultType.ACTIVITY) - logger.info(u'Command finished') + logger.info('Command finished') diff --git a/src/etools/applications/reports/migrations/0002_fix_null_values.py b/src/etools/applications/reports/migrations/0002_fix_null_values.py index df61fd89d3..5668e0d254 100644 --- a/src/etools/applications/reports/migrations/0002_fix_null_values.py +++ b/src/etools/applications/reports/migrations/0002_fix_null_values.py @@ -5,7 +5,7 @@ class Migration(migrations.Migration): dependencies = [ - (u'reports', u'0001_initial'), + ('reports', '0001_initial'), ] operations = [ diff --git a/src/etools/applications/reports/models.py b/src/etools/applications/reports/models.py index fffb05ddd3..bf0f36e965 100644 --- a/src/etools/applications/reports/models.py +++ b/src/etools/applications/reports/models.py @@ -146,7 +146,7 @@ class Meta: ordering = ['name'] def __str__(self): - return u'{} {}'.format( + return '{} {}'.format( self.alternate_id if self.alternate_id else '', self.name ) @@ -290,8 +290,8 @@ class Meta: @cached_property def result_name(self): - return u'{} {}: {}'.format( - self.code if self.code else u'', + return '{} {}: {}'.format( + self.code if self.code else '', self.result_type.name, self.name ) @@ -300,7 +300,7 @@ def result_name(self): def output_name(self): assert self.result_type.name == ResultType.OUTPUT - return u'{}{}{}'.format( + return '{}{}{}'.format( '[Expired] ' if self.expired else '', 'Special- ' if self.special else '', self.name @@ -319,8 +319,8 @@ def special(self): return self.country_programme.special def __str__(self): - return u'{} {}: {}'.format( - self.code if self.code else u'', + return '{} {}: {}'.format( + self.code if self.code else '', self.result_type.name, self.name ) @@ -359,7 +359,7 @@ class LowerResult(TimeStampedModel): code = models.CharField(verbose_name=_("Code"), max_length=50) def __str__(self): - return u'{}: {}'.format( + return '{}: {}'.format( self.code, self.name ) @@ -769,17 +769,17 @@ class Meta: unique_together = (("name", "result", "sector"),) def __str__(self): - return u'{}{} {} {}'.format( - u'' if self.active else u'[Inactive] ', + return '{}{} {} {}'.format( + '' if self.active else '[Inactive] ', self.name, - u'Baseline: {}'.format(self.baseline) if self.baseline else u'', - u'Target: {}'.format(self.target) if self.target else u'' + 'Baseline: {}'.format(self.baseline) if self.baseline else '', + 'Target: {}'.format(self.target) if self.target else '' ) @property def light_repr(self): - return u'{}{}'.format( - u'' if self.active else u'[Inactive] ', + return '{}{}'.format( + '' if self.active else '[Inactive] ', self.name ) diff --git a/src/etools/applications/reports/tests/test_models.py b/src/etools/applications/reports/tests/test_models.py index 1727bcf31f..d73f27be93 100644 --- a/src/etools/applications/reports/tests/test_models.py +++ b/src/etools/applications/reports/tests/test_models.py @@ -18,65 +18,65 @@ class TestStrUnicode(SimpleTestCase): def test_country_programme(self): instance = CountryProgrammeFactory.build(name='xyz', wbs='xyz') - self.assertEqual(str(instance), u'xyz xyz') + self.assertEqual(str(instance), 'xyz xyz') - instance = CountryProgrammeFactory.build(name=u'\xccsland', wbs='xyz') - self.assertEqual(str(instance), u'\xccsland xyz') + instance = CountryProgrammeFactory.build(name='\xccsland', wbs='xyz') + self.assertEqual(str(instance), '\xccsland xyz') - instance = CountryProgrammeFactory.build(name=u'\xccsland', wbs=u'xyz') - self.assertEqual(str(instance), u'\xccsland xyz') + instance = CountryProgrammeFactory.build(name='\xccsland', wbs='xyz') + self.assertEqual(str(instance), '\xccsland xyz') def test_result_type(self): instance = ResultTypeFactory.build(name='xyz') - self.assertEqual(str(instance), u'xyz') + self.assertEqual(str(instance), 'xyz') - instance = ResultTypeFactory.build(name=u'\xccsland') - self.assertEqual(str(instance), u'\xccsland') + instance = ResultTypeFactory.build(name='\xccsland') + self.assertEqual(str(instance), '\xccsland') def test_section(self): instance = SectionFactory.build(name='xyz') - self.assertEqual(str(instance), u' xyz') + self.assertEqual(str(instance), ' xyz') - instance = SectionFactory.build(name=u'\xccsland') - self.assertEqual(str(instance), u' \xccsland') + instance = SectionFactory.build(name='\xccsland') + self.assertEqual(str(instance), ' \xccsland') def test_result(self): instance = ResultFactory.build(name='xyz') - self.assertTrue(str(instance).endswith(u'xyz')) + self.assertTrue(str(instance).endswith('xyz')) - instance = ResultFactory.build(name=u'\xccsland') - self.assertTrue(str(instance).endswith(u'\xccsland')) + instance = ResultFactory.build(name='\xccsland') + self.assertTrue(str(instance).endswith('\xccsland')) def test_lower_result(self): instance = LowerResultFactory.build(name='xyz', code='xyz') - self.assertEqual(str(instance), u'xyz: xyz') + self.assertEqual(str(instance), 'xyz: xyz') - instance = LowerResultFactory.build(name=u'\xccsland', code='xyz') - self.assertEqual(str(instance), u'xyz: \xccsland') + instance = LowerResultFactory.build(name='\xccsland', code='xyz') + self.assertEqual(str(instance), 'xyz: \xccsland') - instance = LowerResultFactory.build(name=u'\xccsland', code=u'xyz') - self.assertEqual(str(instance), u'xyz: \xccsland') + instance = LowerResultFactory.build(name='\xccsland', code='xyz') + self.assertEqual(str(instance), 'xyz: \xccsland') def test_unit(self): instance = UnitFactory.build(type='xyz') - self.assertTrue(str(instance).endswith(u'xyz')) + self.assertTrue(str(instance).endswith('xyz')) - instance = UnitFactory.build(type=u'\xccsland') - self.assertTrue(str(instance).endswith(u'\xccsland')) + instance = UnitFactory.build(type='\xccsland') + self.assertTrue(str(instance).endswith('\xccsland')) def test_indicator_blueprint(self): instance = IndicatorBlueprintFactory.build(title='xyz') - self.assertEqual(str(instance), u'xyz') + self.assertEqual(str(instance), 'xyz') - instance = IndicatorBlueprintFactory.build(title=u'\xccsland') - self.assertEqual(str(instance), u'\xccsland') + instance = IndicatorBlueprintFactory.build(title='\xccsland') + self.assertEqual(str(instance), '\xccsland') def test_indicator(self): instance = IndicatorFactory.build(name='xyz', active=True) - self.assertEqual(str(instance), u'xyz ') + self.assertEqual(str(instance), 'xyz ') - instance = IndicatorFactory.build(name=u'\xccsland', active=True) - self.assertEqual(str(instance), u'\xccsland ') + instance = IndicatorFactory.build(name='\xccsland', active=True) + self.assertEqual(str(instance), '\xccsland ') class TestQuarter(BaseTenantTestCase): diff --git a/src/etools/applications/t2f/migrations/0002_fix_null_values.py b/src/etools/applications/t2f/migrations/0002_fix_null_values.py index 79ad7cdcb7..1ddf6fbd4b 100644 --- a/src/etools/applications/t2f/migrations/0002_fix_null_values.py +++ b/src/etools/applications/t2f/migrations/0002_fix_null_values.py @@ -5,7 +5,7 @@ class Migration(migrations.Migration): dependencies = [ - (u't2f', u'0001_initial'), + ('t2f', '0001_initial'), ] operations = [ diff --git a/src/etools/applications/t2f/tests/test_models.py b/src/etools/applications/t2f/tests/test_models.py index 7a802a7cb9..fba20b5858 100644 --- a/src/etools/applications/t2f/tests/test_models.py +++ b/src/etools/applications/t2f/tests/test_models.py @@ -12,10 +12,10 @@ class TestStrUnicode(BaseTenantTestCase): def test_travel(self): instance = TravelFactory(reference_number='two') - self.assertEqual(str(instance), u'two') + self.assertEqual(str(instance), 'two') - instance = TravelFactory(reference_number=u'tv\xe5') - self.assertEqual(str(instance), u'tv\xe5') + instance = TravelFactory(reference_number='tv\xe5') + self.assertEqual(str(instance), 'tv\xe5') def test_travel_activity(self): tz = timezone.get_default_timezone() @@ -43,13 +43,13 @@ def test_travel_activity(self): def test_itinerary_item(self): travel = TravelFactory() instance = ItineraryItemFactory(origin='here', destination='there', travel=travel) - self.assertTrue(str(instance).endswith(u'here - there')) + self.assertTrue(str(instance).endswith('here - there')) - instance = ItineraryItemFactory(origin='here', destination=u'G\xf6teborg', travel=travel) - self.assertTrue(str(instance).endswith(u'here - G\xf6teborg')) + instance = ItineraryItemFactory(origin='here', destination='G\xf6teborg', travel=travel) + self.assertTrue(str(instance).endswith('here - G\xf6teborg')) - instance = ItineraryItemFactory(origin=u'Przemy\u015bl', destination=u'G\xf6teborg', travel=travel) - self.assertTrue(str(instance).endswith(u'Przemy\u015bl - G\xf6teborg')) + instance = ItineraryItemFactory(origin='Przemy\u015bl', destination='G\xf6teborg', travel=travel) + self.assertTrue(str(instance).endswith('Przemy\u015bl - G\xf6teborg')) class TestTravelActivity(BaseTenantTestCase): diff --git a/src/etools/applications/t2f/tests/test_travel_details.py b/src/etools/applications/t2f/tests/test_travel_details.py index 540354907a..a77c199d16 100644 --- a/src/etools/applications/t2f/tests/test_travel_details.py +++ b/src/etools/applications/t2f/tests/test_travel_details.py @@ -76,8 +76,8 @@ def test_details_view(self): def test_details_view_with_file(self): attachment = TravelAttachmentFactory( travel=self.travel, - name=u'\u0628\u0631\u0646\u0627\u0645\u062c \u062a\u062f\u0631\u064a\u0628 \u0627\u0644\u0645\u062a\u0627\u0628\u0639\u064a\u0646.pdf', # noqa - file=factory.django.FileField(filename=u'travels/lebanon/24800/\u0628\u0631\u0646\u0627\u0645\u062c_\u062a\u062f\u0631\u064a\u0628_\u0627\u0644\u0645\u062a\u0627\u0628\u0639\u064a\u0646.pdf') # noqa + name='\u0628\u0631\u0646\u0627\u0645\u062c \u062a\u062f\u0631\u064a\u0628 \u0627\u0644\u0645\u062a\u0627\u0628\u0639\u064a\u0646.pdf', # noqa + file=factory.django.FileField(filename='travels/lebanon/24800/\u0628\u0631\u0646\u0627\u0645\u062c_\u062a\u062f\u0631\u064a\u0628_\u0627\u0644\u0645\u062a\u0627\u0628\u0639\u064a\u0646.pdf') # noqa ) with self.assertNumQueries(23): response = self.forced_auth_req( @@ -139,8 +139,8 @@ def test_travel_attachment(self): def test_travel_attachment_nonascii(self): attachment = TravelAttachmentFactory( travel=self.travel, - name=u'\u0628\u0631\u0646\u0627\u0645\u062c \u062a\u062f\u0631\u064a\u0628 \u0627\u0644\u0645\u062a\u0627\u0628\u0639\u064a\u0646.pdf', # noqa - file=factory.django.FileField(filename=u'travels/lebanon/24800/\u0628\u0631\u0646\u0627\u0645\u062c_\u062a\u062f\u0631\u064a\u0628_\u0627\u0644\u0645\u062a\u0627\u0628\u0639\u064a\u0646.pdf') # noqa + name='\u0628\u0631\u0646\u0627\u0645\u062c \u062a\u062f\u0631\u064a\u0628 \u0627\u0644\u0645\u062a\u0627\u0628\u0639\u064a\u0646.pdf', # noqa + file=factory.django.FileField(filename='travels/lebanon/24800/\u0628\u0631\u0646\u0627\u0645\u062c_\u062a\u062f\u0631\u064a\u0628_\u0627\u0644\u0645\u062a\u0627\u0628\u0639\u064a\u0646.pdf') # noqa ) response = self.forced_auth_req( 'get', @@ -507,7 +507,7 @@ def test_activity_results(self): self.assertEqual(response.status_code, 400) response_json = json.loads(response.rendered_content) - self.assertEqual(response_json, {u'activities': [{u'result': [u'This field is required.']}]}) + self.assertEqual(response_json, {'activities': [{'result': ['This field is required.']}]}) def test_itinerary_dates(self): dsaregion = DSARegion.objects.first() @@ -747,7 +747,7 @@ def test_incorrect_itinerary_order(self): response = self.forced_auth_req('post', reverse('t2f:travels:list:index'), data=data, user=self.unicef_staff) response_json = json.loads(response.rendered_content) - itinerary_origin_destination_expectation = [u'Origin should match with the previous destination'] + itinerary_origin_destination_expectation = ['Origin should match with the previous destination'] self.assertEqual(response_json['itinerary'], itinerary_origin_destination_expectation) def test_ta_not_required(self): diff --git a/src/etools/applications/t2f/tests/test_travel_list.py b/src/etools/applications/t2f/tests/test_travel_list.py index 33f11f0054..73c1f74d55 100644 --- a/src/etools/applications/t2f/tests/test_travel_list.py +++ b/src/etools/applications/t2f/tests/test_travel_list.py @@ -184,7 +184,7 @@ def test_sorting(self): # to see if all works (non-500) possible_sort_options = response_json['data'][0].keys() for sort_option in possible_sort_options: - log.debug(u'Trying to sort by %s', sort_option) + log.debug('Trying to sort by %s', sort_option) self.forced_auth_req('get', reverse('t2f:travels:list:index'), data={'sort_by': sort_option, 'reverse': False}, user=self.unicef_staff) diff --git a/src/etools/applications/tpm/tests/test_views.py b/src/etools/applications/tpm/tests/test_views.py index 90367cb246..ee0221154d 100644 --- a/src/etools/applications/tpm/tests/test_views.py +++ b/src/etools/applications/tpm/tests/test_views.py @@ -663,7 +663,7 @@ def test_add(self): request_format='multipart', data={ 'file_type': AttachmentFileTypeFactory(code='tpm_partner').id, - 'file': SimpleUploadedFile('hello_world.txt', u'hello world!'.encode('utf-8')), + 'file': SimpleUploadedFile('hello_world.txt', 'hello world!'.encode('utf-8')), } ) self.assertEqual(response.status_code, status.HTTP_201_CREATED) @@ -678,7 +678,7 @@ def test_not_editable_by_tpm(self): request_format='multipart', data={ 'file_type': AttachmentFileTypeFactory(code='tpm_partner').id, - 'file': SimpleUploadedFile('hello_world.txt', u'hello world!'.encode('utf-8')), + 'file': SimpleUploadedFile('hello_world.txt', 'hello world!'.encode('utf-8')), } ) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) @@ -703,7 +703,7 @@ def test_add(self): request_format='multipart', data={ 'file_type': AttachmentFileTypeFactory(code='tpm').id, - 'file': SimpleUploadedFile('hello_world.txt', u'hello world!'.encode('utf-8')), + 'file': SimpleUploadedFile('hello_world.txt', 'hello world!'.encode('utf-8')), } ) self.assertEqual(create_response.status_code, status.HTTP_201_CREATED) @@ -736,7 +736,7 @@ def test_add(self): request_format='multipart', data={ 'file_type': AttachmentFileTypeFactory(code='tpm_report_attachments').id, - 'file': SimpleUploadedFile('hello_world.txt', u'hello world!'.encode('utf-8')), + 'file': SimpleUploadedFile('hello_world.txt', 'hello world!'.encode('utf-8')), } ) self.assertEqual(create_response.status_code, status.HTTP_201_CREATED) @@ -771,7 +771,7 @@ def test_add(self): data={ 'object_id': self.activity.id, 'file_type': AttachmentFileTypeFactory(code='tpm').id, - 'file': SimpleUploadedFile('hello_world.txt', u'hello world!'.encode('utf-8')), + 'file': SimpleUploadedFile('hello_world.txt', 'hello world!'.encode('utf-8')), } ) self.assertEqual(create_response.status_code, status.HTTP_201_CREATED) @@ -806,7 +806,7 @@ def test_add(self): data={ 'object_id': self.activity.id, 'file_type': AttachmentFileTypeFactory(code='tpm_report').id, - 'file': SimpleUploadedFile('hello_world.txt', u'hello world!'.encode('utf-8')), + 'file': SimpleUploadedFile('hello_world.txt', 'hello world!'.encode('utf-8')), } ) self.assertEqual(create_response.status_code, status.HTTP_201_CREATED) diff --git a/src/etools/applications/tpm/tpmpartners/tasks.py b/src/etools/applications/tpm/tpmpartners/tasks.py index 635393827c..7f77308515 100644 --- a/src/etools/applications/tpm/tpmpartners/tasks.py +++ b/src/etools/applications/tpm/tpmpartners/tasks.py @@ -11,14 +11,14 @@ @app.task def update_tpm_partners(country_name=None): - logger.info(u'Starting update values for TPM partners') + logger.info('Starting update values for TPM partners') countries = Country.objects.filter(vision_sync_enabled=True) processed = [] if country_name is not None: countries = countries.filter(name=country_name) for country in countries: try: - logger.info(u'Starting TPM partners update for country {}'.format( + logger.info('Starting TPM partners update for country {}'.format( country.name )) for partner in TPMPartner.objects.all(): @@ -27,7 +27,7 @@ def update_tpm_partners(country_name=None): object_number=partner.vendor_number ).sync() processed.append(country.name) - logger.info(u"Update finished successfully for {}".format(country.name)) + logger.info("Update finished successfully for {}".format(country.name)) except VisionException: - logger.exception(u"{} sync failed".format(TPMPartnerSynchronizer.__name__)) - logger.info(u'TPM Partners synced successfully for {}.'.format(u', '.join(processed))) + logger.exception("{} sync failed".format(TPMPartnerSynchronizer.__name__)) + logger.info('TPM Partners synced successfully for {}.'.format(', '.join(processed))) diff --git a/src/etools/applications/users/admin.py b/src/etools/applications/users/admin.py index 4220d6308b..5660948362 100644 --- a/src/etools/applications/users/admin.py +++ b/src/etools/applications/users/admin.py @@ -32,13 +32,13 @@ class ProfileInline(admin.StackedInline): 'countries_available', ) search_fields = ( - u'office__name', - u'country__name', - u'user__email' + 'office__name', + 'country__name', + 'user__email' ) readonly_fields = ( - u'user', - u'country', + 'user', + 'country', ) fk_name = 'user' @@ -46,13 +46,13 @@ class ProfileInline(admin.StackedInline): def get_fields(self, request, obj=None): fields = super().get_fields(request, obj=obj) - if not request.user.is_superuser and u'country_override' in fields: - fields.remove(u'country_override') + if not request.user.is_superuser and 'country_override' in fields: + fields.remove('country_override') return fields def formfield_for_manytomany(self, db_field, request=None, **kwargs): - if db_field.name == u'countries_available': + if db_field.name == 'countries_available': if request and request.user.is_superuser: kwargs['queryset'] = Country.objects.all() else: @@ -98,13 +98,13 @@ class ProfileAdmin(admin.ModelAdmin): 'countries_available', ) search_fields = ( - u'office__name', - u'country__name', - u'user__email' + 'office__name', + 'country__name', + 'user__email' ) readonly_fields = ( - u'user', - u'country', + 'user', + 'country', ) def has_add_permission(self, request): @@ -130,13 +130,13 @@ def get_queryset(self, request): def get_fields(self, request, obj=None): fields = super().get_fields(request, obj=obj) - if not request.user.is_superuser and u'country_override' in fields: - fields.remove(u'country_override') + if not request.user.is_superuser and 'country_override' in fields: + fields.remove('country_override') return fields def formfield_for_manytomany(self, db_field, request=None, **kwargs): - if db_field.name == u'countries_available': + if db_field.name == 'countries_available': if request and request.user.is_superuser: kwargs['queryset'] = Country.objects.all() else: @@ -223,7 +223,7 @@ def get_readonly_fields(self, request, obj=None): """ fields = list(super().get_readonly_fields(request, obj)) if not request.user.is_superuser: - fields.append(u'is_superuser') + fields.append('is_superuser') return fields diff --git a/src/etools/applications/users/migrations/0003_fix_null_values.py b/src/etools/applications/users/migrations/0003_fix_null_values.py index e38b163b4f..bbbf81f743 100644 --- a/src/etools/applications/users/migrations/0003_fix_null_values.py +++ b/src/etools/applications/users/migrations/0003_fix_null_values.py @@ -5,7 +5,7 @@ class Migration(migrations.Migration): dependencies = [ - (u'users', u'0002_auto_20180329_2123'), + ('users', '0002_auto_20180329_2123'), ] operations = [ diff --git a/src/etools/applications/users/models.py b/src/etools/applications/users/models.py index cab69896fc..3d5708a19e 100644 --- a/src/etools/applications/users/models.py +++ b/src/etools/applications/users/models.py @@ -272,7 +272,7 @@ def last_name(self): return self.user.last_name def __str__(self): - return u'User profile for {}'.format( + return 'User profile for {}'.format( self.user.get_full_name() ) diff --git a/src/etools/applications/users/tasks.py b/src/etools/applications/users/tasks.py index 1a488033a2..f2cff7e708 100644 --- a/src/etools/applications/users/tasks.py +++ b/src/etools/applications/users/tasks.py @@ -68,7 +68,7 @@ def _set_special_attr(self, obj, attr, cleaned_value): if not obj.country == new_country: obj.country = self._get_country(cleaned_value) obj.countries_available.add(obj.country) - logger.info(u"Country Updated for {}".format(obj)) + logger.info("Country Updated for {}".format(obj)) return True return False @@ -80,10 +80,10 @@ def _set_attribute(self, obj, attr, value): field = obj._meta.get_field(attr) if type(value) == list: - value = u'- '.join([str(val) for val in value]) + value = '- '.join([str(val) for val in value]) if field.get_internal_type() == "CharField" and value and len(value) > field.max_length: cleaned_value = value[:field.max_length] - logger.warn(u'The attribute "%s" was trimmed from "%s" to "%s"' % (attr, value, cleaned_value)) + logger.warn('The attribute "%s" was trimmed from "%s" to "%s"' % (attr, value, cleaned_value)) else: cleaned_value = value if cleaned_value == '': @@ -112,7 +112,7 @@ def create_or_update_user(self, record): status['created'] = int(created) user.set_unusable_password() user.groups.add(self.groups['UNICEF User']) - logger.info(u'Group added to user {}'.format(user)) + logger.info('Group added to user {}'.format(user)) profile, _ = UserProfile.objects.get_or_create(user=user) user_updated = self.update_user(user, record) @@ -122,7 +122,7 @@ def create_or_update_user(self, record): status['updated'] = 1 except IntegrityError as e: - logger.exception(u'Integrity error on user retrieving: {} - exception {}'.format(key_value, e)) + logger.exception('Integrity error on user retrieving: {} - exception {}'.format(key_value, e)) status['created'] = status['updated'] = 0 status['errors'] = 1 @@ -130,12 +130,12 @@ def create_or_update_user(self, record): def record_is_valid(self, record): if self.KEY_ATTRIBUTE not in record: - logger.info(u"Discarding Record {} field is missing".format(self.KEY_ATTRIBUTE)) + logger.info("Discarding Record {} field is missing".format(self.KEY_ATTRIBUTE)) return False for field in self.REQUIRED_USER_FIELDS: if isinstance(field, str): if not record.get(field, False): - logger.info(u"User doesn't have the required fields {} missing".format(field)) + logger.info("User doesn't have the required fields {} missing".format(field)) return False elif isinstance(field, tuple): allowed_values = field[1] @@ -143,7 +143,7 @@ def record_is_valid(self, record): allowed_values = [allowed_values, ] if record.get(field[0], False) not in allowed_values: - logger.debug(u"User is not in Unicef organization {}".format(field[1])) + logger.debug("User is not in Unicef organization {}".format(field[1])) return False return True diff --git a/src/etools/applications/users/tests/test_models.py b/src/etools/applications/users/tests/test_models.py index 3f5c254297..428a019af6 100644 --- a/src/etools/applications/users/tests/test_models.py +++ b/src/etools/applications/users/tests/test_models.py @@ -119,35 +119,35 @@ class TestStrUnicode(SimpleTestCase): def test_country(self): instance = CountryFactory.build(name='xyz') - self.assertEqual(str(instance), u'xyz') + self.assertEqual(str(instance), 'xyz') - instance = CountryFactory.build(name=u'Magyarorsz\xe1g') - self.assertEqual(str(instance), u'Magyarorsz\xe1g') + instance = CountryFactory.build(name='Magyarorsz\xe1g') + self.assertEqual(str(instance), 'Magyarorsz\xe1g') def test_workspace_counter(self): instance = models.WorkspaceCounter() instance.workspace = CountryFactory.build(name='xyz') - self.assertEqual(str(instance), u'xyz') + self.assertEqual(str(instance), 'xyz') instance = models.WorkspaceCounter() - instance.workspace = CountryFactory.build(name=u'Magyarorsz\xe1g') - self.assertEqual(str(instance), u'Magyarorsz\xe1g') + instance.workspace = CountryFactory.build(name='Magyarorsz\xe1g') + self.assertEqual(str(instance), 'Magyarorsz\xe1g') def test_office(self): instance = OfficeFactory.build(name='xyz') - self.assertEqual(str(instance), u'xyz') + self.assertEqual(str(instance), 'xyz') - instance = OfficeFactory.build(name=u'Magyarorsz\xe1g') - self.assertEqual(str(instance), u'Magyarorsz\xe1g') + instance = OfficeFactory.build(name='Magyarorsz\xe1g') + self.assertEqual(str(instance), 'Magyarorsz\xe1g') def test_user_profile(self): UserModel = get_user_model() user = UserModel(first_name='Sviatoslav', last_name='') instance = models.UserProfile() instance.user = user - self.assertEqual(str(instance), u'User profile for Sviatoslav') + self.assertEqual(str(instance), 'User profile for Sviatoslav') - user = UserModel(first_name=u'Sventoslav\u016d') + user = UserModel(first_name='Sventoslav\u016d') instance = models.UserProfile() instance.user = user - self.assertEqual(str(instance), u'User profile for Sventoslav\u016d') + self.assertEqual(str(instance), 'User profile for Sventoslav\u016d') diff --git a/src/etools/applications/users/tests/test_views.py b/src/etools/applications/users/tests/test_views.py index c07f3febe5..2a80dca8f8 100644 --- a/src/etools/applications/users/tests/test_views.py +++ b/src/etools/applications/users/tests/test_views.py @@ -142,7 +142,7 @@ def test_api_users_list_values_bad(self): ) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(response.data, [u'Query parameter values are not integers']) + self.assertEqual(response.data, ['Query parameter values are not integers']) @skip('How to create new schemas?') def test_business_area_code(self): diff --git a/src/etools/applications/users/tests/test_views_v3.py b/src/etools/applications/users/tests/test_views_v3.py index e6a0fe77e5..a73b1bace9 100644 --- a/src/etools/applications/users/tests/test_views_v3.py +++ b/src/etools/applications/users/tests/test_views_v3.py @@ -103,7 +103,7 @@ def test_api_users_list_values_bad(self): ) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(response.data, [u'Query parameter values are not integers']) + self.assertEqual(response.data, ['Query parameter values are not integers']) def test_api_users_list_managers(self): response = self.forced_auth_req( diff --git a/src/etools/applications/vision/migrations/0002_fix_null_values.py b/src/etools/applications/vision/migrations/0002_fix_null_values.py index 72245d36ae..43ebc00928 100644 --- a/src/etools/applications/vision/migrations/0002_fix_null_values.py +++ b/src/etools/applications/vision/migrations/0002_fix_null_values.py @@ -4,7 +4,7 @@ class Migration(migrations.Migration): dependencies = [ - (u'vision', u'0001_initial'), + ('vision', '0001_initial'), ] operations = [ diff --git a/src/etools/applications/vision/models.py b/src/etools/applications/vision/models.py index b0e1357266..56a11d49b4 100644 --- a/src/etools/applications/vision/models.py +++ b/src/etools/applications/vision/models.py @@ -21,4 +21,4 @@ class VisionSyncLog(models.Model): date_processed = models.DateTimeField(auto_now=True, verbose_name=_('Date Processed')) def __str__(self): - return u'{0.country} {0.date_processed}:{0.successful} {0.total_processed}'.format(self) + return '{0.country} {0.date_processed}:{0.successful} {0.total_processed}'.format(self) diff --git a/src/etools/applications/vision/tasks.py b/src/etools/applications/vision/tasks.py index ea1cc0c7db..7e9732df8f 100644 --- a/src/etools/applications/vision/tasks.py +++ b/src/etools/applications/vision/tasks.py @@ -56,7 +56,7 @@ def vision_sync_task(country_name=None, synchronizers=SYNC_HANDLERS.keys()): country.vision_last_synced = timezone.now() country.save() - text = u'Created tasks for the following countries: {} and synchronizers: {}'.format( + text = 'Created tasks for the following countries: {} and synchronizers: {}'.format( ',\n '.join([country.name for country in countries]), ',\n '.join([synchronizer for synchronizer in synchronizers]) ) @@ -70,22 +70,22 @@ def sync_handler(self, country_name, handler): Run .sync() on one handler for one country. """ # Scheduled from vision_sync_task() (above). - logger.info(u'Starting vision sync handler {} for country {}'.format(handler, country_name)) + logger.info('Starting vision sync handler {} for country {}'.format(handler, country_name)) try: country = Country.objects.get(name=country_name) except Country.DoesNotExist: - logger.error(u"{} sync failed, Could not find a Country with this name: {}".format( + logger.error("{} sync failed, Could not find a Country with this name: {}".format( handler, country_name )) # No point in retrying if there's no such country else: try: SYNC_HANDLERS[handler](country).sync() - logger.info(u"{} sync successfully for {}".format(handler, country.name)) + logger.info("{} sync successfully for {}".format(handler, country.name)) except VisionException: # Catch and log the exception so we're aware there's a problem. - logger.exception(u"{} sync failed, Country: {}".format( + logger.exception("{} sync failed, Country: {}".format( handler, country_name )) # The 'autoretry_for' in the task decorator tells Celery to diff --git a/src/etools/applications/vision/tests/test_models.py b/src/etools/applications/vision/tests/test_models.py index 4eefe2b654..68833bae71 100644 --- a/src/etools/applications/vision/tests/test_models.py +++ b/src/etools/applications/vision/tests/test_models.py @@ -10,4 +10,4 @@ class TestStrUnicode(SimpleTestCase): def test_vision_sync_log(self): country = CountryFactory.build(name='M\xe9xico', schema_name='Mexico') instance = VisionSyncLog(country=country) - self.assertTrue(str(instance).startswith(u'M\xe9xico')) + self.assertTrue(str(instance).startswith('M\xe9xico')) diff --git a/src/etools/applications/vision/tests/test_tasks.py b/src/etools/applications/vision/tests/test_tasks.py index 0cf2dbea73..df2f7efd34 100644 --- a/src/etools/applications/vision/tests/test_tasks.py +++ b/src/etools/applications/vision/tests/test_tasks.py @@ -143,7 +143,7 @@ def _assertLoggerMessages(self, mock_logger, tenant_countries_used=None, selecte self.assertEqual(mock_logger.call_count, 1) # Verify that each processed country was sent in the message. For some reason, the public # tenant is not listed in this message even though it was synced. - expected_msg = u'Created tasks for the following countries: {} and synchronizers: {}'.format( + expected_msg = 'Created tasks for the following countries: {} and synchronizers: {}'.format( ',\n '.join([country.name for country in tenant_countries_used]), ',\n '.join([synchronizer for synchronizer in selected_synchronizers]) ) From f56d0f9a7ce01c5dad20b2b4888fa48537559094 Mon Sep 17 00:00:00 2001 From: Domenico DiNicola Date: Fri, 25 Jan 2019 12:40:32 -0500 Subject: [PATCH 06/72] 9394 fixed freeze_hact_data command --- .../applications/hact/management/commands/freeze_hact_data.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/etools/applications/hact/management/commands/freeze_hact_data.py b/src/etools/applications/hact/management/commands/freeze_hact_data.py index 4059a66d9e..c1cca617f3 100644 --- a/src/etools/applications/hact/management/commands/freeze_hact_data.py +++ b/src/etools/applications/hact/management/commands/freeze_hact_data.py @@ -61,7 +61,6 @@ def freeze_data(self, hact_history): self.get_or_empty(partner_hact, ['programmatic_visits', 'completed', 'q3'])), ('Programmatic Visits Completed Q4', self.get_or_empty(partner_hact, ['programmatic_visits', 'completed', 'q4'])), - ('Programmatic Visits Planned M.R', partner.hact_min_requirements.get('programme_visits')), ('Spot Checks Planned Q1', getattr(planned_engagement, 'spot_check_planned_q1', None)), ('Spot Checks Planned Q2', getattr(planned_engagement, 'spot_check_planned_q2', None)), ('Spot Checks Planned Q3', getattr(planned_engagement, 'spot_check_planned_q3', None)), From 01663ade087abeb47c58f1dc1cd4df0e54086ffc Mon Sep 17 00:00:00 2001 From: Domenico DiNicola Date: Wed, 30 Jan 2019 11:36:54 -0500 Subject: [PATCH 07/72] 9577 intervetnion list view allows 'end_after' filter --- src/etools/applications/partners/views/interventions_v2.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/etools/applications/partners/views/interventions_v2.py b/src/etools/applications/partners/views/interventions_v2.py index 071eb006bc..cb16fd70f8 100644 --- a/src/etools/applications/partners/views/interventions_v2.py +++ b/src/etools/applications/partners/views/interventions_v2.py @@ -105,6 +105,7 @@ class InterventionListAPIView(QueryStringFilterMixin, ExportModelMixin, Interven ('unicef_focal_points', 'unicef_focal_points__in'), ('start', 'start__gte'), ('end', 'end__lte'), + ('end_after', 'end__gte'), ('office', 'offices__in'), ('location', 'result_links__ll_results__applied_indicators__locations__name__icontains'), ) From 3cd07211f7ca1af64a8141895706b33b8f3b2fc6 Mon Sep 17 00:00:00 2001 From: denes csaba Date: Fri, 1 Feb 2019 16:50:26 +0200 Subject: [PATCH 08/72] Location import deadlock fix --- src/etools/libraries/locations/admin.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/etools/libraries/locations/admin.py b/src/etools/libraries/locations/admin.py index 7c8e6bf859..72dd5e9907 100644 --- a/src/etools/libraries/locations/admin.py +++ b/src/etools/libraries/locations/admin.py @@ -16,6 +16,7 @@ class EtoolsCartoDBTableAdmin(CartoDBTableAdmin): def import_sites(self, request, queryset): # ensure the location tree is valid before we import/update the data with transaction.atomic(): + Location.objects.all_locations().select_for_update().only('id') Location.objects.rebuild() task_list = [] From 73de6419d1af99d79f8573d198a8f56822ea60f5 Mon Sep 17 00:00:00 2001 From: Greg Reinbach Date: Mon, 4 Feb 2019 04:22:40 -0500 Subject: [PATCH 09/72] Add attachment file type migrations, set label for internal prc review type --- .../migrations/0014_auto_20190204_0915.py | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 src/etools/applications/attachments/migrations/0014_auto_20190204_0915.py diff --git a/src/etools/applications/attachments/migrations/0014_auto_20190204_0915.py b/src/etools/applications/attachments/migrations/0014_auto_20190204_0915.py new file mode 100644 index 0000000000..bfc8474aa8 --- /dev/null +++ b/src/etools/applications/attachments/migrations/0014_auto_20190204_0915.py @@ -0,0 +1,28 @@ +# Generated by Django 2.0.9 on 2019-02-04 09:15 + +from django.db import migrations + +def update_internal_prc_review_file_type(apps, schema_editor): + AttachmentFileType = apps.get_model("unicef_attachments", "filetype") + try: + file_type = AttachmentFileType.objects.get( + code="partners_intervention_amendment_internal_prc_review", + ) + except AttachmentFileType.DoesNotExist: + pass + else: + file_type.label = "Internal PRC Review" + file_type.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('attachments', '0013_attachmentflat_pd_ssfa'), + ] + + operations = [ + migrations.RunPython( + update_internal_prc_review_file_type, + ) + ] From c3f6f997136f9086074d396f74a67e768d65cac9 Mon Sep 17 00:00:00 2001 From: Greg Reinbach Date: Mon, 4 Feb 2019 06:31:10 -0500 Subject: [PATCH 10/72] Add Intervention Draft status notification --- .../send_intervention_draft_notification.py | 8 +++++++ .../notifications/intervention-draft.py | 19 +++++++++++++++ src/etools/applications/partners/tasks.py | 6 +++++ .../partners/tests/test_commands.py | 14 +++++++++++ .../applications/partners/tests/test_tasks.py | 14 +++++++++++ .../applications/partners/tests/test_utils.py | 23 +++++++++++++++++++ src/etools/applications/partners/utils.py | 17 ++++++++++++++ 7 files changed, 101 insertions(+) create mode 100644 src/etools/applications/partners/management/commands/send_intervention_draft_notification.py create mode 100644 src/etools/applications/partners/notifications/intervention-draft.py diff --git a/src/etools/applications/partners/management/commands/send_intervention_draft_notification.py b/src/etools/applications/partners/management/commands/send_intervention_draft_notification.py new file mode 100644 index 0000000000..696462f84d --- /dev/null +++ b/src/etools/applications/partners/management/commands/send_intervention_draft_notification.py @@ -0,0 +1,8 @@ +from django.core.management.base import BaseCommand + +from etools.applications.partners.utils import send_intervention_draft_notification + + +class Command(BaseCommand): + def handle(self, *args, **options): + send_intervention_draft_notification() diff --git a/src/etools/applications/partners/notifications/intervention-draft.py b/src/etools/applications/partners/notifications/intervention-draft.py new file mode 100644 index 0000000000..329c514da6 --- /dev/null +++ b/src/etools/applications/partners/notifications/intervention-draft.py @@ -0,0 +1,19 @@ +name = 'partners/intervention/draft' +defaults = { + 'description': 'Intervention in Draft status.', + 'subject': 'eTools PD/SHPD/SSFA Draft Notification', + 'content': """ + Dear Colleague, + + You are the focal point for Programme Document / SSFA {{ reference_number }} {{ title }}. The document has been in a draft status for some time now. If the document was signed, please complete the appropriate fields in eTools to move the document to status signed. + + If the document is no longer needed, please delete it (you can do so by clicking on the arrow next to the save button and then selecting "delete"). + + If you have any further questions, please contact your eTools focal point or contact eTools support via the support button on the lower right hand side if the eTools Window. + + Thank you. + + Please note that replies to this email message are not monitored and cannot be replied to. + + """ +} diff --git a/src/etools/applications/partners/tasks.py b/src/etools/applications/partners/tasks.py index 3bbbcfc962..ff20c890dd 100644 --- a/src/etools/applications/partners/tasks.py +++ b/src/etools/applications/partners/tasks.py @@ -14,6 +14,7 @@ from etools.applications.partners.models import Agreement, Intervention, PartnerOrganization from etools.applications.partners.utils import ( copy_all_attachments, + send_intervention_draft_notification, send_pca_missing_notifications, send_pca_required_notifications, ) @@ -289,3 +290,8 @@ def check_pca_required(): @app.task def check_pca_missing(): run_on_all_tenants(send_pca_missing_notifications) + + +@app.task +def check_intervention_draft_status(): + run_on_all_tenants(send_intervention_draft_notification) diff --git a/src/etools/applications/partners/tests/test_commands.py b/src/etools/applications/partners/tests/test_commands.py index 3830bd83cc..d605177690 100644 --- a/src/etools/applications/partners/tests/test_commands.py +++ b/src/etools/applications/partners/tests/test_commands.py @@ -353,3 +353,17 @@ def test_command(self): with patch(send_path, mock_send): call_command("send_pca_missing_notifications") self.assertEqual(mock_send.call_count, 1) + + +class TestSendInterventionDraftNotifications(BaseTenantTestCase): + @classmethod + def setUpTestData(cls): + call_command("update_notifications") + + def test_command(self): + send_path = "etools.applications.partners.utils.send_notification_with_template" + InterventionFactory(status=Intervention.DRAFT) + mock_send = Mock() + with patch(send_path, mock_send): + call_command("send_intervention_draft_notification") + self.assertEqual(mock_send.call_count, 1) diff --git a/src/etools/applications/partners/tests/test_tasks.py b/src/etools/applications/partners/tests/test_tasks.py index ff17d9ec26..ad221920e2 100644 --- a/src/etools/applications/partners/tests/test_tasks.py +++ b/src/etools/applications/partners/tests/test_tasks.py @@ -1024,3 +1024,17 @@ def test_command(self): with mock.patch(send_path, mock_send): etools.applications.partners.tasks.check_pca_missing() self.assertEqual(mock_send.call_count, 1) + + +class TestCheckInterventionDraftStatus(BaseTenantTestCase): + @classmethod + def setUpTestData(cls): + call_command("update_notifications") + + def test_task(self): + send_path = "etools.applications.partners.utils.send_notification_with_template" + InterventionFactory(status=Intervention.DRAFT) + mock_send = mock.Mock() + with mock.patch(send_path, mock_send): + etools.applications.partners.tasks.check_intervention_draft_status() + self.assertEqual(mock_send.call_count, 1) diff --git a/src/etools/applications/partners/tests/test_utils.py b/src/etools/applications/partners/tests/test_utils.py index fa18c085b6..44600e0704 100644 --- a/src/etools/applications/partners/tests/test_utils.py +++ b/src/etools/applications/partners/tests/test_utils.py @@ -234,3 +234,26 @@ def test_cp_previous(self): with patch(self.send_path, mock_send): utils.send_pca_missing_notifications() self.assertEqual(mock_send.call_count, 0) + + +class TestSendInterventionDraftNotification(BaseTenantTestCase): + @classmethod + def setUpTestData(cls): + call_command("update_notifications") + cls.send_path = "etools.applications.partners.utils.send_notification_with_template" + + def test_send(self): + InterventionFactory(status=Intervention.DRAFT) + mock_send = Mock() + with patch(self.send_path, mock_send): + utils.send_intervention_draft_notification() + self.assertEqual(mock_send.call_count, 1) + + def test_send_not_draft(self): + intervention = InterventionFactory(status=Intervention.SIGNED) + self.assertTrue(intervention.status != Intervention.DRAFT) + self.assertFalse(Intervention.objects.filter(status=Intervention.DRAFT).exists()) + mock_send = Mock() + with patch(self.send_path, mock_send): + utils.send_intervention_draft_notification() + self.assertEqual(mock_send.call_count, 0) diff --git a/src/etools/applications/partners/utils.py b/src/etools/applications/partners/utils.py index d4fb32e568..2b02eeb9a1 100644 --- a/src/etools/applications/partners/utils.py +++ b/src/etools/applications/partners/utils.py @@ -436,3 +436,20 @@ def send_agreement_suspended_notification(agreement, user): "pd_list": pd_list, # section, pd_number, link } ) + + +def send_intervention_draft_notification(): + """Send an email to PD/SHPD/SSFA's focal point(s) if in draft status""" + for intervention in Intervention.objects.filter(status=Intervention.DRAFT): + recipients = [ + u.user.email for u in intervention.unicef_focal_points.all() + if u.user.email + ] + send_notification_with_template( + recipients=recipients, + template_name="partners/intervention/draft", + context={ + "reference_number": intervention.reference_number, + "title": intervention.title, + } + ) From f7059d7c66d01842f816fdea195883775371e88d Mon Sep 17 00:00:00 2001 From: Greg Reinbach Date: Tue, 5 Feb 2019 06:57:18 -0500 Subject: [PATCH 11/72] Add check for intervention past start --- ...nd_intervention_past_start_notification.py | 8 +++ .../notifications/intervention-past-start.py | 20 ++++++++ src/etools/applications/partners/tasks.py | 6 +++ .../partners/tests/test_commands.py | 19 +++++++ .../applications/partners/tests/test_tasks.py | 18 +++++++ .../applications/partners/tests/test_utils.py | 51 +++++++++++++++++++ src/etools/applications/partners/utils.py | 34 ++++++++++++- 7 files changed, 155 insertions(+), 1 deletion(-) create mode 100644 src/etools/applications/partners/management/commands/send_intervention_past_start_notification.py create mode 100644 src/etools/applications/partners/notifications/intervention-past-start.py diff --git a/src/etools/applications/partners/management/commands/send_intervention_past_start_notification.py b/src/etools/applications/partners/management/commands/send_intervention_past_start_notification.py new file mode 100644 index 0000000000..3c540b1186 --- /dev/null +++ b/src/etools/applications/partners/management/commands/send_intervention_past_start_notification.py @@ -0,0 +1,8 @@ +from django.core.management.base import BaseCommand + +from etools.applications.partners.utils import send_intervention_past_start_notification + + +class Command(BaseCommand): + def handle(self, *args, **options): + send_intervention_past_start_notification() diff --git a/src/etools/applications/partners/notifications/intervention-past-start.py b/src/etools/applications/partners/notifications/intervention-past-start.py new file mode 100644 index 0000000000..1c3444dec8 --- /dev/null +++ b/src/etools/applications/partners/notifications/intervention-past-start.py @@ -0,0 +1,20 @@ +name = 'partners/intervention/past-start' +defaults = { + 'description': 'Intervention past start date.', + 'subject': 'eTools PD/SHPD/SSFA Past Start Notification', + 'content': """ + Dear Colleague, + + You are the focal point for the Programme Document / SSFA {{ reference_number }} {{ title }}. + + Please note that the start date of your document has passed, but as yet no FR information has been entered into eTools. + + Please follow the link below to add the FR information to your document. + + {{ url }} + + Thank you. + + Please note that replies to this email message are not monitored and cannot be replied to. + """ +} diff --git a/src/etools/applications/partners/tasks.py b/src/etools/applications/partners/tasks.py index ff20c890dd..8cc7f5f93a 100644 --- a/src/etools/applications/partners/tasks.py +++ b/src/etools/applications/partners/tasks.py @@ -15,6 +15,7 @@ from etools.applications.partners.utils import ( copy_all_attachments, send_intervention_draft_notification, + send_intervention_past_start_notification, send_pca_missing_notifications, send_pca_required_notifications, ) @@ -295,3 +296,8 @@ def check_pca_missing(): @app.task def check_intervention_draft_status(): run_on_all_tenants(send_intervention_draft_notification) + + +@app.task +def check_intervention_past_start(): + run_on_all_tenants(send_intervention_past_start_notification) diff --git a/src/etools/applications/partners/tests/test_commands.py b/src/etools/applications/partners/tests/test_commands.py index d605177690..8fa20b37a3 100644 --- a/src/etools/applications/partners/tests/test_commands.py +++ b/src/etools/applications/partners/tests/test_commands.py @@ -8,6 +8,7 @@ from etools.applications.attachments.tests.factories import AttachmentFactory, AttachmentFileTypeFactory from etools.applications.EquiTrack.tests.cases import BaseTenantTestCase +from etools.applications.funds.tests.factories import FundsReservationHeaderFactory from etools.applications.partners.models import Agreement, Intervention from etools.applications.partners.tests.factories import ( AgreementAmendmentFactory, @@ -367,3 +368,21 @@ def test_command(self): with patch(send_path, mock_send): call_command("send_intervention_draft_notification") self.assertEqual(mock_send.call_count, 1) + + +class TestCheckInterventionPastStartStatus(BaseTenantTestCase): + @classmethod + def setUpTestData(cls): + call_command("update_notifications") + + def test_task(self): + send_path = "etools.applications.partners.utils.send_notification_with_template" + intervention = InterventionFactory( + status=Intervention.SIGNED, + start=datetime.date.today() - datetime.timedelta(days=2), + ) + FundsReservationHeaderFactory(intervention=intervention) + mock_send = Mock() + with patch(send_path, mock_send): + call_command("send_intervention_past_start_notification") + self.assertEqual(mock_send.call_count, 1) diff --git a/src/etools/applications/partners/tests/test_tasks.py b/src/etools/applications/partners/tests/test_tasks.py index ad221920e2..7c832342cc 100644 --- a/src/etools/applications/partners/tests/test_tasks.py +++ b/src/etools/applications/partners/tests/test_tasks.py @@ -1038,3 +1038,21 @@ def test_task(self): with mock.patch(send_path, mock_send): etools.applications.partners.tasks.check_intervention_draft_status() self.assertEqual(mock_send.call_count, 1) + + +class TestCheckInterventionPastStartStatus(BaseTenantTestCase): + @classmethod + def setUpTestData(cls): + call_command("update_notifications") + + def test_task(self): + send_path = "etools.applications.partners.utils.send_notification_with_template" + intervention = InterventionFactory( + status=Intervention.SIGNED, + start=datetime.date.today() - datetime.timedelta(days=2), + ) + FundsReservationHeaderFactory(intervention=intervention) + mock_send = mock.Mock() + with mock.patch(send_path, mock_send): + etools.applications.partners.tasks.check_intervention_past_start() + self.assertEqual(mock_send.call_count, 1) diff --git a/src/etools/applications/partners/tests/test_utils.py b/src/etools/applications/partners/tests/test_utils.py index 44600e0704..f9ee957851 100644 --- a/src/etools/applications/partners/tests/test_utils.py +++ b/src/etools/applications/partners/tests/test_utils.py @@ -3,9 +3,11 @@ from django.conf import settings from django.core.management import call_command +from django.db.models import Count, OuterRef, Subquery from etools.applications.attachments.tests.factories import AttachmentFileTypeFactory from etools.applications.EquiTrack.tests.cases import BaseTenantTestCase +from etools.applications.funds.models import FundsReservationHeader from etools.applications.funds.tests.factories import FundsReservationHeaderFactory from unicef_locations.tests.factories import GatewayTypeFactory, LocationFactory from etools.applications.partners import utils @@ -257,3 +259,52 @@ def test_send_not_draft(self): with patch(self.send_path, mock_send): utils.send_intervention_draft_notification() self.assertEqual(mock_send.call_count, 0) + + +class TestSendInterventionPastStartNotification(BaseTenantTestCase): + @classmethod + def setUpTestData(cls): + call_command("update_notifications") + cls.send_path = "etools.applications.partners.utils.send_notification_with_template" + + def test_send(self): + intervention = InterventionFactory( + status=Intervention.SIGNED, + start=datetime.date.today() - datetime.timedelta(days=2), + ) + FundsReservationHeaderFactory(intervention=intervention) + mock_send = Mock() + with patch(self.send_path, mock_send): + utils.send_intervention_past_start_notification() + self.assertEqual(mock_send.call_count, 1) + + def test_send_not_signed(self): + intervention = InterventionFactory( + status=Intervention.DRAFT, + start=datetime.date.today() - datetime.timedelta(days=2), + ) + FundsReservationHeaderFactory(intervention=intervention) + mock_send = Mock() + with patch(self.send_path, mock_send): + utils.send_intervention_past_start_notification() + self.assertEqual(mock_send.call_count, 0) + + def test_send_not_past(self): + InterventionFactory( + status=Intervention.SIGNED, + start=datetime.date.today() + datetime.timedelta(days=2), + ) + mock_send = Mock() + with patch(self.send_path, mock_send): + utils.send_intervention_past_start_notification() + self.assertEqual(mock_send.call_count, 0) + + def test_send_has_no_frs(self): + intervention = InterventionFactory( + status=Intervention.SIGNED, + start=datetime.date.today() - datetime.timedelta(days=2), + ) + mock_send = Mock() + with patch(self.send_path, mock_send): + utils.send_intervention_past_start_notification() + self.assertEqual(mock_send.call_count, 0) diff --git a/src/etools/applications/partners/utils.py b/src/etools/applications/partners/utils.py index 2b02eeb9a1..42ccd467d7 100644 --- a/src/etools/applications/partners/utils.py +++ b/src/etools/applications/partners/utils.py @@ -3,13 +3,14 @@ from django.conf import settings from django.contrib.contenttypes.models import ContentType -from django.db.models import F, Q +from django.db.models import Count, F, OuterRef, Q, Subquery from django.urls import reverse from django.utils.timezone import make_aware, now from unicef_attachments.models import Attachment, FileType from unicef_notification.utils import send_notification_with_template +from etools.applications.funds.models import FundsReservationHeader from etools.applications.partners.models import ( Agreement, AgreementAmendment, @@ -453,3 +454,34 @@ def send_intervention_draft_notification(): "title": intervention.title, } ) + + +def send_intervention_past_start_notification(): + """Send an email to PD/SHPD/SSFA's focal point(s) if signed + and start date is past with no FR added""" + frs_count = FundsReservationHeader.objects.filter( + intervention=OuterRef("pk") + ).annotate(count=Count("vendor_code")).values("count") + intervention_qs = Intervention.objects.filter( + status=Intervention.SIGNED, + start__lt=datetime.date.today(), + ).annotate( + frs_count=Subquery(frs_count) + ).filter(frs_count__gt=0) + for intervention in intervention_qs.all(): + recipients = [ + u.user.email for u in intervention.unicef_focal_points.all() + if u.user.email + ] + send_notification_with_template( + recipients=recipients, + template_name="partners/intervention/past-start", + context={ + "reference_number": intervention.reference_number, + "title": intervention.title, + "url": "{}{}".format( + settings.HOST, + intervention.get_object_url(), + ) + } + ) From c0be307c5e8367bae765f00712a251b1ace63d01 Mon Sep 17 00:00:00 2001 From: Greg Reinbach Date: Tue, 5 Feb 2019 06:58:15 -0500 Subject: [PATCH 12/72] flake8 cleanup --- src/etools/applications/partners/tests/test_utils.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/etools/applications/partners/tests/test_utils.py b/src/etools/applications/partners/tests/test_utils.py index f9ee957851..5a34fcbf6d 100644 --- a/src/etools/applications/partners/tests/test_utils.py +++ b/src/etools/applications/partners/tests/test_utils.py @@ -3,11 +3,9 @@ from django.conf import settings from django.core.management import call_command -from django.db.models import Count, OuterRef, Subquery from etools.applications.attachments.tests.factories import AttachmentFileTypeFactory from etools.applications.EquiTrack.tests.cases import BaseTenantTestCase -from etools.applications.funds.models import FundsReservationHeader from etools.applications.funds.tests.factories import FundsReservationHeaderFactory from unicef_locations.tests.factories import GatewayTypeFactory, LocationFactory from etools.applications.partners import utils @@ -300,7 +298,7 @@ def test_send_not_past(self): self.assertEqual(mock_send.call_count, 0) def test_send_has_no_frs(self): - intervention = InterventionFactory( + InterventionFactory( status=Intervention.SIGNED, start=datetime.date.today() - datetime.timedelta(days=2), ) From 12cefa5130024017a18c6bf48e4fe2df9bd2ad38 Mon Sep 17 00:00:00 2001 From: robertavram Date: Wed, 6 Feb 2019 15:18:21 -0500 Subject: [PATCH 13/72] Simplify build --- Dockerfile | 33 ++++++----- Pipfile | 18 +++++- Pipfile.lock | 153 +++++++++++++++++++++++---------------------------- 3 files changed, 101 insertions(+), 103 deletions(-) diff --git a/Dockerfile b/Dockerfile index d5f0bcc6a6..7f7b81adc1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,10 @@ FROM python:3.6.4-alpine as builder # available to install 3.4 # Install dependencies +RUN echo "http://dl-3.alpinelinux.org/alpine/edge/main" >> /etc/apk/repositories RUN apk update +RUN apk add --upgrade apk-tools + RUN apk add \ --update alpine-sdk RUN apk add \ @@ -15,16 +18,12 @@ RUN apk add postgresql-dev RUN apk add libffi-dev RUN apk add jpeg-dev - RUN pip install --upgrade \ setuptools \ pip \ wheel \ pipenv -RUN echo "http://dl-3.alpinelinux.org/alpine/edge/main" >> /etc/apk/repositories -RUN apk update -RUN apk add --upgrade apk-tools RUN apk add openssl RUN apk add ca-certificates RUN apk add libressl2.7-libcrypto @@ -36,22 +35,21 @@ RUN apk add geos-dev --update-cache --repository http://dl-3.alpinelinux.org/alp RUN apk add gcc --update-cache --repository http://dl-3.alpinelinux.org/alpine/edge/testing/ RUN apk add g++ --update-cache --repository http://dl-3.alpinelinux.org/alpine/edge/testing/ -#RUN apk add g++ -#RUN apk add py-setuptools - - # http://gis.stackexchange.com/a/74060 ENV CPLUS_INCLUDE_PATH /usr/include/gdal ENV C_INCLUDE_PATH /usr/include/gdal WORKDIR /etools/ -ADD Pipfile.lock . ADD Pipfile . -ADD requirements.txt . -#RUN pipenv lock -r > requirements.txt -#RUN cat requirements.txt -RUN pip wheel --wheel-dir=/tmp/etwheels -r requirements.txt +ADD Pipfile.lock . +#ADD requirements.txt . + +RUN pipenv install --system --ignore-pipfile --deploy +RUN python -c "import os; print(os.__file__)" +#RUN python -m site +#RUN pip wheel --wheel-dir=/tmp/etwheels -r requirements.txt +# FROM python:3.6.4-alpine RUN echo "http://dl-3.alpinelinux.org/alpine/edge/main" >> /etc/apk/repositories @@ -69,11 +67,12 @@ ENV PYTHONUNBUFFERED 1 ENV PYTHONPATH /code WORKDIR /code/ -COPY --from=builder /tmp/etwheels /tmp/etwheels -COPY --from=builder /etools/requirements.txt /code/requirements.txt -RUN pip install --no-index --find-links=/tmp/etwheels -r /code/requirements.txt +COPY --from=builder /usr/local/lib/python3.6/site-packages /usr/local/lib/python3.6/site-packages +#COPY --from=builder /tmp/etwheels /tmp/etwheels +#COPY --from=builder /etools/requirements.txt /code/requirements.txt +#RUN pip install --no-index --find-links=/tmp/etwheels -r /code/requirements.txt -RUN rm -rf /tmp/etwheels +#RUN rm -rf /tmp/etwheels ENV DJANGO_SETTINGS_MODULE etools.config.settings.production RUN SECRET_KEY=not-so-secret-key-just-for-collectstatic DISABLE_JWT_LOGIN=1 python manage.py collectstatic --noinput diff --git a/Pipfile b/Pipfile index 571101d49c..e09bd4083b 100644 --- a/Pipfile +++ b/Pipfile @@ -3,6 +3,20 @@ name = "pypi" url = "https://pypi.org/simple" verify_ssl = true +[dev-packages] +flake8 = "*" +coverage = "*" +mock = "*" +freezegun = "*" +responses = "*" +isort = "*" +ipython = "*" +pdbpp = "*" +tox = "*" +drf-api-checker = "*" +factory_boy = "==2.8" +django-extensions = "*" +sphinx = "*" [packages] amqp = "==2.2.2" @@ -82,7 +96,7 @@ requests = "==2.11.1" simplejson = "==3.16.0" six = "==1.11.0" social-auth-app-django = "==2.1.0" -social-auth-core = {extras = ["azuread"], version = "==1.7.0"} +social-auth-core = {extras = ["azuread"],version = "==1.7.0"} sqlparse = "==0.2.4" static3 = "==0.7.0" tablib = "==0.12.1" @@ -93,7 +107,6 @@ unicef-locations = "==1.5" unicodecsv = "==0.14.1" uritemplate = "==3.0.0" vine = "==1.1.4" -GDAL = "==2.4.0" webencodings = "==0.5.1" xhtml2pdf = "==0.2.3" xlrd = "==1.1.0" @@ -104,6 +117,7 @@ django_celery_results = "==1.0.1" django-post_office = "==3.1.0" Django = "==2.0.9" et_xmlfile = "==1.0.1" +GDAL = "==2.4.0" Jinja2 = "==2.10" MarkupSafe = "==1.1.0" PyJWT = "==1.5.3" diff --git a/Pipfile.lock b/Pipfile.lock index b1334746f1..dca0aa9a20 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,11 +1,11 @@ { "_meta": { "hash": { - "sha256": "3156cc80efc3673865bcd8f4fc4520f4e15e5297c53de108501c27f161aa371e" + "sha256": "bec262f0380c6aa8b55d1a0509ed59627c26ff002d389cb7945062c87854ff93" }, "pipfile-spec": 6, "requires": { - "python_version": "3.7" + "python_version": "3.6.4" }, "sources": [ { @@ -88,13 +88,6 @@ "index": "pypi", "version": "==4.2.1" }, - "certifi": { - "hashes": [ - "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", - "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" - ], - "version": "==2018.11.29" - }, "cffi": { "hashes": [ "sha256:151b7eefd035c56b2b2e1eb9963c90c6302dc15fbd8c1c0a83a163ff2c7d7743", @@ -133,13 +126,6 @@ "index": "pypi", "version": "==1.11.5" }, - "chardet": { - "hashes": [ - "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", - "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" - ], - "version": "==3.0.4" - }, "coreapi": { "hashes": [ "sha256:46145fcc1f7017c076a2ef684969b641d18a2991051fddec9458ad3f78ffc1cb", @@ -232,7 +218,7 @@ "sha256:b25bc19a589e2851361408708a2aac43524608dd35b85d34a295f90f873a75e5", "sha256:f3be14a02280d9977757acfcc8464d0bb5a24c3ed5fbd88b60ca63bb24fce632" ], - "markers": "python_version != '3.3.*' and python_version != '3.0.*' and python_version != '3.2.*' and python_version >= '2.7' and python_version != '3.1.*'", + "index": "pypi", "version": "==1.2" }, "django-celery-email": { @@ -248,7 +234,7 @@ "sha256:8bca2605eeff4418be7ce428a6958d64bee0f5bdf1f8e563fbc09a9e2f3d990f", "sha256:dfa240fb535a1a2d01c9e605ad71629909318eae6b893c5009eafd7265fde10b" ], - "markers": "python_version != '3.3.*' and python_version != '3.0.*' and python_version != '3.2.*' and python_version >= '2.7' and python_version != '3.1.*'", + "index": "pypi", "version": "==1.0.1" }, "django-contrib-comments": { @@ -356,6 +342,7 @@ "sha256:207b663a05d5d6a62765eb30081093837272a888cf00557d89d0e6f467928871", "sha256:827937a944fe47cea393853069cd9315d080298c8ddb0faf787955d6aa51a030" ], + "index": "pypi", "version": "==3.1.0" }, "django-redis-cache": { @@ -477,6 +464,7 @@ "hashes": [ "sha256:614d9722d572f6246302c4491846d2c393c199cfa4edc9af593437691683335b" ], + "index": "pypi", "version": "==1.0.1" }, "etools-validator": { @@ -502,10 +490,10 @@ }, "gdal": { "hashes": [ - "sha256:1f95b3219616387f3da23c18bea030fa46b4b581091de3aa7c32466d62aade4c" + "sha256:b725a580e6faa0bc17edc3e6caa1da9e6efc401fab19e8482631ee179132b4df" ], "index": "pypi", - "version": "==1.10.0" + "version": "==2.4.0" }, "gunicorn": { "hashes": [ @@ -552,7 +540,6 @@ "sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4" ], "index": "pypi", - "markers": "python_version != '3.3.*' and python_version != '3.0.*' and python_version != '3.2.*' and python_version >= '2.7' and python_version != '3.1.*'", "version": "==2.10" }, "jsonfield": { @@ -603,7 +590,6 @@ "sha256:fb7c206e01ad85ce57feeaaa0bf784b97fa3cad0d4a5737bc5295785f5c613a1" ], "index": "pypi", - "markers": "python_version != '3.3.*' and python_version != '3.0.*' and python_version != '3.2.*' and python_version >= '2.7' and python_version != '3.1.*'", "version": "==1.1.0" }, "newrelic": { @@ -676,7 +662,6 @@ "sha256:ff8cff01582fa1a7e533cb97f628531c4014af4b5f38e33cdcfe5eec29b6d888" ], "index": "pypi", - "markers": "python_version != '3.3.*' and python_version != '3.0.*' and python_version != '3.2.*' and python_version >= '2.7' and python_version != '3.1.*'", "version": "==5.3.0" }, "psycopg2": { @@ -928,13 +913,15 @@ "version": "==2.1.0" }, "social-auth-core": { + "extras": [ + "azuread" + ], "hashes": [ "sha256:273eb5bbeded3cfc178ca7e14f0641165df03133a2f787a6e412f782489d56ba", "sha256:7b393754ab75f6e5176568554f4f7b5cd9e4cb6dab23d9614e5c9e1425f3fcf9", "sha256:eb0d0e29d0cfa729cd52437314d4aeb83806c4d6e7824cbe988195b6a4b85163" ], "index": "pypi", - "markers": null, "version": "==1.7.0" }, "sqlparse": { @@ -983,6 +970,7 @@ "hashes": [ "sha256:86712628bdbbd6c60b4261b720b267c3340a8bf96582f6b1e3a59039d2928953" ], + "index": "pypi", "version": "==0.4.2" }, "unicef-djangolib": { @@ -1004,18 +992,21 @@ "hashes": [ "sha256:06e8971eecae5a86a67aed75be4925aaddc9fac1ea82de02190a9b7a5c59e233" ], + "index": "pypi", "version": "==0.2.0" }, "unicef-restlib": { "hashes": [ "sha256:256f81a7f3ee4ab726af02c0c4b872788675676454d44c9006ce4bec596f7351" ], + "index": "pypi", "version": "==0.3.8" }, "unicef-snapshot": { "hashes": [ "sha256:7083ea95b7794b716eb3e528dc01a4ace7a5f0ab5316f5e4cb2be2cf4ef67d72" ], + "index": "pypi", "version": "==0.2.1" }, "unicodecsv": { @@ -1034,14 +1025,6 @@ "index": "pypi", "version": "==3.0.0" }, - "urllib3": { - "hashes": [ - "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", - "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" - ], - "markers": "python_version != '3.0.*' and python_version != '3.1.*' and python_version >= '2.7' and python_version < '4' and python_version != '3.3.*' and python_version != '3.2.*'", - "version": "==1.24.1" - }, "vine": { "hashes": [ "sha256:52116d59bc45392af9fdd3b75ed98ae48a93e822cee21e5fda249105c59a7a72", @@ -1090,6 +1073,14 @@ ], "version": "==0.7.12" }, + "appnope": { + "hashes": [ + "sha256:5b26757dc6f79a3b7dc9fab95359328d5747fcb2409d331ea66d0272b90ab2a0", + "sha256:8b995ffe925347a2138d7ac0fe77155e4311a0ea6d6da4f5128fe4b3cbe5ed71" + ], + "markers": "sys_platform == 'darwin'", + "version": "==0.1.0" + }, "babel": { "hashes": [ "sha256:6778d85147d5d85345c14a26aada5e478ab04e39b078b0745ee6870c2b5cf669", @@ -1158,18 +1149,18 @@ }, "decorator": { "hashes": [ - "sha256:2c51dff8ef3c447388fe5e4453d24a2bf128d3a4c32af3fabef1f01c6851ab82", - "sha256:c39efa13fbdeb4506c476c9b3babf6a718da943dab7811c206005a4a956c080c" + "sha256:33cd704aea07b4c28b3eb2c97d288a06918275dac0ecebdaf1bc8a48d98adb9e", + "sha256:cabb249f4710888a2fc0e13e9a16c343d932033718ff62e1e9bc93a9d3a9122b" ], - "version": "==4.3.0" + "version": "==4.3.2" }, "django-extensions": { "hashes": [ - "sha256:8317a3fe479b1ba3e3a04ecf33fb8d6ccf09bb18f30eab64e34c40a593741d26", - "sha256:a76a61566f1c8d96acc7bcf765080b8e91367a25a2c6f8c5bddd574493839180" + "sha256:6fcedb2ea660c9dbf9ac59441721ffdd4ab5b753fbd6159c3e28f391a65bab46", + "sha256:a607459e5fa8c579a672131b63366fa52fab80adb2a862d362f5fb48cd2d2cac" ], "index": "pypi", - "version": "==2.1.4" + "version": "==2.1.5" }, "djangorestframework": { "hashes": [ @@ -1194,21 +1185,26 @@ "index": "pypi", "version": "==0.4.1" }, + "entrypoints": { + "hashes": [ + "sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19", + "sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451" + ], + "version": "==0.3" + }, "factory-boy": { "hashes": [ - "sha256:6f25cc4761ac109efd503f096e2ad99421b1159f01a29dbb917359dcd68e08ca", - "sha256:d552cb872b310ae78bd7429bf318e42e1e903b1a109e899a523293dfa762ea4f" + "sha256:4b30ace5690cc286060c676140955ff9f22a8ce2ca6f2ee81b30c561b811a510" ], - "markers": "python_version != '3.0.*' and python_version != '3.1.*' and python_version >= '2.7' and python_version != '3.2.*'", - "version": "==2.11.1" + "index": "pypi", + "version": "==2.8" }, "faker": { "hashes": [ - "sha256:228419b0a788a7ac867ebfafdd438461559ab1a0975edb607300852d9acaa78d", - "sha256:52a3dcc6a565b15fe1c95090321756d5a8a7c1caf5ab3df2f573ed70936ff518" + "sha256:16342dca4d92bfc83bab6a7daf6650e0ab087605a66bc38f17523fdb01757910", + "sha256:d871ea315b2dcba9138b8344f2c131a76ac62d6227ca39f69b0c889fec97376c" ], - "markers": "python_version != '3.0.*' and python_version != '3.1.*' and python_version >= '2.7' and python_version != '3.2.*'", - "version": "==1.0.1" + "version": "==1.0.2" }, "fancycompleter": { "hashes": [ @@ -1225,11 +1221,11 @@ }, "flake8": { "hashes": [ - "sha256:6a35f5b8761f45c5513e3405f110a86bea57982c3b75b766ce7b65217abe1670", - "sha256:c01f8a3963b3571a8e6bd7a4063359aff90749e160778e03817cd9b71c9e07d2" + "sha256:c3ba1e130c813191db95c431a18cb4d20a468e98af7a77e2181b68574481ad36", + "sha256:fd9ddf503110bf3d8b1d270e8c673aab29ccb3dd6abf29bae1f54e5116ab4a91" ], "index": "pypi", - "version": "==3.6.0" + "version": "==3.7.5" }, "freezegun": { "hashes": [ @@ -1252,7 +1248,6 @@ "sha256:3f349de3eb99145973fefb7dbe38554414e5c30abd0c8e4b970a7c9d09f3a1d8", "sha256:f3832918bc3c66617f92e35f5d70729187676313caa60c187eb0f28b8fe5e3b5" ], - "markers": "python_version != '3.2.*' and python_version >= '2.7' and python_version != '3.0.*' and python_version != '3.3.*' and python_version != '3.1.*'", "version": "==1.1.0" }, "ipython": { @@ -1292,7 +1287,6 @@ "sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4" ], "index": "pypi", - "markers": "python_version != '3.3.*' and python_version != '3.0.*' and python_version != '3.2.*' and python_version >= '2.7' and python_version != '3.1.*'", "version": "==2.10" }, "markupsafe": { @@ -1327,7 +1321,6 @@ "sha256:fb7c206e01ad85ce57feeaaa0bf784b97fa3cad0d4a5737bc5295785f5c613a1" ], "index": "pypi", - "markers": "python_version != '3.3.*' and python_version != '3.0.*' and python_version != '3.2.*' and python_version >= '2.7' and python_version != '3.1.*'", "version": "==1.1.0" }, "mccabe": { @@ -1350,29 +1343,28 @@ "sha256:0c98a5d0be38ed775798ece1b9727178c4469d9c3b4ada66e8e6b7849f8732af", "sha256:9e1cbf8c12b1f1ce0bb5344b8d7ecf66a6f8a6e91bcb0c84593ed6d3ab5c4ab3" ], - "markers": "python_version >= '2.6' and python_version != '3.0.*' and python_version != '3.1.*'", "version": "==19.0" }, "parso": { "hashes": [ - "sha256:35704a43a3c113cce4de228ddb39aab374b8004f4f2407d070b6a2ca784ce8a2", - "sha256:895c63e93b94ac1e1690f5fdd40b65f07c8171e3e53cbd7793b5b96c0e0a7f24" + "sha256:6ecf7244be8e7283ec9009c72d074830e7e0e611c974f813d76db0390a4e0dd6", + "sha256:8162be7570ffb34ec0b8d215d7f3b6c5fab24f51eb3886d6dee362de96b6db94" ], - "version": "==0.3.1" + "version": "==0.3.3" }, "pbr": { "hashes": [ - "sha256:f59d71442f9ece3dffc17bc36575768e1ee9967756e6b6535f0ee1f0054c3d68", - "sha256:f6d5b23f226a2ba58e14e49aa3b1bfaf814d0199144b95d78458212444de1387" + "sha256:a7953f66e1f82e4b061f43096a4bcc058f7d3d41de9b94ac871770e8bdd831a2", + "sha256:d717573351cfe09f49df61906cd272abaa759b3e91744396b804965ff7bff38b" ], - "version": "==5.1.1" + "version": "==5.1.2" }, "pdbpp": { "hashes": [ - "sha256:535085916fcfb768690ba0aeab2967c2a2163a0a60e5b703776846873e171399" + "sha256:57eaea444394056c62a28d02280766b8ef3d09077dc194e25079fe49b92912c0" ], "index": "pypi", - "version": "==0.9.3" + "version": "==0.9.5" }, "pexpect": { "hashes": [ @@ -1394,17 +1386,15 @@ "sha256:8ddc32f03971bfdf900a81961a48ccf2fb677cf7715108f85295c67405798616", "sha256:980710797ff6a041e9a73a5787804f848996ecaa6f8a1b1e08224a5894f2074a" ], - "markers": "python_version != '3.0.*' and python_version != '3.1.*' and python_version >= '2.7' and python_version != '3.3.*' and python_version != '3.2.*'", "version": "==0.8.1" }, "prompt-toolkit": { "hashes": [ - "sha256:c1d6aff5252ab2ef391c2fe498ed8c088066f66bc64a8d5c095bbf795d9fec34", - "sha256:d4c47f79b635a0e70b84fdb97ebd9a274203706b1ee5ed44c10da62755cf3ec9", - "sha256:fd17048d8335c1e6d5ee403c3569953ba3eb8555d710bfc548faf0712666ea39" + "sha256:88002cc618cacfda8760c4539e76c3b3f148ecdb7035a3d422c7ecdc90c2a3ba", + "sha256:c6655a12e9b08edb8cf5aeab4815fd1e1bdea4ad73d3bbf269cf2e0c4eb75d5e", + "sha256:df5835fb8f417aa55e5cafadbaeb0cf630a1e824aad16989f9f0493e679ec010" ], - "markers": "python_version != '3.0.*' and python_version != '3.1.*' and python_version >= '2.6'", - "version": "==2.0.7" + "version": "==2.0.8" }, "ptyprocess": { "hashes": [ @@ -1418,23 +1408,21 @@ "sha256:bf92637198836372b520efcba9e020c330123be8ce527e535d185ed4b6f45694", "sha256:e76826342cefe3c3d5f7e8ee4316b80d1dd8a300781612ddbc765c17ba25a6c6" ], - "markers": "python_version != '3.0.*' and python_version != '3.1.*' and python_version >= '2.7' and python_version != '3.3.*' and python_version != '3.2.*'", "version": "==1.7.0" }, "pycodestyle": { "hashes": [ - "sha256:cbc619d09254895b0d12c2c691e237b2e91e9b2ecf5e84c26b35400f93dcfb83", - "sha256:cbfca99bd594a10f674d0cd97a3d802a1fdef635d4361e1a2658de47ed261e3a" + "sha256:95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56", + "sha256:e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c" ], - "version": "==2.4.0" + "version": "==2.5.0" }, "pyflakes": { "hashes": [ - "sha256:9a7662ec724d0120012f6e29d6248ae3727d821bba522a0e6b356eff19126a49", - "sha256:f661252913bc1dbe7fcfcbf0af0db3f42ab65aabd1a6ca68fe5d466bace94dae" + "sha256:5e8c00e30c464c99e0b501dc160b13a14af7f27d4dffb529c556e30a159e231d", + "sha256:f277f9ca3e55de669fba45b7393a1449009cff5a37d1af10ebb76c52765269cd" ], - "markers": "python_version != '3.0.*' and python_version != '3.1.*' and python_version >= '2.7' and python_version != '3.3.*' and python_version != '3.2.*'", - "version": "==2.0.0" + "version": "==2.1.0" }, "pygments": { "hashes": [ @@ -1500,18 +1488,17 @@ }, "sphinx": { "hashes": [ - "sha256:429e3172466df289f0f742471d7e30ba3ee11f3b5aecd9a840480d03f14bcfe5", - "sha256:c4cb17ba44acffae3d3209646b6baec1e215cad3065e852c68cc569d4df1b9f8" + "sha256:b53904fa7cb4b06a39409a492b949193a1b68cc7241a1a8ce9974f86f0d24287", + "sha256:c1c00fc4f6e8b101a0d037065043460dffc2d507257f2f11acaed71fd2b0c83c" ], "index": "pypi", - "version": "==1.8.3" + "version": "==1.8.4" }, "sphinxcontrib-websupport": { "hashes": [ "sha256:68ca7ff70785cbe1e7bccc71a48b5b6d965d79ca50629606c7861a21b206d9dd", "sha256:9de47f375baf1ea07cdb3436ff39d7a9c76042c10a769c52353ec46e4e8fc3b9" ], - "markers": "python_version != '3.2.*' and python_version >= '2.7' and python_version != '3.0.*' and python_version != '3.3.*' and python_version != '3.1.*'", "version": "==1.1.0" }, "text-unidecode": { @@ -1548,16 +1535,14 @@ "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" ], - "markers": "python_version != '3.0.*' and python_version != '3.1.*' and python_version >= '2.7' and python_version < '4' and python_version != '3.3.*' and python_version != '3.2.*'", "version": "==1.24.1" }, "virtualenv": { "hashes": [ - "sha256:34b9ae3742abed2f95d3970acf4d80533261d6061b51160b197f84e5b4c98b4c", - "sha256:fa736831a7b18bd2bfeef746beb622a92509e9733d645952da136b0639cd40cd" + "sha256:58c359370401e0af817fb0070911e599c5fdc836166306b04fd0f278151ed125", + "sha256:729f0bcab430e4ef137646805b5b1d8efbb43fe53d4a0f33328624a84a5121f7" ], - "markers": "python_version != '3.0.*' and python_version != '3.1.*' and python_version >= '2.7' and python_version != '3.3.*' and python_version != '3.2.*'", - "version": "==16.2.0" + "version": "==16.3.0" }, "wcwidth": { "hashes": [ From e587c471b4d8f175fce3c3c453c4f68d6c9486ff Mon Sep 17 00:00:00 2001 From: robertavram Date: Wed, 6 Feb 2019 16:57:18 -0500 Subject: [PATCH 14/72] cleanup --- .circleci/config.yml | 35 +++-------------- .circleci/images/primary/Dockerfile | 59 ++++++++++++++-------------- Dockerfile | 61 +++++++++++------------------ 3 files changed, 58 insertions(+), 97 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 572f443aa3..1cb9e8a111 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -4,7 +4,7 @@ jobs: working_directory: ~/code # The primary container is an instance of the first list image listed. Your build commands run in this container. docker: - - image: unicef/etools:test-base-p3 + - image: unicef/etools:test-base-p3-v2 environment: PGHOST: 127.0.0.1 DATABASE_URL: "postgis://postgres:postgres@localhost:5432/circle_test" @@ -19,10 +19,12 @@ jobs: - checkout - restore_cache: key: deps2-{{ .Branch }}--{{ checksum "Pipfile.lock" }}-{{ checksum ".circleci/config.yml" }} + - run: + name: Install Requirements + command: pipenv install -d --ignore-pipfile - run: name: Run Tests command: | - pip install -q tox pipenv tox -re d20,report - save_cache: key: deps2-{{ .Branch }}--{{ checksum "src/requirements/test.txt" }}-{{ checksum ".circleci/config.yml" }} @@ -37,16 +39,7 @@ jobs: working_directory: ~/code # The primary container is an instance of the first list image listed. Your build commands run in this container. docker: - - image: unicef/etools:test-base-p3 - environment: - PGHOST: 127.0.0.1 - DATABASE_URL: "postgis://postgres:postgres@localhost:5432/circle_test" - - image: circleci/postgres:9.5-alpine-postgis - environment: - POSTGRES_USER: postgres - PGUSER: postgres - POSTGRES_DB: circle_test - POSTGRES_PASSWORD: postgres + - image: unicef/etools:test-base-p3-v2 steps: - checkout - setup_remote_docker: @@ -60,29 +53,11 @@ jobs: curl -L -o /tmp/docker-$VER.tgz https://get.docker.com/builds/Linux/x86_64/docker-$VER.tgz tar -xz -C /tmp -f /tmp/docker-$VER.tgz mv /tmp/docker/* /usr/bin - - run: - name: see all docker containers - command: | - docker ps -a - - run: - name: see if postgres is available locally - command: | - pg_isready - - run: - name: Start DB Image - command: | - docker run --name docker-postgres -e PGUSER=postgres -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=circle_test -d circleci/postgres:9.5-alpine-postgis - run: name: Building the image command: | TAG=${CIRCLE_BRANCH} docker build -t unicef/etools:$TAG . - - run: - name: Test in the image - command: | - TAG=${CIRCLE_BRANCH} - HOSTIP=`/sbin/ip route|awk '/default/ { print $3 }'` - echo "Skipping tests in image for now... to be fixed" - run: name: Pushing to Docker Hub command: | diff --git a/.circleci/images/primary/Dockerfile b/.circleci/images/primary/Dockerfile index 2aef803050..938b3a3166 100644 --- a/.circleci/images/primary/Dockerfile +++ b/.circleci/images/primary/Dockerfile @@ -1,34 +1,35 @@ -FROM python:3.6.4-jessie -# python:3.6.4-jessie has python 2.7 and 3.6 installed, and packages -# available to install 3.4 +FROM python:3.6.4-alpine +# test-base-p3-v2 +RUN echo "http://dl-3.alpinelinux.org/alpine/edge/main" >> /etc/apk/repositories +RUN apk update +RUN apk add --upgrade apk-tools -# Install dependencies -RUN apt-get update -RUN apt-get install -y --no-install-recommends \ - build-essential \ - libcurl4-openssl-dev \ - libjpeg-dev \ - vim \ - ntp \ - git-core -RUN apt-get install -y --no-install-recommends \ - python-pip \ - postgresql-client \ - libpq-dev \ - python3-dev -RUN apt-get install -y --no-install-recommends \ - python-gdal \ - gdal-bin \ - libgdal-dev \ - libgdal1h \ - libgdal1-dev \ +RUN apk add \ + --update alpine-sdk + +RUN apk add openssl \ + ca-certificates \ + libressl2.7-libcrypto +RUN apk add \ libxml2-dev \ libxslt-dev \ - xmlsec1 + xmlsec-dev +RUN apk add postgresql-dev \ + libffi-dev\ + jpeg-dev + +RUN apk add --update-cache --repository http://dl-3.alpinelinux.org/alpine/edge/testing/ \ + gdal \ + gdal-dev \ + py-gdal \ + geos \ + geos-dev \ + gcc \ + g++ -RUN pip install virtualenv +RUN pip install --upgrade \ + setuptools \ + pip \ + wheel \ + pipenv -# http://gis.stackexchange.com/a/74060 -ENV CPLUS_INCLUDE_PATH /usr/include/gdal -ENV C_INCLUDE_PATH /usr/include/gdal -ENV REQUIREMENTS_FILE base.txt diff --git a/Dockerfile b/Dockerfile index 7f7b81adc1..b03ec9b66c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,22 +1,31 @@ FROM python:3.6.4-alpine as builder -# python:3.6.4-jessie has python 2.7 and 3.6 installed, and packages -# available to install 3.4 -# Install dependencies RUN echo "http://dl-3.alpinelinux.org/alpine/edge/main" >> /etc/apk/repositories RUN apk update RUN apk add --upgrade apk-tools RUN apk add \ --update alpine-sdk + +RUN apk add openssl \ + ca-certificates \ + libressl2.7-libcrypto RUN apk add \ libxml2-dev \ libxslt-dev \ xmlsec-dev - -RUN apk add postgresql-dev -RUN apk add libffi-dev -RUN apk add jpeg-dev +RUN apk add postgresql-dev \ + libffi-dev\ + jpeg-dev + +RUN apk add --update-cache --repository http://dl-3.alpinelinux.org/alpine/edge/testing/ \ + gdal \ + gdal-dev \ + py-gdal \ + geos \ + geos-dev \ + gcc \ + g++ RUN pip install --upgrade \ setuptools \ @@ -24,55 +33,31 @@ RUN pip install --upgrade \ wheel \ pipenv -RUN apk add openssl -RUN apk add ca-certificates -RUN apk add libressl2.7-libcrypto -RUN apk add gdal --update-cache --repository http://dl-3.alpinelinux.org/alpine/edge/testing/ -RUN apk add gdal-dev --update-cache --repository http://dl-3.alpinelinux.org/alpine/edge/testing/ -RUN apk add py-gdal --update-cache --repository http://dl-3.alpinelinux.org/alpine/edge/testing/ -RUN apk add geos --update-cache --repository http://dl-3.alpinelinux.org/alpine/edge/testing/ -RUN apk add geos-dev --update-cache --repository http://dl-3.alpinelinux.org/alpine/edge/testing/ -RUN apk add gcc --update-cache --repository http://dl-3.alpinelinux.org/alpine/edge/testing/ -RUN apk add g++ --update-cache --repository http://dl-3.alpinelinux.org/alpine/edge/testing/ - -# http://gis.stackexchange.com/a/74060 -ENV CPLUS_INCLUDE_PATH /usr/include/gdal -ENV C_INCLUDE_PATH /usr/include/gdal - WORKDIR /etools/ ADD Pipfile . ADD Pipfile.lock . -#ADD requirements.txt . - RUN pipenv install --system --ignore-pipfile --deploy -RUN python -c "import os; print(os.__file__)" -#RUN python -m site -#RUN pip wheel --wheel-dir=/tmp/etwheels -r requirements.txt -# + FROM python:3.6.4-alpine RUN echo "http://dl-3.alpinelinux.org/alpine/edge/main" >> /etc/apk/repositories RUN apk update RUN apk add --upgrade apk-tools RUN apk add postgresql-client -RUN apk add openssl -RUN apk add ca-certificates -RUN apk add libressl2.7-libcrypto +RUN apk add openssl \ + ca-certificates \ + libressl2.7-libcrypto RUN apk add gdal --update-cache --repository http://dl-3.alpinelinux.org/alpine/edge/testing/ ADD src /code/ ADD manage.py /code/manage.py -ENV PYTHONUNBUFFERED 1 -ENV PYTHONPATH /code + WORKDIR /code/ COPY --from=builder /usr/local/lib/python3.6/site-packages /usr/local/lib/python3.6/site-packages -#COPY --from=builder /tmp/etwheels /tmp/etwheels -#COPY --from=builder /etools/requirements.txt /code/requirements.txt -#RUN pip install --no-index --find-links=/tmp/etwheels -r /code/requirements.txt - -#RUN rm -rf /tmp/etwheels +ENV PYTHONUNBUFFERED 1 +ENV PYTHONPATH /code ENV DJANGO_SETTINGS_MODULE etools.config.settings.production RUN SECRET_KEY=not-so-secret-key-just-for-collectstatic DISABLE_JWT_LOGIN=1 python manage.py collectstatic --noinput From 1282c177effcd5d822ba8f7c8a296941243a173d Mon Sep 17 00:00:00 2001 From: Greg Reinbach Date: Thu, 7 Feb 2019 05:11:45 -0500 Subject: [PATCH 15/72] Generate final report when finalizing engagement objects --- src/etools/applications/audit/models.py | 79 ++++++++++++++++++- src/etools/applications/audit/tests/base.py | 14 +++- .../audit/tests/test_transitions.py | 31 ++++++-- src/etools/applications/audit/utils.py | 27 +++++++ 4 files changed, 141 insertions(+), 10 deletions(-) create mode 100644 src/etools/applications/audit/utils.py diff --git a/src/etools/applications/audit/models.py b/src/etools/applications/audit/models.py index ec0810390d..d8efdf0495 100644 --- a/src/etools/applications/audit/models.py +++ b/src/etools/applications/audit/models.py @@ -30,6 +30,7 @@ ValidateMARiskExtra, ) from etools.applications.audit.transitions.serializers import EngagementCancelSerializer +from etools.applications.audit.utils import generate_final_report from etools.applications.EquiTrack.utils import get_environment from etools.applications.partners.models import PartnerOrganization, PartnerStaffMember from etools.libraries.fsm.views import has_action_permission @@ -272,7 +273,8 @@ def cancel(self, cancel_comment): @transition(status, source=STATUSES.report_submitted, target=STATUSES.final, permission=has_action_permission(action='finalize')) def finalize(self): - self.date_of_final_report = timezone.now() + self.date_of_final_report = timezone.now().date() + self.generate_final_report() def get_object_url(self, **kwargs): return build_frontend_url('ap', 'engagements', self.id, 'overview', **kwargs) @@ -398,6 +400,13 @@ class SpotCheck(Engagement): internal_controls = models.TextField(verbose_name=_('Internal Controls'), blank=True) + final_report = CodedGenericRelation( + Attachment, + verbose_name=_('Spot Check Final Report'), + code='spot_check_final_report', + blank=True, + ) + objects = models.Manager() class Meta: @@ -435,6 +444,18 @@ def finalize(self, *args, **kwargs): def get_object_url(self, **kwargs): return build_frontend_url('ap', 'spot-checks', self.id, 'overview', **kwargs) + def generate_final_report(self): + from etools.applications.audit.serializers.engagement import SpotCheckSerializer + from etools.applications.audit.serializers.export import SpotCheckPDFSerializer + generate_final_report( + self, + 'spot_check_final_report', + SpotCheckSerializer, + SpotCheckPDFSerializer, + 'audit/spotcheck_pdf.html', + 'spotcheck_final_report.pdf', + ) + class Finding(models.Model): PRIORITIES = Choices( @@ -505,6 +526,13 @@ def __str__(self): class MicroAssessment(Engagement): + final_report = CodedGenericRelation( + Attachment, + verbose_name=_('Micro Assessment Final Report'), + code='micro_assessment_final_report', + blank=True, + ) + objects = models.Manager() class Meta: @@ -533,6 +561,18 @@ def submit(self, *args, **kwargs): def get_object_url(self, **kwargs): return build_frontend_url('ap', 'micro-assessments', self.id, 'overview', **kwargs) + def generate_final_report(self): + from etools.applications.audit.serializers.engagement import MicroAssessmentSerializer + from etools.applications.audit.serializers.export import MicroAssessmentPDFSerializer + generate_final_report( + self, + 'micro_assessment_final_report', + MicroAssessmentSerializer, + MicroAssessmentPDFSerializer, + 'audit/microassessment_pdf.html', + 'microassessment_final_report.pdf', + ) + class DetailedFindingInfo(models.Model): finding = models.TextField(verbose_name=_('Description of Finding')) @@ -574,6 +614,13 @@ class Audit(Engagement): verbose_name=_('Audit Opinion'), max_length=20, choices=OPTIONS, default='', blank=True, ) + final_report = CodedGenericRelation( + Attachment, + verbose_name=_('Audit Final Report'), + code='audit_final_report', + blank=True, + ) + objects = models.Manager() class Meta: @@ -619,6 +666,17 @@ def finalize(self, *args, **kwargs): def get_object_url(self, **kwargs): return build_frontend_url('ap', 'audits', self.id, 'overview', **kwargs) + def generate_final_report(self): + from etools.applications.audit.serializers.engagement import AuditSerializer + from etools.applications.audit.serializers.export import AuditPDFSerializer + generate_final_report( + self, + 'audit_final_report', + AuditSerializer, + AuditPDFSerializer, + 'audit/audit_pdf.html', + ) + class FinancialFinding(models.Model): TITLE_CHOICES = Choices( @@ -678,6 +736,13 @@ def __str__(self): class SpecialAudit(Engagement): + final_report = CodedGenericRelation( + Attachment, + verbose_name=_('Special Audit Final Report'), + code='special_audit_final_report', + blank=True, + ) + objects = models.Manager() class Meta: @@ -711,6 +776,18 @@ def finalize(self, *args, **kwargs): def get_object_url(self, **kwargs): return build_frontend_url('ap', 'special-audits', self.id, 'overview', **kwargs) + def generate_final_report(self): + from etools.applications.audit.serializers.engagement import SpecialAuditSerializer + from etools.applications.audit.serializers.export import SpecialAuditPDFSerializer + generate_final_report( + self, + 'special_audit_final_report', + SpecialAuditSerializer, + SpecialAuditPDFSerializer, + 'audit/special_audit_pdf.html', + 'special_audit_final_report.pdf', + ) + class SpecificProcedure(models.Model): audit = models.ForeignKey( diff --git a/src/etools/applications/audit/tests/base.py b/src/etools/applications/audit/tests/base.py index 6eafc3145c..ffac55f90d 100644 --- a/src/etools/applications/audit/tests/base.py +++ b/src/etools/applications/audit/tests/base.py @@ -1,6 +1,7 @@ import os import tempfile from datetime import timedelta +from unittest.mock import Mock, patch from django.conf import settings from django.core.files import File @@ -28,6 +29,10 @@ def setUpTestData(cls): call_command('update_notifications') call_command('update_audit_permissions', verbosity=0) + # ensure media directory exists + if not os.path.exists(settings.MEDIA_ROOT): + os.makedirs(settings.MEDIA_ROOT) + def setUp(self): super().setUp() EmailTemplate.objects.get_or_create(name='audit/staff_member/invite') @@ -51,6 +56,8 @@ def setUp(self): class EngagementTransitionsTestCaseMixin(AuditTestCaseMixin): engagement_factory = None endpoint = '' + mock_filepath = Mock(return_value="{}test.pdf".format(settings.MEDIA_ROOT)) + filepath = "etools.applications.audit.utils.generate_file_path" def _fill_category(self, code, **kwargs): blueprints = RiskBluePrint.objects.filter(category__code=code) @@ -58,7 +65,7 @@ def _fill_category(self, code, **kwargs): RiskFactory(blueprint=blueprint, engagement=self.engagement, **kwargs) def _fill_date_fields(self): - self.engagement.date_of_field_visit = timezone.now() + self.engagement.date_of_field_visit = timezone.now().date() self.engagement.date_of_draft_report_to_ip = self.engagement.date_of_field_visit + timedelta(days=1) self.engagement.date_of_comments_by_ip = self.engagement.date_of_draft_report_to_ip + timedelta(days=1) self.engagement.date_of_draft_report_to_unicef = self.engagement.date_of_comments_by_ip + timedelta(days=1) @@ -100,8 +107,9 @@ def _init_submitted_engagement(self): def _init_finalized_engagement(self): self._init_submitted_engagement() - self.engagement.finalize() - self.engagement.save() + with patch(self.filepath, self.mock_filepath): + self.engagement.finalize() + self.engagement.save() def _init_cancelled_engagement(self): self.engagement.cancel('cancel_comment') diff --git a/src/etools/applications/audit/tests/test_transitions.py b/src/etools/applications/audit/tests/test_transitions.py index 7e29961b69..893cdc2f3a 100644 --- a/src/etools/applications/audit/tests/test_transitions.py +++ b/src/etools/applications/audit/tests/test_transitions.py @@ -1,15 +1,26 @@ import random +from unittest.mock import patch + +from django.contrib.contenttypes.models import ContentType from factory import fuzzy from rest_framework import status +from unicef_attachments.models import Attachment from etools.applications.audit.models import SpecificProcedure from etools.applications.audit.tests.base import EngagementTransitionsTestCaseMixin -from etools.applications.audit.tests.factories import (AuditFactory, KeyInternalControlFactory, - MicroAssessmentFactory, SpecialAuditFactory, SpotCheckFactory,) -from etools.applications.audit.transitions.conditions import (AuditSubmitReportRequiredFieldsCheck, - EngagementSubmitReportRequiredFieldsCheck, - SPSubmitReportRequiredFieldsCheck,) +from etools.applications.audit.tests.factories import ( + AuditFactory, + KeyInternalControlFactory, + MicroAssessmentFactory, + SpecialAuditFactory, + SpotCheckFactory, +) +from etools.applications.audit.transitions.conditions import ( + AuditSubmitReportRequiredFieldsCheck, + EngagementSubmitReportRequiredFieldsCheck, + SPSubmitReportRequiredFieldsCheck, +) from etools.applications.EquiTrack.tests.cases import BaseTenantTestCase @@ -207,7 +218,15 @@ def test_finalize_auditor(self): def test_finalize_focal_point(self): self._init_submitted_engagement() - self._test_finalize(self.unicef_focal_point, status.HTTP_200_OK) + content_type = ContentType.objects.get_for_model(self.engagement) + attachment_qs = Attachment.objects.filter( + code='spot_check_final_report', + content_type=content_type, + object_id=self.engagement.pk, + ) + with patch(self.filepath, self.mock_filepath): + self._test_finalize(self.unicef_focal_point, status.HTTP_200_OK) + self.assertTrue(attachment_qs.exists()) def test_cancel_auditor(self): self._test_cancel(self.auditor, status.HTTP_403_FORBIDDEN) diff --git a/src/etools/applications/audit/utils.py b/src/etools/applications/audit/utils.py new file mode 100644 index 0000000000..7dec9d1d28 --- /dev/null +++ b/src/etools/applications/audit/utils.py @@ -0,0 +1,27 @@ +from django.contrib.contenttypes.models import ContentType + +from easy_pdf.rendering import render_to_pdf +from unicef_attachments.models import Attachment + +from etools.applications.attachments.models import generate_file_path + + +def generate_final_report(obj, code, labels, pdf, template, filename): + labels_serializer = labels(obj) + pdf_serializer = pdf(obj) + context = { + 'engagement': pdf_serializer.data, + 'serializer': labels_serializer, + } + + content_type = ContentType.objects.get_for_model(obj) + attachment, __ = Attachment.objects.get_or_create( + code=code, + content_type=content_type, + object_id=obj.pk, + ) + + with open(generate_file_path(attachment, filename), "wb") as fp: + fp.write(render_to_pdf(template, context)) + attachment.file = fp.name + attachment.save() From fd4f615194d3d13bd3ae00a4c53c9d2d3996857e Mon Sep 17 00:00:00 2001 From: Greg Reinbach Date: Thu, 7 Feb 2019 08:48:56 -0500 Subject: [PATCH 16/72] Update intervention frs calculation query, handle non distinct values --- src/etools/applications/partners/models.py | 24 +++++++--- .../partners/tests/test_api_partners.py | 47 +++++++++++++++++++ 2 files changed, 65 insertions(+), 6 deletions(-) diff --git a/src/etools/applications/partners/models.py b/src/etools/applications/partners/models.py index c57bf9c959..5671ee0f13 100644 --- a/src/etools/applications/partners/models.py +++ b/src/etools/applications/partners/models.py @@ -5,7 +5,7 @@ from django.conf import settings from django.contrib.postgres.fields import ArrayField, JSONField from django.db import connection, models, transaction -from django.db.models import Case, CharField, Count, F, Max, Min, Q, When +from django.db.models import Case, CharField, Count, F, Max, Min, OuterRef, Q, Subquery, Sum, When from django.db.models.signals import post_save, pre_delete from django.urls import reverse from django.utils import timezone @@ -21,6 +21,7 @@ from etools.applications.EquiTrack.encoders import EToolsEncoder from etools.applications.EquiTrack.utils import get_current_year, get_quarter, import_permissions +from etools.applications.funds.models import FundsReservationHeader from etools.applications.partners.validation import interventions as intervention_validation from etools.applications.partners.validation.agreements import ( agreement_transition_to_ended_valid, @@ -31,7 +32,7 @@ from etools.applications.t2f.models import Travel, TravelType from etools.applications.tpm.models import TPMVisit from etools.applications.users.models import Office -from etools.libraries.djangolib.models import DSum, StringConcat +from etools.libraries.djangolib.models import StringConcat def _get_partner_base_path(partner): @@ -1487,6 +1488,9 @@ def detail_qs(self): return qs def frs_qs(self): + frs_query = FundsReservationHeader.objects.filter( + intervention=OuterRef("pk") + ).order_by().values("intervention") qs = self.get_queryset().prefetch_related( # 'frs__fr_items', # TODO: Figure out a way in which to add locations that is more performant @@ -1497,10 +1501,18 @@ def frs_qs(self): Max("frs__end_date"), Min("frs__start_date"), Count("frs__currency", distinct=True), - frs__outstanding_amt_local__sum=DSum("frs__outstanding_amt_local"), - frs__actual_amt_local__sum=DSum("frs__actual_amt_local"), - frs__total_amt_local__sum=DSum("frs__total_amt_local"), - frs__intervention_amt__sum=DSum("frs__intervention_amt"), + frs__outstanding_amt_local__sum=Subquery( + frs_query.annotate(total=Sum("outstanding_amt_local")).values("total")[:1] + ), + frs__actual_amt_local__sum=Subquery( + frs_query.annotate(total=Sum("actual_amt_local")).values("total")[:1] + ), + frs__total_amt_local__sum=Subquery( + frs_query.annotate(total=Sum("total_amt_local")).values("total")[:1] + ), + frs__intervention_amt__sum=Subquery( + frs_query.annotate(total=Sum("intervention_amt")).values("total")[:1] + ), location_p_codes=StringConcat("flat_locations__p_code", separator="|", distinct=True), donors=StringConcat("frs__fr_items__donor", separator="|", distinct=True), donor_codes=StringConcat("frs__fr_items__donor_code", separator="|", distinct=True), diff --git a/src/etools/applications/partners/tests/test_api_partners.py b/src/etools/applications/partners/tests/test_api_partners.py index 4683fda16f..368fedf791 100644 --- a/src/etools/applications/partners/tests/test_api_partners.py +++ b/src/etools/applications/partners/tests/test_api_partners.py @@ -1179,6 +1179,53 @@ def test_api_partners_retreive_actual_fr_amounts(self): self.assertEqual(Decimal(response.data["interventions"][0]["actual_amount"]), Decimal(fr_header_1.actual_amt_local + fr_header_2.actual_amt_local)) + def test_api_partners_retreive_same_fr_amounts(self): + self.intervention.status = Intervention.ACTIVE + self.intervention.save() + fr_header_1 = FundsReservationHeaderFactory( + intervention=self.intervention, + total_amt=Decimal("300.00"), + actual_amt=Decimal("250.00"), + outstanding_amt=Decimal("200.00"), + total_amt_local=Decimal("100.00"), + actual_amt_local=Decimal("50.00"), + outstanding_amt_local=Decimal("20.00"), + intervention_amt=Decimal("10.00"), + ) + fr_header_2 = FundsReservationHeaderFactory( + intervention=self.intervention, + total_amt=Decimal("300.00"), + actual_amt=Decimal("250.00"), + outstanding_amt=Decimal("200.00"), + total_amt_local=Decimal("100.00"), + actual_amt_local=Decimal("50.00"), + outstanding_amt_local=Decimal("20.00"), + intervention_amt=Decimal("10.00"), + ) + + response = self.forced_auth_req( + 'get', + reverse('partners_api:partner-detail', args=[self.partner.pk]), + user=self.unicef_staff, + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual( + Decimal(response.data["interventions"][0]["actual_amount"]), + Decimal(fr_header_1.actual_amt_local + fr_header_2.actual_amt_local) + ) + self.assertEqual( + Decimal(response.data["interventions"][0]["frs_total_frs_amt"]), + Decimal(fr_header_1.total_amt_local + fr_header_2.total_amt_local) + ) + self.assertEqual( + Decimal(response.data["interventions"][0]["frs_total_intervention_amt"]), + Decimal(fr_header_1.intervention_amt + fr_header_2.intervention_amt) + ) + self.assertEqual( + Decimal(response.data["interventions"][0]["frs_total_outstanding_amt"]), + Decimal(fr_header_1.outstanding_amt_local + fr_header_2.outstanding_amt_local) + ) + def test_api_partners_retrieve_staff_members(self): response = self.forced_auth_req( 'get', From 65838e497b344f87ada77025cd455be8acb7a3d7 Mon Sep 17 00:00:00 2001 From: robertavram Date: Thu, 7 Feb 2019 10:06:45 -0500 Subject: [PATCH 17/72] Add runtest after Django override --- .circleci/config.yml | 3 --- .circleci/images/primary/Dockerfile | 2 ++ tox.ini | 7 ++++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 1cb9e8a111..93f9e0fc5c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -19,9 +19,6 @@ jobs: - checkout - restore_cache: key: deps2-{{ .Branch }}--{{ checksum "Pipfile.lock" }}-{{ checksum ".circleci/config.yml" }} - - run: - name: Install Requirements - command: pipenv install -d --ignore-pipfile - run: name: Run Tests command: | diff --git a/.circleci/images/primary/Dockerfile b/.circleci/images/primary/Dockerfile index 53b28e5786..2c37db0bff 100644 --- a/.circleci/images/primary/Dockerfile +++ b/.circleci/images/primary/Dockerfile @@ -27,6 +27,8 @@ RUN apk add --update-cache --repository http://dl-3.alpinelinux.org/alpine/edge/ gcc \ g++ +RUN apk add bash + RUN pip install --upgrade \ setuptools \ pip \ diff --git a/tox.ini b/tox.ini index 5978c23469..9e1ac49c15 100644 --- a/tox.ini +++ b/tox.ini @@ -14,17 +14,18 @@ setenv = commands = pipenv install -d --ignore-pipfile - ./runtests.sh [testenv:d20] commands = - pip install "django>2.0,<2.1" {[testenv]commands} + pip install "django>2.0,<2.1" + ./runtests.sh [testenv:d21] commands = - pip install "django>2.1,<2.2" {[testenv]commands} + pip install "django>2.1,<2.2" + ./runtests.sh [testenv:report] commands = From 653958f201ce6f622d17e1260034cfe79ca27cbf Mon Sep 17 00:00:00 2001 From: robertavram Date: Thu, 7 Feb 2019 10:12:47 -0500 Subject: [PATCH 18/72] Remove old requirements --- requirements.txt | 109 ----------------------- src/requirements/base.txt | 109 ----------------------- src/requirements/input/base.in | 51 ----------- src/requirements/input/test.in | 16 ---- src/requirements/test.txt | 158 --------------------------------- 5 files changed, 443 deletions(-) delete mode 100644 requirements.txt delete mode 100644 src/requirements/base.txt delete mode 100644 src/requirements/input/base.in delete mode 100644 src/requirements/input/test.in delete mode 100644 src/requirements/test.txt diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 2e33f2b3ec..0000000000 --- a/requirements.txt +++ /dev/null @@ -1,109 +0,0 @@ -amqp==2.2.2 -asn1crypto==0.24.0 -azure-common==1.1.8 -azure-nspkg==2.0.0 -azure-storage==0.20.2 -babel==2.6.0 -billiard==3.5.0.3 -carto==1.3.1 -celery==4.2.1 -cffi==1.11.5 -coreapi==2.3.3 -coreschema==0.0.4 -cryptography==2.4.1 -defusedxml==0.5.0 -diff-match-patch==20121119 -dj-database-url==0.5 -dj-static==0.0.6 -django-appconf==1.0.2 -django-celery-beat==1.2 -django-celery-email==2.0.1 -django-celery-results==1.0.1 -django-contrib-comments==1.9 -django-cors-headers==2.4 -django-debug-toolbar==1.11 -django-easy-pdf==0.1.1 -django-filter==2.0 -django-fsm==2.6 -django-import-export==1.1 -django-js-asset==1.1.0 -django-leaflet==0.24 -django-logentry-admin==1.0.4 -django-model-utils==3.1.2 -django-mptt==0.9.1 -django-ordered-model==1.5 -django-post-office==3.1.0 -django-redis-cache==1.8 -django-rest-swagger==2.2 -django-storages==1.6.6 -django-tenants==2.1 -django-timezone-field==3.0 -django-waffle==0.14 -django==2.0.9 -djangorestframework-csv==2.1.0 -djangorestframework-gis==0.14 -djangorestframework-jwt==1.11.0 -djangorestframework-recursive==0.1.2 -djangorestframework-xml==1.3 -djangorestframework==3.9.0 -drf-nested-routers==0.91 -drf-querystringfilter==1.0.0 -drfpasswordless==1.2 -et-xmlfile==1.0.1 -etools-validator==0.3.2 -flower==0.9.2 -future==0.15.2 -gdal==2.4.0 -gunicorn==19.9 -html5lib==1.0.1 -idna==2.6 -itypes==1.1.0 -jdcal==1.4 -jinja2==2.10 -jsonfield==2.0.2 -kombu==4.2.1 -markupsafe==1.1.0 -newrelic==2.94.0.79 -oauthlib==2.0.7 -odfpy==1.3.6 -openapi-codec==1.3.2 -openpyxl==2.5.9 -pillow==5.3.0 -psycopg2-binary==2.7.5 -psycopg2==2.7.5 -pycparser==2.18 -pyjwt==1.5.3 -pypdf2==1.26.0 -pyrestcli==0.6.4 -python-crontab==2.3.5 -python-dateutil==2.5.3 -python3-openid==3.1.0 -pytz==2018.3 -pyyaml==3.12 -raven==6.9 -redis==2.10.6 -reportlab==3.5.9 -requests-oauthlib==0.8.0 -requests==2.11.1 -simplejson==3.16.0 -six==1.11.0 -social-auth-app-django==2.1.0 -social-auth-core[azuread]==1.7.0 -sqlparse==0.2.4 -static3==0.7.0 -tablib==0.12.1 -tenant-schemas-celery==0.2.1 -tornado==5.1.1 -unicef-attachments==0.4.2 -unicef-djangolib==0.5.2 -unicef-locations==1.5 -unicef-notification==0.2.0 -unicef-restlib==0.3.8 -unicef-snapshot==0.2.1 -unicodecsv==0.14.1 -uritemplate==3.0.0 -vine==1.1.4 -webencodings==0.5.1 -xhtml2pdf==0.2.3 -xlrd==1.1.0 -xlwt==1.3.0 diff --git a/src/requirements/base.txt b/src/requirements/base.txt deleted file mode 100644 index 2ebbbcbf60..0000000000 --- a/src/requirements/base.txt +++ /dev/null @@ -1,109 +0,0 @@ -amqp==2.2.2 # via kombu -asn1crypto==0.24.0 # via cryptography -azure-common==1.1.8 # via azure-storage -azure-nspkg==2.0.0 # via azure-common, azure-storage -azure-storage==0.20.2 -babel==2.6.0 # via flower -billiard==3.5.0.3 # via celery -carto==1.3.1 -celery==4.2.1 -cffi==1.11.5 # via cryptography -coreapi==2.3.3 # via django-rest-swagger, openapi-codec -coreschema==0.0.4 # via coreapi -cryptography==2.4.1 # via social-auth-core -defusedxml==0.5.0 # via djangorestframework-xml, python3-openid, social-auth-core -diff-match-patch==20121119 # via django-import-export -dj-database-url==0.5 -dj-static==0.0.6 -django-appconf==1.0.2 # via django-celery-email -django-celery-beat==1.2 -django-celery-email==2.0.1 -django-celery-results==1.0.1 -django-contrib-comments==1.9 -django-cors-headers==2.4 -django-debug-toolbar==1.11 -django-easy-pdf==0.1.1 -django-filter==2.0 -django-fsm==2.6 -django-import-export==1.1 -django-js-asset==1.1.0 # via django-mptt -django-leaflet==0.24 -django-logentry-admin==1.0.4 -django-model-utils==3.1.2 -django-mptt==0.9.1 -django-ordered-model==1.5 -django-post-office==3.1.0 # via unicef-notification -django-redis-cache==1.8 -django-rest-swagger==2.2 -django-storages==1.6.6 -django-tenants==2.1 -django-timezone-field==3.0 # via django-celery-beat -django-waffle==0.14 -django==2.0.9 -djangorestframework-csv==2.1.0 -djangorestframework-gis==0.14 -djangorestframework-jwt==1.11.0 -djangorestframework-recursive==0.1.2 -djangorestframework-xml==1.3 -djangorestframework==3.9.0 -drf-nested-routers==0.91 -drf-querystringfilter==1.0.0 # via unicef-attachments -drfpasswordless==1.2 -et-xmlfile==1.0.1 # via openpyxl -etools-validator==0.3.2 -flower==0.9.2 -future==0.15.2 # via pyrestcli -gdal==1.10.0 -gunicorn==19.9 -html5lib==1.0.1 # via xhtml2pdf -idna==2.6 # via cryptography -itypes==1.1.0 # via coreapi -jdcal==1.4 # via openpyxl -jinja2==2.10 # via coreschema -jsonfield==2.0.2 # via django-post-office -kombu==4.2.1 # via celery -markupsafe==1.1.0 # via jinja2 -newrelic==2.94.0.79 -oauthlib==2.0.7 # via requests-oauthlib, social-auth-core -odfpy==1.3.6 # via tablib -openapi-codec==1.3.2 # via django-rest-swagger -openpyxl==2.5.9 # via tablib -pillow==5.3.0 # via reportlab, xhtml2pdf -psycopg2-binary==2.7.5 # via django-tenants, unicef-notification, unicef-snapshot -psycopg2==2.7.5 -pycparser==2.18 # via cffi -pyjwt==1.5.3 # via djangorestframework-jwt, social-auth-core -pypdf2==1.26.0 # via xhtml2pdf -pyrestcli==0.6.4 # via carto -python-crontab==2.3.5 # via django-celery-beat -python-dateutil==2.5.3 # via azure-storage, pyrestcli, python-crontab -python3-openid==3.1.0 # via social-auth-core -pytz==2018.3 # via babel, celery, django, django-timezone-field, etools-validator, flower, unicef-attachments, unicef-djangolib, unicef-restlib -pyyaml==3.12 -raven==6.9 -redis==2.10.6 # via django-redis-cache -reportlab==3.5.9 # via django-easy-pdf, xhtml2pdf -requests-oauthlib==0.8.0 # via social-auth-core -requests==2.11.1 # via azure-storage, carto, coreapi, pyrestcli, requests-oauthlib, social-auth-core -simplejson==3.16.0 # via django-rest-swagger -six==1.11.0 # via cryptography, djangorestframework-csv, html5lib, python-dateutil, social-auth-app-django, social-auth-core, unicef-attachments, unicef-djangolib, unicef-restlib, xhtml2pdf -social-auth-app-django==2.1.0 -social-auth-core[azuread]==1.7.0 -sqlparse==0.2.4 # via django-debug-toolbar -static3==0.7.0 # via dj-static -tablib==0.12.1 # via django-import-export -tenant-schemas-celery==0.2.1 -tornado==5.1.1 # via flower -unicef-attachments==0.4.2 -unicef-djangolib==0.5.2 -unicef-locations==1.5 -unicef_notification==0.2.0 -unicef_restlib==0.3.8 -unicef_snapshot==0.2.1 -unicodecsv==0.14.1 # via djangorestframework-csv, tablib, unicef-attachments, unicef-djangolib, unicef-restlib -uritemplate==3.0.0 # via coreapi -vine==1.1.4 # via amqp -webencodings==0.5.1 # via html5lib -xhtml2pdf==0.2.3 # via django-easy-pdf -xlrd==1.1.0 # via tablib -xlwt==1.3.0 # via tablib diff --git a/src/requirements/input/base.in b/src/requirements/input/base.in deleted file mode 100644 index 6b4a8bebc2..0000000000 --- a/src/requirements/input/base.in +++ /dev/null @@ -1,51 +0,0 @@ -azure-storage==0.20.2 -carto==1.3.1 -celery==4.2.1 -dj-database-url==0.5 -dj-static==0.0.6 -Django==2.0.9 -djangorestframework==3.9 -djangorestframework-csv==2.1.0 -djangorestframework-jwt==1.11.0 -djangorestframework-recursive==0.1.2 -djangorestframework-gis==0.14 -djangorestframework-xml==1.3 -django-celery-beat==1.2 -django-celery-email==2.0.1 -django-celery-results==1.0.1 -django-cors-headers==2.4 -django-debug-toolbar==1.11 -django-easy-pdf==0.1.1 -django-filter==2.0 -django-fsm==2.6 -django-import-export==1.1 -django-leaflet==0.24 -django-logentry-admin==1.0.4 -django-model-utils==3.1.2 -django-mptt==0.9.1 -django-ordered-model==1.5 -django-redis-cache==1.8 -django-rest-swagger==2.2 -django-storages==1.6.6 -django-tenants==2.1 -django-waffle==0.14 -django-contrib-comments==1.9 -drf-nested-routers==0.91 -etools-validator==0.3.2 -flower==0.9.2 -GDAL==1.10.0 -gunicorn==19.9 -drfpasswordless==1.2 -newrelic==2.94.0.79 -social-auth-core[azuread]==1.7.0 -social-auth-app-django==2.1.0 -pyyaml==3.12 -psycopg2==2.7.5 -raven==6.9 -tenant-schemas-celery==0.2.1 -unicef-attachments==0.4.2 -unicef-djangolib==0.5.2 -unicef-locations==1.5 -unicef_notification==0.2.0 -unicef_restlib==0.3.8 -unicef_snapshot==0.2.1 diff --git a/src/requirements/input/test.in b/src/requirements/input/test.in deleted file mode 100644 index dee632403f..0000000000 --- a/src/requirements/input/test.in +++ /dev/null @@ -1,16 +0,0 @@ -coverage -factory-boy -Faker -flake8 -freezegun -mock -responses -isort -ipython -pdbpp -tox -drf-api-checker>=0.4 -django-extensions -invoke -pip-tools -sphinx diff --git a/src/requirements/test.txt b/src/requirements/test.txt deleted file mode 100644 index 98a663f27f..0000000000 --- a/src/requirements/test.txt +++ /dev/null @@ -1,158 +0,0 @@ -alabaster==0.7.11 # via sphinx -amqp==2.2.2 -asn1crypto==0.24.0 -azure-common==1.1.8 -azure-nspkg==2.0.0 -azure-storage==0.20.2 -babel==2.6.0 -backcall==0.1.0 # via ipython -billiard==3.5.0.3 -carto==1.3.1 -celery==4.2.1 -cffi==1.11.5 -click==6.7 # via pip-tools -cookies==2.2.1 # via responses -coreapi==2.3.3 -coreschema==0.0.4 -coverage==4.5.1 -cryptography==2.4.1 -decorator==4.3.0 # via ipython, traitlets -defusedxml==0.5.0 -diff-match-patch==20121119 -dj-database-url==0.5 -dj-static==0.0.6 -django-appconf==1.0.2 -django-celery-beat==1.2 -django-celery-email==2.0.1 -django-celery-results==1.0.1 -django-contrib-comments==1.9 -django-cors-headers==2.4 -django-debug-toolbar==1.11 -django-easy-pdf==0.1.1 -django-extensions==2.0.7 -django-filter==2.0 -django-fsm==2.6 -django-import-export==1.1 -django-js-asset==1.1.0 -django-leaflet==0.24 -django-logentry-admin==1.0.4 -django-model-utils==3.1.2 -django-mptt==0.9.1 -django-ordered-model==1.5 -django-post-office==3.1.0 -django-redis-cache==1.8 -django-rest-swagger==2.2 -django-storages==1.6.6 -django-tenants==2.1 -django-timezone-field==3.0 -django-waffle==0.14 -django==2.0.9 -djangorestframework-csv==2.1.0 -djangorestframework-gis==0.14 -djangorestframework-jwt==1.11.0 -djangorestframework-recursive==0.1.2 -djangorestframework-xml==1.3 -djangorestframework==3.9.0 -docutils==0.14 # via sphinx -drf-api-checker==0.4.1 -drf-nested-routers==0.91 -drf-querystringfilter==1.0.0 -drfpasswordless==1.2 -et-xmlfile==1.0.1 -etools-validator==0.3.2 -factory-boy==2.8 -faker==0.8.12 -fancycompleter==0.8 # via pdbpp -first==2.0.1 # via pip-tools -flake8==3.5.0 -flower==0.9.2 -freezegun==0.3.10 -future==0.15.2 -gdal==1.10.0 -gunicorn==19.9 -html5lib==1.0.1 -idna==2.6 -imagesize==1.0.0 # via sphinx -invoke==1.0.0 -ipython-genutils==0.2.0 # via traitlets -ipython==6.3.1 -isort==4.3.4 -itypes==1.1.0 -jdcal==1.4 -jedi==0.12.0 # via ipython -jinja2==2.10 -jsonfield==2.0.2 -kombu==4.2.1 -markupsafe==1.1.0 -mccabe==0.6.1 # via flake8 -mock==2.0.0 -newrelic==2.94.0.79 -oauthlib==2.0.7 -odfpy==1.3.6 -openapi-codec==1.3.2 -openpyxl==2.5.9 -packaging==17.1 # via sphinx -parso==0.2.0 # via jedi -pbr==3.1.1 # via mock -pdbpp==0.9.2 -pexpect==4.5.0 # via ipython -pickleshare==0.7.4 # via ipython -pillow==5.3.0 -pip-tools==2.0.2 -pluggy==0.6.0 # via tox -prompt-toolkit==1.0.15 # via ipython -psycopg2-binary==2.7.5 -psycopg2==2.7.5 -ptyprocess==0.5.2 # via pexpect -py==1.5.3 # via tox -pycodestyle==2.3.1 # via flake8 -pycparser==2.18 -pyflakes==1.6.0 # via flake8 -pygments==2.2.0 # via ipython, pdbpp, sphinx -pyjwt==1.5.3 -pyparsing==2.2.0 # via packaging -pypdf2==1.26.0 -pyrestcli==0.6.4 -python-crontab==2.3.5 -python-dateutil==2.5.3 -python3-openid==3.1.0 -pytz==2018.3 -pyyaml==3.12 -raven==6.9 -redis==2.10.6 -reportlab==3.5.9 -requests-oauthlib==0.8.0 -requests==2.11.1 -responses==0.8.1 -simplegeneric==0.8.1 # via ipython -simplejson==3.16.0 -six==1.11.0 -snowballstemmer==1.2.1 # via sphinx -social-auth-app-django==2.1.0 -social-auth-core[azuread]==1.7.0 -sphinx==1.7.5 -sphinxcontrib-websupport==1.1.0 # via sphinx -sqlparse==0.2.4 -static3==0.7.0 -tablib==0.12.1 -tenant-schemas-celery==0.2.1 -text-unidecode==1.2 # via faker -tornado==5.1.1 -tox==3.0.0 -traitlets==4.3.2 # via ipython -unicef-attachments==0.4.2 -unicef-djangolib==0.5.2 -unicef-locations==1.5 -unicef_notification==0.2.0 -unicef_restlib==0.3.8 -unicef_snapshot==0.2.1 -unicodecsv==0.14.1 -uritemplate==3.0.0 -vine==1.1.4 -virtualenv==15.2.0 # via tox -wcwidth==0.1.7 # via prompt-toolkit -webencodings==0.5.1 -wmctrl==0.3 # via pdbpp -xhtml2pdf==0.2.3 -xlrd==1.1.0 -xlwt==1.3.0 From 6a9a232ddc203e9a5eb3b4f73ee3203e09875989 Mon Sep 17 00:00:00 2001 From: robertavram Date: Thu, 7 Feb 2019 10:26:40 -0500 Subject: [PATCH 19/72] Simplify ci --- .circleci/config.yml | 16 +--------------- tox.ini | 2 +- 2 files changed, 2 insertions(+), 16 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 93f9e0fc5c..f0000c3747 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -33,23 +33,9 @@ jobs: destination: coverage build_and_deploy: - working_directory: ~/code - # The primary container is an instance of the first list image listed. Your build commands run in this container. - docker: - - image: unicef/etools:test-base-p3-v2 + machine: true steps: - checkout - - setup_remote_docker: - reusable: true - exclusive: true - - run: - name: Install Docker client - command: | - set -x - VER="17.03.0-ce" - curl -L -o /tmp/docker-$VER.tgz https://get.docker.com/builds/Linux/x86_64/docker-$VER.tgz - tar -xz -C /tmp -f /tmp/docker-$VER.tgz - mv /tmp/docker/* /usr/bin - run: name: Building the image command: | diff --git a/tox.ini b/tox.ini index 9e1ac49c15..329519bf5a 100644 --- a/tox.ini +++ b/tox.ini @@ -13,7 +13,7 @@ setenv = C_INCLUDE_PATH=/usr/include/gdal commands = - pipenv install -d --ignore-pipfile + pipenv install --dev --ignore-pipfile [testenv:d20] commands = From 64b8cd58e98e91332d76400dd623b248e94dfca1 Mon Sep 17 00:00:00 2001 From: robertavram Date: Thu, 7 Feb 2019 10:39:47 -0500 Subject: [PATCH 20/72] Install coverage in tox report --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index 329519bf5a..ec80bfbe67 100644 --- a/tox.ini +++ b/tox.ini @@ -29,4 +29,5 @@ commands = [testenv:report] commands = + pip install coverage coverage html From 603ce3c3bff912df83f155f26c58b31166702b21 Mon Sep 17 00:00:00 2001 From: robertavram Date: Thu, 7 Feb 2019 10:42:09 -0500 Subject: [PATCH 21/72] No need to recreate env --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index f0000c3747..069770003f 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -22,7 +22,7 @@ jobs: - run: name: Run Tests command: | - tox -re d20,report + tox -e d20,report - save_cache: key: deps2-{{ .Branch }}--{{ checksum "src/requirements/test.txt" }}-{{ checksum ".circleci/config.yml" }} paths: From d44583d36c1d19b01e16f04d8f329b217150bda8 Mon Sep 17 00:00:00 2001 From: robertavram Date: Thu, 7 Feb 2019 11:07:27 -0500 Subject: [PATCH 22/72] savecache fix --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 069770003f..09981c592a 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -24,7 +24,7 @@ jobs: command: | tox -e d20,report - save_cache: - key: deps2-{{ .Branch }}--{{ checksum "src/requirements/test.txt" }}-{{ checksum ".circleci/config.yml" }} + key: deps2-{{ .Branch }}--{{ checksum "Pipfile.lock" }}-{{ checksum ".circleci/config.yml" }} paths: - "env1" - /root/.cache/pip From 4cb1ee8dde6c503576aec7eb212d8409aa76aa55 Mon Sep 17 00:00:00 2001 From: Domenico DiNicola Date: Wed, 23 Jan 2019 10:23:18 -0500 Subject: [PATCH 23/72] v6.8 --- src/etools/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/etools/__init__.py b/src/etools/__init__.py index 5fbe6f684d..a2a6825a5c 100644 --- a/src/etools/__init__.py +++ b/src/etools/__init__.py @@ -1,2 +1,2 @@ -VERSION = __version__ = '6.7' +VERSION = __version__ = '6.8' NAME = 'eTools' From f95eea42b259a57d518251023db88b4437b43d49 Mon Sep 17 00:00:00 2001 From: Domenico DiNicola Date: Wed, 2 Jan 2019 11:41:50 -0500 Subject: [PATCH 24/72] 8996 django2.1 --- .circleci/config.yml | 2 +- .../migrations/0013_auto_20190102_1905.py | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 src/etools/applications/audit/migrations/0013_auto_20190102_1905.py diff --git a/.circleci/config.yml b/.circleci/config.yml index 09981c592a..0adca8a19c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -22,7 +22,7 @@ jobs: - run: name: Run Tests command: | - tox -e d20,report + tox -e d21,report - save_cache: key: deps2-{{ .Branch }}--{{ checksum "Pipfile.lock" }}-{{ checksum ".circleci/config.yml" }} paths: diff --git a/src/etools/applications/audit/migrations/0013_auto_20190102_1905.py b/src/etools/applications/audit/migrations/0013_auto_20190102_1905.py new file mode 100644 index 0000000000..b105b28f46 --- /dev/null +++ b/src/etools/applications/audit/migrations/0013_auto_20190102_1905.py @@ -0,0 +1,18 @@ +# Generated by Django 2.1.4 on 2019-01-02 19:05 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('audit', '0012_auto_20181229_0249'), + ] + + operations = [ + migrations.AlterField( + model_name='engagement', + name='joint_audit', + field=models.BooleanField(blank=True, default=False, verbose_name='Joint Audit'), + ), + ] From 2f966ca4ced86038bf91de0f7bfb55e5f20c4866 Mon Sep 17 00:00:00 2001 From: Domenico DiNicola Date: Thu, 7 Feb 2019 13:34:45 -0500 Subject: [PATCH 25/72] pipenv and dj21 --- Pipfile | 2 +- Pipfile.lock | 16 ++++------------ 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/Pipfile b/Pipfile index e09bd4083b..c9ed32b493 100644 --- a/Pipfile +++ b/Pipfile @@ -115,7 +115,7 @@ Babel = "==2.6.0" django_celery_beat = "==1.2" django_celery_results = "==1.0.1" django-post_office = "==3.1.0" -Django = "==2.0.9" +Django = "==2.1.5" et_xmlfile = "==1.0.1" GDAL = "==2.4.0" Jinja2 = "==2.10" diff --git a/Pipfile.lock b/Pipfile.lock index dca0aa9a20..0f4de80857 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "bec262f0380c6aa8b55d1a0509ed59627c26ff002d389cb7945062c87854ff93" + "sha256": "5aafd5b2c4a08491b89fd1579636b92bafa12b60d7e03f3eeab249e6f980675f" }, "pipfile-spec": 6, "requires": { @@ -199,11 +199,11 @@ }, "django": { "hashes": [ - "sha256:25df265e1fdb74f7e7305a1de620a84681bcc9c05e84a3ed97e4a1a63024f18d", - "sha256:d6d94554abc82ca37e447c3d28958f5ac39bd7d4adaa285543ae97fb1129fd69" + "sha256:a32c22af23634e1d11425574dce756098e015a165be02e4690179889b207c7a8", + "sha256:d6393918da830530a9516bbbcbf7f1214c3d733738779f06b0f649f49cc698c3" ], "index": "pypi", - "version": "==2.0.9" + "version": "==2.1.5" }, "django-appconf": { "hashes": [ @@ -1073,14 +1073,6 @@ ], "version": "==0.7.12" }, - "appnope": { - "hashes": [ - "sha256:5b26757dc6f79a3b7dc9fab95359328d5747fcb2409d331ea66d0272b90ab2a0", - "sha256:8b995ffe925347a2138d7ac0fe77155e4311a0ea6d6da4f5128fe4b3cbe5ed71" - ], - "markers": "sys_platform == 'darwin'", - "version": "==0.1.0" - }, "babel": { "hashes": [ "sha256:6778d85147d5d85345c14a26aada5e478ab04e39b078b0745ee6870c2b5cf669", From c62f89a07b88158417fdbdf2539ecb85274c7f0d Mon Sep 17 00:00:00 2001 From: robertavram Date: Fri, 8 Feb 2019 09:13:03 -0500 Subject: [PATCH 26/72] Update docker file dev to be closer to the production env --- Dockerfile-dev | 73 +++++++++++++++++++++++--------------------------- 1 file changed, 34 insertions(+), 39 deletions(-) diff --git a/Dockerfile-dev b/Dockerfile-dev index 7ea635a4d9..f88c91f56c 100644 --- a/Dockerfile-dev +++ b/Dockerfile-dev @@ -1,35 +1,31 @@ -FROM python:3.6.4-jessie -# python:3.6.4-jessie has python 2.7 and 3.6 installed, and packages -# available to install 3.4 - -# Install dependencies -RUN apt-get update -RUN apt-get install -y --no-install-recommends \ - build-essential \ - libcurl4-openssl-dev \ - libjpeg-dev \ - vim \ - ntp \ - libpq-dev -RUN apt-get install -y --no-install-recommends \ - git-core -RUN apt-get install -y --no-install-recommends \ - python3-dev \ - python-software-properties \ - python-setuptools -RUN apt-get install -y --no-install-recommends \ - postgresql-client \ - libpq-dev \ - python-psycopg2 -RUN apt-get install -y --no-install-recommends \ - python-gdal \ - gdal-bin \ - libgdal-dev \ - libgdal1h \ - libgdal1-dev \ +FROM python:3.6.4-alpine + +RUN echo "http://dl-3.alpinelinux.org/alpine/edge/main" >> /etc/apk/repositories +RUN apk update +RUN apk add --upgrade apk-tools + +RUN apk add \ + --update alpine-sdk + +RUN apk add openssl \ + ca-certificates \ + libressl2.7-libcrypto +RUN apk add \ libxml2-dev \ libxslt-dev \ - xmlsec1 + xmlsec-dev +RUN apk add postgresql-dev \ + libffi-dev\ + jpeg-dev + +RUN apk add --update-cache --repository http://dl-3.alpinelinux.org/alpine/edge/testing/ \ + gdal \ + gdal-dev \ + py-gdal \ + geos \ + geos-dev \ + gcc \ + g++ RUN pip install --upgrade \ setuptools \ @@ -37,16 +33,15 @@ RUN pip install --upgrade \ wheel \ pipenv - -# http://gis.stackexchange.com/a/74060 -ENV CPLUS_INCLUDE_PATH /usr/include/gdal -ENV C_INCLUDE_PATH /usr/include/gdal - -ADD Pipfile.lock / -RUN pipenv install --verbose --system --deploy --ignore-pipfile +WORKDIR /etools/ +ADD Pipfile . +ADD Pipfile.lock . +RUN pipenv install --dev --system --ignore-pipfile +RUN apk add bash ENV PYTHONUNBUFFERED 1 +ENV PYTHONPATH /code +ENV DJANGO_SETTINGS_MODULE etools.config.settings.local + VOLUME "./:/code/" WORKDIR /code/ - -ENV DJANGO_SETTINGS_MODULE etools.config.settings.local From 533368c75640aa128e776c4bdde72cc5f8b13196 Mon Sep 17 00:00:00 2001 From: Greg Reinbach Date: Fri, 8 Feb 2019 09:37:39 -0500 Subject: [PATCH 27/72] Do not allow special characters in PD Indicator target/baseline fields --- .../applications/reports/serializers/v2.py | 5 ++- .../reports/tests/test_serializers.py | 9 +++++ .../reports/tests/test_validators.py | 39 +++++++++++++++++++ src/etools/applications/reports/validators.py | 18 +++++++++ 4 files changed, 69 insertions(+), 2 deletions(-) create mode 100644 src/etools/applications/reports/tests/test_validators.py create mode 100644 src/etools/applications/reports/validators.py diff --git a/src/etools/applications/reports/serializers/v2.py b/src/etools/applications/reports/serializers/v2.py index c552be8e1b..4f488be314 100644 --- a/src/etools/applications/reports/serializers/v2.py +++ b/src/etools/applications/reports/serializers/v2.py @@ -17,6 +17,7 @@ ResultType, SpecialReportingRequirement, ) +from etools.applications.reports.validators import value_numbers class MinimalOutputListSerializer(serializers.ModelSerializer): @@ -138,8 +139,8 @@ def update(self, instance, validated_data): class AppliedIndicatorSerializer(serializers.ModelSerializer): indicator = IndicatorBlueprintCUSerializer(required=False, allow_null=True) - target = serializers.JSONField(required=False) - baseline = serializers.JSONField(required=False) + target = serializers.JSONField(required=False, validators=[value_numbers]) + baseline = serializers.JSONField(required=False, validators=[value_numbers]) class Meta: model = AppliedIndicator diff --git a/src/etools/applications/reports/tests/test_serializers.py b/src/etools/applications/reports/tests/test_serializers.py index 4b991c87a3..42444149e7 100644 --- a/src/etools/applications/reports/tests/test_serializers.py +++ b/src/etools/applications/reports/tests/test_serializers.py @@ -190,6 +190,15 @@ def test_validate(self): serializer = AppliedIndicatorSerializer(data=self.data) self.assertTrue(serializer.is_valid()) + def test_validate_value_numbers(self): + self.data["target"] = {"d": 123, "v": "$321"} + self.data["baseline"] = {"d": "wrong", "v": 321} + self.intervention.flat_locations.add(self.location) + serializer = AppliedIndicatorSerializer(data=self.data) + self.assertFalse(serializer.is_valid()) + self.assertIn("target", serializer.errors) + self.assertIn("baseline", serializer.errors) + def test_create(self): applied_qs = AppliedIndicator.objects.filter( lower_result__pk=self.lower_result.pk diff --git a/src/etools/applications/reports/tests/test_validators.py b/src/etools/applications/reports/tests/test_validators.py new file mode 100644 index 0000000000..7b579592b0 --- /dev/null +++ b/src/etools/applications/reports/tests/test_validators.py @@ -0,0 +1,39 @@ +import json + +from django import forms + +from etools.applications.EquiTrack.tests.cases import BaseTenantTestCase +from etools.applications.reports import validators + + +class TestValueNumbers(BaseTenantTestCase): + def test_empty_string(self): + with self.assertRaises(forms.ValidationError): + (validators.value_numbers("")) + + def test_json_valid_str(self): + d = json.dumps({"v": "123.00", "d": "321,00"}) + self.assertIsNone(validators.value_numbers(d)) + + def test_json_valid(self): + d = json.dumps({"v": 123.00, "d": 321}) + self.assertIsNone(validators.value_numbers(d)) + + def test_json_invalid(self): + d = json.dumps({"v": "123.00", "d": "$321,00"}) + with self.assertRaises(forms.ValidationError): + validators.value_numbers(d) + + def test_valid_str(self): + self.assertIsNone( + validators.value_numbers({"v": "123.00", "d": "321,00"}) + ) + + def test_valid(self): + self.assertIsNone( + validators.value_numbers({"v": 123.00, "d": 321}) + ) + + def test_invalid(self): + with self.assertRaises(forms.ValidationError): + validators.value_numbers({"v": "$123.00", "d": "321,00"}) diff --git a/src/etools/applications/reports/validators.py b/src/etools/applications/reports/validators.py new file mode 100644 index 0000000000..5bbfa80855 --- /dev/null +++ b/src/etools/applications/reports/validators.py @@ -0,0 +1,18 @@ +import json +import re + +from django import forms + + +re_allowed_chars = re.compile("^[0-9,\.]+$") + +def value_numbers(data): + """Ensure that each value in json object is a number""" + if isinstance(data, str): + try: + data = json.loads(data) + except ValueError: + raise forms.ValidationError("Invalid data") + for v in data.values(): + if not re_allowed_chars.match(str(v)): + raise forms.ValidationError("Invalid number") From 852f2ca888210d297042a4f9cf36596c299e1176 Mon Sep 17 00:00:00 2001 From: denes csaba Date: Fri, 8 Feb 2019 16:38:53 +0200 Subject: [PATCH 28/72] Reporting requirement edit 'in termination' status perms --- src/etools/applications/partners/permissions.py | 6 ++++-- src/etools/assets/partner/intervention_permissions.csv | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/etools/applications/partners/permissions.py b/src/etools/applications/partners/permissions.py index 4a8f8705b9..d5b8c6bf2a 100644 --- a/src/etools/applications/partners/permissions.py +++ b/src/etools/applications/partners/permissions.py @@ -1,3 +1,4 @@ +import datetime from django.apps import apps from django.utils.lru_cache import lru_cache from django.utils.translation import ugettext as _ @@ -87,7 +88,7 @@ def get_permissions(self): class InterventionPermissions(PMPPermissions): MODEL_NAME = 'partners.Intervention' - EXTRA_FIELDS = ['sections_present'] + EXTRA_FIELDS = ['sections_present', 'reporting_requirements'] def __init__(self, **kwargs): """ @@ -125,7 +126,8 @@ def prp_server_on(): 'prp_mode_off': prp_mode_off(), 'prp_server_on': prp_server_on(), 'user_adds_amendment+prp_mode_on': user_added_amendment(self.instance) and not prp_mode_off(), - 'termination_doc_attached': self.instance.termination_doc_attachment.exists() + 'termination_doc_attached': self.instance.termination_doc_attachment.exists(), + 'not_ended': self.instance.end >= datetime.datetime.now().date() } diff --git a/src/etools/assets/partner/intervention_permissions.csv b/src/etools/assets/partner/intervention_permissions.csv index 50f05d7966..42df514bee 100644 --- a/src/etools/assets/partner/intervention_permissions.csv +++ b/src/etools/assets/partner/intervention_permissions.csv @@ -91,6 +91,7 @@ Field no,Field Name,Group,Condition,Status,Action,Allowed 3.13.1,reporting_requirements,Partnership Manager,prp_mode_on,Draft,Edit,TRUE 3.13.1,reporting_requirements,Partnership Manager,prp_mode_on+contingency_on,Signed,Edit,TRUE 3.13.1,reporting_requirements,Partnership Manager,user_adds_amendment+prp_mode_on,*,Edit,TRUE +3.13.1,reporting_requirements,Partnership Manager,not_ended,Terminated,Edit,TRUE 3.13.1,reporting_requirements,Partnership Manager,termination_doc_attached,Active,Edit,TRUE 3.13.1,reporting_requirements,Partnership Manager,termination_doc_attached,Signed,Edit,TRUE 3.13.1,reporting_requirements,UNICEF User,prp_server_on,*,View,TRUE From 7378b9eabe73d758316173de7dc6496534d55239 Mon Sep 17 00:00:00 2001 From: Greg Reinbach Date: Fri, 8 Feb 2019 09:45:09 -0500 Subject: [PATCH 29/72] flake8 cleanup --- src/etools/applications/reports/validators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/etools/applications/reports/validators.py b/src/etools/applications/reports/validators.py index 5bbfa80855..2966aa8750 100644 --- a/src/etools/applications/reports/validators.py +++ b/src/etools/applications/reports/validators.py @@ -3,8 +3,8 @@ from django import forms +re_allowed_chars = re.compile("^[0-9,.]+$") -re_allowed_chars = re.compile("^[0-9,\.]+$") def value_numbers(data): """Ensure that each value in json object is a number""" From 140a14d5142f79cdaa530833b663d0a08ef64432 Mon Sep 17 00:00:00 2001 From: Greg Reinbach Date: Fri, 8 Feb 2019 10:38:31 -0500 Subject: [PATCH 30/72] Draft intervention notification only after 7 days Fix url in notification Cleaner query for past start notification --- .../applications/partners/tests/test_utils.py | 11 ++++++++++- src/etools/applications/partners/utils.py | 17 ++++++++--------- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/etools/applications/partners/tests/test_utils.py b/src/etools/applications/partners/tests/test_utils.py index 5a34fcbf6d..4909e7a17b 100644 --- a/src/etools/applications/partners/tests/test_utils.py +++ b/src/etools/applications/partners/tests/test_utils.py @@ -243,12 +243,21 @@ def setUpTestData(cls): cls.send_path = "etools.applications.partners.utils.send_notification_with_template" def test_send(self): - InterventionFactory(status=Intervention.DRAFT) + intervention = InterventionFactory(status=Intervention.DRAFT) + intervention.created = datetime.datetime(2018, 1, 1, 12, 55, 12, 12345) + intervention.save() mock_send = Mock() with patch(self.send_path, mock_send): utils.send_intervention_draft_notification() self.assertEqual(mock_send.call_count, 1) + def test_send_not_week_old(self): + InterventionFactory(status=Intervention.DRAFT) + mock_send = Mock() + with patch(self.send_path, mock_send): + utils.send_intervention_draft_notification() + self.assertEqual(mock_send.call_count, 0) + def test_send_not_draft(self): intervention = InterventionFactory(status=Intervention.SIGNED) self.assertTrue(intervention.status != Intervention.DRAFT) diff --git a/src/etools/applications/partners/utils.py b/src/etools/applications/partners/utils.py index 42ccd467d7..b660439c4c 100644 --- a/src/etools/applications/partners/utils.py +++ b/src/etools/applications/partners/utils.py @@ -441,7 +441,10 @@ def send_agreement_suspended_notification(agreement, user): def send_intervention_draft_notification(): """Send an email to PD/SHPD/SSFA's focal point(s) if in draft status""" - for intervention in Intervention.objects.filter(status=Intervention.DRAFT): + for intervention in Intervention.objects.filter( + status=Intervention.DRAFT, + created__lt=datetime.date.today() - datetime.timedelta(days=7), + ): recipients = [ u.user.email for u in intervention.unicef_focal_points.all() if u.user.email @@ -459,15 +462,11 @@ def send_intervention_draft_notification(): def send_intervention_past_start_notification(): """Send an email to PD/SHPD/SSFA's focal point(s) if signed and start date is past with no FR added""" - frs_count = FundsReservationHeader.objects.filter( - intervention=OuterRef("pk") - ).annotate(count=Count("vendor_code")).values("count") intervention_qs = Intervention.objects.filter( status=Intervention.SIGNED, start__lt=datetime.date.today(), - ).annotate( - frs_count=Subquery(frs_count) - ).filter(frs_count__gt=0) + frs__isnull=False, + ) for intervention in intervention_qs.all(): recipients = [ u.user.email for u in intervention.unicef_focal_points.all() @@ -479,9 +478,9 @@ def send_intervention_past_start_notification(): context={ "reference_number": intervention.reference_number, "title": intervention.title, - "url": "{}{}".format( + "url": "{}pmp/interventions/{}/details".format( settings.HOST, - intervention.get_object_url(), + intervention.pk, ) } ) From 0088511a79a75a72aecebe43e6725f6b77c69ff8 Mon Sep 17 00:00:00 2001 From: Greg Reinbach Date: Fri, 8 Feb 2019 10:40:14 -0500 Subject: [PATCH 31/72] flake8 cleanup --- src/etools/applications/partners/utils.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/etools/applications/partners/utils.py b/src/etools/applications/partners/utils.py index b660439c4c..ee3f87e499 100644 --- a/src/etools/applications/partners/utils.py +++ b/src/etools/applications/partners/utils.py @@ -3,14 +3,13 @@ from django.conf import settings from django.contrib.contenttypes.models import ContentType -from django.db.models import Count, F, OuterRef, Q, Subquery +from django.db.models import F, Q from django.urls import reverse from django.utils.timezone import make_aware, now from unicef_attachments.models import Attachment, FileType from unicef_notification.utils import send_notification_with_template -from etools.applications.funds.models import FundsReservationHeader from etools.applications.partners.models import ( Agreement, AgreementAmendment, From 84eaf6aa84592bdbd274a09623ad39fae7b7e5b1 Mon Sep 17 00:00:00 2001 From: Domenico DiNicola Date: Tue, 15 Jan 2019 14:18:46 -0500 Subject: [PATCH 32/72] permission refactoring --- .../applications/EquiTrack/permissions.py | 90 +++++++++++++++++-- src/etools/applications/EquiTrack/utils.py | 82 ----------------- src/etools/applications/partners/models.py | 3 +- .../applications/users/tests/test_views_v3.py | 2 +- src/etools/applications/users/views.py | 3 +- src/etools/applications/users/views_v3.py | 4 +- 6 files changed, 89 insertions(+), 95 deletions(-) diff --git a/src/etools/applications/EquiTrack/permissions.py b/src/etools/applications/EquiTrack/permissions.py index c63328593d..6d30a4ec2b 100644 --- a/src/etools/applications/EquiTrack/permissions.py +++ b/src/etools/applications/EquiTrack/permissions.py @@ -1,10 +1,86 @@ -from rest_framework.permissions import BasePermission +import codecs +import csv +from django.conf import settings +from django.core.cache import cache -class IsSuperUserOrStaff(BasePermission): - """ - Allows access to super users or staff. - """ +from etools.applications.EquiTrack.utils import Vividict - def has_permission(self, request, view): - return request.user and (request.user.is_superuser or request.user.is_staff) + +def process_permissions(permission_dict): + ''' + :param permission_dict: the csv file read as a generator of dictionaries + where the header contains the following keys: + + 'Group' - the Django Group the user should belong to - field may be blank. + 'Condition' - the condition that should be required to satisfy. + 'Status' - the status of the model (represents state) + 'Field' - the field we are targetting (eg: start_date) this needs to be spelled exactly as it is on the model + 'Action' - One of the following values: 'view', 'edit', 'required' + 'Allowed' - the boolean 'TRUE' or 'FALSE' if the action should be allowed if the: group match, status match and + condition match are all valid + + *** note that in order for the system to know what the default behaviour should be on a specified field for a + specific action, only the conditions opposite to the default should be defined. + + :return: + a nested dictionary where the first key is the field targeted, the following nested key is the action possible, + and the last nested key is the action parameter + eg: + {'start_date': {'edit': {'false': [{'condition': 'condition2', + 'group': 'UNICEF USER', + 'status': 'Active'}]}, + 'required': {'true': [{'condition': '', + 'group': 'UNICEF USER', + 'status': 'Active'}, + {'condition': '', + 'group': 'UNICEF USER', + 'status': 'Signed'}]}, + 'view': {'true': [{'condition': 'condition1', + 'group': 'PM', + 'status': 'Active'}]}}} + ''' + + result = Vividict() + possible_actions = ['edit', 'required', 'view'] + + for row in permission_dict: + field = row['Field Name'] + action = row['Action'].lower() + allowed = row['Allowed'].lower() + assert action in possible_actions + + if isinstance(result[field][action][allowed], dict): + result[field][action][allowed] = [] + + # this action should not have been defined with any other allowed param + assert list(result[field][action].keys()) == [allowed], \ + 'There cannot be two types of "allowed" defined on the same ' \ + 'field with the same action as the system will not be able' \ + ' to have a default behaviour. field=%r, action=%r, allowed=%r' \ + % (field, action, allowed) + + result[field][action][allowed].append({ + 'group': row['Group'], + 'condition': row['Condition'], + 'status': row['Status'].lower() + }) + return result + + +def import_permissions(model_name): + permission_file_map = { + 'Intervention': settings.PACKAGE_ROOT + '/assets/partner/intervention_permissions.csv', + 'Agreement': settings.PACKAGE_ROOT + '/assets/partner/agreement_permissions.csv' + } + + def process_file(): + with codecs.open(permission_file_map[model_name], 'r', encoding='ascii') as csvfile: + sheet = csv.DictReader(csvfile, delimiter=',', quotechar='|') + result = process_permissions(sheet) + return result + + cache_key = "public-{}-permissions".format(model_name.lower()) + response = cache.get_or_set(cache_key, process_file, 60 * 60 * 24) + + return response diff --git a/src/etools/applications/EquiTrack/utils.py b/src/etools/applications/EquiTrack/utils.py index 5cab984da6..3001961572 100644 --- a/src/etools/applications/EquiTrack/utils.py +++ b/src/etools/applications/EquiTrack/utils.py @@ -1,11 +1,8 @@ -import codecs -import csv import hashlib from datetime import datetime from django.conf import settings from django.contrib.sites.models import Site -from django.core.cache import cache def get_environment(): @@ -27,85 +24,6 @@ def __hash__(self): return hash(frozenset(self.items())) -def proccess_permissions(permission_dict): - """ - :param permission_dict: the csv file read as a generator of dictionaries - where the header contains the following keys: - - 'Group' - the Django Group the user should belong to - field may be blank. - 'Condition' - the condition that should be required to satisfy. - 'Status' - the status of the model (represents state) - 'Field' - the field we are targetting (eg: start_date) this needs to be spelled exactly as it is on the model - 'Action' - One of the following values: 'view', 'edit', 'required' - 'Allowed' - the boolean 'TRUE' or 'FALSE' if the action should be allowed if the: group match, status match and - condition match are all valid - - *** note that in order for the system to know what the default behaviour should be on a specified field for a - specific action, only the conditions opposite to the default should be defined. - - :return: - a nested dictionary where the first key is the field targeted, the following nested key is the action possible, - and the last nested key is the action parameter - eg: - {'start_date': {'edit': {'false': [{'condition': 'condition2', - 'group': 'UNICEF USER', - 'status': 'Active'}]}, - 'required': {'true': [{'condition': '', - 'group': 'UNICEF USER', - 'status': 'Active'}, - {'condition': '', - 'group': 'UNICEF USER', - 'status': 'Signed'}]}, - 'view': {'true': [{'condition': 'condition1', - 'group': 'PM', - 'status': 'Active'}]}}} - """ - - result = Vividict() - possible_actions = ['edit', 'required', 'view'] - - for row in permission_dict: - field = row['Field Name'] - action = row['Action'].lower() - allowed = row['Allowed'].lower() - assert action in possible_actions - - if isinstance(result[field][action][allowed], dict): - result[field][action][allowed] = [] - - # this action should not have been defined with any other allowed param - assert list(result[field][action].keys()) == [allowed], \ - 'There cannot be two types of "allowed" defined on the same ' \ - 'field with the same action as the system will not be able' \ - ' to have a default behaviour. field=%r, action=%r, allowed=%r' \ - % (field, action, allowed) - - result[field][action][allowed].append({ - 'group': row['Group'], - 'condition': row['Condition'], - 'status': row['Status'].lower() - }) - return result - - -def import_permissions(model_name): - permission_file_map = { - 'Intervention': settings.PACKAGE_ROOT + '/assets/partner/intervention_permissions.csv', - 'Agreement': settings.PACKAGE_ROOT + '/assets/partner/agreement_permissions.csv' - } - - def process_file(): - with codecs.open(permission_file_map[model_name], 'r', encoding='ascii') as csvfile: - sheet = csv.DictReader(csvfile, delimiter=',', quotechar='|') - result = proccess_permissions(sheet) - return result - - cache_key = "public-{}-permissions".format(model_name.lower()) - response = cache.get_or_set(cache_key, process_file, 60 * 60 * 24) - - return response - - def get_current_year(): return datetime.today().year diff --git a/src/etools/applications/partners/models.py b/src/etools/applications/partners/models.py index 5671ee0f13..0e62ed3ff5 100644 --- a/src/etools/applications/partners/models.py +++ b/src/etools/applications/partners/models.py @@ -20,7 +20,8 @@ from unicef_locations.models import Location from etools.applications.EquiTrack.encoders import EToolsEncoder -from etools.applications.EquiTrack.utils import get_current_year, get_quarter, import_permissions +from etools.applications.EquiTrack.permissions import import_permissions +from etools.applications.EquiTrack.utils import get_current_year, get_quarter from etools.applications.funds.models import FundsReservationHeader from etools.applications.partners.validation import interventions as intervention_validation from etools.applications.partners.validation.agreements import ( diff --git a/src/etools/applications/users/tests/test_views_v3.py b/src/etools/applications/users/tests/test_views_v3.py index e6a0fe77e5..fb13bf680d 100644 --- a/src/etools/applications/users/tests/test_views_v3.py +++ b/src/etools/applications/users/tests/test_views_v3.py @@ -158,7 +158,7 @@ def test_minimal_verbosity(self): user=self.unicef_superuser ) response_json = json.loads(response.rendered_content) - self.assertEqual(len(response_json), 2) + self.assertEqual(len(response_json), 1) class TestMyProfileAPIView(BaseTenantTestCase): diff --git a/src/etools/applications/users/views.py b/src/etools/applications/users/views.py index ab05519221..877ef67808 100644 --- a/src/etools/applications/users/views.py +++ b/src/etools/applications/users/views.py @@ -16,7 +16,6 @@ from rest_framework.views import APIView from etools.applications.audit.models import Auditor -from etools.applications.EquiTrack.permissions import IsSuperUserOrStaff from etools.applications.tpm.models import ThirdPartyMonitor from etools.applications.users.forms import ProfileForm from etools.applications.users.models import Country, Office, UserProfile @@ -131,7 +130,7 @@ class StaffUsersView(ListAPIView): """ model = UserProfile serializer_class = SimpleProfileSerializer - permission_classes = (IsSuperUserOrStaff, ) + permission_classes = (IsAdminUser, ) def get_queryset(self): user = self.request.user diff --git a/src/etools/applications/users/views_v3.py b/src/etools/applications/users/views_v3.py index 152f0dddba..7d7faf3ed5 100644 --- a/src/etools/applications/users/views_v3.py +++ b/src/etools/applications/users/views_v3.py @@ -5,9 +5,9 @@ from rest_framework import status from rest_framework.exceptions import ValidationError from rest_framework.generics import ListAPIView, RetrieveAPIView +from rest_framework.permissions import IsAdminUser from rest_framework.response import Response -from etools.applications.EquiTrack.permissions import IsSuperUserOrStaff from etools.applications.users import views as v1, views_v2 as v2 from etools.applications.users.serializers_v3 import ( CountryDetailSerializer, @@ -59,7 +59,7 @@ class UsersListAPIView(ListAPIView): """ model = get_user_model() serializer_class = MinimalUserSerializer - permission_classes = (IsSuperUserOrStaff, ) + permission_classes = (IsAdminUser, ) def get_queryset(self, pk=None): user = self.request.user From 0b26b1ce263af64b31e1aba665a031b05f221644 Mon Sep 17 00:00:00 2001 From: Domenico DiNicola Date: Tue, 22 Jan 2019 12:35:01 -0500 Subject: [PATCH 33/72] pythonlib --- .../applications/EquiTrack/permissions.py | 6 +-- src/etools/applications/EquiTrack/utils.py | 37 ------------------- .../applications/audit/serializers/export.py | 2 +- .../applications/audit/serializers/risks.py | 2 +- .../management/commands/freeze_hact_data.py | 4 +- .../hact/migrations/0001_initial.py | 4 +- src/etools/applications/hact/models.py | 6 +-- .../partners/migrations/0001_initial.py | 2 +- .../migrations/0010_auto_20180514_1104.py | 2 +- src/etools/applications/partners/models.py | 8 ++-- .../applications/partners/permissions.py | 3 +- .../partners/serializers/interventions_v2.py | 2 +- src/etools/applications/utils/common/utils.py | 11 ------ src/etools/libraries/pythonlib/__init__.py | 0 src/etools/libraries/pythonlib/collections.py | 16 ++++++++ src/etools/libraries/pythonlib/datetime.py | 12 ++++++ .../pythonlib}/encoders.py | 2 +- src/etools/libraries/pythonlib/hash.py | 5 +++ .../libraries/pythonlib/tests/__init__.py | 0 .../pythonlib}/tests/test_utils.py | 8 ++-- 20 files changed, 59 insertions(+), 73 deletions(-) create mode 100644 src/etools/libraries/pythonlib/__init__.py create mode 100644 src/etools/libraries/pythonlib/collections.py create mode 100644 src/etools/libraries/pythonlib/datetime.py rename src/etools/{applications/EquiTrack => libraries/pythonlib}/encoders.py (86%) create mode 100644 src/etools/libraries/pythonlib/hash.py create mode 100644 src/etools/libraries/pythonlib/tests/__init__.py rename src/etools/{applications/EquiTrack => libraries/pythonlib}/tests/test_utils.py (77%) diff --git a/src/etools/applications/EquiTrack/permissions.py b/src/etools/applications/EquiTrack/permissions.py index 6d30a4ec2b..c5d5ede8a8 100644 --- a/src/etools/applications/EquiTrack/permissions.py +++ b/src/etools/applications/EquiTrack/permissions.py @@ -4,11 +4,11 @@ from django.conf import settings from django.core.cache import cache -from etools.applications.EquiTrack.utils import Vividict +from etools.libraries.pythonlib.collections import Vividict def process_permissions(permission_dict): - ''' + """ :param permission_dict: the csv file read as a generator of dictionaries where the header contains the following keys: @@ -39,7 +39,7 @@ def process_permissions(permission_dict): 'view': {'true': [{'condition': 'condition1', 'group': 'PM', 'status': 'Active'}]}}} - ''' + """ result = Vividict() possible_actions = ['edit', 'required', 'view'] diff --git a/src/etools/applications/EquiTrack/utils.py b/src/etools/applications/EquiTrack/utils.py index 3001961572..0f2fbd9e1a 100644 --- a/src/etools/applications/EquiTrack/utils.py +++ b/src/etools/applications/EquiTrack/utils.py @@ -1,6 +1,3 @@ -import hashlib -from datetime import datetime - from django.conf import settings from django.contrib.sites.models import Site @@ -11,37 +8,3 @@ def get_environment(): def get_current_site(): return Site.objects.get_current() - - -class Vividict(dict): - def __missing__(self, key): - value = self[key] = type(self)() - return value - - -class HashableDict(dict): - def __hash__(self): - return hash(frozenset(self.items())) - - -def get_current_year(): - return datetime.today().year - - -def get_quarter(retrieve_date=None): - if not retrieve_date: - retrieve_date = datetime.today() - month = retrieve_date.month - if 0 < month <= 3: - quarter = 'q1' - elif 3 < month <= 6: - quarter = 'q2' - elif 6 < month <= 9: - quarter = 'q3' - else: - quarter = 'q4' - return quarter - - -def h11(w): - return hashlib.md5(w).hexdigest()[:9] diff --git a/src/etools/applications/audit/serializers/export.py b/src/etools/applications/audit/serializers/export.py index 1a1638f138..81003d9401 100644 --- a/src/etools/applications/audit/serializers/export.py +++ b/src/etools/applications/audit/serializers/export.py @@ -30,7 +30,7 @@ RiskRootSerializer, ) from etools.applications.partners.models import PartnerOrganization -from etools.applications.utils.common.utils import to_choices_list +from etools.libraries.pythonlib.collections import to_choices_list class AuditorPDFSerializer(serializers.ModelSerializer): diff --git a/src/etools/applications/audit/serializers/risks.py b/src/etools/applications/audit/serializers/risks.py index c48db3e46d..b2b6b196ce 100644 --- a/src/etools/applications/audit/serializers/risks.py +++ b/src/etools/applications/audit/serializers/risks.py @@ -8,7 +8,7 @@ from unicef_restlib.serializers import RecursiveListSerializer, WritableListSerializer, WritableNestedSerializerMixin from etools.applications.audit.models import Risk, RiskBluePrint, RiskCategory -from etools.applications.utils.common.utils import to_choices_list +from etools.libraries.pythonlib.collections import to_choices_list class BaseRiskSerializer(WritableNestedSerializerMixin, serializers.ModelSerializer): diff --git a/src/etools/applications/hact/management/commands/freeze_hact_data.py b/src/etools/applications/hact/management/commands/freeze_hact_data.py index c1cca617f3..ed2a64933b 100644 --- a/src/etools/applications/hact/management/commands/freeze_hact_data.py +++ b/src/etools/applications/hact/management/commands/freeze_hact_data.py @@ -4,7 +4,7 @@ from django.core.management import BaseCommand from django.db import transaction -from etools.applications.EquiTrack.encoders import EToolsEncoder +from etools.libraries.pythonlib.encoders import CustomJSONEncoder from etools.applications.EquiTrack.util_scripts import set_country from etools.applications.hact.models import HactHistory from etools.applications.partners.models import hact_default, PartnerOrganization, PlannedEngagement @@ -75,7 +75,7 @@ def freeze_data(self, hact_history): ('Audit Completed', self.get_or_empty(partner_hact, ['audits', 'completed'])), ('Audit Outstanding Findings', self.get_or_empty(partner_hact, ['outstanding_findings', ])), ] - hact_history.partner_values = json.dumps(partner_values, cls=EToolsEncoder) + hact_history.partner_values = json.dumps(partner_values, cls=CustomJSONEncoder) hact_history.save() @transaction.atomic diff --git a/src/etools/applications/hact/migrations/0001_initial.py b/src/etools/applications/hact/migrations/0001_initial.py index ff14a27c9f..59c886d55c 100644 --- a/src/etools/applications/hact/migrations/0001_initial.py +++ b/src/etools/applications/hact/migrations/0001_initial.py @@ -26,7 +26,7 @@ class Migration(migrations.Migration): ('modified', model_utils.fields.AutoLastModifiedField( default=django.utils.timezone.now, editable=False, verbose_name='modified')), ('year', models.IntegerField( - default=etools.applications.EquiTrack.utils.get_current_year, unique=True, verbose_name='Year')), + default=etools.libraries.pythonlib.datetime.get_current_year, unique=True, verbose_name='Year')), ('partner_values', django.contrib.postgres.fields.jsonb.JSONField( blank=True, null=True, verbose_name='Partner Values')), ], @@ -42,7 +42,7 @@ class Migration(migrations.Migration): default=django.utils.timezone.now, editable=False, verbose_name='created')), ('modified', model_utils.fields.AutoLastModifiedField( default=django.utils.timezone.now, editable=False, verbose_name='modified')), - ('year', models.IntegerField(default=etools.applications.EquiTrack.utils.get_current_year, verbose_name='Year')), + ('year', models.IntegerField(default=etools.libraries.pythonlib.datetime.get_current_year, verbose_name='Year')), ('partner_values', django.contrib.postgres.fields.jsonb.JSONField( blank=True, null=True, verbose_name='Partner Values')), ], diff --git a/src/etools/applications/hact/models.py b/src/etools/applications/hact/models.py index 1df9643646..fb088836c2 100644 --- a/src/etools/applications/hact/models.py +++ b/src/etools/applications/hact/models.py @@ -11,9 +11,9 @@ from model_utils.models import TimeStampedModel from etools.applications.audit.models import Audit, Engagement, MicroAssessment, SpecialAudit, SpotCheck -from etools.applications.EquiTrack.encoders import EToolsEncoder -from etools.applications.EquiTrack.utils import get_current_year +from etools.libraries.pythonlib.encoders import CustomJSONEncoder from etools.applications.partners.models import PartnerOrganization, PartnerType +from etools.libraries.pythonlib.datetime import get_current_year class HactHistory(TimeStampedModel): @@ -50,7 +50,7 @@ def update(self): 'cash_transfers_partner_type': self.get_cash_transfer_partner_type(), 'spot_checks_completed': self.get_spot_checks_completed(), }, - }, cls=EToolsEncoder) + }, cls=CustomJSONEncoder) self.save() @staticmethod diff --git a/src/etools/applications/partners/migrations/0001_initial.py b/src/etools/applications/partners/migrations/0001_initial.py index b98733aedd..dd38d3f982 100644 --- a/src/etools/applications/partners/migrations/0001_initial.py +++ b/src/etools/applications/partners/migrations/0001_initial.py @@ -259,7 +259,7 @@ class Migration(migrations.Migration): default=django.utils.timezone.now, editable=False, verbose_name='created')), ('modified', model_utils.fields.AutoLastModifiedField( default=django.utils.timezone.now, editable=False, verbose_name='modified')), - ('year', models.IntegerField(default=etools.applications.EquiTrack.utils.get_current_year, verbose_name='Year')), + ('year', models.IntegerField(default=etools.libraries.pythonlib.datetime.get_current_year, verbose_name='Year')), ('programmatic_q1', models.IntegerField(default=0, verbose_name='Programmatic Q1')), ('programmatic_q2', models.IntegerField(default=0, verbose_name='Programmatic Q2')), ('programmatic_q3', models.IntegerField(default=0, verbose_name='Programmatic Q3')), diff --git a/src/etools/applications/partners/migrations/0010_auto_20180514_1104.py b/src/etools/applications/partners/migrations/0010_auto_20180514_1104.py index 832fb61dac..a60262d0c9 100644 --- a/src/etools/applications/partners/migrations/0010_auto_20180514_1104.py +++ b/src/etools/applications/partners/migrations/0010_auto_20180514_1104.py @@ -38,7 +38,7 @@ class Migration(migrations.Migration): ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), - ('year', models.IntegerField(default=etools.applications.EquiTrack.utils.get_current_year, verbose_name='Year')), + ('year', models.IntegerField(default=etools.libraries.pythonlib.datetime.get_current_year, verbose_name='Year')), ('programmatic_q1', models.IntegerField(default=0, verbose_name='Programmatic Q1')), ('programmatic_q2', models.IntegerField(default=0, verbose_name='Programmatic Q2')), ('programmatic_q3', models.IntegerField(default=0, verbose_name='Programmatic Q3')), diff --git a/src/etools/applications/partners/models.py b/src/etools/applications/partners/models.py index 0e62ed3ff5..74d2831713 100644 --- a/src/etools/applications/partners/models.py +++ b/src/etools/applications/partners/models.py @@ -19,9 +19,7 @@ from unicef_djangolib.fields import CodedGenericRelation, CurrencyField from unicef_locations.models import Location -from etools.applications.EquiTrack.encoders import EToolsEncoder from etools.applications.EquiTrack.permissions import import_permissions -from etools.applications.EquiTrack.utils import get_current_year, get_quarter from etools.applications.funds.models import FundsReservationHeader from etools.applications.partners.validation import interventions as intervention_validation from etools.applications.partners.validation.agreements import ( @@ -34,6 +32,8 @@ from etools.applications.tpm.models import TPMVisit from etools.applications.users.models import Office from etools.libraries.djangolib.models import StringConcat +from etools.libraries.pythonlib.datetime import get_current_year, get_quarter +from etools.libraries.pythonlib.encoders import CustomJSONEncoder def _get_partner_base_path(partner): @@ -519,7 +519,7 @@ def save(self, *args, **kwargs): super().save(*args, **kwargs) if hact_is_string: - self.hact_values = json.dumps(self.hact_values, cls=EToolsEncoder) + self.hact_values = json.dumps(self.hact_values, cls=CustomJSONEncoder) @cached_property def partner_type_slug(self): @@ -798,7 +798,7 @@ def hact_support(self): hact['outstanding_findings'] = sum([ audit.pending_unsupported_amount for audit in audits if audit.pending_unsupported_amount]) hact['assurance_coverage'] = self.assurance_coverage - self.hact_values = json.dumps(hact, cls=EToolsEncoder) + self.hact_values = json.dumps(hact, cls=CustomJSONEncoder) self.save() def get_admin_url(self): diff --git a/src/etools/applications/partners/permissions.py b/src/etools/applications/partners/permissions.py index 4a8f8705b9..66efd397d5 100644 --- a/src/etools/applications/partners/permissions.py +++ b/src/etools/applications/partners/permissions.py @@ -6,13 +6,14 @@ from rest_framework import permissions from etools.applications.environment.helpers import tenant_switch_is_active -from etools.applications.EquiTrack.utils import HashableDict from etools.applications.utils.common.utils import get_all_field_names from etools.libraries.djangolib.utils import is_user_in_groups +from etools.libraries.pythonlib.collections import HashableDict # READ_ONLY_API_GROUP_NAME is the name of the permissions group that provides read-only access to some list views. # Initially, this is only being used for PRP-related endpoints. + READ_ONLY_API_GROUP_NAME = 'Read-Only API' diff --git a/src/etools/applications/partners/serializers/interventions_v2.py b/src/etools/applications/partners/serializers/interventions_v2.py index d4f79abc0e..63edd29147 100644 --- a/src/etools/applications/partners/serializers/interventions_v2.py +++ b/src/etools/applications/partners/serializers/interventions_v2.py @@ -13,7 +13,6 @@ from unicef_locations.serializers import LocationSerializer from unicef_snapshot.serializers import SnapshotModelSerializer -from etools.applications.EquiTrack.utils import h11 from etools.applications.funds.models import FundsCommitmentItem, FundsReservationHeader from etools.applications.funds.serializers import FRHeaderSerializer, FRsSerializer from etools.applications.partners.models import ( @@ -35,6 +34,7 @@ RAMIndicatorSerializer, ReportingRequirementSerializer, ) +from etools.libraries.pythonlib.hash import h11 class InterventionBudgetCUSerializer(serializers.ModelSerializer): diff --git a/src/etools/applications/utils/common/utils.py b/src/etools/applications/utils/common/utils.py index c36de796eb..3b8a3d264d 100644 --- a/src/etools/applications/utils/common/utils.py +++ b/src/etools/applications/utils/common/utils.py @@ -17,14 +17,3 @@ def get_all_field_names(TheModel): if not (field.many_to_one and field.related_model is None) and not isinstance(field, GenericForeignKey) ))) - - -def strip_text(text): - return '\r\n'.join(map(lambda line: line.lstrip(), text.splitlines())) - - -def to_choices_list(value): - if isinstance(value, dict): - return value.items() - - return value diff --git a/src/etools/libraries/pythonlib/__init__.py b/src/etools/libraries/pythonlib/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/etools/libraries/pythonlib/collections.py b/src/etools/libraries/pythonlib/collections.py new file mode 100644 index 0000000000..881c2e8095 --- /dev/null +++ b/src/etools/libraries/pythonlib/collections.py @@ -0,0 +1,16 @@ +def to_choices_list(value): + if isinstance(value, dict): + return value.items() + + return value + + +class Vividict(dict): + def __missing__(self, key): + value = self[key] = type(self)() + return value + + +class HashableDict(dict): + def __hash__(self): + return hash(frozenset(self.items())) diff --git a/src/etools/libraries/pythonlib/datetime.py b/src/etools/libraries/pythonlib/datetime.py new file mode 100644 index 0000000000..38c0e41bbd --- /dev/null +++ b/src/etools/libraries/pythonlib/datetime.py @@ -0,0 +1,12 @@ +from datetime import datetime + + +def get_current_year(): + return datetime.today().year + + +def get_quarter(retrieve_date=None): + if not retrieve_date: + retrieve_date = datetime.today() + quarter = (retrieve_date.month - 1) // 3 + 1 + return 'q{}'.format(quarter) diff --git a/src/etools/applications/EquiTrack/encoders.py b/src/etools/libraries/pythonlib/encoders.py similarity index 86% rename from src/etools/applications/EquiTrack/encoders.py rename to src/etools/libraries/pythonlib/encoders.py index 430cba26e4..3bd91647fc 100644 --- a/src/etools/applications/EquiTrack/encoders.py +++ b/src/etools/libraries/pythonlib/encoders.py @@ -3,7 +3,7 @@ from datetime import datetime -class EToolsEncoder(json.JSONEncoder): +class CustomJSONEncoder(json.JSONEncoder): def default(self, o): if isinstance(o, decimal.Decimal): return float(o) diff --git a/src/etools/libraries/pythonlib/hash.py b/src/etools/libraries/pythonlib/hash.py new file mode 100644 index 0000000000..729e9b2420 --- /dev/null +++ b/src/etools/libraries/pythonlib/hash.py @@ -0,0 +1,5 @@ +import hashlib + + +def h11(w): + return hashlib.md5(w).hexdigest()[:9] diff --git a/src/etools/libraries/pythonlib/tests/__init__.py b/src/etools/libraries/pythonlib/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/etools/applications/EquiTrack/tests/test_utils.py b/src/etools/libraries/pythonlib/tests/test_utils.py similarity index 77% rename from src/etools/applications/EquiTrack/tests/test_utils.py rename to src/etools/libraries/pythonlib/tests/test_utils.py index b0f78d5c10..7b53c229f8 100644 --- a/src/etools/applications/EquiTrack/tests/test_utils.py +++ b/src/etools/libraries/pythonlib/tests/test_utils.py @@ -4,7 +4,7 @@ from freezegun import freeze_time -from etools.applications.EquiTrack import utils +from etools.libraries.pythonlib.datetime import get_current_year, get_quarter PATH_SET_TENANT = "etools.applications.libraries.tenant_support.set_tenant" @@ -18,16 +18,16 @@ class TestUtils(SimpleTestCase): def test_get_current_year(self): """test get current year function""" - current_year = utils.get_current_year() + current_year = get_current_year() self.assertEqual(current_year, 2013) @freeze_time("2013-05-26") def test_get_quarter_default(self): """test current quarter function""" - quarter = utils.get_quarter() + quarter = get_quarter() self.assertEqual(quarter, 'q2') def test_get_quarter(self): """test current quarter function""" - quarter = utils.get_quarter(datetime(2016, 10, 1)) + quarter = get_quarter(datetime(2016, 10, 1)) self.assertEqual(quarter, 'q4') From 806705e4256bdf124f2825d64ac072cc4faf7b6e Mon Sep 17 00:00:00 2001 From: robertavram Date: Fri, 8 Feb 2019 11:57:42 -0500 Subject: [PATCH 34/72] Fix build_deploy --- Dockerfile | 3 ++- src/etools/config/settings/base.py | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index b03ec9b66c..62d814d62e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -48,7 +48,8 @@ RUN apk add postgresql-client RUN apk add openssl \ ca-certificates \ libressl2.7-libcrypto -RUN apk add gdal --update-cache --repository http://dl-3.alpinelinux.org/alpine/edge/testing/ +RUN apk add geos \ + gdal --update-cache --repository http://dl-3.alpinelinux.org/alpine/edge/testing/ ADD src /code/ ADD manage.py /code/manage.py diff --git a/src/etools/config/settings/base.py b/src/etools/config/settings/base.py index 9dd2308d81..8268352389 100644 --- a/src/etools/config/settings/base.py +++ b/src/etools/config/settings/base.py @@ -542,3 +542,6 @@ def get_from_secrets_or_env(var_name, default=None): ATTACHMENT_FILEPATH_PREFIX_FUNC = "etools.applications.attachments.utils.get_filepath_prefix" ATTACHMENT_FLAT_MODEL = "etools.applications.attachments.models.AttachmentFlat" ATTACHMENT_DENORMALIZE_FUNC = "etools.applications.attachments.utils.denormalize_attachment" + +GEOS_LIBRARY_PATH = os.getenv('GEOS_LIBRARY_PATH', '/usr/lib/libgeos_c.so.1') # default path +GDAL_LIBRARY_PATH = os.getenv('GDAL_LIBRARY_PATH', '/usr/lib/libgdal.so.20') # default path From 790bf1f751c83f495c7f12787177512941f112a0 Mon Sep 17 00:00:00 2001 From: Domenico DiNicola Date: Thu, 17 Jan 2019 12:04:14 -0500 Subject: [PATCH 35/72] djangolib --- .../applications/EquiTrack/tests/test_urls.py | 17 ++++ .../applications/EquiTrack/urlresolvers.py | 25 ++++++ src/etools/applications/EquiTrack/utils.py | 10 --- .../applications/action_points/models.py | 4 +- .../applications/action_points/serializers.py | 2 +- .../action_points/tests/factories.py | 2 +- src/etools/applications/audit/models.py | 6 +- .../audit/purchase_order/models.py | 2 +- src/etools/applications/firms/utils.py | 3 +- .../hact/migrations/0001_initial.py | 2 +- .../partners/migrations/0001_initial.py | 2 +- .../migrations/0010_auto_20180514_1104.py | 2 +- .../applications/partners/permissions.py | 3 +- src/etools/applications/partners/tasks.py | 2 +- .../applications/partners/tests/test_api.py | 2 +- .../partners/tests/test_api_interventions.py | 2 +- src/etools/applications/t2f/models.py | 2 +- .../common/views.py => tokens/utils.py} | 0 .../applications/tpm/export/serializers.py | 2 +- src/etools/applications/tpm/models.py | 4 +- .../utils/common/tests/test_utils.py | 81 +------------------ .../applications/utils/common/urlresolvers.py | 44 ---------- src/etools/applications/utils/common/utils.py | 19 ----- .../{utils => djangolib/tests}/__init__.py | 0 .../libraries/djangolib/urlresolvers.py | 10 +++ src/etools/libraries/djangolib/utils.py | 31 +++++++ .../libraries/locations/tests/test_api.py | 2 +- .../libraries/pythonlib/urlresolvers.py | 13 +++ .../{utils/test => tests}/api_checker.py | 0 src/etools/libraries/tests/utils.py | 65 +++++++++++++++ src/etools/libraries/utils/test/__init__.py | 0 31 files changed, 184 insertions(+), 175 deletions(-) create mode 100644 src/etools/applications/EquiTrack/tests/test_urls.py create mode 100644 src/etools/applications/EquiTrack/urlresolvers.py delete mode 100644 src/etools/applications/EquiTrack/utils.py rename src/etools/applications/{utils/common/views.py => tokens/utils.py} (100%) delete mode 100644 src/etools/applications/utils/common/urlresolvers.py rename src/etools/libraries/{utils => djangolib/tests}/__init__.py (100%) create mode 100644 src/etools/libraries/djangolib/urlresolvers.py create mode 100644 src/etools/libraries/pythonlib/urlresolvers.py rename src/etools/libraries/{utils/test => tests}/api_checker.py (100%) create mode 100644 src/etools/libraries/tests/utils.py delete mode 100644 src/etools/libraries/utils/test/__init__.py diff --git a/src/etools/applications/EquiTrack/tests/test_urls.py b/src/etools/applications/EquiTrack/tests/test_urls.py new file mode 100644 index 0000000000..a68b9b3221 --- /dev/null +++ b/src/etools/applications/EquiTrack/tests/test_urls.py @@ -0,0 +1,17 @@ +from django.conf import settings +from django.urls import reverse + +from etools.applications.EquiTrack.tests.cases import BaseTenantTestCase +from etools.applications.EquiTrack.urlresolvers import build_frontend_url +from etools.applications.users.tests.factories import UserFactory + + +class TestFrontendUrl(BaseTenantTestCase): + def test_staff_user_url(self): + self.assertIn( + settings.HOST + reverse('main'), + build_frontend_url('test', user=UserFactory(is_staff=True)) + ) + + def test_common_user_url(self): + self.assertIn(settings.HOST, build_frontend_url('test', user=UserFactory(is_staff=False))) diff --git a/src/etools/applications/EquiTrack/urlresolvers.py b/src/etools/applications/EquiTrack/urlresolvers.py new file mode 100644 index 0000000000..9912f37d9a --- /dev/null +++ b/src/etools/applications/EquiTrack/urlresolvers.py @@ -0,0 +1,25 @@ +from django.conf import settings +from django.db import connection +from django.urls import reverse + +from django_tenants.utils import get_tenant_model + +from etools.libraries.pythonlib.urlresolvers import update_url_with_kwargs + + +def build_frontend_url(*parts, user=None): + + if not user or user.is_staff: + frontend_url = '{}{}'.format(settings.HOST, reverse('main')) + else: + frontend_url = '{}{}'.format(settings.HOST, reverse('social:begin', kwargs={'backend': 'azuread-b2c-oauth2'})) + + change_country_view = update_url_with_kwargs( + reverse('users:country-change'), + country=get_tenant_model().objects.get(schema_name=connection.schema_name).id, + next='/'.join(map(str, ('',) + parts)) + ) + + frontend_url = update_url_with_kwargs(frontend_url, next=change_country_view) + + return frontend_url diff --git a/src/etools/applications/EquiTrack/utils.py b/src/etools/applications/EquiTrack/utils.py deleted file mode 100644 index 0f2fbd9e1a..0000000000 --- a/src/etools/applications/EquiTrack/utils.py +++ /dev/null @@ -1,10 +0,0 @@ -from django.conf import settings -from django.contrib.sites.models import Site - - -def get_environment(): - return settings.ENVIRONMENT - - -def get_current_site(): - return Site.objects.get_current() diff --git a/src/etools/applications/action_points/models.py b/src/etools/applications/action_points/models.py index 323626027c..3c6dceedf8 100644 --- a/src/etools/applications/action_points/models.py +++ b/src/etools/applications/action_points/models.py @@ -11,12 +11,12 @@ from unicef_notification.models import Notification from unicef_snapshot.models import Activity +from etools.applications.EquiTrack.urlresolvers import build_frontend_url from etools.applications.action_points.categories.models import Category from etools.applications.action_points.transitions.conditions import ActionPointCompleteActionsTakenCheck from etools.applications.action_points.transitions.serializers.serializers import ActionPointCompleteSerializer -from etools.applications.EquiTrack.utils import get_environment +from etools.libraries.djangolib.utils import get_environment from etools.libraries.fsm.views import has_action_permission -from etools.applications.utils.common.urlresolvers import build_frontend_url from etools.libraries.djangolib.models import GroupWrapper diff --git a/src/etools/applications/action_points/serializers.py b/src/etools/applications/action_points/serializers.py index 788da1704b..41ba3a12f3 100644 --- a/src/etools/applications/action_points/serializers.py +++ b/src/etools/applications/action_points/serializers.py @@ -12,13 +12,13 @@ from etools.applications.action_points.categories.models import Category from etools.applications.action_points.categories.serializers import CategorySerializer from etools.applications.action_points.models import ActionPoint -from etools.applications.EquiTrack.utils import get_current_site from etools.applications.partners.serializers.interventions_v2 import BaseInterventionListSerializer from etools.applications.partners.serializers.partner_organization_v2 import MinimalPartnerOrganizationListSerializer from etools.applications.permissions2.serializers import PermissionsBasedSerializerMixin from etools.applications.reports.serializers.v1 import ResultSerializer, SectionSerializer from etools.applications.users.serializers import OfficeSerializer from etools.applications.users.serializers_v3 import MinimalUserSerializer +from etools.libraries.djangolib.utils import get_current_site class ActionPointBaseSerializer(UserContextSerializerMixin, SnapshotModelSerializer, serializers.ModelSerializer): diff --git a/src/etools/applications/action_points/tests/factories.py b/src/etools/applications/action_points/tests/factories.py index 7a59120c31..82d2e59226 100644 --- a/src/etools/applications/action_points/tests/factories.py +++ b/src/etools/applications/action_points/tests/factories.py @@ -6,13 +6,13 @@ import factory.fuzzy from django_comments.models import Comment -from etools.applications.EquiTrack.utils import get_current_site from etools.applications.action_points.models import ActionPoint from etools.applications.action_points.categories.models import Category from etools.applications.firms.tests.factories import BaseUserFactory from unicef_locations.tests.factories import LocationFactory from etools.applications.partners.tests.factories import InterventionFactory, ResultFactory from etools.applications.reports.tests.factories import SectionFactory +from etools.libraries.djangolib.utils import get_current_site from etools.libraries.tests.factories import InheritedTrait diff --git a/src/etools/applications/audit/models.py b/src/etools/applications/audit/models.py index 53ac5a0eb8..7509a5c479 100644 --- a/src/etools/applications/audit/models.py +++ b/src/etools/applications/audit/models.py @@ -31,11 +31,11 @@ ) from etools.applications.audit.transitions.serializers import EngagementCancelSerializer from etools.applications.audit.utils import generate_final_report -from etools.applications.EquiTrack.utils import get_environment +from etools.applications.EquiTrack.urlresolvers import build_frontend_url from etools.applications.partners.models import PartnerOrganization, PartnerStaffMember -from etools.libraries.fsm.views import has_action_permission -from etools.applications.utils.common.urlresolvers import build_frontend_url from etools.libraries.djangolib.models import GroupWrapper, InheritedModelMixin +from etools.libraries.djangolib.utils import get_environment +from etools.libraries.fsm.views import has_action_permission class Engagement(InheritedModelMixin, TimeStampedModel, models.Model): diff --git a/src/etools/applications/audit/purchase_order/models.py b/src/etools/applications/audit/purchase_order/models.py index a359673bd1..59fa0369af 100644 --- a/src/etools/applications/audit/purchase_order/models.py +++ b/src/etools/applications/audit/purchase_order/models.py @@ -4,8 +4,8 @@ from model_utils.models import TimeStampedModel from unicef_notification.utils import send_notification_with_template -from etools.applications.EquiTrack.utils import get_environment from etools.applications.firms.models import BaseFirm, BaseStaffMember +from etools.libraries.djangolib.utils import get_environment class AuditorFirm(BaseFirm): diff --git a/src/etools/applications/firms/utils.py b/src/etools/applications/firms/utils.py index 9ddae56490..7bc271d153 100644 --- a/src/etools/applications/firms/utils.py +++ b/src/etools/applications/firms/utils.py @@ -2,9 +2,10 @@ import uuid from django.urls import reverse + from unicef_notification.utils import send_notification_with_template -from etools.applications.EquiTrack.utils import get_environment +from etools.libraries.djangolib.utils import get_environment def generate_username(): diff --git a/src/etools/applications/hact/migrations/0001_initial.py b/src/etools/applications/hact/migrations/0001_initial.py index 59c886d55c..18c9304f0b 100644 --- a/src/etools/applications/hact/migrations/0001_initial.py +++ b/src/etools/applications/hact/migrations/0001_initial.py @@ -6,7 +6,7 @@ import model_utils.fields -import etools.applications.EquiTrack.utils +import etools.applications.EquiTrack.urlresolvers class Migration(migrations.Migration): diff --git a/src/etools/applications/partners/migrations/0001_initial.py b/src/etools/applications/partners/migrations/0001_initial.py index dd38d3f982..63c04deb6d 100644 --- a/src/etools/applications/partners/migrations/0001_initial.py +++ b/src/etools/applications/partners/migrations/0001_initial.py @@ -9,7 +9,7 @@ import model_utils.fields from unicef_djangolib.fields import CurrencyField, QuarterField -import etools.applications.EquiTrack.utils +import etools.applications.EquiTrack.urlresolvers import etools.applications.partners.models diff --git a/src/etools/applications/partners/migrations/0010_auto_20180514_1104.py b/src/etools/applications/partners/migrations/0010_auto_20180514_1104.py index a60262d0c9..4547a70320 100644 --- a/src/etools/applications/partners/migrations/0010_auto_20180514_1104.py +++ b/src/etools/applications/partners/migrations/0010_auto_20180514_1104.py @@ -2,7 +2,7 @@ from django.db import migrations, models import django.db.models.deletion import django.utils.timezone -import etools.applications.EquiTrack.utils +import etools.applications.EquiTrack.urlresolvers import model_utils.fields diff --git a/src/etools/applications/partners/permissions.py b/src/etools/applications/partners/permissions.py index 66efd397d5..ce0a81fcd9 100644 --- a/src/etools/applications/partners/permissions.py +++ b/src/etools/applications/partners/permissions.py @@ -6,8 +6,7 @@ from rest_framework import permissions from etools.applications.environment.helpers import tenant_switch_is_active -from etools.applications.utils.common.utils import get_all_field_names -from etools.libraries.djangolib.utils import is_user_in_groups +from etools.libraries.djangolib.utils import get_all_field_names, is_user_in_groups from etools.libraries.pythonlib.collections import HashableDict # READ_ONLY_API_GROUP_NAME is the name of the permissions group that provides read-only access to some list views. diff --git a/src/etools/applications/partners/tasks.py b/src/etools/applications/partners/tasks.py index 3bbbcfc962..404229b702 100644 --- a/src/etools/applications/partners/tasks.py +++ b/src/etools/applications/partners/tasks.py @@ -10,7 +10,6 @@ from django_tenants.utils import schema_context from unicef_notification.utils import send_notification_with_template -from etools.applications.EquiTrack.utils import get_environment from etools.applications.partners.models import Agreement, Intervention, PartnerOrganization from etools.applications.partners.utils import ( copy_all_attachments, @@ -21,6 +20,7 @@ from etools.applications.partners.validation.interventions import InterventionValid from etools.applications.users.models import Country from etools.config.celery import app +from etools.libraries.djangolib.utils import get_environment from etools.libraries.tenant_support.utils import run_on_all_tenants logger = get_task_logger(__name__) diff --git a/src/etools/applications/partners/tests/test_api.py b/src/etools/applications/partners/tests/test_api.py index cc38b62b2f..265648a6f2 100644 --- a/src/etools/applications/partners/tests/test_api.py +++ b/src/etools/applications/partners/tests/test_api.py @@ -6,7 +6,7 @@ from etools.applications.partners.models import PartnerType, PartnerOrganization from etools.applications.partners.tests.factories import (AgreementFactory, AgreementAmendmentFactory, PartnerFactory, InterventionFactory, InterventionAmendmentFactory, InterventionResultLinkFactory) -from etools.libraries.utils.test.api_checker import ApiCheckerMixin, ViewSetChecker, AssertTimeStampedMixin +from etools.libraries.tests.api_checker import ApiCheckerMixin, ViewSetChecker, AssertTimeStampedMixin class TestAPIAgreements(ApiCheckerMixin, AssertTimeStampedMixin, BaseTenantTestCase): diff --git a/src/etools/applications/partners/tests/test_api_interventions.py b/src/etools/applications/partners/tests/test_api_interventions.py index af2d31abb0..667ce0ffae 100644 --- a/src/etools/applications/partners/tests/test_api_interventions.py +++ b/src/etools/applications/partners/tests/test_api_interventions.py @@ -44,7 +44,7 @@ SectionFactory, ) from etools.applications.users.tests.factories import GroupFactory, UserFactory -from etools.applications.utils.common.utils import get_all_field_names +from etools.libraries.djangolib.utils import get_all_field_names def _add_user_to_partnership_manager_group(user): diff --git a/src/etools/applications/t2f/models.py b/src/etools/applications/t2f/models.py index 5c30410cba..786630a1a2 100644 --- a/src/etools/applications/t2f/models.py +++ b/src/etools/applications/t2f/models.py @@ -14,10 +14,10 @@ from unicef_djangolib.fields import CodedGenericRelation from unicef_notification.utils import send_notification +from etools.applications.EquiTrack.urlresolvers import build_frontend_url from etools.applications.action_points.models import ActionPoint from etools.applications.t2f.serializers.mailing import TravelMailSerializer from etools.applications.users.models import WorkspaceCounter -from etools.applications.utils.common.urlresolvers import build_frontend_url log = logging.getLogger(__name__) diff --git a/src/etools/applications/utils/common/views.py b/src/etools/applications/tokens/utils.py similarity index 100% rename from src/etools/applications/utils/common/views.py rename to src/etools/applications/tokens/utils.py diff --git a/src/etools/applications/tpm/export/serializers.py b/src/etools/applications/tpm/export/serializers.py index d149803610..043f343f8e 100644 --- a/src/etools/applications/tpm/export/serializers.py +++ b/src/etools/applications/tpm/export/serializers.py @@ -7,7 +7,7 @@ from rest_framework import serializers from unicef_restlib.fields import CommaSeparatedExportField -from etools.applications.utils.common.urlresolvers import build_frontend_url +from etools.applications.EquiTrack.urlresolvers import build_frontend_url class UsersExportField(serializers.Field): diff --git a/src/etools/applications/tpm/models.py b/src/etools/applications/tpm/models.py index e9f87e85e3..c34be60ec8 100644 --- a/src/etools/applications/tpm/models.py +++ b/src/etools/applications/tpm/models.py @@ -14,9 +14,10 @@ from unicef_djangolib.fields import CodedGenericRelation from unicef_notification.utils import send_notification_with_template +from etools.applications.EquiTrack.urlresolvers import build_frontend_url from etools.applications.action_points.models import ActionPoint from etools.applications.activities.models import Activity -from etools.applications.EquiTrack.utils import get_environment +from etools.libraries.djangolib.utils import get_environment from etools.libraries.fsm.views import has_action_permission from etools.applications.publics.models import SoftDeleteMixin from etools.applications.tpm.tpmpartners.models import TPMPartner, TPMPartnerStaffMember @@ -30,7 +31,6 @@ TPMVisitCancelSerializer, TPMVisitRejectSerializer, ) -from etools.applications.utils.common.urlresolvers import build_frontend_url from etools.libraries.djangolib.models import GroupWrapper diff --git a/src/etools/applications/utils/common/tests/test_utils.py b/src/etools/applications/utils/common/tests/test_utils.py index 2334dd6512..460bec5589 100644 --- a/src/etools/applications/utils/common/tests/test_utils.py +++ b/src/etools/applications/utils/common/tests/test_utils.py @@ -1,74 +1,6 @@ -from django.conf import settings -from django.contrib.auth.models import Group -from django.contrib.contenttypes.fields import GenericForeignKey -from django.db import models -from django.test import TestCase from django.urls import reverse -from django.utils.translation import ugettext_lazy as _ -from rest_framework import status - -from etools.applications.EquiTrack.tests.cases import BaseTenantTestCase -from etools.applications.users.tests.factories import UserFactory -from etools.applications.utils.common.urlresolvers import build_frontend_url -from etools.applications.utils.common.utils import get_all_field_names - - -class CommonUtilsTest(TestCase): - """Tests for utils.common.utils""" - - def test_get_all_field_names(self): - """Exercise get_all_field_names() which is Django-provided code to replace Model._meta.get_all_field_names()""" - class Useless: - pass - - class Dummy(models.Model): - """Model to contain the many different types of fields I want to test. - - The list of fields in the model is not exhaustive, but it covers a variety of Django field types. - """ - # CHOICES should not be in the list of field names - CHOICES = (('to be'), ('not to be')) - - # fields 1 - 9 inclusive should be in the list of field names - field01 = models.CharField(max_length=50) - field02 = models.IntegerField(primary_key=True) - field03 = models.IntegerField(db_index=True) - field04 = models.IntegerField(editable=False) - field05 = models.IntegerField() - field06 = models.DateField() - field07 = models.TextField(blank=True) - field08 = models.IntegerField(unique=True) - field09 = models.IntegerField(default=42) - # fields 10 and 11 should be in the list of field names, along with the automatically-created fields - # 'field10_id' and 'field11_id' - field10 = models.ForeignKey(Group, on_delete=models.CASCADE) - field11 = models.OneToOneField(Group, on_delete=models.CASCADE) - # field 12 should be in the list of field names, but it doesn't get a 'field12_id' because it's M2M - field12 = models.ManyToManyField(Group) - # field 13 shouldn't be in the list of field names. Generic FKs are excluded according to the Django doc. - # https://docs.djangoproject.com/en/1.10/ref/models/meta/#migrating-from-the-old-api - field13 = GenericForeignKey() - # fields 14 and 15 shouldn't be in the list of field names because they're not Django fields. - field14 = {} - field15 = Useless() - - class Meta: - verbose_name_plural = _('Dummies') - app_label = 'tests' - expected_field_names = ['field{:02}'.format(i + 1) for i in range(12)] - expected_field_names += ['field10_id', 'field11_id'] - expected_field_names.sort() - - actual_field_names = sorted(get_all_field_names(Dummy)) - - self.assertEqual(expected_field_names, actual_field_names) - - # Bonus -- if we're still under Django < 1.10 where Model._meta.get_all_field_names() still exists, - # ensure our function produces the same results as that one. - if hasattr(Dummy._meta, 'get_all_field_names'): - actual_field_names = sorted(Dummy._meta.get_all_field_names()) - self.assertEqual(expected_field_names, actual_field_names) +from rest_framework import status class TestExportMixin(object): @@ -82,14 +14,3 @@ def _test_export(self, user, url_name, args=tuple(), kwargs=None, status_code=st self.assertEqual(response.status_code, status_code) if status_code == status.HTTP_200_OK: self.assertIn(response._headers['content-disposition'][0], 'Content-Disposition') - - -class TestFrontendUrl(BaseTenantTestCase): - def test_staff_user_url(self): - self.assertIn( - settings.HOST + reverse('main'), - build_frontend_url('test', user=UserFactory(is_staff=True)) - ) - - def test_common_user_url(self): - self.assertIn(settings.HOST, build_frontend_url('test', user=UserFactory(is_staff=False))) diff --git a/src/etools/applications/utils/common/urlresolvers.py b/src/etools/applications/utils/common/urlresolvers.py deleted file mode 100644 index f2e9ce06cf..0000000000 --- a/src/etools/applications/utils/common/urlresolvers.py +++ /dev/null @@ -1,44 +0,0 @@ -from urllib.parse import parse_qsl, urlencode, urljoin, urlparse, urlunparse - -from django.conf import settings -from django.db import connection -from django.urls import reverse - -from etools.applications.users.models import Country - - -def build_absolute_url(url): - if not url: - return '' - - return urljoin(settings.HOST, url) - - -def update_url_with_kwargs(url, **kwargs): - if not url: - return - - url_parts = list(urlparse(url)) - query = dict(parse_qsl(url_parts[4])) - query.update(kwargs) - url_parts[4] = urlencode(query) - - return urlunparse(url_parts) - - -def build_frontend_url(*parts, user=None, **kwargs): - - if not user or user.is_staff: - frontend_url = '{}{}'.format(settings.HOST, reverse('main')) - else: - frontend_url = '{}{}'.format(settings.HOST, reverse('social:begin', kwargs={'backend': 'azuread-b2c-oauth2'})) - - change_country_view = update_url_with_kwargs( - reverse('users:country-change'), - country=Country.objects.get(schema_name=connection.schema_name).id, - next='/'.join(map(str, ('',) + parts)) - ) - - frontend_url = update_url_with_kwargs(frontend_url, next=change_country_view) - - return frontend_url diff --git a/src/etools/applications/utils/common/utils.py b/src/etools/applications/utils/common/utils.py index 3b8a3d264d..e69de29bb2 100644 --- a/src/etools/applications/utils/common/utils.py +++ b/src/etools/applications/utils/common/utils.py @@ -1,19 +0,0 @@ -from itertools import chain - -from django.contrib.contenttypes.fields import GenericForeignKey - - -def get_all_field_names(TheModel): - """Return a list of all field names that are possible for this model (including reverse relation names). - Any internal-only field names are not included. - - Replacement for MyModel._meta.get_all_field_names() which does not exist under Django 1.10. - https://github.com/django/django/blob/stable/1.7.x/django/db/models/options.py#L422 - https://docs.djangoproject.com/en/1.10/ref/models/meta/#migrating-from-the-old-api - """ - return list(set(chain.from_iterable( - (field.name, field.attname) if hasattr(field, 'attname') else (field.name,) - for field in TheModel._meta.get_fields() - if not (field.many_to_one and field.related_model is None) and - not isinstance(field, GenericForeignKey) - ))) diff --git a/src/etools/libraries/utils/__init__.py b/src/etools/libraries/djangolib/tests/__init__.py similarity index 100% rename from src/etools/libraries/utils/__init__.py rename to src/etools/libraries/djangolib/tests/__init__.py diff --git a/src/etools/libraries/djangolib/urlresolvers.py b/src/etools/libraries/djangolib/urlresolvers.py new file mode 100644 index 0000000000..f00453035b --- /dev/null +++ b/src/etools/libraries/djangolib/urlresolvers.py @@ -0,0 +1,10 @@ +from urllib.parse import urljoin + +from django.conf import settings + + +def build_absolute_url(url): + if not url: + return '' + + return urljoin(settings.HOST, url) diff --git a/src/etools/libraries/djangolib/utils.py b/src/etools/libraries/djangolib/utils.py index 3666fe03e0..a124028ebb 100644 --- a/src/etools/libraries/djangolib/utils.py +++ b/src/etools/libraries/djangolib/utils.py @@ -1,3 +1,18 @@ +from itertools import chain + +from django.conf import settings +from django.contrib.contenttypes.fields import GenericForeignKey +from django.contrib.sites.models import Site + + +def get_environment(): + return settings.ENVIRONMENT + + +def get_current_site(): + return Site.objects.get_current() + + def is_user_in_groups(user, group_names): """Utility function; returns True if user is in ANY of the groups in the group_names list, False if the user is in none of them. Note that group_names should be a tuple or list, not a single string. @@ -6,3 +21,19 @@ def is_user_in_groups(user, group_names): # Anticipate common programming oversight. raise ValueError('group_names parameter must be a tuple or list, not a string') return user.groups.filter(name__in=group_names).exists() + + +def get_all_field_names(TheModel): + """Return a list of all field names that are possible for this model (including reverse relation names). + Any internal-only field names are not included. + + Replacement for MyModel._meta.get_all_field_names() which does not exist under Django 1.10. + https://github.com/django/django/blob/stable/1.7.x/django/db/models/options.py#L422 + https://docs.djangoproject.com/en/1.10/ref/models/meta/#migrating-from-the-old-api + """ + return list(set(chain.from_iterable( + (field.name, field.attname) if hasattr(field, 'attname') else (field.name,) + for field in TheModel._meta.get_fields() + if not (field.many_to_one and field.related_model is None) and + not isinstance(field, GenericForeignKey) + ))) diff --git a/src/etools/libraries/locations/tests/test_api.py b/src/etools/libraries/locations/tests/test_api.py index e80d00733e..0a675dbea1 100644 --- a/src/etools/libraries/locations/tests/test_api.py +++ b/src/etools/libraries/locations/tests/test_api.py @@ -3,7 +3,7 @@ from unicef_locations.tests.factories import CartoDBTableFactory, GatewayTypeFactory, LocationFactory from etools.applications.EquiTrack.tests.cases import BaseTenantTestCase -from etools.libraries.utils.test.api_checker import AssertTimeStampedMixin, ViewSetChecker +from etools.libraries.tests.api_checker import AssertTimeStampedMixin, ViewSetChecker class TestAPILocations(AssertTimeStampedMixin, BaseTenantTestCase, metaclass=ViewSetChecker): diff --git a/src/etools/libraries/pythonlib/urlresolvers.py b/src/etools/libraries/pythonlib/urlresolvers.py new file mode 100644 index 0000000000..56aa87d321 --- /dev/null +++ b/src/etools/libraries/pythonlib/urlresolvers.py @@ -0,0 +1,13 @@ +from urllib.parse import parse_qsl, urlencode, urlparse, urlunparse + + +def update_url_with_kwargs(url, **kwargs): + if not url: + return + + url_parts = list(urlparse(url)) + query = dict(parse_qsl(url_parts[4])) + query.update(kwargs) + url_parts[4] = urlencode(query) + + return urlunparse(url_parts) diff --git a/src/etools/libraries/utils/test/api_checker.py b/src/etools/libraries/tests/api_checker.py similarity index 100% rename from src/etools/libraries/utils/test/api_checker.py rename to src/etools/libraries/tests/api_checker.py diff --git a/src/etools/libraries/tests/utils.py b/src/etools/libraries/tests/utils.py new file mode 100644 index 0000000000..9bc80a1aef --- /dev/null +++ b/src/etools/libraries/tests/utils.py @@ -0,0 +1,65 @@ +from django.contrib.auth.models import Group +from django.contrib.contenttypes.fields import GenericForeignKey +from django.db import models +from django.test import TestCase +from django.utils.translation import ugettext_lazy as _ + +from etools.libraries.djangolib.utils import get_all_field_names + + +class CommonUtilsTest(TestCase): + """Tests for utils.common.utils""" + + def test_get_all_field_names(self): + """Exercise get_all_field_names() which is Django-provided code to replace Model._meta.get_all_field_names()""" + class Useless: + pass + + class Dummy(models.Model): + """Model to contain the many different types of fields I want to test. + + The list of fields in the model is not exhaustive, but it covers a variety of Django field types. + """ + # CHOICES should not be in the list of field names + CHOICES = (('to be'), ('not to be')) + + # fields 1 - 9 inclusive should be in the list of field names + field01 = models.CharField(max_length=50) + field02 = models.IntegerField(primary_key=True) + field03 = models.IntegerField(db_index=True) + field04 = models.IntegerField(editable=False) + field05 = models.IntegerField() + field06 = models.DateField() + field07 = models.TextField(blank=True) + field08 = models.IntegerField(unique=True) + field09 = models.IntegerField(default=42) + # fields 10 and 11 should be in the list of field names, along with the automatically-created fields + # 'field10_id' and 'field11_id' + field10 = models.ForeignKey(Group, on_delete=models.CASCADE) + field11 = models.OneToOneField(Group, on_delete=models.CASCADE) + # field 12 should be in the list of field names, but it doesn't get a 'field12_id' because it's M2M + field12 = models.ManyToManyField(Group) + # field 13 shouldn't be in the list of field names. Generic FKs are excluded according to the Django doc. + # https://docs.djangoproject.com/en/1.10/ref/models/meta/#migrating-from-the-old-api + field13 = GenericForeignKey() + # fields 14 and 15 shouldn't be in the list of field names because they're not Django fields. + field14 = {} + field15 = Useless() + + class Meta: + verbose_name_plural = _('Dummies') + app_label = 'tests' + + expected_field_names = ['field{:02}'.format(i + 1) for i in range(12)] + expected_field_names += ['field10_id', 'field11_id'] + expected_field_names.sort() + + actual_field_names = sorted(get_all_field_names(Dummy)) + + self.assertEqual(expected_field_names, actual_field_names) + + # Bonus -- if we're still under Django < 1.10 where Model._meta.get_all_field_names() still exists, + # ensure our function produces the same results as that one. + if hasattr(Dummy._meta, 'get_all_field_names'): + actual_field_names = sorted(Dummy._meta.get_all_field_names()) + self.assertEqual(expected_field_names, actual_field_names) diff --git a/src/etools/libraries/utils/test/__init__.py b/src/etools/libraries/utils/test/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 From 49f79d4a351359fea9eee9bc36ce6c5d513c125b Mon Sep 17 00:00:00 2001 From: Domenico DiNicola Date: Wed, 23 Jan 2019 16:23:08 -0500 Subject: [PATCH 36/72] cleaning --- .../applications/t2f/serializers/export.py | 44 +------------------ .../applications/t2f/tests/test_exports.py | 9 ++-- .../t2f/tests/test_travel_list.py | 1 - src/etools/applications/t2f/urls.py | 3 +- src/etools/applications/t2f/views/exports.py | 19 +------- 5 files changed, 8 insertions(+), 68 deletions(-) diff --git a/src/etools/applications/t2f/serializers/export.py b/src/etools/applications/t2f/serializers/export.py index 6c5ff55b2e..57eafbb88a 100644 --- a/src/etools/applications/t2f/serializers/export.py +++ b/src/etools/applications/t2f/serializers/export.py @@ -5,22 +5,6 @@ from etools.applications.t2f.models import TravelAttachment -class YesOrEmptyField(serializers.BooleanField): - def to_representation(self, value): - value = super().to_representation(value) - if value: - return _('Yes') - return '' - - -class YesOrNoField(serializers.BooleanField): - def to_representation(self, value): - value = super().to_representation(value) - if value: - return _('Yes') - return _('No') - - class TravelActivityExportSerializer(serializers.Serializer): reference_number = serializers.CharField(source='travel.reference_number', read_only=True) traveler = serializers.CharField(source='travel.traveler.get_full_name', read_only=True) @@ -83,31 +67,7 @@ def get_hact_visit_report(self, obj): return "Yes" if TravelAttachment.objects.filter( travel=obj.travel, type__istartswith="HACT Programme Monitoring", - ).exists() else "No" - - -class FinanceExportSerializer(serializers.Serializer): - reference_number = serializers.CharField() - traveler = serializers.CharField(source='traveler.get_full_name', read_only=True) - office = serializers.CharField(source='office.name', read_only=True) - section = serializers.CharField(source='section.name', read_only=True) - status = serializers.CharField() - supervisor = serializers.CharField(source='supervisor.get_full_name', read_only=True) - start_date = serializers.DateTimeField(format='%d-%b-%Y') - end_date = serializers.DateTimeField(format='%d-%b-%Y') - purpose_of_travel = serializers.CharField(source='purpose') - mode_of_travel = serializers.SerializerMethodField() - international_travel = YesOrNoField() - require_ta = YesOrNoField(source='ta_required') - - class Meta: - fields = ('reference_number', 'traveler', 'office', 'section', 'status', 'supervisor', 'start_date', - 'end_date', 'purpose_of_travel', 'mode_of_travel', 'international_travel', 'require_ta') - - def get_mode_of_travel(self, obj): - if obj.mode_of_travel: - return ', '.join(obj.mode_of_travel) - return '' + ).exists() else "" class TravelAdminExportSerializer(serializers.Serializer): @@ -121,7 +81,7 @@ class TravelAdminExportSerializer(serializers.Serializer): departure_time = serializers.DateTimeField(source='departure_date', format='%d-%b-%Y %I:%M %p') arrival_time = serializers.DateTimeField(source='arrival_date', format='%d-%b-%Y %I:%M %p') dsa_area = serializers.CharField(source='dsa_region.area_code', read_only=True) - overnight_travel = YesOrEmptyField() + overnight_travel = serializers.BooleanField() mode_of_travel = serializers.CharField() airline = serializers.SerializerMethodField() diff --git a/src/etools/applications/t2f/tests/test_exports.py b/src/etools/applications/t2f/tests/test_exports.py index 1a6ddbd749..45bc080727 100644 --- a/src/etools/applications/t2f/tests/test_exports.py +++ b/src/etools/applications/t2f/tests/test_exports.py @@ -40,9 +40,6 @@ def test_urls(self): export_url = reverse('t2f:travels:list:activity_export') self.assertEqual(export_url, '/api/t2f/travels/export/') - export_url = reverse('t2f:travels:list:finance_export') - self.assertEqual(export_url, '/api/t2f/travels/finance-export/') - export_url = reverse('t2f:travels:list:travel_admin_export') self.assertEqual(export_url, '/api/t2f/travels/travel-admin-export/') @@ -205,7 +202,7 @@ def test_activity_export(self): '14-Nov-2017', '', '', - 'No', + '', ]) self.assertEqual(rows[2], [ @@ -225,7 +222,7 @@ def test_activity_export(self): '14-Nov-2017', 'YES', 'Lenox Lewis', - 'No', + '', ]) self.assertEqual(rows[3], [ @@ -245,7 +242,7 @@ def test_activity_export(self): '14-Nov-2017', '', '', - 'No', + '', ]) self.assertEqual(rows[4], [ diff --git a/src/etools/applications/t2f/tests/test_travel_list.py b/src/etools/applications/t2f/tests/test_travel_list.py index 79f5784879..576ee3f798 100644 --- a/src/etools/applications/t2f/tests/test_travel_list.py +++ b/src/etools/applications/t2f/tests/test_travel_list.py @@ -36,7 +36,6 @@ def test_urls(self): ('state_change', 'save_and_submit/', {'transition_name': 'save_and_submit'}), ('state_change', 'mark_as_completed/', {'transition_name': Travel.COMPLETE}), ('activity_export', 'export/', {}), - ('finance_export', 'finance-export/', {}), ('travel_admin_export', 'travel-admin-export/', {}), ('activities', 'activities/1/', {'partner_organization_pk': 1}), ('activities-intervention', 'activities/partnership/1/', {'partnership_pk': 1}), diff --git a/src/etools/applications/t2f/urls.py b/src/etools/applications/t2f/urls.py index f091fc0496..cd339229ff 100644 --- a/src/etools/applications/t2f/urls.py +++ b/src/etools/applications/t2f/urls.py @@ -2,7 +2,7 @@ from etools.applications.t2f.models import Travel from etools.applications.t2f.views.dashboard import ActionPointDashboardViewSet, TravelDashboardViewSet -from etools.applications.t2f.views.exports import FinanceExport, TravelActivityExport, TravelAdminExport +from etools.applications.t2f.views.exports import TravelActivityExport, TravelAdminExport from etools.applications.t2f.views.generics import PermissionMatrixView, StaticDataView, VendorNumberListView from etools.applications.t2f.views.travel import ( TravelActivityPerInterventionViewSet, @@ -50,7 +50,6 @@ url(r'^$', travel_list, name='index'), url(r'^(?Psave_and_submit|mark_as_completed)/$', travel_list_state_change, name='state_change'), url(r'^export/$', TravelActivityExport.as_view(), name='activity_export'), - url(r'^finance-export/$', FinanceExport.as_view(), name='finance_export'), url(r'^travel-admin-export/$', TravelAdminExport.as_view(), name='travel_admin_export'), url(r'^activities/partnership/(?P[0-9]+)/', TravelActivityPerInterventionViewSet.as_view({'get': 'list'}), name='activities-intervention'), diff --git a/src/etools/applications/t2f/views/exports.py b/src/etools/applications/t2f/views/exports.py index 528c678a58..46d9309397 100644 --- a/src/etools/applications/t2f/views/exports.py +++ b/src/etools/applications/t2f/views/exports.py @@ -7,13 +7,12 @@ from rest_framework import generics, status from rest_framework.permissions import IsAdminUser from rest_framework.response import Response -from rest_framework_csv import renderers from unicef_restlib.views import QueryStringFilterMixin +from etools.applications.EquiTrack.renderers import FriendlyCSVRenderer from etools.applications.t2f.filters import travel_list from etools.applications.t2f.models import ItineraryItem, Travel, TravelActivity from etools.applications.t2f.serializers.export import ( - FinanceExportSerializer, TravelActivityExportSerializer, TravelAdminExportSerializer, ) @@ -28,7 +27,7 @@ class ExportBaseView(generics.GenericAPIView): travel_list.ShowHiddenFilter, travel_list.TravelSortFilter, travel_list.TravelFilterBoxFilter) - renderer_classes = (renderers.CSVRenderer,) + renderer_classes = (FriendlyCSVRenderer,) def get_renderer_context(self): context = super().get_renderer_context() @@ -88,20 +87,6 @@ def get(self, request): return response -class FinanceExport(ExportBaseView): - serializer_class = FinanceExportSerializer - - def get(self, request): - queryset = self.filter_queryset(self.get_queryset()) - queryset = queryset.select_related( - 'traveler', 'office', 'section', 'supervisor') - serializer = self.get_serializer(queryset, many=True) - - response = Response(data=serializer.data, status=status.HTTP_200_OK) - response['Content-Disposition'] = 'attachment; filename="FinanceExport.csv"' - return response - - class TravelAdminExport(ExportBaseView): serializer_class = TravelAdminExportSerializer From fffbd2161383580e64e77108273bd6f975fe7057 Mon Sep 17 00:00:00 2001 From: Domenico DiNicola Date: Tue, 22 Jan 2019 11:10:57 -0500 Subject: [PATCH 37/72] 9364 staff spot checks export: rename export file name --- src/etools/applications/audit/views.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/etools/applications/audit/views.py b/src/etools/applications/audit/views.py index d42cdf9e2c..171260acc6 100644 --- a/src/etools/applications/audit/views.py +++ b/src/etools/applications/audit/views.py @@ -258,6 +258,7 @@ class EngagementViewSet( ordering_fields = ('agreement__order_number', 'agreement__auditor_firm__name', 'partner__name', 'engagement_type', 'status') filter_class = EngagementFilter + export_filename = 'engagements' ENGAGEMENT_MAPPING = { Engagement.TYPES.audit: { @@ -380,7 +381,7 @@ def export_list_csv(self, request, *args, **kwargs): serializer = EngagementExportSerializer(engagements, many=True) return Response(serializer.data, headers={ - 'Content-Disposition': 'attachment;filename=engagements_{}.csv'.format(timezone.now().date()) + 'Content-Disposition': 'attachment;filename={}_{}.csv'.format(self.export_filename, timezone.now().date()) }) @@ -434,6 +435,7 @@ def export_csv(self, request, *args, **kwargs): class StaffSpotCheckViewSet(SpotCheckViewSet): unicef_engagements = True + export_filename = 'staff_spot_checks' serializer_class = StaffSpotCheckSerializer serializer_action_classes = { 'list': StaffSpotCheckListSerializer From 33707a7f0e859382822c9a3670d9890479d77115 Mon Sep 17 00:00:00 2001 From: Domenico DiNicola Date: Fri, 8 Feb 2019 14:45:12 -0500 Subject: [PATCH 38/72] expose as integer --- .../partners/serializers/partner_organization_v2.py | 2 ++ src/etools/applications/partners/tests/test_views.py | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/etools/applications/partners/serializers/partner_organization_v2.py b/src/etools/applications/partners/serializers/partner_organization_v2.py index f53b819d67..6dad5ec7b0 100644 --- a/src/etools/applications/partners/serializers/partner_organization_v2.py +++ b/src/etools/applications/partners/serializers/partner_organization_v2.py @@ -308,6 +308,8 @@ class PartnerOrganizationDashboardSerializer(serializers.ModelSerializer): action_points = serializers.ReadOnlyField(read_only=True) total_ct_cp = serializers.FloatField(read_only=True) total_ct_ytd = serializers.FloatField(read_only=True) + outstanding_dct_amount_6_to_9_months_usd = serializers.FloatField(read_only=True) + outstanding_dct_amount_more_than_9_months_usd = serializers.FloatField(read_only=True) class Meta: model = PartnerOrganization diff --git a/src/etools/applications/partners/tests/test_views.py b/src/etools/applications/partners/tests/test_views.py index 6e2675b089..eafb39719d 100644 --- a/src/etools/applications/partners/tests/test_views.py +++ b/src/etools/applications/partners/tests/test_views.py @@ -1919,8 +1919,8 @@ def setUp(self): def test_queryset(self): self.assertEqual(self.record['total_ct_cp'], 789.0) self.assertEqual(self.record['total_ct_ytd'], 123.0) - self.assertEqual(self.record['outstanding_dct_amount_6_to_9_months_usd'], '69.00') - self.assertEqual(self.record['outstanding_dct_amount_more_than_9_months_usd'], '90.00') + self.assertEqual(self.record['outstanding_dct_amount_6_to_9_months_usd'], 69.00) + self.assertEqual(self.record['outstanding_dct_amount_more_than_9_months_usd'], 90.00) def test_sections(self): self.assertEqual(self.record['sections'], '{}|{}'.format(self.sec1.name, self.sec2.name)) From 03bb6c1654fb287a65d36da5df24c8a1ec7f8744 Mon Sep 17 00:00:00 2001 From: Domenico DiNicola Date: Fri, 8 Feb 2019 13:29:43 -0500 Subject: [PATCH 39/72] update requirements --- Pipfile | 24 ++++---- Pipfile.lock | 161 +++++++++++++++++++++++++++++---------------------- 2 files changed, 103 insertions(+), 82 deletions(-) diff --git a/Pipfile b/Pipfile index c9ed32b493..916c126c8d 100644 --- a/Pipfile +++ b/Pipfile @@ -25,7 +25,7 @@ azure-common = "==1.1.8" azure-nspkg = "==2.0.0" azure-storage = "==0.20.2" billiard = "==3.5.0.3" -carto = "==1.3.1" +carto = "==1.4" celery = "==4.2.1" cffi = "==1.11.5" coreapi = "==2.3.3" @@ -41,9 +41,9 @@ django-contrib-comments = "==1.9" django-cors-headers = "==2.4" django-debug-toolbar = "==1.11" django-easy-pdf = "==0.1.1" -django-filter = "==2.0" +django-filter = "==2.1" django-fsm = "==2.6" -django-import-export = "==1.1" +django-import-export = "==1.2" django-js-asset = "==1.1.0" django-leaflet = "==0.24" django-logentry-admin = "==1.0.4" @@ -60,8 +60,8 @@ djangorestframework-csv = "==2.1.0" djangorestframework-gis = "==0.14" djangorestframework-jwt = "==1.11.0" djangorestframework-recursive = "==0.1.2" -djangorestframework-xml = "==1.3" -djangorestframework = "==3.9.0" +djangorestframework-xml = "==1.4" +djangorestframework = "==3.9.1" drf-nested-routers = "==0.91" drf-querystringfilter = "==1.0.0" drfpasswordless = "==1.2" @@ -80,7 +80,7 @@ oauthlib = "==2.0.7" odfpy = "==1.3.6" openapi-codec = "==1.3.2" openpyxl = "==2.5.9" -psycopg2-binary = "==2.7.5" +psycopg2-binary = "==2.7.7" psycopg2 = "==2.7.5" pycparser = "==2.18" pyrestcli = "==0.6.4" @@ -88,11 +88,11 @@ python-crontab = "==2.3.5" python-dateutil = "==2.5.3" python3-openid = "==3.1.0" pytz = "==2018.3" -raven = "==6.9" +raven = "==6.10" redis = "==2.10.6" reportlab = "==3.5.9" requests-oauthlib = "==0.8.0" -requests = "==2.11.1" +requests = "==2.21" simplejson = "==3.16.0" six = "==1.11.0" social-auth-app-django = "==2.1.0" @@ -112,8 +112,8 @@ xhtml2pdf = "==0.2.3" xlrd = "==1.1.0" xlwt = "==1.3.0" Babel = "==2.6.0" -django_celery_beat = "==1.2" -django_celery_results = "==1.0.1" +django_celery_beat = "==1.4" +django_celery_results = "==1.0.4" django-post_office = "==3.1.0" Django = "==2.1.5" et_xmlfile = "==1.0.1" @@ -124,9 +124,9 @@ PyJWT = "==1.5.3" PyPDF2 = "==1.26.0" PyYAML = "==3.12" unicef_attachments = "==0.4.2" -unicef_notification = "==0.2.0" +unicef_notification = "==0.2.1" unicef_restlib = "==0.3.8" -unicef_snapshot = "==0.2.1" +unicef_snapshot = "==0.2.2" Pillow = "==5.3.0" [requires] diff --git a/Pipfile.lock b/Pipfile.lock index 0f4de80857..5a21481ee8 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "5aafd5b2c4a08491b89fd1579636b92bafa12b60d7e03f3eeab249e6f980675f" + "sha256": "2cf2a142b8bfd0394dbb206bca41091782ec0a75a62679832406e8e974110296" }, "pipfile-spec": 6, "requires": { @@ -75,10 +75,10 @@ }, "carto": { "hashes": [ - "sha256:ff183bc1a07c142ef707b6c5fc759452aa446db619e73afad086f8058db4beed" + "sha256:9a54ece9d8f940bc3de3cb742e189c4ea681494d5ec251fec469319a39093dbc" ], "index": "pypi", - "version": "==1.3.1" + "version": "==1.4" }, "celery": { "hashes": [ @@ -88,6 +88,13 @@ "index": "pypi", "version": "==4.2.1" }, + "certifi": { + "hashes": [ + "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", + "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" + ], + "version": "==2018.11.29" + }, "cffi": { "hashes": [ "sha256:151b7eefd035c56b2b2e1eb9963c90c6302dc15fbd8c1c0a83a163ff2c7d7743", @@ -126,6 +133,13 @@ "index": "pypi", "version": "==1.11.5" }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, "coreapi": { "hashes": [ "sha256:46145fcc1f7017c076a2ef684969b641d18a2991051fddec9458ad3f78ffc1cb", @@ -215,11 +229,11 @@ }, "django-celery-beat": { "hashes": [ - "sha256:b25bc19a589e2851361408708a2aac43524608dd35b85d34a295f90f873a75e5", - "sha256:f3be14a02280d9977757acfcc8464d0bb5a24c3ed5fbd88b60ca63bb24fce632" + "sha256:3c2c22647455be5503aca7450db64ea53acacee2d0aef3d7ac49aa3ef3845724", + "sha256:bfc22dad2884524697e1fcdfa63c0555a65151a97902c3045cd2cf7bf63970e4" ], "index": "pypi", - "version": "==1.2" + "version": "==1.4" }, "django-celery-email": { "hashes": [ @@ -231,11 +245,11 @@ }, "django-celery-results": { "hashes": [ - "sha256:8bca2605eeff4418be7ce428a6958d64bee0f5bdf1f8e563fbc09a9e2f3d990f", - "sha256:dfa240fb535a1a2d01c9e605ad71629909318eae6b893c5009eafd7265fde10b" + "sha256:80292a68c8b705c788ff0bca9cacc5a431a4de39d7ff49e2ca8277b700d3d616", + "sha256:89ae9e32076efc65bcba31bc729870da9b230c63af22b673b79170c4a98039b1" ], "index": "pypi", - "version": "==1.0.1" + "version": "==1.0.4" }, "django-contrib-comments": { "hashes": [ @@ -270,11 +284,11 @@ }, "django-filter": { "hashes": [ - "sha256:6f4e4bc1a11151178520567b50320e5c32f8edb552139d93ea3e30613b886f56", - "sha256:86c3925020c27d072cdae7b828aaa5d165c2032a629abbe3c3a1be1edae61c58" + "sha256:3dafb7d2810790498895c22a1f31b2375795910680ac9c1432821cbedb1e176d", + "sha256:a3014de317bef0cd43075a0f08dfa1d319a7ccc5733c3901fb860da70b0dda68" ], "index": "pypi", - "version": "==2.0" + "version": "==2.1" }, "django-fsm": { "hashes": [ @@ -286,11 +300,11 @@ }, "django-import-export": { "hashes": [ - "sha256:51823434e06721725e0e51b8da424b3c0e915c93cbb65b607464e3d9613f200e", - "sha256:54d0c9a0e0b0513c9db7ea47c41e6499ffe576e13f724d37003e18d974f5c3b5" + "sha256:830824f79aae39e4212bb03aabdd83dc57931420557b757981cf6add8d07e611", + "sha256:99fae7d963af4ade97af9237a843f250312421fa4ee350a3b735fcc5684c3fb5" ], "index": "pypi", - "version": "==1.1" + "version": "==1.2" }, "django-js-asset": { "hashes": [ @@ -393,11 +407,11 @@ }, "djangorestframework": { "hashes": [ - "sha256:607865b0bb1598b153793892101d881466bd5a991de12bd6229abb18b1c86136", - "sha256:63f76cbe1e7d12b94c357d7e54401103b2e52aef0f7c1650d6c820ad708776e5" + "sha256:79c6efbb2514bc50cf25906d7c0a5cfead714c7af667ff4bd110312cd380ae66", + "sha256:a4138613b67e3a223be6c97f53b13d759c5b90d2b433bad670b8ebf95402075f" ], "index": "pypi", - "version": "==3.9.0" + "version": "==3.9.1" }, "djangorestframework-csv": { "hashes": [ @@ -432,11 +446,11 @@ }, "djangorestframework-xml": { "hashes": [ - "sha256:caea8e446298b7fe1eb9a79306f35554db7531c2e637734d32de3cf99afbdc5a", - "sha256:f7d5efc26eabbca73db0ff0f0c15b59ca08e36660c02f96563a0d937321f519f" + "sha256:d8118580b6c0e94a6b908a78c8d842e9f349901dfff43d91adc2d73a54f4ba59", + "sha256:d85d5744e75fe01ea2af667b15f6aa7df97c710516477ba493558da8432f6b0f" ], "index": "pypi", - "version": "==1.3" + "version": "==1.4" }, "drf-nested-routers": { "hashes": [ @@ -702,39 +716,39 @@ }, "psycopg2-binary": { "hashes": [ - "sha256:04afb59bbbd2eab3148e6816beddc74348078b8c02a1113ea7f7822f5be4afe3", - "sha256:098b18f4d8857a8f9b206d1dc54db56c2255d5d26458917e7bcad61ebfe4338f", - "sha256:0bf855d4a7083e20ead961fda4923887094eaeace0ab2d76eb4aa300f4bbf5bd", - "sha256:197dda3ffd02057820be83fe4d84529ea70bf39a9a4daee1d20ffc74eb3d042e", - "sha256:278ef63afb4b3d842b4609f2c05ffbfb76795cf6a184deeb8707cd5ed3c981a5", - "sha256:3cbf8c4fc8f22f0817220891cf405831559f4d4c12c4f73913730a2ea6c47a47", - "sha256:4305aed922c4d9d6163ab3a41d80b5a1cfab54917467da8168552c42cad84d32", - "sha256:47ee296f704fb8b2a616dec691cdcfd5fa0f11943955e88faa98cbd1dc3b3e3d", - "sha256:4a0e38cb30457e70580903367161173d4a7d1381eb2f2cfe4e69b7806623f484", - "sha256:4d6c294c6638a71cafb82a37f182f24321f1163b08b5d5ca076e11fe838a3086", - "sha256:4f3233c366500730f839f92833194fd8f9a5c4529c8cd8040aa162c3740de8e5", - "sha256:5221f5a3f4ca2ddf0d58e8b8a32ca50948be9a43351fda797eb4e72d7a7aa34d", - "sha256:5c6ca0b507540a11eaf9e77dee4f07c131c2ec80ca0cffa146671bf690bc1c02", - "sha256:789bd89d71d704db2b3d5e67d6d518b158985d791d3b2dec5ab85457cfc9677b", - "sha256:7b94d29239efeaa6a967f3b5971bd0518d2a24edd1511edbf4a2c8b815220d07", - "sha256:89bc65ef3301c74cf32db25334421ea6adbe8f65601ea45dcaaf095abed910bb", - "sha256:89d6d3a549f405c20c9ae4dc94d7ed2de2fa77427a470674490a622070732e62", - "sha256:97521704ac7127d7d8ba22877da3c7bf4a40366587d238ec679ff38e33177498", - "sha256:a395b62d5f44ff6f633231abe568e2203b8fabf9797cd6386aa92497df912d9a", - "sha256:a6d32c37f714c3f34158f3fa659f3a8f2658d5f53c4297d45579b9677cc4d852", - "sha256:a89ee5c26f72f2d0d74b991ce49e42ddeb4ac0dc2d8c06a0f2770a1ab48f4fe0", - "sha256:b4c8b0ef3608e59317bfc501df84a61e48b5445d45f24d0391a24802de5f2d84", - "sha256:b5fcf07140219a1f71e18486b8dc28e2e1b76a441c19374805c617aa6d9a9d55", - "sha256:b86f527f00956ecebad6ab3bb30e3a75fedf1160a8716978dd8ce7adddedd86f", - "sha256:be4c4aa22ba22f70de36c98b06480e2f1697972d49eb20d525f400d204a6d272", - "sha256:c2ac7aa1a144d4e0e613ac7286dae85671e99fe7a1353954d4905629c36b811c", - "sha256:de26ef4787b5e778e8223913a3e50368b44e7480f83c76df1f51d23bd21cea16", - "sha256:e70ebcfc5372dc7b699c0110454fc4263967f30c55454397e5769eb72c0eb0ce", - "sha256:eadbd32b6bc48b67b0457fccc94c86f7ccc8178ab839f684eb285bb592dc143e", - "sha256:ecbc6dfff6db06b8b72ae8a2f25ff20fbdcb83cb543811a08f7cb555042aa729" - ], - "index": "pypi", - "version": "==2.7.5" + "sha256:19a2d1f3567b30f6c2bb3baea23f74f69d51f0c06c2e2082d0d9c28b0733a4c2", + "sha256:2b69cf4b0fa2716fd977aa4e1fd39af6110eb47b2bb30b4e5a469d8fbecfc102", + "sha256:2e952fa17ba48cbc2dc063ddeec37d7dc4ea0ef7db0ac1eda8906365a8543f31", + "sha256:348b49dd737ff74cfb5e663e18cb069b44c64f77ec0523b5794efafbfa7df0b8", + "sha256:3d72a5fdc5f00ca85160915eb9a973cf9a0ab8148f6eda40708bf672c55ac1d1", + "sha256:4957452f7868f43f32c090dadb4188e9c74a4687323c87a882e943c2bd4780c3", + "sha256:5138cec2ee1e53a671e11cc519505eb08aaaaf390c508f25b09605763d48de4b", + "sha256:587098ca4fc46c95736459d171102336af12f0d415b3b865972a79c03f06259f", + "sha256:5b79368bcdb1da4a05f931b62760bea0955ee2c81531d8e84625df2defd3f709", + "sha256:5cf43807392247d9bc99737160da32d3fa619e0bfd85ba24d1c78db205f472a4", + "sha256:676d1a80b1eebc0cacae8dd09b2fde24213173bf65650d22b038c5ed4039f392", + "sha256:6b0211ecda389101a7d1d3df2eba0cf7ffbdd2480ca6f1d2257c7bd739e84110", + "sha256:79cde4660de6f0bb523c229763bd8ad9a93ac6760b72c369cf1213955c430934", + "sha256:7aba9786ac32c2a6d5fb446002ed936b47d5e1f10c466ef7e48f66eb9f9ebe3b", + "sha256:7c8159352244e11bdd422226aa17651110b600d175220c451a9acf795e7414e0", + "sha256:945f2eedf4fc6b2432697eb90bb98cc467de5147869e57405bfc31fa0b824741", + "sha256:96b4e902cde37a7fc6ab306b3ac089a3949e6ce3d824eeca5b19dc0bedb9f6e2", + "sha256:9a7bccb1212e63f309eb9fab47b6eaef796f59850f169a25695b248ca1bf681b", + "sha256:a3bfcac727538ec11af304b5eccadbac952d4cca1a551a29b8fe554e3ad535dc", + "sha256:b19e9f1b85c5d6136f5a0549abdc55dcbd63aba18b4f10d0d063eb65ef2c68b4", + "sha256:b664011bb14ca1f2287c17185e222f2098f7b4c857961dbcf9badb28786dbbf4", + "sha256:bde7959ef012b628868d69c474ec4920252656d0800835ed999ba5e4f57e3e2e", + "sha256:cb095a0657d792c8de9f7c9a0452385a309dfb1bbbb3357d6b1e216353ade6ca", + "sha256:d16d42a1b9772152c1fe606f679b2316551f7e1a1ce273e7f808e82a136cdb3d", + "sha256:d444b1545430ffc1e7a24ce5a9be122ccd3b135a7b7e695c5862c5aff0b11159", + "sha256:d93ccc7bf409ec0a23f2ac70977507e0b8a8d8c54e5ee46109af2f0ec9e411f3", + "sha256:df6444f952ca849016902662e1a47abf4fa0678d75f92fd9dd27f20525f809cd", + "sha256:e63850d8c52ba2b502662bf3c02603175c2397a9acc756090e444ce49508d41e", + "sha256:ec43358c105794bc2b6fd34c68d27f92bea7102393c01889e93f4b6a70975728", + "sha256:f4c6926d9c03dadce7a3b378b40d2fea912c1344ef9b29869f984fb3d2a2420b" + ], + "index": "pypi", + "version": "==2.7.7" }, "pycparser": { "hashes": [ @@ -813,11 +827,11 @@ }, "raven": { "hashes": [ - "sha256:3fd787d19ebb49919268f06f19310e8112d619ef364f7989246fc8753d469888", - "sha256:95f44f3ea2c1b176d5450df4becdb96c15bf2632888f9ab193e9dd22300ce46a" + "sha256:3fa6de6efa2493a7c827472e984ce9b020797d0da16f1db67197bcc23c8fae54", + "sha256:44a13f87670836e153951af9a3c80405d36b43097db869a36e92809673692ce4" ], "index": "pypi", - "version": "==6.9" + "version": "==6.10" }, "redis": { "hashes": [ @@ -863,11 +877,11 @@ }, "requests": { "hashes": [ - "sha256:545c4855cd9d7c12671444326337013766f4eea6068c3f0307fb2dc2696d580e", - "sha256:5acf980358283faba0b897c73959cecf8b841205bb4b2ad3ef545f46eae1a133" + "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", + "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" ], "index": "pypi", - "version": "==2.11.1" + "version": "==2.21.0" }, "requests-oauthlib": { "hashes": [ @@ -990,10 +1004,10 @@ }, "unicef-notification": { "hashes": [ - "sha256:06e8971eecae5a86a67aed75be4925aaddc9fac1ea82de02190a9b7a5c59e233" + "sha256:a11cc3faadb75421f7b7e0f76d6696e604e8c1872411c4066833b6413932518f" ], "index": "pypi", - "version": "==0.2.0" + "version": "==0.2.1" }, "unicef-restlib": { "hashes": [ @@ -1004,10 +1018,10 @@ }, "unicef-snapshot": { "hashes": [ - "sha256:7083ea95b7794b716eb3e528dc01a4ace7a5f0ab5316f5e4cb2be2cf4ef67d72" + "sha256:a270ae16d4507f6b25192c02bbababaeebd6323233160d56a921de517514110b" ], "index": "pypi", - "version": "==0.2.1" + "version": "==0.2.2" }, "unicodecsv": { "hashes": [ @@ -1025,6 +1039,13 @@ "index": "pypi", "version": "==3.0.0" }, + "urllib3": { + "hashes": [ + "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", + "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" + ], + "version": "==1.24.1" + }, "vine": { "hashes": [ "sha256:52116d59bc45392af9fdd3b75ed98ae48a93e822cee21e5fda249105c59a7a72", @@ -1156,11 +1177,11 @@ }, "djangorestframework": { "hashes": [ - "sha256:607865b0bb1598b153793892101d881466bd5a991de12bd6229abb18b1c86136", - "sha256:63f76cbe1e7d12b94c357d7e54401103b2e52aef0f7c1650d6c820ad708776e5" + "sha256:79c6efbb2514bc50cf25906d7c0a5cfead714c7af667ff4bd110312cd380ae66", + "sha256:a4138613b67e3a223be6c97f53b13d759c5b90d2b433bad670b8ebf95402075f" ], "index": "pypi", - "version": "==3.9.0" + "version": "==3.9.1" }, "docutils": { "hashes": [ @@ -1449,11 +1470,11 @@ }, "requests": { "hashes": [ - "sha256:545c4855cd9d7c12671444326337013766f4eea6068c3f0307fb2dc2696d580e", - "sha256:5acf980358283faba0b897c73959cecf8b841205bb4b2ad3ef545f46eae1a133" + "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", + "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" ], "index": "pypi", - "version": "==2.11.1" + "version": "==2.21.0" }, "responses": { "hashes": [ From 0388a6fb8b78f8959f244c8a43779cd62a301184 Mon Sep 17 00:00:00 2001 From: Greg Reinbach Date: Mon, 11 Feb 2019 05:19:38 -0500 Subject: [PATCH 40/72] Fix task and command tests for draft notifications --- src/etools/applications/partners/tests/test_commands.py | 4 +++- src/etools/applications/partners/tests/test_tasks.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/etools/applications/partners/tests/test_commands.py b/src/etools/applications/partners/tests/test_commands.py index 8fa20b37a3..568d9a04aa 100644 --- a/src/etools/applications/partners/tests/test_commands.py +++ b/src/etools/applications/partners/tests/test_commands.py @@ -363,7 +363,9 @@ def setUpTestData(cls): def test_command(self): send_path = "etools.applications.partners.utils.send_notification_with_template" - InterventionFactory(status=Intervention.DRAFT) + intervention = InterventionFactory(status=Intervention.DRAFT) + intervention.created = datetime.datetime(2018, 1, 1, 12, 55, 12, 12345) + intervention.save() mock_send = Mock() with patch(send_path, mock_send): call_command("send_intervention_draft_notification") diff --git a/src/etools/applications/partners/tests/test_tasks.py b/src/etools/applications/partners/tests/test_tasks.py index 7c832342cc..7ee086eb7c 100644 --- a/src/etools/applications/partners/tests/test_tasks.py +++ b/src/etools/applications/partners/tests/test_tasks.py @@ -1033,7 +1033,9 @@ def setUpTestData(cls): def test_task(self): send_path = "etools.applications.partners.utils.send_notification_with_template" - InterventionFactory(status=Intervention.DRAFT) + intervention = InterventionFactory(status=Intervention.DRAFT) + intervention.created = datetime.datetime(2018, 1, 1, 12, 55, 12, 12345) + intervention.save() mock_send = mock.Mock() with mock.patch(send_path, mock_send): etools.applications.partners.tasks.check_intervention_draft_status() From 128ac383baa82119e12e0e27c2e403291ae071c8 Mon Sep 17 00:00:00 2001 From: denes csaba Date: Tue, 12 Feb 2019 20:39:44 +0200 Subject: [PATCH 41/72] fixing tests & trying to fix PR --- .../applications/partners/permissions.py | 4 +- .../partners/serializers/interventions_v2.py | 17 ++++-- .../partners/tests/test_api_interventions.py | 56 ++++++++++++++++++- 3 files changed, 69 insertions(+), 8 deletions(-) diff --git a/src/etools/applications/partners/permissions.py b/src/etools/applications/partners/permissions.py index 31594270b2..2323253234 100644 --- a/src/etools/applications/partners/permissions.py +++ b/src/etools/applications/partners/permissions.py @@ -89,7 +89,7 @@ def get_permissions(self): class InterventionPermissions(PMPPermissions): MODEL_NAME = 'partners.Intervention' - EXTRA_FIELDS = ['sections_present', 'reporting_requirements'] + EXTRA_FIELDS = ['sections_present'] def __init__(self, **kwargs): """ @@ -128,7 +128,7 @@ def prp_server_on(): 'prp_server_on': prp_server_on(), 'user_adds_amendment+prp_mode_on': user_added_amendment(self.instance) and not prp_mode_off(), 'termination_doc_attached': self.instance.termination_doc_attachment.exists(), - 'not_ended': self.instance.end >= datetime.datetime.now().date() + 'not_ended': self.instance.end >= datetime.datetime.now().date() if self.instance.end else False } diff --git a/src/etools/applications/partners/serializers/interventions_v2.py b/src/etools/applications/partners/serializers/interventions_v2.py index 63edd29147..fa4679d591 100644 --- a/src/etools/applications/partners/serializers/interventions_v2.py +++ b/src/etools/applications/partners/serializers/interventions_v2.py @@ -1,4 +1,4 @@ -from datetime import date +from datetime import datetime, date from operator import itemgetter from django.db import transaction @@ -825,10 +825,17 @@ def validate(self, data): self.intervention = self.context["intervention"] if self.intervention.status != Intervention.DRAFT: - if not self.intervention.in_amendment and not self.intervention.termination_doc_attachment.exists(): - raise serializers.ValidationError( - _("Changes not allowed when PD not in amendment state.") - ) + if self.intervention.status == Intervention.TERMINATED: + ended = self.intervention.end < datetime.now().date() if self.intervention.end else False + if ended: + raise serializers.ValidationError( + _("Changes not allowed when PD is terminated.") + ) + else: + if not self.intervention.in_amendment and not self.intervention.termination_doc_attachment.exists(): + raise serializers.ValidationError( + _("Changes not allowed when PD not in amendment state.") + ) if not self.intervention.start: raise serializers.ValidationError( diff --git a/src/etools/applications/partners/tests/test_api_interventions.py b/src/etools/applications/partners/tests/test_api_interventions.py index af2d31abb0..39f1029af6 100644 --- a/src/etools/applications/partners/tests/test_api_interventions.py +++ b/src/etools/applications/partners/tests/test_api_interventions.py @@ -23,6 +23,7 @@ from etools.applications.EquiTrack.tests.cases import BaseTenantTestCase from etools.applications.EquiTrack.tests.mixins import URLAssertionMixin from etools.applications.partners.models import Intervention, InterventionAmendment, InterventionResultLink +from etools.applications.partners.permissions import InterventionPermissions from etools.applications.partners.tests.factories import ( AgreementFactory, FileTypeFactory, @@ -110,7 +111,7 @@ class TestInterventionsAPI(BaseTenantTestCase): 'signed': [], 'active': [''] } - ALL_FIELDS = get_all_field_names(Intervention) + ['sections_present'] + ALL_FIELDS = get_all_field_names(Intervention) + InterventionPermissions.EXTRA_FIELDS def setUp(self): setup_intervention_test_data(self) @@ -2087,6 +2088,59 @@ def test_post_invalid_not_amendment_state(self): ]} ) + def test_validation_terminated_pd_requirements_editable_qpr(self): + intervention = InterventionFactory( + start=datetime.date(2001, 1, 1), + status=Intervention.TERMINATED + ) + print('intervention.end', intervention.end) + result_link = InterventionResultLinkFactory(intervention=intervention) + lower_result = LowerResultFactory(result_link=result_link) + AppliedIndicatorFactory(lower_result=lower_result) + + response = self.forced_auth_req( + "post", + self._get_url(ReportingRequirement.TYPE_QPR, intervention=intervention), + user=self.unicef_staff, + data={ + "report_type": ReportingRequirement.TYPE_QPR, + "reporting_requirements": [{ + "start_date": datetime.date(2001, 2, 1), + "end_date": datetime.date(2001, 3, 31), + "due_date": datetime.date(2001, 4, 15), + }] + } + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + print('response.data', response.data) + + + def test_validation_terminated_pd_requirements_editable_hr(self): + intervention = InterventionFactory( + start=datetime.date(2001, 1, 1), + status=Intervention.TERMINATED + ) + print('intervention.end', intervention.end) + result_link = InterventionResultLinkFactory(intervention=intervention) + lower_result = LowerResultFactory(result_link=result_link) + AppliedIndicatorFactory(lower_result=lower_result, is_high_frequency=True) + + response = self.forced_auth_req( + "post", + self._get_url(ReportingRequirement.TYPE_HR, intervention=intervention), + user=self.unicef_staff, + data={ + "report_type": ReportingRequirement.TYPE_HR, + "reporting_requirements": [{ + "start_date": datetime.date(2001, 2, 1), + "end_date": datetime.date(2001, 3, 31), + "due_date": datetime.date(2001, 4, 15), + }] + } + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + print('response.data', response.data) + def test_patch_invalid(self): for report_type, _ in ReportingRequirement.TYPE_CHOICES: response = self.forced_auth_req( From 5085e1526abc3bf65edd835d342a25471190de6b Mon Sep 17 00:00:00 2001 From: denes csaba Date: Tue, 12 Feb 2019 20:49:00 +0200 Subject: [PATCH 42/72] fixes --- .../partners/serializers/interventions_v2.py | 2 +- .../partners/tests/test_api_interventions.py | 71 +++++++++++++++++-- 2 files changed, 66 insertions(+), 7 deletions(-) diff --git a/src/etools/applications/partners/serializers/interventions_v2.py b/src/etools/applications/partners/serializers/interventions_v2.py index fa4679d591..2157a87546 100644 --- a/src/etools/applications/partners/serializers/interventions_v2.py +++ b/src/etools/applications/partners/serializers/interventions_v2.py @@ -826,7 +826,7 @@ def validate(self, data): if self.intervention.status != Intervention.DRAFT: if self.intervention.status == Intervention.TERMINATED: - ended = self.intervention.end < datetime.now().date() if self.intervention.end else False + ended = self.intervention.end < datetime.now().date() if self.intervention.end else True if ended: raise serializers.ValidationError( _("Changes not allowed when PD is terminated.") diff --git a/src/etools/applications/partners/tests/test_api_interventions.py b/src/etools/applications/partners/tests/test_api_interventions.py index 39f1029af6..add4d85729 100644 --- a/src/etools/applications/partners/tests/test_api_interventions.py +++ b/src/etools/applications/partners/tests/test_api_interventions.py @@ -2088,12 +2088,12 @@ def test_post_invalid_not_amendment_state(self): ]} ) - def test_validation_terminated_pd_requirements_editable_qpr(self): + def test_requirements_pd_terminated_and_ended_qpr(self): intervention = InterventionFactory( start=datetime.date(2001, 1, 1), + end=datetime.date(2002, 1, 1), status=Intervention.TERMINATED ) - print('intervention.end', intervention.end) result_link = InterventionResultLinkFactory(intervention=intervention) lower_result = LowerResultFactory(result_link=result_link) AppliedIndicatorFactory(lower_result=lower_result) @@ -2112,15 +2112,44 @@ def test_validation_terminated_pd_requirements_editable_qpr(self): } ) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - print('response.data', response.data) + self.assertEqual( + response.data, + {"non_field_errors": [ + "Changes not allowed when PD is terminated." + ]} + ) + + def test_requirements_pd_terminated_but_not_ended_qpr(self): + intervention = InterventionFactory( + start=datetime.date(2001, 1, 1), + end=datetime.date.today() + datetime.timedelta(days=2), + status=Intervention.TERMINATED + ) + result_link = InterventionResultLinkFactory(intervention=intervention) + lower_result = LowerResultFactory(result_link=result_link) + AppliedIndicatorFactory(lower_result=lower_result) + response = self.forced_auth_req( + "post", + self._get_url(ReportingRequirement.TYPE_QPR, intervention=intervention), + user=self.unicef_staff, + data={ + "report_type": ReportingRequirement.TYPE_QPR, + "reporting_requirements": [{ + "start_date": datetime.date(2001, 2, 1), + "end_date": datetime.date(2001, 3, 31), + "due_date": datetime.date(2001, 4, 15), + }] + } + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) - def test_validation_terminated_pd_requirements_editable_hr(self): + def test_requirements_pd_terminated_and_ended_hr(self): intervention = InterventionFactory( start=datetime.date(2001, 1, 1), + end=datetime.date(2002, 1, 1), status=Intervention.TERMINATED ) - print('intervention.end', intervention.end) result_link = InterventionResultLinkFactory(intervention=intervention) lower_result = LowerResultFactory(result_link=result_link) AppliedIndicatorFactory(lower_result=lower_result, is_high_frequency=True) @@ -2139,7 +2168,37 @@ def test_validation_terminated_pd_requirements_editable_hr(self): } ) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - print('response.data', response.data) + self.assertEqual( + response.data, + {"non_field_errors": [ + "Changes not allowed when PD is terminated." + ]} + ) + + def test_requirements_pd_terminated_but_not_ended_hr(self): + intervention = InterventionFactory( + start=datetime.date(2001, 1, 1), + end=datetime.date.today() + datetime.timedelta(days=2), + status=Intervention.TERMINATED + ) + result_link = InterventionResultLinkFactory(intervention=intervention) + lower_result = LowerResultFactory(result_link=result_link) + AppliedIndicatorFactory(lower_result=lower_result, is_high_frequency=True) + + response = self.forced_auth_req( + "post", + self._get_url(ReportingRequirement.TYPE_HR, intervention=intervention), + user=self.unicef_staff, + data={ + "report_type": ReportingRequirement.TYPE_HR, + "reporting_requirements": [{ + "start_date": datetime.date(2001, 2, 1), + "end_date": datetime.date(2001, 3, 31), + "due_date": datetime.date(2001, 4, 15), + }] + } + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) def test_patch_invalid(self): for report_type, _ in ReportingRequirement.TYPE_CHOICES: From 05ac503693ec7039e47cefaf78e09f933527ec0a Mon Sep 17 00:00:00 2001 From: Greg Reinbach Date: Thu, 14 Feb 2019 06:46:18 -0500 Subject: [PATCH 43/72] Fix intervention admin handling of intervention attachments, prc review and pd signed documents --- src/etools/applications/partners/admin.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/etools/applications/partners/admin.py b/src/etools/applications/partners/admin.py index d829c76aca..7e31c40d52 100644 --- a/src/etools/applications/partners/admin.py +++ b/src/etools/applications/partners/admin.py @@ -200,10 +200,18 @@ class InterventionResultsLinkAdmin(admin.ModelAdmin): class PRCReviewAttachmentInline(AttachmentSingleInline): verbose_name_plural = _("Review Document by PRC") + def get_queryset(self, request): + qs = super().get_queryset(request) + return qs.filter(code='partners_intervention_prc_review') + class SignedPDAttachmentInline(AttachmentSingleInline): verbose_name_plural = _("Signed PD Document") + def get_queryset(self, request): + qs = super().get_queryset(request) + return qs.filter(code='partners_intervention_signed_pd') + class InterventionAdmin(CountryUsersAdminMixin, HiddenPartnerMixin, SnapshotModelAdmin): model = Intervention @@ -236,6 +244,8 @@ class InterventionAdmin(CountryUsersAdminMixin, HiddenPartnerMixin, SnapshotMode readonly_fields = ( 'total_budget', 'attachments_link', + 'prc_review_document', + 'signed_pd_document', ) filter_horizontal = ( 'sections', @@ -321,6 +331,7 @@ def save_formset(self, request, form, formset, change): defaults={ "file": instance.attachment, "uploaded_by": request.user, + "code": instance.attachment_file.core_filters["code"], } ) From 764f6ceb6bf89e42ab15d444a249a0690988f7f8 Mon Sep 17 00:00:00 2001 From: Greg Reinbach Date: Thu, 14 Feb 2019 10:01:21 -0500 Subject: [PATCH 44/72] Clean up attachment inlines in partners admin --- src/etools/applications/partners/admin.py | 96 +++++++++++++---------- 1 file changed, 56 insertions(+), 40 deletions(-) diff --git a/src/etools/applications/partners/admin.py b/src/etools/applications/partners/admin.py index 7e31c40d52..30c9f92eee 100644 --- a/src/etools/applications/partners/admin.py +++ b/src/etools/applications/partners/admin.py @@ -1,3 +1,4 @@ +from django import forms from django.contrib import admin from django.contrib.contenttypes.models import ContentType from django.db import models @@ -34,15 +35,43 @@ PlannedEngagement, ) +class AttachmentSingleInline(AttachmentSingleInline): + def get_queryset(self, request): + qs = super().get_queryset(request) + return qs.filter(code=self.code) -class InterventionAmendmentAttachmentFileInline(AttachmentSingleInline): - verbose_name_plural = _("Attachment") + def get_formset(self, request, obj=None, **kwargs): + formset = super().get_formset(request, obj, **kwargs) + formset.code = self.code + return formset + + def has_add_permission(self, request): + return True + + +class AttachmentInlineAdminMixin: + def save_formset(self, request, form, formset, change): + instances = formset.save() + for instance in instances: + instance.code = formset.code + instance.save() + + +class InterventionAmendmentSignedInline(AttachmentSingleInline): + verbose_name_plural = _("Signed Attachment") + code = 'partners_intervention_amendment_signed' -class InterventionAmendmentsAdmin(admin.ModelAdmin): +class InterventionAmendmentPRCReviewInline(AttachmentSingleInline): + verbose_name_plural = _("PRC Reviewed Attachment") + code = 'partners_intervention_amendment_internal_prc_review' + + +class InterventionAmendmentsAdmin(AttachmentInlineAdminMixin, admin.ModelAdmin): model = InterventionAmendment readonly_fields = [ 'amendment_number', + 'signed_amendment', ] list_display = ( 'intervention', @@ -55,7 +84,8 @@ class InterventionAmendmentsAdmin(admin.ModelAdmin): 'types' ) inlines = [ - InterventionAmendmentAttachmentFileInline, + InterventionAmendmentSignedInline, + InterventionAmendmentPRCReviewInline, ] def has_delete_permission(self, request, obj=None): @@ -136,9 +166,10 @@ class InterventionPlannedVisitsInline(admin.TabularInline): class AttachmentFileInline(AttachmentSingleInline): verbose_name_plural = _("Attachment") + code = 'partners_intervention_attachment' -class InterventionAttachmentAdmin(admin.ModelAdmin): +class InterventionAttachmentAdmin(AttachmentInlineAdminMixin, admin.ModelAdmin): model = InterventionAttachment list_display = ( 'intervention', @@ -156,13 +187,6 @@ class InterventionAttachmentAdmin(admin.ModelAdmin): AttachmentFileInline, ] - def attachment_file(self, obj): - content_type = ContentType.objects.get_for_model(obj) - return Attachment.objects.get( - object_id=obj.pk, - content_type=content_type - ) - class InterventionAttachmentsInline(admin.TabularInline): model = InterventionAttachment @@ -199,21 +223,20 @@ class InterventionResultsLinkAdmin(admin.ModelAdmin): class PRCReviewAttachmentInline(AttachmentSingleInline): verbose_name_plural = _("Review Document by PRC") - - def get_queryset(self, request): - qs = super().get_queryset(request) - return qs.filter(code='partners_intervention_prc_review') + code = 'partners_intervention_prc_review' class SignedPDAttachmentInline(AttachmentSingleInline): verbose_name_plural = _("Signed PD Document") + code = 'partners_intervention_signed_pd' - def get_queryset(self, request): - qs = super().get_queryset(request) - return qs.filter(code='partners_intervention_signed_pd') - -class InterventionAdmin(CountryUsersAdminMixin, HiddenPartnerMixin, SnapshotModelAdmin): +class InterventionAdmin( + AttachmentInlineAdminMixin, + CountryUsersAdminMixin, + HiddenPartnerMixin, + SnapshotModelAdmin +): model = Intervention date_hierarchy = 'start' @@ -319,28 +342,13 @@ def attachments_link(self, obj): attachments_link.short_description = 'attachments' - def save_formset(self, request, form, formset, change): - instances = formset.save() - for instance in instances: - if isinstance(instance, InterventionAttachment): - # update attachment file data - content_type = ContentType.objects.get_for_model(instance) - Attachment.objects.update_or_create( - object_id=instance.pk, - content_type=content_type, - defaults={ - "file": instance.attachment, - "uploaded_by": request.user, - "code": instance.attachment_file.core_filters["code"], - } - ) - class AssessmentReportInline(AttachmentSingleInline): verbose_name_plural = _("Report") + code = 'partners_assessment_report' -class AssessmentAdmin(admin.ModelAdmin): +class AssessmentAdmin(AttachmentInlineAdminMixin, admin.ModelAdmin): model = Assessment fields = ( 'partner', @@ -557,9 +565,10 @@ class PlannedEngagementAdmin(admin.ModelAdmin): class SignedAmendmentInline(AttachmentSingleInline): verbose_name_plural = _("Signed Amendment") + code = 'partners_agreement_amendment' -class AgreementAmendmentAdmin(admin.ModelAdmin): +class AgreementAmendmentAdmin(AttachmentInlineAdminMixin, admin.ModelAdmin): model = AgreementAmendment fields = ( 'agreement', @@ -597,9 +606,16 @@ def get_max_num(self, request, obj=None, **kwargs): class AgreementAttachmentInline(AttachmentSingleInline): verbose_name_plural = _('Attachment') + code = 'partners_agreement' -class AgreementAdmin(ExportMixin, HiddenPartnerMixin, CountryUsersAdminMixin, SnapshotModelAdmin): +class AgreementAdmin( + AttachmentInlineAdminMixin, + ExportMixin, + HiddenPartnerMixin, + CountryUsersAdminMixin, + SnapshotModelAdmin, +): list_filter = ( 'partner', From b438c5e8e733fd685dfa05928f5d4386cad3d043 Mon Sep 17 00:00:00 2001 From: Greg Reinbach Date: Thu, 14 Feb 2019 10:19:35 -0500 Subject: [PATCH 45/72] flake8 cleanup --- src/etools/applications/partners/admin.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/etools/applications/partners/admin.py b/src/etools/applications/partners/admin.py index 30c9f92eee..0590030d3b 100644 --- a/src/etools/applications/partners/admin.py +++ b/src/etools/applications/partners/admin.py @@ -1,6 +1,4 @@ -from django import forms from django.contrib import admin -from django.contrib.contenttypes.models import ContentType from django.db import models from django.forms import SelectMultiple from django.urls import reverse @@ -9,7 +7,6 @@ from import_export.admin import ExportMixin from unicef_attachments.admin import AttachmentSingleInline -from unicef_attachments.models import Attachment from unicef_snapshot.admin import ActivityInline, SnapshotModelAdmin from etools.applications.partners.exports import PartnerExport @@ -35,6 +32,7 @@ PlannedEngagement, ) + class AttachmentSingleInline(AttachmentSingleInline): def get_queryset(self, request): qs = super().get_queryset(request) From e701f986ff7f4f146198e5041575f7de20bb89ee Mon Sep 17 00:00:00 2001 From: Domenico DiNicola Date: Fri, 8 Feb 2019 16:40:10 -0500 Subject: [PATCH 46/72] get vision api from settings --- .../applications/EquiTrack/templatetags/etools.py | 6 ++++++ .../admin/users/country/change_form.html | 15 ++++++++------- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/etools/applications/EquiTrack/templatetags/etools.py b/src/etools/applications/EquiTrack/templatetags/etools.py index 2e8d5df301..a142e387a9 100644 --- a/src/etools/applications/EquiTrack/templatetags/etools.py +++ b/src/etools/applications/EquiTrack/templatetags/etools.py @@ -1,4 +1,5 @@ from django import template +from django.conf import settings from django.utils.safestring import mark_safe from etools import NAME, VERSION @@ -9,3 +10,8 @@ @register.simple_tag def etools_version(): return mark_safe('{}: v{}'.format(NAME, VERSION)) + + +@register.simple_tag +def vision_url(): + return settings.VISION_URL diff --git a/src/etools/applications/users/templates/admin/users/country/change_form.html b/src/etools/applications/users/templates/admin/users/country/change_form.html index cddcd03999..7a15cf6643 100644 --- a/src/etools/applications/users/templates/admin/users/country/change_form.html +++ b/src/etools/applications/users/templates/admin/users/country/change_form.html @@ -1,12 +1,13 @@ -{% extends "admin/change_form.html" %}{% load i18n %} +{% extends "admin/change_form.html" %}{% load i18n etools %} {% block object-tools-items %} {{ block.super }} -
  • {% trans "API Partner" %}
  • -
  • {% trans "API Programme" %}
  • -
  • {% trans "API RAM" %}
  • -
  • {% trans "API Fund Reservation" %}
  • -
  • {% trans "API Fund Commitment" %}
  • -
  • {% trans "API DCT" %}
  • + {% vision_url %} +
  • {% trans "API Partner" %}
  • +
  • {% trans "API Programme" %}
  • +
  • {% trans "API RAM" %}
  • +
  • {% trans "API Fund Reservation" %}
  • +
  • {% trans "API Fund Commitment" %}
  • +
  • {% trans "API DCT" %}
  • {% trans "Sync Partners" %}
  • {% trans "Sync Programme" %}
  • From 0437676754ccc2dcb802985ca3bfc9c9cdcba91a Mon Sep 17 00:00:00 2001 From: Domenico DiNicola Date: Thu, 14 Feb 2019 11:43:35 -0500 Subject: [PATCH 47/72] partial --- .../users/templates/admin/users/country/change_form.html | 1 - 1 file changed, 1 deletion(-) diff --git a/src/etools/applications/users/templates/admin/users/country/change_form.html b/src/etools/applications/users/templates/admin/users/country/change_form.html index 7a15cf6643..80b6948cf4 100644 --- a/src/etools/applications/users/templates/admin/users/country/change_form.html +++ b/src/etools/applications/users/templates/admin/users/country/change_form.html @@ -1,7 +1,6 @@ {% extends "admin/change_form.html" %}{% load i18n etools %} {% block object-tools-items %} {{ block.super }} - {% vision_url %}
  • {% trans "API Partner" %}
  • {% trans "API Programme" %}
  • {% trans "API RAM" %}
  • From e23fb81fe78b9eceb5d47c53cc035f87530f6605 Mon Sep 17 00:00:00 2001 From: Greg Reinbach Date: Fri, 15 Feb 2019 09:44:16 -0500 Subject: [PATCH 48/72] Add link attachment endpoints for audit engagement models --- .../audit/serializers/attachments.py | 59 ++++++++++++++ .../audit/tests/test_attachment_link_views.py | 35 ++++++++ .../applications/audit/tests/test_views.py | 24 ++++-- src/etools/applications/audit/urls.py | 64 +++++++++------ src/etools/applications/audit/views.py | 81 ++++++++++++++++++- 5 files changed, 228 insertions(+), 35 deletions(-) create mode 100644 src/etools/applications/audit/serializers/attachments.py create mode 100644 src/etools/applications/audit/tests/test_attachment_link_views.py diff --git a/src/etools/applications/audit/serializers/attachments.py b/src/etools/applications/audit/serializers/attachments.py new file mode 100644 index 0000000000..88bb9cb19e --- /dev/null +++ b/src/etools/applications/audit/serializers/attachments.py @@ -0,0 +1,59 @@ +from rest_framework import serializers +from unicef_attachments.models import AttachmentLink +from unicef_attachments.serializers import AttachmentLinkSerializer + +from etools.applications.attachments.utils import get_file_type, get_source + + +class ListAttachmentLinkSerializer(AttachmentLinkSerializer): + source = serializers.SerializerMethodField() + file_type = serializers.SerializerMethodField() + + class Meta(AttachmentLinkSerializer.Meta): + fields = ( + "id", + "attachment", + "filename", + "url", + "file_type", + "created", + "source", + ) + + def get_source(self, obj): + return get_source(obj.attachment) + + def get_file_type(self, obj): + return get_file_type(obj.attachment) + + +class BaseAttachmentLinkSerializer(serializers.Serializer): + def create(self, validated_data): + links = [] + for attachment in validated_data["attachments"]: + links.append(AttachmentLink.objects.create( + attachment=attachment["attachment"], + content_type=self.context["content_type"], + object_id=self.context["object_id"], + )) + return {"attachments": links} + + +class EngagementAttachmentLinkSerializer(BaseAttachmentLinkSerializer): + attachments = ListAttachmentLinkSerializer(many=True, allow_empty=False) + + +class SpotCheckAttachmentLinkSerializer(BaseAttachmentLinkSerializer): + attachments = ListAttachmentLinkSerializer(many=True, allow_empty=False) + + +class MicroAssessmentAttachmentLinkSerializer(BaseAttachmentLinkSerializer): + attachments = ListAttachmentLinkSerializer(many=True, allow_empty=False) + + +class AuditAttachmentLinkSerializer(BaseAttachmentLinkSerializer): + attachments = ListAttachmentLinkSerializer(many=True, allow_empty=False) + + +class SpecialAuditAttachmentLinkSerializer(BaseAttachmentLinkSerializer): + attachments = ListAttachmentLinkSerializer(many=True, allow_empty=False) diff --git a/src/etools/applications/audit/tests/test_attachment_link_views.py b/src/etools/applications/audit/tests/test_attachment_link_views.py new file mode 100644 index 0000000000..a4c1c42beb --- /dev/null +++ b/src/etools/applications/audit/tests/test_attachment_link_views.py @@ -0,0 +1,35 @@ +from django.urls import reverse + +from rest_framework import status +from unicef_attachments.models import AttachmentLink + +from etools.applications.attachments.tests.factories import AttachmentFactory +from etools.applications.audit.tests.factories import EngagementFactory, UserFactory +from etools.applications.EquiTrack.tests.cases import BaseTenantTestCase + + +class TestEngagementAttachmentsView(BaseTenantTestCase): + @classmethod + def setUpTestData(cls): + cls.user = UserFactory(unicef_user=True) + cls.engagement = EngagementFactory() + cls.attachment = AttachmentFactory(content_object=cls.engagement) + + def test_add(self): + links_qs = AttachmentLink.objects + self.assertEqual(links_qs.count(), 0) + create_response = self.forced_auth_req( + 'post', + reverse('audit:engagement-links', args=[self.engagement.pk]), + user=self.user, + data={'attachments': [{'attachment': self.attachment.pk}]} + ) + self.assertEqual(create_response.status_code, status.HTTP_201_CREATED) + + list_response = self.forced_auth_req( + 'get', + reverse('audit:engagement-links', args=[self.engagement.pk]), + user=self.user + ) + self.assertEqual(list_response.status_code, status.HTTP_200_OK) + self.assertEqual(links_qs.count(), 1) diff --git a/src/etools/applications/audit/tests/test_views.py b/src/etools/applications/audit/tests/test_views.py index 03ed2753c0..cbc7e91c50 100644 --- a/src/etools/applications/audit/tests/test_views.py +++ b/src/etools/applications/audit/tests/test_views.py @@ -9,17 +9,25 @@ from mock import Mock, patch from rest_framework import status -from etools.applications.action_points.tests.factories import ActionPointFactory, ActionPointCategoryFactory +from etools.applications.action_points.tests.factories import ActionPointCategoryFactory, ActionPointFactory from etools.applications.attachments.tests.factories import AttachmentFactory, AttachmentFileTypeFactory -from etools.applications.audit.models import Engagement, Risk, Auditor +from etools.applications.audit.models import Auditor, Engagement, Risk from etools.applications.audit.tests.base import AuditTestCaseMixin, EngagementTransitionsTestCaseMixin -from etools.applications.audit.tests.factories import (AuditFactory, AuditPartnerFactory, - EngagementFactory, MicroAssessmentFactory, - PurchaseOrderFactory, RiskBluePrintFactory, RiskCategoryFactory, - SpecialAuditFactory, SpotCheckFactory, UserFactory, - StaffSpotCheckFactory) -from etools.applications.EquiTrack.tests.cases import BaseTenantTestCase +from etools.applications.audit.tests.factories import ( + AuditFactory, + AuditPartnerFactory, + EngagementFactory, + MicroAssessmentFactory, + PurchaseOrderFactory, + RiskBluePrintFactory, + RiskCategoryFactory, + SpecialAuditFactory, + SpotCheckFactory, + StaffSpotCheckFactory, + UserFactory, +) from etools.applications.audit.tests.test_transitions import MATransitionsTestCaseMixin +from etools.applications.EquiTrack.tests.cases import BaseTenantTestCase from etools.applications.partners.models import PartnerType from etools.applications.reports.tests.factories import SectionFactory diff --git a/src/etools/applications/audit/urls.py b/src/etools/applications/audit/urls.py index 6c2214b01a..b67e0a3ab2 100644 --- a/src/etools/applications/audit/urls.py +++ b/src/etools/applications/audit/urls.py @@ -3,40 +3,27 @@ from rest_framework_nested import routers from unicef_restlib.routers import NestedComplexRouter -from etools.applications.audit.views import ( - AuditorFirmViewSet, - AuditorStaffMembersViewSet, - AuditViewSet, - EngagementActionPointViewSet, - EngagementAttachmentsViewSet, - EngagementViewSet, - MicroAssessmentViewSet, - PurchaseOrderViewSet, - ReportAttachmentsViewSet, - SpecialAuditViewSet, - SpotCheckViewSet, - StaffSpotCheckViewSet, -) +from etools.applications.audit import views root_api = routers.SimpleRouter() -root_api.register(r'audit-firms', AuditorFirmViewSet, base_name='audit-firms') -root_api.register(r'purchase-orders', PurchaseOrderViewSet, base_name='purchase-orders') -root_api.register(r'engagements', EngagementViewSet, base_name='engagements') -root_api.register(r'micro-assessments', MicroAssessmentViewSet, base_name='micro-assessments') -root_api.register(r'spot-checks', SpotCheckViewSet, base_name='spot-checks') -root_api.register(r'staff-spot-checks', StaffSpotCheckViewSet, base_name='staff-spot-checks') -root_api.register(r'audits', AuditViewSet, base_name='audits') -root_api.register(r'special-audits', SpecialAuditViewSet, base_name='special-audits') +root_api.register(r'audit-firms', views.AuditorFirmViewSet, base_name='audit-firms') +root_api.register(r'purchase-orders', views.PurchaseOrderViewSet, base_name='purchase-orders') +root_api.register(r'engagements', views.EngagementViewSet, base_name='engagements') +root_api.register(r'micro-assessments', views.MicroAssessmentViewSet, base_name='micro-assessments') +root_api.register(r'spot-checks', views.SpotCheckViewSet, base_name='spot-checks') +root_api.register(r'staff-spot-checks', views.StaffSpotCheckViewSet, base_name='staff-spot-checks') +root_api.register(r'audits', views.AuditViewSet, base_name='audits') +root_api.register(r'special-audits', views.SpecialAuditViewSet, base_name='special-audits') auditor_staffmember_api = NestedComplexRouter(root_api, r'audit-firms', lookup='auditor_firm') -auditor_staffmember_api.register(r'staff-members', AuditorStaffMembersViewSet, base_name='auditorstaffmembers') +auditor_staffmember_api.register(r'staff-members', views.AuditorStaffMembersViewSet, base_name='auditorstaffmembers') engagement_action_points_api = NestedComplexRouter(root_api, r'engagements', lookup='engagement') -engagement_action_points_api.register(r'action-points', EngagementActionPointViewSet, base_name='action-points') +engagement_action_points_api.register(r'action-points', views.EngagementActionPointViewSet, base_name='action-points') attachments_api = NestedComplexRouter(root_api, r'engagements') -attachments_api.register(r'engagement-attachments', EngagementAttachmentsViewSet, base_name='engagement-attachments') -attachments_api.register(r'report-attachments', ReportAttachmentsViewSet, base_name='report-attachments') +attachments_api.register(r'engagement-attachments', views.EngagementAttachmentsViewSet, base_name='engagement-attachments') +attachments_api.register(r'report-attachments', views.ReportAttachmentsViewSet, base_name='report-attachments') app_name = 'audit' urlpatterns = [ @@ -44,4 +31,29 @@ url(r'^', include(engagement_action_points_api.urls)), url(r'^', include(attachments_api.urls)), url(r'^', include(root_api.urls)), + url( + r'^engagement/(?P\d+)/links', + view=views.EngagementAttachmentLinksView.as_view(), + name='engagement-links' + ), + url( + r'^spot-check/(?P\d+)/links', + view=views.SpotCheckAttachmentLinksView.as_view(), + name='spot-check-links' + ), + url( + r'^micro-assessment/(?P\d+)/links', + view=views.MicroAssessmentAttachmentLinksView.as_view(), + name='micro-assessment-links' + ), + url( + r'^audit/(?P\d+)/links', + view=views.AuditAttachmentLinksView.as_view(), + name='audit-links' + ), + url( + r'^special-audit/(?P\d+)/links', + view=views.SpecialAuditAttachmentLinksView.as_view(), + name='special-audit-links' + ), ] diff --git a/src/etools/applications/audit/views.py b/src/etools/applications/audit/views.py index 171260acc6..586167e7e9 100644 --- a/src/etools/applications/audit/views.py +++ b/src/etools/applications/audit/views.py @@ -9,11 +9,12 @@ from easy_pdf.rendering import render_to_pdf_response from rest_framework import generics, mixins, viewsets from rest_framework.decorators import action +from rest_framework.exceptions import NotFound from rest_framework.filters import OrderingFilter, SearchFilter from rest_framework.permissions import IsAuthenticated from rest_framework.renderers import JSONRenderer from rest_framework.response import Response -from unicef_attachments.models import Attachment +from unicef_attachments.models import Attachment, AttachmentLink from unicef_restlib.pagination import DynamicPageNumberPagination from unicef_restlib.views import MultiSerializerViewSetMixin, NestedViewSetMixin, SafeTenantViewSetMixin @@ -49,6 +50,14 @@ ) from etools.applications.audit.purchase_order.models import AuditorFirm, AuditorStaffMember, PurchaseOrder from etools.applications.audit.purchase_order.synchronizers import POSynchronizer +from etools.applications.audit.serializers.attachments import ( + AuditAttachmentLinkSerializer, + EngagementAttachmentLinkSerializer, + ListAttachmentLinkSerializer, + MicroAssessmentAttachmentLinkSerializer, + SpecialAuditAttachmentLinkSerializer, + SpotCheckAttachmentLinkSerializer, +) from etools.applications.audit.serializers.auditor import ( AuditorFirmLightSerializer, AuditorFirmSerializer, @@ -592,3 +601,73 @@ def get_parent_filter(self): filters = super().get_parent_filter() filters.update({'code': 'audit_report'}) return filters + + +class BaseAttachmentLinksView(generics.ListCreateAPIView): + metadata_class = PermissionBasedMetadata + permission_classes = [IsAuthenticated] + + def get_content_type(self, model_name): + try: + return ContentType.objects.get_by_natural_key( + "audit", + model_name, + ) + except ContentType.DoesNotExist: + raise NotFound() + + def set_content_object(self): + self.content_type = self.get_content_type(self.content_model_name) + + try: + self.object_id = self.kwargs.get("object_pk") + model_cls = self.content_type.model_class() + self.content_object = model_cls.objects.get( + pk=self.object_id + ) + except model_cls.DoesNotExist: + raise NotFound() + + def get_serializer_context(self): + self.set_content_object() + context = super().get_serializer_context() + context["content_type"] = self.content_type + context["object_id"] = self.object_id + return context + + def get_queryset(self): + self.set_content_object() + return AttachmentLink.objects.filter( + content_type=self.content_type, + object_id=self.object_id, + ) + + def list(self, request, *args, **kwargs): + queryset = self.get_queryset() + serializer = ListAttachmentLinkSerializer(queryset, many=True) + return Response(serializer.data) + + +class EngagementAttachmentLinksView(BaseAttachmentLinksView): + serializer_class = EngagementAttachmentLinkSerializer + content_model_name = "engagement" + + +class SpotCheckAttachmentLinksView(BaseAttachmentLinksView): + serializer_class = SpotCheckAttachmentLinkSerializer + content_model_name = "spotcheck" + + +class MicroAssessmentAttachmentLinksView(BaseAttachmentLinksView): + serializer_class = MicroAssessmentAttachmentLinkSerializer + content_model_name = "microassessment" + + +class AuditAttachmentLinksView(BaseAttachmentLinksView): + serializer_class = AuditAttachmentLinkSerializer + content_model_name = "audit" + + +class SpecialAuditAttachmentLinksView(BaseAttachmentLinksView): + serializer_class = SpecialAuditAttachmentLinkSerializer + content_model_name = "specialaudit" From a66f22e09745b6c1e7d3203a082837261768c141 Mon Sep 17 00:00:00 2001 From: Domenico DiNicola Date: Fri, 15 Feb 2019 09:41:08 -0500 Subject: [PATCH 49/72] 9019 alternative --- Pipfile | 2 +- Pipfile.lock | 18 +++++++++--------- .../action_points/export/renderers.py | 1 + .../action_points/export/serializers.py | 7 ++++++- 4 files changed, 17 insertions(+), 11 deletions(-) diff --git a/Pipfile b/Pipfile index 916c126c8d..7414c46ce0 100644 --- a/Pipfile +++ b/Pipfile @@ -125,7 +125,7 @@ PyPDF2 = "==1.26.0" PyYAML = "==3.12" unicef_attachments = "==0.4.2" unicef_notification = "==0.2.1" -unicef_restlib = "==0.3.8" +unicef_restlib = "==0.4" unicef_snapshot = "==0.2.2" Pillow = "==5.3.0" diff --git a/Pipfile.lock b/Pipfile.lock index 5a21481ee8..6f0025809c 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "2cf2a142b8bfd0394dbb206bca41091782ec0a75a62679832406e8e974110296" + "sha256": "91f7de7665d547437383c4cd726b05e957ceff26291f6f3cdc7ae29b035832aa" }, "pipfile-spec": 6, "requires": { @@ -1011,10 +1011,10 @@ }, "unicef-restlib": { "hashes": [ - "sha256:256f81a7f3ee4ab726af02c0c4b872788675676454d44c9006ce4bec596f7351" + "sha256:668c6fdd272a07097123ac0f74f966e91593898a4fc732935c6237b83617e1bd" ], "index": "pypi", - "version": "==0.3.8" + "version": "==0.4" }, "unicef-snapshot": { "hashes": [ @@ -1360,10 +1360,10 @@ }, "parso": { "hashes": [ - "sha256:6ecf7244be8e7283ec9009c72d074830e7e0e611c974f813d76db0390a4e0dd6", - "sha256:8162be7570ffb34ec0b8d215d7f3b6c5fab24f51eb3886d6dee362de96b6db94" + "sha256:4580328ae3f548b358f4901e38c0578229186835f0fa0846e47369796dd5bcc9", + "sha256:68406ebd7eafe17f8e40e15a84b56848eccbf27d7c1feb89e93d8fca395706db" ], - "version": "==0.3.3" + "version": "==0.3.4" }, "pbr": { "hashes": [ @@ -1552,10 +1552,10 @@ }, "virtualenv": { "hashes": [ - "sha256:58c359370401e0af817fb0070911e599c5fdc836166306b04fd0f278151ed125", - "sha256:729f0bcab430e4ef137646805b5b1d8efbb43fe53d4a0f33328624a84a5121f7" + "sha256:8b9abfc51c38b70f61634bf265e5beacf6fae11fc25d355d1871f49b8e45f0db", + "sha256:cceab52aa7d4df1e1871a70236eb2b89fcfe29b6b43510d9738689787c513261" ], - "version": "==16.3.0" + "version": "==16.4.0" }, "wcwidth": { "hashes": [ diff --git a/src/etools/applications/action_points/export/renderers.py b/src/etools/applications/action_points/export/renderers.py index 1319fb7046..0b088033b4 100644 --- a/src/etools/applications/action_points/export/renderers.py +++ b/src/etools/applications/action_points/export/renderers.py @@ -4,6 +4,7 @@ class ActionPointCSVRenderer(ListSeperatorCSVRenderMixin, FriendlyCSVRenderer): + header = [ 'ref', 'cp_output', 'partner', 'office', 'section', 'category', 'assigned_to', 'due_date', 'status', 'high_priority', 'description', 'intervention', 'pd_ssfa', 'location', 'related_module', diff --git a/src/etools/applications/action_points/export/serializers.py b/src/etools/applications/action_points/export/serializers.py index 0fc12345e2..22944e3c6c 100644 --- a/src/etools/applications/action_points/export/serializers.py +++ b/src/etools/applications/action_points/export/serializers.py @@ -2,6 +2,7 @@ class ActionPointExportSerializer(serializers.Serializer): + ref = serializers.CharField(source='reference_number', read_only=True) cp_output = serializers.CharField(allow_null=True) partner = serializers.CharField(source='partner.name', allow_null=True) @@ -22,4 +23,8 @@ class ActionPointExportSerializer(serializers.Serializer): related_ref = serializers.CharField(source='related_object.reference_number', read_only=True, allow_null=True) related_object_str = serializers.CharField() related_object_url = serializers.CharField() - action_taken = serializers.SlugRelatedField(source='comments', many=True, read_only=True, slug_field='comment') + action_taken = serializers.SerializerMethodField() + + def get_action_taken(self, obj): + return ";\n\n".join(["{} ({}): {}".format(c.user if c.user else '-', c.submit_date.strftime( + "%d %b %Y"), c.comment) for c in obj.comments.all()]) From 10e9e2015ffe5a57e21c593fafdf780f206c8d66 Mon Sep 17 00:00:00 2001 From: Greg Reinbach Date: Fri, 15 Feb 2019 09:52:54 -0500 Subject: [PATCH 50/72] Add further audit engagement attachment link tests --- .../audit/tests/test_attachment_link_views.py | 123 +++++++++++++++++- 1 file changed, 122 insertions(+), 1 deletion(-) diff --git a/src/etools/applications/audit/tests/test_attachment_link_views.py b/src/etools/applications/audit/tests/test_attachment_link_views.py index a4c1c42beb..9520dd6972 100644 --- a/src/etools/applications/audit/tests/test_attachment_link_views.py +++ b/src/etools/applications/audit/tests/test_attachment_link_views.py @@ -4,7 +4,14 @@ from unicef_attachments.models import AttachmentLink from etools.applications.attachments.tests.factories import AttachmentFactory -from etools.applications.audit.tests.factories import EngagementFactory, UserFactory +from etools.applications.audit.tests.factories import ( + AuditFactory, + EngagementFactory, + MicroAssessmentFactory, + SpecialAuditFactory, + SpotCheckFactory, + UserFactory, +) from etools.applications.EquiTrack.tests.cases import BaseTenantTestCase @@ -33,3 +40,117 @@ def test_add(self): ) self.assertEqual(list_response.status_code, status.HTTP_200_OK) self.assertEqual(links_qs.count(), 1) + + +class TestSpotCheckAttachmentsView(BaseTenantTestCase): + @classmethod + def setUpTestData(cls): + cls.user = UserFactory(unicef_user=True) + cls.spotcheck = SpotCheckFactory() + cls.attachment = AttachmentFactory(content_object=cls.spotcheck) + + def test_add(self): + links_qs = AttachmentLink.objects + self.assertEqual(links_qs.count(), 0) + create_response = self.forced_auth_req( + 'post', + reverse('audit:spot-check-links', args=[self.spotcheck.pk]), + user=self.user, + data={'attachments': [{'attachment': self.attachment.pk}]} + ) + self.assertEqual(create_response.status_code, status.HTTP_201_CREATED) + + list_response = self.forced_auth_req( + 'get', + reverse('audit:spot-check-links', args=[self.spotcheck.pk]), + user=self.user + ) + self.assertEqual(list_response.status_code, status.HTTP_200_OK) + self.assertEqual(links_qs.count(), 1) + + +class TestMicroAssessmentAttachmentsView(BaseTenantTestCase): + @classmethod + def setUpTestData(cls): + cls.user = UserFactory(unicef_user=True) + cls.microassessment = MicroAssessmentFactory() + cls.attachment = AttachmentFactory(content_object=cls.microassessment) + + def test_add(self): + links_qs = AttachmentLink.objects + self.assertEqual(links_qs.count(), 0) + create_response = self.forced_auth_req( + 'post', + reverse( + 'audit:micro-assessment-links', + args=[self.microassessment.pk], + ), + user=self.user, + data={'attachments': [{'attachment': self.attachment.pk}]} + ) + self.assertEqual(create_response.status_code, status.HTTP_201_CREATED) + + list_response = self.forced_auth_req( + 'get', + reverse( + 'audit:micro-assessment-links', + args=[self.microassessment.pk], + ), + user=self.user + ) + self.assertEqual(list_response.status_code, status.HTTP_200_OK) + self.assertEqual(links_qs.count(), 1) + + +class TestAuditAttachmentsView(BaseTenantTestCase): + @classmethod + def setUpTestData(cls): + cls.user = UserFactory(unicef_user=True) + cls.audit = AuditFactory() + cls.attachment = AttachmentFactory(content_object=cls.audit) + + def test_add(self): + links_qs = AttachmentLink.objects + self.assertEqual(links_qs.count(), 0) + create_response = self.forced_auth_req( + 'post', + reverse('audit:audit-links', args=[self.audit.pk]), + user=self.user, + data={'attachments': [{'attachment': self.attachment.pk}]} + ) + self.assertEqual(create_response.status_code, status.HTTP_201_CREATED) + + list_response = self.forced_auth_req( + 'get', + reverse('audit:audit-links', args=[self.audit.pk]), + user=self.user + ) + self.assertEqual(list_response.status_code, status.HTTP_200_OK) + self.assertEqual(links_qs.count(), 1) + + +class TestSpecialAuditAttachmentsView(BaseTenantTestCase): + @classmethod + def setUpTestData(cls): + cls.user = UserFactory(unicef_user=True) + cls.specialaudit = SpecialAuditFactory() + cls.attachment = AttachmentFactory(content_object=cls.specialaudit) + + def test_add(self): + links_qs = AttachmentLink.objects + self.assertEqual(links_qs.count(), 0) + create_response = self.forced_auth_req( + 'post', + reverse('audit:special-audit-links', args=[self.specialaudit.pk]), + user=self.user, + data={'attachments': [{'attachment': self.attachment.pk}]} + ) + self.assertEqual(create_response.status_code, status.HTTP_201_CREATED) + + list_response = self.forced_auth_req( + 'get', + reverse('audit:special-audit-links', args=[self.specialaudit.pk]), + user=self.user + ) + self.assertEqual(list_response.status_code, status.HTTP_200_OK) + self.assertEqual(links_qs.count(), 1) From f65f5f2ac6fd287e994fc3e4e24eec39f7250abb Mon Sep 17 00:00:00 2001 From: denes csaba Date: Fri, 15 Feb 2019 22:13:23 +0200 Subject: [PATCH 51/72] Changes to be committed: modified: src/etools/templates/partners/pca/french_pdf.html modified: src/etools/templates/partners/pca/portuguese_pdf.html modified: src/etools/templates/partners/pca/spanish_pdf.html --- .../templates/partners/pca/french_pdf.html | 2 +- .../partners/pca/portuguese_pdf.html | 26 +++++++++---------- .../templates/partners/pca/spanish_pdf.html | 4 +-- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/etools/templates/partners/pca/french_pdf.html b/src/etools/templates/partners/pca/french_pdf.html index 43ca4e6f95..83e5d068c2 100644 --- a/src/etools/templates/partners/pca/french_pdf.html +++ b/src/etools/templates/partners/pca/french_pdf.html @@ -1170,7 +1170,7 @@ long terme, financier ou autre, en rapport avec le présent accord.
    11.5     - En cas de résiliation du présent accord conformément à la présente article 1.0, l’IP transférera à + En cas de résiliation du présent accord conformément à la présente article 11.0, l’IP transférera à l’UNICEF, ou suivra les instructions de l’UNICEF à ce sujet, tout solde de fonds non dépensés détenus par l’IP et provenant de Transferts de fonds, ainsi que tous les équipements et fournitures non utilisés fournis par l’UNICEF aux termes du présent accord, et tous biens durables fournis par diff --git a/src/etools/templates/partners/pca/portuguese_pdf.html b/src/etools/templates/partners/pca/portuguese_pdf.html index 87bdbc60ae..8d567f0555 100644 --- a/src/etools/templates/partners/pca/portuguese_pdf.html +++ b/src/etools/templates/partners/pca/portuguese_pdf.html @@ -344,7 +344,7 @@
    (e)    Providenciar ao UNICEF os relatórios exigidos nos termos do presente Acordo, de forma oportuna e satisfatória e, fornecer todas as outras informações sobre - o Documento do Programa e a utilização de qualquer dinheiro , suprimentos e equipamentos transferidos + o Documento do Programa e a utilização de qualquer dinheiro, suprimentos e equipamentos transferidos pelo UNICEF e que, o UNICEF pode razoavelmente solicitar;
    (f)    Exercer o mais alto padrão de cuidados ao manusear e @@ -433,7 +433,7 @@
    (A) Transferência de dinheiro pelo UNICEF para e, em nome do PI.

    Provisões Gerais:

      -
    1. O UNICEF irá providenciar ao PI assistência em dinheiro ( “Transferência de Dinheiro”) para as +
    2. O UNICEF irá providenciar ao PI assistência em dinheiro (“Transferência de Dinheiro”) para as realização das actividades previstas no Documentos do Programa. Esta transferência está sujeita à disponibilidade de fundos e nos termos do presente Acordo. A assistência do UNICEF para o PI não poderá exceder os montantes incluídos no Documento do Programa. O UNICEF vai prestar a assistência @@ -441,9 +441,9 @@ Transferência de Dinheiro” e a cada uma nominada de (“Modalidade de Transferência de Dinheiro”):
    3. (a)    Pagamento adiantado feito pelo UNICEF para o PI (a que - se refere no HACT como “Transferência Directa de Dinheiro” ).
      + se refere no HACT como “Transferência Directa de Dinheiro”).
    (b)    Reembolso feito pelo UNICEF para o PI (a que se refere - no HACT como “Reembolso” ); e
    + no HACT como “Reembolso”); e
    (c)    Pagamento feito pelo UNICEF em nome do PI ao vendedor ou fornecedor (a que se refere no HACT e no presente Acordo como “Pagamento directo”).
    @@ -493,7 +493,7 @@ feito antes das despesas anteriores terem sido reportadas ao UNICEF, usando o FACE, e progresso das actividades reportado utilizando o PDPR. Se o segundo pedido for recebido, de forma oportuna, correta e completa, o UNICEF irá determinar o montante a ser transferido e irá transferir esse - montante para, ou , onde é utilizada a modalidade de Pagamento Directo em nome do PI e, dentro de + montante para, ou, onde é utilizada a modalidade de Pagamento Directo em nome do PI e, dentro de um período de tempo razoável.

    Procedimentos adicionais aplicáveis apenas à modalidade de pagamento @@ -521,7 +521,7 @@ e a relação de custo e eficácia.

    (c)    Que não hajam outros motivos para crer que, as despesas - violam o disposto no presente Acordo, incluindo no Documento do Programa ; e + violam o disposto no presente Acordo, incluindo no Documento do Programa; e
    (d)    Sem prejuízo do disposto no parágrafo 5 alínea c) acima, as anteriores Transferências de Dinheiro em Parcelas terão sido, para a satisfação do UNICEF, @@ -531,7 +531,7 @@ de Dinheiro em Parcela incluindo:
    (a)    Tomar em consideração o progresso geral feito até à data - no âmbito do Documento do Programa ; ou + no âmbito do Documento do Programa; ou
    (b)    Para compensar os eventuais saldos ou balanços não reportados dos remanescentes das anteriores Transferências de Dinheiro em Parcela feitas ao PI. @@ -725,7 +725,7 @@
    (vi)    Vai incluir os reembolsos ou ajustes recebidos pelo PI contra qualquer Transferências de Dinheiro em Parcelas efectuadas anteriormente.
    -
    (d)    O UNICEF terá acesso , mediante pedido, a todos os documentos e registos que apoiam +
    (d)    O UNICEF terá acesso, mediante pedido, a todos os documentos e registos que apoiam ou que, podem ser considerados como de apoio à informação contida no FACE.

    Despesas não elegíveis:

    @@ -733,7 +733,7 @@ seu critério exclusivo) e sendo por isso, não incluídas no formulário do FACE:
    (i)    Despesas não efectuadas para as actividades ou que, não - sejam necessárias para a realização das actividades , incluídas no Documento do Programa. + sejam necessárias para a realização das actividades, incluídas no Documento do Programa.
    (ii)    Despesas do imposto sobre o valor acrescentado (IVA) a menos que, o PI possa demonstrar razoavelmente ao UNICEF que é incapaz de recuperar o IVA. @@ -872,7 +872,7 @@
  • Qualquer disputa, controvérsia ou reclamação entre as Partes decorrente do presente Acordo ou, violação, rescisão ou nulidade deste Acordo, salvo se resolvido de forma amigável ao abrigo do - número anterior , no prazo de sessenta (60) dias após a recepção de um pedido por escrito de uma + número anterior, no prazo de sessenta (60) dias após a recepção de um pedido por escrito de uma das Partes à outra Parte, para a resolução amigável, esta deve ser referida por qualquer das Partes para a arbitragem de acordo com as Regras de Arbitragem da UNCITRAL então obtidas. As decisões do tribunal de arbitragem devem ser baseadas nos princípios gerais do direito comercial internacional. @@ -882,11 +882,11 @@ outras medidas de protecção sejam tomadas com relação aos bens, serviços ou quaisquer outros bens, tangíveis ou não tangíveis, ou de qualquer informação confidencial fornecida ao abrigo do Acordo, conforme apropriado e de acordo com a autoridade do tribunal de arbitragem nos termos do - artigo 26º ( “Medidas Provisórias de Protecção”) e o artigo 34º ( “Forma e Efeitos da Sentença Arbitral”) + artigo 26º (“Medidas Provisórias de Protecção”) e o artigo 34º (“Forma e Efeitos da Sentença Arbitral”) que são constantes das Regras de Arbitragem da UNCITRAL. O tribunal de arbitragem não têm autoridade - para decretar sentenças punitivas. Além disso, salvo declaração expressa em contrário no Acordo , + para decretar sentenças punitivas. Além disso, salvo declaração expressa em contrário no Acordo, o tribunal de arbitragem não têm autoridade para conceder juros e prevalece o London Bank Offered - Rate (“LIBOR”), e esses juros devem apenas juros simples. As Partes devem estar vinculadas a + Rate (“LIBOR”), e esses juros devem apenas juros simples. As Partes devem estar vinculadas a qualquer decisão de arbitragem e como resultado de tal arbitragem, de adjudicação final de qualquer disputa, controvérsia ou reclamação.
  • diff --git a/src/etools/templates/partners/pca/spanish_pdf.html b/src/etools/templates/partners/pca/spanish_pdf.html index 19233bf462..6a5d916ced 100644 --- a/src/etools/templates/partners/pca/spanish_pdf.html +++ b/src/etools/templates/partners/pca/spanish_pdf.html @@ -1486,8 +1486,8 @@ a) no sean utilizados para brindar apoyo a personas o entidades relacionadas con el terrorismo; b) no sean transferidos por el SI a ninguna persona ni entidad incluida en la Lista Consolidada del Comité del Consejo de Seguridad de las Naciones Unidas disponible en - - https://www.un.org/sc/suborg/en/sanctions/un-sc-consolidated-list + + https://www.un.org/securitycouncil/sanctions/un-sc-consolidated-list ; y c) no sean utilizados, en el caso del dinero, para realizar pagos a personas o entidades, ni para la importación de bienes, si dicho pago o importación están prohibidos por decisión del Consejo de From 50841c3d2c1bfcdadfd079fedb64a6dc9be34572 Mon Sep 17 00:00:00 2001 From: denes csaba Date: Fri, 15 Feb 2019 22:47:53 +0200 Subject: [PATCH 52/72] Capitalization --- .../templates/partners/pca/ifrc_english_pdf.html | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/etools/templates/partners/pca/ifrc_english_pdf.html b/src/etools/templates/partners/pca/ifrc_english_pdf.html index e209faa0d6..cdf47c5858 100644 --- a/src/etools/templates/partners/pca/ifrc_english_pdf.html +++ b/src/etools/templates/partners/pca/ifrc_english_pdf.html @@ -923,8 +923,8 @@ (C)   ASSURANCE ACTIVITIES
    IP consents to the sharing by UNICEF with its donors of the audit reports referred to in - article 14.1; the spot check and programmatic visit reports referred to in article 14.2; - and the investigation reports referred to in article 14.3. + Article 14.1; the spot check and programmatic visit reports referred to in Article 14.2; + and the investigation reports referred to in Article 14.3.
    (D)   SUPPORT TO TERRORISM @@ -1426,9 +1426,9 @@
    15.4     - IP consents to the public disclosure by UNICEF of the audit reports referred to in article 15.1; - the spot check and programmatic visit reports referred to in article 15.2; and the investigation - reports referred to in article 15.3. (it is understood that investigation reports under article 14.5, + IP consents to the public disclosure by UNICEF of the audit reports referred to in Article 15.1; + the spot check and programmatic visit reports referred to in Article 15.2; and the investigation + reports referred to in Article 15.3. (it is understood that investigation reports under Article 14.5, or perpetrator information, will only be shared within the UN).
    From e30eeeb0650ab2ff36691e1720561babe7d27cad Mon Sep 17 00:00:00 2001 From: Domenico DiNicola Date: Wed, 13 Feb 2019 13:45:20 -0500 Subject: [PATCH 53/72] 9676: remove total_amount_of_ineligible_expenditure has mandatory field --- src/etools/applications/audit/transitions/conditions.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/etools/applications/audit/transitions/conditions.py b/src/etools/applications/audit/transitions/conditions.py index 3732a205ed..5ee6f7502f 100644 --- a/src/etools/applications/audit/transitions/conditions.py +++ b/src/etools/applications/audit/transitions/conditions.py @@ -134,8 +134,7 @@ class EngagementSubmitReportRequiredFieldsCheck(BaseRequiredFieldsCheck): class SPSubmitReportRequiredFieldsCheck(EngagementSubmitReportRequiredFieldsCheck): fields = EngagementSubmitReportRequiredFieldsCheck.fields + [ - 'total_amount_tested', 'total_amount_of_ineligible_expenditure', 'internal_controls', - 'exchange_rate', + 'total_amount_tested', 'internal_controls', 'exchange_rate', ] From cce5a937e6fc62182abb4510422ae5fcff134821 Mon Sep 17 00:00:00 2001 From: Domenico Date: Wed, 20 Feb 2019 13:10:20 -0500 Subject: [PATCH 54/72] Update __init__.py --- src/etools/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/etools/__init__.py b/src/etools/__init__.py index 4d4352217b..90d5014778 100644 --- a/src/etools/__init__.py +++ b/src/etools/__init__.py @@ -1 +1,2 @@ VERSION = __version__ = '6.8' +NAME = 'eTools' From fc5c06fd98af3497705b43528b2b917e06c8c1c9 Mon Sep 17 00:00:00 2001 From: Domenico Date: Wed, 20 Feb 2019 13:16:50 -0500 Subject: [PATCH 55/72] Update __init__.py --- src/etools/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/etools/__init__.py b/src/etools/__init__.py index 90d5014778..a2a6825a5c 100644 --- a/src/etools/__init__.py +++ b/src/etools/__init__.py @@ -1,2 +1,2 @@ VERSION = __version__ = '6.8' -NAME = 'eTools' +NAME = 'eTools' From cd8362adf50de89495fcae6a7bdb618c61c16dcd Mon Sep 17 00:00:00 2001 From: Greg Reinbach Date: Thu, 21 Feb 2019 08:23:41 -0500 Subject: [PATCH 56/72] Travel Activity is required for T2FActionPoint models Otherwise it will not be considered a T2FActionPoint, rather just a ActionPoint --- src/etools/applications/t2f/admin.py | 3 ++- src/etools/applications/t2f/forms.py | 11 +++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 src/etools/applications/t2f/forms.py diff --git a/src/etools/applications/t2f/admin.py b/src/etools/applications/t2f/admin.py index 0bcc4411e9..c4b0ebc91e 100644 --- a/src/etools/applications/t2f/admin.py +++ b/src/etools/applications/t2f/admin.py @@ -4,6 +4,7 @@ from etools.applications.publics.admin import AdminListMixin from etools.applications.t2f import models from etools.applications.t2f.models import T2FActionPoint +from etools.applications.t2f.forms import T2FActionPointAdminForm @admin.register(models.Travel) @@ -83,4 +84,4 @@ class TravelAttachmentAdmin(AdminListMixin, admin.ModelAdmin): @admin.register(T2FActionPoint) class T2FActionPointAdmin(ActionPointAdmin): - pass + form = T2FActionPointAdminForm diff --git a/src/etools/applications/t2f/forms.py b/src/etools/applications/t2f/forms.py new file mode 100644 index 0000000000..08b031ebfd --- /dev/null +++ b/src/etools/applications/t2f/forms.py @@ -0,0 +1,11 @@ +from django import forms +from etools.applications.t2f.models import T2FActionPoint + + +class T2FActionPointAdminForm(forms.ModelForm): + model = T2FActionPoint + fields = "__all__" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields["travel_activity"].required = True From 4f2f6660fab35c2565d32262b0312a737c917d5c Mon Sep 17 00:00:00 2001 From: Greg Reinbach Date: Fri, 22 Feb 2019 04:39:08 -0500 Subject: [PATCH 57/72] Require engagement value in EngagementActionPointAdmin --- src/etools/applications/audit/admin.py | 20 ++++++++++++++++---- src/etools/applications/audit/forms.py | 11 +++++++++++ 2 files changed, 27 insertions(+), 4 deletions(-) create mode 100644 src/etools/applications/audit/forms.py diff --git a/src/etools/applications/audit/admin.py b/src/etools/applications/audit/admin.py index 9f889d73da..d823c8a53a 100644 --- a/src/etools/applications/audit/admin.py +++ b/src/etools/applications/audit/admin.py @@ -1,12 +1,23 @@ - from django.contrib import admin from ordered_model.admin import OrderedModelAdmin from etools.applications.action_points.admin import ActionPointAdmin -from etools.applications.audit.models import (Audit, Engagement, FinancialFinding, Finding, MicroAssessment, - Risk, RiskBluePrint, RiskCategory, SpecialAuditRecommendation, - SpecificProcedure, SpotCheck, EngagementActionPoint) +from etools.applications.audit.forms import EngagementActionPointAdminForm +from etools.applications.audit.models import ( + Audit, + Engagement, + EngagementActionPoint, + FinancialFinding, + Finding, + MicroAssessment, + Risk, + RiskBluePrint, + RiskCategory, + SpecialAuditRecommendation, + SpecificProcedure, + SpotCheck, +) @admin.register(Engagement) @@ -94,4 +105,5 @@ class SpecialAuditRecommendationAdmin(admin.ModelAdmin): @admin.register(EngagementActionPoint) class EngagementActionPointAdmin(ActionPointAdmin): + form = EngagementActionPointAdminForm list_display = ('engagement', ) + ActionPointAdmin.list_display diff --git a/src/etools/applications/audit/forms.py b/src/etools/applications/audit/forms.py new file mode 100644 index 0000000000..1309443684 --- /dev/null +++ b/src/etools/applications/audit/forms.py @@ -0,0 +1,11 @@ +from django import forms +from etools.applications.audit.models import EngagementActionPoint + + +class EngagementActionPointAdminForm(forms.ModelForm): + model = EngagementActionPoint + fields = "__all__" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields["engagement"].required = True From 44219b238521acf73728365ae24f4623b3a1a00c Mon Sep 17 00:00:00 2001 From: Greg Reinbach Date: Mon, 25 Feb 2019 04:45:40 -0500 Subject: [PATCH 58/72] Ensure code is set for Intervention Attachment formset --- src/etools/applications/partners/admin.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/etools/applications/partners/admin.py b/src/etools/applications/partners/admin.py index 0590030d3b..ff0d475fea 100644 --- a/src/etools/applications/partners/admin.py +++ b/src/etools/applications/partners/admin.py @@ -193,6 +193,12 @@ class InterventionAttachmentsInline(admin.TabularInline): 'attachment', ) extra = 0 + code = 'partners_intervention_attachment' + + def get_formset(self, request, obj=None, **kwargs): + formset = super().get_formset(request, obj, **kwargs) + formset.code = self.code + return formset class InterventionResultsLinkAdmin(admin.ModelAdmin): From 5aaf7980cf13d35349ec7b68de6ac7fea550a89f Mon Sep 17 00:00:00 2001 From: Greg Reinbach Date: Mon, 25 Feb 2019 05:00:01 -0500 Subject: [PATCH 59/72] Add missing filename to param list in generate_final_report call --- src/etools/applications/audit/models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/etools/applications/audit/models.py b/src/etools/applications/audit/models.py index 7509a5c479..218c440343 100644 --- a/src/etools/applications/audit/models.py +++ b/src/etools/applications/audit/models.py @@ -675,6 +675,7 @@ def generate_final_report(self): AuditSerializer, AuditPDFSerializer, 'audit/audit_pdf.html', + 'audit_final_report.pdf', ) From abcf23cfb84c3690ba864198eaedd25da40c86d2 Mon Sep 17 00:00:00 2001 From: Greg Reinbach Date: Mon, 25 Feb 2019 06:31:05 -0500 Subject: [PATCH 60/72] Update generate final report to use tempfile and django storage properly --- src/etools/applications/audit/utils.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/etools/applications/audit/utils.py b/src/etools/applications/audit/utils.py index 7dec9d1d28..f3ee4907de 100644 --- a/src/etools/applications/audit/utils.py +++ b/src/etools/applications/audit/utils.py @@ -1,4 +1,7 @@ +import tempfile + from django.contrib.contenttypes.models import ContentType +from django.core.files import File from easy_pdf.rendering import render_to_pdf from unicef_attachments.models import Attachment @@ -21,7 +24,8 @@ def generate_final_report(obj, code, labels, pdf, template, filename): object_id=obj.pk, ) - with open(generate_file_path(attachment, filename), "wb") as fp: - fp.write(render_to_pdf(template, context)) - attachment.file = fp.name - attachment.save() + file_path = generate_file_path(attachment, filename) + temp_filename = tempfile.NamedTemporaryFile(suffix="pdf") + with open(temp_filename.name, "wb") as fp: + attachment.file = File(fp, name=file_path) + attachment.file.write(render_to_pdf(template, context)) From d7a27c4cf904afcdd1b953fd92cb51d5d9225f08 Mon Sep 17 00:00:00 2001 From: Greg Reinbach Date: Tue, 26 Feb 2019 05:12:42 -0500 Subject: [PATCH 61/72] Correct InterventionAttachment admin handling --- src/etools/applications/partners/admin.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/etools/applications/partners/admin.py b/src/etools/applications/partners/admin.py index ff0d475fea..b03d347926 100644 --- a/src/etools/applications/partners/admin.py +++ b/src/etools/applications/partners/admin.py @@ -1,4 +1,5 @@ from django.contrib import admin +from django.contrib.contenttypes.models import ContentType from django.db import models from django.forms import SelectMultiple from django.urls import reverse @@ -7,6 +8,7 @@ from import_export.admin import ExportMixin from unicef_attachments.admin import AttachmentSingleInline +from unicef_attachments.models import Attachment from unicef_snapshot.admin import ActivityInline, SnapshotModelAdmin from etools.applications.partners.exports import PartnerExport @@ -346,6 +348,23 @@ def attachments_link(self, obj): attachments_link.short_description = 'attachments' + def save_formset(self, request, form, formset, change): + instances = formset.save() + for instance in instances: + if isinstance(instance, InterventionAttachment): + # update attachment file data + content_type = ContentType.objects.get_for_model(instance) + Attachment.objects.update_or_create( + object_id=instance.pk, + content_type=content_type, + defaults={ + "code": formset.code, + "file": instance.attachment, + "uploaded_by": request.user, + } + ) + + class AssessmentReportInline(AttachmentSingleInline): verbose_name_plural = _("Report") From 21ea05408ba04577cdc0ebb553ea6d910e197f27 Mon Sep 17 00:00:00 2001 From: Greg Reinbach Date: Tue, 26 Feb 2019 05:16:18 -0500 Subject: [PATCH 62/72] flake8 cleanup --- src/etools/applications/partners/admin.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/etools/applications/partners/admin.py b/src/etools/applications/partners/admin.py index b03d347926..fff685e29d 100644 --- a/src/etools/applications/partners/admin.py +++ b/src/etools/applications/partners/admin.py @@ -365,7 +365,6 @@ def save_formset(self, request, form, formset, change): ) - class AssessmentReportInline(AttachmentSingleInline): verbose_name_plural = _("Report") code = 'partners_assessment_report' From c3fec5dba60f4f1ec0757af7239d43b584178d87 Mon Sep 17 00:00:00 2001 From: Greg Reinbach Date: Tue, 26 Feb 2019 15:35:55 -0500 Subject: [PATCH 63/72] Override attachment field value in InterventionAttachmentInline --- src/etools/applications/partners/admin.py | 2 ++ src/etools/applications/partners/forms.py | 25 +++++++++++++++++++++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/etools/applications/partners/admin.py b/src/etools/applications/partners/admin.py index fff685e29d..cc118f0935 100644 --- a/src/etools/applications/partners/admin.py +++ b/src/etools/applications/partners/admin.py @@ -15,6 +15,7 @@ from etools.applications.partners.forms import ( # TODO intervention sector locations cleanup PartnersAdminForm, PartnerStaffMemberForm, + InterventionAttachmentForm, ) from etools.applications.partners.mixins import CountryUsersAdminMixin, HiddenPartnerMixin from etools.applications.partners.models import ( # TODO intervention sector locations cleanup @@ -190,6 +191,7 @@ class InterventionAttachmentAdmin(AttachmentInlineAdminMixin, admin.ModelAdmin): class InterventionAttachmentsInline(admin.TabularInline): model = InterventionAttachment + form = InterventionAttachmentForm fields = ( 'type', 'attachment', diff --git a/src/etools/applications/partners/forms.py b/src/etools/applications/partners/forms.py index 16e3a22a03..8f512cdc89 100644 --- a/src/etools/applications/partners/forms.py +++ b/src/etools/applications/partners/forms.py @@ -1,4 +1,3 @@ - import logging from django import forms @@ -10,7 +9,12 @@ from unicef_djangolib.forms import AutoSizeTextForm -from etools.applications.partners.models import PartnerOrganization, PartnerStaffMember, PartnerType +from etools.applications.partners.models import ( + InterventionAttachment, + PartnerOrganization, + PartnerStaffMember, + PartnerType, +) logger = logging.getLogger('partners.forms') @@ -85,3 +89,20 @@ def clean(self): raise ValidationError({'active': self.ERROR_MESSAGES['user_unavailable']}) return cleaned_data + + +class InterventionAttachmentForm(forms.ModelForm): + class Meta: + model = InterventionAttachment + fields = ( + 'type', + 'attachment', + ) + + def __init__(self, *args, **kwargs): + instance = kwargs.get("instance", None) + if instance: + attachment = instance.attachment_file.last() + if attachment: + instance.attachment = attachment.file + super().__init__(*args, **kwargs) From 6c24bfa34ade6d37e000417a630ae0359e29cdc9 Mon Sep 17 00:00:00 2001 From: Greg Reinbach Date: Wed, 27 Feb 2019 07:27:15 -0500 Subject: [PATCH 64/72] Remove attachment_file from list display in InterventionAttachment admin --- src/etools/applications/partners/admin.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/etools/applications/partners/admin.py b/src/etools/applications/partners/admin.py index cc118f0935..e3f36b3df3 100644 --- a/src/etools/applications/partners/admin.py +++ b/src/etools/applications/partners/admin.py @@ -174,7 +174,6 @@ class InterventionAttachmentAdmin(AttachmentInlineAdminMixin, admin.ModelAdmin): model = InterventionAttachment list_display = ( 'intervention', - 'attachment_file', 'type', ) list_filter = ( From 9c6bbe1cf0d632aea0bebbe05ddc292ae9c62550 Mon Sep 17 00:00:00 2001 From: Greg Reinbach Date: Thu, 28 Feb 2019 10:20:38 -0500 Subject: [PATCH 65/72] Update way generated file is saved Ensure file type created properly, with label --- .../audit/tests/test_transitions.py | 3 +++ src/etools/applications/audit/utils.py | 22 ++++++++++++++----- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/etools/applications/audit/tests/test_transitions.py b/src/etools/applications/audit/tests/test_transitions.py index 893cdc2f3a..78fe2547e6 100644 --- a/src/etools/applications/audit/tests/test_transitions.py +++ b/src/etools/applications/audit/tests/test_transitions.py @@ -227,6 +227,9 @@ def test_finalize_focal_point(self): with patch(self.filepath, self.mock_filepath): self._test_finalize(self.unicef_focal_point, status.HTTP_200_OK) self.assertTrue(attachment_qs.exists()) + attachment = attachment_qs.get() + assert attachment.file + def test_cancel_auditor(self): self._test_cancel(self.auditor, status.HTTP_403_FORBIDDEN) diff --git a/src/etools/applications/audit/utils.py b/src/etools/applications/audit/utils.py index f3ee4907de..fff66a9748 100644 --- a/src/etools/applications/audit/utils.py +++ b/src/etools/applications/audit/utils.py @@ -1,10 +1,10 @@ import tempfile from django.contrib.contenttypes.models import ContentType -from django.core.files import File +from django.core.files.base import ContentFile from easy_pdf.rendering import render_to_pdf -from unicef_attachments.models import Attachment +from unicef_attachments.models import Attachment, FileType from etools.applications.attachments.models import generate_file_path @@ -18,14 +18,24 @@ def generate_final_report(obj, code, labels, pdf, template, filename): } content_type = ContentType.objects.get_for_model(obj) + file_type, __ = FileType.objects.get_or_create( + code=code, + defaults={ + "label": code.replace("_", " ").title(), + "name": code.replace("_", " "), + } + ) attachment, __ = Attachment.objects.get_or_create( code=code, content_type=content_type, object_id=obj.pk, + defaults={ + "file_type": file_type, + } ) file_path = generate_file_path(attachment, filename) - temp_filename = tempfile.NamedTemporaryFile(suffix="pdf") - with open(temp_filename.name, "wb") as fp: - attachment.file = File(fp, name=file_path) - attachment.file.write(render_to_pdf(template, context)) + attachment.file.save( + file_path, + ContentFile(render_to_pdf(template, context)), + ) From 0a7298e8c4403cbd53719aa54871db9dc6204697 Mon Sep 17 00:00:00 2001 From: Greg Reinbach Date: Thu, 28 Feb 2019 10:23:19 -0500 Subject: [PATCH 66/72] flake8 cleanup --- src/etools/applications/audit/tests/test_transitions.py | 1 - src/etools/applications/audit/utils.py | 2 -- 2 files changed, 3 deletions(-) diff --git a/src/etools/applications/audit/tests/test_transitions.py b/src/etools/applications/audit/tests/test_transitions.py index 78fe2547e6..a1e3a5801a 100644 --- a/src/etools/applications/audit/tests/test_transitions.py +++ b/src/etools/applications/audit/tests/test_transitions.py @@ -230,7 +230,6 @@ def test_finalize_focal_point(self): attachment = attachment_qs.get() assert attachment.file - def test_cancel_auditor(self): self._test_cancel(self.auditor, status.HTTP_403_FORBIDDEN) diff --git a/src/etools/applications/audit/utils.py b/src/etools/applications/audit/utils.py index fff66a9748..0414cc5656 100644 --- a/src/etools/applications/audit/utils.py +++ b/src/etools/applications/audit/utils.py @@ -1,5 +1,3 @@ -import tempfile - from django.contrib.contenttypes.models import ContentType from django.core.files.base import ContentFile From d743a94dc44f005cfd288b2cde8a956965d8efa1 Mon Sep 17 00:00:00 2001 From: Greg Reinbach Date: Thu, 28 Feb 2019 13:29:07 -0500 Subject: [PATCH 67/72] Reduce t2f permissions for Traveler group --- src/etools/applications/t2f/permissions.py | 250 ++++++++++----------- 1 file changed, 125 insertions(+), 125 deletions(-) diff --git a/src/etools/applications/t2f/permissions.py b/src/etools/applications/t2f/permissions.py index 91c6681c9d..2e77160216 100644 --- a/src/etools/applications/t2f/permissions.py +++ b/src/etools/applications/t2f/permissions.py @@ -6353,14 +6353,14 @@ ('edit', 'action_points', 'person_responsible'): True, ('edit', 'action_points', 'status'): True, ('edit', 'action_points', 'trip_reference_number'): True, - ('edit', 'activities', 'date'): True, - ('edit', 'activities', 'id'): True, - ('edit', 'activities', 'locations'): True, - ('edit', 'activities', 'partner'): True, - ('edit', 'activities', 'partnership'): True, - ('edit', 'activities', 'primary_traveler'): True, - ('edit', 'activities', 'result'): True, - ('edit', 'activities', 'travel_type'): True, + ('edit', 'activities', 'date'): False, + ('edit', 'activities', 'id'): False, + ('edit', 'activities', 'locations'): False, + ('edit', 'activities', 'partner'): False, + ('edit', 'activities', 'partnership'): False, + ('edit', 'activities', 'primary_traveler'): False, + ('edit', 'activities', 'result'): False, + ('edit', 'activities', 'travel_type'): False, ('edit', 'clearances', 'id'): False, ('edit', 'clearances', 'medical_clearance'): False, ('edit', 'clearances', 'security_clearance'): False, @@ -6395,7 +6395,7 @@ ('edit', 'itinerary', 'origin'): False, ('edit', 'itinerary', 'overnight_travel'): False, ('edit', 'travel', 'action_points'): True, - ('edit', 'travel', 'activities'): True, + ('edit', 'travel', 'activities'): False, ('edit', 'travel', 'clearances'): False, ('edit', 'travel', 'cost_assignments'): False, ('edit', 'travel', 'cost_summary'): False, @@ -6504,68 +6504,68 @@ ('edit', 'action_points', 'person_responsible'): True, ('edit', 'action_points', 'status'): True, ('edit', 'action_points', 'trip_reference_number'): True, - ('edit', 'activities', 'date'): True, - ('edit', 'activities', 'id'): True, - ('edit', 'activities', 'locations'): True, - ('edit', 'activities', 'partner'): True, - ('edit', 'activities', 'partnership'): True, - ('edit', 'activities', 'primary_traveler'): True, - ('edit', 'activities', 'result'): True, - ('edit', 'activities', 'travel_type'): True, - ('edit', 'clearances', 'id'): True, - ('edit', 'clearances', 'medical_clearance'): True, - ('edit', 'clearances', 'security_clearance'): True, - ('edit', 'clearances', 'security_course'): True, - ('edit', 'cost_assignments', 'business_area'): True, - ('edit', 'cost_assignments', 'delegate'): True, - ('edit', 'cost_assignments', 'fund'): True, - ('edit', 'cost_assignments', 'grant'): True, - ('edit', 'cost_assignments', 'id'): True, - ('edit', 'cost_assignments', 'share'): True, - ('edit', 'cost_assignments', 'wbs'): True, - ('edit', 'deductions', 'accomodation'): True, - ('edit', 'deductions', 'breakfast'): True, - ('edit', 'deductions', 'date'): True, - ('edit', 'deductions', 'day_of_the_week'): True, - ('edit', 'deductions', 'dinner'): True, - ('edit', 'deductions', 'id'): True, - ('edit', 'deductions', 'lunch'): True, - ('edit', 'deductions', 'no_dsa'): True, + ('edit', 'activities', 'date'): False, + ('edit', 'activities', 'id'): False, + ('edit', 'activities', 'locations'): False, + ('edit', 'activities', 'partner'): False, + ('edit', 'activities', 'partnership'): False, + ('edit', 'activities', 'primary_traveler'): False, + ('edit', 'activities', 'result'): False, + ('edit', 'activities', 'travel_type'): False, + ('edit', 'clearances', 'id'): False, + ('edit', 'clearances', 'medical_clearance'): False, + ('edit', 'clearances', 'security_clearance'): False, + ('edit', 'clearances', 'security_course'): False, + ('edit', 'cost_assignments', 'business_area'): False, + ('edit', 'cost_assignments', 'delegate'): False, + ('edit', 'cost_assignments', 'fund'): False, + ('edit', 'cost_assignments', 'grant'): False, + ('edit', 'cost_assignments', 'id'): False, + ('edit', 'cost_assignments', 'share'): False, + ('edit', 'cost_assignments', 'wbs'): False, + ('edit', 'deductions', 'accomodation'): False, + ('edit', 'deductions', 'breakfast'): False, + ('edit', 'deductions', 'date'): False, + ('edit', 'deductions', 'day_of_the_week'): False, + ('edit', 'deductions', 'dinner'): False, + ('edit', 'deductions', 'id'): False, + ('edit', 'deductions', 'lunch'): False, + ('edit', 'deductions', 'no_dsa'): False, ('edit', 'expenses', 'account_currency'): False, - ('edit', 'expenses', 'amount'): True, + ('edit', 'expenses', 'amount'): False, ('edit', 'expenses', 'document_currency'): False, - ('edit', 'expenses', 'id'): True, - ('edit', 'expenses', 'type'): True, - ('edit', 'itinerary', 'airlines'): True, - ('edit', 'itinerary', 'arrival_date'): True, - ('edit', 'itinerary', 'departure_date'): True, - ('edit', 'itinerary', 'destination'): True, - ('edit', 'itinerary', 'dsa_region'): True, - ('edit', 'itinerary', 'id'): True, - ('edit', 'itinerary', 'mode_of_travel'): True, - ('edit', 'itinerary', 'origin'): True, - ('edit', 'itinerary', 'overnight_travel'): True, - ('edit', 'travel', 'action_points'): True, - ('edit', 'travel', 'activities'): True, - ('edit', 'travel', 'clearances'): True, - ('edit', 'travel', 'cost_assignments'): True, - ('edit', 'travel', 'cost_summary'): True, + ('edit', 'expenses', 'id'): False, + ('edit', 'expenses', 'type'): False, + ('edit', 'itinerary', 'airlines'): False, + ('edit', 'itinerary', 'arrival_date'): False, + ('edit', 'itinerary', 'departure_date'): False, + ('edit', 'itinerary', 'destination'): False, + ('edit', 'itinerary', 'dsa_region'): False, + ('edit', 'itinerary', 'id'): False, + ('edit', 'itinerary', 'mode_of_travel'): False, + ('edit', 'itinerary', 'origin'): False, + ('edit', 'itinerary', 'overnight_travel'): False, + ('edit', 'travel', 'action_points'): False, + ('edit', 'travel', 'activities'): False, + ('edit', 'travel', 'clearances'): False, + ('edit', 'travel', 'cost_assignments'): False, + ('edit', 'travel', 'cost_summary'): False, ('edit', 'travel', 'currency'): False, - ('edit', 'travel', 'deductions'): True, - ('edit', 'travel', 'end_date'): True, - ('edit', 'travel', 'expenses'): True, - ('edit', 'travel', 'id'): True, - ('edit', 'travel', 'international_travel'): True, - ('edit', 'travel', 'itinerary'): True, - ('edit', 'travel', 'office'): True, - ('edit', 'travel', 'purpose'): True, - ('edit', 'travel', 'reference_number'): True, + ('edit', 'travel', 'deductions'): False, + ('edit', 'travel', 'end_date'): False, + ('edit', 'travel', 'expenses'): False, + ('edit', 'travel', 'id'): False, + ('edit', 'travel', 'international_travel'): False, + ('edit', 'travel', 'itinerary'): False, + ('edit', 'travel', 'office'): False, + ('edit', 'travel', 'purpose'): False, + ('edit', 'travel', 'reference_number'): False, ('edit', 'travel', 'report'): True, - ('edit', 'travel', 'section'): True, - ('edit', 'travel', 'start_date'): True, - ('edit', 'travel', 'status'): True, - ('edit', 'travel', 'supervisor'): True, - ('edit', 'travel', 'ta_required'): True, + ('edit', 'travel', 'section'): False, + ('edit', 'travel', 'start_date'): False, + ('edit', 'travel', 'status'): False, + ('edit', 'travel', 'supervisor'): False, + ('edit', 'travel', 'ta_required'): False, ('edit', 'travel', 'traveler'): False, ('view', 'action_points', 'action_point_number'): True, ('view', 'action_points', 'actions_taken'): True, @@ -6957,68 +6957,68 @@ ('edit', 'action_points', 'person_responsible'): True, ('edit', 'action_points', 'status'): True, ('edit', 'action_points', 'trip_reference_number'): True, - ('edit', 'activities', 'date'): True, - ('edit', 'activities', 'id'): True, - ('edit', 'activities', 'locations'): True, - ('edit', 'activities', 'partner'): True, - ('edit', 'activities', 'partnership'): True, - ('edit', 'activities', 'primary_traveler'): True, - ('edit', 'activities', 'result'): True, - ('edit', 'activities', 'travel_type'): True, - ('edit', 'clearances', 'id'): True, - ('edit', 'clearances', 'medical_clearance'): True, - ('edit', 'clearances', 'security_clearance'): True, - ('edit', 'clearances', 'security_course'): True, - ('edit', 'cost_assignments', 'business_area'): True, - ('edit', 'cost_assignments', 'delegate'): True, - ('edit', 'cost_assignments', 'fund'): True, - ('edit', 'cost_assignments', 'grant'): True, - ('edit', 'cost_assignments', 'id'): True, - ('edit', 'cost_assignments', 'share'): True, - ('edit', 'cost_assignments', 'wbs'): True, - ('edit', 'deductions', 'accomodation'): True, - ('edit', 'deductions', 'breakfast'): True, - ('edit', 'deductions', 'date'): True, - ('edit', 'deductions', 'day_of_the_week'): True, - ('edit', 'deductions', 'dinner'): True, - ('edit', 'deductions', 'id'): True, - ('edit', 'deductions', 'lunch'): True, - ('edit', 'deductions', 'no_dsa'): True, + ('edit', 'activities', 'date'): False, + ('edit', 'activities', 'id'): False, + ('edit', 'activities', 'locations'): False, + ('edit', 'activities', 'partner'): False, + ('edit', 'activities', 'partnership'): False, + ('edit', 'activities', 'primary_traveler'): False, + ('edit', 'activities', 'result'): False, + ('edit', 'activities', 'travel_type'): False, + ('edit', 'clearances', 'id'): False, + ('edit', 'clearances', 'medical_clearance'): False, + ('edit', 'clearances', 'security_clearance'): False, + ('edit', 'clearances', 'security_course'): False, + ('edit', 'cost_assignments', 'business_area'): False, + ('edit', 'cost_assignments', 'delegate'): False, + ('edit', 'cost_assignments', 'fund'): False, + ('edit', 'cost_assignments', 'grant'): False, + ('edit', 'cost_assignments', 'id'): False, + ('edit', 'cost_assignments', 'share'): False, + ('edit', 'cost_assignments', 'wbs'): False, + ('edit', 'deductions', 'accomodation'): False, + ('edit', 'deductions', 'breakfast'): False, + ('edit', 'deductions', 'date'): False, + ('edit', 'deductions', 'day_of_the_week'): False, + ('edit', 'deductions', 'dinner'): False, + ('edit', 'deductions', 'id'): False, + ('edit', 'deductions', 'lunch'): False, + ('edit', 'deductions', 'no_dsa'): False, ('edit', 'expenses', 'account_currency'): False, - ('edit', 'expenses', 'amount'): True, + ('edit', 'expenses', 'amount'): False, ('edit', 'expenses', 'document_currency'): False, - ('edit', 'expenses', 'id'): True, - ('edit', 'expenses', 'type'): True, - ('edit', 'itinerary', 'airlines'): True, - ('edit', 'itinerary', 'arrival_date'): True, - ('edit', 'itinerary', 'departure_date'): True, - ('edit', 'itinerary', 'destination'): True, - ('edit', 'itinerary', 'dsa_region'): True, - ('edit', 'itinerary', 'id'): True, - ('edit', 'itinerary', 'mode_of_travel'): True, - ('edit', 'itinerary', 'origin'): True, - ('edit', 'itinerary', 'overnight_travel'): True, - ('edit', 'travel', 'action_points'): True, - ('edit', 'travel', 'activities'): True, - ('edit', 'travel', 'clearances'): True, - ('edit', 'travel', 'cost_assignments'): True, - ('edit', 'travel', 'cost_summary'): True, + ('edit', 'expenses', 'id'): False, + ('edit', 'expenses', 'type'): False, + ('edit', 'itinerary', 'airlines'): False, + ('edit', 'itinerary', 'arrival_date'): False, + ('edit', 'itinerary', 'departure_date'): False, + ('edit', 'itinerary', 'destination'): False, + ('edit', 'itinerary', 'dsa_region'): False, + ('edit', 'itinerary', 'id'): False, + ('edit', 'itinerary', 'mode_of_travel'): False, + ('edit', 'itinerary', 'origin'): False, + ('edit', 'itinerary', 'overnight_travel'): False, + ('edit', 'travel', 'action_points'): False, + ('edit', 'travel', 'activities'): False, + ('edit', 'travel', 'clearances'): False, + ('edit', 'travel', 'cost_assignments'): False, + ('edit', 'travel', 'cost_summary'): False, ('edit', 'travel', 'currency'): False, - ('edit', 'travel', 'deductions'): True, - ('edit', 'travel', 'end_date'): True, - ('edit', 'travel', 'expenses'): True, - ('edit', 'travel', 'id'): True, - ('edit', 'travel', 'international_travel'): True, - ('edit', 'travel', 'itinerary'): True, - ('edit', 'travel', 'office'): True, - ('edit', 'travel', 'purpose'): True, - ('edit', 'travel', 'reference_number'): True, + ('edit', 'travel', 'deductions'): False, + ('edit', 'travel', 'end_date'): False, + ('edit', 'travel', 'expenses'): False, + ('edit', 'travel', 'id'): False, + ('edit', 'travel', 'international_travel'): False, + ('edit', 'travel', 'itinerary'): False, + ('edit', 'travel', 'office'): False, + ('edit', 'travel', 'purpose'): False, + ('edit', 'travel', 'reference_number'): False, ('edit', 'travel', 'report'): True, - ('edit', 'travel', 'section'): True, - ('edit', 'travel', 'start_date'): True, - ('edit', 'travel', 'status'): True, - ('edit', 'travel', 'supervisor'): True, - ('edit', 'travel', 'ta_required'): True, + ('edit', 'travel', 'section'): False, + ('edit', 'travel', 'start_date'): False, + ('edit', 'travel', 'status'): False, + ('edit', 'travel', 'supervisor'): False, + ('edit', 'travel', 'ta_required'): False, ('edit', 'travel', 'traveler'): False, ('view', 'action_points', 'action_point_number'): True, ('view', 'action_points', 'actions_taken'): True, From b6c7ec9390cbf8c204d3eeaa359c4419ba38ca55 Mon Sep 17 00:00:00 2001 From: Greg Reinbach Date: Thu, 28 Feb 2019 15:22:37 -0500 Subject: [PATCH 68/72] Reset reject permissions for Traveler --- src/etools/applications/t2f/permissions.py | 116 ++++++++++----------- 1 file changed, 58 insertions(+), 58 deletions(-) diff --git a/src/etools/applications/t2f/permissions.py b/src/etools/applications/t2f/permissions.py index 2e77160216..0830f2c2d1 100644 --- a/src/etools/applications/t2f/permissions.py +++ b/src/etools/applications/t2f/permissions.py @@ -6957,68 +6957,68 @@ ('edit', 'action_points', 'person_responsible'): True, ('edit', 'action_points', 'status'): True, ('edit', 'action_points', 'trip_reference_number'): True, - ('edit', 'activities', 'date'): False, - ('edit', 'activities', 'id'): False, - ('edit', 'activities', 'locations'): False, - ('edit', 'activities', 'partner'): False, - ('edit', 'activities', 'partnership'): False, - ('edit', 'activities', 'primary_traveler'): False, - ('edit', 'activities', 'result'): False, - ('edit', 'activities', 'travel_type'): False, - ('edit', 'clearances', 'id'): False, - ('edit', 'clearances', 'medical_clearance'): False, - ('edit', 'clearances', 'security_clearance'): False, - ('edit', 'clearances', 'security_course'): False, - ('edit', 'cost_assignments', 'business_area'): False, - ('edit', 'cost_assignments', 'delegate'): False, - ('edit', 'cost_assignments', 'fund'): False, - ('edit', 'cost_assignments', 'grant'): False, - ('edit', 'cost_assignments', 'id'): False, - ('edit', 'cost_assignments', 'share'): False, - ('edit', 'cost_assignments', 'wbs'): False, - ('edit', 'deductions', 'accomodation'): False, - ('edit', 'deductions', 'breakfast'): False, - ('edit', 'deductions', 'date'): False, - ('edit', 'deductions', 'day_of_the_week'): False, - ('edit', 'deductions', 'dinner'): False, - ('edit', 'deductions', 'id'): False, - ('edit', 'deductions', 'lunch'): False, - ('edit', 'deductions', 'no_dsa'): False, + ('edit', 'activities', 'date'): True, + ('edit', 'activities', 'id'): True, + ('edit', 'activities', 'locations'): True, + ('edit', 'activities', 'partner'): True, + ('edit', 'activities', 'partnership'): True, + ('edit', 'activities', 'primary_traveler'): True, + ('edit', 'activities', 'result'): True, + ('edit', 'activities', 'travel_type'): True, + ('edit', 'clearances', 'id'): True, + ('edit', 'clearances', 'medical_clearance'): True, + ('edit', 'clearances', 'security_clearance'): True, + ('edit', 'clearances', 'security_course'): True, + ('edit', 'cost_assignments', 'business_area'): True, + ('edit', 'cost_assignments', 'delegate'): True, + ('edit', 'cost_assignments', 'fund'): True, + ('edit', 'cost_assignments', 'grant'): True, + ('edit', 'cost_assignments', 'id'): True, + ('edit', 'cost_assignments', 'share'): True, + ('edit', 'cost_assignments', 'wbs'): True, + ('edit', 'deductions', 'accomodation'): True, + ('edit', 'deductions', 'breakfast'): True, + ('edit', 'deductions', 'date'): True, + ('edit', 'deductions', 'day_of_the_week'): True, + ('edit', 'deductions', 'dinner'): True, + ('edit', 'deductions', 'id'): True, + ('edit', 'deductions', 'lunch'): True, + ('edit', 'deductions', 'no_dsa'): True, ('edit', 'expenses', 'account_currency'): False, - ('edit', 'expenses', 'amount'): False, + ('edit', 'expenses', 'amount'): True, ('edit', 'expenses', 'document_currency'): False, - ('edit', 'expenses', 'id'): False, - ('edit', 'expenses', 'type'): False, - ('edit', 'itinerary', 'airlines'): False, - ('edit', 'itinerary', 'arrival_date'): False, - ('edit', 'itinerary', 'departure_date'): False, - ('edit', 'itinerary', 'destination'): False, - ('edit', 'itinerary', 'dsa_region'): False, - ('edit', 'itinerary', 'id'): False, - ('edit', 'itinerary', 'mode_of_travel'): False, - ('edit', 'itinerary', 'origin'): False, - ('edit', 'itinerary', 'overnight_travel'): False, - ('edit', 'travel', 'action_points'): False, - ('edit', 'travel', 'activities'): False, - ('edit', 'travel', 'clearances'): False, - ('edit', 'travel', 'cost_assignments'): False, - ('edit', 'travel', 'cost_summary'): False, + ('edit', 'expenses', 'id'): True, + ('edit', 'expenses', 'type'): True, + ('edit', 'itinerary', 'airlines'): True, + ('edit', 'itinerary', 'arrival_date'): True, + ('edit', 'itinerary', 'departure_date'): True, + ('edit', 'itinerary', 'destination'): True, + ('edit', 'itinerary', 'dsa_region'): True, + ('edit', 'itinerary', 'id'): True, + ('edit', 'itinerary', 'mode_of_travel'): True, + ('edit', 'itinerary', 'origin'): True, + ('edit', 'itinerary', 'overnight_travel'): True, + ('edit', 'travel', 'action_points'): True, + ('edit', 'travel', 'activities'): True, + ('edit', 'travel', 'clearances'): True, + ('edit', 'travel', 'cost_assignments'): True, + ('edit', 'travel', 'cost_summary'): True, ('edit', 'travel', 'currency'): False, - ('edit', 'travel', 'deductions'): False, - ('edit', 'travel', 'end_date'): False, - ('edit', 'travel', 'expenses'): False, - ('edit', 'travel', 'id'): False, - ('edit', 'travel', 'international_travel'): False, - ('edit', 'travel', 'itinerary'): False, - ('edit', 'travel', 'office'): False, - ('edit', 'travel', 'purpose'): False, - ('edit', 'travel', 'reference_number'): False, + ('edit', 'travel', 'deductions'): True, + ('edit', 'travel', 'end_date'): True, + ('edit', 'travel', 'expenses'): True, + ('edit', 'travel', 'id'): True, + ('edit', 'travel', 'international_travel'): True, + ('edit', 'travel', 'itinerary'): True, + ('edit', 'travel', 'office'): True, + ('edit', 'travel', 'purpose'): True, + ('edit', 'travel', 'reference_number'): True, ('edit', 'travel', 'report'): True, - ('edit', 'travel', 'section'): False, - ('edit', 'travel', 'start_date'): False, - ('edit', 'travel', 'status'): False, - ('edit', 'travel', 'supervisor'): False, - ('edit', 'travel', 'ta_required'): False, + ('edit', 'travel', 'section'): True, + ('edit', 'travel', 'start_date'): True, + ('edit', 'travel', 'status'): True, + ('edit', 'travel', 'supervisor'): True, + ('edit', 'travel', 'ta_required'): True, ('edit', 'travel', 'traveler'): False, ('view', 'action_points', 'action_point_number'): True, ('view', 'action_points', 'actions_taken'): True, From f6e928c88ac146f7a14a0e52ca0926494064f20a Mon Sep 17 00:00:00 2001 From: Greg Reinbach Date: Tue, 5 Mar 2019 05:46:53 -0500 Subject: [PATCH 69/72] Validate overlapping dates in Approved, and Completed status as well --- src/etools/applications/t2f/serializers/travel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/etools/applications/t2f/serializers/travel.py b/src/etools/applications/t2f/serializers/travel.py index 2a035d7cb5..1b010593d5 100644 --- a/src/etools/applications/t2f/serializers/travel.py +++ b/src/etools/applications/t2f/serializers/travel.py @@ -205,7 +205,7 @@ def validate(self, attrs): if 'mode_of_travel' in attrs and attrs['mode_of_travel'] is None: attrs['mode_of_travel'] = [] - if self.transition_name == Travel.SUBMIT_FOR_APPROVAL: + if self.transition_name in [Travel.SUBMIT_FOR_APPROVAL, Travel.APPROVED, Travel.COMPLETED]: traveler = attrs.get('traveler', None) if not traveler and self.instance: traveler = self.instance.traveler From 4de78c8363705124d9e26ee0b84686eda211d8c0 Mon Sep 17 00:00:00 2001 From: Greg Reinbach Date: Wed, 6 Mar 2019 05:36:14 -0500 Subject: [PATCH 70/72] Allow start/end dates to match current itineraries --- .../applications/t2f/serializers/travel.py | 13 +- .../t2f/tests/test_overlapping_trips.py | 136 ++++++++++++++++-- 2 files changed, 138 insertions(+), 11 deletions(-) diff --git a/src/etools/applications/t2f/serializers/travel.py b/src/etools/applications/t2f/serializers/travel.py index 1b010593d5..ba82781d25 100644 --- a/src/etools/applications/t2f/serializers/travel.py +++ b/src/etools/applications/t2f/serializers/travel.py @@ -1,3 +1,4 @@ +import datetime import operator from itertools import chain @@ -218,7 +219,17 @@ def validate(self, attrs): # or end date between the range of the start and end date of the current trip travel_q = Q(traveler=traveler) travel_q &= ~Q(status__in=[Travel.PLANNED, Travel.CANCELLED]) - travel_q &= Q(start_date__range=(start_date, end_date)) | Q(end_date__range=(start_date, end_date)) + travel_q &= Q( + start_date__date__range=( + start_date.date(), + end_date.date() - datetime.timedelta(days=1), + ) + ) | Q( + end_date__date__range=( + start_date.date() + datetime.timedelta(days=1), + end_date.date(), + ) + ) # In case of first save, no id present if self.instance: diff --git a/src/etools/applications/t2f/tests/test_overlapping_trips.py b/src/etools/applications/t2f/tests/test_overlapping_trips.py index 0b0bdc7d7d..8fd6ff995b 100644 --- a/src/etools/applications/t2f/tests/test_overlapping_trips.py +++ b/src/etools/applications/t2f/tests/test_overlapping_trips.py @@ -1,7 +1,6 @@ - +import datetime import json import logging -from datetime import datetime from django.urls import reverse @@ -40,8 +39,8 @@ def setUpTestData(cls): cls.travel = TravelFactory(reference_number=make_travel_reference_number(), traveler=cls.traveler, supervisor=cls.unicef_staff, - start_date=datetime(2017, 4, 4, 12, 00, tzinfo=UTC), - end_date=datetime(2017, 4, 14, 16, 00, tzinfo=UTC)) + start_date=datetime.datetime(2017, 4, 4, 12, 00, tzinfo=UTC), + end_date=datetime.datetime(2017, 4, 14, 16, 00, tzinfo=UTC)) ItineraryItemFactory(travel=cls.travel) ItineraryItemFactory(travel=cls.travel) cls.travel.submit_for_approval() @@ -86,7 +85,7 @@ def test_overlapping_trips(self): travel_id = response_json['id'] - with freeze_time(datetime(2017, 4, 14, 16, 00, tzinfo=UTC)): + with freeze_time(datetime.datetime(2017, 4, 14, 16, 00, tzinfo=UTC)): response = self.forced_auth_req('post', reverse('t2f:travels:details:state_change', kwargs={'travel_pk': travel_id, 'transition_name': Travel.SUBMIT_FOR_APPROVAL}), @@ -98,7 +97,7 @@ def test_overlapping_trips(self): def test_almost_overlapping_trips(self): currency = PublicsCurrencyFactory() - dsa_rate = PublicsDSARateFactory(effective_from_date=datetime(2017, 4, 10, 16, 00, tzinfo=UTC)) + dsa_rate = PublicsDSARateFactory(effective_from_date=datetime.datetime(2017, 4, 10, 16, 00, tzinfo=UTC)) dsa_region = dsa_rate.region data = {'itinerary': [{'origin': 'Berlin', @@ -133,7 +132,7 @@ def test_almost_overlapping_trips(self): data=data, user=self.traveler) response_json = json.loads(response.rendered_content) - with freeze_time(datetime(2017, 4, 14, 16, 00, tzinfo=UTC)): + with freeze_time(datetime.datetime(2017, 4, 14, 16, 00, tzinfo=UTC)): response = self.forced_auth_req('post', reverse('t2f:travels:details:state_change', kwargs={'travel_pk': response_json['id'], 'transition_name': Travel.SUBMIT_FOR_APPROVAL}), @@ -178,7 +177,7 @@ def test_edit_to_overlap(self): data=data, user=self.traveler) response_json = json.loads(response.rendered_content) - with freeze_time(datetime(2017, 4, 14, 16, 00, tzinfo=UTC)): + with freeze_time(datetime.datetime(2017, 4, 14, 16, 00, tzinfo=UTC)): response = self.forced_auth_req('post', reverse('t2f:travels:details:state_change', kwargs={'travel_pk': response_json['id'], 'transition_name': Travel.SUBMIT_FOR_APPROVAL}), @@ -220,17 +219,18 @@ def test_edit_to_overlap(self): 'transition_name': Travel.SUBMIT_FOR_APPROVAL}), data=response_json, user=self.traveler) response_json = json.loads(response.rendered_content) + assert response.status_code == 400 self.assertEqual(response_json, {'non_field_errors': ['You have an existing trip with overlapping dates. ' 'Please adjust your trip accordingly.']}) def test_daylight_saving(self): budapest_tz = pytz.timezone('Europe/Budapest') - self.travel.end_date = budapest_tz.localize(datetime(2017, 10, 29, 2, 0), is_dst=True) + self.travel.end_date = budapest_tz.localize(datetime.datetime(2017, 10, 29, 2, 0), is_dst=True) self.travel.save() # Same date as the previous, but it's already after daylight saving - start_date = budapest_tz.localize(datetime(2017, 10, 29, 2, 0), is_dst=False).isoformat() + start_date = budapest_tz.localize(datetime.datetime(2017, 10, 29, 2, 0), is_dst=False).isoformat() currency = PublicsCurrencyFactory() dsa_region = PublicsDSARegionFactory() @@ -266,3 +266,119 @@ def test_daylight_saving(self): response = self.forced_auth_req('post', reverse('t2f:travels:list:index'), data=data, user=self.traveler) self.assertEqual(response.status_code, 201) + + def test_start_end_match(self): + # the new itinerary start date matches the end date of a + # current itinerary + currency = PublicsCurrencyFactory() + dsa_region = PublicsDSARegionFactory() + + data = {'itinerary': [{'origin': 'Berlin', + 'destination': 'Budapest', + 'departure_date': '2017-04-14T17:06:55.821490', + 'arrival_date': '2017-04-20T17:06:55.821490', + 'dsa_region': dsa_region.id, + 'overnight_travel': False, + 'mode_of_travel': ModeOfTravel.RAIL, + 'airlines': []}, + {'origin': 'Budapest', + 'destination': 'Berlin', + 'departure_date': '2017-05-20T12:06:55.821490', + 'arrival_date': '2017-05-21T12:06:55.821490', + 'dsa_region': dsa_region.id, + 'overnight_travel': False, + 'mode_of_travel': ModeOfTravel.RAIL, + 'airlines': []}], + 'activities': [], + 'ta_required': True, + 'international_travel': False, + 'mode_of_travel': [ModeOfTravel.BOAT], + 'traveler': self.traveler.id, + 'supervisor': self.unicef_staff.id, + 'start_date': '2017-04-14T15:06:55+01:00', + 'end_date': '2017-05-22T15:02:13+01:00', + 'currency': currency.id, + 'purpose': 'Purpose', + 'additional_note': 'Notes'} + + response = self.forced_auth_req( + 'post', + reverse('t2f:travels:list:index'), + data=data, + user=self.traveler, + ) + response_json = json.loads(response.rendered_content) + + travel_id = response_json['id'] + + response = self.forced_auth_req( + 'post', + reverse( + 't2f:travels:details:state_change', + kwargs={ + 'travel_pk': travel_id, + 'transition_name': Travel.SUBMIT_FOR_APPROVAL, + } + ), + data=response_json, + user=self.traveler, + ) + assert response.status_code == 200 + + def test_end_start_match(self): + # the new itinerary end date matches the start date + # of a current itinerary + currency = PublicsCurrencyFactory() + dsa_region = PublicsDSARegionFactory() + + data = {'itinerary': [{'origin': 'Berlin', + 'destination': 'Budapest', + 'departure_date': '2017-03-14T17:06:55.821490', + 'arrival_date': '2017-03-20T17:06:55.821490', + 'dsa_region': dsa_region.id, + 'overnight_travel': False, + 'mode_of_travel': ModeOfTravel.RAIL, + 'airlines': []}, + {'origin': 'Budapest', + 'destination': 'Berlin', + 'departure_date': '2017-03-25T12:06:55.821490', + 'arrival_date': '2017-04-04T12:06:55.821490', + 'dsa_region': dsa_region.id, + 'overnight_travel': False, + 'mode_of_travel': ModeOfTravel.RAIL, + 'airlines': []}], + 'activities': [], + 'ta_required': True, + 'international_travel': False, + 'mode_of_travel': [ModeOfTravel.BOAT], + 'traveler': self.traveler.id, + 'supervisor': self.unicef_staff.id, + 'start_date': '2017-03-14T15:06:55+01:00', + 'end_date': '2017-04-04T15:02:13+01:00', + 'currency': currency.id, + 'purpose': 'Purpose', + 'additional_note': 'Notes'} + + response = self.forced_auth_req( + 'post', + reverse('t2f:travels:list:index'), + data=data, + user=self.traveler, + ) + response_json = json.loads(response.rendered_content) + + travel_id = response_json['id'] + + response = self.forced_auth_req( + 'post', + reverse( + 't2f:travels:details:state_change', + kwargs={ + 'travel_pk': travel_id, + 'transition_name': Travel.SUBMIT_FOR_APPROVAL, + } + ), + data=response_json, + user=self.traveler, + ) + assert response.status_code == 200 From b38a05265c028a80a6a6704f94208b4656006f21 Mon Sep 17 00:00:00 2001 From: Greg Reinbach Date: Wed, 6 Mar 2019 09:52:08 -0500 Subject: [PATCH 71/72] Don't prepend to settings.HOST for email links --- src/etools/applications/partners/tasks.py | 2 +- src/etools/applications/partners/tests/test_tasks.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/etools/applications/partners/tasks.py b/src/etools/applications/partners/tasks.py index 7384ad379e..8f7631129b 100644 --- a/src/etools/applications/partners/tasks.py +++ b/src/etools/applications/partners/tasks.py @@ -41,7 +41,7 @@ def get_intervention_context(intervention): 'number': str(intervention), 'partner': intervention.agreement.partner.name, 'start_date': str(intervention.start), - 'url': 'https://{}/pmp/interventions/{}/details'.format(settings.HOST, intervention.id), + 'url': '{}/pmp/interventions/{}/details'.format(settings.HOST, intervention.id), 'unicef_focal_points': [focal_point.email for focal_point in intervention.unicef_focal_points.all()] } diff --git a/src/etools/applications/partners/tests/test_tasks.py b/src/etools/applications/partners/tests/test_tasks.py index 4553a19170..c58735ae7a 100644 --- a/src/etools/applications/partners/tests/test_tasks.py +++ b/src/etools/applications/partners/tests/test_tasks.py @@ -68,7 +68,7 @@ def test_simple_intervention(self): self.assertEqual(result['partner'], self.intervention.agreement.partner.name) self.assertEqual(result['start_date'], 'None') self.assertEqual(result['url'], - 'https://{}/pmp/interventions/{}/details'.format(settings.HOST, self.intervention.id)) + '{}/pmp/interventions/{}/details'.format(settings.HOST, self.intervention.id)) self.assertEqual(result['unicef_focal_points'], []) def test_non_trivial_intervention(self): @@ -88,7 +88,7 @@ def test_non_trivial_intervention(self): self.assertEqual(result['partner'], self.intervention.agreement.partner.name) self.assertEqual(result['start_date'], '2017-08-01') self.assertEqual(result['url'], - 'https://{}/pmp/interventions/{}/details'.format(settings.HOST, self.intervention.id)) + '{}/pmp/interventions/{}/details'.format(settings.HOST, self.intervention.id)) self.assertEqual(result['unicef_focal_points'], [self.focal_point_user.email]) From 13a4adf156fc65b2616d65bbddd9bfd581983b7c Mon Sep 17 00:00:00 2001 From: Greg Reinbach Date: Wed, 6 Mar 2019 14:36:51 -0500 Subject: [PATCH 72/72] Add check_trip_dates condition to mark_as_completed transition --- src/etools/applications/t2f/models.py | 29 ++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/src/etools/applications/t2f/models.py b/src/etools/applications/t2f/models.py index 786630a1a2..f59d7a5e3f 100644 --- a/src/etools/applications/t2f/models.py +++ b/src/etools/applications/t2f/models.py @@ -1,9 +1,11 @@ +import datetime import logging from decimal import Decimal from django.conf import settings from django.contrib.postgres.fields.array import ArrayField from django.db import connection, models +from django.db.models import Q from django.db.models.signals import post_save from django.dispatch import receiver from django.utils.timezone import now as timezone_now @@ -177,6 +179,31 @@ def check_trip_report(self): raise TransitionError('Field report has to be filled.') return True + def check_trip_dates(self): + if self.start_date and self.end_date: + start_date = self.start_date.date() + end_date = self.end_date.date() + travel_q = Q(traveler=self.traveler) + travel_q &= ~Q(status__in=[Travel.PLANNED, Travel.CANCELLED]) + travel_q &= Q( + start_date__date__range=( + start_date, + end_date - datetime.timedelta(days=1), + ) + ) | Q( + end_date__date__range=( + start_date + datetime.timedelta(days=1), + end_date, + ) + ) + travel_q &= ~Q(pk=self.pk) + if Travel.objects.filter(travel_q).exists(): + raise TransitionError( + 'You have an existing trip with overlapping dates. ' + 'Please adjust your trip accordingly.' + ) + return True + def check_state_flow(self): # Complete action should be called only after certification was done. # Special case is the TA not required NOT international travel, where supervisor should be able to complete it @@ -245,7 +272,7 @@ def plan(self): pass @transition(status, source=[SUBMITTED, APPROVED, PLANNED, CANCELLED], target=COMPLETED, - conditions=[check_trip_report, check_state_flow]) + conditions=[check_trip_report, check_trip_dates, check_state_flow]) def mark_as_completed(self): self.completed_at = timezone_now() if not self.ta_required and self.status == self.PLANNED: