From a2812540f8893af11231d42b7c2d9936e9b29880 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20Alfred=C3=A9en?= Date: Tue, 20 Aug 2024 16:12:02 +0200 Subject: [PATCH 01/19] App status end2end test improvements (#217) --- cypress/e2e/ui-tests/test-deploy-app.cy.js | 30 +++++++++++++++++++--- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/cypress/e2e/ui-tests/test-deploy-app.cy.js b/cypress/e2e/ui-tests/test-deploy-app.cy.js index 659d2adb..572faba2 100644 --- a/cypress/e2e/ui-tests/test-deploy-app.cy.js +++ b/cypress/e2e/ui-tests/test-deploy-app.cy.js @@ -276,7 +276,7 @@ describe("Test deploying app", () => { it("can deploy a dash app", { defaultCommandTimeout: 100000 }, () => { // Names of objects to create const project_name = "e2e-deploy-app-test" - const app_name = "e2e-dash-example" + let app_name = "e2e-dash-example" const app_description = "e2e-dash-description" const source_code_url = "https://doi.org/example" const image_name = "ghcr.io/scilifelabdatacentre/dash-covid-in-sweden:20240117-063059" @@ -285,6 +285,7 @@ describe("Test deploying app", () => { const app_type = "Dash App" if (createResources === true) { + // Create Dash app cy.log("Creating a dash app") cy.visit("/projects/") cy.contains('.card-title', project_name).parents('.card-body').siblings('.card-footer').find('a:contains("Open")').first().click() @@ -299,6 +300,7 @@ describe("Test deploying app", () => { cy.get('tr:contains("' + app_name + '")').find('span').should('contain', 'Running') cy.get('tr:contains("' + app_name + '")').find('span').should('contain', 'public') + // Verify Dash app values cy.log("Checking that all dash app settings were saved") cy.visit("/projects/") cy.contains('.card-title', project_name).parents('.card-body').siblings('.card-footer').find('a:contains("Open")').first().click() @@ -310,6 +312,20 @@ describe("Test deploying app", () => { cy.get('#id_image').should('have.value', image_name) cy.get('#id_port').should('have.value', image_port) + // Edit Dash app + cy.log("Editing the dash app settings (non redeployment fields)") + cy.visit("/projects/") + cy.contains('.card-title', project_name).parents('.card-body').siblings('.card-footer').find('a:contains("Open")').first().click() + cy.get('tr:contains("' + app_name + '")').find('i.bi-three-dots-vertical').click() + cy.get('tr:contains("' + app_name + '")').find('a').contains('Settings').click() + app_name = app_name + "-edited" + cy.get('#id_name').type("-edited") + cy.get('#submit-id-submit').contains('Submit').click() + // Verify that the app status still equals Running + cy.get('tr:contains("' + app_name + '")').find('span').should('contain', 'Running') + cy.get('tr:contains("' + app_name + '")').find('span').should('contain', 'public') + + // Delete the Dash app cy.log("Deleting the dash app") cy.visit("/projects/") cy.contains('.card-title', project_name).parents('.card-body').siblings('.card-footer').find('a:contains("Open")').first().click() @@ -446,6 +462,11 @@ describe("Test deploying app", () => { // check that the app was updated with the correct subdomain cy.get('a').contains(app_name).should('have.attr', 'href').and('include', subdomain_3) + // Verify that the app status is not Deleted (Deleting and Created ok) + cy.get('tr:contains("' + app_name + '")', {timeout: 5000}).find('span').should('not.contain', 'Deleted') + // Finally verify status equals Running + cy.get('tr:contains("' + app_name + '")', {timeout: 100000}).find('span').should('contain', 'Running') + } else { cy.log('Skipped because create_resources is not true'); } @@ -475,14 +496,15 @@ describe("Test deploying app", () => { cy.get('#id_port').type("8501") cy.get('#id_image').type("hkqxqxkhkqwxhkxwh") // input random string cy.get('#submit-id-submit').contains('Submit').click() - // check that the app was created - cy.get('tr:contains("' + app_name_statuses + '")').find('span').should('contain', 'Image Error') + // Check that the app was created. Using custom timeout of 5 secs + cy.get('tr:contains("' + app_name_statuses + '")', {timeout: 5000}).find('span').should('contain', 'Image Error') cy.log("Now updating the app to give a correct image reference - expecting Running") cy.get('tr:contains("' + app_name_statuses + '")').find('i.bi-three-dots-vertical').click() cy.get('tr:contains("' + app_name_statuses + '")').find('a').contains('Settings').click() cy.get('#id_image').clear().type(image_name) cy.get('#submit-id-submit').contains('Submit').click() - cy.get('tr:contains("' + app_name_statuses + '")').find('span').should('contain', 'Running') + // Using longer custom timeout for correct image to be set to Running + cy.get('tr:contains("' + app_name_statuses + '")', {timeout: 30000}).find('span').should('contain', 'Running') } else { cy.log('Skipped because create_resources is not true'); } From d77fde2c5c770701fda502d4cf43b8ba7d04f782 Mon Sep 17 00:00:00 2001 From: Nikita Churikov <8545082+churnikov@users.noreply.github.com> Date: Tue, 27 Aug 2024 16:11:19 +0200 Subject: [PATCH 02/19] Change `get_common_field` method to a class that is a child of a `crispy_form.Div`. Moved `custom_field.py` to `field/custom.py` (#218) --- apps/forms/__init__.py | 2 +- apps/forms/base.py | 28 ----------------- apps/forms/custom.py | 22 +++++++------- apps/forms/dash.py | 19 ++++++------ apps/forms/field/__init__.py | 0 apps/forms/field/common.py | 30 +++++++++++++++++++ .../{custom_field.py => field/custom.py} | 0 apps/forms/jupyter.py | 8 ++--- apps/forms/rstudio.py | 7 +++-- apps/forms/shiny.py | 23 +++++++------- apps/forms/tissuumaps.py | 17 +++++------ apps/forms/vscode.py | 9 +++--- 12 files changed, 85 insertions(+), 80 deletions(-) create mode 100644 apps/forms/field/__init__.py create mode 100644 apps/forms/field/common.py rename apps/forms/{custom_field.py => field/custom.py} (100%) diff --git a/apps/forms/__init__.py b/apps/forms/__init__.py index b388094a..8b1ace01 100644 --- a/apps/forms/__init__.py +++ b/apps/forms/__init__.py @@ -1,4 +1,4 @@ -from .custom_field import CustomField # isort:skip +from apps.forms.field.custom import CustomField # isort:skip from .base import AppBaseForm, BaseForm from .custom import CustomAppForm from .dash import DashForm diff --git a/apps/forms/base.py b/apps/forms/base.py index 298cb7b7..45a556a2 100644 --- a/apps/forms/base.py +++ b/apps/forms/base.py @@ -7,8 +7,6 @@ from django.template import loader from django.utils.safestring import mark_safe -from apps.constants import HELP_MESSAGE_MAP -from apps.forms import CustomField from apps.helpers import get_select_options from apps.models import BaseAppInstance, Subdomain, VolumeInstance from apps.types_.subdomain import SubdomainCandidateName, SubdomainTuple @@ -146,32 +144,6 @@ def validate_subdomain(self, subdomain_input): return SubdomainTuple(subdomain_input, True) - def get_common_field(self, field_name: str, **kwargs): - """ - This function is very useful because it allows you to create a custom field, - that has a question_mark with tooltip next to the label. So "Name (?)" will have a tooltip. - The text in the tooltip is defined in HELP_MESSAGE_MAP. - The CustomField class just inherits the crispy_forms.layout.Field class and adds the - help_message attribute to it. The template then uses it to render the tooltip for all fields - using this class. - """ - - spinner = kwargs.pop("spinner", False) - - template = "apps/custom_field.html" - base_args = dict( - css_class="form-control form-control-with-spinner" if spinner else "form-control", - wrapper_class="mb-3", - rows=3, - help_message=HELP_MESSAGE_MAP.get(field_name, ""), - spinner=spinner, - ) - - base_args.update(kwargs) - field = CustomField(field_name, **base_args) - field.set_template(template) - return Div(field, css_class="form-input-with-spinner" if spinner else None) - class Meta: # Specify model to be used model = BaseAppInstance diff --git a/apps/forms/custom.py b/apps/forms/custom.py index f861c26b..f51e0a6f 100644 --- a/apps/forms/custom.py +++ b/apps/forms/custom.py @@ -2,11 +2,11 @@ from django import forms from apps.forms.base import AppBaseForm +from apps.forms.field.common import SRVCommonDivField from apps.models import CustomAppInstance, VolumeInstance from projects.models import Flavor __all__ = ["CustomAppForm"] -from apps.forms import CustomField class CustomAppForm(AppBaseForm): @@ -24,22 +24,22 @@ def _setup_form_helper(self): super()._setup_form_helper() body = Div( - self.get_common_field("name", placeholder="Name your app"), - self.get_common_field("description", rows=3, placeholder="Provide a detailed description of your app"), + SRVCommonDivField("name", placeholder="Name your app"), + SRVCommonDivField("description", rows=3, placeholder="Provide a detailed description of your app"), Field("tags"), - self.get_common_field("subdomain", placeholder="Enter a subdomain or leave blank for a random one."), + SRVCommonDivField("subdomain", placeholder="Enter a subdomain or leave blank for a random one."), Field("volume"), - self.get_common_field("path", placeholder="/home/..."), - self.get_common_field("flavor"), - self.get_common_field("access"), - self.get_common_field("source_code_url", placeholder="Provide a link to the public source code"), - self.get_common_field( + SRVCommonDivField("path", placeholder="/home/..."), + SRVCommonDivField("flavor"), + SRVCommonDivField("access"), + SRVCommonDivField("source_code_url", placeholder="Provide a link to the public source code"), + SRVCommonDivField( "note_on_linkonly_privacy", rows=1, placeholder="Describe why you want to make the app accessible only via a link", ), - self.get_common_field("port", placeholder="8000"), - self.get_common_field("image"), + SRVCommonDivField("port", placeholder="8000"), + SRVCommonDivField("image"), css_class="card-body", ) self.helper.layout = Layout(body, self.footer) diff --git a/apps/forms/dash.py b/apps/forms/dash.py index 807e35a7..03802fdb 100644 --- a/apps/forms/dash.py +++ b/apps/forms/dash.py @@ -2,6 +2,7 @@ from django import forms from apps.forms.base import AppBaseForm +from apps.forms.field.common import SRVCommonDivField from apps.models import DashInstance from projects.models import Flavor @@ -20,21 +21,21 @@ def _setup_form_fields(self): def _setup_form_helper(self): super()._setup_form_helper() body = Div( - self.get_common_field("name", placeholder="Name your app"), - self.get_common_field("description", rows="3", placeholder="Provide a detailed description of your app"), + SRVCommonDivField("name", placeholder="Name your app"), + SRVCommonDivField("description", rows="3", placeholder="Provide a detailed description of your app"), Field("tags"), - self.get_common_field( + SRVCommonDivField( "subdomain", placeholder="Enter a subdomain or leave blank for a random one", spinner=True ), - self.get_common_field("flavor"), - self.get_common_field("access"), - self.get_common_field( + SRVCommonDivField("flavor"), + SRVCommonDivField("access"), + SRVCommonDivField( "note_on_linkonly_privacy", placeholder="Describe why you want to make the app accessible only via a link", ), - self.get_common_field("source_code_url", placeholder="Provide a link to the public source code"), - self.get_common_field("port", placeholder="8000"), - self.get_common_field("image", placeholder="registry/repository/image:tag"), + SRVCommonDivField("source_code_url", placeholder="Provide a link to the public source code"), + SRVCommonDivField("port", placeholder="8000"), + SRVCommonDivField("image", placeholder="registry/repository/image:tag"), css_class="card-body", ) diff --git a/apps/forms/field/__init__.py b/apps/forms/field/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/forms/field/common.py b/apps/forms/field/common.py new file mode 100644 index 00000000..7f98a917 --- /dev/null +++ b/apps/forms/field/common.py @@ -0,0 +1,30 @@ +from crispy_forms.layout import Div + +from apps.constants import HELP_MESSAGE_MAP +from apps.forms.field.custom import CustomField + + +class SRVCommonDivField(Div): + """ + This class is the most common field used in the apps creation forms. + Resulting field has a question_mark with tooltip next to the label. So "Name (?)" will have a tooltip. + The text in the tooltip is defined in apps.constants.HELP_MESSAGE_MAP. + The CustomField class just inherits the crispy_forms.layout.Field class and adds the + help_message attribute to it. The template then uses it to render the tooltip for all fields + using this class. + """ + + def __init__(self, field_name: str, spinner=False, css_class=None, template="apps/custom_field.html", **kwargs): + if css_class is None and spinner: + css_class = "form-input-with-spinner" + base_args = dict( + css_class="form-control form-control-with-spinner" if spinner else "form-control", + wrapper_class="mb-3", + rows=3, + help_message=HELP_MESSAGE_MAP.get(field_name, ""), + spinner=spinner, + ) + base_args.update(kwargs) + field_ = CustomField(field_name, **base_args) + field_.set_template(template) + super().__init__(field_, css_class=css_class) diff --git a/apps/forms/custom_field.py b/apps/forms/field/custom.py similarity index 100% rename from apps/forms/custom_field.py rename to apps/forms/field/custom.py diff --git a/apps/forms/jupyter.py b/apps/forms/jupyter.py index fea95a33..87921948 100644 --- a/apps/forms/jupyter.py +++ b/apps/forms/jupyter.py @@ -2,8 +2,8 @@ from django import forms from apps.forms.base import AppBaseForm +from apps.forms.field.common import SRVCommonDivField from apps.models import JupyterInstance, VolumeInstance -from projects.models import Flavor __all__ = ["JupyterForm"] @@ -15,10 +15,10 @@ def _setup_form_helper(self): super()._setup_form_helper() body = Div( - self.get_common_field("name", placeholder="Name your app"), + SRVCommonDivField("name", placeholder="Name your app"), Field("volume"), - self.get_common_field("access"), - self.get_common_field("flavor"), + SRVCommonDivField("access"), + SRVCommonDivField("flavor"), css_class="card-body", ) diff --git a/apps/forms/rstudio.py b/apps/forms/rstudio.py index 39317c62..ab80037d 100644 --- a/apps/forms/rstudio.py +++ b/apps/forms/rstudio.py @@ -2,6 +2,7 @@ from django import forms from apps.forms.base import AppBaseForm +from apps.forms.field.common import SRVCommonDivField from apps.models import RStudioInstance, VolumeInstance __all__ = ["RStudioForm"] @@ -13,10 +14,10 @@ class RStudioForm(AppBaseForm): def _setup_form_helper(self): super()._setup_form_helper() body = Div( - self.get_common_field("name", placeholder="Name your app"), + SRVCommonDivField("name", placeholder="Name your app"), Field("volume"), - self.get_common_field("flavor"), - self.get_common_field("access"), + SRVCommonDivField("flavor"), + SRVCommonDivField("access"), css_class="card-body", ) diff --git a/apps/forms/shiny.py b/apps/forms/shiny.py index f434f40d..2d3e34bf 100644 --- a/apps/forms/shiny.py +++ b/apps/forms/shiny.py @@ -1,7 +1,8 @@ -from crispy_forms.layout import HTML, Div, Field, Layout +from crispy_forms.layout import Div, Layout from django import forms from apps.forms.base import AppBaseForm +from apps.forms.field.common import SRVCommonDivField from apps.models import ShinyInstance from projects.models import Flavor @@ -26,21 +27,21 @@ def _setup_form_fields(self): def _setup_form_helper(self): super()._setup_form_helper() body = Div( - self.get_common_field("name", placeholder="Name your app"), - self.get_common_field("description", rows="3", placeholder="Provide a detailed description of your app"), - Field("tags"), - self.get_common_field( + SRVCommonDivField("name", placeholder="Name your app"), + SRVCommonDivField("description", rows="3", placeholder="Provide a detailed description of your app"), + SRVCommonDivField("tags"), + SRVCommonDivField( "subdomain", placeholder="Enter a subdomain or leave blank for a random one", spinner=True ), - self.get_common_field("flavor"), - self.get_common_field("access"), - self.get_common_field( + SRVCommonDivField("flavor"), + SRVCommonDivField("access"), + SRVCommonDivField( "note_on_linkonly_privacy", placeholder="Describe why you want to make the app accessible only via a link", ), - self.get_common_field("source_code_url", placeholder="Provide a link to the public source code"), - self.get_common_field("port", placeholder="3838"), - self.get_common_field("image", placeholder="registry/repository/image:tag"), + SRVCommonDivField("source_code_url", placeholder="Provide a link to the public source code"), + SRVCommonDivField("port", placeholder="3838"), + SRVCommonDivField("image", placeholder="registry/repository/image:tag"), css_class="card-body", ) diff --git a/apps/forms/tissuumaps.py b/apps/forms/tissuumaps.py index 22ae2c00..3dd1ebaa 100644 --- a/apps/forms/tissuumaps.py +++ b/apps/forms/tissuumaps.py @@ -1,9 +1,8 @@ -from crispy_forms.layout import HTML, Div, Field, Layout -from django import forms +from crispy_forms.layout import Div, Field, Layout from apps.forms.base import AppBaseForm +from apps.forms.field.common import SRVCommonDivField from apps.models import TissuumapsInstance -from projects.models import Flavor __all__ = ["TissuumapsForm"] @@ -19,16 +18,16 @@ def _setup_form_fields(self): def _setup_form_helper(self): super()._setup_form_helper() body = Div( - self.get_common_field("name", placeholder="Name your app"), - self.get_common_field("description", rows="3", placeholder="Provide a detailed description of your app"), + SRVCommonDivField("name", placeholder="Name your app"), + SRVCommonDivField("description", rows="3", placeholder="Provide a detailed description of your app"), Field("tags"), - self.get_common_field( + SRVCommonDivField( "subdomain", placeholder="Enter a subdomain or leave blank for a random one", spinner=True ), Field("volume"), - self.get_common_field("flavor"), - self.get_common_field("access"), - self.get_common_field( + SRVCommonDivField("flavor"), + SRVCommonDivField("access"), + SRVCommonDivField( "note_on_linkonly_privacy", placeholder="Describe why you want to make the app accessible only via a link", ), diff --git a/apps/forms/vscode.py b/apps/forms/vscode.py index 8d1f9745..fb05ab07 100644 --- a/apps/forms/vscode.py +++ b/apps/forms/vscode.py @@ -1,7 +1,8 @@ -from crispy_forms.layout import HTML, Div, Field, Layout +from crispy_forms.layout import Div, Field, Layout from django import forms from apps.forms.base import AppBaseForm +from apps.forms.field.common import SRVCommonDivField from apps.models import VolumeInstance, VSCodeInstance __all__ = ["VSCodeForm"] @@ -13,10 +14,10 @@ class VSCodeForm(AppBaseForm): def _setup_form_helper(self): super()._setup_form_helper() body = Div( - self.get_common_field("name", placeholder="Name your app"), + SRVCommonDivField("name", placeholder="Name your app"), Field("volume"), - self.get_common_field("flavor"), - self.get_common_field("access"), + SRVCommonDivField("flavor"), + SRVCommonDivField("access"), css_class="card-body", ) From 6ac0a986bb453bf97b058d63c7c7c61babd8f5d8 Mon Sep 17 00:00:00 2001 From: Arnold Kochari Date: Mon, 2 Sep 2024 15:02:18 +0200 Subject: [PATCH 03/19] Preparation before renaming repo to 'serve' (#222) --- .github/workflows/ci.yaml | 2 +- .github/workflows/e2e-tests.yaml | 4 ++-- .github/workflows/publish.yaml | 2 +- README.md | 14 +++++++------- pyproject.toml | 2 +- templates/common/footer.html | 2 +- templates/portal/about.html | 2 +- 7 files changed, 14 insertions(+), 14 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 1eed4bb2..fc05820c 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -31,7 +31,7 @@ on: jobs: CI: - if: github.repository == 'scilifelabdatacentre/stackn' + if: github.repository == 'scilifelabdatacentre/serve' runs-on: ubuntu-20.04 env: working-directory: . diff --git a/.github/workflows/e2e-tests.yaml b/.github/workflows/e2e-tests.yaml index 04049af2..47734b44 100644 --- a/.github/workflows/e2e-tests.yaml +++ b/.github/workflows/e2e-tests.yaml @@ -26,7 +26,7 @@ on: jobs: e2e: - if: github.repository == 'scilifelabdatacentre/stackn' + if: github.repository == 'scilifelabdatacentre/serve' runs-on: ubuntu-20.04 env: working-directory: . @@ -147,4 +147,4 @@ jobs: ## limits ssh access and adds the ssh public key for the user which triggered the workflow limit-access-to-actor: true ## limits ssh access and adds the ssh public keys of the listed GitHub users - limit-access-to-users: sandstromviktor,churnikov,hamzaimran08,alfredeen,akochari + limit-access-to-users: churnikov,hamzaimran08,alfredeen,akochari,anondo1969 diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 873bfeb4..06e7bcda 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -26,7 +26,7 @@ on: jobs: publish-on-success: if: | - github.repository == 'scilifelabdatacentre/stackn' && + github.repository == 'scilifelabdatacentre/serve' && ( (github.event_name == 'push' && contains(github.ref, 'refs/tags/')) || diff --git a/README.md b/README.md index 1359fd82..3fca4a92 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,9 @@ Code style: black
- Pre-commit - CI - End2end tests + Pre-commit + CI + End2end tests

@@ -16,11 +16,11 @@ SciLifeLab Serve ([https://serve.scilifelab.se](https://serve.scilifelab.se)) is a platform offering machine learning model serving, app hosting (Shiny, Streamlit, Dash, etc.), web-based integrated development environments, and other tools to life science researchers affiliated with a Swedish research institute. It is developed and operated by the [SciLifeLab Data Centre](https://github.com/ScilifelabDataCentre), part of [SciLifeLab](https://scilifelab.se/). See [this page for information about funders and mandate](https://serve.scilifelab.se/about/). -This repository contains source code for SciLifeLab Serve. It is based on the open-source platform [Stackn](https://github.com/scaleoutsystems/stackn). +This repository contains source code for the main Django application of SciLifeLab Serve. ## Reporting bugs and requesting features -If you are using SciLifeLab Serve and notice a bug or if there is a feature you would like to be added feel free to [create an issue](https://github.com/ScilifelabDataCentre/stackn/issues/new/choose) with a bug report or feature request. +If you are using SciLifeLab Serve and notice a bug or if there is a feature you would like to be added feel free to [create an issue](https://github.com/ScilifelabDataCentre/serve/issues/new/choose) with a bug report or feature request. ## Development and contributions @@ -36,8 +36,8 @@ It is possible to deploy and work with the user interface of Serve without a run 1. Clone this repository locally: ``` -$ git clone https://github.com/ScilifelabDataCentre/stackn -$ cd stackn +$ git clone https://github.com/ScilifelabDataCentre/serve +$ cd serve ``` 2. Create a copy of the .env template file diff --git a/pyproject.toml b/pyproject.toml index 7721e6a7..b76034bd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ license = "Apache 2.0" maintainers = ["Team Whale ",] authors = ["Team Whale ",] homepage = "https://serve.scilifelab.se" -repository = "https://github.com/ScilifelabDataCentre/stackn" +repository = "https://github.com/ScilifelabDataCentre/serve" keywords = ["machine learning", "life science", "research", "django", "python"] packages = [] diff --git a/templates/common/footer.html b/templates/common/footer.html index c1aee30f..7af05e86 100644 --- a/templates/common/footer.html +++ b/templates/common/footer.html @@ -33,7 +33,7 @@
-

SciLifeLab Serve (beta) is developed and operated by the SciLifeLab Data Centre. SciLifeLab Serve is free to use for all life science researchers affiliated with a Swedish research institution and their collaborators. The service is hosted on a Kubernetes cluster. The code behind SciLifeLab Serve is available on Github.

+

SciLifeLab Serve (beta) is developed and operated by the SciLifeLab Data Centre. SciLifeLab Serve is free to use for all life science researchers affiliated with a Swedish research institution and their collaborators. The service is hosted on a Kubernetes cluster. The code behind SciLifeLab Serve is available on Github.

Please email serve@scilifelab.se with any questions.

version {{version_text}} diff --git a/templates/portal/about.html b/templates/portal/about.html index e8427968..f918301d 100644 --- a/templates/portal/about.html +++ b/templates/portal/about.html @@ -10,7 +10,7 @@

About SciLifeLab Serve

SciLifeLab Serve (beta) is a platform for serving applications and machine learning models that is offered to life science researchers affiliated with a Swedish research institute and their collaborators. Currently, the service is running in beta testing mode. The service is available free of charge to researchers with and without an affiliation to SciLifeLab, to collaborators of these researchers, as well as to data-producing infrastructures in Sweden.

Infrastructure behind the service

-

SciLifeLab Serve is developed and maintained by the SciLifeLab Data Centre. Initially the service was based on STACKn, a platform developed by Scaleout systems (a spinoff from the Department of Information Technology at Uppsala University). Since then, the SciLifeLab Data Centre team has expanded and improved the platform. The most up to date source code behind SciLifeLab Serve is available with an open source license on GitHub. We welcome any other organisations running a similar service using our code.

+

SciLifeLab Serve is developed and maintained by the SciLifeLab Data Centre. Initially the service was based on STACKn, a platform developed by Scaleout systems (a spinoff from the Department of Information Technology at Uppsala University). Since then, the SciLifeLab Data Centre team has expanded and improved the platform. The most up to date source code behind SciLifeLab Serve is available with an open source license on GitHub. We welcome any other organisations running a similar service using our code.

The main SciLifeLab Serve application is built with Python on Django. SciLifeLab Serve components include a number of open source applications: JupyterLab (source code), RStudio Server (source code, modifications), code-server (VS Code, source code), ShinyProxy (source code), MinIO (unmodified), TensorFlow Serving (source code), TorchServe (source code), MLflow (source code). We use Bootstrap and Bootstrap Icons for user interface styling, JavaScript libraries jQuery and aos for dynamic elements of the website.

SciLifeLab Serve as well as the applications and models deployed through the service are hosted on a Kubernetes cluster located and administered on hardware at the KTH Royal Institute of Technology in Stockholm and managed by the SciLifeLab Data Centre.

From 8d06ae8a497ff08a219a6db545a17f5773a21f84 Mon Sep 17 00:00:00 2001 From: Nikita Churikov <8545082+churnikov@users.noreply.github.com> Date: Mon, 2 Sep 2024 15:31:29 +0200 Subject: [PATCH 04/19] SS-1088 Add 500 page when everything breaks (#219) Co-authored-by: Hamza --- templates/500.html | 38 ++++++++++++++++++++++++++++++++++++ templates/base.html | 2 +- templates/common/navbar.html | 30 ++++++++++++++++------------ 3 files changed, 56 insertions(+), 14 deletions(-) create mode 100644 templates/500.html diff --git a/templates/500.html b/templates/500.html new file mode 100644 index 00000000..2c3fc371 --- /dev/null +++ b/templates/500.html @@ -0,0 +1,38 @@ + + + +{% load static sekizai_tags %} + + + Internal error | SciLifeLab Serve (beta) + {% include 'common/common_head.html' %} + + + + + {% include 'common/navbar.html' with minimum=True %} + +
+

Internal error. Something went wrong.

+

+ An error has occurred. We are sorry for the inconvenience. +

+

+ Error code: 500 +

+

+ In the meantime, here is what you can do +

+
    +
  • Refresh the page (sometimes this helps)
  • +
  • Try again in a few minutes
  • +
  • If the problem persists, please contact us at serve@scilifelab.se + and provide information on how to reproduce the error.
  • +
+ {# Link hardcoded intentionally, this page should be there in case almost **everything** breaks #} + + Take me home + +
+ + diff --git a/templates/base.html b/templates/base.html index ca0af799..d3ae18ae 100644 --- a/templates/base.html +++ b/templates/base.html @@ -13,7 +13,7 @@ - {% include 'common/navbar.html' %} + {% include 'common/navbar.html' with minimum=False %}
{% block content %}{% endblock %} diff --git a/templates/common/navbar.html b/templates/common/navbar.html index 1941291b..912b6ad4 100644 --- a/templates/common/navbar.html +++ b/templates/common/navbar.html @@ -8,6 +8,7 @@
+ {% if not minimum %}
+ {% endif %} -{% for obj in maintenance_mode %} -{% if obj.message_in_header %} -
-
-
- -
-
-
+{% if not minumum %} + {% for obj in maintenance_mode %} + {% if obj.message_in_header %} +
+
+
+ +
+
+
+ {% endif %} + {% endfor %} {% endif %} -{% endfor %} From 44092f3884774415f44e87167e6faba43966f1c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20Alfred=C3=A9en?= Date: Tue, 3 Sep 2024 08:18:21 +0200 Subject: [PATCH 05/19] Upgraded Cypress to 13.3.0 (#220) --- .github/workflows/e2e-tests.yaml | 2 +- Dockerfile.cypress | 2 +- cypress.config.js | 1 - .../test-brute-force-login-attempts.cy.js | 22 +- cypress/e2e/ui-tests/test-collections.cy.js | 24 +- cypress/e2e/ui-tests/test-deploy-app.cy.js | 397 ++++++++++--- .../test-login-account-handling.cy.js | 17 +- .../test-project-as-contributor.cy.js | 76 +-- .../e2e/ui-tests/test-public-webpages.cy.js | 24 +- cypress/e2e/ui-tests/test-signup.cy.js | 41 +- .../test-superuser-functionality.cy.js | 103 ++-- .../e2e/ui-tests/test-views-as-reader.cy.js | 46 +- cypress/e2e/verify-test-setup.cy.js | 38 +- cypress/support/commands.js | 8 + cypress/support/e2e.js | 2 +- package-lock.json | 548 ++++++++++-------- package.json | 4 +- 17 files changed, 863 insertions(+), 492 deletions(-) diff --git a/.github/workflows/e2e-tests.yaml b/.github/workflows/e2e-tests.yaml index 47734b44..875d1575 100644 --- a/.github/workflows/e2e-tests.yaml +++ b/.github/workflows/e2e-tests.yaml @@ -121,7 +121,7 @@ jobs: - name: Cypress run e2e tests - uses: cypress-io/github-action@v5 + uses: cypress-io/github-action@v6 with: working-directory: ${{env.working-directory}} config: pageLoadTimeout=100000,baseUrl=${{ env.STUDIO_URL }} diff --git a/Dockerfile.cypress b/Dockerfile.cypress index 8377f4ed..e0d92da9 100644 --- a/Dockerfile.cypress +++ b/Dockerfile.cypress @@ -1,4 +1,4 @@ -FROM cypress/included:11.2.0 +FROM cypress/included:13.3.0 WORKDIR /app diff --git a/cypress.config.js b/cypress.config.js index 1c090dbb..b7205d7f 100644 --- a/cypress.config.js +++ b/cypress.config.js @@ -10,7 +10,6 @@ module.exports = defineConfig({ e2e: { baseUrl: 'http://studio.127.0.0.1.nip.io:8080', //baseUrl: 'https://serve-dev.scilifelab.se', - experimentalSessionAndOrigin: true, setupNodeEvents(on, config) { // implement node event listeners here diff --git a/cypress/e2e/ui-tests/test-brute-force-login-attempts.cy.js b/cypress/e2e/ui-tests/test-brute-force-login-attempts.cy.js index 2d984222..e296b7ed 100644 --- a/cypress/e2e/ui-tests/test-brute-force-login-attempts.cy.js +++ b/cypress/e2e/ui-tests/test-brute-force-login-attempts.cy.js @@ -3,31 +3,39 @@ describe("Test brute force login attempts are blocked", () => { let users before(() => { + cy.logf("Begin before() hook", Cypress.currentTest) + // do db reset if needed if (Cypress.env('do_reset_db') === true) { - cy.log("Resetting db state. Running db-reset.sh"); + cy.logf("Resetting db state. Running db-reset.sh", Cypress.currentTest); cy.exec("./cypress/e2e/db-reset.sh"); cy.wait(Cypress.env('wait_db_reset')); } else { - cy.log("Skipping resetting the db state."); + cy.logf("Skipping resetting the db state.", Cypress.currentTest); } // seed the db with a user cy.visit("/") - cy.log("Running seed-brute-force-login-user.py") + cy.logf("Running seed-brute-force-login-user.py", Cypress.currentTest) cy.exec("./cypress/e2e/db-seed-brute-force-login-user.sh") + + cy.logf("End before() hook", Cypress.currentTest) }) beforeEach(() => { + cy.logf("Begin beforeEach() hook", Cypress.currentTest) + cy.fixture('users.json').then(function (data) { users = data; }) + + cy.logf("End beforeEach() hook", Cypress.currentTest) }) it("can first login but repeated failed login attempts result in locked out account", () => { // First verify that the user can login - cy.log("First verify that the user can login") + cy.logf("First verify that the user can login", Cypress.currentTest) cy.loginViaUI(users.brute_force_login_user.email, users.brute_force_login_user.password) cy.visit("/") cy.get('button.btn-profile').click() @@ -36,7 +44,7 @@ describe("Test brute force login attempts are blocked", () => { cy.get('div.col-8').should("contain", users.brute_force_login_user.email) // Sign out before logging in again - cy.log("Sign out before logging in again") + cy.logf("Sign out before logging in again", Cypress.currentTest) cy.get('button.btn-profile').click() cy.get('li.btn-group').find('button').contains("Sign out").click() cy.get("title").should("have.text", "Logout | SciLifeLab Serve (beta)") @@ -47,7 +55,7 @@ describe("Test brute force login attempts are blocked", () => { while (n_remaining_attempts > 0) { n_remaining_attempts-- - cy.log("Attempting an incorrect login attempt using loginViaUINoValidation") + cy.logf("Attempting an incorrect login attempt using loginViaUINoValidation", Cypress.currentTest) cy.loginViaUINoValidation(users.brute_force_login_user.email, "BAD-PASSWORD") // The user view should stay on login page cy.url().should("include", "accounts/login/") @@ -55,7 +63,7 @@ describe("Test brute force login attempts are blocked", () => { } // Perform one more incorrect login attempt which should result in a locked account - cy.log("Attempting an incorrect login attempt using loginViaUINoValidation") + cy.logf("Attempting an incorrect login attempt using loginViaUINoValidation", Cypress.currentTest) cy.loginViaUINoValidation(users.brute_force_login_user.email, "BAD-PASSWORD") // The user view should stay on login page cy.url().should("include", "accounts/login/") diff --git a/cypress/e2e/ui-tests/test-collections.cy.js b/cypress/e2e/ui-tests/test-collections.cy.js index 46ba9eb0..40d183cb 100644 --- a/cypress/e2e/ui-tests/test-collections.cy.js +++ b/cypress/e2e/ui-tests/test-collections.cy.js @@ -3,22 +3,24 @@ describe("Test collections functionality", () => { // Tests performed as an authenticated user that has superuser privileges because that's the one that can currently create collections // user: no-reply-collections-user@scilifelab.se" - let users - before(() => { + cy.logf("Begin before() hook", Cypress.currentTest) + // do db reset if needed if (Cypress.env('do_reset_db') === true) { - cy.log("Resetting db state. Running db-reset.sh"); + cy.logf("Resetting db state. Running db-reset.sh", Cypress.currentTest); cy.exec("./cypress/e2e/db-reset.sh"); cy.wait(Cypress.env('wait_db_reset')); } else { - cy.log("Skipping resetting the db state."); + cy.logf("Skipping resetting the db state.", Cypress.currentTest); } // seed the db with a user cy.visit("/") - cy.log("Running seed_collections_user.py") + cy.logf("Running seed_collections_user.py", Cypress.currentTest) cy.exec("./cypress/e2e/db-seed-collections-user.sh") + + cy.logf("End before() hook", Cypress.currentTest) }) it("can create a new collection through the django admin interface, can subsequently see this collection page", () => { @@ -31,7 +33,7 @@ describe("Test collections functionality", () => { const collection_app_name = "collection-app-name" // created in the seed_collections_user.py const collection_project_name = "e2e-collections-test-proj" // created in the seed_collections_user.py - cy.log("Creating a collection") + cy.logf("Creating a collection", Cypress.currentTest) // log in as admin cy.visit("/admin/") cy.get('#id_username').type('no-reply-collections-user@scilifelab.se') @@ -49,23 +51,23 @@ describe("Test collections functionality", () => { cy.get('input[name=_save]').click() cy.get('li.success').should('contain', "was added successfully") // confirm collection was created - cy.log("Adding an app to the collection") + cy.logf("Adding an app to the collection", Cypress.currentTest) cy.get('tr.model-dashinstance').find('a').first().click() cy.get('tr').find('a').contains(collection_app_name).click() cy.get('#id_collections').select(collection_name) cy.get('input[name=_save]').click() cy.get('li.success').should('contain', "was changed successfully") // confirm app name has been added to the collection - cy.log("Can see the collection listed on the homepage") + cy.logf("Can see the collection listed on the homepage", Cypress.currentTest) cy.visit('/home/') cy.get('#collections').find('a').should('contain', collection_name) cy.get('#collections').find('a').should('have.attr', 'href', "/collections/" + collection_slug) - cy.log("Can see the collection listed on the collections overview page") + cy.logf("Can see the collection listed on the collections overview page", Cypress.currentTest) cy.visit('/collections') cy.get('a.collection-logo').should('have.attr', 'href', "/collections/" + collection_slug) - cy.log("Can see the created collection page") + cy.logf("Can see the created collection page", Cypress.currentTest) cy.visit("/collections/" + collection_slug) cy.get('h3').should('contain', collection_name) cy.get('#collection-description').should('contain', collection_description) @@ -79,7 +81,7 @@ describe("Test collections functionality", () => { cy.get('h4#datasets').should("exist") cy.get('div#zenodo-entries').should('not.contain', "Fetching Zenodo entries failed") - cy.log("Cleaning up afterwards - removing the project and app") + cy.logf("Cleaning up afterwards - removing the project and app", Cypress.currentTest) cy.visit("/projects/") cy.contains('.card-title', collection_project_name).parents('.card-body').siblings('.card-footer').find('.confirm-delete').click() .then((href) => { diff --git a/cypress/e2e/ui-tests/test-deploy-app.cy.js b/cypress/e2e/ui-tests/test-deploy-app.cy.js index 572faba2..7788aa25 100644 --- a/cypress/e2e/ui-tests/test-deploy-app.cy.js +++ b/cypress/e2e/ui-tests/test-deploy-app.cy.js @@ -1,24 +1,40 @@ describe("Test deploying app", () => { + // The default command timeout should not be so long + // Instead use longer timeouts on specific commands where deemed necessary and valid + const defaultCmdTimeoutMs = 10000 + // The longer timeout is often used when waiting for k8s operations to complete + const longCmdTimeoutMs = 180000 + + // Function to verify the displayed app status permission level + const verifyAppStatus = (app_name, expected_status, expected_permission) => { + cy.get('tr:contains("' + app_name + '")', {timeout: longCmdTimeoutMs}).find('span', {timeout: longCmdTimeoutMs}).should('contain', expected_status) + if (expected_permission != "") { + cy.get('tr:contains("' + app_name + '")').find('span').should('contain', expected_permission) + } + }; + // Tests performed as an authenticated user that // creates and deletes apps. // user: e2e_tests_deploy_app_user - let users - before({ defaultCommandTimeout: 100000 }, () => { + + before({ defaultCommandTimeout: defaultCmdTimeoutMs }, () => { + cy.logf("Begin before() hook", Cypress.currentTest) + // do db reset if needed if (Cypress.env('do_reset_db') === true) { - cy.log("Resetting db state. Running db-reset.sh"); + cy.logf("Resetting db state. Running db-reset.sh", Cypress.currentTest); cy.exec("./cypress/e2e/db-reset.sh"); cy.wait(Cypress.env('wait_db_reset')); } else { - cy.log("Skipping resetting the db state."); + cy.logf("Skipping resetting the db state.", Cypress.currentTest); } // seed the db with a user cy.visit("/") - cy.log("Running seed-deploy-app-user.py") + cy.logf("Running seed-deploy-app-user.py", Cypress.currentTest) cy.exec("./cypress/e2e/db-seed-deploy-app-user.sh") // username in fixture must match username in db-reset.sh cy.fixture('users.json').then(function (data) { @@ -28,19 +44,25 @@ describe("Test deploying app", () => { }) const project_name = "e2e-deploy-app-test" cy.createBlankProject(project_name) + + cy.logf("End before() hook", Cypress.currentTest) }) beforeEach(() => { + cy.logf("Begin beforeEach() hook", Cypress.currentTest) + // username in fixture must match username in db-reset.sh - cy.log("Logging in") + cy.logf("Logging in", Cypress.currentTest) cy.fixture('users.json').then(function (data) { users = data cy.loginViaApi(users.deploy_app_user.email, users.deploy_app_user.password) }) + + cy.logf("End beforeEach() hook", Cypress.currentTest) }) - it("can deploy a project and public app using the custom app chart", { defaultCommandTimeout: 100000 }, () => { + it("can deploy a project and public app using the custom app chart", { defaultCommandTimeout: defaultCmdTimeoutMs }, () => { // Names of objects to create const project_name = "e2e-deploy-app-test" const app_name_project = "e2e-streamlit-example-project" @@ -66,7 +88,7 @@ describe("Test deploying app", () => { cy.contains('.card-title', project_name).parents('.card-body').siblings('.card-footer').find('a:contains("Open")').first().click() // Create an app with project permissions - cy.log("Now creating a project app") + cy.logf("Now creating a project app", Cypress.currentTest) cy.get('div.card-body:contains("' + app_type + '")').find('a:contains("Create")').click() cy.get('#id_name').type(app_name_project) cy.get('#id_description').type(app_description) @@ -77,15 +99,14 @@ describe("Test deploying app", () => { cy.get('#id_path').clear().type(app_path) cy.get('#submit-id-submit').contains('Submit').click() // check that the app was created - cy.get('tr:contains("' + app_name_project + '")').find('span').should('contain', 'Running') - cy.get('tr:contains("' + app_name_project + '")').find('span').should('contain', 'project') + verifyAppStatus(app_name_project, "Running", "project") // check that the app is not visible under public apps cy.visit('/apps/') cy.get('h3').should('contain', 'Public apps') - cy.get('h5.card-title').should('not.exist') + cy.contains('h5.card-title', app_name_project).should('not.exist') // make this app public as an update and check that it works - cy.log("Now making the project app public") + cy.logf("Now making the project app public", Cypress.currentTest) cy.visit("/projects/") cy.contains('.card-title', project_name).parents('.card-body').siblings('.card-footer').find('a:contains("Open")').first().click() cy.get('tr:contains("' + app_name_project + '")').find('i.bi-three-dots-vertical').click() @@ -93,17 +114,21 @@ describe("Test deploying app", () => { cy.get('#id_access').select('Public') cy.get('#id_source_code_url').type(app_source_code_public) cy.get('#submit-id-submit').contains('Submit').click() - cy.get('tr:contains("' + app_name_project + '")').find('span').should('contain', 'Running') - cy.get('tr:contains("' + app_name_project + '")').find('span').should('contain', 'public') + verifyAppStatus(app_name_project, "Running", "public") - cy.log("Now deleting the project app (by now public)") + // Wait for 5 seconds and check the app status again + cy.wait(5000).then(() => { + verifyAppStatus(app_name_project, "Running", "public") + }) + + cy.logf("Now deleting the project app (by now public)", Cypress.currentTest) cy.get('tr:contains("' + app_name_project + '")').find('i.bi-three-dots-vertical').click() cy.get('tr:contains("' + app_name_project + '")').find('a.confirm-delete').click() cy.get('button').contains('Delete').click() - cy.get('tr:contains("' + app_name_project + '")').find('span').should('contain', 'Deleted') + verifyAppStatus(app_name_project, "Deleted", "") // Create a public app and verify that it is displayed on the public apps page - cy.log("Now creating a public app") + cy.logf("Now creating a public app", Cypress.currentTest) cy.get('div.card-body:contains("' + app_type + '")').find('a:contains("Create")').click() cy.get('#id_name').type(app_name_public) cy.get('#id_description').type(app_description) @@ -115,15 +140,19 @@ describe("Test deploying app", () => { cy.get('#id_volume').select(volume_display_text) cy.get('#submit-id-submit').contains('Submit').click() - cy.get('tr:contains("' + app_name_public + '")').find('span').should('contain', 'Running') - cy.get('tr:contains("' + app_name_public + '")').find('span').should('contain', 'public') + verifyAppStatus(app_name_public, "Running", "public") + + // Wait for 5 seconds and check the app status again + cy.wait(5000).then(() => { + verifyAppStatus(app_name_public, "Running", "public") + }) cy.visit("/apps") cy.get('h5.card-title').should('contain', app_name_public) cy.get('.card-text').find('p').should('contain', app_description) // Check that the public app is displayed on the homepage - cy.log("Now checking if the public app is displayed when not logged in.") + cy.logf("Now checking if the public app is displayed when not logged in.", Cypress.currentTest) cy.visit("/home/") cy.get('h5').should('contain', app_name_public) // Log out and check that the public app is still displayed on the homepage @@ -141,7 +170,7 @@ describe("Test deploying app", () => { }) // Check that the logs page opens for the app - cy.log("Now checking that the logs page for the app opens") + cy.logf("Now checking that the logs page for the app opens", Cypress.currentTest) cy.visit("/projects/") cy.contains('.card-title', project_name).parents('.card-body').siblings('.card-footer').find('a:contains("Open")').first().click() cy.get('tr:contains("' + app_name_public + '")').find('i.bi-three-dots-vertical').click() @@ -149,7 +178,7 @@ describe("Test deploying app", () => { cy.get('h3').should('contain', "Logs") // Try changing the name, description, etc. of the app and verify it works - cy.log("Now changing the name and description of the public app") + cy.logf("Now changing the name and description of the public app", Cypress.currentTest) cy.visit("/projects/") cy.contains('.card-title', project_name).parents('.card-body').siblings('.card-footer').find('a:contains("Open")').first().click() cy.get('tr:contains("' + app_name_public + '")').find('i.bi-three-dots-vertical').click() @@ -171,8 +200,15 @@ describe("Test deploying app", () => { cy.get('#id_path').should('have.value', app_path) cy.get('#id_path').clear().type(app_path_2) cy.get('#submit-id-submit').contains('Submit').click() - cy.get('tr:contains("' + app_name_public_2 + '")').find('span').should('contain', 'link') - cy.get('tr:contains("' + app_name_public_2 + '")').find('span').should('contain', 'Running') // NB: it will get status "Running" but it won't work because the new port is incorrect + + // NB: it will get status "Running" but it won't work because the new port is incorrect + verifyAppStatus(app_name_public_2, "Running", "link") + + // Wait for 5 seconds and check the app status again + cy.wait(5000).then(() => { + verifyAppStatus(app_name_public_2, "Running", "link") + }) + // Check that the changes were saved cy.visit("/projects/") cy.contains('.card-title', project_name).parents('.card-body').siblings('.card-footer').find('a:contains("Open")').first().click() @@ -187,24 +223,27 @@ describe("Test deploying app", () => { cy.get('#id_path').should('have.value', app_path_2) // Remove the created public app and verify that it is deleted from public apps page - cy.log("Now deleting the public app") + cy.logf("Now deleting the public app", Cypress.currentTest) cy.visit("/projects/") cy.contains('.card-title', project_name).parents('.card-body').siblings('.card-footer').find('a:contains("Open")').first().click() cy.get('tr:contains("' + app_name_public_2 + '")').find('i.bi-three-dots-vertical').click() cy.get('tr:contains("' + app_name_public_2 + '")').find('a.confirm-delete').click() cy.get('button').contains('Delete').click() - cy.get('tr:contains("' + app_name_public_2 + '")').find('span').should('contain', 'Deleted') + verifyAppStatus(app_name_public_2, "Deleted", "") + + // check that the app is not visible under public apps cy.visit("/apps") cy.get("title").should("have.text", "Apps | SciLifeLab Serve (beta)") cy.get('h3').should('contain', 'Public apps') - cy.get('h5.card-title').should('not.exist') + cy.contains('h5.card-title', app_name_public_2).should('not.exist') + } else { - cy.log('Skipped because create_resources is not true'); + cy.logf('Skipped because create_resources is not true', Cypress.currentTest); } }) // This test is skipped because it will only work against a Serve instance running on our cluster. should be switched on for the e2e tests against remote. - it.skip("can deploy a shiny app", { defaultCommandTimeout: 100000 }, () => { + it.skip("can deploy a shiny app", { defaultCommandTimeout: defaultCmdTimeoutMs }, () => { // Names of objects to create const project_name = "e2e-deploy-app-test" const app_name = "e2e-shiny-example" @@ -216,7 +255,7 @@ describe("Test deploying app", () => { const app_type = "Shiny App" if (createResources === true) { - cy.log("Creating a shiny app") + cy.logf("Creating a shiny app", Cypress.currentTest) cy.visit("/projects/") cy.contains('.card-title', project_name).parents('.card-body').siblings('.card-footer').find('a:contains("Open")').first().click() cy.get('div.card-body:contains("' + app_type + '")').find('a:contains("Create")').click() @@ -230,7 +269,7 @@ describe("Test deploying app", () => { // cy.get('tr:contains("' + app_name + '")').find('span').should('contain', 'Running') // for now commented out because it takes shinyproxy a really long time to start up and therefore status "Running" can take 5 minutes to show up cy.get('tr:contains("' + app_name + '")').find('span').should('contain', 'public') - cy.log("Checking that all shiny app settings were saved") + cy.logf("Checking that all shiny app settings were saved", Cypress.currentTest) cy.visit("/projects/") cy.contains('.card-title', project_name).parents('.card-body').siblings('.card-footer').find('a:contains("Open")').first().click() cy.get('tr:contains("' + app_name + '")').find('i.bi-three-dots-vertical').click() @@ -241,42 +280,45 @@ describe("Test deploying app", () => { cy.get('#id_image').should('have.value', image_name) cy.get('#id_port').should('have.value', image_port) - cy.log("Checking that the shiny app is displayed on the public apps page") + cy.logf("Checking that the shiny app is displayed on the public apps page", Cypress.currentTest) cy.visit("/apps") cy.get('h5.card-title').should('contain', app_name) cy.get('.card-text').find('p').should('contain', app_description) - cy.log("Checking that instructions for running the app locally are displayed on public apps page") + cy.logf("Checking that instructions for running the app locally are displayed on public apps page", Cypress.currentTest) cy.get('a[data-bs-target="#dockerInfoModal"]').click() cy.get('div#dockerInfoModal').should('be.visible') cy.get('code').first().should('contain', image_name) cy.get('code').first().should('contain', image_port) cy.get('div.modal-footer').find('button').contains('Close').click() - cy.log("Checking that source code URL is displayed on the public apps page") + cy.logf("Checking that source code URL is displayed on the public apps page", Cypress.currentTest) cy.visit("/apps") cy.get('a#source-code-url').should('have.attr', 'href', source_code_url) - cy.log("Deleting the shiny app") + cy.logf("Deleting the shiny app", Cypress.currentTest) cy.visit("/projects/") cy.contains('.card-title', project_name).parents('.card-body').siblings('.card-footer').find('a:contains("Open")').first().click() cy.get('tr:contains("' + app_name + '")').find('i.bi-three-dots-vertical').click() cy.get('tr:contains("' + app_name + '")').find('a.confirm-delete').click() cy.get('button').contains('Delete').click() cy.get('tr:contains("' + app_name + '")').find('span').should('contain', 'Deleted') + // check that the app is not visible under public apps cy.visit("/apps") cy.get("title").should("have.text", "Apps | SciLifeLab Serve (beta)") cy.get('h3').should('contain', 'Public apps') - cy.get('h5.card-title').should('not.exist') + cy.contains('h5.card-title', app_name).should('not.exist') + } else { - cy.log('Skipped because create_resources is not true'); + cy.logf('Skipped because create_resources is not true', Cypress.currentTest); } }) - it("can deploy a dash app", { defaultCommandTimeout: 100000 }, () => { + it("can deploy a dash app", { defaultCommandTimeout: defaultCmdTimeoutMs }, () => { + // Simple test to create and delete a Dash app // Names of objects to create const project_name = "e2e-deploy-app-test" - let app_name = "e2e-dash-example" + const app_name = "e2e-dash-example" const app_description = "e2e-dash-description" const source_code_url = "https://doi.org/example" const image_name = "ghcr.io/scilifelabdatacentre/dash-covid-in-sweden:20240117-063059" @@ -286,7 +328,7 @@ describe("Test deploying app", () => { if (createResources === true) { // Create Dash app - cy.log("Creating a dash app") + cy.logf("Creating a dash app", Cypress.currentTest) cy.visit("/projects/") cy.contains('.card-title', project_name).parents('.card-body').siblings('.card-footer').find('a:contains("Open")').first().click() cy.get('div.card-body:contains("' + app_type + '")').find('a:contains("Create")').click() @@ -297,11 +339,14 @@ describe("Test deploying app", () => { cy.get('#id_image').clear().type(image_name) cy.get('#id_port').clear().type(image_port) cy.get('#submit-id-submit').contains('Submit').click() - cy.get('tr:contains("' + app_name + '")').find('span').should('contain', 'Running') - cy.get('tr:contains("' + app_name + '")').find('span').should('contain', 'public') + // Back on project page + cy.url().should("not.include", "/apps/settings") + cy.get('h3').should('have.text', project_name); + // check that the app was created + verifyAppStatus(app_name, "Running", "public") // Verify Dash app values - cy.log("Checking that all dash app settings were saved") + cy.logf("Checking that all dash app settings were saved", Cypress.currentTest) cy.visit("/projects/") cy.contains('.card-title', project_name).parents('.card-body').siblings('.card-footer').find('a:contains("Open")').first().click() cy.get('tr:contains("' + app_name + '")').find('i.bi-three-dots-vertical').click() @@ -312,37 +357,27 @@ describe("Test deploying app", () => { cy.get('#id_image').should('have.value', image_name) cy.get('#id_port').should('have.value', image_port) - // Edit Dash app - cy.log("Editing the dash app settings (non redeployment fields)") - cy.visit("/projects/") - cy.contains('.card-title', project_name).parents('.card-body').siblings('.card-footer').find('a:contains("Open")').first().click() - cy.get('tr:contains("' + app_name + '")').find('i.bi-three-dots-vertical').click() - cy.get('tr:contains("' + app_name + '")').find('a').contains('Settings').click() - app_name = app_name + "-edited" - cy.get('#id_name').type("-edited") - cy.get('#submit-id-submit').contains('Submit').click() - // Verify that the app status still equals Running - cy.get('tr:contains("' + app_name + '")').find('span').should('contain', 'Running') - cy.get('tr:contains("' + app_name + '")').find('span').should('contain', 'public') - // Delete the Dash app - cy.log("Deleting the dash app") + cy.logf("Deleting the dash app", Cypress.currentTest) cy.visit("/projects/") cy.contains('.card-title', project_name).parents('.card-body').siblings('.card-footer').find('a:contains("Open")').first().click() cy.get('tr:contains("' + app_name + '")').find('i.bi-three-dots-vertical').click() cy.get('tr:contains("' + app_name + '")').find('a.confirm-delete').click() cy.get('button').contains('Delete').click() - cy.get('tr:contains("' + app_name + '")').find('span').should('contain', 'Deleted') - cy.visit("/apps") + verifyAppStatus(app_name, "Deleted", "") + + // check that the app is not visible under public apps + cy.visit('/apps/') cy.get("title").should("have.text", "Apps | SciLifeLab Serve (beta)") cy.get('h3').should('contain', 'Public apps') - cy.get('h5.card-title').should('not.exist') + cy.contains('h5.card-title', app_name).should('not.exist') + } else { - cy.log('Skipped because create_resources is not true'); + cy.logf('Skipped because create_resources is not true', Cypress.currentTest); } }) - it("can deploy a tissuumaps app", { defaultCommandTimeout: 100000 }, () => { + it("can deploy a tissuumaps app", { defaultCommandTimeout: defaultCmdTimeoutMs }, () => { // Names of objects to create const project_name = "e2e-deploy-app-test" const app_name = "e2e-tissuumaps-example" @@ -356,17 +391,22 @@ describe("Test deploying app", () => { cy.visit("/projects/") cy.contains('.card-title', project_name).parents('.card-body').siblings('.card-footer').find('a:contains("Open")').first().click() - cy.log("Creating a tisuumaps app") + cy.logf("Creating a tisuumaps app", Cypress.currentTest) cy.get('div.card-body:contains("' + app_type + '")').find('a:contains("Create")').click() cy.get('#id_name').type(app_name) cy.get('#id_description').type(app_description) cy.get('#id_access').select('Public') cy.get('#id_volume').select(volume_display_text) cy.get('#submit-id-submit').contains('Submit').click() - cy.get('tr:contains("' + app_name + '")').find('span').should('contain', 'Running') - cy.get('tr:contains("' + app_name + '")').find('span').should('contain', 'public') - cy.log("Checking that all tissuumaps app settings were saved") + verifyAppStatus(app_name, "Running", "public") + + // Wait for 5 seconds and check the app status again + cy.wait(5000).then(() => { + verifyAppStatus(app_name, "Running", "public") + }) + + cy.logf("Checking that all tissuumaps app settings were saved", Cypress.currentTest) cy.visit("/projects/") cy.contains('.card-title', project_name).parents('.card-body').siblings('.card-footer').find('a:contains("Open")').first().click() cy.get('tr:contains("' + app_name + '")').find('i.bi-three-dots-vertical').click() @@ -376,23 +416,209 @@ describe("Test deploying app", () => { cy.get('#id_access').find(':selected').should('contain', 'Public') cy.get('#id_volume').find(':selected').should('contain', 'project-vol') - cy.log("Deleting the tissuumaps app") + cy.logf("Deleting the tissuumaps app", Cypress.currentTest) cy.visit("/projects/") cy.contains('.card-title', project_name).parents('.card-body').siblings('.card-footer').find('a:contains("Open")').first().click() cy.get('tr:contains("' + app_name + '")').find('i.bi-three-dots-vertical').click() cy.get('tr:contains("' + app_name + '")').find('a.confirm-delete').click() cy.get('button').contains('Delete').click() - cy.get('tr:contains("' + app_name + '")').find('span').should('contain', 'Deleted') - cy.visit("/apps") + verifyAppStatus(app_name, "Deleted", "") + + // check that the app is not visible under public apps + cy.visit('/apps/') cy.get("title").should("have.text", "Apps | SciLifeLab Serve (beta)") cy.get('h3').should('contain', 'Public apps') - cy.get('h5.card-title').should('not.exist') + cy.contains('h5.card-title', app_name).should('not.exist') + } else { - cy.log('Skipped because create_resources is not true'); + cy.logf('Skipped because create_resources is not true', Cypress.currentTest); } }) - it("can set and change custom subdomain", { defaultCommandTimeout: 100000 }, () => { + it("can modify app settings resulting in no k8s redeployment shows correct app status", { defaultCommandTimeout: defaultCmdTimeoutMs }, () => { + // An advanced test to verify user can modify app settings such as the name and description + // Names of objects to create + const project_name = "e2e-deploy-app-test" + const app_name = "e2e-change-app-settings-no-redeploy" + const app_name_edited = app_name + "-edited" + const app_description = "e2e-change-app-settings-description" + const source_code_url = "https://doi.org/example" + const image_name = "ghcr.io/scilifelabdatacentre/dash-covid-in-sweden:20240117-063059" + const image_port = "8000" + const createResources = Cypress.env('create_resources'); + const app_type = "Dash App" + + if (createResources === true) { + // Create Dash app + cy.logf("Creating a dash app", Cypress.currentTest) + cy.visit("/projects/") + cy.contains('.card-title', project_name).parents('.card-body').siblings('.card-footer').find('a:contains("Open")').first().click() + cy.get('div.card-body:contains("' + app_type + '")').find('a:contains("Create")').click() + cy.get('#id_name').type(app_name) + cy.get('#id_description').type(app_description) + cy.get('#id_access').select('Public') + cy.get('#id_source_code_url').type(source_code_url) + cy.get('#id_image').clear().type(image_name) + cy.get('#id_port').clear().type(image_port) + cy.get('#submit-id-submit').contains('Submit').click() + // Back on project page + cy.url().should("not.include", "/apps/settings") + cy.get('h3').should('have.text', project_name); + // check that the app was created + verifyAppStatus(app_name, "Running", "public") + + // Verify Dash app values + cy.logf("Checking that all dash app settings were saved", Cypress.currentTest) + cy.visit("/projects/") + cy.contains('.card-title', project_name).parents('.card-body').siblings('.card-footer').find('a:contains("Open")').first().click() + cy.get('tr:contains("' + app_name + '")').find('i.bi-three-dots-vertical').click() + cy.get('tr:contains("' + app_name + '")').find('a').contains('Settings').click() + cy.get('#id_name').should('have.value', app_name) + cy.get('#id_description').should('have.value', app_description) + cy.get('#id_access').find(':selected').should('contain', 'Public') + cy.get('#id_image').should('have.value', image_name) + cy.get('#id_port').should('have.value', image_port) + + // Edit Dash app: modify the app name and description + cy.logf("Editing the dash app settings (non redeployment fields)", Cypress.currentTest) + cy.visit("/projects/") + cy.contains('.card-title', project_name).parents('.card-body').siblings('.card-footer').find('a:contains("Open")').first().click() + cy.get('tr:contains("' + app_name + '")').find('i.bi-three-dots-vertical').click() + cy.get('tr:contains("' + app_name + '")').find('a').contains('Settings').click() + // Here we change the app name from app_name to app_name_edited + cy.get('#id_name').type("-edited") + cy.get('#id_description').type(", edited description.") + cy.get('#submit-id-submit').contains('Submit').click() + // Back on project page + cy.url().should("not.include", "/apps/settings") + cy.get('h3').should('have.text', project_name); + // Verify that the app status still equals Running + verifyAppStatus(app_name_edited, "Running", "public") + + // Wait for 20 seconds and check the app status again + // This is a brittle part of the test, therefore we wait a longer time to see if the status (incorrectly) changes + cy.wait(20000).then(() => { + verifyAppStatus(app_name_edited, "Running", "public") + }) + + // Delete the Dash app + cy.logf("Deleting the dash app", Cypress.currentTest) + cy.visit("/projects/") + cy.contains('.card-title', project_name).parents('.card-body').siblings('.card-footer').find('a:contains("Open")').first().click() + cy.get('tr:contains("' + app_name_edited + '")').find('i.bi-three-dots-vertical').click() + cy.get('tr:contains("' + app_name_edited + '")').find('a.confirm-delete').click() + cy.get('button').contains('Delete').click() + verifyAppStatus(app_name_edited, "Deleted", "") + + // check that the app is not visible under public apps + cy.visit('/apps/') + cy.get("title").should("have.text", "Apps | SciLifeLab Serve (beta)") + cy.get('h3').should('contain', 'Public apps') + cy.contains('h5.card-title', app_name_edited).should('not.exist') + + } else { + cy.logf('Skipped because create_resources is not true', Cypress.currentTest); + } + }) + + it("can modify app settings resulting in k8s redeployment shows correct app status", { defaultCommandTimeout: defaultCmdTimeoutMs }, () => { + // An advanced test to verify user can modify app settings resulting in k8s redeployment (image) + // still shows the correct app status. + // Names of objects to create + const project_name = "e2e-deploy-app-test" + const app_name = "e2e-change-app-settings-redeploy" + const app_description = "e2e-change-app-settings-description" + const source_code_url = "https://doi.org/example" + const image_name = "ghcr.io/scilifelabdatacentre/dash-covid-in-sweden:20240117-063059" + const image_port = "8000" + const createResources = Cypress.env('create_resources'); + const app_type = "Dash App" + + if (createResources === true) { + // Create Dash app + cy.logf("Creating a dash app", Cypress.currentTest) + cy.visit("/projects/") + cy.contains('.card-title', project_name).parents('.card-body').siblings('.card-footer').find('a:contains("Open")').first().click() + cy.get('div.card-body:contains("' + app_type + '")').find('a:contains("Create")').click() + cy.get('#id_name').type(app_name) + cy.get('#id_description').type(app_description) + cy.get('#id_access').select('Public') + cy.get('#id_source_code_url').type(source_code_url) + cy.get('#id_image').clear().type(image_name) + cy.get('#id_port').clear().type(image_port) + cy.get('#submit-id-submit').contains('Submit').click() + // Back on project page + cy.url().should("not.include", "/apps/settings") + cy.get('h3').should('have.text', project_name); + // check that the app was created + verifyAppStatus(app_name, "Running", "public") + + // Verify Dash app values + cy.logf("Checking that all dash app settings were saved", Cypress.currentTest) + cy.visit("/projects/") + cy.contains('.card-title', project_name).parents('.card-body').siblings('.card-footer').find('a:contains("Open")').first().click() + cy.get('tr:contains("' + app_name + '")').find('i.bi-three-dots-vertical').click() + cy.get('tr:contains("' + app_name + '")').find('a').contains('Settings').click() + cy.get('#id_name').should('have.value', app_name) + cy.get('#id_description').should('have.value', app_description) + cy.get('#id_access').find(':selected').should('contain', 'Public') + cy.get('#id_image').should('have.value', image_name) + cy.get('#id_port').should('have.value', image_port) + + // Edit Dash app: modify the app image to an invalid image + cy.logf("Editing the dash app settings field Image to an invalid value", Cypress.currentTest) + cy.visit("/projects/") + cy.contains('.card-title', project_name).parents('.card-body').siblings('.card-footer').find('a:contains("Open")').first().click() + cy.get('tr:contains("' + app_name + '")').find('i.bi-three-dots-vertical').click() + cy.get('tr:contains("' + app_name + '")').find('a').contains('Settings').click() + cy.get('#id_image').type("-BAD") + cy.get('#submit-id-submit').contains('Submit').click() + // Back on project page + cy.url().should("not.include", "/apps/settings") + cy.get('h3').should('have.text', project_name); + // Verify that the app status now equals Image Error + verifyAppStatus(app_name, "Image Error", "public") + + // Edit Dash app: modify the app image back to a valid image + cy.logf("Editing the dash app settings field Image to a valid value", Cypress.currentTest) + cy.visit("/projects/") + cy.contains('.card-title', project_name).parents('.card-body').siblings('.card-footer').find('a:contains("Open")').first().click() + cy.get('tr:contains("' + app_name + '")').find('i.bi-three-dots-vertical').click() + cy.get('tr:contains("' + app_name + '")').find('a').contains('Settings').click() + cy.get('#id_image').clear().type(image_name) + cy.get('#submit-id-submit').contains('Submit').click() + // Back on project page + cy.url().should("not.include", "/apps/settings") + cy.get('h3').should('have.text', project_name); + // Verify that the app status now equals Running + verifyAppStatus(app_name, "Running", "public") + + // Wait for 5 seconds and check the app status again + cy.wait(5000).then(() => { + verifyAppStatus(app_name, "Running", "public") + }) + + // Delete the Dash app + cy.logf("Deleting the dash app", Cypress.currentTest) + cy.visit("/projects/") + cy.contains('.card-title', project_name).parents('.card-body').siblings('.card-footer').find('a:contains("Open")').first().click() + cy.get('tr:contains("' + app_name + '")').find('i.bi-three-dots-vertical').click() + cy.get('tr:contains("' + app_name + '")').find('a.confirm-delete').click() + cy.get('button').contains('Delete').click() + verifyAppStatus(app_name, "Deleted", "") + + // check that the app is not visible under public apps + cy.visit('/apps/') + cy.get("title").should("have.text", "Apps | SciLifeLab Serve (beta)") + cy.get('h3').should('contain', 'Public apps') + cy.contains('h5.card-title', app_name).should('not.exist') + + } else { + cy.logf('Skipped because create_resources is not true', Cypress.currentTest); + } + }) + + it("can set and change custom subdomain", { defaultCommandTimeout: defaultCmdTimeoutMs }, () => { // Names of objects to create const project_name = "e2e-deploy-app-test" const app_name = "e2e-subdomain-example" @@ -411,7 +637,7 @@ describe("Test deploying app", () => { cy.visit("/projects/") cy.contains('.card-title', project_name).parents('.card-body').siblings('.card-footer').find('a:contains("Open")').first().click() // Create an app and set a custom subdomain for it - cy.log("Now creating an app with a custom subdomain") + cy.logf("Now creating an app with a custom subdomain", Cypress.currentTest) cy.get('div.card-body:contains("' + app_type + '")').find('a:contains("Create")').click() // fill out other fields cy.get('#id_name').type(app_name) @@ -429,7 +655,7 @@ describe("Test deploying app", () => { cy.get('a').contains(app_name).should('have.attr', 'href').and('include', subdomain) // Try using the same subdomain the second time - cy.log("Now trying to create an app with an already taken subdomain") + cy.logf("Now trying to create an app with an already taken subdomain", Cypress.currentTest) cy.get('div.card-body:contains("' + app_type + '")').find('a:contains("Create")').click() cy.get('#id_name').type(app_name_2) @@ -451,7 +677,7 @@ describe("Test deploying app", () => { cy.get('a').contains(app_name_2).should('have.attr', 'href').and('include', subdomain_2) // Change subdomain of a previously created app - cy.log("Now changing subdomain of an already created app") + cy.logf("Now changing subdomain of an already created app", Cypress.currentTest) cy.visit("/projects/") cy.contains('.card-title', project_name).parents('.card-body').siblings('.card-footer').find('a:contains("Open")').first().click() cy.get('tr:contains("' + app_name + '")').find('i.bi-three-dots-vertical').click() @@ -463,12 +689,17 @@ describe("Test deploying app", () => { cy.get('a').contains(app_name).should('have.attr', 'href').and('include', subdomain_3) // Verify that the app status is not Deleted (Deleting and Created ok) - cy.get('tr:contains("' + app_name + '")', {timeout: 5000}).find('span').should('not.contain', 'Deleted') + cy.get('tr:contains("' + app_name + '")').find('span').should('not.contain', 'Deleted') // Finally verify status equals Running - cy.get('tr:contains("' + app_name + '")', {timeout: 100000}).find('span').should('contain', 'Running') + verifyAppStatus(app_name, "Running", "") + + // Wait for 5 seconds and check the app status again + cy.wait(5000).then(() => { + verifyAppStatus(app_name, "Running", "") + }) } else { - cy.log('Skipped because create_resources is not true'); + cy.logf('Skipped because create_resources is not true', Cypress.currentTest); } }) @@ -488,7 +719,7 @@ describe("Test deploying app", () => { cy.contains('.card-title', project_name).parents('.card-body').siblings('.card-footer').find('a:contains("Open")').first().click() // Create an app with project permissions - cy.log("Now creating an app with a non-existent image reference - expecting Image Error") + cy.logf("Now creating an app with a non-existent image reference - expecting Image Error", Cypress.currentTest) cy.get('div.card-body:contains("' + app_type + '")').find('a:contains("Create")').click() cy.get('#id_name').type(app_name_statuses) cy.get('#id_description').type(app_description) @@ -497,16 +728,16 @@ describe("Test deploying app", () => { cy.get('#id_image').type("hkqxqxkhkqwxhkxwh") // input random string cy.get('#submit-id-submit').contains('Submit').click() // Check that the app was created. Using custom timeout of 5 secs - cy.get('tr:contains("' + app_name_statuses + '")', {timeout: 5000}).find('span').should('contain', 'Image Error') - cy.log("Now updating the app to give a correct image reference - expecting Running") + cy.get('tr:contains("' + app_name_statuses + '")').find('span', {timeout: longCmdTimeoutMs}).should('contain', 'Image Error') + cy.logf("Now updating the app to give a correct image reference - expecting Running", Cypress.currentTest) cy.get('tr:contains("' + app_name_statuses + '")').find('i.bi-three-dots-vertical').click() cy.get('tr:contains("' + app_name_statuses + '")').find('a').contains('Settings').click() cy.get('#id_image').clear().type(image_name) cy.get('#submit-id-submit').contains('Submit').click() // Using longer custom timeout for correct image to be set to Running - cy.get('tr:contains("' + app_name_statuses + '")', {timeout: 30000}).find('span').should('contain', 'Running') + cy.get('tr:contains("' + app_name_statuses + '")', {timeout: longCmdTimeoutMs}).find('span', {timeout: longCmdTimeoutMs}).should('contain', 'Running') } else { - cy.log('Skipped because create_resources is not true'); + cy.logf('Skipped because create_resources is not true', Cypress.currentTest); } }) diff --git a/cypress/e2e/ui-tests/test-login-account-handling.cy.js b/cypress/e2e/ui-tests/test-login-account-handling.cy.js index c7df4a23..1312da8e 100644 --- a/cypress/e2e/ui-tests/test-login-account-handling.cy.js +++ b/cypress/e2e/ui-tests/test-login-account-handling.cy.js @@ -3,27 +3,30 @@ describe("Test login, profile page view, password change, password reset", () => let users before(() => { + cy.logf("Begin before() hook", Cypress.currentTest) + // do db reset if needed if (Cypress.env('do_reset_db') === true) { - cy.log("Resetting db state. Running db-reset.sh"); + cy.logf("Resetting db state. Running db-reset.sh", Cypress.currentTest); cy.exec("./cypress/e2e/db-reset.sh"); cy.wait(Cypress.env('wait_db_reset')); } else { - cy.log("Skipping resetting the db state."); + cy.logf("Skipping resetting the db state.", Cypress.currentTest); } // seed the db with a user cy.visit("/") - cy.log("Running seed-login-user.py") + cy.logf("Running seed-login-user.py", Cypress.currentTest) cy.exec("./cypress/e2e/db-seed-login-user.sh") - }) - beforeEach(() => { cy.fixture('users.json').then(function (data) { users = data; - }) + }) + + cy.logf("End before() hook", Cypress.currentTest) }) + it("can login an existing user through the UI when input is valid", () => { cy.visit("accounts/login/") @@ -34,7 +37,7 @@ describe("Test login, profile page view, password change, password reset", () => cy.get("button").contains('Login').click() .then((href) => { - cy.log(href) + cy.logf(href, Cypress.currentTest) cy.url().should("include", "projects") cy.get('h3').should('contain', 'My projects') cy.get('h3').parent().parent().find('p').first().should('not.contain', 'You need to be logged in') diff --git a/cypress/e2e/ui-tests/test-project-as-contributor.cy.js b/cypress/e2e/ui-tests/test-project-as-contributor.cy.js index 5395cb58..541488da 100644 --- a/cypress/e2e/ui-tests/test-project-as-contributor.cy.js +++ b/cypress/e2e/ui-tests/test-project-as-contributor.cy.js @@ -7,29 +7,37 @@ describe("Test project contributor user functionality", () => { let users before(() => { + cy.logf("Begin before() hook", Cypress.currentTest) + // do db reset if needed if (Cypress.env('do_reset_db') === true) { - cy.log("Resetting db state. Running db-reset.sh"); + cy.logf("Resetting db state. Running db-reset.sh", Cypress.currentTest); cy.exec("./cypress/e2e/db-reset.sh"); cy.wait(Cypress.env('wait_db_reset')); } else { - cy.log("Skipping resetting the db state."); + cy.logf("Skipping resetting the db state.", Cypress.currentTest); } // seed the db with a user cy.visit("/") - cy.log("Running seed_contributor.py") + cy.logf("Running seed_contributor.py", Cypress.currentTest) cy.exec("./cypress/e2e/db-seed-contributor.sh") + + cy.logf("End before() hook", Cypress.currentTest) }) beforeEach(() => { + cy.logf("Begin beforeEach() hook", Cypress.currentTest) + // email in fixture must match email in db-reset.sh - cy.log("Logging in as contributor user") + cy.logf("Logging in as contributor user", Cypress.currentTest) cy.fixture('users.json').then(function (data) { users = data cy.loginViaApi(users.contributor.email, users.contributor.password) }) + + cy.logf("End beforeEach() hook", Cypress.currentTest) }) it("can create a new project with default template, open settings, change description, delete from settings", { defaultCommandTimeout: 100000 }, () => { @@ -43,7 +51,7 @@ describe("Test project contributor user functionality", () => { cy.visit("/projects/") cy.get("title").should("have.text", "My projects | SciLifeLab Serve (beta)") - cy.log("Create a new project") + cy.logf("Create a new project", Cypress.currentTest) // Click button for UI to create a new project cy.get("a").contains('New project').click() cy.url().should("include", "projects/templates") @@ -64,32 +72,32 @@ describe("Test project contributor user functionality", () => { cy.get('h3').should('contain', project_name) cy.get('.card-text').should('contain', project_description) - cy.log("Check that the correct deployment options are available") + cy.logf("Check that the correct deployment options are available", Cypress.currentTest) cy.get('.card-header').find('h5').should('contain', 'Develop') cy.get('.card-header').find('h5').should('contain', 'Serve') cy.get('.card-header').find('h5').should('not.contain', 'Models') cy.get('.card-header').find('h5').should('not.contain', 'Additional options [admins only]') - cy.log("Check that project settings are available") + cy.logf("Check that project settings are available", Cypress.currentTest) cy.get('[data-cy="settings"]').click() cy.url().should("include", "settings") cy.get('h3').should('contain', 'Project settings') - cy.log("Check that the correct project settings are visible (i.e. no extra settings)") + cy.logf("Check that the correct project settings are visible (i.e. no extra settings)", Cypress.currentTest) cy.get('.list-group').find('a').should('contain', 'Access') cy.get('.list-group').find('a').should('not.contain', 'S3 storage') cy.get('.list-group').find('a').should('not.contain', 'MLFlow') cy.get('.list-group').find('a').should('not.contain', 'Flavors') cy.get('.list-group').find('a').should('not.contain', 'Environments') - cy.log("Change project description") + cy.logf("Change project description", Cypress.currentTest) cy.get('textarea[name=description]').clear().type(project_description_2) cy.get('button').contains('Save').click() cy.visit("/projects/") cy.contains('.card-title', project_name).parents('.card-body').siblings('.card-footer').find('a:contains("Open")').first().click() cy.get('.card-text').should('contain', project_description_2) - cy.log("Delete the project from the settings menu") + cy.logf("Delete the project from the settings menu", Cypress.currentTest) cy.get('[data-cy="settings"]').click() cy.get('a').contains("Delete").click() .then((href) => { @@ -132,7 +140,7 @@ describe("Test project contributor user functionality", () => { cy.get("input[name=save]").contains('Create project').click() cy.wait(5000) // sometimes it takes a while to create a project .then((href) => { - cy.log(href) + cy.logf(href, Cypress.currentTest) cy.reload() cy.get("title").should("have.text", project_title_name) cy.get('h3').should('contain', project_name) @@ -201,7 +209,7 @@ describe("Test project contributor user functionality", () => { cy.get('div#modalConfirmDelete').should('have.css', 'display', 'block') cy.get("h1#modalConfirmDeleteLabel").then(function($elem) { - cy.log($elem.text()) + cy.logf($elem.text(), Cypress.currentTest) cy.get('div#modalConfirmDeleteFooter').find('button').contains('Delete').click() @@ -223,7 +231,7 @@ describe("Test project contributor user functionality", () => { cy.get("input[name=save]").contains('Create project').click() cy.wait(5000) // sometimes it takes a while to create a project .then((href) => { - cy.log(href) + cy.logf(href, Cypress.currentTest) // Check that the app limits work using Jupyter Lab as example // step 1. create 3 jupyter lab instances (current limit) Cypress._.times(3, () => { @@ -292,39 +300,39 @@ describe("Test project contributor user functionality", () => { // First we need to get a URL of the second user's project // log in as second user - cy.log("Now logging in as the second user") + cy.logf("Now logging in as the second user", Cypress.currentTest) cy.fixture('users.json').then(function (data) { users = data cy.loginViaUI(users.contributor_collaborator.email, users.contributor_collaborator.password) }) // get the second user's project's URL - cy.log("Checking the second user's project URL") + cy.logf("Checking the second user's project URL", Cypress.currentTest) cy.visit('/projects/') cy.get('h5.card-title').should('contain', project_name) // check access to project cy.get('a.btn').contains('Open').click() .then((href) => { - cy.log(href) + cy.logf(href, Cypress.currentTest) let projectURL cy.url().then(url => { projectURL = url }); // Now we can check if the contributor user has access to this project // log back in as contributor user - cy.log("Now logging back in as contributor user") + cy.logf("Now logging back in as contributor user", Cypress.currentTest) cy.fixture('users.json').then(function (data) { users = data cy.loginViaApi(users.contributor.email, users.contributor.password) }) - cy.log("Checking that can't see the project in the list of projects") + cy.logf("Checking that can't see the project in the list of projects", Cypress.currentTest) cy.visit("/projects/") cy.contains('.card-title', project_name).should('not.exist') // cannot see the project - cy.log("Checking that can't open the project using direct URL") + cy.logf("Checking that can't open the project using direct URL", Cypress.currentTest) cy.then(() => cy.request({url: projectURL, failOnStatusCode: false}).its('status').should('equal', 403) // cannot open the project using a direct link ) // Finally, unauthenticated user - cy.log("Logging out the contributor user and testing as an unauthenticated user") + cy.logf("Logging out the contributor user and testing as an unauthenticated user", Cypress.currentTest) cy.clearCookies(); cy.clearLocalStorage(); Cypress.session.clearAllSavedSessions() @@ -342,7 +350,7 @@ describe("Test project contributor user functionality", () => { const app_type = "Jupyter Lab" // Create a project - cy.log("Now creating a project") + cy.logf("Now creating a project", Cypress.currentTest) cy.visit("/projects/") // Click button for UI to create a new project cy.get("a").contains('New project').click() @@ -355,7 +363,7 @@ describe("Test project contributor user functionality", () => { cy.wait(5000) // sometimes it takes a while to create a project // Create private app - cy.log("Now creating a private app") + cy.logf("Now creating a private app", Cypress.currentTest) cy.get('div.card-body:contains("' + app_type + '")').find('a:contains("Create")').click() cy.get('#id_name').type(private_app_name) cy.get('#id_access').select('Private') @@ -363,7 +371,7 @@ describe("Test project contributor user functionality", () => { cy.get('tr:contains("' + private_app_name + '")').find('span').should('contain', 'private') // check that the app got greated // Create project app - cy.log("Now creating a project app") + cy.logf("Now creating a project app", Cypress.currentTest) cy.get('div.card-body:contains("' + app_type + '")').find('a:contains("Create")').click() cy.get('#id_name').type(project_app_name) cy.get('#id_access').select('Project') @@ -371,7 +379,7 @@ describe("Test project contributor user functionality", () => { cy.get('tr:contains("' + project_app_name + '")').find('span').should('contain', 'project') // check that the app got greated // Give access to this project to a collaborator user - cy.log("Now giving access to another user") + cy.logf("Now giving access to another user", Cypress.currentTest) // Go to project settings cy.get('[data-cy="settings"]').click() cy.get('a[href="#access"]').click() @@ -387,14 +395,14 @@ describe("Test project contributor user functionality", () => { // Log out step is not needed because cypress sessions take care of that // Log in as contributor's collaborator user - cy.log("Now logging in as contributor's collaborator user") + cy.logf("Now logging in as contributor's collaborator user", Cypress.currentTest) cy.fixture('users.json').then(function (data) { users = data cy.loginViaUI(users.contributor_collaborator.email, users.contributor_collaborator.password) }) // Check that the contributor's collaborator user has correct access - cy.log("Now checking access to project and apps") + cy.logf("Now checking access to project and apps", Cypress.currentTest) cy.visit('/projects/') cy.get('h5.card-title').should('contain', project_name_access) // check access to project cy.contains('.card-title', project_name_access).parents('.card-body').siblings('.card-footer').find('a:contains("Open")').first().click() @@ -404,14 +412,14 @@ describe("Test project contributor user functionality", () => { // to be added: go to URL and check that it opens successfully // Log back in as contributor user - cy.log("Now logging back in as contributor user") + cy.logf("Now logging back in as contributor user", Cypress.currentTest) cy.fixture('users.json').then(function (data) { users = data cy.loginViaApi(users.contributor.email, users.contributor.password) }) // Remove access to the project - cy.log("Now removing access from contributor's collaborator user") + cy.logf("Now removing access from contributor's collaborator user", Cypress.currentTest) cy.visit('/projects/') cy.contains('.card-title', project_name_access).parents('.card-body').siblings('.card-footer').find('a:contains("Open")').first().click() cy.get('[data-cy="settings"]').click() @@ -422,27 +430,27 @@ describe("Test project contributor user functionality", () => { }) // Log in as contributor's collaborator user - cy.log("Now again logging in as contributor's collaborator user") + cy.logf("Now again logging in as contributor's collaborator user", Cypress.currentTest) cy.fixture('users.json').then(function (data) { users = data cy.loginViaUI(users.contributor_collaborator.email, users.contributor_collaborator.password) }) // Check that the contributor's collaborator user no longer has access to the project - cy.log("Now checking that contributor's collaborator user no longer has access") + cy.logf("Now checking that contributor's collaborator user no longer has access", Cypress.currentTest) cy.visit('/projects/') cy.contains('.card-title', project_name_access).should('not.exist') // check visibility of project // to-do: save the url of the project in a previous step and check if possible to open that with a direct link // Log back in as contributor user - cy.log("Now logging back in as contributor user") + cy.logf("Now logging back in as contributor user", Cypress.currentTest) cy.fixture('users.json').then(function (data) { users = data cy.loginViaApi(users.contributor.email, users.contributor.password) }) // Delete the created project - cy.log("Now deleting the created project") + cy.logf("Now deleting the created project", Cypress.currentTest) cy.visit("/projects/") cy.contains('.card-title', project_name_access).parents('.card-body').siblings('.card-footer').find('.confirm-delete').click() .then((href) => { @@ -456,10 +464,10 @@ describe("Test project contributor user functionality", () => { it("can create a file management instance", { defaultCommandTimeout: 100000 }, () => { const project_name = "e2e-create-proj-test" - cy.log("Creating a blank project") + cy.logf("Creating a blank project", Cypress.currentTest) cy.createBlankProject(project_name) - cy.log("Activating file managing tools") + cy.logf("Activating file managing tools", Cypress.currentTest) cy.visit("/projects/") cy.contains('.card-title', project_name).parents('.card-body').siblings('.card-footer').find('a:contains("Open")').first().click() diff --git a/cypress/e2e/ui-tests/test-public-webpages.cy.js b/cypress/e2e/ui-tests/test-public-webpages.cy.js index cd82036f..574547b9 100644 --- a/cypress/e2e/ui-tests/test-public-webpages.cy.js +++ b/cypress/e2e/ui-tests/test-public-webpages.cy.js @@ -1,8 +1,9 @@ describe("Tests of the public pages of the website", () => { beforeEach(() => { - + cy.logf("Begin beforeEach() hook", Cypress.currentTest) cy.visit("/") + cy.logf("End beforeEach() hook", Cypress.currentTest) }) it("should open the home page on link click", () => { @@ -15,7 +16,22 @@ describe("Tests of the public pages of the website", () => { cy.url().should("include", "/apps") cy.get('h3').should('contain', 'Public apps') cy.get("title").should("have.text", "Apps | SciLifeLab Serve (beta)") - cy.get('p').should('contain', 'No public apps available.') + + if (Cypress.env('do_reset_db') === true) { + // This test was flaky before as other test failures could make this test fail as well + cy.get('p').should('contain', 'No public apps available.') + } else { + cy.get('h3').then($parent => { + if ($parent.find("span.ghost-number").length > 0) { + cy.get('span.ghost-number').then(($element) => { + // There are public apps and the text must be an integer + const text = $element.text().trim(); + const isInteger = Number.isInteger(Number(text)); + expect(isInteger).to.be.true; + }); + } + }); + } }) it("should open the Models page on link click", () => { @@ -40,9 +56,9 @@ describe("Tests of the public pages of the website", () => { it("should open the login page on link click", () => { cy.get("li.nav-item a").contains("Log in").click() cy.url().should("include", "accounts/login") - }) + }) it("should have proper title", () => { - cy.get("title").should("have.text", "Home | SciLifeLab Serve (beta)") + cy.get("title").should("have.text", "Home | SciLifeLab Serve (beta)") }) }) diff --git a/cypress/e2e/ui-tests/test-signup.cy.js b/cypress/e2e/ui-tests/test-signup.cy.js index 57350391..d0bb31bc 100644 --- a/cypress/e2e/ui-tests/test-signup.cy.js +++ b/cypress/e2e/ui-tests/test-signup.cy.js @@ -2,30 +2,39 @@ describe("Test sign up", () => { let users + before(() => { + cy.logf("Begin before() hook", Cypress.currentTest) + + cy.fixture('users.json').then(function (data) { + users = data; + }) + + cy.logf("End before() hook", Cypress.currentTest) + }) + beforeEach(() => { + cy.logf("Begin beforeEach() hook", Cypress.currentTest) + // reset and seed the database prior to every test if (Cypress.env('do_reset_db') === true) { - cy.log("Resetting db state. Running db-reset.sh"); + cy.logf("Resetting db state. Running db-reset.sh", Cypress.currentTest); cy.exec("./cypress/e2e/db-reset.sh"); cy.wait(Cypress.env('wait_db_reset')); } else { - cy.log("Skipping resetting the db state."); + cy.logf("Skipping resetting the db state.", Cypress.currentTest); } - }) - beforeEach(() => { - cy.fixture('users.json').then(function (data) { - users = data; - }) + cy.logf("End beforeEach() hook", Cypress.currentTest) }) + it("can create new user account with valid form input", () => { cy.visit("/signup/"); cy.get("title").should("have.text", "Register | SciLifeLab Serve (beta)") - cy.log("Creating user account with valid input") + cy.logf("Creating user account with valid input", Cypress.currentTest) cy.get('input[name=email]').type(users.signup_user.email); cy.get('input[name=first_name]').type("first name"); cy.get('input[name=last_name]').type("last name"); @@ -50,16 +59,16 @@ describe("Test sign up", () => { // HTML form checks cy.visit("/signup/"); - cy.log("First name is a required field in the HTML form") + cy.logf("First name is a required field in the HTML form", Cypress.currentTest) cy.get('input[name=first_name]').invoke('prop', 'validationMessage').should('equal', 'Please fill out this field.') - cy.log("Last name is a required field in the HTML form") + cy.logf("Last name is a required field in the HTML form", Cypress.currentTest) cy.get('input[name=last_name]').invoke('prop', 'validationMessage').should('equal', 'Please fill out this field.') - cy.log("Department is not a required field in the HTML form") + cy.logf("Department is not a required field in the HTML form", Cypress.currentTest) cy.get('input[name=department]').invoke('prop', 'validationMessage').should('not.equal', 'Please fill out this field.') // department is not a required field because those without uni affiliation do not need to fill it out // Backend checks - cy.log("User without uni email asked for additional info by front and backend") + cy.logf("User without uni email asked for additional info by front and backend", Cypress.currentTest) cy.visit("/signup/"); cy.get('[id="id_request_account_info"]').should('have.class', 'hidden') cy.get('input[name=email]').type("test-email@test.se"); // non-uni email @@ -77,7 +86,7 @@ describe("Test sign up", () => { cy.get('input[name=email]').clear().type("test-email@uu.se"); cy.get('[id="id_request_account_info"]').should('have.class', 'hidden') - cy.log("Invalid email rejected by the backend") + cy.logf("Invalid email rejected by the backend", Cypress.currentTest) cy.visit("/signup/"); cy.get('input[name=email]').type("test-email@test"); cy.get('input[name=first_name]').type("first name"); @@ -87,7 +96,7 @@ describe("Test sign up", () => { cy.get("input#submit-id-save").click(); cy.get('[id="validation_email"]').should('exist') - cy.log("Mismatching email and affiliation rejected by the backend") + cy.logf("Mismatching email and affiliation rejected by the backend", Cypress.currentTest) cy.visit("/signup/") cy.get('input[name=first_name]').type("first name"); cy.get('input[name=last_name]').type("last name"); @@ -98,7 +107,7 @@ describe("Test sign up", () => { cy.get("input#submit-id-save").click(); cy.get('[id="validation_affiliation"]').should('exist') - cy.log("Empty department rejected by the backend") + cy.logf("Empty department rejected by the backend", Cypress.currentTest) cy.visit("/signup/") cy.get('input[name=email]').type("test-email@ki.se"); // department becomes a required field for uni emails cy.get('input[name=first_name]').type("first name"); @@ -109,7 +118,7 @@ describe("Test sign up", () => { cy.get("input#submit-id-save").click(); cy.get('[id="validation_department"]').should('exist') - cy.log("Mismatching passwords rejected by the backend") + cy.logf("Mismatching passwords rejected by the backend", Cypress.currentTest) cy.visit("/signup/") cy.get('input[name=email]').type("test-email@test.kth.se"); cy.get('input[name=first_name]').type("first name"); diff --git a/cypress/e2e/ui-tests/test-superuser-functionality.cy.js b/cypress/e2e/ui-tests/test-superuser-functionality.cy.js index fb368fc1..23f73e36 100644 --- a/cypress/e2e/ui-tests/test-superuser-functionality.cy.js +++ b/cypress/e2e/ui-tests/test-superuser-functionality.cy.js @@ -1,34 +1,44 @@ describe("Test superuser access", () => { + // The longer timeout is often used when waiting for k8s operations to complete + const longCmdTimeoutMs = 180000 + // Tests performed as an authenticated user that has superuser privileges // user: no-reply-superuser@scilifelab.se - let users before(() => { + cy.logf("Begin before() hook", Cypress.currentTest) + // do db reset if needed if (Cypress.env('do_reset_db') === true) { - cy.log("Resetting db state. Running db-reset.sh"); + cy.logf("Resetting db state. Running db-reset.sh", Cypress.currentTest); cy.exec("./cypress/e2e/db-reset.sh"); cy.wait(Cypress.env('wait_db_reset')); } else { - cy.log("Skipping resetting the db state."); + cy.logf("Skipping resetting the db state.", Cypress.currentTest); } // seed the db with a user cy.visit("/") - cy.log("Running seed_superuser.py") + cy.logf("Running seed_superuser.py", Cypress.currentTest) cy.exec("./cypress/e2e/db-seed-superuser.sh") + + cy.logf("End before() hook", Cypress.currentTest) }) beforeEach(() => { + cy.logf("Begin beforeEach() hook", Cypress.currentTest) + // email in fixture must match email in db-reset.sh - cy.log("Logging in as superuser") + cy.logf("Logging in as superuser", Cypress.currentTest) cy.fixture('users.json').then(function (data) { users = data cy.loginViaApi(users.superuser.email, users.superuser.password) }) + + cy.logf("End beforeEach() hook", Cypress.currentTest) }) it("can see extra deployment options and extra settings in a project", () => { @@ -40,7 +50,7 @@ describe("Test superuser access", () => { cy.visit("/projects/") cy.get("title").should("have.text", "My projects | SciLifeLab Serve (beta)") - cy.log("Creating a project as a superuser") + cy.logf("Creating a project as a superuser", Cypress.currentTest) // Click button for UI to create a new project cy.get("a").contains('New project').click() cy.url().should("include", "projects/templates") @@ -55,30 +65,30 @@ describe("Test superuser access", () => { cy.get('input[name=name]').type(project_name) cy.get('textarea[name=description]').type(project_description) cy.get("input[name=save]").contains('Create project').click() - cy.wait(5000) // sometimes it takes a while to create a project + //cy.wait(5000) // sometimes it takes a while to create a project. Not needed because of cypress retryability. - cy.get('h3').should('contain', project_name) + cy.get('h3', {timeout: longCmdTimeoutMs}).should('contain', project_name) cy.get('.card-text').should('contain', project_description) - cy.log("Checking that project settings are available") + cy.logf("Checking that project settings are available", Cypress.currentTest) cy.get('[data-cy="settings"]').click() cy.url().should("include", "settings") cy.get('h3').should('contain', 'Project settings') - cy.log("Checking that the correct project settings are visible (i.e. with extra settings)") + cy.logf("Checking that the correct project settings are visible (i.e. with extra settings)", Cypress.currentTest) cy.get('.list-group').find('a').should('contain', 'Access') cy.get('.list-group').find('a').should('contain', 'Flavors') cy.get('.list-group').find('a').should('contain', 'Environments') - cy.log("Changing project description") + cy.logf("Changing project description", Cypress.currentTest) cy.get('textarea[name=description]').clear().type(project_description_2) cy.get('button').contains('Save').click() cy.visit("/projects/") cy.contains('.card-title', project_name).parents('.card-body').siblings('.card-footer').find('a:contains("Open")').first().click() cy.get('.card-text').should('contain', project_description_2) - cy.log("Deleting the project from the settings menu") + cy.logf("Deleting the project from the settings menu", Cypress.currentTest) cy.get('[data-cy="settings"]').click() cy.get('a').contains("Delete").click() .then((href) => { @@ -103,11 +113,11 @@ describe("Test superuser access", () => { const private_app_name = "Regular user's private app" // from seed_superuser.py const private_app_name_2 = "App renamed by superuser" - cy.log("Verifying that a project of a regular user is visible") + cy.logf("Verifying that a project of a regular user is visible", Cypress.currentTest) cy.visit("/projects/") cy.get('h5.card-title').should('contain', project_name) - cy.log("Verifying that can edit the description of a project of a regular user") + cy.logf("Verifying that can edit the description of a project of a regular user", Cypress.currentTest) cy.contains('.card-title', project_name).parents('.card-body').siblings('.card-footer').find('a:contains("Open")').first().click() cy.get('.card-text').should('contain', project_description) cy.get('[data-cy="settings"]').click() @@ -117,27 +127,28 @@ describe("Test superuser access", () => { cy.contains('.card-title', project_name).parents('.card-body').siblings('.card-footer').find('a:contains("Open")').first().click() cy.get('.card-text').should('contain', project_description_2) - cy.log("Verifying that a private app of a regular user is visible") + cy.logf("Verifying that a private app of a regular user is visible", Cypress.currentTest) cy.visit("/projects/") cy.contains('.card-title', project_name).parents('.card-body').siblings('.card-footer').find('a:contains("Open")').first().click() cy.get('tr:contains("' + private_app_name + '")').should('exist') // regular user's private app visible - cy.log("Verifying that can edit the private app of a regular user") + cy.logf("Verifying that can edit the private app of a regular user", Cypress.currentTest) cy.get('tr:contains("' + private_app_name + '")').find('i.bi-three-dots-vertical').click() cy.get('tr:contains("' + private_app_name + '")').find('a').contains('Settings').click() cy.get('#id_name').clear().type(private_app_name_2) // change name cy.get('#submit-id-submit').contains('Submit').click() cy.get('tr:contains("' + private_app_name_2 + '")').should('exist') // regular user's private app now has a different name - cy.wait(10000) - cy.get('tr:contains("' + private_app_name_2 + '")').find('span').should('contain', 'Running') // add this because to make sure the app is running before deleting otherwise it gives an error, - cy.log("Deleting a regular user's private app") + + //cy.wait(10000) // Not needed because of the retryability built into cypress. + cy.get('tr:contains("' + private_app_name_2 + '")', {timeout: longCmdTimeoutMs}).find('span', {timeout: longCmdTimeoutMs}).should('contain', 'Running') // add this because to make sure the app is running before deleting otherwise it gives an error, + cy.logf("Deleting a regular user's private app", Cypress.currentTest) cy.get('tr:contains("' + private_app_name_2 + '")').find('i.bi-three-dots-vertical').click() cy.get('tr:contains("' + private_app_name_2 + '")').find('a.confirm-delete').click() cy.get('button').contains('Delete').click() - cy.wait(5000) - cy.get('tr:contains("' + private_app_name_2 + '")').find('span').should('contain', 'Deleted') + //cy.wait(5000) // Not needed because of the retryability built into cypress. + cy.get('tr:contains("' + private_app_name_2 + '")', {timeout: longCmdTimeoutMs}).find('span', {timeout: longCmdTimeoutMs}).should('contain', 'Deleted') - cy.log("Deleting a regular user's project") + cy.logf("Deleting a regular user's project", Cypress.currentTest) cy.visit("/projects/") cy.contains('.card-title', project_name).parents('.card-body').siblings('.card-footer').find('.confirm-delete').click() .then((href) => { @@ -153,7 +164,7 @@ describe("Test superuser access", () => { const project_name = "e2e-proj-flavor-test" const new_flavor_name = "4 CPU, 8 GB RAM" - cy.log("Logging in as a regular user and creating a project") + cy.logf("Logging in as a regular user and creating a project", Cypress.currentTest) cy.fixture('users.json').then(function (data) { users = data cy.loginViaUI(users.superuser_testuser.email, users.superuser_testuser.password) @@ -163,11 +174,11 @@ describe("Test superuser access", () => { cy.get("a").contains('Create').first().click() cy.get('input[name=name]').type(project_name) cy.get("input[name=save]").contains('Create project').click() - cy.wait(5000) // sometimes it takes a while to create a project + //cy.wait(5000) // sometimes it takes a while to create a project. Not needed because of cypress retryability. cy.get('h3').should('contain', project_name) Cypress.session.clearAllSavedSessions() - cy.log("Logging in as a superuser and creating a new flavor in the regular user's project") + cy.logf("Logging in as a superuser and creating a new flavor in the regular user's project", Cypress.currentTest) cy.fixture('users.json').then(function (data) { users = data cy.loginViaUI(users.superuser.email, users.superuser.password) @@ -184,7 +195,7 @@ describe("Test superuser access", () => { cy.get('button').contains("Create").click() Cypress.session.clearAllSavedSessions() - cy.log("Logging back in as a regular user and using the new flavor for an app") + cy.logf("Logging back in as a regular user and using the new flavor for an app", Cypress.currentTest) const createResources = Cypress.env('create_resources'); if (createResources === true) { @@ -211,7 +222,7 @@ describe("Test superuser access", () => { cy.get('#submit-id-submit').contains('Submit').click() cy.get('tr:contains("' + app_name + '")').find('span').should('contain', 'Running') - cy.log("Changing the flavor setting") + cy.logf("Changing the flavor setting", Cypress.currentTest) cy.visit("/projects/") cy.contains('.card-title', project_name).parents('.card-body').siblings('.card-footer').find('a:contains("Open")').first().click() cy.get('tr:contains("' + app_name + '")').find('i.bi-three-dots-vertical').click() @@ -221,7 +232,7 @@ describe("Test superuser access", () => { cy.get('#submit-id-submit').contains('Submit').click() cy.get('tr:contains("' + app_name + '")').find('span').should('contain', 'Running') - cy.log("Checking that the new flavor setting was saved in the database") + cy.logf("Checking that the new flavor setting was saved in the database", Cypress.currentTest) cy.visit("/projects/") cy.contains('.card-title', project_name).parents('.card-body').siblings('.card-footer').find('a:contains("Open")').first().click() cy.get('tr:contains("' + app_name + '")').find('i.bi-three-dots-vertical').click() @@ -229,10 +240,10 @@ describe("Test superuser access", () => { cy.get('#id_flavor').find(':selected').should('contain', new_flavor_name) } else { - cy.log('Skipped because create_resources is not true'); + cy.logf('Skipped because create_resources is not true', Cypress.currentTest); } - cy.log("Deleting the created project") + cy.logf("Deleting the created project", Cypress.currentTest) cy.visit("/projects/") cy.contains('.card-title', project_name).parents('.card-body').siblings('.card-footer').find('.confirm-delete').click() .then((href) => { @@ -251,10 +262,10 @@ describe("Test superuser access", () => { const project_name_pvc = "e2e-superuser-pvc-test" const volume_name = "e2e-project-vol" - cy.log("Creating a blank project") + cy.logf("Creating a blank project", Cypress.currentTest) cy.createBlankProject(project_name_pvc) - cy.log("Creating a persistent volume") + cy.logf("Creating a persistent volume", Cypress.currentTest) cy.visit("/projects/") cy.contains('.card-title', project_name_pvc).parents('.card-body').siblings('.card-footer').find('a:contains("Open")').first().click() @@ -274,7 +285,7 @@ describe("Test superuser access", () => { cy.get('tr:contains("' + volume_name + '")').find('span').should('contain', 'Deleted') // confirm the volume has been deleted */ - cy.log("Deleting the created project") + cy.logf("Deleting the created project", Cypress.currentTest) cy.visit("/projects/") cy.contains('.card-title', project_name_pvc).parents('.card-body').siblings('.card-footer').find('.confirm-delete').click() .then((href) => { @@ -291,7 +302,7 @@ describe("Test superuser access", () => { // Names of projects to create const project_name = "e2e-superuser-proj-limits-test" - cy.log("Create 10 projects (current limit for regular users)") + cy.logf("Create 10 projects (current limit for regular users)", Cypress.currentTest) Cypress._.times(10, () => { // better to write this out rather than use the createBlankProject command because then we can do a 5000 ms pause only once cy.visit("/projects/") @@ -302,16 +313,16 @@ describe("Test superuser access", () => { }); cy.wait(5000) // sometimes it takes a while to create a project but just waiting once at the end should be enough - cy.log("Check that it is still possible to click the button to create a new project") + cy.logf("Check that it is still possible to click the button to create a new project", Cypress.currentTest) cy.visit("/projects/") cy.get("a").contains('New project').should('exist') - cy.log("Create one more project to check it is possible to bypass the limit") + cy.logf("Create one more project to check it is possible to bypass the limit", Cypress.currentTest) cy.createBlankProject(project_name) cy.visit("/projects/") cy.get('h5:contains("' + project_name + '")').its('length').should('eq', 11) // check that the superuser now bypassed the limit for regular users - cy.log("Now delete all created projects") + cy.logf("Now delete all created projects", Cypress.currentTest) Cypress._.times(11, () => { cy.visit("/projects/") cy.contains('.card-title', project_name).parents('.card-body').siblings('.card-footer').find('.confirm-delete').click() @@ -328,25 +339,25 @@ describe("Test superuser access", () => { const project_name = "e2e-create-proj-test-apps-limit" const app_name = "e2e-create-jl" - cy.log("Creating a blank project") + cy.logf("Creating a blank project", Cypress.currentTest) cy.createBlankProject(project_name) .then(() => { - cy.log("Create 3 jupyter lab instances (current limit)") + cy.logf("Create 3 jupyter lab instances (current limit)", Cypress.currentTest) Cypress._.times(3, () => { cy.get('[data-cy="create-app-card"]').contains('Jupyter Lab').parent().siblings().find('.btn').click() cy.get('#id_name').type(app_name) cy.get('#submit-id-submit').contains('Submit').click() }); - cy.log("Check that the button to create another one still works") + cy.logf("Check that the button to create another one still works", Cypress.currentTest) cy.get('[data-cy="create-app-card"]').contains('Jupyter Lab').parent().siblings().find('.btn').should('have.attr', 'href') - cy.log("Check that it is possible to create another one and therefore bypass the limit") + cy.logf("Check that it is possible to create another one and therefore bypass the limit", Cypress.currentTest) cy.get('[data-cy="create-app-card"]').contains('Jupyter Lab').parent().siblings().find('.btn').click() cy.get('#id_name').type(app_name) cy.get('#submit-id-submit').contains('Submit').click() cy.get('tr:contains("' + app_name + '")').its('length').should('eq', 4) // we now have an extra app }) - cy.log("Deleting the created project") + cy.logf("Deleting the created project", Cypress.currentTest) cy.visit("/projects/") cy.contains('.card-title', project_name).parents('.card-body').siblings('.card-footer').find('.confirm-delete').click() .then((href) => { @@ -368,24 +379,24 @@ describe("Test superuser access", () => { const regular_article_slug ="regular-article" const regular_article_content = "regular-article-content" - cy.log("Creating the root article") + cy.logf("Creating the root article", Cypress.currentTest) cy.visit("/docs/") cy.get('h1').should('contain', 'Congratulations') // check that django-wiki was correctly installed cy.get('#id_title').clear().type(root_article_name) cy.get('#id_content').clear().type(root_article_content) cy.get('button[name="save_changes"]').click() - cy.log("Checking that the root article was successfully created") + cy.logf("Checking that the root article was successfully created", Cypress.currentTest) cy.get('h1#article-title').contains(root_article_name) cy.get('div.wiki-article').contains(root_article_content) - cy.log("Adding a regular article") + cy.logf("Adding a regular article", Cypress.currentTest) cy.get(".btn-group").get(".btn").contains("Add a new article").click() cy.get(".btn-group").get("a.dropdown-item").contains("New article below").click() cy.url().should("include", "/docs/_create/") cy.get('#id_title').clear().type(regular_article_name) cy.get('#id_content').clear().type(regular_article_content) cy.get('button[name="save_changes"]').click() - cy.log("Checking that the regular article was successfully created") + cy.logf("Checking that the regular article was successfully created", Cypress.currentTest) cy.url().should("include", regular_article_slug) cy.get('h1#article-title').contains(regular_article_name) cy.get('div.wiki-article').contains(regular_article_content) diff --git a/cypress/e2e/ui-tests/test-views-as-reader.cy.js b/cypress/e2e/ui-tests/test-views-as-reader.cy.js index 0e53e6cc..a96e3c8e 100644 --- a/cypress/e2e/ui-tests/test-views-as-reader.cy.js +++ b/cypress/e2e/ui-tests/test-views-as-reader.cy.js @@ -6,29 +6,29 @@ describe("Test views as authenticated user", () => { let users before(() => { - // do db reset if needed - if (Cypress.env('do_reset_db') === true) { - cy.log("Resetting db state. Running db-reset.sh"); - cy.exec("./cypress/e2e/db-reset.sh"); - cy.wait(Cypress.env('wait_db_reset')); - } - else { - cy.log("Skipping resetting the db state."); - } - // seed the db with a user - cy.visit("/") - cy.log("Running seed_reader_user.py") - cy.exec("./cypress/e2e/db-seed-reader-user.sh") - // log in - cy.log("Logging in") - cy.fixture('users.json').then(function (data) { - users = data - - cy.loginViaApi(users.reader_user.email, users.reader_user.password) - }) - }) - - beforeEach(() => { + cy.logf("Begin before() hook", Cypress.currentTest) + // do db reset if needed + if (Cypress.env('do_reset_db') === true) { + cy.logf("Resetting db state. Running db-reset.sh", Cypress.currentTest); + cy.exec("./cypress/e2e/db-reset.sh"); + cy.wait(Cypress.env('wait_db_reset')); + } + else { + cy.logf("Skipping resetting the db state.", Cypress.currentTest); + } + // seed the db with a user + cy.visit("/") + cy.logf("Running seed_reader_user.py", Cypress.currentTest) + cy.exec("./cypress/e2e/db-seed-reader-user.sh") + // log in + cy.logf("Logging in", Cypress.currentTest) + cy.fixture('users.json').then(function (data) { + users = data + + cy.loginViaApi(users.reader_user.email, users.reader_user.password) + }) + + cy.logf("End before() hook", Cypress.currentTest) }) diff --git a/cypress/e2e/verify-test-setup.cy.js b/cypress/e2e/verify-test-setup.cy.js index d35cfab4..9b661ff6 100644 --- a/cypress/e2e/verify-test-setup.cy.js +++ b/cypress/e2e/verify-test-setup.cy.js @@ -1,17 +1,47 @@ describe("Simple tests to verify the test framework setup", () => { + before(() => { + // runs once before all tests in the block + cy.logf("Start before() hook.", Cypress.currentTest) + + //cy.log("Start before() hook", `(TEST: ${Cypress.currentTest.title}, ${new Date().getTime()})`) + expect(Cypress.currentTest.titlePath[0]).to.eq('Simple tests to verify the test framework setup') + expect(Cypress.currentTest.title).to.be.a('string') + cy.logf("End before() hook.", Cypress.currentTest) + }) + + beforeEach(() => { + // runs before each test in the block + cy.logf("Start beforeEach() hook", Cypress.currentTest) + expect(Cypress.currentTest.titlePath[0]).to.eq('Simple tests to verify the test framework setup') + cy.logf("End beforeEach() hook", Cypress.currentTest) + }) + it("passes", () => { }) + it("verify current test title", () => { + expect(Cypress.currentTest.titlePath[1]).to.eq('verify current test title') + }) + it("cypress can log to the terminal", () => { - cy.log("Verify that this message is output to the terminal.") + expect(Cypress.currentTest.titlePath[1]).to.eq('cypress can log to the terminal') + cy.log("Verify that this message is output to the terminal.", `(TEST: ${Cypress.currentTest.title}, ${new Date().getTime()})`) + }) + + it("cypress can log to the terminal using custom command logf", () => { + expect(Cypress.currentTest.titlePath[1]).to.eq('cypress can log to the terminal using custom command logf') + cy.logf("Verify that this message is output to the terminal.", Cypress.currentTest) + cy.logf("Verify that this message with args [a, b] is output to the terminal.", Cypress.currentTest, ["a", "b"]) }) it("can access and parse the test fixtures", () => { + expect(Cypress.currentTest.titlePath[1]).to.eq('can access and parse the test fixtures') + cy.fixture('users.json').then(function (data) { - cy.log(data.login_user.username) - cy.log(data.contributor.username) - cy.log(data.contributor.email) + cy.logf(data.login_user.username, Cypress.currentTest) + cy.logf(data.contributor.username, Cypress.currentTest) + cy.logf(data.contributor.email, Cypress.currentTest) }) }) diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 48a09e8e..4621061f 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -137,3 +137,11 @@ Cypress.Commands.add('deleteBlankProject', (project_name) => { }) }) + +Cypress.Commands.add('logf', (msg, currentTest, args = []) => { + if (args.length > 0) { + cy.log(msg, args, `(TEST: ${currentTest.title}, ${new Date().getTime()})`) + } else { + cy.log(msg, `(TEST: ${currentTest.title}, ${new Date().getTime()})`) + } +}) diff --git a/cypress/support/e2e.js b/cypress/support/e2e.js index 91051798..5560a268 100644 --- a/cypress/support/e2e.js +++ b/cypress/support/e2e.js @@ -19,4 +19,4 @@ import './commands' // Alternatively you can use CommonJS syntax: // require('./commands') -require('cypress-terminal-report/src/installLogsCollector')(); +require('cypress-terminal-report/src/installLogsCollector')({enableExtendedCollector: true}); diff --git a/package-lock.json b/package-lock.json index 1deaa61c..2703b2dc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,8 +4,8 @@ "packages": { "": { "devDependencies": { - "cypress": "^11.2.0", - "cypress-terminal-report": "^5.1.1" + "cypress": "^13.13.3", + "cypress-terminal-report": "^6.1.2" } }, "node_modules/@colors/colors": { @@ -33,9 +33,9 @@ "json-stringify-safe": "~5.0.1", "mime-types": "~2.1.19", "performance-now": "^2.1.0", - "qs": "~6.10.3", + "qs": "6.10.4", "safe-buffer": "^5.1.2", - "tough-cookie": "~2.5.0", + "tough-cookie": "^4.1.3", "tunnel-agent": "^0.6.0", "uuid": "^8.3.2" }, @@ -43,9 +43,9 @@ "engines": { "node": ">= 6" }, - "integrity": "sha512-M83/wfQ1EkspjkE2lNWNV5ui2Cv7UCv1swW1DqljahbzLVWltcsexQh8jYtuS/vzFXP+HySntGM83ZXA9fn17w==", - "resolved": "https://registry.npmjs.org/@cypress/request/-/request-2.88.11.tgz", - "version": "2.88.11" + "integrity": "sha512-TWivJlJi8ZDx2wGOw1dbLuHJKUYX7bWySw377nlnGOW3hP9/MUKIsEdXT/YngWxVdgNCHRBmFlBipE+5/2ZZlQ==", + "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.1.tgz", + "version": "3.0.1" }, "node_modules/@cypress/xvfb": { "dependencies": { @@ -67,10 +67,14 @@ "version": "3.2.7" }, "node_modules/@types/node": { + "dependencies": { + "undici-types": "~6.19.2" + }, "dev": true, - "integrity": "sha512-n3eFEaoem0WNwLux+k272P0+aq++5o05bA9CfiwKPdYPB5ZambWKdWoeHy7/OJiizMhzg27NLaZ6uzjLTzXceQ==", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.43.tgz", - "version": "14.18.43" + "integrity": "sha512-DkFrJOe+rfdHTqqMg0bSNlGlQ85hSoh2TPzZyhHsXnMtligRWpxUySiyw8FY14ITt24HVCiQPWxS3KO/QlGmWg==", + "optional": true, + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.0.tgz", + "version": "22.5.0" }, "node_modules/@types/sinonjs__fake-timers": { "dev": true, @@ -80,19 +84,19 @@ }, "node_modules/@types/sizzle": { "dev": true, - "integrity": "sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ==", - "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.3.tgz", - "version": "2.3.3" + "integrity": "sha512-0vWLNK2D5MT9dg0iOo8GlKguPAU02QjmZitPEsXRuJXU/OGIOt9vT9Fc26wtYuavLxtO45v9PGleoL9Z0k1LHg==", + "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.8.tgz", + "version": "2.3.8" }, "node_modules/@types/yauzl": { "dependencies": { "@types/node": "*" }, "dev": true, - "integrity": "sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", "optional": true, - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.0.tgz", - "version": "2.10.0" + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "version": "2.10.3" }, "node_modules/aggregate-error": { "dependencies": { @@ -204,9 +208,9 @@ }, "node_modules/async": { "dev": true, - "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", - "version": "3.2.4" + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "version": "3.2.6" }, "node_modules/asynckit": { "dev": true, @@ -234,15 +238,9 @@ }, "node_modules/aws4": { "dev": true, - "integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.12.0.tgz", - "version": "1.12.0" - }, - "node_modules/balanced-match": { - "dev": true, - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "version": "1.0.2" + "integrity": "sha512-u5w79Rd7SU4JaIlA/zFqG+gOiuq25q5VLyZ8E+ijJeILuTxVzZgp2CaGw/UTw6pXYN9XMO9yiqj/nEHmhTG5CA==", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.1.tgz", + "version": "1.13.1" }, "node_modules/base64-js": { "dev": true, @@ -285,16 +283,6 @@ "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", "version": "3.7.2" }, - "node_modules/brace-expansion": { - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - }, - "dev": true, - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "version": "1.1.11" - }, "node_modules/buffer": { "dependencies": { "base64-js": "^1.3.1", @@ -333,22 +321,28 @@ "engines": { "node": ">=6" }, - "integrity": "sha512-A+Fezp4zxnit6FanDmv9EqXNAi3vt9DWp51/71UEhXukb7QUuvtv9344h91dyAxuTLoSYJFU299qzR3tzwPAhw==", - "resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.3.0.tgz", - "version": "2.3.0" + "integrity": "sha512-9EtFOZR8g22CL7BWjJ9BUx1+A/djkofnyW3aOXZORNW2kxoUpx2h+uN2cOqwPmFhnpVmxg+KW2OjOSgChTEvsQ==", + "resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.4.0.tgz", + "version": "2.4.0" }, "node_modules/call-bind": { "dependencies": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" }, "dev": true, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" }, - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "version": "1.0.2" + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "version": "1.0.7" }, "node_modules/caseless": { "dev": true, @@ -404,9 +398,9 @@ "url": "https://github.com/sponsors/sibiraj-s" } ], - "integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz", - "version": "3.8.0" + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "version": "3.9.0" }, "node_modules/clean-stack": { "dev": true, @@ -437,12 +431,12 @@ "engines": { "node": "10.* || >= 12.*" }, - "integrity": "sha512-w5Jac5SykAeZJKntOxJCrm63Eg5/4dhMWIcuTbo9rpE+brgaSZo0RuNJZeOyMgsUdhDeojvgyQLmjI+K50ZGyg==", + "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", "optionalDependencies": { "@colors/colors": "1.5.0" }, - "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.3.tgz", - "version": "0.6.3" + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", + "version": "0.6.5" }, "node_modules/cli-truncate": { "dependencies": { @@ -501,9 +495,9 @@ "engines": { "node": ">= 6" }, - "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", - "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", - "version": "5.1.0" + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "version": "6.2.1" }, "node_modules/common-tags": { "dev": true, @@ -514,12 +508,6 @@ "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", "version": "1.8.2" }, - "node_modules/concat-map": { - "dev": true, - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "version": "0.0.1" - }, "node_modules/core-util-is": { "dev": true, "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", @@ -545,24 +533,23 @@ "cypress": "bin/cypress" }, "dependencies": { - "@cypress/request": "^2.88.10", + "@cypress/request": "^3.0.1", "@cypress/xvfb": "^1.2.4", - "@types/node": "^14.14.31", "@types/sinonjs__fake-timers": "8.1.1", "@types/sizzle": "^2.3.2", "arch": "^2.2.0", "blob-util": "^2.0.2", "bluebird": "^3.7.2", - "buffer": "^5.6.0", + "buffer": "^5.7.1", "cachedir": "^2.3.0", "chalk": "^4.1.0", "check-more-types": "^2.24.0", "cli-cursor": "^3.1.0", "cli-table3": "~0.6.1", - "commander": "^5.1.0", + "commander": "^6.2.1", "common-tags": "^1.8.0", "dayjs": "^1.10.4", - "debug": "^4.3.2", + "debug": "^4.3.4", "enquirer": "^2.3.6", "eventemitter2": "6.4.7", "execa": "4.1.0", @@ -571,46 +558,48 @@ "figures": "^3.2.0", "fs-extra": "^9.1.0", "getos": "^3.2.1", - "is-ci": "^3.0.0", + "is-ci": "^3.0.1", "is-installed-globally": "~0.4.0", "lazy-ass": "^1.6.0", "listr2": "^3.8.3", "lodash": "^4.17.21", "log-symbols": "^4.0.0", - "minimist": "^1.2.6", + "minimist": "^1.2.8", "ospath": "^1.2.2", "pretty-bytes": "^5.6.0", + "process": "^0.11.10", "proxy-from-env": "1.0.0", "request-progress": "^3.0.0", - "semver": "^7.3.2", + "semver": "^7.5.3", "supports-color": "^8.1.1", - "tmp": "~0.2.1", + "tmp": "~0.2.3", "untildify": "^4.0.0", "yauzl": "^2.10.0" }, "dev": true, "engines": { - "node": ">=12.0.0" + "node": "^16.0.0 || ^18.0.0 || >=20.0.0" }, "hasInstallScript": true, - "integrity": "sha512-u61UGwtu7lpsNWLUma/FKNOsrjcI6wleNmda/TyKHe0dOBcVjbCPlp1N6uwFZ0doXev7f/91YDpU9bqDCFeBLA==", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-11.2.0.tgz", - "version": "11.2.0" + "integrity": "sha512-hUxPrdbJXhUOTzuML+y9Av7CKoYznbD83pt8g3klgpioEha0emfx4WNIuVRx0C76r0xV2MIwAW9WYiXfVJYFQw==", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.13.3.tgz", + "version": "13.13.3" }, "node_modules/cypress-terminal-report": { "dependencies": { "chalk": "^4.0.0", "fs-extra": "^10.1.0", - "semver": "^7.3.5", + "process": "^0.11.10", + "semver": "^7.5.4", "tv4": "^1.3.0" }, "dev": true, - "integrity": "sha512-iZxb6QHV/zf4WRIsaNSf4kXLMgU/qWVLErEIs9kWlpR1mFxoLmSyR2SeC2imFupbvuP5FmkqBT2KVrmTDeQsaA==", + "integrity": "sha512-lGYLyy/fB2j3ZSfwGCZAh9z6Xmc5hompUFdAWxXgDimPZwsilQS9w7WI7QG/UoAAnLrl2P1zu+dX38HThQiSgg==", "peerDependencies": { - "cypress": ">=4.10.0" + "cypress": ">=10.0.0" }, - "resolved": "https://registry.npmjs.org/cypress-terminal-report/-/cypress-terminal-report-5.1.1.tgz", - "version": "5.1.1" + "resolved": "https://registry.npmjs.org/cypress-terminal-report/-/cypress-terminal-report-6.1.2.tgz", + "version": "6.1.2" }, "node_modules/cypress-terminal-report/node_modules/fs-extra": { "dependencies": { @@ -640,9 +629,9 @@ }, "node_modules/dayjs": { "dev": true, - "integrity": "sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ==", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.7.tgz", - "version": "1.11.7" + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "version": "1.11.13" }, "node_modules/debug": { "dependencies": { @@ -652,14 +641,31 @@ "engines": { "node": ">=6.0" }, - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", "peerDependenciesMeta": { "supports-color": { "optional": true } }, - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "version": "4.3.4" + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "version": "4.3.6" + }, + "node_modules/define-data-property": { + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + }, + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "version": "1.1.4" }, "node_modules/delayed-stream": { "dev": true, @@ -697,15 +703,37 @@ }, "node_modules/enquirer": { "dependencies": { - "ansi-colors": "^4.1.1" + "ansi-colors": "^4.1.1", + "strip-ansi": "^6.0.1" }, "dev": true, "engines": { "node": ">=8.6" }, - "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", - "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", - "version": "2.3.6" + "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", + "version": "2.4.1" + }, + "node_modules/es-define-property": { + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "version": "1.0.0" + }, + "node_modules/es-errors": { + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "version": "1.3.0" }, "node_modules/escape-string-regexp": { "dev": true, @@ -854,31 +882,33 @@ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", "version": "9.1.0" }, - "node_modules/fs.realpath": { - "dev": true, - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "version": "1.0.0" - }, "node_modules/function-bind": { "dev": true, - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "version": "1.1.1" + "funding": { + "url": "https://github.com/sponsors/ljharb" + }, + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "version": "1.1.2" }, "node_modules/get-intrinsic": { "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.3" + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" }, "dev": true, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" }, - "integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", - "version": "1.2.0" + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "version": "1.2.4" }, "node_modules/get-stream": { "dependencies": { @@ -913,26 +943,6 @@ "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", "version": "0.1.7" }, - "node_modules/glob": { - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "dev": true, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "version": "7.2.3" - }, "node_modules/global-dirs": { "dependencies": { "ini": "2.0.0" @@ -948,24 +958,24 @@ "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.1.tgz", "version": "3.0.1" }, + "node_modules/gopd": { + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + }, + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "version": "1.0.1" + }, "node_modules/graceful-fs": { "dev": true, "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "version": "4.2.11" }, - "node_modules/has": { - "dependencies": { - "function-bind": "^1.1.1" - }, - "dev": true, - "engines": { - "node": ">= 0.4.0" - }, - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "version": "1.0.3" - }, "node_modules/has-flag": { "dev": true, "engines": { @@ -975,6 +985,30 @@ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "version": "4.0.0" }, + "node_modules/has-property-descriptors": { + "dependencies": { + "es-define-property": "^1.0.0" + }, + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + }, + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "version": "1.0.2" + }, + "node_modules/has-proto": { + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + }, + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "version": "1.0.3" + }, "node_modules/has-symbols": { "dev": true, "engines": { @@ -987,6 +1021,18 @@ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", "version": "1.0.3" }, + "node_modules/hasown": { + "dependencies": { + "function-bind": "^1.1.2" + }, + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "version": "2.0.2" + }, "node_modules/http-signature": { "dependencies": { "assert-plus": "^1.0.0", @@ -1039,22 +1085,6 @@ "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", "version": "4.0.0" }, - "node_modules/inflight": { - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - }, - "dev": true, - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "version": "1.0.6" - }, - "node_modules/inherits": { - "dev": true, - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "version": "2.0.4" - }, "node_modules/ini": { "dev": true, "engines": { @@ -1310,18 +1340,6 @@ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", "version": "6.2.0" }, - "node_modules/lru-cache": { - "dependencies": { - "yallist": "^4.0.0" - }, - "dev": true, - "engines": { - "node": ">=10" - }, - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "version": "6.0.0" - }, "node_modules/merge-stream": { "dev": true, "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", @@ -1358,18 +1376,6 @@ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", "version": "2.1.0" }, - "node_modules/minimatch": { - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "dev": true, - "engines": { - "node": "*" - }, - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "version": "3.1.2" - }, "node_modules/minimist": { "dev": true, "funding": { @@ -1399,12 +1405,15 @@ }, "node_modules/object-inspect": { "dev": true, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" }, - "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", - "version": "1.12.3" + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "version": "1.13.2" }, "node_modules/once": { "dependencies": { @@ -1451,15 +1460,6 @@ "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", "version": "4.0.0" }, - "node_modules/path-is-absolute": { - "dev": true, - "engines": { - "node": ">=0.10.0" - }, - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "version": "1.0.1" - }, "node_modules/path-key": { "dev": true, "engines": { @@ -1502,6 +1502,15 @@ "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", "version": "5.6.0" }, + "node_modules/process": { + "dev": true, + "engines": { + "node": ">= 0.6.0" + }, + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "version": "0.11.10" + }, "node_modules/proxy-from-env": { "dev": true, "integrity": "sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A==", @@ -1529,9 +1538,9 @@ "engines": { "node": ">=6" }, - "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", - "version": "2.3.0" + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "version": "2.3.1" }, "node_modules/qs": { "dependencies": { @@ -1548,6 +1557,12 @@ "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.4.tgz", "version": "6.10.4" }, + "node_modules/querystringify": { + "dev": true, + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "version": "2.2.0" + }, "node_modules/request-progress": { "dependencies": { "throttleit": "^1.0.0" @@ -1557,6 +1572,12 @@ "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-3.0.0.tgz", "version": "3.0.0" }, + "node_modules/requires-port": { + "dev": true, + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "version": "1.0.0" + }, "node_modules/restore-cursor": { "dependencies": { "onetime": "^5.1.0", @@ -1572,24 +1593,9 @@ }, "node_modules/rfdc": { "dev": true, - "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==", - "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", - "version": "1.3.0" - }, - "node_modules/rimraf": { - "bin": { - "rimraf": "bin.js" - }, - "dependencies": { - "glob": "^7.1.3" - }, - "dev": true, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "version": "3.0.2" + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "version": "1.4.1" }, "node_modules/rxjs": { "dependencies": { @@ -1630,16 +1636,30 @@ "bin": { "semver": "bin/semver.js" }, + "dev": true, + "engines": { + "node": ">=10" + }, + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "version": "7.6.3" + }, + "node_modules/set-function-length": { "dependencies": { - "lru-cache": "^6.0.0" + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" }, "dev": true, "engines": { - "node": ">=10" + "node": ">= 0.4" }, - "integrity": "sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.0.tgz", - "version": "7.5.0" + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "version": "1.2.2" }, "node_modules/shebang-command": { "dependencies": { @@ -1664,17 +1684,21 @@ }, "node_modules/side-channel": { "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" }, "dev": true, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" }, - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "version": "1.0.4" + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "version": "1.0.6" }, "node_modules/signal-exit": { "dev": true, @@ -1717,9 +1741,9 @@ "engines": { "node": ">=0.10.0" }, - "integrity": "sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.17.0.tgz", - "version": "1.17.0" + "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", + "version": "1.18.0" }, "node_modules/string-width": { "dependencies": { @@ -1773,9 +1797,12 @@ }, "node_modules/throttleit": { "dev": true, - "integrity": "sha512-rkTVqu6IjfQ/6+uNuuc3sZek4CEYxTJom3IktzgdSxcZqdARuebbA/f4QmAxMQIxqq9ZLEUkSYqvuk1I6VKq4g==", - "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.0.tgz", - "version": "1.0.0" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + }, + "integrity": "sha512-vDZpf9Chs9mAdfY046mcPt8fg5QSZr37hEH4TXYBnDF+izxgrbRGUAAaBvIk/fJm9aOFCGFd1EsNg5AZCbnQCQ==", + "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.1.tgz", + "version": "1.0.1" }, "node_modules/through": { "dev": true, @@ -1784,35 +1811,43 @@ "version": "2.3.8" }, "node_modules/tmp": { - "dependencies": { - "rimraf": "^3.0.0" - }, "dev": true, "engines": { - "node": ">=8.17.0" + "node": ">=14.14" }, - "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", - "version": "0.2.1" + "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", + "version": "0.2.3" }, "node_modules/tough-cookie": { "dependencies": { - "psl": "^1.1.28", - "punycode": "^2.1.1" + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" }, "dev": true, "engines": { - "node": ">=0.8" + "node": ">=6" + }, + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "version": "4.1.4" + }, + "node_modules/tough-cookie/node_modules/universalify": { + "dev": true, + "engines": { + "node": ">= 4.0.0" }, - "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", - "version": "2.5.0" + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "version": "0.2.0" }, "node_modules/tslib": { "dev": true, - "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", - "version": "2.5.0" + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "version": "2.7.0" }, "node_modules/tunnel-agent": { "dependencies": { @@ -1853,14 +1888,21 @@ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", "version": "0.21.3" }, + "node_modules/undici-types": { + "dev": true, + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "optional": true, + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "version": "6.19.8" + }, "node_modules/universalify": { "dev": true, "engines": { "node": ">= 10.0.0" }, - "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", - "version": "2.0.0" + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "version": "2.0.1" }, "node_modules/untildify": { "dev": true, @@ -1871,6 +1913,16 @@ "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", "version": "4.0.0" }, + "node_modules/url-parse": { + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + }, + "dev": true, + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "version": "1.5.10" + }, "node_modules/uuid": { "bin": { "uuid": "dist/bin/uuid" @@ -1932,12 +1984,6 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "version": "1.0.2" }, - "node_modules/yallist": { - "dev": true, - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "version": "4.0.0" - }, "node_modules/yauzl": { "dependencies": { "buffer-crc32": "~0.2.3", diff --git a/package.json b/package.json index e99a4556..16bed598 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "devDependencies": { - "cypress": "^11.2.0", - "cypress-terminal-report": "^5.1.1" + "cypress": "^13.13.3", + "cypress-terminal-report": "^6.1.2" } } From 5432b7e3bfec9cff1f4f4eafeaf8c9df55a74666 Mon Sep 17 00:00:00 2001 From: Nikita Churikov <8545082+churnikov@users.noreply.github.com> Date: Tue, 3 Sep 2024 11:22:28 +0200 Subject: [PATCH 06/19] SS-1086 Added advanced input group to the shiny apps with site_dir option (#221) - Added advanced input group to the shiny apps; - This advanced group has settings for a custom site_dir for shiny apps; - Added migration file for the new field to shiny apps instances; - Moved SubdomainInputGroup to separate module; - Added missing closing div to the create_base.html; - Added new template for prepend-append field with our custom tooltip in div_label_with_help_toggle.html; --- apps/forms/base.py | 29 +-------------- apps/forms/field/widget.py | 30 ++++++++++++++++ apps/forms/shiny.py | 36 +++++++++++++++++++ .../0010_shinyinstance_shiny_site_dir.py | 17 +++++++++ apps/models/app_types/shiny.py | 2 ++ templates/apps/create_base.html | 1 + templates/apps/custom_field.html | 6 +--- .../partials/div_label_with_help_toggle.html | 5 +++ .../srv_prepend_append_input_group.html | 15 ++++++++ 9 files changed, 108 insertions(+), 33 deletions(-) create mode 100644 apps/forms/field/widget.py create mode 100644 apps/migrations/0010_shinyinstance_shiny_site_dir.py create mode 100644 templates/apps/partials/div_label_with_help_toggle.html create mode 100644 templates/apps/partials/srv_prepend_append_input_group.html diff --git a/apps/forms/base.py b/apps/forms/base.py index 45a556a2..aa5133ba 100644 --- a/apps/forms/base.py +++ b/apps/forms/base.py @@ -4,10 +4,8 @@ from crispy_forms.layout import Button, Div, Submit from django import forms from django.shortcuts import get_object_or_404 -from django.template import loader -from django.utils.safestring import mark_safe -from apps.helpers import get_select_options +from apps.forms.field.widget import SubdomainInputGroup from apps.models import BaseAppInstance, Subdomain, VolumeInstance from apps.types_.subdomain import SubdomainCandidateName, SubdomainTuple from projects.models import Flavor, Project @@ -15,31 +13,6 @@ __all__ = ["BaseForm", "AppBaseForm"] -# Custom Widget that adds boostrap-style input group to the subdomain field -class SubdomainInputGroup(forms.Widget): - subdomain_template = "apps/partials/subdomain_input_group.html" - - def __init__(self, base_widget, data, *args, **kwargs): - # Initialise widget and get base instance - super(SubdomainInputGroup, self).__init__(*args, **kwargs) - self.base_widget = base_widget(*args, **kwargs) - self.data = data - - def get_context(self, name, value, attrs=None): - return { - "initial_subdomain": value, - "project_pk": self.data["project_pk"], - "hidden": self.data["hidden"], - "subdomain_list": get_select_options(self.data["project_pk"]), - } - - def render(self, name, value, attrs=None, renderer=None): - # Render base widget and add bootstrap spans - context = self.get_context(name, value, attrs) - template = loader.get_template(self.subdomain_template).render(context) - return mark_safe(template) - - class BaseForm(forms.ModelForm): """The most generic form for apps running on serve. Current intended use is for VolumesK8S type apps""" diff --git a/apps/forms/field/widget.py b/apps/forms/field/widget.py new file mode 100644 index 00000000..1b76dc2d --- /dev/null +++ b/apps/forms/field/widget.py @@ -0,0 +1,30 @@ +from django import forms +from django.template import loader +from django.utils.safestring import mark_safe + +from apps.helpers import get_select_options + + +# Custom Widget that adds boostrap-style input group to the subdomain field +class SubdomainInputGroup(forms.Widget): + subdomain_template = "apps/partials/subdomain_input_group.html" + + def __init__(self, base_widget, data, *args, **kwargs): + # Initialise widget and get base instance + super(SubdomainInputGroup, self).__init__(*args, **kwargs) + self.base_widget = base_widget(*args, **kwargs) + self.data = data + + def get_context(self, name, value, attrs=None): + return { + "initial_subdomain": value, + "project_pk": self.data["project_pk"], + "hidden": self.data["hidden"], + "subdomain_list": get_select_options(self.data["project_pk"]), + } + + def render(self, name, value, attrs=None, renderer=None): + # Render base widget and add bootstrap spans + context = self.get_context(name, value, attrs) + template = loader.get_template(self.subdomain_template).render(context) + return mark_safe(template) diff --git a/apps/forms/shiny.py b/apps/forms/shiny.py index 2d3e34bf..e3c4b812 100644 --- a/apps/forms/shiny.py +++ b/apps/forms/shiny.py @@ -1,5 +1,7 @@ +from crispy_forms.bootstrap import Accordion, AccordionGroup, PrependedText from crispy_forms.layout import Div, Layout from django import forms +from django.utils.safestring import mark_safe from apps.forms.base import AppBaseForm from apps.forms.field.common import SRVCommonDivField @@ -13,6 +15,7 @@ class ShinyForm(AppBaseForm): flavor = forms.ModelChoiceField(queryset=Flavor.objects.none(), required=False, empty_label=None) port = forms.IntegerField(min_value=3000, max_value=9999, required=True) image = forms.CharField(max_length=255, required=True) + shiny_site_dir = forms.CharField(max_length=255, required=False, label="Path to site_dir") def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -23,6 +26,15 @@ def __init__(self, *args, **kwargs): def _setup_form_fields(self): # Handle Volume field super()._setup_form_fields() + self.fields["shiny_site_dir"].widget.attrs.update({"class": "textinput form-control"}) + self.fields["shiny_site_dir"].help_text = ( + "Provide a path to the Shiny app inside your " "Docker image if it is different from /srv/shiny-server/" + ) + self.fields["shiny_site_dir"].bottom_help_text = mark_safe( + "Use this field to specify subfolder if you did not place your app directly in /srv/shiny-server/. " + 'You can find more about it ' + "in our documentation." + ) def _setup_form_helper(self): super()._setup_form_helper() @@ -42,11 +54,33 @@ def _setup_form_helper(self): SRVCommonDivField("source_code_url", placeholder="Provide a link to the public source code"), SRVCommonDivField("port", placeholder="3838"), SRVCommonDivField("image", placeholder="registry/repository/image:tag"), + Accordion( + AccordionGroup( + "Advanced settings", + PrependedText( + "shiny_site_dir", + "/srv/shiny-server", + template="apps/partials/srv_prepend_append_input_group.html", + ), + active=False, + ), + ), css_class="card-body", ) self.helper.layout = Layout(body, self.footer) + def clean_shiny_site_dir(self): + cleaned_data = super().clean() + shiny_site_dir = cleaned_data.get("shiny_site_dir", None) + if shiny_site_dir and shiny_site_dir.startswith("/"): + self.add_error("shiny_site_dir", "Path must not start with a forward slash.") + # Check that the path is ascii + if shiny_site_dir and not shiny_site_dir.isascii(): + self.add_error("shiny_site_dir", "Path must be ASCII.") + + return shiny_site_dir + def clean(self): cleaned_data = super().clean() access = cleaned_data.get("access", None) @@ -70,8 +104,10 @@ class Meta: "port", "image", "tags", + "shiny_site_dir", ] labels = { "tags": "Keywords", "note_on_linkonly_privacy": "Reason for choosing the link only option", + "shiny_site_dir": "Custom subpath for Shiny app after /srv/shiny-server/", } diff --git a/apps/migrations/0010_shinyinstance_shiny_site_dir.py b/apps/migrations/0010_shinyinstance_shiny_site_dir.py new file mode 100644 index 00000000..b32b066b --- /dev/null +++ b/apps/migrations/0010_shinyinstance_shiny_site_dir.py @@ -0,0 +1,17 @@ +# Generated by Django 5.0.2 on 2024-09-02 09:49 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("apps", "0009_alter_customappinstance_volume_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="shinyinstance", + name="shiny_site_dir", + field=models.CharField(default="", max_length=255), + ), + ] diff --git a/apps/models/app_types/shiny.py b/apps/models/app_types/shiny.py index db7a4838..290f955a 100644 --- a/apps/models/app_types/shiny.py +++ b/apps/models/app_types/shiny.py @@ -35,6 +35,7 @@ class ShinyInstance(BaseAppInstance, SocialMixin, LogsEnabledMixin): container_waittime = models.IntegerField(default=20000) heartbeat_timeout = models.IntegerField(default=60000) heartbeat_rate = models.IntegerField(default=10000) + shiny_site_dir = models.CharField(max_length=255, default="") # The following three settings control the pre-init and seats behaviour (see documentation) # These settings override the Helm chart default values @@ -50,6 +51,7 @@ def get_k8s_values(self): port=self.port, image=self.image, path=self.path, + site_dir="/srv/shiny-server/" + self.shiny_site_dir, proxyheartbeatrate=self.heartbeat_rate, proxyheartbeattimeout=self.heartbeat_timeout, proxycontainerwaittime=self.container_waittime, diff --git a/templates/apps/create_base.html b/templates/apps/create_base.html index 77c16c52..201b17da 100644 --- a/templates/apps/create_base.html +++ b/templates/apps/create_base.html @@ -23,6 +23,7 @@

Create {{ form.model_name }}

{% endif %} +
{% crispy form %}
diff --git a/templates/apps/custom_field.html b/templates/apps/custom_field.html index a296c1dd..1567ce2e 100644 --- a/templates/apps/custom_field.html +++ b/templates/apps/custom_field.html @@ -1,9 +1,5 @@
-
- - - -
+ {% include "apps/partials/div_label_with_help_toggle.html" %}
{% if spinner %} diff --git a/templates/apps/partials/div_label_with_help_toggle.html b/templates/apps/partials/div_label_with_help_toggle.html new file mode 100644 index 00000000..4be51a40 --- /dev/null +++ b/templates/apps/partials/div_label_with_help_toggle.html @@ -0,0 +1,5 @@ +
+ + + +
diff --git a/templates/apps/partials/srv_prepend_append_input_group.html b/templates/apps/partials/srv_prepend_append_input_group.html new file mode 100644 index 00000000..99c1e745 --- /dev/null +++ b/templates/apps/partials/srv_prepend_append_input_group.html @@ -0,0 +1,15 @@ +{% include "apps/partials/div_label_with_help_toggle.html" with help_message=field.help_text %} +
+
+ {% if crispy_prepended_text %} + {{ crispy_prepended_text }} + {% endif %} + {{ field }} + {% if crispy_appended_text %} + {{ crispy_appended_text }} + {% endif %} +
+ {% if field.field.bottom_help_text %} + {{ field.field.bottom_help_text }} + {% endif %} +
From 83f98674f74c67f10c814bd7e9b4eb4129a16c46 Mon Sep 17 00:00:00 2001 From: Nikita Churikov <8545082+churnikov@users.noreply.github.com> Date: Tue, 3 Sep 2024 11:47:03 +0200 Subject: [PATCH 07/19] SS-1049 pycharm dev setup (#207) Co-authored-by: Hamza --- .gitignore | 2 + README.md | 89 +++++++++++++++++++++++++++++++++++++++++++++ docker-compose.yaml | 1 + local.Dockerfile | 31 ++++++++++++++++ 4 files changed, 123 insertions(+) create mode 100644 local.Dockerfile diff --git a/.gitignore b/.gitignore index 37577ba1..0f542f7c 100644 --- a/.gitignore +++ b/.gitignore @@ -154,3 +154,5 @@ node_modules # The media folder contents media/* !media/.gitkeep + +local_.dockerfile diff --git a/README.md b/README.md index 3fca4a92..a61afe26 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,12 @@ We welcome contributions to the code. When you want to make a contribution pleas The `main` branch contains code behind the version that is currently deployed in production - https://serve.scilifelab.se. The branches `develop` and `staging` contain current development versions at different stages. +### Local development setup + +There are multiple ways to set up a local development environment for SciLifeLab Serve. Below you can find instructions on how to set up a local development environment using Docker Compose or Rancher Desktop. + +Both have their pros and cons. Docker Compose is easier to set up but does not provide a full Kubernetes environment. Rancher Desktop provides a full Kubernetes environment but is more complex to set up. + ### Deploy Serve for local development with Docker Compose It is possible to deploy and work with the user interface of Serve without a running Kubernetes cluster, in that case you can skip the step related to that below. However, in order to be able to deploy and modify resources (apps, notebooks, persistent storage, etc.) a running local cluster is required; it can be created for example with [microk8s](https://microk8s.io/). @@ -75,6 +81,89 @@ This assumes you have the correct ssh key in your ssh-agent. If you like to give $ docker compose up -d ``` +### Deploy Serve for local development with Rancher Desktop + +Start with instructions in [Serve Charts > How to Deploy](https://github.com/ScilifelabDataCentre/serve-charts?tab=readme-ov-file#how-to-deploy) and come back here when you get to the point of building the studio image. + +Again, this setup assumes you have [Rancher Desktop](https://rancherdesktop.io/) installed and running. + +```bash +$ git clone git@github.com:ScilifelabDataCentre/stackn.git +$ cd stackn +$ git checkout develop +$ cp .env.template .env +$ cp ~/.kube/config cluster.conf +$ cp ~/.ssh/id_rsa.pub id_rsa.pub +$ cat Dockerfile local.Dockerfile > local_.dockerfile +$ nerdctl build --namespace k8s.io -t mystudio -f local_.dockerfile . +``` + +Now continue setting up serve charts until you get to the PyCharm setup. + +##### PyCharm setup + +> Prerequisite: This setup assumes you have PyCharm Professional installed. + +1. Do this weirdness due to [this](https://youtrack.jetbrains.com/issue/PY-55338/Connection-to-python-console-refused-with-docker-interpreter-on-Linux) + 1. go to Help | Find Action | Registry + 2. disable python.use.targets.api + 3. recreate the interpreter from scratch +2. Setup ssh interpreter +```bash +# This will open ssh connection from the pod to our host machine +# Because we made everything super NOT secure for local development you can ssh there without password and as root +$ sudo kubectl port-forward svc/serve-studio 22:22 +``` +3. Set up the interpreter in PyCharm + 1. Go to `PyCharm | Settings | Project: stackn | Python Interpreter` + 2. Add new interpreter + 3. Choose SSH + 4. Host: localhost + 5. Port: 22 + 6. Username: root + 7. Auth type: Password + 8. Password: root + 9. Interpreter path: /usr/local/bin/python + 10. Python interpreter path: /usr/local/bin/python + 11. **Important** Don't accept synchronization option from PyCharm. There is no need for it because we already synchronize the code with the pod using volume mounts provided by Rancher Desktop. +4. Set up the environment variables + 1. Go to Run | Edit Configurations + 2. Add new configuration + 3. Choose `Django server` + 4. Name: `Serve` + 5. Host: `0.0.0.0` + 6. Port: `8080` + 7. Click Modify options and select `No reload` and `Environment variables` + 8. Add environment variables from the studio pod + 9. Make sure the Working directory is /app. + 10. The Path mappings should be /path/to/your/stackn=/app. + +```bash +$ kubectl get po +# find the studio pod serve-studio-123 +$ kubectl exec -it -- /bin/bash +# Run migrations +# For simplicity just run sh scripts/run_web.sh +$ sh scripts/run_web.sh +# And then stop the process when the server is running + +# Now you are in the studio container +# Type env +$ env +# Copy the whole output in to the pycharm environment configuration +``` + +Copy environment variables to the PyCharm Django configuration. The environment variables need to be separated by a semi-colon. To achieve this, click on the list icon in the Environment variables input box and then in the popup, click paste. + +Make sure that the Django Framework settings in PyCharm are correctly setup. +To check, go to PyCharm | Settings | Languages & Frameworks | Django and check the following settings +- Enable Django Support should be checked. +- Django project root should be `/path/to/your/stackn` +- Settings should be `studio/settings.py` +- Manage script should be `manage.py` + +Now that you are done, you can run Django server using PyCharm and access the studio at [http://studio.127.0.0.1.nip.io/](http://studio.127.0.0.1.nip.io/) + ## Contact information To get in touch with the development team behind SciLifeLab Serve send us an email: serve@scilifelab.se. diff --git a/docker-compose.yaml b/docker-compose.yaml index 499545f9..28318f7d 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -16,6 +16,7 @@ services: container_name: studio build: context: . + target: runtime image: stackn:develop env_file: - .env diff --git a/local.Dockerfile b/local.Dockerfile new file mode 100644 index 00000000..cb295bf9 --- /dev/null +++ b/local.Dockerfile @@ -0,0 +1,31 @@ +FROM runtime as local + +USER root + +RUN apk add --update --no-cache openssh + +RUN ssh-keygen -A \ + && mkdir -p /root/.ssh \ + && touch /root/.ssh/authorized_keys \ + && chmod 700 /root/.ssh \ + && chmod 600 /root/.ssh/authorized_keys + +COPY id_rsa.pub /root/.ssh/authorized_keys + +RUN sed -i '/^AllowTcpForwarding/d' /etc/ssh/sshd_config \ + && sed -i '/^GatewayPorts/d' /etc/ssh/sshd_config \ + && echo "AllowTcpForwarding yes" >> /etc/ssh/sshd_config \ + && echo 'PermitRootLogin yes' >> /etc/ssh/sshd_config \ + && echo 'PermitEmptyPasswords yes' >> /etc/ssh/sshd_config \ + && echo 'PasswordAuthentication yes' >> /etc/ssh/sshd_config \ + && echo 'ChallengeResponseAuthentication no' >> /etc/ssh/sshd_config \ + && echo 'GatewayPorts clientspecified' >> /etc/ssh/sshd_config + +RUN sed -i '/UsePAM/d' /etc/ssh/sshd_config + +# Set root password (optional, for testing purposes) +RUN echo 'root:password' | chpasswd + +EXPOSE 22 + +CMD ["/usr/sbin/sshd", "-D"] From 1ce61ccbeb26652d5c8627d78396f5c49b8e94a6 Mon Sep 17 00:00:00 2001 From: Mahbub Ul Alam Date: Tue, 10 Sep 2024 10:04:46 +0200 Subject: [PATCH 08/19] Added acknowledgement text and replaced MinIO with filebrowser information (#224) --- templates/portal/about.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/templates/portal/about.html b/templates/portal/about.html index f918301d..c285f0fc 100644 --- a/templates/portal/about.html +++ b/templates/portal/about.html @@ -9,9 +9,10 @@

About SciLifeLab Serve

SciLifeLab Serve (beta) is a platform for serving applications and machine learning models that is offered to life science researchers affiliated with a Swedish research institute and their collaborators. Currently, the service is running in beta testing mode. The service is available free of charge to researchers with and without an affiliation to SciLifeLab, to collaborators of these researchers, as well as to data-producing infrastructures in Sweden.

+

If you use SciLifeLab Serve for sharing apps and models that accompany your publications, presentations, etc., you can add the following acknowledgement text: “Technical infrastructure for hosting the [app ‘xxx’/model ‘xxx’] was provided by SciLifeLab Serve (https://serve.scilifelab.se), a platform developed and supported by SciLifeLab Data Centre.”

Infrastructure behind the service

SciLifeLab Serve is developed and maintained by the SciLifeLab Data Centre. Initially the service was based on STACKn, a platform developed by Scaleout systems (a spinoff from the Department of Information Technology at Uppsala University). Since then, the SciLifeLab Data Centre team has expanded and improved the platform. The most up to date source code behind SciLifeLab Serve is available with an open source license on GitHub. We welcome any other organisations running a similar service using our code.

-

The main SciLifeLab Serve application is built with Python on Django. SciLifeLab Serve components include a number of open source applications: JupyterLab (source code), RStudio Server (source code, modifications), code-server (VS Code, source code), ShinyProxy (source code), MinIO (unmodified), TensorFlow Serving (source code), TorchServe (source code), MLflow (source code). We use Bootstrap and Bootstrap Icons for user interface styling, JavaScript libraries jQuery and aos for dynamic elements of the website.

+

The main SciLifeLab Serve application is built with Python on Django. SciLifeLab Serve components include a number of open source applications: JupyterLab (source code), RStudio Server (source code, modifications), code-server (VS Code, source code), ShinyProxy (source code), filebrowser (source code, modifications), TensorFlow Serving (source code), TorchServe (source code), MLflow (source code). We use Bootstrap and Bootstrap Icons for user interface styling, JavaScript libraries jQuery and aos for dynamic elements of the website.

SciLifeLab Serve as well as the applications and models deployed through the service are hosted on a Kubernetes cluster located and administered on hardware at the KTH Royal Institute of Technology in Stockholm and managed by the SciLifeLab Data Centre.

Organisations behind the service

From 341e4814c714508e6150afa7caeb1f77a594744a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20Alfred=C3=A9en?= Date: Wed, 11 Sep 2024 08:59:58 +0200 Subject: [PATCH 09/19] SS 1080 App edits and app status (#216) Co-authored-by: akochari --- .gitignore | 1 + api/views.py | 14 +- apps/helpers.py | 31 +- cypress/e2e/ui-tests/test-deploy-app.cy.js | 108 +- docker-compose.yaml | 2 +- static/tagulous/lib/jquery.js | 2 + .../lib/select2-3/select2-spinner.gif | Bin 0 -> 1849 bytes static/tagulous/lib/select2-3/select2.css | 704 ++ static/tagulous/lib/select2-3/select2.min.js | 23 + static/tagulous/lib/select2-3/select2.png | Bin 0 -> 613 bytes static/tagulous/lib/select2-3/select2x2.png | Bin 0 -> 845 bytes static/tagulous/lib/select2-4/css/select2.css | 537 ++ .../lib/select2-4/css/select2.min.css | 1 + static/tagulous/lib/select2-4/js/i18n/af.js | 3 + static/tagulous/lib/select2-4/js/i18n/ar.js | 3 + static/tagulous/lib/select2-4/js/i18n/az.js | 3 + static/tagulous/lib/select2-4/js/i18n/bg.js | 3 + static/tagulous/lib/select2-4/js/i18n/bn.js | 3 + static/tagulous/lib/select2-4/js/i18n/bs.js | 3 + static/tagulous/lib/select2-4/js/i18n/ca.js | 3 + static/tagulous/lib/select2-4/js/i18n/cs.js | 3 + static/tagulous/lib/select2-4/js/i18n/da.js | 3 + static/tagulous/lib/select2-4/js/i18n/de.js | 3 + static/tagulous/lib/select2-4/js/i18n/dsb.js | 3 + static/tagulous/lib/select2-4/js/i18n/el.js | 3 + static/tagulous/lib/select2-4/js/i18n/en.js | 3 + static/tagulous/lib/select2-4/js/i18n/eo.js | 3 + static/tagulous/lib/select2-4/js/i18n/es.js | 3 + static/tagulous/lib/select2-4/js/i18n/et.js | 3 + static/tagulous/lib/select2-4/js/i18n/eu.js | 3 + static/tagulous/lib/select2-4/js/i18n/fa.js | 3 + static/tagulous/lib/select2-4/js/i18n/fi.js | 3 + static/tagulous/lib/select2-4/js/i18n/fr.js | 3 + static/tagulous/lib/select2-4/js/i18n/gl.js | 3 + static/tagulous/lib/select2-4/js/i18n/he.js | 3 + static/tagulous/lib/select2-4/js/i18n/hi.js | 3 + static/tagulous/lib/select2-4/js/i18n/hr.js | 3 + static/tagulous/lib/select2-4/js/i18n/hsb.js | 3 + static/tagulous/lib/select2-4/js/i18n/hu.js | 3 + static/tagulous/lib/select2-4/js/i18n/hy.js | 3 + static/tagulous/lib/select2-4/js/i18n/id.js | 3 + static/tagulous/lib/select2-4/js/i18n/is.js | 3 + static/tagulous/lib/select2-4/js/i18n/it.js | 3 + static/tagulous/lib/select2-4/js/i18n/ja.js | 3 + static/tagulous/lib/select2-4/js/i18n/ka.js | 3 + static/tagulous/lib/select2-4/js/i18n/km.js | 3 + static/tagulous/lib/select2-4/js/i18n/ko.js | 3 + static/tagulous/lib/select2-4/js/i18n/lt.js | 3 + static/tagulous/lib/select2-4/js/i18n/lv.js | 3 + static/tagulous/lib/select2-4/js/i18n/mk.js | 3 + static/tagulous/lib/select2-4/js/i18n/ms.js | 3 + static/tagulous/lib/select2-4/js/i18n/nb.js | 3 + static/tagulous/lib/select2-4/js/i18n/ne.js | 3 + static/tagulous/lib/select2-4/js/i18n/nl.js | 3 + static/tagulous/lib/select2-4/js/i18n/pa.js | 3 + static/tagulous/lib/select2-4/js/i18n/pl.js | 3 + static/tagulous/lib/select2-4/js/i18n/ps.js | 3 + .../tagulous/lib/select2-4/js/i18n/pt-BR.js | 3 + static/tagulous/lib/select2-4/js/i18n/pt.js | 3 + static/tagulous/lib/select2-4/js/i18n/ro.js | 3 + static/tagulous/lib/select2-4/js/i18n/ru.js | 3 + static/tagulous/lib/select2-4/js/i18n/sk.js | 3 + static/tagulous/lib/select2-4/js/i18n/sl.js | 3 + static/tagulous/lib/select2-4/js/i18n/sq.js | 3 + .../tagulous/lib/select2-4/js/i18n/sr-Cyrl.js | 3 + static/tagulous/lib/select2-4/js/i18n/sr.js | 3 + static/tagulous/lib/select2-4/js/i18n/sv.js | 3 + static/tagulous/lib/select2-4/js/i18n/te.js | 3 + static/tagulous/lib/select2-4/js/i18n/th.js | 3 + static/tagulous/lib/select2-4/js/i18n/tk.js | 3 + static/tagulous/lib/select2-4/js/i18n/tr.js | 3 + static/tagulous/lib/select2-4/js/i18n/uk.js | 3 + static/tagulous/lib/select2-4/js/i18n/vi.js | 3 + .../tagulous/lib/select2-4/js/i18n/zh-CN.js | 3 + .../tagulous/lib/select2-4/js/i18n/zh-TW.js | 3 + .../tagulous/lib/select2-4/js/select2.full.js | 6521 +++++++++++++++++ .../lib/select2-4/js/select2.full.min.js | 2 + static/tagulous/lib/select2-4/js/select2.js | 6209 ++++++++++++++++ .../tagulous/lib/select2-4/js/select2.min.js | 2 + studio/settings.py | 1 + templates/user/account_delete_form.html | 2 +- 81 files changed, 14326 insertions(+), 20 deletions(-) create mode 100644 static/tagulous/lib/jquery.js create mode 100644 static/tagulous/lib/select2-3/select2-spinner.gif create mode 100644 static/tagulous/lib/select2-3/select2.css create mode 100644 static/tagulous/lib/select2-3/select2.min.js create mode 100644 static/tagulous/lib/select2-3/select2.png create mode 100644 static/tagulous/lib/select2-3/select2x2.png create mode 100644 static/tagulous/lib/select2-4/css/select2.css create mode 100644 static/tagulous/lib/select2-4/css/select2.min.css create mode 100644 static/tagulous/lib/select2-4/js/i18n/af.js create mode 100644 static/tagulous/lib/select2-4/js/i18n/ar.js create mode 100644 static/tagulous/lib/select2-4/js/i18n/az.js create mode 100644 static/tagulous/lib/select2-4/js/i18n/bg.js create mode 100644 static/tagulous/lib/select2-4/js/i18n/bn.js create mode 100644 static/tagulous/lib/select2-4/js/i18n/bs.js create mode 100644 static/tagulous/lib/select2-4/js/i18n/ca.js create mode 100644 static/tagulous/lib/select2-4/js/i18n/cs.js create mode 100644 static/tagulous/lib/select2-4/js/i18n/da.js create mode 100644 static/tagulous/lib/select2-4/js/i18n/de.js create mode 100644 static/tagulous/lib/select2-4/js/i18n/dsb.js create mode 100644 static/tagulous/lib/select2-4/js/i18n/el.js create mode 100644 static/tagulous/lib/select2-4/js/i18n/en.js create mode 100644 static/tagulous/lib/select2-4/js/i18n/eo.js create mode 100644 static/tagulous/lib/select2-4/js/i18n/es.js create mode 100644 static/tagulous/lib/select2-4/js/i18n/et.js create mode 100644 static/tagulous/lib/select2-4/js/i18n/eu.js create mode 100644 static/tagulous/lib/select2-4/js/i18n/fa.js create mode 100644 static/tagulous/lib/select2-4/js/i18n/fi.js create mode 100644 static/tagulous/lib/select2-4/js/i18n/fr.js create mode 100644 static/tagulous/lib/select2-4/js/i18n/gl.js create mode 100644 static/tagulous/lib/select2-4/js/i18n/he.js create mode 100644 static/tagulous/lib/select2-4/js/i18n/hi.js create mode 100644 static/tagulous/lib/select2-4/js/i18n/hr.js create mode 100644 static/tagulous/lib/select2-4/js/i18n/hsb.js create mode 100644 static/tagulous/lib/select2-4/js/i18n/hu.js create mode 100644 static/tagulous/lib/select2-4/js/i18n/hy.js create mode 100644 static/tagulous/lib/select2-4/js/i18n/id.js create mode 100644 static/tagulous/lib/select2-4/js/i18n/is.js create mode 100644 static/tagulous/lib/select2-4/js/i18n/it.js create mode 100644 static/tagulous/lib/select2-4/js/i18n/ja.js create mode 100644 static/tagulous/lib/select2-4/js/i18n/ka.js create mode 100644 static/tagulous/lib/select2-4/js/i18n/km.js create mode 100644 static/tagulous/lib/select2-4/js/i18n/ko.js create mode 100644 static/tagulous/lib/select2-4/js/i18n/lt.js create mode 100644 static/tagulous/lib/select2-4/js/i18n/lv.js create mode 100644 static/tagulous/lib/select2-4/js/i18n/mk.js create mode 100644 static/tagulous/lib/select2-4/js/i18n/ms.js create mode 100644 static/tagulous/lib/select2-4/js/i18n/nb.js create mode 100644 static/tagulous/lib/select2-4/js/i18n/ne.js create mode 100644 static/tagulous/lib/select2-4/js/i18n/nl.js create mode 100644 static/tagulous/lib/select2-4/js/i18n/pa.js create mode 100644 static/tagulous/lib/select2-4/js/i18n/pl.js create mode 100644 static/tagulous/lib/select2-4/js/i18n/ps.js create mode 100644 static/tagulous/lib/select2-4/js/i18n/pt-BR.js create mode 100644 static/tagulous/lib/select2-4/js/i18n/pt.js create mode 100644 static/tagulous/lib/select2-4/js/i18n/ro.js create mode 100644 static/tagulous/lib/select2-4/js/i18n/ru.js create mode 100644 static/tagulous/lib/select2-4/js/i18n/sk.js create mode 100644 static/tagulous/lib/select2-4/js/i18n/sl.js create mode 100644 static/tagulous/lib/select2-4/js/i18n/sq.js create mode 100644 static/tagulous/lib/select2-4/js/i18n/sr-Cyrl.js create mode 100644 static/tagulous/lib/select2-4/js/i18n/sr.js create mode 100644 static/tagulous/lib/select2-4/js/i18n/sv.js create mode 100644 static/tagulous/lib/select2-4/js/i18n/te.js create mode 100644 static/tagulous/lib/select2-4/js/i18n/th.js create mode 100644 static/tagulous/lib/select2-4/js/i18n/tk.js create mode 100644 static/tagulous/lib/select2-4/js/i18n/tr.js create mode 100644 static/tagulous/lib/select2-4/js/i18n/uk.js create mode 100644 static/tagulous/lib/select2-4/js/i18n/vi.js create mode 100644 static/tagulous/lib/select2-4/js/i18n/zh-CN.js create mode 100644 static/tagulous/lib/select2-4/js/i18n/zh-TW.js create mode 100644 static/tagulous/lib/select2-4/js/select2.full.js create mode 100644 static/tagulous/lib/select2-4/js/select2.full.min.js create mode 100644 static/tagulous/lib/select2-4/js/select2.js create mode 100644 static/tagulous/lib/select2-4/js/select2.min.js diff --git a/.gitignore b/.gitignore index 0f542f7c..27b475f8 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,7 @@ downloads/ eggs/ .eggs/ lib/ +!static/tagulous/lib/ lib64/ parts/ sdist/ diff --git a/api/views.py b/api/views.py index f4cd685c..73051128 100644 --- a/api/views.py +++ b/api/views.py @@ -921,25 +921,25 @@ def update_app_status(request: HttpRequest) -> HttpResponse: if result == HandleUpdateStatusResponseCode.NO_ACTION: return Response( - "OK. NO_ACTION. No action performed. Possibly the event time is older \ - than the currently stored time.", + f"OK. NO_ACTION. No action performed. Possibly the event time is older \ + than the currently stored time. {release=}, {new_status=}", 200, ) elif result == HandleUpdateStatusResponseCode.CREATED_FIRST_STATUS: - return Response("OK. CREATED_FIRST_STATUS. Created a missing AppStatus.", 200) + return Response(f"OK. CREATED_FIRST_STATUS. Created missing AppStatus. {release=}, {new_status=}", 200) elif result == HandleUpdateStatusResponseCode.UPDATED_STATUS: return Response( - "OK. UPDATED_STATUS. Updated the app status. \ - Determined that the submitted event was newer and different status.", + f"OK. UPDATED_STATUS. Updated the app status. \ + Determined that the submitted event was newer and different status. {release=}, {new_status=}", 200, ) elif result == HandleUpdateStatusResponseCode.UPDATED_TIME_OF_STATUS: return Response( - "OK. UPDATED_TIME_OF_STATUS. Updated only the event time of the status. \ - Determined that the new and old status codes are the same.", + f"OK. UPDATED_TIME_OF_STATUS. Updated only the event time of the status. \ + Determined that the new and old status codes are the same. {release=}, {new_status=}", 200, ) diff --git a/apps/helpers.py b/apps/helpers.py index 686d4446..71ca51ec 100644 --- a/apps/helpers.py +++ b/apps/helpers.py @@ -124,6 +124,7 @@ def handle_update_status_request( # We wrap the select and update tasks in a select_for_update lock # to avoid race conditions. + # TODO: Check this. Seems like this is OK for the modified app instance. subdomain = Subdomain.objects.get(subdomain=release) with transaction.atomic(): @@ -245,6 +246,29 @@ def create_instance_from_form(form, project, app_slug, app_id=None): """ from .tasks import deploy_resource + new_app = app_id is None + + logger.debug(f"Creating or updating a user app via UI form for app_id={app_id}, new_app={new_app}") + + # Do not deploy resource for edits that do not require a k8s re-deployment + do_deploy = False + + if new_app: + do_deploy = True + else: + # Only re-deploy existing apps if one of the following fields was changed: + redeployment_fields = ["subdomain", "volume", "path", "flavor", "port", "image"] + logger.debug(f"An existing app has changed. The changed form fields: {form.changed_data}") + + # Because not all forms contain all fields, we check if the supposedly changed field + # is actually contained in the form + for field in form.changed_data: + if field.lower() in redeployment_fields and ( + field.lower() in form.Meta.fields or field.lower() == "subdomain" + ): + # subdomain is a special field not contained in meta fields + do_deploy = True + subdomain_name, is_created_by_user = get_subdomain_name(form) instance = form.save(commit=False) @@ -266,7 +290,11 @@ def create_instance_from_form(form, project, app_slug, app_id=None): setup_instance(instance, subdomain, app, project, status) save_instance_and_related_data(instance, form) - deploy_resource.delay(instance.serialize()) + if do_deploy: + logger.debug(f"Now deploying resource app with app_id = {app_id}") + deploy_resource.delay(instance.serialize()) + else: + logger.debug(f"Not re-deploying this app with app_id = {app_id}") def get_subdomain_name(form): @@ -284,6 +312,7 @@ def handle_subdomain_change(instance, subdomain, subdomain_name): from .tasks import delete_resource if instance.subdomain.subdomain != subdomain_name: + # The user modified the subdomain name # In this special case, we avoid async task. delete_resource(instance.serialize()) old_subdomain = instance.subdomain diff --git a/cypress/e2e/ui-tests/test-deploy-app.cy.js b/cypress/e2e/ui-tests/test-deploy-app.cy.js index 7788aa25..ca80303e 100644 --- a/cypress/e2e/ui-tests/test-deploy-app.cy.js +++ b/cypress/e2e/ui-tests/test-deploy-app.cy.js @@ -435,7 +435,7 @@ describe("Test deploying app", () => { } }) - it("can modify app settings resulting in no k8s redeployment shows correct app status", { defaultCommandTimeout: defaultCmdTimeoutMs }, () => { + it("can modify app settings resulting in NO k8s redeployment shows correct app status", { defaultCommandTimeout: defaultCmdTimeoutMs }, () => { // An advanced test to verify user can modify app settings such as the name and description // Names of objects to create const project_name = "e2e-deploy-app-test" @@ -618,7 +618,95 @@ describe("Test deploying app", () => { } }) - it("can set and change custom subdomain", { defaultCommandTimeout: defaultCmdTimeoutMs }, () => { + it("can set and change subdomain", { defaultCommandTimeout: defaultCmdTimeoutMs }, () => { + // A test to verify creating an app and changing the subdomain + const project_name = "e2e-deploy-app-test" + const app_name = "e2e-subdomain-change" + const app_description = "e2e-subdomain-change-description" + const source_code_url = "https://doi.org/example" + const image_name = "ghcr.io/scilifelabdatacentre/dash-covid-in-sweden:20240117-063059" + const image_port = "8000" + const createResources = Cypress.env('create_resources'); + const app_type = "Dash App" + const subdomain_change = "subdomain-change" + + if (createResources === true) { + // Create Dash app without custom subdomain + cy.logf("Creating a dash app", Cypress.currentTest) + cy.visit("/projects/") + cy.contains('.card-title', project_name).parents('.card-body').siblings('.card-footer').find('a:contains("Open")').first().click() + cy.get('div.card-body:contains("' + app_type + '")').find('a:contains("Create")').click() + cy.get('#id_name').type(app_name) + cy.get('#id_description').type(app_description) + cy.get('#id_access').select('Public') + cy.get('#id_source_code_url').type(source_code_url) + cy.get('#id_image').clear().type(image_name) + cy.get('#id_port').clear().type(image_port) + cy.get('#submit-id-submit').contains('Submit').click() + // Back on project page + cy.url().should("not.include", "/apps/settings") + cy.get('h3').should('have.text', project_name); + // check that the app was created + verifyAppStatus(app_name, "Running", "public") + + // Verify Dash app values + cy.logf("Checking that all dash app settings were saved", Cypress.currentTest) + cy.visit("/projects/") + cy.contains('.card-title', project_name).parents('.card-body').siblings('.card-footer').find('a:contains("Open")').first().click() + cy.get('tr:contains("' + app_name + '")').find('i.bi-three-dots-vertical').click() + cy.get('tr:contains("' + app_name + '")').find('a').contains('Settings').click() + cy.get('#id_name').should('have.value', app_name) + cy.get('#id_description').should('have.value', app_description) + cy.get('#id_access').find(':selected').should('contain', 'Public') + cy.get('#id_image').should('have.value', image_name) + cy.get('#id_port').should('have.value', image_port) + + // Edit Dash app: change the suibdomain to a custom value + cy.logf("Editing the dash app settings field subdomain", Cypress.currentTest) + cy.visit("/projects/") + cy.contains('.card-title', project_name).parents('.card-body').siblings('.card-footer').find('a:contains("Open")').first().click() + cy.get('tr:contains("' + app_name + '")').find('i.bi-three-dots-vertical').click() + cy.get('tr:contains("' + app_name + '")').find('a').contains('Settings').click() + cy.get('#id_subdomain').clear().type(subdomain_change) + cy.get('#submit-id-submit').contains('Submit').click() + // Back on project page + cy.url().should("not.include", "/apps/settings") + cy.get('h3').should('have.text', project_name); + // Verify that the app status now equals Running + verifyAppStatus(app_name, "Running", "public") + + // Wait for 5 seconds and check the app status again + cy.wait(5000).then(() => { + verifyAppStatus(app_name, "Running", "public") + }) + + // Delete the Dash app + cy.logf("Deleting the dash app", Cypress.currentTest) + cy.visit("/projects/") + cy.contains('.card-title', project_name).parents('.card-body').siblings('.card-footer').find('a:contains("Open")').first().click() + cy.get('tr:contains("' + app_name + '")').find('i.bi-three-dots-vertical').click() + cy.get('tr:contains("' + app_name + '")').find('a.confirm-delete').click() + cy.get('button').contains('Delete').click() + verifyAppStatus(app_name, "Deleted", "") + + // check that the app is not visible under public apps + cy.visit('/apps/') + cy.get("title").should("have.text", "Apps | SciLifeLab Serve (beta)") + cy.get('h3').should('contain', 'Public apps') + cy.contains('h5.card-title', app_name).should('not.exist') + + } else { + cy.logf('Skipped because create_resources is not true', Cypress.currentTest); + } + + }) + + it("can set and change custom subdomain several times", { defaultCommandTimeout: defaultCmdTimeoutMs }, () => { + // An advanced test to verify creating apps and changing subdomains. Steps taken: + // 1. Create app e2e-subdomain-example, subdomain=subdomain-test + // 2. Attempt create app e2e-second-subdomain-example, using subdomain=subdomain-test + // 3. Create app e2e-second-subdomain-example, subdomain=subdomain-test2 + // 4. Change the subdomain of the first app to subdomain=subdomain-test3 // Names of objects to create const project_name = "e2e-deploy-app-test" const app_name = "e2e-subdomain-example" @@ -640,14 +728,14 @@ describe("Test deploying app", () => { cy.logf("Now creating an app with a custom subdomain", Cypress.currentTest) cy.get('div.card-body:contains("' + app_type + '")').find('a:contains("Create")').click() // fill out other fields - cy.get('#id_name').type(app_name) - cy.get('#id_description').type(app_description) + cy.get('#id_name').clear().type(app_name) + cy.get('#id_description').clear().type(app_description) cy.get('#id_port').clear().type("8501") cy.get('#id_image').clear().type(image_name) cy.get('#id_volume').select(volume_display_text) cy.get('#id_path').clear().type("/home") // fill out subdomain field - cy.get('#id_subdomain').type(subdomain) + cy.get('#id_subdomain').clear().type(subdomain) // create the app cy.get('#submit-id-submit').contains('Submit').click() @@ -658,16 +746,16 @@ describe("Test deploying app", () => { cy.logf("Now trying to create an app with an already taken subdomain", Cypress.currentTest) cy.get('div.card-body:contains("' + app_type + '")').find('a:contains("Create")').click() - cy.get('#id_name').type(app_name_2) + cy.get('#id_name').clear().type(app_name_2) cy.get('#id_port').clear().type("8501") cy.get('#id_image').clear().type(image_name) // fill out subdomain field - cy.get('#id_subdomain').type(subdomain) + cy.get('#id_subdomain').clear().type(subdomain) cy.get('#id_subdomain').blur(); cy.get('#div_id_subdomain').should('contain.text', 'The subdomain is not available'); - + // instead use a new subdomain cy.get('#id_subdomain').clear().type(subdomain_2) cy.get('#id_subdomain').blur(); cy.get('#div_id_subdomain').should('contain.text', 'The subdomain is available'); @@ -682,7 +770,7 @@ describe("Test deploying app", () => { cy.contains('.card-title', project_name).parents('.card-body').siblings('.card-footer').find('a:contains("Open")').first().click() cy.get('tr:contains("' + app_name + '")').find('i.bi-three-dots-vertical').click() cy.get('tr:contains("' + app_name + '")').find('a').contains("Settings").click() - cy.get('#id_subdomain').type(subdomain_3) + cy.get('#id_subdomain').clear().type(subdomain_3) cy.get('#submit-id-submit').contains('Submit').click() // check that the app was updated with the correct subdomain @@ -691,7 +779,7 @@ describe("Test deploying app", () => { // Verify that the app status is not Deleted (Deleting and Created ok) cy.get('tr:contains("' + app_name + '")').find('span').should('not.contain', 'Deleted') // Finally verify status equals Running - verifyAppStatus(app_name, "Running", "") + verifyAppStatus(app_name, "Running", "") // TODO: Here. Fix this! // Wait for 5 seconds and check the app status again cy.wait(5000).then(() => { diff --git a/docker-compose.yaml b/docker-compose.yaml index 28318f7d..3bb7df0d 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -122,7 +122,7 @@ services: event-listener: user: "${UID}:${GID}" container_name: event-listener - image: ghcr.io/scilifelabdatacentre/serve-event-listener/event-listener:v0.1.5 + image: ghcr.io/scilifelabdatacentre/serve-event-listener/event-listener:v0.1.8 env_file: - .env volumes: diff --git a/static/tagulous/lib/jquery.js b/static/tagulous/lib/jquery.js new file mode 100644 index 00000000..c4c6022f --- /dev/null +++ b/static/tagulous/lib/jquery.js @@ -0,0 +1,2 @@ +/*! jQuery v3.6.0 | (c) OpenJS Foundation and other contributors | jquery.org/license */ +!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";var t=[],r=Object.getPrototypeOf,s=t.slice,g=t.flat?function(e){return t.flat.call(e)}:function(e){return t.concat.apply([],e)},u=t.push,i=t.indexOf,n={},o=n.toString,v=n.hasOwnProperty,a=v.toString,l=a.call(Object),y={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType&&"function"!=typeof e.item},x=function(e){return null!=e&&e===e.window},E=C.document,c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.6.0",S=function(e,t){return new S.fn.init(e,t)};function p(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp(F),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(p.childNodes),p.childNodes),t[p.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!N[t+" "]&&(!v||!v.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&(U.test(t)||z.test(t))){(f=ee.test(t)&&ye(e.parentNode)||e)===e&&d.scope||((s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=S)),o=(l=h(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+xe(l[o]);c=l.join(",")}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){N(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return g(t.replace($,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[S]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e&&e.namespaceURI,n=e&&(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:p;return r!=C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),p!=C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.scope=ce(function(e){return a.appendChild(e).appendChild(C.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=S,!C.getElementsByName||!C.getElementsByName(S).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+S+"-]").length||v.push("~="),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||v.push("\\["+M+"*name"+M+"*="+M+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+S+"+*").length||v.push(".#.+[+~]"),e.querySelectorAll("\\\f"),v.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",F)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),y=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},j=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e==C||e.ownerDocument==p&&y(p,e)?-1:t==C||t.ownerDocument==p&&y(p,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==C?-1:t==C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]==p?-1:s[r]==p?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(T(e),d.matchesSelector&&E&&!N[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){N(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,n,r){return m(n)?S.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?S.grep(e,function(e){return e===n!==r}):"string"!=typeof n?S.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(S.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||D,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:q.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof S?t[0]:t,S.merge(this,S.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),N.test(r[1])&&S.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(S):S.makeArray(e,this)}).prototype=S.fn,D=S(E);var L=/^(?:parents|prev(?:Until|All))/,H={children:!0,contents:!0,next:!0,prev:!0};function O(e,t){while((e=e[t])&&1!==e.nodeType);return e}S.fn.extend({has:function(e){var t=S(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i;ce=E.createDocumentFragment().appendChild(E.createElement("div")),(fe=E.createElement("input")).setAttribute("type","radio"),fe.setAttribute("checked","checked"),fe.setAttribute("name","t"),ce.appendChild(fe),y.checkClone=ce.cloneNode(!0).cloneNode(!0).lastChild.checked,ce.innerHTML="",y.noCloneChecked=!!ce.cloneNode(!0).lastChild.defaultValue,ce.innerHTML="",y.option=!!ce.lastChild;var ge={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function ve(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?S.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;n",""]);var me=/<|&#?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function je(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&S(e).children("tbody")[0]||e}function De(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function qe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Le(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var _t,zt=[],Ut=/(=)\?(?=&|$)|\?\?/;S.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=zt.pop()||S.expando+"_"+wt.guid++;return this[e]=!0,e}}),S.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Ut.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Ut.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Ut,"$1"+r):!1!==e.jsonp&&(e.url+=(Tt.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||S.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?S(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,zt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),y.createHTMLDocument=((_t=E.implementation.createHTMLDocument("").body).innerHTML="
",2===_t.childNodes.length),S.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(y.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=N.exec(e))?[t.createElement(i[1])]:(i=xe([e],t,o),o&&o.length&&S(o).remove(),S.merge([],i.childNodes)));var r,i,o},S.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(S.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},S.expr.pseudos.animated=function(t){return S.grep(S.timers,function(e){return t===e.elem}).length},S.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=S.css(e,"position"),c=S(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=S.css(e,"top"),u=S.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,S.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},S.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){S.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===S.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===S.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=S(e).offset()).top+=S.css(e,"borderTopWidth",!0),i.left+=S.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-S.css(r,"marginTop",!0),left:t.left-i.left-S.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===S.css(e,"position"))e=e.offsetParent;return e||re})}}),S.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;S.fn[t]=function(e){return $(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),S.each(["top","left"],function(e,n){S.cssHooks[n]=Fe(y.pixelPosition,function(e,t){if(t)return t=We(e,n),Pe.test(t)?S(e).position()[n]+"px":t})}),S.each({Height:"height",Width:"width"},function(a,s){S.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){S.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return $(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?S.css(e,t,i):S.style(e,t,n,i)},s,n?e:void 0,n)}})}),S.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){S.fn[t]=function(e){return this.on(t,e)}}),S.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),S.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){S.fn[n]=function(e,t){return 0v)e5ZBQx4|Y-Q?nr@Px3?9h(3ZWr3^tj=`TP57gKr87N$ zp2wWee1GRRCwo_xahnw)5cxNPJbCg2L6DV|6`#+yw6v6!mDS$f9-JvFD^n;GQ&UrZ zzh5jCkByB101O60U0q#p_1BM>Cv-vP?&s4@g_((4_1L=L$(a91)0=J91Gas#R{McE znYG^9*0A5YZ>#;~+Wkn(W5B0^yELIYLP!K}mB~<)AM@1&nqekynuaEGqPrzoH|KodRXJy)%+w_fu3nE5>@Bd_b zqC$EQ;{c`T&?EsNO|igL9gC7Ygxv?aQUEXMq?~>wg{EyW;VcJ37CUF#HjrT=KQO_* zS>M9yydXk18D(+QDJ1>r);Lav_uYKp$T?4vr{Q$lTo&pKv^?(>L-)G2*lwH!Ah7k? z7oH<8h-(KTKt5V6$8gF)C7Io&P5=SjTh)=zV=E2EUhQZP##L8S{d%UK>>+y82>+FV+#^BzW7u3F)Bb>=lYQ%%j`F>ASe zo*cw@V#u6T`A2He;70mR(V&iV&-7{qP~=SRf&jm9-T{*ZeZ}$rd0#6c&fLG^xJcf5 z+p<`wJYgW+_s*V{uI$nMB;%8`S_3>PfGOj3Rq}@Cx^+j?rk92fANSFDBYnOqQ>Vdj z)(|$AhP4t&Lb=Gvo2#3Gl%9<=Gv`Mz?Po@P4iLF!x}GUWJICDlFk-hS^Whyh7x~VH z@0vD1>HYD4&e+~yzS*-sFR{9`{QEEZO1zg7>R&7cHts-6j!xHVdA8eI+ZlVzd%`es zJT@$#GX(gvCJ1oJN%yLBK}{V=V;seo;!w|Yte!W1%5qLNFWqvZW>h&IiH+oPT=b@E zPhGzv5=(Un*X>v`>%8h_nj^NdYcE6NHS_ifkCV$*D)Tqrbu`s;<=t<4 zAHNqNV?6(g<1PY-w@#I-WYFViz?9TrkMr)u0g`O`u|>T;k|2sV*YF^punvT;$SuTy{j3Gv)yqD!R_CF>yR)MzmmYS5v+~R zXAdD%ng9?df;wd8GxR#%3O+gz};Vo;)sK%Bj-q>Oq%R7JU-KD?vYu>#2UjaDo z&8$>5xW~?KPD_#XFToU1hIb*VOMidUr6iYiO0N|i-7s`T8!cFT`rN!^1Pt78J93i6 z5HI1wIM$94m{3SLDvISDe6$ZG1;eq_D9RTaaC>=cO{@Bs>$IlPCPJJ$h$)-3vzNUQ6OsN#_zWxey!_9%hxwH2_dEJi=yY|1c7nDm2_Lm!Cof8-R_+9UkS zcBE(o47yE)oMR(Q=dp1a2wTX5KvvGyLqlWTa7V&!A*|w|)ax~1_~aJ0=_Lilg*0iQk7#ZD EAHN$8j{pDw literal 0 HcmV?d00001 diff --git a/static/tagulous/lib/select2-3/select2.css b/static/tagulous/lib/select2-3/select2.css new file mode 100644 index 00000000..2d07a034 --- /dev/null +++ b/static/tagulous/lib/select2-3/select2.css @@ -0,0 +1,704 @@ +/* +Version: 3.5.2 Timestamp: Sat Nov 1 14:43:36 EDT 2014 +*/ +.select2-container { + margin: 0; + position: relative; + display: inline-block; + /* inline-block for ie7 */ + zoom: 1; + *display: inline; + vertical-align: middle; +} + +.select2-container, +.select2-drop, +.select2-search, +.select2-search input { + /* + Force border-box so that % widths fit the parent + container without overlap because of margin/padding. + More Info : http://www.quirksmode.org/css/box.html + */ + -webkit-box-sizing: border-box; /* webkit */ + -moz-box-sizing: border-box; /* firefox */ + box-sizing: border-box; /* css3 */ +} + +.select2-container .select2-choice { + display: block; + height: 26px; + padding: 0 0 0 8px; + overflow: hidden; + position: relative; + + border: 1px solid #aaa; + white-space: nowrap; + line-height: 26px; + color: #444; + text-decoration: none; + + border-radius: 4px; + + background-clip: padding-box; + + -webkit-touch-callout: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + + background-color: #fff; + background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #eee), color-stop(0.5, #fff)); + background-image: -webkit-linear-gradient(center bottom, #eee 0%, #fff 50%); + background-image: -moz-linear-gradient(center bottom, #eee 0%, #fff 50%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr = '#ffffff', endColorstr = '#eeeeee', GradientType = 0); + background-image: linear-gradient(to top, #eee 0%, #fff 50%); +} + +html[dir="rtl"] .select2-container .select2-choice { + padding: 0 8px 0 0; +} + +.select2-container.select2-drop-above .select2-choice { + border-bottom-color: #aaa; + + border-radius: 0 0 4px 4px; + + background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #eee), color-stop(0.9, #fff)); + background-image: -webkit-linear-gradient(center bottom, #eee 0%, #fff 90%); + background-image: -moz-linear-gradient(center bottom, #eee 0%, #fff 90%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#eeeeee', GradientType=0); + background-image: linear-gradient(to bottom, #eee 0%, #fff 90%); +} + +.select2-container.select2-allowclear .select2-choice .select2-chosen { + margin-right: 42px; +} + +.select2-container .select2-choice > .select2-chosen { + margin-right: 26px; + display: block; + overflow: hidden; + + white-space: nowrap; + + text-overflow: ellipsis; + float: none; + width: auto; +} + +html[dir="rtl"] .select2-container .select2-choice > .select2-chosen { + margin-left: 26px; + margin-right: 0; +} + +.select2-container .select2-choice abbr { + display: none; + width: 12px; + height: 12px; + position: absolute; + right: 24px; + top: 8px; + + font-size: 1px; + text-decoration: none; + + border: 0; + background: url('select2.png') right top no-repeat; + cursor: pointer; + outline: 0; +} + +.select2-container.select2-allowclear .select2-choice abbr { + display: inline-block; +} + +.select2-container .select2-choice abbr:hover { + background-position: right -11px; + cursor: pointer; +} + +.select2-drop-mask { + border: 0; + margin: 0; + padding: 0; + position: fixed; + left: 0; + top: 0; + min-height: 100%; + min-width: 100%; + height: auto; + width: auto; + opacity: 0; + z-index: 9998; + /* styles required for IE to work */ + background-color: #fff; + filter: alpha(opacity=0); +} + +.select2-drop { + width: 100%; + margin-top: -1px; + position: absolute; + z-index: 9999; + top: 100%; + + background: #fff; + color: #000; + border: 1px solid #aaa; + border-top: 0; + + border-radius: 0 0 4px 4px; + + -webkit-box-shadow: 0 4px 5px rgba(0, 0, 0, .15); + box-shadow: 0 4px 5px rgba(0, 0, 0, .15); +} + +.select2-drop.select2-drop-above { + margin-top: 1px; + border-top: 1px solid #aaa; + border-bottom: 0; + + border-radius: 4px 4px 0 0; + + -webkit-box-shadow: 0 -4px 5px rgba(0, 0, 0, .15); + box-shadow: 0 -4px 5px rgba(0, 0, 0, .15); +} + +.select2-drop-active { + border: 1px solid #5897fb; + border-top: none; +} + +.select2-drop.select2-drop-above.select2-drop-active { + border-top: 1px solid #5897fb; +} + +.select2-drop-auto-width { + border-top: 1px solid #aaa; + width: auto; +} + +.select2-drop-auto-width .select2-search { + padding-top: 4px; +} + +.select2-container .select2-choice .select2-arrow { + display: inline-block; + width: 18px; + height: 100%; + position: absolute; + right: 0; + top: 0; + + border-left: 1px solid #aaa; + border-radius: 0 4px 4px 0; + + background-clip: padding-box; + + background: #ccc; + background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #ccc), color-stop(0.6, #eee)); + background-image: -webkit-linear-gradient(center bottom, #ccc 0%, #eee 60%); + background-image: -moz-linear-gradient(center bottom, #ccc 0%, #eee 60%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr = '#eeeeee', endColorstr = '#cccccc', GradientType = 0); + background-image: linear-gradient(to top, #ccc 0%, #eee 60%); +} + +html[dir="rtl"] .select2-container .select2-choice .select2-arrow { + left: 0; + right: auto; + + border-left: none; + border-right: 1px solid #aaa; + border-radius: 4px 0 0 4px; +} + +.select2-container .select2-choice .select2-arrow b { + display: block; + width: 100%; + height: 100%; + background: url('select2.png') no-repeat 0 1px; +} + +html[dir="rtl"] .select2-container .select2-choice .select2-arrow b { + background-position: 2px 1px; +} + +.select2-search { + display: inline-block; + width: 100%; + min-height: 26px; + margin: 0; + padding-left: 4px; + padding-right: 4px; + + position: relative; + z-index: 10000; + + white-space: nowrap; +} + +.select2-search input { + width: 100%; + height: auto !important; + min-height: 26px; + padding: 4px 20px 4px 5px; + margin: 0; + + outline: 0; + font-family: sans-serif; + font-size: 1em; + + border: 1px solid #aaa; + border-radius: 0; + + -webkit-box-shadow: none; + box-shadow: none; + + background: #fff url('select2.png') no-repeat 100% -22px; + background: url('select2.png') no-repeat 100% -22px, -webkit-gradient(linear, left bottom, left top, color-stop(0.85, #fff), color-stop(0.99, #eee)); + background: url('select2.png') no-repeat 100% -22px, -webkit-linear-gradient(center bottom, #fff 85%, #eee 99%); + background: url('select2.png') no-repeat 100% -22px, -moz-linear-gradient(center bottom, #fff 85%, #eee 99%); + background: url('select2.png') no-repeat 100% -22px, linear-gradient(to bottom, #fff 85%, #eee 99%) 0 0; +} + +html[dir="rtl"] .select2-search input { + padding: 4px 5px 4px 20px; + + background: #fff url('select2.png') no-repeat -37px -22px; + background: url('select2.png') no-repeat -37px -22px, -webkit-gradient(linear, left bottom, left top, color-stop(0.85, #fff), color-stop(0.99, #eee)); + background: url('select2.png') no-repeat -37px -22px, -webkit-linear-gradient(center bottom, #fff 85%, #eee 99%); + background: url('select2.png') no-repeat -37px -22px, -moz-linear-gradient(center bottom, #fff 85%, #eee 99%); + background: url('select2.png') no-repeat -37px -22px, linear-gradient(to bottom, #fff 85%, #eee 99%) 0 0; +} + +.select2-drop.select2-drop-above .select2-search input { + margin-top: 4px; +} + +.select2-search input.select2-active { + background: #fff url('select2-spinner.gif') no-repeat 100%; + background: url('select2-spinner.gif') no-repeat 100%, -webkit-gradient(linear, left bottom, left top, color-stop(0.85, #fff), color-stop(0.99, #eee)); + background: url('select2-spinner.gif') no-repeat 100%, -webkit-linear-gradient(center bottom, #fff 85%, #eee 99%); + background: url('select2-spinner.gif') no-repeat 100%, -moz-linear-gradient(center bottom, #fff 85%, #eee 99%); + background: url('select2-spinner.gif') no-repeat 100%, linear-gradient(to bottom, #fff 85%, #eee 99%) 0 0; +} + +.select2-container-active .select2-choice, +.select2-container-active .select2-choices { + border: 1px solid #5897fb; + outline: none; + + -webkit-box-shadow: 0 0 5px rgba(0, 0, 0, .3); + box-shadow: 0 0 5px rgba(0, 0, 0, .3); +} + +.select2-dropdown-open .select2-choice { + border-bottom-color: transparent; + -webkit-box-shadow: 0 1px 0 #fff inset; + box-shadow: 0 1px 0 #fff inset; + + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + + background-color: #eee; + background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #fff), color-stop(0.5, #eee)); + background-image: -webkit-linear-gradient(center bottom, #fff 0%, #eee 50%); + background-image: -moz-linear-gradient(center bottom, #fff 0%, #eee 50%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#eeeeee', endColorstr='#ffffff', GradientType=0); + background-image: linear-gradient(to top, #fff 0%, #eee 50%); +} + +.select2-dropdown-open.select2-drop-above .select2-choice, +.select2-dropdown-open.select2-drop-above .select2-choices { + border: 1px solid #5897fb; + border-top-color: transparent; + + background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0, #fff), color-stop(0.5, #eee)); + background-image: -webkit-linear-gradient(center top, #fff 0%, #eee 50%); + background-image: -moz-linear-gradient(center top, #fff 0%, #eee 50%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#eeeeee', endColorstr='#ffffff', GradientType=0); + background-image: linear-gradient(to bottom, #fff 0%, #eee 50%); +} + +.select2-dropdown-open .select2-choice .select2-arrow { + background: transparent; + border-left: none; + filter: none; +} +html[dir="rtl"] .select2-dropdown-open .select2-choice .select2-arrow { + border-right: none; +} + +.select2-dropdown-open .select2-choice .select2-arrow b { + background-position: -18px 1px; +} + +html[dir="rtl"] .select2-dropdown-open .select2-choice .select2-arrow b { + background-position: -16px 1px; +} + +.select2-hidden-accessible { + border: 0; + clip: rect(0 0 0 0); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + width: 1px; +} + +/* results */ +.select2-results { + max-height: 200px; + padding: 0 0 0 4px; + margin: 4px 4px 4px 0; + position: relative; + overflow-x: hidden; + overflow-y: auto; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); +} + +html[dir="rtl"] .select2-results { + padding: 0 4px 0 0; + margin: 4px 0 4px 4px; +} + +.select2-results ul.select2-result-sub { + margin: 0; + padding-left: 0; +} + +.select2-results li { + list-style: none; + display: list-item; + background-image: none; +} + +.select2-results li.select2-result-with-children > .select2-result-label { + font-weight: bold; +} + +.select2-results .select2-result-label { + padding: 3px 7px 4px; + margin: 0; + cursor: pointer; + + min-height: 1em; + + -webkit-touch-callout: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.select2-results-dept-1 .select2-result-label { padding-left: 20px } +.select2-results-dept-2 .select2-result-label { padding-left: 40px } +.select2-results-dept-3 .select2-result-label { padding-left: 60px } +.select2-results-dept-4 .select2-result-label { padding-left: 80px } +.select2-results-dept-5 .select2-result-label { padding-left: 100px } +.select2-results-dept-6 .select2-result-label { padding-left: 110px } +.select2-results-dept-7 .select2-result-label { padding-left: 120px } + +.select2-results .select2-highlighted { + background: #3875d7; + color: #fff; +} + +.select2-results li em { + background: #feffde; + font-style: normal; +} + +.select2-results .select2-highlighted em { + background: transparent; +} + +.select2-results .select2-highlighted ul { + background: #fff; + color: #000; +} + +.select2-results .select2-no-results, +.select2-results .select2-searching, +.select2-results .select2-ajax-error, +.select2-results .select2-selection-limit { + background: #f4f4f4; + display: list-item; + padding-left: 5px; +} + +/* +disabled look for disabled choices in the results dropdown +*/ +.select2-results .select2-disabled.select2-highlighted { + color: #666; + background: #f4f4f4; + display: list-item; + cursor: default; +} +.select2-results .select2-disabled { + background: #f4f4f4; + display: list-item; + cursor: default; +} + +.select2-results .select2-selected { + display: none; +} + +.select2-more-results.select2-active { + background: #f4f4f4 url('select2-spinner.gif') no-repeat 100%; +} + +.select2-results .select2-ajax-error { + background: rgba(255, 50, 50, .2); +} + +.select2-more-results { + background: #f4f4f4; + display: list-item; +} + +/* disabled styles */ + +.select2-container.select2-container-disabled .select2-choice { + background-color: #f4f4f4; + background-image: none; + border: 1px solid #ddd; + cursor: default; +} + +.select2-container.select2-container-disabled .select2-choice .select2-arrow { + background-color: #f4f4f4; + background-image: none; + border-left: 0; +} + +.select2-container.select2-container-disabled .select2-choice abbr { + display: none; +} + + +/* multiselect */ + +.select2-container-multi .select2-choices { + height: auto !important; + height: 1%; + margin: 0; + padding: 0 5px 0 0; + position: relative; + + border: 1px solid #aaa; + cursor: text; + overflow: hidden; + + background-color: #fff; + background-image: -webkit-gradient(linear, 0% 0%, 0% 100%, color-stop(1%, #eee), color-stop(15%, #fff)); + background-image: -webkit-linear-gradient(top, #eee 1%, #fff 15%); + background-image: -moz-linear-gradient(top, #eee 1%, #fff 15%); + background-image: linear-gradient(to bottom, #eee 1%, #fff 15%); +} + +html[dir="rtl"] .select2-container-multi .select2-choices { + padding: 0 0 0 5px; +} + +.select2-locked { + padding: 3px 5px 3px 5px !important; +} + +.select2-container-multi .select2-choices { + min-height: 26px; +} + +.select2-container-multi.select2-container-active .select2-choices { + border: 1px solid #5897fb; + outline: none; + + -webkit-box-shadow: 0 0 5px rgba(0, 0, 0, .3); + box-shadow: 0 0 5px rgba(0, 0, 0, .3); +} +.select2-container-multi .select2-choices li { + float: left; + list-style: none; +} +html[dir="rtl"] .select2-container-multi .select2-choices li +{ + float: right; +} +.select2-container-multi .select2-choices .select2-search-field { + margin: 0; + padding: 0; + white-space: nowrap; +} + +.select2-container-multi .select2-choices .select2-search-field input { + padding: 5px; + margin: 1px 0; + + font-family: sans-serif; + font-size: 100%; + color: #666; + outline: 0; + border: 0; + -webkit-box-shadow: none; + box-shadow: none; + background: transparent !important; +} + +.select2-container-multi .select2-choices .select2-search-field input.select2-active { + background: #fff url('select2-spinner.gif') no-repeat 100% !important; +} + +.select2-default { + color: #999 !important; +} + +.select2-container-multi .select2-choices .select2-search-choice { + padding: 3px 5px 3px 18px; + margin: 3px 0 3px 5px; + position: relative; + + line-height: 13px; + color: #333; + cursor: default; + border: 1px solid #aaaaaa; + + border-radius: 3px; + + -webkit-box-shadow: 0 0 2px #fff inset, 0 1px 0 rgba(0, 0, 0, 0.05); + box-shadow: 0 0 2px #fff inset, 0 1px 0 rgba(0, 0, 0, 0.05); + + background-clip: padding-box; + + -webkit-touch-callout: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + + background-color: #e4e4e4; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#eeeeee', endColorstr='#f4f4f4', GradientType=0); + background-image: -webkit-gradient(linear, 0% 0%, 0% 100%, color-stop(20%, #f4f4f4), color-stop(50%, #f0f0f0), color-stop(52%, #e8e8e8), color-stop(100%, #eee)); + background-image: -webkit-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eee 100%); + background-image: -moz-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eee 100%); + background-image: linear-gradient(to bottom, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eee 100%); +} +html[dir="rtl"] .select2-container-multi .select2-choices .select2-search-choice +{ + margin: 3px 5px 3px 0; + padding: 3px 18px 3px 5px; +} +.select2-container-multi .select2-choices .select2-search-choice .select2-chosen { + cursor: default; +} +.select2-container-multi .select2-choices .select2-search-choice-focus { + background: #d4d4d4; +} + +.select2-search-choice-close { + display: block; + width: 12px; + height: 13px; + position: absolute; + right: 3px; + top: 4px; + + font-size: 1px; + outline: none; + background: url('select2.png') right top no-repeat; +} +html[dir="rtl"] .select2-search-choice-close { + right: auto; + left: 3px; +} + +.select2-container-multi .select2-search-choice-close { + left: 3px; +} + +html[dir="rtl"] .select2-container-multi .select2-search-choice-close { + left: auto; + right: 2px; +} + +.select2-container-multi .select2-choices .select2-search-choice .select2-search-choice-close:hover { + background-position: right -11px; +} +.select2-container-multi .select2-choices .select2-search-choice-focus .select2-search-choice-close { + background-position: right -11px; +} + +/* disabled styles */ +.select2-container-multi.select2-container-disabled .select2-choices { + background-color: #f4f4f4; + background-image: none; + border: 1px solid #ddd; + cursor: default; +} + +.select2-container-multi.select2-container-disabled .select2-choices .select2-search-choice { + padding: 3px 5px 3px 5px; + border: 1px solid #ddd; + background-image: none; + background-color: #f4f4f4; +} + +.select2-container-multi.select2-container-disabled .select2-choices .select2-search-choice .select2-search-choice-close { display: none; + background: none; +} +/* end multiselect */ + + +.select2-result-selectable .select2-match, +.select2-result-unselectable .select2-match { + text-decoration: underline; +} + +.select2-offscreen, .select2-offscreen:focus { + clip: rect(0 0 0 0) !important; + width: 1px !important; + height: 1px !important; + border: 0 !important; + margin: 0 !important; + padding: 0 !important; + overflow: hidden !important; + position: absolute !important; + outline: 0 !important; + left: 0px !important; + top: 0px !important; +} + +.select2-display-none { + display: none; +} + +.select2-measure-scrollbar { + position: absolute; + top: -10000px; + left: -10000px; + width: 100px; + height: 100px; + overflow: scroll; +} + +/* Retina-ize icons */ + +@media only screen and (-webkit-min-device-pixel-ratio: 1.5), only screen and (min-resolution: 2dppx) { + .select2-search input, + .select2-search-choice-close, + .select2-container .select2-choice abbr, + .select2-container .select2-choice .select2-arrow b { + background-image: url('select2x2.png') !important; + background-repeat: no-repeat !important; + background-size: 60px 40px !important; + } + + .select2-search input { + background-position: 100% -21px !important; + } +} diff --git a/static/tagulous/lib/select2-3/select2.min.js b/static/tagulous/lib/select2-3/select2.min.js new file mode 100644 index 00000000..999f6b1f --- /dev/null +++ b/static/tagulous/lib/select2-3/select2.min.js @@ -0,0 +1,23 @@ +/* +Copyright 2014 Igor Vaynberg + +Version: 3.5.2 Timestamp: Sat Nov 1 14:43:36 EDT 2014 + +This software is licensed under the Apache License, Version 2.0 (the "Apache License") or the GNU +General Public License version 2 (the "GPL License"). You may choose either license to govern your +use of this software only upon the condition that you accept all of the terms of either the Apache +License or the GPL License. + +You may obtain a copy of the Apache License and the GPL License at: + +http://www.apache.org/licenses/LICENSE-2.0 +http://www.gnu.org/licenses/gpl-2.0.html + +Unless required by applicable law or agreed to in writing, software distributed under the Apache License +or the GPL Licesnse is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, +either express or implied. See the Apache License and the GPL License for the specific language governing +permissions and limitations under the Apache License and the GPL License. +*/ +!function(a){"undefined"==typeof a.fn.each2&&a.extend(a.fn,{each2:function(b){for(var c=a([0]),d=-1,e=this.length;++dc;c+=1)if(r(a,b[c]))return c;return-1}function q(){var b=a(l);b.appendTo(document.body);var c={width:b.width()-b[0].clientWidth,height:b.height()-b[0].clientHeight};return b.remove(),c}function r(a,c){return a===c?!0:a===b||c===b?!1:null===a||null===c?!1:a.constructor===String?a+""==c+"":c.constructor===String?c+""==a+"":!1}function s(a,b,c){var d,e,f;if(null===a||a.length<1)return[];for(d=a.split(b),e=0,f=d.length;f>e;e+=1)d[e]=c(d[e]);return d}function t(a){return a.outerWidth(!1)-a.width()}function u(c){var d="keyup-change-value";c.on("keydown",function(){a.data(c,d)===b&&a.data(c,d,c.val())}),c.on("keyup",function(){var e=a.data(c,d);e!==b&&c.val()!==e&&(a.removeData(c,d),c.trigger("keyup-change"))})}function v(c){c.on("mousemove",function(c){var d=h;(d===b||d.x!==c.pageX||d.y!==c.pageY)&&a(c.target).trigger("mousemove-filtered",c)})}function w(a,c,d){d=d||b;var e;return function(){var b=arguments;window.clearTimeout(e),e=window.setTimeout(function(){c.apply(d,b)},a)}}function x(a,b){var c=w(a,function(a){b.trigger("scroll-debounced",a)});b.on("scroll",function(a){p(a.target,b.get())>=0&&c(a)})}function y(a){a[0]!==document.activeElement&&window.setTimeout(function(){var d,b=a[0],c=a.val().length;a.focus();var e=b.offsetWidth>0||b.offsetHeight>0;e&&b===document.activeElement&&(b.setSelectionRange?b.setSelectionRange(c,c):b.createTextRange&&(d=b.createTextRange(),d.collapse(!1),d.select()))},0)}function z(b){b=a(b)[0];var c=0,d=0;if("selectionStart"in b)c=b.selectionStart,d=b.selectionEnd-c;else if("selection"in document){b.focus();var e=document.selection.createRange();d=document.selection.createRange().text.length,e.moveStart("character",-b.value.length),c=e.text.length-d}return{offset:c,length:d}}function A(a){a.preventDefault(),a.stopPropagation()}function B(a){a.preventDefault(),a.stopImmediatePropagation()}function C(b){if(!g){var c=b[0].currentStyle||window.getComputedStyle(b[0],null);g=a(document.createElement("div")).css({position:"absolute",left:"-10000px",top:"-10000px",display:"none",fontSize:c.fontSize,fontFamily:c.fontFamily,fontStyle:c.fontStyle,fontWeight:c.fontWeight,letterSpacing:c.letterSpacing,textTransform:c.textTransform,whiteSpace:"nowrap"}),g.attr("class","select2-sizer"),a(document.body).append(g)}return g.text(b.val()),g.width()}function D(b,c,d){var e,g,f=[];e=a.trim(b.attr("class")),e&&(e=""+e,a(e.split(/\s+/)).each2(function(){0===this.indexOf("select2-")&&f.push(this)})),e=a.trim(c.attr("class")),e&&(e=""+e,a(e.split(/\s+/)).each2(function(){0!==this.indexOf("select2-")&&(g=d(this),g&&f.push(g))})),b.attr("class",f.join(" "))}function E(a,b,c,d){var e=o(a.toUpperCase()).indexOf(o(b.toUpperCase())),f=b.length;return 0>e?(c.push(d(a)),void 0):(c.push(d(a.substring(0,e))),c.push(""),c.push(d(a.substring(e,e+f))),c.push(""),c.push(d(a.substring(e+f,a.length))),void 0)}function F(a){var b={"\\":"\","&":"&","<":"<",">":">",'"':""","'":"'","/":"/"};return String(a).replace(/[&<>"'\/\\]/g,function(a){return b[a]})}function G(c){var d,e=null,f=c.quietMillis||100,g=c.url,h=this;return function(i){window.clearTimeout(d),d=window.setTimeout(function(){var d=c.data,f=g,j=c.transport||a.fn.select2.ajaxDefaults.transport,k={type:c.type||"GET",cache:c.cache||!1,jsonpCallback:c.jsonpCallback||b,dataType:c.dataType||"json"},l=a.extend({},a.fn.select2.ajaxDefaults.params,k);d=d?d.call(h,i.term,i.page,i.context):null,f="function"==typeof f?f.call(h,i.term,i.page,i.context):f,e&&"function"==typeof e.abort&&e.abort(),c.params&&(a.isFunction(c.params)?a.extend(l,c.params.call(h)):a.extend(l,c.params)),a.extend(l,{url:f,dataType:c.dataType,data:d,success:function(a){var b=c.results(a,i.page,i);i.callback(b)},error:function(a,b,c){var d={hasError:!0,jqXHR:a,textStatus:b,errorThrown:c};i.callback(d)}}),e=j.call(h,l)},f)}}function H(b){var d,e,c=b,f=function(a){return""+a.text};a.isArray(c)&&(e=c,c={results:e}),a.isFunction(c)===!1&&(e=c,c=function(){return e});var g=c();return g.text&&(f=g.text,a.isFunction(f)||(d=g.text,f=function(a){return a[d]})),function(b){var g,d=b.term,e={results:[]};return""===d?(b.callback(c()),void 0):(g=function(c,e){var h,i;if(c=c[0],c.children){h={};for(i in c)c.hasOwnProperty(i)&&(h[i]=c[i]);h.children=[],a(c.children).each2(function(a,b){g(b,h.children)}),(h.children.length||b.matcher(d,f(h),c))&&e.push(h)}else b.matcher(d,f(c),c)&&e.push(c)},a(c().results).each2(function(a,b){g(b,e.results)}),b.callback(e),void 0)}}function I(c){var d=a.isFunction(c);return function(e){var f=e.term,g={results:[]},h=d?c(e):c;a.isArray(h)&&(a(h).each(function(){var a=this.text!==b,c=a?this.text:this;(""===f||e.matcher(f,c))&&g.results.push(a?this:{id:this,text:this})}),e.callback(g))}}function J(b,c){if(a.isFunction(b))return!0;if(!b)return!1;if("string"==typeof b)return!0;throw new Error(c+" must be a string, function, or falsy value")}function K(b,c){if(a.isFunction(b)){var d=Array.prototype.slice.call(arguments,2);return b.apply(c,d)}return b}function L(b){var c=0;return a.each(b,function(a,b){b.children?c+=L(b.children):c++}),c}function M(a,c,d,e){var h,i,j,k,l,f=a,g=!1;if(!e.createSearchChoice||!e.tokenSeparators||e.tokenSeparators.length<1)return b;for(;;){for(i=-1,j=0,k=e.tokenSeparators.length;k>j&&(l=e.tokenSeparators[j],i=a.indexOf(l),!(i>=0));j++);if(0>i)break;if(h=a.substring(0,i),a=a.substring(i+l.length),h.length>0&&(h=e.createSearchChoice.call(this,h,c),h!==b&&null!==h&&e.id(h)!==b&&null!==e.id(h))){for(g=!1,j=0,k=c.length;k>j;j++)if(r(e.id(h),e.id(c[j]))){g=!0;break}g||d(h)}}return f!==a?a:void 0}function N(){var b=this;a.each(arguments,function(a,c){b[c].remove(),b[c]=null})}function O(b,c){var d=function(){};return d.prototype=new b,d.prototype.constructor=d,d.prototype.parent=b.prototype,d.prototype=a.extend(d.prototype,c),d}if(window.Select2===b){var c,d,e,f,g,i,j,h={x:0,y:0},k={TAB:9,ENTER:13,ESC:27,SPACE:32,LEFT:37,UP:38,RIGHT:39,DOWN:40,SHIFT:16,CTRL:17,ALT:18,PAGE_UP:33,PAGE_DOWN:34,HOME:36,END:35,BACKSPACE:8,DELETE:46,isArrow:function(a){switch(a=a.which?a.which:a){case k.LEFT:case k.RIGHT:case k.UP:case k.DOWN:return!0}return!1},isControl:function(a){var b=a.which;switch(b){case k.SHIFT:case k.CTRL:case k.ALT:return!0}return a.metaKey?!0:!1},isFunctionKey:function(a){return a=a.which?a.which:a,a>=112&&123>=a}},l="
",m={"\u24b6":"A","\uff21":"A","\xc0":"A","\xc1":"A","\xc2":"A","\u1ea6":"A","\u1ea4":"A","\u1eaa":"A","\u1ea8":"A","\xc3":"A","\u0100":"A","\u0102":"A","\u1eb0":"A","\u1eae":"A","\u1eb4":"A","\u1eb2":"A","\u0226":"A","\u01e0":"A","\xc4":"A","\u01de":"A","\u1ea2":"A","\xc5":"A","\u01fa":"A","\u01cd":"A","\u0200":"A","\u0202":"A","\u1ea0":"A","\u1eac":"A","\u1eb6":"A","\u1e00":"A","\u0104":"A","\u023a":"A","\u2c6f":"A","\ua732":"AA","\xc6":"AE","\u01fc":"AE","\u01e2":"AE","\ua734":"AO","\ua736":"AU","\ua738":"AV","\ua73a":"AV","\ua73c":"AY","\u24b7":"B","\uff22":"B","\u1e02":"B","\u1e04":"B","\u1e06":"B","\u0243":"B","\u0182":"B","\u0181":"B","\u24b8":"C","\uff23":"C","\u0106":"C","\u0108":"C","\u010a":"C","\u010c":"C","\xc7":"C","\u1e08":"C","\u0187":"C","\u023b":"C","\ua73e":"C","\u24b9":"D","\uff24":"D","\u1e0a":"D","\u010e":"D","\u1e0c":"D","\u1e10":"D","\u1e12":"D","\u1e0e":"D","\u0110":"D","\u018b":"D","\u018a":"D","\u0189":"D","\ua779":"D","\u01f1":"DZ","\u01c4":"DZ","\u01f2":"Dz","\u01c5":"Dz","\u24ba":"E","\uff25":"E","\xc8":"E","\xc9":"E","\xca":"E","\u1ec0":"E","\u1ebe":"E","\u1ec4":"E","\u1ec2":"E","\u1ebc":"E","\u0112":"E","\u1e14":"E","\u1e16":"E","\u0114":"E","\u0116":"E","\xcb":"E","\u1eba":"E","\u011a":"E","\u0204":"E","\u0206":"E","\u1eb8":"E","\u1ec6":"E","\u0228":"E","\u1e1c":"E","\u0118":"E","\u1e18":"E","\u1e1a":"E","\u0190":"E","\u018e":"E","\u24bb":"F","\uff26":"F","\u1e1e":"F","\u0191":"F","\ua77b":"F","\u24bc":"G","\uff27":"G","\u01f4":"G","\u011c":"G","\u1e20":"G","\u011e":"G","\u0120":"G","\u01e6":"G","\u0122":"G","\u01e4":"G","\u0193":"G","\ua7a0":"G","\ua77d":"G","\ua77e":"G","\u24bd":"H","\uff28":"H","\u0124":"H","\u1e22":"H","\u1e26":"H","\u021e":"H","\u1e24":"H","\u1e28":"H","\u1e2a":"H","\u0126":"H","\u2c67":"H","\u2c75":"H","\ua78d":"H","\u24be":"I","\uff29":"I","\xcc":"I","\xcd":"I","\xce":"I","\u0128":"I","\u012a":"I","\u012c":"I","\u0130":"I","\xcf":"I","\u1e2e":"I","\u1ec8":"I","\u01cf":"I","\u0208":"I","\u020a":"I","\u1eca":"I","\u012e":"I","\u1e2c":"I","\u0197":"I","\u24bf":"J","\uff2a":"J","\u0134":"J","\u0248":"J","\u24c0":"K","\uff2b":"K","\u1e30":"K","\u01e8":"K","\u1e32":"K","\u0136":"K","\u1e34":"K","\u0198":"K","\u2c69":"K","\ua740":"K","\ua742":"K","\ua744":"K","\ua7a2":"K","\u24c1":"L","\uff2c":"L","\u013f":"L","\u0139":"L","\u013d":"L","\u1e36":"L","\u1e38":"L","\u013b":"L","\u1e3c":"L","\u1e3a":"L","\u0141":"L","\u023d":"L","\u2c62":"L","\u2c60":"L","\ua748":"L","\ua746":"L","\ua780":"L","\u01c7":"LJ","\u01c8":"Lj","\u24c2":"M","\uff2d":"M","\u1e3e":"M","\u1e40":"M","\u1e42":"M","\u2c6e":"M","\u019c":"M","\u24c3":"N","\uff2e":"N","\u01f8":"N","\u0143":"N","\xd1":"N","\u1e44":"N","\u0147":"N","\u1e46":"N","\u0145":"N","\u1e4a":"N","\u1e48":"N","\u0220":"N","\u019d":"N","\ua790":"N","\ua7a4":"N","\u01ca":"NJ","\u01cb":"Nj","\u24c4":"O","\uff2f":"O","\xd2":"O","\xd3":"O","\xd4":"O","\u1ed2":"O","\u1ed0":"O","\u1ed6":"O","\u1ed4":"O","\xd5":"O","\u1e4c":"O","\u022c":"O","\u1e4e":"O","\u014c":"O","\u1e50":"O","\u1e52":"O","\u014e":"O","\u022e":"O","\u0230":"O","\xd6":"O","\u022a":"O","\u1ece":"O","\u0150":"O","\u01d1":"O","\u020c":"O","\u020e":"O","\u01a0":"O","\u1edc":"O","\u1eda":"O","\u1ee0":"O","\u1ede":"O","\u1ee2":"O","\u1ecc":"O","\u1ed8":"O","\u01ea":"O","\u01ec":"O","\xd8":"O","\u01fe":"O","\u0186":"O","\u019f":"O","\ua74a":"O","\ua74c":"O","\u01a2":"OI","\ua74e":"OO","\u0222":"OU","\u24c5":"P","\uff30":"P","\u1e54":"P","\u1e56":"P","\u01a4":"P","\u2c63":"P","\ua750":"P","\ua752":"P","\ua754":"P","\u24c6":"Q","\uff31":"Q","\ua756":"Q","\ua758":"Q","\u024a":"Q","\u24c7":"R","\uff32":"R","\u0154":"R","\u1e58":"R","\u0158":"R","\u0210":"R","\u0212":"R","\u1e5a":"R","\u1e5c":"R","\u0156":"R","\u1e5e":"R","\u024c":"R","\u2c64":"R","\ua75a":"R","\ua7a6":"R","\ua782":"R","\u24c8":"S","\uff33":"S","\u1e9e":"S","\u015a":"S","\u1e64":"S","\u015c":"S","\u1e60":"S","\u0160":"S","\u1e66":"S","\u1e62":"S","\u1e68":"S","\u0218":"S","\u015e":"S","\u2c7e":"S","\ua7a8":"S","\ua784":"S","\u24c9":"T","\uff34":"T","\u1e6a":"T","\u0164":"T","\u1e6c":"T","\u021a":"T","\u0162":"T","\u1e70":"T","\u1e6e":"T","\u0166":"T","\u01ac":"T","\u01ae":"T","\u023e":"T","\ua786":"T","\ua728":"TZ","\u24ca":"U","\uff35":"U","\xd9":"U","\xda":"U","\xdb":"U","\u0168":"U","\u1e78":"U","\u016a":"U","\u1e7a":"U","\u016c":"U","\xdc":"U","\u01db":"U","\u01d7":"U","\u01d5":"U","\u01d9":"U","\u1ee6":"U","\u016e":"U","\u0170":"U","\u01d3":"U","\u0214":"U","\u0216":"U","\u01af":"U","\u1eea":"U","\u1ee8":"U","\u1eee":"U","\u1eec":"U","\u1ef0":"U","\u1ee4":"U","\u1e72":"U","\u0172":"U","\u1e76":"U","\u1e74":"U","\u0244":"U","\u24cb":"V","\uff36":"V","\u1e7c":"V","\u1e7e":"V","\u01b2":"V","\ua75e":"V","\u0245":"V","\ua760":"VY","\u24cc":"W","\uff37":"W","\u1e80":"W","\u1e82":"W","\u0174":"W","\u1e86":"W","\u1e84":"W","\u1e88":"W","\u2c72":"W","\u24cd":"X","\uff38":"X","\u1e8a":"X","\u1e8c":"X","\u24ce":"Y","\uff39":"Y","\u1ef2":"Y","\xdd":"Y","\u0176":"Y","\u1ef8":"Y","\u0232":"Y","\u1e8e":"Y","\u0178":"Y","\u1ef6":"Y","\u1ef4":"Y","\u01b3":"Y","\u024e":"Y","\u1efe":"Y","\u24cf":"Z","\uff3a":"Z","\u0179":"Z","\u1e90":"Z","\u017b":"Z","\u017d":"Z","\u1e92":"Z","\u1e94":"Z","\u01b5":"Z","\u0224":"Z","\u2c7f":"Z","\u2c6b":"Z","\ua762":"Z","\u24d0":"a","\uff41":"a","\u1e9a":"a","\xe0":"a","\xe1":"a","\xe2":"a","\u1ea7":"a","\u1ea5":"a","\u1eab":"a","\u1ea9":"a","\xe3":"a","\u0101":"a","\u0103":"a","\u1eb1":"a","\u1eaf":"a","\u1eb5":"a","\u1eb3":"a","\u0227":"a","\u01e1":"a","\xe4":"a","\u01df":"a","\u1ea3":"a","\xe5":"a","\u01fb":"a","\u01ce":"a","\u0201":"a","\u0203":"a","\u1ea1":"a","\u1ead":"a","\u1eb7":"a","\u1e01":"a","\u0105":"a","\u2c65":"a","\u0250":"a","\ua733":"aa","\xe6":"ae","\u01fd":"ae","\u01e3":"ae","\ua735":"ao","\ua737":"au","\ua739":"av","\ua73b":"av","\ua73d":"ay","\u24d1":"b","\uff42":"b","\u1e03":"b","\u1e05":"b","\u1e07":"b","\u0180":"b","\u0183":"b","\u0253":"b","\u24d2":"c","\uff43":"c","\u0107":"c","\u0109":"c","\u010b":"c","\u010d":"c","\xe7":"c","\u1e09":"c","\u0188":"c","\u023c":"c","\ua73f":"c","\u2184":"c","\u24d3":"d","\uff44":"d","\u1e0b":"d","\u010f":"d","\u1e0d":"d","\u1e11":"d","\u1e13":"d","\u1e0f":"d","\u0111":"d","\u018c":"d","\u0256":"d","\u0257":"d","\ua77a":"d","\u01f3":"dz","\u01c6":"dz","\u24d4":"e","\uff45":"e","\xe8":"e","\xe9":"e","\xea":"e","\u1ec1":"e","\u1ebf":"e","\u1ec5":"e","\u1ec3":"e","\u1ebd":"e","\u0113":"e","\u1e15":"e","\u1e17":"e","\u0115":"e","\u0117":"e","\xeb":"e","\u1ebb":"e","\u011b":"e","\u0205":"e","\u0207":"e","\u1eb9":"e","\u1ec7":"e","\u0229":"e","\u1e1d":"e","\u0119":"e","\u1e19":"e","\u1e1b":"e","\u0247":"e","\u025b":"e","\u01dd":"e","\u24d5":"f","\uff46":"f","\u1e1f":"f","\u0192":"f","\ua77c":"f","\u24d6":"g","\uff47":"g","\u01f5":"g","\u011d":"g","\u1e21":"g","\u011f":"g","\u0121":"g","\u01e7":"g","\u0123":"g","\u01e5":"g","\u0260":"g","\ua7a1":"g","\u1d79":"g","\ua77f":"g","\u24d7":"h","\uff48":"h","\u0125":"h","\u1e23":"h","\u1e27":"h","\u021f":"h","\u1e25":"h","\u1e29":"h","\u1e2b":"h","\u1e96":"h","\u0127":"h","\u2c68":"h","\u2c76":"h","\u0265":"h","\u0195":"hv","\u24d8":"i","\uff49":"i","\xec":"i","\xed":"i","\xee":"i","\u0129":"i","\u012b":"i","\u012d":"i","\xef":"i","\u1e2f":"i","\u1ec9":"i","\u01d0":"i","\u0209":"i","\u020b":"i","\u1ecb":"i","\u012f":"i","\u1e2d":"i","\u0268":"i","\u0131":"i","\u24d9":"j","\uff4a":"j","\u0135":"j","\u01f0":"j","\u0249":"j","\u24da":"k","\uff4b":"k","\u1e31":"k","\u01e9":"k","\u1e33":"k","\u0137":"k","\u1e35":"k","\u0199":"k","\u2c6a":"k","\ua741":"k","\ua743":"k","\ua745":"k","\ua7a3":"k","\u24db":"l","\uff4c":"l","\u0140":"l","\u013a":"l","\u013e":"l","\u1e37":"l","\u1e39":"l","\u013c":"l","\u1e3d":"l","\u1e3b":"l","\u017f":"l","\u0142":"l","\u019a":"l","\u026b":"l","\u2c61":"l","\ua749":"l","\ua781":"l","\ua747":"l","\u01c9":"lj","\u24dc":"m","\uff4d":"m","\u1e3f":"m","\u1e41":"m","\u1e43":"m","\u0271":"m","\u026f":"m","\u24dd":"n","\uff4e":"n","\u01f9":"n","\u0144":"n","\xf1":"n","\u1e45":"n","\u0148":"n","\u1e47":"n","\u0146":"n","\u1e4b":"n","\u1e49":"n","\u019e":"n","\u0272":"n","\u0149":"n","\ua791":"n","\ua7a5":"n","\u01cc":"nj","\u24de":"o","\uff4f":"o","\xf2":"o","\xf3":"o","\xf4":"o","\u1ed3":"o","\u1ed1":"o","\u1ed7":"o","\u1ed5":"o","\xf5":"o","\u1e4d":"o","\u022d":"o","\u1e4f":"o","\u014d":"o","\u1e51":"o","\u1e53":"o","\u014f":"o","\u022f":"o","\u0231":"o","\xf6":"o","\u022b":"o","\u1ecf":"o","\u0151":"o","\u01d2":"o","\u020d":"o","\u020f":"o","\u01a1":"o","\u1edd":"o","\u1edb":"o","\u1ee1":"o","\u1edf":"o","\u1ee3":"o","\u1ecd":"o","\u1ed9":"o","\u01eb":"o","\u01ed":"o","\xf8":"o","\u01ff":"o","\u0254":"o","\ua74b":"o","\ua74d":"o","\u0275":"o","\u01a3":"oi","\u0223":"ou","\ua74f":"oo","\u24df":"p","\uff50":"p","\u1e55":"p","\u1e57":"p","\u01a5":"p","\u1d7d":"p","\ua751":"p","\ua753":"p","\ua755":"p","\u24e0":"q","\uff51":"q","\u024b":"q","\ua757":"q","\ua759":"q","\u24e1":"r","\uff52":"r","\u0155":"r","\u1e59":"r","\u0159":"r","\u0211":"r","\u0213":"r","\u1e5b":"r","\u1e5d":"r","\u0157":"r","\u1e5f":"r","\u024d":"r","\u027d":"r","\ua75b":"r","\ua7a7":"r","\ua783":"r","\u24e2":"s","\uff53":"s","\xdf":"s","\u015b":"s","\u1e65":"s","\u015d":"s","\u1e61":"s","\u0161":"s","\u1e67":"s","\u1e63":"s","\u1e69":"s","\u0219":"s","\u015f":"s","\u023f":"s","\ua7a9":"s","\ua785":"s","\u1e9b":"s","\u24e3":"t","\uff54":"t","\u1e6b":"t","\u1e97":"t","\u0165":"t","\u1e6d":"t","\u021b":"t","\u0163":"t","\u1e71":"t","\u1e6f":"t","\u0167":"t","\u01ad":"t","\u0288":"t","\u2c66":"t","\ua787":"t","\ua729":"tz","\u24e4":"u","\uff55":"u","\xf9":"u","\xfa":"u","\xfb":"u","\u0169":"u","\u1e79":"u","\u016b":"u","\u1e7b":"u","\u016d":"u","\xfc":"u","\u01dc":"u","\u01d8":"u","\u01d6":"u","\u01da":"u","\u1ee7":"u","\u016f":"u","\u0171":"u","\u01d4":"u","\u0215":"u","\u0217":"u","\u01b0":"u","\u1eeb":"u","\u1ee9":"u","\u1eef":"u","\u1eed":"u","\u1ef1":"u","\u1ee5":"u","\u1e73":"u","\u0173":"u","\u1e77":"u","\u1e75":"u","\u0289":"u","\u24e5":"v","\uff56":"v","\u1e7d":"v","\u1e7f":"v","\u028b":"v","\ua75f":"v","\u028c":"v","\ua761":"vy","\u24e6":"w","\uff57":"w","\u1e81":"w","\u1e83":"w","\u0175":"w","\u1e87":"w","\u1e85":"w","\u1e98":"w","\u1e89":"w","\u2c73":"w","\u24e7":"x","\uff58":"x","\u1e8b":"x","\u1e8d":"x","\u24e8":"y","\uff59":"y","\u1ef3":"y","\xfd":"y","\u0177":"y","\u1ef9":"y","\u0233":"y","\u1e8f":"y","\xff":"y","\u1ef7":"y","\u1e99":"y","\u1ef5":"y","\u01b4":"y","\u024f":"y","\u1eff":"y","\u24e9":"z","\uff5a":"z","\u017a":"z","\u1e91":"z","\u017c":"z","\u017e":"z","\u1e93":"z","\u1e95":"z","\u01b6":"z","\u0225":"z","\u0240":"z","\u2c6c":"z","\ua763":"z","\u0386":"\u0391","\u0388":"\u0395","\u0389":"\u0397","\u038a":"\u0399","\u03aa":"\u0399","\u038c":"\u039f","\u038e":"\u03a5","\u03ab":"\u03a5","\u038f":"\u03a9","\u03ac":"\u03b1","\u03ad":"\u03b5","\u03ae":"\u03b7","\u03af":"\u03b9","\u03ca":"\u03b9","\u0390":"\u03b9","\u03cc":"\u03bf","\u03cd":"\u03c5","\u03cb":"\u03c5","\u03b0":"\u03c5","\u03c9":"\u03c9","\u03c2":"\u03c3"};i=a(document),f=function(){var a=1;return function(){return a++}}(),c=O(Object,{bind:function(a){var b=this;return function(){a.apply(b,arguments)}},init:function(c){var d,e,g=".select2-results";this.opts=c=this.prepareOpts(c),this.id=c.id,c.element.data("select2")!==b&&null!==c.element.data("select2")&&c.element.data("select2").destroy(),this.container=this.createContainer(),this.liveRegion=a(".select2-hidden-accessible"),0==this.liveRegion.length&&(this.liveRegion=a("",{role:"status","aria-live":"polite"}).addClass("select2-hidden-accessible").appendTo(document.body)),this.containerId="s2id_"+(c.element.attr("id")||"autogen"+f()),this.containerEventName=this.containerId.replace(/([.])/g,"_").replace(/([;&,\-\.\+\*\~':"\!\^#$%@\[\]\(\)=>\|])/g,"\\$1"),this.container.attr("id",this.containerId),this.container.attr("title",c.element.attr("title")),this.body=a(document.body),D(this.container,this.opts.element,this.opts.adaptContainerCssClass),this.container.attr("style",c.element.attr("style")),this.container.css(K(c.containerCss,this.opts.element)),this.container.addClass(K(c.containerCssClass,this.opts.element)),this.elementTabIndex=this.opts.element.attr("tabindex"),this.opts.element.data("select2",this).attr("tabindex","-1").before(this.container).on("click.select2",A),this.container.data("select2",this),this.dropdown=this.container.find(".select2-drop"),D(this.dropdown,this.opts.element,this.opts.adaptDropdownCssClass),this.dropdown.addClass(K(c.dropdownCssClass,this.opts.element)),this.dropdown.data("select2",this),this.dropdown.on("click",A),this.results=d=this.container.find(g),this.search=e=this.container.find("input.select2-input"),this.queryCount=0,this.resultsPage=0,this.context=null,this.initContainer(),this.container.on("click",A),v(this.results),this.dropdown.on("mousemove-filtered",g,this.bind(this.highlightUnderEvent)),this.dropdown.on("touchstart touchmove touchend",g,this.bind(function(a){this._touchEvent=!0,this.highlightUnderEvent(a)})),this.dropdown.on("touchmove",g,this.bind(this.touchMoved)),this.dropdown.on("touchstart touchend",g,this.bind(this.clearTouchMoved)),this.dropdown.on("click",this.bind(function(){this._touchEvent&&(this._touchEvent=!1,this.selectHighlighted())})),x(80,this.results),this.dropdown.on("scroll-debounced",g,this.bind(this.loadMoreIfNeeded)),a(this.container).on("change",".select2-input",function(a){a.stopPropagation()}),a(this.dropdown).on("change",".select2-input",function(a){a.stopPropagation()}),a.fn.mousewheel&&d.mousewheel(function(a,b,c,e){var f=d.scrollTop();e>0&&0>=f-e?(d.scrollTop(0),A(a)):0>e&&d.get(0).scrollHeight-d.scrollTop()+e<=d.height()&&(d.scrollTop(d.get(0).scrollHeight-d.height()),A(a))}),u(e),e.on("keyup-change input paste",this.bind(this.updateResults)),e.on("focus",function(){e.addClass("select2-focused")}),e.on("blur",function(){e.removeClass("select2-focused")}),this.dropdown.on("mouseup",g,this.bind(function(b){a(b.target).closest(".select2-result-selectable").length>0&&(this.highlightUnderEvent(b),this.selectHighlighted(b))})),this.dropdown.on("click mouseup mousedown touchstart touchend focusin",function(a){a.stopPropagation()}),this.nextSearchTerm=b,a.isFunction(this.opts.initSelection)&&(this.initSelection(),this.monitorSource()),null!==c.maximumInputLength&&this.search.attr("maxlength",c.maximumInputLength);var h=c.element.prop("disabled");h===b&&(h=!1),this.enable(!h);var i=c.element.prop("readonly");i===b&&(i=!1),this.readonly(i),j=j||q(),this.autofocus=c.element.prop("autofocus"),c.element.prop("autofocus",!1),this.autofocus&&this.focus(),this.search.attr("placeholder",c.searchInputPlaceholder)},destroy:function(){var a=this.opts.element,c=a.data("select2"),d=this;this.close(),a.length&&a[0].detachEvent&&d._sync&&a.each(function(){d._sync&&this.detachEvent("onpropertychange",d._sync)}),this.propertyObserver&&(this.propertyObserver.disconnect(),this.propertyObserver=null),this._sync=null,c!==b&&(c.container.remove(),c.liveRegion.remove(),c.dropdown.remove(),a.show().removeData("select2").off(".select2").prop("autofocus",this.autofocus||!1),this.elementTabIndex?a.attr({tabindex:this.elementTabIndex}):a.removeAttr("tabindex"),a.show()),N.call(this,"container","liveRegion","dropdown","results","search")},optionToData:function(a){return a.is("option")?{id:a.prop("value"),text:a.text(),element:a.get(),css:a.attr("class"),disabled:a.prop("disabled"),locked:r(a.attr("locked"),"locked")||r(a.data("locked"),!0)}:a.is("optgroup")?{text:a.attr("label"),children:[],element:a.get(),css:a.attr("class")}:void 0},prepareOpts:function(c){var d,e,g,h,i=this;if(d=c.element,"select"===d.get(0).tagName.toLowerCase()&&(this.select=e=c.element),e&&a.each(["id","multiple","ajax","query","createSearchChoice","initSelection","data","tags"],function(){if(this in c)throw new Error("Option '"+this+"' is not allowed for Select2 when attached to a ","
"," ","
    ","
","
"].join(""));return b},enableInterface:function(){this.parent.enableInterface.apply(this,arguments)&&this.focusser.prop("disabled",!this.isInterfaceEnabled())},opening:function(){var c,d,e;this.opts.minimumResultsForSearch>=0&&this.showSearch(!0),this.parent.opening.apply(this,arguments),this.showSearchInput!==!1&&this.search.val(this.focusser.val()),this.opts.shouldFocusInput(this)&&(this.search.focus(),c=this.search.get(0),c.createTextRange?(d=c.createTextRange(),d.collapse(!1),d.select()):c.setSelectionRange&&(e=this.search.val().length,c.setSelectionRange(e,e))),""===this.search.val()&&this.nextSearchTerm!=b&&(this.search.val(this.nextSearchTerm),this.search.select()),this.focusser.prop("disabled",!0).val(""),this.updateResults(!0),this.opts.element.trigger(a.Event("select2-open"))},close:function(){this.opened()&&(this.parent.close.apply(this,arguments),this.focusser.prop("disabled",!1),this.opts.shouldFocusInput(this)&&this.focusser.focus())},focus:function(){this.opened()?this.close():(this.focusser.prop("disabled",!1),this.opts.shouldFocusInput(this)&&this.focusser.focus())},isFocused:function(){return this.container.hasClass("select2-container-active")},cancel:function(){this.parent.cancel.apply(this,arguments),this.focusser.prop("disabled",!1),this.opts.shouldFocusInput(this)&&this.focusser.focus()},destroy:function(){a("label[for='"+this.focusser.attr("id")+"']").attr("for",this.opts.element.attr("id")),this.parent.destroy.apply(this,arguments),N.call(this,"selection","focusser")},initContainer:function(){var b,g,c=this.container,d=this.dropdown,e=f();this.opts.minimumResultsForSearch<0?this.showSearch(!1):this.showSearch(!0),this.selection=b=c.find(".select2-choice"),this.focusser=c.find(".select2-focusser"),b.find(".select2-chosen").attr("id","select2-chosen-"+e),this.focusser.attr("aria-labelledby","select2-chosen-"+e),this.results.attr("id","select2-results-"+e),this.search.attr("aria-owns","select2-results-"+e),this.focusser.attr("id","s2id_autogen"+e),g=a("label[for='"+this.opts.element.attr("id")+"']"),this.opts.element.focus(this.bind(function(){this.focus()})),this.focusser.prev().text(g.text()).attr("for",this.focusser.attr("id"));var h=this.opts.element.attr("title");this.opts.element.attr("title",h||g.text()),this.focusser.attr("tabindex",this.elementTabIndex),this.search.attr("id",this.focusser.attr("id")+"_search"),this.search.prev().text(a("label[for='"+this.focusser.attr("id")+"']").text()).attr("for",this.search.attr("id")),this.search.on("keydown",this.bind(function(a){if(this.isInterfaceEnabled()&&229!=a.keyCode){if(a.which===k.PAGE_UP||a.which===k.PAGE_DOWN)return A(a),void 0;switch(a.which){case k.UP:case k.DOWN:return this.moveHighlight(a.which===k.UP?-1:1),A(a),void 0;case k.ENTER:return this.selectHighlighted(),A(a),void 0;case k.TAB:return this.selectHighlighted({noFocus:!0}),void 0;case k.ESC:return this.cancel(a),A(a),void 0}}})),this.search.on("blur",this.bind(function(){document.activeElement===this.body.get(0)&&window.setTimeout(this.bind(function(){this.opened()&&this.search.focus()}),0)})),this.focusser.on("keydown",this.bind(function(a){if(this.isInterfaceEnabled()&&a.which!==k.TAB&&!k.isControl(a)&&!k.isFunctionKey(a)&&a.which!==k.ESC){if(this.opts.openOnEnter===!1&&a.which===k.ENTER)return A(a),void 0;if(a.which==k.DOWN||a.which==k.UP||a.which==k.ENTER&&this.opts.openOnEnter){if(a.altKey||a.ctrlKey||a.shiftKey||a.metaKey)return;return this.open(),A(a),void 0}return a.which==k.DELETE||a.which==k.BACKSPACE?(this.opts.allowClear&&this.clear(),A(a),void 0):void 0}})),u(this.focusser),this.focusser.on("keyup-change input",this.bind(function(a){if(this.opts.minimumResultsForSearch>=0){if(a.stopPropagation(),this.opened())return;this.open()}})),b.on("mousedown touchstart","abbr",this.bind(function(a){this.isInterfaceEnabled()&&(this.clear(),B(a),this.close(),this.selection&&this.selection.focus())})),b.on("mousedown touchstart",this.bind(function(c){n(b),this.container.hasClass("select2-container-active")||this.opts.element.trigger(a.Event("select2-focus")),this.opened()?this.close():this.isInterfaceEnabled()&&this.open(),A(c)})),d.on("mousedown touchstart",this.bind(function(){this.opts.shouldFocusInput(this)&&this.search.focus()})),b.on("focus",this.bind(function(a){A(a)})),this.focusser.on("focus",this.bind(function(){this.container.hasClass("select2-container-active")||this.opts.element.trigger(a.Event("select2-focus")),this.container.addClass("select2-container-active")})).on("blur",this.bind(function(){this.opened()||(this.container.removeClass("select2-container-active"),this.opts.element.trigger(a.Event("select2-blur")))})),this.search.on("focus",this.bind(function(){this.container.hasClass("select2-container-active")||this.opts.element.trigger(a.Event("select2-focus")),this.container.addClass("select2-container-active")})),this.initContainerWidth(),this.opts.element.hide(),this.setPlaceholder()},clear:function(b){var c=this.selection.data("select2-data");if(c){var d=a.Event("select2-clearing");if(this.opts.element.trigger(d),d.isDefaultPrevented())return;var e=this.getPlaceholderOption();this.opts.element.val(e?e.val():""),this.selection.find(".select2-chosen").empty(),this.selection.removeData("select2-data"),this.setPlaceholder(),b!==!1&&(this.opts.element.trigger({type:"select2-removed",val:this.id(c),choice:c}),this.triggerChange({removed:c}))}},initSelection:function(){if(this.isPlaceholderOptionSelected())this.updateSelection(null),this.close(),this.setPlaceholder();else{var c=this;this.opts.initSelection.call(null,this.opts.element,function(a){a!==b&&null!==a&&(c.updateSelection(a),c.close(),c.setPlaceholder(),c.nextSearchTerm=c.opts.nextSearchTerm(a,c.search.val()))})}},isPlaceholderOptionSelected:function(){var a;return this.getPlaceholder()===b?!1:(a=this.getPlaceholderOption())!==b&&a.prop("selected")||""===this.opts.element.val()||this.opts.element.val()===b||null===this.opts.element.val()},prepareOpts:function(){var b=this.parent.prepareOpts.apply(this,arguments),c=this;return"select"===b.element.get(0).tagName.toLowerCase()?b.initSelection=function(a,b){var d=a.find("option").filter(function(){return this.selected&&!this.disabled});b(c.optionToData(d))}:"data"in b&&(b.initSelection=b.initSelection||function(c,d){var e=c.val(),f=null;b.query({matcher:function(a,c,d){var g=r(e,b.id(d));return g&&(f=d),g},callback:a.isFunction(d)?function(){d(f)}:a.noop})}),b},getPlaceholder:function(){return this.select&&this.getPlaceholderOption()===b?b:this.parent.getPlaceholder.apply(this,arguments)},setPlaceholder:function(){var a=this.getPlaceholder();if(this.isPlaceholderOptionSelected()&&a!==b){if(this.select&&this.getPlaceholderOption()===b)return;this.selection.find(".select2-chosen").html(this.opts.escapeMarkup(a)),this.selection.addClass("select2-default"),this.container.removeClass("select2-allowclear")}},postprocessResults:function(a,b,c){var d=0,e=this;if(this.findHighlightableChoices().each2(function(a,b){return r(e.id(b.data("select2-data")),e.opts.element.val())?(d=a,!1):void 0}),c!==!1&&(b===!0&&d>=0?this.highlight(d):this.highlight(0)),b===!0){var g=this.opts.minimumResultsForSearch;g>=0&&this.showSearch(L(a.results)>=g)}},showSearch:function(b){this.showSearchInput!==b&&(this.showSearchInput=b,this.dropdown.find(".select2-search").toggleClass("select2-search-hidden",!b),this.dropdown.find(".select2-search").toggleClass("select2-offscreen",!b),a(this.dropdown,this.container).toggleClass("select2-with-searchbox",b))},onSelect:function(a,b){if(this.triggerSelect(a)){var c=this.opts.element.val(),d=this.data();this.opts.element.val(this.id(a)),this.updateSelection(a),this.opts.element.trigger({type:"select2-selected",val:this.id(a),choice:a}),this.nextSearchTerm=this.opts.nextSearchTerm(a,this.search.val()),this.close(),b&&b.noFocus||!this.opts.shouldFocusInput(this)||this.focusser.focus(),r(c,this.id(a))||this.triggerChange({added:a,removed:d})}},updateSelection:function(a){var d,e,c=this.selection.find(".select2-chosen");this.selection.data("select2-data",a),c.empty(),null!==a&&(d=this.opts.formatSelection(a,c,this.opts.escapeMarkup)),d!==b&&c.append(d),e=this.opts.formatSelectionCssClass(a,c),e!==b&&c.addClass(e),this.selection.removeClass("select2-default"),this.opts.allowClear&&this.getPlaceholder()!==b&&this.container.addClass("select2-allowclear")},val:function(){var a,c=!1,d=null,e=this,f=this.data();if(0===arguments.length)return this.opts.element.val();if(a=arguments[0],arguments.length>1&&(c=arguments[1]),this.select)this.select.val(a).find("option").filter(function(){return this.selected}).each2(function(a,b){return d=e.optionToData(b),!1}),this.updateSelection(d),this.setPlaceholder(),c&&this.triggerChange({added:d,removed:f});else{if(!a&&0!==a)return this.clear(c),void 0;if(this.opts.initSelection===b)throw new Error("cannot call val() if initSelection() is not defined");this.opts.element.val(a),this.opts.initSelection(this.opts.element,function(a){e.opts.element.val(a?e.id(a):""),e.updateSelection(a),e.setPlaceholder(),c&&e.triggerChange({added:a,removed:f})})}},clearSearch:function(){this.search.val(""),this.focusser.val("")},data:function(a){var c,d=!1;return 0===arguments.length?(c=this.selection.data("select2-data"),c==b&&(c=null),c):(arguments.length>1&&(d=arguments[1]),a?(c=this.data(),this.opts.element.val(a?this.id(a):""),this.updateSelection(a),d&&this.triggerChange({added:a,removed:c})):this.clear(d),void 0)}}),e=O(c,{createContainer:function(){var b=a(document.createElement("div")).attr({"class":"select2-container select2-container-multi"}).html(["
    ","
  • "," "," ","
  • ","
","
","
    ","
","
"].join(""));return b},prepareOpts:function(){var b=this.parent.prepareOpts.apply(this,arguments),c=this;return"select"===b.element.get(0).tagName.toLowerCase()?b.initSelection=function(a,b){var d=[];a.find("option").filter(function(){return this.selected&&!this.disabled}).each2(function(a,b){d.push(c.optionToData(b))}),b(d)}:"data"in b&&(b.initSelection=b.initSelection||function(c,d){var e=s(c.val(),b.separator,b.transformVal),f=[];b.query({matcher:function(c,d,g){var h=a.grep(e,function(a){return r(a,b.id(g))}).length;return h&&f.push(g),h},callback:a.isFunction(d)?function(){for(var a=[],c=0;c0||(this.selectChoice(null),this.clearPlaceholder(),this.container.hasClass("select2-container-active")||this.opts.element.trigger(a.Event("select2-focus")),this.open(),this.focusSearch(),b.preventDefault()))})),this.container.on("focus",b,this.bind(function(){this.isInterfaceEnabled()&&(this.container.hasClass("select2-container-active")||this.opts.element.trigger(a.Event("select2-focus")),this.container.addClass("select2-container-active"),this.dropdown.addClass("select2-drop-active"),this.clearPlaceholder())})),this.initContainerWidth(),this.opts.element.hide(),this.clearSearch()},enableInterface:function(){this.parent.enableInterface.apply(this,arguments)&&this.search.prop("disabled",!this.isInterfaceEnabled())},initSelection:function(){if(""===this.opts.element.val()&&""===this.opts.element.text()&&(this.updateSelection([]),this.close(),this.clearSearch()),this.select||""!==this.opts.element.val()){var c=this;this.opts.initSelection.call(null,this.opts.element,function(a){a!==b&&null!==a&&(c.updateSelection(a),c.close(),c.clearSearch())})}},clearSearch:function(){var a=this.getPlaceholder(),c=this.getMaxSearchWidth();a!==b&&0===this.getVal().length&&this.search.hasClass("select2-focused")===!1?(this.search.val(a).addClass("select2-default"),this.search.width(c>0?c:this.container.css("width"))):this.search.val("").width(10)},clearPlaceholder:function(){this.search.hasClass("select2-default")&&this.search.val("").removeClass("select2-default")},opening:function(){this.clearPlaceholder(),this.resizeSearch(),this.parent.opening.apply(this,arguments),this.focusSearch(),""===this.search.val()&&this.nextSearchTerm!=b&&(this.search.val(this.nextSearchTerm),this.search.select()),this.updateResults(!0),this.opts.shouldFocusInput(this)&&this.search.focus(),this.opts.element.trigger(a.Event("select2-open"))},close:function(){this.opened()&&this.parent.close.apply(this,arguments)},focus:function(){this.close(),this.search.focus()},isFocused:function(){return this.search.hasClass("select2-focused")},updateSelection:function(b){var c=[],d=[],e=this;a(b).each(function(){p(e.id(this),c)<0&&(c.push(e.id(this)),d.push(this))}),b=d,this.selection.find(".select2-search-choice").remove(),a(b).each(function(){e.addSelectedChoice(this)}),e.postprocessResults()},tokenize:function(){var a=this.search.val();a=this.opts.tokenizer.call(this,a,this.data(),this.bind(this.onSelect),this.opts),null!=a&&a!=b&&(this.search.val(a),a.length>0&&this.open())},onSelect:function(a,c){this.triggerSelect(a)&&""!==a.text&&(this.addSelectedChoice(a),this.opts.element.trigger({type:"selected",val:this.id(a),choice:a}),this.nextSearchTerm=this.opts.nextSearchTerm(a,this.search.val()),this.clearSearch(),this.updateResults(),(this.select||!this.opts.closeOnSelect)&&this.postprocessResults(a,!1,this.opts.closeOnSelect===!0),this.opts.closeOnSelect?(this.close(),this.search.width(10)):this.countSelectableResults()>0?(this.search.width(10),this.resizeSearch(),this.getMaximumSelectionSize()>0&&this.val().length>=this.getMaximumSelectionSize()?this.updateResults(!0):this.nextSearchTerm!=b&&(this.search.val(this.nextSearchTerm),this.updateResults(),this.search.select()),this.positionDropdown()):(this.close(),this.search.width(10)),this.triggerChange({added:a}),c&&c.noFocus||this.focusSearch())},cancel:function(){this.close(),this.focusSearch()},addSelectedChoice:function(c){var j,k,d=!c.locked,e=a("
  • "),f=a("
  • "),g=d?e:f,h=this.id(c),i=this.getVal();j=this.opts.formatSelection(c,g.find("div"),this.opts.escapeMarkup),j!=b&&g.find("div").replaceWith(a("
    ").html(j)),k=this.opts.formatSelectionCssClass(c,g.find("div")),k!=b&&g.addClass(k),d&&g.find(".select2-search-choice-close").on("mousedown",A).on("click dblclick",this.bind(function(b){this.isInterfaceEnabled()&&(this.unselect(a(b.target)),this.selection.find(".select2-search-choice-focus").removeClass("select2-search-choice-focus"),A(b),this.close(),this.focusSearch())})).on("focus",this.bind(function(){this.isInterfaceEnabled()&&(this.container.addClass("select2-container-active"),this.dropdown.addClass("select2-drop-active"))})),g.data("select2-data",c),g.insertBefore(this.searchContainer),i.push(h),this.setVal(i)},unselect:function(b){var d,e,c=this.getVal();if(b=b.closest(".select2-search-choice"),0===b.length)throw"Invalid argument: "+b+". Must be .select2-search-choice";if(d=b.data("select2-data")){var f=a.Event("select2-removing");if(f.val=this.id(d),f.choice=d,this.opts.element.trigger(f),f.isDefaultPrevented())return!1;for(;(e=p(this.id(d),c))>=0;)c.splice(e,1),this.setVal(c),this.select&&this.postprocessResults();return b.remove(),this.opts.element.trigger({type:"select2-removed",val:this.id(d),choice:d}),this.triggerChange({removed:d}),!0}},postprocessResults:function(a,b,c){var d=this.getVal(),e=this.results.find(".select2-result"),f=this.results.find(".select2-result-with-children"),g=this;e.each2(function(a,b){var c=g.id(b.data("select2-data"));p(c,d)>=0&&(b.addClass("select2-selected"),b.find(".select2-result-selectable").addClass("select2-selected"))}),f.each2(function(a,b){b.is(".select2-result-selectable")||0!==b.find(".select2-result-selectable:not(.select2-selected)").length||b.addClass("select2-selected")}),-1==this.highlight()&&c!==!1&&this.opts.closeOnSelect===!0&&g.highlight(0),!this.opts.createSearchChoice&&!e.filter(".select2-result:not(.select2-selected)").length>0&&(!a||a&&!a.more&&0===this.results.find(".select2-no-results").length)&&J(g.opts.formatNoMatches,"formatNoMatches")&&this.results.append("
  • "+K(g.opts.formatNoMatches,g.opts.element,g.search.val())+"
  • ")},getMaxSearchWidth:function(){return this.selection.width()-t(this.search)},resizeSearch:function(){var a,b,c,d,e,f=t(this.search);a=C(this.search)+10,b=this.search.offset().left,c=this.selection.width(),d=this.selection.offset().left,e=c-(b-d)-f,a>e&&(e=c-f),40>e&&(e=c-f),0>=e&&(e=a),this.search.width(Math.floor(e))},getVal:function(){var a;return this.select?(a=this.select.val(),null===a?[]:a):(a=this.opts.element.val(),s(a,this.opts.separator,this.opts.transformVal))},setVal:function(b){var c;this.select?this.select.val(b):(c=[],a(b).each(function(){p(this,c)<0&&c.push(this)}),this.opts.element.val(0===c.length?"":c.join(this.opts.separator)))},buildChangeDetails:function(a,b){for(var b=b.slice(0),a=a.slice(0),c=0;c0&&c--,a.splice(d,1),d--);return{added:b,removed:a}},val:function(c,d){var e,f=this;if(0===arguments.length)return this.getVal();if(e=this.data(),e.length||(e=[]),!c&&0!==c)return this.opts.element.val(""),this.updateSelection([]),this.clearSearch(),d&&this.triggerChange({added:this.data(),removed:e}),void 0;if(this.setVal(c),this.select)this.opts.initSelection(this.select,this.bind(this.updateSelection)),d&&this.triggerChange(this.buildChangeDetails(e,this.data()));else{if(this.opts.initSelection===b)throw new Error("val() cannot be called if initSelection() is not defined");this.opts.initSelection(this.opts.element,function(b){var c=a.map(b,f.id);f.setVal(c),f.updateSelection(b),f.clearSearch(),d&&f.triggerChange(f.buildChangeDetails(e,f.data()))})}this.clearSearch()},onSortStart:function(){if(this.select)throw new Error("Sorting of elements is not supported when attached to instead.");this.search.width(0),this.searchContainer.hide()},onSortEnd:function(){var b=[],c=this;this.searchContainer.show(),this.searchContainer.appendTo(this.searchContainer.parent()),this.resizeSearch(),this.selection.find(".select2-search-choice").each(function(){b.push(c.opts.id(a(this).data("select2-data")))}),this.setVal(b),this.triggerChange()},data:function(b,c){var e,f,d=this;return 0===arguments.length?this.selection.children(".select2-search-choice").map(function(){return a(this).data("select2-data")}).get():(f=this.data(),b||(b=[]),e=a.map(b,function(a){return d.opts.id(a)}),this.setVal(e),this.updateSelection(b),this.clearSearch(),c&&this.triggerChange(this.buildChangeDetails(f,this.data())),void 0)}}),a.fn.select2=function(){var d,e,f,g,h,c=Array.prototype.slice.call(arguments,0),i=["val","destroy","opened","open","close","focus","isFocused","container","dropdown","onSortStart","onSortEnd","enable","disable","readonly","positionDropdown","data","search"],j=["opened","isFocused","container","dropdown"],k=["val","data"],l={search:"externalSearch"};return this.each(function(){if(0===c.length||"object"==typeof c[0])d=0===c.length?{}:a.extend({},c[0]),d.element=a(this),"select"===d.element.get(0).tagName.toLowerCase()?h=d.element.prop("multiple"):(h=d.multiple||!1,"tags"in d&&(d.multiple=h=!0)),e=h?new window.Select2["class"].multi:new window.Select2["class"].single,e.init(d);else{if("string"!=typeof c[0])throw"Invalid arguments to select2 plugin: "+c;if(p(c[0],i)<0)throw"Unknown method: "+c[0];if(g=b,e=a(this).data("select2"),e===b)return;if(f=c[0],"container"===f?g=e.container:"dropdown"===f?g=e.dropdown:(l[f]&&(f=l[f]),g=e[f].apply(e,c.slice(1))),p(c[0],j)>=0||p(c[0],k)>=0&&1==c.length)return!1}}),g===b?this:g},a.fn.select2.defaults={width:"copy",loadMorePadding:0,closeOnSelect:!0,openOnEnter:!0,containerCss:{},dropdownCss:{},containerCssClass:"",dropdownCssClass:"",formatResult:function(a,b,c,d){var e=[];return E(this.text(a),c.term,e,d),e.join("")},transformVal:function(b){return a.trim(b)},formatSelection:function(a,c,d){return a?d(this.text(a)):b},sortResults:function(a){return a},formatResultCssClass:function(a){return a.css},formatSelectionCssClass:function(){return b},minimumResultsForSearch:0,minimumInputLength:0,maximumInputLength:null,maximumSelectionSize:0,id:function(a){return a==b?null:a.id},text:function(b){return b&&this.data&&this.data.text?a.isFunction(this.data.text)?this.data.text(b):b[this.data.text]:b.text +},matcher:function(a,b){return o(""+b).toUpperCase().indexOf(o(""+a).toUpperCase())>=0},separator:",",tokenSeparators:[],tokenizer:M,escapeMarkup:F,blurOnChange:!1,selectOnBlur:!1,adaptContainerCssClass:function(a){return a},adaptDropdownCssClass:function(){return null},nextSearchTerm:function(){return b},searchInputPlaceholder:"",createSearchChoicePosition:"top",shouldFocusInput:function(a){var b="ontouchstart"in window||navigator.msMaxTouchPoints>0;return b?a.opts.minimumResultsForSearch<0?!1:!0:!0}},a.fn.select2.locales=[],a.fn.select2.locales.en={formatMatches:function(a){return 1===a?"One result is available, press enter to select it.":a+" results are available, use up and down arrow keys to navigate."},formatNoMatches:function(){return"No matches found"},formatAjaxError:function(){return"Loading failed"},formatInputTooShort:function(a,b){var c=b-a.length;return"Please enter "+c+" or more character"+(1==c?"":"s")},formatInputTooLong:function(a,b){var c=a.length-b;return"Please delete "+c+" character"+(1==c?"":"s")},formatSelectionTooBig:function(a){return"You can only select "+a+" item"+(1==a?"":"s")},formatLoadMore:function(){return"Loading more results\u2026"},formatSearching:function(){return"Searching\u2026"}},a.extend(a.fn.select2.defaults,a.fn.select2.locales.en),a.fn.select2.ajaxDefaults={transport:a.ajax,params:{type:"GET",cache:!1,dataType:"json"}},window.Select2={query:{ajax:G,local:H,tags:I},util:{debounce:w,markMatch:E,escapeMarkup:F,stripDiacritics:o},"class":{"abstract":c,single:d,multi:e}}}}(jQuery); diff --git a/static/tagulous/lib/select2-3/select2.png b/static/tagulous/lib/select2-3/select2.png new file mode 100644 index 0000000000000000000000000000000000000000..1d804ffb99699b9e030f1010314de0970b5a000d GIT binary patch literal 613 zcmV-r0-F7aP)#WY!I$JQV$)A5aAS1BM||2XVJl=+L1^1S1H% zM-&lx?NZpUrHhn>fk<>POqf2sh40}xxGZfc+t+#Eb(qHy9_3*1(U%t9t)QDnI#YAL(|ACV(>)>6WD-t!8tutHkdb^#3`HzoJG3A2@T`% zA|K@o*b!`R#(7)PWrMFn2))Ca3MR4(zaT`Zr61*kZK5NPnZwQszxh$fyv3?&4c>$q z2m=+yc0dRXRAsPDxF6sD;@rK4JGdR_``1S~o6Xi@2&aR6hcSrEp9HVRzEqVDqBn<1%hR=D4e1f^ra^A|34Cjc=Gny{F(o#MrvPYgZuTJOz(n)-F<| zj()qR;C={)N<0RRvDZ^@6ND+W*}gh-Lip(MDt!(zMSO)!j2j+*hxgzC-e3$@(O2p* zu;+gddm(cZwXTCLx*Ky4THOa*^b^F`woveIeCK^0aR|TJ00000NkvXXu0mjfA#WC6 literal 0 HcmV?d00001 diff --git a/static/tagulous/lib/select2-3/select2x2.png b/static/tagulous/lib/select2-3/select2x2.png new file mode 100644 index 0000000000000000000000000000000000000000..4bdd5c961d452c49dfa0789c2c7ffb82c238fc24 GIT binary patch literal 845 zcmV-T1G4;yP)upQ6WKflyv?C|ADVW!U!t`EpA+x zB)5#EjWk-_X77YJZtQo`E0SF)^1bZr%)B7Cd`*OK*r z5WG-7e-R9G9^69ksDt29&oyHqxPSt|-S>xi3%PTd+GjY+BGF|nWC(7D-sd(kxqd9~ zS@2YF5vB+>dP8+$l^{oO3-lEWiGA*QIU)Wds#9M6RZ9N zcQ4y4)xqQOxD=vwu%7cz1nY#$lT&y8HCmkWgpwQP#3dhnYj9|2aS_R}IUF_^6s#$= zTm%~>A#oM?KIg$kh=<`gJkeoHa2LrulVy$Yx+N_0R3$4I!R*0677f(FKqm`2_o4~W z0h}fQZ`lC^1A+m;fM7uI(R1`S0KtG@KrkQ}5DW+&@cTnDVIow56KciMk7a899t0bC zC1KI{TsMe5NAR%GD_5`B-@ad4k~K3SO%H z_M31|`HV?E6)u$E3c&*<*n20+V@mRCop>R5;DWuZCmjSo7p@R&OYl^@G
    ');return e.attr("dir",this.options.get("dir")),this.$container=e,this.$container[0].classList.add("select2-container--"+this.options.get("theme")),o.StoreData(e[0],"element",this.$element),e},r}),u.define("select2/dropdown/attachContainer",[],function(){function e(e,t,n){e.call(this,t,n)}return e.prototype.position=function(e,t,n){n.find(".dropdown-wrapper").append(t),t[0].classList.add("select2-dropdown--below"),n[0].classList.add("select2-container--below")},e}),u.define("select2/dropdown/stopPropagation",[],function(){function e(){}return e.prototype.bind=function(e,t,n){e.call(this,t,n);this.$dropdown.on(["blur","change","click","dblclick","focus","focusin","focusout","input","keydown","keyup","keypress","mousedown","mouseenter","mouseleave","mousemove","mouseover","mouseup","search","touchend","touchstart"].join(" "),function(e){e.stopPropagation()})},e}),u.define("select2/selection/stopPropagation",[],function(){function e(){}return e.prototype.bind=function(e,t,n){e.call(this,t,n);this.$selection.on(["blur","change","click","dblclick","focus","focusin","focusout","input","keydown","keyup","keypress","mousedown","mouseenter","mouseleave","mousemove","mouseover","mouseup","search","touchend","touchstart"].join(" "),function(e){e.stopPropagation()})},e}),a=function(u){var d,p,e=["wheel","mousewheel","DOMMouseScroll","MozMousePixelScroll"],t="onwheel"in document||9<=document.documentMode?["wheel"]:["mousewheel","DomMouseScroll","MozMousePixelScroll"],h=Array.prototype.slice;if(u.event.fixHooks)for(var n=e.length;n;)u.event.fixHooks[e[--n]]=u.event.mouseHooks;var f=u.event.special.mousewheel={version:"3.1.12",setup:function(){if(this.addEventListener)for(var e=t.length;e;)this.addEventListener(t[--e],s,!1);else this.onmousewheel=s;u.data(this,"mousewheel-line-height",f.getLineHeight(this)),u.data(this,"mousewheel-page-height",f.getPageHeight(this))},teardown:function(){if(this.removeEventListener)for(var e=t.length;e;)this.removeEventListener(t[--e],s,!1);else this.onmousewheel=null;u.removeData(this,"mousewheel-line-height"),u.removeData(this,"mousewheel-page-height")},getLineHeight:function(e){var t=u(e),e=t["offsetParent"in u.fn?"offsetParent":"parent"]();return e.length||(e=u("body")),parseInt(e.css("fontSize"),10)||parseInt(t.css("fontSize"),10)||16},getPageHeight:function(e){return u(e).height()},settings:{adjustOldDeltas:!0,normalizeOffset:!0}};function s(e){var t,n=e||window.event,s=h.call(arguments,1),i=0,o=0,r=0,a=0,l=0,c=0;if(e=u.event.fix(n),e.type="mousewheel","detail"in n&&(r=-1*n.detail),"wheelDelta"in n&&(r=n.wheelDelta),"wheelDeltaY"in n&&(r=n.wheelDeltaY),"wheelDeltaX"in n&&(o=-1*n.wheelDeltaX),"axis"in n&&n.axis===n.HORIZONTAL_AXIS&&(o=-1*r,r=0),i=0===r?o:r,"deltaY"in n&&(i=r=-1*n.deltaY),"deltaX"in n&&(o=n.deltaX,0===r&&(i=-1*o)),0!==r||0!==o)return 1===n.deltaMode?(i*=t=u.data(this,"mousewheel-line-height"),r*=t,o*=t):2===n.deltaMode&&(i*=t=u.data(this,"mousewheel-page-height"),r*=t,o*=t),a=Math.max(Math.abs(r),Math.abs(o)),(!p||a 0) { + name.splice(i - 1, 2); + i -= 2; + } + } + } + //end trimDots + + name = name.join('/'); + } + + //Apply map config if available. + if ((baseParts || starMap) && map) { + nameParts = name.split('/'); + + for (i = nameParts.length; i > 0; i -= 1) { + nameSegment = nameParts.slice(0, i).join("/"); + + if (baseParts) { + //Find the longest baseName segment match in the config. + //So, do joins on the biggest to smallest lengths of baseParts. + for (j = baseParts.length; j > 0; j -= 1) { + mapValue = map[baseParts.slice(0, j).join('/')]; + + //baseName segment has config, find if it has one for + //this name. + if (mapValue) { + mapValue = mapValue[nameSegment]; + if (mapValue) { + //Match, update name to the new value. + foundMap = mapValue; + foundI = i; + break; + } + } + } + } + + if (foundMap) { + break; + } + + //Check for a star map match, but just hold on to it, + //if there is a shorter segment match later in a matching + //config, then favor over this star map. + if (!foundStarMap && starMap && starMap[nameSegment]) { + foundStarMap = starMap[nameSegment]; + starI = i; + } + } + + if (!foundMap && foundStarMap) { + foundMap = foundStarMap; + foundI = starI; + } + + if (foundMap) { + nameParts.splice(0, foundI, foundMap); + name = nameParts.join('/'); + } + } + + return name; + } + + function makeRequire(relName, forceSync) { + return function () { + //A version of a require function that passes a moduleName + //value for items that may need to + //look up paths relative to the moduleName + var args = aps.call(arguments, 0); + + //If first arg is not require('string'), and there is only + //one arg, it is the array form without a callback. Insert + //a null so that the following concat is correct. + if (typeof args[0] !== 'string' && args.length === 1) { + args.push(null); + } + return req.apply(undef, args.concat([relName, forceSync])); + }; + } + + function makeNormalize(relName) { + return function (name) { + return normalize(name, relName); + }; + } + + function makeLoad(depName) { + return function (value) { + defined[depName] = value; + }; + } + + function callDep(name) { + if (hasProp(waiting, name)) { + var args = waiting[name]; + delete waiting[name]; + defining[name] = true; + main.apply(undef, args); + } + + if (!hasProp(defined, name) && !hasProp(defining, name)) { + throw new Error('No ' + name); + } + return defined[name]; + } + + //Turns a plugin!resource to [plugin, resource] + //with the plugin being undefined if the name + //did not have a plugin prefix. + function splitPrefix(name) { + var prefix, + index = name ? name.indexOf('!') : -1; + if (index > -1) { + prefix = name.substring(0, index); + name = name.substring(index + 1, name.length); + } + return [prefix, name]; + } + + //Creates a parts array for a relName where first part is plugin ID, + //second part is resource ID. Assumes relName has already been normalized. + function makeRelParts(relName) { + return relName ? splitPrefix(relName) : []; + } + + /** + * Makes a name map, normalizing the name, and using a plugin + * for normalization if necessary. Grabs a ref to plugin + * too, as an optimization. + */ + makeMap = function (name, relParts) { + var plugin, + parts = splitPrefix(name), + prefix = parts[0], + relResourceName = relParts[1]; + + name = parts[1]; + + if (prefix) { + prefix = normalize(prefix, relResourceName); + plugin = callDep(prefix); + } + + //Normalize according + if (prefix) { + if (plugin && plugin.normalize) { + name = plugin.normalize(name, makeNormalize(relResourceName)); + } else { + name = normalize(name, relResourceName); + } + } else { + name = normalize(name, relResourceName); + parts = splitPrefix(name); + prefix = parts[0]; + name = parts[1]; + if (prefix) { + plugin = callDep(prefix); + } + } + + //Using ridiculous property names for space reasons + return { + f: prefix ? prefix + '!' + name : name, //fullName + n: name, + pr: prefix, + p: plugin + }; + }; + + function makeConfig(name) { + return function () { + return (config && config.config && config.config[name]) || {}; + }; + } + + handlers = { + require: function (name) { + return makeRequire(name); + }, + exports: function (name) { + var e = defined[name]; + if (typeof e !== 'undefined') { + return e; + } else { + return (defined[name] = {}); + } + }, + module: function (name) { + return { + id: name, + uri: '', + exports: defined[name], + config: makeConfig(name) + }; + } + }; + + main = function (name, deps, callback, relName) { + var cjsModule, depName, ret, map, i, relParts, + args = [], + callbackType = typeof callback, + usingExports; + + //Use name if no relName + relName = relName || name; + relParts = makeRelParts(relName); + + //Call the callback to define the module, if necessary. + if (callbackType === 'undefined' || callbackType === 'function') { + //Pull out the defined dependencies and pass the ordered + //values to the callback. + //Default to [require, exports, module] if no deps + deps = !deps.length && callback.length ? ['require', 'exports', 'module'] : deps; + for (i = 0; i < deps.length; i += 1) { + map = makeMap(deps[i], relParts); + depName = map.f; + + //Fast path CommonJS standard dependencies. + if (depName === "require") { + args[i] = handlers.require(name); + } else if (depName === "exports") { + //CommonJS module spec 1.1 + args[i] = handlers.exports(name); + usingExports = true; + } else if (depName === "module") { + //CommonJS module spec 1.1 + cjsModule = args[i] = handlers.module(name); + } else if (hasProp(defined, depName) || + hasProp(waiting, depName) || + hasProp(defining, depName)) { + args[i] = callDep(depName); + } else if (map.p) { + map.p.load(map.n, makeRequire(relName, true), makeLoad(depName), {}); + args[i] = defined[depName]; + } else { + throw new Error(name + ' missing ' + depName); + } + } + + ret = callback ? callback.apply(defined[name], args) : undefined; + + if (name) { + //If setting exports via "module" is in play, + //favor that over return value and exports. After that, + //favor a non-undefined return value over exports use. + if (cjsModule && cjsModule.exports !== undef && + cjsModule.exports !== defined[name]) { + defined[name] = cjsModule.exports; + } else if (ret !== undef || !usingExports) { + //Use the return value from the function. + defined[name] = ret; + } + } + } else if (name) { + //May just be an object definition for the module. Only + //worry about defining if have a module name. + defined[name] = callback; + } + }; + + requirejs = require = req = function (deps, callback, relName, forceSync, alt) { + if (typeof deps === "string") { + if (handlers[deps]) { + //callback in this case is really relName + return handlers[deps](callback); + } + //Just return the module wanted. In this scenario, the + //deps arg is the module name, and second arg (if passed) + //is just the relName. + //Normalize module name, if it contains . or .. + return callDep(makeMap(deps, makeRelParts(callback)).f); + } else if (!deps.splice) { + //deps is a config object, not an array. + config = deps; + if (config.deps) { + req(config.deps, config.callback); + } + if (!callback) { + return; + } + + if (callback.splice) { + //callback is an array, which means it is a dependency list. + //Adjust args if there are dependencies + deps = callback; + callback = relName; + relName = null; + } else { + deps = undef; + } + } + + //Support require(['a']) + callback = callback || function () {}; + + //If relName is a function, it is an errback handler, + //so remove it. + if (typeof relName === 'function') { + relName = forceSync; + forceSync = alt; + } + + //Simulate async callback; + if (forceSync) { + main(undef, deps, callback, relName); + } else { + //Using a non-zero value because of concern for what old browsers + //do, and latest browsers "upgrade" to 4 if lower value is used: + //http://www.whatwg.org/specs/web-apps/current-work/multipage/timers.html#dom-windowtimers-settimeout: + //If want a value immediately, use require('id') instead -- something + //that works in almond on the global level, but not guaranteed and + //unlikely to work in other AMD implementations. + setTimeout(function () { + main(undef, deps, callback, relName); + }, 4); + } + + return req; + }; + + /** + * Just drops the config on the floor, but returns req in case + * the config return value is used. + */ + req.config = function (cfg) { + return req(cfg); + }; + + /** + * Expose module registry for debugging and tooling + */ + requirejs._defined = defined; + + define = function (name, deps, callback) { + if (typeof name !== 'string') { + throw new Error('See almond README: incorrect module build, no module name'); + } + + //This module may not have dependencies + if (!deps.splice) { + //deps is not an array, so probably means + //an object literal or factory function for + //the value. Adjust args. + callback = deps; + deps = []; + } + + if (!hasProp(defined, name) && !hasProp(waiting, name)) { + waiting[name] = [name, deps, callback]; + } + }; + + define.amd = { + jQuery: true + }; +}()); + +S2.requirejs = requirejs;S2.require = require;S2.define = define; +} +}()); +S2.define("almond", function(){}); + +/* global jQuery:false, $:false */ +S2.define('jquery',[],function () { + var _$ = jQuery || $; + + if (_$ == null && console && console.error) { + console.error( + 'Select2: An instance of jQuery or a jQuery-compatible library was not ' + + 'found. Make sure that you are including jQuery before Select2 on your ' + + 'web page.' + ); + } + + return _$; +}); + +S2.define('select2/utils',[ + 'jquery' +], function ($) { + var Utils = {}; + + Utils.Extend = function (ChildClass, SuperClass) { + var __hasProp = {}.hasOwnProperty; + + function BaseConstructor () { + this.constructor = ChildClass; + } + + for (var key in SuperClass) { + if (__hasProp.call(SuperClass, key)) { + ChildClass[key] = SuperClass[key]; + } + } + + BaseConstructor.prototype = SuperClass.prototype; + ChildClass.prototype = new BaseConstructor(); + ChildClass.__super__ = SuperClass.prototype; + + return ChildClass; + }; + + function getMethods (theClass) { + var proto = theClass.prototype; + + var methods = []; + + for (var methodName in proto) { + var m = proto[methodName]; + + if (typeof m !== 'function') { + continue; + } + + if (methodName === 'constructor') { + continue; + } + + methods.push(methodName); + } + + return methods; + } + + Utils.Decorate = function (SuperClass, DecoratorClass) { + var decoratedMethods = getMethods(DecoratorClass); + var superMethods = getMethods(SuperClass); + + function DecoratedClass () { + var unshift = Array.prototype.unshift; + + var argCount = DecoratorClass.prototype.constructor.length; + + var calledConstructor = SuperClass.prototype.constructor; + + if (argCount > 0) { + unshift.call(arguments, SuperClass.prototype.constructor); + + calledConstructor = DecoratorClass.prototype.constructor; + } + + calledConstructor.apply(this, arguments); + } + + DecoratorClass.displayName = SuperClass.displayName; + + function ctr () { + this.constructor = DecoratedClass; + } + + DecoratedClass.prototype = new ctr(); + + for (var m = 0; m < superMethods.length; m++) { + var superMethod = superMethods[m]; + + DecoratedClass.prototype[superMethod] = + SuperClass.prototype[superMethod]; + } + + var calledMethod = function (methodName) { + // Stub out the original method if it's not decorating an actual method + var originalMethod = function () {}; + + if (methodName in DecoratedClass.prototype) { + originalMethod = DecoratedClass.prototype[methodName]; + } + + var decoratedMethod = DecoratorClass.prototype[methodName]; + + return function () { + var unshift = Array.prototype.unshift; + + unshift.call(arguments, originalMethod); + + return decoratedMethod.apply(this, arguments); + }; + }; + + for (var d = 0; d < decoratedMethods.length; d++) { + var decoratedMethod = decoratedMethods[d]; + + DecoratedClass.prototype[decoratedMethod] = calledMethod(decoratedMethod); + } + + return DecoratedClass; + }; + + var Observable = function () { + this.listeners = {}; + }; + + Observable.prototype.on = function (event, callback) { + this.listeners = this.listeners || {}; + + if (event in this.listeners) { + this.listeners[event].push(callback); + } else { + this.listeners[event] = [callback]; + } + }; + + Observable.prototype.trigger = function (event) { + var slice = Array.prototype.slice; + var params = slice.call(arguments, 1); + + this.listeners = this.listeners || {}; + + // Params should always come in as an array + if (params == null) { + params = []; + } + + // If there are no arguments to the event, use a temporary object + if (params.length === 0) { + params.push({}); + } + + // Set the `_type` of the first object to the event + params[0]._type = event; + + if (event in this.listeners) { + this.invoke(this.listeners[event], slice.call(arguments, 1)); + } + + if ('*' in this.listeners) { + this.invoke(this.listeners['*'], arguments); + } + }; + + Observable.prototype.invoke = function (listeners, params) { + for (var i = 0, len = listeners.length; i < len; i++) { + listeners[i].apply(this, params); + } + }; + + Utils.Observable = Observable; + + Utils.generateChars = function (length) { + var chars = ''; + + for (var i = 0; i < length; i++) { + var randomChar = Math.floor(Math.random() * 36); + chars += randomChar.toString(36); + } + + return chars; + }; + + Utils.bind = function (func, context) { + return function () { + func.apply(context, arguments); + }; + }; + + Utils._convertData = function (data) { + for (var originalKey in data) { + var keys = originalKey.split('-'); + + var dataLevel = data; + + if (keys.length === 1) { + continue; + } + + for (var k = 0; k < keys.length; k++) { + var key = keys[k]; + + // Lowercase the first letter + // By default, dash-separated becomes camelCase + key = key.substring(0, 1).toLowerCase() + key.substring(1); + + if (!(key in dataLevel)) { + dataLevel[key] = {}; + } + + if (k == keys.length - 1) { + dataLevel[key] = data[originalKey]; + } + + dataLevel = dataLevel[key]; + } + + delete data[originalKey]; + } + + return data; + }; + + Utils.hasScroll = function (index, el) { + // Adapted from the function created by @ShadowScripter + // and adapted by @BillBarry on the Stack Exchange Code Review website. + // The original code can be found at + // http://codereview.stackexchange.com/q/13338 + // and was designed to be used with the Sizzle selector engine. + + var $el = $(el); + var overflowX = el.style.overflowX; + var overflowY = el.style.overflowY; + + //Check both x and y declarations + if (overflowX === overflowY && + (overflowY === 'hidden' || overflowY === 'visible')) { + return false; + } + + if (overflowX === 'scroll' || overflowY === 'scroll') { + return true; + } + + return ($el.innerHeight() < el.scrollHeight || + $el.innerWidth() < el.scrollWidth); + }; + + Utils.escapeMarkup = function (markup) { + var replaceMap = { + '\\': '\', + '&': '&', + '<': '<', + '>': '>', + '"': '"', + '\'': ''', + '/': '/' + }; + + // Do not try to escape the markup if it's not a string + if (typeof markup !== 'string') { + return markup; + } + + return String(markup).replace(/[&<>"'\/\\]/g, function (match) { + return replaceMap[match]; + }); + }; + + // Cache objects in Utils.__cache instead of $.data (see #4346) + Utils.__cache = {}; + + var id = 0; + Utils.GetUniqueElementId = function (element) { + // Get a unique element Id. If element has no id, + // creates a new unique number, stores it in the id + // attribute and returns the new id with a prefix. + // If an id already exists, it simply returns it with a prefix. + + var select2Id = element.getAttribute('data-select2-id'); + + if (select2Id != null) { + return select2Id; + } + + // If element has id, use it. + if (element.id) { + select2Id = 'select2-data-' + element.id; + } else { + select2Id = 'select2-data-' + (++id).toString() + + '-' + Utils.generateChars(4); + } + + element.setAttribute('data-select2-id', select2Id); + + return select2Id; + }; + + Utils.StoreData = function (element, name, value) { + // Stores an item in the cache for a specified element. + // name is the cache key. + var id = Utils.GetUniqueElementId(element); + if (!Utils.__cache[id]) { + Utils.__cache[id] = {}; + } + + Utils.__cache[id][name] = value; + }; + + Utils.GetData = function (element, name) { + // Retrieves a value from the cache by its key (name) + // name is optional. If no name specified, return + // all cache items for the specified element. + // and for a specified element. + var id = Utils.GetUniqueElementId(element); + if (name) { + if (Utils.__cache[id]) { + if (Utils.__cache[id][name] != null) { + return Utils.__cache[id][name]; + } + return $(element).data(name); // Fallback to HTML5 data attribs. + } + return $(element).data(name); // Fallback to HTML5 data attribs. + } else { + return Utils.__cache[id]; + } + }; + + Utils.RemoveData = function (element) { + // Removes all cached items for a specified element. + var id = Utils.GetUniqueElementId(element); + if (Utils.__cache[id] != null) { + delete Utils.__cache[id]; + } + + element.removeAttribute('data-select2-id'); + }; + + Utils.copyNonInternalCssClasses = function (dest, src) { + var classes; + + var destinationClasses = dest.getAttribute('class').trim().split(/\s+/); + + destinationClasses = destinationClasses.filter(function (clazz) { + // Save all Select2 classes + return clazz.indexOf('select2-') === 0; + }); + + var sourceClasses = src.getAttribute('class').trim().split(/\s+/); + + sourceClasses = sourceClasses.filter(function (clazz) { + // Only copy non-Select2 classes + return clazz.indexOf('select2-') !== 0; + }); + + var replacements = destinationClasses.concat(sourceClasses); + + dest.setAttribute('class', replacements.join(' ')); + }; + + return Utils; +}); + +S2.define('select2/results',[ + 'jquery', + './utils' +], function ($, Utils) { + function Results ($element, options, dataAdapter) { + this.$element = $element; + this.data = dataAdapter; + this.options = options; + + Results.__super__.constructor.call(this); + } + + Utils.Extend(Results, Utils.Observable); + + Results.prototype.render = function () { + var $results = $( + '
      ' + ); + + if (this.options.get('multiple')) { + $results.attr('aria-multiselectable', 'true'); + } + + this.$results = $results; + + return $results; + }; + + Results.prototype.clear = function () { + this.$results.empty(); + }; + + Results.prototype.displayMessage = function (params) { + var escapeMarkup = this.options.get('escapeMarkup'); + + this.clear(); + this.hideLoading(); + + var $message = $( + '' + ); + + var message = this.options.get('translations').get(params.message); + + $message.append( + escapeMarkup( + message(params.args) + ) + ); + + $message[0].className += ' select2-results__message'; + + this.$results.append($message); + }; + + Results.prototype.hideMessages = function () { + this.$results.find('.select2-results__message').remove(); + }; + + Results.prototype.append = function (data) { + this.hideLoading(); + + var $options = []; + + if (data.results == null || data.results.length === 0) { + if (this.$results.children().length === 0) { + this.trigger('results:message', { + message: 'noResults' + }); + } + + return; + } + + data.results = this.sort(data.results); + + for (var d = 0; d < data.results.length; d++) { + var item = data.results[d]; + + var $option = this.option(item); + + $options.push($option); + } + + this.$results.append($options); + }; + + Results.prototype.position = function ($results, $dropdown) { + var $resultsContainer = $dropdown.find('.select2-results'); + $resultsContainer.append($results); + }; + + Results.prototype.sort = function (data) { + var sorter = this.options.get('sorter'); + + return sorter(data); + }; + + Results.prototype.highlightFirstItem = function () { + var $options = this.$results + .find('.select2-results__option--selectable'); + + var $selected = $options.filter('.select2-results__option--selected'); + + // Check if there are any selected options + if ($selected.length > 0) { + // If there are selected options, highlight the first + $selected.first().trigger('mouseenter'); + } else { + // If there are no selected options, highlight the first option + // in the dropdown + $options.first().trigger('mouseenter'); + } + + this.ensureHighlightVisible(); + }; + + Results.prototype.setClasses = function () { + var self = this; + + this.data.current(function (selected) { + var selectedIds = selected.map(function (s) { + return s.id.toString(); + }); + + var $options = self.$results + .find('.select2-results__option--selectable'); + + $options.each(function () { + var $option = $(this); + + var item = Utils.GetData(this, 'data'); + + // id needs to be converted to a string when comparing + var id = '' + item.id; + + if ((item.element != null && item.element.selected) || + (item.element == null && selectedIds.indexOf(id) > -1)) { + this.classList.add('select2-results__option--selected'); + $option.attr('aria-selected', 'true'); + } else { + this.classList.remove('select2-results__option--selected'); + $option.attr('aria-selected', 'false'); + } + }); + + }); + }; + + Results.prototype.showLoading = function (params) { + this.hideLoading(); + + var loadingMore = this.options.get('translations').get('searching'); + + var loading = { + disabled: true, + loading: true, + text: loadingMore(params) + }; + var $loading = this.option(loading); + $loading.className += ' loading-results'; + + this.$results.prepend($loading); + }; + + Results.prototype.hideLoading = function () { + this.$results.find('.loading-results').remove(); + }; + + Results.prototype.option = function (data) { + var option = document.createElement('li'); + option.classList.add('select2-results__option'); + option.classList.add('select2-results__option--selectable'); + + var attrs = { + 'role': 'option' + }; + + var matches = window.Element.prototype.matches || + window.Element.prototype.msMatchesSelector || + window.Element.prototype.webkitMatchesSelector; + + if ((data.element != null && matches.call(data.element, ':disabled')) || + (data.element == null && data.disabled)) { + attrs['aria-disabled'] = 'true'; + + option.classList.remove('select2-results__option--selectable'); + option.classList.add('select2-results__option--disabled'); + } + + if (data.id == null) { + option.classList.remove('select2-results__option--selectable'); + } + + if (data._resultId != null) { + option.id = data._resultId; + } + + if (data.title) { + option.title = data.title; + } + + if (data.children) { + attrs.role = 'group'; + attrs['aria-label'] = data.text; + + option.classList.remove('select2-results__option--selectable'); + option.classList.add('select2-results__option--group'); + } + + for (var attr in attrs) { + var val = attrs[attr]; + + option.setAttribute(attr, val); + } + + if (data.children) { + var $option = $(option); + + var label = document.createElement('strong'); + label.className = 'select2-results__group'; + + this.template(data, label); + + var $children = []; + + for (var c = 0; c < data.children.length; c++) { + var child = data.children[c]; + + var $child = this.option(child); + + $children.push($child); + } + + var $childrenContainer = $('
        ', { + 'class': 'select2-results__options select2-results__options--nested', + 'role': 'none' + }); + + $childrenContainer.append($children); + + $option.append(label); + $option.append($childrenContainer); + } else { + this.template(data, option); + } + + Utils.StoreData(option, 'data', data); + + return option; + }; + + Results.prototype.bind = function (container, $container) { + var self = this; + + var id = container.id + '-results'; + + this.$results.attr('id', id); + + container.on('results:all', function (params) { + self.clear(); + self.append(params.data); + + if (container.isOpen()) { + self.setClasses(); + self.highlightFirstItem(); + } + }); + + container.on('results:append', function (params) { + self.append(params.data); + + if (container.isOpen()) { + self.setClasses(); + } + }); + + container.on('query', function (params) { + self.hideMessages(); + self.showLoading(params); + }); + + container.on('select', function () { + if (!container.isOpen()) { + return; + } + + self.setClasses(); + + if (self.options.get('scrollAfterSelect')) { + self.highlightFirstItem(); + } + }); + + container.on('unselect', function () { + if (!container.isOpen()) { + return; + } + + self.setClasses(); + + if (self.options.get('scrollAfterSelect')) { + self.highlightFirstItem(); + } + }); + + container.on('open', function () { + // When the dropdown is open, aria-expended="true" + self.$results.attr('aria-expanded', 'true'); + self.$results.attr('aria-hidden', 'false'); + + self.setClasses(); + self.ensureHighlightVisible(); + }); + + container.on('close', function () { + // When the dropdown is closed, aria-expended="false" + self.$results.attr('aria-expanded', 'false'); + self.$results.attr('aria-hidden', 'true'); + self.$results.removeAttr('aria-activedescendant'); + }); + + container.on('results:toggle', function () { + var $highlighted = self.getHighlightedResults(); + + if ($highlighted.length === 0) { + return; + } + + $highlighted.trigger('mouseup'); + }); + + container.on('results:select', function () { + var $highlighted = self.getHighlightedResults(); + + if ($highlighted.length === 0) { + return; + } + + var data = Utils.GetData($highlighted[0], 'data'); + + if ($highlighted.hasClass('select2-results__option--selected')) { + self.trigger('close', {}); + } else { + self.trigger('select', { + data: data + }); + } + }); + + container.on('results:previous', function () { + var $highlighted = self.getHighlightedResults(); + + var $options = self.$results.find('.select2-results__option--selectable'); + + var currentIndex = $options.index($highlighted); + + // If we are already at the top, don't move further + // If no options, currentIndex will be -1 + if (currentIndex <= 0) { + return; + } + + var nextIndex = currentIndex - 1; + + // If none are highlighted, highlight the first + if ($highlighted.length === 0) { + nextIndex = 0; + } + + var $next = $options.eq(nextIndex); + + $next.trigger('mouseenter'); + + var currentOffset = self.$results.offset().top; + var nextTop = $next.offset().top; + var nextOffset = self.$results.scrollTop() + (nextTop - currentOffset); + + if (nextIndex === 0) { + self.$results.scrollTop(0); + } else if (nextTop - currentOffset < 0) { + self.$results.scrollTop(nextOffset); + } + }); + + container.on('results:next', function () { + var $highlighted = self.getHighlightedResults(); + + var $options = self.$results.find('.select2-results__option--selectable'); + + var currentIndex = $options.index($highlighted); + + var nextIndex = currentIndex + 1; + + // If we are at the last option, stay there + if (nextIndex >= $options.length) { + return; + } + + var $next = $options.eq(nextIndex); + + $next.trigger('mouseenter'); + + var currentOffset = self.$results.offset().top + + self.$results.outerHeight(false); + var nextBottom = $next.offset().top + $next.outerHeight(false); + var nextOffset = self.$results.scrollTop() + nextBottom - currentOffset; + + if (nextIndex === 0) { + self.$results.scrollTop(0); + } else if (nextBottom > currentOffset) { + self.$results.scrollTop(nextOffset); + } + }); + + container.on('results:focus', function (params) { + params.element[0].classList.add('select2-results__option--highlighted'); + params.element[0].setAttribute('aria-selected', 'true'); + }); + + container.on('results:message', function (params) { + self.displayMessage(params); + }); + + if ($.fn.mousewheel) { + this.$results.on('mousewheel', function (e) { + var top = self.$results.scrollTop(); + + var bottom = self.$results.get(0).scrollHeight - top + e.deltaY; + + var isAtTop = e.deltaY > 0 && top - e.deltaY <= 0; + var isAtBottom = e.deltaY < 0 && bottom <= self.$results.height(); + + if (isAtTop) { + self.$results.scrollTop(0); + + e.preventDefault(); + e.stopPropagation(); + } else if (isAtBottom) { + self.$results.scrollTop( + self.$results.get(0).scrollHeight - self.$results.height() + ); + + e.preventDefault(); + e.stopPropagation(); + } + }); + } + + this.$results.on('mouseup', '.select2-results__option--selectable', + function (evt) { + var $this = $(this); + + var data = Utils.GetData(this, 'data'); + + if ($this.hasClass('select2-results__option--selected')) { + if (self.options.get('multiple')) { + self.trigger('unselect', { + originalEvent: evt, + data: data + }); + } else { + self.trigger('close', {}); + } + + return; + } + + self.trigger('select', { + originalEvent: evt, + data: data + }); + }); + + this.$results.on('mouseenter', '.select2-results__option--selectable', + function (evt) { + var data = Utils.GetData(this, 'data'); + + self.getHighlightedResults() + .removeClass('select2-results__option--highlighted') + .attr('aria-selected', 'false'); + + self.trigger('results:focus', { + data: data, + element: $(this) + }); + }); + }; + + Results.prototype.getHighlightedResults = function () { + var $highlighted = this.$results + .find('.select2-results__option--highlighted'); + + return $highlighted; + }; + + Results.prototype.destroy = function () { + this.$results.remove(); + }; + + Results.prototype.ensureHighlightVisible = function () { + var $highlighted = this.getHighlightedResults(); + + if ($highlighted.length === 0) { + return; + } + + var $options = this.$results.find('.select2-results__option--selectable'); + + var currentIndex = $options.index($highlighted); + + var currentOffset = this.$results.offset().top; + var nextTop = $highlighted.offset().top; + var nextOffset = this.$results.scrollTop() + (nextTop - currentOffset); + + var offsetDelta = nextTop - currentOffset; + nextOffset -= $highlighted.outerHeight(false) * 2; + + if (currentIndex <= 2) { + this.$results.scrollTop(0); + } else if (offsetDelta > this.$results.outerHeight() || offsetDelta < 0) { + this.$results.scrollTop(nextOffset); + } + }; + + Results.prototype.template = function (result, container) { + var template = this.options.get('templateResult'); + var escapeMarkup = this.options.get('escapeMarkup'); + + var content = template(result, container); + + if (content == null) { + container.style.display = 'none'; + } else if (typeof content === 'string') { + container.innerHTML = escapeMarkup(content); + } else { + $(container).append(content); + } + }; + + return Results; +}); + +S2.define('select2/keys',[ + +], function () { + var KEYS = { + BACKSPACE: 8, + TAB: 9, + ENTER: 13, + SHIFT: 16, + CTRL: 17, + ALT: 18, + ESC: 27, + SPACE: 32, + PAGE_UP: 33, + PAGE_DOWN: 34, + END: 35, + HOME: 36, + LEFT: 37, + UP: 38, + RIGHT: 39, + DOWN: 40, + DELETE: 46 + }; + + return KEYS; +}); + +S2.define('select2/selection/base',[ + 'jquery', + '../utils', + '../keys' +], function ($, Utils, KEYS) { + function BaseSelection ($element, options) { + this.$element = $element; + this.options = options; + + BaseSelection.__super__.constructor.call(this); + } + + Utils.Extend(BaseSelection, Utils.Observable); + + BaseSelection.prototype.render = function () { + var $selection = $( + '' + ); + + this._tabindex = 0; + + if (Utils.GetData(this.$element[0], 'old-tabindex') != null) { + this._tabindex = Utils.GetData(this.$element[0], 'old-tabindex'); + } else if (this.$element.attr('tabindex') != null) { + this._tabindex = this.$element.attr('tabindex'); + } + + $selection.attr('title', this.$element.attr('title')); + $selection.attr('tabindex', this._tabindex); + $selection.attr('aria-disabled', 'false'); + + this.$selection = $selection; + + return $selection; + }; + + BaseSelection.prototype.bind = function (container, $container) { + var self = this; + + var resultsId = container.id + '-results'; + + this.container = container; + + this.$selection.on('focus', function (evt) { + self.trigger('focus', evt); + }); + + this.$selection.on('blur', function (evt) { + self._handleBlur(evt); + }); + + this.$selection.on('keydown', function (evt) { + self.trigger('keypress', evt); + + if (evt.which === KEYS.SPACE) { + evt.preventDefault(); + } + }); + + container.on('results:focus', function (params) { + self.$selection.attr('aria-activedescendant', params.data._resultId); + }); + + container.on('selection:update', function (params) { + self.update(params.data); + }); + + container.on('open', function () { + // When the dropdown is open, aria-expanded="true" + self.$selection.attr('aria-expanded', 'true'); + self.$selection.attr('aria-owns', resultsId); + + self._attachCloseHandler(container); + }); + + container.on('close', function () { + // When the dropdown is closed, aria-expanded="false" + self.$selection.attr('aria-expanded', 'false'); + self.$selection.removeAttr('aria-activedescendant'); + self.$selection.removeAttr('aria-owns'); + + self.$selection.trigger('focus'); + + self._detachCloseHandler(container); + }); + + container.on('enable', function () { + self.$selection.attr('tabindex', self._tabindex); + self.$selection.attr('aria-disabled', 'false'); + }); + + container.on('disable', function () { + self.$selection.attr('tabindex', '-1'); + self.$selection.attr('aria-disabled', 'true'); + }); + }; + + BaseSelection.prototype._handleBlur = function (evt) { + var self = this; + + // This needs to be delayed as the active element is the body when the tab + // key is pressed, possibly along with others. + window.setTimeout(function () { + // Don't trigger `blur` if the focus is still in the selection + if ( + (document.activeElement == self.$selection[0]) || + ($.contains(self.$selection[0], document.activeElement)) + ) { + return; + } + + self.trigger('blur', evt); + }, 1); + }; + + BaseSelection.prototype._attachCloseHandler = function (container) { + + $(document.body).on('mousedown.select2.' + container.id, function (e) { + var $target = $(e.target); + + var $select = $target.closest('.select2'); + + var $all = $('.select2.select2-container--open'); + + $all.each(function () { + if (this == $select[0]) { + return; + } + + var $element = Utils.GetData(this, 'element'); + + $element.select2('close'); + }); + }); + }; + + BaseSelection.prototype._detachCloseHandler = function (container) { + $(document.body).off('mousedown.select2.' + container.id); + }; + + BaseSelection.prototype.position = function ($selection, $container) { + var $selectionContainer = $container.find('.selection'); + $selectionContainer.append($selection); + }; + + BaseSelection.prototype.destroy = function () { + this._detachCloseHandler(this.container); + }; + + BaseSelection.prototype.update = function (data) { + throw new Error('The `update` method must be defined in child classes.'); + }; + + /** + * Helper method to abstract the "enabled" (not "disabled") state of this + * object. + * + * @return {true} if the instance is not disabled. + * @return {false} if the instance is disabled. + */ + BaseSelection.prototype.isEnabled = function () { + return !this.isDisabled(); + }; + + /** + * Helper method to abstract the "disabled" state of this object. + * + * @return {true} if the disabled option is true. + * @return {false} if the disabled option is false. + */ + BaseSelection.prototype.isDisabled = function () { + return this.options.get('disabled'); + }; + + return BaseSelection; +}); + +S2.define('select2/selection/single',[ + 'jquery', + './base', + '../utils', + '../keys' +], function ($, BaseSelection, Utils, KEYS) { + function SingleSelection () { + SingleSelection.__super__.constructor.apply(this, arguments); + } + + Utils.Extend(SingleSelection, BaseSelection); + + SingleSelection.prototype.render = function () { + var $selection = SingleSelection.__super__.render.call(this); + + $selection[0].classList.add('select2-selection--single'); + + $selection.html( + '' + + '' + + '' + + '' + ); + + return $selection; + }; + + SingleSelection.prototype.bind = function (container, $container) { + var self = this; + + SingleSelection.__super__.bind.apply(this, arguments); + + var id = container.id + '-container'; + + this.$selection.find('.select2-selection__rendered') + .attr('id', id) + .attr('role', 'textbox') + .attr('aria-readonly', 'true'); + this.$selection.attr('aria-labelledby', id); + this.$selection.attr('aria-controls', id); + + this.$selection.on('mousedown', function (evt) { + // Only respond to left clicks + if (evt.which !== 1) { + return; + } + + self.trigger('toggle', { + originalEvent: evt + }); + }); + + this.$selection.on('focus', function (evt) { + // User focuses on the container + }); + + this.$selection.on('blur', function (evt) { + // User exits the container + }); + + container.on('focus', function (evt) { + if (!container.isOpen()) { + self.$selection.trigger('focus'); + } + }); + }; + + SingleSelection.prototype.clear = function () { + var $rendered = this.$selection.find('.select2-selection__rendered'); + $rendered.empty(); + $rendered.removeAttr('title'); // clear tooltip on empty + }; + + SingleSelection.prototype.display = function (data, container) { + var template = this.options.get('templateSelection'); + var escapeMarkup = this.options.get('escapeMarkup'); + + return escapeMarkup(template(data, container)); + }; + + SingleSelection.prototype.selectionContainer = function () { + return $(''); + }; + + SingleSelection.prototype.update = function (data) { + if (data.length === 0) { + this.clear(); + return; + } + + var selection = data[0]; + + var $rendered = this.$selection.find('.select2-selection__rendered'); + var formatted = this.display(selection, $rendered); + + $rendered.empty().append(formatted); + + var title = selection.title || selection.text; + + if (title) { + $rendered.attr('title', title); + } else { + $rendered.removeAttr('title'); + } + }; + + return SingleSelection; +}); + +S2.define('select2/selection/multiple',[ + 'jquery', + './base', + '../utils' +], function ($, BaseSelection, Utils) { + function MultipleSelection ($element, options) { + MultipleSelection.__super__.constructor.apply(this, arguments); + } + + Utils.Extend(MultipleSelection, BaseSelection); + + MultipleSelection.prototype.render = function () { + var $selection = MultipleSelection.__super__.render.call(this); + + $selection[0].classList.add('select2-selection--multiple'); + + $selection.html( + '
          ' + ); + + return $selection; + }; + + MultipleSelection.prototype.bind = function (container, $container) { + var self = this; + + MultipleSelection.__super__.bind.apply(this, arguments); + + var id = container.id + '-container'; + this.$selection.find('.select2-selection__rendered').attr('id', id); + + this.$selection.on('click', function (evt) { + self.trigger('toggle', { + originalEvent: evt + }); + }); + + this.$selection.on( + 'click', + '.select2-selection__choice__remove', + function (evt) { + // Ignore the event if it is disabled + if (self.isDisabled()) { + return; + } + + var $remove = $(this); + var $selection = $remove.parent(); + + var data = Utils.GetData($selection[0], 'data'); + + self.trigger('unselect', { + originalEvent: evt, + data: data + }); + } + ); + + this.$selection.on( + 'keydown', + '.select2-selection__choice__remove', + function (evt) { + // Ignore the event if it is disabled + if (self.isDisabled()) { + return; + } + + evt.stopPropagation(); + } + ); + }; + + MultipleSelection.prototype.clear = function () { + var $rendered = this.$selection.find('.select2-selection__rendered'); + $rendered.empty(); + $rendered.removeAttr('title'); + }; + + MultipleSelection.prototype.display = function (data, container) { + var template = this.options.get('templateSelection'); + var escapeMarkup = this.options.get('escapeMarkup'); + + return escapeMarkup(template(data, container)); + }; + + MultipleSelection.prototype.selectionContainer = function () { + var $container = $( + '
        • ' + + '' + + '' + + '
        • ' + ); + + return $container; + }; + + MultipleSelection.prototype.update = function (data) { + this.clear(); + + if (data.length === 0) { + return; + } + + var $selections = []; + + var selectionIdPrefix = this.$selection.find('.select2-selection__rendered') + .attr('id') + '-choice-'; + + for (var d = 0; d < data.length; d++) { + var selection = data[d]; + + var $selection = this.selectionContainer(); + var formatted = this.display(selection, $selection); + + var selectionId = selectionIdPrefix + Utils.generateChars(4) + '-'; + + if (selection.id) { + selectionId += selection.id; + } else { + selectionId += Utils.generateChars(4); + } + + $selection.find('.select2-selection__choice__display') + .append(formatted) + .attr('id', selectionId); + + var title = selection.title || selection.text; + + if (title) { + $selection.attr('title', title); + } + + var removeItem = this.options.get('translations').get('removeItem'); + + var $remove = $selection.find('.select2-selection__choice__remove'); + + $remove.attr('title', removeItem()); + $remove.attr('aria-label', removeItem()); + $remove.attr('aria-describedby', selectionId); + + Utils.StoreData($selection[0], 'data', selection); + + $selections.push($selection); + } + + var $rendered = this.$selection.find('.select2-selection__rendered'); + + $rendered.append($selections); + }; + + return MultipleSelection; +}); + +S2.define('select2/selection/placeholder',[ + +], function () { + function Placeholder (decorated, $element, options) { + this.placeholder = this.normalizePlaceholder(options.get('placeholder')); + + decorated.call(this, $element, options); + } + + Placeholder.prototype.normalizePlaceholder = function (_, placeholder) { + if (typeof placeholder === 'string') { + placeholder = { + id: '', + text: placeholder + }; + } + + return placeholder; + }; + + Placeholder.prototype.createPlaceholder = function (decorated, placeholder) { + var $placeholder = this.selectionContainer(); + + $placeholder.html(this.display(placeholder)); + $placeholder[0].classList.add('select2-selection__placeholder'); + $placeholder[0].classList.remove('select2-selection__choice'); + + var placeholderTitle = placeholder.title || + placeholder.text || + $placeholder.text(); + + this.$selection.find('.select2-selection__rendered').attr( + 'title', + placeholderTitle + ); + + return $placeholder; + }; + + Placeholder.prototype.update = function (decorated, data) { + var singlePlaceholder = ( + data.length == 1 && data[0].id != this.placeholder.id + ); + var multipleSelections = data.length > 1; + + if (multipleSelections || singlePlaceholder) { + return decorated.call(this, data); + } + + this.clear(); + + var $placeholder = this.createPlaceholder(this.placeholder); + + this.$selection.find('.select2-selection__rendered').append($placeholder); + }; + + return Placeholder; +}); + +S2.define('select2/selection/allowClear',[ + 'jquery', + '../keys', + '../utils' +], function ($, KEYS, Utils) { + function AllowClear () { } + + AllowClear.prototype.bind = function (decorated, container, $container) { + var self = this; + + decorated.call(this, container, $container); + + if (this.placeholder == null) { + if (this.options.get('debug') && window.console && console.error) { + console.error( + 'Select2: The `allowClear` option should be used in combination ' + + 'with the `placeholder` option.' + ); + } + } + + this.$selection.on('mousedown', '.select2-selection__clear', + function (evt) { + self._handleClear(evt); + }); + + container.on('keypress', function (evt) { + self._handleKeyboardClear(evt, container); + }); + }; + + AllowClear.prototype._handleClear = function (_, evt) { + // Ignore the event if it is disabled + if (this.isDisabled()) { + return; + } + + var $clear = this.$selection.find('.select2-selection__clear'); + + // Ignore the event if nothing has been selected + if ($clear.length === 0) { + return; + } + + evt.stopPropagation(); + + var data = Utils.GetData($clear[0], 'data'); + + var previousVal = this.$element.val(); + this.$element.val(this.placeholder.id); + + var unselectData = { + data: data + }; + this.trigger('clear', unselectData); + if (unselectData.prevented) { + this.$element.val(previousVal); + return; + } + + for (var d = 0; d < data.length; d++) { + unselectData = { + data: data[d] + }; + + // Trigger the `unselect` event, so people can prevent it from being + // cleared. + this.trigger('unselect', unselectData); + + // If the event was prevented, don't clear it out. + if (unselectData.prevented) { + this.$element.val(previousVal); + return; + } + } + + this.$element.trigger('input').trigger('change'); + + this.trigger('toggle', {}); + }; + + AllowClear.prototype._handleKeyboardClear = function (_, evt, container) { + if (container.isOpen()) { + return; + } + + if (evt.which == KEYS.DELETE || evt.which == KEYS.BACKSPACE) { + this._handleClear(evt); + } + }; + + AllowClear.prototype.update = function (decorated, data) { + decorated.call(this, data); + + this.$selection.find('.select2-selection__clear').remove(); + this.$selection[0].classList.remove('select2-selection--clearable'); + + if (this.$selection.find('.select2-selection__placeholder').length > 0 || + data.length === 0) { + return; + } + + var selectionId = this.$selection.find('.select2-selection__rendered') + .attr('id'); + + var removeAll = this.options.get('translations').get('removeAllItems'); + + var $remove = $( + '' + ); + $remove.attr('title', removeAll()); + $remove.attr('aria-label', removeAll()); + $remove.attr('aria-describedby', selectionId); + Utils.StoreData($remove[0], 'data', data); + + this.$selection.prepend($remove); + this.$selection[0].classList.add('select2-selection--clearable'); + }; + + return AllowClear; +}); + +S2.define('select2/selection/search',[ + 'jquery', + '../utils', + '../keys' +], function ($, Utils, KEYS) { + function Search (decorated, $element, options) { + decorated.call(this, $element, options); + } + + Search.prototype.render = function (decorated) { + var searchLabel = this.options.get('translations').get('search'); + var $search = $( + '' + + '' + + '' + ); + + this.$searchContainer = $search; + this.$search = $search.find('textarea'); + + this.$search.prop('autocomplete', this.options.get('autocomplete')); + this.$search.attr('aria-label', searchLabel()); + + var $rendered = decorated.call(this); + + this._transferTabIndex(); + $rendered.append(this.$searchContainer); + + return $rendered; + }; + + Search.prototype.bind = function (decorated, container, $container) { + var self = this; + + var resultsId = container.id + '-results'; + var selectionId = container.id + '-container'; + + decorated.call(this, container, $container); + + self.$search.attr('aria-describedby', selectionId); + + container.on('open', function () { + self.$search.attr('aria-controls', resultsId); + self.$search.trigger('focus'); + }); + + container.on('close', function () { + self.$search.val(''); + self.resizeSearch(); + self.$search.removeAttr('aria-controls'); + self.$search.removeAttr('aria-activedescendant'); + self.$search.trigger('focus'); + }); + + container.on('enable', function () { + self.$search.prop('disabled', false); + + self._transferTabIndex(); + }); + + container.on('disable', function () { + self.$search.prop('disabled', true); + }); + + container.on('focus', function (evt) { + self.$search.trigger('focus'); + }); + + container.on('results:focus', function (params) { + if (params.data._resultId) { + self.$search.attr('aria-activedescendant', params.data._resultId); + } else { + self.$search.removeAttr('aria-activedescendant'); + } + }); + + this.$selection.on('focusin', '.select2-search--inline', function (evt) { + self.trigger('focus', evt); + }); + + this.$selection.on('focusout', '.select2-search--inline', function (evt) { + self._handleBlur(evt); + }); + + this.$selection.on('keydown', '.select2-search--inline', function (evt) { + evt.stopPropagation(); + + self.trigger('keypress', evt); + + self._keyUpPrevented = evt.isDefaultPrevented(); + + var key = evt.which; + + if (key === KEYS.BACKSPACE && self.$search.val() === '') { + var $previousChoice = self.$selection + .find('.select2-selection__choice').last(); + + if ($previousChoice.length > 0) { + var item = Utils.GetData($previousChoice[0], 'data'); + + self.searchRemoveChoice(item); + + evt.preventDefault(); + } + } + }); + + this.$selection.on('click', '.select2-search--inline', function (evt) { + if (self.$search.val()) { + evt.stopPropagation(); + } + }); + + // Try to detect the IE version should the `documentMode` property that + // is stored on the document. This is only implemented in IE and is + // slightly cleaner than doing a user agent check. + // This property is not available in Edge, but Edge also doesn't have + // this bug. + var msie = document.documentMode; + var disableInputEvents = msie && msie <= 11; + + // Workaround for browsers which do not support the `input` event + // This will prevent double-triggering of events for browsers which support + // both the `keyup` and `input` events. + this.$selection.on( + 'input.searchcheck', + '.select2-search--inline', + function (evt) { + // IE will trigger the `input` event when a placeholder is used on a + // search box. To get around this issue, we are forced to ignore all + // `input` events in IE and keep using `keyup`. + if (disableInputEvents) { + self.$selection.off('input.search input.searchcheck'); + return; + } + + // Unbind the duplicated `keyup` event + self.$selection.off('keyup.search'); + } + ); + + this.$selection.on( + 'keyup.search input.search', + '.select2-search--inline', + function (evt) { + // IE will trigger the `input` event when a placeholder is used on a + // search box. To get around this issue, we are forced to ignore all + // `input` events in IE and keep using `keyup`. + if (disableInputEvents && evt.type === 'input') { + self.$selection.off('input.search input.searchcheck'); + return; + } + + var key = evt.which; + + // We can freely ignore events from modifier keys + if (key == KEYS.SHIFT || key == KEYS.CTRL || key == KEYS.ALT) { + return; + } + + // Tabbing will be handled during the `keydown` phase + if (key == KEYS.TAB) { + return; + } + + self.handleSearch(evt); + } + ); + }; + + /** + * This method will transfer the tabindex attribute from the rendered + * selection to the search box. This allows for the search box to be used as + * the primary focus instead of the selection container. + * + * @private + */ + Search.prototype._transferTabIndex = function (decorated) { + this.$search.attr('tabindex', this.$selection.attr('tabindex')); + this.$selection.attr('tabindex', '-1'); + }; + + Search.prototype.createPlaceholder = function (decorated, placeholder) { + this.$search.attr('placeholder', placeholder.text); + }; + + Search.prototype.update = function (decorated, data) { + var searchHadFocus = this.$search[0] == document.activeElement; + + this.$search.attr('placeholder', ''); + + decorated.call(this, data); + + this.resizeSearch(); + if (searchHadFocus) { + this.$search.trigger('focus'); + } + }; + + Search.prototype.handleSearch = function () { + this.resizeSearch(); + + if (!this._keyUpPrevented) { + var input = this.$search.val(); + + this.trigger('query', { + term: input + }); + } + + this._keyUpPrevented = false; + }; + + Search.prototype.searchRemoveChoice = function (decorated, item) { + this.trigger('unselect', { + data: item + }); + + this.$search.val(item.text); + this.handleSearch(); + }; + + Search.prototype.resizeSearch = function () { + this.$search.css('width', '25px'); + + var width = '100%'; + + if (this.$search.attr('placeholder') === '') { + var minimumWidth = this.$search.val().length + 1; + + width = (minimumWidth * 0.75) + 'em'; + } + + this.$search.css('width', width); + }; + + return Search; +}); + +S2.define('select2/selection/selectionCss',[ + '../utils' +], function (Utils) { + function SelectionCSS () { } + + SelectionCSS.prototype.render = function (decorated) { + var $selection = decorated.call(this); + + var selectionCssClass = this.options.get('selectionCssClass') || ''; + + if (selectionCssClass.indexOf(':all:') !== -1) { + selectionCssClass = selectionCssClass.replace(':all:', ''); + + Utils.copyNonInternalCssClasses($selection[0], this.$element[0]); + } + + $selection.addClass(selectionCssClass); + + return $selection; + }; + + return SelectionCSS; +}); + +S2.define('select2/selection/eventRelay',[ + 'jquery' +], function ($) { + function EventRelay () { } + + EventRelay.prototype.bind = function (decorated, container, $container) { + var self = this; + var relayEvents = [ + 'open', 'opening', + 'close', 'closing', + 'select', 'selecting', + 'unselect', 'unselecting', + 'clear', 'clearing' + ]; + + var preventableEvents = [ + 'opening', 'closing', 'selecting', 'unselecting', 'clearing' + ]; + + decorated.call(this, container, $container); + + container.on('*', function (name, params) { + // Ignore events that should not be relayed + if (relayEvents.indexOf(name) === -1) { + return; + } + + // The parameters should always be an object + params = params || {}; + + // Generate the jQuery event for the Select2 event + var evt = $.Event('select2:' + name, { + params: params + }); + + self.$element.trigger(evt); + + // Only handle preventable events if it was one + if (preventableEvents.indexOf(name) === -1) { + return; + } + + params.prevented = evt.isDefaultPrevented(); + }); + }; + + return EventRelay; +}); + +S2.define('select2/translation',[ + 'jquery', + 'require' +], function ($, require) { + function Translation (dict) { + this.dict = dict || {}; + } + + Translation.prototype.all = function () { + return this.dict; + }; + + Translation.prototype.get = function (key) { + return this.dict[key]; + }; + + Translation.prototype.extend = function (translation) { + this.dict = $.extend({}, translation.all(), this.dict); + }; + + // Static functions + + Translation._cache = {}; + + Translation.loadPath = function (path) { + if (!(path in Translation._cache)) { + var translations = require(path); + + Translation._cache[path] = translations; + } + + return new Translation(Translation._cache[path]); + }; + + return Translation; +}); + +S2.define('select2/diacritics',[ + +], function () { + var diacritics = { + '\u24B6': 'A', + '\uFF21': 'A', + '\u00C0': 'A', + '\u00C1': 'A', + '\u00C2': 'A', + '\u1EA6': 'A', + '\u1EA4': 'A', + '\u1EAA': 'A', + '\u1EA8': 'A', + '\u00C3': 'A', + '\u0100': 'A', + '\u0102': 'A', + '\u1EB0': 'A', + '\u1EAE': 'A', + '\u1EB4': 'A', + '\u1EB2': 'A', + '\u0226': 'A', + '\u01E0': 'A', + '\u00C4': 'A', + '\u01DE': 'A', + '\u1EA2': 'A', + '\u00C5': 'A', + '\u01FA': 'A', + '\u01CD': 'A', + '\u0200': 'A', + '\u0202': 'A', + '\u1EA0': 'A', + '\u1EAC': 'A', + '\u1EB6': 'A', + '\u1E00': 'A', + '\u0104': 'A', + '\u023A': 'A', + '\u2C6F': 'A', + '\uA732': 'AA', + '\u00C6': 'AE', + '\u01FC': 'AE', + '\u01E2': 'AE', + '\uA734': 'AO', + '\uA736': 'AU', + '\uA738': 'AV', + '\uA73A': 'AV', + '\uA73C': 'AY', + '\u24B7': 'B', + '\uFF22': 'B', + '\u1E02': 'B', + '\u1E04': 'B', + '\u1E06': 'B', + '\u0243': 'B', + '\u0182': 'B', + '\u0181': 'B', + '\u24B8': 'C', + '\uFF23': 'C', + '\u0106': 'C', + '\u0108': 'C', + '\u010A': 'C', + '\u010C': 'C', + '\u00C7': 'C', + '\u1E08': 'C', + '\u0187': 'C', + '\u023B': 'C', + '\uA73E': 'C', + '\u24B9': 'D', + '\uFF24': 'D', + '\u1E0A': 'D', + '\u010E': 'D', + '\u1E0C': 'D', + '\u1E10': 'D', + '\u1E12': 'D', + '\u1E0E': 'D', + '\u0110': 'D', + '\u018B': 'D', + '\u018A': 'D', + '\u0189': 'D', + '\uA779': 'D', + '\u01F1': 'DZ', + '\u01C4': 'DZ', + '\u01F2': 'Dz', + '\u01C5': 'Dz', + '\u24BA': 'E', + '\uFF25': 'E', + '\u00C8': 'E', + '\u00C9': 'E', + '\u00CA': 'E', + '\u1EC0': 'E', + '\u1EBE': 'E', + '\u1EC4': 'E', + '\u1EC2': 'E', + '\u1EBC': 'E', + '\u0112': 'E', + '\u1E14': 'E', + '\u1E16': 'E', + '\u0114': 'E', + '\u0116': 'E', + '\u00CB': 'E', + '\u1EBA': 'E', + '\u011A': 'E', + '\u0204': 'E', + '\u0206': 'E', + '\u1EB8': 'E', + '\u1EC6': 'E', + '\u0228': 'E', + '\u1E1C': 'E', + '\u0118': 'E', + '\u1E18': 'E', + '\u1E1A': 'E', + '\u0190': 'E', + '\u018E': 'E', + '\u24BB': 'F', + '\uFF26': 'F', + '\u1E1E': 'F', + '\u0191': 'F', + '\uA77B': 'F', + '\u24BC': 'G', + '\uFF27': 'G', + '\u01F4': 'G', + '\u011C': 'G', + '\u1E20': 'G', + '\u011E': 'G', + '\u0120': 'G', + '\u01E6': 'G', + '\u0122': 'G', + '\u01E4': 'G', + '\u0193': 'G', + '\uA7A0': 'G', + '\uA77D': 'G', + '\uA77E': 'G', + '\u24BD': 'H', + '\uFF28': 'H', + '\u0124': 'H', + '\u1E22': 'H', + '\u1E26': 'H', + '\u021E': 'H', + '\u1E24': 'H', + '\u1E28': 'H', + '\u1E2A': 'H', + '\u0126': 'H', + '\u2C67': 'H', + '\u2C75': 'H', + '\uA78D': 'H', + '\u24BE': 'I', + '\uFF29': 'I', + '\u00CC': 'I', + '\u00CD': 'I', + '\u00CE': 'I', + '\u0128': 'I', + '\u012A': 'I', + '\u012C': 'I', + '\u0130': 'I', + '\u00CF': 'I', + '\u1E2E': 'I', + '\u1EC8': 'I', + '\u01CF': 'I', + '\u0208': 'I', + '\u020A': 'I', + '\u1ECA': 'I', + '\u012E': 'I', + '\u1E2C': 'I', + '\u0197': 'I', + '\u24BF': 'J', + '\uFF2A': 'J', + '\u0134': 'J', + '\u0248': 'J', + '\u24C0': 'K', + '\uFF2B': 'K', + '\u1E30': 'K', + '\u01E8': 'K', + '\u1E32': 'K', + '\u0136': 'K', + '\u1E34': 'K', + '\u0198': 'K', + '\u2C69': 'K', + '\uA740': 'K', + '\uA742': 'K', + '\uA744': 'K', + '\uA7A2': 'K', + '\u24C1': 'L', + '\uFF2C': 'L', + '\u013F': 'L', + '\u0139': 'L', + '\u013D': 'L', + '\u1E36': 'L', + '\u1E38': 'L', + '\u013B': 'L', + '\u1E3C': 'L', + '\u1E3A': 'L', + '\u0141': 'L', + '\u023D': 'L', + '\u2C62': 'L', + '\u2C60': 'L', + '\uA748': 'L', + '\uA746': 'L', + '\uA780': 'L', + '\u01C7': 'LJ', + '\u01C8': 'Lj', + '\u24C2': 'M', + '\uFF2D': 'M', + '\u1E3E': 'M', + '\u1E40': 'M', + '\u1E42': 'M', + '\u2C6E': 'M', + '\u019C': 'M', + '\u24C3': 'N', + '\uFF2E': 'N', + '\u01F8': 'N', + '\u0143': 'N', + '\u00D1': 'N', + '\u1E44': 'N', + '\u0147': 'N', + '\u1E46': 'N', + '\u0145': 'N', + '\u1E4A': 'N', + '\u1E48': 'N', + '\u0220': 'N', + '\u019D': 'N', + '\uA790': 'N', + '\uA7A4': 'N', + '\u01CA': 'NJ', + '\u01CB': 'Nj', + '\u24C4': 'O', + '\uFF2F': 'O', + '\u00D2': 'O', + '\u00D3': 'O', + '\u00D4': 'O', + '\u1ED2': 'O', + '\u1ED0': 'O', + '\u1ED6': 'O', + '\u1ED4': 'O', + '\u00D5': 'O', + '\u1E4C': 'O', + '\u022C': 'O', + '\u1E4E': 'O', + '\u014C': 'O', + '\u1E50': 'O', + '\u1E52': 'O', + '\u014E': 'O', + '\u022E': 'O', + '\u0230': 'O', + '\u00D6': 'O', + '\u022A': 'O', + '\u1ECE': 'O', + '\u0150': 'O', + '\u01D1': 'O', + '\u020C': 'O', + '\u020E': 'O', + '\u01A0': 'O', + '\u1EDC': 'O', + '\u1EDA': 'O', + '\u1EE0': 'O', + '\u1EDE': 'O', + '\u1EE2': 'O', + '\u1ECC': 'O', + '\u1ED8': 'O', + '\u01EA': 'O', + '\u01EC': 'O', + '\u00D8': 'O', + '\u01FE': 'O', + '\u0186': 'O', + '\u019F': 'O', + '\uA74A': 'O', + '\uA74C': 'O', + '\u0152': 'OE', + '\u01A2': 'OI', + '\uA74E': 'OO', + '\u0222': 'OU', + '\u24C5': 'P', + '\uFF30': 'P', + '\u1E54': 'P', + '\u1E56': 'P', + '\u01A4': 'P', + '\u2C63': 'P', + '\uA750': 'P', + '\uA752': 'P', + '\uA754': 'P', + '\u24C6': 'Q', + '\uFF31': 'Q', + '\uA756': 'Q', + '\uA758': 'Q', + '\u024A': 'Q', + '\u24C7': 'R', + '\uFF32': 'R', + '\u0154': 'R', + '\u1E58': 'R', + '\u0158': 'R', + '\u0210': 'R', + '\u0212': 'R', + '\u1E5A': 'R', + '\u1E5C': 'R', + '\u0156': 'R', + '\u1E5E': 'R', + '\u024C': 'R', + '\u2C64': 'R', + '\uA75A': 'R', + '\uA7A6': 'R', + '\uA782': 'R', + '\u24C8': 'S', + '\uFF33': 'S', + '\u1E9E': 'S', + '\u015A': 'S', + '\u1E64': 'S', + '\u015C': 'S', + '\u1E60': 'S', + '\u0160': 'S', + '\u1E66': 'S', + '\u1E62': 'S', + '\u1E68': 'S', + '\u0218': 'S', + '\u015E': 'S', + '\u2C7E': 'S', + '\uA7A8': 'S', + '\uA784': 'S', + '\u24C9': 'T', + '\uFF34': 'T', + '\u1E6A': 'T', + '\u0164': 'T', + '\u1E6C': 'T', + '\u021A': 'T', + '\u0162': 'T', + '\u1E70': 'T', + '\u1E6E': 'T', + '\u0166': 'T', + '\u01AC': 'T', + '\u01AE': 'T', + '\u023E': 'T', + '\uA786': 'T', + '\uA728': 'TZ', + '\u24CA': 'U', + '\uFF35': 'U', + '\u00D9': 'U', + '\u00DA': 'U', + '\u00DB': 'U', + '\u0168': 'U', + '\u1E78': 'U', + '\u016A': 'U', + '\u1E7A': 'U', + '\u016C': 'U', + '\u00DC': 'U', + '\u01DB': 'U', + '\u01D7': 'U', + '\u01D5': 'U', + '\u01D9': 'U', + '\u1EE6': 'U', + '\u016E': 'U', + '\u0170': 'U', + '\u01D3': 'U', + '\u0214': 'U', + '\u0216': 'U', + '\u01AF': 'U', + '\u1EEA': 'U', + '\u1EE8': 'U', + '\u1EEE': 'U', + '\u1EEC': 'U', + '\u1EF0': 'U', + '\u1EE4': 'U', + '\u1E72': 'U', + '\u0172': 'U', + '\u1E76': 'U', + '\u1E74': 'U', + '\u0244': 'U', + '\u24CB': 'V', + '\uFF36': 'V', + '\u1E7C': 'V', + '\u1E7E': 'V', + '\u01B2': 'V', + '\uA75E': 'V', + '\u0245': 'V', + '\uA760': 'VY', + '\u24CC': 'W', + '\uFF37': 'W', + '\u1E80': 'W', + '\u1E82': 'W', + '\u0174': 'W', + '\u1E86': 'W', + '\u1E84': 'W', + '\u1E88': 'W', + '\u2C72': 'W', + '\u24CD': 'X', + '\uFF38': 'X', + '\u1E8A': 'X', + '\u1E8C': 'X', + '\u24CE': 'Y', + '\uFF39': 'Y', + '\u1EF2': 'Y', + '\u00DD': 'Y', + '\u0176': 'Y', + '\u1EF8': 'Y', + '\u0232': 'Y', + '\u1E8E': 'Y', + '\u0178': 'Y', + '\u1EF6': 'Y', + '\u1EF4': 'Y', + '\u01B3': 'Y', + '\u024E': 'Y', + '\u1EFE': 'Y', + '\u24CF': 'Z', + '\uFF3A': 'Z', + '\u0179': 'Z', + '\u1E90': 'Z', + '\u017B': 'Z', + '\u017D': 'Z', + '\u1E92': 'Z', + '\u1E94': 'Z', + '\u01B5': 'Z', + '\u0224': 'Z', + '\u2C7F': 'Z', + '\u2C6B': 'Z', + '\uA762': 'Z', + '\u24D0': 'a', + '\uFF41': 'a', + '\u1E9A': 'a', + '\u00E0': 'a', + '\u00E1': 'a', + '\u00E2': 'a', + '\u1EA7': 'a', + '\u1EA5': 'a', + '\u1EAB': 'a', + '\u1EA9': 'a', + '\u00E3': 'a', + '\u0101': 'a', + '\u0103': 'a', + '\u1EB1': 'a', + '\u1EAF': 'a', + '\u1EB5': 'a', + '\u1EB3': 'a', + '\u0227': 'a', + '\u01E1': 'a', + '\u00E4': 'a', + '\u01DF': 'a', + '\u1EA3': 'a', + '\u00E5': 'a', + '\u01FB': 'a', + '\u01CE': 'a', + '\u0201': 'a', + '\u0203': 'a', + '\u1EA1': 'a', + '\u1EAD': 'a', + '\u1EB7': 'a', + '\u1E01': 'a', + '\u0105': 'a', + '\u2C65': 'a', + '\u0250': 'a', + '\uA733': 'aa', + '\u00E6': 'ae', + '\u01FD': 'ae', + '\u01E3': 'ae', + '\uA735': 'ao', + '\uA737': 'au', + '\uA739': 'av', + '\uA73B': 'av', + '\uA73D': 'ay', + '\u24D1': 'b', + '\uFF42': 'b', + '\u1E03': 'b', + '\u1E05': 'b', + '\u1E07': 'b', + '\u0180': 'b', + '\u0183': 'b', + '\u0253': 'b', + '\u24D2': 'c', + '\uFF43': 'c', + '\u0107': 'c', + '\u0109': 'c', + '\u010B': 'c', + '\u010D': 'c', + '\u00E7': 'c', + '\u1E09': 'c', + '\u0188': 'c', + '\u023C': 'c', + '\uA73F': 'c', + '\u2184': 'c', + '\u24D3': 'd', + '\uFF44': 'd', + '\u1E0B': 'd', + '\u010F': 'd', + '\u1E0D': 'd', + '\u1E11': 'd', + '\u1E13': 'd', + '\u1E0F': 'd', + '\u0111': 'd', + '\u018C': 'd', + '\u0256': 'd', + '\u0257': 'd', + '\uA77A': 'd', + '\u01F3': 'dz', + '\u01C6': 'dz', + '\u24D4': 'e', + '\uFF45': 'e', + '\u00E8': 'e', + '\u00E9': 'e', + '\u00EA': 'e', + '\u1EC1': 'e', + '\u1EBF': 'e', + '\u1EC5': 'e', + '\u1EC3': 'e', + '\u1EBD': 'e', + '\u0113': 'e', + '\u1E15': 'e', + '\u1E17': 'e', + '\u0115': 'e', + '\u0117': 'e', + '\u00EB': 'e', + '\u1EBB': 'e', + '\u011B': 'e', + '\u0205': 'e', + '\u0207': 'e', + '\u1EB9': 'e', + '\u1EC7': 'e', + '\u0229': 'e', + '\u1E1D': 'e', + '\u0119': 'e', + '\u1E19': 'e', + '\u1E1B': 'e', + '\u0247': 'e', + '\u025B': 'e', + '\u01DD': 'e', + '\u24D5': 'f', + '\uFF46': 'f', + '\u1E1F': 'f', + '\u0192': 'f', + '\uA77C': 'f', + '\u24D6': 'g', + '\uFF47': 'g', + '\u01F5': 'g', + '\u011D': 'g', + '\u1E21': 'g', + '\u011F': 'g', + '\u0121': 'g', + '\u01E7': 'g', + '\u0123': 'g', + '\u01E5': 'g', + '\u0260': 'g', + '\uA7A1': 'g', + '\u1D79': 'g', + '\uA77F': 'g', + '\u24D7': 'h', + '\uFF48': 'h', + '\u0125': 'h', + '\u1E23': 'h', + '\u1E27': 'h', + '\u021F': 'h', + '\u1E25': 'h', + '\u1E29': 'h', + '\u1E2B': 'h', + '\u1E96': 'h', + '\u0127': 'h', + '\u2C68': 'h', + '\u2C76': 'h', + '\u0265': 'h', + '\u0195': 'hv', + '\u24D8': 'i', + '\uFF49': 'i', + '\u00EC': 'i', + '\u00ED': 'i', + '\u00EE': 'i', + '\u0129': 'i', + '\u012B': 'i', + '\u012D': 'i', + '\u00EF': 'i', + '\u1E2F': 'i', + '\u1EC9': 'i', + '\u01D0': 'i', + '\u0209': 'i', + '\u020B': 'i', + '\u1ECB': 'i', + '\u012F': 'i', + '\u1E2D': 'i', + '\u0268': 'i', + '\u0131': 'i', + '\u24D9': 'j', + '\uFF4A': 'j', + '\u0135': 'j', + '\u01F0': 'j', + '\u0249': 'j', + '\u24DA': 'k', + '\uFF4B': 'k', + '\u1E31': 'k', + '\u01E9': 'k', + '\u1E33': 'k', + '\u0137': 'k', + '\u1E35': 'k', + '\u0199': 'k', + '\u2C6A': 'k', + '\uA741': 'k', + '\uA743': 'k', + '\uA745': 'k', + '\uA7A3': 'k', + '\u24DB': 'l', + '\uFF4C': 'l', + '\u0140': 'l', + '\u013A': 'l', + '\u013E': 'l', + '\u1E37': 'l', + '\u1E39': 'l', + '\u013C': 'l', + '\u1E3D': 'l', + '\u1E3B': 'l', + '\u017F': 'l', + '\u0142': 'l', + '\u019A': 'l', + '\u026B': 'l', + '\u2C61': 'l', + '\uA749': 'l', + '\uA781': 'l', + '\uA747': 'l', + '\u01C9': 'lj', + '\u24DC': 'm', + '\uFF4D': 'm', + '\u1E3F': 'm', + '\u1E41': 'm', + '\u1E43': 'm', + '\u0271': 'm', + '\u026F': 'm', + '\u24DD': 'n', + '\uFF4E': 'n', + '\u01F9': 'n', + '\u0144': 'n', + '\u00F1': 'n', + '\u1E45': 'n', + '\u0148': 'n', + '\u1E47': 'n', + '\u0146': 'n', + '\u1E4B': 'n', + '\u1E49': 'n', + '\u019E': 'n', + '\u0272': 'n', + '\u0149': 'n', + '\uA791': 'n', + '\uA7A5': 'n', + '\u01CC': 'nj', + '\u24DE': 'o', + '\uFF4F': 'o', + '\u00F2': 'o', + '\u00F3': 'o', + '\u00F4': 'o', + '\u1ED3': 'o', + '\u1ED1': 'o', + '\u1ED7': 'o', + '\u1ED5': 'o', + '\u00F5': 'o', + '\u1E4D': 'o', + '\u022D': 'o', + '\u1E4F': 'o', + '\u014D': 'o', + '\u1E51': 'o', + '\u1E53': 'o', + '\u014F': 'o', + '\u022F': 'o', + '\u0231': 'o', + '\u00F6': 'o', + '\u022B': 'o', + '\u1ECF': 'o', + '\u0151': 'o', + '\u01D2': 'o', + '\u020D': 'o', + '\u020F': 'o', + '\u01A1': 'o', + '\u1EDD': 'o', + '\u1EDB': 'o', + '\u1EE1': 'o', + '\u1EDF': 'o', + '\u1EE3': 'o', + '\u1ECD': 'o', + '\u1ED9': 'o', + '\u01EB': 'o', + '\u01ED': 'o', + '\u00F8': 'o', + '\u01FF': 'o', + '\u0254': 'o', + '\uA74B': 'o', + '\uA74D': 'o', + '\u0275': 'o', + '\u0153': 'oe', + '\u01A3': 'oi', + '\u0223': 'ou', + '\uA74F': 'oo', + '\u24DF': 'p', + '\uFF50': 'p', + '\u1E55': 'p', + '\u1E57': 'p', + '\u01A5': 'p', + '\u1D7D': 'p', + '\uA751': 'p', + '\uA753': 'p', + '\uA755': 'p', + '\u24E0': 'q', + '\uFF51': 'q', + '\u024B': 'q', + '\uA757': 'q', + '\uA759': 'q', + '\u24E1': 'r', + '\uFF52': 'r', + '\u0155': 'r', + '\u1E59': 'r', + '\u0159': 'r', + '\u0211': 'r', + '\u0213': 'r', + '\u1E5B': 'r', + '\u1E5D': 'r', + '\u0157': 'r', + '\u1E5F': 'r', + '\u024D': 'r', + '\u027D': 'r', + '\uA75B': 'r', + '\uA7A7': 'r', + '\uA783': 'r', + '\u24E2': 's', + '\uFF53': 's', + '\u00DF': 's', + '\u015B': 's', + '\u1E65': 's', + '\u015D': 's', + '\u1E61': 's', + '\u0161': 's', + '\u1E67': 's', + '\u1E63': 's', + '\u1E69': 's', + '\u0219': 's', + '\u015F': 's', + '\u023F': 's', + '\uA7A9': 's', + '\uA785': 's', + '\u1E9B': 's', + '\u24E3': 't', + '\uFF54': 't', + '\u1E6B': 't', + '\u1E97': 't', + '\u0165': 't', + '\u1E6D': 't', + '\u021B': 't', + '\u0163': 't', + '\u1E71': 't', + '\u1E6F': 't', + '\u0167': 't', + '\u01AD': 't', + '\u0288': 't', + '\u2C66': 't', + '\uA787': 't', + '\uA729': 'tz', + '\u24E4': 'u', + '\uFF55': 'u', + '\u00F9': 'u', + '\u00FA': 'u', + '\u00FB': 'u', + '\u0169': 'u', + '\u1E79': 'u', + '\u016B': 'u', + '\u1E7B': 'u', + '\u016D': 'u', + '\u00FC': 'u', + '\u01DC': 'u', + '\u01D8': 'u', + '\u01D6': 'u', + '\u01DA': 'u', + '\u1EE7': 'u', + '\u016F': 'u', + '\u0171': 'u', + '\u01D4': 'u', + '\u0215': 'u', + '\u0217': 'u', + '\u01B0': 'u', + '\u1EEB': 'u', + '\u1EE9': 'u', + '\u1EEF': 'u', + '\u1EED': 'u', + '\u1EF1': 'u', + '\u1EE5': 'u', + '\u1E73': 'u', + '\u0173': 'u', + '\u1E77': 'u', + '\u1E75': 'u', + '\u0289': 'u', + '\u24E5': 'v', + '\uFF56': 'v', + '\u1E7D': 'v', + '\u1E7F': 'v', + '\u028B': 'v', + '\uA75F': 'v', + '\u028C': 'v', + '\uA761': 'vy', + '\u24E6': 'w', + '\uFF57': 'w', + '\u1E81': 'w', + '\u1E83': 'w', + '\u0175': 'w', + '\u1E87': 'w', + '\u1E85': 'w', + '\u1E98': 'w', + '\u1E89': 'w', + '\u2C73': 'w', + '\u24E7': 'x', + '\uFF58': 'x', + '\u1E8B': 'x', + '\u1E8D': 'x', + '\u24E8': 'y', + '\uFF59': 'y', + '\u1EF3': 'y', + '\u00FD': 'y', + '\u0177': 'y', + '\u1EF9': 'y', + '\u0233': 'y', + '\u1E8F': 'y', + '\u00FF': 'y', + '\u1EF7': 'y', + '\u1E99': 'y', + '\u1EF5': 'y', + '\u01B4': 'y', + '\u024F': 'y', + '\u1EFF': 'y', + '\u24E9': 'z', + '\uFF5A': 'z', + '\u017A': 'z', + '\u1E91': 'z', + '\u017C': 'z', + '\u017E': 'z', + '\u1E93': 'z', + '\u1E95': 'z', + '\u01B6': 'z', + '\u0225': 'z', + '\u0240': 'z', + '\u2C6C': 'z', + '\uA763': 'z', + '\u0386': '\u0391', + '\u0388': '\u0395', + '\u0389': '\u0397', + '\u038A': '\u0399', + '\u03AA': '\u0399', + '\u038C': '\u039F', + '\u038E': '\u03A5', + '\u03AB': '\u03A5', + '\u038F': '\u03A9', + '\u03AC': '\u03B1', + '\u03AD': '\u03B5', + '\u03AE': '\u03B7', + '\u03AF': '\u03B9', + '\u03CA': '\u03B9', + '\u0390': '\u03B9', + '\u03CC': '\u03BF', + '\u03CD': '\u03C5', + '\u03CB': '\u03C5', + '\u03B0': '\u03C5', + '\u03CE': '\u03C9', + '\u03C2': '\u03C3', + '\u2019': '\'' + }; + + return diacritics; +}); + +S2.define('select2/data/base',[ + '../utils' +], function (Utils) { + function BaseAdapter ($element, options) { + BaseAdapter.__super__.constructor.call(this); + } + + Utils.Extend(BaseAdapter, Utils.Observable); + + BaseAdapter.prototype.current = function (callback) { + throw new Error('The `current` method must be defined in child classes.'); + }; + + BaseAdapter.prototype.query = function (params, callback) { + throw new Error('The `query` method must be defined in child classes.'); + }; + + BaseAdapter.prototype.bind = function (container, $container) { + // Can be implemented in subclasses + }; + + BaseAdapter.prototype.destroy = function () { + // Can be implemented in subclasses + }; + + BaseAdapter.prototype.generateResultId = function (container, data) { + var id = container.id + '-result-'; + + id += Utils.generateChars(4); + + if (data.id != null) { + id += '-' + data.id.toString(); + } else { + id += '-' + Utils.generateChars(4); + } + return id; + }; + + return BaseAdapter; +}); + +S2.define('select2/data/select',[ + './base', + '../utils', + 'jquery' +], function (BaseAdapter, Utils, $) { + function SelectAdapter ($element, options) { + this.$element = $element; + this.options = options; + + SelectAdapter.__super__.constructor.call(this); + } + + Utils.Extend(SelectAdapter, BaseAdapter); + + SelectAdapter.prototype.current = function (callback) { + var self = this; + + var data = Array.prototype.map.call( + this.$element[0].querySelectorAll(':checked'), + function (selectedElement) { + return self.item($(selectedElement)); + } + ); + + callback(data); + }; + + SelectAdapter.prototype.select = function (data) { + var self = this; + + data.selected = true; + + // If data.element is a DOM node, use it instead + if ( + data.element != null && data.element.tagName.toLowerCase() === 'option' + ) { + data.element.selected = true; + + this.$element.trigger('input').trigger('change'); + + return; + } + + if (this.$element.prop('multiple')) { + this.current(function (currentData) { + var val = []; + + data = [data]; + data.push.apply(data, currentData); + + for (var d = 0; d < data.length; d++) { + var id = data[d].id; + + if (val.indexOf(id) === -1) { + val.push(id); + } + } + + self.$element.val(val); + self.$element.trigger('input').trigger('change'); + }); + } else { + var val = data.id; + + this.$element.val(val); + this.$element.trigger('input').trigger('change'); + } + }; + + SelectAdapter.prototype.unselect = function (data) { + var self = this; + + if (!this.$element.prop('multiple')) { + return; + } + + data.selected = false; + + if ( + data.element != null && + data.element.tagName.toLowerCase() === 'option' + ) { + data.element.selected = false; + + this.$element.trigger('input').trigger('change'); + + return; + } + + this.current(function (currentData) { + var val = []; + + for (var d = 0; d < currentData.length; d++) { + var id = currentData[d].id; + + if (id !== data.id && val.indexOf(id) === -1) { + val.push(id); + } + } + + self.$element.val(val); + + self.$element.trigger('input').trigger('change'); + }); + }; + + SelectAdapter.prototype.bind = function (container, $container) { + var self = this; + + this.container = container; + + container.on('select', function (params) { + self.select(params.data); + }); + + container.on('unselect', function (params) { + self.unselect(params.data); + }); + }; + + SelectAdapter.prototype.destroy = function () { + // Remove anything added to child elements + this.$element.find('*').each(function () { + // Remove any custom data set by Select2 + Utils.RemoveData(this); + }); + }; + + SelectAdapter.prototype.query = function (params, callback) { + var data = []; + var self = this; + + var $options = this.$element.children(); + + $options.each(function () { + if ( + this.tagName.toLowerCase() !== 'option' && + this.tagName.toLowerCase() !== 'optgroup' + ) { + return; + } + + var $option = $(this); + + var option = self.item($option); + + var matches = self.matches(params, option); + + if (matches !== null) { + data.push(matches); + } + }); + + callback({ + results: data + }); + }; + + SelectAdapter.prototype.addOptions = function ($options) { + this.$element.append($options); + }; + + SelectAdapter.prototype.option = function (data) { + var option; + + if (data.children) { + option = document.createElement('optgroup'); + option.label = data.text; + } else { + option = document.createElement('option'); + + if (option.textContent !== undefined) { + option.textContent = data.text; + } else { + option.innerText = data.text; + } + } + + if (data.id !== undefined) { + option.value = data.id; + } + + if (data.disabled) { + option.disabled = true; + } + + if (data.selected) { + option.selected = true; + } + + if (data.title) { + option.title = data.title; + } + + var normalizedData = this._normalizeItem(data); + normalizedData.element = option; + + // Override the option's data with the combined data + Utils.StoreData(option, 'data', normalizedData); + + return $(option); + }; + + SelectAdapter.prototype.item = function ($option) { + var data = {}; + + data = Utils.GetData($option[0], 'data'); + + if (data != null) { + return data; + } + + var option = $option[0]; + + if (option.tagName.toLowerCase() === 'option') { + data = { + id: $option.val(), + text: $option.text(), + disabled: $option.prop('disabled'), + selected: $option.prop('selected'), + title: $option.prop('title') + }; + } else if (option.tagName.toLowerCase() === 'optgroup') { + data = { + text: $option.prop('label'), + children: [], + title: $option.prop('title') + }; + + var $children = $option.children('option'); + var children = []; + + for (var c = 0; c < $children.length; c++) { + var $child = $($children[c]); + + var child = this.item($child); + + children.push(child); + } + + data.children = children; + } + + data = this._normalizeItem(data); + data.element = $option[0]; + + Utils.StoreData($option[0], 'data', data); + + return data; + }; + + SelectAdapter.prototype._normalizeItem = function (item) { + if (item !== Object(item)) { + item = { + id: item, + text: item + }; + } + + item = $.extend({}, { + text: '' + }, item); + + var defaults = { + selected: false, + disabled: false + }; + + if (item.id != null) { + item.id = item.id.toString(); + } + + if (item.text != null) { + item.text = item.text.toString(); + } + + if (item._resultId == null && item.id && this.container != null) { + item._resultId = this.generateResultId(this.container, item); + } + + return $.extend({}, defaults, item); + }; + + SelectAdapter.prototype.matches = function (params, data) { + var matcher = this.options.get('matcher'); + + return matcher(params, data); + }; + + return SelectAdapter; +}); + +S2.define('select2/data/array',[ + './select', + '../utils', + 'jquery' +], function (SelectAdapter, Utils, $) { + function ArrayAdapter ($element, options) { + this._dataToConvert = options.get('data') || []; + + ArrayAdapter.__super__.constructor.call(this, $element, options); + } + + Utils.Extend(ArrayAdapter, SelectAdapter); + + ArrayAdapter.prototype.bind = function (container, $container) { + ArrayAdapter.__super__.bind.call(this, container, $container); + + this.addOptions(this.convertToOptions(this._dataToConvert)); + }; + + ArrayAdapter.prototype.select = function (data) { + var $option = this.$element.find('option').filter(function (i, elm) { + return elm.value == data.id.toString(); + }); + + if ($option.length === 0) { + $option = this.option(data); + + this.addOptions($option); + } + + ArrayAdapter.__super__.select.call(this, data); + }; + + ArrayAdapter.prototype.convertToOptions = function (data) { + var self = this; + + var $existing = this.$element.find('option'); + var existingIds = $existing.map(function () { + return self.item($(this)).id; + }).get(); + + var $options = []; + + // Filter out all items except for the one passed in the argument + function onlyItem (item) { + return function () { + return $(this).val() == item.id; + }; + } + + for (var d = 0; d < data.length; d++) { + var item = this._normalizeItem(data[d]); + + // Skip items which were pre-loaded, only merge the data + if (existingIds.indexOf(item.id) >= 0) { + var $existingOption = $existing.filter(onlyItem(item)); + + var existingData = this.item($existingOption); + var newData = $.extend(true, {}, item, existingData); + + var $newOption = this.option(newData); + + $existingOption.replaceWith($newOption); + + continue; + } + + var $option = this.option(item); + + if (item.children) { + var $children = this.convertToOptions(item.children); + + $option.append($children); + } + + $options.push($option); + } + + return $options; + }; + + return ArrayAdapter; +}); + +S2.define('select2/data/ajax',[ + './array', + '../utils', + 'jquery' +], function (ArrayAdapter, Utils, $) { + function AjaxAdapter ($element, options) { + this.ajaxOptions = this._applyDefaults(options.get('ajax')); + + if (this.ajaxOptions.processResults != null) { + this.processResults = this.ajaxOptions.processResults; + } + + AjaxAdapter.__super__.constructor.call(this, $element, options); + } + + Utils.Extend(AjaxAdapter, ArrayAdapter); + + AjaxAdapter.prototype._applyDefaults = function (options) { + var defaults = { + data: function (params) { + return $.extend({}, params, { + q: params.term + }); + }, + transport: function (params, success, failure) { + var $request = $.ajax(params); + + $request.then(success); + $request.fail(failure); + + return $request; + } + }; + + return $.extend({}, defaults, options, true); + }; + + AjaxAdapter.prototype.processResults = function (results) { + return results; + }; + + AjaxAdapter.prototype.query = function (params, callback) { + var matches = []; + var self = this; + + if (this._request != null) { + // JSONP requests cannot always be aborted + if (typeof this._request.abort === 'function') { + this._request.abort(); + } + + this._request = null; + } + + var options = $.extend({ + type: 'GET' + }, this.ajaxOptions); + + if (typeof options.url === 'function') { + options.url = options.url.call(this.$element, params); + } + + if (typeof options.data === 'function') { + options.data = options.data.call(this.$element, params); + } + + function request () { + var $request = options.transport(options, function (data) { + var results = self.processResults(data, params); + + if (self.options.get('debug') && window.console && console.error) { + // Check to make sure that the response included a `results` key. + if (!results || !results.results || !Array.isArray(results.results)) { + console.error( + 'Select2: The AJAX results did not return an array in the ' + + '`results` key of the response.' + ); + } + } + + callback(results); + }, function () { + // Attempt to detect if a request was aborted + // Only works if the transport exposes a status property + if ('status' in $request && + ($request.status === 0 || $request.status === '0')) { + return; + } + + self.trigger('results:message', { + message: 'errorLoading' + }); + }); + + self._request = $request; + } + + if (this.ajaxOptions.delay && params.term != null) { + if (this._queryTimeout) { + window.clearTimeout(this._queryTimeout); + } + + this._queryTimeout = window.setTimeout(request, this.ajaxOptions.delay); + } else { + request(); + } + }; + + return AjaxAdapter; +}); + +S2.define('select2/data/tags',[ + 'jquery' +], function ($) { + function Tags (decorated, $element, options) { + var tags = options.get('tags'); + + var createTag = options.get('createTag'); + + if (createTag !== undefined) { + this.createTag = createTag; + } + + var insertTag = options.get('insertTag'); + + if (insertTag !== undefined) { + this.insertTag = insertTag; + } + + decorated.call(this, $element, options); + + if (Array.isArray(tags)) { + for (var t = 0; t < tags.length; t++) { + var tag = tags[t]; + var item = this._normalizeItem(tag); + + var $option = this.option(item); + + this.$element.append($option); + } + } + } + + Tags.prototype.query = function (decorated, params, callback) { + var self = this; + + this._removeOldTags(); + + if (params.term == null || params.page != null) { + decorated.call(this, params, callback); + return; + } + + function wrapper (obj, child) { + var data = obj.results; + + for (var i = 0; i < data.length; i++) { + var option = data[i]; + + var checkChildren = ( + option.children != null && + !wrapper({ + results: option.children + }, true) + ); + + var optionText = (option.text || '').toUpperCase(); + var paramsTerm = (params.term || '').toUpperCase(); + + var checkText = optionText === paramsTerm; + + if (checkText || checkChildren) { + if (child) { + return false; + } + + obj.data = data; + callback(obj); + + return; + } + } + + if (child) { + return true; + } + + var tag = self.createTag(params); + + if (tag != null) { + var $option = self.option(tag); + $option.attr('data-select2-tag', 'true'); + + self.addOptions([$option]); + + self.insertTag(data, tag); + } + + obj.results = data; + + callback(obj); + } + + decorated.call(this, params, wrapper); + }; + + Tags.prototype.createTag = function (decorated, params) { + if (params.term == null) { + return null; + } + + var term = params.term.trim(); + + if (term === '') { + return null; + } + + return { + id: term, + text: term + }; + }; + + Tags.prototype.insertTag = function (_, data, tag) { + data.unshift(tag); + }; + + Tags.prototype._removeOldTags = function (_) { + var $options = this.$element.find('option[data-select2-tag]'); + + $options.each(function () { + if (this.selected) { + return; + } + + $(this).remove(); + }); + }; + + return Tags; +}); + +S2.define('select2/data/tokenizer',[ + 'jquery' +], function ($) { + function Tokenizer (decorated, $element, options) { + var tokenizer = options.get('tokenizer'); + + if (tokenizer !== undefined) { + this.tokenizer = tokenizer; + } + + decorated.call(this, $element, options); + } + + Tokenizer.prototype.bind = function (decorated, container, $container) { + decorated.call(this, container, $container); + + this.$search = container.dropdown.$search || container.selection.$search || + $container.find('.select2-search__field'); + }; + + Tokenizer.prototype.query = function (decorated, params, callback) { + var self = this; + + function createAndSelect (data) { + // Normalize the data object so we can use it for checks + var item = self._normalizeItem(data); + + // Check if the data object already exists as a tag + // Select it if it doesn't + var $existingOptions = self.$element.find('option').filter(function () { + return $(this).val() === item.id; + }); + + // If an existing option wasn't found for it, create the option + if (!$existingOptions.length) { + var $option = self.option(item); + $option.attr('data-select2-tag', true); + + self._removeOldTags(); + self.addOptions([$option]); + } + + // Select the item, now that we know there is an option for it + select(item); + } + + function select (data) { + self.trigger('select', { + data: data + }); + } + + params.term = params.term || ''; + + var tokenData = this.tokenizer(params, this.options, createAndSelect); + + if (tokenData.term !== params.term) { + // Replace the search term if we have the search box + if (this.$search.length) { + this.$search.val(tokenData.term); + this.$search.trigger('focus'); + } + + params.term = tokenData.term; + } + + decorated.call(this, params, callback); + }; + + Tokenizer.prototype.tokenizer = function (_, params, options, callback) { + var separators = options.get('tokenSeparators') || []; + var term = params.term; + var i = 0; + + var createTag = this.createTag || function (params) { + return { + id: params.term, + text: params.term + }; + }; + + while (i < term.length) { + var termChar = term[i]; + + if (separators.indexOf(termChar) === -1) { + i++; + + continue; + } + + var part = term.substr(0, i); + var partParams = $.extend({}, params, { + term: part + }); + + var data = createTag(partParams); + + if (data == null) { + i++; + continue; + } + + callback(data); + + // Reset the term to not include the tokenized portion + term = term.substr(i + 1) || ''; + i = 0; + } + + return { + term: term + }; + }; + + return Tokenizer; +}); + +S2.define('select2/data/minimumInputLength',[ + +], function () { + function MinimumInputLength (decorated, $e, options) { + this.minimumInputLength = options.get('minimumInputLength'); + + decorated.call(this, $e, options); + } + + MinimumInputLength.prototype.query = function (decorated, params, callback) { + params.term = params.term || ''; + + if (params.term.length < this.minimumInputLength) { + this.trigger('results:message', { + message: 'inputTooShort', + args: { + minimum: this.minimumInputLength, + input: params.term, + params: params + } + }); + + return; + } + + decorated.call(this, params, callback); + }; + + return MinimumInputLength; +}); + +S2.define('select2/data/maximumInputLength',[ + +], function () { + function MaximumInputLength (decorated, $e, options) { + this.maximumInputLength = options.get('maximumInputLength'); + + decorated.call(this, $e, options); + } + + MaximumInputLength.prototype.query = function (decorated, params, callback) { + params.term = params.term || ''; + + if (this.maximumInputLength > 0 && + params.term.length > this.maximumInputLength) { + this.trigger('results:message', { + message: 'inputTooLong', + args: { + maximum: this.maximumInputLength, + input: params.term, + params: params + } + }); + + return; + } + + decorated.call(this, params, callback); + }; + + return MaximumInputLength; +}); + +S2.define('select2/data/maximumSelectionLength',[ + +], function (){ + function MaximumSelectionLength (decorated, $e, options) { + this.maximumSelectionLength = options.get('maximumSelectionLength'); + + decorated.call(this, $e, options); + } + + MaximumSelectionLength.prototype.bind = + function (decorated, container, $container) { + var self = this; + + decorated.call(this, container, $container); + + container.on('select', function () { + self._checkIfMaximumSelected(); + }); + }; + + MaximumSelectionLength.prototype.query = + function (decorated, params, callback) { + var self = this; + + this._checkIfMaximumSelected(function () { + decorated.call(self, params, callback); + }); + }; + + MaximumSelectionLength.prototype._checkIfMaximumSelected = + function (_, successCallback) { + var self = this; + + this.current(function (currentData) { + var count = currentData != null ? currentData.length : 0; + if (self.maximumSelectionLength > 0 && + count >= self.maximumSelectionLength) { + self.trigger('results:message', { + message: 'maximumSelected', + args: { + maximum: self.maximumSelectionLength + } + }); + return; + } + + if (successCallback) { + successCallback(); + } + }); + }; + + return MaximumSelectionLength; +}); + +S2.define('select2/dropdown',[ + 'jquery', + './utils' +], function ($, Utils) { + function Dropdown ($element, options) { + this.$element = $element; + this.options = options; + + Dropdown.__super__.constructor.call(this); + } + + Utils.Extend(Dropdown, Utils.Observable); + + Dropdown.prototype.render = function () { + var $dropdown = $( + '' + + '' + + '' + ); + + $dropdown.attr('dir', this.options.get('dir')); + + this.$dropdown = $dropdown; + + return $dropdown; + }; + + Dropdown.prototype.bind = function () { + // Should be implemented in subclasses + }; + + Dropdown.prototype.position = function ($dropdown, $container) { + // Should be implemented in subclasses + }; + + Dropdown.prototype.destroy = function () { + // Remove the dropdown from the DOM + this.$dropdown.remove(); + }; + + return Dropdown; +}); + +S2.define('select2/dropdown/search',[ + 'jquery' +], function ($) { + function Search () { } + + Search.prototype.render = function (decorated) { + var $rendered = decorated.call(this); + var searchLabel = this.options.get('translations').get('search'); + + var $search = $( + '' + + '' + + '' + ); + + this.$searchContainer = $search; + this.$search = $search.find('input'); + + this.$search.prop('autocomplete', this.options.get('autocomplete')); + this.$search.attr('aria-label', searchLabel()); + + $rendered.prepend($search); + + return $rendered; + }; + + Search.prototype.bind = function (decorated, container, $container) { + var self = this; + + var resultsId = container.id + '-results'; + + decorated.call(this, container, $container); + + this.$search.on('keydown', function (evt) { + self.trigger('keypress', evt); + + self._keyUpPrevented = evt.isDefaultPrevented(); + }); + + // Workaround for browsers which do not support the `input` event + // This will prevent double-triggering of events for browsers which support + // both the `keyup` and `input` events. + this.$search.on('input', function (evt) { + // Unbind the duplicated `keyup` event + $(this).off('keyup'); + }); + + this.$search.on('keyup input', function (evt) { + self.handleSearch(evt); + }); + + container.on('open', function () { + self.$search.attr('tabindex', 0); + self.$search.attr('aria-controls', resultsId); + + self.$search.trigger('focus'); + + window.setTimeout(function () { + self.$search.trigger('focus'); + }, 0); + }); + + container.on('close', function () { + self.$search.attr('tabindex', -1); + self.$search.removeAttr('aria-controls'); + self.$search.removeAttr('aria-activedescendant'); + + self.$search.val(''); + self.$search.trigger('blur'); + }); + + container.on('focus', function () { + if (!container.isOpen()) { + self.$search.trigger('focus'); + } + }); + + container.on('results:all', function (params) { + if (params.query.term == null || params.query.term === '') { + var showSearch = self.showSearch(params); + + if (showSearch) { + self.$searchContainer[0].classList.remove('select2-search--hide'); + } else { + self.$searchContainer[0].classList.add('select2-search--hide'); + } + } + }); + + container.on('results:focus', function (params) { + if (params.data._resultId) { + self.$search.attr('aria-activedescendant', params.data._resultId); + } else { + self.$search.removeAttr('aria-activedescendant'); + } + }); + }; + + Search.prototype.handleSearch = function (evt) { + if (!this._keyUpPrevented) { + var input = this.$search.val(); + + this.trigger('query', { + term: input + }); + } + + this._keyUpPrevented = false; + }; + + Search.prototype.showSearch = function (_, params) { + return true; + }; + + return Search; +}); + +S2.define('select2/dropdown/hidePlaceholder',[ + +], function () { + function HidePlaceholder (decorated, $element, options, dataAdapter) { + this.placeholder = this.normalizePlaceholder(options.get('placeholder')); + + decorated.call(this, $element, options, dataAdapter); + } + + HidePlaceholder.prototype.append = function (decorated, data) { + data.results = this.removePlaceholder(data.results); + + decorated.call(this, data); + }; + + HidePlaceholder.prototype.normalizePlaceholder = function (_, placeholder) { + if (typeof placeholder === 'string') { + placeholder = { + id: '', + text: placeholder + }; + } + + return placeholder; + }; + + HidePlaceholder.prototype.removePlaceholder = function (_, data) { + var modifiedData = data.slice(0); + + for (var d = data.length - 1; d >= 0; d--) { + var item = data[d]; + + if (this.placeholder.id === item.id) { + modifiedData.splice(d, 1); + } + } + + return modifiedData; + }; + + return HidePlaceholder; +}); + +S2.define('select2/dropdown/infiniteScroll',[ + 'jquery' +], function ($) { + function InfiniteScroll (decorated, $element, options, dataAdapter) { + this.lastParams = {}; + + decorated.call(this, $element, options, dataAdapter); + + this.$loadingMore = this.createLoadingMore(); + this.loading = false; + } + + InfiniteScroll.prototype.append = function (decorated, data) { + this.$loadingMore.remove(); + this.loading = false; + + decorated.call(this, data); + + if (this.showLoadingMore(data)) { + this.$results.append(this.$loadingMore); + this.loadMoreIfNeeded(); + } + }; + + InfiniteScroll.prototype.bind = function (decorated, container, $container) { + var self = this; + + decorated.call(this, container, $container); + + container.on('query', function (params) { + self.lastParams = params; + self.loading = true; + }); + + container.on('query:append', function (params) { + self.lastParams = params; + self.loading = true; + }); + + this.$results.on('scroll', this.loadMoreIfNeeded.bind(this)); + }; + + InfiniteScroll.prototype.loadMoreIfNeeded = function () { + var isLoadMoreVisible = $.contains( + document.documentElement, + this.$loadingMore[0] + ); + + if (this.loading || !isLoadMoreVisible) { + return; + } + + var currentOffset = this.$results.offset().top + + this.$results.outerHeight(false); + var loadingMoreOffset = this.$loadingMore.offset().top + + this.$loadingMore.outerHeight(false); + + if (currentOffset + 50 >= loadingMoreOffset) { + this.loadMore(); + } + }; + + InfiniteScroll.prototype.loadMore = function () { + this.loading = true; + + var params = $.extend({}, {page: 1}, this.lastParams); + + params.page++; + + this.trigger('query:append', params); + }; + + InfiniteScroll.prototype.showLoadingMore = function (_, data) { + return data.pagination && data.pagination.more; + }; + + InfiniteScroll.prototype.createLoadingMore = function () { + var $option = $( + '
        • ' + ); + + var message = this.options.get('translations').get('loadingMore'); + + $option.html(message(this.lastParams)); + + return $option; + }; + + return InfiniteScroll; +}); + +S2.define('select2/dropdown/attachBody',[ + 'jquery', + '../utils' +], function ($, Utils) { + function AttachBody (decorated, $element, options) { + this.$dropdownParent = $(options.get('dropdownParent') || document.body); + + decorated.call(this, $element, options); + } + + AttachBody.prototype.bind = function (decorated, container, $container) { + var self = this; + + decorated.call(this, container, $container); + + container.on('open', function () { + self._showDropdown(); + self._attachPositioningHandler(container); + + // Must bind after the results handlers to ensure correct sizing + self._bindContainerResultHandlers(container); + }); + + container.on('close', function () { + self._hideDropdown(); + self._detachPositioningHandler(container); + }); + + this.$dropdownContainer.on('mousedown', function (evt) { + evt.stopPropagation(); + }); + }; + + AttachBody.prototype.destroy = function (decorated) { + decorated.call(this); + + this.$dropdownContainer.remove(); + }; + + AttachBody.prototype.position = function (decorated, $dropdown, $container) { + // Clone all of the container classes + $dropdown.attr('class', $container.attr('class')); + + $dropdown[0].classList.remove('select2'); + $dropdown[0].classList.add('select2-container--open'); + + $dropdown.css({ + position: 'absolute', + top: -999999 + }); + + this.$container = $container; + }; + + AttachBody.prototype.render = function (decorated) { + var $container = $(''); + + var $dropdown = decorated.call(this); + $container.append($dropdown); + + this.$dropdownContainer = $container; + + return $container; + }; + + AttachBody.prototype._hideDropdown = function (decorated) { + this.$dropdownContainer.detach(); + }; + + AttachBody.prototype._bindContainerResultHandlers = + function (decorated, container) { + + // These should only be bound once + if (this._containerResultsHandlersBound) { + return; + } + + var self = this; + + container.on('results:all', function () { + self._positionDropdown(); + self._resizeDropdown(); + }); + + container.on('results:append', function () { + self._positionDropdown(); + self._resizeDropdown(); + }); + + container.on('results:message', function () { + self._positionDropdown(); + self._resizeDropdown(); + }); + + container.on('select', function () { + self._positionDropdown(); + self._resizeDropdown(); + }); + + container.on('unselect', function () { + self._positionDropdown(); + self._resizeDropdown(); + }); + + this._containerResultsHandlersBound = true; + }; + + AttachBody.prototype._attachPositioningHandler = + function (decorated, container) { + var self = this; + + var scrollEvent = 'scroll.select2.' + container.id; + var resizeEvent = 'resize.select2.' + container.id; + var orientationEvent = 'orientationchange.select2.' + container.id; + + var $watchers = this.$container.parents().filter(Utils.hasScroll); + $watchers.each(function () { + Utils.StoreData(this, 'select2-scroll-position', { + x: $(this).scrollLeft(), + y: $(this).scrollTop() + }); + }); + + $watchers.on(scrollEvent, function (ev) { + var position = Utils.GetData(this, 'select2-scroll-position'); + $(this).scrollTop(position.y); + }); + + $(window).on(scrollEvent + ' ' + resizeEvent + ' ' + orientationEvent, + function (e) { + self._positionDropdown(); + self._resizeDropdown(); + }); + }; + + AttachBody.prototype._detachPositioningHandler = + function (decorated, container) { + var scrollEvent = 'scroll.select2.' + container.id; + var resizeEvent = 'resize.select2.' + container.id; + var orientationEvent = 'orientationchange.select2.' + container.id; + + var $watchers = this.$container.parents().filter(Utils.hasScroll); + $watchers.off(scrollEvent); + + $(window).off(scrollEvent + ' ' + resizeEvent + ' ' + orientationEvent); + }; + + AttachBody.prototype._positionDropdown = function () { + var $window = $(window); + + var isCurrentlyAbove = this.$dropdown[0].classList + .contains('select2-dropdown--above'); + var isCurrentlyBelow = this.$dropdown[0].classList + .contains('select2-dropdown--below'); + + var newDirection = null; + + var offset = this.$container.offset(); + + offset.bottom = offset.top + this.$container.outerHeight(false); + + var container = { + height: this.$container.outerHeight(false) + }; + + container.top = offset.top; + container.bottom = offset.top + container.height; + + var dropdown = { + height: this.$dropdown.outerHeight(false) + }; + + var viewport = { + top: $window.scrollTop(), + bottom: $window.scrollTop() + $window.height() + }; + + var enoughRoomAbove = viewport.top < (offset.top - dropdown.height); + var enoughRoomBelow = viewport.bottom > (offset.bottom + dropdown.height); + + var css = { + left: offset.left, + top: container.bottom + }; + + // Determine what the parent element is to use for calculating the offset + var $offsetParent = this.$dropdownParent; + + // For statically positioned elements, we need to get the element + // that is determining the offset + if ($offsetParent.css('position') === 'static') { + $offsetParent = $offsetParent.offsetParent(); + } + + var parentOffset = { + top: 0, + left: 0 + }; + + if ( + $.contains(document.body, $offsetParent[0]) || + $offsetParent[0].isConnected + ) { + parentOffset = $offsetParent.offset(); + } + + css.top -= parentOffset.top; + css.left -= parentOffset.left; + + if (!isCurrentlyAbove && !isCurrentlyBelow) { + newDirection = 'below'; + } + + if (!enoughRoomBelow && enoughRoomAbove && !isCurrentlyAbove) { + newDirection = 'above'; + } else if (!enoughRoomAbove && enoughRoomBelow && isCurrentlyAbove) { + newDirection = 'below'; + } + + if (newDirection == 'above' || + (isCurrentlyAbove && newDirection !== 'below')) { + css.top = container.top - parentOffset.top - dropdown.height; + } + + if (newDirection != null) { + this.$dropdown[0].classList.remove('select2-dropdown--below'); + this.$dropdown[0].classList.remove('select2-dropdown--above'); + this.$dropdown[0].classList.add('select2-dropdown--' + newDirection); + + this.$container[0].classList.remove('select2-container--below'); + this.$container[0].classList.remove('select2-container--above'); + this.$container[0].classList.add('select2-container--' + newDirection); + } + + this.$dropdownContainer.css(css); + }; + + AttachBody.prototype._resizeDropdown = function () { + var css = { + width: this.$container.outerWidth(false) + 'px' + }; + + if (this.options.get('dropdownAutoWidth')) { + css.minWidth = css.width; + css.position = 'relative'; + css.width = 'auto'; + } + + this.$dropdown.css(css); + }; + + AttachBody.prototype._showDropdown = function (decorated) { + this.$dropdownContainer.appendTo(this.$dropdownParent); + + this._positionDropdown(); + this._resizeDropdown(); + }; + + return AttachBody; +}); + +S2.define('select2/dropdown/minimumResultsForSearch',[ + +], function () { + function countResults (data) { + var count = 0; + + for (var d = 0; d < data.length; d++) { + var item = data[d]; + + if (item.children) { + count += countResults(item.children); + } else { + count++; + } + } + + return count; + } + + function MinimumResultsForSearch (decorated, $element, options, dataAdapter) { + this.minimumResultsForSearch = options.get('minimumResultsForSearch'); + + if (this.minimumResultsForSearch < 0) { + this.minimumResultsForSearch = Infinity; + } + + decorated.call(this, $element, options, dataAdapter); + } + + MinimumResultsForSearch.prototype.showSearch = function (decorated, params) { + if (countResults(params.data.results) < this.minimumResultsForSearch) { + return false; + } + + return decorated.call(this, params); + }; + + return MinimumResultsForSearch; +}); + +S2.define('select2/dropdown/selectOnClose',[ + '../utils' +], function (Utils) { + function SelectOnClose () { } + + SelectOnClose.prototype.bind = function (decorated, container, $container) { + var self = this; + + decorated.call(this, container, $container); + + container.on('close', function (params) { + self._handleSelectOnClose(params); + }); + }; + + SelectOnClose.prototype._handleSelectOnClose = function (_, params) { + if (params && params.originalSelect2Event != null) { + var event = params.originalSelect2Event; + + // Don't select an item if the close event was triggered from a select or + // unselect event + if (event._type === 'select' || event._type === 'unselect') { + return; + } + } + + var $highlightedResults = this.getHighlightedResults(); + + // Only select highlighted results + if ($highlightedResults.length < 1) { + return; + } + + var data = Utils.GetData($highlightedResults[0], 'data'); + + // Don't re-select already selected resulte + if ( + (data.element != null && data.element.selected) || + (data.element == null && data.selected) + ) { + return; + } + + this.trigger('select', { + data: data + }); + }; + + return SelectOnClose; +}); + +S2.define('select2/dropdown/closeOnSelect',[ + +], function () { + function CloseOnSelect () { } + + CloseOnSelect.prototype.bind = function (decorated, container, $container) { + var self = this; + + decorated.call(this, container, $container); + + container.on('select', function (evt) { + self._selectTriggered(evt); + }); + + container.on('unselect', function (evt) { + self._selectTriggered(evt); + }); + }; + + CloseOnSelect.prototype._selectTriggered = function (_, evt) { + var originalEvent = evt.originalEvent; + + // Don't close if the control key is being held + if (originalEvent && (originalEvent.ctrlKey || originalEvent.metaKey)) { + return; + } + + this.trigger('close', { + originalEvent: originalEvent, + originalSelect2Event: evt + }); + }; + + return CloseOnSelect; +}); + +S2.define('select2/dropdown/dropdownCss',[ + '../utils' +], function (Utils) { + function DropdownCSS () { } + + DropdownCSS.prototype.render = function (decorated) { + var $dropdown = decorated.call(this); + + var dropdownCssClass = this.options.get('dropdownCssClass') || ''; + + if (dropdownCssClass.indexOf(':all:') !== -1) { + dropdownCssClass = dropdownCssClass.replace(':all:', ''); + + Utils.copyNonInternalCssClasses($dropdown[0], this.$element[0]); + } + + $dropdown.addClass(dropdownCssClass); + + return $dropdown; + }; + + return DropdownCSS; +}); + +S2.define('select2/dropdown/tagsSearchHighlight',[ + '../utils' +], function (Utils) { + function TagsSearchHighlight () { } + + TagsSearchHighlight.prototype.highlightFirstItem = function (decorated) { + var $options = this.$results + .find( + '.select2-results__option--selectable' + + ':not(.select2-results__option--selected)' + ); + + if ($options.length > 0) { + var $firstOption = $options.first(); + var data = Utils.GetData($firstOption[0], 'data'); + var firstElement = data.element; + + if (firstElement && firstElement.getAttribute) { + if (firstElement.getAttribute('data-select2-tag') === 'true') { + $firstOption.trigger('mouseenter'); + + return; + } + } + } + + decorated.call(this); + }; + + return TagsSearchHighlight; +}); + +S2.define('select2/i18n/en',[],function () { + // English + return { + errorLoading: function () { + return 'The results could not be loaded.'; + }, + inputTooLong: function (args) { + var overChars = args.input.length - args.maximum; + + var message = 'Please delete ' + overChars + ' character'; + + if (overChars != 1) { + message += 's'; + } + + return message; + }, + inputTooShort: function (args) { + var remainingChars = args.minimum - args.input.length; + + var message = 'Please enter ' + remainingChars + ' or more characters'; + + return message; + }, + loadingMore: function () { + return 'Loading more results…'; + }, + maximumSelected: function (args) { + var message = 'You can only select ' + args.maximum + ' item'; + + if (args.maximum != 1) { + message += 's'; + } + + return message; + }, + noResults: function () { + return 'No results found'; + }, + searching: function () { + return 'Searching…'; + }, + removeAllItems: function () { + return 'Remove all items'; + }, + removeItem: function () { + return 'Remove item'; + }, + search: function() { + return 'Search'; + } + }; +}); + +S2.define('select2/defaults',[ + 'jquery', + + './results', + + './selection/single', + './selection/multiple', + './selection/placeholder', + './selection/allowClear', + './selection/search', + './selection/selectionCss', + './selection/eventRelay', + + './utils', + './translation', + './diacritics', + + './data/select', + './data/array', + './data/ajax', + './data/tags', + './data/tokenizer', + './data/minimumInputLength', + './data/maximumInputLength', + './data/maximumSelectionLength', + + './dropdown', + './dropdown/search', + './dropdown/hidePlaceholder', + './dropdown/infiniteScroll', + './dropdown/attachBody', + './dropdown/minimumResultsForSearch', + './dropdown/selectOnClose', + './dropdown/closeOnSelect', + './dropdown/dropdownCss', + './dropdown/tagsSearchHighlight', + + './i18n/en' +], function ($, + + ResultsList, + + SingleSelection, MultipleSelection, Placeholder, AllowClear, + SelectionSearch, SelectionCSS, EventRelay, + + Utils, Translation, DIACRITICS, + + SelectData, ArrayData, AjaxData, Tags, Tokenizer, + MinimumInputLength, MaximumInputLength, MaximumSelectionLength, + + Dropdown, DropdownSearch, HidePlaceholder, InfiniteScroll, + AttachBody, MinimumResultsForSearch, SelectOnClose, CloseOnSelect, + DropdownCSS, TagsSearchHighlight, + + EnglishTranslation) { + function Defaults () { + this.reset(); + } + + Defaults.prototype.apply = function (options) { + options = $.extend(true, {}, this.defaults, options); + + if (options.dataAdapter == null) { + if (options.ajax != null) { + options.dataAdapter = AjaxData; + } else if (options.data != null) { + options.dataAdapter = ArrayData; + } else { + options.dataAdapter = SelectData; + } + + if (options.minimumInputLength > 0) { + options.dataAdapter = Utils.Decorate( + options.dataAdapter, + MinimumInputLength + ); + } + + if (options.maximumInputLength > 0) { + options.dataAdapter = Utils.Decorate( + options.dataAdapter, + MaximumInputLength + ); + } + + if (options.maximumSelectionLength > 0) { + options.dataAdapter = Utils.Decorate( + options.dataAdapter, + MaximumSelectionLength + ); + } + + if (options.tags) { + options.dataAdapter = Utils.Decorate(options.dataAdapter, Tags); + } + + if (options.tokenSeparators != null || options.tokenizer != null) { + options.dataAdapter = Utils.Decorate( + options.dataAdapter, + Tokenizer + ); + } + } + + if (options.resultsAdapter == null) { + options.resultsAdapter = ResultsList; + + if (options.ajax != null) { + options.resultsAdapter = Utils.Decorate( + options.resultsAdapter, + InfiniteScroll + ); + } + + if (options.placeholder != null) { + options.resultsAdapter = Utils.Decorate( + options.resultsAdapter, + HidePlaceholder + ); + } + + if (options.selectOnClose) { + options.resultsAdapter = Utils.Decorate( + options.resultsAdapter, + SelectOnClose + ); + } + + if (options.tags) { + options.resultsAdapter = Utils.Decorate( + options.resultsAdapter, + TagsSearchHighlight + ); + } + } + + if (options.dropdownAdapter == null) { + if (options.multiple) { + options.dropdownAdapter = Dropdown; + } else { + var SearchableDropdown = Utils.Decorate(Dropdown, DropdownSearch); + + options.dropdownAdapter = SearchableDropdown; + } + + if (options.minimumResultsForSearch !== 0) { + options.dropdownAdapter = Utils.Decorate( + options.dropdownAdapter, + MinimumResultsForSearch + ); + } + + if (options.closeOnSelect) { + options.dropdownAdapter = Utils.Decorate( + options.dropdownAdapter, + CloseOnSelect + ); + } + + if (options.dropdownCssClass != null) { + options.dropdownAdapter = Utils.Decorate( + options.dropdownAdapter, + DropdownCSS + ); + } + + options.dropdownAdapter = Utils.Decorate( + options.dropdownAdapter, + AttachBody + ); + } + + if (options.selectionAdapter == null) { + if (options.multiple) { + options.selectionAdapter = MultipleSelection; + } else { + options.selectionAdapter = SingleSelection; + } + + // Add the placeholder mixin if a placeholder was specified + if (options.placeholder != null) { + options.selectionAdapter = Utils.Decorate( + options.selectionAdapter, + Placeholder + ); + } + + if (options.allowClear) { + options.selectionAdapter = Utils.Decorate( + options.selectionAdapter, + AllowClear + ); + } + + if (options.multiple) { + options.selectionAdapter = Utils.Decorate( + options.selectionAdapter, + SelectionSearch + ); + } + + if (options.selectionCssClass != null) { + options.selectionAdapter = Utils.Decorate( + options.selectionAdapter, + SelectionCSS + ); + } + + options.selectionAdapter = Utils.Decorate( + options.selectionAdapter, + EventRelay + ); + } + + // If the defaults were not previously applied from an element, it is + // possible for the language option to have not been resolved + options.language = this._resolveLanguage(options.language); + + // Always fall back to English since it will always be complete + options.language.push('en'); + + var uniqueLanguages = []; + + for (var l = 0; l < options.language.length; l++) { + var language = options.language[l]; + + if (uniqueLanguages.indexOf(language) === -1) { + uniqueLanguages.push(language); + } + } + + options.language = uniqueLanguages; + + options.translations = this._processTranslations( + options.language, + options.debug + ); + + return options; + }; + + Defaults.prototype.reset = function () { + function stripDiacritics (text) { + // Used 'uni range + named function' from http://jsperf.com/diacritics/18 + function match(a) { + return DIACRITICS[a] || a; + } + + return text.replace(/[^\u0000-\u007E]/g, match); + } + + function matcher (params, data) { + // Always return the object if there is nothing to compare + if (params.term == null || params.term.trim() === '') { + return data; + } + + // Do a recursive check for options with children + if (data.children && data.children.length > 0) { + // Clone the data object if there are children + // This is required as we modify the object to remove any non-matches + var match = $.extend(true, {}, data); + + // Check each child of the option + for (var c = data.children.length - 1; c >= 0; c--) { + var child = data.children[c]; + + var matches = matcher(params, child); + + // If there wasn't a match, remove the object in the array + if (matches == null) { + match.children.splice(c, 1); + } + } + + // If any children matched, return the new object + if (match.children.length > 0) { + return match; + } + + // If there were no matching children, check just the plain object + return matcher(params, match); + } + + var original = stripDiacritics(data.text).toUpperCase(); + var term = stripDiacritics(params.term).toUpperCase(); + + // Check if the text contains the term + if (original.indexOf(term) > -1) { + return data; + } + + // If it doesn't contain the term, don't return anything + return null; + } + + this.defaults = { + amdLanguageBase: './i18n/', + autocomplete: 'off', + closeOnSelect: true, + debug: false, + dropdownAutoWidth: false, + escapeMarkup: Utils.escapeMarkup, + language: {}, + matcher: matcher, + minimumInputLength: 0, + maximumInputLength: 0, + maximumSelectionLength: 0, + minimumResultsForSearch: 0, + selectOnClose: false, + scrollAfterSelect: false, + sorter: function (data) { + return data; + }, + templateResult: function (result) { + return result.text; + }, + templateSelection: function (selection) { + return selection.text; + }, + theme: 'default', + width: 'resolve' + }; + }; + + Defaults.prototype.applyFromElement = function (options, $element) { + var optionLanguage = options.language; + var defaultLanguage = this.defaults.language; + var elementLanguage = $element.prop('lang'); + var parentLanguage = $element.closest('[lang]').prop('lang'); + + var languages = Array.prototype.concat.call( + this._resolveLanguage(elementLanguage), + this._resolveLanguage(optionLanguage), + this._resolveLanguage(defaultLanguage), + this._resolveLanguage(parentLanguage) + ); + + options.language = languages; + + return options; + }; + + Defaults.prototype._resolveLanguage = function (language) { + if (!language) { + return []; + } + + if ($.isEmptyObject(language)) { + return []; + } + + if ($.isPlainObject(language)) { + return [language]; + } + + var languages; + + if (!Array.isArray(language)) { + languages = [language]; + } else { + languages = language; + } + + var resolvedLanguages = []; + + for (var l = 0; l < languages.length; l++) { + resolvedLanguages.push(languages[l]); + + if (typeof languages[l] === 'string' && languages[l].indexOf('-') > 0) { + // Extract the region information if it is included + var languageParts = languages[l].split('-'); + var baseLanguage = languageParts[0]; + + resolvedLanguages.push(baseLanguage); + } + } + + return resolvedLanguages; + }; + + Defaults.prototype._processTranslations = function (languages, debug) { + var translations = new Translation(); + + for (var l = 0; l < languages.length; l++) { + var languageData = new Translation(); + + var language = languages[l]; + + if (typeof language === 'string') { + try { + // Try to load it with the original name + languageData = Translation.loadPath(language); + } catch (e) { + try { + // If we couldn't load it, check if it wasn't the full path + language = this.defaults.amdLanguageBase + language; + languageData = Translation.loadPath(language); + } catch (ex) { + // The translation could not be loaded at all. Sometimes this is + // because of a configuration problem, other times this can be + // because of how Select2 helps load all possible translation files + if (debug && window.console && console.warn) { + console.warn( + 'Select2: The language file for "' + language + '" could ' + + 'not be automatically loaded. A fallback will be used instead.' + ); + } + } + } + } else if ($.isPlainObject(language)) { + languageData = new Translation(language); + } else { + languageData = language; + } + + translations.extend(languageData); + } + + return translations; + }; + + Defaults.prototype.set = function (key, value) { + var camelKey = $.camelCase(key); + + var data = {}; + data[camelKey] = value; + + var convertedData = Utils._convertData(data); + + $.extend(true, this.defaults, convertedData); + }; + + var defaults = new Defaults(); + + return defaults; +}); + +S2.define('select2/options',[ + 'jquery', + './defaults', + './utils' +], function ($, Defaults, Utils) { + function Options (options, $element) { + this.options = options; + + if ($element != null) { + this.fromElement($element); + } + + if ($element != null) { + this.options = Defaults.applyFromElement(this.options, $element); + } + + this.options = Defaults.apply(this.options); + } + + Options.prototype.fromElement = function ($e) { + var excludedData = ['select2']; + + if (this.options.multiple == null) { + this.options.multiple = $e.prop('multiple'); + } + + if (this.options.disabled == null) { + this.options.disabled = $e.prop('disabled'); + } + + if (this.options.autocomplete == null && $e.prop('autocomplete')) { + this.options.autocomplete = $e.prop('autocomplete'); + } + + if (this.options.dir == null) { + if ($e.prop('dir')) { + this.options.dir = $e.prop('dir'); + } else if ($e.closest('[dir]').prop('dir')) { + this.options.dir = $e.closest('[dir]').prop('dir'); + } else { + this.options.dir = 'ltr'; + } + } + + $e.prop('disabled', this.options.disabled); + $e.prop('multiple', this.options.multiple); + + if (Utils.GetData($e[0], 'select2Tags')) { + if (this.options.debug && window.console && console.warn) { + console.warn( + 'Select2: The `data-select2-tags` attribute has been changed to ' + + 'use the `data-data` and `data-tags="true"` attributes and will be ' + + 'removed in future versions of Select2.' + ); + } + + Utils.StoreData($e[0], 'data', Utils.GetData($e[0], 'select2Tags')); + Utils.StoreData($e[0], 'tags', true); + } + + if (Utils.GetData($e[0], 'ajaxUrl')) { + if (this.options.debug && window.console && console.warn) { + console.warn( + 'Select2: The `data-ajax-url` attribute has been changed to ' + + '`data-ajax--url` and support for the old attribute will be removed' + + ' in future versions of Select2.' + ); + } + + $e.attr('ajax--url', Utils.GetData($e[0], 'ajaxUrl')); + Utils.StoreData($e[0], 'ajax-Url', Utils.GetData($e[0], 'ajaxUrl')); + } + + var dataset = {}; + + function upperCaseLetter(_, letter) { + return letter.toUpperCase(); + } + + // Pre-load all of the attributes which are prefixed with `data-` + for (var attr = 0; attr < $e[0].attributes.length; attr++) { + var attributeName = $e[0].attributes[attr].name; + var prefix = 'data-'; + + if (attributeName.substr(0, prefix.length) == prefix) { + // Get the contents of the attribute after `data-` + var dataName = attributeName.substring(prefix.length); + + // Get the data contents from the consistent source + // This is more than likely the jQuery data helper + var dataValue = Utils.GetData($e[0], dataName); + + // camelCase the attribute name to match the spec + var camelDataName = dataName.replace(/-([a-z])/g, upperCaseLetter); + + // Store the data attribute contents into the dataset since + dataset[camelDataName] = dataValue; + } + } + + // Prefer the element's `dataset` attribute if it exists + // jQuery 1.x does not correctly handle data attributes with multiple dashes + if ($.fn.jquery && $.fn.jquery.substr(0, 2) == '1.' && $e[0].dataset) { + dataset = $.extend(true, {}, $e[0].dataset, dataset); + } + + // Prefer our internal data cache if it exists + var data = $.extend(true, {}, Utils.GetData($e[0]), dataset); + + data = Utils._convertData(data); + + for (var key in data) { + if (excludedData.indexOf(key) > -1) { + continue; + } + + if ($.isPlainObject(this.options[key])) { + $.extend(this.options[key], data[key]); + } else { + this.options[key] = data[key]; + } + } + + return this; + }; + + Options.prototype.get = function (key) { + return this.options[key]; + }; + + Options.prototype.set = function (key, val) { + this.options[key] = val; + }; + + return Options; +}); + +S2.define('select2/core',[ + 'jquery', + './options', + './utils', + './keys' +], function ($, Options, Utils, KEYS) { + var Select2 = function ($element, options) { + if (Utils.GetData($element[0], 'select2') != null) { + Utils.GetData($element[0], 'select2').destroy(); + } + + this.$element = $element; + + this.id = this._generateId($element); + + options = options || {}; + + this.options = new Options(options, $element); + + Select2.__super__.constructor.call(this); + + // Set up the tabindex + + var tabindex = $element.attr('tabindex') || 0; + Utils.StoreData($element[0], 'old-tabindex', tabindex); + $element.attr('tabindex', '-1'); + + // Set up containers and adapters + + var DataAdapter = this.options.get('dataAdapter'); + this.dataAdapter = new DataAdapter($element, this.options); + + var $container = this.render(); + + this._placeContainer($container); + + var SelectionAdapter = this.options.get('selectionAdapter'); + this.selection = new SelectionAdapter($element, this.options); + this.$selection = this.selection.render(); + + this.selection.position(this.$selection, $container); + + var DropdownAdapter = this.options.get('dropdownAdapter'); + this.dropdown = new DropdownAdapter($element, this.options); + this.$dropdown = this.dropdown.render(); + + this.dropdown.position(this.$dropdown, $container); + + var ResultsAdapter = this.options.get('resultsAdapter'); + this.results = new ResultsAdapter($element, this.options, this.dataAdapter); + this.$results = this.results.render(); + + this.results.position(this.$results, this.$dropdown); + + // Bind events + + var self = this; + + // Bind the container to all of the adapters + this._bindAdapters(); + + // Register any DOM event handlers + this._registerDomEvents(); + + // Register any internal event handlers + this._registerDataEvents(); + this._registerSelectionEvents(); + this._registerDropdownEvents(); + this._registerResultsEvents(); + this._registerEvents(); + + // Set the initial state + this.dataAdapter.current(function (initialData) { + self.trigger('selection:update', { + data: initialData + }); + }); + + // Hide the original select + $element[0].classList.add('select2-hidden-accessible'); + $element.attr('aria-hidden', 'true'); + + // Synchronize any monitored attributes + this._syncAttributes(); + + Utils.StoreData($element[0], 'select2', this); + + // Ensure backwards compatibility with $element.data('select2'). + $element.data('select2', this); + }; + + Utils.Extend(Select2, Utils.Observable); + + Select2.prototype._generateId = function ($element) { + var id = ''; + + if ($element.attr('id') != null) { + id = $element.attr('id'); + } else if ($element.attr('name') != null) { + id = $element.attr('name') + '-' + Utils.generateChars(2); + } else { + id = Utils.generateChars(4); + } + + id = id.replace(/(:|\.|\[|\]|,)/g, ''); + id = 'select2-' + id; + + return id; + }; + + Select2.prototype._placeContainer = function ($container) { + $container.insertAfter(this.$element); + + var width = this._resolveWidth(this.$element, this.options.get('width')); + + if (width != null) { + $container.css('width', width); + } + }; + + Select2.prototype._resolveWidth = function ($element, method) { + var WIDTH = /^width:(([-+]?([0-9]*\.)?[0-9]+)(px|em|ex|%|in|cm|mm|pt|pc))/i; + + if (method == 'resolve') { + var styleWidth = this._resolveWidth($element, 'style'); + + if (styleWidth != null) { + return styleWidth; + } + + return this._resolveWidth($element, 'element'); + } + + if (method == 'element') { + var elementWidth = $element.outerWidth(false); + + if (elementWidth <= 0) { + return 'auto'; + } + + return elementWidth + 'px'; + } + + if (method == 'style') { + var style = $element.attr('style'); + + if (typeof(style) !== 'string') { + return null; + } + + var attrs = style.split(';'); + + for (var i = 0, l = attrs.length; i < l; i = i + 1) { + var attr = attrs[i].replace(/\s/g, ''); + var matches = attr.match(WIDTH); + + if (matches !== null && matches.length >= 1) { + return matches[1]; + } + } + + return null; + } + + if (method == 'computedstyle') { + var computedStyle = window.getComputedStyle($element[0]); + + return computedStyle.width; + } + + return method; + }; + + Select2.prototype._bindAdapters = function () { + this.dataAdapter.bind(this, this.$container); + this.selection.bind(this, this.$container); + + this.dropdown.bind(this, this.$container); + this.results.bind(this, this.$container); + }; + + Select2.prototype._registerDomEvents = function () { + var self = this; + + this.$element.on('change.select2', function () { + self.dataAdapter.current(function (data) { + self.trigger('selection:update', { + data: data + }); + }); + }); + + this.$element.on('focus.select2', function (evt) { + self.trigger('focus', evt); + }); + + this._syncA = Utils.bind(this._syncAttributes, this); + this._syncS = Utils.bind(this._syncSubtree, this); + + this._observer = new window.MutationObserver(function (mutations) { + self._syncA(); + self._syncS(mutations); + }); + this._observer.observe(this.$element[0], { + attributes: true, + childList: true, + subtree: false + }); + }; + + Select2.prototype._registerDataEvents = function () { + var self = this; + + this.dataAdapter.on('*', function (name, params) { + self.trigger(name, params); + }); + }; + + Select2.prototype._registerSelectionEvents = function () { + var self = this; + var nonRelayEvents = ['toggle', 'focus']; + + this.selection.on('toggle', function () { + self.toggleDropdown(); + }); + + this.selection.on('focus', function (params) { + self.focus(params); + }); + + this.selection.on('*', function (name, params) { + if (nonRelayEvents.indexOf(name) !== -1) { + return; + } + + self.trigger(name, params); + }); + }; + + Select2.prototype._registerDropdownEvents = function () { + var self = this; + + this.dropdown.on('*', function (name, params) { + self.trigger(name, params); + }); + }; + + Select2.prototype._registerResultsEvents = function () { + var self = this; + + this.results.on('*', function (name, params) { + self.trigger(name, params); + }); + }; + + Select2.prototype._registerEvents = function () { + var self = this; + + this.on('open', function () { + self.$container[0].classList.add('select2-container--open'); + }); + + this.on('close', function () { + self.$container[0].classList.remove('select2-container--open'); + }); + + this.on('enable', function () { + self.$container[0].classList.remove('select2-container--disabled'); + }); + + this.on('disable', function () { + self.$container[0].classList.add('select2-container--disabled'); + }); + + this.on('blur', function () { + self.$container[0].classList.remove('select2-container--focus'); + }); + + this.on('query', function (params) { + if (!self.isOpen()) { + self.trigger('open', {}); + } + + this.dataAdapter.query(params, function (data) { + self.trigger('results:all', { + data: data, + query: params + }); + }); + }); + + this.on('query:append', function (params) { + this.dataAdapter.query(params, function (data) { + self.trigger('results:append', { + data: data, + query: params + }); + }); + }); + + this.on('keypress', function (evt) { + var key = evt.which; + + if (self.isOpen()) { + if (key === KEYS.ESC || (key === KEYS.UP && evt.altKey)) { + self.close(evt); + + evt.preventDefault(); + } else if (key === KEYS.ENTER || key === KEYS.TAB) { + self.trigger('results:select', {}); + + evt.preventDefault(); + } else if ((key === KEYS.SPACE && evt.ctrlKey)) { + self.trigger('results:toggle', {}); + + evt.preventDefault(); + } else if (key === KEYS.UP) { + self.trigger('results:previous', {}); + + evt.preventDefault(); + } else if (key === KEYS.DOWN) { + self.trigger('results:next', {}); + + evt.preventDefault(); + } + } else { + if (key === KEYS.ENTER || key === KEYS.SPACE || + (key === KEYS.DOWN && evt.altKey)) { + self.open(); + + evt.preventDefault(); + } + } + }); + }; + + Select2.prototype._syncAttributes = function () { + this.options.set('disabled', this.$element.prop('disabled')); + + if (this.isDisabled()) { + if (this.isOpen()) { + this.close(); + } + + this.trigger('disable', {}); + } else { + this.trigger('enable', {}); + } + }; + + Select2.prototype._isChangeMutation = function (mutations) { + var self = this; + + if (mutations.addedNodes && mutations.addedNodes.length > 0) { + for (var n = 0; n < mutations.addedNodes.length; n++) { + var node = mutations.addedNodes[n]; + + if (node.selected) { + return true; + } + } + } else if (mutations.removedNodes && mutations.removedNodes.length > 0) { + return true; + } else if (Array.isArray(mutations)) { + return mutations.some(function (mutation) { + return self._isChangeMutation(mutation); + }); + } + + return false; + }; + + Select2.prototype._syncSubtree = function (mutations) { + var changed = this._isChangeMutation(mutations); + var self = this; + + // Only re-pull the data if we think there is a change + if (changed) { + this.dataAdapter.current(function (currentData) { + self.trigger('selection:update', { + data: currentData + }); + }); + } + }; + + /** + * Override the trigger method to automatically trigger pre-events when + * there are events that can be prevented. + */ + Select2.prototype.trigger = function (name, args) { + var actualTrigger = Select2.__super__.trigger; + var preTriggerMap = { + 'open': 'opening', + 'close': 'closing', + 'select': 'selecting', + 'unselect': 'unselecting', + 'clear': 'clearing' + }; + + if (args === undefined) { + args = {}; + } + + if (name in preTriggerMap) { + var preTriggerName = preTriggerMap[name]; + var preTriggerArgs = { + prevented: false, + name: name, + args: args + }; + + actualTrigger.call(this, preTriggerName, preTriggerArgs); + + if (preTriggerArgs.prevented) { + args.prevented = true; + + return; + } + } + + actualTrigger.call(this, name, args); + }; + + Select2.prototype.toggleDropdown = function () { + if (this.isDisabled()) { + return; + } + + if (this.isOpen()) { + this.close(); + } else { + this.open(); + } + }; + + Select2.prototype.open = function () { + if (this.isOpen()) { + return; + } + + if (this.isDisabled()) { + return; + } + + this.trigger('query', {}); + }; + + Select2.prototype.close = function (evt) { + if (!this.isOpen()) { + return; + } + + this.trigger('close', { originalEvent : evt }); + }; + + /** + * Helper method to abstract the "enabled" (not "disabled") state of this + * object. + * + * @return {true} if the instance is not disabled. + * @return {false} if the instance is disabled. + */ + Select2.prototype.isEnabled = function () { + return !this.isDisabled(); + }; + + /** + * Helper method to abstract the "disabled" state of this object. + * + * @return {true} if the disabled option is true. + * @return {false} if the disabled option is false. + */ + Select2.prototype.isDisabled = function () { + return this.options.get('disabled'); + }; + + Select2.prototype.isOpen = function () { + return this.$container[0].classList.contains('select2-container--open'); + }; + + Select2.prototype.hasFocus = function () { + return this.$container[0].classList.contains('select2-container--focus'); + }; + + Select2.prototype.focus = function (data) { + // No need to re-trigger focus events if we are already focused + if (this.hasFocus()) { + return; + } + + this.$container[0].classList.add('select2-container--focus'); + this.trigger('focus', {}); + }; + + Select2.prototype.enable = function (args) { + if (this.options.get('debug') && window.console && console.warn) { + console.warn( + 'Select2: The `select2("enable")` method has been deprecated and will' + + ' be removed in later Select2 versions. Use $element.prop("disabled")' + + ' instead.' + ); + } + + if (args == null || args.length === 0) { + args = [true]; + } + + var disabled = !args[0]; + + this.$element.prop('disabled', disabled); + }; + + Select2.prototype.data = function () { + if (this.options.get('debug') && + arguments.length > 0 && window.console && console.warn) { + console.warn( + 'Select2: Data can no longer be set using `select2("data")`. You ' + + 'should consider setting the value instead using `$element.val()`.' + ); + } + + var data = []; + + this.dataAdapter.current(function (currentData) { + data = currentData; + }); + + return data; + }; + + Select2.prototype.val = function (args) { + if (this.options.get('debug') && window.console && console.warn) { + console.warn( + 'Select2: The `select2("val")` method has been deprecated and will be' + + ' removed in later Select2 versions. Use $element.val() instead.' + ); + } + + if (args == null || args.length === 0) { + return this.$element.val(); + } + + var newVal = args[0]; + + if (Array.isArray(newVal)) { + newVal = newVal.map(function (obj) { + return obj.toString(); + }); + } + + this.$element.val(newVal).trigger('input').trigger('change'); + }; + + Select2.prototype.destroy = function () { + Utils.RemoveData(this.$container[0]); + this.$container.remove(); + + this._observer.disconnect(); + this._observer = null; + + this._syncA = null; + this._syncS = null; + + this.$element.off('.select2'); + this.$element.attr('tabindex', + Utils.GetData(this.$element[0], 'old-tabindex')); + + this.$element[0].classList.remove('select2-hidden-accessible'); + this.$element.attr('aria-hidden', 'false'); + Utils.RemoveData(this.$element[0]); + this.$element.removeData('select2'); + + this.dataAdapter.destroy(); + this.selection.destroy(); + this.dropdown.destroy(); + this.results.destroy(); + + this.dataAdapter = null; + this.selection = null; + this.dropdown = null; + this.results = null; + }; + + Select2.prototype.render = function () { + var $container = $( + '' + + '' + + '' + + '' + ); + + $container.attr('dir', this.options.get('dir')); + + this.$container = $container; + + this.$container[0].classList + .add('select2-container--' + this.options.get('theme')); + + Utils.StoreData($container[0], 'element', this.$element); + + return $container; + }; + + return Select2; +}); + +S2.define('jquery-mousewheel',[ + 'jquery' +], function ($) { + // Used to shim jQuery.mousewheel for non-full builds. + return $; +}); + +S2.define('jquery.select2',[ + 'jquery', + 'jquery-mousewheel', + + './select2/core', + './select2/defaults', + './select2/utils' +], function ($, _, Select2, Defaults, Utils) { + if ($.fn.select2 == null) { + // All methods that should return the element + var thisMethods = ['open', 'close', 'destroy']; + + $.fn.select2 = function (options) { + options = options || {}; + + if (typeof options === 'object') { + this.each(function () { + var instanceOptions = $.extend(true, {}, options); + + var instance = new Select2($(this), instanceOptions); + }); + + return this; + } else if (typeof options === 'string') { + var ret; + var args = Array.prototype.slice.call(arguments, 1); + + this.each(function () { + var instance = Utils.GetData(this, 'select2'); + + if (instance == null && window.console && console.error) { + console.error( + 'The select2(\'' + options + '\') method was called on an ' + + 'element that is not using Select2.' + ); + } + + ret = instance[options].apply(instance, args); + }); + + // Check if we should be returning `this` + if (thisMethods.indexOf(options) > -1) { + return this; + } + + return ret; + } else { + throw new Error('Invalid arguments for Select2: ' + options); + } + }; + } + + if ($.fn.select2.defaults == null) { + $.fn.select2.defaults = Defaults; + } + + return Select2; +}); + + // Return the AMD loader configuration so it can be used outside of this file + return { + define: S2.define, + require: S2.require + }; +}()); + + // Autoload the jQuery bindings + // We know that all of the modules exist above this, so we're safe + var select2 = S2.require('jquery.select2'); + + // Hold the AMD module references on the jQuery function that was just loaded + // This allows Select2 to use the internal loader outside of this file, such + // as in the language files. + jQuery.fn.select2.amd = S2; + + // Return the Select2 instance for anyone who is importing it. + return select2; +})); diff --git a/static/tagulous/lib/select2-4/js/select2.min.js b/static/tagulous/lib/select2-4/js/select2.min.js new file mode 100644 index 00000000..c76b83e9 --- /dev/null +++ b/static/tagulous/lib/select2-4/js/select2.min.js @@ -0,0 +1,2 @@ +/*! Select2 4.1.0-rc.0 | https://github.com/select2/select2/blob/master/LICENSE.md */ +!function(n){"function"==typeof define&&define.amd?define(["jquery"],n):"object"==typeof module&&module.exports?module.exports=function(e,t){return void 0===t&&(t="undefined"!=typeof window?require("jquery"):require("jquery")(e)),n(t),t}:n(jQuery)}(function(t){var e,n,s,p,r,o,h,f,g,m,y,v,i,a,_,s=(t&&t.fn&&t.fn.select2&&t.fn.select2.amd&&(u=t.fn.select2.amd),u&&u.requirejs||(u?n=u:u={},g={},m={},y={},v={},i=Object.prototype.hasOwnProperty,a=[].slice,_=/\.js$/,h=function(e,t){var n,s,i=c(e),r=i[0],t=t[1];return e=i[1],r&&(n=x(r=l(r,t))),r?e=n&&n.normalize?n.normalize(e,(s=t,function(e){return l(e,s)})):l(e,t):(r=(i=c(e=l(e,t)))[0],e=i[1],r&&(n=x(r))),{f:r?r+"!"+e:e,n:e,pr:r,p:n}},f={require:function(e){return w(e)},exports:function(e){var t=g[e];return void 0!==t?t:g[e]={}},module:function(e){return{id:e,uri:"",exports:g[e],config:(t=e,function(){return y&&y.config&&y.config[t]||{}})};var t}},r=function(e,t,n,s){var i,r,o,a,l,c=[],u=typeof n,d=A(s=s||e);if("undefined"==u||"function"==u){for(t=!t.length&&n.length?["require","exports","module"]:t,a=0;a":">",'"':""","'":"'","/":"/"};return"string"!=typeof e?e:String(e).replace(/[&<>"'\/\\]/g,function(e){return t[e]})},s.__cache={};var n=0;return s.GetUniqueElementId=function(e){var t=e.getAttribute("data-select2-id");return null!=t||(t=e.id?"select2-data-"+e.id:"select2-data-"+(++n).toString()+"-"+s.generateChars(4),e.setAttribute("data-select2-id",t)),t},s.StoreData=function(e,t,n){e=s.GetUniqueElementId(e);s.__cache[e]||(s.__cache[e]={}),s.__cache[e][t]=n},s.GetData=function(e,t){var n=s.GetUniqueElementId(e);return t?s.__cache[n]&&null!=s.__cache[n][t]?s.__cache[n][t]:r(e).data(t):s.__cache[n]},s.RemoveData=function(e){var t=s.GetUniqueElementId(e);null!=s.__cache[t]&&delete s.__cache[t],e.removeAttribute("data-select2-id")},s.copyNonInternalCssClasses=function(e,t){var n=(n=e.getAttribute("class").trim().split(/\s+/)).filter(function(e){return 0===e.indexOf("select2-")}),t=(t=t.getAttribute("class").trim().split(/\s+/)).filter(function(e){return 0!==e.indexOf("select2-")}),t=n.concat(t);e.setAttribute("class",t.join(" "))},s}),u.define("select2/results",["jquery","./utils"],function(d,p){function s(e,t,n){this.$element=e,this.data=n,this.options=t,s.__super__.constructor.call(this)}return p.Extend(s,p.Observable),s.prototype.render=function(){var e=d('
            ');return this.options.get("multiple")&&e.attr("aria-multiselectable","true"),this.$results=e},s.prototype.clear=function(){this.$results.empty()},s.prototype.displayMessage=function(e){var t=this.options.get("escapeMarkup");this.clear(),this.hideLoading();var n=d(''),s=this.options.get("translations").get(e.message);n.append(t(s(e.args))),n[0].className+=" select2-results__message",this.$results.append(n)},s.prototype.hideMessages=function(){this.$results.find(".select2-results__message").remove()},s.prototype.append=function(e){this.hideLoading();var t=[];if(null!=e.results&&0!==e.results.length){e.results=this.sort(e.results);for(var n=0;n",{class:"select2-results__options select2-results__options--nested",role:"none"});i.append(l),o.append(a),o.append(i)}else this.template(e,t);return p.StoreData(t,"data",e),t},s.prototype.bind=function(t,e){var i=this,n=t.id+"-results";this.$results.attr("id",n),t.on("results:all",function(e){i.clear(),i.append(e.data),t.isOpen()&&(i.setClasses(),i.highlightFirstItem())}),t.on("results:append",function(e){i.append(e.data),t.isOpen()&&i.setClasses()}),t.on("query",function(e){i.hideMessages(),i.showLoading(e)}),t.on("select",function(){t.isOpen()&&(i.setClasses(),i.options.get("scrollAfterSelect")&&i.highlightFirstItem())}),t.on("unselect",function(){t.isOpen()&&(i.setClasses(),i.options.get("scrollAfterSelect")&&i.highlightFirstItem())}),t.on("open",function(){i.$results.attr("aria-expanded","true"),i.$results.attr("aria-hidden","false"),i.setClasses(),i.ensureHighlightVisible()}),t.on("close",function(){i.$results.attr("aria-expanded","false"),i.$results.attr("aria-hidden","true"),i.$results.removeAttr("aria-activedescendant")}),t.on("results:toggle",function(){var e=i.getHighlightedResults();0!==e.length&&e.trigger("mouseup")}),t.on("results:select",function(){var e,t=i.getHighlightedResults();0!==t.length&&(e=p.GetData(t[0],"data"),t.hasClass("select2-results__option--selected")?i.trigger("close",{}):i.trigger("select",{data:e}))}),t.on("results:previous",function(){var e,t=i.getHighlightedResults(),n=i.$results.find(".select2-results__option--selectable"),s=n.index(t);s<=0||(e=s-1,0===t.length&&(e=0),(s=n.eq(e)).trigger("mouseenter"),t=i.$results.offset().top,n=s.offset().top,s=i.$results.scrollTop()+(n-t),0===e?i.$results.scrollTop(0):n-t<0&&i.$results.scrollTop(s))}),t.on("results:next",function(){var e,t=i.getHighlightedResults(),n=i.$results.find(".select2-results__option--selectable"),s=n.index(t)+1;s>=n.length||((e=n.eq(s)).trigger("mouseenter"),t=i.$results.offset().top+i.$results.outerHeight(!1),n=e.offset().top+e.outerHeight(!1),e=i.$results.scrollTop()+n-t,0===s?i.$results.scrollTop(0):tthis.$results.outerHeight()||s<0)&&this.$results.scrollTop(n))},s.prototype.template=function(e,t){var n=this.options.get("templateResult"),s=this.options.get("escapeMarkup"),e=n(e,t);null==e?t.style.display="none":"string"==typeof e?t.innerHTML=s(e):d(t).append(e)},s}),u.define("select2/keys",[],function(){return{BACKSPACE:8,TAB:9,ENTER:13,SHIFT:16,CTRL:17,ALT:18,ESC:27,SPACE:32,PAGE_UP:33,PAGE_DOWN:34,END:35,HOME:36,LEFT:37,UP:38,RIGHT:39,DOWN:40,DELETE:46}}),u.define("select2/selection/base",["jquery","../utils","../keys"],function(n,s,i){function r(e,t){this.$element=e,this.options=t,r.__super__.constructor.call(this)}return s.Extend(r,s.Observable),r.prototype.render=function(){var e=n('');return this._tabindex=0,null!=s.GetData(this.$element[0],"old-tabindex")?this._tabindex=s.GetData(this.$element[0],"old-tabindex"):null!=this.$element.attr("tabindex")&&(this._tabindex=this.$element.attr("tabindex")),e.attr("title",this.$element.attr("title")),e.attr("tabindex",this._tabindex),e.attr("aria-disabled","false"),this.$selection=e},r.prototype.bind=function(e,t){var n=this,s=e.id+"-results";this.container=e,this.$selection.on("focus",function(e){n.trigger("focus",e)}),this.$selection.on("blur",function(e){n._handleBlur(e)}),this.$selection.on("keydown",function(e){n.trigger("keypress",e),e.which===i.SPACE&&e.preventDefault()}),e.on("results:focus",function(e){n.$selection.attr("aria-activedescendant",e.data._resultId)}),e.on("selection:update",function(e){n.update(e.data)}),e.on("open",function(){n.$selection.attr("aria-expanded","true"),n.$selection.attr("aria-owns",s),n._attachCloseHandler(e)}),e.on("close",function(){n.$selection.attr("aria-expanded","false"),n.$selection.removeAttr("aria-activedescendant"),n.$selection.removeAttr("aria-owns"),n.$selection.trigger("focus"),n._detachCloseHandler(e)}),e.on("enable",function(){n.$selection.attr("tabindex",n._tabindex),n.$selection.attr("aria-disabled","false")}),e.on("disable",function(){n.$selection.attr("tabindex","-1"),n.$selection.attr("aria-disabled","true")})},r.prototype._handleBlur=function(e){var t=this;window.setTimeout(function(){document.activeElement==t.$selection[0]||n.contains(t.$selection[0],document.activeElement)||t.trigger("blur",e)},1)},r.prototype._attachCloseHandler=function(e){n(document.body).on("mousedown.select2."+e.id,function(e){var t=n(e.target).closest(".select2");n(".select2.select2-container--open").each(function(){this!=t[0]&&s.GetData(this,"element").select2("close")})})},r.prototype._detachCloseHandler=function(e){n(document.body).off("mousedown.select2."+e.id)},r.prototype.position=function(e,t){t.find(".selection").append(e)},r.prototype.destroy=function(){this._detachCloseHandler(this.container)},r.prototype.update=function(e){throw new Error("The `update` method must be defined in child classes.")},r.prototype.isEnabled=function(){return!this.isDisabled()},r.prototype.isDisabled=function(){return this.options.get("disabled")},r}),u.define("select2/selection/single",["jquery","./base","../utils","../keys"],function(e,t,n,s){function i(){i.__super__.constructor.apply(this,arguments)}return n.Extend(i,t),i.prototype.render=function(){var e=i.__super__.render.call(this);return e[0].classList.add("select2-selection--single"),e.html(''),e},i.prototype.bind=function(t,e){var n=this;i.__super__.bind.apply(this,arguments);var s=t.id+"-container";this.$selection.find(".select2-selection__rendered").attr("id",s).attr("role","textbox").attr("aria-readonly","true"),this.$selection.attr("aria-labelledby",s),this.$selection.attr("aria-controls",s),this.$selection.on("mousedown",function(e){1===e.which&&n.trigger("toggle",{originalEvent:e})}),this.$selection.on("focus",function(e){}),this.$selection.on("blur",function(e){}),t.on("focus",function(e){t.isOpen()||n.$selection.trigger("focus")})},i.prototype.clear=function(){var e=this.$selection.find(".select2-selection__rendered");e.empty(),e.removeAttr("title")},i.prototype.display=function(e,t){var n=this.options.get("templateSelection");return this.options.get("escapeMarkup")(n(e,t))},i.prototype.selectionContainer=function(){return e("")},i.prototype.update=function(e){var t,n;0!==e.length?(n=e[0],t=this.$selection.find(".select2-selection__rendered"),e=this.display(n,t),t.empty().append(e),(n=n.title||n.text)?t.attr("title",n):t.removeAttr("title")):this.clear()},i}),u.define("select2/selection/multiple",["jquery","./base","../utils"],function(i,e,c){function r(e,t){r.__super__.constructor.apply(this,arguments)}return c.Extend(r,e),r.prototype.render=function(){var e=r.__super__.render.call(this);return e[0].classList.add("select2-selection--multiple"),e.html('
              '),e},r.prototype.bind=function(e,t){var n=this;r.__super__.bind.apply(this,arguments);var s=e.id+"-container";this.$selection.find(".select2-selection__rendered").attr("id",s),this.$selection.on("click",function(e){n.trigger("toggle",{originalEvent:e})}),this.$selection.on("click",".select2-selection__choice__remove",function(e){var t;n.isDisabled()||(t=i(this).parent(),t=c.GetData(t[0],"data"),n.trigger("unselect",{originalEvent:e,data:t}))}),this.$selection.on("keydown",".select2-selection__choice__remove",function(e){n.isDisabled()||e.stopPropagation()})},r.prototype.clear=function(){var e=this.$selection.find(".select2-selection__rendered");e.empty(),e.removeAttr("title")},r.prototype.display=function(e,t){var n=this.options.get("templateSelection");return this.options.get("escapeMarkup")(n(e,t))},r.prototype.selectionContainer=function(){return i('
            • ')},r.prototype.update=function(e){if(this.clear(),0!==e.length){for(var t=[],n=this.$selection.find(".select2-selection__rendered").attr("id")+"-choice-",s=0;s')).attr("title",s()),e.attr("aria-label",s()),e.attr("aria-describedby",n),a.StoreData(e[0],"data",t),this.$selection.prepend(e),this.$selection[0].classList.add("select2-selection--clearable"))},e}),u.define("select2/selection/search",["jquery","../utils","../keys"],function(s,a,l){function e(e,t,n){e.call(this,t,n)}return e.prototype.render=function(e){var t=this.options.get("translations").get("search"),n=s('');this.$searchContainer=n,this.$search=n.find("textarea"),this.$search.prop("autocomplete",this.options.get("autocomplete")),this.$search.attr("aria-label",t());e=e.call(this);return this._transferTabIndex(),e.append(this.$searchContainer),e},e.prototype.bind=function(e,t,n){var s=this,i=t.id+"-results",r=t.id+"-container";e.call(this,t,n),s.$search.attr("aria-describedby",r),t.on("open",function(){s.$search.attr("aria-controls",i),s.$search.trigger("focus")}),t.on("close",function(){s.$search.val(""),s.resizeSearch(),s.$search.removeAttr("aria-controls"),s.$search.removeAttr("aria-activedescendant"),s.$search.trigger("focus")}),t.on("enable",function(){s.$search.prop("disabled",!1),s._transferTabIndex()}),t.on("disable",function(){s.$search.prop("disabled",!0)}),t.on("focus",function(e){s.$search.trigger("focus")}),t.on("results:focus",function(e){e.data._resultId?s.$search.attr("aria-activedescendant",e.data._resultId):s.$search.removeAttr("aria-activedescendant")}),this.$selection.on("focusin",".select2-search--inline",function(e){s.trigger("focus",e)}),this.$selection.on("focusout",".select2-search--inline",function(e){s._handleBlur(e)}),this.$selection.on("keydown",".select2-search--inline",function(e){var t;e.stopPropagation(),s.trigger("keypress",e),s._keyUpPrevented=e.isDefaultPrevented(),e.which!==l.BACKSPACE||""!==s.$search.val()||0<(t=s.$selection.find(".select2-selection__choice").last()).length&&(t=a.GetData(t[0],"data"),s.searchRemoveChoice(t),e.preventDefault())}),this.$selection.on("click",".select2-search--inline",function(e){s.$search.val()&&e.stopPropagation()});var t=document.documentMode,o=t&&t<=11;this.$selection.on("input.searchcheck",".select2-search--inline",function(e){o?s.$selection.off("input.search input.searchcheck"):s.$selection.off("keyup.search")}),this.$selection.on("keyup.search input.search",".select2-search--inline",function(e){var t;o&&"input"===e.type?s.$selection.off("input.search input.searchcheck"):(t=e.which)!=l.SHIFT&&t!=l.CTRL&&t!=l.ALT&&t!=l.TAB&&s.handleSearch(e)})},e.prototype._transferTabIndex=function(e){this.$search.attr("tabindex",this.$selection.attr("tabindex")),this.$selection.attr("tabindex","-1")},e.prototype.createPlaceholder=function(e,t){this.$search.attr("placeholder",t.text)},e.prototype.update=function(e,t){var n=this.$search[0]==document.activeElement;this.$search.attr("placeholder",""),e.call(this,t),this.resizeSearch(),n&&this.$search.trigger("focus")},e.prototype.handleSearch=function(){var e;this.resizeSearch(),this._keyUpPrevented||(e=this.$search.val(),this.trigger("query",{term:e})),this._keyUpPrevented=!1},e.prototype.searchRemoveChoice=function(e,t){this.trigger("unselect",{data:t}),this.$search.val(t.text),this.handleSearch()},e.prototype.resizeSearch=function(){this.$search.css("width","25px");var e="100%";""===this.$search.attr("placeholder")&&(e=.75*(this.$search.val().length+1)+"em"),this.$search.css("width",e)},e}),u.define("select2/selection/selectionCss",["../utils"],function(n){function e(){}return e.prototype.render=function(e){var t=e.call(this),e=this.options.get("selectionCssClass")||"";return-1!==e.indexOf(":all:")&&(e=e.replace(":all:",""),n.copyNonInternalCssClasses(t[0],this.$element[0])),t.addClass(e),t},e}),u.define("select2/selection/eventRelay",["jquery"],function(o){function e(){}return e.prototype.bind=function(e,t,n){var s=this,i=["open","opening","close","closing","select","selecting","unselect","unselecting","clear","clearing"],r=["opening","closing","selecting","unselecting","clearing"];e.call(this,t,n),t.on("*",function(e,t){var n;-1!==i.indexOf(e)&&(t=t||{},n=o.Event("select2:"+e,{params:t}),s.$element.trigger(n),-1!==r.indexOf(e)&&(t.prevented=n.isDefaultPrevented()))})},e}),u.define("select2/translation",["jquery","require"],function(t,n){function s(e){this.dict=e||{}}return s.prototype.all=function(){return this.dict},s.prototype.get=function(e){return this.dict[e]},s.prototype.extend=function(e){this.dict=t.extend({},e.all(),this.dict)},s._cache={},s.loadPath=function(e){var t;return e in s._cache||(t=n(e),s._cache[e]=t),new s(s._cache[e])},s}),u.define("select2/diacritics",[],function(){return{"Ⓐ":"A","A":"A","À":"A","Á":"A","Â":"A","Ầ":"A","Ấ":"A","Ẫ":"A","Ẩ":"A","Ã":"A","Ā":"A","Ă":"A","Ằ":"A","Ắ":"A","Ẵ":"A","Ẳ":"A","Ȧ":"A","Ǡ":"A","Ä":"A","Ǟ":"A","Ả":"A","Å":"A","Ǻ":"A","Ǎ":"A","Ȁ":"A","Ȃ":"A","Ạ":"A","Ậ":"A","Ặ":"A","Ḁ":"A","Ą":"A","Ⱥ":"A","Ɐ":"A","Ꜳ":"AA","Æ":"AE","Ǽ":"AE","Ǣ":"AE","Ꜵ":"AO","Ꜷ":"AU","Ꜹ":"AV","Ꜻ":"AV","Ꜽ":"AY","Ⓑ":"B","B":"B","Ḃ":"B","Ḅ":"B","Ḇ":"B","Ƀ":"B","Ƃ":"B","Ɓ":"B","Ⓒ":"C","C":"C","Ć":"C","Ĉ":"C","Ċ":"C","Č":"C","Ç":"C","Ḉ":"C","Ƈ":"C","Ȼ":"C","Ꜿ":"C","Ⓓ":"D","D":"D","Ḋ":"D","Ď":"D","Ḍ":"D","Ḑ":"D","Ḓ":"D","Ḏ":"D","Đ":"D","Ƌ":"D","Ɗ":"D","Ɖ":"D","Ꝺ":"D","DZ":"DZ","DŽ":"DZ","Dz":"Dz","Dž":"Dz","Ⓔ":"E","E":"E","È":"E","É":"E","Ê":"E","Ề":"E","Ế":"E","Ễ":"E","Ể":"E","Ẽ":"E","Ē":"E","Ḕ":"E","Ḗ":"E","Ĕ":"E","Ė":"E","Ë":"E","Ẻ":"E","Ě":"E","Ȅ":"E","Ȇ":"E","Ẹ":"E","Ệ":"E","Ȩ":"E","Ḝ":"E","Ę":"E","Ḙ":"E","Ḛ":"E","Ɛ":"E","Ǝ":"E","Ⓕ":"F","F":"F","Ḟ":"F","Ƒ":"F","Ꝼ":"F","Ⓖ":"G","G":"G","Ǵ":"G","Ĝ":"G","Ḡ":"G","Ğ":"G","Ġ":"G","Ǧ":"G","Ģ":"G","Ǥ":"G","Ɠ":"G","Ꞡ":"G","Ᵹ":"G","Ꝿ":"G","Ⓗ":"H","H":"H","Ĥ":"H","Ḣ":"H","Ḧ":"H","Ȟ":"H","Ḥ":"H","Ḩ":"H","Ḫ":"H","Ħ":"H","Ⱨ":"H","Ⱶ":"H","Ɥ":"H","Ⓘ":"I","I":"I","Ì":"I","Í":"I","Î":"I","Ĩ":"I","Ī":"I","Ĭ":"I","İ":"I","Ï":"I","Ḯ":"I","Ỉ":"I","Ǐ":"I","Ȉ":"I","Ȋ":"I","Ị":"I","Į":"I","Ḭ":"I","Ɨ":"I","Ⓙ":"J","J":"J","Ĵ":"J","Ɉ":"J","Ⓚ":"K","K":"K","Ḱ":"K","Ǩ":"K","Ḳ":"K","Ķ":"K","Ḵ":"K","Ƙ":"K","Ⱪ":"K","Ꝁ":"K","Ꝃ":"K","Ꝅ":"K","Ꞣ":"K","Ⓛ":"L","L":"L","Ŀ":"L","Ĺ":"L","Ľ":"L","Ḷ":"L","Ḹ":"L","Ļ":"L","Ḽ":"L","Ḻ":"L","Ł":"L","Ƚ":"L","Ɫ":"L","Ⱡ":"L","Ꝉ":"L","Ꝇ":"L","Ꞁ":"L","LJ":"LJ","Lj":"Lj","Ⓜ":"M","M":"M","Ḿ":"M","Ṁ":"M","Ṃ":"M","Ɱ":"M","Ɯ":"M","Ⓝ":"N","N":"N","Ǹ":"N","Ń":"N","Ñ":"N","Ṅ":"N","Ň":"N","Ṇ":"N","Ņ":"N","Ṋ":"N","Ṉ":"N","Ƞ":"N","Ɲ":"N","Ꞑ":"N","Ꞥ":"N","NJ":"NJ","Nj":"Nj","Ⓞ":"O","O":"O","Ò":"O","Ó":"O","Ô":"O","Ồ":"O","Ố":"O","Ỗ":"O","Ổ":"O","Õ":"O","Ṍ":"O","Ȭ":"O","Ṏ":"O","Ō":"O","Ṑ":"O","Ṓ":"O","Ŏ":"O","Ȯ":"O","Ȱ":"O","Ö":"O","Ȫ":"O","Ỏ":"O","Ő":"O","Ǒ":"O","Ȍ":"O","Ȏ":"O","Ơ":"O","Ờ":"O","Ớ":"O","Ỡ":"O","Ở":"O","Ợ":"O","Ọ":"O","Ộ":"O","Ǫ":"O","Ǭ":"O","Ø":"O","Ǿ":"O","Ɔ":"O","Ɵ":"O","Ꝋ":"O","Ꝍ":"O","Œ":"OE","Ƣ":"OI","Ꝏ":"OO","Ȣ":"OU","Ⓟ":"P","P":"P","Ṕ":"P","Ṗ":"P","Ƥ":"P","Ᵽ":"P","Ꝑ":"P","Ꝓ":"P","Ꝕ":"P","Ⓠ":"Q","Q":"Q","Ꝗ":"Q","Ꝙ":"Q","Ɋ":"Q","Ⓡ":"R","R":"R","Ŕ":"R","Ṙ":"R","Ř":"R","Ȑ":"R","Ȓ":"R","Ṛ":"R","Ṝ":"R","Ŗ":"R","Ṟ":"R","Ɍ":"R","Ɽ":"R","Ꝛ":"R","Ꞧ":"R","Ꞃ":"R","Ⓢ":"S","S":"S","ẞ":"S","Ś":"S","Ṥ":"S","Ŝ":"S","Ṡ":"S","Š":"S","Ṧ":"S","Ṣ":"S","Ṩ":"S","Ș":"S","Ş":"S","Ȿ":"S","Ꞩ":"S","Ꞅ":"S","Ⓣ":"T","T":"T","Ṫ":"T","Ť":"T","Ṭ":"T","Ț":"T","Ţ":"T","Ṱ":"T","Ṯ":"T","Ŧ":"T","Ƭ":"T","Ʈ":"T","Ⱦ":"T","Ꞇ":"T","Ꜩ":"TZ","Ⓤ":"U","U":"U","Ù":"U","Ú":"U","Û":"U","Ũ":"U","Ṹ":"U","Ū":"U","Ṻ":"U","Ŭ":"U","Ü":"U","Ǜ":"U","Ǘ":"U","Ǖ":"U","Ǚ":"U","Ủ":"U","Ů":"U","Ű":"U","Ǔ":"U","Ȕ":"U","Ȗ":"U","Ư":"U","Ừ":"U","Ứ":"U","Ữ":"U","Ử":"U","Ự":"U","Ụ":"U","Ṳ":"U","Ų":"U","Ṷ":"U","Ṵ":"U","Ʉ":"U","Ⓥ":"V","V":"V","Ṽ":"V","Ṿ":"V","Ʋ":"V","Ꝟ":"V","Ʌ":"V","Ꝡ":"VY","Ⓦ":"W","W":"W","Ẁ":"W","Ẃ":"W","Ŵ":"W","Ẇ":"W","Ẅ":"W","Ẉ":"W","Ⱳ":"W","Ⓧ":"X","X":"X","Ẋ":"X","Ẍ":"X","Ⓨ":"Y","Y":"Y","Ỳ":"Y","Ý":"Y","Ŷ":"Y","Ỹ":"Y","Ȳ":"Y","Ẏ":"Y","Ÿ":"Y","Ỷ":"Y","Ỵ":"Y","Ƴ":"Y","Ɏ":"Y","Ỿ":"Y","Ⓩ":"Z","Z":"Z","Ź":"Z","Ẑ":"Z","Ż":"Z","Ž":"Z","Ẓ":"Z","Ẕ":"Z","Ƶ":"Z","Ȥ":"Z","Ɀ":"Z","Ⱬ":"Z","Ꝣ":"Z","ⓐ":"a","a":"a","ẚ":"a","à":"a","á":"a","â":"a","ầ":"a","ấ":"a","ẫ":"a","ẩ":"a","ã":"a","ā":"a","ă":"a","ằ":"a","ắ":"a","ẵ":"a","ẳ":"a","ȧ":"a","ǡ":"a","ä":"a","ǟ":"a","ả":"a","å":"a","ǻ":"a","ǎ":"a","ȁ":"a","ȃ":"a","ạ":"a","ậ":"a","ặ":"a","ḁ":"a","ą":"a","ⱥ":"a","ɐ":"a","ꜳ":"aa","æ":"ae","ǽ":"ae","ǣ":"ae","ꜵ":"ao","ꜷ":"au","ꜹ":"av","ꜻ":"av","ꜽ":"ay","ⓑ":"b","b":"b","ḃ":"b","ḅ":"b","ḇ":"b","ƀ":"b","ƃ":"b","ɓ":"b","ⓒ":"c","c":"c","ć":"c","ĉ":"c","ċ":"c","č":"c","ç":"c","ḉ":"c","ƈ":"c","ȼ":"c","ꜿ":"c","ↄ":"c","ⓓ":"d","d":"d","ḋ":"d","ď":"d","ḍ":"d","ḑ":"d","ḓ":"d","ḏ":"d","đ":"d","ƌ":"d","ɖ":"d","ɗ":"d","ꝺ":"d","dz":"dz","dž":"dz","ⓔ":"e","e":"e","è":"e","é":"e","ê":"e","ề":"e","ế":"e","ễ":"e","ể":"e","ẽ":"e","ē":"e","ḕ":"e","ḗ":"e","ĕ":"e","ė":"e","ë":"e","ẻ":"e","ě":"e","ȅ":"e","ȇ":"e","ẹ":"e","ệ":"e","ȩ":"e","ḝ":"e","ę":"e","ḙ":"e","ḛ":"e","ɇ":"e","ɛ":"e","ǝ":"e","ⓕ":"f","f":"f","ḟ":"f","ƒ":"f","ꝼ":"f","ⓖ":"g","g":"g","ǵ":"g","ĝ":"g","ḡ":"g","ğ":"g","ġ":"g","ǧ":"g","ģ":"g","ǥ":"g","ɠ":"g","ꞡ":"g","ᵹ":"g","ꝿ":"g","ⓗ":"h","h":"h","ĥ":"h","ḣ":"h","ḧ":"h","ȟ":"h","ḥ":"h","ḩ":"h","ḫ":"h","ẖ":"h","ħ":"h","ⱨ":"h","ⱶ":"h","ɥ":"h","ƕ":"hv","ⓘ":"i","i":"i","ì":"i","í":"i","î":"i","ĩ":"i","ī":"i","ĭ":"i","ï":"i","ḯ":"i","ỉ":"i","ǐ":"i","ȉ":"i","ȋ":"i","ị":"i","į":"i","ḭ":"i","ɨ":"i","ı":"i","ⓙ":"j","j":"j","ĵ":"j","ǰ":"j","ɉ":"j","ⓚ":"k","k":"k","ḱ":"k","ǩ":"k","ḳ":"k","ķ":"k","ḵ":"k","ƙ":"k","ⱪ":"k","ꝁ":"k","ꝃ":"k","ꝅ":"k","ꞣ":"k","ⓛ":"l","l":"l","ŀ":"l","ĺ":"l","ľ":"l","ḷ":"l","ḹ":"l","ļ":"l","ḽ":"l","ḻ":"l","ſ":"l","ł":"l","ƚ":"l","ɫ":"l","ⱡ":"l","ꝉ":"l","ꞁ":"l","ꝇ":"l","lj":"lj","ⓜ":"m","m":"m","ḿ":"m","ṁ":"m","ṃ":"m","ɱ":"m","ɯ":"m","ⓝ":"n","n":"n","ǹ":"n","ń":"n","ñ":"n","ṅ":"n","ň":"n","ṇ":"n","ņ":"n","ṋ":"n","ṉ":"n","ƞ":"n","ɲ":"n","ʼn":"n","ꞑ":"n","ꞥ":"n","nj":"nj","ⓞ":"o","o":"o","ò":"o","ó":"o","ô":"o","ồ":"o","ố":"o","ỗ":"o","ổ":"o","õ":"o","ṍ":"o","ȭ":"o","ṏ":"o","ō":"o","ṑ":"o","ṓ":"o","ŏ":"o","ȯ":"o","ȱ":"o","ö":"o","ȫ":"o","ỏ":"o","ő":"o","ǒ":"o","ȍ":"o","ȏ":"o","ơ":"o","ờ":"o","ớ":"o","ỡ":"o","ở":"o","ợ":"o","ọ":"o","ộ":"o","ǫ":"o","ǭ":"o","ø":"o","ǿ":"o","ɔ":"o","ꝋ":"o","ꝍ":"o","ɵ":"o","œ":"oe","ƣ":"oi","ȣ":"ou","ꝏ":"oo","ⓟ":"p","p":"p","ṕ":"p","ṗ":"p","ƥ":"p","ᵽ":"p","ꝑ":"p","ꝓ":"p","ꝕ":"p","ⓠ":"q","q":"q","ɋ":"q","ꝗ":"q","ꝙ":"q","ⓡ":"r","r":"r","ŕ":"r","ṙ":"r","ř":"r","ȑ":"r","ȓ":"r","ṛ":"r","ṝ":"r","ŗ":"r","ṟ":"r","ɍ":"r","ɽ":"r","ꝛ":"r","ꞧ":"r","ꞃ":"r","ⓢ":"s","s":"s","ß":"s","ś":"s","ṥ":"s","ŝ":"s","ṡ":"s","š":"s","ṧ":"s","ṣ":"s","ṩ":"s","ș":"s","ş":"s","ȿ":"s","ꞩ":"s","ꞅ":"s","ẛ":"s","ⓣ":"t","t":"t","ṫ":"t","ẗ":"t","ť":"t","ṭ":"t","ț":"t","ţ":"t","ṱ":"t","ṯ":"t","ŧ":"t","ƭ":"t","ʈ":"t","ⱦ":"t","ꞇ":"t","ꜩ":"tz","ⓤ":"u","u":"u","ù":"u","ú":"u","û":"u","ũ":"u","ṹ":"u","ū":"u","ṻ":"u","ŭ":"u","ü":"u","ǜ":"u","ǘ":"u","ǖ":"u","ǚ":"u","ủ":"u","ů":"u","ű":"u","ǔ":"u","ȕ":"u","ȗ":"u","ư":"u","ừ":"u","ứ":"u","ữ":"u","ử":"u","ự":"u","ụ":"u","ṳ":"u","ų":"u","ṷ":"u","ṵ":"u","ʉ":"u","ⓥ":"v","v":"v","ṽ":"v","ṿ":"v","ʋ":"v","ꝟ":"v","ʌ":"v","ꝡ":"vy","ⓦ":"w","w":"w","ẁ":"w","ẃ":"w","ŵ":"w","ẇ":"w","ẅ":"w","ẘ":"w","ẉ":"w","ⱳ":"w","ⓧ":"x","x":"x","ẋ":"x","ẍ":"x","ⓨ":"y","y":"y","ỳ":"y","ý":"y","ŷ":"y","ỹ":"y","ȳ":"y","ẏ":"y","ÿ":"y","ỷ":"y","ẙ":"y","ỵ":"y","ƴ":"y","ɏ":"y","ỿ":"y","ⓩ":"z","z":"z","ź":"z","ẑ":"z","ż":"z","ž":"z","ẓ":"z","ẕ":"z","ƶ":"z","ȥ":"z","ɀ":"z","ⱬ":"z","ꝣ":"z","Ά":"Α","Έ":"Ε","Ή":"Η","Ί":"Ι","Ϊ":"Ι","Ό":"Ο","Ύ":"Υ","Ϋ":"Υ","Ώ":"Ω","ά":"α","έ":"ε","ή":"η","ί":"ι","ϊ":"ι","ΐ":"ι","ό":"ο","ύ":"υ","ϋ":"υ","ΰ":"υ","ώ":"ω","ς":"σ","’":"'"}}),u.define("select2/data/base",["../utils"],function(n){function s(e,t){s.__super__.constructor.call(this)}return n.Extend(s,n.Observable),s.prototype.current=function(e){throw new Error("The `current` method must be defined in child classes.")},s.prototype.query=function(e,t){throw new Error("The `query` method must be defined in child classes.")},s.prototype.bind=function(e,t){},s.prototype.destroy=function(){},s.prototype.generateResultId=function(e,t){e=e.id+"-result-";return e+=n.generateChars(4),null!=t.id?e+="-"+t.id.toString():e+="-"+n.generateChars(4),e},s}),u.define("select2/data/select",["./base","../utils","jquery"],function(e,a,l){function n(e,t){this.$element=e,this.options=t,n.__super__.constructor.call(this)}return a.Extend(n,e),n.prototype.current=function(e){var t=this;e(Array.prototype.map.call(this.$element[0].querySelectorAll(":checked"),function(e){return t.item(l(e))}))},n.prototype.select=function(i){var e,r=this;if(i.selected=!0,null!=i.element&&"option"===i.element.tagName.toLowerCase())return i.element.selected=!0,void this.$element.trigger("input").trigger("change");this.$element.prop("multiple")?this.current(function(e){var t=[];(i=[i]).push.apply(i,e);for(var n=0;nthis.maximumInputLength?this.trigger("results:message",{message:"inputTooLong",args:{maximum:this.maximumInputLength,input:t.term,params:t}}):e.call(this,t,n)},e}),u.define("select2/data/maximumSelectionLength",[],function(){function e(e,t,n){this.maximumSelectionLength=n.get("maximumSelectionLength"),e.call(this,t,n)}return e.prototype.bind=function(e,t,n){var s=this;e.call(this,t,n),t.on("select",function(){s._checkIfMaximumSelected()})},e.prototype.query=function(e,t,n){var s=this;this._checkIfMaximumSelected(function(){e.call(s,t,n)})},e.prototype._checkIfMaximumSelected=function(e,t){var n=this;this.current(function(e){e=null!=e?e.length:0;0=n.maximumSelectionLength?n.trigger("results:message",{message:"maximumSelected",args:{maximum:n.maximumSelectionLength}}):t&&t()})},e}),u.define("select2/dropdown",["jquery","./utils"],function(t,e){function n(e,t){this.$element=e,this.options=t,n.__super__.constructor.call(this)}return e.Extend(n,e.Observable),n.prototype.render=function(){var e=t('');return e.attr("dir",this.options.get("dir")),this.$dropdown=e},n.prototype.bind=function(){},n.prototype.position=function(e,t){},n.prototype.destroy=function(){this.$dropdown.remove()},n}),u.define("select2/dropdown/search",["jquery"],function(r){function e(){}return e.prototype.render=function(e){var t=e.call(this),n=this.options.get("translations").get("search"),e=r('');return this.$searchContainer=e,this.$search=e.find("input"),this.$search.prop("autocomplete",this.options.get("autocomplete")),this.$search.attr("aria-label",n()),t.prepend(e),t},e.prototype.bind=function(e,t,n){var s=this,i=t.id+"-results";e.call(this,t,n),this.$search.on("keydown",function(e){s.trigger("keypress",e),s._keyUpPrevented=e.isDefaultPrevented()}),this.$search.on("input",function(e){r(this).off("keyup")}),this.$search.on("keyup input",function(e){s.handleSearch(e)}),t.on("open",function(){s.$search.attr("tabindex",0),s.$search.attr("aria-controls",i),s.$search.trigger("focus"),window.setTimeout(function(){s.$search.trigger("focus")},0)}),t.on("close",function(){s.$search.attr("tabindex",-1),s.$search.removeAttr("aria-controls"),s.$search.removeAttr("aria-activedescendant"),s.$search.val(""),s.$search.trigger("blur")}),t.on("focus",function(){t.isOpen()||s.$search.trigger("focus")}),t.on("results:all",function(e){null!=e.query.term&&""!==e.query.term||(s.showSearch(e)?s.$searchContainer[0].classList.remove("select2-search--hide"):s.$searchContainer[0].classList.add("select2-search--hide"))}),t.on("results:focus",function(e){e.data._resultId?s.$search.attr("aria-activedescendant",e.data._resultId):s.$search.removeAttr("aria-activedescendant")})},e.prototype.handleSearch=function(e){var t;this._keyUpPrevented||(t=this.$search.val(),this.trigger("query",{term:t})),this._keyUpPrevented=!1},e.prototype.showSearch=function(e,t){return!0},e}),u.define("select2/dropdown/hidePlaceholder",[],function(){function e(e,t,n,s){this.placeholder=this.normalizePlaceholder(n.get("placeholder")),e.call(this,t,n,s)}return e.prototype.append=function(e,t){t.results=this.removePlaceholder(t.results),e.call(this,t)},e.prototype.normalizePlaceholder=function(e,t){return"string"==typeof t&&(t={id:"",text:t}),t},e.prototype.removePlaceholder=function(e,t){for(var n=t.slice(0),s=t.length-1;0<=s;s--){var i=t[s];this.placeholder.id===i.id&&n.splice(s,1)}return n},e}),u.define("select2/dropdown/infiniteScroll",["jquery"],function(n){function e(e,t,n,s){this.lastParams={},e.call(this,t,n,s),this.$loadingMore=this.createLoadingMore(),this.loading=!1}return e.prototype.append=function(e,t){this.$loadingMore.remove(),this.loading=!1,e.call(this,t),this.showLoadingMore(t)&&(this.$results.append(this.$loadingMore),this.loadMoreIfNeeded())},e.prototype.bind=function(e,t,n){var s=this;e.call(this,t,n),t.on("query",function(e){s.lastParams=e,s.loading=!0}),t.on("query:append",function(e){s.lastParams=e,s.loading=!0}),this.$results.on("scroll",this.loadMoreIfNeeded.bind(this))},e.prototype.loadMoreIfNeeded=function(){var e=n.contains(document.documentElement,this.$loadingMore[0]);!this.loading&&e&&(e=this.$results.offset().top+this.$results.outerHeight(!1),this.$loadingMore.offset().top+this.$loadingMore.outerHeight(!1)<=e+50&&this.loadMore())},e.prototype.loadMore=function(){this.loading=!0;var e=n.extend({},{page:1},this.lastParams);e.page++,this.trigger("query:append",e)},e.prototype.showLoadingMore=function(e,t){return t.pagination&&t.pagination.more},e.prototype.createLoadingMore=function(){var e=n('
            • '),t=this.options.get("translations").get("loadingMore");return e.html(t(this.lastParams)),e},e}),u.define("select2/dropdown/attachBody",["jquery","../utils"],function(u,o){function e(e,t,n){this.$dropdownParent=u(n.get("dropdownParent")||document.body),e.call(this,t,n)}return e.prototype.bind=function(e,t,n){var s=this;e.call(this,t,n),t.on("open",function(){s._showDropdown(),s._attachPositioningHandler(t),s._bindContainerResultHandlers(t)}),t.on("close",function(){s._hideDropdown(),s._detachPositioningHandler(t)}),this.$dropdownContainer.on("mousedown",function(e){e.stopPropagation()})},e.prototype.destroy=function(e){e.call(this),this.$dropdownContainer.remove()},e.prototype.position=function(e,t,n){t.attr("class",n.attr("class")),t[0].classList.remove("select2"),t[0].classList.add("select2-container--open"),t.css({position:"absolute",top:-999999}),this.$container=n},e.prototype.render=function(e){var t=u(""),e=e.call(this);return t.append(e),this.$dropdownContainer=t},e.prototype._hideDropdown=function(e){this.$dropdownContainer.detach()},e.prototype._bindContainerResultHandlers=function(e,t){var n;this._containerResultsHandlersBound||(n=this,t.on("results:all",function(){n._positionDropdown(),n._resizeDropdown()}),t.on("results:append",function(){n._positionDropdown(),n._resizeDropdown()}),t.on("results:message",function(){n._positionDropdown(),n._resizeDropdown()}),t.on("select",function(){n._positionDropdown(),n._resizeDropdown()}),t.on("unselect",function(){n._positionDropdown(),n._resizeDropdown()}),this._containerResultsHandlersBound=!0)},e.prototype._attachPositioningHandler=function(e,t){var n=this,s="scroll.select2."+t.id,i="resize.select2."+t.id,r="orientationchange.select2."+t.id,t=this.$container.parents().filter(o.hasScroll);t.each(function(){o.StoreData(this,"select2-scroll-position",{x:u(this).scrollLeft(),y:u(this).scrollTop()})}),t.on(s,function(e){var t=o.GetData(this,"select2-scroll-position");u(this).scrollTop(t.y)}),u(window).on(s+" "+i+" "+r,function(e){n._positionDropdown(),n._resizeDropdown()})},e.prototype._detachPositioningHandler=function(e,t){var n="scroll.select2."+t.id,s="resize.select2."+t.id,t="orientationchange.select2."+t.id;this.$container.parents().filter(o.hasScroll).off(n),u(window).off(n+" "+s+" "+t)},e.prototype._positionDropdown=function(){var e=u(window),t=this.$dropdown[0].classList.contains("select2-dropdown--above"),n=this.$dropdown[0].classList.contains("select2-dropdown--below"),s=null,i=this.$container.offset();i.bottom=i.top+this.$container.outerHeight(!1);var r={height:this.$container.outerHeight(!1)};r.top=i.top,r.bottom=i.top+r.height;var o=this.$dropdown.outerHeight(!1),a=e.scrollTop(),l=e.scrollTop()+e.height(),c=ai.bottom+o,a={left:i.left,top:r.bottom},l=this.$dropdownParent;"static"===l.css("position")&&(l=l.offsetParent());i={top:0,left:0};(u.contains(document.body,l[0])||l[0].isConnected)&&(i=l.offset()),a.top-=i.top,a.left-=i.left,t||n||(s="below"),e||!c||t?!c&&e&&t&&(s="below"):s="above",("above"==s||t&&"below"!==s)&&(a.top=r.top-i.top-o),null!=s&&(this.$dropdown[0].classList.remove("select2-dropdown--below"),this.$dropdown[0].classList.remove("select2-dropdown--above"),this.$dropdown[0].classList.add("select2-dropdown--"+s),this.$container[0].classList.remove("select2-container--below"),this.$container[0].classList.remove("select2-container--above"),this.$container[0].classList.add("select2-container--"+s)),this.$dropdownContainer.css(a)},e.prototype._resizeDropdown=function(){var e={width:this.$container.outerWidth(!1)+"px"};this.options.get("dropdownAutoWidth")&&(e.minWidth=e.width,e.position="relative",e.width="auto"),this.$dropdown.css(e)},e.prototype._showDropdown=function(e){this.$dropdownContainer.appendTo(this.$dropdownParent),this._positionDropdown(),this._resizeDropdown()},e}),u.define("select2/dropdown/minimumResultsForSearch",[],function(){function e(e,t,n,s){this.minimumResultsForSearch=n.get("minimumResultsForSearch"),this.minimumResultsForSearch<0&&(this.minimumResultsForSearch=1/0),e.call(this,t,n,s)}return e.prototype.showSearch=function(e,t){return!(function e(t){for(var n=0,s=0;s');return e.attr("dir",this.options.get("dir")),this.$container=e,this.$container[0].classList.add("select2-container--"+this.options.get("theme")),r.StoreData(e[0],"element",this.$element),e},o}),u.define("jquery-mousewheel",["jquery"],function(e){return e}),u.define("jquery.select2",["jquery","jquery-mousewheel","./select2/core","./select2/defaults","./select2/utils"],function(i,e,r,t,o){var a;return null==i.fn.select2&&(a=["open","close","destroy"],i.fn.select2=function(t){if("object"==typeof(t=t||{}))return this.each(function(){var e=i.extend(!0,{},t);new r(i(this),e)}),this;if("string"!=typeof t)throw new Error("Invalid arguments for Select2: "+t);var n,s=Array.prototype.slice.call(arguments,1);return this.each(function(){var e=o.GetData(this,"select2");null==e&&window.console&&console.error&&console.error("The select2('"+t+"') method was called on an element that is not using Select2."),n=e[t].apply(e,s)}),-1Delete Account

              {% if account_can_be_deleted == False %} {% endif %} From c35dc1c4e7581d1b995d6153db678ebdf0e68e6c Mon Sep 17 00:00:00 2001 From: Nikita Churikov <8545082+churnikov@users.noreply.github.com> Date: Wed, 11 Sep 2024 20:30:44 +0200 Subject: [PATCH 10/19] SS-1108 environment support in jupyter notebooks (#223) Co-authored-by: akochari --- apps/constants.py | 2 + apps/forms/jupyter.py | 15 +++- .../0011_jupyterinstance_environment.py | 21 +++++ apps/models/app_types/jupyter.py | 5 ++ cypress/e2e/setup-scripts/seed_superuser.py | 11 ++- .../test-superuser-functionality.cy.js | 90 ++++++++++++++----- fixtures/projects_templates.json | 2 +- ...nt_app_alter_environment_image_and_more.py | 66 ++++++++++++++ ...t_rename_jupyter_lab_to_default_jupyter.py | 20 +++++ projects/models.py | 25 ++++-- templates/projects/settings.html | 26 +++--- 11 files changed, 239 insertions(+), 44 deletions(-) create mode 100644 apps/migrations/0011_jupyterinstance_environment.py create mode 100644 projects/migrations/0002_alter_environment_app_alter_environment_image_and_more.py create mode 100644 projects/migrations/0003_alter_environment_rename_jupyter_lab_to_default_jupyter.py diff --git a/apps/constants.py b/apps/constants.py index 4437091d..149b2f67 100644 --- a/apps/constants.py +++ b/apps/constants.py @@ -20,4 +20,6 @@ "3000-9999.", "note_on_linkonly_privacy": "This option can be used only for a limited amount of time, for example while under " "development or during peer review.", + "environment": "Select the environment that you want to use for your app. The environment is a Docker image that " + "contains the software and dependencies needed to run your app.", } diff --git a/apps/forms/jupyter.py b/apps/forms/jupyter.py index 87921948..81a7dbe3 100644 --- a/apps/forms/jupyter.py +++ b/apps/forms/jupyter.py @@ -1,5 +1,6 @@ from crispy_forms.layout import HTML, Div, Field, Layout from django import forms +from django.utils.safestring import mark_safe from apps.forms.base import AppBaseForm from apps.forms.field.common import SRVCommonDivField @@ -10,6 +11,17 @@ class JupyterForm(AppBaseForm): volume = forms.ModelMultipleChoiceField(queryset=VolumeInstance.objects.none(), required=False) + environment = forms.ModelChoiceField(queryset=None, required=True, empty_label=None) + + def _setup_form_fields(self): + super()._setup_form_fields() + self.fields["environment"].label = "Environment" + self.fields["environment"].queryset = self.project.environment_set.filter(app__slug="jupyter-lab") + self.fields["environment"].help_text = mark_safe( + "Select the environment to run the app in. " + "Read more about environments in the " + 'documentation.' + ) def _setup_form_helper(self): super()._setup_form_helper() @@ -19,6 +31,7 @@ def _setup_form_helper(self): Field("volume"), SRVCommonDivField("access"), SRVCommonDivField("flavor"), + SRVCommonDivField("environment"), css_class="card-body", ) @@ -26,4 +39,4 @@ def _setup_form_helper(self): class Meta: model = JupyterInstance - fields = ["name", "volume", "flavor", "access"] + fields = ["name", "volume", "flavor", "access", "environment"] diff --git a/apps/migrations/0011_jupyterinstance_environment.py b/apps/migrations/0011_jupyterinstance_environment.py new file mode 100644 index 00000000..cef26d40 --- /dev/null +++ b/apps/migrations/0011_jupyterinstance_environment.py @@ -0,0 +1,21 @@ +# Generated by Django 5.0.2 on 2024-09-06 18:25 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("apps", "0010_shinyinstance_shiny_site_dir"), + ("projects", "0001_initial"), + ] + + operations = [ + migrations.AddField( + model_name="jupyterinstance", + name="environment", + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, to="projects.environment" + ), + ), + ] diff --git a/apps/models/app_types/jupyter.py b/apps/models/app_types/jupyter.py index cfc12b1b..bc21b8de 100644 --- a/apps/models/app_types/jupyter.py +++ b/apps/models/app_types/jupyter.py @@ -1,6 +1,7 @@ from django.db import models from apps.models import AppInstanceManager, BaseAppInstance +from projects.models import Environment class JupyterInstanceManager(AppInstanceManager): @@ -15,6 +16,7 @@ class JupyterInstance(BaseAppInstance): ) volume = models.ManyToManyField("VolumeInstance", blank=True) access = models.CharField(max_length=20, default="project", choices=ACCESS_TYPES) + environment: Environment = models.ForeignKey(Environment, on_delete=models.DO_NOTHING, null=True, blank=True) def get_k8s_values(self): k8s_values = super().get_k8s_values() @@ -30,9 +32,12 @@ def get_k8s_values(self): for object in self.volume.all(): volumeK8s_dict["volumeK8s"][object.name] = dict(release=object.subdomain.subdomain) k8s_values["apps"] = volumeK8s_dict + if self.environment: + k8s_values["appconfig"] = {"image": self.environment.get_full_image_reference()} # This is just do fix a legacy. # TODO: Change the jupyter chart to fetch port from appconfig as other apps k8s_values["service"]["targetport"] = 8888 + return k8s_values class Meta: diff --git a/cypress/e2e/setup-scripts/seed_superuser.py b/cypress/e2e/setup-scripts/seed_superuser.py index 0e36e439..5e3612a0 100755 --- a/cypress/e2e/setup-scripts/seed_superuser.py +++ b/cypress/e2e/setup-scripts/seed_superuser.py @@ -9,7 +9,7 @@ from apps.app_registry import APP_REGISTRY from apps.helpers import create_instance_from_form -from projects.models import Flavor, Project, ProjectTemplate +from projects.models import Environment, Flavor, Project, ProjectTemplate from projects.tasks import create_resources_from_template from studio.utils import get_logger @@ -56,11 +56,18 @@ create_resources_from_template(user.username, project.slug, project_template.template) flavor = Flavor.objects.filter(project=project).first() + environment = Environment.objects.filter(project=project).first() # define variables needed app_slug = "jupyter-lab" - data = {"name": "Regular user's private app", "flavor": str(flavor.pk), "access": "private", "volume": None} + data = { + "name": "Regular user's private app", + "flavor": str(flavor.pk), + "access": "private", + "volume": None, + "environment": str(environment.pk), + } if app_slug not in APP_REGISTRY: raise ValueError(f"Form class not found for app slug {app_slug}") diff --git a/cypress/e2e/ui-tests/test-superuser-functionality.cy.js b/cypress/e2e/ui-tests/test-superuser-functionality.cy.js index 23f73e36..ec1c11d0 100644 --- a/cypress/e2e/ui-tests/test-superuser-functionality.cy.js +++ b/cypress/e2e/ui-tests/test-superuser-functionality.cy.js @@ -144,9 +144,9 @@ describe("Test superuser access", () => { cy.logf("Deleting a regular user's private app", Cypress.currentTest) cy.get('tr:contains("' + private_app_name_2 + '")').find('i.bi-three-dots-vertical').click() cy.get('tr:contains("' + private_app_name_2 + '")').find('a.confirm-delete').click() - cy.get('button').contains('Delete').click() + cy.get('#id_delete_button').contains('Delete').click() //cy.wait(5000) // Not needed because of the retryability built into cypress. - cy.get('tr:contains("' + private_app_name_2 + '")', {timeout: longCmdTimeoutMs}).find('span', {timeout: longCmdTimeoutMs}).should('contain', 'Deleted') + cy.get('tr:contains("' + private_app_name_2 + '")', {timeout: longCmdTimeoutMs}).find('span', {timeout: longCmdTimeoutMs}).should('contain', 'Deleted') cy.logf("Deleting a regular user's project", Cypress.currentTest) cy.visit("/projects/") @@ -159,10 +159,11 @@ describe("Test superuser access", () => { }) - it("can create a new flavor and a regular user can subsequently use it", { defaultCommandTimeout: 100000 }, () => { + it("can create a new flavor or environment and a regular user can subsequently use those", { defaultCommandTimeout: 100000 }, () => { // Names of objects to create - const project_name = "e2e-proj-flavor-test" + const project_name = "e2e-proj-flavor-env-test" const new_flavor_name = "4 CPU, 8 GB RAM" + const new_environment_name = "e2e test environment" cy.logf("Logging in as a regular user and creating a project", Cypress.currentTest) cy.fixture('users.json').then(function (data) { @@ -178,11 +179,13 @@ describe("Test superuser access", () => { cy.get('h3').should('contain', project_name) Cypress.session.clearAllSavedSessions() - cy.logf("Logging in as a superuser and creating a new flavor in the regular user's project", Cypress.currentTest) + cy.logf("Logging in as a superuser", Cypress.currentTest) cy.fixture('users.json').then(function (data) { users = data cy.loginViaUI(users.superuser.email, users.superuser.password) }) + + cy.logf("Creating a new flavor in the regular user's project", Cypress.currentTest) cy.visit("/projects/") cy.contains('.card-title', project_name).parents('.card-body').siblings('.card-footer').find('a:contains("Open")').first().click() cy.get('[data-cy="settings"]').click() @@ -192,53 +195,98 @@ describe("Test superuser access", () => { cy.get('input[name="cpu_lim"]').clear().type("4000m") cy.get('input[name="mem_req"]').clear().type("2Gi") cy.get('input[name="mem_lim"]').clear().type("8Gi") - cy.get('button').contains("Create").click() + cy.get('button').contains("Create flavor").click() + + cy.logf("Creating a new Jupyter Lab environment in the regular user's project", Cypress.currentTest) + cy.visit("/projects/") + cy.contains('.card-title', project_name).parents('.card-body').siblings('.card-footer').find('a:contains("Open")').first().click() + cy.get('[data-cy="settings"]').click() + cy.get('.list-group').find('a').contains('Environments').click() + cy.get('input[name="environment_name"]').type(new_environment_name) + cy.get('input[name="environment_repository"]').clear().type("dockerhub.io") + cy.get('input[name="environment_image"]').clear().type("jupyter/minimal-notebook:latest") + cy.get('#environment_app').select('Jupyter Lab') + cy.get('button').contains("Create environment").click() Cypress.session.clearAllSavedSessions() - cy.logf("Logging back in as a regular user and using the new flavor for an app", Cypress.currentTest) + cy.logf("Logging back in as a regular user and using the new flavor and environment", Cypress.currentTest) const createResources = Cypress.env('create_resources'); if (createResources === true) { - const app_type = "Dash App" - const app_name = "e2e-dash-example" - const app_description = "e2e-dash-description" - const image_name = "ghcr.io/scilifelabdatacentre/dash-covid-in-sweden:20240117-063059" - const image_port = "8000" - cy.fixture('users.json').then(function (data) { users = data cy.loginViaUI(users.superuser_testuser.email, users.superuser_testuser.password) }) + + cy.logf("Checking the flavour functionality", Cypress.currentTest) + + const app_type_flavor = "Dash App" + const app_name_flavor = "e2e-dash-example" + const app_description = "e2e-dash-description" + const image_name = "ghcr.io/scilifelabdatacentre/dash-covid-in-sweden:20240117-063059" + const image_port = "8000" + cy.visit("/projects/") cy.contains('.card-title', project_name).parents('.card-body').siblings('.card-footer').find('a:contains("Open")').first().click() - cy.get('div.card-body:contains("' + app_type + '")').find('a:contains("Create")').click() - cy.get('#id_name').type(app_name) + cy.get('div.card-body:contains("' + app_type_flavor + '")').find('a:contains("Create")').click() + cy.get('#id_name').type(app_name_flavor) cy.get('#id_description').type(app_description) cy.get('#id_access').select('Project') cy.get('#id_flavor').select('2 vCPU, 4 GB RAM') cy.get('#id_image').clear().type(image_name) cy.get('#id_port').clear().type(image_port) cy.get('#submit-id-submit').contains('Submit').click() - cy.get('tr:contains("' + app_name + '")').find('span').should('contain', 'Running') + cy.get('tr:contains("' + app_name_flavor + '")').find('span').should('contain', 'Running') cy.logf("Changing the flavor setting", Cypress.currentTest) cy.visit("/projects/") cy.contains('.card-title', project_name).parents('.card-body').siblings('.card-footer').find('a:contains("Open")').first().click() - cy.get('tr:contains("' + app_name + '")').find('i.bi-three-dots-vertical').click() - cy.get('tr:contains("' + app_name + '")').find('a').contains('Settings').click() + cy.get('tr:contains("' + app_name_flavor + '")').find('i.bi-three-dots-vertical').click() + cy.get('tr:contains("' + app_name_flavor + '")').find('a').contains('Settings').click() cy.get('#id_flavor').find(':selected').should('contain', '2 vCPU, 4 GB RAM') cy.get('#id_flavor').select(new_flavor_name) cy.get('#submit-id-submit').contains('Submit').click() - cy.get('tr:contains("' + app_name + '")').find('span').should('contain', 'Running') + cy.get('tr:contains("' + app_name_flavor + '")').find('span').should('contain', 'Running') cy.logf("Checking that the new flavor setting was saved in the database", Cypress.currentTest) cy.visit("/projects/") cy.contains('.card-title', project_name).parents('.card-body').siblings('.card-footer').find('a:contains("Open")').first().click() - cy.get('tr:contains("' + app_name + '")').find('i.bi-three-dots-vertical').click() - cy.get('tr:contains("' + app_name + '")').find('a').contains('Settings').click() + cy.get('tr:contains("' + app_name_flavor + '")').find('i.bi-three-dots-vertical').click() + cy.get('tr:contains("' + app_name_flavor + '")').find('a').contains('Settings').click() cy.get('#id_flavor').find(':selected').should('contain', new_flavor_name) + cy.logf("Checking the Jupyter Lab environment functionality", Cypress.currentTest) + + const app_type_env = "Jupyter Lab" + const app_name_env = "e2e-jupyter-lab-env-test" + + cy.visit("/projects/") + cy.contains('.card-title', project_name).parents('.card-body').siblings('.card-footer').find('a:contains("Open")').first().click() + cy.get('div.card-body:contains("' + app_type_env + '")').find('a:contains("Create")').click() + cy.get('#id_name').type(app_name_env) + cy.get('#id_environment').select('Default Jupyter Lab') + cy.get('#submit-id-submit').contains('Submit').click() + cy.get('tr:contains("' + app_name_env + '")').find('span').should('contain', 'Running') + + cy.logf("Changing the environment setting", Cypress.currentTest) + cy.visit("/projects/") + cy.contains('.card-title', project_name).parents('.card-body').siblings('.card-footer').find('a:contains("Open")').first().click() + cy.get('tr:contains("' + app_name_env + '")').find('i.bi-three-dots-vertical').click() + cy.get('tr:contains("' + app_name_env + '")').find('a').contains('Settings').click() + cy.get('#id_environment').find(':selected').should('contain', 'Default Jupyter Lab') + cy.get('#id_environment').select(new_environment_name) + cy.get('#submit-id-submit').contains('Submit').click() + cy.get('tr:contains("' + app_name_env + '")').find('span').should('contain', 'Running') + + cy.logf("Checking that the new environment setting was saved in the database", Cypress.currentTest) + cy.visit("/projects/") + cy.contains('.card-title', project_name).parents('.card-body').siblings('.card-footer').find('a:contains("Open")').first().click() + cy.get('tr:contains("' + app_name_env + '")').find('i.bi-three-dots-vertical').click() + cy.get('tr:contains("' + app_name_env + '")').find('a').contains('Settings').click() + cy.get('#id_environment').find(':selected').should('contain', new_environment_name) + + } else { cy.logf('Skipped because create_resources is not true', Cypress.currentTest); } diff --git a/fixtures/projects_templates.json b/fixtures/projects_templates.json index fc59f871..5b5e7208 100644 --- a/fixtures/projects_templates.json +++ b/fixtures/projects_templates.json @@ -13,7 +13,7 @@ } }, "environments": { - "Jupyter Lab": { + "Default Jupyter Lab": { "app": "jupyter-lab", "image": "serve-jupyterlab:231030-1145", "repository": "ghcr.io/scilifelabdatacentre" diff --git a/projects/migrations/0002_alter_environment_app_alter_environment_image_and_more.py b/projects/migrations/0002_alter_environment_app_alter_environment_image_and_more.py new file mode 100644 index 00000000..7e6851ac --- /dev/null +++ b/projects/migrations/0002_alter_environment_app_alter_environment_image_and_more.py @@ -0,0 +1,66 @@ +# Generated by Django 5.0.2 on 2024-09-09 15:23 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("apps", "0011_jupyterinstance_environment"), + ("projects", "0001_initial"), + ] + + operations = [ + migrations.AlterField( + model_name="environment", + name="app", + field=models.ForeignKey( + help_text="App associated with the environment", + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="apps.apps", + ), + ), + migrations.AlterField( + model_name="environment", + name="image", + field=models.CharField( + help_text="Image name. Could be like jupyter/minimal-notebook:latest", max_length=100 + ), + ), + migrations.AlterField( + model_name="environment", + name="name", + field=models.CharField(help_text="Display name for the environment for users", max_length=100), + ), + migrations.AlterField( + model_name="environment", + name="project", + field=models.ForeignKey( + blank=True, + help_text="Project associated with the environment", + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="projects.project", + ), + ), + migrations.AlterField( + model_name="environment", + name="repository", + field=models.CharField( + blank=True, help_text="Repository name. Could be empty or like ghcr.io", max_length=100, null=True + ), + ), + migrations.AlterField( + model_name="environment", + name="slug", + field=models.CharField( + blank=True, help_text="This one seem to be legacy and unused", max_length=100, null=True + ), + ), + migrations.AlterField( + model_name="environment", + name="public", + field=models.BooleanField(default=False, help_text="Seems to be legacy and have no effect."), + ), + ] diff --git a/projects/migrations/0003_alter_environment_rename_jupyter_lab_to_default_jupyter.py b/projects/migrations/0003_alter_environment_rename_jupyter_lab_to_default_jupyter.py new file mode 100644 index 00000000..f871878b --- /dev/null +++ b/projects/migrations/0003_alter_environment_rename_jupyter_lab_to_default_jupyter.py @@ -0,0 +1,20 @@ +# Written manually by Nikita Churikov on 2024-09-11 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("projects", "0002_alter_environment_app_alter_environment_image_and_more"), + ] + + operations = [ + migrations.RunSQL( + """ + UPDATE projects_environment + SET name = 'Default Jupyter Lab' + WHERE name = 'Jupyter Lab' + """ + ) + ] diff --git a/projects/models.py b/projects/models.py index eab020f9..5e9868b1 100644 --- a/projects/models.py +++ b/projects/models.py @@ -34,27 +34,40 @@ class BasicAuth(models.Model): class Environment(models.Model): - app = models.ForeignKey(settings.APPS_MODEL, on_delete=models.CASCADE, null=True) + app = models.ForeignKey( + settings.APPS_MODEL, on_delete=models.CASCADE, null=True, help_text="App associated with the environment" + ) created_at = models.DateTimeField(auto_now_add=True) - image = models.CharField(max_length=100) - name = models.CharField(max_length=100) + image = models.CharField(max_length=100, help_text="Image name. Could be like jupyter/minimal-notebook:latest") + name = models.CharField(max_length=100, help_text="Display name for the environment for users") project = models.ForeignKey( settings.PROJECTS_MODEL, on_delete=models.CASCADE, null=True, blank=True, + help_text="Project associated with the environment", ) - repository = models.CharField(max_length=100, blank=True, null=True) - slug = models.CharField(max_length=100, null=True, blank=True) + repository = models.CharField( + max_length=100, blank=True, null=True, help_text="Repository name. Could be empty or like ghcr.io" + ) + slug = models.CharField(max_length=100, null=True, blank=True, help_text="This one seem to be legacy and unused") updated_at = models.DateTimeField(auto_now=True) - public = models.BooleanField(default=False) + public = models.BooleanField(default=False, help_text="Seems to be legacy and have no effect.") def __str__(self): return str(self.name) + def get_full_image_reference(self): + """ + Get the full image reference for the environment + + It's either just the image name or the repository/image name + """ + return f"{self.repository}/{self.image}" if self.repository else self.image + class Flavor(models.Model): created_at = models.DateTimeField(auto_now_add=True) diff --git a/templates/projects/settings.html b/templates/projects/settings.html index 69e10861..2ee413ec 100644 --- a/templates/projects/settings.html +++ b/templates/projects/settings.html @@ -106,7 +106,7 @@
              Flavors
              {% endfor %}
              - +
              or create new flavor: @@ -138,7 +138,7 @@
              Flavors
              - +
              @@ -152,6 +152,15 @@
              Flavors
              Environments
              +{# Warn that this is a legacy way of environments management #} +
              + +
              +
              {% csrf_token %} @@ -164,7 +173,7 @@
              Environments
              {% endfor %}
              - +
              or create new Environment: @@ -176,15 +185,6 @@
              Environments
              -
              - - -
              Environments
              - + From 3a06acadff2e9c9e8fdca9171310ebfec2b265dd Mon Sep 17 00:00:00 2001 From: Nikita Churikov <8545082+churnikov@users.noreply.github.com> Date: Thu, 12 Sep 2024 17:33:15 +0200 Subject: [PATCH 11/19] SS-1096 fix login password. form now automatically lowercases the email (#228) Signed-off-by: Nikita Churikov --- templates/registration/login.html | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/templates/registration/login.html b/templates/registration/login.html index 5f4e2d14..60c9861b 100644 --- a/templates/registration/login.html +++ b/templates/registration/login.html @@ -47,7 +47,9 @@

              Log in

              {% csrf_token %}
              - +
              From c551b8d9facbb62824bd89c26fe4234c29ffedcb Mon Sep 17 00:00:00 2001 From: Nikita Churikov <8545082+churnikov@users.noreply.github.com> Date: Thu, 12 Sep 2024 17:40:40 +0200 Subject: [PATCH 12/19] SS-1101 Add password help text to the signup and password reset forms (#226) Signed-off-by: Nikita Churikov --- common/forms.py | 2 ++ templates/registration/password_reset_confirm.html | 2 ++ templates/registration/signup.html | 2 +- 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/common/forms.py b/common/forms.py index d55d964d..1d660e2a 100644 --- a/common/forms.py +++ b/common/forms.py @@ -6,6 +6,7 @@ from django import forms from django.conf import settings +from django.contrib.auth import password_validation from django.contrib.auth.forms import UserCreationForm from django.contrib.auth.models import User from django.core.exceptions import ValidationError @@ -117,6 +118,7 @@ class UserForm(BootstrapErrorFormMixin, UserCreationForm): min_length=8, label="Password", widget=forms.PasswordInput(attrs={"class": "form-control"}), + help_text=password_validation.password_validators_help_text_html(), ) password2 = forms.CharField( min_length=8, diff --git a/templates/registration/password_reset_confirm.html b/templates/registration/password_reset_confirm.html index 1619c209..c0ac3b9f 100644 --- a/templates/registration/password_reset_confirm.html +++ b/templates/registration/password_reset_confirm.html @@ -40,6 +40,8 @@

              Password reset

              id="new_password2">
              +
              {{form.new_password1.help_text}}
              + {% if form.new_password2.errors %}
              {% for error in form.new_password2.errors %} diff --git a/templates/registration/signup.html b/templates/registration/signup.html index 0849f4c5..1df44fe0 100644 --- a/templates/registration/signup.html +++ b/templates/registration/signup.html @@ -120,7 +120,6 @@

              Register

              {{form.password1}} -
              {{form.password1.help_text}}
              {% if form.password1.errors %}
              {% for error in form.password1.errors %} @@ -142,6 +141,7 @@

              Register

              {% endif %}
              +
              {{form.password1.help_text}}
              From f301abb7a7c400c524f70923cd5e71b9c3742f46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20Alfred=C3=A9en?= Date: Fri, 13 Sep 2024 20:38:23 +0200 Subject: [PATCH 13/19] App status enhancements (#227) --- api/views.py | 40 +++++++++---------- apps/helpers.py | 27 ++++++++----- apps/models/base/base.py | 6 +-- cypress/e2e/ui-tests/test-deploy-app.cy.js | 6 +-- .../test-project-as-contributor.cy.js | 26 ++++++++---- .../test-superuser-functionality.cy.js | 4 +- studio/urls.py | 1 + 7 files changed, 61 insertions(+), 49 deletions(-) diff --git a/api/views.py b/api/views.py index 73051128..c5686495 100644 --- a/api/views.py +++ b/api/views.py @@ -878,7 +878,7 @@ def update_app_status(request: HttpRequest) -> HttpResponse: # POST verb if request.method == "POST": - logger.info("API method update_app_status called with POST verb.") + logger.debug("API method update_app_status called with POST verb.") utc = pytz.UTC @@ -891,7 +891,7 @@ def update_app_status(request: HttpRequest) -> HttpResponse: new_status = request.data["new-status"] if len(new_status) > 15: - logger.debug("Status code is longer than 15 chars so shortening: %s", new_status) + logger.debug(f"Status code is longer than 15 chars so shortening: {new_status}") new_status = new_status[:15] event_ts = datetime.strptime(request.data["event-ts"], "%Y-%m-%dT%H:%M:%S.%fZ") @@ -901,19 +901,16 @@ def update_app_status(request: HttpRequest) -> HttpResponse: event_msg = request.data.get("event-msg", None) except KeyError as err: - logger.error("API method called with invalid input. Missing required input parameter: %s", err) + logger.error(f"API method called with invalid input. Missing required input parameter: {err}") return Response(f"Invalid input. Missing required input parameter: {err}", 400) except Exception as err: - logger.error("API method called with invalid input: %s, %s", err, type(err)) + logger.error(f"API method called with invalid input: {err}, type: {type(err)}") return Response(f"Invalid input. {err}", 400) logger.debug( - "API method update_app_status input: release=%s, new_status=%s, event_ts=%s, event_msg=%s", - release, - new_status, - event_ts, - event_msg, + f"API method update_app_status input: release={release}, new_status={new_status}, \ + event_ts={event_ts}, event_msg={event_msg}" ) try: @@ -921,40 +918,39 @@ def update_app_status(request: HttpRequest) -> HttpResponse: if result == HandleUpdateStatusResponseCode.NO_ACTION: return Response( - f"OK. NO_ACTION. No action performed. Possibly the event time is older \ - than the currently stored time. {release=}, {new_status=}", + "OK. NO_ACTION. No action performed. Possibly the event time is older \ + than the currently stored time.", 200, ) elif result == HandleUpdateStatusResponseCode.CREATED_FIRST_STATUS: - return Response(f"OK. CREATED_FIRST_STATUS. Created missing AppStatus. {release=}, {new_status=}", 200) + return Response("OK. CREATED_FIRST_STATUS. Created a missing AppStatus.", 200) elif result == HandleUpdateStatusResponseCode.UPDATED_STATUS: return Response( - f"OK. UPDATED_STATUS. Updated the app status. \ - Determined that the submitted event was newer and different status. {release=}, {new_status=}", + "OK. UPDATED_STATUS. Updated the app status. \ + Determined that the submitted event was newer and different status.", 200, ) elif result == HandleUpdateStatusResponseCode.UPDATED_TIME_OF_STATUS: return Response( - f"OK. UPDATED_TIME_OF_STATUS. Updated only the event time of the status. \ - Determined that the new and old status codes are the same. {release=}, {new_status=}", + "OK. UPDATED_TIME_OF_STATUS. Updated only the event time of the status. \ + Determined that the new and old status codes are the same.", 200, ) else: - logger.error("Unknown return code from handle_update_status_request() = %s", result, exc_info=True) + logger.error(f"Unknown return code from handle_update_status_request() = {result}", exc_info=True) return Response(f"Unknown return code from handle_update_status_request() = {result}", 500) except ObjectDoesNotExist: - logger.error("The specified app instance was not found release=%s.", release) - return Response(f"The specified app instance was not found {release=}.", 404) + # This is often not a problem. It typically happens during app re-deployemnts. + logger.warning(f"The specified app instance was not found release={release}") + return Response(f"The specified app instance was not found {release=}", 404) except Exception as err: - logger.error( - "Unable to update the status of the specified app instance %s. %s, %s", release, err, type(err) - ) + logger.error(f"Unable to update the status of the specified app instance {release}. {err}, {type(err)}") return Response(f"Unable to update the status of the specified app instance {release=}.", 500) # GET verb diff --git a/apps/helpers.py b/apps/helpers.py index 71ca51ec..52fa116f 100644 --- a/apps/helpers.py +++ b/apps/helpers.py @@ -124,28 +124,31 @@ def handle_update_status_request( # We wrap the select and update tasks in a select_for_update lock # to avoid race conditions. - # TODO: Check this. Seems like this is OK for the modified app instance. + # release takes on the value of the subdomain subdomain = Subdomain.objects.get(subdomain=release) with transaction.atomic(): instance = BaseAppInstance.objects.select_for_update().filter(subdomain=subdomain).last() if instance is None: - logger.info("The specified app instance was not found release=%s.", release) + logger.info(f"The specified app instance identified by release {release} was not found") raise ObjectDoesNotExist - logger.debug("The app instance exists. name=%s", instance.name) + logger.debug(f"The app instance identified by release {release} exists. App name={instance.name}") # Also get the latest app status object for this app instance if instance.app_status is None: # Missing app status so create one now - logger.info("AppInstance %s does not have an associated AppStatus. Creating one now.", release) + logger.debug(f"AppInstance {release} does not have an associated AppStatus. Creating one now.") app_status = AppStatus.objects.create() update_status(instance, app_status, new_status, event_ts, event_msg) return HandleUpdateStatusResponseCode.CREATED_FIRST_STATUS else: app_status = instance.app_status - logger.debug("AppStatus %s, %s, %s.", app_status.status, app_status.time, app_status.info) + logger.debug( + f"AppStatus object was created or updated with status {app_status.status}, ts={app_status.time}, \ + {app_status.info}" + ) # Now determine whether to update the state and status @@ -153,9 +156,8 @@ def handle_update_status_request( time_ftm = "%Y-%m-%d %H:%M:%S" if event_ts <= app_status.time: msg = "The incoming event-ts is older than the current status ts so nothing to do." - msg += ( - f"event_ts={event_ts.strftime(time_ftm)}, app_status.time={str(app_status.time.strftime(time_ftm))}" - ) + msg += f"event_ts={event_ts.strftime(time_ftm)} vs \ + app_status.time={str(app_status.time.strftime(time_ftm))}" logger.debug(msg) return HandleUpdateStatusResponseCode.NO_ACTION @@ -163,18 +165,20 @@ def handle_update_status_request( if new_status == instance.app_status.status: # The same status. Simply update the time. - logger.debug("The same status. Simply update the time.") + logger.debug(f"The same status {new_status}. Simply update the time.") update_status_time(app_status, event_ts, event_msg) return HandleUpdateStatusResponseCode.UPDATED_TIME_OF_STATUS # Different status and newer time - logger.debug("Different status and newer time.") + logger.debug( + f"Different status and newer time. New status={new_status} vs Old={instance.app_status.status}" + ) status_object = instance.app_status update_status(instance, status_object, new_status, event_ts, event_msg) return HandleUpdateStatusResponseCode.UPDATED_STATUS except Exception as err: - logger.error("Unable to fetch or update the specified app instance %s. %s, %s", release, err, type(err)) + logger.error(f"Unable to fetch or update the specified app instance with release={release}. {err}, {type(err)}") raise @@ -268,6 +272,7 @@ def create_instance_from_form(form, project, app_slug, app_id=None): ): # subdomain is a special field not contained in meta fields do_deploy = True + break subdomain_name, is_created_by_user = get_subdomain_name(form) diff --git a/apps/models/base/base.py b/apps/models/base/base.py index 01bb0094..a3ceab0c 100644 --- a/apps/models/base/base.py +++ b/apps/models/base/base.py @@ -28,9 +28,9 @@ def get_app_instances_of_project_filter(self, user, project, include_deleted=Fal if hasattr(self.model, "access"): q &= Q(owner=user) | Q( - access__in=["project", "public", "private", "link"] - if user.is_superuser - else ["project", "public", "link"] + access__in=( + ["project", "public", "private", "link"] if user.is_superuser else ["project", "public", "link"] + ) ) else: q &= Q(owner=user) diff --git a/cypress/e2e/ui-tests/test-deploy-app.cy.js b/cypress/e2e/ui-tests/test-deploy-app.cy.js index ca80303e..2521160b 100644 --- a/cypress/e2e/ui-tests/test-deploy-app.cy.js +++ b/cypress/e2e/ui-tests/test-deploy-app.cy.js @@ -1,10 +1,12 @@ describe("Test deploying app", () => { + // Tests performed as an authenticated user that creates and deletes apps. + // The default command timeout should not be so long // Instead use longer timeouts on specific commands where deemed necessary and valid const defaultCmdTimeoutMs = 10000 // The longer timeout is often used when waiting for k8s operations to complete - const longCmdTimeoutMs = 180000 + const longCmdTimeoutMs = 240000 // Function to verify the displayed app status permission level const verifyAppStatus = (app_name, expected_status, expected_permission) => { @@ -14,8 +16,6 @@ describe("Test deploying app", () => { } }; - // Tests performed as an authenticated user that - // creates and deletes apps. // user: e2e_tests_deploy_app_user let users diff --git a/cypress/e2e/ui-tests/test-project-as-contributor.cy.js b/cypress/e2e/ui-tests/test-project-as-contributor.cy.js index 541488da..34f2387a 100644 --- a/cypress/e2e/ui-tests/test-project-as-contributor.cy.js +++ b/cypress/e2e/ui-tests/test-project-as-contributor.cy.js @@ -1,9 +1,14 @@ describe("Test project contributor user functionality", () => { - // Tests performed as an authenticated user that - // creates and deletes objects. - // user: e2e_tests_contributor_tester + // Tests performed as an authenticated user that creates and deletes objects. + + // The default command timeout should not be so long + // Instead use longer timeouts on specific commands where deemed necessary and valid + const defaultCmdTimeoutMs = 10000 + // The longer timeout is often used when waiting for k8s operations to complete + const longCmdTimeoutMs = 180000 + // user: e2e_tests_contributor_tester let users before(() => { @@ -40,7 +45,7 @@ describe("Test project contributor user functionality", () => { cy.logf("End beforeEach() hook", Cypress.currentTest) }) - it("can create a new project with default template, open settings, change description, delete from settings", { defaultCommandTimeout: 100000 }, () => { + it("can create a new project with default template, open settings, change description, delete from settings", { defaultCommandTimeout: defaultCmdTimeoutMs }, () => { // Names of objects to create const project_name = "e2e-create-default-proj-test" @@ -115,7 +120,7 @@ describe("Test project contributor user functionality", () => { // This test cannot run properly in GitHub workflows because there is an issue with minio creation there. Therefore, it should be run locally to make sure things work. For GitHub, skipping it. // TODO: When models are launched, make sure that this test is activated - it.skip("can create a new project with ML serving template, open settings, delete from settings", { defaultCommandTimeout: 100000 }, () => { + it.skip("can create a new project with ML serving template, open settings, delete from settings", { defaultCommandTimeout: defaultCmdTimeoutMs }, () => { // Names of objects to create const project_name = "e2e-create-ml-proj-test" @@ -191,7 +196,7 @@ describe("Test project contributor user functionality", () => { }) }) - it("can delete a project from projects overview", { defaultCommandTimeout: 100000 }, () => { + it("can delete a project from projects overview", { defaultCommandTimeout: defaultCmdTimeoutMs }, () => { // Names of objects to create const project_name = "e2e-delete-proj-test" @@ -461,7 +466,7 @@ describe("Test project contributor user functionality", () => { }) }) - it("can create a file management instance", { defaultCommandTimeout: 100000 }, () => { + it("can create a file management instance", { defaultCommandTimeout: defaultCmdTimeoutMs }, () => { const project_name = "e2e-create-proj-test" cy.logf("Creating a blank project", Cypress.currentTest) @@ -474,6 +479,11 @@ describe("Test project contributor user functionality", () => { cy.get('div.card-body:contains("File Manager")').find('a:contains("Create")').click() cy.get('#submit-id-submit').click() - cy.get('tr:contains("File Manager")').find('span').should('contain', 'Running') + cy.get('tr:contains("File Manager")', {timeout: longCmdTimeoutMs}).find('span', {timeout: longCmdTimeoutMs}).should('contain', 'Running') + + // Wait for 5 seconds and check the status again + cy.wait(5000).then(() => { + cy.get('tr:contains("File Manager")', {timeout: longCmdTimeoutMs}).find('span', {timeout: longCmdTimeoutMs}).should('contain', 'Running') + }) }) }) diff --git a/cypress/e2e/ui-tests/test-superuser-functionality.cy.js b/cypress/e2e/ui-tests/test-superuser-functionality.cy.js index ec1c11d0..3e333a4f 100644 --- a/cypress/e2e/ui-tests/test-superuser-functionality.cy.js +++ b/cypress/e2e/ui-tests/test-superuser-functionality.cy.js @@ -144,7 +144,7 @@ describe("Test superuser access", () => { cy.logf("Deleting a regular user's private app", Cypress.currentTest) cy.get('tr:contains("' + private_app_name_2 + '")').find('i.bi-three-dots-vertical').click() cy.get('tr:contains("' + private_app_name_2 + '")').find('a.confirm-delete').click() - cy.get('#id_delete_button').contains('Delete').click() + cy.get('button').contains('Delete').click() //cy.wait(5000) // Not needed because of the retryability built into cypress. cy.get('tr:contains("' + private_app_name_2 + '")', {timeout: longCmdTimeoutMs}).find('span', {timeout: longCmdTimeoutMs}).should('contain', 'Deleted') @@ -203,7 +203,7 @@ describe("Test superuser access", () => { cy.get('[data-cy="settings"]').click() cy.get('.list-group').find('a').contains('Environments').click() cy.get('input[name="environment_name"]').type(new_environment_name) - cy.get('input[name="environment_repository"]').clear().type("dockerhub.io") + cy.get('input[name="environment_repository"]').clear().type("docker.io") cy.get('input[name="environment_image"]').clear().type("jupyter/minimal-notebook:latest") cy.get('#environment_app').select('Jupyter Lab') cy.get('button').contains("Create environment").click() diff --git a/studio/urls.py b/studio/urls.py index dfe4e530..b117a144 100644 --- a/studio/urls.py +++ b/studio/urls.py @@ -13,6 +13,7 @@ 1. Import the include() function: from django.urls import include, path 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ + from django.conf import settings from django.conf.urls.static import static from django.contrib import admin From dbb5e975b0c0cc412eada16e276eee762bc88c51 Mon Sep 17 00:00:00 2001 From: Hamza Date: Mon, 16 Sep 2024 10:02:18 +0200 Subject: [PATCH 14/19] SS-1089 Add connection pooling (#229) --- common/context_processors.py | 10 +- poetry.lock | 407 +++++++++++++++++++++-------------- pyproject.toml | 18 +- studio/settings.py | 18 +- 4 files changed, 278 insertions(+), 175 deletions(-) diff --git a/common/context_processors.py b/common/context_processors.py index 70f90cd7..a95367e1 100644 --- a/common/context_processors.py +++ b/common/context_processors.py @@ -1,6 +1,14 @@ +from studio.utils import get_logger + from .models import MaintenanceMode +logger = get_logger(__name__) + def maintenance_mode(request): - data = MaintenanceMode.objects.all() + try: + data = MaintenanceMode.objects.all() + except Exception as e: + logger.debug("Error fetching maintenance mode data: %s", e) + data = [] return {"maintenance_mode": data} diff --git a/poetry.lock b/poetry.lock index e8f365bb..35798b3a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -872,17 +872,17 @@ test-randomorder = ["pytest-randomly"] [[package]] name = "django" -version = "5.0.2" +version = "5.1.1" description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." optional = false python-versions = ">=3.10" files = [ - {file = "Django-5.0.2-py3-none-any.whl", hash = "sha256:56ab63a105e8bb06ee67381d7b65fe6774f057e41a8bab06c8020c8882d8ecd4"}, - {file = "Django-5.0.2.tar.gz", hash = "sha256:b5bb1d11b2518a5f91372a282f24662f58f66749666b0a286ab057029f728080"}, + {file = "Django-5.1.1-py3-none-any.whl", hash = "sha256:71603f27dac22a6533fb38d83072eea9ddb4017fead6f67f2562a40402d61c3f"}, + {file = "Django-5.1.1.tar.gz", hash = "sha256:021ffb7fdab3d2d388bc8c7c2434eb9c1f6f4d09e6119010bbb1694dda286bc2"}, ] [package.dependencies] -asgiref = ">=3.7.0,<4" +asgiref = ">=3.8.1,<4" sqlparse = ">=0.3.1" tzdata = {version = "*", markers = "sys_platform == \"win32\""} @@ -925,18 +925,19 @@ ipware = ["django-ipware (>=3)"] [[package]] name = "django-celery-beat" -version = "2.6.0" +version = "2.7.0" description = "Database-backed Periodic Tasks." optional = false -python-versions = "*" +python-versions = ">=3.8" files = [ - {file = "django-celery-beat-2.6.0.tar.gz", hash = "sha256:f75b2d129731f1214be8383e18fae6bfeacdb55dffb2116ce849222c0106f9ad"}, + {file = "django_celery_beat-2.7.0-py3-none-any.whl", hash = "sha256:851c680d8fbf608ca5fecd5836622beea89fa017bc2b3f94a5b8c648c32d84b1"}, + {file = "django_celery_beat-2.7.0.tar.gz", hash = "sha256:8482034925e09b698c05ad61c36ed2a8dbc436724a3fe119215193a4ca6dc967"}, ] [package.dependencies] celery = ">=5.2.3,<6.0" cron-descriptor = ">=1.2.32" -Django = ">=2.2,<5.1" +Django = ">=2.2,<5.2" django-timezone-field = ">=5.0" python-crontab = ">=2.3.4" tzdata = "*" @@ -957,44 +958,45 @@ django = ">=3.2" [[package]] name = "django-compressor" -version = "4.4" +version = "4.5.1" description = "('Compresses linked and inline JavaScript or CSS into single cached files.',)" optional = false -python-versions = "*" +python-versions = ">=3.8" files = [ - {file = "django_compressor-4.4-py2.py3-none-any.whl", hash = "sha256:6e2b0c0becb9607f5099c2546a824c5b84a6918a34bc37a8a622ffa250313596"}, - {file = "django_compressor-4.4.tar.gz", hash = "sha256:1b0acc9cfba9f69bc38e7c41da9b0d70a20bc95587b643ffef9609cf46064f67"}, + {file = "django_compressor-4.5.1-py2.py3-none-any.whl", hash = "sha256:87741edee4e7f24f3e0b8072d94a990cfb010cb2ca7cc443944da8e193cdea65"}, + {file = "django_compressor-4.5.1.tar.gz", hash = "sha256:c1d8a48a2ee4d8b7f23c411eb9c97e2d88db18a18ba1c9e8178d5f5b8366a822"}, ] [package.dependencies] +Django = ">=4.2" django-appconf = ">=1.0.3" -rcssmin = "1.1.1" -rjsmin = "1.2.1" +rcssmin = "1.1.2" +rjsmin = "1.2.2" [[package]] name = "django-cors-headers" -version = "4.3.1" +version = "4.4.0" description = "django-cors-headers is a Django application for handling the server headers required for Cross-Origin Resource Sharing (CORS)." optional = false python-versions = ">=3.8" files = [ - {file = "django-cors-headers-4.3.1.tar.gz", hash = "sha256:0bf65ef45e606aff1994d35503e6b677c0b26cafff6506f8fd7187f3be840207"}, - {file = "django_cors_headers-4.3.1-py3-none-any.whl", hash = "sha256:0b1fd19297e37417fc9f835d39e45c8c642938ddba1acce0c1753d3edef04f36"}, + {file = "django_cors_headers-4.4.0-py3-none-any.whl", hash = "sha256:5c6e3b7fe870876a1efdfeb4f433782c3524078fa0dc9e0195f6706ce7a242f6"}, + {file = "django_cors_headers-4.4.0.tar.gz", hash = "sha256:92cf4633e22af67a230a1456cb1b7a02bb213d6536d2dcb2a4a24092ea9cebc2"}, ] [package.dependencies] asgiref = ">=3.6" -Django = ">=3.2" +django = ">=3.2" [[package]] name = "django-crispy-forms" -version = "2.1" +version = "2.3" description = "Best way to have Django DRY forms" optional = false python-versions = ">=3.8" files = [ - {file = "django-crispy-forms-2.1.tar.gz", hash = "sha256:4d7ec431933ad4d4b5c5a6de4a584d24613c347db9ac168723c9aaf63af4bb96"}, - {file = "django_crispy_forms-2.1-py3-none-any.whl", hash = "sha256:d592044771412ae1bd539cc377203aa61d4eebe77fcbc07fbc8f12d3746d4f6b"}, + {file = "django_crispy_forms-2.3-py3-none-any.whl", hash = "sha256:efc4c31e5202bbec6af70d383a35e12fc80ea769d464fb0e7fe21768bb138a20"}, + {file = "django_crispy_forms-2.3.tar.gz", hash = "sha256:2db17ae08527201be1273f0df789e5f92819e23dd28fec69cffba7f3762e1a38"}, ] [package.dependencies] @@ -1016,13 +1018,13 @@ Django = ">=3.2" [[package]] name = "django-filter" -version = "24.2" +version = "24.3" description = "Django-filter is a reusable Django application for allowing users to filter querysets dynamically." optional = false python-versions = ">=3.8" files = [ - {file = "django-filter-24.2.tar.gz", hash = "sha256:48e5fc1da3ccd6ca0d5f9bb550973518ce977a4edde9d2a8a154a7f4f0b9f96e"}, - {file = "django_filter-24.2-py3-none-any.whl", hash = "sha256:df2ee9857e18d38bed203c8745f62a803fa0f31688c9fe6f8e868120b1848e48"}, + {file = "django_filter-24.3-py3-none-any.whl", hash = "sha256:c4852822928ce17fb699bcfccd644b3574f1a2d80aeb2b4ff4f16b02dd49dc64"}, + {file = "django_filter-24.3.tar.gz", hash = "sha256:d8ccaf6732afd21ca0542f6733b11591030fa98669f8d15599b358e24a2cd9c3"}, ] [package.dependencies] @@ -1107,17 +1109,17 @@ tests = ["coverage[toml]", "mock-django"] [[package]] name = "django-nyt" -version = "1.4" +version = "1.4.1" description = "A pluggable notification system written for the Django framework." optional = false python-versions = ">=3.8" files = [ - {file = "django_nyt-1.4-py3-none-any.whl", hash = "sha256:1297293ce352610b7cdc6d4d2d92729cf7b4ed4f8d2bb2ce4adc9e7147f640e7"}, - {file = "django_nyt-1.4.tar.gz", hash = "sha256:03ac867963c1d935025a281b2d83c0d0d72c180616fa6ee86f3706d0d0009ad8"}, + {file = "django_nyt-1.4.1-py3-none-any.whl", hash = "sha256:1b3f72fabe663c0ceef395e0d018624b980949a347cdb2cc10e7c4f2fecd6aff"}, + {file = "django_nyt-1.4.1.tar.gz", hash = "sha256:47660e56509c9e0fd21c2e590b4fb7a78ebd903b1d987ee81bf6a0ac3352b4a1"}, ] [package.dependencies] -django = ">=2.2,<5.1" +django = ">=2.2,<5.2" [package.extras] docs = ["channels (==4.0.0)", "sphinx (==6.2.1)", "sphinx-rtd-theme (==1.2.0)"] @@ -1205,17 +1207,17 @@ Django = ">=3.2,<6.0" [[package]] name = "djangorestframework" -version = "3.15.1" +version = "3.15.2" description = "Web APIs for Django, made easy." optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" files = [ - {file = "djangorestframework-3.15.1-py3-none-any.whl", hash = "sha256:3ccc0475bce968608cf30d07fb17d8e52d1d7fc8bfe779c905463200750cbca6"}, - {file = "djangorestframework-3.15.1.tar.gz", hash = "sha256:f88fad74183dfc7144b2756d0d2ac716ea5b4c7c9840995ac3bfd8ec034333c1"}, + {file = "djangorestframework-3.15.2-py3-none-any.whl", hash = "sha256:2b8871b062ba1aefc2de01f773875441a961fefbf79f5eed1e32b2f096944b20"}, + {file = "djangorestframework-3.15.2.tar.gz", hash = "sha256:36fe88cd2d6c6bec23dca9804bab2ba5517a8bb9d8f47ebc68981b56840107ad"}, ] [package.dependencies] -django = ">=3.0" +django = ">=4.2" [[package]] name = "docker" @@ -2405,86 +2407,117 @@ files = [ wcwidth = "*" [[package]] -name = "psycopg2-binary" -version = "2.9.9" -description = "psycopg2 - Python-PostgreSQL Database Adapter" +name = "psycopg" +version = "3.2.1" +description = "PostgreSQL database adapter for Python" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" +files = [ + {file = "psycopg-3.2.1-py3-none-any.whl", hash = "sha256:ece385fb413a37db332f97c49208b36cf030ff02b199d7635ed2fbd378724175"}, + {file = "psycopg-3.2.1.tar.gz", hash = "sha256:dc8da6dc8729dacacda3cc2f17d2c9397a70a66cf0d2b69c91065d60d5f00cb7"}, +] + +[package.dependencies] +psycopg-binary = {version = "3.2.1", optional = true, markers = "implementation_name != \"pypy\" and extra == \"binary\""} +psycopg-c = {version = "3.2.1", optional = true, markers = "implementation_name != \"pypy\" and extra == \"c\""} +psycopg-pool = {version = "*", optional = true, markers = "extra == \"pool\""} +typing-extensions = ">=4.4" +tzdata = {version = "*", markers = "sys_platform == \"win32\""} + +[package.extras] +binary = ["psycopg-binary (==3.2.1)"] +c = ["psycopg-c (==3.2.1)"] +dev = ["ast-comments (>=1.1.2)", "black (>=24.1.0)", "codespell (>=2.2)", "dnspython (>=2.1)", "flake8 (>=4.0)", "mypy (>=1.6)", "types-setuptools (>=57.4)", "wheel (>=0.37)"] +docs = ["Sphinx (>=5.0)", "furo (==2022.6.21)", "sphinx-autobuild (>=2021.3.14)", "sphinx-autodoc-typehints (>=1.12)"] +pool = ["psycopg-pool"] +test = ["anyio (>=4.0)", "mypy (>=1.6)", "pproxy (>=2.7)", "pytest (>=6.2.5)", "pytest-cov (>=3.0)", "pytest-randomly (>=3.5)"] + +[[package]] +name = "psycopg-binary" +version = "3.2.1" +description = "PostgreSQL database adapter for Python -- C optimisation distribution" +optional = false +python-versions = ">=3.8" files = [ - {file = "psycopg2-binary-2.9.9.tar.gz", hash = "sha256:7f01846810177d829c7692f1f5ada8096762d9172af1b1a28d4ab5b77c923c1c"}, - {file = "psycopg2_binary-2.9.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c2470da5418b76232f02a2fcd2229537bb2d5a7096674ce61859c3229f2eb202"}, - {file = "psycopg2_binary-2.9.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c6af2a6d4b7ee9615cbb162b0738f6e1fd1f5c3eda7e5da17861eacf4c717ea7"}, - {file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:75723c3c0fbbf34350b46a3199eb50638ab22a0228f93fb472ef4d9becc2382b"}, - {file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83791a65b51ad6ee6cf0845634859d69a038ea9b03d7b26e703f94c7e93dbcf9"}, - {file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0ef4854e82c09e84cc63084a9e4ccd6d9b154f1dbdd283efb92ecd0b5e2b8c84"}, - {file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed1184ab8f113e8d660ce49a56390ca181f2981066acc27cf637d5c1e10ce46e"}, - {file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d2997c458c690ec2bc6b0b7ecbafd02b029b7b4283078d3b32a852a7ce3ddd98"}, - {file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:b58b4710c7f4161b5e9dcbe73bb7c62d65670a87df7bcce9e1faaad43e715245"}, - {file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:0c009475ee389757e6e34611d75f6e4f05f0cf5ebb76c6037508318e1a1e0d7e"}, - {file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8dbf6d1bc73f1d04ec1734bae3b4fb0ee3cb2a493d35ede9badbeb901fb40f6f"}, - {file = "psycopg2_binary-2.9.9-cp310-cp310-win32.whl", hash = "sha256:3f78fd71c4f43a13d342be74ebbc0666fe1f555b8837eb113cb7416856c79682"}, - {file = "psycopg2_binary-2.9.9-cp310-cp310-win_amd64.whl", hash = "sha256:876801744b0dee379e4e3c38b76fc89f88834bb15bf92ee07d94acd06ec890a0"}, - {file = "psycopg2_binary-2.9.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ee825e70b1a209475622f7f7b776785bd68f34af6e7a46e2e42f27b659b5bc26"}, - {file = "psycopg2_binary-2.9.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1ea665f8ce695bcc37a90ee52de7a7980be5161375d42a0b6c6abedbf0d81f0f"}, - {file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:143072318f793f53819048fdfe30c321890af0c3ec7cb1dfc9cc87aa88241de2"}, - {file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c332c8d69fb64979ebf76613c66b985414927a40f8defa16cf1bc028b7b0a7b0"}, - {file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7fc5a5acafb7d6ccca13bfa8c90f8c51f13d8fb87d95656d3950f0158d3ce53"}, - {file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:977646e05232579d2e7b9c59e21dbe5261f403a88417f6a6512e70d3f8a046be"}, - {file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b6356793b84728d9d50ead16ab43c187673831e9d4019013f1402c41b1db9b27"}, - {file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:bc7bb56d04601d443f24094e9e31ae6deec9ccb23581f75343feebaf30423359"}, - {file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:77853062a2c45be16fd6b8d6de2a99278ee1d985a7bd8b103e97e41c034006d2"}, - {file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:78151aa3ec21dccd5cdef6c74c3e73386dcdfaf19bced944169697d7ac7482fc"}, - {file = "psycopg2_binary-2.9.9-cp311-cp311-win32.whl", hash = "sha256:dc4926288b2a3e9fd7b50dc6a1909a13bbdadfc67d93f3374d984e56f885579d"}, - {file = "psycopg2_binary-2.9.9-cp311-cp311-win_amd64.whl", hash = "sha256:b76bedd166805480ab069612119ea636f5ab8f8771e640ae103e05a4aae3e417"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:8532fd6e6e2dc57bcb3bc90b079c60de896d2128c5d9d6f24a63875a95a088cf"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0605eaed3eb239e87df0d5e3c6489daae3f7388d455d0c0b4df899519c6a38d"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f8544b092a29a6ddd72f3556a9fcf249ec412e10ad28be6a0c0d948924f2212"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2d423c8d8a3c82d08fe8af900ad5b613ce3632a1249fd6a223941d0735fce493"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e5afae772c00980525f6d6ecf7cbca55676296b580c0e6abb407f15f3706996"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e6f98446430fdf41bd36d4faa6cb409f5140c1c2cf58ce0bbdaf16af7d3f119"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c77e3d1862452565875eb31bdb45ac62502feabbd53429fdc39a1cc341d681ba"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:cb16c65dcb648d0a43a2521f2f0a2300f40639f6f8c1ecbc662141e4e3e1ee07"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:911dda9c487075abd54e644ccdf5e5c16773470a6a5d3826fda76699410066fb"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:57fede879f08d23c85140a360c6a77709113efd1c993923c59fde17aa27599fe"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-win32.whl", hash = "sha256:64cf30263844fa208851ebb13b0732ce674d8ec6a0c86a4e160495d299ba3c93"}, - {file = "psycopg2_binary-2.9.9-cp312-cp312-win_amd64.whl", hash = "sha256:81ff62668af011f9a48787564ab7eded4e9fb17a4a6a74af5ffa6a457400d2ab"}, - {file = "psycopg2_binary-2.9.9-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2293b001e319ab0d869d660a704942c9e2cce19745262a8aba2115ef41a0a42a"}, - {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:03ef7df18daf2c4c07e2695e8cfd5ee7f748a1d54d802330985a78d2a5a6dca9"}, - {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a602ea5aff39bb9fac6308e9c9d82b9a35c2bf288e184a816002c9fae930b77"}, - {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8359bf4791968c5a78c56103702000105501adb557f3cf772b2c207284273984"}, - {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:275ff571376626195ab95a746e6a04c7df8ea34638b99fc11160de91f2fef503"}, - {file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:f9b5571d33660d5009a8b3c25dc1db560206e2d2f89d3df1cb32d72c0d117d52"}, - {file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:420f9bbf47a02616e8554e825208cb947969451978dceb77f95ad09c37791dae"}, - {file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:4154ad09dac630a0f13f37b583eae260c6aa885d67dfbccb5b02c33f31a6d420"}, - {file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a148c5d507bb9b4f2030a2025c545fccb0e1ef317393eaba42e7eabd28eb6041"}, - {file = "psycopg2_binary-2.9.9-cp37-cp37m-win32.whl", hash = "sha256:68fc1f1ba168724771e38bee37d940d2865cb0f562380a1fb1ffb428b75cb692"}, - {file = "psycopg2_binary-2.9.9-cp37-cp37m-win_amd64.whl", hash = "sha256:281309265596e388ef483250db3640e5f414168c5a67e9c665cafce9492eda2f"}, - {file = "psycopg2_binary-2.9.9-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:60989127da422b74a04345096c10d416c2b41bd7bf2a380eb541059e4e999980"}, - {file = "psycopg2_binary-2.9.9-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:246b123cc54bb5361588acc54218c8c9fb73068bf227a4a531d8ed56fa3ca7d6"}, - {file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34eccd14566f8fe14b2b95bb13b11572f7c7d5c36da61caf414d23b91fcc5d94"}, - {file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18d0ef97766055fec15b5de2c06dd8e7654705ce3e5e5eed3b6651a1d2a9a152"}, - {file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d3f82c171b4ccd83bbaf35aa05e44e690113bd4f3b7b6cc54d2219b132f3ae55"}, - {file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ead20f7913a9c1e894aebe47cccf9dc834e1618b7aa96155d2091a626e59c972"}, - {file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ca49a8119c6cbd77375ae303b0cfd8c11f011abbbd64601167ecca18a87e7cdd"}, - {file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:323ba25b92454adb36fa425dc5cf6f8f19f78948cbad2e7bc6cdf7b0d7982e59"}, - {file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:1236ed0952fbd919c100bc839eaa4a39ebc397ed1c08a97fc45fee2a595aa1b3"}, - {file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:729177eaf0aefca0994ce4cffe96ad3c75e377c7b6f4efa59ebf003b6d398716"}, - {file = "psycopg2_binary-2.9.9-cp38-cp38-win32.whl", hash = "sha256:804d99b24ad523a1fe18cc707bf741670332f7c7412e9d49cb5eab67e886b9b5"}, - {file = "psycopg2_binary-2.9.9-cp38-cp38-win_amd64.whl", hash = "sha256:a6cdcc3ede532f4a4b96000b6362099591ab4a3e913d70bcbac2b56c872446f7"}, - {file = "psycopg2_binary-2.9.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:72dffbd8b4194858d0941062a9766f8297e8868e1dd07a7b36212aaa90f49472"}, - {file = "psycopg2_binary-2.9.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:30dcc86377618a4c8f3b72418df92e77be4254d8f89f14b8e8f57d6d43603c0f"}, - {file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:31a34c508c003a4347d389a9e6fcc2307cc2150eb516462a7a17512130de109e"}, - {file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:15208be1c50b99203fe88d15695f22a5bed95ab3f84354c494bcb1d08557df67"}, - {file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1873aade94b74715be2246321c8650cabf5a0d098a95bab81145ffffa4c13876"}, - {file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a58c98a7e9c021f357348867f537017057c2ed7f77337fd914d0bedb35dace7"}, - {file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4686818798f9194d03c9129a4d9a702d9e113a89cb03bffe08c6cf799e053291"}, - {file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:ebdc36bea43063116f0486869652cb2ed7032dbc59fbcb4445c4862b5c1ecf7f"}, - {file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:ca08decd2697fdea0aea364b370b1249d47336aec935f87b8bbfd7da5b2ee9c1"}, - {file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ac05fb791acf5e1a3e39402641827780fe44d27e72567a000412c648a85ba860"}, - {file = "psycopg2_binary-2.9.9-cp39-cp39-win32.whl", hash = "sha256:9dba73be7305b399924709b91682299794887cbbd88e38226ed9f6712eabee90"}, - {file = "psycopg2_binary-2.9.9-cp39-cp39-win_amd64.whl", hash = "sha256:f7ae5d65ccfbebdfa761585228eb4d0df3a8b15cfb53bd953e713e09fbb12957"}, + {file = "psycopg_binary-3.2.1-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:cad2de17804c4cfee8640ae2b279d616bb9e4734ac3c17c13db5e40982bd710d"}, + {file = "psycopg_binary-3.2.1-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:592b27d6c46a40f9eeaaeea7c1fef6f3c60b02c634365eb649b2d880669f149f"}, + {file = "psycopg_binary-3.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9a997efbaadb5e1a294fb5760e2f5643d7b8e4e3fe6cb6f09e6d605fd28e0291"}, + {file = "psycopg_binary-3.2.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c1d2b6438fb83376f43ebb798bf0ad5e57bc56c03c9c29c85bc15405c8c0ac5a"}, + {file = "psycopg_binary-3.2.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b1f087bd84bdcac78bf9f024ebdbfacd07fc0a23ec8191448a50679e2ac4a19e"}, + {file = "psycopg_binary-3.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:415c3b72ea32119163255c6504085f374e47ae7345f14bc3f0ef1f6e0976a879"}, + {file = "psycopg_binary-3.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f092114f10f81fb6bae544a0ec027eb720e2d9c74a4fcdaa9dd3899873136935"}, + {file = "psycopg_binary-3.2.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:06a7aae34edfe179ddc04da005e083ff6c6b0020000399a2cbf0a7121a8a22ea"}, + {file = "psycopg_binary-3.2.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:0b018631e5c80ce9bc210b71ea885932f9cca6db131e4df505653d7e3873a938"}, + {file = "psycopg_binary-3.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f8a509aeaac364fa965454e80cd110fe6d48ba2c80f56c9b8563423f0b5c3cfd"}, + {file = "psycopg_binary-3.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:413977d18412ff83486eeb5875eb00b185a9391c57febac45b8993bf9c0ff489"}, + {file = "psycopg_binary-3.2.1-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:62b1b7b07e00ee490afb39c0a47d8282a9c2822c7cfed9553a04b0058adf7e7f"}, + {file = "psycopg_binary-3.2.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:f8afb07114ea9b924a4a0305ceb15354ccf0ef3c0e14d54b8dbeb03e50182dd7"}, + {file = "psycopg_binary-3.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40bb515d042f6a345714ec0403df68ccf13f73b05e567837d80c886c7c9d3805"}, + {file = "psycopg_binary-3.2.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6418712ba63cebb0c88c050b3997185b0ef54173b36568522d5634ac06153040"}, + {file = "psycopg_binary-3.2.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:101472468d59c74bb8565fab603e032803fd533d16be4b2d13da1bab8deb32a3"}, + {file = "psycopg_binary-3.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa3931f308ab4a479d0ee22dc04bea867a6365cac0172e5ddcba359da043854b"}, + {file = "psycopg_binary-3.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dc314a47d44fe1a8069b075a64abffad347a3a1d8652fed1bab5d3baea37acb2"}, + {file = "psycopg_binary-3.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:cc304a46be1e291031148d9d95c12451ffe783ff0cc72f18e2cc7ec43cdb8c68"}, + {file = "psycopg_binary-3.2.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6f9e13600647087df5928875559f0eb8f496f53e6278b7da9511b4b3d0aff960"}, + {file = "psycopg_binary-3.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b140182830c76c74d17eba27df3755a46442ce8d4fb299e7f1cf2f74a87c877b"}, + {file = "psycopg_binary-3.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:3c838806eeb99af39f934b7999e35f947a8e577997cc892c12b5053a97a9057f"}, + {file = "psycopg_binary-3.2.1-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:7066d3dca196ed0dc6172f9777b2d62e4f138705886be656cccff2d555234d60"}, + {file = "psycopg_binary-3.2.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:28ada5f610468c57d8a4a055a8ea915d0085a43d794266c4f3b9d02f4288f4db"}, + {file = "psycopg_binary-3.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e8213bf50af073b1aa8dc3cff123bfeedac86332a16c1b7274910bc88a847c7"}, + {file = "psycopg_binary-3.2.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:74d623261655a169bc84a9669890975c229f2fa6e19a7f2d10a77675dcf1a707"}, + {file = "psycopg_binary-3.2.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:42781ba94e8842ee98bca5a7d0c44cc9d067500fedca2d6a90fa3609b6d16b42"}, + {file = "psycopg_binary-3.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33e6669091d09f8ba36e10ce678a6d9916e110446236a9b92346464a3565635e"}, + {file = "psycopg_binary-3.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b09e8a576a2ac69d695032ee76f31e03b30781828b5dd6d18c6a009e5a3d1c35"}, + {file = "psycopg_binary-3.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8f28ff0cb9f1defdc4a6f8c958bf6787274247e7dfeca811f6e2f56602695fb1"}, + {file = "psycopg_binary-3.2.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4c84fcac8a3a3479ac14673095cc4e1fdba2935499f72c436785ac679bec0d1a"}, + {file = "psycopg_binary-3.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:950fd666ec9e9fe6a8eeb2b5a8f17301790e518953730ad44d715b59ffdbc67f"}, + {file = "psycopg_binary-3.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:334046a937bb086c36e2c6889fe327f9f29bfc085d678f70fac0b0618949f674"}, + {file = "psycopg_binary-3.2.1-cp38-cp38-macosx_12_0_x86_64.whl", hash = "sha256:1d6833f607f3fc7b22226a9e121235d3b84c0eda1d3caab174673ef698f63788"}, + {file = "psycopg_binary-3.2.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d353e028b8f848b9784450fc2abf149d53a738d451eab3ee4c85703438128b9"}, + {file = "psycopg_binary-3.2.1-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f34e369891f77d0738e5d25727c307d06d5344948771e5379ea29c76c6d84555"}, + {file = "psycopg_binary-3.2.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0ab58213cc976a1666f66bc1cb2e602315cd753b7981a8e17237ac2a185bd4a1"}, + {file = "psycopg_binary-3.2.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b0104a72a17aa84b3b7dcab6c84826c595355bf54bb6ea6d284dcb06d99c6801"}, + {file = "psycopg_binary-3.2.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:059cbd4e6da2337e17707178fe49464ed01de867dc86c677b30751755ec1dc51"}, + {file = "psycopg_binary-3.2.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:73f9c9b984be9c322b5ec1515b12df1ee5896029f5e72d46160eb6517438659c"}, + {file = "psycopg_binary-3.2.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:af0469c00f24c4bec18c3d2ede124bf62688d88d1b8a5f3c3edc2f61046fe0d7"}, + {file = "psycopg_binary-3.2.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:463d55345f73ff391df8177a185ad57b552915ad33f5cc2b31b930500c068b22"}, + {file = "psycopg_binary-3.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:302b86f92c0d76e99fe1b5c22c492ae519ce8b98b88d37ef74fda4c9e24c6b46"}, + {file = "psycopg_binary-3.2.1-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:0879b5d76b7d48678d31278242aaf951bc2d69ca4e4d7cef117e4bbf7bfefda9"}, + {file = "psycopg_binary-3.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f99e59f8a5f4dcd9cbdec445f3d8ac950a492fc0e211032384d6992ed3c17eb7"}, + {file = "psycopg_binary-3.2.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84837e99353d16c6980603b362d0f03302d4b06c71672a6651f38df8a482923d"}, + {file = "psycopg_binary-3.2.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ce965caf618061817f66c0906f0452aef966c293ae0933d4fa5a16ea6eaf5bb"}, + {file = "psycopg_binary-3.2.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78c2007caf3c90f08685c5378e3ceb142bafd5636be7495f7d86ec8a977eaeef"}, + {file = "psycopg_binary-3.2.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7a84b5eb194a258116154b2a4ff2962ea60ea52de089508db23a51d3d6b1c7d1"}, + {file = "psycopg_binary-3.2.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:4a42b8f9ab39affcd5249b45cac763ac3cf12df962b67e23fd15a2ee2932afe5"}, + {file = "psycopg_binary-3.2.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:788ffc43d7517c13e624c83e0e553b7b8823c9655e18296566d36a829bfb373f"}, + {file = "psycopg_binary-3.2.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:21927f41c4d722ae8eb30d62a6ce732c398eac230509af5ba1749a337f8a63e2"}, + {file = "psycopg_binary-3.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:921f0c7f39590763d64a619de84d1b142587acc70fd11cbb5ba8fa39786f3073"}, +] + +[[package]] +name = "psycopg-c" +version = "3.2.1" +description = "PostgreSQL database adapter for Python -- C optimisation distribution" +optional = false +python-versions = ">=3.8" +files = [ + {file = "psycopg_c-3.2.1.tar.gz", hash = "sha256:2d09943cc8a855c42c1e23b4298957b7ce8f27bf3683258c52fd139f601f7cda"}, ] +[[package]] +name = "psycopg-pool" +version = "3.2.2" +description = "Connection Pool for Psycopg" +optional = false +python-versions = ">=3.8" +files = [ + {file = "psycopg_pool-3.2.2-py3-none-any.whl", hash = "sha256:273081d0fbfaced4f35e69200c89cb8fbddfe277c38cc86c235b90a2ec2c8153"}, + {file = "psycopg_pool-3.2.2.tar.gz", hash = "sha256:9e22c370045f6d7f2666a5ad1b0caf345f9f1912195b0b25d0d3bcc4f3a7389c"}, +] + +[package.dependencies] +typing-extensions = ">=4.4" + [[package]] name = "py-partiql-parser" version = "0.5.0" @@ -2940,34 +2973,58 @@ files = [ [[package]] name = "rcssmin" -version = "1.1.1" +version = "1.1.2" description = "CSS Minifier" optional = false python-versions = "*" files = [ - {file = "rcssmin-1.1.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:d4e263fa9428704fd94c2cb565c7519ca1d225217943f71caffe6741ab5b9df1"}, - {file = "rcssmin-1.1.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:c7278c1c25bb90d8e554df92cfb3b6a1195004ead50f764653d3093933ee0877"}, - {file = "rcssmin-1.1.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:f15673e97f0a68b4c378c4d15b088fe96d60bc106d278c88829923118833c20f"}, - {file = "rcssmin-1.1.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:d0afc6e7b64ef30d6dcde88830ec1a237b9f16a39f920a8fd159928684ccf8db"}, - {file = "rcssmin-1.1.1-cp310-cp310-manylinux1_i686.whl", hash = "sha256:705c9112d0ed54ea40aecf97e7fd29bdf0f1c46d278a32d8f957f31dde90778a"}, - {file = "rcssmin-1.1.1-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:f7a1fcdbafaacac0530da04edca4a44303baab430ea42e7d59aece4b3f3e9a51"}, - {file = "rcssmin-1.1.1-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:cf74d7ea5e191f0f344b354eed8b7c83eeafbd9a97bec3a579c3d26edf11b005"}, - {file = "rcssmin-1.1.1-cp311-cp311-manylinux1_i686.whl", hash = "sha256:908fe072efd2432fb0975a61124609a8e05021367f6a3463d45f5e3e74c4fdda"}, - {file = "rcssmin-1.1.1-cp311-cp311-manylinux1_x86_64.whl", hash = "sha256:35da6a6999e9e2c5b0e691b42ed56cc479373e0ecab33ef5277dfecce625e44a"}, - {file = "rcssmin-1.1.1-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:e923c105100ab70abde1c01d3196ddd6b07255e32073685542be4e3a60870c8e"}, - {file = "rcssmin-1.1.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:868215e1fd0e92a6122e0ed5973dfc7bb8330fe1e92274d05b2585253b38c0ca"}, - {file = "rcssmin-1.1.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:c7728e3b546b1b6ea08cab721e8e21409dbcc11b881d0b87d10b0be8930af2a2"}, - {file = "rcssmin-1.1.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:271e3d2f8614a6d4637ed8fff3d90007f03e2a654cd9444f37d888797662ba72"}, - {file = "rcssmin-1.1.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:42576d95dfad53d77df2e68dfdec95b89b10fad320f241f1af3ca1438578254a"}, - {file = "rcssmin-1.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:79421230dd67c37ec61ed9892813d2b839b68f2f48ef55c75f976e81701d60b4"}, - {file = "rcssmin-1.1.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:8fcfd10ae2a1c4ce231a33013f2539e07c3836bf17cc945cc25cc30bf8e68e45"}, - {file = "rcssmin-1.1.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:c30f8bc839747b6da59274e0c6e4361915d66532e26448d589cb2b1846d7bf11"}, - {file = "rcssmin-1.1.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:ee386bec6d62f8c814d65c011d604a7c82d24aa3f718facd66e850eea8d6a5a1"}, - {file = "rcssmin-1.1.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:8a26fec3c1e6b7a3765ccbaccc20fbb5c0ed3422cc381e01a2607f08d7621c44"}, - {file = "rcssmin-1.1.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:a04d58a2a21e9a089306d3f99c4b12bf5b656a79c198ef2321e80f8fd9afab06"}, - {file = "rcssmin-1.1.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:914e589f40573035006913861ed2adc28fbe70082a8b6bff5be7ee430b7b5c2e"}, - {file = "rcssmin-1.1.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:a417735d4023d47d048a6288c88dbceadd20abaaf65a11bb4fda1e8458057019"}, - {file = "rcssmin-1.1.1.tar.gz", hash = "sha256:4f9400b4366d29f5f5446f58e78549afa8338e6a59740c73115e9f6ac413dc64"}, + {file = "rcssmin-1.1.2-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:0101a55992ded00220d38ebaf003d0858c1e6e8b365df6f18f9d7a2cdc5574b3"}, + {file = "rcssmin-1.1.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9d7a5c6a08948ae5d7e1422ac34031fc05242786e6b600b37948412ee16f3655"}, + {file = "rcssmin-1.1.2-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:5f602bcaa457680a89904c10d1a539ffae5d4de08ec61a2044efc93c9a743860"}, + {file = "rcssmin-1.1.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:d9384421d8cb516202349da7b6502d77df14860fa710f91dbe0b03b3b895dda3"}, + {file = "rcssmin-1.1.2-cp310-cp310-manylinux1_i686.whl", hash = "sha256:d40964dc7fbd74be30b1ef5543bb75c479a53d53362ecccba3f1a5de6453faca"}, + {file = "rcssmin-1.1.2-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:c9a55657a3d11c5901259b14f941ed7eec118cd5a4416535102eda491de6753f"}, + {file = "rcssmin-1.1.2-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:065b7f85902c87011951b1d084fa694099d70feb6bef1c3e89456a1a0668c73b"}, + {file = "rcssmin-1.1.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b37db48eacf5b8ad09ee14eb25db8cc7176ce2339c1028499547858c152a936a"}, + {file = "rcssmin-1.1.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:f1468ccbfe16a2b4cff5bc0d330488bb496f71b542d26bd2005b2ab05a295752"}, + {file = "rcssmin-1.1.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b3577e4955b89324cd8214ae8e9c2ce452197ea2f789527538cf41e662930495"}, + {file = "rcssmin-1.1.2-cp311-cp311-manylinux1_i686.whl", hash = "sha256:83dd2c3f7b70fae9c97bab99fd7a709dc852f0bc3e7482f8cfb378e0c5f1aed8"}, + {file = "rcssmin-1.1.2-cp311-cp311-manylinux1_x86_64.whl", hash = "sha256:2219d704f27006ecd17ef4b5744cac5f68a276be0889b114130660fb798f3583"}, + {file = "rcssmin-1.1.2-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:a5e758c8db03a57fef847542bf14d96da75913cc0e2495540c018943c1f2d142"}, + {file = "rcssmin-1.1.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a0e353ecbfde3dba0a6579a674fc1dcdc1ff7e47ceff3b29beb6e235b53a981c"}, + {file = "rcssmin-1.1.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:140d1c0ba21fb8c3a2bafae164540a6816e8ec3492bea6ace0c52eebd2cf303f"}, + {file = "rcssmin-1.1.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ef0d472d8f6b8271fef3e388f98085a257703a635556a3ea77287616ab594ea9"}, + {file = "rcssmin-1.1.2-cp312-cp312-manylinux1_i686.whl", hash = "sha256:e58a54e63f79c996284240ec2dfb2992e8b90ad8941b1c70997e256b65940de7"}, + {file = "rcssmin-1.1.2-cp312-cp312-manylinux1_x86_64.whl", hash = "sha256:657324bfb76fa9e97badabe26af97a1622446f33c9073dd0510c7c7e8c7b96da"}, + {file = "rcssmin-1.1.2-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:fdffd58868a752cac0400249ce21bddf55e00d3206f8885d4a3aca1dab487070"}, + {file = "rcssmin-1.1.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:82aad2c3526055956fba537ab067ad937b3f57a1bd13c89bf691fced9c24c89d"}, + {file = "rcssmin-1.1.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:3a860a1f4304e9813900f70fe6b1d429aff73a6fa30ef07f345f68d0a4e33abc"}, + {file = "rcssmin-1.1.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:0451a2c7e7c1ff893ecb7820de862f2ea6336cc69d473ecb0206212e49c817fc"}, + {file = "rcssmin-1.1.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:283b5931eb25e159c0c37bba0717cf3ada2ff77a2827337790cd3921d33ae1eb"}, + {file = "rcssmin-1.1.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4cf7de426fbcce30df31261c1ce84d06fb2ae2f239b0c16ab7e9e083e0462432"}, + {file = "rcssmin-1.1.2-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:47d4d5d67f9ab8f18a6412f8eff843612ebddfff131260fd25a0b273a3d3b1a0"}, + {file = "rcssmin-1.1.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:ce4fce4b3f0cf3621f13f6b8aebbe6c90de3587e383d13dbc7c4c1576f27fda8"}, + {file = "rcssmin-1.1.2-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:3ab50d50ecbfa41d8984b545871df447c90a0c55e16a594e0c97057eb694a7af"}, + {file = "rcssmin-1.1.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:8eddfed4ac175a97eb228a289f820ff1bffcc3db7c63345e4590a49de7051244"}, + {file = "rcssmin-1.1.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:bc2d0ccf449f3f4c385789c9e7898f77fa40f88211cc7d385502f5ea11bc36f4"}, + {file = "rcssmin-1.1.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:754c3cab791da42cabfd430289dd991183d2e97f6c36a758ac4fd83c6998cb77"}, + {file = "rcssmin-1.1.2-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:997d3f6defd707f8a7bd39cedb765cedbf9c790ed0bc1f1c8fb65f4026007021"}, + {file = "rcssmin-1.1.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:9c9688d42654f06535a2a052a0c2f02989b9052149f6cd3263ed320a10379657"}, + {file = "rcssmin-1.1.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:a8856db4529e524a632478d88f34d075ec7e772804257d9b99071b8bdd5f3733"}, + {file = "rcssmin-1.1.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d59a28d6819ad43ed660dfcdf297593f04e8bc227d7abd9548f12a16d6e645f1"}, + {file = "rcssmin-1.1.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:d1c9efc26682ff610194a0c56b0f192355136e066ff78683806778f0bc5e5093"}, + {file = "rcssmin-1.1.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:e5f08c79991c8ee0aa7064aa58ae92b0811d4a84948636ec0219b39d257470aa"}, + {file = "rcssmin-1.1.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:2d39b277a568f6208a4beea237c2d1188bd680deaff39f3742278462645a87ed"}, + {file = "rcssmin-1.1.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2b6a19926f7257ebb7d23040bb0ace05fa43dff7b60d8a9c35bfdf551fa57686"}, + {file = "rcssmin-1.1.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:69283a445483ac439af10ca9f50463c3770ef79a99833af7d5105c75f8ab3c5d"}, + {file = "rcssmin-1.1.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:a8e4c1bfa110d450970df60d904feab71c287e777a3cf522fd20e766483c8ce4"}, + {file = "rcssmin-1.1.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:19e48f7f66a3fa329215a02b3a952737b0b12f9976cd79ff2c236f96960d71d1"}, + {file = "rcssmin-1.1.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:e3ad642a1bfcbbddc3c36ad4c5676ef53267dd7c193c746a2f6670fe5bd03a60"}, + {file = "rcssmin-1.1.2-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:20f8592341960b031967f7ada165e6c8077dfc01eb4092fb970a5a4e64506695"}, + {file = "rcssmin-1.1.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:1c1d9a24132f4c48d080a59c20fce99a4fd9c393eb63f9779f2497ae28992de1"}, + {file = "rcssmin-1.1.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:50576521c1e08e8f137225b8a6e8ae88e867f53dfd82c537616591c67f112aad"}, + {file = "rcssmin-1.1.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:f363c9d242e1afce4fafbbf6bed52452464b19199e66075bed606764f31ca329"}, + {file = "rcssmin-1.1.2.tar.gz", hash = "sha256:bc75eb75bd6d345c0c51fd80fc487ddd6f9fd409dd7861b3fe98dee85018e1e9"}, ] [[package]] @@ -3147,34 +3204,58 @@ six = "*" [[package]] name = "rjsmin" -version = "1.2.1" +version = "1.2.2" description = "Javascript Minifier" optional = false python-versions = "*" files = [ - {file = "rjsmin-1.2.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:35827844d2085bd59d34214dfba6f1fc42a215c455887437b07dbf9c73019cc1"}, - {file = "rjsmin-1.2.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:812af25c08d6a5ae98019a2e1b47ebb47f7469abd351670c353d619eaeae4064"}, - {file = "rjsmin-1.2.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:b8464629a18fe69f70677854c93a3707976024b226a0ce62707c618f923e1346"}, - {file = "rjsmin-1.2.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:bd1faedc425006d9e86b23837d164f01d105b7a8b66b767a9766d0014773db2a"}, - {file = "rjsmin-1.2.1-cp310-cp310-manylinux1_i686.whl", hash = "sha256:99c074cd6a8302ff47118a9c3d086f89328dc8e5c4b105aa1f348fb85c765a30"}, - {file = "rjsmin-1.2.1-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:bc5bc2f94e59bc81562c572b7f1bdd6bcec4f61168dc68a2993bad2d355b6e19"}, - {file = "rjsmin-1.2.1-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:35f21046504544e2941e04190ce24161255479133751550e36ddb3f4af0ecdca"}, - {file = "rjsmin-1.2.1-cp311-cp311-manylinux1_i686.whl", hash = "sha256:ca90630b84fe94bb07739c3e3793e87d30c6ee450dde08653121f0d9153c8d0d"}, - {file = "rjsmin-1.2.1-cp311-cp311-manylinux1_x86_64.whl", hash = "sha256:7dd58b5ed88233bc61dc80b0ed87b93a1786031d9977c70d335221ef1ac5581a"}, - {file = "rjsmin-1.2.1-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:f0895b360dccf7e2d6af8762a52985e3fbaa56778de1bf6b20dbc96134253807"}, - {file = "rjsmin-1.2.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:747bc9d3bc8a220f40858e6aad50b2ae2eb7f69c924d4fa3803b81be1c1ddd02"}, - {file = "rjsmin-1.2.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:f7cd33602ec0f393a0058e883284496bb4dbbdd34e0bbe23b594c8933ddf9b65"}, - {file = "rjsmin-1.2.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:3453ee6d5e7a2723ec45c2909e2382371783400e8d51952b692884c6d850a3d0"}, - {file = "rjsmin-1.2.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:8c340e251619c97571a5ade20f147f1f7e8664f66a2d6d7319e05e3ef6a4423c"}, - {file = "rjsmin-1.2.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:145c6af8df42d8af102d0d39a6de2e5fa66aef9e38947cfb9d65377d1b9940b2"}, - {file = "rjsmin-1.2.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:bbd7a0abaa394afd951f5d4e05249d306fec1c9674bfee179787674dddd0bdb7"}, - {file = "rjsmin-1.2.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:eb770aaf637919b0011c4eb87b9ac6317079fb9800eb17c90dda05fc9de4ebc3"}, - {file = "rjsmin-1.2.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:5d67ec09da46a492186e35cabca02a0d092eda5ef5b408a419b99ee4acf28d5c"}, - {file = "rjsmin-1.2.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:d332e44a1b21ad63401cc7eebc81157e3d982d5fb503bb4faaea5028068d71e9"}, - {file = "rjsmin-1.2.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:113132a40ce7d03b2ced4fac215f0297338ed1c207394b739266efab7831988b"}, - {file = "rjsmin-1.2.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:122aa52bcf7ad9f12728d309012d1308c6ecfe4d6b09ea867a110dcad7b7728c"}, - {file = "rjsmin-1.2.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:8a6710e358c661dcdcfd027e67de3afd72a6af4c88101dcf110de39e9bbded39"}, - {file = "rjsmin-1.2.1.tar.gz", hash = "sha256:1f982be8e011438777a94307279b40134a3935fc0f079312ee299725b8af5411"}, + {file = "rjsmin-1.2.2-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:4420107304ba7a00b5b9b56cdcd166b9876b34e626829fc4552c85d8fdc3737a"}, + {file = "rjsmin-1.2.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:155a2f3312c1f8c6cec7b5080581cafc761dc0e41d64bfb5d46a772c5230ded8"}, + {file = "rjsmin-1.2.2-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:88fcb58d65f88cbfa752d51c1ebe5845553f9706def6d9671e98283411575e3e"}, + {file = "rjsmin-1.2.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:6eae13608b88f4ce32e0557c8fdef58e69bb4d293182202a03e800f0d33b5268"}, + {file = "rjsmin-1.2.2-cp310-cp310-manylinux1_i686.whl", hash = "sha256:81f92fb855fb613ebd04a6d6d46483e71fe3c4f22042dc30dcc938fbd748e59c"}, + {file = "rjsmin-1.2.2-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:897db9bf25538047e9388951d532dc291a629b5d041180a8a1a8c102e9d44b90"}, + {file = "rjsmin-1.2.2-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:5938af8c46734f92f74fdc4d0b6324137c0e09f0a8c3825c83e4cfca1b532e40"}, + {file = "rjsmin-1.2.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0424a7b9096fa2b0ab577c4dc7acd683e6cfb5c718ad39a9fb293cb6cbaba95b"}, + {file = "rjsmin-1.2.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:1714ed93c2bd40c5f970905d2eeda4a6844e09087ae11277d4d43b3e68c32a47"}, + {file = "rjsmin-1.2.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:35596fa6d2d44a5471715c464657123995da78aa6f79bccfbb4b8d6ff7d0a4b4"}, + {file = "rjsmin-1.2.2-cp311-cp311-manylinux1_i686.whl", hash = "sha256:3968667158948355b9a62e9641497aac7ac069c076a595e93199d0fe3a40217a"}, + {file = "rjsmin-1.2.2-cp311-cp311-manylinux1_x86_64.whl", hash = "sha256:d07d14354694f6a47f572f2aa2a1ad74b76723e62a0d2b6df796138b71888247"}, + {file = "rjsmin-1.2.2-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:a78dfa6009235b902454ac53264252b7b94f1e43e3a9e97c4cadae88e409b882"}, + {file = "rjsmin-1.2.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9b7a45001e58243a455d11d2de925cadb8c2a0dc737001de646a0f4d90cf0034"}, + {file = "rjsmin-1.2.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:86c5e657b74b6c9482bb96f18a79d61750f4e8204759cce179f7eb17d395c683"}, + {file = "rjsmin-1.2.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8c2c30b86c7232443a4a726e1bbee34f800556e581e95fc07194ecbf8e02d1d2"}, + {file = "rjsmin-1.2.2-cp312-cp312-manylinux1_i686.whl", hash = "sha256:8982c3ef27fac26dd6b7d0c55ae98fa550fee72da2db010b87211e4b5dd78a67"}, + {file = "rjsmin-1.2.2-cp312-cp312-manylinux1_x86_64.whl", hash = "sha256:3fc27ae4ece99e2c994cd79df2f0d3f7ac650249f632d19aa8ce85118e33bf0f"}, + {file = "rjsmin-1.2.2-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:41113d8d6cae7f7406b30143cc49cc045bbb3fadc2f28df398cea30e1daa60b1"}, + {file = "rjsmin-1.2.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3aa09a89b2b7aa2b9251329fe0c3e36c2dc2f10f78b8811e5be92a072596348b"}, + {file = "rjsmin-1.2.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:5abb8d1241f4ea97950b872fa97a422ba8413fe02358f64128ff0cf745017f07"}, + {file = "rjsmin-1.2.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5abc686a9ef7eaf208f9ad1fb5fb949556ecb7cc1fee27290eb7f194e01d97bd"}, + {file = "rjsmin-1.2.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:076adcf04c34f712c9427fd9ba6a75bbf7aab975650dfc78cbdd0fbdbe49ca63"}, + {file = "rjsmin-1.2.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:8cb8947ddd250fce58261b0357846cd5d55419419c0f7dfb131dc4b733579a26"}, + {file = "rjsmin-1.2.2-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:9069c48b6508b9c5b05435e2c6042c2a0e2f97b35d7b9c27ceaea5fd377ffdc5"}, + {file = "rjsmin-1.2.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:02b61cf9b6bc518fdac667f3ca3dab051cb8bd1bf4cba28b6d29153ec27990ad"}, + {file = "rjsmin-1.2.2-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:09eca8581797244587916e5e07e36c4c86d54a4b7e5c7697484a95b75803515d"}, + {file = "rjsmin-1.2.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:c52b9dd45c837f1c5c2e8d40776f9e63257f8dbd5f79b85f648cc70da6c1e4e9"}, + {file = "rjsmin-1.2.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:4fe4ce990412c053a6bcd47d55133927e22fd3d100233d73355f60f9053054c5"}, + {file = "rjsmin-1.2.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:aa883b9363b5134239066060879d5eb422a0d4ccf24ccf871f65a5b34c64926f"}, + {file = "rjsmin-1.2.2-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:6f4e95c5ac95b4cbb519917b3aa1d3d92fc6939c371637674c4a42b67b2b3f44"}, + {file = "rjsmin-1.2.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:ae3cd64e18e62aa330b24dd6f7b9809ce0a694afd1f01fe99c21f9acd1cb0ea6"}, + {file = "rjsmin-1.2.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:7999d797fcf805844d2d91598651785497249f592f31674da0964e794b3be019"}, + {file = "rjsmin-1.2.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e733fea039a7b5ad7c06cc8bf215ee7afac81d462e273b3ab55c1ccc906cf127"}, + {file = "rjsmin-1.2.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:ccca74461bd53a99ff3304fcf299ea861df89846be3207329cb82d717ce47ea6"}, + {file = "rjsmin-1.2.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:88f59ad24f91bf9c25d5c2ca3c84a72eed0028f57a98e3b85a915ece5c25be1e"}, + {file = "rjsmin-1.2.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:7a8b56fbd64adcc4402637f0e07b90b441e9981d720a10eb6265118018b42682"}, + {file = "rjsmin-1.2.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2c24686cfdf86e55692183f7867e72c9e982add479c244eda7b8390f96db2c6c"}, + {file = "rjsmin-1.2.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6c0d9f9ea8d9cd48cbcdc74a1c2e85d4d588af12bb8f0b672070ae7c9b6e6306"}, + {file = "rjsmin-1.2.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:27abd32c9f5b6e0c0a3bcad43e8e24108c6d6c13a4e6c50c97497ea2b4614bb4"}, + {file = "rjsmin-1.2.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:e0e009f6f8460901f5144b34ac2948f94af2f9b8c9b5425da705dbc8152c36c2"}, + {file = "rjsmin-1.2.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:41e6013cb37a5b3563c19aa35f8e659fa536aa4197a0e3b6a57a381638294a15"}, + {file = "rjsmin-1.2.2-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:62cbd38c9f5090f0a6378a45c415b4f96ae871216cedab0dfa21965620c0be4c"}, + {file = "rjsmin-1.2.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2fd5254d36f10a17564b63e8bf9ac579c7b5f211364e11e9753ff5b562843c67"}, + {file = "rjsmin-1.2.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6cf0309d001a0d45d731dbaab1afd0c23d135c9e029fe56c935c1798094686fc"}, + {file = "rjsmin-1.2.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:bfbe333dab8d23f0a71da90e2d8e8b762a739cbd55a6f948b2dfda089b6d5853"}, + {file = "rjsmin-1.2.2.tar.gz", hash = "sha256:8c1bcd821143fecf23242012b55e13610840a839cd467b358f16359010d62dae"}, ] [[package]] @@ -3652,20 +3733,20 @@ watchdog = ["watchdog (>=2.3)"] [[package]] name = "wiki" -version = "0.11.1" +version = "0.11.2" description = "A wiki system written for the Django framework." optional = false python-versions = ">=3.9" files = [ - {file = "wiki-0.11.1-py3-none-any.whl", hash = "sha256:73a3ae7c9cfb7848cd1f5df86abfe40097f8364d301defce742f29a31de0254c"}, - {file = "wiki-0.11.1.tar.gz", hash = "sha256:42e389ee9d8c3fc3c83179adfc7d511c44a407879f30f5a73e2804f3c5a9d602"}, + {file = "wiki-0.11.2-py3-none-any.whl", hash = "sha256:3430efaad03795eb9b0392f5f28e543902afa135a1182bd105f2cbfebbeb20aa"}, + {file = "wiki-0.11.2.tar.gz", hash = "sha256:69cc348ff7c3c3936d66a2bd47a6b286c2e75ef04140b4c9c64497736c1594d7"}, ] [package.dependencies] bleach = {version = ">=6,<7", extras = ["css"]} -django = ">=3.2,<5.1" +django = ">=3.2,<5.2" django-mptt = ">=0.13,<0.17" -django-nyt = ">=1.4,<1.5" +django-nyt = ">=1.4.1,<1.5" django-sekizai = ">=0.10" markdown = ">=3.4,<3.7" pillow = "*" @@ -3875,4 +3956,4 @@ tests = ["hypothesis", "moto", "pytest", "pytest-cov", "pytest-django", "pytest- [metadata] lock-version = "2.0" python-versions = "^3.10.0" -content-hash = "cca73bd296e27c132739ea7cd69d18f8a4ab19c6bc8e7deada8e0c47962bd0b5" +content-hash = "e75664cab7e8e181df0486ea23892908f2b08001e27cd7c5f4a6e7a76058ef08" diff --git a/pyproject.toml b/pyproject.toml index b76034bd..78eaf60c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,27 +18,27 @@ packages = [] [tool.poetry.dependencies] python = "^3.10.0" -django = "==5.0.2" -django-celery-beat = "==2.6.0" -django-compressor = "==4.4" -django-cors-headers = "==4.3.1" +django = "==5.1.1" +django-celery-beat = "==2.7.0" +django-compressor = "==4.5.1" +django-cors-headers = "==4.4.0" django-extensions = "==3.2.3" -django-filter = "==24.2" +django-filter = "==24.3" django-tagulous = "==1.3.3" django-guardian = "==2.4.0" -djangorestframework = "== 3.15.1" -django-crispy-forms = "==2.1" +djangorestframework = "== 3.15.2" +django-crispy-forms = "==2.3" crispy-bootstrap5 = "==2024.2" # django-wiki -wiki = "==0.11.1" +wiki = "==0.11.2" # Other Python and project-related libraries flower = "==2.0.1" requests = "==2.31.0" amqp = "==5.1.1" -psycopg2-binary = "==2.9.9" +psycopg = {extras = ["binary", "pool", "c"], version = "^3.2.1"} redis = "==5.0.1" watchdog = "==3.0.0" drf-nested-routers = "==0.93.4" diff --git a/studio/settings.py b/studio/settings.py index 9da6d630..185a7f58 100644 --- a/studio/settings.py +++ b/studio/settings.py @@ -170,7 +170,14 @@ if TESTING: DATABASES = { "default": { - "ENGINE": "django.db.backends.postgresql_psycopg2", + "ENGINE": "django.db.backends.postgresql", + "OPTIONS": { + "pool": { + "min_size": 2, + "max_size": 4, + "timeout": 10, + } + }, "NAME": "postgres", "USER": "postgres", "PASSWORD": "postgres", @@ -181,7 +188,14 @@ else: DATABASES = { "default": { - "ENGINE": "django.db.backends.postgresql_psycopg2", + "ENGINE": "django.db.backends.postgresql", + "OPTIONS": { + "pool": { + "min_size": 2, + "max_size": 4, + "timeout": 10, + } + }, "NAME": "postgres", "USER": "postgres", "PASSWORD": "postgres", From 13fd4f49dd0a654c68ec1ff25538ecd67f3aa5b7 Mon Sep 17 00:00:00 2001 From: Arnold Kochari Date: Mon, 16 Sep 2024 13:47:14 +0200 Subject: [PATCH 15/19] SS-674 add event announcements functionality (#225) --- portal/admin.py | 19 ++++- portal/migrations/0002_eventsobject.py | 27 +++++++ portal/models.py | 44 ++++++++++ portal/urls.py | 7 +- portal/views.py | 41 +++++++--- projects/admin.py | 7 +- templates/common/footer.html | 15 ++-- templates/events/events.html | 106 +++++++++++++++++++++++++ templates/portal/home.html | 54 ++++++++++++- 9 files changed, 298 insertions(+), 22 deletions(-) create mode 100644 portal/migrations/0002_eventsobject.py create mode 100644 templates/events/events.html diff --git a/portal/admin.py b/portal/admin.py index 4a3d217a..a74aea6a 100644 --- a/portal/admin.py +++ b/portal/admin.py @@ -1,6 +1,12 @@ from django.contrib import admin -from .models import Collection, NewsObject, PublicModelObject, PublishedModel +from .models import ( + Collection, + EventsObject, + NewsObject, + PublicModelObject, + PublishedModel, +) class CollectionAdmin(admin.ModelAdmin): @@ -12,7 +18,16 @@ def connected_apps(self, obj): return app_list or "No apps connected" +class EventsAdmin(admin.ModelAdmin): + list_display = ("title", "start_time") + + +class NewsAdmin(admin.ModelAdmin): + list_display = ("title", "created_on") + + admin.site.register(Collection, CollectionAdmin) -admin.site.register(NewsObject) +admin.site.register(NewsObject, NewsAdmin) +admin.site.register(EventsObject, EventsAdmin) admin.site.register(PublishedModel) admin.site.register(PublicModelObject) diff --git a/portal/migrations/0002_eventsobject.py b/portal/migrations/0002_eventsobject.py new file mode 100644 index 00000000..92337940 --- /dev/null +++ b/portal/migrations/0002_eventsobject.py @@ -0,0 +1,27 @@ +# Generated by Django 5.0.2 on 2024-09-11 15:31 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("portal", "0001_initial"), + ] + + operations = [ + migrations.CreateModel( + name="EventsObject", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("created_on", models.DateTimeField(auto_now_add=True)), + ("title", models.CharField(default="", max_length=200)), + ("description", models.TextField(blank=True, default="", max_length=2024, null=True)), + ("start_time", models.DateTimeField()), + ("end_time", models.DateTimeField()), + ("venue", models.CharField(default="", max_length=100)), + ("speaker", models.CharField(default="", max_length=200)), + ("registration_url", models.URLField()), + ("recording_url", models.URLField(blank=True, null=True)), + ], + ), + ] diff --git a/portal/models.py b/portal/models.py index f8f2697f..b43d27e2 100644 --- a/portal/models.py +++ b/portal/models.py @@ -98,3 +98,47 @@ def save(self, *args, **kwargs): if not self.slug: self.slug = slugify(self.name) super(Collection, self).save(*args, **kwargs) + + +class EventsObject(models.Model): + created_on = models.DateTimeField(auto_now_add=True) + title = models.CharField(max_length=200, default="") + description = models.TextField(blank=True, null=True, default="", max_length=2024) + start_time = models.DateTimeField(auto_now=False, auto_now_add=False) + end_time = models.DateTimeField(auto_now=False, auto_now_add=False) + venue = models.CharField(max_length=100, default="") + speaker = models.CharField(max_length=200, default="") + registration_url = models.URLField(max_length=200) + recording_url = models.URLField(blank=True, null=True, max_length=200) + + @property + def event_title(self): + return self.title + + @property + def event_description(self): + return self.description + + @property + def event_start_time(self): + return self.start_time + + @property + def event_end_time(self): + return self.end_time + + @property + def event_venue(self): + return self.venue + + @property + def event_speaker(self): + return self.speaker + + @property + def event_registration_url(self): + return self.registration_url + + @property + def event_recording_url(self): + return self.recording_url diff --git a/portal/urls.py b/portal/urls.py index 1381effd..776f691b 100644 --- a/portal/urls.py +++ b/portal/urls.py @@ -12,8 +12,9 @@ path("teaching/", views.teaching, name="teaching"), path("privacy/", views.privacy, name="privacy"), path("apps/", views.public_apps, name="apps"), - path("news/", views.news, name="news"), - path("collections/", views.index, name="collections_index"), - path("collections//", views.collection, name="collection"), + path("news/", views.get_news, name="news"), + path("events/", views.get_events, name="events"), + path("collections/", views.get_collections_index, name="collections_index"), + path("collections//", views.get_collection, name="collection"), path("", views.HomeViewDynamic.as_view(), name="home-dynamic"), ] diff --git a/portal/views.py b/portal/views.py index f5f47a41..eef646ac 100644 --- a/portal/views.py +++ b/portal/views.py @@ -3,12 +3,13 @@ from django.conf import settings from django.db.models import Q from django.shortcuts import get_object_or_404, redirect, render +from django.utils import timezone from django.views.generic import View from apps.models import BaseAppInstance, SocialMixin from studio.utils import get_logger -from .models import NewsObject +from .models import EventsObject, NewsObject logger = get_logger(__name__) @@ -144,30 +145,40 @@ class HomeView(View): def get(self, request, app_id=0): published_apps, request = get_public_apps(request, app_id=app_id, get_all=False) published_models = PublishedModel.objects.all() - news_objects = NewsObject.objects.all().order_by("-created_on") - for news in news_objects: - news.body_html = markdown.markdown(news.body) - link_all_news = False if published_models.count() >= 3: published_models = published_models[:3] else: published_models = published_models + news_objects = NewsObject.objects.all().order_by("-created_on") + link_all_news = False if news_objects.count() > 3: news_objects = news_objects[:3] link_all_news = True else: news_objects = news_objects + for news in news_objects: + news.body_html = markdown.markdown(news.body) collection_objects = Collection.objects.all().order_by("-created_on") link_all_collections = False - if collection_objects.count() > 3: collection_objects = collection_objects[:3] link_all_collections = True else: collection_objects = collection_objects + events_objects = EventsObject.objects.all().order_by("-start_time") + link_all_events = False + if events_objects.count() > 3: + link_all_events = True + events_objects = events_objects[:3] + else: + events_objects = events_objects + for event in events_objects: + event.description_html = markdown.markdown(event.description) + event.past = True if event.start_time.date() < timezone.now().date() else False + context = { "published_apps": published_apps, "published_models": published_models, @@ -175,6 +186,8 @@ def get(self, request, app_id=0): "link_all_news": link_all_news, "collection_objects": collection_objects, "link_all_collections": link_all_collections, + "events_objects": events_objects, + "link_all_events": link_all_events, } return render(request, self.template, context=context) @@ -205,14 +218,14 @@ def privacy(request): return render(request, template, locals()) -def news(request): +def get_news(request): news_objects = NewsObject.objects.all().order_by("-created_on") for news in news_objects: news.body_html = markdown.markdown(news.body) return render(request, "news/news.html", {"news_objects": news_objects}) -def index(request): +def get_collections_index(request): template = "collections/index.html" collection_objects = Collection.objects.all().order_by("-created_on") @@ -222,7 +235,7 @@ def index(request): return render(request, template, context=context) -def collection(request, slug, app_id=0): +def get_collection(request, slug, app_id=0): template = "collections/collection.html" collection = get_object_or_404(Collection, slug=slug) @@ -236,3 +249,13 @@ def collection(request, slug, app_id=0): } return render(request, template, context=context) + + +def get_events(request): + future_events = EventsObject.objects.filter(start_time__date__gte=timezone.now().date()).order_by("-start_time") + for event in future_events: + event.description_html = markdown.markdown(event.description) + past_events = EventsObject.objects.filter(start_time__date__lt=timezone.now().date()).order_by("-start_time") + for event in past_events: + event.description_html = markdown.markdown(event.description) + return render(request, "events/events.html", {"future_events": future_events, "past_events": past_events}) diff --git a/projects/admin.py b/projects/admin.py index 5d0d8301..db5a65d6 100644 --- a/projects/admin.py +++ b/projects/admin.py @@ -4,7 +4,6 @@ from .models import BasicAuth, Environment, Flavor, Project, ProjectLog, ProjectTemplate admin.site.register(ProjectTemplate) -admin.site.register(Environment) admin.site.register(ProjectLog) admin.site.register(BasicAuth) @@ -24,3 +23,9 @@ def update_app_limits(self, request, queryset): class FlavorAdmin(admin.ModelAdmin): list_display = ("name", "project", "updated_at") list_filter = ["project"] + + +@admin.register(Environment) +class EnvironmentAdmin(admin.ModelAdmin): + list_display = ("name", "project", "updated_at") + list_filter = ["project"] diff --git a/templates/common/footer.html b/templates/common/footer.html index 7af05e86..7a8868ba 100644 --- a/templates/common/footer.html +++ b/templates/common/footer.html @@ -19,20 +19,25 @@ {% endfor %}
              -
              + + -
              +

              SciLifeLab Serve (beta) is developed and operated by the SciLifeLab Data Centre. SciLifeLab Serve is free to use for all life science researchers affiliated with a Swedish research institution and their collaborators. The service is hosted on a Kubernetes cluster. The code behind SciLifeLab Serve is available on Github.

              Please email serve@scilifelab.se with any questions.

              diff --git a/templates/events/events.html b/templates/events/events.html new file mode 100644 index 00000000..bf400449 --- /dev/null +++ b/templates/events/events.html @@ -0,0 +1,106 @@ +{% extends 'base.html' %} + +{% block title %}Events{% endblock %} +{% load static %} +{% load custom_tags %} + +{% block content %} + + +

              +
              +
              +

              Events

              +

              Below you can find an overview of events organized by the SciLifeLab Serve team. Life science researchers in Sweden is our primary focus but some webinars may also be interesting for an international audience. Our events are open to everyone (regardless of affiliation) so feel free to sign up and attend. However, please pay attention to the described target audience of each event as some of them are prepared for groups with specific prior knowledge. When possible, we try to record our webinars and provide links to the recorded video afterwards on this page.

              +

              If you have any questions about the events below or if you would like to collaborate with us on organising an event, feel free to get in touch with us (serve@scilifelab.se).

              +
              +
              + +
              +
              +

              Upcoming events

              +
              +

              +
              + + {% if future_events %} + {% for event in future_events %} +
              +
              +
              +
              +
              {{ event.start_time|date:"j" }}
              +
              {{ event.start_time|date:"M" }}
              +
              +
              +
              +
              +
              {{ event.title }}
              + {% if event.description_html %}

              {{ event.description_html|safe }}

              {% endif %} +

              Date and time: + {% if event.start_time|date:"Y-m-d" == event.end_time|date:"Y-m-d" %} + {{ event.start_time|date:"M. j, Y" }} {{ event.start_time|date:"H:i" }}-{{ event.end_time|date:"H:i" }} + {% else %} + {{ event.start_time|date:"M. j, Y, H:i" }} - {{ event.end_time|date:"M. j, Y, H:i" }} + {% endif %} +

              + {% if event.speaker %}

              Speaker(s): {{ event.speaker }}

              {% endif %} +

              Venue: {{ event.venue }}

              + Register for this event +
              +
              +
              +
              + {% endfor %} + {% else %} +
              +
              + There are no upcoming events at this point. +
              +
              + {% endif %} + +
              +
              +

              Past events

              +
              +

              +
              + + {% if past_events %} + {% for event in past_events %} +
              +
              +
              +
              +
              {{ event.start_time|date:"j" }}
              +
              {{ event.start_time|date:"M" }}
              +
              +
              +
              +
              +
              {{ event.title }}
              + {% if event.description_html %}

              {{ event.description_html|safe }}

              {% endif %} +

              Date and time: + {% if event.start_time|date:"Y-m-d" == event.end_time|date:"Y-m-d" %} + {{ event.start_time|date:"M. j, Y" }} {{ event.start_time|date:"H:i" }}-{{ event.end_time|date:"H:i" }} + {% else %} + {{ event.start_time|date:"M. j, Y, H:i" }} - {{ event.end_time|date:"M. j, Y, H:i" }} + {% endif %} +

              Venue: {{ event.venue }}

              + {% if event.recording_url %}See the recording{% endif %} +
              +
              +
              +
              + {% endfor %} + {% else %} +
              +
              + There are no past events. +
              +
              + {% endif %} + +
              +{% endblock %} diff --git a/templates/portal/home.html b/templates/portal/home.html index 7e507251..7b382748 100644 --- a/templates/portal/home.html +++ b/templates/portal/home.html @@ -174,12 +174,63 @@

              Collections

              {% endif %} + {% if events_objects %} + + {% endif %} + {% if news_objects %}

              News

              -
              +
              {% for news in news_objects %} @@ -209,6 +260,5 @@
              {{ news.title }}
              {% endif %} -
              {% endblock %} From c9ebece6e4053e95344e2cb354eb5c09db63d0c3 Mon Sep 17 00:00:00 2001 From: Nikita Churikov <8545082+churnikov@users.noreply.github.com> Date: Mon, 16 Sep 2024 14:37:45 +0200 Subject: [PATCH 16/19] Update apps_fixtures.json to bump version of shinyproxy --- fixtures/apps_fixtures.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fixtures/apps_fixtures.json b/fixtures/apps_fixtures.json index 9cf122b4..5f599aef 100644 --- a/fixtures/apps_fixtures.json +++ b/fixtures/apps_fixtures.json @@ -383,7 +383,7 @@ { "fields": { "category": "serve", - "chart": "ghcr.io/scilifelabdatacentre/serve-charts/shinyproxy:1.3.1", + "chart": "ghcr.io/scilifelabdatacentre/serve-charts/shinyproxy:1.4.0", "created_on": "2023-08-25T21:34:37.815Z", "description": "", "logo": "shinyapp-logo.svg", From 26f21593c36543e120fceb31978293ad8ef7c84e Mon Sep 17 00:00:00 2001 From: Nikita Churikov <8545082+churnikov@users.noreply.github.com> Date: Tue, 17 Sep 2024 11:24:54 +0200 Subject: [PATCH 17/19] Change display of prepend text for `shiny_site_dir` (#230) Signed-off-by: Nikita Churikov --- apps/forms/shiny.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/forms/shiny.py b/apps/forms/shiny.py index e3c4b812..215d745c 100644 --- a/apps/forms/shiny.py +++ b/apps/forms/shiny.py @@ -59,7 +59,7 @@ def _setup_form_helper(self): "Advanced settings", PrependedText( "shiny_site_dir", - "/srv/shiny-server", + "/srv/shiny-server/", template="apps/partials/srv_prepend_append_input_group.html", ), active=False, From 8aa75450472d6b5ef36616fed1df90e6fa35ffb7 Mon Sep 17 00:00:00 2001 From: Hamza Date: Wed, 18 Sep 2024 14:10:34 +0200 Subject: [PATCH 18/19] SS-1130 Add access to redeployment fields (#231) --- apps/helpers.py | 2 +- fixtures/apps_fixtures.json | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/apps/helpers.py b/apps/helpers.py index 52fa116f..775d5454 100644 --- a/apps/helpers.py +++ b/apps/helpers.py @@ -261,7 +261,7 @@ def create_instance_from_form(form, project, app_slug, app_id=None): do_deploy = True else: # Only re-deploy existing apps if one of the following fields was changed: - redeployment_fields = ["subdomain", "volume", "path", "flavor", "port", "image"] + redeployment_fields = ["subdomain", "volume", "path", "flavor", "port", "image", "access"] logger.debug(f"An existing app has changed. The changed form fields: {form.changed_data}") # Because not all forms contain all fields, we check if the supposedly changed field diff --git a/fixtures/apps_fixtures.json b/fixtures/apps_fixtures.json index 5f599aef..9c528833 100644 --- a/fixtures/apps_fixtures.json +++ b/fixtures/apps_fixtures.json @@ -2,7 +2,7 @@ { "fields": { "category": "develop", - "chart": "ghcr.io/scilifelabdatacentre/serve-charts/lab:1.0.1", + "chart": "ghcr.io/scilifelabdatacentre/serve-charts/lab:1.0.2", "created_on": "2021-02-19T21:34:37.815Z", "description": "", "logo": "jupyter-lab-logo.svg", @@ -273,7 +273,7 @@ { "fields": { "category": "develop", - "chart": "ghcr.io/scilifelabdatacentre/serve-charts/vscode:1.0.0", + "chart": "ghcr.io/scilifelabdatacentre/serve-charts/vscode:1.0.1", "created_on": "2021-02-19T21:34:37.815Z", "description": "", "logo": "vscode-logo.svg", @@ -311,7 +311,7 @@ { "fields": { "category": "serve", - "chart": "ghcr.io/scilifelabdatacentre/serve-charts/custom-app:1.0.1", + "chart": "ghcr.io/scilifelabdatacentre/serve-charts/custom-app:1.0.2", "created_on": "2023-08-25T21:34:37.815Z", "description": "Apps built with Gradio, Streamlit, Flask, etc.", "logo": "default-logo.svg", @@ -329,7 +329,7 @@ { "fields": { "category": "develop", - "chart": "ghcr.io/scilifelabdatacentre/serve-charts/rstudio:1.0.1", + "chart": "ghcr.io/scilifelabdatacentre/serve-charts/rstudio:1.0.2", "created_on": "2023-08-31T09:30:00.000Z", "description": "", "logo": "rstudio-logo.svg", @@ -347,7 +347,7 @@ { "fields": { "category": "serve", - "chart": "ghcr.io/scilifelabdatacentre/serve-charts/dash-app:1.0.1", + "chart": "ghcr.io/scilifelabdatacentre/serve-charts/dash-app:1.0.2", "created_on": "2023-08-25T21:34:37.815Z", "description": "", "logo": "dashapp-logo.svg", @@ -365,7 +365,7 @@ { "fields": { "category": "serve", - "chart": "ghcr.io/scilifelabdatacentre/serve-charts/shinyapp:1.0.2", + "chart": "ghcr.io/scilifelabdatacentre/serve-charts/shinyapp:1.0.3", "created_on": "2023-08-25T21:34:37.815Z", "description": "", "logo": "shinyapp-logo.svg", @@ -383,7 +383,7 @@ { "fields": { "category": "serve", - "chart": "ghcr.io/scilifelabdatacentre/serve-charts/shinyproxy:1.4.0", + "chart": "ghcr.io/scilifelabdatacentre/serve-charts/shinyproxy:1.4.1", "created_on": "2023-08-25T21:34:37.815Z", "description": "", "logo": "shinyapp-logo.svg", @@ -470,7 +470,7 @@ { "fields": { "category": "serve", - "chart": "ghcr.io/scilifelabdatacentre/serve-charts/tissuumaps:1.0.1", + "chart": "ghcr.io/scilifelabdatacentre/serve-charts/tissuumaps:1.0.2", "created_on": "2023-08-25T21:34:37.815Z", "description": "App to visualise and explore data using TissUUmaps.", "logo": "tissuumaps-logo.svg", From 74efddbca2add1ba9f086c6b4c4e39fce1616164 Mon Sep 17 00:00:00 2001 From: Nikita Churikov <8545082+churnikov@users.noreply.github.com> Date: Wed, 18 Sep 2024 14:47:47 +0200 Subject: [PATCH 19/19] SS-1125 Added environment support for rstudio (#232) Signed-off-by: Nikita Churikov --- apps/forms/rstudio.py | 15 +++++++- .../0012_rstudioinstance_environment.py | 21 +++++++++++ apps/models/app_types/rstudio.py | 4 ++ fixtures/projects_templates.json | 5 +++ ...add_rstudio_environment_to_old_projects.py | 37 +++++++++++++++++++ 5 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 apps/migrations/0012_rstudioinstance_environment.py create mode 100644 projects/migrations/0004_add_rstudio_environment_to_old_projects.py diff --git a/apps/forms/rstudio.py b/apps/forms/rstudio.py index ab80037d..c4ac4a86 100644 --- a/apps/forms/rstudio.py +++ b/apps/forms/rstudio.py @@ -1,5 +1,6 @@ from crispy_forms.layout import HTML, Div, Field, Layout from django import forms +from django.utils.safestring import mark_safe from apps.forms.base import AppBaseForm from apps.forms.field.common import SRVCommonDivField @@ -10,6 +11,17 @@ class RStudioForm(AppBaseForm): volume = forms.ModelMultipleChoiceField(queryset=VolumeInstance.objects.none(), required=False) + environment = forms.ModelChoiceField(queryset=None, required=True, empty_label=None) + + def _setup_form_fields(self): + super()._setup_form_fields() + self.fields["environment"].label = "Environment" + self.fields["environment"].queryset = self.project.environment_set.filter(app__slug="rstudio") + self.fields["environment"].help_text = mark_safe( + "Select the environment to run the app in. " + "Read more about environments in the " + 'documentation.' + ) def _setup_form_helper(self): super()._setup_form_helper() @@ -18,6 +30,7 @@ def _setup_form_helper(self): Field("volume"), SRVCommonDivField("flavor"), SRVCommonDivField("access"), + SRVCommonDivField("environment"), css_class="card-body", ) @@ -25,4 +38,4 @@ def _setup_form_helper(self): class Meta: model = RStudioInstance - fields = ["name", "volume", "flavor", "access"] + fields = ["name", "volume", "flavor", "access", "environment"] diff --git a/apps/migrations/0012_rstudioinstance_environment.py b/apps/migrations/0012_rstudioinstance_environment.py new file mode 100644 index 00000000..bc40a6cf --- /dev/null +++ b/apps/migrations/0012_rstudioinstance_environment.py @@ -0,0 +1,21 @@ +# Generated by Django 5.0.2 on 2024-09-18 09:49 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("apps", "0011_jupyterinstance_environment"), + ("projects", "0003_alter_environment_rename_jupyter_lab_to_default_jupyter"), + ] + + operations = [ + migrations.AddField( + model_name="rstudioinstance", + name="environment", + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, to="projects.environment" + ), + ), + ] diff --git a/apps/models/app_types/rstudio.py b/apps/models/app_types/rstudio.py index 64a60bd0..58b62296 100644 --- a/apps/models/app_types/rstudio.py +++ b/apps/models/app_types/rstudio.py @@ -1,6 +1,7 @@ from django.db import models from apps.models import AppInstanceManager, BaseAppInstance +from projects.models import Environment class RStudioInstanceManager(AppInstanceManager): @@ -15,6 +16,7 @@ class RStudioInstance(BaseAppInstance): ) volume = models.ManyToManyField("VolumeInstance", blank=True) access = models.CharField(max_length=20, default="project", choices=ACCESS_TYPES) + environment: Environment = models.ForeignKey(Environment, on_delete=models.DO_NOTHING, null=True, blank=True) def get_k8s_values(self): k8s_values = super().get_k8s_values() @@ -30,6 +32,8 @@ def get_k8s_values(self): for object in self.volume.all(): volumeK8s_dict["volumeK8s"][object.name] = dict(release=object.subdomain.subdomain) k8s_values["apps"] = volumeK8s_dict + if self.environment: + k8s_values["appconfig"] = {"image": self.environment.get_full_image_reference()} # This is just do fix a legacy. # TODO: Change the rstdio chart to fetch port from appconfig as other apps diff --git a/fixtures/projects_templates.json b/fixtures/projects_templates.json index 5b5e7208..15145f80 100644 --- a/fixtures/projects_templates.json +++ b/fixtures/projects_templates.json @@ -18,6 +18,11 @@ "image": "serve-jupyterlab:231030-1145", "repository": "ghcr.io/scilifelabdatacentre" }, + "Default RStudio": { + "app": "rstudio", + "image": "serve-rstudio:231030-1146", + "repository": "ghcr.io/scilifelabdatacentre" + }, "MLflow Serving": { "app": "mlflow-serve", "image": "serve-mlflow:231030-1149", diff --git a/projects/migrations/0004_add_rstudio_environment_to_old_projects.py b/projects/migrations/0004_add_rstudio_environment_to_old_projects.py new file mode 100644 index 00000000..75023d90 --- /dev/null +++ b/projects/migrations/0004_add_rstudio_environment_to_old_projects.py @@ -0,0 +1,37 @@ +# Written manually by Nikita Churikov on 2024-09-18 + +from django.db import migrations + + +# create a new "Default Rstudio" environment in every project that does not have it +def create_default_rstudio_environments(apps, schema_editor): + Project = apps.get_model("projects", "Project") + Environment = apps.get_model("projects", "Environment") + AppsTemplate = apps.get_model("apps", "Apps") + # check if RStudio app template exists. It doesn't exist on the first migration + if AppsTemplate.objects.filter(name="RStudio").exists(): + RStudioTemplate = AppsTemplate.objects.get(name="RStudio") + RStudioInstance = apps.get_model("apps", "RStudioInstance") + for project in Project.objects.all(): + env = Environment.objects.create( + app=RStudioTemplate, + project=project, + name="Default RStudio", + image="serve-rstudio:231030-1146", + repository="ghcr.io/scilifelabdatacentre", + ) + env.save() + for rstudio_instance in RStudioInstance.objects.filter(project=project): + rstudio_instance.environment = env + rstudio_instance.save() + + +class Migration(migrations.Migration): + dependencies = [ + ("projects", "0003_alter_environment_rename_jupyter_lab_to_default_jupyter"), + ("apps", "0012_rstudioinstance_environment"), + ] + + operations = [ + migrations.RunPython(create_default_rstudio_environments), + ]