├── .docker-hub-test ├── .editorconfig ├── .gitattributes ├── .gitignore ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── Dockerfile ├── LICENSE ├── Pipfile ├── Pipfile.lock ├── README-de.md ├── README-el.md ├── README.md ├── THANKS.md ├── ci └── deploy-docker ├── data └── .keep ├── docker-compose.env.example ├── docker-compose.yml.example ├── docs ├── Dockerfile ├── Makefile ├── _static │ ├── .keep │ ├── Screenshot_first_logged.png │ ├── Screenshot_first_run_login.png │ ├── Screenshot_upload_and_scanned.png │ ├── custom.css │ ├── lxc-install.svg │ └── screenshot.png ├── api.rst ├── changelog.rst ├── conf.py ├── consumption.rst ├── contributing.rst ├── customising.rst ├── examples │ └── lxc │ │ ├── lxc-install.sh │ │ └── paperless.conf ├── extending.rst ├── guesswork.rst ├── index.rst ├── migrating.rst ├── requirements.rst ├── requirements.txt ├── scanners.rst ├── screenshots.rst ├── setup.rst ├── troubleshooting.rst └── utilities.rst ├── management └── commands │ └── create_superuser_with_password.py ├── media └── documents │ ├── originals │ └── .keep │ └── thumbnails │ └── .keep ├── overrides └── README.md ├── paperless.conf.example ├── presentation ├── README.md ├── contrib │ ├── font-awesome-4.3.0 │ │ ├── css │ │ │ ├── font-awesome.css │ │ │ └── font-awesome.min.css │ │ ├── fonts │ │ │ ├── FontAwesome.otf │ │ │ ├── fontawesome-webfont.eot │ │ │ ├── fontawesome-webfont.svg │ │ │ ├── fontawesome-webfont.ttf │ │ │ ├── fontawesome-webfont.woff │ │ │ └── fontawesome-webfont.woff2 │ │ ├── less │ │ │ ├── animated.less │ │ │ ├── bordered-pulled.less │ │ │ ├── core.less │ │ │ ├── fixed-width.less │ │ │ ├── font-awesome.less │ │ │ ├── icons.less │ │ │ ├── larger.less │ │ │ ├── list.less │ │ │ ├── mixins.less │ │ │ ├── path.less │ │ │ ├── rotated-flipped.less │ │ │ ├── stacked.less │ │ │ └── variables.less │ │ └── scss │ │ │ ├── _animated.scss │ │ │ ├── _bordered-pulled.scss │ │ │ ├── _core.scss │ │ │ ├── _fixed-width.scss │ │ │ ├── _icons.scss │ │ │ ├── _larger.scss │ │ │ ├── _list.scss │ │ │ ├── _mixins.scss │ │ │ ├── _path.scss │ │ │ ├── _rotated-flipped.scss │ │ │ ├── _stacked.scss │ │ │ ├── _variables.scss │ │ │ └── font-awesome.scss │ └── google │ │ ├── css │ │ └── lato.css │ │ └── fonts │ │ ├── DvlFBScY1r-FMtZSYIYoYw.ttf │ │ ├── HkF_qI1x_noxlxhrhMQYEKCWcynf_cDxXwCLxiixG1c.ttf │ │ ├── LqowQDslGv4DmUBAfWa2Vw.ttf │ │ └── v0SdcGFAl2aezM9Vq_aFTQ.ttf ├── css │ ├── print │ │ ├── paper.css │ │ └── pdf.css │ ├── reveal.css │ ├── reveal.scss │ └── theme │ │ ├── README.md │ │ ├── beige.css │ │ ├── black.css │ │ ├── blood.css │ │ ├── league.css │ │ ├── moon.css │ │ ├── night.css │ │ ├── serif.css │ │ ├── simple.css │ │ ├── sky.css │ │ ├── solarized.css │ │ ├── source │ │ ├── beige.scss │ │ ├── black.scss │ │ ├── blood.scss │ │ ├── league.scss │ │ ├── moon.scss │ │ ├── night.scss │ │ ├── serif.scss │ │ ├── simple.scss │ │ ├── sky.scss │ │ ├── solarized.scss │ │ └── white.scss │ │ ├── template │ │ ├── mixins.scss │ │ ├── settings.scss │ │ └── theme.scss │ │ └── white.css ├── img │ ├── kitten.jpg │ ├── pony.png │ ├── repo.svg │ └── stack.jpg ├── index.html ├── js │ └── reveal.js ├── lib │ ├── css │ │ └── zenburn.css │ ├── font │ │ ├── league-gothic │ │ │ ├── LICENSE │ │ │ ├── league-gothic.css │ │ │ ├── league-gothic.eot │ │ │ ├── league-gothic.ttf │ │ │ └── league-gothic.woff │ │ └── source-sans-pro │ │ │ ├── LICENSE │ │ │ ├── source-sans-pro-italic.eot │ │ │ ├── source-sans-pro-italic.ttf │ │ │ ├── source-sans-pro-italic.woff │ │ │ ├── source-sans-pro-regular.eot │ │ │ ├── source-sans-pro-regular.ttf │ │ │ ├── source-sans-pro-regular.woff │ │ │ ├── source-sans-pro-semibold.eot │ │ │ ├── source-sans-pro-semibold.ttf │ │ │ ├── source-sans-pro-semibold.woff │ │ │ ├── source-sans-pro-semibolditalic.eot │ │ │ ├── source-sans-pro-semibolditalic.ttf │ │ │ ├── source-sans-pro-semibolditalic.woff │ │ │ └── source-sans-pro.css │ └── js │ │ ├── classList.js │ │ ├── head.min.js │ │ └── html5shiv.js └── plugin │ ├── highlight │ └── highlight.js │ ├── leap │ └── leap.js │ ├── markdown │ ├── example.html │ ├── example.md │ ├── markdown.js │ └── marked.js │ ├── math │ └── math.js │ ├── multiplex │ ├── client.js │ ├── index.js │ └── master.js │ ├── notes-server │ ├── client.js │ ├── index.js │ └── notes.html │ ├── notes │ ├── notes.html │ └── notes.js │ ├── print-pdf │ └── print-pdf.js │ ├── remotes │ └── remotes.js │ ├── search │ └── search.js │ └── zoom-js │ └── zoom.js ├── requirements.txt ├── resources └── logo │ ├── print │ ├── eps │ │ ├── Black logo - no background.eps │ │ ├── Color logo - no background.eps │ │ ├── Color logo with background.eps │ │ └── White logo - no background.eps │ └── pdf │ │ ├── Black logo - no background.pdf │ │ ├── Color logo - no background.pdf │ │ ├── Color logo with background.pdf │ │ └── White logo - no background.pdf │ └── web │ ├── png │ ├── Black logo - no background.png │ ├── Color logo - no background.png │ ├── Color logo with background.png │ └── White logo - no background.png │ └── svg │ ├── Black logo - no background.svg │ ├── Color logo - no background.svg │ ├── Color logo with background.svg │ ├── White logo - no background.svg │ └── square.svg ├── scripts ├── docker-entrypoint.sh ├── gunicorn.conf ├── paperless-consumer.service ├── paperless-webserver.service └── post-consumption-example.sh └── src ├── documents ├── __init__.py ├── actions.py ├── admin.py ├── apps.py ├── checks.py ├── consumer.py ├── filters.py ├── forms.py ├── loggers.py ├── mail.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ ├── change_storage_type.py │ │ ├── document_consumer.py │ │ ├── document_correspondents.py │ │ ├── document_exporter.py │ │ ├── document_importer.py │ │ ├── document_logs.py │ │ ├── document_renamer.py │ │ ├── document_retagger.py │ │ └── loaddata_stdin.py ├── managers.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_auto_20151226_1316.py │ ├── 0003_sender.py │ ├── 0004_auto_20160114_1844.py │ ├── 0005_auto_20160123_0313.py │ ├── 0006_auto_20160123_0430.py │ ├── 0007_auto_20160126_2114.py │ ├── 0008_document_file_type.py │ ├── 0009_auto_20160214_0040.py │ ├── 0010_log.py │ ├── 0011_auto_20160303_1929.py │ ├── 0012_auto_20160305_0040.py │ ├── 0013_auto_20160325_2111.py │ ├── 0014_document_checksum.py │ ├── 0015_add_insensitive_to_match.py │ ├── 0016_auto_20170325_1558.py │ ├── 0017_auto_20170512_0507.py │ ├── 0018_auto_20170715_1712.py │ ├── 0019_add_consumer_user.py │ ├── 0020_document_added.py │ ├── 0021_document_storage_type.py │ ├── 0022_auto_20181007_1420.py │ ├── 0023_document_current_filename.py │ └── __init__.py ├── mixins.py ├── models.py ├── parsers.py ├── serialisers.py ├── settings.py ├── signals │ ├── __init__.py │ └── handlers.py ├── static │ ├── documents │ │ └── img │ │ │ ├── gif.png │ │ │ ├── image.png │ │ │ ├── jpg.png │ │ │ ├── pdf.png │ │ │ ├── png.png │ │ │ └── tiff.png │ ├── js │ │ └── colours.js │ └── paperless.css ├── templates │ ├── admin │ │ ├── base_site.html │ │ ├── documents │ │ │ └── document │ │ │ │ ├── change_form.html │ │ │ │ ├── change_list.html │ │ │ │ ├── change_list_results.html │ │ │ │ └── select_object.html │ │ └── index.html │ └── documents │ │ └── index.html ├── templatetags │ ├── __init__.py │ ├── customisation.py │ └── hacks.py ├── tests │ ├── __init__.py │ ├── factories.py │ ├── samples │ │ ├── inline_mail.txt │ │ ├── letter.pdf │ │ └── mail.txt │ ├── test_checks.py │ ├── test_consumer.py │ ├── test_document_model.py │ ├── test_file_handling.py │ ├── test_importer.py │ ├── test_logger.py │ ├── test_mail.py │ ├── test_matchables.py │ └── test_models.py └── views.py ├── manage.py ├── paperless ├── __init__.py ├── checks.py ├── db.py ├── middleware.py ├── mixins.py ├── models.py ├── settings.py ├── static │ └── paperless │ │ └── img │ │ ├── favicon.ico │ │ ├── logo-dark.png │ │ └── logo-light.png ├── urls.py ├── version.py ├── views.py └── wsgi.py ├── paperless_tesseract ├── __init__.py ├── apps.py ├── languages.py ├── parsers.py ├── signals.py └── tests │ ├── __init__.py │ ├── samples │ └── no-text.png │ ├── test_date.py │ ├── test_ocr.py │ └── test_signals.py ├── paperless_text ├── __init__.py ├── apps.py ├── parsers.py └── signals.py ├── reminders ├── __init__.py ├── admin.py ├── apps.py ├── filters.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_auto_20181007_1420.py │ └── __init__.py ├── models.py ├── serialisers.py ├── tests.py └── views.py ├── setup.cfg └── tox.ini /.docker-hub-test: -------------------------------------------------------------------------------- 1 | Docker Hub test 2 2 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig: http://EditorConfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = tab 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | end_of_line = lf 11 | charset = utf-8 12 | max_line_length = 79 13 | 14 | [{*.html,*.css,*.js}] 15 | max_line_length = off 16 | 17 | [*.py] 18 | indent_size = 4 19 | indent_style = space 20 | 21 | [*.yml] 22 | indent_style = space 23 | 24 | # Tests don't get a line width restriction. It's still a good idea to follow 25 | # the 79 character rule, but in the interests of clarity, tests often need to 26 | # violate it. 27 | [**/test_*.py] 28 | max_line_length = off 29 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | THANKS.md merge=union 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | #lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .coverage.* 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | *,cover 45 | .pytest_cache 46 | 47 | # Translations 48 | *.mo 49 | *.pot 50 | 51 | # Django stuff: 52 | *.log 53 | 54 | # Sphinx documentation 55 | docs/_build/ 56 | 57 | # PyBuilder 58 | target/ 59 | 60 | # Stored PDFs 61 | media/documents/*.gpg 62 | media/documents/thumbnails/* 63 | media/documents/originals/* 64 | media/overrides.css 65 | media/overrides.js 66 | 67 | # Sqlite database 68 | db.sqlite3 69 | db.sqlite3-journal 70 | 71 | # PyCharm 72 | .idea 73 | 74 | # Other stuff that doesn't belong 75 | .virtualenv 76 | virtualenv 77 | docker-compose.yml 78 | docker-compose.env 79 | 80 | # Used for development 81 | scripts/import-for-development 82 | scripts/nuke 83 | 84 | # Static files collected by the collectstatic command 85 | ./static/ 86 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | before_install: 4 | - sudo apt-get update -qq 5 | - sudo apt-get install -qq libpoppler-cpp-dev unpaper tesseract-ocr imagemagick ghostscript optipng 6 | 7 | sudo: false 8 | 9 | matrix: 10 | include: 11 | - python: "3.5" 12 | - python: "3.6" 13 | - python: "3.7-dev" 14 | - env: 15 | - BUILD_DOCKER=1 16 | # Variable to add to publish the Docker image: 17 | # * DOCKER_USERNAME 18 | # * DOCKER_PASSWORD, to be encrypted, use `travis encrypt DOCKER_PASSWORD=` 19 | services: 20 | - docker 21 | before_install: 22 | - true 23 | install: 24 | - true 25 | script: 26 | - docker build --tag=the-paperless-project/paperless . 27 | after_success: 28 | - true 29 | 30 | install: 31 | - pip install --upgrade pip pipenv sphinx 32 | - pipenv lock -r > requirements.txt 33 | - pip install -r requirements.txt 34 | 35 | script: 36 | - cd src/ 37 | - pytest --cov 38 | - pycodestyle 39 | - sphinx-build -b html ../docs ../docs/_build -W 40 | 41 | after_success: 42 | - coveralls 43 | 44 | deploy: 45 | - provider: script 46 | skip_cleanup: true 47 | script: ci/deploy-docker 48 | on: 49 | tags: true 50 | condition: '"${BUILD_DOCKER}" = 1' 51 | - provider: script 52 | skip_cleanup: true 53 | script: ci/deploy-docker 54 | on: 55 | branch: master 56 | condition: '"${BUILD_DOCKER}" = 1' 57 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * Unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at code@danielquinn.org. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4 to remove puritanical language. The original is available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.12 2 | 3 | LABEL maintainer="The Paperless Project https://github.com/the-paperless-project/paperless" \ 4 | contributors="Guy Addadi , Pit Kleyersburg , \ 5 | Sven Fischer " 6 | 7 | # Copy Pipfiles file, init script and gunicorn.conf 8 | COPY Pipfile* /usr/src/paperless/ 9 | COPY scripts/docker-entrypoint.sh /sbin/docker-entrypoint.sh 10 | COPY scripts/gunicorn.conf /usr/src/paperless/ 11 | 12 | # Set export and consumption directories 13 | ENV PAPERLESS_EXPORT_DIR=/export \ 14 | PAPERLESS_CONSUMPTION_DIR=/consume 15 | 16 | RUN apk add --no-cache \ 17 | bash \ 18 | curl \ 19 | ghostscript \ 20 | gnupg \ 21 | imagemagick \ 22 | libmagic \ 23 | libpq \ 24 | optipng \ 25 | poppler \ 26 | python3 \ 27 | shadow \ 28 | sudo \ 29 | tesseract-ocr \ 30 | tzdata \ 31 | unpaper && \ 32 | apk add --no-cache --virtual .build-dependencies \ 33 | g++ \ 34 | gcc \ 35 | jpeg-dev \ 36 | musl-dev \ 37 | poppler-dev \ 38 | postgresql-dev \ 39 | python3-dev \ 40 | zlib-dev && \ 41 | # Install python dependencies 42 | python3 -m ensurepip && \ 43 | rm -r /usr/lib/python*/ensurepip && \ 44 | cd /usr/src/paperless && \ 45 | pip3 install --upgrade pip pipenv && \ 46 | pipenv install --system --deploy && \ 47 | # Remove build dependencies 48 | apk del .build-dependencies && \ 49 | # Create the consumption directory 50 | mkdir -p $PAPERLESS_CONSUMPTION_DIR && \ 51 | # Create user 52 | addgroup -g 1000 paperless && \ 53 | adduser -D -u 1000 -G paperless -h /usr/src/paperless paperless && \ 54 | chown -Rh paperless:paperless /usr/src/paperless && \ 55 | mkdir -p $PAPERLESS_EXPORT_DIR && \ 56 | # Avoid setrlimit warnings 57 | # See: https://gitlab.alpinelinux.org/alpine/aports/issues/11122 58 | echo 'Set disable_coredump false' >> /etc/sudo.conf && \ 59 | # Setup entrypoint 60 | chmod 755 /sbin/docker-entrypoint.sh 61 | 62 | WORKDIR /usr/src/paperless/src 63 | # Mount volumes and set Entrypoint 64 | VOLUME ["/usr/src/paperless/data", "/usr/src/paperless/media", "/consume", "/export"] 65 | ENTRYPOINT ["/sbin/docker-entrypoint.sh"] 66 | CMD ["--help"] 67 | 68 | # Copy application 69 | COPY src/ /usr/src/paperless/src/ 70 | COPY data/ /usr/src/paperless/data/ 71 | COPY media/ /usr/src/paperless/media/ 72 | 73 | # Collect static files 74 | RUN sudo -HEu paperless /usr/src/paperless/src/manage.py collectstatic --clear --no-input 75 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.python.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | django = "<2.1,>=2.0" 8 | pillow = "*" 9 | coveralls = "*" 10 | dateparser = "*" 11 | django-cors-headers = "*" 12 | django-crispy-forms = "*" 13 | django-extensions = "*" 14 | django-filter = "*" 15 | djangorestframework = "*" 16 | factory-boy = "*" 17 | filemagic = "*" 18 | fuzzywuzzy = {extras = ["speedup"],version = "==0.15.0"} 19 | gunicorn = "*" 20 | inotify-simple = "*" 21 | langdetect = "*" 22 | pdftotext = "*" 23 | pyocr = "*" 24 | python-dateutil = "*" 25 | python-dotenv = "*" 26 | python-gnupg = "*" 27 | pytz = "*" 28 | sphinx = "*" 29 | tox = "*" 30 | pycodestyle = "*" 31 | pytest = "*" 32 | pytest-cov = "*" 33 | pytest-django = "*" 34 | pytest-sugar = "*" 35 | pytest-env = "*" 36 | pytest-xdist = "*" 37 | psycopg2 = "*" 38 | djangoql = "*" 39 | whitenoise = "*" 40 | brotli = "*" 41 | 42 | [dev-packages] 43 | ipython = "*" 44 | -------------------------------------------------------------------------------- /THANKS.md: -------------------------------------------------------------------------------- 1 | # Thanks for using Paperless! 2 | 3 | Working on this project has been exhausting, but rewarding at the same time. 4 | It's just wonderful that so many people are using this thing, and in so many 5 | crazy ways. 6 | 7 | This file is here for everyone to post their own stories about how you use this 8 | code. It helps me to understand who's using it and why, and maybe to give 9 | others an idea of how it might be used. It's based on a Twitter exchange 10 | between [John Glanville](https://twitter.com/hexapodium) and 11 | [Julia Evans](https://github.com/jvns) and later better defined [here](https://github.com/paulmolluzzo/thanks-md). 12 | 13 | To contribute, simply issue a pull request that appends to this file something 14 | like this: 15 | 16 | ``` 17 | ### Your Name 18 | Some friendly message 19 | ``` 20 | -------------------------------------------------------------------------------- /ci/deploy-docker: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ "${DOCKER_USERNAME}" == "" -o "${DOCKER_PASSWORD}" == "" ] 4 | then 5 | exit 0 6 | fi 7 | 8 | docker login --username=${DOCKER_USERNAME} --password=${DOCKER_PASSWORD} 9 | if [ "${TRAVIS_TAG}" != "" ] 10 | then 11 | docker tag the-paperless-project/paperless the-paperless-project/paperless:${TRAVIS_TAG} 12 | docker push the-paperless-project/paperless:${TRAVIS_TAG} 13 | else 14 | docker push the-paperless-project/paperless 15 | fi 16 | -------------------------------------------------------------------------------- /data/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-paperless-project/paperless/9b0063c9731f7c5f65b1852cb8caff97f5e40ba4/data/.keep -------------------------------------------------------------------------------- /docker-compose.env.example: -------------------------------------------------------------------------------- 1 | # Environment variables to set for Paperless 2 | # Commented out variables will be replaced with a default within Paperless. 3 | # 4 | # In addition to what you see here, you can also define any values you find in 5 | # paperless.conf.example here. Values like: 6 | # 7 | # * PAPERLESS_PASSPHRASE 8 | # * PAPERLESS_CONSUMPTION_DIR 9 | # * PAPERLESS_CONSUME_MAIL_HOST 10 | # 11 | # ...are all explained in that file but can be defined here, since the Docker 12 | # installation doesn't make use of paperless.conf. 13 | # 14 | # NOTE: values in paperless.conf should be wrapped in double quotes, but not in this file 15 | # Example: 16 | # paperless.conf: PAPERLESS_FORGIVING_OCR="true" 17 | # docker-compose.env (this file): PAPERLESS_FORGIVING_OCR=true 18 | 19 | # Use this variable to set a timezone for the Paperless Docker containers. If not specified, defaults to UTC. 20 | # TZ=America/Los_Angeles 21 | 22 | # Additional languages to install for text recognition. Note that this is 23 | # different from PAPERLESS_OCR_LANGUAGE (default=eng), which defines the 24 | # default language used when guessing the language from the OCR output. 25 | # PAPERLESS_OCR_LANGUAGES=deu ita 26 | 27 | # Set Paperless to use SSL for the web interface. 28 | # Enabling this will require ssl.key and ssl.cert files in paperless' data directory. 29 | # PAPERLESS_USE_SSL=false 30 | 31 | # You can change the default user and group id to a custom one 32 | # USERMAP_UID=1000 33 | # USERMAP_GID=1000 34 | -------------------------------------------------------------------------------- /docker-compose.yml.example: -------------------------------------------------------------------------------- 1 | version: '2.1' 2 | 3 | services: 4 | webserver: 5 | build: ./ 6 | # uncomment the following line to start automatically on system boot 7 | # restart: always 8 | ports: 9 | # You can adapt the port you want Paperless to listen on by 10 | # modifying the part before the `:`. 11 | - "8000:8000" 12 | healthcheck: 13 | test: ["CMD", "curl" , "-f", "http://localhost:8000"] 14 | interval: 30s 15 | timeout: 10s 16 | retries: 5 17 | volumes: 18 | - data:/usr/src/paperless/data 19 | - media:/usr/src/paperless/media 20 | # You have to adapt the local path you want the consumption 21 | # directory to mount to by modifying the part before the ':'. 22 | - ./consume:/consume 23 | env_file: docker-compose.env 24 | # The reason the line is here is so that the webserver that doesn't do 25 | # any text recognition and doesn't have to install unnecessary 26 | # languages the user might have set in the env-file by overwriting the 27 | # value with nothing. 28 | environment: 29 | - PAPERLESS_OCR_LANGUAGES= 30 | command: ["gunicorn", "-b", "0.0.0.0:8000"] 31 | 32 | consumer: 33 | build: ./ 34 | # uncomment the following line to start automatically on system boot 35 | # restart: always 36 | depends_on: 37 | webserver: 38 | condition: service_healthy 39 | volumes: 40 | - data:/usr/src/paperless/data 41 | - media:/usr/src/paperless/media 42 | # This should be set to the same value as the consume directory 43 | # in the webserver service above. 44 | - ./consume:/consume 45 | # Likewise, you can add a local path to mount a directory for 46 | # exporting. This is not strictly needed for paperless to 47 | # function, only if you're exporting your files: uncomment 48 | # it and fill in a local path if you know you're going to 49 | # want to export your documents. 50 | # - /path/to/another/arbitrary/place:/export 51 | env_file: docker-compose.env 52 | command: ["document_consumer"] 53 | 54 | volumes: 55 | data: 56 | media: 57 | -------------------------------------------------------------------------------- /docs/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.5.1 2 | MAINTAINER Pit Kleyersburg 3 | 4 | # Install Sphinx and Pygments 5 | RUN pip install Sphinx Pygments 6 | 7 | # Setup directories, copy data 8 | RUN mkdir /build 9 | COPY . /build 10 | WORKDIR /build/docs 11 | 12 | # Build documentation 13 | RUN make html 14 | 15 | # Start webserver 16 | WORKDIR /build/docs/_build/html 17 | EXPOSE 8000/tcp 18 | CMD ["python3", "-m", "http.server"] 19 | -------------------------------------------------------------------------------- /docs/_static/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-paperless-project/paperless/9b0063c9731f7c5f65b1852cb8caff97f5e40ba4/docs/_static/.keep -------------------------------------------------------------------------------- /docs/_static/Screenshot_first_logged.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-paperless-project/paperless/9b0063c9731f7c5f65b1852cb8caff97f5e40ba4/docs/_static/Screenshot_first_logged.png -------------------------------------------------------------------------------- /docs/_static/Screenshot_first_run_login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-paperless-project/paperless/9b0063c9731f7c5f65b1852cb8caff97f5e40ba4/docs/_static/Screenshot_first_run_login.png -------------------------------------------------------------------------------- /docs/_static/Screenshot_upload_and_scanned.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-paperless-project/paperless/9b0063c9731f7c5f65b1852cb8caff97f5e40ba4/docs/_static/Screenshot_upload_and_scanned.png -------------------------------------------------------------------------------- /docs/_static/custom.css: -------------------------------------------------------------------------------- 1 | /* override table width restrictions */ 2 | @media screen and (min-width: 767px) { 3 | 4 | .wy-table-responsive table td { 5 | /* !important prevents the common CSS stylesheets from 6 | overriding this as on RTD they are loaded after this stylesheet */ 7 | white-space: normal !important; 8 | } 9 | 10 | .wy-table-responsive { 11 | overflow: visible !important; 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /docs/_static/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-paperless-project/paperless/9b0063c9731f7c5f65b1852cb8caff97f5e40ba4/docs/_static/screenshot.png -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | .. _api: 2 | 3 | The REST API 4 | ############ 5 | 6 | Paperless makes use of the `Django REST Framework`_ standard API interface 7 | because of its inherent awesomeness. Conveniently, the system is also 8 | self-documenting, so to learn more about the access points, schema, what's 9 | accepted and what isn't, you need only visit ``/api`` on your local Paperless 10 | installation. 11 | 12 | .. _Django REST Framework: http://django-rest-framework.org/ 13 | 14 | 15 | .. _api-uploading: 16 | 17 | Uploading 18 | --------- 19 | 20 | File uploads in an API are hard and so far as I've been able to tell, there's 21 | no standard way of accepting them, so rather than crowbar file uploads into the 22 | REST API and endure that headache, I've left that process to a simple HTTP 23 | POST, documented on the :ref:`consumption page `. 24 | -------------------------------------------------------------------------------- /docs/customising.rst: -------------------------------------------------------------------------------- 1 | .. _customising: 2 | 3 | Customising Paperless 4 | ##################### 5 | 6 | Currently, the Paperless' interface is just the default Django admin, which 7 | while powerful, is rather boring. If you'd like to give the site a bit of a 8 | face-lift, or if you simply want to adjust the colours, contrast, or font size 9 | to make things easier to read, you can do that by adding your own CSS or 10 | Javascript quite easily. 11 | 12 | 13 | .. _customising-overrides: 14 | 15 | Overrides 16 | ========= 17 | 18 | On every page load, Paperless looks for two files in your media root directory 19 | (the directory defined by your ``PAPERLESS_MEDIADIR`` configuration variable or 20 | the default, ``/media/``) for two files: 21 | 22 | * ``overrides.css`` 23 | * ``overrides.js`` 24 | 25 | If it finds either or both of those files, they'll be loaded into the page: the 26 | CSS in the ````, and the Javascript stuffed into the last line of the 27 | ````. 28 | 29 | 30 | .. _customising-overrides-note: 31 | 32 | An important note about customisation 33 | ------------------------------------- 34 | 35 | Any changes you make to the site with your CSS or Javascript are likely to 36 | depend on the structure of the current HTML and/or the existing CSS rules. For 37 | the most part it's safe to assume that these bits won't change, but *sometimes 38 | they do* as features are added or bugs are fixed. 39 | 40 | If you make a change that you think others would appreciate though, submit it 41 | as a pull request and maybe we can find a way to work it into the project by 42 | default! -------------------------------------------------------------------------------- /docs/examples/lxc/paperless.conf: -------------------------------------------------------------------------------- 1 | 2 | ServerName paperless.lan 3 | 4 | Alias /static/ /home/paperless/paperless/static/ 5 | 6 | Require all granted 7 | 8 | 9 | WSGIScriptAlias / /home/paperless/paperless/src/paperless/wsgi.py 10 | WSGIDaemonProcess paperless.lan user=paperless group=paperless threads=5 python-path=/home/paperless/paperless/src 11 | WSGIProcessGroup paperless.lan 12 | 13 | 14 | 15 | Require all granted 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. _index: 2 | 3 | Paperless 4 | ========= 5 | 6 | Paperless is a simple Django application running in two parts: 7 | a :ref:`consumer ` (the thing that does the indexing) and 8 | the :ref:`webserver ` (the part that lets you search & 9 | download already-indexed documents). If you want to learn more about its 10 | functions keep on reading after the installation section. 11 | 12 | 13 | .. _index-why-this-exists: 14 | 15 | Why This Exists 16 | =============== 17 | 18 | Paper is a nightmare. Environmental issues aside, there's no excuse for it in 19 | the 21st century. It takes up space, collects dust, doesn't support any form 20 | of a search feature, indexing is tedious, it's heavy and prone to damage & 21 | loss. 22 | 23 | I wrote this to make "going paperless" easier. I do not have to worry about 24 | finding stuff again. I feed documents right from the post box into the scanner 25 | and then shred them. Perhaps you might find it useful too. 26 | 27 | 28 | 29 | 30 | Contents 31 | ======== 32 | 33 | .. toctree:: 34 | :maxdepth: 2 35 | 36 | requirements 37 | setup 38 | consumption 39 | api 40 | utilities 41 | guesswork 42 | migrating 43 | customising 44 | extending 45 | troubleshooting 46 | contributing 47 | scanners 48 | screenshots 49 | changelog 50 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-paperless-project/paperless/9b0063c9731f7c5f65b1852cb8caff97f5e40ba4/docs/requirements.txt -------------------------------------------------------------------------------- /docs/scanners.rst: -------------------------------------------------------------------------------- 1 | .. _scanners: 2 | 3 | Scanner Recommendations 4 | ======================= 5 | 6 | As Paperless operates by watching a folder for new files, doesn't care what 7 | scanner you use, but sometimes finding a scanner that will write to an FTP, 8 | NFS, or SMB server can be difficult. This page is here to help you find one 9 | that works right for you based on recommentations from other Paperless users. 10 | 11 | Physical scanners 12 | ----------------- 13 | 14 | +---------+----------------+-----+-----+-----+----------------+ 15 | | Brand | Model | Supports | Recommended By | 16 | +---------+----------------+-----+-----+-----+----------------+ 17 | | | | FTP | NFS | SMB | | 18 | +=========+================+=====+=====+=====+================+ 19 | | Brother | `ADS-1500W`_ | yes | no | yes | `danielquinn`_ | 20 | +---------+----------------+-----+-----+-----+----------------+ 21 | | Brother | `MFC-J6930DW`_ | yes | | | `ayounggun`_ | 22 | +---------+----------------+-----+-----+-----+----------------+ 23 | | Brother | `MFC-J5910DW`_ | yes | | | `bmsleight`_ | 24 | +---------+----------------+-----+-----+-----+----------------+ 25 | | Brother | `MFC-9142CDN`_ | yes | | yes | `REOLDEV`_ | 26 | +---------+----------------+-----+-----+-----+----------------+ 27 | | Fujitsu | `ix500`_ | yes | | yes | `eonist`_ | 28 | +---------+----------------+-----+-----+-----+----------------+ 29 | 30 | .. _ADS-1500W: https://www.brother.ca/en/p/ads1500w 31 | .. _MFC-J6930DW: https://www.brother.ca/en/p/MFCJ6930DW 32 | .. _MFC-J5910DW: https://www.brother.co.uk/printers/inkjet-printers/mfcj5910dw 33 | .. _MFC-9142CDN: https://www.brother.co.uk/printers/laser-printers/mfc9140cdn 34 | .. _ix500: http://www.fujitsu.com/us/products/computing/peripheral/scanners/scansnap/ix500/ 35 | 36 | .. _danielquinn: https://github.com/danielquinn 37 | .. _ayounggun: https://github.com/ayounggun 38 | .. _bmsleight: https://github.com/bmsleight 39 | .. _eonist: https://github.com/eonist 40 | .. _REOLDEV: https://github.com/REOLDEV 41 | 42 | Mobile phone software 43 | ----------------------- 44 | 45 | You can use your phone to "scan" documents. The regular camera app will work, but may have too low contrast for OCR to work well. Apps specifically for scanning are recommended. 46 | 47 | +-------------------+----------------+-----+-----+-----+-------+--------+----------------+ 48 | | Name | OS | Supports | Recommended By | 49 | +-------------------+----------------+-----+-----+-----+-------+--------+----------------+ 50 | | | | FTP | NFS | SMB | Email | WebDav | | 51 | +===================+================+=====+=====+=====+=======+========+================+ 52 | | `Genius Scan`_ | Android | yes | no | yes | yes | yes | `hannahswain`_ | 53 | +-------------------+----------------+-----+-----+-----+-------+--------+----------------+ 54 | 55 | 56 | .. _Genius Scan: https://play.google.com/store/apps/details?id=com.thegrizzlylabs.geniusscan.free 57 | 58 | .. _hannahswain: https://github.com/hannahswain 59 | -------------------------------------------------------------------------------- /docs/screenshots.rst: -------------------------------------------------------------------------------- 1 | .. _screenshots: 2 | 3 | Screenshots 4 | =========== 5 | 6 | Once everything is set-up login to paperless using the web front-end 7 | 8 | .. image:: ./_static/Screenshot_first_run_login.png 9 | 10 | Nice clean interface 11 | 12 | .. image:: ./_static/Screenshot_first_logged.png 13 | 14 | Some documents loaded in via ftp or using the scanners ftp. 15 | 16 | .. image:: ./_static/Screenshot_upload_and_scanned.png 17 | -------------------------------------------------------------------------------- /management/commands/create_superuser_with_password.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.management.commands import createsuperuser 2 | from django.core.management import CommandError 3 | 4 | 5 | class Command(createsuperuser.Command): 6 | help = 'Crate a superuser, and allow password to be provided' 7 | 8 | def add_arguments(self, parser): 9 | super(Command, self).add_arguments(parser) 10 | parser.add_argument( 11 | '--password', dest='password', default=None, 12 | help='Specifies the password for the superuser.', 13 | ) 14 | parser.add_argument( 15 | '--preserve', dest='preserve', default=False, action='store_true', 16 | help='Exit normally if the user already exists.', 17 | ) 18 | 19 | def handle(self, *args, **options): 20 | password = options.get('password') 21 | username = options.get('username') 22 | database = options.get('database') 23 | 24 | if password and not username: 25 | raise CommandError("--username is required if specifying --password") 26 | 27 | if username and options.get('preserve'): 28 | exists = self.UserModel._default_manager.db_manager(database).filter(username=username).exists() 29 | if exists: 30 | self.stdout.write("User exists, exiting normally due to --preserve") 31 | return 32 | 33 | super(Command, self).handle(*args, **options) 34 | 35 | if password: 36 | user = self.UserModel._default_manager.db_manager(database).get(username=username) 37 | user.set_password(password) 38 | user.save() 39 | -------------------------------------------------------------------------------- /media/documents/originals/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-paperless-project/paperless/9b0063c9731f7c5f65b1852cb8caff97f5e40ba4/media/documents/originals/.keep -------------------------------------------------------------------------------- /media/documents/thumbnails/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-paperless-project/paperless/9b0063c9731f7c5f65b1852cb8caff97f5e40ba4/media/documents/thumbnails/.keep -------------------------------------------------------------------------------- /overrides/README.md: -------------------------------------------------------------------------------- 1 | # Customizing Paperless 2 | 3 | *See customization 4 | [documentation](https://paperless.readthedocs.io/en/latest/customising.html) 5 | for more detail!* 6 | 7 | The example `.css` and `.js` snippets in this folder can be placed into 8 | one of two files in your ``PAPERLESS_MEDIADIR`` folder: `overrides.js` or 9 | `overrides.css`. Please feel free to submit pull requests to the main 10 | repository with other examples of customizations that you think others may 11 | find useful. -------------------------------------------------------------------------------- /presentation/README.md: -------------------------------------------------------------------------------- 1 | # Presentation 2 | 3 | This presentation was written with 4 | [reaveal.js](http://lab.hakim.se/reveal-js/), and requires no special 5 | software to view. Simply open `index.html` in a browser and you're good 6 | to go. 7 | -------------------------------------------------------------------------------- /presentation/contrib/font-awesome-4.3.0/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-paperless-project/paperless/9b0063c9731f7c5f65b1852cb8caff97f5e40ba4/presentation/contrib/font-awesome-4.3.0/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /presentation/contrib/font-awesome-4.3.0/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-paperless-project/paperless/9b0063c9731f7c5f65b1852cb8caff97f5e40ba4/presentation/contrib/font-awesome-4.3.0/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /presentation/contrib/font-awesome-4.3.0/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-paperless-project/paperless/9b0063c9731f7c5f65b1852cb8caff97f5e40ba4/presentation/contrib/font-awesome-4.3.0/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /presentation/contrib/font-awesome-4.3.0/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-paperless-project/paperless/9b0063c9731f7c5f65b1852cb8caff97f5e40ba4/presentation/contrib/font-awesome-4.3.0/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /presentation/contrib/font-awesome-4.3.0/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-paperless-project/paperless/9b0063c9731f7c5f65b1852cb8caff97f5e40ba4/presentation/contrib/font-awesome-4.3.0/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /presentation/contrib/font-awesome-4.3.0/less/animated.less: -------------------------------------------------------------------------------- 1 | // Animated Icons 2 | // -------------------------- 3 | 4 | .@{fa-css-prefix}-spin { 5 | -webkit-animation: fa-spin 2s infinite linear; 6 | animation: fa-spin 2s infinite linear; 7 | } 8 | 9 | .@{fa-css-prefix}-pulse { 10 | -webkit-animation: fa-spin 1s infinite steps(8); 11 | animation: fa-spin 1s infinite steps(8); 12 | } 13 | 14 | @-webkit-keyframes fa-spin { 15 | 0% { 16 | -webkit-transform: rotate(0deg); 17 | transform: rotate(0deg); 18 | } 19 | 100% { 20 | -webkit-transform: rotate(359deg); 21 | transform: rotate(359deg); 22 | } 23 | } 24 | 25 | @keyframes fa-spin { 26 | 0% { 27 | -webkit-transform: rotate(0deg); 28 | transform: rotate(0deg); 29 | } 30 | 100% { 31 | -webkit-transform: rotate(359deg); 32 | transform: rotate(359deg); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /presentation/contrib/font-awesome-4.3.0/less/bordered-pulled.less: -------------------------------------------------------------------------------- 1 | // Bordered & Pulled 2 | // ------------------------- 3 | 4 | .@{fa-css-prefix}-border { 5 | padding: .2em .25em .15em; 6 | border: solid .08em @fa-border-color; 7 | border-radius: .1em; 8 | } 9 | 10 | .pull-right { float: right; } 11 | .pull-left { float: left; } 12 | 13 | .@{fa-css-prefix} { 14 | &.pull-left { margin-right: .3em; } 15 | &.pull-right { margin-left: .3em; } 16 | } 17 | -------------------------------------------------------------------------------- /presentation/contrib/font-awesome-4.3.0/less/core.less: -------------------------------------------------------------------------------- 1 | // Base Class Definition 2 | // ------------------------- 3 | 4 | .@{fa-css-prefix} { 5 | display: inline-block; 6 | font: normal normal normal @fa-font-size-base/1 FontAwesome; // shortening font declaration 7 | font-size: inherit; // can't have font-size inherit on line above, so need to override 8 | text-rendering: auto; // optimizelegibility throws things off #1094 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | transform: translate(0, 0); // ensures no half-pixel rendering in firefox 12 | 13 | } 14 | -------------------------------------------------------------------------------- /presentation/contrib/font-awesome-4.3.0/less/fixed-width.less: -------------------------------------------------------------------------------- 1 | // Fixed Width Icons 2 | // ------------------------- 3 | .@{fa-css-prefix}-fw { 4 | width: (18em / 14); 5 | text-align: center; 6 | } 7 | -------------------------------------------------------------------------------- /presentation/contrib/font-awesome-4.3.0/less/font-awesome.less: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome 4.3.0 by @davegandy - http://fontawesome.io - @fontawesome 3 | * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) 4 | */ 5 | 6 | @import "variables.less"; 7 | @import "mixins.less"; 8 | @import "path.less"; 9 | @import "core.less"; 10 | @import "larger.less"; 11 | @import "fixed-width.less"; 12 | @import "list.less"; 13 | @import "bordered-pulled.less"; 14 | @import "animated.less"; 15 | @import "rotated-flipped.less"; 16 | @import "stacked.less"; 17 | @import "icons.less"; 18 | -------------------------------------------------------------------------------- /presentation/contrib/font-awesome-4.3.0/less/larger.less: -------------------------------------------------------------------------------- 1 | // Icon Sizes 2 | // ------------------------- 3 | 4 | /* makes the font 33% larger relative to the icon container */ 5 | .@{fa-css-prefix}-lg { 6 | font-size: (4em / 3); 7 | line-height: (3em / 4); 8 | vertical-align: -15%; 9 | } 10 | .@{fa-css-prefix}-2x { font-size: 2em; } 11 | .@{fa-css-prefix}-3x { font-size: 3em; } 12 | .@{fa-css-prefix}-4x { font-size: 4em; } 13 | .@{fa-css-prefix}-5x { font-size: 5em; } 14 | -------------------------------------------------------------------------------- /presentation/contrib/font-awesome-4.3.0/less/list.less: -------------------------------------------------------------------------------- 1 | // List Icons 2 | // ------------------------- 3 | 4 | .@{fa-css-prefix}-ul { 5 | padding-left: 0; 6 | margin-left: @fa-li-width; 7 | list-style-type: none; 8 | > li { position: relative; } 9 | } 10 | .@{fa-css-prefix}-li { 11 | position: absolute; 12 | left: -@fa-li-width; 13 | width: @fa-li-width; 14 | top: (2em / 14); 15 | text-align: center; 16 | &.@{fa-css-prefix}-lg { 17 | left: (-@fa-li-width + (4em / 14)); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /presentation/contrib/font-awesome-4.3.0/less/mixins.less: -------------------------------------------------------------------------------- 1 | // Mixins 2 | // -------------------------- 3 | 4 | .fa-icon() { 5 | display: inline-block; 6 | font: normal normal normal @fa-font-size-base/1 FontAwesome; // shortening font declaration 7 | font-size: inherit; // can't have font-size inherit on line above, so need to override 8 | text-rendering: auto; // optimizelegibility throws things off #1094 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | transform: translate(0, 0); // ensures no half-pixel rendering in firefox 12 | 13 | } 14 | 15 | .fa-icon-rotate(@degrees, @rotation) { 16 | filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=@rotation); 17 | -webkit-transform: rotate(@degrees); 18 | -ms-transform: rotate(@degrees); 19 | transform: rotate(@degrees); 20 | } 21 | 22 | .fa-icon-flip(@horiz, @vert, @rotation) { 23 | filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=@rotation, mirror=1); 24 | -webkit-transform: scale(@horiz, @vert); 25 | -ms-transform: scale(@horiz, @vert); 26 | transform: scale(@horiz, @vert); 27 | } 28 | -------------------------------------------------------------------------------- /presentation/contrib/font-awesome-4.3.0/less/path.less: -------------------------------------------------------------------------------- 1 | /* FONT PATH 2 | * -------------------------- */ 3 | 4 | @font-face { 5 | font-family: 'FontAwesome'; 6 | src: url('@{fa-font-path}/fontawesome-webfont.eot?v=@{fa-version}'); 7 | src: url('@{fa-font-path}/fontawesome-webfont.eot?#iefix&v=@{fa-version}') format('embedded-opentype'), 8 | url('@{fa-font-path}/fontawesome-webfont.woff2?v=@{fa-version}') format('woff2'), 9 | url('@{fa-font-path}/fontawesome-webfont.woff?v=@{fa-version}') format('woff'), 10 | url('@{fa-font-path}/fontawesome-webfont.ttf?v=@{fa-version}') format('truetype'), 11 | url('@{fa-font-path}/fontawesome-webfont.svg?v=@{fa-version}#fontawesomeregular') format('svg'); 12 | // src: url('@{fa-font-path}/FontAwesome.otf') format('opentype'); // used when developing fonts 13 | font-weight: normal; 14 | font-style: normal; 15 | } 16 | -------------------------------------------------------------------------------- /presentation/contrib/font-awesome-4.3.0/less/rotated-flipped.less: -------------------------------------------------------------------------------- 1 | // Rotated & Flipped Icons 2 | // ------------------------- 3 | 4 | .@{fa-css-prefix}-rotate-90 { .fa-icon-rotate(90deg, 1); } 5 | .@{fa-css-prefix}-rotate-180 { .fa-icon-rotate(180deg, 2); } 6 | .@{fa-css-prefix}-rotate-270 { .fa-icon-rotate(270deg, 3); } 7 | 8 | .@{fa-css-prefix}-flip-horizontal { .fa-icon-flip(-1, 1, 0); } 9 | .@{fa-css-prefix}-flip-vertical { .fa-icon-flip(1, -1, 2); } 10 | 11 | // Hook for IE8-9 12 | // ------------------------- 13 | 14 | :root .@{fa-css-prefix}-rotate-90, 15 | :root .@{fa-css-prefix}-rotate-180, 16 | :root .@{fa-css-prefix}-rotate-270, 17 | :root .@{fa-css-prefix}-flip-horizontal, 18 | :root .@{fa-css-prefix}-flip-vertical { 19 | filter: none; 20 | } 21 | -------------------------------------------------------------------------------- /presentation/contrib/font-awesome-4.3.0/less/stacked.less: -------------------------------------------------------------------------------- 1 | // Stacked Icons 2 | // ------------------------- 3 | 4 | .@{fa-css-prefix}-stack { 5 | position: relative; 6 | display: inline-block; 7 | width: 2em; 8 | height: 2em; 9 | line-height: 2em; 10 | vertical-align: middle; 11 | } 12 | .@{fa-css-prefix}-stack-1x, .@{fa-css-prefix}-stack-2x { 13 | position: absolute; 14 | left: 0; 15 | width: 100%; 16 | text-align: center; 17 | } 18 | .@{fa-css-prefix}-stack-1x { line-height: inherit; } 19 | .@{fa-css-prefix}-stack-2x { font-size: 2em; } 20 | .@{fa-css-prefix}-inverse { color: @fa-inverse; } 21 | -------------------------------------------------------------------------------- /presentation/contrib/font-awesome-4.3.0/scss/_animated.scss: -------------------------------------------------------------------------------- 1 | // Spinning Icons 2 | // -------------------------- 3 | 4 | .#{$fa-css-prefix}-spin { 5 | -webkit-animation: fa-spin 2s infinite linear; 6 | animation: fa-spin 2s infinite linear; 7 | } 8 | 9 | .#{$fa-css-prefix}-pulse { 10 | -webkit-animation: fa-spin 1s infinite steps(8); 11 | animation: fa-spin 1s infinite steps(8); 12 | } 13 | 14 | @-webkit-keyframes fa-spin { 15 | 0% { 16 | -webkit-transform: rotate(0deg); 17 | transform: rotate(0deg); 18 | } 19 | 100% { 20 | -webkit-transform: rotate(359deg); 21 | transform: rotate(359deg); 22 | } 23 | } 24 | 25 | @keyframes fa-spin { 26 | 0% { 27 | -webkit-transform: rotate(0deg); 28 | transform: rotate(0deg); 29 | } 30 | 100% { 31 | -webkit-transform: rotate(359deg); 32 | transform: rotate(359deg); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /presentation/contrib/font-awesome-4.3.0/scss/_bordered-pulled.scss: -------------------------------------------------------------------------------- 1 | // Bordered & Pulled 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix}-border { 5 | padding: .2em .25em .15em; 6 | border: solid .08em $fa-border-color; 7 | border-radius: .1em; 8 | } 9 | 10 | .pull-right { float: right; } 11 | .pull-left { float: left; } 12 | 13 | .#{$fa-css-prefix} { 14 | &.pull-left { margin-right: .3em; } 15 | &.pull-right { margin-left: .3em; } 16 | } 17 | -------------------------------------------------------------------------------- /presentation/contrib/font-awesome-4.3.0/scss/_core.scss: -------------------------------------------------------------------------------- 1 | // Base Class Definition 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix} { 5 | display: inline-block; 6 | font: normal normal normal #{$fa-font-size-base}/1 FontAwesome; // shortening font declaration 7 | font-size: inherit; // can't have font-size inherit on line above, so need to override 8 | text-rendering: auto; // optimizelegibility throws things off #1094 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | transform: translate(0, 0); // ensures no half-pixel rendering in firefox 12 | 13 | } 14 | -------------------------------------------------------------------------------- /presentation/contrib/font-awesome-4.3.0/scss/_fixed-width.scss: -------------------------------------------------------------------------------- 1 | // Fixed Width Icons 2 | // ------------------------- 3 | .#{$fa-css-prefix}-fw { 4 | width: (18em / 14); 5 | text-align: center; 6 | } 7 | -------------------------------------------------------------------------------- /presentation/contrib/font-awesome-4.3.0/scss/_larger.scss: -------------------------------------------------------------------------------- 1 | // Icon Sizes 2 | // ------------------------- 3 | 4 | /* makes the font 33% larger relative to the icon container */ 5 | .#{$fa-css-prefix}-lg { 6 | font-size: (4em / 3); 7 | line-height: (3em / 4); 8 | vertical-align: -15%; 9 | } 10 | .#{$fa-css-prefix}-2x { font-size: 2em; } 11 | .#{$fa-css-prefix}-3x { font-size: 3em; } 12 | .#{$fa-css-prefix}-4x { font-size: 4em; } 13 | .#{$fa-css-prefix}-5x { font-size: 5em; } 14 | -------------------------------------------------------------------------------- /presentation/contrib/font-awesome-4.3.0/scss/_list.scss: -------------------------------------------------------------------------------- 1 | // List Icons 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix}-ul { 5 | padding-left: 0; 6 | margin-left: $fa-li-width; 7 | list-style-type: none; 8 | > li { position: relative; } 9 | } 10 | .#{$fa-css-prefix}-li { 11 | position: absolute; 12 | left: -$fa-li-width; 13 | width: $fa-li-width; 14 | top: (2em / 14); 15 | text-align: center; 16 | &.#{$fa-css-prefix}-lg { 17 | left: -$fa-li-width + (4em / 14); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /presentation/contrib/font-awesome-4.3.0/scss/_mixins.scss: -------------------------------------------------------------------------------- 1 | // Mixins 2 | // -------------------------- 3 | 4 | @mixin fa-icon() { 5 | display: inline-block; 6 | font: normal normal normal #{$fa-font-size-base}/1 FontAwesome; // shortening font declaration 7 | font-size: inherit; // can't have font-size inherit on line above, so need to override 8 | text-rendering: auto; // optimizelegibility throws things off #1094 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | transform: translate(0, 0); // ensures no half-pixel rendering in firefox 12 | 13 | } 14 | 15 | @mixin fa-icon-rotate($degrees, $rotation) { 16 | filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation}); 17 | -webkit-transform: rotate($degrees); 18 | -ms-transform: rotate($degrees); 19 | transform: rotate($degrees); 20 | } 21 | 22 | @mixin fa-icon-flip($horiz, $vert, $rotation) { 23 | filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation}); 24 | -webkit-transform: scale($horiz, $vert); 25 | -ms-transform: scale($horiz, $vert); 26 | transform: scale($horiz, $vert); 27 | } 28 | -------------------------------------------------------------------------------- /presentation/contrib/font-awesome-4.3.0/scss/_path.scss: -------------------------------------------------------------------------------- 1 | /* FONT PATH 2 | * -------------------------- */ 3 | 4 | @font-face { 5 | font-family: 'FontAwesome'; 6 | src: url('#{$fa-font-path}/fontawesome-webfont.eot?v=#{$fa-version}'); 7 | src: url('#{$fa-font-path}/fontawesome-webfont.eot?#iefix&v=#{$fa-version}') format('embedded-opentype'), 8 | url('#{$fa-font-path}/fontawesome-webfont.woff2?v=#{$fa-version}') format('woff2'), 9 | url('#{$fa-font-path}/fontawesome-webfont.woff?v=#{$fa-version}') format('woff'), 10 | url('#{$fa-font-path}/fontawesome-webfont.ttf?v=#{$fa-version}') format('truetype'), 11 | url('#{$fa-font-path}/fontawesome-webfont.svg?v=#{$fa-version}#fontawesomeregular') format('svg'); 12 | // src: url('#{$fa-font-path}/FontAwesome.otf') format('opentype'); // used when developing fonts 13 | font-weight: normal; 14 | font-style: normal; 15 | } 16 | -------------------------------------------------------------------------------- /presentation/contrib/font-awesome-4.3.0/scss/_rotated-flipped.scss: -------------------------------------------------------------------------------- 1 | // Rotated & Flipped Icons 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix}-rotate-90 { @include fa-icon-rotate(90deg, 1); } 5 | .#{$fa-css-prefix}-rotate-180 { @include fa-icon-rotate(180deg, 2); } 6 | .#{$fa-css-prefix}-rotate-270 { @include fa-icon-rotate(270deg, 3); } 7 | 8 | .#{$fa-css-prefix}-flip-horizontal { @include fa-icon-flip(-1, 1, 0); } 9 | .#{$fa-css-prefix}-flip-vertical { @include fa-icon-flip(1, -1, 2); } 10 | 11 | // Hook for IE8-9 12 | // ------------------------- 13 | 14 | :root .#{$fa-css-prefix}-rotate-90, 15 | :root .#{$fa-css-prefix}-rotate-180, 16 | :root .#{$fa-css-prefix}-rotate-270, 17 | :root .#{$fa-css-prefix}-flip-horizontal, 18 | :root .#{$fa-css-prefix}-flip-vertical { 19 | filter: none; 20 | } 21 | -------------------------------------------------------------------------------- /presentation/contrib/font-awesome-4.3.0/scss/_stacked.scss: -------------------------------------------------------------------------------- 1 | // Stacked Icons 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix}-stack { 5 | position: relative; 6 | display: inline-block; 7 | width: 2em; 8 | height: 2em; 9 | line-height: 2em; 10 | vertical-align: middle; 11 | } 12 | .#{$fa-css-prefix}-stack-1x, .#{$fa-css-prefix}-stack-2x { 13 | position: absolute; 14 | left: 0; 15 | width: 100%; 16 | text-align: center; 17 | } 18 | .#{$fa-css-prefix}-stack-1x { line-height: inherit; } 19 | .#{$fa-css-prefix}-stack-2x { font-size: 2em; } 20 | .#{$fa-css-prefix}-inverse { color: $fa-inverse; } 21 | -------------------------------------------------------------------------------- /presentation/contrib/font-awesome-4.3.0/scss/font-awesome.scss: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome 4.3.0 by @davegandy - http://fontawesome.io - @fontawesome 3 | * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) 4 | */ 5 | 6 | @import "variables"; 7 | @import "mixins"; 8 | @import "path"; 9 | @import "core"; 10 | @import "larger"; 11 | @import "fixed-width"; 12 | @import "list"; 13 | @import "bordered-pulled"; 14 | @import "animated"; 15 | @import "rotated-flipped"; 16 | @import "stacked"; 17 | @import "icons"; 18 | -------------------------------------------------------------------------------- /presentation/contrib/google/css/lato.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Lato'; 3 | font-style: normal; 4 | font-weight: 400; 5 | src: local('Lato Regular'), local('Lato-Regular'), url(../fonts/v0SdcGFAl2aezM9Vq_aFTQ.ttf) format('truetype'); 6 | } 7 | @font-face { 8 | font-family: 'Lato'; 9 | font-style: normal; 10 | font-weight: 700; 11 | src: local('Lato Bold'), local('Lato-Bold'), url(../fonts/DvlFBScY1r-FMtZSYIYoYw.ttf) format('truetype'); 12 | } 13 | @font-face { 14 | font-family: 'Lato'; 15 | font-style: italic; 16 | font-weight: 400; 17 | src: local('Lato Italic'), local('Lato-Italic'), url(../fonts/LqowQDslGv4DmUBAfWa2Vw.ttf) format('truetype'); 18 | } 19 | @font-face { 20 | font-family: 'Lato'; 21 | font-style: italic; 22 | font-weight: 700; 23 | src: local('Lato Bold Italic'), local('Lato-BoldItalic'), url(../fonts/HkF_qI1x_noxlxhrhMQYEKCWcynf_cDxXwCLxiixG1c.ttf) format('truetype'); 24 | } 25 | -------------------------------------------------------------------------------- /presentation/contrib/google/fonts/DvlFBScY1r-FMtZSYIYoYw.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-paperless-project/paperless/9b0063c9731f7c5f65b1852cb8caff97f5e40ba4/presentation/contrib/google/fonts/DvlFBScY1r-FMtZSYIYoYw.ttf -------------------------------------------------------------------------------- /presentation/contrib/google/fonts/HkF_qI1x_noxlxhrhMQYEKCWcynf_cDxXwCLxiixG1c.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-paperless-project/paperless/9b0063c9731f7c5f65b1852cb8caff97f5e40ba4/presentation/contrib/google/fonts/HkF_qI1x_noxlxhrhMQYEKCWcynf_cDxXwCLxiixG1c.ttf -------------------------------------------------------------------------------- /presentation/contrib/google/fonts/LqowQDslGv4DmUBAfWa2Vw.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-paperless-project/paperless/9b0063c9731f7c5f65b1852cb8caff97f5e40ba4/presentation/contrib/google/fonts/LqowQDslGv4DmUBAfWa2Vw.ttf -------------------------------------------------------------------------------- /presentation/contrib/google/fonts/v0SdcGFAl2aezM9Vq_aFTQ.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-paperless-project/paperless/9b0063c9731f7c5f65b1852cb8caff97f5e40ba4/presentation/contrib/google/fonts/v0SdcGFAl2aezM9Vq_aFTQ.ttf -------------------------------------------------------------------------------- /presentation/css/theme/README.md: -------------------------------------------------------------------------------- 1 | ## Dependencies 2 | 3 | Themes are written using Sass to keep things modular and reduce the need for repeated selectors across files. Make sure that you have the reveal.js development environment including the Grunt dependencies installed before proceding: https://github.com/hakimel/reveal.js#full-setup 4 | 5 | You also need to install Ruby and then Sass (with `gem install sass`). 6 | 7 | ## Creating a Theme 8 | 9 | To create your own theme, start by duplicating any ```.scss``` file in [/css/theme/source](https://github.com/hakimel/reveal.js/blob/master/css/theme/source) and adding it to the compilation list in the [Gruntfile](https://github.com/hakimel/reveal.js/blob/master/Gruntfile.js). 10 | 11 | Each theme file does four things in the following order: 12 | 13 | 1. **Include [/css/theme/template/mixins.scss](https://github.com/hakimel/reveal.js/blob/master/css/theme/template/mixins.scss)** 14 | Shared utility functions. 15 | 16 | 2. **Include [/css/theme/template/settings.scss](https://github.com/hakimel/reveal.js/blob/master/css/theme/template/settings.scss)** 17 | Declares a set of custom variables that the template file (step 4) expects. Can be overridden in step 3. 18 | 19 | 3. **Override** 20 | This is where you override the default theme. Either by specifying variables (see [settings.scss](https://github.com/hakimel/reveal.js/blob/master/css/theme/template/settings.scss) for reference) or by adding full selectors with hardcoded styles. 21 | 22 | 4. **Include [/css/theme/template/theme.scss](https://github.com/hakimel/reveal.js/blob/master/css/theme/template/theme.scss)** 23 | The template theme file which will generate final CSS output based on the currently defined variables. 24 | 25 | When you are done, run `grunt css-themes` to compile the Sass file to CSS and you are ready to use your new theme. 26 | -------------------------------------------------------------------------------- /presentation/css/theme/source/beige.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Beige theme for reveal.js. 3 | * 4 | * Copyright (C) 2011-2012 Hakim El Hattab, http://hakim.se 5 | */ 6 | 7 | 8 | // Default mixins and settings ----------------- 9 | @import "../template/mixins"; 10 | @import "../template/settings"; 11 | // --------------------------------------------- 12 | 13 | 14 | 15 | // Include theme-specific fonts 16 | @import url(../../lib/font/league-gothic/league-gothic.css); 17 | @import url(https://fonts.googleapis.com/css?family=Lato:400,700,400italic,700italic); 18 | 19 | 20 | // Override theme settings (see ../template/settings.scss) 21 | $mainColor: #333; 22 | $headingColor: #333; 23 | $headingTextShadow: none; 24 | $backgroundColor: #f7f3de; 25 | $linkColor: #8b743d; 26 | $linkColorHover: lighten( $linkColor, 20% ); 27 | $selectionBackgroundColor: rgba(79, 64, 28, 0.99); 28 | $heading1TextShadow: 0 1px 0 #ccc, 0 2px 0 #c9c9c9, 0 3px 0 #bbb, 0 4px 0 #b9b9b9, 0 5px 0 #aaa, 0 6px 1px rgba(0,0,0,.1), 0 0 5px rgba(0,0,0,.1), 0 1px 3px rgba(0,0,0,.3), 0 3px 5px rgba(0,0,0,.2), 0 5px 10px rgba(0,0,0,.25), 0 20px 20px rgba(0,0,0,.15); 29 | 30 | // Background generator 31 | @mixin bodyBackground() { 32 | @include radial-gradient( rgba(247,242,211,1), rgba(255,255,255,1) ); 33 | } 34 | 35 | 36 | 37 | // Theme template ------------------------------ 38 | @import "../template/theme"; 39 | // --------------------------------------------- -------------------------------------------------------------------------------- /presentation/css/theme/source/black.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Black theme for reveal.js. This is the opposite of the 'white' theme. 3 | * 4 | * Copyright (C) 2015 Hakim El Hattab, http://hakim.se 5 | */ 6 | 7 | 8 | // Default mixins and settings ----------------- 9 | @import "../template/mixins"; 10 | @import "../template/settings"; 11 | // --------------------------------------------- 12 | 13 | 14 | // Include theme-specific fonts 15 | @import url(../../lib/font/source-sans-pro/source-sans-pro.css); 16 | 17 | 18 | // Override theme settings (see ../template/settings.scss) 19 | $backgroundColor: #222; 20 | 21 | $mainColor: #fff; 22 | $headingColor: #fff; 23 | 24 | $mainFontSize: 38px; 25 | $mainFont: 'Source Sans Pro', Helvetica, sans-serif; 26 | $headingFont: 'Source Sans Pro', Helvetica, sans-serif; 27 | $headingTextShadow: none; 28 | $headingLetterSpacing: normal; 29 | $headingTextTransform: uppercase; 30 | $headingFontWeight: 600; 31 | $linkColor: #42affa; 32 | $linkColorHover: lighten( $linkColor, 15% ); 33 | $selectionBackgroundColor: lighten( $linkColor, 25% ); 34 | 35 | $heading1Size: 2.5em; 36 | $heading2Size: 1.6em; 37 | $heading3Size: 1.3em; 38 | $heading4Size: 1.0em; 39 | 40 | section.has-light-background { 41 | &, h1, h2, h3, h4, h5, h6 { 42 | color: #222; 43 | } 44 | } 45 | 46 | 47 | // Theme template ------------------------------ 48 | @import "../template/theme"; 49 | // --------------------------------------------- -------------------------------------------------------------------------------- /presentation/css/theme/source/blood.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Blood theme for reveal.js 3 | * Author: Walther http://github.com/Walther 4 | * 5 | * Designed to be used with highlight.js theme 6 | * "monokai_sublime.css" available from 7 | * https://github.com/isagalaev/highlight.js/ 8 | * 9 | * For other themes, change $codeBackground accordingly. 10 | * 11 | */ 12 | 13 | // Default mixins and settings ----------------- 14 | @import "../template/mixins"; 15 | @import "../template/settings"; 16 | // --------------------------------------------- 17 | 18 | // Include theme-specific fonts 19 | 20 | @import url(https://fonts.googleapis.com/css?family=Ubuntu:300,700,300italic,700italic); 21 | 22 | // Colors used in the theme 23 | $blood: #a23; 24 | $coal: #222; 25 | $codeBackground: #23241f; 26 | 27 | // Main text 28 | $mainFont: Ubuntu, 'sans-serif'; 29 | $mainFontSize: 36px; 30 | $mainColor: #eee; 31 | 32 | // Headings 33 | $headingFont: Ubuntu, 'sans-serif'; 34 | $headingTextShadow: 2px 2px 2px $coal; 35 | 36 | // h1 shadow, borrowed humbly from 37 | // (c) Default theme by Hakim El Hattab 38 | $heading1TextShadow: 0 1px 0 #ccc, 0 2px 0 #c9c9c9, 0 3px 0 #bbb, 0 4px 0 #b9b9b9, 0 5px 0 #aaa, 0 6px 1px rgba(0,0,0,.1), 0 0 5px rgba(0,0,0,.1), 0 1px 3px rgba(0,0,0,.3), 0 3px 5px rgba(0,0,0,.2), 0 5px 10px rgba(0,0,0,.25), 0 20px 20px rgba(0,0,0,.15); 39 | 40 | // Links 41 | $linkColor: $blood; 42 | $linkColorHover: lighten( $linkColor, 20% ); 43 | 44 | // Text selection 45 | $selectionBackgroundColor: $blood; 46 | $selectionColor: #fff; 47 | 48 | // Background generator 49 | @mixin bodyBackground() { 50 | @include radial-gradient( $coal, lighten( $coal, 25% ) ); 51 | } 52 | 53 | // Theme template ------------------------------ 54 | @import "../template/theme"; 55 | // --------------------------------------------- 56 | 57 | // some overrides after theme template import 58 | 59 | .reveal p { 60 | font-weight: 300; 61 | text-shadow: 1px 1px $coal; 62 | } 63 | 64 | .reveal h1, 65 | .reveal h2, 66 | .reveal h3, 67 | .reveal h4, 68 | .reveal h5, 69 | .reveal h6 { 70 | font-weight: 700; 71 | } 72 | 73 | .reveal a, 74 | .reveal a:hover { 75 | text-shadow: 2px 2px 2px #000; 76 | } 77 | 78 | .reveal small a, 79 | .reveal small a:hover { 80 | text-shadow: 1px 1px 1px #000; 81 | } 82 | 83 | .reveal p code { 84 | background-color: $codeBackground; 85 | display: inline-block; 86 | border-radius: 7px; 87 | } 88 | 89 | .reveal small code { 90 | vertical-align: baseline; 91 | } -------------------------------------------------------------------------------- /presentation/css/theme/source/league.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * League theme for reveal.js. 3 | * 4 | * This was the default theme pre-3.0.0. 5 | * 6 | * Copyright (C) 2011-2012 Hakim El Hattab, http://hakim.se 7 | */ 8 | 9 | 10 | // Default mixins and settings ----------------- 11 | @import "../template/mixins"; 12 | @import "../template/settings"; 13 | // --------------------------------------------- 14 | 15 | 16 | 17 | // Include theme-specific fonts 18 | @import url(../../lib/font/league-gothic/league-gothic.css); 19 | @import url(https://fonts.googleapis.com/css?family=Lato:400,700,400italic,700italic); 20 | 21 | // Override theme settings (see ../template/settings.scss) 22 | $headingTextShadow: 0px 0px 6px rgba(0,0,0,0.2); 23 | $heading1TextShadow: 0 1px 0 #ccc, 0 2px 0 #c9c9c9, 0 3px 0 #bbb, 0 4px 0 #b9b9b9, 0 5px 0 #aaa, 0 6px 1px rgba(0,0,0,.1), 0 0 5px rgba(0,0,0,.1), 0 1px 3px rgba(0,0,0,.3), 0 3px 5px rgba(0,0,0,.2), 0 5px 10px rgba(0,0,0,.25), 0 20px 20px rgba(0,0,0,.15); 24 | 25 | // Background generator 26 | @mixin bodyBackground() { 27 | @include radial-gradient( rgba(28,30,32,1), rgba(85,90,95,1) ); 28 | } 29 | 30 | 31 | 32 | // Theme template ------------------------------ 33 | @import "../template/theme"; 34 | // --------------------------------------------- -------------------------------------------------------------------------------- /presentation/css/theme/source/moon.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Solarized Dark theme for reveal.js. 3 | * Author: Achim Staebler 4 | */ 5 | 6 | 7 | // Default mixins and settings ----------------- 8 | @import "../template/mixins"; 9 | @import "../template/settings"; 10 | // --------------------------------------------- 11 | 12 | 13 | 14 | // Include theme-specific fonts 15 | @import url(../../lib/font/league-gothic/league-gothic.css); 16 | @import url(https://fonts.googleapis.com/css?family=Lato:400,700,400italic,700italic); 17 | 18 | /** 19 | * Solarized colors by Ethan Schoonover 20 | */ 21 | html * { 22 | color-profile: sRGB; 23 | rendering-intent: auto; 24 | } 25 | 26 | // Solarized colors 27 | $base03: #002b36; 28 | $base02: #073642; 29 | $base01: #586e75; 30 | $base00: #657b83; 31 | $base0: #839496; 32 | $base1: #93a1a1; 33 | $base2: #eee8d5; 34 | $base3: #fdf6e3; 35 | $yellow: #b58900; 36 | $orange: #cb4b16; 37 | $red: #dc322f; 38 | $magenta: #d33682; 39 | $violet: #6c71c4; 40 | $blue: #268bd2; 41 | $cyan: #2aa198; 42 | $green: #859900; 43 | 44 | // Override theme settings (see ../template/settings.scss) 45 | $mainColor: $base1; 46 | $headingColor: $base2; 47 | $headingTextShadow: none; 48 | $backgroundColor: $base03; 49 | $linkColor: $blue; 50 | $linkColorHover: lighten( $linkColor, 20% ); 51 | $selectionBackgroundColor: $magenta; 52 | 53 | 54 | 55 | // Theme template ------------------------------ 56 | @import "../template/theme"; 57 | // --------------------------------------------- 58 | -------------------------------------------------------------------------------- /presentation/css/theme/source/night.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Black theme for reveal.js. 3 | * 4 | * Copyright (C) 2011-2012 Hakim El Hattab, http://hakim.se 5 | */ 6 | 7 | 8 | // Default mixins and settings ----------------- 9 | @import "../template/mixins"; 10 | @import "../template/settings"; 11 | // --------------------------------------------- 12 | 13 | 14 | // Include theme-specific fonts 15 | @import url(https://fonts.googleapis.com/css?family=Montserrat:700); 16 | @import url(https://fonts.googleapis.com/css?family=Open+Sans:400,700,400italic,700italic); 17 | 18 | 19 | // Override theme settings (see ../template/settings.scss) 20 | $backgroundColor: #111; 21 | 22 | $mainFont: 'Open Sans', sans-serif; 23 | $linkColor: #e7ad52; 24 | $linkColorHover: lighten( $linkColor, 20% ); 25 | $headingFont: 'Montserrat', Impact, sans-serif; 26 | $headingTextShadow: none; 27 | $headingLetterSpacing: -0.03em; 28 | $headingTextTransform: none; 29 | $selectionBackgroundColor: #e7ad52; 30 | $mainFontSize: 30px; 31 | 32 | 33 | // Theme template ------------------------------ 34 | @import "../template/theme"; 35 | // --------------------------------------------- -------------------------------------------------------------------------------- /presentation/css/theme/source/serif.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * A simple theme for reveal.js presentations, similar 3 | * to the default theme. The accent color is brown. 4 | * 5 | * This theme is Copyright (C) 2012-2013 Owen Versteeg, http://owenversteeg.com - it is MIT licensed. 6 | */ 7 | 8 | 9 | // Default mixins and settings ----------------- 10 | @import "../template/mixins"; 11 | @import "../template/settings"; 12 | // --------------------------------------------- 13 | 14 | 15 | 16 | // Override theme settings (see ../template/settings.scss) 17 | $mainFont: 'Palatino Linotype', 'Book Antiqua', Palatino, FreeSerif, serif; 18 | $mainColor: #000; 19 | $headingFont: 'Palatino Linotype', 'Book Antiqua', Palatino, FreeSerif, serif; 20 | $headingColor: #383D3D; 21 | $headingTextShadow: none; 22 | $headingTextTransform: none; 23 | $backgroundColor: #F0F1EB; 24 | $linkColor: #51483D; 25 | $linkColorHover: lighten( $linkColor, 20% ); 26 | $selectionBackgroundColor: #26351C; 27 | 28 | .reveal a { 29 | line-height: 1.3em; 30 | } 31 | 32 | 33 | // Theme template ------------------------------ 34 | @import "../template/theme"; 35 | // --------------------------------------------- 36 | -------------------------------------------------------------------------------- /presentation/css/theme/source/simple.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * A simple theme for reveal.js presentations, similar 3 | * to the default theme. The accent color is darkblue. 4 | * 5 | * This theme is Copyright (C) 2012 Owen Versteeg, https://github.com/StereotypicalApps. It is MIT licensed. 6 | * reveal.js is Copyright (C) 2011-2012 Hakim El Hattab, http://hakim.se 7 | */ 8 | 9 | 10 | // Default mixins and settings ----------------- 11 | @import "../template/mixins"; 12 | @import "../template/settings"; 13 | // --------------------------------------------- 14 | 15 | 16 | 17 | // Include theme-specific fonts 18 | @import url(https://fonts.googleapis.com/css?family=News+Cycle:400,700); 19 | @import url(https://fonts.googleapis.com/css?family=Lato:400,700,400italic,700italic); 20 | 21 | 22 | // Override theme settings (see ../template/settings.scss) 23 | $mainFont: 'Lato', sans-serif; 24 | $mainColor: #000; 25 | $headingFont: 'News Cycle', Impact, sans-serif; 26 | $headingColor: #000; 27 | $headingTextShadow: none; 28 | $headingTextTransform: none; 29 | $backgroundColor: #fff; 30 | $linkColor: #00008B; 31 | $linkColorHover: lighten( $linkColor, 20% ); 32 | $selectionBackgroundColor: rgba(0, 0, 0, 0.99); 33 | 34 | 35 | 36 | // Theme template ------------------------------ 37 | @import "../template/theme"; 38 | // --------------------------------------------- -------------------------------------------------------------------------------- /presentation/css/theme/source/sky.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Sky theme for reveal.js. 3 | * 4 | * Copyright (C) 2011-2012 Hakim El Hattab, http://hakim.se 5 | */ 6 | 7 | 8 | // Default mixins and settings ----------------- 9 | @import "../template/mixins"; 10 | @import "../template/settings"; 11 | // --------------------------------------------- 12 | 13 | 14 | 15 | // Include theme-specific fonts 16 | @import url(https://fonts.googleapis.com/css?family=Quicksand:400,700,400italic,700italic); 17 | @import url(https://fonts.googleapis.com/css?family=Open+Sans:400italic,700italic,400,700); 18 | 19 | 20 | // Override theme settings (see ../template/settings.scss) 21 | $mainFont: 'Open Sans', sans-serif; 22 | $mainColor: #333; 23 | $headingFont: 'Quicksand', sans-serif; 24 | $headingColor: #333; 25 | $headingLetterSpacing: -0.08em; 26 | $headingTextShadow: none; 27 | $backgroundColor: #f7fbfc; 28 | $linkColor: #3b759e; 29 | $linkColorHover: lighten( $linkColor, 20% ); 30 | $selectionBackgroundColor: #134674; 31 | 32 | // Fix links so they are not cut off 33 | .reveal a { 34 | line-height: 1.3em; 35 | } 36 | 37 | // Background generator 38 | @mixin bodyBackground() { 39 | @include radial-gradient( #add9e4, #f7fbfc ); 40 | } 41 | 42 | 43 | 44 | // Theme template ------------------------------ 45 | @import "../template/theme"; 46 | // --------------------------------------------- 47 | -------------------------------------------------------------------------------- /presentation/css/theme/source/solarized.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Solarized Light theme for reveal.js. 3 | * Author: Achim Staebler 4 | */ 5 | 6 | 7 | // Default mixins and settings ----------------- 8 | @import "../template/mixins"; 9 | @import "../template/settings"; 10 | // --------------------------------------------- 11 | 12 | 13 | 14 | // Include theme-specific fonts 15 | @import url(../../lib/font/league-gothic/league-gothic.css); 16 | @import url(https://fonts.googleapis.com/css?family=Lato:400,700,400italic,700italic); 17 | 18 | 19 | /** 20 | * Solarized colors by Ethan Schoonover 21 | */ 22 | html * { 23 | color-profile: sRGB; 24 | rendering-intent: auto; 25 | } 26 | 27 | // Solarized colors 28 | $base03: #002b36; 29 | $base02: #073642; 30 | $base01: #586e75; 31 | $base00: #657b83; 32 | $base0: #839496; 33 | $base1: #93a1a1; 34 | $base2: #eee8d5; 35 | $base3: #fdf6e3; 36 | $yellow: #b58900; 37 | $orange: #cb4b16; 38 | $red: #dc322f; 39 | $magenta: #d33682; 40 | $violet: #6c71c4; 41 | $blue: #268bd2; 42 | $cyan: #2aa198; 43 | $green: #859900; 44 | 45 | // Override theme settings (see ../template/settings.scss) 46 | $mainColor: $base00; 47 | $headingColor: $base01; 48 | $headingTextShadow: none; 49 | $backgroundColor: $base3; 50 | $linkColor: $blue; 51 | $linkColorHover: lighten( $linkColor, 20% ); 52 | $selectionBackgroundColor: $magenta; 53 | 54 | // Background generator 55 | // @mixin bodyBackground() { 56 | // @include radial-gradient( rgba($base3,1), rgba(lighten($base3, 20%),1) ); 57 | // } 58 | 59 | 60 | 61 | // Theme template ------------------------------ 62 | @import "../template/theme"; 63 | // --------------------------------------------- 64 | -------------------------------------------------------------------------------- /presentation/css/theme/source/white.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * White theme for reveal.js. This is the opposite of the 'black' theme. 3 | * 4 | * Copyright (C) 2015 Hakim El Hattab, http://hakim.se 5 | */ 6 | 7 | 8 | // Default mixins and settings ----------------- 9 | @import "../template/mixins"; 10 | @import "../template/settings"; 11 | // --------------------------------------------- 12 | 13 | 14 | // Include theme-specific fonts 15 | @import url(../../lib/font/source-sans-pro/source-sans-pro.css); 16 | 17 | 18 | // Override theme settings (see ../template/settings.scss) 19 | $backgroundColor: #fff; 20 | 21 | $mainColor: #222; 22 | $headingColor: #222; 23 | 24 | $mainFontSize: 38px; 25 | $mainFont: 'Source Sans Pro', Helvetica, sans-serif; 26 | $headingFont: 'Source Sans Pro', Helvetica, sans-serif; 27 | $headingTextShadow: none; 28 | $headingLetterSpacing: normal; 29 | $headingTextTransform: uppercase; 30 | $headingFontWeight: 600; 31 | $linkColor: #2a76dd; 32 | $linkColorHover: lighten( $linkColor, 15% ); 33 | $selectionBackgroundColor: lighten( $linkColor, 25% ); 34 | 35 | $heading1Size: 2.5em; 36 | $heading2Size: 1.6em; 37 | $heading3Size: 1.3em; 38 | $heading4Size: 1.0em; 39 | 40 | section.has-dark-background { 41 | &, h1, h2, h3, h4, h5, h6 { 42 | color: #fff; 43 | } 44 | } 45 | 46 | 47 | // Theme template ------------------------------ 48 | @import "../template/theme"; 49 | // --------------------------------------------- -------------------------------------------------------------------------------- /presentation/css/theme/template/mixins.scss: -------------------------------------------------------------------------------- 1 | @mixin vertical-gradient( $top, $bottom ) { 2 | background: $top; 3 | background: -moz-linear-gradient( top, $top 0%, $bottom 100% ); 4 | background: -webkit-gradient( linear, left top, left bottom, color-stop(0%,$top), color-stop(100%,$bottom) ); 5 | background: -webkit-linear-gradient( top, $top 0%, $bottom 100% ); 6 | background: -o-linear-gradient( top, $top 0%, $bottom 100% ); 7 | background: -ms-linear-gradient( top, $top 0%, $bottom 100% ); 8 | background: linear-gradient( top, $top 0%, $bottom 100% ); 9 | } 10 | 11 | @mixin horizontal-gradient( $top, $bottom ) { 12 | background: $top; 13 | background: -moz-linear-gradient( left, $top 0%, $bottom 100% ); 14 | background: -webkit-gradient( linear, left top, right top, color-stop(0%,$top), color-stop(100%,$bottom) ); 15 | background: -webkit-linear-gradient( left, $top 0%, $bottom 100% ); 16 | background: -o-linear-gradient( left, $top 0%, $bottom 100% ); 17 | background: -ms-linear-gradient( left, $top 0%, $bottom 100% ); 18 | background: linear-gradient( left, $top 0%, $bottom 100% ); 19 | } 20 | 21 | @mixin radial-gradient( $outer, $inner, $type: circle ) { 22 | background: $outer; 23 | background: -moz-radial-gradient( center, $type cover, $inner 0%, $outer 100% ); 24 | background: -webkit-gradient( radial, center center, 0px, center center, 100%, color-stop(0%,$inner), color-stop(100%,$outer) ); 25 | background: -webkit-radial-gradient( center, $type cover, $inner 0%, $outer 100% ); 26 | background: -o-radial-gradient( center, $type cover, $inner 0%, $outer 100% ); 27 | background: -ms-radial-gradient( center, $type cover, $inner 0%, $outer 100% ); 28 | background: radial-gradient( center, $type cover, $inner 0%, $outer 100% ); 29 | } -------------------------------------------------------------------------------- /presentation/css/theme/template/settings.scss: -------------------------------------------------------------------------------- 1 | // Base settings for all themes that can optionally be 2 | // overridden by the super-theme 3 | 4 | // Background of the presentation 5 | $backgroundColor: #2b2b2b; 6 | 7 | // Primary/body text 8 | $mainFont: 'Lato', sans-serif; 9 | $mainFontSize: 36px; 10 | $mainColor: #eee; 11 | 12 | // Vertical spacing between blocks of text 13 | $blockMargin: 20px; 14 | 15 | // Headings 16 | $headingMargin: 0 0 $blockMargin 0; 17 | $headingFont: 'League Gothic', Impact, sans-serif; 18 | $headingColor: #eee; 19 | $headingLineHeight: 1.2; 20 | $headingLetterSpacing: normal; 21 | $headingTextTransform: uppercase; 22 | $headingTextShadow: none; 23 | $headingFontWeight: normal; 24 | $heading1TextShadow: $headingTextShadow; 25 | 26 | $heading1Size: 3.77em; 27 | $heading2Size: 2.11em; 28 | $heading3Size: 1.55em; 29 | $heading4Size: 1.00em; 30 | 31 | // Links and actions 32 | $linkColor: #13DAEC; 33 | $linkColorHover: lighten( $linkColor, 20% ); 34 | 35 | // Text selection 36 | $selectionBackgroundColor: #FF5E99; 37 | $selectionColor: #fff; 38 | 39 | // Generates the presentation background, can be overridden 40 | // to return a background image or gradient 41 | @mixin bodyBackground() { 42 | background: $backgroundColor; 43 | } -------------------------------------------------------------------------------- /presentation/img/kitten.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-paperless-project/paperless/9b0063c9731f7c5f65b1852cb8caff97f5e40ba4/presentation/img/kitten.jpg -------------------------------------------------------------------------------- /presentation/img/pony.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-paperless-project/paperless/9b0063c9731f7c5f65b1852cb8caff97f5e40ba4/presentation/img/pony.png -------------------------------------------------------------------------------- /presentation/img/stack.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-paperless-project/paperless/9b0063c9731f7c5f65b1852cb8caff97f5e40ba4/presentation/img/stack.jpg -------------------------------------------------------------------------------- /presentation/lib/css/zenburn.css: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Zenburn style from voldmar.ru (c) Vladimir Epifanov 4 | based on dark.css by Ivan Sagalaev 5 | 6 | */ 7 | 8 | .hljs { 9 | display: block; padding: 0.5em; 10 | background: #3F3F3F; 11 | color: #DCDCDC; 12 | } 13 | 14 | .hljs-keyword, 15 | .hljs-tag, 16 | .css .hljs-class, 17 | .css .hljs-id, 18 | .lisp .hljs-title, 19 | .nginx .hljs-title, 20 | .hljs-request, 21 | .hljs-status, 22 | .clojure .hljs-attribute { 23 | color: #E3CEAB; 24 | } 25 | 26 | .django .hljs-template_tag, 27 | .django .hljs-variable, 28 | .django .hljs-filter .hljs-argument { 29 | color: #DCDCDC; 30 | } 31 | 32 | .hljs-number, 33 | .hljs-date { 34 | color: #8CD0D3; 35 | } 36 | 37 | .dos .hljs-envvar, 38 | .dos .hljs-stream, 39 | .hljs-variable, 40 | .apache .hljs-sqbracket { 41 | color: #EFDCBC; 42 | } 43 | 44 | .dos .hljs-flow, 45 | .diff .hljs-change, 46 | .python .exception, 47 | .python .hljs-built_in, 48 | .hljs-literal, 49 | .tex .hljs-special { 50 | color: #EFEFAF; 51 | } 52 | 53 | .diff .hljs-chunk, 54 | .hljs-subst { 55 | color: #8F8F8F; 56 | } 57 | 58 | .dos .hljs-keyword, 59 | .python .hljs-decorator, 60 | .hljs-title, 61 | .haskell .hljs-type, 62 | .diff .hljs-header, 63 | .ruby .hljs-class .hljs-parent, 64 | .apache .hljs-tag, 65 | .nginx .hljs-built_in, 66 | .tex .hljs-command, 67 | .hljs-prompt { 68 | color: #efef8f; 69 | } 70 | 71 | .dos .hljs-winutils, 72 | .ruby .hljs-symbol, 73 | .ruby .hljs-symbol .hljs-string, 74 | .ruby .hljs-string { 75 | color: #DCA3A3; 76 | } 77 | 78 | .diff .hljs-deletion, 79 | .hljs-string, 80 | .hljs-tag .hljs-value, 81 | .hljs-preprocessor, 82 | .hljs-pragma, 83 | .hljs-built_in, 84 | .sql .hljs-aggregate, 85 | .hljs-javadoc, 86 | .smalltalk .hljs-class, 87 | .smalltalk .hljs-localvars, 88 | .smalltalk .hljs-array, 89 | .css .hljs-rules .hljs-value, 90 | .hljs-attr_selector, 91 | .hljs-pseudo, 92 | .apache .hljs-cbracket, 93 | .tex .hljs-formula, 94 | .coffeescript .hljs-attribute { 95 | color: #CC9393; 96 | } 97 | 98 | .hljs-shebang, 99 | .diff .hljs-addition, 100 | .hljs-comment, 101 | .java .hljs-annotation, 102 | .hljs-template_comment, 103 | .hljs-pi, 104 | .hljs-doctype { 105 | color: #7F9F7F; 106 | } 107 | 108 | .coffeescript .javascript, 109 | .javascript .xml, 110 | .tex .hljs-formula, 111 | .xml .javascript, 112 | .xml .vbscript, 113 | .xml .css, 114 | .xml .hljs-cdata { 115 | opacity: 0.5; 116 | } 117 | 118 | -------------------------------------------------------------------------------- /presentation/lib/font/league-gothic/LICENSE: -------------------------------------------------------------------------------- 1 | SIL Open Font License (OFL) 2 | http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=OFL 3 | -------------------------------------------------------------------------------- /presentation/lib/font/league-gothic/league-gothic.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'League Gothic'; 3 | src: url('league-gothic.eot'); 4 | src: url('league-gothic.eot?#iefix') format('embedded-opentype'), 5 | url('league-gothic.woff') format('woff'), 6 | url('league-gothic.ttf') format('truetype'); 7 | 8 | font-weight: normal; 9 | font-style: normal; 10 | } -------------------------------------------------------------------------------- /presentation/lib/font/league-gothic/league-gothic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-paperless-project/paperless/9b0063c9731f7c5f65b1852cb8caff97f5e40ba4/presentation/lib/font/league-gothic/league-gothic.eot -------------------------------------------------------------------------------- /presentation/lib/font/league-gothic/league-gothic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-paperless-project/paperless/9b0063c9731f7c5f65b1852cb8caff97f5e40ba4/presentation/lib/font/league-gothic/league-gothic.ttf -------------------------------------------------------------------------------- /presentation/lib/font/league-gothic/league-gothic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-paperless-project/paperless/9b0063c9731f7c5f65b1852cb8caff97f5e40ba4/presentation/lib/font/league-gothic/league-gothic.woff -------------------------------------------------------------------------------- /presentation/lib/font/source-sans-pro/source-sans-pro-italic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-paperless-project/paperless/9b0063c9731f7c5f65b1852cb8caff97f5e40ba4/presentation/lib/font/source-sans-pro/source-sans-pro-italic.eot -------------------------------------------------------------------------------- /presentation/lib/font/source-sans-pro/source-sans-pro-italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-paperless-project/paperless/9b0063c9731f7c5f65b1852cb8caff97f5e40ba4/presentation/lib/font/source-sans-pro/source-sans-pro-italic.ttf -------------------------------------------------------------------------------- /presentation/lib/font/source-sans-pro/source-sans-pro-italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-paperless-project/paperless/9b0063c9731f7c5f65b1852cb8caff97f5e40ba4/presentation/lib/font/source-sans-pro/source-sans-pro-italic.woff -------------------------------------------------------------------------------- /presentation/lib/font/source-sans-pro/source-sans-pro-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-paperless-project/paperless/9b0063c9731f7c5f65b1852cb8caff97f5e40ba4/presentation/lib/font/source-sans-pro/source-sans-pro-regular.eot -------------------------------------------------------------------------------- /presentation/lib/font/source-sans-pro/source-sans-pro-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-paperless-project/paperless/9b0063c9731f7c5f65b1852cb8caff97f5e40ba4/presentation/lib/font/source-sans-pro/source-sans-pro-regular.ttf -------------------------------------------------------------------------------- /presentation/lib/font/source-sans-pro/source-sans-pro-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-paperless-project/paperless/9b0063c9731f7c5f65b1852cb8caff97f5e40ba4/presentation/lib/font/source-sans-pro/source-sans-pro-regular.woff -------------------------------------------------------------------------------- /presentation/lib/font/source-sans-pro/source-sans-pro-semibold.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-paperless-project/paperless/9b0063c9731f7c5f65b1852cb8caff97f5e40ba4/presentation/lib/font/source-sans-pro/source-sans-pro-semibold.eot -------------------------------------------------------------------------------- /presentation/lib/font/source-sans-pro/source-sans-pro-semibold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-paperless-project/paperless/9b0063c9731f7c5f65b1852cb8caff97f5e40ba4/presentation/lib/font/source-sans-pro/source-sans-pro-semibold.ttf -------------------------------------------------------------------------------- /presentation/lib/font/source-sans-pro/source-sans-pro-semibold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-paperless-project/paperless/9b0063c9731f7c5f65b1852cb8caff97f5e40ba4/presentation/lib/font/source-sans-pro/source-sans-pro-semibold.woff -------------------------------------------------------------------------------- /presentation/lib/font/source-sans-pro/source-sans-pro-semibolditalic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-paperless-project/paperless/9b0063c9731f7c5f65b1852cb8caff97f5e40ba4/presentation/lib/font/source-sans-pro/source-sans-pro-semibolditalic.eot -------------------------------------------------------------------------------- /presentation/lib/font/source-sans-pro/source-sans-pro-semibolditalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-paperless-project/paperless/9b0063c9731f7c5f65b1852cb8caff97f5e40ba4/presentation/lib/font/source-sans-pro/source-sans-pro-semibolditalic.ttf -------------------------------------------------------------------------------- /presentation/lib/font/source-sans-pro/source-sans-pro-semibolditalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-paperless-project/paperless/9b0063c9731f7c5f65b1852cb8caff97f5e40ba4/presentation/lib/font/source-sans-pro/source-sans-pro-semibolditalic.woff -------------------------------------------------------------------------------- /presentation/lib/font/source-sans-pro/source-sans-pro.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Source Sans Pro'; 3 | src: url('source-sans-pro-regular.eot'); 4 | src: url('source-sans-pro-regular.eot?#iefix') format('embedded-opentype'), 5 | url('source-sans-pro-regular.woff') format('woff'), 6 | url('source-sans-pro-regular.ttf') format('truetype'); 7 | font-weight: normal; 8 | font-style: normal; 9 | } 10 | 11 | @font-face { 12 | font-family: 'Source Sans Pro'; 13 | src: url('source-sans-pro-italic.eot'); 14 | src: url('source-sans-pro-italic.eot?#iefix') format('embedded-opentype'), 15 | url('source-sans-pro-italic.woff') format('woff'), 16 | url('source-sans-pro-italic.ttf') format('truetype'); 17 | font-weight: normal; 18 | font-style: italic; 19 | } 20 | 21 | @font-face { 22 | font-family: 'Source Sans Pro'; 23 | src: url('source-sans-pro-semibold.eot'); 24 | src: url('source-sans-pro-semibold.eot?#iefix') format('embedded-opentype'), 25 | url('source-sans-pro-semibold.woff') format('woff'), 26 | url('source-sans-pro-semibold.ttf') format('truetype'); 27 | font-weight: 600; 28 | font-style: normal; 29 | } 30 | 31 | @font-face { 32 | font-family: 'Source Sans Pro'; 33 | src: url('source-sans-pro-semibolditalic.eot'); 34 | src: url('source-sans-pro-semibolditalic.eot?#iefix') format('embedded-opentype'), 35 | url('source-sans-pro-semibolditalic.woff') format('woff'), 36 | url('source-sans-pro-semibolditalic.ttf') format('truetype'); 37 | font-weight: 600; 38 | font-style: italic; 39 | } -------------------------------------------------------------------------------- /presentation/lib/js/classList.js: -------------------------------------------------------------------------------- 1 | /*! @source http://purl.eligrey.com/github/classList.js/blob/master/classList.js*/ 2 | if(typeof document!=="undefined"&&!("classList" in document.createElement("a"))){(function(j){var a="classList",f="prototype",m=(j.HTMLElement||j.Element)[f],b=Object,k=String[f].trim||function(){return this.replace(/^\s+|\s+$/g,"")},c=Array[f].indexOf||function(q){var p=0,o=this.length;for(;p 3 | Copyright Tero Piirainen (tipiirai) 4 | License MIT / http://bit.ly/mit-license 5 | Version 0.96 6 | 7 | http://headjs.com 8 | */(function(a){function z(){d||(d=!0,s(e,function(a){p(a)}))}function y(c,d){var e=a.createElement("script");e.type="text/"+(c.type||"javascript"),e.src=c.src||c,e.async=!1,e.onreadystatechange=e.onload=function(){var a=e.readyState;!d.done&&(!a||/loaded|complete/.test(a))&&(d.done=!0,d())},(a.body||b).appendChild(e)}function x(a,b){if(a.state==o)return b&&b();if(a.state==n)return k.ready(a.name,b);if(a.state==m)return a.onpreload.push(function(){x(a,b)});a.state=n,y(a.url,function(){a.state=o,b&&b(),s(g[a.name],function(a){p(a)}),u()&&d&&s(g.ALL,function(a){p(a)})})}function w(a,b){a.state===undefined&&(a.state=m,a.onpreload=[],y({src:a.url,type:"cache"},function(){v(a)}))}function v(a){a.state=l,s(a.onpreload,function(a){a.call()})}function u(a){a=a||h;var b;for(var c in a){if(a.hasOwnProperty(c)&&a[c].state!=o)return!1;b=!0}return b}function t(a){return Object.prototype.toString.call(a)=="[object Function]"}function s(a,b){if(!!a){typeof a=="object"&&(a=[].slice.call(a));for(var c=0;c 2 | 19 | 21 | 22 | 24 | image/svg+xml 25 | 27 | 28 | 29 | 30 | 31 | 33 | 53 | 56 | 58 | 62 | 70 | 72 | 74 | 79 | 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /scripts/gunicorn.conf: -------------------------------------------------------------------------------- 1 | bind = '127.0.0.1:8000' 2 | backlog = 2048 3 | workers = 3 4 | worker_class = 'sync' 5 | worker_connections = 1000 6 | timeout = 20 7 | keepalive = 2 8 | spew = False 9 | daemon = False 10 | pidfile = None 11 | umask = 0 12 | user = None 13 | group = None 14 | tmp_upload_dir = None 15 | loglevel = 'info' 16 | errorlog = '-' 17 | accesslog = '-' 18 | proc_name = None 19 | 20 | def pre_fork(server, worker): 21 | pass 22 | 23 | def pre_exec(server): 24 | server.log.info("Forked child, re-executing.") 25 | 26 | def when_ready(server): 27 | server.log.info("Server is ready. Spawning workers") 28 | 29 | def worker_int(worker): 30 | worker.log.info("worker received INT or QUIT signal") 31 | 32 | ## get traceback info 33 | import threading, sys, traceback 34 | id2name = dict([(th.ident, th.name) for th in threading.enumerate()]) 35 | code = [] 36 | for threadId, stack in sys._current_frames().items(): 37 | code.append("\n# Thread: %s(%d)" % (id2name.get(threadId,""), 38 | threadId)) 39 | for filename, lineno, name, line in traceback.extract_stack(stack): 40 | code.append('File: "%s", line %d, in %s' % (filename, 41 | lineno, name)) 42 | if line: 43 | code.append(" %s" % (line.strip())) 44 | worker.log.debug("\n".join(code)) 45 | 46 | def worker_abort(worker): 47 | worker.log.info("worker received SIGABRT signal") 48 | 49 | -------------------------------------------------------------------------------- /scripts/paperless-consumer.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Paperless consumer 3 | 4 | [Service] 5 | User=paperless 6 | Group=paperless 7 | ExecStart=/home/paperless/project/virtualenv/bin/python /home/paperless/project/src/manage.py document_consumer 8 | 9 | [Install] 10 | WantedBy=multi-user.target 11 | -------------------------------------------------------------------------------- /scripts/paperless-webserver.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Paperless webserver 3 | After=network.target 4 | Wants=network.target 5 | 6 | [Service] 7 | User=paperless 8 | Group=paperless 9 | ExecStart=/home/paperless/project/virtualenv/bin/gunicorn --pythonpath=/home/paperless/project/src paperless.wsgi -w 2 10 | 11 | [Install] 12 | WantedBy=multi-user.target 13 | -------------------------------------------------------------------------------- /scripts/post-consumption-example.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | DOCUMENT_ID=${1} 4 | DOCUMENT_FILE_NAME=${2} 5 | DOCUMENT_SOURCE_PATH=${3} 6 | DOCUMENT_THUMBNAIL_PATH=${4} 7 | DOCUMENT_DOWNLOAD_URL=${5} 8 | DOCUMENT_THUMBNAIL_URL=${6} 9 | DOCUMENT_CORRESPONDENT=${7} 10 | DOCUMENT_TAGS=${8} 11 | 12 | echo " 13 | 14 | A document with an id of ${DOCUMENT_ID} was just consumed. I know the 15 | following additional information about it: 16 | 17 | * Generated File Name: ${DOCUMENT_FILE_NAME} 18 | * Source Path: ${DOCUMENT_SOURCE_PATH} 19 | * Thumbnail Path: ${DOCUMENT_THUMBNAIL_PATH} 20 | * Download URL: ${DOCUMENT_DOWNLOAD_URL} 21 | * Thumbnail URL: ${DOCUMENT_THUMBNAIL_URL} 22 | * Correspondent: ${DOCUMENT_CORRESPONDENT} 23 | * Tags: ${DOCUMENT_TAGS} 24 | 25 | It was consumed with the passphrase ${PASSPHRASE} 26 | 27 | " 28 | -------------------------------------------------------------------------------- /src/documents/__init__.py: -------------------------------------------------------------------------------- 1 | from .checks import changed_password_check 2 | -------------------------------------------------------------------------------- /src/documents/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | from django.db.models.signals import post_delete 3 | 4 | 5 | class DocumentsConfig(AppConfig): 6 | 7 | name = "documents" 8 | 9 | def ready(self): 10 | 11 | from .signals import document_consumption_started 12 | from .signals import document_consumption_finished 13 | from .signals.handlers import ( 14 | set_correspondent, 15 | set_tags, 16 | run_pre_consume_script, 17 | run_post_consume_script, 18 | cleanup_document_deletion, 19 | set_log_entry 20 | ) 21 | 22 | document_consumption_started.connect(run_pre_consume_script) 23 | 24 | document_consumption_finished.connect(set_tags) 25 | document_consumption_finished.connect(set_correspondent) 26 | document_consumption_finished.connect(set_log_entry) 27 | document_consumption_finished.connect(run_post_consume_script) 28 | 29 | post_delete.connect(cleanup_document_deletion) 30 | 31 | AppConfig.ready(self) 32 | -------------------------------------------------------------------------------- /src/documents/checks.py: -------------------------------------------------------------------------------- 1 | import textwrap 2 | 3 | from django.conf import settings 4 | from django.core.checks import Error, register 5 | from django.db.utils import OperationalError, ProgrammingError 6 | 7 | 8 | @register() 9 | def changed_password_check(app_configs, **kwargs): 10 | 11 | from documents.models import Document 12 | from paperless.db import GnuPG 13 | 14 | try: 15 | encrypted_doc = Document.objects.filter( 16 | storage_type=Document.STORAGE_TYPE_GPG).first() 17 | except (OperationalError, ProgrammingError): 18 | return [] # No documents table yet 19 | 20 | if encrypted_doc: 21 | 22 | if not settings.PASSPHRASE: 23 | return [Error( 24 | "The database contains encrypted documents but no password " 25 | "is set." 26 | )] 27 | 28 | if not GnuPG.decrypted(encrypted_doc.source_file): 29 | return [Error(textwrap.dedent( 30 | """ 31 | The current password doesn't match the password of the 32 | existing documents. 33 | 34 | If you intend to change your password, you must first export 35 | all of the old documents, start fresh with the new password 36 | and then re-import them." 37 | """))] 38 | 39 | return [] 40 | -------------------------------------------------------------------------------- /src/documents/filters.py: -------------------------------------------------------------------------------- 1 | from django_filters.rest_framework import BooleanFilter, FilterSet 2 | 3 | from .models import Correspondent, Document, Tag 4 | 5 | 6 | CHAR_KWARGS = ( 7 | "startswith", "endswith", "contains", 8 | "istartswith", "iendswith", "icontains" 9 | ) 10 | 11 | 12 | class CorrespondentFilterSet(FilterSet): 13 | 14 | class Meta: 15 | model = Correspondent 16 | fields = { 17 | "name": [ 18 | "startswith", "endswith", "contains", 19 | "istartswith", "iendswith", "icontains" 20 | ], 21 | "slug": ["istartswith", "iendswith", "icontains"] 22 | } 23 | 24 | 25 | class TagFilterSet(FilterSet): 26 | 27 | class Meta: 28 | model = Tag 29 | fields = { 30 | "name": [ 31 | "startswith", "endswith", "contains", 32 | "istartswith", "iendswith", "icontains" 33 | ], 34 | "slug": ["istartswith", "iendswith", "icontains"] 35 | } 36 | 37 | 38 | class DocumentFilterSet(FilterSet): 39 | 40 | tags_empty = BooleanFilter( 41 | label="Is tagged", 42 | field_name="tags", 43 | lookup_expr="isnull", 44 | exclude=True 45 | ) 46 | 47 | class Meta: 48 | model = Document 49 | fields = { 50 | 51 | "title": CHAR_KWARGS, 52 | "content": ("contains", "icontains"), 53 | 54 | "correspondent__name": CHAR_KWARGS, 55 | "correspondent__slug": CHAR_KWARGS, 56 | 57 | "tags__name": CHAR_KWARGS, 58 | "tags__slug": CHAR_KWARGS, 59 | 60 | } 61 | -------------------------------------------------------------------------------- /src/documents/forms.py: -------------------------------------------------------------------------------- 1 | import magic 2 | import os 3 | 4 | from datetime import datetime 5 | from time import mktime 6 | 7 | from django import forms 8 | from django.conf import settings 9 | 10 | from .models import Document, Correspondent 11 | 12 | 13 | class UploadForm(forms.Form): 14 | 15 | TYPE_LOOKUP = { 16 | "application/pdf": Document.TYPE_PDF, 17 | "image/png": Document.TYPE_PNG, 18 | "image/jpeg": Document.TYPE_JPG, 19 | "image/gif": Document.TYPE_GIF, 20 | "image/tiff": Document.TYPE_TIF, 21 | } 22 | 23 | correspondent = forms.CharField( 24 | max_length=Correspondent._meta.get_field("name").max_length, 25 | required=False 26 | ) 27 | title = forms.CharField( 28 | max_length=Document._meta.get_field("title").max_length, 29 | required=False 30 | ) 31 | document = forms.FileField() 32 | 33 | def __init__(self, *args, **kwargs): 34 | forms.Form.__init__(self, *args, **kwargs) 35 | self._file_type = None 36 | 37 | def clean_correspondent(self): 38 | """ 39 | I suppose it might look cleaner to use .get_or_create() here, but that 40 | would also allow someone to fill up the db with bogus correspondents 41 | before all validation was met. 42 | """ 43 | 44 | corresp = self.cleaned_data.get("correspondent") 45 | 46 | if not corresp: 47 | return None 48 | 49 | if not Correspondent.SAFE_REGEX.match(corresp) or " - " in corresp: 50 | raise forms.ValidationError( 51 | "That correspondent name is suspicious.") 52 | 53 | return corresp 54 | 55 | def clean_title(self): 56 | 57 | title = self.cleaned_data.get("title") 58 | 59 | if not title: 60 | return None 61 | 62 | if not Correspondent.SAFE_REGEX.match(title) or " - " in title: 63 | raise forms.ValidationError("That title is suspicious.") 64 | 65 | return title 66 | 67 | def clean_document(self): 68 | 69 | document = self.cleaned_data.get("document").read() 70 | 71 | with magic.Magic(flags=magic.MAGIC_MIME_TYPE) as m: 72 | file_type = m.id_buffer(document) 73 | 74 | if file_type not in self.TYPE_LOOKUP: 75 | raise forms.ValidationError("The file type is invalid.") 76 | 77 | self._file_type = self.TYPE_LOOKUP[file_type] 78 | 79 | return document 80 | 81 | def save(self): 82 | """ 83 | Since the consumer already does a lot of work, it's easier just to save 84 | to-be-consumed files to the consumption directory rather than have the 85 | form do that as well. Think of it as a poor-man's queue server. 86 | """ 87 | 88 | correspondent = self.cleaned_data.get("correspondent") 89 | title = self.cleaned_data.get("title") 90 | document = self.cleaned_data.get("document") 91 | 92 | t = int(mktime(datetime.now().timetuple())) 93 | file_name = os.path.join( 94 | settings.CONSUMPTION_DIR, 95 | "{} - {}.{}".format(correspondent, title, self._file_type) 96 | ) 97 | 98 | with open(file_name, "wb") as f: 99 | f.write(document) 100 | os.utime(file_name, times=(t, t)) 101 | -------------------------------------------------------------------------------- /src/documents/loggers.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | 4 | class PaperlessLogger(logging.StreamHandler): 5 | """ 6 | A logger smart enough to know to log some kinds of messages to the database 7 | for later retrieval in a pretty interface. 8 | """ 9 | 10 | def emit(self, record): 11 | 12 | logging.StreamHandler.emit(self, record) 13 | 14 | # We have to do the import here or Django will barf when it tries to 15 | # load this because the apps aren't loaded at that point 16 | from .models import Log 17 | 18 | kwargs = {"message": record.msg, "level": record.levelno} 19 | 20 | if hasattr(record, "group"): 21 | kwargs["group"] = record.group 22 | 23 | Log.objects.create(**kwargs) 24 | -------------------------------------------------------------------------------- /src/documents/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-paperless-project/paperless/9b0063c9731f7c5f65b1852cb8caff97f5e40ba4/src/documents/management/__init__.py -------------------------------------------------------------------------------- /src/documents/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-paperless-project/paperless/9b0063c9731f7c5f65b1852cb8caff97f5e40ba4/src/documents/management/commands/__init__.py -------------------------------------------------------------------------------- /src/documents/management/commands/document_correspondents.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from django.core.management.base import BaseCommand 4 | 5 | from documents.models import Correspondent, Document 6 | 7 | from ...mixins import Renderable 8 | 9 | 10 | class Command(Renderable, BaseCommand): 11 | 12 | help = """ 13 | Using the current set of correspondent rules, apply said rules to all 14 | documents in the database, effectively allowing you to back-tag all 15 | previously indexed documents with correspondent created (or modified) 16 | after their initial import. 17 | """.replace(" ", "") 18 | 19 | TOO_MANY_CONTINUE = ( 20 | "Detected {} potential correspondents for {}, so we've opted for {}") 21 | TOO_MANY_SKIP = ( 22 | "Detected {} potential correspondents for {}, so we're skipping it") 23 | CHANGE_MESSAGE = ( 24 | 'Document {}: "{}" was given the correspondent id {}: "{}"') 25 | 26 | def __init__(self, *args, **kwargs): 27 | self.verbosity = 0 28 | BaseCommand.__init__(self, *args, **kwargs) 29 | 30 | def add_arguments(self, parser): 31 | parser.add_argument( 32 | "--use-first", 33 | default=False, 34 | action="store_true", 35 | help="By default this command won't try to assign a correspondent " 36 | "if more than one matches the document. Use this flag if " 37 | "you'd rather it just pick the first one it finds." 38 | ) 39 | 40 | def handle(self, *args, **options): 41 | 42 | self.verbosity = options["verbosity"] 43 | 44 | for document in Document.objects.filter(correspondent__isnull=True): 45 | 46 | potential_correspondents = list( 47 | Correspondent.match_all(document.content)) 48 | 49 | if not potential_correspondents: 50 | continue 51 | 52 | potential_count = len(potential_correspondents) 53 | correspondent = potential_correspondents[0] 54 | 55 | if potential_count > 1: 56 | if not options["use_first"]: 57 | print( 58 | self.TOO_MANY_SKIP.format(potential_count, document), 59 | file=sys.stderr 60 | ) 61 | continue 62 | print( 63 | self.TOO_MANY_CONTINUE.format( 64 | potential_count, 65 | document, 66 | correspondent 67 | ), 68 | file=sys.stderr 69 | ) 70 | 71 | document.correspondent = correspondent 72 | document.save(update_fields=("correspondent",)) 73 | 74 | print( 75 | self.CHANGE_MESSAGE.format( 76 | document.pk, 77 | document.title, 78 | correspondent.pk, 79 | correspondent.name 80 | ), 81 | file=sys.stderr 82 | ) 83 | -------------------------------------------------------------------------------- /src/documents/management/commands/document_logs.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand 2 | 3 | from documents.models import Log 4 | 5 | 6 | class Command(BaseCommand): 7 | 8 | help = "A quick & dirty way to see what's in the logs" 9 | 10 | def handle(self, *args, **options): 11 | for l in Log.objects.order_by("pk"): 12 | print(l) 13 | -------------------------------------------------------------------------------- /src/documents/management/commands/document_renamer.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand 2 | 3 | from documents.models import Document, Tag 4 | 5 | from ...mixins import Renderable 6 | 7 | 8 | class Command(Renderable, BaseCommand): 9 | 10 | help = """ 11 | This will rename all documents to match the latest filename format. 12 | """.replace(" ", "") 13 | 14 | def __init__(self, *args, **kwargs): 15 | self.verbosity = 0 16 | BaseCommand.__init__(self, *args, **kwargs) 17 | 18 | def handle(self, *args, **options): 19 | 20 | self.verbosity = options["verbosity"] 21 | 22 | for document in Document.objects.all(): 23 | # Saving the document again will generate a new filename and rename 24 | document.save() 25 | -------------------------------------------------------------------------------- /src/documents/management/commands/document_retagger.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand 2 | 3 | from documents.models import Document, Tag 4 | 5 | from ...mixins import Renderable 6 | 7 | 8 | class Command(Renderable, BaseCommand): 9 | 10 | help = """ 11 | Using the current set of tagging rules, apply said rules to all 12 | documents in the database, effectively allowing you to back-tag all 13 | previously indexed documents with tags created (or modified) after 14 | their initial import. 15 | """.replace(" ", "") 16 | 17 | def __init__(self, *args, **kwargs): 18 | self.verbosity = 0 19 | BaseCommand.__init__(self, *args, **kwargs) 20 | 21 | def handle(self, *args, **options): 22 | 23 | self.verbosity = options["verbosity"] 24 | 25 | for document in Document.objects.all(): 26 | 27 | tags = Tag.objects.exclude( 28 | pk__in=document.tags.values_list("pk", flat=True)) 29 | 30 | for tag in Tag.match_all(document.content, tags): 31 | print('Tagging {} with "{}"'.format(document, tag)) 32 | document.tags.add(tag) 33 | -------------------------------------------------------------------------------- /src/documents/management/commands/loaddata_stdin.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from django.core.management.commands.loaddata import Command as LoadDataCommand 4 | 5 | 6 | class Command(LoadDataCommand): 7 | """ 8 | Allow the loading of data from standard in. Sourced originally from: 9 | https://gist.github.com/bmispelon/ad5a2c333443b3a1d051 (MIT licensed) 10 | """ 11 | 12 | def parse_name(self, fixture_name): 13 | self.compression_formats['stdin'] = (lambda x, y: sys.stdin, None) 14 | if fixture_name == '-': 15 | return '-', 'json', 'stdin' 16 | 17 | def find_fixtures(self, fixture_label): 18 | if fixture_label == '-': 19 | return [('-', None, '-')] 20 | return super(Command, self).find_fixtures(fixture_label) 21 | -------------------------------------------------------------------------------- /src/documents/managers.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | 3 | from django.db import models 4 | from django.db.models.aggregates import Max 5 | 6 | 7 | class GroupConcat(models.Aggregate): 8 | """ 9 | Theoretically, this should work in Sqlite, PostgreSQL, and MySQL, but I've 10 | only ever tested it in Sqlite. 11 | """ 12 | 13 | ENGINE_SQLITE = 1 14 | ENGINE_POSTGRESQL = 2 15 | ENGINE_MYSQL = 3 16 | ENGINES = { 17 | "django.db.backends.sqlite3": ENGINE_SQLITE, 18 | "django.db.backends.postgresql_psycopg2": ENGINE_POSTGRESQL, 19 | "django.db.backends.postgresql": ENGINE_POSTGRESQL, 20 | "django.db.backends.mysql": ENGINE_MYSQL 21 | } 22 | 23 | def __init__(self, expression, separator="\n", **extra): 24 | 25 | self.engine = self._get_engine() 26 | self.function = self._get_function() 27 | self.template = self._get_template(separator) 28 | 29 | models.Aggregate.__init__( 30 | self, 31 | expression, 32 | output_field=models.CharField(), 33 | **extra 34 | ) 35 | 36 | def _get_engine(self): 37 | engine = settings.DATABASES["default"]["ENGINE"] 38 | try: 39 | return self.ENGINES[engine] 40 | except KeyError: 41 | raise NotImplementedError( 42 | "There's currently no support for {} when it comes to group " 43 | "concatenation in Paperless".format(engine) 44 | ) 45 | 46 | def _get_function(self): 47 | if self.engine == self.ENGINE_POSTGRESQL: 48 | return "STRING_AGG" 49 | return "GROUP_CONCAT" 50 | 51 | def _get_template(self, separator): 52 | if self.engine == self.ENGINE_MYSQL: 53 | return "%(function)s(%(expressions)s SEPARATOR '{}')".format( 54 | separator) 55 | return "%(function)s(%(expressions)s, '{}')".format(separator) 56 | 57 | 58 | class LogQuerySet(models.query.QuerySet): 59 | 60 | def by_group(self): 61 | return self.values("group").annotate( 62 | time=Max("modified"), 63 | messages=GroupConcat("message"), 64 | ).order_by("-time") 65 | 66 | 67 | class LogManager(models.Manager): 68 | 69 | def get_queryset(self): 70 | return LogQuerySet(self.model, using=self._db) 71 | -------------------------------------------------------------------------------- /src/documents/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9 on 2015-12-20 19:10 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | from django.conf import settings 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | initial = True 12 | 13 | dependencies = [ 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='Document', 19 | fields=[ 20 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('sender', models.CharField(blank=True, db_index=True, max_length=128)), 22 | ('title', models.CharField(blank=True, db_index=True, max_length=128)), 23 | ('content', models.TextField(db_index=("mysql" not in settings.DATABASES["default"]["ENGINE"]))), 24 | ('created', models.DateTimeField(auto_now_add=True)), 25 | ('modified', models.DateTimeField(auto_now=True)), 26 | ], 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /src/documents/migrations/0002_auto_20151226_1316.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9 on 2015-12-26 13:16 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.utils.timezone 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('documents', '0001_initial'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AlterModelOptions( 17 | name='document', 18 | options={'ordering': ('sender', 'title')}, 19 | ), 20 | migrations.AlterField( 21 | model_name='document', 22 | name='created', 23 | field=models.DateTimeField(default=django.utils.timezone.now, editable=False), 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /src/documents/migrations/0003_sender.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9 on 2016-01-11 12:21 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | from django.template.defaultfilters import slugify 7 | 8 | import django.db.models.deletion 9 | 10 | 11 | DOCUMENT_SENDER_MAP = {} 12 | 13 | 14 | def move_sender_strings_to_sender_model(apps, schema_editor): 15 | 16 | sender_model = apps.get_model("documents", "Sender") 17 | document_model = apps.get_model("documents", "Document") 18 | 19 | # Create the sender and log the relationship with the document 20 | for document in document_model.objects.all(): 21 | if document.sender: 22 | DOCUMENT_SENDER_MAP[document.pk], created = sender_model.objects.get_or_create( 23 | name=document.sender, 24 | defaults={"slug": slugify(document.sender)} 25 | ) 26 | 27 | 28 | def realign_senders(apps, schema_editor): 29 | document_model = apps.get_model("documents", "Document") 30 | for pk, sender in DOCUMENT_SENDER_MAP.items(): 31 | document_model.objects.filter(pk=pk).update(sender=sender) 32 | 33 | 34 | class Migration(migrations.Migration): 35 | dependencies = [ 36 | ('documents', '0002_auto_20151226_1316'), 37 | ] 38 | 39 | operations = [ 40 | migrations.CreateModel( 41 | name='Sender', 42 | fields=[ 43 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 44 | ('name', models.CharField(max_length=128, unique=True)), 45 | ('slug', models.SlugField()), 46 | ], 47 | ), 48 | migrations.RunPython(move_sender_strings_to_sender_model), 49 | migrations.RemoveField( 50 | model_name='document', 51 | name='sender', 52 | ), 53 | migrations.AddField( 54 | model_name='document', 55 | name='sender', 56 | field=models.ForeignKey(blank=True, on_delete=django.db.models.deletion.CASCADE, to='documents.Sender'), 57 | ), 58 | migrations.RunPython(realign_senders), 59 | ] 60 | -------------------------------------------------------------------------------- /src/documents/migrations/0004_auto_20160114_1844.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9 on 2016-01-14 18:44 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('documents', '0003_sender'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AlterField( 17 | model_name='document', 18 | name='sender', 19 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='documents', to='documents.Sender'), 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /src/documents/migrations/0005_auto_20160123_0313.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9 on 2016-01-23 03:13 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('documents', '0004_auto_20160114_1844'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterModelOptions( 16 | name='sender', 17 | options={'ordering': ('name',)}, 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /src/documents/migrations/0006_auto_20160123_0430.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9 on 2016-01-23 04:30 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('documents', '0005_auto_20160123_0313'), 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='Tag', 17 | fields=[ 18 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 19 | ('name', models.CharField(max_length=128, unique=True)), 20 | ('slug', models.SlugField(blank=True)), 21 | ('colour', models.PositiveIntegerField(choices=[(1, '#a6cee3'), (2, '#1f78b4'), (3, '#b2df8a'), (4, '#33a02c'), (5, '#fb9a99'), (6, '#e31a1c'), (7, '#fdbf6f'), (8, '#ff7f00'), (9, '#cab2d6'), (10, '#6a3d9a'), (11, '#ffff99'), (12, '#b15928'), (13, '#000000'), (14, '#cccccc')], default=1)), 22 | ], 23 | options={ 24 | 'abstract': False, 25 | }, 26 | ), 27 | migrations.AlterField( 28 | model_name='sender', 29 | name='slug', 30 | field=models.SlugField(blank=True), 31 | ), 32 | migrations.AddField( 33 | model_name='document', 34 | name='tags', 35 | field=models.ManyToManyField(related_name='documents', to='documents.Tag'), 36 | ), 37 | ] 38 | -------------------------------------------------------------------------------- /src/documents/migrations/0007_auto_20160126_2114.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9 on 2016-01-26 21:14 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('documents', '0006_auto_20160123_0430'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='tag', 17 | name='match', 18 | field=models.CharField(blank=True, max_length=256), 19 | ), 20 | migrations.AddField( 21 | model_name='tag', 22 | name='matching_algorithm', 23 | field=models.PositiveIntegerField(blank=True, choices=[(1, 'Any'), (2, 'All'), (3, 'Literal'), (4, 'Regular Expression')], help_text='Which algorithm you want to use when matching text to the OCR\'d PDF. Here, "any" looks for any occurrence of any word provided in the PDF, while "all" requires that every word provided appear in the PDF, albeit not in the order provided. A "literal" match means that the text you enter must appear in the PDF exactly as you\'ve entered it, and "regular expression" uses a regex to match the PDF. If you don\'t know what a regex is, you probably don\'t want this option.', null=True), 24 | ), 25 | migrations.AlterField( 26 | model_name='tag', 27 | name='colour', 28 | field=models.PositiveIntegerField(choices=[(1, '#a6cee3'), (2, '#1f78b4'), (3, '#b2df8a'), (4, '#33a02c'), (5, '#fb9a99'), (6, '#e31a1c'), (7, '#fdbf6f'), (8, '#ff7f00'), (9, '#cab2d6'), (10, '#6a3d9a'), (11, '#b15928'), (12, '#000000'), (13, '#cccccc')], default=1), 29 | ), 30 | ] 31 | -------------------------------------------------------------------------------- /src/documents/migrations/0008_document_file_type.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9 on 2016-01-29 22:58 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('documents', '0007_auto_20160126_2114'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='document', 17 | name='file_type', 18 | field=models.CharField(choices=[('pdf', 'PDF'), ('png', 'PNG'), ('jpg', 'JPG'), ('gif', 'GIF'), ('tiff', 'TIFF')], default='pdf', editable=False, max_length=4), 19 | preserve_default=False, 20 | ), 21 | migrations.AlterField( 22 | model_name='document', 23 | name='tags', 24 | field=models.ManyToManyField(blank=True, related_name='documents', to='documents.Tag'), 25 | ), 26 | ] 27 | 28 | -------------------------------------------------------------------------------- /src/documents/migrations/0009_auto_20160214_0040.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9 on 2016-02-14 00:40 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('documents', '0008_document_file_type'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='tag', 17 | name='matching_algorithm', 18 | field=models.PositiveIntegerField(choices=[(1, 'Any'), (2, 'All'), (3, 'Literal'), (4, 'Regular Expression')], default=1, help_text='Which algorithm you want to use when matching text to the OCR\'d PDF. Here, "any" looks for any occurrence of any word provided in the PDF, while "all" requires that every word provided appear in the PDF, albeit not in the order provided. A "literal" match means that the text you enter must appear in the PDF exactly as you\'ve entered it, and "regular expression" uses a regex to match the PDF. If you don\'t know what a regex is, you probably don\'t want this option.'), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /src/documents/migrations/0010_log.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9 on 2016-02-27 17:54 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('documents', '0009_auto_20160214_0040'), 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='Log', 17 | fields=[ 18 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 19 | ('group', models.UUIDField(blank=True)), 20 | ('message', models.TextField()), 21 | ('level', models.PositiveIntegerField(choices=[(10, 'Debugging'), (20, 'Informational'), (30, 'Warning'), (40, 'Error'), (50, 'Critical')], default=20)), 22 | ('component', models.PositiveIntegerField(choices=[(1, 'Consumer'), (2, 'Mail Fetcher')])), 23 | ('created', models.DateTimeField(auto_now_add=True)), 24 | ('modified', models.DateTimeField(auto_now=True)), 25 | ], 26 | options={ 27 | 'ordering': ('-modified',), 28 | }, 29 | ), 30 | ] 31 | -------------------------------------------------------------------------------- /src/documents/migrations/0011_auto_20160303_1929.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.2 on 2016-03-03 19:29 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations 6 | 7 | 8 | class Migration(migrations.Migration): 9 | atomic = False 10 | dependencies = [ 11 | ('documents', '0010_log'), 12 | ] 13 | 14 | operations = [ 15 | migrations.RenameModel( 16 | old_name='Sender', 17 | new_name='Correspondent', 18 | ), 19 | migrations.AlterModelOptions( 20 | name='document', 21 | options={'ordering': ('correspondent', 'title')}, 22 | ), 23 | migrations.RenameField( 24 | model_name='document', 25 | old_name='sender', 26 | new_name='correspondent', 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /src/documents/migrations/0013_auto_20160325_2111.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.4 on 2016-03-25 21:11 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.utils.timezone 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('documents', '0012_auto_20160305_0040'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AddField( 17 | model_name='correspondent', 18 | name='match', 19 | field=models.CharField(blank=True, max_length=256), 20 | ), 21 | migrations.AddField( 22 | model_name='correspondent', 23 | name='matching_algorithm', 24 | field=models.PositiveIntegerField(choices=[(1, 'Any'), (2, 'All'), (3, 'Literal'), (4, 'Regular Expression')], default=1, help_text='Which algorithm you want to use when matching text to the OCR\'d PDF. Here, "any" looks for any occurrence of any word provided in the PDF, while "all" requires that every word provided appear in the PDF, albeit not in the order provided. A "literal" match means that the text you enter must appear in the PDF exactly as you\'ve entered it, and "regular expression" uses a regex to match the PDF. If you don\'t know what a regex is, you probably don\'t want this option.'), 25 | ), 26 | migrations.AlterField( 27 | model_name='document', 28 | name='created', 29 | field=models.DateTimeField(default=django.utils.timezone.now), 30 | ), 31 | migrations.RemoveField( 32 | model_name='log', 33 | name='component', 34 | ), 35 | ] 36 | -------------------------------------------------------------------------------- /src/documents/migrations/0015_add_insensitive_to_match.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.2 on 2016-10-05 21:38 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('documents', '0014_document_checksum'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='document', 17 | name='checksum', 18 | field=models.CharField(editable=False, help_text='The checksum of the original document (before it was encrypted). We use this to prevent duplicate document imports.', max_length=32, unique=True), 19 | ), 20 | migrations.AddField( 21 | model_name='correspondent', 22 | name='is_insensitive', 23 | field=models.BooleanField(default=True), 24 | ), 25 | migrations.AddField( 26 | model_name='tag', 27 | name='is_insensitive', 28 | field=models.BooleanField(default=True), 29 | ), 30 | ] 31 | -------------------------------------------------------------------------------- /src/documents/migrations/0016_auto_20170325_1558.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.5 on 2017-03-25 15:58 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | from django.conf import settings 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('documents', '0015_add_insensitive_to_match'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AlterField( 17 | model_name='document', 18 | name='content', 19 | field=models.TextField(blank=True, db_index=("mysql" not in settings.DATABASES["default"]["ENGINE"]), help_text='The raw, text-only data of the document. This field is primarily used for searching.'), 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /src/documents/migrations/0017_auto_20170512_0507.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.5 on 2017-05-12 05:07 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('documents', '0016_auto_20170325_1558'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='correspondent', 17 | name='matching_algorithm', 18 | field=models.PositiveIntegerField(choices=[(1, 'Any'), (2, 'All'), (3, 'Literal'), (4, 'Regular Expression'), (5, 'Fuzzy Match')], default=1, help_text='Which algorithm you want to use when matching text to the OCR\'d PDF. Here, "any" looks for any occurrence of any word provided in the PDF, while "all" requires that every word provided appear in the PDF, albeit not in the order provided. A "literal" match means that the text you enter must appear in the PDF exactly as you\'ve entered it, and "regular expression" uses a regex to match the PDF. (If you don\'t know what a regex is, you probably don\'t want this option.) Finally, a "fuzzy match" looks for words or phrases that are mostly—but not exactly—the same, which can be useful for matching against documents containg imperfections that foil accurate OCR.'), 19 | ), 20 | migrations.AlterField( 21 | model_name='tag', 22 | name='matching_algorithm', 23 | field=models.PositiveIntegerField(choices=[(1, 'Any'), (2, 'All'), (3, 'Literal'), (4, 'Regular Expression'), (5, 'Fuzzy Match')], default=1, help_text='Which algorithm you want to use when matching text to the OCR\'d PDF. Here, "any" looks for any occurrence of any word provided in the PDF, while "all" requires that every word provided appear in the PDF, albeit not in the order provided. A "literal" match means that the text you enter must appear in the PDF exactly as you\'ve entered it, and "regular expression" uses a regex to match the PDF. (If you don\'t know what a regex is, you probably don\'t want this option.) Finally, a "fuzzy match" looks for words or phrases that are mostly—but not exactly—the same, which can be useful for matching against documents containg imperfections that foil accurate OCR.'), 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /src/documents/migrations/0018_auto_20170715_1712.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.5 on 2017-07-15 17:12 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('documents', '0017_auto_20170512_0507'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AlterField( 17 | model_name='document', 18 | name='correspondent', 19 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='documents', to='documents.Correspondent'), 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /src/documents/migrations/0019_add_consumer_user.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.5 on 2017-07-15 17:12 3 | from __future__ import unicode_literals 4 | 5 | from django.contrib.auth.models import User 6 | from django.db import migrations 7 | 8 | 9 | def forwards_func(apps, schema_editor): 10 | User.objects.create(username="consumer") 11 | 12 | 13 | def reverse_func(apps, schema_editor): 14 | User.objects.get(username="consumer").delete() 15 | 16 | 17 | class Migration(migrations.Migration): 18 | dependencies = [ 19 | ('documents', '0018_auto_20170715_1712'), 20 | ] 21 | 22 | operations = [ 23 | migrations.RunPython(forwards_func, reverse_func), 24 | ] 25 | -------------------------------------------------------------------------------- /src/documents/migrations/0020_document_added.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import migrations, models 5 | import django.utils.timezone 6 | 7 | 8 | def set_added_time_to_created_time(apps, schema_editor): 9 | Document = apps.get_model("documents", "Document") 10 | for doc in Document.objects.all(): 11 | doc.added = doc.created 12 | doc.save() 13 | 14 | 15 | class Migration(migrations.Migration): 16 | dependencies = [ 17 | ('documents', '0019_add_consumer_user'), 18 | ] 19 | 20 | operations = [ 21 | migrations.AddField( 22 | model_name='document', 23 | name='added', 24 | field=models.DateTimeField(db_index=True, default=django.utils.timezone.now, editable=False), 25 | ), 26 | migrations.RunPython(set_added_time_to_created_time) 27 | ] 28 | -------------------------------------------------------------------------------- /src/documents/migrations/0021_document_storage_type.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.10 on 2018-02-04 13:07 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('documents', '0020_document_added'), 12 | ] 13 | 14 | operations = [ 15 | 16 | # Add the field with the default GPG-encrypted value 17 | migrations.AddField( 18 | model_name='document', 19 | name='storage_type', 20 | field=models.CharField(choices=[('unencrypted', 'Unencrypted'), ('gpg', 'Encrypted with GNU Privacy Guard')], default='gpg', editable=False, max_length=11), 21 | ), 22 | 23 | # Now that the field is added, change the default to unencrypted 24 | migrations.AlterField( 25 | model_name='document', 26 | name='storage_type', 27 | field=models.CharField(choices=[('unencrypted', 'Unencrypted'), ('gpg', 'Encrypted with GNU Privacy Guard')], default='unencrypted', editable=False, max_length=11), 28 | ), 29 | 30 | ] 31 | -------------------------------------------------------------------------------- /src/documents/migrations/0022_auto_20181007_1420.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.8 on 2018-10-07 14:20 2 | 3 | from django.db import migrations, models 4 | from django.utils.text import slugify 5 | 6 | 7 | def re_slug_all_the_things(apps, schema_editor): 8 | """ 9 | Rewrite all slug values to make sure they're actually slugs before we brand 10 | them as uneditable. 11 | """ 12 | 13 | Tag = apps.get_model("documents", "Tag") 14 | Correspondent = apps.get_model("documents", "Correspondent") 15 | 16 | for klass in (Tag, Correspondent): 17 | for instance in klass.objects.all(): 18 | klass.objects.filter( 19 | pk=instance.pk 20 | ).update( 21 | slug=slugify(instance.slug) 22 | ) 23 | 24 | 25 | class Migration(migrations.Migration): 26 | 27 | dependencies = [ 28 | ('documents', '0021_document_storage_type'), 29 | ] 30 | 31 | operations = [ 32 | migrations.AlterModelOptions( 33 | name='tag', 34 | options={'ordering': ('name',)}, 35 | ), 36 | migrations.AlterField( 37 | model_name='correspondent', 38 | name='slug', 39 | field=models.SlugField(blank=True, editable=False), 40 | ), 41 | migrations.AlterField( 42 | model_name='document', 43 | name='file_type', 44 | field=models.CharField(choices=[('pdf', 'PDF'), ('png', 'PNG'), ('jpg', 'JPG'), ('gif', 'GIF'), ('tiff', 'TIFF'), ('txt', 'TXT'), ('csv', 'CSV'), ('md', 'MD')], editable=False, max_length=4), 45 | ), 46 | migrations.AlterField( 47 | model_name='tag', 48 | name='slug', 49 | field=models.SlugField(blank=True, editable=False), 50 | ), 51 | migrations.RunPython(re_slug_all_the_things, migrations.RunPython.noop) 52 | ] 53 | -------------------------------------------------------------------------------- /src/documents/migrations/0023_document_current_filename.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.10 on 2019-04-26 18:57 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | def set_filename(apps, schema_editor): 7 | Document = apps.get_model("documents", "Document") 8 | for doc in Document.objects.all(): 9 | file_name = "{:07}.{}".format(doc.pk, doc.file_type) 10 | if doc.storage_type == "gpg": 11 | file_name += ".gpg" 12 | 13 | # Set filename 14 | doc.filename = file_name 15 | 16 | # Save document 17 | doc.save() 18 | 19 | 20 | class Migration(migrations.Migration): 21 | 22 | dependencies = [ 23 | ('documents', '0022_auto_20181007_1420'), 24 | ] 25 | 26 | operations = [ 27 | migrations.AddField( 28 | model_name='document', 29 | name='filename', 30 | field=models.FilePathField(default=None, 31 | null=True, 32 | editable=False, 33 | help_text='Current filename in storage', 34 | max_length=256), 35 | ), 36 | migrations.RunPython(set_filename) 37 | ] 38 | -------------------------------------------------------------------------------- /src/documents/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-paperless-project/paperless/9b0063c9731f7c5f65b1852cb8caff97f5e40ba4/src/documents/migrations/__init__.py -------------------------------------------------------------------------------- /src/documents/mixins.py: -------------------------------------------------------------------------------- 1 | class Renderable: 2 | """ 3 | A handy mixin to make it easier/cleaner to print output based on a 4 | verbosity value. 5 | """ 6 | 7 | def _render(self, text, verbosity): 8 | if self.verbosity >= verbosity: 9 | print(text) 10 | -------------------------------------------------------------------------------- /src/documents/serialisers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | from .models import Correspondent, Tag, Document, Log 4 | 5 | 6 | class CorrespondentSerializer(serializers.HyperlinkedModelSerializer): 7 | 8 | class Meta: 9 | model = Correspondent 10 | fields = ( 11 | "id", 12 | "slug", 13 | "name", 14 | "match", 15 | "matching_algorithm", 16 | "is_insensitive" 17 | ) 18 | 19 | 20 | class TagSerializer(serializers.HyperlinkedModelSerializer): 21 | 22 | class Meta: 23 | model = Tag 24 | fields = ( 25 | "id", 26 | "slug", 27 | "name", 28 | "colour", 29 | "match", 30 | "matching_algorithm", 31 | "is_insensitive" 32 | ) 33 | 34 | 35 | class CorrespondentField(serializers.HyperlinkedRelatedField): 36 | def get_queryset(self): 37 | return Correspondent.objects.all() 38 | 39 | 40 | class TagsField(serializers.HyperlinkedRelatedField): 41 | def get_queryset(self): 42 | return Tag.objects.all() 43 | 44 | 45 | class DocumentSerializer(serializers.ModelSerializer): 46 | 47 | correspondent = CorrespondentField( 48 | view_name="drf:correspondent-detail", allow_null=True) 49 | tags = TagsField(view_name="drf:tag-detail", many=True) 50 | 51 | class Meta: 52 | model = Document 53 | fields = ( 54 | "id", 55 | "correspondent", 56 | "title", 57 | "content", 58 | "file_type", 59 | "tags", 60 | "checksum", 61 | "created", 62 | "modified", 63 | "added", 64 | "file_name", 65 | "download_url", 66 | "thumbnail_url", 67 | ) 68 | 69 | 70 | class LogSerializer(serializers.ModelSerializer): 71 | 72 | time = serializers.DateTimeField() 73 | messages = serializers.CharField() 74 | 75 | class Meta: 76 | model = Log 77 | fields = ( 78 | "time", 79 | "messages" 80 | ) 81 | -------------------------------------------------------------------------------- /src/documents/settings.py: -------------------------------------------------------------------------------- 1 | # Defines the names of file/thumbnail for the manifest 2 | # for exporting/importing commands 3 | EXPORTER_FILE_NAME = "__exported_file_name__" 4 | EXPORTER_THUMBNAIL_NAME = "__exported_thumbnail_name__" 5 | -------------------------------------------------------------------------------- /src/documents/signals/__init__.py: -------------------------------------------------------------------------------- 1 | from django.dispatch import Signal 2 | 3 | document_consumption_started = Signal(providing_args=["filename"]) 4 | document_consumption_finished = Signal(providing_args=["document"]) 5 | document_consumer_declaration = Signal(providing_args=[]) 6 | -------------------------------------------------------------------------------- /src/documents/static/documents/img/gif.png: -------------------------------------------------------------------------------- 1 | image.png -------------------------------------------------------------------------------- /src/documents/static/documents/img/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-paperless-project/paperless/9b0063c9731f7c5f65b1852cb8caff97f5e40ba4/src/documents/static/documents/img/image.png -------------------------------------------------------------------------------- /src/documents/static/documents/img/jpg.png: -------------------------------------------------------------------------------- 1 | image.png -------------------------------------------------------------------------------- /src/documents/static/documents/img/pdf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-paperless-project/paperless/9b0063c9731f7c5f65b1852cb8caff97f5e40ba4/src/documents/static/documents/img/pdf.png -------------------------------------------------------------------------------- /src/documents/static/documents/img/png.png: -------------------------------------------------------------------------------- 1 | image.png -------------------------------------------------------------------------------- /src/documents/static/documents/img/tiff.png: -------------------------------------------------------------------------------- 1 | image.png -------------------------------------------------------------------------------- /src/documents/static/js/colours.js: -------------------------------------------------------------------------------- 1 | // The following jQuery snippet will add a small square next to the selection 2 | // drop-down on the `Add tag` page that will update to show the selected tag 3 | // color as the drop-down value is changed. 4 | 5 | django.jQuery(document).ready(function(){ 6 | 7 | if (django.jQuery("#id_colour").length) { 8 | 9 | let colour; 10 | let colour_num; 11 | 12 | colour_num = django.jQuery("#id_colour").val() - 1; 13 | colour = django.jQuery('#id_colour')[0][colour_num].text; 14 | django.jQuery('#id_colour').after('
'); 15 | 16 | django.jQuery('.colour_square').css({ 17 | 'float': 'left', 18 | 'width': '20px', 19 | 'height': '20px', 20 | 'margin': '5px', 21 | 'border': '1px solid rgba(0, 0, 0, .2)', 22 | 'background': colour 23 | }); 24 | 25 | django.jQuery('#id_colour').change(function () { 26 | colour_num = django.jQuery("#id_colour").val() - 1; 27 | colour = django.jQuery('#id_colour')[0][colour_num].text; 28 | django.jQuery('.colour_square').css({'background': colour}); 29 | }); 30 | 31 | } else if (django.jQuery("select[id*='colour']").length) { 32 | 33 | django.jQuery('select[id*="-colour"]').each(function (index, element) { 34 | let id; 35 | let loop_colour_num; 36 | let loop_colour; 37 | 38 | id = "colour_square_" + index; 39 | django.jQuery(element).after('
'); 40 | 41 | loop_colour_num = django.jQuery(element).val() - 1; 42 | loop_colour = django.jQuery(element)[0][loop_colour_num].text; 43 | 44 | django.jQuery("").appendTo("head"); 52 | django.jQuery('#' + id).css({'background': loop_colour}); 53 | 54 | console.log(id, loop_colour_num, loop_colour); 55 | 56 | django.jQuery(element).change(function () { 57 | loop_colour_num = django.jQuery(element).val() - 1; 58 | loop_colour = django.jQuery(element)[0][loop_colour_num].text; 59 | django.jQuery('#' + id).css({'background': loop_colour}); 60 | console.log('#' + id, loop_colour) 61 | }); 62 | }) 63 | 64 | } 65 | 66 | }); 67 | -------------------------------------------------------------------------------- /src/documents/static/paperless.css: -------------------------------------------------------------------------------- 1 | th.column-document, 2 | td.field-document { 3 | text-align: center; 4 | } 5 | 6 | td a.tag { 7 | padding: 0 0.5em; 8 | color: #ffffff; 9 | border-radius: 0.2em; 10 | margin: 1px; 11 | display: inline-block; 12 | } 13 | 14 | #result_list th.column-note { 15 | text-align: right; 16 | } 17 | #result_list td.field-note { 18 | text-align: right; 19 | } 20 | #result_list td textarea { 21 | width: 90%; 22 | height: 5em; 23 | } -------------------------------------------------------------------------------- /src/documents/templates/admin/base_site.html: -------------------------------------------------------------------------------- 1 | {% extends 'admin/base_site.html' %} 2 | 3 | {# NOTE: This should probably be extending base.html. See CSS comment below details. #} 4 | 5 | 6 | {% load static %} 7 | {% load custom_css from customisation %} 8 | {% load custom_js from customisation %} 9 | 10 | 11 | {% block extrahead %} 12 | 13 | 53 | {% endblock %} 54 | 55 | 56 | {% block branding %} 57 |

58 | Paperless 59 |

60 | {% endblock %} 61 | 62 | 63 | {% block blockbots %} 64 | 65 | {% comment %} 66 | This really should be extending `extrastyle`, but the the 67 | django-flat-responsive package decided that it wanted to put its CSS in 68 | this block, so to make sure that overrides are in fact overriding 69 | everything else, we have to do the Wrong Thing here. 70 | 71 | Once we switch to Django 2.x and drop django-flat-responsive, we should 72 | switch this to `extrastyle` where it should be. 73 | {% endcomment %} 74 | 75 | {{ block.super }} 76 | 77 | {% custom_css %} 78 | 79 | {% endblock blockbots %} 80 | 81 | 82 | {% block footer %} 83 | 84 | {% comment %} 85 | The Django admin doesn't have a block for Javascript you'd want placed in 86 | the footer, so we have to use this one instead. 87 | {% endcomment %} 88 | 89 | {{ block.super }} 90 | 91 | {% custom_js %} 92 | 93 | {% endblock footer %} 94 | -------------------------------------------------------------------------------- /src/documents/templates/admin/documents/document/change_form.html: -------------------------------------------------------------------------------- 1 | {% extends 'admin/change_form.html' %} 2 | 3 | {% block content %} 4 | 5 | {{ block.super }} 6 |
7 |

Preview

8 | 9 |
10 | 11 | {% if next_object %} 12 | 20 | {% endif %} 21 | 22 | {% endblock content %} 23 | 24 | {% block extrastyle %} 25 | {{ block.super }} 26 | 53 | {% endblock %} 54 | 55 | {% block footer %} 56 | 57 | {{ block.super }} 58 | 59 | {# Hack to force Django to make the created date a date input rather than `text` (the default) #} 60 | 63 | 64 | {% endblock footer %} 65 | -------------------------------------------------------------------------------- /src/documents/templates/admin/documents/document/change_list.html: -------------------------------------------------------------------------------- 1 | {% extends 'admin/change_list.html' %} 2 | 3 | 4 | {% load admin_actions from admin_list%} 5 | {% load result_list from hacks %} 6 | 7 | 8 | {% block result_list %} 9 | {% if action_form and actions_on_top and cl.show_admin_actions %}{% admin_actions %}{% endif %} 10 | {% result_list cl %} 11 | {% if action_form and actions_on_bottom and cl.show_admin_actions %}{% admin_actions %}{% endif %} 12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /src/documents/templates/admin/documents/document/select_object.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base_site.html" %} 2 | 3 | 4 | {% load i18n l10n admin_urls static %} 5 | {% load staticfiles %} 6 | 7 | 8 | {% block extrahead %} 9 | {{ block.super }} 10 | {{ media }} 11 | 12 | {% endblock %} 13 | 14 | 15 | {% block bodyclass %}{{ block.super }} app-{{ opts.app_label }} model-{{ opts.model_name }} delete-confirmation delete-selected-confirmation{% endblock %} 16 | 17 | 18 | {% block breadcrumbs %} 19 | 25 | {% endblock %} 26 | 27 | {% block content %} 28 |

Please select the {{itemname}}.

29 |
{% csrf_token %} 30 |
31 | {% for obj in queryset %} 32 | 33 | {% endfor %} 34 |

35 | 40 |

41 | 42 | 43 | 44 |

45 | 46 | {% trans "Go back" %} 47 |

48 |
49 |
50 | {% endblock %} 51 | -------------------------------------------------------------------------------- /src/documents/templates/admin/index.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/index.html" %} 2 | 3 | 4 | {% load i18n static %} 5 | 6 | 7 | {# This block adds a search form on the admin start page and on the module start page so that #} 8 | {# the user can quickly search for documents #} 9 | {% block pretitle %} 10 |
11 |

{% trans 'Search documents' %}

12 | 13 |
20 |
21 |
22 | {% endblock %} 23 | 24 | 25 | {# This whole block is here just to override the `get_admin_log` line so #} 26 | {# that the log entries aren't limited to the current user #} 27 | {% block sidebar %} 28 | 57 | {% endblock %} 58 | -------------------------------------------------------------------------------- /src/documents/templates/documents/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Paperless 6 | 7 | 8 | 9 | {# One day someone (maybe even myself) is going to write a proper web front-end for Paperless, and this is where it'll start. #} 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/documents/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-paperless-project/paperless/9b0063c9731f7c5f65b1852cb8caff97f5e40ba4/src/documents/templatetags/__init__.py -------------------------------------------------------------------------------- /src/documents/templatetags/customisation.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from django import template 4 | from django.conf import settings 5 | from django.utils.safestring import mark_safe 6 | 7 | register = template.Library() 8 | 9 | 10 | @register.simple_tag() 11 | def custom_css(): 12 | theme_path = os.path.join( 13 | settings.MEDIA_ROOT, 14 | "overrides.css" 15 | ) 16 | if os.path.exists(theme_path): 17 | return mark_safe( 18 | ''.format( 19 | os.path.join(settings.MEDIA_URL, "overrides.css") 20 | ) 21 | ) 22 | return "" 23 | 24 | 25 | @register.simple_tag() 26 | def custom_js(): 27 | theme_path = os.path.join( 28 | settings.MEDIA_ROOT, 29 | "overrides.js" 30 | ) 31 | if os.path.exists(theme_path): 32 | return mark_safe( 33 | ''.format( 34 | os.path.join(settings.MEDIA_URL, "overrides.js") 35 | ) 36 | ) 37 | return "" 38 | -------------------------------------------------------------------------------- /src/documents/templatetags/hacks.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from django.contrib.admin.templatetags.admin_list import ( 4 | result_headers, 5 | result_hidden_fields, 6 | results 7 | ) 8 | from django.template import Library 9 | 10 | 11 | EXTRACT_URL = re.compile(r'href="(.*?)"') 12 | 13 | register = Library() 14 | 15 | 16 | @register.inclusion_tag("admin/documents/document/change_list_results.html") 17 | def result_list(cl): 18 | """ 19 | Copy/pasted from django.contrib.admin.templatetags.admin_list just so I can 20 | modify the value passed to `.inclusion_tag()` in the decorator here. There 21 | must be a cleaner way... right? 22 | """ 23 | headers = list(result_headers(cl)) 24 | num_sorted_fields = 0 25 | for h in headers: 26 | if h['sortable'] and h['sorted']: 27 | num_sorted_fields += 1 28 | return {'cl': cl, 29 | 'result_hidden_fields': list(result_hidden_fields(cl)), 30 | 'result_headers': headers, 31 | 'num_sorted_fields': num_sorted_fields, 32 | 'results': map(add_doc_edit_url, results(cl))} 33 | 34 | 35 | def add_doc_edit_url(result): 36 | """ 37 | Make the document edit URL accessible to the view as a separate item 38 | """ 39 | title = result[1] 40 | match = re.search(EXTRACT_URL, title) 41 | edit_doc_url = match.group(1) 42 | result.append(edit_doc_url) 43 | return result 44 | -------------------------------------------------------------------------------- /src/documents/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-paperless-project/paperless/9b0063c9731f7c5f65b1852cb8caff97f5e40ba4/src/documents/tests/__init__.py -------------------------------------------------------------------------------- /src/documents/tests/factories.py: -------------------------------------------------------------------------------- 1 | import factory 2 | 3 | from ..models import Document, Correspondent 4 | 5 | 6 | class CorrespondentFactory(factory.DjangoModelFactory): 7 | 8 | class Meta: 9 | model = Correspondent 10 | 11 | name = factory.Faker("name") 12 | 13 | 14 | class DocumentFactory(factory.DjangoModelFactory): 15 | 16 | class Meta: 17 | model = Document 18 | -------------------------------------------------------------------------------- /src/documents/tests/samples/letter.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-paperless-project/paperless/9b0063c9731f7c5f65b1852cb8caff97f5e40ba4/src/documents/tests/samples/letter.pdf -------------------------------------------------------------------------------- /src/documents/tests/test_checks.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from django.test import TestCase 4 | 5 | from ..checks import changed_password_check 6 | from ..models import Document 7 | from .factories import DocumentFactory 8 | 9 | 10 | class ChecksTestCase(TestCase): 11 | 12 | def test_changed_password_check_empty_db(self): 13 | self.assertEqual(changed_password_check(None), []) 14 | 15 | def test_changed_password_check_no_encryption(self): 16 | DocumentFactory.create(storage_type=Document.STORAGE_TYPE_UNENCRYPTED) 17 | self.assertEqual(changed_password_check(None), []) 18 | 19 | @unittest.skip("I don't know how to test this") 20 | def test_changed_password_check_gpg_encryption_with_good_password(self): 21 | pass 22 | 23 | @unittest.skip("I don't know how to test this") 24 | def test_changed_password_check_fail(self): 25 | pass 26 | -------------------------------------------------------------------------------- /src/documents/tests/test_document_model.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | 3 | from django.test import TestCase 4 | 5 | from ..models import Document, Correspondent 6 | 7 | 8 | class TestDocument(TestCase): 9 | 10 | def test_file_deletion(self): 11 | document = Document.objects.create( 12 | correspondent=Correspondent.objects.create(name="Test0"), 13 | title="Title", 14 | content="content", 15 | checksum="checksum", 16 | ) 17 | file_path = document.source_path 18 | thumb_path = document.thumbnail_path 19 | with mock.patch("documents.signals.handlers.os.unlink") as mock_unlink: 20 | document.delete() 21 | mock_unlink.assert_any_call(file_path) 22 | mock_unlink.assert_any_call(thumb_path) 23 | self.assertEqual(mock_unlink.call_count, 2) 24 | -------------------------------------------------------------------------------- /src/documents/tests/test_importer.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import CommandError 2 | from django.test import TestCase 3 | 4 | from ..management.commands.document_importer import Command 5 | 6 | from documents.settings import EXPORTER_FILE_NAME 7 | 8 | 9 | class TestImporter(TestCase): 10 | 11 | def __init__(self, *args, **kwargs): 12 | TestCase.__init__(self, *args, **kwargs) 13 | 14 | def test_check_manifest_exists(self): 15 | cmd = Command() 16 | self.assertRaises( 17 | CommandError, cmd._check_manifest_exists, "/tmp/manifest.json") 18 | 19 | def test_check_manifest(self): 20 | 21 | cmd = Command() 22 | cmd.source = "/tmp" 23 | 24 | cmd.manifest = [{"model": "documents.document"}] 25 | with self.assertRaises(CommandError) as cm: 26 | cmd._check_manifest() 27 | self.assertTrue( 28 | 'The manifest file contains a record' in str(cm.exception)) 29 | 30 | cmd.manifest = [{ 31 | "model": "documents.document", 32 | EXPORTER_FILE_NAME: "noexist.pdf" 33 | }] 34 | # self.assertRaises(CommandError, cmd._check_manifest) 35 | with self.assertRaises(CommandError) as cm: 36 | cmd._check_manifest() 37 | self.assertTrue( 38 | 'The manifest file refers to "noexist.pdf"' in str(cm.exception)) 39 | -------------------------------------------------------------------------------- /src/documents/tests/test_logger.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import uuid 3 | 4 | from unittest import mock 5 | 6 | from django.test import TestCase 7 | 8 | from ..models import Log 9 | 10 | 11 | class TestPaperlessLog(TestCase): 12 | 13 | def __init__(self, *args, **kwargs): 14 | TestCase.__init__(self, *args, **kwargs) 15 | self.logger = logging.getLogger( 16 | "documents.management.commands.document_consumer") 17 | 18 | def test_that_it_saves_at_all(self): 19 | 20 | kw = {"group": uuid.uuid4()} 21 | 22 | self.assertEqual(Log.objects.all().count(), 0) 23 | 24 | with mock.patch("logging.StreamHandler.emit") as __: 25 | 26 | # Debug messages are ignored by default 27 | self.logger.debug("This is a debugging message", extra=kw) 28 | self.assertEqual(Log.objects.all().count(), 0) 29 | 30 | self.logger.info("This is an informational message", extra=kw) 31 | self.assertEqual(Log.objects.all().count(), 1) 32 | 33 | self.logger.warning("This is an warning message", extra=kw) 34 | self.assertEqual(Log.objects.all().count(), 2) 35 | 36 | self.logger.error("This is an error message", extra=kw) 37 | self.assertEqual(Log.objects.all().count(), 3) 38 | 39 | self.logger.critical("This is a critical message", extra=kw) 40 | self.assertEqual(Log.objects.all().count(), 4) 41 | 42 | def test_groups(self): 43 | 44 | kw1 = {"group": uuid.uuid4()} 45 | kw2 = {"group": uuid.uuid4()} 46 | 47 | self.assertEqual(Log.objects.all().count(), 0) 48 | 49 | with mock.patch("logging.StreamHandler.emit") as __: 50 | 51 | # Debug messages are ignored by default 52 | self.logger.debug("This is a debugging message", extra=kw1) 53 | self.assertEqual(Log.objects.all().count(), 0) 54 | 55 | self.logger.info("This is an informational message", extra=kw2) 56 | self.assertEqual(Log.objects.all().count(), 1) 57 | self.assertEqual(Log.objects.filter(group=kw2["group"]).count(), 1) 58 | 59 | self.logger.warning("This is an warning message", extra=kw1) 60 | self.assertEqual(Log.objects.all().count(), 2) 61 | self.assertEqual(Log.objects.filter(group=kw1["group"]).count(), 1) 62 | 63 | self.logger.error("This is an error message", extra=kw2) 64 | self.assertEqual(Log.objects.all().count(), 3) 65 | self.assertEqual(Log.objects.filter(group=kw2["group"]).count(), 2) 66 | 67 | self.logger.critical("This is a critical message", extra=kw1) 68 | self.assertEqual(Log.objects.all().count(), 4) 69 | self.assertEqual(Log.objects.filter(group=kw1["group"]).count(), 2) 70 | 71 | def test_groupped_query(self): 72 | 73 | kw = {"group": uuid.uuid4()} 74 | with mock.patch("logging.StreamHandler.emit") as __: 75 | self.logger.info("Message 0", extra=kw) 76 | self.logger.info("Message 1", extra=kw) 77 | self.logger.info("Message 2", extra=kw) 78 | self.logger.info("Message 3", extra=kw) 79 | 80 | self.assertEqual(Log.objects.all().by_group().count(), 1) 81 | self.assertEqual( 82 | Log.objects.all().by_group()[0]["messages"], 83 | "Message 0\nMessage 1\nMessage 2\nMessage 3" 84 | ) 85 | -------------------------------------------------------------------------------- /src/documents/tests/test_mail.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import os 3 | import magic 4 | 5 | from hashlib import md5 6 | from unittest import mock 7 | 8 | from django.conf import settings 9 | from django.test import TestCase 10 | 11 | from ..mail import Message, Attachment 12 | 13 | 14 | class TestMessage(TestCase): 15 | 16 | def __init__(self, *args, **kwargs): 17 | 18 | TestCase.__init__(self, *args, **kwargs) 19 | self.sample = os.path.join( 20 | settings.BASE_DIR, 21 | "documents", 22 | "tests", 23 | "samples", 24 | "mail.txt" 25 | ) 26 | 27 | def test_init(self): 28 | 29 | with open(self.sample, "rb") as f: 30 | 31 | with mock.patch("logging.StreamHandler.emit") as __: 32 | message = Message(f.read()) 33 | 34 | self.assertTrue(message) 35 | self.assertEqual(message.subject, "Test 0") 36 | 37 | data = message.attachment.read() 38 | 39 | self.assertEqual( 40 | md5(data).hexdigest(), "7c89655f9e9eb7dd8cde8568e8115d59") 41 | 42 | self.assertEqual( 43 | message.attachment.content_type, "application/pdf") 44 | with magic.Magic(flags=magic.MAGIC_MIME_TYPE) as m: 45 | self.assertEqual(m.id_buffer(data), "application/pdf") 46 | 47 | 48 | class TestInlineMessage(TestCase): 49 | 50 | def __init__(self, *args, **kwargs): 51 | 52 | TestCase.__init__(self, *args, **kwargs) 53 | self.sample = os.path.join( 54 | settings.BASE_DIR, 55 | "documents", 56 | "tests", 57 | "samples", 58 | "inline_mail.txt" 59 | ) 60 | 61 | def test_init(self): 62 | 63 | with open(self.sample, "rb") as f: 64 | 65 | with mock.patch("logging.StreamHandler.emit") as __: 66 | message = Message(f.read()) 67 | 68 | self.assertTrue(message) 69 | self.assertEqual(message.subject, "Paperless Inline Image") 70 | 71 | data = message.attachment.read() 72 | 73 | self.assertEqual( 74 | md5(data).hexdigest(), "30c00a7b42913e65f7fdb0be40b9eef3") 75 | 76 | self.assertEqual( 77 | message.attachment.content_type, "image/png") 78 | with magic.Magic(flags=magic.MAGIC_MIME_TYPE) as m: 79 | self.assertEqual(m.id_buffer(data), "image/png") 80 | 81 | 82 | class TestAttachment(TestCase): 83 | 84 | def test_init(self): 85 | data = base64.encodebytes(b"0") 86 | self.assertEqual(Attachment(data, "application/pdf").suffix, "pdf") 87 | self.assertEqual(Attachment(data, "image/png").suffix, "png") 88 | self.assertEqual(Attachment(data, "image/jpeg").suffix, "jpeg") 89 | self.assertEqual(Attachment(data, "image/gif").suffix, "gif") 90 | self.assertEqual(Attachment(data, "image/tiff").suffix, "tiff") 91 | self.assertEqual(Attachment(data, "image/png").read(), data) 92 | -------------------------------------------------------------------------------- /src/documents/tests/test_models.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | from ..models import Document, Correspondent 4 | from .factories import DocumentFactory, CorrespondentFactory 5 | 6 | 7 | class CorrespondentTestCase(TestCase): 8 | 9 | def test___str__(self): 10 | for s in ("test", "οχι", "test with fun_charÅc'\"terß"): 11 | correspondent = CorrespondentFactory.create(name=s) 12 | self.assertEqual(str(correspondent), s) 13 | 14 | 15 | class DocumentTestCase(TestCase): 16 | 17 | def test_correspondent_deletion_does_not_cascade(self): 18 | 19 | self.assertEqual(Correspondent.objects.all().count(), 0) 20 | correspondent = CorrespondentFactory.create() 21 | self.assertEqual(Correspondent.objects.all().count(), 1) 22 | 23 | self.assertEqual(Document.objects.all().count(), 0) 24 | DocumentFactory.create(correspondent=correspondent) 25 | self.assertEqual(Document.objects.all().count(), 1) 26 | self.assertIsNotNone(Document.objects.all().first().correspondent) 27 | 28 | correspondent.delete() 29 | self.assertEqual(Correspondent.objects.all().count(), 0) 30 | self.assertEqual(Document.objects.all().count(), 1) 31 | self.assertIsNone(Document.objects.all().first().correspondent) 32 | -------------------------------------------------------------------------------- /src/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | 7 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "paperless.settings") 8 | 9 | from django.core.management import execute_from_command_line 10 | 11 | execute_from_command_line(sys.argv) 12 | -------------------------------------------------------------------------------- /src/paperless/__init__.py: -------------------------------------------------------------------------------- 1 | from .checks import paths_check, binaries_check 2 | -------------------------------------------------------------------------------- /src/paperless/checks.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | 4 | from django.conf import settings 5 | from django.core.checks import Error, Warning, register 6 | 7 | 8 | @register() 9 | def paths_check(app_configs, **kwargs): 10 | """ 11 | Check the various paths for existence, readability and writeability 12 | """ 13 | 14 | check_messages = [] 15 | 16 | exists_message = "{} is set but doesn't exist." 17 | exists_hint = "Create a directory at {}" 18 | writeable_message = "{} is not writeable" 19 | writeable_hint = ( 20 | "Set the permissions of {} to be writeable by the user running the " 21 | "Paperless services" 22 | ) 23 | 24 | directory = os.getenv("PAPERLESS_DBDIR") 25 | if directory: 26 | if not os.path.exists(directory): 27 | check_messages.append(Error( 28 | exists_message.format("PAPERLESS_DBDIR"), 29 | exists_hint.format(directory) 30 | )) 31 | if not check_messages: 32 | if not os.access(directory, os.W_OK | os.X_OK): 33 | check_messages.append(Error( 34 | writeable_message.format("PAPERLESS_DBDIR"), 35 | writeable_hint.format(directory) 36 | )) 37 | 38 | directory = os.getenv("PAPERLESS_MEDIADIR") 39 | if directory: 40 | if not os.path.exists(directory): 41 | check_messages.append(Error( 42 | exists_message.format("PAPERLESS_MEDIADIR"), 43 | exists_hint.format(directory) 44 | )) 45 | if not check_messages: 46 | if not os.access(directory, os.W_OK | os.X_OK): 47 | check_messages.append(Error( 48 | writeable_message.format("PAPERLESS_MEDIADIR"), 49 | writeable_hint.format(directory) 50 | )) 51 | 52 | directory = os.getenv("PAPERLESS_STATICDIR") 53 | if directory: 54 | if not os.path.exists(directory): 55 | check_messages.append(Error( 56 | exists_message.format("PAPERLESS_STATICDIR"), 57 | exists_hint.format(directory) 58 | )) 59 | if not check_messages: 60 | if not os.access(directory, os.W_OK | os.X_OK): 61 | check_messages.append(Error( 62 | writeable_message.format("PAPERLESS_STATICDIR"), 63 | writeable_hint.format(directory) 64 | )) 65 | 66 | return check_messages 67 | 68 | 69 | @register() 70 | def binaries_check(app_configs, **kwargs): 71 | """ 72 | Paperless requires the existence of a few binaries, so we do some checks 73 | for those here. 74 | """ 75 | 76 | error = "Paperless can't find {}. Without it, consumption is impossible." 77 | hint = "Either it's not in your ${PATH} or it's not installed." 78 | 79 | binaries = ( 80 | settings.CONVERT_BINARY, 81 | settings.OPTIPNG_BINARY, 82 | settings.UNPAPER_BINARY, 83 | "tesseract" 84 | ) 85 | 86 | check_messages = [] 87 | for binary in binaries: 88 | if shutil.which(binary) is None: 89 | check_messages.append(Warning(error.format(binary), hint)) 90 | 91 | return check_messages 92 | -------------------------------------------------------------------------------- /src/paperless/db.py: -------------------------------------------------------------------------------- 1 | import gnupg 2 | 3 | from django.conf import settings 4 | 5 | 6 | class GnuPG: 7 | """ 8 | A handy singleton to use when handling encrypted files. 9 | """ 10 | 11 | gpg = gnupg.GPG(gnupghome=settings.GNUPG_HOME) 12 | 13 | @classmethod 14 | def decrypted(cls, file_handle, passphrase=None): 15 | 16 | if not passphrase: 17 | passphrase = settings.PASSPHRASE 18 | 19 | return cls.gpg.decrypt_file(file_handle, passphrase=passphrase).data 20 | 21 | @classmethod 22 | def encrypted(cls, file_handle, passphrase=None): 23 | 24 | if not passphrase: 25 | passphrase = settings.PASSPHRASE 26 | 27 | return cls.gpg.encrypt_file( 28 | file_handle, 29 | recipients=None, 30 | passphrase=passphrase, 31 | symmetric=True 32 | ).data 33 | -------------------------------------------------------------------------------- /src/paperless/middleware.py: -------------------------------------------------------------------------------- 1 | from django.utils.deprecation import MiddlewareMixin 2 | from .models import User 3 | 4 | 5 | class Middleware(MiddlewareMixin): 6 | """ 7 | This is a dummy authentication middleware class that creates what 8 | is roughly an Anonymous authenticated user so we can disable login 9 | and not interfere with existing user ID's. It's only used if 10 | login is disabled in paperless.conf (default is to require login) 11 | """ 12 | 13 | def process_request(self, request): 14 | request.user = User() 15 | -------------------------------------------------------------------------------- /src/paperless/mixins.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.mixins import AccessMixin 2 | from django.contrib.auth import authenticate, login 3 | import base64 4 | 5 | 6 | class SessionOrBasicAuthMixin(AccessMixin): 7 | """ 8 | Session or Basic Authentication mixin for Django. 9 | It determines if the requester is already logged in or if they have 10 | provided proper http-authorization and returning the view if all goes 11 | well, otherwise responding with a 401. 12 | 13 | Base for mixin found here: https://djangosnippets.org/snippets/3073/ 14 | """ 15 | 16 | def dispatch(self, request, *args, **kwargs): 17 | 18 | # check if user is authenticated via the session 19 | if request.user.is_authenticated: 20 | 21 | # Already logged in, just return the view. 22 | return super(SessionOrBasicAuthMixin, self).dispatch( 23 | request, *args, **kwargs 24 | ) 25 | 26 | # apparently not authenticated via session, maybe via HTTP Basic? 27 | if 'HTTP_AUTHORIZATION' in request.META: 28 | auth = request.META['HTTP_AUTHORIZATION'].split() 29 | if len(auth) == 2: 30 | # NOTE: Support for only basic authentication 31 | if auth[0].lower() == "basic": 32 | authString = base64.b64decode(auth[1]).decode('utf-8') 33 | uname, passwd = authString.split(':') 34 | user = authenticate(username=uname, password=passwd) 35 | if user is not None: 36 | if user.is_active: 37 | login(request, user) 38 | request.user = user 39 | return super( 40 | SessionOrBasicAuthMixin, self 41 | ).dispatch( 42 | request, *args, **kwargs 43 | ) 44 | 45 | # nope, really not authenticated 46 | return self.handle_no_permission() 47 | -------------------------------------------------------------------------------- /src/paperless/models.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import User as DjangoUser 2 | 3 | 4 | class User: 5 | """ 6 | This is a dummy django User used with our middleware to disable 7 | login authentication if that is configured in paperless.conf 8 | """ 9 | 10 | is_superuser = True 11 | is_active = True 12 | is_staff = True 13 | is_authenticated = True 14 | 15 | @property 16 | def id(self): 17 | return DjangoUser.objects.order_by("pk").first().pk 18 | 19 | @property 20 | def pk(self): 21 | return self.id 22 | 23 | 24 | """ 25 | NOTE: These are here as a hack instead of being in the User definition 26 | NOTE: above due to the way pycodestyle handles lamdbdas. 27 | NOTE: See https://github.com/PyCQA/pycodestyle/issues/379 for more. 28 | """ 29 | 30 | User.has_module_perms = lambda *_: True 31 | User.has_perm = lambda *_: True 32 | -------------------------------------------------------------------------------- /src/paperless/static/paperless/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-paperless-project/paperless/9b0063c9731f7c5f65b1852cb8caff97f5e40ba4/src/paperless/static/paperless/img/favicon.ico -------------------------------------------------------------------------------- /src/paperless/static/paperless/img/logo-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-paperless-project/paperless/9b0063c9731f7c5f65b1852cb8caff97f5e40ba4/src/paperless/static/paperless/img/logo-dark.png -------------------------------------------------------------------------------- /src/paperless/static/paperless/img/logo-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-paperless-project/paperless/9b0063c9731f7c5f65b1852cb8caff97f5e40ba4/src/paperless/static/paperless/img/logo-light.png -------------------------------------------------------------------------------- /src/paperless/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.conf.urls import include, static, url 3 | from django.contrib import admin 4 | from django.urls import reverse_lazy 5 | from django.views.decorators.csrf import csrf_exempt 6 | from django.views.generic import RedirectView 7 | from rest_framework.routers import DefaultRouter 8 | 9 | from paperless.views import FaviconView 10 | from documents.views import ( 11 | CorrespondentViewSet, 12 | DocumentViewSet, 13 | FetchView, 14 | LogViewSet, 15 | PushView, 16 | TagViewSet 17 | ) 18 | from reminders.views import ReminderViewSet 19 | 20 | router = DefaultRouter() 21 | router.register(r"correspondents", CorrespondentViewSet) 22 | router.register(r"documents", DocumentViewSet) 23 | router.register(r"logs", LogViewSet) 24 | router.register(r"reminders", ReminderViewSet) 25 | router.register(r"tags", TagViewSet) 26 | 27 | urlpatterns = [ 28 | 29 | # API 30 | url( 31 | r"^api/auth/", 32 | include( 33 | ('rest_framework.urls', 'rest_framework'), 34 | namespace="rest_framework") 35 | ), 36 | url(r"^api/", include((router.urls, 'drf'), namespace="drf")), 37 | 38 | # File downloads 39 | url( 40 | r"^fetch/(?Pdoc|thumb|preview)/(?P\d+)$", 41 | FetchView.as_view(), 42 | name="fetch" 43 | ), 44 | 45 | # File uploads 46 | url(r"^push$", csrf_exempt(PushView.as_view()), name="push"), 47 | 48 | # Favicon 49 | url(r"^favicon.ico$", FaviconView.as_view(), name="favicon"), 50 | 51 | # The Django admin 52 | url(r"admin/", admin.site.urls), 53 | 54 | # Redirect / to /admin 55 | url(r"^$", RedirectView.as_view( 56 | permanent=True, url=reverse_lazy("admin:index"))), 57 | 58 | ] + static.static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 59 | 60 | # Text in each page's

(and above login form). 61 | admin.site.site_header = 'Paperless' 62 | # Text at the end of each page's . 63 | admin.site.site_title = 'Paperless' 64 | # Text at the top of the admin index page. 65 | admin.site.index_title = 'Paperless administration' 66 | -------------------------------------------------------------------------------- /src/paperless/version.py: -------------------------------------------------------------------------------- 1 | __version__ = (2, 6, 1) 2 | -------------------------------------------------------------------------------- /src/paperless/views.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from django.http import HttpResponse 4 | from django.views.generic import View 5 | from rest_framework.pagination import PageNumberPagination 6 | 7 | 8 | class StandardPagination(PageNumberPagination): 9 | page_size = 25 10 | page_size_query_param = "page-size" 11 | max_page_size = 100000 12 | 13 | 14 | class FaviconView(View): 15 | 16 | def get(self, request, *args, **kwargs): 17 | favicon = os.path.join( 18 | os.path.dirname(__file__), 19 | "static", 20 | "paperless", 21 | "img", 22 | "favicon.ico" 23 | ) 24 | with open(favicon, "rb") as f: 25 | return HttpResponse(f, content_type="image/x-icon") 26 | -------------------------------------------------------------------------------- /src/paperless/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for paperless project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.10/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "paperless.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /src/paperless_tesseract/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-paperless-project/paperless/9b0063c9731f7c5f65b1852cb8caff97f5e40ba4/src/paperless_tesseract/__init__.py -------------------------------------------------------------------------------- /src/paperless_tesseract/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class PaperlessTesseractConfig(AppConfig): 5 | 6 | name = "paperless_tesseract" 7 | 8 | def ready(self): 9 | 10 | from documents.signals import document_consumer_declaration 11 | 12 | from .signals import ConsumerDeclaration 13 | 14 | document_consumer_declaration.connect(ConsumerDeclaration.handle) 15 | 16 | AppConfig.ready(self) 17 | -------------------------------------------------------------------------------- /src/paperless_tesseract/signals.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from .parsers import RasterisedDocumentParser 4 | 5 | 6 | class ConsumerDeclaration: 7 | 8 | MATCHING_FILES = re.compile(r"^.*\.(pdf|jpe?g|gif|png|tiff?|pnm|bmp)$") 9 | 10 | @classmethod 11 | def handle(cls, sender, **kwargs): 12 | return cls.test 13 | 14 | @classmethod 15 | def test(cls, doc): 16 | 17 | if cls.MATCHING_FILES.match(doc.lower()): 18 | return { 19 | "parser": RasterisedDocumentParser, 20 | "weight": 0 21 | } 22 | 23 | return None 24 | -------------------------------------------------------------------------------- /src/paperless_tesseract/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-paperless-project/paperless/9b0063c9731f7c5f65b1852cb8caff97f5e40ba4/src/paperless_tesseract/tests/__init__.py -------------------------------------------------------------------------------- /src/paperless_tesseract/tests/samples/no-text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-paperless-project/paperless/9b0063c9731f7c5f65b1852cb8caff97f5e40ba4/src/paperless_tesseract/tests/samples/no-text.png -------------------------------------------------------------------------------- /src/paperless_tesseract/tests/test_ocr.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pyocr 3 | 4 | from django.test import TestCase, override_settings 5 | from pyocr.libtesseract.tesseract_raw import \ 6 | TesseractError as OtherTesseractError 7 | from tempfile import TemporaryDirectory 8 | from unittest import mock, skipIf 9 | 10 | from ..parsers import image_to_string, strip_excess_whitespace 11 | 12 | 13 | class FakeTesseract(object): 14 | 15 | @staticmethod 16 | def can_detect_orientation(): 17 | return True 18 | 19 | @staticmethod 20 | def detect_orientation(file_handle, lang): 21 | raise OtherTesseractError("arbitrary status", "message") 22 | 23 | @staticmethod 24 | def image_to_string(file_handle, lang): 25 | return "This is test text" 26 | 27 | 28 | class FakePyOcr(object): 29 | 30 | @staticmethod 31 | def get_available_tools(): 32 | return [FakeTesseract] 33 | 34 | 35 | @override_settings(SCRATCH_DIR=os.path.join( 36 | os.path.dirname(__file__), "samples")) 37 | class TestOCR(TestCase): 38 | 39 | text_cases = [ 40 | ("simple string", "simple string"), 41 | ( 42 | "simple newline\n testing string", 43 | "simple newline\ntesting string" 44 | ), 45 | ( 46 | "utf-8 строка с пробелами в конце ", 47 | "utf-8 строка с пробелами в конце" 48 | ) 49 | ] 50 | 51 | TESSERACT_INSTALLED = bool(pyocr.get_available_tools()) 52 | 53 | def test_strip_excess_whitespace(self): 54 | for source, result in self.text_cases: 55 | actual_result = strip_excess_whitespace(source) 56 | self.assertEqual( 57 | result, 58 | actual_result, 59 | "strip_exceess_whitespace({}) != '{}', but '{}'".format( 60 | source, 61 | result, 62 | actual_result 63 | ) 64 | ) 65 | 66 | @skipIf(not TESSERACT_INSTALLED, "Tesseract not installed. Skipping") 67 | @mock.patch("paperless_tesseract.parsers.pyocr", FakePyOcr) 68 | def test_image_to_string_with_text_free_page(self): 69 | """ 70 | This test is sort of silly, since it's really just reproducing an odd 71 | exception thrown by pyocr when it encounters a page with no text. 72 | Actually running this test against an installation of Tesseract results 73 | in a segmentation fault rooted somewhere deep inside pyocr where I 74 | don't care to dig. Regardless, if you run the consumer normally, 75 | text-free pages are now handled correctly so long as we work around 76 | this weird exception. 77 | """ 78 | image_to_string(["no-text.png", "en"]) 79 | -------------------------------------------------------------------------------- /src/paperless_tesseract/tests/test_signals.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | from ..signals import ConsumerDeclaration 4 | 5 | 6 | class SignalsTestCase(TestCase): 7 | 8 | def test_test_handles_various_file_names_true(self): 9 | 10 | prefixes = ( 11 | "doc", "My Document", "Μυ Γρεεκ Δοψθμεντ", "Doc -with - tags", 12 | "A document with a . in it", "Doc with -- in it" 13 | ) 14 | suffixes = ( 15 | "pdf", "jpg", "jpeg", "gif", "png", "tiff", "tif", "pnm", "bmp", 16 | "PDF", "JPG", "JPEG", "GIF", "PNG", "TIFF", "TIF", "PNM", "BMP", 17 | "pDf", "jPg", "jpEg", "gIf", "pNg", "tIff", "tIf", "pNm", "bMp", 18 | ) 19 | 20 | for prefix in prefixes: 21 | for suffix in suffixes: 22 | name = "{}.{}".format(prefix, suffix) 23 | self.assertTrue(ConsumerDeclaration.test(name)) 24 | 25 | def test_test_handles_various_file_names_false(self): 26 | 27 | prefixes = ("doc",) 28 | suffixes = ("txt", "markdown", "",) 29 | 30 | for prefix in prefixes: 31 | for suffix in suffixes: 32 | name = "{}.{}".format(prefix, suffix) 33 | self.assertFalse(ConsumerDeclaration.test(name)) 34 | 35 | self.assertFalse(ConsumerDeclaration.test("")) 36 | self.assertFalse(ConsumerDeclaration.test("doc")) 37 | -------------------------------------------------------------------------------- /src/paperless_text/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-paperless-project/paperless/9b0063c9731f7c5f65b1852cb8caff97f5e40ba4/src/paperless_text/__init__.py -------------------------------------------------------------------------------- /src/paperless_text/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class PaperlessTextConfig(AppConfig): 5 | 6 | name = "paperless_text" 7 | 8 | def ready(self): 9 | 10 | from documents.signals import document_consumer_declaration 11 | 12 | from .signals import ConsumerDeclaration 13 | 14 | document_consumer_declaration.connect(ConsumerDeclaration.handle) 15 | 16 | AppConfig.ready(self) 17 | -------------------------------------------------------------------------------- /src/paperless_text/signals.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from .parsers import TextDocumentParser 4 | 5 | 6 | class ConsumerDeclaration: 7 | 8 | MATCHING_FILES = re.compile(r"^.*\.(te?xt|md|csv)$") 9 | 10 | @classmethod 11 | def handle(cls, sender, **kwargs): 12 | return cls.test 13 | 14 | @classmethod 15 | def test(cls, doc): 16 | 17 | if cls.MATCHING_FILES.match(doc.lower()): 18 | return { 19 | "parser": TextDocumentParser, 20 | "weight": 10 21 | } 22 | 23 | return None 24 | -------------------------------------------------------------------------------- /src/reminders/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-paperless-project/paperless/9b0063c9731f7c5f65b1852cb8caff97f5e40ba4/src/reminders/__init__.py -------------------------------------------------------------------------------- /src/reminders/admin.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.contrib import admin 3 | 4 | from .models import Reminder 5 | 6 | 7 | class ReminderAdmin(admin.ModelAdmin): 8 | 9 | class Media: 10 | css = { 11 | "all": ("paperless.css",) 12 | } 13 | 14 | list_per_page = settings.PAPERLESS_LIST_PER_PAGE 15 | list_display = ("date", "document", "note") 16 | list_filter = ("date",) 17 | list_editable = ("note",) 18 | 19 | 20 | admin.site.register(Reminder, ReminderAdmin) 21 | -------------------------------------------------------------------------------- /src/reminders/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class RemindersConfig(AppConfig): 5 | name = "reminders" 6 | -------------------------------------------------------------------------------- /src/reminders/filters.py: -------------------------------------------------------------------------------- 1 | from django_filters.rest_framework import CharFilter, FilterSet 2 | 3 | from .models import Reminder 4 | 5 | 6 | class ReminderFilterSet(FilterSet): 7 | 8 | class Meta(object): 9 | model = Reminder 10 | fields = { 11 | "document": ["exact"], 12 | "date": ["gt", "lt", "gte", "lte", "exact"], 13 | "note": ["istartswith", "iendswith", "icontains"] 14 | } 15 | -------------------------------------------------------------------------------- /src/reminders/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.5 on 2017-03-25 15:58 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | initial = True 12 | 13 | dependencies = [ 14 | ('documents', '0016_auto_20170325_1558'), 15 | ] 16 | 17 | operations = [ 18 | migrations.CreateModel( 19 | name='Reminder', 20 | fields=[ 21 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 22 | ('date', models.DateTimeField()), 23 | ('note', models.TextField(blank=True)), 24 | ('document', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='documents.Document')), 25 | ], 26 | ), 27 | ] 28 | -------------------------------------------------------------------------------- /src/reminders/migrations/0002_auto_20181007_1420.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.8 on 2018-10-07 14:20 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('reminders', '0001_initial'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='reminder', 16 | name='document', 17 | field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='documents.Document'), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /src/reminders/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-paperless-project/paperless/9b0063c9731f7c5f65b1852cb8caff97f5e40ba4/src/reminders/migrations/__init__.py -------------------------------------------------------------------------------- /src/reminders/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class Reminder(models.Model): 5 | 6 | document = models.ForeignKey( 7 | "documents.Document", on_delete=models.PROTECT) 8 | date = models.DateTimeField() 9 | note = models.TextField(blank=True) 10 | -------------------------------------------------------------------------------- /src/reminders/serialisers.py: -------------------------------------------------------------------------------- 1 | from documents.models import Document 2 | from rest_framework import serializers 3 | 4 | from .models import Reminder 5 | 6 | 7 | class ReminderSerializer(serializers.HyperlinkedModelSerializer): 8 | 9 | document = serializers.HyperlinkedRelatedField( 10 | view_name="drf:document-detail", queryset=Document.objects) 11 | 12 | class Meta(object): 13 | model = Reminder 14 | fields = ("id", "document", "date", "note") 15 | -------------------------------------------------------------------------------- /src/reminders/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /src/reminders/views.py: -------------------------------------------------------------------------------- 1 | from django_filters.rest_framework import DjangoFilterBackend 2 | from rest_framework.filters import OrderingFilter 3 | from rest_framework.permissions import IsAuthenticated 4 | from rest_framework.viewsets import ( 5 | ModelViewSet, 6 | ) 7 | 8 | from .filters import ReminderFilterSet 9 | from .models import Reminder 10 | from .serialisers import ReminderSerializer 11 | from paperless.views import StandardPagination 12 | 13 | 14 | class ReminderViewSet(ModelViewSet): 15 | model = Reminder 16 | queryset = Reminder.objects 17 | serializer_class = ReminderSerializer 18 | pagination_class = StandardPagination 19 | permission_classes = (IsAuthenticated,) 20 | filter_backends = (DjangoFilterBackend, OrderingFilter) 21 | filter_class = ReminderFilterSet 22 | ordering_fields = ("date", "document") 23 | -------------------------------------------------------------------------------- /src/setup.cfg: -------------------------------------------------------------------------------- 1 | [pycodestyle] 2 | exclude = migrations, paperless/settings.py, .tox 3 | 4 | 5 | [tool:pytest] 6 | DJANGO_SETTINGS_MODULE=paperless.settings 7 | addopts = --pythonwarnings=all -n auto 8 | env = 9 | PAPERLESS_PASSPHRASE=THISISNOTASECRET 10 | PAPERLESS_SECRET=paperless 11 | PAPERLESS_EMAIL_SECRET=paperless 12 | 13 | 14 | [coverage:run] 15 | source = 16 | ./ 17 | omit = 18 | */tests 19 | -------------------------------------------------------------------------------- /src/tox.ini: -------------------------------------------------------------------------------- 1 | # Tox (http://tox.testrun.org/) is a tool for running tests 2 | # in multiple virtualenvs. This configuration file will run the 3 | # test suite on all supported python versions. To use it, "pip install tox" 4 | # and then run "tox" from this directory. 5 | 6 | [tox] 7 | skipsdist = True 8 | envlist = py34, py35, py36, py37, pycodestyle, doc 9 | 10 | [testenv] 11 | commands = pytest 12 | deps = -r{toxinidir}/../requirements.txt 13 | 14 | [testenv:pycodestyle] 15 | commands=pycodestyle 16 | deps=pycodestyle 17 | 18 | [testenv:doc] 19 | deps = 20 | -r {toxinidir}/../requirements.txt 21 | commands=sphinx-build -b html ../docs ../docs/_build -W 22 | --------------------------------------------------------------------------------