diff --git a/.envs/.example b/.envs/.example new file mode 100644 index 00000000..ff7e1fe1 --- /dev/null +++ b/.envs/.example @@ -0,0 +1,40 @@ +# Use this file to create a .env with filled variables +# Django Variables +DJANGO_DEBUG=True +DEBUG_TOOLBAR=True +DJANGO_SECRET_KEY=change_me +USE_MINIO=True + +# Sentry Variables +DJANGO_ADMIN_URL=jandig-admin/ +ENABLE_SENTRY=False +SENTRY_DSN= +HEALTH_CHECK_URL=api/v1/status/ +SENTRY_TRACES_SAMPLE_RATE=0.1 + +## Amazon AWS Variables +AWS_ACCESS_KEY_ID=minio +AWS_SECRET_ACCESS_KEY=minio123 +MINIO_ROOT_USER=minio +MINIO_ROOT_PASSWORD=minio123 +AWS_STORAGE_BUCKET_NAME=jandig-cdn +AWS_PRIVATE_STORAGE_BUCKET_NAME=jandig-private-cdn +AWS_S3_REGION_NAME=us-east-2 +AWS_STATIC_LOCATION=static +MINIO_S3_ENDPOINT_URL=http://storage:9000 +MINIO_SITE_REGION=us-east-2 +MINIO_USER_ACCESS_KEY=minio-access-key +MINIO_USER_SECRET_KEY=minio-secret-key + +## Postgres variables +POSTGRES_HOST=postgres +POSTGRES_PORT=5432 +POSTGRES_DB=jandig +POSTGRES_USER=jandig +POSTGRES_PASSWORD=secret + +# Email server variables +SMTP_SERVER=smtp.gmail.com +SMTP_PORT=587 +JANDIG_EMAIL=local_jandig@jandig.com +JANDIG_EMAIL_PASSWORD=local_password diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md index b4823e2b..7f203b91 100644 --- a/.github/CODE_OF_CONDUCT.md +++ b/.github/CODE_OF_CONDUCT.md @@ -2,45 +2,66 @@ ## Our Pledge -In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. +We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards -Examples of behavior that contributes to creating a positive environment include: +Examples of behavior that contributes to a positive environment for our community include: -* Using welcoming and inclusive language -* Being respectful of differing viewpoints and experiences -* Gracefully accepting constructive criticism -* Focusing on what is best for the community -* Showing empathy towards other community members +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience +- Focusing on what is best not just for us as individuals, but for the overall community -Examples of unacceptable behavior by participants include: +Examples of unacceptable behavior include: -* The use of sexualized language or imagery and unwelcome sexual attention or advances -* Trolling, insulting/derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or electronic address, without explicit permission -* Other conduct which could reasonably be considered inappropriate in a professional setting +- The use of sexualized language or imagery, and sexual attention or advances of any kind +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others’ private information, such as a physical or email address, without their explicit permission +- Other conduct which could reasonably be considered inappropriate in a professional setting -## Our Responsibilities +## Enforcement Responsibilities -Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. +Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. -Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. +Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. ## Scope -This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. +This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Enforcement -Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at contato@memelab.com.br. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at jandig@memelab.com.br. All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the reporter of any incident. + +## Enforcement Guidelines +Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: + +1. Correction

+**Community Impact:** Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.

+**Consequence:** A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. + +2. Warning

+**Community Impact:** A violation through a single incident or series of actions.

+**Consequence:** A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. + +3. Temporary Ban

+**Community Impact:** A serious violation of community standards, including sustained inappropriate behavior.

+**Consequence:** A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. -Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. +4. Permanent Ban

+**Community Impact:** Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.

+**Consequence:** A permanent ban from any sort of public interaction within the community. ## Attribution -This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] +This Code of Conduct is adapted from the [Contributor Covenant][homepage] version 2.0, available at [https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][version]. [homepage]: http://contributor-covenant.org -[version]: http://contributor-covenant.org/version/1/4/ +[version]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..e688e4cb --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,88 @@ +name: CI pipeline + +on: push + +jobs: + linter: + name: run / linter + runs-on: ubuntu-latest + steps: + - name: Check out code from Github + uses: actions/checkout@v3 + - name: Set up Python 3.10 + uses: actions/setup-python@v4 + with: + python-version: "3.10" + - name: Installing flake8 + run: | + python -m pip install --upgrade pip + pip install flake8 + - name: Run flake8 linter + run: | + flake8 --max-line-length=200 --exclude=*/migrations src/ + + build: + name: run / build + runs-on: ubuntu-latest + needs: linter + steps: + - name: Check out code from Github + uses: actions/checkout@v3 + - name: Build image + run: | + # docker login -u $DOCKER_HUB_USER -p $DOCKER_HUB_PASS + docker build . -t jandigarte/django:$GITHUB_SHA + docker save -o docker_image_$GITHUB_SHA jandigarte/django:$GITHUB_SHA + - name: Caching image + uses: actions/cache@v3 + with: + key: jandigarte + path: docker_image_${{ github.sha }} + + migrations: + name: test / migrations + runs-on: ubuntu-latest + needs: build + steps: + - name: Check out code from Github + uses: actions/checkout@v3 + - name: Restoring cached image + uses: actions/cache@v3 + with: + key: jandigarte + path: docker_image_${{ github.sha }} + - name: Running container + env: + IMAGE_NAME: jandigarte/django:${{ github.sha }} + run: | + cp .envs/.example .envs/.env + docker load -i docker_image_$GITHUB_SHA + docker-compose -f docker-compose.ci.yml -p jandigarte_$GITHUB_SHA up --no-build -d + - name: Test migrations + run: | + docker exec jandigarte_${{ github.sha }}_django_1 sh -c "\ + poetry run src/manage.py makemigrations --check --dry-run" + + test: + name: test / unity + runs-on: ubuntu-latest + needs: build + steps: + - name: Check out code from Github + uses: actions/checkout@v3 + - name: Restoring cached image + uses: actions/cache@v3 + with: + key: jandigarte + path: docker_image_${{ github.sha }} + - name: Running container + env: + IMAGE_NAME: jandigarte/django:${{ github.sha }} + run: | + cp .envs/.example .envs/.env + docker load -i docker_image_$GITHUB_SHA + docker-compose -f docker-compose.ci.yml -p jandigarte_$GITHUB_SHA up --no-build -d + - name: Running users tests + run: | + docker exec jandigarte_${{ github.sha }}_django_1 sh -c "\ + poetry run src/manage.py test" \ No newline at end of file diff --git a/.github/workflows/dependencies.yml b/.github/workflows/dependencies.yml new file mode 100644 index 00000000..d44d267f --- /dev/null +++ b/.github/workflows/dependencies.yml @@ -0,0 +1,21 @@ +name: Dependencies pipeline + +on: + push: + paths: + - 'pyproject.toml' + +jobs: + dependencies: + name: test / dependencies + runs-on: ubuntu-latest + steps: + - name: Check out code from Github + uses: actions/checkout@v3 + - name: Check diff + id: diff_lock + run: | + echo "::set-output name=dependencies::$(git diff --name-only | grep poetry.lock -c)" + - name: Check poetry lock + if: ${{ steps.diff_lock.outputs.dependencies == 0 }} + run: exit 1 diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml new file mode 100644 index 00000000..ebb9996b --- /dev/null +++ b/.github/workflows/push.yml @@ -0,0 +1,24 @@ +name: Docker pipeline + +on: + push: + branches: + - develop + +# TODO: Change environment and login envs +jobs: + django: + name: push / django + environment: testes + runs-on: ubuntu-latest + needs: dependencies + steps: + - name: Check out code from Github + uses: actions/checkout@v3 + - name: Build django image + run: | + docker build . -t jandigarte/django:latest + - name: Push django image + run: | + docker login -u ${{ secrets.DOCKER_HUB_USER }} -p ${{ secrets.DOCKER_HUB_PASS }} + docker push jandigarte/django:latest diff --git a/.gitignore b/.gitignore index 5e7d1ba2..865d2101 100644 --- a/.gitignore +++ b/.gitignore @@ -2,9 +2,6 @@ src/ARte/config/__pycache__/ *.pyc -##vscode -\.vscode/ - ##sqlite *.sqlite3 @@ -25,4 +22,4 @@ docker/media/ *.mo *.po~ -src/.envs/.env +*/**/.env diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index a51586c9..200c388e 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -8,10 +8,10 @@ stages: - docker:dind script: - docker login -u $DOCKER_HUB_USER -p $DOCKER_HUB_PASS - - docker build . -f docker/Dockerfile -t jandigarte/django:$DOCKER_TAG --cache-from jandigarte/django:$DOCKER_TAG + - docker build . -t jandigarte/django:$DOCKER_TAG --cache-from jandigarte/django:$DOCKER_TAG - docker push jandigarte/django:$DOCKER_TAG tags: - - docker + - gitlab-org-docker build develop: extends: .build @@ -32,7 +32,7 @@ build tag: environment: production script: - docker login -u $DOCKER_HUB_USER -p $DOCKER_HUB_PASS - - "docker build . -f docker/Dockerfile + - "docker build . -t jandigarte/django:$CI_COMMIT_TAG -t jandigarte/django:$DOCKER_TAG -t jandigarte/django:latest" diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..1a7d670e --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "python.testing.pytestArgs": [ + "src", + "--ds=config.test_settings", + ], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true +} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..e62822d6 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,49 @@ +FROM python:3.10-slim-bullseye + +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + gettext \ + docutils-common \ + curl \ + wget + +COPY ./pyproject.toml /pyproject.toml +COPY ./poetry.lock /poetry.lock + +ENV PATH="$PATH:/root/.local/bin" \ + POETRY_NO_INTERACTION=1 \ + POETRY_VIRTUALENVS_CREATE=false \ + POETRY_CACHE_DIR='/var/cache/pypoetry' \ + TINI_VERSION=v0.19.0 \ + # poetry: + POETRY_VERSION=1.3.1 + +# Installing `poetry` package manager: +# https://github.com/python-poetry/poetry +RUN curl -sSL https://install.python-poetry.org | python3 - \ + && poetry --version + +RUN pip install --upgrade pip +RUN poetry install + +RUN dpkgArch="$(dpkg --print-architecture | awk -F- '{ print $NF }')" \ + && wget "https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini-${dpkgArch}" -O /usr/local/bin/tini \ + && chmod +x /usr/local/bin/tini && tini --version + + +RUN mkdir -p /jandig/src /jandig/locale /jandig/docs /jandig/static /jandig/build + +WORKDIR /jandig + +COPY ./src/ /jandig/src/ +COPY ./docs/ /jandig/docs/ +COPY ./locale/ /jandig/locale/ +COPY ./tasks.py /jandig/tasks.py +COPY ./run.sh /jandig/run.sh +COPY ./etc/ /jandig/etc/ + + +RUN find . | grep -E "(__pycache__|\.pyc|\.pyo$)" | xargs rm -rf + + +ENTRYPOINT ["tini", "--"] \ No newline at end of file diff --git a/Makefile b/Makefile index d552bd7e..99c09b28 100644 --- a/Makefile +++ b/Makefile @@ -1,40 +1,30 @@ -BACKUP_DIR = backup_$(shell date +'%d_%m_%Y-%H_%M_%S') -PASSWORD = -ROOT = -IP = -DESTINATION = -DOMAIN = staging.jandig.app -CERTPATH = /etc/letsencrypt/live/$(DOMAIN) -EMAIL = email@example.com -backup: - #sudo docker-compose -f docker/docker-compose.deploy.yml stop - mkdir $(BACKUP_DIR) +RUNNING_CONTAINER := $(shell docker compose ps --services --filter "status=running" | grep django ) - #sudo cp -r /var/lib/docker/volumes/ $(BACKUP_DIR)/volumes - sudo cp -r ./docker/media/ $(BACKUP_DIR)/media +test: + @if [[ -n "${RUNNING_CONTAINER}" ]]; then \ + docker compose exec django poetry run pytest src/core src/users; \ + else \ + docker compose run --rm django poetry run pytest src/core src/users;\ + fi - sudo chmod 755 $(BACKUP_DIR) - zip -r $(BACKUP_DIR).zip $(BACKUP_DIR) +test-ui: + docker compose up -d + poetry run pytest src/tests - sudo cp -r $(BACKUP_DIR).zip ./backups - sudo rm -rf ./$(BACKUP_DIR).zip - sudo rm -rf $(BACKUP_DIR) - #sudo docker-compose -f docker/docker-compose.deploy.yml up +lint: + poetry run black --line-length=200 src + poetry run isort src +flake8: + poetry run flake8 --max-line-length=200 --exclude=*/migrations src - +migrations: + poetry run python src/manage.py makemigrations +migrate: + poetry run python src/manage.py migrate - sshpass -p $(PASSWORD) scp ./backups/backup_$(shell date +'%d_%m_%Y-%H_%M_%S').zip $(ROOT)@$(IP):$(DESTINATION) - echo 'Send via scp' -fake-cert: - mkdir -p $(CERTPATH) - curl -s https://raw.githubusercontent.com/certbot/certbot/master/certbot-nginx/certbot_nginx/_internal/tls_configs/options-ssl-nginx.conf > "$(CERTPATH)/../../options-ssl-nginx.conf" +gen: + poetry run playwright codegen -b chromium --target python-pytest localhost:8000 - curl -s https://raw.githubusercontent.com/certbot/certbot/master/certbot/certbot/ssl-dhparams.pem > "$(CERTPATH)/../../ssl-dhparams.pem" - openssl req -x509 -nodes -newkey rsa:1024 -days 1 -keyout '$(CERTPATH)/privkey.pem' -out '$(CERTPATH)/fullchain.pem' -subj '/CN=localhost' - -cert: - rm -Rf /etc/letsencrypt/live/$(DOMAIN) - sudo rm -Rf /etc/letsencrypt/archive/$(DOMAIN) - sudo rm -Rf /etc/letsencrypt/renewal/$(DOMAIN).conf - certbot certonly --webroot -w /var/www/certbot --staging --email $(EMAIL) -d $(DOMAIN) --rsa-key-size 4096 --agree-tos --force-renewal \ No newline at end of file +translate_es: + poetry run inv i18n -l es_ES diff --git a/README.md b/README.md index 24971710..9993c655 100644 --- a/README.md +++ b/README.md @@ -41,26 +41,31 @@ You can find interviews and references to Jandig in the press [here](http://meme To contribute to Jandig ARte it would be awesome if you read [Contributing](https://github.com/memeLab/ARte/blob/master/.github/CONTRIBUTING.md) and our [Code of conduct](https://github.com/memeLab/ARte/blob/master/.github/CODE_OF_CONDUCT.md). After a good read you are ready to move foward! ### Prerequisites -We use docker and docker-compose to ensure a consistent development environment and to make the deploy process as painless as possible, so all you need on your development tools to run Jandig ARte is [Docker](https://www.docker.com/) and [Docker-Compose](https://docs.docker.com/compose/overview/). +We use docker and docker-compose to ensure a consistent development environment and to make the deploy process as painless as possible, so all you need on your development tools to run Jandig ARte is [Docker](https://www.docker.com/) and [Docker-Compose](https://docs.docker.com/compose/overview/). +
On Windows [Docker Desktop](https://docs.docker.com/desktop/windows/install/) also includes Docker Compose along with other Docker apps. ### Installing -Docker has good documentation on their website for installing docker and docker-compose for different operating systems like [Ubuntu](https://docs.docker.com/install/linux/docker-ce/ubuntu/) and [Debian](https://docs.docker.com/install/linux/docker-ce/debian/). To install docker-compose choose your operating system [here](https://docs.docker.com/compose/install/). +Docker has good documentation on their website for installing docker and docker-compose for different operating systems like [Ubuntu](https://docs.docker.com/install/linux/docker-ce/ubuntu/) and [Debian](https://docs.docker.com/install/linux/docker-ce/debian/). To install docker-compose choose your operating system [here](https://docs.docker.com/compose/install/).
+On Windows, you'll only need install [Docker Desktop](https://docs.docker.com/desktop/windows/install/) and [WSL2](https://docs.docker.com/desktop/windows/install/#wsl-2-backend). ### Running + +#### Linux OS + To run Jandig ARte all you need to do is: - Clone this repo - Navigate to the repository folder -- Run docker-compose passing the docker-compose.yml +- Run docker-compose - Voila! -``` +```bash git clone https://github.com/memeLab/Jandig cd Jandig -docker-compose -f docker/docker-compose.yml up +docker-compose up ``` If you get any error saying ``permission denied`` try run the command with sudo. ``` -sudo docker-compose -f docker/docker-compose.yml up +sudo docker-compose up ``` Jandig ARte server will run at localhost. To test modifications you just need to run a web browser and access [localhost:8000](localhost:8000). If you want to test on a mobile device, you will need a https connection, we recommend [ngrok](https://www.npmjs.com/package/ngrok) to generate a https link for you. @@ -75,3 +80,34 @@ ngrok will prompt 3 links: ![usage](https://user-images.githubusercontent.com/12930004/54871980-ab41da00-4d9b-11e9-8b80-bb1d4bec420d.png) Select the one with `https` at beginning. + +#### Windows OS + +To run Jandig ARte all you need to do is: +- Clone this repo +- Navigate to the repository folder using WSL2 +- Run the conteiners 'jandigarte/django' and 'jandigarte/requirements' on Docker Desktop +- Run docker-compose passing the docker-compose.yml +- Voila! + +```bash +git clone https://github.com/memeLab/Jandig +cd Jandig +docker-compose up +``` +If you get any error saying ``permission denied`` try run the command with sudo. +``` +sudo docker-compose up +``` + +Jandig ARte server will run at localhost, but by default WSL2 enables wsl localhost to accessible from windows, but no vice versa. To access you'll need to use windows system IP from wsl2. + +- Run this command on terminal: +``` +vim /etc/resolv.conf +``` +- Get Network Interface (WSL) IP. It should be like: +``` +nameserver 172.21.176.1 +``` +- Access the localhost using this IP (e.g.: 172.21.176.1:8000) \ No newline at end of file diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..d2b60f4d --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,14 @@ +# Security Policy + +This document aims to provide instructions about how to report security vulnerabilities. Jandig, as an open-source community, prizes this kind of interaction since we'll use these pieces of information for tracking and correcting defects. + + +## Reporting Security Issues + +At Jandig, we value keeping the security integrity of the platform. Therefore, vulnerabilities reports and issues related to security are welcome and must be made through GitHub issues. +Before creating a report issue, you must collect data about the vulnerability, such as print screens and paths for reaching the problem. Once you have collected all the information needed to reproduce the error, you must now describe it as a bug report issue. You can find more details about how to create a issue on [CONTRIBUTING](https://github.com/memeLab/Jandig/blob/develop/.github/CONTRIBUTING.md) document. + + +## Language + +We prefer to keep all internal communication of the project in English. diff --git a/docker-compose.ci.yml b/docker-compose.ci.yml new file mode 100644 index 00000000..683b5f72 --- /dev/null +++ b/docker-compose.ci.yml @@ -0,0 +1,63 @@ +version: '3.5' + +services: + django: + image: "${IMAGE_NAME}" + ports: + - 8000:8000 + links: + - "postgres:postgres" + - "storage:storage" + - "createbuckets:createbuckets" + volumes: + - ./src/:/src/ + - ./docs/:/src/docs/ + - ./etc/:/src/etc/ + - ./locale/:/src/locale/ + - ./run.sh:/src/run.sh + - ./tasks.py:/src/tasks.py + env_file: + - .envs/.env + command: ./run.sh + + postgres: + image: postgres:12.6 + env_file: + - .envs/.env + command: postgres -c max_connections=10000 + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + + storage: + image: minio/minio:latest + ports: + - 9000:9000 + - 9001:9001 + volumes: + - storage:/storage + env_file: + - .envs/.env + command: server /storage --console-address ":9001" + + createbuckets: + image: minio/mc + depends_on: + - storage + env_file: + - .envs/.env + entrypoint: > + /bin/sh -c " + until (/usr/bin/mc config host add myminio $${MINIO_S3_ENDPOINT_URL} $${MINIO_ROOT_USER} $${MINIO_ROOT_PASSWORD}) do echo '...waiting...' && sleep 1; done; + /usr/bin/mc mb myminio/$${AWS_STORAGE_BUCKET_NAME}; + /usr/bin/mc policy set download myminio/$${AWS_STORAGE_BUCKET_NAME}; + /usr/bin/mc mb myminio/$${AWS_PRIVATE_STORAGE_BUCKET_NAME}; + /usr/bin/mc admin user add myminio $${MINIO_USER_ACCESS_KEY} $${MINIO_USER_SECRET_KEY}; + /usr/bin/mc admin policy set myminio readwrite user=$${MINIO_USER_ACCESS_KEY}; + exit 0; + " + +volumes: + postgres_data: + storage: diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..555e958e --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,74 @@ +services: + django: + build: + dockerfile: Dockerfile + context: . + ports: + - 8000:8000 + volumes: + - ./src/:/jandig/src/ + - ./docs/:/jandig/docs/ + - ./etc/:/jandig/etc/ + - ./locale/:/jandig/locale/ + - ./run.sh:/jandig/run.sh + - ./tasks.py:/jandig/tasks.py + env_file: + - .envs/.example + depends_on: + storage: + condition: service_started + createbuckets: + condition: service_started + postgres: + condition: service_healthy + command: /jandig/run.sh + + postgres: + image: postgres:15.4 + env_file: + - .envs/.example + environment: + PGUSER: jandig + command: postgres -c max_connections=10000 + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready", "-d", "jandig"] + interval: 5s + timeout: 60s + retries: 20 + start_interval: 5s + + storage: + image: minio/minio:latest + ports: + - 9000:9000 + - 9001:9001 + volumes: + - media_data:/storage + env_file: + - .envs/.env + command: server /storage --console-address ":9001" + + createbuckets: + image: minio/mc:RELEASE.2022-09-16T09-16-47Z + depends_on: + - storage + env_file: + - .envs/.env + entrypoint: > + /bin/sh -c " + until (/usr/bin/mc config host add myminio $${MINIO_S3_ENDPOINT_URL} $${MINIO_ROOT_USER} $${MINIO_ROOT_PASSWORD}) do echo '...waiting...' && sleep 1; done; + /usr/bin/mc mb myminio/$${AWS_STORAGE_BUCKET_NAME}; + /usr/bin/mc policy set download myminio/$${AWS_STORAGE_BUCKET_NAME}; + /usr/bin/mc mb myminio/$${AWS_PRIVATE_STORAGE_BUCKET_NAME}; + /usr/bin/mc admin user add myminio $${MINIO_USER_ACCESS_KEY} $${MINIO_USER_SECRET_KEY}; + /usr/bin/mc admin policy set myminio readwrite user=$${MINIO_USER_ACCESS_KEY}; + exit 0; + " + +volumes: + postgres_data: + media_data: \ No newline at end of file diff --git a/docker/Dockerfile b/docker/Dockerfile deleted file mode 100644 index b59f6441..00000000 --- a/docker/Dockerfile +++ /dev/null @@ -1,12 +0,0 @@ -FROM jandigarte/requirements:latest - -RUN mkdir -p /ARte -WORKDIR /ARte -COPY docs/ /ARte/docs/ -COPY ./src/ARte /ARte/src/ARte/ -COPY ./locale/ /ARte/locale/ -COPY ./tasks.py /ARte/tasks.py -COPY ./run.sh /ARte/run.sh -COPY ./etc/ /ARte/etc/ - -RUN find . | grep -E "(__pycache__|\.pyc|\.pyo$)" | xargs rm -rf diff --git a/docker/base.Dockerfile b/docker/base.Dockerfile deleted file mode 100644 index a47841ff..00000000 --- a/docker/base.Dockerfile +++ /dev/null @@ -1,15 +0,0 @@ -FROM debian:buster-slim - -RUN apt-get update && apt-get install -y --no-install-recommends \ - python3 \ - python3-pip \ - python3-setuptools \ - gettext \ - docutils-common -COPY ./src/requirements.txt /src/requirements.txt - -RUN pip3 install --upgrade pip -RUN pip3 install --no-cache-dir toolz -RUN pip3 install --no-cache-dir -r /src/requirements.txt - -RUN rm -rf ~/.cache/pip diff --git a/docker/build-base.sh b/docker/build-base.sh deleted file mode 100755 index c782a0dc..00000000 --- a/docker/build-base.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env bash - -set -o errexit -set -o pipefail -set -o nounset - -# Build base image -docker build ../ -f base.Dockerfile -t jandigarte/requirements:latest - -# Check if user wants to publish -if [ $# -eq 1 ]; then - if [ "$1" = "publish" ]; then - echo; echo; echo "PUBLISHING IMAGES" - docker push jandigarte/requirements:latest; - fi -else - echo "Execute 'sh build.sh publish' to publish images" -fi diff --git a/docker/docker-compose.deploy.yml b/docker/docker-compose.deploy.yml deleted file mode 100644 index c523a3c5..00000000 --- a/docker/docker-compose.deploy.yml +++ /dev/null @@ -1,136 +0,0 @@ -version: '3.5' - -services: - nginx: - image: nginx:1.15-alpine - ports: - - "80:80" - - "443:443" - volumes: - - ../src/data/nginx:/etc/nginx/conf.d - - ../src/data/certbot/conf:/etc/letsencrypt - - ../src/data/certbot/www:/var/www/certbot - - static_dev_files:/usr/share/nginx/html/static/dev - - static_staging_files:/usr/share/nginx/html/static/staging - - static_prod_files:/usr/share/nginx/html/static/prod - command: "/bin/sh -c 'while :; do sleep 6h & wait $${!}; nginx -s reload; done & nginx -g \"daemon off;\"'" - depends_on: - - dev_django - - staging_django - - prod_django - labels: - - "com.centurylinklabs.watchtower.enable=false" - - certbot: - image: certbot/certbot - volumes: - - ../src/data/certbot/conf:/etc/letsencrypt - - ../src/data/certbot/www:/var/www/certbot - entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'" - labels: - - "com.centurylinklabs.watchtower.enable=false" - - # DEV ------------------------------------------------------------------------------------------ # - - dev_django: - image: jandigarte/django:dev - ports: - - 8000:8000 - env_file: - - ../src/.envs/.dev_deploy - depends_on: - - dev_postgres - volumes: - - static_dev_files:/collect/ - - ./media/dev/:/ARte/src/ARte/users/media/ - command: inv db -p docs collect run -g -p - labels: - - "com.centurylinklabs.watchtower.enable=true" - - dev_postgres: - image: postgres:11.6 - env_file: - - ../src/.envs/.dev_deploy - volumes: - - postgres_dev_data:/var/lib/postgresql/data - ports: - - "5432:5432" - labels: - - "com.centurylinklabs.watchtower.enable=false" - # ------------------------------------------------------------------------------------------ # - - # STAGING ------------------------------------------------------------------------------------------ # - - staging_django: - image: jandigarte/django:staging - ports: - - 8001:8000 - env_file: - - ../src/.envs/.staging_deploy - depends_on: - - staging_postgres - volumes: - - static_staging_files:/collect/ - - ./media/staging/:/ARte/src/ARte/users/media/ - command: inv db -p docs collect run -g -p - labels: - - "com.centurylinklabs.watchtower.enable=true" - - staging_postgres: - image: postgres:11.6 - env_file: - - ../src/.envs/.staging_deploy - volumes: - - postgres_staging_data:/var/lib/postgresql/data - ports: - - "5433:5432" - labels: - - "com.centurylinklabs.watchtower.enable=false" - - # ------------------------------------------------------------------------------------------ # - - # PROD ------------------------------------------------------------------------------------------ # - - prod_django: - image: jandigarte/django:latest - ports: - - 8002:8000 - env_file: - - ../src/.envs/.prod_deploy - depends_on: - - prod_postgres - volumes: - - static_prod_files:/collect/ - - ./media/prod/:/ARte/src/ARte/users/media/ - command: inv db -p docs collect run -g -p - labels: - - "com.centurylinklabs.watchtower.enable=true" - - prod_postgres: - image: postgres:11.6 - env_file: - - ../src/.envs/.prod_deploy - volumes: - - postgres_prod_data:/var/lib/postgresql/data - ports: - - "5434:5432" - labels: - - "com.centurylinklabs.watchtower.enable=false" - - # ------------------------------------------------------------------------------------------ # - - watchtower: - image: containrrr/watchtower - volumes: - - /var/run/docker.sock:/var/run/docker.sock - command: --interval 30 - labels: - - "com.centurylinklabs.watchtower.enable=false" - -volumes: - postgres_dev_data: - postgres_staging_data: - postgres_prod_data: - static_dev_files: - static_staging_files: - static_prod_files: \ No newline at end of file diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml deleted file mode 100644 index a5f4e9cb..00000000 --- a/docker/docker-compose.yml +++ /dev/null @@ -1,33 +0,0 @@ -version: '3.5' - -services: - django: - image: jandigarte/django:latest - restart: always - build: - context: ../ - dockerfile: ./docker/Dockerfile - ports: - - 8000:8000 - volumes: - - ../src/ARte/:/ARte/src/ARte/ - - ../locale/:/ARte/locale/ - - ./media/:/ARte/src/ARte/users/media/ - env_file: - - ../src/.envs/.env - depends_on: - - postgres - command: ./run.sh - - postgres: - image: postgres:12.6 - env_file: - - ../src/.envs/.env - command: postgres -c max_connections=10000 - ports: - - "5432:5432" - volumes: - - postgres_data:/var/lib/postgresql/data - -volumes: - postgres_data: diff --git a/docker/init-production.sh b/docker/init-production.sh deleted file mode 100755 index 63e2cfc9..00000000 --- a/docker/init-production.sh +++ /dev/null @@ -1,85 +0,0 @@ -#!/bin/bash - -domains=(dev.jandig.app staging.jandig.app jandig.app) -rsa_key_size=4096 -data_path="../src/data/certbot" -email="" # Adding a valid address is strongly recommended -staging=1 # Set to 1 if you're testing your setup to avoid hitting request limits -composefile=./docker-compose.deploy.yml - - -if [ -d "$data_path" ]; then - read -p "Existing data found for $domains. Continue and replace existing certificate? (y/N) " decision - if [ "$decision" != "Y" ] && [ "$decision" != "y" ]; then - docker-compose -f $composefile down - docker-compose -f $composefile up -d nginx - exit - fi -fi - - -if [ ! -e "$data_path/conf/options-ssl-nginx.conf" ] || [ ! -e "$data_path/conf/ssl-dhparams.pem" ]; then - echo "### Downloading recommended TLS parameters ..." - mkdir -p "$data_path/conf" - curl -s https://raw.githubusercontent.com/certbot/certbot/master/certbot-nginx/certbot_nginx/options-ssl-nginx.conf > "$data_path/conf/options-ssl-nginx.conf" - curl -s https://raw.githubusercontent.com/certbot/certbot/master/certbot/ssl-dhparams.pem > "$data_path/conf/ssl-dhparams.pem" - echo -fi - - -for domain in ${domains[@]} -do - echo "### Creating dummy certificate for $domain ..." - path="/etc/letsencrypt/live/$domain" - mkdir -p "$data_path/conf/live/$domain" - docker-compose -f $composefile run --rm --entrypoint "\ - openssl req -x509 -nodes -newkey rsa:1024 -days 1\ - -keyout '$path/privkey.pem' \ - -out '$path/fullchain.pem' \ - -subj '/CN=localhost'" certbot - echo -done - -echo "### Starting nginx ..." -docker-compose -f $composefile up --force-recreate -d nginx -echo - -for domain in ${domains[@]} -do - echo "### Deleting dummy certificate for $domain ..." - docker-compose -f $composefile run --rm --entrypoint "\ - rm -Rf /etc/letsencrypt/live/$domain && \ - rm -Rf /etc/letsencrypt/archive/$domain && \ - rm -Rf /etc/letsencrypt/renewal/$domain.conf" certbot - echo -done - - - -# Select appropriate email arg -case "$email" in - "") email_arg="--register-unsafely-without-email" ;; - *) email_arg="--email $email" ;; -esac - -# Enable staging mode if needed -if [ $staging != "0" ]; then staging_arg="--staging"; fi - -#Join $domains to -d args -for domain in "${domains[@]}" -do - domain_arg="-d $domain" - echo "### Requesting Let's Encrypt certificate for $domain ..." - docker-compose -f $composefile run --rm --entrypoint "\ - certbot certonly --webroot -w /var/www/certbot \ - $staging_arg \ - $email_arg \ - $domain_arg \ - --rsa-key-size $rsa_key_size \ - --agree-tos \ - --force-renewal" certbot - echo -done - -echo "### Reloading nginx ..." -docker-compose -f $composefile exec nginx nginx -s reload diff --git a/docker/nonginx.yml b/docker/nonginx.yml deleted file mode 100644 index 762d88ef..00000000 --- a/docker/nonginx.yml +++ /dev/null @@ -1,33 +0,0 @@ -version: "3.5" - -services: - - prod_django: - image: jandigarte/django:latest - ports: - - 8000:8000 - build: - context: ../ - dockerfile: ./docker/Dockerfile - env_file: - - ../src/.envs/.prod_deploy - depends_on: - - prod_postgres - volumes: - - ./static/:/ARte/collect/ - - ./media/prod/:/ARte/src/ARte/users/media/ - command: inv db -p docs collect run -g -p - - prod_postgres: - image: postgres:11.6 - env_file: - - ../src/.envs/.prod_deploy - command: postgres -c max_connections=10000 - volumes: - - postgres_prod_data:/var/lib/postgresql/data - ports: - - "5432:5432" - -volumes: - postgres_prod_data: - static_prod_files: \ No newline at end of file diff --git a/docker/taskfile.sh b/docker/taskfile.sh deleted file mode 100644 index d9a606c4..00000000 --- a/docker/taskfile.sh +++ /dev/null @@ -1,66 +0,0 @@ -#!/bin/bash - -data_path="../src/data/certbot" - -rsa_key_size=4096 -email="" # Adding a valid address is strongly recommended -staging=0 # Set to 1 if you're testing your setup to avoid hitting request limits -composefile=./docker-compose.deploy.yml - -function fake-cert { - path="/etc/letsencrypt/live/$1" - echo "### Creating dummy certificate for $1 ..." - mkdir -p "$data_path/conf/live/$1" - docker-compose -f $composefile run --rm --entrypoint "\ - openssl req -x509 -nodes -newkey rsa:1024 -days 1\ - -keyout '$path/privkey.pem' \ - -out '$path/fullchain.pem' \ - -subj '/CN=localhost'" certbot - echo - chmod 644 '$path/privkey.pem' -} - -function delete-cert { - echo "### Deleting dummy certificate for $1 ..." - docker-compose -f $composefile run --rm --entrypoint "\ - rm -Rf /etc/letsencrypt/live/$1 && \ - rm -Rf /etc/letsencrypt/archive/$1 && \ - rm -Rf /etc/letsencrypt/renewal/$1.conf" certbot - echo -} - -function download-params { - if [ ! -e "$data_path/conf/options-ssl-nginx.conf" ] || [ ! -e "$data_path/conf/ssl-dhparams.pem" ]; then - echo "### Downloading recommended TLS parameters ..." - mkdir -p "$data_path/conf" - curl -s https://raw.githubusercontent.com/certbot/certbot/master/certbot-nginx/certbot_nginx/options-ssl-nginx.conf > "$data_path/conf/options-ssl-nginx.conf" - curl -s https://raw.githubusercontent.com/certbot/certbot/master/certbot/ssl-dhparams.pem > "$data_path/conf/ssl-dhparams.pem" - echo - fi -} - -function request-cert { - # Select appropriate email arg - case "$email" in - "") email_arg="--register-unsafely-without-email" ;; - *) email_arg="--email $email" ;; - esac - - # Enable staging mode if needed - if [ $staging != "0" ]; then staging_arg="--staging"; fi - - domain_arg="-d $1" - echo "### Requesting Let's Encrypt certificate for $1 ..." - docker-compose -f $composefile run --rm --entrypoint "\ - certbot certonly --webroot -w /var/www/certbot \ - $staging_arg \ - $email_arg \ - $domain_arg \ - --rsa-key-size $rsa_key_size \ - --agree-tos \ - --force-renewal" certbot - echo -} - - -"$@" \ No newline at end of file diff --git a/docs/animacoes-ingles.rst b/docs/animacoes-ingles.rst new file mode 100644 index 00000000..bc1c6361 --- /dev/null +++ b/docs/animacoes-ingles.rst @@ -0,0 +1,103 @@ +Tips for producing animation +============================= +This document contains technical guidelines and best practices for +production of augmented reality animations for Jandig. Lots of +of these guidelines can be used to produce content for +other platforms. + +In addition to producing a version considering the following limitations, +it is recommended that an “ideal” version be produced, which can be +used in controlled environments and/or in the future (as these +limitations will decrease). + +File format +~~~~~~~~~~~~~~~~~~ + +Currently, the only supported format is GIF. + +Details +~~~~~~~~ + +Use as few details and small elements as possible, +as they may not be identifiable by the public. + +One way to test here is to save the storyboard images to +300x300px and see if you can identify all the elements. It's +important to remember that the public can see the animation from a distance, +so that it looks very small on the phone screen. + +Amount of colors +~~~~~~~~~~~~~~~~~~~ + +The recommendation is to reduce as much as possible, so it will not +compromise the original colors. + +To optimize these values, we recommend minimizing the use of color degradation and +avoid fade transitions. + +A technique to plan this usage before producing the animation is +export the storyboard images to GIF with different amounts of +Colors. + +Resolution +~~~~~~~~~ + +To increase device compatibility, we limit the resolution +of the display frame (which appears in full screen on the smartphone) in +640x480 pixels. As the animation will only appear in a part of this +framework, we recommend creating the content at 300x300px. + +Framerate +~~~~~~~~~ + +The frame rate (measured in frames per second) +recommended is a maximum of 12 fps. + +Loop +~~~~ + +To create the illusion of continuity, the animation must be looped. In other words, the transition from the last to the first frame must be imperceptible. + +Time +~~~~~ + +The shorter the animation, the better. This will allow for better quality +of images and ensure that the public watches all the material. So far, the longest animation made for Jandig is approximately +20 seconds. The recommendation is that it should have up to 15 seconds. + +File size +~~~~~~~~~~~~~~~~~~ + +While the following parameters have flexibility as to guidelines, +this is the toughest. Files should ideally be up to 500 kB and should not exceed 1 MB. This is a decisive factor in the choice of works to be +included in an exhibition, the smaller the better. + +This limitation exists mainly for the following reasons: - We do not have +control of the public connection speed when accessing the content, +which can cause the download of all works to take time. - Not +we want to burden the public's data plan. - Smaller files are more +lightweight require less processing, making the +platform compatible with a greater number of phones. + +Conclusion +~~~~~~~~~ + +To reach an optimal result, the ideal is to test the parameters together. + +A recommended process is exporting the parameters, limiting them as much as possible. +(8 colors, 200x200px, 5 fps) and also with the minimum recommendations of +limitation (256 colors, 300x300px, 12 fps) and compare. + +From there, modify one parameter at a time from the minimum limitations, +reducing only the framerate/resolution/colors to see what is the minimum +that works fine. + +If, after reducing the 3 to the minimum that works well, the file is still +large, keep decreasing the parameters in different combinations +to reach an optimal result. Also remember to keep the +high resolution version. Here the recommendation is to save with 1000x1000px and +24fps. + +In case you use Adobe Media encoder, there is a tutorial that was developed +by UEMG for `download in +PDF `__. diff --git a/docs/animacoes.rst b/docs/animacoes-portugues.rst similarity index 100% rename from docs/animacoes.rst rename to docs/animacoes-portugues.rst diff --git a/docs/architecture-document.md b/docs/architecture-document.md index 94f17f36..a095ca21 100644 --- a/docs/architecture-document.md +++ b/docs/architecture-document.md @@ -4,6 +4,13 @@ |--------|------------|-----------|------| |1.0|Initial version|Victor Gomide & Emanuel Holanda|05/10/2020| |1.1|New diagrams in PlantUml| VIctor Gomide & Emanuel Holanda |24/10/2020| +|1.2|Reference the Plant UML source code in the doc|Gabrielle Ribeiro, Gustavo Duarte & Victor Amaral|09/02/2022| +|1.3|Refactor the architectural reference topic|Hugo Sobral, Sofia Patrocínio|10/02/2022| +|1.4|Fix typos|Sofia Patrocínio, Hugo Sobral|10/02/2022| +|1.5|Update package diagram logic view Plant UML|João Pedro Guedes|10/02/2022| +|1.6|Add images to technologies and links to their documentation|João Pedro Guedes|15/02/2022| +|1.7|Add description of the actors present in the use case view|Sofia Patrocinio|21/02/2022| +|1.7|Add description in logical view and write implementation view|João Pedro Guedes|23/02/2022| # Software Architecture Document (SAD) @@ -21,8 +28,9 @@ This document is extremely important for understanding the project as a whole, s - **SAD:** Software Architecture Document - **App:** Application -- **MTV:** Model-Template-View +- **MTV:** Model-View-Template - **PWA:** Progressive Web App +- **MVC:** Model-View-Controller ### References @@ -33,6 +41,7 @@ This document is extremely important for understanding the project as a whole, s - [Documento de Arquitetura de Software RDI-AEE](https://www.google.com/url?sa=t&rct=j&q=&esrc=s&source=web&cd=&cad=rja&uact=8&ved=2ahUKEwiE78LEr5zsAhV7GLkGHSWyAVMQFjAAegQIBRAC&url=http%3A%2F%2Frepositorio.aee.edu.br%2Fbitstream%2Faee%2F1106%2F3%2FTCC2_2018_2_GabrielLeiteDias_MatheusLimadeAlbuquerque_Apendice2.pdf&usg=AOvVaw2wXEOkYpBHmN32ChHHDgOh) - [The Django Book: Django's Structure](https://djangobook.com/mdj2-django-structure/) + ### Overview In order to explain Jandig ARte's architecture from different points of views, here's an approach of the next topics: @@ -45,12 +54,43 @@ In order to explain Jandig ARte's architecture from different points of views, h ## Architectural Representation -Project's main programming language is Python, through Django framework. Django uses an exclusive architecture called **MTV (Model-Template-View)**, in which **Model** represents the data layer, **Template** represents user's interface and **View** acts as an intermediary layer between them: - -![](images/mtv-architecture-diagram.png) +Project's main programming language is Python, through Django framework. Django uses an exclusive architecture called **MVT (Model-View-Template)**, in which **Model** represents the data layer, **Template** represents user's interface and **View** acts as an intermediary layer. Jandig also uses **Jinja** for enhancing Django's Template view for MVT architecture. Jandig ARte is a **Progressive Web App (PWA)**, which means it is an web app that has a similar use to a native mobile app. It uses **PostgreSQL** as database. +### Django Framework + +![django-icon](https://www.djangoproject.com/m/img/logos/django-logo-negative.png) + +Django is a Python based web framework that provides a rapid, clean and pragmatic development. Django also offers a bunch of benefits that can take care of the hassle of web development, *i.e.*, Django is a fast, secure and scalable tool. You can read more about it [here](https://www.djangoproject.com/start/overview/) + +### Model-View-Template + +Model-View-Template (also called MVT) is a specific Django architecture focused on web applications. Although it seems similar to *MVC* architecture, *MVT* is slightly different. The design model determines the workflow of a Django app. The specific Jandig structure can be seen in the following picture: + +![mtv-architecture-diagram](http://www.plantuml.com/plantuml/proxy?cache=no&src=https://raw.githubusercontent.com/memeLab/Jandig/develop/docs/images/plantUML/mtv-architecture-diagram.puml) + +**Model** stands for the data representation in the app. It's commonly represented as the database tables. +**View** is responsible for the HTTP communication, including requests as well as responses. +**Template** stands for the visualization layer and can be seen as the front-end dynamic component of Django. + +### Jinja +![jinja-logo](https://jinja.palletsprojects.com/en/3.0.x/_images/jinja-logo.png) + +Jinja is a Python based web template engine. Jinja can generate any markup as source code and also provides Python-like expressions for templates. The engine template allows customization of tags, filters, tests and global settings, and unlike the Django template engine, Jinja allows the template designer to call functions with arguments on objects. You can read more about it [here](https://jinja.palletsprojects.com/en/3.0.x/intro/) + +### Progressive Web App + +A progressive Web App is a subtype of web software applications that is built to deliver enhanced capabilities, reliability and installability while reaching anyone, anywhere on any device with a standards-compilant browser (this includes desktop as well as mobile devices). + +This kind of application offers a bunch of benefits to projects, since PWA's are ever-present, on home screens, docks and taskbars. They can also read and write files from the local file system, interact with data stored on device and even access the device hardware. + +### PostgreSQL + +![postgreSQL](https://miro.medium.com/max/800/0*z58cqZWxu2_4q5-g.jpg) + +PostgreSQL is an advanced version of SQL. In short, PostgreSQL is an open source relational database system that supports both SQL and JSON querying, it also provides support to different functions of SQL-like statements, such as foreign keys, subqueries, triggers and many different user-defined types and functions. You can read more about it [here](https://www.postgresql.org/) + ## Architectural Goals and Constraints @@ -61,53 +101,53 @@ Jandig was created for providing a low-cost and easy-to-use augmented reality ex - No previous knowledge of programming necessary for using it; - Free, or cheap enough; - Available in different languages, so people around the world can use it; -- Open Source, so community can work together for making it awesome! +- Open Source, so the community can work together to make it awesome! ## Use-Case View ### Account Access and Management -The diagram below shows how account access and management is done, with users and system as actors. Please note that Visitor is an user that hasn't log in the app. - -![](images/use-case-diagram-user.png) +The diagram below shows how account access and management is done, with users and system as actors. Please note that Visitor is a user that hasn't log in the app to access an exhibition. +![use-case-diagram-user](http://www.plantuml.com/plantuml/proxy?cache=no&src=https://raw.githubusercontent.com/memeLab/Jandig/develop/docs/images/plantUML/use-case-diagram-user.puml) ### Artist Role -The following diagram shows the application's features focused on the Artist's role. - - -![](images/use-case-diagram-artist.png) +The following diagram shows the application's features focused on the Artist's role. The Artist's role can share their artworks and exhibitions. +![use-case-diagram-artist](http://www.plantuml.com/plantuml/proxy?cache=no&src=https://raw.githubusercontent.com/memeLab/Jandig/develop/docs/images/plantUML/use-case-diagram-artist.puml) ### Other Features -Below are shown some other interesting features from Jandig ARte: +Below are shown some other interesting features from Jandig ARte that doesn't need log in: -![](images/use-case-diagram-features.png) +![use-case-diagram-features](http://www.plantuml.com/plantuml/proxy?cache=no&src=https://raw.githubusercontent.com/memeLab/Jandig/develop/docs/images/plantUML/use-case-diagram-features.puml) ## Logical View -### Overview +The logical view describe the system scructure, showning how the system is organized in terms of packages, intefaces etc. +### Package Diagram Since the software is Django-based, it contains projects, apps and layers. in Jandig ARte case, we have two main apps: "core" and "users". -![](images/package-diagram-logical-view.png) - +![package-diagram-logical-view](http://www.plantuml.com/plantuml/proxy?cache=no&src=https://raw.githubusercontent.com/memeLab/Jandig/develop/docs/images/plantUML/package-diagram-logical-view.puml) -### Architecturally Significant Design Packages +## Implementation View +The implementation view describes how the system will be implemented. One of its main features is the Class Diagram. + +### Architecturally Significant Design Packages #### "Profile" Class Diagram -![](images/class-diagram-profile.png) +![Profile-class-diagram](http://www.plantuml.com/plantuml/proxy?cache=no&src=https://raw.githubusercontent.com/memeLab/Jandig/develop/docs/images/plantUML/class-diagram-profile.puml) #### "Marker" Class Diagram -![](images/class-diagram-marker.png) +![Marker-class-diagram](http://www.plantuml.com/plantuml/proxy?cache=no&src=https://raw.githubusercontent.com/memeLab/Jandig/develop/docs/images/plantUML/class-diagram-marker.puml) #### "Artwork" Class Diagram -![](images/class-diagram-artwork.png) +![Artwork-class-diagram](http://www.plantuml.com/plantuml/proxy?cache=no&src=https://raw.githubusercontent.com/memeLab/Jandig/develop/docs/images/plantUML/class-diagram-artwork.puml) #### "Object" Class Diagram -![](images/class-diagram-object.png) +![Object-class-diagram](http://www.plantuml.com/plantuml/proxy?cache=no&src=https://raw.githubusercontent.com/memeLab/Jandig/develop/docs/images/plantUML/class-diagram-object.puml) #### "Exhibit" Class Diagram -![](images/class-diagram-exhibit.png) +![Exhibit-class-diagram](http://www.plantuml.com/plantuml/proxy?cache=no&src=https://raw.githubusercontent.com/memeLab/Jandig/develop/docs/images/plantUML/class-diagram-exhibit.puml) diff --git a/docs/conf.py b/docs/conf.py index 453f7feb..8ade9a1e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -58,5 +58,21 @@ # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] +html_theme_options = { + 'logo': 'logo.png', + 'github_user': 'memeLab', + 'github_repo': 'Jandig', + 'html_sidebars': { + '**': [ + 'search.html', + 'navigation.html', + ] + }, + 'extra_nav_links': { + 'Official Site': 'https://jandig.app', + 'Github': 'https://github.com/memelab/Jandig' + }, +} + def setup(app): app.add_css_file('reset.css') \ No newline at end of file diff --git a/docs/fix-code-smells.md b/docs/fix-code-smells.md new file mode 100644 index 00000000..cb1e4799 --- /dev/null +++ b/docs/fix-code-smells.md @@ -0,0 +1,29 @@ +# Fixing code smells on project + +To identify and map python code smells on Jandig project, the development team uses [Prospector](https://github.com/PyCQA/prospector). Prospector is a Python tool used to output information about errors, unnecessary code complexity and convention violations for the language. + + +## Installation + +Before installing Prospector, you need to assure that pip is properly installed on your machine. Once you have pip, you can simply type the command + +``` +pip install prospector +``` + +You can also follow the [official Prospector installation guide](https://github.com/PyCQA/prospector#installation). + + +## Running Prospector on Jandig + +To get the information about code smells you need just to run the following command on the root directory of Jandig: + +``` +prospector +``` + +If you want to store the output on a ```.txt``` file, you can must the following command: + +``` +prospector > filename.txt +``` \ No newline at end of file diff --git a/docs/images/installation-guide-createFile.png b/docs/images/installation-guide-createFile.png new file mode 100644 index 00000000..4c07fe8b Binary files /dev/null and b/docs/images/installation-guide-createFile.png differ diff --git a/docs/images/installation-guide-duplicate-dependency.png b/docs/images/installation-guide-duplicate-dependency.png new file mode 100644 index 00000000..3c0bcd2c Binary files /dev/null and b/docs/images/installation-guide-duplicate-dependency.png differ diff --git a/docs/images/installation-guide-expected-window.png b/docs/images/installation-guide-expected-window.png new file mode 100644 index 00000000..817e4556 Binary files /dev/null and b/docs/images/installation-guide-expected-window.png differ diff --git a/docs/images/installation-guide-migration.png b/docs/images/installation-guide-migration.png new file mode 100644 index 00000000..65ccb283 Binary files /dev/null and b/docs/images/installation-guide-migration.png differ diff --git a/docs/images/installation-guide-minio.png b/docs/images/installation-guide-minio.png new file mode 100644 index 00000000..5e788c21 Binary files /dev/null and b/docs/images/installation-guide-minio.png differ diff --git a/docs/images/installation-guide-terminal-expected.png b/docs/images/installation-guide-terminal-expected.png new file mode 100644 index 00000000..957662e2 Binary files /dev/null and b/docs/images/installation-guide-terminal-expected.png differ diff --git a/docs/images/plantUML/mtv-architecture-diagram.puml b/docs/images/plantUML/mtv-architecture-diagram.puml index 24c079b2..56a6889e 100644 --- a/docs/images/plantUML/mtv-architecture-diagram.puml +++ b/docs/images/plantUML/mtv-architecture-diagram.puml @@ -3,7 +3,7 @@ left to right direction rectangle "Client Side" as C { - rectangle "Django Template" as DT + rectangle "Jinja Template" as DT } rectangle "Server Side" as S { diff --git a/docs/images/plantUML/package-diagram-logical-view.puml b/docs/images/plantUML/package-diagram-logical-view.puml index 78318bf7..68d41145 100644 --- a/docs/images/plantUML/package-diagram-logical-view.puml +++ b/docs/images/plantUML/package-diagram-logical-view.puml @@ -3,6 +3,42 @@ hide circle package ArTE <> { + package config { + + } + + package core <> { + package "models.py" as M2 { + class Exhibit + } + + package jinja2 { + package users { + + } + } + + package migrations { + + } + + package static { + package css { + + } + package image { + + } + package js { + + } + } + + package view_s { + + } + } + package users <> { package "models.py" as M1 { class Profile @@ -10,13 +46,28 @@ package ArTE <> { class Artwork class Object } - } + package jinja2 { + package users { - package core <> { - package "models.py" as M2 { - class Exhibit + } + } + package media { + package markers { + + } + } + package migrations { + + } + package services { + + } + package statics { + package css { + } } } + } @enduml diff --git a/docs/index.rst b/docs/index.rst index b051a821..d282568a 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -9,7 +9,9 @@ Ajuda .. toctree:: :maxdepth: 2 - animacoes - marcadores - publicando-obras-ingles + animacoes-portugues + marcadores-portugues publicando-obras-portugues + animacoes-ingles + marcadores-ingles + publicando-obras-ingles diff --git a/docs/installation-guide.md b/docs/installation-guide.md new file mode 100644 index 00000000..5f1dbc02 --- /dev/null +++ b/docs/installation-guide.md @@ -0,0 +1,77 @@ +## Revision History + +|Version | description| Author(s) | date | +|--------|------------|-----------|------| +|1.0|Initial version|João Victor Valadão|15/07/2022| +|1.1|Migration problems|Marcelo Araújo dos Santos|18/07/2022| +|1.2|applying the requested changes|João Victor Valadão|22/07/2022| + + +# Installation Guide + +## Introduction + +This document provides an assistance guide with the details (step by step) of the installation of the Jandig and how to fix the most common bugs that may appear when beginners are trying to set up the environment, and start contributing to the Jandig project. So, as mentioned earlier, the purpose of this document is to help new contributors interact with the code, especially new developers that are interested in the application. Make sure to check the prerequisites to run Jandig ARte, because they are tools that you will need in your development workspace that you can access in the [description](http://memelab.com.br/jandig/README.md/) of the repository. + +## Step 1 - Cloning the Repository + +The first step after downloading the prerequisites is clone the repository, so we are going to need the following code: +``` +git clone https://github.com/memeLab/Jandig +cd Jandig +``` +If you are having any problems with the git commands consider checking the Github documentation for problems with the [cloning](https://docs.github.com/pt/repositories/creating-and-managing-repositories/troubleshooting-cloning-errors) or your user [authentication](https://docs.github.com/pt/account-and-profile/setting-up-and-managing-your-personal-account-on-github/managing-email-preferences/setting-your-commit-email-address) + +## Step 2 - Running the Docker + +Jandig uses docker and docker-compose to ensure a consistent development environment and to make the deploy process as painless as possible, so we are going to initialize the docker and then run the docker-compose.yml +``` +docker-compose -f docker/docker-compose.yml up +``` + +### ERROR - Env file not found + +If you receive an error message saying it couldn't find env file, you probably didn't create the .env file yet. So navigate to **Jandig\src\\.envs\\** and create a file named **.env** and copy the content of **.example** and paste it in this file. Then run the above code again. + +### ERROR - System could't find CreateFile +If you are receiving the following error using Windows and Docker, that means your Docker has problems to initiate. Try to initialize the Docker Desktop manually with admin privileges, you can check the Docker Desktop running in your toolbar. If your docker isn't open yet consult the [Docker Desktop](https://docs.microsoft.com/pt-br/windows/wsl/tutorials/wsl-containers) documentation. + +![env-file](./images/installation-guide-createFile.png) + + +### ERROR - Minio +If you encounter a screen as the image below, or some error message related to Minio, it's very likely that the `USE_MINIO` in **.env** file is set to True. To correct this, set it to False (`USE_MINIO=False`). + +![env-file](./images/installation-guide-minio.png) + +### ERROR - Migration + + +Jandig implemented Invoke with some "recipes" to make our lives as developers easier. So in order to solve the backend issue with migrations (image below) try doing the following code to run the migrations on our containers: +``` +cd docker/ +docker-compose run django inv db +# or if you already have a django container running +docker-compose exec django inv db +``` + +![env-file](./images/installation-guide-migration.png) + +If the problem isn't fixed do the following steps, as @GustavoAPS commented [here](https://github.com/memeLab/Jandig/issues/436#issuecomment-1034420328): +1. Run the container. +2. Open a new terminal and list the containers with "docker ps" +3. Enter the project container with "docker exec -it docker_django_1 bash" +4. Inside the container make the migrations with: `python3 src/ARte/manage.py migrate` +5. After the migrations, the problem should be solved. + +## Step 3 - Jandig ARte in the localhost +After these steps you should have Jandig ARte running in your localhost, by default the project uses the **localhost:8000**. To access it you can copy the instruction above and paste in your browser or click [here](http://localhost:8000/) + +![env-file](./images/installation-guide-terminal-expected.png) + +The expected web page is shown below + +![env-file](./images/installation-guide-expected-window.png) + + +Need more help? consult the issue "Problem to run Jandig ARte #436" [here](https://github.com/memeLab/Jandig/issues/436). diff --git a/docs/marcadores-ingles.rst b/docs/marcadores-ingles.rst new file mode 100644 index 00000000..86ec6594 --- /dev/null +++ b/docs/marcadores-ingles.rst @@ -0,0 +1,59 @@ +Hints to produce Tags +============================== + +This document contains tecnical, aesthetics and good practices guidelines to the production of Tags of augmented reality Jandig. + +Borders +------ + +The borders are graphic elements that trigger the recognition of the object associated with each tag. For this reason, one should not cover them and them must always +be +seen completely by the camera. Put the finger over the border or approach the camera too close to the tag will derail the recognition, for example. This feature should +be always taken into account in the production and application of the tags. + +We use by default 20% of width from the border in the Jandig tags, that is in a tag with 10 centimeters of width, we will have a border of 2 centimeters of width. + +The central image of the Tag should not touch the borders. The minimum distance of the imagen to the internal margin of the border should be 2% of the total width of +the Tag. + +Although little texts (exhibition name, app url) can be applied over the border without prejudice in the recognition, is recomended the height of the text never +overtake the total porcentage of the border. + +.. image:: images/MarkerGuide.png + :width: 320px + :align: center + +Symmetry +-------- + +Considering that the visualization of the object depends on the position of the Tag in relation of the camera, we avoid using images with symmetry vertical as well as +horizantal. This rule aim avoid that the recognition system get confused about which orientation it must show the image. + +Colors and gradient +------------------ + +To ensure endless possibilities of application of the Tags, we dont use colors or gradients in the central images. The only color used is 100% black, without +utilization of tones. + +This is not a limitation of the system. It is possible to use any image as a Tag, following other recomedations of this document. + +Printing and about that +----------------- + +Reflexes, including over the borders, can impede that your Tags be recognized as a Tag. So that thay are recognized more easily by the system, they should utilize +opaque paints and materials in them production. + +It's important that the external and internal margins of the border be always well delimited. In case of stickers or background application more dark, guarantee a +white margin around the black border. This reservation of space must be at least 3% of the total marker width. + +Illumination +---------- + +The quality and color of ambient lighting can influence the Tag reading. For a good visualization, prefer a well distributed illumination, that does not generate +reflections and avoid using amber colored lighting. + +Stickers +-------- + +Although Tags can be recognized even in very small formats, we usually produce Jandig Tags stickers with 5 x 5 centimeters. This dimension associates good perfomance +with good readability of all elements, including the text. diff --git a/docs/marcadores.rst b/docs/marcadores-portugues.rst similarity index 100% rename from docs/marcadores.rst rename to docs/marcadores-portugues.rst diff --git a/docs/old hotsite/css/style.css b/docs/old hotsite/css/style.css index a42f5e70..28b125b2 100644 --- a/docs/old hotsite/css/style.css +++ b/docs/old hotsite/css/style.css @@ -41,7 +41,7 @@ h3 { font-size: 18px; } -.button:hover { +button.button:hover { background: #93e3e5; cursor: pointer; } @@ -84,11 +84,13 @@ h3 { } } -} + @media screen and (max-width: 600px) { .footer {padding: 0 0% 0 0%;} .row {padding: 0 0 0 0; } +} + /*.row { display: flex; justify-content: space-between; diff --git a/docs/real_docs/development-server.md b/docs/real_docs/development-server.md index 8397e5e8..bdbed3d3 100644 --- a/docs/real_docs/development-server.md +++ b/docs/real_docs/development-server.md @@ -1,9 +1,9 @@ ## Welcome to the development server guide -Run the local development server using sqlite as database and serving static files with whitenoise (-w flag). The server will be available at http://localhost:8000. +Run the local development server. The server will be available at http://localhost:8000. ```shell -$ inv db collect run -w +$ inv db -p collect run -w ``` With the previous command you can use ARte only on the local machine running the server. Because of security protocols, other devices must access through a ssl (https) connection. To do this, we use [ngrok](https://ngrok.com/download). The ngrok will generate a https url pointing to your local server, so you can access ARte through external devices. diff --git a/etc/nginx.conf b/etc/nginx.conf deleted file mode 100644 index c28b89e5..00000000 --- a/etc/nginx.conf +++ /dev/null @@ -1,33 +0,0 @@ -user nginx; -worker_processes 14; -worker_rlimit_nofile 65535; - -error_log /var/log/nginx/error.log warn; -pid /var/run/nginx.pid; - - -events { - worker_connections 999999; -} - - -http { - include /etc/nginx/mime.types; - default_type application/octet-stream; - - log_format main '$remote_addr - $remote_user [$time_local] "$request" ' - '$status $body_bytes_sent "$http_referer" ' - '"$http_user_agent" "$http_x_forwarded_for"'; - - access_log /var/log/nginx/access.log main; - - sendfile on; - #tcp_nopush on; - - keepalive_timeout 65; - - gzip on; - proxy_cache_path /home/ubuntu/cache levels=1:2 keys_zone=jandigcache:1024m max_size=10g; - - include /etc/nginx/conf.d/*.conf; -} \ No newline at end of file diff --git a/etc/nginx_server.conf b/etc/nginx_server.conf deleted file mode 100644 index 05aaa2f3..00000000 --- a/etc/nginx_server.conf +++ /dev/null @@ -1,69 +0,0 @@ -server { - listen 80; - server_name localhost; - - location / { - root /usr/share/nginx/html; - index index.html index.htm; - } - - error_page 500 502 503 504 /50x.html; - location = /50x.html { - root /usr/share/nginx/html; - } -} - -server { - listen 80 backlog=65536; - server_name jandig.app; - - #location / { - # proxy_set_header Host $http_host; - # proxy_set_header X-Real-IP $remote_addr; - # proxy_pass http://localhost:8000/; - #} - - location / { - return 301 https://$host$request_uri; - } - - location /.well-known/acme-challenge/ { - root /etc/letsencrypt; - } -} - -server { - listen 443 ssl backlog=65536; - server_name jandig.app; - - ssl_certificate /etc/letsencrypt/live/jandig.app/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/jandig.app/privkey.pem; - - include /etc/letsencrypt/options-ssl-nginx.conf; - ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; - - proxy_buffers 32 16k; - proxy_buffer_size 16k; - proxy_busy_buffers_size 64k; - proxy_buffering on; - proxy_cache jandigcache; - - location / { - proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504; - proxy_cache_revalidate on; - proxy_cache_background_update on; - proxy_cache_lock on; - - proxy_set_header Host $http_host; - proxy_set_header X-Real-IP $remote_addr; - proxy_pass http://localhost:8000/; - } - - location /static { - sendfile on; - sendfile_max_chunk 5m; - alias /home/ubuntu/jandig2/docker/volumes/docker_static_prod_files/_data/; - } - charset utf-8; - client_max_body_size 0; -} diff --git a/etc/scripts/compilemessages.py b/etc/scripts/compilemessages.py index 2a2c285b..2af14779 100644 --- a/etc/scripts/compilemessages.py +++ b/etc/scripts/compilemessages.py @@ -24,12 +24,12 @@ def main(): # Build locale list all_locales = [] for basedir in basedirs: - locale_dirs = filter(os.path.isdir, glob.glob('%s/*' % basedir)) + locale_dirs = filter(os.path.isdir, glob.glob(f'{basedir}/*')) all_locales.extend(map(os.path.basename, locale_dirs)) locales = set(all_locales) for basedir in basedirs: - dirs = [os.path.join(basedir, l, 'LC_MESSAGES') for l in locales] + dirs = [os.path.join(basedir, locale, 'LC_MESSAGES') for locale in locales] locations = [] for ldir in dirs: for dirpath, dirnames, filenames in os.walk(ldir): @@ -41,8 +41,8 @@ def compile_messages(locations): """ Locations is a list of tuples: [(directory, file), ...] """ - for i, (dirpath, f) in enumerate(locations): - print('processing file %s in %s\n' % (f, dirpath)) + for _, (dirpath, f) in enumerate(locations): + print(f'processing file {f} in {dirpath}\n') # Program args po_path = os.path.join(dirpath, f) @@ -51,12 +51,12 @@ def compile_messages(locations): args = [program] + program_options + extra_args # Execute command - output, errors, status = popen_wrapper(args) + __, errors, status = popen_wrapper(args) if status: if errors: - msg = "Execution of %s failed: %s" % (program, errors) + msg = f'Execution of {program} failed: {errors}' else: - msg = "Execution of %s failed" % program + msg = f'Execution of {program} failed' raise RuntimeError(msg) @@ -65,13 +65,11 @@ def popen_wrapper(args, os_err_exc_type=RuntimeError): Friendly wrapper around Popen. Return stdout output, stderr output, and OS status code. """ - try: - p = Popen(args, shell=False, stdout=PIPE, stderr=PIPE, close_fds=True) - except OSError as err: - raise os_err_exc_type('Error executing %s' % args[0]) from err - output, errors = p.communicate() - return output, errors, p.returncode + with Popen(args, shell=False, stdout=PIPE, stderr=PIPE, close_fds=True) as p: + output, errors = p.communicate() + return output, errors, p.returncode + raise os_err_exc_type('Error executing') if __name__ == '__main__': - main() \ No newline at end of file + main() diff --git a/etc/ulimits.conf b/etc/ulimits.conf deleted file mode 100644 index 8160f20a..00000000 --- a/etc/ulimits.conf +++ /dev/null @@ -1,64 +0,0 @@ -# /etc/security/limits.d/jandig.conf -# -#Each line describes a limit for a user in the form: -# -# -# -#Where: -# can be: -# - a user name -# - a group name, with @group syntax -# - the wildcard *, for default entry -# - the wildcard %, can be also used with %group syntax, -# for maxlogin limit -# - NOTE: group and wildcard limits are not applied to root. -# To apply a limit to the root user, must be -# the literal username root. -# -# can have the two values: -# - "soft" for enforcing the soft limits -# - "hard" for enforcing hard limits -# -# can be one of the following: -# - core - limits the core file size (KB) -# - data - max data size (KB) -# - fsize - maximum filesize (KB) -# - memlock - max locked-in-memory address space (KB) -# - nofile - max number of open files -# - rss - max resident set size (KB) -# - stack - max stack size (KB) -# - cpu - max CPU time (MIN) -# - nproc - max number of processes -# - as - address space limit (KB) -# - maxlogins - max number of logins for this user -# - maxsyslogins - max number of logins on the system -# - priority - the priority to run user process with -# - locks - max number of file locks the user can hold -# - sigpending - max number of pending signals -# - msgqueue - max memory used by POSIX message queues (bytes) -# - nice - max nice priority allowed to raise to values: [-20, 19] -# - rtprio - max realtime priority -# - chroot - change root to directory (Debian-specific) -# -# -# - -#* soft core 0 -#root hard core 100000 -#* hard rss 10000 -#@student hard nproc 20 -#@faculty soft nproc 20 -#@faculty hard nproc 50 -#ftp hard nproc 0 -#ftp - chroot /ftp -#@student - maxlogins 4 - - -root hard nofile 1000000 -root soft nofile 1000000 -ubuntu hard nofile 1000000 -ubuntu soft nofile 1000000 -nginx hard nofile 1000000 -nginx soft nofile 1000000 - -# End of file \ No newline at end of file diff --git a/locale/en_US/LC_MESSAGES/django.po b/locale/en_US/LC_MESSAGES/django.po index 5a40018d..f20300e3 100644 --- a/locale/en_US/LC_MESSAGES/django.po +++ b/locale/en_US/LC_MESSAGES/django.po @@ -336,8 +336,44 @@ msgstr "" msgid "Your Exhibitions" msgstr "" +#: src/ARte/core/jinja2/core/community.jinja2:7 +msgid "An art experimentation for digital exhibtions in physical spaces" +msgstr "Uma experimentação de arte para exibições digitais em espaços físicos" + +#: src/ARte/core/jinja2/core/community.jinja2:9 +msgid "Cell phones and tablets are used to read markers and thus open “windows” where other art objects can be found. The public is invited to transcend the exhibition space, taking stickers with markers to be pasted in other places, creating Temporary Autonomous Zones (TAZes). The public becomes a co-author, and markers and digital works are reconfigured by spaces." +msgstr "Celulares e tablets são usados para ler marcadores e assim abrir “janelas” onde se encontram outros objetos de arte. O público é convidado a transcender o espaço de exposição, levando adesivos com marcadores para serem colados em outros locais, criando Zonas Autônomas Temporárias (TAZes). O público se torna co-autor, e os marcadores e obras digitais são reconfigurados pelos espaços." + +#: src/ARte/core/jinja2/core/community.jinja2:12 +msgid "Know" +msgstr "Entenda" + +#: src/ARte/core/jinja2/core/community.jinja2:14 +msgid "The Jandig project is an investigation into the intervention of markers for viewing works through augmented reality on urban space. It is a collaborative digital art project that proposes the creation of Temporary Autonomous Zones (TAZes) in each space where it is installed. These TAZes are formed through markers spread across a space by artists and the public – who thus become co-creators of that experience. Users interact with markers, using mobile devices to open real-world windows to view digital creations (Licensed under Creative Commons)." +msgstr "O projeto Jandig é uma investigação a respeito da intervenção de marcadores para visualização de obras por meio de realidade aumentada sobre o espaço urbano. Trata-se de um projeto colaborativo de arte digital que propõe a criação de Zonas Autônomas Temporárias (TAZes) em cada espaço em que é instalado. Essas TAZes são formadas através de marcadores espalhados por um espaço por artistas e pelo público – que assim torna-se co-criador daquela experiência. Os usuários interagem com marcadores, utilizando dispositivos móveis para abrir janelas no mundo real para visualizar criações digitais (cedidas através de licença Creative Commons)." + +#: src/ARte/core/jinja2/core/community.jinja2:18 +msgid "Our goals" +msgstr "Nossos objetivos" + +#: src/ARte/core/jinja2/core/community.jinja2:20 +msgid "The Jandig platform makes exhibitions possible using augmented reality." +msgstr "A plataforma Jandig viabiliza a realização de exposições com o uso de realidade aumentada." + +#: src/ARte/core/jinja2/core/community.jinja2:22 +msgid "Each exhibition is made from the arrangement of several markers, which use adhesives, stencil images, stamps, or others, in different sizes as support. These supports are also delivered to the public that circulate through the space, so that they can make local interference and that it is possible to “viralize” the markers while the exhibition is available, leaving traces and providing interactions between participants/visitors." +msgstr "Cada exposição é feita a partir da disposição de diversos marcadores, que utilizam como suporte adesivos, imagens de stencil, de carimbos, ou outras, em diferentes tamanhos. Esses suportes são também entregues ao público que circulam pelo espaço, de modo que possam fazer interferências locais e que seja possível “viralizar” os marcadores enquanto a exposição estiver disponível, deixando rastros e proporcionando interações entre os participantes/visitantes." + +#: src/ARte/core/jinja2/core/community.jinja2:24 +msgid "To see through the “windows” and see what the fabulous images reveal, the public must point their devices at the markers, so that the Jandig platform viewer can read them and show the content in augmented reality." +msgstr "Para enxergar através das “janelas” e ver as imagens fabulosas o que elas revelam, o público deverá apontar seus dispositivos para os marcadores, para que o visualizador da plataforma Jandig ufaça a leitura dos mesmos e mostre o conteúdo em realidade aumentada." + +#: src/ARte/core/jinja2/core/community.jinja2:26 +msgid "Borders are the graphic elements that trigger the recognition of the object associated with each marker. For this reason, they should not be covered and they should always be seen completely by the camera. Placing your finger on one of the edges or bringing the camera too close to the marker makes recognition impossible, for example. This characteristic must always be taken into account in the production and application of markers." +msgstr "As bordas são os elementos gráficos que engatilham o reconhecimento do objeto associado a cada marcador. Por esse motivo, não se deve cobri-las e elas devem sempre ser vistas completamente pela câmera. Colocar o dedo sobre uma das bordas ou aproximar demais a câmera do marcador inviabiliza o reconhecimento, por exemplo. Essa característica deve sempre ser levada em consideração na produção e aplicação dos marcadores." + #: src/ARte/users/jinja2/users/profile.jinja2:21 -msgid "You have no exhibits :c" +msgid "You have no Exhibitions. :c" msgstr "" #: src/ARte/users/jinja2/users/profile.jinja2:22 @@ -351,7 +387,7 @@ msgid "Your Artworks" msgstr "" #: src/ARte/users/jinja2/users/profile.jinja2:32 -msgid "You have no artworks :c" +msgid "You have no Artworks. :c" msgstr "" #: src/ARte/users/jinja2/users/profile.jinja2:36 @@ -359,7 +395,7 @@ msgid "Your Markers" msgstr "" #: src/ARte/users/jinja2/users/profile.jinja2:43 -msgid "You have no markers :c" +msgid "You have no Markers. :c" msgstr "" #: src/ARte/users/jinja2/users/profile.jinja2:47 @@ -367,7 +403,7 @@ msgid "Your Objects" msgstr "" #: src/ARte/users/jinja2/users/profile.jinja2:54 -msgid "You have no objects :c" +msgid "You have no Objects. :c" msgstr "" #: src/ARte/users/jinja2/users/profile.jinja2:55 diff --git a/locale/pt_BR/LC_MESSAGES/django.po b/locale/pt_BR/LC_MESSAGES/django.po index b866b9f2..52101bf2 100644 --- a/locale/pt_BR/LC_MESSAGES/django.po +++ b/locale/pt_BR/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-02-17 21:09+0000\n" +"POT-Creation-Date: 2024-07-06 15:42+0000\n" "PO-Revision-Date: 2020-02-17 18:13-0300\n" "Last-Translator: \n" "Language-Team: \n" @@ -18,312 +18,337 @@ msgstr "" "Plural-Forms: nplurals=2; plural=(n > 1);\n" "X-Generator: Poedit 2.3\n" -#: src/ARte/config/settings.py:146 +#: src/config/settings.py:179 msgid "English" msgstr "Inglês" -#: src/ARte/config/settings.py:147 +#: src/config/settings.py:180 msgid "Brazilian Portuguese" msgstr "Português do Brasil" -#: src/ARte/users/forms.py:24 src/ARte/users/forms.py:85 +#: src/users/forms.py:32 src/users/forms.py:93 msgid "Your e-mail address" msgstr "Seu endereço de e-mail" -#: src/ARte/users/forms.py:29 src/ARte/users/forms.py:89 +#: src/users/forms.py:37 src/users/forms.py:97 msgid "Your username" msgstr "Seu nome de usuário" -#: src/ARte/users/forms.py:35 +#: src/users/forms.py:43 msgid "email" msgstr "email" -#: src/ARte/users/forms.py:36 +#: src/users/forms.py:44 msgid "chosen username" msgstr "nome de usuário escolhido" -#: src/ARte/users/forms.py:37 src/ARte/users/forms.py:123 +#: src/users/forms.py:45 src/users/forms.py:129 msgid "password" msgstr "senha" -#: src/ARte/users/forms.py:38 +#: src/users/forms.py:46 msgid "confirm password" msgstr "confirmar senha" -#: src/ARte/users/forms.py:48 +#: src/users/forms.py:56 msgid "E-mail taken" msgstr "E-mail já existente" -#: src/ARte/users/forms.py:57 +#: src/users/forms.py:64 msgid "Old Password" msgstr "Senha antiga" -#: src/ARte/users/forms.py:58 +#: src/users/forms.py:65 msgid "New Password" msgstr "Nova senha" -#: src/ARte/users/forms.py:59 +#: src/users/forms.py:66 msgid "New Password Again" msgstr "Nova senha novamente" -#: src/ARte/users/forms.py:78 +#: src/users/forms.py:86 msgid "E-mail" msgstr "E-mail" -#: src/ARte/users/forms.py:79 +#: src/users/forms.py:87 msgid "Username" msgstr "Usuário" -#: src/ARte/users/forms.py:80 src/ARte/users/forms.py:99 +#: src/users/forms.py:88 src/users/forms.py:104 msgid "Personal Bio / Description" msgstr "Biografia / Descrição" -#: src/ARte/users/forms.py:81 src/ARte/users/forms.py:103 +#: src/users/forms.py:89 src/users/forms.py:108 msgid "Personal Website" msgstr "Site pessoal" -#: src/ARte/users/forms.py:109 +#: src/users/forms.py:114 msgid "Username already in use" msgstr "Nome de usuário já em uso" -#: src/ARte/users/forms.py:115 +#: src/users/forms.py:120 msgid "Email address must be unique" msgstr "O endereço de e-mail deve ser único" -#: src/ARte/users/forms.py:122 +#: src/users/forms.py:128 msgid "username / email" msgstr "nome de usuário / email" -#: src/ARte/users/forms.py:130 src/ARte/users/forms.py:136 +#: src/users/forms.py:135 src/users/forms.py:141 msgid "Username/email not found" msgstr "Nome de usuário / email não encontrado" -#: src/ARte/users/forms.py:152 -msgid "Wrong password" +#: src/users/forms.py:167 +#, fuzzy +msgid "Wrong password!" msgstr "Senha errada" -#: src/ARte/users/forms.py:169 src/ARte/users/forms.py:171 -#: src/ARte/users/forms.py:187 +#: src/users/forms.py:185 src/users/forms.py:222 msgid "browse file" msgstr "procurar arquivo" -#: src/ARte/users/forms.py:173 src/ARte/users/forms.py:189 -#: src/ARte/users/forms.py:215 src/ARte/users/forms.py:216 +#: src/users/forms.py:187 src/users/forms.py:224 src/users/forms.py:257 +#: src/users/forms.py:258 msgid "declare different author name" msgstr "declarar outro autor" -#: src/ARte/users/forms.py:174 +#: src/users/forms.py:188 msgid "Marker's title" msgstr "Título do Marcador" -#: src/ARte/users/forms.py:193 +#: src/users/forms.py:228 msgid "Object's title" msgstr "Título do Objeto" -#: src/ARte/users/forms.py:217 +#: src/users/forms.py:259 msgid "Artwork title" msgstr "Título da Obra" -#: src/ARte/users/forms.py:218 +#: src/users/forms.py:260 msgid "Artwork description" msgstr "Descrição da Obra" -#: src/ARte/users/forms.py:239 +#: src/users/forms.py:274 +#, fuzzy +msgid "Url can't contain spaces or special characters" +msgstr "Urls não podem conter espaços ou caracteres especiais (i.e: .:, /)" + +#: src/users/forms.py:280 msgid "Exhibit Title" msgstr "Título da Exposição" -#: src/ARte/users/forms.py:240 -msgid "Exhibit URL" -msgstr "URL da exposição" - -#: src/ARte/users/forms.py:251 +#: src/users/forms.py:281 msgid "Complete with your Exhibit URL here" msgstr "Complete sua URL de exibição aqui" -#: src/ARte/core/jinja2/core/base.jinja2:44 +#: src/blog/jinja2/blog/category.jinja2:8 src/blog/jinja2/blog/detail.jinja2:5 #, fuzzy -msgid "Please override the \"content\" block of your template!" -msgstr "Por favor sobrescreva o bloco \"conteúdo\" de seu modelo!" +msgid "< Back to Blog" +msgstr "Página inicial" -#: src/ARte/core/jinja2/core/collection.jinja2:13 -#, fuzzy -msgid "Jandig Exhibitions" -msgstr "Exposições Jandig" +#: src/blog/jinja2/blog/clipping.jinja2:12 +#: src/core/jinja2/core/useful_links.jinja2:15 +msgid "Clipping" +msgstr "Clipping" -#: src/ARte/core/jinja2/core/collection.jinja2:18 -msgid "All Exhibits" -msgstr "Todas Exposições" +#: src/blog/jinja2/blog/clipping.jinja2:14 +msgid "The content below is in the original languages." +msgstr "Os conteúdos abaixo estão em suas linguas originais" -#: src/ARte/core/jinja2/core/collection.jinja2:23 -#, fuzzy -msgid "Jandig Artworks" -msgstr "Obras Jandig" +#: src/blog/jinja2/blog/clipping.jinja2:22 +msgid "View as PDF" +msgstr "Ver como PDF" -#: src/ARte/core/jinja2/core/collection.jinja2:28 -msgid "All Artworks" -msgstr "Todas as Obras" +#: src/blog/jinja2/blog/clipping.jinja2:24 +msgid "View as JPG" +msgstr "Ver como JPG" -#: src/ARte/core/jinja2/core/collection.jinja2:33 -#, fuzzy -msgid "Jandig Markers" -msgstr "Marcadores Jandig" +#: src/blog/jinja2/blog/clipping.jinja2:26 +msgid "View File" +msgstr "Ver Arquivo" -#: src/ARte/core/jinja2/core/collection.jinja2:38 -msgid "All Markers" -msgstr "Todos os Marcadores" +#: src/blog/jinja2/blog/detail.jinja2:13 +msgid "Categories:" +msgstr "Categorias" -#: src/ARte/core/jinja2/core/collection.jinja2:43 +#: src/blog/jinja2/blog/detail.jinja2:35 #, fuzzy -msgid "Jandig Objects" -msgstr "Objetos Jandig" +msgid "Back to Top" +msgstr "Página inicial" -#: src/ARte/core/jinja2/core/collection.jinja2:48 -msgid "All Objects" -msgstr "Todos os Objetos" +#: src/blog/jinja2/blog/index.jinja2:12 +#: src/core/jinja2/core/useful_links.jinja2:12 +msgid "Memories" +msgstr "Memórias" -#: src/ARte/core/jinja2/core/exhibit_detail.jinja2:11 -msgid "See this exhibition" -msgstr "Ver essa exposição" +#: src/blog/jinja2/blog/post_preview.jinja2:4 +msgid "Read More" +msgstr "Ler Mais" + +#: src/blog/jinja2/blog/post_preview.jinja2:8 +msgid "See more" +msgstr "Ver mais" -#: src/ARte/core/jinja2/core/exhibit_detail.jinja2:12 -#: src/ARte/users/jinja2/users/components/item-list.jinja2:60 +#: src/core/jinja2/core/base.jinja2:44 +#, fuzzy +msgid "Please override the \"content\" block of your template!" +msgstr "Por favor sobrescreva o bloco \"conteúdo\" de seu modelo!" + +#: src/core/jinja2/core/collection.jinja2:50 +msgid "We found no content on your Collection, try uploading an object." +msgstr "Não encontramos nenhum objeto por aqui :( Tente fazer upload." + +#: src/core/jinja2/core/exhibit_detail.jinja2:9 +#: src/users/jinja2/users/components/item-list.jinja2:75 +#: src/users/jinja2/users/components/moderator-item-list.jinja2:31 msgid "Created by " msgstr "Criado por " -#: src/ARte/core/jinja2/core/exhibit_detail.jinja2:15 +#: src/core/jinja2/core/exhibit_detail.jinja2:10 +#: src/users/jinja2/users/components/moderator-item-list.jinja2:34 +msgid "See this exhibition" +msgstr "Ver essa exposição" + +#: src/core/jinja2/core/exhibit_detail.jinja2:12 msgid "Exhibition Artworks" msgstr "Obras dessa Exposição" -#: src/ARte/core/jinja2/core/exhibit_select.jinja2:14 +#: src/core/jinja2/core/exhibit_select.jinja2:16 msgid "Choose the Exhibition you're seeing" msgstr "Escolha a Exposição que está vendo" -#: src/ARte/core/jinja2/core/exhibit_select.jinja2:23 -#: src/ARte/users/jinja2/users/login.jinja2:37 -#: src/ARte/users/jinja2/users/password-change.jinja2:26 -#: src/ARte/users/jinja2/users/profile-edit.jinja2:31 -#: src/ARte/users/jinja2/users/recover-edit-password.jinja2:26 -#: src/ARte/users/jinja2/users/recover-password-code.jinja2:26 -#: src/ARte/users/jinja2/users/recover-password.jinja2:26 -#: src/ARte/users/jinja2/users/signup.jinja2:32 -#: src/ARte/users/jinja2/users/upload.jinja2:93 +#: src/core/jinja2/core/exhibit_select.jinja2:28 +#: src/users/jinja2/users/edit-marker.jinja2:26 +#: src/users/jinja2/users/edit-object.jinja2:63 +#: src/users/jinja2/users/login.jinja2:40 +#: src/users/jinja2/users/password-change.jinja2:26 +#: src/users/jinja2/users/profile-edit.jinja2:31 +#: src/users/jinja2/users/recover-edit-password.jinja2:26 +#: src/users/jinja2/users/recover-password-code.jinja2:26 +#: src/users/jinja2/users/recover-password.jinja2:26 +#: src/users/jinja2/users/signup.jinja2:32 +#: src/users/jinja2/users/upload-marker.jinja2:52 +#: src/users/jinja2/users/upload-object.jinja2:88 +#: src/users/jinja2/users/upload.jinja2:107 msgid "Submit" msgstr "Enviar" -#: src/ARte/core/jinja2/core/footer.jinja2:3 +#: src/core/jinja2/core/footer.jinja2:3 msgid "About Us" msgstr "Sobre Nós" -#: src/ARte/core/jinja2/core/footer.jinja2:4 -#: src/ARte/core/jinja2/core/footer.jinja2:9 -msgid "Jandig on Github" -msgstr "Jandig no GitHub" - -#: src/ARte/core/jinja2/core/footer.jinja2:6 +#: src/core/jinja2/core/footer.jinja2:5 msgid "Jandig on Instagram" msgstr "Jandig no Instagram" -#: src/ARte/core/jinja2/core/footer.jinja2:7 +#: src/core/jinja2/core/footer.jinja2:6 msgid "Jandig on Twitter" msgstr "Jandig no Twitter" -#: src/ARte/core/jinja2/core/footer.jinja2:8 +#: src/core/jinja2/core/footer.jinja2:7 msgid "Jandig on Facebook" msgstr "Jandig no Facebook" -#: src/ARte/core/jinja2/core/footer.jinja2:10 +#: src/core/jinja2/core/footer.jinja2:8 +msgid "Jandig on Github" +msgstr "Jandig no GitHub" + +#: src/core/jinja2/core/footer.jinja2:9 msgid "Jandig on Telegram" msgstr "Jandig no Telegram" -#: src/ARte/core/jinja2/core/footer.jinja2:14 +#: src/core/jinja2/core/footer.jinja2:13 msgid "Content under CC BY-SA 4.0 unless otherwise noted." msgstr "" "Conteúdo sob licenciamento CC BY-SA 4.0, a não ser quando explicitado o " "contrário." -#: src/ARte/core/jinja2/core/header.jinja2:21 +#: src/core/jinja2/core/header.jinja2:19 msgid "Welcome, " msgstr "Olá, " -#: src/ARte/core/jinja2/core/header.jinja2:28 -#: src/ARte/users/jinja2/users/login.jinja2:21 -#: src/ARte/users/jinja2/users/signup.jinja2:14 +#: src/core/jinja2/core/header.jinja2:26 src/users/jinja2/users/login.jinja2:21 +#: src/users/jinja2/users/signup.jinja2:14 msgid "Sign up" msgstr "Cadastro" -#: src/ARte/core/jinja2/core/header.jinja2:31 -#: src/ARte/users/jinja2/users/signup.jinja2:15 +#: src/core/jinja2/core/header.jinja2:29 +#: src/users/jinja2/users/signup.jinja2:15 msgid "Log in" msgstr "Entrar" -#: src/ARte/core/jinja2/core/home.jinja2:37 +#: src/core/jinja2/core/home.jinja2:41 msgid "Welcome to Jandig" msgstr "Bem vindo ao Jandig" -#: src/ARte/core/jinja2/core/home.jinja2:38 +#: src/core/jinja2/core/home.jinja2:42 #, fuzzy msgid "An Open Source Augmented Reality art community." msgstr "Uma comunidade open source de arte em Realidade Aumentada." -#: src/ARte/core/jinja2/core/home.jinja2:39 +#: src/core/jinja2/core/home.jinja2:43 msgid "To see the Artworks you need to grant camera access to the app." msgstr "Para ver as Obras você precisa dar acesso a câmera no aplicativo." -#: src/ARte/core/jinja2/core/home.jinja2:41 +#: src/core/jinja2/core/home.jinja2:45 msgid "Go to camera" msgstr "Ir para câmera" -#: src/ARte/core/jinja2/core/language-select-modal.jinja2:3 +#: src/core/jinja2/core/language-select-modal.jinja2:3 msgid "Select your language" msgstr "Selecione seu idioma" -#: src/ARte/core/jinja2/core/language-select-modal.jinja2:16 +#: src/core/jinja2/core/language-select-modal.jinja2:16 msgid "Ok" msgstr "Ok" -#: src/ARte/core/jinja2/core/ui.jinja2:4 +#: src/core/jinja2/core/ui.jinja2:4 msgid "Change exhibition" msgstr "Alterar exposição" -#: src/ARte/core/jinja2/core/ui.jinja2:5 +#: src/core/jinja2/core/ui.jinja2:5 msgid "Back to home" msgstr "Página inicial" -#: src/ARte/core/jinja2/core/useful_links.jinja2:5 +#: src/core/jinja2/core/useful_links.jinja2:5 msgid "AR Viewer" msgstr "Visualizador RA" -#: src/ARte/core/jinja2/core/useful_links.jinja2:9 +#: src/core/jinja2/core/useful_links.jinja2:9 msgid "Collection" msgstr "Coleção" -#: src/ARte/core/jinja2/core/useful_links.jinja2:12 -msgid "My Stuff" -msgstr "Minhas Coisas" +#: src/core/jinja2/core/useful_links.jinja2:18 +msgid "My Creations" +msgstr "Minhas Criações" -#: src/ARte/core/jinja2/core/useful_links.jinja2:15 +#: src/core/jinja2/core/useful_links.jinja2:21 msgid "Help" msgstr "Ajuda" -#: src/ARte/users/jinja2/users/artwork-create.jinja2:11 -#: src/ARte/users/jinja2/users/components/createbox.jinja2:13 +#: src/users/jinja2/users/artwork.jinja2:11 +msgid "Edit Jandig Artwork" +msgstr "Editar Obra Jandig" + +#: src/users/jinja2/users/artwork.jinja2:11 +#: src/users/jinja2/users/components/createbox.jinja2:13 msgid "Create Jandig Artwork" msgstr "Criar Obra Jandig" -#: src/ARte/users/jinja2/users/artwork-create.jinja2:13 +#: src/users/jinja2/users/artwork.jinja2:13 msgid "Select Marker" msgstr "Escolher Marcador" -#: src/ARte/users/jinja2/users/artwork-create.jinja2:16 +#: src/users/jinja2/users/artwork.jinja2:16 msgid "Select Object" msgstr "Escolher Objeto" -#: src/ARte/users/jinja2/users/artwork-create.jinja2:22 +#: src/users/jinja2/users/artwork.jinja2:22 msgid "Select Marker (1/3)" msgstr "Escolher Marcador (1/3)" -#: src/ARte/users/jinja2/users/artwork-create.jinja2:48 -#: src/ARte/users/jinja2/users/artwork-create.jinja2:83 +#: src/users/jinja2/users/artwork.jinja2:36 +#: src/users/jinja2/users/artwork.jinja2:58 msgid "" "I agree to share this content under CC BY-SA 4.0 and I'm aware that, once " "uploaded, I cannot remove it. " @@ -331,206 +356,291 @@ msgstr "" "Concordo em publicar esse conteúdo sob CC BY-SA 4.0 e entendo que, uma vez " "publicado, não posso mais removê-lo. " -#: src/ARte/users/jinja2/users/artwork-create.jinja2:53 -#: src/ARte/users/jinja2/users/artwork-create.jinja2:87 -#: src/ARte/users/jinja2/users/exhibit-create.jinja2:33 +#: src/users/jinja2/users/artwork.jinja2:41 +#: src/users/jinja2/users/artwork.jinja2:62 +#: src/users/jinja2/users/exhibit-create.jinja2:44 +#: src/users/jinja2/users/exhibit-edit.jinja2:34 msgid "Next" msgstr "Próximo" -#: src/ARte/users/jinja2/users/exhibit-create.jinja2:43 -msgid "Your exhibit URL will look like this" -msgstr "Sua URL de exibição vai se parecer com isso." - -#: src/ARte/users/jinja2/users/artwork-create.jinja2:57 +#: src/users/jinja2/users/artwork.jinja2:45 msgid "Select Object (2/3)" msgstr "Selecione Objeto (2/3)" -#: src/ARte/users/jinja2/users/artwork-create.jinja2:91 +#: src/users/jinja2/users/artwork.jinja2:66 msgid "About your artwork (3/3)" msgstr "Sobre sua obra (3/3)" -#: src/ARte/users/jinja2/users/artwork-create.jinja2:100 +#: src/users/jinja2/users/artwork.jinja2:75 +msgid "Edit Artwork" +msgstr "Editar Obra" + +#: src/users/jinja2/users/artwork.jinja2:75 msgid "Publish artwork" msgstr "Publicar obra" -#: src/ARte/users/jinja2/users/exhibit-create.jinja2:11 -msgid "Create Jandig Exhibition" -msgstr "Criar Exposição Jandig" +#: src/users/jinja2/users/edit-marker.jinja2:9 +#: src/users/jinja2/users/edit-object.jinja2:9 +#, fuzzy +msgid "Edit object" +msgstr "objeto" -#: src/ARte/users/jinja2/users/artwork-edit.jinja2:11 -msgid "Edit Jandig Artwork" -msgstr "Editar Obra Jandig" +#: src/users/jinja2/users/edit-marker.jinja2:15 +#, fuzzy +msgid "Edit Marker's title" +msgstr "Título do Marcador" -#: src/ARte/users/jinja2/users/artwork-edit.jinja2:75 -msgid "Edit Artwork" -msgstr "Editar Obra" +#: src/users/jinja2/users/edit-marker.jinja2:19 +msgid "" +"You can't edit the marker patt and the png for now, but hopefully soon " +"enough it will be possible!" +msgstr "" +"Você não pode editar o arquivo patt ou png de um marcador agora, mas devemos " +"atualizar isto assim que possível." -#: src/ARte/users/jinja2/users/exhibit-edit.jinja2:12 -msgid "Edit Jandig Exhibition" -msgstr "Editar Exposição Jandig" +#: src/users/jinja2/users/edit-object.jinja2:15 +#, fuzzy +msgid "Edit Object's title" +msgstr "Título do Objeto" -#: src/ARte/users/jinja2/users/exhibit-edit.jinja2:49 -msgid "Edit Exhibit" -msgstr "Editar Exposição" +#: src/users/jinja2/users/edit-object.jinja2:19 +#: src/users/jinja2/users/upload-object.jinja2:27 +#: src/users/jinja2/users/upload.jinja2:29 +msgid "Choose Object" +msgstr "Escolher Objeto" -#: src/ARte/users/jinja2/users/exhibit-create.jinja2:13 +#: src/users/jinja2/users/edit-object.jinja2:26 +#: src/users/jinja2/users/upload-object.jinja2:36 +#: src/users/jinja2/users/upload.jinja2:47 +msgid "Adjust scale" +msgstr "Ajustar escala" + +#: src/users/jinja2/users/edit-object.jinja2:29 +#: src/users/jinja2/users/upload-object.jinja2:39 +#: src/users/jinja2/users/upload.jinja2:50 +msgid "" +"Scale should be adjusted relative to Marker size on the screen. A scale of 2 " +"will render an Object twice the size of the Marker." +msgstr "" +"A escala deve ser ajustada relativa ao tamanho do Marcador na tela. Uma " +"escala de 2 irá renderizar um Objeto duas vezes o tamanho do Marcador." + +#: src/users/jinja2/users/edit-object.jinja2:36 +#: src/users/jinja2/users/upload-object.jinja2:46 +#: src/users/jinja2/users/upload.jinja2:57 +msgid "Adjust position" +msgstr "Ajustar posição" + +#: src/users/jinja2/users/exhibit-create.jinja2:12 +msgid "Create Jandig Exhibition" +msgstr "Criar Exposição Jandig" + +#: src/users/jinja2/users/exhibit-create.jinja2:14 +#: src/users/jinja2/users/exhibit-edit.jinja2:14 msgid "Select Artworks" msgstr "Escolher Obras" -#: src/ARte/users/jinja2/users/exhibit-create.jinja2:20 +#: src/users/jinja2/users/exhibit-create.jinja2:21 +#: src/users/jinja2/users/exhibit-edit.jinja2:21 msgid "Select Artworks (1/2)" msgstr "Escolher Obras (1/2)" -#: src/ARte/users/jinja2/users/exhibit-create.jinja2:22 +#: src/users/jinja2/users/exhibit-create.jinja2:24 +#: src/users/jinja2/users/exhibit-edit.jinja2:23 msgid "Choose from your repository" msgstr "Escolha de seu repositório" -#: src/ARte/users/jinja2/users/exhibit-create.jinja2:37 +#: src/users/jinja2/users/exhibit-create.jinja2:28 +#: src/users/jinja2/users/profile.jinja2:43 +msgid "You have no Artworks. :c" +msgstr "Você não tem Obras. :c" + +#: src/users/jinja2/users/exhibit-create.jinja2:30 +#: src/users/jinja2/users/profile.jinja2:29 +#: src/users/jinja2/users/profile.jinja2:44 +msgid "Create one" +msgstr "Criar" + +#: src/users/jinja2/users/exhibit-create.jinja2:48 +#: src/users/jinja2/users/exhibit-edit.jinja2:38 msgid "Exhibit Information (2/2)" msgstr "Informações da Exposição (2/2)" -#: src/ARte/users/jinja2/users/exhibit-create.jinja2:46 +#: src/users/jinja2/users/exhibit-create.jinja2:53 +#: src/users/jinja2/users/exhibit-edit.jinja2:43 +msgid "Your exhibit URL will look like this" +msgstr "Sua URL de exibição vai se parecer com isso." + +#: src/users/jinja2/users/exhibit-create.jinja2:59 msgid "Publish Exhibit" msgstr "Publicar Exposição" -#: src/ARte/users/jinja2/users/exhibit-create.jinja2:82 +#: src/users/jinja2/users/exhibit-create.jinja2:95 +#, fuzzy msgid "Urls can't contain spaces or special characters (i.e: .:, /)" msgstr "Urls não podem conter espaços ou caracteres especiais (i.e: .:, /)" -#: src/ARte/users/jinja2/users/invalid-recovering-email.jinja2:14 +#: src/users/jinja2/users/exhibit-edit.jinja2:12 +msgid "Edit Jandig Exhibition" +msgstr "Editar Exposição Jandig" + +#: src/users/jinja2/users/exhibit-edit.jinja2:49 +msgid "Edit Exhibit" +msgstr "Editar Exposição" + +#: src/users/jinja2/users/invalid-recovering-email.jinja2:14 msgid "There is no user with this e-mail/username." msgstr "Não há usuário com esse e-mail/nome." -#: src/ARte/users/jinja2/users/invalid-recovering-email.jinja2:18 -#: src/ARte/users/jinja2/users/wrong-verification-code.jinja2:18 +#: src/users/jinja2/users/invalid-recovering-email.jinja2:18 +#: src/users/jinja2/users/wrong-verification-code.jinja2:18 msgid "Click here to return" msgstr "Clique aqui para voltar" -#: src/ARte/users/jinja2/users/login.jinja2:20 +#: src/users/jinja2/users/login.jinja2:20 msgid "Login to continue" msgstr "Faça login para continuar" -#: src/ARte/users/jinja2/users/login.jinja2:40 +#: src/users/jinja2/users/login.jinja2:36 +#: src/users/jinja2/users/signup.jinja2:29 +msgid "Remember me" +msgstr "Lembre-se de mim" + +#: src/users/jinja2/users/login.jinja2:43 msgid "Recover password" msgstr "Recuperar senha" -#: src/ARte/users/jinja2/users/password-change.jinja2:14 +#: src/users/jinja2/users/moderator-page.jinja2:12 +#, fuzzy +msgid "Jandig Exhibits" +msgstr "Exposições Jandig" + +#: src/users/jinja2/users/moderator-page.jinja2:18 +#, fuzzy +msgid "Jandig Artworks" +msgstr "Obras Jandig" + +#: src/users/jinja2/users/moderator-page.jinja2:24 +#, fuzzy +msgid "Jandig Markers" +msgstr "Marcadores Jandig" + +#: src/users/jinja2/users/moderator-page.jinja2:30 +#, fuzzy +msgid "Jandig Objects" +msgstr "Objetos Jandig" + +#: src/users/jinja2/users/password-change.jinja2:14 msgid "Type your new password." msgstr "Digite sua nova senha." -#: src/ARte/users/jinja2/users/profile-edit.jinja2:7 +#: src/users/jinja2/users/permission-denied.jinja2:4 +msgid "" +"You dont have permission to be here, please click on the button to go back." +msgstr "" + +#: src/users/jinja2/users/permission-denied.jinja2:5 +msgid "Take me back to where I belong!" +msgstr "" + +#: src/users/jinja2/users/components/userbox.jinja2:23 +#: src/users/jinja2/users/profile-edit.jinja2:7 msgid "Log out" msgstr "Sair" -#: src/ARte/users/jinja2/users/profile-edit.jinja2:8 +#: src/users/jinja2/users/profile-edit.jinja2:8 msgid "Remove account" msgstr "Remover conta" -#: src/ARte/users/jinja2/users/profile-edit.jinja2:19 +#: src/users/jinja2/users/profile-edit.jinja2:19 msgid "Change Password" msgstr "Trocar Senha" -#: src/ARte/users/jinja2/users/profile.jinja2:16 +#: src/users/jinja2/users/profile.jinja2:18 msgid "Your Exhibitions" msgstr "Suas Exposições" -#: src/ARte/users/jinja2/users/profile.jinja2:23 -msgid "You have no Exhibitions. :(" -msgstr "Você não tem Exposições. :(" +#: src/users/jinja2/users/profile.jinja2:20 +msgid "Exhibitions" +msgstr "Exposições" -#: src/ARte/users/jinja2/users/profile.jinja2:24 -#: src/ARte/users/jinja2/users/profile.jinja2:35 -msgid "Create one" -msgstr "Criar" +#: src/users/jinja2/users/profile.jinja2:28 +msgid "You have no Exhibitions. :c" +msgstr "Você não tem Exposições. :c" -#: src/ARte/users/jinja2/users/profile.jinja2:27 +#: src/users/jinja2/users/profile.jinja2:33 msgid "Your Artworks" msgstr "Suas Obras" -#: src/ARte/users/jinja2/users/profile.jinja2:34 -msgid "You have no Artworks. :(" -msgstr "Você não tem Obras. :(" +#: src/users/jinja2/users/profile.jinja2:35 +msgid "Artworks" +msgstr "Obras" -#: src/ARte/users/jinja2/users/profile.jinja2:38 +#: src/users/jinja2/users/profile.jinja2:48 msgid "Your Markers" msgstr "Seus Marcadores" -#: src/ARte/users/jinja2/users/profile.jinja2:45 -msgid "You have no markers. :(" -msgstr "Você não tem marcadores. :(" +#: src/users/jinja2/users/profile.jinja2:50 +msgid "Markers" +msgstr "Marcadores" -#: src/ARte/users/jinja2/users/profile.jinja2:46 -#: src/ARte/users/jinja2/users/profile.jinja2:57 +#: src/users/jinja2/users/profile.jinja2:58 +msgid "You have no Markers. :c" +msgstr "Você não tem Marcadores. :c" + +#: src/users/jinja2/users/profile.jinja2:59 +#: src/users/jinja2/users/profile.jinja2:74 msgid "Upload one" msgstr "Enviar" -#: src/ARte/users/jinja2/users/profile.jinja2:49 +#: src/users/jinja2/users/profile.jinja2:63 msgid "Your Objects" msgstr "Seus Objetos" -#: src/ARte/users/jinja2/users/profile.jinja2:56 -msgid "You have no objects. :(" -msgstr "Você não tem objetos. :(" +#: src/users/jinja2/users/profile.jinja2:65 +msgid "Objects" +msgstr "Objetos" + +#: src/users/jinja2/users/profile.jinja2:73 +msgid "You have no Objects. :c" +msgstr "Você não tem Objetos. :c" -#: src/ARte/users/jinja2/users/recover-edit-password.jinja2:14 +#: src/users/jinja2/users/recover-edit-password.jinja2:14 msgid "Choose a new password and repeat it." msgstr "Escolha uma nova senha e repita." -#: src/ARte/users/jinja2/users/recover-password-code.jinja2:14 +#: src/users/jinja2/users/recover-password-code.jinja2:14 msgid "Insert the code you have received on e-mail." msgstr "Digite o código que você recebeu em seu e-mail." -#: src/ARte/users/jinja2/users/recover-password.jinja2:14 +#: src/users/jinja2/users/recover-password.jinja2:14 msgid "Type your username or e-mail" msgstr "Digite seu nome de usuário ou email" -#: src/ARte/users/jinja2/users/signup.jinja2:29 -msgid "Remember me" -msgstr "Lembre-se de mim" - -#: src/ARte/users/jinja2/users/components/createbox.jinja2:5 -#: src/ARte/users/jinja2/users/upload.jinja2:10 +#: src/users/jinja2/users/components/createbox.jinja2:5 +#: src/users/jinja2/users/upload-marker.jinja2:11 +#: src/users/jinja2/users/upload.jinja2:10 msgid "Upload Marker" msgstr "Enviar Marcador" -#: src/ARte/users/jinja2/users/components/createbox.jinja2:8 -#: src/ARte/users/jinja2/users/upload.jinja2:12 -msgid "Upload Object" -msgstr "Enviar Objeto" - -#: src/ARte/users/jinja2/users/upload.jinja2:20 -msgid "Choose Object" -msgstr "Escolher Objeto" +#: src/users/jinja2/users/upload-marker.jinja2:19 +#: src/users/jinja2/users/upload.jinja2:36 +msgid "Choose Marker's title" +msgstr "Escolha o título do seu Marcador" -#: src/ARte/users/jinja2/users/upload.jinja2:26 +#: src/users/jinja2/users/upload-marker.jinja2:23 +#: src/users/jinja2/users/upload.jinja2:40 msgid "Choose Marker image" msgstr "Selecione uma imagem de Marcador" -#: src/ARte/users/jinja2/users/upload.jinja2:40 -msgid "Adjust scale" -msgstr "Ajustar escala" - -#: src/ARte/users/jinja2/users/upload.jinja2:45 -msgid "Adjust position" -msgstr "Ajustar posição" - -#: src/ARte/users/jinja2/users/upload.jinja2:53 -msgid "Adjust rotation" -msgstr "Ajustar rotação" - -#: src/ARte/users/jinja2/users/upload.jinja2:65 -msgid "Choose .patt file" -msgstr "Selecione um arquivo .patt" - -#: src/ARte/users/jinja2/users/upload.jinja2:77 +#: src/users/jinja2/users/upload-marker.jinja2:35 +#: src/users/jinja2/users/upload.jinja2:89 msgid "I'm this Marker author" msgstr "Sou autor/a desse Marcador" -#: src/ARte/users/jinja2/users/upload.jinja2:79 -msgid "I'm this Object author" -msgstr "Sou autor/a desse Objeto" - -#: src/ARte/users/jinja2/users/upload.jinja2:90 +#: src/users/jinja2/users/upload-marker.jinja2:48 +#: src/users/jinja2/users/upload-object.jinja2:84 +#: src/users/jinja2/users/upload.jinja2:103 msgid "" "I agree to have this content under CC BY-SA 4.0 and I'm " @@ -540,127 +650,177 @@ msgstr "" "creativecommons.org/licenses/by-sa/4.0/legalcode\">CC BY-SA 4.0 e estou " "ciente que não posso removê-lo depois que outras pessoas o estiverem usando." -#: src/ARte/users/jinja2/users/wrong-verification-code.jinja2:14 +#: src/users/jinja2/users/components/createbox.jinja2:8 +#: src/users/jinja2/users/upload-object.jinja2:10 +#: src/users/jinja2/users/upload.jinja2:12 +msgid "Upload Object" +msgstr "Enviar Objeto" + +#: src/users/jinja2/users/upload-object.jinja2:12 +#: src/users/jinja2/users/upload.jinja2:14 +#, fuzzy +msgid "Edit_object" +msgstr "objeto" + +#: src/users/jinja2/users/upload-object.jinja2:20 +#: src/users/jinja2/users/upload.jinja2:22 +msgid "Choose Object's title" +msgstr "Escolha o título do Objeto" + +#: src/users/jinja2/users/upload-object.jinja2:72 +#: src/users/jinja2/users/upload.jinja2:92 +msgid "I'm this Object author" +msgstr "Sou autor/a desse Objeto" + +#: src/users/jinja2/users/upload.jinja2:80 +msgid "Choose .patt file" +msgstr "Selecione um arquivo .patt" + +#: src/users/jinja2/users/wrong-verification-code.jinja2:14 msgid "Your verification code is invalid." msgstr "Seu código de verificação é inválido." -#: src/ARte/users/jinja2/users/components/createbox.jinja2:2 +#: src/users/jinja2/users/components/createbox.jinja2:2 msgid "Come on! Create new content now!" msgstr "Vem cá! Crie conteúdo agora!" -#: src/ARte/users/jinja2/users/components/createbox.jinja2:16 +#: src/users/jinja2/users/components/createbox.jinja2:16 msgid "Create Exhibition" msgstr "Criar Exposição" -#: src/ARte/users/jinja2/users/components/elements-modal.jinja2:20 +#: src/users/jinja2/users/components/elements-modal.jinja2:18 msgid "by " msgstr "por " -#: src/ARte/users/jinja2/users/components/elements-modal.jinja2:25 +#: src/users/jinja2/users/components/elements-modal.jinja2:24 msgid "Uploaded by " msgstr "Publicado por " -#: src/ARte/users/jinja2/users/components/elements-modal.jinja2:27 +#: src/users/jinja2/users/components/elements-modal.jinja2:26 msgid "in " msgstr "em " -#: src/ARte/users/jinja2/users/components/elements-modal.jinja2:34 -#: src/ARte/users/jinja2/users/components/elements-modal.jinja2:51 +#: src/users/jinja2/users/components/elements-modal.jinja2:33 +#: src/users/jinja2/users/components/elements-modal.jinja2:50 msgid " Used in " msgstr " Usado em " -#: src/ARte/users/jinja2/users/components/elements-modal.jinja2:35 +#: src/users/jinja2/users/components/elements-modal.jinja2:34 msgid " artworks" msgstr " Obras" -#: src/ARte/users/jinja2/users/components/elements-modal.jinja2:36 +#: src/users/jinja2/users/components/elements-modal.jinja2:35 msgid " and in " msgstr " e em " -#: src/ARte/users/jinja2/users/components/elements-modal.jinja2:37 -#: src/ARte/users/jinja2/users/components/elements-modal.jinja2:53 -#: src/ARte/users/jinja2/users/components/userbox.jinja2:48 +#: src/users/jinja2/users/components/elements-modal.jinja2:36 +#: src/users/jinja2/users/components/elements-modal.jinja2:52 +#: src/users/jinja2/users/components/userbox.jinja2:49 msgid " Exhibits" msgstr " Exposições" -#: src/ARte/users/jinja2/users/components/elements-modal.jinja2:88 -#: src/ARte/users/jinja2/users/components/elements-modal.jinja2:92 +#: src/users/jinja2/users/components/elements-modal.jinja2:88 +#: src/users/jinja2/users/components/elements-modal.jinja2:92 msgid "This is a Jandig " msgstr "Isso é um " -#: src/ARte/users/jinja2/users/components/elements-modal.jinja2:100 +#: src/users/jinja2/users/components/elements-modal.jinja2:100 msgid "Share this " msgstr "Compartilhe " -#: src/ARte/users/jinja2/users/components/elements-modal.jinja2:103 +#: src/users/jinja2/users/components/elements-modal.jinja2:103 msgid "Create artwork with this " msgstr "Criar obra com " -#: src/ARte/users/jinja2/users/components/item-list.jinja2:18 -#: src/ARte/users/jinja2/users/components/item-list.jinja2:21 -#: src/ARte/users/jinja2/users/components/item-list.jinja2:49 -#: src/ARte/users/jinja2/users/components/item-list.jinja2:68 -msgid "Delete" -msgstr "Excluir" - -#: src/ARte/users/jinja2/users/components/item-list.jinja2:62 +#: src/users/jinja2/users/components/item-list.jinja2:77 #, fuzzy msgid "Artwork(s)" msgstr "Obra(s)" -#: src/ARte/users/jinja2/users/components/item-list.jinja2:63 +#: src/users/jinja2/users/components/item-list.jinja2:81 msgid "See this Exhibition" msgstr "Ver essa Exposição" -#: src/ARte/users/jinja2/users/components/item-list.jinja2:65 +#: src/users/jinja2/users/components/item-list.jinja2:88 +#: src/users/jinja2/users/components/moderator-item-list.jinja2:13 +#: src/users/jinja2/users/components/moderator-item-list.jinja2:22 +#: src/users/jinja2/users/components/moderator-item-list.jinja2:36 +msgid "Delete" +msgstr "Excluir" + +#: src/users/jinja2/users/components/item-list.jinja2:114 +#, fuzzy +msgid "Preview Artwork" +msgstr "Editar Obra" + +#: src/users/jinja2/users/components/item-list.jinja2:125 msgid "Edit" msgstr "Editar" -#: src/ARte/users/jinja2/users/components/userbox.jinja2:9 +#: src/users/jinja2/users/components/moderator-item-list.jinja2:33 +#, fuzzy +msgid "artwork(s)" +msgstr "Obra(s)" + +#: src/users/jinja2/users/components/userbox.jinja2:9 msgid " marker(s)" msgstr " marcador(es)" -#: src/ARte/users/jinja2/users/components/userbox.jinja2:11 -#: src/ARte/users/jinja2/users/components/userbox.jinja2:18 +#: src/users/jinja2/users/components/userbox.jinja2:11 +#: src/users/jinja2/users/components/userbox.jinja2:18 msgid "No uploads yet." -msgstr "Ainda sem conteúdo." +msgstr "Sem conteúdo." -#: src/ARte/users/jinja2/users/components/userbox.jinja2:16 +#: src/users/jinja2/users/components/userbox.jinja2:16 msgid " Object(s)" msgstr " Objeto(s)" -#: src/ARte/users/jinja2/users/components/userbox.jinja2:22 +#: src/users/jinja2/users/components/userbox.jinja2:22 msgid "Edit profile" msgstr "Editar perfil" -#: src/ARte/users/jinja2/users/components/userbox.jinja2:27 +#: src/users/jinja2/users/components/userbox.jinja2:28 msgid "Jandig Artist" msgstr "Artista Jandig" -#: src/ARte/users/jinja2/users/components/userbox.jinja2:30 +#: src/users/jinja2/users/components/userbox.jinja2:31 msgid " Artworks" msgstr " Obras" -#: src/ARte/users/jinja2/users/components/userbox.jinja2:34 +#: src/users/jinja2/users/components/userbox.jinja2:35 msgid "No Artworks yet." -msgstr "Sem Obras ainda." +msgstr "Sem Obras." -#: src/ARte/users/jinja2/users/components/userbox.jinja2:38 -#: src/ARte/users/jinja2/users/components/userbox.jinja2:56 +#: src/users/jinja2/users/components/userbox.jinja2:39 +#: src/users/jinja2/users/components/userbox.jinja2:57 msgid "Create your first!" msgstr "Crie sua primeira!" -#: src/ARte/users/jinja2/users/components/userbox.jinja2:45 +#: src/users/jinja2/users/components/userbox.jinja2:46 msgid "Jandig Curator" msgstr "Curador Jandig" -#: src/ARte/users/jinja2/users/components/userbox.jinja2:52 +#: src/users/jinja2/users/components/userbox.jinja2:53 msgid "No Exhibitions yet." -msgstr "Sem Exposições ainda." +msgstr "Sem Exposições." + +#~ msgid "Exhibit URL" +#~ msgstr "URL da exposição" + +#~ msgid "All Exhibits" +#~ msgstr "Todas Exposições" -#: src/ARte/users/jinja2/users/edit-object.jinja2:31 -msgid "Scale should be adjusted relative to Marker size on the screen. A scale of 2 will render an Object twice the size of the Marker." -msgstr "A escala deve ser ajustada relativa ao tamanho do Marcador na tela. Uma escala de 2 irá renderizar um Objeto duas vezes o tamanho do Marcador." +#~ msgid "All Artworks" +#~ msgstr "Todas as Obras" + +#~ msgid "All Markers" +#~ msgstr "Todos os Marcadores" + +#~ msgid "All Objects" +#~ msgstr "Todos os Objetos" + +#~ msgid "Adjust rotation" +#~ msgstr "Ajustar rotação" #~ msgid " exhibits" #~ msgstr " exposições" @@ -689,9 +849,6 @@ msgstr "A escala deve ser ajustada relativa ao tamanho do Marcador na tela. Uma #~ msgid "Authorize camera" #~ msgstr "Autorizar câmera" -#~ msgid "object" -#~ msgstr "objeto" - #~ msgid "Footer links" #~ msgstr "Links do rodapé" @@ -702,13 +859,13 @@ msgstr "A escala deve ser ajustada relativa ao tamanho do Marcador na tela. Uma #~ msgstr "Atrubuição-CompartilhaIgual 4.0 Internacional (CC BY-SA 4.0)" #~ msgid "No objects yet." -#~ msgstr "Ainda sem objetos." +#~ msgstr "Sem objetos." #~ msgid "No markers yet." -#~ msgstr "Ainda sem marcadores." +#~ msgstr "Sem marcadores." #~ msgid "No exhibits yet." -#~ msgstr "Ainda sem exposições." +#~ msgstr "Sem exposições." #~ msgid "Create exhibit" #~ msgstr "Criar exposição" @@ -724,3 +881,14 @@ msgstr "A escala deve ser ajustada relativa ao tamanho do Marcador na tela. Uma #~ msgid " author" #~ msgstr " autor" + +#~ msgid "" +#~ "Position should be adjusted relative to the Marker's size on the screen. " +#~ "If horizontal position is 2, the center of the Object will be in a " +#~ "distance 2 times the size of its Marker's side to the right. If it's -1, " +#~ "it will be shown to the left." +#~ msgstr "" +#~ "A posição deve ser ajustada em relação ao tamanho do marcador na tela. Se " +#~ "a posição horizontal for 2, o centro do Objeto estará a uma distância de " +#~ "2 vezes o tamanho do lado do seu Marcador à direita. Se for -1, será " +#~ "mostrado à esquerda." diff --git a/locustfile.py b/locustfile.py index b9ea8519..cd1bbdea 100644 --- a/locustfile.py +++ b/locustfile.py @@ -1,13 +1,13 @@ from locust import HttpLocust, Set, between -def index(l): - l.client.get("/") +def index(load): + load.client.get("/") -def load_gifs(l): - l.client.get("/collection/") +def load_gifs(load): + load.client.get("/collection/") -def exhibit(l): - l.client.get("/longavida/") +def exhibit(load): + load.client.get("/longavida/") class UserBehavior(Set): s = {exhibit:1} diff --git a/makefile.py b/makefile.py deleted file mode 100644 index 63cba821..00000000 --- a/makefile.py +++ /dev/null @@ -1,21 +0,0 @@ -import schedule -import time -import subprocess -import os - -def job(): - sudoPassword = '' - command = 'make backup' - os.system('echo %s|sudo -S %s' % (sudoPassword, command)) - - print("backup done") - - -schedule.every().day.at("22:00").do(job) - -while 1: - schedule.run_pending() - time.sleep(1) - - - diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 00000000..3a193c17 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,1813 @@ +# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. + +[[package]] +name = "alabaster" +version = "0.7.13" +description = "A configurable sidebar-enabled Sphinx theme" +optional = false +python-versions = ">=3.6" +files = [ + {file = "alabaster-0.7.13-py3-none-any.whl", hash = "sha256:1ee19aca801bbabb5ba3f5f258e4422dfa86f82f3e9cefb0859b283cdd7f62a3"}, + {file = "alabaster-0.7.13.tar.gz", hash = "sha256:a27a4a084d5e690e16e01e03ad2b2e552c61a65469419b907243193de1a84ae2"}, +] + +[[package]] +name = "asgiref" +version = "3.6.0" +description = "ASGI specs, helper code, and adapters" +optional = false +python-versions = ">=3.7" +files = [ + {file = "asgiref-3.6.0-py3-none-any.whl", hash = "sha256:71e68008da809b957b7ee4b43dbccff33d1b23519fb8344e33f049897077afac"}, + {file = "asgiref-3.6.0.tar.gz", hash = "sha256:9567dfe7bd8d3c8c892227827c41cce860b368104c3431da67a0c5a65a949506"}, +] + +[package.extras] +tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] + +[[package]] +name = "attrs" +version = "22.2.0" +description = "Classes Without Boilerplate" +optional = false +python-versions = ">=3.6" +files = [ + {file = "attrs-22.2.0-py3-none-any.whl", hash = "sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836"}, + {file = "attrs-22.2.0.tar.gz", hash = "sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99"}, +] + +[package.extras] +cov = ["attrs[tests]", "coverage-enable-subprocess", "coverage[toml] (>=5.3)"] +dev = ["attrs[docs,tests]"] +docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope.interface"] +tests = ["attrs[tests-no-zope]", "zope.interface"] +tests-no-zope = ["cloudpickle", "cloudpickle", "hypothesis", "hypothesis", "mypy (>=0.971,<0.990)", "mypy (>=0.971,<0.990)", "pympler", "pympler", "pytest (>=4.3.0)", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-mypy-plugins", "pytest-xdist[psutil]", "pytest-xdist[psutil]"] + +[[package]] +name = "babel" +version = "2.11.0" +description = "Internationalization utilities" +optional = false +python-versions = ">=3.6" +files = [ + {file = "Babel-2.11.0-py3-none-any.whl", hash = "sha256:1ad3eca1c885218f6dce2ab67291178944f810a10a9b5f3cb8382a5a232b64fe"}, + {file = "Babel-2.11.0.tar.gz", hash = "sha256:5ef4b3226b0180dedded4229651c8b0e1a3a6a2837d45a073272f313e4cf97f6"}, +] + +[package.dependencies] +pytz = ">=2015.7" + +[[package]] +name = "black" +version = "22.12.0" +description = "The uncompromising code formatter." +optional = false +python-versions = ">=3.7" +files = [ + {file = "black-22.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9eedd20838bd5d75b80c9f5487dbcb06836a43833a37846cf1d8c1cc01cef59d"}, + {file = "black-22.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:159a46a4947f73387b4d83e87ea006dbb2337eab6c879620a3ba52699b1f4351"}, + {file = "black-22.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d30b212bffeb1e252b31dd269dfae69dd17e06d92b87ad26e23890f3efea366f"}, + {file = "black-22.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:7412e75863aa5c5411886804678b7d083c7c28421210180d67dfd8cf1221e1f4"}, + {file = "black-22.12.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c116eed0efb9ff870ded8b62fe9f28dd61ef6e9ddd28d83d7d264a38417dcee2"}, + {file = "black-22.12.0-cp37-cp37m-win_amd64.whl", hash = "sha256:1f58cbe16dfe8c12b7434e50ff889fa479072096d79f0a7f25e4ab8e94cd8350"}, + {file = "black-22.12.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77d86c9f3db9b1bf6761244bc0b3572a546f5fe37917a044e02f3166d5aafa7d"}, + {file = "black-22.12.0-cp38-cp38-win_amd64.whl", hash = "sha256:82d9fe8fee3401e02e79767016b4907820a7dc28d70d137eb397b92ef3cc5bfc"}, + {file = "black-22.12.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:101c69b23df9b44247bd88e1d7e90154336ac4992502d4197bdac35dd7ee3320"}, + {file = "black-22.12.0-cp39-cp39-win_amd64.whl", hash = "sha256:559c7a1ba9a006226f09e4916060982fd27334ae1998e7a38b3f33a37f7a2148"}, + {file = "black-22.12.0-py3-none-any.whl", hash = "sha256:436cc9167dd28040ad90d3b404aec22cedf24a6e4d7de221bec2730ec0c97bcf"}, + {file = "black-22.12.0.tar.gz", hash = "sha256:229351e5a18ca30f447bf724d007f890f97e13af070bb6ad4c0a441cd7596a2f"}, +] + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +pathspec = ">=0.9.0" +platformdirs = ">=2" +tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""} + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.7.4)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[package]] +name = "boto3" +version = "1.26.60" +description = "The AWS SDK for Python" +optional = false +python-versions = ">= 3.7" +files = [ + {file = "boto3-1.26.60-py3-none-any.whl", hash = "sha256:5fd2810217a74a38078a19fb85a9e5d6934d0c146eb060967a3ffd7ab33cdf00"}, + {file = "boto3-1.26.60.tar.gz", hash = "sha256:f0824b3bcf803800d3ecef903b4840427e4b3d37a069f6fc9a86310f7e036ad5"}, +] + +[package.dependencies] +botocore = ">=1.29.60,<1.30.0" +jmespath = ">=0.7.1,<2.0.0" +s3transfer = ">=0.6.0,<0.7.0" + +[package.extras] +crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] + +[[package]] +name = "botocore" +version = "1.29.60" +description = "Low-level, data-driven core of boto 3." +optional = false +python-versions = ">= 3.7" +files = [ + {file = "botocore-1.29.60-py3-none-any.whl", hash = "sha256:c4ae251e7df0cf01d893eb945bc8f23c14989ed349775a8e16c949f08a068f9a"}, + {file = "botocore-1.29.60.tar.gz", hash = "sha256:a21217ccf4613c9ebbe4c3192e13ba91d46be642560e39a16406662a398a107b"}, +] + +[package.dependencies] +jmespath = ">=0.7.1,<2.0.0" +python-dateutil = ">=2.1,<3.0.0" +urllib3 = ">=1.25.4,<1.27" + +[package.extras] +crt = ["awscrt (==0.15.3)"] + +[[package]] +name = "certifi" +version = "2022.12.7" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"}, + {file = "certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"}, +] + +[[package]] +name = "cffi" +version = "1.15.1" +description = "Foreign Function Interface for Python calling C code." +optional = false +python-versions = "*" +files = [ + {file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"}, + {file = "cffi-1.15.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2"}, + {file = "cffi-1.15.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914"}, + {file = "cffi-1.15.1-cp27-cp27m-win32.whl", hash = "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3"}, + {file = "cffi-1.15.1-cp27-cp27m-win_amd64.whl", hash = "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e"}, + {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162"}, + {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b"}, + {file = "cffi-1.15.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21"}, + {file = "cffi-1.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4"}, + {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01"}, + {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e"}, + {file = "cffi-1.15.1-cp310-cp310-win32.whl", hash = "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2"}, + {file = "cffi-1.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d"}, + {file = "cffi-1.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac"}, + {file = "cffi-1.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c"}, + {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef"}, + {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8"}, + {file = "cffi-1.15.1-cp311-cp311-win32.whl", hash = "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d"}, + {file = "cffi-1.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104"}, + {file = "cffi-1.15.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e"}, + {file = "cffi-1.15.1-cp36-cp36m-win32.whl", hash = "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf"}, + {file = "cffi-1.15.1-cp36-cp36m-win_amd64.whl", hash = "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497"}, + {file = "cffi-1.15.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426"}, + {file = "cffi-1.15.1-cp37-cp37m-win32.whl", hash = "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9"}, + {file = "cffi-1.15.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045"}, + {file = "cffi-1.15.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192"}, + {file = "cffi-1.15.1-cp38-cp38-win32.whl", hash = "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314"}, + {file = "cffi-1.15.1-cp38-cp38-win_amd64.whl", hash = "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5"}, + {file = "cffi-1.15.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585"}, + {file = "cffi-1.15.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27"}, + {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76"}, + {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3"}, + {file = "cffi-1.15.1-cp39-cp39-win32.whl", hash = "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee"}, + {file = "cffi-1.15.1-cp39-cp39-win_amd64.whl", hash = "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c"}, + {file = "cffi-1.15.1.tar.gz", hash = "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9"}, +] + +[package.dependencies] +pycparser = "*" + +[[package]] +name = "charset-normalizer" +version = "3.0.1" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = "*" +files = [ + {file = "charset-normalizer-3.0.1.tar.gz", hash = "sha256:ebea339af930f8ca5d7a699b921106c6e29c617fe9606fa7baa043c1cdae326f"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88600c72ef7587fe1708fd242b385b6ed4b8904976d5da0893e31df8b3480cb6"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c75ffc45f25324e68ab238cb4b5c0a38cd1c3d7f1fb1f72b5541de469e2247db"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:db72b07027db150f468fbada4d85b3b2729a3db39178abf5c543b784c1254539"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62595ab75873d50d57323a91dd03e6966eb79c41fa834b7a1661ed043b2d404d"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ff6f3db31555657f3163b15a6b7c6938d08df7adbfc9dd13d9d19edad678f1e8"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:772b87914ff1152b92a197ef4ea40efe27a378606c39446ded52c8f80f79702e"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70990b9c51340e4044cfc394a81f614f3f90d41397104d226f21e66de668730d"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:292d5e8ba896bbfd6334b096e34bffb56161c81408d6d036a7dfa6929cff8783"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:2edb64ee7bf1ed524a1da60cdcd2e1f6e2b4f66ef7c077680739f1641f62f555"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:31a9ddf4718d10ae04d9b18801bd776693487cbb57d74cc3458a7673f6f34639"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:44ba614de5361b3e5278e1241fda3dc1838deed864b50a10d7ce92983797fa76"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:12db3b2c533c23ab812c2b25934f60383361f8a376ae272665f8e48b88e8e1c6"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c512accbd6ff0270939b9ac214b84fb5ada5f0409c44298361b2f5e13f9aed9e"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-win32.whl", hash = "sha256:502218f52498a36d6bf5ea77081844017bf7982cdbe521ad85e64cabee1b608b"}, + {file = "charset_normalizer-3.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:601f36512f9e28f029d9481bdaf8e89e5148ac5d89cffd3b05cd533eeb423b59"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0298eafff88c99982a4cf66ba2efa1128e4ddaca0b05eec4c456bbc7db691d8d"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a8d0fc946c784ff7f7c3742310cc8a57c5c6dc31631269876a88b809dbeff3d3"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:87701167f2a5c930b403e9756fab1d31d4d4da52856143b609e30a1ce7160f3c"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e76c0f23218b8f46c4d87018ca2e441535aed3632ca134b10239dfb6dadd6b"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0c0a590235ccd933d9892c627dec5bc7511ce6ad6c1011fdf5b11363022746c1"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c7fe7afa480e3e82eed58e0ca89f751cd14d767638e2550c77a92a9e749c317"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79909e27e8e4fcc9db4addea88aa63f6423ebb171db091fb4373e3312cb6d603"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ac7b6a045b814cf0c47f3623d21ebd88b3e8cf216a14790b455ea7ff0135d18"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:72966d1b297c741541ca8cf1223ff262a6febe52481af742036a0b296e35fa5a"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:f9d0c5c045a3ca9bedfc35dca8526798eb91a07aa7a2c0fee134c6c6f321cbd7"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:5995f0164fa7df59db4746112fec3f49c461dd6b31b841873443bdb077c13cfc"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4a8fcf28c05c1f6d7e177a9a46a1c52798bfe2ad80681d275b10dcf317deaf0b"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:761e8904c07ad053d285670f36dd94e1b6ab7f16ce62b9805c475b7aa1cffde6"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-win32.whl", hash = "sha256:71140351489970dfe5e60fc621ada3e0f41104a5eddaca47a7acb3c1b851d6d3"}, + {file = "charset_normalizer-3.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:9ab77acb98eba3fd2a85cd160851816bfce6871d944d885febf012713f06659c"}, + {file = "charset_normalizer-3.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:84c3990934bae40ea69a82034912ffe5a62c60bbf6ec5bc9691419641d7d5c9a"}, + {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74292fc76c905c0ef095fe11e188a32ebd03bc38f3f3e9bcb85e4e6db177b7ea"}, + {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c95a03c79bbe30eec3ec2b7f076074f4281526724c8685a42872974ef4d36b72"}, + {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4c39b0e3eac288fedc2b43055cfc2ca7a60362d0e5e87a637beac5d801ef478"}, + {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df2c707231459e8a4028eabcd3cfc827befd635b3ef72eada84ab13b52e1574d"}, + {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:93ad6d87ac18e2a90b0fe89df7c65263b9a99a0eb98f0a3d2e079f12a0735837"}, + {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:59e5686dd847347e55dffcc191a96622f016bc0ad89105e24c14e0d6305acbc6"}, + {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:cd6056167405314a4dc3c173943f11249fa0f1b204f8b51ed4bde1a9cd1834dc"}, + {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:083c8d17153ecb403e5e1eb76a7ef4babfc2c48d58899c98fcaa04833e7a2f9a"}, + {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:f5057856d21e7586765171eac8b9fc3f7d44ef39425f85dbcccb13b3ebea806c"}, + {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:7eb33a30d75562222b64f569c642ff3dc6689e09adda43a082208397f016c39a"}, + {file = "charset_normalizer-3.0.1-cp36-cp36m-win32.whl", hash = "sha256:95dea361dd73757c6f1c0a1480ac499952c16ac83f7f5f4f84f0658a01b8ef41"}, + {file = "charset_normalizer-3.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:eaa379fcd227ca235d04152ca6704c7cb55564116f8bc52545ff357628e10602"}, + {file = "charset_normalizer-3.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3e45867f1f2ab0711d60c6c71746ac53537f1684baa699f4f668d4c6f6ce8e14"}, + {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cadaeaba78750d58d3cc6ac4d1fd867da6fc73c88156b7a3212a3cd4819d679d"}, + {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:911d8a40b2bef5b8bbae2e36a0b103f142ac53557ab421dc16ac4aafee6f53dc"}, + {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:503e65837c71b875ecdd733877d852adbc465bd82c768a067badd953bf1bc5a3"}, + {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a60332922359f920193b1d4826953c507a877b523b2395ad7bc716ddd386d866"}, + {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:16a8663d6e281208d78806dbe14ee9903715361cf81f6d4309944e4d1e59ac5b"}, + {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:a16418ecf1329f71df119e8a65f3aa68004a3f9383821edcb20f0702934d8087"}, + {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:9d9153257a3f70d5f69edf2325357251ed20f772b12e593f3b3377b5f78e7ef8"}, + {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:02a51034802cbf38db3f89c66fb5d2ec57e6fe7ef2f4a44d070a593c3688667b"}, + {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:2e396d70bc4ef5325b72b593a72c8979999aa52fb8bcf03f701c1b03e1166918"}, + {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:11b53acf2411c3b09e6af37e4b9005cba376c872503c8f28218c7243582df45d"}, + {file = "charset_normalizer-3.0.1-cp37-cp37m-win32.whl", hash = "sha256:0bf2dae5291758b6f84cf923bfaa285632816007db0330002fa1de38bfcb7154"}, + {file = "charset_normalizer-3.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:2c03cc56021a4bd59be889c2b9257dae13bf55041a3372d3295416f86b295fb5"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:024e606be3ed92216e2b6952ed859d86b4cfa52cd5bc5f050e7dc28f9b43ec42"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4b0d02d7102dd0f997580b51edc4cebcf2ab6397a7edf89f1c73b586c614272c"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:358a7c4cb8ba9b46c453b1dd8d9e431452d5249072e4f56cfda3149f6ab1405e"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81d6741ab457d14fdedc215516665050f3822d3e56508921cc7239f8c8e66a58"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8b8af03d2e37866d023ad0ddea594edefc31e827fee64f8de5611a1dbc373174"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9cf4e8ad252f7c38dd1f676b46514f92dc0ebeb0db5552f5f403509705e24753"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e696f0dd336161fca9adbb846875d40752e6eba585843c768935ba5c9960722b"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c22d3fe05ce11d3671297dc8973267daa0f938b93ec716e12e0f6dee81591dc1"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:109487860ef6a328f3eec66f2bf78b0b72400280d8f8ea05f69c51644ba6521a"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:37f8febc8ec50c14f3ec9637505f28e58d4f66752207ea177c1d67df25da5aed"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:f97e83fa6c25693c7a35de154681fcc257c1c41b38beb0304b9c4d2d9e164479"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a152f5f33d64a6be73f1d30c9cc82dfc73cec6477ec268e7c6e4c7d23c2d2291"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:39049da0ffb96c8cbb65cbf5c5f3ca3168990adf3551bd1dee10c48fce8ae820"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-win32.whl", hash = "sha256:4457ea6774b5611f4bed5eaa5df55f70abde42364d498c5134b7ef4c6958e20e"}, + {file = "charset_normalizer-3.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:e62164b50f84e20601c1ff8eb55620d2ad25fb81b59e3cd776a1902527a788af"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8eade758719add78ec36dc13201483f8e9b5d940329285edcd5f70c0a9edbd7f"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8499ca8f4502af841f68135133d8258f7b32a53a1d594aa98cc52013fff55678"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3fc1c4a2ffd64890aebdb3f97e1278b0cc72579a08ca4de8cd2c04799a3a22be"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:00d3ffdaafe92a5dc603cb9bd5111aaa36dfa187c8285c543be562e61b755f6b"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c2ac1b08635a8cd4e0cbeaf6f5e922085908d48eb05d44c5ae9eabab148512ca"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f6f45710b4459401609ebebdbcfb34515da4fc2aa886f95107f556ac69a9147e"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ae1de54a77dc0d6d5fcf623290af4266412a7c4be0b1ff7444394f03f5c54e3"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3b590df687e3c5ee0deef9fc8c547d81986d9a1b56073d82de008744452d6541"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab5de034a886f616a5668aa5d098af2b5385ed70142090e2a31bcbd0af0fdb3d"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9cb3032517f1627cc012dbc80a8ec976ae76d93ea2b5feaa9d2a5b8882597579"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:608862a7bf6957f2333fc54ab4399e405baad0163dc9f8d99cb236816db169d4"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:0f438ae3532723fb6ead77e7c604be7c8374094ef4ee2c5e03a3a17f1fca256c"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:356541bf4381fa35856dafa6a965916e54bed415ad8a24ee6de6e37deccf2786"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-win32.whl", hash = "sha256:39cf9ed17fe3b1bc81f33c9ceb6ce67683ee7526e65fde1447c772afc54a1bb8"}, + {file = "charset_normalizer-3.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:0a11e971ed097d24c534c037d298ad32c6ce81a45736d31e0ff0ad37ab437d59"}, + {file = "charset_normalizer-3.0.1-py3-none-any.whl", hash = "sha256:7e189e2e1d3ed2f4aebabd2d5b0f931e883676e51c7624826e0a4e5fe8a0bf24"}, +] + +[[package]] +name = "click" +version = "8.1.3" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, + {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "dj-inmemorystorage" +version = "2.1.0" +description = "A non-persistent in-memory data storage backend for Django." +optional = false +python-versions = "*" +files = [ + {file = "dj-inmemorystorage-2.1.0.tar.gz", hash = "sha256:1771801613414262803a1a1e97dafd2b7a563e78fbcbfa2b6f841c9d8e7b872a"}, + {file = "dj_inmemorystorage-2.1.0-py2.py3-none-any.whl", hash = "sha256:81aa007a7cdb1899b3cd92404f656c82cd690a831b8698a43045f859d7276945"}, +] + +[package.dependencies] +Django = ">=1.11" +six = ">=1.10" + +[[package]] +name = "django" +version = "4.1.5" +description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." +optional = false +python-versions = ">=3.8" +files = [ + {file = "Django-4.1.5-py3-none-any.whl", hash = "sha256:4b214a05fe4c99476e99e2445c8b978c8369c18d4dea8e22ec412862715ad763"}, + {file = "Django-4.1.5.tar.gz", hash = "sha256:ff56ebd7ead0fd5dbe06fe157b0024a7aaea2e0593bb3785fb594cf94dad58ef"}, +] + +[package.dependencies] +asgiref = ">=3.5.2,<4" +sqlparse = ">=0.2.2" +tzdata = {version = "*", markers = "sys_platform == \"win32\""} + +[package.extras] +argon2 = ["argon2-cffi (>=19.1.0)"] +bcrypt = ["bcrypt"] + +[[package]] +name = "django-boogie" +version = "0.13.3" +description = "Django with swagger: better managers, APIs, configurations and url mappers." +optional = false +python-versions = "*" +files = [ + {file = "django-boogie-0.13.3.tar.gz", hash = "sha256:657799ae308a04ab32ac153c4204c5f7b8d12c032e89146d9112e662baa08c3a"}, +] + +[package.dependencies] +django = ">=2.0" +sidekick = ">=0.5.0,<0.6.0" + +[package.extras] +configurations = ["django-environ (>=0.4,<1.0)"] +dev-stack = ["django-boogie[stack]", "django-boogie[test]", "django-debug-toolbar", "django-spaghetti-and-meatballs", "docutils (>=0.14,<1.0)", "flake8 (>=3.5)", "hyperpython (>=1.1.0,<1.2.0)", "invoke (>=1.0)", "manuel (>=1.10)", "sphinx-autobuild (>=0.7,<1.0)"] +extra = ["django-bulk-update (>=2.2)", "django-manager-utils (>=1.1)", "django-model-utils (>=3.0)", "django-polymorphic (>=2.0,<3.0)"] +pandas = ["django-pandas (>=0.5.0,<0.6.0)", "pandas (>=0.22,<1.0)"] +rest = ["django-filter (>=1.1,<2.0)", "djangorestframework (>=3.8,<4.0)"] +rules = ["rules (<2)"] +stack = ["django-bulk-update (>=2.2)", "django-environ (>=0.4,<1.0)", "django-extensions (>=2.1,<3.0)", "django-filter (>=1.1,<2.0)", "django-manager-utils (>=1.1)", "django-model-utils (>=3.0)", "django-pandas (>=0.5.0,<0.6.0)", "django-polymorphic (>=2.0,<3.0)", "djangorestframework (>=3.8,<4.0)", "pandas (>=0.22,<1.0)", "rules (<2)"] +test = ["beautifulsoup4 (>=4.6,<5.0)", "django-stubs (>=0.12.1,<0.13.0)", "factory_boy (>=2.0,<3.0)", "faker", "model_mommy (>=1.6,<2.0)", "mypy (>=0.701,<1.0)", "pytest (>=3.8,<4.0)", "pytest-cov (>=2.6,<3.0)", "pytest-django (>=3.1,<4.0)"] + +[[package]] +name = "django-cors-headers" +version = "3.13.0" +description = "django-cors-headers is a Django application for handling the server headers required for Cross-Origin Resource Sharing (CORS)." +optional = false +python-versions = ">=3.7" +files = [ + {file = "django-cors-headers-3.13.0.tar.gz", hash = "sha256:f9dc6b4e3f611c3199700b3e5f3398c28757dcd559c2f82932687f3d0443cfdf"}, + {file = "django_cors_headers-3.13.0-py3-none-any.whl", hash = "sha256:37e42883b5f1f2295df6b4bba96eb2417a14a03270cb24b2a07f021cd4487cf4"}, +] + +[package.dependencies] +Django = ">=3.2" + +[[package]] +name = "django-debug-toolbar" +version = "3.8.1" +description = "A configurable set of panels that display various debug information about the current request/response." +optional = false +python-versions = ">=3.7" +files = [ + {file = "django_debug_toolbar-3.8.1-py3-none-any.whl", hash = "sha256:879f8a4672d41621c06a4d322dcffa630fc4df056cada6e417ed01db0e5e0478"}, + {file = "django_debug_toolbar-3.8.1.tar.gz", hash = "sha256:24ef1a7d44d25e60d7951e378454c6509bf536dce7e7d9d36e7c387db499bc27"}, +] + +[package.dependencies] +django = ">=3.2.4" +sqlparse = ">=0.2" + +[[package]] +name = "django-environ" +version = "0.9.0" +description = "A package that allows you to utilize 12factor inspired environment variables to configure your Django application." +optional = false +python-versions = ">=3.4,<4" +files = [ + {file = "django-environ-0.9.0.tar.gz", hash = "sha256:bff5381533056328c9ac02f71790bd5bf1cea81b1beeb648f28b81c9e83e0a21"}, + {file = "django_environ-0.9.0-py2.py3-none-any.whl", hash = "sha256:f21a5ef8cc603da1870bbf9a09b7e5577ab5f6da451b843dbcc721a7bca6b3d9"}, +] + +[package.extras] +develop = ["coverage[toml] (>=5.0a4)", "furo (>=2021.8.17b43,<2021.9.dev0)", "pytest (>=4.6.11)", "sphinx (>=3.5.0)", "sphinx-notfound-page"] +docs = ["furo (>=2021.8.17b43,<2021.9.dev0)", "sphinx (>=3.5.0)", "sphinx-notfound-page"] +testing = ["coverage[toml] (>=5.0a4)", "pytest (>=4.6.11)"] + +[[package]] +name = "django-extensions" +version = "3.2.1" +description = "Extensions for Django" +optional = false +python-versions = ">=3.6" +files = [ + {file = "django-extensions-3.2.1.tar.gz", hash = "sha256:2a4f4d757be2563cd1ff7cfdf2e57468f5f931cc88b23cf82ca75717aae504a4"}, + {file = "django_extensions-3.2.1-py3-none-any.whl", hash = "sha256:421464be390289513f86cb5e18eb43e5dc1de8b4c27ba9faa3b91261b0d67e09"}, +] + +[package.dependencies] +Django = ">=3.2" + +[[package]] +name = "django-htmx" +version = "1.18.0" +description = "Extensions for using Django with htmx." +optional = false +python-versions = ">=3.8" +files = [ + {file = "django_htmx-1.18.0-py3-none-any.whl", hash = "sha256:48f3b8a784467bfcc30562067b87ecbc4ad9b739cc269eec5f3789c16d2cb2ce"}, + {file = "django_htmx-1.18.0.tar.gz", hash = "sha256:db8a0cc15bcd0f7ae929bcb9108d9e6be228843092aca7956d977c31c4d95aae"}, +] + +[package.dependencies] +asgiref = ">=3.6" +django = ">=3.2" + +[[package]] +name = "django-storages" +version = "1.13.2" +description = "Support for many storage backends in Django" +optional = false +python-versions = ">=3.7" +files = [ + {file = "django-storages-1.13.2.tar.gz", hash = "sha256:cbadd15c909ceb7247d4ffc503f12a9bec36999df8d0bef7c31e57177d512688"}, + {file = "django_storages-1.13.2-py3-none-any.whl", hash = "sha256:31dc5a992520be571908c4c40d55d292660ece3a55b8141462b4e719aa38eab3"}, +] + +[package.dependencies] +Django = ">=3.2" + +[package.extras] +azure = ["azure-storage-blob (>=12.0.0)"] +boto3 = ["boto3 (>=1.4.4)"] +dropbox = ["dropbox (>=7.2.1)"] +google = ["google-cloud-storage (>=1.27.0)"] +libcloud = ["apache-libcloud"] +sftp = ["paramiko (>=1.10.0)"] + +[[package]] +name = "djangorestframework" +version = "3.14.0" +description = "Web APIs for Django, made easy." +optional = false +python-versions = ">=3.6" +files = [ + {file = "djangorestframework-3.14.0-py3-none-any.whl", hash = "sha256:eb63f58c9f218e1a7d064d17a70751f528ed4e1d35547fdade9aaf4cd103fd08"}, + {file = "djangorestframework-3.14.0.tar.gz", hash = "sha256:579a333e6256b09489cbe0a067e66abe55c6595d8926be6b99423786334350c8"}, +] + +[package.dependencies] +django = ">=3.0" +pytz = "*" + +[[package]] +name = "docutils" +version = "0.19" +description = "Docutils -- Python Documentation Utilities" +optional = false +python-versions = ">=3.7" +files = [ + {file = "docutils-0.19-py3-none-any.whl", hash = "sha256:5e1de4d849fee02c63b040a4a3fd567f4ab104defd8a5511fbbc24a8a017efbc"}, + {file = "docutils-0.19.tar.gz", hash = "sha256:33995a6753c30b7f577febfc2c50411fec6aac7f7ffeb7c4cfe5991072dcf9e6"}, +] + +[[package]] +name = "drf-nested-routers" +version = "0.93.4" +description = "Nested resources for the Django Rest Framework" +optional = false +python-versions = ">=3.5" +files = [ + {file = "drf-nested-routers-0.93.4.tar.gz", hash = "sha256:01aa556b8c08608bb74fb34f6ca065a5183f2cda4dc0478192cc17a2581d71b0"}, + {file = "drf_nested_routers-0.93.4-py2.py3-none-any.whl", hash = "sha256:996b77f3f4dfaf64569e7b8f04e3919945f90f95366838ca5b8bed9dd709d6c5"}, +] + +[package.dependencies] +Django = ">=1.11" +djangorestframework = ">=3.6.0" + +[[package]] +name = "exceptiongroup" +version = "1.1.0" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.1.0-py3-none-any.whl", hash = "sha256:327cbda3da756e2de031a3107b81ab7b3770a602c4d16ca618298c526f4bec1e"}, + {file = "exceptiongroup-1.1.0.tar.gz", hash = "sha256:bcb67d800a4497e1b404c2dd44fca47d3b7a5e5433dbab67f96c1a685cdfdf23"}, +] + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "execnet" +version = "1.9.0" +description = "execnet: rapid multi-Python deployment" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "execnet-1.9.0-py2.py3-none-any.whl", hash = "sha256:a295f7cc774947aac58dde7fdc85f4aa00c42adf5d8f5468fc630c1acf30a142"}, + {file = "execnet-1.9.0.tar.gz", hash = "sha256:8f694f3ba9cc92cab508b152dcfe322153975c29bda272e2fd7f3f00f36e47c5"}, +] + +[package.extras] +testing = ["pre-commit"] + +[[package]] +name = "factory-boy" +version = "3.2.1" +description = "A versatile test fixtures replacement based on thoughtbot's factory_bot for Ruby." +optional = false +python-versions = ">=3.6" +files = [ + {file = "factory_boy-3.2.1-py2.py3-none-any.whl", hash = "sha256:eb02a7dd1b577ef606b75a253b9818e6f9eaf996d94449c9d5ebb124f90dc795"}, + {file = "factory_boy-3.2.1.tar.gz", hash = "sha256:a98d277b0c047c75eb6e4ab8508a7f81fb03d2cb21986f627913546ef7a2a55e"}, +] + +[package.dependencies] +Faker = ">=0.7.0" + +[package.extras] +dev = ["Django", "Pillow", "SQLAlchemy", "coverage", "flake8", "isort", "mongoengine", "tox", "wheel (>=0.32.0)", "zest.releaser[recommended]"] +doc = ["Sphinx", "sphinx-rtd-theme", "sphinxcontrib-spelling"] + +[[package]] +name = "faker" +version = "16.6.1" +description = "Faker is a Python package that generates fake data for you." +optional = false +python-versions = ">=3.7" +files = [ + {file = "Faker-16.6.1-py3-none-any.whl", hash = "sha256:2375d0bbaf405dc4f1cbc771485a78ad952c776798e5c228eef3e7b337f78868"}, + {file = "Faker-16.6.1.tar.gz", hash = "sha256:b76e5d2405470e3d38d37d1bfaa9d9bbf171bdf41c814f5bbd8117b121f6bccb"}, +] + +[package.dependencies] +python-dateutil = ">=2.4" + +[[package]] +name = "flake8" +version = "5.0.4" +description = "the modular source code checker: pep8 pyflakes and co" +optional = false +python-versions = ">=3.6.1" +files = [ + {file = "flake8-5.0.4-py2.py3-none-any.whl", hash = "sha256:7a1cf6b73744f5806ab95e526f6f0d8c01c66d7bbe349562d22dfca20610b248"}, + {file = "flake8-5.0.4.tar.gz", hash = "sha256:6fbe320aad8d6b95cec8b8e47bc933004678dc63095be98528b7bdd2a9f510db"}, +] + +[package.dependencies] +mccabe = ">=0.7.0,<0.8.0" +pycodestyle = ">=2.9.0,<2.10.0" +pyflakes = ">=2.5.0,<2.6.0" + +[[package]] +name = "gevent" +version = "23.9.1" +description = "Coroutine-based network library" +optional = false +python-versions = ">=3.8" +files = [ + {file = "gevent-23.9.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:a3c5e9b1f766a7a64833334a18539a362fb563f6c4682f9634dea72cbe24f771"}, + {file = "gevent-23.9.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b101086f109168b23fa3586fccd1133494bdb97f86920a24dc0b23984dc30b69"}, + {file = "gevent-23.9.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:36a549d632c14684bcbbd3014a6ce2666c5f2a500f34d58d32df6c9ea38b6535"}, + {file = "gevent-23.9.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:272cffdf535978d59c38ed837916dfd2b5d193be1e9e5dcc60a5f4d5025dd98a"}, + {file = "gevent-23.9.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcb8612787a7f4626aa881ff15ff25439561a429f5b303048f0fca8a1c781c39"}, + {file = "gevent-23.9.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:d57737860bfc332b9b5aa438963986afe90f49645f6e053140cfa0fa1bdae1ae"}, + {file = "gevent-23.9.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:5f3c781c84794926d853d6fb58554dc0dcc800ba25c41d42f6959c344b4db5a6"}, + {file = "gevent-23.9.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:dbb22a9bbd6a13e925815ce70b940d1578dbe5d4013f20d23e8a11eddf8d14a7"}, + {file = "gevent-23.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:707904027d7130ff3e59ea387dddceedb133cc742b00b3ffe696d567147a9c9e"}, + {file = "gevent-23.9.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:45792c45d60f6ce3d19651d7fde0bc13e01b56bb4db60d3f32ab7d9ec467374c"}, + {file = "gevent-23.9.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e24c2af9638d6c989caffc691a039d7c7022a31c0363da367c0d32ceb4a0648"}, + {file = "gevent-23.9.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e1ead6863e596a8cc2a03e26a7a0981f84b6b3e956101135ff6d02df4d9a6b07"}, + {file = "gevent-23.9.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65883ac026731ac112184680d1f0f1e39fa6f4389fd1fc0bf46cc1388e2599f9"}, + {file = "gevent-23.9.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf7af500da05363e66f122896012acb6e101a552682f2352b618e541c941a011"}, + {file = "gevent-23.9.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:c3e5d2fa532e4d3450595244de8ccf51f5721a05088813c1abd93ad274fe15e7"}, + {file = "gevent-23.9.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c84d34256c243b0a53d4335ef0bc76c735873986d478c53073861a92566a8d71"}, + {file = "gevent-23.9.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ada07076b380918829250201df1d016bdafb3acf352f35e5693b59dceee8dd2e"}, + {file = "gevent-23.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:921dda1c0b84e3d3b1778efa362d61ed29e2b215b90f81d498eb4d8eafcd0b7a"}, + {file = "gevent-23.9.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:ed7a048d3e526a5c1d55c44cb3bc06cfdc1947d06d45006cc4cf60dedc628904"}, + {file = "gevent-23.9.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7c1abc6f25f475adc33e5fc2dbcc26a732608ac5375d0d306228738a9ae14d3b"}, + {file = "gevent-23.9.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4368f341a5f51611411ec3fc62426f52ac3d6d42eaee9ed0f9eebe715c80184e"}, + {file = "gevent-23.9.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:52b4abf28e837f1865a9bdeef58ff6afd07d1d888b70b6804557e7908032e599"}, + {file = "gevent-23.9.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:52e9f12cd1cda96603ce6b113d934f1aafb873e2c13182cf8e86d2c5c41982ea"}, + {file = "gevent-23.9.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:de350fde10efa87ea60d742901e1053eb2127ebd8b59a7d3b90597eb4e586599"}, + {file = "gevent-23.9.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:fde6402c5432b835fbb7698f1c7f2809c8d6b2bd9d047ac1f5a7c1d5aa569303"}, + {file = "gevent-23.9.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:dd6c32ab977ecf7c7b8c2611ed95fa4aaebd69b74bf08f4b4960ad516861517d"}, + {file = "gevent-23.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:455e5ee8103f722b503fa45dedb04f3ffdec978c1524647f8ba72b4f08490af1"}, + {file = "gevent-23.9.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:7ccf0fd378257cb77d91c116e15c99e533374a8153632c48a3ecae7f7f4f09fe"}, + {file = "gevent-23.9.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d163d59f1be5a4c4efcdd13c2177baaf24aadf721fdf2e1af9ee54a998d160f5"}, + {file = "gevent-23.9.1-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:7532c17bc6c1cbac265e751b95000961715adef35a25d2b0b1813aa7263fb397"}, + {file = "gevent-23.9.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:78eebaf5e73ff91d34df48f4e35581ab4c84e22dd5338ef32714264063c57507"}, + {file = "gevent-23.9.1-cp38-cp38-win32.whl", hash = "sha256:f632487c87866094546a74eefbca2c74c1d03638b715b6feb12e80120960185a"}, + {file = "gevent-23.9.1-cp38-cp38-win_amd64.whl", hash = "sha256:62d121344f7465e3739989ad6b91f53a6ca9110518231553fe5846dbe1b4518f"}, + {file = "gevent-23.9.1-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:bf456bd6b992eb0e1e869e2fd0caf817f0253e55ca7977fd0e72d0336a8c1c6a"}, + {file = "gevent-23.9.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43daf68496c03a35287b8b617f9f91e0e7c0d042aebcc060cadc3f049aadd653"}, + {file = "gevent-23.9.1-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:7c28e38dcde327c217fdafb9d5d17d3e772f636f35df15ffae2d933a5587addd"}, + {file = "gevent-23.9.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:fae8d5b5b8fa2a8f63b39f5447168b02db10c888a3e387ed7af2bd1b8612e543"}, + {file = "gevent-23.9.1-cp39-cp39-win32.whl", hash = "sha256:2c7b5c9912378e5f5ccf180d1fdb1e83f42b71823483066eddbe10ef1a2fcaa2"}, + {file = "gevent-23.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:a2898b7048771917d85a1d548fd378e8a7b2ca963db8e17c6d90c76b495e0e2b"}, + {file = "gevent-23.9.1.tar.gz", hash = "sha256:72c002235390d46f94938a96920d8856d4ffd9ddf62a303a0d7c118894097e34"}, +] + +[package.dependencies] +cffi = {version = ">=1.12.2", markers = "platform_python_implementation == \"CPython\" and sys_platform == \"win32\""} +greenlet = [ + {version = ">=2.0.0", markers = "platform_python_implementation == \"CPython\" and python_version < \"3.11\""}, + {version = ">=3.0rc3", markers = "platform_python_implementation == \"CPython\" and python_version >= \"3.11\""}, +] +"zope.event" = "*" +"zope.interface" = "*" + +[package.extras] +dnspython = ["dnspython (>=1.16.0,<2.0)", "idna"] +docs = ["furo", "repoze.sphinx.autointerface", "sphinx", "sphinxcontrib-programoutput", "zope.schema"] +monitor = ["psutil (>=5.7.0)"] +recommended = ["cffi (>=1.12.2)", "dnspython (>=1.16.0,<2.0)", "idna", "psutil (>=5.7.0)"] +test = ["cffi (>=1.12.2)", "coverage (>=5.0)", "dnspython (>=1.16.0,<2.0)", "idna", "objgraph", "psutil (>=5.7.0)", "requests", "setuptools"] + +[[package]] +name = "greenlet" +version = "3.0.3" +description = "Lightweight in-process concurrent programming" +optional = false +python-versions = ">=3.7" +files = [ + {file = "greenlet-3.0.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:9da2bd29ed9e4f15955dd1595ad7bc9320308a3b766ef7f837e23ad4b4aac31a"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d353cadd6083fdb056bb46ed07e4340b0869c305c8ca54ef9da3421acbdf6881"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dca1e2f3ca00b84a396bc1bce13dd21f680f035314d2379c4160c98153b2059b"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ed7fb269f15dc662787f4119ec300ad0702fa1b19d2135a37c2c4de6fadfd4a"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd4f49ae60e10adbc94b45c0b5e6a179acc1736cf7a90160b404076ee283cf83"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:73a411ef564e0e097dbe7e866bb2dda0f027e072b04da387282b02c308807405"}, + {file = "greenlet-3.0.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7f362975f2d179f9e26928c5b517524e89dd48530a0202570d55ad6ca5d8a56f"}, + {file = "greenlet-3.0.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:649dde7de1a5eceb258f9cb00bdf50e978c9db1b996964cd80703614c86495eb"}, + {file = "greenlet-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:68834da854554926fbedd38c76e60c4a2e3198c6fbed520b106a8986445caaf9"}, + {file = "greenlet-3.0.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:b1b5667cced97081bf57b8fa1d6bfca67814b0afd38208d52538316e9422fc61"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52f59dd9c96ad2fc0d5724107444f76eb20aaccb675bf825df6435acb7703559"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:afaff6cf5200befd5cec055b07d1c0a5a06c040fe5ad148abcd11ba6ab9b114e"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe754d231288e1e64323cfad462fcee8f0288654c10bdf4f603a39ed923bef33"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2797aa5aedac23af156bbb5a6aa2cd3427ada2972c828244eb7d1b9255846379"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7f009caad047246ed379e1c4dbcb8b020f0a390667ea74d2387be2998f58a22"}, + {file = "greenlet-3.0.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c5e1536de2aad7bf62e27baf79225d0d64360d4168cf2e6becb91baf1ed074f3"}, + {file = "greenlet-3.0.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:894393ce10ceac937e56ec00bb71c4c2f8209ad516e96033e4b3b1de270e200d"}, + {file = "greenlet-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:1ea188d4f49089fc6fb283845ab18a2518d279c7cd9da1065d7a84e991748728"}, + {file = "greenlet-3.0.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:70fb482fdf2c707765ab5f0b6655e9cfcf3780d8d87355a063547b41177599be"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4d1ac74f5c0c0524e4a24335350edad7e5f03b9532da7ea4d3c54d527784f2e"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:149e94a2dd82d19838fe4b2259f1b6b9957d5ba1b25640d2380bea9c5df37676"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15d79dd26056573940fcb8c7413d84118086f2ec1a8acdfa854631084393efcc"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b7db1ebff4ba09aaaeae6aa491daeb226c8150fc20e836ad00041bcb11230"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fcd2469d6a2cf298f198f0487e0a5b1a47a42ca0fa4dfd1b6862c999f018ebbf"}, + {file = "greenlet-3.0.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1f672519db1796ca0d8753f9e78ec02355e862d0998193038c7073045899f305"}, + {file = "greenlet-3.0.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2516a9957eed41dd8f1ec0c604f1cdc86758b587d964668b5b196a9db5bfcde6"}, + {file = "greenlet-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:bba5387a6975598857d86de9eac14210a49d554a77eb8261cc68b7d082f78ce2"}, + {file = "greenlet-3.0.3-cp37-cp37m-macosx_11_0_universal2.whl", hash = "sha256:5b51e85cb5ceda94e79d019ed36b35386e8c37d22f07d6a751cb659b180d5274"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:daf3cb43b7cf2ba96d614252ce1684c1bccee6b2183a01328c98d36fcd7d5cb0"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99bf650dc5d69546e076f413a87481ee1d2d09aaaaaca058c9251b6d8c14783f"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2dd6e660effd852586b6a8478a1d244b8dc90ab5b1321751d2ea15deb49ed414"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3391d1e16e2a5a1507d83e4a8b100f4ee626e8eca43cf2cadb543de69827c4c"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1f145462f1fa6e4a4ae3c0f782e580ce44d57c8f2c7aae1b6fa88c0b2efdb41"}, + {file = "greenlet-3.0.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1a7191e42732df52cb5f39d3527217e7ab73cae2cb3694d241e18f53d84ea9a7"}, + {file = "greenlet-3.0.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0448abc479fab28b00cb472d278828b3ccca164531daab4e970a0458786055d6"}, + {file = "greenlet-3.0.3-cp37-cp37m-win32.whl", hash = "sha256:b542be2440edc2d48547b5923c408cbe0fc94afb9f18741faa6ae970dbcb9b6d"}, + {file = "greenlet-3.0.3-cp37-cp37m-win_amd64.whl", hash = "sha256:01bc7ea167cf943b4c802068e178bbf70ae2e8c080467070d01bfa02f337ee67"}, + {file = "greenlet-3.0.3-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:1996cb9306c8595335bb157d133daf5cf9f693ef413e7673cb07e3e5871379ca"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ddc0f794e6ad661e321caa8d2f0a55ce01213c74722587256fb6566049a8b04"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9db1c18f0eaad2f804728c67d6c610778456e3e1cc4ab4bbd5eeb8e6053c6fc"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7170375bcc99f1a2fbd9c306f5be8764eaf3ac6b5cb968862cad4c7057756506"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b66c9c1e7ccabad3a7d037b2bcb740122a7b17a53734b7d72a344ce39882a1b"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:098d86f528c855ead3479afe84b49242e174ed262456c342d70fc7f972bc13c4"}, + {file = "greenlet-3.0.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:81bb9c6d52e8321f09c3d165b2a78c680506d9af285bfccbad9fb7ad5a5da3e5"}, + {file = "greenlet-3.0.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fd096eb7ffef17c456cfa587523c5f92321ae02427ff955bebe9e3c63bc9f0da"}, + {file = "greenlet-3.0.3-cp38-cp38-win32.whl", hash = "sha256:d46677c85c5ba00a9cb6f7a00b2bfa6f812192d2c9f7d9c4f6a55b60216712f3"}, + {file = "greenlet-3.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:419b386f84949bf0e7c73e6032e3457b82a787c1ab4a0e43732898a761cc9dbf"}, + {file = "greenlet-3.0.3-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:da70d4d51c8b306bb7a031d5cff6cc25ad253affe89b70352af5f1cb68e74b53"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:086152f8fbc5955df88382e8a75984e2bb1c892ad2e3c80a2508954e52295257"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d73a9fe764d77f87f8ec26a0c85144d6a951a6c438dfe50487df5595c6373eac"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7dcbe92cc99f08c8dd11f930de4d99ef756c3591a5377d1d9cd7dd5e896da71"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1551a8195c0d4a68fac7a4325efac0d541b48def35feb49d803674ac32582f61"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:64d7675ad83578e3fc149b617a444fab8efdafc9385471f868eb5ff83e446b8b"}, + {file = "greenlet-3.0.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b37eef18ea55f2ffd8f00ff8fe7c8d3818abd3e25fb73fae2ca3b672e333a7a6"}, + {file = "greenlet-3.0.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:77457465d89b8263bca14759d7c1684df840b6811b2499838cc5b040a8b5b113"}, + {file = "greenlet-3.0.3-cp39-cp39-win32.whl", hash = "sha256:57e8974f23e47dac22b83436bdcf23080ade568ce77df33159e019d161ce1d1e"}, + {file = "greenlet-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:c5ee858cfe08f34712f548c3c363e807e7186f03ad7a5039ebadb29e8c6be067"}, + {file = "greenlet-3.0.3.tar.gz", hash = "sha256:43374442353259554ce33599da8b692d5aa96f8976d567d4badf263371fbe491"}, +] + +[package.extras] +docs = ["Sphinx", "furo"] +test = ["objgraph", "psutil"] + +[[package]] +name = "gunicorn" +version = "20.1.0" +description = "WSGI HTTP Server for UNIX" +optional = false +python-versions = ">=3.5" +files = [ + {file = "gunicorn-20.1.0-py3-none-any.whl", hash = "sha256:9dcc4547dbb1cb284accfb15ab5667a0e5d1881cc443e0677b4882a4067a807e"}, + {file = "gunicorn-20.1.0.tar.gz", hash = "sha256:e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8"}, +] + +[package.dependencies] +setuptools = ">=3.0" + +[package.extras] +eventlet = ["eventlet (>=0.24.1)"] +gevent = ["gevent (>=1.4.0)"] +setproctitle = ["setproctitle"] +tornado = ["tornado (>=0.2)"] + +[[package]] +name = "idna" +version = "3.4" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.5" +files = [ + {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, + {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, +] + +[[package]] +name = "imagesize" +version = "1.4.1" +description = "Getting image size from png/jpeg/jpeg2000/gif file" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, + {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "invoke" +version = "1.7.3" +description = "Pythonic task execution" +optional = false +python-versions = "*" +files = [ + {file = "invoke-1.7.3-py3-none-any.whl", hash = "sha256:d9694a865764dd3fd91f25f7e9a97fb41666e822bbb00e670091e3f43933574d"}, + {file = "invoke-1.7.3.tar.gz", hash = "sha256:41b428342d466a82135d5ab37119685a989713742be46e42a3a399d685579314"}, +] + +[[package]] +name = "isort" +version = "5.12.0" +description = "A Python utility / library to sort Python imports." +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "isort-5.12.0-py3-none-any.whl", hash = "sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6"}, + {file = "isort-5.12.0.tar.gz", hash = "sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504"}, +] + +[package.extras] +colors = ["colorama (>=0.4.3)"] +pipfile-deprecated-finder = ["pip-shims (>=0.5.2)", "pipreqs", "requirementslib"] +plugins = ["setuptools"] +requirements-deprecated-finder = ["pip-api", "pipreqs"] + +[[package]] +name = "jinja2" +version = "3.1.2" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +files = [ + {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, + {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "jmespath" +version = "1.0.1" +description = "JSON Matching Expressions" +optional = false +python-versions = ">=3.7" +files = [ + {file = "jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980"}, + {file = "jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe"}, +] + +[[package]] +name = "markupsafe" +version = "2.1.2" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.7" +files = [ + {file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:665a36ae6f8f20a4676b53224e33d456a6f5a72657d9c83c2aa00765072f31f7"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:340bea174e9761308703ae988e982005aedf427de816d1afe98147668cc03036"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22152d00bf4a9c7c83960521fc558f55a1adbc0631fbb00a9471e097b19d72e1"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28057e985dace2f478e042eaa15606c7efccb700797660629da387eb289b9323"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca244fa73f50a800cf8c3ebf7fd93149ec37f5cb9596aa8873ae2c1d23498601"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d9d971ec1e79906046aa3ca266de79eac42f1dbf3612a05dc9368125952bd1a1"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7e007132af78ea9df29495dbf7b5824cb71648d7133cf7848a2a5dd00d36f9ff"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7313ce6a199651c4ed9d7e4cfb4aa56fe923b1adf9af3b420ee14e6d9a73df65"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-win32.whl", hash = "sha256:c4a549890a45f57f1ebf99c067a4ad0cb423a05544accaf2b065246827ed9603"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:835fb5e38fd89328e9c81067fd642b3593c33e1e17e2fdbf77f5676abb14a156"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2ec4f2d48ae59bbb9d1f9d7efb9236ab81429a764dedca114f5fdabbc3788013"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:608e7073dfa9e38a85d38474c082d4281f4ce276ac0010224eaba11e929dd53a"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65608c35bfb8a76763f37036547f7adfd09270fbdbf96608be2bead319728fcd"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da25303d91526aac3672ee6d49a2f3db2d9502a4a60b55519feb1a4c7714e07d"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9cad97ab29dfc3f0249b483412c85c8ef4766d96cdf9dcf5a1e3caa3f3661cf1"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1bea30e9bf331f3fef67e0a3877b2288593c98a21ccb2cf29b74c581a4eb3af0"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-win32.whl", hash = "sha256:7df70907e00c970c60b9ef2938d894a9381f38e6b9db73c5be35e59d92e06625"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:e55e40ff0cc8cc5c07996915ad367fa47da6b3fc091fdadca7f5403239c5fec3"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a6e40afa7f45939ca356f348c8e23048e02cb109ced1eb8420961b2f40fb373a"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf877ab4ed6e302ec1d04952ca358b381a882fbd9d1b07cccbfd61783561f98a"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63ba06c9941e46fa389d389644e2d8225e0e3e5ebcc4ff1ea8506dce646f8c8a"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f1cd098434e83e656abf198f103a8207a8187c0fc110306691a2e94a78d0abb2"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:a6f2fcca746e8d5910e18782f976489939d54a91f9411c32051b4aab2bd7c513"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0b462104ba25f1ac006fdab8b6a01ebbfbce9ed37fd37fd4acd70c67c973e460"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-win32.whl", hash = "sha256:7668b52e102d0ed87cb082380a7e2e1e78737ddecdde129acadb0eccc5423859"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6d6607f98fcf17e534162f0709aaad3ab7a96032723d8ac8750ffe17ae5a0666"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a806db027852538d2ad7555b203300173dd1b77ba116de92da9afbc3a3be3eed"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a4abaec6ca3ad8660690236d11bfe28dfd707778e2442b45addd2f086d6ef094"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f03a532d7dee1bed20bc4884194a16160a2de9ffc6354b3878ec9682bb623c54"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4cf06cdc1dda95223e9d2d3c58d3b178aa5dacb35ee7e3bbac10e4e1faacb419"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22731d79ed2eb25059ae3df1dfc9cb1546691cc41f4e3130fe6bfbc3ecbbecfa"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f8ffb705ffcf5ddd0e80b65ddf7bed7ee4f5a441ea7d3419e861a12eaf41af58"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8db032bf0ce9022a8e41a22598eefc802314e81b879ae093f36ce9ddf39ab1ba"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2298c859cfc5463f1b64bd55cb3e602528db6fa0f3cfd568d3605c50678f8f03"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-win32.whl", hash = "sha256:50c42830a633fa0cf9e7d27664637532791bfc31c731a87b202d2d8ac40c3ea2"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:bb06feb762bade6bf3c8b844462274db0c76acc95c52abe8dbed28ae3d44a147"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:99625a92da8229df6d44335e6fcc558a5037dd0a760e11d84be2260e6f37002f"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8bca7e26c1dd751236cfb0c6c72d4ad61d986e9a41bbf76cb445f69488b2a2bd"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40dfd3fefbef579ee058f139733ac336312663c6706d1163b82b3003fb1925c4"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2e7821bffe00aa6bd07a23913b7f4e01328c3d5cc0b40b36c0bd81d362faeb65"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c0a33bc9f02c2b17c3ea382f91b4db0e6cde90b63b296422a939886a7a80de1c"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b8526c6d437855442cdd3d87eede9c425c4445ea011ca38d937db299382e6fa3"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-win32.whl", hash = "sha256:137678c63c977754abe9086a3ec011e8fd985ab90631145dfb9294ad09c102a7"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed"}, + {file = "MarkupSafe-2.1.2.tar.gz", hash = "sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d"}, +] + +[[package]] +name = "mccabe" +version = "0.7.0" +description = "McCabe checker, plugin for flake8" +optional = false +python-versions = ">=3.6" +files = [ + {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, + {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, +] + +[[package]] +name = "mypy-extensions" +version = "0.4.3" +description = "Experimental type system extensions for programs checked with the mypy typechecker." +optional = false +python-versions = "*" +files = [ + {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, + {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, +] + +[[package]] +name = "packaging" +version = "23.0" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.7" +files = [ + {file = "packaging-23.0-py3-none-any.whl", hash = "sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2"}, + {file = "packaging-23.0.tar.gz", hash = "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"}, +] + +[[package]] +name = "pathspec" +version = "0.11.0" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.7" +files = [ + {file = "pathspec-0.11.0-py3-none-any.whl", hash = "sha256:3a66eb970cbac598f9e5ccb5b2cf58930cd8e3ed86d393d541eaf2d8b1705229"}, + {file = "pathspec-0.11.0.tar.gz", hash = "sha256:64d338d4e0914e91c1792321e6907b5a593f1ab1851de7fc269557a21b30ebbc"}, +] + +[[package]] +name = "pillow" +version = "9.4.0" +description = "Python Imaging Library (Fork)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "Pillow-9.4.0-1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1b4b4e9dda4f4e4c4e6896f93e84a8f0bcca3b059de9ddf67dac3c334b1195e1"}, + {file = "Pillow-9.4.0-1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:fb5c1ad6bad98c57482236a21bf985ab0ef42bd51f7ad4e4538e89a997624e12"}, + {file = "Pillow-9.4.0-1-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:f0caf4a5dcf610d96c3bd32932bfac8aee61c96e60481c2a0ea58da435e25acd"}, + {file = "Pillow-9.4.0-1-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:3f4cc516e0b264c8d4ccd6b6cbc69a07c6d582d8337df79be1e15a5056b258c9"}, + {file = "Pillow-9.4.0-1-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:b8c2f6eb0df979ee99433d8b3f6d193d9590f735cf12274c108bd954e30ca858"}, + {file = "Pillow-9.4.0-1-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:b70756ec9417c34e097f987b4d8c510975216ad26ba6e57ccb53bc758f490dab"}, + {file = "Pillow-9.4.0-1-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:43521ce2c4b865d385e78579a082b6ad1166ebed2b1a2293c3be1d68dd7ca3b9"}, + {file = "Pillow-9.4.0-2-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:9d9a62576b68cd90f7075876f4e8444487db5eeea0e4df3ba298ee38a8d067b0"}, + {file = "Pillow-9.4.0-2-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:87708d78a14d56a990fbf4f9cb350b7d89ee8988705e58e39bdf4d82c149210f"}, + {file = "Pillow-9.4.0-2-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:8a2b5874d17e72dfb80d917213abd55d7e1ed2479f38f001f264f7ce7bae757c"}, + {file = "Pillow-9.4.0-2-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:83125753a60cfc8c412de5896d10a0a405e0bd88d0470ad82e0869ddf0cb3848"}, + {file = "Pillow-9.4.0-2-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:9e5f94742033898bfe84c93c831a6f552bb629448d4072dd312306bab3bd96f1"}, + {file = "Pillow-9.4.0-2-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:013016af6b3a12a2f40b704677f8b51f72cb007dac785a9933d5c86a72a7fe33"}, + {file = "Pillow-9.4.0-2-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:99d92d148dd03fd19d16175b6d355cc1b01faf80dae93c6c3eb4163709edc0a9"}, + {file = "Pillow-9.4.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:2968c58feca624bb6c8502f9564dd187d0e1389964898f5e9e1fbc8533169157"}, + {file = "Pillow-9.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c5c1362c14aee73f50143d74389b2c158707b4abce2cb055b7ad37ce60738d47"}, + {file = "Pillow-9.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd752c5ff1b4a870b7661234694f24b1d2b9076b8bf337321a814c612665f343"}, + {file = "Pillow-9.4.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9a3049a10261d7f2b6514d35bbb7a4dfc3ece4c4de14ef5876c4b7a23a0e566d"}, + {file = "Pillow-9.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16a8df99701f9095bea8a6c4b3197da105df6f74e6176c5b410bc2df2fd29a57"}, + {file = "Pillow-9.4.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:94cdff45173b1919350601f82d61365e792895e3c3a3443cf99819e6fbf717a5"}, + {file = "Pillow-9.4.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:ed3e4b4e1e6de75fdc16d3259098de7c6571b1a6cc863b1a49e7d3d53e036070"}, + {file = "Pillow-9.4.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d5b2f8a31bd43e0f18172d8ac82347c8f37ef3e0b414431157718aa234991b28"}, + {file = "Pillow-9.4.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:09b89ddc95c248ee788328528e6a2996e09eaccddeeb82a5356e92645733be35"}, + {file = "Pillow-9.4.0-cp310-cp310-win32.whl", hash = "sha256:f09598b416ba39a8f489c124447b007fe865f786a89dbfa48bb5cf395693132a"}, + {file = "Pillow-9.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:f6e78171be3fb7941f9910ea15b4b14ec27725865a73c15277bc39f5ca4f8391"}, + {file = "Pillow-9.4.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:3fa1284762aacca6dc97474ee9c16f83990b8eeb6697f2ba17140d54b453e133"}, + {file = "Pillow-9.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:eaef5d2de3c7e9b21f1e762f289d17b726c2239a42b11e25446abf82b26ac132"}, + {file = "Pillow-9.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a4dfdae195335abb4e89cc9762b2edc524f3c6e80d647a9a81bf81e17e3fb6f0"}, + {file = "Pillow-9.4.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6abfb51a82e919e3933eb137e17c4ae9c0475a25508ea88993bb59faf82f3b35"}, + {file = "Pillow-9.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:451f10ef963918e65b8869e17d67db5e2f4ab40e716ee6ce7129b0cde2876eab"}, + {file = "Pillow-9.4.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:6663977496d616b618b6cfa43ec86e479ee62b942e1da76a2c3daa1c75933ef4"}, + {file = "Pillow-9.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:60e7da3a3ad1812c128750fc1bc14a7ceeb8d29f77e0a2356a8fb2aa8925287d"}, + {file = "Pillow-9.4.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:19005a8e58b7c1796bc0167862b1f54a64d3b44ee5d48152b06bb861458bc0f8"}, + {file = "Pillow-9.4.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f715c32e774a60a337b2bb8ad9839b4abf75b267a0f18806f6f4f5f1688c4b5a"}, + {file = "Pillow-9.4.0-cp311-cp311-win32.whl", hash = "sha256:b222090c455d6d1a64e6b7bb5f4035c4dff479e22455c9eaa1bdd4c75b52c80c"}, + {file = "Pillow-9.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:ba6612b6548220ff5e9df85261bddc811a057b0b465a1226b39bfb8550616aee"}, + {file = "Pillow-9.4.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:5f532a2ad4d174eb73494e7397988e22bf427f91acc8e6ebf5bb10597b49c493"}, + {file = "Pillow-9.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dd5a9c3091a0f414a963d427f920368e2b6a4c2f7527fdd82cde8ef0bc7a327"}, + {file = "Pillow-9.4.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef21af928e807f10bf4141cad4746eee692a0dd3ff56cfb25fce076ec3cc8abe"}, + {file = "Pillow-9.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:847b114580c5cc9ebaf216dd8c8dbc6b00a3b7ab0131e173d7120e6deade1f57"}, + {file = "Pillow-9.4.0-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:653d7fb2df65efefbcbf81ef5fe5e5be931f1ee4332c2893ca638c9b11a409c4"}, + {file = "Pillow-9.4.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:46f39cab8bbf4a384ba7cb0bc8bae7b7062b6a11cfac1ca4bc144dea90d4a9f5"}, + {file = "Pillow-9.4.0-cp37-cp37m-win32.whl", hash = "sha256:7ac7594397698f77bce84382929747130765f66406dc2cd8b4ab4da68ade4c6e"}, + {file = "Pillow-9.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:46c259e87199041583658457372a183636ae8cd56dbf3f0755e0f376a7f9d0e6"}, + {file = "Pillow-9.4.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:0e51f608da093e5d9038c592b5b575cadc12fd748af1479b5e858045fff955a9"}, + {file = "Pillow-9.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:765cb54c0b8724a7c12c55146ae4647e0274a839fb6de7bcba841e04298e1011"}, + {file = "Pillow-9.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:519e14e2c49fcf7616d6d2cfc5c70adae95682ae20f0395e9280db85e8d6c4df"}, + {file = "Pillow-9.4.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d197df5489004db87d90b918033edbeee0bd6df3848a204bca3ff0a903bef837"}, + {file = "Pillow-9.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0845adc64fe9886db00f5ab68c4a8cd933ab749a87747555cec1c95acea64b0b"}, + {file = "Pillow-9.4.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:e1339790c083c5a4de48f688b4841f18df839eb3c9584a770cbd818b33e26d5d"}, + {file = "Pillow-9.4.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:a96e6e23f2b79433390273eaf8cc94fec9c6370842e577ab10dabdcc7ea0a66b"}, + {file = "Pillow-9.4.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:7cfc287da09f9d2a7ec146ee4d72d6ea1342e770d975e49a8621bf54eaa8f30f"}, + {file = "Pillow-9.4.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d7081c084ceb58278dd3cf81f836bc818978c0ccc770cbbb202125ddabec6628"}, + {file = "Pillow-9.4.0-cp38-cp38-win32.whl", hash = "sha256:df41112ccce5d47770a0c13651479fbcd8793f34232a2dd9faeccb75eb5d0d0d"}, + {file = "Pillow-9.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:7a21222644ab69ddd9967cfe6f2bb420b460dae4289c9d40ff9a4896e7c35c9a"}, + {file = "Pillow-9.4.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:0f3269304c1a7ce82f1759c12ce731ef9b6e95b6df829dccd9fe42912cc48569"}, + {file = "Pillow-9.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cb362e3b0976dc994857391b776ddaa8c13c28a16f80ac6522c23d5257156bed"}, + {file = "Pillow-9.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a2e0f87144fcbbe54297cae708c5e7f9da21a4646523456b00cc956bd4c65815"}, + {file = "Pillow-9.4.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:28676836c7796805914b76b1837a40f76827ee0d5398f72f7dcc634bae7c6264"}, + {file = "Pillow-9.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0884ba7b515163a1a05440a138adeb722b8a6ae2c2b33aea93ea3118dd3a899e"}, + {file = "Pillow-9.4.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:53dcb50fbdc3fb2c55431a9b30caeb2f7027fcd2aeb501459464f0214200a503"}, + {file = "Pillow-9.4.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:e8c5cf126889a4de385c02a2c3d3aba4b00f70234bfddae82a5eaa3ee6d5e3e6"}, + {file = "Pillow-9.4.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6c6b1389ed66cdd174d040105123a5a1bc91d0aa7059c7261d20e583b6d8cbd2"}, + {file = "Pillow-9.4.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0dd4c681b82214b36273c18ca7ee87065a50e013112eea7d78c7a1b89a739153"}, + {file = "Pillow-9.4.0-cp39-cp39-win32.whl", hash = "sha256:6d9dfb9959a3b0039ee06c1a1a90dc23bac3b430842dcb97908ddde05870601c"}, + {file = "Pillow-9.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:54614444887e0d3043557d9dbc697dbb16cfb5a35d672b7a0fcc1ed0cf1c600b"}, + {file = "Pillow-9.4.0-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:b9b752ab91e78234941e44abdecc07f1f0d8f51fb62941d32995b8161f68cfe5"}, + {file = "Pillow-9.4.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d3b56206244dc8711f7e8b7d6cad4663917cd5b2d950799425076681e8766286"}, + {file = "Pillow-9.4.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aabdab8ec1e7ca7f1434d042bf8b1e92056245fb179790dc97ed040361f16bfd"}, + {file = "Pillow-9.4.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:db74f5562c09953b2c5f8ec4b7dfd3f5421f31811e97d1dbc0a7c93d6e3a24df"}, + {file = "Pillow-9.4.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:e9d7747847c53a16a729b6ee5e737cf170f7a16611c143d95aa60a109a59c336"}, + {file = "Pillow-9.4.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:b52ff4f4e002f828ea6483faf4c4e8deea8d743cf801b74910243c58acc6eda3"}, + {file = "Pillow-9.4.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:575d8912dca808edd9acd6f7795199332696d3469665ef26163cd090fa1f8bfa"}, + {file = "Pillow-9.4.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3c4ed2ff6760e98d262e0cc9c9a7f7b8a9f61aa4d47c58835cdaf7b0b8811bb"}, + {file = "Pillow-9.4.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e621b0246192d3b9cb1dc62c78cfa4c6f6d2ddc0ec207d43c0dedecb914f152a"}, + {file = "Pillow-9.4.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:8f127e7b028900421cad64f51f75c051b628db17fb00e099eb148761eed598c9"}, + {file = "Pillow-9.4.0.tar.gz", hash = "sha256:a1c2d7780448eb93fbcc3789bf3916aa5720d942e37945f4056680317f1cd23e"}, +] + +[package.extras] +docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-issues (>=3.0.1)", "sphinx-removed-in", "sphinxext-opengraph"] +tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] + +[[package]] +name = "platformdirs" +version = "2.6.2" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +optional = false +python-versions = ">=3.7" +files = [ + {file = "platformdirs-2.6.2-py3-none-any.whl", hash = "sha256:83c8f6d04389165de7c9b6f0c682439697887bca0aa2f1c87ef1826be3584490"}, + {file = "platformdirs-2.6.2.tar.gz", hash = "sha256:e1fea1fe471b9ff8332e229df3cb7de4f53eeea4998d3b6bfff542115e998bd2"}, +] + +[package.extras] +docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.5)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.2.2)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] + +[[package]] +name = "playwright" +version = "1.41.2" +description = "A high-level API to automate web browsers" +optional = false +python-versions = ">=3.8" +files = [ + {file = "playwright-1.41.2-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:cf68335a5dfa4038fa797a4ba0105faee0094ebbb372547d7a27feec5b23c672"}, + {file = "playwright-1.41.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:431e3a05f8c99147995e2b3e8475d07818745294fd99f1510b61756e73bdcf68"}, + {file = "playwright-1.41.2-py3-none-macosx_11_0_universal2.whl", hash = "sha256:0608717cbf291a625ba6f751061af0fc0cc9bdace217e69d87b1eb1383b03406"}, + {file = "playwright-1.41.2-py3-none-manylinux1_x86_64.whl", hash = "sha256:4bf214d812092cf5b9b9648ba84611aa35e28685519911342a7da3a3031f9ed6"}, + {file = "playwright-1.41.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eaa17ab44622c447de26ed8f7d99912719568d8dbc3a9db0e07f0ae1487709d9"}, + {file = "playwright-1.41.2-py3-none-win32.whl", hash = "sha256:edb210a015e70bb0d328bf1c9b65fa3a08361f33e4d7c4ddd1ad2adb6d9b4479"}, + {file = "playwright-1.41.2-py3-none-win_amd64.whl", hash = "sha256:71ead0f33e00f5a8533c037c647938b99f219436a1b27d4ba4de4e6bf0567278"}, +] + +[package.dependencies] +greenlet = "3.0.3" +pyee = "11.0.1" + +[[package]] +name = "pluggy" +version = "1.0.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.6" +files = [ + {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, + {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "psycopg2-binary" +version = "2.9.5" +description = "psycopg2 - Python-PostgreSQL Database Adapter" +optional = false +python-versions = ">=3.6" +files = [ + {file = "psycopg2-binary-2.9.5.tar.gz", hash = "sha256:33e632d0885b95a8b97165899006c40e9ecdc634a529dca7b991eb7de4ece41c"}, + {file = "psycopg2_binary-2.9.5-cp310-cp310-macosx_10_15_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:0775d6252ccb22b15da3b5d7adbbf8cfe284916b14b6dc0ff503a23edb01ee85"}, + {file = "psycopg2_binary-2.9.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2ec46ed947801652c9643e0b1dc334cfb2781232e375ba97312c2fc256597632"}, + {file = "psycopg2_binary-2.9.5-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3520d7af1ebc838cc6084a3281145d5cd5bdd43fdef139e6db5af01b92596cb7"}, + {file = "psycopg2_binary-2.9.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5cbc554ba47ecca8cd3396ddaca85e1ecfe3e48dd57dc5e415e59551affe568e"}, + {file = "psycopg2_binary-2.9.5-cp310-cp310-manylinux_2_24_aarch64.whl", hash = "sha256:5d28ecdf191db558d0c07d0f16524ee9d67896edf2b7990eea800abeb23ebd61"}, + {file = "psycopg2_binary-2.9.5-cp310-cp310-manylinux_2_24_ppc64le.whl", hash = "sha256:b9c33d4aef08dfecbd1736ceab8b7b3c4358bf10a0121483e5cd60d3d308cc64"}, + {file = "psycopg2_binary-2.9.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:05b3d479425e047c848b9782cd7aac9c6727ce23181eb9647baf64ffdfc3da41"}, + {file = "psycopg2_binary-2.9.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:1e491e6489a6cb1d079df8eaa15957c277fdedb102b6a68cfbf40c4994412fd0"}, + {file = "psycopg2_binary-2.9.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:9e32cedc389bcb76d9f24ea8a012b3cb8385ee362ea437e1d012ffaed106c17d"}, + {file = "psycopg2_binary-2.9.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:46850a640df62ae940e34a163f72e26aca1f88e2da79148e1862faaac985c302"}, + {file = "psycopg2_binary-2.9.5-cp310-cp310-win32.whl", hash = "sha256:3d790f84201c3698d1bfb404c917f36e40531577a6dda02e45ba29b64d539867"}, + {file = "psycopg2_binary-2.9.5-cp310-cp310-win_amd64.whl", hash = "sha256:1764546ffeaed4f9428707be61d68972eb5ede81239b46a45843e0071104d0dd"}, + {file = "psycopg2_binary-2.9.5-cp311-cp311-macosx_10_9_universal2.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:426c2ae999135d64e6a18849a7d1ad0e1bd007277e4a8f4752eaa40a96b550ff"}, + {file = "psycopg2_binary-2.9.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7cf1d44e710ca3a9ce952bda2855830fe9f9017ed6259e01fcd71ea6287565f5"}, + {file = "psycopg2_binary-2.9.5-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:024030b13bdcbd53d8a93891a2cf07719715724fc9fee40243f3bd78b4264b8f"}, + {file = "psycopg2_binary-2.9.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bcda1c84a1c533c528356da5490d464a139b6e84eb77cc0b432e38c5c6dd7882"}, + {file = "psycopg2_binary-2.9.5-cp311-cp311-manylinux_2_24_aarch64.whl", hash = "sha256:2ef892cabdccefe577088a79580301f09f2a713eb239f4f9f62b2b29cafb0577"}, + {file = "psycopg2_binary-2.9.5-cp311-cp311-manylinux_2_24_ppc64le.whl", hash = "sha256:af0516e1711995cb08dc19bbd05bec7dbdebf4185f68870595156718d237df3e"}, + {file = "psycopg2_binary-2.9.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e72c91bda9880f097c8aa3601a2c0de6c708763ba8128006151f496ca9065935"}, + {file = "psycopg2_binary-2.9.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:e67b3c26e9b6d37b370c83aa790bbc121775c57bfb096c2e77eacca25fd0233b"}, + {file = "psycopg2_binary-2.9.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:5fc447058d083b8c6ac076fc26b446d44f0145308465d745fba93a28c14c9e32"}, + {file = "psycopg2_binary-2.9.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d892bfa1d023c3781a3cab8dd5af76b626c483484d782e8bd047c180db590e4c"}, + {file = "psycopg2_binary-2.9.5-cp311-cp311-win32.whl", hash = "sha256:2abccab84d057723d2ca8f99ff7b619285d40da6814d50366f61f0fc385c3903"}, + {file = "psycopg2_binary-2.9.5-cp311-cp311-win_amd64.whl", hash = "sha256:bef7e3f9dc6f0c13afdd671008534be5744e0e682fb851584c8c3a025ec09720"}, + {file = "psycopg2_binary-2.9.5-cp36-cp36m-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:6e63814ec71db9bdb42905c925639f319c80e7909fb76c3b84edc79dadef8d60"}, + {file = "psycopg2_binary-2.9.5-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:212757ffcecb3e1a5338d4e6761bf9c04f750e7d027117e74aa3cd8a75bb6fbd"}, + {file = "psycopg2_binary-2.9.5-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f8a9bcab7b6db2e3dbf65b214dfc795b4c6b3bb3af922901b6a67f7cb47d5f8"}, + {file = "psycopg2_binary-2.9.5-cp36-cp36m-manylinux_2_24_aarch64.whl", hash = "sha256:56b2957a145f816726b109ee3d4e6822c23f919a7d91af5a94593723ed667835"}, + {file = "psycopg2_binary-2.9.5-cp36-cp36m-manylinux_2_24_ppc64le.whl", hash = "sha256:f95b8aca2703d6a30249f83f4fe6a9abf2e627aa892a5caaab2267d56be7ab69"}, + {file = "psycopg2_binary-2.9.5-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:70831e03bd53702c941da1a1ad36c17d825a24fbb26857b40913d58df82ec18b"}, + {file = "psycopg2_binary-2.9.5-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:dbc332beaf8492b5731229a881807cd7b91b50dbbbaf7fe2faf46942eda64a24"}, + {file = "psycopg2_binary-2.9.5-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:2d964eb24c8b021623df1c93c626671420c6efadbdb8655cb2bd5e0c6fa422ba"}, + {file = "psycopg2_binary-2.9.5-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:95076399ec3b27a8f7fa1cc9a83417b1c920d55cf7a97f718a94efbb96c7f503"}, + {file = "psycopg2_binary-2.9.5-cp36-cp36m-win32.whl", hash = "sha256:3fc33295cfccad697a97a76dec3f1e94ad848b7b163c3228c1636977966b51e2"}, + {file = "psycopg2_binary-2.9.5-cp36-cp36m-win_amd64.whl", hash = "sha256:02551647542f2bf89073d129c73c05a25c372fc0a49aa50e0de65c3c143d8bd0"}, + {file = "psycopg2_binary-2.9.5-cp37-cp37m-macosx_10_15_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:63e318dbe52709ed10d516a356f22a635e07a2e34c68145484ed96a19b0c4c68"}, + {file = "psycopg2_binary-2.9.5-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7e518a0911c50f60313cb9e74a169a65b5d293770db4770ebf004245f24b5c5"}, + {file = "psycopg2_binary-2.9.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9d38a4656e4e715d637abdf7296e98d6267df0cc0a8e9a016f8ba07e4aa3eeb"}, + {file = "psycopg2_binary-2.9.5-cp37-cp37m-manylinux_2_24_aarch64.whl", hash = "sha256:68d81a2fe184030aa0c5c11e518292e15d342a667184d91e30644c9d533e53e1"}, + {file = "psycopg2_binary-2.9.5-cp37-cp37m-manylinux_2_24_ppc64le.whl", hash = "sha256:7ee3095d02d6f38bd7d9a5358fcc9ea78fcdb7176921528dd709cc63f40184f5"}, + {file = "psycopg2_binary-2.9.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:46512486be6fbceef51d7660dec017394ba3e170299d1dc30928cbedebbf103a"}, + {file = "psycopg2_binary-2.9.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b911dfb727e247340d36ae20c4b9259e4a64013ab9888ccb3cbba69b77fd9636"}, + {file = "psycopg2_binary-2.9.5-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:422e3d43b47ac20141bc84b3d342eead8d8099a62881a501e97d15f6addabfe9"}, + {file = "psycopg2_binary-2.9.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c5682a45df7d9642eff590abc73157c887a68f016df0a8ad722dcc0f888f56d7"}, + {file = "psycopg2_binary-2.9.5-cp37-cp37m-win32.whl", hash = "sha256:b8104f709590fff72af801e916817560dbe1698028cd0afe5a52d75ceb1fce5f"}, + {file = "psycopg2_binary-2.9.5-cp37-cp37m-win_amd64.whl", hash = "sha256:7b3751857da3e224f5629400736a7b11e940b5da5f95fa631d86219a1beaafec"}, + {file = "psycopg2_binary-2.9.5-cp38-cp38-macosx_10_15_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:043a9fd45a03858ff72364b4b75090679bd875ee44df9c0613dc862ca6b98460"}, + {file = "psycopg2_binary-2.9.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9ffdc51001136b699f9563b1c74cc1f8c07f66ef7219beb6417a4c8aaa896c28"}, + {file = "psycopg2_binary-2.9.5-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c15ba5982c177bc4b23a7940c7e4394197e2d6a424a2d282e7c236b66da6d896"}, + {file = "psycopg2_binary-2.9.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc85b3777068ed30aff8242be2813038a929f2084f69e43ef869daddae50f6ee"}, + {file = "psycopg2_binary-2.9.5-cp38-cp38-manylinux_2_24_aarch64.whl", hash = "sha256:215d6bf7e66732a514f47614f828d8c0aaac9a648c46a831955cb103473c7147"}, + {file = "psycopg2_binary-2.9.5-cp38-cp38-manylinux_2_24_ppc64le.whl", hash = "sha256:7d07f552d1e412f4b4e64ce386d4c777a41da3b33f7098b6219012ba534fb2c2"}, + {file = "psycopg2_binary-2.9.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a0adef094c49f242122bb145c3c8af442070dc0e4312db17e49058c1702606d4"}, + {file = "psycopg2_binary-2.9.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:00475004e5ed3e3bf5e056d66e5dcdf41a0dc62efcd57997acd9135c40a08a50"}, + {file = "psycopg2_binary-2.9.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:7d88db096fa19d94f433420eaaf9f3c45382da2dd014b93e4bf3215639047c16"}, + {file = "psycopg2_binary-2.9.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:902844f9c4fb19b17dfa84d9e2ca053d4a4ba265723d62ea5c9c26b38e0aa1e6"}, + {file = "psycopg2_binary-2.9.5-cp38-cp38-win32.whl", hash = "sha256:4e7904d1920c0c89105c0517dc7e3f5c20fb4e56ba9cdef13048db76947f1d79"}, + {file = "psycopg2_binary-2.9.5-cp38-cp38-win_amd64.whl", hash = "sha256:a36a0e791805aa136e9cbd0ffa040d09adec8610453ee8a753f23481a0057af5"}, + {file = "psycopg2_binary-2.9.5-cp39-cp39-macosx_10_15_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:25382c7d174c679ce6927c16b6fbb68b10e56ee44b1acb40671e02d29f2fce7c"}, + {file = "psycopg2_binary-2.9.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9c38d3869238e9d3409239bc05bc27d6b7c99c2a460ea337d2814b35fb4fea1b"}, + {file = "psycopg2_binary-2.9.5-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5c6527c8efa5226a9e787507652dd5ba97b62d29b53c371a85cd13f957fe4d42"}, + {file = "psycopg2_binary-2.9.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e59137cdb970249ae60be2a49774c6dfb015bd0403f05af1fe61862e9626642d"}, + {file = "psycopg2_binary-2.9.5-cp39-cp39-manylinux_2_24_aarch64.whl", hash = "sha256:d4c7b3a31502184e856df1f7bbb2c3735a05a8ce0ade34c5277e1577738a5c91"}, + {file = "psycopg2_binary-2.9.5-cp39-cp39-manylinux_2_24_ppc64le.whl", hash = "sha256:b9a794cef1d9c1772b94a72eec6da144c18e18041d294a9ab47669bc77a80c1d"}, + {file = "psycopg2_binary-2.9.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c5254cbd4f4855e11cebf678c1a848a3042d455a22a4ce61349c36aafd4c2267"}, + {file = "psycopg2_binary-2.9.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c5e65c6ac0ae4bf5bef1667029f81010b6017795dcb817ba5c7b8a8d61fab76f"}, + {file = "psycopg2_binary-2.9.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:74eddec4537ab1f701a1647214734bc52cee2794df748f6ae5908e00771f180a"}, + {file = "psycopg2_binary-2.9.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:01ad49d68dd8c5362e4bfb4158f2896dc6e0c02e87b8a3770fc003459f1a4425"}, + {file = "psycopg2_binary-2.9.5-cp39-cp39-win32.whl", hash = "sha256:937880290775033a743f4836aa253087b85e62784b63fd099ee725d567a48aa1"}, + {file = "psycopg2_binary-2.9.5-cp39-cp39-win_amd64.whl", hash = "sha256:484405b883630f3e74ed32041a87456c5e0e63a8e3429aa93e8714c366d62bd1"}, +] + +[[package]] +name = "pycodestyle" +version = "2.9.1" +description = "Python style guide checker" +optional = false +python-versions = ">=3.6" +files = [ + {file = "pycodestyle-2.9.1-py2.py3-none-any.whl", hash = "sha256:d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b"}, + {file = "pycodestyle-2.9.1.tar.gz", hash = "sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785"}, +] + +[[package]] +name = "pycparser" +version = "2.21" +description = "C parser in Python" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, + {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, +] + +[[package]] +name = "pyee" +version = "11.0.1" +description = "A rough port of Node.js's EventEmitter to Python with a few tricks of its own" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pyee-11.0.1-py3-none-any.whl", hash = "sha256:9bcc9647822234f42c228d88de63d0f9ffa881e87a87f9d36ddf5211f6ac977d"}, + {file = "pyee-11.0.1.tar.gz", hash = "sha256:a642c51e3885a33ead087286e35212783a4e9b8d6514a10a5db4e57ac57b2b29"}, +] + +[package.dependencies] +typing-extensions = "*" + +[package.extras] +dev = ["black", "flake8", "flake8-black", "isort", "jupyter-console", "mkdocs", "mkdocs-include-markdown-plugin", "mkdocstrings[python]", "pytest", "pytest-asyncio", "pytest-trio", "toml", "tox", "trio", "trio", "trio-typing", "twine", "twisted", "validate-pyproject[all]"] + +[[package]] +name = "pyflakes" +version = "2.5.0" +description = "passive checker of Python programs" +optional = false +python-versions = ">=3.6" +files = [ + {file = "pyflakes-2.5.0-py2.py3-none-any.whl", hash = "sha256:4579f67d887f804e67edb544428f264b7b24f435b263c4614f384135cea553d2"}, + {file = "pyflakes-2.5.0.tar.gz", hash = "sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3"}, +] + +[[package]] +name = "pygments" +version = "2.14.0" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.6" +files = [ + {file = "Pygments-2.14.0-py3-none-any.whl", hash = "sha256:fa7bd7bd2771287c0de303af8bfdfc731f51bd2c6a47ab69d117138893b82717"}, + {file = "Pygments-2.14.0.tar.gz", hash = "sha256:b3ed06a9e8ac9a9aae5a6f5dbe78a8a58655d17b43b93c078f094ddc476ae297"}, +] + +[package.extras] +plugins = ["importlib-metadata"] + +[[package]] +name = "pymarker" +version = "0.3.2" +description = "A python package to generate AR markers and patterns based on input images" +optional = false +python-versions = ">=3.5" +files = [ + {file = "pymarker-0.3.2-py3-none-any.whl", hash = "sha256:2bb82f8db6340b78e636ea7e38e5ad0d8857259c1eeca7f51da9e7a7fb03179f"}, + {file = "pymarker-0.3.2.tar.gz", hash = "sha256:750f236dfeb718a163a6b5a03665f75a55e7f4b4e1f5547433e97ddad72743f4"}, +] + +[package.dependencies] +click = ">=7.1.1" +pillow = ">=7.1.0" + +[[package]] +name = "pytest" +version = "7.2.1" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-7.2.1-py3-none-any.whl", hash = "sha256:c7c6ca206e93355074ae32f7403e8ea12163b1163c976fee7d4d84027c162be5"}, + {file = "pytest-7.2.1.tar.gz", hash = "sha256:d45e0952f3727241918b8fd0f376f5ff6b301cc0777c6f9a556935c92d8a7d42"}, +] + +[package.dependencies] +attrs = ">=19.2.0" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} + +[package.extras] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] + +[[package]] +name = "pytest-base-url" +version = "2.1.0" +description = "pytest plugin for URL based testing" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest_base_url-2.1.0-py3-none-any.whl", hash = "sha256:3ad15611778764d451927b2a53240c1a7a591b521ea44cebfe45849d2d2812e6"}, + {file = "pytest_base_url-2.1.0.tar.gz", hash = "sha256:02748589a54f9e63fcbe62301d6b0496da0d10231b753e950c63e03aee745d45"}, +] + +[package.dependencies] +pytest = ">=7.0.0" +requests = ">=2.9" + +[package.extras] +test = ["black (>=22.1.0)", "flake8 (>=4.0.1)", "pre-commit (>=2.17.0)", "pytest-localserver (>=0.7.1)", "tox (>=3.24.5)"] + +[[package]] +name = "pytest-django" +version = "4.5.2" +description = "A Django plugin for pytest." +optional = false +python-versions = ">=3.5" +files = [ + {file = "pytest-django-4.5.2.tar.gz", hash = "sha256:d9076f759bb7c36939dbdd5ae6633c18edfc2902d1a69fdbefd2426b970ce6c2"}, + {file = "pytest_django-4.5.2-py3-none-any.whl", hash = "sha256:c60834861933773109334fe5a53e83d1ef4828f2203a1d6a0fa9972f4f75ab3e"}, +] + +[package.dependencies] +pytest = ">=5.4.0" + +[package.extras] +docs = ["sphinx", "sphinx-rtd-theme"] +testing = ["Django", "django-configurations (>=2.0)"] + +[[package]] +name = "pytest-playwright" +version = "0.4.4" +description = "A pytest wrapper with fixtures for Playwright to automate web browsers" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-playwright-0.4.4.tar.gz", hash = "sha256:5488db4cc49028491c5130af0a2bb6b1d0b222a202217f6d14491d4c9aa67ff9"}, + {file = "pytest_playwright-0.4.4-py3-none-any.whl", hash = "sha256:df306f3a60a8631a3cfde1b95a2ed5a89203a3408dfa1154de049ca7de87c90b"}, +] + +[package.dependencies] +playwright = ">=1.18" +pytest = ">=6.2.4,<9.0.0" +pytest-base-url = ">=1.0.0,<3.0.0" +python-slugify = ">=6.0.0,<9.0.0" + +[[package]] +name = "pytest-xdist" +version = "3.1.0" +description = "pytest xdist plugin for distributed testing, most importantly across multiple CPUs" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-xdist-3.1.0.tar.gz", hash = "sha256:40fdb8f3544921c5dfcd486ac080ce22870e71d82ced6d2e78fa97c2addd480c"}, + {file = "pytest_xdist-3.1.0-py3-none-any.whl", hash = "sha256:70a76f191d8a1d2d6be69fc440cdf85f3e4c03c08b520fd5dc5d338d6cf07d89"}, +] + +[package.dependencies] +execnet = ">=1.1" +pytest = ">=6.2.0" + +[package.extras] +psutil = ["psutil (>=3.0)"] +setproctitle = ["setproctitle"] +testing = ["filelock"] + +[[package]] +name = "python-dateutil" +version = "2.8.2" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +files = [ + {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, + {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, +] + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "python-slugify" +version = "8.0.4" +description = "A Python slugify application that also handles Unicode" +optional = false +python-versions = ">=3.7" +files = [ + {file = "python-slugify-8.0.4.tar.gz", hash = "sha256:59202371d1d05b54a9e7720c5e038f928f45daaffe41dd10822f3907b937c856"}, + {file = "python_slugify-8.0.4-py2.py3-none-any.whl", hash = "sha256:276540b79961052b66b7d116620b36518847f52d5fd9e3a70164fc8c50faa6b8"}, +] + +[package.dependencies] +text-unidecode = ">=1.3" + +[package.extras] +unidecode = ["Unidecode (>=1.1.1)"] + +[[package]] +name = "pytz" +version = "2022.7.1" +description = "World timezone definitions, modern and historical" +optional = false +python-versions = "*" +files = [ + {file = "pytz-2022.7.1-py2.py3-none-any.whl", hash = "sha256:78f4f37d8198e0627c5f1143240bb0206b8691d8d7ac6d78fee88b78733f8c4a"}, + {file = "pytz-2022.7.1.tar.gz", hash = "sha256:01a0681c4b9684a28304615eba55d1ab31ae00bf68ec157ec3708a8182dbbcd0"}, +] + +[[package]] +name = "requests" +version = "2.28.2" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.7, <4" +files = [ + {file = "requests-2.28.2-py3-none-any.whl", hash = "sha256:64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa"}, + {file = "requests-2.28.2.tar.gz", hash = "sha256:98b1b2782e3c6c4904938b84c0eb932721069dfdb9134313beff7c83c2df24bf"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<1.27" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "s3transfer" +version = "0.6.0" +description = "An Amazon S3 Transfer Manager" +optional = false +python-versions = ">= 3.7" +files = [ + {file = "s3transfer-0.6.0-py3-none-any.whl", hash = "sha256:06176b74f3a15f61f1b4f25a1fc29a4429040b7647133a463da8fa5bd28d5ecd"}, + {file = "s3transfer-0.6.0.tar.gz", hash = "sha256:2ed07d3866f523cc561bf4a00fc5535827981b117dd7876f036b0c1aca42c947"}, +] + +[package.dependencies] +botocore = ">=1.12.36,<2.0a.0" + +[package.extras] +crt = ["botocore[crt] (>=1.20.29,<2.0a.0)"] + +[[package]] +name = "sentry-sdk" +version = "1.14.0" +description = "Python client for Sentry (https://sentry.io)" +optional = false +python-versions = "*" +files = [ + {file = "sentry-sdk-1.14.0.tar.gz", hash = "sha256:273fe05adf052b40fd19f6d4b9a5556316807246bd817e5e3482930730726bb0"}, + {file = "sentry_sdk-1.14.0-py2.py3-none-any.whl", hash = "sha256:72c00322217d813cf493fe76590b23a757e063ff62fec59299f4af7201dd4448"}, +] + +[package.dependencies] +certifi = "*" +urllib3 = {version = ">=1.26.11", markers = "python_version >= \"3.6\""} + +[package.extras] +aiohttp = ["aiohttp (>=3.5)"] +beam = ["apache-beam (>=2.12)"] +bottle = ["bottle (>=0.12.13)"] +celery = ["celery (>=3)"] +chalice = ["chalice (>=1.16.0)"] +django = ["django (>=1.8)"] +falcon = ["falcon (>=1.4)"] +fastapi = ["fastapi (>=0.79.0)"] +flask = ["blinker (>=1.1)", "flask (>=0.11)"] +httpx = ["httpx (>=0.16.0)"] +opentelemetry = ["opentelemetry-distro (>=0.35b0)"] +pure-eval = ["asttokens", "executing", "pure-eval"] +pymongo = ["pymongo (>=3.1)"] +pyspark = ["pyspark (>=2.4.4)"] +quart = ["blinker (>=1.1)", "quart (>=0.16.1)"] +rq = ["rq (>=0.6)"] +sanic = ["sanic (>=0.8)"] +sqlalchemy = ["sqlalchemy (>=1.2)"] +starlette = ["starlette (>=0.19.1)"] +starlite = ["starlite (>=1.48)"] +tornado = ["tornado (>=5)"] + +[[package]] +name = "setuptools" +version = "67.0.0" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +optional = false +python-versions = ">=3.7" +files = [ + {file = "setuptools-67.0.0-py3-none-any.whl", hash = "sha256:9d790961ba6219e9ff7d9557622d2fe136816a264dd01d5997cfc057d804853d"}, + {file = "setuptools-67.0.0.tar.gz", hash = "sha256:883131c5b6efa70b9101c7ef30b2b7b780a4283d5fc1616383cdf22c83cbefe6"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] + +[[package]] +name = "sidekick" +version = "0.5.2" +description = "The companion that gives you functional superpowers" +optional = false +python-versions = "*" +files = [ + {file = "sidekick-0.5.2.tar.gz", hash = "sha256:3f1b2c65f2d9cadc4ad06ba57010d7aae0ef3eeff92a94ac3aadb9863b692a6a"}, +] + +[package.dependencies] +toolz = ">=0.9,<1.0" + +[package.extras] +dev = ["flake8 (>=3.7,<4.0)", "invoke (>=1.0)", "manuel (>=1.10,<2.0)", "pytest (>=4.2,<5.0)", "pytest-cov (>=2.6,<3.0)"] +docs = ["sphinx"] + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] + +[[package]] +name = "snowballstemmer" +version = "2.2.0" +description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." +optional = false +python-versions = "*" +files = [ + {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, + {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, +] + +[[package]] +name = "sphinx" +version = "5.3.0" +description = "Python documentation generator" +optional = false +python-versions = ">=3.6" +files = [ + {file = "Sphinx-5.3.0.tar.gz", hash = "sha256:51026de0a9ff9fc13c05d74913ad66047e104f56a129ff73e174eb5c3ee794b5"}, + {file = "sphinx-5.3.0-py3-none-any.whl", hash = "sha256:060ca5c9f7ba57a08a1219e547b269fadf125ae25b06b9fa7f66768efb652d6d"}, +] + +[package.dependencies] +alabaster = ">=0.7,<0.8" +babel = ">=2.9" +colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} +docutils = ">=0.14,<0.20" +imagesize = ">=1.3" +Jinja2 = ">=3.0" +packaging = ">=21.0" +Pygments = ">=2.12" +requests = ">=2.5.0" +snowballstemmer = ">=2.0" +sphinxcontrib-applehelp = "*" +sphinxcontrib-devhelp = "*" +sphinxcontrib-htmlhelp = ">=2.0.0" +sphinxcontrib-jsmath = "*" +sphinxcontrib-qthelp = "*" +sphinxcontrib-serializinghtml = ">=1.1.5" + +[package.extras] +docs = ["sphinxcontrib-websupport"] +lint = ["docutils-stubs", "flake8 (>=3.5.0)", "flake8-bugbear", "flake8-comprehensions", "flake8-simplify", "isort", "mypy (>=0.981)", "sphinx-lint", "types-requests", "types-typed-ast"] +test = ["cython", "html5lib", "pytest (>=4.6)", "typed_ast"] + +[[package]] +name = "sphinxcontrib-applehelp" +version = "1.0.4" +description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books" +optional = false +python-versions = ">=3.8" +files = [ + {file = "sphinxcontrib-applehelp-1.0.4.tar.gz", hash = "sha256:828f867945bbe39817c210a1abfd1bc4895c8b73fcaade56d45357a348a07d7e"}, + {file = "sphinxcontrib_applehelp-1.0.4-py3-none-any.whl", hash = "sha256:29d341f67fb0f6f586b23ad80e072c8e6ad0b48417db2bde114a4c9746feb228"}, +] + +[package.extras] +lint = ["docutils-stubs", "flake8", "mypy"] +test = ["pytest"] + +[[package]] +name = "sphinxcontrib-devhelp" +version = "1.0.2" +description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document." +optional = false +python-versions = ">=3.5" +files = [ + {file = "sphinxcontrib-devhelp-1.0.2.tar.gz", hash = "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"}, + {file = "sphinxcontrib_devhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e"}, +] + +[package.extras] +lint = ["docutils-stubs", "flake8", "mypy"] +test = ["pytest"] + +[[package]] +name = "sphinxcontrib-htmlhelp" +version = "2.0.1" +description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" +optional = false +python-versions = ">=3.8" +files = [ + {file = "sphinxcontrib-htmlhelp-2.0.1.tar.gz", hash = "sha256:0cbdd302815330058422b98a113195c9249825d681e18f11e8b1f78a2f11efff"}, + {file = "sphinxcontrib_htmlhelp-2.0.1-py3-none-any.whl", hash = "sha256:c38cb46dccf316c79de6e5515e1770414b797162b23cd3d06e67020e1d2a6903"}, +] + +[package.extras] +lint = ["docutils-stubs", "flake8", "mypy"] +test = ["html5lib", "pytest"] + +[[package]] +name = "sphinxcontrib-jsmath" +version = "1.0.1" +description = "A sphinx extension which renders display math in HTML via JavaScript" +optional = false +python-versions = ">=3.5" +files = [ + {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, + {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, +] + +[package.extras] +test = ["flake8", "mypy", "pytest"] + +[[package]] +name = "sphinxcontrib-qthelp" +version = "1.0.3" +description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document." +optional = false +python-versions = ">=3.5" +files = [ + {file = "sphinxcontrib-qthelp-1.0.3.tar.gz", hash = "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72"}, + {file = "sphinxcontrib_qthelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"}, +] + +[package.extras] +lint = ["docutils-stubs", "flake8", "mypy"] +test = ["pytest"] + +[[package]] +name = "sphinxcontrib-serializinghtml" +version = "1.1.5" +description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)." +optional = false +python-versions = ">=3.5" +files = [ + {file = "sphinxcontrib-serializinghtml-1.1.5.tar.gz", hash = "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952"}, + {file = "sphinxcontrib_serializinghtml-1.1.5-py2.py3-none-any.whl", hash = "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd"}, +] + +[package.extras] +lint = ["docutils-stubs", "flake8", "mypy"] +test = ["pytest"] + +[[package]] +name = "sqlparse" +version = "0.4.4" +description = "A non-validating SQL parser." +optional = false +python-versions = ">=3.5" +files = [ + {file = "sqlparse-0.4.4-py3-none-any.whl", hash = "sha256:5430a4fe2ac7d0f93e66f1efc6e1338a41884b7ddf2a350cedd20ccc4d9d28f3"}, + {file = "sqlparse-0.4.4.tar.gz", hash = "sha256:d446183e84b8349fa3061f0fe7f06ca94ba65b426946ffebe6e3e8295332420c"}, +] + +[package.extras] +dev = ["build", "flake8"] +doc = ["sphinx"] +test = ["pytest", "pytest-cov"] + +[[package]] +name = "text-unidecode" +version = "1.3" +description = "The most basic Text::Unidecode port" +optional = false +python-versions = "*" +files = [ + {file = "text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93"}, + {file = "text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8"}, +] + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] + +[[package]] +name = "toolz" +version = "0.12.0" +description = "List processing tools and functional utilities" +optional = false +python-versions = ">=3.5" +files = [ + {file = "toolz-0.12.0-py3-none-any.whl", hash = "sha256:2059bd4148deb1884bb0eb770a3cde70e7f954cfbbdc2285f1f2de01fd21eb6f"}, + {file = "toolz-0.12.0.tar.gz", hash = "sha256:88c570861c440ee3f2f6037c4654613228ff40c93a6c25e0eba70d17282c6194"}, +] + +[[package]] +name = "typing-extensions" +version = "4.9.0" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.9.0-py3-none-any.whl", hash = "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd"}, + {file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"}, +] + +[[package]] +name = "tzdata" +version = "2022.7" +description = "Provider of IANA time zone data" +optional = false +python-versions = ">=2" +files = [ + {file = "tzdata-2022.7-py2.py3-none-any.whl", hash = "sha256:2b88858b0e3120792a3c0635c23daf36a7d7eeeca657c323da299d2094402a0d"}, + {file = "tzdata-2022.7.tar.gz", hash = "sha256:fe5f866eddd8b96e9fcba978f8e503c909b19ea7efda11e52e39494bad3a7bfa"}, +] + +[[package]] +name = "urllib3" +version = "1.26.14" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" +files = [ + {file = "urllib3-1.26.14-py2.py3-none-any.whl", hash = "sha256:75edcdc2f7d85b137124a6c3c9fc3933cdeaa12ecb9a6a959f22797a0feca7e1"}, + {file = "urllib3-1.26.14.tar.gz", hash = "sha256:076907bf8fd355cde77728471316625a4d2f7e713c125f51953bb5b3eecf4f72"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] +secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] +socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] + +[[package]] +name = "zope-event" +version = "4.6" +description = "Very basic event publishing system" +optional = false +python-versions = "*" +files = [ + {file = "zope.event-4.6-py2.py3-none-any.whl", hash = "sha256:73d9e3ef750cca14816a9c322c7250b0d7c9dbc337df5d1b807ff8d3d0b9e97c"}, + {file = "zope.event-4.6.tar.gz", hash = "sha256:81d98813046fc86cc4136e3698fee628a3282f9c320db18658c21749235fce80"}, +] + +[package.dependencies] +setuptools = "*" + +[package.extras] +docs = ["Sphinx"] +test = ["zope.testrunner"] + +[[package]] +name = "zope-interface" +version = "5.5.2" +description = "Interfaces for Python" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "zope.interface-5.5.2-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:a2ad597c8c9e038a5912ac3cf166f82926feff2f6e0dabdab956768de0a258f5"}, + {file = "zope.interface-5.5.2-cp27-cp27m-win_amd64.whl", hash = "sha256:65c3c06afee96c654e590e046c4a24559e65b0a87dbff256cd4bd6f77e1a33f9"}, + {file = "zope.interface-5.5.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d514c269d1f9f5cd05ddfed15298d6c418129f3f064765295659798349c43e6f"}, + {file = "zope.interface-5.5.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5334e2ef60d3d9439c08baedaf8b84dc9bb9522d0dacbc10572ef5609ef8db6d"}, + {file = "zope.interface-5.5.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc26c8d44472e035d59d6f1177eb712888447f5799743da9c398b0339ed90b1b"}, + {file = "zope.interface-5.5.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:17ebf6e0b1d07ed009738016abf0d0a0f80388e009d0ac6e0ead26fc162b3b9c"}, + {file = "zope.interface-5.5.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f98d4bd7bbb15ca701d19b93263cc5edfd480c3475d163f137385f49e5b3a3a7"}, + {file = "zope.interface-5.5.2-cp310-cp310-win_amd64.whl", hash = "sha256:696f3d5493eae7359887da55c2afa05acc3db5fc625c49529e84bd9992313296"}, + {file = "zope.interface-5.5.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7579960be23d1fddecb53898035a0d112ac858c3554018ce615cefc03024e46d"}, + {file = "zope.interface-5.5.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:765d703096ca47aa5d93044bf701b00bbce4d903a95b41fff7c3796e747b1f1d"}, + {file = "zope.interface-5.5.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e945de62917acbf853ab968d8916290548df18dd62c739d862f359ecd25842a6"}, + {file = "zope.interface-5.5.2-cp311-cp311-win_amd64.whl", hash = "sha256:655796a906fa3ca67273011c9805c1e1baa047781fca80feeb710328cdbed87f"}, + {file = "zope.interface-5.5.2-cp35-cp35m-win_amd64.whl", hash = "sha256:0fb497c6b088818e3395e302e426850f8236d8d9f4ef5b2836feae812a8f699c"}, + {file = "zope.interface-5.5.2-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:008b0b65c05993bb08912f644d140530e775cf1c62a072bf9340c2249e613c32"}, + {file = "zope.interface-5.5.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:404d1e284eda9e233c90128697c71acffd55e183d70628aa0bbb0e7a3084ed8b"}, + {file = "zope.interface-5.5.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:3218ab1a7748327e08ef83cca63eea7cf20ea7e2ebcb2522072896e5e2fceedf"}, + {file = "zope.interface-5.5.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d169ccd0756c15bbb2f1acc012f5aab279dffc334d733ca0d9362c5beaebe88e"}, + {file = "zope.interface-5.5.2-cp36-cp36m-win_amd64.whl", hash = "sha256:e1574980b48c8c74f83578d1e77e701f8439a5d93f36a5a0af31337467c08fcf"}, + {file = "zope.interface-5.5.2-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:0217a9615531c83aeedb12e126611b1b1a3175013bbafe57c702ce40000eb9a0"}, + {file = "zope.interface-5.5.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:311196634bb9333aa06f00fc94f59d3a9fddd2305c2c425d86e406ddc6f2260d"}, + {file = "zope.interface-5.5.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6373d7eb813a143cb7795d3e42bd8ed857c82a90571567e681e1b3841a390d16"}, + {file = "zope.interface-5.5.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:959697ef2757406bff71467a09d940ca364e724c534efbf3786e86eee8591452"}, + {file = "zope.interface-5.5.2-cp37-cp37m-win_amd64.whl", hash = "sha256:dbaeb9cf0ea0b3bc4b36fae54a016933d64c6d52a94810a63c00f440ecb37dd7"}, + {file = "zope.interface-5.5.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:604cdba8f1983d0ab78edc29aa71c8df0ada06fb147cea436dc37093a0100a4e"}, + {file = "zope.interface-5.5.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e74a578172525c20d7223eac5f8ad187f10940dac06e40113d62f14f3adb1e8f"}, + {file = "zope.interface-5.5.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0980d44b8aded808bec5059018d64692f0127f10510eca71f2f0ace8fb11188"}, + {file = "zope.interface-5.5.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6e972493cdfe4ad0411fd9abfab7d4d800a7317a93928217f1a5de2bb0f0d87a"}, + {file = "zope.interface-5.5.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9d783213fab61832dbb10d385a319cb0e45451088abd45f95b5bb88ed0acca1a"}, + {file = "zope.interface-5.5.2-cp38-cp38-win_amd64.whl", hash = "sha256:a16025df73d24795a0bde05504911d306307c24a64187752685ff6ea23897cb0"}, + {file = "zope.interface-5.5.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:40f4065745e2c2fa0dff0e7ccd7c166a8ac9748974f960cd39f63d2c19f9231f"}, + {file = "zope.interface-5.5.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8a2ffadefd0e7206adc86e492ccc60395f7edb5680adedf17a7ee4205c530df4"}, + {file = "zope.interface-5.5.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d692374b578360d36568dd05efb8a5a67ab6d1878c29c582e37ddba80e66c396"}, + {file = "zope.interface-5.5.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4087e253bd3bbbc3e615ecd0b6dd03c4e6a1e46d152d3be6d2ad08fbad742dcc"}, + {file = "zope.interface-5.5.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:fb68d212efd057596dee9e6582daded9f8ef776538afdf5feceb3059df2d2e7b"}, + {file = "zope.interface-5.5.2-cp39-cp39-win_amd64.whl", hash = "sha256:7e66f60b0067a10dd289b29dceabd3d0e6d68be1504fc9d0bc209cf07f56d189"}, + {file = "zope.interface-5.5.2.tar.gz", hash = "sha256:bfee1f3ff62143819499e348f5b8a7f3aa0259f9aca5e0ddae7391d059dce671"}, +] + +[package.dependencies] +setuptools = "*" + +[package.extras] +docs = ["Sphinx", "repoze.sphinx.autointerface"] +test = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] +testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] + +[metadata] +lock-version = "2.0" +python-versions = "^3.10" +content-hash = "84bba7d5ee3576bad6107eb6dd0cbdf63b28e7ca290353eca27a4a1798a38043" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..15fde249 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,64 @@ +[tool.poetry] +name = "Jandig" +version = "2.0.0" +description = "" +authors = ["Your Name "] + +[tool.poetry.dependencies] +python = "^3.10" +MarkupSafe = "^2.1.1" +Django = "^4.0.8" +Jinja2 = "^3.1.2" +gunicorn = "^20.1.0" +Pillow = "^9.2.0" +django-cors-headers = "^3.13.0" +invoke = "^1.7.1" +django-environ = "^0.9.0" +psycopg2-binary = "^2.9.3" +django-boogie = "^0.13.3" +Sphinx = "^5.0.2" +Babel = "^2.10.3" +django-debug-toolbar = "^3.5.0" +gevent = "^23.9.1" +pymarker = "^0.3.1" +django-extensions = "^3.1.5" +factory-boy = "^3.2.1" +boto3 = "^1.24.27" +django-storages = "^1.12.3" +sentry-sdk = "^1.7.0" +djangorestframework = "^3.13.1" +drf-nested-routers = "^0.93.4" +django-htmx = "^1.18.0" + +[tool.poetry.dev-dependencies] +playwright = "^1.41.2" +pytest = "^7.2.0" +pytest-xdist = "^3.0.2" +flake8 = "^5.0.4" +black = "^22.10.0" +isort = "^5.10.1" +pytest-django = "^4.5.2" +dj-inmemorystorage = "^2.1.0" +pytest-playwright = "^0.4.4" + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" + +[tool.pytest.ini_options] +DJANGO_SETTINGS_MODULE = "config.test_settings" +python_files = ["tests.py", "test_*.py", "*_tests.py"] +pythonpath = "src" +addopts="-n4" + +[tool.black] +exclude = ''' +/( + \.git + | migrations +)/ +''' +[tool.isort] +profile = "black" +src_paths = ["src"] +skip_glob = "*/migrations/*.py" \ No newline at end of file diff --git a/run.sh b/run.sh index 3a953847..13bb0be0 100755 --- a/run.sh +++ b/run.sh @@ -1,3 +1,3 @@ #!/bin/bash -pip list -inv i18n --compile docs run -p -g +poetry show +poetry run inv collect db i18n --compile docs run -g diff --git a/src/.envs/.example b/src/.envs/.example deleted file mode 100644 index 1aef234d..00000000 --- a/src/.envs/.example +++ /dev/null @@ -1,29 +0,0 @@ -# Use this file to create a .env with filled variables -# Django Variables -DJANGO_DEBUG=True -DJANGO_SETTINGS_MODULE=config.settings -DJANGO_ADMIN_URL=admin/ -DEV_DB=False -DEV_STATIC=False -DEBUG_TOOLBAR=True -DJANGO_SECRET_KEY= - -## Amazon AWS Variables -AWS_ACCESS_KEY_ID= -AWS_SECRET_ACCESS_KEY= -AWS_STORAGE_BUCKET_NAME= -AWS_S3_REGION_NAME= -USE_S3= - -## Postgres variables -POSTGRES_HOST=postgres -POSTGRES_PORT=5432 -POSTGRES_DB=goldpenny -POSTGRES_USER=goldpenny -POSTGRES_PASSWORD=goldpenny123 - -# Email server variables -SMTP_SERVER=smtp.gmail.com -SMTP_PORT=587 -JANDIG_EMAIL=local_jandig@memelab.com.br -JANDIG_EMAIL_PASSWORD=local_password \ No newline at end of file diff --git a/src/ARte/config/settings.py b/src/ARte/config/settings.py deleted file mode 100644 index d69e88a8..00000000 --- a/src/ARte/config/settings.py +++ /dev/null @@ -1,228 +0,0 @@ -import os -import environ -from .wait_db import start_services -from django.utils.translation import ugettext_lazy as _ - -from socket import gethostbyname, gethostname - -ROOT_DIR = environ.Path(__file__) - 2 # (ARte/config/settings.py - 2 = ARte/) -APPS_DIR = ROOT_DIR.path('ARte') - -# Build paths inside the project like this: os.path.join(BASE_DIR, ...) -BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - -env = environ.Env() - -READ_DOT_ENV_FILE = env.bool('DJANGO_READ_DOT_ENV_FILE', default=False) -if READ_DOT_ENV_FILE: - # OS environment variables take precedence over variables from .env - env.read_env(str(ROOT_DIR.path('.env'))) - -DEBUG = env.bool('DJANGO_DEBUG', False) - - -# SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = env('DJANGO_SECRET_KEY', default='gw)48fp(ct67v4+0_tpxl=$vw=-x&y9(&0n6!n4mpw5m!4gaor') - -ALLOWED_HOSTS = [ - "localhost", - "0.0.0.0", - "127.0.0.1", - gethostname(), - gethostbyname(gethostname()), -] -CUSTOM_ALLOWED_HOSTS = env.list('DJANGO_ALLOWED_HOSTS', default=['*']) -ALLOWED_HOSTS += CUSTOM_ALLOWED_HOSTS -print(f"ALLOWED_HOSTS:{ALLOWED_HOSTS}") -# Application definition - -import sentry_sdk -from sentry_sdk.integrations.django import DjangoIntegration - -sentry_sdk.init( - dsn="https://081a2c3476b24a9f9a51d74bde539b62@o968990.ingest.sentry.io/5920229", - integrations=[DjangoIntegration()], - - # Set traces_sample_rate to 1.0 to capture 100% - # of transactions for performance monitoring. - # We recommend adjusting this value in production. - traces_sample_rate=1.0, - - # If you wish to associate users to errors (assuming you are using - # django.contrib.auth) you may enable sending PII data. - send_default_pii=True -) - -INSTALLED_APPS = [ - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'debug_toolbar', - 'corsheaders', - 'users', - 'core', - 'docs', - #'django_extensions', -] - -MIDDLEWARE = [ - 'debug_toolbar.middleware.DebugToolbarMiddleware', - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.locale.LocaleMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', -] -DEBUG_TOOLBAR_CONFIG = { - 'SHOW_TOOLBAR_CALLBACK': 'config.settings.debug' -} - -def debug(request): - return env.bool('DEBUG_TOOLBAR', False) - -## Let whitenoise serve static files -- DON'T USE IN PRODUCTION -- -if env.bool('DEV_STATIC', False): - MIDDLEWARE.insert(1, 'whitenoise.middleware.WhiteNoiseMiddleware') - STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage' - print('\n ------- SERVING STATIC FILES USING WHITENOISE! -------\n') - -ROOT_URLCONF = 'config.urls' - -TEMPLATES = [ - { - 'BACKEND': 'django.template.backends.jinja2.Jinja2', - 'DIRS': [], - 'APP_DIRS': True, - 'OPTIONS': { - 'environment': 'config.jinja2.environment', - }, - }, - { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', - ], - }, - }, -] - -WSGI_APPLICATION = 'config.wsgi.application' - - -if env.bool('DEV_DB', False): - DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), - } - } -else: - DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.postgresql_psycopg2', - 'HOST': env('POSTGRES_HOST'), - 'NAME': env('POSTGRES_DB'), - 'USER': env('POSTGRES_USER'), - 'PASSWORD': env('POSTGRES_PASSWORD'), - }, - } - - # STARTS SERVICES THAT DJANGO DEPENDS E.G. postgres - start_services() - - -AUTH_PASSWORD_VALIDATORS = [ - { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', - }, -] - - -# Internationalization -# https://docs.djangoproject.com/en/2.1/topics/i18n/ - -LOCALE_PATHS = ( - # os.path.join(str(ROOT_DIR), 'locale'), - '/ARte/locale', -) - -LANGUAGE_CODE = 'en' - -LANGUAGES = ( - ('en-us', _('English')), - ('pt-br', _('Brazilian Portuguese')), -) - -TIME_ZONE = 'UTC' - -USE_I18N = True - -USE_L10N = True - -USE_TZ = True - - -# Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/2.1/howto/static-files/ - -COLLECT_DIR = os.path.dirname(os.path.dirname(BASE_DIR)) -USE_S3 = os.getenv("USE_S3", "False").lower() == "true" -STATICFILES_DIRS = [ - os.path.join(BASE_DIR, 'core', 'static'), - os.path.join(BASE_DIR, 'users', 'static') -] -if USE_S3: - # AWS credentials - AWS_S3_OBJECT_PARAMETERS = { - "CacheControl": "max-age=86400", - } - AWS_ACCESS_KEY_ID = os.getenv("AWS_ACCESS_KEY_ID", "") - AWS_SECRET_ACCESS_KEY = os.getenv("AWS_SECRET_ACCESS_KEY", "") - AWS_STORAGE_BUCKET_NAME = os.getenv("AWS_STORAGE_BUCKET_NAME", "") - AWS_S3_REGION_NAME = os.getenv("AWS_S3_REGION_NAME", "us-east-2") - AWS_S3_CUSTOM_DOMAIN = f"{AWS_STORAGE_BUCKET_NAME}.s3.amazonaws.com" - AWS_DEFAULT_ACL = os.getenv("AWS_DEFAULT_ACL", None) - AWS_STATIC_LOCATION = os.getenv("AWS_STATIC_LOCATION", "static") - - # Static configuration - STATIC_URL = f"https://{AWS_S3_CUSTOM_DOMAIN}/{AWS_STATIC_LOCATION}/" - STATICFILES_STORAGE = "config.storage_backends.StaticStorage" - - AWS_PUBLIC_MEDIA_LOCATION = "media/public" - DEFAULT_FILE_STORAGE = "config.storage_backends.PublicMediaStorage" -else: - STATIC_URL = '/static/' - STATIC_ROOT = os.path.join(COLLECT_DIR, 'collect') - - -MEDIA_URL = '/media/' -MEDIA_ROOT = os.path.join(BASE_DIR, 'users', 'media') - -# LOGIN / LOGOUT -LOGIN_URL = 'login' -LOGIN_REDIRECT_URL = 'home' -LOGOUT_REDIRECT_URL = 'home' - -# Sphinx docs -DOCS_ROOT = os.path.join(BASE_DIR, '../../build/') \ No newline at end of file diff --git a/src/ARte/config/urls.py b/src/ARte/config/urls.py deleted file mode 100644 index 537d4e3c..00000000 --- a/src/ARte/config/urls.py +++ /dev/null @@ -1,18 +0,0 @@ -from django.conf import settings -from django.conf.urls.static import static -from django.contrib import admin -from django.urls import path, include -import debug_toolbar -import os - -urlpatterns = [ - path(os.getenv('DJANGO_ADMIN_URL', 'admin/'), admin.site.urls), - path('', include('core.urls')), - path('', include('core.routes')), - path('users/', include('users.urls')), - path('docs/', include('docs.urls'), name='docs'), -] - -urlpatterns += [path('__debug__/', include(debug_toolbar.urls)),] - -urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) \ No newline at end of file diff --git a/src/ARte/config/wait_db.py b/src/ARte/config/wait_db.py deleted file mode 100644 index fd090b6e..00000000 --- a/src/ARte/config/wait_db.py +++ /dev/null @@ -1,60 +0,0 @@ -import importlib -import os -import time - -import logging - -SERVICES_STARTED = False - -log = logging.getLogger('ej') - - -def start_services(): - global SERVICES_STARTED - - if SERVICES_STARTED: - return - - start_postgres() - - SERVICES_STARTED = True - - - -log = logging.getLogger('ej') - - -def start_postgres(): - settings_path = os.environ['DJANGO_SETTINGS_MODULE'] - settings = importlib.import_module(settings_path) - - db = settings.DATABASES['default'] - dbname = db['NAME'] - user = db['USER'] - password = db['PASSWORD'] - host = db['HOST'] - - for _ in range(100): - if can_connect(dbname, user, password, host): - log.info("Postgres is available. Continuing...") - return - log.warning('Postgres is unavailable. Retrying in 0.5 seconds') - time.sleep(0.5) - - log.critical('Maximum number of attempts connecting to postgres database') - raise RuntimeError('could not connect to database') - - -def can_connect(dbname, user, password, host): - import psycopg2 - - try: - psycopg2.connect( - dbname=dbname, - user=user, - password=password, - host=host - ) - except psycopg2.OperationalError: - return False - return True diff --git a/src/ARte/core/helpers.py b/src/ARte/core/helpers.py deleted file mode 100644 index 2dea8181..00000000 --- a/src/ARte/core/helpers.py +++ /dev/null @@ -1,7 +0,0 @@ -from django.contrib.staticfiles.templatetags.staticfiles import static - -def handle_upload_image(image): - path = "core" + static(f"images/{image.name}") - with open(path, 'wb+') as destination: - for chunk in image.chunks(): - destination.write(chunk) diff --git a/src/ARte/core/jinja2/core/collection.jinja2 b/src/ARte/core/jinja2/core/collection.jinja2 deleted file mode 100644 index 5d148f83..00000000 --- a/src/ARte/core/jinja2/core/collection.jinja2 +++ /dev/null @@ -1,58 +0,0 @@ -{% extends '/core/home.jinja2' %} - -{% block content %} - {% block subcontent %} - {% endblock %} -
-
- - {# FIXME: maybe this can be improved #} - - - {% if exhibits %} -

{{_("Jandig Exhibitions")}}

- {% with repository_list = exhibits, element_type="exhibit"%} - {% include "users/components/item-list.jinja2" %} - {% endwith %} - {% if seeall == False %} - {{_("All Exhibits")}} - {% endif %} - {% endif %} - - {% if artworks %} -

{{_("Jandig Artworks")}}

- {% with repository_list = artworks, element_type="artwork" %} - {% include "users/components/item-list.jinja2" %} - {% endwith %} - {% if seeall == False %} - {{_("All Artworks")}} - {% endif %} - {% endif %} - - {% if markers %} -

{{_("Jandig Markers")}}

- {% with repository_list = markers, element_type="marker"%} - {% include "users/components/item-list.jinja2" %} - {% endwith %} - {% if seeall == False %} - {{_("All Markers")}} - {% endif %} - {% endif %} - - {% if objects %} -

{{_("Jandig Objects")}}

- {% with repository_list = objects, element_type="object" %} - {% include "users/components/item-list.jinja2" %} - {% endwith%} - {% if seeall == False %} - {{_("All Objects")}} - {% endif %} - {% endif %} - - {% if not exhibits and not artworks and not markers and not objects %} -

{{_("We found no content on your Collection, try uploading an object.")}}

- {% endif %} - {% include "users/components/elements-modal.jinja2" %} -
-
-{% endblock %} \ No newline at end of file diff --git a/src/ARte/core/static/css/reset.css b/src/ARte/core/static/css/reset.css deleted file mode 100644 index bd72ecc5..00000000 --- a/src/ARte/core/static/css/reset.css +++ /dev/null @@ -1,810 +0,0 @@ -/* @import url('https://fonts.googleapis.com/css?family=Istok+Web:400,400i,700,700i&display=swap'); */ - -body { - color: #000; - background: #FFF; - margin: 0; - padding: 0; - line-height: 1; - font-family: "Istok Web", sans-serif; - text-align: center; -} - -body.colorful { - background-image: url(../images/icons/bg.png); - background-repeat: repeat-x; - background-position: center top; -} - -h1, h2, h3, h4, h5, h6 { - font-family: "Istok Web", serif; - font-weight: bolder; -} - -p, ul, ol { - line-height: 1.75; - margin: 0 0 1em 0; -} - -section, form { - margin-bottom: 20px; -} - -a, a:visited { - color: #03b595; - color: #777; - padding: 0; - text-decoration: none; -} - -h1 { - font-size: 1.25em; - line-height: 50px; - background-image: linear-gradient(to top, #77777766, #77777766 50%, transparent 50%, transparent); - background-position: 100% 15%; - background-size: 100% 200%; - margin-top: 10px; - width: 100%; -} - -h2 { - font-size: 1.25em; - color: #03b595; - margin: 30px auto 10px; -} - -h3 { - font-size: 1.1em; - line-height: 20px; - text-align: left; -} - -h3:before { - width: 20px; - height: 20px; - background: #03b595; - display: inline-block; - margin: 5px 10px -3px 0px; - content: ' '; -} - -h4 { - font-size: 1.1em; - color: #03b595; -} - -h5 { - font-size: 1.1em; -} - -h6 { - font-size: 1em; - color: #03b595; -} - -hr { - height: 10px; - width: 80%; - border: none; - background: #03b595; - margin: 20px auto; -} - -.account-options a { - color: #777; - padding: 0; - text-decoration: none; - transition: all .2s ease-in; - background-image: linear-gradient(to top, #77777766, #77777766 50%, transparent 50%, transparent); - background-position: 100% 10%; - background-size: 100% 200%; -} - -ul { - list-style: square; -} - -ul, ol { - padding: 0 0 0 5em; - text-align: left; -} - -blockquote { - font-size: 1.1em; - line-height: 2em; - margin: 10px 40px; - color: #888; - font-style: italic; -} - -blockquote:before, blockquote:after { - max-width: 40px; - height: 40px; - display: block; - margin: 0 auto -20px 0; - background-image: url(../images/icons/fill.png); - content: ''; - text-align: left; -} - -blockquote:after { - text-align: right; - margin: -30px 0 20px auto; -} - -input { - width: calc(100% - 24px); - padding: 0 10px; - height: 40px; -} - -textarea { - width: calc(100% - 24px); - padding: 10px; -} - -select { - width: 100%; - height: 40px; - padding: 0 10px; -} - -input[type=submit], -input[type="submit" i], -button, -.button { - width: 100%; - margin: 20px 0; - background: #000; - border: none; - color: #fff; - font-weight: bold; - font-size: 0.75em; - line-height: 40px; - text-transform: uppercase; - text-align: center; - display: inline-block; -} - -input:disabled, input[type="submit" i]:disabled, input[type=submit]:disabled, button.disabled { - opacity: 0.4; - pointer-events: none; - color: #777; -} - -button.secondary, -input[type=submit].secondary { - background: #666; - width: 100%; - line-height: 30px; - color: #fff; -} - -input[type=text] { - padding: 0 10px; -} - -input[type="checkbox" i], input[type="radio" i] { - height: 20px; - width: 45px; - margin: 10px 0; - line-height: 40px; - display: inline-block; - vertical-align: sub; -} - -.flex { - display: flex; - flex-flow: row wrap; - align-items: center; - justify-content: space-between; -} - -.half { - width: calc(50% - 10px); -} - -.one-third { - width: calc(33% - 10px); -} - -.sm-one-third { - flex-basis: calc(33% - 10px); -} - -.sm-half { - flex-basis: calc(50% - 10px); -} - -.sm-full { - flex-basis: 100%; -} - -.left { - text-align: left; -} - -.right { - text-align: right; -} - -/* Structure */ -.container { - max-width: 320px; - margin: 0 auto; - text-align: center; - width: 100%; -} - -.colorful .container { - background-color: #fff; - margin-top: 40px; - padding: 20px; - max-width: 280px; -} - -a.button, a.button:visited { - color: #fff; -} - -a.changeExb, a.jandigIco { - height: 40px; - text-align: left; - line-height: 40px; - background-repeat: no-repeat; - background-size: contain; - min-width: calc(50% - 20px); -} - -a.changeExb { - height: 20px; - line-height: 20px; - font-size: 0.75em; - color: #fff; - padding: 5px 0 5px 40px; - background-image: url(../images/icons/icoChgExb.png); - background-position: center left; -} - -a.jandigIco { - text-indent: -9999px; - background-image: url(../images/icons/jandigIcon.png); - background-position: center right; -} - -.header { - margin-top: 25px; - font-size: 75%; -} - -.aside { - max-width: 120px; - margin-left: -120px; - padding-left: 10px; - height: 70px; - float: right; - background: #fff; -} - -.logo { - text-align: left; - height: 60px; - width: 100%; - float: left; - background-image: linear-gradient(to top, #77777766, #77777766 50%, transparent 50%, transparent); - background-position: 100% 10%; - background-size: 100% 200%; -} - -.logo a { - display: block; -} - -.logo img{ - max-height: 55px; - height: auto; - margin: auto; -} - -.welcome, .icon-menu, .connect { - height: 30px; - text-align: right; - flex-flow: nowrap; -} - -.welcome { - height: 30px; - font-size: 0.65em; - overflow: hidden; - vertical-align: bottom; -} - -.welcome a, .welcome a:visited { - font-size: 1.25em; - font-weight: bold; - color: #03b595; -} - -.welcome p { - line-height: 45px; -} - -.icon-menu a { - width: 30px; - height: 30px; - margin: 0 0 0 10px; - display: block; - font-size: 0; -} - -/****** temporary disable icons ******/ -.icon-menu a { - opacity: 0.2; - pointer-events: none; -} - -.icon-menu a.btnLang { - background: url(../images/icons/icoLng.png) no-repeat; - background-position: center center; - background-size: contain; - margin: 0 0 0 auto; - font-size: 0.8em; - font-weight: bold; - font-family: monospace; - text-transform: uppercase; - color: #000; - padding: 10px; - height: 10px; - width: 10px; - line-height: 13px; - text-align: center; - overflow: hidden; -} - -.icon-menu a.btnSearch { - background: url(../images/icons/icoSrc.png) no-repeat; - background-position: center center; - background-size: contain; -} - -.icon-menu a.btnHelp { - background: url(../images/icons/icoHlp.png) no-repeat; - background-position: center center; - background-size: contain; -} - -.trigger-lang-modal { - margin: 0 0 0 auto; -} - -.contCam { - min-width: 90px; -} - -div.contCam:before { - content: ''; - display: block; - height: 0; - width: 0; - position: relative; - top: -21px; - left: 45px; - border-color: #c9c9c9 transparent transparent #c9c9c9; - border-style: solid; - border-width: 7px; - pointer-events: none; - z-index: 1; -} - -.animCam { - width: 40px; - height: 40px; - margin: -25px auto 0; - background: #03b595; -} - -.useful-links a.cambtn { - width: 40px; - height: 40px; - margin: 0; - padding: 0; - display: block; - text-indent: -999999px; - background: url(../images/icons/icoARV.png) no-repeat; - background-position: center center; - background-size: contain; -} - -.useful-links { - margin-bottom: 20px; - font-size: 0.75em; - font-weight: bold; - clear: both; - font-size: 75%; -} - -.useful-links .container { - height: 50px; - margin: auto; -} - -.useful-links a, -.footer a.useful { - padding: 0 5px; - background-image: linear-gradient(to top, #05f7ae, #05f7ae 50%, transparent 50%, transparent); - background-position: 100% 15%; - background-size: 100% 200%; -} - -.fill { - background: url(../images/icons/fill.png), #e6e6e6; - padding: 20px; - width: calc(100% - 40px); - margin-bottom: 20px; -} - -h1.titExb, h1.titArt, h1.titMrk, h1.titObj { - height: 40px; - line-height: 45px; - text-align: left; - padding-left: 35px; - width: calc(100% - 35px); - background-position: left center; - background-size: 25px 25px; - background-repeat: no-repeat; - border-bottom: 10px solid #f1f1f1; -} - -h1.titExb { - background-image: url(../images/icons/icoExb.png); -} - -h1.titArt { - background-image: url(../images/icons/icoArt.png); -} - -h1.titMrk { - background-image: url(../images/icons/icoMrk.png); -} - -h1.titObj { - background-image: url(../images/icons/icoObj.png); -} - -.footer { - min-height: 90px; -} - -.footer .container { - height: 25px; - margin-bottom: 20px; -} - -.footer a { - font-size: 75%; - line-height: 25px; - margin: auto; -} - -.footer a.useful { - margin: 0; - line-height: 25px; -} - -a.github { - background-image: url(../images/icons/bulGit.png); - background-size: contain; - background-position: left center; - background-repeat: no-repeat; - color: #03b595; - padding-left: 35px; -} - -a.license { - background-image: url(../images/icons/license.png); - background-size: contain; - background-position: left center; - background-repeat: no-repeat; - padding-left: 65px; - font-size: 0.5em; - line-height: 20px; - margin-left: 0; - text-align: left; - white-space: nowrap; - max-width: calc(100% - 160px); - overflow: hidden; - text-indent: -9999999px; -} - -a.memelab { - background-image: url(../images/icons/memelab.png); - background-size: contain; - background-position: left center; - background-repeat: no-repeat; - width: 20px; - height: 20px; - text-indent: -99999px; - max-width: 35px; - margin: 0; -} - -a.instagram, a.twitter, a.facebook, a.telegram, a.sq-github { - background-size: contain; - background-position: left center; - background-repeat: no-repeat; - width: 25px; - height: 25px; - text-indent: -99999px; - max-width: 35px; - margin: 5px 0px 5px 5px; -} - -a.instagram { - background-image: url(../images/icons/icoIst.png); -} - -a.twitter { - background-image: url(../images/icons/icoTwt.png); -} - -a.facebook { - background-image: url(../images/icons/icoFcb.png); -} - -a.telegram { - background-image: url(../images/icons/icoTel.png); -} - -a.sq-github { - background-image: url(../images/icons/icoGit.png); -} - -.form-options { - text-align: left; -} - -.artwork-elements .separator { - width: 10px; - height: 50px; - max-height: 50px; - min-height: 50px; - background: #06f6ab; - margin: 0 10px 0 -5px; -} - -.artwork-elements img.trigger-modal { - margin: 0px 15px 10px; -} - -.artwork-elements .separator:after { - content: ''; - display: block; - height: 0; - width: 0; - position: relative; - top: 15px; - left: 10px; - border-color: #06f6ab transparent transparent #06f6ab; - border-style: solid; - border-width: 7px; - pointer-events: none; - z-index: 1; -} - -/* Media Queries */ -@media all and (min-width: 800px) { - .container { - max-width: 600px; - margin: 0 auto; - } - - .colorful .container { - padding: 20px; - max-width: 560px; - } - - .header { - margin-top: 25px; - } - - .aside { - max-width: 250px; - min-width: 250px; - height: 100px; - margin-left: -250px; - } - - .half { - width: 270px; - } - - .lg-one-third { - flex-basis: calc(33% - 10px); - } - - .lg-half { - flex-basis: calc(50% - 10px); - } - - .lg-full { - flex-basis: 100%; - } - - .logo { - text-align: left; - height: 95px; - width: calc(100% - 160px); - float: left; - background-image: linear-gradient(to top, #77777766, #77777766 50%, transparent 50%, transparent); - background-position: 100% 10%; - background-size: 100% 200%; - } - - .welcome, .icon-menu, .connect { - height: 50px; - text-align: right; - font-size: 0.75em; - } - - .welcome, .welcome p { - line-height: 80px; - font-size: 1em; - } - - .header .logo img{ - max-height: 75px; - height: auto; - margin: auto; - } - - .icon-menu a { - width: 40px; - height: 40px; - margin: 0 0 0 10px; - } - - .icon-menu a.btnLang { - height: 20px; - width: 19px; - line-height: 23px; - font-size: 2.5em; - } - - .useful-links a, a.useful { - padding: 5px; - background-image: linear-gradient(to top, #05f7ae, #05f7ae 50%, transparent 50%, transparent); - background-position: 100% 15%; - background-size: 100% 200%; - } - - .contCam { - min-width: 250px; - } - - div.contCam:before { - top: -10px; - left: 125px; - border-width: 10px; - } - - .animCam { - width: 40px; - height: 40px; - margin: 0 auto; - margin-top: -20px; - background: #03b595; - } - - .useful-links a.cambtn { - width: 40px; - height: 40px; - margin: 0; - padding: 0; - display: block; - text-indent: -999999px; - background: url(../images/icons/icoARV.png) no-repeat; - background-position: center center; - background-size: contain; - } - - .footer { - min-height: 100px; - } - - .footer .container { - height: 30px; - margin-bottom: 40px; - } - - .footer a { - line-height: 30px; - } - - a.instagram, a.twitter, a.facebook, a.telegram, a.sq-github { - width: 30px; - height: 30px; - } - - a.github { - padding-left: 40px; - } - - a.license { - padding-left: 100px; - height: 30px; - line-height: 30px; - font-size: 0.75em; - text-indent: 0; - } - - a.memelab { - width: 30px; - height: 30px; - max-width: 40px; - } -} - -/* Hover Elements */ -.useful-links a, -.footer a.useful, -.account-options a, -.header .logo img, -.animCam, -.memelab, -.social a, -.editProf a.option-link, -.welcome a, -a.changeExb, -a.jandigIco, -.signup-btn, -.login-btn, -.trigger-lang-modal, -.license { - transition: all .2s ease-in; -} - -.useful-links a:hover, -.footer a.useful:hover, -.account-options a:hover { - color: #FFF; - background-position: 0 100%; -} - -.header .logo img:hover, -.animCam:hover, -.memelab:hover, -.social a:hover, -a.changeExb:hover, -a.jandigIco:hover, -.signup-btn:hover, -.login-btn:hover, -.trigger-lang-modal:hover, -.license:hover { - opacity: 0.7; -} - -.editProf a.option-link:hover, -.welcome a:hover { - color: #06f7ae; -} - - -/****** Animation ********/ -.animCam { - animation: colorchange 4s infinite; /* animation-name followed by duration in seconds*/ - -webkit-animation: colorchange 4s infinite; /* Chrome and Safari */ - } - -@keyframes colorchange - { - 0% {background: #03b595;} - 50% {background: #06f7ae;} - 100% {background: #03b595;} - } - -@-webkit-keyframes colorchange - { - 0% {background: #03b595;} - 50% {background: #06f7ae;} - 100% {background: #03b595;} - } \ No newline at end of file diff --git a/src/ARte/core/urls.py b/src/ARte/core/urls.py deleted file mode 100644 index eb0c123a..00000000 --- a/src/ARte/core/urls.py +++ /dev/null @@ -1,20 +0,0 @@ -from django.urls import path, re_path, include -from .views import artwork_preview, see_all, service_worker, index, upload_image, exhibit_select, collection, exhibit_detail, manifest, robots_txt -from .views_s.home import home, ar_viewer, community, marker_generator, documentation - -urlpatterns = [ - path('', home, name='home'), - path('documentation/', documentation, name='documentation'), - path('community/', community, name='community'), - path('collection/', collection, name='collection'), - path('exhibit_select/', exhibit_select, name='exhibit_select'), - path('exhibit/', exhibit_detail, name="exhibit-detail"), - path('artwork/', artwork_preview, name="artwork-preview"), - path('generator/', marker_generator, name='marker-generator'), - path('sw.js', service_worker, name='sw'), - path('manifest.json', manifest, name='manifest'), - path('upload', upload_image, name='upload-image'), - path('i18n/', include('django.conf.urls.i18n')), - path('see_all/', see_all, name='see-all'), - path('robots.txt/', robots_txt), -] diff --git a/src/ARte/core/views.py b/src/ARte/core/views.py deleted file mode 100644 index f93e5d92..00000000 --- a/src/ARte/core/views.py +++ /dev/null @@ -1,126 +0,0 @@ -from django.http import HttpResponseRedirect, HttpResponse -from django.shortcuts import render -from django.urls import reverse -from django.shortcuts import redirect -from django.views.decorators.cache import cache_page -from django.views.decorators.http import require_http_methods - -from .helpers import handle_upload_image -from .forms import UploadFileForm, ExhibitForm - -from .models import Exhibit, Artwork, Marker, Object - - - -@cache_page(60 * 60) -@require_http_methods(["GET"]) -def service_worker(request): - return render(request, 'core/sw.js', - content_type='application/x-javascript') - - -@cache_page(60 * 60) -@require_http_methods(["GET"]) -def manifest(request): - return render(request, 'core/manifest.json', - content_type='application/x-javascript') - - -def index(request): - ctx = { - "artworks": [ - ] - } - - return render(request, 'core/exhibit.jinja2', ctx) - - -@cache_page(60 * 2) -@require_http_methods(["GET"]) -def collection(request): - - exhibits = Exhibit.objects.all().order_by('-id')[:4] - artworks = Artwork.objects.all().order_by('-id')[:6] - markers = Marker.objects.all().order_by('-id')[:8] - objects = Object.objects.all().order_by('-id')[:8] - - ctx = { - "artworks": artworks, - "exhibits": exhibits, - "markers": markers, - "objects": objects, - "seeall": False, - } - - return render(request, 'core/collection.jinja2', ctx) - - -@cache_page(60 * 2) -@require_http_methods(["GET"]) -def see_all(request): - request_type = request.GET.get('which') - ctx = {} - if request_type == 'objects': - ctx = {'objects': Object.objects.all(), "seeall": True, } - elif request_type == 'markers': - ctx = {'markers': Marker.objects.all(), "seeall": True, } - elif request_type == 'artworks': - ctx = {'artworks': Artwork.objects.all(), "seeall": True, } - elif request_type == 'exhibits': - ctx = {'exhibits': Exhibit.objects.all(), "seeall": True, } - - return render(request, 'core/collection.jinja2', ctx) - -def upload_image(request): - if request.method == 'POST': - form = UploadFileForm(request.POST, request.FILES) - image = request.FILES.get('file') - if form.is_valid() and image: - handle_upload_image(image) - return HttpResponseRedirect(reverse('index')) - else: - form = UploadFileForm() - return render(request, 'core/upload.jinja2', {'form': form}) - - -def exhibit_select(request): - if request.method == 'POST': - form = ExhibitForm(request.POST) - if form.is_valid(): - exhibit = form.cleaned_data.get('exhibit') - return redirect("/" + exhibit.slug) - else: - form = ExhibitForm() - - return render(request, 'core/exhibit_select.jinja2', {'form': form}) - - -@cache_page(60 * 60) -@require_http_methods(["GET"]) -def exhibit_detail(request): - index = request.GET.get("id") - exhibit = Exhibit.objects.get(id=index) - ctx = { - 'exhibit': exhibit, - 'exhibitImage': "https://cdn3.iconfinder.com/data/icons/basic-mobile-part-2/512/painter-512.png", - 'artworks': exhibit.artworks.all() - } - return render(request, 'core/exhibit_detail.jinja2', ctx) - -@require_http_methods(["GET"]) -def artwork_preview(request): - artwork_id = request.GET.get("id") - - ctx = { - "artworks": Artwork.objects.filter(id=artwork_id) - } - return render(request, 'core/exhibit.jinja2', ctx) - - -@require_http_methods(["GET"]) -def robots_txt(request): - lines = [ - "User-Agent: *", - "Disallow: ", - ] - return HttpResponse("\n".join(lines), content_type="text/plain") diff --git a/src/ARte/core/views_s/home.py b/src/ARte/core/views_s/home.py deleted file mode 100644 index 3b33a504..00000000 --- a/src/ARte/core/views_s/home.py +++ /dev/null @@ -1,17 +0,0 @@ -from django.shortcuts import render, Http404 -from django.urls import reverse - -def home(request): - return render(request, 'users/profile.jinja2',{}) - -def documentation(request): - return render(request, 'core/documentation.jinja2',{}) - -def ar_viewer(request): - return render(request, 'core/exhibit.jinja2') - -def community(request): - return render(request, 'core/community.jinja2') - -def marker_generator(request): - return render(request,'core/generator.html',{}) diff --git a/src/ARte/manage.py b/src/ARte/manage.py deleted file mode 100755 index 5c0f0af3..00000000 --- a/src/ARte/manage.py +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env python -import os -import sys - -if __name__ == '__main__': - os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') - try: - from django.core.management import execute_from_command_line - except ImportError as exc: - raise ImportError( - "Couldn't import Django. Are you sure it's installed and " - "available on your PYTHONPATH environment variable? Did you " - "forget to activate a virtual environment?" - ) from exc - execute_from_command_line(sys.argv) diff --git a/src/ARte/users/admin.py b/src/ARte/users/admin.py deleted file mode 100644 index 9a6a54e5..00000000 --- a/src/ARte/users/admin.py +++ /dev/null @@ -1,4 +0,0 @@ -from django.contrib import admin -from users.models import Profile - -admin.site.register(Profile) \ No newline at end of file diff --git a/src/ARte/users/choices.py b/src/ARte/users/choices.py deleted file mode 100644 index 6808e15d..00000000 --- a/src/ARte/users/choices.py +++ /dev/null @@ -1,240 +0,0 @@ - -COUNTRY_CHOICES = ( - ('AF', 'AFGHANISTAN'), - ('AL', 'ALBANIA'), - ('DZ', 'ALGERIA'), - ('AS', 'AMERICAN SAMOA'), - ('AD', 'ANDORRA'), - ('AO', 'ANGOLA'), - ('AI', 'ANGUILLA'), - ('AQ', 'ANTARCTICA'), - ('AG', 'ANTIGUA AND BARBUDA'), - ('AR', 'ARGENTINA'), - ('AM', 'ARMENIA'), - ('AW', 'ARUBA'), - ('AU', 'AUSTRALIA'), - ('AT', 'AUSTRIA'), - ('AZ', 'AZERBAIJAN'), - ('BS', 'BAHAMAS'), - ('BH', 'BAHRAIN'), - ('BD', 'BANGLADESH'), - ('BB', 'BARBADOS'), - ('BY', 'BELARUS'), - ('BE', 'BELGIUM'), - ('BZ', 'BELIZE'), - ('BJ', 'BENIN'), - ('BM', 'BERMUDA'), - ('BT', 'BHUTAN'), - ('BO', 'BOLIVIA'), - ('BA', 'BOSNIA AND HERZEGOVINA'), - ('BW', 'BOTSWANA'), - ('BV', 'BOUVET ISLAND'), - ('BR', 'BRAZIL'), - ('IO', 'BRITISH INDIAN OCEAN TERRITORY'), - ('BN', 'BRUNEI DARUSSALAM'), - ('BG', 'BULGARIA'), - ('BF', 'BURKINA FASO'), - ('BI', 'BURUNDI'), - ('KH', 'CAMBODIA'), - ('CM', 'CAMEROON'), - ('CA', 'CANADA'), - ('CV', 'CAPE VERDE'), - ('KY', 'CAYMAN ISLANDS'), - ('CF', 'CENTRAL AFRICAN REPUBLIC'), - ('TD', 'CHAD'), - ('CL', 'CHILE'), - ('CN', "PEOPLE'S REPUBLIC OF CHINA"), - ('CX', 'ISLAND'), - ('CC', 'COCOS (KEELING) ISLANDS'), - ('CO', 'COLOMBIA'), - ('KM', 'COMOROS'), - ('CG', 'CONGO'), - ('CD', 'CONGO, THE DEMOCRATIC REPUBLIC OF'), - ('CK', 'COOK ISLANDS'), - ('CR', 'COSTA RICA'), - ('CI', "CÔTE D'IVOIRE"), - ('HR', 'CROATIA'), - ('CU', 'CUBA'), - ('CY', 'CYPRUS'), - ('CZ', 'CZECH REPUBLIC'), - ('DK', 'DENMARK'), - ('DJ', 'DJIBOUTI'), - ('DM', 'DOMINICA'), - ('DO', 'DOMINICAN REPUBLIC'), - ('EC', 'ECUADOR'), - ('EG', 'EGYPT'), - ('EH', 'WESTERN SAHARA'), - ('SV', 'EL SALVADOR'), - ('GQ', 'EQUATORIAL GUINEA'), - ('ER', 'ERITREA'), - ('EE', 'ESTONIA'), - ('ET', 'ETHIOPIA'), - ('FK', 'FALKLAND ISLANDS (MALVINAS)'), - ('FO', 'ISLANDS'), - ('FJ', 'FIJI'), - ('FI', 'FINLAND'), - ('FR', 'FRANCE'), - ('GF', 'FRENCH GUIANA'), - ('PF', 'FRENCH POLYNESIA'), - ('TF', 'FRENCH SOUTHERN TERRITORIES'), - ('GA', 'GABON'), - ('GM', 'GAMBIA'), - ('GE', 'GEORGIA'), - ('DE', 'GERMANY'), - ('GH', 'GHANA'), - ('GI', 'GIBRALTAR'), - ('GR', 'GREECE'), - ('GL', 'GREENLAND'), - ('GD', 'GRENADA'), - ('GP', 'GUADELOUPE'), - ('GU', 'GUAM'), - ('GT', 'GUATEMALA'), - ('GN', 'GUINEA'), - ('GW', 'GUINEA-BISSAU'), - ('GY', 'GUYANA'), - ('HT', 'HAITI'), - ('HM', 'HEARD ISLAND AND MCDONALD ISLANDS'), - ('HN', 'HONDURAS'), - ('HK', 'HONG KONG'), - ('HU', 'HUNGARY'), - ('IS', 'ICELAND'), - ('IN', 'INDIA'), - ('ID', 'INDONESIA'), - ('IR', 'IRAN, ISLAMIC REPUBLIC OF'), - ('IQ', 'IRAQ'), - ('IE', 'IRELAND'), - ('IL', 'ISRAEL'), - ('IT', 'ITALY'), - ('JM', 'JAMAICA'), - ('JP', 'JAPAN'), - ('JO', 'JORDAN'), - ('KZ', 'KAZAKHSTAN'), - ('KE', 'KENYA'), - ('KI', 'KIRIBATI'), - ('KP', "KOREA, DEMOCRATIC PEOPLE'S REPUBLIC OF"), - ('KR', 'KOREA, REPUBLIC OF'), - ('KW', 'KUWAIT'), - ('KG', 'KYRGYZSTAN'), - ('LA', "LAO PEOPLE'S DEMOCRATIC REPUBLIC"), - ('LV', 'LATVIA'), - ('LB', 'LEBANON'), - ('LS', 'LESOTHO'), - ('LR', 'LIBERIA'), - ('LY', 'LIBYAN ARAB JAMAHIRIYA'), - ('LI', 'LIECHTENSTEIN'), - ('LT', 'LITHUANIA'), - ('LU', 'LUXEMBOURG'), - ('MO', 'MACAO'), - ('MK', 'MACEDONIA, THE FORMER YUGOSLAV REPUBLIC OF'), - ('MG', 'MADAGASCAR'), - ('MW', 'MALAWI'), - ('MY', 'MALAYSIA'), - ('MV', 'MALDIVES'), - ('ML', 'MALI'), - ('MT', 'MALTA'), - ('MH', 'MARSHALL ISLANDS'), - ('MQ', 'MARTINIQUE'), - ('MR', 'MAURITANIA'), - ('MU', 'MAURITIUS'), - ('YT', 'MAYOTTE'), - ('MX', 'MEXICO'), - ('FM', 'MICRONESIA, FEDERATED STATES OF'), - ('MD', 'MOLDOVA, REPUBLIC OF'), - ('MC', 'MONACO'), - ('MN', 'MONGOLIA'), - ('MS', 'MONTSERRAT'), - ('MA', 'MOROCCO'), - ('MZ', 'MOZAMBIQUE'), - ('MM', 'MYANMAR'), - ('NA', 'NAMIBIA'), - ('NR', 'NAURU'), - ('NP', 'NEPAL'), - ('NL', 'NETHERLANDS'), - ('AN', 'NETHERLANDS ANTILLES'), - ('NC', 'NEW CALEDONIA'), - ('NZ', 'NEW ZEALAND'), - ('NI', 'NICARAGUA'), - ('NE', 'NIGER'), - ('NG', 'NIGERIA'), - ('NU', 'NIUE'), - ('NF', 'NORFOLK ISLAND'), - ('MP', 'NORTHERN MARIANA ISLANDS'), - ('NO', 'NORWAY'), - ('OM', 'OMAN'), - ('PK', 'PAKISTAN'), - ('PW', 'PALAU'), - ('PS', 'PALESTINIAN TERRITORY, OCCUPIED'), - ('PA', 'PANAMA'), - ('PG', 'PAPUA NEW GUINEA'), - ('PY', 'PARAGUAY'), - ('PE', 'PERU'), - ('PH', 'PHILIPPINES'), - ('PN', 'PITCAIRN'), - ('PL', 'POLAND'), - ('PT', 'PORTUGAL'), - ('PR', 'PUERTO RICO'), - ('QA', 'QATAR'), - ('RE', 'RÉUNION'), - ('RO', 'ROMANIA'), - ('RU', 'RUSSIAN FEDERATION'), - ('RW', 'RWANDA'), - ('SH', 'SAINT HELENA'), - ('KN', 'SAINT KITTS AND NEVIS'), - ('LC', 'SAINT LUCIA'), - ('PM', 'SAINT PIERRE AND MIQUELON'), - ('VC', 'SAINT VINCENT AND THE GRENADINES'), - ('WS', 'SAMOA'), - ('SM', 'SAN MARINO'), - ('ST', 'SAO TOME AND PRINCIPE'), - ('SA', 'SAUDI ARABIA'), - ('SN', 'SENEGAL'), - ('CS', 'SERBIA AND MONTENEGRO'), - ('SC', 'SEYCHELLES'), - ('SL', 'SIERRA LEONE'), - ('SG', 'SINGAPORE'), - ('SK', 'SLOVAKIA'), - ('SI', 'SLOVENIA'), - ('SB', 'SOLOMON ISLANDS'), - ('SO', 'SOMALIA'), - ('ZA', 'SOUTH AFRICA'), - ('GS', 'SOUTH GEORGIA AND SOUTH SANDWICH ISLANDS'), - ('ES', 'SPAIN'), - ('LK', 'SRI LANKA'), - ('SD', 'SUDAN'), - ('SR', 'SURINAME'), - ('SJ', 'SVALBARD AND JAN MAYEN'), - ('SZ', 'SWAZILAND'), - ('SE', 'SWEDEN'), - ('CH', 'SWITZERLAND'), - ('SY', 'SYRIAN ARAB REPUBLIC'), - ('TW', 'TAIWAN, REPUBLIC OF CHINA'), - ('TJ', 'TAJIKISTAN'), - ('TZ', 'TANZANIA, UNITED REPUBLIC OF'), - ('TH', 'THAILAND'), - ('TL', 'TIMOR-LESTE'), - ('TG', 'TOGO'), - ('TK', 'TOKELAU'), - ('TO', 'TONGA'), - ('TT', 'TRINIDAD AND TOBAGO'), - ('TN', 'TUNISIA'), - ('TR', 'TURKEY'), - ('TM', 'TURKMENISTAN'), - ('TC', 'TURKS AND CAICOS ISLANDS'), - ('TV', 'TUVALU'), - ('UG', 'UGANDA'), - ('UA', 'UKRAINE'), - ('AE', 'UNITED ARAB EMIRATES'), - ('GB', 'UNITED KINGDOM'), - ('US', 'UNITED STATES'), - ('UM', 'UNITED STATES MINOR OUTLYING ISLANDS'), - ('UY', 'URUGUAY'), - ('UZ', 'UZBEKISTAN'), - ('VE', 'VENEZUELA'), - ('VU', 'VANUATU'), - ('VN', 'VIET NAM'), - ('VG', 'BRITISH VIRGIN ISLANDS'), - ('VI', 'U.S. VIRGIN ISLANDS'), - ('WF', 'WALLIS AND FUTUNA'), - ('YE', 'YEMEN'), - ('ZW', 'ZIMBABWE'), -) \ No newline at end of file diff --git a/src/ARte/users/factory.py b/src/ARte/users/factory.py deleted file mode 100644 index cb7c5fc6..00000000 --- a/src/ARte/users/factory.py +++ /dev/null @@ -1,26 +0,0 @@ -import factory -from factory.django import DjangoModelFactory - -from .models import Object -from django.contrib.auth.models import User - -class UserFactory(DjangoModelFactory): - username = 'Testador' - email = 'testador@memelab.com' - - class Meta: - model = User - -class ObjectFactory(DjangoModelFactory): - id = 1 - source = 'objects/osaka.gif' - owner = 'Matheus' - author = 'Matheus' - uploaded_at = (2020, 11, 25, 14, 30, 0) - title = 'osaka' - position = '0 0 0' - scale = '1 1' - rotation = '270 0 0' - class Meta: - - model = Object \ No newline at end of file diff --git a/src/ARte/users/forms.py b/src/ARte/users/forms.py deleted file mode 100644 index c3bf2420..00000000 --- a/src/ARte/users/forms.py +++ /dev/null @@ -1,251 +0,0 @@ -import re - -import logging -log = logging.getLogger('ej') - -from django import forms -from django.contrib.auth import get_user_model, authenticate -from django.contrib.auth.forms import UserCreationForm, AuthenticationForm -from django.contrib.auth.forms import PasswordChangeForm as OrigPasswordChangeForm -from django.utils.translation import ugettext_lazy as _ -from django.forms.widgets import HiddenInput - -from core.models import Marker, Object, Artwork -from .models import Profile - -User = get_user_model() - -class SignupForm(UserCreationForm): - """ - Form to register a new user - """ - - email = forms.EmailField( - max_length=254, - help_text=_('Your e-mail address'), - ) - - username = forms.CharField( - max_length=12, - help_text=_('Your username'), - ) - - def __init__(self, *args, **kwargs): - super(SignupForm, self).__init__(*args, **kwargs) - - self.fields['email'].widget.attrs['placeholder'] = _('email') - self.fields['username'].widget.attrs['placeholder'] = _('chosen username') - self.fields['password1'].widget.attrs['placeholder'] = _('password') - self.fields['password2'].widget.attrs['placeholder'] = _('confirm password') - - class Meta: - model = User - fields = ['email', 'username', 'password1', 'password2'] - - def clean_email(self): - email = self.cleaned_data.get('email') - username = self.cleaned_data.get('username') - if email and User.objects.filter(email=email).exclude(username=username).exists(): - raise forms.ValidationError(_('E-mail taken')) - - return email - -from .choices import COUNTRY_CHOICES - -class PasswordChangeForm(OrigPasswordChangeForm): - def __init__(self, *args, **kwargs): - super(PasswordChangeForm, self).__init__(*args, **kwargs) - self.fields['old_password'].widget.attrs['placeholder'] = _('Old Password') - self.fields['new_password1'].widget.attrs['placeholder'] = _('New Password') - self.fields['new_password2'].widget.attrs['placeholder'] = _('New Password Again') - - -class ProfileForm(forms.ModelForm): - class Meta: - model = User - fields = ['username','email', 'bio', 'country', 'personal_site'] - field_order=['email', 'username', 'personal_site', 'country', 'bio'] - - def __init__(self, *args, **kwargs): - super(ProfileForm, self).__init__(*args, **kwargs) - self.initial['username'] = self.instance.user.username - - # FIXME: user.email come as a string of a tuple, no idea why. "('email@bla.com',)" - # email = self.instance.user.email.replace("('", "").replace("',)", "") - self.initial['email'] = self.instance.user.email - self.initial['bio'] = self.instance.bio - self.initial['country'] = self.instance.country - self.initial['personal_site'] = self.instance.personal_site - self.fields['email'].widget.attrs['placeholder'] = _('E-mail') - self.fields['username'].widget.attrs['placeholder'] = _('Username') - self.fields['bio'].widget.attrs['placeholder'] = _('Personal Bio / Description') - self.fields['personal_site'].widget.attrs['placeholder'] = _('Personal Website') - - email = forms.EmailField( - max_length=254, - help_text=_('Your e-mail address'), - ) - username = forms.CharField( - max_length=12, - help_text=_('Your username'), - ) - country = forms.ChoiceField( - choices=COUNTRY_CHOICES, - required=False - ) - bio = forms.CharField( - max_length=500, - required=False, - widget=forms.Textarea, - help_text=_('Personal Bio / Description'), - ) - personal_site = forms.URLField( - required=False, - help_text=_('Personal Website'), - ) - - def clean_username(self): - username = self.cleaned_data.get('username') - if username and User.objects.filter(username=username).exclude(username=self.instance.user.username).exists(): - raise forms.ValidationError(_('Username already in use')) - return username - - def clean_email(self): - email = self.cleaned_data.get('email') - if email and User.objects.filter(email=email).exclude(username=self.instance.user.username).exists(): - raise forms.ValidationError(_('Email address must be unique')) - - return email - -class LoginForm(AuthenticationForm): - def __init__(self, *args, **kwargs): - super(LoginForm, self).__init__(*args, **kwargs) - self.fields['username'].widget.attrs['placeholder'] = _('username / email') - self.fields['password'].widget.attrs['placeholder'] = _('password') - - def clean_username(self): - username_or_email = self.cleaned_data.get('username') - if '@' in username_or_email: - if not User.objects.filter(email=username_or_email).exists(): - raise forms.ValidationError(_('Username/email not found')) - user = User.objects.get(email=username_or_email) - if user: - return user.username - else: - if not User.objects.filter(username=username_or_email).exists(): - raise forms.ValidationError(_('Username/email not found')) - - # Already is a valid username - return username_or_email - - def clean_password(self): - password = self.cleaned_data.get('password') - username_or_email = self.data.get('username') - user = None - username_or_email_wrong = False - - if '@' in username_or_email: - if User.objects.filter(email=username_or_email).exists(): - username = User.objects.get(email=username_or_email).username - user = authenticate(username = username, password=password) - else: - username_or_email_wrong = True - # raise forms.ValidationError(_('Email Wrong!')) - else: - if User.objects.filter(username=username_or_email).exists(): - user = authenticate(username=username_or_email, password=password) - else: - username_or_email_wrong = True - # raise forms.ValidationError(_('Username Wrong!')) - - if not user and not username_or_email_wrong: - raise forms.ValidationError(_('Wrong password!')) - - return password - - -class RecoverPasswordForm(forms.Form): - username_or_email = forms.CharField(label='username / email', max_length="50") - -class RecoverPasswordCodeForm(forms.Form): - verification_code = forms.CharField(label='Verification code', max_length="200") - - -class UploadMarkerForm(forms.ModelForm): - - def __init__(self, *args, **kwargs): - super(UploadMarkerForm, self).__init__(*args, **kwargs) - - log.warning(self.fields) - self.fields['source'].widget.attrs['placeholder'] = _('browse file') - self.fields['source'].widget.attrs['accept'] = 'image/png, image/jpg' - self.fields['patt'].widget.attrs['placeholder'] = _('browse file') - self.fields['patt'].widget.attrs['accept'] = '.patt' - self.fields['author'].widget.attrs['placeholder'] = _('declare different author name') - self.fields['title'].widget.attrs['placeholder'] = _("Marker's title") - - class Meta: - model = Marker - exclude = ('owner', 'uploaded_at') - - - -class UploadObjectForm(forms.ModelForm): - - def __init__(self, *args, **kwargs): - super(UploadObjectForm, self).__init__(*args, **kwargs) - - self.fields['source'].widget.attrs['placeholder'] = _('browse file') - self.fields['source'].widget.attrs['accept'] = 'image/*, .mp4, .webm' - self.fields['author'].widget.attrs['placeholder'] = _('declare different author name') - self.fields['scale'].widget = HiddenInput() - self.fields['rotation'].widget = HiddenInput() - self.fields['position'].widget = HiddenInput() - self.fields['title'].widget.attrs['placeholder'] = _("Object's title") - log.warning(self.fields) - - class Meta: - model = Object - exclude = ('uploaded_at', 'owner') - - -class ArtworkForm(forms.Form): - - marker = forms.ImageField(required=False) - marker_author = forms.CharField(max_length=12, required=False) - augmented = forms.ImageField(required=False) - augmented_author = forms.CharField(max_length=12, required=False) - existent_marker = forms.IntegerField(min_value=1, required=False) - existent_object = forms.IntegerField(min_value=1, required=False) - title = forms.CharField(max_length=50) - description = forms.CharField(widget=forms.Textarea, max_length=500, required=False) - - def __init__(self, *args, **kwargs): - super(ArtworkForm, self).__init__(*args, **kwargs) - - self.fields['marker_author'].widget.attrs['placeholder'] = _('declare different author name') - self.fields['augmented_author'].widget.attrs['placeholder'] = _('declare different author name') - self.fields['title'].widget.attrs['placeholder'] = _('Artwork title') - self.fields['description'].widget.attrs['placeholder'] = _('Artwork description') - - - -class ExhibitForm(forms.Form): - - name = forms.CharField(max_length=50, required=True) - slug = forms.CharField(max_length=50, required=True) - - # FIXME: maybe this can be improved. Possible bug on max artworks per exhibit - artworks = forms.CharField(max_length=1000) - - def clean_slug(self): - data = self.cleaned_data['slug'] - if not re.match("^[a-zA-Z0-9_]*$", data): - raise forms.ValidationError(_("Url can't contain spaces or special characters")) - return data - - def __init__(self, *args, **kwargs): - super(ExhibitForm, self).__init__(*args, **kwargs) - - self.fields['name'].widget.attrs['placeholder'] = _('Exhibit Title') - self.fields['slug'].widget.attrs['placeholder'] = _('Complete with your Exhibit URL here') diff --git a/src/ARte/users/jinja2/users/artwork-create.jinja2 b/src/ARte/users/jinja2/users/artwork-create.jinja2 deleted file mode 100644 index de97332b..00000000 --- a/src/ARte/users/jinja2/users/artwork-create.jinja2 +++ /dev/null @@ -1,200 +0,0 @@ -{% extends '/core/home.jinja2' %} - - -{% block content %} -
- {# FIXME: maybe this can be improved #} - - - - -

{{ _('Create Jandig Artwork') }}

- - - - -
-{% endblock %} \ No newline at end of file diff --git a/src/ARte/users/services/encrypt_service.py b/src/ARte/users/services/encrypt_service.py deleted file mode 100644 index 7c47d398..00000000 --- a/src/ARte/users/services/encrypt_service.py +++ /dev/null @@ -1,15 +0,0 @@ -import hashlib, secrets -from datetime import datetime - - -class EncryptService(): - def generate_verification_code(self, email): - datetime_now = datetime.now() - today = '{}{}{}{}{}{}{}'.format(datetime_now.year, datetime_now.month, datetime_now.day, datetime_now.hour, datetime_now.minute, datetime_now.second, datetime_now.microsecond) - decrypt_code = str(today) + (email * 4) + secrets.token_hex(16) - verification_code = self.generate_hash_code(decrypt_code) - return verification_code - - def generate_hash_code(self, decrypt_code): - hash_code = hashlib.sha256(bytes(decrypt_code, encoding='utf-8')) - return hash_code.hexdigest() \ No newline at end of file diff --git a/src/ARte/users/tests.py b/src/ARte/users/tests.py deleted file mode 100644 index 889fcfd4..00000000 --- a/src/ARte/users/tests.py +++ /dev/null @@ -1,100 +0,0 @@ -import factory - -from django.test import TestCase, Client, RequestFactory -from unittest import mock -from .views import edit_object, recover_password -from .services.email_service import EmailService -from .services.encrypt_service import EncryptService -from .services.user_service import UserService -from .factory import ObjectFactory, UserFactory - -class UserTestCase(TestCase): - def setUp(self): - self.client_test = RequestFactory() - self.email_service = EmailService('You have requested a new password.') - self.encrypt_service = EncryptService() - self.user_service = UserService() - - request = self.client_test.get('/recover/', follow=True) - def test_redirect_to_recover_password_page(self): - response = recover_password(request) - self.assertEqual(response.status_code, 200) - - def test_recover_password_invalid_email(self): - request = self.client_test.post('/recover/', {'username_or_email': 'testadorinvalid@memelab.com'}, follow=True) - response = recover_password(request) - self.assertEqual(response.status_code, 302) - self.assertEqual(response.url, '/users/invalid-recovering-email') - - def test_recover_password_invalid_username(self): - request = self.client_test.post('/recover/', {'username_or_email': 'testadorinvalid'}, follow=True) - response = recover_password(request) - self.assertEqual(response.status_code, 302) - self.assertEqual(response.url, '/users/invalid-recovering-email') - - def test_recover_password_valid_email(self): - request = self.client_test.post('/recover/', {'username_or_email': 'testador@memelab.com'}, follow=True) - user_data = UserFactory() - response = recover_password(request) - self.assertEqual(response.status_code, 302) - self.assertEqual(response.url, '/users/recover-code/') - - def test_recover_password_valid_username(self): - request = self.client_test.post('/recover/', {'username_or_email': 'Testador'}, follow=True) - user_data = UserFactory() - response = recover_password(request) - self.assertEqual(response.status_code, 302) - self.assertEqual(response.url, '/users/recover-code/') - - def test_build_multipart_message(self): - email = "testador@memelab.com" - response = self.email_service.build_multipart_message(email) - self.assertEquals(response['From'], "jandig@memelab.com.br") - self.assertEquals(response['To'], email) - - @mock.patch('users.services.email_service.smtplib.SMTP.quit') - def test_send_email_to_recover_password(self, mock_quit): - email = "testador@memelab.com" - response = self.email_service.build_multipart_message(email) - self.email_service.send_email_to_recover_password(response) - mock_quit.assert_called_once() - - @mock.patch('users.services.encrypt_service.EncryptService.generate_hash_code') - def test_generate_verification_code(self, mock_hash): - email = "testador@memelab.com" - self.encrypt_service.generate_verification_code(email) - mock_hash.assert_called_once() - - def test_check_if_username_or_email_exist(self): - email = "testador@memelab.com" - user_data = UserFactory() - response = self.user_service.check_if_username_or_email_exist(email) - self.assertTrue(response) - - def test_check_if_username_or_email_doesnt_exist(self): - email = "testadorinvalido@memelab.com" - user_data = UserFactory() - response = self.user_service.check_if_username_or_email_exist(email) - self.assertFalse(response) - - def test_get_user_email_by_email(self): - email = "testador@memelab.com" - user_data = UserFactory() - response = self.user_service.get_user_email(email) - self.assertEquals(response, email) - - def test_get_user_email_by_username(self): - username = "Testador" - user_data = UserFactory() - response = self.user_service.get_user_email(username) - self.assertEquals(response, "testador@memelab.com") - - -class EditObjectTestCase(TestCase): - def setUp(self): - self.client_test = RequestFactory() - - def test_redirect(self): - request = self.client_test.get('objects/edit', follow=True) - response = edit_object(request) - self.assertEqual(response.status_code, 200) \ No newline at end of file diff --git a/src/ARte/users/urls.py b/src/ARte/users/urls.py deleted file mode 100644 index b257a25c..00000000 --- a/src/ARte/users/urls.py +++ /dev/null @@ -1,43 +0,0 @@ -from django.urls import path -from django.contrib.auth import views as auth_views - -from .forms import LoginForm -from .views import download_exhibit, edit_object, edit_marker, signup, recover_password, recover_edit_password, invalid_recovering_email_or_username, recover_code, wrong_verification_code, profile, marker_upload, object_upload, create_artwork, create_exhibit, edit_artwork, element_get, edit_exhibit, edit_profile, edit_password, delete, related_content, mod_delete, permission_denied, mod - -urlpatterns = [ - path('signup/', signup, name='signup'), - path('login/', auth_views.LoginView.as_view( - template_name='users/login.jinja2', - authentication_form=LoginForm, - ), name='login'), - path('logout/', auth_views.LogoutView.as_view(), name='logout'), - path('recover/', recover_password, name='recover'), - path('recover-code/', recover_code, name='recover-code'), - - path('profile/', profile, name='profile'), - path('profile/edit/', edit_profile, name="edit-profile"), - path('profile/edit-password/', edit_password, name="edit-password"), - path('wrong-verification-code', wrong_verification_code, name="wrong-verification-code"), - path('invalid-recovering-email', invalid_recovering_email_or_username, name="invalid_recovering_email_or_username"), - path('recover-edit-password', recover_edit_password, name="recover-edit-password"), - - path('markers/upload/', marker_upload, name='marker-upload'), - path('objects/upload/', object_upload, name='object-upload'), - path('element/get/', element_get, name='element-get'), - - path('objects/edit/', edit_object, name='edit-object'), - path('markers/edit/', edit_marker, name='edit-marker'), - - path('artworks/create/', create_artwork, name='create-artwork'), - path('artworks/edit/', edit_artwork, name="edit-artwork"), - - path('exhibits/create/', create_exhibit, name='create-exhibit'), - path('exhibits/edit/', edit_exhibit, name='edit-exhibit'), - path('download-exhibit', download_exhibit, name="download-exhibit"), - - path('content/delete/', delete, name='delete-content'), - path('moderator-page/', mod, name='moderator-page'), - path('permission-denied/', permission_denied, name='permission-denied'), - path('content/mod-delete/', mod_delete, name='mod-delete-content'), - path('related-content', related_content, name='related-content'), -] diff --git a/src/ARte/users/views.py b/src/ARte/users/views.py deleted file mode 100644 index 24e06720..00000000 --- a/src/ARte/users/views.py +++ /dev/null @@ -1,669 +0,0 @@ -import json -import logging -from datetime import datetime -import hashlib -log = logging.getLogger('ej') -from django.contrib.auth import login, authenticate -import django.contrib.auth -from django.contrib.auth.models import User -from django.shortcuts import render, redirect, get_object_or_404 -from django.contrib.auth import update_session_auth_hash, get_user_model -from django.contrib.auth.forms import SetPasswordForm -from django.contrib.auth.decorators import login_required -from django.utils.translation import ugettext_lazy as _ -from django.http import Http404 -from django.http import HttpResponse -from django.views.decorators.cache import cache_page -from django.views.decorators.http import require_http_methods - - -from .forms import SignupForm, RecoverPasswordCodeForm, RecoverPasswordForm, UploadMarkerForm, UploadObjectForm, ArtworkForm, ExhibitForm, ProfileForm, PasswordChangeForm -from core.models import Exhibit, Marker, Object, Artwork -from core.helpers import * -from .models import Profile -from .services.email_service import EmailService -from .services.user_service import UserService -from .services.encrypt_service import EncryptService - - -def signup(request): - - if request.method == 'POST': - form = SignupForm(request.POST) - - if form.is_valid(): - form.save() - username = form.cleaned_data.get('username') - raw_password = form.cleaned_data.get('password1') - user = authenticate(username=username, password=raw_password) - login(request, user) - return redirect('home') - - - else: - form = SignupForm() - - return render(request, 'users/signup.jinja2', {'form': form}) - -User = get_user_model() - -def recover_password(request): - if request.method == 'POST': - recover_password_form = RecoverPasswordForm(request.POST) - - if recover_password_form.is_valid(): - username_or_email = recover_password_form.cleaned_data.get('username_or_email') - user_service = UserService() - username_or_email_is_valid = user_service.check_if_username_or_email_exist(username_or_email) - if (not username_or_email_is_valid): - return redirect('invalid_recovering_email_or_username') - - global global_recovering_email - global_recovering_email = user_service.get_user_email(username_or_email) - - global global_verification_code - encrypt_service = EncryptService() - global_verification_code = encrypt_service.generate_verification_code(global_recovering_email) - - build_message_and_send_to_user(global_recovering_email) - - return redirect('recover-code') - - else: - recover_password_form = RecoverPasswordForm() - - return render(request, 'users/recover-password.jinja2', {'form': recover_password_form}) - -def build_message_and_send_to_user(email): - message = 'You have requested a new password. This is your verification code: {}\nCopy it and put into the field.'.format(global_verification_code) - email_service = EmailService(message) - multipart_message = email_service.build_multipart_message(email) - email_service.send_email_to_recover_password(multipart_message) - - -def recover_code(request): - if request.method == 'POST': - form = RecoverPasswordCodeForm(request.POST) - - if form.is_valid(): - code = form.cleaned_data.get('verification_code') - - log.warning('Inserido: ' + code) - log.warning('Correto: ' + global_verification_code) - - if(code == global_verification_code): - global recover_password_user - recover_password_user = User.objects.get(email=global_recovering_email) - return redirect('recover-edit-password') - else: - return redirect('wrong-verification-code') - - return redirect('home') - else: - form = RecoverPasswordCodeForm() - - return render(request, 'users/recover-password-code.jinja2', {'form': form}) - -def recover_edit_password(request): - if request.method == 'POST': - form = SetPasswordForm(recover_password_user, data=request.POST) - - if form.is_valid(): - form.save() - - return redirect('login') - else: - form = SetPasswordForm(recover_password_user) - - return render(request, 'users/recover-edit-password.jinja2', {'form': form}) - -@require_http_methods(["GET"]) -def wrong_verification_code(request): - return render(request, 'users/wrong-verification-code.jinja2') - -@require_http_methods(["GET"]) -def invalid_recovering_email_or_username(request): - return render(request, 'users/invalid-recovering-email.jinja2') - - -@login_required -@require_http_methods(["GET"]) -def profile(request): - profile = Profile.objects.select_related().get(user=request.user) - - exhibits = profile.exhibits.all() - markers = profile.marker_set.all() - objects = profile.object_set.all() - artworks = profile.artwork_set.all() - - ctx = { - 'exhibits': exhibits, - 'artworks': artworks, - 'markers':markers, - 'objects':objects, - 'profile':True - } - return render(request, 'users/profile.jinja2', ctx) - -@cache_page(60 * 60) -def get_element(request, form, form_class, form_type, source, author, existent_element): - element = None - - if(source and author): - instance = form_type(source=source, author=author) - element = form_class(instance=instance).save(commit=False) - element.save() - elif(existent_element): - qs = form_type.objects.filter(id=existent_element) - if qs: - element = qs[0] - element.owner = request.user.profile - - return element - -@cache_page(60 * 60) -def get_marker(request, form): - marker_src = form.cleaned_data['marker'] - marker_author = form.cleaned_data['marker_author'] - existent_marker = form.cleaned_data['existent_marker'] - - return get_element(request, form, UploadMarkerForm, Marker, source=marker_src, author=marker_author, existent_element=existent_marker) - -@cache_page(60 * 60) -def get_augmented(request, form): - object_src = form.cleaned_data['augmented'] - object_author = form.cleaned_data['augmented_author'] - existent_object = form.cleaned_data['existent_object'] - - return get_element(request, form, UploadObjectForm, Object, source=object_src, author=object_author, existent_element=existent_object) - -@login_required -def create_artwork(request): - if request.method == 'POST': - form = ArtworkForm(request.POST, request.FILES) - - if form.is_valid(): - - marker = get_marker(request,form) - augmented = get_augmented(request, form) - - if marker and augmented: - artwork_title = form.cleaned_data['title'] - artwork_desc = form.cleaned_data['description'] - Artwork( - author=request.user.profile, - marker=marker, - augmented=augmented, - title=artwork_title, - description=artwork_desc - ).save() - return redirect('home') - else: - form = ArtworkForm() - - marker_list = Marker.objects.all() - object_list = Object.objects.all() - - return render( - request, - 'users/artwork-create.jinja2', - { - 'form': form, - 'marker_list': marker_list, - 'object_list': object_list, - } - ) - -@login_required -def create_exhibit(request): - if request.method == 'POST': - form = ExhibitForm(request.POST) - if form.is_valid(): - ids = form.cleaned_data['artworks'].split(',') - artworks = Artwork.objects.filter(id__in=ids) - exhibit = Exhibit( - owner=request.user.profile, - name=form.cleaned_data['name'], - slug=form.cleaned_data['slug'], - ) - - exhibit.save() - exhibit.artworks.set(artworks) - - return redirect('home') - else: - form = ExhibitForm() - - artworks = Artwork.objects.all() - - return render( - request, - 'users/exhibit-create.jinja2', - { - 'form': form, - 'artworks': artworks, - } - ) - -@require_http_methods(["GET"]) -def download_exhibit(request): - exhibit_id = request.GET.get('id') - exhibit = Exhibit.objects.get(id=exhibit_id) - artworks = list(exhibit.artworks.all()) - - marker_names = [] - object_names = [] - patt_names = [] - - all_data = [] - - for artwork in artworks: - marker_names.append(artwork.marker.source.name) - object_names.append(artwork.augmented.source.name) - patt_names.append(str(artwork.marker.patt)) - - for marker_name in marker_names: - data = { - "link": marker_name - } - - all_data.append(data) - - for object_name in object_names: - data = { - "link": object_name - } - - all_data.append(data) - - for patt_name in patt_names: - data = { - "link": patt_name - } - - all_data.append(data) - - return HttpResponse(json.dumps(all_data)) - - -@cache_page(60 * 2) -@require_http_methods(["GET"]) -def element_get(request): - if request.GET.get('marker_id', None): - element_type = 'marker' - element = get_object_or_404(Marker, pk=request.GET['marker_id']) - elif request.GET.get('object_id', None): - element_type = 'object' - element = get_object_or_404(Object, pk=request.GET['object_id']) - elif request.GET.get('artwork_id', None): - element_type = 'artwork' - element = get_object_or_404(Artwork, pk=request.GET['artwork_id']) - - if element_type == 'artwork': - data = { - 'id_marker' : element.marker.id, - 'id_object' : element.augmented.id, - 'type': element_type, - 'author': element.author.user.username, - 'exhibits': element.exhibits_count, - 'created_at': element.created_at.strftime('%d %b, %Y'), - 'marker': element.marker.source.url, - 'augmented': element.augmented.source.url, - 'augmented_size': element.augmented.source.size, - 'title': element.title, - 'description': element.description, - } - else: - data = { - 'id' : element.id, - 'type': element_type, - 'author': element.author, - 'owner': element.owner.user.username, - 'artworks': element.artworks_count, - 'exhibits': element.exhibits_count, - 'source': element.source.url, - 'size': element.source.size, - 'uploaded_at': element.uploaded_at.strftime('%d %b, %Y'), - } - - serialized = json.dumps(data) - - return HttpResponse(serialized, content_type='application/json') - -def upload_elements(request, form_class, form_type, route): - if request.method == 'POST': - form = form_class(request.POST, request.FILES) - if form.is_valid(): - upload = form.save(commit=False) - upload.owner = request.user.profile - upload.save() - return redirect('home') - else: - form = form_class() - return render(request,'users/upload.jinja2', - { - 'form_type': form_type, - 'form': form, - 'route': route, - 'edit': False - } - ) - -@login_required -def marker_upload(request): - return upload_elements(request, UploadMarkerForm, 'marker', 'marker-upload') - -@login_required -def object_upload(request): - return upload_elements(request, UploadObjectForm, 'object', 'object-upload') - -def edit_elements(request, form_class, route, model, model_data): - if(not model or model.owner != Profile.objects.get(user=request.user)): - raise Http404 - - if(request.method == "POST"): - form = form_class(request.POST, request.FILES, instance = model) - - form.full_clean() - if form.is_valid(): - form.cleaned_data["source"] == model.source - form.save() - return redirect('profile') - else: - log.warning(form.errors) - - - return render( - request, route, - { - 'form': form_class(initial=model_data), - 'model': model, - } - ) - -@login_required -def edit_object(request): - index = request.GET.get("id", "-1") - model = Object.objects.get(id=index) - - model_data = { - "source": model.source, - "uploaded_at": model.uploaded_at, - "author": model.author, - "scale": model.scale, - "position": model.position, - "rotation": model.rotation, - "title": model.title, - } - return edit_elements(request, UploadObjectForm, route='users/edit-object.jinja2', model=model, model_data=model_data) - -@login_required -def edit_marker(request): - index = request.GET.get("id", "-1") - model = Marker.objects.get(id=index) - - model_data = { - "source": model.source, - "uploaded_at": model.uploaded_at, - "author": model.author, - "patt": model.patt, - "title": model.title, - } - - return edit_elements(request, UploadMarkerForm, route='users/edit-marker.jinja2', model=model, model_data=model_data) - - -@login_required -def edit_artwork(request): - index = request.GET.get("id","-1") - model = Artwork.objects.filter(id=index) - if(not model or model.first().author != Profile.objects.get(user=request.user)): - raise Http404 - - if(request.method == "POST"): - form = ArtworkForm(request.POST, request.FILES) - - form.full_clean() - if form.is_valid(): - model_data={ - "marker": get_marker(request,form), - "augmented": get_augmented(request, form), - "title": form.cleaned_data["title"], - "description": form.cleaned_data["description"], - } - print(model_data['augmented']) - model.update(**model_data) - return redirect('profile') - - model = model.first() - model_data = { - "marker": model.marker, - "marker_author": model.marker.author, - "augmented": model.augmented, - "augmented_author": model.augmented.author, - "title": model.title, - "description": model.description, - "existent_marker": model.marker.id, - "existent_object": model.augmented.id, - } - - return render( - request, - 'users/artwork-edit.jinja2', - { - 'form': ArtworkForm(initial=model_data), - 'marker_list': Marker.objects.all(), - 'object_list': Object.objects.all(), - 'selected_marker': model.marker.id, - 'selected_object': model.augmented.id - } - ) - - -@login_required -def edit_exhibit(request): - index = request.GET.get("id","-1") - model = Exhibit.objects.filter(id=index) - if(not model or model.first().owner != Profile.objects.get(user=request.user)): - raise Http404 - - if(request.method == "POST"): - form = ExhibitForm(request.POST) - - form.full_clean() - if form.is_valid(): - ids = form.cleaned_data['artworks'].split(',') - artworks = Artwork.objects.filter(id__in=ids) - - model_data={ - "name":form.cleaned_data["name"], - "slug": form.cleaned_data["slug"], - } - model.update(**model_data) - model = model.first() - model.artworks.set(artworks) - - return redirect('profile') - - model = model.first() - model_artworks = "" - for artwork in model.artworks.all(): - model_artworks += str(artwork.id) + "," - - model_artworks = model_artworks[:-1] - - model_data = { - "name": model.name, - "slug": model.slug, - "artworks": model_artworks - } - - artworks = Artwork.objects.filter(author=request.user.profile) - return render( - request, - 'users/exhibit-edit.jinja2', - { - 'form': ExhibitForm(initial=model_data), - 'artworks': artworks, - 'selected_artworks': model_artworks, - } - ) - -@login_required -def edit_password(request): - - if request.method == 'POST': - form = PasswordChangeForm(request.user, data=request.POST) - if form.is_valid(): - form.save() - update_session_auth_hash(request, form.user) - return redirect('profile') - else: - profile = Profile.objects.get(user=request.user) - ctx={ - 'form_password': PasswordChangeForm(request.user), - 'form_profile': ProfileForm(instance=profile) - } - return render(request,'users/profile-edit.jinja2',ctx) - return Http404 - -@login_required -def edit_profile(request): - profile = Profile.objects.get(user=request.user) - if request.method == 'POST': - form = ProfileForm(request.POST, instance=profile) - - if form.is_valid(): - profile = form.save(commit=False) - user = profile.user - user.email = form.cleaned_data['email'] - user.username = form.cleaned_data['username'] - user.save(force_update=True) - profile.save() - - return redirect('profile') - else: - form = ProfileForm(instance=profile) - - return render( - request, - 'users/profile-edit.jinja2', - { - 'form_profile': form, - 'form_password': PasswordChangeForm(request.user), - } - ) - -@login_required -@require_http_methods(["GET"]) -def delete(request): - content_type = request.GET.get('content_type', None) - - if content_type == 'marker': - delete_content(Marker, request.user, request.GET.get('id', -1)) - elif content_type == 'object': - delete_content(Object, request.user, request.GET.get('id', -1)) - elif content_type == 'artwork': - delete_content(Artwork, request.user, request.GET.get('id', -1)) - elif content_type == 'exhibit': - delete_content(Exhibit, request.user, request.GET.get('id', -1)) - return redirect('profile') - -def delete_content(model, user, instance_id): - qs = model.objects.filter(id=instance_id) - - if qs: - instance = qs[0] - if user.has_perm('users.moderator'): - delete_content_Moderator(instance,user) - else: - isArtwork = isinstance(instance, Artwork) - if isArtwork: - hasPermission = (instance.author == user.profile) - else: - hasPermission = (instance.owner == user.profile) - - isInstanceSameTypeofModel = isinstance(instance, model) - if isInstanceSameTypeofModel and hasPermission: - instance.delete() - - -def delete_content_Moderator(instance,user): - - isInstanceSameTypeofModel = isinstance(instance, model) - isObject = isinstance(instance, Object) - isMarker = isinstance(instance, Marker) - isArtwork = isinstance(instance, Artwork) - - - if isInstanceSameTypeofModel or not instance.in_use: - instance.delete() - elif instance.in_use: - if isObject: - artworkIn = Artwork.objects.filter(augmented=instance) - artworkIn.delete() - instance.delete() - elif isMarker: - artworkIn = Artwork.objects.filter(marker=instance) - artworkIn.delete() - instance.delete() - elif isArtwork: - instance.delete() - -@require_http_methods(["GET"]) -def related_content(request): - element_id = request.GET.get('id') - element_type = request.GET.get('type') - element = None - ctx = {} - - if element_type == 'object': - element = Object.objects.get(id=element_id) - - artworks = element.artworks_list - exhibits = element.exhibits_list - - ctx = {'artworks': artworks, 'exhibits': exhibits, "seeall:":False} - elif element_type == 'marker': - element = Marker.objects.get(id=element_id) - - artworks = element.artworks_list - exhibits = element.exhibits_list - - ctx = {'artworks': artworks, 'exhibits': exhibits, "seeall:":False} - elif element_type == 'artwork': - element = Artwork.objects.get(id=element_id) - - exhibits = element.exhibits_list - - ctx = {'exhibits': exhibits, "seeall:":False} - - return render(request, 'core/collection.jinja2', ctx) - -@login_required -@require_http_methods(["GET"]) -def mod_delete(request): - content_type = request.GET.get('content_type', None) - if content_type == 'marker': - delete_content(Marker, request.user, request.GET.get('instance_id', -1)) - elif content_type == 'object': - delete_content(Object, request.user, request.GET.get('instance_id', -1)) - elif content_type == 'artwork': - delete_content(Artwork, request.user, request.GET.get('instance_id', -1)) - elif content_type == 'exhibit': - delete_content(Exhibit, request.user, request.GET.get('id', -1)) - return redirect('moderator-page') - - -def mod(request): - ctx = { - "objects" : Object.objects.all(), - "markers" : Marker.objects.all(), - "artworks": Artwork.objects.all(), - "exhibits": Exhibit.objects.all(), - "permission" : request.user.has_perm('users.moderator'), - } - return render(request, 'users/moderator-page.jinja2', ctx) - -def permission_denied (request): - return render(request, 'users/permission-denied.jinja2') \ No newline at end of file diff --git a/src/ARte/config/__init__.py b/src/__init__.py similarity index 100% rename from src/ARte/config/__init__.py rename to src/__init__.py diff --git a/src/ARte/core/__init__.py b/src/blog/__init__.py similarity index 100% rename from src/ARte/core/__init__.py rename to src/blog/__init__.py diff --git a/src/blog/admin.py b/src/blog/admin.py new file mode 100644 index 00000000..cb2d3a08 --- /dev/null +++ b/src/blog/admin.py @@ -0,0 +1,15 @@ +from blog.models import Category, Clipping, Post, PostImage +from django.contrib import admin + +admin.site.register(Category) +admin.site.register(PostImage) +admin.site.register(Clipping) + + +@admin.register(Post) +class PostAdmin(admin.ModelAdmin): + list_display = ("title", "status", "created", "updated") + raw_id_fields = ("author",) + + def get_queryset(self, request): + return super().get_queryset(request).select_related("author") diff --git a/src/blog/apps.py b/src/blog/apps.py new file mode 100644 index 00000000..6be26c73 --- /dev/null +++ b/src/blog/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class BlogConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "blog" diff --git a/src/blog/jinja2/blog/category.jinja2 b/src/blog/jinja2/blog/category.jinja2 new file mode 100644 index 00000000..7f8ed61d --- /dev/null +++ b/src/blog/jinja2/blog/category.jinja2 @@ -0,0 +1,10 @@ +{% extends "blog/index.jinja2" %} + +{% block page_title %} +

{{ category }}

+{% endblock page_title %} + +{%block button%} + {{ _('< Back to Blog') }} +
+{%endblock%} \ No newline at end of file diff --git a/src/blog/jinja2/blog/clipping.jinja2 b/src/blog/jinja2/blog/clipping.jinja2 new file mode 100644 index 00000000..30d70cf7 --- /dev/null +++ b/src/blog/jinja2/blog/clipping.jinja2 @@ -0,0 +1,31 @@ +{% extends '/core/home.jinja2' %} + +{% block extra_css %} + +{% endblock %} + +{% block content %} +
+ {% block button %} + {% endblock %} + {% block page_title %} +

{{ _("Clipping") }}

+ {% endblock page_title %} + {{_( "The content below is in the original languages.")}} + {% for clipping in clippings %} +
+

{{ clipping.title }}

+ {{ clipping.created.date().strftime('%d %B %Y') }} +

{{ clipping.description }}

+ {{ clipping.link }} + {%if clipping.file.url[-3:] == "pdf" %} + {{ _("View as PDF")}} + {% elif clipping.file.url[-3:] in ["png","jpg"] %} + {{ _("View as JPG")}} + {% else %} + {{ _("View File")}} + {% endif %} +
+ {% endfor %} +
+{% endblock %} \ No newline at end of file diff --git a/src/blog/jinja2/blog/detail.jinja2 b/src/blog/jinja2/blog/detail.jinja2 new file mode 100644 index 00000000..13d48ccb --- /dev/null +++ b/src/blog/jinja2/blog/detail.jinja2 @@ -0,0 +1,26 @@ +{% extends "blog/index.jinja2" %} + +{% block content %} +
+ {{ _('< Back to Blog') }} +
+ {% block page_title %} +

{{ post.title }}

+ {% endblock page_title %} +
+

{{ post.body |safe }}

+ {% for image in images %} +
+ + {% if image.description %} + {{ image.description }} + {% endif %} +
+ {% endfor %} +
+ +
+
+ {{_("Back to Top")}} + +{% endblock %} \ No newline at end of file diff --git a/src/blog/jinja2/blog/index.jinja2 b/src/blog/jinja2/blog/index.jinja2 new file mode 100644 index 00000000..615d2736 --- /dev/null +++ b/src/blog/jinja2/blog/index.jinja2 @@ -0,0 +1,18 @@ +{% extends '/core/home.jinja2' %} + +{% block extra_css %} + +{% endblock %} + +{% block content %} +
+ {% block button %} + {% endblock %} + {% block page_title %} +

{{ _("Memories") }}

+ {% endblock page_title %} +
+ {% include "/blog/post_preview.jinja2" %} +
+
+{% endblock %} \ No newline at end of file diff --git a/src/blog/jinja2/blog/post_preview.jinja2 b/src/blog/jinja2/blog/post_preview.jinja2 new file mode 100644 index 00000000..5d0db089 --- /dev/null +++ b/src/blog/jinja2/blog/post_preview.jinja2 @@ -0,0 +1,9 @@ +{% for post in posts %} +
+

{{ post.title }}

+

{{ post.body[:PREVIEW_SIZE] | safe }}... {{ _("Read More") }}

+
+{% endfor %} +{% if posts.count() == page_size %} + +{% endif %} \ No newline at end of file diff --git a/src/blog/migrations/0001_initial.py b/src/blog/migrations/0001_initial.py new file mode 100644 index 00000000..d1c06fa2 --- /dev/null +++ b/src/blog/migrations/0001_initial.py @@ -0,0 +1,93 @@ +# Generated by Django 4.1.5 on 2024-06-23 17:41 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ("users", "0009_alter_profile_id"), + ] + + operations = [ + migrations.CreateModel( + name="Category", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=100)), + ], + options={ + "verbose_name_plural": "categories", + }, + ), + migrations.CreateModel( + name="PostImage", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("file", models.FileField()), + ( + "description", + models.CharField(blank=True, max_length=500, null=True), + ), + ], + ), + migrations.CreateModel( + name="Post", + fields=[ + ("id", models.BigAutoField(primary_key=True, serialize=False)), + ("title", models.CharField(max_length=200)), + ( + "status", + models.CharField( + choices=[("draft", "Draft"), ("published", "Published")], + default="draft", + max_length=20, + ), + ), + ("body", models.TextField()), + ("created", models.DateTimeField()), + ("updated", models.DateTimeField(auto_now=True)), + ( + "author", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="posts", + to="users.profile", + ), + ), + ( + "categories", + models.ManyToManyField( + blank=True, related_name="posts", to="blog.category" + ), + ), + ( + "images", + models.ManyToManyField( + blank=True, related_name="posts", to="blog.postimage" + ), + ), + ], + ), + ] diff --git a/src/blog/migrations/0002_legacy_posts.py b/src/blog/migrations/0002_legacy_posts.py new file mode 100644 index 00000000..5e19b39e --- /dev/null +++ b/src/blog/migrations/0002_legacy_posts.py @@ -0,0 +1,410 @@ +from django.db import migrations, models +import datetime +from zoneinfo import ZoneInfo + + +def create_initial_posts(apps, schema_editor): + Category = apps.get_model("blog", "Category") + Post = apps.get_model("blog", "Post") + PostImage = apps.get_model("blog", "PostImage") + + obras_category, _ = Category.objects.get_or_create(name="Obras") + noticias_category, _ = Category.objects.get_or_create(name="Notícias") + ativas_category, _ = Category.objects.get_or_create(name="Ativas") + exhibitions_category, _ = Category.objects.get_or_create(name="Exhibitions") + + timezone = ZoneInfo("America/Sao_Paulo") + initial_posts = [ + { + "post": { + "id": 1, + "title": "Making Of", + "status": "published", + "created": datetime.datetime(2011, 11, 25, 17, 0, tzinfo=timezone), + "categories": [exhibitions_category], + "body": """Behind-the-scenes images of development, various interventions, and exhibitions.""", + }, + "images": [ + { + "file": "IMG_1283-2.jpg", + "description": "Preparing homemade paste for the first urban intervention", + }, + { + "file": "IMG_1289.jpg", + "description": "Preparing homemade paste for the first urban intervention", + }, + { + "file": "IMG_20170419_124719_832.jpg", + "description": "Installing the Janelas Digitais exhibition", + }, + { + "file": "IMG_20170420_154121_254-1.jpg", + "description": "Installing the Janelas Digitais exhibition", + }, + { + "file": "IMG_20170420_170458.jpg", + "description": "Installing the Janelas Digitais exhibition", + }, + { + "file": "2011-11-29-19.40.27.jpg", + "description": "Drafting the poster for the CulturaDigital.br festival", + }, + { + "file": "IMG_20170413_163535_258.jpg", + "description": "Test of the “Sexy na Janela” animation", + }, + {"file": "Jandig_20131031_154312.jpg", "description": "Marker tests"}, + { + "file": "IMG_20170519_193224_636.jpg", + "description": "Marker tests for the “Algo a Mais” exhibition", + }, + { + "file": "Jandig_20120331_201203.jpg", + "description": "Testing a marker on a yellow T-shirt", + }, + { + "file": "Jandig_20120331_200803.jpg", + "description": "Testing a marker on a sticker", + }, + ], + }, + { + "post": { + "id": 2, + "title": "Urban Interventions", + "status": "published", + "created": datetime.datetime(2011, 11, 25, 17, 1, tzinfo=timezone), + "categories": [exhibitions_category], + "body": """Urban interventions in London, UK (2017); Montreal, Canada (2017); and São Paulo, Brazil (2011).""", + }, + "images": [ + {"file": "DLTITQXXkAA_-Ar.jpg", "description": "London"}, + {"file": "IMG_20171017_165803462_HDR.jpg", "description": "Montreal"}, + {"file": "DMXanr4WkAEQ1pP.jpg", "description": "Montreal"}, + {"file": "IMG_1304.jpg", "description": "São Paulo"}, + {"file": "IMG_1322.jpg", "description": "São Paulo"}, + {"file": "IMG_1325.jpg", "description": "São Paulo"}, + {"file": "IMG_1328.jpg", "description": "São Paulo"}, + {"file": "IMG_1338.jpg", "description": "São Paulo"}, + {"file": "IMG_13491.jpg", "description": "São Paulo"}, + {"file": "IMG_1350.jpg", "description": "São Paulo"}, + {"file": "IMG_1355.jpg", "description": "São Paulo"}, + {"file": "IMG_1327-1.jpg", "description": "São Paulo"}, + ], + }, + { + "post": { + "id": 3, + "title": "Ônibus Hacker", + "status": "published", + "created": datetime.datetime(2011, 11, 25, 17, 2, tzinfo=timezone), + "categories": [exhibitions_category], + "body": """The intervention was carried out on the Ônibus Hacker in São Paulo, Brazil (December 2011), during the departure for the CulturaDigital.br festival at MAM in Rio de Janeiro.""", + }, + "images": [ + {"file": "IMG_1358.jpg", "description": None}, + {"file": "IMG_1362.jpg", "description": None}, + {"file": "IMG_1365-1.jpg", "description": None}, + ], + }, + { + "post": { + "id": 4, + "title": "CulturaDigital.br Festival", + "status": "published", + "created": datetime.datetime(2011, 11, 25, 17, 3, tzinfo=timezone), + "categories": [exhibitions_category], + "body": """Posters and a lecture on Jandig were presented at the Museum of Modern Art in Rio de Janeiro, Brazil, from December 2 to 4, 2011, during the CulturaDigital.br Festival. + + More than just an event for showcasing ideas and projects, the CulturaDigital.br Festival was a gathering of Brazilian digital culture agents with their global peers. It brought together creators, producers, and activists working at the intersection of culture, politics, and technology, promoting innovations in their fields. + + Jandig had its first concrete realization during the Festival. It was our trial by fire! We conducted an intervention and had a representative give a lecture presenting the project and the techniques and concepts involved in its execution.""", + }, + "images": [ + { + "file": "6477047909_edf32607a8_o.jpg", + "description": "Talk presenting Jandig", + }, + { + "file": "6477047909_edf32607a8_o.jpg", + "description": "Remix of one of Jandig's posters and a photo taken at the event", + }, + { + "file": "fitacrepe_mam-1.jpg", + "description": "One of Jandig's posters with markers spread across the event", + }, + ], + }, + { + "post": { + "id": 5, + "title": "Campus Party Brazil", + "status": "published", + "created": datetime.datetime(2011, 11, 25, 17, 4, tzinfo=timezone), + "categories": [exhibitions_category], + "body": """The first official Jandig exhibition occurred from February 6 to 12, 2012, at Campus Party in São Paulo, Brazil. We displayed a series of markers of various sizes arranged in a maze. The markers were printed by us on 180g paper and later cut with a paper cutter. + + During the seven-day exhibition, attendees could bring t-shirts to have a marker printed on them using the silk-screen technique. While the t-shirts dried, they were hung on a clothesline as part of the installation. + + On the esteemed Digital Arts stage, we had the privilege of sharing the (still brief) history of Jandig: from its inception, the journey of uniting people, the software development, the first interventions, to the invaluable lessons we've gleaned along the way. This session was followed by an interview with Campus Party TV. + + Moreover, during the event, we had the exciting opportunity to collaborate with the renowned band Móveis Coloniais de Acaju. The band incorporated images of Jandig and the unique sound of the stamp into a music video recorded live during the event, adding a new dimension to our artistic expression. They also interviewed us during the event.""", + }, + "images": [ + { + "file": "jandig-CP-6-1.jpg", + "description": "Placa apresentando o Jandig", + }, + { + "file": "palestra_jandig_cp2012-5.jpg", + "description": "Palestra sobre o Jandig no palco de Artes Digitais", + }, + { + "file": "palestra_jandig_cp2012-6.jpg", + "description": "Demonstração durante a palestra", + }, + { + "file": "palestra_jandig_cp2012-10.jpg", + "description": "Um urso de pelúcia ganhou um marcador", + }, + { + "file": "palestra_jandig_cp2012-13-1.jpg", + "description": "Entrevista para a TV Campus Party", + }, + { + "file": "jandig-CP-15-1.jpg", + "description": "Aplicando silk-screen em uma camiseta", + }, + { + "file": "jandig-CP-14.jpg", + "description": "Camiseta após a aplicação de silk", + }, + { + "file": "jandig-CP-17.jpg", + "description": "Verificando se a tinta secou", + }, + {"file": "jandig-CP-1-1.jpg", "description": "Exposição"}, + {"file": "Jandig_20120211_133940.jpg", "description": "Exposição"}, + {"file": "Jandig_20120211_134014.jpg", "description": "Exposição"}, + { + "file": "jandig-CP-7.jpg", + "description": "Entrevista para a Móveis Coloniais de Acaju", + }, + { + "file": "Jandig_20120211_134137.jpg", + "description": "Superfície utilizada pela Móveis Coloniais de Araju para gravar som de carimbadas, após a gravação", + }, + ], + }, + { + "post": { + "id": 6, + "title": "AiR – Mobile Media Art", + "status": "published", + "created": datetime.datetime(2011, 11, 25, 17, 5, tzinfo=timezone), + "categories": [exhibitions_category], + "body": """Mobile Media Art Artists in Residency was an initiative organized by NIMk (Netherlands) and Vivo ARTE.MOV (Brazil) from March to April 2012. This pioneering artistic residency utilized mobile laboratories equipped for digital production and dissemination, developed in Amsterdam and São Paulo. + Pixel was selected for the residency with the Jandig initiative. During the residency, Jandig inspired the design of the Narrative Navigation installation. The final installation was presented in São Paulo and Rotterdam (Netherlands). + During the residency Artistic interventions at the São Paulo Cultural Center and at Dom José Gaspar Square (São Paulo, Brazil).""", + }, + "images": [ + {"file": "2012-03-25-19.07.29.jpg", "description": None}, + {"file": "2012-04-15-16.39.58.jpg", "description": None}, + {"file": "LabMovel-24.jpg", "description": None}, + {"file": "LabMovel-26.jpg", "description": None}, + {"file": "LabMovel-32.jpg", "description": None}, + {"file": "LabMovel-37.jpg", "description": None}, + {"file": "LabMovel-38.jpg", "description": None}, + {"file": "LabMovel-41.jpg", "description": None}, + {"file": "LabMovel-43.jpg", "description": None}, + ], + }, + { + "post": { + "id": 7, + "title": "BaixoCentro Festival", + "status": "published", + "created": datetime.datetime(2011, 11, 25, 17, 6, tzinfo=timezone), + "categories": [exhibitions_category], + "body": """BaixoCentro, a street festival that began as a seed of an idea at the Casa da Cultura Digital in mid-2011, was nurtured into existence by a collective effort. The aim was to create a festival that would connect cultural hubs in the neighborhoods around the Minhocão in São Paulo, a vision that we all shared and worked towards. + + By early 2012, the festival had blossomed into a network of over a hundred volunteers, a testament to the community's dedication and belief in the project. A crowdfunding campaign, which achieved the project's funding, was a significant milestone, making it one of the most successful campaigns in Brazil at the time. + + Jandig carried out urban interventions from March 23 to 31 for the festival's 2012 program. The interventions started with meetings with the public at the Casa da Cultura Digital, who then joined us in posting art in various locations.""", + }, + "images": [ + {"file": "2012-03-31-20.42.27.jpg", "description": None}, + {"file": "Jandig_20120331_204753.jpg", "description": None}, + {"file": "Jandig_20120331_204837.jpg", "description": None}, + {"file": "2012-03-31-21.08.32.jpg", "description": None}, + ], + }, + { + "post": { + "id": 8, + "title": "Algo a Mais?", + "status": "published", + "created": datetime.datetime(2011, 11, 25, 17, 7, tzinfo=timezone), + "categories": [exhibitions_category], + "body": """In 2012, we were invited to create an exhibition for the opening of Sesc Sorocaba (Brazil). + + The Algo a Mais? (Something More?) exhibition theme was robots. We invited animators to create content to appear in augmented reality, a designer to create the markers, and a hacker to develop moving robots that held markers. + Besides the robots, we also had markers in magnets (allowing the public to rearrange part of the exhibition). We gave stickers with markers to the attendees, allowing them to get part of the exhibition. + + The Algo a Mais? exhibition was a resounding success. Originally planned for September, its popularity led to an extension. Sesc, impressed by the response, requested to prolong the exhibition for an additional month until the end of October. + + + + """, + }, + "images": [ + {"file": "IMG_1567.jpg", "description": None}, + {"file": "IMG_1541.jpg", "description": None}, + {"file": "2012-09-02-16.50.06.jpg", "description": None}, + {"file": "IMG_1564.jpg", "description": None}, + {"file": "2012-09-01-10.22.42.jpg", "description": None}, + {"file": "2012-09-01-10.24.07.jpg", "description": None}, + {"file": "2012-09-01-10.24.18.jpg", "description": None}, + {"file": "IMG_1565.jpg", "description": None}, + ], + }, + { + "post": { + "id": 9, + "title": "FISL", + "status": "published", + "created": datetime.datetime(2011, 11, 25, 17, 8, tzinfo=timezone), + "categories": [exhibitions_category], + "body": """The International Free Software Forum - FISL - took place annually from 2000 to 2018 in Porto Alegre, Brazil. FISL is considered one of the world's most significant open-source events, providing integrated technical, political, and social discussions about free software. In July 2012, the event hosted 7,709 participants from 23 countries. + + At the invitation of the event's organization, we gave a lecture. We also created artwork based on the event's logo, which was featured on posters throughout the exhibition area, and stickers were distributed to participants (along with stickers from other exhibitions).""", + }, + "images": [ + {"file": "Jandig_20120724_184642.jpg", "description": None}, + {"file": "Ay1b6kICYAEP2FF.jpg", "description": None}, + {"file": "Ay1cEu1CAAAcYvO.jpg", "description": None}, + {"file": "Ayv8FkGCIAIqVWJ.jpg", "description": None}, + {"file": "Jandig_20120724_194605.jpg", "description": None}, + {"file": "Jandig_20120724_201008.jpg", "description": None}, + ], + }, + { + "post": { + "id": 10, + "title": "Janelas Digitais", + "status": "published", + "created": datetime.datetime(2011, 11, 25, 17, 9, tzinfo=timezone), + "categories": [exhibitions_category], + "body": """The Janelas Digitais exhibition was at Coletivo Digital São Paulo (Brazil) from April to June 2017. + + The text below is by Thiago Esperandio, curator of Coletivo Digital. + + Janelas Digitais (Jandig), conceived by VJ Pixel + There is much to say about the Jandig Project that brings this exhibition to Coletivo Digital. However, it’s inevitable to break any formality and start by saying it is incredibly cool. The reaction of anyone who downloads the app and sees this joyful fusion of art and technology is usually a big smile… whether it’s due to the interaction with a technology that still feels quite futuristic or the access to virtual works that bring a playful and colorful vision to the black and white markers (most likely, it’s both). After this initial moment of fun, Janelas Digitais becomes even more extraordinary when we know that the works – both virtual and tangible – are licensed under Creative Commons, allowing other creators to use them; that the app’s source code is open for anyone to explore and learn from; and that the entire project involved various artists working collaboratively. In times of technological wars, the rise of ultra-conservative thinking, a society of control, and the internet under attack from corrupt powers, it is a privilege for Coletivo Digital to host these digital windows, bringing such fraternal and inspiring landscapes to our welcoming space.""", + }, + "images": [ + {"file": "IMG_9909-1.jpg", "description": None}, + {"file": "MG_9905.jpg", "description": None}, + {"file": "Jandig.jpg", "description": None}, + {"file": "IMG_9921-2.jpg", "description": None}, + {"file": "IMG_20170421_191253_178.jpg", "description": None}, + {"file": "IMG_9935-1.jpg", "description": None}, + {"file": "Jandig_20170420_220437.jpg", "description": None}, + {"file": "Jandig_20170420_220602.jpg", "description": None}, + {"file": "IMG_20170313_150654.jpg", "description": None}, + {"file": "Jandig_20170420_220550.jpg", "description": None}, + {"file": "Jandig_20170420_201543.jpg", "description": None}, + {"file": "IMG_9923.jpg", "description": None}, + {"file": "Jandig2.jpg", "description": None}, + {"file": "FB_IMG_1492460805870.jpg", "description": None}, + ], + }, + { + "post": { + "id": 11, + "title": "Virada Cultural", + "status": "published", + "created": datetime.datetime(2011, 11, 25, 17, 10, tzinfo=timezone), + "categories": [exhibitions_category], + "body": """Exhibition during Virada Cultural at Sesc Belenzinho (São Paulo, Brazil), May 2017. + + Virada Cultural is an annual event promoted by the São Paulo municipal government since 2005. With the support of various artistic and institutional partners, it aims to provide 24 continuous hours of cultural events at various locations throughout the city. + + This exhibition premiered the cube with markers. In addition to the displayed boxes, there were also buildable cubes with markers. Some works from this exhibition have become “classics,” such as Tokusatsu and Temaki.""", + }, + "images": [ + {"file": "IMG_20170521_142327_972.jpg", "description": None}, + {"file": "Jandig_20170520_204811.jpg", "description": None}, + {"file": "Jandig_20170520_201449.jpg", "description": None}, + { + "file": "Jandig_20170520_201544.jpg", + "description": None, + }, + {"file": "IMG_20170520_214145_290.jpg", "description": None}, + {"file": "IMG_20170521_154532_735.jpg", "description": None}, + {"file": "IMG_20170520_233302589.jpg", "description": None}, + {"file": "IMG-20170520-WA0003.jpg", "description": None}, + {"file": "IMG_20170520_205738_867.jpg", "description": None}, + {"file": "IMG_20170521_172545_124.jpg", "description": None}, + ], + }, + { + "post": { + "id": 12, + "title": "GAS Station", + "status": "published", + "created": datetime.datetime(2011, 11, 25, 17, 11, tzinfo=timezone), + "categories": [exhibitions_category], + "body": """GAS Station (Games and Art Stratford) is a space dedicated to projects, ideas, and conversations at the intersection of games, performing arts, and technology. GAS supports early-stage projects in London with space, equipment, and mentorship, fostering partnerships and exchanges among professionals from various creative fields. The space is curated by ZU-UK, an internationally renowned company known for creating critically acclaimed and socially engaged performances and digital art that place the audience at the center of the experience. + + In October 2017, a Jandig intervention was carried out in the institution's container area.""", + }, + "images": [ + {"file": "20171006_155417.jpg", "description": None}, + {"file": "20171006_154846.jpg", "description": None}, + {"file": "20171006_155353.jpg", "description": None}, + {"file": "20171006_155338.jpg", "description": None}, + {"file": "20171006_155322.jpg", "description": None}, + {"file": "20171006_154803.jpg", "description": None}, + ], + }, + ] + + for dict_data in initial_posts: + post_data = dict_data["post"] + images = dict_data["images"] + categories = post_data["categories"] + + del post_data["categories"] + post = Post.objects.create(**post_data) + post.categories.set(categories) + post.save() + + for image_data in images: + image_data["file"] = "post_images/" + image_data["file"] + image = PostImage.objects.create(**image_data) + image.posts.add(post) + + +def remove_initial_posts(apps, schema_editor): + Category = apps.get_model("blog", "Category") + Post = apps.get_model("blog", "Post") + PostImage = apps.get_model("blog", "PostImage") + + Category.objects.filter(name="Obras").delete() + Category.objects.filter(name="Notícias").delete() + Category.objects.filter(name="Ativas").delete() + + Post.objects.filter(id__in=range(0, 12)).delete() + PostImage.objects.filter(id__in=range(0, 99)).delete() + + +class Migration(migrations.Migration): + + dependencies = [ + ("blog", "0001_initial"), + ] + + operations = [ + migrations.RunPython(create_initial_posts, reverse_code=remove_initial_posts), + ] diff --git a/src/blog/migrations/0003_alter_post_created_alter_postimage_file.py b/src/blog/migrations/0003_alter_post_created_alter_postimage_file.py new file mode 100644 index 00000000..1e432858 --- /dev/null +++ b/src/blog/migrations/0003_alter_post_created_alter_postimage_file.py @@ -0,0 +1,23 @@ +# Generated by Django 4.1.5 on 2024-06-23 18:10 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("blog", "0002_legacy_posts"), + ] + + operations = [ + migrations.AlterField( + model_name="post", + name="created", + field=models.DateTimeField(auto_now_add=True), + ), + migrations.AlterField( + model_name="postimage", + name="file", + field=models.FileField(upload_to="post_images/"), + ), + ] diff --git a/src/blog/migrations/0004_clipping.py b/src/blog/migrations/0004_clipping.py new file mode 100644 index 00000000..bb7bbea4 --- /dev/null +++ b/src/blog/migrations/0004_clipping.py @@ -0,0 +1,25 @@ +# Generated by Django 4.1.5 on 2024-06-23 21:14 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("blog", "0003_alter_post_created_alter_postimage_file"), + ] + + operations = [ + migrations.CreateModel( + name="Clipping", + fields=[ + ("id", models.BigAutoField(primary_key=True, serialize=False)), + ("title", models.CharField(max_length=200)), + ("description", models.CharField(max_length=500)), + ("link", models.URLField()), + ("file", models.FileField(upload_to="")), + ("created", models.DateTimeField()), + ("updated", models.DateTimeField(auto_now=True)), + ], + ), + ] diff --git a/src/blog/migrations/0005_legacy_clippings.py b/src/blog/migrations/0005_legacy_clippings.py new file mode 100644 index 00000000..16505759 --- /dev/null +++ b/src/blog/migrations/0005_legacy_clippings.py @@ -0,0 +1,138 @@ +from django.db import migrations, models +import datetime +from zoneinfo import ZoneInfo + + +def create_initial_clippings(apps, schema_editor): + Clipping = apps.get_model("blog", "Clipping") + + timezone = ZoneInfo("America/Sao_Paulo") + initial_clippings = [ + { + "id": 1, + "title": "Jandig #CulturaDigitalBR", + "description": "Select Magazine", + "created": datetime.datetime(2011, 12, 12, tzinfo=timezone), + "link": "https://www.select.art.br/jandig-culturadigitalbr/", + "file": "7.-Jandig-CulturaDigitalBR.pdf", + }, + { + "id": 2, + "title": "Uma parceria inusitada: MARTE + JANDIG + Escola de Design/UEMG", + "description": "Estado de Minas Gerais University Website", + "created": datetime.datetime(2019, 7, 22, tzinfo=timezone), + "link": "http://ed.uemg.br/uma-parceria-inusitada-marte-jandig-escola-de-design-uemg/", + "file": "Jandig-UEMG.jpg", + }, + { + "id": 3, + "title": "Programação formativa do X-Reality contou com workshops de realidade virtual e aumentada", + "description": "LabArteMídia Website", + "created": datetime.datetime(2019, 7, 1, tzinfo=timezone), + "link": "https://sites.usp.br/labartemidia/programacao-formativa-do-x-reality-contou-com-workshops-de-realidade-virtual-e-aumentada/", + "file": "Jandig-LabArteMidia.pdf", + }, + { + "id": 4, + "title": "Create Art with Augmented Reality", + "description": "Mozilla Open Leaders Blog", + "created": datetime.datetime(2019, 5, 31, tzinfo=timezone), + "link": "https://medium.com/read-write-participate/create-art-with-augmented-reality-e26572524021", + "file": "Jandig-MozOL.pdf", + }, + { + "id": 5, + "title": "Programação Cultural / Abril de 2017", + "description": "Coletivo Digital Website", + "created": datetime.datetime(2017, 4, 10, tzinfo=timezone), + "link": "http://portalnovo.coletivodigital.org.br/2017/04/", + "file": "1-programacao-cultural-abril-de-2017.pdf", + }, + { + "id": 6, + "title": "Sesc Sorocaba abre as portas em 1º de setembro com visão sustentável", + "description": "Sorocaba's Newspaper", + "created": datetime.datetime(2012, 8, 21, tzinfo=timezone), + "link": "http://www.diariodesorocaba.com.br/noticia/222472", + "file": "2-sesc-Sorocaba-abre-as-portas.pdf", + }, + { + "id": 7, + "title": "Do artista ao articulador", + "description": "Select Magazine", + "created": datetime.datetime(2012, 7, 30, tzinfo=timezone), + "link": "https://www.select.art.br/do-artista-ao-articulador/", + "file": "3.-Do-artista-ao-articulador.pdf", + }, + { + "id": 8, + "title": "Ráfagas de software libre", + "description": "20 Minutos Newspaper", + "created": datetime.datetime(2012, 7, 30, tzinfo=timezone), + "link": "https://blogs.20minutos.es/codigo-abierto/category/software-libre/", + "file": "9.-Rafagas-de-software-livre.pdf", + }, + { + "id": 9, + "title": "Conheça o Jandig", + "description": "Labmóvel Website", + "created": datetime.datetime(2012, 3, 31, tzinfo=timezone), + "link": "https://labmovel.net/2012/03/31/conheca-o-jandig/", + "file": "4-conheca-o-Jandig.pdf", + }, + { + "id": 10, + "title": "Entrevista com Pixel", + "description": "Labmóvel Website", + "created": datetime.datetime(2012, 3, 21, tzinfo=timezone), + "link": "https://labmovel.net/2012/03/21/entrevista-com-pixel/", + "file": "5.-Entrevista-com-Pixel.pdf", + }, + { + "id": 11, + "title": "Campus Party: hackers, multinacionales y activistas", + "description": "20 Minutos Newspaper", + "created": datetime.datetime(2012, 2, 12, tzinfo=timezone), + "link": "https://blogs.20minutos.es/codigo-abierto/2012/02/12/campus-party-hackers-multinacionales-y-activistas/", + "file": "10.-Campus-Party-hackers-multinacionales-y-activistas.pdf", + }, + { + "id": 12, + "title": "New artist in residence projects mobile media art", + "description": "NIMk Website", + "created": datetime.datetime(2012, 2, 1, tzinfo=timezone), + "link": "http://www.nimk.nl/eng/new-artist-in-residence-projects-mobile-media-art", + "file": "8.-New-artist-in-residence-projects-mobile-media-art.pdf", + }, + { + "id": 13, + "title": "Residência artística Labmovel + NimK", + "description": "Site Labmóvel", + "created": datetime.datetime(2012, 2, 1, tzinfo=timezone), + "link": "https://labmovel.net/2012/02/01/residencia-artistica-labmovel-nimk/", + "file": "6-residencia-artistica-labmovel-NimK.pdf", + }, + ] + + for clipping_data in initial_clippings: + clipping_data["file"] = "clipping_files/" + clipping_data["file"] + post = Clipping.objects.create(**clipping_data) + + +def remove_initial_clippings(apps, schema_editor): + Clipping = apps.get_model("blog", "Clipping") + + Clipping.objects.filter(id__in=range(0, 14)).delete() + + +class Migration(migrations.Migration): + + dependencies = [ + ("blog", "0004_clipping"), + ] + + operations = [ + migrations.RunPython( + create_initial_clippings, reverse_code=remove_initial_clippings + ), + ] diff --git a/src/blog/migrations/0006_alter_clipping_created_alter_clipping_file.py b/src/blog/migrations/0006_alter_clipping_created_alter_clipping_file.py new file mode 100644 index 00000000..212912db --- /dev/null +++ b/src/blog/migrations/0006_alter_clipping_created_alter_clipping_file.py @@ -0,0 +1,23 @@ +# Generated by Django 4.1.5 on 2024-06-23 21:15 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("blog", "0005_legacy_clippings"), + ] + + operations = [ + migrations.AlterField( + model_name="clipping", + name="created", + field=models.DateTimeField(auto_now_add=True), + ), + migrations.AlterField( + model_name="clipping", + name="file", + field=models.FileField(upload_to="clipping_files/"), + ), + ] diff --git a/src/ARte/core/migrations/__init__.py b/src/blog/migrations/__init__.py similarity index 100% rename from src/ARte/core/migrations/__init__.py rename to src/blog/migrations/__init__.py diff --git a/src/blog/models.py b/src/blog/models.py new file mode 100644 index 00000000..20e7d4d9 --- /dev/null +++ b/src/blog/models.py @@ -0,0 +1,67 @@ +from django.core.files.storage import default_storage +from django.db import models +from users.models import Profile + +IMAGE_BASE_PATH = "post_images/" + + +class PostStatus(models.TextChoices): + DRAFT = "draft" + PUBLISHED = "published" + + +class Category(models.Model): + name = models.CharField(max_length=100) + + def __str__(self): + return self.name + + class Meta: + verbose_name_plural = "categories" + + +class PostImage(models.Model): + file = models.FileField(storage=default_storage, upload_to=IMAGE_BASE_PATH) + description = models.CharField(max_length=500, blank=True, null=True) + + def __str__(self): + return self.file.name.lstrip(IMAGE_BASE_PATH) + + +class Clipping(models.Model): + id = models.BigAutoField(primary_key=True) + title = models.CharField(max_length=200) + description = models.CharField(max_length=500) + link = models.URLField() + file = models.FileField(upload_to="clipping_files/") + created = models.DateTimeField(auto_now_add=True, editable=True) + updated = models.DateTimeField(auto_now=True, editable=True) + + def __str__(self): + return self.title + + +class Post(models.Model): + id = models.BigAutoField(primary_key=True) + title = models.CharField(max_length=200) + status = models.CharField( + choices=PostStatus.choices, max_length=20, default=PostStatus.DRAFT + ) + author = models.ForeignKey( + Profile, + on_delete=models.DO_NOTHING, + related_name="posts", + null=True, + blank=True, + ) + body = models.TextField() + created = models.DateTimeField(auto_now_add=True, editable=True) + updated = models.DateTimeField(auto_now=True, editable=True) + categories = models.ManyToManyField(Category, related_name="posts", blank=True) + images = models.ManyToManyField(PostImage, related_name="posts", blank=True) + + def __str__(self): + return self.title + + def get_absolute_url(self): + return f"/memories/{self.slug}/" diff --git a/src/blog/static/css/blog.css b/src/blog/static/css/blog.css new file mode 100644 index 00000000..a2fffb48 --- /dev/null +++ b/src/blog/static/css/blog.css @@ -0,0 +1,81 @@ +/* General Blog Pages */ +#blog-area, +#clipping-area, +#posts-area{ + max-width: 600px; + margin: 0 auto; +} +.post{ + margin: 0px 1.5em 3em 1.5em; +} +.post a, +#category-header a{ + + transition: all 0.2s ease-in; + background-image: linear-gradient( to top, #05f7ae, #05f7ae 50%, transparent 50%, transparent ); + background-position: 100% 0%; + background-size: 100% 200%; +} +.post a:hover, +#category-header a:hover{ + color: #fff; + background-position: 0 100%; +} + +#blog-area p { + text-align: justify; + margin-left: 1.5em; +} + + +/* Post Detail Page */ +#post, +#post-area{ + max-width: 600px; + margin: 0 auto; +} +#post{ + margin-top: 25px; +} +#post p{ + text-align: justify; + margin: 1.5em 1.5em 0 1.5em; +} +#back-button{ + float: left; +} + +.post-image img{ + max-width: 100%; + max-height: 100%; +} +.post-image h5{ + margin-top: 10px; +} +.post-image small{ + display: block; +} + + +/* Clipping page */ +.clipping{ + text-align: left; + margin: 1.5em 1.5em 0 1.5em; +} +.clipping p{ + margin: 0; +} +.clipping h3{ + margin: 0; +} +.clipping a{ + display: inline-block; + margin-bottom: 8px; +} + + +@media all and (min-width: 800px) { + #blog-area p { + text-align: left; + } +} \ No newline at end of file diff --git a/src/blog/tests.py b/src/blog/tests.py new file mode 100644 index 00000000..7ce503c2 --- /dev/null +++ b/src/blog/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/src/blog/urls.py b/src/blog/urls.py new file mode 100644 index 00000000..124606f4 --- /dev/null +++ b/src/blog/urls.py @@ -0,0 +1,11 @@ +# blog/urls.py + +from blog import views +from django.urls import path + +urlpatterns = [ + path("", views.blog_index, name="blog_index"), + path("post//", views.blog_detail, name="blog_detail"), + path("clipping/", views.clipping, name="clipping"), + path("category//", views.blog_category, name="blog_category"), +] diff --git a/src/blog/views.py b/src/blog/views.py new file mode 100644 index 00000000..a5ef25f4 --- /dev/null +++ b/src/blog/views.py @@ -0,0 +1,70 @@ +from blog.models import Category, Clipping, Post, PostStatus +from django.shortcuts import render + +PREVIEW_SIZE = 300 +PAGE_SIZE = 4 + + +def blog_index(request): + actual_page_number = int(request.GET.get("page", "1")) + initial_post = 0 + (actual_page_number - 1) * PAGE_SIZE + last_post = PAGE_SIZE * actual_page_number + posts = ( + Post.objects.prefetch_related("categories") + .filter(status=PostStatus.PUBLISHED) + .all() + .order_by("-created")[initial_post:last_post] + ) + + context = { + "next_page_number": actual_page_number + 1, + "posts": posts, + "PREVIEW_SIZE": PREVIEW_SIZE, + "page_size": PAGE_SIZE, + "page_url": "/memories/", + "blog_categories": Category.objects.all(), + } + if request.htmx: + return render(request, "blog/post_preview.jinja2", context) + + return render(request, "blog/index.jinja2", context) + + +def blog_category(request, category): + actual_page_number = int(request.GET.get("page", "1")) + initial_post = 0 + (actual_page_number - 1) * PAGE_SIZE + last_post = PAGE_SIZE * actual_page_number + posts = ( + Post.objects.prefetch_related("categories") + .filter(categories__name__contains=category) + .order_by("-created")[initial_post:last_post] + ) + + context = { + "next_page_number": actual_page_number + 1, + "category": category, + "posts": posts, + "PREVIEW_SIZE": PREVIEW_SIZE, + "page_size": PAGE_SIZE, + "page_url": request.path, + "blog_categories": Category.objects.all(), + } + + if request.htmx: + return render(request, "blog/post_preview.jinja2", context) + + return render(request, "blog/category.jinja2", context) + + +def blog_detail(request, pk): + post = Post.objects.prefetch_related("images", "categories").get(pk=pk) + + context = {"post": post, "images": post.images.all()} + + return render(request, "blog/detail.jinja2", context) + + +def clipping(request): + clippings = Clipping.objects.all().order_by("-created") + context = {"clippings": clippings} + return render(request, "blog/clipping.jinja2", context) diff --git a/src/ARte/users/__init__.py b/src/config/__init__.py similarity index 100% rename from src/ARte/users/__init__.py rename to src/config/__init__.py diff --git a/src/ARte/config/jinja2.py b/src/config/jinja2.py similarity index 55% rename from src/ARte/config/jinja2.py rename to src/config/jinja2.py index 54bc4333..85c4b3fa 100644 --- a/src/ARte/config/jinja2.py +++ b/src/config/jinja2.py @@ -1,22 +1,22 @@ +from django.conf import settings from django.contrib.staticfiles.storage import staticfiles_storage from django.urls import reverse from django.utils import translation -from django.conf import settings - from jinja2 import Environment def environment(**options): - options['extensions'] = ['jinja2.ext.i18n', 'jinja2.ext.with_'] + options["extensions"] = ["jinja2.ext.i18n"] env = Environment(**options) - - env.globals.update({ - 'static': staticfiles_storage.url, - 'url': reverse, - 'LANGUAGES': settings.LANGUAGES, - 'CUR_LANGUAGE': translation.get_language(), - }) + + env.globals.update( + { + "static": staticfiles_storage.url, + "url": reverse, + "LANGUAGES": settings.LANGUAGES, + "CUR_LANGUAGE": translation.get_language(), + } + ) env.install_gettext_translations(translation, newstyle=True) return env - diff --git a/src/config/settings.py b/src/config/settings.py new file mode 100644 index 00000000..9cef592a --- /dev/null +++ b/src/config/settings.py @@ -0,0 +1,250 @@ +import logging +import os +import re +import sys +from socket import gethostbyname, gethostname + +import environ +import sentry_sdk +from django.utils.translation import gettext_lazy as _ +from sentry_sdk.integrations.django import DjangoIntegration + +ROOT_DIR = environ.Path("/jandig/") +BASE_DIR = "/jandig/src" + +env = environ.Env() + +READ_DOT_ENV_FILE = env.bool("DJANGO_READ_DOT_ENV_FILE", default=False) +if READ_DOT_ENV_FILE: + # OS environment variables take precedence over variables from .env + env.read_env(str(ROOT_DIR.path(".env"))) + +DEBUG = env.bool("DJANGO_DEBUG", False) + +CSRF_TRUSTED_ORIGINS = ['https://*.jandig.app'] + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = env("DJANGO_SECRET_KEY", default="change_me") + +ALLOWED_HOSTS = [ + "localhost", + "0.0.0.0", + "127.0.0.1", + gethostname(), + gethostbyname(gethostname()), +] +CUSTOM_ALLOWED_HOSTS = env.list("DJANGO_ALLOWED_HOSTS", default=["*"]) +ALLOWED_HOSTS += CUSTOM_ALLOWED_HOSTS + + +# Sentry configuration +ENABLE_SENTRY = env("ENABLE_SENTRY", default=False) +HEALTH_CHECK_URL = env("HEALTH_CHECK_URL", default="api/v1/status/") +SENTRY_TRACES_SAMPLE_RATE = env("SENTRY_TRACES_SAMPLE_RATE", default=0.1) +DJANGO_ADMIN_URL = env("DJANGO_ADMIN_URL", default="admin/") +SENTRY_ENVIRONMENT = env("SENTRY_ENVIRONMENT", default="") + +def traces_sampler(sampling_context): + url = sampling_context["wsgi_environ"]["PATH_INFO"] + is_health_check = url == f"/{HEALTH_CHECK_URL}" + is_django_admin = re.search(f"^/{DJANGO_ADMIN_URL.strip('/')}/*", url) is not None + if is_health_check or is_django_admin: + return 0 + return SENTRY_TRACES_SAMPLE_RATE + + +if ENABLE_SENTRY: + SENTRY_DSN = env("SENTRY_DSN") + sentry_sdk.init( + dsn=SENTRY_DSN, + environment=SENTRY_ENVIRONMENT, + integrations=[DjangoIntegration()], + # If you wish to associate users to errors (assuming you are using + # django.contrib.auth) you may enable sending PII data. + send_default_pii=True, + traces_sampler=traces_sampler, + ) + + +INSTALLED_APPS = [ + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + 'django_extensions', + "debug_toolbar", + "django_htmx", + "corsheaders", + "users", + "core", + "blog", +] + +MIDDLEWARE = [ + "debug_toolbar.middleware.DebugToolbarMiddleware", + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.locale.LocaleMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", + "django_htmx.middleware.HtmxMiddleware", +] +DEBUG_TOOLBAR_CONFIG = {"SHOW_TOOLBAR_CALLBACK": "config.settings.debug"} + + +def debug(request): + return env.bool("DEBUG_TOOLBAR", False) + + +ROOT_URLCONF = "config.urls" + +PAGE_SIZE = 20 + +REST_FRAMEWORK = { + "DEFAULT_RENDERER_CLASSES": [ + "rest_framework.renderers.JSONRenderer", + ], + "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.LimitOffsetPagination", + "PAGE_SIZE": PAGE_SIZE, +} + +TEMPLATES = [ + { + "BACKEND": "django.template.backends.jinja2.Jinja2", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "environment": "config.jinja2.environment", + }, + }, + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + ], + }, + }, +] + +WSGI_APPLICATION = "config.wsgi.application" + +DATABASES = { + "default": { + "ENGINE": "django.db.backends.postgresql_psycopg2", + "HOST": env("POSTGRES_HOST", default="localhost"), + "NAME": env("POSTGRES_DB", default="jandig"), + "USER": env("POSTGRES_USER", default="jandig"), + "PASSWORD": env("POSTGRES_PASSWORD", default="secret"), + }, +} + +AUTH_PASSWORD_VALIDATORS = [ + { + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", + }, +] + +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" + +# Internationalization +# https://docs.djangoproject.com/en/2.1/topics/i18n/ + +LOCALE_PATHS = ( + os.path.join(str(ROOT_DIR), 'locale'), +) + +LANGUAGE_CODE = "en" + +LANGUAGES = ( + ("en-us", _("English")), + ("pt-br", _("Brazilian Portuguese")), +) + +TIME_ZONE = "UTC" + +USE_I18N = True + +USE_TZ = True + + +# AWS credentials +AWS_S3_OBJECT_PARAMETERS = { + "CacheControl": "max-age=86400", +} +AWS_ACCESS_KEY_ID = os.getenv("AWS_ACCESS_KEY_ID", "") +AWS_SECRET_ACCESS_KEY = os.getenv("AWS_SECRET_ACCESS_KEY", "") +AWS_STORAGE_BUCKET_NAME = os.getenv("AWS_STORAGE_BUCKET_NAME", "") +AWS_S3_REGION_NAME = os.getenv("AWS_S3_REGION_NAME", "us-east-2") +AWS_DEFAULT_ACL = os.getenv("AWS_DEFAULT_ACL", None) +AWS_STATIC_LOCATION = os.getenv("AWS_STATIC_LOCATION", "static") +AWS_MEDIA_LOCATION = os.getenv("AWS_MEDIA_LOCATION", "media") +USE_MINIO = os.getenv("USE_MINIO", "false").lower() in ("true", "True", "1") +if USE_MINIO: + AWS_S3_ENDPOINT_URL = os.getenv("MINIO_S3_ENDPOINT_URL", "http://storage:9000") + AWS_S3_CUSTOM_DOMAIN = f"localhost:9000/{AWS_STORAGE_BUCKET_NAME}" + AWS_S3_USE_SSL = False + AWS_S3_SECURE_URLS = False + AWS_S3_URL_PROTOCOL = "http:" + +else: + AWS_S3_CUSTOM_DOMAIN = f"{AWS_STORAGE_BUCKET_NAME}.s3.amazonaws.com" + AWS_S3_URL_PROTOCOL = "https:" + +# Static configuration +# Add your own apps statics in this list +STATICFILES_DIRS = [ + os.path.join(BASE_DIR, "core", "static"), + os.path.join(BASE_DIR, "users", "static"), + os.path.join(BASE_DIR, "blog", "static"), +] + +# STATIC_ROOT = "/jandig/static" +STATICFILES_STORAGE = "config.storage_backends.StaticStorage" + +# MEDIA_ROOT = os.path.join(BASE_DIR, "users", "media") + +AWS_PUBLIC_MEDIA_LOCATION = "media/public" +DEFAULT_FILE_STORAGE = "config.storage_backends.PublicMediaStorage" + +AWS_PRIVATE_MEDIA_LOCATION = "media/private" +PRIVATE_FILE_STORAGE = "config.storage_backends.PrivateMediaStorage" + +AWS_PRIVATE_MEDIA_DIFFERENT_BUCKET_LOCATION = "media/private" +AWS_PRIVATE_STORAGE_BUCKET_NAME = os.getenv("AWS_PRIVATE_STORAGE_BUCKET_NAME", "") +PRIVATE_FILE_DIFFERENT_BUCKET_STORAGE = "config.storage_backends.PrivateMediaStorage" + +# LOGIN / LOGOUT +LOGIN_URL = "login" +LOGIN_REDIRECT_URL = "home" +LOGOUT_REDIRECT_URL = "home" + +# Sphinx docs +DOCS_ROOT = "/jandig/build/" + +SMTP_SERVER = env("SMTP_SERVER", default="smtp.gmail.com") +SMTP_PORT = env("SMTP_PORT", default=587) +JANDIG_EMAIL = env("JANDIG_EMAIL", default="jandig@jandig.com") +JANDIG_EMAIL_PASSWORD = env("JANDIG_EMAIL_PASSWORD", default="password") + +if len(sys.argv) > 1 and sys.argv[1] == "test": + logging.disable(logging.CRITICAL) diff --git a/src/ARte/config/storage_backends.py b/src/config/storage_backends.py similarity index 100% rename from src/ARte/config/storage_backends.py rename to src/config/storage_backends.py diff --git a/src/config/test_settings.py b/src/config/test_settings.py new file mode 100644 index 00000000..956d5ab2 --- /dev/null +++ b/src/config/test_settings.py @@ -0,0 +1,3 @@ +from config.settings import * # noqa F403 F401 + +DEFAULT_FILE_STORAGE = "inmemorystorage.InMemoryStorage" diff --git a/src/config/urls.py b/src/config/urls.py new file mode 100644 index 00000000..67ee1129 --- /dev/null +++ b/src/config/urls.py @@ -0,0 +1,18 @@ +import debug_toolbar +from django.conf import settings +from django.conf.urls.static import serve +from django.contrib import admin +from django.urls import include, path, re_path + +urlpatterns = [ + path(settings.DJANGO_ADMIN_URL, admin.site.urls), + path("users/", include("users.urls")), + path("memories/", include("blog.urls")), + re_path("^docs/(?P.*)$", serve, {"document_root": settings.DOCS_ROOT}), + path("", include("core.urls")), + path("", include("core.routes")), +] + +urlpatterns += [ + path("__debug__/", include(debug_toolbar.urls)), +] diff --git a/src/ARte/config/wsgi.py b/src/config/wsgi.py similarity index 59% rename from src/ARte/config/wsgi.py rename to src/config/wsgi.py index a9f185cf..85093356 100644 --- a/src/ARte/config/wsgi.py +++ b/src/config/wsgi.py @@ -2,6 +2,6 @@ from django.core.wsgi import get_wsgi_application -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings") application = get_wsgi_application() diff --git a/src/ARte/users/migrations/__init__.py b/src/core/__init__.py similarity index 100% rename from src/ARte/users/migrations/__init__.py rename to src/core/__init__.py diff --git a/src/ARte/core/admin.py b/src/core/admin.py similarity index 57% rename from src/ARte/core/admin.py rename to src/core/admin.py index 54a96543..2211979b 100644 --- a/src/ARte/core/admin.py +++ b/src/core/admin.py @@ -1,7 +1,8 @@ from django.contrib import admin -from core.models import Exhibit, Artwork, Object, Marker + +from core.models import Artwork, Exhibit, Marker, Object admin.site.register(Exhibit) admin.site.register(Object) admin.site.register(Marker) -admin.site.register(Artwork) \ No newline at end of file +admin.site.register(Artwork) diff --git a/src/ARte/core/apps.py b/src/core/apps.py similarity index 78% rename from src/ARte/core/apps.py rename to src/core/apps.py index 26f78a8e..5ef1d600 100644 --- a/src/ARte/core/apps.py +++ b/src/core/apps.py @@ -2,4 +2,4 @@ class CoreConfig(AppConfig): - name = 'core' + name = "core" diff --git a/src/ARte/core/forms.py b/src/core/forms.py similarity index 90% rename from src/ARte/core/forms.py rename to src/core/forms.py index 4bdaeeb2..b2d7c03b 100644 --- a/src/ARte/core/forms.py +++ b/src/core/forms.py @@ -1,9 +1,11 @@ from django import forms + from .models import Exhibit + class UploadFileForm(forms.Form): file = forms.ImageField(required=False) class ExhibitForm(forms.Form): - exhibit = forms.ModelChoiceField(queryset=Exhibit.objects.all().order_by('name')) \ No newline at end of file + exhibit = forms.ModelChoiceField(queryset=Exhibit.objects.all().order_by("name")) diff --git a/src/core/helpers.py b/src/core/helpers.py new file mode 100644 index 00000000..ae59c5f1 --- /dev/null +++ b/src/core/helpers.py @@ -0,0 +1,23 @@ +from django.core.files.storage import default_storage +from django.templatetags.static import static + + +def handle_upload_image(image): + path = "core" + static(f"images/{image.name}") + with open(path, "wb+") as destination: + for chunk in image.chunks(): + destination.write(chunk) + + +def handle_upload_patt(patt): + path = "core" + static(f"patts/{patt.name}") + with default_storage.open(path, "wb") as destination: + for chunk in patt.chunks(): + destination.write(chunk) + + +def handle_upload_marker(image): + path = "core" + static(f"markers/{image.name}") + with open(path, "wb+") as destination: + for chunk in image.chunks(): + destination.write(chunk) diff --git a/src/ARte/core/jinja2/core/arviewer.jinja2 b/src/core/jinja2/core/arviewer.jinja2 similarity index 100% rename from src/ARte/core/jinja2/core/arviewer.jinja2 rename to src/core/jinja2/core/arviewer.jinja2 diff --git a/src/ARte/core/jinja2/core/base.jinja2 b/src/core/jinja2/core/base.jinja2 similarity index 100% rename from src/ARte/core/jinja2/core/base.jinja2 rename to src/core/jinja2/core/base.jinja2 diff --git a/src/core/jinja2/core/collection.jinja2 b/src/core/jinja2/core/collection.jinja2 new file mode 100644 index 00000000..cd4133ff --- /dev/null +++ b/src/core/jinja2/core/collection.jinja2 @@ -0,0 +1,57 @@ +{% extends '/core/home.jinja2' %} + +{% block content %} + {% block subcontent %} + {% endblock %} +
+
+ {# FIXME: maybe this can be improved #} + + + {% for element_type, repository_list, class_type in [("exhibit",exhibits, "titExb"), + ("artwork",artworks, "titArt"), + ("marker",markers, "titMrk"), + ("object",objects, "titObj") + ]%} + + {% if repository_list %} +

{{_("Jandig " + element_type.capitalize() + "s")}}

+ {% with repository_list = repository_list, element_type=element_type %} + {% include "users/components/item-list.jinja2" %} + {% endwith %} + {% if seeall == False %} + {{_("All " + element_type.capitalize() + "s")}} + {% endif %} + {% if seeall == True %} + + {% endif %} + {% endif %} + {% endfor %} + {% if not exhibits and not artworks and not markers and not objects %} +

{{_("We found no content on your Collection, try uploading an object.")}}

+ {% endif %} + + + {% include "users/components/elements-modal.jinja2" %} +
+
+{% endblock %} diff --git a/src/ARte/core/jinja2/core/community.jinja2 b/src/core/jinja2/core/community.jinja2 similarity index 100% rename from src/ARte/core/jinja2/core/community.jinja2 rename to src/core/jinja2/core/community.jinja2 diff --git a/src/ARte/core/jinja2/core/documentation.jinja2 b/src/core/jinja2/core/documentation.jinja2 similarity index 100% rename from src/ARte/core/jinja2/core/documentation.jinja2 rename to src/core/jinja2/core/documentation.jinja2 diff --git a/src/ARte/core/jinja2/core/exhibit.jinja2 b/src/core/jinja2/core/exhibit.jinja2 similarity index 100% rename from src/ARte/core/jinja2/core/exhibit.jinja2 rename to src/core/jinja2/core/exhibit.jinja2 diff --git a/src/ARte/core/jinja2/core/exhibit_detail.jinja2 b/src/core/jinja2/core/exhibit_detail.jinja2 similarity index 100% rename from src/ARte/core/jinja2/core/exhibit_detail.jinja2 rename to src/core/jinja2/core/exhibit_detail.jinja2 diff --git a/src/ARte/core/jinja2/core/exhibit_select.jinja2 b/src/core/jinja2/core/exhibit_select.jinja2 similarity index 100% rename from src/ARte/core/jinja2/core/exhibit_select.jinja2 rename to src/core/jinja2/core/exhibit_select.jinja2 diff --git a/src/ARte/core/jinja2/core/footer.jinja2 b/src/core/jinja2/core/footer.jinja2 similarity index 100% rename from src/ARte/core/jinja2/core/footer.jinja2 rename to src/core/jinja2/core/footer.jinja2 diff --git a/src/ARte/core/jinja2/core/generator.html b/src/core/jinja2/core/generator.html similarity index 98% rename from src/ARte/core/jinja2/core/generator.html rename to src/core/jinja2/core/generator.html index 26183707..9af97e45 100644 --- a/src/ARte/core/jinja2/core/generator.html +++ b/src/core/jinja2/core/generator.html @@ -2,6 +2,7 @@ Jandig Marker Generator + diff --git a/src/ARte/core/jinja2/core/header.jinja2 b/src/core/jinja2/core/header.jinja2 similarity index 98% rename from src/ARte/core/jinja2/core/header.jinja2 rename to src/core/jinja2/core/header.jinja2 index 2c7a4326..9810f1a7 100644 --- a/src/ARte/core/jinja2/core/header.jinja2 +++ b/src/core/jinja2/core/header.jinja2 @@ -9,7 +9,7 @@
diff --git a/src/ARte/core/jinja2/core/home.jinja2 b/src/core/jinja2/core/home.jinja2 similarity index 91% rename from src/ARte/core/jinja2/core/home.jinja2 rename to src/core/jinja2/core/home.jinja2 index fa8a191b..6199f324 100644 --- a/src/ARte/core/jinja2/core/home.jinja2 +++ b/src/core/jinja2/core/home.jinja2 @@ -12,6 +12,7 @@ + @@ -20,9 +21,12 @@ + + {% block extra_css %} + {% endblock %} + - {% include "core/header.jinja2" %} diff --git a/src/ARte/core/jinja2/core/language-select-modal.jinja2 b/src/core/jinja2/core/language-select-modal.jinja2 similarity index 100% rename from src/ARte/core/jinja2/core/language-select-modal.jinja2 rename to src/core/jinja2/core/language-select-modal.jinja2 diff --git a/src/ARte/core/jinja2/core/manifest.json b/src/core/jinja2/core/manifest.json similarity index 100% rename from src/ARte/core/jinja2/core/manifest.json rename to src/core/jinja2/core/manifest.json diff --git a/src/ARte/core/jinja2/core/sw.js b/src/core/jinja2/core/sw.js similarity index 100% rename from src/ARte/core/jinja2/core/sw.js rename to src/core/jinja2/core/sw.js diff --git a/src/ARte/core/jinja2/core/ui.jinja2 b/src/core/jinja2/core/ui.jinja2 similarity index 100% rename from src/ARte/core/jinja2/core/ui.jinja2 rename to src/core/jinja2/core/ui.jinja2 diff --git a/src/ARte/core/jinja2/core/upload.jinja2 b/src/core/jinja2/core/upload.jinja2 similarity index 100% rename from src/ARte/core/jinja2/core/upload.jinja2 rename to src/core/jinja2/core/upload.jinja2 diff --git a/src/ARte/core/jinja2/core/useful_links.jinja2 b/src/core/jinja2/core/useful_links.jinja2 similarity index 65% rename from src/ARte/core/jinja2/core/useful_links.jinja2 rename to src/core/jinja2/core/useful_links.jinja2 index 39e4877f..bbb5bbaf 100644 --- a/src/ARte/core/jinja2/core/useful_links.jinja2 +++ b/src/core/jinja2/core/useful_links.jinja2 @@ -9,7 +9,13 @@ {{ _('Collection') }}
+ +
{{ _('Help') }} diff --git a/src/ARte/core/migrations/0001_initial.py b/src/core/migrations/0001_initial.py similarity index 100% rename from src/ARte/core/migrations/0001_initial.py rename to src/core/migrations/0001_initial.py diff --git a/src/ARte/core/migrations/0002_exhibit_creation_date.py b/src/core/migrations/0002_exhibit_creation_date.py similarity index 100% rename from src/ARte/core/migrations/0002_exhibit_creation_date.py rename to src/core/migrations/0002_exhibit_creation_date.py diff --git a/src/ARte/core/migrations/0003_auto_20200213_1934.py b/src/core/migrations/0003_auto_20200213_1934.py similarity index 100% rename from src/ARte/core/migrations/0003_auto_20200213_1934.py rename to src/core/migrations/0003_auto_20200213_1934.py diff --git a/src/ARte/core/migrations/0004_delete_artwork2.py b/src/core/migrations/0004_delete_artwork2.py similarity index 100% rename from src/ARte/core/migrations/0004_delete_artwork2.py rename to src/core/migrations/0004_delete_artwork2.py diff --git a/src/ARte/core/migrations/0005_fake_create_tables.py b/src/core/migrations/0005_fake_create_tables.py similarity index 100% rename from src/ARte/core/migrations/0005_fake_create_tables.py rename to src/core/migrations/0005_fake_create_tables.py diff --git a/src/core/migrations/0006_fake_alter_model_table.py b/src/core/migrations/0006_fake_alter_model_table.py new file mode 100644 index 00000000..79b84298 --- /dev/null +++ b/src/core/migrations/0006_fake_alter_model_table.py @@ -0,0 +1,28 @@ +# Generated by Django 4.1.2 on 2022-10-30 19:57 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("core", "0005_fake_create_tables"), + ] + + operations = [ + migrations.SeparateDatabaseAndState( + state_operations=[ + migrations.AlterModelTable( + name="artwork", + table=None, + ), + migrations.AlterModelTable( + name="marker", + table=None, + ), + migrations.AlterModelTable( + name="object", + table=None, + )], + database_operations=[]), + ] diff --git a/src/core/migrations/0007_alter_artwork_id_alter_exhibit_id_alter_marker_id_and_more.py b/src/core/migrations/0007_alter_artwork_id_alter_exhibit_id_alter_marker_id_and_more.py new file mode 100644 index 00000000..d7b983b0 --- /dev/null +++ b/src/core/migrations/0007_alter_artwork_id_alter_exhibit_id_alter_marker_id_and_more.py @@ -0,0 +1,41 @@ +# Generated by Django 4.1.2 on 2022-10-30 20:03 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("core", "0006_fake_alter_model_table"), + ] + + operations = [ + migrations.AlterField( + model_name="artwork", + name="id", + field=models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + migrations.AlterField( + model_name="exhibit", + name="id", + field=models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + migrations.AlterField( + model_name="marker", + name="id", + field=models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + migrations.AlterField( + model_name="object", + name="id", + field=models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ) + ] diff --git a/src/core/migrations/0008_alter_artwork_author_alter_marker_owner_and_more.py b/src/core/migrations/0008_alter_artwork_author_alter_marker_owner_and_more.py new file mode 100644 index 00000000..6169cc73 --- /dev/null +++ b/src/core/migrations/0008_alter_artwork_author_alter_marker_owner_and_more.py @@ -0,0 +1,41 @@ +# Generated by Django 4.1.5 on 2023-06-24 18:58 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ("core", "0007_alter_artwork_id_alter_exhibit_id_alter_marker_id_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="artwork", + name="author", + field=models.ForeignKey( + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="artworks", + to="users.profile", + ), + ), + migrations.AlterField( + model_name="marker", + name="owner", + field=models.ForeignKey( + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="markers", + to="users.profile", + ), + ), + migrations.AlterField( + model_name="object", + name="owner", + field=models.ForeignKey( + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="ar_objects", + to="users.profile", + ), + ), + ] diff --git a/src/ARte/users/services/__init__.py b/src/core/migrations/__init__.py similarity index 100% rename from src/ARte/users/services/__init__.py rename to src/core/migrations/__init__.py diff --git a/src/ARte/core/models.py b/src/core/models.py similarity index 57% rename from src/ARte/core/models.py rename to src/core/models.py index 853416b2..8f301cb3 100644 --- a/src/ARte/core/models.py +++ b/src/core/models.py @@ -1,19 +1,68 @@ +import re + +from django.core.files.base import ContentFile from django.db import models -from users.models import Profile -from datetime import datetime +from django.db.models.signals import post_delete from django.dispatch import receiver -from django.db.models.signals import post_save, post_delete -import urllib -import re +from PIL import Image +from pymarker.core import generate_marker_from_image, generate_patt_from_image + +from config.storage_backends import PublicMediaStorage +from users.models import Profile + +import logging +log = logging.getLogger() + +def create_patt(filename, original_filename): + filestorage = PublicMediaStorage() + with Image.open(filestorage.open(filename)) as image: + patt_str = generate_patt_from_image(image) + # string_file = StringIO(patt_str.encode('UTF-8')) + # string_file.name = original_filename + patt_file = filestorage.save( + "patts/" + original_filename + ".patt", + ContentFile(patt_str.encode("utf-8")), + ) + return patt_file + + +def create_marker(filename, original_filename): + filestorage = PublicMediaStorage() + with Image.open(filestorage.open(filename)) as image: + marker_image = generate_marker_from_image(image) + marker_image.name = original_filename + marker_image.__commited = False + # marker = filestorage.save("markers/" + original_filename, marker_image) + return marker_image + class Marker(models.Model): - owner = models.ForeignKey(Profile, on_delete=models.DO_NOTHING) - source = models.ImageField(upload_to='markers/') + owner = models.ForeignKey(Profile, on_delete=models.DO_NOTHING, related_name="markers") + source = models.ImageField(upload_to="markers/") uploaded_at = models.DateTimeField(auto_now=True) author = models.CharField(max_length=60, blank=False) - title = models.CharField(max_length=60, default='') - patt = models.FileField(upload_to='patts/') - + title = models.CharField(max_length=60, default="") + patt = models.FileField(upload_to="patts/") + + def save(self, *args, **kwargs): + # filestorage = PublicMediaStorage() + # # Image Filename + # original_filename = self.source.name + # filename = filestorage.save(f"original_{original_filename}", self.source) + # # Complete Image URL on storage + # print("aaaaa"*30) + # with Image.open(self.source) as image: + # print(image) + # print("aaaaa"*30) + # # fileurl = filestorage.url(filename) + # print(filename) + # self.source = create_marker(filename, original_filename) + # self.patt = create_patt(filename, original_filename) + print("B" * 30) + print(self.source) + print(self.patt) + print("B" * 30) + super().save(*args, **kwargs) def __str__(self): return self.source.name @@ -24,16 +73,14 @@ def artworks_count(self): @property def artworks_list(self): - return Artwork.objects.filter(marker=self) + return Artwork.objects.filter(marker=self).order_by("-id") @property def exhibits_count(self): - from core.models import Exhibit return Exhibit.objects.filter(artworks__marker=self).count() @property def exhibits_list(self): - from core.models import Exhibit return Exhibit.objects.filter(artworks__marker=self) @property @@ -42,17 +89,17 @@ def in_use(self): return True return False + class Object(models.Model): - owner = models.ForeignKey(Profile, on_delete=models.DO_NOTHING) - source = models.FileField(upload_to='objects/') + owner = models.ForeignKey(Profile, on_delete=models.DO_NOTHING, related_name="ar_objects") + source = models.FileField(upload_to="objects/") uploaded_at = models.DateTimeField(auto_now=True) author = models.CharField(max_length=60, blank=False) - title = models.CharField(max_length=60, default='') + title = models.CharField(max_length=60, default="") scale = models.CharField(default="1 1", max_length=50) position = models.CharField(default="0 0 0", max_length=50) rotation = models.CharField(default="270 0 0", max_length=50) - def __str__(self): return self.source.name @@ -62,16 +109,14 @@ def artworks_count(self): @property def artworks_list(self): - return Artwork.objects.filter(augmented=self) + return Artwork.objects.filter(augmented=self).order_by("-id") @property def exhibits_count(self): - from core.models import Exhibit return Exhibit.objects.filter(artworks__augmented=self).count() @property def exhibits_list(self): - from core.models import Exhibit return Exhibit.objects.filter(artworks__augmented=self) @property @@ -80,108 +125,105 @@ def in_use(self): return True return False - + @property def xproportion(self): - ''' + """ The 'xproportion' method is used to always reduce scale - to 1:[something], so that new calculations can be made + to 1:[something], so that new calculations can be made when a new scale value is entered by the user. - ''' - a = re.findall(r'[\d\.\d]+', self.scale) + """ + a = re.findall(r"[\d\.\d]+", self.scale) width = float(a[0]) height = float(a[1]) - if width > height : - height = (height*1.0)/width + if width > height: + height = (height * 1.0) / width width = 1 - else : - width = (width*1.0)/height + else: + width = (width * 1.0) / height height = 1 return width @property def yproportion(self): - ''' + """ The 'yproportion' method is used to always reduce scale - to 1:[something], so that new calculations can be made + to 1:[something], so that new calculations can be made when a new scale value is entered by the user. - ''' - a = re.findall(r'[\d\.\d]+', self.scale) + """ + a = re.findall(r"[\d\.\d]+", self.scale) width = float(a[0]) height = float(a[1]) - if width > height : - height = (height*1.0)/width + if width > height: + height = (height * 1.0) / width width = 1 - else : - width = (width*1.0)/height + else: + width = (width * 1.0) / height height = 1 return height @property def xscale(self): - ''' + """ The 'xscale' method returns the original proportion of the Object multiplied by the scale value entered by the user, and thus the Object appears resized in augmented reality. - ''' - a = re.findall(r'[\d\.\d]+', self.scale) + """ + a = re.findall(r"[\d\.\d]+", self.scale) return a[0] @property def yscale(self): - ''' + """ The 'yscale' method returns the original proportion of the Object multiplied by the scale value entered by the user, and thus the Object appears resized in augmented reality. - ''' - a = re.findall(r'[\d\.\d]+', self.scale) + """ + a = re.findall(r"[\d\.\d]+", self.scale) return a[1] @property def fullscale(self): - ''' + """ The 'fullscale' method is a workaround to show the users the last scale value entered by them, when they attempt to edit it. - ''' + """ x = self.xscale y = self.yscale if x > y: return x - else: - return y + return y @property def xposition(self): - a = re.findall(r'[\d\.\d]+', self.position) - return a[0] + x = self.position.split(" ")[0] + return float(x) @property def yposition(self): - a = re.findall(r'[\d\.\d]+', self.position) - return a[1] + y = self.position.split(" ")[1] + return float(y) + class Artwork(models.Model): - author = models.ForeignKey(Profile, on_delete=models.DO_NOTHING) + author = models.ForeignKey(Profile, on_delete=models.DO_NOTHING, related_name="artworks") marker = models.ForeignKey(Marker, on_delete=models.DO_NOTHING) augmented = models.ForeignKey(Object, on_delete=models.DO_NOTHING) title = models.CharField(max_length=50, blank=False) description = models.TextField(max_length=500, blank=True) created_at = models.DateTimeField(auto_now=True) - @property def exhibits_count(self): - from core.models import Exhibit return Exhibit.objects.filter(artworks__in=[self]).count() @property def exhibits_list(self): - from core.models import Exhibit return list(Exhibit.objects.filter(artworks__in=[self])) - + @property def in_use(self): if self.exhibits_count > 0: @@ -189,16 +231,18 @@ def in_use(self): return False + @receiver(post_delete, sender=Object) @receiver(post_delete, sender=Marker) def remove_source_file(sender, instance, **kwargs): instance.source.delete(False) + class Exhibit(models.Model): - owner = models.ForeignKey(Profile,on_delete=models.DO_NOTHING,related_name="exhibits") + owner = models.ForeignKey(Profile, on_delete=models.DO_NOTHING, related_name="exhibits") name = models.CharField(unique=True, max_length=50) slug = models.CharField(unique=True, max_length=50) - artworks = models.ManyToManyField(Artwork,related_name="exhibits") + artworks = models.ManyToManyField(Artwork, related_name="exhibits") creation_date = models.DateTimeField(auto_now=True) def __str__(self): diff --git a/src/ARte/core/routes.py b/src/core/routes.py similarity index 75% rename from src/ARte/core/routes.py rename to src/core/routes.py index 2c7eff76..1b99d0aa 100644 --- a/src/ARte/core/routes.py +++ b/src/core/routes.py @@ -11,8 +11,5 @@ @urlpatterns.route("/") def exhibit(request, exhibit): - ctx = { - 'exhibit' : exhibit, - 'artworks': exhibit.artworks.all() - } + ctx = {"exhibit": exhibit, "artworks": exhibit.artworks.all()} return ctx diff --git a/src/ARte/core/static/js/rotationLock.js b/src/core/serializers/__init__.py similarity index 100% rename from src/ARte/core/static/js/rotationLock.js rename to src/core/serializers/__init__.py diff --git a/src/core/serializers/artworks.py b/src/core/serializers/artworks.py new file mode 100644 index 00000000..04751a7d --- /dev/null +++ b/src/core/serializers/artworks.py @@ -0,0 +1,21 @@ +from rest_framework.serializers import ModelSerializer + +from core.models import Artwork + + +class ArtworkSerializer(ModelSerializer): + class Meta: + model = Artwork + fields = ( + "id", + "author", + "marker", + "augmented", + "title", + "description", + "created_at", + ) + read_only_fields = ( + "id", + "uploaded_at", + ) diff --git a/src/core/serializers/exhibits.py b/src/core/serializers/exhibits.py new file mode 100644 index 00000000..502e7272 --- /dev/null +++ b/src/core/serializers/exhibits.py @@ -0,0 +1,13 @@ +from rest_framework.serializers import ModelSerializer + +from core.models import Exhibit + + +class ExhibitSerializer(ModelSerializer): + class Meta: + model = Exhibit + fields = ("id", "owner", "name", "slug", "artworks", "creation_date") + read_only_fields = ( + "id", + "creation_date", + ) diff --git a/src/core/serializers/markers.py b/src/core/serializers/markers.py new file mode 100644 index 00000000..9b0ac3ae --- /dev/null +++ b/src/core/serializers/markers.py @@ -0,0 +1,13 @@ +from rest_framework.serializers import ModelSerializer + +from core.models import Marker + + +class MarkerSerializer(ModelSerializer): + class Meta: + model = Marker + fields = ("id", "owner", "source", "uploaded_at", "author", "title", "patt") + read_only_fields = ( + "id", + "uploaded_at", + ) diff --git a/src/core/serializers/objects.py b/src/core/serializers/objects.py new file mode 100644 index 00000000..38f7edd4 --- /dev/null +++ b/src/core/serializers/objects.py @@ -0,0 +1,23 @@ +from rest_framework.serializers import ModelSerializer + +from core.models import Object + + +class ObjectSerializer(ModelSerializer): + class Meta: + model = Object + fields = ( + "id", + "owner", + "source", + "uploaded_at", + "author", + "title", + "scale", + "position", + "rotation", + ) + read_only_fields = ( + "id", + "uploaded_at", + ) diff --git a/src/ARte/core/static/ar-gif.min.js b/src/core/static/ar-gif.min.js similarity index 100% rename from src/ARte/core/static/ar-gif.min.js rename to src/core/static/ar-gif.min.js diff --git a/src/ARte/core/static/artoolkit.min.js b/src/core/static/artoolkit.min.js similarity index 100% rename from src/ARte/core/static/artoolkit.min.js rename to src/core/static/artoolkit.min.js diff --git a/src/ARte/core/static/artoolkit.three.js b/src/core/static/artoolkit.three.js similarity index 100% rename from src/ARte/core/static/artoolkit.three.js rename to src/core/static/artoolkit.three.js diff --git a/src/ARte/core/static/css/exhibit-detail.css b/src/core/static/css/exhibit-detail.css similarity index 100% rename from src/ARte/core/static/css/exhibit-detail.css rename to src/core/static/css/exhibit-detail.css diff --git a/src/ARte/core/static/css/generator.css b/src/core/static/css/generator.css similarity index 100% rename from src/ARte/core/static/css/generator.css rename to src/core/static/css/generator.css diff --git a/src/ARte/core/static/css/home.css b/src/core/static/css/home.css similarity index 97% rename from src/ARte/core/static/css/home.css rename to src/core/static/css/home.css index 9f933ab2..93da6413 100644 --- a/src/ARte/core/static/css/home.css +++ b/src/core/static/css/home.css @@ -7,6 +7,7 @@ padding: 0px 8px; } + .header .login-btn { background-color: #03b595; } @@ -51,7 +52,6 @@ height: 40px; line-height: 40px; width: calc(50% - 5px); - padding-left: 0px; margin: 0; margin-left: 10px; } diff --git a/src/ARte/core/static/css/language-select-modal.css b/src/core/static/css/language-select-modal.css similarity index 100% rename from src/ARte/core/static/css/language-select-modal.css rename to src/core/static/css/language-select-modal.css diff --git a/src/ARte/core/static/css/main.css b/src/core/static/css/main.css similarity index 100% rename from src/ARte/core/static/css/main.css rename to src/core/static/css/main.css diff --git a/src/core/static/css/reset.css b/src/core/static/css/reset.css new file mode 100644 index 00000000..2b0de121 --- /dev/null +++ b/src/core/static/css/reset.css @@ -0,0 +1,896 @@ +/* @import url('https://fonts.googleapis.com/css?family=Istok+Web:400,400i,700,700i&display=swap'); */ + +body { + color: #000; + background: #fff; + margin: 0; + padding: 0; + line-height: 1; + font-family: "Istok Web", sans-serif; + text-align: center; +} + +body.colorful { + background-image: url(../images/icons/bg.png); + background-repeat: repeat-x; + background-position: center top; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + font-family: "Istok Web", serif; + font-weight: bolder; +} + +p, +ul, +ol { + line-height: 1.75; + margin: 0 0 1em 0; +} + +section, +form { + margin-bottom: 20px; +} + +a, +a:visited { + color: #03b595; + color: #777; + padding: 0; + text-decoration: none; +} + +h1 { + font-size: 1.25em; + line-height: 50px; + background-image: linear-gradient( + to top, + #77777766, + #77777766 50%, + transparent 50%, + transparent + ); + background-position: 100% 15%; + background-size: 100% 200%; + margin-top: 10px; + width: 100%; +} + +h2 { + font-size: 1.25em; + color: #03b595; + margin: 30px auto 10px; +} + +h3 { + font-size: 1.1em; + line-height: 20px; + text-align: left; +} + +h3:before { + width: 20px; + height: 20px; + background: #03b595; + display: inline-block; + margin: 5px 10px -3px 0px; + content: " "; +} + +h4 { + font-size: 1.1em; + color: #03b595; +} + +h5 { + font-size: 1.1em; +} + +h6 { + font-size: 1em; + color: #03b595; +} + +hr { + height: 10px; + width: 80%; + border: none; + background: #03b595; + margin: 20px auto; +} + +.account-options a { + color: #777; + padding: 0; + text-decoration: none; + transition: all 0.2s ease-in; + background-image: linear-gradient( + to top, + #77777766, + #77777766 50%, + transparent 50%, + transparent + ); + background-position: 100% 10%; + background-size: 100% 200%; +} + +ul { + list-style: square; +} + +ul, +ol { + padding: 0 0 0 5em; + text-align: left; +} + +blockquote { + font-size: 1.1em; + line-height: 2em; + margin: 10px 40px; + color: #888; + font-style: italic; +} + +blockquote:before, +blockquote:after { + max-width: 40px; + height: 40px; + display: block; + margin: 0 auto -20px 0; + background-image: url(../images/icons/fill.png); + content: ""; + text-align: left; +} + +blockquote:after { + text-align: right; + margin: -30px 0 20px auto; +} + +input { + width: calc(100% - 24px); + padding: 0 10px; + height: 40px; +} + +textarea { + width: calc(100% - 24px); + padding: 10px; +} + +select { + width: 100%; + height: 40px; + padding: 0 10px; +} + +input[type="submit"], +input[type="submit" i], +button, +.button { + width: 100%; + margin: 20px 0; + background: #000; + border: none; + color: #fff; + font-weight: bold; + font-size: 0.75em; + line-height: 40px; + text-transform: uppercase; + text-align: center; + display: inline-block; +} + +input:disabled, +input[type="submit" i]:disabled, +input[type="submit"]:disabled, +button.disabled, +button:disabled { + opacity: 0.4; + pointer-events: none; + color: #777; +} + +button.secondary, +input[type="submit"].secondary { + background: #666; + width: 100%; + line-height: 30px; + color: #fff; +} + +input[type="text"] { + padding: 0 10px; +} + +input[type="checkbox" i], +input[type="radio" i] { + height: 20px; + width: 45px; + margin: 10px 0; + line-height: 40px; + display: inline-block; + vertical-align: sub; +} + +.flex { + display: flex; + flex-flow: row wrap; + align-items: center; + justify-content: space-between; +} + +.half { + width: calc(50% - 10px); +} + +.one-third { + width: calc(33% - 10px); +} + +.sm-one-third { + flex-basis: calc(33% - 10px); +} + +.sm-half { + flex-basis: calc(50% - 10px); +} + +.sm-full { + flex-basis: 100%; +} + +.left { + text-align: left; +} + +.right { + text-align: right; +} + +/* Structure */ +.container { + max-width: 320px; + margin: 0 auto; + text-align: center; + width: 100%; +} + +.colorful .container { + background-color: #fff; + margin-top: 40px; + padding: 20px; + max-width: 280px; +} + +a.button, +a.button:visited { + color: #fff; +} + +a.changeExb, +a.jandigIco { + height: 40px; + text-align: left; + line-height: 40px; + background-repeat: no-repeat; + background-size: contain; + min-width: calc(50% - 20px); +} + +a.changeExb { + height: 20px; + line-height: 20px; + font-size: 0.75em; + color: #fff; + padding: 5px 0 5px 40px; + background-image: url(../images/icons/icoChgExb.png); + background-position: center left; +} + +a.jandigIco { + text-indent: -9999px; + background-image: url(../images/icons/jandigIcon.png); + background-position: center right; +} + +.header { + margin-top: 25px; + font-size: 75%; +} + +.aside { + max-width: 120px; + margin-left: -120px; + padding-left: 10px; + height: 70px; + float: right; + background: #fff; +} + +.logo { + text-align: left; + height: 60px; + width: 100%; + float: left; + background-image: linear-gradient( + to top, + #77777766, + #77777766 50%, + transparent 50%, + transparent + ); + background-position: 100% 10%; + background-size: 100% 200%; +} + +.logo a { + display: block; +} + +.logo img { + max-height: 55px; + height: auto; + margin: auto; +} + +.welcome, +.icon-menu, +.connect { + height: 30px; + text-align: right; + flex-flow: nowrap; +} + +.welcome { + height: 30px; + font-size: 0.65em; + overflow: hidden; + vertical-align: bottom; +} + +.welcome a, +.welcome a:visited { + font-size: 1.25em; + font-weight: bold; + color: #03b595; +} + +.welcome p { + line-height: 45px; +} + +.icon-menu a { + width: 30px; + height: 30px; + margin: 0 0 0 10px; + display: block; + font-size: 0; +} + +/****** temporary disable icons ******/ +.icon-menu a { + opacity: 0.2; + pointer-events: none; +} + +.icon-menu a.btnLang { + background: url(../images/icons/icoLng.png) no-repeat; + background-position: center center; + background-size: contain; + margin: 0 0 0 auto; + font-size: 0.8em; + font-weight: bold; + font-family: monospace; + text-transform: uppercase; + color: #000; + padding: 10px; + height: 10px; + width: 10px; + line-height: 13px; + text-align: center; + overflow: hidden; +} + +.icon-menu a.btnSearch { + background: url(../images/icons/icoSrc.png) no-repeat; + background-position: center center; + background-size: contain; +} + +.icon-menu a.btnHelp { + background: url(../images/icons/icoHlp.png) no-repeat; + background-position: center center; + background-size: contain; +} + +.trigger-lang-modal { + margin: 0 0 0 auto; +} + +.contCam { + min-width: 90px; +} + +div.contCam:before { + content: ""; + display: block; + height: 0; + width: 0; + position: relative; + top: -21px; + left: 45px; + border-color: #c9c9c9 transparent transparent #c9c9c9; + border-style: solid; + border-width: 7px; + pointer-events: none; + z-index: 1; +} + +.animCam { + width: 40px; + height: 40px; + margin: -25px auto 0; + background: #03b595; +} + +.useful-links a.cambtn { + width: 40px; + height: 40px; + margin: 0; + padding: 0; + display: block; + text-indent: -999999px; + background: url(../images/icons/icoARV.png) no-repeat; + background-position: center center; + background-size: contain; +} + +.useful-links { + margin-bottom: 20px; + font-size: 0.75em; + font-weight: bold; + clear: both; + font-size: 75%; +} + +.useful-links .container { + height: 50px; + margin: auto; + column-gap: 1.5em; +} + +.useful-links a, +.footer a.useful { + padding: 0 5px; + background-image: linear-gradient( + to top, + #05f7ae, + #05f7ae 50%, + transparent 50%, + transparent + ); + background-position: 100% 15%; + background-size: 100% 200%; +} + +.fill { + background: url(../images/icons/fill.png), #e6e6e6; + padding: 20px; + width: calc(100% - 40px); + margin-bottom: 20px; +} + +h1.titExb, +h1.titArt, +h1.titMrk, +h1.titObj { + height: 40px; + line-height: 45px; + text-align: left; + padding-left: 35px; + width: calc(100% - 35px); + background-position: left center; + background-size: 25px 25px; + background-repeat: no-repeat; + border-bottom: 10px solid #f1f1f1; +} + +h1.titExb { + background-image: url(../images/icons/icoExb.png); +} + +h1.titArt { + background-image: url(../images/icons/icoArt.png); +} + +h1.titMrk { + background-image: url(../images/icons/icoMrk.png); +} + +h1.titObj { + background-image: url(../images/icons/icoObj.png); +} + +.footer { + min-height: 90px; +} + +.footer .container { + height: 25px; + margin-bottom: 20px; +} + +.footer a { + font-size: 75%; + line-height: 25px; + margin: auto; +} + +.footer a.useful { + margin: 0; + line-height: 25px; +} + +a.github { + background-image: url(../images/icons/bulGit.png); + background-size: contain; + background-position: left center; + background-repeat: no-repeat; + color: #03b595; + padding-left: 35px; +} + +a.license { + background-image: url(../images/icons/license.png); + background-size: contain; + background-position: left center; + background-repeat: no-repeat; + padding-left: 65px; + font-size: 0.5em; + line-height: 20px; + margin-left: 0; + text-align: left; + white-space: nowrap; + max-width: calc(100% - 160px); + overflow: inherit; + text-indent: -9999999px; +} + +a.memelab { + background-image: url(../images/icons/memelab.png); + background-size: contain; + background-position: left center; + background-repeat: no-repeat; + width: 20px; + height: 20px; + text-indent: -99999px; + max-width: 35px; + margin: 0; +} + +a.instagram, +a.twitter, +a.facebook, +a.telegram, +a.sq-github { + background-size: contain; + background-position: left center; + background-repeat: no-repeat; + width: 25px; + height: 25px; + text-indent: -99999px; + max-width: 35px; + margin: 5px 0px 5px 5px; +} + +a.instagram { + background-image: url(../images/icons/icoIst.png); +} + +a.twitter { + background-image: url(../images/icons/icoTwt.png); +} + +a.facebook { + background-image: url(../images/icons/icoFcb.png); +} + +a.telegram { + background-image: url(../images/icons/icoTel.png); +} + +a.sq-github { + background-image: url(../images/icons/icoGit.png); +} + +.form-options { + text-align: left; +} + +.artwork-elements .separator { + width: 10px; + height: 50px; + max-height: 50px; + min-height: 50px; + background: #06f6ab; + margin: 0 10px 0 -5px; +} + +.artwork-elements img.trigger-modal { + cursor: pointer; + margin: 0px 15px 10px; +} + +.artwork-elements .separator:after { + content: ""; + display: block; + height: 0; + width: 0; + position: relative; + top: 15px; + left: 10px; + border-color: #06f6ab transparent transparent #06f6ab; + border-style: solid; + border-width: 7px; + pointer-events: none; + z-index: 1; +} + +/* Media Queries */ +@media all and (min-width: 800px) { + .container { + max-width: 600px; + margin: 0 auto; + } + + .colorful .container { + padding: 20px; + max-width: 560px; + } + + .header { + margin-top: 25px; + } + + .aside { + max-width: 250px; + min-width: 250px; + height: 100px; + margin-left: -250px; + } + + .half { + width: 270px; + } + + .lg-one-third { + flex-basis: calc(33% - 10px); + } + + .lg-half { + flex-basis: calc(50% - 10px); + } + + .lg-full { + flex-basis: 100%; + } + + .logo { + text-align: left; + height: 95px; + width: calc(100% - 160px); + float: left; + background-image: linear-gradient( + to top, + #77777766, + #77777766 50%, + transparent 50%, + transparent + ); + background-position: 100% 10%; + background-size: 100% 200%; + } + + .welcome, + .icon-menu, + .connect { + height: 50px; + text-align: right; + font-size: 0.75em; + } + + .welcome, + .welcome p { + line-height: 80px; + font-size: 1em; + } + + .header .logo img { + max-height: 75px; + height: auto; + margin: auto; + } + + .icon-menu a { + width: 40px; + height: 40px; + margin: 0 0 0 10px; + } + + .icon-menu a.btnLang { + height: 20px; + width: 19px; + line-height: 23px; + font-size: 2.5em; + } + + .useful-links a, + a.useful { + padding: 5px; + background-image: linear-gradient( + to top, + #05f7ae, + #05f7ae 50%, + transparent 50%, + transparent + ); + background-position: 100% 15%; + background-size: 100% 200%; + } + .useful-links .container { + column-gap: 0; + } + .contCam { + min-width: 250px; + } + + div.contCam:before { + top: -10px; + left: 125px; + border-width: 10px; + } + + .animCam { + width: 40px; + height: 40px; + margin: 0 auto; + margin-top: -20px; + background: #03b595; + } + + .useful-links a.cambtn { + width: 40px; + height: 40px; + margin: 0; + padding: 0; + display: block; + text-indent: -999999px; + background: url(../images/icons/icoARV.png) no-repeat; + background-position: center center; + background-size: contain; + } + + .footer { + min-height: 100px; + } + + .footer .container { + height: 30px; + margin-bottom: 40px; + } + + .footer a { + line-height: 30px; + } + + a.instagram, + a.twitter, + a.facebook, + a.telegram, + a.sq-github { + width: 30px; + height: 30px; + } + + a.github { + padding-left: 40px; + } + + a.license { + padding-left: 100px; + height: 30px; + line-height: 30px; + font-size: 0.75em; + text-indent: 0; + } + + a.memelab { + width: 30px; + height: 30px; + max-width: 40px; + } + +} + +/* Hover Elements */ +.useful-links a, +.footer a.useful, +.account-options a, +.header .logo img, +.animCam, +.memelab, +.social a, +.editProf a.option-link, +.welcome a, +a.changeExb, +a.jandigIco, +.signup-btn, +.login-btn, +.trigger-lang-modal, +.license { + transition: all 0.2s ease-in; +} + +.useful-links a:hover, +.footer a.useful:hover, +.account-options a:hover { + color: #fff; + background-position: 0 100%; +} + +.header .logo img:hover, +.animCam:hover, +.memelab:hover, +.social a:hover, +a.changeExb:hover, +a.jandigIco:hover, +.signup-btn:hover, +.login-btn:hover, +.trigger-lang-modal:hover, +.license:hover { + opacity: 0.7; +} + +.editProf a.option-link:hover, +.welcome a:hover { + color: #06f7ae; +} + +/****** Animation ********/ +.animCam { + animation: colorchange 4s infinite; /* animation-name followed by duration in seconds*/ + -webkit-animation: colorchange 4s infinite; /* Chrome and Safari */ +} + +@keyframes colorchange { + 0% { + background: #03b595; + } + 50% { + background: #06f7ae; + } + 100% { + background: #03b595; + } +} + +@-webkit-keyframes colorchange { + 0% { + background: #03b595; + } + 50% { + background: #06f7ae; + } + 100% { + background: #03b595; + } +} diff --git a/src/ARte/core/static/images/icons/app/App Icon Back.svg b/src/core/static/images/icons/app/App Icon Back.svg similarity index 100% rename from src/ARte/core/static/images/icons/app/App Icon Back.svg rename to src/core/static/images/icons/app/App Icon Back.svg diff --git a/src/ARte/core/static/images/icons/app/App Icon Front.svg b/src/core/static/images/icons/app/App Icon Front.svg similarity index 100% rename from src/ARte/core/static/images/icons/app/App Icon Front.svg rename to src/core/static/images/icons/app/App Icon Front.svg diff --git a/src/ARte/core/static/images/icons/app/GooglePlay.png b/src/core/static/images/icons/app/GooglePlay.png similarity index 100% rename from src/ARte/core/static/images/icons/app/GooglePlay.png rename to src/core/static/images/icons/app/GooglePlay.png diff --git a/src/ARte/core/static/images/icons/app/Icon-20@1x.png b/src/core/static/images/icons/app/Icon-20@1x.png similarity index 100% rename from src/ARte/core/static/images/icons/app/Icon-20@1x.png rename to src/core/static/images/icons/app/Icon-20@1x.png diff --git a/src/ARte/core/static/images/icons/app/Icon-20@2x.png b/src/core/static/images/icons/app/Icon-20@2x.png similarity index 100% rename from src/ARte/core/static/images/icons/app/Icon-20@2x.png rename to src/core/static/images/icons/app/Icon-20@2x.png diff --git a/src/ARte/core/static/images/icons/app/Icon-20@3x.png b/src/core/static/images/icons/app/Icon-20@3x.png similarity index 100% rename from src/ARte/core/static/images/icons/app/Icon-20@3x.png rename to src/core/static/images/icons/app/Icon-20@3x.png diff --git a/src/ARte/core/static/images/icons/app/Icon-29.png b/src/core/static/images/icons/app/Icon-29.png similarity index 100% rename from src/ARte/core/static/images/icons/app/Icon-29.png rename to src/core/static/images/icons/app/Icon-29.png diff --git a/src/ARte/core/static/images/icons/app/Icon-29@2x.png b/src/core/static/images/icons/app/Icon-29@2x.png similarity index 100% rename from src/ARte/core/static/images/icons/app/Icon-29@2x.png rename to src/core/static/images/icons/app/Icon-29@2x.png diff --git a/src/ARte/core/static/images/icons/app/Icon-29@3x.png b/src/core/static/images/icons/app/Icon-29@3x.png similarity index 100% rename from src/ARte/core/static/images/icons/app/Icon-29@3x.png rename to src/core/static/images/icons/app/Icon-29@3x.png diff --git a/src/ARte/core/static/images/icons/app/Icon-40.png b/src/core/static/images/icons/app/Icon-40.png similarity index 100% rename from src/ARte/core/static/images/icons/app/Icon-40.png rename to src/core/static/images/icons/app/Icon-40.png diff --git a/src/ARte/core/static/images/icons/app/Icon-40@2x.png b/src/core/static/images/icons/app/Icon-40@2x.png similarity index 100% rename from src/ARte/core/static/images/icons/app/Icon-40@2x.png rename to src/core/static/images/icons/app/Icon-40@2x.png diff --git a/src/ARte/core/static/images/icons/app/Icon-40@3x.png b/src/core/static/images/icons/app/Icon-40@3x.png similarity index 100% rename from src/ARte/core/static/images/icons/app/Icon-40@3x.png rename to src/core/static/images/icons/app/Icon-40@3x.png diff --git a/src/ARte/core/static/images/icons/app/Icon-60@2x.png b/src/core/static/images/icons/app/Icon-60@2x.png similarity index 100% rename from src/ARte/core/static/images/icons/app/Icon-60@2x.png rename to src/core/static/images/icons/app/Icon-60@2x.png diff --git a/src/ARte/core/static/images/icons/app/Icon-60@3x.png b/src/core/static/images/icons/app/Icon-60@3x.png similarity index 100% rename from src/ARte/core/static/images/icons/app/Icon-60@3x.png rename to src/core/static/images/icons/app/Icon-60@3x.png diff --git a/src/ARte/core/static/images/icons/app/Icon-76.png b/src/core/static/images/icons/app/Icon-76.png similarity index 100% rename from src/ARte/core/static/images/icons/app/Icon-76.png rename to src/core/static/images/icons/app/Icon-76.png diff --git a/src/ARte/core/static/images/icons/app/Icon-76@2x.png b/src/core/static/images/icons/app/Icon-76@2x.png similarity index 100% rename from src/ARte/core/static/images/icons/app/Icon-76@2x.png rename to src/core/static/images/icons/app/Icon-76@2x.png diff --git a/src/ARte/core/static/images/icons/app/Icon-83.5@2x.png b/src/core/static/images/icons/app/Icon-83.5@2x.png similarity index 100% rename from src/ARte/core/static/images/icons/app/Icon-83.5@2x.png rename to src/core/static/images/icons/app/Icon-83.5@2x.png diff --git a/src/ARte/core/static/images/icons/app/iTunesArtwork@2x.png b/src/core/static/images/icons/app/iTunesArtwork@2x.png similarity index 100% rename from src/ARte/core/static/images/icons/app/iTunesArtwork@2x.png rename to src/core/static/images/icons/app/iTunesArtwork@2x.png diff --git a/src/ARte/core/static/images/icons/app/ic_launcher_HDPI.png b/src/core/static/images/icons/app/ic_launcher_HDPI.png similarity index 100% rename from src/ARte/core/static/images/icons/app/ic_launcher_HDPI.png rename to src/core/static/images/icons/app/ic_launcher_HDPI.png diff --git a/src/ARte/core/static/images/icons/app/ic_launcher_MDPI.png b/src/core/static/images/icons/app/ic_launcher_MDPI.png similarity index 100% rename from src/ARte/core/static/images/icons/app/ic_launcher_MDPI.png rename to src/core/static/images/icons/app/ic_launcher_MDPI.png diff --git a/src/ARte/core/static/images/icons/app/ic_launcher_XHDPI.png b/src/core/static/images/icons/app/ic_launcher_XHDPI.png similarity index 100% rename from src/ARte/core/static/images/icons/app/ic_launcher_XHDPI.png rename to src/core/static/images/icons/app/ic_launcher_XHDPI.png diff --git a/src/ARte/core/static/images/icons/app/ic_launcher_XXHDPI.png b/src/core/static/images/icons/app/ic_launcher_XXHDPI.png similarity index 100% rename from src/ARte/core/static/images/icons/app/ic_launcher_XXHDPI.png rename to src/core/static/images/icons/app/ic_launcher_XXHDPI.png diff --git a/src/ARte/core/static/images/icons/app/ic_launcher_XXXHDPI.png b/src/core/static/images/icons/app/ic_launcher_XXXHDPI.png similarity index 100% rename from src/ARte/core/static/images/icons/app/ic_launcher_XXXHDPI.png rename to src/core/static/images/icons/app/ic_launcher_XXXHDPI.png diff --git a/src/ARte/core/static/images/icons/app/ic_notification_HDPI.png b/src/core/static/images/icons/app/ic_notification_HDPI.png similarity index 100% rename from src/ARte/core/static/images/icons/app/ic_notification_HDPI.png rename to src/core/static/images/icons/app/ic_notification_HDPI.png diff --git a/src/ARte/core/static/images/icons/app/ic_notification_MDPI.png b/src/core/static/images/icons/app/ic_notification_MDPI.png similarity index 100% rename from src/ARte/core/static/images/icons/app/ic_notification_MDPI.png rename to src/core/static/images/icons/app/ic_notification_MDPI.png diff --git a/src/ARte/core/static/images/icons/app/ic_notification_XHDPI.png b/src/core/static/images/icons/app/ic_notification_XHDPI.png similarity index 100% rename from src/ARte/core/static/images/icons/app/ic_notification_XHDPI.png rename to src/core/static/images/icons/app/ic_notification_XHDPI.png diff --git a/src/ARte/core/static/images/icons/app/ic_notification_XXHDPI.png b/src/core/static/images/icons/app/ic_notification_XXHDPI.png similarity index 100% rename from src/ARte/core/static/images/icons/app/ic_notification_XXHDPI.png rename to src/core/static/images/icons/app/ic_notification_XXHDPI.png diff --git a/src/ARte/core/static/images/icons/app/ic_notification_XXXHDPI.png b/src/core/static/images/icons/app/ic_notification_XXXHDPI.png similarity index 100% rename from src/ARte/core/static/images/icons/app/ic_notification_XXXHDPI.png rename to src/core/static/images/icons/app/ic_notification_XXXHDPI.png diff --git a/src/ARte/core/static/images/icons/app/ic_status_bar_HDPI.png b/src/core/static/images/icons/app/ic_status_bar_HDPI.png similarity index 100% rename from src/ARte/core/static/images/icons/app/ic_status_bar_HDPI.png rename to src/core/static/images/icons/app/ic_status_bar_HDPI.png diff --git a/src/ARte/core/static/images/icons/app/ic_status_bar_MDPI.png b/src/core/static/images/icons/app/ic_status_bar_MDPI.png similarity index 100% rename from src/ARte/core/static/images/icons/app/ic_status_bar_MDPI.png rename to src/core/static/images/icons/app/ic_status_bar_MDPI.png diff --git a/src/ARte/core/static/images/icons/app/ic_status_bar_XHDPI.png b/src/core/static/images/icons/app/ic_status_bar_XHDPI.png similarity index 100% rename from src/ARte/core/static/images/icons/app/ic_status_bar_XHDPI.png rename to src/core/static/images/icons/app/ic_status_bar_XHDPI.png diff --git a/src/ARte/core/static/images/icons/app/ic_status_bar_XXHDPI.png b/src/core/static/images/icons/app/ic_status_bar_XXHDPI.png similarity index 100% rename from src/ARte/core/static/images/icons/app/ic_status_bar_XXHDPI.png rename to src/core/static/images/icons/app/ic_status_bar_XXHDPI.png diff --git a/src/ARte/core/static/images/icons/app/ic_status_bar_XXXHDPI.png b/src/core/static/images/icons/app/ic_status_bar_XXXHDPI.png similarity index 100% rename from src/ARte/core/static/images/icons/app/ic_status_bar_XXXHDPI.png rename to src/core/static/images/icons/app/ic_status_bar_XXXHDPI.png diff --git a/src/ARte/core/static/images/icons/bdgArt.png b/src/core/static/images/icons/bdgArt.png similarity index 100% rename from src/ARte/core/static/images/icons/bdgArt.png rename to src/core/static/images/icons/bdgArt.png diff --git a/src/ARte/core/static/images/icons/bdgCur.png b/src/core/static/images/icons/bdgCur.png similarity index 100% rename from src/ARte/core/static/images/icons/bdgCur.png rename to src/core/static/images/icons/bdgCur.png diff --git a/src/ARte/core/static/images/icons/bg.png b/src/core/static/images/icons/bg.png similarity index 100% rename from src/ARte/core/static/images/icons/bg.png rename to src/core/static/images/icons/bg.png diff --git a/src/ARte/core/static/images/icons/bulGit.png b/src/core/static/images/icons/bulGit.png similarity index 100% rename from src/ARte/core/static/images/icons/bulGit.png rename to src/core/static/images/icons/bulGit.png diff --git a/src/ARte/core/static/images/icons/delete.png b/src/core/static/images/icons/delete.png similarity index 100% rename from src/ARte/core/static/images/icons/delete.png rename to src/core/static/images/icons/delete.png diff --git a/src/ARte/core/static/images/icons/edit.png b/src/core/static/images/icons/edit.png similarity index 100% rename from src/ARte/core/static/images/icons/edit.png rename to src/core/static/images/icons/edit.png diff --git a/favicon.ico b/src/core/static/images/icons/favicon.ico similarity index 100% rename from favicon.ico rename to src/core/static/images/icons/favicon.ico diff --git a/src/ARte/core/static/images/icons/fill.png b/src/core/static/images/icons/fill.png similarity index 100% rename from src/ARte/core/static/images/icons/fill.png rename to src/core/static/images/icons/fill.png diff --git a/src/ARte/core/static/images/icons/header_icon.png b/src/core/static/images/icons/header_icon.png similarity index 100% rename from src/ARte/core/static/images/icons/header_icon.png rename to src/core/static/images/icons/header_icon.png diff --git a/src/ARte/core/static/images/icons/icoARV.png b/src/core/static/images/icons/icoARV.png similarity index 100% rename from src/ARte/core/static/images/icons/icoARV.png rename to src/core/static/images/icons/icoARV.png diff --git a/src/ARte/core/static/images/icons/icoArt.png b/src/core/static/images/icons/icoArt.png similarity index 100% rename from src/ARte/core/static/images/icons/icoArt.png rename to src/core/static/images/icons/icoArt.png diff --git a/src/ARte/core/static/images/icons/icoCamPurp.png b/src/core/static/images/icons/icoCamPurp.png similarity index 100% rename from src/ARte/core/static/images/icons/icoCamPurp.png rename to src/core/static/images/icons/icoCamPurp.png diff --git a/src/ARte/core/static/images/icons/icoChgExb.png b/src/core/static/images/icons/icoChgExb.png similarity index 100% rename from src/ARte/core/static/images/icons/icoChgExb.png rename to src/core/static/images/icons/icoChgExb.png diff --git a/src/ARte/core/static/images/icons/icoEpt.png b/src/core/static/images/icons/icoEpt.png similarity index 100% rename from src/ARte/core/static/images/icons/icoEpt.png rename to src/core/static/images/icons/icoEpt.png diff --git a/src/ARte/core/static/images/icons/icoEptG.png b/src/core/static/images/icons/icoEptG.png similarity index 100% rename from src/ARte/core/static/images/icons/icoEptG.png rename to src/core/static/images/icons/icoEptG.png diff --git a/src/ARte/core/static/images/icons/icoExb.png b/src/core/static/images/icons/icoExb.png similarity index 100% rename from src/ARte/core/static/images/icons/icoExb.png rename to src/core/static/images/icons/icoExb.png diff --git a/src/ARte/core/static/images/icons/icoFcb.png b/src/core/static/images/icons/icoFcb.png similarity index 100% rename from src/ARte/core/static/images/icons/icoFcb.png rename to src/core/static/images/icons/icoFcb.png diff --git a/src/ARte/core/static/images/icons/icoGit.png b/src/core/static/images/icons/icoGit.png similarity index 100% rename from src/ARte/core/static/images/icons/icoGit.png rename to src/core/static/images/icons/icoGit.png diff --git a/src/ARte/core/static/images/icons/icoHlp.png b/src/core/static/images/icons/icoHlp.png similarity index 100% rename from src/ARte/core/static/images/icons/icoHlp.png rename to src/core/static/images/icons/icoHlp.png diff --git a/src/ARte/core/static/images/icons/icoIst.png b/src/core/static/images/icons/icoIst.png similarity index 100% rename from src/ARte/core/static/images/icons/icoIst.png rename to src/core/static/images/icons/icoIst.png diff --git a/src/ARte/core/static/images/icons/icoLng.png b/src/core/static/images/icons/icoLng.png similarity index 100% rename from src/ARte/core/static/images/icons/icoLng.png rename to src/core/static/images/icons/icoLng.png diff --git a/src/ARte/core/static/images/icons/icoMrk.png b/src/core/static/images/icons/icoMrk.png similarity index 100% rename from src/ARte/core/static/images/icons/icoMrk.png rename to src/core/static/images/icons/icoMrk.png diff --git a/src/ARte/core/static/images/icons/icoObj.png b/src/core/static/images/icons/icoObj.png similarity index 100% rename from src/ARte/core/static/images/icons/icoObj.png rename to src/core/static/images/icons/icoObj.png diff --git a/src/ARte/core/static/images/icons/icoSrc.png b/src/core/static/images/icons/icoSrc.png similarity index 100% rename from src/ARte/core/static/images/icons/icoSrc.png rename to src/core/static/images/icons/icoSrc.png diff --git a/src/ARte/core/static/images/icons/icoTel.png b/src/core/static/images/icons/icoTel.png similarity index 100% rename from src/ARte/core/static/images/icons/icoTel.png rename to src/core/static/images/icons/icoTel.png diff --git a/src/ARte/core/static/images/icons/icoTwt.png b/src/core/static/images/icons/icoTwt.png similarity index 100% rename from src/ARte/core/static/images/icons/icoTwt.png rename to src/core/static/images/icons/icoTwt.png diff --git a/src/ARte/core/static/images/icons/jandigIcon.png b/src/core/static/images/icons/jandigIcon.png similarity index 100% rename from src/ARte/core/static/images/icons/jandigIcon.png rename to src/core/static/images/icons/jandigIcon.png diff --git a/src/ARte/core/static/images/icons/jandigMG.png b/src/core/static/images/icons/jandigMG.png similarity index 100% rename from src/ARte/core/static/images/icons/jandigMG.png rename to src/core/static/images/icons/jandigMG.png diff --git a/src/ARte/core/static/images/icons/license.png b/src/core/static/images/icons/license.png similarity index 100% rename from src/ARte/core/static/images/icons/license.png rename to src/core/static/images/icons/license.png diff --git a/src/ARte/core/static/images/icons/memelab.png b/src/core/static/images/icons/memelab.png similarity index 100% rename from src/ARte/core/static/images/icons/memelab.png rename to src/core/static/images/icons/memelab.png diff --git a/src/ARte/core/static/images/icons/preview.png b/src/core/static/images/icons/preview.png similarity index 100% rename from src/ARte/core/static/images/icons/preview.png rename to src/core/static/images/icons/preview.png diff --git a/src/ARte/core/static/ios-manifest.json b/src/core/static/ios-manifest.json similarity index 100% rename from src/ARte/core/static/ios-manifest.json rename to src/core/static/ios-manifest.json diff --git a/src/ARte/core/static/js/bowser.js b/src/core/static/js/bowser.js similarity index 100% rename from src/ARte/core/static/js/bowser.js rename to src/core/static/js/bowser.js diff --git a/src/ARte/core/static/js/detect.js b/src/core/static/js/detect.js similarity index 100% rename from src/ARte/core/static/js/detect.js rename to src/core/static/js/detect.js diff --git a/src/ARte/core/static/js/fetch-worker.js b/src/core/static/js/fetch-worker.js similarity index 100% rename from src/ARte/core/static/js/fetch-worker.js rename to src/core/static/js/fetch-worker.js diff --git a/src/core/static/js/htmx.min.js b/src/core/static/js/htmx.min.js new file mode 100644 index 00000000..de5f0f1a --- /dev/null +++ b/src/core/static/js/htmx.min.js @@ -0,0 +1 @@ +(function(e,t){if(typeof define==="function"&&define.amd){define([],t)}else if(typeof module==="object"&&module.exports){module.exports=t()}else{e.htmx=e.htmx||t()}})(typeof self!=="undefined"?self:this,function(){return function(){"use strict";var Q={onLoad:F,process:zt,on:de,off:ge,trigger:ce,ajax:Nr,find:C,findAll:f,closest:v,values:function(e,t){var r=dr(e,t||"post");return r.values},remove:_,addClass:z,removeClass:n,toggleClass:$,takeClass:W,defineExtension:Ur,removeExtension:Br,logAll:V,logNone:j,logger:null,config:{historyEnabled:true,historyCacheSize:10,refreshOnHistoryMiss:false,defaultSwapStyle:"innerHTML",defaultSwapDelay:0,defaultSettleDelay:20,includeIndicatorStyles:true,indicatorClass:"htmx-indicator",requestClass:"htmx-request",addedClass:"htmx-added",settlingClass:"htmx-settling",swappingClass:"htmx-swapping",allowEval:true,allowScriptTags:true,inlineScriptNonce:"",attributesToSettle:["class","style","width","height"],withCredentials:false,timeout:0,wsReconnectDelay:"full-jitter",wsBinaryType:"blob",disableSelector:"[hx-disable], [data-hx-disable]",useTemplateFragments:false,scrollBehavior:"smooth",defaultFocusScroll:false,getCacheBusterParam:false,globalViewTransitions:false,methodsThatUseUrlParams:["get"],selfRequestsOnly:false,ignoreTitle:false,scrollIntoViewOnBoost:true,triggerSpecsCache:null},parseInterval:d,_:t,createEventSource:function(e){return new EventSource(e,{withCredentials:true})},createWebSocket:function(e){var t=new WebSocket(e,[]);t.binaryType=Q.config.wsBinaryType;return t},version:"1.9.12"};var r={addTriggerHandler:Lt,bodyContains:se,canAccessLocalStorage:U,findThisElement:xe,filterValues:yr,hasAttribute:o,getAttributeValue:te,getClosestAttributeValue:ne,getClosestMatch:c,getExpressionVars:Hr,getHeaders:xr,getInputValues:dr,getInternalData:ae,getSwapSpecification:wr,getTriggerSpecs:it,getTarget:ye,makeFragment:l,mergeObjects:le,makeSettleInfo:T,oobSwap:Ee,querySelectorExt:ue,selectAndSwap:je,settleImmediately:nr,shouldCancel:ut,triggerEvent:ce,triggerErrorEvent:fe,withExtensions:R};var w=["get","post","put","delete","patch"];var i=w.map(function(e){return"[hx-"+e+"], [data-hx-"+e+"]"}).join(", ");var S=e("head"),q=e("title"),H=e("svg",true);function e(e,t){return new RegExp("<"+e+"(\\s[^>]*>|>)([\\s\\S]*?)<\\/"+e+">",!!t?"gim":"im")}function d(e){if(e==undefined){return undefined}let t=NaN;if(e.slice(-2)=="ms"){t=parseFloat(e.slice(0,-2))}else if(e.slice(-1)=="s"){t=parseFloat(e.slice(0,-1))*1e3}else if(e.slice(-1)=="m"){t=parseFloat(e.slice(0,-1))*1e3*60}else{t=parseFloat(e)}return isNaN(t)?undefined:t}function ee(e,t){return e.getAttribute&&e.getAttribute(t)}function o(e,t){return e.hasAttribute&&(e.hasAttribute(t)||e.hasAttribute("data-"+t))}function te(e,t){return ee(e,t)||ee(e,"data-"+t)}function u(e){return e.parentElement}function re(){return document}function c(e,t){while(e&&!t(e)){e=u(e)}return e?e:null}function L(e,t,r){var n=te(t,r);var i=te(t,"hx-disinherit");if(e!==t&&i&&(i==="*"||i.split(" ").indexOf(r)>=0)){return"unset"}else{return n}}function ne(t,r){var n=null;c(t,function(e){return n=L(t,e,r)});if(n!=="unset"){return n}}function h(e,t){var r=e.matches||e.matchesSelector||e.msMatchesSelector||e.mozMatchesSelector||e.webkitMatchesSelector||e.oMatchesSelector;return r&&r.call(e,t)}function A(e){var t=/<([a-z][^\/\0>\x20\t\r\n\f]*)/i;var r=t.exec(e);if(r){return r[1].toLowerCase()}else{return""}}function s(e,t){var r=new DOMParser;var n=r.parseFromString(e,"text/html");var i=n.body;while(t>0){t--;i=i.firstChild}if(i==null){i=re().createDocumentFragment()}return i}function N(e){return/",0);var a=i.querySelector("template").content;if(Q.config.allowScriptTags){oe(a.querySelectorAll("script"),function(e){if(Q.config.inlineScriptNonce){e.nonce=Q.config.inlineScriptNonce}e.htmxExecuted=navigator.userAgent.indexOf("Firefox")===-1})}else{oe(a.querySelectorAll("script"),function(e){_(e)})}return a}switch(r){case"thead":case"tbody":case"tfoot":case"colgroup":case"caption":return s(""+n+"
",1);case"col":return s(""+n+"
",2);case"tr":return s(""+n+"
",2);case"td":case"th":return s(""+n+"
",3);case"script":case"style":return s("
"+n+"
",1);default:return s(n,0)}}function ie(e){if(e){e()}}function I(e,t){return Object.prototype.toString.call(e)==="[object "+t+"]"}function k(e){return I(e,"Function")}function P(e){return I(e,"Object")}function ae(e){var t="htmx-internal-data";var r=e[t];if(!r){r=e[t]={}}return r}function M(e){var t=[];if(e){for(var r=0;r=0}function se(e){if(e.getRootNode&&e.getRootNode()instanceof window.ShadowRoot){return re().body.contains(e.getRootNode().host)}else{return re().body.contains(e)}}function D(e){return e.trim().split(/\s+/)}function le(e,t){for(var r in t){if(t.hasOwnProperty(r)){e[r]=t[r]}}return e}function E(e){try{return JSON.parse(e)}catch(e){b(e);return null}}function U(){var e="htmx:localStorageTest";try{localStorage.setItem(e,e);localStorage.removeItem(e);return true}catch(e){return false}}function B(t){try{var e=new URL(t);if(e){t=e.pathname+e.search}if(!/^\/$/.test(t)){t=t.replace(/\/+$/,"")}return t}catch(e){return t}}function t(e){return Tr(re().body,function(){return eval(e)})}function F(t){var e=Q.on("htmx:load",function(e){t(e.detail.elt)});return e}function V(){Q.logger=function(e,t,r){if(console){console.log(t,e,r)}}}function j(){Q.logger=null}function C(e,t){if(t){return e.querySelector(t)}else{return C(re(),e)}}function f(e,t){if(t){return e.querySelectorAll(t)}else{return f(re(),e)}}function _(e,t){e=p(e);if(t){setTimeout(function(){_(e);e=null},t)}else{e.parentElement.removeChild(e)}}function z(e,t,r){e=p(e);if(r){setTimeout(function(){z(e,t);e=null},r)}else{e.classList&&e.classList.add(t)}}function n(e,t,r){e=p(e);if(r){setTimeout(function(){n(e,t);e=null},r)}else{if(e.classList){e.classList.remove(t);if(e.classList.length===0){e.removeAttribute("class")}}}}function $(e,t){e=p(e);e.classList.toggle(t)}function W(e,t){e=p(e);oe(e.parentElement.children,function(e){n(e,t)});z(e,t)}function v(e,t){e=p(e);if(e.closest){return e.closest(t)}else{do{if(e==null||h(e,t)){return e}}while(e=e&&u(e));return null}}function g(e,t){return e.substring(0,t.length)===t}function G(e,t){return e.substring(e.length-t.length)===t}function J(e){var t=e.trim();if(g(t,"<")&&G(t,"/>")){return t.substring(1,t.length-2)}else{return t}}function Z(e,t){if(t.indexOf("closest ")===0){return[v(e,J(t.substr(8)))]}else if(t.indexOf("find ")===0){return[C(e,J(t.substr(5)))]}else if(t==="next"){return[e.nextElementSibling]}else if(t.indexOf("next ")===0){return[K(e,J(t.substr(5)))]}else if(t==="previous"){return[e.previousElementSibling]}else if(t.indexOf("previous ")===0){return[Y(e,J(t.substr(9)))]}else if(t==="document"){return[document]}else if(t==="window"){return[window]}else if(t==="body"){return[document.body]}else{return re().querySelectorAll(J(t))}}var K=function(e,t){var r=re().querySelectorAll(t);for(var n=0;n=0;n--){var i=r[n];if(i.compareDocumentPosition(e)===Node.DOCUMENT_POSITION_FOLLOWING){return i}}};function ue(e,t){if(t){return Z(e,t)[0]}else{return Z(re().body,e)[0]}}function p(e){if(I(e,"String")){return C(e)}else{return e}}function ve(e,t,r){if(k(t)){return{target:re().body,event:e,listener:t}}else{return{target:p(e),event:t,listener:r}}}function de(t,r,n){jr(function(){var e=ve(t,r,n);e.target.addEventListener(e.event,e.listener)});var e=k(r);return e?r:n}function ge(t,r,n){jr(function(){var e=ve(t,r,n);e.target.removeEventListener(e.event,e.listener)});return k(r)?r:n}var pe=re().createElement("output");function me(e,t){var r=ne(e,t);if(r){if(r==="this"){return[xe(e,t)]}else{var n=Z(e,r);if(n.length===0){b('The selector "'+r+'" on '+t+" returned no matches!");return[pe]}else{return n}}}}function xe(e,t){return c(e,function(e){return te(e,t)!=null})}function ye(e){var t=ne(e,"hx-target");if(t){if(t==="this"){return xe(e,"hx-target")}else{return ue(e,t)}}else{var r=ae(e);if(r.boosted){return re().body}else{return e}}}function be(e){var t=Q.config.attributesToSettle;for(var r=0;r0){o=e.substr(0,e.indexOf(":"));t=e.substr(e.indexOf(":")+1,e.length)}else{o=e}var r=re().querySelectorAll(t);if(r){oe(r,function(e){var t;var r=i.cloneNode(true);t=re().createDocumentFragment();t.appendChild(r);if(!Se(o,e)){t=r}var n={shouldSwap:true,target:e,fragment:t};if(!ce(e,"htmx:oobBeforeSwap",n))return;e=n.target;if(n["shouldSwap"]){Fe(o,e,e,t,a)}oe(a.elts,function(e){ce(e,"htmx:oobAfterSwap",n)})});i.parentNode.removeChild(i)}else{i.parentNode.removeChild(i);fe(re().body,"htmx:oobErrorNoTarget",{content:i})}return e}function Ce(e,t,r){var n=ne(e,"hx-select-oob");if(n){var i=n.split(",");for(var a=0;a0){var r=t.replace("'","\\'");var n=e.tagName.replace(":","\\:");var i=o.querySelector(n+"[id='"+r+"']");if(i&&i!==o){var a=e.cloneNode();we(e,i);s.tasks.push(function(){we(e,a)})}}})}function Oe(e){return function(){n(e,Q.config.addedClass);zt(e);Nt(e);qe(e);ce(e,"htmx:load")}}function qe(e){var t="[autofocus]";var r=h(e,t)?e:e.querySelector(t);if(r!=null){r.focus()}}function a(e,t,r,n){Te(e,r,n);while(r.childNodes.length>0){var i=r.firstChild;z(i,Q.config.addedClass);e.insertBefore(i,t);if(i.nodeType!==Node.TEXT_NODE&&i.nodeType!==Node.COMMENT_NODE){n.tasks.push(Oe(i))}}}function He(e,t){var r=0;while(r-1){var t=e.replace(H,"");var r=t.match(q);if(r){return r[2]}}}function je(e,t,r,n,i,a){i.title=Ve(n);var o=l(n);if(o){Ce(r,o,i);o=Be(r,o,a);Re(o);return Fe(e,r,t,o,i)}}function _e(e,t,r){var n=e.getResponseHeader(t);if(n.indexOf("{")===0){var i=E(n);for(var a in i){if(i.hasOwnProperty(a)){var o=i[a];if(!P(o)){o={value:o}}ce(r,a,o)}}}else{var s=n.split(",");for(var l=0;l0){var o=t[0];if(o==="]"){n--;if(n===0){if(a===null){i=i+"true"}t.shift();i+=")})";try{var s=Tr(e,function(){return Function(i)()},function(){return true});s.source=i;return s}catch(e){fe(re().body,"htmx:syntax:error",{error:e,source:i});return null}}}else if(o==="["){n++}if(Qe(o,a,r)){i+="(("+r+"."+o+") ? ("+r+"."+o+") : (window."+o+"))"}else{i=i+o}a=t.shift()}}}function y(e,t){var r="";while(e.length>0&&!t.test(e[0])){r+=e.shift()}return r}function tt(e){var t;if(e.length>0&&Ze.test(e[0])){e.shift();t=y(e,Ke).trim();e.shift()}else{t=y(e,x)}return t}var rt="input, textarea, select";function nt(e,t,r){var n=[];var i=Ye(t);do{y(i,Je);var a=i.length;var o=y(i,/[,\[\s]/);if(o!==""){if(o==="every"){var s={trigger:"every"};y(i,Je);s.pollInterval=d(y(i,/[,\[\s]/));y(i,Je);var l=et(e,i,"event");if(l){s.eventFilter=l}n.push(s)}else if(o.indexOf("sse:")===0){n.push({trigger:"sse",sseEvent:o.substr(4)})}else{var u={trigger:o};var l=et(e,i,"event");if(l){u.eventFilter=l}while(i.length>0&&i[0]!==","){y(i,Je);var f=i.shift();if(f==="changed"){u.changed=true}else if(f==="once"){u.once=true}else if(f==="consume"){u.consume=true}else if(f==="delay"&&i[0]===":"){i.shift();u.delay=d(y(i,x))}else if(f==="from"&&i[0]===":"){i.shift();if(Ze.test(i[0])){var c=tt(i)}else{var c=y(i,x);if(c==="closest"||c==="find"||c==="next"||c==="previous"){i.shift();var h=tt(i);if(h.length>0){c+=" "+h}}}u.from=c}else if(f==="target"&&i[0]===":"){i.shift();u.target=tt(i)}else if(f==="throttle"&&i[0]===":"){i.shift();u.throttle=d(y(i,x))}else if(f==="queue"&&i[0]===":"){i.shift();u.queue=y(i,x)}else if(f==="root"&&i[0]===":"){i.shift();u[f]=tt(i)}else if(f==="threshold"&&i[0]===":"){i.shift();u[f]=y(i,x)}else{fe(e,"htmx:syntax:error",{token:i.shift()})}}n.push(u)}}if(i.length===a){fe(e,"htmx:syntax:error",{token:i.shift()})}y(i,Je)}while(i[0]===","&&i.shift());if(r){r[t]=n}return n}function it(e){var t=te(e,"hx-trigger");var r=[];if(t){var n=Q.config.triggerSpecsCache;r=n&&n[t]||nt(e,t,n)}if(r.length>0){return r}else if(h(e,"form")){return[{trigger:"submit"}]}else if(h(e,'input[type="button"], input[type="submit"]')){return[{trigger:"click"}]}else if(h(e,rt)){return[{trigger:"change"}]}else{return[{trigger:"click"}]}}function at(e){ae(e).cancelled=true}function ot(e,t,r){var n=ae(e);n.timeout=setTimeout(function(){if(se(e)&&n.cancelled!==true){if(!ct(r,e,Wt("hx:poll:trigger",{triggerSpec:r,target:e}))){t(e)}ot(e,t,r)}},r.pollInterval)}function st(e){return location.hostname===e.hostname&&ee(e,"href")&&ee(e,"href").indexOf("#")!==0}function lt(t,r,e){if(t.tagName==="A"&&st(t)&&(t.target===""||t.target==="_self")||t.tagName==="FORM"){r.boosted=true;var n,i;if(t.tagName==="A"){n="get";i=ee(t,"href")}else{var a=ee(t,"method");n=a?a.toLowerCase():"get";if(n==="get"){}i=ee(t,"action")}e.forEach(function(e){ht(t,function(e,t){if(v(e,Q.config.disableSelector)){m(e);return}he(n,i,e,t)},r,e,true)})}}function ut(e,t){if(e.type==="submit"||e.type==="click"){if(t.tagName==="FORM"){return true}if(h(t,'input[type="submit"], button')&&v(t,"form")!==null){return true}if(t.tagName==="A"&&t.href&&(t.getAttribute("href")==="#"||t.getAttribute("href").indexOf("#")!==0)){return true}}return false}function ft(e,t){return ae(e).boosted&&e.tagName==="A"&&t.type==="click"&&(t.ctrlKey||t.metaKey)}function ct(e,t,r){var n=e.eventFilter;if(n){try{return n.call(t,r)!==true}catch(e){fe(re().body,"htmx:eventFilter:error",{error:e,source:n.source});return true}}return false}function ht(a,o,e,s,l){var u=ae(a);var t;if(s.from){t=Z(a,s.from)}else{t=[a]}if(s.changed){t.forEach(function(e){var t=ae(e);t.lastValue=e.value})}oe(t,function(n){var i=function(e){if(!se(a)){n.removeEventListener(s.trigger,i);return}if(ft(a,e)){return}if(l||ut(e,a)){e.preventDefault()}if(ct(s,a,e)){return}var t=ae(e);t.triggerSpec=s;if(t.handledFor==null){t.handledFor=[]}if(t.handledFor.indexOf(a)<0){t.handledFor.push(a);if(s.consume){e.stopPropagation()}if(s.target&&e.target){if(!h(e.target,s.target)){return}}if(s.once){if(u.triggeredOnce){return}else{u.triggeredOnce=true}}if(s.changed){var r=ae(n);if(r.lastValue===n.value){return}r.lastValue=n.value}if(u.delayed){clearTimeout(u.delayed)}if(u.throttle){return}if(s.throttle>0){if(!u.throttle){o(a,e);u.throttle=setTimeout(function(){u.throttle=null},s.throttle)}}else if(s.delay>0){u.delayed=setTimeout(function(){o(a,e)},s.delay)}else{ce(a,"htmx:trigger");o(a,e)}}};if(e.listenerInfos==null){e.listenerInfos=[]}e.listenerInfos.push({trigger:s.trigger,listener:i,on:n});n.addEventListener(s.trigger,i)})}var vt=false;var dt=null;function gt(){if(!dt){dt=function(){vt=true};window.addEventListener("scroll",dt);setInterval(function(){if(vt){vt=false;oe(re().querySelectorAll("[hx-trigger='revealed'],[data-hx-trigger='revealed']"),function(e){pt(e)})}},200)}}function pt(t){if(!o(t,"data-hx-revealed")&&X(t)){t.setAttribute("data-hx-revealed","true");var e=ae(t);if(e.initHash){ce(t,"revealed")}else{t.addEventListener("htmx:afterProcessNode",function(e){ce(t,"revealed")},{once:true})}}}function mt(e,t,r){var n=D(r);for(var i=0;i=0){var t=wt(n);setTimeout(function(){xt(s,r,n+1)},t)}};t.onopen=function(e){n=0};ae(s).webSocket=t;t.addEventListener("message",function(e){if(yt(s)){return}var t=e.data;R(s,function(e){t=e.transformResponse(t,null,s)});var r=T(s);var n=l(t);var i=M(n.children);for(var a=0;a0){ce(u,"htmx:validation:halted",i);return}t.send(JSON.stringify(l));if(ut(e,u)){e.preventDefault()}})}else{fe(u,"htmx:noWebSocketSourceError")}}function wt(e){var t=Q.config.wsReconnectDelay;if(typeof t==="function"){return t(e)}if(t==="full-jitter"){var r=Math.min(e,6);var n=1e3*Math.pow(2,r);return n*Math.random()}b('htmx.config.wsReconnectDelay must either be a function or the string "full-jitter"')}function St(e,t,r){var n=D(r);for(var i=0;i0){setTimeout(i,n)}else{i()}}function Ht(t,i,e){var a=false;oe(w,function(r){if(o(t,"hx-"+r)){var n=te(t,"hx-"+r);a=true;i.path=n;i.verb=r;e.forEach(function(e){Lt(t,e,i,function(e,t){if(v(e,Q.config.disableSelector)){m(e);return}he(r,n,e,t)})})}});return a}function Lt(n,e,t,r){if(e.sseEvent){Rt(n,r,e.sseEvent)}else if(e.trigger==="revealed"){gt();ht(n,r,t,e);pt(n)}else if(e.trigger==="intersect"){var i={};if(e.root){i.root=ue(n,e.root)}if(e.threshold){i.threshold=parseFloat(e.threshold)}var a=new IntersectionObserver(function(e){for(var t=0;t0){t.polling=true;ot(n,r,e)}else{ht(n,r,t,e)}}function At(e){if(!e.htmxExecuted&&Q.config.allowScriptTags&&(e.type==="text/javascript"||e.type==="module"||e.type==="")){var t=re().createElement("script");oe(e.attributes,function(e){t.setAttribute(e.name,e.value)});t.textContent=e.textContent;t.async=false;if(Q.config.inlineScriptNonce){t.nonce=Q.config.inlineScriptNonce}var r=e.parentElement;try{r.insertBefore(t,e)}catch(e){b(e)}finally{if(e.parentElement){e.parentElement.removeChild(e)}}}}function Nt(e){if(h(e,"script")){At(e)}oe(f(e,"script"),function(e){At(e)})}function It(e){var t=e.attributes;if(!t){return false}for(var r=0;r0){var o=n.shift();var s=o.match(/^\s*([a-zA-Z:\-\.]+:)(.*)/);if(a===0&&s){o.split(":");i=s[1].slice(0,-1);r[i]=s[2]}else{r[i]+=o}a+=Bt(o)}for(var l in r){Ft(e,l,r[l])}}}function jt(e){Ae(e);for(var t=0;tQ.config.historyCacheSize){i.shift()}while(i.length>0){try{localStorage.setItem("htmx-history-cache",JSON.stringify(i));break}catch(e){fe(re().body,"htmx:historyCacheError",{cause:e,cache:i});i.shift()}}}function Yt(e){if(!U()){return null}e=B(e);var t=E(localStorage.getItem("htmx-history-cache"))||[];for(var r=0;r=200&&this.status<400){ce(re().body,"htmx:historyCacheMissLoad",o);var e=l(this.response);e=e.querySelector("[hx-history-elt],[data-hx-history-elt]")||e;var t=Zt();var r=T(t);var n=Ve(this.response);if(n){var i=C("title");if(i){i.innerHTML=n}else{window.document.title=n}}Ue(t,e,r);nr(r.tasks);Jt=a;ce(re().body,"htmx:historyRestore",{path:a,cacheMiss:true,serverResponse:this.response})}else{fe(re().body,"htmx:historyCacheMissLoadError",o)}};e.send()}function ar(e){er();e=e||location.pathname+location.search;var t=Yt(e);if(t){var r=l(t.content);var n=Zt();var i=T(n);Ue(n,r,i);nr(i.tasks);document.title=t.title;setTimeout(function(){window.scrollTo(0,t.scroll)},0);Jt=e;ce(re().body,"htmx:historyRestore",{path:e,item:t})}else{if(Q.config.refreshOnHistoryMiss){window.location.reload(true)}else{ir(e)}}}function or(e){var t=me(e,"hx-indicator");if(t==null){t=[e]}oe(t,function(e){var t=ae(e);t.requestCount=(t.requestCount||0)+1;e.classList["add"].call(e.classList,Q.config.requestClass)});return t}function sr(e){var t=me(e,"hx-disabled-elt");if(t==null){t=[]}oe(t,function(e){var t=ae(e);t.requestCount=(t.requestCount||0)+1;e.setAttribute("disabled","")});return t}function lr(e,t){oe(e,function(e){var t=ae(e);t.requestCount=(t.requestCount||0)-1;if(t.requestCount===0){e.classList["remove"].call(e.classList,Q.config.requestClass)}});oe(t,function(e){var t=ae(e);t.requestCount=(t.requestCount||0)-1;if(t.requestCount===0){e.removeAttribute("disabled")}})}function ur(e,t){for(var r=0;r=0}function wr(e,t){var r=t?t:ne(e,"hx-swap");var n={swapStyle:ae(e).boosted?"innerHTML":Q.config.defaultSwapStyle,swapDelay:Q.config.defaultSwapDelay,settleDelay:Q.config.defaultSettleDelay};if(Q.config.scrollIntoViewOnBoost&&ae(e).boosted&&!br(e)){n["show"]="top"}if(r){var i=D(r);if(i.length>0){for(var a=0;a0?l.join(":"):null;n["scroll"]=u;n["scrollTarget"]=f}else if(o.indexOf("show:")===0){var c=o.substr(5);var l=c.split(":");var h=l.pop();var f=l.length>0?l.join(":"):null;n["show"]=h;n["showTarget"]=f}else if(o.indexOf("focus-scroll:")===0){var v=o.substr("focus-scroll:".length);n["focusScroll"]=v=="true"}else if(a==0){n["swapStyle"]=o}else{b("Unknown modifier in hx-swap: "+o)}}}}return n}function Sr(e){return ne(e,"hx-encoding")==="multipart/form-data"||h(e,"form")&&ee(e,"enctype")==="multipart/form-data"}function Er(t,r,n){var i=null;R(r,function(e){if(i==null){i=e.encodeParameters(t,n,r)}});if(i!=null){return i}else{if(Sr(r)){return mr(n)}else{return pr(n)}}}function T(e){return{tasks:[],elts:[e]}}function Cr(e,t){var r=e[0];var n=e[e.length-1];if(t.scroll){var i=null;if(t.scrollTarget){i=ue(r,t.scrollTarget)}if(t.scroll==="top"&&(r||i)){i=i||r;i.scrollTop=0}if(t.scroll==="bottom"&&(n||i)){i=i||n;i.scrollTop=i.scrollHeight}}if(t.show){var i=null;if(t.showTarget){var a=t.showTarget;if(t.showTarget==="window"){a="body"}i=ue(r,a)}if(t.show==="top"&&(r||i)){i=i||r;i.scrollIntoView({block:"start",behavior:Q.config.scrollBehavior})}if(t.show==="bottom"&&(n||i)){i=i||n;i.scrollIntoView({block:"end",behavior:Q.config.scrollBehavior})}}}function Rr(e,t,r,n){if(n==null){n={}}if(e==null){return n}var i=te(e,t);if(i){var a=i.trim();var o=r;if(a==="unset"){return null}if(a.indexOf("javascript:")===0){a=a.substr(11);o=true}else if(a.indexOf("js:")===0){a=a.substr(3);o=true}if(a.indexOf("{")!==0){a="{"+a+"}"}var s;if(o){s=Tr(e,function(){return Function("return ("+a+")")()},{})}else{s=E(a)}for(var l in s){if(s.hasOwnProperty(l)){if(n[l]==null){n[l]=s[l]}}}}return Rr(u(e),t,r,n)}function Tr(e,t,r){if(Q.config.allowEval){return t()}else{fe(e,"htmx:evalDisallowedError");return r}}function Or(e,t){return Rr(e,"hx-vars",true,t)}function qr(e,t){return Rr(e,"hx-vals",false,t)}function Hr(e){return le(Or(e),qr(e))}function Lr(t,r,n){if(n!==null){try{t.setRequestHeader(r,n)}catch(e){t.setRequestHeader(r,encodeURIComponent(n));t.setRequestHeader(r+"-URI-AutoEncoded","true")}}}function Ar(t){if(t.responseURL&&typeof URL!=="undefined"){try{var e=new URL(t.responseURL);return e.pathname+e.search}catch(e){fe(re().body,"htmx:badResponseUrl",{url:t.responseURL})}}}function O(e,t){return t.test(e.getAllResponseHeaders())}function Nr(e,t,r){e=e.toLowerCase();if(r){if(r instanceof Element||I(r,"String")){return he(e,t,null,null,{targetOverride:p(r),returnPromise:true})}else{return he(e,t,p(r.source),r.event,{handler:r.handler,headers:r.headers,values:r.values,targetOverride:p(r.target),swapOverride:r.swap,select:r.select,returnPromise:true})}}else{return he(e,t,null,null,{returnPromise:true})}}function Ir(e){var t=[];while(e){t.push(e);e=e.parentElement}return t}function kr(e,t,r){var n;var i;if(typeof URL==="function"){i=new URL(t,document.location.href);var a=document.location.origin;n=a===i.origin}else{i=t;n=g(t,document.location.origin)}if(Q.config.selfRequestsOnly){if(!n){return false}}return ce(e,"htmx:validateUrl",le({url:i,sameHost:n},r))}function he(t,r,n,i,a,e){var o=null;var s=null;a=a!=null?a:{};if(a.returnPromise&&typeof Promise!=="undefined"){var l=new Promise(function(e,t){o=e;s=t})}if(n==null){n=re().body}var M=a.handler||Mr;var X=a.select||null;if(!se(n)){ie(o);return l}var u=a.targetOverride||ye(n);if(u==null||u==pe){fe(n,"htmx:targetError",{target:te(n,"hx-target")});ie(s);return l}var f=ae(n);var c=f.lastButtonClicked;if(c){var h=ee(c,"formaction");if(h!=null){r=h}var v=ee(c,"formmethod");if(v!=null){if(v.toLowerCase()!=="dialog"){t=v}}}var d=ne(n,"hx-confirm");if(e===undefined){var D=function(e){return he(t,r,n,i,a,!!e)};var U={target:u,elt:n,path:r,verb:t,triggeringEvent:i,etc:a,issueRequest:D,question:d};if(ce(n,"htmx:confirm",U)===false){ie(o);return l}}var g=n;var p=ne(n,"hx-sync");var m=null;var x=false;if(p){var B=p.split(":");var F=B[0].trim();if(F==="this"){g=xe(n,"hx-sync")}else{g=ue(n,F)}p=(B[1]||"drop").trim();f=ae(g);if(p==="drop"&&f.xhr&&f.abortable!==true){ie(o);return l}else if(p==="abort"){if(f.xhr){ie(o);return l}else{x=true}}else if(p==="replace"){ce(g,"htmx:abort")}else if(p.indexOf("queue")===0){var V=p.split(" ");m=(V[1]||"last").trim()}}if(f.xhr){if(f.abortable){ce(g,"htmx:abort")}else{if(m==null){if(i){var y=ae(i);if(y&&y.triggerSpec&&y.triggerSpec.queue){m=y.triggerSpec.queue}}if(m==null){m="last"}}if(f.queuedRequests==null){f.queuedRequests=[]}if(m==="first"&&f.queuedRequests.length===0){f.queuedRequests.push(function(){he(t,r,n,i,a)})}else if(m==="all"){f.queuedRequests.push(function(){he(t,r,n,i,a)})}else if(m==="last"){f.queuedRequests=[];f.queuedRequests.push(function(){he(t,r,n,i,a)})}ie(o);return l}}var b=new XMLHttpRequest;f.xhr=b;f.abortable=x;var w=function(){f.xhr=null;f.abortable=false;if(f.queuedRequests!=null&&f.queuedRequests.length>0){var e=f.queuedRequests.shift();e()}};var j=ne(n,"hx-prompt");if(j){var S=prompt(j);if(S===null||!ce(n,"htmx:prompt",{prompt:S,target:u})){ie(o);w();return l}}if(d&&!e){if(!confirm(d)){ie(o);w();return l}}var E=xr(n,u,S);if(t!=="get"&&!Sr(n)){E["Content-Type"]="application/x-www-form-urlencoded"}if(a.headers){E=le(E,a.headers)}var _=dr(n,t);var C=_.errors;var R=_.values;if(a.values){R=le(R,a.values)}var z=Hr(n);var $=le(R,z);var T=yr($,n);if(Q.config.getCacheBusterParam&&t==="get"){T["org.htmx.cache-buster"]=ee(u,"id")||"true"}if(r==null||r===""){r=re().location.href}var O=Rr(n,"hx-request");var W=ae(n).boosted;var q=Q.config.methodsThatUseUrlParams.indexOf(t)>=0;var H={boosted:W,useUrlParams:q,parameters:T,unfilteredParameters:$,headers:E,target:u,verb:t,errors:C,withCredentials:a.credentials||O.credentials||Q.config.withCredentials,timeout:a.timeout||O.timeout||Q.config.timeout,path:r,triggeringEvent:i};if(!ce(n,"htmx:configRequest",H)){ie(o);w();return l}r=H.path;t=H.verb;E=H.headers;T=H.parameters;C=H.errors;q=H.useUrlParams;if(C&&C.length>0){ce(n,"htmx:validation:halted",H);ie(o);w();return l}var G=r.split("#");var J=G[0];var L=G[1];var A=r;if(q){A=J;var Z=Object.keys(T).length!==0;if(Z){if(A.indexOf("?")<0){A+="?"}else{A+="&"}A+=pr(T);if(L){A+="#"+L}}}if(!kr(n,A,H)){fe(n,"htmx:invalidPath",H);ie(s);return l}b.open(t.toUpperCase(),A,true);b.overrideMimeType("text/html");b.withCredentials=H.withCredentials;b.timeout=H.timeout;if(O.noHeaders){}else{for(var N in E){if(E.hasOwnProperty(N)){var K=E[N];Lr(b,N,K)}}}var I={xhr:b,target:u,requestConfig:H,etc:a,boosted:W,select:X,pathInfo:{requestPath:r,finalRequestPath:A,anchor:L}};b.onload=function(){try{var e=Ir(n);I.pathInfo.responsePath=Ar(b);M(n,I);lr(k,P);ce(n,"htmx:afterRequest",I);ce(n,"htmx:afterOnLoad",I);if(!se(n)){var t=null;while(e.length>0&&t==null){var r=e.shift();if(se(r)){t=r}}if(t){ce(t,"htmx:afterRequest",I);ce(t,"htmx:afterOnLoad",I)}}ie(o);w()}catch(e){fe(n,"htmx:onLoadError",le({error:e},I));throw e}};b.onerror=function(){lr(k,P);fe(n,"htmx:afterRequest",I);fe(n,"htmx:sendError",I);ie(s);w()};b.onabort=function(){lr(k,P);fe(n,"htmx:afterRequest",I);fe(n,"htmx:sendAbort",I);ie(s);w()};b.ontimeout=function(){lr(k,P);fe(n,"htmx:afterRequest",I);fe(n,"htmx:timeout",I);ie(s);w()};if(!ce(n,"htmx:beforeRequest",I)){ie(o);w();return l}var k=or(n);var P=sr(n);oe(["loadstart","loadend","progress","abort"],function(t){oe([b,b.upload],function(e){e.addEventListener(t,function(e){ce(n,"htmx:xhr:"+t,{lengthComputable:e.lengthComputable,loaded:e.loaded,total:e.total})})})});ce(n,"htmx:beforeSend",I);var Y=q?null:Er(b,n,T);b.send(Y);return l}function Pr(e,t){var r=t.xhr;var n=null;var i=null;if(O(r,/HX-Push:/i)){n=r.getResponseHeader("HX-Push");i="push"}else if(O(r,/HX-Push-Url:/i)){n=r.getResponseHeader("HX-Push-Url");i="push"}else if(O(r,/HX-Replace-Url:/i)){n=r.getResponseHeader("HX-Replace-Url");i="replace"}if(n){if(n==="false"){return{}}else{return{type:i,path:n}}}var a=t.pathInfo.finalRequestPath;var o=t.pathInfo.responsePath;var s=ne(e,"hx-push-url");var l=ne(e,"hx-replace-url");var u=ae(e).boosted;var f=null;var c=null;if(s){f="push";c=s}else if(l){f="replace";c=l}else if(u){f="push";c=o||a}if(c){if(c==="false"){return{}}if(c==="true"){c=o||a}if(t.pathInfo.anchor&&c.indexOf("#")===-1){c=c+"#"+t.pathInfo.anchor}return{type:f,path:c}}else{return{}}}function Mr(l,u){var f=u.xhr;var c=u.target;var e=u.etc;var t=u.requestConfig;var h=u.select;if(!ce(l,"htmx:beforeOnLoad",u))return;if(O(f,/HX-Trigger:/i)){_e(f,"HX-Trigger",l)}if(O(f,/HX-Location:/i)){er();var r=f.getResponseHeader("HX-Location");var v;if(r.indexOf("{")===0){v=E(r);r=v["path"];delete v["path"]}Nr("GET",r,v).then(function(){tr(r)});return}var n=O(f,/HX-Refresh:/i)&&"true"===f.getResponseHeader("HX-Refresh");if(O(f,/HX-Redirect:/i)){location.href=f.getResponseHeader("HX-Redirect");n&&location.reload();return}if(n){location.reload();return}if(O(f,/HX-Retarget:/i)){if(f.getResponseHeader("HX-Retarget")==="this"){u.target=l}else{u.target=ue(l,f.getResponseHeader("HX-Retarget"))}}var d=Pr(l,u);var i=f.status>=200&&f.status<400&&f.status!==204;var g=f.response;var a=f.status>=400;var p=Q.config.ignoreTitle;var o=le({shouldSwap:i,serverResponse:g,isError:a,ignoreTitle:p},u);if(!ce(c,"htmx:beforeSwap",o))return;c=o.target;g=o.serverResponse;a=o.isError;p=o.ignoreTitle;u.target=c;u.failed=a;u.successful=!a;if(o.shouldSwap){if(f.status===286){at(l)}R(l,function(e){g=e.transformResponse(g,f,l)});if(d.type){er()}var s=e.swapOverride;if(O(f,/HX-Reswap:/i)){s=f.getResponseHeader("HX-Reswap")}var v=wr(l,s);if(v.hasOwnProperty("ignoreTitle")){p=v.ignoreTitle}c.classList.add(Q.config.swappingClass);var m=null;var x=null;var y=function(){try{var e=document.activeElement;var t={};try{t={elt:e,start:e?e.selectionStart:null,end:e?e.selectionEnd:null}}catch(e){}var r;if(h){r=h}if(O(f,/HX-Reselect:/i)){r=f.getResponseHeader("HX-Reselect")}if(d.type){ce(re().body,"htmx:beforeHistoryUpdate",le({history:d},u));if(d.type==="push"){tr(d.path);ce(re().body,"htmx:pushedIntoHistory",{path:d.path})}else{rr(d.path);ce(re().body,"htmx:replacedInHistory",{path:d.path})}}var n=T(c);je(v.swapStyle,c,l,g,n,r);if(t.elt&&!se(t.elt)&&ee(t.elt,"id")){var i=document.getElementById(ee(t.elt,"id"));var a={preventScroll:v.focusScroll!==undefined?!v.focusScroll:!Q.config.defaultFocusScroll};if(i){if(t.start&&i.setSelectionRange){try{i.setSelectionRange(t.start,t.end)}catch(e){}}i.focus(a)}}c.classList.remove(Q.config.swappingClass);oe(n.elts,function(e){if(e.classList){e.classList.add(Q.config.settlingClass)}ce(e,"htmx:afterSwap",u)});if(O(f,/HX-Trigger-After-Swap:/i)){var o=l;if(!se(l)){o=re().body}_e(f,"HX-Trigger-After-Swap",o)}var s=function(){oe(n.tasks,function(e){e.call()});oe(n.elts,function(e){if(e.classList){e.classList.remove(Q.config.settlingClass)}ce(e,"htmx:afterSettle",u)});if(u.pathInfo.anchor){var e=re().getElementById(u.pathInfo.anchor);if(e){e.scrollIntoView({block:"start",behavior:"auto"})}}if(n.title&&!p){var t=C("title");if(t){t.innerHTML=n.title}else{window.document.title=n.title}}Cr(n.elts,v);if(O(f,/HX-Trigger-After-Settle:/i)){var r=l;if(!se(l)){r=re().body}_e(f,"HX-Trigger-After-Settle",r)}ie(m)};if(v.settleDelay>0){setTimeout(s,v.settleDelay)}else{s()}}catch(e){fe(l,"htmx:swapError",u);ie(x);throw e}};var b=Q.config.globalViewTransitions;if(v.hasOwnProperty("transition")){b=v.transition}if(b&&ce(l,"htmx:beforeTransition",u)&&typeof Promise!=="undefined"&&document.startViewTransition){var w=new Promise(function(e,t){m=e;x=t});var S=y;y=function(){document.startViewTransition(function(){S();return w})}}if(v.swapDelay>0){setTimeout(y,v.swapDelay)}else{y()}}if(a){fe(l,"htmx:responseError",le({error:"Response Status Error Code "+f.status+" from "+u.pathInfo.requestPath},u))}}var Xr={};function Dr(){return{init:function(e){return null},onEvent:function(e,t){return true},transformResponse:function(e,t,r){return e},isInlineSwap:function(e){return false},handleSwap:function(e,t,r,n){return false},encodeParameters:function(e,t,r){return null}}}function Ur(e,t){if(t.init){t.init(r)}Xr[e]=le(Dr(),t)}function Br(e){delete Xr[e]}function Fr(e,r,n){if(e==undefined){return r}if(r==undefined){r=[]}if(n==undefined){n=[]}var t=te(e,"hx-ext");if(t){oe(t.split(","),function(e){e=e.replace(/ /g,"");if(e.slice(0,7)=="ignore:"){n.push(e.slice(7));return}if(n.indexOf(e)<0){var t=Xr[e];if(t&&r.indexOf(t)<0){r.push(t)}}})}return Fr(u(e),r,n)}var Vr=false;re().addEventListener("DOMContentLoaded",function(){Vr=true});function jr(e){if(Vr||re().readyState==="complete"){e()}else{re().addEventListener("DOMContentLoaded",e)}}function _r(){if(Q.config.includeIndicatorStyles!==false){re().head.insertAdjacentHTML("beforeend","")}}function zr(){var e=re().querySelector('meta[name="htmx-config"]');if(e){return E(e.content)}else{return null}}function $r(){var e=zr();if(e){Q.config=le(Q.config,e)}}jr(function(){$r();_r();var e=re().body;zt(e);var t=re().querySelectorAll("[hx-trigger='restored'],[data-hx-trigger='restored']");e.addEventListener("htmx:abort",function(e){var t=e.target;var r=ae(t);if(r&&r.xhr){r.xhr.abort()}});const r=window.onpopstate?window.onpopstate.bind(window):null;window.onpopstate=function(e){if(e.state&&e.state.htmx){ar();oe(t,function(e){ce(e,"htmx:restored",{document:re(),triggerEvent:ce})})}else{if(r){r(e)}}};setTimeout(function(){ce(e,"htmx:load",{});e=null},0)});return Q}()}); \ No newline at end of file diff --git a/src/ARte/core/static/js/main.js b/src/core/static/js/main.js similarity index 100% rename from src/ARte/core/static/js/main.js rename to src/core/static/js/main.js diff --git a/src/ARte/core/static/js/multi-marker.js b/src/core/static/js/multi-marker.js similarity index 100% rename from src/ARte/core/static/js/multi-marker.js rename to src/core/static/js/multi-marker.js diff --git a/src/ARte/core/static/js/pdfGenerator.js b/src/core/static/js/pdfGenerator.js similarity index 100% rename from src/ARte/core/static/js/pdfGenerator.js rename to src/core/static/js/pdfGenerator.js diff --git a/src/ARte/users/media/markers/.gitkeep b/src/core/static/js/rotationLock.js similarity index 100% rename from src/ARte/users/media/markers/.gitkeep rename to src/core/static/js/rotationLock.js diff --git a/src/ARte/core/static/js/takePhoto.js b/src/core/static/js/takePhoto.js similarity index 100% rename from src/ARte/core/static/js/takePhoto.js rename to src/core/static/js/takePhoto.js diff --git a/src/ARte/core/static/js/threex-arpatternfile.js b/src/core/static/js/threex-arpatternfile.js similarity index 100% rename from src/ARte/core/static/js/threex-arpatternfile.js rename to src/core/static/js/threex-arpatternfile.js diff --git a/src/ARte/core/static/three.min.js b/src/core/static/three.min.js similarity index 100% rename from src/ARte/core/static/three.min.js rename to src/core/static/three.min.js diff --git a/src/ARte/users/media/objects/.gitkeep b/src/core/tests/__init__.py similarity index 100% rename from src/ARte/users/media/objects/.gitkeep rename to src/core/tests/__init__.py diff --git a/src/core/tests/test_artworks_api.py b/src/core/tests/test_artworks_api.py new file mode 100644 index 00000000..f1df0650 --- /dev/null +++ b/src/core/tests/test_artworks_api.py @@ -0,0 +1,51 @@ +"""Test using the artwork API for Jandig Artwork""" + +from django.conf import settings +from django.core.files.uploadedfile import SimpleUploadedFile +from django.test import TestCase + +from core.models import Artwork, Marker, Object +from users.models import User + +fake_file = SimpleUploadedFile("fake_file.png", b"these are the file contents!") + + +class TestArtworkAPI(TestCase): + def setUp(self): + self.user = User.objects.create() + self.profile = self.user.profile + + def test_url(self): + response = self.client.get("/api/v1/artworks/") + self.assertEqual(response.status_code, 200) + data = response.json() + self.assertEqual(data["count"], 0) + self.assertEqual(data["next"], None) + self.assertEqual(data["previous"], None) + self.assertEqual(data["results"], []) + + def test_api_artworks_lists_one_artwork(self): + marker = Marker.objects.create(owner=self.profile, source=fake_file) + obj = Object.objects.create(owner=self.profile, source=fake_file) + artwork = Artwork.objects.create(author=self.profile, augmented=obj, marker=marker) + self.assertEqual(artwork.author, self.profile) + response = self.client.get("/api/v1/artworks/") + self.assertEqual(response.status_code, 200) + + def test_api_artwork_lists_multiple_artworks(self): + for _ in range(0, settings.PAGE_SIZE + 1): + marker = Marker.objects.create(owner=self.profile, source=fake_file) + obj = Object.objects.create(owner=self.profile, source=fake_file) + Artwork.objects.create(author=self.profile, augmented=obj, marker=marker) + + response = self.client.get("/api/v1/artworks/") + self.assertEqual(response.status_code, 200) + + def test_retrieve_artwork(self): + marker = Marker.objects.create(owner=self.profile, source=fake_file) + obj = Object.objects.create(owner=self.profile, source=fake_file) + artwork = Artwork.objects.create(author=self.profile, augmented=obj, marker=marker) # noqa F841 + self.assertEqual(artwork.author, self.profile) + + response = self.client.get("/api/v1/artworks/1/") + self.assertEqual(response.status_code, 200) diff --git a/src/core/tests/test_exhibits_api.py b/src/core/tests/test_exhibits_api.py new file mode 100644 index 00000000..66365b98 --- /dev/null +++ b/src/core/tests/test_exhibits_api.py @@ -0,0 +1,52 @@ +"""Test using the exhibit API for Jandig Exhibit""" + +from django.conf import settings +from django.core.files.uploadedfile import SimpleUploadedFile +from django.test import TestCase + +from core.models import Artwork, Exhibit, Marker, Object +from users.models import User + +fake_file = SimpleUploadedFile("fake_file.png", b"these are the file contents!") + + +class TestExhibitAPI(TestCase): + def setUp(self): + self.user = User.objects.create() + self.profile = self.user.profile + + def test_url(self): + response = self.client.get("/api/v1/exhibits/") + self.assertEqual(response.status_code, 200) + data = response.json() + self.assertEqual(data["count"], 0) + self.assertEqual(data["next"], None) + self.assertEqual(data["previous"], None) + self.assertEqual(data["results"], []) + + def test_api_exhibits_lists_one_exhibit(self): + marker = Marker.objects.create(owner=self.profile, source=fake_file) + obj = Object.objects.create(owner=self.profile, source=fake_file) + artwork = Artwork.objects.create(author=self.profile, augmented=obj, marker=marker) + exhibit = Exhibit.objects.create(owner=self.profile, name="test") + exhibit.artworks.add(artwork) + response = self.client.get("/api/v1/artworks/") + self.assertEqual(response.status_code, 200) + + def test_api_exhibit_lists_multiple_exhibits(self): + for i in range(0, settings.PAGE_SIZE + 1): + marker = Marker.objects.create(owner=self.profile, source=fake_file) + obj = Object.objects.create(owner=self.profile, source=fake_file) + artwork = Artwork.objects.create(author=self.profile, augmented=obj, marker=marker) # noqa F841 + exhibit = Exhibit.objects.create(owner=self.profile, name=f"name_{i}", slug=f"slug_{i}") # noqa F841 + response = self.client.get("/api/v1/exhibits/") + self.assertEqual(response.status_code, 200) + + def test_retrieve_exhibit(self): + marker = Marker.objects.create(owner=self.profile, source=fake_file) + obj = Object.objects.create(owner=self.profile, source=fake_file) + artwork = Artwork.objects.create(author=self.profile, augmented=obj, marker=marker) + exhibit = Exhibit.objects.create(owner=self.profile, name="test") + exhibit.artworks.add(artwork) + response = self.client.get("/api/v1/exhibits/1/") + self.assertEqual(response.status_code, 200) diff --git a/src/core/tests/test_markers_api.py b/src/core/tests/test_markers_api.py new file mode 100644 index 00000000..3ce10ad5 --- /dev/null +++ b/src/core/tests/test_markers_api.py @@ -0,0 +1,74 @@ +"""Test using the marker API for Jandig Markers""" + +from django.conf import settings +from django.core.files.uploadedfile import SimpleUploadedFile +from django.test import TestCase + +from core.models import Marker +from core.serializers.markers import MarkerSerializer +from users.models import User + +fake_file = SimpleUploadedFile("fake_file.png", b"these are the file contents!") + + +class TestMarkerAPI(TestCase): + def setUp(self): + self.user = User.objects.create() + self.profile = self.user.profile + + def test_url(self): + response = self.client.get("/api/v1/markers/") + self.assertEqual(response.status_code, 200) + data = response.json() + self.assertEqual(data["count"], 0) + self.assertEqual(data["next"], None) + self.assertEqual(data["previous"], None) + self.assertEqual(data["results"], []) + + def test_api_markers_lists_one_marker(self): + marker = Marker.objects.create(owner=self.profile, source=fake_file) + response = self.client.get("/api/v1/markers/") + self.assertEqual(response.status_code, 200) + data = response.json() + self.assertEqual(data["count"], 1) + self.assertEqual(data["next"], None) + self.assertEqual(data["previous"], None) + first_result = data["results"][0] + serializer_data = MarkerSerializer(marker).data + serializer_data["source"] = "http://testserver" + serializer_data["source"] + # Asserts the serializer is being used by the endpoint + self.assertDictEqual(first_result, serializer_data) + + # Asserts the serializer uses all the needed fields + self.assertIn("id", first_result) + self.assertIn("owner", first_result) + self.assertIn("source", first_result) + self.assertIn("uploaded_at", first_result) + self.assertIn("author", first_result) + self.assertIn("title", first_result) + self.assertIn("patt", first_result) + + def test_api_markers_lists_multiple_markers(self): + for _ in range(0, settings.PAGE_SIZE + 1): + Marker.objects.create(owner=self.profile, source=fake_file) + + response = self.client.get("/api/v1/markers/") + self.assertEqual(response.status_code, 200) + data = response.json() + self.assertEqual(data["count"], settings.PAGE_SIZE + 1) + self.assertEqual( + data["next"], + f"http://testserver/api/v1/markers/?limit={settings.PAGE_SIZE}&offset=20", + ) + self.assertEqual(data["previous"], None) + self.assertEqual(len(data["results"]), 20) + + def test_retrieve_marker(self): + marker = Marker.objects.create(owner=self.profile, source=fake_file) + response = self.client.get(f"/api/v1/markers/{marker.id}/") + self.assertEqual(response.status_code, 200) + data = response.json() + serializer_data = MarkerSerializer(marker).data + serializer_data["source"] = "http://testserver" + serializer_data["source"] + # Asserts the serializer is being used by the endpoint + self.assertDictEqual(data, serializer_data) diff --git a/src/core/tests/test_objects_api.py b/src/core/tests/test_objects_api.py new file mode 100644 index 00000000..d084ad78 --- /dev/null +++ b/src/core/tests/test_objects_api.py @@ -0,0 +1,76 @@ +"""Test using the object API for Jandig Objects""" + +from django.conf import settings +from django.core.files.uploadedfile import SimpleUploadedFile +from django.test import TestCase + +from core.models import Object +from core.serializers.objects import ObjectSerializer +from users.models import User + +fake_file = SimpleUploadedFile("fake_file.png", b"these are the file contents!") + + +class TestObjectAPI(TestCase): + def setUp(self): + self.user = User.objects.create() + self.profile = self.user.profile + + def test_url(self): + response = self.client.get("/api/v1/objects/") + self.assertEqual(response.status_code, 200) + data = response.json() + self.assertEqual(data["count"], 0) + self.assertEqual(data["next"], None) + self.assertEqual(data["previous"], None) + self.assertEqual(data["results"], []) + + def test_api_objects_lists_one_object(self): + obj = Object.objects.create(owner=self.profile, source=fake_file) + response = self.client.get("/api/v1/objects/") + self.assertEqual(response.status_code, 200) + data = response.json() + self.assertEqual(data["count"], 1) + self.assertEqual(data["next"], None) + self.assertEqual(data["previous"], None) + first_result = data["results"][0] + serializer_data = ObjectSerializer(obj).data + serializer_data["source"] = "http://testserver" + serializer_data["source"] + # Asserts the serializer is being used by the endpoint + self.assertDictEqual(first_result, serializer_data) + + # Asserts the serializer uses all the needed fields + self.assertIn("id", first_result) + self.assertIn("owner", first_result) + self.assertIn("source", first_result) + self.assertIn("uploaded_at", first_result) + self.assertIn("author", first_result) + self.assertIn("title", first_result) + self.assertIn("scale", first_result) + self.assertIn("position", first_result) + self.assertIn("rotation", first_result) + + def test_api_objects_lists_multiple_objects(self): + for _ in range(0, settings.PAGE_SIZE + 1): + Object.objects.create(owner=self.profile, source=fake_file) + + response = self.client.get("/api/v1/objects/") + self.assertEqual(response.status_code, 200) + data = response.json() + self.assertEqual(data["count"], settings.PAGE_SIZE + 1) + self.assertEqual( + data["next"], + f"http://testserver/api/v1/objects/?limit={settings.PAGE_SIZE}&offset=20", + ) + self.assertEqual(data["previous"], None) + self.assertEqual(len(data["results"]), 20) + + def test_retrieve_object(self): + obj = Object.objects.create(owner=self.profile, source=fake_file) + response = self.client.get(f"/api/v1/objects/{obj.id}/") + self.assertEqual(response.status_code, 200) + data = response.json() + serializer_data = ObjectSerializer(obj).data + serializer_data["source"] = "http://testserver" + serializer_data["source"] + # Asserts the serializer is being used by the endpoint + self.assertDictEqual(data, serializer_data) diff --git a/src/core/urls.py b/src/core/urls.py new file mode 100644 index 00000000..eb4cc5f7 --- /dev/null +++ b/src/core/urls.py @@ -0,0 +1,51 @@ +from django.conf import settings +from django.urls import include, path +from rest_framework_nested.routers import DefaultRouter + +from core.views.artworks import ArtworkViewset +from core.views.exhibits import ExhibitViewset +from core.views.markers import MarkerViewset +from core.views.objects import ObjectViewset +from core.views.static_views import ( + community, + documentation, + health_check, + home, + marker_generator, +) +from core.views.views import ( + artwork_preview, + collection, + exhibit_detail, + exhibit_select, + manifest, + robots_txt, + see_all, + service_worker, + upload_image, +) + +api_router = DefaultRouter() +api_router.register("markers", MarkerViewset, basename="marker") +api_router.register("objects", ObjectViewset, basename="object") +api_router.register("artworks", ArtworkViewset, basename="artwork") +api_router.register("exhibits", ExhibitViewset, basename="exhibit") + +urlpatterns = [ + path("", home, name="home"), + path("api/v1/", include(api_router.urls)), + path("documentation/", documentation, name="documentation"), + path("community/", community, name="community"), + path("collection/", collection, name="collection"), + path("exhibit_select/", exhibit_select, name="exhibit_select"), + path("exhibit/", exhibit_detail, name="exhibit-detail"), + path("artwork/", artwork_preview, name="artwork-preview"), + path("generator/", marker_generator, name="marker-generator"), + path("sw.js", service_worker, name="sw"), + path("manifest.json", manifest, name="manifest"), + path("upload", upload_image, name="upload-image"), + path("i18n/", include("django.conf.urls.i18n")), + path("see_all/", see_all, name="see_all"), + path("robots.txt/", robots_txt), + path(settings.HEALTH_CHECK_URL, health_check), +] diff --git a/src/core/views/__init__.py b/src/core/views/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/core/views/artworks.py b/src/core/views/artworks.py new file mode 100644 index 00000000..eaaea233 --- /dev/null +++ b/src/core/views/artworks.py @@ -0,0 +1,10 @@ +from rest_framework.mixins import ListModelMixin, RetrieveModelMixin +from rest_framework.viewsets import GenericViewSet + +from core.models import Artwork +from core.serializers.artworks import ArtworkSerializer + + +class ArtworkViewset(ListModelMixin, RetrieveModelMixin, GenericViewSet): + serializer_class = ArtworkSerializer + queryset = Artwork.objects.all().order_by("id") diff --git a/src/core/views/exhibits.py b/src/core/views/exhibits.py new file mode 100644 index 00000000..e3a2fe46 --- /dev/null +++ b/src/core/views/exhibits.py @@ -0,0 +1,10 @@ +from rest_framework.mixins import ListModelMixin, RetrieveModelMixin +from rest_framework.viewsets import GenericViewSet + +from core.models import Exhibit +from core.serializers.exhibits import ExhibitSerializer + + +class ExhibitViewset(ListModelMixin, RetrieveModelMixin, GenericViewSet): + serializer_class = ExhibitSerializer + queryset = Exhibit.objects.all().order_by("id") diff --git a/src/core/views/markers.py b/src/core/views/markers.py new file mode 100644 index 00000000..bd185f5a --- /dev/null +++ b/src/core/views/markers.py @@ -0,0 +1,10 @@ +from rest_framework.mixins import ListModelMixin, RetrieveModelMixin +from rest_framework.viewsets import GenericViewSet + +from core.models import Marker +from core.serializers.markers import MarkerSerializer + + +class MarkerViewset(ListModelMixin, RetrieveModelMixin, GenericViewSet): + serializer_class = MarkerSerializer + queryset = Marker.objects.all().order_by("id") diff --git a/src/core/views/objects.py b/src/core/views/objects.py new file mode 100644 index 00000000..e7a01f61 --- /dev/null +++ b/src/core/views/objects.py @@ -0,0 +1,10 @@ +from rest_framework.mixins import ListModelMixin, RetrieveModelMixin +from rest_framework.viewsets import GenericViewSet + +from core.models import Object +from core.serializers.objects import ObjectSerializer + + +class ObjectViewset(ListModelMixin, RetrieveModelMixin, GenericViewSet): + serializer_class = ObjectSerializer + queryset = Object.objects.all().order_by("id") diff --git a/src/core/views/static_views.py b/src/core/views/static_views.py new file mode 100644 index 00000000..e1bc971d --- /dev/null +++ b/src/core/views/static_views.py @@ -0,0 +1,26 @@ +from django.http import JsonResponse +from django.shortcuts import render + + +def home(request): + return render(request, "users/profile.jinja2", {}) + + +def documentation(request): + return render(request, "core/documentation.jinja2", {}) + + +def ar_viewer(request): + return render(request, "core/exhibit.jinja2") + + +def community(request): + return render(request, "core/community.jinja2") + + +def marker_generator(request): + return render(request, "core/generator.html", {}) + + +def health_check(request): + return JsonResponse({"status": "ok"}, status=200) diff --git a/src/core/views/views.py b/src/core/views/views.py new file mode 100644 index 00000000..8e38659d --- /dev/null +++ b/src/core/views/views.py @@ -0,0 +1,133 @@ +from django.core.paginator import Paginator +from django.http import HttpResponse, HttpResponseRedirect +from django.shortcuts import redirect, render +from django.urls import reverse +from django.views.decorators.cache import cache_page +from django.views.decorators.http import require_http_methods + +from core.forms import ExhibitForm, UploadFileForm +from core.helpers import handle_upload_image +from core.models import Artwork, Exhibit, Marker, Object + + +@cache_page(60 * 60) +@require_http_methods(["GET"]) +def service_worker(request): + return render(request, "core/sw.js", content_type="application/x-javascript") + + +@cache_page(60 * 60) +@require_http_methods(["GET"]) +def manifest(request): + return render( + request, "core/manifest.json", content_type="application/x-javascript" + ) + + +def index(request): + ctx = {"artworks": []} + + return render(request, "core/exhibit.jinja2", ctx) + + +@cache_page(60 * 2) +@require_http_methods(["GET"]) +def collection(request): + exhibits = Exhibit.objects.all().order_by("creation_date")[:4] + artworks = Artwork.objects.all().order_by("created_at")[:6] + markers = Marker.objects.all().order_by("uploaded_at")[:8] + objects = Object.objects.all().order_by("uploaded_at")[:8] + + ctx = { + "artworks": artworks, + "exhibits": exhibits, + "markers": markers, + "objects": objects, + "seeall": False, + } + + return render(request, "core/collection.jinja2", ctx) + + +@cache_page(60 * 2) +@require_http_methods(["GET"]) +def see_all(request): + request_type = request.GET.get("which") + ctx = {} + per_page = 20 + page = request.GET.get("page", 1) + + data_types = { + "objects": Object.objects.all().order_by("uploaded_at"), + "markers": Marker.objects.all().order_by("uploaded_at"), + "artworks": Artwork.objects.all().order_by("created_at"), + "exhibits": Exhibit.objects.all().order_by("creation_date"), + } + + data = data_types.get(request_type) + if data: + paginator = Paginator(data, per_page) + data = paginator.get_page(page) + data.adjusted_elided_pages = paginator.get_elided_page_range(page) + ctx = { + request_type: data, + "seeall": True, + } + + return render(request, "core/collection.jinja2", ctx) + + +def upload_image(request): + if request.method == "POST": + form = UploadFileForm(request.POST, request.FILES) + image = request.FILES.get("file") + if form.is_valid() and image: + handle_upload_image(image) + return HttpResponseRedirect(reverse("index")) + else: + form = UploadFileForm() + return render(request, "core/upload.jinja2", {"form": form}) + + +def exhibit_select(request): + if request.method == "POST": + form = ExhibitForm(request.POST) + if form.is_valid(): + exhibit = form.cleaned_data.get("exhibit") + return redirect("/" + exhibit.slug) + else: + form = ExhibitForm() + + return render(request, "core/exhibit_select.jinja2", {"form": form}) + + +@cache_page(60 * 60) +@require_http_methods(["GET"]) +def exhibit_detail(request): + index = request.GET.get("id") + exhibit = Exhibit.objects.get(id=index) + ctx = { + "exhibit": exhibit, + "exhibitImage": "https://cdn3.iconfinder.com/data/icons/basic-mobile-part-2/512/painter-512.png", + "artworks": exhibit.artworks.all(), + } + return render(request, "core/exhibit_detail.jinja2", ctx) + + +@require_http_methods(["GET"]) +def artwork_preview(request): + artwork_id = request.GET.get("id") + + ctx = { + "artworks": Artwork.objects.filter(id=artwork_id).order_by("-id"), + } + return render(request, "core/exhibit.jinja2", ctx) + + +@require_http_methods(["GET"]) +def robots_txt(request): + lines = [ + "User-Agent: *", + "Disallow: ", + ] + return HttpResponse("\n".join(lines), content_type="text/plain") diff --git a/src/data/nginx/http.conf b/src/data/nginx/http.conf deleted file mode 100644 index 2ed04c0e..00000000 --- a/src/data/nginx/http.conf +++ /dev/null @@ -1,36 +0,0 @@ -server { - listen 80; - server_name dev.jandig.app; - - location / { - return 301 https://$host$request_uri; - } - - location /.well-known/acme-challenge/ { - root /var/www/certbot; - } -} -server { - listen 80; - server_name staging.jandig.app; - - location / { - return 301 https://$host$request_uri; - } - - location /.well-known/acme-challenge/ { - root /var/www/certbot; - } -} -server { - listen 80; - server_name jandig.app; - - location / { - return 301 https://$host$request_uri; - } - - location /.well-known/acme-challenge/ { - root /var/www/certbot; - } -} \ No newline at end of file diff --git a/src/data/nginx/ssl.conf b/src/data/nginx/ssl.conf deleted file mode 100644 index f9c7d854..00000000 --- a/src/data/nginx/ssl.conf +++ /dev/null @@ -1,76 +0,0 @@ -# upstream dev_django { -# server 208.113.128.83:8000; -# } -# upstream staging_django { -# server 208.113.128.83:8001; -# } -# upstream prod_django { -# server 208.113.128.83:8002; -# } - -server { - listen 443 ssl; - server_name dev.jandig.app; - - ssl_certificate /etc/letsencrypt/live/dev.jandig.app/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/dev.jandig.app/privkey.pem; - - include /etc/letsencrypt/options-ssl-nginx.conf; - ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; - - location / { - proxy_set_header Host $http_host; - proxy_set_header X-Real-IP $remote_addr; - proxy_pass http://dev_django:8000/; - } - - location /static { - alias /usr/share/nginx/html/static/dev; - } - charset utf-8; - client_max_body_size 0; -} -server { - listen 443 ssl; - server_name staging.jandig.app; - - ssl_certificate /etc/letsencrypt/live/staging.jandig.app/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/staging.jandig.app/privkey.pem; - - include /etc/letsencrypt/options-ssl-nginx.conf; - ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; - - location / { - proxy_set_header Host $http_host; - proxy_set_header X-Real-IP $remote_addr; - proxy_pass http://staging_django:8000/; - } - - location /static { - alias /usr/share/nginx/html/static/staging; - } - charset utf-8; - client_max_body_size 0; -} -server { - listen 443 ssl; - server_name jandig.app; - - ssl_certificate /etc/letsencrypt/live/jandig.app/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/jandig.app/privkey.pem; - - include /etc/letsencrypt/options-ssl-nginx.conf; - ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; - - location / { - proxy_set_header Host $http_host; - proxy_set_header X-Real-IP $remote_addr; - proxy_pass http://prod_django:8000/; - } - - location /static { - alias /usr/share/nginx/html/static/prod; - } - charset utf-8; - client_max_body_size 0; -} \ No newline at end of file diff --git a/src/manage.py b/src/manage.py new file mode 100755 index 00000000..41464304 --- /dev/null +++ b/src/manage.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python +import os +import sys + +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings") + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError("Couldn't import Django. Are you sure it's installed and " "available on your PYTHONPATH environment variable? Did you " "forget to activate a virtual environment?") from exc + execute_from_command_line(sys.argv) diff --git a/src/requirements.txt b/src/requirements.txt deleted file mode 100644 index c498a8fc..00000000 --- a/src/requirements.txt +++ /dev/null @@ -1,19 +0,0 @@ -django ~= 2.2.24 -jinja2 ~= 2.10 -gunicorn ~= 19.0 -pillow ~= 5.3.0 -django-cors-headers -invoke ~= 1.2.0 -django-environ == 0.4.4 -psycopg2-binary~=2.8.6 -django-boogie -sphinx ~= 2.1.1 -django-docs ~= 0.3.1 -babel ~= 2.7.0 -django-debug-toolbar -gevent -django-extensions -factory_boy -boto3~=1.17.26 -django-storages~=1.11.1 -sentry-sdk~=1.3.1 \ No newline at end of file diff --git a/src/tests/home_tests.py b/src/tests/home_tests.py new file mode 100644 index 00000000..a1cc097e --- /dev/null +++ b/src/tests/home_tests.py @@ -0,0 +1,22 @@ +import re +from playwright.sync_api import Page, expect + +def test_home_loads_in_english(page: Page): + page.goto("http://localhost:8000/") + + page.get_by_role("heading", name="Welcome to Jandig").click() + + +def test_changing_language_to_portuguese_and_back_to_english(page: Page) -> None: + page.goto("http://localhost:8000/") + page.locator(".trigger-lang-modal").click() + page.get_by_label("pt-br").check() + page.get_by_role("button", name="Ok").click() + expect(page).to_have_url("http://localhost:8000/") + + expect(page.get_by_role("heading")).to_have_text("Bem vindo ao Jandig") + page.locator(".trigger-lang-modal").click() + page.get_by_label("en-us").check() + page.get_by_role("button", name="Ok").click() + expect(page).to_have_url("http://localhost:8000/") + expect(page.get_by_role("heading")).to_have_text("Welcome to Jandig") \ No newline at end of file diff --git a/src/users/__init__.py b/src/users/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/users/admin.py b/src/users/admin.py new file mode 100644 index 00000000..64b5c99c --- /dev/null +++ b/src/users/admin.py @@ -0,0 +1,108 @@ +from typing import Any +from django.contrib import admin +from django.contrib.auth.admin import UserAdmin +from django.contrib.auth.models import User +from django.db.models.query import QuerySet +from django.http import HttpRequest +from django.db.models import Q, Count +from django.urls import reverse +from django.utils.html import format_html +from django.contrib import admin +from users.models import Profile +from django.contrib.admin.filters import ListFilter +admin.site.unregister(User) + +class NoArtFilter(admin.SimpleListFilter): + title = "Art Qtdy" + + # Parameter for the filter that will be used in the URL query. + parameter_name = "art_qtd" + def lookups(self, request, model_admin): + return [ + ("no_artwork", "Zero Artworks"), + ("no_marker", "Zero Markers"), + ("no_objects", "Zero Objects"), + ("no_exhibits", "Zero Exhibits"), + ("no_art", "Zero Content"), + ] + + def queryset(self, request, queryset): + conditions = Q() + if not self.value(): + return queryset + query = self.value().split(",") + if "no_artwork" in query: + conditions &= Q(_artworks_count=0) + if "no_marker" in query: + conditions &= Q(_markers_count=0) + if "no_objects" in query: + conditions &= Q(_ar_objects_count=0) + if "no_exhibits" in query: + conditions &= Q(_exhibits_count=0) + if "no_art" in query: + return queryset.filter(Q(_artworks_count=0)&Q(_ar_objects_count=0)&Q(_exhibits_count=0)&Q(_markers_count=0)) + + # Apply the filter + filtered_queryset = queryset.filter(conditions) + return filtered_queryset + +@admin.register(Profile) +class ProfileAdmin(admin.ModelAdmin): + list_display=["id", "user_link","artworks_count","markers_count","ar_objects_count","exhibits_count", "created", "last_login"] + ordering=["-id"] + list_filter = [NoArtFilter] + def get_queryset(self, request): + queryset = super().get_queryset(request).select_related("user").prefetch_related("artworks", "markers", "exhibits", "ar_objects") + queryset = queryset.annotate( + _markers_count = Count("markers", distinct=True), + _artworks_count = Count("artworks", distinct=True), + _ar_objects_count = Count("ar_objects", distinct=True), + _exhibits_count = Count("exhibits", distinct=True) + ) + return queryset + + def username(self, obj): + return obj.user.username + + def created(self,obj): + return obj.user.date_joined + + def last_login(self,obj): + return obj.user.last_login + + def user_link(self,obj): + """Link to related User""" + link = reverse("admin:index") + "auth/user/?id=" + str(obj.user.id) + return format_html('{}', link, obj.user.username) + + def artworks_count(self, obj): + return obj._artworks_count + + def markers_count(self, obj): + return obj._markers_count + + def ar_objects_count(self, obj): + return obj._ar_objects_count + + def exhibits_count(self, obj): + return obj._exhibits_count + + artworks_count.admin_order_field = "_artworks_count" + markers_count.admin_order_field= "_markers_count" + ar_objects_count.admin_order_field="_ar_objects_count" + exhibits_count.admin_order_field="_exhibits_count" + + +@admin.register(User) +class JandigUserAdmin(UserAdmin): + list_display = ("username", "email","profile_link", "last_login", "date_joined", "is_staff") + list_filter = ("is_staff", "is_superuser", "is_active", "groups") + def get_queryset(self, request: HttpRequest) -> QuerySet[Any]: + queryset = super().get_queryset(request).select_related("profile") + return queryset + + + def profile_link(self,obj): + """Link to related Profile""" + link = reverse("admin:index") + "users/profile/?id=" + str(obj.profile.id) + return format_html('{}', link, obj.username) diff --git a/src/ARte/users/apps.py b/src/users/apps.py similarity index 77% rename from src/ARte/users/apps.py rename to src/users/apps.py index 4ce1fabc..3ef1284a 100644 --- a/src/ARte/users/apps.py +++ b/src/users/apps.py @@ -2,4 +2,4 @@ class UsersConfig(AppConfig): - name = 'users' + name = "users" diff --git a/src/users/choices.py b/src/users/choices.py new file mode 100644 index 00000000..c9cc23b2 --- /dev/null +++ b/src/users/choices.py @@ -0,0 +1,239 @@ +COUNTRY_CHOICES = ( + ("AF", "AFGHANISTAN"), + ("AL", "ALBANIA"), + ("DZ", "ALGERIA"), + ("AS", "AMERICAN SAMOA"), + ("AD", "ANDORRA"), + ("AO", "ANGOLA"), + ("AI", "ANGUILLA"), + ("AQ", "ANTARCTICA"), + ("AG", "ANTIGUA AND BARBUDA"), + ("AR", "ARGENTINA"), + ("AM", "ARMENIA"), + ("AW", "ARUBA"), + ("AU", "AUSTRALIA"), + ("AT", "AUSTRIA"), + ("AZ", "AZERBAIJAN"), + ("BS", "BAHAMAS"), + ("BH", "BAHRAIN"), + ("BD", "BANGLADESH"), + ("BB", "BARBADOS"), + ("BY", "BELARUS"), + ("BE", "BELGIUM"), + ("BZ", "BELIZE"), + ("BJ", "BENIN"), + ("BM", "BERMUDA"), + ("BT", "BHUTAN"), + ("BO", "BOLIVIA"), + ("BA", "BOSNIA AND HERZEGOVINA"), + ("BW", "BOTSWANA"), + ("BV", "BOUVET ISLAND"), + ("BR", "BRAZIL"), + ("IO", "BRITISH INDIAN OCEAN TERRITORY"), + ("BN", "BRUNEI DARUSSALAM"), + ("BG", "BULGARIA"), + ("BF", "BURKINA FASO"), + ("BI", "BURUNDI"), + ("KH", "CAMBODIA"), + ("CM", "CAMEROON"), + ("CA", "CANADA"), + ("CV", "CAPE VERDE"), + ("KY", "CAYMAN ISLANDS"), + ("CF", "CENTRAL AFRICAN REPUBLIC"), + ("TD", "CHAD"), + ("CL", "CHILE"), + ("CN", "PEOPLE'S REPUBLIC OF CHINA"), + ("CX", "ISLAND"), + ("CC", "COCOS (KEELING) ISLANDS"), + ("CO", "COLOMBIA"), + ("KM", "COMOROS"), + ("CG", "CONGO"), + ("CD", "CONGO, THE DEMOCRATIC REPUBLIC OF"), + ("CK", "COOK ISLANDS"), + ("CR", "COSTA RICA"), + ("CI", "CÔTE D'IVOIRE"), + ("HR", "CROATIA"), + ("CU", "CUBA"), + ("CY", "CYPRUS"), + ("CZ", "CZECH REPUBLIC"), + ("DK", "DENMARK"), + ("DJ", "DJIBOUTI"), + ("DM", "DOMINICA"), + ("DO", "DOMINICAN REPUBLIC"), + ("EC", "ECUADOR"), + ("EG", "EGYPT"), + ("EH", "WESTERN SAHARA"), + ("SV", "EL SALVADOR"), + ("GQ", "EQUATORIAL GUINEA"), + ("ER", "ERITREA"), + ("EE", "ESTONIA"), + ("ET", "ETHIOPIA"), + ("FK", "FALKLAND ISLANDS (MALVINAS)"), + ("FO", "ISLANDS"), + ("FJ", "FIJI"), + ("FI", "FINLAND"), + ("FR", "FRANCE"), + ("GF", "FRENCH GUIANA"), + ("PF", "FRENCH POLYNESIA"), + ("TF", "FRENCH SOUTHERN TERRITORIES"), + ("GA", "GABON"), + ("GM", "GAMBIA"), + ("GE", "GEORGIA"), + ("DE", "GERMANY"), + ("GH", "GHANA"), + ("GI", "GIBRALTAR"), + ("GR", "GREECE"), + ("GL", "GREENLAND"), + ("GD", "GRENADA"), + ("GP", "GUADELOUPE"), + ("GU", "GUAM"), + ("GT", "GUATEMALA"), + ("GN", "GUINEA"), + ("GW", "GUINEA-BISSAU"), + ("GY", "GUYANA"), + ("HT", "HAITI"), + ("HM", "HEARD ISLAND AND MCDONALD ISLANDS"), + ("HN", "HONDURAS"), + ("HK", "HONG KONG"), + ("HU", "HUNGARY"), + ("IS", "ICELAND"), + ("IN", "INDIA"), + ("ID", "INDONESIA"), + ("IR", "IRAN, ISLAMIC REPUBLIC OF"), + ("IQ", "IRAQ"), + ("IE", "IRELAND"), + ("IL", "ISRAEL"), + ("IT", "ITALY"), + ("JM", "JAMAICA"), + ("JP", "JAPAN"), + ("JO", "JORDAN"), + ("KZ", "KAZAKHSTAN"), + ("KE", "KENYA"), + ("KI", "KIRIBATI"), + ("KP", "KOREA, DEMOCRATIC PEOPLE'S REPUBLIC OF"), + ("KR", "KOREA, REPUBLIC OF"), + ("KW", "KUWAIT"), + ("KG", "KYRGYZSTAN"), + ("LA", "LAO PEOPLE'S DEMOCRATIC REPUBLIC"), + ("LV", "LATVIA"), + ("LB", "LEBANON"), + ("LS", "LESOTHO"), + ("LR", "LIBERIA"), + ("LY", "LIBYAN ARAB JAMAHIRIYA"), + ("LI", "LIECHTENSTEIN"), + ("LT", "LITHUANIA"), + ("LU", "LUXEMBOURG"), + ("MO", "MACAO"), + ("MK", "MACEDONIA, THE FORMER YUGOSLAV REPUBLIC OF"), + ("MG", "MADAGASCAR"), + ("MW", "MALAWI"), + ("MY", "MALAYSIA"), + ("MV", "MALDIVES"), + ("ML", "MALI"), + ("MT", "MALTA"), + ("MH", "MARSHALL ISLANDS"), + ("MQ", "MARTINIQUE"), + ("MR", "MAURITANIA"), + ("MU", "MAURITIUS"), + ("YT", "MAYOTTE"), + ("MX", "MEXICO"), + ("FM", "MICRONESIA, FEDERATED STATES OF"), + ("MD", "MOLDOVA, REPUBLIC OF"), + ("MC", "MONACO"), + ("MN", "MONGOLIA"), + ("MS", "MONTSERRAT"), + ("MA", "MOROCCO"), + ("MZ", "MOZAMBIQUE"), + ("MM", "MYANMAR"), + ("NA", "NAMIBIA"), + ("NR", "NAURU"), + ("NP", "NEPAL"), + ("NL", "NETHERLANDS"), + ("AN", "NETHERLANDS ANTILLES"), + ("NC", "NEW CALEDONIA"), + ("NZ", "NEW ZEALAND"), + ("NI", "NICARAGUA"), + ("NE", "NIGER"), + ("NG", "NIGERIA"), + ("NU", "NIUE"), + ("NF", "NORFOLK ISLAND"), + ("MP", "NORTHERN MARIANA ISLANDS"), + ("NO", "NORWAY"), + ("OM", "OMAN"), + ("PK", "PAKISTAN"), + ("PW", "PALAU"), + ("PS", "PALESTINIAN TERRITORY, OCCUPIED"), + ("PA", "PANAMA"), + ("PG", "PAPUA NEW GUINEA"), + ("PY", "PARAGUAY"), + ("PE", "PERU"), + ("PH", "PHILIPPINES"), + ("PN", "PITCAIRN"), + ("PL", "POLAND"), + ("PT", "PORTUGAL"), + ("PR", "PUERTO RICO"), + ("QA", "QATAR"), + ("RE", "RÉUNION"), + ("RO", "ROMANIA"), + ("RU", "RUSSIAN FEDERATION"), + ("RW", "RWANDA"), + ("SH", "SAINT HELENA"), + ("KN", "SAINT KITTS AND NEVIS"), + ("LC", "SAINT LUCIA"), + ("PM", "SAINT PIERRE AND MIQUELON"), + ("VC", "SAINT VINCENT AND THE GRENADINES"), + ("WS", "SAMOA"), + ("SM", "SAN MARINO"), + ("ST", "SAO TOME AND PRINCIPE"), + ("SA", "SAUDI ARABIA"), + ("SN", "SENEGAL"), + ("CS", "SERBIA AND MONTENEGRO"), + ("SC", "SEYCHELLES"), + ("SL", "SIERRA LEONE"), + ("SG", "SINGAPORE"), + ("SK", "SLOVAKIA"), + ("SI", "SLOVENIA"), + ("SB", "SOLOMON ISLANDS"), + ("SO", "SOMALIA"), + ("ZA", "SOUTH AFRICA"), + ("GS", "SOUTH GEORGIA AND SOUTH SANDWICH ISLANDS"), + ("ES", "SPAIN"), + ("LK", "SRI LANKA"), + ("SD", "SUDAN"), + ("SR", "SURINAME"), + ("SJ", "SVALBARD AND JAN MAYEN"), + ("SZ", "SWAZILAND"), + ("SE", "SWEDEN"), + ("CH", "SWITZERLAND"), + ("SY", "SYRIAN ARAB REPUBLIC"), + ("TW", "TAIWAN, REPUBLIC OF CHINA"), + ("TJ", "TAJIKISTAN"), + ("TZ", "TANZANIA, UNITED REPUBLIC OF"), + ("TH", "THAILAND"), + ("TL", "TIMOR-LESTE"), + ("TG", "TOGO"), + ("TK", "TOKELAU"), + ("TO", "TONGA"), + ("TT", "TRINIDAD AND TOBAGO"), + ("TN", "TUNISIA"), + ("TR", "TURKEY"), + ("TM", "TURKMENISTAN"), + ("TC", "TURKS AND CAICOS ISLANDS"), + ("TV", "TUVALU"), + ("UG", "UGANDA"), + ("UA", "UKRAINE"), + ("AE", "UNITED ARAB EMIRATES"), + ("GB", "UNITED KINGDOM"), + ("US", "UNITED STATES"), + ("UM", "UNITED STATES MINOR OUTLYING ISLANDS"), + ("UY", "URUGUAY"), + ("UZ", "UZBEKISTAN"), + ("VE", "VENEZUELA"), + ("VU", "VANUATU"), + ("VN", "VIET NAM"), + ("VG", "BRITISH VIRGIN ISLANDS"), + ("VI", "U.S. VIRGIN ISLANDS"), + ("WF", "WALLIS AND FUTUNA"), + ("YE", "YEMEN"), + ("ZW", "ZIMBABWE"), +) diff --git a/src/users/factory.py b/src/users/factory.py new file mode 100644 index 00000000..fc89688a --- /dev/null +++ b/src/users/factory.py @@ -0,0 +1,28 @@ +from django.contrib.auth.models import User +from factory.django import DjangoModelFactory + +from core.models import Object + + +class UserFactory(DjangoModelFactory): + username = "Testador" + email = "testador@memelab.com" + + class Meta: + model = User + + +class ObjectFactory(DjangoModelFactory): + id = 1 + source = "objects/osaka.gif" + owner = "Matheus" + author = "Matheus" + uploaded_at = (2020, 11, 25, 14, 30, 0) + title = "osaka" + position = "0 0 0" + scale = "1 1" + rotation = "270 0 0" + + class Meta: + + model = Object diff --git a/src/users/forms.py b/src/users/forms.py new file mode 100644 index 00000000..7e321b84 --- /dev/null +++ b/src/users/forms.py @@ -0,0 +1,281 @@ +import logging +import re +from io import BytesIO + +from django import forms +from django.contrib.auth import authenticate, get_user_model +from django.contrib.auth.forms import AuthenticationForm +from django.contrib.auth.forms import PasswordChangeForm as OrigPasswordChangeForm +from django.contrib.auth.forms import UserCreationForm +from django.core.files.base import ContentFile, File +from django.forms.widgets import HiddenInput +from django.utils.translation import gettext_lazy as _ +from PIL import Image +from pymarker.core import generate_marker_from_image, generate_patt_from_image + +from core.models import Marker, Object + +from .choices import COUNTRY_CHOICES + +log = logging.getLogger("ej") + +User = get_user_model() + + +class SignupForm(UserCreationForm): + """ + Form to register a new user + """ + + email = forms.EmailField( + max_length=254, + help_text=_("Your e-mail address"), + ) + + username = forms.CharField( + max_length=12, + help_text=_("Your username"), + ) + + def __init__(self, *args, **kwargs): + super(SignupForm, self).__init__(*args, **kwargs) + + self.fields["email"].widget.attrs["placeholder"] = _("email") + self.fields["username"].widget.attrs["placeholder"] = _("chosen username") + self.fields["password1"].widget.attrs["placeholder"] = _("password") + self.fields["password2"].widget.attrs["placeholder"] = _("confirm password") + + class Meta: + model = User + fields = ["email", "username", "password1", "password2"] + + def clean_email(self): + email = self.cleaned_data.get("email") + username = self.cleaned_data.get("username") + if email and User.objects.filter(email=email).exclude(username=username).exists(): + raise forms.ValidationError(_("E-mail taken")) + + return email + + +class PasswordChangeForm(OrigPasswordChangeForm): + def __init__(self, *args, **kwargs): + super(PasswordChangeForm, self).__init__(*args, **kwargs) + self.fields["old_password"].widget.attrs["placeholder"] = _("Old Password") + self.fields["new_password1"].widget.attrs["placeholder"] = _("New Password") + self.fields["new_password2"].widget.attrs["placeholder"] = _("New Password Again") + + +class ProfileForm(forms.ModelForm): + class Meta: + model = User + fields = ["username", "email", "bio", "country", "personal_site"] + + field_order = ["email", "username", "personal_site", "country", "bio"] + + def __init__(self, *args, **kwargs): + super(ProfileForm, self).__init__(*args, **kwargs) + self.initial["username"] = self.instance.user.username + + # FIXME: user.email come as a string of a tuple, no idea why. "('email@bla.com',)" + # email = self.instance.user.email.replace("('", "").replace("',)", "") + self.initial["email"] = self.instance.user.email + self.initial["bio"] = self.instance.bio + self.initial["country"] = self.instance.country + self.initial["personal_site"] = self.instance.personal_site + self.fields["email"].widget.attrs["placeholder"] = _("E-mail") + self.fields["username"].widget.attrs["placeholder"] = _("Username") + self.fields["bio"].widget.attrs["placeholder"] = _("Personal Bio / Description") + self.fields["personal_site"].widget.attrs["placeholder"] = _("Personal Website") + + email = forms.EmailField( + max_length=254, + help_text=_("Your e-mail address"), + ) + username = forms.CharField( + max_length=12, + help_text=_("Your username"), + ) + country = forms.ChoiceField(choices=COUNTRY_CHOICES, required=False) + bio = forms.CharField( + max_length=500, + required=False, + widget=forms.Textarea, + help_text=_("Personal Bio / Description"), + ) + personal_site = forms.URLField( + required=False, + help_text=_("Personal Website"), + ) + + def clean_username(self): + username = self.cleaned_data.get("username") + if username and User.objects.filter(username=username).exclude(username=self.instance.user.username).exists(): + raise forms.ValidationError(_("Username already in use")) + return username + + def clean_email(self): + email = self.cleaned_data.get("email") + if email and User.objects.filter(email=email).exclude(username=self.instance.user.username).exists(): + raise forms.ValidationError(_("Email address must be unique")) + + return email + + +class LoginForm(AuthenticationForm): + def __init__(self, *args, **kwargs): + super(LoginForm, self).__init__(*args, **kwargs) + self.fields["username"].widget.attrs["placeholder"] = _("username / email") + self.fields["password"].widget.attrs["placeholder"] = _("password") + + def clean_username(self): + username_or_email = self.cleaned_data.get("username") + if "@" in username_or_email: + if not User.objects.filter(email=username_or_email).exists(): + raise forms.ValidationError(_("Username/email not found")) + user = User.objects.get(email=username_or_email) + if user: + return user.username + else: + if not User.objects.filter(username=username_or_email).exists(): + raise forms.ValidationError(_("Username/email not found")) + + # Already is a valid username + return username_or_email + + def clean_password(self): + password = self.cleaned_data.get("password") + username_or_email = self.cleaned_data.get("username") + user = None + username_or_email_wrong = False + + if "@" in username_or_email: + if User.objects.filter(email=username_or_email).exists(): + username = User.objects.get(email=username_or_email).username + user = authenticate(username=username, password=password) + else: + username_or_email_wrong = True + # raise forms.ValidationError(_('Email Wrong!')) + else: + if User.objects.filter(username=username_or_email).exists(): + user = authenticate(username=username_or_email, password=password) + else: + username_or_email_wrong = True + # raise forms.ValidationError(_('Username Wrong!')) + + if not user and not username_or_email_wrong: + raise forms.ValidationError(_("Wrong password!")) + + return password + + +class RecoverPasswordForm(forms.Form): + username_or_email = forms.CharField(label="username / email", max_length="50") + + +class RecoverPasswordCodeForm(forms.Form): + verification_code = forms.CharField(label="Verification code", max_length="200") + + +class UploadMarkerForm(forms.ModelForm): + def __init__(self, *args, **kwargs): + super(UploadMarkerForm, self).__init__(*args, **kwargs) + + log.warning(self.fields) + self.fields["source"].widget.attrs["placeholder"] = _("browse file") + self.fields["source"].widget.attrs["accept"] = "image/png, image/jpg" + self.fields["author"].widget.attrs["placeholder"] = _("declare different author name") + self.fields["title"].widget.attrs["placeholder"] = _("Marker's title") + + class Meta: + model = Marker + exclude = ("owner", "uploaded_at", "patt") + + def save(self, *args, **kwargs): + commit = kwargs.get("commit", True) + + with Image.open(self.instance.source) as image: + pil_image = generate_marker_from_image(image) + blob = BytesIO() + pil_image.save(blob, "JPEG") + filename = self.instance.source.name + self.instance.source.save(filename, File(blob), save=commit) + patt_str = generate_patt_from_image(image) + + self.instance.patt.save( + f"{filename}.patt", + ContentFile(patt_str.encode("utf-8")), + save=commit, + ) + + if kwargs.get("owner"): + self.instance.owner = kwargs.get("owner") + del kwargs["owner"] + + return super(UploadMarkerForm, self).save(*args, **kwargs) + + +class UploadObjectForm(forms.ModelForm): + def __init__(self, *args, **kwargs): + super(UploadObjectForm, self).__init__(*args, **kwargs) + + self.fields["source"].widget.attrs["placeholder"] = _("browse file") + self.fields["source"].widget.attrs["accept"] = "image/*, .mp4, .webm" + self.fields["author"].widget.attrs["placeholder"] = _("declare different author name") + self.fields["scale"].widget = HiddenInput() + self.fields["rotation"].widget = HiddenInput() + self.fields["position"].widget = HiddenInput() + self.fields["title"].widget.attrs["placeholder"] = _("Object's title") + log.warning(self.fields) + + class Meta: + model = Object + fields = ("source", "author", "title", "scale", "position", "rotation") + + def save(self, *args, **kwargs): + if owner := kwargs.get("owner", None): + self.instance.owner = owner + del kwargs["owner"] + + return super(UploadObjectForm, self).save(*args, **kwargs) + + +class ArtworkForm(forms.Form): + + marker = forms.ImageField(required=False) + marker_author = forms.CharField(max_length=12, required=False) + augmented = forms.ImageField(required=False) + augmented_author = forms.CharField(max_length=12, required=False) + existent_marker = forms.IntegerField(min_value=1, required=False) + existent_object = forms.IntegerField(min_value=1, required=False) + title = forms.CharField(max_length=50) + description = forms.CharField(widget=forms.Textarea, max_length=500, required=False) + + def __init__(self, *args, **kwargs): + super(ArtworkForm, self).__init__(*args, **kwargs) + + self.fields["marker_author"].widget.attrs["placeholder"] = _("declare different author name") + self.fields["augmented_author"].widget.attrs["placeholder"] = _("declare different author name") + self.fields["title"].widget.attrs["placeholder"] = _("Artwork title") + self.fields["description"].widget.attrs["placeholder"] = _("Artwork description") + + +class ExhibitForm(forms.Form): + + name = forms.CharField(max_length=50, required=True) + slug = forms.CharField(max_length=50, required=True) + + # FIXME: maybe this can be improved. Possible bug on max artworks per exhibit + artworks = forms.CharField(max_length=1000) + + def clean_slug(self): + data = self.cleaned_data["slug"] + if not re.match("^[a-zA-Z0-9_]*$", data): + raise forms.ValidationError(_("Url can't contain spaces or special characters")) + return data + + def __init__(self, *args, **kwargs): + super(ExhibitForm, self).__init__(*args, **kwargs) + + self.fields["name"].widget.attrs["placeholder"] = _("Exhibit Title") + self.fields["slug"].widget.attrs["placeholder"] = _("Complete with your Exhibit URL here") diff --git a/src/ARte/users/jinja2/users/artwork-edit.jinja2 b/src/users/jinja2/users/artwork.jinja2 similarity index 55% rename from src/ARte/users/jinja2/users/artwork-edit.jinja2 rename to src/users/jinja2/users/artwork.jinja2 index 85241a4e..c8278075 100644 --- a/src/ARte/users/jinja2/users/artwork-edit.jinja2 +++ b/src/users/jinja2/users/artwork.jinja2 @@ -4,11 +4,11 @@ {% block content %}
{# FIXME: maybe this can be improved #} - + -

{{ _('Edit Jandig Artwork') }}

+

{{ _('Edit Jandig Artwork') if selected_marker else _('Create Jandig Artwork')}}

@@ -37,7 +37,7 @@

- @@ -58,7 +58,7 @@ {{ _("I agree to share this content under CC BY-SA 4.0 and I'm aware that, once uploaded, I cannot remove it. ") }}

- @@ -72,119 +72,94 @@ {{ form.visible_fields()[7] }} {{ form.visible_fields()[7].errors }}

- + {% endblock %} \ No newline at end of file diff --git a/src/ARte/users/jinja2/users/components/artworks-list.jinja2 b/src/users/jinja2/users/components/artworks-list.jinja2 similarity index 56% rename from src/ARte/users/jinja2/users/components/artworks-list.jinja2 rename to src/users/jinja2/users/components/artworks-list.jinja2 index 35dcdfb4..594e0b1a 100644 --- a/src/ARte/users/jinja2/users/components/artworks-list.jinja2 +++ b/src/users/jinja2/users/components/artworks-list.jinja2 @@ -8,7 +8,6 @@ {% endblock %} \ No newline at end of file diff --git a/src/ARte/users/jinja2/users/components/createbox.jinja2 b/src/users/jinja2/users/components/createbox.jinja2 similarity index 100% rename from src/ARte/users/jinja2/users/components/createbox.jinja2 rename to src/users/jinja2/users/components/createbox.jinja2 diff --git a/src/ARte/users/jinja2/users/components/elements-modal.jinja2 b/src/users/jinja2/users/components/elements-modal.jinja2 similarity index 93% rename from src/ARte/users/jinja2/users/components/elements-modal.jinja2 rename to src/users/jinja2/users/components/elements-modal.jinja2 index 0d271352..d06d1d9e 100644 --- a/src/ARte/users/jinja2/users/components/elements-modal.jinja2 +++ b/src/users/jinja2/users/components/elements-modal.jinja2 @@ -7,15 +7,16 @@ {% endblock %} {% block content %} -

+

Download file:

+ + {% if form_type == 'marker' and edit == False %} +

{{ _('Upload Marker') }}

+ {% endif %} +
+
+
+ {{ csrf_input }} +

+

+

{{_("Choose Marker's title")}} + {{ form.visible_fields()[2] }} + {{ form.visible_fields()[2].errors }} +

+

{{ _("Choose Marker image") }}

+ {{ form.visible_fields()[0] }} + {{ form.visible_fields()[0].errors }} +

+ +
+ +

+ + + {% if form_type == 'marker' %} + + {% endif %} + +

+

+ {{ form.visible_fields()[1] }} + {{ form.visible_fields()[1].errors }} +

+
+

+ + +

+
+ +
+
+
+ + + +{% endblock %} \ No newline at end of file diff --git a/src/users/jinja2/users/upload-object.jinja2 b/src/users/jinja2/users/upload-object.jinja2 new file mode 100644 index 00000000..a01fa45e --- /dev/null +++ b/src/users/jinja2/users/upload-object.jinja2 @@ -0,0 +1,184 @@ + +{% extends '/core/home.jinja2' %} + +{% block content %} +
+ {# FIXME: maybe this can be improved #} + + + {% if form_type == 'object' and edit == False %} +

{{ _('Upload Object') }}

+ {% else %} +

{{_('Edit_object')}}

+ {% endif %} +
+
+
+ {{ csrf_input }} + {% if form_type == 'object' %} +

+

{{_("Choose Object's title")}} + {{ form.visible_fields()[2] }} + {{ form.visible_fields()[2].errors }} +

+ {% endif %} +

+ {%if route == 'object-upload' %} +

{{_("Choose Object")}}

+ {% endif %} + {{ form.visible_fields()[0] }} + {{ form.visible_fields()[0].errors }} +

+ + + {% if form_type == 'object' %} +
+

{{_("Adjust scale")}}

+ + ? + + + +

{{_("Adjust position")}}

+ + ? + + + {% with xposition=0, yposition=0 %} + {% include "users/components/object-position.jinja2" %} + {% endwith %} + + {{ form.hidden_fields()[0] }} + {{ form.hidden_fields()[0].errors }} + {{ form.hidden_fields()[1] }} + {{ form.hidden_fields()[1].errors }} + {{ form.hidden_fields()[2] }} + {{ form.hidden_fields()[2].errors }} + +
+ {% endif %} +

+ + + {% if form_type == 'object' %} + + {% endif %} + +

+

+ {{ form.visible_fields()[1] }} + {{ form.visible_fields()[1].errors }} +

+
+

+ + +

+
+ +
+
+
+ + +
+{% endblock %} \ No newline at end of file diff --git a/src/ARte/users/jinja2/users/upload.jinja2 b/src/users/jinja2/users/upload.jinja2 similarity index 91% rename from src/ARte/users/jinja2/users/upload.jinja2 rename to src/users/jinja2/users/upload.jinja2 index 80756ebc..6899cd61 100644 --- a/src/ARte/users/jinja2/users/upload.jinja2 +++ b/src/users/jinja2/users/upload.jinja2 @@ -55,9 +55,9 @@

{{_("Adjust position")}}

- ? @@ -84,14 +84,13 @@ {% endif %}

- {% if form_type == 'marker' %} - {{ _("I'm this Marker author") }} + {% elif form_type == 'object' %} - {{ _("I'm this Object author") }} - {% endif %} + + {% endif %}

{{ form.visible_fields()[1] }} @@ -100,7 +99,9 @@

- {{ _('I agree to have this content under CC BY-SA 4.0 and I\'m aware that it can\'t be removed after other people are using it.') }} +

@@ -132,7 +133,7 @@ $("#id_source").change( function(e) { - var previous_content_box = document.getElementById('content-box'); + var previous_content_box = document.getElementById('content-box'); if(previous_content_box){ previous_content_box.remove() } @@ -146,23 +147,23 @@ content_box.style.position = "relative" var close_icon = document.createElement("div"); - close_icon.id = "close-icon"; - close_icon.style.position = "absolute" + close_icon.id = "close-icon"; + close_icon.style.position = "absolute" close_icon.style.top = "10px" close_icon.style.right = "10px" - close_icon.style.padding = "5px 10px" + close_icon.style.padding = "5px 10px" close_icon.style.borderRadius= "5px" close_icon.style.cursor= "pointer" close_icon.style.color = "white" close_icon.style.background = "#ff6961" close_icon.innerHTML = "x" close_icon.onclick = function(){ - this.parentElement.remove(); + this.parentElement.remove(); document.querySelector('input[type="file"]').value = "" } content_box.appendChild(close_icon); - + if (file.type === "video/mp4" || file.type === "video/webm") { image_preview = document.createElement("video"); @@ -217,7 +218,7 @@ var ypos = document.getElementById('Y-position').value; var scl = scal*xprop + " " + scal*yprop; var rot = "270 0 0"; - var pos = xpos + " " + ypos + " 0"; //messing with the Z gives some really weird things + var pos = xpos/100 + " " + ypos/100 + " 0"; //messing with the Z gives some really weird things $('#id_scale').val(scl); $('#id_rotation').val(rot); diff --git a/src/ARte/users/jinja2/users/wrong-verification-code.jinja2 b/src/users/jinja2/users/wrong-verification-code.jinja2 similarity index 100% rename from src/ARte/users/jinja2/users/wrong-verification-code.jinja2 rename to src/users/jinja2/users/wrong-verification-code.jinja2 diff --git a/src/ARte/users/migrations/0001_initial.py b/src/users/migrations/0001_initial.py similarity index 100% rename from src/ARte/users/migrations/0001_initial.py rename to src/users/migrations/0001_initial.py diff --git a/src/ARte/users/migrations/0002_auto_20190609_0203.py b/src/users/migrations/0002_auto_20190609_0203.py similarity index 100% rename from src/ARte/users/migrations/0002_auto_20190609_0203.py rename to src/users/migrations/0002_auto_20190609_0203.py diff --git a/src/ARte/users/migrations/0003_artwork_created_at.py b/src/users/migrations/0003_artwork_created_at.py similarity index 100% rename from src/ARte/users/migrations/0003_artwork_created_at.py rename to src/users/migrations/0003_artwork_created_at.py diff --git a/src/ARte/users/migrations/0004_auto_20190715_2232.py b/src/users/migrations/0004_auto_20190715_2232.py similarity index 100% rename from src/ARte/users/migrations/0004_auto_20190715_2232.py rename to src/users/migrations/0004_auto_20190715_2232.py diff --git a/src/ARte/users/migrations/0005_object_source_20200130_1904.py b/src/users/migrations/0005_object_source_20200130_1904.py similarity index 100% rename from src/ARte/users/migrations/0005_object_source_20200130_1904.py rename to src/users/migrations/0005_object_source_20200130_1904.py diff --git a/src/ARte/users/migrations/0006_auto_20200213_1934.py b/src/users/migrations/0006_auto_20200213_1934.py similarity index 100% rename from src/ARte/users/migrations/0006_auto_20200213_1934.py rename to src/users/migrations/0006_auto_20200213_1934.py diff --git a/src/ARte/users/migrations/0007_auto_20200220_1425.py b/src/users/migrations/0007_auto_20200220_1425.py similarity index 100% rename from src/ARte/users/migrations/0007_auto_20200220_1425.py rename to src/users/migrations/0007_auto_20200220_1425.py diff --git a/src/ARte/users/migrations/0008_rename_model_tables.py b/src/users/migrations/0008_rename_model_tables.py similarity index 100% rename from src/ARte/users/migrations/0008_rename_model_tables.py rename to src/users/migrations/0008_rename_model_tables.py diff --git a/src/users/migrations/0009_alter_profile_id.py b/src/users/migrations/0009_alter_profile_id.py new file mode 100644 index 00000000..d1d86985 --- /dev/null +++ b/src/users/migrations/0009_alter_profile_id.py @@ -0,0 +1,20 @@ +# Generated by Django 4.1.2 on 2022-10-30 19:33 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("users", "0008_rename_model_tables"), + ("core", "0008_alter_artwork_author_alter_marker_owner_and_more"), + ] + operations = [ + migrations.AlterField( + model_name="profile", + name="id", + field=models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ] diff --git a/src/users/migrations/__init__.py b/src/users/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/ARte/users/models.py b/src/users/models.py similarity index 85% rename from src/ARte/users/models.py rename to src/users/models.py index 9790c8fc..9a781a0b 100644 --- a/src/ARte/users/models.py +++ b/src/users/models.py @@ -1,12 +1,11 @@ -from django.db import models from django.contrib.auth.models import User -from django.db.models.signals import post_save, post_delete +from django.db import models +from django.db.models.signals import post_save from django.dispatch import receiver -from django.core.files.storage import default_storage -import re from .choices import COUNTRY_CHOICES + class Profile(models.Model): user = models.OneToOneField(User, on_delete=models.DO_NOTHING) bio = models.TextField(max_length=500, blank=True) @@ -21,9 +20,10 @@ class Meta: ("moderator", "Can moderate content"), ] + @receiver(post_save, sender=User) def create_user_profile(sender, instance, created, **kwargs): - if created: + if created: Profile.objects.create(user=instance) diff --git a/src/users/services/__init__.py b/src/users/services/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/ARte/users/services/email_service.py b/src/users/services/email_service.py similarity index 64% rename from src/ARte/users/services/email_service.py rename to src/users/services/email_service.py index d7eb1e41..7c5c63b6 100644 --- a/src/ARte/users/services/email_service.py +++ b/src/users/services/email_service.py @@ -1,21 +1,22 @@ import smtplib -import os from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText +from django.conf import settings + class EmailService: def __init__(self, email_message): - self.smtp_server = os.environ.get("SMTP_SERVER", "smtp.gmail.com") - self.smtp_port = int(os.environ.get("SMTP_PORT", 587)) - self.jandig_email = os.environ["JANDIG_EMAIL"] - self.jandig_email_password = os.environ["JANDIG_EMAIL_PASSWORD"] + self.smtp_server = settings.SMTP_SERVER + self.smtp_port = settings.SMTP_PORT + self.jandig_email = settings.JANDIG_EMAIL + self.jandig_email_password = settings.JANDIG_EMAIL_PASSWORD self.email_message = email_message def send_email_to_recover_password(self, multipart_message): email_server = smtplib.SMTP(self.smtp_server, self.smtp_port) email_server.starttls() - email_server.login(multipart_message["From"], self.jandig_email_password) + email_server.login(self.jandig_email, self.jandig_email_password) email_server.sendmail( multipart_message["From"], multipart_message["To"], @@ -24,8 +25,8 @@ def send_email_to_recover_password(self, multipart_message): email_server.quit() def build_multipart_message(self, user_email): - multipart_message = MIMEMultipart() - multipart_message["From"] = self.jandig_email + multipart_message = MIMEMultipart("alternative") + multipart_message["From"] = f"Jandig <{self.jandig_email}>" multipart_message["To"] = "{}".format(user_email) multipart_message["Subject"] = "Recover Password" diff --git a/src/users/services/encrypt_service.py b/src/users/services/encrypt_service.py new file mode 100644 index 00000000..6a8e0d1d --- /dev/null +++ b/src/users/services/encrypt_service.py @@ -0,0 +1,24 @@ +import hashlib +import secrets +from datetime import datetime + + +class EncryptService: + def generate_verification_code(self, email): + datetime_now = datetime.now() + _year = datetime_now.year + _month = datetime_now.month + _day = datetime_now.day + _hour = datetime_now.hour + _minute = datetime_now.minute + _second = datetime_now.second + _microsec = datetime_now.microsecond + + today = f"{_year}{_month}{_day}{_hour}{_minute}{_second}{_microsec}" + decrypt_code = str(today) + (email * 4) + secrets.token_hex(16) + verification_code = self.generate_hash_code(decrypt_code) + return verification_code + + def generate_hash_code(self, decrypt_code): + hash_code = hashlib.sha256(bytes(decrypt_code, encoding="utf-8")) + return hash_code.hexdigest() diff --git a/src/ARte/users/services/user_service.py b/src/users/services/user_service.py similarity index 79% rename from src/ARte/users/services/user_service.py rename to src/users/services/user_service.py index 77408755..b79df83e 100644 --- a/src/ARte/users/services/user_service.py +++ b/src/users/services/user_service.py @@ -1,22 +1,23 @@ import logging -log = logging.getLogger('ej') from django.contrib.auth.models import User +log = logging.getLogger("ej") -class UserService(): + +class UserService: def get_user_email(self, username_or_email): - if '@' in username_or_email: + if "@" in username_or_email: return username_or_email user = User.objects.get(username=username_or_email) log.warning(user) return user.email def check_if_username_or_email_exist(self, username_or_email): - if '@' in username_or_email: + if "@" in username_or_email: if not User.objects.filter(email=username_or_email).exists(): return False else: if not User.objects.filter(username=username_or_email).exists(): return False - return True \ No newline at end of file + return True diff --git a/src/ARte/users/static/css/elements-modal.css b/src/users/static/css/elements-modal.css similarity index 100% rename from src/ARte/users/static/css/elements-modal.css rename to src/users/static/css/elements-modal.css diff --git a/src/ARte/users/static/css/generic-modal.css b/src/users/static/css/generic-modal.css similarity index 100% rename from src/ARte/users/static/css/generic-modal.css rename to src/users/static/css/generic-modal.css diff --git a/src/ARte/users/static/css/label.css b/src/users/static/css/label.css similarity index 100% rename from src/ARte/users/static/css/label.css rename to src/users/static/css/label.css diff --git a/src/ARte/users/static/css/login.css b/src/users/static/css/login.css similarity index 100% rename from src/ARte/users/static/css/login.css rename to src/users/static/css/login.css diff --git a/src/ARte/users/static/css/marker-creation.css b/src/users/static/css/marker-creation.css similarity index 100% rename from src/ARte/users/static/css/marker-creation.css rename to src/users/static/css/marker-creation.css diff --git a/src/ARte/users/static/css/object-position.css b/src/users/static/css/object-position.css similarity index 98% rename from src/ARte/users/static/css/object-position.css rename to src/users/static/css/object-position.css index cdb68d0b..7c45c1b5 100644 --- a/src/ARte/users/static/css/object-position.css +++ b/src/users/static/css/object-position.css @@ -8,6 +8,7 @@ opacity: 0.7; -webkit-transition: .2s; transition: opacity .2s; + padding:0px } .slider::-webkit-slider-thumb { @@ -39,7 +40,6 @@ display: flex; justify-content: center; align-items: flex-end; - margin: 20px; } #box-shadow { diff --git a/src/ARte/users/static/css/profile.css b/src/users/static/css/profile.css similarity index 98% rename from src/ARte/users/static/css/profile.css rename to src/users/static/css/profile.css index 723893c4..e25dfac8 100644 --- a/src/ARte/users/static/css/profile.css +++ b/src/users/static/css/profile.css @@ -36,6 +36,7 @@ line-height: 25px; font-size: 0.85em; margin-bottom: 0; + white-space: nowrap; } .bdgArt { @@ -188,6 +189,7 @@ font-size: 0.85em; margin-bottom: 0; flex-basis: calc(50% - 70px); + white-space: nowrap; } .bdgArt,.bdgCur,.bdgArt.empty, .bdgCur.empty { diff --git a/src/ARte/users/static/css/repository-list.css b/src/users/static/css/repository-list.css similarity index 100% rename from src/ARte/users/static/css/repository-list.css rename to src/users/static/css/repository-list.css diff --git a/src/ARte/users/static/css/signup.css b/src/users/static/css/signup.css similarity index 96% rename from src/ARte/users/static/css/signup.css rename to src/users/static/css/signup.css index 6f8d4ae9..232de35d 100644 --- a/src/ARte/users/static/css/signup.css +++ b/src/users/static/css/signup.css @@ -42,6 +42,10 @@ select#id_exhibit { padding-left: 5%; } +.errorlist{ + color: red ; +} + .form-options input{ width: 20px; height: 20px; diff --git a/src/ARte/users/static/css/upload.css b/src/users/static/css/upload.css similarity index 88% rename from src/ARte/users/static/css/upload.css rename to src/users/static/css/upload.css index fb57c41d..d1c88dd9 100644 --- a/src/ARte/users/static/css/upload.css +++ b/src/users/static/css/upload.css @@ -77,6 +77,19 @@ video{ max-height: 100%; } +div#imageContainer { + max-width: 320px; + height: 320px; + margin: 25px auto; + background: url(../images/icons/icoEptG.png) no-repeat center center; + background-size: cover; +} + +div#imageContainer img { + max-width: 100%; + height: auto; +} + @media only screen and (max-width: 600px){ #content-box { height: 60%; diff --git a/src/users/tests/__init__.py b/src/users/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/users/tests/test_users.py b/src/users/tests/test_users.py new file mode 100644 index 00000000..0bb37a02 --- /dev/null +++ b/src/users/tests/test_users.py @@ -0,0 +1,97 @@ +from unittest import mock + +from django.test import RequestFactory, TestCase + +from users.factory import UserFactory +from users.services.email_service import EmailService +from users.services.encrypt_service import EncryptService +from users.services.user_service import UserService +from users.views import recover_password + + +@mock.patch("smtplib.SMTP.quit") +@mock.patch("smtplib.SMTP.login", side_effect=lambda *args, **kwargs: None) +@mock.patch("smtplib.SMTP.sendmail", side_effect=lambda *args, **kwargs: None) +class UserTestCase(TestCase): + def setUp(self): + self.client_test = RequestFactory() + self.email_service = EmailService("You have requested a new password.") + self.encrypt_service = EncryptService() + self.user_service = UserService() + + def test_redirect_to_recover_password_page(self, *args, **kwargs): + request = self.client_test.get("/recover/", follow=True) + response = recover_password(request) + self.assertEqual(response.status_code, 200) + + def test_recover_password_invalid_email(self, *args, **kwargs): + request = self.client_test.post( + "/recover/", + {"username_or_email": "testadorinvalid@memelab.com"}, + follow=True, + ) + response = recover_password(request) + self.assertEqual(response.status_code, 302) + self.assertEqual(response.url, "/users/invalid-recovering-email") + + def test_recover_password_invalid_username(self, *args, **kwargs): + request = self.client_test.post("/recover/", {"username_or_email": "testadorinvalid"}, follow=True) + response = recover_password(request) + self.assertEqual(response.status_code, 302) + self.assertEqual(response.url, "/users/invalid-recovering-email") + + def test_recover_password_valid_email(self, *args, **kwargs): + request = self.client_test.post("/recover/", {"username_or_email": "testador@memelab.com"}, follow=True) + UserFactory() + response = recover_password(request) + self.assertEqual(response.status_code, 302) + self.assertEqual(response.url, "/users/recover-code/") + + def test_recover_password_valid_username(self, *args, **kwargs): + request = self.client_test.post("/recover/", {"username_or_email": "Testador"}, follow=True) + UserFactory() + response = recover_password(request) + self.assertEqual(response.status_code, 302) + self.assertEqual(response.url, "/users/recover-code/") + + def test_build_multipart_message(self, *args, **kwargs): + email = "testador@memelab.com" + response = self.email_service.build_multipart_message(email) + self.assertEqual(response["From"], "Jandig ") + self.assertEqual(response["To"], email) + + def test_send_email_to_recover_password(self, mock_quit, *args, **kwargs): + email = "testador@memelab.com" + response = self.email_service.build_multipart_message(email) + self.email_service.send_email_to_recover_password(response) + mock_quit.assert_called_once() + + @mock.patch("users.services.encrypt_service.EncryptService.generate_hash_code") + def test_generate_verification_code(self, mock_hash, *args, **kwargs): + email = "testador@memelab.com" + self.encrypt_service.generate_verification_code(email) + mock_hash.assert_called_once() + + def test_check_if_username_or_email_exist(self, *args, **kwargs): + email = "testador@memelab.com" + UserFactory() + response = self.user_service.check_if_username_or_email_exist(email) + self.assertTrue(response) + + def test_check_if_username_or_email_doesnt_exist(self, *args, **kwargs): + email = "testadorinvalido@memelab.com" + UserFactory() + response = self.user_service.check_if_username_or_email_exist(email) + self.assertFalse(response) + + def test_get_user_email_by_email(self, *args, **kwargs): + email = "testador@memelab.com" + UserFactory() + response = self.user_service.get_user_email(email) + self.assertEqual(response, email) + + def test_get_user_email_by_username(self, *args, **kwargs): + username = "Testador" + UserFactory() + response = self.user_service.get_user_email(username) + self.assertEqual(response, "testador@memelab.com") diff --git a/src/users/urls.py b/src/users/urls.py new file mode 100644 index 00000000..a4fcb038 --- /dev/null +++ b/src/users/urls.py @@ -0,0 +1,74 @@ +from django.contrib.auth import views as auth_views +from django.urls import path + +from .forms import LoginForm +from .views import ( + create_artwork, + create_exhibit, + delete, + download_exhibit, + edit_artwork, + edit_exhibit, + edit_marker, + edit_object, + edit_password, + edit_profile, + element_get, + invalid_recovering_email_or_username, + marker_upload, + mod, + mod_delete, + object_upload, + permission_denied, + profile, + recover_code, + recover_edit_password, + recover_password, + related_content, + signup, + wrong_verification_code, +) + +urlpatterns = [ + path("signup/", signup, name="signup"), + path( + "login/", + auth_views.LoginView.as_view( + template_name="users/login.jinja2", + authentication_form=LoginForm, + ), + name="login", + ), + path("logout/", auth_views.LogoutView.as_view(), name="logout"), + path("recover/", recover_password, name="recover"), + path("recover-code/", recover_code, name="recover-code"), + path("profile/", profile, name="profile"), + path("profile/edit/", edit_profile, name="edit-profile"), + path("profile/edit-password/", edit_password, name="edit-password"), + path( + "wrong-verification-code", + wrong_verification_code, + name="wrong-verification-code", + ), + path( + "invalid-recovering-email", + invalid_recovering_email_or_username, + name="invalid_recovering_email_or_username", + ), + path("recover-edit-password", recover_edit_password, name="recover-edit-password"), + path("markers/upload/", marker_upload, name="marker-upload"), + path("objects/upload/", object_upload, name="object-upload"), + path("element/get/", element_get, name="element-get"), + path("objects/edit/", edit_object, name="edit-object"), + path("markers/edit/", edit_marker, name="edit-marker"), + path("artworks/create/", create_artwork, name="create-artwork"), + path("artworks/edit/", edit_artwork, name="edit-artwork"), + path("exhibits/create/", create_exhibit, name="create-exhibit"), + path("exhibits/edit/", edit_exhibit, name="edit-exhibit"), + path("download-exhibit", download_exhibit, name="download-exhibit"), + path("content/delete/", delete, name="delete-content"), + path("moderator-page/", mod, name="moderator-page"), + path("permission-denied/", permission_denied, name="permission-denied"), + path("content/mod-delete/", mod_delete, name="mod-delete-content"), + path("related-content", related_content, name="related-content"), +] diff --git a/src/users/views.py b/src/users/views.py new file mode 100644 index 00000000..76b5bdb0 --- /dev/null +++ b/src/users/views.py @@ -0,0 +1,726 @@ +import json +import logging + +from django.contrib.auth import ( + authenticate, + get_user_model, + login, + update_session_auth_hash, +) +from django.contrib.auth.decorators import login_required +from django.contrib.auth.forms import SetPasswordForm +from django.http import Http404, JsonResponse +from django.shortcuts import get_object_or_404, redirect, render +from django.views.decorators.cache import cache_page +from django.views.decorators.http import require_http_methods + +from core.models import Artwork, Exhibit, Marker, Object + +from .forms import ( + ArtworkForm, + ExhibitForm, + PasswordChangeForm, + ProfileForm, + RecoverPasswordCodeForm, + RecoverPasswordForm, + SignupForm, + UploadMarkerForm, + UploadObjectForm, +) +from .models import Profile +from .services.email_service import EmailService +from .services.encrypt_service import EncryptService +from .services.user_service import UserService + +log = logging.getLogger("ej") + + +def signup(request): + if request.method == "POST": + form = SignupForm(request.POST) + + if form.is_valid(): + form.save() + username = form.cleaned_data.get("username") + raw_password = form.cleaned_data.get("password1") + user = authenticate(username=username, password=raw_password) + login(request, user) + return redirect("home") + + else: + form = SignupForm() + + return render(request, "users/signup.jinja2", {"form": form}) + + +User = get_user_model() + + +def recover_password(request): + if request.method == "POST": + recover_password_form = RecoverPasswordForm(request.POST) + + if recover_password_form.is_valid(): + username_or_email = recover_password_form.cleaned_data.get("username_or_email") + user_service = UserService() + username_or_email_is_valid = user_service.check_if_username_or_email_exist(username_or_email) + if not username_or_email_is_valid: + return redirect("invalid_recovering_email_or_username") + + global global_recovering_email + global_recovering_email = user_service.get_user_email(username_or_email) + + global global_verification_code + encrypt_service = EncryptService() + global_verification_code = encrypt_service.generate_verification_code(global_recovering_email) + + build_message_and_send_to_user(global_recovering_email) + + return redirect("recover-code") + + recover_password_form = RecoverPasswordForm() + return render(request, "users/recover-password.jinja2", {"form": recover_password_form}) + + +def build_message_and_send_to_user(email): + message = f"You have requested a new password. This is your verification code: {global_verification_code}\nCopy it and put into the field." + email_service = EmailService(message) + multipart_message = email_service.build_multipart_message(email) + email_service.send_email_to_recover_password(multipart_message) + + +def recover_code(request): + if request.method == "POST": + form = RecoverPasswordCodeForm(request.POST) + + if form.is_valid(): + code = form.cleaned_data.get("verification_code") + + log.warning("Inserido: %s", code) + log.warning("Correto: %s", global_verification_code) + + if code == global_verification_code: + global recover_password_user + recover_password_user = User.objects.get(email=global_recovering_email) + return redirect("recover-edit-password") + + return redirect("wrong-verification-code") + return redirect("home") + + form = RecoverPasswordCodeForm() + return render(request, "users/recover-password-code.jinja2", {"form": form}) + + +def recover_edit_password(request): + if request.method == "POST": + form = SetPasswordForm(recover_password_user, data=request.POST) + + if form.is_valid(): + form.save() + + return redirect("login") + else: + form = SetPasswordForm(recover_password_user) + + return render(request, "users/recover-edit-password.jinja2", {"form": form}) + + +@require_http_methods(["GET"]) +def wrong_verification_code(request): + return render(request, "users/wrong-verification-code.jinja2") + + +@require_http_methods(["GET"]) +def invalid_recovering_email_or_username(request): + return render(request, "users/invalid-recovering-email.jinja2") + + +@login_required +@require_http_methods(["GET"]) +def profile(request): + + user = request.GET.get("user") + + if not user: + user = request.user + + profile = Profile.objects.prefetch_related("exhibits", "markers", "ar_objects", "artworks").get(user=user) + + exhibits = profile.exhibits.all() + markers = profile.markers.all() + objects = profile.ar_objects.all() + artworks = profile.artworks.all() + + ctx = { + "exhibits": exhibits, + "artworks": artworks, + "markers": markers, + "objects": objects, + "profile": True, + "button_enable": False if user else True, + } + return render(request, "users/profile.jinja2", ctx) + + +@cache_page(60 * 60) +def get_element(request, form, form_class, form_type, source, author, existent_element): + element = None + + if source and author: + instance = form_type(source=source, author=author) + element = form_class(instance=instance).save(commit=False) + element.save() + elif existent_element: + qs = form_type.objects.filter(id=existent_element) + if qs: + element = qs[0] + element.owner = request.user.profile + + return element + + +@cache_page(60 * 60) +def get_marker(request, form): + marker_src = form.cleaned_data["marker"] + marker_author = form.cleaned_data["marker_author"] + existent_marker = form.cleaned_data["existent_marker"] + + return get_element( + request, + form, + UploadMarkerForm, + Marker, + source=marker_src, + author=marker_author, + existent_element=existent_marker, + ) + + +@cache_page(60 * 60) +def get_augmented(request, form): + object_src = form.cleaned_data["augmented"] + object_author = form.cleaned_data["augmented_author"] + existent_object = form.cleaned_data["existent_object"] + + return get_element( + request, + form, + UploadObjectForm, + Object, + source=object_src, + author=object_author, + existent_element=existent_object, + ) + + +@login_required +def create_artwork(request): + if request.method == "POST": + form = ArtworkForm(request.POST, request.FILES) + + if form.is_valid(): + + marker = get_marker(request, form) + augmented = get_augmented(request, form) + + if marker and augmented: + artwork_title = form.cleaned_data["title"] + artwork_desc = form.cleaned_data["description"] + Artwork( + author=request.user.profile, + marker=marker, + augmented=augmented, + title=artwork_title, + description=artwork_desc, + ).save() + return redirect("home") + else: + form = ArtworkForm() + + marker_list = Marker.objects.all() + object_list = Object.objects.all() + + return render( + request, + "users/artwork.jinja2", + { + "form": form, + "marker_list": marker_list, + "object_list": object_list, + }, + ) + + +@login_required +def create_exhibit(request): + if request.method == "POST": + form = ExhibitForm(request.POST) + if form.is_valid(): + ids = form.cleaned_data["artworks"].split(",") + artworks = Artwork.objects.filter(id__in=ids).order_by("-id") + exhibit = Exhibit( + owner=request.user.profile, + name=form.cleaned_data["name"], + slug=form.cleaned_data["slug"], + ) + + exhibit.save() + exhibit.artworks.set(artworks) + + return redirect("home") + else: + form = ExhibitForm() + + artworks = Artwork.objects.all().order_by("-id") + + return render( + request, + "users/exhibit-create.jinja2", + { + "form": form, + "artworks": artworks, + }, + ) + + +@require_http_methods(["GET"]) +def download_exhibit(request): + exhibit_id = request.GET.get("id") + exhibit = Exhibit.objects.get(id=exhibit_id) + artworks = list(exhibit.artworks.all()) + + marker_names = [] + object_names = [] + patt_names = [] + + all_data = [] + + for artwork in artworks: + marker_names.append(artwork.marker.source.name) + object_names.append(artwork.augmented.source.name) + patt_names.append(str(artwork.marker.patt)) + + for marker_name in marker_names: + data = {"link": marker_name} + + all_data.append(data) + + for object_name in object_names: + data = {"link": object_name} + + all_data.append(data) + + for patt_name in patt_names: + data = {"link": patt_name} + + all_data.append(data) + + return JsonResponse(all_data) + + +@cache_page(60 * 2) +@require_http_methods(["GET"]) +def element_get(request): + if request.GET.get("marker_id", None): + element_type = "marker" + element = get_object_or_404(Marker, pk=request.GET["marker_id"]) + elif request.GET.get("object_id", None): + element_type = "object" + element = get_object_or_404(Object, pk=request.GET["object_id"]) + elif request.GET.get("artwork_id", None): + element_type = "artwork" + element = get_object_or_404(Artwork, pk=request.GET["artwork_id"]) + + if element_type == "artwork": + data = { + "id_marker": element.marker.id, + "id_object": element.augmented.id, + "type": element_type, + "author": element.author.user.username, + "owner_id": element.author.user.id, + "exhibits": element.exhibits_count, + "created_at": element.created_at.strftime("%d %b, %Y"), + "marker": element.marker.source.url, + "augmented": element.augmented.source.url, + "augmented_size": element.augmented.source.size, + "title": element.title, + "description": element.description, + } + else: + data = { + "id": element.id, + "type": element_type, + "author": element.author, + "owner": element.owner.user.username, + "owner_id": element.owner_id, + "artworks": element.artworks_count, + "exhibits": element.exhibits_count, + "source": element.source.url, + "size": element.source.size, + "uploaded_at": element.uploaded_at.strftime("%d %b, %Y"), + } + + serialized = json.dumps(data) + + return JsonResponse(serialized) + + +def upload_elements(request, form_class, form_type, route): + if request.method == "POST": + form = form_class(request.POST, request.FILES) + if form.is_valid(): + upload = form.save(commit=False) + upload.owner = request.user.profile + upload.save() + return redirect("home") + else: + form = form_class() + + if form_type == "marker": + return render( + request, + "users/upload-marker.jinja2", + {"form_type": form_type, "form": form, "route": route, "edit": False}, + ) + + return render( + request, + "users/upload-object.jinja2", + {"form_type": form_type, "form": form, "route": route, "edit": False}, + ) + + +@login_required +def object_upload(request): + return upload_elements(request, UploadObjectForm, "object", "object-upload") + + +@login_required +def marker_upload(request): + return upload_elements(request, UploadMarkerForm, "marker", "marker-upload") + + +def edit_elements(request, form_class, route, model, model_data): + if not model or model.owner != Profile.objects.get(user=request.user): + raise Http404 + + if request.method == "POST": + form = form_class(request.POST, request.FILES, instance=model) + + form.full_clean() + if form.is_valid(): + form.save() + return redirect("profile") + + log.warning(form.errors) + + return render( + request, + route, + { + "form": form_class(initial=model_data), + "model": model, + }, + ) + + +@login_required +def edit_object(request): + index = request.GET.get("id", "-1") + model = Object.objects.get(id=index) + + model_data = { + "source": model.source, + "uploaded_at": model.uploaded_at, + "author": model.author, + "scale": model.scale, + "position": model.position, + "rotation": model.rotation, + "title": model.title, + } + return edit_elements( + request, + UploadObjectForm, + route="users/edit-object.jinja2", + model=model, + model_data=model_data, + ) + + +@login_required +def edit_marker(request): + index = request.GET.get("id", "-1") + model = Marker.objects.get(id=index) + + model_data = { + "source": model.source, + "uploaded_at": model.uploaded_at, + "author": model.author, + "patt": model.patt, + "title": model.title, + } + + return edit_elements( + request, + UploadMarkerForm, + route="users/edit-marker.jinja2", + model=model, + model_data=model_data, + ) + + +@login_required +def edit_artwork(request): + index = request.GET.get("id", "-1") + model = Artwork.objects.filter(id=index).order_by("-id") + if not model or model.first().author != Profile.objects.get(user=request.user): + raise Http404 + + if request.method == "POST": + form = ArtworkForm(request.POST, request.FILES) + + form.full_clean() + if form.is_valid(): + model_data = { + "marker": get_marker(request, form), + "augmented": get_augmented(request, form), + "title": form.cleaned_data["title"], + "description": form.cleaned_data["description"], + } + print(model_data["augmented"]) + model.update(**model_data) + return redirect("profile") + + model = model.first() + model_data = { + "marker": model.marker, + "marker_author": model.marker.author, + "augmented": model.augmented, + "augmented_author": model.augmented.author, + "title": model.title, + "description": model.description, + "existent_marker": model.marker.id, + "existent_object": model.augmented.id, + } + + return render( + request, + "users/artwork.jinja2", + { + "form": ArtworkForm(initial=model_data), + "marker_list": Marker.objects.all(), + "object_list": Object.objects.all(), + "selected_marker": model.marker.id, + "selected_object": model.augmented.id, + }, + ) + + +@login_required +def edit_exhibit(request): + index = request.GET.get("id", "-1") + model = Exhibit.objects.filter(id=index) + if not model or model.first().owner != Profile.objects.get(user=request.user): + raise Http404 + + if request.method == "POST": + form = ExhibitForm(request.POST) + + form.full_clean() + if form.is_valid(): + ids = form.cleaned_data["artworks"].split(",") + artworks = Artwork.objects.filter(id__in=ids).order_by("-id") + + model_data = { + "name": form.cleaned_data["name"], + "slug": form.cleaned_data["slug"], + } + model.update(**model_data) + model = model.first() + model.artworks.set(artworks) + + return redirect("profile") + + model = model.first() + model_artworks = "" + for artwork in model.artworks.all(): + model_artworks += str(artwork.id) + "," + + model_artworks = model_artworks[:-1] + + model_data = {"name": model.name, "slug": model.slug, "artworks": model_artworks} + + artworks = Artwork.objects.filter(author=request.user.profile).order_by("-id") + + return render( + request, + "users/exhibit-edit.jinja2", + { + "form": ExhibitForm(initial=model_data), + "artworks": artworks, + "selected_artworks": model_artworks, + }, + ) + + +@login_required +def edit_password(request): + if request.method == "POST": + form = PasswordChangeForm(request.user, data=request.POST) + if form.is_valid(): + form.save() + update_session_auth_hash(request, form.user) + return redirect("profile") + + profile = Profile.objects.get(user=request.user) + ctx = { + "form_password": PasswordChangeForm(request.user), + "form_profile": ProfileForm(instance=profile), + } + return render(request, "users/profile-edit.jinja2", ctx) + return Http404 + + +@login_required +def edit_profile(request): + profile = Profile.objects.get(user=request.user) + if request.method == "POST": + form = ProfileForm(request.POST, instance=profile) + + if form.is_valid(): + profile = form.save(commit=False) + user = profile.user + user.email = form.cleaned_data["email"] + user.username = form.cleaned_data["username"] + user.save(force_update=True) + profile.save() + + return redirect("profile") + else: + form = ProfileForm(instance=profile) + + return render( + request, + "users/profile-edit.jinja2", + { + "form_profile": form, + "form_password": PasswordChangeForm(request.user), + }, + ) + + +@login_required +@require_http_methods(["GET"]) +def delete(request): + content_type = request.GET.get("content_type", None) + + if content_type == "marker": + delete_content(Marker, request.user, request.GET.get("id", -1)) + elif content_type == "object": + delete_content(Object, request.user, request.GET.get("id", -1)) + elif content_type == "artwork": + delete_content(Artwork, request.user, request.GET.get("id", -1)) + elif content_type == "exhibit": + delete_content(Exhibit, request.user, request.GET.get("id", -1)) + return redirect("profile") + + +def delete_content(model, user, instance_id): + qs = model.objects.filter(id=instance_id) + + if qs: + instance = qs[0] + if user.has_perm("users.moderator"): + delete_content_Moderator(instance, user, model) + else: + isArtwork = isinstance(instance, Artwork) + if isArtwork: + hasPermission = instance.author == user.profile + else: + hasPermission = instance.owner == user.profile + + isInstanceSameTypeofModel = isinstance(instance, model) + if isInstanceSameTypeofModel and hasPermission: + instance.delete() + + +def delete_content_Moderator(instance, user, model): + isInstanceSameTypeofModel = isinstance(instance, model) + isObject = isinstance(instance, Object) + isMarker = isinstance(instance, Marker) + isArtwork = isinstance(instance, Artwork) + + if isInstanceSameTypeofModel or not instance.in_use: + instance.delete() + elif instance.in_use: + if isObject: + artworkIn = Artwork.objects.filter(augmented=instance) + artworkIn.delete() + instance.delete() + elif isMarker: + artworkIn = Artwork.objects.filter(marker=instance) + artworkIn.delete() + instance.delete() + elif isArtwork: + instance.delete() + + +@require_http_methods(["GET"]) +def related_content(request): + element_id = request.GET.get("id") + element_type = request.GET.get("type") + element = None + ctx = {} + + if element_type == "object": + element = Object.objects.get(id=element_id) + + artworks = element.artworks_list + exhibits = element.exhibits_list + + ctx = {"artworks": artworks, "exhibits": exhibits, "seeall:": False} + elif element_type == "marker": + element = Marker.objects.get(id=element_id) + + artworks = element.artworks_list + exhibits = element.exhibits_list + + ctx = {"artworks": artworks, "exhibits": exhibits, "seeall:": False} + elif element_type == "artwork": + element = Artwork.objects.get(id=element_id) + + exhibits = element.exhibits_list + + ctx = {"exhibits": exhibits, "seeall:": False} + + return render(request, "core/collection.jinja2", ctx) + + +@login_required +@require_http_methods(["GET"]) +def mod_delete(request): + content_type = request.GET.get("content_type", None) + if content_type == "marker": + delete_content(Marker, request.user, request.GET.get("instance_id", -1)) + elif content_type == "object": + delete_content(Object, request.user, request.GET.get("instance_id", -1)) + elif content_type == "artwork": + delete_content(Artwork, request.user, request.GET.get("instance_id", -1)) + elif content_type == "exhibit": + delete_content(Exhibit, request.user, request.GET.get("id", -1)) + return redirect("moderator-page") + + +def mod(request): + ctx = { + "objects": Object.objects.all(), + "markers": Marker.objects.all(), + "artworks": Artwork.objects.all().order_by("-id"), + "exhibits": Exhibit.objects.all(), + "permission": request.user.has_perm("users.moderator"), + } + return render(request, "users/moderator-page.jinja2", ctx) + + +def permission_denied(request): + return render(request, "users/permission-denied.jinja2") diff --git a/tasks.py b/tasks.py index 0879f47f..4194aa1f 100644 --- a/tasks.py +++ b/tasks.py @@ -4,7 +4,7 @@ python = sys.executable directory = os.path.dirname(__file__) -sys.path.append('src') +sys.path.append('jandig') # # Call python manage.py in a more robust way @@ -12,7 +12,7 @@ def robust_manage(ctx, cmd, env=None, **kwargs): kwargs = {k.replace('_', '-'): v for k, v in kwargs.items() if v is not False} opts = ' '.join(f'--{k} {"" if v is True else v}' for k, v in kwargs.items()) - cmd = f'{python} src/ARte/manage.py {cmd} {opts}' + cmd = f'{python} ./src/manage.py {cmd} {opts}' env = {**os.environ, **(env or {})} path = env.get("PYTHONPATH", ":".join(sys.path)) env.setdefault('PYTHONPATH', f'src:{path}') @@ -20,40 +20,32 @@ def robust_manage(ctx, cmd, env=None, **kwargs): ctx.run(cmd, pty=True, env=env) -def manage(ctx, cmd, postgres=False, whitenoise=False): - cmd = f'python3 src/ARte/manage.py {cmd}' - ctx.run(cmd, pty=True, env=default_env(postgres, whitenoise)) - - -def default_env(postgres, whitenoise): - os.environ['DEV_DB'] = 'True' if not postgres else 'False' - os.environ['DEV_STATIC'] = 'True' if whitenoise else 'False' - e = os.environ - return e +def manage(ctx, cmd): + cmd = f'python3 ./src/manage.py {cmd}' + ctx.run(cmd, pty=True, env=os.environ) @task -def run(ctx, ssl=False, gunicorn=False, postgres=False, whitenoise=False): +def run(ctx, ssl=False, gunicorn=False): """ Run development server """ if gunicorn: - ctx.run('cd src/ARte && gunicorn --worker-connections=10000 --workers=4 --log-level debug --bind 0.0.0.0:8000 config.wsgi', env={"DEV_DB":"False"}) + ctx.run('cd src && gunicorn --reload --worker-connections=10000 --workers=4 --log-level debug --bind 0.0.0.0:8000 config.wsgi') else: - manage(ctx, "runserver 0.0.0.0:8000", postgres, whitenoise) - + manage(ctx, "runserver 0.0.0.0:8000") @task -def db(ctx, make=False, postgres=False): +def db(ctx, make=False): """ Run migrations """ if make: - manage(ctx, "makemigrations", postgres) - manage(ctx, "migrate", postgres) + manage(ctx, "makemigrations") + manage(ctx, "migrate") else: - manage(ctx, "migrate", postgres) + manage(ctx, "migrate") @task @@ -64,46 +56,6 @@ def collect(ctx): manage(ctx, "collectstatic --no-input --clear") -@task -def install_deps(ctx): - """ - Install all dependencies - """ - ctx.run('pip3 install -r src/requirements.txt') - - -@task -def docker(ctx, build=False): - command = 'sudo docker-compose -f docker/docker-compose.yml up' - - if build: - command += ' --build' - - ctx.run(command) - - -@task -def build_base(ctx, publish=False): - """ - Build base docker images - """ - command = './src/etc/scripts/build-base.sh' - - if publish: - command += ' publish' - - ctx.run(command) - - -@task -def init_production(ctx): - """ - Init production environment - """ - command = './src/etc/scripts/init-production.sh' - ctx.run(command) - - # # Translations # @@ -139,8 +91,3 @@ def i18n(ctx, compile=False, edit=False, lang='pt_BR', keep_pot=False): @task def docs(ctx): ctx.run('sphinx-build docs/ build/') - - -#@task -#def populate(ctx): - # manage(ctx, 'populate_db')