├── .env.example ├── .github ├── dependabot.yml └── workflows │ └── test.yml ├── .gitignore ├── .pre-commit-config.yaml ├── Dockerfile ├── LICENSE.txt ├── Makefile ├── Procfile.dev ├── README.md ├── bin └── post_compile ├── bucket-policy.json ├── locale └── pt_BR │ └── LC_MESSAGES │ └── django.po ├── manage.py ├── poetry.lock ├── pyproject.toml ├── runtime.txt └── web ├── __init__.py ├── foi_requests ├── __init__.py ├── admin.py ├── apps.py ├── fixtures │ ├── public_bodies_and_esics.json │ └── sample_foi_requests.json ├── forms.py ├── management │ └── commands │ │ └── load_public_bodies_and_esics.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_auto_20180328_1901.py │ ├── 0003_auto_20180709_1644.py │ ├── 0004_auto_20180709_1804.py │ ├── 0005_add_message_contraint_to_avoid_rejections_without_message_20180709_1846.py │ ├── 0006_auto_20180710_1228.py │ ├── 0007_add_constraint_foi_requests_message_can_only_send_approved_messages_20180710_1228.py │ ├── 0008_auto_20180717_1131.py │ ├── 0009_extract_esic_from_publicbody_and_add_location_fields_to_it.py │ ├── 0010_rename_message_title_to_summary.py │ ├── 0011_add_can_publish_field_to_foi_request.py │ ├── 0012_add_public_body_level.py │ ├── 0013_auto_20190814_1521.py │ ├── 0014_foirequest_esic_protocol_password.py │ └── __init__.py ├── models.py ├── tests │ ├── __init__.py │ ├── conftest.py │ ├── test_admin.py │ ├── test_commands.py │ ├── test_forms.py │ ├── test_models.py │ ├── test_utils.py │ └── test_views.py ├── urls.py ├── utils.py └── views.py ├── settings.py ├── settings_test.py ├── static ├── favicon.ico └── web │ ├── images │ └── _logo.png │ └── styles │ ├── .keep │ └── main.scss ├── templates ├── _fluxo_pedidos.svg ├── _logo.svg ├── _logo_fs.svg ├── _search_request.html ├── base.html ├── faq.html ├── foi_requests │ ├── _load_selectize.html │ ├── _message_details.html │ ├── foi_request_new.html │ ├── foirequest_detail.html │ ├── index.html │ └── publicbody_form.html └── index.html ├── urls.py ├── whoami ├── __init__.py ├── apps.py ├── tests │ ├── __init__.py │ └── test_view.py ├── urls.py └── views.py └── wsgi.py /.env.example: -------------------------------------------------------------------------------- 1 | ENV=dev 2 | DEBUG=True 3 | SECRET_KEY=')c*%*n=#oz9*hl@8py6_-pl^s7n%ae=g05+y-z3jx7sc+ua69q' 4 | DATABASE_URL=postgresql://queremossaber:queremossaber@localhost/queremossaber 5 | ENABLE_S3=False 6 | AWS_ACCESS_KEY_ID= 7 | AWS_SECRET_ACCESS_KEY= 8 | AWS_S3_REGION_NAME= 9 | AWS_S3_ENDPOINT_URL= 10 | AWS_STORAGE_BUCKET_NAME= 11 | AWS_LOCATION=pedidos-anonimos/uploads 12 | SESSION_COOKIE_SECURE=False 13 | CSRF_COOKIE_SECURE=False 14 | CSRF_COOKIE_DOMAIN= 15 | # SENTRY_DSN 16 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "poetry" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | permissions: 3 | contents: read 4 | 5 | on: 6 | push: 7 | branches: [ master ] 8 | pull_request: 9 | branches: [ master ] 10 | 11 | jobs: 12 | test: 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | python-version: ["3.12"] 17 | 18 | services: 19 | postgres: 20 | image: postgres:14 21 | env: 22 | POSTGRES_DB: queremossaber 23 | POSTGRES_USER: queremossaber 24 | POSTGRES_PASSWORD: queremossaber 25 | ports: 26 | - 5432:5432 27 | options: >- 28 | --health-cmd pg_isready 29 | --health-interval 10s 30 | --health-timeout 5s 31 | --health-retries 5 32 | 33 | steps: 34 | - uses: actions/checkout@v4 35 | 36 | - name: Set up Python ${{ matrix.python-version }} 37 | uses: actions/setup-python@v3 38 | with: 39 | python-version: ${{ matrix.python-version }} 40 | 41 | - name: Cache dependencies 42 | uses: actions/cache@v3 43 | with: 44 | path: | 45 | ~/.cache/pypoetry 46 | ~/.cache/pre-commit 47 | key: ${{ runner.os }}-py${{ matrix.python-version }}-${{ hashFiles('**/poetry.lock') }}-${{ hashFiles('**/.pre-commit-config.yaml') }} 48 | restore-keys: | 49 | ${{ runner.os }}-py${{ matrix.python-version }}- 50 | 51 | - name: Install dependencies 52 | run: | 53 | python -m pip install --upgrade pip poetry 54 | sudo apt-get update && sudo apt-get install -y gettext 55 | make install 56 | 57 | - name: Copy environment file 58 | run: | 59 | cp .env.example .env 60 | cat .env 61 | 62 | - name: Test 63 | run: make test 64 | 65 | - name: Run migrations and fixtures 66 | run: | 67 | make migrate 68 | make load_fixtures 69 | 70 | - name: Make translations 71 | run: | 72 | make make_translations 73 | make compile_translations 74 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | .static_storage/ 57 | .media/ 58 | local_settings.py 59 | *.sqlite3 60 | upload/ 61 | 62 | # Flask stuff: 63 | instance/ 64 | .webassets-cache 65 | 66 | # Scrapy stuff: 67 | .scrapy 68 | 69 | # Sphinx documentation 70 | docs/_build/ 71 | 72 | # PyBuilder 73 | target/ 74 | 75 | # Jupyter Notebook 76 | .ipynb_checkpoints 77 | 78 | # pyenv 79 | .python-version 80 | 81 | # celery beat schedule file 82 | celerybeat-schedule 83 | 84 | # SageMath parsed files 85 | *.sage.py 86 | 87 | # Environments 88 | .env 89 | .venv 90 | env/ 91 | venv/ 92 | ENV/ 93 | env.bak/ 94 | venv.bak/ 95 | 96 | # Spyder project settings 97 | .spyderproject 98 | .spyproject 99 | 100 | # Rope project settings 101 | .ropeproject 102 | 103 | # mkdocs documentation 104 | /site 105 | 106 | # mypy 107 | .mypy_cache/ 108 | 109 | # Temporary files 110 | .#* 111 | *~ 112 | 113 | # Generated files 114 | web/static/web/styles/*.css 115 | web/static/web/styles/*.css.map 116 | staticfiles/ 117 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/astral-sh/ruff-pre-commit 3 | rev: v0.6.3 4 | hooks: 5 | - id: ruff 6 | args: [ 7 | --fix, 8 | --exit-non-zero-on-fix, 9 | ] 10 | - id: ruff-format 11 | 12 | - repo: https://github.com/pre-commit/pre-commit-hooks 13 | rev: v4.6.0 14 | hooks: 15 | - id: trailing-whitespace 16 | - id: end-of-file-fixer 17 | - id: check-yaml 18 | - id: check-added-large-files 19 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build stage 2 | FROM python:3.12 AS builder 3 | 4 | # Set environment variables 5 | ENV PYTHONDONTWRITEBYTECODE 1 6 | ENV PYTHONUNBUFFERED 1 7 | 8 | # Set the working directory in the container 9 | WORKDIR /app 10 | 11 | # Install system dependencies, including gettext 12 | RUN apt-get update && apt-get install -y \ 13 | gettext \ 14 | && rm -rf /var/lib/apt/lists/* 15 | 16 | # Install Poetry 17 | RUN pip install poetry 18 | 19 | # Copy only pyproject.toml and poetry.lock (if it exists) 20 | COPY pyproject.toml poetry.lock* ./ 21 | 22 | # Install project dependencies 23 | RUN poetry config virtualenvs.create false \ 24 | && poetry install --no-interaction --no-ansi --no-root 25 | 26 | # Copy the project code into the container 27 | COPY . . 28 | 29 | # Ensure the env variables are present -- although they won't be used 30 | # during the build, they need to be set. 31 | COPY .env.example .env 32 | 33 | # Install the project itself 34 | RUN poetry install --no-interaction --no-ansi --no-root 35 | 36 | # Collect static files 37 | RUN make sass 38 | RUN python manage.py collectstatic --noinput 39 | 40 | # Compile translations 41 | RUN python manage.py compilemessages 42 | 43 | # Run stage 44 | FROM python:3.12-slim 45 | 46 | # Set environment variables 47 | ENV PYTHONDONTWRITEBYTECODE 1 48 | ENV PYTHONUNBUFFERED 1 49 | ENV PORT 8000 50 | 51 | # Set the working directory in the container 52 | WORKDIR /app 53 | 54 | # Copy the installed dependencies and project files from the builder stage 55 | COPY --from=builder /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages 56 | COPY --from=builder /usr/local/bin /usr/local/bin 57 | COPY --from=builder /app /app 58 | 59 | # Expose the port the app runs on 60 | EXPOSE ${PORT} 61 | 62 | # Start the application 63 | CMD gunicorn --bind 0.0.0.0:${PORT} web.wsgi:application 64 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Vitor Baptista 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: install help watch_sass sass load_fixtures migrate server create_admin 2 | 3 | help: 4 | @echo 'install: install dependencies' 5 | @echo 'test: run tests' 6 | @echo 'create_admin: create a superuser (admin)' 7 | @echo 'encode_gcloud_credentials' 8 | @echo 'load_fixtures: load database fixtures' 9 | @echo 'migrate: migrate database' 10 | @echo 'sass: compile styles' 11 | @echo 'server: start server' 12 | @echo 'watch_sass: watch changes and compile' 13 | @echo 'make_translations: regenerate translation files' 14 | @echo 'compile_translations: compile translation files' 15 | @echo 'setup_bucket_policy: applies bucket-policy.json policy to s3://queremosaber' 16 | 17 | install: 18 | poetry install --no-root 19 | poetry run pre-commit install 20 | 21 | test: 22 | poetry run pytest 23 | poetry run pre-commit run --all-files 24 | 25 | watch_sass: sass 26 | poetry run watchmedo shell-command --patterns="*.scss" --recursive --command 'make sass' web/static/web/styles 27 | 28 | sass: 29 | poetry run pysassc web/static/web/styles/main.scss web/static/web/styles/main.css --sourcemap 30 | 31 | load_fixtures: 32 | poetry run python manage.py loaddata public_bodies_and_esics sample_foi_requests 33 | 34 | migrate: 35 | poetry run python manage.py migrate 36 | 37 | server: 38 | poetry run honcho -f Procfile.dev start 39 | 40 | create_admin: 41 | poetry run python manage.py createsuperuser 42 | 43 | make_translations: 44 | poetry run django-admin makemessages --all --ignore "env*" 45 | 46 | compile_translations: 47 | # FIXME: This will compile all .po files in the current folder, including in 48 | # `.tox` and `env` 49 | poetry run django-admin compilemessages 50 | 51 | setup_bucket_policy: 52 | s3cmd setpolicy bucket-policy.json s3://queremossaber 53 | -------------------------------------------------------------------------------- /Procfile.dev: -------------------------------------------------------------------------------- 1 | web: python manage.py runserver 2 | sass: make watch_sass 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Queremos Saber 2 | 3 | [![Test](https://github.com/FiquemSabendo/queremossaber/actions/workflows/test.yml/badge.svg)](https://github.com/FiquemSabendo/queremossaber/actions/workflows/test.yml) 4 | 5 | Plataforma que permite o envio de pedidos pela Lei de Acesso à 6 | Informação sem revelar sua identidade. 7 | 8 | ## Instalando 9 | 10 | Assumindo que você já tem o Python e `poetry` instalados, instale as dependências com: 11 | 12 | ``` 13 | make install 14 | ``` 15 | 16 | ## Executando 17 | 18 | 1. Copie o `.env.example` para `.env` e altere as configurações (no mínimo, o 19 | `DATABASE_URL`) 20 | 1. Execute as database migrations com `make migrate` 21 | 1. Carregue as fixtures no seu banco de dados com `make load_fixtures` 22 | 1. Crie um superusuário com `make create_admin` 23 | 1. Execute `make watch_sass` em um terminal separado, para compilar o SASS para 24 | CSS 25 | 1. Rode o servidor com `make server` 26 | 27 | A partir desse momento, você já pode acessar o projeto em 28 | [http://localhost:8000](http://localhost:8000). 29 | 30 | ## Testes 31 | 32 | Para rodar os testes, primeiro se certifique que seu usuário do postgres tenha 33 | permissões para criar um banco de dados e executar comandos no banco de dados. 34 | 35 | ``` 36 | sudo -u postgres psql 37 | ALTER ROLE queremossaber WITH CREATEDB; 38 | ``` 39 | 40 | Isso permitirá que ele crie o banco de dados de testes durante sua execução. 41 | Uma vez isso esteja configurado, para rodar os testes basta executar: 42 | 43 | ``` 44 | make test 45 | ``` 46 | 47 | ## Pedidos de exemplo 48 | 49 | Quando você carrega as fixtures no seu banco de dados, os seguintes pedidos de exemplo são criados: 50 | 51 | * Rejeitado em moderação: SL6F4L46 52 | * Aprovado mas não enviado: H4BRYOXF 53 | * Respondido com um PDF: HQCYR6KQ 54 | * Aguardando resposta atrasada do órgão público: GQ2XOQM7 55 | 56 | ## Configurando uploads para Digital Ocean Spaces 57 | 58 | Durante desenvolvimento, os arquivos enviados são salvos no filesystem local. 59 | Em produção, usamos o Digital Ocean Spaces. Para habilitá-lo, configure as 60 | variáveis que iniciam com `AWS_` no arquivo `.env` e adicione `ENABLE_S3=True`. 61 | -------------------------------------------------------------------------------- /bin/post_compile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | cd "$1" || exit 1 4 | echo "-----> Compiling SCSS" 5 | make sass 6 | echo "-----> Collecting static files" 7 | python manage.py collectstatic --noinput --traceback 8 | echo "-----> Compiling translation files" 9 | make compile_translations 10 | -------------------------------------------------------------------------------- /bucket-policy.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Sid": "PublicReadGetObject", 6 | "Effect": "Allow", 7 | "Principal": "*", 8 | "Action": "s3:GetObject", 9 | "Resource": "arn:aws:s3:::queremossaber/pedidos-anonimos/uploads/*" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /locale/pt_BR/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2019-02-18 19:36+0000\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Plural-Forms: nplurals=2; plural=(n > 1);\n" 20 | 21 | #: web/foi_requests/admin.py:8 22 | msgid "moderation status" 23 | msgstr "status de moderação" 24 | 25 | #: web/foi_requests/admin.py:13 web/foi_requests/admin.py:120 26 | msgid "Pending" 27 | msgstr "Aguardando" 28 | 29 | #: web/foi_requests/admin.py:14 30 | msgid "Approved" 31 | msgstr "Aprovado" 32 | 33 | #: web/foi_requests/admin.py:15 web/foi_requests/admin.py:102 34 | #: web/foi_requests/models.py:198 35 | msgid "Rejected" 36 | msgstr "Rejeitado" 37 | 38 | #: web/foi_requests/admin.py:30 39 | msgid "sender type" 40 | msgstr "remetente" 41 | 42 | #: web/foi_requests/admin.py:35 43 | msgid "User" 44 | msgstr "Usuário" 45 | 46 | #: web/foi_requests/admin.py:36 47 | msgid "Government" 48 | msgstr "Governo" 49 | 50 | #: web/foi_requests/admin.py:111 51 | msgid "status" 52 | msgstr "status" 53 | 54 | #: web/foi_requests/admin.py:121 55 | msgid "Approved (not sent)" 56 | msgstr "Aprovado (não enviado)" 57 | 58 | #: web/foi_requests/admin.py:101 web/foi_requests/models.py:200 59 | msgid "Sent" 60 | msgstr "Enviado" 61 | 62 | #: web/foi_requests/models.py:84 63 | msgid "Local Public Bodies must have a \"UF\"." 64 | msgstr "Órgãos municipais devem ter um \"Estado\"." 65 | 66 | #: web/foi_requests/models.py:86 67 | msgid "Local Public Bodies must have a \"municipality\"." 68 | msgstr "Órgãos municipais devem ter uma \"cidade\"." 69 | 70 | #: web/foi_requests/models.py:89 71 | msgid "State Public Bodies must have a \"UF\"." 72 | msgstr "Órgãos estaduais devem ter um \"Estado\"." 73 | 74 | #: web/foi_requests/models.py:91 75 | msgid "State Public Bodies must not have a \"municipality\"." 76 | msgstr "Órgãos estaduais não podem ter uma \"cidade\"." 77 | 78 | #: web/foi_requests/models.py:95 79 | msgid "Federal Public Bodies must not have a \"UF\"." 80 | msgstr "Órgãos federais não podem ter um \"Estado\"." 81 | 82 | #: web/foi_requests/models.py:97 83 | msgid "Federal Public Bodies must not have a \"municipality\"." 84 | msgstr "Órgãos federais não podem ter uma \"cidade\"." 85 | 86 | #: web/foi_requests/models.py:108 87 | msgid "Delayed" 88 | msgstr "Atrasado" 89 | 90 | #: web/foi_requests/models.py:109 91 | msgid "Finished" 92 | msgstr "Finalizado" 93 | 94 | #: web/foi_requests/models.py:110 95 | msgid "Waiting for government reply" 96 | msgstr "Aguardando resposta do órgão" 97 | 98 | #: web/foi_requests/models.py:111 99 | msgid "Waiting for user reply" 100 | msgstr "Aguardando resposta do cidadão" 101 | 102 | #: web/foi_requests/models.py:140 103 | msgid "Protocol can not be changed." 104 | msgstr "Protocolo não pode ser modificado" 105 | 106 | #: web/foi_requests/models.py:197 107 | msgid "Pending moderation" 108 | msgstr "Aguardando moderação" 109 | 110 | #: web/foi_requests/models.py:199 111 | msgid "Ready to be sent" 112 | msgstr "Aguardando envio" 113 | 114 | #: web/foi_requests/models.py:314 115 | msgid "Message can either have a \"sender\" or a \"receiver\", not both." 116 | msgstr "Mensagem pode ter ou um \"sender\" ou um \"receiver\", não ambos." 117 | 118 | #: web/foi_requests/models.py:326 119 | msgid "Government messages must have a \"sent_at\" date." 120 | msgstr "" 121 | "Mensagens vindas do órgão público devem ter uma data de envio \"sent_at\"." 122 | 123 | #: web/foi_requests/models.py:332 124 | msgid "Only approved user messages can be marked as sent." 125 | msgstr "Somente mensagens aprovadas podem ser marcadas como enviadas." 126 | 127 | #: web/foi_requests/models.py:337 128 | msgid "A message can not be rejected without an explanation." 129 | msgstr "Uma mensagem não pode ser rejeitada sem uma explicação." 130 | 131 | #: web/settings.py:127 132 | msgid "Brazilian Portuguese" 133 | msgstr "Português brasileiro" 134 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "web.settings") 7 | try: 8 | from django.core.management import execute_from_command_line 9 | except ImportError as exc: 10 | raise ImportError( 11 | "Couldn't import Django. Are you sure it's installed and " 12 | "available on your PYTHONPATH environment variable? Did you " 13 | "forget to activate a virtual environment?" 14 | ) from exc 15 | execute_from_command_line(sys.argv) 16 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "asgiref" 5 | version = "3.8.1" 6 | description = "ASGI specs, helper code, and adapters" 7 | optional = false 8 | python-versions = ">=3.8" 9 | files = [ 10 | {file = "asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47"}, 11 | {file = "asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590"}, 12 | ] 13 | 14 | [package.dependencies] 15 | typing-extensions = {version = ">=4", markers = "python_version < \"3.11\""} 16 | 17 | [package.extras] 18 | tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] 19 | 20 | [[package]] 21 | name = "boto3" 22 | version = "1.35.78" 23 | description = "The AWS SDK for Python" 24 | optional = false 25 | python-versions = ">=3.8" 26 | files = [ 27 | {file = "boto3-1.35.78-py3-none-any.whl", hash = "sha256:5ef7166fe5060637b92af8dc152cd7acecf96b3fc9c5456706a886cadb534391"}, 28 | {file = "boto3-1.35.78.tar.gz", hash = "sha256:fc8001519c8842e766ad3793bde3fbd0bb39e821a582fc12cf67876b8f3cf7f1"}, 29 | ] 30 | 31 | [package.dependencies] 32 | botocore = ">=1.35.78,<1.36.0" 33 | jmespath = ">=0.7.1,<2.0.0" 34 | s3transfer = ">=0.10.0,<0.11.0" 35 | 36 | [package.extras] 37 | crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] 38 | 39 | [[package]] 40 | name = "botocore" 41 | version = "1.35.78" 42 | description = "Low-level, data-driven core of boto 3." 43 | optional = false 44 | python-versions = ">=3.8" 45 | files = [ 46 | {file = "botocore-1.35.78-py3-none-any.whl", hash = "sha256:41c37bd7c0326f25122f33ec84fb80fc0a14d7fcc9961431b0e57568e88c9cb5"}, 47 | {file = "botocore-1.35.78.tar.gz", hash = "sha256:6905036c25449ae8dba5e950e4b908e4b8a6fe6b516bf61e007ecb62fa21f323"}, 48 | ] 49 | 50 | [package.dependencies] 51 | jmespath = ">=0.7.1,<2.0.0" 52 | python-dateutil = ">=2.1,<3.0.0" 53 | urllib3 = {version = ">=1.25.4,<2.2.0 || >2.2.0,<3", markers = "python_version >= \"3.10\""} 54 | 55 | [package.extras] 56 | crt = ["awscrt (==0.22.0)"] 57 | 58 | [[package]] 59 | name = "certifi" 60 | version = "2024.12.14" 61 | description = "Python package for providing Mozilla's CA Bundle." 62 | optional = false 63 | python-versions = ">=3.6" 64 | files = [ 65 | {file = "certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56"}, 66 | {file = "certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db"}, 67 | ] 68 | 69 | [[package]] 70 | name = "cfgv" 71 | version = "3.4.0" 72 | description = "Validate configuration and produce human readable error messages." 73 | optional = false 74 | python-versions = ">=3.8" 75 | files = [ 76 | {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, 77 | {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, 78 | ] 79 | 80 | [[package]] 81 | name = "colorama" 82 | version = "0.4.6" 83 | description = "Cross-platform colored terminal text." 84 | optional = false 85 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 86 | files = [ 87 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 88 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 89 | ] 90 | 91 | [[package]] 92 | name = "distlib" 93 | version = "0.3.9" 94 | description = "Distribution utilities" 95 | optional = false 96 | python-versions = "*" 97 | files = [ 98 | {file = "distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87"}, 99 | {file = "distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403"}, 100 | ] 101 | 102 | [[package]] 103 | name = "django" 104 | version = "5.2" 105 | description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." 106 | optional = false 107 | python-versions = ">=3.10" 108 | files = [ 109 | {file = "Django-5.2-py3-none-any.whl", hash = "sha256:91ceed4e3a6db5aedced65e3c8f963118ea9ba753fc620831c77074e620e7d83"}, 110 | {file = "Django-5.2.tar.gz", hash = "sha256:1a47f7a7a3d43ce64570d350e008d2949abe8c7e21737b351b6a1611277c6d89"}, 111 | ] 112 | 113 | [package.dependencies] 114 | asgiref = ">=3.8.1" 115 | sqlparse = ">=0.3.1" 116 | tzdata = {version = "*", markers = "sys_platform == \"win32\""} 117 | 118 | [package.extras] 119 | argon2 = ["argon2-cffi (>=19.1.0)"] 120 | bcrypt = ["bcrypt"] 121 | 122 | [[package]] 123 | name = "django-debug-toolbar" 124 | version = "4.4.6" 125 | description = "A configurable set of panels that display various debug information about the current request/response." 126 | optional = false 127 | python-versions = ">=3.8" 128 | files = [ 129 | {file = "django_debug_toolbar-4.4.6-py3-none-any.whl", hash = "sha256:3beb671c9ec44ffb817fad2780667f172bd1c067dbcabad6268ce39a81335f45"}, 130 | {file = "django_debug_toolbar-4.4.6.tar.gz", hash = "sha256:36e421cb908c2f0675e07f9f41e3d1d8618dc386392ec82d23bcfcd5d29c7044"}, 131 | ] 132 | 133 | [package.dependencies] 134 | django = ">=4.2.9" 135 | sqlparse = ">=0.2" 136 | 137 | [[package]] 138 | name = "django-environ" 139 | version = "0.11.2" 140 | description = "A package that allows you to utilize 12factor inspired environment variables to configure your Django application." 141 | optional = false 142 | python-versions = ">=3.6,<4" 143 | files = [ 144 | {file = "django-environ-0.11.2.tar.gz", hash = "sha256:f32a87aa0899894c27d4e1776fa6b477e8164ed7f6b3e410a62a6d72caaf64be"}, 145 | {file = "django_environ-0.11.2-py2.py3-none-any.whl", hash = "sha256:0ff95ab4344bfeff693836aa978e6840abef2e2f1145adff7735892711590c05"}, 146 | ] 147 | 148 | [package.extras] 149 | develop = ["coverage[toml] (>=5.0a4)", "furo (>=2021.8.17b43,<2021.9.dev0)", "pytest (>=4.6.11)", "sphinx (>=3.5.0)", "sphinx-notfound-page"] 150 | docs = ["furo (>=2021.8.17b43,<2021.9.dev0)", "sphinx (>=3.5.0)", "sphinx-notfound-page"] 151 | testing = ["coverage[toml] (>=5.0a4)", "pytest (>=4.6.11)"] 152 | 153 | [[package]] 154 | name = "django-livesync" 155 | version = "0.5" 156 | description = "TODO" 157 | optional = false 158 | python-versions = "*" 159 | files = [ 160 | {file = "django-livesync-0.5.tar.gz", hash = "sha256:3939b599e51a98de7c21e2cdb22662c6b293a94edd70e68ef15baf4f1a6056f3"}, 161 | {file = "django_livesync-0.5-py3-none-any.whl", hash = "sha256:16214ccd8c7003cdbee769fc2f088340218aa0c0394b7feb51fbe569d7f0632c"}, 162 | ] 163 | 164 | [package.dependencies] 165 | tornado = ">=4.5.1" 166 | watchdog = ">=0.8.3" 167 | websocket-client = ">=0.40.0" 168 | 169 | [[package]] 170 | name = "django-storages" 171 | version = "1.14.4" 172 | description = "Support for many storage backends in Django" 173 | optional = false 174 | python-versions = ">=3.7" 175 | files = [ 176 | {file = "django-storages-1.14.4.tar.gz", hash = "sha256:69aca94d26e6714d14ad63f33d13619e697508ee33ede184e462ed766dc2a73f"}, 177 | {file = "django_storages-1.14.4-py3-none-any.whl", hash = "sha256:d61930acb4a25e3aebebc6addaf946a3b1df31c803a6bf1af2f31c9047febaa3"}, 178 | ] 179 | 180 | [package.dependencies] 181 | boto3 = {version = ">=1.4.4", optional = true, markers = "extra == \"s3\""} 182 | Django = ">=3.2" 183 | 184 | [package.extras] 185 | azure = ["azure-core (>=1.13)", "azure-storage-blob (>=12)"] 186 | boto3 = ["boto3 (>=1.4.4)"] 187 | dropbox = ["dropbox (>=7.2.1)"] 188 | google = ["google-cloud-storage (>=1.27)"] 189 | libcloud = ["apache-libcloud"] 190 | s3 = ["boto3 (>=1.4.4)"] 191 | sftp = ["paramiko (>=1.15)"] 192 | 193 | [[package]] 194 | name = "django-widget-tweaks" 195 | version = "1.5.0" 196 | description = "Tweak the form field rendering in templates, not in python-level form definitions." 197 | optional = false 198 | python-versions = ">=3.8" 199 | files = [ 200 | {file = "django-widget-tweaks-1.5.0.tar.gz", hash = "sha256:1c2180681ebb994e922c754804c7ffebbe1245014777ac47897a81f57cc629c7"}, 201 | {file = "django_widget_tweaks-1.5.0-py3-none-any.whl", hash = "sha256:a41b7b2f05bd44d673d11ebd6c09a96f1d013ee98121cb98c384fe84e33b881e"}, 202 | ] 203 | 204 | [[package]] 205 | name = "exceptiongroup" 206 | version = "1.2.2" 207 | description = "Backport of PEP 654 (exception groups)" 208 | optional = false 209 | python-versions = ">=3.7" 210 | files = [ 211 | {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, 212 | {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, 213 | ] 214 | 215 | [package.extras] 216 | test = ["pytest (>=6)"] 217 | 218 | [[package]] 219 | name = "filelock" 220 | version = "3.16.1" 221 | description = "A platform independent file lock." 222 | optional = false 223 | python-versions = ">=3.8" 224 | files = [ 225 | {file = "filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0"}, 226 | {file = "filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435"}, 227 | ] 228 | 229 | [package.extras] 230 | docs = ["furo (>=2024.8.6)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4.1)"] 231 | testing = ["covdefaults (>=2.3)", "coverage (>=7.6.1)", "diff-cover (>=9.2)", "pytest (>=8.3.3)", "pytest-asyncio (>=0.24)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.26.4)"] 232 | typing = ["typing-extensions (>=4.12.2)"] 233 | 234 | [[package]] 235 | name = "gunicorn" 236 | version = "23.0.0" 237 | description = "WSGI HTTP Server for UNIX" 238 | optional = false 239 | python-versions = ">=3.7" 240 | files = [ 241 | {file = "gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d"}, 242 | {file = "gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec"}, 243 | ] 244 | 245 | [package.dependencies] 246 | packaging = "*" 247 | 248 | [package.extras] 249 | eventlet = ["eventlet (>=0.24.1,!=0.36.0)"] 250 | gevent = ["gevent (>=1.4.0)"] 251 | setproctitle = ["setproctitle"] 252 | testing = ["coverage", "eventlet", "gevent", "pytest", "pytest-cov"] 253 | tornado = ["tornado (>=0.2)"] 254 | 255 | [[package]] 256 | name = "honcho" 257 | version = "2.0.0" 258 | description = "Honcho: a Python clone of Foreman. For managing Procfile-based applications." 259 | optional = false 260 | python-versions = "*" 261 | files = [ 262 | {file = "honcho-2.0.0-py3-none-any.whl", hash = "sha256:56dcd04fc72d362a4befb9303b1a1a812cba5da283526fbc6509be122918ddf3"}, 263 | {file = "honcho-2.0.0.tar.gz", hash = "sha256:af3815c03c634bf67d50f114253ea9fef72ecff26e4fd06b29234789ac5b8b2e"}, 264 | ] 265 | 266 | [package.dependencies] 267 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 268 | 269 | [package.extras] 270 | docs = ["sphinx"] 271 | export = ["jinja2 (>=3.1.2,<4)"] 272 | 273 | [[package]] 274 | name = "identify" 275 | version = "2.6.3" 276 | description = "File identification library for Python" 277 | optional = false 278 | python-versions = ">=3.9" 279 | files = [ 280 | {file = "identify-2.6.3-py2.py3-none-any.whl", hash = "sha256:9edba65473324c2ea9684b1f944fe3191db3345e50b6d04571d10ed164f8d7bd"}, 281 | {file = "identify-2.6.3.tar.gz", hash = "sha256:62f5dae9b5fef52c84cc188514e9ea4f3f636b1d8799ab5ebc475471f9e47a02"}, 282 | ] 283 | 284 | [package.extras] 285 | license = ["ukkonen"] 286 | 287 | [[package]] 288 | name = "iniconfig" 289 | version = "2.0.0" 290 | description = "brain-dead simple config-ini parsing" 291 | optional = false 292 | python-versions = ">=3.7" 293 | files = [ 294 | {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, 295 | {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, 296 | ] 297 | 298 | [[package]] 299 | name = "jmespath" 300 | version = "1.0.1" 301 | description = "JSON Matching Expressions" 302 | optional = false 303 | python-versions = ">=3.7" 304 | files = [ 305 | {file = "jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980"}, 306 | {file = "jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe"}, 307 | ] 308 | 309 | [[package]] 310 | name = "libsass" 311 | version = "0.23.0" 312 | description = "Sass for Python: A straightforward binding of libsass for Python." 313 | optional = false 314 | python-versions = ">=3.8" 315 | files = [ 316 | {file = "libsass-0.23.0-cp38-abi3-macosx_11_0_x86_64.whl", hash = "sha256:34cae047cbbfc4ffa832a61cbb110f3c95f5471c6170c842d3fed161e40814dc"}, 317 | {file = "libsass-0.23.0-cp38-abi3-macosx_14_0_arm64.whl", hash = "sha256:ea97d1b45cdc2fc3590cb9d7b60f1d8915d3ce17a98c1f2d4dd47ee0d9c68ce6"}, 318 | {file = "libsass-0.23.0-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:4a218406d605f325d234e4678bd57126a66a88841cb95bee2caeafdc6f138306"}, 319 | {file = "libsass-0.23.0-cp38-abi3-win32.whl", hash = "sha256:31e86d92a5c7a551df844b72d83fc2b5e50abc6fbbb31e296f7bebd6489ed1b4"}, 320 | {file = "libsass-0.23.0-cp38-abi3-win_amd64.whl", hash = "sha256:a2ec85d819f353cbe807432d7275d653710d12b08ec7ef61c124a580a8352f3c"}, 321 | {file = "libsass-0.23.0.tar.gz", hash = "sha256:6f209955ede26684e76912caf329f4ccb57e4a043fd77fe0e7348dd9574f1880"}, 322 | ] 323 | 324 | [[package]] 325 | name = "nodeenv" 326 | version = "1.9.1" 327 | description = "Node.js virtual environment builder" 328 | optional = false 329 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 330 | files = [ 331 | {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, 332 | {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, 333 | ] 334 | 335 | [[package]] 336 | name = "packaging" 337 | version = "24.2" 338 | description = "Core utilities for Python packages" 339 | optional = false 340 | python-versions = ">=3.8" 341 | files = [ 342 | {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, 343 | {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, 344 | ] 345 | 346 | [[package]] 347 | name = "platformdirs" 348 | version = "4.3.6" 349 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." 350 | optional = false 351 | python-versions = ">=3.8" 352 | files = [ 353 | {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, 354 | {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, 355 | ] 356 | 357 | [package.extras] 358 | docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] 359 | test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] 360 | type = ["mypy (>=1.11.2)"] 361 | 362 | [[package]] 363 | name = "pluggy" 364 | version = "1.5.0" 365 | description = "plugin and hook calling mechanisms for python" 366 | optional = false 367 | python-versions = ">=3.8" 368 | files = [ 369 | {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, 370 | {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, 371 | ] 372 | 373 | [package.extras] 374 | dev = ["pre-commit", "tox"] 375 | testing = ["pytest", "pytest-benchmark"] 376 | 377 | [[package]] 378 | name = "pre-commit" 379 | version = "4.0.1" 380 | description = "A framework for managing and maintaining multi-language pre-commit hooks." 381 | optional = false 382 | python-versions = ">=3.9" 383 | files = [ 384 | {file = "pre_commit-4.0.1-py2.py3-none-any.whl", hash = "sha256:efde913840816312445dc98787724647c65473daefe420785f885e8ed9a06878"}, 385 | {file = "pre_commit-4.0.1.tar.gz", hash = "sha256:80905ac375958c0444c65e9cebebd948b3cdb518f335a091a670a89d652139d2"}, 386 | ] 387 | 388 | [package.dependencies] 389 | cfgv = ">=2.0.0" 390 | identify = ">=1.0.0" 391 | nodeenv = ">=0.11.1" 392 | pyyaml = ">=5.1" 393 | virtualenv = ">=20.10.0" 394 | 395 | [[package]] 396 | name = "psycopg2-binary" 397 | version = "2.9.10" 398 | description = "psycopg2 - Python-PostgreSQL Database Adapter" 399 | optional = false 400 | python-versions = ">=3.8" 401 | files = [ 402 | {file = "psycopg2-binary-2.9.10.tar.gz", hash = "sha256:4b3df0e6990aa98acda57d983942eff13d824135fe2250e6522edaa782a06de2"}, 403 | {file = "psycopg2_binary-2.9.10-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:0ea8e3d0ae83564f2fc554955d327fa081d065c8ca5cc6d2abb643e2c9c1200f"}, 404 | {file = "psycopg2_binary-2.9.10-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:3e9c76f0ac6f92ecfc79516a8034a544926430f7b080ec5a0537bca389ee0906"}, 405 | {file = "psycopg2_binary-2.9.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ad26b467a405c798aaa1458ba09d7e2b6e5f96b1ce0ac15d82fd9f95dc38a92"}, 406 | {file = "psycopg2_binary-2.9.10-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:270934a475a0e4b6925b5f804e3809dd5f90f8613621d062848dd82f9cd62007"}, 407 | {file = "psycopg2_binary-2.9.10-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:48b338f08d93e7be4ab2b5f1dbe69dc5e9ef07170fe1f86514422076d9c010d0"}, 408 | {file = "psycopg2_binary-2.9.10-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f4152f8f76d2023aac16285576a9ecd2b11a9895373a1f10fd9db54b3ff06b4"}, 409 | {file = "psycopg2_binary-2.9.10-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:32581b3020c72d7a421009ee1c6bf4a131ef5f0a968fab2e2de0c9d2bb4577f1"}, 410 | {file = "psycopg2_binary-2.9.10-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:2ce3e21dc3437b1d960521eca599d57408a695a0d3c26797ea0f72e834c7ffe5"}, 411 | {file = "psycopg2_binary-2.9.10-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e984839e75e0b60cfe75e351db53d6db750b00de45644c5d1f7ee5d1f34a1ce5"}, 412 | {file = "psycopg2_binary-2.9.10-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3c4745a90b78e51d9ba06e2088a2fe0c693ae19cc8cb051ccda44e8df8a6eb53"}, 413 | {file = "psycopg2_binary-2.9.10-cp310-cp310-win32.whl", hash = "sha256:e5720a5d25e3b99cd0dc5c8a440570469ff82659bb09431c1439b92caf184d3b"}, 414 | {file = "psycopg2_binary-2.9.10-cp310-cp310-win_amd64.whl", hash = "sha256:3c18f74eb4386bf35e92ab2354a12c17e5eb4d9798e4c0ad3a00783eae7cd9f1"}, 415 | {file = "psycopg2_binary-2.9.10-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:04392983d0bb89a8717772a193cfaac58871321e3ec69514e1c4e0d4957b5aff"}, 416 | {file = "psycopg2_binary-2.9.10-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:1a6784f0ce3fec4edc64e985865c17778514325074adf5ad8f80636cd029ef7c"}, 417 | {file = "psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5f86c56eeb91dc3135b3fd8a95dc7ae14c538a2f3ad77a19645cf55bab1799c"}, 418 | {file = "psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b3d2491d4d78b6b14f76881905c7a8a8abcf974aad4a8a0b065273a0ed7a2cb"}, 419 | {file = "psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2286791ececda3a723d1910441c793be44625d86d1a4e79942751197f4d30341"}, 420 | {file = "psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:512d29bb12608891e349af6a0cccedce51677725a921c07dba6342beaf576f9a"}, 421 | {file = "psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5a507320c58903967ef7384355a4da7ff3f28132d679aeb23572753cbf2ec10b"}, 422 | {file = "psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6d4fa1079cab9018f4d0bd2db307beaa612b0d13ba73b5c6304b9fe2fb441ff7"}, 423 | {file = "psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:851485a42dbb0bdc1edcdabdb8557c09c9655dfa2ca0460ff210522e073e319e"}, 424 | {file = "psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:35958ec9e46432d9076286dda67942ed6d968b9c3a6a2fd62b48939d1d78bf68"}, 425 | {file = "psycopg2_binary-2.9.10-cp311-cp311-win32.whl", hash = "sha256:ecced182e935529727401b24d76634a357c71c9275b356efafd8a2a91ec07392"}, 426 | {file = "psycopg2_binary-2.9.10-cp311-cp311-win_amd64.whl", hash = "sha256:ee0e8c683a7ff25d23b55b11161c2663d4b099770f6085ff0a20d4505778d6b4"}, 427 | {file = "psycopg2_binary-2.9.10-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:880845dfe1f85d9d5f7c412efea7a08946a46894537e4e5d091732eb1d34d9a0"}, 428 | {file = "psycopg2_binary-2.9.10-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9440fa522a79356aaa482aa4ba500b65f28e5d0e63b801abf6aa152a29bd842a"}, 429 | {file = "psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3923c1d9870c49a2d44f795df0c889a22380d36ef92440ff618ec315757e539"}, 430 | {file = "psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b2c956c028ea5de47ff3a8d6b3cc3330ab45cf0b7c3da35a2d6ff8420896526"}, 431 | {file = "psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f758ed67cab30b9a8d2833609513ce4d3bd027641673d4ebc9c067e4d208eec1"}, 432 | {file = "psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cd9b4f2cfab88ed4a9106192de509464b75a906462fb846b936eabe45c2063e"}, 433 | {file = "psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dc08420625b5a20b53551c50deae6e231e6371194fa0651dbe0fb206452ae1f"}, 434 | {file = "psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d7cd730dfa7c36dbe8724426bf5612798734bff2d3c3857f36f2733f5bfc7c00"}, 435 | {file = "psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:155e69561d54d02b3c3209545fb08938e27889ff5a10c19de8d23eb5a41be8a5"}, 436 | {file = "psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3cc28a6fd5a4a26224007712e79b81dbaee2ffb90ff406256158ec4d7b52b47"}, 437 | {file = "psycopg2_binary-2.9.10-cp312-cp312-win32.whl", hash = "sha256:ec8a77f521a17506a24a5f626cb2aee7850f9b69a0afe704586f63a464f3cd64"}, 438 | {file = "psycopg2_binary-2.9.10-cp312-cp312-win_amd64.whl", hash = "sha256:18c5ee682b9c6dd3696dad6e54cc7ff3a1a9020df6a5c0f861ef8bfd338c3ca0"}, 439 | {file = "psycopg2_binary-2.9.10-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:26540d4a9a4e2b096f1ff9cce51253d0504dca5a85872c7f7be23be5a53eb18d"}, 440 | {file = "psycopg2_binary-2.9.10-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e217ce4d37667df0bc1c397fdcd8de5e81018ef305aed9415c3b093faaeb10fb"}, 441 | {file = "psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:245159e7ab20a71d989da00f280ca57da7641fa2cdcf71749c193cea540a74f7"}, 442 | {file = "psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c4ded1a24b20021ebe677b7b08ad10bf09aac197d6943bfe6fec70ac4e4690d"}, 443 | {file = "psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3abb691ff9e57d4a93355f60d4f4c1dd2d68326c968e7db17ea96df3c023ef73"}, 444 | {file = "psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8608c078134f0b3cbd9f89b34bd60a943b23fd33cc5f065e8d5f840061bd0673"}, 445 | {file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:230eeae2d71594103cd5b93fd29d1ace6420d0b86f4778739cb1a5a32f607d1f"}, 446 | {file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bb89f0a835bcfc1d42ccd5f41f04870c1b936d8507c6df12b7737febc40f0909"}, 447 | {file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f0c2d907a1e102526dd2986df638343388b94c33860ff3bbe1384130828714b1"}, 448 | {file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f8157bed2f51db683f31306aa497311b560f2265998122abe1dce6428bd86567"}, 449 | {file = "psycopg2_binary-2.9.10-cp313-cp313-win_amd64.whl", hash = "sha256:27422aa5f11fbcd9b18da48373eb67081243662f9b46e6fd07c3eb46e4535142"}, 450 | {file = "psycopg2_binary-2.9.10-cp38-cp38-macosx_12_0_x86_64.whl", hash = "sha256:eb09aa7f9cecb45027683bb55aebaaf45a0df8bf6de68801a6afdc7947bb09d4"}, 451 | {file = "psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b73d6d7f0ccdad7bc43e6d34273f70d587ef62f824d7261c4ae9b8b1b6af90e8"}, 452 | {file = "psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce5ab4bf46a211a8e924d307c1b1fcda82368586a19d0a24f8ae166f5c784864"}, 453 | {file = "psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:056470c3dc57904bbf63d6f534988bafc4e970ffd50f6271fc4ee7daad9498a5"}, 454 | {file = "psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73aa0e31fa4bb82578f3a6c74a73c273367727de397a7a0f07bd83cbea696baa"}, 455 | {file = "psycopg2_binary-2.9.10-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:8de718c0e1c4b982a54b41779667242bc630b2197948405b7bd8ce16bcecac92"}, 456 | {file = "psycopg2_binary-2.9.10-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:5c370b1e4975df846b0277b4deba86419ca77dbc25047f535b0bb03d1a544d44"}, 457 | {file = "psycopg2_binary-2.9.10-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:ffe8ed017e4ed70f68b7b371d84b7d4a790368db9203dfc2d222febd3a9c8863"}, 458 | {file = "psycopg2_binary-2.9.10-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:8aecc5e80c63f7459a1a2ab2c64df952051df196294d9f739933a9f6687e86b3"}, 459 | {file = "psycopg2_binary-2.9.10-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:7a813c8bdbaaaab1f078014b9b0b13f5de757e2b5d9be6403639b298a04d218b"}, 460 | {file = "psycopg2_binary-2.9.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d00924255d7fc916ef66e4bf22f354a940c67179ad3fd7067d7a0a9c84d2fbfc"}, 461 | {file = "psycopg2_binary-2.9.10-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7559bce4b505762d737172556a4e6ea8a9998ecac1e39b5233465093e8cee697"}, 462 | {file = "psycopg2_binary-2.9.10-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8b58f0a96e7a1e341fc894f62c1177a7c83febebb5ff9123b579418fdc8a481"}, 463 | {file = "psycopg2_binary-2.9.10-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b269105e59ac96aba877c1707c600ae55711d9dcd3fc4b5012e4af68e30c648"}, 464 | {file = "psycopg2_binary-2.9.10-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:79625966e176dc97ddabc142351e0409e28acf4660b88d1cf6adb876d20c490d"}, 465 | {file = "psycopg2_binary-2.9.10-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:8aabf1c1a04584c168984ac678a668094d831f152859d06e055288fa515e4d30"}, 466 | {file = "psycopg2_binary-2.9.10-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:19721ac03892001ee8fdd11507e6a2e01f4e37014def96379411ca99d78aeb2c"}, 467 | {file = "psycopg2_binary-2.9.10-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7f5d859928e635fa3ce3477704acee0f667b3a3d3e4bb109f2b18d4005f38287"}, 468 | {file = "psycopg2_binary-2.9.10-cp39-cp39-win32.whl", hash = "sha256:3216ccf953b3f267691c90c6fe742e45d890d8272326b4a8b20850a03d05b7b8"}, 469 | {file = "psycopg2_binary-2.9.10-cp39-cp39-win_amd64.whl", hash = "sha256:30e34c4e97964805f715206c7b789d54a78b70f3ff19fbe590104b71c45600e5"}, 470 | ] 471 | 472 | [[package]] 473 | name = "pytest" 474 | version = "8.3.4" 475 | description = "pytest: simple powerful testing with Python" 476 | optional = false 477 | python-versions = ">=3.8" 478 | files = [ 479 | {file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"}, 480 | {file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"}, 481 | ] 482 | 483 | [package.dependencies] 484 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 485 | exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} 486 | iniconfig = "*" 487 | packaging = "*" 488 | pluggy = ">=1.5,<2" 489 | tomli = {version = ">=1", markers = "python_version < \"3.11\""} 490 | 491 | [package.extras] 492 | dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] 493 | 494 | [[package]] 495 | name = "pytest-django" 496 | version = "4.9.0" 497 | description = "A Django plugin for pytest." 498 | optional = false 499 | python-versions = ">=3.8" 500 | files = [ 501 | {file = "pytest_django-4.9.0-py3-none-any.whl", hash = "sha256:1d83692cb39188682dbb419ff0393867e9904094a549a7d38a3154d5731b2b99"}, 502 | {file = "pytest_django-4.9.0.tar.gz", hash = "sha256:8bf7bc358c9ae6f6fc51b6cebb190fe20212196e6807121f11bd6a3b03428314"}, 503 | ] 504 | 505 | [package.dependencies] 506 | pytest = ">=7.0.0" 507 | 508 | [package.extras] 509 | docs = ["sphinx", "sphinx-rtd-theme"] 510 | testing = ["Django", "django-configurations (>=2.0)"] 511 | 512 | [[package]] 513 | name = "python-dateutil" 514 | version = "2.9.0.post0" 515 | description = "Extensions to the standard Python datetime module" 516 | optional = false 517 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" 518 | files = [ 519 | {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, 520 | {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, 521 | ] 522 | 523 | [package.dependencies] 524 | six = ">=1.5" 525 | 526 | [[package]] 527 | name = "pyyaml" 528 | version = "6.0.2" 529 | description = "YAML parser and emitter for Python" 530 | optional = false 531 | python-versions = ">=3.8" 532 | files = [ 533 | {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, 534 | {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, 535 | {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, 536 | {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, 537 | {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, 538 | {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, 539 | {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, 540 | {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, 541 | {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, 542 | {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, 543 | {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, 544 | {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, 545 | {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, 546 | {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, 547 | {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, 548 | {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, 549 | {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, 550 | {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, 551 | {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, 552 | {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, 553 | {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, 554 | {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, 555 | {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, 556 | {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, 557 | {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, 558 | {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, 559 | {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, 560 | {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, 561 | {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, 562 | {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, 563 | {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, 564 | {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, 565 | {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, 566 | {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, 567 | {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, 568 | {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, 569 | {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, 570 | {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, 571 | {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, 572 | {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, 573 | {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, 574 | {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, 575 | {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, 576 | {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, 577 | {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, 578 | {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, 579 | {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, 580 | {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, 581 | {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, 582 | {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, 583 | {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, 584 | {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, 585 | {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, 586 | ] 587 | 588 | [[package]] 589 | name = "raven" 590 | version = "6.10.0" 591 | description = "Raven is a client for Sentry (https://getsentry.com)" 592 | optional = false 593 | python-versions = "*" 594 | files = [ 595 | {file = "raven-6.10.0-py2.py3-none-any.whl", hash = "sha256:44a13f87670836e153951af9a3c80405d36b43097db869a36e92809673692ce4"}, 596 | {file = "raven-6.10.0.tar.gz", hash = "sha256:3fa6de6efa2493a7c827472e984ce9b020797d0da16f1db67197bcc23c8fae54"}, 597 | ] 598 | 599 | [package.extras] 600 | flask = ["Flask (>=0.8)", "blinker (>=1.1)"] 601 | tests = ["Flask (>=0.8)", "Flask-Login (>=0.2.0)", "ZConfig", "aiohttp", "anyjson", "blinker (>=1.1)", "blinker (>=1.1)", "bottle", "celery (>=2.5)", "coverage (<4)", "exam (>=0.5.2)", "flake8 (==3.5.0)", "logbook", "mock", "nose", "pytest (>=3.2.0,<3.3.0)", "pytest-cov (==2.5.1)", "pytest-flake8 (==1.0.0)", "pytest-pythonpath (==0.7.2)", "pytest-timeout (==1.2.1)", "pytest-xdist (==1.18.2)", "pytz", "requests", "sanic (>=0.7.0)", "tornado (>=4.1,<5.0)", "tox", "webob", "webtest", "wheel"] 602 | 603 | [[package]] 604 | name = "s3transfer" 605 | version = "0.10.4" 606 | description = "An Amazon S3 Transfer Manager" 607 | optional = false 608 | python-versions = ">=3.8" 609 | files = [ 610 | {file = "s3transfer-0.10.4-py3-none-any.whl", hash = "sha256:244a76a24355363a68164241438de1b72f8781664920260c48465896b712a41e"}, 611 | {file = "s3transfer-0.10.4.tar.gz", hash = "sha256:29edc09801743c21eb5ecbc617a152df41d3c287f67b615f73e5f750583666a7"}, 612 | ] 613 | 614 | [package.dependencies] 615 | botocore = ">=1.33.2,<2.0a.0" 616 | 617 | [package.extras] 618 | crt = ["botocore[crt] (>=1.33.2,<2.0a.0)"] 619 | 620 | [[package]] 621 | name = "sentry-sdk" 622 | version = "2.19.2" 623 | description = "Python client for Sentry (https://sentry.io)" 624 | optional = false 625 | python-versions = ">=3.6" 626 | files = [ 627 | {file = "sentry_sdk-2.19.2-py2.py3-none-any.whl", hash = "sha256:ebdc08228b4d131128e568d696c210d846e5b9d70aa0327dec6b1272d9d40b84"}, 628 | {file = "sentry_sdk-2.19.2.tar.gz", hash = "sha256:467df6e126ba242d39952375dd816fbee0f217d119bf454a8ce74cf1e7909e8d"}, 629 | ] 630 | 631 | [package.dependencies] 632 | certifi = "*" 633 | django = {version = ">=1.8", optional = true, markers = "extra == \"django\""} 634 | urllib3 = ">=1.26.11" 635 | 636 | [package.extras] 637 | aiohttp = ["aiohttp (>=3.5)"] 638 | anthropic = ["anthropic (>=0.16)"] 639 | arq = ["arq (>=0.23)"] 640 | asyncpg = ["asyncpg (>=0.23)"] 641 | beam = ["apache-beam (>=2.12)"] 642 | bottle = ["bottle (>=0.12.13)"] 643 | celery = ["celery (>=3)"] 644 | celery-redbeat = ["celery-redbeat (>=2)"] 645 | chalice = ["chalice (>=1.16.0)"] 646 | clickhouse-driver = ["clickhouse-driver (>=0.2.0)"] 647 | django = ["django (>=1.8)"] 648 | falcon = ["falcon (>=1.4)"] 649 | fastapi = ["fastapi (>=0.79.0)"] 650 | flask = ["blinker (>=1.1)", "flask (>=0.11)", "markupsafe"] 651 | grpcio = ["grpcio (>=1.21.1)", "protobuf (>=3.8.0)"] 652 | http2 = ["httpcore[http2] (==1.*)"] 653 | httpx = ["httpx (>=0.16.0)"] 654 | huey = ["huey (>=2)"] 655 | huggingface-hub = ["huggingface_hub (>=0.22)"] 656 | langchain = ["langchain (>=0.0.210)"] 657 | launchdarkly = ["launchdarkly-server-sdk (>=9.8.0)"] 658 | litestar = ["litestar (>=2.0.0)"] 659 | loguru = ["loguru (>=0.5)"] 660 | openai = ["openai (>=1.0.0)", "tiktoken (>=0.3.0)"] 661 | openfeature = ["openfeature-sdk (>=0.7.1)"] 662 | opentelemetry = ["opentelemetry-distro (>=0.35b0)"] 663 | opentelemetry-experimental = ["opentelemetry-distro"] 664 | pure-eval = ["asttokens", "executing", "pure_eval"] 665 | pymongo = ["pymongo (>=3.1)"] 666 | pyspark = ["pyspark (>=2.4.4)"] 667 | quart = ["blinker (>=1.1)", "quart (>=0.16.1)"] 668 | rq = ["rq (>=0.6)"] 669 | sanic = ["sanic (>=0.8)"] 670 | sqlalchemy = ["sqlalchemy (>=1.2)"] 671 | starlette = ["starlette (>=0.19.1)"] 672 | starlite = ["starlite (>=1.48)"] 673 | tornado = ["tornado (>=6)"] 674 | 675 | [[package]] 676 | name = "six" 677 | version = "1.17.0" 678 | description = "Python 2 and 3 compatibility utilities" 679 | optional = false 680 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" 681 | files = [ 682 | {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, 683 | {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, 684 | ] 685 | 686 | [[package]] 687 | name = "sqlparse" 688 | version = "0.5.3" 689 | description = "A non-validating SQL parser." 690 | optional = false 691 | python-versions = ">=3.8" 692 | files = [ 693 | {file = "sqlparse-0.5.3-py3-none-any.whl", hash = "sha256:cf2196ed3418f3ba5de6af7e82c694a9fbdbfecccdfc72e281548517081f16ca"}, 694 | {file = "sqlparse-0.5.3.tar.gz", hash = "sha256:09f67787f56a0b16ecdbde1bfc7f5d9c3371ca683cfeaa8e6ff60b4807ec9272"}, 695 | ] 696 | 697 | [package.extras] 698 | dev = ["build", "hatch"] 699 | doc = ["sphinx"] 700 | 701 | [[package]] 702 | name = "tomli" 703 | version = "2.2.1" 704 | description = "A lil' TOML parser" 705 | optional = false 706 | python-versions = ">=3.8" 707 | files = [ 708 | {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, 709 | {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, 710 | {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, 711 | {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, 712 | {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, 713 | {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, 714 | {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, 715 | {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, 716 | {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, 717 | {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, 718 | {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, 719 | {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, 720 | {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, 721 | {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, 722 | {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, 723 | {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, 724 | {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, 725 | {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, 726 | {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, 727 | {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, 728 | {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, 729 | {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, 730 | {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, 731 | {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, 732 | {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, 733 | {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, 734 | {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, 735 | {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, 736 | {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, 737 | {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, 738 | {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, 739 | {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, 740 | ] 741 | 742 | [[package]] 743 | name = "tornado" 744 | version = "6.4.2" 745 | description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." 746 | optional = false 747 | python-versions = ">=3.8" 748 | files = [ 749 | {file = "tornado-6.4.2-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e828cce1123e9e44ae2a50a9de3055497ab1d0aeb440c5ac23064d9e44880da1"}, 750 | {file = "tornado-6.4.2-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:072ce12ada169c5b00b7d92a99ba089447ccc993ea2143c9ede887e0937aa803"}, 751 | {file = "tornado-6.4.2-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a017d239bd1bb0919f72af256a970624241f070496635784d9bf0db640d3fec"}, 752 | {file = "tornado-6.4.2-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c36e62ce8f63409301537222faffcef7dfc5284f27eec227389f2ad11b09d946"}, 753 | {file = "tornado-6.4.2-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bca9eb02196e789c9cb5c3c7c0f04fb447dc2adffd95265b2c7223a8a615ccbf"}, 754 | {file = "tornado-6.4.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:304463bd0772442ff4d0f5149c6f1c2135a1fae045adf070821c6cdc76980634"}, 755 | {file = "tornado-6.4.2-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:c82c46813ba483a385ab2a99caeaedf92585a1f90defb5693351fa7e4ea0bf73"}, 756 | {file = "tornado-6.4.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:932d195ca9015956fa502c6b56af9eb06106140d844a335590c1ec7f5277d10c"}, 757 | {file = "tornado-6.4.2-cp38-abi3-win32.whl", hash = "sha256:2876cef82e6c5978fde1e0d5b1f919d756968d5b4282418f3146b79b58556482"}, 758 | {file = "tornado-6.4.2-cp38-abi3-win_amd64.whl", hash = "sha256:908b71bf3ff37d81073356a5fadcc660eb10c1476ee6e2725588626ce7e5ca38"}, 759 | {file = "tornado-6.4.2.tar.gz", hash = "sha256:92bad5b4746e9879fd7bf1eb21dce4e3fc5128d71601f80005afa39237ad620b"}, 760 | ] 761 | 762 | [[package]] 763 | name = "typing-extensions" 764 | version = "4.12.2" 765 | description = "Backported and Experimental Type Hints for Python 3.8+" 766 | optional = false 767 | python-versions = ">=3.8" 768 | files = [ 769 | {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, 770 | {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, 771 | ] 772 | 773 | [[package]] 774 | name = "tzdata" 775 | version = "2024.2" 776 | description = "Provider of IANA time zone data" 777 | optional = false 778 | python-versions = ">=2" 779 | files = [ 780 | {file = "tzdata-2024.2-py2.py3-none-any.whl", hash = "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd"}, 781 | {file = "tzdata-2024.2.tar.gz", hash = "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc"}, 782 | ] 783 | 784 | [[package]] 785 | name = "urllib3" 786 | version = "2.2.3" 787 | description = "HTTP library with thread-safe connection pooling, file post, and more." 788 | optional = false 789 | python-versions = ">=3.8" 790 | files = [ 791 | {file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"}, 792 | {file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"}, 793 | ] 794 | 795 | [package.extras] 796 | brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] 797 | h2 = ["h2 (>=4,<5)"] 798 | socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] 799 | zstd = ["zstandard (>=0.18.0)"] 800 | 801 | [[package]] 802 | name = "virtualenv" 803 | version = "20.28.0" 804 | description = "Virtual Python Environment builder" 805 | optional = false 806 | python-versions = ">=3.8" 807 | files = [ 808 | {file = "virtualenv-20.28.0-py3-none-any.whl", hash = "sha256:23eae1b4516ecd610481eda647f3a7c09aea295055337331bb4e6892ecce47b0"}, 809 | {file = "virtualenv-20.28.0.tar.gz", hash = "sha256:2c9c3262bb8e7b87ea801d715fae4495e6032450c71d2309be9550e7364049aa"}, 810 | ] 811 | 812 | [package.dependencies] 813 | distlib = ">=0.3.7,<1" 814 | filelock = ">=3.12.2,<4" 815 | platformdirs = ">=3.9.1,<5" 816 | 817 | [package.extras] 818 | docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] 819 | test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] 820 | 821 | [[package]] 822 | name = "watchdog" 823 | version = "6.0.0" 824 | description = "Filesystem events monitoring" 825 | optional = false 826 | python-versions = ">=3.9" 827 | files = [ 828 | {file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26"}, 829 | {file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112"}, 830 | {file = "watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3"}, 831 | {file = "watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c"}, 832 | {file = "watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2"}, 833 | {file = "watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c"}, 834 | {file = "watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948"}, 835 | {file = "watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860"}, 836 | {file = "watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0"}, 837 | {file = "watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c"}, 838 | {file = "watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134"}, 839 | {file = "watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b"}, 840 | {file = "watchdog-6.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e6f0e77c9417e7cd62af82529b10563db3423625c5fce018430b249bf977f9e8"}, 841 | {file = "watchdog-6.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:90c8e78f3b94014f7aaae121e6b909674df5b46ec24d6bebc45c44c56729af2a"}, 842 | {file = "watchdog-6.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e7631a77ffb1f7d2eefa4445ebbee491c720a5661ddf6df3498ebecae5ed375c"}, 843 | {file = "watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881"}, 844 | {file = "watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11"}, 845 | {file = "watchdog-6.0.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7a0e56874cfbc4b9b05c60c8a1926fedf56324bb08cfbc188969777940aef3aa"}, 846 | {file = "watchdog-6.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6439e374fc012255b4ec786ae3c4bc838cd7309a540e5fe0952d03687d8804e"}, 847 | {file = "watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13"}, 848 | {file = "watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379"}, 849 | {file = "watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e"}, 850 | {file = "watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f"}, 851 | {file = "watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26"}, 852 | {file = "watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c"}, 853 | {file = "watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2"}, 854 | {file = "watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a"}, 855 | {file = "watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680"}, 856 | {file = "watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f"}, 857 | {file = "watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282"}, 858 | ] 859 | 860 | [package.extras] 861 | watchmedo = ["PyYAML (>=3.10)"] 862 | 863 | [[package]] 864 | name = "websocket-client" 865 | version = "1.8.0" 866 | description = "WebSocket client for Python with low level API options" 867 | optional = false 868 | python-versions = ">=3.8" 869 | files = [ 870 | {file = "websocket_client-1.8.0-py3-none-any.whl", hash = "sha256:17b44cc997f5c498e809b22cdf2d9c7a9e71c02c8cc2b6c56e7c2d1239bfa526"}, 871 | {file = "websocket_client-1.8.0.tar.gz", hash = "sha256:3239df9f44da632f96012472805d40a23281a991027ce11d2f45a6f24ac4c3da"}, 872 | ] 873 | 874 | [package.extras] 875 | docs = ["Sphinx (>=6.0)", "myst-parser (>=2.0.0)", "sphinx-rtd-theme (>=1.1.0)"] 876 | optional = ["python-socks", "wsaccel"] 877 | test = ["websockets"] 878 | 879 | [[package]] 880 | name = "whitenoise" 881 | version = "6.8.2" 882 | description = "Radically simplified static file serving for WSGI applications" 883 | optional = false 884 | python-versions = ">=3.9" 885 | files = [ 886 | {file = "whitenoise-6.8.2-py3-none-any.whl", hash = "sha256:df12dce147a043d1956d81d288c6f0044147c6d2ab9726e5772ac50fb45d2280"}, 887 | {file = "whitenoise-6.8.2.tar.gz", hash = "sha256:486bd7267a375fa9650b136daaec156ac572971acc8bf99add90817a530dd1d4"}, 888 | ] 889 | 890 | [package.extras] 891 | brotli = ["brotli"] 892 | 893 | [metadata] 894 | lock-version = "2.0" 895 | python-versions = ">=3.10,<3.13" 896 | content-hash = "5821553f6c61b0b01b3e9b509f09117a22f423cf51557ca27c402458fec35952" 897 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "queremossaber" 3 | version = "0.1.0" 4 | description = "" 5 | authors = ["Vitor Baptista "] 6 | readme = "README.md" 7 | package-mode = false 8 | 9 | [tool.poetry.dependencies] 10 | python = ">=3.10,<3.13" 11 | django = "^5.1.3" 12 | django-widget-tweaks = "^1.5.0" 13 | django-environ = "0.11.2" 14 | django-storages = {extras = ["s3"], version = "^1.14.4"} 15 | raven = "6.10.0" 16 | psycopg2-binary = "^2.9.10" 17 | libsass = "^0.23.0" 18 | whitenoise = "^6.8.2" 19 | gunicorn = "^23.0.0" 20 | sentry-sdk = {extras = ["django"], version = "^2.19.2"} 21 | 22 | [tool.poetry.group.dev.dependencies] 23 | django-debug-toolbar = "^4.4.6" 24 | django-livesync = "0.5" 25 | honcho = "^2.0.0" 26 | pre-commit = "^4.0.1" 27 | pytest-django = "^4.9.0" 28 | watchdog = "^6.0.0" 29 | 30 | [tool.pytest.ini_options] 31 | DJANGO_SETTINGS_MODULE = "web.settings_test" 32 | 33 | [build-system] 34 | requires = ["poetry-core"] 35 | build-backend = "poetry.core.masonry.api" 36 | -------------------------------------------------------------------------------- /runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.9.16 2 | -------------------------------------------------------------------------------- /web/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FiquemSabendo/queremossaber/81f02d0ba8cc07ee0d2176236e99653b8dac3775/web/__init__.py -------------------------------------------------------------------------------- /web/foi_requests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FiquemSabendo/queremossaber/81f02d0ba8cc07ee0d2176236e99653b8dac3775/web/foi_requests/__init__.py -------------------------------------------------------------------------------- /web/foi_requests/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.utils.translation import gettext_lazy as _ 3 | from django.urls import reverse 4 | from django.utils.safestring import mark_safe 5 | 6 | from .models import PublicBody, Esic, FOIRequest, Message 7 | 8 | 9 | class ModerationStatusListFilter(admin.SimpleListFilter): 10 | title = _("moderation status") 11 | parameter_name = "moderation_status" 12 | 13 | def lookups(self, request, model_admin): 14 | return ( 15 | ("pending", _("Pending")), 16 | ("approved", _("Approved")), 17 | ("rejected", _("Rejected")), 18 | ) 19 | 20 | def queryset(self, request, queryset): 21 | value = self.value() 22 | moderation_status = { 23 | "approved": True, 24 | "rejected": None, 25 | } 26 | 27 | if value is not None: 28 | return queryset.filter(moderation_status=moderation_status[value]) 29 | 30 | 31 | class ModerationSenderTypeFilter(admin.SimpleListFilter): 32 | title = _("sender type") 33 | parameter_name = "sender_type" 34 | 35 | def lookups(self, request, model_admin): 36 | return ( 37 | ("user", _("User")), 38 | ("government", _("Government")), 39 | ) 40 | 41 | def queryset(self, request, queryset): 42 | value = self.value() 43 | if value is not None: 44 | sender_is_null = value == "government" 45 | return queryset.exclude(sender_id__isnull=sender_is_null) 46 | 47 | 48 | class MessageInline(admin.StackedInline): 49 | model = Message 50 | 51 | list_display = ("moderation_status",) 52 | list_filter = (ModerationStatusListFilter,) 53 | 54 | readonly_fields = ("moderated_at",) 55 | fieldsets = ( 56 | ( 57 | "Message", 58 | { 59 | "fields": ( 60 | "foi_request", 61 | "sender", 62 | "receiver", 63 | "summary", 64 | "body", 65 | "attached_file", 66 | "sent_at", 67 | ), 68 | }, 69 | ), 70 | ( 71 | "Moderation", 72 | { 73 | "fields": ( 74 | "moderation_status", 75 | "moderation_message", 76 | "moderated_at", 77 | ) 78 | }, 79 | ), 80 | ) 81 | ordering = ["created_at"] 82 | autocomplete_fields = ("sender", "receiver") 83 | 84 | 85 | @admin.register(FOIRequest) 86 | class FOIRequestAdmin(admin.ModelAdmin): 87 | list_display = ( 88 | "protocol", 89 | "esic_protocol", 90 | "public_body", 91 | "esic", 92 | "status", 93 | "can_publish", 94 | ) 95 | 96 | readonly_fields = ( 97 | "protocol", 98 | "created_at", 99 | "updated_at", 100 | ) 101 | 102 | inlines = (MessageInline,) 103 | 104 | 105 | def approve_messages(modeladmin, request, queryset): 106 | queryset.update(moderation_status=True) 107 | 108 | 109 | approve_messages.short_description = "Approve selected messages" 110 | 111 | 112 | class ModerationStatusListFilter(admin.SimpleListFilter): 113 | title = _("status") 114 | 115 | parameter_name = "moderation_status" 116 | 117 | def lookups(self, request, model_admin): 118 | # Need to replace None with 'pending' to differentiate between "disable 119 | # filter" and "filter None values" 120 | 121 | return ( 122 | ("pending", _("Pending")), 123 | ("approved_not_sent", _("Approved (not sent)")), 124 | ("sent", _("Sent")), 125 | ("rejected", _("Rejected")), 126 | ) 127 | 128 | def queryset(self, request, queryset): 129 | filters = {} 130 | 131 | value = self.value() 132 | if value is not None: 133 | param = self.parameter_name 134 | 135 | value_to_filters = { 136 | "pending": {param: None}, 137 | "approved_not_sent": {param: True, "sent_at__isnull": True}, 138 | "sent": {param: True, "sent_at__isnull": False}, 139 | "rejected": {param: False}, 140 | } 141 | 142 | filters = value_to_filters[value] 143 | 144 | return queryset.filter(**filters) 145 | 146 | 147 | @admin.register(Message) 148 | class MessageAdmin(admin.ModelAdmin): 149 | actions = (approve_messages,) 150 | 151 | list_display = ( 152 | "foi_request_link", 153 | "sender_type", 154 | "receiver", 155 | "summary", 156 | "body", 157 | "sent_at", 158 | "created_at", 159 | ) 160 | 161 | # We change the messages on the FOIRequest change page instead 162 | list_display_links = None 163 | 164 | list_filter = ( 165 | ModerationStatusListFilter, 166 | ModerationSenderTypeFilter, 167 | ) 168 | 169 | fieldsets = ( 170 | ( 171 | "Message", 172 | { 173 | "fields": ( 174 | "foi_request", 175 | "sender", 176 | "receiver", 177 | "summary", 178 | "body", 179 | "attached_file", 180 | "created_at", 181 | "updated_at", 182 | "sent_at", 183 | ), 184 | }, 185 | ), 186 | ) 187 | 188 | readonly_fields = ( 189 | "moderated_at", 190 | "created_at", 191 | "updated_at", 192 | ) 193 | 194 | class Meta: 195 | ordering = ["created_at"] 196 | 197 | def get_fieldsets(self, request, obj=None): 198 | extra_fieldsets = () 199 | sender_is_user = obj and obj.sender is None 200 | 201 | if sender_is_user: 202 | extra_fieldsets = ( 203 | ( 204 | "Moderation", 205 | { 206 | "fields": ( 207 | "moderation_status", 208 | "moderation_message", 209 | "moderated_at", 210 | ), 211 | }, 212 | ), 213 | ) 214 | 215 | return self.fieldsets + extra_fieldsets 216 | 217 | def get_readonly_fields(self, request, obj=None): 218 | extra_readonly_fields = () 219 | 220 | if obj: 221 | # Disallow editing fields 222 | extra_readonly_fields = ( 223 | "foi_request", 224 | "sender", 225 | "receiver", 226 | ) 227 | 228 | return self.readonly_fields + extra_readonly_fields 229 | 230 | def foi_request_link(self, obj): 231 | foi_request = obj.foi_request 232 | url = reverse( 233 | f"admin:{foi_request._meta.app_label}_{foi_request._meta.model_name}_change", 234 | args=(foi_request.pk,), 235 | ) 236 | return mark_safe(f'{foi_request.protocol}') 237 | 238 | foi_request_link.short_description = _("FOI Request") 239 | 240 | 241 | admin.site.register(PublicBody, search_fields=["name"]) 242 | admin.site.register(Esic) 243 | -------------------------------------------------------------------------------- /web/foi_requests/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class FoiRequestsConfig(AppConfig): 5 | name = "web.foi_requests" 6 | -------------------------------------------------------------------------------- /web/foi_requests/fixtures/sample_foi_requests.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "model": "foi_requests.foirequest", 4 | "pk": 1, 5 | "fields": { 6 | "protocol": "HQCYR6KQ", 7 | "esic_protocol": "00075001035201319", 8 | "created_at": "2018-09-06T15:07:38.665Z", 9 | "updated_at": "2018-09-06T15:10:58.273Z" 10 | } 11 | }, 12 | { 13 | "model": "foi_requests.foirequest", 14 | "pk": 2, 15 | "fields": { 16 | "protocol": "SL6F4L46", 17 | "esic_protocol": "", 18 | "created_at": "2018-09-06T15:20:01.068Z", 19 | "updated_at": "2018-09-06T15:20:42.461Z" 20 | } 21 | }, 22 | { 23 | "model": "foi_requests.foirequest", 24 | "pk": 3, 25 | "fields": { 26 | "protocol": "H4BRYOXF", 27 | "esic_protocol": "", 28 | "created_at": "2018-09-06T15:24:14.924Z", 29 | "updated_at": "2018-09-06T15:24:14.924Z" 30 | } 31 | }, 32 | { 33 | "model": "foi_requests.foirequest", 34 | "pk": 4, 35 | "fields": { 36 | "protocol": "GQ2XOQM7", 37 | "esic_protocol": "123456", 38 | "created_at": "2018-08-05T15:29:46.233Z", 39 | "updated_at": "2018-08-05T15:29:46.233Z" 40 | } 41 | }, 42 | { 43 | "model": "foi_requests.message", 44 | "pk": 1, 45 | "fields": { 46 | "foi_request": 1, 47 | "sender": null, 48 | "receiver": 63, 49 | "summary": "C\u00f3digo-fonte e datasets usados para treinar modelo de detec\u00e7\u00e3o de outliers para o FNDE", 50 | "body": "Gostaria de obter acesso ao c\u00f3digo-fonte, documenta\u00e7\u00e3o (se houver) e conjuntos de dados usados para cria\u00e7\u00e3o do modelo estat\u00edstico descrito no relat\u00f3rio de atividades subsidi\u00e1rias entitulado \"Proposta de metodologia baseada em gest\u00e3o de riscos para aprimoramento do modelo de tratamento e sele\u00e7\u00e3o das presta\u00e7\u00f5es de contas das Pol\u00edticas Federais da Educa\u00e7\u00e3o\", de junho de 2018, dispon\u00edvel em https://auditoria.cgu.gov.br/download/11478.pdf.\r\n\r\nIdealmente, esse acesso seria disponibilizado online, permitindo que toda comunidade de data science possa entender e, quem sabe, melhorar o modelo. Caso isso n\u00e3o seja poss\u00edvel, deixo \u00e0 CGU a decis\u00e3o de melhor forma de envio dos arquivos, dependendo do tamanho dos mesmos.", 51 | "sent_at": "2018-07-27T16:29:00Z", 52 | "created_at": "2018-09-06T15:07:38.671Z", 53 | "updated_at": "2018-09-06T15:07:38.671Z", 54 | "moderation_status": true, 55 | "moderation_message": "", 56 | "moderated_at": "2018-09-06T15:07:38.653Z", 57 | "attached_file": "" 58 | } 59 | }, 60 | { 61 | "model": "foi_requests.message", 62 | "pk": 2, 63 | "fields": { 64 | "foi_request": 1, 65 | "sender": 63, 66 | "receiver": null, 67 | "summary": "", 68 | "body": "Prezado(a) Sr(a),\r\n\r\nEm aten\u00e7\u00e3o ao seu requerimento, encaminhamos, na caixa \u201cAnexos\u201d, a resposta fornecida pelo setor respons\u00e1vel pelo assunto em quest\u00e3o.\r\n\r\nInformamos, ainda, que, nos termos do Art. 15 da Lei 12.527/2011, \u00e9 poss\u00edvel recorrer do indeferimento ou das raz\u00f5es da negativa de acesso, no prazo de 10 dias.\r\n\r\nCordialmente.\r\n\r\nServi\u00e7o de Informa\u00e7\u00f5es ao Cidad\u00e3o\r\n\r\nMinist\u00e9rio da Transpar\u00eancia e Controladoria-Geral da Uni\u00e3o.", 69 | "sent_at": "2018-08-20T18:52:00Z", 70 | "created_at": "2018-09-06T15:07:38.676Z", 71 | "updated_at": "2018-09-06T15:10:58.277Z", 72 | "moderation_status": true, 73 | "moderation_message": "", 74 | "moderated_at": "2018-09-06T15:07:38.676Z", 75 | "attached_file": "2c26b46b68ffc68ff99b453c.pdf" 76 | } 77 | }, 78 | { 79 | "model": "foi_requests.message", 80 | "pk": 3, 81 | "fields": { 82 | "foi_request": 2, 83 | "sender": null, 84 | "receiver": 296, 85 | "summary": "", 86 | "body": "Ol\u00e1, meu nome \u00e9 XXX. Gostaria de saber quanto foi repassado ao Museu Nacional entre 2015 e 2018. Obrigado.", 87 | "sent_at": null, 88 | "created_at": "2018-09-06T15:20:01.073Z", 89 | "updated_at": "2018-09-06T15:20:42.485Z", 90 | "moderation_status": false, 91 | "moderation_message": "Cont\u00e9m informa\u00e7\u00f5es pessoais (retiradas para preservar a identidade do solicitante).", 92 | "moderated_at": "2018-09-06T15:20:42.460Z", 93 | "attached_file": "" 94 | } 95 | }, 96 | { 97 | "model": "foi_requests.message", 98 | "pk": 4, 99 | "fields": { 100 | "foi_request": 3, 101 | "sender": null, 102 | "receiver": 247, 103 | "summary": "", 104 | "body": "Gostaria da agenda oficial do presidente Michel Temer em mar\u00e7o de 2017.", 105 | "sent_at": null, 106 | "created_at": "2018-09-06T15:24:14.929Z", 107 | "updated_at": "2018-09-06T15:24:14.929Z", 108 | "moderation_status": true, 109 | "moderation_message": "", 110 | "moderated_at": null, 111 | "attached_file": "" 112 | } 113 | }, 114 | { 115 | "model": "foi_requests.message", 116 | "pk": 5, 117 | "fields": { 118 | "foi_request": 4, 119 | "sender": null, 120 | "receiver": 234, 121 | "summary": "N\u00famero de feminic\u00eddios entre 2015 e 2018", 122 | "body": "Gostaria do n\u00famero de feminic\u00eddios registrados entre 2015 e 2018 em todo o Brasil.", 123 | "sent_at": "2018-08-06T16:21:00Z", 124 | "created_at": "2018-08-05T15:29:46.233Z", 125 | "updated_at": "2018-08-05T15:29:46.233Z", 126 | "moderation_status": true, 127 | "moderation_message": "", 128 | "moderated_at": "2018-08-06T12:00:46.233Z", 129 | "attached_file": "" 130 | } 131 | } 132 | ] 133 | -------------------------------------------------------------------------------- /web/foi_requests/forms.py: -------------------------------------------------------------------------------- 1 | from django.forms import ModelForm, CharField, Textarea 2 | 3 | from .models import Message, Esic, PublicBody, FOIRequest 4 | 5 | 6 | class MessageForm(ModelForm): 7 | class Meta: 8 | model = Message 9 | fields = [ 10 | "receiver", 11 | "summary", 12 | "body", 13 | ] 14 | 15 | summary = CharField() 16 | body = CharField(min_length=55, max_length=2000, widget=Textarea) 17 | 18 | def __init__(self, *args, **kwargs): 19 | super(MessageForm, self).__init__(*args, **kwargs) 20 | self.fields["receiver"].required = True 21 | 22 | 23 | class FOIRequestForm(ModelForm): 24 | class Meta: 25 | model = FOIRequest 26 | fields = [ 27 | "can_publish", 28 | "previous_protocol", 29 | ] 30 | 31 | 32 | class EsicForm(ModelForm): 33 | class Meta: 34 | model = Esic 35 | fields = [ 36 | "url", 37 | ] 38 | 39 | 40 | class PublicBodyForm(ModelForm): 41 | class Meta: 42 | model = PublicBody 43 | fields = [ 44 | "name", 45 | "municipality", 46 | "uf", 47 | "level", 48 | ] 49 | -------------------------------------------------------------------------------- /web/foi_requests/management/commands/load_public_bodies_and_esics.py: -------------------------------------------------------------------------------- 1 | import csv 2 | import io 3 | from urllib.request import urlopen 4 | 5 | from django.core.management.base import BaseCommand 6 | 7 | from web.foi_requests import models 8 | 9 | 10 | ESICS_URL = "https://raw.githubusercontent.com/vitorbaptista/dataset-sics-brasil/master/data/sics-brasil.csv" # noqa: E501 11 | 12 | 13 | class Command(BaseCommand): 14 | help = "Load new PublicBody and ESic from the CSV" 15 | 16 | def add_arguments(self, parser): 17 | parser.add_argument( 18 | "esics_url", 19 | nargs="?", 20 | help="URL to CSV containing the public bodies and eSICs", 21 | default=ESICS_URL, 22 | ) 23 | 24 | def handle(self, *args, **options): 25 | esics_url = options["esics_url"] 26 | 27 | written_count = 0 28 | 29 | for esic in self._esics(esics_url): 30 | _create_or_update_public_body_and_esic(esic) 31 | written_count += 1 32 | 33 | msg = "Loaded {} public bodies and their respective eSICs in the database" 34 | self.stdout.write(msg.format(written_count)) 35 | 36 | def _esics(self, url): 37 | response = urlopen(url) 38 | # It's a pity we're reading everything here, but I couldn't make 39 | # urlopen() read the file in text-mode, so I can't pass it directly to 40 | # the CSV reader 41 | response_text = response.read().decode("utf-8") 42 | return csv.DictReader(io.StringIO(response_text)) 43 | 44 | 45 | def _create_or_update_public_body_and_esic(esic_data): 46 | # TODO: Some e-SICs have e-mails instead 47 | esic_url = esic_data["url"] 48 | esic, _ = models.Esic.objects.update_or_create(url=esic_url) 49 | 50 | public_body, _ = models.PublicBody.objects.update_or_create( 51 | name=esic_data["orgao"], 52 | municipality=esic_data["municipio"], 53 | uf=esic_data["uf"], 54 | defaults={ 55 | "esic": esic, 56 | }, 57 | ) 58 | 59 | return public_body 60 | -------------------------------------------------------------------------------- /web/foi_requests/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.2 on 2018-03-04 13:23 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | initial = True 9 | 10 | dependencies = [] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name="FOIRequest", 15 | fields=[ 16 | ( 17 | "id", 18 | models.AutoField( 19 | auto_created=True, 20 | primary_key=True, 21 | serialize=False, 22 | verbose_name="ID", 23 | ), 24 | ), 25 | ("protocol", models.CharField(max_length=8, unique=True)), 26 | ("esic_protocol", models.CharField(max_length=255)), 27 | ("moderation_status", models.NullBooleanField()), 28 | ("moderation_message", models.TextField(blank=True)), 29 | ("moderated_at", models.DateTimeField(null=True)), 30 | ("created_at", models.DateTimeField(auto_now_add=True)), 31 | ("updated_at", models.DateTimeField(auto_now=True)), 32 | ], 33 | ), 34 | migrations.CreateModel( 35 | name="Message", 36 | fields=[ 37 | ( 38 | "id", 39 | models.AutoField( 40 | auto_created=True, 41 | primary_key=True, 42 | serialize=False, 43 | verbose_name="ID", 44 | ), 45 | ), 46 | ("title", models.TextField(blank=True)), 47 | ("body", models.TextField()), 48 | ("sent_at", models.DateTimeField(null=True)), 49 | ("created_at", models.DateTimeField(auto_now_add=True)), 50 | ("updated_at", models.DateTimeField(auto_now=True)), 51 | ( 52 | "foi_request", 53 | models.ForeignKey( 54 | on_delete=django.db.models.deletion.CASCADE, 55 | to="foi_requests.FOIRequest", 56 | ), 57 | ), 58 | ], 59 | ), 60 | migrations.CreateModel( 61 | name="PublicBody", 62 | fields=[ 63 | ( 64 | "id", 65 | models.AutoField( 66 | auto_created=True, 67 | primary_key=True, 68 | serialize=False, 69 | verbose_name="ID", 70 | ), 71 | ), 72 | ("name", models.CharField(max_length=255)), 73 | ("esic_url", models.URLField()), 74 | ("created_at", models.DateTimeField(auto_now_add=True)), 75 | ("updated_at", models.DateTimeField(auto_now=True)), 76 | ], 77 | ), 78 | migrations.AddField( 79 | model_name="message", 80 | name="receiver", 81 | field=models.ForeignKey( 82 | null=True, 83 | on_delete=django.db.models.deletion.PROTECT, 84 | related_name="messages_received", 85 | to="foi_requests.PublicBody", 86 | ), 87 | ), 88 | migrations.AddField( 89 | model_name="message", 90 | name="sender", 91 | field=models.ForeignKey( 92 | null=True, 93 | on_delete=django.db.models.deletion.PROTECT, 94 | related_name="messages_sent", 95 | to="foi_requests.PublicBody", 96 | ), 97 | ), 98 | ] 99 | -------------------------------------------------------------------------------- /web/foi_requests/migrations/0002_auto_20180328_1901.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.2 on 2018-03-28 19:01 2 | 3 | from django.db import migrations, models 4 | import web.foi_requests.models 5 | import web.foi_requests.utils 6 | 7 | 8 | class Migration(migrations.Migration): 9 | dependencies = [ 10 | ("foi_requests", "0001_initial"), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterModelOptions( 15 | name="foirequest", 16 | options={"ordering": ["-created_at", "-moderation_status"]}, 17 | ), 18 | migrations.AlterModelOptions( 19 | name="message", 20 | options={"ordering": ["-created_at"]}, 21 | ), 22 | migrations.AddField( 23 | model_name="message", 24 | name="attached_file", 25 | field=models.FileField( 26 | blank=True, 27 | null=True, 28 | upload_to=web.foi_requests.models.Message._attached_file_path, 29 | ), 30 | ), 31 | migrations.AlterField( 32 | model_name="foirequest", 33 | name="esic_protocol", 34 | field=models.CharField(blank=True, max_length=255), 35 | ), 36 | migrations.AlterField( 37 | model_name="foirequest", 38 | name="moderation_status", 39 | field=models.NullBooleanField( 40 | choices=[(None, "Pending"), (True, "Approved"), (False, "Rejected")] 41 | ), 42 | ), 43 | migrations.AlterField( 44 | model_name="foirequest", 45 | name="protocol", 46 | field=models.CharField( 47 | default=web.foi_requests.utils.generate_protocol, 48 | max_length=8, 49 | unique=True, 50 | ), 51 | ), 52 | ] 53 | -------------------------------------------------------------------------------- /web/foi_requests/migrations/0003_auto_20180709_1644.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.2 on 2018-07-09 16:44 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("foi_requests", "0002_auto_20180328_1901"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AlterModelOptions( 13 | name="foirequest", 14 | options={"ordering": ["-created_at"]}, 15 | ), 16 | migrations.AlterModelOptions( 17 | name="message", 18 | options={"ordering": ["-created_at", "-moderation_status"]}, 19 | ), 20 | migrations.RemoveField( 21 | model_name="foirequest", 22 | name="moderated_at", 23 | ), 24 | migrations.RemoveField( 25 | model_name="foirequest", 26 | name="moderation_message", 27 | ), 28 | migrations.RemoveField( 29 | model_name="foirequest", 30 | name="moderation_status", 31 | ), 32 | migrations.AddField( 33 | model_name="message", 34 | name="moderated_at", 35 | field=models.DateTimeField(null=True), 36 | ), 37 | migrations.AddField( 38 | model_name="message", 39 | name="moderation_message", 40 | field=models.TextField(blank=True), 41 | ), 42 | migrations.AddField( 43 | model_name="message", 44 | name="moderation_status", 45 | field=models.NullBooleanField( 46 | choices=[(None, "Pending"), (True, "Approved"), (False, "Rejected")] 47 | ), 48 | ), 49 | migrations.AlterField( 50 | model_name="message", 51 | name="sent_at", 52 | field=models.DateTimeField(null=True, verbose_name="Sent date"), 53 | ), 54 | ] 55 | -------------------------------------------------------------------------------- /web/foi_requests/migrations/0004_auto_20180709_1804.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.2 on 2018-07-09 18:04 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | dependencies = [ 9 | ("foi_requests", "0003_auto_20180709_1644"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="message", 15 | name="receiver", 16 | field=models.ForeignKey( 17 | blank=True, 18 | null=True, 19 | on_delete=django.db.models.deletion.PROTECT, 20 | related_name="messages_received", 21 | to="foi_requests.PublicBody", 22 | ), 23 | ), 24 | migrations.AlterField( 25 | model_name="message", 26 | name="sender", 27 | field=models.ForeignKey( 28 | blank=True, 29 | null=True, 30 | on_delete=django.db.models.deletion.PROTECT, 31 | related_name="messages_sent", 32 | to="foi_requests.PublicBody", 33 | ), 34 | ), 35 | ] 36 | -------------------------------------------------------------------------------- /web/foi_requests/migrations/0005_add_message_contraint_to_avoid_rejections_without_message_20180709_1846.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.2 on 2018-07-09 18:46 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("foi_requests", "0004_auto_20180709_1804"), 9 | ] 10 | 11 | operations = [ 12 | migrations.RunSQL( 13 | [ 14 | """ 15 | ALTER TABLE foi_requests_message ADD CONSTRAINT rejected_messages_have_moderation_message CHECK ( 16 | moderation_status IS NOT FALSE OR (moderation_message IS NOT NULL AND moderation_message != '') 17 | ); 18 | """ 19 | ], 20 | [ 21 | """ 22 | ALTER TABLE foi_requests_message DROP CONSTRAINT rejected_messages_have_moderation_message; 23 | """ 24 | ], 25 | ) 26 | ] 27 | -------------------------------------------------------------------------------- /web/foi_requests/migrations/0006_auto_20180710_1228.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.2 on 2018-07-10 12:28 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ( 9 | "foi_requests", 10 | "0005_add_message_contraint_to_avoid_rejections_without_message_20180709_1846", 11 | ), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name="message", 17 | name="moderated_at", 18 | field=models.DateTimeField(blank=True, null=True), 19 | ), 20 | migrations.AlterField( 21 | model_name="message", 22 | name="sent_at", 23 | field=models.DateTimeField(blank=True, null=True, verbose_name="Sent date"), 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /web/foi_requests/migrations/0007_add_constraint_foi_requests_message_can_only_send_approved_messages_20180710_1228.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.2 on 2018-07-10 12:28 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("foi_requests", "0006_auto_20180710_1228"), 9 | ] 10 | 11 | operations = [ 12 | migrations.RunSQL( 13 | [ 14 | "ALTER TABLE foi_requests_message ADD CONSTRAINT only_approved_messages_can_have_sent_at CHECK (sent_at IS NULL OR moderation_status IS TRUE);", 15 | ], 16 | [ 17 | "ALTER TABLE foi_requests_message DROP CONSTRAINT only_approved_messages_can_have_sent_at;", 18 | ], 19 | ) 20 | ] 21 | -------------------------------------------------------------------------------- /web/foi_requests/migrations/0008_auto_20180717_1131.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.2 on 2018-07-17 14:31 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ( 9 | "foi_requests", 10 | "0007_add_constraint_foi_requests_message_can_only_send_approved_messages_20180710_1228", 11 | ), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name="publicbody", 17 | name="name", 18 | field=models.CharField(max_length=255, unique=True), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /web/foi_requests/migrations/0009_extract_esic_from_publicbody_and_add_location_fields_to_it.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.2 on 2018-08-03 11:44 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | dependencies = [ 9 | ("foi_requests", "0008_auto_20180717_1131"), 10 | ] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name="Esic", 15 | fields=[ 16 | ( 17 | "id", 18 | models.AutoField( 19 | auto_created=True, 20 | primary_key=True, 21 | serialize=False, 22 | verbose_name="ID", 23 | ), 24 | ), 25 | ("url", models.URLField(unique=True)), 26 | ("username", models.CharField(blank=True, max_length=255)), 27 | ("created_at", models.DateTimeField(auto_now_add=True)), 28 | ("updated_at", models.DateTimeField(auto_now=True)), 29 | ], 30 | ), 31 | migrations.RemoveField( 32 | model_name="publicbody", 33 | name="esic_url", 34 | ), 35 | migrations.AddField( 36 | model_name="publicbody", 37 | name="municipality", 38 | field=models.CharField(blank=True, max_length=255), 39 | ), 40 | migrations.AddField( 41 | model_name="publicbody", 42 | name="uf", 43 | field=models.CharField( 44 | blank=True, 45 | choices=[ 46 | ("AC", "Acre"), 47 | ("AL", "Alagoas"), 48 | ("AM", "Amazonas"), 49 | ("AP", "Amapá"), 50 | ("BA", "Bahia"), 51 | ("CE", "Ceará"), 52 | ("DF", "DF"), 53 | ("ES", "Espírito Santo"), 54 | ("GO", "Goiás"), 55 | ("MA", "Maranhão"), 56 | ("MG", "Minas Gerais"), 57 | ("MS", "Mato Grosso do Sul"), 58 | ("MT", "Mato Grosso"), 59 | ("PA", "Pará"), 60 | ("PB", "Paraíba"), 61 | ("PE", "Pernambuco"), 62 | ("PI", "Piauí"), 63 | ("PR", "Paraná"), 64 | ("RJ", "Rio de Janeiro"), 65 | ("RN", "Rio Grande do Norte"), 66 | ("RO", "Rondônia"), 67 | ("RR", "Roraima"), 68 | ("RS", "Rio Grande do Sul"), 69 | ("SC", "Santa Catarina"), 70 | ("SE", "Sergipe"), 71 | ("SP", "São Paulo"), 72 | ("TO", "Tocantins"), 73 | ], 74 | max_length=2, 75 | ), 76 | ), 77 | migrations.AddField( 78 | model_name="publicbody", 79 | name="esic", 80 | field=models.ForeignKey( 81 | blank=True, 82 | null=True, 83 | on_delete=django.db.models.deletion.PROTECT, 84 | to="foi_requests.Esic", 85 | ), 86 | ), 87 | ] 88 | -------------------------------------------------------------------------------- /web/foi_requests/migrations/0010_rename_message_title_to_summary.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.1 on 2018-09-07 10:38 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ( 9 | "foi_requests", 10 | "0009_extract_esic_from_publicbody_and_add_location_fields_to_it", 11 | ), 12 | ] 13 | 14 | operations = [ 15 | migrations.RenameField( 16 | model_name="message", 17 | old_name="title", 18 | new_name="summary", 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /web/foi_requests/migrations/0011_add_can_publish_field_to_foi_request.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.1 on 2018-09-21 15:44 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("foi_requests", "0010_rename_message_title_to_summary"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AddField( 13 | model_name="foirequest", 14 | name="can_publish", 15 | field=models.BooleanField(default=False), 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /web/foi_requests/migrations/0012_add_public_body_level.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.2 on 2019-02-09 18:11 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("foi_requests", "0011_add_can_publish_field_to_foi_request"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AddField( 13 | model_name="publicbody", 14 | name="level", 15 | field=models.CharField( 16 | choices=[ 17 | ("Local", "Municipal"), 18 | ("State", "Estadual"), 19 | ("Federal", "Federal"), 20 | ], 21 | default="Local", 22 | max_length=10, 23 | ), 24 | ), 25 | migrations.AlterField( 26 | model_name="foirequest", 27 | name="can_publish", 28 | field=models.BooleanField(default=True), 29 | ), 30 | ] 31 | -------------------------------------------------------------------------------- /web/foi_requests/migrations/0013_auto_20190814_1521.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.1 on 2019-08-14 18:21 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("foi_requests", "0012_add_public_body_level"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AlterModelOptions( 13 | name="publicbody", 14 | options={"ordering": ["name"]}, 15 | ), 16 | migrations.AddField( 17 | model_name="foirequest", 18 | name="previous_protocol", 19 | field=models.CharField(blank=True, max_length=8), 20 | ), 21 | migrations.AlterField( 22 | model_name="publicbody", 23 | name="uf", 24 | field=models.CharField( 25 | blank=True, 26 | choices=[ 27 | ("AC", "Acre"), 28 | ("AL", "Alagoas"), 29 | ("AM", "Amazonas"), 30 | ("AP", "Amapá"), 31 | ("BA", "Bahia"), 32 | ("CE", "Ceará"), 33 | ("DF", "Distrito Federal"), 34 | ("ES", "Espírito Santo"), 35 | ("GO", "Goiás"), 36 | ("MA", "Maranhão"), 37 | ("MG", "Minas Gerais"), 38 | ("MS", "Mato Grosso do Sul"), 39 | ("MT", "Mato Grosso"), 40 | ("PA", "Pará"), 41 | ("PB", "Paraíba"), 42 | ("PE", "Pernambuco"), 43 | ("PI", "Piauí"), 44 | ("PR", "Paraná"), 45 | ("RJ", "Rio de Janeiro"), 46 | ("RN", "Rio Grande do Norte"), 47 | ("RO", "Rondônia"), 48 | ("RR", "Roraima"), 49 | ("RS", "Rio Grande do Sul"), 50 | ("SC", "Santa Catarina"), 51 | ("SE", "Sergipe"), 52 | ("SP", "São Paulo"), 53 | ("TO", "Tocantins"), 54 | ], 55 | max_length=2, 56 | ), 57 | ), 58 | ] 59 | -------------------------------------------------------------------------------- /web/foi_requests/migrations/0014_foirequest_esic_protocol_password.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.28 on 2024-11-20 13:07 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("foi_requests", "0013_auto_20190814_1521"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AddField( 13 | model_name="foirequest", 14 | name="esic_protocol_password", 15 | field=models.CharField(blank=True, max_length=255, null=True), 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /web/foi_requests/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FiquemSabendo/queremossaber/81f02d0ba8cc07ee0d2176236e99653b8dac3775/web/foi_requests/migrations/__init__.py -------------------------------------------------------------------------------- /web/foi_requests/models.py: -------------------------------------------------------------------------------- 1 | import os 2 | import enum 3 | import hashlib 4 | from django.db import models 5 | from django.core.exceptions import ValidationError 6 | from django.utils import timezone 7 | from django.utils.translation import gettext_lazy as _ 8 | 9 | from . import utils 10 | 11 | 12 | class Esic(models.Model): 13 | url = models.URLField(unique=True) 14 | username = models.CharField(max_length=255, blank=True) 15 | # NOTE: This `username` will be extracted to its own class later, whenever 16 | # we have multiple accounts per eSIC. Meanwhile, we'll keep it here for 17 | # simplicity. This is the username in the eSIC system, for systems that 18 | # have logins. 19 | created_at = models.DateTimeField(auto_now_add=True) 20 | updated_at = models.DateTimeField(auto_now=True) 21 | 22 | def __str__(self): 23 | return self.url 24 | 25 | 26 | class PublicBody(models.Model): 27 | LEVELS = ( 28 | ("Local", "Municipal"), 29 | ("State", "Estadual"), 30 | ("Federal", "Federal"), 31 | ) 32 | 33 | UFS = ( 34 | ("AC", "Acre"), 35 | ("AL", "Alagoas"), 36 | ("AM", "Amazonas"), 37 | ("AP", "Amapá"), 38 | ("BA", "Bahia"), 39 | ("CE", "Ceará"), 40 | ("DF", "Distrito Federal"), 41 | ("ES", "Espírito Santo"), 42 | ("GO", "Goiás"), 43 | ("MA", "Maranhão"), 44 | ("MG", "Minas Gerais"), 45 | ("MS", "Mato Grosso do Sul"), 46 | ("MT", "Mato Grosso"), 47 | ("PA", "Pará"), 48 | ("PB", "Paraíba"), 49 | ("PE", "Pernambuco"), 50 | ("PI", "Piauí"), 51 | ("PR", "Paraná"), 52 | ("RJ", "Rio de Janeiro"), 53 | ("RN", "Rio Grande do Norte"), 54 | ("RO", "Rondônia"), 55 | ("RR", "Roraima"), 56 | ("RS", "Rio Grande do Sul"), 57 | ("SC", "Santa Catarina"), 58 | ("SE", "Sergipe"), 59 | ("SP", "São Paulo"), 60 | ("TO", "Tocantins"), 61 | ) 62 | 63 | # NOTE: We might add a "parent_public_body" attribute later to deal with 64 | # cases like Secretaria de Meio-Ambiente de São Paulo, whose parent would 65 | # be the Prefeitura de São Paulo. If the time comes, We can get a very 66 | # similar relationship by looking for the PublicBodies that are in the 67 | # same Esic system. This is a pretty clear relationship. 68 | esic = models.ForeignKey(Esic, null=True, blank=True, on_delete=models.PROTECT) 69 | name = models.CharField(max_length=255, blank=False, unique=True) 70 | level = models.CharField(max_length=10, choices=LEVELS, default="Local") 71 | municipality = models.CharField(max_length=255, blank=True) 72 | uf = models.CharField(max_length=2, choices=UFS, blank=True) 73 | created_at = models.DateTimeField(auto_now_add=True) 74 | updated_at = models.DateTimeField(auto_now=True) 75 | 76 | class Meta: 77 | ordering = ["name"] 78 | 79 | def __str__(self): 80 | return self.name 81 | 82 | def clean(self, *args, **kwargs): 83 | error_data = {} 84 | 85 | if self.level == "Local": 86 | if not self.uf: 87 | error_data["uf"] = _('Local Public Bodies must have a "UF".') 88 | if not self.municipality: 89 | error_data["municipality"] = _( 90 | 'Local Public Bodies must have a "municipality".' 91 | ) 92 | elif self.level == "State": 93 | if not self.uf: 94 | error_data["uf"] = _('State Public Bodies must have a "UF".') 95 | if self.municipality: 96 | msg = _('State Public Bodies must not have a "municipality".') 97 | error_data["municipality"] = msg 98 | elif self.level == "Federal": 99 | if self.uf: 100 | error_data["uf"] = _('Federal Public Bodies must not have a "UF".') 101 | if self.municipality: 102 | msg = _('Federal Public Bodies must not have a "municipality".') 103 | error_data["municipality"] = msg 104 | 105 | if error_data: 106 | raise ValidationError(error_data) 107 | 108 | return super(PublicBody, self).clean(*args, **kwargs) 109 | 110 | 111 | class FOIRequest(models.Model): 112 | class STATUS(enum.Enum): 113 | delayed = _("Delayed") 114 | finished = _("Finished") 115 | waiting_government = _("Waiting for government reply") 116 | waiting_user = _("Waiting for user reply") 117 | 118 | REPLY_DAYS = 20 # Public body has to answer in X days 119 | APPEAL_DAYS = 10 # Citizen can appeal in X days 120 | 121 | protocol = models.CharField( 122 | max_length=8, unique=True, default=utils.generate_protocol 123 | ) 124 | # previous_protocol isn't a foreign key to avoid leaking the information on 125 | # what protocols exist by raising an exception if a user adds an inexistent 126 | # previous_protocol. 127 | previous_protocol = models.CharField(max_length=8, blank=True) 128 | esic_protocol = models.CharField(max_length=255, blank=True) 129 | esic_protocol_password = models.CharField(max_length=255, blank=True, null=True) 130 | created_at = models.DateTimeField(auto_now_add=True) 131 | updated_at = models.DateTimeField(auto_now=True) 132 | can_publish = models.BooleanField(default=True) 133 | 134 | class Meta: 135 | ordering = ["-created_at"] 136 | 137 | def __init__(self, *args, **kwargs): 138 | super(FOIRequest, self).__init__(*args, **kwargs) 139 | self._original_protocol = self.protocol 140 | 141 | def save(self, *args, **kwargs): 142 | self.clean() 143 | return super(FOIRequest, self).save(*args, **kwargs) 144 | 145 | def clean(self, *args, **kwargs): 146 | if self._original_protocol != self.protocol: 147 | raise ValidationError({"protocol": _("Protocol can not be changed.")}) 148 | return super(FOIRequest, self).clean(*args, **kwargs) 149 | 150 | def get_absolute_url(self): 151 | from django.urls import reverse 152 | 153 | return reverse("foirequest_detail", args=[self.protocol]) 154 | 155 | def __str__(self): 156 | return self.protocol 157 | 158 | @property 159 | def public_body(self): 160 | if self.first_message: 161 | return self.first_message.receiver 162 | 163 | @property 164 | def esic(self): 165 | if self.public_body: 166 | return self.public_body.esic 167 | 168 | @property 169 | def summary(self): 170 | if self.first_message: 171 | return self.first_message.summary 172 | 173 | @property 174 | def status(self): 175 | last_message = self.last_message 176 | status = None 177 | 178 | if not last_message: 179 | status = self.STATUS.waiting_user 180 | elif not last_message.is_from_user: 181 | appeal_deadline = timezone.now() - timezone.timedelta(days=self.APPEAL_DAYS) 182 | if last_message.sent_at <= appeal_deadline: 183 | status = self.STATUS.finished 184 | else: 185 | status = self.STATUS.waiting_user 186 | pass 187 | elif last_message.is_sent: 188 | reply_deadline = timezone.now() - timezone.timedelta(days=self.REPLY_DAYS) 189 | if last_message.sent_at <= reply_deadline: 190 | status = self.STATUS.delayed 191 | else: 192 | status = self.STATUS.waiting_government 193 | else: 194 | status = last_message.status 195 | 196 | return status 197 | 198 | @property 199 | def moderation_message(self): 200 | if not self.pk: 201 | return None 202 | first_message = self.first_message 203 | if first_message: 204 | return first_message.moderation_message 205 | 206 | @property 207 | def first_message(self): 208 | if not self.pk: 209 | return None 210 | return self.message_set.order_by("created_at").first() 211 | 212 | @property 213 | def last_message(self): 214 | if not self.pk: 215 | return None 216 | return self.message_set.order_by("created_at").last() 217 | 218 | 219 | class Message(models.Model): 220 | class STATUS(enum.Enum): 221 | pending = _("Pending moderation") 222 | rejected = _("Rejected") 223 | ready = _("Ready to be sent") 224 | sent = _("Sent") 225 | 226 | foi_request = models.ForeignKey(FOIRequest, on_delete=models.CASCADE) 227 | sender = models.ForeignKey( 228 | PublicBody, 229 | null=True, 230 | blank=True, 231 | related_name="messages_sent", 232 | on_delete=models.PROTECT, 233 | ) 234 | receiver = models.ForeignKey( 235 | PublicBody, 236 | null=True, 237 | blank=True, 238 | related_name="messages_received", 239 | on_delete=models.PROTECT, 240 | ) 241 | summary = models.TextField(blank=True) 242 | body = models.TextField(blank=False) 243 | sent_at = models.DateTimeField(null=True, blank=True, verbose_name="Sent date") 244 | created_at = models.DateTimeField(auto_now_add=True) 245 | updated_at = models.DateTimeField(auto_now=True) 246 | 247 | # Moderation-related attributes 248 | moderation_status = models.BooleanField( 249 | null=True, 250 | choices=( 251 | (None, "Pending"), 252 | (True, "Approved"), 253 | (False, "Rejected"), 254 | ), 255 | ) 256 | moderation_message = models.TextField(blank=True) 257 | moderated_at = models.DateTimeField(null=True, blank=True) 258 | 259 | @property 260 | def status(self): 261 | status = None 262 | 263 | if self.is_pending_moderation: 264 | status = self.STATUS.pending 265 | elif self.is_rejected: 266 | status = self.STATUS.rejected 267 | elif not self.is_sent: 268 | status = self.STATUS.ready 269 | else: 270 | status = self.STATUS.sent 271 | 272 | return status 273 | 274 | @property 275 | def sender_type(self): 276 | sender_type = "user" 277 | if self.sender is not None: 278 | sender_type = "government" 279 | return sender_type 280 | 281 | def __str__(self): 282 | summary = self.summary 283 | if not summary: 284 | summary = self.body[0:100] 285 | 286 | return "(%s) %s" % (self.sender_type, summary) 287 | 288 | def _attached_file_path(self, filename): 289 | root, ext = os.path.splitext(filename) 290 | hash_size = 24 291 | 292 | hasher = hashlib.sha256() 293 | hasher.update(root.encode("utf-8")) 294 | hashed_filename = "{}{}".format(hasher.hexdigest()[:hash_size], ext) 295 | 296 | return hashed_filename 297 | 298 | attached_file = models.FileField( 299 | upload_to=_attached_file_path, blank=True, null=True 300 | ) 301 | 302 | @property 303 | def is_from_user(self): 304 | return self.sender is None 305 | 306 | @property 307 | def is_approved(self): 308 | return self.moderation_status is True 309 | 310 | @property 311 | def is_rejected(self): 312 | return self.moderation_status is False 313 | 314 | @property 315 | def is_pending_moderation(self): 316 | return self.moderation_status is None 317 | 318 | @property 319 | def is_sent(self): 320 | return self.sent_at is not None 321 | 322 | class Meta: 323 | ordering = ["-created_at", "-moderation_status"] 324 | 325 | def __init__(self, *args, **kwargs): 326 | super(Message, self).__init__(*args, **kwargs) 327 | self._original_moderation_status = self.moderation_status 328 | 329 | def save(self, *args, **kwargs): 330 | self.clean() 331 | return super(Message, self).save(*args, **kwargs) 332 | 333 | def clean(self): 334 | self._update_moderated_at_if_needed() 335 | 336 | if self.sender and self.receiver: 337 | msg = _('Message can either have a "sender" or a "receiver", not both.') 338 | raise ValidationError( 339 | { 340 | "sender": msg, 341 | "receiver": msg, 342 | } 343 | ) 344 | 345 | if not self.is_from_user: 346 | # Government messages are automatically approved 347 | if not self.moderation_status: 348 | self.approve() 349 | if not self.sent_at: 350 | raise ValidationError( 351 | { 352 | "sent_at": _('Government messages must have a "sent_at" date.'), 353 | } 354 | ) 355 | 356 | if self.is_from_user and not self.is_approved: 357 | if self.sent_at is not None: 358 | raise ValidationError( 359 | { 360 | "sent_at": _( 361 | "Only approved user messages can be marked as sent." 362 | ), 363 | } 364 | ) 365 | 366 | if self.is_rejected and not self.moderation_message: 367 | raise ValidationError( 368 | { 369 | "moderation_status": _( 370 | "A message can not be rejected without an explanation." 371 | ), # noqa: E501 372 | } 373 | ) 374 | 375 | def approve(self): 376 | self.moderated_at = timezone.now() 377 | self.moderation_status = True 378 | 379 | def reject(self): 380 | self.moderated_at = timezone.now() 381 | self.moderation_status = False 382 | 383 | def get_absolute_url(self): 384 | return self.foi_request.get_absolute_url() 385 | 386 | def _update_moderated_at_if_needed(self): 387 | if self._original_moderation_status != self.moderation_status: 388 | self.moderated_at = timezone.now() 389 | self._original_moderation_status = self.moderation_status 390 | -------------------------------------------------------------------------------- /web/foi_requests/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FiquemSabendo/queremossaber/81f02d0ba8cc07ee0d2176236e99653b8dac3775/web/foi_requests/tests/__init__.py -------------------------------------------------------------------------------- /web/foi_requests/tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from django.db import transaction 3 | from django.utils import timezone 4 | 5 | from ..models import Message, FOIRequest, Esic, PublicBody 6 | 7 | 8 | @pytest.fixture 9 | def public_body(esic): 10 | return PublicBody(name="example", esic=esic) 11 | 12 | 13 | @pytest.fixture 14 | def esic(): 15 | return Esic(url="http://example.com") 16 | 17 | 18 | @pytest.fixture 19 | def foi_request(): 20 | return FOIRequest() 21 | 22 | 23 | @pytest.fixture 24 | def message(foi_request): 25 | return Message(foi_request=foi_request) 26 | 27 | 28 | @pytest.fixture 29 | def foi_request_with_sent_user_message(foi_request, message_from_user): 30 | with transaction.atomic(): 31 | message_from_user.approve() 32 | message_from_user.foi_request = foi_request 33 | message_from_user.sent_at = timezone.now() 34 | save_message(message_from_user) 35 | foi_request.refresh_from_db() 36 | return foi_request 37 | 38 | 39 | @pytest.fixture 40 | def message_from_user(public_body): 41 | return Message(sender=None, receiver=public_body) 42 | 43 | 44 | @pytest.fixture 45 | def message_from_government(public_body): 46 | return Message(sender=public_body, sent_at=timezone.now(), receiver=None) 47 | 48 | 49 | def save_message(message): 50 | # FIXME: Ideally a simple message.save() would save everything, but I 51 | # couldn't find out how to do so in Django. Not yet. 52 | with transaction.atomic(): 53 | if message.sender: 54 | save_public_body(message.sender) 55 | message.sender_id = message.sender.id 56 | if message.receiver: 57 | save_public_body(message.receiver) 58 | message.receiver_id = message.receiver.id 59 | message.foi_request.save() 60 | message.foi_request_id = message.foi_request.id 61 | message.save() 62 | 63 | 64 | def save_public_body(public_body): 65 | with transaction.atomic(): 66 | if public_body.esic: 67 | public_body.esic.save() 68 | public_body.esic_id = public_body.esic.id 69 | public_body.save() 70 | return public_body 71 | -------------------------------------------------------------------------------- /web/foi_requests/tests/test_admin.py: -------------------------------------------------------------------------------- 1 | import unittest.mock as mock 2 | import pytest 3 | 4 | from ..admin import ( 5 | ModerationStatusListFilter, 6 | ModerationSenderTypeFilter, 7 | ) 8 | 9 | 10 | class TestModerationStatusListFilter: 11 | def test_lookups_are_as_expected(self): 12 | list_filter = self._create_list_filter() 13 | lookup_keys = [lookup[0] for lookup in list_filter.lookups(None, None)] 14 | assert lookup_keys == [ 15 | "pending", 16 | "approved_not_sent", 17 | "sent", 18 | "rejected", 19 | ] 20 | 21 | @pytest.mark.parametrize( 22 | "value,filters", 23 | ( 24 | (None, {}), 25 | ("pending", {"moderation_status": None}), 26 | ("approved_not_sent", {"moderation_status": True, "sent_at__isnull": True}), 27 | ("sent", {"moderation_status": True, "sent_at__isnull": False}), 28 | ("rejected", {"moderation_status": False}), 29 | ), 30 | ) 31 | def test_queryset(self, value, filters): 32 | list_filter = self._create_list_filter(value) 33 | queryset = mock.Mock() 34 | list_filter.queryset(None, queryset) 35 | 36 | queryset.filter.assert_called_with(**filters) 37 | 38 | def _create_list_filter(self, params_value=None): 39 | params = { 40 | ModerationStatusListFilter.parameter_name: [params_value], 41 | } 42 | return ModerationStatusListFilter( 43 | None, # request 44 | params, # params 45 | None, # Model 46 | None, # ModelAdmin 47 | ) 48 | 49 | 50 | class TestModerationSenderTypeFilter: 51 | @pytest.mark.parametrize( 52 | "value,filters", 53 | ( 54 | ("user", {"sender_id__isnull": False}), 55 | ("government", {"sender_id__isnull": True}), 56 | ), 57 | ) 58 | def test_queryset(self, value, filters): 59 | list_filter = self._create_list_filter(value) 60 | queryset = mock.Mock() 61 | list_filter.queryset(None, queryset) 62 | 63 | queryset.exclude.assert_called_with(**filters) 64 | 65 | def test_queryset_doesnt_filter_if_value_is_none(self): 66 | list_filter = self._create_list_filter() 67 | queryset = mock.Mock() 68 | list_filter.queryset(None, queryset) 69 | 70 | queryset.exclude.assert_not_called() 71 | 72 | def _create_list_filter(self, params_value=None): 73 | params = { 74 | ModerationSenderTypeFilter.parameter_name: [params_value], 75 | } 76 | return ModerationSenderTypeFilter( 77 | None, # request 78 | params, # params 79 | None, # Model 80 | None, # ModelAdmin 81 | ) 82 | -------------------------------------------------------------------------------- /web/foi_requests/tests/test_commands.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from ..management.commands.load_public_bodies_and_esics import ( 4 | _create_or_update_public_body_and_esic, 5 | ) 6 | 7 | 8 | class TestLoadPublicBodiesAndEsicsCommand(object): 9 | @pytest.mark.django_db() 10 | def test_create_or_update_public_body_and_esic(self, esic_data): 11 | public_body = _create_or_update_public_body_and_esic(esic_data) 12 | 13 | assert public_body.name == esic_data["orgao"] 14 | assert public_body.uf == esic_data["uf"] 15 | assert public_body.municipality == esic_data["municipio"] 16 | assert public_body.esic.url == esic_data["url"] 17 | 18 | is_saved = lambda model: model.pk is not None # noqa: E731 19 | assert is_saved(public_body) 20 | assert is_saved(public_body.esic) 21 | 22 | 23 | @pytest.fixture 24 | def esic_data(): 25 | return { 26 | "orgao": "ACME Inc", 27 | "uf": "DF", 28 | "municipio": "Brasília", 29 | "cod_ibge": "5300108", 30 | "url": "http://www.example.com", 31 | } 32 | -------------------------------------------------------------------------------- /web/foi_requests/tests/test_forms.py: -------------------------------------------------------------------------------- 1 | from ..forms import MessageForm 2 | 3 | 4 | class TestMessageForm: 5 | def test_receiver_is_required(self): 6 | assert MessageForm().fields["receiver"].required 7 | -------------------------------------------------------------------------------- /web/foi_requests/tests/test_models.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from django.urls import reverse 3 | from django.db import transaction 4 | from django.db.utils import IntegrityError 5 | from django.core.exceptions import ValidationError 6 | from django.utils import timezone 7 | 8 | from ..models import Message, FOIRequest, PublicBody 9 | from .conftest import save_message 10 | 11 | 12 | class TestMessage(object): 13 | @pytest.mark.django_db() 14 | def test_foi_request_isnt_created_if_message_creation_fails(self): 15 | initial_foi_requests_count = FOIRequest.objects.count() 16 | message = Message() 17 | message.body = None 18 | 19 | with transaction.atomic(): 20 | with pytest.raises(IntegrityError): 21 | message.save() 22 | 23 | assert initial_foi_requests_count == FOIRequest.objects.count() 24 | 25 | @pytest.mark.django_db() 26 | def test_message_doesnt_create_new_foi_request_if_it_already_has_one( 27 | self, foi_request 28 | ): 29 | foi_request.save() 30 | message = Message(foi_request=foi_request) 31 | message.save() 32 | 33 | assert message.foi_request == foi_request 34 | 35 | def test_get_absolute_url_points_to_foi_request_absolute_url(self, message): 36 | assert message.get_absolute_url() == message.foi_request.get_absolute_url() 37 | 38 | @pytest.mark.django_db() 39 | def test_message_updates_moderated_at_when_moderation_status_is_set(self, message): 40 | assert message.moderated_at is None 41 | 42 | message.moderation_status = True 43 | save_message(message) 44 | 45 | assert message.moderated_at is not None 46 | 47 | def test_message_non_approved_can_not_have_sent_at(self): 48 | message = Message(moderation_status=None, sent_at=timezone.now()) 49 | 50 | with pytest.raises(ValidationError): 51 | message.clean() 52 | 53 | def test_message_approved_can_have_sent_at(self): 54 | message = Message(moderation_status=True, sent_at=timezone.now()) 55 | 56 | message.clean() 57 | 58 | def test_message_approve_approves_the_message(self): 59 | message = Message(moderation_status=None) 60 | assert not message.is_approved 61 | 62 | message.approve() 63 | 64 | assert message.is_approved 65 | 66 | def test_message_reject_rejects_the_message(self): 67 | message = Message(moderation_status=None) 68 | assert not message.is_rejected 69 | 70 | message.reject() 71 | 72 | assert message.is_rejected 73 | 74 | def test_message_reject_fails_if_moderation_message_is_empty(self): 75 | message = Message(moderation_message="") 76 | 77 | message.clean() 78 | 79 | message.reject() 80 | 81 | with pytest.raises(ValidationError): 82 | message.clean() 83 | 84 | def test_is_from_user_is_true_if_sender_is_none(self): 85 | message = Message(sender=None) 86 | 87 | assert message.is_from_user 88 | 89 | def test_is_from_user_is_false_if_sender_is_not_none(self): 90 | public_body = PublicBody() 91 | message = Message(sender=public_body) 92 | 93 | assert not message.is_from_user 94 | 95 | def test_government_messages_must_have_sent_at(self, message_from_government): 96 | message_from_government.sent_at = None 97 | 98 | with pytest.raises(ValidationError) as excinfo: 99 | message_from_government.clean() 100 | 101 | assert "sent_at" in excinfo.value.error_dict 102 | 103 | def test_message_is_automatically_approved_when_sender_is_government( 104 | self, message_from_government 105 | ): 106 | message_from_government.moderation_status = None 107 | 108 | message_from_government.clean() 109 | 110 | assert message_from_government.is_approved 111 | 112 | def test_message_is_not_automatically_approved_when_sender_is_user( 113 | self, message_from_user 114 | ): 115 | message_from_user.moderation_status = None 116 | 117 | message_from_user.clean() 118 | 119 | assert not message_from_user.is_approved 120 | 121 | def test_not_moderated_user_message_has_status_pending(self, message_from_user): 122 | message_from_user.moderation_status = None 123 | 124 | assert message_from_user.is_pending_moderation 125 | assert message_from_user.status == Message.STATUS.pending 126 | 127 | def test_rejected_user_message_has_status_rejected(self, message_from_user): 128 | message_from_user.reject() 129 | 130 | assert message_from_user.status == Message.STATUS.rejected 131 | 132 | def test_approved_unsent_user_message_has_status_ready(self, message_from_user): 133 | message_from_user.approve() 134 | message_from_user.sent_at = None 135 | 136 | assert message_from_user.status == Message.STATUS.ready 137 | 138 | def test_sent_user_message_has_status_sent(self, message_from_user): 139 | message_from_user.approve() 140 | message_from_user.sent_at = timezone.now() 141 | 142 | assert message_from_user.status == Message.STATUS.sent 143 | 144 | def test_attached_file_hashes_filename(self, message): 145 | filename = "foo.pdf" 146 | expected_filename = "2c26b46b68ffc68ff99b453c.pdf" 147 | 148 | generated_filename = message.attached_file.field.upload_to(message, filename) 149 | 150 | assert generated_filename.endswith(expected_filename) 151 | 152 | def test_message_cant_have_both_sender_and_receiver(self, message, public_body): 153 | message.sender = public_body 154 | message.receiver = public_body 155 | 156 | with pytest.raises(ValidationError) as excinfo: 157 | message.clean() 158 | 159 | assert "sender" in excinfo.value.error_dict 160 | assert "receiver" in excinfo.value.error_dict 161 | 162 | 163 | class TestFOIRequest(object): 164 | def test_protocol_is_automatically_generated(self): 165 | foi_request = FOIRequest() 166 | 167 | assert foi_request.protocol is not None 168 | 169 | def test_protocol_is_unique(self): 170 | foi_request_1 = FOIRequest() 171 | foi_request_2 = FOIRequest() 172 | 173 | assert foi_request_1.protocol != foi_request_2.protocol 174 | 175 | def test_str_includes_protocol(self): 176 | foi_request = FOIRequest() 177 | 178 | assert foi_request.protocol in str(foi_request) 179 | 180 | @pytest.mark.django_db() 181 | def test_public_body_returns_first_messages_receiver( 182 | self, public_body, foi_request 183 | ): 184 | first_message = Message(foi_request=foi_request, receiver=public_body) 185 | last_message = Message(foi_request=foi_request, receiver=None) 186 | 187 | save_message(first_message) 188 | save_message(last_message) 189 | 190 | assert foi_request.public_body == first_message.receiver 191 | 192 | def test_public_body_returns_none_if_there_are_no_messages(self): 193 | assert FOIRequest().public_body is None 194 | 195 | @pytest.mark.django_db() 196 | def test_esic_returns_public_body_esic(self, public_body, foi_request): 197 | assert public_body.esic 198 | message = Message(foi_request=foi_request, receiver=public_body) 199 | 200 | save_message(message) 201 | 202 | assert foi_request.esic == foi_request.public_body.esic 203 | 204 | def test_esic_returns_none_if_there_are_no_messages(self): 205 | assert FOIRequest().public_body is None 206 | 207 | @pytest.mark.django_db() 208 | def test_protocol_cant_be_changed(self, foi_request): 209 | foi_request.save() 210 | original_protocol = foi_request.protocol 211 | 212 | with pytest.raises(ValidationError): 213 | foi_request.protocol = "somethingelse" 214 | foi_request.save() 215 | 216 | foi_request.refresh_from_db() 217 | assert foi_request.protocol == original_protocol 218 | 219 | @pytest.mark.django_db() 220 | def test_last_message_returns_the_last_created_message(self, foi_request): 221 | first_message = Message(foi_request=foi_request) 222 | last_message = Message(foi_request=foi_request) 223 | 224 | save_message(first_message) 225 | save_message(last_message) 226 | 227 | foi_request.refresh_from_db() 228 | assert foi_request.last_message == last_message 229 | 230 | @pytest.mark.django_db() 231 | @pytest.mark.parametrize( 232 | "sent_at,status", 233 | [ 234 | ( 235 | timezone.now() - timezone.timedelta(days=FOIRequest.REPLY_DAYS), 236 | FOIRequest.STATUS.delayed, 237 | ), 238 | ( 239 | timezone.now() - timezone.timedelta(days=FOIRequest.REPLY_DAYS - 1), 240 | FOIRequest.STATUS.waiting_government, 241 | ), 242 | ], 243 | ) 244 | def test_status_last_message_is_sent_and_from_user( 245 | self, sent_at, status, foi_request_with_sent_user_message 246 | ): 247 | last_message = foi_request_with_sent_user_message.last_message 248 | last_message.sent_at = sent_at 249 | last_message.save() 250 | 251 | assert status == foi_request_with_sent_user_message.status 252 | 253 | @pytest.mark.django_db() 254 | @pytest.mark.parametrize( 255 | "sent_at,status", 256 | [ 257 | ( 258 | timezone.now() - timezone.timedelta(days=FOIRequest.APPEAL_DAYS), 259 | FOIRequest.STATUS.finished, 260 | ), 261 | ( 262 | timezone.now() - timezone.timedelta(days=FOIRequest.APPEAL_DAYS - 1), 263 | FOIRequest.STATUS.waiting_user, 264 | ), 265 | ], 266 | ) 267 | def test_status_last_message_is_from_government( 268 | self, sent_at, status, foi_request, message_from_government 269 | ): 270 | with transaction.atomic(): 271 | foi_request.save() 272 | message_from_government.foi_request = foi_request 273 | message_from_government.sent_at = sent_at 274 | save_message(message_from_government) 275 | foi_request.refresh_from_db() 276 | 277 | assert not message_from_government.is_from_user 278 | assert status == foi_request.status 279 | 280 | def test_status_is_waiting_user_when_there_are_no_messages(self, foi_request): 281 | assert foi_request.last_message is None 282 | assert foi_request.status is FOIRequest.STATUS.waiting_user 283 | 284 | @pytest.mark.django_db() 285 | def test_summary_returns_first_messages_summary(self): 286 | foi_request = FOIRequest() 287 | 288 | with transaction.atomic(): 289 | foi_request.save() 290 | first_message = Message(foi_request=foi_request, summary="First message") 291 | last_message = Message(foi_request=foi_request, summary="Last message") 292 | first_message.save() 293 | last_message.save() 294 | foi_request.message_set.set([first_message, last_message]) 295 | 296 | assert foi_request.summary == first_message.summary 297 | 298 | @pytest.mark.django_db() 299 | def test_moderation_message_returns_first_messages_moderation_message( 300 | self, foi_request 301 | ): 302 | first_message = Message(foi_request=foi_request, moderation_message="first") 303 | last_message = Message(foi_request=foi_request, moderation_message="last") 304 | 305 | save_message(first_message) 306 | save_message(last_message) 307 | 308 | foi_request.refresh_from_db() 309 | assert foi_request.moderation_message == first_message.moderation_message 310 | 311 | def test_moderation_message_is_none_if_there_are_no_messages(self, foi_request): 312 | assert foi_request.moderation_message is None 313 | 314 | def test_summary_returns_none_if_there_are_no_messages(self): 315 | assert FOIRequest().summary is None 316 | 317 | def test_get_absolute_url(self, foi_request): 318 | expected_url = reverse("foirequest_detail", args=[foi_request.protocol]) 319 | 320 | assert foi_request.get_absolute_url() == expected_url 321 | 322 | 323 | class TestPublicBody: 324 | @pytest.mark.parametrize( 325 | "uf,municipality,level", 326 | ( 327 | ("AC", "Rio Branco", "Local"), 328 | ("AC", "", "State"), 329 | ("", "", "Federal"), 330 | ), 331 | ) 332 | def test_clean_valid_level(self, uf, municipality, level, public_body): 333 | public_body.uf = uf 334 | public_body.municipality = municipality 335 | public_body.level = level 336 | 337 | public_body.clean() 338 | 339 | @pytest.mark.parametrize( 340 | "uf,municipality,level", 341 | ( 342 | ("AC", "", "Local"), 343 | ("", "Rio Branco", "Local"), 344 | ("", "", "State"), 345 | ("AC", "", "Federal"), 346 | ("AC", "Rio Branco", "Federal"), 347 | ), 348 | ) 349 | def test_clean_invalid_level(self, uf, municipality, level, public_body): 350 | public_body.uf = uf 351 | public_body.municipality = municipality 352 | public_body.level = level 353 | 354 | with pytest.raises(ValidationError): 355 | public_body.clean() 356 | -------------------------------------------------------------------------------- /web/foi_requests/tests/test_utils.py: -------------------------------------------------------------------------------- 1 | from .. import utils 2 | 3 | 4 | class TestGenerateProtocol(object): 5 | def test_returns_random_protocol(self): 6 | assert utils.generate_protocol() != utils.generate_protocol() 7 | 8 | def test_defaults_to_8_characters(self): 9 | assert len(utils.generate_protocol()) == 8 10 | 11 | def test_accept_different_length_as_parameter(self): 12 | assert len(utils.generate_protocol(15)) == 15 13 | -------------------------------------------------------------------------------- /web/foi_requests/tests/test_views.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from django.urls import reverse 3 | 4 | from ..models import PublicBody 5 | from ..views import FOIRequestRedirectView, CreatePublicBodyView, CreateFOIRequestView 6 | from .conftest import save_public_body 7 | 8 | 9 | class TestFOIRequestRedirectView(object): 10 | def test_redirects_to_foirequest_detail(self, rf): 11 | protocol = "ABC" 12 | expected_url = reverse("foirequest_detail", kwargs={"slug": protocol}) 13 | 14 | request = rf.get("?protocol=" + protocol) 15 | response = FOIRequestRedirectView.as_view()(request) 16 | 17 | assert response.url == expected_url 18 | 19 | 20 | class TestCreatePublicBodyView(object): 21 | @pytest.mark.django_db() 22 | def test_form_valid_adds_created_esic_to_public_body(self, rf): 23 | # FIXME: be more intelligent on how to set the URL and params 24 | name = "public body name" 25 | url = "http://example.com" 26 | request = rf.post( 27 | "/p/public_body/new/", 28 | { 29 | "url": url, 30 | "name": name, 31 | "level": "Federal", 32 | }, 33 | ) 34 | 35 | CreatePublicBodyView.as_view()(request) 36 | 37 | public_body = PublicBody.objects.filter(name=name).first() 38 | assert public_body is not None 39 | assert public_body.esic.url == url 40 | 41 | 42 | class TestCreateFOIRequestView: 43 | URL = reverse("foi_request_new") 44 | 45 | def test_context_data_contains_message_and_foi_request_forms(self): 46 | context = CreateFOIRequestView().get_context_data() 47 | 48 | assert "message_form" in context 49 | assert "foi_request_form" in context 50 | 51 | def test_get_passes_receiver_parameter_to_message_form(self, rf): 52 | params = {"receiver": "51"} 53 | request = rf.get(self.URL, params) 54 | 55 | response = CreateFOIRequestView.as_view()(request) 56 | 57 | message_form = response.context_data["message_form"] 58 | assert message_form.initial.get("receiver") == params["receiver"] 59 | 60 | @pytest.mark.django_db() 61 | def test_post_validates_message_form(self, client): 62 | # foi_request_form is always valid, so we don't need to test it 63 | response = client.post(self.URL) 64 | 65 | message_form = response.context[-1]["message_form"] 66 | assert not message_form.is_valid() 67 | 68 | @pytest.mark.django_db() 69 | def test_post_creates_foi_request_and_message_and_redirects_to_foi_request_page( 70 | self, public_body, client 71 | ): 72 | save_public_body(public_body) 73 | 74 | context = { 75 | "receiver": public_body.pk, 76 | "summary": "summary", 77 | "body": "body" * 100, 78 | "can_publish": True, 79 | } 80 | 81 | response = client.post(self.URL, context, follow=True) 82 | 83 | foi_request = response.context[-1]["object"] 84 | message = foi_request.first_message 85 | assert foi_request.can_publish is context["can_publish"] 86 | assert message.receiver_id == context["receiver"] 87 | assert message.summary == context["summary"] 88 | assert message.body == context["body"] 89 | -------------------------------------------------------------------------------- /web/foi_requests/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path, re_path 2 | from django.views.decorators.cache import cache_page 3 | 4 | from . import views 5 | 6 | 7 | ONE_DAY = 60 * 60 * 24 8 | 9 | urlpatterns = [ 10 | path("", views.FOIRequestRedirectView.as_view(), name="foirequest_search"), 11 | path( 12 | "new/", 13 | cache_page(ONE_DAY)(views.CreateFOIRequestView.as_view()), 14 | name="foi_request_new", 15 | ), 16 | path( 17 | "public_body/new/", 18 | cache_page(ONE_DAY)(views.CreatePublicBodyView.as_view()), 19 | name="publicbody_new", 20 | ), 21 | re_path( 22 | r"^(?P[\w\d]+)/$", 23 | views.FOIRequestView.as_view(), 24 | name="foirequest_detail", 25 | ), 26 | ] 27 | -------------------------------------------------------------------------------- /web/foi_requests/utils.py: -------------------------------------------------------------------------------- 1 | import random 2 | import string 3 | 4 | 5 | def generate_protocol(length=8): 6 | alphanumeric = string.ascii_uppercase + string.digits 7 | return "".join(random.choices(alphanumeric, k=length)) 8 | -------------------------------------------------------------------------------- /web/foi_requests/views.py: -------------------------------------------------------------------------------- 1 | from django.views.generic import TemplateView 2 | from django.views.generic.edit import CreateView 3 | from django.views.generic.detail import DetailView 4 | from django.views.generic.base import RedirectView 5 | from django.utils.decorators import method_decorator 6 | from django.views.decorators.cache import never_cache 7 | from django.shortcuts import render, redirect 8 | from django.urls import reverse 9 | from django.db import transaction 10 | 11 | from .forms import MessageForm, EsicForm, PublicBodyForm, FOIRequestForm 12 | from .models import FOIRequest, PublicBody 13 | 14 | 15 | class CreateMessageView(CreateView): 16 | form_class = MessageForm 17 | template_name = "foi_requests/foi_request_new.html" 18 | 19 | def get_initial(self): 20 | return { 21 | "receiver": self.request.GET.get("receiver"), 22 | } 23 | 24 | 25 | # Desabilita cache para não cachearmos o CSRF token 26 | @method_decorator(never_cache, name="dispatch") 27 | class CreateFOIRequestView(TemplateView): 28 | template_name = "foi_requests/foi_request_new.html" 29 | 30 | def get_context_data(self, **kwargs): 31 | context = super(CreateFOIRequestView, self).get_context_data(**kwargs) 32 | 33 | message_form_initial = {} 34 | 35 | if hasattr(self, "request"): 36 | message_form_initial["receiver"] = self.request.GET.get("receiver") 37 | 38 | forms = { 39 | "message_form": kwargs.get( 40 | "message_form", MessageForm(initial=message_form_initial) 41 | ), 42 | "foi_request_form": kwargs.get("foi_request_form", FOIRequestForm()), 43 | } 44 | 45 | return {**context, **forms} 46 | 47 | def post(self, request): 48 | message_form = MessageForm(request.POST) 49 | foi_request_form = FOIRequestForm(request.POST) 50 | context = { 51 | "message_form": message_form, 52 | "foi_request_form": foi_request_form, 53 | } 54 | 55 | if all([message_form.is_valid(), foi_request_form.is_valid()]): 56 | with transaction.atomic(): 57 | foi_request = foi_request_form.save() 58 | message_form.instance.foi_request = foi_request 59 | message_form.save() 60 | return redirect(foi_request) 61 | 62 | return render(request, self.template_name, context) 63 | 64 | 65 | # Desabilita cache para não cachearmos o CSRF token 66 | @method_decorator(never_cache, name="dispatch") 67 | class CreatePublicBodyView(CreateView): 68 | form_class = PublicBodyForm 69 | model = PublicBody 70 | 71 | def get_context_data(self, **kwargs): 72 | data = super(CreatePublicBodyView, self).get_context_data(**kwargs) 73 | if self.request.POST: 74 | data["esic_form"] = EsicForm(self.request.POST) 75 | else: 76 | data["esic_form"] = EsicForm() 77 | return data 78 | 79 | def form_valid(self, form): 80 | context = self.get_context_data() 81 | esic_form = context["esic_form"] 82 | 83 | if esic_form.is_valid(): 84 | # TODO: Add transactionwith transaction.commit_on_success(): 85 | esic_form.save() 86 | form.instance.esic = esic_form.instance 87 | self.object = form.save() 88 | 89 | return super(CreatePublicBodyView, self).form_valid(form) 90 | 91 | def get_success_url(self): 92 | return "{url}?receiver={receiver}".format( 93 | url=reverse("foi_request_new"), receiver=self.object.id 94 | ) 95 | 96 | 97 | class FOIRequestView(DetailView): 98 | model = FOIRequest 99 | slug_field = "protocol" 100 | 101 | 102 | class FOIRequestRedirectView(RedirectView): 103 | pattern_name = "foirequest_search" 104 | permanent = True 105 | 106 | def get_redirect_url(self, *args, **kwargs): 107 | protocol = self.request.GET.get("protocol") 108 | return reverse("foirequest_detail", kwargs={"slug": protocol}) 109 | -------------------------------------------------------------------------------- /web/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for web project. 3 | 4 | Generated by 'django-admin startproject' using Django 2.0.2. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.0/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/2.0/ref/settings/ 11 | """ 12 | 13 | import os 14 | import environ 15 | import warnings 16 | from django.utils.translation import gettext_lazy as _ 17 | from pathlib import Path 18 | import sentry_sdk 19 | 20 | 21 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 22 | # Preciso converter pra str por causa de um erro no livesync. Talvez novas 23 | # versões aceitem Path. 24 | BASE_DIR = str(Path(__file__).resolve().parent.parent) 25 | 26 | DEFAULT_ENV_PATH = os.path.join(BASE_DIR, ".env") 27 | env = environ.Env( 28 | DEBUG=(bool, False), 29 | ENV_PATH=(str, DEFAULT_ENV_PATH), 30 | ENABLE_S3=(bool, False), 31 | HEROKU_APP_ID=(str, None), 32 | SESSION_COOKIE_SECURE=(bool, False), 33 | CSRF_COOKIE_SECURE=(bool, False), 34 | CSRF_COOKIE_DOMAIN=(str, None), 35 | CSRF_TRUSTED_ORIGINS=( 36 | list, 37 | ["https://queremossaber.org.br"], 38 | ), 39 | ENV=(str, "dev"), 40 | ) 41 | env.read_env(env.str("ENV_PATH")) 42 | 43 | # Quick-start development settings - unsuitable for production 44 | # See https://docs.djangoproject.com/en/2.0/howto/deployment/checklist/ 45 | 46 | # SECURITY WARNING: keep the secret key used in production secret! 47 | SECRET_KEY = env("SECRET_KEY") 48 | 49 | # SECURITY WARNING: don't run with debug turned on in production! 50 | DEBUG = env("DEBUG") 51 | 52 | ENV = env("ENV") 53 | IN_DEV = ENV == "dev" 54 | 55 | allowed_hosts_dev = [] 56 | if IN_DEV: 57 | allowed_hosts_dev = ["localhost", "127.0.0.1"] 58 | 59 | ALLOWED_HOSTS = [ 60 | "queremossaber.org.br", 61 | ] + allowed_hosts_dev 62 | 63 | INTERNAL_IPS = [ 64 | "127.0.0.1", 65 | ] 66 | 67 | SESSION_COOKIE_SECURE = env("SESSION_COOKIE_SECURE") 68 | CSRF_COOKIE_SECURE = env("CSRF_COOKIE_SECURE") 69 | CSRF_COOKIE_DOMAIN = env("CSRF_COOKIE_DOMAIN") 70 | CSRF_TRUSTED_ORIGINS = env("CSRF_TRUSTED_ORIGINS") 71 | 72 | # Application definition 73 | 74 | extra_apps = [] 75 | if IN_DEV: 76 | extra_apps += ["livesync", "debug_toolbar"] 77 | 78 | INSTALLED_APPS = [ 79 | "web.foi_requests", 80 | "web.whoami", 81 | "whitenoise.runserver_nostatic", 82 | "django.contrib.admin", 83 | "django.contrib.auth", 84 | "django.contrib.contenttypes", 85 | "django.contrib.sessions", 86 | "django.contrib.messages", 87 | "django.contrib.staticfiles", 88 | "widget_tweaks", 89 | ] + extra_apps 90 | 91 | extra_middleware = [] 92 | if IN_DEV: 93 | extra_middleware += [ 94 | "debug_toolbar.middleware.DebugToolbarMiddleware", 95 | "livesync.core.middleware.DjangoLiveSyncMiddleware", 96 | ] 97 | 98 | MIDDLEWARE = [ 99 | "django.middleware.security.SecurityMiddleware", 100 | "whitenoise.middleware.WhiteNoiseMiddleware", 101 | "django.contrib.sessions.middleware.SessionMiddleware", 102 | "django.middleware.common.CommonMiddleware", 103 | "django.middleware.csrf.CsrfViewMiddleware", 104 | "django.contrib.auth.middleware.AuthenticationMiddleware", 105 | "django.contrib.messages.middleware.MessageMiddleware", 106 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 107 | ] + extra_middleware 108 | 109 | ROOT_URLCONF = "web.urls" 110 | 111 | TEMPLATES = [ 112 | { 113 | "BACKEND": "django.template.backends.django.DjangoTemplates", 114 | "DIRS": [os.path.join(BASE_DIR, "web", "templates")], 115 | "APP_DIRS": True, 116 | "OPTIONS": { 117 | "context_processors": [ 118 | "django.template.context_processors.debug", 119 | "django.template.context_processors.request", 120 | "django.contrib.auth.context_processors.auth", 121 | "django.contrib.messages.context_processors.messages", 122 | ], 123 | }, 124 | }, 125 | ] 126 | 127 | WSGI_APPLICATION = "web.wsgi.application" 128 | 129 | 130 | # Database 131 | # https://docs.djangoproject.com/en/2.0/ref/settings/#databases 132 | 133 | MAX_CONN_AGE = 600 134 | 135 | DATABASES = { 136 | "default": env.db(), 137 | } 138 | 139 | DEFAULT_AUTO_FIELD = "django.db.models.AutoField" 140 | 141 | # Password validation 142 | # https://docs.djangoproject.com/en/2.0/ref/settings/#auth-password-validators 143 | 144 | AUTH_PASSWORD_VALIDATORS = [ 145 | { 146 | "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", 147 | }, 148 | { 149 | "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", 150 | }, 151 | { 152 | "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", 153 | }, 154 | { 155 | "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", 156 | }, 157 | ] 158 | 159 | 160 | # Internationalization 161 | # https://docs.djangoproject.com/en/2.0/topics/i18n/ 162 | 163 | LANGUAGE_CODE = "pt-br" 164 | 165 | LANGUAGES = [ 166 | ("pt-br", _("Brazilian Portuguese")), 167 | ] 168 | 169 | LOCALE_PATHS = [os.path.join(BASE_DIR, "locale")] 170 | 171 | TIME_ZONE = "Brazil/East" 172 | 173 | USE_I18N = True 174 | 175 | USE_TZ = True 176 | 177 | 178 | # Static files (CSS, JavaScript, Images) 179 | # https://docs.djangoproject.com/en/2.0/howto/static-files/ 180 | 181 | STATIC_ROOT = os.path.join(BASE_DIR, "staticfiles") 182 | STATIC_URL = "/static/" 183 | STATICFILES_DIRS = [ 184 | os.path.join(BASE_DIR, "web", "static"), 185 | ] 186 | 187 | STORAGES = { 188 | "default": { 189 | "BACKEND": "django.core.files.storage.FileSystemStorage", 190 | }, 191 | "staticfiles": { 192 | "BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage", 193 | }, 194 | } 195 | 196 | # Uploaded files variables. Will only be used if ENABLE_S3 is False. 197 | MEDIA_URL = "/upload/" 198 | MEDIA_ROOT = os.path.join(BASE_DIR, "media") 199 | 200 | 201 | ENABLE_S3 = env("ENABLE_S3") 202 | if ENABLE_S3: 203 | STORAGES["default"] = { 204 | "BACKEND": "storages.backends.s3.S3Storage", 205 | "OPTIONS": { 206 | "access_key": env("AWS_ACCESS_KEY_ID"), 207 | "secret_key": env("AWS_SECRET_ACCESS_KEY"), 208 | "region_name": env("AWS_S3_REGION_NAME"), 209 | "endpoint_url": env("AWS_S3_ENDPOINT_URL"), 210 | "bucket_name": env("AWS_STORAGE_BUCKET_NAME"), 211 | "location": env("AWS_LOCATION"), 212 | "default_acl": "public-read", 213 | "file_overwrite": False, 214 | "querystring_auth": False, 215 | "object_parameters": { 216 | "CacheControl": "public, max-age=31556926", 217 | }, 218 | }, 219 | } 220 | 221 | DJANGO_LIVESYNC = { 222 | "HOST": "localhost", 223 | "PORT": 9001, 224 | } 225 | 226 | LOGGING = { 227 | "version": 1, 228 | "disable_existing_loggers": False, 229 | "handlers": { 230 | "console": { 231 | "class": "logging.StreamHandler", 232 | }, 233 | }, 234 | "loggers": { 235 | "django": { 236 | "handlers": ["console"], 237 | "propagate": True, 238 | "level": "DEBUG", 239 | }, 240 | "whitenoise": { 241 | "handlers": ["console"], 242 | "level": "DEBUG", 243 | }, 244 | "django.security.csrf": { 245 | "handlers": ["console"], 246 | "level": "DEBUG", 247 | }, 248 | }, 249 | } 250 | 251 | ## Warnings 252 | 253 | # This is deprecated and will be removed in Django 6. By then, we just need to 254 | # remove this. 255 | warnings.filterwarnings( 256 | "ignore", "The FORMS_URLFIELD_ASSUME_HTTPS transitional setting is deprecated." 257 | ) 258 | FORMS_URLFIELD_ASSUME_HTTPS = True 259 | 260 | if not IN_DEV: 261 | sentry_sdk.init( 262 | dsn="https://fba9f8fa2d6ca1301b2eef8056066ecf@o1430401.ingest.us.sentry.io/4508636591816704", 263 | # Set traces_sample_rate to 1.0 to capture 100% 264 | # of transactions for tracing. 265 | traces_sample_rate=1.0, 266 | # Set profiles_sample_rate to 1.0 to profile 100% 267 | # of sampled transactions. 268 | # We recommend adjusting this value in production. 269 | profiles_sample_rate=1.0, 270 | ) 271 | -------------------------------------------------------------------------------- /web/settings_test.py: -------------------------------------------------------------------------------- 1 | from .settings import * # noqa: F403 2 | 3 | STORAGES = { 4 | "default": { 5 | "BACKEND": "django.core.files.storage.FileSystemStorage", 6 | }, 7 | "staticfiles": { 8 | "BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage", 9 | }, 10 | } 11 | -------------------------------------------------------------------------------- /web/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FiquemSabendo/queremossaber/81f02d0ba8cc07ee0d2176236e99653b8dac3775/web/static/favicon.ico -------------------------------------------------------------------------------- /web/static/web/images/_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FiquemSabendo/queremossaber/81f02d0ba8cc07ee0d2176236e99653b8dac3775/web/static/web/images/_logo.png -------------------------------------------------------------------------------- /web/static/web/styles/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FiquemSabendo/queremossaber/81f02d0ba8cc07ee0d2176236e99653b8dac3775/web/static/web/styles/.keep -------------------------------------------------------------------------------- /web/static/web/styles/main.scss: -------------------------------------------------------------------------------- 1 | $cyan: #48dbfb; 2 | $yellow: #feca57; 3 | $green: #6bb844; 4 | $blue: #2e86de; 5 | $red: #ff6b6b; 6 | $pale: #c8d6e5; 7 | $white: #fff; 8 | $black: #434343; 9 | $pink: #f368e0; 10 | $accent: $green; 11 | $dark-grey: #2d2d2d; 12 | $black-15: rgba($black, .15); 13 | $link: $green; 14 | $link-visited: $green; 15 | 16 | *, 17 | *::before, 18 | *::after { 19 | box-sizing: border-box; 20 | } 21 | 22 | body { 23 | font-family: 'Roboto', sans-serif; 24 | line-height: 1.5; 25 | font-weight: 400; 26 | margin: 0; 27 | display: flex; 28 | flex-direction: column; 29 | min-height: 100vh; 30 | } 31 | 32 | a, 33 | a:link { 34 | color: $link; 35 | text-decoration: none; 36 | 37 | &:hover { 38 | text-decoration: underline; 39 | } 40 | } 41 | 42 | a:visited { 43 | color: $link-visited; 44 | } 45 | 46 | .wrapper { 47 | margin: 0 auto; 48 | padding-left: 2rem; 49 | padding-right: 2rem; 50 | 51 | @media (min-width: 48rem) { 52 | max-width: 44rem; 53 | } 54 | 55 | @media (min-width: 64rem) { 56 | max-width: 60rem; 57 | } 58 | 59 | @media (min-width: 80rem) { 60 | max-width: 76rem; 61 | } 62 | } 63 | 64 | header, 65 | main, 66 | footer { 67 | padding: 1rem 0; 68 | 69 | @media (min-width: 48rem) { 70 | padding: 2rem 0; 71 | } 72 | } 73 | 74 | header { 75 | text-align: center; 76 | } 77 | 78 | footer { 79 | background-color: $dark-grey; 80 | color: $white; 81 | text-align: left; 82 | 83 | p { 84 | color: $white; 85 | } 86 | 87 | .fs-logo svg { 88 | width: 170px; 89 | } 90 | } 91 | 92 | header { 93 | @media (min-width: 48rem) { 94 | margin-top: 4rem; 95 | } 96 | 97 | p { 98 | margin: 0; 99 | } 100 | 101 | .logo-wrapper { 102 | margin-top: 2rem; 103 | margin-bottom: 2rem; 104 | 105 | @media (min-width: 48rem) { 106 | margin: 0; 107 | } 108 | } 109 | 110 | .logo, 111 | .logo:link, 112 | .logo:visited { 113 | font-size: 1.5rem; 114 | font-weight: 900; 115 | text-decoration: none; 116 | color: $black; 117 | 118 | @media (min-width: 48rem) { 119 | font-size: 3.5rem; 120 | } 121 | 122 | svg { 123 | fill: $dark-grey; 124 | } 125 | } 126 | 127 | .request { 128 | @media (min-width: 48rem) { 129 | display: flex; 130 | align-items: center; 131 | justify-content: center; 132 | margin: 2rem 0; 133 | } 134 | 135 | form { 136 | position: relative; 137 | margin-bottom: 2rem; 138 | 139 | @media (min-width: 48rem) { 140 | padding: 0; 141 | margin-bottom: 0; 142 | } 143 | 144 | input { 145 | background: transparent; 146 | padding-right: 6rem; 147 | width: 100%; 148 | } 149 | 150 | button { 151 | position: absolute; 152 | padding-top: .625rem; 153 | padding-bottom: .625rem; 154 | font-size: .875rem; 155 | top: 0; 156 | right: 0; 157 | } 158 | } 159 | 160 | > .button { 161 | @media (min-width: 48rem) { 162 | margin-left: 1rem; 163 | } 164 | } 165 | } 166 | } 167 | 168 | main { 169 | svg { 170 | display: block; 171 | max-width: 100%; 172 | height: 12rem; 173 | margin: 3rem auto; 174 | 175 | text { 176 | font-size: 1.2rem; 177 | 178 | @media (max-width: 48rem) { 179 | font-size: 1.375rem; 180 | font-weight: 400; 181 | } 182 | } 183 | } 184 | 185 | h2 { 186 | font-size: 1.5rem; 187 | text-transform:uppercase; 188 | margin: 5rem 0 .5rem; 189 | 190 | @media (min-width: 48rem) { 191 | font-size: 2.5rem; 192 | } 193 | } 194 | 195 | p { 196 | font-size: 1rem; 197 | 198 | @media (min-width: 48rem) { 199 | font-size: 1.375rem; 200 | } 201 | 202 | small { 203 | font-size: .75rem; 204 | 205 | @media (min-width: 48rem) { 206 | font-size: .875rem; 207 | } 208 | } 209 | } 210 | 211 | li { 212 | font-size: 1rem; 213 | 214 | @media (min-width: 48rem) { 215 | font-size: 1.375rem; 216 | } 217 | } 218 | } 219 | 220 | .faq { 221 | dt, 222 | dd, 223 | p { 224 | color: $black; 225 | font-size: 1rem; 226 | 227 | @media (min-width: 48rem) { 228 | font-size: 1.375rem; 229 | } 230 | } 231 | 232 | dt { 233 | color: $black; 234 | text-transform: unset; 235 | margin-bottom: 0.5rem; 236 | } 237 | 238 | dd { 239 | margin-bottom: 2rem; 240 | } 241 | } 242 | label { 243 | display: block; 244 | font-size: .75rem; 245 | text-transform: uppercase; 246 | font-weight: bold; 247 | } 248 | 249 | select { 250 | font-family: inherit; 251 | font-size: 1rem; 252 | width: 100%; 253 | border-radius: 0; 254 | margin-bottom: .5rem; 255 | background-color: rgba($pale, .2); 256 | border: 0; 257 | border-bottom: 1px solid $black; 258 | padding: .625rem; 259 | 260 | &:focus { 261 | background-color: $white; 262 | outline: none; 263 | border-bottom-color: $green; 264 | box-shadow: $green 0 1px 0; 265 | } 266 | } 267 | 268 | textarea, 269 | input[type="text"], 270 | input[type="email"], 271 | input[type="url"], 272 | input[type="search"] { 273 | background-color: rgba($pale, .2); 274 | font-family: inherit; 275 | font-size: 1rem; 276 | width: 100%; 277 | padding: .625rem 0; 278 | border: 0; 279 | border-bottom: 1px solid $black; 280 | margin: 0; 281 | 282 | &:focus { 283 | background-color: $white; 284 | border-bottom-color: $green; 285 | box-shadow: $green 0 1px 0; 286 | outline: none; 287 | } 288 | } 289 | 290 | textarea { 291 | resize: none; 292 | } 293 | 294 | .form-field { 295 | padding: 2rem 0; 296 | } 297 | 298 | .form-field-checkbox { 299 | input[type="checkbox"] { 300 | vertical-align: middle; 301 | } 302 | 303 | label { 304 | font-size: 1rem; 305 | display: unset; 306 | text-transform: unset; 307 | font-weight: unset; 308 | } 309 | } 310 | 311 | .form-submit { 312 | padding: 1rem 0; 313 | font-size: .875rem; 314 | 315 | .link-dimmed, 316 | .link-dimmed:link, 317 | .link-dimmed:visited { 318 | color: $black; 319 | } 320 | } 321 | 322 | button, 323 | .button, 324 | .button:visited, 325 | .button:link { 326 | background-color: $link; 327 | border-radius: .125rem; 328 | border: 0; 329 | box-shadow: $black-15 0 .125rem 0; 330 | color: $white; 331 | font-family: inherit; 332 | font-size: 1rem; 333 | line-height: 1; 334 | padding: .75rem 2rem; 335 | text-decoration: none; 336 | white-space: nowrap; 337 | 338 | &:hover { 339 | opacity: .75; 340 | } 341 | 342 | &:active { 343 | box-shadow: none; 344 | outline: none; 345 | opacity: 1; 346 | } 347 | 348 | &.button-tiny { 349 | font-size: .75rem; 350 | padding: .25rem 1rem; 351 | } 352 | } 353 | 354 | .steps { 355 | @media (min-width: 48rem) { 356 | display: flex; 357 | align-items: flex-start; 358 | justify-content: space-between; 359 | } 360 | 361 | li { 362 | counter-increment: step; 363 | flex: 1; 364 | text-align: center; 365 | padding: .5rem; 366 | 367 | @media (min-width: 48rem) { 368 | padding: 1rem; 369 | } 370 | 371 | &::before { 372 | content: counter(step); 373 | font-size: 1.5rem; 374 | color: $white; 375 | width: 2em; 376 | line-height: 2em; 377 | margin: 0 auto; 378 | height: 2em; 379 | border-radius: 100%; 380 | display: block; 381 | background-color: $accent; 382 | 383 | @media (min-width: 48rem) { 384 | font-size: 2.5rem; 385 | } 386 | } 387 | 388 | p { 389 | font-size: 1.25rem; 390 | 391 | @media (min-width: 48rem) { 392 | font-size: 1.5rem; 393 | } 394 | } 395 | } 396 | } 397 | 398 | .info, 399 | .warning { 400 | font-size: 1rem; 401 | padding: 1rem; 402 | border-radius: .25rem; 403 | } 404 | 405 | .info { 406 | background-color: $green; 407 | color: $white; 408 | 409 | h2, 410 | p { 411 | margin: 0; 412 | padding: 0; 413 | } 414 | 415 | h2 { 416 | font-size: 1.25rem; 417 | font-weight: 900; 418 | margin-bottom: .5rem; 419 | } 420 | 421 | p { 422 | font-size: 1rem; 423 | } 424 | 425 | margin-bottom: 1rem; 426 | } 427 | 428 | .warning { 429 | background-color: $yellow; 430 | color: $black; 431 | } 432 | 433 | dl, 434 | dt, 435 | dd { 436 | list-style: none; 437 | padding: 0; 438 | margin: 0; 439 | } 440 | 441 | dt { 442 | font-weight: 900; 443 | text-transform: uppercase; 444 | font-size: .625rem; 445 | letter-spacing: .4px; 446 | color: rgba($black, .5); 447 | } 448 | 449 | .request-details { 450 | dd { 451 | margin-bottom: 1rem; 452 | } 453 | 454 | h2 { 455 | margin: 0; 456 | padding: 0; 457 | text-align: left; 458 | } 459 | 460 | .moderation-message { 461 | white-space: pre-line; 462 | } 463 | } 464 | 465 | .message-details { 466 | border: 1px solid $pale; 467 | padding: 1rem; 468 | margin-bottom: 2rem; 469 | 470 | .message-meta { 471 | border-bottom: 1px solid $pale; 472 | } 473 | .message-body { 474 | white-space: pre-line; 475 | } 476 | } 477 | 478 | .message-publicbody { 479 | background-color: rgba($yellow, .2); 480 | } 481 | 482 | .request-status { 483 | padding: .5rem 1rem; 484 | font-size: .625rem; 485 | text-transform: uppercase; 486 | font-weight: 900; 487 | border-radius: .25rem; 488 | display: inline-block; 489 | 490 | background-color: $pale; 491 | color: $black; 492 | 493 | &-rejected { 494 | background-color: $red; 495 | color: $white; 496 | } 497 | } 498 | -------------------------------------------------------------------------------- /web/templates/_fluxo_pedidos.svg: -------------------------------------------------------------------------------- 1 | 2 | image/svg+xmlcadastramos a 9 | resposta no seu 10 | protocolo 11 | você cadastra 12 | seu pedido 13 | fazemosseu pedido 14 | o órgão 15 | responde 16 | 17 | -------------------------------------------------------------------------------- /web/templates/_logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /web/templates/_logo_fs.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /web/templates/_search_request.html: -------------------------------------------------------------------------------- 1 |
4 | 9 | 12 |
13 | -------------------------------------------------------------------------------- /web/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | {% load static %} 3 | 4 | 5 | 6 | Queremos Saber 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 |
24 |
25 | 28 |

29 | Faça pedidos de acesso à informação sem revelar sua identidade. 30 |

31 |
32 | 33 |
34 | {% include '_search_request.html' %} 35 | 36 | Fazer Pedido 37 | 38 |
39 |
40 |
41 |
42 |
43 | {% block content %}{% endblock %} 44 |
45 |
46 | 69 | {% block scripts %}{% endblock %} 70 | 71 | 72 | -------------------------------------------------------------------------------- /web/templates/faq.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 |
5 |

Perguntas frequentes

6 | 7 |
8 |
O que é a Lei de Acesso à Informação (LAI)?
9 |
10 | A Lei de Acesso à Informação (Lei 12.527/2011), também conhecida como LAI, regulamenta o direito constitucional do cidadão de acessar qualquer informação pública sem que seja necessário apresentar o motivo. Ela obriga órgãos públicos a considerar a publicidade como regra e o sigilo como exceção e vale para os três poderes da União, Estados, Distrito Federal e Municípios. 11 |
12 | 13 |
Por que este projeto foi criado?
14 |
15 |

16 | A Lei de Acesso à Informação (LAI) representou um grande avanço na transparência pública. Infelizmente, ela tem um grave problema: exigir a identificação do solicitante. 17 |

18 |

19 | Em um mundo ideal, essa exigência não seria problemática. Infelizmente, esse não é o caso no Brasil. Há diversos relatos de ameaças, sutis ou agressivas, demissões e outras represálias feitas por causa de um pedido de informação. Você pode encontrar alguns desses casos no relatório Identidade Revelada, lançado em 2018 pela ONG Artigo 19. 20 |

21 |

22 | Acreditamos que a única forma de garantir que todos sejam tratados da mesma forma é não revelar a identidade de ninguém. Por isso criamos este projeto. 23 |

24 |
25 | 26 |
Para quem este projeto foi criado?
27 |
28 | Nossa plataforma é direcionada a pessoas que se sintam em risco ao realizar pedidos de informação a um determinado órgão público. Independente do motivo -- seja por sua sua profissão, pelo teor do pedido ou por divergências políticas --, se você se sente constrangido(a) pela obrigatoriedade de se identificar ao buscar uma informação pública, pode recorrer ao Queremos Saber para fazê-lo em segurança. 29 |
30 | 31 |
Eu preciso pagar para enviar pedidos?
32 |
33 | Não. Assim como o acesso à informação no Brasil, o uso desta plataforma é totalmente gratuito. 34 |
35 | 36 |
Posso enviar pedidos para qualquer órgão?
37 |
38 | Quase. Você pode enviar pedidos para qualquer órgão que aceite pedidos pela Internet e aceite pedidos vindos de empresas. 39 |
40 | 41 |
Como entro com um recurso?
42 |
43 | Infelizmente, ainda não é possível entrar com recursos pelo Queremos Saber. Esta é a funcionalidade mais importante que ainda não foi implementada. Sabemos da sua importância e estamos trabalhando para disponibilizá-la o quanto antes. Até lá, caso você receba uma resposta não satisfatória, recomendamos abrir outro pedido. 44 |
45 | 46 |
Em quanto tempo terei minha resposta?
47 |
48 | A LAI garante que, caso disponíveis, as informações são concedidas imediatamente ao solicitante. Do contrário, como em pedidos online, o órgão tem até 20 dias para enviá-las, com direito a mais 10 dias de prorrogação, se necessário e mediante justificativa do órgão. É possível ver mais regras de prazos nesse link. 49 |
50 | 51 |
O prazo de resposta da LAI começa a contar quando eu envio um pedido?
52 |
53 | Não, só depois do pedido passar pela nossa moderação e ser cadastrado no órgão público. 54 |
55 | 56 |
Posso fazer qualquer tipo de pedido?
57 |
58 | Sim. É preciso apenas se certificar de que o pedido seja válido e não apresente conteúdo difamatório ou preconceituoso em seu texto. Apesar disso, pedimos que você só use essa plataforma para pedidos em que você não quer revelar sua identidade. Isso evitaria sobrecarregar nosso sistema com pedidos não sensíveis. 59 |
60 | 61 |
Como sei se minha identidade não será revelada?
62 |
63 | Nós não pedimos nenhum dado pessoal para cadastrar um novo pedido, nem guardamos registros de acesso ou endereços IP. Em outras palavras, nem nós mesmos sabemos quem você é. Você também pode usar o navegador Tor para garantir sua privacidade mesmo caso nossos servidores sejam invadidos. 64 |
65 | 66 |
Qual a relação deste projeto com o Queremos Saber antigo?
67 |
68 | O Queremos Saber foi uma plataforma de pedidos de informação criada pelo 69 | grupo Transparência Hacker em 2011, antes mesmo da LAI entrar em vigor. 70 | Ele permitia enviar pedidos para diversos órgãos, que ficavam públicos. 71 | Assim, todos poderiam não só ver as respostas, mas acompanhar que órgãos 72 | estavam cumprindo os prazos legais. 73 | 74 | Infelizmente, quando os órgãos criaram seus próprios portais de 75 | transparência, passaram a exigir que os pedidos fossem cadastrados nos 76 | seus sistemas. Com isso, os pedidos feitos pelo Queremos Saber eram 77 | recusados e a plataforma entrou em desuso. 78 | 79 | Usar o mesmo nome para esta plataforma é uma pequena homenagem a primeira 80 | plataforma brasileira de pedidos de acesso à informação. 81 |
82 | 83 |
O código-fonte está disponível?
84 |
85 | Sim. Você pode acessá-lo no GitHub. 86 |
87 | 88 |
89 |
90 | {% endblock %} 91 | -------------------------------------------------------------------------------- /web/templates/foi_requests/_load_selectize.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | -------------------------------------------------------------------------------- /web/templates/foi_requests/_message_details.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | De 5 |
6 |
7 | {{ message.sender | default:"Solicitante" }} 8 |
9 |
10 | Enviado em 11 |
12 |
13 | {{ message.sent_at | default:"Não enviado" }} 14 |
15 | {% if message.attached_file %} 16 |
17 | Arquivos anexos 18 |
19 |
20 | {{ message.attached_file.name }} 21 |
22 | {% endif %} 23 |
24 | 25 |
26 | {{ message.body }} 27 |
28 |
29 | -------------------------------------------------------------------------------- /web/templates/foi_requests/foi_request_new.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block scripts %} 4 | {{ block.super }} 5 | {% include "foi_requests/_load_selectize.html" %} 6 | {% endblock %} 7 | 8 | {% block content %} 9 | {% load widget_tweaks %} 10 |
11 |

12 | Novo pedido 13 |

14 | 15 | 16 | Por favor, só use esse site para pedidos sensíveis, quando você não queira 17 | se identificar, pois cada pedido é enviado manualmente por um voluntário. 18 | 19 | 20 |
23 | {% csrf_token %} 24 | 25 |
26 | {{ message_form.receiver.errors }} 27 | 30 | {% render_field message_form.receiver class+="selectize" %} 31 | 32 | Não encontrou o órgão público que queria? Cadastre um novo órgão. 33 | 34 |
35 | 36 |
37 | {{ message_form.summary.errors }} 38 | 41 | {% render_field message_form.summary placeholder="Qual o resumo do seu pedido?" %} 42 |
43 | 44 |
45 | {{ message_form.body.errors }} 46 | 49 | {% render_field message_form.body placeholder="Descreva seu pedido de forma direta e detalhada, evitando quaisquer detalhes que possam te identificar." %} 50 |
51 | 52 |
53 | {{ foi_request_form.previous_protocol_id.errors }} 54 | 57 | {% render_field foi_request_form.previous_protocol placeholder="Protocolo de um pedido anterior relacionado a este, caso exista (e.x. ABCDEFGH)" %} 58 |
59 | 60 |
61 | {{ foi_request_form.can_publish.errors }} 62 | 66 |
67 | 68 |
69 | 72 | ou 73 | cancelar 74 |
75 |
76 |
77 | {% endblock %} 78 | -------------------------------------------------------------------------------- /web/templates/foi_requests/foirequest_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 |
5 |
6 |

7 | Pedido: {{ object.protocol }} 8 |

9 |

10 | ATENÇÃO: Anote esse número de protocolo em um lugar seguro. 11 | Ele é a única maneira de acompanhar o andamento do pedido. 12 |

13 |
14 | 15 |
16 |
17 |
Status
18 |
19 |
20 | {{ object.status.value }} 21 |
22 | {% if object.moderation_message %} 23 |
24 | Comentários da moderação 25 |
26 |
27 | {{ object.moderation_message }} 28 |
29 | {% endif %} 30 | 31 |
32 | Aberto em 33 |
34 |
35 | {{ object.created_at }} 36 |
37 |
38 | Órgão 39 |
40 |
41 | {{ object.public_body.name }} 42 |
43 | {% if object.summary %} 44 |
45 | Resumo 46 |
47 |
48 | {{ object.summary }} 49 |
50 | {% endif %} 51 |
52 |
53 | 54 |
55 |

Mensagens

56 | {% for message in object.message_set.all %} 57 | {% include "foi_requests/_message_details.html" %} 58 | {% endfor %} 59 |
60 |
61 | {% endblock %} 62 | -------------------------------------------------------------------------------- /web/templates/foi_requests/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block content %} 3 | Index page 4 | {% endblock %} 5 | -------------------------------------------------------------------------------- /web/templates/foi_requests/publicbody_form.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 | {% load widget_tweaks %} 5 | 6 |
7 |

Cadastrar órgão público

8 | 9 |
12 | {% csrf_token %} 13 | 14 |
15 | {{ form.name.errors }} 16 | 17 | 20 | {% render_field form.name placeholder="Qual o nome do órgão público?" %} 21 | 22 | Antes de cadastrar um novo órgão, por favor confira se ele já não existe aqui. 23 | 24 |
25 | 26 |
27 | {{ form.name.errors }} 28 | 29 | 32 | {% render_field form.level %} 33 |
34 | 35 |
36 | {{ form.uf.errors }} 37 | 38 | 41 | {% render_field form.uf %} 42 |
43 | 44 |
45 | {{ form.municipality.errors }} 46 | 47 | 50 | {% render_field form.municipality %} 51 |
52 | 53 |
54 | {{ esic_form.url.errors }} 55 | 58 | {% render_field esic_form.url placeholder="Qual o endereço da página onde podemos enviar pedidos? (ex.: http://exemplo.gov.br/esic)" %} 59 |
60 | 61 |
62 | 65 | ou 66 | cancelar 67 |
68 |
69 |
70 | {% endblock %} 71 | 72 | {% block scripts %} 73 | 99 | {% endblock %} 100 | -------------------------------------------------------------------------------- /web/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 |
5 |
6 |

7 | Como funciona? 8 |

9 | 10 | {% include '_fluxo_pedidos.svg' %} 11 | 12 |

13 | Nós fazemos o seu pedido em nosso nome. Dessa forma, o órgão público não saberá que o pedido é seu, garantindo sua privacidade. Funciona assim: 14 |

15 |
    16 |
  1. 17 | Você cadastra seu pedido no Queremos Saber 18 |

    19 | Não pedimos nenhum dado pessoal para cadastrar um pedido. Cada pedido 20 | tem um número de protocolo único (e.x.: HQCYR6KQ), que é a única forma 21 | de acompanhar o pedido (não perca!) 22 |

    23 |
  2. 24 | 25 |
  3. 26 | O pedido passa por moderação 27 |

    28 | Essa moderação serve para evitar que mensagens que não sejam pedidos de informação sejam criadas. 29 |

    30 |
  4. 31 | 32 |
  5. 33 | O pedido é cadastrado no órgão em nosso nome 34 |

    35 | Do ponto de vista do órgão, somos nós quem estamos fazendo o pedido. É só a partir deste momento que os prazos legais para resposta começam a contar. 36 |

    37 |
  6. 38 | 39 |
  7. 40 | O órgão responde 41 |

    42 | Cadastramos essa resposta no nosso sistema. Você poderá vê-la na próxima vez checar o pedido usando o seu protocolo. 43 |

    44 |
  8. 45 |
46 | 47 |

48 | Você pode pensar neste site como um intermediador. Nós recebemos e entregamos as mensagens enviadas entre você e o órgão público, mas sem revelar quem as enviou. 49 |

50 |

51 | Como não temos nenhuma informação pessoal sua, não temos como 52 | notificá-lo da resposta. Por isso, recomendamos que você consulte 53 | regularmente seu pedido até receber sua resposta. 54 |

55 |
56 | 57 |
58 |

59 | É seguro? 60 |

61 |

62 | Não pedimos nenhum dado pessoal nem guardamos registros de acesso. Em 63 | outras palavras, nem nós mesmos sabemos quem fez o pedido. Os principais 64 | riscos de alguém identificá-lo como solicitante seriam alguém reconhecer 65 | sua forma de escrita, você ser visto acessando este site, ou encontrarem 66 | com você o número do protocolo do pedido. Para evitar que isso aconteça, 67 | recomendamos que você: 68 |

69 |
    70 |
  • 71 | Só acesse esta página de sua casa ou de uma rede pública como a WiFi de um shopping. 72 |
  • 73 |
  • 74 | Guarde o número de protocolo do seu pedido em um lugar seguro. 75 |
  • 76 |
77 |
78 | 79 |
80 |

Onde posso saber mais sobre este projeto?

81 |

82 | Você pode ler nossa página de Perguntas Frequentes, acessar o código-fonte no GitHub, ou entrar em contato via queremossaber@fiquemsabendo.com.br. 83 |

84 |
85 |
86 | {% endblock %} 87 | -------------------------------------------------------------------------------- /web/urls.py: -------------------------------------------------------------------------------- 1 | """web URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/2.0/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | 17 | from django.views.generic import TemplateView 18 | from django.contrib import admin 19 | from django.urls import include, path, re_path 20 | from django.conf import settings 21 | from django.views.static import serve 22 | from django.views.decorators.cache import cache_page 23 | 24 | 25 | ONE_DAY = 60 * 60 * 24 26 | 27 | urlpatterns = [ 28 | path( 29 | "", 30 | cache_page(ONE_DAY)(TemplateView.as_view(template_name="index.html")), 31 | name="index", 32 | ), 33 | path( 34 | "faq/", 35 | cache_page(ONE_DAY)(TemplateView.as_view(template_name="faq.html")), 36 | name="faq", 37 | ), 38 | path("p/", include("web.foi_requests.urls")), 39 | path("whoami/", include("web.whoami.urls")), 40 | path("a/", admin.site.urls), 41 | ] 42 | 43 | if not settings.ENABLE_S3: 44 | urlpatterns = [ 45 | re_path( 46 | r"^upload/(?P.*)$", 47 | serve, 48 | { 49 | "document_root": settings.MEDIA_ROOT, 50 | "show_indexes": False, 51 | }, 52 | ) 53 | ] + urlpatterns 54 | 55 | if settings.DEBUG: 56 | import debug_toolbar.toolbar 57 | 58 | urlpatterns += debug_toolbar.toolbar.debug_toolbar_urls() 59 | -------------------------------------------------------------------------------- /web/whoami/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FiquemSabendo/queremossaber/81f02d0ba8cc07ee0d2176236e99653b8dac3775/web/whoami/__init__.py -------------------------------------------------------------------------------- /web/whoami/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class WhoamiConfig(AppConfig): 5 | name = "web.whoami" 6 | -------------------------------------------------------------------------------- /web/whoami/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FiquemSabendo/queremossaber/81f02d0ba8cc07ee0d2176236e99653b8dac3775/web/whoami/tests/__init__.py -------------------------------------------------------------------------------- /web/whoami/tests/test_view.py: -------------------------------------------------------------------------------- 1 | import json 2 | import pytest 3 | from django.urls import reverse 4 | 5 | from ..views import WhoamiView 6 | 7 | 8 | class TestWhoamiView: 9 | URL = reverse("whoami_get") 10 | 11 | @pytest.mark.parametrize( 12 | "header", 13 | ( 14 | "REMOTE_ADDR", 15 | "HTTP_X_FORWARDED_FOR", 16 | "HTTP_X_REAL_IP", 17 | "HTTP_USER_AGENT", 18 | ), 19 | ) 20 | def test_get_returns_header(self, rf, header): 21 | request = rf.get(self.URL, **{header: "foobar"}) 22 | 23 | response = WhoamiView.as_view()(request) 24 | 25 | data = json.loads(response.content) 26 | assert data.get(header) == "foobar" 27 | -------------------------------------------------------------------------------- /web/whoami/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from . import views 4 | 5 | urlpatterns = [ 6 | path("", views.WhoamiView.as_view(), name="whoami_get"), 7 | ] 8 | -------------------------------------------------------------------------------- /web/whoami/views.py: -------------------------------------------------------------------------------- 1 | from django.views import View 2 | from django.http import JsonResponse 3 | 4 | 5 | class WhoamiView(View): 6 | def get(self, request): 7 | headers_to_keep = set( 8 | ( 9 | "REMOTE_ADDR", 10 | "HTTP_X_FORWARDED_FOR", 11 | "HTTP_X_REAL_IP", 12 | "HTTP_USER_AGENT", 13 | ) 14 | ) 15 | http_headers = { 16 | key: value 17 | for (key, value) in request.META.items() 18 | if key in headers_to_keep 19 | } 20 | return JsonResponse(http_headers) 21 | -------------------------------------------------------------------------------- /web/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for web 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/2.0/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", "web.settings") 15 | 16 | application = get_wsgi_application() 17 | --------------------------------------------------------------------------------