diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..81c2bdc --- /dev/null +++ b/.flake8 @@ -0,0 +1,10 @@ +[flake8] +exclude = */docs/*,*/.tox/*,*/.venv/*,*/.pycharm_helpers/*,*/migrations/*,docs/*,fabfile.py,*/__init__.py,secret.py +# PEP standard wrap line length +max-line-length = 79 + +# E12x continuation line indentation +# E251 no spaces around keyword / parameter equals +# E303 too many blank lines (3) +# F405 name may be undefined, or defined from star imports: module +ignore = E125,E126,E251,E303,F405 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 9e2b1f5..d2569fc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,10 @@ -__pycache__ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] *.py[co] -.cache +*$py.class + +# dev local configs .project .pydevproject .vscode @@ -8,11 +12,120 @@ __pycache__ *.db *.orig *.DS_Store +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ .coverage -.tox -db.sqlite3 -*.egg-info/* -docs/_build/* -dist/* -build/* -venv +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# dotenv +.env + +# virtualenv +.venv +venv/ +ENV/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ + +# Pycharm +.idea + +# Pg cluster for docker +deployment/pg/postgres_data/9.3 +deployment/pg/postgres_staging_data/9.3 +deployment/pg/postgres_dev_data/9.3 +deployment/sql/td_biblio-old.sql +# Template for apt-cacher +# static and media dirs +deployment/static +deployment/media +deployment/docker/71-apt-cacher-ng +core/settings/secret.py +.idea/ +REQUIREMENTS-dev.txt +REQUIREMENTS.txt +core/ +deployment/ +manage.py diff --git a/.travis.yml b/.travis.yml index d89992b..e66e56c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,13 +4,26 @@ python: - 3.4 - 3.5 - 3.6 +services: + - postgresql + env: - - DJANGO_RELEASE='Django>=1.8,<1.9' - - DJANGO_RELEASE='Django>=1.9,<1.10' - - DJANGO_RELEASE='Django>=1.10,<1.11' - - DJANGO_RELEASE='Django>=1.11,<1.12' - - DJANGO_RELEASE='Django>=2.0,<2.1' - - DJANGO_RELEASE='Django>=2.1,<2.2' + global: + - ON_TRAVIS=true + - DATABASE_URL='postgres://postgres:@localhost:5432/test_db' + - SECRET_KEY='tT\xd7\xb06\xf7\x9b\xff\x0fZL\xca\xca\x11\xefM\xacr\xfb\xdf\xca\x9b' + - DJANGO_SETTINGS_MODULE=core.settings.test_travis + - RABBITMQ_HOST='rabbitmq' + - DJANGO_RELEASE='Django>=1.8,<1.9' + - DJANGO_RELEASE='Django>=1.9,<1.10' + - DJANGO_RELEASE='Django>=1.10,<1.11' + - DJANGO_RELEASE='Django>=1.11,<1.12' + - DJANGO_RELEASE='Django>=2.0,<2.1' + - DJANGO_RELEASE='Django>=2.1,<2.2' + +sudo: false +dist: trusty + matrix: exclude: ### These older Django version don't support Python 3.5+: @@ -27,10 +40,32 @@ matrix: # Django 2.1+ dropped support for python 3.4 - python: 3.4 env: DJANGO_RELEASE='Django>=2.1,<2.2' + +addons: + postgresql: "9.3" + apt: + packages: + - postgresql-9.3-postgis-2.3 + install: - pip install "$DJANGO_RELEASE" - - pip install -r requirements/dev.txt + - pip install coveralls + - pip install -r REQUIREMENTS-dev.txt + - nodeenv -p --node=0.10.31 + - npm -g install yuglify + +before_script: + - psql -c 'create database test_db;' -U postgres + - psql -c 'CREATE EXTENSION postgis;' -U postgres -d test_db + script: - - flake8 td_biblio/ - - PYTHONPATH=$(pwd) DATABASE_URL="sqlite:///:memory:" py.test + - flake8 --config .flake8 . + - python manage.py makemigrations + - python manage.py migrate + - python manage.py collectstatic --noinput --verbosity 0 + - coverage run manage.py test + # - flake8 td_biblio/ + # - PYTHONPATH=$(pwd) DATABASE_URL="sqlite:///:memory:" py.test after_success: coveralls + + diff --git a/MANIFEST.in b/MANIFEST.in index f78c6c8..599f200 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -7,8 +7,9 @@ include requirements.txt include requirements/base.txt include requirements/dev.txt include requirements/heroku.txt -recursive-include sandbox * +recursive-include core * recursive-include docs * recursive-include td_biblio/static * recursive-include td_biblio/templates/td_biblio * recursive-include td_biblio/tests/fixtures * + diff --git a/Makefile b/Makefile index f140379..38b0792 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,13 @@ +PROJECT_ID := td-biblio +SHELL := /bin/bash + default: help +# ---------------------------------------------------------------------------- +# S E T U P C O M M A N D S +# ---------------------------------------------------------------------------- + + VENV_NAME = venv VENV_PATH = $(shell pwd)/$(VENV_NAME) PIP = $(VIRTUAL_ENV) $(VENV_PATH)/bin/pip @@ -24,16 +32,346 @@ dev: venv ## start the development server @$(MANAGE) runserver .PHONY: dev -migrate: venv ## perform database migrations - @$(MANAGE) migrate -.PHONY: migrate - test: venv ## run the test suite @$(TESTS_DATABASE_URL) $(PYTEST) .PHONY: test + +setup: + @echo + @echo "------------------------------------------------------------------" + @echo "Building dist file " + @echo "------------------------------------------------------------------" + @python setup.py sdist + +# ---------------------------------------------------------------------------- +# P R O D U C T I O N C O M M A N D S +# ---------------------------------------------------------------------------- +default: web +run: build permissions web migrate collectstatic + +deploy: run + @echo + @echo "------------------------------------------------------------------" + @echo "Bringing up fresh instance " + @echo "You can access it on http://localhost:63200" + @echo "------------------------------------------------------------------" + +build: + @echo + @echo "------------------------------------------------------------------" + @echo "Building in production mode" + @echo "------------------------------------------------------------------" + @docker-compose -f deployment/docker-compose.yml -p $(PROJECT_ID) build uwsgi + +web: + @echo + @echo "------------------------------------------------------------------" + @echo "Running in production mode" + @echo "------------------------------------------------------------------" + @docker-compose -f deployment/docker-compose.yml -p $(PROJECT_ID) up -d web + @# Dont confuse this with the dbbackup make command below + @# This one runs the postgis-backup cron container + @# We add --no-recreate so that it does not destroy & recreate the db container + @docker-compose -f deployment/docker-compose.yml -p $(PROJECT_ID) up --no-recreate --no-deps -d dbbackups + +permissions: + # Probably we want something more granular here.... + # Your sudo password will be needed to set the file permissions + # on logs, media, static and pg dirs + @if [ ! -d "logs" ]; then mkdir logs; fi + @if [ ! -d "media" ]; then mkdir media; fi + @if [ ! -d "static" ]; then mkdir static; fi + @if [ ! -d "backups" ]; then mkdir backups; fi + @if [ -d "logs" ]; then sudo chmod -R a+rwx logs; fi + @if [ -d "media" ]; then sudo chmod -R a+rwx media; fi + @if [ -d "static" ]; then sudo chmod -R a+rwx static; fi + @if [ -d "pg" ]; then sudo chmod -R a+rwx pg; fi + @if [ -d "backups" ]; then sudo chmod -R a+rwx backups; fi + +db: + @echo + @echo "------------------------------------------------------------------" + @echo "Running db in production mode" + @echo "------------------------------------------------------------------" + @docker-compose -f deployment/docker-compose.yml -p $(PROJECT_ID) up -d db + +nginx: + @echo + @echo "------------------------------------------------------------------" + @echo "Running nginx in production mode" + @echo "Normally you should use this only for testing" + @echo "In a production environment you will typically use nginx running" + @echo "on the host rather if you have a multi-site host." + @echo "------------------------------------------------------------------" + @docker-compose -f deployment/docker-compose.yml -p $(PROJECT_ID) up -d nginx + @echo "Site should now be available at http://localhost" + +migrate: + @echo + @echo "------------------------------------------------------------------" + @echo "Running migrate static in production mode" + @echo "------------------------------------------------------------------" + @#http://stackoverflow.com/questions/29689365/auth-user-error-with-django-1-8-and-syncdb-migrate + @#and + @#http://stackoverflow.com/questions/3143635/how-to-ignore-mv-error + @# We add the '-' prefix to the next line as the migration may fail + @# but we want to continue anyway. + @docker-compose -f deployment/docker-compose.yml -p $(PROJECT_ID) run uwsgi python manage.py migrate + +update-migrations: + @echo + @echo "------------------------------------------------------------------" + @echo "Running update migrations in production mode" + @echo "------------------------------------------------------------------" + @docker-compose -f deployment/docker-compose.yml -p $(PROJECT_ID) run uwsgi python manage.py makemigrations + +collectstatic: + @echo + @echo "------------------------------------------------------------------" + @echo "Collecting static in production mode" + @echo "------------------------------------------------------------------" + #@docker-compose -f deployment/docker-compose.yml -p $(PROJECT_ID) run uwsgi python manage.py collectstatic --noinput + #We need to run collect static in the same context as the running + # uwsgi container it seems so I use docker exec here + # no -it flag so we can run over remote shell + @docker exec $(PROJECT_ID)-uwsgi python manage.py collectstatic --noinput + +reload: + @echo + @echo "------------------------------------------------------------------" + @echo "Reload django project in production mode" + @echo "------------------------------------------------------------------" + # no -it flag so we can run over remote shell + @docker exec $(PROJECT_ID)-uwsgi uwsgi --reload /tmp/django.pid + +kill: + @echo + @echo "------------------------------------------------------------------" + @echo "Killing in production mode" + @echo "------------------------------------------------------------------" + @docker-compose -f deployment/docker-compose.yml -p $(PROJECT_ID) kill + +rm: dbbackup rm-only + + +rm-only: kill + @echo + @echo "------------------------------------------------------------------" + @echo "Removing production instance!!! " + @echo "------------------------------------------------------------------" + @docker-compose -f deployment/docker-compose.yml -p $(PROJECT_ID) rm + +logs: + @echo + @echo "------------------------------------------------------------------" + @echo "Showing uwsgi logs in production mode" + @echo "------------------------------------------------------------------" + @docker-compose -f deployment/docker-compose.yml -p $(PROJECT_ID) logs uwsgi + +dblogs: + @echo + @echo "------------------------------------------------------------------" + @echo "Showing db logs in production mode" + @echo "------------------------------------------------------------------" + @docker-compose -f deployment/docker-compose.yml -p $(PROJECT_ID) logs db + +nginxlogs: + @echo + @echo "------------------------------------------------------------------" + @echo "Showing nginx logs in production mode" + @echo "------------------------------------------------------------------" + @docker-compose -f deployment/docker-compose.yml -p $(PROJECT_ID) logs web + +shell: + @echo + @echo "------------------------------------------------------------------" + @echo "Shelling in in production mode" + @echo "------------------------------------------------------------------" + @docker-compose -f deployment/docker-compose.yml -p $(PROJECT_ID) run uwsgi /bin/bash + +superuser: + @echo + @echo "------------------------------------------------------------------" + @echo "Creating a superuser in production mode" + @echo "------------------------------------------------------------------" + @docker-compose -f deployment/docker-compose.yml -p $(PROJECT_ID) run uwsgi python manage.py createsuperuser + +dbbash: + @echo + @echo "------------------------------------------------------------------" + @echo "Bashing in to production database" + @echo "------------------------------------------------------------------" + @docker exec -t -i $(PROJECT_ID)-db /bin/bash + +dbsnapshot: + @echo + @echo "------------------------------------------------------------------" + @echo "Grab a quick snapshot of the database and place in the host filesystem" + @echo "------------------------------------------------------------------" + @docker exec -t -i $(PROJECT_ID)-db /bin/bash -c "PGPASSWORD=docker pg_dump -Fc -h localhost -U docker -f /tmp/$(PROJECT_ID)-snapshot.dmp gis" + @docker cp $(PROJECT_ID)-db:/tmp/$(PROJECT_ID)-snapshot.dmp . + @docker exec -t -i $(PROJECT_ID)-db /bin/bash -c "rm /tmp/$(PROJECT_ID)-snapshot.dmp" + @ls -lahtr *.dmp + +dbschema: + @echo + @echo "------------------------------------------------------------------" + @echo "Print the database schema to stdio" + @echo "------------------------------------------------------------------" + @docker exec -t -i $(PROJECT_ID)-db /bin/bash -c "PGPASSWORD=docker pg_dump -s -h localhost -U docker gis" + +dbshell: + @echo + @echo "------------------------------------------------------------------" + @echo "Shelling in in production database" + @echo "------------------------------------------------------------------" + @docker exec -t -i $(PROJECT_ID)-db psql -U docker -h localhost gis + +dbrestore: + @echo + @echo "------------------------------------------------------------------" + @echo "Restore dump from backups/latest.dmp in production mode" + @echo "------------------------------------------------------------------" + @# - prefix causes command to continue even if it fails + -@docker exec -t -i $(PROJECT_ID)-db su - postgres -c "dropdb gis" + @docker exec -t -i $(PROJECT_ID)-db su - postgres -c "createdb -O docker -T template_postgis gis" + @docker exec -t -i $(PROJECT_ID)-db pg_restore /backups/latest.dmp | docker exec -i $(PROJECT_ID)-db su - postgres -c "psql gis" + +db-fresh-restore: + @echo + @echo "------------------------------------------------------------------" + @echo "Restore dump from backups/latest.dmp in production mode" + @echo "------------------------------------------------------------------" + -@docker exec -t -i $(PROJECT_ID)-db su - postgres -c "dropdb gis" + @docker exec -t -i $(PROJECT_ID)-db su - postgres -c "createdb -O docker -T template_postgis gis" + @docker exec -t -i $(PROJECT_ID)-db su - postgres -c "psql gis -f + /sql/td_biblio-old.sql" + @docker exec -t -i $(PROJECT_ID)-db su - postgres -c "psql gis -f /sql/migration.sql" + +dbbackup: + @echo + @echo "------------------------------------------------------------------" + @echo "Create `date +%d-%B-%Y`.dmp in production mode" + @echo "Warning: backups/latest.dmp will be replaced with a symlink to " + @echo "the new backup." + @echo "------------------------------------------------------------------" + @# - prefix causes command to continue even if it fails + @# Explicitly don't use -it so we can call this make target over a remote ssh session + @docker exec $(PROJECT_ID)-db-backups /backups.sh + @docker exec $(PROJECT_ID)-db-backups cat /var/log/cron.log | tail -2 | head -1 | awk '{print $4}' + -@if [ -f "backups/latest.dmp" ]; then rm backups/latest.dmp; fi + # backups is intentionally missing from front of first clause below otherwise symlink comes + # out with wrong path... + @ln -s `date +%Y`/`date +%B`/PG_$(PROJECT_ID)_gis.`date +%d-%B-%Y`.dmp backups/latest.dmp + @echo "Backup should be at: backups/`date +%Y`/`date +%B`/PG_$(PROJECT_ID)_gis.`date +%d-%B-%Y`.dmp" + +maillogs: + @echo + @echo "------------------------------------------------------------------" + @echo "Showing smtp logs in production mode" + @echo "------------------------------------------------------------------" + @docker exec -t -i $(PROJECT_ID)-smtp tail -f /var/log/mail.log + +mailerrorlogs: + @echo + @echo "------------------------------------------------------------------" + @echo "Showing smtp error logs in production mode" + @echo "------------------------------------------------------------------" + @docker exec -t -i $(PROJECT_ID)-smtp tail -f /var/log/mail.err + +create-machine: + @echo + @echo "------------------------------------------------------------------" + @echo "Creating a docker machine." + @echo "------------------------------------------------------------------" + @docker-machine create -d virtualbox $(PROJECT_ID) + +enable-machine: + @echo + @echo "------------------------------------------------------------------" + @echo "Enabling docker machine." + @echo "------------------------------------------------------------------" + @echo "eval \"$(docker-machine env freshwater)\"" + +gruntserver: + @echo + @echo "------------------------------------------------------------------" + @echo "Run grunt" + @echo "------------------------------------------------------------------" + @docker-compose -f deployment/docker-compose.yml -p $(PROJECT_ID) run uwsgi python manage.py gruntserver + +# ---------------------------------------------------------------------------- +# DEVELOPMENT C O M M A N D S +# --no-deps will attach to prod deps if running +# after running you will have ssh and web ports open (see dockerfile for no's) +# and you can set your pycharm to use the python in the container +# Note that pycharm will copy in resources to the /root/ user folder +# for pydevd etc. If they dont get copied, restart pycharm... +# ---------------------------------------------------------------------------- + +devweb: db + @echo + @echo "------------------------------------------------------------------" + @echo "Running in DEVELOPMENT mode" + @echo "------------------------------------------------------------------" + @docker-compose -f deployment/docker-compose.yml -p $(PROJECT_ID) up --no-deps -d devweb + +build-devweb: db + @echo + @echo "------------------------------------------------------------------" + @echo "Building devweb" + @echo "------------------------------------------------------------------" + @docker-compose -f deployment/docker-compose.yml -p $(PROJECT_ID) build devweb + +# Run pep8 style checking +#http://pypi.python.org/pypi/pep8 +pep8: + @echo + @echo "-----------" + @echo "PEP8 issues" + @echo "-----------" + @pep8 --version + @pep8 --repeat --ignore=E203,E121,E122,E123,E124,E125,E126,E127,E128,E402 --exclude='../django_project/.pycharm_helpers','../django_project/*/migrations/','../django_project/*/urls.py','../django_project/core/settings/secret.py' ../django_project || true + + +# --------------- help -------------------------------- + help: - @echo "// Django TailorDev Biblio" + @echo "Django TailorDev Biblio" @echo "" - @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' -.PHONY: help + @echo "* **build** - builds all required containers." + @echo "* **build-devweb** - build the development container. See [development notes](README-dev.md)." + @echo "* **collectstatic** - run the django collectstatic command." + @echo "* **create-machine** ." + @echo "* **db** - build and run the db container." + @echo "* **dbbackup** - make a snapshot of the database, saving it to deployments/backups/YYYY/MM/project-DDMMYYYY.dmp. It also creates a symlink to backups/latest.dmp for the latest backup." + @echo "* **dbbash** - open a bash shell inside the database container." + @echo "* **dblogs** - view the database logs." + @echo "* **dbrestore** - restore deployment/backups/latest.dmp over the active database. Will delete any existing data in your database and replace with the restore, so **use with caution**." + @echo "* **dbschema** - dump the current db schema (without data) to stdio. Useful if you want to compare changes between instances." + @echo "* **dbshell** - get a psql prompt into the db container. " + @echo "* **dbsnapshot** - as above but makes the backup as deployment/snapshot.smp - replacing any pre-existing snapshot." + @echo "* **dbsync** - use this from a development or offsite machine. It will rsync all database backups from deployment/backups to your offsite machine." + @echo "* **default** ." + @echo "* **deploy** ." + @echo "* **devweb** - create an ssh container derived from uwsgi that can be used as a remote interpreter for PyCharm. See [development notes](README-dev.md)." + @echo "* **enable-machine** - " + @echo "* **kill** - kills all running containers. Does not remove them." + @echo "* **logs** - view the logs of all running containers. Note that you can also view individual logs in the deployment/logs directory." + @echo "* **mailerrorlogs** - View the error logs from the mail server." + @echo "* **maillogs** - view the transaction logs from the mail server." + @echo "* **mediasync** - use this from a development or offsite machine. It will rsync all media backups from deployment/media to your offsite machine." + @echo "* **migrate** - run any pending migrations. " + @echo "* **nginx** - builds and runs the nginx container." + @echo "* **nginxlogs** - view just the nginx activity logs." + @echo "* **permissions** - Update the permissions of shared volumes. Note this will destroy any existing permissions you have in place." + @echo "* **reload** - reload the uwsgi process. Useful when you need django to pick up any changes you may have deployed." + @echo "* **rm** - remove all containers." + @echo "* **rm-only** - remove any containers without trying to kill them first. " + @echo "* **run** - builds and runs the complete orchestrated set of containers." + @echo "* **shell** - open a bash shell in the uwsgi (where django runs) container." + @echo "* **superuser** - create a django superuser account." + @echo "* **update-migrations** - freshen all migration definitions to match the current code base." + @echo "* **web** - same as **run** - runs the production site." + @echo "* **pep8** - Run Python PEP8 check." \ No newline at end of file diff --git a/Procfile b/Procfile index ea7f85b..2a06672 100644 --- a/Procfile +++ b/Procfile @@ -1 +1 @@ -web: gunicorn sandbox.wsgi --log-file - +web: gunicorn core.wsgi --log-file - diff --git a/README.md b/README.md index cf21880..1d34f01 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,7 @@ login: rosalind password: idiscovereddnastructurefirst ``` +The latest source code is available at https://github.com/TailorDev/django-tailordev-biblio ## Installation ### Install `td_biblio` @@ -72,12 +73,72 @@ urlpatterns = [ ] ``` +### Run with docker locally + +Quick Installation Guide +------------------------ +Django TailorDev Biblio is a django app so it will help if you have +some knowledge of running a django site. + + git clone https://github.com/TailorDev/django-tailordev-biblio.git + + make build + + make permissions + + make web + + # Wait a few seconds for the DB to start before to do the next command + + make migrate + + make collectstatic + + +So as to create your admin account: +``` +make superuser +``` + + +Install as a Django Package +--------------------------- + +1. Add "td-biblio" to your INSTALLED_APPS setting like this: + + INSTALLED_APPS = [ + # other apps here + 'td-biblio', + ] + +2. Include the td-biblio URLconf in your project urls.py like this: + ```perforce + # for django >= 2.0 + path('bibliography/', include('fish.urls', namespace='td_biblio')), + ``` + + ```perforce + # for django <= 2.0 + url(r'^bibliography/', include('td_biblio.urls', namespace='td_biblio')), + ``` + +3. Run `python manage.py migrate` to create the bibliography models. + + And finally migrate your database from your project root path: ```bash $ python manage.py migrate td_biblio ``` +Or to migrate and collect static files with docker +```bash +$ make migrations && make collectstatic +``` + +_Nota bene:_ to use makefile you will need to have **make** installed on +your +computer ### Add a base template In order to use `td_biblio` templates, you will need to create a base template @@ -124,11 +185,14 @@ dependencies in a virtual environment via: $ make bootstrap ``` -And then start the development server via: +And then start the local development server via: ```bash -$ make dev +$ make devweb ``` +Or to run in production mode via: + +$ make web ### Running the Tests @@ -171,3 +235,13 @@ $ twine upload dist/* `django-tailordev-biblio` is released under the MIT License. See the bundled LICENSE file for details. + +Thank you +_________ + +Thank you to the individual contributors who have helped to build +Django-TailorDev-Biblio: + +* Julien Maupetit (Lead developer) +* Alison Mukoma: mukomalison@gmail.com :nerd_face: + diff --git a/REQUIREMENTS-dev.txt b/REQUIREMENTS-dev.txt new file mode 100644 index 0000000..2fb7271 --- /dev/null +++ b/REQUIREMENTS-dev.txt @@ -0,0 +1,3 @@ +-r deployment/docker/REQUIREMENTS.txt +-r deployment/docker/REQUIREMENTS-dev.txt +nodeenv==1.2.0 \ No newline at end of file diff --git a/REQUIREMENTS.txt b/REQUIREMENTS.txt new file mode 100644 index 0000000..5751b5c --- /dev/null +++ b/REQUIREMENTS.txt @@ -0,0 +1 @@ +-r deployment/docker/REQUIREMENTS.txt \ No newline at end of file diff --git a/core/__init__.py b/core/__init__.py new file mode 100644 index 0000000..8ec3a7d --- /dev/null +++ b/core/__init__.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- +"""Django settings configurations seperated by concern.""" + +__author__ = 'Alison Mukoma ' +__licence__ = 'MIT' +__date__ = '11/01/19' +__copywrite__ = 'tailordev.fr' diff --git a/sandbox/__init__.py b/core/settings/__init__.py similarity index 100% rename from sandbox/__init__.py rename to core/settings/__init__.py diff --git a/core/settings/base.py b/core/settings/base.py new file mode 100644 index 0000000..f06400d --- /dev/null +++ b/core/settings/base.py @@ -0,0 +1,172 @@ +# coding=utf-8 +""" +core.settings.base +""" +# Django settings for tailordev-biblio project. + +from .utils import absolute_path + +ADMINS = ( + ('Alison Mukoma', 'mukomalison@gmail.com'), +) + +MANAGERS = ADMINS + +# Local time zone for this installation. Choices can be found here: +# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name +# although not all choices may be available on all operating systems. +# In a Windows environment this must be set to your system time zone. +TIME_ZONE = 'UTC' + +# Language code for this installation. All choices can be found here: +# http://www.i18nguy.com/unicode/language-identifiers.html +LANGUAGE_CODE = 'en-us' + +SITE_ID = 1 + +# If you set this to False, Django will make some optimizations so as not +# to load the internationalization machinery. +USE_I18N = True + +# If you set this to False, Django will not format dates, numbers and +# calendars according to the current locale. +USE_L10N = True + +# If you set this to False, Django will not use timezone-aware datetimes. +USE_TZ = True + +# Absolute filesystem path to the directory that will hold user-uploaded files. +# Example: "/var/www/example.com/media/" +MEDIA_ROOT = '/home/web/media' + +# URL that handles the media served from MEDIA_ROOT. Make sure to use a +# trailing slash. +# Examples: "http://example.com/media/", "http://media.example.com/" +# MEDIA_URL = '/media/' +# setting full MEDIA_URL to be able to use it for the feeds +MEDIA_URL = '/media/' + +# Absolute path to the directory static files should be collected to. +# Don't put anything in this directory yourself; store your static files +# in apps' "static/" subdirectories and in STATICFILES_DIRS. +# Example: "/var/www/example.com/static/" +STATIC_ROOT = '/home/web/static' + +# URL prefix for static files. +# Example: "http://example.com/static/", "http://static.example.com/" +STATIC_URL = '/static/' + +# Additional locations of static files +STATICFILES_DIRS = ( + # Put strings here, like "/home/html/static" or "C:/www/django/static". + # Always use forward slashes, even on Windows. + # Don't forget to use absolute paths, not relative paths. + absolute_path('core', 'base_static'), + absolute_path('td_biblio', 'static'), +) + +# List of finder classes that know how to find static files in +# various locations. +STATICFILES_FINDERS = ( + 'django.contrib.staticfiles.finders.FileSystemFinder', + 'django.contrib.staticfiles.finders.AppDirectoriesFinder', + # 'django.contrib.staticfiles.finders.DefaultStorageFinder', +) + +# import SECRET_KEY into current namespace +# noinspection PyUnresolvedReferences +from .secret import SECRET_KEY # noqa + +INSTALLED_APPS = ( + # Django apps + 'django.contrib.admin', + 'django.contrib.admindocs', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.sites', + 'django.contrib.messages', + 'django.contrib.humanize', + 'django.contrib.sitemaps', + 'django.contrib.syndication', + 'django.contrib.staticfiles', + 'django.contrib.gis', +) + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [ + # project level templates + absolute_path('core', 'base_templates'), + absolute_path('td_biblio', 'templates'), + ], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + + # `allauth` needs this from django + 'django.template.context_processors.request', + ], + }, + }, +] + +MIDDLEWARE = ( + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +) + +# Django < 1.10 compat +MIDDLEWARE_CLASSES = ( + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.auth.middleware.SessionAuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", +) + +ROOT_URLCONF = 'core.urls' + +# Python dotted path to the WSGI application used by Django's runserver. +WSGI_APPLICATION = 'core.wsgi.application' + +# A sample logging configuration. The only tangible logging +# performed by this configuration is to send an email to +# the site admins on every HTTP 500 error when DEBUG=False. +# See http://docs.djangoproject.com/en/dev/topics/logging for +# more details on how to customize your logging configuration. +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'filters': { + 'require_debug_false': { + '()': 'django.utils.log.RequireDebugFalse' + } + }, + 'handlers': { + 'mail_admins': { + 'level': 'ERROR', + 'filters': ['require_debug_false'], + 'class': 'django.utils.log.AdminEmailHandler' + } + }, + 'loggers': { + 'django.request': { + 'handlers': ['mail_admins'], + 'level': 'ERROR', + 'propagate': True, + }, + } +} diff --git a/core/settings/celery_settings.py b/core/settings/celery_settings.py new file mode 100644 index 0000000..db44f0a --- /dev/null +++ b/core/settings/celery_settings.py @@ -0,0 +1,9 @@ + +CELERYBEAT_SCHEDULE = { + 'import_reference_records': { + 'task': 'tasks.import_reference_record', + 'schedule': 3600.0, # update every 60 minutes + } +} + +CELERY_TIMEZONE = 'UTC' diff --git a/core/settings/contrib.py b/core/settings/contrib.py new file mode 100644 index 0000000..c87594e --- /dev/null +++ b/core/settings/contrib.py @@ -0,0 +1,98 @@ +# coding=utf-8 +""" +core.settings.contrib +""" +from .base import * # noqa +from .celery_settings import * # noqa +import os + +try: + from django.core.urlresolvers import reverse_lazy +except Exception as e: + from django.urls import reverse_lazy + +import dj_database_url + +STOP_WORDS = ( + 'a', 'an', 'and', 'if', 'is', 'the', 'in', 'i', 'you', 'other', + 'this', 'that', 'to', +) + +STATICFILES_STORAGE = 'pipeline.storage.PipelineCachedStorage' +STATICFILES_FINDERS += ( + 'pipeline.finders.PipelineFinder', +) + +# Django-allauth related settings +AUTHENTICATION_BACKENDS = ( + # Needed to login by username in Django admin, regardless of `allauth` + 'django.contrib.auth.backends.ModelBackend', + + # `allauth` specific authentication methods, such as login by e-mail + 'allauth.account.auth_backends.AuthenticationBackend', +) + +# Django grappelli need to be added before django.contrib.admin +INSTALLED_APPS = ( + 'grappelli', +) + INSTALLED_APPS + +# Grapelli settings +GRAPPELLI_ADMIN_TITLE = 'Td-Biblio Admin Page' + +INSTALLED_APPS += ( + 'allauth', + 'allauth.account', + 'allauth.socialaccount', + 'allauth.socialaccount.providers.google', + 'allauth.socialaccount.providers.github', + 'easyaudit', + 'rolepermissions', + 'rest_framework', # for future API needs + 'whitenoise.runserver_nostatic', + 'celery', + 'pipeline', +) + +MIDDLEWARE += ( + 'easyaudit.middleware.easyaudit.EasyAuditMiddleware', + 'whitenoise.middleware.WhiteNoiseMiddleware', +) + +# For heroku default postgres database +DATABASES = {"default": dj_database_url.config(conn_max_age=600)} + +# Defines whether to log model related events, +# such as when an object is created, updated, or deleted +DJANGO_EASY_AUDIT_WATCH_MODEL_EVENTS = True + +# Defines whether to log user authentication events, +# such as logins, logouts and failed logins. +DJANGO_EASY_AUDIT_WATCH_AUTH_EVENTS = True + +# Defines whether to log URL requests made to the project +DJANGO_EASY_AUDIT_WATCH_REQUEST_EVENTS = True + +SOCIALACCOUNT_PROVIDERS = { + 'github': { + 'SCOPE': ['user:email', 'public_repo', 'read:org'] + } +} + +ACCOUNT_UNIQUE_EMAIL = True +ACCOUNT_USERNAME_REQUIRED = True +ACCOUNT_EMAIL_REQUIRED = True +# ACCOUNT_SIGNUP_FORM_CLASS = 'base.forms.SignupForm' +#ACCOUNT_AUTHENTICATION_METHOD = 'username_email' +LOGIN_URL = reverse_lazy("login") +LOGIN_REDIRECT_URL = reverse_lazy("td_biblio:import") + +# ROLEPERMISSIONS_MODULE = 'roles.settings.roles' + +# This we may not need but in for scaling purpose in case extend apps +# features to need more resources +CELERY_ACCEPT_CONTENT = ['json'] +CELERY_TASK_SERIALIZER = 'json' +CELERY_RESULT_SERIALIZER = 'json' + +BROKER_URL = 'amqp://guest:guest@%s:5672//' % os.environ['RABBITMQ_HOST'] diff --git a/core/settings/dev.py b/core/settings/dev.py new file mode 100644 index 0000000..f499cf6 --- /dev/null +++ b/core/settings/dev.py @@ -0,0 +1,59 @@ +from .project import * # noqa + +# Set debug to True for development +DEBUG = True +TEMPLATE_DEBUG = DEBUG +LOGGING_OUTPUT_ENABLED = DEBUG +LOGGING_LOG_SQL = DEBUG + +EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' + +# Disable caching while in development +CACHES = { + 'default': { + 'BACKEND': 'django.core.cache.backends.dummy.DummyCache', + } +} + +# Make sure static files storage is set to default +STATIC_FILES_STORAGE = 'django.contrib.staticfiles.storage.StaticFilesStorage' + +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'formatters': { + # define output formats + 'verbose': { + 'format': ( + '%(levelname)s %(name)s %(asctime)s %(module)s %(process)d ' + '%(thread)d %(message)s') + }, + 'simple': { + 'format': ( + '%(name)s %(levelname)s %(filename)s L%(lineno)s: ' + '%(message)s') + }, + }, + 'handlers': { + # console output + 'console': { + 'class': 'logging.StreamHandler', + 'formatter': 'simple', + 'level': 'DEBUG', + } + }, + 'loggers': { + 'django.db.backends': { + 'handlers': ['console'], + 'level': 'INFO', # switch to DEBUG to show actual SQL + } + }, + # root logger + # non handled logs will propagate to the root logger + 'root': { + 'handlers': ['console'], + 'level': 'WARNING' + } +} + +PIPELINE['PIPELINE_ENABLED'] = False diff --git a/core/settings/dev_docker.py b/core/settings/dev_docker.py new file mode 100644 index 0000000..c3436c2 --- /dev/null +++ b/core/settings/dev_docker.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +"""Settings for when running under docker in development mode.""" +from .dev import * # noqa + +ALLOWED_HOSTS = ['*', + u'0.0.0.0', 'tailordev-biblio.herokuapp.com'] + +ADMINS = () + +# Set debug to True for development +DEBUG = True +TEMPLATE_DEBUG = DEBUG +LOGGING_OUTPUT_ENABLED = DEBUG +LOGGING_LOG_SQL = DEBUG + +DATABASES = { + 'default': { + 'ENGINE': 'django.contrib.gis.db.backends.postgis', + 'NAME': 'gis', + 'USER': 'docker', + 'PASSWORD': 'docker', + 'HOST': 'db', + 'PORT': 5432, + 'TEST_NAME': 'unittests', + } +} diff --git a/core/settings/prod.py b/core/settings/prod.py new file mode 100644 index 0000000..a82c49c --- /dev/null +++ b/core/settings/prod.py @@ -0,0 +1,30 @@ +# coding=utf-8 + +"""Project level settings.""" +from .project import * # noqa + +# Hosts/domain names that are valid for this site; required if DEBUG is False +# See https://docs.djangoproject.com/en/1.5/ref/settings/#allowed-hosts +# Localhost:9000 for vagrant +# Changes for live site +# ['*'] for testing but not for production + +ALLOWED_HOSTS = [ + 'localhost:9000', +] + +PIPELINE['YUI_BINARY'] = '/usr/bin/yui-compressor' +PIPELINE['JS_COMPRESSOR'] = 'pipeline.compressors.yui.YUICompressor' +PIPELINE['CSS_COMPRESSOR'] = 'pipeline.compressors.yui.YUICompressor' +PIPELINE_YUI_JS_ARGUMENTS = '--nomunge' +PIPELINE_DISABLE_WRAPPER = True + +# Comment if you are not running behind proxy +USE_X_FORWARDED_HOST = True + +# Set debug to false for production +DEBUG = TEMPLATE_DEBUG = False + +SERVER_EMAIL = '' # existing email n33ds to be added +EMAIL_HOST = 'tailordev.fr' +DEFAULT_FROM_EMAIL = 'info@tailordev.fr' diff --git a/core/settings/prod_docker.py b/core/settings/prod_docker.py new file mode 100644 index 0000000..361b2a5 --- /dev/null +++ b/core/settings/prod_docker.py @@ -0,0 +1,40 @@ + +"""Configuration for production server""" +# noinspection PyUnresolvedReferences +from .prod import * # noqa +import os + +DEBUG = False + +ALLOWED_HOSTS = ['*'] + +ADMINS = ( + ('Alison Mukoma', 'mukomalison@gmail.com'), +) + +DATABASES = { + 'default': { + 'ENGINE': 'django.contrib.gis.db.backends.postgis', + 'NAME': os.environ.get('DATABASE_NAME'), + 'USER': os.environ.get('DATABASE_USERNAME'), + 'PASSWORD': os.environ.get('DATABASE_PASSWORD'), + 'HOST': os.environ.get('DATABASE_HOST'), + 'PORT': 5432, + 'TEST_NAME': 'unittests', + } +} + + +# See fig.yml file for postfix container definition +# +EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' +# Host for sending e-mail. +EMAIL_HOST = 'smtp' +# Port for sending e-mail. +EMAIL_PORT = 25 +# SMTP authentication information for EMAIL_HOST. +# See fig.yml for where these are defined +EMAIL_HOST_USER = 'noreply@tailordev.fr' +EMAIL_HOST_PASSWORD = 'docker' +EMAIL_USE_TLS = False +EMAIL_SUBJECT_PREFIX = '[TAILORDEV BIBLIOGRAPHY]' diff --git a/core/settings/project.py b/core/settings/project.py new file mode 100644 index 0000000..44bd268 --- /dev/null +++ b/core/settings/project.py @@ -0,0 +1,50 @@ +# coding=utf-8 + +"""Project level settings. + +Adjust these values as needed but don't commit passwords etc. to any public +repository! +""" + +from django.utils.translation import ugettext_lazy as _ +from .contrib import * # noqa + +# Project apps +INSTALLED_APPS += ( + 'td_biblio', +) + +# Set languages which want to be translated +LANGUAGES = ( + ('en', _('English')), +) + +VALID_DOMAIN = [ + '0.0.0.0', +] + +PIPELINE = { + 'STYLESHEETS': { + 'td-biblio-base': { + 'source_filenames': { + 'css/styles.css', + }, + 'output_filename': 'css/styles.css', + 'extra_content': { + 'media': 'screen, projection', + } + } + }, + 'JAVASCRIPT': { + + } +} +# library needs to be added to path below +REQUIRE_JS_PATH = '/static/js/libs/requirejs-2.3.5/require.js' + +GRUNT_MODULES = { + 'base-view': { + 'main': 'js/app', + 'optimized': 'js/optimized.js', + } +} diff --git a/core/settings/test.py b/core/settings/test.py new file mode 100644 index 0000000..7c79202 --- /dev/null +++ b/core/settings/test.py @@ -0,0 +1,40 @@ +from .project import * # noqa + +# http://hustoknow.blogspot.com/2011/02/setting-up-django-nose-on-hudson.html +INSTALLED_APPS += ( + 'django_nose', # don't remove this comma +) + +PIPELINE['PIPELINE_ENABLED'] = False +TEST_RUNNER = 'django.test.runner.DiscoverRunner' + +NOSE_ARGS = ( + '--with-coverage', + '--cover-erase', + '--cover-html', + '--cover-html-dir=xmlrunner/html', + '--cover-inclusive', + # '--cover-package=django_app', + '--nocapture', + '--nologcapture' +) + +EMAIL_BACKEND = 'django.core.mail.backends.filebased.EmailBackend' +# change this to a proper location +EMAIL_FILE_PATH = '/tmp/' + +LOGGING = { + # internal dictConfig version - DON'T CHANGE + 'version': 1, + 'disable_existing_loggers': True, + 'handlers': { + 'nullhandler': { + 'class': 'logging.NullHandler', + }, + }, + # default root logger + 'root': { + 'level': 'DEBUG', + 'handlers': ['nullhandler'], + } +} diff --git a/core/settings/test_docker.py b/core/settings/test_docker.py new file mode 100644 index 0000000..92ccd11 --- /dev/null +++ b/core/settings/test_docker.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +__author__ = 'Alison Mukoma ' +from .test import * # noqa + +STATICFILES_STORAGE = 'pipeline.storage.PipelineStorage' + +DATABASES = { + 'default': { + 'ENGINE': 'django.contrib.gis.db.backends.postgis', + 'NAME': 'test_db', + 'USER': 'docker', + 'PASSWORD': 'docker', + 'HOST': 'db', + # Set to empty string for default. + 'PORT': '', + } +} + +MEDIA_ROOT = '/tmp/media' +STATIC_ROOT = '/tmp/static' diff --git a/core/settings/test_travis.py b/core/settings/test_travis.py new file mode 100644 index 0000000..dd3b10b --- /dev/null +++ b/core/settings/test_travis.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +__author__ = 'Alison Mukoma ' + +from .test import * # noqa + +DATABASES = { + 'default': { + 'ENGINE': 'django.contrib.gis.db.backends.postgis', + 'NAME': 'test_db', + 'USER': 'postgres', + 'PASSWORD': '', + 'HOST': 'localhost', + # Set to empty string for default. + 'PORT': '', + } +} + +MEDIA_ROOT = '/tmp/media' +STATIC_ROOT = '/tmp/static' diff --git a/core/settings/utils.py b/core/settings/utils.py new file mode 100644 index 0000000..8fc1b33 --- /dev/null +++ b/core/settings/utils.py @@ -0,0 +1,39 @@ +# coding=utf-8 + +"""Helpers for settings.""" +import os + +# Absolute filesystem path to the Django project directory: +DJANGO_ROOT = os.path.dirname( + os.path.dirname( + os.path.dirname(os.path.abspath(__file__)) + )) + + +def absolute_path(*args): + """Get an absolute path for a file that is relative to the django root. + + :param args: List of path elements. + :type args: list + + :returns: An absolute path. + :rtype: str + """ + return os.path.join(DJANGO_ROOT, *args) + + +def ensure_secret_key_file(): + """Checks that secret.py exists in settings dir. + + If not, creates one with a random generated SECRET_KEY setting.""" + secret_path = absolute_path('core', 'settings', 'secret.py') + if not os.path.exists(secret_path): + from django.utils.crypto import get_random_string + secret_key = get_random_string( + 50, 'abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+)') + with open(secret_path, 'w') as f: + f.write("SECRET_KEY = " + repr(secret_key) + "\n") + + +# Import the secret key +ensure_secret_key_file() diff --git a/sandbox/static/styles.css b/core/static/styles.css similarity index 100% rename from sandbox/static/styles.css rename to core/static/styles.css diff --git a/core/urls.py b/core/urls.py new file mode 100644 index 0000000..20b82a7 --- /dev/null +++ b/core/urls.py @@ -0,0 +1,30 @@ +"""mysite URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/1.8/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') +Including another URLconf + 1. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) +""" +from django.conf.urls import url, include +from django.contrib import admin +from django.conf import settings +from django.conf.urls.static import static + +urlpatterns = [ + url(r'^admin/', admin.site.urls), + url(r'^grappelli/', include('grappelli.urls')), + url("^", include("td_biblio.urls", namespace="td_biblio")), + +] + +if settings.DEBUG: + urlpatterns += static( + settings.MEDIA_URL, document_root=settings.MEDIA_ROOT + ) diff --git a/core/wsgi.py b/core/wsgi.py new file mode 100644 index 0000000..edcbd81 --- /dev/null +++ b/core/wsgi.py @@ -0,0 +1,56 @@ +# coding=utf-8 +""" +WSGI config for project. + +This module contains the WSGI application used by Django's development server +and any production WSGI deployments. It should expose a module-level variable +named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover +this application via the ``WSGI_APPLICATION`` setting. + +Usually you will have the standard Django WSGI application here, but it also +might make sense to replace the whole Django WSGI application with a custom one +that later delegates to the Django one. For example, you could introduce WSGI +middleware here, or combine a Django application with an application of another +framework. + +""" +import os + +# We put this here so that low level uwsgi errors also get reported +# noinspection PyUnresolvedReferences +# pylint: +# from raven.contrib.django.raven_compat.middleware.wsgi import Sentry # noqa +from django.core.wsgi import get_wsgi_application +# We defer to a DJANGO_SETTINGS_MODULE already in the environment. This breaks +# if running multiple sites in the same mod_wsgi process. To fix this, use +# mod_wsgi daemon mode with each site in its own daemon process, or use +# os.environ["DJANGO_SETTINGS_MODULE"] = "td_biblio.settings" +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "core.settings") + +# This application object is used by any WSGI server configured to use this +# file. This includes Django's development server, if the WSGI_APPLICATION +# setting points here. + +# Customised by Tim so we can access env vars set in apache + +import django.core.handlers.wsgi # noqa + +_application = get_wsgi_application() + + +def application(environ, start_response): + """Factory for the application instance. + + :param environ: os environment passed in by web server. + :type environ: dict + + :param start_response: ? + :type start_response: ? + + Places env vars defined in apache conf into a context accessible by django. + """ + return _application(environ, start_response) + +# Apply WSGI middleware here. +# from helloworld.wsgi import HelloWorldApplication +# application = HelloWorldApplication(application) diff --git a/deployment/docker-compose.yml b/deployment/docker-compose.yml new file mode 100644 index 0000000..f91fe62 --- /dev/null +++ b/deployment/docker-compose.yml @@ -0,0 +1,146 @@ +# +# Production server with UWSGI configured to run on port 8080 +# and web configured to run directly on port 80 +# +# docker-compose build +# docker-compose up -d web +# +# See accompanying Make commands for easy collectstatic etc. +# note that we use a gis powered db for future freedom :-) + +smtp: + # Note you cannot scale if you use container_name + container_name: td-biblio-smtp + image: catatnight/postfix + hostname: postfix + environment: + # You could change this to something more suitable + - maildomain=tailordev.fr + - smtp_user=noreply:docker + restart: unless-stopped + +db: + # Note you cannot scale if you use container_name + container_name: td-biblio-db + image: kartoza/postgis:9.6-2.4 + volumes: + #- ./pg/postgres_data:/var/lib/postgresql + - ./backups:/backups + - ./sql:/sql + environment: + - USERNAME=docker + - PASS=docker + restart: unless-stopped +# Uncomment the next line to have an access with PGAdmin using localhost and port 25432 on your computer. +# Only for development ! +# ports: +# - "25432:5432" + +uwsgi: + # Note you cannot scale if you use container_name + container_name: td-biblio-uwsgi + build: docker + hostname: uwsgi + environment: + - DATABASE_NAME=gis + - DATABASE_USERNAME=docker + - DATABASE_PASSWORD=docker + - DATABASE_HOST=db + - DJANGO_SETTINGS_MODULE=core.settings.prod_docker + - VIRTUAL_HOST=biblio.tailordev.fr + - VIRTUAL_PORT=8080 + - RABBITMQ_HOST=rabbitmq + volumes: + - ../../td-biblio:/home/web/django_project + - ./static:/home/web/static:rw + - ./media:/home/web/media:rw + - ./reports:/home/web/reports + - ./logs:/var/log/ + links: + - smtp:smtp + - db:db + restart: on-failure:5 + user: root + +dbbackups: + # Note you cannot scale if you use container_name + container_name: td-biblio-db-backups + image: kartoza/pg-backup:9.4 + hostname: pg-backups + volumes: + - ./backups:/backups + links: + - db:db + environment: + # take care to let the project name below match that + # declared in the top of the makefile + - DUMPPREFIX=PG_biblio + # These are all defaults anyway, but setting explicitly in + # case we ever want to ever use different credentials + - PGUSER=docker + - PGPASSWORD=docker + - PGPORT=5432 + - PGHOST=db + - PGDATABASE=gis + restart: unless-stopped + +# This is normally the main entry point for a production server +web: + # Note you cannot scale if you use container_name + container_name: td-biblio-web + image: nginx + hostname: nginx + volumes: + - ./sites-enabled:/etc/nginx/conf.d:ro + # I dont use volumes_from as I want to use the ro modifier + - ./static:/home/web/static:ro + - ./media:/home/web/media:ro + - ./logs:/var/log/nginx + links: + - uwsgi:uwsgi + ports: + - "63200:8080" + restart: unless-stopped + +# This is the entry point for a development server. +# Run with --no-deps to run attached to the services +# from produ environment if wanted +devweb: + # Note you cannot scale if you use container_name + container_name: td-biblio-dev-web + build: docker + dockerfile: Dockerfile-dev + hostname: uwsgi + environment: + - DATABASE_NAME=gis + - DATABASE_USERNAME=docker + - DATABASE_PASSWORD=docker + - DATABASE_HOST=db + - DJANGO_SETTINGS_MODULE=core.settings.prod_docker + - PYTHONPATH=/home/web/django_project + - VIRTUAL_HOST=biblio.tailordev.fr + - VIRTUAL_PORT=8080 + - RABBITMQ_HOST=rabbitmq + volumes: + - ../../td-biblio:/home/web/django_project + - ./static:/home/web/static + - ./media:/home/web/media + - ./reports:/home/web/reports + - ./logs:/var/log/ + links: + - smtp:smtp + - db:db + ports: + # for django test server + - "63302:8080" + # for ssh + - "63303:22" + +rabbitmq: + image: library/rabbitmq + hostname: rabbitmq + environment: + - RABBIT_PASSWORD=rabbit_test_password + - USER=rabbit_user + - RABBITMQ_NODENAME=rabbit + restart: unless-stopped diff --git a/deployment/docker/Dockerfile b/deployment/docker/Dockerfile new file mode 100644 index 0000000..a352a8d --- /dev/null +++ b/deployment/docker/Dockerfile @@ -0,0 +1,51 @@ +#--------- Generic stuff all our Dockerfiles should start with so we get caching ------------ +# Note this base image is based on debian +FROM python:3.6 +MAINTAINER Alison Mukoma + +RUN export DEBIAN_FRONTEND=noninteractive +ENV DEBIAN_FRONTEND noninteractive +RUN dpkg-divert --local --rename --add /sbin/initctl + +RUN apt-get update -y + +RUN apt-get install -y python3-pip \ + python-gdal \ + python-geoip \ + python3-setuptools \ + rpl + +RUN apt-get -y --force-yes install yui-compressor + +ADD REQUIREMENTS.txt /REQUIREMENTS.txt +RUN pip install -r /REQUIREMENTS.txt +RUN pip install uwsgi + +# Install Node js +RUN curl -sL https://deb.nodesource.com/setup_6.x -o nodesource_setup.sh +RUN bash nodesource_setup.sh +RUN apt-get -y --force-yes install nodejs +RUN npm -g install yuglify + +# Debian is messed up and aliases node as nodejs +# So when yuglify is installed it references the wrong node binary... +# lets fix that here... + +RUN rpl "env node" "env nodejs" /usr/lib/node_modules/yuglify/bin/yuglify + +# Install grunt +RUN npm install -g grunt-cli +ADD package.json /package.json +ADD Gruntfile.js /Gruntfile.js +RUN cd / && npm install grunt grunt-contrib-concat grunt-contrib-uglify +RUN cd / && npm install grunt-contrib-requirejs + +#USER www-data +WORKDIR /home/web/django_project + +ADD uwsgi.conf /uwsgi.conf + +# Open port 8080 as we will be running our uwsgi socket on that +EXPOSE 8080 + +CMD ["uwsgi", "--ini", "/uwsgi.conf"] diff --git a/deployment/docker/Dockerfile-dev b/deployment/docker/Dockerfile-dev new file mode 100644 index 0000000..e62a9b9 --- /dev/null +++ b/deployment/docker/Dockerfile-dev @@ -0,0 +1,34 @@ +#--------- Generic stuff all our Dockerfiles should start with so we get caching ------------ +# Note this base image is based on debian +FROM fish_uwsgi +MAINTAINER Alison Mukoma + +# https://docs.docker.com/examples/running_ssh_service/ +# Sudo is needed by pycharm when it tries to pip install packages +RUN apt-get update && apt-get install -y openssh-server sudo +RUN mkdir /var/run/sshd +RUN echo 'root:docker' | chpasswd +RUN sed -i 's/PermitRootLogin without-password/PermitRootLogin yes/' /etc/ssh/sshd_config + +# SSH login fix. Otherwise user is kicked off after login +RUN sed 's@session\s*required\s*pam_loginuid.so@session optional pam_loginuid.so@g' -i /etc/pam.d/sshd + +ENV NOTVISIBLE "in users profile" +RUN echo "export VISIBLE=now" >> /etc/profile + +# End of cut & paste section + +ADD REQUIREMENTS-dev.txt /REQUIREMENTS-dev.txt +RUN pip install -r /REQUIREMENTS-dev.txt +ADD bashrc /root/.bashrc + +# -------------------------------------------------------- +# Open ports as needed +# -------------------------------------------------------- + +# Open port 8080 as we will be running our django dev server on +EXPOSE 8080 +# Open port 22 as we will be using a remote interpreter from pycharm +EXPOSE 22 + +CMD ["/usr/sbin/sshd", "-D"] diff --git a/deployment/docker/Gruntfile.js b/deployment/docker/Gruntfile.js new file mode 100644 index 0000000..25c7d8d --- /dev/null +++ b/deployment/docker/Gruntfile.js @@ -0,0 +1,28 @@ +module.exports = function(grunt) { + + // Project configuration. + grunt.initConfig({ + pkg: grunt.file.readJSON('/package.json'), + + requirejs: { + compile: { + options: { + baseUrl: '/home/web/django_project/td_biblio/static/js', + mainConfigFile: '/home/web/django_project/td_biblio/static/js/app.js', + name: 'libs/almond/almond', + include: ['app.js'], + out: '/home/web/django_project/td_biblio/static/js/optimized.js' + } + } + } + + }); + + // Load plugins here. + grunt.loadNpmTasks('grunt-contrib-concat'); + grunt.loadNpmTasks('grunt-contrib-uglify'); + grunt.loadNpmTasks('grunt-contrib-requirejs'); + + // Register tasks here. + grunt.registerTask('default', ['requirejs']); +}; diff --git a/deployment/docker/REQUIREMENTS-dev.txt b/deployment/docker/REQUIREMENTS-dev.txt new file mode 100644 index 0000000..5d5b73b --- /dev/null +++ b/deployment/docker/REQUIREMENTS-dev.txt @@ -0,0 +1,12 @@ +django-nose +coverage +pep8>=1.5.7,<1.6 +pylint +flake8==3.3.0 + +# documentation +Sphinx + +# pdb plus plus +pdbpp + diff --git a/deployment/docker/REQUIREMENTS.txt b/deployment/docker/REQUIREMENTS.txt new file mode 100644 index 0000000..fd1592c --- /dev/null +++ b/deployment/docker/REQUIREMENTS.txt @@ -0,0 +1,62 @@ +psycopg2-binary +#Django>=1.7 + +# just for testing with this version +Django==2.0 + +django-braces==1.9.0 +django-model-utils==1.4.0 +django-pipeline==1.6.14 +django-role-permissions==2.2.0 +markdown==2.6.11 +celery==4.1.0 + +# Raven pinned to 3.3.7 - see +# https://github.com/getsentry/raven-python/issues/337 +raven==5.8.1 +django-sentry==1.13.5 +requests==2.18.4 +pytz +# support special characters + +django-allauth==0.35.0 + +# Django Easy Audit +# https://github.com/soynatan/django-easy-audit +git+https://github.com/soynatan/django-easy-audit.git +django-grappelli==2.11.1 + +djangorestframework==3.7.7 +django-filter==1.1.0 +coreapi==2.3.3 +pygbif==0.2.0 + +# Bibliography loaders +bibtexparser +eutils +habanero>=0.3.0 + +# Heroku +# WSGI +gunicorn + +# Settings +dj-database-url + +# Statics +whitenoise + +# Testing +pytest>=3.6 +pytest-cov>=2.4.0 +pytest-django>=3.1.2 +pytest-mock>=1.6.3 +coveralls + +# Test tixtures +factory-boy>=2.6.0 + +# PyPI +twine +wheel + diff --git a/deployment/docker/bashrc b/deployment/docker/bashrc new file mode 100644 index 0000000..0c2ee35 --- /dev/null +++ b/deployment/docker/bashrc @@ -0,0 +1,11 @@ +#!/bin/bash + +# This is intended to be placed in the docker dev environment so +# that the django project is in the path when you log in + +if [ -f /etc/bashrc ]; then + . /etc/bashrc +fi +export PYTHONPATH=/home/web/django_project:$PYTHONPATH +export DJANGO_SETTINGS_MODULE=core.settings.dev_docker +cd /home/web/django_project diff --git a/deployment/docker/package.json b/deployment/docker/package.json new file mode 100644 index 0000000..3e7e71c --- /dev/null +++ b/deployment/docker/package.json @@ -0,0 +1,17 @@ +{ + "name": "django-tailordev-biblio", + "version": "1.0.0", + "description": "Package for django app", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "Alison Mukoma", + "license": "MIT", + "devDependencies": { + "grunt": "^1.0.1", + "grunt-contrib-concat": "^1.0.1", + "grunt-contrib-requirejs": "^1.0.0", + "grunt-contrib-uglify": "^3.3.0" + } +} diff --git a/deployment/docker/uwsgi.conf b/deployment/docker/uwsgi.conf new file mode 100644 index 0000000..15cd2c4 --- /dev/null +++ b/deployment/docker/uwsgi.conf @@ -0,0 +1,21 @@ +[uwsgi] + +# Touch this file to reload uwsgi +#touch-reload = /tmp/touch-me-to-reload +chdir = /home/web/django_project +module = core.wsgi +master = true +pidfile=/tmp/django.pid +socket = 0.0.0.0:8080 +workers = 4 +cheaper = 2 +env = DJANGO_SETTINGS_MODULE=core.settings.prod_docker +# disabled so we run in the foreground for docker +#daemonize = /tmp/docker.log +req-logger = file:/var/log/uwsgi-requests.log +logger = file:/var/log/uwsgi-errors.log +#uid = 1000 +#gid = 1000 +memory-report = true +harakiri = 20 +plugin = python36 \ No newline at end of file diff --git a/manage.py b/manage.py new file mode 100644 index 0000000..344579e --- /dev/null +++ b/manage.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python +# coding=utf-8 +""" +django_app.manage +""" +import os +import sys + +if __name__ == "__main__": + os.environ.setdefault( + "DJANGO_SETTINGS_MODULE", + "core.settings.dev") + + from django.core.management import execute_from_command_line + + execute_from_command_line(sys.argv) diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 1ffaf09..0000000 --- a/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -# Heroku defaults to root requirements.txt file to install dependencies --r requirements/heroku.txt diff --git a/requirements/base.txt b/requirements/base.txt deleted file mode 100644 index b4798c0..0000000 --- a/requirements/base.txt +++ /dev/null @@ -1,7 +0,0 @@ -# Django -Django>=1.7 - -# Bibliography loaders -bibtexparser -eutils -habanero>=0.3.0 diff --git a/requirements/dev.txt b/requirements/dev.txt deleted file mode 100644 index 3654387..0000000 --- a/requirements/dev.txt +++ /dev/null @@ -1,25 +0,0 @@ -# This file pulls in everything a developer needs. If it's a basic package -# needed to run the site, it belongs in `requirements/base.txt`. If it's a -# package for developers (testing, docs, etc.), it goes in this file. --r base.txt - -# Settings -dj-database-url - -# Statics -whitenoise - -# Testing -pytest>=3.6 -pytest-cov>=2.4.0 -pytest-django>=3.1.2 -pytest-mock>=1.6.3 -flake8 -coveralls - -# Fixtures -factory-boy>=2.6.0 - -# PyPI -twine -wheel diff --git a/requirements/heroku.txt b/requirements/heroku.txt deleted file mode 100644 index aca243c..0000000 --- a/requirements/heroku.txt +++ /dev/null @@ -1,8 +0,0 @@ -# This file pulls in everything needed to run the sandbox in production (heroku) --r dev.txt - -# WSGI -gunicorn - -# Database -psycopg2 diff --git a/sandbox/manage.py b/sandbox/manage.py deleted file mode 100755 index 46475d9..0000000 --- a/sandbox/manage.py +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env python -import os -import sys - -if __name__ == "__main__": - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings") - try: - from django.core.management import execute_from_command_line - except ImportError: - # The above import may fail for some other reason. Ensure that the - # issue is really that Django is missing to avoid masking other - # exceptions on Python 2. - try: - import django # noqa - except ImportError: - raise ImportError( - "Couldn't import Django. Are you sure it's installed and " - "available on your PYTHONPATH environment variable? Did you " - "forget to activate a virtual environment?" - ) - raise - execute_from_command_line(sys.argv) diff --git a/sandbox/settings.py b/sandbox/settings.py deleted file mode 100644 index d3d0d38..0000000 --- a/sandbox/settings.py +++ /dev/null @@ -1,116 +0,0 @@ -""" -Django settings for td_biblio sandbox. -""" - -import os - -import dj_database_url - -try: - from django.core.urlresolvers import reverse_lazy -except: - from django.urls import reverse_lazy - -# Build paths inside the project like this: os.path.join(BASE_DIR, ...) -BASE_DIR = os.path.dirname(os.path.abspath(__file__)) - -# Default secret key is for development only! -SECRET_KEY = os.environ.get( - "DJANGO_SECRET_KEY", "2#jyyh#-tgk$%wdx0zgfx=-)#hr9ni(_m66a3!rmco8^a5cns!" -) - -# SECURITY WARNING: don't run with debug turned on in production! -DEBUG = os.environ.get("IS_PUBLIC_SANDBOX") is None - -ALLOWED_HOSTS = ["localhost", "127.0.0.1", "tailordev-biblio.herokuapp.com"] - -LOGIN_URL = reverse_lazy("login") -LOGIN_REDIRECT_URL = reverse_lazy("td_biblio:import") - -# Application definition -INSTALLED_APPS = [ - "django.contrib.admin", - "django.contrib.auth", - "django.contrib.contenttypes", - "django.contrib.sessions", - "django.contrib.messages", - "whitenoise.runserver_nostatic", - "django.contrib.staticfiles", - "td_biblio", -] - -MIDDLEWARE = ( - "django.middleware.security.SecurityMiddleware", - "whitenoise.middleware.WhiteNoiseMiddleware", - "django.contrib.sessions.middleware.SessionMiddleware", - "django.middleware.common.CommonMiddleware", - "django.middleware.csrf.CsrfViewMiddleware", - "django.contrib.auth.middleware.AuthenticationMiddleware", - "django.contrib.messages.middleware.MessageMiddleware", - "django.middleware.clickjacking.XFrameOptionsMiddleware", -) - -# Django < 1.10 compat -MIDDLEWARE_CLASSES = ( - "django.contrib.sessions.middleware.SessionMiddleware", - "django.middleware.common.CommonMiddleware", - "django.middleware.csrf.CsrfViewMiddleware", - "django.contrib.auth.middleware.AuthenticationMiddleware", - "django.contrib.auth.middleware.SessionAuthenticationMiddleware", - "django.contrib.messages.middleware.MessageMiddleware", - "django.middleware.clickjacking.XFrameOptionsMiddleware", -) - -ROOT_URLCONF = "sandbox.urls" - -TEMPLATES = [ - { - "BACKEND": "django.template.backends.django.DjangoTemplates", - "DIRS": [os.path.join(BASE_DIR, "templates")], - "APP_DIRS": True, - "OPTIONS": { - "context_processors": [ - "django.template.context_processors.debug", - "django.template.context_processors.request", - "django.contrib.auth.context_processors.auth", - "django.contrib.messages.context_processors.messages", - ] - }, - } -] - -# Django < 1.8 compat -TEMPLATE_DIRS = [os.path.join(BASE_DIR, "templates")] - -WSGI_APPLICATION = "sandbox.wsgi.application" - - -# Database -DATABASES = {"default": dj_database_url.config(conn_max_age=600)} - - -# Password validation -AUTH_PASSWORD_VALIDATORS = [ - { - "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator" # noqa - }, - {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"}, # noqa - {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"}, # noqa - { - "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator" # noqa - }, -] - - -# Internationalization -LANGUAGE_CODE = "en-us" -TIME_ZONE = "UTC" -USE_I18N = True -USE_L10N = True -USE_TZ = True - - -# Static files (CSS, JavaScript, Images) -STATIC_URL = "/static/" -STATIC_ROOT = os.path.join(BASE_DIR, "public", "static") -STATICFILES_DIRS = (os.path.join(BASE_DIR, "static"),) diff --git a/sandbox/templates/registration/login.html b/sandbox/templates/registration/login.html deleted file mode 100644 index d056d89..0000000 --- a/sandbox/templates/registration/login.html +++ /dev/null @@ -1,13 +0,0 @@ -{% extends "_layouts/base.html" %} -{% load i18n %} - -{% block content %} - -
- {% csrf_token %} - - {{ form }} - - -
-{% endblock %} diff --git a/sandbox/urls.py b/sandbox/urls.py deleted file mode 100644 index 7992cd6..0000000 --- a/sandbox/urls.py +++ /dev/null @@ -1,10 +0,0 @@ -"""td_biblio urls""" - -from django.conf.urls import include, url -from django.contrib import admin - -urlpatterns = [ - url("^admin/", admin.site.urls), - url("^auth/", include("django.contrib.auth.urls")), - url("^", include("td_biblio.urls", namespace="td_biblio")), -] diff --git a/sandbox/wsgi.py b/sandbox/wsgi.py deleted file mode 100644 index 535681f..0000000 --- a/sandbox/wsgi.py +++ /dev/null @@ -1,11 +0,0 @@ -""" -WSGI config for td_biblio sandbox. -""" - -import os - -from django.core.wsgi import get_wsgi_application - -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "sandbox.settings") - -application = get_wsgi_application() diff --git a/setup.cfg b/setup.cfg index f9e40d1..4a05385 100644 --- a/setup.cfg +++ b/setup.cfg @@ -2,7 +2,7 @@ universal=1 [tool:pytest] -DJANGO_SETTINGS_MODULE=sandbox.settings +DJANGO_SETTINGS_MODULE=core.settings.dev_docker addopts = -vs --cov=td_biblio --cov-report term-missing testpaths = td_biblio/tests diff --git a/setup.py b/setup.py index 1f158ef..70f8242 100755 --- a/setup.py +++ b/setup.py @@ -27,6 +27,9 @@ def parse_requirements(requirements, ignore=("setuptools",)): return list(packages) +with open("README.md") as f: + readme = f.read() + setup( name="django-tailordev-biblio", version=__import__("td_biblio").__version__, @@ -61,7 +64,9 @@ def parse_requirements(requirements, ignore=("setuptools",)): "Development Status :: 5 - Production/Stable", "Operating System :: OS Independent", ], - install_requires=parse_requirements("requirements/base.txt"), - tests_require=parse_requirements("requirements/dev.txt"), + install_requires=parse_requirements( + "deployment/docker/REQUIREMENTS.txt"), + tests_require=parse_requirements("deployment/docker/REQUIREMENTS.txt"), keywords="django biblio bibliography bibtex publication", + dependency_links=[] ) diff --git a/tatus b/tatus new file mode 100644 index 0000000..6e93c47 --- /dev/null +++ b/tatus @@ -0,0 +1,1885 @@ +commit e9cf493d9f2f3ff8333b3d5e294104ab86a8a90b (HEAD -> dockerise_me) +Author: Alison Mukoma +Date: Sat Jan 12 00:33:19 2019 +0200 + + Add docker configs into own dir + +commit 6af5950739f5b661459a3863fa0d574f48222112 (tag: 2.0.0, origin/master, origin/HEAD) +Author: Julien Maupetit +Date: Tue Nov 13 22:24:49 2018 +0100 + + Bump release to 2.0.0 + +commit 6b4741a057ae8cc4e682713d8a86eb54b37d80ed +Author: Anas +Date: Fri Aug 31 15:07:51 2018 +0100 + + Make journal's name optionnal + + Books, etc. are not related to Journal. + +commit 608b0bd85c55bf241cec5aea7311fff6e77628f7 +Author: Julien Maupetit +Date: Tue Nov 13 21:05:35 2018 +0100 + + Update compatibility matrix + + * Remove Django 1.8 + * Add Django 2.0 + * Add Django 2.1 + + It should also work with Python 3.7 but it's not yet available on + Travis.CI. + +commit b2ead96617f77c1643f1774eac6be62462607563 +Author: Julien Maupetit +Date: Tue Nov 13 20:37:26 2018 +0100 + + Ignore VSCode settings + +commit 1077bfd6cec982f0035a83ab068e2343b036a002 +Author: Julien Maupetit +Date: Tue Nov 13 20:36:45 2018 +0100 + + Fix flake8 linting errors + +commit cc5a7cc196f0c0b100a2865ee4d649ad644ef5dd +Author: Julien Maupetit +Date: Mon Nov 12 23:58:27 2018 +0100 + + Increase flake8 max line length + + To fit with Black formatter max line length, increase flake8 option to 89. + +commit 9ef5255f272da14f07eea62ae472fe0ab90f808d +Author: Julien Maupetit +Date: Mon Nov 12 23:48:55 2018 +0100 + + Fix test_load_records_from_an_existing_doi date + +commit 56a0a6a0c006b72d61563398d9aed7331fc369f7 +Author: Julien Maupetit +Date: Mon Nov 12 23:43:36 2018 +0100 + + Upgrade pytest to 3.6+ + +commit df4f5b709552ea1ab86cfb443e9301c49be245d8 +Author: Julien Maupetit +Date: Mon Nov 12 23:22:40 2018 +0100 + + Fix Django > 2.0 compatibility + +commit 7870b3b5a088293906d50320cbb8e3763d939485 +Author: Julien Maupetit +Date: Mon Nov 12 23:07:42 2018 +0100 + + Format code with black + +commit 06f0f865d2ec2cc21b74653fded1cab8d5f95720 +Author: Julien Maupetit +Date: Sun Dec 3 18:55:37 2017 +0100 + + Fix pep8 + +commit b509867fdf4755dd552792cf490aaa82595e066c +Author: Julien Maupetit +Date: Sun Dec 3 18:12:56 2017 +0100 + + Make lazy choices for AuthorDuplicatesForm + +commit 45337a04857d36bb114e190e14e4931b0c0677d1 +Author: Julien Maupetit +Date: Sun Dec 3 18:00:25 2017 +0100 + + Fix setup.py install requires type + +commit 87ab40aedace54fbe7abcf0ce65b94dd51033c2b +Author: Julien Maupetit +Date: Sun Dec 3 17:00:08 2017 +0100 + + Add author alias support in entry list view + +commit dfc43263afbd970863d8783f5eaeabb13f1426c3 +Author: Julien Maupetit +Date: Sun Dec 3 16:26:07 2017 +0100 + + Fix entry list filtering + +commit 726ead82179575dbce81bd27de9f099b32d16052 +Author: Julien Maupetit +Date: Sat Dec 2 16:10:50 2017 +0100 + + Add support for custom paginate by + +commit 04d04290ed646e5a885996f7d1d877aca7a99042 +Author: Julien Maupetit +Date: Sat Dec 2 15:36:11 2017 +0100 + + Redirect to the same page + +commit d1740325409232ab97aaa02387ab3298d9193999 +Author: Julien Maupetit +Date: Sat Dec 2 15:20:58 2017 +0100 + + Add FindDuplicatedAuthorsView admin view + +commit dde3dde1d9b90480e0ee2d915e5a6770055fc5d3 +Author: Julien Maupetit +Date: Fri Dec 1 18:59:50 2017 +0100 + + Add human model alias field (fk to self) + +commit eb4f54937444a44fc3c6d310f2469afbf5989bfc +Author: Julien Maupetit +Date: Fri Dec 1 18:59:32 2017 +0100 + + Add management command shortcut + +commit 98f3620237d5f0f1e95f80fcadd558ca36fddcf5 (tag: 1.2.0) +Author: Julien Maupetit +Date: Fri Dec 1 14:06:46 2017 +0100 + + Bump release to 1.2.0 + +commit a445f65e2376ba9f5256c2462369e0b69e52bef9 +Merge: c7c4c01 7c267b6 +Author: Julien Maupetit +Date: Fri Dec 1 13:58:51 2017 +0100 + + Merge pull request #27 from TailorDev/fix-heroku-deploy + + Use heroku latest python 3.6 release + +commit 7c267b6bf7ada0092ff9d0e4ccc75ea6400c21a2 +Author: Julien Maupetit +Date: Fri Dec 1 13:58:04 2017 +0100 + + Use heroku latest python 3.6 release + +commit c7c4c0146e7972d2a2ff8aecd040271caeece6f5 +Merge: f7898eb baffce9 +Author: Julien Maupetit +Date: Fri Dec 1 11:40:28 2017 +0100 + + Merge pull request #25 from TailorDev/add-doi-tests + + Make batch importation more robust + +commit baffce961a1ec4a122286f9a2e43ecd60173fa99 +Author: Julien Maupetit +Date: Fri Dec 1 11:22:00 2017 +0100 + + Fix flake8 E722 rule + +commit 54903899e5e389dd1cf19aafda16c0399d86a7ce +Author: Julien Maupetit +Date: Fri Dec 1 10:47:22 2017 +0100 + + Add term-missing in coverage report + +commit c8901e95e0527cde9cae5618fe783d205d100b91 +Author: Julien Maupetit +Date: Fri Dec 1 10:46:51 2017 +0100 + + Add tests for loaders to_record exceptions + +commit 03354b8152cbc664723cb8d562045ce41db00cf7 +Author: Julien Maupetit +Date: Fri Dec 1 10:42:21 2017 +0100 + + Add pytest-mock dependency + +commit f3cf7165d61b9ca878e87ab5605b3a1e6aa2f70f +Author: Julien Maupetit +Date: Fri Dec 1 10:04:53 2017 +0100 + + Better catch to_record exceptions + +commit fceccb30659e46ed46933bda58611e72950b8095 +Author: Julien Maupetit +Date: Tue Sep 26 11:06:53 2017 +0200 + + Fix pep8 + +commit 3a0cee954538357fd946b15916a729bfbd564f97 +Author: Julien Maupetit +Date: Tue Sep 26 10:19:30 2017 +0200 + + Fix test (journal full name) + +commit b4456418a2030823ed640b84a9e958c7f5f83ce4 +Author: Julien Maupetit +Date: Tue Sep 26 10:14:06 2017 +0200 + + Catch PMIDLoader errors during batch importation + +commit eab4f6762dd285af0ad1ac213a214507337a2b51 +Author: Julien Maupetit +Date: Fri Jun 23 23:06:07 2017 +0200 + + Fix issue related to empty authors names + +commit a0f1b25d1f0a28572c4ff1e9fa89b7233d3f8724 +Author: Julien Maupetit +Date: Fri Jun 23 22:58:16 2017 +0200 + + Catch DOILoader errors during batch importation + +commit 454f2d8626f66270b6eaff34f1c4cc3b78c25fb9 +Author: Julien Maupetit +Date: Fri Jun 23 22:57:29 2017 +0200 + + Add error message style + +commit 5322c464126f33afa65e42d956f0db213f5c93a3 +Author: Julien Maupetit +Date: Fri Jun 23 16:45:46 2017 +0200 + + Add new fixtures + +commit f7898eb35f5bb53eb007db11ca60fa257f34e380 +Author: Julien Maupetit +Date: Wed Jun 14 11:10:51 2017 +0200 + + Add PyPI publication docs/requirements + +commit a0c823aa7ab87c2a21514d06a57724261ba600c1 (tag: 1.1.0) +Author: Julien Maupetit +Date: Wed Jun 14 11:01:31 2017 +0200 + + Bump release to 1.1.0 + +commit 691c1cfb00a9b16b4bfbbf2e7b933032a6e5930e +Author: Julien Maupetit +Date: Wed Jun 14 11:01:13 2017 +0200 + + Add missing files to dist + +commit 6e79436cf72e5136f2a6b12248969b74ab89fd28 +Author: Julien Maupetit +Date: Wed Jun 14 11:00:52 2017 +0200 + + Add Django 1.11 compat to setup file + +commit 6fc2e75958d4cff9a09a056df503078ef93bbe5f +Merge: 1ab1ec6 287b7a2 +Author: Julien Maupetit +Date: Mon Jun 12 22:16:00 2017 +0200 + + Merge pull request #20 from TailorDev/prettier-sandbox + + Prettier sandbox + +commit 287b7a259667fe0e9f475ef7dce7df3325a93b07 +Author: Julien Maupetit +Date: Mon Jun 12 21:47:49 2017 +0200 + + Force input PMIDs / DOIs sorting + +commit 688f64303da1d19d41e7b7b65be6e6c18f4a2e40 +Author: Julien Maupetit +Date: Mon Jun 12 21:42:24 2017 +0200 + + Fix Django<1.8 compat + +commit d331ae9cdbba0a24df2d6360d84599768df898fc +Author: Julien Maupetit +Date: Mon Jun 12 21:42:09 2017 +0200 + + Fix py2 unicode compat + +commit 74c543d05e93eb66087cca19295991072d8255dd +Author: Julien Maupetit +Date: Mon Jun 12 21:20:44 2017 +0200 + + Handle empty form submission + +commit 0496e3784d8aab3979ac0c44887fd0536eb0b7ea +Author: Julien Maupetit +Date: Mon Jun 12 20:50:41 2017 +0200 + + Add login page + +commit d1a4677cec682f46e278a5a243dfe4be8c9c4ef5 +Author: Julien Maupetit +Date: Mon Jun 12 17:50:12 2017 +0200 + + Improve docs + +commit ff467ca20f1644edfce8032bcd66ca89232ebf46 +Author: Julien Maupetit +Date: Mon Jun 12 17:40:29 2017 +0200 + + Add sandbox section to the docs + +commit 9a90032f0d3e85ff6bac3679799d384b7721c2fa +Author: Julien Maupetit +Date: Mon Jun 12 17:15:58 2017 +0200 + + Make the sandbox responsive + +commit 9f2917a173e2943f762166a5dd97d07cd8b0570d +Author: Julien Maupetit +Date: Mon Jun 12 16:23:43 2017 +0200 + + Improve messages style + +commit 5834a3690d0b7173b58e7625f7ab25dbe8fa5a2d +Author: Julien Maupetit +Date: Mon Jun 12 16:14:32 2017 +0200 + + Improve import form styles + +commit 08cb257b21bdefb302ac5cf358cf6fffec1e038f +Author: Julien Maupetit +Date: Mon Jun 12 16:02:24 2017 +0200 + + Fix left sidebar width + +commit c38f308b03869606b36e704128e7553a12790bde +Author: Julien Maupetit +Date: Mon Jun 12 16:00:37 2017 +0200 + + Fix admin import message position + +commit 1ab1ec6404a9767d1755e8e2fed7437e2920f655 +Merge: 9032cb0 069c4ee +Author: Julien Maupetit +Date: Sat Jun 10 00:32:51 2017 +0200 + + Merge pull request #19 from TailorDev/deploy-heroku + + Don't prefix td_biblio urls in the sandbox + +commit 9032cb0bd73750f9410fac232136c6151404a919 +Merge: 283cfb0 645a840 +Author: Julien Maupetit +Date: Sat Jun 10 00:30:13 2017 +0200 + + Merge pull request #18 from TailorDev/deploy-heroku + + Add sandbox automated deployment to heroku + +commit f84a85f0d917005e84b7ddc8f395e639e55185c9 +Author: Julien Maupetit +Date: Sat Jun 10 00:26:45 2017 +0200 + + Add basic entry list integration + +commit 7c62ed9bc21f4f787a453c7f31090e04e9b3f2b1 +Author: Julien Maupetit +Date: Fri Jun 9 23:34:05 2017 +0200 + + Add milligram & basic styles + +commit 93b3de647af4944cd8d109f587342cf90a62b8d4 +Author: Julien Maupetit +Date: Fri Jun 9 23:05:48 2017 +0200 + + Fix indentation + +commit 1c2e686f9b54f478a1a03574598461e4e722eee4 +Author: Julien Maupetit +Date: Fri Jun 9 23:04:27 2017 +0200 + + Move base styles to a CSS file + +commit 069c4ee8d003a180ab28bbdb319390d0d7609573 +Author: Julien Maupetit +Date: Fri Jun 9 22:56:16 2017 +0200 + + Don't prefix td_biblio urls in the sandbox + +commit 645a8406e4955c239b8642f7fbb33403e415cec4 +Author: Julien Maupetit +Date: Fri Jun 9 22:50:51 2017 +0200 + + Inactivate the whitenoise STATICFILES_STORAGE + + As stated in the documentation, this storage may lead to failures for CSS files + mentionning extra files. + http://whitenoise.evans.io/en/stable/django.html#troubleshooting-the-whitenoise-storage-backend + +commit cbea93ad1ccb7e85e835009e26c5835b8a0fc67d +Author: Julien Maupetit +Date: Fri Jun 9 22:36:21 2017 +0200 + + Add static directory (required for collectstatic) + +commit 668ce7560939c7e15f8359c21d48272055060902 +Author: Julien Maupetit +Date: Fri Jun 9 22:34:06 2017 +0200 + + Add docs for heroku deployment + +commit 4523069780b3f8ccfb2f518f54cc84b8c20ad2df +Author: Julien Maupetit +Date: Fri Jun 9 22:33:39 2017 +0200 + + Add & configure whitenoise to serve static files + +commit c6f17783a2d2b32e5d5aeea037011cc9a618e13d +Author: Julien Maupetit +Date: Fri Jun 9 18:55:20 2017 +0200 + + Fix CI script + +commit 81663cb20c41c4079806aa444edd6c0ca437b4ac +Author: Julien Maupetit +Date: Fri Jun 9 18:31:27 2017 +0200 + + Add Makefile to ease common tasks + +commit 94c0a470f26030980aee3405c78646122196c191 +Author: Julien Maupetit +Date: Fri Jun 9 17:41:00 2017 +0200 + + Update sandbox configuration for Heroku + +commit 9c53d68160e4d35d83931bf63a9057d586aad5aa +Author: Julien Maupetit +Date: Fri Jun 9 17:40:18 2017 +0200 + + Add heroku web description + +commit 0fe064e5c256b8e1bbe3fff72060fd01cb46a175 +Author: Julien Maupetit +Date: Fri Jun 9 17:31:04 2017 +0200 + + Refactor requirements + +commit 283cfb0b92c168e03f12137f1c9ea36ade2daab6 +Merge: 2a22546 dc34479 +Author: Julien Maupetit +Date: Fri Jun 9 11:07:22 2017 +0200 + + Merge pull request #17 from TailorDev/fix-map + + Fix Abstract Human map method + +commit dc3447976edf95bf74081648584860329549b742 +Author: Julien Maupetit +Date: Fri Jun 9 00:09:26 2017 +0200 + + Fix quotes + +commit e70a67e3be8e0aa54f368466d7e831ba8d565d42 +Author: Julien Maupetit +Date: Fri Jun 9 00:05:40 2017 +0200 + + Fix map method naming -> _set_user + +commit 2a22546da5dfbc1a82cdd4181c9a76d43bc9d945 +Author: Julien Maupetit +Date: Thu Jun 8 23:56:17 2017 +0200 + + Clean legacy code + +commit d0e0db0dd50cc372090a0b773d465d9de5ad250f +Author: Julien Maupetit +Date: Thu Jun 8 23:47:11 2017 +0200 + + Fix docs + +commit 31105dc1af3ee4fbe9c8a90323e0255557d688d9 +Author: Julien Maupetit +Date: Thu Jun 8 23:33:50 2017 +0200 + + Fix coveralls default branch + +commit bd089781897fdb564054beb9c270670d6e3dfb1d +Merge: bee529e 6c316ff +Author: Julien Maupetit +Date: Thu Jun 8 23:31:59 2017 +0200 + + Merge pull request #16 from TailorDev/coveralls + + WIP: Integrate coveralls + +commit 6c316ffa38fc0f0675473ba211b90e72723938bb +Author: Julien Maupetit +Date: Thu Jun 8 23:31:02 2017 +0200 + + Add coveralls badge to the project + +commit f1552dd3c2c9a9ac84ba2be3f6df54bded527393 +Author: Julien Maupetit +Date: Thu Jun 8 19:25:39 2017 +0200 + + Integrate coveralls + + Fix #11 + +commit bee529e80d69e89f2cb1771879fe9a973c92bb96 +Merge: 3d6d621 1e8f4fc +Author: Julien Maupetit +Date: Thu Jun 8 23:14:16 2017 +0200 + + Merge pull request #15 from TailorDev/django-1.11 + + Add Django 1.11 compatibility + +commit 1e8f4fc91b06d0851b33c84fea50758be14a7ac1 +Author: Julien Maupetit +Date: Thu Jun 8 19:07:54 2017 +0200 + + Update compatibility matrix in the docs + + Fix #12 + +commit c863c9803ff608867db040692cd3c7590dfcc10d +Author: Julien Maupetit +Date: Thu Jun 8 18:57:51 2017 +0200 + + Check Django 1.11 compatibility + +commit 3d6d6211ba90cec8376672cbdaeaa90c953545ea +Merge: 6f27c41 1f32f02 +Author: Julien Maupetit +Date: Thu Jun 8 19:00:20 2017 +0200 + + Merge pull request #14 from TailorDev/fix-batch-import-perm + + Improve batch import view + +commit 1f32f021082cd8c91306e3f43f87687333fb3234 +Author: Julien Maupetit +Date: Thu Jun 8 18:11:41 2017 +0200 + + Restore compatibility with Django<1.10 releases + +commit be8b9720b105e491584bf7450b61b336b468b318 +Author: Julien Maupetit +Date: Thu Jun 8 17:26:37 2017 +0200 + + Remove the import success view in favor of a msg + +commit a584023a1f7a408f2e8fcdf0cb3d8d1157db8952 +Author: Julien Maupetit +Date: Thu Jun 8 17:05:39 2017 +0200 + + Restrict import view to super users + +commit 6f27c418f3b1ee77b2da203c1b01bd37693a64d9 +Merge: 05828b2 dc91d3a +Author: Julien Maupetit +Date: Thu Jun 8 16:19:56 2017 +0200 + + Merge pull request #13 from TailorDev/batch-import-view + + Add batch importation from PMIDs or DOIs + +commit dc91d3a079a4dbee7ee2b567c56a8395e3942dd9 +Author: Julien Maupetit +Date: Thu Jun 8 15:52:52 2017 +0200 + + Fix DOILoader docstrings + +commit 8b0a011a7f41c1d2f3448c9ae6315466d8159a6c +Author: Julien Maupetit +Date: Thu Jun 8 14:20:46 2017 +0200 + + Submit a list to CN and expect unicode response + + This fixes python 2 compat + +commit 69f45f0f490d854335eed03198754fbb65fc9110 +Author: Julien Maupetit +Date: Wed May 24 16:14:53 2017 +0200 + + Improve importation form wording + +commit d395f39200fe3b77d413e8746fad4f8f832b943c +Author: Julien Maupetit +Date: Wed May 24 15:50:31 2017 +0200 + + Fix inmportation form placeholders + + Carriage returns are not rendered in firefox + +commit e664c5ec6bb90f65ab159fdf332c06d4bd0f42e2 +Author: Julien Maupetit +Date: Wed May 24 15:31:12 2017 +0200 + + Add app_name url to td_biblio + +commit f33c302b93ea7673f248a0ee219145661fea6634 +Author: Julien Maupetit +Date: Wed May 24 11:44:00 2017 +0200 + + Fix python 2/3 compatibility + +commit b46d65d18a520b4bc4afcb65d182e643a7bccce6 +Author: Julien Maupetit +Date: Tue May 23 17:03:54 2017 +0200 + + Bump habanero release to 0.3.0 + +commit 0235cc8738326589ba370e1f00b9a94fc0b9c883 +Author: Julien Maupetit +Date: Tue May 23 17:02:27 2017 +0200 + + Fix pep8 + +commit 9d8ed89b9426180c887662f89a82e1e11b6ba576 +Author: Julien Maupetit +Date: Tue May 23 16:55:17 2017 +0200 + + Add missing test for the Entry model + +commit 144eac89eb409f1d0306726f8c7a8e8c15b807de +Author: Julien Maupetit +Date: Tue May 23 16:55:00 2017 +0200 + + Add tests for importation views + +commit 24bb92edc18ea65166873fa41cd8db3ed6d62b5d +Author: Julien Maupetit +Date: Tue May 23 16:29:19 2017 +0200 + + Add tests for forms + +commit cbffdeaf350e0c936f0fadbfeb4a432bdb8ef616 +Author: Julien Maupetit +Date: Tue May 23 16:27:53 2017 +0200 + + Fix typo + +commit ea09b341b7dba1ca1a7c9788d260b43b8d2cf6a0 +Author: Julien Maupetit +Date: Tue May 23 12:53:41 2017 +0200 + + Improve DOILoader robustness + +commit 6fcb6e8f6b37e2bf42cb838ff46510f14d2c4dad +Author: Julien Maupetit +Date: Tue May 23 12:02:13 2017 +0200 + + Fix namespaced url + +commit 5771b577b9c3db9f36679fdd181db690b10329a9 +Author: Julien Maupetit +Date: Tue May 23 11:59:08 2017 +0200 + + Add tests for the BibTexLoader + +commit 4ec03ba6e5fd359110f45585e96d2b117d776953 +Author: Julien Maupetit +Date: Tue May 23 11:19:47 2017 +0200 + + Add tests for batch PMID/DOI loading + +commit b57e2f5800a325bede243572aa619e590a7604c3 +Author: Julien Maupetit +Date: Mon May 15 23:18:26 2017 +0200 + + Add draft implementation of the importation view + +commit 8c34d9281d0e0ecdacf39a9d6cc9f0e594c296c7 +Author: Julien Maupetit +Date: Mon May 15 22:59:50 2017 +0200 + + Fix empty number requests + +commit a35e341d7c13940809992794f84ae88d40a3c068 +Author: Julien Maupetit +Date: Fri May 12 16:39:07 2017 +0200 + + Add DOILoader + +commit a01d6a349ea62a9693db463f95d1f798ebf430d7 +Author: Julien Maupetit +Date: Wed Apr 12 08:15:35 2017 +0200 + + Fix python 2/3 compatibility + +commit 99eef0a4a1e3544b27562ffd93bb4c0f345e8cb8 +Author: Julien Maupetit +Date: Tue Apr 11 23:10:50 2017 +0200 + + Fix py27 unicode support + +commit 6ebd9ae02026d867ef0f9b3eda85f4b7cedabef1 +Author: Julien Maupetit +Date: Tue Apr 11 22:50:57 2017 +0200 + + Fix pep8 + +commit 971423519934c9776ee15647856cd1022a86edaa +Author: Julien Maupetit +Date: Tue Apr 11 18:59:37 2017 +0200 + + Add PubmedLoader + +commit 11c17e71a7a0cee89a7b517b9536c16a51da25e2 +Author: Julien Maupetit +Date: Tue Apr 11 18:09:59 2017 +0200 + + Move utils.managers -> utils.loaders + +commit 9fdefeaa1a9c24fb4c1144afa90c5c5d63fb3ce5 +Author: Julien Maupetit +Date: Tue Apr 11 18:05:38 2017 +0200 + + Refactor the bibtex loader + + * Add a BaseLoader to ease new loader implementation + * Add a BibTexLoader + +commit 05828b21d0b5f4be7c1ce139cef81ecd2e5192df (tag: 1.0.1) +Author: Julien Maupetit +Date: Thu Mar 23 19:26:52 2017 +0100 + + Bump release to 1.0.1 + +commit c1fa30f8c89370b44c4fb31f755779afab710657 +Author: Julien Maupetit +Date: Thu Mar 23 19:23:32 2017 +0100 + + Fix missing dev requirements + +commit 5c06dd7df62af1c6a1cdc18621f1a4fdb96ab76b (tag: 1.0.0) +Author: Julien Maupetit +Date: Thu Mar 23 18:56:53 2017 +0100 + + Bump release to 1.0.0 + +commit b6ea911fa5fc8b02a8a4bcc28e8a723465d7f221 +Author: Julien Maupetit +Date: Thu Mar 23 18:50:25 2017 +0100 + + Add badges to the README + +commit 5b1fd36eeba60f4ed109818651d44f5625522d89 +Merge: c087586 a3bcae0 +Author: Julien Maupetit +Date: Thu Mar 23 18:41:45 2017 +0100 + + Merge pull request #8 from TailorDev/fix-js-deps + + Remove jQuery implicit dependency + +commit a3bcae07cad926cde23132cc054b30c63d4d5d36 +Author: Julien Maupetit +Date: Thu Mar 23 17:46:28 2017 +0100 + + Remove jQuery implicit dependency + + Fix #2 + These changes implement the form submission on filter change in pure JavaScript. + +commit c087586f69da97bc4af552016ee5e0ad6850646b +Merge: 2003f28 8bb6e9f +Author: Julien Maupetit +Date: Thu Mar 23 18:19:39 2017 +0100 + + Merge pull request #7 from TailorDev/fix-bibtex-import + + Fix bibtex import + +commit 8bb6e9fec4ff942951e319f7abdca21c874773c7 +Author: Julien Maupetit +Date: Thu Mar 23 18:11:32 2017 +0100 + + Use a logger to maintain Django>=1.7,<1.9 compat. + +commit 2c8e212eafe0a45bfa232354fe2bea3e25d91166 +Author: Julien Maupetit +Date: Thu Mar 23 18:02:02 2017 +0100 + + Fix handle usage with the new arg parser + +commit a4a3cb43d7e79d5800b0782fb96cf77f55213998 +Author: Julien Maupetit +Date: Thu Mar 23 17:11:07 2017 +0100 + + Improve base template and document it + +commit a6501c9da1e444647539b1fb80f2714101e8e4c4 +Author: Julien Maupetit +Date: Thu Mar 23 17:06:48 2017 +0100 + + Add log message when importation is done + +commit f57e3fbdc47ee11a572cd80fc521bd7337f9aa23 +Author: Julien Maupetit +Date: Thu Mar 23 17:05:39 2017 +0100 + + Switch bibtex_import command to argparse + + Fix #4 + +commit 2003f28410b741c3da69127230d548bc0a94b25a +Merge: 3ae4479 f2d1eba +Author: Julien Maupetit +Date: Thu Mar 23 17:46:14 2017 +0100 + + Merge pull request #6 from TailorDev/fix-static-urls + + Fix static urls in templates + +commit f2d1eba7d04d62d8cdf75db3db61283d711c0988 +Author: Julien Maupetit +Date: Thu Mar 23 16:13:03 2017 +0100 + + Fix static urls in templates + + Fix #5 + +commit 3ae44790549d575c93729f42dd51da5d22291820 +Merge: aaa50c8 4afd76e +Author: Julien Maupetit +Date: Thu Mar 23 17:11:56 2017 +0100 + + Merge pull request #1 from TailorDev/py3-dj110 + + Add python 3 and django >= 1.7 compatibility + +commit 4afd76e7e155ada68eb9c0a96a6b451cf59687df +Author: Julien Maupetit +Date: Thu Mar 23 15:44:52 2017 +0100 + + Fix classifiers and other meta + +commit d3fcc401ccd93c7cd69f97e6822cfe656a304ba2 +Author: Julien Maupetit +Date: Thu Mar 23 15:32:53 2017 +0100 + + Update docs + +commit 8cb4655139d51ce4fdebc2aad1a57f450bc01b82 +Author: Julien Maupetit +Date: Thu Mar 23 15:31:46 2017 +0100 + + Fix LICENSE formatting & copyright holder + +commit 58c932811b90581eb68e14f07be72017714e0cc2 +Author: Julien Maupetit +Date: Fri Mar 17 09:32:33 2017 +0100 + + Fix author repr for py2/3 compat + +commit 8d4cb1f2af5e79a418cf392d03ee25981e160c18 +Author: Julien Maupetit +Date: Fri Mar 17 01:24:43 2017 +0100 + + Ignore django 1.7 with python 3.5+ + + It raises the following error: + AttributeError: module 'html.parser' has no attribute 'HTMLParseError' + +commit 5ec27c5c1da7b3550a0a10b2831b6a3e510851fa +Author: Julien Maupetit +Date: Fri Mar 17 00:35:05 2017 +0100 + + Restore non lazy url reverse + + This must fix compatibility with older django releases (<1.9) + +commit b8f271cb15b2c49140ba1077783a3366d9a8b37c +Author: Julien Maupetit +Date: Thu Mar 16 22:17:55 2017 +0100 + + Add travis configuration + + Run tests with python2.7 or python3.4 combined with django 1.7 to 1.10 + +commit 9d10aa4810840fccb2fba8cd41dac3a61135b66a +Author: Julien Maupetit +Date: Thu Mar 16 17:12:49 2017 +0100 + + Configure flake8 & fix pep8 + +commit 138d88d5ee854f70696fe71c25a82ff9811a4014 +Author: Julien Maupetit +Date: Thu Mar 16 16:58:57 2017 +0100 + + Add py.test coverage + +commit df18ebbf690d3ef9e05fe9c84980d4c3f692936f +Author: Julien Maupetit +Date: Thu Mar 16 16:58:26 2017 +0100 + + Run the sandbox server from the project root + +commit 4e209211145eaade105b95f2828706bb23bacbb4 +Author: Julien Maupetit +Date: Thu Mar 16 16:58:13 2017 +0100 + + Fix missing wsgi file + +commit 624636310cb13035d99852e89a42c8f13ad6b531 +Author: Julien Maupetit +Date: Thu Mar 16 16:47:48 2017 +0100 + + Fix map usage + +commit 9b8f3c2054f524a34f58fe15a275c7a716978b55 +Author: Julien Maupetit +Date: Thu Mar 16 16:47:21 2017 +0100 + + Simplify the sandbox tree + +commit 97f7252d89a74a6ba301b3389968c6e73ec93246 +Author: Julien Maupetit +Date: Thu Mar 16 16:34:46 2017 +0100 + + Fix unicode -> str + +commit 3c80a83c77ede2c9159f12c3531c6a96063b9472 +Author: Julien Maupetit +Date: Thu Mar 16 16:29:39 2017 +0100 + + Remove unused py2/3 compatibility layer + +commit 437f2565d125865fe076a3f3850fc19178b4006e +Author: Julien Maupetit +Date: Thu Mar 16 16:28:23 2017 +0100 + + Use new bibtexparser method to import bibtex files + +commit 0496bd5d38a933e76579e56489004428429d7a17 +Author: Julien Maupetit +Date: Thu Mar 16 16:27:33 2017 +0100 + + Switch to py.test + + Fix #3 + +commit 1df1f58710b390b330c08c86d689c7eb7081550c +Author: Julien Maupetit +Date: Thu Mar 16 15:56:04 2017 +0100 + + Fix unicode strings + +commit 06bbf48d340b376a88cda1e4cc79665d940df7ef +Author: Julien Maupetit +Date: Thu Mar 16 15:16:36 2017 +0100 + + Lazy reverse urls in tests + +commit 4ed5716f7844f7168a6fe5b142d7bd73486b87e4 +Author: Julien Maupetit +Date: Thu Mar 16 15:16:08 2017 +0100 + + Make td_biblio a configurable app + +commit 14c393bd003cb569f8f4ba76bebdaaca67fbc24a +Author: Julien Maupetit +Date: Wed Mar 15 11:48:19 2017 +0100 + + Switch to factory boy >= 2.6 + + * names is no longer required as Faker is now bundled with factory boy + * Use new factory boy schema with faker + +commit 5cc0df9f3371a3d11db358b89f198a632e64c530 +Author: Julien Maupetit +Date: Wed Mar 15 11:31:42 2017 +0100 + + Fix pep8 + +commit 86a1cdd964564ffbdbf91e6fc31d696edffb62e3 +Author: Julien Maupetit +Date: Wed Mar 15 11:18:10 2017 +0100 + + Fix xrange -> range + +commit 33a844b89d62ad4649ab484fd349e6f8aeb34ae3 +Author: Julien Maupetit +Date: Wed Mar 15 11:17:36 2017 +0100 + + Fix lambda syntax + +commit 8d613072e3f73c0ebea093140a975fd72249e6ae +Author: Julien Maupetit +Date: Wed Mar 15 11:16:15 2017 +0100 + + Lazy load gettext + +commit 6aa23223792164193615171d7e43cad8d779bc81 +Author: Julien Maupetit +Date: Wed Mar 15 11:06:26 2017 +0100 + + Remove silly headers + +commit 9dd63cb5dd0a366d14980de2b149b6327216afda +Author: Julien Maupetit +Date: Wed Mar 15 11:06:10 2017 +0100 + + Improve audience + +commit f204fa0c61b45ec7752ce6953869c7f9c936aeb3 +Author: Julien Maupetit +Date: Wed Mar 15 10:14:04 2017 +0100 + + Fix required base dependencies file name + +commit 50a6549254ffa67fe199fc7d130554a08c3f4ec6 +Author: Julien Maupetit +Date: Wed Mar 15 10:12:35 2017 +0100 + + Add setup config + +commit b5f8bb6d58ba2c7ab99da0c3d56f34e6065402d7 +Author: Julien Maupetit +Date: Wed Mar 15 10:11:40 2017 +0100 + + Move requirements to project root + +commit 3c3e1f7b3e7792f5f568a62f89394231edd01c61 +Author: Julien Maupetit +Date: Wed Mar 15 10:07:47 2017 +0100 + + Make setup.py executable and fix meta + +commit a82cd0bdf4bb28d0fa554819888e024bed4b4117 +Author: Julien Maupetit +Date: Wed Mar 15 10:01:34 2017 +0100 + + Fix package meta + +commit d423668902a87c17e73f3521e58571709c9b9283 +Author: Julien Maupetit +Date: Wed Mar 15 09:59:36 2017 +0100 + + Switch to django new url schema + +commit 0a8c4578c8be2aa5b1f51ae2d66fcb28e214f83b +Author: Julien Maupetit +Date: Wed Mar 15 09:57:43 2017 +0100 + + Remove unused Sphinx dependency + +commit 36767e873f8b23bbb1d5396df0b787170bea6e8f +Author: Julien Maupetit +Date: Wed Mar 15 09:57:28 2017 +0100 + + Ignore py3 cache + +commit d1f5584c84eb20b735c178f1673c644aab0c80a6 +Author: Julien Maupetit +Date: Wed Mar 15 09:54:20 2017 +0100 + + Add django initial migration + +commit 5ab6363dc6519cf7239d7c377afd64c5b8da9585 +Author: Julien Maupetit +Date: Wed Mar 15 09:53:50 2017 +0100 + + Remove South legacy migrations + +commit 96a2309bbf8313c76e0c9e77f90a5af19b465b19 +Author: Julien Maupetit +Date: Tue Mar 14 16:41:43 2017 +0100 + + Upgrade Django minimal release compatibility + +commit 90c9606145e15194ee4cd61bd189ad09ca075320 +Author: Julien Maupetit +Date: Tue Mar 14 16:37:00 2017 +0100 + + Ignore virtualenv + +commit 02ba42b0ac6ae08016a1da1c530379f400fb375d +Author: Julien Maupetit +Date: Tue Mar 14 16:36:19 2017 +0100 + + Fix pip requirements parsing + +commit aaa50c8537d6ba65410a0b9abfda4475fab5f38c (tag: 0.3) +Merge: b9dc77e 7d7d9a8 +Author: Julien Maupetit +Date: Tue Feb 3 11:52:47 2015 +0100 + + Merge branch 'release/0.3' + +commit 7d7d9a8aae81cc93753cd7b5ee85ebb871af8a08 +Author: Julien Maupetit +Date: Tue Feb 3 11:52:36 2015 +0100 + + Bump release to 0.3 + +commit 2794db8727bb14fff457bb11a521d750edf70ee2 +Author: Julien Maupetit +Date: Tue Feb 3 11:49:34 2015 +0100 + + Use partial template for publication list + +commit 0a2df2a6ba1fcf46c4f0199f71ccb465ce551eb8 +Merge: 19eea04 f0db670 +Author: Julien Maupetit +Date: Tue Feb 3 11:28:57 2015 +0100 + + Merge branch 'release/0.2.1' into develop + +commit b9dc77e2c3e180c9339c7e421235153ed6d4a231 (tag: 0.2.1) +Merge: 054161c f0db670 +Author: Julien Maupetit +Date: Tue Feb 3 11:28:44 2015 +0100 + + Merge branch 'release/0.2.1' + +commit f0db6703149f6ee5e1619eec67808bda6569f7fa +Author: Julien Maupetit +Date: Tue Feb 3 11:28:24 2015 +0100 + + Bump release to 0.2.1 + +commit 19eea04053be505710a9537cfeb6afdcb0b2511f +Author: Julien Maupetit +Date: Tue Feb 3 11:26:34 2015 +0100 + + Entry last/first authors should be properties + + * Fix tests + +commit 8e7977f77273d69d6dda5617cd921151269a6cdc +Merge: e414475 227a4ce +Author: Julien Maupetit +Date: Mon Jun 2 20:38:59 2014 +0200 + + Merge branch 'release/0.2' into develop + +commit 054161c18ca39369ac425f1dea9e6dd59e52116d (tag: 0.2) +Merge: 59bf6a2 227a4ce +Author: Julien Maupetit +Date: Mon Jun 2 20:38:53 2014 +0200 + + Merge branch 'release/0.2' + +commit 227a4cec0490de613f50efa90eec02b8ab7a2892 +Author: Julien Maupetit +Date: Mon Jun 2 20:38:40 2014 +0200 + + Update changelog + +commit 00d013402a2210a9da12c05eec9286c3f97e4b1a +Author: Julien Maupetit +Date: Mon Jun 2 20:37:13 2014 +0200 + + Bump release to 0.2 + +commit e41447524417d81e0b939718a7d670aeff2e7b02 +Author: Julien Maupetit +Date: Mon Jun 2 17:36:19 2014 +0200 + + Add mapped user to Author admin list + +commit e43414dd845013aecec36254a97eed4d424156f7 +Author: Julien Maupetit +Date: Mon Jun 2 17:28:28 2014 +0200 + + Add ordering meta for Author and Editor objects + + * by default authors and editors are ordered by their last names, first names + +commit ce4c799c3d35453685d1c36ef8ff60f326f613b6 +Merge: c5ac89f 5dc80ce +Author: Julien Maupetit +Date: Mon Jun 2 15:17:43 2014 +0200 + + Merge branch 'feature/incomplete-date' into develop + + Fix issue #11 + +commit 5dc80cede3fbc94cb64da75302a4ef53086acb63 +Author: Julien Maupetit +Date: Mon Jun 2 15:17:09 2014 +0200 + + Add tests for the templatetag library + +commit f80ada52a1f5d518251bf33b8daf5025494d5e5b +Author: Julien Maupetit +Date: Mon Jun 2 15:16:53 2014 +0200 + + Ignore migrations for coverage + +commit f33256a5190a0454d676d3b60f78ccd6b073f7f8 +Author: Julien Maupetit +Date: Fri May 30 18:58:34 2014 +0200 + + Add a publication_date templatetag filter + + * The publication_date filter changes publication date format considering if the publication date is complete or not + * Integrate this filter in the ieeetr bibliography style + +commit e673fe270fed2da6ec2ff1a33f670554f072c218 +Author: Julien Maupetit +Date: Fri May 30 18:44:49 2014 +0200 + + Add a test for partial publication date flag ... + + ... while importing from BibTeX + +commit 42939bccd506958f952ad1d5c8996c8b9a1070b0 +Author: Julien Maupetit +Date: Fri May 30 18:44:23 2014 +0200 + + Add support for bibtex month field import + +commit f2eff3629f22334266be1bcda0f7462059663813 +Author: Julien Maupetit +Date: Fri May 30 17:52:00 2014 +0200 + + Add is_partial_publication_date field to Entry + + We need this flag to identify full publication dates (day/month/year) from partial publication dates (year only most of the time). + + * add admin integration + * add south migrations + +commit 99708284517d5e23403ae18a4d5e5ebdc5ba1798 +Author: Julien Maupetit +Date: Fri May 30 17:51:09 2014 +0200 + + Ignore build + +commit c5ac89f8f804dd527a1e5f1468f14a4a4d99a956 +Merge: 426bba8 25e6332 +Author: Julien Maupetit +Date: Thu Feb 13 12:17:51 2014 +0100 + + Merge branch 'hotfix/0.1.5' into develop + +commit 59bf6a29195257009810b5eb51002c507e259c81 (tag: 0.1.5) +Merge: a1bf11f 25e6332 +Author: Julien Maupetit +Date: Thu Feb 13 12:17:43 2014 +0100 + + Merge branch 'hotfix/0.1.5' + +commit 25e63324b464ccc4a5b7acd291f3dfd07e13afab +Author: Julien Maupetit +Date: Thu Feb 13 12:16:29 2014 +0100 + + Fix misplaced quotes around publications titles + +commit 45d1983fa9aeb09342f8b9a196a90a2187028e49 +Author: Julien Maupetit +Date: Thu Feb 13 12:15:55 2014 +0100 + + Bump version to 0.1.5 + +commit 426bba88c1cf557501f4a0745e8e44ebffd8e83f +Merge: d31caf5 70df96f +Author: Julien Maupetit +Date: Mon Feb 10 16:46:32 2014 +0100 + + Merge branch 'hotfix/0.1.4' into develop + +commit a1bf11fbcd501b67ec467bdd265e6728b82cf9e7 (tag: 0.1.4) +Merge: 34de620 70df96f +Author: Julien Maupetit +Date: Mon Feb 10 16:46:26 2014 +0100 + + Merge branch 'hotfix/0.1.4' + +commit 70df96fc205bd7059c1eb57ed8e6c996a24b5ddf +Author: Julien Maupetit +Date: Mon Feb 10 16:45:41 2014 +0100 + + Add tests for the new entry get_authors method + +commit e30c56406b372d9d50176bfafd8e04d0dc626ede +Author: Julien Maupetit +Date: Mon Feb 10 16:44:06 2014 +0100 + + Fix issue #9 (add Entry get_authors method) + + To access an entry authors list, use the Entry.get_authors() method. This ensure authors ordering. + +commit 5e2697b55f1720c4c144840e680004fb28a3cfcc +Author: Julien Maupetit +Date: Mon Feb 10 16:41:59 2014 +0100 + + Add more flexibity to run tests independantly + +commit 4fed92ec9741f6270f487f1b1a106ce07355db24 +Author: Julien Maupetit +Date: Mon Feb 10 16:40:55 2014 +0100 + + Fix authors rank in factory + +commit c5224a8eee912bbce404ce2d0847869690dcc5c0 +Author: Julien Maupetit +Date: Mon Feb 10 15:30:55 2014 +0100 + + Fix issue #10 (journal abbreviation display) + +commit 5ef8e5092cdcbd2ad9fa5e3f560391c98d4459a1 +Author: Julien Maupetit +Date: Mon Feb 10 14:42:28 2014 +0100 + + Bump version to 0.1.4 + +commit d31caf58a5765e68d8f758edea63116f4b8a11bc +Merge: b661357 bfb6b0b +Author: Julien Maupetit +Date: Mon Feb 3 15:19:08 2014 +0100 + + Merge branch 'hotfix/0.1.3' into develop + +commit 34de6205a6c4b58022da3112deb8b99769126ae2 (tag: 0.1.3) +Merge: 1164720 bfb6b0b +Author: Julien Maupetit +Date: Mon Feb 3 15:18:55 2014 +0100 + + Merge branch 'hotfix/0.1.3' + +commit bfb6b0bfdeccad12c050bf0074d2a85bc3e65535 +Author: Julien Maupetit +Date: Mon Feb 3 15:18:21 2014 +0100 + + Fix issue #8 (name unpacking) + +commit d25f860c56e4e51203574ee8da4297c7aaa6195a +Author: Julien Maupetit +Date: Mon Feb 3 15:18:13 2014 +0100 + + Bump version to 0.1.3 + +commit b6613579aeddedbac02be99e88e54ab7683e05b1 +Merge: 2108969 2002f3b +Author: Julien Maupetit +Date: Fri Jan 10 16:58:07 2014 +0100 + + Merge branch 'hotfix/0.1.2' into develop + +commit 11647204b16275e1b0d6aca608abaee6a90111f8 (tag: 0.1.2) +Merge: 7ba6f9b 2002f3b +Author: Julien Maupetit +Date: Fri Jan 10 16:58:00 2014 +0100 + + Merge branch 'hotfix/0.1.2' + +commit 2002f3ba475c7021d0eca3a5d16765b0f3e134e5 +Author: Julien Maupetit +Date: Fri Jan 10 16:57:28 2014 +0100 + + Bump version to 0.1.2 & update module docstring + +commit 921e636ee6ed5927d25dc4f92c14437b2ba5b723 +Author: Julien Maupetit +Date: Fri Jan 10 16:57:12 2014 +0100 + + Fix typos + +commit 52ccd03918c71f4ade414d55b295c1b5c3019679 +Author: Julien Maupetit +Date: Fri Jan 10 16:53:27 2014 +0100 + + Add requirements for packaging + +commit 2108969491e7b165050e3dc6f45916d1e76dcb83 +Merge: 5828188 540662d +Author: Julien Maupetit +Date: Fri Jan 10 16:39:59 2014 +0100 + + Merge branch 'hotfix/0.1.1' into develop + +commit 7ba6f9bdc0451c840205b9c4b6f67455909699ac (tag: 0.1.1) +Merge: f237072 540662d +Author: Julien Maupetit +Date: Fri Jan 10 16:39:54 2014 +0100 + + Merge branch 'hotfix/0.1.1' + +commit 540662dd68f2702314912b84d8527f6ee6bb73ce +Author: Julien Maupetit +Date: Fri Jan 10 16:39:39 2014 +0100 + + Bump version to 0.1.1 + +commit ae2aedbfaec22a927a2855d437d47bb25dae4a22 +Author: Julien Maupetit +Date: Fri Jan 10 16:38:58 2014 +0100 + + Fix installation instruction in the doc + +commit 5828188c7e6883687615ec8894bf753a342f854a +Merge: b092830 f576908 +Author: Julien Maupetit +Date: Fri Jan 10 16:33:32 2014 +0100 + + Merge branch 'release/0.1' into develop + +commit f237072a19c0e6e6edd39601bacf1bb2bafd9e02 (tag: 0.1) +Merge: 7d1b0a1 f576908 +Author: Julien Maupetit +Date: Fri Jan 10 16:33:27 2014 +0100 + + Merge branch 'release/0.1' + +commit f57690882e9648b4c04f14b94267c7eb243f4b84 +Author: Julien Maupetit +Date: Fri Jan 10 16:33:19 2014 +0100 + + Update changelog for this first release + +commit c7902e104a2eb7ec0b9e8ca1fe2855a8438f265c +Author: Julien Maupetit +Date: Fri Jan 10 16:27:59 2014 +0100 + + Bump version to 0.1 + +commit b0928301965202faa2a73740658b82dd455bbe4f +Author: Julien Maupetit +Date: Fri Jan 10 16:27:01 2014 +0100 + + Minor fix for the release + +commit 92ba0d635639a7a4bd6c77144e5d0d4c596cc848 +Author: Julien Maupetit +Date: Fri Jan 10 16:26:32 2014 +0100 + + Update documentation for the first public release + +commit 48bbdadadf8be7e62d6af26167455ee7fdd8a52a +Author: Julien Maupetit +Date: Fri Jan 10 16:25:55 2014 +0100 + + Add test fixtures (.bib) for packaging + +commit 6ead90ef2cd9396f991b50c901076100de2155b3 +Merge: ce606cf 70b83f9 +Author: Julien Maupetit +Date: Fri Jan 10 16:13:17 2014 +0100 + + Merge branch 'feature/bibtex-parser' into develop + +commit 70b83f9f90625e1238713c1d780337509ae584e4 +Author: Julien Maupetit +Date: Fri Jan 10 16:12:38 2014 +0100 + + Add test and fixtures for the bibtex_import command + +commit ef43052df23c04bae792873e45135295d2999e0e +Author: Julien Maupetit +Date: Fri Jan 10 16:12:09 2014 +0100 + + Add a missing test for models + +commit bae6df7ed8d4a1ab1c25902175f6b9711b604e6b +Author: Julien Maupetit +Date: Fri Jan 10 16:11:02 2014 +0100 + + Add logging info to the bibtex_import command + +commit 9f6e816b1a2f55b400b03287c92ceae209a4a704 +Author: Julien Maupetit +Date: Fri Jan 10 15:23:58 2014 +0100 + + Customize bibtexparser record post-treatments + +commit 3d6f0509423b0948343d34ffbf654fe623fb97ab +Author: Julien Maupetit +Date: Thu Jan 9 18:06:06 2014 +0100 + + Add an admin command line for bibtex import + +commit 0978762cf2fdf87a5203059cc103ad0c9f16f694 +Author: Julien Maupetit +Date: Thu Jan 9 18:03:47 2014 +0100 + + First draft for the bibtex importation tool + +commit 167da4199f350ed37c2f2ab813b90b80cc605e21 +Author: Julien Maupetit +Date: Thu Jan 9 18:01:24 2014 +0100 + + Improve the Entry unicode representation + +commit 6cec883af765e229d928719b4a8f17b41c05bb70 +Author: Julien Maupetit +Date: Thu Jan 9 18:00:47 2014 +0100 + + Add bibtexparser as a dependency + +commit ce606cf2b97a9c01bd499b4b2321c5315a6becc9 +Merge: c21c7fe ee5ea66 +Author: Julien Maupetit +Date: Thu Jan 9 14:53:34 2014 +0100 + + Merge branch 'feature/author-position' into develop + +commit ee5ea66b3218de9d59ae192e701e456a41b86597 +Author: Julien Maupetit +Date: Thu Jan 9 14:51:56 2014 +0100 + + Add tests for the new AuthorEntryRank model + + Test coverage is now back to 100% + +commit c9d8734de64c046695b3a3f969ce0d6eb4593f06 +Author: Julien Maupetit +Date: Thu Jan 9 14:51:21 2014 +0100 + + Update AuthorEntryRank unicode representation + +commit 49a5deabcff2087ddfae639ab7ff43b9163bf2c6 +Author: Julien Maupetit +Date: Thu Jan 9 14:26:23 2014 +0100 + + Fix tests for the new Entry-Authors relashionship + +commit 3050badf4b8a5ff15e3fe7dd485dcb8936f5d27e +Author: Julien Maupetit +Date: Thu Jan 9 14:25:19 2014 +0100 + + Add EntryWithStaticAuthorsFactory + +commit b1056f2c8282115fdacf8e3008cfb512dca1af29 +Author: Julien Maupetit +Date: Thu Jan 9 12:49:53 2014 +0100 + + Add EntryWithAuthorsFactory + +commit 53d398dc74ac9e8ae1bdb9dc2adbff628c20481f +Author: Julien Maupetit +Date: Thu Jan 9 12:49:14 2014 +0100 + + Allow Entry authors (+rank) inline edition in the admin + +commit 504a7b421a25eda458564ddbc24722fb3bd50522 +Author: Julien Maupetit +Date: Thu Jan 9 12:46:31 2014 +0100 + + Add an intermediate AuthorEntryRank model + + Primary work on issue #1 + +commit c21c7fee6be006686c3b04a613f6c3b34a6feae3 +Author: Julien Maupetit +Date: Sat Jan 4 00:01:45 2014 +0100 + + Add tests for the Author/User mapping + +commit 2e763d8e5688e2dd8ec4ebdbb74752a1d41b24ae +Author: Julien Maupetit +Date: Sat Jan 4 00:01:10 2014 +0100 + + Add test for collection filtering in the EntryListView + +commit 3663cbdc2ccd2bf28cd0fb838b06122eaed404db +Author: Julien Maupetit +Date: Fri Jan 3 23:40:48 2014 +0100 + + Add Author/User mapping method to AbstractHuman + + First step to fix issue #3 + +commit 9575a84286c5724f09adfd65a748c5081da3d934 +Author: Julien Maupetit +Date: Fri Jan 3 22:59:02 2014 +0100 + + Improve admin interface performance and usability + +commit 6c345716946d883876132c95a148f8f02210128e +Author: Julien Maupetit +Date: Fri Jan 3 22:44:37 2014 +0100 + + Display the collection size in admin list + +commit 6a396aadda50e92a7c59fb43548ca13e2c33c74b +Author: Julien Maupetit +Date: Fri Jan 3 22:32:55 2014 +0100 + + Add EntryListView collection filter - Fix issue #4 + + The most part of the work now should be: automatically generate collections from lab teams and platforms. This should be managed outside from this app. + +commit 6af0026ee9253612554f11e8b200e69f383611e7 +Author: Julien Maupetit +Date: Fri Jan 3 20:26:20 2014 +0100 + + Improve Entry model admin + + Fix issue #5 + +commit 388419b321d82d8d51c5887d9dd3df85b38b8726 +Author: Julien Maupetit +Date: Fri Jan 3 19:54:25 2014 +0100 + + Add an example of bibliography custom style + + This is the first step to implement the issue #2 + +commit 12aa2ad1db8e9a058f39ae601fa13090ca664498 +Author: Julien Maupetit +Date: Fri Jan 3 09:10:54 2014 +0100 + + Add a legend to publications stats + +commit e1c88f649f4d33dbf11d6927acc1d3cbf0c954d1 +Author: Julien Maupetit +Date: Fri Jan 3 09:09:46 2014 +0100 + + Update Entry admin required fields and improve help texts + +commit 2d3251c3551e40125271a3b5baf1a2e0a48478c8 +Author: Julien Maupetit +Date: Fri Jan 3 00:37:59 2014 +0100 + + EntryListView date filtering must be restricted to current queryset + +commit 3e1df71833d0fd45b771010e2f567e4e5db3f13c +Author: Julien Maupetit +Date: Fri Jan 3 00:34:57 2014 +0100 + + Add journals metric to the EntryListView + +commit e34f44a32dbb5bd85a0b21eb4691563f4dfe54a8 +Author: Julien Maupetit +Date: Fri Jan 3 00:28:30 2014 +0100 + + Add more assertions for context testing + +commit db0ba7c5736de87e1c74de3e3c9663a9b3d9c475 +Author: Julien Maupetit +Date: Fri Jan 3 00:24:48 2014 +0100 + + Add tests for the EntryListView filtering + +commit 1dfd79692470430a93c08b3f0bdb59290548a6b6 +Author: Julien Maupetit +Date: Fri Jan 3 00:00:11 2014 +0100 + + Add by Author EntryListView filtering + +commit 307b513f2b6ca32547f912437f58000519972d3e +Author: Julien Maupetit +Date: Thu Jan 2 23:21:09 2014 +0100 + + Update an Entry representation and related test + +commit c80b1400e8fb685a983a87340b7c4cbbe6f91eec +Author: Julien Maupetit +Date: Thu Jan 2 22:36:37 2014 +0100 + + Fix pep8 + +commit 4cf38effdf57bb84463ace1c59ade8aef6f51334 +Author: Julien Maupetit +Date: Thu Jan 2 22:35:59 2014 +0100 + + Increase tests to 100% code coverage. + + w00t! + +commit f66659710a39ce6569f0c38342efd0965854a184 +Author: Julien Maupetit +Date: Thu Jan 2 19:58:12 2014 +0100 + + We don't need to test standard factories + +commit f78939598402d8b3a3a43eecae24500aa9ba83a8 +Author: Julien Maupetit +Date: Thu Jan 2 19:57:24 2014 +0100 + + Improve EntryList DOM for semantic purpose + +commit ab364d74239cf87b390b7b0191709407f4ab4a90 +Author: Julien Maupetit +Date: Thu Jan 2 19:55:53 2014 +0100 + + The AbstractHuman _set_first_initial method should not return anything + +commit 4f55fe2813e43bac1bc939d493b63478ced19910 +Author: Julien Maupetit +Date: Tue Dec 31 17:26:42 2013 +0100 + + Add tests for the EntryListView + + * Modify running tests settings + * Add a default _layout/base.html template to inherit from + * Add 5 tests for now + + Known issue: we need to refine per page testing combined with qs filtering + +commit 609fb740da08061476e7b44beaafb0bf2d2d78c2 +Author: Julien Maupetit +Date: Tue Dec 31 12:08:02 2013 +0100 + + Fix and add more tests for Entry model qs ordering + + * fix the base test by explicitly setting the publication date + * add a specific test for ordering (ids order is not publication dates order) + +commit 2761fc1dfb8fa8c7f7951dd3c83f2ae37d29e97b +Author: Julien Maupetit +Date: Tue Dec 31 12:01:17 2013 +0100 + + Refactor the EntryListView + + * separate parameter checking from context modification + * ease testing + +commit 9fd33cb14bb54d31af6d84d7ec906f6d1f10117b +Author: Julien Maupetit +Date: Tue Dec 31 12:00:42 2013 +0100 + + Add configuration file for coverage + +commit a0b098816611a48c50c4d2a832a588218822aa76 +Author: Julien Maupetit +Date: Tue Dec 31 11:48:31 2013 +0100 + + Improve DOM semantic + +commit 9011177ae5f5bd0072300e96a531224b3d9491b5 +Author: Julien Maupetit +Date: Mon Dec 30 16:35:48 2013 +0100 + + Add functionnal filtering to entry list + +commit 78f768cddd20dc3660d5347a0ab32a21577c73c5 +Author: Julien Maupetit +Date: Mon Dec 30 14:01:59 2013 +0100 + + Auto-submit entry list filtering on form change + + Add a new javascript (jQuery script) to all layouts + +commit 4cb3aff6fd2de0ee6406a4cc6f0c871193d63cb8 +Author: Julien Maupetit +Date: Mon Dec 30 13:41:26 2013 +0100 + + Add entry list basic view + +commit e8eda2323b3486d1c88ffb0d5fd9ac7efca555d1 +Author: Julien Maupetit +Date: Mon Dec 30 13:40:54 2013 +0100 + + Add first author to entry admin list display + +commit 9656208c1b39878715940f5ade3e8f66f0a67adf +Author: Julien Maupetit +Date: Thu Dec 26 21:32:08 2013 +0100 + + Update admin display, ordering and filters + +commit 399cd799ae993412a6ad2455b8e11f4019aa9509 +Author: Julien Maupetit +Date: Thu Dec 26 21:19:44 2013 +0100 + + Add models admin + +commit 8e21fa6b00cf0fc2da9f2d28d3ca6777db83ef8a +Author: Julien Maupetit +Date: Thu Dec 26 20:53:33 2013 +0100 + + Add a test for django user/author linking + +commit 42f249521a64406507b3d5e41d4139e94f5d1487 +Author: Julien Maupetit +Date: Thu Dec 26 20:45:45 2013 +0100 + + Removed useless duplicated test for the AbstractHuman model + +commit 4fd89c7652c5176309e65a024d54f18c9c329285 +Author: Julien Maupetit +Date: Thu Dec 26 20:42:47 2013 +0100 + + Fix pep8 + +commit 8aa5546b428f9b632920d1a4b789ce36d13f863d +Author: Julien Maupetit +Date: Thu Dec 26 20:41:08 2013 +0100 + + Add tests for all current models + +commit 0c94f559b87b4525f2ae9905cae2ec14b45482ce +Author: Julien Maupetit +Date: Thu Dec 26 20:39:39 2013 +0100 + + Fix abstract factories and refine the CollectionFactory + + Since we use the FuzzyText attribute, factory-boy>=2.3.0 is now required + +commit 1daf4790c2c10ba23f0aae618e72980efaef215b +Author: Julien Maupetit +Date: Thu Dec 26 19:31:20 2013 +0100 + + Configure app tests running + +commit b7e8c863d83b03419360f74eba7e0078a0081660 +Author: Julien Maupetit +Date: Thu Dec 26 14:55:43 2013 +0100 + + Implement the EntryFactory + +commit 5f2a34986fee70fbdae8a123e55df2404a66c5a3 +Author: Julien Maupetit +Date: Thu Dec 26 14:54:46 2013 +0100 + + Upgrade local development dependencies + +commit 708e766c8bf869d4271346cd8ff10bfdd25e2b0a +Author: Julien Maupetit +Date: Thu Dec 26 14:43:32 2013 +0100 + + Some more work on models + + * Human `initials` field as been renamed `first_initial` + * Entry `publisher` field may be null + +commit 0f9940244834f3b9ba711dbb9b94fbd785ff0497 +Author: Julien Maupetit +Date: Thu Dec 26 13:28:17 2013 +0100 + + Implement the JournalFactory + +commit 8245693932cf329e92894be64324d619b3a7339b +Author: Julien Maupetit +Date: Thu Dec 26 13:22:52 2013 +0100 + + Fix setup script + +commit 57f6b104feaafae5c37eb12e0e7442bf4a97ef6b +Author: Julien Maupetit +Date: Tue Dec 24 10:06:52 2013 +0100 + + First draft for dynamic fixtures generation + +commit 21e8955c5b7f13d790dc5d6c8081313f421f8d01 +Author: Julien Maupetit +Date: Tue Dec 24 10:06:14 2013 +0100 + + Add names and loremipsum dependencies for fixture generation + +commit 5429317a1d22111343217a8dd10c685e49f0b7d2 +Author: Julien Maupetit +Date: Tue Dec 24 10:05:41 2013 +0100 + + Fix AbstractHuman model save method + +commit 972db7986ba973bfba2b9ba3853cac75a7cc4f12 +Author: Julien Maupetit +Date: Tue Dec 24 09:21:14 2013 +0100 + + Set Publisher and Journal models as entities + +commit b5056a886d1a033f26fe109414d223e9e5fc7626 +Author: Julien Maupetit +Date: Wed Nov 20 23:50:27 2013 +0100 + + First draft for models + +commit 652788e911ff468380252b98064a99a39276ee76 +Author: Julien Maupetit +Date: Wed Nov 20 16:44:47 2013 +0100 + + Update project base informations + +commit c7ce637c0df93b7d7b4d26f7ee50005c62f1095f +Author: Julien Maupetit +Date: Wed Nov 20 16:29:12 2013 +0100 + + First import of the template app + +commit 7d1b0a1246c45da157b342d14e34e024e4dddd92 +Author: Julien Maupetit +Date: Wed Nov 20 16:28:34 2013 +0100 + + Initial commit diff --git a/td_biblio/admin.py b/td_biblio/admin.py index d3d3b58..3662e3c 100644 --- a/td_biblio/admin.py +++ b/td_biblio/admin.py @@ -72,7 +72,8 @@ class EntryAdmin(admin.ModelAdmin): ("Cross References", {"fields": ("crossref",)}), ) inlines = (AuthorEntryRankInline,) - list_display = ("title", "first_author", "type", "publication_date", "journal") + list_display = ("title", "first_author", "type", "publication_date", + "journal") list_filter = ("publication_date", "journal", "authors") list_per_page = 20 list_select_related = True diff --git a/td_biblio/exceptions.py b/td_biblio/exceptions.py deleted file mode 100644 index 62b955a..0000000 --- a/td_biblio/exceptions.py +++ /dev/null @@ -1,15 +0,0 @@ -# td_biblio exceptions -class BaseLoaderError(Exception): - pass - - -class BibTeXLoaderError(BaseLoaderError): - pass - - -class PMIDLoaderError(BaseLoaderError): - pass - - -class DOILoaderError(BaseLoaderError): - pass diff --git a/td_biblio/exceptions/__init__.py b/td_biblio/exceptions/__init__.py new file mode 100644 index 0000000..543bd10 --- /dev/null +++ b/td_biblio/exceptions/__init__.py @@ -0,0 +1,9 @@ +# coding: utf-8 +__author__ = 'Alison Mukoma ' +__license__ = 'MIT' +__copyright__ = 'tailordev.fr' + +from .base import * # noqa +from .bibtex import * # noqa +from .doi import * # noqa +from .pmid import * # noqa diff --git a/td_biblio/exceptions/base.py b/td_biblio/exceptions/base.py new file mode 100644 index 0000000..a845912 --- /dev/null +++ b/td_biblio/exceptions/base.py @@ -0,0 +1,2 @@ +class BaseLoaderError(Exception): + pass diff --git a/td_biblio/exceptions/bibtex.py b/td_biblio/exceptions/bibtex.py new file mode 100644 index 0000000..c3ab02b --- /dev/null +++ b/td_biblio/exceptions/bibtex.py @@ -0,0 +1,5 @@ +from td_biblio.exceptions.base import BaseLoaderError + + +class BibTeXLoaderError(BaseLoaderError): + pass diff --git a/td_biblio/exceptions/doi.py b/td_biblio/exceptions/doi.py new file mode 100644 index 0000000..2c4c690 --- /dev/null +++ b/td_biblio/exceptions/doi.py @@ -0,0 +1,5 @@ +from td_biblio.exceptions.base import BaseLoaderError + + +class DOILoaderError(BaseLoaderError): + pass diff --git a/td_biblio/exceptions/pmid.py b/td_biblio/exceptions/pmid.py new file mode 100644 index 0000000..96130f7 --- /dev/null +++ b/td_biblio/exceptions/pmid.py @@ -0,0 +1,5 @@ +from td_biblio.exceptions.base import BaseLoaderError + + +class PMIDLoaderError(BaseLoaderError): + pass diff --git a/td_biblio/factories.py b/td_biblio/factories.py deleted file mode 100644 index 3884342..0000000 --- a/td_biblio/factories.py +++ /dev/null @@ -1,164 +0,0 @@ -# -*- coding: utf-8 -*- -import datetime -import factory -import random - -from factory.django import DjangoModelFactory -from factory.fuzzy import BaseFuzzyAttribute - -from . import models - - -JOURNAL_CHOICES = [ - ("Bioinformatics", "Bioinformatics"), - ("BMC Bioinf.", "BMC Bioinformatics"), - ("JACS", "Journal of the American Chemical Society"), - ("J. Comput. Chem.", "Journal of Computational Chemistry"), - ("Nat. Biotechnol.", "Nature Biotechnology"), - ("Nucleic Acids Res.", "Nucleic Acids Research"), - ( - "PNAS", - "Proceedings of the National Academy of Sciences of the United " - "States of America", - ), - ( - "Proteins Struct. Funct. Bioinf.", - "Proteins: Structure, Function, and Bioinformatics", - ), -] - -ENTRY_TYPES_RAW_CHOICES = [c[0] for c in models.Entry.ENTRY_TYPES_CHOICES] - - -# Custom fuzzy attributes definition -# -class FuzzyPages(BaseFuzzyAttribute): - """Random pages numbers separated by double-hyphens""" - - def __init__(self, low, high=None, **kwargs): - if high is None: - high = low - low = 1 - - self.low = low - self.high = high - - super(FuzzyPages, self).__init__(**kwargs) - - def fuzz(self): - start = random.randint(self.low, self.high) - end = random.randint(start, self.high) - return "%d--%d" % (start, end) - - -# Factories -# -class AbstractHumanFactory(DjangoModelFactory): - - first_name = factory.Faker("first_name") - last_name = factory.Faker("last_name") - - class Meta: - model = models.AbstractHuman - abstract = True - - -class AuthorFactory(AbstractHumanFactory): - class Meta: - model = models.Author - - -class EditorFactory(AbstractHumanFactory): - class Meta: - model = models.Editor - - -class AbstractEntityFactory(DjangoModelFactory): - class Meta: - model = models.AbstractEntity - abstract = True - - -class JournalFactory(AbstractEntityFactory): - - name = factory.Iterator(JOURNAL_CHOICES, getter=lambda c: c[1]) - abbreviation = factory.Iterator(JOURNAL_CHOICES, getter=lambda c: c[0]) - - class Meta: - model = models.Journal - django_get_or_create = ("abbreviation",) - - -class PublisherFactory(AbstractEntityFactory): - class Meta: - model = models.Publisher - - -class EntryFactory(DjangoModelFactory): - - type = factory.fuzzy.FuzzyChoice(ENTRY_TYPES_RAW_CHOICES) - title = factory.Sequence(lambda n: "Entry title %s" % n) - journal = factory.SubFactory(JournalFactory) - publication_date = factory.fuzzy.FuzzyDate(datetime.date(1942, 1, 1)) - volume = factory.fuzzy.FuzzyInteger(1, 10) - number = factory.fuzzy.FuzzyInteger(1, 50) - pages = FuzzyPages(1, 2000) - - class Meta: - model = models.Entry - - -class CollectionFactory(DjangoModelFactory): - - name = factory.Sequence(lambda n: "Collection name %s" % n) - short_description = factory.fuzzy.FuzzyText(length=42) - - class Meta: - model = models.Collection - - # m2m - @factory.post_generation - def entries(self, create, extracted, **kwargs): - if not create: - return - - if extracted: - for entry in extracted: - self.entries.add(entry) - - -class AuthorEntryRankFactory(DjangoModelFactory): - - author = factory.SubFactory(AuthorFactory) - entry = factory.SubFactory(EntryFactory) - rank = factory.Iterator(range(1, 4), cycle=True) - - class Meta: - model = models.AuthorEntryRank - - -class EntryWithAuthorsFactory(EntryFactory): - - author1 = factory.RelatedFactory(AuthorEntryRankFactory, "entry") - author2 = factory.RelatedFactory(AuthorEntryRankFactory, "entry") - author3 = factory.RelatedFactory(AuthorEntryRankFactory, "entry") - - -class EntryWithStaticAuthorsFactory(EntryFactory): - """Fix two authors first and last names""" - - author1 = factory.RelatedFactory( - AuthorEntryRankFactory, - "entry", - author__first_name="John", - author__last_name="McClane", - rank=1, - ) - - author2 = factory.RelatedFactory( - AuthorEntryRankFactory, - "entry", - author__first_name="Holly", - author__last_name="Gennero", - rank=2, - ) diff --git a/td_biblio/factories/__init__.py b/td_biblio/factories/__init__.py new file mode 100644 index 0000000..8febf9f --- /dev/null +++ b/td_biblio/factories/__init__.py @@ -0,0 +1,17 @@ +# coding: utf-8 +__author__ = 'Alison Mukoma ' +__license__ = 'MIT' +__copyright__ = 'tailordev.fr' + +from .abstract_entity import * # noqa +from .abstract_human import * # noqa +from .author import * # noqa +from .author_entry_rank import * # noqa +from .collection import * # noqa +from .editor import * # noqa +from .entry import * # noqa +from .entry_with_author import * # noqa +from .entry_with_static_author import * # noqa +from .fuzzy_pages_attribute import * # noqa +from .journal import * # noqa +from .publisher import * # noqa diff --git a/td_biblio/factories/abstract_entity.py b/td_biblio/factories/abstract_entity.py new file mode 100644 index 0000000..4cca37f --- /dev/null +++ b/td_biblio/factories/abstract_entity.py @@ -0,0 +1,8 @@ +from factory.django import DjangoModelFactory +from td_biblio.models import AbstractEntity + + +class AbstractEntityFactory(DjangoModelFactory): + class Meta: + model = AbstractEntity + abstract = True diff --git a/td_biblio/factories/abstract_human.py b/td_biblio/factories/abstract_human.py new file mode 100644 index 0000000..3e12e77 --- /dev/null +++ b/td_biblio/factories/abstract_human.py @@ -0,0 +1,14 @@ +import factory + +from factory.django import DjangoModelFactory +from td_biblio.models import AbstractHuman + + +class AbstractHumanFactory(DjangoModelFactory): + + first_name = factory.Faker("first_name") + last_name = factory.Faker("last_name") + + class Meta: + model = AbstractHuman + abstract = True diff --git a/td_biblio/factories/author.py b/td_biblio/factories/author.py new file mode 100644 index 0000000..cd2b1cf --- /dev/null +++ b/td_biblio/factories/author.py @@ -0,0 +1,7 @@ +from td_biblio.factories.abstract_human import AbstractHumanFactory +from td_biblio.models import Author + + +class AuthorFactory(AbstractHumanFactory): + class Meta: + model = Author diff --git a/td_biblio/factories/author_entry_rank.py b/td_biblio/factories/author_entry_rank.py new file mode 100644 index 0000000..c4cf6ed --- /dev/null +++ b/td_biblio/factories/author_entry_rank.py @@ -0,0 +1,17 @@ +import factory + +from factory.django import DjangoModelFactory + +from td_biblio.factories.author import AuthorFactory +from td_biblio.factories.entry import EntryFactory +from td_biblio.models import AuthorEntryRank + + +class AuthorEntryRankFactory(DjangoModelFactory): + + author = factory.SubFactory(AuthorFactory) + entry = factory.SubFactory(EntryFactory) + rank = factory.Iterator(range(1, 4), cycle=True) + + class Meta: + model = AuthorEntryRank diff --git a/td_biblio/factories/collection.py b/td_biblio/factories/collection.py new file mode 100644 index 0000000..04fce94 --- /dev/null +++ b/td_biblio/factories/collection.py @@ -0,0 +1,23 @@ +import factory +from factory.django import DjangoModelFactory + +from td_biblio.models import Collection + + +class CollectionFactory(DjangoModelFactory): + + name = factory.Sequence(lambda n: "Collection name %s" % n) + short_description = factory.fuzzy.FuzzyText(length=42) + + class Meta: + model = Collection + + # m2m + @factory.post_generation + def entries(self, create, extracted, **kwargs): + if not create: + return + + if extracted: + for entry in extracted: + self.entries.add(entry) diff --git a/td_biblio/factories/editor.py b/td_biblio/factories/editor.py new file mode 100644 index 0000000..dd892b8 --- /dev/null +++ b/td_biblio/factories/editor.py @@ -0,0 +1,7 @@ +from td_biblio.factories.abstract_human import AbstractHumanFactory +from td_biblio.models import Editor + + +class EditorFactory(AbstractHumanFactory): + class Meta: + model = Editor diff --git a/td_biblio/factories/entry.py b/td_biblio/factories/entry.py new file mode 100644 index 0000000..655782f --- /dev/null +++ b/td_biblio/factories/entry.py @@ -0,0 +1,25 @@ +import datetime +import factory + +from factory.django import DjangoModelFactory + +from td_biblio.factories.fuzzy_pages_attribute import FuzzyPages +from td_biblio.factories.journal import JournalFactory +from td_biblio.models import Entry + + +ENTRY_TYPES_RAW_CHOICES = [c[0] for c in Entry.ENTRY_TYPES_CHOICES] + + +class EntryFactory(DjangoModelFactory): + + type = factory.fuzzy.FuzzyChoice(ENTRY_TYPES_RAW_CHOICES) + title = factory.Sequence(lambda n: "Entry title %s" % n) + journal = factory.SubFactory(JournalFactory) + publication_date = factory.fuzzy.FuzzyDate(datetime.date(1942, 1, 1)) + volume = factory.fuzzy.FuzzyInteger(1, 10) + number = factory.fuzzy.FuzzyInteger(1, 50) + pages = FuzzyPages(1, 2000) + + class Meta: + model = Entry diff --git a/td_biblio/factories/entry_with_author.py b/td_biblio/factories/entry_with_author.py new file mode 100644 index 0000000..778364d --- /dev/null +++ b/td_biblio/factories/entry_with_author.py @@ -0,0 +1,11 @@ +import factory + +from td_biblio.factories.author_entry_rank import AuthorEntryRankFactory +from td_biblio.factories.entry import EntryFactory + + +class EntryWithAuthorsFactory(EntryFactory): + + author1 = factory.RelatedFactory(AuthorEntryRankFactory, "entry") + author2 = factory.RelatedFactory(AuthorEntryRankFactory, "entry") + author3 = factory.RelatedFactory(AuthorEntryRankFactory, "entry") diff --git a/td_biblio/factories/entry_with_static_author.py b/td_biblio/factories/entry_with_static_author.py new file mode 100644 index 0000000..28252f4 --- /dev/null +++ b/td_biblio/factories/entry_with_static_author.py @@ -0,0 +1,24 @@ +import factory + +from td_biblio.factories.author_entry_rank import AuthorEntryRankFactory +from td_biblio.factories.entry import EntryFactory + + +class EntryWithStaticAuthorsFactory(EntryFactory): + """Fix two authors first and last names""" + + author1 = factory.RelatedFactory( + AuthorEntryRankFactory, + "entry", + author__first_name="John", + author__last_name="McClane", + rank=1, + ) + + author2 = factory.RelatedFactory( + AuthorEntryRankFactory, + "entry", + author__first_name="Holly", + author__last_name="Gennero", + rank=2, + ) diff --git a/td_biblio/factories/fuzzy_pages_attribute.py b/td_biblio/factories/fuzzy_pages_attribute.py new file mode 100644 index 0000000..b28e69a --- /dev/null +++ b/td_biblio/factories/fuzzy_pages_attribute.py @@ -0,0 +1,24 @@ +import random + +from factory.fuzzy import BaseFuzzyAttribute + +# Custom fuzzy attributes definition + + +class FuzzyPages(BaseFuzzyAttribute): + """Random pages numbers separated by double-hyphens""" + + def __init__(self, low, high=None, **kwargs): + if high is None: + high = low + low = 1 + + self.low = low + self.high = high + + super(FuzzyPages, self).__init__(**kwargs) + + def fuzz(self): + start = random.randint(self.low, self.high) + end = random.randint(start, self.high) + return "%d--%d" % (start, end) diff --git a/td_biblio/factories/journal.py b/td_biblio/factories/journal.py new file mode 100644 index 0000000..9aeaefc --- /dev/null +++ b/td_biblio/factories/journal.py @@ -0,0 +1,33 @@ +import factory + +from td_biblio.factories.abstract_entity import AbstractEntityFactory +from td_biblio.models import Journal + + +JOURNAL_CHOICES = [ + ("Bioinformatics", "Bioinformatics"), + ("BMC Bioinf.", "BMC Bioinformatics"), + ("JACS", "Journal of the American Chemical Society"), + ("J. Comput. Chem.", "Journal of Computational Chemistry"), + ("Nat. Biotechnol.", "Nature Biotechnology"), + ("Nucleic Acids Res.", "Nucleic Acids Research"), + ( + "PNAS", + "Proceedings of the National Academy of Sciences of the United " + "States of America", + ), + ( + "Proteins Struct. Funct. Bioinf.", + "Proteins: Structure, Function, and Bioinformatics", + ), +] + + +class JournalFactory(AbstractEntityFactory): + + name = factory.Iterator(JOURNAL_CHOICES, getter=lambda c: c[1]) + abbreviation = factory.Iterator(JOURNAL_CHOICES, getter=lambda c: c[0]) + + class Meta: + model = Journal + django_get_or_create = ("abbreviation",) diff --git a/td_biblio/factories/publisher.py b/td_biblio/factories/publisher.py new file mode 100644 index 0000000..bb86b4d --- /dev/null +++ b/td_biblio/factories/publisher.py @@ -0,0 +1,7 @@ +from td_biblio.factories.abstract_entity import AbstractEntityFactory +from td_biblio.models import Publisher + + +class PublisherFactory(AbstractEntityFactory): + class Meta: + model = Publisher diff --git a/td_biblio/forms.py b/td_biblio/forms.py deleted file mode 100644 index e5a2915..0000000 --- a/td_biblio/forms.py +++ /dev/null @@ -1,118 +0,0 @@ -from operator import methodcaller - -from django import forms -from django.core.validators import RegexValidator -from django.utils.translation import ugettext_lazy as _ - -from td_biblio.models import Author - -DOI_REGEX = r"(10[.][0-9]{4,}(?:[.][0-9]+)*/(?:(?![\"&'<>])\S)+)" -doi_validator = RegexValidator( - DOI_REGEX, _("One (or more) DOI is not valid"), "invalid" -) - -PMID_REGEX = r"^-?\d+\Z" -pmid_validator = RegexValidator( - PMID_REGEX, _("One (or more) PMID is not valid"), "invalid" -) - - -def text_to_list(raw): - """Transform a raw text list to a python sorted object list - Supported separators: coma, space and carriage return - """ - return sorted( - list( - set( - id.strip() - for r in map(methodcaller("split", ","), raw.split()) - for id in r - if len(id) - ) - ) - ) - - -class EntryBatchImportForm(forms.Form): - - pmids = forms.CharField( - label=_("PMID"), - widget=forms.Textarea(attrs={"placeholder": "ex: 26588162, 19569182"}), - help_text=_( - "Paste a list of PubMed Identifiers " "(comma separated or one per line)" - ), - required=False, - ) - - dois = forms.CharField( - label=_("DOI"), - widget=forms.Textarea( - attrs={"placeholder": "ex: 10.1093/nar/gks419, 10.1093/nar/gkp323"} - ), - help_text=_( - "Paste a list of Digital Object Identifiers " - "(comma separated or one per line)" - ), - required=False, - ) - - def clean_pmids(self): - """Transform raw data in a PMID list""" - pmids = text_to_list(self.cleaned_data["pmids"]) - for pmid in pmids: - pmid_validator(pmid) - return pmids - - def clean_dois(self): - """Transform raw data in a DOI list""" - dois = text_to_list(self.cleaned_data["dois"]) - for doi in dois: - doi_validator(doi) - return dois - - def clean(self): - super(EntryBatchImportForm, self).clean() - - dois = self.cleaned_data.get("dois", []) - pmids = self.cleaned_data.get("pmids", []) - - if not len(dois) and not len(pmids): - raise forms.ValidationError( - _("You need to submit at least one valid DOI or PMID") - ) - - -class AuthorDuplicatesForm(forms.Form): - def get_authors_choices(): - return Author.objects.values_list("id", "last_name") - - authors = forms.MultipleChoiceField( - label=_("Authors pool"), - help_text=_("Authors to merge"), - choices=get_authors_choices, - ) - - alias = forms.ChoiceField( - label=_("Target author"), - help_text=_("Reference author for which we will define aliases"), - choices=get_authors_choices, - ) - - def clean_authors(self): - authors = self.cleaned_data["authors"] - return Author.objects.filter(id__in=authors) - - def clean_alias(self): - alias = self.cleaned_data["alias"] - return Author.objects.get(id=alias) - - def clean(self): - super(AuthorDuplicatesForm, self).clean() - - authors = self.cleaned_data.get("authors", []) - alias = self.cleaned_data.get("alias", None) - - if alias in authors: - raise forms.ValidationError( - _("Target author cannot be part of the selection") - ) diff --git a/td_biblio/forms/__init__.py b/td_biblio/forms/__init__.py new file mode 100644 index 0000000..a58e7cf --- /dev/null +++ b/td_biblio/forms/__init__.py @@ -0,0 +1,7 @@ +# coding: utf-8 +__author__ = 'Alison Mukoma ' +__license__ = 'MIT' +__copyright__ = 'tailordev.fr' + +from .author_duplicates import * # noqa +from .entry_batch_import import * # noqa diff --git a/td_biblio/forms/author_duplicates.py b/td_biblio/forms/author_duplicates.py new file mode 100644 index 0000000..734e7a5 --- /dev/null +++ b/td_biblio/forms/author_duplicates.py @@ -0,0 +1,39 @@ +from django import forms +from django.utils.translation import ugettext_lazy as _ +from td_biblio.models import Author + + +class AuthorDuplicatesForm(forms.Form): + def get_authors_choices(): + return Author.objects.values_list("id", "last_name") + + authors = forms.MultipleChoiceField( + label=_("Authors pool"), + help_text=_("Authors to merge"), + choices=get_authors_choices, + ) + + alias = forms.ChoiceField( + label=_("Target author"), + help_text=_("Reference author for which we will define aliases"), + choices=get_authors_choices, + ) + + def clean_authors(self): + authors = self.cleaned_data["authors"] + return Author.objects.filter(id__in=authors) + + def clean_alias(self): + alias = self.cleaned_data["alias"] + return Author.objects.get(id=alias) + + def clean(self): + super(AuthorDuplicatesForm, self).clean() + + authors = self.cleaned_data.get("authors", []) + alias = self.cleaned_data.get("alias", None) + + if alias in authors: + raise forms.ValidationError( + _("Target author cannot be part of the selection") + ) diff --git a/td_biblio/forms/entry_batch_import.py b/td_biblio/forms/entry_batch_import.py new file mode 100644 index 0000000..a72eb9a --- /dev/null +++ b/td_biblio/forms/entry_batch_import.py @@ -0,0 +1,55 @@ +from django import forms +from django.utils.translation import ugettext_lazy as _ +from td_biblio.utils.doi_validator import doi_validator +from td_biblio.utils.pmid_validator import pmid_validator +from td_biblio.utils.text_to_list import text_to_list + + +class EntryBatchImportForm(forms.Form): + + pmids = forms.CharField( + label=_("PMID"), + widget=forms.Textarea(attrs={"placeholder": "ex: 26588162, 19569182"}), + help_text=_( + "Paste a list of PubMed Identifiers " "(comma separated or one " + "per line)" + ), + required=False, + ) + + dois = forms.CharField( + label=_("DOI"), + widget=forms.Textarea( + attrs={"placeholder": "ex: 10.1093/nar/gks419, 10.1093/nar/gkp323"} + ), + help_text=_( + "Paste a list of Digital Object Identifiers " + "(comma separated or one per line)" + ), + required=False, + ) + + def clean_pmids(self): + """Transform raw data in a PMID list""" + pmids = text_to_list(self.cleaned_data["pmids"]) + for pmid in pmids: + pmid_validator(pmid) + return pmids + + def clean_dois(self): + """Transform raw data in a DOI list""" + dois = text_to_list(self.cleaned_data["dois"]) + for doi in dois: + doi_validator(doi) + return dois + + def clean(self): + super(EntryBatchImportForm, self).clean() + + dois = self.cleaned_data.get("dois", []) + pmids = self.cleaned_data.get("pmids", []) + + if not len(dois) and not len(pmids): + raise forms.ValidationError( + _("You need to submit at least one valid DOI or PMID") + ) diff --git a/td_biblio/management/commands/bibtex_import.py b/td_biblio/management/commands/bibtex_import.py index d73964a..f71918b 100644 --- a/td_biblio/management/commands/bibtex_import.py +++ b/td_biblio/management/commands/bibtex_import.py @@ -14,7 +14,8 @@ def _get_logger(self): return logger def add_arguments(self, parser): - parser.add_argument("bibtex", help="The path to the BibTeX file to import") + parser.add_argument("bibtex", help="The path to the BibTeX file to " + "import") def handle(self, *args, **options): logger = self._get_logger() diff --git a/td_biblio/migrations/0001_initial.py b/td_biblio/migrations/0001_initial.py index 658f65a..18684a1 100644 --- a/td_biblio/migrations/0001_initial.py +++ b/td_biblio/migrations/0001_initial.py @@ -28,7 +28,8 @@ class Migration(migrations.Migration): ), ( "first_name", - models.CharField(max_length=100, verbose_name="First name"), + models.CharField(max_length=100, verbose_name="First " + "name"), ), ( "last_name", @@ -37,7 +38,8 @@ class Migration(migrations.Migration): ( "first_initial", models.CharField( - blank=True, max_length=10, verbose_name="First Initial(s)" + blank=True, max_length=10, verbose_name="First " + "Initial(s)" ), ), ( @@ -101,7 +103,8 @@ class Migration(migrations.Migration): verbose_name="ID", ), ), - ("name", models.CharField(max_length=100, verbose_name="Name")), + ("name", models.CharField(max_length=100, + verbose_name="Name")), ( "short_description", models.TextField( @@ -128,7 +131,8 @@ class Migration(migrations.Migration): ), ( "first_name", - models.CharField(max_length=100, verbose_name="First name"), + models.CharField(max_length=100, verbose_name="First " + "name"), ), ( "last_name", @@ -137,7 +141,8 @@ class Migration(migrations.Migration): ( "first_initial", models.CharField( - blank=True, max_length=10, verbose_name="First Initial(s)" + blank=True, max_length=10, verbose_name="First " + "Initial(s)" ), ), ( @@ -178,7 +183,8 @@ class Migration(migrations.Migration): ("conference", "Conference"), ("inbook", "Book chapter"), ("incollection", "Book from a collection"), - ("inproceedings", "Conference proceedings article"), + ("inproceedings", "Conference proceedings " + "article"), ("manual", "Technical documentation"), ("mastersthesis", "Master's Thesis"), ("misc", "Miscellaneous"), @@ -192,16 +198,20 @@ class Migration(migrations.Migration): verbose_name="Entry type", ), ), - ("title", models.CharField(max_length=255, verbose_name="Title")), + ("title", models.CharField(max_length=255, + verbose_name="Title")), ( "publication_date", - models.DateField(null=True, verbose_name="Publication date"), + models.DateField(null=True, verbose_name="Publication " + "date"), ), ( "is_partial_publication_date", models.BooleanField( default=True, - help_text="Check this if the publication date is incomplete (for example if only the year is valid)", + help_text="Check this if the publication date " + "isincomplete (for example if only the " + "year is valid)", verbose_name="Partial publication date?", ), ), @@ -209,7 +219,8 @@ class Migration(migrations.Migration): "volume", models.CharField( blank=True, - help_text="The volume of a journal or multi-volume book", + help_text="The volume of a journal or multi-volume " + "book", max_length=50, verbose_name="Volume", ), @@ -218,7 +229,10 @@ class Migration(migrations.Migration): "number", models.CharField( blank=True, - help_text="The '(issue) number' of a journal, magazine, or tech-report, if applicable. (Most publications have a 'volume', but no 'number' field.)", + help_text="The '(issue) number' of a journal, " + "magazine, or tech-report, if applicable. " + "(Most publications have a 'volume', " + "but no 'number' field.)", max_length=50, verbose_name="Number", ), @@ -227,7 +241,8 @@ class Migration(migrations.Migration): "pages", models.CharField( blank=True, - help_text="Page numbers, separated either by commas or double-hyphens", + help_text="Page numbers, separated either by commas " + "or double-hyphens", max_length=50, verbose_name="Pages", ), @@ -236,7 +251,8 @@ class Migration(migrations.Migration): "url", models.URLField( blank=True, - help_text="The WWW address where to find this resource", + help_text="The WWW address where to find this " + "resource", verbose_name="URL", ), ), @@ -244,7 +260,8 @@ class Migration(migrations.Migration): "doi", models.CharField( blank=True, - help_text="Digital Object Identifier for this resource", + help_text="Digital Object Identifier for this " + "resource", max_length=100, verbose_name="DOI", ), @@ -280,7 +297,8 @@ class Migration(migrations.Migration): "booktitle", models.CharField( blank=True, - help_text="The title of the book, if only part of it is being cited", + help_text="The title of the book, if only part of it " + "is being cited", max_length=50, verbose_name="Book title", ), @@ -289,7 +307,8 @@ class Migration(migrations.Migration): "edition", models.CharField( blank=True, - help_text="The edition of a book, long form (such as 'First' or 'Second')", + help_text="The edition of a book, long form " + "(such as 'First' or 'Second')", max_length=100, verbose_name="Edition", ), @@ -297,7 +316,8 @@ class Migration(migrations.Migration): ( "chapter", models.CharField( - blank=True, max_length=50, verbose_name="Chapter number" + blank=True, max_length=50, verbose_name="Chapter " + "number" ), ), ( @@ -322,7 +342,9 @@ class Migration(migrations.Migration): "address", models.CharField( blank=True, - help_text="Publisher's address (usually just the city, but can be the full address for lesser-known publishers)", + help_text="Publisher's address (usually just the " + "city, but can be the full address for " + "lesser-known publishers)", max_length=250, verbose_name="Address", ), @@ -331,7 +353,8 @@ class Migration(migrations.Migration): "annote", models.CharField( blank=True, - help_text="An annotation for annotated bibliography styles (not typical)", + help_text="An annotation for annotated bibliography " + "styles (not typical)", max_length=250, verbose_name="Annote", ), @@ -363,7 +386,8 @@ class Migration(migrations.Migration): ( "editors", models.ManyToManyField( - blank=True, related_name="entries", to="td_biblio.Editor" + blank=True, related_name="entries", + to="td_biblio.Editor" ), ), ], @@ -385,7 +409,8 @@ class Migration(migrations.Migration): verbose_name="ID", ), ), - ("name", models.CharField(max_length=150, verbose_name="Name")), + ("name", models.CharField(max_length=150, + verbose_name="Name")), ( "abbreviation", models.CharField( @@ -396,7 +421,8 @@ class Migration(migrations.Migration): ), ), ], - options={"verbose_name": "Journal", "verbose_name_plural": "Journals"}, + options={"verbose_name": "Journal", + "verbose_name_plural": "Journals"}, ), migrations.CreateModel( name="Publisher", @@ -410,7 +436,8 @@ class Migration(migrations.Migration): verbose_name="ID", ), ), - ("name", models.CharField(max_length=150, verbose_name="Name")), + ("name", models.CharField(max_length=150, + verbose_name="Name")), ( "abbreviation", models.CharField( @@ -421,7 +448,8 @@ class Migration(migrations.Migration): ), ), ], - options={"verbose_name": "Publisher", "verbose_name_plural": "Publishers"}, + options={"verbose_name": "Publisher", + "verbose_name_plural": "Publishers"}, ), migrations.AddField( model_name="entry", @@ -454,7 +482,8 @@ class Migration(migrations.Migration): model_name="authorentryrank", name="entry", field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, to="td_biblio.Entry" + on_delete=django.db.models.deletion.CASCADE, + to="td_biblio.Entry" ), ), ] diff --git a/td_biblio/models/__init__.py b/td_biblio/models/__init__.py new file mode 100644 index 0000000..e84f2b0 --- /dev/null +++ b/td_biblio/models/__init__.py @@ -0,0 +1,14 @@ +# coding: utf-8 +__author__ = 'Alison Mukoma ' +__license__ = 'MIT' +__copyright__ = 'tailordev.fr' + +from .abstract_entity import * # noqa +from .abstract_human import * # noqa +from .author import * # noqa +from .author_entry_rank import * # noqa +from .entry import * # noqa +from .editor import * # noqa +from .collection import * # noqa +from .journal import * # noqa +from .publisher import * # noqa diff --git a/td_biblio/models/abstract_entity.py b/td_biblio/models/abstract_entity.py new file mode 100644 index 0000000..bb0c7b7 --- /dev/null +++ b/td_biblio/models/abstract_entity.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +from django.db import models +from django.utils.translation import ugettext_lazy as _ + + +class AbstractEntity(models.Model): + """Simple abstract entity""" + + name = models.CharField(_("Name"), max_length=150) + abbreviation = models.CharField( + _("Entity abbreviation"), + max_length=100, + blank=True, + help_text=_("e.g. Proc Natl Acad Sci U S A"), + ) + + class Meta: + abstract = True + + def __str__(self): + return self.name diff --git a/td_biblio/models/abstract_human.py b/td_biblio/models/abstract_human.py new file mode 100644 index 0000000..58f8f7f --- /dev/null +++ b/td_biblio/models/abstract_human.py @@ -0,0 +1,77 @@ +# -*- coding: utf-8 -*- +from django.db import models +from django.conf import settings +from django.contrib.auth import get_user_model +from django.utils.translation import ugettext_lazy as _ + + +class AbstractHuman(models.Model): + """Simple Abstract Human model + + Note that this model may be linked to django registered users + """ + + first_name = models.CharField(_("First name"), max_length=100) + last_name = models.CharField(_("Last name"), max_length=100) + first_initial = models.CharField(_("First Initial(s)"), max_length=10, + blank=True) + + # This is a django user + user = models.ForeignKey( + settings.AUTH_USER_MODEL, blank=True, null=True, + on_delete=models.CASCADE + ) + + alias = models.ForeignKey( + "self", + on_delete=models.CASCADE, + related_name="aliases", + related_query_name="alias_human", + blank=True, + null=True, + ) + + class Meta: + abstract = True + + def __str__(self): + return self.get_formatted_name() + + def save(self, *args, **kwargs): + """Set initials and try to set django user before saving""" + + self._set_first_initial() + self._set_user() + super(AbstractHuman, self).save(*args, **kwargs) + + def _set_first_initial(self, force=False): + """Set author first name initial""" + + if self.first_initial and not force: + return + self.first_initial = " ".join([c[0] for c in self.first_name.split()]) + + def get_formatted_name(self): + """Return author formated full name, e.g. Maupetit J""" + + return "%s %s" % (self.last_name, self.first_initial) + + def _set_user(self): + """Look for local django user based on human name""" + + if "" in (self.last_name, self.first_name): + return + + self._set_first_initial() + + User = get_user_model() + try: + self.user = User.objects.get( + models.Q(last_name__iexact=self.last_name), + models.Q(first_name__iexact=self.first_name) | models.Q( + first_name__istartswith=self.first_initial[0]), + ) + except User.DoesNotExist: + pass + except User.MultipleObjectsReturned: + pass diff --git a/td_biblio/models/author.py b/td_biblio/models/author.py new file mode 100644 index 0000000..3c836de --- /dev/null +++ b/td_biblio/models/author.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +from django.utils.translation import ugettext_lazy as _ +from td_biblio.models.abstract_human import AbstractHuman + + +class Author(AbstractHuman): + """Entry author""" + + class Meta: + ordering = ("last_name", "first_name") + verbose_name = _("Author") + verbose_name_plural = _("Authors") diff --git a/td_biblio/models/author_entry_rank.py b/td_biblio/models/author_entry_rank.py new file mode 100644 index 0000000..8927a0d --- /dev/null +++ b/td_biblio/models/author_entry_rank.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +from django.db import models +from django.utils.translation import ugettext_lazy as _ +from td_biblio.models.author import Author +from td_biblio.models.entry import Entry + + +class AuthorEntryRank(models.Model): + """Give the author rank for an entry author sequence""" + + author = models.ForeignKey(Author, on_delete=models.CASCADE) + entry = models.ForeignKey(Entry, on_delete=models.CASCADE) + rank = models.IntegerField( + _("Rank"), help_text=_("Author rank in entry authors sequence") + ) + + class Meta: + verbose_name = _("Author Entry Rank") + verbose_name_plural = _("Author Entry Ranks") + ordering = ("rank",) + + def __str__(self): + return "%(author)s:%(rank)d:%(entry)s" % { + "author": self.author, + "entry": self.entry, + "rank": self.rank, + } diff --git a/td_biblio/models/collection.py b/td_biblio/models/collection.py new file mode 100644 index 0000000..36effb6 --- /dev/null +++ b/td_biblio/models/collection.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from django.db import models +from django.utils.translation import ugettext_lazy as _ + + +class Collection(models.Model): + """Define a collection of entries""" + + name = models.CharField(_("Name"), max_length=100) + short_description = models.TextField(_("Short description"), blank=True, + null=True) + entries = models.ManyToManyField("Entry", related_name="collections") + + class Meta: + verbose_name = _("Collection") + verbose_name_plural = _("Collections") + + def __str__(self): + return self.name diff --git a/td_biblio/models/editor.py b/td_biblio/models/editor.py new file mode 100644 index 0000000..11c6690 --- /dev/null +++ b/td_biblio/models/editor.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +from django.utils.translation import ugettext_lazy as _ +from td_biblio.models.abstract_human import AbstractHuman + + +class Editor(AbstractHuman): + """Journal or book editor""" + + class Meta: + ordering = ("last_name", "first_name") + verbose_name = _("Editor") + verbose_name_plural = _("Editors") diff --git a/td_biblio/models.py b/td_biblio/models/entry.py similarity index 63% rename from td_biblio/models.py rename to td_biblio/models/entry.py index 7ad6e66..5329c1f 100644 --- a/td_biblio/models.py +++ b/td_biblio/models/entry.py @@ -1,132 +1,8 @@ # -*- coding: utf-8 -*- -from django.conf import settings -from django.contrib.auth import get_user_model from django.db import models from django.utils.translation import ugettext_lazy as _ -class AbstractHuman(models.Model): - """Simple Abstract Human model - - Note that this model may be linked to django registered users - """ - - first_name = models.CharField(_("First name"), max_length=100) - last_name = models.CharField(_("Last name"), max_length=100) - first_initial = models.CharField(_("First Initial(s)"), max_length=10, blank=True) - - # This is a django user - user = models.ForeignKey( - settings.AUTH_USER_MODEL, blank=True, null=True, on_delete=models.CASCADE - ) - - alias = models.ForeignKey( - "self", - on_delete=models.CASCADE, - related_name="aliases", - related_query_name="alias_human", - blank=True, - null=True, - ) - - class Meta: - abstract = True - - def __str__(self): - return self.get_formatted_name() - - def save(self, *args, **kwargs): - """Set initials and try to set django user before saving""" - - self._set_first_initial() - self._set_user() - super(AbstractHuman, self).save(*args, **kwargs) - - def _set_first_initial(self, force=False): - """Set author first name initial""" - - if self.first_initial and not force: - return - self.first_initial = " ".join([c[0] for c in self.first_name.split()]) - - def get_formatted_name(self): - """Return author formated full name, e.g. Maupetit J""" - - return "%s %s" % (self.last_name, self.first_initial) - - def _set_user(self): - """Look for local django user based on human name""" - - if "" in (self.last_name, self.first_name): - return - - self._set_first_initial() - - User = get_user_model() - try: - self.user = User.objects.get( - models.Q(last_name__iexact=self.last_name), - models.Q(first_name__iexact=self.first_name) - | models.Q(first_name__istartswith=self.first_initial[0]), - ) - except User.DoesNotExist: - pass - except User.MultipleObjectsReturned: - pass - - -class Author(AbstractHuman): - """Entry author""" - - class Meta: - ordering = ("last_name", "first_name") - verbose_name = _("Author") - verbose_name_plural = _("Authors") - - -class Editor(AbstractHuman): - """Journal or book editor""" - - class Meta: - ordering = ("last_name", "first_name") - verbose_name = _("Editor") - verbose_name_plural = _("Editors") - - -class AbstractEntity(models.Model): - """Simple abstract entity""" - - name = models.CharField(_("Name"), max_length=150) - abbreviation = models.CharField( - _("Entity abbreviation"), - max_length=100, - blank=True, - help_text=_("e.g. Proc Natl Acad Sci U S A"), - ) - - class Meta: - abstract = True - - def __str__(self): - return self.name - - -class Journal(AbstractEntity): - """Peer reviewed journal""" - - class Meta: - verbose_name = _("Journal") - verbose_name_plural = _("Journals") - - -class Publisher(AbstractEntity): - """Journal or book publisher""" - - class Meta: - verbose_name = _("Publisher") - verbose_name_plural = _("Publishers") - - class Entry(models.Model): """The core model for references @@ -185,7 +61,8 @@ class Entry(models.Model): ) type = models.CharField( - _("Entry type"), max_length=50, choices=ENTRY_TYPES_CHOICES, default=ARTICLE + _("Entry type"), max_length=50, choices=ENTRY_TYPES_CHOICES, + default=ARTICLE ) # Base fields @@ -225,10 +102,12 @@ class Entry(models.Model): _("Pages"), max_length=50, blank=True, - help_text=_("Page numbers, separated either by commas or " "double-hyphens"), + help_text=_("Page numbers, separated either by commas " + "or " "double-hyphens"), ) url = models.URLField( - _("URL"), blank=True, help_text=_("The WWW address where to find this resource") + _("URL"), blank=True, help_text=_("The WWW address where to find " + "this resource") ) # Identifiers @@ -259,7 +138,8 @@ class Entry(models.Model): _("Book title"), max_length=50, blank=True, - help_text=_("The title of the book, if only part of it is being cited"), + help_text=_("The title of the book, if only part of it is being " + "cited"), ) edition = models.CharField( _("Edition"), @@ -288,7 +168,8 @@ class Entry(models.Model): ) # Misc - editors = models.ManyToManyField("Editor", related_name="entries", blank=True) + editors = models.ManyToManyField("Editor", related_name="entries", + blank=True) publisher = models.ForeignKey( "Publisher", related_name="entries", @@ -309,7 +190,8 @@ class Entry(models.Model): _("Annote"), max_length=250, blank=True, - help_text=_("An annotation for annotated bibliography styles (not typical)"), + help_text=_("An annotation for annotated bibliography styles (not " + "typical)"), ) note = models.TextField( _("Note"), blank=True, help_text=_("Miscellaneous extra information") @@ -377,40 +259,3 @@ def get_authors(self): queryset is not (M2M with a through case). """ return [aer.author for aer in self.authorentryrank_set.all()] - - -class Collection(models.Model): - """Define a collection of entries""" - - name = models.CharField(_("Name"), max_length=100) - short_description = models.TextField(_("Short description"), blank=True, null=True) - entries = models.ManyToManyField("Entry", related_name="collections") - - class Meta: - verbose_name = _("Collection") - verbose_name_plural = _("Collections") - - def __str__(self): - return self.name - - -class AuthorEntryRank(models.Model): - """Give the author rank for an entry author sequence""" - - author = models.ForeignKey(Author, on_delete=models.CASCADE) - entry = models.ForeignKey(Entry, on_delete=models.CASCADE) - rank = models.IntegerField( - _("Rank"), help_text=_("Author rank in entry authors sequence") - ) - - class Meta: - verbose_name = _("Author Entry Rank") - verbose_name_plural = _("Author Entry Ranks") - ordering = ("rank",) - - def __str__(self): - return "%(author)s:%(rank)d:%(entry)s" % { - "author": self.author, - "entry": self.entry, - "rank": self.rank, - } diff --git a/td_biblio/models/journal.py b/td_biblio/models/journal.py new file mode 100644 index 0000000..367aa98 --- /dev/null +++ b/td_biblio/models/journal.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +from django.utils.translation import ugettext_lazy as _ +from td_biblio.models.abstract_entity import AbstractEntity + + +class Journal(AbstractEntity): + """Peer reviewed journal""" + + class Meta: + verbose_name = _("Journal") + verbose_name_plural = _("Journals") diff --git a/td_biblio/models/publisher.py b/td_biblio/models/publisher.py new file mode 100644 index 0000000..4457f4c --- /dev/null +++ b/td_biblio/models/publisher.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +from django.utils.translation import ugettext_lazy as _ +from td_biblio.models.abstract_entity import AbstractEntity + + +class Publisher(AbstractEntity): + """Journal or book publisher""" + + class Meta: + verbose_name = _("Publisher") + verbose_name_plural = _("Publishers") diff --git a/td_biblio/tests/__init__.py b/td_biblio/tests/__init__.py index e69de29..cac2cd5 100644 --- a/td_biblio/tests/__init__.py +++ b/td_biblio/tests/__init__.py @@ -0,0 +1,13 @@ +# coding: utf-8 +__author__ = 'Alison Mukoma ' +__license__ = 'MIT' +__copyright__ = 'tailordev.fr' + +from .conftest import * # noqa +from .test_commands import * # noqa +from .test_factories import * # noqa +from .test_forms import * # noqa +from .test_loaders import * # noqa +from .test_models import * # noqa +from .test_templatetags import * # noqa +from .test_views import * # noqa diff --git a/td_biblio/tests/test_factories.py b/td_biblio/tests/test_factories.py index 61db9e5..6160e5a 100644 --- a/td_biblio/tests/test_factories.py +++ b/td_biblio/tests/test_factories.py @@ -7,7 +7,7 @@ import pytest from django.test import TestCase -from ..factories import FuzzyPages, EntryFactory +from td_biblio.factories import FuzzyPages, EntryFactory @pytest.mark.django_db diff --git a/td_biblio/tests/test_forms.py b/td_biblio/tests/test_forms.py index c45b795..db5846a 100644 --- a/td_biblio/tests/test_forms.py +++ b/td_biblio/tests/test_forms.py @@ -7,7 +7,8 @@ from __future__ import unicode_literals from django.test import TestCase -from ..forms import text_to_list, EntryBatchImportForm +from td_biblio.utils.text_to_list import text_to_list +from td_biblio.forms.entry_batch_import import EntryBatchImportForm def test_text_to_list(): diff --git a/td_biblio/tests/test_loaders.py b/td_biblio/tests/test_loaders.py index 3b2d6fc..78ce5c6 100644 --- a/td_biblio/tests/test_loaders.py +++ b/td_biblio/tests/test_loaders.py @@ -13,9 +13,9 @@ from eutils.exceptions import EutilsNCBIError from requests.exceptions import HTTPError -from ..exceptions import DOILoaderError, PMIDLoaderError -from ..utils.loaders import BibTeXLoader, DOILoader, PubmedLoader -from ..models import Author, Entry, Journal +from td_biblio.exceptions import DOILoaderError, PMIDLoaderError +from td_biblio.utils.loaders import BibTeXLoader, DOILoader, PubmedLoader +from td_biblio.models import Author, Entry, Journal from .fixtures.entries import PMIDs as FPMIDS, DOIs as FDOIS FileNotFoundError = getattr(__builtins__, "FileNotFoundError", IOError) @@ -63,7 +63,8 @@ def test_save_records_from_a_bibtex_file(self): def _test_entry_authors(self, entry, expected_authors): for rank, author in enumerate(entry.get_authors()): - self.assertEqual(author.get_formatted_name(), expected_authors[rank]) + self.assertEqual(author.get_formatted_name(), expected_authors[ + rank]) def test_author_rank(self): """ @@ -90,7 +91,8 @@ def test_author_rank(self): # Case 2 entry = Entry.objects.get(title__startswith="fpocket") - expected_authors = ["Schmidtke P", "Le Guilloux V", "Maupetit J", "Tufféry P"] + expected_authors = ["Schmidtke P", "Le Guilloux V", "Maupetit J", + "Tufféry P"] self._test_entry_authors(entry, expected_authors) def test_partial_publication_date(self): @@ -151,7 +153,8 @@ def test_load_records_given_an_existing_pmid(self): self.assertEqual(record["number"], expected["number"]) self.assertEqual(record["pages"], expected["pages"]) self.assertEqual(record["year"], expected["year"]) - self.assertEqual(record["publication_date"], expected["publication_date"]) + self.assertEqual(record["publication_date"], expected[ + "publication_date"]) self.assertEqual( record["is_partial_publication_date"], expected["is_partial_publication_date"], @@ -262,7 +265,8 @@ def test_load_records_from_an_existing_doi(self): self.assertEqual(record["number"], expected["number"]) self.assertEqual(record["pages"], expected["pages"]) self.assertEqual(record["year"], expected["year"]) - self.assertEqual(record["publication_date"], expected["publication_date"]) + self.assertEqual(record["publication_date"], expected[ + "publication_date"]) self.assertEqual( record["is_partial_publication_date"], expected["is_partial_publication_date"], diff --git a/td_biblio/tests/test_models.py b/td_biblio/tests/test_models.py index 7872b78..fbbce16 100644 --- a/td_biblio/tests/test_models.py +++ b/td_biblio/tests/test_models.py @@ -10,7 +10,7 @@ from django.contrib.auth import get_user_model from django.test import TestCase -from ..factories import ( +from td_biblio.factories import ( AuthorFactory, EditorFactory, JournalFactory, @@ -21,7 +21,7 @@ EntryWithStaticAuthorsFactory, ) -from ..models import ( +from td_biblio.models import ( Author, Editor, Journal, @@ -89,7 +89,8 @@ def test_set_first_initial(self): human = self.factory(first_name="John", last_name="McClane") self.assertEqual(human.first_initial, "J") - human = self.factory(first_name="John Jack Junior", last_name="McClane") + human = self.factory(first_name="John Jack Junior", + last_name="McClane") self.assertEqual(human.first_initial, "J J J") human.first_name = "Jumping Jack Flash" @@ -137,14 +138,16 @@ def test_set_user(self): # We have 2 possible matches user = get_user_model().objects.create( - username="johnjuniormcclane", first_name="John Junior", last_name="McClane" + username="johnjuniormcclane", first_name="John Junior", + last_name="McClane" ) human = self.factory(first_name="J", last_name="McClane") self.assertIsNone(human.user) # We have 2 possible matches user = get_user_model().objects.create( - username="johnjuniormcclane2", first_name="John Junior", last_name="McClane" + username="johnjuniormcclane2", first_name="John Junior", + last_name="McClane" ) human = self.factory(first_name="John", last_name="McClane") self.assertIsNone(human.user) @@ -230,7 +233,8 @@ def test_str(self): """ Test __str__ method """ - journal = JournalFactory(name="Die Hard Journal", abbreviation="Die Hard J") + journal = JournalFactory(name="Die Hard Journal", + abbreviation="Die Hard J") entry = self.factory( title="Yippee-ki-yay, motherfucker", diff --git a/td_biblio/tests/test_templatetags.py b/td_biblio/tests/test_templatetags.py index 134fe2b..4a4589d 100644 --- a/td_biblio/tests/test_templatetags.py +++ b/td_biblio/tests/test_templatetags.py @@ -9,7 +9,7 @@ from django.test import TestCase -from ..factories import EntryFactory +from td_biblio.factories.entry import EntryFactory @pytest.mark.django_db @@ -24,7 +24,8 @@ def setUp(self): # Special entry with an incomplete publication date EntryFactory( - publication_date=datetime.date(1980, 1, 1), is_partial_publication_date=True + publication_date=datetime.date(1980, 1, 1), + is_partial_publication_date=True ) self.url = reverse("td_biblio:entry_list") @@ -36,4 +37,5 @@ def test_publication_date_filter(self): self.assertContains(response, publication_date_block, count=5) publication_date_block = '1980.' - self.assertContains(response, publication_date_block, count=1, html=True) + self.assertContains(response, publication_date_block, count=1, + html=True) diff --git a/td_biblio/tests/test_views.py b/td_biblio/tests/test_views.py index d4e3f94..f1fb717 100644 --- a/td_biblio/tests/test_views.py +++ b/td_biblio/tests/test_views.py @@ -18,8 +18,8 @@ from django.test import TestCase -from ..factories import CollectionFactory, EntryWithAuthorsFactory -from ..models import Entry +from td_biblio.factories import CollectionFactory, EntryWithAuthorsFactory +from td_biblio.models.entry import Entry @pytest.mark.django_db @@ -177,7 +177,8 @@ def test_author_year_filtering(self): """ # Get a valid date entry = Entry.objects.get(id=1) - params = {"author": entry.first_author.id, "year": entry.publication_date.year} + params = {"author": entry.first_author.id, + "year": entry.publication_date.year} self._test_filtering(**params) @@ -194,7 +195,8 @@ def test_get_queryset(self): self.assertEqual(response.context["current_publication_year"], date) self.assertEqual( - response.context["n_publications_filter"], self.n_publications_per_year + response.context["n_publications_filter"], + self.n_publications_per_year ) def test_get_context_data(self): @@ -211,9 +213,11 @@ def test_get_context_data(self): publication_years = [datetime.date(y, 1, 1) for y in years_range] # Context - self.assertEqual(response.context["n_publications_total"], self.n_publications) + self.assertEqual(response.context["n_publications_total"], + self.n_publications) - self.assertEqual(response.context["n_publications_filter"], self.n_publications) + self.assertEqual(response.context["n_publications_filter"], + self.n_publications) self.assertListEqual( list(response.context["publication_years"]), publication_years @@ -256,13 +260,15 @@ def test_user_must_be_a_logged_superuser(self): self.assertRedirects(response, login_redirect_url) # Log user as a normal user - self.client.login(username=self.user.username, password=self.fake_password) + self.client.login(username=self.user.username, + password=self.fake_password) response = self.client.get(self.url) self.assertEqual(response.status_code, 302) self.assertRedirects(response, login_redirect_url) # Log user as a normal user - self.client.login(username=self.user.username, password=self.fake_password) + self.client.login(username=self.user.username, + password=self.fake_password) # A standard user should be redirected to the login page response = self.client.get(self.url) @@ -270,7 +276,8 @@ def test_user_must_be_a_logged_superuser(self): self.assertRedirects(response, login_redirect_url) # Log user as a super user - self.client.login(username=self.superuser.username, password=self.fake_password) + self.client.login(username=self.superuser.username, + password=self.fake_password) # A super user should not be redirected response = self.client.get(self.url) @@ -281,7 +288,8 @@ def test_get(self): """ Test the EntryBatchImportView get method """ - self.client.login(username=self.superuser.username, password=self.fake_password) + self.client.login(username=self.superuser.username, + password=self.fake_password) response = self.client.get(self.url) # Standard response @@ -292,7 +300,8 @@ def test_post(self): """ Test the EntryBatchImportView post method """ - self.client.login(username=self.superuser.username, password=self.fake_password) + self.client.login(username=self.superuser.username, + password=self.fake_password) self.assertEqual(Entry.objects.count(), 0) @@ -316,5 +325,6 @@ def test_post(self): # We have two messages (we did the same request two times) self.assertEqual(len(response_messages), 2) for m in response_messages: - self.assertEqual(str(m), "We have successfully imported 4 reference(s).") + self.assertEqual(str(m), "We have successfully imported 4 " + "reference(s).") self.assertEqual(m.level, messages.SUCCESS) diff --git a/td_biblio/urls.py b/td_biblio/urls.py index 3b4c138..319b050 100644 --- a/td_biblio/urls.py +++ b/td_biblio/urls.py @@ -1,12 +1,16 @@ # -*- coding: utf-8 -*- from django.conf.urls import url -from . import views +from td_biblio.views.entry_batch_import import EntryBatchImportView +from td_biblio.views.entry_list import EntryListView +from td_biblio.views.find_duplicated_authors import FindDuplicatedAuthorsView + app_name = "td_biblio" urlpatterns = [ # Entry List - url("^$", views.EntryListView.as_view(), name="entry_list"), - url("^import/$", views.EntryBatchImportView.as_view(), name="import"), - url("^duplicates/$", views.FindDuplicatedAuthorsView.as_view(), name="duplicates"), + url("^$", EntryListView.as_view(), name="entry_list"), + url("^import/$", EntryBatchImportView.as_view(), name="import"), + url("^duplicates/$", FindDuplicatedAuthorsView.as_view(), + name="duplicates"), ] diff --git a/td_biblio/utils/doi_validator.py b/td_biblio/utils/doi_validator.py new file mode 100644 index 0000000..1afadde --- /dev/null +++ b/td_biblio/utils/doi_validator.py @@ -0,0 +1,9 @@ +from django.core.validators import RegexValidator +from django.utils.translation import ugettext_lazy as _ + + +DOI_REGEX = r"(10[.][0-9]{4,}(?:[.][0-9]+)*/(?:(?![\"&'<>])\S)+)" + +doi_validator = RegexValidator( + DOI_REGEX, _("One (or more) DOI is not valid"), "invalid" +) diff --git a/td_biblio/utils/loaders.py b/td_biblio/utils/loaders.py index 0583abd..e1a185d 100644 --- a/td_biblio/utils/loaders.py +++ b/td_biblio/utils/loaders.py @@ -140,7 +140,8 @@ def save_record(self, record): last_name=record_author["last_name"], ) - AuthorEntryRank.objects.get_or_create(entry=entry, author=author, rank=rank) + AuthorEntryRank.objects.get_or_create(entry=entry, + author=author, rank=rank) logger.debug("(New) Entry imported with success: {}".format(entry)) def save_records(self): @@ -173,7 +174,8 @@ def to_record(self, input): # Publication date pub_date = {"day": 1, "month": 1, "year": 1900} - input_date = dict((k, v) for (k, v) in input.items() if k in pub_date.keys()) + input_date = dict((k, v) for (k, v) in input.items() if k in + pub_date.keys()) pub_date.update(input_date) # Check if month is numerical or not try: @@ -263,7 +265,8 @@ def load_records(self, PMIDs=None): "An error occured while loading the following PMID: {}. " "Check logs for details." ).format(entry.pmid) - logger.error("{}, error: {} [{}], data: {}".format(msg, e, v, entry)) + logger.error("{}, error: {} [{}], data: {}".format(msg, e, + v, entry)) raise PMIDLoaderError(msg) self.records.append(record) @@ -301,7 +304,8 @@ def to_record(self, input): record = { "title": input.get("title", ""), "authors": [ - {"first_name": a.get("given", ""), "last_name": a.get("family", "")} + {"first_name": a.get("given", ""), "last_name": a.get( + "family", "")} for a in input.get("author") ], "journal": journal, @@ -333,6 +337,7 @@ def load_records(self, DOIs=None): "An error occured while loading the following DOI: {}. " "Check logs for details." ).format(data.get("DOI")) - logger.error("{}, error: {} [{}], data: {}".format(msg, e, v, data)) + logger.error("{}, error: {} [{}], data: {}".format(msg, e, + v, data)) raise DOILoaderError(msg) self.records.append(record) diff --git a/td_biblio/utils/pmid_validator.py b/td_biblio/utils/pmid_validator.py new file mode 100644 index 0000000..9c8edce --- /dev/null +++ b/td_biblio/utils/pmid_validator.py @@ -0,0 +1,8 @@ +from django.core.validators import RegexValidator +from django.utils.translation import ugettext_lazy as _ + + +PMID_REGEX = r"^-?\d+\Z" +pmid_validator = RegexValidator( + PMID_REGEX, _("One (or more) PMID is not valid"), "invalid" +) diff --git a/td_biblio/utils/text_to_list.py b/td_biblio/utils/text_to_list.py new file mode 100644 index 0000000..6587f16 --- /dev/null +++ b/td_biblio/utils/text_to_list.py @@ -0,0 +1,17 @@ +from operator import methodcaller + + +def text_to_list(raw): + """Transform a raw text list to a python sorted object list + Supported separators: coma, space and carriage return + """ + return sorted( + list( + set( + id.strip() + for r in map(methodcaller("split", ","), raw.split()) + for id in r + if len(id) + ) + ) + ) diff --git a/td_biblio/views.py b/td_biblio/views.py deleted file mode 100644 index e4e2c49..0000000 --- a/td_biblio/views.py +++ /dev/null @@ -1,269 +0,0 @@ -# -*- coding: utf-8 -*- -import datetime -import logging - -from django.contrib import messages -from django.contrib.auth.decorators import login_required, user_passes_test - -try: - from django.core.urlresolvers import reverse_lazy -except ImportError: - from django.urls import reverse_lazy - -from django.utils.encoding import force_text -from django.utils.translation import ugettext_lazy as _ -from django.views.generic import FormView, ListView -from django.views.generic.edit import FormMixin - -from .exceptions import DOILoaderError, PMIDLoaderError -from .forms import AuthorDuplicatesForm, EntryBatchImportForm -from .models import Author, Collection, Entry, Journal -from .utils.loaders import DOILoader, PubmedLoader - -logger = logging.getLogger("td_biblio") - - -def superuser_required(function=None): - """ - Decorator for views that checks that the user is a super user redirecting - to the log-in page if necessary. - - Inspired by Django 'login_required' decorator - """ - actual_decorator = user_passes_test(lambda u: u.is_superuser) - if function: - return actual_decorator(function) - return actual_decorator - - -class LoginRequiredMixin(object): - @classmethod - def as_view(cls, **initkwargs): - view = super(LoginRequiredMixin, cls).as_view(**initkwargs) - return login_required(view) - - -class SuperuserRequiredMixin(object): - @classmethod - def as_view(cls, **initkwargs): - view = super(SuperuserRequiredMixin, cls).as_view(**initkwargs) - return superuser_required(view) - - -class EntryListView(ListView): - """Entry list view""" - - model = Entry - paginate_by = 20 - template = "td_biblio/entry_list.html" - - def get(self, request, *args, **kwargs): - """Check GET request parameters validity and store them""" - - # -- Publication year - year = self.request.GET.get("year", None) - if year is not None: - try: - year = datetime.date(int(year), 1, 1) - except ValueError: - year = None - self.current_publication_date = year - - # -- Publication author - author = self.request.GET.get("author", None) - if author is not None: - try: - author = int(author) - except ValueError: - author = None - self.current_publication_author = author - - # -- Publication collection - collection = self.request.GET.get("collection", None) - if collection is not None: - try: - collection = int(collection) - except ValueError: - collection = None - self.current_publication_collection = collection - - return super(EntryListView, self).get(request, *args, **kwargs) - - def get_queryset(self): - """ - Add GET requests filters - """ - filters = dict() - - # Publication date - if self.current_publication_date: - year = self.current_publication_date.year - filters["publication_date__year"] = year - - # Publication authors - if self.current_publication_author: - author = Author.objects.get(id=self.current_publication_author) - aliases = list(author.aliases.values_list("id", flat=True)) - filters["authors__id__in"] = [author.id] + aliases - - # Publication collection - if self.current_publication_collection: - filters["collections__id"] = self.current_publication_collection - - # Base queryset - qs = super(EntryListView, self).get_queryset() - - # Return filtered queryset - return qs.filter(**filters) - - def get_context_data(self, **kwargs): - """ - Add filtering data to context - """ - ctx = super(EntryListView, self).get_context_data(**kwargs) - - # -- Metrics - # Publications (Entries) - ctx["n_publications_total"] = Entry.objects.count() - ctx["n_publications_filter"] = self.get_queryset().count() - - # Authors (from selected entries) - ctx["n_authors_total"] = Author.objects.filter(alias=None).count() - author_ids = self.get_queryset().values_list("authors__id", flat=True) - author_ids = list(set(author_ids)) - filtered_authors = Author.objects.filter(id__in=author_ids, alias=None) - ctx["n_authors_filter"] = filtered_authors.count() - - # Journals (Entries) - ctx["n_journals_total"] = Journal.objects.count() - journal_ids = self.get_queryset().values_list("journal__id", flat=True) - journal_ids = list(set(journal_ids)) - ctx["n_journals_filter"] = len(journal_ids) - - # -- Filters - # publication date - ctx["publication_years"] = self.get_queryset().dates( - "publication_date", "year", order="DESC" - ) - ctx["current_publication_year"] = self.current_publication_date - - # Publication author - authors_order = ("last_name", "first_name") - ctx["publication_authors"] = filtered_authors.order_by(*authors_order) - ctx["current_publication_author"] = self.current_publication_author - - # Publication collection - ctx["publication_collections"] = Collection.objects.all() - ctx[ - "current_publication_collection" - ] = self.current_publication_collection # noqa - - return ctx - - -class EntryBatchImportView(LoginRequiredMixin, SuperuserRequiredMixin, FormView): - - form_class = EntryBatchImportForm - template_name = "td_biblio/entry_import.html" - success_url = reverse_lazy("td_biblio:entry_list") - - def form_valid(self, form): - """Save to database""" - # PMIDs - pmids = form.cleaned_data["pmids"] - if len(pmids): - pm_loader = PubmedLoader() - - try: - pm_loader.load_records(PMIDs=pmids) - except PMIDLoaderError as e: - messages.error(self.request, e) - return self.form_invalid(form) - - pm_loader.save_records() - - # DOIs - dois = form.cleaned_data["dois"] - if len(dois): - doi_loader = DOILoader() - - try: - doi_loader.load_records(DOIs=dois) - except DOILoaderError as e: - messages.error(self.request, e) - return self.form_invalid(form) - - doi_loader.save_records() - - messages.success( - self.request, - _("We have successfully imported {} reference(s).").format( - len(dois) + len(pmids) - ), - ) - - return super(EntryBatchImportView, self).form_valid(form) - - -class FindDuplicatedAuthorsView( - LoginRequiredMixin, SuperuserRequiredMixin, FormMixin, ListView -): - - form_class = AuthorDuplicatesForm - model = Author - ordering = ("last_name", "first_name") - paginate_by = 30 - queryset = Author.objects.filter(alias=None) - success_url = reverse_lazy("td_biblio:duplicates") - template_name = "td_biblio/find_duplicated_authors.html" - - def _add_aliases(self, authors, alias): - return authors.update(alias=alias) - - def form_valid(self, form): - - authors = form.cleaned_data["authors"] - alias = form.cleaned_data["alias"] - match = self._add_aliases(authors, alias) - - messages.success( - self.request, - _("Added '{}' as alias for {} author(s).").format( - alias.get_formatted_name(), match - ), - ) - - return super(FindDuplicatedAuthorsView, self).form_valid(form) - - def get_context_data(self, **kwargs): - - ctx = super(FindDuplicatedAuthorsView, self).get_context_data(**kwargs) - ctx.update({"paginate_by": self.get_paginate_by(self.queryset)}) - return ctx - - def get_paginate_by(self, queryset): - - by = self.request.GET.get("by", None) - if not by: - return self.paginate_by - try: - return int(by) - except ValueError: - pass - - def get_success_url(self): - """Add get parameters""" - url = force_text(self.success_url) - if self.request.GET: - url = "{}?{}".format(url, self.request.GET.urlencode()) - return url - - def post(self, request, *args, **kwargs): - - self.object_list = self.get_queryset() - - form = self.get_form() - if form.is_valid(): - return self.form_valid(form) - else: - return self.form_invalid(form) diff --git a/td_biblio/views/__init__.py b/td_biblio/views/__init__.py new file mode 100644 index 0000000..e6df719 --- /dev/null +++ b/td_biblio/views/__init__.py @@ -0,0 +1,9 @@ +# coding: utf-8 +__author__ = 'Alison Mukoma ' +__license__ = 'MIT' +__copyright__ = 'tailordev.fr' + +from .entry_batch_import import * # noqa +from .entry_list import * # noqa +from .find_duplicated_authors import * # noqa +from .mixins import * # noqa diff --git a/td_biblio/views/entry_batch_import.py b/td_biblio/views/entry_batch_import.py new file mode 100644 index 0000000..7a5a5de --- /dev/null +++ b/td_biblio/views/entry_batch_import.py @@ -0,0 +1,63 @@ +import logging + +from django.contrib import messages + +try: + from django.core.urlresolvers import reverse_lazy +except ImportError: + from django.urls import reverse_lazy + +from django.views.generic import FormView +from django.utils.translation import ugettext_lazy as _ + +from td_biblio.utils.loaders import DOILoader, PubmedLoader +from td_biblio.exceptions import PMIDLoaderError, DOILoaderError +from td_biblio.forms.entry_batch_import import EntryBatchImportForm +from td_biblio.views.mixins import LoginRequiredMixin, SuperuserRequiredMixin + +logger = logging.getLogger("td_biblio") + + +class EntryBatchImportView(LoginRequiredMixin, SuperuserRequiredMixin, + FormView): + + form_class = EntryBatchImportForm + template_name = "td_biblio/entry_import.html" + success_url = reverse_lazy("td_biblio:entry_list") + + def form_valid(self, form): + """Save to database""" + # PMIDs + pmids = form.cleaned_data["pmids"] + if len(pmids): + pm_loader = PubmedLoader() + + try: + pm_loader.load_records(PMIDs=pmids) + except PMIDLoaderError as e: + messages.error(self.request, e) + return self.form_invalid(form) + + pm_loader.save_records() + + # DOIs + dois = form.cleaned_data["dois"] + if len(dois): + doi_loader = DOILoader() + + try: + doi_loader.load_records(DOIs=dois) + except DOILoaderError as e: + messages.error(self.request, e) + return self.form_invalid(form) + + doi_loader.save_records() + + messages.success( + self.request, + _("We have successfully imported {} reference(s).").format( + len(dois) + len(pmids) + ), + ) + + return super(EntryBatchImportView, self).form_valid(form) diff --git a/td_biblio/views/entry_list.py b/td_biblio/views/entry_list.py new file mode 100644 index 0000000..dcd822f --- /dev/null +++ b/td_biblio/views/entry_list.py @@ -0,0 +1,122 @@ +import datetime +import logging + +from django.views.generic import ListView + +from td_biblio.models.author import Author +from td_biblio.models.entry import Entry +from td_biblio.models.journal import Journal +from td_biblio.models.collection import Collection + +logger = logging.getLogger("td_biblio") + + +class EntryListView(ListView): + """Entry list view""" + + model = Entry + paginate_by = 20 + template = "td_biblio/entry_list.html" + + def get(self, request, *args, **kwargs): + """Check GET request parameters validity and store them""" + + # -- Publication year + year = self.request.GET.get("year", None) + if year is not None: + try: + year = datetime.date(int(year), 1, 1) + except ValueError: + year = None + self.current_publication_date = year + + # -- Publication author + author = self.request.GET.get("author", None) + if author is not None: + try: + author = int(author) + except ValueError: + author = None + self.current_publication_author = author + + # -- Publication collection + collection = self.request.GET.get("collection", None) + if collection is not None: + try: + collection = int(collection) + except ValueError: + collection = None + self.current_publication_collection = collection + + return super(EntryListView, self).get(request, *args, **kwargs) + + def get_queryset(self): + """ + Add GET requests filters + """ + filters = dict() + + # Publication date + if self.current_publication_date: + year = self.current_publication_date.year + filters["publication_date__year"] = year + + # Publication authors + if self.current_publication_author: + author = Author.objects.get(id=self.current_publication_author) + aliases = list(author.aliases.values_list("id", flat=True)) + filters["authors__id__in"] = [author.id] + aliases + + # Publication collection + if self.current_publication_collection: + filters["collections__id"] = self.current_publication_collection + + # Base queryset + qs = super(EntryListView, self).get_queryset() + + # Return filtered queryset + return qs.filter(**filters) + + def get_context_data(self, **kwargs): + """ + Add filtering data to context + """ + ctx = super(EntryListView, self).get_context_data(**kwargs) + + # -- Metrics + # Publications (Entries) + ctx["n_publications_total"] = Entry.objects.count() + ctx["n_publications_filter"] = self.get_queryset().count() + + # Authors (from selected entries) + ctx["n_authors_total"] = Author.objects.filter(alias=None).count() + author_ids = self.get_queryset().values_list("authors__id", flat=True) + author_ids = list(set(author_ids)) + filtered_authors = Author.objects.filter(id__in=author_ids, alias=None) + ctx["n_authors_filter"] = filtered_authors.count() + + # Journals (Entries) + ctx["n_journals_total"] = Journal.objects.count() + journal_ids = self.get_queryset().values_list("journal__id", flat=True) + journal_ids = list(set(journal_ids)) + ctx["n_journals_filter"] = len(journal_ids) + + # -- Filters + # publication date + ctx["publication_years"] = self.get_queryset().dates( + "publication_date", "year", order="DESC" + ) + ctx["current_publication_year"] = self.current_publication_date + + # Publication author + authors_order = ("last_name", "first_name") + ctx["publication_authors"] = filtered_authors.order_by(*authors_order) + ctx["current_publication_author"] = self.current_publication_author + + # Publication collection + ctx["publication_collections"] = Collection.objects.all() + ctx[ + "current_publication_collection" + ] = self.current_publication_collection # noqa + + return ctx diff --git a/td_biblio/views/find_duplicated_authors.py b/td_biblio/views/find_duplicated_authors.py new file mode 100644 index 0000000..05e2d79 --- /dev/null +++ b/td_biblio/views/find_duplicated_authors.py @@ -0,0 +1,84 @@ +import logging + +from td_biblio.forms import AuthorDuplicatesForm +from td_biblio.models.author import Author +from td_biblio.views.mixins import LoginRequiredMixin, SuperuserRequiredMixin + +from django.contrib import messages + +try: + from django.core.urlresolvers import reverse_lazy +except ImportError: + from django.urls import reverse_lazy + +from django.utils.encoding import force_text + +from django.utils.translation import ugettext_lazy as _ +from django.views.generic import ListView +from django.views.generic.edit import FormMixin + +logger = logging.getLogger("td_biblio") + + +class FindDuplicatedAuthorsView( + LoginRequiredMixin, SuperuserRequiredMixin, FormMixin, ListView +): + + form_class = AuthorDuplicatesForm + model = Author + ordering = ("last_name", "first_name") + paginate_by = 30 + queryset = Author.objects.filter(alias=None) + success_url = reverse_lazy("td_biblio:duplicates") + template_name = "td_biblio/find_duplicated_authors.html" + + def _add_aliases(self, authors, alias): + return authors.update(alias=alias) + + def form_valid(self, form): + + authors = form.cleaned_data["authors"] + alias = form.cleaned_data["alias"] + match = self._add_aliases(authors, alias) + + messages.success( + self.request, + _("Added '{}' as alias for {} author(s).").format( + alias.get_formatted_name(), match + ), + ) + + return super(FindDuplicatedAuthorsView, self).form_valid(form) + + def get_context_data(self, **kwargs): + + ctx = super(FindDuplicatedAuthorsView, self).get_context_data(**kwargs) + ctx.update({"paginate_by": self.get_paginate_by(self.queryset)}) + return ctx + + def get_paginate_by(self, queryset): + + by = self.request.GET.get("by", None) + if not by: + return self.paginate_by + try: + return int(by) + except ValueError: + pass + + def get_success_url(self): + """Add get parameters""" + url = force_text(self.success_url) + if self.request.GET: + url = "{}?{}".format(url, self.request.GET.urlencode()) + return url + + def post(self, request, *args, **kwargs): + + self.object_list = self.get_queryset() + + form = self.get_form() + if form.is_valid(): + return self.form_valid(form) + else: + return self.form_invalid(form) diff --git a/td_biblio/views/mixins.py b/td_biblio/views/mixins.py new file mode 100644 index 0000000..ee776aa --- /dev/null +++ b/td_biblio/views/mixins.py @@ -0,0 +1,32 @@ +import logging + +from django.contrib.auth.decorators import login_required, user_passes_test + +logger = logging.getLogger("td_biblio") + + +def superuser_required(function=None): + """ + Decorator for views that checks that the user is a super user redirecting + to the log-in page if necessary. + + Inspired by Django 'login_required' decorator + """ + actual_decorator = user_passes_test(lambda u: u.is_superuser) + if function: + return actual_decorator(function) + return actual_decorator + + +class LoginRequiredMixin(object): + @classmethod + def as_view(cls, **initkwargs): + view = super(LoginRequiredMixin, cls).as_view(**initkwargs) + return login_required(view) + + +class SuperuserRequiredMixin(object): + @classmethod + def as_view(cls, **initkwargs): + view = super(SuperuserRequiredMixin, cls).as_view(**initkwargs) + return superuser_required(view)