diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 1080c51c757975e2b6ef8e3a9e05b1fc02f14818..ed4b7571de273d1c687b568c076f6b2ae164c8cb 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,3 +1,24 @@ -Fixes #ISSUEID +<!-- +Thanks for contributing to Hypha! -Add description here. +Please ensure your contributions pass all necessary linting/testing and that the appropriate documentation has been updated. +--> + +## Description +<!-- +Describe briefly what your pull request changes. If this is resoving an issue, please specify below via "Fixes #<Github Issue ID>" +--> +Fixes #ISSUEID. + + +## Test Steps +<!-- +If step does not require manual testing, skip/remove this section. + +Give a brief overview of the steps required for a user/dev to test this contribution. Important things to include: + - Required user roles for where neccesary (ie. "As a Staff Admin...") + - Clear & validatable expected results (ie. "Confirm the submit button is now not clickable") + - Langauge that can be understood by non-technical testers if being tested by users +--> + + - [ ] ... diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index fc0e89bc604c2a0adc1c76f2dc897a2397011149..9b6212a2e105eee09b489111e2c42921caa0c076 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -18,6 +18,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v3 + with: + fetch-depth: 0 - name: Deploy docs uses: mhausenblas/mkdocs-deploy-gh-pages@master diff --git a/.github/workflows/hypha-ci.yml b/.github/workflows/hypha-ci.yml index a18aadccfa435f57026b39b3f841079706978d5b..e3d0378f0eb053be1cd24bbbffe6a695c954e8d9 100644 --- a/.github/workflows/hypha-ci.yml +++ b/.github/workflows/hypha-ci.yml @@ -13,18 +13,14 @@ concurrency: group: ${{ github.head_ref || github.run_id }} cancel-in-progress: true -env: - NODE_VERSION: 18 - PYTHON_VERSION: "3.11" - jobs: build-fe: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: - node-version: ${{ env.NODE_VERSION }} + node-version-file: ".nvmrc" cache: "npm" - name: install node dependencies run: npm install --quiet @@ -39,7 +35,7 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 with: - python-version: ${{ env.PYTHON_VERSION }} + python-version-file: ".python-version" cache: "pip" cache-dependency-path: "**/requirements*.txt" - name: install python dependencies @@ -53,10 +49,10 @@ jobs: lint-fe: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: - node-version: ${{ env.NODE_VERSION }} + node-version-file: ".nvmrc" cache: "npm" - name: install node dependencies run: npm install --quiet @@ -66,16 +62,16 @@ jobs: lint-be: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/setup-python@v4 with: - python-version: ${{ env.PYTHON_VERSION }} + python-version-file: ".python-version" - name: Install python dependencies - run: pip install `grep -E "ruff|djhtml|black" requirements-dev.txt` - - name: Run ruff + run: pip install `grep -E "ruff|djhtml" requirements-dev.txt` + - name: Run linting run: ruff check --output-format=github . - - name: Run black - run: black . --check + - name: Run formating check + run: ruff format --check . - name: Run djhtml run: djhtml hypha/ --check @@ -99,25 +95,31 @@ jobs: matrix: group: [1, 2, 3] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/setup-python@v4 with: - python-version: ${{ env.PYTHON_VERSION }} + python-version-file: ".python-version" cache: "pip" cache-dependency-path: "**/requirements*.txt" - uses: codecov/codecov-action@v3 - name: install python dependencies run: | python3 -m venv venv - . venv/bin/activate + source venv/bin/activate mkdir hypha/static_compiled - pip install --upgrade pip - pip install wheel + pip install --upgrade pip wheel pip install -r requirements-dev.txt - - name: run python tests + + - name: Run django checks + run: venv/bin/python manage.py check + + - name: Check Django Migrations run: | - . venv/bin/activate - python manage.py check - python manage.py makemigrations --check --noinput --verbosity=1 - python manage.py collectstatic --noinput --no-post-process --verbosity=1 - pytest --cov --cov-report term:skip-covered --splits 3 --group ${{ matrix.group }} + venv/bin/python manage.py makemigrations --dry-run --verbosity=3 + venv/bin/python manage.py makemigrations --check + + - name: Run collect static + run: venv/bin/python manage.py collectstatic --noinput --no-post-process --verbosity=1 + + - name: Run pytest + run: venv/bin/pytest --cov --cov-report term:skip-covered --splits 3 --group ${{ matrix.group }} diff --git a/.nvmrc b/.nvmrc index 07c7cf304e9d7fbeeae262b69d3f3578c8f85089..790e1105f22b58a307902ab92042c2a7b72f0bbd 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v18.14.2 +v20.10.0 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index db4a02c90fa323608f3cf817b191fea663fa3e2c..da61f5362f13dd2fb2736d6787230debf546d937 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,24 +1,20 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.1 + rev: v0.1.7 hooks: + # Run the linter. - id: ruff args: [--fix, --exit-non-zero-on-fix] - - repo: https://github.com/psf/black - rev: 23.10.0 - hooks: - - id: black - # It is recommended to specify the latest version of Python - # supported by your project here, or alternatively use - # pre-commit's default_language_version, see - # https://pre-commit.com/#top_level-default_language_version - language_version: python3.11 + # Run the formatter. + - id: ruff-format + - repo: https://github.com/rtts/djhtml rev: "3.0.6" hooks: - id: djhtml files: .*/templates/.*\.html$ + - repo: https://github.com/pre-commit/mirrors-prettier - rev: v3.0.3 # Use the sha or tag you want to point at + rev: v3.1.0 hooks: - id: prettier diff --git a/.prettierignore b/.prettierignore index ddc6779e198b24ff29a16d9e2cb751bebae81364..bed37e35bd97313835f446430954d0fe107a9aaf 100644 --- a/.prettierignore +++ b/.prettierignore @@ -11,10 +11,12 @@ # Ignore all files in the static & media directory. static_compiled/** media/** +/static/** # Ignore all files in the virtualenv directory. .venv/** venv/** +.ruff_cache/** # Ignore node files node_modules/** diff --git a/.python-version b/.python-version index 371cfe355dd3e241cbd73707957e299ddef5bc4c..d4b278f0a7dac5c03ca7345890437a5b1c706095 100644 --- a/.python-version +++ b/.python-version @@ -1 +1 @@ -3.11.1 +3.11.7 diff --git a/.vscode/settings.json b/.vscode/settings.json index 06d6178f35b640c810350cb1f0d275b2b416e854..839b1bed5e071e933505494720e1529387d1c395 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,6 +5,9 @@ "coreutils", "modelcluster", "pagedown", + "pytestmark", + "ratelimit", + "SIGNUP", "WAGTAILADMIN", "wagtailcore" ] diff --git a/Makefile b/Makefile index e9c5c50a18097b855c342dee89ec0c3e2f724ce7..3b313d99659031d94ca5a3e37af05e217da574a3 100644 --- a/Makefile +++ b/Makefile @@ -33,7 +33,7 @@ build: fmt: @echo "run code formatters on all code." python -m ruff --fix . - python -m black . + python -m ruff format . npx prettier . --write djhtml hypha/ @@ -52,7 +52,7 @@ endif lint: @echo "Checking python code style with ruff" ruff check . - black . --check + ruff format --check . @echo "Checking html file indendation." djhtml hypha/ --check @echo "Checking js and css code style." diff --git a/docs/getting-started/architecture.md b/docs/getting-started/architecture.md index a43a187a2494fa532cebd9df9f80d45567fa7012..8c4e59620a3c17d0c0a330cb34350681cf1109e1 100644 --- a/docs/getting-started/architecture.md +++ b/docs/getting-started/architecture.md @@ -4,8 +4,6 @@ Hypha consists of three distinctive components: 2. Then the **Admin or Wagtail Admin**, is used to create custom forms, setup funds/labs and workflow around them. Think of it as the back-office of for your submissions and projects. -3. Hypha also provides capability to build landing pages and display available funds and labs through **public site**. - ``` Integrations @@ -18,36 +16,15 @@ Hypha consists of three distinctive components: - ┌────────────┠- │PUBLIC SITE │◀────┠Databases - └────────────┘ │ ┌────────────┠┌ ─ ─ ─ ─ ─ ─ ─ ─ - ┌────────────┠│ │ Django / │ ┌────────────┠│ - │ APPLY SITE │◀────┼──────│ Wagtail │◀───────│ │ PostgreSQL │ - └────────────┘ │ └────────────┘ └────────────┘ │ - ┌────────────┠│ │ - │ WAGTAIL │ │ ─ ─ ─ ─ ─ ─ ─ ─ ┘ - │ ADMIN │◀────┘ - └────────────┘ + ┌───────────┠+ │APPLY SITE │◀───┠┌───────────┠+ └───────────┘ │ │ DJANGO │ ┌───────────┠+ ├────│ BACKEND │◀────────│PostgresSQL│ + ┌───────────┠│ └───────────┘ └───────────┘ + │ ADMIN │◀───┘ + └───────────┘ ``` -## Public site - -!!! warning Public Site is depreciated and will be removed - The public is due for removal. Read more at https://github.com/HyphaApp/hypha/pull/3110 - -The Public site is intended to be a heavily cached public site with no behaviour that requires authentication, excluding the Wagtail Admin. The ultimate aim would be to serve this site statically. - -The coupling between the Public and Apply sites has been done in such as way as to minimise the interaction between the two sites and facilitate a means of separation should the need arise. Their relationship is defined in the Public fund models: - -## Apply site - -@TODO - -## Wagtail Admin - -@TODO - - ## Django Hypha is built on top of the Django Web Framework. All the pages are rendered server side. It uses wagtail CMS for creating and managing custom application forms, public pages and settings. @@ -62,21 +39,10 @@ Wagtail is used in Hypha to construct and manage forms, pages, users and user ro ### Media -Media is encouraged to be split into two distinct storage locations. A Public and a Private location, applicant media should exist only in the Private location. This is for two reasons: - -1. This separates the site media, which is served publicly with no authentication, from the Applicant media, which has permissions checks, it reduces the risk of a miss-configured storage exposing the Applicant data. -2. Maintains separation between the two halves of the platform, Apply and Public. +Media is encouraged to be split into two distinct storage locations. A Public and a Private location, applicant media should exist only in the Private location. Media should also be served from a view that inherits from the [PrivateMediaView](https://github.com/HyphaApp/hypha/blob/main/hypha/apply/utils/storage.py) which will confirm that the file isn't made public and can be configured to return the file object from an authenticated view. -### URL configuration - -The two site have different url configurations, this limits the Apply site to a subset of the urls in the project. This is configured as part of the middleware stack using [apply\_url\_conf\_middleware](https://github.com/HyphaApp/hypha/blob/main/hypha/apply/middleware.py). This swaps out the url configuration based on the site homepage configured in the wagtail admin. - -The default url configuration is for the Public site which are shared by the Public and Apply sites. - -The Public site has access to the "public authentication" urls, this enables reverse lookup of the url in templates, such as the [login button](https://github.com/HyphaApp/hypha/blob/main/hypha/public/utils/templates/utils/includes/login_button.html), but the user is redirected to the apply site. Visiting [https:///login](https:///login) will present a login screen. - ## External Integrations diff --git a/docs/references/workflows.md b/docs/references/workflows.md index f84975ed04b72500075e0fb8da5f36bf1aaefde7..a5f1f84749d5cd8b56a2e1bba1003112509b5d40 100644 --- a/docs/references/workflows.md +++ b/docs/references/workflows.md @@ -1,22 +1,31 @@ -A workflow describes the process from a submitted application, through various stages of review, to acceptance of an application. +The **Workflow** defines the stages and processes that the application should undertake. Creating a new Fund or Lab requires you to select one of these workflows. -> â„¹ï¸ _Hypha covers more than just the application phase, but workflows are used in the application process only._ +Each workflow has a predetermined amount of stages (e.g. request, proposal), application forms, review forms, and determination forms associated with this Fund or Lab. + +Each workflow offers different statuses (e.g. External Review, Ready for Determination), and different actions (e.g Invite to proposal). + +!!! info + Hypha covers more than just the application phase, but workflows are used in the application process only. ## What are the 4 workflows? -1. Request -2. Request with external review -3. Request with community review -4. Concept and proposal +1. [Request](#request) +2. [Request with external review](#request-with-external-review) +3. [Request with community review](#request-with-community-review) +4. [Concept and Proposal](#concept-and-proposal) All workflows begin with applicant drafting, revising and submitting an application (`DRAFT_STATE`) — only transition available is to `INITIAL_STATE`, upon applicant taking the action to submit their application/request. -### Request +### 💠Request -The request workflow is a single stage process with no advisory council review. +The request workflow is a single stage process with no advisory council review. This application process requires less time and effort than the other workflow processes. -Proposal Persona: Funding organization offers a rapid response fund or another type of grantmaking that requires a streamline process that does not require an external review process. This application process could also be used for in-kind services like coaching, security audits, etc. +**Proposal Persona:** + +Funding organization offers a rapid response fund or another type of grantmaking that requires a streamline process that does not require an external review process. This application process could also be used for in-kind services like coaching, security audits, etc. + +  @@ -31,13 +40,15 @@ Once an application is submitted (`INITIAL_STATE`) — it can transition into th - Accepted (`accepted`) — application accepted. Staff can still edit this submission. - Rejected (`rejected`) — application rejected. Permissions removed from all roles. -### Request with external review +### 👳 Request with external review This workflow is a single stage process with an advisory council review or external review stage -- includes functionalties for external reviewers like advisory board members to access applications and submit reviews. Proposal Persona: This funding organization relies on external partners for evaluations. Proposals submitted to this workflow are reviewed by staff members and an advisory board that is made up of trusted community members. - + + + Once an application is submitted (`INITIAL_STATE`) — it can transition into the following: @@ -53,11 +64,15 @@ Once an application is submitted (`INITIAL_STATE`) — it can transition into th - Accepted (`ext_accepted`) — application accepted. Staff can still edit this submission. - Rejected (`ext_rejected`) — application rejected. Permissions removed from all roles. -### Request with community review +### 👪 Request with community review + +This workflow is a single stage application process with functionalties for external reviewers, including applicants to carry out peer review of each other applications. -This workflow is a single stage process with an advisory council review or external review stage. +**Proposal Persona:** + +This funding organization works with the community to co-design a meaningful definition of success. Applications are reviewed by staff members and an advisory board that is made up of trusted community members. -Proposal Persona: This funding organization works with the community to co-design a meaningful definition of success. Applications are reviewed by staff members and an advisory board that is made up of trusted community members. +  @@ -77,16 +92,28 @@ Once an application is submitted (`INITIAL_STATE`) — it can transition into th - Accept application but additional information is required (`com_almost`) — can transition to accepting application (`com_accepted`) or revert back to ready for discussion (`com_post_external_review_discussion`) - Reject application (`com_rejected`) -### Concept and Proposal +### 💡 Concept and Proposal This workflow is a two-stage process: the first stage is the request and the second stage includes an advisory council review or external review stage. -Proposal Persona: This application process is continually informed by feedback from grantee partners and community members. Applicants could use the workflow to follow the trajectory of the submission process as this workflow is transparent from the concept note (first stage) all the way to the proposal (second stage) with prospective and current applicants about funding priorities and decisions. +**Proposal Persona:** + +This application process is continually informed by feedback from grantee partners and community members. Applicants could use the workflow to follow the trajectory of the submission process as this workflow is transparent from the concept note (first stage) all the way to the proposal (second stage) with prospective and current applicants about funding priorities and decisions. The proposal stage has functionalities for applications to be reviewed by staff members and an advisory board that is made up of trusted community members. +   +**Stage 1** + + + +**Stage 2** + + + + Once an application is submitted (`INITIAL_STATE`) — it can transition into the following: - A request for more information (`concept_more_info`) — opens editing permissions to applicant again to revise their application to provide the information requested by the screeners @@ -109,59 +136,3 @@ Once an application is submitted (`INITIAL_STATE`) — it can transition into th - Proposal accepted (`proposal_accepted`) - Proposal accepted but additional info required (`proposal_almost`) - Proposal rejected (`proposal_rejected`) - - -# Select a Workflow - -The **Workflow** defines the stages and processes that the application should undertake with OTF. Creating a new Fund or Lab requires you to select one of these workflows. - -Each workflow has a predetermined amount of stages (e.g. request, proposal), application forms, review forms, and determination forms associated with this Fund or Lab. - -Each workflow offers different statuses (e.g. External Review, Ready for Determination), and different actions (e.g Invite to proposal). - -The four hard-coded workflows are: Request, Request with external review, Request with community review, Concept & Proposal. - -### 💠Request - -This application process requires less time and effort than the other workflow processes. This workflow has only a single stage with no external review. - -**Proposal Persona** -Funding organization offers a rapid response fund or another type of grantmaking that requires a streamline process that does not require an external review process. This application process could also be used for in-kind services like coaching, security audits, etc. - - - - - -### 👳 Request with External Reviewer - -The Request - External Reviewer workflow is a single stage application process with functionalties for external reviewers like advisory board members to access applications and submit reviews. - -**Proposal Persona** -This funding organization relies on external partners for evaluations. Proposals submitted to this workflow are reviewed by staff members and an advisory board that is made up of trusted community members. - - - - -### 👪 Request with Community Review - -The Request with Community Review workflow is a single stage application process with functionalties for external reviewers, including applicants to carry out peer review of each other applications. - -**Proposal Persona** -This funding organization works with the community to co-design a meaningful definition of success. Applications are reviewed by staff members and an advisory board that is made up of trusted community members. - - - - -### 💡Concept and Proposal - -**Proposal Persona** -Applicants could use this workflow to follow the trajectory of the submission process as this workflow is transparent from the concept note (first stage) all the way to the proposal (second stage) with prospective and current applicants about funding priorities and decisions. The proposal stage has functionalities for applications to be reviewed by staff members and an advisory board that is made up of trusted community members. - -**Stage 1** - - - -**Stage 2** - - - diff --git a/docs/setup/administrators/configuration.md b/docs/setup/administrators/configuration.md index 93f93d8aac705dd80cf46af895d0ea407b9910de..bf9301b48d9f2d6c457985b3802d2b260e7bc892 100644 --- a/docs/setup/administrators/configuration.md +++ b/docs/setup/administrators/configuration.md @@ -77,11 +77,11 @@ This determines the length of time for which the user will remain logged in. The ### If users should be able to register accounts without first creating applications - ENABLE_REGISTRATION_WITHOUT_APPLICATION = env.bool('ENABLE_REGISTRATION_WITHOUT_APPLICATION', False) + ENABLE_PUBLIC_SIGNUP = env.bool('ENABLE_PUBLIC_SIGNUP', True) ### If users are forced to log in before creating applications - FORCE_LOGIN_FOR_APPLICATION = env.bool('FORCE_LOGIN_FOR_APPLICATION', False) + FORCE_LOGIN_FOR_APPLICATION = env.bool('FORCE_LOGIN_FOR_APPLICATION', True) ### Set the allowed file extension for all uploads fields. @@ -160,6 +160,14 @@ Possible values are: fund, round, status, lead, reviewers, screening_statuses, c SUBMISSIONS_TABLE_EXCLUDED_FIELDS = env.list('SUBMISSIONS_TABLE_EXCLUDED_FIELDS', []) +### Default visibility for reviews. + +Possible values are: 'reviewers' or 'private'. +Private: Visible only to staff. +Reviewers: Visible to other reviewers and staff. + + REVIEW_VISIBILITY_DEFAULT = env.str('REVIEW_VISIBILITY_DEFAULT', 'private') + ### Should submission automatically transition after all reviewer roles are assigned. TRANSITION_AFTER_ASSIGNED = env.bool('TRANSITION_AFTER_ASSIGNED', False) diff --git a/docs/setup/deployment/stand-alone.md b/docs/setup/deployment/stand-alone.md index cda69916bdee03fc7500313f5cdc54b9eea14ca4..d35e01461e640f7b8bb68079519ed9eabf12a158 100644 --- a/docs/setup/deployment/stand-alone.md +++ b/docs/setup/deployment/stand-alone.md @@ -201,8 +201,6 @@ BASIC_AUTH_PASSWORD: [PASS] BASIC_AUTH_WHITELISTED_HTTP_HOSTS: www.example.org,apply.example.org CLOUDFLARE_API_ZONEID: [KEY] CLOUDFLARE_BEARER_TOKEN: [KEY] -MAILCHIMP_API_KEY: [KEY]-us10 -MAILCHIMP_LIST_ID: [ID] MAILGUN_API_KEY: [KEY] SEND_READY_FOR_REVIEW: false SLACK_DESTINATION_ROOM: #notify diff --git a/hypha/apply/activity/forms.py b/hypha/apply/activity/forms.py index ec7fc37632b2081c94c99de72e7aa55e006b855d..5d07d9c4d427787281021251902c2599f12550d2 100644 --- a/hypha/apply/activity/forms.py +++ b/hypha/apply/activity/forms.py @@ -1,10 +1,17 @@ from django import forms +from django.db import transaction +from django.utils.translation import gettext_lazy as _ +from django_file_form.forms import FileFormMixin from pagedown.widgets import PagedownWidget -from .models import Activity +from hypha.apply.stream_forms.fields import MultiFileField +from .models import Activity, ActivityAttachment + + +class CommentForm(FileFormMixin, forms.ModelForm): + attachments = MultiFileField(label=_("Attachments"), required=False) -class CommentForm(forms.ModelForm): class Meta: model = Activity fields = ("message", "visibility") @@ -33,3 +40,14 @@ class CommentForm(forms.ModelForm): visibility.choices = self.visibility_choices visibility.initial = visibility.initial[0] visibility.widget = forms.HiddenInput() + + @transaction.atomic + def save(self, commit=True): + instance = super().save(commit=True) + added_files = self.cleaned_data["attachments"] + if added_files: + ActivityAttachment.objects.bulk_create( + ActivityAttachment(activity=instance, file=file) for file in added_files + ) + + return instance diff --git a/hypha/apply/activity/messaging.py b/hypha/apply/activity/messaging.py index 31e69cf7a0e90430b054ffdbbe4c62b714b3f567..10606c97ba74d8ef4c4a96b511680b1626b2bead 100644 --- a/hypha/apply/activity/messaging.py +++ b/hypha/apply/activity/messaging.py @@ -35,7 +35,7 @@ class MessengerBackend: user=user, source=source, related=related, - **kwargs + **kwargs, ) elif sources: @@ -51,7 +51,7 @@ class MessengerBackend: user=user, sources=sources, related=related, - **kwargs + **kwargs, ) diff --git a/hypha/apply/activity/migrations/0077_remove_deleted_submission_actions.py b/hypha/apply/activity/migrations/0077_remove_deleted_submission_actions.py new file mode 100644 index 0000000000000000000000000000000000000000..d06f5ceec6abae08d62c7f082df417941a68c0ae --- /dev/null +++ b/hypha/apply/activity/migrations/0077_remove_deleted_submission_actions.py @@ -0,0 +1,32 @@ +from django.db import migrations + +from hypha.apply.activity.messaging import MESSAGES +from hypha.apply.activity.models import Event +from hypha.apply.funds.models.submissions import ApplicationSubmission + + +def remove_submission_events_without_submission(apps, schema_editor): + """ " + Remove all NEW_SUBMISSION events that don't belong to a submission + """ + + # Pull all existing submission IDs + all_application_ids = ApplicationSubmission.objects.all().values_list( + "id", flat=True + ) + + # Filter events by NEW_SUBMISSION & exclude any event that has an object_id that exists in all_application_ids + new_sub_events = Event.objects.filter(type=MESSAGES.NEW_SUBMISSION).exclude( + object_id__in=all_application_ids + ) + + # Remove these events + new_sub_events.delete() + + +class Migration(migrations.Migration): + dependencies = [ + ("activity", "0076_alter_event_type"), + ] + + operations = [migrations.RunPython(remove_submission_events_without_submission)] diff --git a/hypha/apply/activity/migrations/0078_activityattachment.py b/hypha/apply/activity/migrations/0078_activityattachment.py new file mode 100644 index 0000000000000000000000000000000000000000..b8f5903a2530d190b4f899b2132e20d09e821dee --- /dev/null +++ b/hypha/apply/activity/migrations/0078_activityattachment.py @@ -0,0 +1,49 @@ +# Generated by Django 4.2.8 on 2023-12-18 09:35 + +import django.core.files.storage +from django.db import migrations, models +import django.db.models.deletion +import hypha.apply.activity.models +import uuid + + +class Migration(migrations.Migration): + dependencies = [ + ("activity", "0077_remove_deleted_submission_actions"), + ] + + operations = [ + migrations.CreateModel( + name="ActivityAttachment", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "uuid", + models.UUIDField(default=uuid.uuid4, editable=False, unique=True), + ), + ( + "file", + models.FileField( + storage=django.core.files.storage.FileSystemStorage(), + upload_to=hypha.apply.activity.models.get_attachment_upload_path, + ), + ), + ( + "activity", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="attachments", + to="activity.activity", + ), + ), + ], + ), + ] diff --git a/hypha/apply/activity/models.py b/hypha/apply/activity/models.py index 0e438c7061a0eb791c3be794c7ecf62b970ffa44..321d078ae0c8452714741635eac22b9603ad615f 100644 --- a/hypha/apply/activity/models.py +++ b/hypha/apply/activity/models.py @@ -1,12 +1,19 @@ +import os +import uuid + from django.conf import settings from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType from django.db import models from django.db.models import Case, Value, When from django.db.models.functions import Concat +from django.urls import reverse from django.utils import timezone +from django.utils.text import get_valid_filename from django.utils.translation import gettext as _ +from hypha.apply.utils.storage import PrivateStorage + from .options import MESSAGES COMMENT = "comment" @@ -95,6 +102,33 @@ class ActionManager(ActivityBaseManager): type = ACTION +def get_attachment_upload_path(instance, filename): + return f"activity/attachments/{instance.id}/{get_valid_filename(filename)}" + + +class ActivityAttachment(models.Model): + wagtail_reference_index_ignore = True + + uuid = models.UUIDField(unique=True, default=uuid.uuid4, editable=False) + + activity = models.ForeignKey( + "Activity", on_delete=models.CASCADE, related_name="attachments" + ) + file = models.FileField( + upload_to=get_attachment_upload_path, storage=PrivateStorage() + ) + + @property + def filename(self): + return os.path.basename(self.file.name) + + def __str__(self): + return self.filename + + def get_absolute_url(self): + return reverse("activity:attachment", kwargs={"file_pk": str(self.uuid)}) + + class Activity(models.Model): timestamp = models.DateTimeField() type = models.CharField(choices=ACTIVITY_TYPES.items(), max_length=30) diff --git a/hypha/apply/activity/options.py b/hypha/apply/activity/options.py index 117727d85734e3a18584fd615a3a83d8dffa9716..5aa17b9d57e67ebabefad81f7c684f1cadc64630 100644 --- a/hypha/apply/activity/options.py +++ b/hypha/apply/activity/options.py @@ -14,8 +14,9 @@ class MESSAGES(TextChoices): TRANSITION = "TRANSITION", _("transitioned") BATCH_TRANSITION = "BATCH_TRANSITION", _("batch transitioned") DETERMINATION_OUTCOME = "DETERMINATION_OUTCOME", _("sent determination outcome") - BATCH_DETERMINATION_OUTCOME = "BATCH_DETERMINATION_OUTCOME", _( - "sent batch determination outcome" + BATCH_DETERMINATION_OUTCOME = ( + "BATCH_DETERMINATION_OUTCOME", + _("sent batch determination outcome"), ) INVITED_TO_PROPOSAL = "INVITED_TO_PROPOSAL", _("invited to proposal") REVIEWERS_UPDATED = "REVIEWERS_UPDATED", _("updated reviewers") @@ -23,8 +24,9 @@ class MESSAGES(TextChoices): PARTNERS_UPDATED = "PARTNERS_UPDATED", _("updated partners") PARTNERS_UPDATED_PARTNER = "PARTNERS_UPDATED_PARTNER", _("partners updated partner") READY_FOR_REVIEW = "READY_FOR_REVIEW", _("marked ready for review") - BATCH_READY_FOR_REVIEW = "BATCH_READY_FOR_REVIEW", _( - "marked batch ready for review" + BATCH_READY_FOR_REVIEW = ( + "BATCH_READY_FOR_REVIEW", + _("marked batch ready for review"), ) NEW_REVIEW = "NEW_REVIEW", _("added new review") COMMENT = "COMMENT", _("added comment") @@ -44,8 +46,9 @@ class MESSAGES(TextChoices): APPROVE_PAF = "APPROVE_PAF", _("approved paf") PROJECT_TRANSITION = "PROJECT_TRANSITION", _("transitioned project") REQUEST_PROJECT_CHANGE = "REQUEST_PROJECT_CHANGE", _("requested project change") - SUBMIT_CONTRACT_DOCUMENTS = "SUBMIT_CONTRACT_DOCUMENTS", _( - "submitted contract documents" + SUBMIT_CONTRACT_DOCUMENTS = ( + "SUBMIT_CONTRACT_DOCUMENTS", + _("submitted contract documents"), ) UPLOAD_DOCUMENT = "UPLOAD_DOCUMENT", _("uploaded document to project") REMOVE_DOCUMENT = "REMOVE_DOCUMENT", _("removed document from project") @@ -66,8 +69,9 @@ class MESSAGES(TextChoices): DELETE_REMINDER = "DELETE_REMINDER", _("deleted reminder") REVIEW_REMINDER = "REVIEW_REMINDER", _("reminder to review") BATCH_DELETE_SUBMISSION = "BATCH_DELETE_SUBMISSION", _("batch deleted submissions") - BATCH_ARCHIVE_SUBMISSION = "BATCH_ARCHIVE_SUBMISSION", _( - "batch archive submissions" + BATCH_ARCHIVE_SUBMISSION = ( + "BATCH_ARCHIVE_SUBMISSION", + _("batch archive submissions"), ) STAFF_ACCOUNT_CREATED = "STAFF_ACCOUNT_CREATED", _("created new account") STAFF_ACCOUNT_EDITED = "STAFF_ACCOUNT_EDITED", _("edited account") diff --git a/hypha/apply/activity/templates/activity/include/comment_form.html b/hypha/apply/activity/templates/activity/include/comment_form.html index 4dc4dcffc41d17affb87e52f9f0f4b512ca6bd6e..3d7e8f78a247b7b2dd834eab62072dae744382ca 100644 --- a/hypha/apply/activity/templates/activity/include/comment_form.html +++ b/hypha/apply/activity/templates/activity/include/comment_form.html @@ -1,6 +1,5 @@ {% load i18n %} -<h4 class="m-0">Add communication</h4> <div class="wrapper wrapper--comments"> {% trans "Submit" as submit %} {% include "funds/includes/delegated_form_base.html" with form=comment_form value=submit extra_classes="form__comments" %} diff --git a/hypha/apply/activity/templates/activity/include/listing_base.html b/hypha/apply/activity/templates/activity/include/listing_base.html index 450b1dc5b7e154cf32848f485238b35a2b273920..de7994bd854cb033c97306c466b96758f68adcd3 100644 --- a/hypha/apply/activity/templates/activity/include/listing_base.html +++ b/hypha/apply/activity/templates/activity/include/listing_base.html @@ -1,60 +1,103 @@ -{% load i18n activity_tags bleach_tags markdown_tags submission_tags apply_tags %} -<div class="feed__item feed__item--{{ activity.type }}" id="communications#{{ activity.id }}"> - <div class="feed__pre-content"> - <p class="feed__label feed__label--{{ activity.type }}">{{ activity.type|capfirst }}</p> +{% load i18n activity_tags bleach_tags markdown_tags submission_tags apply_tags heroicons %} +<div class="feed__item feed__item--{{ activity.type }} border shadow-sm rounded-sm pb-2 " id="communications#{{ activity.id }}"> + <div class="feed__pre-content hidden lg:block"> + <p class="feed__label lg:py-2 feed__label--{{ activity.type }}"> + {% if activity.type == 'comment' %} + {% heroicon_mini "chat-bubble-left" class="inline align-text-bottom mr-2" aria_hidden=true %} + {% else %} + {{ activity.type|capfirst }} + {% endif %} + </p> </div> <div class="feed__content js-feed-content"> - <div class="feed__meta js-feed-meta"> - <p class="feed__label feed__label--{{ activity.type }} feed__label--mobile">{{ activity.type|capfirst }}</p> - <p class="feed__meta-item"><span>{{ activity|display_author:request.user }}</span> <relative-time class="text-fg-muted text-sm" datetime={{ activity.timestamp|date:"c" }}>{{ activity.timestamp|date:"SHORT_DATETIME_FORMAT" }}</relative-time> </p> - - {% if editable %} - {% if activity.user == request.user %} - <p class="feed__meta-item feed__meta-item--edit-button"> - <a class="link link--edit-submission is-active js-edit-comment" href="#"> - {% trans "Edit" %} - <svg class="icon icon--pen"><use xlink:href="#pen"></use></svg> - </a> - </p> + <div class="feed__meta js-feed-meta py-2 bg-slate-50 shadow-sm"> + <p class="feed__meta-item pl-3"> + <span>{{ activity|display_author:request.user }}</span> + <relative-time class="text-fg-muted text-sm" data-tippy-content="{{ activity.timestamp|date:"SHORT_DATETIME_FORMAT" }}" datetime={{ activity.timestamp|date:"c" }}>{{ activity.timestamp|date:"SHORT_DATETIME_FORMAT" }}</relative-time> + {% if activity.edited %} + • + <span class="js-last-edited text-fg-muted text-sm" data-tippy-content="{{ activity.edited|date:"SHORT_DATETIME_FORMAT" }}">{% trans "edited" %}</span> {% endif %} - <p class="feed__meta-item feed__meta-item--last-edited" {% if not activity.edited %} hidden {% endif %}> - ({% trans "Last edited" %}: <span class="js-last-edited">{{ activity.edited|date:"SHORT_DATETIME_FORMAT" }}</span>) + </p> + + {% if editable and activity.user == request.user %} + <p class="feed__meta-item feed__meta-item--edit-button"> + <a class="link link--edit-submission is-active js-edit-comment" href="#"> + {% trans "Edit" %} + <svg class="icon icon--pen"><use xlink:href="#pen"></use></svg> + </a> </p> {% endif %} <p class="feed__meta-item feed__meta-item--right" {% if not activity.private %} hidden {% endif %}> - <svg class="icon icon--eye"><use xlink:href="#eye"></use></svg> + {% heroicon_mini "eye" size=17 class="inline align-text-bottom fill-fg-muted mr-1" aria_hidden=true %} <span class="js-comment-visibility">{{ activity.visibility|visibility_display:request.user }}</span> </p> </div> - <p class="feed__heading"> + <div class="feed__heading"> {% if submission_title %} {% trans "updated" %} <a href="{{ activity.source.get_absolute_url }}">{{ activity.source.title }}</a> {% endif %} {% if editable %} - <div class="feed__comment js-comment" data-id="{{activity.id}}" data-comment="{{activity|display_for:request.user|to_markdown}}" + <div class="feed__comment js-comment px-3 prose" + data-id="{{activity.id}}" data-comment="{{activity|display_for:request.user|to_markdown}}" data-visibility-options="{{activity|visibility_options:activity.user}}" data-visibility="{{activity.visibility}}" - data-edit-url="{% url 'api:v1:comments-edit' pk=activity.pk %}"> + data-edit-url="{% url 'api:v1:comments-edit' pk=activity.pk %}" + > {{ activity|display_for:request.user|submission_links|markdown|bleach }} </div> - - <div class="js-edit-block" aria-live="polite"></div> + <style> + @media only screen and (min-width: 1024px){ + .js-edit-block .form .wmd-preview, .js-edit-block .form .wmd-input { + max-width: 70%; + } + } + </style> + <div class="js-edit-block pr-3" aria-live="polite"></div> {% else %} - {{ activity|display_for:request.user|submission_links|markdown|bleach }} + <div class="px-3 prose"> + {{ activity|display_for:request.user|submission_links|markdown|bleach }} + </div> {% endif %} + {% if not submission_title and activity|user_can_see_related:request.user %} - {% with url=activity.related_object.get_absolute_url %} - {% if url %} - <a href="{{ url }}" target="_blank" class="feed__related-item"> - {% trans "View " %}{{ activity.related_object|model_verbose_name }} <svg class="icon"><use xlink:href="#arrow-head-pixels--solid"></use></svg> + <div class="px-3"> + {% with url=activity.related_object.get_absolute_url %} + {% if url %} + <a href="{{ url }}" target="_blank" class="feed__related-item"> + {% trans "View " %}{{ activity.related_object|model_verbose_name }} <svg class="icon"><use xlink:href="#arrow-head-pixels--solid"></use></svg> + </a> + {% endif %} + {% endwith %} + </div> + {% endif %} + + </div> + {% with activity.attachments.all as attachments %} + {% if attachments %} + <div class="section-attachments flex gap-2 flex-col max-w-xl mt-4 px-3 pb-2"> + {% for attachment in attachments %} + <a href="{{attachment.get_absolute_url }}" + class="flex justify-between border rounded px-3 py-2 font-medium bg-slate-50 hover:bg-slate-200 transition-colors" + target="_blank" + rel="noopener noreferrer" + title="{{ attachment.filename }}" + > + <span class="truncate text-sm"> + {% heroicon_mini "paper-clip" class="inline align-text-bottom" aria_hidden=true %} + {{ attachment.filename|truncatechars_middle:45 }} + </span> + <span> + {% heroicon_mini "arrow-small-down" class="inline align-text-bottom rounded" aria_hidden=true %} + </span> </a> - {% endif %} - {% endwith %} + {% endfor %} + </div> {% endif %} - </p> + {% endwith %} </div> </div> diff --git a/hypha/apply/activity/urls.py b/hypha/apply/activity/urls.py index eebe907ae055791f9df4a4e3068325ae2911ecab..4ec83895229310fb7344edade816b52789075519 100644 --- a/hypha/apply/activity/urls.py +++ b/hypha/apply/activity/urls.py @@ -1,6 +1,6 @@ from django.urls import include, path -from .views import NotificationsView +from .views import AttachmentView, NotificationsView app_name = "activity" @@ -8,4 +8,9 @@ app_name = "activity" urlpatterns = [ path("anymail/", include("anymail.urls")), path("notifications/", NotificationsView.as_view(), name="notifications"), + path( + "activities/attachment/<uuid:file_pk>/download/", + AttachmentView.as_view(), + name="attachment", + ), ] diff --git a/hypha/apply/activity/views.py b/hypha/apply/activity/views.py index ec106f4224533ac884f0ace33d2a69507a97da61..52b125342721e55d0b2e9d85ea4e8ecbb09735dc 100644 --- a/hypha/apply/activity/views.py +++ b/hypha/apply/activity/views.py @@ -1,16 +1,18 @@ from django.conf import settings +from django.shortcuts import get_object_or_404 from django.utils import timezone from django.utils.decorators import method_decorator from django.views.generic import CreateView, ListView from django_ratelimit.decorators import ratelimit from hypha.apply.users.decorators import staff_required +from hypha.apply.utils.storage import PrivateMediaView from hypha.apply.utils.views import DelegatedViewMixin from .filters import NotificationFilter from .forms import CommentForm from .messaging import MESSAGES, messenger -from .models import COMMENT, Activity +from .models import COMMENT, Activity, ActivityAttachment from .services import get_related_comments_for_user @@ -60,6 +62,19 @@ class CommentFormView(DelegatedViewMixin, CreateView): return kwargs +class AttachmentView(PrivateMediaView): + model = ActivityAttachment + + def dispatch(self, *args, **kwargs): + file_pk = kwargs.get("file_pk") + self.instance = get_object_or_404(ActivityAttachment, uuid=file_pk) + + return super().dispatch(*args, **kwargs) + + def get_media(self, *args, **kwargs): + return self.instance.file + + @method_decorator(staff_required, name="dispatch") class NotificationsView(ListView): model = Activity diff --git a/hypha/apply/categories/admin_helpers.py b/hypha/apply/categories/admin_helpers.py index addea237ab42f4a7fb5021ee472a01031808632c..7afbcfe26d20dc5746586bb9e21802ca47adb46d 100644 --- a/hypha/apply/categories/admin_helpers.py +++ b/hypha/apply/categories/admin_helpers.py @@ -46,7 +46,7 @@ class MetaTermButtonHelper(ButtonHelper): add_child_button = self.add_child_button( pk=getattr(obj, self.opts.pk.attname), child_verbose_name=obj.node_child_verbose_name, - **kwargs + **kwargs, ) buttons.append(add_child_button) diff --git a/hypha/apply/dashboard/services.py b/hypha/apply/dashboard/services.py new file mode 100644 index 0000000000000000000000000000000000000000..7f9ad5bfca8c1af39e42fc690a56d30d89396a98 --- /dev/null +++ b/hypha/apply/dashboard/services.py @@ -0,0 +1,28 @@ +from django.db.models import Count + +from hypha.apply.projects.models import PAFApprovals + + +def get_paf_for_review(user, is_paf_approval_sequential): + """Return a list of paf approvals ready for user's review""" + + paf_approvals = PAFApprovals.objects.annotate( + roles_count=Count("paf_reviewer_role__user_roles") + ).filter( + roles_count=len(list(user.groups.all())), + approved=False, + ) + + for role in user.groups.all(): + paf_approvals = paf_approvals.filter(paf_reviewer_role__user_roles__id=role.id) + + if is_paf_approval_sequential: + all_matched_paf_approvals = list(paf_approvals) + for matched_paf_approval in all_matched_paf_approvals: + if matched_paf_approval.project.paf_approvals.filter( + paf_reviewer_role__sort_order__lt=matched_paf_approval.paf_reviewer_role.sort_order, + approved=False, + ).exists(): + paf_approvals = paf_approvals.exclude(id=matched_paf_approval.id) + + return paf_approvals diff --git a/hypha/apply/dashboard/templates/dashboard/applicant_dashboard.html b/hypha/apply/dashboard/templates/dashboard/applicant_dashboard.html index 7bd4f583db3dda3b7cf70fea789ffcc5cf8f3c94..d9c99e91c518a0c0a7d199474d60ef9d09d310c4 100644 --- a/hypha/apply/dashboard/templates/dashboard/applicant_dashboard.html +++ b/hypha/apply/dashboard/templates/dashboard/applicant_dashboard.html @@ -1,97 +1,139 @@ {% extends "base-apply.html" %} {% load render_table from django_tables2 %} -{% load i18n static wagtailcore_tags workflow_tags statusbar_tags heroicons dashboard_statusbar_tags %} +{% load i18n static wagtailcore_tags workflow_tags statusbar_tags heroicons dashboard_statusbar_tags apply_tags invoice_tools markdown_tags bleach_tags %} +{% block body_class %}bg-light-grey{% endblock %} {% block title %}{% trans "Dashboard" %}{% endblock %} -{% block content_wrapper %} - <main class="wrapper wrapper--large wrapper--main bg-light-grey" id="main"> - {% block content %} - <div class="admin-bar"> - <div class="admin-bar__inner wrapper--applicant-dashboard"> - <div class="my-auto"> - <h1 class="heading heading--no-margin font-bold">{% trans "My dashboard" %}</h1> - <p class="m-0">{% trans "An overview of active and past submissions and projects" %}</p> - </div> - <div class="wrapper wrapper--cta-box flex items-center"> - <div class="flex-1"> - <h3 class="heading heading--no-margin font-bold">{% trans "Submit a new application" %}</h3> - <p class="text-base m-0">{% trans "Apply now for our open rounds" %}</p> - </div> - <div> - <a class="button button--blue-white" href="{% pageurl APPLY_SITE.root_page %}" class="button">{% trans "Apply" %}</a> - </div> - </div> +{% block content %} + <div class="admin-bar"> + <div class="admin-bar__inner wrapper--applicant-dashboard"> + <div class="my-auto"> + <h1 class="heading heading--no-margin font-bold">{% trans "My dashboard" %}</h1> + <p class="m-0">{% trans "An overview of active and past submissions and projects" %}</p> + </div> + <div class="wrapper wrapper--cta-box flex items-center"> + <div class="flex-1"> + <h3 class="heading heading--no-margin font-bold">{% trans "Submit a new application" %}</h3> + <p class="text-base m-0">{% trans "Apply now for our open rounds" %}</p> + </div> + <div> + <a class="button button--blue-white" href="{% pageurl APPLY_SITE.root_page %}" class="button">{% trans "Apply" %}</a> </div> </div> + </div> + </div> - <div class="wrapper wrapper--large wrapper--inner-space-medium"> - {% if my_active_submissions %} - <div class="mb-10"> - <div class="flex"> - <h2 class="font-light flex-1">{% trans "My active submissions" %}</h2> + <div class="wrapper wrapper--large wrapper--inner-space-medium"> + {% if my_tasks.count %} + <div class="w-8/12 m-auto mb-10"> + <h2 class="text-center font-light">{% trans "My tasks" %}</h2> + {% for task in my_tasks.data %} + <div class="bg-white p-1 flex mb-1 items-center"> + <svg class="icon icon--dashboard-tasks"><use xlink:href="#{{ task.icon }}"></use></svg> + <div class="flex-1">{{ task.text|markdown|bleach }}</div> + <a class="button button-primary m-2" href="{{ task.url }}">View</a> + </div> + {% endfor %} + </div> + {% endif %} + + {% if my_active_submissions %} + <div class="mb-10"> + <div class="flex"> + <h2 class="font-light flex-1">{% trans "My active submissions" %}</h2> + </div> + {% for submission in my_active_submissions %} + <div class="wrapper wrapper--status-bar-outer"> + <div class="wrapper wrapper--status-bar-inner"> + <div class="mt-5 ml-4 lg:max-w-[30%]"> + <h4 class="heading mb-2 font-bold line-clamp-3 hover:line-clamp-none"><a class="link" href="{% url 'funds:submissions:detail' submission.id %}">{{ submission.title }}</a></h4> + <p class="m-0 text-gray-400">{% trans "Submitted on " %} {{ submission.submit_time.date }} {% trans "by" %} {{ submission.user.get_full_name }}</p> + </div> + {% status_bar submission.workflow submission.phase request.user css_class="status-bar--small" %} </div> - {% for submission in my_active_submissions %} - <div class="wrapper wrapper--status-bar-outer"> - <div class="wrapper wrapper--status-bar-inner"> - <div class="mt-5 ml-4"> - <h4 class="heading mb-2 font-bold"><a class="link link--underlined" href="{% url 'funds:submissions:detail' submission.id %}">{{ submission.title }}</a></h4> - <p class="m-0 text-gray-400">{% trans "Submitted on " %} {{ submission.submit_time.date }} {% trans "by" %} {{ submission.user.get_full_name }}</p> - </div> - {% status_bar submission.workflow submission.phase request.user css_class="status-bar--small" %} - </div> - {% if request.user|has_edit_perm:submission %} - <a class="button button--primary ml-4" href="{% url 'funds:submissions:edit' submission.id %}"> - {% if submission.status == 'draft_proposal' %} - {% trans "Start your" %} {{ submission.stage }} {% trans "application" %} - {% else %} - {% trans "Edit" %} - {% endif %} - </a> + {% if request.user|has_edit_perm:submission %} + <a class="button button--primary ml-4" href="{% url 'funds:submissions:edit' submission.id %}"> + {% if submission.status == 'draft_proposal' %} + {% trans "Start your" %} {{ submission.stage }} {% trans "application" %} + {% else %} + {% trans "Edit" %} {% endif %} - </div> - {% empty %} - {% trans "No active submissions" %} - {% endfor %} + </a> + {% endif %} </div> - {% endif %} + {% empty %} + {% trans "No active submissions" %} + {% endfor %} + </div> + {% endif %} - {% if active_projects.count %} - <div class="mb-10"> - <div class="flex"> - <h2 class="font-light flex-1">{% trans "My active projects" %}</h2> + {% if active_projects.count %} + <div class="mb-10"> + <div class="flex"> + <h2 class="font-light flex-1">{% trans "My active projects" %}</h2> + </div> + {% for project in active_projects.data %} + <div class="wrapper wrapper--status-bar-outer"> + <div class="wrapper wrapper--status-bar-inner"> + <div class="mt-5 ml-4 lg:max-w-[30%]"> + <h4 class="heading mb-2 font-bold line-clamp-3 hover:line-clamp-none"><a class="link" href="{% url 'apply:projects:detail' project.id %}">{{ project.title }}</a></h4> + <p class="m-0 text-gray-400">{% trans "Project start date: " %} {{ project.created_at.date }}</p> + </div> + {% project_status_bar project.status request.user css_class="status-bar--small" %} </div> - {% for project in active_projects.data %} - <div class="wrapper wrapper--status-bar-outer"> - <div class="wrapper wrapper--status-bar-inner"> - <div class="mt-5 ml-4"> - <h4 class="heading mb-2 font-bold"><a class="link link--underlined" href="{% url 'apply:projects:detail' project.id %}">{{ project.title }}</a></h4> - <p class="m-0 text-gray-400">{% trans "Project start date: " %} {{ project.created_at.date }}</p> - </div> - {% project_status_bar project.status request.user css_class="status-bar--small" %} + </div> + {% empty %} + {% trans "No active projects" %} + {% endfor %} + </div> + {% endif %} + + {% if active_invoices.count %} + <div class="mb-10"> + <div class="flex"> + <h2 class="font-light flex-1">{% trans "My active invoices" %}</h2> + </div> + {% for invoice in active_invoices.data %} + <div class="wrapper wrapper--status-bar-outer"> + <div class="wrapper wrapper--status-bar-inner pl-4 pr-4"> + <div class="max-w-[33%] w-full text-left my-auto"> + <h4 class="heading heading--no-margin font-bold"><a class="link" href="{{ invoice.get_absolute_url }}"> + {% if invoice.invoice_number %}{{ invoice.invoice_number }}{% else %}{{ invoice.vendor_document_number }}{% endif %} + </a></h4> + <p class="m-0 text-gray-400">{% trans "Date added: " %} {{ invoice.requested_at }}</p> + </div> + <div class="max-w-[33%] w-full text-center my-auto"> + <p class="text-2xl">{% if invoice.invoice_amount %}{{ invoice.invoice_amount | format_number_as_currency }}{% else %}-{% endif %}</p> + </div> + <div class="max-w-[33%] w-full flex my-auto"> + {% display_invoice_table_status_for_user invoice.status request.user as invoice_status %} + <div class="w-full flex-1"></div> + <div class="max-w-fit w-full text-right"> + <p class="{{ invoice_status|invoice_status_bg_color }} text-base py-2 px-3 {{ invoice_status|invoice_status_fg_color }}">{{ invoice_status }}</p> </div> </div> - {% empty %} - {% trans "No active projects" %} - {% endfor %} + </div> </div> - {% endif %} + {% empty %} + {% trans "No active invoices" %} + {% endfor %} </div> + {% endif %} + </div> - {% if historical_submissions.count %} - <div class="wrapper wrapper--large wrapper--inner-space-medium mb-8"> - <h2 class="text-xl mb-2">{% trans "Submission history" %}</h3> - {% render_table historical_submissions.table %} - </div> - {% endif %} + {% if historical_submissions.count %} + <div class="wrapper wrapper--large wrapper--inner-space-medium mb-8"> + <h2 class="text-xl mb-2">{% trans "Submission history" %}</h2> + {% render_table historical_submissions.table %} + </div> + {% endif %} - {% if historical_projects.count %} - <div class="wrapper wrapper--large wrapper--inner-space-medium mb-8"> - <h2 class="text-xl mb-2">{% trans "Project history" %}</h2> - {% render_table historical_projects.table %} - </div> - {% endif %} + {% if historical_projects.count %} + <div class="wrapper wrapper--large wrapper--inner-space-medium mb-8"> + <h2 class="text-xl mb-2">{% trans "Project history" %}</h2> + {% render_table historical_projects.table %} + </div> + {% endif %} - {% endblock %} - </main> {% endblock %} diff --git a/hypha/apply/dashboard/templates/dashboard/contracting_dashboard.html b/hypha/apply/dashboard/templates/dashboard/contracting_dashboard.html index a382b38822cd77d49a028280610e5a2a916d30a5..533ee6b65138bbfacc272a2bedf4375084ccd526 100644 --- a/hypha/apply/dashboard/templates/dashboard/contracting_dashboard.html +++ b/hypha/apply/dashboard/templates/dashboard/contracting_dashboard.html @@ -1,6 +1,6 @@ {% extends "base-apply.html" %} {% load render_table from django_tables2 %} -{% load i18n static %} +{% load i18n static markdown_tags bleach_tags %} {% block title %}{% trans "Dashboard" %}{% endblock %} @@ -15,14 +15,23 @@ {% endadminbar %} <div class="wrapper wrapper--large wrapper--inner-space-medium"> - {% if paf_waiting_for_approval.count %} - {% include "dashboard/includes/paf_waiting_for_approval.html" with paf_waiting_for_approval=paf_waiting_for_approval %} + {% if my_tasks.count %} + <div class="w-8/12 m-auto mb-10"> + <h2 class="text-center font-light">{% trans "My tasks" %}</h2> + {% for task in my_tasks.data %} + <div class="bg-white p-1 flex mb-1 items-center"> + <svg class="icon icon--dashboard-tasks"><use xlink:href="#{{ task.icon }}"></use></svg> + <div class="flex-1">{{ task.text|markdown|bleach }}</div> + <a class="button button-primary m-2" href="{{ task.url }}">View</a> + </div> + {% endfor %} + </div> {% endif %} - {% if paf_waiting_for_assignment.count %} - <div id="paf_waiting_for_assignment" class="wrapper wrapper--bottom-space"> - <h4 class="heading heading--normal">{% trans "PAF waiting for assignee" %}</h4> - {% render_table paf_waiting_for_assignment.table %} + {% if paf_for_review.count %} + <div id="paf_for_review" class="wrapper wrapper--bottom-space"> + <h4 class="heading heading--normal">{% trans "PAFs for review" %}</h4> + {% render_table paf_for_review.table %} </div> {% endif %} diff --git a/hypha/apply/dashboard/templates/dashboard/dashboard.html b/hypha/apply/dashboard/templates/dashboard/dashboard.html index 006d7812ef70475f7c1f68212cc3a16be6086925..3f5732864c07de26f7e49a2396bac01ab27ab04b 100644 --- a/hypha/apply/dashboard/templates/dashboard/dashboard.html +++ b/hypha/apply/dashboard/templates/dashboard/dashboard.html @@ -1,6 +1,6 @@ {% extends "base-apply.html" %} {% load render_table from django_tables2 %} -{% load i18n static %} +{% load i18n static bleach_tags markdown_tags %} {% block extra_css %} {{ my_reviewed.filterset.form.media.css }} @@ -24,6 +24,19 @@ <div class="wrapper wrapper--large wrapper--inner-space-medium"> <div class="wrapper wrapper--bottom-space"> + {% if my_tasks.count %} + <div class="w-8/12 m-auto mb-10"> + <h2 class="text-center font-light">{% trans "My tasks" %}</h2> + {% for task in my_tasks.data %} + <div class="bg-white p-1 flex mb-1 items-center"> + <svg class="icon icon--dashboard-tasks"><use xlink:href="#{{ task.icon }}"></use></svg> + <div class="flex-1">{{ task.text|markdown|bleach }}</div> + <a class="button button-primary m-2" href="{{ task.url }}">View</a> + </div> + {% endfor %} + </div> + {% endif %} + <div class="stat-block"> <a href="#submissions-awaiting-review" class="stat-block__item border"> <p class="stat-block__number">{{ awaiting_reviews.count }}</p> @@ -39,15 +52,6 @@ <div class="stat-block__view">{% trans "View" %}</div> {% endif %} </a> - {% if not paf_waiting_for_approval.count is None%} - <a href="#paf-awaiting-approval" class="stat-block__item border"> - <p class="stat-block__number">{{ paf_waiting_for_approval.count }}</p> - <p class="stat-block__text">{% trans "Projects awaiting approval" %}</p> - {% if paf_waiting_for_approval.count %} - <div class="stat-block__view">{% trans "View" %}</div> - {% endif %} - </a> - {% endif %} <a href="#active-invoices" class="stat-block__item border"> <p class="stat-block__number">{{ active_invoices.count }}</p> <p class="stat-block__text">{% trans "Requests for invoices requiring your attention" %}</p> @@ -72,14 +76,10 @@ {% include "funds/includes/round-block.html" with can_export=can_export closed_rounds=rounds.closed open_rounds=rounds.open title="Your rounds and labs" page_type='dashboard' %} {% endif %} - {% if paf_waiting_for_approval.count %} - {% include "dashboard/includes/paf_waiting_for_approval.html" with paf_waiting_for_approval=paf_waiting_for_approval %} - {% endif %} - - {% if paf_waiting_for_assignment.count %} - <div id="paf_waiting_for_assignment" class="wrapper wrapper--bottom-space"> - <h4 class="heading heading--normal">{% trans "PAF waiting for assignee" %}</h4> - {% render_table paf_waiting_for_assignment.table %} + {% if paf_for_review.count %} + <div id="paf_for_review" class="wrapper wrapper--bottom-space"> + <h4 class="heading heading--normal">{% trans "PAFs for review" %}</h4> + {% render_table paf_for_review.table %} </div> {% endif %} @@ -125,6 +125,7 @@ {% block extra_js %} {{ my_reviewed.filterset.form.media.js }} <script src="{% static 'js/apply/url-search-params.js' %}"></script> + <script src="{% static 'js/apply/all-submissions-table.js' %}"></script> <script src="{% static 'js/apply/submission-filters.js' %}"></script> <script src="{% static 'js/apply/tabs.js' %}"></script> <script src="{% static 'js/apply/flag.js' %}"></script> diff --git a/hypha/apply/dashboard/templates/dashboard/finance_dashboard.html b/hypha/apply/dashboard/templates/dashboard/finance_dashboard.html index 2b0e6f9a5dd3f98e8a49a34b22c8b55f9b5de6a8..5510e4e4c4142ba5f4b991f7a8386f6862ff20ba 100644 --- a/hypha/apply/dashboard/templates/dashboard/finance_dashboard.html +++ b/hypha/apply/dashboard/templates/dashboard/finance_dashboard.html @@ -1,6 +1,6 @@ {% extends "base-apply.html" %} {% load render_table from django_tables2 %} -{% load i18n static %} +{% load i18n static markdown_tags bleach_tags %} {% block title %}{% trans "Dashboard" %}{% endblock %} @@ -15,6 +15,19 @@ {% endadminbar %} <div class="wrapper wrapper--large wrapper--inner-space-medium"> + {% if my_tasks.count %} + <div class="w-8/12 m-auto mb-10"> + <h2 class="text-center font-light">{% trans "My tasks" %}</h2> + {% for task in my_tasks.data %} + <div class="bg-white p-1 flex mb-1 items-center"> + <svg class="icon icon--dashboard-tasks"><use xlink:href="#{{ task.icon }}"></use></svg> + <div class="flex-1">{{ task.text|markdown|bleach }}</div> + <a class="button button-primary m-2" href="{{ task.url }}">View</a> + </div> + {% endfor %} + </div> + {% endif %} + <div class="wrapper wrapper--bottom-space" role="tablist" aria-label="Invoice Tabs" x-data="{ tab: '{% trans "Active" %}' }" @@ -84,16 +97,13 @@ </div> - {% if paf_waiting_for_approval.count %} - {% include "dashboard/includes/paf_waiting_for_approval.html" with paf_waiting_for_approval=paf_waiting_for_approval %} - {% endif %} - - {% if paf_waiting_for_assignment.count %} - <div id="paf_waiting_for_assignment" class="wrapper wrapper--bottom-space"> - <h4 class="heading heading--normal">{% trans "PAF waiting for assignee" %}</h4> - {% render_table paf_waiting_for_assignment.table %} + {% if paf_for_review.count %} + <div id="paf_for_review" class="wrapper wrapper--bottom-space"> + <h4 class="heading heading--normal">{% trans "PAFs for review" %}</h4> + {% render_table paf_for_review.table %} </div> {% endif %} + </div> {% endblock %} diff --git a/hypha/apply/dashboard/templates/dashboard/includes/paf_waiting_for_approval.html b/hypha/apply/dashboard/templates/dashboard/includes/paf_waiting_for_approval.html deleted file mode 100644 index 0fdfcaa1a55c3617e701533744494a2ed0bd6301..0000000000000000000000000000000000000000 --- a/hypha/apply/dashboard/templates/dashboard/includes/paf_waiting_for_approval.html +++ /dev/null @@ -1,48 +0,0 @@ -{% load render_table from django_tables2 %} -{% load i18n %} - -<div id="paf-awaiting-approval" class="wrapper wrapper--bottom-space" x-data="{ tab: '{% trans "Awaiting your approval" %}' }"> - <section class="section section--with-options"> - <h2 class="text-xl mb-2"> - {% trans 'PAF waiting for internal approval' %} - </h2> - <nav> - <a class="tab__item tab__item--alt" - role="tab" - href="#" - id="tab-paf-awaiting" - aria-controls="panel-paf-awaiting" - :class="{ 'tab__item--active': tab === '{% trans "Awaiting your approval" %}' }" - @click.prevent="tab = '{% trans "Awaiting your approval" %}'" - >{% trans "Awaiting your approval" %}</a> - <a class="tab__item tab__item--alt" - role="tab" - href="#" - id="tab-paf-approved" - aria-controls="panel-paf-approved" - :class="{ 'tab__item--active': tab === '{% trans "Approved by you" %}' }" - @click.prevent="tab = '{% trans "Approved by you" %}'" - >{% trans "Approved by you" %}</a> - </nav> - </section> - - <div x-show="tab === '{% trans "Awaiting your approval" %}'" role="tabpanel" tabindex="0" aria-labelledby="tab-paf-awaiting" id="panel-paf-awaiting"> - {% if paf_waiting_for_approval.awaiting_your_approval.count %} - {% render_table paf_waiting_for_approval.awaiting_your_approval.table %} - {% else %} - <div class="border px-2 py-4"> - {% trans "You have approved all PAFs, no PAF is waiting for your Approval " %} - </div> - {% endif %} - </div> - <div x-show="tab === '{% trans "Approved by you" %}'" role="tabpanel" tabindex="0" aria-labelledby="tab-paf-approved" id="panel-paf-approved"> - {% if paf_waiting_for_approval.approved_by_you.count %} - {% render_table paf_waiting_for_approval.approved_by_you.table %} - {% else %} - <div class="border px-2 py-4"> - {% trans "No PAF is approved by you yet " %} - </div> - {% endif %} - </div> - -</div> diff --git a/hypha/apply/dashboard/templates/dashboard/reviewer_dashboard.html b/hypha/apply/dashboard/templates/dashboard/reviewer_dashboard.html index 1d6d0c05b667b8c308a190c3bc01c27d4cc8f564..444531c53772cd335eeaf536dbb6346a32174ba6 100644 --- a/hypha/apply/dashboard/templates/dashboard/reviewer_dashboard.html +++ b/hypha/apply/dashboard/templates/dashboard/reviewer_dashboard.html @@ -69,15 +69,6 @@ </div> {% endif %} - {% if paf_waiting_for_assignment.count %} - <div id="paf_waiting_for_assignment" class="wrapper wrapper--bottom-space"> - <h2 class="text-xl mb-2"> - {% trans "PAF waiting for assignee" %} - </h2> - {% render_table paf_waiting_for_assignment.table %} - </div> - {% endif %} - {% if my_inactive_submissions.data %} <div class="wrapper wrapper--bottom-space"> <h2 class="text-xl mb-2"> diff --git a/hypha/apply/dashboard/views.py b/hypha/apply/dashboard/views.py index 0e7e0b641df1ed46feacd0f12e1018169444de34..bb5611808bfff45d80d72109a29fc22edec712ba 100644 --- a/hypha/apply/dashboard/views.py +++ b/hypha/apply/dashboard/views.py @@ -1,9 +1,9 @@ from django.conf import settings -from django.db.models import Count -from django.http import HttpResponseRedirect +from django.http import HttpResponseForbidden, HttpResponseRedirect from django.shortcuts import render from django.urls import reverse, reverse_lazy from django.views.generic import TemplateView +from django_tables2 import RequestConfig from hypha.apply.funds.models import ( ApplicationSubmission, @@ -21,16 +21,18 @@ from hypha.apply.funds.tables import ( review_filter_for_user, ) from hypha.apply.projects.filters import ProjectListFilter -from hypha.apply.projects.models import Invoice, PAFApprovals, Project, ProjectSettings -from hypha.apply.projects.models.project import INTERNAL_APPROVAL -from hypha.apply.projects.permissions import has_permission +from hypha.apply.projects.models import Invoice, Project, ProjectSettings +from hypha.apply.projects.models.payment import DECLINED, PAID from hypha.apply.projects.tables import ( InvoiceDashboardTable, - ProjectsAssigneeDashboardTable, + PAFForReviewDashboardTable, ProjectsDashboardTable, ) +from hypha.apply.todo.views import render_task_templates_for_user from hypha.apply.utils.views import ViewDispatcher +from .services import get_paf_for_review + class MySubmissionContextMixin: def get_context_data(self, **kwargs): @@ -90,15 +92,41 @@ class AdminDashboardView(MyFlaggedMixin, TemplateView): "can_export": can_export_submissions(self.request.user), "my_reviewed": self.my_reviewed(submissions), "projects": self.projects(), - "paf_waiting_for_approval": self.paf_waiting_for_approval(), "rounds": self.rounds(), "my_flagged": self.my_flagged(submissions), - "paf_waiting_for_assignment": self.paf_waiting_for_approver_assignment(), + "paf_for_review": self.paf_for_review(), + "my_tasks": self.my_tasks(), } ) return context + def paf_for_review(self): + if not self.request.user.is_apply_staff: + return {"count": None, "table": None} + project_settings = ProjectSettings.for_request(self.request) + + paf_approvals = get_paf_for_review( + user=self.request.user, + is_paf_approval_sequential=project_settings.paf_approval_sequential, + ) + paf_table = PAFForReviewDashboardTable( + paf_approvals, prefix="paf-review-", order_by="-date_requested" + ) + RequestConfig(self.request, paginate=False).configure(paf_table) + + return { + "count": paf_approvals.count(), + "table": paf_table, + } + + def my_tasks(self): + tasks = render_task_templates_for_user(self.request, self.request.user) + return { + "count": len(tasks), + "data": tasks, + } + def awaiting_reviews(self, submissions): submissions = submissions.in_review_for(self.request.user).order_by( "-submit_time" @@ -140,109 +168,11 @@ class AdminDashboardView(MyFlaggedMixin, TemplateView): return { "count": projects.count(), "filterset": filterset, - "table": ProjectsDashboardTable(projects[:limit]), + "table": ProjectsDashboardTable(data=projects[:limit], prefix="project-"), "display_more": projects.count() > limit, "url": reverse("apply:projects:all"), } - def paf_waiting_for_approver_assignment(self): - project_settings = ProjectSettings.for_request(self.request) - - paf_approvals = PAFApprovals.objects.annotate( - roles_count=Count("paf_reviewer_role__user_roles") - ).filter( - roles_count=len(list(self.request.user.groups.all())), - approved=False, - user__isnull=True, - ) - - for role in self.request.user.groups.all(): - paf_approvals = paf_approvals.filter( - paf_reviewer_role__user_roles__id=role.id - ) - - paf_approvals_ids = paf_approvals.values_list("id", flat=True) - projects = Project.objects.filter( - paf_approvals__id__in=paf_approvals_ids - ).for_table() - - if project_settings.paf_approval_sequential: - all_projects = list(projects) - for project in all_projects: - matched_paf_approval = ( - paf_approvals.filter(project=project) - .order_by("paf_reviewer_role__sort_order") - .first() - ) - if project.paf_approvals.filter( - paf_reviewer_role__sort_order__lt=matched_paf_approval.paf_reviewer_role.sort_order, - approved=False, - ).exists(): - projects = projects.exclude(id=project.id) - - return { - "count": projects.count(), - "table": ProjectsAssigneeDashboardTable(projects), - } - - def paf_waiting_for_approval(self): - if ( - not self.request.user.is_apply_staff - or not PAFApprovals.objects.filter( - project__status=INTERNAL_APPROVAL, - user=self.request.user, - ).exists() - ): - return { - "count": None, - "awaiting_your_approval": { - "count": None, - "table": None, - }, - "approved_by_you": { - "count": None, - "table": None, - }, - } - - waiting_paf_approval = Project.objects.internal_approval().for_table() - project_settings = ProjectSettings.for_request(self.request) - if project_settings.paf_approval_sequential: - awaiting_user_approval = [] - for waiting_project in waiting_paf_approval.filter( - paf_approvals__approved=False - ): - permission, _ = has_permission( - "paf_status_update", - self.request.user, - object=waiting_project, - raise_exception=False, - request=self.request, - ) - if permission: - awaiting_user_approval.append(waiting_project) - else: - awaiting_user_approval = waiting_paf_approval.filter( - paf_approvals__user=self.request.user, - paf_approvals__approved=False, - ) - approved_by_user = waiting_paf_approval.filter( - paf_approvals__user=self.request.user, - paf_approvals__approved=True, - ) - - return { - "count": len(awaiting_user_approval) + len(approved_by_user), - "awaiting_your_approval": { - "count": len(awaiting_user_approval), - "table": ProjectsDashboardTable(data=awaiting_user_approval), - }, - "approved_by_you": { - "count": len(approved_by_user), - "table": ProjectsDashboardTable(data=approved_by_user), - }, - } - def my_reviewed(self, submissions): """Staff reviewer's reviewed submissions for 'Previous reviews' block""" submissions = submissions.reviewed_by(self.request.user).order_by( @@ -288,13 +218,39 @@ class FinanceDashboardView(MyFlaggedMixin, TemplateView): "active_invoices": self.active_invoices(), "invoices_for_approval": self.invoices_for_approval(), "invoices_to_convert": self.invoices_to_convert(), - "paf_waiting_for_approval": self.paf_waiting_for_approval(), - "paf_waiting_for_assignment": self.paf_waiting_for_approver_assignment(), + "paf_for_review": self.paf_for_review(), + "my_tasks": self.my_tasks(), } ) return context + def paf_for_review(self): + if not self.request.user.is_finance: + return {"count": None, "table": None} + project_settings = ProjectSettings.for_request(self.request) + + paf_approvals = get_paf_for_review( + user=self.request.user, + is_paf_approval_sequential=project_settings.paf_approval_sequential, + ) + paf_table = PAFForReviewDashboardTable( + paf_approvals, prefix="paf-review-", order_by="-date_requested" + ) + RequestConfig(self.request, paginate=False).configure(paf_table) + + return { + "count": paf_approvals.count(), + "table": paf_table, + } + + def my_tasks(self): + tasks = render_task_templates_for_user(self.request, self.request.user) + return { + "count": len(tasks), + "data": tasks, + } + def active_invoices(self): if self.request.user.is_finance_level_2: invoices = Invoice.objects.for_finance_2() @@ -306,46 +262,6 @@ class FinanceDashboardView(MyFlaggedMixin, TemplateView): "table": InvoiceDashboardTable(invoices), } - def paf_waiting_for_approver_assignment(self): - project_settings = ProjectSettings.for_request(self.request) - - paf_approvals = PAFApprovals.objects.annotate( - roles_count=Count("paf_reviewer_role__user_roles") - ).filter( - roles_count=len(list(self.request.user.groups.all())), - approved=False, - user__isnull=True, - ) - - for role in self.request.user.groups.all(): - paf_approvals = paf_approvals.filter( - paf_reviewer_role__user_roles__id=role.id - ) - - paf_approvals_ids = paf_approvals.values_list("id", flat=True) - projects = Project.objects.filter( - paf_approvals__id__in=paf_approvals_ids - ).for_table() - - if project_settings.paf_approval_sequential: - all_projects = list(projects) - for project in all_projects: - matched_paf_approval = ( - paf_approvals.filter(project=project) - .order_by("paf_reviewer_role__sort_order") - .first() - ) - if project.paf_approvals.filter( - paf_reviewer_role__sort_order__lt=matched_paf_approval.paf_reviewer_role.sort_order, - approved=False, - ).exists(): - projects = projects.exclude(id=project.id) - - return { - "count": projects.count(), - "table": ProjectsAssigneeDashboardTable(projects), - } - def invoices_for_approval(self): if self.request.user.is_finance_level_2: invoices = Invoice.objects.approved_by_finance_1() @@ -366,64 +282,6 @@ class FinanceDashboardView(MyFlaggedMixin, TemplateView): "table": InvoiceDashboardTable(invoices), } - def paf_waiting_for_approval(self): - if ( - not self.request.user.is_finance - or not PAFApprovals.objects.filter( - project__status=INTERNAL_APPROVAL, - user=self.request.user, - ).exists() - ): - return { - "count": None, - "awaiting_your_approval": { - "count": None, - "table": None, - }, - "approved_by_you": { - "count": None, - "table": None, - }, - } - - waiting_paf_approval = Project.objects.internal_approval().for_table() - project_settings = ProjectSettings.for_request(self.request) - if project_settings.paf_approval_sequential: - awaiting_user_approval = [] - for waiting_project in waiting_paf_approval.filter( - paf_approvals__approved=False - ): - permission, _ = has_permission( - "paf_status_update", - self.request.user, - object=waiting_project, - raise_exception=False, - request=self.request, - ) - if permission: - awaiting_user_approval.append(waiting_project) - else: - awaiting_user_approval = waiting_paf_approval.filter( - paf_approvals__user=self.request.user, - paf_approvals__approved=False, - ) - approved_by_user = waiting_paf_approval.filter( - paf_approvals__user=self.request.user, - paf_approvals__approved=True, - ) - - return { - "count": len(awaiting_user_approval) + len(approved_by_user), - "awaiting_your_approval": { - "count": len(awaiting_user_approval), - "table": ProjectsDashboardTable(data=awaiting_user_approval), - }, - "approved_by_you": { - "count": len(approved_by_user), - "table": ProjectsDashboardTable(data=approved_by_user), - }, - } - class ReviewerDashboardView(MyFlaggedMixin, MySubmissionContextMixin, TemplateView): template_name = "dashboard/reviewer_dashboard.html" @@ -463,7 +321,6 @@ class ReviewerDashboardView(MyFlaggedMixin, MySubmissionContextMixin, TemplateVi "awaiting_reviews": self.awaiting_reviews(submissions), "my_reviewed": self.my_reviewed(submissions), "my_flagged": self.my_flagged(submissions), - "paf_waiting_for_assignment": self.paf_waiting_for_approver_assignment(), } ) @@ -486,46 +343,6 @@ class ReviewerDashboardView(MyFlaggedMixin, MySubmissionContextMixin, TemplateVi "table": ReviewerSubmissionsTable(submissions[:limit], prefix="my-review-"), } - def paf_waiting_for_approver_assignment(self): - project_settings = ProjectSettings.for_request(self.request) - - paf_approvals = PAFApprovals.objects.annotate( - roles_count=Count("paf_reviewer_role__user_roles") - ).filter( - roles_count=len(list(self.request.user.groups.all())), - approved=False, - user__isnull=True, - ) - - for role in self.request.user.groups.all(): - paf_approvals = paf_approvals.filter( - paf_reviewer_role__user_roles__id=role.id - ) - - paf_approvals_ids = paf_approvals.values_list("id", flat=True) - projects = Project.objects.filter( - paf_approvals__id__in=paf_approvals_ids - ).for_table() - - if project_settings.paf_approval_sequential: - all_projects = list(projects) - for project in all_projects: - matched_paf_approval = ( - paf_approvals.filter(project=project) - .order_by("paf_reviewer_role__sort_order") - .first() - ) - if project.paf_approvals.filter( - paf_reviewer_role__sort_order__lt=matched_paf_approval.paf_reviewer_role.sort_order, - approved=False, - ).exists(): - projects = projects.exclude(id=project.id) - - return { - "count": projects.count(), - "table": ProjectsAssigneeDashboardTable(projects), - } - def my_reviewed(self, submissions): """Staff reviewer's reviewed submissions for 'Previous reviews' block""" submissions = submissions.reviewed_by(self.request.user).order_by( @@ -582,110 +399,38 @@ class ContractingDashboardView(MyFlaggedMixin, TemplateView): context = super().get_context_data(**kwargs) context.update( { - "paf_waiting_for_approval": self.paf_waiting_for_approval(), "projects_in_contracting": self.projects_in_contracting(), - "paf_waiting_for_assignment": self.paf_waiting_for_approver_assignment(), + "paf_for_review": self.paf_for_review(), + "my_tasks": self.my_tasks(), } ) return context - def paf_waiting_for_approval(self): - if ( - not self.request.user.is_contracting - or not PAFApprovals.objects.filter( - project__status=INTERNAL_APPROVAL, - user=self.request.user, - ).exists() - ): - return { - "count": None, - "awaiting_your_approval": { - "count": None, - "table": None, - }, - "approved_by_you": { - "count": None, - "table": None, - }, - } - - waiting_paf_approval = Project.objects.internal_approval().for_table() + def paf_for_review(self): + if not self.request.user.is_contracting: + return {"count": None, "table": None} project_settings = ProjectSettings.for_request(self.request) - if project_settings.paf_approval_sequential: - awaiting_user_approval = [] - for waiting_project in waiting_paf_approval.filter( - paf_approvals__approved=False - ): - permission, _ = has_permission( - "paf_status_update", - self.request.user, - object=waiting_project, - raise_exception=False, - request=self.request, - ) - if permission: - awaiting_user_approval.append(waiting_project) - else: - awaiting_user_approval = waiting_paf_approval.filter( - paf_approvals__user=self.request.user, - paf_approvals__approved=False, - ) - approved_by_user = waiting_paf_approval.filter( - paf_approvals__user=self.request.user, - paf_approvals__approved=True, + + paf_approvals = get_paf_for_review( + user=self.request.user, + is_paf_approval_sequential=project_settings.paf_approval_sequential, ) + paf_table = PAFForReviewDashboardTable( + paf_approvals, prefix="paf-review-", order_by="-date_requested" + ) + RequestConfig(self.request, paginate=False).configure(paf_table) return { - "count": len(awaiting_user_approval) + len(approved_by_user), - "awaiting_your_approval": { - "count": len(awaiting_user_approval), - "table": ProjectsDashboardTable(data=awaiting_user_approval), - }, - "approved_by_you": { - "count": len(approved_by_user), - "table": ProjectsDashboardTable(data=approved_by_user), - }, + "count": paf_approvals.count(), + "table": paf_table, } - def paf_waiting_for_approver_assignment(self): - project_settings = ProjectSettings.for_request(self.request) - - paf_approvals = PAFApprovals.objects.annotate( - roles_count=Count("paf_reviewer_role__user_roles") - ).filter( - roles_count=len(list(self.request.user.groups.all())), - approved=False, - user__isnull=True, - ) - - for role in self.request.user.groups.all(): - paf_approvals = paf_approvals.filter( - paf_reviewer_role__user_roles__id=role.id - ) - - paf_approvals_ids = paf_approvals.values_list("id", flat=True) - projects = Project.objects.filter( - paf_approvals__id__in=paf_approvals_ids - ).for_table() - - if project_settings.paf_approval_sequential: - all_projects = list(projects) - for project in all_projects: - matched_paf_approval = ( - paf_approvals.filter(project=project) - .order_by("paf_reviewer_role__sort_order") - .first() - ) - if project.paf_approvals.filter( - paf_reviewer_role__sort_order__lt=matched_paf_approval.paf_reviewer_role.sort_order, - approved=False, - ).exists(): - projects = projects.exclude(id=project.id) - + def my_tasks(self): + tasks = render_task_templates_for_user(self.request, self.request.user) return { - "count": projects.count(), - "table": ProjectsAssigneeDashboardTable(projects), + "count": len(tasks), + "data": tasks, } def projects_in_contracting(self): @@ -712,11 +457,16 @@ class ContractingDashboardView(MyFlaggedMixin, TemplateView): "count": projects_in_contracting.count(), "waiting_for_contract": { "count": waiting_for_contract.count(), - "table": ProjectsDashboardTable(data=waiting_for_contract), + "table": ProjectsDashboardTable( + data=waiting_for_contract, prefix="project-waiting-contract-" + ), }, "waiting_for_contract_approval": { "count": waiting_for_contract_approval.count(), - "table": ProjectsDashboardTable(data=waiting_for_contract_approval), + "table": ProjectsDashboardTable( + data=waiting_for_contract_approval, + prefix="project-waiting-approval-", + ), }, } @@ -772,10 +522,19 @@ class ApplicantDashboardView(TemplateView): context = super().get_context_data(**kwargs) context["my_active_submissions"] = my_active_submissions context["active_projects"] = self.active_project_data() + context["active_invoices"] = self.active_invoices() context["historical_projects"] = self.historical_project_data() context["historical_submissions"] = self.historical_submission_data() + context["my_tasks"] = self.my_tasks() return context + def my_tasks(self): + tasks = render_task_templates_for_user(self.request, self.request.user) + return { + "count": len(tasks), + "data": tasks, + } + def active_project_data(self): active_projects = ( Project.objects.filter(user=self.request.user) @@ -801,13 +560,23 @@ class ApplicantDashboardView(TemplateView): for submission in active_subs: yield submission.from_draft() + def active_invoices(self): + active_invoices = ( + Invoice.objects.filter(project__user=self.request.user) + .exclude(status__in=[PAID, DECLINED]) + .order_by("-requested_at") + ) + return {"count": active_invoices.count(), "data": active_invoices} + def historical_project_data(self): historical_projects = ( Project.objects.filter(user=self.request.user).complete().for_table() ) return { "count": historical_projects.count(), - "table": ProjectsDashboardTable(data=historical_projects), + "table": ProjectsDashboardTable( + data=historical_projects, prefix="past-project-" + ), } def historical_submission_data(self): @@ -833,3 +602,14 @@ class DashboardView(ViewDispatcher): applicant_view = ApplicantDashboardView finance_view = FinanceDashboardView contracting_view = ContractingDashboardView + + def dispatch(self, request, *args, **kwargs): + response = super().dispatch(request, *args, **kwargs) + + # Handle the case when there is no dashboard for the user + # and redirect them to the home page of apply site. + # Suggestion: create a dedicated dashboard for user without any role. + if isinstance(response, HttpResponseForbidden): + return HttpResponseRedirect("/") + + return response diff --git a/hypha/apply/determinations/forms.py b/hypha/apply/determinations/forms.py index 9a337cf29164fa0bd1808bc12c4e50117550fedc..1315c7d45ccc6c3fd790e8746e7dad78f8724ffd 100644 --- a/hypha/apply/determinations/forms.py +++ b/hypha/apply/determinations/forms.py @@ -520,8 +520,11 @@ class DeterminationModelForm(StreamBaseForm, forms.ModelForm, metaclass=MixedMet self.fields[field].disabled = True if self.draft_button_name in self.data: - for field in self.fields.values(): - field.required = False + # A determination must be set for saving a draft, + # this forces outcome to be validated. + unreq_fields = [name for name in self.fields if name != "outcome"] + for name in unreq_fields: + self.fields[name].required = False if edit: self.fields.pop("outcome") @@ -548,8 +551,8 @@ class DeterminationModelForm(StreamBaseForm, forms.ModelForm, metaclass=MixedMet self.instance.outcome = int( self.cleaned_data[self.instance.determination_field.id] ) - # Need to catch KeyError as outcome field would not exist in case of edit. except KeyError: + # Need to catch KeyError as outcome field would not exist in case of edit. pass self.instance.is_draft = self.draft_button_name in self.data self.instance.form_data = self.cleaned_data["form_data"] diff --git a/hypha/apply/determinations/templates/determinations/base_determination_form.html b/hypha/apply/determinations/templates/determinations/base_determination_form.html index d575ce5492f4e1639a4baafd3c34cf6cbff9d91d..6d59488bcc3cf860944fa0d7e5804dbda502712e 100644 --- a/hypha/apply/determinations/templates/determinations/base_determination_form.html +++ b/hypha/apply/determinations/templates/determinations/base_determination_form.html @@ -39,7 +39,7 @@ {% block form_buttons %} <div class="form__group"> {% if form.draft_button_name %} - <button class="button button--submit button--white" type="submit" name="{{ form.draft_button_name }}" formnovalidate>{% trans "Save draft" %}</button> + <button class="button button--submit button--white" type="submit" name="{{ form.draft_button_name }}">{% trans "Save draft" %}</button> {% endif %} <button class="button button--submit button--primary" :disabled="isFormSubmitting" type="submit" name="submit">{% trans "Submit" %}</button> </div> diff --git a/hypha/apply/funds/blocks.py b/hypha/apply/funds/blocks.py index 8b89f248783cdfdcfbf07ecc55806d0200da311a..4984c4823afbabe4f2d81c1c5d11e09573a360a3 100644 --- a/hypha/apply/funds/blocks.py +++ b/hypha/apply/funds/blocks.py @@ -51,6 +51,8 @@ class ValueBlock(ApplicationSingleIncludeFieldBlock): icon = "decimal" def prepare_data(self, value, data, serialize): + if not data: + return data return format_number_as_currency(str(data)) @@ -99,6 +101,8 @@ class AddressFieldBlock(ApplicationSingleIncludeFieldBlock): return ", ".join(data[field] for field in ADDRESS_FIELDS_ORDER if data[field]) def prepare_data(self, value, data, serialize): + if not data: + return data data = json.loads(data) data = {field: data[field] for field in ADDRESS_FIELDS_ORDER} @@ -193,6 +197,8 @@ class DurationBlock(ApplicationSingleIncludeFieldBlock): return field_kwargs def prepare_data(self, value, data, serialize): + if not data: + return data if value["duration_type"] == self.DAYS: return self.DURATION_DAY_OPTIONS[int(data)] if value["duration_type"] == self.WEEKS: diff --git a/hypha/apply/funds/differ.py b/hypha/apply/funds/differ.py index 322fd86895f0e2629699bb58058d5d70ab178145..66ccf9ffbd9db78fc104f2b7dad267ee1f4b42ca 100644 --- a/hypha/apply/funds/differ.py +++ b/hypha/apply/funds/differ.py @@ -6,25 +6,21 @@ from django.utils.html import format_html from django.utils.safestring import mark_safe -def wrap_with_span(text, class_name): +def wrap_deleted(text): return format_html( - '<span class="diff diff__{}">{}</span>', class_name, mark_safe(text) + '<span class="bg-red-200 line-through">{}</span>', mark_safe(text) ) -def wrap_deleted(text): - return wrap_with_span(text, "deleted") - - def wrap_added(text): - return wrap_with_span(text, "added") + return format_html('<span class="bg-green-200">{}</span>', mark_safe(text)) def compare(answer_a, answer_b, should_bleach=True): if should_bleach: cleaner = Cleaner(tags=["h4"], attributes={}, strip=True) - answer_a = re.sub("(<li[^>]*>)", r"\1â— ", answer_a) - answer_b = re.sub("(<li[^>]*>)", r"\1â— ", answer_b) + answer_a = re.sub("(<li[^>]*>)", r"\1â—¦ ", answer_a) + answer_b = re.sub("(<li[^>]*>)", r"\1â—¦ ", answer_b) answer_a = cleaner.clean(answer_a) answer_b = cleaner.clean(answer_b) @@ -37,20 +33,20 @@ def compare(answer_a, answer_b, should_bleach=True): to_diff.append(mark_safe(diff.b[b0:b1])) elif opcode == "insert": from_diff.append(mark_safe(diff.a[a0:a1])) - to_diff.append(wrap_with_span(diff.b[b0:b1], "added")) + to_diff.append(wrap_added(diff.b[b0:b1])) elif opcode == "delete": - from_diff.append(wrap_with_span(diff.a[a0:a1], "deleted")) + from_diff.append(wrap_deleted(diff.a[a0:a1])) to_diff.append(mark_safe(diff.b[b0:b1])) elif opcode == "replace": - from_diff.append(wrap_with_span(diff.a[a0:a1], "deleted")) - to_diff.append(wrap_with_span(diff.b[b0:b1], "added")) + from_diff.append(wrap_deleted(diff.a[a0:a1])) + to_diff.append(wrap_added(diff.b[b0:b1])) from_display = "".join(from_diff) to_display = "".join(to_diff) - from_display = re.sub(r"([â—â—‹]|[0-9]{1,2}[\)\.])", r"<br>\1", from_display) - to_display = re.sub(r"([â—â—‹]|[0-9]{1,2}[\)\.])", r"<br>\1", to_display) from_display = re.sub("(\\.\n)", r"\1<br><br>", from_display) to_display = re.sub("(\\.\n)", r"\1<br><br>", to_display) + from_display = re.sub(r"([â—¦])", r"<br>\1", from_display) + to_display = re.sub(r"([â—¦])", r"<br>\1", to_display) from_display = mark_safe(from_display) to_display = mark_safe(to_display) diff --git a/hypha/apply/funds/migrations/0113_alter_assignedreviewers_reviewer.py b/hypha/apply/funds/migrations/0113_alter_assignedreviewers_reviewer.py new file mode 100644 index 0000000000000000000000000000000000000000..1efafcb7f21b596f0e06529bf8939a31c8ffd153 --- /dev/null +++ b/hypha/apply/funds/migrations/0113_alter_assignedreviewers_reviewer.py @@ -0,0 +1,27 @@ +# Generated by Django 3.2.22 on 2023-11-01 15:18 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("funds", "0112_add_organization_name"), + ] + + operations = [ + migrations.AlterField( + model_name="assignedreviewers", + name="reviewer", + field=models.ForeignKey( + limit_choices_to={ + "groups__name__in": ["Staff", "Reviewer", "Community Reviewer"], + "is_active": True, + }, + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ] diff --git a/hypha/apply/funds/migrations/0114_alter_assignedreviewers_reviewer.py b/hypha/apply/funds/migrations/0114_alter_assignedreviewers_reviewer.py new file mode 100644 index 0000000000000000000000000000000000000000..1d07aadbe582ba649d1caadb93a3ae130282c312 --- /dev/null +++ b/hypha/apply/funds/migrations/0114_alter_assignedreviewers_reviewer.py @@ -0,0 +1,27 @@ +# Generated by Django 4.2.7 on 2023-11-29 20:04 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("funds", "0113_alter_assignedreviewers_reviewer"), + ] + + operations = [ + migrations.AlterField( + model_name="assignedreviewers", + name="reviewer", + field=models.ForeignKey( + limit_choices_to={ + "groups__name__in": ["Staff", "Reviewer", "Community reviewer"], + "is_active": True, + }, + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ] diff --git a/hypha/apply/funds/models/submissions.py b/hypha/apply/funds/models/submissions.py index ae0a1f8a573db084fe74783e7cd0c9b3d447713c..5af8a58e67b19b5b74dca2c099c33d52bea3c35f 100644 --- a/hypha/apply/funds/models/submissions.py +++ b/hypha/apply/funds/models/submissions.py @@ -547,11 +547,11 @@ class ApplicationSubmission( def ensure_user_has_account(self): if self.user and self.user.is_authenticated: self.form_data["email"] = self.user.email - self.form_data["full_name"] = self.user.get_full_name() - # Ensure applying user should have applicant role - if not self.user.is_applicant: - applicant_group = Group.objects.get(name=APPLICANT_GROUP_NAME) - self.user.groups.add(applicant_group) + if name := self.user.get_full_name(): + self.form_data["full_name"] = name + else: + # user doesn't have name set, so use the one from the form + self.user.full_name = self.form_data["full_name"] self.user.save() else: # Rely on the form having the following must include fields (see blocks.py) @@ -564,11 +564,6 @@ class ApplicationSubmission( self.user, _ = User.objects.get_or_create( email=email, defaults={"full_name": full_name} ) - # Ensure applying user should have applicant role - if not self.user.is_applicant: - applicant_group = Group.objects.get(name=APPLICANT_GROUP_NAME) - self.user.groups.add(applicant_group) - self.user.save() else: self.user, _ = User.objects.get_or_create_and_notify( email=email, @@ -576,6 +571,12 @@ class ApplicationSubmission( defaults={"full_name": full_name}, ) + # Make sure the user is in the applicant group + if not self.user.is_applicant: + applicant_group = Group.objects.get(name=APPLICANT_GROUP_NAME) + self.user.groups.add(applicant_group) + self.user.save() + def get_from_parent(self, attribute): try: return getattr(self.round.specific, attribute) diff --git a/hypha/apply/funds/models/utils.py b/hypha/apply/funds/models/utils.py index f7267b5c24131f4bd02b27baeeb421c6314f8bb6..dd52fbe334097e54ea86cd1a07eedee8f6693994 100644 --- a/hypha/apply/funds/models/utils.py +++ b/hypha/apply/funds/models/utils.py @@ -1,4 +1,5 @@ from django.db import models +from django.shortcuts import redirect from django.urls import reverse from django.utils.translation import gettext_lazy as _ from wagtail.admin.panels import ( @@ -130,7 +131,7 @@ class WorkflowStreamForm(WorkflowHelpers, AbstractStreamForm): # type: ignore source=form_submission, ) - return super().render_landing_page(request, form_submission, *args, **kwargs) + return redirect("apply:submissions:success", pk=form_submission.id) content_panels = AbstractStreamForm.content_panels + [ FieldPanel("workflow_name"), diff --git a/hypha/apply/funds/permissions.py b/hypha/apply/funds/permissions.py index b8d63e7721d325cfcfa28dd6693da04b438c85ac..c10b6f17df91c5fa90544a1f825c1bf7ac2d721c 100644 --- a/hypha/apply/funds/permissions.py +++ b/hypha/apply/funds/permissions.py @@ -1,6 +1,8 @@ from django.conf import settings from django.core.exceptions import PermissionDenied +from ..users.groups import STAFF_GROUP_NAME, SUPERADMIN, TEAMADMIN_GROUP_NAME + def has_permission(action, user, object=None, raise_exception=True): value, reason = permissions_map[action](user, object) @@ -28,16 +30,64 @@ def can_bulk_delete_submissions(user) -> bool: return False -def can_access_archived_submissions(user): - if user.is_apply_staff and settings.SUBMISSIONS_ARCHIVED_ACCESS_STAFF: +def get_archive_view_groups() -> list: + """ + Returns a list of groups that can view archived submissions + """ + + archive_access_view_groups = [SUPERADMIN] + + if settings.SUBMISSIONS_ARCHIVED_VIEW_ACCESS_STAFF: + archive_access_view_groups.append(STAFF_GROUP_NAME) + if settings.SUBMISSIONS_ARCHIVED_VIEW_ACCESS_STAFF_ADMIN: + archive_access_view_groups.append(TEAMADMIN_GROUP_NAME) + + return archive_access_view_groups + + +def can_view_archived_submissions(user) -> bool: + """ + Return a boolean based on if a user can view archived submissions + """ + archive_view_groups = get_archive_view_groups() + + if user.is_apply_staff and STAFF_GROUP_NAME in archive_view_groups: + return True + if user.is_apply_staff_admin and TEAMADMIN_GROUP_NAME in archive_view_groups: + return True + return False + + +def get_archive_alter_groups() -> list: + """ + Returns a list of groups that can archive & unarchive submissions + """ + + archive_access_groups = [SUPERADMIN] + + if settings.SUBMISSIONS_ARCHIVED_ACCESS_STAFF: + archive_access_groups.append(STAFF_GROUP_NAME) + if settings.SUBMISSIONS_ARCHIVED_ACCESS_STAFF_ADMIN: + archive_access_groups.append(TEAMADMIN_GROUP_NAME) + + return archive_access_groups + + +def can_alter_archived_submissions(user) -> bool: + """ + Return a boolean based on if a user can alter archived submissions + """ + archive_access_groups = get_archive_alter_groups() + + if user.is_apply_staff and STAFF_GROUP_NAME in archive_access_groups: return True - if user.is_apply_staff_admin and settings.SUBMISSIONS_ARCHIVED_ACCESS_STAFF_ADMIN: + if user.is_apply_staff_admin and TEAMADMIN_GROUP_NAME in archive_access_groups: return True return False def can_bulk_archive_submissions(user) -> bool: - if can_access_archived_submissions(user) and can_bulk_delete_submissions(user): + if can_alter_archived_submissions(user) and can_bulk_delete_submissions(user): return True return False @@ -87,7 +137,7 @@ def is_user_has_access_to_view_submission(user, submission): if not user.is_authenticated: return False, "Login Required" - if submission.is_archive and not can_access_archived_submissions(user): + if submission.is_archive and not can_view_archived_submissions(user): return False, "Archived Submission" if user.is_apply_staff or submission.user == user or user.is_reviewer: diff --git a/hypha/apply/funds/tables.py b/hypha/apply/funds/tables.py index cf45b752711bdaf9d0c86443d65ee14e0d42728f..d381df11fa0ba718f10d444c018f5d83cd957ce2 100644 --- a/hypha/apply/funds/tables.py +++ b/hypha/apply/funds/tables.py @@ -176,7 +176,19 @@ class BaseAdminSubmissionsTable(SubmissionsTable): organization_name = tables.Column() class Meta(SubmissionsTable.Meta): - fields = ("title", "phase", "stage", "fund", "round", "lead", "submit_time", "last_update", "screening_status", "reviews_stats", "organization_name") # type: ignore + fields = ( + "title", + "phase", + "stage", + "fund", + "round", + "lead", + "submit_time", + "last_update", + "screening_status", + "reviews_stats", + "organization_name", + ) sequence = fields + ("comments",) def render_lead(self, value): diff --git a/hypha/apply/funds/templates/funds/applicationsubmission_detail.html b/hypha/apply/funds/templates/funds/applicationsubmission_detail.html index 13898bd66b93d907670c3c0f8a74a1e202bb1bb3..d672e19ab0d144284d6069ca7387dc2191abf0a8 100644 --- a/hypha/apply/funds/templates/funds/applicationsubmission_detail.html +++ b/hypha/apply/funds/templates/funds/applicationsubmission_detail.html @@ -1,5 +1,5 @@ {% extends "base-apply.html" %} -{% load i18n static workflow_tags wagtailcore_tags statusbar_tags %} +{% load i18n static workflow_tags wagtailcore_tags statusbar_tags archive_tags %} {% load heroicons %} {% block title %}{{ object.title }}{% endblock %} @@ -9,7 +9,6 @@ <ul class="messages"> {% trans "This submission is sealed" as warning_message %} {% include "includes/message_item.html" with message=warning_message tag="warning" close=False %} - } </ul> {% endif %} <div class="admin-bar"> @@ -73,7 +72,7 @@ {% if object.is_archive %} <div class="py-2 bg-red-600 text-white font-bold text-center"> {% heroicon_outline "lock-closed" aria_hidden="true" size=16 stroke_width=2 class="inline align-baseline mr-1" %} - {% trans "This submission has been archived." %} + {% trans "This submission has been archived. This is visible to the roles " %} {{ archive_access_groups|join_with_commas }} </div> {% endif %} @@ -178,6 +177,7 @@ <div class="tabs__content" id="tab-2"> <div class="feed"> {% if not object.is_archive %} + <h4 class="m-0">{% trans "Add communication" %}</h4> {% include "activity/include/comment_form.html" %} {% include "activity/include/comment_list.html" with editable=True %} {% else %} diff --git a/hypha/apply/funds/templates/funds/includes/admin_primary_actions.html b/hypha/apply/funds/templates/funds/includes/admin_primary_actions.html index 117564366f424709cbe7d9a98eb12fabff6d506d..03cb4af92090e5b425fb82ae244fb5e3bba79586 100644 --- a/hypha/apply/funds/templates/funds/includes/admin_primary_actions.html +++ b/hypha/apply/funds/templates/funds/includes/admin_primary_actions.html @@ -69,9 +69,11 @@ <a data-fancybox data-src="#create-reminder" class="button button--white button--full-width button--bottom-space" href="#">{% trans "Create Reminder" %}</a> - <a data-fancybox data-src="#archive-submission" class="button button--white button--full-width button--bottom-space" href="#"> - {% heroicon_outline "lock-closed" aria_hidden="true" size=15 stroke_width=2 class="inline align-baseline mr-1" %} - {% trans "Archive Submission" %} - </a> + {% if can_archive %} + <a data-fancybox data-src="#archive-submission" class="button button--white button--full-width button--bottom-space" href="#"> + {% heroicon_outline "lock-closed" aria_hidden="true" size=15 stroke_width=2 class="inline align-baseline mr-1" %} + {% trans "Archive Submission" %} + </a> + {% endif %} </details> {% endif %} diff --git a/hypha/apply/funds/templates/funds/includes/delegated_form_base.html b/hypha/apply/funds/templates/funds/includes/delegated_form_base.html index fd2127f1e8e67db47267a2855921d5843742a8f3..33a7eb412caffe173f09fafbc3def9859a226ce2 100644 --- a/hypha/apply/funds/templates/funds/includes/delegated_form_base.html +++ b/hypha/apply/funds/templates/funds/includes/delegated_form_base.html @@ -12,13 +12,16 @@ {% for hidden in form.hidden_fields %} {{ hidden }} {% endfor %} - {% for field in form.visible_fields %} - {% if field.field %} - {% include "forms/includes/field.html" %} - {% else %} - {{ field }} - {% endif %} - {% endfor %} + + <div class="fields--visible"> + {% for field in form.visible_fields %} + {% if field.field %} + {% include "forms/includes/field.html" %} + {% else %} + {{ field }} + {% endif %} + {% endfor %} + </div> <div class="form__group"> {% if cancel %} diff --git a/hypha/apply/funds/templates/funds/includes/revision_diff_table.html b/hypha/apply/funds/templates/funds/includes/revision_diff_table.html deleted file mode 100644 index 6017274d367d9d3baeb7cffe56f9bd51fcdaa4d0..0000000000000000000000000000000000000000 --- a/hypha/apply/funds/templates/funds/includes/revision_diff_table.html +++ /dev/null @@ -1,25 +0,0 @@ -{% load i18n %} -<table class="revision-diff-table"> - <tr><td><h3>{% trans "Proposal Information" %}</h3></td><td><h3>{% trans "Proposal Information" %}</h3></td></tr> - <tr><td><h5>{% trans "Submitted" %}</h5>{{ timestamps.0|date:"DATETIME_FORMAT"}}</td><td><h5>{% trans "Submitted" %}</h5>{{ timestamps.1|date:"DATETIME_FORMAT"}}</td></tr> - {% for from_field, to_field in required_fields %} - {% if forloop.first %} - <tr><td><h4>{% trans "Title" %}</h4>{{ from_field }}</td><td><h4>{% trans "Title" %}</h4>{{ to_field }}</td></tr> - {% elif forloop.counter == 2 %} - <tr><td><h5>{% trans "Legal Name" %}</h5>{{ from_field }}</td><td><h5>{% trans "Legal Name" %}</h5>{{ to_field }}</td></tr> - {% elif forloop.counter == 3 %} - <tr><td><h5>{% trans "E-mail" %}</h5>{{ from_field }}</td><td><h5>{% trans "E-mail" %}</h5>{{ to_field }}</td></tr> - {% elif forloop.counter == 4 %} - <tr><td><h5>{% trans "Requested Funding" %}</h5>{{ from_field }}</td><td><h5>{% trans "Requested Funding" %}</h5>{{ to_field }}</td></tr> - {% elif forloop.counter == 5 %} - <tr><td><h5>{% trans "Project Duration" %}</h5>{{ from_field }}</td><td><h5>{% trans "Project Duration" %}</h5>{{ to_field }}</td></tr> - {% elif forloop.counter == 6 %} - <tr><td><h5>{% trans "Address" %}</h5>{{ from_field }}</td><td><h5>{% trans "Address" %}</h5>{{ to_field }}</td></tr> - {% else %} - <tr><td>{{ from_field }}</td><td>{{ to_field }}</td></tr> - {% endif %} - {% endfor %} - {% for from_field, to_field in stream_fields %} - <tr><td>{{ from_field }}</td><td>{{ to_field }}</td></tr> - {% endfor %} -</table> diff --git a/hypha/apply/funds/templates/funds/includes/status_bar.html b/hypha/apply/funds/templates/funds/includes/status_bar.html index 3cdde65fe9d9748399b1cf90218d5eeb3fe6c233..beff7c317d8f0f282625c38d3696118496733383 100644 --- a/hypha/apply/funds/templates/funds/includes/status_bar.html +++ b/hypha/apply/funds/templates/funds/includes/status_bar.html @@ -1,5 +1,5 @@ {% load statusbar_tags %} -<div class="status-bar {{ class }}"> +<div class="status-bar my-6 {{ class }}"> {% for phase in phases %} {% ifchanged phase.step %} {% status_display current_phase phase public as display_text %} @@ -14,12 +14,12 @@ {% endifchanged %} {% endfor %} </div> -<div class="status-bar--mobile"> - <h6 class="status-bar__subheading"> +<div class="status-bar--mobile my-2"> + <div class="status-bar__subheading status-bar__text"> {% if public %} {{ current_phase.public_name }} {% else %} {{ current_phase }} {% endif %} - </h6> + </div> </div> diff --git a/hypha/apply/funds/templates/funds/includes/status_bar_item.html b/hypha/apply/funds/templates/funds/includes/status_bar_item.html index 0925a321983639b740cacd67ebc5f5fb44c68458..97aaf8362d6a9648eee2006f188d7e00ba91bc33 100644 --- a/hypha/apply/funds/templates/funds/includes/status_bar_item.html +++ b/hypha/apply/funds/templates/funds/includes/status_bar_item.html @@ -4,9 +4,5 @@ {% elif is_complete %} status-bar__item--is-complete {% endif %}"> - <span class="status-bar__tooltip" - data-title="{{ label }}" aria-label="{{ label }}"></span> - <svg class="status-bar__icon"> - <use xlink:href="#tick-alt"></use> - </svg> + <div class="status-bar__tooltip status-bar__text" style="--tooltip-chars:{{ label|length }}ch;">{{ label }}</div> </div> diff --git a/hypha/apply/funds/templates/funds/includes/table_filter_and_search.html b/hypha/apply/funds/templates/funds/includes/table_filter_and_search.html index f561b17a748b77283e10f70f9f7abfd90cbe936f..4b239395255ade00f707c97ed5817314a76ef8d0 100644 --- a/hypha/apply/funds/templates/funds/includes/table_filter_and_search.html +++ b/hypha/apply/funds/templates/funds/includes/table_filter_and_search.html @@ -52,10 +52,12 @@ {% trans "Delete" %} </button> - <button data-fancybox data-src="#batch-archive-submission" class="button button--action js-batch-button" type="button"> - <svg class="icon icon--archive"><use xlink:href="#archive"></use></svg> - {% trans "Archive" %} - </button> + {% if can_bulk_archive %} + <button data-fancybox data-src="#batch-archive-submission" class="button button--action js-batch-button" type="button"> + <svg class="icon icon--archive"><use xlink:href="#archive"></use></svg> + {% trans "Archive" %} + </button> + {% endif %} </div> {% endif %} </div> diff --git a/hypha/apply/funds/templates/funds/lab_type_landing.html b/hypha/apply/funds/templates/funds/lab_type_landing.html deleted file mode 100644 index b13d2c423b3ce21c2514adc6f1d628cfadf9eec0..0000000000000000000000000000000000000000 --- a/hypha/apply/funds/templates/funds/lab_type_landing.html +++ /dev/null @@ -1,4 +0,0 @@ -{% extends "funds/application_base_landing.html" %} -{% load wagtailcore_tags wagtailsettings_tags %} - -{% block extra_text %}{{ settings.funds.ApplicationSettings.extra_text_lab|richtext }}{% endblock %} diff --git a/hypha/apply/funds/templates/funds/revisions_compare.html b/hypha/apply/funds/templates/funds/revisions_compare.html index fbae8efe386da0951bd99f85dfd3fbb21318917b..f71d741389b46be1bd8d37f4abeaa4b6f470cc4c 100644 --- a/hypha/apply/funds/templates/funds/revisions_compare.html +++ b/hypha/apply/funds/templates/funds/revisions_compare.html @@ -17,7 +17,39 @@ {% endadminbar %} - <div class="wrapper wrapper--large wrapper--tabs"> - {% include "funds/includes/revision_diff_table.html" %} - </div> + <table class="revision-diff-table prose prose-h4:mt-0 max-w-none mb-8"> + <tr> + <td> + <h3>{% trans "Proposal Information (Old)" %}</h3> + <p>{% trans "Submitted" %}: <strong>{{ timestamps.0|date:"DATETIME_FORMAT"}}</strong></p> + </td> + <td> + <h3>{% trans "Proposal Information (New)" %}</h3> + <p>{% trans "Submitted" %}: <strong>{{ timestamps.1|date:"DATETIME_FORMAT"}}</strong></p> + </td> + </tr> + + {% for from_field, to_field in required_fields %} + {% if forloop.first %} + <tr><td><h4>{% trans "Title" %}</h4>{{ from_field }}</td><td><h4>{% trans "Title" %}</h4>{{ to_field }}</td></tr> + {% elif forloop.counter == 2 %} + <tr><td><h4>{% trans "Legal Name" %}</h4>{{ from_field }}</td><td><h4>{% trans "Legal Name" %}</h4>{{ to_field }}</td></tr> + {% elif forloop.counter == 3 %} + <tr><td><h4>{% trans "E-mail" %}</h4>{{ from_field }}</td><td><h4>{% trans "E-mail" %}</h4>{{ to_field }}</td></tr> + {% elif forloop.counter == 4 %} + <tr><td><h4>{% trans "Address" %}</h4>{{ from_field }}</td><td><h4>{% trans "Address" %}</h4>{{ to_field }}</td></tr> + {% elif forloop.counter == 5 %} + <tr><td><h4>{% trans "Project Duration" %}</h4>{{ from_field }}</td><td><h4>{% trans "Project Duration" %}</h4>{{ to_field }}</td></tr> + {% elif forloop.counter == 6 %} + <tr><td><h4>{% trans "Requested Funding" %}</h4>{{ from_field }}</td><td><h4>{% trans "Requested Funding" %}</h4>{{ to_field }}</td></tr> + + {% else %} + <tr><td>{{ from_field }}</td><td>{{ to_field }}</td></tr> + {% endif %} + {% endfor %} + {% for from_field, to_field in stream_fields %} + <tr><td>{{ from_field }}</td><td>{{ to_field }}</td></tr> + {% endfor %} + </table> + {% endblock %} diff --git a/hypha/apply/funds/templates/funds/round_landing.html b/hypha/apply/funds/templates/funds/round_landing.html deleted file mode 100644 index 0ed5e7f0e678b2c322fe7499bc599a42c822b686..0000000000000000000000000000000000000000 --- a/hypha/apply/funds/templates/funds/round_landing.html +++ /dev/null @@ -1,3 +0,0 @@ -{% extends "funds/application_base_landing.html" %} - -{% block page_title %}{{ page.get_parent.title }}{% endblock %} diff --git a/hypha/apply/funds/templates/funds/application_base_landing.html b/hypha/apply/funds/templates/funds/submission-success.html similarity index 59% rename from hypha/apply/funds/templates/funds/application_base_landing.html rename to hypha/apply/funds/templates/funds/submission-success.html index 1097899405aa599618c02bce4d53bf90da8b76f0..b032625997f310680e55861d2b1220e6c1afde70 100644 --- a/hypha/apply/funds/templates/funds/application_base_landing.html +++ b/hypha/apply/funds/templates/funds/submission-success.html @@ -29,25 +29,46 @@ </p> {% with email_context=page.specific %} - <p>{{ email_context.confirmation_text_extra|urlize }}</p> + {% if email_context.confirmation_text_extra %} + <p data-testid="db-confirmation-text-extra">{{ email_context.confirmation_text_extra|urlize }}</p> + {% endif %} {% endwith %} - {% block extra_text %} - <div class="prose"> + {% if form_submission.round and settings.funds.ApplicationSettings.extra_text_round %} + <div class="prose" data-testid="db-extra-text"> {{ settings.funds.ApplicationSettings.extra_text_round|richtext }} </div> - {% endblock %} + {% elif settings.funds.ApplicationSettings.extra_text_lab %} + <div class="prose" data-testid="db-extra-text"> + {{ settings.funds.ApplicationSettings.extra_text_lab|richtext }} + </div> + {% endif %} {% endif %} - <div class="mt-4"> - {% if request.user.is_authenticated %} + <div class="mt-4 space-x-2"> + {% if request.user.is_authenticated and request.user.can_access_dashboard%} <a class="button button--primary" href="{% url 'dashboard:dashboard' %}" > {% trans "Go to your dashboard" %} </a> + {% if form_submission.status == 'draft' %} + <a + class="button button--secondary" + href="{% url 'apply:submissions:edit' form_submission.id %}" + > + {% trans "Continue editing" %} + </a> + {% else %} + <a + class="button button--secondary" + href="{% url 'apply:submissions:detail' form_submission.id %}" + > + {% trans "View your submission" %} + </a> + {% endif %} {% else %} <a class="button button--primary" diff --git a/hypha/apply/funds/templatetags/archive_tags.py b/hypha/apply/funds/templatetags/archive_tags.py new file mode 100644 index 0000000000000000000000000000000000000000..652244e0faa7b5943a097bde0677034bdf4801b5 --- /dev/null +++ b/hypha/apply/funds/templatetags/archive_tags.py @@ -0,0 +1,22 @@ +from django import template + +register = template.Library() + + +@register.filter +def join_with_commas(obj_list: list): + """ + Takes a list of objects and returns their string representations, + separated by commas and with 'and' between the penultimate and final items + For example, for a list of fruit objects: + [<Fruit: apples>, <Fruit: oranges>, <Fruit: pears>] -> 'apples, oranges and pears' + + Inspired by: https://stackoverflow.com/a/1242107 + """ + if not obj_list: + return "" + list_len = len(obj_list) + if list_len == 1: + return f"{obj_list[0]}" + + return f"{', '.join(str(obj) for obj in obj_list[:list_len-1])} and {obj_list[list_len-1]}" diff --git a/hypha/apply/funds/tests/test_models.py b/hypha/apply/funds/tests/test_models.py index 6259dc7afbb7e39cbcd9947a89fac3db90b11c39..9550a2cea07f4f2ad5474a70a1b24760ad011d09 100644 --- a/hypha/apply/funds/tests/test_models.py +++ b/hypha/apply/funds/tests/test_models.py @@ -243,8 +243,10 @@ class TestFormSubmission(TestCase): response = page.serve(request) if not ignore_errors: - # Check the data we submit is correct - self.assertNotContains(response, "errors") + # check it is redirected + self.assertEqual(response.status_code, 302) + # check "success" is present in the redirect url location + self.assertIn("success", response.url) return response def test_workflow_and_draft(self): @@ -312,9 +314,10 @@ class TestFormSubmission(TestCase): # Lead + 2 x applicant self.assertEqual(self.User.objects.count(), 3) - first_user, second_user = self.User.objects.get( - email=self.email - ), self.User.objects.get(email=email) + first_user, second_user = ( + self.User.objects.get(email=self.email), + self.User.objects.get(email=email), + ) self.assertEqual(ApplicationSubmission.objects.count(), 2) self.assertEqual(ApplicationSubmission.objects.first().user, first_user) self.assertEqual(ApplicationSubmission.objects.last().user, second_user) @@ -345,7 +348,7 @@ class TestFormSubmission(TestCase): self.assertEqual(self.User.objects.count(), 2) response = self.submit_form(email="", name="", user=user, ignore_errors=True) - self.assertEqual(response.status_code, 200) + assert response.status_code == 302 and "success" in response.url # Lead + applicant self.assertEqual(self.User.objects.count(), 2) diff --git a/hypha/apply/funds/tests/test_views.py b/hypha/apply/funds/tests/test_views.py index 054c9df1121a24f2843f868a1c07ce05a5d9c4fb..4cc8d634572ac71df2b13ea64ec1db8024d5164d 100644 --- a/hypha/apply/funds/tests/test_views.py +++ b/hypha/apply/funds/tests/test_views.py @@ -2,6 +2,7 @@ import re from datetime import timedelta from bs4 import BeautifulSoup +from django.conf import settings from django.contrib.auth.models import AnonymousUser from django.core.exceptions import PermissionDenied from django.http import Http404 @@ -1644,7 +1645,7 @@ class TestAnonSubmissionFileView(BaseSubmissionFileViewTestCase): self.assertEqual(response.status_code, 200) self.assertEqual(len(response.redirect_chain), 2) for path, _ in response.redirect_chain: - self.assertIn(reverse("users_public:login"), path) + self.assertIn(reverse(settings.LOGIN_URL), path) class BaseProjectDeleteTestCase(BaseViewTestCase): diff --git a/hypha/apply/funds/urls.py b/hypha/apply/funds/urls.py index 87809e3ec21201d688c934a7e032e91c34b2ac87..6b11739e0634c7961615f30e161f836ea0efc769 100644 --- a/hypha/apply/funds/urls.py +++ b/hypha/apply/funds/urls.py @@ -27,6 +27,7 @@ from .views import ( SubmissionSealedView, SubmissionStaffFlaggedView, SubmissionUserFlaggedView, + submission_success, ) from .views_beta import ( bulk_archive_submissions, @@ -74,6 +75,7 @@ app_name = "funds" submission_urls = ( [ path("", SubmissionOverviewView.as_view(), name="overview"), + path("success/<int:pk>/", submission_success, name="success"), path("all/", SubmissionListView.as_view(), name="list"), path("all-beta/", submission_all_beta, name="list-beta"), path("all-beta/bulk_archive/", bulk_archive_submissions, name="bulk-archive"), diff --git a/hypha/apply/funds/views.py b/hypha/apply/funds/views.py index b942613db05e05c7e5718ac7cc3b37ac67ecaf61..f0a75f95433bfce9fd63d018e0fae90cb5b9c8e5 100644 --- a/hypha/apply/funds/views.py +++ b/hypha/apply/funds/views.py @@ -9,11 +9,12 @@ from django.contrib import messages from django.contrib.auth import get_user_model from django.contrib.auth.decorators import login_required, permission_required from django.contrib.auth.mixins import UserPassesTestMixin +from django.contrib.auth.models import Group from django.contrib.humanize.templatetags.humanize import intcomma from django.core.exceptions import PermissionDenied from django.db.models import Count, F, Q from django.http import FileResponse, Http404, HttpResponse, HttpResponseRedirect -from django.shortcuts import get_object_or_404 +from django.shortcuts import get_object_or_404, render from django.urls import reverse_lazy from django.utils import timezone from django.utils.decorators import method_decorator @@ -52,7 +53,10 @@ from hypha.apply.projects.forms import CreateProjectForm from hypha.apply.projects.models import Project from hypha.apply.review.models import Review from hypha.apply.stream_forms.blocks import GroupToggleBlock +from hypha.apply.todo.options import PROJECT_WAITING_PAF +from hypha.apply.todo.views import add_task_to_user_group from hypha.apply.users.decorators import staff_or_finance_required, staff_required +from hypha.apply.users.groups import STAFF_GROUP_NAME from hypha.apply.utils.models import PDFPageSettings from hypha.apply.utils.pdfs import draw_submission_content, make_pdf from hypha.apply.utils.storage import PrivateMediaView @@ -93,9 +97,12 @@ from .models import ( RoundsAndLabs, ) from .permissions import ( - can_access_archived_submissions, can_access_drafts, + can_alter_archived_submissions, + can_bulk_archive_submissions, can_export_submissions, + can_view_archived_submissions, + get_archive_view_groups, has_permission, ) from .tables import ( @@ -123,6 +130,17 @@ from .workflow import ( User = get_user_model() +def submission_success(request, pk): + submission = get_object_or_404(ApplicationSubmission, pk=pk) + return render( + request, + "funds/submission-success.html", + { + "form_submission": submission, + }, + ) + + class SubmissionStatsMixin: def get_context_data(self, **kwargs): submissions = ApplicationSubmission.objects.exclude_draft() @@ -280,6 +298,10 @@ class BatchArchiveSubmissionView(DelegatedViewMixin, FormView): context_name = "batch_archive_submission_form" def form_valid(self, form): + # If a user without archive edit access is somehow able to access batch archive submissions + # (ie. they were looking at the submission list when permissions changed) "refresh" the page + if not can_alter_archived_submissions(self.request.user): + return HttpResponseRedirect(self.request.path) submissions = form.cleaned_data["submissions"] services.bulk_archive_submissions( submissions=submissions, @@ -530,10 +552,12 @@ class SubmissionAdminListView(BaseAdminSubmissionsTable, DelegateableListView): return submissions def get_context_data(self, **kwargs): - show_archive = can_access_archived_submissions(self.request.user) + show_archive = can_view_archived_submissions(self.request.user) + can_archive = can_bulk_archive_submissions(self.request.user) return super().get_context_data( show_archive=show_archive, + can_bulk_archive=can_archive, **kwargs, ) @@ -767,6 +791,12 @@ class CreateProjectView(DelegatedViewMixin, CreateView): source=self.object, related=self.object.submission, ) + # add task for staff to add PAF to the project + add_task_to_user_group( + code=PROJECT_WAITING_PAF, + user_group=Group.objects.filter(name=STAFF_GROUP_NAME), + related_obj=self.object, + ) return response def get_context_data(self, **kwargs): @@ -812,6 +842,10 @@ class UnarchiveSubmissionView(DelegatedViewMixin, UpdateView): context_name = "unarchive_form" def form_valid(self, form): + # If a user without archive edit access is somehow able to access "Unarchive Submission" + # (ie. they were looking at the submission when permissions changed) "refresh" the page + if not can_alter_archived_submissions(self.request.user): + return HttpResponseRedirect(self.request.path) response = super().form_valid(form) # Record activity messenger( @@ -833,6 +867,10 @@ class ArchiveSubmissionView(DelegatedViewMixin, UpdateView): context_name = "archive_form" def form_valid(self, form): + # If a user without archive edit access is somehow able to access "Archive Submission" + # (ie. they were looking at the submission when permissions changed) "refresh" the page + if not can_alter_archived_submissions(self.request.user): + return HttpResponseRedirect(self.request.path) response = super().form_valid(form) submission = self.get_object() # Record activity @@ -1073,10 +1111,13 @@ class AdminSubmissionDetailView(ActivityContextMixin, DelegateableView, DetailVi public_page = self.object.get_from_parent("detail")() default_screening_statuses = get_default_screening_statues() + return super().get_context_data( other_submissions=other_submissions, public_page=public_page, default_screening_statuses=default_screening_statuses, + archive_access_groups=get_archive_view_groups(), + can_archive=can_alter_archived_submissions(self.request.user), **kwargs, ) diff --git a/hypha/apply/funds/views_beta.py b/hypha/apply/funds/views_beta.py index 0f1cdb6475b3b3725a7f0c68c20b6f9a74bcc76a..a9a3984924818ae2888fa9fb8e2fc78d8c34e035 100644 --- a/hypha/apply/funds/views_beta.py +++ b/hypha/apply/funds/views_beta.py @@ -86,7 +86,7 @@ def submission_all_beta( selected_sort = request.GET.get("sort") page = request.GET.get("page", 1) - can_view_archives = permissions.can_access_archived_submissions(request.user) + can_view_archives = permissions.can_view_archived_submissions(request.user) selected_fund_objects = ( Page.objects.filter(id__in=selected_funds) if selected_funds else [] @@ -309,7 +309,9 @@ def bulk_update_submissions_status(request: HttpRequest) -> HttpResponse: submissions = ApplicationSubmission.objects.filter(id__in=submission_ids) - redirect: HttpResponse = BatchDeterminationCreateView.should_redirect(request, submissions, transitions) # type: ignore + redirect: HttpResponse = BatchDeterminationCreateView.should_redirect( + request, submissions, transitions + ) if redirect: return HttpResponseClientRedirect(redirect.url) diff --git a/hypha/apply/projects/constants.py b/hypha/apply/projects/constants.py new file mode 100644 index 0000000000000000000000000000000000000000..37422514990b85ad3dafd239d12158765e140958 --- /dev/null +++ b/hypha/apply/projects/constants.py @@ -0,0 +1,38 @@ +from django.conf import settings +from django.utils.translation import gettext_lazy as _ + +# INT refers Invoice table +INT_STAFF_PENDING = _("Staff pending") +INT_FINANCE_PENDING = _("Finance pending") +INT_VENDOR_PENDING = _("Vendor pending") + +# duplicate statuses +INT_DECLINED = _("Declined") +INT_PAID = _("Paid") +INT_PAYMENT_FAILED = _("Payment failed") + +# INVOICE_TABLE_STATUSES = [INT_STAFF_PENDING, INT_FINANCE_PENDING, INT_VENDOR_PENDING, INT_DECLINED, +# INT_PAID, INT_PAYMENT_FAILED] + +INT_ORG_PENDING = _("{} pending").format(settings.ORG_SHORT_NAME) +INT_REQUEST_FOR_CHANGE = _("Request for change") + +# INVOICE_TABLE_PUBLIC_STATUSES = [INT_ORG_PENDING, INT_REQUEST_FOR_CHANGE, +# INT_DECLINED, INT_PAID, INT_PAYMENT_FAILED] + + +INVOICE_STATUS_BG_COLORS = { + INT_ORG_PENDING: "bg-yellow-100", + INT_PAID: "bg-green-100", + INT_REQUEST_FOR_CHANGE: "bg-blue-100", + INT_PAYMENT_FAILED: "bg-red-100", + INT_DECLINED: "bg-pink-100", +} + +INVOICE_STATUS_FG_COLORS = { + INT_ORG_PENDING: "text-yellow-700", + INT_PAID: "text-green-700", + INT_REQUEST_FOR_CHANGE: "text-blue-700", + INT_PAYMENT_FAILED: "text-red-700", + INT_DECLINED: "text-pink-700", +} diff --git a/hypha/apply/projects/migrations/0081_alter_project_value.py b/hypha/apply/projects/migrations/0081_alter_project_value.py new file mode 100644 index 0000000000000000000000000000000000000000..bde87b1c87c3983179d2cc672fd4770c60655a7b --- /dev/null +++ b/hypha/apply/projects/migrations/0081_alter_project_value.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.8 on 2023-12-11 20:16 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("application_projects", "0080_alter_invoice_status"), + ] + + operations = [ + migrations.AlterField( + model_name="project", + name="value", + field=models.DecimalField( + decimal_places=2, + default=0, + max_digits=20, + validators=[django.core.validators.MinValueValidator(limit_value=0)], + ), + ), + ] diff --git a/hypha/apply/projects/models/project.py b/hypha/apply/projects/models/project.py index 6ab5d0ef7d7217997f25c9caeabdeb2a3da1baad..14aae7c1773049e6901f760983942c3085deba90 100644 --- a/hypha/apply/projects/models/project.py +++ b/hypha/apply/projects/models/project.py @@ -206,7 +206,12 @@ class Project(BaseStreamForm, AccessFormData, models.Model): blank=True, related_name="projects", ) - value = models.PositiveIntegerField(default=0) + value = models.DecimalField( + default=0, + max_digits=20, + decimal_places=2, + validators=[MinValueValidator(limit_value=0)], + ) proposed_start = models.DateTimeField(_("Proposed Start Date"), null=True) proposed_end = models.DateTimeField(_("Proposed End Date"), null=True) @@ -388,9 +393,7 @@ class Project(BaseStreamForm, AccessFormData, models.Model): return False def get_absolute_url(self): - if settings.PROJECTS_ENABLED: - return reverse("apply:projects:detail", args=[self.id]) - return "#" + return reverse("apply:projects:detail", args=[self.id]) @property def can_make_approval(self): diff --git a/hypha/apply/projects/services.py b/hypha/apply/projects/services.py new file mode 100644 index 0000000000000000000000000000000000000000..b1ae26e20159d7af306a92b9169232418283c300 --- /dev/null +++ b/hypha/apply/projects/services.py @@ -0,0 +1,154 @@ +from django.conf import settings +from django.contrib.auth.models import Group + +from hypha.apply.todo.options import ( + INVOICE_REQUIRED_CHANGES, + INVOICE_WAITING_APPROVAL, + INVOICE_WAITING_PAID, +) +from hypha.apply.todo.views import ( + add_task_to_user, + add_task_to_user_group, + remove_tasks_for_user_group, +) +from hypha.apply.users.groups import ( + APPROVER_GROUP_NAME, + FINANCE_GROUP_NAME, + STAFF_GROUP_NAME, +) + +from .models.payment import ( + APPROVED_BY_FINANCE, + APPROVED_BY_FINANCE_2, + APPROVED_BY_STAFF, + CHANGES_REQUESTED_BY_FINANCE, + CHANGES_REQUESTED_BY_FINANCE_2, + CHANGES_REQUESTED_BY_STAFF, + RESUBMITTED, + SUBMITTED, +) + + +def handle_tasks_on_invoice_update(old_status, invoice): + if old_status in [SUBMITTED, RESUBMITTED]: + # remove invoice waiting approval task for staff + remove_tasks_for_user_group( + code=INVOICE_WAITING_APPROVAL, + user_group=Group.objects.filter(name=STAFF_GROUP_NAME), + related_obj=invoice, + ) + if invoice.status == CHANGES_REQUESTED_BY_STAFF: + # add invoice required changes task for applicant + add_task_to_user( + code=INVOICE_REQUIRED_CHANGES, + user=invoice.project.user, + related_obj=invoice, + ) + elif invoice.status == APPROVED_BY_STAFF: + # add invoice waiting approval task for finance group + add_task_to_user_group( + code=INVOICE_WAITING_APPROVAL, + user_group=Group.objects.filter(name=FINANCE_GROUP_NAME), + related_obj=invoice, + ) + if old_status == APPROVED_BY_STAFF: + # remove invoice waiting approval task for finance group + remove_tasks_for_user_group( + code=INVOICE_WAITING_APPROVAL, + user_group=Group.objects.filter(name=FINANCE_GROUP_NAME), + related_obj=invoice, + ) + if invoice.status == CHANGES_REQUESTED_BY_FINANCE: + # add invoice required changes task for staff + add_task_to_user_group( + code=INVOICE_REQUIRED_CHANGES, + user_group=Group.objects.filter(name=STAFF_GROUP_NAME), + related_obj=invoice, + ) + elif invoice.status == APPROVED_BY_FINANCE: + if settings.INVOICE_EXTENDED_WORKFLOW: + # add invoice waiting approval task for finance2 group + add_task_to_user_group( + code=INVOICE_WAITING_APPROVAL, + user_group=Group.objects.filter(name=FINANCE_GROUP_NAME).filter( + name=APPROVER_GROUP_NAME + ), + related_obj=invoice, + ) + else: + # add invoice waiting paid task for finance + add_task_to_user_group( + code=INVOICE_WAITING_PAID, + user_group=Group.objects.filter(name=FINANCE_GROUP_NAME), + related_obj=invoice, + ) + if old_status == CHANGES_REQUESTED_BY_FINANCE: + # remove invoice required changes task for staff + remove_tasks_for_user_group( + code=INVOICE_REQUIRED_CHANGES, + user_group=Group.objects.filter(name=STAFF_GROUP_NAME), + related_obj=invoice, + ) + if invoice.status == CHANGES_REQUESTED_BY_STAFF: + # add invoice required changes task for applicant + add_task_to_user( + code=INVOICE_REQUIRED_CHANGES, + user=invoice.project.user, + related_obj=invoice, + ) + if not settings.INVOICE_EXTENDED_WORKFLOW and old_status == APPROVED_BY_FINANCE: + # remove invoice waiting paid task for finance group + remove_tasks_for_user_group( + code=INVOICE_WAITING_PAID, + user_group=Group.objects.filter(name=FINANCE_GROUP_NAME), + related_obj=invoice, + ) + if settings.INVOICE_EXTENDED_WORKFLOW: + if old_status == APPROVED_BY_FINANCE: + # remove invoice waiting approval task for finance2 group + remove_tasks_for_user_group( + code=INVOICE_WAITING_APPROVAL, + user_group=Group.objects.filter(name=FINANCE_GROUP_NAME).filter( + name=APPROVER_GROUP_NAME + ), + related_obj=invoice, + ) + if invoice.status == CHANGES_REQUESTED_BY_FINANCE_2: + # add invoice required changes task for finance + add_task_to_user_group( + code=INVOICE_REQUIRED_CHANGES, + user_group=Group.objects.filter(name=FINANCE_GROUP_NAME), + related_obj=invoice, + ) + elif invoice.status == APPROVED_BY_FINANCE_2: + # add invoice waiting paid task for finance2 + add_task_to_user_group( + code=INVOICE_WAITING_PAID, + user_group=Group.objects.filter(name=FINANCE_GROUP_NAME).filter( + name=APPROVER_GROUP_NAME + ), + related_obj=invoice, + ) + if old_status == CHANGES_REQUESTED_BY_FINANCE_2: + # remove invoice required changes task for finance + remove_tasks_for_user_group( + code=INVOICE_REQUIRED_CHANGES, + user_group=Group.objects.filter(name=FINANCE_GROUP_NAME), + related_obj=invoice, + ) + if invoice.status == CHANGES_REQUESTED_BY_FINANCE: + # add invoice required changes task for staff + add_task_to_user_group( + code=INVOICE_REQUIRED_CHANGES, + user_group=Group.objects.filter(name=STAFF_GROUP_NAME), + related_obj=invoice, + ) + if old_status == APPROVED_BY_FINANCE_2: + # remove invoice waiting paid task for finance2 + remove_tasks_for_user_group( + code=INVOICE_WAITING_PAID, + user_group=Group.objects.filter(name=FINANCE_GROUP_NAME).filter( + name=APPROVER_GROUP_NAME + ), + related_obj=invoice, + ) diff --git a/hypha/apply/projects/tables.py b/hypha/apply/projects/tables.py index 117d6db83bd59105cf473888bb3029e413ea1be9..2838b6792aa920b20b2ec80827ac34626403052b 100644 --- a/hypha/apply/projects/tables.py +++ b/hypha/apply/projects/tables.py @@ -4,7 +4,7 @@ import django_tables2 as tables from django.utils.safestring import mark_safe from django.utils.translation import gettext_lazy as _ -from .models import Invoice, Project, Report +from .models import Invoice, PAFApprovals, Project, Report class BaseInvoiceTable(tables.Table): @@ -32,6 +32,7 @@ class InvoiceDashboardTable(BaseInvoiceTable): ] model = Invoice order_by = ["-requested_at"] + template_name = "application_projects/tables/table.html" attrs = {"class": "invoices-table"} @@ -51,6 +52,7 @@ class InvoiceListTable(BaseInvoiceTable): model = Invoice orderable = True order_by = ["-requested_at"] + template_name = "application_projects/tables/table.html" attrs = {"class": "invoices-table"} @@ -104,6 +106,7 @@ class ProjectsDashboardTable(BaseProjectsTable): "end_date", ] model = Project + template_name = "application_projects/tables/table.html" orderable = False attrs = {"class": "projects-table"} @@ -124,6 +127,50 @@ class ProjectsAssigneeDashboardTable(BaseProjectsTable): attrs = {"class": "projects-table"} +class PAFForReviewDashboardTable(tables.Table): + date_requested = tables.DateColumn( + verbose_name=_("Date requested"), + accessor="created_at", + orderable=True, + ) + title = tables.LinkColumn( + "funds:projects:detail", + text=lambda r: textwrap.shorten(r.project.title, width=30, placeholder="..."), + accessor="project__title", + args=[tables.utils.A("project__pk")], + orderable=False, + ) + status = tables.Column(verbose_name=_("Status"), accessor="pk", orderable=False) + fund = tables.Column( + verbose_name=_("Fund"), accessor="project__submission__page", orderable=False + ) + + assignee = tables.Column( + verbose_name=_("Assignee"), accessor="user", orderable=False + ) + + class Meta: + fields = ["date_requested", "title", "fund", "status", "assignee"] + model = PAFApprovals + template_name = ( + "funds/tables/table.html" # todo: update it with Project table template + ) + attrs = {"class": "paf-review-table"} + + def order_date_requested(self, qs, is_descending): + direction = "-" if is_descending else "" + + qs = qs.order_by(f"{direction}created_at") + + return qs, True + + def render_status(self, record): + if record.user: + return _("Waiting for approval") + else: + return _("Waiting for assignee") + + class ProjectsListTable(BaseProjectsTable): class Meta: fields = [ @@ -162,7 +209,8 @@ class ReportListTable(tables.Table): ] sequence = ["project", "report_period", "..."] model = Report - attrs = {"class": "responsive-table"} + template_name = "application_projects/tables/table.html" + attrs = {"class": "projects-table"} def render_report_period(self, record): return f"{record.start} to {record.end_date}" diff --git a/hypha/apply/projects/templates/application_projects/includes/invoices.html b/hypha/apply/projects/templates/application_projects/includes/invoices.html index fd2747916a190213bdce205651a58efe6efcab09..4e825a2912ac71bd4d070f04c182e0c09011b3c3 100644 --- a/hypha/apply/projects/templates/application_projects/includes/invoices.html +++ b/hypha/apply/projects/templates/application_projects/includes/invoices.html @@ -1,4 +1,4 @@ -{% load i18n invoice_tools humanize %} +{% load i18n invoice_tools humanize heroicons %} <div id="invoices" class="data-block"> <div class="data-block__header"> @@ -12,71 +12,71 @@ {% endif %} </div> <div class="data-block__body"> - <table class="data-block__table"> - <thead> - <tr> - <th class="data-block__table-date">{% trans "Date submitted" %}</th> - <th class="data-block__table-amount">{% trans "Invoice number" %}</th> - <th class="data-block__table-status">{% trans "Status" %}</th> - <th class="data-block__table-update"></th> - </tr> - </thead> - <tbody> - {% for invoice in object.invoices.not_rejected %} - {% display_invoice_status_for_user user invoice as invoice_status %} + {% if object.invoices.not_rejected %} + <table class="data-block__table"> + <thead> <tr> - <td class="py-4 px-2.5"><span class="data-block__mobile-label">{% trans "Date submitted" %}: </span>{{ invoice.requested_at.date }}</td> - <td class="py-4 px-2.5"><span class="data-block__mobile-label">{% trans "Invoice number" %}: </span>{{ invoice.invoice_number }}</td> - <td class="py-4 px-2.5"><span class="data-block__mobile-label">{% trans "Status" %}: </span>{{ invoice_status }}</td> - <td class="flex flex-wrap justify-end py-4 px-0"> - <a class="data-block__action-icon-link" href="{{ invoice.get_absolute_url }}" target="_blank"> - <svg class="icon icon--project-eye"><use xlink:href="#eye"></use></svg> - {% trans "View" %} - </a> - {% can_edit invoice user as user_can_edit_request %} - {% if user_can_edit_request %} - <a class="data-block__action-icon-link" target="_blank" href="{% url "apply:projects:invoice-edit" pk=invoice.project.pk invoice_pk=invoice.pk %}"> - <svg class="icon icon--project-pen"><use xlink:href="#pen"></use></svg> - {% trans "Edit" %} + <th class="data-block__table-date">{% trans "Date submitted" %}</th> + <th class="data-block__table-amount">{% trans "Invoice No." %}</th> + <th class="data-block__table-status">{% trans "Status" %}</th> + <th class="data-block__table-update"></th> + </tr> + </thead> + <tbody> + {% for invoice in object.invoices.not_rejected %} + {% display_invoice_status_for_user user invoice as invoice_status %} + <tr> + <td class="py-4 px-2.5"><span class="data-block__mobile-label">{% trans "Date submitted" %}: </span>{{ invoice.requested_at.date }}</td> + <td class="py-4 px-2.5"><span class="data-block__mobile-label">{% trans "Invoice number" %}: </span>{{ invoice.invoice_number }}</td> + <td class="py-4 px-2.5"><span class="data-block__mobile-label">{% trans "Status" %}: </span>{{ invoice_status }}</td> + <td class="flex flex-wrap justify-center py-4 px-0 gap-2 xl:flex-nowrap"> + <a class="data-block__action-icon-link " href="{{ invoice.get_absolute_url }}" > + {% heroicon_mini "eye" size=16 aria_hidden=true class="mr-1" %} + {% trans "View" %} </a> - {% endif %} + {% can_edit invoice user as user_can_edit_request %} + {% if user_can_edit_request %} + <a class="data-block__action-icon-link" href="{% url "apply:projects:invoice-edit" pk=invoice.project.pk invoice_pk=invoice.pk %}"> + {% heroicon_mini "pencil-square" size=16 aria_hidden=true class="mr-1" %} + {% trans "Edit" %} + </a> + {% endif %} - {% can_delete invoice user as user_can_delete_request %} - {% if user.is_applicant and user_can_delete_request %} - <a class="data-block__action-icon-link data-block__action-icon-link--remove" target="_blank" href="{% url 'apply:projects:invoice-delete' pk=invoice.project.pk invoice_pk=invoice.pk %}"> - <svg class="icon icon--delete" style="margin-left:0; margin-right:2px"><use xlink:href="#delete"></use></svg> - {% trans "Delete" %} - </a> - {% endif %} - {% can_change_status invoice user as can_change_invoice_status %} - {% if can_change_invoice_status %} - <a - data-fancybox - data-src="#change-invoice-status-{{ invoice.id }}" - id="update_invoice_status-{{ invoice.id }}" - class="data-block__button button button--primary" - href="#" - > - {% trans "Update Status" %} - </a> - <div class="modal" id="change-invoice-status-{{ invoice.id }}"> - {% get_invoice_form invoice user as invoice_form %} - {% get_invoice_form_id invoice_form invoice as invoice_form_id %} - <h4 class="modal__project-header-bar">{% trans "Update Invoice status" %}</h4> - <p>{% trans "Current status" %}: {{ invoice_status }}</p> - {% trans "Update Status" as update %} - {% include 'funds/includes/delegated_form_base.html' with form=invoice_form value=update action=invoice.get_absolute_url form_id=invoice_form_id %} - </div> - {% endif %} - </td> - </tr> - {% empty %} - <tr> - <td colspan="5">{% trans "No active Invoices." %}</td> - </tr> - {% endfor %} - </tbody> - </table> + {% can_delete invoice user as user_can_delete_request %} + {% if user.is_applicant and user_can_delete_request %} + <a class="data-block__action-icon-link text-red-500" href="{% url 'apply:projects:invoice-delete' pk=invoice.project.pk invoice_pk=invoice.pk %}"> + {% heroicon_mini "trash" size=16 aria_hidden=true class="mr-1" %} + {% trans "Delete" %} + </a> + {% endif %} + {% can_change_status invoice user as can_change_invoice_status %} + {% if can_change_invoice_status %} + <a + data-fancybox + data-src="#change-invoice-status-{{ invoice.id }}" + id="update_invoice_status-{{ invoice.id }}" + class="data-block__button button button--primary" + href="#" + > + {% trans "Update Status" %} + </a> + <div class="modal" id="change-invoice-status-{{ invoice.id }}"> + {% get_invoice_form invoice user as invoice_form %} + {% get_invoice_form_id invoice_form invoice as invoice_form_id %} + <h4 class="modal__project-header-bar">{% trans "Update Invoice status" %}</h4> + <p>{% trans "Current status" %}: {{ invoice_status }}</p> + {% trans "Update Status" as update %} + {% include 'funds/includes/delegated_form_base.html' with form=invoice_form value=update action=invoice.get_absolute_url form_id=invoice_form_id %} + </div> + {% endif %} + </td> + </tr> + {% endfor %} + </tbody> + </table> + {% else %} + <p>{% trans "No active invoices yet." %}</p> + {% endif %} {% if object.invoices.rejected %} <p class="data-block__rejected"> @@ -100,8 +100,8 @@ <td class="py-4 px-2.5"><span class="data-block__mobile-label">{% trans "Invoice number" %}: </span>{{ invoice.invoice_number }}</td> <td class="py-4 px-2.5"><span class="data-block__mobile-label">{% trans "Status" %}: </span>{{ invoice_status }}</td> <td class="flex justify-end py-4 px-0"> - <a class="data-block__action-icon-link" href="{{ invoice.get_absolute_url }}" target="_blank"> - <svg class="icon icon--project-eye"><use xlink:href="#eye"></use></svg> + <a class="data-block__action-icon-link" href="{{ invoice.get_absolute_url }}" > + {% heroicon_mini "eye" size=16 aria_hidden=true class="mr-1" %} {% trans "View" %} </a> </td> diff --git a/hypha/apply/projects/templates/application_projects/invoice_detail.html b/hypha/apply/projects/templates/application_projects/invoice_detail.html index 4739e8100e53cf40f0958d0f8c2e43008d06d1b5..f7b50961a6fbc71fc8a895f15e378c0ce460cd69 100644 --- a/hypha/apply/projects/templates/application_projects/invoice_detail.html +++ b/hypha/apply/projects/templates/application_projects/invoice_detail.html @@ -1,17 +1,18 @@ {% extends "base-apply.html" %} {% load i18n humanize invoice_tools %} -{% block title %}{% trans "Invoice" %}: {{ object.project.title }}{% endblock %} +{% block title %}{% trans "Invoice" %}: {{ object.invoice_number }} - {{ object.project.title }}{% endblock %} +{% block body_class %}bg-light-grey{% endblock %} {% block content %} {% display_invoice_status_for_user user object as invoice_status %} {% adminbar %} {% slot back_link %} - <a class="simplified__projects-link" href="{{ object.project.get_absolute_url }}"> + <a class="simplified__projects-link" href="{% url 'apply:projects:detail' object.project.id %}"> {% trans "View project page" %} </a> {% endslot %} - {% slot header %}{% trans "Invoice" %}{% endslot %} + {% slot header %}{% trans "Invoice" %}: {{ object.invoice_number }}{% endslot %} {% slot sub_heading %}{% trans "For" %}: {{ object.project.title }}{% endslot %} {% endadminbar %} @@ -66,12 +67,14 @@ <p class="card__text"><a target="_blank" href="{% url "apply:projects:invoice-document" pk=object.project.pk invoice_pk=object.pk %}">{{object.filename}}</a></p> <embed src="{% url "apply:projects:invoice-document" pk=object.project.pk invoice_pk=object.pk %}" width="800px" height="800px" /> </div> - <div class="card__inner"> - <h5 class="card__heading">{% trans "Supporting Documents" %}</h5> - {% for document in object.supporting_documents.all %} - <p class="card__text"><a href="{% url "apply:projects:invoice-supporting-document" pk=object.project.pk invoice_pk=object.pk file_pk=document.pk %}">{{document.filename}}</a></p> - {% endfor %} - </div> + {% if object.supporting_documents.exists %} + <div class="card__inner"> + <h5 class="card__heading">{% trans "Supporting Documents" %}</h5> + {% for document in object.supporting_documents.all %} + <p class="card__text"><a href="{% url "apply:projects:invoice-supporting-document" pk=object.project.pk invoice_pk=object.pk file_pk=document.pk %}">{{document.filename}}</a></p> + {% endfor %} + </div> + {% endif %} </div> </div> <aside class="sidebar"> diff --git a/hypha/apply/projects/templates/application_projects/project_approval_detail.html b/hypha/apply/projects/templates/application_projects/project_approval_detail.html index f2f5eb89c4eabcd7d88b02b16ecc2e7f801231b7..48f32d0c20688e2a90d44676306fd8ca386f753e 100644 --- a/hypha/apply/projects/templates/application_projects/project_approval_detail.html +++ b/hypha/apply/projects/templates/application_projects/project_approval_detail.html @@ -2,6 +2,7 @@ {% load i18n static approval_tools project_tags apply_tags %} {% block title %}{{ object.title }}{% endblock %} +{% block body_class %}bg-light-grey{% endblock %} {% block extra_css %} <link rel="stylesheet" href="{% static 'css/apply/fancybox.css' %}"> @@ -167,6 +168,7 @@ </div> {% endblock content %} + {% block extra_js %} {{ block.super }} <script src="{% static 'js/apply/jquery.fancybox.min.js' %}"></script> diff --git a/hypha/apply/projects/templates/application_projects/project_detail.html b/hypha/apply/projects/templates/application_projects/project_detail.html index 6dcb136c02266be7c1c2b43eb0471121d4d6ab8a..549cae841d9ec2654401440d500eb9341571ea59 100644 --- a/hypha/apply/projects/templates/application_projects/project_detail.html +++ b/hypha/apply/projects/templates/application_projects/project_detail.html @@ -219,6 +219,7 @@ {# Tab 2 #} <div class="tabs__content" id="tab-2"> <div class="feed"> + <h4 class="m-0">{% trans "Add communication" %}</h4> {% include "activity/include/comment_form.html" %} {% include "activity/include/comment_list.html" with editable=False %} </div> @@ -245,6 +246,5 @@ <script src="{% static 'js/apply/toggle-payment-block.js' %}"></script> <script src="{% static 'js/apply/past-reports-pagination.js' %}"></script> <script src="{% static 'js/apply/report-calculator.js' %}"></script> - <script src="{% static 'js/apply/report-frequency.js' %}"></script> <script src="{% static 'js/apply/jquery.fancybox.min.js' %}"></script> {% endblock %} diff --git a/hypha/apply/projects/templates/application_projects/tables/table.html b/hypha/apply/projects/templates/application_projects/tables/table.html index 8c02c3bceff9ea2f76fa691411f208983a35dab4..457600b3ffb00d9a8ee4667fdbf09fb35ac8bddb 100644 --- a/hypha/apply/projects/templates/application_projects/tables/table.html +++ b/hypha/apply/projects/templates/application_projects/tables/table.html @@ -1,6 +1,19 @@ {% extends 'django_tables2/table.html' %} {% load django_tables2 table_tags review_tags wagtailimages_tags i18n %} +{% block table.tbody.row %} + <tr {{ row.attrs.as_html }}> + {% for column, cell in row.items %} + <td {{ column.attrs.td.as_html }}> + {% if column.name != "selected" %} + <span class="mobile-label {{ column.attrs.td.class }}">{{ column.header }}: </span> + {% endif %} + {% if column.localize == None %}{{ cell }}{% else %}{% if column.localize %}{{ cell|localize }}{% else %}{{ cell|unlocalize }}{% endif %}{% endif %} + </td> + {% endfor %} + </tr> +{% endblock %} + {% block pagination %} {% if table.page and table.paginator.num_pages > 0 %} <div class="pagination--wrapper"> diff --git a/hypha/apply/projects/templatetags/invoice_tools.py b/hypha/apply/projects/templatetags/invoice_tools.py index a808740e74b350fe690ce82fc139e7aeda5c08e1..5f7c74a2a7f28533e9ce11d2d6e3cb21f0ce98ff 100644 --- a/hypha/apply/projects/templatetags/invoice_tools.py +++ b/hypha/apply/projects/templatetags/invoice_tools.py @@ -7,13 +7,20 @@ from django.utils.translation import gettext_lazy as _ from hypha.apply.activity.models import Activity from hypha.apply.activity.templatetags.activity_tags import display_for +from hypha.apply.projects.constants import ( + INVOICE_STATUS_BG_COLORS, + INVOICE_STATUS_FG_COLORS, +) from hypha.apply.projects.models.project import ( CLOSING, COMPLETE, INVOICING_AND_REPORTING, ProjectSettings, ) -from hypha.apply.projects.utils import get_invoice_public_status +from hypha.apply.projects.utils import ( + get_invoice_public_status, + get_invoice_table_status, +) register = template.Library() @@ -135,3 +142,18 @@ def get_comment_for_invoice_action(invoice, action): related_content_type__model="invoice", related_object_id=invoice.id, ).first() + + +@register.filter +def invoice_status_bg_color(invoice_status): + return INVOICE_STATUS_BG_COLORS.get(invoice_status, "bg-gray-100") + + +@register.filter +def invoice_status_fg_color(invoice_status): + return INVOICE_STATUS_FG_COLORS.get(invoice_status, "text-gray-700") + + +@register.simple_tag +def display_invoice_table_status_for_user(status, user): + return get_invoice_table_status(status, user) diff --git a/hypha/apply/projects/templatetags/project_tags.py b/hypha/apply/projects/templatetags/project_tags.py index 5432966ef40093f975ff4610e302678fb5261e42..5b1e70ed9749857f4aa9544cb43c61e677d9b386 100644 --- a/hypha/apply/projects/templatetags/project_tags.py +++ b/hypha/apply/projects/templatetags/project_tags.py @@ -146,6 +146,11 @@ def user_next_step_on_project(project, user, request=None): org_short_name=settings.ORG_SHORT_NAME ), } + if settings.STAFF_UPLOAD_CONTRACT: + return { + "heading": _("Waiting for"), + "text": _("Awaiting signed contract from Staff/Contracting team"), + } return { "heading": _("Waiting for"), "text": _("Awaiting signed contract from Contracting team"), diff --git a/hypha/apply/projects/tests/test_settings.py b/hypha/apply/projects/tests/test_settings.py index b006647fa687c296692cf97ab098e946ee3a7efe..58b7b1175d946370f4bf953ae986863bee0b4036 100644 --- a/hypha/apply/projects/tests/test_settings.py +++ b/hypha/apply/projects/tests/test_settings.py @@ -1,15 +1,18 @@ -from django.test import TestCase, override_settings +# Fix me, for details on why this is commented out, see +# https://github.com/HyphaApp/hypha/issues/3606 -from hypha.apply.users.tests.factories import StaffFactory +# from django.test import TestCase, override_settings +# from hypha.apply.users.tests.factories import StaffFactory -class TestProjectFeatureFlag(TestCase): - @override_settings(PROJECTS_ENABLED=False) - def test_urls_404_when_turned_off(self): - self.client.force_login(StaffFactory()) - response = self.client.get("/apply/projects/", follow=True) - self.assertEqual(response.status_code, 404) +# class TestProjectFeatureFlag(TestCase): +# @override_settings(PROJECTS_ENABLED=False) +# def test_urls_404_when_turned_off(self): +# self.client.force_login(StaffFactory()) - response = self.client.get("/apply/projects/1/", follow=True) - self.assertEqual(response.status_code, 404) +# response = self.client.get("/apply/projects/", follow=True) +# self.assertEqual(response.status_code, 404) + +# response = self.client.get("/apply/projects/1/", follow=True) +# self.assertEqual(response.status_code, 404) diff --git a/hypha/apply/projects/tests/test_views.py b/hypha/apply/projects/tests/test_views.py index 688f9d2119feefaac2ef88bafd5f54c3dd2c57b7..480e767079a55fb4d219abf13f265a25a7240b99 100644 --- a/hypha/apply/projects/tests/test_views.py +++ b/hypha/apply/projects/tests/test_views.py @@ -2,6 +2,7 @@ import json from io import BytesIO from dateutil.relativedelta import relativedelta +from django.conf import settings from django.contrib.auth.models import AnonymousUser from django.core.exceptions import PermissionDenied from django.test import RequestFactory, TestCase, override_settings @@ -771,7 +772,7 @@ class TestAnonPacketView(BasePacketFileViewTestCase): self.assertEqual(response.status_code, 200) self.assertEqual(len(response.redirect_chain), 2) for path, _ in response.redirect_chain: - self.assertIn(reverse("users_public:login"), path) + self.assertIn(reverse(settings.LOGIN_URL), path) class TestProjectDetailApprovalView(TestCase): diff --git a/hypha/apply/projects/utils.py b/hypha/apply/projects/utils.py index 52718af26c9e78a8fe4f622157ce2a31450e9d97..7325c1830b061ab3d906895360a5aefc1c324a9e 100644 --- a/hypha/apply/projects/utils.py +++ b/hypha/apply/projects/utils.py @@ -1,6 +1,16 @@ from django.conf import settings from django.utils.translation import gettext_lazy as _ +from .constants import ( + INT_DECLINED, + INT_FINANCE_PENDING, + INT_ORG_PENDING, + INT_PAID, + INT_PAYMENT_FAILED, + INT_REQUEST_FOR_CHANGE, + INT_STAFF_PENDING, + INT_VENDOR_PENDING, +) from .models import Deliverable, Project from .models.payment import ( APPROVED_BY_FINANCE, @@ -147,3 +157,31 @@ def get_project_status_display_value(project_status): def get_project_public_status(project_status): return dict(PROJECT_PUBLIC_STATUSES)[project_status] + + +def get_invoice_table_status(invoice_status, user): + if invoice_status in [SUBMITTED, RESUBMITTED]: + if user.is_applicant: + return INT_ORG_PENDING + return INT_STAFF_PENDING + if invoice_status == CHANGES_REQUESTED_BY_STAFF: + if user.is_applicant: + return INT_REQUEST_FOR_CHANGE + return INT_VENDOR_PENDING + if invoice_status in [APPROVED_BY_STAFF, CHANGES_REQUESTED_BY_FINANCE]: + if user.is_applicant: + return INT_ORG_PENDING + return INT_FINANCE_PENDING + if settings.INVOICE_EXTENDED_WORKFLOW and invoice_status in [ + APPROVED_BY_FINANCE, + CHANGES_REQUESTED_BY_FINANCE_2, + ]: + if user.is_applicant: + return INT_ORG_PENDING + return INT_FINANCE_PENDING + if invoice_status == PAID: + return INT_PAID + if invoice_status == DECLINED: + return INT_DECLINED + if invoice_status == PAYMENT_FAILED: + return INT_PAYMENT_FAILED diff --git a/hypha/apply/projects/views/payment.py b/hypha/apply/projects/views/payment.py index f767741dc3c1494103b0ef24e3c1fe88101e7581..1daae109f4da68456c3c44134991440ec6eb493d 100644 --- a/hypha/apply/projects/views/payment.py +++ b/hypha/apply/projects/views/payment.py @@ -2,6 +2,7 @@ from django.conf import settings from django.contrib import messages from django.contrib.auth.decorators import login_required from django.contrib.auth.mixins import UserPassesTestMixin +from django.contrib.auth.models import Group from django.core.exceptions import PermissionDenied from django.db import transaction from django.shortcuts import get_object_or_404, redirect @@ -14,7 +15,19 @@ from django_tables2 import SingleTableMixin from hypha.apply.activity.messaging import MESSAGES, messenger from hypha.apply.activity.models import APPLICANT, COMMENT, Activity +from hypha.apply.todo.options import ( + INVOICE_REQUIRED_CHANGES, + INVOICE_WAITING_APPROVAL, + PROJECT_WAITING_INVOICE, +) +from hypha.apply.todo.views import ( + add_task_to_user_group, + remove_tasks_for_user, + remove_tasks_for_user_group, + remove_tasks_of_related_obj, +) from hypha.apply.users.decorators import staff_or_finance_required +from hypha.apply.users.groups import STAFF_GROUP_NAME from hypha.apply.utils.storage import PrivateMediaView from hypha.apply.utils.views import DelegateableView, DelegatedViewMixin, ViewDispatcher @@ -23,10 +36,13 @@ from ..forms import ChangeInvoiceStatusForm, CreateInvoiceForm, EditInvoiceForm from ..models.payment import ( APPROVED_BY_FINANCE, APPROVED_BY_STAFF, + CHANGES_REQUESTED_BY_FINANCE, + CHANGES_REQUESTED_BY_STAFF, INVOICE_TRANISTION_TO_RESUBMITTED, Invoice, ) from ..models.project import PROJECT_ACTION_MESSAGE_TAG, Project +from ..services import handle_tasks_on_invoice_update from ..tables import InvoiceListTable @@ -58,6 +74,10 @@ class ChangeInvoiceStatusView(DelegatedViewMixin, InvoiceAccessMixin, UpdateView model = Invoice def form_valid(self, form): + invoice = get_object_or_404( + Invoice, pk=self.kwargs["invoice_pk"] + ) # to get the old status + old_status = invoice.status response = super().form_valid(form) if form.cleaned_data["comment"]: invoice_status_change = _( @@ -100,6 +120,8 @@ class ChangeInvoiceStatusView(DelegatedViewMixin, InvoiceAccessMixin, UpdateView related=self.object, ) + handle_tasks_on_invoice_update(old_status=old_status, invoice=self.object) + return response @@ -119,6 +141,9 @@ class DeleteInvoiceView(DeleteView): @transaction.atomic() def form_valid(self, form): + # remove all tasks related to this invoice irrespective of code and users/user_group + remove_tasks_of_related_obj(related_obj=self.object) + response = super().form_valid(form) messenger( @@ -223,6 +248,22 @@ class CreateInvoiceView(CreateView): source=self.project, related=self.object, ) + + if len(self.project.invoices.all()) == 1: + # remove Project waiting invoices task for applicant on first invoice + remove_tasks_for_user( + code=PROJECT_WAITING_INVOICE, + user=self.project.user, + related_obj=self.project, + ) + + # add Invoice waiting approval task for Staff group + add_task_to_user_group( + code=INVOICE_WAITING_APPROVAL, + user_group=Group.objects.filter(name=STAFF_GROUP_NAME), + related_obj=self.object, + ) + messages.success( self.request, _("Invoice added"), extra_tags=PROJECT_ACTION_MESSAGE_TAG ) @@ -274,7 +315,9 @@ class EditInvoiceView(InvoiceAccessMixin, UpdateView): return self.form_invalid(form) def form_valid(self, form): + old_status = self.object.status response = super().form_valid(form) + if form.cleaned_data: if self.object.status in INVOICE_TRANISTION_TO_RESUBMITTED: self.object.transition_invoice_to_resubmitted() @@ -305,6 +348,38 @@ class EditInvoiceView(InvoiceAccessMixin, UpdateView): related=self.object, ) + if self.request.user.is_applicant and old_status == CHANGES_REQUESTED_BY_STAFF: + # remove invoice required changes task for applicant + remove_tasks_for_user( + code=INVOICE_REQUIRED_CHANGES, + user=self.object.project.user, + related_obj=self.object, + ) + + # add invoice waiting approval task for staff group + add_task_to_user_group( + code=INVOICE_WAITING_APPROVAL, + user_group=Group.objects.filter(name=STAFF_GROUP_NAME), + related_obj=self.object, + ) + + if ( + self.request.user.is_apply_staff + and old_status == CHANGES_REQUESTED_BY_FINANCE + ): + # remove invoice required changes task for staff group + remove_tasks_for_user_group( + code=INVOICE_REQUIRED_CHANGES, + user_group=Group.objects.filter(name=STAFF_GROUP_NAME), + related_obj=self.object, + ) + # add invoice waiting approval task for staff group + add_task_to_user_group( + code=INVOICE_WAITING_APPROVAL, + user_group=Group.objects.filter(name=STAFF_GROUP_NAME), + related_obj=self.object, + ) + # Required for django-file-form: delete temporary files for the new files # that are uploaded. form.delete_temporary_files() diff --git a/hypha/apply/projects/views/project.py b/hypha/apply/projects/views/project.py index eb200582d09bcb29c1605753eb783b1e3c1bfb5e..c8c03844ed1751985b669b34d53f4344a19e8875 100644 --- a/hypha/apply/projects/views/project.py +++ b/hypha/apply/projects/views/project.py @@ -6,6 +6,7 @@ from django.conf import settings from django.contrib import messages from django.contrib.auth.decorators import login_required from django.contrib.auth.mixins import UserPassesTestMixin +from django.contrib.auth.models import Group from django.core.exceptions import ObjectDoesNotExist, PermissionDenied from django.db import transaction from django.db.models import Count, Q @@ -40,10 +41,28 @@ from hypha.apply.activity.messaging import MESSAGES, messenger from hypha.apply.activity.models import ACTION, ALL, COMMENT, TEAM, Activity from hypha.apply.activity.views import ActivityContextMixin, CommentFormView from hypha.apply.stream_forms.models import BaseStreamForm +from hypha.apply.todo.options import ( + PAF_REQUIRED_CHANGES, + PAF_WAITING_APPROVAL, + PAF_WAITING_ASSIGNEE, + PROJECT_SUBMIT_PAF, + PROJECT_WAITING_CONTRACT, + PROJECT_WAITING_CONTRACT_DOCUMENT, + PROJECT_WAITING_CONTRACT_REVIEW, + PROJECT_WAITING_INVOICE, + PROJECT_WAITING_PAF, +) +from hypha.apply.todo.views import ( + add_task_to_user, + add_task_to_user_group, + remove_tasks_for_user, + remove_tasks_for_user_group, +) from hypha.apply.users.decorators import ( staff_or_finance_or_contracting_required, staff_required, ) +from hypha.apply.users.groups import CONTRACTING_GROUP_NAME, STAFF_GROUP_NAME from hypha.apply.utils.models import PDFPageSettings from hypha.apply.utils.storage import PrivateMediaView from hypha.apply.utils.views import DelegateableView, DelegatedViewMixin, ViewDispatcher @@ -108,6 +127,20 @@ class SendForApprovalView(DelegatedViewMixin, UpdateView): response = super().form_valid(form) + # remove PAF submission task for staff group + remove_tasks_for_user_group( + code=PROJECT_SUBMIT_PAF, + user_group=Group.objects.filter(name=STAFF_GROUP_NAME), + related_obj=self.object, + ) + + # remove PAF rejection task for staff if exists + remove_tasks_for_user_group( + code=PAF_REQUIRED_CHANGES, + user_group=Group.objects.filter(name=STAFF_GROUP_NAME), + related_obj=self.object, + ) + project_settings = ProjectSettings.for_request(self.request) paf_approvals = self.object.paf_approvals.filter(approved=False) @@ -130,6 +163,10 @@ class SendForApprovalView(DelegatedViewMixin, UpdateView): source=self.object, related=[paf_approvals.first()], ) + # add PAF waiting approval task for paf_approval user + add_task_to_user( + code=PAF_WAITING_APPROVAL, user=user, related_obj=self.object + ) else: messenger( MESSAGES.ASSIGN_PAF_APPROVER, @@ -137,6 +174,12 @@ class SendForApprovalView(DelegatedViewMixin, UpdateView): user=self.request.user, source=self.object, ) + # add PAF waiting assignee task for paf_approval reviewer_roles + add_task_to_user_group( + code=PAF_WAITING_ASSIGNEE, + user_group=paf_approvals.first().paf_reviewer_role.user_roles.all(), + related_obj=self.object, + ) else: if paf_approvals.filter(user__isnull=False).exists(): messenger( @@ -152,6 +195,13 @@ class SendForApprovalView(DelegatedViewMixin, UpdateView): source=self.object, related=paf_approvals.filter(user__isnull=False), ) + # add PAF waiting approval task for paf_approvals users + for paf_approval in paf_approvals.filter(user__isnull=False): + add_task_to_user( + code=PAF_WAITING_APPROVAL, + user=paf_approval.user, + related_obj=self.object, + ) if paf_approvals.filter(user__isnull=True).exists(): messenger( MESSAGES.ASSIGN_PAF_APPROVER, @@ -159,6 +209,13 @@ class SendForApprovalView(DelegatedViewMixin, UpdateView): user=self.request.user, source=self.object, ) + # add PAF waiting assignee task for paf_approvals reviewer_roles + for paf_approval in paf_approvals.filter(user__isnull=True): + add_task_to_user_group( + code=PAF_WAITING_ASSIGNEE, + user_group=paf_approval.paf_reviewer_role.user_roles.all(), + related_obj=self.object, + ) project.status = INTERNAL_APPROVAL project.save(update_fields=["status"]) @@ -407,6 +464,12 @@ class ApproveContractView(DelegatedViewMixin, UpdateView): source=self.project, related=self.object, ) + # remove Project waiting contract review task for staff + remove_tasks_for_user_group( + code=PROJECT_WAITING_CONTRACT_REVIEW, + user_group=Group.objects.filter(name=STAFF_GROUP_NAME), + related_obj=self.project, + ) self.project.status = INVOICING_AND_REPORTING self.project.save(update_fields=["status"]) @@ -418,6 +481,12 @@ class ApproveContractView(DelegatedViewMixin, UpdateView): source=self.project, related=old_stage, ) + # add Project waiting invoice task for applicant + add_task_to_user( + code=PROJECT_WAITING_INVOICE, + user=self.project.user, + related_obj=self.project, + ) messages.success( self.request, @@ -468,6 +537,7 @@ class UploadContractView(DelegatedViewMixin, CreateView): extra_tags=PROJECT_ACTION_MESSAGE_TAG, ) elif self.request.user.is_contracting: + # :todo: update same date when staff uploads the contract(with STAFF_UPLOAD_CONTRACT setting) form.instance.uploaded_by_contractor_at = timezone.now() messages.success( self.request, @@ -485,6 +555,25 @@ class UploadContractView(DelegatedViewMixin, CreateView): source=project, related=form.instance, ) + # remove Project waiting contract task for contracting/staff group + if settings.STAFF_UPLOAD_CONTRACT: + remove_tasks_for_user_group( + code=PROJECT_WAITING_CONTRACT, + user_group=Group.objects.filter(name=STAFF_GROUP_NAME), + related_obj=project, + ) + else: + remove_tasks_for_user_group( + code=PROJECT_WAITING_CONTRACT, + user_group=Group.objects.filter(name=CONTRACTING_GROUP_NAME), + related_obj=project, + ) + # add Project waiting contract document task for applicant + add_task_to_user( + code=PROJECT_WAITING_CONTRACT_DOCUMENT, + user=project.user, + related_obj=project, + ) return response @@ -524,6 +613,18 @@ class SubmitContractDocumentsView(DelegatedViewMixin, UpdateView): user=self.request.user, source=project, ) + # remove project waiting contract documents task for applicant + remove_tasks_for_user( + code=PROJECT_WAITING_CONTRACT_DOCUMENT, + user=project.user, + related_obj=project, + ) + # add project waiting contract review task for staff + add_task_to_user_group( + code=PROJECT_WAITING_CONTRACT_REVIEW, + user_group=Group.objects.filter(name=STAFF_GROUP_NAME), + related_obj=project, + ) messages.success( self.request, @@ -626,6 +727,35 @@ class ChangePAFStatusView(DelegatedViewMixin, UpdateView): self.object.save(update_fields=["status"]) paf_approval.save() + # remove PAF waiting assignee/approval task for paf approval user/reviewer roles. + if project_settings.paf_approval_sequential: + if paf_approval.user: + remove_tasks_for_user( + code=PAF_WAITING_APPROVAL, + user=paf_approval.user, + related_obj=self.object, + ) + else: + remove_tasks_for_user_group( + code=PAF_WAITING_ASSIGNEE, + user_group=paf_approval.paf_reviewer_role.user_roles.all(), + related_obj=self.object, + ) + else: + for approval in self.object.paf_approvals.filter(approved=False): + if approval.user: + remove_tasks_for_user( + code=PAF_WAITING_APPROVAL, + user=approval.user, + related_obj=self.object, + ) + else: + remove_tasks_for_user_group( + code=PAF_WAITING_ASSIGNEE, + user_group=approval.paf_reviewer_role.user_roles.all(), + related_obj=self.object, + ) + if not paf_approval.user: paf_approval.user = self.request.user paf_approval.save(update_fields=["user"]) @@ -644,12 +774,32 @@ class ChangePAFStatusView(DelegatedViewMixin, UpdateView): source=self.object, related=old_stage, ) + # add PAF required changes task to staff user group + add_task_to_user_group( + code=PAF_REQUIRED_CHANGES, + user_group=Group.objects.filter(name=STAFF_GROUP_NAME), + related_obj=self.object, + ) + messages.success( self.request, _("PAF status has been updated"), extra_tags=PROJECT_ACTION_MESSAGE_TAG, ) elif paf_status == APPROVE: + # remove task for paf approval user/user_group related to this paf_approval of project + if paf_approval.user: + remove_tasks_for_user( + code=PAF_WAITING_APPROVAL, + user=paf_approval.user, + related_obj=self.object, + ) + else: + remove_tasks_for_user_group( + code=PAF_WAITING_ASSIGNEE, + user_group=paf_approval.paf_reviewer_role.user_roles.all(), + related_obj=self.object, + ) paf_approval.approved = True paf_approval.approved_at = timezone.now() paf_approval.user = self.request.user @@ -668,6 +818,12 @@ class ChangePAFStatusView(DelegatedViewMixin, UpdateView): source=self.object, related=[next_paf_approval], ) + # add PAF waiting approval task for next paf approval user + add_task_to_user( + code=PAF_WAITING_APPROVAL, + user=next_paf_approval.user, + related_obj=self.object, + ) else: messenger( MESSAGES.ASSIGN_PAF_APPROVER, @@ -675,6 +831,12 @@ class ChangePAFStatusView(DelegatedViewMixin, UpdateView): user=self.request.user, source=self.object, ) + # add PAF waiting assignee task for nex paf approval reviewer roles + add_task_to_user_group( + code=PAF_WAITING_ASSIGNEE, + user_group=next_paf_approval.paf_reviewer_role.user_roles.all(), + related_obj=self.object, + ) messages.success( self.request, _("PAF has been approved"), @@ -708,6 +870,19 @@ class ChangePAFStatusView(DelegatedViewMixin, UpdateView): source=self.object, related=old_stage, ) + # add project waiting contract task to staff/contracting groups + if settings.STAFF_UPLOAD_CONTRACT: + add_task_to_user_group( + code=PROJECT_WAITING_CONTRACT, + user_group=Group.objects.filter(name=STAFF_GROUP_NAME), + related_obj=self.object, + ) + else: + add_task_to_user_group( + code=PROJECT_WAITING_CONTRACT, + user_group=Group.objects.filter(name=CONTRACTING_GROUP_NAME), + related_obj=self.object, + ) return response @@ -782,8 +957,26 @@ class UpdateAssignApproversView(DelegatedViewMixin, UpdateView): project = self.kwargs["object"] + old_paf_approval = get_latest_project_paf_approval_via_roles( + project=project, roles=self.request.user.groups.all() + ) + response = super().form_valid(form) + # remove current task of user/user_group related to latest paf_approval of project + if old_paf_approval.user: + remove_tasks_for_user( + code=PAF_WAITING_APPROVAL, + user=old_paf_approval.user, + related_obj=project, + ) + else: + remove_tasks_for_user_group( + code=PAF_WAITING_ASSIGNEE, + user_group=old_paf_approval.paf_reviewer_role.user_roles.all(), + related_obj=project, + ) + paf_approval = get_latest_project_paf_approval_via_roles( project=project, roles=self.request.user.groups.all() ) @@ -796,6 +989,12 @@ class UpdateAssignApproversView(DelegatedViewMixin, UpdateView): source=self.object, related=[paf_approval], ) + # add PAF waiting approval task to updated paf_approval user + add_task_to_user( + code=PAF_WAITING_APPROVAL, + user=paf_approval.user, + related_obj=self.object, + ) else: messenger( MESSAGES.ASSIGN_PAF_APPROVER, @@ -803,6 +1002,12 @@ class UpdateAssignApproversView(DelegatedViewMixin, UpdateView): user=self.request.user, source=self.object, ) + # add paf waiting for assignee task + add_task_to_user_group( + code=PAF_WAITING_ASSIGNEE, + user_group=paf_approval.paf_reviewer_role.user_roles.all(), + related_obj=self.object, + ) return response @@ -834,6 +1039,35 @@ class UpdatePAFApproversView(DelegatedViewMixin, UpdateView): "user__id", flat=True ) ) + # remove PAF waiting assignee/approval task for paf approval user/reviewer roles. + if project_settings.paf_approval_sequential: + paf_approval = project.paf_approvals.filter(approved=False).first() + if paf_approval.user: + remove_tasks_for_user( + code=PAF_WAITING_APPROVAL, + user=paf_approval.user, + related_obj=project, + ) + else: + remove_tasks_for_user_group( + code=PAF_WAITING_ASSIGNEE, + user_group=paf_approval.paf_reviewer_role.user_roles.all(), + related_obj=project, + ) + else: + for approval in project.paf_approvals.filter(approved=False): + if approval.user: + remove_tasks_for_user( + code=PAF_WAITING_APPROVAL, + user=approval.user, + related_obj=project, + ) + else: + remove_tasks_for_user_group( + code=PAF_WAITING_ASSIGNEE, + user_group=approval.paf_reviewer_role.user_roles.all(), + related_obj=project, + ) response = super().form_valid(form) @@ -852,6 +1086,10 @@ class UpdatePAFApproversView(DelegatedViewMixin, UpdateView): source=self.object, related=[paf_approvals.first()], ) + # add PAF waiting approval task to paf_approval user + add_task_to_user( + code=PAF_WAITING_APPROVAL, user=user, related_obj=self.object + ) elif not user: messenger( MESSAGES.ASSIGN_PAF_APPROVER, @@ -859,6 +1097,12 @@ class UpdatePAFApproversView(DelegatedViewMixin, UpdateView): user=self.request.user, source=self.object, ) + # add PAF waiting assignee to paf_approvals reviewer roles + add_task_to_user_group( + code=PAF_WAITING_ASSIGNEE, + user_group=paf_approvals.first().paf_reviewer_role.user_roles.all(), + related_obj=self.object, + ) else: if paf_approvals.filter(user__isnull=False).exists(): messenger( @@ -868,6 +1112,13 @@ class UpdatePAFApproversView(DelegatedViewMixin, UpdateView): source=self.object, related=paf_approvals.filter(user__isnull=False), ) + # add PAF waiting approval task for paf_approvals users + for paf_approval in paf_approvals.filter(user__isnull=False): + add_task_to_user( + code=PAF_WAITING_APPROVAL, + user=paf_approval.user, + related_obj=self.object, + ) if paf_approvals.filter(user__isnull=True).exists(): messenger( MESSAGES.ASSIGN_PAF_APPROVER, @@ -875,7 +1126,15 @@ class UpdatePAFApproversView(DelegatedViewMixin, UpdateView): user=self.request.user, source=self.object, ) + # add PAF waiting assignee task for paf_approvals reviewer_roles + for paf_approval in paf_approvals.filter(user__isnull=True): + add_task_to_user_group( + code=PAF_WAITING_ASSIGNEE, + user_group=paf_approval.paf_reviewer_role.user_roles.all(), + related_obj=self.object, + ) elif paf_approvals: + # :todo: check if this is covering any case(might be a duplicate of SendForApprovalView) if paf_approvals.filter(user__isnull=False).exists(): messenger( MESSAGES.APPROVE_PAF, @@ -884,6 +1143,13 @@ class UpdatePAFApproversView(DelegatedViewMixin, UpdateView): source=self.object, related=paf_approvals.filter(user__isnull=False), ) + # add PAF waiting approval task for paf_approvals users + for paf_approval in paf_approvals.filter(user__isnull=False): + add_task_to_user( + code=PAF_WAITING_APPROVAL, + user=paf_approval.user, + related_obj=self.object, + ) if paf_approvals.filter(user__isnull=True).exists(): messenger( MESSAGES.ASSIGN_PAF_APPROVER, @@ -891,6 +1157,13 @@ class UpdatePAFApproversView(DelegatedViewMixin, UpdateView): user=self.request.user, source=self.object, ) + # add PAF waiting assignee task for paf_approvals reviewer_roles + for paf_approval in paf_approvals.filter(user__isnull=True): + add_task_to_user_group( + code=PAF_WAITING_ASSIGNEE, + user_group=paf_approval.paf_reviewer_role.user_roles.all(), + related_obj=self.object, + ) messages.success( self.request, @@ -1542,6 +1815,18 @@ class ProjectApprovalFormEditView(BaseStreamForm, UpdateView): self.sow_form.save(sow_form_fields=sow_form_fields, project=self.object) self.paf_form.delete_temporary_files() self.sow_form.delete_temporary_files() + # remove PAF addition task for staff group + remove_tasks_for_user_group( + code=PROJECT_WAITING_PAF, + user_group=Group.objects.filter(name=STAFF_GROUP_NAME), + related_obj=self.object, + ) + # add PAF submission task for staff group + add_task_to_user_group( + code=PROJECT_SUBMIT_PAF, + user_group=Group.objects.filter(name=STAFF_GROUP_NAME), + related_obj=self.object, + ) return HttpResponseRedirect(self.get_success_url()) else: if not self.paf_form.is_valid(): @@ -1556,6 +1841,18 @@ class ProjectApprovalFormEditView(BaseStreamForm, UpdateView): paf_form_fields = [] self.paf_form.save(paf_form_fields=paf_form_fields) self.paf_form.delete_temporary_files() + # remove PAF addition task for staff group + remove_tasks_for_user_group( + code=PROJECT_WAITING_PAF, + user_group=Group.objects.filter(name=STAFF_GROUP_NAME), + related_obj=self.object, + ) + # add PAF submission task for staff group + add_task_to_user_group( + code=PROJECT_SUBMIT_PAF, + user_group=Group.objects.filter(name=STAFF_GROUP_NAME), + related_obj=self.object, + ) return HttpResponseRedirect(self.get_success_url()) else: return self.form_invalid(self.paf_form) diff --git a/hypha/apply/review/blocks.py b/hypha/apply/review/blocks.py index 988bdfc44b45e37bee2ee735eefa5f8b7dd01c0c..2bc25ca696e89b2d0639178dcdaea20fd4abb6ea 100644 --- a/hypha/apply/review/blocks.py +++ b/hypha/apply/review/blocks.py @@ -1,6 +1,7 @@ import json from django import forms +from django.conf import settings from django.utils.safestring import mark_safe from django.utils.translation import gettext_lazy as _ from wagtail.blocks import RichTextBlock @@ -8,7 +9,6 @@ from wagtail.blocks import RichTextBlock from hypha.apply.review.fields import ScoredAnswerField from hypha.apply.review.options import ( NA, - PRIVATE, RATE_CHOICE_NA, RATE_CHOICES, RATE_CHOICES_DICT, @@ -152,7 +152,7 @@ class VisibilityBlock(ReviewMustIncludeFieldBlock): def get_field_kwargs(self, struct_value): kwargs = super(VisibilityBlock, self).get_field_kwargs(struct_value) kwargs["choices"] = VISIBILITY.items() - kwargs["initial"] = PRIVATE + kwargs["initial"] = settings.REVIEW_VISIBILITY_DEFAULT kwargs["help_text"] = mark_safe( "<br>".join( [ diff --git a/hypha/apply/stream_forms/blocks.py b/hypha/apply/stream_forms/blocks.py index e67c7e57d859b3beb7eaf7ff1cc9814d68ef573f..70d264dd58cc49614085e6357ffd758c776ea60c 100644 --- a/hypha/apply/stream_forms/blocks.py +++ b/hypha/apply/stream_forms/blocks.py @@ -285,6 +285,8 @@ class CheckboxesFieldBlock(OptionalFormFieldBlock): return kwargs def prepare_data(self, value, data, serialize=False): + if not data: + return data base_prepare = super().prepare_data return [base_prepare(value, item, serialize) for item in data] @@ -443,7 +445,9 @@ class MultiFileFieldBlock(UploadableMediaBlock): def prepare_data(self, value, data, serialize): if serialize: - return [file.serialize() for file in data] + if data: + return [file.serialize() for file in data] + return None return data def no_response(self): diff --git a/hypha/apply/stream_forms/models.py b/hypha/apply/stream_forms/models.py index b20e5875dde3936c8a47b6bb32647a6b6998888f..27c0f0cf4a4676a81b5fe71acc0eb4affd60a039 100644 --- a/hypha/apply/stream_forms/models.py +++ b/hypha/apply/stream_forms/models.py @@ -74,9 +74,15 @@ class BaseStreamForm: "You are logged in so this information is fetched from your user account." ) if isinstance(block, FullNameBlock) and user and user.is_authenticated: - field_from_block.disabled = True - field_from_block.initial = user.full_name - field_from_block.help_text = disabled_help_text + if user.full_name: + field_from_block.disabled = True + field_from_block.initial = user.full_name + field_from_block.help_text = disabled_help_text + else: + field_from_block.help_text = _( + "You are logged in but your user account does not have a " + "full name. We'll update your user account with the name you provide here." + ) if isinstance(block, EmailBlock) and user and user.is_authenticated: field_from_block.disabled = True field_from_block.initial = user.email diff --git a/hypha/public/mailchimp/__init__.py b/hypha/apply/todo/__init__.py similarity index 100% rename from hypha/public/mailchimp/__init__.py rename to hypha/apply/todo/__init__.py diff --git a/hypha/apply/todo/apps.py b/hypha/apply/todo/apps.py new file mode 100644 index 0000000000000000000000000000000000000000..b96e45e77612f0a7f8d25a48a70c0732795db266 --- /dev/null +++ b/hypha/apply/todo/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class TodoConfig(AppConfig): + name = "hypha.apply.todo" diff --git a/hypha/apply/todo/migrations/0001_initial.py b/hypha/apply/todo/migrations/0001_initial.py new file mode 100644 index 0000000000000000000000000000000000000000..2a6f39fc8162a8f1236c9b8bdc8920017d46bb7c --- /dev/null +++ b/hypha/apply/todo/migrations/0001_initial.py @@ -0,0 +1,93 @@ +# Generated by Django 4.1.13 on 2023-11-22 10:41 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("contenttypes", "0002_remove_content_type_name"), + ("auth", "0012_alter_user_first_name_max_length"), + ] + + operations = [ + migrations.CreateModel( + name="Task", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "code", + models.CharField( + choices=[ + ("project_waiting_paf", "Project waiting PAF"), + ("project_submit_paf", "Project submit PAF"), + ("paf_required_changes", "PAF required changes"), + ("paf_waiting_assignee", "PAF waiting assignee"), + ("paf_waiting_approval", "PAF waiting approval"), + ("project_waiting_contract", "Project waiting contract"), + ( + "project_waiting_contract_document", + "Project waiting contract document", + ), + ( + "project_waiting_contract_review", + "Project waiting contract review", + ), + ("project_waiting_invoice", "Project waiting invoice"), + ("invoice_required_changes", "Invoice required changes"), + ("invoice_waiting_approval", "Invoice waiting approval"), + ("invoice_waiting_paid", "Invoice waiting paid"), + ("report_due", "Report due"), + ], + max_length=50, + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ( + "related_object_id", + models.PositiveIntegerField(blank=True, null=True), + ), + ( + "related_content_type", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="task_related", + to="contenttypes.contenttype", + ), + ), + ( + "user", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="task", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "user_group", + models.ManyToManyField( + blank=True, related_name="task", to="auth.group" + ), + ), + ], + options={ + "ordering": ("-created_at",), + }, + ), + ] diff --git a/hypha/public/mailchimp/migrations/__init__.py b/hypha/apply/todo/migrations/__init__.py similarity index 100% rename from hypha/public/mailchimp/migrations/__init__.py rename to hypha/apply/todo/migrations/__init__.py diff --git a/hypha/apply/todo/models.py b/hypha/apply/todo/models.py new file mode 100644 index 0000000000000000000000000000000000000000..f9dab5ded9898c605ca6d01cca8792296fba08df --- /dev/null +++ b/hypha/apply/todo/models.py @@ -0,0 +1,36 @@ +from django.contrib.auth.models import Group +from django.contrib.contenttypes.fields import GenericForeignKey +from django.contrib.contenttypes.models import ContentType +from django.db import models + +from hypha.apply.users.models import User + +from .options import TASKS_CODE_CHOICES + + +class Task(models.Model): + code = models.CharField(choices=TASKS_CODE_CHOICES, max_length=50) + user = models.ForeignKey( + User, blank=True, null=True, on_delete=models.CASCADE, related_name="task" + ) + user_group = models.ManyToManyField( + Group, + related_name="task", + blank=True, + ) + created_at = models.DateTimeField(auto_now_add=True) + related_content_type = models.ForeignKey( + ContentType, + blank=True, + null=True, + on_delete=models.CASCADE, + related_name="task_related", + ) + related_object_id = models.PositiveIntegerField(blank=True, null=True) + related_object = GenericForeignKey("related_content_type", "related_object_id") + + class Meta: + ordering = ("-created_at",) + + def save(self, **kwargs): + return super().save(**kwargs) diff --git a/hypha/apply/todo/options.py b/hypha/apply/todo/options.py new file mode 100644 index 0000000000000000000000000000000000000000..1fa7cb5f42ad031d0db4efc1182e33488a6e2302 --- /dev/null +++ b/hypha/apply/todo/options.py @@ -0,0 +1,152 @@ +import copy + +from django.utils.translation import gettext as _ + +from hypha.apply.activity.adapters.utils import link_to + +PROJECT_WAITING_PAF = "project_waiting_paf" +PROJECT_SUBMIT_PAF = "project_submit_paf" +PAF_REQUIRED_CHANGES = "paf_required_changes" +PAF_WAITING_ASSIGNEE = "paf_waiting_assignee" +PAF_WAITING_APPROVAL = "paf_waiting_approval" +PROJECT_WAITING_CONTRACT = "project_waiting_contract" +PROJECT_WAITING_CONTRACT_DOCUMENT = "project_waiting_contract_document" +PROJECT_WAITING_CONTRACT_REVIEW = "project_waiting_contract_review" +PROJECT_WAITING_INVOICE = "project_waiting_invoice" +INVOICE_REQUIRED_CHANGES = "invoice_required_changes" +INVOICE_WAITING_APPROVAL = "invoice_waiting_approval" +INVOICE_WAITING_PAID = "invoice_waiting_paid" +REPORT_DUE = "report_due" + +TASKS_CODE_CHOICES = ( + (PROJECT_WAITING_PAF, "Project waiting PAF"), + (PROJECT_SUBMIT_PAF, "Project submit PAF"), + (PAF_REQUIRED_CHANGES, "PAF required changes"), + (PAF_WAITING_ASSIGNEE, "PAF waiting assignee"), + (PAF_WAITING_APPROVAL, "PAF waiting approval"), + (PROJECT_WAITING_CONTRACT, "Project waiting contract"), + (PROJECT_WAITING_CONTRACT_DOCUMENT, "Project waiting contract document"), + (PROJECT_WAITING_CONTRACT_REVIEW, "Project waiting contract review"), + (PROJECT_WAITING_INVOICE, "Project waiting invoice"), + (INVOICE_REQUIRED_CHANGES, "Invoice required changes"), + (INVOICE_WAITING_APPROVAL, "Invoice waiting approval"), + (INVOICE_WAITING_PAID, "Invoice waiting paid"), + (REPORT_DUE, "Report due"), +) + + +template_map = { + # SUBMISSIONS ACTIONS + # :todo: actions for mupltiple stages of submission + # PROJECT actions + # draft state (staff action) + PROJECT_WAITING_PAF: { + "text": _("Project [{related.title}]({link}) is waiting for PAF"), + "icon": "dashboard-paf", + "url": "{link}", + "type": _("project"), + }, + PROJECT_SUBMIT_PAF: { + "text": _("Project [{related.title}]({link}) is waiting for PAF submission"), + "icon": "dashboard-paf", + "url": "{link}", + "type": _("project"), + }, + PAF_REQUIRED_CHANGES: { + "text": _( + "PAF for project [{related.title}]({link}) required changes or more information" + ), + "icon": "dashboard-paf", + "url": "{link}", + "type": _("project"), + }, + # internal approval state (approvers/finance... action) + PAF_WAITING_ASSIGNEE: { + "text": _("PAF for project [{related.title}]({link}) is waiting for assignee"), + "icon": "dashboard-paf", + "url": "{link}", + "type": _("project"), + }, + PAF_WAITING_APPROVAL: { + "text": _( + "PAF for project [{related.title}]({link}) is waiting for your approval" + ), + "icon": "dashboard-paf", + "url": "{link}", + "type": _("project"), + }, + # contracting state (vendor/staff/contracting team action) + PROJECT_WAITING_CONTRACT: { + "text": _("Project [{related.title}]({link}) is waiting for contract"), + "icon": "dashboard-contract", + "url": "{link}", + "type": _("project"), + }, + PROJECT_WAITING_CONTRACT_DOCUMENT: { + "text": _( + "Project [{related.title}]({link}) is waiting for contracting documents" + ), + "icon": "dashboard-document", + "url": "{link}", + "type": _("project"), + }, + PROJECT_WAITING_CONTRACT_REVIEW: { + "text": _( + "Contract for project [{related.title}]({link}) is waiting for review" + ), + "icon": "dashboard-contract", + "url": "{link}", + "type": _("project"), + }, + # invoicing and reporting (vendor/staff/finance team action) + PROJECT_WAITING_INVOICE: { + "text": _("Project [{related.title}]({link}) is waiting for invoice"), + "icon": "dashboard-invoice", + "url": "{link}", + "type": _("project"), + }, + INVOICE_REQUIRED_CHANGES: { + "text": _( + "Invoice [{related.invoice_number}]({link}) required changes or more information" + ), + "icon": "dashboard-invoice", + "url": "{link}", + "type": _("project"), + }, + INVOICE_WAITING_APPROVAL: { + "text": _( + "Invoice [{related.invoice_number}]({link}) is waiting for your approval" + ), + "icon": "dashboard-invoice", + "url": "{link}", + "type": _("project"), + }, + INVOICE_WAITING_PAID: { + "text": _("Invoice [{related.invoice_number}]({link}) is waiting to be paid"), + "icon": "dashboard-invoice", + "url": "{link}", + "type": _("project"), + }, + REPORT_DUE: { + "text": _("Report for project [{related.title}]({link}) is due"), + "icon": "dashboard-report", + "url": "{link}", + "type": _("project"), + }, +} + + +def get_task_template(request, code, related_obj, **kwargs): + templates = copy.deepcopy(template_map) + try: + template = templates[code] + except KeyError: + # Unregistered code + return + template_kwargs = { + "related": related_obj, + "link": link_to(related_obj, request), + } + template["text"] = template["text"].format(**template_kwargs) + template["url"] = template["url"].format(**template_kwargs) + return template diff --git a/hypha/apply/todo/services.py b/hypha/apply/todo/services.py new file mode 100644 index 0000000000000000000000000000000000000000..47b85fe7b76f460a3b471577849b00df94806b01 --- /dev/null +++ b/hypha/apply/todo/services.py @@ -0,0 +1,68 @@ +from django.contrib.contenttypes.models import ContentType +from django.db.models import Count + +from hypha.apply.activity.adapters.utils import get_users_for_groups + +from .models import Task + + +def validate_user_uniquness(code, user, related_obj): + """ + code + related_object + user should be unique together. + """ + matching_tasks = Task.objects.filter( + code=code, + related_content_type=ContentType.objects.get_for_model(related_obj).id, + related_object_id=related_obj.id, + ) + if matching_tasks.filter(user=user).exists(): + # if same task already assigned to the same user + # raise ValidationError("Task is already assigned to the user") # :todo: add validation msg as a log msg? + return False + else: + # if same task is already assigned to user's user_group + user_group_matching_tasks = matching_tasks.annotate( + group_count=Count("user_group") + ).filter(group_count=len(user.groups.all())) + for group in user.groups.all(): + user_group_matching_tasks = user_group_matching_tasks.filter( + user_group__id=group.id + ) + if user_group_matching_tasks.exists(): + # raise ValidationError("Task is already assigned to user's group") + return False + return True + + +def validate_user_groups_uniqueness(code, user_groups, related_obj): + """ + code + related_object + user_group should be unique together. + """ + matching_tasks = Task.objects.filter( + code=code, + related_content_type=ContentType.objects.get_for_model(related_obj).id, + related_object_id=related_obj.id, + ) + user_group_matching_tasks = matching_tasks.annotate( + group_count=Count("user_group") + ).filter(group_count=len(user_groups)) + for group in user_groups: + user_group_matching_tasks = user_group_matching_tasks.filter( + user_group__id=group.id + ) + if user_group_matching_tasks.exists(): + # same task with same user_group already exists + # :todo: add validation msg as a log msg? + return False + + # user with exact user group already assigned for same task + users = get_users_for_groups( + list(user_groups), exact_match=True + ) # users with provided user_group + + for user in users: + if matching_tasks.filter(user=user).exists(): + Task.objects.filter( + id=matching_tasks.id + ).delete() # delete those user's tasks + return True diff --git a/hypha/public/mailchimp/tests/__init__.py b/hypha/apply/todo/tests.py similarity index 100% rename from hypha/public/mailchimp/tests/__init__.py rename to hypha/apply/todo/tests.py diff --git a/hypha/apply/todo/views.py b/hypha/apply/todo/views.py new file mode 100644 index 0000000000000000000000000000000000000000..276b770cb8d77953109abcf8d66c2fa0241eb15a --- /dev/null +++ b/hypha/apply/todo/views.py @@ -0,0 +1,138 @@ +from django.contrib.auth.models import Group +from django.contrib.contenttypes.models import ContentType +from django.db.models import Count + +from .models import Task +from .options import get_task_template +from .services import validate_user_groups_uniqueness, validate_user_uniquness + + +def add_task_to_user(code, user, related_obj): + """ + Add task for a user + input: + code: TASKS_CODE_CHOICES.keys() + user: User object + related_obj: Object - Submission, Project, Invoice, Report + output: task - Task object / None in case of no creation + """ + user_uniqueness = validate_user_uniquness( + code=code, user=user, related_obj=related_obj + ) + if user_uniqueness: + task = Task.objects.create(code=code, user=user, related_object=related_obj) + return task + return None + + +def add_task_to_user_group(code, user_group, related_obj): + """ + Add task for user_groups + input: + code: TASKS_CODE_CHOICES.keys() + user_group: Queryset - Group objects + related_obj: Object - Submission, Project, Invoice, Report + output: task - Task object / None in case of no creation + """ + user_groups_uniqueness = validate_user_groups_uniqueness( + code=code, user_groups=user_group, related_obj=related_obj + ) + if user_groups_uniqueness: + task = Task.objects.create(code=code, related_object=related_obj) + groups = [Group.objects.filter(id=group.id).first() for group in user_group] + task.user_group.add(*groups) + return task + return None + + +def remove_tasks_for_user(code, user, related_obj): + """ + Remove task for a user + input: + code: TASKS_CODE_CHOICES.keys() + user: User object + related_obj: Object - Submission, Project, Invoice, Report + output: None + """ + task = Task.objects.filter( + code=code, + user=user, + related_content_type=ContentType.objects.get_for_model(related_obj).id, + related_object_id=related_obj.id, + ).first() + if task: + task.delete() + return None + + +def remove_tasks_for_user_group(code, user_group, related_obj): + """ + Remove task for user_groups + input: + code: TASKS_CODE_CHOICES.keys() + user_group: Queryset - Group objects + related_obj: Object - Submission, Project, Invoice, Report + output: None + """ + matching_tasks = Task.objects.filter( + code=code, + related_content_type=ContentType.objects.get_for_model(related_obj).id, + related_object_id=related_obj.id, + ) + user_group_matching_tasks = matching_tasks.annotate( + group_count=Count("user_group") + ).filter(group_count=len(user_group.all())) + for group in user_group.all(): + user_group_matching_tasks = user_group_matching_tasks.filter( + user_group__id=group.id + ) + if user_group_matching_tasks.exists(): + user_group_matching_tasks.delete() + return None + + +def remove_tasks_of_related_obj(related_obj): + """ + Remove all tasks of a related object irrespective of their code and users + input: + related_obj: Object - Submission, Project, Invoice, Report + """ + Task.objects.filter( + related_content_type=ContentType.objects.get_for_model(related_obj).id, + related_object_id=related_obj.id, + ).delete() + return None + + +def get_tasks_for_user(user): + user_tasks = Task.objects.filter(user=user).annotate( + group_count=Count("user_group") + ) + user_group_tasks = Task.objects.annotate(group_count=Count("user_group")).filter( + group_count=len(user.groups.all()) + ) + for group in user.groups.all(): + user_group_tasks = user_group_tasks.filter(user_group__id=group.id) + + return user_tasks.union(user_group_tasks) + + +def render_task_templates_for_user(request, user): + """ + input: request (HttpRequest) + input: user (User object) + + output: [{ + "text":"", + "icon":"", + "url":"", + "type":"", + }, + ] + """ + tasks = get_tasks_for_user(user) + templates = [ + get_task_template(request, code=task.code, related_obj=task.related_object) + for task in tasks + ] + return templates diff --git a/hypha/apply/urls.py b/hypha/apply/urls.py index 82ec8597497719a5014a39ce792f2b2209980271..875098d215a4614648c88cdca0e5e6cd3fea5ca7 100644 --- a/hypha/apply/urls.py +++ b/hypha/apply/urls.py @@ -21,9 +21,7 @@ urlpatterns = [ # page and advances user to download backup code page. path( "account/two_factor/setup/complete/", - RedirectView.as_view( - url=reverse_lazy("users:backup_tokens_password"), permanent=False - ), + RedirectView.as_view(url=reverse_lazy("users:backup_tokens"), permanent=False), name="two_factor:setup_complete", ), path("", include(tf_urls, "two_factor")), @@ -39,3 +37,4 @@ urlpatterns += base_urlpatterns handler404 = "hypha.apply.utils.views.page_not_found" +handler403 = "hypha.apply.utils.views.permission_denied" diff --git a/hypha/apply/users/admin_views.py b/hypha/apply/users/admin_views.py index 2f49edb44ad0eb0259c7dcb21aab49098c8dce4f..f9a433c7bf5c1438e3cd8e6a1b46cf268e8b2593 100644 --- a/hypha/apply/users/admin_views.py +++ b/hypha/apply/users/admin_views.py @@ -9,6 +9,7 @@ from django.core.paginator import Paginator from django.db.models import Q from django.http import HttpResponse from django.shortcuts import get_object_or_404 +from django.template.defaultfilters import mark_safe from django.template.response import TemplateResponse from django.utils.translation import gettext as _ from django.views.decorators.vary import vary_on_headers @@ -16,7 +17,9 @@ from wagtail.admin.auth import any_permission_required from wagtail.admin.filters import WagtailFilterSet from wagtail.admin.forms.search import SearchForm from wagtail.compat import AUTH_USER_APP_LABEL, AUTH_USER_MODEL_NAME -from wagtail.users.views.groups import GroupViewSet +from wagtail.users.views.groups import GroupViewSet, IndexView + +from .models import GroupDesc User = get_user_model() @@ -200,12 +203,48 @@ def index(request, *args): ) +class CustomGroupIndexView(IndexView): + """ + Overriding of wagtail.users.views.groups.IndexView to allow for the addition of help text to the displayed group names. This is done utilizing the get_queryset method + """ + + def get_queryset(self): + """ + Overriding the normal queryset that would return all Group objects, this returnd an iterable of groups with custom names containing HTML help text. + """ + group_qs = super().get_queryset() + + custom_groups = [] + + for group in group_qs: + help_text = GroupDesc.get_from_group(group) + if help_text: + group.name = mark_safe( + f"{group.name}<p class=group-help-text>{help_text}</p>" + ) + + custom_groups.append(group) + + return custom_groups + + class CustomGroupViewSet(GroupViewSet): """ Overriding the wagtail.users.views.groups.GroupViewSet just to use custom users view(index) when getting all users for a group. """ + index_view_class = CustomGroupIndexView + @property def users_view(self): return index + + def __init__(self, name, **kwargs): + super().__init__(name, **kwargs) + + @property + def index_view(self): + return self.index_view_class.as_view( + **self.get_index_view_kwargs(), + ) diff --git a/hypha/apply/users/forms.py b/hypha/apply/users/forms.py index 13dcd722e7c1e7baffd5d66aa7058f41de46314f..ef8005b26de1888e9aa38075e4a071a98462f5e4 100644 --- a/hypha/apply/users/forms.py +++ b/hypha/apply/users/forms.py @@ -1,11 +1,12 @@ from django import forms from django.contrib.auth import get_user_model from django.contrib.auth.forms import AuthenticationForm +from django.template.defaultfilters import mark_safe from django.utils.translation import gettext_lazy as _ from django_select2.forms import Select2Widget from wagtail.users.forms import UserCreationForm, UserEditForm -from .models import AuthSettings +from .models import AuthSettings, GroupDesc User = get_user_model() @@ -23,7 +24,39 @@ class CustomAuthenticationForm(AuthenticationForm): ) +class PasswordlessAuthForm(forms.Form): + """Form to collect the email for passwordless login or signup (if enabled) + + Adds login extra text and user content to the form, if configured in the + wagtail auth settings. + """ + + email = forms.EmailField( + label=_("Email Address"), + required=True, + max_length=254, + widget=forms.EmailInput(attrs={"autofocus": True, "autocomplete": "email"}), + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.request = kwargs.pop("request", None) + self.user_settings = AuthSettings.load(request_or_site=self.request) + self.extra_text = self.user_settings.extra_text + if self.user_settings.consent_show: + self.fields["consent"] = forms.BooleanField( + label=self.user_settings.consent_text, + help_text=self.user_settings.consent_help, + required=True, + ) + + class CustomUserAdminFormBase: + error_messages = { + "duplicate_username": _("A user with that email already exists."), + "password_mismatch": _("The two password fields didn't match."), + } + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -36,28 +69,73 @@ class CustomUserAdminFormBase: ) +class GroupsModelMultipleChoiceField(forms.ModelMultipleChoiceField): + """ + A custom ModelMultipleChoiceField utilized to provide a custom label for the group prompts + """ + + @classmethod + def get_group_mmcf( + cls, model_mulitple_choice_field: forms.ModelMultipleChoiceField + ): # Handle the insertion of group help text + group_field_dict = model_mulitple_choice_field.__dict__ + queryset = group_field_dict[ + "_queryset" + ] # Pull the queryset form the group field + unneeded_keys = ("empty_label", "_queryset") + for key in unneeded_keys: + group_field_dict.pop( + key, None + ) # Pop unneeded keys/values, ignore if they don't exist. + + # Overwrite the existing group's ModelMultipleChoiceField with the custom GroupsModelMultipleChoiceField that will provide the help text + return GroupsModelMultipleChoiceField(queryset=queryset, **group_field_dict) + + def label_from_instance(self, group_obj): + """ + Overwriting ModelMultipleChoiceField's label from instance to provide help_text (if it exists) + """ + help_text = GroupDesc.get_from_group(group_obj) + if help_text: + return mark_safe( + f'{group_obj.name}<p class="group-help-text">{help_text}</p>' + ) + return group_obj.name + + class CustomUserEditForm(CustomUserAdminFormBase, UserEditForm): - pass + # pass + """ + A custom UserEditForm used to provide custom fields (ie. custom group fields) + """ + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # Overwrite the existing group's ModelMultipleChoiceField with the custom GroupsModelMultipleChoiceField that will provide the help text + self.fields["groups"] = GroupsModelMultipleChoiceField.get_group_mmcf( + self.fields["groups"] + ) -class CustomUserCreationForm(CustomUserAdminFormBase, UserCreationForm): - error_messages = { - "duplicate_username": _("A user with that email already exists."), - "password_mismatch": _("The two password fields didn't match."), - } - def __init__(self, request=None, *args, **kwargs): +class CustomUserCreationForm(CustomUserAdminFormBase, UserCreationForm): + def __init__(self, register_view=False, request=None, *args, **kwargs): self.request = request super().__init__(*args, **kwargs) self.user_settings = AuthSettings.load(request_or_site=self.request) - if self.user_settings.consent_show: + if register_view and self.user_settings.consent_show: self.fields["consent"] = forms.BooleanField( label=self.user_settings.consent_text, help_text=self.user_settings.consent_help, required=True, ) + # Overwrite the existing group's ModelMultipleChoiceField with the custom GroupsModelMultipleChoiceField that will provide the help text + self.fields["groups"] = GroupsModelMultipleChoiceField.get_group_mmcf( + self.fields["groups"] + ) + class ProfileForm(forms.ModelForm): class Meta: @@ -65,18 +143,21 @@ class ProfileForm(forms.ModelForm): fields = ["full_name", "email", "slack"] def __init__(self, *args, **kwargs): + self.request = kwargs.pop("request", None) super().__init__(*args, **kwargs) if not self.instance.is_apply_staff_or_finance: del self.fields["slack"] - if not self.instance.has_usable_password(): - # User is registered with oauth - no password change allowed - email_field = self.fields["email"] - email_field.disabled = True - email_field.required = False - email_field.help_text = _( - "You are registered using OAuth, please contact an admin if you need to change your email address." - ) + if self.request is not None: + backend = self.request.session["_auth_user_backend"] + if "social_core.backends" in backend: + # User is registered with oauth - no password change allowed + email_field = self.fields["email"] + email_field.disabled = True + email_field.required = False + email_field.help_text = _( + "You are registered using OAuth, please contact an admin if you need to change your email address." + ) def clean_slack(self): slack = self.cleaned_data["slack"] @@ -139,22 +220,19 @@ class EmailChangePasswordForm(forms.Form): return self.user -class TWOFAPasswordForm(forms.Form): - password = forms.CharField( - label=_("Please type your password to confirm"), - strip=False, - widget=forms.PasswordInput(attrs={"autofocus": True}), +class Disable2FAConfirmationForm(forms.Form): + confirmation_text = forms.CharField( + label=_('To proceed, type "disable" below and then click on "confirm":'), + strip=True, + # add widget with autofocus to avoid password autofill + widget=forms.TextInput(attrs={"autofocus": True, "autocomplete": "off"}), ) - def __init__(self, user, *args, **kwargs): - super().__init__(*args, **kwargs) - self.user = user - - def clean_password(self): - password = self.cleaned_data["password"] - if not self.user.check_password(password): + def clean_confirmation_text(self): + text = self.cleaned_data["confirmation_text"] + if text != "disable": raise forms.ValidationError( - _("Incorrect password. Please try again."), - code="password_incorrect", + _("Incorrect input."), + code="confirmation_text_incorrect", ) - return password + return text diff --git a/hypha/apply/users/groups.py b/hypha/apply/users/groups.py index 41bcc0abf76cd42ea019d5d19ffa14961232412b..882f02f819fc600fc8db802ffb543f64e3d9e200 100644 --- a/hypha/apply/users/groups.py +++ b/hypha/apply/users/groups.py @@ -1,48 +1,93 @@ -APPLICANT_GROUP_NAME = "Applicant" -STAFF_GROUP_NAME = "Staff" -REVIEWER_GROUP_NAME = "Reviewer" -TEAMADMIN_GROUP_NAME = "Staff Admin" -PARTNER_GROUP_NAME = "Partner" -COMMUNITY_REVIEWER_GROUP_NAME = "Community reviewer" -APPROVER_GROUP_NAME = "Approver" -FINANCE_GROUP_NAME = "Finance" -CONTRACTING_GROUP_NAME = "Contracting" +from django.utils.translation import gettext_lazy as _ + +SUPERADMIN = _("Administrator") +APPLICANT_GROUP_NAME = _("Applicant") +STAFF_GROUP_NAME = _("Staff") +REVIEWER_GROUP_NAME = _("Reviewer") +TEAMADMIN_GROUP_NAME = _("Staff Admin") +PARTNER_GROUP_NAME = _("Partner") +COMMUNITY_REVIEWER_GROUP_NAME = _("Community reviewer") +APPROVER_GROUP_NAME = _("Approver") +FINANCE_GROUP_NAME = _("Finance") +CONTRACTING_GROUP_NAME = _("Contracting") + +APPLICANT_HELP_TEXT = _( + "Can access their own application and communicate via the communication tab." +) +STAFF_HELP_TEXT = _( + "View and edit all submissions, submit reviews, send determinations, and set up applications." +) +REVIEWER_HELP_TEXT = _( + "Has a dashboard and can submit reviews. Advisory Council Members are typically assigned this role." +) + +TEAMADMIN_HELP_TEXT = _( + "Can view application message log. Must also be in group Staff." +) + +PARTNER_HELP_TEXT = _( + "Can view, edit, and comment on a specific application they are assigned to." +) + +COMMUNITY_REVIEWER_HELP_TEXT = _( + "An applicant with access to other applications utilizing the community/peer review workflow." +) + +APPROVER_HELP_TEXT = _( + "Can review/approve PAF, and access compliance documents. Must also be in group: Staff, Contracting, or Finance." +) +FINANCE_HELP_TEXT = _( + "Can review/approve the PAF, access documents associated with contracting, and access invoices approved by Staff." +) +CONTRACTING_HELP_TEXT = _( + "Can review/approve the PAF and access documents associated with contracting." +) + GROUPS = [ { "name": APPLICANT_GROUP_NAME, "permissions": [], + "help_text": APPLICANT_HELP_TEXT, }, { "name": STAFF_GROUP_NAME, "permissions": [], + "help_text": STAFF_HELP_TEXT, }, { "name": REVIEWER_GROUP_NAME, "permissions": [], + "help_text": REVIEWER_HELP_TEXT, }, { "name": TEAMADMIN_GROUP_NAME, "permissions": [], + "help_text": TEAMADMIN_HELP_TEXT, }, { "name": PARTNER_GROUP_NAME, "permissions": [], + "help_text": PARTNER_HELP_TEXT, }, { "name": COMMUNITY_REVIEWER_GROUP_NAME, "permissions": [], + "help_text": COMMUNITY_REVIEWER_HELP_TEXT, }, { "name": APPROVER_GROUP_NAME, "permissions": [], + "help_text": APPROVER_HELP_TEXT, }, { "name": FINANCE_GROUP_NAME, "permissions": [], + "help_text": FINANCE_HELP_TEXT, }, { "name": CONTRACTING_GROUP_NAME, "permissions": [], + "help_text": CONTRACTING_HELP_TEXT, }, ] diff --git a/hypha/apply/users/middleware.py b/hypha/apply/users/middleware.py index fd11afec3749570a842a75c533280b4cd9b72beb..d38207b9d744153a72a9c82ab8da536353c53ab3 100644 --- a/hypha/apply/users/middleware.py +++ b/hypha/apply/users/middleware.py @@ -1,17 +1,33 @@ +import logging + from django.conf import settings -from django.shortcuts import redirect +from django.core.exceptions import MiddlewareNotUsed +from django.urls import set_urlconf +from django.utils.log import log_response +from django.utils.translation import gettext_lazy as _ from social_core.exceptions import AuthForbidden from social_django.middleware import ( SocialAuthExceptionMiddleware as _SocialAuthExceptionMiddleware, ) -ALLOWED_SUBPATH_FOR_UNVERIFIED_USERS = [ - "login/", - "logout/", - "account/", +from hypha.apply.users.views import mfa_failure_view + +logger = logging.getLogger("django.security.two_factor") + +TWO_FACTOR_EXEMPTED_PATH_PREFIXES = [ + "/auth/", + "/login/", + "/logout/", + "/account/", + "/apply/submissions/success/", ] +def get_page_path(wagtail_page): + _, _, page_path = wagtail_page.get_url_parts() + return page_path + + class SocialAuthExceptionMiddleware(_SocialAuthExceptionMiddleware): """ Wrapper around SocialAuthExceptionMiddleware to customise messages @@ -31,31 +47,74 @@ class TwoFactorAuthenticationMiddleware: To activate this middleware set env variable ENFORCE_TWO_FACTOR as True. This will redirect all request from unverified users to enable 2FA first. - Except the request made on the url paths listed in ALLOWED_SUBPATH_FOR_UNVERIFIED_USERS. + Except the request made on the url paths listed in TWO_FACTOR_EXEMPTED_PATH_PREFIXES. """ + reason = _("Two factor authentication required") + def __init__(self, get_response): + if not settings.ENFORCE_TWO_FACTOR: + raise MiddlewareNotUsed() + self.get_response = get_response - def is_path_allowed(self, path): - for sub_path in ALLOWED_SUBPATH_FOR_UNVERIFIED_USERS: - if sub_path in path: + def _accept(self, request): + return self.get_response(request) + + def _reject(self, request, reason): + set_urlconf("hypha.apply.urls") + response = mfa_failure_view(request, reason=reason) + log_response( + "Forbidden (%s): %s", + reason, + request.path, + response=response, + request=request, + logger=logger, + ) + return response + + def whitelisted_paths(self, path): + if path == "/": + return True + + for sub_path in TWO_FACTOR_EXEMPTED_PATH_PREFIXES: + if path.startswith(sub_path): return True return False + def get_urls_open_rounds(self): + from hypha.apply.funds.models import ApplicationBase + + return map( + get_page_path, ApplicationBase.objects.order_by_end_date().specific() + ) + + def get_urls_open_labs(self): + from hypha.apply.funds.models import LabBase + + return map( + get_page_path, + LabBase.objects.public().live().specific(), + ) + def __call__(self, request): + if self.whitelisted_paths(request.path): + return self._accept(request) + # code to execute before the view user = request.user - if settings.ENFORCE_TWO_FACTOR: - if ( - user.is_authenticated - and not user.is_verified() - and not user.social_auth.exists() - ): - if not self.is_path_allowed(request.path): - return redirect("/account/two_factor/required/") - - response = self.get_response(request) - - # code to execute after view - return response + if user.is_authenticated: + if user.social_auth.exists() or user.is_verified(): + return self._accept(request) + + # Allow rounds and lab detail pages + if request.path in self.get_urls_open_rounds(): + return self._accept(request) + + if request.path in self.get_urls_open_labs(): + return self._accept(request) + + return self._reject(request, self.reason) + + return self._accept(request) diff --git a/hypha/apply/users/migrations/0021_groupdesc.py b/hypha/apply/users/migrations/0021_groupdesc.py new file mode 100644 index 0000000000000000000000000000000000000000..44116250b45a6e53290f1bd60a361bf1b881ce61 --- /dev/null +++ b/hypha/apply/users/migrations/0021_groupdesc.py @@ -0,0 +1,44 @@ +# Generated by Django 3.2.22 on 2023-10-31 17:26 + +from django.db import migrations, models +from django.contrib.auth.models import Group +import django.db.models.deletion + +from hypha.apply.users.groups import GROUPS +from hypha.apply.users.models import GroupDesc + + +def add_desc_groups(apps, schema_editor): + for group_data in GROUPS: + group, created = Group.objects.get_or_create(name=group_data["name"]) + if group_data.get("help_text") is not None: + GroupDesc.objects.create(group=group, help_text=group_data["help_text"]) + + +class Migration(migrations.Migration): + dependencies = [ + ("auth", "0012_alter_user_first_name_max_length"), + ("users", "0020_auto_20230625_1825"), + ] + + operations = [ + migrations.CreateModel( + name="GroupDesc", + fields=[ + ( + "group", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + primary_key=True, + serialize=False, + to="auth.group", + ), + ), + ( + "help_text", + models.CharField(max_length=255, verbose_name="Help Text"), + ), + ], + ), + migrations.RunPython(add_desc_groups), + ] diff --git a/hypha/apply/users/migrations/0021_pendingsignup.py b/hypha/apply/users/migrations/0021_pendingsignup.py new file mode 100644 index 0000000000000000000000000000000000000000..a40e95239c435f609af7dce4433f1f0f4bb9ce6d --- /dev/null +++ b/hypha/apply/users/migrations/0021_pendingsignup.py @@ -0,0 +1,34 @@ +# Generated by Django 3.2.21 on 2023-09-12 08:07 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("users", "0020_auto_20230625_1825"), + ] + + operations = [ + migrations.CreateModel( + name="PendingSignup", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("email", models.EmailField(max_length=254, unique=True)), + ("created", models.DateTimeField(auto_now_add=True)), + ("modified", models.DateTimeField(auto_now=True)), + ("token", models.CharField(max_length=255, unique=True)), + ], + options={ + "verbose_name_plural": "Pending signups", + "ordering": ("created",), + }, + ), + ] diff --git a/hypha/apply/users/migrations/0022_confirmaccesstoken.py b/hypha/apply/users/migrations/0022_confirmaccesstoken.py new file mode 100644 index 0000000000000000000000000000000000000000..20b34b7c3e6efb5dc744c87201b79c686f65cc74 --- /dev/null +++ b/hypha/apply/users/migrations/0022_confirmaccesstoken.py @@ -0,0 +1,42 @@ +# Generated by Django 3.2.22 on 2023-10-31 06:59 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + dependencies = [ + ("users", "0021_pendingsignup"), + ] + + operations = [ + migrations.CreateModel( + name="ConfirmAccessToken", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("token", models.CharField(max_length=6)), + ("created", models.DateTimeField(auto_now_add=True)), + ("modified", models.DateTimeField(auto_now=True)), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "verbose_name_plural": "Confirm Access Tokens", + "ordering": ("modified",), + }, + ), + ] diff --git a/hypha/apply/users/migrations/0023_merge_0021_groupdesc_0022_confirmaccesstoken.py b/hypha/apply/users/migrations/0023_merge_0021_groupdesc_0022_confirmaccesstoken.py new file mode 100644 index 0000000000000000000000000000000000000000..7461821d1c9b23a862a59e3aecd1a5b99c08b35e --- /dev/null +++ b/hypha/apply/users/migrations/0023_merge_0021_groupdesc_0022_confirmaccesstoken.py @@ -0,0 +1,12 @@ +# Generated by Django 4.1.13 on 2023-11-27 14:46 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("users", "0021_groupdesc"), + ("users", "0022_confirmaccesstoken"), + ] + + operations = [] diff --git a/hypha/apply/users/models.py b/hypha/apply/users/models.py index 44ee07b515f51bc6ec66a59e1c23e2fedb6eba8e..43783f54429fffdc6fce4bca88a23b7d602a7a5e 100644 --- a/hypha/apply/users/models.py +++ b/hypha/apply/users/models.py @@ -6,6 +6,7 @@ from django.db import IntegrityError, models from django.db.models.constants import LOOKUP_SEP from django.db.models.utils import resolve_callables from django.urls import reverse +from django.utils.crypto import get_random_string from django.utils.functional import cached_property from django.utils.translation import gettext_lazy as _ from wagtail.admin.panels import FieldPanel, MultiFieldPanel @@ -23,7 +24,11 @@ from .groups import ( STAFF_GROUP_NAME, TEAMADMIN_GROUP_NAME, ) -from .utils import get_user_by_email, is_user_already_registered, send_activation_email +from .utils import ( + get_user_by_email, + is_user_already_registered, + send_activation_email, +) class UserQuerySet(models.QuerySet): @@ -169,7 +174,7 @@ class UserManager(BaseUserManager.from_queryset(UserQuerySet)): # Coming from registration without application temp_pass = kwargs.pop("password") else: - temp_pass = BaseUserManager().make_random_password(length=32) + temp_pass = get_random_string(length=32) temp_pass_hash = make_password(temp_pass) @@ -185,10 +190,6 @@ class UserManager(BaseUserManager.from_queryset(UserQuerySet)): send_activation_email(user, site, redirect_url=redirect_url) _created = True - applicant_group = Group.objects.get(name=APPLICANT_GROUP_NAME) - if applicant_group not in user.groups.all(): - user.groups.add(applicant_group) - user.save() return user, _created @@ -285,6 +286,18 @@ class User(AbstractUser): and not self.groups.filter(name=APPROVER_GROUP_NAME).exists() ) + @cached_property + def can_access_dashboard(self): + return ( + self.is_apply_staff + or self.is_reviewer + or self.is_partner + or self.is_community_reviewer + or self.is_finance + or self.is_contracting + or self.is_applicant + ) + @cached_property def is_finance_level_2(self): # disable finance2 user if invoice flow in not extended @@ -362,3 +375,65 @@ class AuthSettings(BaseGenericSetting): _("Register form customizations"), ), ] + + +class GroupDesc(models.Model): + group = models.OneToOneField(Group, on_delete=models.CASCADE, primary_key=True) + help_text = models.CharField(verbose_name="Help Text", max_length=255) + + @staticmethod + def get_from_group(group_obj: Group) -> str | None: + """ + Get the group description/help text string from a Group object. Returns None if group doesn't have a help text entry. + + Args: + group_obj (Group): The group to retrieve the description of. + """ + try: + return GroupDesc.objects.get(group_id=group_obj.id).help_text + except (exceptions.ObjectDoesNotExist, exceptions.FieldError): + return None + + def __str__(self): + return self.help_text + + +class PendingSignup(models.Model): + """This model tracks pending passwordless self-signups, and is used to + generate a one-time use URLfor each signup. + + The URL is sent to the user via email, and when they click on it, they are + redirected to the registration page, where a new is created. + + Once the user is created, the PendingSignup instance is deleted. + """ + + email = models.EmailField(unique=True) + created = models.DateTimeField(auto_now_add=True) + modified = models.DateTimeField(auto_now=True) + token = models.CharField(max_length=255, unique=True) + + def __str__(self): + return f"{self.email} ({self.created})" + + class Meta: + ordering = ("created",) + verbose_name_plural = "Pending signups" + + +class ConfirmAccessToken(models.Model): + """ + Once the user is created, the PendingSignup instance is deleted. + """ + + token = models.CharField(max_length=6) + user = models.ForeignKey(User, on_delete=models.CASCADE) + created = models.DateTimeField(auto_now_add=True) + modified = models.DateTimeField(auto_now=True) + + def __str__(self): + return f"ConfirmAccessToken: {self.user.email} ({self.created})" + + class Meta: + ordering = ("modified",) + verbose_name_plural = "Confirm Access Tokens" diff --git a/hypha/apply/users/services.py b/hypha/apply/users/services.py new file mode 100644 index 0000000000000000000000000000000000000000..8762aa00c2be7f7f98cdb9d85da5258de084d8c4 --- /dev/null +++ b/hypha/apply/users/services.py @@ -0,0 +1,162 @@ +from django.conf import settings +from django.contrib.auth import get_user_model +from django.http import HttpRequest +from django.urls import reverse +from django.utils.crypto import get_random_string +from django.utils.encoding import force_bytes +from django.utils.http import urlsafe_base64_encode +from wagtail.models import Site + +from hypha.core.mail import MarkdownMail + +from .models import PendingSignup +from .tokens import PasswordlessLoginTokenGenerator, PasswordlessSignupTokenGenerator +from .utils import get_redirect_url, get_user_by_email + +User = get_user_model() + + +class PasswordlessAuthService: + login_token_generator_class = PasswordlessLoginTokenGenerator + signup_token_generator_class = PasswordlessSignupTokenGenerator + + next_url = None + + def __init__(self, request: HttpRequest, redirect_field_name: str = "next") -> None: + self.redirect_field_name = redirect_field_name + self.next_url = get_redirect_url(request, self.redirect_field_name) + self.request = request + self.site = Site.find_for_request(request) + + def _get_login_path(self, user): + token = self.login_token_generator_class().make_token(user) + uid = urlsafe_base64_encode(force_bytes(user.pk)) + login_path = reverse( + "users:do_passwordless_login", kwargs={"uidb64": uid, "token": token} + ) + + if self.next_url: + login_path = f"{login_path}?next={self.next_url}" + + return login_path + + def _get_signup_path(self, signup_obj): + token = self.signup_token_generator_class().make_token(user=signup_obj) + uid = urlsafe_base64_encode(force_bytes(signup_obj.pk)) + + signup_path = reverse( + "users:do_passwordless_signup", kwargs={"uidb64": uid, "token": token} + ) + + if self.next_url: + signup_path = f"{signup_path}?next={self.next_url}" + + return signup_path + + def get_email_context(self) -> dict: + return { + "org_long_name": settings.ORG_LONG_NAME, + "org_email": settings.ORG_EMAIL, + "org_short_name": settings.ORG_SHORT_NAME, + "site": self.site, + } + + def send_email_no_account_found(self, to): + context = self.get_email_context() + subject = "Login attempt at {org_long_name}".format(**context) + # Force subject to a single line to avoid header-injection issues. + subject = "".join(subject.splitlines()) + + email = MarkdownMail("users/emails/passwordless_login_no_account_found.md") + email.send( + to=to, + subject=subject, + from_email=settings.DEFAULT_FROM_EMAIL, + context=context, + ) + + def send_login_email(self, user): + login_path = self._get_login_path(user) + timeout_minutes = self.login_token_generator_class().TIMEOUT // 60 + + context = self.get_email_context() + context.update( + { + "user": user, + "is_active": user.is_active, + "name": user.get_full_name(), + "username": user.get_username(), + "login_path": login_path, + "timeout_minutes": timeout_minutes, + } + ) + + subject = "Login to {username} at {org_long_name}".format(**context) + # Force subject to a single line to avoid header-injection issues. + subject = "".join(subject.splitlines()) + + email = MarkdownMail("users/emails/passwordless_login_email.md") + email.send( + to=user.email, + subject=subject, + from_email=settings.DEFAULT_FROM_EMAIL, + context=context, + ) + + def send_new_account_login_email(self, signup_obj): + signup_path = self._get_signup_path(signup_obj) + timeout_minutes = self.login_token_generator_class().TIMEOUT // 60 + + context = self.get_email_context() + context.update( + { + "signup_path": signup_path, + "timeout_minutes": timeout_minutes, + } + ) + + subject = "Welcome to {org_long_name}".format(**context) + # Force subject to a single line to avoid header-injection issues. + subject = "".join(subject.splitlines()) + + email = MarkdownMail("users/emails/passwordless_new_account_login.md") + email.send( + to=signup_obj.email, + subject=subject, + from_email=settings.DEFAULT_FROM_EMAIL, + context=context, + ) + + def initiate_login_signup(self, email: str) -> None: + """Send a passwordless login/signup email. + + If the user exists, send a login email. If the user does not exist, send a + signup invite email. + + Args: + email: Email address to send the email to. + request: HttpRequest object. + next_url: URL to redirect to after login/signup. Defaults to None. + + Returns: + None + """ + if user := get_user_by_email(email): + self.send_login_email(user) + return + + # No account found + if not settings.ENABLE_PUBLIC_SIGNUP: + self.send_email_no_account_found(email) + return + + # Self registration is enabled + signup_obj, _ = PendingSignup.objects.update_or_create( + email=email, + defaults={ + "token": get_random_string(32, "abcdefghijklmnopqrstuvwxyz0123456789") + }, + ) + self.send_new_account_login_email(signup_obj) + + return True diff --git a/hypha/apply/users/templates/elevate/elevate.html b/hypha/apply/users/templates/elevate/elevate.html index 9a55046959590ced3e9d0930aa3768f83282b2f9..1a5dbd29c5358dbad817da7c1bb589127abccff0 100644 --- a/hypha/apply/users/templates/elevate/elevate.html +++ b/hypha/apply/users/templates/elevate/elevate.html @@ -1,34 +1,73 @@ {% extends "base-apply.html" %} -{% load i18n wagtailcore_tags %} +{% load i18n wagtailcore_tags heroicons %} {% block title %}{% trans "Confirm access" %}{% endblock %} {% block body_class %}bg-white{% endblock %} {% block content %} - <div class="max-w-lg px-4 pt-4 mx-auto md:mt-5 md:py-4"> + <div class="max-w-md px-4 pt-4 mx-auto md:mt-5 md:py-4"> - <form class="form" method="post" action="./" class="px-4 pt-4"> - {% csrf_token %} - <h2 class="text-2xl">{% trans "Confirm access" %}</h2> + <h2 class="text-2xl text-center">{% trans "Confirm access" %}</h2> - <p class="px-3 py-2 bg-orange-100 rounded mb-4"> - Signed in as <strong>{{ request.user }} ({{ request.user.email }})</strong> - </p> + <p class="text-center mb-4"> + Signed in as <strong>{% if request.user.full_name %} {{ request.user.full_name }} ({{ request.user.email }}) {% else %}{{ request.user.email }} {% endif %}</strong> + </p> + + <section id="section-form"> + + {% if request.user.has_usable_password %} + <form + class="form form--error-inline mb-4 px-4 pt-4 border rounded-sm bg-gray-50" + method="post" + action="./" + data-test-id="section-password-input" + id="form-password-input" + > + {% for field in form %} + {% include "forms/includes/field.html" %} + {% endfor %} + + <div class="form__group"> + <button class="button button--primary" type="submit">{% trans "Confirm" %}</button> + </div> + </form> + {% else %} + <section data-test-id="section-confirm" id="confirm-code-input" class="mb-4 px-4 pt-4 text-center"> + + <button + class="button button--primary" + type="submit" + hx-post="{% url 'users:elevate_send_confirm_access_email' %}{% if request.GET.next %}?next={{request.GET.next}}{% endif %}" + hx-swap="outerHTML" + hx-target="#confirm-code-input" + > + {% trans "Send a confirmation code to your email" %} + </button> + </section> + {% endif %} - {% if form.non_field_errors %} - <div class="wrapper wrapper--error">{{ form.non_field_errors.as_text }}</div> + {% if request.user.has_usable_password %} + <section data-test-id="section-send-email" class="px-4 border pt-2 pb-4"> + <p>{% trans "Having problems?" %}</p> + <ul class="list-disc ml-4"> + <li> + <a + class="m-0" + type="submit" + hx-post="{% url 'users:elevate_send_confirm_access_email' %}{% if request.GET.next %}?next={{request.GET.next}}{% endif %}" + hx-target="#section-form" + > + {% trans "Send a confirmation code to your email" %} + </a> + </li> + </ul> + </section> {% endif %} - {% for field in form %} - {% include "forms/includes/field.html" %} - {% endfor %} + </section> - <div class="form__group"> - <button class="button button--primary" type="submit">{% trans "Confirm" %}</button> - </div> - </form> - <p class="text-xs text-center max-w-sm mt-8 text-gray-500 mx-auto"> + <p class="text-xs text-center max-w-xs mt-8 text-gray-500 mx-auto leading-relaxed"> {% blocktrans %} <strong>Tip:</strong> You are entering sudo mode. After you've performed a sudo-protected action, you'll only be asked to re-authenticate again after a few hours of inactivity. diff --git a/hypha/apply/users/templates/two_factor/_base_focus.html b/hypha/apply/users/templates/two_factor/_base_focus.html index 2a63a6db9b5c4f98a178ce0c12b6010155e33bf6..d53a7af9f9951f7d36cb0786b2ffd52c55e75a83 100644 --- a/hypha/apply/users/templates/two_factor/_base_focus.html +++ b/hypha/apply/users/templates/two_factor/_base_focus.html @@ -13,10 +13,12 @@ {% endslot %} {% comment %} {% slot sub_heading %}{% trans "All submissions ready for discussion." %}{% endslot %} {% endcomment %} - <a href="{% url 'dashboard:dashboard' %}" class="button button--primary button--arrow-pixels-white" hx-boost='true'> - {% trans "Go to my dashboard" %} - <svg><use xlink:href="#arrow-head-pixels--solid"></use></svg> - </a> + {% if user.can_access_dashboard %} + <a href="{% url 'dashboard:dashboard' %}" class="button button--primary button--arrow-pixels-white" hx-boost='true'> + {% trans "Go to my dashboard" %} + <svg><use xlink:href="#arrow-head-pixels--solid"></use></svg> + </a> + {% endif %} {% endadminbar %} <div class="wrapper wrapper--inner-space-medium max-w-2xl two-factor"> diff --git a/hypha/apply/users/templates/two_factor/_wizard_actions.html b/hypha/apply/users/templates/two_factor/_wizard_actions.html index eaff592606c70899396093c95d943ef8af1c4fd3..4931f35879c1668653f7b17930babe08a6097b24 100644 --- a/hypha/apply/users/templates/two_factor/_wizard_actions.html +++ b/hypha/apply/users/templates/two_factor/_wizard_actions.html @@ -1,7 +1,7 @@ {% load i18n %} {% if wizard.steps.current == 'token' %} - {% trans "Login" as button_text %} + {% trans "Submit" as button_text %} {% elif wizard.steps.current == 'generator' %} {% trans "Next" as button_text %} {% elif wizard.steps.current == 'welcome' %} @@ -10,12 +10,22 @@ {% trans "Next" as button_text %} {% endif %} -<button type="submit" class="button button--primary">{{ button_text }}</button> +<button + type="submit" + class="button button--primary mb-4" +> + {{ button_text }} +</button> <script> - var lbl = document.querySelector("label[for=id_generator-token]"); + const lbl = document.querySelector("label[for=id_generator-token]"); + const otpInput = document.querySelector("#id_token-otp_token"); if (lbl) { lbl.textContent = "{% trans "Verification code" %}:"; } + if(otpInput){ + // set max-width to 6 characters + otpInput.style.maxWidth = "10ch"; + } </script> diff --git a/hypha/apply/users/templates/two_factor/admin/disable.html b/hypha/apply/users/templates/two_factor/admin/disable.html index 5a86419b7f780fd8bbbb166f776df901a9e2b8b8..1e1a97e25430f7b557d2a38fa638b2645e8c349a 100644 --- a/hypha/apply/users/templates/two_factor/admin/disable.html +++ b/hypha/apply/users/templates/two_factor/admin/disable.html @@ -10,22 +10,21 @@ <form class="form" action="" method="POST" novalidate> <div class="tab-content"> {% csrf_token %} - <section id="account" class="active nice-padding"> - <p>{% trans "Are you sure you want to disable the Two Factor Authentication for this user? Please type your password to confirm." %}</p> + <p>{% trans "Are you sure you want to disable the Two Factor Authentication for this user?" %}</p> <ul class="fields"> {% block fields %} - {% include "wagtailadmin/shared/field_as_li.html" with field=form.password %} + {% include "wagtailadmin/shared/field_as_li.html" with field=form.confirmation_text %} {% endblock %} <li> - <button class="button button--primary" type="submit">{% trans 'Disable 2FA' %}</button> + <button class="button button--primary" type="submit">{% trans 'Confirm' %}</button> </li> </ul> + </section> </div> </form> - {% endblock %} diff --git a/hypha/apply/users/templates/two_factor/core/backup_tokens.html b/hypha/apply/users/templates/two_factor/core/backup_tokens.html index 297cd8503d7b596883c0986bce7de540be3f0348..ab0108d20862eca1f12acfa0039a777dbddbdcaf 100644 --- a/hypha/apply/users/templates/two_factor/core/backup_tokens.html +++ b/hypha/apply/users/templates/two_factor/core/backup_tokens.html @@ -14,7 +14,7 @@ cols="8" rows="{{ device.token_set.count }}" id="list-backup-tokens" - class="border" + class="font-mono pr-0 font-medium leading-tight bg-orange-100 resize-none" >{% for token in device.token_set.all %}{{ token.token }}{% if not forloop.last %}
{% endif %}{% endfor %}</textarea> <form method="post" class="actions actions-footer">{% csrf_token %}{{ form }} @@ -34,11 +34,14 @@ <p class="hide-print">{% blocktrans %}Once done, acknowledge you have stored the codes securely and then click "Finish".{% endblocktrans %}</p> <div class="form"> <ul class="errorlist hidden error-action-agree"><li>Please confirm you have stored the codes securely below.</li></ul> - <div class="form__group form__group--checkbox"> + <div class="form__item mb-4"> <input type="checkbox" id="action_agree" name="action_agree" value="action_agree"> - <label for="action_agree"> I have stored the backup codes securely.</label><br><br> + <label for="action_agree"> I have stored the backup codes securely.</label> </div> - <a class="btn btn-link btn-finish" href="{% url 'users:account' %}">{% trans "Finish" %}</a> + <div class="form__item"> + <a class="btn btn-link btn-finish" href="{% url 'users:account' %}">{% trans "Finish" %}</a> + </div> + </div> </form> {% else %} @@ -87,18 +90,21 @@ {# Instantiate clipboard by passing a HTML element, uses clipboard.js #} var clipboardBtn = document.querySelector('.btn-copy-to-clipboard'); - var clipboard = new ClipboardJS(clipboardBtn); - var tooltip = tippy(clipboardBtn, { - trigger: 'manual', - animation: 'fade' - }); + if (clipboardBtn) { + var clipboard = new ClipboardJS(clipboardBtn); + var tooltip = tippy(clipboardBtn, { + trigger: 'manual', + animation: 'fade' + }); + + clipboard.on('success', function (e) { + tooltip.show(); + }); + clipboard.on('error', function (e) { + tooltip.setContent("Use ctrl/cmd + C to copy the backup codes.") + tooltip.show(); + }); + } - clipboard.on('success', function (e) { - tooltip.show(); - }); - clipboard.on('error', function (e) { - tooltip.setContent("Use ctrl/cmd + C to copy the backup codes.") - tooltip.show(); - }); </script> {% endblock %} diff --git a/hypha/apply/users/templates/two_factor/core/setup.html b/hypha/apply/users/templates/two_factor/core/setup.html index 8d9066ab8774f8388d62bba6040cb47dc591c24a..6cadae9cae5e4d72689fdd4a0ff7a1594caa2d7c 100644 --- a/hypha/apply/users/templates/two_factor/core/setup.html +++ b/hypha/apply/users/templates/two_factor/core/setup.html @@ -52,7 +52,7 @@ {% include "two_factor/_wizard_forms.html" %} {# hidden submit button to enable [enter] key #} - <input type="submit" value="" class="d-none" /> + <input type="submit" value="" class="hidden" /> {% include "two_factor/_wizard_actions.html" %} </form> diff --git a/hypha/apply/users/templates/two_factor/core/setup_complete.html b/hypha/apply/users/templates/two_factor/core/setup_complete.html index 973817d57fa0db4815959aba1204496546b52515..627f4a458b13ed7ea79ba5bac6af5be15a88cc26 100644 --- a/hypha/apply/users/templates/two_factor/core/setup_complete.html +++ b/hypha/apply/users/templates/two_factor/core/setup_complete.html @@ -13,13 +13,13 @@ To get the backup codes you can continue to Show Codes.{% endblocktrans %}</p> {% if not phone_methods %} - <a href="{% url 'users:backup_tokens_password' %}" class="btn btn-link">{% trans "Show Codes" %}</a> + <a href="{% url 'users:backup_tokens' %}" class="btn btn-link">{% trans "Show Codes" %}</a> {% else %} <p>{% blocktrans trimmed %}However, it might happen that you don't have access to your primary token device. To enable account recovery, add a phone number.{% endblocktrans %}</p> - <p><a href="{% url 'users:backup_tokens_password' %}" class="btn btn-block">{% trans "Show Codes" %}</a></p> + <p><a href="{% url 'users:backup_tokens' %}" class="btn btn-block">{% trans "Show Codes" %}</a></p> <p><a href="{% url 'two_factor:phone_create' %}" class="btn btn-success">{% trans "Add Phone Number" %}</a></p> {% endif %} diff --git a/hypha/apply/users/templates/two_factor/core/two_factor_required.html b/hypha/apply/users/templates/two_factor/core/two_factor_required.html index ad0f7254cfd5dfae947dcfff4154a734a7a09605..d5cf73ae1f854559030c0773abed73333add3460 100644 --- a/hypha/apply/users/templates/two_factor/core/two_factor_required.html +++ b/hypha/apply/users/templates/two_factor/core/two_factor_required.html @@ -1,22 +1,38 @@ <!--Custom template to enforce 2FA and Copied from two_factor/core/otp_required.html--> - {% extends "two_factor/_base_focus.html" %} {% load i18n %} {% block content %} - <h1>{% block title %}{% trans "Permission Denied" %}{% endblock %}</h1> + <h1>{% block title %}{% trans "Permission Denied" %}: {{ reason }}{% endblock %}</h1> + + <div class="prose mb-4"> + <p> + {% blocktrans trimmed %} + The page you are trying to access requires users to verify their + identity using two-factor authentication for security reasons. + {% endblocktrans %} + </p> + + <p> + {% blocktrans trimmed %} + In order to access this page, you need to set up these security + features. Without setting them up, you will only be able to access + your account's profile section or log out from the system. + {% endblocktrans %} + </p> + + <p> + {% blocktrans trimmed %} + Two-factor authentication has not been set up + for your account yet. Please set it up to enhance + the security of your account. + {% endblocktrans %} + </p> + </div> - <p>{% blocktrans trimmed %}The page you requested, enforces users to verify using - two-factor authentication for security reasons. You need to set up these - security features in order to access this page. Without setting up these security features, - You can only access the account(Profile section) or can logout from the system.{% endblocktrans %}</p> - <p>{% blocktrans trimmed %}Two-factor authentication is not already set up for your - account. Set up two-factor authentication for enhanced account - security.{% endblocktrans %}</p> + <a href="{% url 'two_factor:setup' %}?next={{request.path}}" class="btn btn-primary"> + {% trans "Set up Two-Factor Authentication (2FA)" %} + </a> - <p> - <a href="{% url 'two_factor:setup' %}" class="btn btn-primary"> - {% trans "Set up Two-Factor Authentication (2FA)" %}</a> - </p> {% endblock %} diff --git a/hypha/apply/users/templates/two_factor/profile/disable.html b/hypha/apply/users/templates/two_factor/profile/disable.html index 683c8c3038ab994b56ae598d088a9e48ca79c191..b4dc0b43c91adc7701d8b77bb0f4f913bad60e6c 100644 --- a/hypha/apply/users/templates/two_factor/profile/disable.html +++ b/hypha/apply/users/templates/two_factor/profile/disable.html @@ -1,39 +1,40 @@ + {% extends "two_factor/_base_focus.html" %} {% load i18n %} {% block content %} - <p><a href="{% url 'users:account'%}" - class="btn btn-link">{% trans "Back to account" %}</a></p> <h1>{% block title %}{% trans "Disable Two-factor Authentication" %}{% endblock %}</h1> - <p>{% blocktrans trimmed %}Disabling Two-factor authentication weakens your account security. + <p class="mb-4">{% blocktrans trimmed %}Disabling Two-factor authentication weakens your account security. We recommend reenabling it when you can.{% endblocktrans %}</p> - <div class="wrapper wrapper--small wrapper--inner-space-medium"> - <form class="form" action="" method="POST" novalidate> - {% if form.non_field_errors %} - <ul class="errorlist"> - {% for error in form.non_field_errors %} - <li>{{ error }}</li> - {% endfor %} - </ul> - {% endif %} - - {% if form.errors %} - <ul class="errorlist"> - {% blocktrans trimmed count counter=form.errors.items|length %} - <li>Please correct the error below.</li> - {% plural %} - <li>Please correct the errors below.</li> - {% endblocktrans %} - </ul> - {% endif %} - - {% csrf_token %} - - {% for field in form %} - {% include "forms/includes/field.html" %} - {% endfor %} - - <button class="btn btn-danger" type="submit">{% trans 'Disable Two-factor Authentication' %}</button> - </form> - </div> + + <form class="form" action="" method="POST" novalidate> + {% if form.non_field_errors %} + <ul class="errorlist"> + {% for error in form.non_field_errors %} + <li>{{ error }}</li> + {% endfor %} + </ul> + {% endif %} + + {% if form.errors %} + <ul class="errorlist"> + {% blocktrans trimmed count counter=form.errors.items|length %} + <li>Please correct the error below.</li> + {% plural %} + <li>Please correct the errors below.</li> + {% endblocktrans %} + </ul> + {% endif %} + + {% csrf_token %} + + {% for field in form %} + {% include "forms/includes/field.html" %} + {% endfor %} + + <button class="btn btn-danger" type="submit"> + {% trans 'Confirm' %} + </button> + </form> + {% endblock %} diff --git a/hypha/apply/users/templates/two_factor/profile/profile.html b/hypha/apply/users/templates/two_factor/profile/profile.html index b5e73b325406dcb7aea85a353a9155bfed54a5a6..21a9b92f27d5312326142c79e3df2981c7399468 100644 --- a/hypha/apply/users/templates/two_factor/profile/profile.html +++ b/hypha/apply/users/templates/two_factor/profile/profile.html @@ -48,7 +48,7 @@ You have {{ counter }} backup tokens remaining. {% endblocktrans %} </p> - <p><a href="{% url 'users:backup_tokens_password' %}" + <p><a href="{% url 'users:backup_tokens' %}" class="btn btn-info">{% trans "Show Codes" %}</a></p> <h2>{% trans "Disable Two-Factor Authentication" %}</h2> diff --git a/hypha/apply/users/templates/users/account.html b/hypha/apply/users/templates/users/account.html index 12346f36c0d47ba705d27d8c49c788f36bed56c3..1e95d50328ecaa964f55b5e5456a729fe9860aa7 100644 --- a/hypha/apply/users/templates/users/account.html +++ b/hypha/apply/users/templates/users/account.html @@ -8,10 +8,12 @@ {% slot header %}{% trans "Welcome" %} {{ user }}{% endslot %} {% slot sub_heading %}{% trans "Manage your account details and security." %}{% endslot %} - <a href="{% url 'dashboard:dashboard' %}" class="button button--primary button--arrow-pixels-white" hx-boost='true'> - {% trans "Go to my dashboard" %} - <svg><use xlink:href="#arrow-head-pixels--solid"></use></svg> - </a> + {% if user.can_access_dashboard %} + <a href="{% url 'dashboard:dashboard' %}" class="button button--primary button--arrow-pixels-white" hx-boost='true'> + {% trans "Go to my dashboard" %} + <svg><use xlink:href="#arrow-head-pixels--solid"></use></svg> + </a> + {% endif %} {% endadminbar %} <div class="profile"> @@ -28,23 +30,38 @@ </form> </div> - {% if show_change_password and user.has_usable_password and not backends.associated %} - <div class="profile__column"> - <h2 class="text-2xl">{% trans "Account Security" %}</h2> - <h3 class="text-base">{% trans "Password" %}</h3> - <p><a class="button button--primary" href="{% url 'users:password_change' %}">{% trans "Update password" %}</a></p> - <h4 class="text-base mt-8">{% trans "Two-Factor Authentication (2FA)" %}</h4> - {% if default_device %} - <div> - <p><a class="button button--primary" href="{% url 'users:backup_tokens_password' %}">{% trans "Backup codes" %}</a></p> - <p><a class="button button--primary button--warning" href="{% url 'two_factor:disable' %}">{% trans "Disable 2FA" %}</a></p> - </div> - {% else %} - <p><a class="button button--primary" href="{% url 'two_factor:setup' %}">{% trans "Enable 2FA" %}</a></p> - {% endif %} - </div> - {% endif %} + <div class="profile__column"> + <h2 class="text-2xl">{% trans "Account Security" %}</h2> + + {% if show_change_password %} + <div class="block_manage_password mb-8"> + <h3 class="text-base mb-0">{% trans "Password" %}</h3> + <p> + {% if user.has_usable_password %} + <a class="button button--primary" href="{% url 'users:password_change' %}"> + {% trans "Update password" %} + </a> + {% else %} + <button class="button button--primary" + hx-post="{% url 'users:set_user_password' %}" + hx-swap="outerHTML" + > + {% trans "Set Password" %} + </button> + {% endif %} + </p> + </div> + {% endif %} + + <h3 class="text-base mb-2">{% trans "Two-Factor Authentication (2FA)" %}</h3> + {% if default_device %} + <a class="button button--primary mb-2" href="{% url 'users:backup_tokens' %}">{% trans "Backup codes" %}</a> + <a class="button button--primary button--warning mb-2" href="{% url 'two_factor:disable' %}">{% trans "Disable 2FA" %}</a> + {% else %} + <a class="button button--primary" href="{% url 'two_factor:setup' %}">{% trans "Enable 2FA" %}</a> + {% endif %} + </div> {% if swappable_form %} @@ -63,13 +80,13 @@ </form> {% endif %} - {# Remove the comment block tags below when such need arises. e.g. adding new providers #} - {% comment %} - {% can_use_oauth as show_oauth_link %} - {% if show_oauth_link %} - <a href="{% url 'users:oauth' %}">{% trans "Manage OAuth" %}</a> - {% endif %} - {% endcomment %} + {# Remove the comment block tags below when such need arises. e.g. adding new providers #} + {% comment %} + {% can_use_oauth as show_oauth_link %} + {% if show_oauth_link %} + <a href="{% url 'users:oauth' %}">{% trans "Manage OAuth" %}</a> + {% endif %} + {% endcomment %} </div> {% endif %} </div> diff --git a/hypha/apply/users/templates/users/activation/email_subject.txt b/hypha/apply/users/templates/users/activation/email_subject.txt new file mode 100644 index 0000000000000000000000000000000000000000..367b3ea742df0f1ed6cd65b4b11200dd0200c2ef --- /dev/null +++ b/hypha/apply/users/templates/users/activation/email_subject.txt @@ -0,0 +1,3 @@ +{% load i18n %}{% autoescape off %} +{% blocktranslate %}Account details for {{ username }} at {{ org_long_name }}{% endblocktranslate %} +{% endautoescape %} diff --git a/hypha/apply/users/templates/users/activation/invalid.html b/hypha/apply/users/templates/users/activation/invalid.html index b6dc037fd6cbd9c33a8cdd5522b7cd24020eff66..1457244442cf9a197bd2e2b2d9f7c7543e67506d 100644 --- a/hypha/apply/users/templates/users/activation/invalid.html +++ b/hypha/apply/users/templates/users/activation/invalid.html @@ -1,18 +1,25 @@ -{% extends 'base.html' %} -{% load i18n %} +{% extends "base-apply.html" %} +{% load i18n heroicons %} {% block title %}{% trans "Invalid activation" %}{% endblock %} -{% block page_title %}{% trans "Invalid activation URL" %}{% endblock %} +{% block body_class %}bg-white{% endblock %} {% block content %} - {% url 'users:password_reset' as password_reset %} - <div class="wrapper wrapper--small wrapper--bottom-space"> - <p><strong>{% trans "Two possible reasons:" %}</strong></p> - <ul> - <li>{% trans "The activation link has expired." %}</li> - <li>{% trans "The account has already been activated." %}</li> - </ul> + <div class="w-full bg-white mt-5 md:py-4"> - <p>{% blocktrans %}First try to <a href="{{ password_reset }}">reset your password</a>. If that fails please contact {{ ORG_SHORT_NAME }} at{% endblocktrans %} <a href="mailto:{{ ORG_EMAIL }}">{{ ORG_EMAIL }}</a></p> - </div> + <section class="max-w-2xl"> + {% heroicon_outline "exclamation-triangle" aria_hidden="true" size=64 class="stroke-red-600" %} + + <h2 class="text-2xl">{% trans "Invalid activation URL" %}</h2> + {% url 'users:password_reset' as password_reset %} + <div class="wrapper wrapper--small wrapper--bottom-space"> + <p><strong>{% trans "Two possible reasons:" %}</strong></p> + <ol class="list-decimal pl-6"> + <li>{% trans "The activation link has expired." %}</li> + <li>{% trans "The account has already been activated." %}</li> + </ol> + + <p>{% blocktrans %}First try to <a href="{{ password_reset }}">reset your password</a>. If that fails please contact {{ ORG_SHORT_NAME }} at{% endblocktrans %} <a href="mailto:{{ ORG_EMAIL }}">{{ ORG_EMAIL }}</a></p> + </section> + </div> {% endblock %} diff --git a/hypha/apply/users/templates/users/email_change/confirm_password.html b/hypha/apply/users/templates/users/email_change/confirm_password.html deleted file mode 100644 index 5b5b638acbcacf8ac9383593c66d99c898b7c8a3..0000000000000000000000000000000000000000 --- a/hypha/apply/users/templates/users/email_change/confirm_password.html +++ /dev/null @@ -1,40 +0,0 @@ -{% extends 'base.html' %} -{% load i18n %} -{% block header_modifier %}header--light-bg{% endblock %} -{% block page_title %}{% trans "Enter Password" %}{% endblock %} -{% block title %}{% trans "Enter Password" %}{% endblock %} - - -{% block content %} - <div class="wrapper wrapper--small wrapper--inner-space-medium"> - <form class="form" action="" method="POST" novalidate> - {% if form.non_field_errors %} - <ul class="errorlist"> - {% for error in form.non_field_errors %} - <li>{{ error }}</li> - {% endfor %} - </ul> - {% endif %} - - {% if form.errors %} - <ul class="errorlist"> - {% blocktrans trimmed count counter=form.errors.items|length %} - <li>Please correct the error below.</li> - {% plural %} - <li>Please correct the errors below.</li> - {% endblocktrans %} - </ul> - {% endif %} - - {% csrf_token %} - - {% for field in form %} - {% include "forms/includes/field.html" %} - {% endfor %} - - <div class="form__group"> - <button class="button button--primary" type="submit">{% trans 'Submit' %}</button> - </div> - </form> - </div> -{% endblock %} diff --git a/hypha/apply/users/templates/users/email_change/done.html b/hypha/apply/users/templates/users/email_change/done.html index 448999f44acd2263458bddf4bd395cc18d72a808..fcf6194f7d99a48b74807fef036e626f90c186a6 100644 --- a/hypha/apply/users/templates/users/email_change/done.html +++ b/hypha/apply/users/templates/users/email_change/done.html @@ -1,11 +1,21 @@ -{% extends "base.html" %} +{% extends "base-apply.html" %} {% load i18n %} -{% block header_modifier %}header--light-bg{% endblock %} -{% block page_title %}{% trans "Check your email" %}{% endblock %} +{% block page_title %}{% trans "Email Change - Verify Email" %}{% endblock %} {% block title %}{% trans "Verify Email" %}{% endblock %} {% block content %} - <div class="wrapper wrapper--small wrapper--bottom-space"> - <p>{% trans "To start using the new email, please click on the confirmation link that has been sent to you on your new email." %}</p> + + {% adminbar %} + {% slot header %}{% trans "Email Update" %}{% endslot %} + {% endadminbar %} + + <div class="wrapper mt-6 prose"> + <h2>{% trans "Confirm & verify your new email!" %} </h2> + <p> + {% trans "We have sent a confirmation link to your new email." %} + </p> + <p> + {% trans "To start using the new email, please click on the confirmation link that has been sent to you on your new email." %} + </p> </div> {% endblock %} diff --git a/hypha/apply/users/templates/users/emails/confirm_access.md b/hypha/apply/users/templates/users/emails/confirm_access.md new file mode 100644 index 0000000000000000000000000000000000000000..f9ee524e436afbca82a1d4f7b9f0757c9d0a1d3f --- /dev/null +++ b/hypha/apply/users/templates/users/emails/confirm_access.md @@ -0,0 +1,19 @@ +{% load i18n wagtailadmin_tags %}{% base_url_setting as base_url %} +{% blocktrans %}Dear {{ user }},{% endblocktrans %} + +{% blocktrans %}To confirm access at {{ org_long_name }} use the code below (valid for {{ timeout_minutes }} minutes):{% endblocktrans %} + +{{ token }} + +{% blocktrans %}If you did not request this email, please ignore it.{% endblocktrans %} + +{% if org_email %} +{% blocktrans %}If you have any questions, please contact us at {{ org_email }}.{% endblocktrans %} +{% endif %} + +{% blocktrans %}Kind Regards, +The {{ org_short_name }} Team{% endblocktrans %} + +-- +{{ org_long_name }} +{% if site %}{{ site.root_url }}{% else %}{{ base_url }}{% endif %} diff --git a/hypha/apply/users/templates/users/emails/passwordless_login_email.md b/hypha/apply/users/templates/users/emails/passwordless_login_email.md new file mode 100644 index 0000000000000000000000000000000000000000..9ebb66c1239049c64c8f6775e7b3880b1403c54c --- /dev/null +++ b/hypha/apply/users/templates/users/emails/passwordless_login_email.md @@ -0,0 +1,26 @@ +{% load i18n wagtailadmin_tags %}{% base_url_setting as base_url %}{% firstof name username as user %} +{% blocktrans %}Dear {{ user }},{% endblocktrans %} + +{% if is_active %} +{% blocktrans %}Login to your account on the {{ org_long_name }} web site by clicking this link or copying and pasting it to your browser:{% endblocktrans %} + +{% if site %}{{ site.root_url }}{% else %}{{ base_url }}{% endif %}{{ login_path }} + +{% blocktrans %}This link will valid for {{ timeout_minutes }} minutes and can be used only once.{% endblocktrans %} + +{% else %} +{% blocktrans %}Your account on the {{ org_long_name }} web site is deactivated. Please contact site administrators.{% endblocktrans %} +{% endif %} + +{% blocktrans %}If you did not request this email, please ignore it.{% endblocktrans %} + +{% if org_email %} +{% blocktrans %}If you have any questions, please contact us at {{ org_email }}.{% endblocktrans %} +{% endif %} + +{% blocktrans %}Kind Regards, +The {{ org_short_name }} Team{% endblocktrans %} + +-- +{{ org_long_name }} +{% if site %}{{ site.root_url }}{% else %}{{ base_url }}{% endif %} diff --git a/hypha/apply/users/templates/users/emails/passwordless_login_no_account_found.md b/hypha/apply/users/templates/users/emails/passwordless_login_no_account_found.md new file mode 100644 index 0000000000000000000000000000000000000000..9f9cea09efc7e17da8f00933bddf8202b8018f3b --- /dev/null +++ b/hypha/apply/users/templates/users/emails/passwordless_login_no_account_found.md @@ -0,0 +1,16 @@ +{% load i18n wagtailadmin_tags %}{% base_url_setting as base_url %} + +{% blocktrans %}Dear,{% endblocktrans %} + +{% blocktrans %}It looks like you are trying to login on {{ org_long_name }} web site, but we could not find any account with the email provided.{% endblocktrans %} + +{% if org_email %} +{% blocktrans %}If you have any questions, please contact us at {{ org_email }}.{% endblocktrans %} +{% endif %} + +{% blocktrans %}Kind Regards, +The {{ org_short_name }} Team{% endblocktrans %} + +-- +{{ org_long_name }} +{% if site %}{{ site.root_url }}{% else %}{{ base_url }}{% endif %} diff --git a/hypha/apply/users/templates/users/emails/passwordless_new_account_login.md b/hypha/apply/users/templates/users/emails/passwordless_new_account_login.md new file mode 100644 index 0000000000000000000000000000000000000000..ab23da16a81d6029fddbaead4f700e67e02623b7 --- /dev/null +++ b/hypha/apply/users/templates/users/emails/passwordless_new_account_login.md @@ -0,0 +1,21 @@ +{% load i18n wagtailadmin_tags %}{% base_url_setting as base_url %} +{% blocktrans %}Dear,{% endblocktrans %} + +{% blocktrans %}Welcome to {{ org_long_name }} web site. Create your account by clicking this link or copying and pasting it to your browser:{% endblocktrans %} + +{% if site %}{{ site.root_url }}{% else %}{{ base_url }}{% endif %}{{ signup_path }} + +{% blocktrans %}This link will valid for {{ timeout_minutes }} minutes and can be used only once.{% endblocktrans %} + +{% blocktrans %}If you did not request this email, please ignore it.{% endblocktrans %} + +{% if org_email %} +{% blocktrans %}If you have any questions, please contact us at {{ org_email }}.{% endblocktrans %} +{% endif %} + +{% blocktrans %}Kind Regards, +The {{ org_short_name }} Team{% endblocktrans %} + +-- +{{ org_long_name }} +{% if site %}{{ site.root_url }}{% else %}{{ base_url }}{% endif %} diff --git a/hypha/apply/users/templates/users/emails/set_password.txt b/hypha/apply/users/templates/users/emails/set_password.txt new file mode 100644 index 0000000000000000000000000000000000000000..a5c66b62cfe42b7d71ff20dfd087bebaf8a360f9 --- /dev/null +++ b/hypha/apply/users/templates/users/emails/set_password.txt @@ -0,0 +1,15 @@ +{% load i18n wagtailadmin_tags %}{% base_url_setting as base_url %}{% firstof name username as user %} +{% blocktrans %}Dear {{ user }},{% endblocktrans %} + +{% blocktrans %}Set your account password on the {{ org_long_name }} web site by clicking this link or copying and pasting it to your browser:{% endblocktrans %} + +{% if site %}{{ site.root_url }}{% else %}{{ base_url }}{% endif %}{{ activation_path }} + +{% blocktrans %}This link can be used only once and will lead you to a page where you can set your password. It will remain active for {{ timeout_days }} days, so please set your password as soon as possible.{% endblocktrans %} + +{% blocktrans %}Kind Regards, +The {{ org_short_name }} Team{% endblocktrans %} + +-- +{{ org_long_name }} +{% if site %}{{ site.root_url }}{% else %}{{ base_url }}{% endif %} diff --git a/hypha/apply/users/templates/users/emails/set_password_subject.txt b/hypha/apply/users/templates/users/emails/set_password_subject.txt new file mode 100644 index 0000000000000000000000000000000000000000..ac5b535ead2106a5f0f52654d08c7c664879d9cd --- /dev/null +++ b/hypha/apply/users/templates/users/emails/set_password_subject.txt @@ -0,0 +1,3 @@ +{% load i18n %}{% autoescape off %} +{% blocktranslate %}Set password for {{ username }} at {{ org_long_name }}{% endblocktranslate %} +{% endautoescape %} diff --git a/hypha/apply/users/templates/users/login.html b/hypha/apply/users/templates/users/login.html index ab5154f588e5752417c054591d395864625eb119..37815bf1c946c5109372dc02775b8a17ca68c6f3 100644 --- a/hypha/apply/users/templates/users/login.html +++ b/hypha/apply/users/templates/users/login.html @@ -31,18 +31,36 @@ {% if wizard.steps.current == 'auth' %} + <style> + .id_auth-password { + margin-bottom: 0.25rem; + } + </style> + <h2 class="text-2xl">Log in to {{ ORG_SHORT_NAME }}</h2> {% for field in form %} - <div class="form__group"> + <div class="relative max-w-sm {% if field.auto_id == "id_auth-password" %}mb-4{% endif %}"> {% include "forms/includes/field.html" %} + {% if field.auto_id == "id_auth-password" %} + <div class="text-right"> + <a class="link text-sm hover:opacity-75" href="{% url 'users:password_reset' %}{% if redirect_url %}?next={{ redirect_url }}{% endif %}" hx-boost="true">{% trans "Forgot your password?" %}</a> + </div> + {% endif %} </div> {% endfor %} + {% if settings.users.AuthSettings.extra_text %} - {{ settings.users.AuthSettings.extra_text|richtext}} + <div class="prose prose-sm mb-6 rounded-sm bg-slate-50 p-4"> + {{ settings.users.AuthSettings.extra_text|richtext}} + </div> {% endif %} - <div class="form__group"> + <div class="form__group max-w-sm flex items-center justify-between gap-4"> <button class="link link--button link--button-secondary" type="submit">{% trans "Log in" %}</button> + + {% if ENABLE_PUBLIC_SIGNUP %} + <a class="hover:opacity-75" href="{% url 'users_public:register' %}{% if redirect_url %}?next={{ redirect_url }}{% endif %}" hx-boost="true"> {% trans "Create account" %}</a> + {% endif %} </div> {% if GOOGLE_OAUTH2 %} @@ -56,20 +74,13 @@ <a class="link link--button link--button-tertiary" href="{% url "social:begin" "google-oauth2" %}{% if next %}?next={{ next }}{% endif %}">{% blocktrans %}Log in with your {{ ORG_SHORT_NAME }} email{% endblocktrans %}</a> </div> {% endif %} - - <div class="inline-flex gap-8 w-full mt-4"> - {% if ENABLE_REGISTRATION_WITHOUT_APPLICATION %} - <a href="{% url 'users_public:register' %}{% if redirect_url %}?next={{ redirect_url }}{% endif %}" hx-boost="true"> {% trans "Create account" %}</a> - {% endif %} - <a class="link" href="{% url 'users:password_reset' %}{% if redirect_url %}?next={{ redirect_url }}{% endif %}" hx-boost="true">{% trans "Forgot your password?" %}</a> - </div> {% else %} <div class="form__group"> {{ wizard.form }} </div> - {# hidden submit button to enable [enter] key #} + {# hidden submit button to enable [enter] key #} <div class="sr-only"><input type="submit" value=""/></div> {% if other_devices %} @@ -90,7 +101,7 @@ {% if backup_tokens %} <p>{% trans "As a last resort, you can use a backup codes:" %} <button name="wizard_goto_step" type="submit" value="backup" - class="link link--button link--button-tertiary">{% trans "Use Backup Code" %}</button> + class="button button--transparent">{% trans "Use Backup Code" %}</button> </p> {% endif %} {% endif %} diff --git a/hypha/apply/users/templates/users/partials/confirmation_code_sent.html b/hypha/apply/users/templates/users/partials/confirmation_code_sent.html new file mode 100644 index 0000000000000000000000000000000000000000..c3a7f8feddf4826289f8bb3cde1219ba756a9639 --- /dev/null +++ b/hypha/apply/users/templates/users/partials/confirmation_code_sent.html @@ -0,0 +1,74 @@ +{% load i18n heroicons %} +<form + class="form form--error-inline px-4 py-4 mb-4 border rounded-sm bg-gray-50 w-full text-center" + id="elevate-check-code-form" + x-data="{ code: '' }" +> + {% csrf_token %} + {% if error %} + <p class="mb-4 font-bold text-red-700">{% trans "Invalid code, please try again!" %}</p> + {% else %} + <p class="mb-4"> + {% heroicon_mini "check-circle" class="inline align-text-bottom fill-green-700" aria_hidden=true %} + <em>{% trans "An email containing a code has been sent. Please check your email for the code." %}</em> + </p> + {% endif %} + + <div class="mb-4"> + <label class="font-bold mr-1" for="id_code">{% trans "Enter Code" %}: </label> + <input + name='code' + id="id_code" + autofocus + required + type='text' + maxlength='6' + class="mb-2 !w-28 placeholder:text-gray-400 text-center tracking-wider" + x-model="code" + autocomplete="off" + placeholder="_ _ _ _ _ _" + data-1p-ignore + > + </div> + + <div> + <button + class="button button-primary block mb-4" + type="submit" + hx-post="{% url 'users:elevate_check_code' %}{% if redirect_url %}?next={{ redirect_url }}{% endif %}" + hx-validate="true" + hx-target="#section-form" + x-bind:disabled="code ? false : true" + > + {% trans "Confirm" %} + </button> + </div> + {% if error %} + <button + class="link hover:underline" + hx-post="{% url 'users:elevate_send_confirm_access_email' %}{% if request.GET.next %}?next={{request.GET.next}}{% endif %}" + hx-target="#section-form" + > + {% trans "Re-send code?" %} + </button> + {% endif %} +</form> + +{% if request.user.has_usable_password %} + <section data-test-id="section-send-email" class="px-4 border pt-2 pb-4"> + <p>{% trans "Having problems?" %}</p> + <ul class="list-disc ml-4"> + <li> + <a + class="m-0" + type="submit" + hx-boost="true" + href="{% url 'users:elevate' %}{% if redirect_url %}?next={{ redirect_url }}{% endif %}" + > + {% trans "Use your password" %} + </a> + </li> + </ul> + </section> +{% endif %} + diff --git a/hypha/apply/users/templates/users/partials/passwordless_login_signup_sent.html b/hypha/apply/users/templates/users/partials/passwordless_login_signup_sent.html new file mode 100644 index 0000000000000000000000000000000000000000..62a07ec4299d848c42b834ee948c056826d4fd8c --- /dev/null +++ b/hypha/apply/users/templates/users/partials/passwordless_login_signup_sent.html @@ -0,0 +1,29 @@ +{% extends base_template %} +{% load i18n heroicons %} + +{% block content %} + <section class="prose mt-8"> + <div> + {% heroicon_outline "document-check" aria_hidden="true" size=64 %} + </div> + <h2 class="mt-4"> + {% trans "Check your inbox to proceed!" %} + </h2> + + <p> + {% if ENABLE_PUBLIC_SIGNUP %} + {% trans "We have sent you an email containing a link for logging in or signing up. Please check your email and use the link provided to either login or create your account." %}</p> + {% else %} + {% trans "We've sent you an email with a login link. Kindly check your email and follow the link to access your account." %}</p> + {% endif %} + </p> + + <p> + {% blocktrans %}Check your "Spam" folder, if you don't find the email in your inbox.{% endblocktrans %} + </p> + + <p> + <a href="{% url 'users_public:passwordless_login_signup' %}{% if redirect_url %}?next={{ redirect_url }}{% endif %}" class="font-medium hover:underline">Try again</a> + </p> + </section> +{% endblock content %} diff --git a/hypha/apply/users/templates/users/passwordless_login_signup.html b/hypha/apply/users/templates/users/passwordless_login_signup.html new file mode 100644 index 0000000000000000000000000000000000000000..d837caf90f50a786d2149d69991b4dc771dfa7b0 --- /dev/null +++ b/hypha/apply/users/templates/users/passwordless_login_signup.html @@ -0,0 +1,69 @@ +{% extends base_template %} +{% load i18n wagtailcore_tags heroicons %} + +{% block title %}{% trans "Login or Signup" %}{% endblock %} + +{% block content %} + <div class="max-w-2xl bg-white mt-5 md:py-4"> + + <section class="pt-4 px-5"> + <form class="form form--user-login" method="post" hx-post="./" hx-swap="outerHTML" hx-target="#main"> + {% csrf_token %} + + {% if redirect_url %} + <input type="hidden" name="next" value="{{ redirect_url }}"> + {% endif %} + + <h2 class="text-2xl">Login {% if ENABLE_PUBLIC_SIGNUP %}or Signup {% endif %}to {{ ORG_SHORT_NAME }}</h2> + + <div> + {% for hidden in form.hidden_fields %} + {{ hidden }} + {% endfor %} + {% for field in form.visible_fields %} + {% if field.field %} + {% include "forms/includes/field.html" %} + {% else %} + {{ field }} + {% endif %} + {% endfor %} + </div> + + {% if settings.users.AuthSettings.extra_text %} + <div class="prose prose-sm mb-6 p-4 rounded-sm bg-slate-50"> + {{ settings.users.AuthSettings.extra_text|richtext}} + </div> + {% endif %} + + <div class="form__group"> + <button class="link link--button link--button-secondary" type="submit">{% trans "Next" %}</button> + </div> + + <div class="flex items-center justify-start relative mb-4"> + <hr class="inline w-32 h-px my-6 bg-gray-300 border-0"> + <span class="px-3 text-gray-400 font-medium">or</span> + <hr class="inline w-32 h-px my-6 bg-gray-300 border-0"> + </div> + + <section> + {% if GOOGLE_OAUTH2 %} + <a + class="link link--button link--button-tertiary" + href="{% url "social:begin" "google-oauth2" %}{% if next %}?next={{ next }}{% endif %}" + > + {% blocktrans %}Log in with your {{ ORG_SHORT_NAME }} email{% endblocktrans %} + </a> + {% endif %} + + <a + class="link link--button link--button-tertiary" + href="{% url 'users_public:login' %}{% if next %}?next={{next}}{% endif %}" + > + {% heroicon_mini "key" size=18 class="inline align-text-bottom mr-1" aria_hidden=true %} + {% trans "Login with Password" %} + </a> + </section> + </form> + </section> + </div> +{% endblock %} diff --git a/hypha/apply/users/templates/wagtailusers/groups/index.html b/hypha/apply/users/templates/wagtailusers/groups/index.html new file mode 100644 index 0000000000000000000000000000000000000000..12256926fcbc12eda9a6f413bec2772a024ba79c --- /dev/null +++ b/hypha/apply/users/templates/wagtailusers/groups/index.html @@ -0,0 +1,8 @@ +{% extends "wagtailadmin/generic/index.html" %} +{% load i18n static %} + +{% block extra_css %} + <link rel="stylesheet" href="{% static 'css/apply/wagtail_groups_list.css' %}"> + {{ block.super }} + {{ media.css }} +{% endblock %} \ No newline at end of file diff --git a/hypha/apply/users/templates/wagtailusers/users/create.html b/hypha/apply/users/templates/wagtailusers/users/create.html index cdc722089f7d52892f772698ab734e8b0f9d403e..2c91a52a01634e97c43622828857da7ad68bbad4 100644 --- a/hypha/apply/users/templates/wagtailusers/users/create.html +++ b/hypha/apply/users/templates/wagtailusers/users/create.html @@ -1,4 +1,5 @@ {% extends "wagtailusers/users/create.html" %} +{% load static %} {% block fields %} {% if form.separate_username_field %} @@ -24,3 +25,7 @@ {% include "wagtailadmin/shared/field_as_li.html" with field=form.is_active %} {% endif %} {% endblock fields %} + +{% block extra_css %} + <link rel="stylesheet" href="{% static 'css/apply/wagtail_groups_list.css' %}"> +{% endblock %} diff --git a/hypha/apply/users/templates/wagtailusers/users/edit.html b/hypha/apply/users/templates/wagtailusers/users/edit.html index ab4f80297d8bc430908ca169d738db12674ab601..b4d72a1f076e23c94fb11f1f5bb1a3ddc9001f30 100644 --- a/hypha/apply/users/templates/wagtailusers/users/edit.html +++ b/hypha/apply/users/templates/wagtailusers/users/edit.html @@ -2,6 +2,7 @@ {% extends "wagtailusers/users/edit.html" %} {% load wagtailimages_tags %} {% load users_tags %} +{% load static %} {% load i18n %} {% block content %} @@ -106,6 +107,7 @@ {% endblock %} {% block extra_css %} + <link rel="stylesheet" href="{% static 'css/apply/wagtail_groups_list.css' %}"> {{ block.super }} {{ form.media.css }} {% endblock %} diff --git a/hypha/apply/users/tests/test_forms.py b/hypha/apply/users/tests/test_forms.py index dc7fd7d6f958e6bb141074b3b6c1cc631d4baa9d..1b3c5f35c575fe1bf2128fc3de6e8524682a7fb3 100644 --- a/hypha/apply/users/tests/test_forms.py +++ b/hypha/apply/users/tests/test_forms.py @@ -1,5 +1,5 @@ from django.forms.models import model_to_dict -from django.test import TestCase +from django.test import RequestFactory, TestCase from ..forms import EmailChangePasswordForm, ProfileForm from .factories import StaffFactory, UserFactory @@ -12,9 +12,11 @@ class BaseTestProfileForm(TestCase): data.update(**values) return data - def submit_form(self, instance, **extra_data): + def submit_form(self, instance, request=None, **extra_data): form = ProfileForm( - instance=instance, data=self.form_data(instance, **extra_data) + instance=instance, + data=self.form_data(instance, **extra_data), + request=request, ) if form.is_valid(): form.save() @@ -28,7 +30,7 @@ class TestProfileForm(BaseTestProfileForm): def test_email_unique(self): other_user = UserFactory() - form = self.submit_form(self.user, email=other_user.email) + form = self.submit_form(instance=self.user, email=other_user.email) # form will update the other user's email with same user email, only non exiting email address can be added self.assertTrue(form.is_valid()) self.user.refresh_from_db() @@ -36,13 +38,13 @@ class TestProfileForm(BaseTestProfileForm): def test_can_change_email(self): new_email = "me@another.com" - self.submit_form(self.user, email=new_email) + self.submit_form(instance=self.user, email=new_email) self.user.refresh_from_db() self.assertEqual(self.user.email, new_email) def test_cant_set_slack_name(self): slack_name = "@foobar" - self.submit_form(self.user, slack=slack_name) + self.submit_form(instance=self.user, slack=slack_name) self.user.refresh_from_db() self.assertNotEqual(self.user.slack, slack_name) @@ -51,29 +53,33 @@ class TestStaffProfileForm(BaseTestProfileForm): def setUp(self): self.staff = StaffFactory() - def test_cant_change_email(self): + def test_cant_change_email_oauth(self): new_email = "me@this.com" - self.submit_form(self.staff, email=new_email) + request = RequestFactory().get("/") + request.session = { + "_auth_user_backend": "social_core.backends.google.GoogleOAuth2" + } + self.submit_form(instance=self.staff, request=request, email=new_email) self.staff.refresh_from_db() self.assertNotEqual(new_email, self.staff.email) def test_can_set_slack_name(self): slack_name = "@foobar" - self.submit_form(self.staff, slack=slack_name) + self.submit_form(instance=self.staff, slack=slack_name) self.staff.refresh_from_db() self.assertEqual(self.staff.slack, slack_name) def test_can_set_slack_name_with_trailing_space(self): slack_name = "@foobar" - self.submit_form(self.staff, slack=slack_name) + self.submit_form(instance=self.staff, slack=slack_name) self.staff.refresh_from_db() self.assertEqual(self.staff.slack, slack_name) def test_cant_set_slack_name_with_space(self): slack_name = "@ foobar" - form = self.submit_form(self.staff, slack=slack_name) + form = self.submit_form(instance=self.staff, slack=slack_name) self.assertFalse(form.is_valid()) self.staff.refresh_from_db() @@ -81,14 +87,14 @@ class TestStaffProfileForm(BaseTestProfileForm): def test_auto_prepend_at(self): slack_name = "foobar" - self.submit_form(self.staff, slack=slack_name) + self.submit_form(instance=self.staff, slack=slack_name) self.staff.refresh_from_db() self.assertEqual(self.staff.slack, "@" + slack_name) def test_can_clear_slack_name(self): slack_name = "" - self.submit_form(self.staff, slack=slack_name) + self.submit_form(instance=self.staff, slack=slack_name) self.staff.refresh_from_db() self.assertEqual(self.staff.slack, slack_name) diff --git a/hypha/apply/users/tests/test_middleware.py b/hypha/apply/users/tests/test_middleware.py index a5de674bb4cd1480baf8ee7e182822f6517bd96b..d446661b641bcb19c3930bb2c366cf15f3f39bf8 100644 --- a/hypha/apply/users/tests/test_middleware.py +++ b/hypha/apply/users/tests/test_middleware.py @@ -4,7 +4,7 @@ from django.urls import reverse from hypha.apply.users.tests.factories import UserFactory -from ..middleware import ALLOWED_SUBPATH_FOR_UNVERIFIED_USERS +from ..middleware import TWO_FACTOR_EXEMPTED_PATH_PREFIXES @override_settings(ROOT_URLCONF="hypha.apply.urls", ENFORCE_TWO_FACTOR=True) @@ -17,14 +17,10 @@ class TestTwoFactorAuthenticationMiddleware(TestCase): self.client.force_login(user) response = self.client.get(settings.LOGIN_REDIRECT_URL, follow=True) - self.assertRedirects( - response, reverse("users:two_factor_required"), status_code=301 - ) + assert "Permission Denied" in response.content.decode("utf-8") response = self.client.get(reverse("funds:submissions:list"), follow=True) - self.assertRedirects( - response, reverse("users:two_factor_required"), status_code=301 - ) + assert "Permission Denied" in response.content.decode("utf-8") def test_verified_user_redirect(self): user = UserFactory() @@ -40,6 +36,6 @@ class TestTwoFactorAuthenticationMiddleware(TestCase): user = UserFactory() self.client.force_login(user) - for path in ALLOWED_SUBPATH_FOR_UNVERIFIED_USERS: + for path in TWO_FACTOR_EXEMPTED_PATH_PREFIXES: response = self.client.get(path, follow=True) self.assertEqual(response.status_code, 200) diff --git a/hypha/apply/users/tests/test_oauth_access.py b/hypha/apply/users/tests/test_oauth_access.py index 124d211b32e441247ce38595e8ba23c463cf2f34..8c7a9244de1b6b9a744647eea0c3bfdc83e8fcb5 100644 --- a/hypha/apply/users/tests/test_oauth_access.py +++ b/hypha/apply/users/tests/test_oauth_access.py @@ -22,7 +22,7 @@ class TestOAuthAccess(TestCase): response = self.client.get(oauth_page, follow=True) self.assertRedirects( response, - reverse("users_public:login") + "?next=" + reverse("users:oauth"), + reverse(settings.LOGIN_URL) + "?next=" + reverse("users:oauth"), status_code=301, target_status_code=200, ) diff --git a/hypha/apply/users/tests/test_registration.py b/hypha/apply/users/tests/test_registration.py index ea0019c6b6967b4c223064cae1d58d55ca3b2a47..6f654e96d310bb52f5bfb6da6aa4f90184df38bc 100644 --- a/hypha/apply/users/tests/test_registration.py +++ b/hypha/apply/users/tests/test_registration.py @@ -1,3 +1,4 @@ +from django.conf import settings from django.core import mail from django.test import TestCase, override_settings from django.urls import reverse @@ -8,17 +9,17 @@ from hypha.apply.utils.testing import make_request @override_settings(ROOT_URLCONF="hypha.apply.urls") class TestRegistration(TestCase): - @override_settings(ENABLE_REGISTRATION_WITHOUT_APPLICATION=False) + @override_settings(ENABLE_PUBLIC_SIGNUP=False) def test_registration_enabled_has_no_link(self): response = self.client.get("/", follow=True) self.assertNotContains(response, reverse("users_public:register")) - @override_settings(ENABLE_REGISTRATION_WITHOUT_APPLICATION=True) + @override_settings(ENABLE_PUBLIC_SIGNUP=True) def test_registration_enabled_has_link(self): response = self.client.get("/", follow=True) self.assertContains(response, reverse("users_public:register")) - @override_settings(ENABLE_REGISTRATION_WITHOUT_APPLICATION=True) + @override_settings(ENABLE_PUBLIC_SIGNUP=True) def test_registration(self): response = self.client.post( reverse("users_public:register"), @@ -35,7 +36,7 @@ class TestRegistration(TestCase): assert response.status_code == 302 assert reverse("users_public:register-success") in response.url - @override_settings(ENABLE_REGISTRATION_WITHOUT_APPLICATION=True) + @override_settings(ENABLE_PUBLIC_SIGNUP=True) def test_duplicate_registration_fails(self): response = self.client.post( reverse("users_public:register"), @@ -61,13 +62,11 @@ class TestRegistration(TestCase): assert len(mail.outbox) == 0 self.assertContains(response, "A user with that email already exists") - @override_settings( - FORCE_LOGIN_FOR_APPLICATION=True, ENABLE_REGISTRATION_WITHOUT_APPLICATION=False - ) + @override_settings(FORCE_LOGIN_FOR_APPLICATION=True, ENABLE_PUBLIC_SIGNUP=False) def test_force_login(self): fund = FundTypeFactory() response = fund.serve( make_request(None, {}, method="get", site=fund.get_site()) ) assert response.status_code == 302 - assert response.url == reverse("users_public:login") + "?next=/" + assert response.url == reverse(settings.LOGIN_URL) + "?next=/" diff --git a/hypha/apply/users/tests/test_tokens.py b/hypha/apply/users/tests/test_tokens.py new file mode 100644 index 0000000000000000000000000000000000000000..8a406828590491c7e4dcb918da80bd492ed38c72 --- /dev/null +++ b/hypha/apply/users/tests/test_tokens.py @@ -0,0 +1,63 @@ +import pytest +from ddf import G + +from hypha.apply.users.models import PendingSignup +from hypha.apply.users.tests.factories import UserFactory + +from ..tokens import PasswordlessLoginTokenGenerator, PasswordlessSignupTokenGenerator + +# mark all test to use database +pytestmark = pytest.mark.django_db + + +def test_passwordless_login_token(time_machine, settings): + """ + Test to check that the tokens are generated correctly and that they are valid + for the correct amount of time. + """ + settings.PASSWORDLESS_LOGIN_TIMEOUT = 60 + + time_machine.move_to("2021-01-01 00:00:00", tick=False) + # Create a token generator + token_generator = PasswordlessLoginTokenGenerator() + # Create a user + user = UserFactory() + # Create a token + token = token_generator.make_token(user) + + # Check that the token is valid + assert token_generator.check_token(user, token) + + # negative check + assert token_generator.check_token(user, "invalid-token") is False + + # timeout check + time_machine.shift(delta=62) + assert token_generator.check_token(user, token) is False + + +def test_passwordless_signup_token(time_machine, settings): + """ + Test to check that the tokens are generated correctly and that they are valid + for the correct amount of time. + """ + settings.PASSWORDLESS_SIGNUP_TIMEOUT = 60 + + time_machine.move_to("2021-01-01 00:00:00", tick=False) + + # Create a token generator + token_generator = PasswordlessSignupTokenGenerator() + # Create a user + signup_obj = G(PendingSignup) + + # Create a token + token = token_generator.make_token(user=signup_obj) + # Check that the token is valid + assert token_generator.check_token(user=signup_obj, token=token) + + # negative check + assert token_generator.check_token(signup_obj, "invalid-token") is False + + # timeout check + time_machine.shift(delta=62) + assert token_generator.check_token(signup_obj, token) is False diff --git a/hypha/apply/users/tests/test_views.py b/hypha/apply/users/tests/test_views.py index 45351e8e8f5825c461320166fa9590f51a914d2e..0171d310df02ef444aa218cdcbbd7d8c8a04a642 100644 --- a/hypha/apply/users/tests/test_views.py +++ b/hypha/apply/users/tests/test_views.py @@ -1,3 +1,4 @@ +from django.conf import settings from django.core import mail from django.test import TestCase, override_settings from django.urls import reverse @@ -25,7 +26,7 @@ class TestProfileView(BaseTestProfielView): # Initial redirect will be via to https through a 301 self.assertRedirects( response, - reverse("users_public:login") + "?next=" + self.url, + reverse(settings.LOGIN_URL) + "?next=" + self.url, status_code=301, ) diff --git a/hypha/apply/users/tokens.py b/hypha/apply/users/tokens.py new file mode 100644 index 0000000000000000000000000000000000000000..6c842fdfb122079bcd4e285ba467a795637f46db --- /dev/null +++ b/hypha/apply/users/tokens.py @@ -0,0 +1,81 @@ +from django.conf import settings +from django.contrib.auth.tokens import PasswordResetTokenGenerator +from django.utils.crypto import constant_time_compare +from django.utils.http import base36_to_int + + +class PasswordlessLoginTokenGenerator(PasswordResetTokenGenerator): + key_salt = None + TIMEOUT = None + + def __init__(self) -> None: + self.key_salt = ( + self.key_salt or "hypha.apply.users.tokens.PasswordlessLoginTokenGenerator" + ) + self.TIMEOUT = self.TIMEOUT or settings.PASSWORDLESS_LOGIN_TIMEOUT + super().__init__() + + def check_token(self, user, token): + """ + Check that a token is correct for a given user. + """ + if not (user and token): + return False + # Parse the token + try: + ts_b36, _ = token.split("-") + except ValueError: + return False + + try: + ts = base36_to_int(ts_b36) + except ValueError: + return False + + # Check that the timestamp/uid has not been tampered with + for secret in [self.secret, *self.secret_fallbacks]: + if constant_time_compare( + self._make_token_with_timestamp(user, ts, secret), + token, + ): + break + else: + return False + + # Check the timestamp is within limit. + if (self._num_seconds(self._now()) - ts) > self.TIMEOUT: + return False + + return True + + +class PasswordlessSignupTokenGenerator(PasswordlessLoginTokenGenerator): + key_salt = None + TIMEOUT = None + + def __init__(self) -> None: + self.key_salt = ( + self.key_salt or "hypha.apply.users.tokens.PasswordlessLoginTokenGenerator" + ) + self.TIMEOUT = self.TIMEOUT or settings.PASSWORDLESS_SIGNUP_TIMEOUT + super().__init__() + + def _make_hash_value(self, user, timestamp): + """ + Hash the signup request's primary key, email, and some user state + that's sure to change after a signup is completed produce a token that is + invalidated when it's used. + + The token field and modified field will be updated after creating or + updating the signup request. + + Failing those things, settings.PASSWORDLESS_SIGNUP_TIMEOUT eventually + invalidates the token. + + Running this data through salted_hmac() prevents password cracking + attempts using the reset token, provided the secret isn't compromised. + """ + # Truncate microseconds so that tokens are consistent even if the + # database doesn't support microseconds. + modified_timestamp = user.modified.replace(microsecond=0, tzinfo=None) + return f"{user.pk}{user.token}{modified_timestamp}{timestamp}{user.email}" diff --git a/hypha/apply/users/urls.py b/hypha/apply/users/urls.py index 9d8e26b75e1aa0fec29349feebd2d22a6751e6c8..080f44d30be7f7e2f75fffabe6c1ef29c62d40c5 100644 --- a/hypha/apply/users/urls.py +++ b/hypha/apply/users/urls.py @@ -10,19 +10,24 @@ from .views import ( BackupTokensView, EmailChangeConfirmationView, EmailChangeDoneView, - EmailChangePasswordView, LoginView, + PasswordLessLoginSignupView, + PasswordlessLoginView, + PasswordlessSignupView, PasswordResetConfirmView, PasswordResetView, RegisterView, RegistrationSuccessView, TWOFAAdminDisableView, TWOFADisableView, - TWOFARequiredMessageView, TWOFASetupView, + account_email_change, become, create_password, + elevate_check_code_view, oauth, + send_confirm_access_email_view, + set_password_view, ) app_name = "users" @@ -30,12 +35,9 @@ app_name = "users" public_urlpatterns = [ path( - "login/", - LoginView.as_view( - template_name="users/login.html", redirect_authenticated_user=True - ), - name="login", + "auth/", PasswordLessLoginSignupView.as_view(), name="passwordless_login_signup" ), + path("login/", LoginView.as_view(), name="login"), # Log out path("logout/", auth_views.LogoutView.as_view(next_page="/"), name="logout"), path("register/", RegisterView.as_view(), name="register"), @@ -44,114 +46,129 @@ public_urlpatterns = [ ), ] -urlpatterns = [ +account_urls = [ + path( + "", + ratelimit(key="ip", rate=settings.DEFAULT_RATE_LIMIT, method="GET")( + AccountView.as_view() + ), + name="account", + ), + path( + "change-email/", + account_email_change, + name="email_change_confirm_password", + ), path( - "account/", + "password/", include( [ path( - "", - ratelimit(key="ip", rate=settings.DEFAULT_RATE_LIMIT, method="GET")( - AccountView.as_view() + "change/", + ratelimit( + key="user", + rate=settings.DEFAULT_RATE_LIMIT, + method="POST", + )( + auth_views.PasswordChangeView.as_view( + template_name="users/change_password.html", + success_url=reverse_lazy("users:account"), + ) ), - name="account", - ), - path( - "password/", - include( - [ - path( - "", - EmailChangePasswordView.as_view(), - name="email_change_confirm_password", - ), - path( - "change/", - ratelimit( - key="user", - rate=settings.DEFAULT_RATE_LIMIT, - method="POST", - )( - auth_views.PasswordChangeView.as_view( - template_name="users/change_password.html", - success_url=reverse_lazy("users:account"), - ) - ), - name="password_change", - ), - path( - "reset/", - PasswordResetView.as_view(), - name="password_reset", - ), - path( - "reset/done/", - auth_views.PasswordResetDoneView.as_view( - template_name="users/password_reset/done.html" - ), - name="password_reset_done", - ), - path( - "reset/confirm/<uidb64>/<token>/", - PasswordResetConfirmView.as_view(), - name="password_reset_confirm", - ), - path( - "reset/complete/", - auth_views.PasswordResetCompleteView.as_view( - template_name="users/password_reset/complete.html" - ), - name="password_reset_complete", - ), - ] - ), - ), - path( - "confirmation/done/", - EmailChangeDoneView.as_view(), - name="confirm_link_sent", - ), - path( - "confirmation/<uidb64>/<token>/", - EmailChangeConfirmationView.as_view(), - name="confirm_email", - ), - path( - "activate/<uidb64>/<token>/", - ActivationView.as_view(), - name="activate", + name="password_change", ), - path("activate/", create_password, name="activate_password"), - path("oauth", oauth, name="oauth"), - # Two factor redirect path( - "two_factor/required/", - TWOFARequiredMessageView.as_view(), - name="two_factor_required", + "reset/", + PasswordResetView.as_view(), + name="password_reset", ), - path("two_factor/setup/", TWOFASetupView.as_view(), name="setup"), path( - "two_factor/backup_tokens/password/", - BackupTokensView.as_view(), - name="backup_tokens_password", + "reset/done/", + auth_views.PasswordResetDoneView.as_view( + template_name="users/password_reset/done.html" + ), + name="password_reset_done", ), - path("two_factor/disable/", TWOFADisableView.as_view(), name="disable"), path( - "two_factor/admin/disable/<str:user_id>/", - TWOFAAdminDisableView.as_view(), - name="admin_disable", + "reset/confirm/<uidb64>/<token>/", + PasswordResetConfirmView.as_view(), + name="password_reset_confirm", ), path( - "sessions/trusted-device/", - elevate_view, - {"template_name": "elevate/elevate.html"}, - name="elevate", + "reset/complete/", + auth_views.PasswordResetCompleteView.as_view( + template_name="users/password_reset/complete.html" + ), + name="password_reset_complete", ), ] ), ), + path( + "confirmation/done/", + EmailChangeDoneView.as_view(), + name="confirm_link_sent", + ), + path( + "confirmation/<uidb64>/<token>/", + EmailChangeConfirmationView.as_view(), + name="confirm_email", + ), + path( + "activate/<uidb64>/<token>/", + ActivationView.as_view(), + name="activate", + ), + path("activate/", create_password, name="activate_password"), + path("oauth", oauth, name="oauth"), + # 2FA + path("two_factor/setup/", TWOFASetupView.as_view(), name="setup"), + path( + "two_factor/backup_tokens/", + BackupTokensView.as_view(), + name="backup_tokens", + ), + path("two_factor/disable/", TWOFADisableView.as_view(), name="disable"), + path( + "two_factor/admin/disable/<str:user_id>/", + TWOFAAdminDisableView.as_view(), + name="admin_disable", + ), + path( + "auth/<uidb64>/<token>/signup/", + PasswordlessSignupView.as_view(), + name="do_passwordless_signup", + ), + path( + "auth/<uidb64>/<token>/", + PasswordlessLoginView.as_view(), + name="do_passwordless_login", + ), + path( + "auth/set-user-password/", + set_password_view, + name="set_user_password", + ), + path( + "sessions/trusted-device/", + elevate_view, + {"template_name": "elevate/elevate.html"}, + name="elevate", + ), + path( + "sessions/send-confirm-access-email/", + send_confirm_access_email_view, + name="elevate_send_confirm_access_email", + ), + path( + "sessions/verify-confirmation-code/", + elevate_check_code_view, + name="elevate_check_code", + ), ] +urlpatterns = [path("account/", include(account_urls))] + if settings.HIJACK_ENABLE: urlpatterns += [ path("account/become/", become, name="become"), diff --git a/hypha/apply/users/utils.py b/hypha/apply/users/utils.py index e46c33ead6d40ac669555dd3114c76dee09bcc0a..6f544ea3174d5680d850a97891395b19abea3e44 100644 --- a/hypha/apply/users/utils.py +++ b/hypha/apply/users/utils.py @@ -1,9 +1,12 @@ +import string + from django.conf import settings from django.contrib.auth import get_user_model from django.contrib.auth.tokens import PasswordResetTokenGenerator from django.core.mail import send_mail from django.template.loader import render_to_string from django.urls import reverse +from django.utils.crypto import get_random_string from django.utils.encoding import force_bytes from django.utils.http import url_has_allowed_host_and_scheme, urlsafe_base64_encode from django.utils.translation import gettext_lazy as _ @@ -53,7 +56,13 @@ def can_use_oauth_check(user): return False -def send_activation_email(user, site=None, redirect_url=""): +def send_activation_email( + user, + site=None, + email_template="users/activation/email.txt", + email_subject_template="users/activation/email_subject.txt", + redirect_url="", +): """ Send the activation email. The activation key is the username, signed using TimestampSigner. @@ -82,10 +91,10 @@ def send_activation_email(user, site=None, redirect_url=""): if site: context.update(site=site) - subject = "Account details for {username} at {org_long_name}".format(**context) + subject = render_to_string(email_subject_template, context) # Force subject to a single line to avoid header-injection issues. subject = "".join(subject.splitlines()) - message = render_to_string("users/activation/email.txt", context) + message = render_to_string(email_template, context) user.email_user(subject, message, settings.DEFAULT_FROM_EMAIL) @@ -157,3 +166,11 @@ def get_redirect_url( require_https=request.is_secure(), ) return redirect_to if url_is_safe else "" + + +def generate_numeric_token(length=6): + """ + Generate a random 6 digit string of numbers. + We use this formatting to allow leading 0s. + """ + return get_random_string(length, allowed_chars=string.digits) diff --git a/hypha/apply/users/views.py b/hypha/apply/users/views.py index 990f3279f80a05ae1d41d7695708ab34421b6982..6fa58f44caf0e1dda8a10ecebcc28ead39d7ca36 100644 --- a/hypha/apply/users/views.py +++ b/hypha/apply/users/views.py @@ -1,4 +1,5 @@ import datetime +import time from typing import Any from urllib.parse import urlencode @@ -15,12 +16,13 @@ from django.contrib.auth.views import ( from django.contrib.auth.views import PasswordResetView as DjPasswordResetView from django.contrib.sites.shortcuts import get_current_site from django.core.exceptions import PermissionDenied -from django.core.signing import BadSignature, Signer, TimestampSigner, dumps, loads -from django.http import HttpResponseRedirect +from django.core.signing import TimestampSigner, dumps, loads +from django.http import HttpResponse, HttpResponseRedirect from django.shortcuts import Http404, get_object_or_404, redirect, render, resolve_url from django.template.loader import render_to_string from django.template.response import TemplateResponse from django.urls import reverse, reverse_lazy +from django.utils import timezone from django.utils.decorators import method_decorator from django.utils.encoding import force_str from django.utils.http import urlsafe_base64_decode @@ -30,9 +32,12 @@ from django.views.decorators.debug import sensitive_post_parameters from django.views.generic import UpdateView from django.views.generic.base import TemplateView, View from django.views.generic.edit import FormView +from django_htmx.http import HttpResponseClientRedirect from django_otp import devices_for_user from django_ratelimit.decorators import ratelimit from elevate.mixins import ElevateMixin +from elevate.utils import grant_elevated_privileges +from elevate.views import redirect_to_elevate from hijack.views import AcquireUserView from two_factor.forms import AuthenticationTokenForm, BackupTokenForm from two_factor.utils import default_device, get_otpauth_url, totp_digits @@ -45,17 +50,26 @@ from wagtail.models import Site from wagtail.users.views.users import change_user_perm from hypha.apply.home.models import ApplyHomePage +from hypha.core.mail import MarkdownMail from .decorators import require_oauth_whitelist from .forms import ( BecomeUserForm, CustomAuthenticationForm, CustomUserCreationForm, - EmailChangePasswordForm, + Disable2FAConfirmationForm, + PasswordlessAuthForm, ProfileForm, - TWOFAPasswordForm, ) -from .utils import get_redirect_url, send_confirmation_email +from .models import ConfirmAccessToken, PendingSignup +from .services import PasswordlessAuthService +from .tokens import PasswordlessLoginTokenGenerator, PasswordlessSignupTokenGenerator +from .utils import ( + generate_numeric_token, + get_redirect_url, + send_activation_email, + send_confirmation_email, +) User = get_user_model() @@ -72,24 +86,24 @@ class RegisterView(View): # We keep /register in the urls in order to test (where we turn on/off # the setting per test), but when disabled, we want to pretend it doesn't # exist va 404 - if not settings.ENABLE_REGISTRATION_WITHOUT_APPLICATION: + if not settings.ENABLE_PUBLIC_SIGNUP: raise Http404 if request.user.is_authenticated: - return redirect("dashboard:dashboard") + return redirect(settings.LOGIN_REDIRECT_URL) ctx = { - "form": self.form(), + "form": self.form(register_view=True), "redirect_url": get_redirect_url(request, self.redirect_field_name), } return render(request, "users/register.html", ctx) def post(self, request): # See comment in get() above about doing this here rather than in urls - if not settings.ENABLE_REGISTRATION_WITHOUT_APPLICATION: + if not settings.ENABLE_PUBLIC_SIGNUP: raise Http404 - form = self.form(data=request.POST) + form = self.form(register_view=True, data=request.POST) context = {} if form.is_valid(): # If using wagtail password management @@ -136,6 +150,10 @@ class LoginView(TwoFactorLoginView): ("backup", BackupTokenForm), ) + redirect_field_name = "next" + redirect_authenticated_user = True + template_name = "users/login.html" + def get_context_data(self, form, **kwargs): context_data = super(LoginView, self).get_context_data(form, **kwargs) context_data["is_public_site"] = True @@ -158,28 +176,28 @@ class AccountView(UpdateView): def get_object(self): return self.request.user + def get_form_kwargs(self) -> dict[str, Any]: + kwargs = super().get_form_kwargs() + kwargs["request"] = self.request + return kwargs + def form_valid(self, form): updated_email = form.cleaned_data["email"] name = form.cleaned_data["full_name"] slack = form.cleaned_data.get("slack", "") user = get_object_or_404(User, id=self.request.user.id) - if updated_email: + if user.email != updated_email: base_url = reverse("users:email_change_confirm_password") query_dict = {"updated_email": updated_email, "name": name, "slack": slack} signer = TimestampSigner() signed_value = signer.sign(dumps(query_dict)) - # Using session variables for redirect validation - token_signer = Signer() - self.request.session["signed_token"] = token_signer.sign(user.email) return redirect( "{}?{}".format(base_url, urlencode({"value": signed_value})) ) - return super(AccountView, self).form_valid(form) + return super().form_valid(form) - def get_success_url( - self, - ): + def get_success_url(self): return reverse_lazy("users:account") def get_context_data(self, **kwargs): @@ -200,72 +218,54 @@ class AccountView(UpdateView): ) -@method_decorator(login_required, name="dispatch") -class EmailChangePasswordView(FormView): - form_class = EmailChangePasswordForm - template_name = "users/email_change/confirm_password.html" - success_url = reverse_lazy("users:confirm_link_sent") - title = _("Enter Password") +@login_required +def account_email_change(request): + if request.user.has_usable_password() and not request.is_elevated(): + return redirect_to_elevate(request.get_full_path()) - def get_initial(self): - """ - Validating the redirection from account via session variable - """ - if "signed_token" not in self.request.session: - raise Http404 - signer = Signer() - try: - signer.unsign(self.request.session["signed_token"]) - except BadSignature as e: - raise Http404 from e - return super(EmailChangePasswordView, self).get_initial() + signer = TimestampSigner() + try: + unsigned_value = signer.unsign( + request.GET.get("value"), max_age=settings.PASSWORD_PAGE_TIMEOUT + ) + except Exception: + messages.error( + request, + _("Password Page timed out. Try changing the email again."), + ) + return redirect("users:account") + value = loads(unsigned_value) - def get_form_kwargs(self): - kwargs = super().get_form_kwargs() - kwargs["user"] = self.request.user - return kwargs + if slack := value["slack"] is not None: + request.user.slack = slack - def form_valid(self, form): - # Make sure redirection url is inaccessible after email is sent - if "signed_token" in self.request.session: - del self.request.session["signed_token"] - signer = TimestampSigner() - try: - unsigned_value = signer.unsign( - self.request.GET.get("value"), max_age=settings.PASSWORD_PAGE_TIMEOUT - ) - except Exception: - messages.error( - self.request, - _("Password Page timed out. Try changing the email again."), - ) - return redirect("users:account") - value = loads(unsigned_value) - form.save(**value) - user = self.request.user - if user.email != value["updated_email"]: - send_confirmation_email( - user, - signer.sign(dumps(value["updated_email"])), - updated_email=value["updated_email"], - site=Site.find_for_request(self.request), - ) - # alert email - user.email_user( - subject="Alert! An attempt to update your email.", - message=render_to_string( - "users/email_change/update_info_email.html", - { - "name": user.get_full_name(), - "username": user.get_username(), - "org_email": settings.ORG_EMAIL, - "org_short_name": settings.ORG_SHORT_NAME, - "org_long_name": settings.ORG_LONG_NAME, - }, - ), - from_email=settings.DEFAULT_FROM_EMAIL, + request.user.full_name = value["name"] + request.user.save() + + if request.user.email != value["updated_email"]: + send_confirmation_email( + request.user, + signer.sign(dumps(value["updated_email"])), + updated_email=value["updated_email"], + site=Site.find_for_request(request), ) - return super(EmailChangePasswordView, self).form_valid(form) + + # alert email + request.user.email_user( + subject="Alert! An attempt to update your email.", + message=render_to_string( + "users/email_change/update_info_email.html", + { + "name": request.user.get_full_name(), + "username": request.user.get_username(), + "org_email": settings.ORG_EMAIL, + "org_short_name": settings.ORG_SHORT_NAME, + "org_long_name": settings.ORG_LONG_NAME, + }, + ), + from_email=settings.DEFAULT_FROM_EMAIL, + ) + return redirect("users:confirm_link_sent") @method_decorator(login_required, name="dispatch") @@ -349,10 +349,7 @@ class ActivationView(TemplateView): if self.valid(user, kwargs.get("token")): user.backend = settings.CUSTOM_AUTH_BACKEND login(request, user) - if ( - settings.WAGTAILUSERS_PASSWORD_ENABLED - and settings.ENABLE_REGISTRATION_WITHOUT_APPLICATION - ): + if settings.WAGTAILUSERS_PASSWORD_ENABLED and settings.ENABLE_PUBLIC_SIGNUP: # In this case, the user entered a password while registering, # and so they shouldn't need to activate a password return redirect("users:account") @@ -496,46 +493,34 @@ class TWOFASetupView(TwoFactorSetupView): name="dispatch", ) @method_decorator(login_required, name="dispatch") -class TWOFADisableView(TwoFactorDisableView): +class TWOFADisableView(ElevateMixin, TwoFactorDisableView): """ View for disabling two-factor for a user's account. """ template_name = "two_factor/profile/disable.html" success_url = reverse_lazy("users:account") - form_class = TWOFAPasswordForm - - def get_form_kwargs(self): - kwargs = super().get_form_kwargs() - kwargs["user"] = self.request.user - return kwargs + form_class = Disable2FAConfirmationForm @method_decorator( permission_required(change_user_perm, raise_exception=True), name="dispatch" ) -class TWOFAAdminDisableView(FormView): +class TWOFAAdminDisableView(ElevateMixin, FormView): """ View for PasswordForm to confirm the Disable 2FA process on wagtail admin. """ - form_class = TWOFAPasswordForm + form_class = Disable2FAConfirmationForm template_name = "two_factor/admin/disable.html" user = None def get_form_kwargs(self): kwargs = super().get_form_kwargs() - # pass request's user to form to validate the password - kwargs["user"] = self.request.user # store the user from url for redirecting to the same user's account edit page self.user = get_object_or_404(User, pk=self.kwargs.get("user_id")) return kwargs - def get_form(self, form_class=None): - form = super(TWOFAAdminDisableView, self).get_form(form_class=form_class) - form.fields["password"].label = "Password" - return form - def form_valid(self, form): for device in devices_for_user(self.user): device.delete() @@ -545,13 +530,23 @@ class TWOFAAdminDisableView(FormView): return reverse("wagtailusers_users:edit", args=[self.user.id]) def get_context_data(self, **kwargs): - ctx = super(TWOFAAdminDisableView, self).get_context_data(**kwargs) + ctx = super().get_context_data(**kwargs) ctx["user"] = self.user return ctx -class TWOFARequiredMessageView(TemplateView): - template_name = "two_factor/core/two_factor_required.html" +def mfa_failure_view( + request, reason, template_name="two_factor/core/two_factor_required.html" +): + """Renders a template asking the user to setup 2FA. + + Used by hypha.apply.users.middlewares.TwoFactorAuthenticationMiddleware, + if ENFORCE_TWO_FACTOR is enabled. + """ + ctx = { + "reason": reason, + } + return render(request, template_name, ctx) class BackupTokensView(ElevateMixin, TwoFactorBackupTokensView): @@ -611,3 +606,228 @@ class PasswordResetConfirmView(DjPasswordResetConfirmView): redirect_url = f"{redirect_url}?next={next_path}" return HttpResponseRedirect(redirect_url) + + +@method_decorator( + ratelimit(key="ip", rate=settings.DEFAULT_RATE_LIMIT, method="POST"), + name="dispatch", +) +@method_decorator( + ratelimit(key="post:email", rate=settings.DEFAULT_RATE_LIMIT, method="POST"), + name="dispatch", +) +class PasswordLessLoginSignupView(FormView): + """This view is used to collect the email address for passwordless login/signup. + + If the email address is already associated with an account, an email is sent. If not, + if the registration is enabled an email is sent, to allow the user to create an account. + + NOTE: This view should never expose whether an email address is associated with an account. + """ + + template_name = "users/passwordless_login_signup.html" + redirect_field_name = "next" + http_method_names = ["get", "post"] + form_class = PasswordlessAuthForm + + def get(self, request, *args, **kwargs): + if request.user.is_authenticated: + return redirect(settings.LOGIN_REDIRECT_URL) + + return super().get(request, *args, **kwargs) + + def get_context_data(self, **kwargs: Any) -> dict[str, Any]: + ctx = super().get_context_data(**kwargs) + if self.request.htmx: + ctx["base_template"] = "includes/_partial-main.html" + else: + ctx["base_template"] = "base-apply.html" + ctx["redirect_url"] = get_redirect_url(self.request, self.redirect_field_name) + return ctx + + def post(self, request): + form = self.get_form() + if form.is_valid(): + service = PasswordlessAuthService( + request, redirect_field_name=self.redirect_field_name + ) + + email = form.cleaned_data["email"] + service.initiate_login_signup(email=email) + + return TemplateResponse( + self.request, + "users/partials/passwordless_login_signup_sent.html", + self.get_context_data(), + ) + else: + return self.render_to_response(self.get_context_data(form=form)) + + +class PasswordlessLoginView(LoginView): + """This view is used to capture the passwordless login token and log the user in. + + If the token is valid, the user is logged in and redirected to the dashboard. + If the token is invalid, the user is shown invalid token page. + + This view inherits from LoginView to reuse the 2FA views, if a mfa device is added + to the user. + """ + + def get(self, request, uidb64, token, *args, **kwargs): + try: + user = User.objects.get(pk=force_str(urlsafe_base64_decode(uidb64))) + except (TypeError, ValueError, OverflowError, User.DoesNotExist): + user = None + + if user and self.check_token(user, token): + user.backend = settings.CUSTOM_AUTH_BACKEND + + if default_device(user): + # User has mfa, set the user details and redirect to 2fa login + self.storage.reset() + self.storage.authenticated_user = user + self.storage.data["authentication_time"] = int(time.time()) + return self.render_goto_step("token") + + # No mfa, log the user in + login(request, user) + + if redirect_url := get_redirect_url(request, self.redirect_field_name): + return redirect(redirect_url) + + return redirect("dashboard:dashboard") + + return render(request, "users/activation/invalid.html") + + def check_token(self, user, token): + token_generator = PasswordlessLoginTokenGenerator() + return token_generator.check_token(user, token) + + +class PasswordlessSignupView(TemplateView): + """This view is used to capture the passwordless login token and log the user in. + + If the token is valid, the user is logged in and redirected to the dashboard. + If the token is invalid, the user is shown invalid token page. + """ + + redirect_field_name = "next" + + def get(self, request, *args, **kwargs): + pending_signup = self.get_pending_signup(kwargs.get("uidb64")) + token = kwargs.get("token") + token_generator = PasswordlessSignupTokenGenerator() + + if pending_signup and token_generator.check_token(pending_signup, token): + user = User.objects.create(email=pending_signup.email, is_active=True) + user.set_unusable_password() + user.save() + pending_signup.delete() + + user.backend = settings.CUSTOM_AUTH_BACKEND + login(request, user) + + redirect_url = get_redirect_url(request, self.redirect_field_name) + + if redirect_url: + return redirect(redirect_url) + + # If 2FA is enabled, redirect to setup page instead of dashboard + if settings.ENFORCE_TWO_FACTOR: + redirect_url = redirect_url or reverse("dashboard:dashboard") + return redirect(reverse("two_factor:setup") + f"?next={redirect_url}") + + return redirect("dashboard:dashboard") + + return render(request, "users/activation/invalid.html") + + def get_pending_signup(self, uidb64): + """ + Given the verified uid, look up and return the corresponding user + account if it exists, or `None` if it doesn't. + """ + try: + return PendingSignup.objects.get( + **{"pk": force_str(urlsafe_base64_decode(uidb64))} + ) + except (TypeError, ValueError, OverflowError, PendingSignup.DoesNotExist): + return None + + +@login_required +def send_confirm_access_email_view(request): + """Sends email with link to login in an elevated mode.""" + token_obj, _ = ConfirmAccessToken.objects.update_or_create( + user=request.user, token=generate_numeric_token + ) + email_context = { + "org_long_name": settings.ORG_LONG_NAME, + "org_email": settings.ORG_EMAIL, + "org_short_name": settings.ORG_SHORT_NAME, + "token": token_obj.token, + "username": request.user.email, + "site": Site.find_for_request(request), + "user": request.user, + "timeout_minutes": settings.PASSWORDLESS_LOGIN_TIMEOUT // 60, + } + subject = "Confirmation code for {org_long_name}: {token}".format(**email_context) + email = MarkdownMail("users/emails/confirm_access.md") + email.send( + to=request.user.email, + subject=subject, + from_email=settings.DEFAULT_FROM_EMAIL, + context=email_context, + ) + return render( + request, + "users/partials/confirmation_code_sent.html", + {"redirect_url": get_redirect_url(request, "next")}, + ) + + +@never_cache +@login_required +@ratelimit(key="user", rate=settings.DEFAULT_RATE_LIMIT) +def elevate_check_code_view(request): + """Checks if the code is correct and if so, elevates the user session.""" + token = request.POST.get("code") + + def validate_token_and_age(token): + try: + token_obj = ConfirmAccessToken.objects.get(user=request.user, token=token) + token_age_in_seconds = (timezone.now() - token_obj.modified).total_seconds() + if token_age_in_seconds <= settings.PASSWORDLESS_LOGIN_TIMEOUT: + token_obj.delete() + return True + except ConfirmAccessToken.DoesNotExist: + return False + + redirect_url = get_redirect_url(request, "next") + if token and validate_token_and_age(token): + grant_elevated_privileges(request) + return HttpResponseClientRedirect(redirect_url) + + return render( + request, + "users/partials/confirmation_code_sent.html", + {"error": True, "redirect_url": redirect_url}, + ) + + +@login_required +def set_password_view(request): + """Sends email with link to set password to user that doesn't have usable password. + + This will the case when the user signed up using passwordless signup or using oauth. + """ + site = Site.find_for_request(request) + + if not request.user.has_usable_password(): + send_activation_email( + user=request.user, + site=site, + email_template="users/emails/set_password.txt", + email_subject_template="users/emails/set_password_subject.txt", + ) + return HttpResponse("✓ Check your email for password set link.") diff --git a/hypha/apply/utils/templates/apply/403.html b/hypha/apply/utils/templates/apply/403.html new file mode 100644 index 0000000000000000000000000000000000000000..dc66c58154cb9a5a8ff7ae0f87643d878e7c0fb8 --- /dev/null +++ b/hypha/apply/utils/templates/apply/403.html @@ -0,0 +1,13 @@ +{% extends "base-apply.html" %} +{% load wagtailcore_tags wagtailsettings_tags %} + +{% block title %}{{ settings.utils.SystemMessagesSettings.title_403 }}{% endblock %} + +{% block body_class %}template-403{% endblock %} + +{% block content %} + <div class="wrapper wrapper--small wrapper--inner-space-large"> + <h1>{{ settings.utils.SystemMessagesSettings.title_403 }}</h1> + <div class="rich-text">{{ settings.utils.SystemMessagesSettings.body_403|richtext }}</div> + </div> +{% endblock %} diff --git a/hypha/apply/utils/templatetags/apply_tags.py b/hypha/apply/utils/templatetags/apply_tags.py index fd1028c89e9d44f3740915a2aa0049606be95785..06b19b8415cfc2cbbaca6b5dae8374082cf50a69 100644 --- a/hypha/apply/utils/templatetags/apply_tags.py +++ b/hypha/apply/utils/templatetags/apply_tags.py @@ -1,6 +1,7 @@ import babel.numbers from django import template from django.conf import settings +from django.template.defaultfilters import stringfilter register = template.Library() @@ -24,3 +25,16 @@ def format_number_as_currency(amount): return babel.numbers.get_currency_symbol( settings.CURRENCY_CODE, locale=settings.CURRENCY_LOCALE ) + + +@register.filter(is_safe=True) +@stringfilter +def truncatechars_middle(value, arg): + try: + ln = int(arg) + except ValueError: + return value + if len(value) <= ln: + return value + else: + return "{}...{}".format(value[: ln // 2], value[-((ln + 1) // 2) :]) diff --git a/hypha/apply/utils/views.py b/hypha/apply/utils/views.py index 2262ba1c57e552939655082c82b7c76e752b8f8f..a3bc8f408354b2598c5ad1a98c627d961e06800e 100644 --- a/hypha/apply/utils/views.py +++ b/hypha/apply/utils/views.py @@ -22,6 +22,12 @@ def page_not_found(request, exception=None, template_name="apply/404.html"): return defaults.page_not_found(request, exception, template_name) +def permission_denied(request, exception=None, template_name="apply/403.html"): + if not request.user.is_authenticated: + template_name = "403.html" + return defaults.permission_denied(request, exception, template_name) + + @method_decorator(login_required, name="dispatch") class ViewDispatcher(View): admin_view: View = None diff --git a/hypha/core/components.py b/hypha/core/components.py index c8bfd01de03e3ae6d71f972e692830e78582590b..cfedf05f66e33bcfa5587499caab39d2ff4c5768 100644 --- a/hypha/core/components.py +++ b/hypha/core/components.py @@ -14,3 +14,8 @@ class DropdownMenu(component.Component): def get_context_data(self, **kwargs) -> dict: return {"id": str(uuid.uuid4())} + + +@component.register("scroll-to-top") +class ScrollToTop(component.Component): + template_name = "components/scroll-to-top.html" diff --git a/hypha/core/context_processors.py b/hypha/core/context_processors.py index 88467023a0dfd2fa03284d77eb4c5cfe2c0ebbe7..d3a1cccf7be64f8581267ca15639268080323f1b 100644 --- a/hypha/core/context_processors.py +++ b/hypha/core/context_processors.py @@ -12,7 +12,7 @@ def global_vars(request): "ORG_GUIDE_URL": settings.ORG_GUIDE_URL, "ORG_URL": settings.ORG_URL, "GOOGLE_OAUTH2": settings.SOCIAL_AUTH_GOOGLE_OAUTH2_KEY, - "ENABLE_REGISTRATION_WITHOUT_APPLICATION": settings.ENABLE_REGISTRATION_WITHOUT_APPLICATION, + "ENABLE_PUBLIC_SIGNUP": settings.ENABLE_PUBLIC_SIGNUP, "ENABLE_GOOGLE_TRANSLATE": settings.ENABLE_GOOGLE_TRANSLATE, "SENTRY_TRACES_SAMPLE_RATE": settings.SENTRY_TRACES_SAMPLE_RATE, "SENTRY_ENVIRONMENT": settings.SENTRY_ENVIRONMENT, diff --git a/hypha/core/templates/components/scroll-to-top.html b/hypha/core/templates/components/scroll-to-top.html new file mode 100644 index 0000000000000000000000000000000000000000..e82af1e466ff8c43b994be37d2657989eb1e6d98 --- /dev/null +++ b/hypha/core/templates/components/scroll-to-top.html @@ -0,0 +1,14 @@ +{% load heroicons %} + +<template x-teleport="body" x-data="{scrollBackTop: false, lastScrollTop: 0}"> + <button + x-show="scrollBackTop" + x-transition + x-transition.duration.500ms + x-on:scroll.window.throttle.50ms="scrollBackTop = (window.pageYOffset < lastScrollTop && window.pageYOffset > window.outerHeight * 0.4) ? true : false; lastScrollTop = window.pageYOffset;" + @click="window.scrollTo({top: 0, behavior: 'smooth'})" + aria-label="Back to top" + class="fixed top-0 right-1/2 px-3 py-2 mt-10 -mr-[64px] text-white bg-light-blue/80 z-30 hover:bg-light-blue hover:text-white hover:shadow-lg transition-all focus:outline-none cursor-pointer shadow-lg rounded-2xl"> + {% heroicon_mini "arrow-long-up" class="inline align-text-bottom" size=18 aria_hidden=true %} Back to top + </button> +</template> diff --git a/hypha/core/utils.py b/hypha/core/utils.py index 0b3ac7295d14483d15ed9cf009e2e57f08d3a058..a07009f6ef11fa97e17ac6f54f3de42cda2d34b3 100644 --- a/hypha/core/utils.py +++ b/hypha/core/utils.py @@ -19,6 +19,6 @@ def markdown_to_html(text: str) -> str: escape=False, hard_wrap=True, renderer="html", - plugins=["strikethrough", "footnotes", "table"], + plugins=["strikethrough", "footnotes", "table", "url"], ) return md(text) diff --git a/hypha/images/migrations/0006_alter_rendition_file.py b/hypha/images/migrations/0006_alter_rendition_file.py new file mode 100644 index 0000000000000000000000000000000000000000..b4c05a782844b21ec9fac9e88d347eaec9cef05a --- /dev/null +++ b/hypha/images/migrations/0006_alter_rendition_file.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.7 on 2023-11-09 05:24 + +from django.db import migrations +import wagtail.images.models + + +class Migration(migrations.Migration): + dependencies = [ + ("images", "0005_auto_20230214_0658"), + ] + + operations = [ + migrations.AlterField( + model_name="rendition", + name="file", + field=wagtail.images.models.WagtailImageField( + height_field="height", + storage=wagtail.images.models.get_rendition_storage, + upload_to=wagtail.images.models.get_rendition_upload_to, + width_field="width", + ), + ), + ] diff --git a/hypha/locale/django.pot b/hypha/locale/django.pot index 59cefaa710fb3ec585ca572c4206a80931df32b2..e08120d6923815c8e6e40005a31d5758fc62c20d 100644 --- a/hypha/locale/django.pot +++ b/hypha/locale/django.pot @@ -6473,38 +6473,6 @@ msgstr "" msgid "Promoted RFPs" msgstr "" -#: hypha/public/mailchimp/forms.py:6 -msgid "Email Address" -msgstr "" - -#: hypha/public/mailchimp/forms.py:7 -msgid "First Name" -msgstr "" - -#: hypha/public/mailchimp/forms.py:8 -msgid "Last Name" -msgstr "" - -#: hypha/public/mailchimp/models.py:18 -msgid "The title of the newsletter signup form." -msgstr "" - -#: hypha/public/mailchimp/templates/mailchimp/newsletter_signup.html:16 -msgid "Sign up" -msgstr "" - -#: hypha/public/mailchimp/views.py:86 -msgid "Sorry, there were errors with your form." -msgstr "" - -#: hypha/public/mailchimp/views.py:91 -msgid "Sorry, there has been an problem. Please try again later." -msgstr "" - -#: hypha/public/mailchimp/views.py:98 -msgid "Thank you for subscribing" -msgstr "" - #: hypha/public/navigation/models.py:14 msgid "Leave blank to use the page's own title" msgstr "" @@ -6659,22 +6627,6 @@ msgid "" "is not defined." msgstr "" -#: hypha/public/utils/models.py:268 -msgid "Your Twitter username without the @, e.g. katyperry" -msgstr "" - -#: hypha/public/utils/models.py:273 -msgid "Your Facebook app ID." -msgstr "" - -#: hypha/public/utils/models.py:279 -msgid "Default sharing text to use if social text has not been set on a page." -msgstr "" - -#: hypha/public/utils/models.py:286 -msgid "Site name, used by Open Graph." -msgstr "" - #: hypha/public/utils/models.py:303 msgid "Default site logo" msgstr "" @@ -6727,22 +6679,6 @@ msgstr "" msgid "You might also like…" msgstr "" -#: hypha/templates/includes/share.html:4 -msgid "Share" -msgstr "" - -#: hypha/templates/includes/share.html:8 -msgid "Share on Twitter" -msgstr "" - -#: hypha/templates/includes/share.html:14 -msgid "Share on LinkedIn" -msgstr "" - -#: hypha/templates/includes/share.html:21 -msgid "Share on Facebook" -msgstr "" - #: hypha/templates/password_required.html:4 #: hypha/templates/password_required.html:9 msgid "Password required" diff --git a/hypha/public/forms/migrations/0005_remove_formpage_social_image_and_more.py b/hypha/public/forms/migrations/0005_remove_formpage_social_image_and_more.py new file mode 100644 index 0000000000000000000000000000000000000000..da06fd5fd75ded0b205232d6ccdc642a4bdd27ef --- /dev/null +++ b/hypha/public/forms/migrations/0005_remove_formpage_social_image_and_more.py @@ -0,0 +1,20 @@ +# Generated by Django 4.2.9 on 2024-01-07 18:52 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("public_forms", "0004_auto_20220722_0844"), + ] + + operations = [ + migrations.RemoveField( + model_name="formpage", + name="social_image", + ), + migrations.RemoveField( + model_name="formpage", + name="social_text", + ), + ] diff --git a/hypha/public/funds/migrations/0015_remove_baseapplicationpage_social_image_and_more.py b/hypha/public/funds/migrations/0015_remove_baseapplicationpage_social_image_and_more.py new file mode 100644 index 0000000000000000000000000000000000000000..4888ad5080aef9fbdbc7a1989e9d627a1948fc55 --- /dev/null +++ b/hypha/public/funds/migrations/0015_remove_baseapplicationpage_social_image_and_more.py @@ -0,0 +1,52 @@ +# Generated by Django 4.2.9 on 2024-01-07 18:52 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("public_funds", "0014_auto_20220722_0844"), + ] + + operations = [ + migrations.RemoveField( + model_name="baseapplicationpage", + name="social_image", + ), + migrations.RemoveField( + model_name="baseapplicationpage", + name="social_text", + ), + migrations.RemoveField( + model_name="fundindex", + name="social_image", + ), + migrations.RemoveField( + model_name="fundindex", + name="social_text", + ), + migrations.RemoveField( + model_name="labindex", + name="social_image", + ), + migrations.RemoveField( + model_name="labindex", + name="social_text", + ), + migrations.RemoveField( + model_name="labpage", + name="social_image", + ), + migrations.RemoveField( + model_name="labpage", + name="social_text", + ), + migrations.RemoveField( + model_name="opencallindexpage", + name="social_image", + ), + migrations.RemoveField( + model_name="opencallindexpage", + name="social_text", + ), + ] diff --git a/hypha/public/home/migrations/0014_remove_homepage_social_image_and_more.py b/hypha/public/home/migrations/0014_remove_homepage_social_image_and_more.py new file mode 100644 index 0000000000000000000000000000000000000000..d931488efaab45b0e71072a0977f2b92daf9e617 --- /dev/null +++ b/hypha/public/home/migrations/0014_remove_homepage_social_image_and_more.py @@ -0,0 +1,20 @@ +# Generated by Django 4.2.9 on 2024-01-07 18:52 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("home", "0013_alter_homepage_our_work"), + ] + + operations = [ + migrations.RemoveField( + model_name="homepage", + name="social_image", + ), + migrations.RemoveField( + model_name="homepage", + name="social_text", + ), + ] diff --git a/hypha/public/home/templates/home/home_page.html b/hypha/public/home/templates/home/home_page.html index a6ee50260e26f88beee258f2c154c5ca9e990c62..3afe23c5d580d10e17e32289fbd04fffb39a2615 100644 --- a/hypha/public/home/templates/home/home_page.html +++ b/hypha/public/home/templates/home/home_page.html @@ -27,10 +27,6 @@ </a> <div class="header__inner header__inner--mobile-buttons"> - <button class="button js-search-toggle" aria-haspopup="true"> - <svg class="header__icon header__icon--open-search header__icon--open-search-menu-closed icon icon--mobile-menu"><use xlink:href="#magnifying-glass"></use></svg> - <svg class="header__icon header__icon--close-search header__icon--close-search-menu-closed icon icon--mobile-menu"><use xlink:href="#cross"></use></svg> - </button> <button class="button button--left-space js-mobile-menu-toggle" aria-haspopup="true"> <svg class="icon icon--mobile-menu"><use xlink:href="#mobile-menu-toggle"></use></svg> </button> @@ -40,11 +36,6 @@ {% cache 3600 navigation__primary wagtail_site %} {% primarynav %} {% endcache %} - - <button class="button button--contains-icons button--left-space js-search-toggle" aria-haspopup="true" aria-label="Toggle desktop search"> - <svg class="header__icon header__icon--open-search icon"><use xlink:href="#magnifying-glass"></use></svg> - <svg class="header__icon header__icon--close-search icon"><use xlink:href="#cross"></use></svg> - </button> </section> <section class="header__menus header__menus--mobile"> @@ -57,14 +48,6 @@ <svg class="header__logo header__logo--mobile"><use xlink:href="#logo-mobile"></use></svg> {% endif %} </a> - <div class="header__inner header__inner--mobile-buttons"> - <button class="button js-mobile-search-toggle" aria-haspopup="true" aria-label="Toggle mobile search"> - <svg class="header__icon header__icon--open-search icon icon--mobile-menu"><use xlink:href="#magnifying-glass"></use></svg> - </button> - <button class="button button--left-space js-mobile-menu-close"> - <svg class="header__icon header__icon--cross icon icon--mobile-menu"><use xlink:href="#cross"></use></svg> - </button> - </div> </div> {% cache 3600 navigation__primary wagtail_site %} {% primarynav %} @@ -73,21 +56,12 @@ <div class="header__button-container"> {% include "utils/includes/login_button.html" %} - {% if ENABLE_REGISTRATION_WITHOUT_APPLICATION %} + {% if ENABLE_PUBLIC_SIGNUP %} {% include "utils/includes/register_button.html" %} {% endif %} </div> </div> - <div class="header__search"> - <form action="{% url 'search' %}" method="get" role="search" class="form form--header-search-desktop"> - <button class="button" type="submit" aria-label="Search"> - <svg class="icon icon--magnifying-glass icon--search"><use xlink:href="#magnifying-glass"></use></svg> - </button> - <input class="input input--transparent input--secondary" type="text" placeholder="Search…" name="query"{% if search_query %} value="{{ search_query }}{% endif %}" aria-label="Search input"> - </form> - </div> - <div class="wrapper wrapper--medium wrapper--page-title"> <h1 class="header__title header__title--homepage">{% block page_title %}{{ page.title }}{% endblock %}</h1> <p class="header__strapline">{{ page.strapline }}</p> diff --git a/hypha/public/mailchimp/apps.py b/hypha/public/mailchimp/apps.py deleted file mode 100644 index d952a8383af7aef582c6fbf9390c50399d4bdc6c..0000000000000000000000000000000000000000 --- a/hypha/public/mailchimp/apps.py +++ /dev/null @@ -1,5 +0,0 @@ -from django.apps import AppConfig - - -class MailchimpConfig(AppConfig): - name = "hypha.public.mailchimp" diff --git a/hypha/public/mailchimp/forms.py b/hypha/public/mailchimp/forms.py deleted file mode 100644 index 6e64b51b410a347a270d9c98939c40fdb70abef1..0000000000000000000000000000000000000000 --- a/hypha/public/mailchimp/forms.py +++ /dev/null @@ -1,16 +0,0 @@ -from django import forms -from django.utils.translation import gettext_lazy as _ - - -class NewsletterForm(forms.Form): - email = forms.EmailField(label=_("Email Address")) - fname = forms.CharField(label=_("First Name"), required=False) - lname = forms.CharField(label=_("Last Name"), required=False) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - for field in self.fields.values(): - class_name = "input--secondary" - if field.required: - class_name += " input__secondary--required" - field.widget.attrs = {"class": class_name} diff --git a/hypha/public/mailchimp/migrations/0001_add_newsletter_setting.py b/hypha/public/mailchimp/migrations/0001_add_newsletter_setting.py deleted file mode 100644 index 25a7613a9713b64d66f79116053b7de281975eac..0000000000000000000000000000000000000000 --- a/hypha/public/mailchimp/migrations/0001_add_newsletter_setting.py +++ /dev/null @@ -1,49 +0,0 @@ -# Generated by Django 2.1.11 on 2019-10-03 12:56 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - initial = True - - dependencies = [ - ("wagtailcore", "0041_group_collection_permissions_verbose_name_plural"), - ] - - operations = [ - migrations.CreateModel( - name="NewsletterSettings", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "newsletter_title", - models.CharField( - default="Get the latest internet freedom news", - help_text="The title of the newsletter signup form.", - max_length=255, - verbose_name="Newsletter title", - ), - ), - ( - "site", - models.OneToOneField( - editable=False, - on_delete=django.db.models.deletion.CASCADE, - to="wagtailcore.Site", - ), - ), - ], - options={ - "verbose_name": "newsletter settings", - }, - ), - ] diff --git a/hypha/public/mailchimp/models.py b/hypha/public/mailchimp/models.py deleted file mode 100644 index 878fbe6895d8aef64c4041dd41558ec8e1cedece..0000000000000000000000000000000000000000 --- a/hypha/public/mailchimp/models.py +++ /dev/null @@ -1,23 +0,0 @@ -from django.db import models -from django.utils.translation import gettext_lazy as _ -from wagtail.admin.panels import FieldPanel -from wagtail.contrib.settings.models import BaseSiteSetting - -from hypha.core.wagtail.admin import register_public_site_setting - - -@register_public_site_setting -class NewsletterSettings(BaseSiteSetting): - class Meta: - verbose_name = "newsletter settings" - - newsletter_title = models.CharField( - "Newsletter title", - max_length=255, - default="Get the latest internet freedom news", - help_text=_("The title of the newsletter signup form."), - ) - - panels = [ - FieldPanel("newsletter_title"), - ] diff --git a/hypha/public/mailchimp/templates/mailchimp/newsletter_signup.html b/hypha/public/mailchimp/templates/mailchimp/newsletter_signup.html deleted file mode 100644 index f56ceafa6783ccca321ec724654a495f22bc246e..0000000000000000000000000000000000000000 --- a/hypha/public/mailchimp/templates/mailchimp/newsletter_signup.html +++ /dev/null @@ -1,23 +0,0 @@ -{% load static i18n %} - -<h4>{{ settings.mailchimp.NewsletterSettings.newsletter_title }}</h4> -<form class="form newsletter-form" action="#" data-actionpath="{{ PUBLIC_SITE.root_url }}{% url "newsletter:subscribe" %}" method="post"> - <div> - {% for field in newsletter_form %} - <label for="{{ field.id_for_label }}"{% if field.field.required %} required{% endif %}> - <span>{{ field.label }}</span> - {% if field.field.required %} - <span class="form__required">*</span> - {% endif %} - </label> - {{ field }} - {% endfor %} - <div class="form-actions form-wrapper"> - <button class="form-submit button button--transparent--wide link--footer-signup" type="submit">{% trans 'Sign up' %}</button> - </div> - </div> -</form> - -{% block extra_js %} - <script src="{% static 'js/public/protect-form.js' %}"></script> -{% endblock %} diff --git a/hypha/public/mailchimp/tests/test_views.py b/hypha/public/mailchimp/tests/test_views.py deleted file mode 100644 index b5f7d3c69256739b23a5a65abdad70f369f5222b..0000000000000000000000000000000000000000 --- a/hypha/public/mailchimp/tests/test_views.py +++ /dev/null @@ -1,58 +0,0 @@ -import re -from unittest import mock -from urllib import parse - -from django.test import TestCase, override_settings -from django.urls import reverse - -any_url = re.compile(".") - - -class TestNewsletterView(TestCase): - url = reverse("newsletter:subscribe") - - def setUp(self): - self.origin = "https://testserver/" - - def assertNewsletterRedirects(self, response, target_url, *args, **kwargs): - url = response.redirect_chain[0][0] - parts = parse.urlsplit(url) - self.assertTrue(parts.query.startswith("newsletter-")) - target_url = target_url + "?" + parts.query - return self.assertRedirects(response, target_url, *args, **kwargs) - - def test_redirected_home_if_get(self): - response = self.client.get(self.url, secure=True, follow=True) - request = response.request - self.assertRedirects( - response, - "{}://{}/".format(request["wsgi.url_scheme"], request["SERVER_NAME"]), - ) - - @override_settings(MAILCHIMP_API_KEY="a" * 32, MAILCHIMP_LIST_ID="12345") - def test_can_subscribe(self): - with mock.patch( - "hypha.public.mailchimp.views.subscribe_to_mailchimp" - ) as mc_mock: - mc_mock.return_value = None - response = self.client.post( - self.url, data={"email": "email@email.com"}, secure=True, follow=True - ) - - self.assertNewsletterRedirects(response, self.origin) - - mc_mock.assert_called_once_with( - email="email@email.com", data={"fname": "", "lname": ""} - ) - - def test_error_in_form(self): - with mock.patch( - "hypha.public.mailchimp.views.subscribe_to_mailchimp" - ) as mc_mock: - mc_mock.return_value = None - response = self.client.post( - self.url, data={"email": "email_is_bad.com"}, secure=True, follow=True - ) - self.assertNewsletterRedirects(response, self.origin) - - assert not mc_mock.called, "method should not have been called" diff --git a/hypha/public/mailchimp/urls.py b/hypha/public/mailchimp/urls.py deleted file mode 100644 index 82819cc22f6d2b79f5cb6228237d57a562ffa4fa..0000000000000000000000000000000000000000 --- a/hypha/public/mailchimp/urls.py +++ /dev/null @@ -1,8 +0,0 @@ -from django.urls import path - -from .views import MailchimpSubscribeView - -app_name = "newsletter" - - -urlpatterns = [path("subscribe/", MailchimpSubscribeView.as_view(), name="subscribe")] diff --git a/hypha/public/mailchimp/views.py b/hypha/public/mailchimp/views.py deleted file mode 100644 index 4fe50350e5de248e8890bf78dbcdd11be37067cc..0000000000000000000000000000000000000000 --- a/hypha/public/mailchimp/views.py +++ /dev/null @@ -1,113 +0,0 @@ -import logging -import uuid - -from django.conf import settings -from django.contrib import messages -from django.http import HttpResponseRedirect -from django.utils.decorators import method_decorator -from django.utils.translation import gettext as _ -from django.views.decorators.csrf import csrf_exempt -from django.views.generic import RedirectView -from django.views.generic.edit import FormMixin -from django_ratelimit.decorators import ratelimit -from mailchimp3 import MailChimp - -from .forms import NewsletterForm - -logger = logging.getLogger(__name__) - - -def subscribe_to_mailchimp(email: str, data) -> None: - mailchimp_enabled = settings.MAILCHIMP_API_KEY and settings.MAILCHIMP_LIST_ID - - dummy_key = "a" * 32 - - if not mailchimp_enabled: - raise Exception( - f"Incorrect Mailchimp configuration: " - f"API_KEY: {settings.MAILCHIMP_API_KEY}, LIST_ID: {settings.MAILCHIMP_LIST_ID}" - ) - - client = MailChimp( - mc_api=settings.MAILCHIMP_API_KEY or dummy_key, - timeout=5.0, - enabled=mailchimp_enabled, - ) - data = {k.upper(): v for k, v in data.items()} - - client.lists.members.create( - settings.MAILCHIMP_LIST_ID, - { - "email_address": email, - "status": "pending", - "merge_fields": data, - }, - ) - - -@method_decorator( - ratelimit(key="ip", rate=settings.DEFAULT_RATE_LIMIT, method="POST"), - name="dispatch", -) -@method_decorator( - ratelimit(key="post:email", rate=settings.DEFAULT_RATE_LIMIT, method="POST"), - name="dispatch", -) -@method_decorator(csrf_exempt, name="dispatch") -class MailchimpSubscribeView(FormMixin, RedirectView): - form_class = NewsletterForm - - def post(self, request, *args, **kwargs): - form = self.get_form() - if form.is_valid(): - return self.form_valid(form) - else: - return self.form_invalid(form) - - def form_invalid(self, form): - self.error(form) - return HttpResponseRedirect(self.get_success_url()) - - def form_valid(self, form): - data = form.cleaned_data.copy() - email = data.pop("email") - - try: - subscribe_to_mailchimp(email=email, data=data) - self.success() - except Exception as e: - self.warning(e) - - return super().form_valid(form) - - def error(self, form): - messages.error( - self.request, - _("Sorry, there were errors with your form.") + str(form.errors), - ) - - def warning(self, e): - messages.warning( - self.request, _("Sorry, there has been an problem. Please try again later.") - ) - # If there is a problem with subscribing uncomment this to get notifications. - # When things work warnings is only about spam scipts. - # logger.error(e.args[0]) - - def success(self): - messages.success(self.request, _("Thank you for subscribing")) - - def get_success_url(self): - # Go back to where you came from, default to front page. - origin = ( - self.request.META.get("HTTP_ORIGIN") - or self.request.META.get("HTTP_REFERER") - or "/" - ) - - # Add cache busting query string. - return origin + "?newsletter-" + uuid.uuid4().hex - - def get_redirect_url(self): - # We don't know where you came from, go home - return "/" diff --git a/hypha/public/navigation/templates/navigation/primarynav-apply.html b/hypha/public/navigation/templates/navigation/primarynav-apply.html index 0980776acb4ffaf2606d912b74fa15fca2b94516..c8b22b352f6290a05a9ed878d35a12a73d245505 100644 --- a/hypha/public/navigation/templates/navigation/primarynav-apply.html +++ b/hypha/public/navigation/templates/navigation/primarynav-apply.html @@ -1,15 +1,14 @@ {% if request.user.is_authenticated %} <nav role="navigation" aria-label="Primary" class="w-full"> <ul class="nav nav--primary" role="menubar"> - {% if request.user.is_apply_staff %} + {% if request.user.can_access_dashboard %} {% include "navigation/primarynav-apply-item.html" with name="My dashboard" url="dashboard:dashboard" %} + {% endif %} + {% if request.user.is_apply_staff %} {% include "navigation/primarynav-apply-item.html" with name="Submissions" url="funds:submissions:overview" %} {% include "navigation/primarynav-apply-item.html" with name="Projects" url="apply:projects:overview" %} {% elif request.user.is_finance or request.user.is_contracting %} - {% include "navigation/primarynav-apply-item.html" with name="My dashboard" url="dashboard:dashboard" %} {% include "navigation/primarynav-apply-item.html" with name="Projects" url="apply:projects:overview" %} - {% else %} - {% include "navigation/primarynav-apply-item.html" with name="My dashboard" url="dashboard:dashboard" %} {% endif %} </ul> </nav> diff --git a/hypha/public/news/migrations/0015_remove_newsindex_social_image_and_more.py b/hypha/public/news/migrations/0015_remove_newsindex_social_image_and_more.py new file mode 100644 index 0000000000000000000000000000000000000000..f1d1687d0b13909ffc524111f9ec83107e5d992f --- /dev/null +++ b/hypha/public/news/migrations/0015_remove_newsindex_social_image_and_more.py @@ -0,0 +1,28 @@ +# Generated by Django 4.2.9 on 2024-01-07 18:52 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("news", "0014_alter_newspagenewstype_news_type"), + ] + + operations = [ + migrations.RemoveField( + model_name="newsindex", + name="social_image", + ), + migrations.RemoveField( + model_name="newsindex", + name="social_text", + ), + migrations.RemoveField( + model_name="newspage", + name="social_image", + ), + migrations.RemoveField( + model_name="newspage", + name="social_text", + ), + ] diff --git a/hypha/public/news/templates/news/news_page.html b/hypha/public/news/templates/news/news_page.html index 9d40bb989b4fdaf40414f30bbe1754185f919906..2164a308ef81312119cb3c8ca2fed3d352fb5e4a 100644 --- a/hypha/public/news/templates/news/news_page.html +++ b/hypha/public/news/templates/news/news_page.html @@ -39,7 +39,6 @@ </ul> {% endif %} - {% include "includes/share.html" %} </article> {% include "includes/relatedcontent.html" with related_documents=page.related_documents.all related_pages=page.related_pages.all %} diff --git a/hypha/public/partner/migrations/0003_remove_partnerindexpage_social_image_and_more.py b/hypha/public/partner/migrations/0003_remove_partnerindexpage_social_image_and_more.py new file mode 100644 index 0000000000000000000000000000000000000000..a61ebbc866fa95904898105f55f7fc70b9c7be78 --- /dev/null +++ b/hypha/public/partner/migrations/0003_remove_partnerindexpage_social_image_and_more.py @@ -0,0 +1,28 @@ +# Generated by Django 4.2.9 on 2024-01-07 18:52 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("partner", "0002_currency_symbol_setting"), + ] + + operations = [ + migrations.RemoveField( + model_name="partnerindexpage", + name="social_image", + ), + migrations.RemoveField( + model_name="partnerindexpage", + name="social_text", + ), + migrations.RemoveField( + model_name="partnerpage", + name="social_image", + ), + migrations.RemoveField( + model_name="partnerpage", + name="social_text", + ), + ] diff --git a/hypha/public/people/migrations/0016_remove_personindexpage_social_image_and_more.py b/hypha/public/people/migrations/0016_remove_personindexpage_social_image_and_more.py new file mode 100644 index 0000000000000000000000000000000000000000..f2729adb6bdce2d69db97929f83fc83e7e456a2b --- /dev/null +++ b/hypha/public/people/migrations/0016_remove_personindexpage_social_image_and_more.py @@ -0,0 +1,28 @@ +# Generated by Django 4.2.9 on 2024-01-07 18:52 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("people", "0015_alter_personpage_biography"), + ] + + operations = [ + migrations.RemoveField( + model_name="personindexpage", + name="social_image", + ), + migrations.RemoveField( + model_name="personindexpage", + name="social_text", + ), + migrations.RemoveField( + model_name="personpage", + name="social_image", + ), + migrations.RemoveField( + model_name="personpage", + name="social_text", + ), + ] diff --git a/hypha/public/projects/migrations/0011_remove_projectindexpage_social_image_and_more.py b/hypha/public/projects/migrations/0011_remove_projectindexpage_social_image_and_more.py new file mode 100644 index 0000000000000000000000000000000000000000..ee51e678370c4c59263a22541413a074a4e31549 --- /dev/null +++ b/hypha/public/projects/migrations/0011_remove_projectindexpage_social_image_and_more.py @@ -0,0 +1,28 @@ +# Generated by Django 4.2.9 on 2024-01-07 18:52 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("projects", "0010_alter_projectpage_body"), + ] + + operations = [ + migrations.RemoveField( + model_name="projectindexpage", + name="social_image", + ), + migrations.RemoveField( + model_name="projectindexpage", + name="social_text", + ), + migrations.RemoveField( + model_name="projectpage", + name="social_image", + ), + migrations.RemoveField( + model_name="projectpage", + name="social_text", + ), + ] diff --git a/hypha/public/search/__init__.py b/hypha/public/search/__init__.py deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/hypha/public/search/templates/search/includes/search_result.html b/hypha/public/search/templates/search/includes/search_result.html deleted file mode 100644 index 9e7d4b8365e153c5a30ce20dc5eb507939d5f52d..0000000000000000000000000000000000000000 --- a/hypha/public/search/templates/search/includes/search_result.html +++ /dev/null @@ -1,31 +0,0 @@ -{% load static wagtailcore_tags wagtailsearchpromotions_tags wagtailimages_tags %} - -<a class="listing" href="{% pageurl result %}"> - {# breadcrumbs #} - <h6 class="listing__path"> - {% for ancestor in result.get_ancestors %} - {% if not ancestor.is_root %} - {% if ancestor.depth > 2 %} - <span>{{ ancestor.title }}</span> - {% if ancestor.depth|add:1 < result.depth %} - <span class="nav__item--breadcrumb"></span> - {% endif %} - {% else %}<span class="nav__item--breadcrumb"></span>{% endif %} {# the first one #} - {% endif %} - {% endfor %} - </h6> - - {% if result.listing_image or result.icon %} - {% image result.listing_image|default:result.icon fill-180x180 class="listing__image" %} - {% else %} - <div class="listing__image listing__image--default"> - <svg><use xlink:href="#logo-mobile-no-text"></use></svg> - </div> - {% endif %} - - <h4 class="listing__title">{{ result.listing_title|default:result.title }}</h4> - - {% if pick.description or result.listing_summary or result.search_description or result.listing_summary or result.introduction %} - <h6 class="listing__teaser">{{ pick.description|default:result.listing_summary|default:result.search_description|default:result.listing_summary|default:result.introduction|truncatechars_html:155 }}</h6> - {% endif %} -</a> diff --git a/hypha/public/search/templates/search/search.html b/hypha/public/search/templates/search/search.html deleted file mode 100644 index 98361a1ec1b4491d4941f46a3e0fc357be86b220..0000000000000000000000000000000000000000 --- a/hypha/public/search/templates/search/search.html +++ /dev/null @@ -1,41 +0,0 @@ -{% extends "base.html" %} -{% load static wagtailcore_tags wagtailsearchpromotions_tags %} -{% block body_class %}template-searchresults light-grey-bg{% endblock %} -{% block page_title %}Search results{% endblock %} -{% block title %}{% if search_query %}Search results for “{{ search_query }}”{% else %}Search{% endif %}{% endblock %} -{% block content %} - <div class="wrapper wrapper--small wrapper--inner-space-medium"> - <h2 class="heading heading--no-margin">{% if search_query %}Search results for “{{ search_query }}”{% else %}Search{% endif %}</h2> - - {% if search_results %} - {% with count=search_results.paginator.count %} - <p>{{ count }} result{{ count|pluralize }} found.</p> - {% endwith %} - {% elif search_query and not search_picks %} - <p>No results found.</p> - {% endif %} - - <form class="form" action="{% url 'search' %}" method="get" role="search" aria-label="Search form"> - <input class="input input--bottom-space" type="text" placeholder="Search…" name="query"{% if search_query %} value="{{ search_query }}"{% endif %} aria-label="Search input"> - <input class="link link--button" type="submit" value="Search" aria-label="search"> - </form> - - {% get_search_promotions search_query as search_picks %} - {% if search_picks %} - <div class="wrapper wrapper--listings"> - {% for pick in search_picks %} - {% include "search/includes/search_result.html" with result=pick.page.specific %} - {% endfor %} - </div> - {% endif %} - - {% if search_results %} - <div class="wrapper wrapper--listings"> - {% for result in search_results %} - {% include "search/includes/search_result.html" with result=result.specific %} - {% endfor %} - </div> - {% include "includes/pagination.html" with paginator_page=search_results %} - {% endif %} - </div> -{% endblock %} diff --git a/hypha/public/search/views.py b/hypha/public/search/views.py deleted file mode 100644 index ade7b21730126c74c6d656441ecf405905b2fb25..0000000000000000000000000000000000000000 --- a/hypha/public/search/views.py +++ /dev/null @@ -1,64 +0,0 @@ -import re - -from django.conf import settings -from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator -from django.http import Http404 -from django.shortcuts import render -from wagtail.models import Page, Site -from wagtail.search.models import Query - - -def search(request): - site = Site.find_for_request(request) - if ( - not site.is_default_site - and "apply" in site.site_name.lower() - and "apply" in site.hostname - and "apply" in site.root_page.title.lower() - ): - raise Http404 - - search_query = request.GET.get("query", None) - page = request.GET.get("page", 1) - - # Search - if search_query: - # Allow only word characters and spaces in search query. - words = re.findall(r"\w+", search_query.strip()) - search_query = " ".join(words) - - public_site = site.root_page - - search_results = ( - Page.objects.live() - .descendant_of( - public_site, - inclusive=True, - ) - .specific() - .search(search_query, operator="and") - ) - query = Query.get(search_query) - - # Record hit - query.add_hit() - else: - search_results = Page.objects.none() - - # Pagination - paginator = Paginator(search_results, settings.DEFAULT_PER_PAGE) - try: - search_results = paginator.page(page) - except PageNotAnInteger: - search_results = paginator.page(1) - except EmptyPage: - search_results = paginator.page(paginator.num_pages) - - return render( - request, - "search/search.html", - { - "search_query": search_query, - "search_results": search_results, - }, - ) diff --git a/hypha/public/standardpages/migrations/0007_remove_indexpage_social_image_and_more.py b/hypha/public/standardpages/migrations/0007_remove_indexpage_social_image_and_more.py new file mode 100644 index 0000000000000000000000000000000000000000..a7dfe858d4f8d3607b21edc79e4d730c6b0d822e --- /dev/null +++ b/hypha/public/standardpages/migrations/0007_remove_indexpage_social_image_and_more.py @@ -0,0 +1,28 @@ +# Generated by Django 4.2.9 on 2024-01-07 18:52 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("standardpages", "0006_alter_informationpage_body"), + ] + + operations = [ + migrations.RemoveField( + model_name="indexpage", + name="social_image", + ), + migrations.RemoveField( + model_name="indexpage", + name="social_text", + ), + migrations.RemoveField( + model_name="informationpage", + name="social_image", + ), + migrations.RemoveField( + model_name="informationpage", + name="social_text", + ), + ] diff --git a/hypha/public/standardpages/templates/standardpages/information_page.html b/hypha/public/standardpages/templates/standardpages/information_page.html index e676eafdaccafb2138480d349939b3ff9a788209..53f0ceda8a5589a108161e7581edb284559403c3 100644 --- a/hypha/public/standardpages/templates/standardpages/information_page.html +++ b/hypha/public/standardpages/templates/standardpages/information_page.html @@ -10,7 +10,6 @@ {% endif %} {% include_block page.body %} - {% include "includes/share.html" %} </article> </div> diff --git a/hypha/public/urls.py b/hypha/public/urls.py index cbc340f23d81203392434914b8bfbb2b96a54038..4c9a378fa158dbeb2c2848457d2490f0f34bb370 100644 --- a/hypha/public/urls.py +++ b/hypha/public/urls.py @@ -1,17 +1,13 @@ -from django.urls import include, path +from django.urls import path -from .mailchimp import urls as newsletter_urls from .news import feeds as news_feeds from .partner import views as partner_views -from .search import views as search_views urlpatterns = [ - path("search/", search_views.search, name="search"), path("news/feed/", news_feeds.NewsFeed(), name="news_feed"), path( "news/<int:news_type>/feed/", news_feeds.NewsTypeFeed(), name="news_type_feed" ), - path("newsletter/", include(newsletter_urls)), path( "about/portfolio/", partner_views.InvestmentTableView.as_view(), diff --git a/hypha/public/utils/context_processors.py b/hypha/public/utils/context_processors.py index 7bdfc326d969d60ca2ff25410e8f060d614ecb5e..b19178e54ada1cbac5f232a2063193a55cc04a7e 100644 --- a/hypha/public/utils/context_processors.py +++ b/hypha/public/utils/context_processors.py @@ -1,12 +1,7 @@ -from django.conf import settings - from hypha.public.home.models import HomePage -from hypha.public.mailchimp.forms import NewsletterForm def global_vars(request): return { "PUBLIC_SITE": HomePage.objects.first().get_site(), - "newsletter_form": NewsletterForm(), - "newsletter_enabled": settings.MAILCHIMP_API_KEY and settings.MAILCHIMP_LIST_ID, } diff --git a/hypha/public/utils/migrations/0009_systemmessagessettings_body_403_and_more.py b/hypha/public/utils/migrations/0009_systemmessagessettings_body_403_and_more.py new file mode 100644 index 0000000000000000000000000000000000000000..16d4308e43099a181824a93d064c1dbebbf6acf4 --- /dev/null +++ b/hypha/public/utils/migrations/0009_systemmessagessettings_body_403_and_more.py @@ -0,0 +1,28 @@ +# Generated by Django 4.2.7 on 2023-11-28 13:51 + +from django.db import migrations, models +import wagtail.fields + + +class Migration(migrations.Migration): + dependencies = [ + ("utils", "0008_systemmessagessettings_nav_content"), + ] + + operations = [ + migrations.AddField( + model_name="systemmessagessettings", + name="body_403", + field=wagtail.fields.RichTextField( + default="<p>You might not have access to the requested resource.</p>", + verbose_name="Text", + ), + ), + migrations.AddField( + model_name="systemmessagessettings", + name="title_403", + field=models.CharField( + default="Permission Denied", max_length=255, verbose_name="Title" + ), + ), + ] diff --git a/hypha/public/utils/migrations/0010_delete_socialmediasettings.py b/hypha/public/utils/migrations/0010_delete_socialmediasettings.py new file mode 100644 index 0000000000000000000000000000000000000000..bf29e280068a8b88dd92b59820fdf364b48d336f --- /dev/null +++ b/hypha/public/utils/migrations/0010_delete_socialmediasettings.py @@ -0,0 +1,15 @@ +# Generated by Django 4.2.9 on 2024-01-07 18:52 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("utils", "0009_systemmessagessettings_body_403_and_more"), + ] + + operations = [ + migrations.DeleteModel( + name="SocialMediaSettings", + ), + ] diff --git a/hypha/public/utils/models.py b/hypha/public/utils/models.py index f98c22130e43b1e56322fa5534cd2257f8450703..64c96eec04591ab51d78ca4bc1d661d05503ca4a 100644 --- a/hypha/public/utils/models.py +++ b/hypha/public/utils/models.py @@ -13,7 +13,6 @@ from wagtail.admin.panels import ( ) from wagtail.contrib.settings.models import ( BaseGenericSetting, - BaseSiteSetting, register_setting, ) from wagtail.fields import RichTextField, StreamField @@ -21,8 +20,6 @@ from wagtail.models import Orderable, Page from wagtail.snippets.models import register_snippet from wagtailcache.cache import WagtailCacheMixin, cache_page -from hypha.core.wagtail.admin import register_public_site_setting - class LinkFields(models.Model): """ @@ -118,31 +115,6 @@ class RelatedPage(Orderable, models.Model): ] -# Generic social fields abstract class to add social image/text to any new content type easily. -class SocialFields(models.Model): - social_image = models.ForeignKey( - "images.CustomImage", - null=True, - blank=True, - on_delete=models.SET_NULL, - related_name="+", - ) - social_text = models.CharField(max_length=255, blank=True) - - class Meta: - abstract = True - - promote_panels = [ - MultiFieldPanel( - [ - FieldPanel("social_image"), - FieldPanel("social_text"), - ], - "Social networks", - ), - ] - - # Generic listing fields abstract class to add listing image/text to any new content type easily. class ListingFields(models.Model): listing_image = models.ForeignKey( @@ -260,33 +232,6 @@ class CallToActionSnippet(models.Model): return self.title -@register_public_site_setting -class SocialMediaSettings(BaseSiteSetting): - twitter_handle = models.CharField( - max_length=255, - blank=True, - help_text=_("Your Twitter username without the @, e.g. katyperry"), - ) - facebook_app_id = models.CharField( - max_length=255, - blank=True, - help_text=_("Your Facebook app ID."), - ) - default_sharing_text = models.CharField( - max_length=255, - blank=True, - help_text=_( - "Default sharing text to use if social text has not been set on a page." - ), - ) - site_name = models.CharField( - max_length=255, - blank=True, - default="hypha", - help_text=_("Site name, used by Open Graph."), - ) - - @register_setting class SystemMessagesSettings(BaseGenericSetting): wagtail_reference_index_ignore = True @@ -344,6 +289,16 @@ class SystemMessagesSettings(BaseGenericSetting): default="<p>You may be trying to find a page that doesn’t exist or has been moved.</p>", ) + title_403 = models.CharField( + "Title", + max_length=255, + default="Permission Denied", + ) + body_403 = RichTextField( + "Text", + default="<p>You might not have access to the requested resource.</p>", + ) + panels = [ MultiFieldPanel( [ @@ -362,11 +317,18 @@ class SystemMessagesSettings(BaseGenericSetting): ], "404 page", ), + MultiFieldPanel( + [ + FieldPanel("title_403"), + FieldPanel("body_403"), + ], + "403 page", + ), ] @method_decorator(cache_page, name="serve") -class BasePage(WagtailCacheMixin, SocialFields, ListingFields, Page): +class BasePage(WagtailCacheMixin, ListingFields, Page): wagtail_reference_index_ignore = True show_in_menus_default = True @@ -383,9 +345,7 @@ class BasePage(WagtailCacheMixin, SocialFields, ListingFields, Page): content_panels = Page.content_panels + [FieldPanel("header_image")] - promote_panels = ( - Page.promote_panels + SocialFields.promote_panels + ListingFields.promote_panels - ) + promote_panels = Page.promote_panels + ListingFields.promote_panels def cache_control(self): return f"public, s-maxage={settings.CACHE_CONTROL_S_MAXAGE}" diff --git a/hypha/public/utils/templates/utils/includes/login_button.html b/hypha/public/utils/templates/utils/includes/login_button.html index 37fa3b165450795281550edff9baebde90196853..328eff33ba58c4ffd45d172b363ec5fbb42914a0 100644 --- a/hypha/public/utils/templates/utils/includes/login_button.html +++ b/hypha/public/utils/templates/utils/includes/login_button.html @@ -1,9 +1,30 @@ {% load i18n %} -<a href="{{ APPLY_SITE.root_url }}{% url 'users_public:login' %}{% if redirect_url %}?next={{ redirect_url }}{% endif %}" class="button button--transparent button--contains-icons {{ class }}"> - <svg class="icon icon--person"><use xlink:href="#person-icon"></use></svg> - {% if user.is_authenticated %} - My {{ ORG_SHORT_NAME }} + +{% if user.is_authenticated %} + {% if user.can_access_dashboard %} + <a + class="button button--transparent button--contains-icons {{ class }}" + href="{{ APPLY_SITE.root_url }}{% url 'dashboard:dashboard' %}" + > + <svg class="icon icon--person"><use xlink:href="#person-icon"></use></svg> + My {{ ORG_SHORT_NAME }} + </a> {% else %} - {% trans "Login" %} + <a + class="button button--transparent button--contains-icons {{ class }}" + href="{{ APPLY_SITE.root_url }}{% url 'users:account' %}" + title="Goto your account" + > + <svg class="icon icon--person"><use xlink:href="#person-icon"></use></svg> + {{ user }} + </a> {% endif %} -</a> +{% else %} + <a + class="button button--transparent button--contains-icons {{ class }}" + href="{{ APPLY_SITE.root_url }}{% url 'users_public:passwordless_login_signup' %}{% if redirect_url %}?next={{ redirect_url }}{% endif %}" + > + <svg class="icon icon--person"><use xlink:href="#person-icon"></use></svg> + {% trans "Login" %} + </a> +{% endif %} diff --git a/hypha/public/utils/templates/utils/includes/register_button.html b/hypha/public/utils/templates/utils/includes/register_button.html index 29e10dd26a74120e40d54f491d3e38398947c496..648fb72b04833bd0ba466b5bed78184b147af02d 100644 --- a/hypha/public/utils/templates/utils/includes/register_button.html +++ b/hypha/public/utils/templates/utils/includes/register_button.html @@ -1,4 +1,4 @@ {% load i18n %} <a href="{{ APPLY_SITE.root_url }}{% url 'users_public:register' %}{% if redirect_url %}?next={{ redirect_url }}{% endif %}" class="button button--transparent {{ class }}"> - {% trans "Register" %} + {% trans "Sign up" %} </a> diff --git a/hypha/public/utils/templatetags/util_tags.py b/hypha/public/utils/templatetags/util_tags.py index 5e43c9b19aafaa26d26f702e618ed09bbbabfa70..63e01d8cac3fad538e6673350a13caada8f8d294 100644 --- a/hypha/public/utils/templatetags/util_tags.py +++ b/hypha/public/utils/templatetags/util_tags.py @@ -1,20 +1,9 @@ from django import template from wagtail.coreutils import camelcase_to_underscore -from hypha.public.utils.models import SocialMediaSettings - register = template.Library() -# Social text -@register.filter(name="social_text") -def social_text(page, site): - try: - return page.social_text - except AttributeError: - return SocialMediaSettings.for_site(site).default_sharing_text - - # Get widget type of a field @register.filter(name="widget_type") def widget_type(bound_field): diff --git a/hypha/settings/base.py b/hypha/settings/base.py index 818cc03457032fda9dea0f2a77694f210658e239..ef5c0276a13528e2c1e74e3edfc6dc429fe144b1 100644 --- a/hypha/settings/base.py +++ b/hypha/settings/base.py @@ -27,7 +27,7 @@ CURRENCY_LOCALE = env.str("CURRENCY_LOCALE", "en_US") DEFAULT_PER_PAGE = 20 # Form Rate-Limit Configuration -# DEFAULT_RATE_LIMIT is used by login, password, 2FA and Mailchimp forms. +# DEFAULT_RATE_LIMIT is used by login, password, 2FA, etc DEFAULT_RATE_LIMIT = env.str("DEFAULT_RATE_LIMIT", "5/m") # IF Hypha should enforce 2FA for all users. @@ -54,10 +54,18 @@ FILE_ACCEPT_ATTR_VALUE = ", ".join(["." + ext for ext in FILE_ALLOWED_EXTENSIONS # Only effects setting external reviewers for now. GIVE_STAFF_LEAD_PERMS = env.bool("GIVE_STAFF_LEAD_PERMS", False) -# Provide permissions for Archived submissions +# Provide permissions for viewing archived submissions +SUBMISSIONS_ARCHIVED_VIEW_ACCESS_STAFF = env.bool( + "SUBMISSIONS_ARCHIVED_ACCESS_STAFF", False +) +SUBMISSIONS_ARCHIVED_VIEW_ACCESS_STAFF_ADMIN = env.bool( + "SUBMISSIONS_ARCHIVED_ACCESS_STAFF_ADMIN", True +) + +# Provide permissions for archiving submissions SUBMISSIONS_ARCHIVED_ACCESS_STAFF = env.bool("SUBMISSIONS_ARCHIVED_ACCESS_STAFF", False) SUBMISSIONS_ARCHIVED_ACCESS_STAFF_ADMIN = env.bool( - "SUBMISSIONS_ARCHIVED_ACCESS_STAFF_ADMIN", False + "SUBMISSIONS_ARCHIVED_ACCESS_STAFF_ADMIN", True ) # Enable staff to "hijack" (become) other users. @@ -136,14 +144,9 @@ TRANSITION_AFTER_ASSIGNED = env.bool("TRANSITION_AFTER_ASSIGNED", False) # Possible values are: False, 1,2,3,… TRANSITION_AFTER_REVIEWS = env.bool("TRANSITION_AFTER_REVIEWS", False) -# Forces users to log in first in order to make an application. This is particularly useful in conjunction -# with ENABLE_REGISTRATION_WITHOUT_APPLICATION -FORCE_LOGIN_FOR_APPLICATION = env.bool("FORCE_LOGIN_FOR_APPLICATION", False) +# Default visibility for reviews. +REVIEW_VISIBILITY_DEFAULT = env.str("REVIEW_VISIBILITY_DEFAULT", "private") -# Enable users to create accounts without submitting an application. -ENABLE_REGISTRATION_WITHOUT_APPLICATION = env.bool( - "ENABLE_REGISTRATION_WITHOUT_APPLICATION", False -) # Project settings. @@ -172,11 +175,24 @@ LANGUAGE_CODE = env.str("LANGUAGE_CODE", "en") # Number of seconds that password reset and account activation links are valid (default 259200, 3 days). PASSWORD_RESET_TIMEOUT = env.int("PASSWORD_RESET_TIMEOUT", 259200) +# Timeout for passwordless login links (default 900, 15 minutes). +PASSWORDLESS_LOGIN_TIMEOUT = env.int("PASSWORDLESS_LOGIN_TIMEOUT", 900) # 15 minutes + +# Enable users to create accounts without submitting an application. +ENABLE_PUBLIC_SIGNUP = env.bool("ENABLE_PUBLIC_SIGNUP", True) + +# Forces users to log in first in order to make an application. This is particularly useful in conjunction +# with ENABLE_PUBLIC_SIGNUP +# @deprecated: This setting is deprecated and will be removed in a future release. +FORCE_LOGIN_FOR_APPLICATION = env.bool("FORCE_LOGIN_FOR_APPLICATION", True) + +# Timeout for passwordless signup links (default 900, 15 minutes). +PASSWORDLESS_SIGNUP_TIMEOUT = env.int("PASSWORDLESS_SIGNUP_TIMEOUT", 900) # 15 minutes + # Seconds to enter password on password page while email change/2FA change (default 120). PASSWORD_PAGE_TIMEOUT = env.int("PASSWORD_PAGE_TIMEOUT", 120) # Template engines and options to be used with Django. - TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", @@ -280,7 +296,7 @@ MEDIA_URL = env.str("MEDIA_URL", "/media/") # Wagtail settings WAGTAIL_CACHE_TIMEOUT = CACHE_CONTROL_MAX_AGE -WAGTAIL_FRONTEND_LOGIN_URL = "/login/" +WAGTAIL_FRONTEND_LOGIN_URL = "/auth/" WAGTAIL_SITE_NAME = "hypha" WAGTAILIMAGES_IMAGE_MODEL = "images.CustomImage" WAGTAILIMAGES_FEATURE_DETECTION_ENABLED = False @@ -456,14 +472,7 @@ AWS_MIGRATION_BUCKET_NAME = env.str("AWS_MIGRATION_BUCKET_NAME", "") AWS_MIGRATION_ACCESS_KEY_ID = env.str("AWS_MIGRATION_ACCESS_KEY_ID", "") AWS_MIGRATION_SECRET_ACCESS_KEY = env.str("AWS_MIGRATION_SECRET_ACCESS_KEY", "") -# Mailchimp settings. - -MAILCHIMP_API_KEY = env.str("MAILCHIMP_API_KEY", None) -MAILCHIMP_LIST_ID = env.str("MAILCHIMP_LIST_ID", None) - - # Basic auth settings - if env.bool("BASIC_AUTH_ENABLED", False): MIDDLEWARE.insert(0, "baipw.middleware.BasicAuthIPWhitelistMiddleware") BASIC_AUTH_LOGIN = env.str("BASIC_AUTH_LOGIN", None) diff --git a/hypha/settings/django.py b/hypha/settings/django.py index 7d9ca32d6268e36ef5d2b9c75d3062d85867f3df..5e4b72e2e8980e7ccf2dbd5db4aa53db798f0092 100644 --- a/hypha/settings/django.py +++ b/hypha/settings/django.py @@ -18,16 +18,15 @@ INSTALLED_APPS = [ "hypha.apply.review", "hypha.apply.determinations", "hypha.apply.stream_forms", + "hypha.apply.todo", "hypha.apply.utils.apps.UtilsConfig", "hypha.apply.projects.apps.ProjectsConfig", "hypha.public.funds", "hypha.public.home", - "hypha.public.mailchimp", "hypha.public.navigation", "hypha.public.news", "hypha.public.people", "hypha.public.projects", - "hypha.public.search", "hypha.public.standardpages", "hypha.public.forms", "hypha.public.utils", @@ -38,7 +37,6 @@ INSTALLED_APPS = [ "django_web_components", "wagtail.contrib.modeladmin", "wagtail.contrib.settings", - "wagtail.contrib.search_promotions", "wagtail.contrib.forms", "wagtail.contrib.redirects", "wagtail.embeds", @@ -203,7 +201,7 @@ DATETIME_INPUT_FORMATS = [ AUTH_USER_MODEL = "users.User" -LOGIN_URL = "users_public:login" +LOGIN_URL = "users_public:passwordless_login_signup" LOGIN_REDIRECT_URL = "dashboard:dashboard" # https://django-elevate.readthedocs.io/en/latest/config/index.html#configuration diff --git a/hypha/settings/test.py b/hypha/settings/test.py index 3b1e3bf01b7c9d10628b21e66de1864f7471c0b9..51ab2af72b2745ea386888c305bbd8caff48c6f6 100644 --- a/hypha/settings/test.py +++ b/hypha/settings/test.py @@ -30,3 +30,5 @@ TEMPLATES[0]["OPTIONS"]["debug"] = True # An extra salt to be added into the cookie signature. ELEVATE_COOKIE_SALT = SECRET_KEY + +ENFORCE_TWO_FACTOR = False diff --git a/hypha/static_src/src/images/favicons/android-chrome-144.png b/hypha/static_src/src/images/favicons/android-chrome-144.png deleted file mode 100644 index 90ff8e96e63db2067c0aaba084e04ceb09925185..0000000000000000000000000000000000000000 Binary files a/hypha/static_src/src/images/favicons/android-chrome-144.png and /dev/null differ diff --git a/hypha/static_src/src/images/favicons/android-chrome-192x192.png b/hypha/static_src/src/images/favicons/android-chrome-192x192.png index c75978f3cd025424d71e1e44e9f228bb0699be19..8eda0dcb4fe10ce1fa85c8db4fc0a84d895ef41c 100644 Binary files a/hypha/static_src/src/images/favicons/android-chrome-192x192.png and b/hypha/static_src/src/images/favicons/android-chrome-192x192.png differ diff --git a/hypha/static_src/src/images/favicons/android-chrome-512x512.png b/hypha/static_src/src/images/favicons/android-chrome-512x512.png index 9e9d36cce0d5ed1219483132ac70bc117370d886..f62831d0abbe56e9abb64dc31c78bf4eee8e8eb7 100644 Binary files a/hypha/static_src/src/images/favicons/android-chrome-512x512.png and b/hypha/static_src/src/images/favicons/android-chrome-512x512.png differ diff --git a/hypha/static_src/src/images/favicons/apple-icon-120.png b/hypha/static_src/src/images/favicons/apple-icon-120.png deleted file mode 100644 index 8e69eeaa71cee3732b25fe8716da6501f0a50562..0000000000000000000000000000000000000000 Binary files a/hypha/static_src/src/images/favicons/apple-icon-120.png and /dev/null differ diff --git a/hypha/static_src/src/images/favicons/apple-icon-152.png b/hypha/static_src/src/images/favicons/apple-icon-152.png deleted file mode 100644 index f8ec829a936b041d5b267e0e1f53b9eaad197f8e..0000000000000000000000000000000000000000 Binary files a/hypha/static_src/src/images/favicons/apple-icon-152.png and /dev/null differ diff --git a/hypha/static_src/src/images/favicons/apple-icon-180.png b/hypha/static_src/src/images/favicons/apple-icon-180.png deleted file mode 100644 index d3be1f4876df2c6040e15cf1ba23187f17973be8..0000000000000000000000000000000000000000 Binary files a/hypha/static_src/src/images/favicons/apple-icon-180.png and /dev/null differ diff --git a/hypha/static_src/src/images/favicons/apple-icon-76.png b/hypha/static_src/src/images/favicons/apple-icon-76.png deleted file mode 100644 index 9c339ad519c2d0c5ec4bcf68efcdccddcca9917d..0000000000000000000000000000000000000000 Binary files a/hypha/static_src/src/images/favicons/apple-icon-76.png and /dev/null differ diff --git a/hypha/static_src/src/images/favicons/apple-touch-icon-512x512.png b/hypha/static_src/src/images/favicons/apple-touch-icon-512x512.png deleted file mode 100644 index 9e9d36cce0d5ed1219483132ac70bc117370d886..0000000000000000000000000000000000000000 Binary files a/hypha/static_src/src/images/favicons/apple-touch-icon-512x512.png and /dev/null differ diff --git a/hypha/static_src/src/images/favicons/apple-touch-icon.png b/hypha/static_src/src/images/favicons/apple-touch-icon.png index 95cef8d7ed55030f02384eb5f210f9abed3af173..3c76a69a386e8e5830afed4d7d5ff7d7ea7cf1de 100644 Binary files a/hypha/static_src/src/images/favicons/apple-touch-icon.png and b/hypha/static_src/src/images/favicons/apple-touch-icon.png differ diff --git a/hypha/static_src/src/images/favicons/favicon-16.png b/hypha/static_src/src/images/favicons/favicon-16.png deleted file mode 100644 index d6b9ad2e38bd3fbba259c135839178c9fa995afc..0000000000000000000000000000000000000000 Binary files a/hypha/static_src/src/images/favicons/favicon-16.png and /dev/null differ diff --git a/hypha/static_src/src/images/favicons/favicon-16x16.png b/hypha/static_src/src/images/favicons/favicon-16x16.png index b3df2838161db4601d39b6789a3f184d4eccb46b..b12df070c4c948d42ce12f3f9b7092a7d9398f63 100644 Binary files a/hypha/static_src/src/images/favicons/favicon-16x16.png and b/hypha/static_src/src/images/favicons/favicon-16x16.png differ diff --git a/hypha/static_src/src/images/favicons/favicon-32.png b/hypha/static_src/src/images/favicons/favicon-32.png deleted file mode 100644 index a9705041eb12b433320b4b54f4549f2fc661988b..0000000000000000000000000000000000000000 Binary files a/hypha/static_src/src/images/favicons/favicon-32.png and /dev/null differ diff --git a/hypha/static_src/src/images/favicons/favicon-32x32.png b/hypha/static_src/src/images/favicons/favicon-32x32.png index 5153c696b6463b074904e65f5dbae81d61f808a5..d74b72ccc253f036b326ef79945ff77ad5ea186e 100644 Binary files a/hypha/static_src/src/images/favicons/favicon-32x32.png and b/hypha/static_src/src/images/favicons/favicon-32x32.png differ diff --git a/hypha/static_src/src/images/favicons/favicon.ico b/hypha/static_src/src/images/favicons/favicon.ico index 4365d407ed2796bd97ff7b44efb7652d858e5574..8bc1c067bea918ae39f7dcf67fe745df53a2ed50 100644 Binary files a/hypha/static_src/src/images/favicons/favicon.ico and b/hypha/static_src/src/images/favicons/favicon.ico differ diff --git a/hypha/static_src/src/images/favicons/mstile-150.png b/hypha/static_src/src/images/favicons/mstile-150.png deleted file mode 100644 index ac8348baf572804f0ab4203d1435d4beaa90b291..0000000000000000000000000000000000000000 Binary files a/hypha/static_src/src/images/favicons/mstile-150.png and /dev/null differ diff --git a/hypha/static_src/src/images/favicons/mstile-150x150.png b/hypha/static_src/src/images/favicons/mstile-150x150.png index 9af57361535c5eb89dfeb9144b007e06d9666940..9d7c5526277ae07ed137cb21a8b50f3ecdb2d3f2 100644 Binary files a/hypha/static_src/src/images/favicons/mstile-150x150.png and b/hypha/static_src/src/images/favicons/mstile-150x150.png differ diff --git a/hypha/static_src/src/images/favicons/safari-pinned-tab.svg b/hypha/static_src/src/images/favicons/safari-pinned-tab.svg index 5dde630ad27edf5def2680457cf59908716e010c..3effb09a168e09833925aff7c6f9e7bd526f95a1 100644 --- a/hypha/static_src/src/images/favicons/safari-pinned-tab.svg +++ b/hypha/static_src/src/images/favicons/safari-pinned-tab.svg @@ -1,29 +1 @@ -<?xml version="1.0" standalone="no"?> -<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN" - "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd"> -<svg version="1.0" xmlns="http://www.w3.org/2000/svg" - width="512.000000pt" height="512.000000pt" viewBox="0 0 512.000000 512.000000" - preserveAspectRatio="xMidYMid meet"> -<metadata> -Created by potrace 1.11, written by Peter Selinger 2001-2013 -</metadata> -<g transform="translate(0.000000,512.000000) scale(0.100000,-0.100000)" -fill="#000000" stroke="none"> -<path d="M0 2560 l0 -2560 2560 0 2560 0 0 2560 0 2560 -2560 0 -2560 0 0 --2560z m1968 1358 c2 -171 0 -198 -14 -213 -14 -13 -40 -16 -168 -14 l-151 1 --5 -163 -5 -164 -198 -5 -197 -5 -3 -778 c-2 -758 -3 -779 -21 -793 -15 -11 --60 -14 -200 -14 -100 0 -188 5 -196 10 -13 8 -15 108 -18 782 -1 425 0 783 3 -796 5 22 6 22 205 22 l200 0 2 203 3 202 165 1 165 2 2 148 c1 82 2 157 2 167 -1 16 16 17 214 15 l212 -3 3 -197z m1612 37 l0 -165 186 0 c112 0 193 -4 205 --11 19 -10 20 -20 18 -202 l-1 -192 166 -3 166 -2 0 -795 0 -794 -22 -12 c-15 --8 -77 -10 -201 -7 -161 4 -181 6 -193 23 -12 16 -14 150 -14 792 l0 773 -165 -0 -165 0 -2 166 -3 167 -188 -2 c-133 -2 -193 1 -203 10 -11 9 -14 50 -14 209 -0 108 3 200 7 203 3 4 100 7 215 7 l208 0 0 -165z m-1972 -2191 c22 -6 22 -9 -22 -170 l0 -164 158 0 c110 0 162 -4 170 -12 15 -15 17 -370 2 -399 -10 -18 --23 -19 -215 -19 l-205 0 -2 166 -3 167 -150 -2 c-83 -1 -158 1 -168 4 -15 7 --17 27 -17 215 0 157 3 210 13 213 17 8 368 8 395 1z m2370 -7 c15 -10 17 --383 2 -407 -8 -12 -43 -15 -202 -17 l-193 -2 -5 -163 -5 -163 -207 -3 -208 --2 -5 22 c-3 13 -4 109 -3 213 l3 190 200 2 200 2 3 162 c2 115 6 166 15 172 -15 10 387 5 405 -6z"/> -</g> -</svg> +<svg height="525pt" preserveAspectRatio="xMidYMid meet" viewBox="0 0 525 525" width="525pt" xmlns="http://www.w3.org/2000/svg"><g transform="matrix(.1 0 0 -.1 0 525)"><path d="m194 3681c2-751 6-1386 10-1411 3-25 11-70 16-100s12-73 16-95c3-22 17-81 29-131 152-583 532-1094 1045-1404 129-78 273-148 403-194 97-36 97-36 99-13 5 70 9 104 38 277 114 671 441 1312 924 1810 362 373 776 646 1261 832 50 19 287 97 315 103 8 2 62 15 120 29s119 27 135 30c17 3 71 12 120 20 50 9 112 18 139 21 27 2 50 6 51 7 9 10-120 301-183 413-129 232-355 494-572 666-228 181-483 317-757 403-46 14-93 27-105 30-13 2-41 9-63 14-22 6-56 14-75 17s-46 7-60 10c-14 2-43 7-65 10-22 4-53 9-70 11-16 3-647 7-1402 8l-1372 3zm2736 425c198-42 357-97 453-156 27-17 51-30 53-30 10 0 164-114 164-121 0-4-10-11-22-14-57-17-363-166-468-227-107-62-325-202-340-218-3-3-30-23-60-45-115-83-250-202-395-349-82-83-159-163-170-176-11-14-49-61-85-105-132-161-308-423-385-575-61-119-145-291-145-296 0-3-11-30-25-60s-25-56-25-59c0-2-5-16-10-30-13-35-23-27-94 74-122 175-204 375-244 596-3 17-7 430-9 918l-4 888 888-4c488-2 904-7 923-11z"/><path d="m4925 2762c-213-23-549-111-760-199-198-83-495-251-588-333-9-8 475-500 491-500 4 0 30 15 57 33 89 60 209 124 320 172 103 44 301 109 368 120 17 4 50 10 72 15 22 4 62 11 89 15 27 3 52 8 55 10 4 2 6 153 6 337v333h-40c-22 0-53-1-70-3z"/><path d="m2989 1618c-218-301-411-791-464-1180-12-87-14-106-20-169l-7-65h340 340l6 60c27 245 145 586 281 811 26 44 50 83 51 87 4 6-483 498-492 498-2 0-18-19-35-42z"/><path d="m4945 1393c-527-122-893-466-1040-978-25-87-31-114-39-181l-4-30h586l587 1v599c0 525-2 599-15 601-8 1-42-4-75-12z"/></g></svg> \ No newline at end of file diff --git a/hypha/static_src/src/images/favicons/site.webmanifest b/hypha/static_src/src/images/favicons/site.webmanifest index 4cd236b851fd753a4870524194e0fdada8bd2189..3e6f2857ab6a3426a5e9c77a13cca6cbe422a2c5 100644 --- a/hypha/static_src/src/images/favicons/site.webmanifest +++ b/hypha/static_src/src/images/favicons/site.webmanifest @@ -1,6 +1,6 @@ { - "name": "", - "short_name": "", + "name": "Hypha", + "short_name": "Hypha", "icons": [ { "src": "/static/images/favicons/android-chrome-192x192.png", diff --git a/hypha/static_src/src/images/favicons/site.webmanifest.json b/hypha/static_src/src/images/favicons/site.webmanifest.json deleted file mode 100644 index ec3552ca7993f5410acb25cc7bc07f6f17c5b1e2..0000000000000000000000000000000000000000 --- a/hypha/static_src/src/images/favicons/site.webmanifest.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "", - "short_name": "", - "icons": [ - { - "src": "/android-chrome-144.png", - "sizes": "144x144", - "type": "image/png" - } - ], - "theme_color": "#ffffff", - "background_color": "#ffffff", - "display": "standalone" -} diff --git a/hypha/static_src/src/images/logo-small.png b/hypha/static_src/src/images/logo-small.png new file mode 100644 index 0000000000000000000000000000000000000000..f62831d0abbe56e9abb64dc31c78bf4eee8e8eb7 Binary files /dev/null and b/hypha/static_src/src/images/logo-small.png differ diff --git a/hypha/static_src/src/images/logo.png b/hypha/static_src/src/images/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..ee04d9f82622715a774ef2d3d2eedca489c909fe Binary files /dev/null and b/hypha/static_src/src/images/logo.png differ diff --git a/hypha/static_src/src/images/otf_social.jpg b/hypha/static_src/src/images/otf_social.jpg deleted file mode 100644 index 926ea5b4bacb3cac9ee4497acf6957eed08e4df0..0000000000000000000000000000000000000000 Binary files a/hypha/static_src/src/images/otf_social.jpg and /dev/null differ diff --git a/hypha/static_src/src/images/radio-free-asia-logo.svg b/hypha/static_src/src/images/radio-free-asia-logo.svg deleted file mode 100644 index 47118cce988d2aeec55d114ead816b586b92d38d..0000000000000000000000000000000000000000 --- a/hypha/static_src/src/images/radio-free-asia-logo.svg +++ /dev/null @@ -1 +0,0 @@ -<svg width="144" height="61" viewBox="0 0 144 61" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><image width="165" height="62" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAakAAAChCAYAAAB9CECCAAAABGdBTUEAA1teXP8meAAAHhVJREFUeAHtnf1127i2xWfuev9fvQouU8EoFYSuYJQKQlcQpwLLFdipwHIFUSoQU0E0FZhTwehVMG9vXUIDwSRFgh8CyY21EOLjnIODH2geAaKdX35REgEREAEREAEREAEREAEREAEREAEREAEREAEREAEREAEREAEREIF+Cfzar/n5WP/7779jzHaBvET+d37F5Vhne5OU5sIZrn8i88q8//XXXw+4KomACIjALAgoSHkscx6QGIw+IEfILA+VMgzE/AM5RVbgAgQlERCBaRJQkKqxrghKDEIrZAalGDm0tIdDKfJ37LR4VRIBERCBSRBQkCpZRgQmBqXfkXldlIiF2HyAUynyd+StjgdBQUkERGC0BBSkrKXLd0yf0TS2wGTN4k1xi5YXBCtelURABERgVARmH6QQmBZYsQSZwSlCnmo6YGJfkTcIWNlUJ6l5iYAITIvAbIMUglOEpbxHXiEzUM0pbTBZ7q7SOU1acxUBERgfgdkFKSs4JeNbrs49TmHxQcGqc64yKAIi0BGB2QQpBafKOyZFr4JVJSJ1ioAIXIPA5INU/p3TI+Am1wA8sjFT+HuLnVU2Mr/lrgiIwEQJ/Gui8zpOCwHqDoVX5OTYoH8uEYgh8Apuj3lwvySvfhEQARHolcAkd1J4wMagxt3Tsld60zZ+wPS4q9pOe5qanQiIQMgEJrWT4qd/7gIAfIesANXuzltA/Rt47pCjdqakLQIiIAJ+BCazk8KDlEHpG3Lkh0JaFQQO6NOuqgKQukRABPohMImdFALUGnh+IkfISt0TMLuqZ7BmWUkEREAEBiEw6p1U/sDk7ikehJYGIYEM+SO+q9qzoiQCIiACfRIY7U4KAWoJMNw9xX0Cku03BCK07MA/edOjBhEQARHomMAog1T+gOTLEVHHPGSuHgEe+T1jHR7riUtKBERABPwIjO64Dw/GNabKv7mnFAaBLdzgSxWHMNyRFyIgAlMiMKoghQD1DPjJlBZgInPZYx43ClQTWU1NQwQCIjCaIKUAFdBdU+yKAlUxF7WKgAi0IBB8kEJwWmB++uXcFos8oCoDFY/+eFUSAREQgdYEgg5SClCt1/caBg4YlEd/ClTXoK8xRWBiBEIPUnzFfDkx5nOYDgMUAxUDlpIIiIAIeBMI9hX0/DsoBSjvpb2qItdtl++Er+qIBhcBERg3gSB3UnmASsaNVt6DgHZUug1EQARaEQhuJ4UAdYcZJa1mJeVQCHBHxV8bUBIBERABLwJBBSkEqASz0F8x8FrKYJVWWFetabDLI8dEIGwCwRz34UF2/B4DuBZhI5N3ngRu8SLFxlNXaiIgAjMlEESQQoBiYOKbfNFM12EO0z5gknzjbz+HyWqOIiAC3RAI5biP31tE3UxJVgIlwA8i/J9+eVUSAREQgVoErh6k8NC6g6erWt5KaOwEIkxAL1KMfRXlvwgMSOCqx336HmrAlQ5rKP6niduwXJI3IiACIRK4dpDSX5QI8a7o36cDhniPQJX1P5RGEAERGDOBqx33YRe1BrjlmOHJd28CC2jq2M8bnxRFYD4ErrKTQoCKgPh1Ppg10xICOvYrAaNmERCB/xK4VpDaYfhYizB7Ajz2e4djP16VREAEROANgcGP+7CLSuBF/MYTNcyRAI/97uc4cc1ZBESgHoFBd1IIUHwo6Zd2663NnKS4m8rmNGHNVQREoB6BoXdSd3ArqueapGZEQC9RzGixNVURaEJgsJ1UvoviyxLcTSmJgEvgBrup1G1UXQREYN4EhtxJcRelADXv+61q9vpuqoqO+kRgpgQG2Unlu6i/ZspY065PQLup+qwkKQKzIDDUToq7KCURuERAu6lLhNQvAjMj0PtOSt9FzeyOaj9d7abaM5QFEZgMgSF2UvouajK3yyAT+TzIKBpEBERgFASG2Enxjb5oFDTkZCgE3uFNvywUZ+SHCIjA9Qj0upPCUd8KU4uuNz2NPFIC2k2NdOHktgh0TaDXIAVnP3XtsOzNgkAyi1lqkiIgAhcJ9BaksIuKMDp3Ukoi0JTAIt+FN9WTvAiIwMQI9BakwEkBamI3y8DT0S58YOAaTgRCJNDbixP4JMw/JLsMcdLyaTQE/hcvUBxG460cFQER6JzA/3RuEQYRoBa48OGSIlclBjHKKr0lcIum7G3zxZYIEs8XpcYhsIKbmz5cxT1a997LrvmmIfyMMH9mkw7wZ28q6I9NOb/uhwrs+c85Ofqkwfz0cc5Xp4wJ1iT1tVmlV7D+V71fq3z17ettJ9XEIYCOIM+bnfkDcozsk258lAbU2dUciz/A72vKvhEDz1c0Rm86xtewBYePfbgNRlyLuIHtFLIv8GfTQKe1KPxcw8i9ZSiFD6f7HP1/W30s3qA/ddp6qWLoGIbJ0ScN5qePc746YJJA97lA/z3W5fThoqDfq6lg/R8wztrLWKBKveykms4VUDPoMG+RzU5shSJ/OCPkWgl20lqCVxJ6+zwpdeRraU+9Duo/1hMNWor3QCgphiMx1vAD7rPbUJySH8ER+FTiEX+tQvdNCZyq5j5fnKgat7IPD4ED8gb5HQT5STqrVJhW5wHTOQbrFtPatNANShVBIaRARTYJfFoHBUnOBEEA90UER+ISZ0K7j0vcDK85iJ1UFRYEqi0WP4UMd1V3VbIT6eMR16HNXKgPZlvYmMIPxgfMg3MZIj0UDMJPxpHT/hl8n9quk2PTt3rjKO6d+tDVDAMyX0qHSwIj7E8qfD7+WgWfZxUyPl3u+mc+RkLWCT5IEV7+MPiCB8MfqD6HDLQD3146sEETtLPqyNY1zcRDDY77bO2OxWCENr6pGll9C5SXyKnVxmNqtq2Qf0OmDFOGzPuWHz5YLkzQjdDBIyHaYNoj89i2kwT79CdBZtA3vqH4yw9k+sbxukgvRRyLDOc+La2+A/3IWcRsR33Dq0noo/wKmfMwKUPhhytrOs21hMGBusg8uWG5TeIHmqrE/m2VAHzk3H5Hjiy5DGWzTl4+lnDbw665N73sWj6qaAgANo9bCpORCfVa6PR542uXvsP067n50dYWXXKhLZDYuTTKxoDc2pVFPTbyKPNT8rcCGbcpMTr2FUJF9o3uqynk152j63T/45eRg8Ad8l+uoFPfoR4ZnTpXyMeODVbXdXQpU6D/E23PyCad5oqGOoypvywaH+0JchUD9iVFunXaOC6ym57dBtQL72W2I++QqxJ95L1yslEgvLb9pSzyrkDObmo1d3s8lXMCoPtsEzbl0AEZPyuud13OAePwhp5CWnXJhbYAZeeCKRsDcs+uLOqxkS/pL1A5Np3NBS13ZYIl7acHN8cvkDn5lfc/FsiUNfFhFVGvToJsXGBoXUeXMiX6tsnTXNG4szsqym/mANmkQt7tOlufBnN5dgy9oh45bawW/oyjfcfOmmlt/CqQP/VRBv27Apmyppg6oaUgX5yoAekLZA415MYmsunY4a7tdexebXPL2pIdC+KnmWOvCswe7z/0L9CXOP171G+QPyJnyHb6ZCq57r2pW9ctyk/IB6utcRH2YyjdFSju0ZYiu/YXaHtGbpM+YdzdhZw0GQC2OIfY0dmifov8hHyw+hYon5hCN0LdnVOGNj5DmPfIdnqGDm00TStHwRzvZk77J6fOQLJEW+y0b1B/QH5CzpBN4rHk2lSqriV2t9Ax9+bB0ecxo1JXBLAAa+Sz1JXtvuycOfu24v4gdeIGhtm9HWp0LbtOYFhGSriQlZvR9Ca9GlPo4THPzsl86BwT2t379DQX9CXIbrqzdGnbTSd9yrmdqMeW/jenn7sMu59HQc+ODKsn/42toivk4gLdOk1r2qvQp99r9udyryjb6ZntJqGjiFPEfvQ92ooovyIvLF0yYJudTmtg5KquUExs5by8og7K7vjsjmx7qHOuduI6nXzM7VDm0dbL2209ltdGBmVy2Tn5ZBftrm9n95axc+3rKF6cKIH0hHZ+0XyCXiI3lubvPTn6ArtxT7aHMrscaKC6nPgJ/pjwqXaPAj+ZnhJ++Pngi9HAXPXp9AP67ZTBHu/rY6Jt2Nmissqbml5cva+wmRojKB9gn7uJxLTlV+rtnbahqme/9Ar/lhg4cgb/nvO1m+kvZU1aofCEzKudtqjw4W23pagkVgPXjLp105s1BluOw8Sfv7tj6Z9/+Nwi97K0QMcrfExx/QOZ1yeuF661E+T3EK66N+Paxq4o2GmQAlQuBgFXJYImvH1T6LZR6uaLuLLbR1rmw2nbh++wuwGnR9he9GF/IJsLzGHR5n7pyM8Udr7AD96/Zwn+RWjgwydGXiLXSZEjlDp1VvmQWhW0VzbBn7hAYOu2kSlkU7THVp8bPK2ufosFbKOCEb8VtLlNv+UNkdNxhzpzVYqrOu2+fN3d9Tlx5nwgc4DOwtKjvB2kKH9v9bNIecoxH/tgZ4PyA2xmuNZOuY+8N2krqq0YiGCnQQpzemwyrxw6f+gPTfQs2e8oE3yrBD8SGPjUykg7ZX7a6jNtYTzpc4ABbC8xRjrAOGVD8IPE2adSI5jfP7z3F6bN8/qnp14tNfi/ryXYTugB46zbmTjT5rr7pMhHyUNnVaDzw2nbop5YbRHumRU4sf0XrgvqTyjeWTJFxQSNK8jeUKdIwG3L781nt31M9c6CFGAsPSaeQId67z10qVJroWrYjiAT15DrS2TTl+Hc7ldck57H6Nt81PcAsM8f/hT38gLln8gRskl8sKzRvzYNvOb3fdFDgPfmC/JvyAlynfTvOkK+MvA1gv+Zr35Aeg81fCmb5wa6f9bQryvyuUCQ3/U8FrTbTb+jsjUNWBf+Hig/dNPeyrQXXHlvcmf1saDvrAn2YjR0dW+e2R6y0lmQgtOE55OWgHn6VNHEABaWn0CaqIQou+37wZFz2mPyyxAB1PQpqinXWgy8Driv+CB0f8A/o/2J/dYg7kMqQ99HMqcM5F0bbC5LRevznzLhC+3H8R2ZGPWN08aqO26RboHaIE0pRrl3RnLX4NgN1gwM38E+deTt6g/0b+wGlqF7h8uhqM+VNXXokFtk6g2v3BG5p0jcrR+Dj2X7A+wmyAvL/soqVxXde3MPYd6bGZUwxjdeQ0//6tDBuIUtLvZc08tAE+duaszptyGdzx9W/KG20wIVPgjtFNkVlF+ge9TDQ4B9K6ffrvKTs53i/OF0bKuhb+ueleHDAQ3ZWSMe9rDJOZwS6gyiZ22o/zgJXL+QFbjgBi0TZO4gu+OckKNcb+/o36PvbL6oLyFDm88ovyKvHJ2y6qeyjhrt9OE4DsZbIHMdfpqxeQ8hb5G/oH2D7JM4hp1eYC9jA8ZZ4nIcn/WQU5c7qTbzbPMAOmBgdzHa+DKkLj+5bZsOiBsshl7aUI/j8AdhrOkaa8wHxM4BloD/V/DfO+2myt0W++jvfX41fe51iwY36H2DPndxTJ+Racc30Y695hHqr7C/wfX/kD8gx8h24qd5+hVEgi8Z/E3hTGw5dIe2CPWvyOTzO3KCbFKCwnfkDJkyLoMd9NnOfqNrOEdo+4Rch0ECOTelbkNep/2l0/cJfnCcndXH9d+jTv+Z/oOcsGAl6vgkjpdBcYHs3nc+9salg8k/I/smb2AY8C8zqC8x6K+NjYGvjecN/yLknz5zhd4z8liT15zLOAHCrgBE7MpDhg8NN/Ghckzo4AOzSTrp0gAUH5soQ9bVd9Xjo2P5P5R3BS7Uz/RtW24ZduICW2tXrqxepF8kC7kl8unnvGBMt+nZtoPOn65ARZ2yfIhXJsisCmyU3qO0WSDPJtppMjfqxMY5Vpy0tvqaPtfO7i1j59rXLo/7ohaT+bOF7sUbqoXtvlX5aa5pWkGBP7Q+835pOlhA8ssr+fKlYFw+nLkOv+CT/hMu+wIZ0+T2LUwHr9Cn/Q3LJelQ0l63+SMEtzWEOQ6/r0hryA4qAp/2GPAGuQ6LB8jfOg5SlzYupRQCN9CvM86nAmPUL0y5zW1BZ4Q2+pciX0r0q8kaPUF+X2E0q+gLpiuU476ixQsGUk+OpLhxMw/b5odjBd1NE32Ml+LhmkEnaqI3UdkXzOuHM7fMqTOIZGB2i/bI6VtY9RuU75B/R17m7SmuL9DfQH+dtxVeIHMLGfrCtY1zoT2u35E3yAmySZkp5NcHp37WD9sH9H+E/RhXYz9C2aQUheM4uaxpr3PNIOSOn9ZRzGUyXF39QnX4tscc3qHzHnmFHCGbxDlukR8gl5lGc83n9R76Cdo+Iy9NX36l7nfIbZz2quof6GS208auFJS/os3V4ZH/Hu03+RqZeyjO9TNcmb8jb/K5oHhKLr/U9FAWNm9Qv0Pm2kfITCnyS35NcJ1HAowdsk9a+xLCYPxEe0q+dvrWOzl4Xkiajgv1hWXiW1N9ykP/zrIxqqLPfKUjAiIwbgLXPO7LgI5b13ULhO4nohamBlXlp6eNx4grS2eFCLOw6nWL27qCkhMBERCBaxPo8rjvtsFkzBa3gUqhKLewY0y+gYJHAXZi0NrYDZfKCI48vuL41B1VYlDmEcaonJazIiAC8ySAB1aEfJZCJXHm5H8rSx9fC+z4HvlxFzbGFPtwk44IiMB4CXR53Dc0hcehB+xovD12A/umthBRinY+Xkd+GH+L8bOmPkheBERABIYmMMoghQd2AlBFD+2h+fmMxzd8fJJ71Gds+HJgoFISAREQARHokgAC1BK58JffuhynS1vw1yT6vfCxDb3COaPd98jvzXGpcTLgqxc7H97SEQERCIPAqHZSeHgugW2HPNaH1dbni/983mVz9j3yy8AxRR5N8mE3msnJUREQgUICowlSeFDfYQY/kcse1oUTDKyRv0Dnky69xbjyMQodX388h5OaCIiACEyMAIJTjPwT+WIKdeq546++/kH/9cLkvY786A/slh0jXhhy+G5fftITAREYL4Fgd1J4BB6DE9DyeG85XsQnz71emACHCBaYq1Jc1Xmhb3uhX90iIAIicDUCXf4yb9eTOMDgIMEJgSDBWJeO1NrOb+NpYFVDb8Ggju9s0hqyrgiDZ+I2BlhPA/RJLomACMyZAB68j8i1ky8rDLCuPYif4LOvb0PoYUq1jlP9pt6ZFnfUSiIgAjMj0NlxHx5FfMtsV5Dr7ATKsD+ggzuqsafQX1DwOooceFGygcfTcCIgAgEQ6PK4j8EkLphThMDF/5aicbChDnS/wGbQO5GCOdtNmecxnG2j7/IWA4TO+M++Ici+CIhAeAQ620lhalnJ9CK035X0XWzGA34DofSiYLgCoe+ifsk/QGzCRXj0LAvcP7knAiLQA4HOghQedFmFf/fYEUUV/Ze6bi8JBNy/Cdg327XQg2lmO6uyCIjAPAh0FqRyXPsKbI8VfZVdeQB8qBQKs5N/YSIL07Vzr+BnipbsvDWcWu5fOA7JExEQgUEIDBmk+GJF3GJWT9DNWuhfQzX03YnLJNQXKMa27i5X1UVABDwJdB2k/rzgh/eX8/gkfYBtvkQxlsQXJrZjcTb3M1R/9yPjKHdFQAQ6ItB1kEov+MU3/dYXZEq784d+qA9S1++x+HnyG3wzVEL0+4+TkyqIgAjMikCnQQoPubQGvc8tX6LgbupQY5xri4R6dHaJS4hHlOklp9UvAiIwTQJd/p6UIbRHYWkqBdcF2u6Rbwv6Ljbx0z6CHAMAbYSa+HthmY9zeQBPfHRtHYy/tut1y9DbwocM8lFdnb7l4FPa9xiyLwIiMBMCeMDV/VNGcRskGOcV+Sz52oORrv8sUtLCl7uzSflXVi18qLuG/t7V19z5zkN6IiAC4yfQ6XFfjuNHTSzer6Tn9r12YjV9ayN2wCf/TQsDn1ro2qq/25WG5ZCOKuveTw2nKHEREIExEOg8SPG4qObEl/gwfVdT9o1YfgRUd6w3+j02bHxtg0cE3aWvvqO3cuq1q2CbQTitrdCvYIhr3O+MZV0EROBEoPMglVuu+2C5x4N5cfKmeYG7qUNztV41XlpYX7XQdVUXYNvGXpt5uL741rkr3fsqS08ERGD8BPoKUt9rollAzvvYDw+wA/Qfao41hNi+5UO1q6M+M1fvIz/MYwMj5HvNtL3m4BpbBETg+gT6ClJNHi4JPvEvfVHgYfoE3b2vfsd63t/lgEEEX7w5lMxjVdJet3lbV7AnubofdnoaXmZFQASuTaCXIJXvcJo84Lx3UznAL9cGifEPyE3m7Lq8chs6qPPIb9nCjnfQbTGmUeVRXxuexo6uIiACIybQx+9JGRz8FFz3wRvjYfq3UfS9dmHDd+xc71uLabQJJlVu06esSiDQPgWoQBdGbonAkAR+7WswPBgXsP1XX/Zld/IEbrCTSic/S01QBESgkkAvx30cMT/y21SOrk4RKCbAP86bFnepVQREYE4EegtSOcQQXmOe03pOZa7X/C5sKgw1DxGYBIHejvsMHRz7/US5r+9bzDC6TovA/+Y78WnNSrMRARFoTKDvnRQd0qfixssya4WNAtSs11+TF4EzAr3vpDgadlOvuEQsK4nABQLvEKSyCzLqFgERmAmBIXZSRKnvpmZyQ7WcJndRWUsbUhcBEZgQgaF2UnwdnbspXpVEoIzADYJUWtapdhEQgfkRGGQnhQfPAWj13dT87q8mM04VoJrgkqwIzIPAIDsposx/uZdv+kWsK4mAQ+AdglTmtKkqAiIwcwKD7KTION9NPcyct6ZfTEDfRRVzUasIzJ7AYDspQ1q/N2VI6JoT4FHwe+2idD+IgAgUERhsJ2UN/sUqqygCXxWgdBOIgAiUERh8J0VHsJt6xiVhWWnWBDIEqHezJqDJi4AIVBK4VpDSK+mVyzKbzhsEqXQ2s9VERUAEGhO4xnGfeYnitrG3UpgSgScFqCktp+YiAv0QuMpOykwFx37fUF6Zuq6zIZBhpnxZgi9NKImACIhAKYFrBykd+5UuzaQ7dMw36eXV5ESgOwJXOe4z7uefpHXsZ4DM4/qAdU/nMVXNUgREoC2Bq+6kjPM49ntE+c7UdZ0sgRQB6mays9PEREAEOicQRJDirPRLvp2vbWgG+f3TOwQpfQ8V2srIHxEImMBVj/scLvyErQeYA2VC1RsFqAmtpqYiAgMRCCZI5Q8wBaqBFn7gYW6xvvuBx9RwIiACEyAQTJAiy/xBpj+bNIEby5oCfx9qY9VVFAEREIHaBIL5Tsr2GN9PJajzTycpjZvABgHqdtxTkPciIALXJBBkkCIQvfF3zduik7H1Jl8nGGVEBOZNINggxWVBoNIfoh3n/cnvn26wi9KLMONcP3ktAsEQCDpIkZICVTD3Sl1HFKDqkpKcCIjARQLBBynOQIHq4jqGIqAAFcpKyA8RmAiBoN7uK2Oaf/n+VNav9iAIbOGFjviCWAo5IQLTITCKnZTBjR1VgrLe+jNAwrnqLb5w1kKeiMCkCIxiJ2WIY0e1QfkW+WDadL06gYd8p3t1R+SACIjA9AiMaidl8GNHtUR5h7wwbboOToAfFL7kHxwGH1wDioAIzIPAKIMUlwaBigGKgYoBS2lYAhmG+4gAtR92WI0mAiIwNwKjOu6zFwcPyAPye7Q92e0q904gxQj8X3UVoHpHrQFEQARGG6TM0uFhyb/19xH5YNp07Y0Aj/dukMW6N8QyLAIiYBMY7XGfPQmW8+M/vvm3cvtUb02Au6ZbBCdelURABERgMAKj30kZUvx0j8wdFXdW+qRvwLS/PoCrjvfac5QFERABDwKT2UnZc9euyqbhXU6hyeM97Z68EUpRBESgLYFJBikDBcEqRplHgBGyUj0C3IUyOG3qiUtKBERABPojMOkgZbAhWCUoPyIvTJuubwgwOH1F5n9SyLKSCIiACFydwCyCFCnnR4B3KH5GVrAilH/SBsUHBKfsnyaVREAEROD6BGYTpAxqBStD4vhyyRY1BacTEhVEQARCIzC7IGUvQH4MeI+2yG6feDnD/F6Qdaw38YXW9ERgCgRmHaTMAiJYxSh/Qk6Qp5pSTOwFR3qbqU5Q8xIBEZgeAQUpa03zo8AVmvi91dLqGmsxg+N8GWKr75vGuoTyWwTmTUBBqmT9EbAidDFgfcivuIwi7eElj/NSBCaWlURABERgtAQUpGosXb7DiiHKgMVrSLusDP6kyD+QuWM64KokAiIgApMgoCDlsYx50GKgipF/Q46QhwhcGcZhZkDaI3O3pKAEEEoiIALTJKAg1eG6IngxUC2QzfU/KEfIJpl2U7evqVU5oPxHXj+2Ixgdr3mbLiIgAiIgAiIgAiIgAiIgAiIgAiIgAiIgAiIgAiIgAiIgAiIgAiIgAiIgAiIgAh0Q+H+1KtMi9Jz1xQAAAABJRU5ErkJggg==" transform="translate(-7 -1)" fill="none" fill-rule="evenodd"/></svg> \ No newline at end of file diff --git a/hypha/static_src/src/images/usagm.png b/hypha/static_src/src/images/usagm.png deleted file mode 100644 index 3edb74116391bdbae3b06865ace60dc9c307320b..0000000000000000000000000000000000000000 Binary files a/hypha/static_src/src/images/usagm.png and /dev/null differ diff --git a/hypha/static_src/src/javascript/apply/edit-comment.js b/hypha/static_src/src/javascript/apply/edit-comment.js index ae1270d75254bd5d218730da36eb31e722cd9d87..d092bf5fe0583d5af074890f378058f5aa47a903 100644 --- a/hypha/static_src/src/javascript/apply/edit-comment.js +++ b/hypha/static_src/src/javascript/apply/edit-comment.js @@ -3,10 +3,7 @@ const comment = ".js-comment"; const pageDown = ".js-pagedown"; - const feedMeta = ".js-feed-meta"; const editBlock = ".js-edit-block"; - const lastEdited = ".js-last-edited"; - const commentVisibility = ".js-comment-visibility"; const editButton = ".js-edit-comment"; const feedContent = ".js-feed-content"; const commentError = ".js-comment-error"; @@ -22,10 +19,6 @@ const editBlockWrapper = $(this).closest(feedContent).find(editBlock); const commentWrapper = $(this).closest(feedContent).find(comment); const commentContents = $(commentWrapper).attr("data-comment"); - const visibilityOptions = $.parseJSON( - $(commentWrapper).attr("data-visibility-options") - ); - const currentVisibility = $(commentWrapper).attr("data-visibility"); // hide the edit link and original comment $(this).parent().hide(); @@ -36,24 +29,9 @@ <div id="wmd-button-bar-edit-comment" class="wmd-button-bar"></div> <textarea id="wmd-input-edit-comment" class="wmd-input" rows="10">${commentContents}</textarea> <div id="wmd-preview-edit-comment" class="wmd-preview"></div> - <br> - <div>Visible to:</div> </div> `; - const radioButtonsDiv = '<div id="edit-comment-visibility"></div>'; - let key = ""; - let label = ""; - let radioButtons = ""; - - $.each(visibilityOptions, function (idx, value) { - key = value[0]; - label = value[1]; - radioButtons += ` - <input type="radio" name='radio-visibility' value=${key} id='visible-to-${key}' /> - <label for="visible-to-${key}">${label}</label><br>`; - }); - const buttons = ` <div class="wrapper--outer-space-medium"> <button class="button button--primary js-submit-edit" type="submit">Update</button> @@ -62,10 +40,8 @@ `; // add the comment to the editor - const markupEditor = $(markup).append(radioButtonsDiv).append(buttons); + const markupEditor = $(markup).append(buttons); $(editBlockWrapper).append(markupEditor); - $("#edit-comment-visibility").html(radioButtons); - $(`#visible-to-${currentVisibility}`).prop("checked", true); // ensure current visibility is checked // run the editor initEditor(); @@ -88,9 +64,7 @@ .closest(pageDown) .find(".wmd-preview") .html(); - const editedVisibility = $( - 'input[name="radio-visibility"]:checked' - ).val(); + // const editedVisibility = $('input[name="radio-visibility"]:checked').val(); const commentMD = $(this).closest(editBlock).find("textarea").val(); const editUrl = $(commentContainer).attr("data-edit-url"); @@ -102,7 +76,7 @@ }, body: JSON.stringify({ message: editedComment, - visibility: editedVisibility, + // visibility: editedVisibility }), }) .then((response) => { @@ -124,8 +98,6 @@ data.edit_url, commentMD ); - updateVisibility(this, data.visibility); - updateLastEdited(this, data.edited); showComment(this); showEditButton(this); hidePageDownEditor(this); @@ -164,12 +136,7 @@ }; const showEditButton = (el) => { - $(el) - .closest(editBlock) - .siblings(feedMeta) - .find(editButton) - .parent() - .show(); + $(editButton).parent().show(); }; const hidePageDownEditor = (el) => { @@ -180,44 +147,6 @@ $(el).closest(editBlock).siblings(comment).show(); }; - const updateVisibility = (el, visibility) => { - if (visibility !== "all") { - $(el) - .closest(feedContent) - .find(commentVisibility) - .parent() - .attr("hidden", false); - $(el).closest(feedContent).find(commentVisibility).text(visibility); - } else { - $(el) - .closest(feedContent) - .find(commentVisibility) - .parent() - .attr("hidden", true); - $(el) - .closest(feedContent) - .find(commentVisibility) - .html(`${visibility}`); - } - }; - - const updateLastEdited = (el, date) => { - const parsedDate = new Date(date).toISOString().split("T")[0]; - const time = new Date(date).toLocaleTimeString([], { - hour: "2-digit", - minute: "2-digit", - }); - $(el) - .closest(feedContent) - .find(lastEdited) - .parent() - .attr("hidden", false); - $(el) - .closest(feedContent) - .find(lastEdited) - .html(`${parsedDate} ${time}`); - }; - const updateComment = ( el, id, @@ -240,12 +169,4 @@ }; const hideError = () => $(commentError).remove(); - - window.addEventListener("beforeunload", (e) => { - if ($(submitEditButton).length) { - e.preventDefault(); - e.returnValue = - "It looks like you're still editing a comment. Are you sure you want to leave?"; - } - }); })(jQuery); diff --git a/hypha/static_src/src/javascript/apply/report-frequency.js b/hypha/static_src/src/javascript/apply/report-frequency.js deleted file mode 100644 index 218e9528a38a4a728af903448b0b677eb169c8c5..0000000000000000000000000000000000000000 --- a/hypha/static_src/src/javascript/apply/report-frequency.js +++ /dev/null @@ -1,24 +0,0 @@ -(function ($) { - "use strict"; - - const var_repeat = $("#id_does_not_repeat"); - - if (!var_repeat.length) { - return; - } - - if (var_repeat[0].checked) { - $(".form__group--report-every").hide(); - $(".form__group--schedule").hide(); - } - - var_repeat.addEventListener("click", function () { - if (var_repeat.checked) { - $(".form__group--report-every").hide(); - $(".form__group--schedule").hide(); - } else { - $(".form__group--report-every").show(); - $(".form__group--schedule").show(); - } - }); -})(jQuery); diff --git a/hypha/static_src/src/javascript/apply/vendor/alpine-focus.min.js b/hypha/static_src/src/javascript/apply/vendor/alpine-focus.min.js index 86e4f5a8a7ce203bfd77551acd452e823b07eb65..b2efb3af1ec998a8fc9952ec6895568e0aa6bfb2 100644 --- a/hypha/static_src/src/javascript/apply/vendor/alpine-focus.min.js +++ b/hypha/static_src/src/javascript/apply/vendor/alpine-focus.min.js @@ -1,9 +1,15 @@ -(()=>{var _=["input","select","textarea","a[href]","button","[tabindex]","audio[controls]","video[controls]",'[contenteditable]:not([contenteditable="false"])',"details>summary:first-of-type","details"],G=_.join(","),C=typeof Element=="undefined"?function(){}:Element.prototype.matches||Element.prototype.msMatchesSelector||Element.prototype.webkitMatchesSelector,M=function(e,t,a){var u=Array.prototype.slice.apply(e.querySelectorAll(G));return t&&C.call(e,G)&&u.unshift(e),u=u.filter(a),u},Z=function(e){return e.contentEditable==="true"},q=function(e){var t=parseInt(e.getAttribute("tabindex"),10);return isNaN(t)?Z(e)||(e.nodeName==="AUDIO"||e.nodeName==="VIDEO"||e.nodeName==="DETAILS")&&e.getAttribute("tabindex")===null?0:e.tabIndex:t},$=function(e,t){return e.tabIndex===t.tabIndex?e.documentOrder-t.documentOrder:e.tabIndex-t.tabIndex},L=function(e){return e.tagName==="INPUT"},ee=function(e){return L(e)&&e.type==="hidden"},te=function(e){var t=e.tagName==="DETAILS"&&Array.prototype.slice.apply(e.children).some(function(a){return a.tagName==="SUMMARY"});return t},re=function(e,t){for(var a=0;a<e.length;a++)if(e[a].checked&&e[a].form===t)return e[a]},ae=function(e){if(!e.name)return!0;var t=e.form||e.ownerDocument,a=function(l){return t.querySelectorAll('input[type="radio"][name="'+l+'"]')},u;if(typeof window!="undefined"&&typeof window.CSS!="undefined"&&typeof window.CSS.escape=="function")u=a(window.CSS.escape(e.name));else try{u=a(e.name)}catch(s){return console.error("Looks like you have a radio button with a name attribute containing invalid CSS selector characters and need the CSS.escape polyfill: %s",s.message),!1}var r=re(u,e.form);return!r||r===e},ne=function(e){return L(e)&&e.type==="radio"},ie=function(e){return ne(e)&&!ae(e)},ue=function(e,t){if(getComputedStyle(e).visibility==="hidden")return!0;var a=C.call(e,"details>summary:first-of-type"),u=a?e.parentElement:e;if(C.call(u,"details:not([open]) *"))return!0;if(!t||t==="full")for(;e;){if(getComputedStyle(e).display==="none")return!0;e=e.parentElement}else if(t==="non-zero-area"){var r=e.getBoundingClientRect(),s=r.width,l=r.height;return s===0&&l===0}return!1},oe=function(e){if(L(e)||e.tagName==="SELECT"||e.tagName==="TEXTAREA"||e.tagName==="BUTTON")for(var t=e.parentElement;t;){if(t.tagName==="FIELDSET"&&t.disabled){for(var a=0;a<t.children.length;a++){var u=t.children.item(a);if(u.tagName==="LEGEND")return!u.contains(e)}return!0}t=t.parentElement}return!1},R=function(e,t){return!(t.disabled||ee(t)||ue(t,e.displayCheck)||te(t)||oe(t))},se=function(e,t){return!(!R(e,t)||ie(t)||q(t)<0)},W=function(e,t){t=t||{};var a=[],u=[],r=M(e,t.includeContainer,se.bind(null,t));r.forEach(function(l,p){var b=q(l);b===0?a.push(l):u.push({documentOrder:p,tabIndex:b,node:l})});var s=u.sort($).map(function(l){return l.node}).concat(a);return s},B=function(e,t){t=t||{};var a=M(e,t.includeContainer,R.bind(null,t));return a};var ce=_.concat("iframe").join(","),O=function(e,t){if(t=t||{},!e)throw new Error("No node provided");return C.call(e,ce)===!1?!1:R(t,e)};function H(i,e){var t=Object.keys(i);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(i);e&&(a=a.filter(function(u){return Object.getOwnPropertyDescriptor(i,u).enumerable})),t.push.apply(t,a)}return t}function fe(i){for(var e=1;e<arguments.length;e++){var t=arguments[e]!=null?arguments[e]:{};e%2?H(Object(t),!0).forEach(function(a){le(i,a,t[a])}):Object.getOwnPropertyDescriptors?Object.defineProperties(i,Object.getOwnPropertyDescriptors(t)):H(Object(t)).forEach(function(a){Object.defineProperty(i,a,Object.getOwnPropertyDescriptor(t,a))})}return i}function le(i,e,t){return e in i?Object.defineProperty(i,e,{value:t,enumerable:!0,configurable:!0,writable:!0}):i[e]=t,i}var U=function(){var i=[];return{activateTrap:function(t){if(i.length>0){var a=i[i.length-1];a!==t&&a.pause()}var u=i.indexOf(t);u===-1||i.splice(u,1),i.push(t)},deactivateTrap:function(t){var a=i.indexOf(t);a!==-1&&i.splice(a,1),i.length>0&&i[i.length-1].unpause()}}}(),de=function(e){return e.tagName&&e.tagName.toLowerCase()==="input"&&typeof e.select=="function"},be=function(e){return e.key==="Escape"||e.key==="Esc"||e.keyCode===27},ve=function(e){return e.key==="Tab"||e.keyCode===9},K=function(e){return setTimeout(e,0)},x=function(e,t){var a=-1;return e.every(function(u,r){return t(u)?(a=r,!1):!0}),a},A=function(e){for(var t=arguments.length,a=new Array(t>1?t-1:0),u=1;u<t;u++)a[u-1]=arguments[u];return typeof e=="function"?e.apply(void 0,a):e},V=function(e,t){var a=document,u=fe({returnFocusOnDeactivate:!0,escapeDeactivates:!0,delayInitialFocus:!0},t),r={containers:[],tabbableGroups:[],nodeFocusedBeforeActivation:null,mostRecentlyFocusedNode:null,active:!1,paused:!1,delayInitialFocusTimer:void 0},s,l=function(n,o,c){return n&&n[o]!==void 0?n[o]:u[c||o]},p=function(n){return r.containers.some(function(o){return o.contains(n)})},b=function(n){var o=u[n];if(!o)return null;var c=o;if(typeof o=="string"&&(c=a.querySelector(o),!c))throw new Error("`".concat(n,"` refers to no known node"));if(typeof o=="function"&&(c=o(),!c))throw new Error("`".concat(n,"` did not return a node"));return c},v=function(){var n;if(l({},"initialFocus")===!1)return!1;if(b("initialFocus")!==null)n=b("initialFocus");else if(p(a.activeElement))n=a.activeElement;else{var o=r.tabbableGroups[0],c=o&&o.firstTabbableNode;n=c||b("fallbackFocus")}if(!n)throw new Error("Your focus-trap needs to have at least one focusable element");return n},h=function(){if(r.tabbableGroups=r.containers.map(function(n){var o=W(n);if(o.length>0)return{container:n,firstTabbableNode:o[0],lastTabbableNode:o[o.length-1]}}).filter(function(n){return!!n}),r.tabbableGroups.length<=0&&!b("fallbackFocus"))throw new Error("Your focus-trap must have at least one container with at least one tabbable node in it at all times")},m=function f(n){if(n!==!1&&n!==a.activeElement){if(!n||!n.focus){f(v());return}n.focus({preventScroll:!!u.preventScroll}),r.mostRecentlyFocusedNode=n,de(n)&&n.select()}},S=function(n){var o=b("setReturnFocus");return o||n},g=function(n){if(!p(n.target)){if(A(u.clickOutsideDeactivates,n)){s.deactivate({returnFocus:u.returnFocusOnDeactivate&&!O(n.target)});return}A(u.allowOutsideClick,n)||n.preventDefault()}},w=function(n){var o=p(n.target);o||n.target instanceof Document?o&&(r.mostRecentlyFocusedNode=n.target):(n.stopImmediatePropagation(),m(r.mostRecentlyFocusedNode||v()))},k=function(n){h();var o=null;if(r.tabbableGroups.length>0){var c=x(r.tabbableGroups,function(N){var D=N.container;return D.contains(n.target)});if(c<0)n.shiftKey?o=r.tabbableGroups[r.tabbableGroups.length-1].lastTabbableNode:o=r.tabbableGroups[0].firstTabbableNode;else if(n.shiftKey){var d=x(r.tabbableGroups,function(N){var D=N.firstTabbableNode;return n.target===D});if(d<0&&r.tabbableGroups[c].container===n.target&&(d=c),d>=0){var y=d===0?r.tabbableGroups.length-1:d-1,F=r.tabbableGroups[y];o=F.lastTabbableNode}}else{var T=x(r.tabbableGroups,function(N){var D=N.lastTabbableNode;return n.target===D});if(T<0&&r.tabbableGroups[c].container===n.target&&(T=c),T>=0){var X=T===r.tabbableGroups.length-1?0:T+1,J=r.tabbableGroups[X];o=J.firstTabbableNode}}}else o=b("fallbackFocus");o&&(n.preventDefault(),m(o))},E=function(n){if(be(n)&&A(u.escapeDeactivates)!==!1){n.preventDefault(),s.deactivate();return}if(ve(n)){k(n);return}},I=function(n){A(u.clickOutsideDeactivates,n)||p(n.target)||A(u.allowOutsideClick,n)||(n.preventDefault(),n.stopImmediatePropagation())},P=function(){if(!!r.active)return U.activateTrap(s),r.delayInitialFocusTimer=u.delayInitialFocus?K(function(){m(v())}):m(v()),a.addEventListener("focusin",w,!0),a.addEventListener("mousedown",g,{capture:!0,passive:!1}),a.addEventListener("touchstart",g,{capture:!0,passive:!1}),a.addEventListener("click",I,{capture:!0,passive:!1}),a.addEventListener("keydown",E,{capture:!0,passive:!1}),s},j=function(){if(!!r.active)return a.removeEventListener("focusin",w,!0),a.removeEventListener("mousedown",g,!0),a.removeEventListener("touchstart",g,!0),a.removeEventListener("click",I,!0),a.removeEventListener("keydown",E,!0),s};return s={activate:function(n){if(r.active)return this;var o=l(n,"onActivate"),c=l(n,"onPostActivate"),d=l(n,"checkCanFocusTrap");d||h(),r.active=!0,r.paused=!1,r.nodeFocusedBeforeActivation=a.activeElement,o&&o();var y=function(){d&&h(),P(),c&&c()};return d?(d(r.containers.concat()).then(y,y),this):(y(),this)},deactivate:function(n){if(!r.active)return this;clearTimeout(r.delayInitialFocusTimer),r.delayInitialFocusTimer=void 0,j(),r.active=!1,r.paused=!1,U.deactivateTrap(s);var o=l(n,"onDeactivate"),c=l(n,"onPostDeactivate"),d=l(n,"checkCanReturnFocus");o&&o();var y=l(n,"returnFocus","returnFocusOnDeactivate"),F=function(){K(function(){y&&m(S(r.nodeFocusedBeforeActivation)),c&&c()})};return y&&d?(d(S(r.nodeFocusedBeforeActivation)).then(F,F),this):(F(),this)},pause:function(){return r.paused||!r.active?this:(r.paused=!0,j(),this)},unpause:function(){return!r.paused||!r.active?this:(r.paused=!1,h(),P(),this)},updateContainerElements:function(n){var o=[].concat(n).filter(Boolean);return r.containers=o.map(function(c){return typeof c=="string"?a.querySelector(c):c}),r.active&&h(),this}},s.updateContainerElements(e),s};function z(i){let e,t;window.addEventListener("focusin",()=>{e=t,t=document.activeElement}),i.magic("focus",a=>{let u=a;return{__noscroll:!1,__wrapAround:!1,within(r){return u=r,this},withoutScrolling(){return this.__noscroll=!0,this},noscroll(){return this.__noscroll=!0,this},withWrapAround(){return this.__wrapAround=!0,this},wrap(){return this.withWrapAround()},focusable(r){return O(r)},previouslyFocused(){return e},lastFocused(){return e},focused(){return t},focusables(){return Array.isArray(u)?u:B(u,{displayCheck:"none"})},all(){return this.focusables()},isFirst(r){let s=this.all();return s[0]&&s[0].isSameNode(r)},isLast(r){let s=this.all();return s.length&&s.slice(-1)[0].isSameNode(r)},getFirst(){return this.all()[0]},getLast(){return this.all().slice(-1)[0]},getNext(){let r=this.all(),s=document.activeElement;if(r.indexOf(s)!==-1)return this.__wrapAround&&r.indexOf(s)===r.length-1?r[0]:r[r.indexOf(s)+1]},getPrevious(){let r=this.all(),s=document.activeElement;if(r.indexOf(s)!==-1)return this.__wrapAround&&r.indexOf(s)===0?r.slice(-1)[0]:r[r.indexOf(s)-1]},first(){this.focus(this.getFirst())},last(){this.focus(this.getLast())},next(){this.focus(this.getNext())},previous(){this.focus(this.getPrevious())},prev(){return this.previous()},focus(r){!r||setTimeout(()=>{r.hasAttribute("tabindex")||r.setAttribute("tabindex","0"),r.focus({preventScroll:this._noscroll})})}}}),i.directive("trap",i.skipDuringClone((a,{expression:u,modifiers:r},{effect:s,evaluateLater:l,cleanup:p})=>{let b=l(u),v=!1,h={escapeDeactivates:!1,allowOutsideClick:!0,fallbackFocus:()=>a},m=a.querySelector("[autofocus]");m&&(h.initialFocus=m);let S=V(a,h),g=()=>{},w=()=>{},k=()=>{g(),g=()=>{},w(),w=()=>{},S.deactivate({returnFocus:!r.includes("noreturn")})};s(()=>b(E=>{v!==E&&(E&&!v&&setTimeout(()=>{r.includes("inert")&&(g=Y(a)),r.includes("noscroll")&&(w=pe()),S.activate()}),!E&&v&&k(),v=!!E)})),p(k)},(a,{expression:u,modifiers:r},{evaluate:s})=>{r.includes("inert")&&s(u)&&Y(a)}))}function Y(i){let e=[];return Q(i,t=>{let a=t.hasAttribute("aria-hidden");t.setAttribute("aria-hidden","true"),e.push(()=>a||t.removeAttribute("aria-hidden"))}),()=>{for(;e.length;)e.pop()()}}function Q(i,e){i.isSameNode(document.body)||!i.parentNode||Array.from(i.parentNode.children).forEach(t=>{t.isSameNode(i)?Q(i.parentNode,e):e(t)})}function pe(){let i=document.documentElement.style.overflow,e=document.documentElement.style.paddingRight,t=window.innerWidth-document.documentElement.clientWidth;return document.documentElement.style.overflow="hidden",document.documentElement.style.paddingRight=`${t}px`,()=>{document.documentElement.style.overflow=i,document.documentElement.style.paddingRight=e}}document.addEventListener("alpine:init",()=>{window.Alpine.plugin(z)});})(); -/*! -* focus-trap 6.6.1 -* @license MIT, https://github.com/focus-trap/focus-trap/blob/master/LICENSE -*/ -/*! -* tabbable 5.2.1 -* @license MIT, https://github.com/focus-trap/tabbable/blob/master/LICENSE +(()=>{var K=["input","select","textarea","a[href]","button","[tabindex]:not(slot)","audio[controls]","video[controls]",'[contenteditable]:not([contenteditable="false"])',"details>summary:first-of-type","details"],I=K.join(","),V=typeof Element>"u",N=V?function(){}:Element.prototype.matches||Element.prototype.msMatchesSelector||Element.prototype.webkitMatchesSelector,G=!V&&Element.prototype.getRootNode?function(i){return i.getRootNode()}:function(i){return i.ownerDocument},_=function(e,t,a){var n=Array.prototype.slice.apply(e.querySelectorAll(I));return t&&N.call(e,I)&&n.unshift(e),n=n.filter(a),n},$=function i(e,t,a){for(var n=[],r=Array.from(e);r.length;){var s=r.shift();if(s.tagName==="SLOT"){var l=s.assignedElements(),m=l.length?l:s.children,h=i(m,!0,a);a.flatten?n.push.apply(n,h):n.push({scope:s,candidates:h})}else{var v=N.call(s,I);v&&a.filter(s)&&(t||!e.includes(s))&&n.push(s);var p=s.shadowRoot||typeof a.getShadowRoot=="function"&&a.getShadowRoot(s),y=!a.shadowRootFilter||a.shadowRootFilter(s);if(p&&y){var T=i(p===!0?s.children:p.children,!0,a);a.flatten?n.push.apply(n,T):n.push({scope:s,candidates:T})}else r.unshift.apply(r,s.children)}}return n},Y=function(e,t){return e.tabIndex<0&&(t||/^(AUDIO|VIDEO|DETAILS)$/.test(e.tagName)||e.isContentEditable)&&isNaN(parseInt(e.getAttribute("tabindex"),10))?0:e.tabIndex},se=function(e,t){return e.tabIndex===t.tabIndex?e.documentOrder-t.documentOrder:e.tabIndex-t.tabIndex},Z=function(e){return e.tagName==="INPUT"},ce=function(e){return Z(e)&&e.type==="hidden"},le=function(e){var t=e.tagName==="DETAILS"&&Array.prototype.slice.apply(e.children).some(function(a){return a.tagName==="SUMMARY"});return t},fe=function(e,t){for(var a=0;a<e.length;a++)if(e[a].checked&&e[a].form===t)return e[a]},de=function(e){if(!e.name)return!0;var t=e.form||G(e),a=function(l){return t.querySelectorAll('input[type="radio"][name="'+l+'"]')},n;if(typeof window<"u"&&typeof window.CSS<"u"&&typeof window.CSS.escape=="function")n=a(window.CSS.escape(e.name));else try{n=a(e.name)}catch(s){return console.error("Looks like you have a radio button with a name attribute containing invalid CSS selector characters and need the CSS.escape polyfill: %s",s.message),!1}var r=fe(n,e.form);return!r||r===e},be=function(e){return Z(e)&&e.type==="radio"},ve=function(e){return be(e)&&!de(e)},W=function(e){var t=e.getBoundingClientRect(),a=t.width,n=t.height;return a===0&&n===0},he=function(e,t){var a=t.displayCheck,n=t.getShadowRoot;if(getComputedStyle(e).visibility==="hidden")return!0;var r=N.call(e,"details>summary:first-of-type"),s=r?e.parentElement:e;if(N.call(s,"details:not([open]) *"))return!0;var l=G(e).host,m=l?.ownerDocument.contains(l)||e.ownerDocument.contains(e);if(!a||a==="full"){if(typeof n=="function"){for(var h=e;e;){var v=e.parentElement,p=G(e);if(v&&!v.shadowRoot&&n(v)===!0)return W(e);e.assignedSlot?e=e.assignedSlot:!v&&p!==e.ownerDocument?e=p.host:e=v}e=h}if(m)return!e.getClientRects().length}else if(a==="non-zero-area")return W(e);return!1},pe=function(e){if(/^(INPUT|BUTTON|SELECT|TEXTAREA)$/.test(e.tagName))for(var t=e.parentElement;t;){if(t.tagName==="FIELDSET"&&t.disabled){for(var a=0;a<t.children.length;a++){var n=t.children.item(a);if(n.tagName==="LEGEND")return N.call(t,"fieldset[disabled] *")?!0:!n.contains(e)}return!0}t=t.parentElement}return!1},x=function(e,t){return!(t.disabled||ce(t)||he(t,e)||le(t)||pe(t))},M=function(e,t){return!(ve(t)||Y(t)<0||!x(e,t))},ge=function(e){var t=parseInt(e.getAttribute("tabindex"),10);return!!(isNaN(t)||t>=0)},me=function i(e){var t=[],a=[];return e.forEach(function(n,r){var s=!!n.scope,l=s?n.scope:n,m=Y(l,s),h=s?i(n.candidates):l;m===0?s?t.push.apply(t,h):t.push(l):a.push({documentOrder:r,tabIndex:m,item:n,isScope:s,content:h})}),a.sort(se).reduce(function(n,r){return r.isScope?n.push.apply(n,r.content):n.push(r.content),n},[]).concat(t)},z=function(e,t){t=t||{};var a;return t.getShadowRoot?a=$([e],t.includeContainer,{filter:M.bind(null,t),flatten:!1,getShadowRoot:t.getShadowRoot,shadowRootFilter:ge}):a=_(e,t.includeContainer,M.bind(null,t)),me(a)},L=function(e,t){t=t||{};var a;return t.getShadowRoot?a=$([e],t.includeContainer,{filter:x.bind(null,t),flatten:!0,getShadowRoot:t.getShadowRoot}):a=_(e,t.includeContainer,x.bind(null,t)),a},A=function(e,t){if(t=t||{},!e)throw new Error("No node provided");return N.call(e,I)===!1?!1:M(t,e)},ye=K.concat("iframe").join(","),D=function(e,t){if(t=t||{},!e)throw new Error("No node provided");return N.call(e,ye)===!1?!1:x(t,e)};function Q(i,e){var t=Object.keys(i);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(i);e&&(a=a.filter(function(n){return Object.getOwnPropertyDescriptor(i,n).enumerable})),t.push.apply(t,a)}return t}function X(i){for(var e=1;e<arguments.length;e++){var t=arguments[e]!=null?arguments[e]:{};e%2?Q(Object(t),!0).forEach(function(a){we(i,a,t[a])}):Object.getOwnPropertyDescriptors?Object.defineProperties(i,Object.getOwnPropertyDescriptors(t)):Q(Object(t)).forEach(function(a){Object.defineProperty(i,a,Object.getOwnPropertyDescriptor(t,a))})}return i}function we(i,e,t){return e in i?Object.defineProperty(i,e,{value:t,enumerable:!0,configurable:!0,writable:!0}):i[e]=t,i}var J=function(){var i=[];return{activateTrap:function(t){if(i.length>0){var a=i[i.length-1];a!==t&&a.pause()}var n=i.indexOf(t);n===-1||i.splice(n,1),i.push(t)},deactivateTrap:function(t){var a=i.indexOf(t);a!==-1&&i.splice(a,1),i.length>0&&i[i.length-1].unpause()}}}(),Te=function(e){return e.tagName&&e.tagName.toLowerCase()==="input"&&typeof e.select=="function"},Fe=function(e){return e.key==="Escape"||e.key==="Esc"||e.keyCode===27},Se=function(e){return e.key==="Tab"||e.keyCode===9},ee=function(e){return setTimeout(e,0)},te=function(e,t){var a=-1;return e.every(function(n,r){return t(n)?(a=r,!1):!0}),a},O=function(e){for(var t=arguments.length,a=new Array(t>1?t-1:0),n=1;n<t;n++)a[n-1]=arguments[n];return typeof e=="function"?e.apply(void 0,a):e},P=function(e){return e.target.shadowRoot&&typeof e.composedPath=="function"?e.composedPath()[0]:e.target},re=function(e,t){var a=t?.document||document,n=X({returnFocusOnDeactivate:!0,escapeDeactivates:!0,delayInitialFocus:!0},t),r={containers:[],containerGroups:[],tabbableGroups:[],nodeFocusedBeforeActivation:null,mostRecentlyFocusedNode:null,active:!1,paused:!1,delayInitialFocusTimer:void 0},s,l=function(o,u,c){return o&&o[u]!==void 0?o[u]:n[c||u]},m=function(o){return r.containerGroups.findIndex(function(u){var c=u.container,b=u.tabbableNodes;return c.contains(o)||b.find(function(f){return f===o})})},h=function(o){var u=n[o];if(typeof u=="function"){for(var c=arguments.length,b=new Array(c>1?c-1:0),f=1;f<c;f++)b[f-1]=arguments[f];u=u.apply(void 0,b)}if(u===!0&&(u=void 0),!u){if(u===void 0||u===!1)return u;throw new Error("`".concat(o,"` was specified but was not a node, or did not return a node"))}var g=u;if(typeof u=="string"&&(g=a.querySelector(u),!g))throw new Error("`".concat(o,"` as selector refers to no known node"));return g},v=function(){var o=h("initialFocus");if(o===!1)return!1;if(o===void 0)if(m(a.activeElement)>=0)o=a.activeElement;else{var u=r.tabbableGroups[0],c=u&&u.firstTabbableNode;o=c||h("fallbackFocus")}if(!o)throw new Error("Your focus-trap needs to have at least one focusable element");return o},p=function(){if(r.containerGroups=r.containers.map(function(o){var u=z(o,n.tabbableOptions),c=L(o,n.tabbableOptions);return{container:o,tabbableNodes:u,focusableNodes:c,firstTabbableNode:u.length>0?u[0]:null,lastTabbableNode:u.length>0?u[u.length-1]:null,nextTabbableNode:function(f){var g=arguments.length>1&&arguments[1]!==void 0?arguments[1]:!0,w=c.findIndex(function(S){return S===f});if(!(w<0))return g?c.slice(w+1).find(function(S){return A(S,n.tabbableOptions)}):c.slice(0,w).reverse().find(function(S){return A(S,n.tabbableOptions)})}}}),r.tabbableGroups=r.containerGroups.filter(function(o){return o.tabbableNodes.length>0}),r.tabbableGroups.length<=0&&!h("fallbackFocus"))throw new Error("Your focus-trap must have at least one container with at least one tabbable node in it at all times")},y=function d(o){if(o!==!1&&o!==a.activeElement){if(!o||!o.focus){d(v());return}o.focus({preventScroll:!!n.preventScroll}),r.mostRecentlyFocusedNode=o,Te(o)&&o.select()}},T=function(o){var u=h("setReturnFocus",o);return u||(u===!1?!1:o)},F=function(o){var u=P(o);if(!(m(u)>=0)){if(O(n.clickOutsideDeactivates,o)){s.deactivate({returnFocus:n.returnFocusOnDeactivate&&!D(u,n.tabbableOptions)});return}O(n.allowOutsideClick,o)||o.preventDefault()}},R=function(o){var u=P(o),c=m(u)>=0;c||u instanceof Document?c&&(r.mostRecentlyFocusedNode=u):(o.stopImmediatePropagation(),y(r.mostRecentlyFocusedNode||v()))},k=function(o){var u=P(o);p();var c=null;if(r.tabbableGroups.length>0){var b=m(u),f=b>=0?r.containerGroups[b]:void 0;if(b<0)o.shiftKey?c=r.tabbableGroups[r.tabbableGroups.length-1].lastTabbableNode:c=r.tabbableGroups[0].firstTabbableNode;else if(o.shiftKey){var g=te(r.tabbableGroups,function(j){var B=j.firstTabbableNode;return u===B});if(g<0&&(f.container===u||D(u,n.tabbableOptions)&&!A(u,n.tabbableOptions)&&!f.nextTabbableNode(u,!1))&&(g=b),g>=0){var w=g===0?r.tabbableGroups.length-1:g-1,S=r.tabbableGroups[w];c=S.lastTabbableNode}}else{var C=te(r.tabbableGroups,function(j){var B=j.lastTabbableNode;return u===B});if(C<0&&(f.container===u||D(u,n.tabbableOptions)&&!A(u,n.tabbableOptions)&&!f.nextTabbableNode(u))&&(C=b),C>=0){var oe=C===r.tabbableGroups.length-1?0:C+1,ue=r.tabbableGroups[oe];c=ue.firstTabbableNode}}}else c=h("fallbackFocus");c&&(o.preventDefault(),y(c))},E=function(o){if(Fe(o)&&O(n.escapeDeactivates,o)!==!1){o.preventDefault(),s.deactivate();return}if(Se(o)){k(o);return}},q=function(o){var u=P(o);m(u)>=0||O(n.clickOutsideDeactivates,o)||O(n.allowOutsideClick,o)||(o.preventDefault(),o.stopImmediatePropagation())},H=function(){if(r.active)return J.activateTrap(s),r.delayInitialFocusTimer=n.delayInitialFocus?ee(function(){y(v())}):y(v()),a.addEventListener("focusin",R,!0),a.addEventListener("mousedown",F,{capture:!0,passive:!1}),a.addEventListener("touchstart",F,{capture:!0,passive:!1}),a.addEventListener("click",q,{capture:!0,passive:!1}),a.addEventListener("keydown",E,{capture:!0,passive:!1}),s},U=function(){if(r.active)return a.removeEventListener("focusin",R,!0),a.removeEventListener("mousedown",F,!0),a.removeEventListener("touchstart",F,!0),a.removeEventListener("click",q,!0),a.removeEventListener("keydown",E,!0),s};return s={get active(){return r.active},get paused(){return r.paused},activate:function(o){if(r.active)return this;var u=l(o,"onActivate"),c=l(o,"onPostActivate"),b=l(o,"checkCanFocusTrap");b||p(),r.active=!0,r.paused=!1,r.nodeFocusedBeforeActivation=a.activeElement,u&&u();var f=function(){b&&p(),H(),c&&c()};return b?(b(r.containers.concat()).then(f,f),this):(f(),this)},deactivate:function(o){if(!r.active)return this;var u=X({onDeactivate:n.onDeactivate,onPostDeactivate:n.onPostDeactivate,checkCanReturnFocus:n.checkCanReturnFocus},o);clearTimeout(r.delayInitialFocusTimer),r.delayInitialFocusTimer=void 0,U(),r.active=!1,r.paused=!1,J.deactivateTrap(s);var c=l(u,"onDeactivate"),b=l(u,"onPostDeactivate"),f=l(u,"checkCanReturnFocus"),g=l(u,"returnFocus","returnFocusOnDeactivate");c&&c();var w=function(){ee(function(){g&&y(T(r.nodeFocusedBeforeActivation)),b&&b()})};return g&&f?(f(T(r.nodeFocusedBeforeActivation)).then(w,w),this):(w(),this)},pause:function(){return r.paused||!r.active?this:(r.paused=!0,U(),this)},unpause:function(){return!r.paused||!r.active?this:(r.paused=!1,p(),H(),this)},updateContainerElements:function(o){var u=[].concat(o).filter(Boolean);return r.containers=u.map(function(c){return typeof c=="string"?a.querySelector(c):c}),r.active&&p(),this}},s.updateContainerElements(e),s};function ne(i){let e,t;window.addEventListener("focusin",()=>{e=t,t=document.activeElement}),i.magic("focus",a=>{let n=a;return{__noscroll:!1,__wrapAround:!1,within(r){return n=r,this},withoutScrolling(){return this.__noscroll=!0,this},noscroll(){return this.__noscroll=!0,this},withWrapAround(){return this.__wrapAround=!0,this},wrap(){return this.withWrapAround()},focusable(r){return D(r)},previouslyFocused(){return e},lastFocused(){return e},focused(){return t},focusables(){return Array.isArray(n)?n:L(n,{displayCheck:"none"})},all(){return this.focusables()},isFirst(r){let s=this.all();return s[0]&&s[0].isSameNode(r)},isLast(r){let s=this.all();return s.length&&s.slice(-1)[0].isSameNode(r)},getFirst(){return this.all()[0]},getLast(){return this.all().slice(-1)[0]},getNext(){let r=this.all(),s=document.activeElement;if(r.indexOf(s)!==-1)return this.__wrapAround&&r.indexOf(s)===r.length-1?r[0]:r[r.indexOf(s)+1]},getPrevious(){let r=this.all(),s=document.activeElement;if(r.indexOf(s)!==-1)return this.__wrapAround&&r.indexOf(s)===0?r.slice(-1)[0]:r[r.indexOf(s)-1]},first(){this.focus(this.getFirst())},last(){this.focus(this.getLast())},next(){this.focus(this.getNext())},previous(){this.focus(this.getPrevious())},prev(){return this.previous()},focus(r){r&&setTimeout(()=>{r.hasAttribute("tabindex")||r.setAttribute("tabindex","0"),r.focus({preventScroll:this._noscroll})})}}}),i.directive("trap",i.skipDuringClone((a,{expression:n,modifiers:r},{effect:s,evaluateLater:l,cleanup:m})=>{let h=l(n),v=!1,p={escapeDeactivates:!1,allowOutsideClick:!0,fallbackFocus:()=>a},y=a.querySelector("[autofocus]");y&&(p.initialFocus=y);let T=re(a,p),F=()=>{},R=()=>{},k=()=>{F(),F=()=>{},R(),R=()=>{},T.deactivate({returnFocus:!r.includes("noreturn")})};s(()=>h(E=>{v!==E&&(E&&!v&&(r.includes("noscroll")&&(R=Ee()),r.includes("inert")&&(F=ae(a)),setTimeout(()=>{T.activate()},15)),!E&&v&&k(),v=!!E)})),m(k)},(a,{expression:n,modifiers:r},{evaluate:s})=>{r.includes("inert")&&s(n)&&ae(a)}))}function ae(i){let e=[];return ie(i,t=>{let a=t.hasAttribute("aria-hidden");t.setAttribute("aria-hidden","true"),e.push(()=>a||t.removeAttribute("aria-hidden"))}),()=>{for(;e.length;)e.pop()()}}function ie(i,e){i.isSameNode(document.body)||!i.parentNode||Array.from(i.parentNode.children).forEach(t=>{t.isSameNode(i)?ie(i.parentNode,e):e(t)})}function Ee(){let i=document.documentElement.style.overflow,e=document.documentElement.style.paddingRight,t=window.innerWidth-document.documentElement.clientWidth;return document.documentElement.style.overflow="hidden",document.documentElement.style.paddingRight=`${t}px`,()=>{document.documentElement.style.overflow=i,document.documentElement.style.paddingRight=e}}document.addEventListener("alpine:init",()=>{window.Alpine.plugin(ne)});})(); +/*! Bundled license information: + +tabbable/dist/index.esm.js: + (*! + * tabbable 5.3.3 + * @license MIT, https://github.com/focus-trap/tabbable/blob/master/LICENSE + *) + +focus-trap/dist/focus-trap.esm.js: + (*! + * focus-trap 6.9.4 + * @license MIT, https://github.com/focus-trap/focus-trap/blob/master/LICENSE + *) */ diff --git a/hypha/static_src/src/javascript/apply/vendor/alpine.min.js b/hypha/static_src/src/javascript/apply/vendor/alpine.min.js index 154023b356e10c55bd04ae68879c9ac6e143dec6..af7df643800134289562b679679a78453c3a1486 100644 --- a/hypha/static_src/src/javascript/apply/vendor/alpine.min.js +++ b/hypha/static_src/src/javascript/apply/vendor/alpine.min.js @@ -1,5 +1,5 @@ -(()=>{var Ye=!1,Xe=!1,V=[],Ze=-1;function Bt(e){bn(e)}function bn(e){V.includes(e)||V.push(e),vn()}function ye(e){let t=V.indexOf(e);t!==-1&&t>Ze&&V.splice(t,1)}function vn(){!Xe&&!Ye&&(Ye=!0,queueMicrotask(wn))}function wn(){Ye=!1,Xe=!0;for(let e=0;e<V.length;e++)V[e](),Ze=e;V.length=0,Ze=-1,Xe=!1}var C,P,$,Qe,et=!0;function Kt(e){et=!1,e(),et=!0}function zt(e){C=e.reactive,$=e.release,P=t=>e.effect(t,{scheduler:r=>{et?Bt(r):r()}}),Qe=e.raw}function tt(e){P=e}function Vt(e){let t=()=>{};return[n=>{let i=P(n);return e._x_effects||(e._x_effects=new Set,e._x_runEffects=()=>{e._x_effects.forEach(o=>o())}),e._x_effects.add(i),t=()=>{i!==void 0&&(e._x_effects.delete(i),$(i))},i},()=>{t()}]}var Ht=[],qt=[],Ut=[];function Wt(e){Ut.push(e)}function be(e,t){typeof t=="function"?(e._x_cleanups||(e._x_cleanups=[]),e._x_cleanups.push(t)):(t=e,qt.push(t))}function Gt(e){Ht.push(e)}function Jt(e,t,r){e._x_attributeCleanups||(e._x_attributeCleanups={}),e._x_attributeCleanups[t]||(e._x_attributeCleanups[t]=[]),e._x_attributeCleanups[t].push(r)}function rt(e,t){!e._x_attributeCleanups||Object.entries(e._x_attributeCleanups).forEach(([r,n])=>{(t===void 0||t.includes(r))&&(n.forEach(i=>i()),delete e._x_attributeCleanups[r])})}var it=new MutationObserver(nt),ot=!1;function se(){it.observe(document,{subtree:!0,childList:!0,attributes:!0,attributeOldValue:!0}),ot=!0}function st(){En(),it.disconnect(),ot=!1}var ae=[],at=!1;function En(){ae=ae.concat(it.takeRecords()),ae.length&&!at&&(at=!0,queueMicrotask(()=>{Sn(),at=!1}))}function Sn(){nt(ae),ae.length=0}function h(e){if(!ot)return e();st();let t=e();return se(),t}var ct=!1,ve=[];function Yt(){ct=!0}function Xt(){ct=!1,nt(ve),ve=[]}function nt(e){if(ct){ve=ve.concat(e);return}let t=[],r=[],n=new Map,i=new Map;for(let o=0;o<e.length;o++)if(!e[o].target._x_ignoreMutationObserver&&(e[o].type==="childList"&&(e[o].addedNodes.forEach(s=>s.nodeType===1&&t.push(s)),e[o].removedNodes.forEach(s=>s.nodeType===1&&r.push(s))),e[o].type==="attributes")){let s=e[o].target,a=e[o].attributeName,c=e[o].oldValue,l=()=>{n.has(s)||n.set(s,[]),n.get(s).push({name:a,value:s.getAttribute(a)})},u=()=>{i.has(s)||i.set(s,[]),i.get(s).push(a)};s.hasAttribute(a)&&c===null?l():s.hasAttribute(a)?(u(),l()):u()}i.forEach((o,s)=>{rt(s,o)}),n.forEach((o,s)=>{Ht.forEach(a=>a(s,o))});for(let o of r)if(!t.includes(o)&&(qt.forEach(s=>s(o)),o._x_cleanups))for(;o._x_cleanups.length;)o._x_cleanups.pop()();t.forEach(o=>{o._x_ignoreSelf=!0,o._x_ignore=!0});for(let o of t)r.includes(o)||!o.isConnected||(delete o._x_ignoreSelf,delete o._x_ignore,Ut.forEach(s=>s(o)),o._x_ignore=!0,o._x_ignoreSelf=!0);t.forEach(o=>{delete o._x_ignoreSelf,delete o._x_ignore}),t=null,r=null,n=null,i=null}function we(e){return F(L(e))}function M(e,t,r){return e._x_dataStack=[t,...L(r||e)],()=>{e._x_dataStack=e._x_dataStack.filter(n=>n!==t)}}function L(e){return e._x_dataStack?e._x_dataStack:typeof ShadowRoot=="function"&&e instanceof ShadowRoot?L(e.host):e.parentNode?L(e.parentNode):[]}function F(e){let t=new Proxy({},{ownKeys:()=>Array.from(new Set(e.flatMap(r=>Object.keys(r)))),has:(r,n)=>e.some(i=>i.hasOwnProperty(n)),get:(r,n)=>(e.find(i=>{if(i.hasOwnProperty(n)){let o=Object.getOwnPropertyDescriptor(i,n);if(o.get&&o.get._x_alreadyBound||o.set&&o.set._x_alreadyBound)return!0;if((o.get||o.set)&&o.enumerable){let s=o.get,a=o.set,c=o;s=s&&s.bind(t),a=a&&a.bind(t),s&&(s._x_alreadyBound=!0),a&&(a._x_alreadyBound=!0),Object.defineProperty(i,n,{...c,get:s,set:a})}return!0}return!1})||{})[n],set:(r,n,i)=>{let o=e.find(s=>s.hasOwnProperty(n));return o?o[n]=i:e[e.length-1][n]=i,!0}});return t}function Ee(e){let t=n=>typeof n=="object"&&!Array.isArray(n)&&n!==null,r=(n,i="")=>{Object.entries(Object.getOwnPropertyDescriptors(n)).forEach(([o,{value:s,enumerable:a}])=>{if(a===!1||s===void 0)return;let c=i===""?o:`${i}.${o}`;typeof s=="object"&&s!==null&&s._x_interceptor?n[o]=s.initialize(e,c,o):t(s)&&s!==n&&!(s instanceof Element)&&r(s,c)})};return r(e)}function Se(e,t=()=>{}){let r={initialValue:void 0,_x_interceptor:!0,initialize(n,i,o){return e(this.initialValue,()=>An(n,i),s=>lt(n,i,s),i,o)}};return t(r),n=>{if(typeof n=="object"&&n!==null&&n._x_interceptor){let i=r.initialize.bind(r);r.initialize=(o,s,a)=>{let c=n.initialize(o,s,a);return r.initialValue=c,i(o,s,a)}}else r.initialValue=n;return r}}function An(e,t){return t.split(".").reduce((r,n)=>r[n],e)}function lt(e,t,r){if(typeof t=="string"&&(t=t.split(".")),t.length===1)e[t[0]]=r;else{if(t.length===0)throw error;return e[t[0]]||(e[t[0]]={}),lt(e[t[0]],t.slice(1),r)}}var Zt={};function y(e,t){Zt[e]=t}function ce(e,t){return Object.entries(Zt).forEach(([r,n])=>{let i=null;function o(){if(i)return i;{let[s,a]=ut(t);return i={interceptor:Se,...s},be(t,a),i}}Object.defineProperty(e,`$${r}`,{get(){return n(t,o())},enumerable:!1})}),e}function Qt(e,t,r,...n){try{return r(...n)}catch(i){X(i,e,t)}}function X(e,t,r=void 0){Object.assign(e,{el:t,expression:r}),console.warn(`Alpine Expression Error: ${e.message} +(()=>{var tt=!1,rt=!1,V=[],nt=-1;function Vt(e){Sn(e)}function Sn(e){V.includes(e)||V.push(e),An()}function Ee(e){let t=V.indexOf(e);t!==-1&&t>nt&&V.splice(t,1)}function An(){!rt&&!tt&&(tt=!0,queueMicrotask(On))}function On(){tt=!1,rt=!0;for(let e=0;e<V.length;e++)V[e](),nt=e;V.length=0,nt=-1,rt=!1}var T,k,$,ot,it=!0;function qt(e){it=!1,e(),it=!0}function Ut(e){T=e.reactive,$=e.release,k=t=>e.effect(t,{scheduler:r=>{it?Vt(r):r()}}),ot=e.raw}function st(e){k=e}function Wt(e){let t=()=>{};return[n=>{let i=k(n);return e._x_effects||(e._x_effects=new Set,e._x_runEffects=()=>{e._x_effects.forEach(o=>o())}),e._x_effects.add(i),t=()=>{i!==void 0&&(e._x_effects.delete(i),$(i))},i},()=>{t()}]}function q(e,t,r={}){e.dispatchEvent(new CustomEvent(t,{detail:r,bubbles:!0,composed:!0,cancelable:!0}))}function O(e,t){if(typeof ShadowRoot=="function"&&e instanceof ShadowRoot){Array.from(e.children).forEach(i=>O(i,t));return}let r=!1;if(t(e,()=>r=!0),r)return;let n=e.firstElementChild;for(;n;)O(n,t,!1),n=n.nextElementSibling}function v(e,...t){console.warn(`Alpine Warning: ${e}`,...t)}var Gt=!1;function Jt(){Gt&&v("Alpine has already been initialized on this page. Calling Alpine.start() more than once can cause problems."),Gt=!0,document.body||v("Unable to initialize. Trying to load Alpine before `<body>` is available. Did you forget to add `defer` in Alpine's `<script>` tag?"),q(document,"alpine:init"),q(document,"alpine:initializing"),ce(),rr(t=>S(t,O)),Q(t=>ae(t)),Ae((t,r)=>{le(t,r).forEach(n=>n())});let e=t=>!U(t.parentElement,!0);Array.from(document.querySelectorAll(Zt().join(","))).filter(e).forEach(t=>{S(t)}),q(document,"alpine:initialized")}var at=[],Yt=[];function Xt(){return at.map(e=>e())}function Zt(){return at.concat(Yt).map(e=>e())}function ve(e){at.push(e)}function Se(e){Yt.push(e)}function U(e,t=!1){return Z(e,r=>{if((t?Zt():Xt()).some(i=>r.matches(i)))return!0})}function Z(e,t){if(e){if(t(e))return e;if(e._x_teleportBack&&(e=e._x_teleportBack),!!e.parentElement)return Z(e.parentElement,t)}}function Qt(e){return Xt().some(t=>e.matches(t))}var er=[];function tr(e){er.push(e)}function S(e,t=O,r=()=>{}){ir(()=>{t(e,(n,i)=>{r(n,i),er.forEach(o=>o(n,i)),le(n,n.attributes).forEach(o=>o()),n._x_ignore&&i()})})}function ae(e){O(e,t=>{ct(t),nr(t)})}var or=[],sr=[],ar=[];function rr(e){ar.push(e)}function Q(e,t){typeof t=="function"?(e._x_cleanups||(e._x_cleanups=[]),e._x_cleanups.push(t)):(t=e,sr.push(t))}function Ae(e){or.push(e)}function Ce(e,t,r){e._x_attributeCleanups||(e._x_attributeCleanups={}),e._x_attributeCleanups[t]||(e._x_attributeCleanups[t]=[]),e._x_attributeCleanups[t].push(r)}function ct(e,t){e._x_attributeCleanups&&Object.entries(e._x_attributeCleanups).forEach(([r,n])=>{(t===void 0||t.includes(r))&&(n.forEach(i=>i()),delete e._x_attributeCleanups[r])})}function nr(e){if(e._x_cleanups)for(;e._x_cleanups.length;)e._x_cleanups.pop()()}var ut=new MutationObserver(mt),ft=!1;function ce(){ut.observe(document,{subtree:!0,childList:!0,attributes:!0,attributeOldValue:!0}),ft=!0}function dt(){Cn(),ut.disconnect(),ft=!1}var ue=[],lt=!1;function Cn(){ue=ue.concat(ut.takeRecords()),ue.length&&!lt&&(lt=!0,queueMicrotask(()=>{Tn(),lt=!1}))}function Tn(){mt(ue),ue.length=0}function h(e){if(!ft)return e();dt();let t=e();return ce(),t}var pt=!1,Oe=[];function cr(){pt=!0}function lr(){pt=!1,mt(Oe),Oe=[]}function mt(e){if(pt){Oe=Oe.concat(e);return}let t=[],r=[],n=new Map,i=new Map;for(let o=0;o<e.length;o++)if(!e[o].target._x_ignoreMutationObserver&&(e[o].type==="childList"&&(e[o].addedNodes.forEach(s=>s.nodeType===1&&t.push(s)),e[o].removedNodes.forEach(s=>s.nodeType===1&&r.push(s))),e[o].type==="attributes")){let s=e[o].target,a=e[o].attributeName,c=e[o].oldValue,l=()=>{n.has(s)||n.set(s,[]),n.get(s).push({name:a,value:s.getAttribute(a)})},u=()=>{i.has(s)||i.set(s,[]),i.get(s).push(a)};s.hasAttribute(a)&&c===null?l():s.hasAttribute(a)?(u(),l()):u()}i.forEach((o,s)=>{ct(s,o)}),n.forEach((o,s)=>{or.forEach(a=>a(s,o))});for(let o of r)t.includes(o)||(sr.forEach(s=>s(o)),ae(o));t.forEach(o=>{o._x_ignoreSelf=!0,o._x_ignore=!0});for(let o of t)r.includes(o)||o.isConnected&&(delete o._x_ignoreSelf,delete o._x_ignore,ar.forEach(s=>s(o)),o._x_ignore=!0,o._x_ignoreSelf=!0);t.forEach(o=>{delete o._x_ignoreSelf,delete o._x_ignore}),t=null,r=null,n=null,i=null}function Te(e){return F(j(e))}function N(e,t,r){return e._x_dataStack=[t,...j(r||e)],()=>{e._x_dataStack=e._x_dataStack.filter(n=>n!==t)}}function j(e){return e._x_dataStack?e._x_dataStack:typeof ShadowRoot=="function"&&e instanceof ShadowRoot?j(e.host):e.parentNode?j(e.parentNode):[]}function F(e){return new Proxy({objects:e},Rn)}var Rn={ownKeys({objects:e}){return Array.from(new Set(e.flatMap(t=>Object.keys(t))))},has({objects:e},t){return t==Symbol.unscopables?!1:e.some(r=>Object.prototype.hasOwnProperty.call(r,t))},get({objects:e},t,r){return t=="toJSON"?Mn:Reflect.get(e.find(n=>Object.prototype.hasOwnProperty.call(n,t))||{},t,r)},set({objects:e},t,r,n){let i=e.find(s=>Object.prototype.hasOwnProperty.call(s,t))||e[e.length-1],o=Object.getOwnPropertyDescriptor(i,t);return o?.set&&o?.get?Reflect.set(i,t,r,n):Reflect.set(i,t,r)}};function Mn(){return Reflect.ownKeys(this).reduce((t,r)=>(t[r]=Reflect.get(this,r),t),{})}function Re(e){let t=n=>typeof n=="object"&&!Array.isArray(n)&&n!==null,r=(n,i="")=>{Object.entries(Object.getOwnPropertyDescriptors(n)).forEach(([o,{value:s,enumerable:a}])=>{if(a===!1||s===void 0)return;let c=i===""?o:`${i}.${o}`;typeof s=="object"&&s!==null&&s._x_interceptor?n[o]=s.initialize(e,c,o):t(s)&&s!==n&&!(s instanceof Element)&&r(s,c)})};return r(e)}function Me(e,t=()=>{}){let r={initialValue:void 0,_x_interceptor:!0,initialize(n,i,o){return e(this.initialValue,()=>Nn(n,i),s=>ht(n,i,s),i,o)}};return t(r),n=>{if(typeof n=="object"&&n!==null&&n._x_interceptor){let i=r.initialize.bind(r);r.initialize=(o,s,a)=>{let c=n.initialize(o,s,a);return r.initialValue=c,i(o,s,a)}}else r.initialValue=n;return r}}function Nn(e,t){return t.split(".").reduce((r,n)=>r[n],e)}function ht(e,t,r){if(typeof t=="string"&&(t=t.split(".")),t.length===1)e[t[0]]=r;else{if(t.length===0)throw error;return e[t[0]]||(e[t[0]]={}),ht(e[t[0]],t.slice(1),r)}}var ur={};function y(e,t){ur[e]=t}function fe(e,t){return Object.entries(ur).forEach(([r,n])=>{let i=null;function o(){if(i)return i;{let[s,a]=_t(t);return i={interceptor:Me,...s},Q(t,a),i}}Object.defineProperty(e,`$${r}`,{get(){return n(t,o())},enumerable:!1})}),e}function fr(e,t,r,...n){try{return r(...n)}catch(i){ee(i,e,t)}}function ee(e,t,r=void 0){Object.assign(e,{el:t,expression:r}),console.warn(`Alpine Expression Error: ${e.message} ${r?'Expression: "'+r+`" -`:""}`,t),setTimeout(()=>{throw e},0)}var Ae=!0;function er(e){let t=Ae;Ae=!1,e(),Ae=t}function D(e,t,r={}){let n;return x(e,t)(i=>n=i,r),n}function x(...e){return tr(...e)}var tr=ft;function rr(e){tr=e}function ft(e,t){let r={};ce(r,e);let n=[r,...L(e)],i=typeof t=="function"?On(n,t):Tn(n,t,e);return Qt.bind(null,e,t,i)}function On(e,t){return(r=()=>{},{scope:n={},params:i=[]}={})=>{let o=t.apply(F([n,...e]),i);Oe(r,o)}}var dt={};function Cn(e,t){if(dt[e])return dt[e];let r=Object.getPrototypeOf(async function(){}).constructor,n=/^[\n\s]*if.*\(.*\)/.test(e)||/^(let|const)\s/.test(e)?`(async()=>{ ${e} })()`:e,o=(()=>{try{return new r(["__self","scope"],`with (scope) { __self.result = ${n} }; __self.finished = true; return __self.result;`)}catch(s){return X(s,t,e),Promise.resolve()}})();return dt[e]=o,o}function Tn(e,t,r){let n=Cn(t,r);return(i=()=>{},{scope:o={},params:s=[]}={})=>{n.result=void 0,n.finished=!1;let a=F([o,...e]);if(typeof n=="function"){let c=n(n,a).catch(l=>X(l,r,t));n.finished?(Oe(i,n.result,a,s,r),n.result=void 0):c.then(l=>{Oe(i,l,a,s,r)}).catch(l=>X(l,r,t)).finally(()=>n.result=void 0)}}}function Oe(e,t,r,n,i){if(Ae&&typeof t=="function"){let o=t.apply(r,n);o instanceof Promise?o.then(s=>Oe(e,s,r,n)).catch(s=>X(s,i,t)):e(o)}else typeof t=="object"&&t instanceof Promise?t.then(o=>e(o)):e(t)}var pt="x-";function O(e=""){return pt+e}function nr(e){pt=e}var mt={};function p(e,t){return mt[e]=t,{before(r){if(!mt[r]){console.warn("Cannot find directive `${directive}`. `${name}` will use the default order of execution");return}let n=H.indexOf(r);H.splice(n>=0?n:H.indexOf("DEFAULT"),0,e)}}}function le(e,t,r){if(t=Array.from(t),e._x_virtualDirectives){let o=Object.entries(e._x_virtualDirectives).map(([a,c])=>({name:a,value:c})),s=ht(o);o=o.map(a=>s.find(c=>c.name===a.name)?{name:`x-bind:${a.name}`,value:`"${a.value}"`}:a),t=t.concat(o)}let n={};return t.map(ir((o,s)=>n[o]=s)).filter(or).map(Mn(n,r)).sort(Nn).map(o=>Rn(e,o))}function ht(e){return Array.from(e).map(ir()).filter(t=>!or(t))}var _t=!1,ue=new Map,sr=Symbol();function ar(e){_t=!0;let t=Symbol();sr=t,ue.set(t,[]);let r=()=>{for(;ue.get(t).length;)ue.get(t).shift()();ue.delete(t)},n=()=>{_t=!1,r()};e(r),n()}function ut(e){let t=[],r=a=>t.push(a),[n,i]=Vt(e);return t.push(i),[{Alpine:j,effect:n,cleanup:r,evaluateLater:x.bind(x,e),evaluate:D.bind(D,e)},()=>t.forEach(a=>a())]}function Rn(e,t){let r=()=>{},n=mt[t.type]||r,[i,o]=ut(e);Jt(e,t.original,o);let s=()=>{e._x_ignore||e._x_ignoreSelf||(n.inline&&n.inline(e,t,i),n=n.bind(n,e,t,i),_t?ue.get(sr).push(n):n())};return s.runCleanups=o,s}var Te=(e,t)=>({name:r,value:n})=>(r.startsWith(e)&&(r=r.replace(e,t)),{name:r,value:n}),Ce=e=>e;function ir(e=()=>{}){return({name:t,value:r})=>{let{name:n,value:i}=cr.reduce((o,s)=>s(o),{name:t,value:r});return n!==t&&e(n,t),{name:n,value:i}}}var cr=[];function Z(e){cr.push(e)}function or({name:e}){return lr().test(e)}var lr=()=>new RegExp(`^${pt}([^:^.]+)\\b`);function Mn(e,t){return({name:r,value:n})=>{let i=r.match(lr()),o=r.match(/:([a-zA-Z0-9\-:]+)/),s=r.match(/\.[^.\]]+(?=[^\]]*$)/g)||[],a=t||e[r]||r;return{type:i?i[1]:null,value:o?o[1]:null,modifiers:s.map(c=>c.replace(".","")),expression:n,original:a}}}var gt="DEFAULT",H=["ignore","ref","data","id","bind","init","for","model","modelable","transition","show","if",gt,"teleport"];function Nn(e,t){let r=H.indexOf(e.type)===-1?gt:e.type,n=H.indexOf(t.type)===-1?gt:t.type;return H.indexOf(r)-H.indexOf(n)}function q(e,t,r={}){e.dispatchEvent(new CustomEvent(t,{detail:r,bubbles:!0,composed:!0,cancelable:!0}))}function T(e,t){if(typeof ShadowRoot=="function"&&e instanceof ShadowRoot){Array.from(e.children).forEach(i=>T(i,t));return}let r=!1;if(t(e,()=>r=!0),r)return;let n=e.firstElementChild;for(;n;)T(n,t,!1),n=n.nextElementSibling}function S(e,...t){console.warn(`Alpine Warning: ${e}`,...t)}var ur=!1;function dr(){ur&&S("Alpine has already been initialized on this page. Calling Alpine.start() more than once can cause problems."),ur=!0,document.body||S("Unable to initialize. Trying to load Alpine before `<body>` is available. Did you forget to add `defer` in Alpine's `<script>` tag?"),q(document,"alpine:init"),q(document,"alpine:initializing"),se(),Wt(t=>E(t,T)),be(t=>xt(t)),Gt((t,r)=>{le(t,r).forEach(n=>n())});let e=t=>!U(t.parentElement,!0);Array.from(document.querySelectorAll(fr())).filter(e).forEach(t=>{E(t)}),q(document,"alpine:initialized")}var yt=[],pr=[];function mr(){return yt.map(e=>e())}function fr(){return yt.concat(pr).map(e=>e())}function Re(e){yt.push(e)}function Me(e){pr.push(e)}function U(e,t=!1){return Q(e,r=>{if((t?fr():mr()).some(i=>r.matches(i)))return!0})}function Q(e,t){if(!!e){if(t(e))return e;if(e._x_teleportBack&&(e=e._x_teleportBack),!!e.parentElement)return Q(e.parentElement,t)}}function hr(e){return mr().some(t=>e.matches(t))}var _r=[];function gr(e){_r.push(e)}function E(e,t=T,r=()=>{}){ar(()=>{t(e,(n,i)=>{r(n,i),_r.forEach(o=>o(n,i)),le(n,n.attributes).forEach(o=>o()),n._x_ignore&&i()})})}function xt(e){T(e,t=>rt(t))}var bt=[],vt=!1;function ee(e=()=>{}){return queueMicrotask(()=>{vt||setTimeout(()=>{Ne()})}),new Promise(t=>{bt.push(()=>{e(),t()})})}function Ne(){for(vt=!1;bt.length;)bt.shift()()}function xr(){vt=!0}function fe(e,t){return Array.isArray(t)?yr(e,t.join(" ")):typeof t=="object"&&t!==null?In(e,t):typeof t=="function"?fe(e,t()):yr(e,t)}function yr(e,t){let r=o=>o.split(" ").filter(Boolean),n=o=>o.split(" ").filter(s=>!e.classList.contains(s)).filter(Boolean),i=o=>(e.classList.add(...o),()=>{e.classList.remove(...o)});return t=t===!0?t="":t||"",i(n(t))}function In(e,t){let r=a=>a.split(" ").filter(Boolean),n=Object.entries(t).flatMap(([a,c])=>c?r(a):!1).filter(Boolean),i=Object.entries(t).flatMap(([a,c])=>c?!1:r(a)).filter(Boolean),o=[],s=[];return i.forEach(a=>{e.classList.contains(a)&&(e.classList.remove(a),s.push(a))}),n.forEach(a=>{e.classList.contains(a)||(e.classList.add(a),o.push(a))}),()=>{s.forEach(a=>e.classList.add(a)),o.forEach(a=>e.classList.remove(a))}}function W(e,t){return typeof t=="object"&&t!==null?Pn(e,t):Dn(e,t)}function Pn(e,t){let r={};return Object.entries(t).forEach(([n,i])=>{r[n]=e.style[n],n.startsWith("--")||(n=kn(n)),e.style.setProperty(n,i)}),setTimeout(()=>{e.style.length===0&&e.removeAttribute("style")}),()=>{W(e,r)}}function Dn(e,t){let r=e.getAttribute("style",t);return e.setAttribute("style",t),()=>{e.setAttribute("style",r||"")}}function kn(e){return e.replace(/([a-z])([A-Z])/g,"$1-$2").toLowerCase()}function de(e,t=()=>{}){let r=!1;return function(){r?t.apply(this,arguments):(r=!0,e.apply(this,arguments))}}p("transition",(e,{value:t,modifiers:r,expression:n},{evaluate:i})=>{typeof n=="function"&&(n=i(n)),n!==!1&&(!n||typeof n=="boolean"?Ln(e,r,t):$n(e,n,t))});function $n(e,t,r){br(e,fe,""),{enter:i=>{e._x_transition.enter.during=i},"enter-start":i=>{e._x_transition.enter.start=i},"enter-end":i=>{e._x_transition.enter.end=i},leave:i=>{e._x_transition.leave.during=i},"leave-start":i=>{e._x_transition.leave.start=i},"leave-end":i=>{e._x_transition.leave.end=i}}[r](t)}function Ln(e,t,r){br(e,W);let n=!t.includes("in")&&!t.includes("out")&&!r,i=n||t.includes("in")||["enter"].includes(r),o=n||t.includes("out")||["leave"].includes(r);t.includes("in")&&!n&&(t=t.filter((_,b)=>b<t.indexOf("out"))),t.includes("out")&&!n&&(t=t.filter((_,b)=>b>t.indexOf("out")));let s=!t.includes("opacity")&&!t.includes("scale"),a=s||t.includes("opacity"),c=s||t.includes("scale"),l=a?0:1,u=c?pe(t,"scale",95)/100:1,d=pe(t,"delay",0)/1e3,m=pe(t,"origin","center"),v="opacity, transform",k=pe(t,"duration",150)/1e3,xe=pe(t,"duration",75)/1e3,f="cubic-bezier(0.4, 0.0, 0.2, 1)";i&&(e._x_transition.enter.during={transformOrigin:m,transitionDelay:`${d}s`,transitionProperty:v,transitionDuration:`${k}s`,transitionTimingFunction:f},e._x_transition.enter.start={opacity:l,transform:`scale(${u})`},e._x_transition.enter.end={opacity:1,transform:"scale(1)"}),o&&(e._x_transition.leave.during={transformOrigin:m,transitionDelay:`${d}s`,transitionProperty:v,transitionDuration:`${xe}s`,transitionTimingFunction:f},e._x_transition.leave.start={opacity:1,transform:"scale(1)"},e._x_transition.leave.end={opacity:l,transform:`scale(${u})`})}function br(e,t,r={}){e._x_transition||(e._x_transition={enter:{during:r,start:r,end:r},leave:{during:r,start:r,end:r},in(n=()=>{},i=()=>{}){Ie(e,t,{during:this.enter.during,start:this.enter.start,end:this.enter.end},n,i)},out(n=()=>{},i=()=>{}){Ie(e,t,{during:this.leave.during,start:this.leave.start,end:this.leave.end},n,i)}})}window.Element.prototype._x_toggleAndCascadeWithTransitions=function(e,t,r,n){let i=document.visibilityState==="visible"?requestAnimationFrame:setTimeout,o=()=>i(r);if(t){e._x_transition&&(e._x_transition.enter||e._x_transition.leave)?e._x_transition.enter&&(Object.entries(e._x_transition.enter.during).length||Object.entries(e._x_transition.enter.start).length||Object.entries(e._x_transition.enter.end).length)?e._x_transition.in(r):o():e._x_transition?e._x_transition.in(r):o();return}e._x_hidePromise=e._x_transition?new Promise((s,a)=>{e._x_transition.out(()=>{},()=>s(n)),e._x_transitioning.beforeCancel(()=>a({isFromCancelledTransition:!0}))}):Promise.resolve(n),queueMicrotask(()=>{let s=vr(e);s?(s._x_hideChildren||(s._x_hideChildren=[]),s._x_hideChildren.push(e)):i(()=>{let a=c=>{let l=Promise.all([c._x_hidePromise,...(c._x_hideChildren||[]).map(a)]).then(([u])=>u());return delete c._x_hidePromise,delete c._x_hideChildren,l};a(e).catch(c=>{if(!c.isFromCancelledTransition)throw c})})})};function vr(e){let t=e.parentNode;if(!!t)return t._x_hidePromise?t:vr(t)}function Ie(e,t,{during:r,start:n,end:i}={},o=()=>{},s=()=>{}){if(e._x_transitioning&&e._x_transitioning.cancel(),Object.keys(r).length===0&&Object.keys(n).length===0&&Object.keys(i).length===0){o(),s();return}let a,c,l;Fn(e,{start(){a=t(e,n)},during(){c=t(e,r)},before:o,end(){a(),l=t(e,i)},after:s,cleanup(){c(),l()}})}function Fn(e,t){let r,n,i,o=de(()=>{h(()=>{r=!0,n||t.before(),i||(t.end(),Ne()),t.after(),e.isConnected&&t.cleanup(),delete e._x_transitioning})});e._x_transitioning={beforeCancels:[],beforeCancel(s){this.beforeCancels.push(s)},cancel:de(function(){for(;this.beforeCancels.length;)this.beforeCancels.shift()();o()}),finish:o},h(()=>{t.start(),t.during()}),xr(),requestAnimationFrame(()=>{if(r)return;let s=Number(getComputedStyle(e).transitionDuration.replace(/,.*/,"").replace("s",""))*1e3,a=Number(getComputedStyle(e).transitionDelay.replace(/,.*/,"").replace("s",""))*1e3;s===0&&(s=Number(getComputedStyle(e).animationDuration.replace("s",""))*1e3),h(()=>{t.before()}),n=!0,requestAnimationFrame(()=>{r||(h(()=>{t.end()}),Ne(),setTimeout(e._x_transitioning.finish,s+a),i=!0)})})}function pe(e,t,r){if(e.indexOf(t)===-1)return r;let n=e[e.indexOf(t)+1];if(!n||t==="scale"&&isNaN(n))return r;if(t==="duration"||t==="delay"){let i=n.match(/([0-9]+)ms/);if(i)return i[1]}return t==="origin"&&["top","right","left","center","bottom"].includes(e[e.indexOf(t)+2])?[n,e[e.indexOf(t)+2]].join(" "):n}var te=!1;function N(e,t=()=>{}){return(...r)=>te?t(...r):e(...r)}function wr(e){return(...t)=>te&&e(...t)}function Er(e,t){t._x_dataStack||(t._x_dataStack=e._x_dataStack),te=!0,Bn(()=>{jn(t)}),te=!1}function jn(e){let t=!1;E(e,(n,i)=>{T(n,(o,s)=>{if(t&&hr(o))return s();t=!0,i(o,s)})})}function Bn(e){let t=P;tt((r,n)=>{let i=t(r);return $(i),()=>{}}),e(),tt(t)}function me(e,t,r,n=[]){switch(e._x_bindings||(e._x_bindings=C({})),e._x_bindings[t]=r,t=n.includes("camel")?qn(t):t,t){case"value":Kn(e,r);break;case"style":Vn(e,r);break;case"class":zn(e,r);break;case"selected":case"checked":Hn(e,t,r);break;default:Sr(e,t,r);break}}function Kn(e,t){if(e.type==="radio")e.attributes.value===void 0&&(e.value=t),window.fromModel&&(e.checked=Ar(e.value,t));else if(e.type==="checkbox")Number.isInteger(t)?e.value=t:!Number.isInteger(t)&&!Array.isArray(t)&&typeof t!="boolean"&&![null,void 0].includes(t)?e.value=String(t):Array.isArray(t)?e.checked=t.some(r=>Ar(r,e.value)):e.checked=!!t;else if(e.tagName==="SELECT")Un(e,t);else{if(e.value===t)return;e.value=t}}function zn(e,t){e._x_undoAddedClasses&&e._x_undoAddedClasses(),e._x_undoAddedClasses=fe(e,t)}function Vn(e,t){e._x_undoAddedStyles&&e._x_undoAddedStyles(),e._x_undoAddedStyles=W(e,t)}function Hn(e,t,r){Sr(e,t,r),Wn(e,t,r)}function Sr(e,t,r){[null,void 0,!1].includes(r)&&Jn(t)?e.removeAttribute(t):(Or(t)&&(r=t),Gn(e,t,r))}function Gn(e,t,r){e.getAttribute(t)!=r&&e.setAttribute(t,r)}function Wn(e,t,r){e[t]!==r&&(e[t]=r)}function Un(e,t){let r=[].concat(t).map(n=>n+"");Array.from(e.options).forEach(n=>{n.selected=r.includes(n.value)})}function qn(e){return e.toLowerCase().replace(/-(\w)/g,(t,r)=>r.toUpperCase())}function Ar(e,t){return e==t}function Or(e){return["disabled","checked","required","readonly","hidden","open","selected","autofocus","itemscope","multiple","novalidate","allowfullscreen","allowpaymentrequest","formnovalidate","autoplay","controls","loop","muted","playsinline","default","ismap","reversed","async","defer","nomodule"].includes(e)}function Jn(e){return!["aria-pressed","aria-checked","aria-expanded","aria-selected"].includes(e)}function Tr(e,t,r){if(e._x_bindings&&e._x_bindings[t]!==void 0)return e._x_bindings[t];let n=e.getAttribute(t);return n===null?typeof r=="function"?r():r:n===""?!0:Or(t)?!![t,"true"].includes(n):n}function Pe(e,t){var r;return function(){var n=this,i=arguments,o=function(){r=null,e.apply(n,i)};clearTimeout(r),r=setTimeout(o,t)}}function De(e,t){let r;return function(){let n=this,i=arguments;r||(e.apply(n,i),r=!0,setTimeout(()=>r=!1,t))}}function Cr(e){(Array.isArray(e)?e:[e]).forEach(r=>r(j))}var G={},Rr=!1;function Mr(e,t){if(Rr||(G=C(G),Rr=!0),t===void 0)return G[e];G[e]=t,typeof t=="object"&&t!==null&&t.hasOwnProperty("init")&&typeof t.init=="function"&&G[e].init(),Ee(G[e])}function Nr(){return G}var Ir={};function Pr(e,t){let r=typeof t!="function"?()=>t:t;e instanceof Element?wt(e,r()):Ir[e]=r}function Dr(e){return Object.entries(Ir).forEach(([t,r])=>{Object.defineProperty(e,t,{get(){return(...n)=>r(...n)}})}),e}function wt(e,t,r){let n=[];for(;n.length;)n.pop()();let i=Object.entries(t).map(([s,a])=>({name:s,value:a})),o=ht(i);i=i.map(s=>o.find(a=>a.name===s.name)?{name:`x-bind:${s.name}`,value:`"${s.value}"`}:s),le(e,i,r).map(s=>{n.push(s.runCleanups),s()})}var kr={};function $r(e,t){kr[e]=t}function Lr(e,t){return Object.entries(kr).forEach(([r,n])=>{Object.defineProperty(e,r,{get(){return(...i)=>n.bind(t)(...i)},enumerable:!1})}),e}var Yn={get reactive(){return C},get release(){return $},get effect(){return P},get raw(){return Qe},version:"3.12.2",flushAndStopDeferringMutations:Xt,dontAutoEvaluateFunctions:er,disableEffectScheduling:Kt,startObservingMutations:se,stopObservingMutations:st,setReactivityEngine:zt,closestDataStack:L,skipDuringClone:N,onlyDuringClone:wr,addRootSelector:Re,addInitSelector:Me,addScopeToNode:M,deferMutations:Yt,mapAttributes:Z,evaluateLater:x,interceptInit:gr,setEvaluator:rr,mergeProxies:F,findClosest:Q,closestRoot:U,destroyTree:xt,interceptor:Se,transition:Ie,setStyles:W,mutateDom:h,directive:p,throttle:De,debounce:Pe,evaluate:D,initTree:E,nextTick:ee,prefixed:O,prefix:nr,plugin:Cr,magic:y,store:Mr,start:dr,clone:Er,bound:Tr,$data:we,walk:T,data:$r,bind:Pr},j=Yn;function Et(e,t){let r=Object.create(null),n=e.split(",");for(let i=0;i<n.length;i++)r[n[i]]=!0;return t?i=>!!r[i.toLowerCase()]:i=>!!r[i]}var hs={[1]:"TEXT",[2]:"CLASS",[4]:"STYLE",[8]:"PROPS",[16]:"FULL_PROPS",[32]:"HYDRATE_EVENTS",[64]:"STABLE_FRAGMENT",[128]:"KEYED_FRAGMENT",[256]:"UNKEYED_FRAGMENT",[512]:"NEED_PATCH",[1024]:"DYNAMIC_SLOTS",[2048]:"DEV_ROOT_FRAGMENT",[-1]:"HOISTED",[-2]:"BAIL"},_s={[1]:"STABLE",[2]:"DYNAMIC",[3]:"FORWARDED"};var Xn="itemscope,allowfullscreen,formnovalidate,ismap,nomodule,novalidate,readonly";var gs=Et(Xn+",async,autofocus,autoplay,controls,default,defer,disabled,hidden,loop,open,required,reversed,scoped,seamless,checked,muted,multiple,selected");var Fr=Object.freeze({}),xs=Object.freeze([]);var St=Object.assign;var Zn=Object.prototype.hasOwnProperty,he=(e,t)=>Zn.call(e,t),B=Array.isArray,re=e=>jr(e)==="[object Map]";var Qn=e=>typeof e=="string",ke=e=>typeof e=="symbol",_e=e=>e!==null&&typeof e=="object";var ei=Object.prototype.toString,jr=e=>ei.call(e),At=e=>jr(e).slice(8,-1);var $e=e=>Qn(e)&&e!=="NaN"&&e[0]!=="-"&&""+parseInt(e,10)===e;var Le=e=>{let t=Object.create(null);return r=>t[r]||(t[r]=e(r))},ti=/-(\w)/g,ys=Le(e=>e.replace(ti,(t,r)=>r?r.toUpperCase():"")),ri=/\B([A-Z])/g,bs=Le(e=>e.replace(ri,"-$1").toLowerCase()),Ot=Le(e=>e.charAt(0).toUpperCase()+e.slice(1)),vs=Le(e=>e?`on${Ot(e)}`:""),Tt=(e,t)=>e!==t&&(e===e||t===t);var Ct=new WeakMap,ge=[],I,J=Symbol("iterate"),Rt=Symbol("Map key iterate");function ni(e){return e&&e._isEffect===!0}function Br(e,t=Fr){ni(e)&&(e=e.raw);let r=ii(e,t);return t.lazy||r(),r}function zr(e){e.active&&(Kr(e),e.options.onStop&&e.options.onStop(),e.active=!1)}var oi=0;function ii(e,t){let r=function(){if(!r.active)return e();if(!ge.includes(r)){Kr(r);try{return si(),ge.push(r),I=r,e()}finally{ge.pop(),Vr(),I=ge[ge.length-1]}}};return r.id=oi++,r.allowRecurse=!!t.allowRecurse,r._isEffect=!0,r.active=!0,r.raw=e,r.deps=[],r.options=t,r}function Kr(e){let{deps:t}=e;if(t.length){for(let r=0;r<t.length;r++)t[r].delete(e);t.length=0}}var ne=!0,Mt=[];function ai(){Mt.push(ne),ne=!1}function si(){Mt.push(ne),ne=!0}function Vr(){let e=Mt.pop();ne=e===void 0?!0:e}function R(e,t,r){if(!ne||I===void 0)return;let n=Ct.get(e);n||Ct.set(e,n=new Map);let i=n.get(r);i||n.set(r,i=new Set),i.has(I)||(i.add(I),I.deps.push(i),I.options.onTrack&&I.options.onTrack({effect:I,target:e,type:t,key:r}))}function K(e,t,r,n,i,o){let s=Ct.get(e);if(!s)return;let a=new Set,c=u=>{u&&u.forEach(d=>{(d!==I||d.allowRecurse)&&a.add(d)})};if(t==="clear")s.forEach(c);else if(r==="length"&&B(e))s.forEach((u,d)=>{(d==="length"||d>=n)&&c(u)});else switch(r!==void 0&&c(s.get(r)),t){case"add":B(e)?$e(r)&&c(s.get("length")):(c(s.get(J)),re(e)&&c(s.get(Rt)));break;case"delete":B(e)||(c(s.get(J)),re(e)&&c(s.get(Rt)));break;case"set":re(e)&&c(s.get(J));break}let l=u=>{u.options.onTrigger&&u.options.onTrigger({effect:u,target:e,key:r,type:t,newValue:n,oldValue:i,oldTarget:o}),u.options.scheduler?u.options.scheduler(u):u()};a.forEach(l)}var ci=Et("__proto__,__v_isRef,__isVue"),Hr=new Set(Object.getOwnPropertyNames(Symbol).map(e=>Symbol[e]).filter(ke)),li=Fe(),ui=Fe(!1,!0),fi=Fe(!0),di=Fe(!0,!0),je={};["includes","indexOf","lastIndexOf"].forEach(e=>{let t=Array.prototype[e];je[e]=function(...r){let n=g(this);for(let o=0,s=this.length;o<s;o++)R(n,"get",o+"");let i=t.apply(n,r);return i===-1||i===!1?t.apply(n,r.map(g)):i}});["push","pop","shift","unshift","splice"].forEach(e=>{let t=Array.prototype[e];je[e]=function(...r){ai();let n=t.apply(this,r);return Vr(),n}});function Fe(e=!1,t=!1){return function(n,i,o){if(i==="__v_isReactive")return!e;if(i==="__v_isReadonly")return e;if(i==="__v_raw"&&o===(e?t?mi:Ur:t?pi:qr).get(n))return n;let s=B(n);if(!e&&s&&he(je,i))return Reflect.get(je,i,o);let a=Reflect.get(n,i,o);return(ke(i)?Hr.has(i):ci(i))||(e||R(n,"get",i),t)?a:Nt(a)?!s||!$e(i)?a.value:a:_e(a)?e?Wr(a):Be(a):a}}var hi=Gr(),_i=Gr(!0);function Gr(e=!1){return function(r,n,i,o){let s=r[n];if(!e&&(i=g(i),s=g(s),!B(r)&&Nt(s)&&!Nt(i)))return s.value=i,!0;let a=B(r)&&$e(n)?Number(n)<r.length:he(r,n),c=Reflect.set(r,n,i,o);return r===g(o)&&(a?Tt(i,s)&&K(r,"set",n,i,s):K(r,"add",n,i)),c}}function gi(e,t){let r=he(e,t),n=e[t],i=Reflect.deleteProperty(e,t);return i&&r&&K(e,"delete",t,void 0,n),i}function xi(e,t){let r=Reflect.has(e,t);return(!ke(t)||!Hr.has(t))&&R(e,"has",t),r}function yi(e){return R(e,"iterate",B(e)?"length":J),Reflect.ownKeys(e)}var Jr={get:li,set:hi,deleteProperty:gi,has:xi,ownKeys:yi},Yr={get:fi,set(e,t){return console.warn(`Set operation on key "${String(t)}" failed: target is readonly.`,e),!0},deleteProperty(e,t){return console.warn(`Delete operation on key "${String(t)}" failed: target is readonly.`,e),!0}},Ts=St({},Jr,{get:ui,set:_i}),Cs=St({},Yr,{get:di}),It=e=>_e(e)?Be(e):e,Pt=e=>_e(e)?Wr(e):e,Dt=e=>e,Ke=e=>Reflect.getPrototypeOf(e);function ze(e,t,r=!1,n=!1){e=e.__v_raw;let i=g(e),o=g(t);t!==o&&!r&&R(i,"get",t),!r&&R(i,"get",o);let{has:s}=Ke(i),a=n?Dt:r?Pt:It;if(s.call(i,t))return a(e.get(t));if(s.call(i,o))return a(e.get(o));e!==i&&e.get(t)}function Ve(e,t=!1){let r=this.__v_raw,n=g(r),i=g(e);return e!==i&&!t&&R(n,"has",e),!t&&R(n,"has",i),e===i?r.has(e):r.has(e)||r.has(i)}function He(e,t=!1){return e=e.__v_raw,!t&&R(g(e),"iterate",J),Reflect.get(e,"size",e)}function Xr(e){e=g(e);let t=g(this);return Ke(t).has.call(t,e)||(t.add(e),K(t,"add",e,e)),this}function Qr(e,t){t=g(t);let r=g(this),{has:n,get:i}=Ke(r),o=n.call(r,e);o?Zr(r,n,e):(e=g(e),o=n.call(r,e));let s=i.call(r,e);return r.set(e,t),o?Tt(t,s)&&K(r,"set",e,t,s):K(r,"add",e,t),this}function en(e){let t=g(this),{has:r,get:n}=Ke(t),i=r.call(t,e);i?Zr(t,r,e):(e=g(e),i=r.call(t,e));let o=n?n.call(t,e):void 0,s=t.delete(e);return i&&K(t,"delete",e,void 0,o),s}function tn(){let e=g(this),t=e.size!==0,r=re(e)?new Map(e):new Set(e),n=e.clear();return t&&K(e,"clear",void 0,void 0,r),n}function qe(e,t){return function(n,i){let o=this,s=o.__v_raw,a=g(s),c=t?Dt:e?Pt:It;return!e&&R(a,"iterate",J),s.forEach((l,u)=>n.call(i,c(l),c(u),o))}}function Ue(e,t,r){return function(...n){let i=this.__v_raw,o=g(i),s=re(o),a=e==="entries"||e===Symbol.iterator&&s,c=e==="keys"&&s,l=i[e](...n),u=r?Dt:t?Pt:It;return!t&&R(o,"iterate",c?Rt:J),{next(){let{value:d,done:m}=l.next();return m?{value:d,done:m}:{value:a?[u(d[0]),u(d[1])]:u(d),done:m}},[Symbol.iterator](){return this}}}}function z(e){return function(...t){{let r=t[0]?`on key "${t[0]}" `:"";console.warn(`${Ot(e)} operation ${r}failed: target is readonly.`,g(this))}return e==="delete"?!1:this}}var rn={get(e){return ze(this,e)},get size(){return He(this)},has:Ve,add:Xr,set:Qr,delete:en,clear:tn,forEach:qe(!1,!1)},nn={get(e){return ze(this,e,!1,!0)},get size(){return He(this)},has:Ve,add:Xr,set:Qr,delete:en,clear:tn,forEach:qe(!1,!0)},on={get(e){return ze(this,e,!0)},get size(){return He(this,!0)},has(e){return Ve.call(this,e,!0)},add:z("add"),set:z("set"),delete:z("delete"),clear:z("clear"),forEach:qe(!0,!1)},sn={get(e){return ze(this,e,!0,!0)},get size(){return He(this,!0)},has(e){return Ve.call(this,e,!0)},add:z("add"),set:z("set"),delete:z("delete"),clear:z("clear"),forEach:qe(!0,!0)},bi=["keys","values","entries",Symbol.iterator];bi.forEach(e=>{rn[e]=Ue(e,!1,!1),on[e]=Ue(e,!0,!1),nn[e]=Ue(e,!1,!0),sn[e]=Ue(e,!0,!0)});function We(e,t){let r=t?e?sn:nn:e?on:rn;return(n,i,o)=>i==="__v_isReactive"?!e:i==="__v_isReadonly"?e:i==="__v_raw"?n:Reflect.get(he(r,i)&&i in n?r:n,i,o)}var vi={get:We(!1,!1)},Rs={get:We(!1,!0)},wi={get:We(!0,!1)},Ms={get:We(!0,!0)};function Zr(e,t,r){let n=g(r);if(n!==r&&t.call(e,n)){let i=At(e);console.warn(`Reactive ${i} contains both the raw and reactive versions of the same object${i==="Map"?" as keys":""}, which can lead to inconsistencies. Avoid differentiating between the raw and reactive versions of an object and only use the reactive version if possible.`)}}var qr=new WeakMap,pi=new WeakMap,Ur=new WeakMap,mi=new WeakMap;function Ei(e){switch(e){case"Object":case"Array":return 1;case"Map":case"Set":case"WeakMap":case"WeakSet":return 2;default:return 0}}function Si(e){return e.__v_skip||!Object.isExtensible(e)?0:Ei(At(e))}function Be(e){return e&&e.__v_isReadonly?e:an(e,!1,Jr,vi,qr)}function Wr(e){return an(e,!0,Yr,wi,Ur)}function an(e,t,r,n,i){if(!_e(e))return console.warn(`value cannot be made reactive: ${String(e)}`),e;if(e.__v_raw&&!(t&&e.__v_isReactive))return e;let o=i.get(e);if(o)return o;let s=Si(e);if(s===0)return e;let a=new Proxy(e,s===2?n:r);return i.set(e,a),a}function g(e){return e&&g(e.__v_raw)||e}function Nt(e){return Boolean(e&&e.__v_isRef===!0)}y("nextTick",()=>ee);y("dispatch",e=>q.bind(q,e));y("watch",(e,{evaluateLater:t,effect:r})=>(n,i)=>{let o=t(n),s=!0,a,c=r(()=>o(l=>{JSON.stringify(l),s?a=l:queueMicrotask(()=>{i(l,a),a=l}),s=!1}));e._x_effects.delete(c)});y("store",Nr);y("data",e=>we(e));y("root",e=>U(e));y("refs",e=>(e._x_refs_proxy||(e._x_refs_proxy=F(Ai(e))),e._x_refs_proxy));function Ai(e){let t=[],r=e;for(;r;)r._x_refs&&t.push(r._x_refs),r=r.parentNode;return t}var kt={};function $t(e){return kt[e]||(kt[e]=0),++kt[e]}function cn(e,t){return Q(e,r=>{if(r._x_ids&&r._x_ids[t])return!0})}function ln(e,t){e._x_ids||(e._x_ids={}),e._x_ids[t]||(e._x_ids[t]=$t(t))}y("id",e=>(t,r=null)=>{let n=cn(e,t),i=n?n._x_ids[t]:$t(t);return r?`${t}-${i}-${r}`:`${t}-${i}`});y("el",e=>e);un("Focus","focus","focus");un("Persist","persist","persist");function un(e,t,r){y(t,n=>S(`You can't use [$${directiveName}] without first installing the "${e}" plugin here: https://alpinejs.dev/plugins/${r}`,n))}function fn({get:e,set:t},{get:r,set:n}){let i=!0,o,s,a,c,l=P(()=>{let u,d;i?(u=e(),n(u),d=r(),i=!1):(u=e(),d=r(),a=JSON.stringify(u),c=JSON.stringify(d),a!==o?(d=r(),n(u),d=u):(t(d),u=d)),o=JSON.stringify(u),s=JSON.stringify(d)});return()=>{$(l)}}p("modelable",(e,{expression:t},{effect:r,evaluateLater:n,cleanup:i})=>{let o=n(t),s=()=>{let u;return o(d=>u=d),u},a=n(`${t} = __placeholder`),c=u=>a(()=>{},{scope:{__placeholder:u}}),l=s();c(l),queueMicrotask(()=>{if(!e._x_model)return;e._x_removeModelListeners.default();let u=e._x_model.get,d=e._x_model.set,m=fn({get(){return u()},set(v){d(v)}},{get(){return s()},set(v){c(v)}});i(m)})});var Oi=document.createElement("div");p("teleport",(e,{modifiers:t,expression:r},{cleanup:n})=>{e.tagName.toLowerCase()!=="template"&&S("x-teleport can only be used on a <template> tag",e);let i=N(()=>document.querySelector(r),()=>Oi)();i||S(`Cannot find x-teleport element for selector: "${r}"`);let o=e.content.cloneNode(!0).firstElementChild;e._x_teleport=o,o._x_teleportBack=e,e._x_forwardEvents&&e._x_forwardEvents.forEach(s=>{o.addEventListener(s,a=>{a.stopPropagation(),e.dispatchEvent(new a.constructor(a.type,a))})}),M(o,{},e),h(()=>{t.includes("prepend")?i.parentNode.insertBefore(o,i):t.includes("append")?i.parentNode.insertBefore(o,i.nextSibling):i.appendChild(o),E(o),o._x_ignore=!0}),n(()=>o.remove())});var dn=()=>{};dn.inline=(e,{modifiers:t},{cleanup:r})=>{t.includes("self")?e._x_ignoreSelf=!0:e._x_ignore=!0,r(()=>{t.includes("self")?delete e._x_ignoreSelf:delete e._x_ignore})};p("ignore",dn);p("effect",(e,{expression:t},{effect:r})=>r(x(e,t)));function ie(e,t,r,n){let i=e,o=c=>n(c),s={},a=(c,l)=>u=>l(c,u);if(r.includes("dot")&&(t=Ti(t)),r.includes("camel")&&(t=Ci(t)),r.includes("passive")&&(s.passive=!0),r.includes("capture")&&(s.capture=!0),r.includes("window")&&(i=window),r.includes("document")&&(i=document),r.includes("debounce")){let c=r[r.indexOf("debounce")+1]||"invalid-wait",l=Ge(c.split("ms")[0])?Number(c.split("ms")[0]):250;o=Pe(o,l)}if(r.includes("throttle")){let c=r[r.indexOf("throttle")+1]||"invalid-wait",l=Ge(c.split("ms")[0])?Number(c.split("ms")[0]):250;o=De(o,l)}return r.includes("prevent")&&(o=a(o,(c,l)=>{l.preventDefault(),c(l)})),r.includes("stop")&&(o=a(o,(c,l)=>{l.stopPropagation(),c(l)})),r.includes("self")&&(o=a(o,(c,l)=>{l.target===e&&c(l)})),(r.includes("away")||r.includes("outside"))&&(i=document,o=a(o,(c,l)=>{e.contains(l.target)||l.target.isConnected!==!1&&(e.offsetWidth<1&&e.offsetHeight<1||e._x_isShown!==!1&&c(l))})),r.includes("once")&&(o=a(o,(c,l)=>{c(l),i.removeEventListener(t,o,s)})),o=a(o,(c,l)=>{Ri(t)&&Mi(l,r)||c(l)}),i.addEventListener(t,o,s),()=>{i.removeEventListener(t,o,s)}}function Ti(e){return e.replace(/-/g,".")}function Ci(e){return e.toLowerCase().replace(/-(\w)/g,(t,r)=>r.toUpperCase())}function Ge(e){return!Array.isArray(e)&&!isNaN(e)}function Ni(e){return[" ","_"].includes(e)?e:e.replace(/([a-z])([A-Z])/g,"$1-$2").replace(/[_\s]/,"-").toLowerCase()}function Ri(e){return["keydown","keyup"].includes(e)}function Mi(e,t){let r=t.filter(o=>!["window","document","prevent","stop","once","capture"].includes(o));if(r.includes("debounce")){let o=r.indexOf("debounce");r.splice(o,Ge((r[o+1]||"invalid-wait").split("ms")[0])?2:1)}if(r.includes("throttle")){let o=r.indexOf("throttle");r.splice(o,Ge((r[o+1]||"invalid-wait").split("ms")[0])?2:1)}if(r.length===0||r.length===1&&pn(e.key).includes(r[0]))return!1;let i=["ctrl","shift","alt","meta","cmd","super"].filter(o=>r.includes(o));return r=r.filter(o=>!i.includes(o)),!(i.length>0&&i.filter(s=>((s==="cmd"||s==="super")&&(s="meta"),e[`${s}Key`])).length===i.length&&pn(e.key).includes(r[0]))}function pn(e){if(!e)return[];e=Ni(e);let t={ctrl:"control",slash:"/",space:" ",spacebar:" ",cmd:"meta",esc:"escape",up:"arrow-up",down:"arrow-down",left:"arrow-left",right:"arrow-right",period:".",equal:"=",minus:"-",underscore:"_"};return t[e]=e,Object.keys(t).map(r=>{if(t[r]===e)return r}).filter(r=>r)}p("model",(e,{modifiers:t,expression:r},{effect:n,cleanup:i})=>{let o=e;t.includes("parent")&&(o=e.parentNode);let s=x(o,r),a;typeof r=="string"?a=x(o,`${r} = __placeholder`):typeof r=="function"&&typeof r()=="string"?a=x(o,`${r()} = __placeholder`):a=()=>{};let c=()=>{let m;return s(v=>m=v),mn(m)?m.get():m},l=m=>{let v;s(k=>v=k),mn(v)?v.set(m):a(()=>{},{scope:{__placeholder:m}})};typeof r=="string"&&e.type==="radio"&&h(()=>{e.hasAttribute("name")||e.setAttribute("name",r)});var u=e.tagName.toLowerCase()==="select"||["checkbox","radio"].includes(e.type)||t.includes("lazy")?"change":"input";let d=te?()=>{}:ie(e,u,t,m=>{l(Ii(e,t,m,c()))});if(t.includes("fill")&&[null,""].includes(c())&&e.dispatchEvent(new Event(u,{})),e._x_removeModelListeners||(e._x_removeModelListeners={}),e._x_removeModelListeners.default=d,i(()=>e._x_removeModelListeners.default()),e.form){let m=ie(e.form,"reset",[],v=>{ee(()=>e._x_model&&e._x_model.set(e.value))});i(()=>m())}e._x_model={get(){return c()},set(m){l(m)}},e._x_forceModelUpdate=m=>{m=m===void 0?c():m,m===void 0&&typeof r=="string"&&r.match(/\./)&&(m=""),window.fromModel=!0,h(()=>me(e,"value",m)),delete window.fromModel},n(()=>{let m=c();t.includes("unintrusive")&&document.activeElement.isSameNode(e)||e._x_forceModelUpdate(m)})});function Ii(e,t,r,n){return h(()=>{if(r instanceof CustomEvent&&r.detail!==void 0)return r.detail??r.target.value;if(e.type==="checkbox")if(Array.isArray(n)){let i=t.includes("number")?Lt(r.target.value):r.target.value;return r.target.checked?n.concat([i]):n.filter(o=>!Pi(o,i))}else return r.target.checked;else{if(e.tagName.toLowerCase()==="select"&&e.multiple)return t.includes("number")?Array.from(r.target.selectedOptions).map(i=>{let o=i.value||i.text;return Lt(o)}):Array.from(r.target.selectedOptions).map(i=>i.value||i.text);{let i=r.target.value;return t.includes("number")?Lt(i):t.includes("trim")?i.trim():i}}})}function Lt(e){let t=e?parseFloat(e):null;return Di(t)?t:e}function Pi(e,t){return e==t}function Di(e){return!Array.isArray(e)&&!isNaN(e)}function mn(e){return e!==null&&typeof e=="object"&&typeof e.get=="function"&&typeof e.set=="function"}p("cloak",e=>queueMicrotask(()=>h(()=>e.removeAttribute(O("cloak")))));Me(()=>`[${O("init")}]`);p("init",N((e,{expression:t},{evaluate:r})=>typeof t=="string"?!!t.trim()&&r(t,{},!1):r(t,{},!1)));p("text",(e,{expression:t},{effect:r,evaluateLater:n})=>{let i=n(t);r(()=>{i(o=>{h(()=>{e.textContent=o})})})});p("html",(e,{expression:t},{effect:r,evaluateLater:n})=>{let i=n(t);r(()=>{i(o=>{h(()=>{e.innerHTML=o,e._x_ignoreSelf=!0,E(e),delete e._x_ignoreSelf})})})});Z(Te(":",Ce(O("bind:"))));p("bind",(e,{value:t,modifiers:r,expression:n,original:i},{effect:o})=>{if(!t){let a={};Dr(a),x(e,n)(l=>{wt(e,l,i)},{scope:a});return}if(t==="key")return ki(e,n);let s=x(e,n);o(()=>s(a=>{a===void 0&&typeof n=="string"&&n.match(/\./)&&(a=""),h(()=>me(e,t,a,r))}))});function ki(e,t){e._x_keyExpression=t}Re(()=>`[${O("data")}]`);p("data",N((e,{expression:t},{cleanup:r})=>{t=t===""?"{}":t;let n={};ce(n,e);let i={};Lr(i,n);let o=D(e,t,{scope:i});(o===void 0||o===!0)&&(o={}),ce(o,e);let s=C(o);Ee(s);let a=M(e,s);s.init&&D(e,s.init),r(()=>{s.destroy&&D(e,s.destroy),a()})}));p("show",(e,{modifiers:t,expression:r},{effect:n})=>{let i=x(e,r);e._x_doHide||(e._x_doHide=()=>{h(()=>{e.style.setProperty("display","none",t.includes("important")?"important":void 0)})}),e._x_doShow||(e._x_doShow=()=>{h(()=>{e.style.length===1&&e.style.display==="none"?e.removeAttribute("style"):e.style.removeProperty("display")})});let o=()=>{e._x_doHide(),e._x_isShown=!1},s=()=>{e._x_doShow(),e._x_isShown=!0},a=()=>setTimeout(s),c=de(d=>d?s():o(),d=>{typeof e._x_toggleAndCascadeWithTransitions=="function"?e._x_toggleAndCascadeWithTransitions(e,d,s,o):d?a():o()}),l,u=!0;n(()=>i(d=>{!u&&d===l||(t.includes("immediate")&&(d?a():o()),c(d),l=d,u=!1)}))});p("for",(e,{expression:t},{effect:r,cleanup:n})=>{let i=Li(t),o=x(e,i.items),s=x(e,e._x_keyExpression||"index");e._x_prevKeys=[],e._x_lookup={},r(()=>$i(e,i,o,s)),n(()=>{Object.values(e._x_lookup).forEach(a=>a.remove()),delete e._x_prevKeys,delete e._x_lookup})});function $i(e,t,r,n){let i=s=>typeof s=="object"&&!Array.isArray(s),o=e;r(s=>{Fi(s)&&s>=0&&(s=Array.from(Array(s).keys(),f=>f+1)),s===void 0&&(s=[]);let a=e._x_lookup,c=e._x_prevKeys,l=[],u=[];if(i(s))s=Object.entries(s).map(([f,_])=>{let b=hn(t,_,f,s);n(w=>u.push(w),{scope:{index:f,...b}}),l.push(b)});else for(let f=0;f<s.length;f++){let _=hn(t,s[f],f,s);n(b=>u.push(b),{scope:{index:f,..._}}),l.push(_)}let d=[],m=[],v=[],k=[];for(let f=0;f<c.length;f++){let _=c[f];u.indexOf(_)===-1&&v.push(_)}c=c.filter(f=>!v.includes(f));let xe="template";for(let f=0;f<u.length;f++){let _=u[f],b=c.indexOf(_);if(b===-1)c.splice(f,0,_),d.push([xe,f]);else if(b!==f){let w=c.splice(f,1)[0],A=c.splice(b-1,1)[0];c.splice(f,0,A),c.splice(b,0,w),m.push([w,A])}else k.push(_);xe=_}for(let f=0;f<v.length;f++){let _=v[f];a[_]._x_effects&&a[_]._x_effects.forEach(ye),a[_].remove(),a[_]=null,delete a[_]}for(let f=0;f<m.length;f++){let[_,b]=m[f],w=a[_],A=a[b],Y=document.createElement("div");h(()=>{A||S('x-for ":key" is undefined or invalid',o),A.after(Y),w.after(A),A._x_currentIfEl&&A.after(A._x_currentIfEl),Y.before(w),w._x_currentIfEl&&w.after(w._x_currentIfEl),Y.remove()}),A._x_refreshXForScope(l[u.indexOf(b)])}for(let f=0;f<d.length;f++){let[_,b]=d[f],w=_==="template"?o:a[_];w._x_currentIfEl&&(w=w._x_currentIfEl);let A=l[b],Y=u[b],oe=document.importNode(o.content,!0).firstElementChild,jt=C(A);M(oe,jt,o),oe._x_refreshXForScope=gn=>{Object.entries(gn).forEach(([xn,yn])=>{jt[xn]=yn})},h(()=>{w.after(oe),E(oe)}),typeof Y=="object"&&S("x-for key cannot be an object, it must be a string or an integer",o),a[Y]=oe}for(let f=0;f<k.length;f++)a[k[f]]._x_refreshXForScope(l[u.indexOf(k[f])]);o._x_prevKeys=u})}function Li(e){let t=/,([^,\}\]]*)(?:,([^,\}\]]*))?$/,r=/^\s*\(|\)\s*$/g,n=/([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/,i=e.match(n);if(!i)return;let o={};o.items=i[2].trim();let s=i[1].replace(r,"").trim(),a=s.match(t);return a?(o.item=s.replace(t,"").trim(),o.index=a[1].trim(),a[2]&&(o.collection=a[2].trim())):o.item=s,o}function hn(e,t,r,n){let i={};return/^\[.*\]$/.test(e.item)&&Array.isArray(t)?e.item.replace("[","").replace("]","").split(",").map(s=>s.trim()).forEach((s,a)=>{i[s]=t[a]}):/^\{.*\}$/.test(e.item)&&!Array.isArray(t)&&typeof t=="object"?e.item.replace("{","").replace("}","").split(",").map(s=>s.trim()).forEach(s=>{i[s]=t[s]}):i[e.item]=t,e.index&&(i[e.index]=r),e.collection&&(i[e.collection]=n),i}function Fi(e){return!Array.isArray(e)&&!isNaN(e)}function _n(){}_n.inline=(e,{expression:t},{cleanup:r})=>{let n=U(e);n._x_refs||(n._x_refs={}),n._x_refs[t]=e,r(()=>delete n._x_refs[t])};p("ref",_n);p("if",(e,{expression:t},{effect:r,cleanup:n})=>{let i=x(e,t),o=()=>{if(e._x_currentIfEl)return e._x_currentIfEl;let a=e.content.cloneNode(!0).firstElementChild;return M(a,{},e),h(()=>{e.after(a),E(a)}),e._x_currentIfEl=a,e._x_undoIf=()=>{T(a,c=>{c._x_effects&&c._x_effects.forEach(ye)}),a.remove(),delete e._x_currentIfEl},a},s=()=>{!e._x_undoIf||(e._x_undoIf(),delete e._x_undoIf)};r(()=>i(a=>{a?o():s()})),n(()=>e._x_undoIf&&e._x_undoIf())});p("id",(e,{expression:t},{evaluate:r})=>{r(t).forEach(i=>ln(e,i))});Z(Te("@",Ce(O("on:"))));p("on",N((e,{value:t,modifiers:r,expression:n},{cleanup:i})=>{let o=n?x(e,n):()=>{};e.tagName.toLowerCase()==="template"&&(e._x_forwardEvents||(e._x_forwardEvents=[]),e._x_forwardEvents.includes(t)||e._x_forwardEvents.push(t));let s=ie(e,t,r,a=>{o(()=>{},{scope:{$event:a},params:[a]})});i(()=>s())}));Je("Collapse","collapse","collapse");Je("Intersect","intersect","intersect");Je("Focus","trap","focus");Je("Mask","mask","mask");function Je(e,t,r){p(t,n=>S(`You can't use [x-${t}] without first installing the "${e}" plugin here: https://alpinejs.dev/plugins/${r}`,n))}j.setEvaluator(ft);j.setReactivityEngine({reactive:Be,effect:Br,release:zr,raw:g});var Ft=j;window.Alpine=Ft;queueMicrotask(()=>{Ft.start()});})(); +`:""}`,t),setTimeout(()=>{throw e},0)}var Ne=!0;function Ie(e){let t=Ne;Ne=!1;let r=e();return Ne=t,r}function R(e,t,r={}){let n;return x(e,t)(i=>n=i,r),n}function x(...e){return dr(...e)}var dr=xt;function pr(e){dr=e}function xt(e,t){let r={};fe(r,e);let n=[r,...j(e)],i=typeof t=="function"?Pn(n,t):Dn(n,t,e);return fr.bind(null,e,t,i)}function Pn(e,t){return(r=()=>{},{scope:n={},params:i=[]}={})=>{let o=t.apply(F([n,...e]),i);Pe(r,o)}}var gt={};function In(e,t){if(gt[e])return gt[e];let r=Object.getPrototypeOf(async function(){}).constructor,n=/^[\n\s]*if.*\(.*\)/.test(e.trim())||/^(let|const)\s/.test(e.trim())?`(async()=>{ ${e} })()`:e,o=(()=>{try{let s=new r(["__self","scope"],`with (scope) { __self.result = ${n} }; __self.finished = true; return __self.result;`);return Object.defineProperty(s,"name",{value:`[Alpine] ${e}`}),s}catch(s){return ee(s,t,e),Promise.resolve()}})();return gt[e]=o,o}function Dn(e,t,r){let n=In(t,r);return(i=()=>{},{scope:o={},params:s=[]}={})=>{n.result=void 0,n.finished=!1;let a=F([o,...e]);if(typeof n=="function"){let c=n(n,a).catch(l=>ee(l,r,t));n.finished?(Pe(i,n.result,a,s,r),n.result=void 0):c.then(l=>{Pe(i,l,a,s,r)}).catch(l=>ee(l,r,t)).finally(()=>n.result=void 0)}}}function Pe(e,t,r,n,i){if(Ne&&typeof t=="function"){let o=t.apply(r,n);o instanceof Promise?o.then(s=>Pe(e,s,r,n)).catch(s=>ee(s,i,t)):e(o)}else typeof t=="object"&&t instanceof Promise?t.then(o=>e(o)):e(t)}var Et="x-";function C(e=""){return Et+e}function mr(e){Et=e}var yt={};function d(e,t){return yt[e]=t,{before(r){if(!yt[r]){console.warn("Cannot find directive `${directive}`. `${name}` will use the default order of execution");return}let n=W.indexOf(r);W.splice(n>=0?n:W.indexOf("DEFAULT"),0,e)}}}function le(e,t,r){if(t=Array.from(t),e._x_virtualDirectives){let o=Object.entries(e._x_virtualDirectives).map(([a,c])=>({name:a,value:c})),s=vt(o);o=o.map(a=>s.find(c=>c.name===a.name)?{name:`x-bind:${a.name}`,value:`"${a.value}"`}:a),t=t.concat(o)}let n={};return t.map(_r((o,s)=>n[o]=s)).filter(xr).map(Ln(n,r)).sort($n).map(o=>kn(e,o))}function vt(e){return Array.from(e).map(_r()).filter(t=>!xr(t))}var bt=!1,de=new Map,hr=Symbol();function ir(e){bt=!0;let t=Symbol();hr=t,de.set(t,[]);let r=()=>{for(;de.get(t).length;)de.get(t).shift()();de.delete(t)},n=()=>{bt=!1,r()};e(r),n()}function _t(e){let t=[],r=a=>t.push(a),[n,i]=Wt(e);return t.push(i),[{Alpine:B,effect:n,cleanup:r,evaluateLater:x.bind(x,e),evaluate:R.bind(R,e)},()=>t.forEach(a=>a())]}function kn(e,t){let r=()=>{},n=yt[t.type]||r,[i,o]=_t(e);Ce(e,t.original,o);let s=()=>{e._x_ignore||e._x_ignoreSelf||(n.inline&&n.inline(e,t,i),n=n.bind(n,e,t,i),bt?de.get(hr).push(n):n())};return s.runCleanups=o,s}var De=(e,t)=>({name:r,value:n})=>(r.startsWith(e)&&(r=r.replace(e,t)),{name:r,value:n}),ke=e=>e;function _r(e=()=>{}){return({name:t,value:r})=>{let{name:n,value:i}=gr.reduce((o,s)=>s(o),{name:t,value:r});return n!==t&&e(n,t),{name:n,value:i}}}var gr=[];function te(e){gr.push(e)}function xr({name:e}){return yr().test(e)}var yr=()=>new RegExp(`^${Et}([^:^.]+)\\b`);function Ln(e,t){return({name:r,value:n})=>{let i=r.match(yr()),o=r.match(/:([a-zA-Z0-9\-_:]+)/),s=r.match(/\.[^.\]]+(?=[^\]]*$)/g)||[],a=t||e[r]||r;return{type:i?i[1]:null,value:o?o[1]:null,modifiers:s.map(c=>c.replace(".","")),expression:n,original:a}}}var wt="DEFAULT",W=["ignore","ref","data","id","anchor","bind","init","for","model","modelable","transition","show","if",wt,"teleport"];function $n(e,t){let r=W.indexOf(e.type)===-1?wt:e.type,n=W.indexOf(t.type)===-1?wt:t.type;return W.indexOf(r)-W.indexOf(n)}var St=[],At=!1;function re(e=()=>{}){return queueMicrotask(()=>{At||setTimeout(()=>{Le()})}),new Promise(t=>{St.push(()=>{e(),t()})})}function Le(){for(At=!1;St.length;)St.shift()()}function br(){At=!0}function pe(e,t){return Array.isArray(t)?wr(e,t.join(" ")):typeof t=="object"&&t!==null?jn(e,t):typeof t=="function"?pe(e,t()):wr(e,t)}function wr(e,t){let r=o=>o.split(" ").filter(Boolean),n=o=>o.split(" ").filter(s=>!e.classList.contains(s)).filter(Boolean),i=o=>(e.classList.add(...o),()=>{e.classList.remove(...o)});return t=t===!0?t="":t||"",i(n(t))}function jn(e,t){let r=a=>a.split(" ").filter(Boolean),n=Object.entries(t).flatMap(([a,c])=>c?r(a):!1).filter(Boolean),i=Object.entries(t).flatMap(([a,c])=>c?!1:r(a)).filter(Boolean),o=[],s=[];return i.forEach(a=>{e.classList.contains(a)&&(e.classList.remove(a),s.push(a))}),n.forEach(a=>{e.classList.contains(a)||(e.classList.add(a),o.push(a))}),()=>{s.forEach(a=>e.classList.add(a)),o.forEach(a=>e.classList.remove(a))}}function G(e,t){return typeof t=="object"&&t!==null?Fn(e,t):Bn(e,t)}function Fn(e,t){let r={};return Object.entries(t).forEach(([n,i])=>{r[n]=e.style[n],n.startsWith("--")||(n=Kn(n)),e.style.setProperty(n,i)}),setTimeout(()=>{e.style.length===0&&e.removeAttribute("style")}),()=>{G(e,r)}}function Bn(e,t){let r=e.getAttribute("style",t);return e.setAttribute("style",t),()=>{e.setAttribute("style",r||"")}}function Kn(e){return e.replace(/([a-z])([A-Z])/g,"$1-$2").toLowerCase()}function me(e,t=()=>{}){let r=!1;return function(){r?t.apply(this,arguments):(r=!0,e.apply(this,arguments))}}d("transition",(e,{value:t,modifiers:r,expression:n},{evaluate:i})=>{typeof n=="function"&&(n=i(n)),n!==!1&&(!n||typeof n=="boolean"?Hn(e,r,t):zn(e,n,t))});function zn(e,t,r){Er(e,pe,""),{enter:i=>{e._x_transition.enter.during=i},"enter-start":i=>{e._x_transition.enter.start=i},"enter-end":i=>{e._x_transition.enter.end=i},leave:i=>{e._x_transition.leave.during=i},"leave-start":i=>{e._x_transition.leave.start=i},"leave-end":i=>{e._x_transition.leave.end=i}}[r](t)}function Hn(e,t,r){Er(e,G);let n=!t.includes("in")&&!t.includes("out")&&!r,i=n||t.includes("in")||["enter"].includes(r),o=n||t.includes("out")||["leave"].includes(r);t.includes("in")&&!n&&(t=t.filter((g,b)=>b<t.indexOf("out"))),t.includes("out")&&!n&&(t=t.filter((g,b)=>b>t.indexOf("out")));let s=!t.includes("opacity")&&!t.includes("scale"),a=s||t.includes("opacity"),c=s||t.includes("scale"),l=a?0:1,u=c?he(t,"scale",95)/100:1,p=he(t,"delay",0)/1e3,m=he(t,"origin","center"),w="opacity, transform",L=he(t,"duration",150)/1e3,we=he(t,"duration",75)/1e3,f="cubic-bezier(0.4, 0.0, 0.2, 1)";i&&(e._x_transition.enter.during={transformOrigin:m,transitionDelay:`${p}s`,transitionProperty:w,transitionDuration:`${L}s`,transitionTimingFunction:f},e._x_transition.enter.start={opacity:l,transform:`scale(${u})`},e._x_transition.enter.end={opacity:1,transform:"scale(1)"}),o&&(e._x_transition.leave.during={transformOrigin:m,transitionDelay:`${p}s`,transitionProperty:w,transitionDuration:`${we}s`,transitionTimingFunction:f},e._x_transition.leave.start={opacity:1,transform:"scale(1)"},e._x_transition.leave.end={opacity:l,transform:`scale(${u})`})}function Er(e,t,r={}){e._x_transition||(e._x_transition={enter:{during:r,start:r,end:r},leave:{during:r,start:r,end:r},in(n=()=>{},i=()=>{}){$e(e,t,{during:this.enter.during,start:this.enter.start,end:this.enter.end},n,i)},out(n=()=>{},i=()=>{}){$e(e,t,{during:this.leave.during,start:this.leave.start,end:this.leave.end},n,i)}})}window.Element.prototype._x_toggleAndCascadeWithTransitions=function(e,t,r,n){let i=document.visibilityState==="visible"?requestAnimationFrame:setTimeout,o=()=>i(r);if(t){e._x_transition&&(e._x_transition.enter||e._x_transition.leave)?e._x_transition.enter&&(Object.entries(e._x_transition.enter.during).length||Object.entries(e._x_transition.enter.start).length||Object.entries(e._x_transition.enter.end).length)?e._x_transition.in(r):o():e._x_transition?e._x_transition.in(r):o();return}e._x_hidePromise=e._x_transition?new Promise((s,a)=>{e._x_transition.out(()=>{},()=>s(n)),e._x_transitioning&&e._x_transitioning.beforeCancel(()=>a({isFromCancelledTransition:!0}))}):Promise.resolve(n),queueMicrotask(()=>{let s=vr(e);s?(s._x_hideChildren||(s._x_hideChildren=[]),s._x_hideChildren.push(e)):i(()=>{let a=c=>{let l=Promise.all([c._x_hidePromise,...(c._x_hideChildren||[]).map(a)]).then(([u])=>u());return delete c._x_hidePromise,delete c._x_hideChildren,l};a(e).catch(c=>{if(!c.isFromCancelledTransition)throw c})})})};function vr(e){let t=e.parentNode;if(t)return t._x_hidePromise?t:vr(t)}function $e(e,t,{during:r,start:n,end:i}={},o=()=>{},s=()=>{}){if(e._x_transitioning&&e._x_transitioning.cancel(),Object.keys(r).length===0&&Object.keys(n).length===0&&Object.keys(i).length===0){o(),s();return}let a,c,l;Vn(e,{start(){a=t(e,n)},during(){c=t(e,r)},before:o,end(){a(),l=t(e,i)},after:s,cleanup(){c(),l()}})}function Vn(e,t){let r,n,i,o=me(()=>{h(()=>{r=!0,n||t.before(),i||(t.end(),Le()),t.after(),e.isConnected&&t.cleanup(),delete e._x_transitioning})});e._x_transitioning={beforeCancels:[],beforeCancel(s){this.beforeCancels.push(s)},cancel:me(function(){for(;this.beforeCancels.length;)this.beforeCancels.shift()();o()}),finish:o},h(()=>{t.start(),t.during()}),br(),requestAnimationFrame(()=>{if(r)return;let s=Number(getComputedStyle(e).transitionDuration.replace(/,.*/,"").replace("s",""))*1e3,a=Number(getComputedStyle(e).transitionDelay.replace(/,.*/,"").replace("s",""))*1e3;s===0&&(s=Number(getComputedStyle(e).animationDuration.replace("s",""))*1e3),h(()=>{t.before()}),n=!0,requestAnimationFrame(()=>{r||(h(()=>{t.end()}),Le(),setTimeout(e._x_transitioning.finish,s+a),i=!0)})})}function he(e,t,r){if(e.indexOf(t)===-1)return r;let n=e[e.indexOf(t)+1];if(!n||t==="scale"&&isNaN(n))return r;if(t==="duration"||t==="delay"){let i=n.match(/([0-9]+)ms/);if(i)return i[1]}return t==="origin"&&["top","right","left","center","bottom"].includes(e[e.indexOf(t)+2])?[n,e[e.indexOf(t)+2]].join(" "):n}var P=!1;function I(e,t=()=>{}){return(...r)=>P?t(...r):e(...r)}function Sr(e){return(...t)=>P&&e(...t)}var Ar=[];function Fe(e){Ar.push(e)}function Or(e,t){Ar.forEach(r=>r(e,t)),P=!0,Tr(()=>{S(t,(r,n)=>{n(r,()=>{})})}),P=!1}var je=!1;function Cr(e,t){t._x_dataStack||(t._x_dataStack=e._x_dataStack),P=!0,je=!0,Tr(()=>{qn(t)}),P=!1,je=!1}function qn(e){let t=!1;S(e,(n,i)=>{O(n,(o,s)=>{if(t&&Qt(o))return s();t=!0,i(o,s)})})}function Tr(e){let t=k;st((r,n)=>{let i=t(r);return $(i),()=>{}}),e(),st(t)}function _e(e,t,r,n=[]){switch(e._x_bindings||(e._x_bindings=T({})),e._x_bindings[t]=r,t=n.includes("camel")?Qn(t):t,t){case"value":Un(e,r);break;case"style":Gn(e,r);break;case"class":Wn(e,r);break;case"selected":case"checked":Jn(e,t,r);break;default:Mr(e,t,r);break}}function Un(e,t){if(e.type==="radio")e.attributes.value===void 0&&(e.value=t),window.fromModel&&(typeof t=="boolean"?e.checked=ge(e.value)===t:e.checked=Rr(e.value,t));else if(e.type==="checkbox")Number.isInteger(t)?e.value=t:!Array.isArray(t)&&typeof t!="boolean"&&![null,void 0].includes(t)?e.value=String(t):Array.isArray(t)?e.checked=t.some(r=>Rr(r,e.value)):e.checked=!!t;else if(e.tagName==="SELECT")Zn(e,t);else{if(e.value===t)return;e.value=t===void 0?"":t}}function Wn(e,t){e._x_undoAddedClasses&&e._x_undoAddedClasses(),e._x_undoAddedClasses=pe(e,t)}function Gn(e,t){e._x_undoAddedStyles&&e._x_undoAddedStyles(),e._x_undoAddedStyles=G(e,t)}function Jn(e,t,r){Mr(e,t,r),Xn(e,t,r)}function Mr(e,t,r){[null,void 0,!1].includes(r)&&ei(t)?e.removeAttribute(t):(Nr(t)&&(r=t),Yn(e,t,r))}function Yn(e,t,r){e.getAttribute(t)!=r&&e.setAttribute(t,r)}function Xn(e,t,r){e[t]!==r&&(e[t]=r)}function Zn(e,t){let r=[].concat(t).map(n=>n+"");Array.from(e.options).forEach(n=>{n.selected=r.includes(n.value)})}function Qn(e){return e.toLowerCase().replace(/-(\w)/g,(t,r)=>r.toUpperCase())}function Rr(e,t){return e==t}function ge(e){return[1,"1","true","on","yes",!0].includes(e)?!0:[0,"0","false","off","no",!1].includes(e)?!1:e?Boolean(e):null}function Nr(e){return["disabled","checked","required","readonly","hidden","open","selected","autofocus","itemscope","multiple","novalidate","allowfullscreen","allowpaymentrequest","formnovalidate","autoplay","controls","loop","muted","playsinline","default","ismap","reversed","async","defer","nomodule"].includes(e)}function ei(e){return!["aria-pressed","aria-checked","aria-expanded","aria-selected"].includes(e)}function Pr(e,t,r){return e._x_bindings&&e._x_bindings[t]!==void 0?e._x_bindings[t]:Dr(e,t,r)}function Ir(e,t,r,n=!0){if(e._x_bindings&&e._x_bindings[t]!==void 0)return e._x_bindings[t];if(e._x_inlineBindings&&e._x_inlineBindings[t]!==void 0){let i=e._x_inlineBindings[t];return i.extract=n,Ie(()=>R(e,i.expression))}return Dr(e,t,r)}function Dr(e,t,r){let n=e.getAttribute(t);return n===null?typeof r=="function"?r():r:n===""?!0:Nr(t)?!![t,"true"].includes(n):n}function Be(e,t){var r;return function(){var n=this,i=arguments,o=function(){r=null,e.apply(n,i)};clearTimeout(r),r=setTimeout(o,t)}}function Ke(e,t){let r;return function(){let n=this,i=arguments;r||(e.apply(n,i),r=!0,setTimeout(()=>r=!1,t))}}function ze({get:e,set:t},{get:r,set:n}){let i=!0,o,s=k(()=>{let a=e(),c=r();if(i)n(Ot(a)),i=!1,o=JSON.stringify(a);else{let l=JSON.stringify(a);l!==o?(n(Ot(a)),o=l):(t(Ot(c)),o=JSON.stringify(c))}JSON.stringify(r()),JSON.stringify(e())});return()=>{$(s)}}function Ot(e){return typeof e=="object"?JSON.parse(JSON.stringify(e)):e}function kr(e){(Array.isArray(e)?e:[e]).forEach(r=>r(B))}var J={},Lr=!1;function $r(e,t){if(Lr||(J=T(J),Lr=!0),t===void 0)return J[e];J[e]=t,typeof t=="object"&&t!==null&&t.hasOwnProperty("init")&&typeof t.init=="function"&&J[e].init(),Re(J[e])}function jr(){return J}var Fr={};function Br(e,t){let r=typeof t!="function"?()=>t:t;return e instanceof Element?Ct(e,r()):(Fr[e]=r,()=>{})}function Kr(e){return Object.entries(Fr).forEach(([t,r])=>{Object.defineProperty(e,t,{get(){return(...n)=>r(...n)}})}),e}function Ct(e,t,r){let n=[];for(;n.length;)n.pop()();let i=Object.entries(t).map(([s,a])=>({name:s,value:a})),o=vt(i);return i=i.map(s=>o.find(a=>a.name===s.name)?{name:`x-bind:${s.name}`,value:`"${s.value}"`}:s),le(e,i,r).map(s=>{n.push(s.runCleanups),s()}),()=>{for(;n.length;)n.pop()()}}var zr={};function Hr(e,t){zr[e]=t}function Vr(e,t){return Object.entries(zr).forEach(([r,n])=>{Object.defineProperty(e,r,{get(){return(...i)=>n.bind(t)(...i)},enumerable:!1})}),e}var ti={get reactive(){return T},get release(){return $},get effect(){return k},get raw(){return ot},version:"3.13.3",flushAndStopDeferringMutations:lr,dontAutoEvaluateFunctions:Ie,disableEffectScheduling:qt,startObservingMutations:ce,stopObservingMutations:dt,setReactivityEngine:Ut,onAttributeRemoved:Ce,onAttributesAdded:Ae,closestDataStack:j,skipDuringClone:I,onlyDuringClone:Sr,addRootSelector:ve,addInitSelector:Se,interceptClone:Fe,addScopeToNode:N,deferMutations:cr,mapAttributes:te,evaluateLater:x,interceptInit:tr,setEvaluator:pr,mergeProxies:F,extractProp:Ir,findClosest:Z,onElRemoved:Q,closestRoot:U,destroyTree:ae,interceptor:Me,transition:$e,setStyles:G,mutateDom:h,directive:d,entangle:ze,throttle:Ke,debounce:Be,evaluate:R,initTree:S,nextTick:re,prefixed:C,prefix:mr,plugin:kr,magic:y,store:$r,start:Jt,clone:Cr,cloneNode:Or,bound:Pr,$data:Te,walk:O,data:Hr,bind:Br},B=ti;function Tt(e,t){let r=Object.create(null),n=e.split(",");for(let i=0;i<n.length;i++)r[n[i]]=!0;return t?i=>!!r[i.toLowerCase()]:i=>!!r[i]}var ri="itemscope,allowfullscreen,formnovalidate,ismap,nomodule,novalidate,readonly";var Ps=Tt(ri+",async,autofocus,autoplay,controls,default,defer,disabled,hidden,loop,open,required,reversed,scoped,seamless,checked,muted,multiple,selected");var qr=Object.freeze({}),Is=Object.freeze([]);var ni=Object.prototype.hasOwnProperty,xe=(e,t)=>ni.call(e,t),K=Array.isArray,ne=e=>Ur(e)==="[object Map]";var ii=e=>typeof e=="string",He=e=>typeof e=="symbol",ye=e=>e!==null&&typeof e=="object";var oi=Object.prototype.toString,Ur=e=>oi.call(e),Rt=e=>Ur(e).slice(8,-1);var Ve=e=>ii(e)&&e!=="NaN"&&e[0]!=="-"&&""+parseInt(e,10)===e;var qe=e=>{let t=Object.create(null);return r=>t[r]||(t[r]=e(r))},si=/-(\w)/g,Ds=qe(e=>e.replace(si,(t,r)=>r?r.toUpperCase():"")),ai=/\B([A-Z])/g,ks=qe(e=>e.replace(ai,"-$1").toLowerCase()),Mt=qe(e=>e.charAt(0).toUpperCase()+e.slice(1)),Ls=qe(e=>e?`on${Mt(e)}`:""),Nt=(e,t)=>e!==t&&(e===e||t===t);var Pt=new WeakMap,be=[],D,Y=Symbol("iterate"),It=Symbol("Map key iterate");function ci(e){return e&&e._isEffect===!0}function Zr(e,t=qr){ci(e)&&(e=e.raw);let r=ui(e,t);return t.lazy||r(),r}function Qr(e){e.active&&(en(e),e.options.onStop&&e.options.onStop(),e.active=!1)}var li=0;function ui(e,t){let r=function(){if(!r.active)return e();if(!be.includes(r)){en(r);try{return di(),be.push(r),D=r,e()}finally{be.pop(),tn(),D=be[be.length-1]}}};return r.id=li++,r.allowRecurse=!!t.allowRecurse,r._isEffect=!0,r.active=!0,r.raw=e,r.deps=[],r.options=t,r}function en(e){let{deps:t}=e;if(t.length){for(let r=0;r<t.length;r++)t[r].delete(e);t.length=0}}var ie=!0,kt=[];function fi(){kt.push(ie),ie=!1}function di(){kt.push(ie),ie=!0}function tn(){let e=kt.pop();ie=e===void 0?!0:e}function M(e,t,r){if(!ie||D===void 0)return;let n=Pt.get(e);n||Pt.set(e,n=new Map);let i=n.get(r);i||n.set(r,i=new Set),i.has(D)||(i.add(D),D.deps.push(i),D.options.onTrack&&D.options.onTrack({effect:D,target:e,type:t,key:r}))}function H(e,t,r,n,i,o){let s=Pt.get(e);if(!s)return;let a=new Set,c=u=>{u&&u.forEach(p=>{(p!==D||p.allowRecurse)&&a.add(p)})};if(t==="clear")s.forEach(c);else if(r==="length"&&K(e))s.forEach((u,p)=>{(p==="length"||p>=n)&&c(u)});else switch(r!==void 0&&c(s.get(r)),t){case"add":K(e)?Ve(r)&&c(s.get("length")):(c(s.get(Y)),ne(e)&&c(s.get(It)));break;case"delete":K(e)||(c(s.get(Y)),ne(e)&&c(s.get(It)));break;case"set":ne(e)&&c(s.get(Y));break}let l=u=>{u.options.onTrigger&&u.options.onTrigger({effect:u,target:e,key:r,type:t,newValue:n,oldValue:i,oldTarget:o}),u.options.scheduler?u.options.scheduler(u):u()};a.forEach(l)}var pi=Tt("__proto__,__v_isRef,__isVue"),rn=new Set(Object.getOwnPropertyNames(Symbol).map(e=>Symbol[e]).filter(He)),mi=nn();var hi=nn(!0);var Wr=_i();function _i(){let e={};return["includes","indexOf","lastIndexOf"].forEach(t=>{e[t]=function(...r){let n=_(this);for(let o=0,s=this.length;o<s;o++)M(n,"get",o+"");let i=n[t](...r);return i===-1||i===!1?n[t](...r.map(_)):i}}),["push","pop","shift","unshift","splice"].forEach(t=>{e[t]=function(...r){fi();let n=_(this)[t].apply(this,r);return tn(),n}}),e}function nn(e=!1,t=!1){return function(n,i,o){if(i==="__v_isReactive")return!e;if(i==="__v_isReadonly")return e;if(i==="__v_raw"&&o===(e?t?Pi:cn:t?Ni:an).get(n))return n;let s=K(n);if(!e&&s&&xe(Wr,i))return Reflect.get(Wr,i,o);let a=Reflect.get(n,i,o);return(He(i)?rn.has(i):pi(i))||(e||M(n,"get",i),t)?a:Dt(a)?!s||!Ve(i)?a.value:a:ye(a)?e?ln(a):Ze(a):a}}var gi=xi();function xi(e=!1){return function(r,n,i,o){let s=r[n];if(!e&&(i=_(i),s=_(s),!K(r)&&Dt(s)&&!Dt(i)))return s.value=i,!0;let a=K(r)&&Ve(n)?Number(n)<r.length:xe(r,n),c=Reflect.set(r,n,i,o);return r===_(o)&&(a?Nt(i,s)&&H(r,"set",n,i,s):H(r,"add",n,i)),c}}function yi(e,t){let r=xe(e,t),n=e[t],i=Reflect.deleteProperty(e,t);return i&&r&&H(e,"delete",t,void 0,n),i}function bi(e,t){let r=Reflect.has(e,t);return(!He(t)||!rn.has(t))&&M(e,"has",t),r}function wi(e){return M(e,"iterate",K(e)?"length":Y),Reflect.ownKeys(e)}var Ei={get:mi,set:gi,deleteProperty:yi,has:bi,ownKeys:wi},vi={get:hi,set(e,t){return console.warn(`Set operation on key "${String(t)}" failed: target is readonly.`,e),!0},deleteProperty(e,t){return console.warn(`Delete operation on key "${String(t)}" failed: target is readonly.`,e),!0}};var Lt=e=>ye(e)?Ze(e):e,$t=e=>ye(e)?ln(e):e,jt=e=>e,Xe=e=>Reflect.getPrototypeOf(e);function Ue(e,t,r=!1,n=!1){e=e.__v_raw;let i=_(e),o=_(t);t!==o&&!r&&M(i,"get",t),!r&&M(i,"get",o);let{has:s}=Xe(i),a=n?jt:r?$t:Lt;if(s.call(i,t))return a(e.get(t));if(s.call(i,o))return a(e.get(o));e!==i&&e.get(t)}function We(e,t=!1){let r=this.__v_raw,n=_(r),i=_(e);return e!==i&&!t&&M(n,"has",e),!t&&M(n,"has",i),e===i?r.has(e):r.has(e)||r.has(i)}function Ge(e,t=!1){return e=e.__v_raw,!t&&M(_(e),"iterate",Y),Reflect.get(e,"size",e)}function Gr(e){e=_(e);let t=_(this);return Xe(t).has.call(t,e)||(t.add(e),H(t,"add",e,e)),this}function Jr(e,t){t=_(t);let r=_(this),{has:n,get:i}=Xe(r),o=n.call(r,e);o?sn(r,n,e):(e=_(e),o=n.call(r,e));let s=i.call(r,e);return r.set(e,t),o?Nt(t,s)&&H(r,"set",e,t,s):H(r,"add",e,t),this}function Yr(e){let t=_(this),{has:r,get:n}=Xe(t),i=r.call(t,e);i?sn(t,r,e):(e=_(e),i=r.call(t,e));let o=n?n.call(t,e):void 0,s=t.delete(e);return i&&H(t,"delete",e,void 0,o),s}function Xr(){let e=_(this),t=e.size!==0,r=ne(e)?new Map(e):new Set(e),n=e.clear();return t&&H(e,"clear",void 0,void 0,r),n}function Je(e,t){return function(n,i){let o=this,s=o.__v_raw,a=_(s),c=t?jt:e?$t:Lt;return!e&&M(a,"iterate",Y),s.forEach((l,u)=>n.call(i,c(l),c(u),o))}}function Ye(e,t,r){return function(...n){let i=this.__v_raw,o=_(i),s=ne(o),a=e==="entries"||e===Symbol.iterator&&s,c=e==="keys"&&s,l=i[e](...n),u=r?jt:t?$t:Lt;return!t&&M(o,"iterate",c?It:Y),{next(){let{value:p,done:m}=l.next();return m?{value:p,done:m}:{value:a?[u(p[0]),u(p[1])]:u(p),done:m}},[Symbol.iterator](){return this}}}}function z(e){return function(...t){{let r=t[0]?`on key "${t[0]}" `:"";console.warn(`${Mt(e)} operation ${r}failed: target is readonly.`,_(this))}return e==="delete"?!1:this}}function Si(){let e={get(o){return Ue(this,o)},get size(){return Ge(this)},has:We,add:Gr,set:Jr,delete:Yr,clear:Xr,forEach:Je(!1,!1)},t={get(o){return Ue(this,o,!1,!0)},get size(){return Ge(this)},has:We,add:Gr,set:Jr,delete:Yr,clear:Xr,forEach:Je(!1,!0)},r={get(o){return Ue(this,o,!0)},get size(){return Ge(this,!0)},has(o){return We.call(this,o,!0)},add:z("add"),set:z("set"),delete:z("delete"),clear:z("clear"),forEach:Je(!0,!1)},n={get(o){return Ue(this,o,!0,!0)},get size(){return Ge(this,!0)},has(o){return We.call(this,o,!0)},add:z("add"),set:z("set"),delete:z("delete"),clear:z("clear"),forEach:Je(!0,!0)};return["keys","values","entries",Symbol.iterator].forEach(o=>{e[o]=Ye(o,!1,!1),r[o]=Ye(o,!0,!1),t[o]=Ye(o,!1,!0),n[o]=Ye(o,!0,!0)}),[e,r,t,n]}var[Ai,Oi,Ci,Ti]=Si();function on(e,t){let r=t?e?Ti:Ci:e?Oi:Ai;return(n,i,o)=>i==="__v_isReactive"?!e:i==="__v_isReadonly"?e:i==="__v_raw"?n:Reflect.get(xe(r,i)&&i in n?r:n,i,o)}var Ri={get:on(!1,!1)};var Mi={get:on(!0,!1)};function sn(e,t,r){let n=_(r);if(n!==r&&t.call(e,n)){let i=Rt(e);console.warn(`Reactive ${i} contains both the raw and reactive versions of the same object${i==="Map"?" as keys":""}, which can lead to inconsistencies. Avoid differentiating between the raw and reactive versions of an object and only use the reactive version if possible.`)}}var an=new WeakMap,Ni=new WeakMap,cn=new WeakMap,Pi=new WeakMap;function Ii(e){switch(e){case"Object":case"Array":return 1;case"Map":case"Set":case"WeakMap":case"WeakSet":return 2;default:return 0}}function Di(e){return e.__v_skip||!Object.isExtensible(e)?0:Ii(Rt(e))}function Ze(e){return e&&e.__v_isReadonly?e:un(e,!1,Ei,Ri,an)}function ln(e){return un(e,!0,vi,Mi,cn)}function un(e,t,r,n,i){if(!ye(e))return console.warn(`value cannot be made reactive: ${String(e)}`),e;if(e.__v_raw&&!(t&&e.__v_isReactive))return e;let o=i.get(e);if(o)return o;let s=Di(e);if(s===0)return e;let a=new Proxy(e,s===2?n:r);return i.set(e,a),a}function _(e){return e&&_(e.__v_raw)||e}function Dt(e){return Boolean(e&&e.__v_isRef===!0)}y("nextTick",()=>re);y("dispatch",e=>q.bind(q,e));y("watch",(e,{evaluateLater:t,effect:r})=>(n,i)=>{let o=t(n),s=!0,a,c=r(()=>o(l=>{JSON.stringify(l),s?a=l:queueMicrotask(()=>{i(l,a),a=l}),s=!1}));e._x_effects.delete(c)});y("store",jr);y("data",e=>Te(e));y("root",e=>U(e));y("refs",e=>(e._x_refs_proxy||(e._x_refs_proxy=F(ki(e))),e._x_refs_proxy));function ki(e){let t=[],r=e;for(;r;)r._x_refs&&t.push(r._x_refs),r=r.parentNode;return t}var Ft={};function Bt(e){return Ft[e]||(Ft[e]=0),++Ft[e]}function fn(e,t){return Z(e,r=>{if(r._x_ids&&r._x_ids[t])return!0})}function dn(e,t){e._x_ids||(e._x_ids={}),e._x_ids[t]||(e._x_ids[t]=Bt(t))}y("id",e=>(t,r=null)=>{let n=fn(e,t),i=n?n._x_ids[t]:Bt(t);return r?`${t}-${i}-${r}`:`${t}-${i}`});y("el",e=>e);pn("Focus","focus","focus");pn("Persist","persist","persist");function pn(e,t,r){y(t,n=>v(`You can't use [$${t}] without first installing the "${e}" plugin here: https://alpinejs.dev/plugins/${r}`,n))}d("modelable",(e,{expression:t},{effect:r,evaluateLater:n,cleanup:i})=>{let o=n(t),s=()=>{let u;return o(p=>u=p),u},a=n(`${t} = __placeholder`),c=u=>a(()=>{},{scope:{__placeholder:u}}),l=s();c(l),queueMicrotask(()=>{if(!e._x_model)return;e._x_removeModelListeners.default();let u=e._x_model.get,p=e._x_model.set,m=ze({get(){return u()},set(w){p(w)}},{get(){return s()},set(w){c(w)}});i(m)})});d("teleport",(e,{modifiers:t,expression:r},{cleanup:n})=>{e.tagName.toLowerCase()!=="template"&&v("x-teleport can only be used on a <template> tag",e);let i=mn(r),o=e.content.cloneNode(!0).firstElementChild;e._x_teleport=o,o._x_teleportBack=e,e.setAttribute("data-teleport-template",!0),o.setAttribute("data-teleport-target",!0),e._x_forwardEvents&&e._x_forwardEvents.forEach(a=>{o.addEventListener(a,c=>{c.stopPropagation(),e.dispatchEvent(new c.constructor(c.type,c))})}),N(o,{},e);let s=(a,c,l)=>{l.includes("prepend")?c.parentNode.insertBefore(a,c):l.includes("append")?c.parentNode.insertBefore(a,c.nextSibling):c.appendChild(a)};h(()=>{s(o,i,t),S(o),o._x_ignore=!0}),e._x_teleportPutBack=()=>{let a=mn(r);h(()=>{s(e._x_teleport,a,t)})},n(()=>o.remove())});var Li=document.createElement("div");function mn(e){let t=I(()=>document.querySelector(e),()=>Li)();return t||v(`Cannot find x-teleport element for selector: "${e}"`),t}var hn=()=>{};hn.inline=(e,{modifiers:t},{cleanup:r})=>{t.includes("self")?e._x_ignoreSelf=!0:e._x_ignore=!0,r(()=>{t.includes("self")?delete e._x_ignoreSelf:delete e._x_ignore})};d("ignore",hn);d("effect",I((e,{expression:t},{effect:r})=>{r(x(e,t))}));function oe(e,t,r,n){let i=e,o=c=>n(c),s={},a=(c,l)=>u=>l(c,u);if(r.includes("dot")&&(t=$i(t)),r.includes("camel")&&(t=ji(t)),r.includes("passive")&&(s.passive=!0),r.includes("capture")&&(s.capture=!0),r.includes("window")&&(i=window),r.includes("document")&&(i=document),r.includes("debounce")){let c=r[r.indexOf("debounce")+1]||"invalid-wait",l=Qe(c.split("ms")[0])?Number(c.split("ms")[0]):250;o=Be(o,l)}if(r.includes("throttle")){let c=r[r.indexOf("throttle")+1]||"invalid-wait",l=Qe(c.split("ms")[0])?Number(c.split("ms")[0]):250;o=Ke(o,l)}return r.includes("prevent")&&(o=a(o,(c,l)=>{l.preventDefault(),c(l)})),r.includes("stop")&&(o=a(o,(c,l)=>{l.stopPropagation(),c(l)})),r.includes("self")&&(o=a(o,(c,l)=>{l.target===e&&c(l)})),(r.includes("away")||r.includes("outside"))&&(i=document,o=a(o,(c,l)=>{e.contains(l.target)||l.target.isConnected!==!1&&(e.offsetWidth<1&&e.offsetHeight<1||e._x_isShown!==!1&&c(l))})),r.includes("once")&&(o=a(o,(c,l)=>{c(l),i.removeEventListener(t,o,s)})),o=a(o,(c,l)=>{Bi(t)&&Ki(l,r)||c(l)}),i.addEventListener(t,o,s),()=>{i.removeEventListener(t,o,s)}}function $i(e){return e.replace(/-/g,".")}function ji(e){return e.toLowerCase().replace(/-(\w)/g,(t,r)=>r.toUpperCase())}function Qe(e){return!Array.isArray(e)&&!isNaN(e)}function Fi(e){return[" ","_"].includes(e)?e:e.replace(/([a-z])([A-Z])/g,"$1-$2").replace(/[_\s]/,"-").toLowerCase()}function Bi(e){return["keydown","keyup"].includes(e)}function Ki(e,t){let r=t.filter(o=>!["window","document","prevent","stop","once","capture"].includes(o));if(r.includes("debounce")){let o=r.indexOf("debounce");r.splice(o,Qe((r[o+1]||"invalid-wait").split("ms")[0])?2:1)}if(r.includes("throttle")){let o=r.indexOf("throttle");r.splice(o,Qe((r[o+1]||"invalid-wait").split("ms")[0])?2:1)}if(r.length===0||r.length===1&&_n(e.key).includes(r[0]))return!1;let i=["ctrl","shift","alt","meta","cmd","super"].filter(o=>r.includes(o));return r=r.filter(o=>!i.includes(o)),!(i.length>0&&i.filter(s=>((s==="cmd"||s==="super")&&(s="meta"),e[`${s}Key`])).length===i.length&&_n(e.key).includes(r[0]))}function _n(e){if(!e)return[];e=Fi(e);let t={ctrl:"control",slash:"/",space:" ",spacebar:" ",cmd:"meta",esc:"escape",up:"arrow-up",down:"arrow-down",left:"arrow-left",right:"arrow-right",period:".",equal:"=",minus:"-",underscore:"_"};return t[e]=e,Object.keys(t).map(r=>{if(t[r]===e)return r}).filter(r=>r)}d("model",(e,{modifiers:t,expression:r},{effect:n,cleanup:i})=>{let o=e;t.includes("parent")&&(o=e.parentNode);let s=x(o,r),a;typeof r=="string"?a=x(o,`${r} = __placeholder`):typeof r=="function"&&typeof r()=="string"?a=x(o,`${r()} = __placeholder`):a=()=>{};let c=()=>{let m;return s(w=>m=w),gn(m)?m.get():m},l=m=>{let w;s(L=>w=L),gn(w)?w.set(m):a(()=>{},{scope:{__placeholder:m}})};typeof r=="string"&&e.type==="radio"&&h(()=>{e.hasAttribute("name")||e.setAttribute("name",r)});var u=e.tagName.toLowerCase()==="select"||["checkbox","radio"].includes(e.type)||t.includes("lazy")?"change":"input";let p=P?()=>{}:oe(e,u,t,m=>{l(zi(e,t,m,c()))});if(t.includes("fill")&&([null,""].includes(c())||e.type==="checkbox"&&Array.isArray(c()))&&e.dispatchEvent(new Event(u,{})),e._x_removeModelListeners||(e._x_removeModelListeners={}),e._x_removeModelListeners.default=p,i(()=>e._x_removeModelListeners.default()),e.form){let m=oe(e.form,"reset",[],w=>{re(()=>e._x_model&&e._x_model.set(e.value))});i(()=>m())}e._x_model={get(){return c()},set(m){l(m)}},e._x_forceModelUpdate=m=>{m===void 0&&typeof r=="string"&&r.match(/\./)&&(m=""),window.fromModel=!0,h(()=>_e(e,"value",m)),delete window.fromModel},n(()=>{let m=c();t.includes("unintrusive")&&document.activeElement.isSameNode(e)||e._x_forceModelUpdate(m)})});function zi(e,t,r,n){return h(()=>{if(r instanceof CustomEvent&&r.detail!==void 0)return r.detail!==null&&r.detail!==void 0?r.detail:r.target.value;if(e.type==="checkbox")if(Array.isArray(n)){let i=null;return t.includes("number")?i=Kt(r.target.value):t.includes("boolean")?i=ge(r.target.value):i=r.target.value,r.target.checked?n.concat([i]):n.filter(o=>!Hi(o,i))}else return r.target.checked;else return e.tagName.toLowerCase()==="select"&&e.multiple?t.includes("number")?Array.from(r.target.selectedOptions).map(i=>{let o=i.value||i.text;return Kt(o)}):t.includes("boolean")?Array.from(r.target.selectedOptions).map(i=>{let o=i.value||i.text;return ge(o)}):Array.from(r.target.selectedOptions).map(i=>i.value||i.text):t.includes("number")?Kt(r.target.value):t.includes("boolean")?ge(r.target.value):t.includes("trim")?r.target.value.trim():r.target.value})}function Kt(e){let t=e?parseFloat(e):null;return Vi(t)?t:e}function Hi(e,t){return e==t}function Vi(e){return!Array.isArray(e)&&!isNaN(e)}function gn(e){return e!==null&&typeof e=="object"&&typeof e.get=="function"&&typeof e.set=="function"}d("cloak",e=>queueMicrotask(()=>h(()=>e.removeAttribute(C("cloak")))));Se(()=>`[${C("init")}]`);d("init",I((e,{expression:t},{evaluate:r})=>typeof t=="string"?!!t.trim()&&r(t,{},!1):r(t,{},!1)));d("text",(e,{expression:t},{effect:r,evaluateLater:n})=>{let i=n(t);r(()=>{i(o=>{h(()=>{e.textContent=o})})})});d("html",(e,{expression:t},{effect:r,evaluateLater:n})=>{let i=n(t);r(()=>{i(o=>{h(()=>{e.innerHTML=o,e._x_ignoreSelf=!0,S(e),delete e._x_ignoreSelf})})})});te(De(":",ke(C("bind:"))));var xn=(e,{value:t,modifiers:r,expression:n,original:i},{effect:o})=>{if(!t){let a={};Kr(a),x(e,n)(l=>{Ct(e,l,i)},{scope:a});return}if(t==="key")return qi(e,n);if(e._x_inlineBindings&&e._x_inlineBindings[t]&&e._x_inlineBindings[t].extract)return;let s=x(e,n);o(()=>s(a=>{a===void 0&&typeof n=="string"&&n.match(/\./)&&(a=""),h(()=>_e(e,t,a,r))}))};xn.inline=(e,{value:t,modifiers:r,expression:n})=>{t&&(e._x_inlineBindings||(e._x_inlineBindings={}),e._x_inlineBindings[t]={expression:n,extract:!1})};d("bind",xn);function qi(e,t){e._x_keyExpression=t}ve(()=>`[${C("data")}]`);d("data",(e,{expression:t},{cleanup:r})=>{if(Ui(e))return;t=t===""?"{}":t;let n={};fe(n,e);let i={};Vr(i,n);let o=R(e,t,{scope:i});(o===void 0||o===!0)&&(o={}),fe(o,e);let s=T(o);Re(s);let a=N(e,s);s.init&&R(e,s.init),r(()=>{s.destroy&&R(e,s.destroy),a()})});Fe((e,t)=>{e._x_dataStack&&(t._x_dataStack=e._x_dataStack,t.setAttribute("data-has-alpine-state",!0))});function Ui(e){return P?je?!0:e.hasAttribute("data-has-alpine-state"):!1}d("show",(e,{modifiers:t,expression:r},{effect:n})=>{let i=x(e,r);e._x_doHide||(e._x_doHide=()=>{h(()=>{e.style.setProperty("display","none",t.includes("important")?"important":void 0)})}),e._x_doShow||(e._x_doShow=()=>{h(()=>{e.style.length===1&&e.style.display==="none"?e.removeAttribute("style"):e.style.removeProperty("display")})});let o=()=>{e._x_doHide(),e._x_isShown=!1},s=()=>{e._x_doShow(),e._x_isShown=!0},a=()=>setTimeout(s),c=me(p=>p?s():o(),p=>{typeof e._x_toggleAndCascadeWithTransitions=="function"?e._x_toggleAndCascadeWithTransitions(e,p,s,o):p?a():o()}),l,u=!0;n(()=>i(p=>{!u&&p===l||(t.includes("immediate")&&(p?a():o()),c(p),l=p,u=!1)}))});d("for",(e,{expression:t},{effect:r,cleanup:n})=>{let i=Gi(t),o=x(e,i.items),s=x(e,e._x_keyExpression||"index");e._x_prevKeys=[],e._x_lookup={},r(()=>Wi(e,i,o,s)),n(()=>{Object.values(e._x_lookup).forEach(a=>a.remove()),delete e._x_prevKeys,delete e._x_lookup})});function Wi(e,t,r,n){let i=s=>typeof s=="object"&&!Array.isArray(s),o=e;r(s=>{Ji(s)&&s>=0&&(s=Array.from(Array(s).keys(),f=>f+1)),s===void 0&&(s=[]);let a=e._x_lookup,c=e._x_prevKeys,l=[],u=[];if(i(s))s=Object.entries(s).map(([f,g])=>{let b=yn(t,g,f,s);n(E=>u.push(E),{scope:{index:f,...b}}),l.push(b)});else for(let f=0;f<s.length;f++){let g=yn(t,s[f],f,s);n(b=>u.push(b),{scope:{index:f,...g}}),l.push(g)}let p=[],m=[],w=[],L=[];for(let f=0;f<c.length;f++){let g=c[f];u.indexOf(g)===-1&&w.push(g)}c=c.filter(f=>!w.includes(f));let we="template";for(let f=0;f<u.length;f++){let g=u[f],b=c.indexOf(g);if(b===-1)c.splice(f,0,g),p.push([we,f]);else if(b!==f){let E=c.splice(f,1)[0],A=c.splice(b-1,1)[0];c.splice(f,0,A),c.splice(b,0,E),m.push([E,A])}else L.push(g);we=g}for(let f=0;f<w.length;f++){let g=w[f];a[g]._x_effects&&a[g]._x_effects.forEach(Ee),a[g].remove(),a[g]=null,delete a[g]}for(let f=0;f<m.length;f++){let[g,b]=m[f],E=a[g],A=a[b],X=document.createElement("div");h(()=>{A||v('x-for ":key" is undefined or invalid',o),A.after(X),E.after(A),A._x_currentIfEl&&A.after(A._x_currentIfEl),X.before(E),E._x_currentIfEl&&E.after(E._x_currentIfEl),X.remove()}),A._x_refreshXForScope(l[u.indexOf(b)])}for(let f=0;f<p.length;f++){let[g,b]=p[f],E=g==="template"?o:a[g];E._x_currentIfEl&&(E=E._x_currentIfEl);let A=l[b],X=u[b],se=document.importNode(o.content,!0).firstElementChild,Ht=T(A);N(se,Ht,o),se._x_refreshXForScope=wn=>{Object.entries(wn).forEach(([En,vn])=>{Ht[En]=vn})},h(()=>{E.after(se),S(se)}),typeof X=="object"&&v("x-for key cannot be an object, it must be a string or an integer",o),a[X]=se}for(let f=0;f<L.length;f++)a[L[f]]._x_refreshXForScope(l[u.indexOf(L[f])]);o._x_prevKeys=u})}function Gi(e){let t=/,([^,\}\]]*)(?:,([^,\}\]]*))?$/,r=/^\s*\(|\)\s*$/g,n=/([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/,i=e.match(n);if(!i)return;let o={};o.items=i[2].trim();let s=i[1].replace(r,"").trim(),a=s.match(t);return a?(o.item=s.replace(t,"").trim(),o.index=a[1].trim(),a[2]&&(o.collection=a[2].trim())):o.item=s,o}function yn(e,t,r,n){let i={};return/^\[.*\]$/.test(e.item)&&Array.isArray(t)?e.item.replace("[","").replace("]","").split(",").map(s=>s.trim()).forEach((s,a)=>{i[s]=t[a]}):/^\{.*\}$/.test(e.item)&&!Array.isArray(t)&&typeof t=="object"?e.item.replace("{","").replace("}","").split(",").map(s=>s.trim()).forEach(s=>{i[s]=t[s]}):i[e.item]=t,e.index&&(i[e.index]=r),e.collection&&(i[e.collection]=n),i}function Ji(e){return!Array.isArray(e)&&!isNaN(e)}function bn(){}bn.inline=(e,{expression:t},{cleanup:r})=>{let n=U(e);n._x_refs||(n._x_refs={}),n._x_refs[t]=e,r(()=>delete n._x_refs[t])};d("ref",bn);d("if",(e,{expression:t},{effect:r,cleanup:n})=>{e.tagName.toLowerCase()!=="template"&&v("x-if can only be used on a <template> tag",e);let i=x(e,t),o=()=>{if(e._x_currentIfEl)return e._x_currentIfEl;let a=e.content.cloneNode(!0).firstElementChild;return N(a,{},e),h(()=>{e.after(a),S(a)}),e._x_currentIfEl=a,e._x_undoIf=()=>{O(a,c=>{c._x_effects&&c._x_effects.forEach(Ee)}),a.remove(),delete e._x_currentIfEl},a},s=()=>{e._x_undoIf&&(e._x_undoIf(),delete e._x_undoIf)};r(()=>i(a=>{a?o():s()})),n(()=>e._x_undoIf&&e._x_undoIf())});d("id",(e,{expression:t},{evaluate:r})=>{r(t).forEach(i=>dn(e,i))});te(De("@",ke(C("on:"))));d("on",I((e,{value:t,modifiers:r,expression:n},{cleanup:i})=>{let o=n?x(e,n):()=>{};e.tagName.toLowerCase()==="template"&&(e._x_forwardEvents||(e._x_forwardEvents=[]),e._x_forwardEvents.includes(t)||e._x_forwardEvents.push(t));let s=oe(e,t,r,a=>{o(()=>{},{scope:{$event:a},params:[a]})});i(()=>s())}));et("Collapse","collapse","collapse");et("Intersect","intersect","intersect");et("Focus","trap","focus");et("Mask","mask","mask");function et(e,t,r){d(t,n=>v(`You can't use [x-${t}] without first installing the "${e}" plugin here: https://alpinejs.dev/plugins/${r}`,n))}B.setEvaluator(xt);B.setReactivityEngine({reactive:Ze,effect:Zr,release:Qr,raw:_});var zt=B;window.Alpine=zt;queueMicrotask(()=>{zt.start()});})(); diff --git a/hypha/static_src/src/javascript/apply/vendor/htmx.min.js b/hypha/static_src/src/javascript/apply/vendor/htmx.min.js index f889d0ea52bb3f7f20e0866e46568a2f987c1d1f..6c0a0f27f1abd522355d0d815bd73ab77aa79276 100644 --- a/hypha/static_src/src/javascript/apply/vendor/htmx.min.js +++ b/hypha/static_src/src/javascript/apply/vendor/htmx.min.js @@ -1 +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 z={onLoad:t,process:Tt,on:le,off:ue,trigger:ie,ajax:dr,find:b,findAll:f,closest:d,values:function(e,t){var r=Jt(e,t||"post");return r.values},remove:B,addClass:j,removeClass:n,toggleClass:U,takeClass:V,defineExtension:yr,removeExtension:br,logAll:F,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,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},parseInterval:v,_:e,createEventSource:function(e){return new EventSource(e,{withCredentials:true})},createWebSocket:function(e){var t=new WebSocket(e,[]);t.binaryType=z.config.wsBinaryType;return t},version:"1.9.2"};var C={addTriggerHandler:xt,bodyContains:ee,canAccessLocalStorage:D,filterValues:er,hasAttribute:q,getAttributeValue:G,getClosestMatch:c,getExpressionVars:fr,getHeaders:Qt,getInputValues:Jt,getInternalData:Y,getSwapSpecification:rr,getTriggerSpecs:ze,getTarget:de,makeFragment:l,mergeObjects:te,makeSettleInfo:S,oobSwap:me,selectAndSwap:Me,settleImmediately:Bt,shouldCancel:Ke,triggerEvent:ie,triggerErrorEvent:ne,withExtensions:w};var R=["get","post","put","delete","patch"];var O=R.map(function(e){return"[hx-"+e+"], [data-hx-"+e+"]"}).join(", ");function v(e){if(e==undefined){return undefined}if(e.slice(-2)=="ms"){return parseFloat(e.slice(0,-2))||undefined}if(e.slice(-1)=="s"){return parseFloat(e.slice(0,-1))*1e3||undefined}if(e.slice(-1)=="m"){return parseFloat(e.slice(0,-1))*1e3*60||undefined}return parseFloat(e)||undefined}function $(e,t){return e.getAttribute&&e.getAttribute(t)}function q(e,t){return e.hasAttribute&&(e.hasAttribute(t)||e.hasAttribute("data-"+t))}function G(e,t){return $(e,t)||$(e,"data-"+t)}function u(e){return e.parentElement}function J(){return document}function c(e,t){while(e&&!t(e)){e=u(e)}return e?e:null}function T(e,t,r){var n=G(t,r);var i=G(t,"hx-disinherit");if(e!==t&&i&&(i==="*"||i.split(" ").indexOf(r)>=0)){return"unset"}else{return n}}function Z(t,r){var n=null;c(t,function(e){return n=T(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 H(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 i(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=J().createDocumentFragment()}return i}function L(e){return e.match(/<body/)}function l(e){var t=!L(e);if(z.config.useTemplateFragments&&t){var r=i("<body><template>"+e+"</template></body>",0);return r.querySelector("template").content}else{var n=H(e);switch(n){case"thead":case"tbody":case"tfoot":case"colgroup":case"caption":return i("<table>"+e+"</table>",1);case"col":return i("<table><colgroup>"+e+"</colgroup></table>",2);case"tr":return i("<table><tbody>"+e+"</tbody></table>",2);case"td":case"th":return i("<table><tbody><tr>"+e+"</tr></tbody></table>",3);case"script":return i("<div>"+e+"</div>",1);default:return i(e,0)}}}function K(e){if(e){e()}}function A(e,t){return Object.prototype.toString.call(e)==="[object "+t+"]"}function N(e){return A(e,"Function")}function I(e){return A(e,"Object")}function Y(e){var t="htmx-internal-data";var r=e[t];if(!r){r=e[t]={}}return r}function k(e){var t=[];if(e){for(var r=0;r<e.length;r++){t.push(e[r])}}return t}function Q(e,t){if(e){for(var r=0;r<e.length;r++){t(e[r])}}}function P(e){var t=e.getBoundingClientRect();var r=t.top;var n=t.bottom;return r<window.innerHeight&&n>=0}function ee(e){if(e.getRootNode&&e.getRootNode()instanceof ShadowRoot){return J().body.contains(e.getRootNode().host)}else{return J().body.contains(e)}}function M(e){return e.trim().split(/\s+/)}function te(e,t){for(var r in t){if(t.hasOwnProperty(r)){e[r]=t[r]}}return e}function y(e){try{return JSON.parse(e)}catch(e){x(e);return null}}function D(){var e="htmx:localStorageTest";try{localStorage.setItem(e,e);localStorage.removeItem(e);return true}catch(e){return false}}function X(t){try{var e=new URL(t);if(e){t=e.pathname+e.search}if(!t.match("^/$")){t=t.replace(/\/+$/,"")}return t}catch(e){return t}}function e(e){return sr(J().body,function(){return eval(e)})}function t(t){var e=z.on("htmx:load",function(e){t(e.detail.elt)});return e}function F(){z.logger=function(e,t,r){if(console){console.log(t,e,r)}}}function b(e,t){if(t){return e.querySelector(t)}else{return b(J(),e)}}function f(e,t){if(t){return e.querySelectorAll(t)}else{return f(J(),e)}}function B(e,t){e=s(e);if(t){setTimeout(function(){B(e);e=null},t)}else{e.parentElement.removeChild(e)}}function j(e,t,r){e=s(e);if(r){setTimeout(function(){j(e,t);e=null},r)}else{e.classList&&e.classList.add(t)}}function n(e,t,r){e=s(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 U(e,t){e=s(e);e.classList.toggle(t)}function V(e,t){e=s(e);Q(e.parentElement.children,function(e){n(e,t)});j(e,t)}function d(e,t){e=s(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 r(e){var t=e.trim();if(t.startsWith("<")&&t.endsWith("/>")){return t.substring(1,t.length-2)}else{return t}}function _(e,t){if(t.indexOf("closest ")===0){return[d(e,r(t.substr(8)))]}else if(t.indexOf("find ")===0){return[b(e,r(t.substr(5)))]}else if(t.indexOf("next ")===0){return[W(e,r(t.substr(5)))]}else if(t.indexOf("previous ")===0){return[oe(e,r(t.substr(9)))]}else if(t==="document"){return[document]}else if(t==="window"){return[window]}else{return J().querySelectorAll(r(t))}}var W=function(e,t){var r=J().querySelectorAll(t);for(var n=0;n<r.length;n++){var i=r[n];if(i.compareDocumentPosition(e)===Node.DOCUMENT_POSITION_PRECEDING){return i}}};var oe=function(e,t){var r=J().querySelectorAll(t);for(var n=r.length-1;n>=0;n--){var i=r[n];if(i.compareDocumentPosition(e)===Node.DOCUMENT_POSITION_FOLLOWING){return i}}};function re(e,t){if(t){return _(e,t)[0]}else{return _(J().body,e)[0]}}function s(e){if(A(e,"String")){return b(e)}else{return e}}function se(e,t,r){if(N(t)){return{target:J().body,event:e,listener:t}}else{return{target:s(e),event:t,listener:r}}}function le(t,r,n){Sr(function(){var e=se(t,r,n);e.target.addEventListener(e.event,e.listener)});var e=N(r);return e?r:n}function ue(t,r,n){Sr(function(){var e=se(t,r,n);e.target.removeEventListener(e.event,e.listener)});return N(r)?r:n}var fe=J().createElement("output");function ce(e,t){var r=Z(e,t);if(r){if(r==="this"){return[he(e,t)]}else{var n=_(e,r);if(n.length===0){x('The selector "'+r+'" on '+t+" returned no matches!");return[fe]}else{return n}}}}function he(e,t){return c(e,function(e){return G(e,t)!=null})}function de(e){var t=Z(e,"hx-target");if(t){if(t==="this"){return he(e,"hx-target")}else{return re(e,t)}}else{var r=Y(e);if(r.boosted){return J().body}else{return e}}}function ve(e){var t=z.config.attributesToSettle;for(var r=0;r<t.length;r++){if(e===t[r]){return true}}return false}function ge(t,r){Q(t.attributes,function(e){if(!r.hasAttribute(e.name)&&ve(e.name)){t.removeAttribute(e.name)}});Q(r.attributes,function(e){if(ve(e.name)){t.setAttribute(e.name,e.value)}})}function pe(e,t){var r=wr(t);for(var n=0;n<r.length;n++){var i=r[n];try{if(i.isInlineSwap(e)){return true}}catch(e){x(e)}}return e==="outerHTML"}function me(e,i,a){var t="#"+i.id;var o="outerHTML";if(e==="true"){}else if(e.indexOf(":")>0){o=e.substr(0,e.indexOf(":"));t=e.substr(e.indexOf(":")+1,e.length)}else{o=e}var r=J().querySelectorAll(t);if(r){Q(r,function(e){var t;var r=i.cloneNode(true);t=J().createDocumentFragment();t.appendChild(r);if(!pe(o,e)){t=r}var n={shouldSwap:true,target:e,fragment:t};if(!ie(e,"htmx:oobBeforeSwap",n))return;e=n.target;if(n["shouldSwap"]){ke(o,e,e,t,a)}Q(a.elts,function(e){ie(e,"htmx:oobAfterSwap",n)})});i.parentNode.removeChild(i)}else{i.parentNode.removeChild(i);ne(J().body,"htmx:oobErrorNoTarget",{content:i})}return e}function xe(e,t,r){var n=Z(e,"hx-select-oob");if(n){var i=n.split(",");for(let e=0;e<i.length;e++){var a=i[e].split(":",2);var o=a[0].trim();if(o.indexOf("#")===0){o=o.substring(1)}var s=a[1]||"true";var l=t.querySelector("#"+o);if(l){me(s,l,r)}}}Q(f(t,"[hx-swap-oob], [data-hx-swap-oob]"),function(e){var t=G(e,"hx-swap-oob");if(t!=null){me(t,e,r)}})}function ye(e){Q(f(e,"[hx-preserve], [data-hx-preserve]"),function(e){var t=G(e,"id");var r=J().getElementById(t);if(r!=null){e.parentNode.replaceChild(r,e)}})}function be(a,e,o){Q(e.querySelectorAll("[id]"),function(e){if(e.id&&e.id.length>0){var t=e.id.replace("'","\\'");var r=e.tagName.replace(":","\\:");var n=a.querySelector(r+"[id='"+t+"']");if(n&&n!==a){var i=e.cloneNode();ge(e,n);o.tasks.push(function(){ge(e,i)})}}})}function we(e){return function(){n(e,z.config.addedClass);Tt(e);bt(e);Se(e);ie(e,"htmx:load")}}function Se(e){var t="[autofocus]";var r=h(e,t)?e:e.querySelector(t);if(r!=null){r.focus()}}function a(e,t,r,n){be(e,r,n);while(r.childNodes.length>0){var i=r.firstChild;j(i,z.config.addedClass);e.insertBefore(i,t);if(i.nodeType!==Node.TEXT_NODE&&i.nodeType!==Node.COMMENT_NODE){n.tasks.push(we(i))}}}function Ee(e,t){var r=0;while(r<e.length){t=(t<<5)-t+e.charCodeAt(r++)|0}return t}function Ce(e){var t=0;if(e.attributes){for(var r=0;r<e.attributes.length;r++){var n=e.attributes[r];if(n.value){t=Ee(n.name,t);t=Ee(n.value,t)}}}return t}function Re(t){var r=Y(t);if(r.timeout){clearTimeout(r.timeout)}if(r.webSocket){r.webSocket.close()}if(r.sseEventSource){r.sseEventSource.close()}if(r.listenerInfos){Q(r.listenerInfos,function(e){if(e.on){e.on.removeEventListener(e.trigger,e.listener)}})}if(r.onHandlers){for(let e=0;e<r.onHandlers.length;e++){const n=r.onHandlers[e];t.removeEventListener(n.name,n.handler)}}}function o(e){ie(e,"htmx:beforeCleanupElement");Re(e);if(e.children){Q(e.children,function(e){o(e)})}}function Oe(e,t,r){if(e.tagName==="BODY"){return Ne(e,t,r)}else{var n;var i=e.previousSibling;a(u(e),e,t,r);if(i==null){n=u(e).firstChild}else{n=i.nextSibling}Y(e).replacedWith=n;r.elts=[];while(n&&n!==e){if(n.nodeType===Node.ELEMENT_NODE){r.elts.push(n)}n=n.nextElementSibling}o(e);u(e).removeChild(e)}}function qe(e,t,r){return a(e,e.firstChild,t,r)}function Te(e,t,r){return a(u(e),e,t,r)}function He(e,t,r){return a(e,null,t,r)}function Le(e,t,r){return a(u(e),e.nextSibling,t,r)}function Ae(e,t,r){o(e);return u(e).removeChild(e)}function Ne(e,t,r){var n=e.firstChild;a(e,n,t,r);if(n){while(n.nextSibling){o(n.nextSibling);e.removeChild(n.nextSibling)}o(n);e.removeChild(n)}}function Ie(e,t){var r=Z(e,"hx-select");if(r){var n=J().createDocumentFragment();Q(t.querySelectorAll(r),function(e){n.appendChild(e)});t=n}return t}function ke(e,t,r,n,i){switch(e){case"none":return;case"outerHTML":Oe(r,n,i);return;case"afterbegin":qe(r,n,i);return;case"beforebegin":Te(r,n,i);return;case"beforeend":He(r,n,i);return;case"afterend":Le(r,n,i);return;case"delete":Ae(r,n,i);return;default:var a=wr(t);for(var o=0;o<a.length;o++){var s=a[o];try{var l=s.handleSwap(e,r,n,i);if(l){if(typeof l.length!=="undefined"){for(var u=0;u<l.length;u++){var f=l[u];if(f.nodeType!==Node.TEXT_NODE&&f.nodeType!==Node.COMMENT_NODE){i.tasks.push(we(f))}}}return}}catch(e){x(e)}}if(e==="innerHTML"){Ne(r,n,i)}else{ke(z.config.defaultSwapStyle,t,r,n,i)}}}function Pe(e){if(e.indexOf("<title")>-1){var t=e.replace(/<svg(\s[^>]*>|>)([\s\S]*?)<\/svg>/gim,"");var r=t.match(/<title(\s[^>]*>|>)([\s\S]*?)<\/title>/im);if(r){return r[2]}}}function Me(e,t,r,n,i){i.title=Pe(n);var a=l(n);if(a){xe(r,a,i);a=Ie(r,a);ye(a);return ke(e,r,t,a,i)}}function De(e,t,r){var n=e.getResponseHeader(t);if(n.indexOf("{")===0){var i=y(n);for(var a in i){if(i.hasOwnProperty(a)){var o=i[a];if(!I(o)){o={value:o}}ie(r,a,o)}}}else{ie(r,n,[])}}var Xe=/\s/;var g=/[\s,]/;var Fe=/[_$a-zA-Z]/;var Be=/[_$a-zA-Z0-9]/;var je=['"',"'","/"];var p=/[^\s]/;function Ue(e){var t=[];var r=0;while(r<e.length){if(Fe.exec(e.charAt(r))){var n=r;while(Be.exec(e.charAt(r+1))){r++}t.push(e.substr(n,r-n+1))}else if(je.indexOf(e.charAt(r))!==-1){var i=e.charAt(r);var n=r;r++;while(r<e.length&&e.charAt(r)!==i){if(e.charAt(r)==="\\"){r++}r++}t.push(e.substr(n,r-n+1))}else{var a=e.charAt(r);t.push(a)}r++}return t}function Ve(e,t,r){return Fe.exec(e.charAt(0))&&e!=="true"&&e!=="false"&&e!=="this"&&e!==r&&t!=="."}function _e(e,t,r){if(t[0]==="["){t.shift();var n=1;var i=" return (function("+r+"){ return (";var a=null;while(t.length>0){var o=t[0];if(o==="]"){n--;if(n===0){if(a===null){i=i+"true"}t.shift();i+=")})";try{var s=sr(e,function(){return Function(i)()},function(){return true});s.source=i;return s}catch(e){ne(J().body,"htmx:syntax:error",{error:e,source:i});return null}}}else if(o==="["){n++}if(Ve(o,a,r)){i+="(("+r+"."+o+") ? ("+r+"."+o+") : (window."+o+"))"}else{i=i+o}a=t.shift()}}}function m(e,t){var r="";while(e.length>0&&!e[0].match(t)){r+=e.shift()}return r}var We="input, textarea, select";function ze(e){var t=G(e,"hx-trigger");var r=[];if(t){var n=Ue(t);do{m(n,p);var i=n.length;var a=m(n,/[,\[\s]/);if(a!==""){if(a==="every"){var o={trigger:"every"};m(n,p);o.pollInterval=v(m(n,/[,\[\s]/));m(n,p);var s=_e(e,n,"event");if(s){o.eventFilter=s}r.push(o)}else if(a.indexOf("sse:")===0){r.push({trigger:"sse",sseEvent:a.substr(4)})}else{var l={trigger:a};var s=_e(e,n,"event");if(s){l.eventFilter=s}while(n.length>0&&n[0]!==","){m(n,p);var u=n.shift();if(u==="changed"){l.changed=true}else if(u==="once"){l.once=true}else if(u==="consume"){l.consume=true}else if(u==="delay"&&n[0]===":"){n.shift();l.delay=v(m(n,g))}else if(u==="from"&&n[0]===":"){n.shift();var f=m(n,g);if(f==="closest"||f==="find"||f==="next"||f==="previous"){n.shift();f+=" "+m(n,g)}l.from=f}else if(u==="target"&&n[0]===":"){n.shift();l.target=m(n,g)}else if(u==="throttle"&&n[0]===":"){n.shift();l.throttle=v(m(n,g))}else if(u==="queue"&&n[0]===":"){n.shift();l.queue=m(n,g)}else if((u==="root"||u==="threshold")&&n[0]===":"){n.shift();l[u]=m(n,g)}else{ne(e,"htmx:syntax:error",{token:n.shift()})}}r.push(l)}}if(n.length===i){ne(e,"htmx:syntax:error",{token:n.shift()})}m(n,p)}while(n[0]===","&&n.shift())}if(r.length>0){return r}else if(h(e,"form")){return[{trigger:"submit"}]}else if(h(e,'input[type="button"]')){return[{trigger:"click"}]}else if(h(e,We)){return[{trigger:"change"}]}else{return[{trigger:"click"}]}}function $e(e){Y(e).cancelled=true}function Ge(e,t,r){var n=Y(e);n.timeout=setTimeout(function(){if(ee(e)&&n.cancelled!==true){if(!Qe(r,Lt("hx:poll:trigger",{triggerSpec:r,target:e}))){t(e)}Ge(e,t,r)}},r.pollInterval)}function Je(e){return location.hostname===e.hostname&&$(e,"href")&&$(e,"href").indexOf("#")!==0}function Ze(t,r,e){if(t.tagName==="A"&&Je(t)&&(t.target===""||t.target==="_self")||t.tagName==="FORM"){r.boosted=true;var n,i;if(t.tagName==="A"){n="get";i=t.href}else{var a=$(t,"method");n=a?a.toLowerCase():"get";if(n==="get"){}i=$(t,"action")}e.forEach(function(e){et(t,function(e,t){ae(n,i,e,t)},r,e,true)})}}function Ke(e,t){if(e.type==="submit"||e.type==="click"){if(t.tagName==="FORM"){return true}if(h(t,'input[type="submit"], button')&&d(t,"form")!==null){return true}if(t.tagName==="A"&&t.href&&(t.getAttribute("href")==="#"||t.getAttribute("href").indexOf("#")!==0)){return true}}return false}function Ye(e,t){return Y(e).boosted&&e.tagName==="A"&&t.type==="click"&&(t.ctrlKey||t.metaKey)}function Qe(e,t){var r=e.eventFilter;if(r){try{return r(t)!==true}catch(e){ne(J().body,"htmx:eventFilter:error",{error:e,source:r.source});return true}}return false}function et(i,a,e,o,s){var l=Y(i);var t;if(o.from){t=_(i,o.from)}else{t=[i]}if(o.changed){l.lastValue=i.value}Q(t,function(r){var n=function(e){if(!ee(i)){r.removeEventListener(o.trigger,n);return}if(Ye(i,e)){return}if(s||Ke(e,i)){e.preventDefault()}if(Qe(o,e)){return}var t=Y(e);t.triggerSpec=o;if(t.handledFor==null){t.handledFor=[]}if(t.handledFor.indexOf(i)<0){t.handledFor.push(i);if(o.consume){e.stopPropagation()}if(o.target&&e.target){if(!h(e.target,o.target)){return}}if(o.once){if(l.triggeredOnce){return}else{l.triggeredOnce=true}}if(o.changed){if(l.lastValue===i.value){return}else{l.lastValue=i.value}}if(l.delayed){clearTimeout(l.delayed)}if(l.throttle){return}if(o.throttle){if(!l.throttle){a(i,e);l.throttle=setTimeout(function(){l.throttle=null},o.throttle)}}else if(o.delay){l.delayed=setTimeout(function(){a(i,e)},o.delay)}else{ie(i,"htmx:trigger");a(i,e)}}};if(e.listenerInfos==null){e.listenerInfos=[]}e.listenerInfos.push({trigger:o.trigger,listener:n,on:r});r.addEventListener(o.trigger,n)})}var tt=false;var rt=null;function nt(){if(!rt){rt=function(){tt=true};window.addEventListener("scroll",rt);setInterval(function(){if(tt){tt=false;Q(J().querySelectorAll("[hx-trigger='revealed'],[data-hx-trigger='revealed']"),function(e){it(e)})}},200)}}function it(t){if(!q(t,"data-hx-revealed")&&P(t)){t.setAttribute("data-hx-revealed","true");var e=Y(t);if(e.initHash){ie(t,"revealed")}else{t.addEventListener("htmx:afterProcessNode",function(e){ie(t,"revealed")},{once:true})}}}function at(e,t,r){var n=M(r);for(var i=0;i<n.length;i++){var a=n[i].split(/:(.+)/);if(a[0]==="connect"){ot(e,a[1],0)}if(a[0]==="send"){lt(e)}}}function ot(s,r,n){if(!ee(s)){return}if(r.indexOf("/")==0){var e=location.hostname+(location.port?":"+location.port:"");if(location.protocol=="https:"){r="wss://"+e+r}else if(location.protocol=="http:"){r="ws://"+e+r}}var t=z.createWebSocket(r);t.onerror=function(e){ne(s,"htmx:wsError",{error:e,socket:t});st(s)};t.onclose=function(e){if([1006,1012,1013].indexOf(e.code)>=0){var t=ut(n);setTimeout(function(){ot(s,r,n+1)},t)}};t.onopen=function(e){n=0};Y(s).webSocket=t;t.addEventListener("message",function(e){if(st(s)){return}var t=e.data;w(s,function(e){t=e.transformResponse(t,null,s)});var r=S(s);var n=l(t);var i=k(n.children);for(var a=0;a<i.length;a++){var o=i[a];me(G(o,"hx-swap-oob")||"true",o,r)}Bt(r.tasks)})}function st(e){if(!ee(e)){Y(e).webSocket.close();return true}}function lt(u){var f=c(u,function(e){return Y(e).webSocket!=null});if(f){u.addEventListener(ze(u)[0].trigger,function(e){var t=Y(f).webSocket;var r=Qt(u,f);var n=Jt(u,"post");var i=n.errors;var a=n.values;var o=fr(u);var s=te(a,o);var l=er(s,u);l["HEADERS"]=r;if(i&&i.length>0){ie(u,"htmx:validation:halted",i);return}t.send(JSON.stringify(l));if(Ke(e,u)){e.preventDefault()}})}else{ne(u,"htmx:noWebSocketSourceError")}}function ut(e){var t=z.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()}x('htmx.config.wsReconnectDelay must either be a function or the string "full-jitter"')}function ft(e,t,r){var n=M(r);for(var i=0;i<n.length;i++){var a=n[i].split(/:(.+)/);if(a[0]==="connect"){ct(e,a[1])}if(a[0]==="swap"){ht(e,a[1])}}}function ct(t,e){var r=z.createEventSource(e);r.onerror=function(e){ne(t,"htmx:sseError",{error:e,source:r});vt(t)};Y(t).sseEventSource=r}function ht(a,o){var s=c(a,gt);if(s){var l=Y(s).sseEventSource;var u=function(e){if(vt(s)){l.removeEventListener(o,u);return}var t=e.data;w(a,function(e){t=e.transformResponse(t,null,a)});var r=rr(a);var n=de(a);var i=S(a);Me(r.swapStyle,a,n,t,i);Bt(i.tasks);ie(a,"htmx:sseMessage",e)};Y(a).sseListener=u;l.addEventListener(o,u)}else{ne(a,"htmx:noSSESourceError")}}function dt(e,t,r){var n=c(e,gt);if(n){var i=Y(n).sseEventSource;var a=function(){if(!vt(n)){if(ee(e)){t(e)}else{i.removeEventListener(r,a)}}};Y(e).sseListener=a;i.addEventListener(r,a)}else{ne(e,"htmx:noSSESourceError")}}function vt(e){if(!ee(e)){Y(e).sseEventSource.close();return true}}function gt(e){return Y(e).sseEventSource!=null}function pt(e,t,r,n){var i=function(){if(!r.loaded){r.loaded=true;t(e)}};if(n){setTimeout(i,n)}else{i()}}function mt(t,i,e){var a=false;Q(R,function(r){if(q(t,"hx-"+r)){var n=G(t,"hx-"+r);a=true;i.path=n;i.verb=r;e.forEach(function(e){xt(t,e,i,function(e,t){ae(r,n,e,t)})})}});return a}function xt(n,e,t,r){if(e.sseEvent){dt(n,r,e.sseEvent)}else if(e.trigger==="revealed"){nt();et(n,r,t,e);it(n)}else if(e.trigger==="intersect"){var i={};if(e.root){i.root=re(n,e.root)}if(e.threshold){i.threshold=parseFloat(e.threshold)}var a=new IntersectionObserver(function(e){for(var t=0;t<e.length;t++){var r=e[t];if(r.isIntersecting){ie(n,"intersect");break}}},i);a.observe(n);et(n,r,t,e)}else if(e.trigger==="load"){if(!Qe(e,Lt("load",{elt:n}))){pt(n,r,t,e.delay)}}else if(e.pollInterval){t.polling=true;Ge(n,r,e)}else{et(n,r,t,e)}}function yt(e){if(e.type==="text/javascript"||e.type==="module"||e.type===""){var t=J().createElement("script");Q(e.attributes,function(e){t.setAttribute(e.name,e.value)});t.textContent=e.textContent;t.async=false;if(z.config.inlineScriptNonce){t.nonce=z.config.inlineScriptNonce}var r=e.parentElement;try{r.insertBefore(t,e)}catch(e){x(e)}finally{if(e.parentElement){e.parentElement.removeChild(e)}}}}function bt(e){if(h(e,"script")){yt(e)}Q(f(e,"script"),function(e){yt(e)})}function wt(){return document.querySelector("[hx-boost], [data-hx-boost]")}function St(e){if(e.querySelectorAll){var t=wt()?", a, form":"";var r=e.querySelectorAll(O+t+", [hx-sse], [data-hx-sse], [hx-ws],"+" [data-hx-ws], [hx-ext], [data-hx-ext], [hx-trigger], [data-hx-trigger], [hx-on], [data-hx-on]");return r}else{return[]}}function Et(n){var e=function(e){var t=d(e.target,"button, input[type='submit']");if(t!==null){var r=Y(n);r.lastButtonClicked=t}};n.addEventListener("click",e);n.addEventListener("focusin",e);n.addEventListener("focusout",function(e){var t=Y(n);t.lastButtonClicked=null})}function Ct(e){var t=Ue(e);var r=0;for(let e=0;e<t.length;e++){const n=t[e];if(n==="{"){r++}else if(n==="}"){r--}}return r}function Rt(t,e,r){var n=Y(t);n.onHandlers=[];var i=new Function("event",r+"; return;");var a=t.addEventListener(e,function(e){return i.call(t,e)});n.onHandlers.push({event:e,listener:a});return{nodeData:n,code:r,func:i,listener:a}}function Ot(e){var t=G(e,"hx-on");if(t){var r={};var n=t.split("\n");var i=null;var a=0;while(n.length>0){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+=Ct(o)}for(var l in r){Rt(e,l,r[l])}}}function qt(t){if(t.closest&&t.closest(z.config.disableSelector)){return}var r=Y(t);if(r.initHash!==Ce(t)){r.initHash=Ce(t);Re(t);Ot(t);ie(t,"htmx:beforeProcessNode");if(t.value){r.lastValue=t.value}var e=ze(t);var n=mt(t,r,e);if(!n){if(Z(t,"hx-boost")==="true"){Ze(t,r,e)}else if(q(t,"hx-trigger")){e.forEach(function(e){xt(t,e,r,function(){})})}}if(t.tagName==="FORM"){Et(t)}var i=G(t,"hx-sse");if(i){ft(t,r,i)}var a=G(t,"hx-ws");if(a){at(t,r,a)}ie(t,"htmx:afterProcessNode")}}function Tt(e){e=s(e);qt(e);Q(St(e),function(e){qt(e)})}function Ht(e){return e.replace(/([a-z0-9])([A-Z])/g,"$1-$2").toLowerCase()}function Lt(e,t){var r;if(window.CustomEvent&&typeof window.CustomEvent==="function"){r=new CustomEvent(e,{bubbles:true,cancelable:true,detail:t})}else{r=J().createEvent("CustomEvent");r.initCustomEvent(e,true,true,t)}return r}function ne(e,t,r){ie(e,t,te({error:t},r))}function At(e){return e==="htmx:afterProcessNode"}function w(e,t){Q(wr(e),function(e){try{t(e)}catch(e){x(e)}})}function x(e){if(console.error){console.error(e)}else if(console.log){console.log("ERROR: ",e)}}function ie(e,t,r){e=s(e);if(r==null){r={}}r["elt"]=e;var n=Lt(t,r);if(z.logger&&!At(t)){z.logger(e,t,r)}if(r.error){x(r.error);ie(e,"htmx:error",{errorInfo:r})}var i=e.dispatchEvent(n);var a=Ht(t);if(i&&a!==t){var o=Lt(a,n.detail);i=i&&e.dispatchEvent(o)}w(e,function(e){i=i&&e.onEvent(t,n)!==false});return i}var Nt=location.pathname+location.search;function It(){var e=J().querySelector("[hx-history-elt],[data-hx-history-elt]");return e||J().body}function kt(e,t,r,n){if(!D()){return}e=X(e);var i=y(localStorage.getItem("htmx-history-cache"))||[];for(var a=0;a<i.length;a++){if(i[a].url===e){i.splice(a,1);break}}var o={url:e,content:t,title:r,scroll:n};ie(J().body,"htmx:historyItemCreated",{item:o,cache:i});i.push(o);while(i.length>z.config.historyCacheSize){i.shift()}while(i.length>0){try{localStorage.setItem("htmx-history-cache",JSON.stringify(i));break}catch(e){ne(J().body,"htmx:historyCacheError",{cause:e,cache:i});i.shift()}}}function Pt(e){if(!D()){return null}e=X(e);var t=y(localStorage.getItem("htmx-history-cache"))||[];for(var r=0;r<t.length;r++){if(t[r].url===e){return t[r]}}return null}function Mt(e){var t=z.config.requestClass;var r=e.cloneNode(true);Q(f(r,"."+t),function(e){n(e,t)});return r.innerHTML}function Dt(){var e=It();var t=Nt||location.pathname+location.search;var r=J().querySelector('[hx-history="false" i],[data-hx-history="false" i]');if(!r){ie(J().body,"htmx:beforeHistorySave",{path:t,historyElt:e});kt(t,Mt(e),J().title,window.scrollY)}if(z.config.historyEnabled)history.replaceState({htmx:true},J().title,window.location.href)}function Xt(e){if(z.config.getCacheBusterParam){e=e.replace(/org\.htmx\.cache-buster=[^&]*&?/,"");if(e.endsWith("&")||e.endsWith("?")){e=e.slice(0,-1)}}if(z.config.historyEnabled){history.pushState({htmx:true},"",e)}Nt=e}function Ft(e){if(z.config.historyEnabled)history.replaceState({htmx:true},"",e);Nt=e}function Bt(e){Q(e,function(e){e.call()})}function jt(a){var e=new XMLHttpRequest;var o={path:a,xhr:e};ie(J().body,"htmx:historyCacheMiss",o);e.open("GET",a,true);e.setRequestHeader("HX-History-Restore-Request","true");e.onload=function(){if(this.status>=200&&this.status<400){ie(J().body,"htmx:historyCacheMissLoad",o);var e=l(this.response);e=e.querySelector("[hx-history-elt],[data-hx-history-elt]")||e;var t=It();var r=S(t);var n=Pe(this.response);if(n){var i=b("title");if(i){i.innerHTML=n}else{window.document.title=n}}Ne(t,e,r);Bt(r.tasks);Nt=a;ie(J().body,"htmx:historyRestore",{path:a,cacheMiss:true,serverResponse:this.response})}else{ne(J().body,"htmx:historyCacheMissLoadError",o)}};e.send()}function Ut(e){Dt();e=e||location.pathname+location.search;var t=Pt(e);if(t){var r=l(t.content);var n=It();var i=S(n);Ne(n,r,i);Bt(i.tasks);document.title=t.title;window.scrollTo(0,t.scroll);Nt=e;ie(J().body,"htmx:historyRestore",{path:e,item:t})}else{if(z.config.refreshOnHistoryMiss){window.location.reload(true)}else{jt(e)}}}function Vt(e){var t=ce(e,"hx-indicator");if(t==null){t=[e]}Q(t,function(e){var t=Y(e);t.requestCount=(t.requestCount||0)+1;e.classList["add"].call(e.classList,z.config.requestClass)});return t}function _t(e){Q(e,function(e){var t=Y(e);t.requestCount=(t.requestCount||0)-1;if(t.requestCount===0){e.classList["remove"].call(e.classList,z.config.requestClass)}})}function Wt(e,t){for(var r=0;r<e.length;r++){var n=e[r];if(n.isSameNode(t)){return true}}return false}function zt(e){if(e.name===""||e.name==null||e.disabled){return false}if(e.type==="button"||e.type==="submit"||e.tagName==="image"||e.tagName==="reset"||e.tagName==="file"){return false}if(e.type==="checkbox"||e.type==="radio"){return e.checked}return true}function $t(t,r,n,e,i){if(e==null||Wt(t,e)){return}else{t.push(e)}if(zt(e)){var a=$(e,"name");var o=e.value;if(e.multiple){o=k(e.querySelectorAll("option:checked")).map(function(e){return e.value})}if(e.files){o=k(e.files)}if(a!=null&&o!=null){var s=r[a];if(s!==undefined){if(Array.isArray(s)){if(Array.isArray(o)){r[a]=s.concat(o)}else{s.push(o)}}else{if(Array.isArray(o)){r[a]=[s].concat(o)}else{r[a]=[s,o]}}}else{r[a]=o}}if(i){Gt(e,n)}}if(h(e,"form")){var l=e.elements;Q(l,function(e){$t(t,r,n,e,i)})}}function Gt(e,t){if(e.willValidate){ie(e,"htmx:validation:validate");if(!e.checkValidity()){t.push({elt:e,message:e.validationMessage,validity:e.validity});ie(e,"htmx:validation:failed",{message:e.validationMessage,validity:e.validity})}}}function Jt(e,t){var r=[];var n={};var i={};var a=[];var o=Y(e);var s=h(e,"form")&&e.noValidate!==true||G(e,"hx-validate")==="true";if(o.lastButtonClicked){s=s&&o.lastButtonClicked.formNoValidate!==true}if(t!=="get"){$t(r,i,a,d(e,"form"),s)}$t(r,n,a,e,s);if(o.lastButtonClicked){var l=$(o.lastButtonClicked,"name");if(l){n[l]=o.lastButtonClicked.value}}var u=ce(e,"hx-include");Q(u,function(e){$t(r,n,a,e,s);if(!h(e,"form")){Q(e.querySelectorAll(We),function(e){$t(r,n,a,e,s)})}});n=te(n,i);return{errors:a,values:n}}function Zt(e,t,r){if(e!==""){e+="&"}if(String(r)==="[object Object]"){r=JSON.stringify(r)}var n=encodeURIComponent(r);e+=encodeURIComponent(t)+"="+n;return e}function Kt(e){var t="";for(var r in e){if(e.hasOwnProperty(r)){var n=e[r];if(Array.isArray(n)){Q(n,function(e){t=Zt(t,r,e)})}else{t=Zt(t,r,n)}}}return t}function Yt(e){var t=new FormData;for(var r in e){if(e.hasOwnProperty(r)){var n=e[r];if(Array.isArray(n)){Q(n,function(e){t.append(r,e)})}else{t.append(r,n)}}}return t}function Qt(e,t,r){var n={"HX-Request":"true","HX-Trigger":$(e,"id"),"HX-Trigger-Name":$(e,"name"),"HX-Target":G(t,"id"),"HX-Current-URL":J().location.href};or(e,"hx-headers",false,n);if(r!==undefined){n["HX-Prompt"]=r}if(Y(e).boosted){n["HX-Boosted"]="true"}return n}function er(t,e){var r=Z(e,"hx-params");if(r){if(r==="none"){return{}}else if(r==="*"){return t}else if(r.indexOf("not ")===0){Q(r.substr(4).split(","),function(e){e=e.trim();delete t[e]});return t}else{var n={};Q(r.split(","),function(e){e=e.trim();n[e]=t[e]});return n}}else{return t}}function tr(e){return $(e,"href")&&$(e,"href").indexOf("#")>=0}function rr(e,t){var r=t?t:Z(e,"hx-swap");var n={swapStyle:Y(e).boosted?"innerHTML":z.config.defaultSwapStyle,swapDelay:z.config.defaultSwapDelay,settleDelay:z.config.defaultSettleDelay};if(Y(e).boosted&&!tr(e)){n["show"]="top"}if(r){var i=M(r);if(i.length>0){n["swapStyle"]=i[0];for(var a=1;a<i.length;a++){var o=i[a];if(o.indexOf("swap:")===0){n["swapDelay"]=v(o.substr(5))}if(o.indexOf("settle:")===0){n["settleDelay"]=v(o.substr(7))}if(o.indexOf("transition:")===0){n["transition"]=o.substr(11)==="true"}if(o.indexOf("scroll:")===0){var s=o.substr(7);var l=s.split(":");var u=l.pop();var f=l.length>0?l.join(":"):null;n["scroll"]=u;n["scrollTarget"]=f}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}if(o.indexOf("focus-scroll:")===0){var d=o.substr("focus-scroll:".length);n["focusScroll"]=d=="true"}}}}return n}function nr(e){return Z(e,"hx-encoding")==="multipart/form-data"||h(e,"form")&&$(e,"enctype")==="multipart/form-data"}function ir(t,r,n){var i=null;w(r,function(e){if(i==null){i=e.encodeParameters(t,n,r)}});if(i!=null){return i}else{if(nr(r)){return Yt(n)}else{return Kt(n)}}}function S(e){return{tasks:[],elts:[e]}}function ar(e,t){var r=e[0];var n=e[e.length-1];if(t.scroll){var i=null;if(t.scrollTarget){i=re(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=re(r,a)}if(t.show==="top"&&(r||i)){i=i||r;i.scrollIntoView({block:"start",behavior:z.config.scrollBehavior})}if(t.show==="bottom"&&(n||i)){i=i||n;i.scrollIntoView({block:"end",behavior:z.config.scrollBehavior})}}}function or(e,t,r,n){if(n==null){n={}}if(e==null){return n}var i=G(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=sr(e,function(){return Function("return ("+a+")")()},{})}else{s=y(a)}for(var l in s){if(s.hasOwnProperty(l)){if(n[l]==null){n[l]=s[l]}}}}return or(u(e),t,r,n)}function sr(e,t,r){if(z.config.allowEval){return t()}else{ne(e,"htmx:evalDisallowedError");return r}}function lr(e,t){return or(e,"hx-vars",true,t)}function ur(e,t){return or(e,"hx-vals",false,t)}function fr(e){return te(lr(e),ur(e))}function cr(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 hr(t){if(t.responseURL&&typeof URL!=="undefined"){try{var e=new URL(t.responseURL);return e.pathname+e.search}catch(e){ne(J().body,"htmx:badResponseUrl",{url:t.responseURL})}}}function E(e,t){return e.getAllResponseHeaders().match(t)}function dr(e,t,r){e=e.toLowerCase();if(r){if(r instanceof Element||A(r,"String")){return ae(e,t,null,null,{targetOverride:s(r),returnPromise:true})}else{return ae(e,t,s(r.source),r.event,{handler:r.handler,headers:r.headers,values:r.values,targetOverride:s(r.target),swapOverride:r.swap,returnPromise:true})}}else{return ae(e,t,null,null,{returnPromise:true})}}function vr(e){var t=[];while(e){t.push(e);e=e.parentElement}return t}function ae(e,t,n,r,i,M){var a=null;var o=null;i=i!=null?i:{};if(i.returnPromise&&typeof Promise!=="undefined"){var s=new Promise(function(e,t){a=e;o=t})}if(n==null){n=J().body}var D=i.handler||pr;if(!ee(n)){return}var l=i.targetOverride||de(n);if(l==null||l==fe){ne(n,"htmx:targetError",{target:G(n,"hx-target")});return}if(!M){var X=function(){return ae(e,t,n,r,i,true)};var F={target:l,elt:n,path:t,verb:e,triggeringEvent:r,etc:i,issueRequest:X};if(ie(n,"htmx:confirm",F)===false){return}}var u=n;var f=Y(n);var c=Z(n,"hx-sync");var h=null;var d=false;if(c){var v=c.split(":");var g=v[0].trim();if(g==="this"){u=he(n,"hx-sync")}else{u=re(n,g)}c=(v[1]||"drop").trim();f=Y(u);if(c==="drop"&&f.xhr&&f.abortable!==true){return}else if(c==="abort"){if(f.xhr){return}else{d=true}}else if(c==="replace"){ie(u,"htmx:abort")}else if(c.indexOf("queue")===0){var B=c.split(" ");h=(B[1]||"last").trim()}}if(f.xhr){if(f.abortable){ie(u,"htmx:abort")}else{if(h==null){if(r){var p=Y(r);if(p&&p.triggerSpec&&p.triggerSpec.queue){h=p.triggerSpec.queue}}if(h==null){h="last"}}if(f.queuedRequests==null){f.queuedRequests=[]}if(h==="first"&&f.queuedRequests.length===0){f.queuedRequests.push(function(){ae(e,t,n,r,i)})}else if(h==="all"){f.queuedRequests.push(function(){ae(e,t,n,r,i)})}else if(h==="last"){f.queuedRequests=[];f.queuedRequests.push(function(){ae(e,t,n,r,i)})}return}}var m=new XMLHttpRequest;f.xhr=m;f.abortable=d;var x=function(){f.xhr=null;f.abortable=false;if(f.queuedRequests!=null&&f.queuedRequests.length>0){var e=f.queuedRequests.shift();e()}};var y=Z(n,"hx-prompt");if(y){var b=prompt(y);if(b===null||!ie(n,"htmx:prompt",{prompt:b,target:l})){K(a);x();return s}}var w=Z(n,"hx-confirm");if(w){if(!confirm(w)){K(a);x();return s}}var S=Qt(n,l,b);if(i.headers){S=te(S,i.headers)}var E=Jt(n,e);var C=E.errors;var R=E.values;if(i.values){R=te(R,i.values)}var j=fr(n);var O=te(R,j);var q=er(O,n);if(e!=="get"&&!nr(n)){S["Content-Type"]="application/x-www-form-urlencoded"}if(z.config.getCacheBusterParam&&e==="get"){q["org.htmx.cache-buster"]=$(l,"id")||"true"}if(t==null||t===""){t=J().location.href}var T=or(n,"hx-request");var H=Y(n).boosted;var L={boosted:H,parameters:q,unfilteredParameters:O,headers:S,target:l,verb:e,errors:C,withCredentials:i.credentials||T.credentials||z.config.withCredentials,timeout:i.timeout||T.timeout||z.config.timeout,path:t,triggeringEvent:r};if(!ie(n,"htmx:configRequest",L)){K(a);x();return s}t=L.path;e=L.verb;S=L.headers;q=L.parameters;C=L.errors;if(C&&C.length>0){ie(n,"htmx:validation:halted",L);K(a);x();return s}var U=t.split("#");var V=U[0];var A=U[1];var N=null;if(e==="get"){N=V;var _=Object.keys(q).length!==0;if(_){if(N.indexOf("?")<0){N+="?"}else{N+="&"}N+=Kt(q);if(A){N+="#"+A}}m.open("GET",N,true)}else{m.open(e.toUpperCase(),t,true)}m.overrideMimeType("text/html");m.withCredentials=L.withCredentials;m.timeout=L.timeout;if(T.noHeaders){}else{for(var I in S){if(S.hasOwnProperty(I)){var W=S[I];cr(m,I,W)}}}var k={xhr:m,target:l,requestConfig:L,etc:i,boosted:H,pathInfo:{requestPath:t,finalRequestPath:N||t,anchor:A}};m.onload=function(){try{var e=vr(n);k.pathInfo.responsePath=hr(m);D(n,k);_t(P);ie(n,"htmx:afterRequest",k);ie(n,"htmx:afterOnLoad",k);if(!ee(n)){var t=null;while(e.length>0&&t==null){var r=e.shift();if(ee(r)){t=r}}if(t){ie(t,"htmx:afterRequest",k);ie(t,"htmx:afterOnLoad",k)}}K(a);x()}catch(e){ne(n,"htmx:onLoadError",te({error:e},k));throw e}};m.onerror=function(){_t(P);ne(n,"htmx:afterRequest",k);ne(n,"htmx:sendError",k);K(o);x()};m.onabort=function(){_t(P);ne(n,"htmx:afterRequest",k);ne(n,"htmx:sendAbort",k);K(o);x()};m.ontimeout=function(){_t(P);ne(n,"htmx:afterRequest",k);ne(n,"htmx:timeout",k);K(o);x()};if(!ie(n,"htmx:beforeRequest",k)){K(a);x();return s}var P=Vt(n);Q(["loadstart","loadend","progress","abort"],function(t){Q([m,m.upload],function(e){e.addEventListener(t,function(e){ie(n,"htmx:xhr:"+t,{lengthComputable:e.lengthComputable,loaded:e.loaded,total:e.total})})})});ie(n,"htmx:beforeSend",k);m.send(e==="get"?null:ir(m,n,q));return s}function gr(e,t){var r=t.xhr;var n=null;var i=null;if(E(r,/HX-Push:/i)){n=r.getResponseHeader("HX-Push");i="push"}else if(E(r,/HX-Push-Url:/i)){n=r.getResponseHeader("HX-Push-Url");i="push"}else if(E(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=Z(e,"hx-push-url");var l=Z(e,"hx-replace-url");var u=Y(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 pr(s,l){var u=l.xhr;var f=l.target;var e=l.etc;if(!ie(s,"htmx:beforeOnLoad",l))return;if(E(u,/HX-Trigger:/i)){De(u,"HX-Trigger",s)}if(E(u,/HX-Location:/i)){Dt();var t=u.getResponseHeader("HX-Location");var c;if(t.indexOf("{")===0){c=y(t);t=c["path"];delete c["path"]}dr("GET",t,c).then(function(){Xt(t)});return}if(E(u,/HX-Redirect:/i)){location.href=u.getResponseHeader("HX-Redirect");return}if(E(u,/HX-Refresh:/i)){if("true"===u.getResponseHeader("HX-Refresh")){location.reload();return}}if(E(u,/HX-Retarget:/i)){l.target=J().querySelector(u.getResponseHeader("HX-Retarget"))}var h=gr(s,l);var r=u.status>=200&&u.status<400&&u.status!==204;var d=u.response;var n=u.status>=400;var i=te({shouldSwap:r,serverResponse:d,isError:n},l);if(!ie(f,"htmx:beforeSwap",i))return;f=i.target;d=i.serverResponse;n=i.isError;l.target=f;l.failed=n;l.successful=!n;if(i.shouldSwap){if(u.status===286){$e(s)}w(s,function(e){d=e.transformResponse(d,u,s)});if(h.type){Dt()}var a=e.swapOverride;if(E(u,/HX-Reswap:/i)){a=u.getResponseHeader("HX-Reswap")}var c=rr(s,a);f.classList.add(z.config.swappingClass);var v=null;var g=null;var o=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 n=S(f);Me(c.swapStyle,f,s,d,n);if(t.elt&&!ee(t.elt)&&t.elt.id){var r=document.getElementById(t.elt.id);var i={preventScroll:c.focusScroll!==undefined?!c.focusScroll:!z.config.defaultFocusScroll};if(r){if(t.start&&r.setSelectionRange){try{r.setSelectionRange(t.start,t.end)}catch(e){}}r.focus(i)}}f.classList.remove(z.config.swappingClass);Q(n.elts,function(e){if(e.classList){e.classList.add(z.config.settlingClass)}ie(e,"htmx:afterSwap",l)});if(E(u,/HX-Trigger-After-Swap:/i)){var a=s;if(!ee(s)){a=J().body}De(u,"HX-Trigger-After-Swap",a)}var o=function(){Q(n.tasks,function(e){e.call()});Q(n.elts,function(e){if(e.classList){e.classList.remove(z.config.settlingClass)}ie(e,"htmx:afterSettle",l)});if(h.type){if(h.type==="push"){Xt(h.path);ie(J().body,"htmx:pushedIntoHistory",{path:h.path})}else{Ft(h.path);ie(J().body,"htmx:replacedInHistory",{path:h.path})}}if(l.pathInfo.anchor){var e=b("#"+l.pathInfo.anchor);if(e){e.scrollIntoView({block:"start",behavior:"auto"})}}if(n.title){var t=b("title");if(t){t.innerHTML=n.title}else{window.document.title=n.title}}ar(n.elts,c);if(E(u,/HX-Trigger-After-Settle:/i)){var r=s;if(!ee(s)){r=J().body}De(u,"HX-Trigger-After-Settle",r)}K(v)};if(c.settleDelay>0){setTimeout(o,c.settleDelay)}else{o()}}catch(e){ne(s,"htmx:swapError",l);K(g);throw e}};var p=z.config.globalViewTransitions;if(c.hasOwnProperty("transition")){p=c.transition}if(p&&ie(s,"htmx:beforeTransition",l)&&typeof Promise!=="undefined"&&document.startViewTransition){var m=new Promise(function(e,t){v=e;g=t});var x=o;o=function(){document.startViewTransition(function(){x();return m})}}if(c.swapDelay>0){setTimeout(o,c.swapDelay)}else{o()}}if(n){ne(s,"htmx:responseError",te({error:"Response Status Error Code "+u.status+" from "+l.pathInfo.requestPath},l))}}var mr={};function xr(){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 yr(e,t){if(t.init){t.init(C)}mr[e]=te(xr(),t)}function br(e){delete mr[e]}function wr(e,r,n){if(e==undefined){return r}if(r==undefined){r=[]}if(n==undefined){n=[]}var t=G(e,"hx-ext");if(t){Q(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=mr[e];if(t&&r.indexOf(t)<0){r.push(t)}}})}return wr(u(e),r,n)}function Sr(e){if(J().readyState!=="loading"){e()}else{J().addEventListener("DOMContentLoaded",e)}}function Er(){if(z.config.includeIndicatorStyles!==false){J().head.insertAdjacentHTML("beforeend","<style> ."+z.config.indicatorClass+"{opacity:0;transition: opacity 200ms ease-in;} ."+z.config.requestClass+" ."+z.config.indicatorClass+"{opacity:1} ."+z.config.requestClass+"."+z.config.indicatorClass+"{opacity:1} </style>")}}function Cr(){var e=J().querySelector('meta[name="htmx-config"]');if(e){return y(e.content)}else{return null}}function Rr(){var e=Cr();if(e){z.config=te(z.config,e)}}Sr(function(){Rr();Er();var e=J().body;Tt(e);var t=J().querySelectorAll("[hx-trigger='restored'],[data-hx-trigger='restored']");e.addEventListener("htmx:abort",function(e){var t=e.target;var r=Y(t);if(r&&r.xhr){r.xhr.abort()}});var r=window.onpopstate;window.onpopstate=function(e){if(e.state&&e.state.htmx){Ut();Q(t,function(e){ie(e,"htmx:restored",{document:J(),triggerEvent:ie})})}else{if(r){r(e)}}};setTimeout(function(){ie(e,"htmx:load",{});e=null},0)});return z}()}); \ No newline at end of file +(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:t,process:Bt,on:Z,off:K,trigger:ce,ajax:Or,find:C,findAll:f,closest:v,values:function(e,t){var r=ur(e,t||"post");return r.values},remove:B,addClass:F,removeClass:n,toggleClass:V,takeClass:j,defineExtension:kr,removeExtension:Pr,logAll:X,logNone:U,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},parseInterval:d,_:e,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.9"};var r={addTriggerHandler:Tt,bodyContains:se,canAccessLocalStorage:M,findThisElement:de,filterValues:dr,hasAttribute:o,getAttributeValue:te,getClosestAttributeValue:ne,getClosestMatch:c,getExpressionVars:Cr,getHeaders:vr,getInputValues:ur,getInternalData:ae,getSwapSpecification:mr,getTriggerSpecs:Qe,getTarget:ge,makeFragment:l,mergeObjects:le,makeSettleInfo:R,oobSwap:xe,querySelectorExt:ue,selectAndSwap:Ue,settleImmediately:Yt,shouldCancel:it,triggerEvent:ce,triggerErrorEvent:fe,withExtensions:T};var b=["get","post","put","delete","patch"];var w=b.map(function(e){return"[hx-"+e+"], [data-hx-"+e+"]"}).join(", ");function d(e){if(e==undefined){return undefined}if(e.slice(-2)=="ms"){return parseFloat(e.slice(0,-2))||undefined}if(e.slice(-1)=="s"){return parseFloat(e.slice(0,-1))*1e3||undefined}if(e.slice(-1)=="m"){return parseFloat(e.slice(0,-1))*1e3*60||undefined}return parseFloat(e)||undefined}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 S(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=S(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 q(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 i(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 H(e){return e.match(/<body/)}function l(e){var t=!H(e);if(Q.config.useTemplateFragments&&t){var r=i("<body><template>"+e+"</template></body>",0);return r.querySelector("template").content}else{var n=q(e);switch(n){case"thead":case"tbody":case"tfoot":case"colgroup":case"caption":return i("<table>"+e+"</table>",1);case"col":return i("<table><colgroup>"+e+"</colgroup></table>",2);case"tr":return i("<table><tbody>"+e+"</tbody></table>",2);case"td":case"th":return i("<table><tbody><tr>"+e+"</tr></tbody></table>",3);case"script":case"style":return i("<div>"+e+"</div>",1);default:return i(e,0)}}}function ie(e){if(e){e()}}function L(e,t){return Object.prototype.toString.call(e)==="[object "+t+"]"}function A(e){return L(e,"Function")}function N(e){return L(e,"Object")}function ae(e){var t="htmx-internal-data";var r=e[t];if(!r){r=e[t]={}}return r}function I(e){var t=[];if(e){for(var r=0;r<e.length;r++){t.push(e[r])}}return t}function oe(e,t){if(e){for(var r=0;r<e.length;r++){t(e[r])}}}function k(e){var t=e.getBoundingClientRect();var r=t.top;var n=t.bottom;return r<window.innerHeight&&n>=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 P(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){x(e);return null}}function M(){var e="htmx:localStorageTest";try{localStorage.setItem(e,e);localStorage.removeItem(e);return true}catch(e){return false}}function D(t){try{var e=new URL(t);if(e){t=e.pathname+e.search}if(!t.match("^/$")){t=t.replace(/\/+$/,"")}return t}catch(e){return t}}function e(e){return wr(re().body,function(){return eval(e)})}function t(t){var e=Q.on("htmx:load",function(e){t(e.detail.elt)});return e}function X(){Q.logger=function(e,t,r){if(console){console.log(t,e,r)}}}function U(){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 B(e,t){e=s(e);if(t){setTimeout(function(){B(e);e=null},t)}else{e.parentElement.removeChild(e)}}function F(e,t,r){e=s(e);if(r){setTimeout(function(){F(e,t);e=null},r)}else{e.classList&&e.classList.add(t)}}function n(e,t,r){e=s(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 V(e,t){e=s(e);e.classList.toggle(t)}function j(e,t){e=s(e);oe(e.parentElement.children,function(e){n(e,t)});F(e,t)}function v(e,t){e=s(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 _(e,t){return e.substring(e.length-t.length)===t}function z(e){var t=e.trim();if(g(t,"<")&&_(t,"/>")){return t.substring(1,t.length-2)}else{return t}}function W(e,t){if(t.indexOf("closest ")===0){return[v(e,z(t.substr(8)))]}else if(t.indexOf("find ")===0){return[C(e,z(t.substr(5)))]}else if(t==="next"){return[e.nextElementSibling]}else if(t.indexOf("next ")===0){return[$(e,z(t.substr(5)))]}else if(t==="previous"){return[e.previousElementSibling]}else if(t.indexOf("previous ")===0){return[G(e,z(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(z(t))}}var $=function(e,t){var r=re().querySelectorAll(t);for(var n=0;n<r.length;n++){var i=r[n];if(i.compareDocumentPosition(e)===Node.DOCUMENT_POSITION_PRECEDING){return i}}};var G=function(e,t){var r=re().querySelectorAll(t);for(var n=r.length-1;n>=0;n--){var i=r[n];if(i.compareDocumentPosition(e)===Node.DOCUMENT_POSITION_FOLLOWING){return i}}};function ue(e,t){if(t){return W(e,t)[0]}else{return W(re().body,e)[0]}}function s(e){if(L(e,"String")){return C(e)}else{return e}}function J(e,t,r){if(A(t)){return{target:re().body,event:e,listener:t}}else{return{target:s(e),event:t,listener:r}}}function Z(t,r,n){Dr(function(){var e=J(t,r,n);e.target.addEventListener(e.event,e.listener)});var e=A(r);return e?r:n}function K(t,r,n){Dr(function(){var e=J(t,r,n);e.target.removeEventListener(e.event,e.listener)});return A(r)?r:n}var ve=re().createElement("output");function Y(e,t){var r=ne(e,t);if(r){if(r==="this"){return[de(e,t)]}else{var n=W(e,r);if(n.length===0){x('The selector "'+r+'" on '+t+" returned no matches!");return[ve]}else{return n}}}}function de(e,t){return c(e,function(e){return te(e,t)!=null})}function ge(e){var t=ne(e,"hx-target");if(t){if(t==="this"){return de(e,"hx-target")}else{return ue(e,t)}}else{var r=ae(e);if(r.boosted){return re().body}else{return e}}}function me(e){var t=Q.config.attributesToSettle;for(var r=0;r<t.length;r++){if(e===t[r]){return true}}return false}function pe(t,r){oe(t.attributes,function(e){if(!r.hasAttribute(e.name)&&me(e.name)){t.removeAttribute(e.name)}});oe(r.attributes,function(e){if(me(e.name)){t.setAttribute(e.name,e.value)}})}function ye(e,t){var r=Mr(t);for(var n=0;n<r.length;n++){var i=r[n];try{if(i.isInlineSwap(e)){return true}}catch(e){x(e)}}return e==="outerHTML"}function xe(e,i,a){var t="#"+ee(i,"id");var o="outerHTML";if(e==="true"){}else if(e.indexOf(":")>0){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(!ye(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"]){De(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 be(e,t,r){var n=ne(e,"hx-select-oob");if(n){var i=n.split(",");for(let e=0;e<i.length;e++){var a=i[e].split(":",2);var o=a[0].trim();if(o.indexOf("#")===0){o=o.substring(1)}var s=a[1]||"true";var l=t.querySelector("#"+o);if(l){xe(s,l,r)}}}oe(f(t,"[hx-swap-oob], [data-hx-swap-oob]"),function(e){var t=te(e,"hx-swap-oob");if(t!=null){xe(t,e,r)}})}function we(e){oe(f(e,"[hx-preserve], [data-hx-preserve]"),function(e){var t=te(e,"id");var r=re().getElementById(t);if(r!=null){e.parentNode.replaceChild(r,e)}})}function Se(o,e,s){oe(e.querySelectorAll("[id]"),function(e){var t=ee(e,"id");if(t&&t.length>0){var r=t.replace("'","\\'");var n=e.tagName.replace(":","\\:");var i=o.querySelector(n+"[id='"+r+"']");if(i&&i!==o){var a=e.cloneNode();pe(e,i);s.tasks.push(function(){pe(e,a)})}}})}function Ee(e){return function(){n(e,Q.config.addedClass);Bt(e);Ot(e);Ce(e);ce(e,"htmx:load")}}function Ce(e){var t="[autofocus]";var r=h(e,t)?e:e.querySelector(t);if(r!=null){r.focus()}}function a(e,t,r,n){Se(e,r,n);while(r.childNodes.length>0){var i=r.firstChild;F(i,Q.config.addedClass);e.insertBefore(i,t);if(i.nodeType!==Node.TEXT_NODE&&i.nodeType!==Node.COMMENT_NODE){n.tasks.push(Ee(i))}}}function Te(e,t){var r=0;while(r<e.length){t=(t<<5)-t+e.charCodeAt(r++)|0}return t}function Re(e){var t=0;if(e.attributes){for(var r=0;r<e.attributes.length;r++){var n=e.attributes[r];if(n.value){t=Te(n.name,t);t=Te(n.value,t)}}}return t}function Oe(t){var r=ae(t);if(r.onHandlers){for(let e=0;e<r.onHandlers.length;e++){const n=r.onHandlers[e];t.removeEventListener(n.event,n.listener)}delete r.onHandlers}}function qe(e){var t=ae(e);if(t.timeout){clearTimeout(t.timeout)}if(t.webSocket){t.webSocket.close()}if(t.sseEventSource){t.sseEventSource.close()}if(t.listenerInfos){oe(t.listenerInfos,function(e){if(e.on){e.on.removeEventListener(e.trigger,e.listener)}})}if(t.initHash){t.initHash=null}Oe(e)}function m(e){ce(e,"htmx:beforeCleanupElement");qe(e);if(e.children){oe(e.children,function(e){m(e)})}}function He(t,e,r){if(t.tagName==="BODY"){return Pe(t,e,r)}else{var n;var i=t.previousSibling;a(u(t),t,e,r);if(i==null){n=u(t).firstChild}else{n=i.nextSibling}ae(t).replacedWith=n;r.elts=r.elts.filter(function(e){return e!=t});while(n&&n!==t){if(n.nodeType===Node.ELEMENT_NODE){r.elts.push(n)}n=n.nextElementSibling}m(t);u(t).removeChild(t)}}function Le(e,t,r){return a(e,e.firstChild,t,r)}function Ae(e,t,r){return a(u(e),e,t,r)}function Ne(e,t,r){return a(e,null,t,r)}function Ie(e,t,r){return a(u(e),e.nextSibling,t,r)}function ke(e,t,r){m(e);return u(e).removeChild(e)}function Pe(e,t,r){var n=e.firstChild;a(e,n,t,r);if(n){while(n.nextSibling){m(n.nextSibling);e.removeChild(n.nextSibling)}m(n);e.removeChild(n)}}function Me(e,t,r){var n=r||ne(e,"hx-select");if(n){var i=re().createDocumentFragment();oe(t.querySelectorAll(n),function(e){i.appendChild(e)});t=i}return t}function De(e,t,r,n,i){switch(e){case"none":return;case"outerHTML":He(r,n,i);return;case"afterbegin":Le(r,n,i);return;case"beforebegin":Ae(r,n,i);return;case"beforeend":Ne(r,n,i);return;case"afterend":Ie(r,n,i);return;case"delete":ke(r,n,i);return;default:var a=Mr(t);for(var o=0;o<a.length;o++){var s=a[o];try{var l=s.handleSwap(e,r,n,i);if(l){if(typeof l.length!=="undefined"){for(var u=0;u<l.length;u++){var f=l[u];if(f.nodeType!==Node.TEXT_NODE&&f.nodeType!==Node.COMMENT_NODE){i.tasks.push(Ee(f))}}}return}}catch(e){x(e)}}if(e==="innerHTML"){Pe(r,n,i)}else{De(Q.config.defaultSwapStyle,t,r,n,i)}}}function Xe(e){if(e.indexOf("<title")>-1){var t=e.replace(/<svg(\s[^>]*>|>)([\s\S]*?)<\/svg>/gim,"");var r=t.match(/<title(\s[^>]*>|>)([\s\S]*?)<\/title>/im);if(r){return r[2]}}}function Ue(e,t,r,n,i,a){i.title=Xe(n);var o=l(n);if(o){be(r,o,i);o=Me(r,o,a);we(o);return De(e,r,t,o,i)}}function Be(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(!N(o)){o={value:o}}ce(r,a,o)}}}else{var s=n.split(",");for(var l=0;l<s.length;l++){ce(r,s[l].trim(),[])}}}var Fe=/\s/;var p=/[\s,]/;var Ve=/[_$a-zA-Z]/;var je=/[_$a-zA-Z0-9]/;var _e=['"',"'","/"];var ze=/[^\s]/;var We=/[{(]/;var $e=/[})]/;function Ge(e){var t=[];var r=0;while(r<e.length){if(Ve.exec(e.charAt(r))){var n=r;while(je.exec(e.charAt(r+1))){r++}t.push(e.substr(n,r-n+1))}else if(_e.indexOf(e.charAt(r))!==-1){var i=e.charAt(r);var n=r;r++;while(r<e.length&&e.charAt(r)!==i){if(e.charAt(r)==="\\"){r++}r++}t.push(e.substr(n,r-n+1))}else{var a=e.charAt(r);t.push(a)}r++}return t}function Je(e,t,r){return Ve.exec(e.charAt(0))&&e!=="true"&&e!=="false"&&e!=="this"&&e!==r&&t!=="."}function Ze(e,t,r){if(t[0]==="["){t.shift();var n=1;var i=" return (function("+r+"){ return (";var a=null;while(t.length>0){var o=t[0];if(o==="]"){n--;if(n===0){if(a===null){i=i+"true"}t.shift();i+=")})";try{var s=wr(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(Je(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&&!e[0].match(t)){r+=e.shift()}return r}function Ke(e){var t;if(e.length>0&&We.test(e[0])){e.shift();t=y(e,$e).trim();e.shift()}else{t=y(e,p)}return t}var Ye="input, textarea, select";function Qe(e){var t=te(e,"hx-trigger");var r=[];if(t){var n=Ge(t);do{y(n,ze);var i=n.length;var a=y(n,/[,\[\s]/);if(a!==""){if(a==="every"){var o={trigger:"every"};y(n,ze);o.pollInterval=d(y(n,/[,\[\s]/));y(n,ze);var s=Ze(e,n,"event");if(s){o.eventFilter=s}r.push(o)}else if(a.indexOf("sse:")===0){r.push({trigger:"sse",sseEvent:a.substr(4)})}else{var l={trigger:a};var s=Ze(e,n,"event");if(s){l.eventFilter=s}while(n.length>0&&n[0]!==","){y(n,ze);var u=n.shift();if(u==="changed"){l.changed=true}else if(u==="once"){l.once=true}else if(u==="consume"){l.consume=true}else if(u==="delay"&&n[0]===":"){n.shift();l.delay=d(y(n,p))}else if(u==="from"&&n[0]===":"){n.shift();if(We.test(n[0])){var f=Ke(n)}else{var f=y(n,p);if(f==="closest"||f==="find"||f==="next"||f==="previous"){n.shift();var c=Ke(n);if(c.length>0){f+=" "+c}}}l.from=f}else if(u==="target"&&n[0]===":"){n.shift();l.target=Ke(n)}else if(u==="throttle"&&n[0]===":"){n.shift();l.throttle=d(y(n,p))}else if(u==="queue"&&n[0]===":"){n.shift();l.queue=y(n,p)}else if(u==="root"&&n[0]===":"){n.shift();l[u]=Ke(n)}else if(u==="threshold"&&n[0]===":"){n.shift();l[u]=y(n,p)}else{fe(e,"htmx:syntax:error",{token:n.shift()})}}r.push(l)}}if(n.length===i){fe(e,"htmx:syntax:error",{token:n.shift()})}y(n,ze)}while(n[0]===","&&n.shift())}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,Ye)){return[{trigger:"change"}]}else{return[{trigger:"click"}]}}function et(e){ae(e).cancelled=true}function tt(e,t,r){var n=ae(e);n.timeout=setTimeout(function(){if(se(e)&&n.cancelled!==true){if(!ot(r,e,Vt("hx:poll:trigger",{triggerSpec:r,target:e}))){t(e)}tt(e,t,r)}},r.pollInterval)}function rt(e){return location.hostname===e.hostname&&ee(e,"href")&&ee(e,"href").indexOf("#")!==0}function nt(t,r,e){if(t.tagName==="A"&&rt(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){st(t,function(e,t){if(v(e,Q.config.disableSelector)){m(e);return}he(n,i,e,t)},r,e,true)})}}function it(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 at(e,t){return ae(e).boosted&&e.tagName==="A"&&t.type==="click"&&(t.ctrlKey||t.metaKey)}function ot(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 st(a,o,e,s,l){var u=ae(a);var t;if(s.from){t=W(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(at(a,e)){return}if(l||it(e,a)){e.preventDefault()}if(ot(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){if(!u.throttle){o(a,e);u.throttle=setTimeout(function(){u.throttle=null},s.throttle)}}else if(s.delay){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 lt=false;var ut=null;function ft(){if(!ut){ut=function(){lt=true};window.addEventListener("scroll",ut);setInterval(function(){if(lt){lt=false;oe(re().querySelectorAll("[hx-trigger='revealed'],[data-hx-trigger='revealed']"),function(e){ct(e)})}},200)}}function ct(t){if(!o(t,"data-hx-revealed")&&k(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 ht(e,t,r){var n=P(r);for(var i=0;i<n.length;i++){var a=n[i].split(/:(.+)/);if(a[0]==="connect"){vt(e,a[1],0)}if(a[0]==="send"){gt(e)}}}function vt(s,r,n){if(!se(s)){return}if(r.indexOf("/")==0){var e=location.hostname+(location.port?":"+location.port:"");if(location.protocol=="https:"){r="wss://"+e+r}else if(location.protocol=="http:"){r="ws://"+e+r}}var t=Q.createWebSocket(r);t.onerror=function(e){fe(s,"htmx:wsError",{error:e,socket:t});dt(s)};t.onclose=function(e){if([1006,1012,1013].indexOf(e.code)>=0){var t=mt(n);setTimeout(function(){vt(s,r,n+1)},t)}};t.onopen=function(e){n=0};ae(s).webSocket=t;t.addEventListener("message",function(e){if(dt(s)){return}var t=e.data;T(s,function(e){t=e.transformResponse(t,null,s)});var r=R(s);var n=l(t);var i=I(n.children);for(var a=0;a<i.length;a++){var o=i[a];xe(te(o,"hx-swap-oob")||"true",o,r)}Yt(r.tasks)})}function dt(e){if(!se(e)){ae(e).webSocket.close();return true}}function gt(u){var f=c(u,function(e){return ae(e).webSocket!=null});if(f){u.addEventListener(Qe(u)[0].trigger,function(e){var t=ae(f).webSocket;var r=vr(u,f);var n=ur(u,"post");var i=n.errors;var a=n.values;var o=Cr(u);var s=le(a,o);var l=dr(s,u);l["HEADERS"]=r;if(i&&i.length>0){ce(u,"htmx:validation:halted",i);return}t.send(JSON.stringify(l));if(it(e,u)){e.preventDefault()}})}else{fe(u,"htmx:noWebSocketSourceError")}}function mt(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()}x('htmx.config.wsReconnectDelay must either be a function or the string "full-jitter"')}function pt(e,t,r){var n=P(r);for(var i=0;i<n.length;i++){var a=n[i].split(/:(.+)/);if(a[0]==="connect"){yt(e,a[1])}if(a[0]==="swap"){xt(e,a[1])}}}function yt(t,e){var r=Q.createEventSource(e);r.onerror=function(e){fe(t,"htmx:sseError",{error:e,source:r});wt(t)};ae(t).sseEventSource=r}function xt(a,o){var s=c(a,St);if(s){var l=ae(s).sseEventSource;var u=function(e){if(wt(s)){return}if(!se(a)){l.removeEventListener(o,u);return}var t=e.data;T(a,function(e){t=e.transformResponse(t,null,a)});var r=mr(a);var n=ge(a);var i=R(a);Ue(r.swapStyle,n,a,t,i);Yt(i.tasks);ce(a,"htmx:sseMessage",e)};ae(a).sseListener=u;l.addEventListener(o,u)}else{fe(a,"htmx:noSSESourceError")}}function bt(e,t,r){var n=c(e,St);if(n){var i=ae(n).sseEventSource;var a=function(){if(!wt(n)){if(se(e)){t(e)}else{i.removeEventListener(r,a)}}};ae(e).sseListener=a;i.addEventListener(r,a)}else{fe(e,"htmx:noSSESourceError")}}function wt(e){if(!se(e)){ae(e).sseEventSource.close();return true}}function St(e){return ae(e).sseEventSource!=null}function Et(e,t,r,n){var i=function(){if(!r.loaded){r.loaded=true;t(e)}};if(n){setTimeout(i,n)}else{i()}}function Ct(t,i,e){var a=false;oe(b,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){Tt(t,e,i,function(e,t){if(v(e,Q.config.disableSelector)){m(e);return}he(r,n,e,t)})})}});return a}function Tt(n,e,t,r){if(e.sseEvent){bt(n,r,e.sseEvent)}else if(e.trigger==="revealed"){ft();st(n,r,t,e);ct(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;t<e.length;t++){var r=e[t];if(r.isIntersecting){ce(n,"intersect");break}}},i);a.observe(n);st(n,r,t,e)}else if(e.trigger==="load"){if(!ot(e,n,Vt("load",{elt:n}))){Et(n,r,t,e.delay)}}else if(e.pollInterval){t.polling=true;tt(n,r,e)}else{st(n,r,t,e)}}function Rt(e){if(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){x(e)}finally{if(e.parentElement){e.parentElement.removeChild(e)}}}}function Ot(e){if(h(e,"script")){Rt(e)}oe(f(e,"script"),function(e){Rt(e)})}function qt(){return document.querySelector("[hx-boost], [data-hx-boost]")}function Ht(e){var t=null;var r=[];if(document.evaluate){var n=document.evaluate('//*[@*[ starts-with(name(), "hx-on:") or starts-with(name(), "data-hx-on:") ]]',e);while(t=n.iterateNext())r.push(t)}else{var i=document.getElementsByTagName("*");for(var a=0;a<i.length;a++){var o=i[a].attributes;for(var s=0;s<o.length;s++){var l=o[s].name;if(g(l,"hx-on:")||g(l,"data-hx-on:")){r.push(i[a])}}}}return r}function Lt(e){if(e.querySelectorAll){var t=qt()?", a":"";var r=e.querySelectorAll(w+t+", form, [type='submit'], [hx-sse], [data-hx-sse], [hx-ws],"+" [data-hx-ws], [hx-ext], [data-hx-ext], [hx-trigger], [data-hx-trigger], [hx-on], [data-hx-on]");return r}else{return[]}}function At(e){var t=v(e.target,"button, input[type='submit']");var r=It(e);if(r){r.lastButtonClicked=t}}function Nt(e){var t=It(e);if(t){t.lastButtonClicked=null}}function It(e){var t=v(e.target,"button, input[type='submit']");if(!t){return}var r=s("#"+ee(t,"form"))||v(t,"form");if(!r){return}return ae(r)}function kt(e){e.addEventListener("click",At);e.addEventListener("focusin",At);e.addEventListener("focusout",Nt)}function Pt(e){var t=Ge(e);var r=0;for(let e=0;e<t.length;e++){const n=t[e];if(n==="{"){r++}else if(n==="}"){r--}}return r}function Mt(t,e,r){var n=ae(t);if(!Array.isArray(n.onHandlers)){n.onHandlers=[]}var i;var a=function(e){return wr(t,function(){if(!i){i=new Function("event",r)}i.call(t,e)})};t.addEventListener(e,a);n.onHandlers.push({event:e,listener:a})}function Dt(e){var t=te(e,"hx-on");if(t){var r={};var n=t.split("\n");var i=null;var a=0;while(n.length>0){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+=Pt(o)}for(var l in r){Mt(e,l,r[l])}}}function Xt(t){Oe(t);for(var e=0;e<t.attributes.length;e++){var r=t.attributes[e].name;var n=t.attributes[e].value;if(g(r,"hx-on:")||g(r,"data-hx-on:")){let e=r.slice(r.indexOf(":")+1);if(g(e,":"))e="htmx"+e;Mt(t,e,n)}}}function Ut(t){if(v(t,Q.config.disableSelector)){m(t);return}var r=ae(t);if(r.initHash!==Re(t)){qe(t);r.initHash=Re(t);Dt(t);ce(t,"htmx:beforeProcessNode");if(t.value){r.lastValue=t.value}var e=Qe(t);var n=Ct(t,r,e);if(!n){if(ne(t,"hx-boost")==="true"){nt(t,r,e)}else if(o(t,"hx-trigger")){e.forEach(function(e){Tt(t,e,r,function(){})})}}if(t.tagName==="FORM"||ee(t,"type")==="submit"&&o(t,"form")){kt(t)}var i=te(t,"hx-sse");if(i){pt(t,r,i)}var a=te(t,"hx-ws");if(a){ht(t,r,a)}ce(t,"htmx:afterProcessNode")}}function Bt(e){e=s(e);if(v(e,Q.config.disableSelector)){m(e);return}Ut(e);oe(Lt(e),function(e){Ut(e)});oe(Ht(e),Xt)}function Ft(e){return e.replace(/([a-z0-9])([A-Z])/g,"$1-$2").toLowerCase()}function Vt(e,t){var r;if(window.CustomEvent&&typeof window.CustomEvent==="function"){r=new CustomEvent(e,{bubbles:true,cancelable:true,detail:t})}else{r=re().createEvent("CustomEvent");r.initCustomEvent(e,true,true,t)}return r}function fe(e,t,r){ce(e,t,le({error:t},r))}function jt(e){return e==="htmx:afterProcessNode"}function T(e,t){oe(Mr(e),function(e){try{t(e)}catch(e){x(e)}})}function x(e){if(console.error){console.error(e)}else if(console.log){console.log("ERROR: ",e)}}function ce(e,t,r){e=s(e);if(r==null){r={}}r["elt"]=e;var n=Vt(t,r);if(Q.logger&&!jt(t)){Q.logger(e,t,r)}if(r.error){x(r.error);ce(e,"htmx:error",{errorInfo:r})}var i=e.dispatchEvent(n);var a=Ft(t);if(i&&a!==t){var o=Vt(a,n.detail);i=i&&e.dispatchEvent(o)}T(e,function(e){i=i&&(e.onEvent(t,n)!==false&&!n.defaultPrevented)});return i}var _t=location.pathname+location.search;function zt(){var e=re().querySelector("[hx-history-elt],[data-hx-history-elt]");return e||re().body}function Wt(e,t,r,n){if(!M()){return}if(Q.config.historyCacheSize<=0){localStorage.removeItem("htmx-history-cache");return}e=D(e);var i=E(localStorage.getItem("htmx-history-cache"))||[];for(var a=0;a<i.length;a++){if(i[a].url===e){i.splice(a,1);break}}var o={url:e,content:t,title:r,scroll:n};ce(re().body,"htmx:historyItemCreated",{item:o,cache:i});i.push(o);while(i.length>Q.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 $t(e){if(!M()){return null}e=D(e);var t=E(localStorage.getItem("htmx-history-cache"))||[];for(var r=0;r<t.length;r++){if(t[r].url===e){return t[r]}}return null}function Gt(e){var t=Q.config.requestClass;var r=e.cloneNode(true);oe(f(r,"."+t),function(e){n(e,t)});return r.innerHTML}function Jt(){var e=zt();var t=_t||location.pathname+location.search;var r;try{r=re().querySelector('[hx-history="false" i],[data-hx-history="false" i]')}catch(e){r=re().querySelector('[hx-history="false"],[data-hx-history="false"]')}if(!r){ce(re().body,"htmx:beforeHistorySave",{path:t,historyElt:e});Wt(t,Gt(e),re().title,window.scrollY)}if(Q.config.historyEnabled)history.replaceState({htmx:true},re().title,window.location.href)}function Zt(e){if(Q.config.getCacheBusterParam){e=e.replace(/org\.htmx\.cache-buster=[^&]*&?/,"");if(_(e,"&")||_(e,"?")){e=e.slice(0,-1)}}if(Q.config.historyEnabled){history.pushState({htmx:true},"",e)}_t=e}function Kt(e){if(Q.config.historyEnabled)history.replaceState({htmx:true},"",e);_t=e}function Yt(e){oe(e,function(e){e.call()})}function Qt(a){var e=new XMLHttpRequest;var o={path:a,xhr:e};ce(re().body,"htmx:historyCacheMiss",o);e.open("GET",a,true);e.setRequestHeader("HX-History-Restore-Request","true");e.onload=function(){if(this.status>=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=R(t);var n=Xe(this.response);if(n){var i=C("title");if(i){i.innerHTML=n}else{window.document.title=n}}Pe(t,e,r);Yt(r.tasks);_t=a;ce(re().body,"htmx:historyRestore",{path:a,cacheMiss:true,serverResponse:this.response})}else{fe(re().body,"htmx:historyCacheMissLoadError",o)}};e.send()}function er(e){Jt();e=e||location.pathname+location.search;var t=$t(e);if(t){var r=l(t.content);var n=zt();var i=R(n);Pe(n,r,i);Yt(i.tasks);document.title=t.title;setTimeout(function(){window.scrollTo(0,t.scroll)},0);_t=e;ce(re().body,"htmx:historyRestore",{path:e,item:t})}else{if(Q.config.refreshOnHistoryMiss){window.location.reload(true)}else{Qt(e)}}}function tr(e){var t=Y(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 rr(e){var t=Y(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 nr(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 ir(e,t){for(var r=0;r<e.length;r++){var n=e[r];if(n.isSameNode(t)){return true}}return false}function ar(e){if(e.name===""||e.name==null||e.disabled){return false}if(e.type==="button"||e.type==="submit"||e.tagName==="image"||e.tagName==="reset"||e.tagName==="file"){return false}if(e.type==="checkbox"||e.type==="radio"){return e.checked}return true}function or(e,t,r){if(e!=null&&t!=null){var n=r[e];if(n===undefined){r[e]=t}else if(Array.isArray(n)){if(Array.isArray(t)){r[e]=n.concat(t)}else{n.push(t)}}else{if(Array.isArray(t)){r[e]=[n].concat(t)}else{r[e]=[n,t]}}}}function sr(t,r,n,e,i){if(e==null||ir(t,e)){return}else{t.push(e)}if(ar(e)){var a=ee(e,"name");var o=e.value;if(e.multiple&&e.tagName==="SELECT"){o=I(e.querySelectorAll("option:checked")).map(function(e){return e.value})}if(e.files){o=I(e.files)}or(a,o,r);if(i){lr(e,n)}}if(h(e,"form")){var s=e.elements;oe(s,function(e){sr(t,r,n,e,i)})}}function lr(e,t){if(e.willValidate){ce(e,"htmx:validation:validate");if(!e.checkValidity()){t.push({elt:e,message:e.validationMessage,validity:e.validity});ce(e,"htmx:validation:failed",{message:e.validationMessage,validity:e.validity})}}}function ur(e,t){var r=[];var n={};var i={};var a=[];var o=ae(e);if(o.lastButtonClicked&&!se(o.lastButtonClicked)){o.lastButtonClicked=null}var s=h(e,"form")&&e.noValidate!==true||te(e,"hx-validate")==="true";if(o.lastButtonClicked){s=s&&o.lastButtonClicked.formNoValidate!==true}if(t!=="get"){sr(r,i,a,v(e,"form"),s)}sr(r,n,a,e,s);if(o.lastButtonClicked||e.tagName==="BUTTON"||e.tagName==="INPUT"&&ee(e,"type")==="submit"){var l=o.lastButtonClicked||e;var u=ee(l,"name");or(u,l.value,i)}var f=Y(e,"hx-include");oe(f,function(e){sr(r,n,a,e,s);if(!h(e,"form")){oe(e.querySelectorAll(Ye),function(e){sr(r,n,a,e,s)})}});n=le(n,i);return{errors:a,values:n}}function fr(e,t,r){if(e!==""){e+="&"}if(String(r)==="[object Object]"){r=JSON.stringify(r)}var n=encodeURIComponent(r);e+=encodeURIComponent(t)+"="+n;return e}function cr(e){var t="";for(var r in e){if(e.hasOwnProperty(r)){var n=e[r];if(Array.isArray(n)){oe(n,function(e){t=fr(t,r,e)})}else{t=fr(t,r,n)}}}return t}function hr(e){var t=new FormData;for(var r in e){if(e.hasOwnProperty(r)){var n=e[r];if(Array.isArray(n)){oe(n,function(e){t.append(r,e)})}else{t.append(r,n)}}}return t}function vr(e,t,r){var n={"HX-Request":"true","HX-Trigger":ee(e,"id"),"HX-Trigger-Name":ee(e,"name"),"HX-Target":te(t,"id"),"HX-Current-URL":re().location.href};br(e,"hx-headers",false,n);if(r!==undefined){n["HX-Prompt"]=r}if(ae(e).boosted){n["HX-Boosted"]="true"}return n}function dr(t,e){var r=ne(e,"hx-params");if(r){if(r==="none"){return{}}else if(r==="*"){return t}else if(r.indexOf("not ")===0){oe(r.substr(4).split(","),function(e){e=e.trim();delete t[e]});return t}else{var n={};oe(r.split(","),function(e){e=e.trim();n[e]=t[e]});return n}}else{return t}}function gr(e){return ee(e,"href")&&ee(e,"href").indexOf("#")>=0}function mr(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&&!gr(e)){n["show"]="top"}if(r){var i=P(r);if(i.length>0){for(var a=0;a<i.length;a++){var o=i[a];if(o.indexOf("swap:")===0){n["swapDelay"]=d(o.substr(5))}else if(o.indexOf("settle:")===0){n["settleDelay"]=d(o.substr(7))}else if(o.indexOf("transition:")===0){n["transition"]=o.substr(11)==="true"}else if(o.indexOf("ignoreTitle:")===0){n["ignoreTitle"]=o.substr(12)==="true"}else if(o.indexOf("scroll:")===0){var s=o.substr(7);var l=s.split(":");var u=l.pop();var f=l.length>0?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{x("Unknown modifier in hx-swap: "+o)}}}}return n}function pr(e){return ne(e,"hx-encoding")==="multipart/form-data"||h(e,"form")&&ee(e,"enctype")==="multipart/form-data"}function yr(t,r,n){var i=null;T(r,function(e){if(i==null){i=e.encodeParameters(t,n,r)}});if(i!=null){return i}else{if(pr(r)){return hr(n)}else{return cr(n)}}}function R(e){return{tasks:[],elts:[e]}}function xr(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 br(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=wr(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 br(u(e),t,r,n)}function wr(e,t,r){if(Q.config.allowEval){return t()}else{fe(e,"htmx:evalDisallowedError");return r}}function Sr(e,t){return br(e,"hx-vars",true,t)}function Er(e,t){return br(e,"hx-vals",false,t)}function Cr(e){return le(Sr(e),Er(e))}function Tr(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 Rr(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 e.getAllResponseHeaders().match(t)}function Or(e,t,r){e=e.toLowerCase();if(r){if(r instanceof Element||L(r,"String")){return he(e,t,null,null,{targetOverride:s(r),returnPromise:true})}else{return he(e,t,s(r.source),r.event,{handler:r.handler,headers:r.headers,values:r.values,targetOverride:s(r.target),swapOverride:r.swap,select:r.select,returnPromise:true})}}else{return he(e,t,null,null,{returnPromise:true})}}function qr(e){var t=[];while(e){t.push(e);e=e.parentElement}return t}function Hr(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||Ar;var D=a.select||null;if(!se(n)){ie(o);return l}var u=a.targetOverride||ge(n);if(u==null||u==ve){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 X=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:X,question:d};if(ce(n,"htmx:confirm",U)===false){ie(o);return l}}var g=n;var m=ne(n,"hx-sync");var p=null;var y=false;if(m){var B=m.split(":");var F=B[0].trim();if(F==="this"){g=de(n,"hx-sync")}else{g=ue(n,F)}m=(B[1]||"drop").trim();f=ae(g);if(m==="drop"&&f.xhr&&f.abortable!==true){ie(o);return l}else if(m==="abort"){if(f.xhr){ie(o);return l}else{y=true}}else if(m==="replace"){ce(g,"htmx:abort")}else if(m.indexOf("queue")===0){var V=m.split(" ");p=(V[1]||"last").trim()}}if(f.xhr){if(f.abortable){ce(g,"htmx:abort")}else{if(p==null){if(i){var x=ae(i);if(x&&x.triggerSpec&&x.triggerSpec.queue){p=x.triggerSpec.queue}}if(p==null){p="last"}}if(f.queuedRequests==null){f.queuedRequests=[]}if(p==="first"&&f.queuedRequests.length===0){f.queuedRequests.push(function(){he(t,r,n,i,a)})}else if(p==="all"){f.queuedRequests.push(function(){he(t,r,n,i,a)})}else if(p==="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=y;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=vr(n,u,S);if(t!=="get"&&!pr(n)){E["Content-Type"]="application/x-www-form-urlencoded"}if(a.headers){E=le(E,a.headers)}var _=ur(n,t);var C=_.errors;var T=_.values;if(a.values){T=le(T,a.values)}var z=Cr(n);var W=le(T,z);var R=dr(W,n);if(Q.config.getCacheBusterParam&&t==="get"){R["org.htmx.cache-buster"]=ee(u,"id")||"true"}if(r==null||r===""){r=re().location.href}var O=br(n,"hx-request");var $=ae(n).boosted;var q=Q.config.methodsThatUseUrlParams.indexOf(t)>=0;var H={boosted:$,useUrlParams:q,parameters:R,unfilteredParameters:W,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;R=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(R).length!==0;if(Z){if(A.indexOf("?")<0){A+="?"}else{A+="&"}A+=cr(R);if(L){A+="#"+L}}}if(!Hr(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];Tr(b,N,K)}}}var I={xhr:b,target:u,requestConfig:H,etc:a,boosted:$,select:D,pathInfo:{requestPath:r,finalRequestPath:A,anchor:L}};b.onload=function(){try{var e=qr(n);I.pathInfo.responsePath=Rr(b);M(n,I);nr(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(){nr(k,P);fe(n,"htmx:afterRequest",I);fe(n,"htmx:sendError",I);ie(s);w()};b.onabort=function(){nr(k,P);fe(n,"htmx:afterRequest",I);fe(n,"htmx:sendAbort",I);ie(s);w()};b.ontimeout=function(){nr(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=tr(n);var P=rr(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:yr(b,n,R);b.send(Y);return l}function Lr(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 Ar(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)){Be(f,"HX-Trigger",l)}if(O(f,/HX-Location:/i)){Jt();var r=f.getResponseHeader("HX-Location");var v;if(r.indexOf("{")===0){v=E(r);r=v["path"];delete v["path"]}Or("GET",r,v).then(function(){Zt(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)){u.target=re().querySelector(f.getResponseHeader("HX-Retarget"))}var d=Lr(l,u);var i=f.status>=200&&f.status<400&&f.status!==204;var g=f.response;var a=f.status>=400;var m=Q.config.ignoreTitle;var o=le({shouldSwap:i,serverResponse:g,isError:a,ignoreTitle:m},u);if(!ce(c,"htmx:beforeSwap",o))return;c=o.target;g=o.serverResponse;a=o.isError;m=o.ignoreTitle;u.target=c;u.failed=a;u.successful=!a;if(o.shouldSwap){if(f.status===286){et(l)}T(l,function(e){g=e.transformResponse(g,f,l)});if(d.type){Jt()}var s=e.swapOverride;if(O(f,/HX-Reswap:/i)){s=f.getResponseHeader("HX-Reswap")}var v=mr(l,s);if(v.hasOwnProperty("ignoreTitle")){m=v.ignoreTitle}c.classList.add(Q.config.swappingClass);var p=null;var y=null;var x=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"){Zt(d.path);ce(re().body,"htmx:pushedIntoHistory",{path:d.path})}else{Kt(d.path);ce(re().body,"htmx:replacedInHistory",{path:d.path})}}var n=R(c);Ue(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}Be(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&&!m){var t=C("title");if(t){t.innerHTML=n.title}else{window.document.title=n.title}}xr(n.elts,v);if(O(f,/HX-Trigger-After-Settle:/i)){var r=l;if(!se(l)){r=re().body}Be(f,"HX-Trigger-After-Settle",r)}ie(p)};if(v.settleDelay>0){setTimeout(s,v.settleDelay)}else{s()}}catch(e){fe(l,"htmx:swapError",u);ie(y);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){p=e;y=t});var S=x;x=function(){document.startViewTransition(function(){S();return w})}}if(v.swapDelay>0){setTimeout(x,v.swapDelay)}else{x()}}if(a){fe(l,"htmx:responseError",le({error:"Response Status Error Code "+f.status+" from "+u.pathInfo.requestPath},u))}}var Nr={};function Ir(){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 kr(e,t){if(t.init){t.init(r)}Nr[e]=le(Ir(),t)}function Pr(e){delete Nr[e]}function Mr(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=Nr[e];if(t&&r.indexOf(t)<0){r.push(t)}}})}return Mr(u(e),r,n)}function Dr(e){var t=function(){if(!e)return;e();e=null};if(re().readyState==="complete"){t()}else{re().addEventListener("DOMContentLoaded",function(){t()});re().addEventListener("readystatechange",function(){if(re().readyState!=="complete")return;t()})}}function Xr(){if(Q.config.includeIndicatorStyles!==false){re().head.insertAdjacentHTML("beforeend","<style> ."+Q.config.indicatorClass+"{opacity:0} ."+Q.config.requestClass+" ."+Q.config.indicatorClass+"{opacity:1; transition: opacity 200ms ease-in;} ."+Q.config.requestClass+"."+Q.config.indicatorClass+"{opacity:1; transition: opacity 200ms ease-in;} </style>")}}function Ur(){var e=re().querySelector('meta[name="htmx-config"]');if(e){return E(e.content)}else{return null}}function Br(){var e=Ur();if(e){Q.config=le(Q.config,e)}}Dr(function(){Br();Xr();var e=re().body;Bt(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()}});var r=window.onpopstate;window.onpopstate=function(e){if(e.state&&e.state.htmx){er();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/hypha/static_src/src/javascript/main-top.js b/hypha/static_src/src/javascript/main-top.js deleted file mode 100644 index bf2afa502e5ea404c9243c2a220afe8d087181ac..0000000000000000000000000000000000000000 --- a/hypha/static_src/src/javascript/main-top.js +++ /dev/null @@ -1,5 +0,0 @@ -(function () { - "use strict"; - - document.querySelector("html").classList.replace("no-js", "js"); -})(); diff --git a/hypha/static_src/src/javascript/main.js b/hypha/static_src/src/javascript/main.js index c37dbfc02414ce634bc9c71c260653c32ca23606..d76cfac8f66ce73d1415f5d5a9710414949c68ec 100644 --- a/hypha/static_src/src/javascript/main.js +++ b/hypha/static_src/src/javascript/main.js @@ -1,38 +1,6 @@ (function ($) { "use strict"; - let Search = class { - static selector() { - return ".js-search-toggle"; - } - - constructor(node, searchForm) { - this.node = node; - this.searchForm = searchForm; - this.bindEventListeners(); - } - - bindEventListeners() { - this.node.click(this.toggle.bind(this)); - } - - toggle() { - // show the search - this.searchForm[0].classList.toggle("is-visible"); - - // swap the icons - this.node[0] - .querySelector(".header__icon--open-search") - .classList.toggle("is-hidden"); - this.node[0] - .querySelector(".header__icon--close-search") - .classList.toggle("is-unhidden"); - - // add modifier to header to be able to change header icon colours - document.querySelector(".header").classList.toggle("search-open"); - } - }; - let MobileMenu = class { static selector() { return ".js-mobile-menu-toggle"; @@ -42,7 +10,6 @@ this.node = node; this.closeButton = closeButton; this.mobileMenu = mobileMenu; - this.search = search; this.bindEventListeners(); } @@ -55,67 +22,6 @@ toggle() { // toggle mobile menu this.mobileMenu[0].classList.toggle("is-visible"); - - // check if search exists - if (document.body.contains(this.search[0])) { - // reset the search whenever the mobile menu is toggled - if (this.search[0].classList.contains("is-visible")) { - this.search[0].classList.toggle("is-visible"); - document - .querySelector(".header__inner--menu-open") - .classList.toggle("header__inner--search-open"); - } - } - - // reset the search show/hide icons - if ( - this.mobileMenu[0].classList.contains("is-visible") && - document.body.contains(this.search[0]) - ) { - document - .querySelector(".header__icon--open-search-menu-closed") - .classList.remove("is-hidden"); - document - .querySelector(".header__icon--close-search-menu-closed") - .classList.remove("is-unhidden"); - } - } - }; - - let MobileSearch = class { - static selector() { - return ".js-mobile-search-toggle"; - } - - constructor(node, mobileMenu, searchForm, searchToggleButton) { - this.node = node; - this.mobileMenu = mobileMenu[0]; - this.searchForm = searchForm[0]; - this.searchToggleButton = searchToggleButton[0]; - this.bindEventListeners(); - } - - bindEventListeners() { - this.node.click(this.toggle.bind(this)); - } - - toggle() { - // hide the mobile menu - this.mobileMenu.classList.remove("is-visible"); - - // wait for the mobile menu to close - setTimeout(() => { - // open the search - this.searchForm.classList.add("is-visible"); - - // swap the icons - this.searchToggleButton - .querySelector(".header__icon--open-search") - .classList.add("is-hidden"); - this.searchToggleButton - .querySelector(".header__icon--close-search") - .classList.add("is-unhidden"); - }, 250); } }; @@ -123,21 +29,7 @@ new MobileMenu( $(el), $(".js-mobile-menu-close"), - $(".header__menus--mobile"), - $(".header__search") - ); - }); - - $(Search.selector()).each((index, el) => { - new Search($(el), $(".header__search")); - }); - - $(MobileSearch.selector()).each((index, el) => { - new MobileSearch( - $(el), - $(".header__menus--mobile"), - $(".header__search"), - $(".js-search-toggle") + $(".header__menus--mobile") ); }); diff --git a/hypha/static_src/src/sass/apply/abstracts/_mixins.scss b/hypha/static_src/src/sass/apply/abstracts/_mixins.scss index 36660810b28f278076114b1630d7c3683993456e..4a7e42257d17d90d03f87b229009b7f0f22ed488 100644 --- a/hypha/static_src/src/sass/apply/abstracts/_mixins.scss +++ b/hypha/static_src/src/sass/apply/abstracts/_mixins.scss @@ -85,7 +85,7 @@ // Button mixin @mixin button($bg, $hover-bg) { - padding: 0.5em 50px; + padding: 0.5em 2rem; font-weight: $weight--bold; color: $color--white; text-align: center; diff --git a/hypha/static_src/src/sass/apply/components/_dashboard-table.scss b/hypha/static_src/src/sass/apply/components/_dashboard-table.scss new file mode 100644 index 0000000000000000000000000000000000000000..1a63c5334f8438bc3b698d4ac176979c8f7febff --- /dev/null +++ b/hypha/static_src/src/sass/apply/components/_dashboard-table.scss @@ -0,0 +1,52 @@ +// stylelint-disable selector-max-compound-selectors + +.paf-review-table { + @include table-ordering-styles; + + thead { + display: none; + + @include media-query($table-breakpoint) { + display: table-header-group; + } + + th { + // ordering - adjust alignment + &.desc { + position: relative; + color: $color--dark-grey; + + &::after { + position: absolute; + top: 40%; + margin-left: 5px; + } + } + + &.asc { + position: relative; + color: $color--dark-grey; + + &::after { + position: absolute; + top: 50%; + margin-left: 5px; + } + } + } + } + + tbody { + td { + // stylelint-disable-next-line force-element-nesting + > span.mobile-label { + display: inline-block; + width: 90px; + + @include media-query($table-breakpoint) { + display: none; + } + } + } + } +} diff --git a/hypha/static_src/src/sass/apply/components/_data-block.scss b/hypha/static_src/src/sass/apply/components/_data-block.scss index 4525092609910c648874bf26f56a94a95b37ce54..a639f39d8d1c7e90a2af80e98018ffb0e327a35b 100644 --- a/hypha/static_src/src/sass/apply/components/_data-block.scss +++ b/hypha/static_src/src/sass/apply/components/_data-block.scss @@ -156,6 +156,12 @@ text-decoration: underline; color: $color--primary; word-break: normal; + transition-property: opacity; + transition-duration: 150ms; + + &:hover { + opacity: 0.75; + } &:last-child { margin: 0; @@ -168,9 +174,15 @@ display: flex; align-items: center; margin-right: 1rem; - color: $color--primary; word-break: normal; + transition-property: opacity; + transition-duration: 150ms; + + &:hover { + opacity: 0.75; + } + &:last-child { margin: 0; } diff --git a/hypha/static_src/src/sass/apply/components/_feed.scss b/hypha/static_src/src/sass/apply/components/_feed.scss index 5228385373bd59a63c1d04286df35066251136c6..8bda96812d4241272dd7c649b707b4b8403db103 100644 --- a/hypha/static_src/src/sass/apply/components/_feed.scss +++ b/hypha/static_src/src/sass/apply/components/_feed.scss @@ -2,9 +2,7 @@ &__item { position: relative; display: flex; - padding-bottom: 20px; - margin-bottom: 25px; - border-bottom: 1px solid $color--mid-grey; + margin-bottom: 1.5em; &:last-child { border-bottom: 0; @@ -27,10 +25,7 @@ } &__label { - padding: 5px 10px; margin: 0; - font-size: 12px; - font-weight: $weight--bold; color: $color--white; text-align: center; @@ -71,20 +66,11 @@ } &__pre-content { - display: none; width: 110px; - - @include media-query(small-tablet) { - display: block; - } } &__content { width: 100%; - - @include media-query(small-tablet) { - padding-left: 20px; - } } &__meta { diff --git a/hypha/static_src/src/sass/apply/components/_form.scss b/hypha/static_src/src/sass/apply/components/_form.scss index 6dd2417475800bed4af6dbd78e245c8675a3740d..639b40a8782b95aabc2fac9ee912feb09c13bd9a 100644 --- a/hypha/static_src/src/sass/apply/components/_form.scss +++ b/hypha/static_src/src/sass/apply/components/_form.scss @@ -32,6 +32,18 @@ } } + &--error-inline { + // stylelint-disable-next-line selector-class-pattern + .form__error-text { + position: relative; + max-width: 100%; + + &::before { + display: none; + } + } + } + &__group { position: relative; margin-top: 0.5rem; @@ -102,23 +114,24 @@ &--multi_file_field, &--single_file_field, &--file_field { - @include button($color--light-blue, $color--darkest-blue); - max-width: 15rem; - text-align: center; - border: 0; + @include button($color--white, $color--light-blue); + display: inline-block; + color: $color--light-blue; + border: 1px solid $color--mid-grey; - .no-js & { - display: none; + &:focus { + color: $color--light-blue; } - .form__required { + &:hover { color: $color--white; } - &:hover { - .no-js & { - background: none; - } + max-width: 15rem; + text-align: center; + + .no-js & { + display: none; } } @@ -444,42 +457,43 @@ } &__comments { - display: grid; - grid-template-areas: "message" "visibility" "actions"; - grid-template-rows: auto auto auto; - grid-template-columns: 1fr; + .fields--visible { + display: grid; + grid-template-areas: "message" "attachments" "visibility" "actions"; + grid-template-rows: auto auto auto auto; + grid-template-columns: 1fr; - @include media-query(tablet-landscape) { - grid-template-areas: "message visibility" "actions actions"; - grid-template-rows: auto auto; - grid-template-columns: 2fr 1fr; - column-gap: 2rem; + @include media-query(tablet-landscape) { + grid-template-areas: "message attachments" "message visibility"; + grid-template-rows: auto auto; + grid-template-columns: 2fr 1fr; + column-gap: 2rem; + + .wmd-input, + .wmd-preview { + max-width: 100%; + margin-bottom: 0; + } + } - .wmd-input, - .wmd-preview { - max-width: 100%; - margin-bottom: 0; + // stylelint-disable-next-line selector-class-pattern + .id_attachments { + grid-area: attachments; } - } - // stylelint-disable-next-line selector-class-pattern - .id_message { - grid-area: message; + // stylelint-disable-next-line selector-class-pattern + .id_message { + grid-area: message; - > label { - @include hidden; + > label { + @include hidden; + } } - } - // stylelint-disable-next-line selector-class-pattern - .id_visibility_0 { - grid-area: visibility; - } - - .button { - grid-area: actions; - max-width: 210px; - text-align: center; + // stylelint-disable-next-line selector-class-pattern + .id_visibility_0 { + grid-area: visibility; + } } } diff --git a/hypha/static_src/src/sass/apply/components/_icon.scss b/hypha/static_src/src/sass/apply/components/_icon.scss index dcc3730deec2e9d416edeabce3a0da74557c6359..c3478566621144de9d1389015609a6590ad9e1b0 100644 --- a/hypha/static_src/src/sass/apply/components/_icon.scss +++ b/hypha/static_src/src/sass/apply/components/_icon.scss @@ -110,6 +110,13 @@ height: 16px; } + &--dashboard-tasks { + margin-left: 4px; + margin-right: 8px; + width: 24px; + height: 24px; + } + &--action-cross { stroke: $color--black; fill: transparent; diff --git a/hypha/static_src/src/sass/apply/components/_projects-table.scss b/hypha/static_src/src/sass/apply/components/_projects-table.scss index 425d24c246cda1c2d129f3ae940958f51d4b8d60..4af3a5963704c29803717bb52a2cf49266e21ee2 100644 --- a/hypha/static_src/src/sass/apply/components/_projects-table.scss +++ b/hypha/static_src/src/sass/apply/components/_projects-table.scss @@ -1,6 +1,30 @@ +// stylelint-disable selector-max-compound-selectors + .projects-table { @include table-ordering-styles; + thead { + display: none; + + @include media-query($table-breakpoint) { + display: table-header-group; + } + } + + tbody { + td { + // stylelint-disable-next-line force-element-nesting + > span.mobile-label { + display: inline-block; + width: 90px; + + @include media-query($table-breakpoint) { + display: none; + } + } + } + } + .reporting { .icon { margin-right: 0.3rem; @@ -13,4 +37,26 @@ .invoices-table { @include table-ordering-styles; + + thead { + display: none; + + @include media-query($table-breakpoint) { + display: table-header-group; + } + } + + tbody { + td { + // stylelint-disable-next-line force-element-nesting + > span.mobile-label { + display: inline-block; + width: 90px; + + @include media-query($table-breakpoint) { + display: none; + } + } + } + } } diff --git a/hypha/static_src/src/sass/apply/components/_status-bar.scss b/hypha/static_src/src/sass/apply/components/_status-bar.scss index 9e1b5c7309e95b25052d94330d5c1274f8e1c335..e6d9ba86a9bec3bb791e2aa538a5cd35c83c3ef1 100644 --- a/hypha/static_src/src/sass/apply/components/_status-bar.scss +++ b/hypha/static_src/src/sass/apply/components/_status-bar.scss @@ -1,10 +1,15 @@ // stylelint-disable selector-class-pattern .status-bar { + --bar: 3px; + --dot: 20px; + --triangle: 5px; + --tooltip-padding: 10px; + --tooltip-max-width: 12ch; + --tooltip-margin-top: 17px; + $root: &; display: none; - padding: 30px 10px 80px; - margin-right: 16px; @include media-query(tablet-portrait) { display: flex; @@ -17,197 +22,147 @@ } &--small { + --tooltip-max-width: 10ch; width: 100%; - max-width: 800px; - margin-right: 40px; + max-width: 750px; + margin-right: 30px; + margin-left: 16px; } &__subheading { display: inline-block; padding: 5px 10px; - margin: 10px 0 0; color: $color--white; background-color: $color--tomato; } - &__icon { - position: absolute; - top: -10px; - left: 0; - z-index: 30; - display: none; - width: 20px; - height: 20px; - - .status-bar__item--is-current &, - .status-bar__item--is-complete & { - display: block; - - .status-bar--small & { - display: block; - border-radius: 50%; - box-shadow: 0 1px 9px 0 $color--black-50; - } - } - - .status-bar__item:first-of-type & { - left: -10px; - } + &__text { + font-size: map-get($font-sizes, milli); + font-weight: $weight--bold; } &__item { - position: relative; flex: 1; - height: 3px; - background: $color--mid-grey; + position: relative; + padding-block-start: var(--tooltip-margin-top); + + // The bar for each item. + border-block-start: var(--bar) solid $color--mid-grey; - // every items dot + // The dot for each item. &::before { + display: flex; + place-items: center; position: absolute; - top: -10px; - left: 0; - width: 20px; - height: 20px; + top: calc(-1 * var(--dot) / 2); + left: calc(-1 * var(--dot) / 2); + width: var(--dot); + height: var(--dot); background: $color--dark-grey; - border: 5px solid $color--mid-grey; + border: calc(var(--dot) / 4) solid $color--mid-grey; border-radius: 50%; content: ""; - .status-bar--small & { + #{$root}--small & { background: $color--white; } } - // last items dont have a dot - &:last-of-type { - flex: 0; - height: 0; + &--is-current { + &::before { + background: $color--white; + border-color: $color--tomato; + } - &.status-bar__item--is-complete { - &::after { - display: none; - } + // Fill the bar all the way on accepted/declined. + &:last-of-type { + border-color: $color--primary; } } - &:first-of-type { + &--is-complete { + border-color: $color--primary; + &::before { - left: -10px; + font-size: map-get($font-sizes, milli); + font-weight: $weight--bold; + color: $color--white; + background: $color--primary; + border-color: $color--primary; + content: "✓"; + + #{$root}--small & { + background: $color--primary; + } } } - &--is-current { + &:first-of-type { &::before { - position: absolute; - top: -10px; - right: -20px; - z-index: 10; - width: 20px; - height: 20px; - background: $color--white; - border: 5px solid $color--error; - border-radius: 50%; - content: ""; + left: 0; } } - &--is-complete { - background: $color--primary; + &:nth-last-of-type(2) { + flex: 0; + } - &:last-of-type { - &::after { - background: $color--primary; - } - } + &:last-of-type { + flex: 0; &::before { - background: $color--primary; - border-color: $color--primary; - - .status-bar--small & { - background: $color--primary; - } + left: auto; + right: 0; } } } &__tooltip { - // tooltip hover area - not visibile - position: absolute; - top: -10px; - z-index: 100; - width: 20px; - height: 20px; - border-radius: 50%; - opacity: 1; - transition: opacity $transition; - - .status-bar__item:first-of-type & { - left: -10px; + --tooltip-width: min(var(--tooltip-max-width), var(--tooltip-chars)); + width: var(--tooltip-width); + margin-inline-start: calc(-1 * var(--tooltip-width) / 2 - 5px); + text-align: center; + padding-block: var(--tooltip-padding); + color: $color--mid-grey; + + #{$root}__item--is-complete & { + color: $color--primary; } - .status-bar__item--is-current & { - opacity: 1; + #{$root}__item--is-current & { + width: calc(var(--tooltip-width) + var(--tooltip-padding)); + position: relative; + color: $color--white; + background-color: $color--tomato; + padding-inline: var(--tooltip-padding); } &::before { - .status-bar__item--is-current & { - @include triangle(top, $color--error, 5px); + #{$root}__item--is-current & { + @include triangle(top, $color--tomato, 5px); position: absolute; - bottom: -10px; - left: 5px; - } - } - - // tooltip contents - &::after { - position: absolute; - top: 30px; - left: -25px; - text-align: center; - display: block; - padding: 5px 10px; - font-size: 12px; - font-weight: $weight--bold; - background-color: $color--error; - content: attr(data-title); - - // prevent first tooltip hitting viewport edge - .status-bar__item:first-of-type & { - left: 0; - - @include media-query(desktop) { - left: -25px; - } + top: calc(-1 * var(--triangle) - 2px); + left: calc(50% - var(--triangle)); } - // prevent last tooltip hitting viewport edge - .status-bar__item:last-of-type & { - left: -45px; - - @include media-query(tablet-portrait) { - left: -60px; - } - - @include media-query(desktop-medium) { - left: -35px; - } + #{$root}__item--is-current:first-of-type & { + left: var(--triangle); } - .status-bar__item & { - background-color: inherit; - color: $color--mid-grey; + #{$root}__item--is-current:last-of-type & { + left: initial; + right: var(--triangle); } + } - .status-bar__item--is-complete & { - background-color: inherit; - color: $color--primary; - } + #{$root}__item:first-of-type & { + margin-inline-start: initial; + text-align: start; + } - .status-bar__item--is-current & { - background-color: $color--tomato; - color: $color--white; - } + #{$root}__item:last-of-type & { + margin-inline-start: initial; + text-align: end; } } } diff --git a/hypha/static_src/src/sass/apply/components/_two-factor.scss b/hypha/static_src/src/sass/apply/components/_two-factor.scss index 410b24f6b73fd5363d7c967c677c48ee04e9e806..c408502e6bb063d20a265ecb8ece4eedfdbfef27 100644 --- a/hypha/static_src/src/sass/apply/components/_two-factor.scss +++ b/hypha/static_src/src/sass/apply/components/_two-factor.scss @@ -24,24 +24,6 @@ label[for="id_generator-token"] { font-size: 1.2em; } -#list-backup-tokens { - border: $color--mid-grey; - padding: 1em; - line-height: 1.4em; - font-size: larger; - font-family: monospace; - resize: none; - font-style: bold; -} - -.d-none { - display: none; -} - -.bg-white { - background-color: $color--white; -} - // 2FA token field. #id_generator-token { -moz-appearance: textfield; diff --git a/hypha/static_src/src/sass/apply/components/_wrapper.scss b/hypha/static_src/src/sass/apply/components/_wrapper.scss index bff0fd4fd64beeb59d9664463479e149c358ed90..e46a364ea94ac22b4a63dff926e89a4e427f7f46 100644 --- a/hypha/static_src/src/sass/apply/components/_wrapper.scss +++ b/hypha/static_src/src/sass/apply/components/_wrapper.scss @@ -202,7 +202,7 @@ } .card:first-child { - margin-top: 20px; + margin-top: 0; } } diff --git a/hypha/static_src/src/sass/apply/layout/_header.scss b/hypha/static_src/src/sass/apply/layout/_header.scss index cbdb7d25d496128dcce225dd268083b827d745fd..e4e746baecc535a240f8bf6580010f33cbfcb6ac 100644 --- a/hypha/static_src/src/sass/apply/layout/_header.scss +++ b/hypha/static_src/src/sass/apply/layout/_header.scss @@ -26,15 +26,10 @@ } &__logo { - fill: $color--default; + max-width: none; &--mobile { width: 60px; - height: 60px; - - .is-visible & { - fill: $color--white; - } @include media-query(tablet-landscape) { display: none; @@ -46,8 +41,7 @@ @include media-query(tablet-landscape) { display: block; - width: 160px; - height: 40px; + width: 215px; } } } diff --git a/hypha/static_src/src/sass/apply/main.scss b/hypha/static_src/src/sass/apply/main.scss index 40ed6f1c793e4f81f83f4ba159437811792b7889..ab9dae30eda148e37d28cceaa91d3c6f698e9fc7 100644 --- a/hypha/static_src/src/sass/apply/main.scss +++ b/hypha/static_src/src/sass/apply/main.scss @@ -71,6 +71,7 @@ @import "components/activity-notifications"; @import "components/dropdown"; @import "components/banner"; +@import "components/dashboard-table"; // Layout @import "layout/header"; diff --git a/hypha/static_src/src/sass/apply/styleguide.scss b/hypha/static_src/src/sass/apply/styleguide.scss index 6667c9202f6c24cfefb8cdd9700e9fbd982b6751..5499fb5d17026b8a1db7730c1f8c50eb33266e7b 100644 --- a/hypha/static_src/src/sass/apply/styleguide.scss +++ b/hypha/static_src/src/sass/apply/styleguide.scss @@ -65,6 +65,7 @@ @import "components/two-factor"; @import "components/determination"; @import "components/dropdown"; +@import "components/dashboard-table"; // Layout @import "layout/header"; diff --git a/hypha/static_src/src/sass/apply/wagtail_groups_list.scss b/hypha/static_src/src/sass/apply/wagtail_groups_list.scss new file mode 100644 index 0000000000000000000000000000000000000000..8e136ec19b7d1c28de87a6095b11b8538da824f9 --- /dev/null +++ b/hypha/static_src/src/sass/apply/wagtail_groups_list.scss @@ -0,0 +1,27 @@ +.group-help-text { + font-size: smaller; + padding-left: 10px; +} + +// Stylings used for the Wagtail admin "roles" tab when creating/editing a user +.form { + &__label { + p { + margin: 0; + } + + .group-help-text { + color: var(--w-color-grey-400); + } + } +} + +// Stylings used for the Wagtail admin groups view +.title-wrapper { + a { + .group-help-text { + font-weight: normal; + display: inline; + } + } +} diff --git a/hypha/static_src/src/sass/public/components/_icon.scss b/hypha/static_src/src/sass/public/components/_icon.scss index 14a561720997471591651abd1f03e86fc1ac466c..ebe641709061b31ec9b684678528ec3c22931f23 100644 --- a/hypha/static_src/src/sass/public/components/_icon.scss +++ b/hypha/static_src/src/sass/public/components/_icon.scss @@ -13,12 +13,6 @@ fill: $color--white; } - &--footer-social { - @include media-query(tablet-portrait) { - margin-right: 10px; - } - } - &--mobile-menu { width: 32px; height: 28px; diff --git a/hypha/static_src/src/sass/public/layout/_header.scss b/hypha/static_src/src/sass/public/layout/_header.scss index c360d3eb9139c4f220f2b98f3d0003cfb2715479..d3b179044d570ccfb5cc8d513a3fbfb500bdedf5 100644 --- a/hypha/static_src/src/sass/public/layout/_header.scss +++ b/hypha/static_src/src/sass/public/layout/_header.scss @@ -170,24 +170,14 @@ } &__logo { - fill: $color--white; + max-width: none; &--mobile { width: 60px; - height: 60px; @include media-query(tablet-landscape) { display: none; } - - .header--light-bg & { - fill: $color--dark-grey; - } - - // stylelint-disable-next-line selector-class-pattern - .header__menus--mobile.is-visible & { - fill: $color--white; - } } &--desktop { @@ -196,27 +186,6 @@ @include media-query(tablet-landscape) { display: block; width: 215px; - height: 50px; - } - } - - &--desktop-light { - @include media-query(tablet-landscape) { - display: block; - } - - .header--light-bg & { - display: none; - } - } - - &--desktop-dark { - display: none; - - .header--light-bg & { - @include media-query(tablet-landscape) { - display: block; - } } } } diff --git a/hypha/templates/403.html b/hypha/templates/403.html new file mode 100644 index 0000000000000000000000000000000000000000..6a457099ad0817a62ad7a1e95994633f68a38548 --- /dev/null +++ b/hypha/templates/403.html @@ -0,0 +1,13 @@ +{% extends "base.html" %} +{% load wagtailcore_tags wagtailsettings_tags %} + +{% block title %}{{ settings.utils.SystemMessagesSettings.title_403 }}{% endblock %} + +{% block body_class %}template-403{% endblock %} + +{% block content %} + <div class="wrapper wrapper--small wrapper--inner-space-large"> + <h1>{{ settings.utils.SystemMessagesSettings.title_403 }}</h1> + <div class="rich-text">{{ settings.utils.SystemMessagesSettings.body_403|richtext }}</div> + </div> +{% endblock %} diff --git a/hypha/templates/base-apply.html b/hypha/templates/base-apply.html index 4982053b5aa3053d3c696be010eb80b984fd6982..5090295df7fc2e2aaaf0190d59db5add98a8137e 100644 --- a/hypha/templates/base-apply.html +++ b/hypha/templates/base-apply.html @@ -1,12 +1,14 @@ {% load i18n static wagtailcore_tags wagtailimages_tags navigation_tags util_tags hijack cookieconsent_tags %}<!doctype html> {% wagtail_site as current_site %} -<html class="no-js" lang="en"> +{% get_current_language as LANGUAGE_CODE %} +{% get_current_language_bidi as LANGUAGE_BIDI %} +<html class="no-js" lang="{{ LANGUAGE_CODE }}" dir="{% if LANGUAGE_BIDI %}rtl{% else %}ltr{% endif %}"> <head> {# TODO fallbacks if page is not defined e.g. for 404 page #} - <meta charset="utf-8" /> - <meta name="viewport" content="width=device-width, initial-scale=1" /> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width"> <title>{% block title_prefix %}{% if current_site.site_name %}{{ current_site.site_name }} | {% endif %}{% endblock %}{% block title %}{% if page.seo_title %}{{ page.seo_title }}{% else %}{{ page.title }}{% endif %}{% endblock %}{% block title_suffix %}{{ TITLE_SUFFIX }}{% endblock %}</title> - <meta name="description" content="{% if page.search_description %}{{ page.search_description }}{% else %}{{ page.listing_summary }}{% endif %}" /> + <meta name="description" content="{% if page.search_description %}{{ page.search_description }}{% else %}{{ page.listing_summary }}{% endif %}"> <!-- favicons --> {% comment %} @@ -27,6 +29,9 @@ {% block extra_css %}{% endblock %} <link rel="stylesheet" href="{% static 'css/print.css' %}" media="print"> + {% comment %}Detect if javascript is enabled; helps with styling{% endcomment %} + <script>document.querySelector("html").classList.replace("no-js", "js")</script> + <!-- htmx start: installed with npm --> <script src="{% static 'js/apply/vendor/htmx.min.js' %}"></script> <script src="{% static 'js/apply/vendor/htmx-ext-multi-swap.min.js' %}"></script> @@ -37,12 +42,24 @@ document.addEventListener("htmx:beforeRequest", function() { NProgress.start(); }); document.addEventListener("htmx:afterRequest", function() { NProgress.done(); }); }); + {% comment %} + If the htmx response is either 403, 404, or 500, display the error page. + Please note that the error replaces the entire page, rather than displaying + the error response in the target element. + https://stackoverflow.com/a/74823597/782901 + {% endcomment %} + document.addEventListener("htmx:beforeOnLoad", function (event) { + const xhr = event.detail.xhr + if (xhr.status == 500 || xhr.status == 403 || xhr.status == 404) { + event.stopPropagation() // Tell htmx not to process these requests + document.children[0].innerHTML = xhr.response // Swap in body of response instead + } + }); </script> <!-- htmx end --> <script src="{% static 'js/jquery.min.js' %}"></script> <script src="{% static 'js/js.cookie.min.js' %}"></script> - <script src="{% static 'js/main-top.js' %}"></script> <!-- alpine js start --> <style> [x-cloak] {display: none !important} </style> @@ -74,24 +91,36 @@ {% if settings.utils.SystemMessagesSettings.site_logo_default %} {% image settings.utils.SystemMessagesSettings.site_logo_default width-215 as logo_default %} <img class="header__logo header__logo--desktop" + width="215" src="{{ logo_default.url }}" alt="{{ settings.utils.SystemMessagesSettings.site_logo_default.alt }}" > {% if settings.utils.SystemMessagesSettings.site_logo_mobile %} {% image settings.utils.SystemMessagesSettings.site_logo_mobile width-60 as logo_mobile %} <img class="header__logo header__logo--mobile" + width="60" src="{{ logo_mobile.url }}" alt="{{ settings.utils.SystemMessagesSettings.site_logo_mobile.alt }}" > {% else %} <img class="header__logo header__logo--mobile" + width="60" src="{{ logo_default.url }}" alt="{{ settings.utils.SystemMessagesSettings.site_logo_default.alt }}" > {% endif %} {% else %} - <svg class="header__logo header__logo--desktop" aria-label="Site Logo"><use xlink:href="#logo-desktop--dark"></use></svg> - <svg class="header__logo header__logo--mobile" aria-label="Site Logo"><use xlink:href="#logo-mobile"></use></svg> + <img class="header__logo header__logo--desktop" + width="215" + height="40" + src="{% static 'images/logo.png' %}" + alt="Hypha logo" + > + <img class="header__logo header__logo--mobile" + width="60" height="60" + src="{% static 'images/logo-small.png' %}" + alt="Hypha logo" + > {% endif %} </a> @@ -115,9 +144,18 @@ <a href="{{ settings.utils.SystemMessagesSettings.site_logo_link|default:"/" }}" aria-label="Home link"> {% if settings.utils.SystemMessagesSettings.site_logo_mobile %} {% image settings.utils.SystemMessagesSettings.site_logo_mobile width-60 as logo_mobile %} - <img class="header__logo header__logo--mobile" src="{{ logo_mobile.url }}"> + <img class="header__logo header__logo--mobile" + width="60" + src="{{ logo_mobile.url }}" + alt="{{ settings.utils.SystemMessagesSettings.site_logo_mobile.alt }}" + > {% else %} - <svg class="header__logo header__logo--mobile"><use xlink:href="#logo-mobile"></use></svg> + <img class="header__logo header__logo--mobile" + width="60" + height="60" + src="{% static 'images/logo-small.png' %}" + alt="Hypha logo" + > {% endif %} </a> <div class="header__inner header__inner--mobile-buttons"> @@ -167,8 +205,10 @@ {% trans "Log out" %} </a> {% else %} - {% include "utils/includes/login_button.html" %} - {% if ENABLE_REGISTRATION_WITHOUT_APPLICATION %} + {% if request.path != '/auth/' %} + {% include "utils/includes/login_button.html" %} + {% endif %} + {% if ENABLE_PUBLIC_SIGNUP and request.path != '/register/' %} {% include "utils/includes/register_button.html" %} {% endif %} {% endif %} @@ -184,6 +224,10 @@ <footer class="footer"></footer> + {% block scroll_to_top %} + {% #scroll-to-top %} + {% endblock scroll_to_top %} + {% cookie_banner %} <script src="{% static 'js/main.js' %}"></script> diff --git a/hypha/templates/base.html b/hypha/templates/base.html index 382565e0d3b6e29181bdb1aa33d8aa7cc0e759c8..7f782c7192993c4d54fc88380ecdde9584d695b7 100644 --- a/hypha/templates/base.html +++ b/hypha/templates/base.html @@ -1,12 +1,14 @@ -{% load static cache wagtailcore_tags wagtailimages_tags navigation_tags util_tags cookieconsent_tags %}<!doctype html> +{% load static cache wagtailcore_tags wagtailimages_tags navigation_tags util_tags cookieconsent_tags i18n %}<!doctype html> {% wagtail_site as current_site %} -<html class="no-js" lang="en"> +{% get_current_language as LANGUAGE_CODE %} +{% get_current_language_bidi as LANGUAGE_BIDI %} +<html class="no-js" lang="{{ LANGUAGE_CODE }}" dir="{% if LANGUAGE_BIDI %}rtl{% else %}ltr{% endif %}"> <head> {# TODO fallbacks if page is not defined e.g. for 404 page #} - <meta charset="utf-8" /> - <meta name="viewport" content="width=device-width, initial-scale=1" /> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width"> <title>{% block title_prefix %}{% if current_site.site_name %}{{ current_site.site_name }} | {% endif %}{% endblock %}{% block title %}{% if page.seo_title %}{{ page.seo_title }}{% else %}{{ page.title }}{% endif %}{% endblock %}{% block title_suffix %}{{ TITLE_SUFFIX }}{% endblock %}</title> - <meta name="description" content="{% if page.search_description %}{{ page.search_description }}{% else %}{{ page.listing_summary }}{% endif %}" /> + <meta name="description" content="{% if page.search_description %}{{ page.search_description }}{% else %}{{ page.listing_summary }}{% endif %}"> {% block feedlinks %}{% endblock %} <!-- favicons --> @@ -26,35 +28,21 @@ <!-- Twitter summary card - see https://dev.twitter.com/cards/types/summary --> <!-- and https://dev.twitter.com/cards/getting-started --> - <meta name="twitter:card" content="summary" /> - <meta name="twitter:site" content="@{{ settings.utils.SocialMediaSettings.twitter_handle }}" /> - <meta name="twitter:title" content="{{ page.title }}" /> - <meta name="twitter:description" content="{{ page|social_text:current_site }}"> - {% if page.social_image %} - {% image page.social_image width-320 as social_img %} - <meta name="twitter:image" content="{% if page.social_image.is_stored_locally %}{{ current_site.root_url }}{% endif %}{{ social_img.url }}"> - {% else %} - <meta name="twitter:image" content="{{ current_site.root_url }}{% static 'images/otf_social.jpg' %}"> - {% endif %} + <meta name="twitter:card" content="summary"> + <meta name="twitter:title" content="{{ page.title }}"> <!--facebook opengraph tags--> - <meta property="fb:app_id" content="{{ settings.utils.SocialMediaSettings.facebook_app_id }}" /> - <meta property="og:type" content="website" /> - <meta property="og:url" content="{{ page.url }}" /> - <meta property="og:title" content="{{ page.title }}" /> - {% if page.social_image %} - <meta property="og:image" content="{% if page.social_image.is_stored_locally %}{{ current_site.root_url }}{% endif %}{{ social_img.url }}" /> - {% else %} - <meta property="og:image" content="{{ current_site.root_url }}{% static 'images/otf_social.jpg' %}" /> - {% endif %} - <meta property="og:description" content="{{ page|social_text:current_site }}" /> - <meta property="og:site_name" content="{{ settings.utils.SocialMediaSettings.site_name }}" /> + <meta property="og:type" content="website"> + <meta property="og:url" content="{{ page.url }}"> + <meta property="og:title" content="{{ page.title }}"> <link rel="stylesheet" href="{% static 'css/normalize.css' %}"> <link rel="stylesheet" href="{% static 'css/public/main.css' %}"> {% block extra_css %}{% endblock %} <link rel="stylesheet" href="{% static 'css/print.css' %}" media="print"> + <script>document.querySelector("html").classList.replace("no-js", "js")</script> + <!-- alpine js start --> <script defer src="{% static 'js/apply/vendor/alpine.min.js' %}"></script> <style> [x-cloak] {display: none !important} </style> @@ -62,7 +50,6 @@ <script src="{% static 'js/jquery.min.js' %}"></script> - <script src="{% static 'js/main-top.js' %}"></script> </head> <body class="{% block body_class %}template-{{ page.get_verbose_name|slugify }}{% endblock %}"> @@ -87,25 +74,41 @@ <a href="{{ settings.utils.SystemMessagesSettings.site_logo_link|default:"/" }}" aria-label="Home link"> {% if settings.utils.SystemMessagesSettings.site_logo_default %} {% image settings.utils.SystemMessagesSettings.site_logo_default width-215 as logo_default %} - <img class="header__logo header__logo--desktop" src="{{ logo_default.url }}"> + <img class="header__logo header__logo--desktop" + width="215" + src="{{ logo_default.url }}" + alt="{{ settings.utils.SystemMessagesSettings.site_logo_default.alt }}" + > {% if settings.utils.SystemMessagesSettings.site_logo_mobile %} {% image settings.utils.SystemMessagesSettings.site_logo_mobile width-60 as logo_mobile %} - <img class="header__logo header__logo--mobile" src="{{ logo_mobile.url }}"> + <img class="header__logo header__logo--mobile" + width="60" + src="{{ logo_mobile.url }}" + alt="{{ settings.utils.SystemMessagesSettings.site_logo_mobile.alt }}" + > {% else %} - <img class="header__logo header__logo--mobile" src="{{ logo_default.url }}"> + <img class="header__logo header__logo--mobile" + width="60" + src="{{ logo_default.url }}" + alt="{{ settings.utils.SystemMessagesSettings.site_logo_default.alt }}" + > {% endif %} {% else %} - <svg class="header__logo header__logo--desktop header__logo--desktop-light"><use xlink:href="#logo-desktop"></use></svg> - <svg class="header__logo header__logo--desktop header__logo--desktop-dark"><use xlink:href="#logo-desktop--dark"></use></svg> - <svg class="header__logo header__logo--mobile"><use xlink:href="#logo-mobile"></use></svg> + <img class="header__logo header__logo--desktop" + width="215" + height="40" + src="{% static 'images/logo.png' %}" + alt="Hypha logo" + > + <img class="header__logo header__logo--mobile" + width="60" height="60" + src="{% static 'images/logo-small.png' %}" + alt="Hypha logo" + > {% endif %} </a> <div class="header__inner header__inner--mobile-buttons"> - <button class="button js-search-toggle" aria-haspopup="true"> - <svg class="header__icon header__icon--open-search header__icon--open-search-menu-closed icon icon--mobile-menu"><use xlink:href="#magnifying-glass"></use></svg> - <svg class="header__icon header__icon--close-search header__icon--close-search-menu-closed icon icon--mobile-menu"><use xlink:href="#cross"></use></svg> - </button> <button class="button button--left-space js-mobile-menu-toggle" aria-haspopup="true"> <svg class="icon icon--mobile-menu"><use xlink:href="#mobile-menu-toggle"></use></svg> </button> @@ -119,10 +122,6 @@ {% primarynav %} {% endcache %} {% endif %} - <button class="button button--contains-icons button--left-space js-search-toggle" aria-haspopup="true" aria-label="Toggle desktop search"> - <svg class="header__icon header__icon--open-search icon"><use xlink:href="#magnifying-glass"></use></svg> - <svg class="header__icon header__icon--close-search icon"><use xlink:href="#cross"></use></svg> - </button> </section> <section class="header__menus header__menus--mobile"> @@ -130,19 +129,20 @@ <a href="{{ settings.utils.SystemMessagesSettings.site_logo_link|default:"/" }}" aria-label="Home link"> {% if settings.utils.SystemMessagesSettings.site_logo_mobile %} {% image settings.utils.SystemMessagesSettings.site_logo_mobile width-60 as logo_mobile %} - <img class="header__logo header__logo--mobile" src="{{ logo_mobile.url }}"> + <img class="header__logo header__logo--mobile" + width="60" + src="{{ logo_mobile.url }}" + alt="{{ settings.utils.SystemMessagesSettings.site_logo_mobile.alt }}" + > {% else %} - <svg class="header__logo header__logo--mobile"><use xlink:href="#logo-mobile"></use></svg> + <img class="header__logo header__logo--mobile" + width="60" + height="60" + src="{% static 'images/logo-small.png' %}" + alt="Hypha logo" + > {% endif %} </a> - <div class="header__inner header__inner--mobile-buttons"> - <button class="button js-mobile-search-toggle" aria-haspopup="true" aria-label="Toggle mobile search"> - <svg class="header__icon header__icon--open-search icon icon--mobile-menu"><use xlink:href="#magnifying-glass"></use></svg> - </button> - <button class="button button--left-space js-mobile-menu-close"> - <svg class="header__icon header__icon--cross icon icon--mobile-menu"><use xlink:href="#cross"></use></svg> - </button> - </div> </div> {% if settings.utils.SystemMessagesSettings.nav_content %} {{ settings.utils.SystemMessagesSettings.nav_content|safe }} @@ -155,24 +155,19 @@ <div class="header__button-container"> {% include "utils/includes/login_button.html" %} - {% if not request.user.is_authenticated and ENABLE_REGISTRATION_WITHOUT_APPLICATION %} - {% include "utils/includes/register_button.html" %} + + {% if request.user.is_authenticated %} + <a href="{% url 'users_public:logout' %}" class="button button--transparent button--narrow"> + {% trans "Log out" %} + </a> {% endif %} + {% if ENABLE_GOOGLE_TRANSLATE %} <div class="button button--google-translate" id="google_translate_element"></div> {% endif %} </div> </div> - <div class="header__search"> - <form action="{{ PUBLIC_SITE.root_url }}{% url 'search' %}" method="get" role="search" class="form form--header-search-desktop"> - <button class="button" type="submit" aria-label="Search"> - <svg class="icon icon--magnifying-glass icon--search"><use xlink:href="#magnifying-glass"></use></svg> - </button> - <input class="input input--transparent input--secondary" type="text" placeholder="Search…" name="query"{% if search_query %} value="{{ search_query }}{% endif %}" aria-label="Search input"> - </form> - </div> - <div class="wrapper wrapper--small wrapper--page-title"> {% block breadcrumbs %} {% include "navigation/breadcrumbs.html" %} @@ -190,25 +185,10 @@ <footer class="footer"> <div class="grid grid--two wrapper wrapper--large"> - {% if newsletter_enabled %} - <div class="footer__inner"> - {% include "mailchimp/newsletter_signup.html" %} - </div> - {% endif %} - <div class="footer__inner"> - <div class="footer__social-links"> - {% if settings.utils.SocialMediaSettings.twitter_handle %} - <a href="https://twitter.com/{{ settings.utils.SocialMediaSettings.twitter_handle }}"> - <svg class="icon icon--footer-social"><use xlink:href="#twitter"></use></svg> - <h4 class="heading heading--no-margin">@{{ settings.utils.SocialMediaSettings.twitter_handle }}</h4> - </a> - {% endif %} - </div> {{ settings.utils.SystemMessagesSettings.footer_content|safe }} </div> </div> - </footer> {% cookie_banner %} diff --git a/hypha/templates/includes/share.html b/hypha/templates/includes/share.html deleted file mode 100644 index fa189d7f2f03c42ee5c4b3d8c8263bdf7377328a..0000000000000000000000000000000000000000 --- a/hypha/templates/includes/share.html +++ /dev/null @@ -1,26 +0,0 @@ -{% load i18n util_tags wagtailcore_tags wagtailimages_tags %} -{% wagtail_site as current_site %} -<section class="section section--share"> - <h5>{% trans "Share" %}</h5> - {% image page.social_image fill-150x150 as social_img %} - {% with settings.utils.SocialMediaSettings as social_media_settings %} - <!-- see https://dev.twitter.com/web/tweet-button/web-intent --> - <a href="https://twitter.com/intent/tweet?text={{ page|social_text:current_site|urlencode }}&url={{ page.full_url|urlencode }}&via={{ social_media_settings.twitter_handle|urlencode }}" title="{% trans "Share on Twitter" %}"> - <svg class="icon icon--social-share icon--twitter-share"><use xlink:href="#twitter"></use></svg> - </a> - - <!-- see https://developer.linkedin.com/docs/share-on-linkedin --> - <a href="https://www.linkedin.com/shareArticle?mini=true&url={{ page.full_url|urlencode }}&title={{ page.title|urlencode }}&summary={{ page|social_text:current_site|urlencode }}&source={{ social_media_settings.site_name|urlencode }}" - title="{% trans "Share on LinkedIn" %}"> - <svg class="icon icon--social-share icon--linkedin-share"><use xlink:href="#linkedin"></use></svg> - </a> - - <!-- see https://developers.facebook.com/docs/sharing/reference/feed-dialog/v2.5 --> - <!-- Add a default image to use for social sharing here in case one is not provided on the page. --> - {% if social_media_settings.facebook_app_id %} - <a href="https://www.facebook.com/dialog/feed?app_id={{ social_media_settings.facebook_app_id }}&link={{ page.full_url|urlencode }}&picture={% if social_img %}{{ 'http://'|add:current_site.hostname|add:social_img.url|urlencode }}{% endif %}&name={{ page.title|urlencode }}&description={{ page|social_text:current_site|urlencode }}&redirect_uri={{ page.full_url|urlencode }}" title="{% trans "Share on Facebook" %}"> - <svg class="icon icon--social-share icon--facebook-share"><use xlink:href="#facebook"></use></svg> - </a> - {% endif %} - {% endwith %} -</section> diff --git a/hypha/templates/includes/sprites.html b/hypha/templates/includes/sprites.html index 1087051313a55f766558e55cf6fc0566bf4f8555..07d4aab9aaef7ea2457583286c63f07914c3adb3 100644 --- a/hypha/templates/includes/sprites.html +++ b/hypha/templates/includes/sprites.html @@ -4,14 +4,6 @@ <path d="M9.412 17.824C4.772 17.824 1 14.058 1 9.412 1 4.765 4.773 1 9.412 1c4.639 0 8.412 3.774 8.412 8.412 0 4.638-3.776 8.412-8.412 8.412zm0-15.142c-3.71 0-6.73 3.02-6.73 6.73 0 3.71 3.02 6.73 6.73 6.73 3.71 0 6.73-3.02 6.73-6.73 0-3.71-3.02-6.73-6.73-6.73zM21.902 23l-5.373-5.364 1.099-1.107L23 21.894z" /> </symbol> - <symbol id="twitter" viewBox="0 0 20.56 18.88"> - <path d="M20.56,2.24A7.69,7.69,0,0,1,18.14,3,4.71,4.71,0,0,0,20,.35,7.86,7.86,0,0,1,17.31,1.5,4,4,0,0,0,14.23,0,4.52,4.52,0,0,0,10,4.77a5.32,5.32,0,0,0,.11,1.09,11.58,11.58,0,0,1-8.69-5,5.24,5.24,0,0,0-.57,2.4,5,5,0,0,0,1.88,4,3.86,3.86,0,0,1-1.91-.6V6.7a4.66,4.66,0,0,0,3.38,4.67,3.67,3.67,0,0,1-1.11.17,3.72,3.72,0,0,1-.79-.08,4.32,4.32,0,0,0,3.94,3.31,7.87,7.87,0,0,1-5.24,2,7.51,7.51,0,0,1-1-.07,11,11,0,0,0,6.47,2.14c7.76,0,12-7.26,12-13.56,0-.21,0-.41,0-.62a9.25,9.25,0,0,0,2.1-2.47"/> - </symbol> - - <symbol id="facebook" viewBox="0 0 8.95 20.95"> - <path d="M0,6.93H1.92V4.82a6.24,6.24,0,0,1,.62-3.25A3.29,3.29,0,0,1,5.51,0,10.75,10.75,0,0,1,8.95.39L8.47,3.6a5.8,5.8,0,0,0-1.54-.26c-.75,0-1.41.3-1.41,1.14V6.93H8.57l-.21,3.13H5.51V21H1.92V10.06H0Z"/> - </symbol> - <symbol id="home" viewBox="0 0 512 512"> <g><path d="M506.555,208.064L263.859,30.367c-4.68-3.426-11.038-3.426-15.716,0L5.445,208.064 c-5.928,4.341-7.216,12.665-2.875,18.593s12.666,7.214,18.593,2.875L256,57.588l234.837,171.943c2.368,1.735,5.12,2.57,7.848,2.57 c4.096,0,8.138-1.885,10.744-5.445C513.771,220.729,512.483,212.405,506.555,208.064z"/></g> <g><path d="M442.246,232.543c-7.346,0-13.303,5.956-13.303,13.303v211.749H322.521V342.009c0-36.68-29.842-66.52-66.52-66.52 s-66.52,29.842-66.52,66.52v115.587H83.058V245.847c0-7.347-5.957-13.303-13.303-13.303s-13.303,5.956-13.303,13.303v225.053 c0,7.347,5.957,13.303,13.303,13.303h133.029c6.996,0,12.721-5.405,13.251-12.267c0.032-0.311,0.052-0.651,0.052-1.036v-128.89 c0-22.009,17.905-39.914,39.914-39.914s39.914,17.906,39.914,39.914v128.89c0,0.383,0.02,0.717,0.052,1.024 c0.524,6.867,6.251,12.279,13.251,12.279h133.029c7.347,0,13.303-5.956,13.303-13.303V245.847 C455.549,238.499,449.593,232.543,442.246,232.543z"/></g> @@ -29,6 +21,25 @@ <path d="M1 5.72222L6.5 11L15 1" stroke="black" stroke-width="2"/> </svg> + <symbol id="dashboard-contract" viewBox="0 0 24 24" fill="none"> + <path d="M12.8105 12.7789C14.0035 11.9649 14.9404 11.0667 15.6211 10.0842C16.3018 9.10175 16.6421 8.11228 16.6421 7.11579C16.6421 6.52632 16.5439 6.07018 16.3474 5.74737C16.1509 5.42456 15.8842 5.26316 15.5474 5.26316C14.7474 5.26316 14.0667 5.85965 13.5053 7.05263C12.9439 8.24561 12.6632 9.64912 12.6632 11.2632C12.6632 11.5439 12.6737 11.8105 12.6947 12.0632C12.7158 12.3158 12.7544 12.5544 12.8105 12.7789ZM3.84211 20V18.7368H5.10526V20H3.84211ZM7.31579 20V18.7368H8.57895V20H7.31579ZM10.7895 20V18.7368H12.0526V20H10.7895ZM14.2632 20V18.7368H15.5263V20H14.2632ZM17.7368 20V18.7368H19V20H17.7368ZM3.88421 16.5474L3 15.6632L4.34737 14.3158L3 12.9684L3.88421 12.0842L5.23158 13.4316L6.57895 12.0842L7.46316 12.9684L6.11579 14.3158L7.46316 15.6632L6.57895 16.5474L5.23158 15.2L3.88421 16.5474ZM14.3263 15.7895C13.8772 15.7895 13.4737 15.6912 13.1158 15.4947C12.7579 15.2982 12.4526 15.0105 12.2 14.6316C11.8491 14.8281 11.4877 15.014 11.1158 15.1895C10.7439 15.3649 10.3544 15.5298 9.94737 15.6842L9.50526 14.5053C9.89825 14.3509 10.2737 14.193 10.6316 14.0316C10.9895 13.8702 11.3368 13.6912 11.6737 13.4947C11.5754 13.1719 11.5053 12.8246 11.4632 12.4526C11.4211 12.0807 11.4 11.6842 11.4 11.2632C11.4 9.21404 11.7895 7.49123 12.5684 6.09474C13.3474 4.69825 14.3404 4 15.5474 4C16.2632 4 16.8351 4.28772 17.2632 4.86316C17.6912 5.4386 17.9053 6.21053 17.9053 7.17895C17.9053 8.41404 17.5018 9.62105 16.6947 10.8C15.8877 11.9789 14.7614 13.0386 13.3158 13.9789C13.4561 14.1614 13.6105 14.2982 13.7789 14.3895C13.9474 14.4807 14.1298 14.5263 14.3263 14.5263C14.8175 14.5263 15.3193 14.3158 15.8316 13.8947C16.3439 13.4737 16.8246 12.8702 17.2737 12.0842L18.4316 12.6316C18.3614 12.9965 18.3228 13.3193 18.3158 13.6C18.3088 13.8807 18.3263 14.2035 18.3684 14.5684C18.5649 14.4702 18.7719 14.3404 18.9895 14.1789C19.207 14.0175 19.414 13.8175 19.6105 13.5789L20.6211 14.3579C20.2702 14.793 19.8842 15.1404 19.4632 15.4C19.0421 15.6596 18.6491 15.7895 18.2842 15.7895C17.9474 15.7895 17.6737 15.6702 17.4632 15.4316C17.2526 15.193 17.1263 14.8491 17.0842 14.4C16.6772 14.8491 16.2386 15.193 15.7684 15.4316C15.2982 15.6702 14.8175 15.7895 14.3263 15.7895Z" fill="black"/> + </symbol> + + <symbol id="dashboard-paf" viewBox="0 0 24 24" fill="none" > + <path d="M4.5 22C4.0875 22 3.73438 21.8531 3.44063 21.5594C3.14688 21.2656 3 20.9125 3 20.5V5.5C3 5.0875 3.14688 4.73438 3.44063 4.44062C3.73438 4.14688 4.0875 4 4.5 4H9.625C9.70833 3.41667 9.975 2.9375 10.425 2.5625C10.875 2.1875 11.4 2 12 2C12.6 2 13.125 2.1875 13.575 2.5625C14.025 2.9375 14.2917 3.41667 14.375 4H19.5C19.9125 4 20.2656 4.14688 20.5594 4.44062C20.8531 4.73438 21 5.0875 21 5.5V20.5C21 20.9125 20.8531 21.2656 20.5594 21.5594C20.2656 21.8531 19.9125 22 19.5 22H4.5ZM4.5 20.5H19.5V5.5H4.5V20.5ZM7 18H13.825V16.5H7V18ZM7 13.75H17V12.25H7V13.75ZM7 9.5H17V8H7V9.5ZM12 5.075C12.2333 5.075 12.4375 4.9875 12.6125 4.8125C12.7875 4.6375 12.875 4.43333 12.875 4.2C12.875 3.96667 12.7875 3.7625 12.6125 3.5875C12.4375 3.4125 12.2333 3.325 12 3.325C11.7667 3.325 11.5625 3.4125 11.3875 3.5875C11.2125 3.7625 11.125 3.96667 11.125 4.2C11.125 4.43333 11.2125 4.6375 11.3875 4.8125C11.5625 4.9875 11.7667 5.075 12 5.075Z" fill="black"/> + </symbol> + + <symbol id="dashboard-document" viewBox="0 0 24 24" fill="none"> + <path d="M5.5 22C5.1 22 4.75 21.85 4.45 21.55C4.15 21.25 4 20.9 4 20.5V3.5C4 3.1 4.15 2.75 4.45 2.45C4.75 2.15 5.1 2 5.5 2H14.525L20 7.475V20.5C20 20.9 19.85 21.25 19.55 21.55C19.25 21.85 18.9 22 18.5 22H5.5ZM13.775 8.15V3.5H5.5V20.5H18.5V8.15H13.775Z" fill="black"/> + </symbol> + + <symbol id="dashboard-invoice" viewBox="0 0 24 24" fill="none"> + <path d="M11.25 18.975H12.75V17.975H14.25C14.4625 17.975 14.6406 17.9031 14.7844 17.7594C14.9281 17.6156 15 17.4375 15 17.225V13.975C15 13.7625 14.9281 13.5844 14.7844 13.4406C14.6406 13.2969 14.4625 13.225 14.25 13.225H10.5V11.475H15V9.975H12.75V8.975H11.25V9.975H9.75C9.5375 9.975 9.35938 10.0469 9.21563 10.1906C9.07188 10.3344 9 10.5125 9 10.725V13.975C9 14.1875 9.07188 14.3656 9.21563 14.5094C9.35938 14.6531 9.5375 14.725 9.75 14.725H13.5V16.475H9V17.975H11.25V18.975ZM5.5 22C5.1 22 4.75 21.85 4.45 21.55C4.15 21.25 4 20.9 4 20.5V3.5C4 3.1 4.15 2.75 4.45 2.45C4.75 2.15 5.1 2 5.5 2H14.525L20 7.475V20.5C20 20.9 19.85 21.25 19.55 21.55C19.25 21.85 18.9 22 18.5 22H5.5ZM13.275 7.475V3.5H5.5V20.5H18.5V7.475H13.275Z" fill="black"/> + </symbol> + + <symbol id="dashboard-report" viewBox="0 0 24 24" fill="none"> + <path d="M4 19V17.5H13.65V19H4ZM4 14.825V13.325H20V14.825H4ZM4 10.675V9.175H20V10.675H4ZM4 6.5V5H20V6.5H4Z" fill="black"/> + </symbol> <symbol id="hero-standard-right-pixels" viewBox="0 0 326 333"> <g fill-rule="nonzero"> @@ -127,10 +138,6 @@ <path d="M41.09 10.45l-2.77-3.36c-.56-.66-1.39-1.09-2.32-1.09h-24c-.93 0-1.76.43-2.31 1.09l-2.77 3.36c-.58.7-.92 1.58-.92 2.55v25c0 2.21 1.79 4 4 4h28c2.21 0 4-1.79 4-4v-25c0-.97-.34-1.85-.91-2.55zm-17.09 24.55l-11-11h7v-4h8v4h7l-11 11zm-13.75-25l1.63-2h24l1.87 2h-27.5z"/><path d="M0 0h48v48h-48z" fill="none"/> </symbol> - <symbol id="linkedin" viewBox="0 0 17 16" > - <path d="M17 9.81V16h-3.644v-5.776c0-1.45-.527-2.441-1.846-2.441-1.006 0-1.605.667-1.87 1.313-.095.23-.12.552-.12.875V16H5.875s.05-9.782 0-10.796H9.52v1.53l-.024.035h.024v-.035c.484-.734 1.349-1.783 3.284-1.783C15.202 4.95 17 6.494 17 9.81zM2.062 0C.816 0 0 .806 0 1.865 0 2.9.792 3.73 2.014 3.73h.024c1.272 0 2.062-.83 2.062-1.866C4.076.805 3.31 0 2.062 0zM.216 16H3.86V5.204H.216V16z" fill-rule="nonzero" /> - </symbol> - <symbol id="bell-icon" viewBox="0 0 16 16"> <path d="M8 16a2 2 0 0 0 2-2H6a2 2 0 0 0 2 2zm.995-14.901a1 1 0 1 0-1.99 0A5.002 5.002 0 0 0 3 6c0 1.098-.5 6-2 7h14c-1.5-1-2-5.902-2-7 0-2.42-1.72-4.44-4.005-4.901z"/> </symbol> diff --git a/mkdocs.yml b/mkdocs.yml index 5607f87c902120705ffbbb6b0661a874e83cbac4..6ce436846505dd8ecd50e81835e7b8feef092ea7 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,7 +1,7 @@ site_name: Hypha Documentation site_url: https://docs.hypha.app/ site_description: "Documentation for Hypha, an open source submission management platform." -copyright: Copyright © 2018 - 2023 - Open Technology Fund +copyright: Copyright © 2018 - 2024 - Open Technology Fund repo_name: HyphaApp/hypha repo_url: https://github.com/HyphaApp/hypha edit_uri: edit/main/docs/ diff --git a/package-lock.json b/package-lock.json index e026de4a06db354d4ef3dc33eb76bdae998fc9e4..0a6e8b549e31bd3592f41d6bbf89fefd4fc20e16 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,27 +9,27 @@ "version": "1.0.0", "license": "GPL-2.0", "devDependencies": { - "@alpinejs/focus": "^3.12.3", - "@babel/cli": "^7.22.6", - "@babel/core": "^7.22.8", - "@babel/preset-env": "^7.22.7", - "@tailwindcss/forms": "^0.5.3", - "@tailwindcss/typography": "^0.5.9", - "alpinejs": "^3.12.3", - "core-js": "^3.31.1", + "@alpinejs/focus": "^3.13.3", + "@babel/cli": "^7.23.4", + "@babel/core": "^7.23.5", + "@babel/preset-env": "^7.23.5", + "@tailwindcss/forms": "^0.5.7", + "@tailwindcss/typography": "^0.5.10", + "alpinejs": "^3.13.3", + "core-js": "^3.34.0", "daterangepicker": "^3.1.0", - "eslint": "^8.44.0", - "eslint-config-prettier": "^8.8.0", + "eslint": "^8.55.0", + "eslint-config-prettier": "^8.10.0", "eslint-plugin-template": "^0.7.0", - "htmx.org": "^1.9.2", - "nodemon": "^3.0.1", - "npm-run-all2": "^6.0.6", - "prettier": "3.0.0", - "sass": "^1.63.6", + "htmx.org": "^1.9.9", + "nodemon": "^3.0.2", + "npm-run-all2": "^6.1.1", + "prettier": "^3.1.0", + "sass": "^1.69.5", "stylelint": "^15.10.2", "stylelint-config-standard": "^34.0.0", "stylelint-config-standard-scss": "^10.0.0", - "tailwindcss": "^3.3.2" + "tailwindcss": "^3.3.6" }, "engines": { "node": "18.x" @@ -57,12 +57,13 @@ } }, "node_modules/@alpinejs/focus": { - "version": "3.12.3", - "resolved": "https://registry.npmjs.org/@alpinejs/focus/-/focus-3.12.3.tgz", - "integrity": "sha512-kIQwvvUPCfCO2REpceQ3uHNdN3oqDLvvvQNaHVgHoYqk+RrL3EcR6uOHyvHJUgOhaIjN5Uc3b7BaRNrKZbDGew==", + "version": "3.13.3", + "resolved": "https://registry.npmjs.org/@alpinejs/focus/-/focus-3.13.3.tgz", + "integrity": "sha512-fTRX/9wOfysyZ1PJ4gHeUnmiNTIgqBDIqKxeP5iMvj1UHD3TFLDXllvoIKH3ezqcsyQZqxd/q1MFM7dlIhkmeg==", "dev": true, "dependencies": { - "focus-trap": "^6.6.1" + "focus-trap": "^6.9.4", + "tabbable": "^5.3.3" } }, "node_modules/@ampproject/remapping": { @@ -79,14 +80,14 @@ } }, "node_modules/@babel/cli": { - "version": "7.22.6", - "resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.22.6.tgz", - "integrity": "sha512-Be3/RfEDmkMRGT1+ru5nTkfcvWz5jDOYg1V9rXqTz2u9Qt96O1ryboGvxVBp7wOnYWDB8DNHIWb6DThrpudfOw==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.23.4.tgz", + "integrity": "sha512-j3luA9xGKCXVyCa5R7lJvOMM+Kc2JEnAEIgz2ggtjQ/j5YUVgfsg/WsG95bbsgq7YLHuiCOzMnoSasuY16qiCw==", "dev": true, "dependencies": { "@jridgewell/trace-mapping": "^0.3.17", "commander": "^4.0.1", - "convert-source-map": "^1.1.0", + "convert-source-map": "^2.0.0", "fs-readdir-recursive": "^1.1.0", "glob": "^7.2.0", "make-dir": "^2.1.0", @@ -108,12 +109,12 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.22.13", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", - "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", + "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", "dev": true, "dependencies": { - "@babel/highlight": "^7.22.13", + "@babel/highlight": "^7.23.4", "chalk": "^2.4.2" }, "engines": { @@ -121,35 +122,35 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.22.6", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.6.tgz", - "integrity": "sha512-29tfsWTq2Ftu7MXmimyC0C5FDZv5DYxOZkh3XD3+QW4V/BYuv/LyEsjj3c0hqedEaDt6DBfDvexMKU8YevdqFg==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.5.tgz", + "integrity": "sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.22.8", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.8.tgz", - "integrity": "sha512-75+KxFB4CZqYRXjx4NlR4J7yGvKumBuZTmV4NV6v09dVXXkuYVYLT68N6HCzLvfJ+fWCxQsntNzKwwIXL4bHnw==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.5.tgz", + "integrity": "sha512-Cwc2XjUrG4ilcfOw4wBAK+enbdgwAcAJCfGUItPBKR7Mjw4aEfAFYrLxeRp4jWgtNIKn3n2AlBOfwwafl+42/g==", "dev": true, "dependencies": { "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.22.5", - "@babel/generator": "^7.22.7", - "@babel/helper-compilation-targets": "^7.22.6", - "@babel/helper-module-transforms": "^7.22.5", - "@babel/helpers": "^7.22.6", - "@babel/parser": "^7.22.7", - "@babel/template": "^7.22.5", - "@babel/traverse": "^7.22.8", - "@babel/types": "^7.22.5", - "@nicolo-ribaudo/semver-v6": "^6.3.3", - "convert-source-map": "^1.7.0", + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.5", + "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helpers": "^7.23.5", + "@babel/parser": "^7.23.5", + "@babel/template": "^7.22.15", + "@babel/traverse": "^7.23.5", + "@babel/types": "^7.23.5", + "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", - "json5": "^2.2.2" + "json5": "^2.2.3", + "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" @@ -159,13 +160,22 @@ "url": "https://opencollective.com/babel" } }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/@babel/generator": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", - "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.5.tgz", + "integrity": "sha512-BPssCHrBD+0YrxviOa3QzpqwhNIXKEtOa2jQrm4FlmkC2apYgRnQcmPWiGZDlGxiNtltnUFolMe8497Esry+jA==", "dev": true, "dependencies": { - "@babel/types": "^7.23.0", + "@babel/types": "^7.23.5", "@jridgewell/gen-mapping": "^0.3.2", "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" @@ -187,51 +197,57 @@ } }, "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.5.tgz", - "integrity": "sha512-m1EP3lVOPptR+2DwD125gziZNcmoNSHGmJROKoy87loWUQyJaVXDgpmruWqDARZSmtYQ+Dl25okU8+qhVzuykw==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.15.tgz", + "integrity": "sha512-QkBXwGgaoC2GtGZRoma6kv7Szfv06khvhFav67ZExau2RaXzy8MpHSMO2PNoP2XtmQphJQRHFfg77Bq731Yizw==", "dev": true, "dependencies": { - "@babel/types": "^7.22.5" + "@babel/types": "^7.22.15" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.22.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.6.tgz", - "integrity": "sha512-534sYEqWD9VfUm3IPn2SLcH4Q3P86XL+QvqdC7ZsFrzyyPF3T4XGiVghF6PTYNdWg6pXuoqXxNQAhbYeEInTzA==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz", + "integrity": "sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==", "dev": true, "dependencies": { - "@babel/compat-data": "^7.22.6", - "@babel/helper-validator-option": "^7.22.5", - "@nicolo-ribaudo/semver-v6": "^6.3.3", + "@babel/compat-data": "^7.22.9", + "@babel/helper-validator-option": "^7.22.15", "browserslist": "^4.21.9", - "lru-cache": "^5.1.1" + "lru-cache": "^5.1.1", + "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.22.6", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.6.tgz", - "integrity": "sha512-iwdzgtSiBxF6ni6mzVnZCF3xt5qE6cEA0J7nFt8QOAWZ0zjCFceEgpn3vtb2V7WFR6QzP2jmIFOHMTRo7eNJjQ==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.23.5.tgz", + "integrity": "sha512-QELlRWxSpgdwdJzSJn4WAhKC+hvw/AtHbbrIoncKHkhKKR/luAlKkgBDcri1EzWAo8f8VvYVryEHN4tax/V67A==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", - "@babel/helper-member-expression-to-functions": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-member-expression-to-functions": "^7.23.0", "@babel/helper-optimise-call-expression": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.20", "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", "@babel/helper-split-export-declaration": "^7.22.6", - "@nicolo-ribaudo/semver-v6": "^6.3.3" + "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" @@ -240,15 +256,24 @@ "@babel/core": "^7.0.0" } }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/@babel/helper-create-regexp-features-plugin": { - "version": "7.22.6", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.6.tgz", - "integrity": "sha512-nBookhLKxAWo/TUCmhnaEJyLz2dekjQvv5SRpE9epWQBcpedWLKt8aZdsuT9XV5ovzR3fENLjRXVT0GsSlGGhA==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.15.tgz", + "integrity": "sha512-29FkPLFjn4TPEa3RE7GpW+qbE8tlsu3jntNYNfcGsc49LphF1PQIiD+vMZ1z1xVOKt+93khA9tc2JBs3kBjA7w==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", - "@nicolo-ribaudo/semver-v6": "^6.3.3", - "regexpu-core": "^5.3.1" + "regexpu-core": "^5.3.1", + "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" @@ -257,10 +282,19 @@ "@babel/core": "^7.0.0" } }, + "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.1.tgz", - "integrity": "sha512-kX4oXixDxG197yhX+J3Wp+NpL2wuCFjWQAr6yX2jtCnflK9ulMI51ULFGIrWiX1jGfvAxdHp+XQCcP2bZGPs9A==", + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.3.tgz", + "integrity": "sha512-WBrLmuPP47n7PNwsZ57pqam6G/RGo1vw/87b0Blc53tZNGZ4x7YvZ6HgQe2vo1W/FR20OgjeZuGXzudPiXHFug==", "dev": true, "dependencies": { "@babel/helper-compilation-targets": "^7.22.6", @@ -270,7 +304,7 @@ "resolve": "^1.14.2" }, "peerDependencies": { - "@babel/core": "^7.4.0-0" + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "node_modules/@babel/helper-environment-visitor": { @@ -308,46 +342,46 @@ } }, "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.22.5.tgz", - "integrity": "sha512-aBiH1NKMG0H2cGZqspNvsaBe6wNGjbJjuLy29aU+eDZjSbbN53BaxlpB02xm9v34pLTZ1nIQPFYn2qMZoa5BQQ==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.23.0.tgz", + "integrity": "sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA==", "dev": true, "dependencies": { - "@babel/types": "^7.22.5" + "@babel/types": "^7.23.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-imports": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.5.tgz", - "integrity": "sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", + "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", "dev": true, "dependencies": { - "@babel/types": "^7.22.5" + "@babel/types": "^7.22.15" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.22.5.tgz", - "integrity": "sha512-+hGKDt/Ze8GFExiVHno/2dvG5IdstpzCq0y4Qc9OJ25D4q3pKfiIP/4Vp3/JvhDkLKsDK2api3q3fpIgiIF5bw==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz", + "integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==", "dev": true, "dependencies": { - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-module-imports": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-module-imports": "^7.22.15", "@babel/helper-simple-access": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.5", - "@babel/template": "^7.22.5", - "@babel/traverse": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/helper-validator-identifier": "^7.22.20" }, "engines": { "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, "node_modules/@babel/helper-optimise-call-expression": { @@ -372,15 +406,14 @@ } }, "node_modules/@babel/helper-remap-async-to-generator": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.5.tgz", - "integrity": "sha512-cU0Sq1Rf4Z55fgz7haOakIyM7+x/uCFwXpLPaeRzfoUtAEAuUZjZvFPjL/rk5rW693dIgn2hng1W7xbT7lWT4g==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.20.tgz", + "integrity": "sha512-pBGyV4uBqOns+0UvhsTO8qgl8hO89PmiDYv+/COyp1aeMcmfrfruz+/nCMFiYyFF/Knn0yfrC85ZzNFjembFTw==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-wrap-function": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-wrap-function": "^7.22.20" }, "engines": { "node": ">=6.9.0" @@ -390,20 +423,20 @@ } }, "node_modules/@babel/helper-replace-supers": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.22.5.tgz", - "integrity": "sha512-aLdNM5I3kdI/V9xGNyKSF3X/gTyMUBohTZ+/3QdQKAA9vxIiy12E+8E2HoOP1/DjeqU+g6as35QHJNMDDYpuCg==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.22.20.tgz", + "integrity": "sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw==", "dev": true, "dependencies": { - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-member-expression-to-functions": "^7.22.5", - "@babel/helper-optimise-call-expression": "^7.22.5", - "@babel/template": "^7.22.5", - "@babel/traverse": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-member-expression-to-functions": "^7.22.15", + "@babel/helper-optimise-call-expression": "^7.22.5" }, "engines": { "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, "node_modules/@babel/helper-simple-access": { @@ -443,9 +476,9 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", - "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", + "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", "dev": true, "engines": { "node": ">=6.9.0" @@ -461,47 +494,46 @@ } }, "node_modules/@babel/helper-validator-option": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.5.tgz", - "integrity": "sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", + "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-wrap-function": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.22.5.tgz", - "integrity": "sha512-bYqLIBSEshYcYQyfks8ewYA8S30yaGSeRslcvKMvoUk6HHPySbxHq9YRi6ghhzEU+yhQv9bP/jXnygkStOcqZw==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.22.20.tgz", + "integrity": "sha512-pms/UwkOpnQe/PDAEdV/d7dVCoBbB+R4FvYoHGZz+4VPcg7RtYy2KP7S2lbuWM6FCSgob5wshfGESbC/hzNXZw==", "dev": true, "dependencies": { "@babel/helper-function-name": "^7.22.5", - "@babel/template": "^7.22.5", - "@babel/traverse": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/template": "^7.22.15", + "@babel/types": "^7.22.19" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { - "version": "7.22.6", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.22.6.tgz", - "integrity": "sha512-YjDs6y/fVOYFV8hAf1rxd1QvR9wJe1pDBZ2AREKq/SDayfPzgk0PBnVuTCE5X1acEpMMNOVUqoe+OwiZGJ+OaA==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.5.tgz", + "integrity": "sha512-oO7us8FzTEsG3U6ag9MfdF1iA/7Z6dz+MtFhifZk8C8o453rGJFFWUP1t+ULM9TUIAzC9uxXEiXjOiVMyd7QPg==", "dev": true, "dependencies": { - "@babel/template": "^7.22.5", - "@babel/traverse": "^7.22.6", - "@babel/types": "^7.22.5" + "@babel/template": "^7.22.15", + "@babel/traverse": "^7.23.5", + "@babel/types": "^7.23.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/highlight": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", - "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", + "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", "dev": true, "dependencies": { "@babel/helper-validator-identifier": "^7.22.20", @@ -513,9 +545,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", - "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.5.tgz", + "integrity": "sha512-hOOqoiNXrmGdFbhgCzu6GiURxUgM27Xwd/aPuu8RfHEZPBzL1Z54okAHAQjXfcQNwvrlkAmAp4SlRTZ45vlthQ==", "dev": true, "bin": { "parser": "bin/babel-parser.js" @@ -525,9 +557,9 @@ } }, "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.22.5.tgz", - "integrity": "sha512-NP1M5Rf+u2Gw9qfSO4ihjcTGW5zXTi36ITLd4/EoAcEhIZ0yjMqmftDNl3QC19CX7olhrjpyU454g/2W7X0jvQ==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.23.3.tgz", + "integrity": "sha512-iRkKcCqb7iGnq9+3G6rZ+Ciz5VywC4XNRHe57lKM+jOeYAoR0lVqdeeDRfh0tQcTfw/+vBhHn926FmQhLtlFLQ==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -540,14 +572,14 @@ } }, "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.22.5.tgz", - "integrity": "sha512-31Bb65aZaUwqCbWMnZPduIZxCBngHFlzyN6Dq6KAJjtx+lx6ohKHubc61OomYi7XwVD4Ol0XCVz4h+pYFR048g==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.23.3.tgz", + "integrity": "sha512-WwlxbfMNdVEpQjZmK5mhm7oSwD3dS6eU+Iwsi4Knl9wAletWem7kaRsGOG+8UEbRyqxY4SS5zvtfXwX+jMxUwQ==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", - "@babel/plugin-transform-optional-chaining": "^7.22.5" + "@babel/plugin-transform-optional-chaining": "^7.23.3" }, "engines": { "node": ">=6.9.0" @@ -556,29 +588,29 @@ "@babel/core": "^7.13.0" } }, - "node_modules/@babel/plugin-proposal-private-property-in-object": { - "version": "7.21.0-placeholder-for-preset-env.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", - "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.23.3.tgz", + "integrity": "sha512-XaJak1qcityzrX0/IU5nKHb34VaibwP3saKqG6a/tppelgllOH13LUann4ZCIBcVOeE6H18K4Vx9QKkVww3z/w==", "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-plugin-utils": "^7.22.5" + }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@babel/core": "^7.0.0" } }, - "node_modules/@babel/plugin-proposal-unicode-property-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.18.6.tgz", - "integrity": "sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==", + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", "dev": true, - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, "engines": { - "node": ">=4" + "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" @@ -648,9 +680,9 @@ } }, "node_modules/@babel/plugin-syntax-import-assertions": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.22.5.tgz", - "integrity": "sha512-rdV97N7KqsRzeNGoWUOK6yUsWarLjE5Su/Snk9IYPU9CwkWHs4t+rTGOvffTR8XGkJMTAdLfO0xVnXm8wugIJg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.23.3.tgz", + "integrity": "sha512-lPgDSU+SJLK3xmFDTV2ZRQAiM7UuUjGidwBywFavObCiZc1BeAAcMtHJKUya92hPHO+at63JJPLygilZard8jw==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -663,9 +695,9 @@ } }, "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.22.5.tgz", - "integrity": "sha512-KwvoWDeNKPETmozyFE0P2rOLqh39EoQHNjqizrI5B8Vt0ZNS7M56s7dAiAqbYfiAYOuIzIh96z3iR2ktgu3tEg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.23.3.tgz", + "integrity": "sha512-pawnE0P9g10xgoP7yKr6CK63K2FMsTE+FZidZO/1PwRdzmAPVs+HS1mAURUsgaoxammTJvULUdIkEK0gOcU2tA==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -820,9 +852,9 @@ } }, "node_modules/@babel/plugin-transform-arrow-functions": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.22.5.tgz", - "integrity": "sha512-26lTNXoVRdAnsaDXPpvCNUq+OVWEVC6bx7Vvz9rC53F2bagUWW4u4ii2+h8Fejfh7RYqPxn+libeFBBck9muEw==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.23.3.tgz", + "integrity": "sha512-NzQcQrzaQPkaEwoTm4Mhyl8jI1huEL/WWIEvudjTCMJ9aBZNpsJbMASx7EQECtQQPS/DcnFpo0FIh3LvEO9cxQ==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -835,14 +867,14 @@ } }, "node_modules/@babel/plugin-transform-async-generator-functions": { - "version": "7.22.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.22.7.tgz", - "integrity": "sha512-7HmE7pk/Fmke45TODvxvkxRMV9RazV+ZZzhOL9AG8G29TLrr3jkjwF7uJfxZ30EoXpO+LJkq4oA8NjO2DTnEDg==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.23.4.tgz", + "integrity": "sha512-efdkfPhHYTtn0G6n2ddrESE91fgXxjlqLsnUtPWnJs4a4mZIbUaK7ffqKIIUKXSHwcDvaCVX6GXkaJJFqtX7jw==", "dev": true, "dependencies": { - "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-remap-async-to-generator": "^7.22.5", + "@babel/helper-remap-async-to-generator": "^7.22.20", "@babel/plugin-syntax-async-generators": "^7.8.4" }, "engines": { @@ -853,14 +885,14 @@ } }, "node_modules/@babel/plugin-transform-async-to-generator": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.22.5.tgz", - "integrity": "sha512-b1A8D8ZzE/VhNDoV1MSJTnpKkCG5bJo+19R4o4oy03zM7ws8yEMK755j61Dc3EyvdysbqH5BOOTquJ7ZX9C6vQ==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.23.3.tgz", + "integrity": "sha512-A7LFsKi4U4fomjqXJlZg/u0ft/n8/7n7lpffUP/ZULx/DtV9SGlNKZolHH6PE8Xl1ngCc0M11OaeZptXVkfKSw==", "dev": true, "dependencies": { - "@babel/helper-module-imports": "^7.22.5", + "@babel/helper-module-imports": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-remap-async-to-generator": "^7.22.5" + "@babel/helper-remap-async-to-generator": "^7.22.20" }, "engines": { "node": ">=6.9.0" @@ -870,9 +902,9 @@ } }, "node_modules/@babel/plugin-transform-block-scoped-functions": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.22.5.tgz", - "integrity": "sha512-tdXZ2UdknEKQWKJP1KMNmuF5Lx3MymtMN/pvA+p/VEkhK8jVcQ1fzSy8KM9qRYhAf2/lV33hoMPKI/xaI9sADA==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.23.3.tgz", + "integrity": "sha512-vI+0sIaPIO6CNuM9Kk5VmXcMVRiOpDh7w2zZt9GXzmE/9KD70CUEVhvPR/etAeNK/FAEkhxQtXOzVF3EuRL41A==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -885,9 +917,9 @@ } }, "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.22.5.tgz", - "integrity": "sha512-EcACl1i5fSQ6bt+YGuU/XGCeZKStLmyVGytWkpyhCLeQVA0eu6Wtiw92V+I1T/hnezUv7j74dA/Ro69gWcU+hg==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.23.4.tgz", + "integrity": "sha512-0QqbP6B6HOh7/8iNR4CQU2Th/bbRtBp4KS9vcaZd1fZ0wSh5Fyssg0UCIHwxh+ka+pNDREbVLQnHCMHKZfPwfw==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -900,12 +932,12 @@ } }, "node_modules/@babel/plugin-transform-class-properties": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.22.5.tgz", - "integrity": "sha512-nDkQ0NfkOhPTq8YCLiWNxp1+f9fCobEjCb0n8WdbNUBc4IB5V7P1QnX9IjpSoquKrXF5SKojHleVNs2vGeHCHQ==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.23.3.tgz", + "integrity": "sha512-uM+AN8yCIjDPccsKGlw271xjJtGii+xQIF/uMPS8H15L12jZTsLfF4o5vNO7d/oUguOyfdikHGc/yi9ge4SGIg==", "dev": true, "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.22.5", + "@babel/helper-create-class-features-plugin": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { @@ -916,12 +948,12 @@ } }, "node_modules/@babel/plugin-transform-class-static-block": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.22.5.tgz", - "integrity": "sha512-SPToJ5eYZLxlnp1UzdARpOGeC2GbHvr9d/UV0EukuVx8atktg194oe+C5BqQ8jRTkgLRVOPYeXRSBg1IlMoVRA==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.23.4.tgz", + "integrity": "sha512-nsWu/1M+ggti1SOALj3hfx5FXzAY06fwPJsUZD4/A5e1bWi46VUIWtD+kOX6/IdhXGsXBWllLFDSnqSCdUNydQ==", "dev": true, "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.22.5", + "@babel/helper-create-class-features-plugin": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5", "@babel/plugin-syntax-class-static-block": "^7.14.5" }, @@ -933,18 +965,18 @@ } }, "node_modules/@babel/plugin-transform-classes": { - "version": "7.22.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.22.6.tgz", - "integrity": "sha512-58EgM6nuPNG6Py4Z3zSuu0xWu2VfodiMi72Jt5Kj2FECmaYk1RrTXA45z6KBFsu9tRgwQDwIiY4FXTt+YsSFAQ==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.23.5.tgz", + "integrity": "sha512-jvOTR4nicqYC9yzOHIhXG5emiFEOpappSJAl73SDSEDcybD+Puuze8Tnpb9p9qEyYup24tq891gkaygIFvWDqg==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-compilation-targets": "^7.22.6", - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", + "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", "@babel/helper-optimise-call-expression": "^7.22.5", "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.20", "@babel/helper-split-export-declaration": "^7.22.6", "globals": "^11.1.0" }, @@ -956,13 +988,13 @@ } }, "node_modules/@babel/plugin-transform-computed-properties": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.22.5.tgz", - "integrity": "sha512-4GHWBgRf0krxPX+AaPtgBAlTgTeZmqDynokHOX7aqqAB4tHs3U2Y02zH6ETFdLZGcg9UQSD1WCmkVrE9ErHeOg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.23.3.tgz", + "integrity": "sha512-dTj83UVTLw/+nbiHqQSFdwO9CbTtwq1DsDqm3CUEtDrZNET5rT5E6bIdTlOftDTDLMYxvxHNEYO4B9SLl8SLZw==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", - "@babel/template": "^7.22.5" + "@babel/template": "^7.22.15" }, "engines": { "node": ">=6.9.0" @@ -972,9 +1004,9 @@ } }, "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.22.5.tgz", - "integrity": "sha512-GfqcFuGW8vnEqTUBM7UtPd5A4q797LTvvwKxXTgRsFjoqaJiEg9deBG6kWeQYkVEL569NpnmpC0Pkr/8BLKGnQ==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.23.3.tgz", + "integrity": "sha512-n225npDqjDIr967cMScVKHXJs7rout1q+tt50inyBCPkyZ8KxeI6d+GIbSBTT/w/9WdlWDOej3V9HE5Lgk57gw==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -987,12 +1019,12 @@ } }, "node_modules/@babel/plugin-transform-dotall-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.22.5.tgz", - "integrity": "sha512-5/Yk9QxCQCl+sOIB1WelKnVRxTJDSAIxtJLL2/pqL14ZVlbH0fUQUZa/T5/UnQtBNgghR7mfB8ERBKyKPCi7Vw==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.23.3.tgz", + "integrity": "sha512-vgnFYDHAKzFaTVp+mneDsIEbnJ2Np/9ng9iviHw3P/KVcgONxpNULEW/51Z/BaFojG2GI2GwwXck5uV1+1NOYQ==", "dev": true, "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-create-regexp-features-plugin": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { @@ -1003,9 +1035,9 @@ } }, "node_modules/@babel/plugin-transform-duplicate-keys": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.22.5.tgz", - "integrity": "sha512-dEnYD+9BBgld5VBXHnF/DbYGp3fqGMsyxKbtD1mDyIA7AkTSpKXFhCVuj/oQVOoALfBs77DudA0BE4d5mcpmqw==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.23.3.tgz", + "integrity": "sha512-RrqQ+BQmU3Oyav3J+7/myfvRCq7Tbz+kKLLshUmMwNlDHExbGL7ARhajvoBJEvc+fCguPPu887N+3RRXBVKZUA==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -1018,9 +1050,9 @@ } }, "node_modules/@babel/plugin-transform-dynamic-import": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.22.5.tgz", - "integrity": "sha512-0MC3ppTB1AMxd8fXjSrbPa7LT9hrImt+/fcj+Pg5YMD7UQyWp/02+JWpdnCymmsXwIx5Z+sYn1bwCn4ZJNvhqQ==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.23.4.tgz", + "integrity": "sha512-V6jIbLhdJK86MaLh4Jpghi8ho5fGzt3imHOBu/x0jlBaPYqDoWz4RDXjmMOfnh+JWNaQleEAByZLV0QzBT4YQQ==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", @@ -1034,12 +1066,12 @@ } }, "node_modules/@babel/plugin-transform-exponentiation-operator": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.22.5.tgz", - "integrity": "sha512-vIpJFNM/FjZ4rh1myqIya9jXwrwwgFRHPjT3DkUA9ZLHuzox8jiXkOLvwm1H+PQIP3CqfC++WPKeuDi0Sjdj1g==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.23.3.tgz", + "integrity": "sha512-5fhCsl1odX96u7ILKHBj4/Y8vipoqwsJMh4csSA8qFfxrZDEA4Ssku2DyNvMJSmZNOEBT750LfFPbtrnTP90BQ==", "dev": true, "dependencies": { - "@babel/helper-builder-binary-assignment-operator-visitor": "^7.22.5", + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { @@ -1050,9 +1082,9 @@ } }, "node_modules/@babel/plugin-transform-export-namespace-from": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.22.5.tgz", - "integrity": "sha512-X4hhm7FRnPgd4nDA4b/5V280xCx6oL7Oob5+9qVS5C13Zq4bh1qq7LU0GgRU6b5dBWBvhGaXYVB4AcN6+ol6vg==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.23.4.tgz", + "integrity": "sha512-GzuSBcKkx62dGzZI1WVgTWvkkz84FZO5TC5T8dl/Tht/rAla6Dg/Mz9Yhypg+ezVACf/rgDuQt3kbWEv7LdUDQ==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", @@ -1066,9 +1098,9 @@ } }, "node_modules/@babel/plugin-transform-for-of": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.22.5.tgz", - "integrity": "sha512-3kxQjX1dU9uudwSshyLeEipvrLjBCVthCgeTp6CzE/9JYrlAIaeekVxRpCWsDDfYTfRZRoCeZatCQvwo+wvK8A==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.23.3.tgz", + "integrity": "sha512-X8jSm8X1CMwxmK878qsUGJRmbysKNbdpTv/O1/v0LuY/ZkZrng5WYiekYSdg9m09OTmDDUWeEDsTE+17WYbAZw==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -1081,13 +1113,13 @@ } }, "node_modules/@babel/plugin-transform-function-name": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.22.5.tgz", - "integrity": "sha512-UIzQNMS0p0HHiQm3oelztj+ECwFnj+ZRV4KnguvlsD2of1whUeM6o7wGNj6oLwcDoAXQ8gEqfgC24D+VdIcevg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.23.3.tgz", + "integrity": "sha512-I1QXp1LxIvt8yLaib49dRW5Okt7Q4oaxao6tFVKS/anCdEOMtYwWVKoiOA1p34GOWIZjUK0E+zCp7+l1pfQyiw==", "dev": true, "dependencies": { - "@babel/helper-compilation-targets": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", + "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-function-name": "^7.23.0", "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { @@ -1098,9 +1130,9 @@ } }, "node_modules/@babel/plugin-transform-json-strings": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.22.5.tgz", - "integrity": "sha512-DuCRB7fu8MyTLbEQd1ew3R85nx/88yMoqo2uPSjevMj3yoN7CDM8jkgrY0wmVxfJZyJ/B9fE1iq7EQppWQmR5A==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.23.4.tgz", + "integrity": "sha512-81nTOqM1dMwZ/aRXQ59zVubN9wHGqk6UtqRK+/q+ciXmRy8fSolhGVvG09HHRGo4l6fr/c4ZhXUQH0uFW7PZbg==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", @@ -1114,9 +1146,9 @@ } }, "node_modules/@babel/plugin-transform-literals": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.22.5.tgz", - "integrity": "sha512-fTLj4D79M+mepcw3dgFBTIDYpbcB9Sm0bpm4ppXPaO+U+PKFFyV9MGRvS0gvGw62sd10kT5lRMKXAADb9pWy8g==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.23.3.tgz", + "integrity": "sha512-wZ0PIXRxnwZvl9AYpqNUxpZ5BiTGrYt7kueGQ+N5FiQ7RCOD4cm8iShd6S6ggfVIWaJf2EMk8eRzAh52RfP4rQ==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -1129,9 +1161,9 @@ } }, "node_modules/@babel/plugin-transform-logical-assignment-operators": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.22.5.tgz", - "integrity": "sha512-MQQOUW1KL8X0cDWfbwYP+TbVbZm16QmQXJQ+vndPtH/BoO0lOKpVoEDMI7+PskYxH+IiE0tS8xZye0qr1lGzSA==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.23.4.tgz", + "integrity": "sha512-Mc/ALf1rmZTP4JKKEhUwiORU+vcfarFVLfcFiolKUo6sewoxSEgl36ak5t+4WamRsNr6nzjZXQjM35WsU+9vbg==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", @@ -1145,9 +1177,9 @@ } }, "node_modules/@babel/plugin-transform-member-expression-literals": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.22.5.tgz", - "integrity": "sha512-RZEdkNtzzYCFl9SE9ATaUMTj2hqMb4StarOJLrZRbqqU4HSBE7UlBw9WBWQiDzrJZJdUWiMTVDI6Gv/8DPvfew==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.23.3.tgz", + "integrity": "sha512-sC3LdDBDi5x96LA+Ytekz2ZPk8i/Ck+DEuDbRAll5rknJ5XRTSaPKEYwomLcs1AA8wg9b3KjIQRsnApj+q51Ag==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -1160,12 +1192,12 @@ } }, "node_modules/@babel/plugin-transform-modules-amd": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.22.5.tgz", - "integrity": "sha512-R+PTfLTcYEmb1+kK7FNkhQ1gP4KgjpSO6HfH9+f8/yfp2Nt3ggBjiVpRwmwTlfqZLafYKJACy36yDXlEmI9HjQ==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.23.3.tgz", + "integrity": "sha512-vJYQGxeKM4t8hYCKVBlZX/gtIY2I7mRGFNcm85sgXGMTBcoV3QdVtdpbcWEbzbfUIUZKwvgFT82mRvaQIebZzw==", "dev": true, "dependencies": { - "@babel/helper-module-transforms": "^7.22.5", + "@babel/helper-module-transforms": "^7.23.3", "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { @@ -1176,12 +1208,12 @@ } }, "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.22.5.tgz", - "integrity": "sha512-B4pzOXj+ONRmuaQTg05b3y/4DuFz3WcCNAXPLb2Q0GT0TrGKGxNKV4jwsXts+StaM0LQczZbOpj8o1DLPDJIiA==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.23.3.tgz", + "integrity": "sha512-aVS0F65LKsdNOtcz6FRCpE4OgsP2OFnW46qNxNIX9h3wuzaNcSQsJysuMwqSibC98HPrf2vCgtxKNwS0DAlgcA==", "dev": true, "dependencies": { - "@babel/helper-module-transforms": "^7.22.5", + "@babel/helper-module-transforms": "^7.23.3", "@babel/helper-plugin-utils": "^7.22.5", "@babel/helper-simple-access": "^7.22.5" }, @@ -1193,15 +1225,15 @@ } }, "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.22.5.tgz", - "integrity": "sha512-emtEpoaTMsOs6Tzz+nbmcePl6AKVtS1yC4YNAeMun9U8YCsgadPNxnOPQ8GhHFB2qdx+LZu9LgoC0Lthuu05DQ==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.23.3.tgz", + "integrity": "sha512-ZxyKGTkF9xT9YJuKQRo19ewf3pXpopuYQd8cDXqNzc3mUNbOME0RKMoZxviQk74hwzfQsEe66dE92MaZbdHKNQ==", "dev": true, "dependencies": { "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-module-transforms": "^7.22.5", + "@babel/helper-module-transforms": "^7.23.3", "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.5" + "@babel/helper-validator-identifier": "^7.22.20" }, "engines": { "node": ">=6.9.0" @@ -1211,12 +1243,12 @@ } }, "node_modules/@babel/plugin-transform-modules-umd": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.22.5.tgz", - "integrity": "sha512-+S6kzefN/E1vkSsKx8kmQuqeQsvCKCd1fraCM7zXm4SFoggI099Tr4G8U81+5gtMdUeMQ4ipdQffbKLX0/7dBQ==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.23.3.tgz", + "integrity": "sha512-zHsy9iXX2nIsCBFPud3jKn1IRPWg3Ing1qOZgeKV39m1ZgIdpJqvlWVeiHBZC6ITRG0MfskhYe9cLgntfSFPIg==", "dev": true, "dependencies": { - "@babel/helper-module-transforms": "^7.22.5", + "@babel/helper-module-transforms": "^7.23.3", "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { @@ -1243,9 +1275,9 @@ } }, "node_modules/@babel/plugin-transform-new-target": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.22.5.tgz", - "integrity": "sha512-AsF7K0Fx/cNKVyk3a+DW0JLo+Ua598/NxMRvxDnkpCIGFh43+h/v2xyhRUYf6oD8gE4QtL83C7zZVghMjHd+iw==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.23.3.tgz", + "integrity": "sha512-YJ3xKqtJMAT5/TIZnpAR3I+K+WaDowYbN3xyxI8zxx/Gsypwf9B9h0VB+1Nh6ACAAPRS5NSRje0uVv5i79HYGQ==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -1258,9 +1290,9 @@ } }, "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.22.5.tgz", - "integrity": "sha512-6CF8g6z1dNYZ/VXok5uYkkBBICHZPiGEl7oDnAx2Mt1hlHVHOSIKWJaXHjQJA5VB43KZnXZDIexMchY4y2PGdA==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.23.4.tgz", + "integrity": "sha512-jHE9EVVqHKAQx+VePv5LLGHjmHSJR76vawFPTdlxR/LVJPfOEGxREQwQfjuZEOPTwG92X3LINSh3M40Rv4zpVA==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", @@ -1274,9 +1306,9 @@ } }, "node_modules/@babel/plugin-transform-numeric-separator": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.22.5.tgz", - "integrity": "sha512-NbslED1/6M+sXiwwtcAB/nieypGw02Ejf4KtDeMkCEpP6gWFMX1wI9WKYua+4oBneCCEmulOkRpwywypVZzs/g==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.23.4.tgz", + "integrity": "sha512-mps6auzgwjRrwKEZA05cOwuDc9FAzoyFS4ZsG/8F43bTLf/TgkJg7QXOrPO1JO599iA3qgK9MXdMGOEC8O1h6Q==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", @@ -1290,16 +1322,16 @@ } }, "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.22.5.tgz", - "integrity": "sha512-Kk3lyDmEslH9DnvCDA1s1kkd3YWQITiBOHngOtDL9Pt6BZjzqb6hiOlb8VfjiiQJ2unmegBqZu0rx5RxJb5vmQ==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.23.4.tgz", + "integrity": "sha512-9x9K1YyeQVw0iOXJlIzwm8ltobIIv7j2iLyP2jIhEbqPRQ7ScNgwQufU2I0Gq11VjyG4gI4yMXt2VFags+1N3g==", "dev": true, "dependencies": { - "@babel/compat-data": "^7.22.5", - "@babel/helper-compilation-targets": "^7.22.5", + "@babel/compat-data": "^7.23.3", + "@babel/helper-compilation-targets": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5", "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-transform-parameters": "^7.22.5" + "@babel/plugin-transform-parameters": "^7.23.3" }, "engines": { "node": ">=6.9.0" @@ -1309,13 +1341,13 @@ } }, "node_modules/@babel/plugin-transform-object-super": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.22.5.tgz", - "integrity": "sha512-klXqyaT9trSjIUrcsYIfETAzmOEZL3cBYqOYLJxBHfMFFggmXOv+NYSX/Jbs9mzMVESw/WycLFPRx8ba/b2Ipw==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.23.3.tgz", + "integrity": "sha512-BwQ8q0x2JG+3lxCVFohg+KbQM7plfpBwThdW9A6TMtWwLsbDA01Ek2Zb/AgDN39BiZsExm4qrXxjk+P1/fzGrA==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.5" + "@babel/helper-replace-supers": "^7.22.20" }, "engines": { "node": ">=6.9.0" @@ -1325,9 +1357,9 @@ } }, "node_modules/@babel/plugin-transform-optional-catch-binding": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.22.5.tgz", - "integrity": "sha512-pH8orJahy+hzZje5b8e2QIlBWQvGpelS76C63Z+jhZKsmzfNaPQ+LaW6dcJ9bxTpo1mtXbgHwy765Ro3jftmUg==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.23.4.tgz", + "integrity": "sha512-XIq8t0rJPHf6Wvmbn9nFxU6ao4c7WhghTR5WyV8SrJfUFzyxhCm4nhC+iAp3HFhbAKLfYpgzhJ6t4XCtVwqO5A==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", @@ -1341,9 +1373,9 @@ } }, "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.22.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.22.6.tgz", - "integrity": "sha512-Vd5HiWml0mDVtcLHIoEU5sw6HOUW/Zk0acLs/SAeuLzkGNOPc9DB4nkUajemhCmTIz3eiaKREZn2hQQqF79YTg==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.23.4.tgz", + "integrity": "sha512-ZU8y5zWOfjM5vZ+asjgAPwDaBjJzgufjES89Rs4Lpq63O300R/kOz30WCLo6BxxX6QVEilwSlpClnG5cZaikTA==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", @@ -1358,9 +1390,9 @@ } }, "node_modules/@babel/plugin-transform-parameters": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.22.5.tgz", - "integrity": "sha512-AVkFUBurORBREOmHRKo06FjHYgjrabpdqRSwq6+C7R5iTCZOsM4QbcB27St0a4U6fffyAOqh3s/qEfybAhfivg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.23.3.tgz", + "integrity": "sha512-09lMt6UsUb3/34BbECKVbVwrT9bO6lILWln237z7sLaWnMsTi7Yc9fhX5DLpkJzAGfaReXI22wP41SZmnAA3Vw==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -1373,12 +1405,12 @@ } }, "node_modules/@babel/plugin-transform-private-methods": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.22.5.tgz", - "integrity": "sha512-PPjh4gyrQnGe97JTalgRGMuU4icsZFnWkzicB/fUtzlKUqvsWBKEpPPfr5a2JiyirZkHxnAqkQMO5Z5B2kK3fA==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.23.3.tgz", + "integrity": "sha512-UzqRcRtWsDMTLrRWFvUBDwmw06tCQH9Rl1uAjfh6ijMSmGYQ+fpdB+cnqRC8EMh5tuuxSv0/TejGL+7vyj+50g==", "dev": true, "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.22.5", + "@babel/helper-create-class-features-plugin": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { @@ -1389,13 +1421,13 @@ } }, "node_modules/@babel/plugin-transform-private-property-in-object": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.22.5.tgz", - "integrity": "sha512-/9xnaTTJcVoBtSSmrVyhtSvO3kbqS2ODoh2juEU72c3aYonNF0OMGiaz2gjukyKM2wBBYJP38S4JiE0Wfb5VMQ==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.23.4.tgz", + "integrity": "sha512-9G3K1YqTq3F4Vt88Djx1UZ79PDyj+yKRnUy7cZGSMe+a7jkwD259uKKuUzQlPkGam7R+8RJwh5z4xO27fA1o2A==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-create-class-features-plugin": "^7.22.5", + "@babel/helper-create-class-features-plugin": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5", "@babel/plugin-syntax-private-property-in-object": "^7.14.5" }, @@ -1407,9 +1439,9 @@ } }, "node_modules/@babel/plugin-transform-property-literals": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.22.5.tgz", - "integrity": "sha512-TiOArgddK3mK/x1Qwf5hay2pxI6wCZnvQqrFSqbtg1GLl2JcNMitVH/YnqjP+M31pLUeTfzY1HAXFDnUBV30rQ==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.23.3.tgz", + "integrity": "sha512-jR3Jn3y7cZp4oEWPFAlRsSWjxKe4PZILGBSd4nis1TsC5qeSpb+nrtihJuDhNI7QHiVbUaiXa0X2RZY3/TI6Nw==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -1422,13 +1454,13 @@ } }, "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.22.5.tgz", - "integrity": "sha512-rR7KePOE7gfEtNTh9Qw+iO3Q/e4DEsoQ+hdvM6QUDH7JRJ5qxq5AA52ZzBWbI5i9lfNuvySgOGP8ZN7LAmaiPw==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.23.3.tgz", + "integrity": "sha512-KP+75h0KghBMcVpuKisx3XTu9Ncut8Q8TuvGO4IhY+9D5DFEckQefOuIsB/gQ2tG71lCke4NMrtIPS8pOj18BQ==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", - "regenerator-transform": "^0.15.1" + "regenerator-transform": "^0.15.2" }, "engines": { "node": ">=6.9.0" @@ -1438,9 +1470,9 @@ } }, "node_modules/@babel/plugin-transform-reserved-words": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.22.5.tgz", - "integrity": "sha512-DTtGKFRQUDm8svigJzZHzb/2xatPc6TzNvAIJ5GqOKDsGFYgAskjRulbR/vGsPKq3OPqtexnz327qYpP57RFyA==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.23.3.tgz", + "integrity": "sha512-QnNTazY54YqgGxwIexMZva9gqbPa15t/x9VS+0fsEFWplwVpXYZivtgl43Z1vMpc1bdPP2PP8siFeVcnFvA3Cg==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -1453,9 +1485,9 @@ } }, "node_modules/@babel/plugin-transform-shorthand-properties": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.22.5.tgz", - "integrity": "sha512-vM4fq9IXHscXVKzDv5itkO1X52SmdFBFcMIBZ2FRn2nqVYqw6dBexUgMvAjHW+KXpPPViD/Yo3GrDEBaRC0QYA==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.23.3.tgz", + "integrity": "sha512-ED2fgqZLmexWiN+YNFX26fx4gh5qHDhn1O2gvEhreLW2iI63Sqm4llRLCXALKrCnbN4Jy0VcMQZl/SAzqug/jg==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -1468,9 +1500,9 @@ } }, "node_modules/@babel/plugin-transform-spread": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.22.5.tgz", - "integrity": "sha512-5ZzDQIGyvN4w8+dMmpohL6MBo+l2G7tfC/O2Dg7/hjpgeWvUx8FzfeOKxGog9IimPa4YekaQ9PlDqTLOljkcxg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.23.3.tgz", + "integrity": "sha512-VvfVYlrlBVu+77xVTOAoxQ6mZbnIq5FM0aGBSFEcIh03qHf+zNqA4DC/3XMUozTg7bZV3e3mZQ0i13VB6v5yUg==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", @@ -1484,9 +1516,9 @@ } }, "node_modules/@babel/plugin-transform-sticky-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.22.5.tgz", - "integrity": "sha512-zf7LuNpHG0iEeiyCNwX4j3gDg1jgt1k3ZdXBKbZSoA3BbGQGvMiSvfbZRR3Dr3aeJe3ooWFZxOOG3IRStYp2Bw==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.23.3.tgz", + "integrity": "sha512-HZOyN9g+rtvnOU3Yh7kSxXrKbzgrm5X4GncPY1QOquu7epga5MxKHVpYu2hvQnry/H+JjckSYRb93iNfsioAGg==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -1499,9 +1531,9 @@ } }, "node_modules/@babel/plugin-transform-template-literals": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.22.5.tgz", - "integrity": "sha512-5ciOehRNf+EyUeewo8NkbQiUs4d6ZxiHo6BcBcnFlgiJfu16q0bQUw9Jvo0b0gBKFG1SMhDSjeKXSYuJLeFSMA==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.23.3.tgz", + "integrity": "sha512-Flok06AYNp7GV2oJPZZcP9vZdszev6vPBkHLwxwSpaIqx75wn6mUd3UFWsSsA0l8nXAKkyCmL/sR02m8RYGeHg==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -1514,9 +1546,9 @@ } }, "node_modules/@babel/plugin-transform-typeof-symbol": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.22.5.tgz", - "integrity": "sha512-bYkI5lMzL4kPii4HHEEChkD0rkc+nvnlR6+o/qdqR6zrm0Sv/nodmyLhlq2DO0YKLUNd2VePmPRjJXSBh9OIdA==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.23.3.tgz", + "integrity": "sha512-4t15ViVnaFdrPC74be1gXBSMzXk3B4Us9lP7uLRQHTFpV5Dvt33pn+2MyyNxmN3VTTm3oTrZVMUmuw3oBnQ2oQ==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -1529,9 +1561,9 @@ } }, "node_modules/@babel/plugin-transform-unicode-escapes": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.22.5.tgz", - "integrity": "sha512-biEmVg1IYB/raUO5wT1tgfacCef15Fbzhkx493D3urBI++6hpJ+RFG4SrWMn0NEZLfvilqKf3QDrRVZHo08FYg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.23.3.tgz", + "integrity": "sha512-OMCUx/bU6ChE3r4+ZdylEqAjaQgHAgipgW8nsCfu5pGqDcFytVd91AwRvUJSBZDz0exPGgnjoqhgRYLRjFZc9Q==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -1544,12 +1576,12 @@ } }, "node_modules/@babel/plugin-transform-unicode-property-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.22.5.tgz", - "integrity": "sha512-HCCIb+CbJIAE6sXn5CjFQXMwkCClcOfPCzTlilJ8cUatfzwHlWQkbtV0zD338u9dZskwvuOYTuuaMaA8J5EI5A==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.23.3.tgz", + "integrity": "sha512-KcLIm+pDZkWZQAFJ9pdfmh89EwVfmNovFBcXko8szpBeF8z68kWIPeKlmSOkT9BXJxs2C0uk+5LxoxIv62MROA==", "dev": true, "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-create-regexp-features-plugin": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { @@ -1560,12 +1592,12 @@ } }, "node_modules/@babel/plugin-transform-unicode-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.22.5.tgz", - "integrity": "sha512-028laaOKptN5vHJf9/Arr/HiJekMd41hOEZYvNsrsXqJ7YPYuX2bQxh31fkZzGmq3YqHRJzYFFAVYvKfMPKqyg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.23.3.tgz", + "integrity": "sha512-wMHpNA4x2cIA32b/ci3AfwNgheiva2W0WUKWTK7vBHBhDKfPsc5cFGNWm69WBqpwd86u1qwZ9PWevKqm1A3yAw==", "dev": true, "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-create-regexp-features-plugin": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { @@ -1576,12 +1608,12 @@ } }, "node_modules/@babel/plugin-transform-unicode-sets-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.22.5.tgz", - "integrity": "sha512-lhMfi4FC15j13eKrh3DnYHjpGj6UKQHtNKTbtc1igvAhRy4+kLhV07OpLcsN0VgDEw/MjAvJO4BdMJsHwMhzCg==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.23.3.tgz", + "integrity": "sha512-W7lliA/v9bNR83Qc3q1ip9CQMZ09CcHDbHfbLRDNuAhn1Mvkr1ZNF7hPmztMQvtTGVLJ9m8IZqWsTkXOml8dbw==", "dev": true, "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-create-regexp-features-plugin": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { @@ -1592,25 +1624,26 @@ } }, "node_modules/@babel/preset-env": { - "version": "7.22.7", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.22.7.tgz", - "integrity": "sha512-1whfDtW+CzhETuzYXfcgZAh8/GFMeEbz0V5dVgya8YeJyCU6Y/P2Gnx4Qb3MylK68Zu9UiwUvbPMPTpFAOJ+sQ==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.23.5.tgz", + "integrity": "sha512-0d/uxVD6tFGWXGDSfyMD1p2otoaKmu6+GD+NfAx0tMaH+dxORnp7T9TaVQ6mKyya7iBtCIVxHjWT7MuzzM9z+A==", "dev": true, "dependencies": { - "@babel/compat-data": "^7.22.6", - "@babel/helper-compilation-targets": "^7.22.6", + "@babel/compat-data": "^7.23.5", + "@babel/helper-compilation-targets": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-validator-option": "^7.22.5", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.22.5", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.22.5", + "@babel/helper-validator-option": "^7.23.5", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.23.3", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.23.3", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.23.3", "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", "@babel/plugin-syntax-async-generators": "^7.8.4", "@babel/plugin-syntax-class-properties": "^7.12.13", "@babel/plugin-syntax-class-static-block": "^7.14.5", "@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-syntax-export-namespace-from": "^7.8.3", - "@babel/plugin-syntax-import-assertions": "^7.22.5", - "@babel/plugin-syntax-import-attributes": "^7.22.5", + "@babel/plugin-syntax-import-assertions": "^7.23.3", + "@babel/plugin-syntax-import-attributes": "^7.23.3", "@babel/plugin-syntax-import-meta": "^7.10.4", "@babel/plugin-syntax-json-strings": "^7.8.3", "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", @@ -1622,61 +1655,60 @@ "@babel/plugin-syntax-private-property-in-object": "^7.14.5", "@babel/plugin-syntax-top-level-await": "^7.14.5", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", - "@babel/plugin-transform-arrow-functions": "^7.22.5", - "@babel/plugin-transform-async-generator-functions": "^7.22.7", - "@babel/plugin-transform-async-to-generator": "^7.22.5", - "@babel/plugin-transform-block-scoped-functions": "^7.22.5", - "@babel/plugin-transform-block-scoping": "^7.22.5", - "@babel/plugin-transform-class-properties": "^7.22.5", - "@babel/plugin-transform-class-static-block": "^7.22.5", - "@babel/plugin-transform-classes": "^7.22.6", - "@babel/plugin-transform-computed-properties": "^7.22.5", - "@babel/plugin-transform-destructuring": "^7.22.5", - "@babel/plugin-transform-dotall-regex": "^7.22.5", - "@babel/plugin-transform-duplicate-keys": "^7.22.5", - "@babel/plugin-transform-dynamic-import": "^7.22.5", - "@babel/plugin-transform-exponentiation-operator": "^7.22.5", - "@babel/plugin-transform-export-namespace-from": "^7.22.5", - "@babel/plugin-transform-for-of": "^7.22.5", - "@babel/plugin-transform-function-name": "^7.22.5", - "@babel/plugin-transform-json-strings": "^7.22.5", - "@babel/plugin-transform-literals": "^7.22.5", - "@babel/plugin-transform-logical-assignment-operators": "^7.22.5", - "@babel/plugin-transform-member-expression-literals": "^7.22.5", - "@babel/plugin-transform-modules-amd": "^7.22.5", - "@babel/plugin-transform-modules-commonjs": "^7.22.5", - "@babel/plugin-transform-modules-systemjs": "^7.22.5", - "@babel/plugin-transform-modules-umd": "^7.22.5", + "@babel/plugin-transform-arrow-functions": "^7.23.3", + "@babel/plugin-transform-async-generator-functions": "^7.23.4", + "@babel/plugin-transform-async-to-generator": "^7.23.3", + "@babel/plugin-transform-block-scoped-functions": "^7.23.3", + "@babel/plugin-transform-block-scoping": "^7.23.4", + "@babel/plugin-transform-class-properties": "^7.23.3", + "@babel/plugin-transform-class-static-block": "^7.23.4", + "@babel/plugin-transform-classes": "^7.23.5", + "@babel/plugin-transform-computed-properties": "^7.23.3", + "@babel/plugin-transform-destructuring": "^7.23.3", + "@babel/plugin-transform-dotall-regex": "^7.23.3", + "@babel/plugin-transform-duplicate-keys": "^7.23.3", + "@babel/plugin-transform-dynamic-import": "^7.23.4", + "@babel/plugin-transform-exponentiation-operator": "^7.23.3", + "@babel/plugin-transform-export-namespace-from": "^7.23.4", + "@babel/plugin-transform-for-of": "^7.23.3", + "@babel/plugin-transform-function-name": "^7.23.3", + "@babel/plugin-transform-json-strings": "^7.23.4", + "@babel/plugin-transform-literals": "^7.23.3", + "@babel/plugin-transform-logical-assignment-operators": "^7.23.4", + "@babel/plugin-transform-member-expression-literals": "^7.23.3", + "@babel/plugin-transform-modules-amd": "^7.23.3", + "@babel/plugin-transform-modules-commonjs": "^7.23.3", + "@babel/plugin-transform-modules-systemjs": "^7.23.3", + "@babel/plugin-transform-modules-umd": "^7.23.3", "@babel/plugin-transform-named-capturing-groups-regex": "^7.22.5", - "@babel/plugin-transform-new-target": "^7.22.5", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.22.5", - "@babel/plugin-transform-numeric-separator": "^7.22.5", - "@babel/plugin-transform-object-rest-spread": "^7.22.5", - "@babel/plugin-transform-object-super": "^7.22.5", - "@babel/plugin-transform-optional-catch-binding": "^7.22.5", - "@babel/plugin-transform-optional-chaining": "^7.22.6", - "@babel/plugin-transform-parameters": "^7.22.5", - "@babel/plugin-transform-private-methods": "^7.22.5", - "@babel/plugin-transform-private-property-in-object": "^7.22.5", - "@babel/plugin-transform-property-literals": "^7.22.5", - "@babel/plugin-transform-regenerator": "^7.22.5", - "@babel/plugin-transform-reserved-words": "^7.22.5", - "@babel/plugin-transform-shorthand-properties": "^7.22.5", - "@babel/plugin-transform-spread": "^7.22.5", - "@babel/plugin-transform-sticky-regex": "^7.22.5", - "@babel/plugin-transform-template-literals": "^7.22.5", - "@babel/plugin-transform-typeof-symbol": "^7.22.5", - "@babel/plugin-transform-unicode-escapes": "^7.22.5", - "@babel/plugin-transform-unicode-property-regex": "^7.22.5", - "@babel/plugin-transform-unicode-regex": "^7.22.5", - "@babel/plugin-transform-unicode-sets-regex": "^7.22.5", - "@babel/preset-modules": "^0.1.5", - "@babel/types": "^7.22.5", - "@nicolo-ribaudo/semver-v6": "^6.3.3", - "babel-plugin-polyfill-corejs2": "^0.4.4", - "babel-plugin-polyfill-corejs3": "^0.8.2", - "babel-plugin-polyfill-regenerator": "^0.5.1", - "core-js-compat": "^3.31.0" + "@babel/plugin-transform-new-target": "^7.23.3", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.23.4", + "@babel/plugin-transform-numeric-separator": "^7.23.4", + "@babel/plugin-transform-object-rest-spread": "^7.23.4", + "@babel/plugin-transform-object-super": "^7.23.3", + "@babel/plugin-transform-optional-catch-binding": "^7.23.4", + "@babel/plugin-transform-optional-chaining": "^7.23.4", + "@babel/plugin-transform-parameters": "^7.23.3", + "@babel/plugin-transform-private-methods": "^7.23.3", + "@babel/plugin-transform-private-property-in-object": "^7.23.4", + "@babel/plugin-transform-property-literals": "^7.23.3", + "@babel/plugin-transform-regenerator": "^7.23.3", + "@babel/plugin-transform-reserved-words": "^7.23.3", + "@babel/plugin-transform-shorthand-properties": "^7.23.3", + "@babel/plugin-transform-spread": "^7.23.3", + "@babel/plugin-transform-sticky-regex": "^7.23.3", + "@babel/plugin-transform-template-literals": "^7.23.3", + "@babel/plugin-transform-typeof-symbol": "^7.23.3", + "@babel/plugin-transform-unicode-escapes": "^7.23.3", + "@babel/plugin-transform-unicode-property-regex": "^7.23.3", + "@babel/plugin-transform-unicode-regex": "^7.23.3", + "@babel/plugin-transform-unicode-sets-regex": "^7.23.3", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "babel-plugin-polyfill-corejs2": "^0.4.6", + "babel-plugin-polyfill-corejs3": "^0.8.5", + "babel-plugin-polyfill-regenerator": "^0.5.3", + "core-js-compat": "^3.31.0", + "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" @@ -1685,20 +1717,27 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/preset-env/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/@babel/preset-modules": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.5.tgz", - "integrity": "sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA==", + "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", - "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", - "@babel/plugin-transform-dotall-regex": "^7.4.4", "@babel/types": "^7.4.4", "esutils": "^2.0.2" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" } }, "node_modules/@babel/regjsgen": { @@ -1708,12 +1747,12 @@ "dev": true }, "node_modules/@babel/runtime": { - "version": "7.22.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.6.tgz", - "integrity": "sha512-wDb5pWm4WDdF6LFUde3Jl8WzPA+3ZbxYqkC6xAXuD3irdEHN1k0NfTRrJD8ZD378SJ61miMLCqIOXYhd8x+AJQ==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.5.tgz", + "integrity": "sha512-NdUTHcPe4C99WxPub+K9l9tK5/lV4UXIoaHSYgzco9BCyjKAAwzdBI+wWtYqHt7LJdbo74ZjRPJgzVweq1sz0w==", "dev": true, "dependencies": { - "regenerator-runtime": "^0.13.11" + "regenerator-runtime": "^0.14.0" }, "engines": { "node": ">=6.9.0" @@ -1734,19 +1773,19 @@ } }, "node_modules/@babel/traverse": { - "version": "7.23.2", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", - "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.5.tgz", + "integrity": "sha512-czx7Xy5a6sapWWRx61m1Ke1Ra4vczu1mCTtJam5zRTBOonfdJ+S/B6HYmGYu3fJtr8GGET3si6IhgWVBhJ/m8w==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.22.13", - "@babel/generator": "^7.23.0", + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.5", "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-function-name": "^7.23.0", "@babel/helper-hoist-variables": "^7.22.5", "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.23.0", - "@babel/types": "^7.23.0", + "@babel/parser": "^7.23.5", + "@babel/types": "^7.23.5", "debug": "^4.1.0", "globals": "^11.1.0" }, @@ -1755,12 +1794,12 @@ } }, "node_modules/@babel/types": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", - "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.5.tgz", + "integrity": "sha512-ON5kSOJwVO6xXVRTvOI0eOnWe7VdUcIpsovGo9U/Br4Ie4UVFQTboO2cYnDhAGU6Fp+UxSiT+pMft0SMHfuq6w==", "dev": true, "dependencies": { - "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-string-parser": "^7.23.4", "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" }, @@ -1864,18 +1903,18 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.5.1.tgz", - "integrity": "sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ==", + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", + "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", "dev": true, "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, "node_modules/@eslint/eslintrc": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.0.tgz", - "integrity": "sha512-Lj7DECXqIVCqnqjjHMPna4vn6GJcMgul/wuS0je9OZ9gsL0zzDpKPVtcG1HaDVc+9y+qgXneTeUMbCqXJNpH1A==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", "dev": true, "dependencies": { "ajv": "^6.12.4", @@ -1896,9 +1935,9 @@ } }, "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "13.20.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", - "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", + "version": "13.23.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", + "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", "dev": true, "dependencies": { "type-fest": "^0.20.2" @@ -1923,21 +1962,21 @@ } }, "node_modules/@eslint/js": { - "version": "8.44.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.44.0.tgz", - "integrity": "sha512-Ag+9YM4ocKQx9AarydN0KY2j0ErMHNIocPDrVo8zAE44xLTjEtz81OdR68/cydGtk6m6jDb5Za3r2useMzYmSw==", + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.55.0.tgz", + "integrity": "sha512-qQfo2mxH5yVom1kacMtZZJFVdW+E70mqHMJvVg6WTLo+VBuQJ4TojZlfWBjK0ve5BdEeNAVxOsl/nvNMpJOaJA==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, "node_modules/@humanwhocodes/config-array": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz", - "integrity": "sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==", + "version": "0.11.13", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", + "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==", "dev": true, "dependencies": { - "@humanwhocodes/object-schema": "^1.2.1", + "@humanwhocodes/object-schema": "^2.0.1", "debug": "^4.1.1", "minimatch": "^3.0.5" }, @@ -1959,9 +1998,9 @@ } }, "node_modules/@humanwhocodes/object-schema": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", - "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz", + "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", "dev": true }, "node_modules/@jridgewell/gen-mapping": { @@ -2025,15 +2064,6 @@ "dev": true, "optional": true }, - "node_modules/@nicolo-ribaudo/semver-v6": { - "version": "6.3.3", - "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/semver-v6/-/semver-v6-6.3.3.tgz", - "integrity": "sha512-3Yc1fUTs69MG/uZbJlLSI3JISMn2UV2rg+1D/vROUqZyh3l6iYHCs7GMp+M40ZD7yOdDbYjJcU1oTJhrc+dGKg==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -2070,9 +2100,9 @@ } }, "node_modules/@tailwindcss/forms": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.3.tgz", - "integrity": "sha512-y5mb86JUoiUgBjY/o6FJSFZSEttfb3Q5gllE4xoKjAAD+vBrnIhE4dViwUuow3va8mpH4s9jyUbUbrRGoRdc2Q==", + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.7.tgz", + "integrity": "sha512-QE7X69iQI+ZXwldE+rzasvbJiyV/ju1FGHH0Qn2W3FKbuYtqp8LKcy6iSw79fVUT5/Vvf+0XgLCeYVG+UV6hOw==", "dev": true, "dependencies": { "mini-svg-data-uri": "^1.2.3" @@ -2082,9 +2112,9 @@ } }, "node_modules/@tailwindcss/typography": { - "version": "0.5.9", - "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.9.tgz", - "integrity": "sha512-t8Sg3DyynFysV9f4JDOVISGsjazNb48AeIYQwcL+Bsq5uf4RYL75C1giZ43KISjeDGBaTN3Kxh7Xj/vRSMJUUg==", + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.10.tgz", + "integrity": "sha512-Pe8BuPJQJd3FfRnm6H0ulKIGoMEQS+Vq01R6M5aCrFB/ccR/shT+0kXLjouGC1gFLm9hopTFN+DMP0pfwRWzPw==", "dev": true, "dependencies": { "lodash.castarray": "^4.4.0", @@ -2121,6 +2151,12 @@ "integrity": "sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==", "dev": true }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, "node_modules/@vue/reactivity": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.1.5.tgz", @@ -2143,9 +2179,9 @@ "dev": true }, "node_modules/acorn": { - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", - "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", + "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -2180,9 +2216,9 @@ } }, "node_modules/alpinejs": { - "version": "3.12.3", - "resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.12.3.tgz", - "integrity": "sha512-fLz2dfYQ3xCk7Ip8LiIpV2W+9brUyex2TAE7Z0BCvZdUDklJE+n+a8gCgLWzfZ0GzZNZu7HUP8Z0z6Xbm6fsSA==", + "version": "3.13.3", + "resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.13.3.tgz", + "integrity": "sha512-WZ6WQjkAOl+WdW/jukzNHq9zHFDNKmkk/x6WF7WdyNDD6woinrfXCVsZXm0galjbco+pEpYmJLtwlZwcOfIVdg==", "dev": true, "dependencies": { "@vue/reactivity": "~3.1.1" @@ -2268,42 +2304,51 @@ } }, "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.4.tgz", - "integrity": "sha512-9WeK9snM1BfxB38goUEv2FLnA6ja07UMfazFHzCXUb3NyDZAwfXvQiURQ6guTTMeHcOsdknULm1PDhs4uWtKyA==", + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.6.tgz", + "integrity": "sha512-jhHiWVZIlnPbEUKSSNb9YoWcQGdlTLq7z1GHL4AjFxaoOUMuuEVJ+Y4pAaQUGOGk93YsVCKPbqbfw3m0SM6H8Q==", "dev": true, "dependencies": { "@babel/compat-data": "^7.22.6", - "@babel/helper-define-polyfill-provider": "^0.4.1", - "@nicolo-ribaudo/semver-v6": "^6.3.3" + "@babel/helper-define-polyfill-provider": "^0.4.3", + "semver": "^6.3.1" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" } }, "node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.2.tgz", - "integrity": "sha512-Cid+Jv1BrY9ReW9lIfNlNpsI53N+FN7gE+f73zLAUbr9C52W4gKLWSByx47pfDJsEysojKArqOtOKZSVIIUTuQ==", + "version": "0.8.6", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.6.tgz", + "integrity": "sha512-leDIc4l4tUgU7str5BWLS2h8q2N4Nf6lGZP6UrNDxdtfF2g69eJ5L0H7S8A5Ln/arfFAfHor5InAdZuIOwZdgQ==", "dev": true, "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.4.1", - "core-js-compat": "^3.31.0" + "@babel/helper-define-polyfill-provider": "^0.4.3", + "core-js-compat": "^3.33.1" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.1.tgz", - "integrity": "sha512-L8OyySuI6OSQ5hFy9O+7zFjyr4WhAfRjLIOkhQGYl+emwJkd/S4XXT1JpfrgR1jrQ1NcGiOh+yAdGlF8pnC3Jw==", + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.3.tgz", + "integrity": "sha512-8sHeDOmXC8csczMrYEOf0UTNa4yE2SxV5JGeT/LP1n0OYVDUUFPxG9vdk2AlDlIit4t+Kf0xCtpgXPBwnn/9pw==", "dev": true, "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.4.1" + "@babel/helper-define-polyfill-provider": "^0.4.3" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "node_modules/balanced-match": { @@ -2344,9 +2389,9 @@ } }, "node_modules/browserslist": { - "version": "4.21.9", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.9.tgz", - "integrity": "sha512-M0MFoZzbUrRU4KNfCrDLnvyE7gub+peetoTid3TBIqtunaDJyXlwhakT+/VkvSXcfIzFfK/nkCs4nmyTmxdNSg==", + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.2.tgz", + "integrity": "sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A==", "dev": true, "funding": [ { @@ -2363,10 +2408,10 @@ } ], "dependencies": { - "caniuse-lite": "^1.0.30001503", - "electron-to-chromium": "^1.4.431", - "node-releases": "^2.0.12", - "update-browserslist-db": "^1.0.11" + "caniuse-lite": "^1.0.30001565", + "electron-to-chromium": "^1.4.601", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.13" }, "bin": { "browserslist": "cli.js" @@ -2436,9 +2481,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001515", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001515.tgz", - "integrity": "sha512-eEFDwUOZbE24sb+Ecsx3+OvNETqjWIdabMy52oOkIgcUtAsQifjUG9q4U9dgTHJM2mfk4uEPxc0+xuFdJ629QA==", + "version": "1.0.30001566", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001566.tgz", + "integrity": "sha512-ggIhCsTxmITBAMmK8yZjEhCO5/47jKXPu6Dha/wuCS4JePVL+3uiDEBuhu2aIoT+bqTOR8L76Ip1ARL9xYsEJA==", "dev": true, "funding": [ { @@ -2533,15 +2578,15 @@ "dev": true }, "node_modules/convert-source-map": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true }, "node_modules/core-js": { - "version": "3.31.1", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.31.1.tgz", - "integrity": "sha512-2sKLtfq1eFST7l7v62zaqXacPc7uG8ZAya8ogijLhTtaKNcpzpB4TMoTw2Si+8GYKRwFPMMtUT0263QFWFfqyQ==", + "version": "3.34.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.34.0.tgz", + "integrity": "sha512-aDdvlDder8QmY91H88GzNi9EtQi2TjvQhpCX6B1v/dAZHU1AuLgHvRh54RiOerpEhEW46Tkf+vgAViB/CWC0ag==", "dev": true, "hasInstallScript": true, "funding": { @@ -2550,12 +2595,12 @@ } }, "node_modules/core-js-compat": { - "version": "3.31.1", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.31.1.tgz", - "integrity": "sha512-wIDWd2s5/5aJSdpOJHfSibxNODxoGoWOBHt8JSPB41NOE94M7kuTPZCYLOlTtuoXTsBPKobpJ6T+y0SSy5L9SA==", + "version": "3.34.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.34.0.tgz", + "integrity": "sha512-4ZIyeNbW/Cn1wkMMDy+mvrRUxrwFNjKwbhCfQpDd+eLgYipDqp8oGFGtLmhh18EDPKA0g3VUBYOxQGGwvWLVpA==", "dev": true, "dependencies": { - "browserslist": "^4.21.9" + "browserslist": "^4.22.2" }, "funding": { "type": "opencollective", @@ -2830,9 +2875,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.455", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.455.tgz", - "integrity": "sha512-8tgdX0Odl24LtmLwxotpJCVjIndN559AvaOtd67u+2mo+IDsgsTF580NB+uuDCqsHw8yFg53l5+imFV9Fw3cbA==", + "version": "1.4.609", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.609.tgz", + "integrity": "sha512-ihiCP7PJmjoGNuLpl7TjNA8pCQWu09vGyjlPYw1Rqww4gvNuCcmvl+44G+2QyJ6S2K4o+wbTS++Xz0YN8Q9ERw==", "dev": true }, "node_modules/emoji-regex": { @@ -2875,27 +2920,28 @@ } }, "node_modules/eslint": { - "version": "8.44.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.44.0.tgz", - "integrity": "sha512-0wpHoUbDUHgNCyvFB5aXLiQVfK9B0at6gUvzy83k4kAsQ/u769TQDX6iKC+aO4upIHO9WSaA3QoXYQDHbNwf1A==", + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.55.0.tgz", + "integrity": "sha512-iyUUAM0PCKj5QpwGfmCAG9XXbZCWsqP/eWAWrG/W0umvjuLRBECwSFdt+rCntju0xEH7teIABPwXpahftIaTdA==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.4.0", - "@eslint/eslintrc": "^2.1.0", - "@eslint/js": "8.44.0", - "@humanwhocodes/config-array": "^0.11.10", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.55.0", + "@humanwhocodes/config-array": "^0.11.13", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", - "ajv": "^6.10.0", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.0", - "eslint-visitor-keys": "^3.4.1", - "espree": "^9.6.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -2905,7 +2951,6 @@ "globals": "^13.19.0", "graphemer": "^1.4.0", "ignore": "^5.2.0", - "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", @@ -2917,7 +2962,6 @@ "natural-compare": "^1.4.0", "optionator": "^0.9.3", "strip-ansi": "^6.0.1", - "strip-json-comments": "^3.1.0", "text-table": "^0.2.0" }, "bin": { @@ -2931,9 +2975,9 @@ } }, "node_modules/eslint-config-prettier": { - "version": "8.8.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.8.0.tgz", - "integrity": "sha512-wLbQiFre3tdGgpDv67NQKnJuTlcUVYHas3k+DZCc2U2BadthoEY4B7hLPvAxaqdyOGCzuLfii2fqGph10va7oA==", + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.10.0.tgz", + "integrity": "sha512-SM8AMJdeQqRYT9O9zguiruQZaN7+z+E4eAP9oiLNGKMtomwaB1E9dcgUD6ZAn/eQAb52USbvezbiljfZUhbJcg==", "dev": true, "bin": { "eslint-config-prettier": "bin/cli.js" @@ -2952,9 +2996,9 @@ } }, "node_modules/eslint-scope": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.0.tgz", - "integrity": "sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==", + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", "dev": true, "dependencies": { "esrecurse": "^4.3.0", @@ -2968,9 +3012,9 @@ } }, "node_modules/eslint-visitor-keys": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz", - "integrity": "sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -3101,9 +3145,9 @@ } }, "node_modules/espree": { - "version": "9.6.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.0.tgz", - "integrity": "sha512-1FH/IiruXZ84tpUlm0aCUEwMl2Ho5ilqVh0VvQXw+byAz/4SAciyHLlfmL5WYqsvD38oymdUwBss0LtK8m4s/A==", + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", "dev": true, "dependencies": { "acorn": "^8.9.0", @@ -3518,9 +3562,9 @@ } }, "node_modules/htmx.org": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/htmx.org/-/htmx.org-1.9.2.tgz", - "integrity": "sha512-ZGbucKcalQyXdGUl+4Zt3xdRDPmNy70yNhMyDG1eDYUm/ImxmSo2rhIBDa53XitrAVhA+/CGgry+wJ1SO77wrA==", + "version": "1.9.9", + "resolved": "https://registry.npmjs.org/htmx.org/-/htmx.org-1.9.9.tgz", + "integrity": "sha512-PDEZU1me7UGLzQk98LyfLvwFgdtn9mrCVMmAxv1/UjshUnxsc+rouu+Ot2QfFZxsY4mBCoOed5nK7m9Nj2Tu7g==", "dev": true }, "node_modules/ignore": { @@ -4157,19 +4201,19 @@ "dev": true }, "node_modules/node-releases": { - "version": "2.0.13", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", - "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==", + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", "dev": true }, "node_modules/nodemon": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.0.1.tgz", - "integrity": "sha512-g9AZ7HmkhQkqXkRc20w+ZfQ73cHLbE8hnPbtaFbFtCumZsjyMhKk9LajQ07U5Ux28lvFjZ5X7HvWR1xzU8jHVw==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.0.2.tgz", + "integrity": "sha512-9qIN2LNTrEzpOPBaWHTm4Asy1LxXLSickZStAQ4IZe7zsoIpD/A7LWxhZV3t4Zu352uBcqVnRsDXSMR2Sc3lTA==", "dev": true, "dependencies": { "chokidar": "^3.5.2", - "debug": "^3.2.7", + "debug": "^4", "ignore-by-default": "^1.0.1", "minimatch": "^3.1.2", "pstree.remy": "^1.1.8", @@ -4190,15 +4234,6 @@ "url": "https://opencollective.com/nodemon" } }, - "node_modules/nodemon/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "dependencies": { - "ms": "^2.1.1" - } - }, "node_modules/nodemon/node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -4305,9 +4340,9 @@ } }, "node_modules/npm-run-all2": { - "version": "6.0.6", - "resolved": "https://registry.npmjs.org/npm-run-all2/-/npm-run-all2-6.0.6.tgz", - "integrity": "sha512-Ba31DnJj3aqJ5freRdVIoBuRdGjHDt0Sfc7tduR2wYDbtcxsFlga6Sw2pE5Tn3+kdVttVwqzFlmozcT540wDxw==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/npm-run-all2/-/npm-run-all2-6.1.1.tgz", + "integrity": "sha512-lWLbkPZ5BSdXtN8lR+0rc8caKoPdymycpZksyDEC9MOBvfdwTXZ0uVhb7bMcGeXv2/BKtfQuo6Zn3zfc8rxNXA==", "dev": true, "dependencies": { "ansi-styles": "^6.2.1", @@ -4320,6 +4355,7 @@ }, "bin": { "npm-run-all": "bin/npm-run-all/index.js", + "npm-run-all2": "bin/npm-run-all/index.js", "run-p": "bin/run-p/index.js", "run-s": "bin/run-s/index.js" }, @@ -4750,9 +4786,9 @@ } }, "node_modules/prettier": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.0.tgz", - "integrity": "sha512-zBf5eHpwHOGPC47h0zrPyNn+eAEIdEzfywMoYn2XPi0P44Zp0tSq64rq0xAREh4auw2cJZHo9QUob+NqCQky4g==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.0.tgz", + "integrity": "sha512-TQLvXjq5IAibjh8EpBIkNKxO749UEWABoiIZehEPiY4GNpVdhaFKqSTu+QrlU6D2dPAfubRmtJTi4K4YkQ5eXw==", "dev": true, "bin": { "prettier": "bin/prettier.cjs" @@ -5045,15 +5081,15 @@ } }, "node_modules/regenerator-runtime": { - "version": "0.13.11", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", - "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", + "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==", "dev": true }, "node_modules/regenerator-transform": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.1.tgz", - "integrity": "sha512-knzmNAcuyxV+gQCufkYcvOqX/qIIfHLv0u5x79kRxuGojfYVky1f15TzZEu2Avte8QGepvUNTnLskf8E6X6Vyg==", + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", + "integrity": "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==", "dev": true, "dependencies": { "@babel/runtime": "^7.8.4" @@ -5201,9 +5237,9 @@ ] }, "node_modules/sass": { - "version": "1.63.6", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.63.6.tgz", - "integrity": "sha512-MJuxGMHzaOW7ipp+1KdELtqKbfAWbH7OLIdoSMnVe3EXPMTmxTmlaZDCTsgIpPCs3w99lLo9/zDKkOrJuT5byw==", + "version": "1.69.5", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.69.5.tgz", + "integrity": "sha512-qg2+UCJibLr2LCVOt3OlPhr/dqVHWOa9XtZf2OjbLs/T4VPSJ00udtgJxH3neXZm+QqX8B+3cU7RaLqp1iVfcQ==", "dev": true, "dependencies": { "chokidar": ">=3.0.0 <4.0.0", @@ -5813,9 +5849,9 @@ "dev": true }, "node_modules/tailwindcss": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.2.tgz", - "integrity": "sha512-9jPkMiIBXvPc2KywkraqsUfbfj+dHDb+JPWtSJa9MLFdrPyazI7q6WX2sUrm7R9eVR7qqv3Pas7EvQFzxKnI6w==", + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.6.tgz", + "integrity": "sha512-AKjF7qbbLvLaPieoKeTjG1+FyNZT6KaJMJPFeQyLfIp7l82ggH1fbHJSsYIvnbTFQOlkh+gBYpyby5GT1LIdLw==", "dev": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", @@ -5823,10 +5859,10 @@ "chokidar": "^3.5.3", "didyoumean": "^1.2.2", "dlv": "^1.1.3", - "fast-glob": "^3.2.12", + "fast-glob": "^3.3.0", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", - "jiti": "^1.18.2", + "jiti": "^1.19.1", "lilconfig": "^2.1.0", "micromatch": "^4.0.5", "normalize-path": "^3.0.0", @@ -5838,7 +5874,6 @@ "postcss-load-config": "^4.0.1", "postcss-nested": "^6.0.1", "postcss-selector-parser": "^6.0.11", - "postcss-value-parser": "^4.2.0", "resolve": "^1.22.2", "sucrase": "^3.32.0" }, @@ -6011,9 +6046,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", - "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==", + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", + "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", "dev": true, "funding": [ { diff --git a/package.json b/package.json index 5ccda56dbbec8c2dcf7dfefe6e6f0e9ec578bdce..59ed50eb51a1b0d2f1f0c74773effc3cebec80f2 100644 --- a/package.json +++ b/package.json @@ -29,27 +29,27 @@ "not dead" ], "devDependencies": { - "@alpinejs/focus": "^3.12.3", - "@babel/cli": "^7.22.6", - "@babel/core": "^7.22.8", - "@babel/preset-env": "^7.22.7", - "@tailwindcss/forms": "^0.5.3", - "@tailwindcss/typography": "^0.5.9", - "alpinejs": "^3.12.3", - "core-js": "^3.31.1", + "@alpinejs/focus": "^3.13.3", + "@babel/cli": "^7.23.4", + "@babel/core": "^7.23.5", + "@babel/preset-env": "^7.23.5", + "@tailwindcss/forms": "^0.5.7", + "@tailwindcss/typography": "^0.5.10", + "alpinejs": "^3.13.3", + "core-js": "^3.34.0", "daterangepicker": "^3.1.0", - "eslint": "^8.44.0", - "eslint-config-prettier": "^8.8.0", + "eslint": "^8.55.0", + "eslint-config-prettier": "^8.10.0", "eslint-plugin-template": "^0.7.0", - "htmx.org": "^1.9.2", - "nodemon": "^3.0.1", - "npm-run-all2": "^6.0.6", - "prettier": "3.0.0", - "sass": "^1.63.6", + "htmx.org": "^1.9.9", + "nodemon": "^3.0.2", + "npm-run-all2": "^6.1.1", + "prettier": "^3.1.0", + "sass": "^1.69.5", "stylelint": "^15.10.2", "stylelint-config-standard": "^34.0.0", "stylelint-config-standard-scss": "^10.0.0", - "tailwindcss": "^3.3.2" + "tailwindcss": "^3.3.6" }, "scripts": { "heroku-postbuild": "npm run build", @@ -80,7 +80,7 @@ "clean": "rm -rf ./static ./hypha/static_compiled" }, "engines": { - "node": "18.x" + "node": "20.10.x" }, "private": true } diff --git a/requirements-dev.txt b/requirements-dev.txt index 162eec0ecc0e7cbef2a7a2208bb70922d9cc422d..c4f1ca7c91fd65cf60604655236129b9226ba79d 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,21 +1,22 @@ -r requirements.txt -black==23.10.0 coverage==7.3.2 -django-browser-reload==1.12.0 +django-browser-reload==1.12.1 django-coverage-plugin==3.1.0 django-debug-toolbar==4.2.0 +django-dynamic-fixture==4.0.1 djhtml==3.0.6 dslr==0.4.0 factory_boy==3.2.1 Faker==19.13.0 model-bakery==1.10.1 -pre-commit==3.3.3 +pre-commit==3.5.0 pytest-cov==4.1.0 -pytest-django==4.5.2 +pytest-django==4.7.0 pytest-split==0.8.1 pytest-xdist[psutil]==3.3.1 responses==0.23.3 -ruff==0.1.1 +ruff==0.1.7 +time-machine==2.13.0 wagtail-factories==2.1.0 Werkzeug==3.0.1 diff --git a/requirements.txt b/requirements.txt index c2357250a8225407e3bbd2e3b52414268edf6df9..7abfb72c2eaf0c2f2a8dba98e7463815e13d2aec 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,15 +3,14 @@ scout-apm==2.26.1 sentry-sdk==1.16.0 # Production dependencies -Babel==2.11.0 -bleach==5.0.0 -boto3==1.28.68 +Babel==2.13.1 +boto3==1.28.82 celery==5.2.7 click==8.1.7 dj-database-url==2.1.0 -django-anymail==9.0 +django-anymail==10.2 django-basic-auth-ip-whitelist==0.5 -django-bleach==3.0.1 +django-bleach==3.1.0 django-countries==7.5.1 django-elevate==2.0.3 django-extensions==3.2.3 @@ -20,7 +19,7 @@ django-filter==2.4.0 django-formtools==2.4.1 django-fsm==2.8.1 django-heroku==0.3.1 -django-hijack==3.2.6 +django-hijack==3.4.2 django-htmx==1.17.0 django-pagedown==2.2.1 # django-pwned-passwords==4.1.0 @@ -28,29 +27,28 @@ https://github.com/slinkymanbyday/django-pwned-passwords/archive/58c7b832df7360a django-ratelimit==4.1.0 django-referrer-policy==1.0 django-select2==8.0.0 -django-slack==5.18.0 +django-slack==5.19.0 django-storages==1.13.2 django-tables2==2.5.1 django-tinymce==3.5.0 django-two-factor-auth==1.15.5 django-web-components==0.1.1 -django==4.1.13 +django==4.2.9 djangorestframework-api-key==2.3.0 djangorestframework==3.14.0 drf-nested-routers==0.93.4 drf-yasg==1.21.4 environs==9.5.0 -gunicorn==20.1.0 +gunicorn==21.2.0 heroicons==2.5.0 python-docx<1.0.0 htmldocx==0.0.6 -lark==1.1.7 -mailchimp3==3.0.17 -mistune==2.0.4 -more-itertools==9.0.0 -phonenumberslite==8.13.23 +lark==1.1.8 +mistune==3.0.1 +more-itertools==10.1.0 +phonenumberslite==8.13.26 Pillow>=10.0.1 -psycopg2-binary +psycopg[binary]==3.1.14 qrcode==7.4.2 reportlab==3.6.13 social_auth_app_django==5.0.0 @@ -59,6 +57,6 @@ tomd==0.1.3 wagtail-cache==2.3.0 wagtail-purge==0.3.0 wagtail==5.1.3 -whitenoise==5.3.0 +whitenoise==6.6.0 xhtml2pdf==0.2.11 xmltodict==0.13.0 diff --git a/runtime.txt b/runtime.txt index 4b44813f0658efb2ea25be2032a1a0608cc72670..1f79d441fe9d8203caddccc9029d5ddecfbc6b08 100644 --- a/runtime.txt +++ b/runtime.txt @@ -1 +1 @@ -python-3.11.1 +python-3.11.7 diff --git a/tailwind.config.js b/tailwind.config.js index 0151c3c73c73f20b442931cc1eb00c9a44b371d7..7e1f1a25f0b872d53fe4982fc59677811962c6cb 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -12,6 +12,7 @@ module.exports = { "dark-blue": "#0c72a0", tomato: "#f05e54", "mid-grey": "#cfcfcf", + "light-grey": "#f7f7f7", arsenic: "#404041", "fg-muted": "var(--color-fg-muted)", "fg-default": "var(--color-fg-default)",