diff --git a/.flake8 b/.flake8 index 6aca6ccf..818bf3e3 100644 --- a/.flake8 +++ b/.flake8 @@ -1,3 +1,3 @@ [flake8] max-line-length = 120 -ignore = D100 +ignore = D100, W503 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..eb7a18b8 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,16 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v3.2.0 + hooks: + - id: check-yaml + exclude: conda/meta.yaml + + - repo: https://github.com/psf/black + rev: 19.10b0 + hooks: + - id: black + + - repo: https://gitlab.com/pycqa/flake8 + rev: 3.7.9 + hooks: + - id: flake8 \ No newline at end of file diff --git a/Jenkinsfile b/Jenkinsfile index 3f5b46b0..5fde6ee1 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -4,6 +4,9 @@ pipeline { registryCredential = "dockerhub-inriachile" dockerImageName = "lsstts/love-manager:" dockerImage = "" + user_ci = credentials('lsst-io') + LTD_USERNAME="${user_ci_USR}" + LTD_PASSWORD="${user_ci_PSW}" } stages { @@ -72,6 +75,31 @@ pipeline { } } + stage("Deploy documentation") { + agent { + docker { + alwaysPull true + image 'lsstts/develop-env:develop' + args "-u root --entrypoint=''" + } + } + when { + anyOf { + changeset "docs/*" + } + } + steps { + script { + sh "pwd" + sh """ + source /home/saluser/.setup_dev.sh + pip install ltd-conveyor + ltd upload --product love-manager --git-ref ${GIT_BRANCH} --dir ./docs + """ + } + } + } + stage("Trigger develop deployment") { when { branch "develop" diff --git a/README.md b/README.md index b63480b6..02b70e8e 100644 --- a/README.md +++ b/README.md @@ -58,3 +58,18 @@ Once inside the container you will be in the `/usr/src/love/manager` folder, whe cd ../docsrc ./create_docs.sh ``` + +### Linting & Formatting +In order to maintaing code linting and formatting we use `pre-commit` that runs **Flake8** (https://flake8.pycqa.org/) and **Black** (https://github.com/psf/black) using Git Hooks. To enable this you have to: + +1. Install `pre-commit` in your local development environment: +``` +pip install pre-commit +``` + +2. Set up the git hook scripts running: +``` +pre-commit install +``` + +3. Start developing! Linter and Formatter will be executed on every commit you make \ No newline at end of file diff --git a/docs/doctrees/apidoc/api.doctree b/docs/doctrees/apidoc/api.doctree index 57208d79..db159d6c 100644 Binary files a/docs/doctrees/apidoc/api.doctree and b/docs/doctrees/apidoc/api.doctree differ diff --git a/docs/doctrees/apidoc/api.tests.doctree b/docs/doctrees/apidoc/api.tests.doctree index 62854862..d7d9e3be 100644 Binary files a/docs/doctrees/apidoc/api.tests.doctree and b/docs/doctrees/apidoc/api.tests.doctree differ diff --git a/docs/doctrees/apidoc/manager.doctree b/docs/doctrees/apidoc/manager.doctree index c8221b84..ff44d84d 100644 Binary files a/docs/doctrees/apidoc/manager.doctree and b/docs/doctrees/apidoc/manager.doctree differ diff --git a/docs/doctrees/apidoc/subscription.doctree b/docs/doctrees/apidoc/subscription.doctree index cc4c02e4..a8a24847 100644 Binary files a/docs/doctrees/apidoc/subscription.doctree and b/docs/doctrees/apidoc/subscription.doctree differ diff --git a/docs/doctrees/apidoc/ui_framework.doctree b/docs/doctrees/apidoc/ui_framework.doctree index 2d81235c..4692f1aa 100644 Binary files a/docs/doctrees/apidoc/ui_framework.doctree and b/docs/doctrees/apidoc/ui_framework.doctree differ diff --git a/docs/doctrees/environment.pickle b/docs/doctrees/environment.pickle index a23b396e..bce87e0f 100644 Binary files a/docs/doctrees/environment.pickle and b/docs/doctrees/environment.pickle differ diff --git a/docs/doctrees/modules/how_it_works.doctree b/docs/doctrees/modules/how_it_works.doctree index 1c56d4b4..f0247448 100644 Binary files a/docs/doctrees/modules/how_it_works.doctree and b/docs/doctrees/modules/how_it_works.doctree differ diff --git a/docs/doctrees/modules/how_to_use_it.doctree b/docs/doctrees/modules/how_to_use_it.doctree index 873a01ab..65903122 100644 Binary files a/docs/doctrees/modules/how_to_use_it.doctree and b/docs/doctrees/modules/how_to_use_it.doctree differ diff --git a/docs/html/_sources/apidoc/api.tests.rst.txt b/docs/html/_sources/apidoc/api.tests.rst.txt index 232076ac..b18a5ad1 100644 --- a/docs/html/_sources/apidoc/api.tests.rst.txt +++ b/docs/html/_sources/apidoc/api.tests.rst.txt @@ -12,6 +12,14 @@ api.tests.test\_commander module :undoc-members: :show-inheritance: +api.tests.test\_lovecsc module +------------------------------ + +.. automodule:: api.tests.test_lovecsc + :members: + :undoc-members: + :show-inheritance: + api.tests.test\_schema\_validation module ----------------------------------------- @@ -28,10 +36,18 @@ api.tests.tests\_auth\_api module :undoc-members: :show-inheritance: -api.tests.tests\_config module ------------------------------- +api.tests.tests\_configfile module +---------------------------------- + +.. automodule:: api.tests.tests_configfile + :members: + :undoc-members: + :show-inheritance: + +api.tests.tests\_emergencycontact module +---------------------------------------- -.. automodule:: api.tests.tests_config +.. automodule:: api.tests.tests_emergencycontact :members: :undoc-members: :show-inheritance: diff --git a/docs/html/_sources/modules/how_it_works.rst.txt b/docs/html/_sources/modules/how_it_works.rst.txt index 63c4cf38..c3d0c8d8 100644 --- a/docs/html/_sources/modules/how_it_works.rst.txt +++ b/docs/html/_sources/modules/how_it_works.rst.txt @@ -62,7 +62,7 @@ Code organization Currently the application is divided in the following modules and files: -* :code:`api`: This module contains the :code:`API` Django app, which defines the models and API endpoints for authentication (:code:`Auth API`) and Commander (:code:`Commander API`) APIs. For more details please refer to the :ref:`ApiDoc` section +* :code:`api`: This module contains the :code:`API` Django app, which defines the models and API endpoints for authentication (:code:`Auth API`), Commander (:code:`Commander API`), ConfigFile (:code:`ConfigFile API`) and EmergencyContact (:code:`EmergencyContact API`) APIs. For more details please refer to the :ref:`ApiDoc` section * :code:`ui_framework`: This module contains the :code:`UI Framework` Django app, which defines the models and API endpoints for the UI Framework views (:code:`UI Framework API`) API. For more details please refer to the :ref:`ApiDoc` section * :code:`subscription`: This module contains the Django app that defines the consumers that handle the websocket communication. * :code:`manager`: This module contains basic Django configuration files, such as urls and channels routing, etc. diff --git a/docs/html/_sources/modules/how_to_use_it.rst.txt b/docs/html/_sources/modules/how_to_use_it.rst.txt index 5a07e37c..8811e5cc 100644 --- a/docs/html/_sources/modules/how_to_use_it.rst.txt +++ b/docs/html/_sources/modules/how_to_use_it.rst.txt @@ -65,7 +65,7 @@ Returns token, user data and permissions Validate token -------------- Validates a given authorization token, passed through HTTP Headers. -Returns a confirmation of validity, user data, permissions, server_time and (optionally) the LOVE configuraiton file. +Returns a confirmation of validity, user data, permissions, server_time and (optionally) the LOVE configuration file. If the :code:`no_config` flag is added to the end of the URL, then the LOVE config files is not read and the corresponding value is returned as :code:`null` - Url: :code:`/manager/api/validate-token/` or :code:`/manager/api/validate-token/no_config/` @@ -119,7 +119,7 @@ If the :code:`no_config` flag is added to the end of the URL, then the LOVE conf Swap token -------------- Validates a given authorization token, passed through HTTP Headers. -Returns a confirmation of validity, user data, permissions, server_time and (optionally) the LOVE configuraiton file. +Returns a confirmation of validity, user data, permissions, server_time and (optionally) the LOVE configuration file. If the :code:`no_config` flag is added to the end of the URL, then the LOVE config files is not read and the corresponding value is returned as :code:`null` - Url: :code:`/manager/api/swap-token/` or :code:`/manager/api/swap-token/no_config/` diff --git a/docs/html/apidoc/api.html b/docs/html/apidoc/api.html index 3a1df829..3227f2c4 100644 --- a/docs/html/apidoc/api.html +++ b/docs/html/apidoc/api.html @@ -187,10 +187,12 @@

5.1.1. Subpackages5.1.1.1. api.tests package @@ -351,6 +353,212 @@

5.1.2. SubmodulesDefines the Django models for this app (‘api’).

For more information see: https://docs.djangoproject.com/en/2.2/topics/db/models/

+
+
+class api.models.BaseModel(*args, **kwargs)
+

Bases: django.db.models.base.Model

+

Base Model for the models of this app.

+
+
+class Meta
+

Bases: object

+

Define attributes of the Meta class.

+
+
+abstract = False
+

Make this an abstract class in order to be used as an enhanced base model

+
+ +
+ +
+
+creation_timestamp
+

Creation timestamp, autogenerated upon creation

+
+ +
+
+get_next_by_creation_timestamp(*, field=<django.db.models.fields.DateTimeField: creation_timestamp>, is_next=True, **kwargs)
+
+ +
+
+get_next_by_update_timestamp(*, field=<django.db.models.fields.DateTimeField: update_timestamp>, is_next=True, **kwargs)
+
+ +
+
+get_previous_by_creation_timestamp(*, field=<django.db.models.fields.DateTimeField: creation_timestamp>, is_next=False, **kwargs)
+
+ +
+
+get_previous_by_update_timestamp(*, field=<django.db.models.fields.DateTimeField: update_timestamp>, is_next=False, **kwargs)
+
+ +
+
+update_timestamp
+

Update timestamp, autogenerated upon creation and autoupdated on every update

+
+ +
+ +
+
+class api.models.ConfigFile(*args, **kwargs)
+

Bases: api.models.BaseModel

+

ConfigFile Model, that includes actual configuration files, creation date and user.

+
+
+exception DoesNotExist
+

Bases: django.core.exceptions.ObjectDoesNotExist

+
+ +
+
+exception MultipleObjectsReturned
+

Bases: django.core.exceptions.MultipleObjectsReturned

+
+ +
+
+config_file
+

Reference to the config file

+
+ +
+
+file_name
+

The custom name for the configuration

+
+ +
+
+get_next_by_creation_timestamp(*, field=<django.db.models.fields.DateTimeField: creation_timestamp>, is_next=True, **kwargs)
+
+ +
+
+get_next_by_update_timestamp(*, field=<django.db.models.fields.DateTimeField: update_timestamp>, is_next=True, **kwargs)
+
+ +
+
+get_previous_by_creation_timestamp(*, field=<django.db.models.fields.DateTimeField: creation_timestamp>, is_next=False, **kwargs)
+
+ +
+
+get_previous_by_update_timestamp(*, field=<django.db.models.fields.DateTimeField: update_timestamp>, is_next=False, **kwargs)
+
+ +
+
+id
+

A wrapper for a deferred-loading field. When the value is read from this +object the first time, the query is executed.

+
+ +
+
+objects = <django.db.models.manager.Manager object>
+
+ +
+
+user
+

User who created the config file

+
+ +
+
+user_id
+
+ +
+
+validate_file_extension()
+
+ +
+ +
+
+class api.models.EmergencyContact(*args, **kwargs)
+

Bases: api.models.BaseModel

+

EmergencyContact Model

+
+
+exception DoesNotExist
+

Bases: django.core.exceptions.ObjectDoesNotExist

+
+ +
+
+exception MultipleObjectsReturned
+

Bases: django.core.exceptions.MultipleObjectsReturned

+
+ +
+
+contact_info
+

EC’s preferred contact information (work number, cell, none)

+
+ +
+
+email
+

EC’s email

+
+ +
+
+get_next_by_creation_timestamp(*, field=<django.db.models.fields.DateTimeField: creation_timestamp>, is_next=True, **kwargs)
+
+ +
+
+get_next_by_update_timestamp(*, field=<django.db.models.fields.DateTimeField: update_timestamp>, is_next=True, **kwargs)
+
+ +
+
+get_previous_by_creation_timestamp(*, field=<django.db.models.fields.DateTimeField: creation_timestamp>, is_next=False, **kwargs)
+
+ +
+
+get_previous_by_update_timestamp(*, field=<django.db.models.fields.DateTimeField: update_timestamp>, is_next=False, **kwargs)
+
+ +
+
+id
+

A wrapper for a deferred-loading field. When the value is read from this +object the first time, the query is executed.

+
+ +
+
+name
+

EC name

+
+ +
+
+objects = <django.db.models.manager.Manager object>
+
+ +
+
+subsystem
+

EC’s subsystem

+
+ +
+
class api.models.GlobalPermissions(*args, **kwargs)
@@ -485,6 +693,78 @@

5.1.2. Submodules

5.1.9. api.serializers module

Defines the serializer used by the REST API exposed by this app (‘api’).

+
+
+class api.serializers.ConfigFileContentSerializer(instance=None, data=<class 'rest_framework.fields.empty'>, **kwargs)
+

Bases: rest_framework.serializers.ModelSerializer

+

Serializer to map the Model instance into JSON format.

+
+
+class Meta
+

Bases: object

+

Meta class to map serializer’s fields with the model fields.

+
+
+fields = ('id', 'filename', 'content', 'update_timestamp')
+

The fields of the model class to serialize

+
+ +
+
+model
+

alias of api.models.ConfigFile

+
+ +
+ +
+
+get_content(obj)
+
+ +
+
+get_filename(obj)
+
+ +
+ +
+
+class api.serializers.ConfigFileSerializer(instance=None, data=<class 'rest_framework.fields.empty'>, **kwargs)
+

Bases: rest_framework.serializers.ModelSerializer

+

Serializer to map the Model instance into JSON format.

+
+
+class Meta
+

Bases: object

+

Meta class to map serializer’s fields with the model fields.

+
+
+fields = ('id', 'username', 'filename', 'creation_timestamp', 'update_timestamp')
+

The fields of the model class to serialize

+
+ +
+
+model
+

alias of api.models.ConfigFile

+
+ +
+ +
+
+get_filename(obj)
+
+ +
+
+get_username(obj)
+
+ +
+
class api.serializers.ConfigSerializer(instance=None, data=<class 'rest_framework.fields.empty'>, **kwargs)
@@ -492,6 +772,32 @@

5.1.2. SubmodulesCustom Serializer to describe the confi file field for the Apidocs.

+
+
+class api.serializers.EmergencyContactSerializer(instance=None, data=<class 'rest_framework.fields.empty'>, **kwargs)
+

Bases: rest_framework.serializers.ModelSerializer

+

Serializer to map the Model instance into JSON format.

+
+
+class Meta
+

Bases: object

+

Meta class to map serializer’s fields with the model fields.

+
+
+fields = '__all__'
+

The fields of the model class to serialize

+
+ +
+
+model
+

alias of api.models.EmergencyContact

+
+ +
+ +
+
class api.serializers.TimeDataSerializer(instance=None, data=<class 'rest_framework.fields.empty'>, **kwargs)
@@ -645,11 +951,6 @@

5.1.2. Submodules

-
-
-api.serializers.read_config_file()
-
-

5.1.10. api.signals module

@@ -699,6 +1000,70 @@

5.1.2. Submodules

5.1.12. api.views module

Defines the views exposed by the REST API exposed by this app.

+
+
+class api.views.ConfigFileViewSet(**kwargs)
+

Bases: rest_framework.viewsets.ModelViewSet

+

GET, POST, PUT, PATCH or DELETE instances the ConfigFile model.

+
+
+basename = None
+
+ +
+
+content(request, pk=None)
+

Serialize a ConfigFile’s content.

+
+
request: Request

The Requets object

+
+
pk: int

The corresponding ConfigFile pk

+
+
+
+
Returns
+

The response containing the serialized ConfigFile content

+
+
Return type
+

Response

+
+
+
+ +
+
+description = None
+
+ +
+
+detail = None
+
+ +
+
+name = None
+
+ +
+
+queryset = <QuerySet [<ConfigFile: ConfigFile object (5)>, <ConfigFile: ConfigFile object (4)>, <ConfigFile: ConfigFile object (1)>]>
+

Set of objects to be accessed by queries to this viewsets endpoints

+
+ +
+
+serializer_class
+

alias of api.serializers.ConfigFileSerializer

+
+ +
+
+suffix = None
+
+ +
+
class api.views.CustomObtainAuthToken(**kwargs)
@@ -779,6 +1144,50 @@

5.1.2. Submodules

+
+
+class api.views.EmergencyContactViewSet(**kwargs)
+

Bases: rest_framework.viewsets.ModelViewSet

+

GET, POST, PUT, PATCH or DELETE instances the EmergencyContact model.

+
+
+basename = None
+
+ +
+
+description = None
+
+ +
+
+detail = None
+
+ +
+
+name = None
+
+ +
+
+queryset = <QuerySet [<EmergencyContact: EmergencyContact object (3)>, <EmergencyContact: EmergencyContact object (1)>, <EmergencyContact: EmergencyContact object (2)>]>
+

Set of objects to be accessed by queries to this viewsets endpoints

+
+ +
+
+serializer_class
+

alias of api.serializers.EmergencyContactSerializer

+
+ +
+
+suffix = None
+
+ +
+
api.views.commander(self, request, *args, **kwargs)
@@ -834,6 +1243,24 @@

5.1.2. Submodules

+
+
+api.views.lovecsc_observinglog(self, request, *args, **kwargs)
+

Sends an observing log message to the LOVE-commander according to the received parameters

+
+
request: Request

The Request object

+
+
+
+
Returns
+

The response and status code of the request to the LOVE-Commander

+
+
Return type
+

Response

+
+
+
+
api.views.salinfo_metadata(self, request, *args, **kwargs)
diff --git a/docs/html/apidoc/api.tests.html b/docs/html/apidoc/api.tests.html index 7cdad432..a01d9fdb 100644 --- a/docs/html/apidoc/api.tests.html +++ b/docs/html/apidoc/api.tests.html @@ -263,9 +263,41 @@

5.1.1.1.1. Submodules +

5.1.1.1.3. api.tests.test_lovecsc module

+
+
+class api.tests.test_lovecsc.LOVECscTestCase(methodName='runTest')
+

Bases: django.test.testcases.TestCase

+
+
+maxDiff = None
+
+ +
+
+setUp()
+

Define the test suite setup.

+
+ +
+
+test_authorized_lovecsc_data(mock_requests, mock_environ)
+

Test authorized user observing log is sent to love-commander

+
+ +
+
+test_unauthorized_lovecsc(mock_requests, mock_environ)
+

Test an unauthorized user can’t send commands

+
+ +
+

-

5.1.1.1.3. api.tests.test_schema_validation module

+

5.1.1.1.4. api.tests.test_schema_validation module

class api.tests.test_schema_validation.SchemaValidationTestCase(methodName='runTest')
@@ -308,13 +340,18 @@

5.1.1.1.1. Submodules -

5.1.1.1.4. api.tests.tests_auth_api module

+

5.1.1.1.5. api.tests.tests_auth_api module

Test users’ authentication through the API.

class api.tests.tests_auth_api.AuthApiTestCase(methodName='runTest')

Bases: django.test.testcases.TestCase

Test suite for users’ authentication.

+
+
+static get_config_file_sample(name, content)
+
+
setUp()
@@ -402,37 +439,82 @@

5.1.1.1.1. Submodules -

5.1.1.1.5. api.tests.tests_config module

+
+

5.1.1.1.6. api.tests.tests_configfile module

Test users’ authentication through the API.

-
-class api.tests.tests_config.ConfigApiTestCase(methodName='runTest')
+
+class api.tests.tests_configfile.ConfigFileApiTestCase(methodName='runTest')

Bases: django.test.testcases.TestCase

Test suite for config files handling.

-
-setUp()
+
+static get_config_file_sample(name, content)
+
+ +
+
+setUp()

Define the test suite setup.

-
-test_get_config()
-

Test that an authenticated user can get the config file.

+
+test_get_config_file()
+

Test that an authenticated user can get a config file.

+
+ +
+
+test_get_config_file_content()
+

Test that an authenticated user can get a config file content.

+
+ +
+
+test_get_config_files_list()
+

Test that an authenticated user can get a config file.

-
-test_unauthenticated_cannot_get_config()
+
+test_unauthenticated_cannot_get_config_file()

Test that an unauthenticated user cannot get the config file.

+
+
+

5.1.1.1.7. api.tests.tests_emergencycontact module

+

Test users’ authentication through the API.

+
+
+class api.tests.tests_emergencycontact.EmergencyContactApiTestCase(methodName='runTest')
+

Bases: django.test.testcases.TestCase

+

Test suite for config files handling.

+
+
+static get_config_file_sample(name, content)
+
+ +
+
+setUp()
+

Define the test suite setup.

+
+ +
+
+test_list_emergency_contacts()
+

Test that an authenticated user can get a config file.

+
+ +
+
-

5.1.1.1.6. Module contents

+

5.1.1.1.8. Module contents

diff --git a/docs/html/apidoc/manager.html b/docs/html/apidoc/manager.html index 621821ea..a87f4ac6 100644 --- a/docs/html/apidoc/manager.html +++ b/docs/html/apidoc/manager.html @@ -206,7 +206,7 @@

5.3.1. Submodules
-manager.settings.CHANNEL_LAYERS = {'default': {'BACKEND': 'channels_redis.core.RedisChannelLayer', 'CONFIG': {'hosts': ['redis://:admin123@redis:6379/0'], 'symmetric_encryption_keys': ['tbder3gzppu)kl%(u3awhhg^^zu#j&!ceh@$n&v0d38sjx43s8']}}}
+manager.settings.CHANNEL_LAYERS = {'default': {'BACKEND': 'channels_redis.core.RedisChannelLayer', 'CONFIG': {'hosts': ['redis://:admin123@redis:6379/0'], 'symmetric_encryption_keys': ['tbder3gzppu)kl%(u3awhhg^^zuj&!ceh@$n&v0d38sjx43s8.']}}}

Django Channels Channel Layer configuration (dict)

@@ -236,7 +236,7 @@

5.3.1. Submodules
-manager.settings.SECRET_KEY = 'tbder3gzppu)kl%(u3awhhg^^zu#j&!ceh@$n&v0d38sjx43s8'
+manager.settings.SECRET_KEY = 'tbder3gzppu)kl%(u3awhhg^^zuj&!ceh@$n&v0d38sjx43s8.'

Secret Key for Django, read from the SECRET_KEY environment variable (string)

diff --git a/docs/html/apidoc/modules.html b/docs/html/apidoc/modules.html index c6147bc0..8d1bdf92 100644 --- a/docs/html/apidoc/modules.html +++ b/docs/html/apidoc/modules.html @@ -168,10 +168,12 @@

5. ApiDoc5.1.1.1. api.tests package diff --git a/docs/html/apidoc/subscription.html b/docs/html/apidoc/subscription.html index 85426042..20205d4b 100644 --- a/docs/html/apidoc/subscription.html +++ b/docs/html/apidoc/subscription.html @@ -441,70 +441,9 @@

5.4.1. SubmodulesUses an internal data structure (dictionary) to store the heartbeats of LOVE components. Runs 2 tasks in order to dispatch the heartbeats and request the LOVE-Commander’s heartbeat periodically.

-
-commander_heartbeat_task = None
-

Reference to the task that requests the LOVE_COmmander heartbeats.

-
- -
-
-async classmethod dispatch_heartbeats()
-

Dispatch all the heartbeats to the corresponding group in the Channels Layer.

-

This is what the heartbeat_task does

-
- -
-
-heartbeat_data = {}
-

Dictionary comntaining the heartbeats data, indexed by source or component, e.g. “Commander”.

-
- -
-
-heartbeat_task = None
-

Reference to the task that dispatches the heartbeats.

-
- -
-
-classmethod initialize()
-

Initialize the HeartbeatManager

-

Run 2 async tasks in the event loop, one to dispatch the heartbeats periodically, -and the other to request the heartbeats from the LOVE-Commander periodically.

-
- -
-
-async classmethod query_commander()
-

Query the heartbeat from the LOVE-Commander periodically.

-

This is what the commander_heartbeat_task does

-
- -
-
-async classmethod reset()
-

Reset the HeartbeatManager, changing the tasks references and heartbeats dictionary back to their default values.

-
- -
-
-classmethod set_heartbeat_timestamp(source, timestamp)
-

Set a given timestamp as the heartbeat for a given source

-
-
Parameters
-
    -
  • source (string) – Name of the component to save the heartbeat, e.g. “Commander”

  • -
  • timestamp (float) – timestamp of the heartbeat

  • -
-
-
-
- -
-
-async classmethod stop()
-

Stop (cancel) the tasks.

-
+
+instance = None
+
diff --git a/docs/html/apidoc/ui_framework.html b/docs/html/apidoc/ui_framework.html index a7953b7f..587882ca 100644 --- a/docs/html/apidoc/ui_framework.html +++ b/docs/html/apidoc/ui_framework.html @@ -932,7 +932,7 @@

5.5.2. Submodules
-queryset = <QuerySet [<View: Seba>, <View: Scheduler>, <View: HealthStatusSummary>, <View: LATISS>, <View: ATCamera>, <View: AT Lightpath>, <View: ATMount overview>, <View: HealthStatusSummary>, <View: ObservingLog>, <View: Time Displays>, <View: AT generic camera>, <View: CSCSummary>, <View: ScripQueue>, <View: Watcher>, <View: CSCGroup>, <View: TimeSeriesPlot>, <View: Int. TimeSeries>, <View: ATMount state>]>
+queryset = <QuerySet [<View: sqqtest>, <View: CSCSummary>, <View: Random view>, <View: CSCSummary + CSCG>, <View: Dome & Mount>, <View: HealthStatusSummary>, <View: LATISS + Camera>, <View: Network + Scheduler>, <View: TimeDisplay>, <View: Watcher + ObsLogs>, <View: WeatherStation>, <View: Dome-Mount test>, <View: logslogs>, <View: LATISS>, <View: ATCamera>, <View: AT Lightpath>, <View: ATMount overview>, <View: HealthStatusSummary>, <View: ObservingLog>, <View: Time Displays>, '...(remaining elements truncated)...']>

Set of objects to be accessed by queries to this viewsets endpoints

@@ -1028,7 +1028,7 @@

5.5.2. Submodules
-queryset = <QuerySet [<Workspace: My Workspace>]>
+queryset = <QuerySet []>

Set of objects to be accessed by queries to this viewsets endpoints

@@ -1091,7 +1091,7 @@

5.5.2. Submodules
-queryset = <QuerySet [<WorkspaceView: My Workspace - HealthStatusSummary>, <WorkspaceView: My Workspace - Watcher>]>
+queryset = <QuerySet []>

Set of objects to be accessed by queries to this viewsets endpoints

diff --git a/docs/html/genindex.html b/docs/html/genindex.html index 4f32ec7f..127b15c5 100644 --- a/docs/html/genindex.html +++ b/docs/html/genindex.html @@ -178,8 +178,12 @@

Index

A

@@ -326,10 +376,26 @@

D

E

@@ -338,9 +404,15 @@

E

F

@@ -372,15 +446,37 @@

G

  • (in module api.views)
  • +
  • get_config_file_sample() (api.tests.tests_auth_api.AuthApiTestCase static method) + +
  • +
  • get_content() (api.serializers.ConfigFileContentSerializer method) +
  • get_dict() (in module ui_framework.tests.utils)
  • get_file_extension() (ui_framework.serializers.Base64ImageField method)
  • +
  • get_filename() (api.serializers.ConfigFileContentSerializer method) + +
  • get_next_by_created() (api.models.Token method)
  • -
  • get_next_by_creation_timestamp() (ui_framework.models.BaseModel method) +
  • get_next_by_creation_timestamp() (api.models.BaseModel method)
  • -
  • get_next_by_update_timestamp() (ui_framework.models.BaseModel method) +
  • get_next_by_update_timestamp() (api.models.BaseModel method)
  • + + -
      +
    • handle_token_deletion() (in module api.signals) +
    • hanlde_view_deletion() (in module ui_framework.signals)
    • has_read_permission() (ui_framework.models.Workspace static method) -
    • -
    • heartbeat_data (subscription.heartbeat_manager.HeartbeatManager attribute) -
    • -
    • heartbeat_task (subscription.heartbeat_manager.HeartbeatManager attribute)
    • HeartbeatManager (class in subscription.heartbeat_manager)
    • @@ -478,9 +590,13 @@

      H

      I

      - +
      -
      @@ -550,6 +670,8 @@

      M

      • (api.tests.test_commander.SalinfoTestCase attribute) +
      • +
      • (api.tests.test_lovecsc.LOVECscTestCase attribute)
      • (api.tests.test_schema_validation.SchemaValidationTestCase attribute)
      • @@ -562,6 +684,12 @@

        M

        • (api.authentication.TokenAuthentication attribute) +
        • +
        • (api.serializers.ConfigFileContentSerializer.Meta attribute) +
        • +
        • (api.serializers.ConfigFileSerializer.Meta attribute) +
        • +
        • (api.serializers.EmergencyContactSerializer.Meta attribute)
        • (api.serializers.UserSerializer.Meta attribute)
        • @@ -587,6 +715,12 @@

          N

        • name (api.apps.ApiConfig attribute)
            +
          • (api.models.EmergencyContact attribute) +
          • +
          • (api.views.ConfigFileViewSet attribute) +
          • +
          • (api.views.EmergencyContactViewSet attribute) +
          • (subscription.apps.SubscriptionConfig attribute)
          • (ui_framework.apps.UiFrameworkConfig attribute) @@ -608,9 +742,13 @@

            N

            O

              -
            • objects (api.models.GlobalPermissions attribute) +
            • objects (api.models.ConfigFile attribute)
                +
              • (api.models.EmergencyContact attribute) +
              • +
              • (api.models.GlobalPermissions attribute) +
              • (api.models.Token attribute)
              • (ui_framework.models.View attribute) @@ -646,11 +784,13 @@

                P

                Q

                - + + + + + + + diff --git a/docs/html/searchindex.js b/docs/html/searchindex.js index 528fbde1..187d49dc 100644 --- a/docs/html/searchindex.js +++ b/docs/html/searchindex.js @@ -1 +1 @@ -Search.setIndex({docnames:["apidoc/api","apidoc/api.tests","apidoc/manage","apidoc/manager","apidoc/modules","apidoc/subscription","apidoc/ui_framework","apidoc/ui_framework.tests","index","modules/how_it_works","modules/how_to_use_it","modules/overview","modules/readme_link"],envversion:{"sphinx.domains.c":1,"sphinx.domains.changeset":1,"sphinx.domains.citation":1,"sphinx.domains.cpp":1,"sphinx.domains.index":1,"sphinx.domains.javascript":1,"sphinx.domains.math":2,"sphinx.domains.python":1,"sphinx.domains.rst":1,"sphinx.domains.std":1,"sphinx.ext.intersphinx":1,sphinx:56},filenames:["apidoc/api.rst","apidoc/api.tests.rst","apidoc/manage.rst","apidoc/manager.rst","apidoc/modules.rst","apidoc/subscription.rst","apidoc/ui_framework.rst","apidoc/ui_framework.tests.rst","index.rst","modules/how_it_works.rst","modules/how_to_use_it.rst","modules/overview.rst","modules/readme_link.rst"],objects:{"":{api:[0,0,0,"-"],manage:[2,0,0,"-"],manager:[3,0,0,"-"],subscription:[5,0,0,"-"],ui_framework:[6,0,0,"-"]},"api.apps":{ApiConfig:[0,1,1,""]},"api.apps.ApiConfig":{name:[0,2,1,""],ready:[0,3,1,""]},"api.authentication":{ExpiringTokenAuthentication:[0,1,1,""],TokenAuthentication:[0,1,1,""]},"api.authentication.ExpiringTokenAuthentication":{authenticate_credentials:[0,3,1,""],expires_in:[0,3,1,""],is_token_expired:[0,3,1,""],model:[0,2,1,""],token_expire_handler:[0,3,1,""]},"api.authentication.TokenAuthentication":{model:[0,2,1,""]},"api.middleware":{GetTokenMiddleware:[0,1,1,""]},"api.models":{GlobalPermissions:[0,1,1,""],Token:[0,1,1,""]},"api.models.GlobalPermissions":{DoesNotExist:[0,4,1,""],MultipleObjectsReturned:[0,4,1,""],id:[0,2,1,""],objects:[0,2,1,""]},"api.models.Token":{DoesNotExist:[0,4,1,""],MultipleObjectsReturned:[0,4,1,""],get_next_by_created:[0,3,1,""],get_previous_by_created:[0,3,1,""],id:[0,2,1,""],objects:[0,2,1,""],user:[0,2,1,""]},"api.schema_validator":{DefaultingValidator:[0,1,1,""]},"api.schema_validator.DefaultingValidator":{validate:[0,3,1,""]},"api.serializers":{ConfigSerializer:[0,1,1,""],TimeDataSerializer:[0,1,1,""],TokenSerializer:[0,1,1,""],UserPermissionsSerializer:[0,1,1,""],UserSerializer:[0,1,1,""],read_config_file:[0,5,1,""]},"api.serializers.TokenSerializer":{get_config:[0,3,1,""],get_permissions:[0,3,1,""],get_time_data:[0,3,1,""],get_token:[0,3,1,""]},"api.serializers.UserPermissionsSerializer":{can_execute_commands:[0,3,1,""]},"api.serializers.UserSerializer":{Meta:[0,1,1,""]},"api.serializers.UserSerializer.Meta":{fields:[0,2,1,""],model:[0,2,1,""]},"api.signals":{handle_token_deletion:[0,5,1,""]},"api.tests":{test_commander:[1,0,0,"-"],test_schema_validation:[1,0,0,"-"],tests_auth_api:[1,0,0,"-"],tests_config:[1,0,0,"-"]},"api.tests.test_commander":{CommanderTestCase:[1,1,1,""],SalinfoTestCase:[1,1,1,""]},"api.tests.test_commander.CommanderTestCase":{maxDiff:[1,2,1,""],setUp:[1,3,1,""],test_authorized_commander_data:[1,3,1,""],test_unauthorized_commander:[1,3,1,""]},"api.tests.test_commander.SalinfoTestCase":{maxDiff:[1,2,1,""],setUp:[1,3,1,""],test_salinfo_metadata:[1,3,1,""],test_salinfo_topic_data:[1,3,1,""],test_salinfo_topic_data_with_param:[1,3,1,""],test_salinfo_topic_names:[1,3,1,""],test_salinfo_topic_names_with_param:[1,3,1,""]},"api.tests.test_schema_validation":{SchemaValidationTestCase:[1,1,1,""]},"api.tests.test_schema_validation.SchemaValidationTestCase":{maxDiff:[1,2,1,""],script_schema:[1,2,1,""],setUp:[1,3,1,""],test_invalid_config:[1,3,1,""],test_syntax_error:[1,3,1,""],test_valid_config:[1,3,1,""]},"api.tests.tests_auth_api":{AuthApiTestCase:[1,1,1,""]},"api.tests.tests_auth_api.AuthApiTestCase":{setUp:[1,3,1,""],test_user_fails_to_validate_deleted_token:[1,3,1,""],test_user_fails_to_validate_expired_token:[1,3,1,""],test_user_login:[1,3,1,""],test_user_login_failed:[1,3,1,""],test_user_login_twice:[1,3,1,""],test_user_logout:[1,3,1,""],test_user_swap:[1,3,1,""],test_user_swap_forbidden:[1,3,1,""],test_user_swap_no_config:[1,3,1,""],test_user_swap_wrong_credentials:[1,3,1,""],test_user_validate_token:[1,3,1,""],test_user_validate_token_fail:[1,3,1,""],test_user_validate_token_no_config:[1,3,1,""]},"api.tests.tests_config":{ConfigApiTestCase:[1,1,1,""]},"api.tests.tests_config.ConfigApiTestCase":{setUp:[1,3,1,""],test_get_config:[1,3,1,""],test_unauthenticated_cannot_get_config:[1,3,1,""]},"api.views":{CustomObtainAuthToken:[0,1,1,""],CustomSwapAuthToken:[0,1,1,""],commander:[0,5,1,""],get_config:[0,5,1,""],logout:[0,5,1,""],salinfo_metadata:[0,5,1,""],salinfo_topic_data:[0,5,1,""],salinfo_topic_names:[0,5,1,""],validate_config_schema:[0,5,1,""],validate_token:[0,5,1,""]},"api.views.CustomObtainAuthToken":{login_failed_response:[0,2,1,""],login_response:[0,2,1,""],post:[0,3,1,""]},"api.views.CustomSwapAuthToken":{login_failed_response:[0,2,1,""],login_response:[0,2,1,""],post:[0,3,1,""]},"manager.settings":{ALLOWED_HOSTS:[3,6,1,""],AUTH_LDAP_SERVER_URI:[3,6,1,""],CHANNEL_LAYERS:[3,6,1,""],DATABASES:[3,6,1,""],LANGUAGE_CODE:[3,6,1,""],MEDIA_URL:[3,6,1,""],PROCESS_CONNECTION_PASS:[3,6,1,""],SECRET_KEY:[3,6,1,""],STATIC_ROOT:[3,6,1,""],STATIC_URL:[3,6,1,""],TESTING:[3,6,1,""],TIME_ZONE:[3,6,1,""],TOKEN_EXPIRED_AFTER_DAYS:[3,6,1,""],TRACE_TIMESTAMPS:[3,6,1,""]},"manager.utils":{assert_time_data:[3,5,1,""],get_tai_to_utc:[3,5,1,""],get_times:[3,5,1,""]},"subscription.apps":{SubscriptionConfig:[5,1,1,""]},"subscription.apps.SubscriptionConfig":{name:[5,2,1,""]},"subscription.auth":{TokenAuthMiddleware:[5,1,1,""],TokenAuthMiddlewareInstance:[5,1,1,""],get_user:[5,2,1,""]},"subscription.consumers":{SubscriptionConsumer:[5,1,1,""]},"subscription.consumers.SubscriptionConsumer":{connect:[5,3,1,""],disconnect:[5,3,1,""],handle_action_message:[5,3,1,""],handle_data_message:[5,3,1,""],handle_heartbeat_message:[5,3,1,""],handle_subscription_message:[5,3,1,""],logout:[5,3,1,""],receive_json:[5,3,1,""],send_heartbeat:[5,3,1,""],subscription_all_data:[5,3,1,""],subscription_data:[5,3,1,""]},"subscription.heartbeat_manager":{HeartbeatManager:[5,1,1,""]},"subscription.heartbeat_manager.HeartbeatManager":{commander_heartbeat_task:[5,2,1,""],dispatch_heartbeats:[5,3,1,""],heartbeat_data:[5,2,1,""],heartbeat_task:[5,2,1,""],initialize:[5,3,1,""],query_commander:[5,3,1,""],reset:[5,3,1,""],set_heartbeat_timestamp:[5,3,1,""],stop:[5,3,1,""]},"subscription.routing":{websocket_urlpatterns:[5,6,1,""]},"ui_framework.apps":{UiFrameworkConfig:[6,1,1,""]},"ui_framework.apps.UiFrameworkConfig":{name:[6,2,1,""],ready:[6,3,1,""]},"ui_framework.models":{BaseModel:[6,1,1,""],OverwriteStorage:[6,1,1,""],View:[6,1,1,""],Workspace:[6,1,1,""],WorkspaceView:[6,1,1,""]},"ui_framework.models.BaseModel":{Meta:[6,1,1,""],creation_timestamp:[6,2,1,""],get_next_by_creation_timestamp:[6,3,1,""],get_next_by_update_timestamp:[6,3,1,""],get_previous_by_creation_timestamp:[6,3,1,""],get_previous_by_update_timestamp:[6,3,1,""],update_timestamp:[6,2,1,""]},"ui_framework.models.BaseModel.Meta":{"abstract":[6,2,1,""]},"ui_framework.models.OverwriteStorage":{get_available_name:[6,3,1,""]},"ui_framework.models.View":{DoesNotExist:[6,4,1,""],MultipleObjectsReturned:[6,4,1,""],data:[6,2,1,""],get_next_by_creation_timestamp:[6,3,1,""],get_next_by_update_timestamp:[6,3,1,""],get_previous_by_creation_timestamp:[6,3,1,""],get_previous_by_update_timestamp:[6,3,1,""],id:[6,2,1,""],name:[6,2,1,""],objects:[6,2,1,""],thumbnail:[6,2,1,""],workspace_views:[6,2,1,""],workspaces:[6,2,1,""]},"ui_framework.models.Workspace":{DoesNotExist:[6,4,1,""],MultipleObjectsReturned:[6,4,1,""],get_next_by_creation_timestamp:[6,3,1,""],get_next_by_update_timestamp:[6,3,1,""],get_previous_by_creation_timestamp:[6,3,1,""],get_previous_by_update_timestamp:[6,3,1,""],get_sorted_views:[6,3,1,""],has_read_permission:[6,3,1,""],id:[6,2,1,""],name:[6,2,1,""],objects:[6,2,1,""],views:[6,2,1,""],workspace_views:[6,2,1,""]},"ui_framework.models.WorkspaceView":{DoesNotExist:[6,4,1,""],MultipleObjectsReturned:[6,4,1,""],get_next_by_creation_timestamp:[6,3,1,""],get_next_by_update_timestamp:[6,3,1,""],get_previous_by_creation_timestamp:[6,3,1,""],get_previous_by_update_timestamp:[6,3,1,""],id:[6,2,1,""],objects:[6,2,1,""],sort_value:[6,2,1,""],view:[6,2,1,""],view_id:[6,2,1,""],view_name:[6,2,1,""],workspace:[6,2,1,""],workspace_id:[6,2,1,""]},"ui_framework.serializers":{Base64ImageField:[6,1,1,""],ViewSerializer:[6,1,1,""],ViewSummarySerializer:[6,1,1,""],WorkspaceFullSerializer:[6,1,1,""],WorkspaceSerializer:[6,1,1,""],WorkspaceViewSerializer:[6,1,1,""],WorkspaceWithViewNameSerializer:[6,1,1,""]},"ui_framework.serializers.Base64ImageField":{get_file_extension:[6,3,1,""],to_internal_value:[6,3,1,""],to_representation:[6,3,1,""]},"ui_framework.serializers.ViewSerializer":{Meta:[6,1,1,""],thumbnail:[6,2,1,""]},"ui_framework.serializers.ViewSerializer.Meta":{fields:[6,2,1,""],model:[6,2,1,""]},"ui_framework.serializers.ViewSummarySerializer":{Meta:[6,1,1,""]},"ui_framework.serializers.ViewSummarySerializer.Meta":{fields:[6,2,1,""],model:[6,2,1,""]},"ui_framework.serializers.WorkspaceFullSerializer":{Meta:[6,1,1,""]},"ui_framework.serializers.WorkspaceFullSerializer.Meta":{fields:[6,2,1,""],model:[6,2,1,""]},"ui_framework.serializers.WorkspaceSerializer":{Meta:[6,1,1,""]},"ui_framework.serializers.WorkspaceSerializer.Meta":{fields:[6,2,1,""],model:[6,2,1,""]},"ui_framework.serializers.WorkspaceViewSerializer":{Meta:[6,1,1,""]},"ui_framework.serializers.WorkspaceViewSerializer.Meta":{fields:[6,2,1,""],model:[6,2,1,""]},"ui_framework.serializers.WorkspaceWithViewNameSerializer":{Meta:[6,1,1,""]},"ui_framework.serializers.WorkspaceWithViewNameSerializer.Meta":{fields:[6,2,1,""],model:[6,2,1,""]},"ui_framework.signals":{hanlde_view_deletion:[6,5,1,""]},"ui_framework.tests":{test_view_thumbnail:[7,0,0,"-"],tests_api:[7,0,0,"-"],tests_custom_api:[7,0,0,"-"],tests_models:[7,0,0,"-"],utils:[7,0,0,"-"]},"ui_framework.tests.test_view_thumbnail":{ViewThumbnailTestCase:[7,1,1,""]},"ui_framework.tests.test_view_thumbnail.ViewThumbnailTestCase":{setUp:[7,3,1,""],test_delete_view:[7,3,1,""],test_new_view:[7,3,1,""]},"ui_framework.tests.tests_api":{AuthorizedCrudTestCase:[7,1,1,""],UnauthenticatedCrudTestCase:[7,1,1,""],UnauthorizedCrudTestCase:[7,1,1,""]},"ui_framework.tests.tests_api.AuthorizedCrudTestCase":{setUp:[7,3,1,""],test_authorized_create_objects:[7,3,1,""],test_authorized_delete_objects:[7,3,1,""],test_authorized_list_objects:[7,3,1,""],test_authorized_retrieve_objects:[7,3,1,""],test_authorized_update_objects:[7,3,1,""]},"ui_framework.tests.tests_api.UnauthenticatedCrudTestCase":{setUp:[7,3,1,""],test_unauthenticated_create_objects:[7,3,1,""],test_unauthenticated_delete_objects:[7,3,1,""],test_unauthenticated_list_objects:[7,3,1,""],test_unauthenticated_retrieve_objects:[7,3,1,""],test_unauthenticated_update_objects:[7,3,1,""]},"ui_framework.tests.tests_api.UnauthorizedCrudTestCase":{setUp:[7,3,1,""],test_unauthorized_create_objects:[7,3,1,""],test_unauthorized_delete_objects:[7,3,1,""],test_unauthorized_list_objects:[7,3,1,""],test_unauthorized_retrieve_objects:[7,3,1,""],test_unauthorized_update_objects:[7,3,1,""]},"ui_framework.tests.tests_custom_api":{AuthorizedCrudTestCase:[7,1,1,""]},"ui_framework.tests.tests_custom_api.AuthorizedCrudTestCase":{setUp:[7,3,1,""],test_get_full_workspace:[7,3,1,""],test_get_workspaces_with_view_name:[7,3,1,""]},"ui_framework.tests.tests_models":{ViewModelTestCase:[7,1,1,""],WorkspaceAndViewsRelationsTestCase:[7,1,1,""],WorkspaceModelTestCase:[7,1,1,""],WorkspaceViewModelTestCase:[7,1,1,""]},"ui_framework.tests.tests_models.ViewModelTestCase":{setUp:[7,3,1,""],test_create_view:[7,3,1,""],test_delete_view:[7,3,1,""],test_retrieve_view:[7,3,1,""],test_update_view:[7,3,1,""]},"ui_framework.tests.tests_models.WorkspaceAndViewsRelationsTestCase":{setUp:[7,3,1,""],test_add_and_get_views_to_workspace:[7,3,1,""],test_get_workspaces_from_a_view:[7,3,1,""]},"ui_framework.tests.tests_models.WorkspaceModelTestCase":{setUp:[7,3,1,""],test_create_workspace:[7,3,1,""],test_delete_workspace:[7,3,1,""],test_retrieve_workspace:[7,3,1,""],test_update_workspace:[7,3,1,""]},"ui_framework.tests.tests_models.WorkspaceViewModelTestCase":{setUp:[7,3,1,""],test_create_workspace_view:[7,3,1,""],test_delete_workspace_view:[7,3,1,""],test_retrieve_workspace_view:[7,3,1,""],test_update_workspace_view:[7,3,1,""]},"ui_framework.tests.utils":{BaseTestCase:[7,1,1,""],get_dict:[7,5,1,""]},"ui_framework.tests.utils.BaseTestCase":{setUp:[7,3,1,""]},"ui_framework.views":{ViewViewSet:[6,1,1,""],WorkspaceViewSet:[6,1,1,""],WorkspaceViewViewSet:[6,1,1,""]},"ui_framework.views.ViewViewSet":{basename:[6,2,1,""],description:[6,2,1,""],detail:[6,2,1,""],name:[6,2,1,""],queryset:[6,2,1,""],search:[6,3,1,""],serializer_class:[6,2,1,""],suffix:[6,2,1,""],summary:[6,3,1,""]},"ui_framework.views.WorkspaceViewSet":{basename:[6,2,1,""],description:[6,2,1,""],detail:[6,2,1,""],full:[6,3,1,""],name:[6,2,1,""],queryset:[6,2,1,""],serializer_class:[6,2,1,""],suffix:[6,2,1,""],with_view_name:[6,3,1,""]},"ui_framework.views.WorkspaceViewViewSet":{basename:[6,2,1,""],description:[6,2,1,""],detail:[6,2,1,""],name:[6,2,1,""],queryset:[6,2,1,""],serializer_class:[6,2,1,""],suffix:[6,2,1,""]},api:{admin:[0,0,0,"-"],apps:[0,0,0,"-"],authentication:[0,0,0,"-"],middleware:[0,0,0,"-"],models:[0,0,0,"-"],schema_validator:[0,0,0,"-"],serializers:[0,0,0,"-"],signals:[0,0,0,"-"],tests:[1,0,0,"-"],urls:[0,0,0,"-"],views:[0,0,0,"-"]},manager:{asgi:[3,0,0,"-"],routing:[3,0,0,"-"],settings:[3,0,0,"-"],urls:[3,0,0,"-"],utils:[3,0,0,"-"],wsgi:[3,0,0,"-"]},subscription:{apps:[5,0,0,"-"],auth:[5,0,0,"-"],consumers:[5,0,0,"-"],heartbeat_manager:[5,0,0,"-"],routing:[5,0,0,"-"]},ui_framework:{admin:[6,0,0,"-"],apps:[6,0,0,"-"],models:[6,0,0,"-"],serializers:[6,0,0,"-"],signals:[6,0,0,"-"],tests:[7,0,0,"-"],urls:[6,0,0,"-"],views:[6,0,0,"-"]}},objnames:{"0":["py","module","Python module"],"1":["py","class","Python class"],"2":["py","attribute","Python attribute"],"3":["py","method","Python method"],"4":["py","exception","Python exception"],"5":["py","function","Python function"],"6":["py","data","Python data"]},objtypes:{"0":"py:module","1":"py:class","2":"py:attribute","3":"py:method","4":"py:exception","5":"py:function","6":"py:data"},terms:{"abstract":6,"boolean":[0,1],"byte":6,"case":[0,5,6,9],"class":[0,1,3,5,6,7],"default":[0,1,3,5,9,10,12],"final":0,"float":[3,5],"function":[0,3,5,6],"import":[0,3,6],"int":[0,3,6],"new":[6,7,10],"null":10,"public":12,"return":[0,3,5,6,7,10],"static":[3,6],"super":6,"true":[0,1,3,6,10],"try":[6,10],"while":9,And:[0,10],For:[0,3,6,9,10,11],The:[0,3,5,6,9,10,11,12],Then:9,These:[4,9],Use:[0,8,10],Uses:5,__all__:6,abort:10,abov:9,accept:10,access:[3,6,9,11],accessor:6,accord:[0,5],ack:10,acknowledg:[9,10,11],act:[9,12],action:[5,8,9],add:[0,3,6,7],added:[7,10],adding:7,addit:9,addition:9,additionalproperti:1,additt:0,admin123:3,admin:[3,4,10,12],admin_user_pass:12,afer:1,against:[0,10],alarm:10,alarms_sound:0,alia:[0,6],all:[5,7,9,10,11,12],allow:[3,10],allowed_host:3,allski:10,also:[9,11],alter:0,among:9,ani:[0,10,11],anonymousus:5,anoth:[0,3,6,11],api:[4,5,6,7,8,11],apiconfig:0,apidoc:[0,8,9,10],app:[3,4,9],app_modul:[0,5,6],app_nam:[0,5,6],appconfig:[0,5,6],append:10,appli:[0,9],applic:[0,2,3,5,9,10,12],appliedsettingsmatchstart:10,arg:[0,5,6],argument:[0,6],as_view:[0,3,6],asgi:4,asgi_appl:3,assert:3,assert_time_data:3,associ:[5,6],async:5,asynchron:10,asyncjsonwebsocketconsum:5,atcamera:6,atmount:6,atomic_request:3,atpneumat:10,atptg:10,attach:10,attribut:6,auth:[0,4],auth_ldap_server_uri:[3,12],authapitestcas:1,authent:[1,4,5,8,9,11,12],authenticate_credenti:0,authenticationfail:0,authlist:10,author:[0,1,7,10,11],authorizedcrudtestcas:7,authtoken:0,autocommit:3,autocomplet:0,autogener:6,autoupd:6,avail:[0,6,7,10],back:[5,11],backend:[3,10],base64:6,base64imagefield:6,base:[0,1,3,5,6,7,10,11],base_url:6,basemodel:6,basenam:6,basetestcas:7,bash:12,basic:9,becaus:9,been:0,befor:[0,1,12],behavior:7,behind:10,being:[0,3],below:[6,9],between:[0,3,5,10,11],blog:[0,3,6],bool:[0,3,6],both:11,browsabl:10,build:5,built:6,call:5,callabl:3,camera:[6,10],camfe:10,can:[0,1,7,9,10,12],can_execute_command:0,cancel:5,cannot:[1,7,12],categori:[5,10],ceh:3,central:6,certain:10,chang:[5,10],channel:[3,5,10,11,12],channel_lay:3,channels_redi:3,character:10,charg:9,charset:3,check:[0,6,11],checkpoint:1,child:6,children:6,classmethod:[0,5],cleanup:1,client:[7,9,10,11],close:5,close_cod:5,cmd:[10,12],cmd_acknowledg:10,cmd_user_pass:12,code:[0,6,8,10,12],collat:3,com:[0,1,3,6],combin:10,command:[0,1,5,8,11,12],command_data:[0,10],command_nam:[0,10],command_name_1:10,command_name_2:10,commander_heartbeat_task:5,commandertestcas:1,common:7,commun:[9,11],comntain:5,compar:9,compon:[5,9,10],compos:[9,10,12],concaten:6,conext:10,confi:0,config:[0,1,3,5,6],config_path:10,configapitestcas:1,configseri:0,configur:[0,1,3,5,6,9,10,12],configuraiton:10,confirm:10,conn_max_ag:3,connect:[3,5,8,9,11,12],constitut:6,consum:[0,4,10,11],contain:[0,3,5,6,9,10,12],content:[4,8,9,10],contian:10,contrib:[0,6],copi:[0,12],core:[0,3,6],correct:3,correspond:[0,5,6,9,10,12],could:[0,10],creat:[0,3,7,9],create_doc:12,create_forward_many_to_many_manag:6,creation:[6,10],creation_timestamp:[6,10],credenti:[1,9,10,11],critic:[0,10],crud:[7,9,10],csc:[0,5,10,11],csc_1:10,csc_2:10,cscgroup:6,cscsummari:6,currenlti:0,current:[0,3,5,6,9,10],custom:[0,5,6,7],customobtainauthtoken:0,customswapauthtoken:0,dai:3,dalet:7,data1:10,data2:10,data:[0,1,3,5,6,9,11],data_dict:0,databas:[0,3,7,12],date:[0,3,5,10],datetimefield:[0,6],db_engin:12,db_host:12,db_name:12,db_pass:12,db_port:12,db_user:12,debug:12,decod:6,decoded_fil:6,def:6,defaultingvalid:0,defaults_valid:0,defer:[0,6],defin:[0,1,3,5,6,7,9,10,12],deleg:6,delet:[0,1,6,7,9],deploy:[3,12],deprec:12,describ:[0,10,12],descript:[0,1,6],detail:[6,9,10,11],dev:9,develop:[8,9],dict:[0,3,5,6],dictionari:[0,3,5,6,7,10],diffent:9,differ:[0,1,3,5,9,10,11],directory_permissions_mod:6,disabl:10,disconnect:5,disk:6,dispatch:5,dispatch_heartbeat:5,displai:6,divid:9,django:[0,1,2,3,5,6,7,9,11,12],djangoproject:[0,3,6],djangosnippet:6,djangpo:[0,6],doc:[0,3,6],docker:9,docsrc:12,doe:[0,5,12],doesnotexist:[0,6],done:[9,10,11],draft7valid:0,draft:1,drf:[0,11],durat:3,dynam:6,each:[0,1,5,6,9,10],edit:[10,12],either:10,email:[0,10],empti:[0,6,10,12],enabl:10,encod:6,end:10,endpoint:[0,6,9,10],engin:[3,12],enhanc:6,entercontrol:10,entrypoint:3,env:12,environ:[3,11],error:[0,9,10],errorcod:10,establish:[9,10,11,12],etc:9,event:[5,11],event_data:[0,10],event_nam:[0,10],event_name_1:10,event_name_2:10,everi:[6,9,11,12],exampl:[0,3,6,8,10],except:[0,1,6],exec:12,execut:[0,2,6,9,11,12],execute_command:10,exitcontrol:10,expect:[3,5,6,10],expet:0,expir:[0,1],expires_in:0,expiringtokenauthent:0,explain:9,expos:[0,3,6],extens:6,fail:[0,1],fail_cleanup:1,fail_run:1,failur:10,fals:[0,1,3,6,10],fer:11,field:[0,6,7,10],field_11:10,field_12:10,field_21:10,field_22:10,figur:[9,11],file:[0,1,3,6,7,8,9],file_nam:6,file_permissions_mod:6,filenam:6,filesystemstorag:6,final_valid:0,first:[0,6],fixtur:9,flag:[0,10],folder:12,follow:[0,3,5,9,10,11,12],foreignkei:[0,6],format:[0,3,5,9,10],forward:[6,9,10,11],found:[5,6],framework:[6,7,8,11],free:6,frequenc:10,frmework:9,from:[0,3,5,6,7,9,10,11,12],frontend:[9,11,12],full:[3,6],fulli:[6,7,10],further:11,gencam:10,gener:[0,3,5,6,10,11],genera:9,get:[0,1,3,5,6],get_available_nam:6,get_config:0,get_dict:7,get_file_extens:6,get_next_by_cr:0,get_next_by_creation_timestamp:6,get_next_by_update_timestamp:6,get_permiss:0,get_previous_by_cr:0,get_previous_by_creation_timestamp:6,get_previous_by_update_timestamp:6,get_respons:0,get_sorted_view:6,get_tai_to_utc:3,get_tim:3,get_time_data:[0,5,10],get_token:0,get_us:5,gettokenmiddlewar:0,github:[1,6,12],given:[0,5,6,7,9,10],globalpermiss:0,greenwich:[0,3,5,10],group:[0,5,9,10,11],handl:[0,1,5,6,9,11],handle_action_messag:5,handle_data_messag:5,handle_heartbeat_messag:5,handle_subscription_messag:5,handle_token_delet:0,handler:9,hanlde_view_delet:6,has:[0,3,6,9,11,12],has_read_permiss:6,have:[0,5,10,11],header:10,healthstatussummari:6,heartbeat:5,heartbeat_data:5,heartbeat_manag:4,heartbeat_task:5,heartbeatmanag:5,heavili:6,here:12,hola:10,home:[0,3,6],host:[3,12],hourangl:[0,3,5,10],how:[8,11],howto:3,html:12,http:[0,1,3,6,9,10,11,12],identifi:10,ids:[7,10],imag:[6,9],imagefield:6,implement:[6,12],includ:[0,3,6,10,11],incom:5,index:[0,3,5,8,10,12],info:[9,11],inform:[0,3,6,9,10],inherit:7,ini:9,initi:[5,9],inner:5,input:[5,10],insid:[9,12],instanc:[0,3,5,6,9,10,11],instead:[10,12],instruct:[0,12],integr:12,intend:[5,10],interfac:[0,6],intermediari:[10,11],intern:[5,6,10],invalid:[0,1,9,10],is_next:[0,6],is_token_expir:0,iso:10,its:[6,7,9,10,12],ivalid:0,join:5,jpg:6,json:[0,1,5,6,9,10],jsonschema:0,julian:[0,3,5,10],kei:[0,3,6],key11:5,key12:5,key1:10,key21:5,key22:5,key2:10,keyword:0,kwarg:[0,5,6],languag:3,language_cod:3,last:[5,10],lastli:11,latiss:6,latter:9,layer:[3,5,10,12],layout:9,ldap:[3,12],leav:5,length:6,less:0,level:[0,3],librari:9,lightpath:6,like:[6,9,10],list:[0,3,5,6,7,10],load:[0,6,8],local:[0,8],localhost:3,locat:[0,3,5,6,10,12],log:[1,11],login:[0,9],login_failed_respons:0,login_respons:0,loglevel:10,logmessag:10,logout:[0,1,5],loop:5,love:[0,1,3,5,11],love_command:5,love_csc:10,lsst:[1,11,12],mai:10,main:[2,9],make:[5,6,9],manag:[0,4,5,6,7,11,12],manager_rcv:5,mani:[6,10],manytomanydescriptor:6,manytomanyfield:6,map:[0,6],match:[0,5,6],max_length:6,maxdiff:1,maximum:6,measur:[0,3],mechan:10,media:[3,10],media_url:[3,6],messag:[0,3,5,8,9,11],meta:[0,6],metadata:[0,1],method:[1,6],methodnam:[1,7],middlewar:[4,5,12],migrat:9,minimum:1,minseveritynotif:10,minseveritysound:10,mirror:3,miss:0,mjd:[0,3,5,10],mock_environ:1,mock_request:1,mode:12,model:[4,7,9,10],modelseri:[0,6],modelviewset:6,modifi:[0,3,5,10],modul:[4,8,9],moment:10,more:[0,3,6,9,11],most:6,mostli:9,mount:[3,12],move:12,mtm1m3:10,multipleobjectsreturn:[0,6],must:[9,10,12],mute:10,my_app:[0,3,6],my_dev_password:3,myimagefieldnam:6,mymodelnam:6,name:[0,1,3,5,6,7,12],necessari:10,need:[6,11,12],never:9,nginx:3,no_config:[0,1,10],no_debug:12,none:[0,1,3,5,6,10],note:0,number:[0,1,3,5,10],numer:10,obj:7,object:[0,1,5,6,7],objectdoesnotexist:[0,6],observ:11,observinglog:[6,10],obtain:0,obtainauthtoken:0,off:12,onc:[9,11,12],one:[0,5,6,10],onli:[5,6,9,10,12],oper:[9,10,11],option:[0,3,5,10],order:[0,5,6,9,10,12],org:[1,6],organ:8,origin:[0,11],other:[0,5,8,9,11],other_app:[0,3,6],otherwis:[5,12],our:0,out:[10,12],output:[0,1,5,10],outsid:12,over:[6,9,10],overrid:[6,12],overview:[6,8],overwrit:6,overwritestorag:6,packag:[4,8],page:[0,6,8],param:[1,10],paramet:[0,3,5,6,10],parameter_1:10,parameter_2:10,parent:6,pars:5,part:[5,8,11],particular:[5,9,10,11],pass:[1,5,6,10],password:[1,3,9,12],patch:6,path:[0,3,6,10],pattern:5,payload:10,perform:[9,10],period:5,permiss:[0,6,9,10,12],pipe:11,pizza:6,pleas:[0,3,6,9,11,12],plu:10,png:10,port:[3,12],post:[0,6,10],postgr:[3,12],postgresql:[3,9,12],present:0,problem:6,process_connection_pass:[3,12],produc:[3,9,10,11,12],project:[3,4,11,12],properli:7,properti:1,propos:6,provid:[0,9,10,11,12],pull:6,purpos:[7,9,12],put:[6,10],pytest:[9,12],python:[9,11],queri:[0,1,5,6,10],query_command:5,queryset:6,rais:[0,1],raw:6,react:5,read:[0,3,6,10],read_config_fil:0,readi:[0,6],readm:8,readonli:12,reason:10,rebuild:12,receiv:[0,1,5,6,9,10,11],receive_json:5,recept:5,recommend:12,redi:[3,12],redirect:11,redis_host:12,redis_pass:12,redischannellay:3,redoc:10,ref:[0,3,6],refer:[5,6,9],regist:[0,6],regular:10,reject:5,rel:10,relat:[0,6,10],related_nam:6,relationship:[7,10],relev:[0,3],remain:0,remian:0,remov:0,repli:[10,11],repo:12,repons:11,repositori:12,repres:10,represent:6,request:[0,1,5,6,7,9,11],request_tim:[5,10],requet:6,requier:9,requir:[1,9,10],reset:5,respect:[0,3,5,9,10],respond:9,respons:[0,6,9,11],rest:[0,6,7,9,10,11],rest_framework:[0,6],restart:12,result:[0,10],retriev:[7,9],revers:6,reversemanytoonedescriptor:6,rout:[0,4,6,9],rule:[3,5],run:[1,3,5,9],runserv:9,runtest:[1,7],sal:[9,11],sal_vers:[0,10],salindex:[5,10],salinfo:[0,1,10],salinfo_metadata:0,salinfo_topic_data:0,salinfo_topic_nam:0,salinfotestcas:1,salobj:9,save:[5,6,9],scale:[0,3,5,10],schedul:6,schema:[0,1],schema_path:10,schema_valid:4,schemavalidationtestcas:1,scope:5,scripqueu:6,script:9,script_schema:1,scriptqueu:[5,10],search:[6,8,9],search_text:10,seba:6,second:[0,1,3,5,10],secret:3,secret_kei:[3,12],section:[9,11],see:[0,3,6,9,10,12],select:10,self:[0,6],send:[0,1,5,9,10,11],send_heartbeat:5,sender:[0,6],sent:[0,1,5,6,10,11],separ:10,serial:4,serializer_class:6,seriou:[0,10],server:[3,9,10,12],server_tim:10,set:[0,4,5,6,7],set_heartbeat_timestamp:5,setauthlist:10,setloglevel:10,settingsappli:10,settingvers:10,setup:[1,7],setvalu:10,sever:0,should:[10,12],showalarm:10,shown:11,side:6,sider:[9,10],sidereal_greenwich:10,sidereal_summit:[0,3,5,10],sidereal_tim:[0,3,5,10],signal:4,similarli:[9,11],simpl:9,simulationmod:10,sky:10,skycam:10,snippet:6,softwar:[5,9],softwarevers:10,solut:6,solv:6,some:[9,10,12],sort:[6,10],sort_valu:[6,10],sound:0,sourc:[5,10],specifi:10,sqlite3:12,src:[3,12],stablish:10,standard:[0,10],standbi:10,start:[1,7,10],startproject:3,state:[0,6],static_root:3,static_url:3,statu:[0,10],still:7,stop:5,storag:6,store:[5,6,9,10],stream1:[5,10],stream2:5,stream:[5,10],string:[0,1,3,5,6,9,10],structur:[3,5,10],submodul:4,subpackag:4,subscirpt:5,subscrib:[0,5,9,10,11],subscript:[4,8,9],subscription_all_data:5,subscription_data:5,subscriptionconfig:5,subscriptionconsum:5,subseri:[6,7,10],succes:0,succesfulli:10,success:10,suffix:6,suit:1,summar:[6,10],summari:6,summaryst:10,summit:[0,3,5,10],support:5,sure:9,swagger:10,swap:[0,1],symmetric_encryption_kei:3,system:[6,8,10],tabl:10,tai:[0,3,5,9,10],tai_to_utc:[0,3,5,10],target:6,task:5,tbder3gzppu:3,teelemetri:10,tel1:9,tel2:9,telemetri:[5,9,11],telemetry_data:[0,10],telemetry_nam:[0,10],telemetry_name_1:10,telemetry_name_2:10,test:[0,3,4,6,9],test_add_and_get_views_to_workspac:7,test_authorized_commander_data:1,test_authorized_create_object:7,test_authorized_delete_object:7,test_authorized_list_object:7,test_authorized_retrieve_object:7,test_authorized_update_object:7,test_command:[0,4],test_create_view:7,test_create_workspac:7,test_create_workspace_view:7,test_delete_view:7,test_delete_workspac:7,test_delete_workspace_view:7,test_get_config:1,test_get_full_workspac:7,test_get_workspaces_from_a_view:7,test_get_workspaces_with_view_nam:7,test_invalid_config:1,test_new_view:7,test_retrieve_view:7,test_retrieve_workspac:7,test_retrieve_workspace_view:7,test_salinfo_metadata:1,test_salinfo_topic_data:1,test_salinfo_topic_data_with_param:1,test_salinfo_topic_nam:1,test_salinfo_topic_names_with_param:1,test_schema_valid:[0,4],test_syntax_error:1,test_unauthenticated_cannot_get_config:1,test_unauthenticated_create_object:7,test_unauthenticated_delete_object:7,test_unauthenticated_list_object:7,test_unauthenticated_retrieve_object:7,test_unauthenticated_update_object:7,test_unauthorized_command:1,test_unauthorized_create_object:7,test_unauthorized_delete_object:7,test_unauthorized_list_object:7,test_unauthorized_retrieve_object:7,test_unauthorized_update_object:7,test_update_view:7,test_update_workspac:7,test_update_workspace_view:7,test_user_fails_to_validate_deleted_token:1,test_user_fails_to_validate_expired_token:1,test_user_login:1,test_user_login_fail:1,test_user_login_twic:1,test_user_logout:1,test_user_swap:1,test_user_swap_forbidden:1,test_user_swap_no_config:1,test_user_swap_wrong_credenti:1,test_user_validate_token:1,test_user_validate_token_fail:1,test_user_validate_token_no_config:1,test_valid_config:1,test_view_thumbnail:[4,6],testcas:[1,7],tests_api:[4,6],tests_auth_api:[0,4],tests_config:[0,4],tests_custom_api:[4,6],tests_model:[4,6],testscript:1,text:10,than:0,thei:[0,10],them:[0,10],therefor:11,thi:[0,3,5,6,9,10,11,12],those:0,though:5,throgh:[0,6],through:[1,6,7,9,10,11],thumbnail:[6,7,10],ticket:6,time:[0,1,3,5,6,9,10],time_data:[3,5,10],time_zon:3,timedataseri:0,timeseri:6,timeseriesplot:6,timestamp:[0,3,5,6,10],timezon:3,titl:[0,1,10],to_internal_valu:6,to_represent:6,token:[0,1,3,5,9,11],token_expire_handl:0,token_expired_after_dai:3,tokenauthent:0,tokenauthmiddlewar:5,tokenauthmiddlewareinst:5,tokenseri:0,tokn:0,tomchristi:6,tool:12,top:6,topic:[0,3,6],topic_data:[0,1,10],topic_nam:[0,1,10],trace:3,trace_timestamp:3,transfer:10,transform:6,treat:10,tri:9,trigger:10,ts_salobj:1,turn:[9,11],twie:1,two:0,txt:9,type:[0,1,3,5,6],u3awhhg:3,ui_framework:[4,8,9,10],uiframeworkconfig:6,unacknowledg:10,unauthent:[1,7],unauthenticatedcrudtestcas:7,unauthor:[1,7],unauthorizedcrudtestcas:7,uniqu:9,unix:[0,3,5,10],unmut:10,unpars:1,unsubscrib:[5,10],unsubscript:5,unus:[0,6],updat:[6,7,9],update_timestamp:[6,10],upload:6,upon:6,url:[4,5,9,10],urlconf:[0,3,6],urlpattern:[0,3,5,6],use:[0,3,5,7,8,12],used:[0,5,6,7,9,10,12],user:[0,1,3,5,6,7,9,12],user_user_pass:12,usernam:[0,9,10],userpermissionsseri:0,userseri:0,uses:[6,11],using:[0,1,3,9,11,12],usr:[3,12],utc:[0,3,5,9,10],util:[4,6],v0d38sjx43s8:3,valid:[0,1,9],validate_config_schema:0,validate_token:0,validationerror:0,validatorclass:0,valu:[0,3,5,6,9,10,12],value11:5,value12:5,value1:10,value21:5,value22:5,value2:10,value_11:10,value_12:10,value_21:10,value_22:10,variabl:[3,10],variou:10,vaue:10,version:10,vetween:7,via:10,view:[3,4,7,9],view_1:10,view_2:10,view_3:10,view_i:7,view_id1:10,view_id2:10,view_id3:10,view_id:[6,10],view_nam:[6,10],viewmodeltestcas:7,viewseri:6,viewset:6,viewsummaryseri:6,viewthumbnailtestcas:7,viewviewset:6,visual:11,wai:[10,12],wait:[1,9],wait_tim:1,warn:[0,10],watcher:[6,10],websocket:[3,5,8,9,11,12],websocket_urlpattern:5,well:10,wether:[0,3,12],what:5,when:[0,6,7,9,11],where:[10,11,12],which:[0,9,10,11,12],whise:10,who:10,whole:3,with_view_nam:[6,10],within:[6,10],without:[6,10],work:[1,6,8,11],workflow:11,workspac:[6,7],workspace_i:7,workspace_id1:10,workspace_id:[6,10],workspace_view:[6,7],workspaceandviewsrelationstestcas:7,workspacefullseri:6,workspacemodeltestcas:7,workspaceseri:6,workspaceview:[6,7],workspaceviewmodeltestcas:7,workspaceviewseri:6,workspaceviewset:6,workspaceviewviewset:6,workspacewithviewnameseri:6,workview:6,wrapper:[0,6],write:11,written:[6,11],wsgi:4,xml:10,xml_version:[0,10],yaml:[0,1,10],you:[10,12]},titles:["5.1. api package","5.1.1.1. api.tests package","5.2. manage module","5.3. manager package","5. ApiDoc","5.4. subscription package","5.5. ui_framework package","5.5.1.1. ui_framework.tests package","Welcome to LOVE-manager\u2019s documentation!","3. How it works","2. How to use it","1. Overview","4. Readme File"],titleterms:{Use:12,action:10,admin:[0,6],api:[0,1,9,10],apidoc:4,app:[0,5,6],asgi:3,auth:[5,9],authent:[0,10],build:12,channel:9,code:9,command:[9,10],config:10,connect:10,consum:[5,9],content:[0,1,3,5,6,7],creat:10,data:10,databas:9,delet:10,develop:12,docker:12,document:[8,12],environ:12,event:10,exampl:9,file:[10,12],framework:[9,10],full:10,get:[10,12],heartbeat:10,heartbeat_manag:5,how:[9,10],imag:12,indic:8,info:10,initi:12,layer:9,load:12,local:12,log:10,logout:10,love:[8,9,10,12],manag:[2,3,8,9,10],messag:10,metadata:10,middlewar:0,model:[0,6],modul:[0,1,2,3,5,6,7],name:10,observ:10,organ:9,other:10,overview:11,packag:[0,1,3,5,6,7],part:[9,12],password:10,readm:12,request:10,respons:10,retriev:10,rout:[3,5],run:12,sal:10,schema:10,schema_valid:0,scheme:10,search:10,serial:[0,6],set:3,signal:[0,6],submodul:[0,1,3,5,6,7],subpackag:[0,6],subscript:[5,10],summari:10,swap:10,system:12,tabl:8,telemetri:10,test:[1,7,12],test_command:1,test_schema_valid:1,test_view_thumbnail:7,tests_api:7,tests_auth_api:1,tests_config:1,tests_custom_api:7,tests_model:7,token:10,topic:10,type:10,ui_framework:[6,7],unauthent:10,unauthor:10,updat:10,url:[0,3,6],use:10,user:10,util:[3,7],valid:10,variabl:12,view:[0,6,10],websocket:10,welcom:8,work:9,workspac:10,workspaceview:10,wsgi:3}}) \ No newline at end of file +Search.setIndex({docnames:["apidoc/api","apidoc/api.tests","apidoc/manage","apidoc/manager","apidoc/modules","apidoc/subscription","apidoc/ui_framework","apidoc/ui_framework.tests","index","modules/how_it_works","modules/how_to_use_it","modules/overview","modules/readme_link"],envversion:{"sphinx.domains.c":1,"sphinx.domains.changeset":1,"sphinx.domains.citation":1,"sphinx.domains.cpp":1,"sphinx.domains.index":1,"sphinx.domains.javascript":1,"sphinx.domains.math":2,"sphinx.domains.python":1,"sphinx.domains.rst":1,"sphinx.domains.std":1,"sphinx.ext.intersphinx":1,sphinx:56},filenames:["apidoc/api.rst","apidoc/api.tests.rst","apidoc/manage.rst","apidoc/manager.rst","apidoc/modules.rst","apidoc/subscription.rst","apidoc/ui_framework.rst","apidoc/ui_framework.tests.rst","index.rst","modules/how_it_works.rst","modules/how_to_use_it.rst","modules/overview.rst","modules/readme_link.rst"],objects:{"":{api:[0,0,0,"-"],manage:[2,0,0,"-"],manager:[3,0,0,"-"],subscription:[5,0,0,"-"],ui_framework:[6,0,0,"-"]},"api.apps":{ApiConfig:[0,1,1,""]},"api.apps.ApiConfig":{name:[0,2,1,""],ready:[0,3,1,""]},"api.authentication":{ExpiringTokenAuthentication:[0,1,1,""],TokenAuthentication:[0,1,1,""]},"api.authentication.ExpiringTokenAuthentication":{authenticate_credentials:[0,3,1,""],expires_in:[0,3,1,""],is_token_expired:[0,3,1,""],model:[0,2,1,""],token_expire_handler:[0,3,1,""]},"api.authentication.TokenAuthentication":{model:[0,2,1,""]},"api.middleware":{GetTokenMiddleware:[0,1,1,""]},"api.models":{BaseModel:[0,1,1,""],ConfigFile:[0,1,1,""],EmergencyContact:[0,1,1,""],GlobalPermissions:[0,1,1,""],Token:[0,1,1,""]},"api.models.BaseModel":{Meta:[0,1,1,""],creation_timestamp:[0,2,1,""],get_next_by_creation_timestamp:[0,3,1,""],get_next_by_update_timestamp:[0,3,1,""],get_previous_by_creation_timestamp:[0,3,1,""],get_previous_by_update_timestamp:[0,3,1,""],update_timestamp:[0,2,1,""]},"api.models.BaseModel.Meta":{"abstract":[0,2,1,""]},"api.models.ConfigFile":{DoesNotExist:[0,4,1,""],MultipleObjectsReturned:[0,4,1,""],config_file:[0,2,1,""],file_name:[0,2,1,""],get_next_by_creation_timestamp:[0,3,1,""],get_next_by_update_timestamp:[0,3,1,""],get_previous_by_creation_timestamp:[0,3,1,""],get_previous_by_update_timestamp:[0,3,1,""],id:[0,2,1,""],objects:[0,2,1,""],user:[0,2,1,""],user_id:[0,2,1,""],validate_file_extension:[0,3,1,""]},"api.models.EmergencyContact":{DoesNotExist:[0,4,1,""],MultipleObjectsReturned:[0,4,1,""],contact_info:[0,2,1,""],email:[0,2,1,""],get_next_by_creation_timestamp:[0,3,1,""],get_next_by_update_timestamp:[0,3,1,""],get_previous_by_creation_timestamp:[0,3,1,""],get_previous_by_update_timestamp:[0,3,1,""],id:[0,2,1,""],name:[0,2,1,""],objects:[0,2,1,""],subsystem:[0,2,1,""]},"api.models.GlobalPermissions":{DoesNotExist:[0,4,1,""],MultipleObjectsReturned:[0,4,1,""],id:[0,2,1,""],objects:[0,2,1,""]},"api.models.Token":{DoesNotExist:[0,4,1,""],MultipleObjectsReturned:[0,4,1,""],get_next_by_created:[0,3,1,""],get_previous_by_created:[0,3,1,""],id:[0,2,1,""],objects:[0,2,1,""],user:[0,2,1,""]},"api.schema_validator":{DefaultingValidator:[0,1,1,""]},"api.schema_validator.DefaultingValidator":{validate:[0,3,1,""]},"api.serializers":{ConfigFileContentSerializer:[0,1,1,""],ConfigFileSerializer:[0,1,1,""],ConfigSerializer:[0,1,1,""],EmergencyContactSerializer:[0,1,1,""],TimeDataSerializer:[0,1,1,""],TokenSerializer:[0,1,1,""],UserPermissionsSerializer:[0,1,1,""],UserSerializer:[0,1,1,""]},"api.serializers.ConfigFileContentSerializer":{Meta:[0,1,1,""],get_content:[0,3,1,""],get_filename:[0,3,1,""]},"api.serializers.ConfigFileContentSerializer.Meta":{fields:[0,2,1,""],model:[0,2,1,""]},"api.serializers.ConfigFileSerializer":{Meta:[0,1,1,""],get_filename:[0,3,1,""],get_username:[0,3,1,""]},"api.serializers.ConfigFileSerializer.Meta":{fields:[0,2,1,""],model:[0,2,1,""]},"api.serializers.EmergencyContactSerializer":{Meta:[0,1,1,""]},"api.serializers.EmergencyContactSerializer.Meta":{fields:[0,2,1,""],model:[0,2,1,""]},"api.serializers.TokenSerializer":{get_config:[0,3,1,""],get_permissions:[0,3,1,""],get_time_data:[0,3,1,""],get_token:[0,3,1,""]},"api.serializers.UserPermissionsSerializer":{can_execute_commands:[0,3,1,""]},"api.serializers.UserSerializer":{Meta:[0,1,1,""]},"api.serializers.UserSerializer.Meta":{fields:[0,2,1,""],model:[0,2,1,""]},"api.signals":{handle_token_deletion:[0,5,1,""]},"api.tests":{test_commander:[1,0,0,"-"],test_lovecsc:[1,0,0,"-"],test_schema_validation:[1,0,0,"-"],tests_auth_api:[1,0,0,"-"],tests_configfile:[1,0,0,"-"],tests_emergencycontact:[1,0,0,"-"]},"api.tests.test_commander":{CommanderTestCase:[1,1,1,""],SalinfoTestCase:[1,1,1,""]},"api.tests.test_commander.CommanderTestCase":{maxDiff:[1,2,1,""],setUp:[1,3,1,""],test_authorized_commander_data:[1,3,1,""],test_unauthorized_commander:[1,3,1,""]},"api.tests.test_commander.SalinfoTestCase":{maxDiff:[1,2,1,""],setUp:[1,3,1,""],test_salinfo_metadata:[1,3,1,""],test_salinfo_topic_data:[1,3,1,""],test_salinfo_topic_data_with_param:[1,3,1,""],test_salinfo_topic_names:[1,3,1,""],test_salinfo_topic_names_with_param:[1,3,1,""]},"api.tests.test_lovecsc":{LOVECscTestCase:[1,1,1,""]},"api.tests.test_lovecsc.LOVECscTestCase":{maxDiff:[1,2,1,""],setUp:[1,3,1,""],test_authorized_lovecsc_data:[1,3,1,""],test_unauthorized_lovecsc:[1,3,1,""]},"api.tests.test_schema_validation":{SchemaValidationTestCase:[1,1,1,""]},"api.tests.test_schema_validation.SchemaValidationTestCase":{maxDiff:[1,2,1,""],script_schema:[1,2,1,""],setUp:[1,3,1,""],test_invalid_config:[1,3,1,""],test_syntax_error:[1,3,1,""],test_valid_config:[1,3,1,""]},"api.tests.tests_auth_api":{AuthApiTestCase:[1,1,1,""]},"api.tests.tests_auth_api.AuthApiTestCase":{get_config_file_sample:[1,3,1,""],setUp:[1,3,1,""],test_user_fails_to_validate_deleted_token:[1,3,1,""],test_user_fails_to_validate_expired_token:[1,3,1,""],test_user_login:[1,3,1,""],test_user_login_failed:[1,3,1,""],test_user_login_twice:[1,3,1,""],test_user_logout:[1,3,1,""],test_user_swap:[1,3,1,""],test_user_swap_forbidden:[1,3,1,""],test_user_swap_no_config:[1,3,1,""],test_user_swap_wrong_credentials:[1,3,1,""],test_user_validate_token:[1,3,1,""],test_user_validate_token_fail:[1,3,1,""],test_user_validate_token_no_config:[1,3,1,""]},"api.tests.tests_configfile":{ConfigFileApiTestCase:[1,1,1,""]},"api.tests.tests_configfile.ConfigFileApiTestCase":{get_config_file_sample:[1,3,1,""],setUp:[1,3,1,""],test_get_config_file:[1,3,1,""],test_get_config_file_content:[1,3,1,""],test_get_config_files_list:[1,3,1,""],test_unauthenticated_cannot_get_config_file:[1,3,1,""]},"api.tests.tests_emergencycontact":{EmergencyContactApiTestCase:[1,1,1,""]},"api.tests.tests_emergencycontact.EmergencyContactApiTestCase":{get_config_file_sample:[1,3,1,""],setUp:[1,3,1,""],test_list_emergency_contacts:[1,3,1,""]},"api.views":{ConfigFileViewSet:[0,1,1,""],CustomObtainAuthToken:[0,1,1,""],CustomSwapAuthToken:[0,1,1,""],EmergencyContactViewSet:[0,1,1,""],commander:[0,5,1,""],get_config:[0,5,1,""],logout:[0,5,1,""],lovecsc_observinglog:[0,5,1,""],salinfo_metadata:[0,5,1,""],salinfo_topic_data:[0,5,1,""],salinfo_topic_names:[0,5,1,""],validate_config_schema:[0,5,1,""],validate_token:[0,5,1,""]},"api.views.ConfigFileViewSet":{basename:[0,2,1,""],content:[0,3,1,""],description:[0,2,1,""],detail:[0,2,1,""],name:[0,2,1,""],queryset:[0,2,1,""],serializer_class:[0,2,1,""],suffix:[0,2,1,""]},"api.views.CustomObtainAuthToken":{login_failed_response:[0,2,1,""],login_response:[0,2,1,""],post:[0,3,1,""]},"api.views.CustomSwapAuthToken":{login_failed_response:[0,2,1,""],login_response:[0,2,1,""],post:[0,3,1,""]},"api.views.EmergencyContactViewSet":{basename:[0,2,1,""],description:[0,2,1,""],detail:[0,2,1,""],name:[0,2,1,""],queryset:[0,2,1,""],serializer_class:[0,2,1,""],suffix:[0,2,1,""]},"manager.settings":{ALLOWED_HOSTS:[3,6,1,""],AUTH_LDAP_SERVER_URI:[3,6,1,""],CHANNEL_LAYERS:[3,6,1,""],DATABASES:[3,6,1,""],LANGUAGE_CODE:[3,6,1,""],MEDIA_URL:[3,6,1,""],PROCESS_CONNECTION_PASS:[3,6,1,""],SECRET_KEY:[3,6,1,""],STATIC_ROOT:[3,6,1,""],STATIC_URL:[3,6,1,""],TESTING:[3,6,1,""],TIME_ZONE:[3,6,1,""],TOKEN_EXPIRED_AFTER_DAYS:[3,6,1,""],TRACE_TIMESTAMPS:[3,6,1,""]},"manager.utils":{assert_time_data:[3,5,1,""],get_tai_to_utc:[3,5,1,""],get_times:[3,5,1,""]},"subscription.apps":{SubscriptionConfig:[5,1,1,""]},"subscription.apps.SubscriptionConfig":{name:[5,2,1,""]},"subscription.auth":{TokenAuthMiddleware:[5,1,1,""],TokenAuthMiddlewareInstance:[5,1,1,""],get_user:[5,2,1,""]},"subscription.consumers":{SubscriptionConsumer:[5,1,1,""]},"subscription.consumers.SubscriptionConsumer":{connect:[5,3,1,""],disconnect:[5,3,1,""],handle_action_message:[5,3,1,""],handle_data_message:[5,3,1,""],handle_heartbeat_message:[5,3,1,""],handle_subscription_message:[5,3,1,""],logout:[5,3,1,""],receive_json:[5,3,1,""],send_heartbeat:[5,3,1,""],subscription_all_data:[5,3,1,""],subscription_data:[5,3,1,""]},"subscription.heartbeat_manager":{HeartbeatManager:[5,1,1,""]},"subscription.heartbeat_manager.HeartbeatManager":{instance:[5,2,1,""]},"subscription.routing":{websocket_urlpatterns:[5,6,1,""]},"ui_framework.apps":{UiFrameworkConfig:[6,1,1,""]},"ui_framework.apps.UiFrameworkConfig":{name:[6,2,1,""],ready:[6,3,1,""]},"ui_framework.models":{BaseModel:[6,1,1,""],OverwriteStorage:[6,1,1,""],View:[6,1,1,""],Workspace:[6,1,1,""],WorkspaceView:[6,1,1,""]},"ui_framework.models.BaseModel":{Meta:[6,1,1,""],creation_timestamp:[6,2,1,""],get_next_by_creation_timestamp:[6,3,1,""],get_next_by_update_timestamp:[6,3,1,""],get_previous_by_creation_timestamp:[6,3,1,""],get_previous_by_update_timestamp:[6,3,1,""],update_timestamp:[6,2,1,""]},"ui_framework.models.BaseModel.Meta":{"abstract":[6,2,1,""]},"ui_framework.models.OverwriteStorage":{get_available_name:[6,3,1,""]},"ui_framework.models.View":{DoesNotExist:[6,4,1,""],MultipleObjectsReturned:[6,4,1,""],data:[6,2,1,""],get_next_by_creation_timestamp:[6,3,1,""],get_next_by_update_timestamp:[6,3,1,""],get_previous_by_creation_timestamp:[6,3,1,""],get_previous_by_update_timestamp:[6,3,1,""],id:[6,2,1,""],name:[6,2,1,""],objects:[6,2,1,""],thumbnail:[6,2,1,""],workspace_views:[6,2,1,""],workspaces:[6,2,1,""]},"ui_framework.models.Workspace":{DoesNotExist:[6,4,1,""],MultipleObjectsReturned:[6,4,1,""],get_next_by_creation_timestamp:[6,3,1,""],get_next_by_update_timestamp:[6,3,1,""],get_previous_by_creation_timestamp:[6,3,1,""],get_previous_by_update_timestamp:[6,3,1,""],get_sorted_views:[6,3,1,""],has_read_permission:[6,3,1,""],id:[6,2,1,""],name:[6,2,1,""],objects:[6,2,1,""],views:[6,2,1,""],workspace_views:[6,2,1,""]},"ui_framework.models.WorkspaceView":{DoesNotExist:[6,4,1,""],MultipleObjectsReturned:[6,4,1,""],get_next_by_creation_timestamp:[6,3,1,""],get_next_by_update_timestamp:[6,3,1,""],get_previous_by_creation_timestamp:[6,3,1,""],get_previous_by_update_timestamp:[6,3,1,""],id:[6,2,1,""],objects:[6,2,1,""],sort_value:[6,2,1,""],view:[6,2,1,""],view_id:[6,2,1,""],view_name:[6,2,1,""],workspace:[6,2,1,""],workspace_id:[6,2,1,""]},"ui_framework.serializers":{Base64ImageField:[6,1,1,""],ViewSerializer:[6,1,1,""],ViewSummarySerializer:[6,1,1,""],WorkspaceFullSerializer:[6,1,1,""],WorkspaceSerializer:[6,1,1,""],WorkspaceViewSerializer:[6,1,1,""],WorkspaceWithViewNameSerializer:[6,1,1,""]},"ui_framework.serializers.Base64ImageField":{get_file_extension:[6,3,1,""],to_internal_value:[6,3,1,""],to_representation:[6,3,1,""]},"ui_framework.serializers.ViewSerializer":{Meta:[6,1,1,""],thumbnail:[6,2,1,""]},"ui_framework.serializers.ViewSerializer.Meta":{fields:[6,2,1,""],model:[6,2,1,""]},"ui_framework.serializers.ViewSummarySerializer":{Meta:[6,1,1,""]},"ui_framework.serializers.ViewSummarySerializer.Meta":{fields:[6,2,1,""],model:[6,2,1,""]},"ui_framework.serializers.WorkspaceFullSerializer":{Meta:[6,1,1,""]},"ui_framework.serializers.WorkspaceFullSerializer.Meta":{fields:[6,2,1,""],model:[6,2,1,""]},"ui_framework.serializers.WorkspaceSerializer":{Meta:[6,1,1,""]},"ui_framework.serializers.WorkspaceSerializer.Meta":{fields:[6,2,1,""],model:[6,2,1,""]},"ui_framework.serializers.WorkspaceViewSerializer":{Meta:[6,1,1,""]},"ui_framework.serializers.WorkspaceViewSerializer.Meta":{fields:[6,2,1,""],model:[6,2,1,""]},"ui_framework.serializers.WorkspaceWithViewNameSerializer":{Meta:[6,1,1,""]},"ui_framework.serializers.WorkspaceWithViewNameSerializer.Meta":{fields:[6,2,1,""],model:[6,2,1,""]},"ui_framework.signals":{hanlde_view_deletion:[6,5,1,""]},"ui_framework.tests":{test_view_thumbnail:[7,0,0,"-"],tests_api:[7,0,0,"-"],tests_custom_api:[7,0,0,"-"],tests_models:[7,0,0,"-"],utils:[7,0,0,"-"]},"ui_framework.tests.test_view_thumbnail":{ViewThumbnailTestCase:[7,1,1,""]},"ui_framework.tests.test_view_thumbnail.ViewThumbnailTestCase":{setUp:[7,3,1,""],test_delete_view:[7,3,1,""],test_new_view:[7,3,1,""]},"ui_framework.tests.tests_api":{AuthorizedCrudTestCase:[7,1,1,""],UnauthenticatedCrudTestCase:[7,1,1,""],UnauthorizedCrudTestCase:[7,1,1,""]},"ui_framework.tests.tests_api.AuthorizedCrudTestCase":{setUp:[7,3,1,""],test_authorized_create_objects:[7,3,1,""],test_authorized_delete_objects:[7,3,1,""],test_authorized_list_objects:[7,3,1,""],test_authorized_retrieve_objects:[7,3,1,""],test_authorized_update_objects:[7,3,1,""]},"ui_framework.tests.tests_api.UnauthenticatedCrudTestCase":{setUp:[7,3,1,""],test_unauthenticated_create_objects:[7,3,1,""],test_unauthenticated_delete_objects:[7,3,1,""],test_unauthenticated_list_objects:[7,3,1,""],test_unauthenticated_retrieve_objects:[7,3,1,""],test_unauthenticated_update_objects:[7,3,1,""]},"ui_framework.tests.tests_api.UnauthorizedCrudTestCase":{setUp:[7,3,1,""],test_unauthorized_create_objects:[7,3,1,""],test_unauthorized_delete_objects:[7,3,1,""],test_unauthorized_list_objects:[7,3,1,""],test_unauthorized_retrieve_objects:[7,3,1,""],test_unauthorized_update_objects:[7,3,1,""]},"ui_framework.tests.tests_custom_api":{AuthorizedCrudTestCase:[7,1,1,""]},"ui_framework.tests.tests_custom_api.AuthorizedCrudTestCase":{setUp:[7,3,1,""],test_get_full_workspace:[7,3,1,""],test_get_workspaces_with_view_name:[7,3,1,""]},"ui_framework.tests.tests_models":{ViewModelTestCase:[7,1,1,""],WorkspaceAndViewsRelationsTestCase:[7,1,1,""],WorkspaceModelTestCase:[7,1,1,""],WorkspaceViewModelTestCase:[7,1,1,""]},"ui_framework.tests.tests_models.ViewModelTestCase":{setUp:[7,3,1,""],test_create_view:[7,3,1,""],test_delete_view:[7,3,1,""],test_retrieve_view:[7,3,1,""],test_update_view:[7,3,1,""]},"ui_framework.tests.tests_models.WorkspaceAndViewsRelationsTestCase":{setUp:[7,3,1,""],test_add_and_get_views_to_workspace:[7,3,1,""],test_get_workspaces_from_a_view:[7,3,1,""]},"ui_framework.tests.tests_models.WorkspaceModelTestCase":{setUp:[7,3,1,""],test_create_workspace:[7,3,1,""],test_delete_workspace:[7,3,1,""],test_retrieve_workspace:[7,3,1,""],test_update_workspace:[7,3,1,""]},"ui_framework.tests.tests_models.WorkspaceViewModelTestCase":{setUp:[7,3,1,""],test_create_workspace_view:[7,3,1,""],test_delete_workspace_view:[7,3,1,""],test_retrieve_workspace_view:[7,3,1,""],test_update_workspace_view:[7,3,1,""]},"ui_framework.tests.utils":{BaseTestCase:[7,1,1,""],get_dict:[7,5,1,""]},"ui_framework.tests.utils.BaseTestCase":{setUp:[7,3,1,""]},"ui_framework.views":{ViewViewSet:[6,1,1,""],WorkspaceViewSet:[6,1,1,""],WorkspaceViewViewSet:[6,1,1,""]},"ui_framework.views.ViewViewSet":{basename:[6,2,1,""],description:[6,2,1,""],detail:[6,2,1,""],name:[6,2,1,""],queryset:[6,2,1,""],search:[6,3,1,""],serializer_class:[6,2,1,""],suffix:[6,2,1,""],summary:[6,3,1,""]},"ui_framework.views.WorkspaceViewSet":{basename:[6,2,1,""],description:[6,2,1,""],detail:[6,2,1,""],full:[6,3,1,""],name:[6,2,1,""],queryset:[6,2,1,""],serializer_class:[6,2,1,""],suffix:[6,2,1,""],with_view_name:[6,3,1,""]},"ui_framework.views.WorkspaceViewViewSet":{basename:[6,2,1,""],description:[6,2,1,""],detail:[6,2,1,""],name:[6,2,1,""],queryset:[6,2,1,""],serializer_class:[6,2,1,""],suffix:[6,2,1,""]},api:{admin:[0,0,0,"-"],apps:[0,0,0,"-"],authentication:[0,0,0,"-"],middleware:[0,0,0,"-"],models:[0,0,0,"-"],schema_validator:[0,0,0,"-"],serializers:[0,0,0,"-"],signals:[0,0,0,"-"],tests:[1,0,0,"-"],urls:[0,0,0,"-"],views:[0,0,0,"-"]},manager:{asgi:[3,0,0,"-"],routing:[3,0,0,"-"],settings:[3,0,0,"-"],urls:[3,0,0,"-"],utils:[3,0,0,"-"],wsgi:[3,0,0,"-"]},subscription:{apps:[5,0,0,"-"],auth:[5,0,0,"-"],consumers:[5,0,0,"-"],heartbeat_manager:[5,0,0,"-"],routing:[5,0,0,"-"]},ui_framework:{admin:[6,0,0,"-"],apps:[6,0,0,"-"],models:[6,0,0,"-"],serializers:[6,0,0,"-"],signals:[6,0,0,"-"],tests:[7,0,0,"-"],urls:[6,0,0,"-"],views:[6,0,0,"-"]}},objnames:{"0":["py","module","Python module"],"1":["py","class","Python class"],"2":["py","attribute","Python attribute"],"3":["py","method","Python method"],"4":["py","exception","Python exception"],"5":["py","function","Python function"],"6":["py","data","Python data"]},objtypes:{"0":"py:module","1":"py:class","2":"py:attribute","3":"py:method","4":"py:exception","5":"py:function","6":"py:data"},terms:{"abstract":[0,6],"boolean":[0,1],"byte":6,"case":[0,5,6,9],"class":[0,1,3,5,6,7],"default":[0,1,3,9,10,12],"final":0,"float":3,"function":[0,3,5,6],"import":[0,3,6],"int":[0,3,6],"new":[6,7,10],"null":10,"public":12,"return":[0,3,5,6,7,10],"static":[1,3,6],"super":6,"true":[0,1,3,6,10],"try":[6,10],"while":9,And:[0,10],For:[0,3,6,9,10,11],The:[0,3,5,6,9,10,11,12],Then:9,These:[4,9],Use:[0,8,10],Uses:5,__all__:[0,6],abort:10,abov:9,accept:10,access:[0,3,6,9,11],accessor:6,accord:[0,5],ack:10,acknowledg:[9,10,11],act:[9,12],action:[5,8,9],actual:0,add:[0,3,6,7],added:[7,10],adding:7,addit:9,addition:9,additionalproperti:1,additt:0,admin123:3,admin:[3,4,10,12],admin_user_pass:12,afer:1,against:[0,10],alarm:10,alarms_sound:0,alia:[0,6],all:[5,7,9,10,11,12],allow:[3,10],allowed_host:3,allski:10,also:[9,11],alter:0,among:9,ani:[0,10,11],anonymousus:5,anoth:[0,3,6,11],api:[4,5,6,7,8,11],apiconfig:0,apidoc:[0,8,9,10],app:[3,4,9],app_modul:[0,5,6],app_nam:[0,5,6],appconfig:[0,5,6],append:10,appli:[0,9],applic:[0,2,3,5,9,10,12],appliedsettingsmatchstart:10,arg:[0,5,6],argument:[0,6],as_view:[0,3,6],asgi:4,asgi_appl:3,assert:3,assert_time_data:3,associ:[5,6],async:5,asynchron:10,asyncjsonwebsocketconsum:5,atcamera:6,atmount:6,atomic_request:3,atpneumat:10,atptg:10,attach:10,attribut:[0,6],auth:[0,4],auth_ldap_server_uri:[3,12],authapitestcas:1,authent:[1,4,5,8,9,11,12],authenticate_credenti:0,authenticationfail:0,authlist:10,author:[0,1,7,10,11],authorizedcrudtestcas:7,authtoken:0,autocommit:3,autocomplet:0,autogener:[0,6],autoupd:[0,6],avail:[0,6,7,10],back:11,backend:[3,10],base64:6,base64imagefield:6,base:[0,1,3,5,6,7,10,11],base_url:6,basemodel:[0,6],basenam:[0,6],basetestcas:7,bash:12,basic:9,becaus:9,been:0,befor:[0,1,12],behavior:7,behind:10,being:[0,3],below:[6,9],between:[0,3,5,10,11],blog:[0,3,6],bool:[0,3,6],both:11,browsabl:10,build:5,built:6,call:5,callabl:3,camera:[6,10],camfe:10,can:[0,1,7,9,10,12],can_execute_command:0,cannot:[1,7,12],categori:[5,10],ceh:3,cell:0,central:6,certain:10,chang:10,channel:[3,5,10,11,12],channel_lay:3,channels_redi:3,character:10,charg:9,charset:3,check:[0,6,11],checkpoint:1,child:6,children:6,classmethod:0,cleanup:1,client:[7,9,10,11],close:5,close_cod:5,cmd:[10,12],cmd_acknowledg:10,cmd_user_pass:12,code:[0,6,8,10,12],collat:3,com:[0,1,3,6],combin:10,command:[0,1,5,8,11,12],command_data:[0,10],command_nam:[0,10],command_name_1:10,command_name_2:10,commandertestcas:1,common:7,commun:[9,11],compar:9,compon:[5,9,10],compos:[9,10,12],concaten:6,conext:10,confi:0,config:[0,1,3,5,6],config_fil:0,config_path:10,configfil:[0,9],configfileapitestcas:1,configfilecontentseri:0,configfileseri:0,configfileviewset:0,configseri:0,configur:[0,1,3,5,6,9,10,12],confirm:10,conn_max_ag:3,connect:[3,5,8,9,11,12],constitut:6,consum:[0,4,10,11],contact:0,contact_info:0,contain:[0,3,5,6,9,10,12],content:[4,8,9,10],contian:10,contrib:[0,6],copi:[0,12],core:[0,3,6],correct:3,correspond:[0,5,6,9,10,12],could:[0,10],creat:[0,3,7,9],create_doc:12,create_forward_many_to_many_manag:6,creation:[0,6,10],creation_timestamp:[0,6,10],credenti:[1,9,10,11],critic:[0,10],crud:[7,9,10],csc:[0,5,10,11],csc_1:10,csc_2:10,cscg:6,cscsummari:6,currenlti:0,current:[0,3,5,6,9,10],custom:[0,5,6,7],customobtainauthtoken:0,customswapauthtoken:0,dai:3,dalet:7,data1:10,data2:10,data:[0,1,3,5,6,9,11],data_dict:0,databas:[0,3,7,12],date:[0,3,5,10],datetimefield:[0,6],db_engin:12,db_host:12,db_name:12,db_pass:12,db_port:12,db_user:12,debug:12,decod:6,decoded_fil:6,def:6,defaultingvalid:0,defaults_valid:0,defer:[0,6],defin:[0,1,3,5,6,7,9,10,12],deleg:6,delet:[0,1,6,7,9],deploy:[3,12],deprec:12,describ:[0,10,12],descript:[0,1,6],detail:[0,6,9,10,11],dev:9,develop:[8,9],dict:[0,3,5,6],dictionari:[0,3,5,6,7,10],diffent:9,differ:[0,1,3,5,9,10,11],directory_permissions_mod:6,disabl:10,disconnect:5,disk:6,dispatch:5,displai:6,divid:9,django:[0,1,2,3,5,6,7,9,11,12],djangoproject:[0,3,6],djangosnippet:6,djangpo:[0,6],doc:[0,3,6],docker:9,docsrc:12,doe:[0,12],doesnotexist:[0,6],dome:6,done:[9,10,11],draft7valid:0,draft:1,drf:[0,11],durat:3,dynam:6,each:[0,1,5,6,9,10],edit:[10,12],either:10,element:6,email:[0,10],emergencycontact:[0,9],emergencycontactapitestcas:1,emergencycontactseri:0,emergencycontactviewset:0,empti:[0,6,10,12],enabl:10,encod:6,end:10,endpoint:[0,6,9,10],engin:[3,12],enhanc:[0,6],entercontrol:10,entrypoint:3,env:12,environ:[3,11],error:[0,9,10],errorcod:10,establish:[9,10,11,12],etc:9,event:[5,11],event_data:[0,10],event_nam:[0,10],event_name_1:10,event_name_2:10,everi:[0,6,9,11,12],exampl:[0,3,6,8,10],except:[0,1,6],exec:12,execut:[0,2,6,9,11,12],execute_command:10,exitcontrol:10,expect:[3,5,6,10],expet:0,expir:[0,1],expires_in:0,expiringtokenauthent:0,explain:9,expos:[0,3,6],extens:6,fail:[0,1],fail_cleanup:1,fail_run:1,failur:10,fals:[0,1,3,6,10],fer:11,field:[0,6,7,10],field_11:10,field_12:10,field_21:10,field_22:10,figur:[9,11],file:[0,1,3,6,7,8,9],file_nam:[0,6],file_permissions_mod:6,filenam:[0,6],filesystemstorag:6,final_valid:0,first:[0,6],fixtur:9,flag:[0,10],folder:12,follow:[0,3,5,9,10,11,12],foreignkei:[0,6],format:[0,3,5,9,10],forward:[6,9,10,11],found:[5,6],framework:[6,7,8,11],free:6,frequenc:10,frmework:9,from:[0,3,5,6,7,9,10,11,12],frontend:[9,11,12],full:[3,6],fulli:[6,7,10],further:11,gencam:10,gener:[0,3,5,6,10,11],genera:9,get:[0,1,3,5,6],get_available_nam:6,get_config:0,get_config_file_sampl:1,get_cont:0,get_dict:7,get_file_extens:6,get_filenam:0,get_next_by_cr:0,get_next_by_creation_timestamp:[0,6],get_next_by_update_timestamp:[0,6],get_permiss:0,get_previous_by_cr:0,get_previous_by_creation_timestamp:[0,6],get_previous_by_update_timestamp:[0,6],get_respons:0,get_sorted_view:6,get_tai_to_utc:3,get_tim:3,get_time_data:[0,5,10],get_token:0,get_us:5,get_usernam:0,gettokenmiddlewar:0,github:[1,6,12],given:[0,5,6,7,9,10],globalpermiss:0,greenwich:[0,3,5,10],group:[0,5,9,10,11],handl:[0,1,5,6,9,11],handle_action_messag:5,handle_data_messag:5,handle_heartbeat_messag:5,handle_subscription_messag:5,handle_token_delet:0,handler:9,hanlde_view_delet:6,has:[0,3,6,9,11,12],has_read_permiss:6,have:[0,5,10,11],header:10,healthstatussummari:6,heartbeat:5,heartbeat_manag:4,heartbeatmanag:5,heavili:6,here:12,hola:10,home:[0,3,6],host:[3,12],hourangl:[0,3,5,10],how:[8,11],howto:3,html:12,http:[0,1,3,6,9,10,11,12],identifi:10,ids:[7,10],imag:[6,9],imagefield:6,implement:[6,12],includ:[0,3,6,10,11],incom:5,index:[0,3,8,10,12],info:[9,11],inform:[0,3,6,9,10],inherit:7,ini:9,initi:9,inner:5,input:[5,10],insid:[9,12],instanc:[0,3,5,6,9,10,11],instead:[10,12],instruct:[0,12],integr:12,intend:[5,10],interfac:[0,6],intermediari:[10,11],intern:[5,6,10],invalid:[0,1,9,10],is_next:[0,6],is_token_expir:0,iso:10,its:[6,7,9,10,12],ivalid:0,join:5,jpg:6,json:[0,1,5,6,9,10],jsonschema:0,julian:[0,3,5,10],kei:[0,3,6],key11:5,key12:5,key1:10,key21:5,key22:5,key2:10,keyword:0,kwarg:[0,5,6],languag:3,language_cod:3,last:[5,10],lastli:11,latiss:6,latter:9,layer:[3,10,12],layout:9,ldap:[3,12],leav:5,length:6,less:0,level:[0,3],librari:9,lightpath:6,like:[6,9,10],list:[0,3,5,6,7,10],load:[0,6,8],local:[0,8],localhost:3,locat:[0,3,5,6,10,12],log:[0,1,11],login:[0,9],login_failed_respons:0,login_respons:0,loglevel:10,logmessag:10,logout:[0,1,5],logslog:6,love:[0,1,3,5,11],love_csc:10,lovecsc_observinglog:0,lovecsctestcas:1,lsst:[1,11,12],mai:10,main:[2,9],make:[0,5,6,9],manag:[0,4,5,6,7,11,12],manager_rcv:5,mani:[6,10],manytomanydescriptor:6,manytomanyfield:6,map:[0,6],match:[0,5,6],max_length:6,maxdiff:1,maximum:6,measur:[0,3],mechan:10,media:[3,10],media_url:[3,6],messag:[0,3,5,8,9,11],meta:[0,6],metadata:[0,1],method:[1,6],methodnam:[1,7],middlewar:[4,5,12],migrat:9,minimum:1,minseveritynotif:10,minseveritysound:10,mirror:3,miss:0,mjd:[0,3,5,10],mock_environ:1,mock_request:1,mode:12,model:[4,7,9,10],modelseri:[0,6],modelviewset:[0,6],modifi:[0,3,5,10],modul:[4,8,9],moment:10,more:[0,3,6,9,11],most:6,mostli:9,mount:[3,6,12],move:12,mtm1m3:10,multipleobjectsreturn:[0,6],must:[9,10,12],mute:10,my_app:[0,3,6],my_dev_password:3,myimagefieldnam:6,mymodelnam:6,name:[0,1,3,5,6,7,12],necessari:10,need:[6,11,12],network:6,never:9,nginx:3,no_config:[0,1,10],no_debug:12,none:[0,1,3,5,6,10],note:0,number:[0,1,3,5,10],numer:10,obj:[0,7],object:[0,1,5,6,7],objectdoesnotexist:[0,6],observ:[0,1,11],observinglog:[6,10],obslog:6,obtain:0,obtainauthtoken:0,off:12,onc:[9,11,12],one:[0,6,10],onli:[5,6,9,10,12],oper:[9,10,11],option:[0,3,5,10],order:[0,5,6,9,10,12],org:[1,6],organ:8,origin:[0,11],other:[0,8,9,11],other_app:[0,3,6],otherwis:[5,12],our:0,out:[10,12],output:[0,1,5,10],outsid:12,over:[6,9,10],overrid:[6,12],overview:[6,8],overwrit:6,overwritestorag:6,packag:[4,8],page:[0,6,8],param:[1,10],paramet:[0,3,5,6,10],parameter_1:10,parameter_2:10,parent:6,pars:5,part:[5,8,11],particular:[5,9,10,11],pass:[1,5,6,10],password:[1,3,9,12],patch:[0,6],path:[0,3,6,10],pattern:5,payload:10,perform:[9,10],period:5,permiss:[0,6,9,10,12],pipe:11,pizza:6,pleas:[0,3,6,9,11,12],plu:10,png:10,port:[3,12],post:[0,6,10],postgr:[3,12],postgresql:[3,9,12],prefer:0,present:0,problem:6,process_connection_pass:[3,12],produc:[3,9,10,11,12],project:[3,4,11,12],properli:7,properti:1,propos:6,provid:[0,9,10,11,12],pull:6,purpos:[7,9,12],put:[0,6,10],pytest:[9,12],python:[9,11],queri:[0,1,6,10],queryset:[0,6],rais:[0,1],random:6,raw:6,react:5,read:[0,3,6,10],readi:[0,6],readm:8,readonli:12,reason:10,rebuild:12,receiv:[0,1,5,6,9,10,11],receive_json:5,recept:5,recommend:12,redi:[3,12],redirect:11,redis_host:12,redis_pass:12,redischannellay:3,redoc:10,ref:[0,3,6],refer:[0,6,9],regist:[0,6],regular:10,reject:5,rel:10,relat:[0,6,10],related_nam:6,relationship:[7,10],relev:[0,3],remain:[0,6],remian:0,remov:0,repli:[10,11],repo:12,repons:11,repositori:12,repres:10,represent:6,request:[0,1,5,6,7,9,11],request_tim:[5,10],requet:[0,6],requier:9,requir:[1,9,10],respect:[0,3,5,9,10],respond:9,respons:[0,6,9,11],rest:[0,6,7,9,10,11],rest_framework:[0,6],restart:12,result:[0,10],retriev:[7,9],revers:6,reversemanytoonedescriptor:6,rout:[0,4,6,9],rule:[3,5],run:[1,3,5,9],runserv:9,runtest:[1,7],sal:[9,11],sal_vers:[0,10],salindex:[5,10],salinfo:[0,1,10],salinfo_metadata:0,salinfo_topic_data:0,salinfo_topic_nam:0,salinfotestcas:1,salobj:9,save:[6,9],scale:[0,3,5,10],schedul:6,schema:[0,1],schema_path:10,schema_valid:4,schemavalidationtestcas:1,scope:5,script:9,script_schema:1,scriptqueu:[5,10],search:[6,8,9],search_text:10,second:[0,1,3,5,10],secret:3,secret_kei:[3,12],section:[9,11],see:[0,3,6,9,10,12],select:10,self:[0,6],send:[0,1,5,9,10,11],send_heartbeat:5,sender:[0,6],sent:[0,1,5,6,10,11],separ:10,serial:4,serializer_class:[0,6],seriou:[0,10],server:[3,9,10,12],server_tim:10,set:[0,4,5,6,7],setauthlist:10,setloglevel:10,settingsappli:10,settingvers:10,setup:[1,7],setvalu:10,sever:0,should:[10,12],showalarm:10,shown:11,side:6,sider:[9,10],sidereal_greenwich:10,sidereal_summit:[0,3,5,10],sidereal_tim:[0,3,5,10],signal:4,similarli:[9,11],simpl:9,simulationmod:10,sky:10,skycam:10,snippet:6,softwar:[5,9],softwarevers:10,solut:6,solv:6,some:[9,10,12],sort:[6,10],sort_valu:[6,10],sound:0,sourc:10,specifi:10,sqlite3:12,sqqtest:6,src:[3,12],stablish:10,standard:[0,10],standbi:10,start:[1,7,10],startproject:3,state:0,static_root:3,static_url:3,statu:[0,10],still:7,storag:6,store:[5,6,9,10],stream1:[5,10],stream2:5,stream:[5,10],string:[0,1,3,5,6,9,10],structur:[3,5,10],submodul:4,subpackag:4,subscirpt:5,subscrib:[0,5,9,10,11],subscript:[4,8,9],subscription_all_data:5,subscription_data:5,subscriptionconfig:5,subscriptionconsum:5,subseri:[6,7,10],subsystem:0,succes:0,succesfulli:10,success:10,suffix:[0,6],suit:1,summar:[6,10],summari:6,summaryst:10,summit:[0,3,5,10],support:5,sure:9,swagger:10,swap:[0,1],symmetric_encryption_kei:3,system:[6,8,10],tabl:10,tai:[0,3,5,9,10],tai_to_utc:[0,3,5,10],target:6,task:5,tbder3gzppu:3,teelemetri:10,tel1:9,tel2:9,telemetri:[5,9,11],telemetry_data:[0,10],telemetry_nam:[0,10],telemetry_name_1:10,telemetry_name_2:10,test:[0,3,4,6,9],test_add_and_get_views_to_workspac:7,test_authorized_commander_data:1,test_authorized_create_object:7,test_authorized_delete_object:7,test_authorized_list_object:7,test_authorized_lovecsc_data:1,test_authorized_retrieve_object:7,test_authorized_update_object:7,test_command:[0,4],test_create_view:7,test_create_workspac:7,test_create_workspace_view:7,test_delete_view:7,test_delete_workspac:7,test_delete_workspace_view:7,test_get_config_fil:1,test_get_config_file_cont:1,test_get_config_files_list:1,test_get_full_workspac:7,test_get_workspaces_from_a_view:7,test_get_workspaces_with_view_nam:7,test_invalid_config:1,test_list_emergency_contact:1,test_lovecsc:[0,4],test_new_view:7,test_retrieve_view:7,test_retrieve_workspac:7,test_retrieve_workspace_view:7,test_salinfo_metadata:1,test_salinfo_topic_data:1,test_salinfo_topic_data_with_param:1,test_salinfo_topic_nam:1,test_salinfo_topic_names_with_param:1,test_schema_valid:[0,4],test_syntax_error:1,test_unauthenticated_cannot_get_config_fil:1,test_unauthenticated_create_object:7,test_unauthenticated_delete_object:7,test_unauthenticated_list_object:7,test_unauthenticated_retrieve_object:7,test_unauthenticated_update_object:7,test_unauthorized_command:1,test_unauthorized_create_object:7,test_unauthorized_delete_object:7,test_unauthorized_list_object:7,test_unauthorized_lovecsc:1,test_unauthorized_retrieve_object:7,test_unauthorized_update_object:7,test_update_view:7,test_update_workspac:7,test_update_workspace_view:7,test_user_fails_to_validate_deleted_token:1,test_user_fails_to_validate_expired_token:1,test_user_login:1,test_user_login_fail:1,test_user_login_twic:1,test_user_logout:1,test_user_swap:1,test_user_swap_forbidden:1,test_user_swap_no_config:1,test_user_swap_wrong_credenti:1,test_user_validate_token:1,test_user_validate_token_fail:1,test_user_validate_token_no_config:1,test_valid_config:1,test_view_thumbnail:[4,6],testcas:[1,7],tests_api:[4,6],tests_auth_api:[0,4],tests_configfil:[0,4],tests_custom_api:[4,6],tests_emergencycontact:[0,4],tests_model:[4,6],testscript:1,text:10,than:0,thei:[0,10],them:[0,10],therefor:11,thi:[0,3,5,6,9,10,11,12],those:0,though:5,throgh:[0,6],through:[1,6,7,9,10,11],thumbnail:[6,7,10],ticket:6,time:[0,1,3,5,6,9,10],time_data:[3,5,10],time_zon:3,timedataseri:0,timedisplai:6,timestamp:[0,3,5,6,10],timezon:3,titl:[0,1,10],to_internal_valu:6,to_represent:6,token:[0,1,3,5,9,11],token_expire_handl:0,token_expired_after_dai:3,tokenauthent:0,tokenauthmiddlewar:5,tokenauthmiddlewareinst:5,tokenseri:0,tokn:0,tomchristi:6,tool:12,top:6,topic:[0,3,6],topic_data:[0,1,10],topic_nam:[0,1,10],trace:3,trace_timestamp:3,transfer:10,transform:6,treat:10,tri:9,trigger:10,truncat:6,ts_salobj:1,turn:[9,11],twie:1,two:0,txt:9,type:[0,1,3,5,6],u3awhhg:3,ui_framework:[4,8,9,10],uiframeworkconfig:6,unacknowledg:10,unauthent:[1,7],unauthenticatedcrudtestcas:7,unauthor:[1,7],unauthorizedcrudtestcas:7,uniqu:9,unix:[0,3,5,10],unmut:10,unpars:1,unsubscrib:[5,10],unsubscript:5,unus:[0,6],updat:[0,6,7,9],update_timestamp:[0,6,10],upload:6,upon:[0,6],url:[4,5,9,10],urlconf:[0,3,6],urlpattern:[0,3,5,6],use:[0,3,5,7,8,12],used:[0,5,6,7,9,10,12],user:[0,1,3,5,6,7,9,12],user_id:0,user_user_pass:12,usernam:[0,9,10],userpermissionsseri:0,userseri:0,uses:[6,11],using:[0,1,3,9,11,12],usr:[3,12],utc:[0,3,5,9,10],util:[4,6],v0d38sjx43s8:3,valid:[0,1,9],validate_config_schema:0,validate_file_extens:0,validate_token:0,validationerror:0,validatorclass:0,valu:[0,3,6,9,10,12],value11:5,value12:5,value1:10,value21:5,value22:5,value2:10,value_11:10,value_12:10,value_21:10,value_22:10,variabl:[3,10],variou:10,vaue:10,version:10,vetween:7,via:10,view:[3,4,7,9],view_1:10,view_2:10,view_3:10,view_i:7,view_id1:10,view_id2:10,view_id3:10,view_id:[6,10],view_nam:[6,10],viewmodeltestcas:7,viewseri:6,viewset:[0,6],viewsummaryseri:6,viewthumbnailtestcas:7,viewviewset:6,visual:11,wai:[10,12],wait:[1,9],wait_tim:1,warn:[0,10],watcher:[6,10],weatherst:6,websocket:[3,5,8,9,11,12],websocket_urlpattern:5,well:10,wether:[0,3,12],when:[0,6,7,9,11],where:[10,11,12],which:[0,9,10,11,12],whise:10,who:[0,10],whole:3,with_view_nam:[6,10],within:[6,10],without:[6,10],work:[0,1,6,8,11],workflow:11,workspac:[6,7],workspace_i:7,workspace_id1:10,workspace_id:[6,10],workspace_view:[6,7],workspaceandviewsrelationstestcas:7,workspacefullseri:6,workspacemodeltestcas:7,workspaceseri:6,workspaceview:[6,7],workspaceviewmodeltestcas:7,workspaceviewseri:6,workspaceviewset:6,workspaceviewviewset:6,workspacewithviewnameseri:6,workview:6,wrapper:[0,6],write:11,written:[6,11],wsgi:4,xml:10,xml_version:[0,10],yaml:[0,1,10],you:[10,12],zuj:3},titles:["5.1. api package","5.1.1.1. api.tests package","5.2. manage module","5.3. manager package","5. ApiDoc","5.4. subscription package","5.5. ui_framework package","5.5.1.1. ui_framework.tests package","Welcome to LOVE-manager\u2019s documentation!","3. How it works","2. How to use it","1. Overview","4. Readme File"],titleterms:{Use:12,action:10,admin:[0,6],api:[0,1,9,10],apidoc:4,app:[0,5,6],asgi:3,auth:[5,9],authent:[0,10],build:12,channel:9,code:9,command:[9,10],config:10,connect:10,consum:[5,9],content:[0,1,3,5,6,7],creat:10,data:10,databas:9,delet:10,develop:12,docker:12,document:[8,12],environ:12,event:10,exampl:9,file:[10,12],framework:[9,10],full:10,get:[10,12],heartbeat:10,heartbeat_manag:5,how:[9,10],imag:12,indic:8,info:10,initi:12,layer:9,load:12,local:12,log:10,logout:10,love:[8,9,10,12],manag:[2,3,8,9,10],messag:10,metadata:10,middlewar:0,model:[0,6],modul:[0,1,2,3,5,6,7],name:10,observ:10,organ:9,other:10,overview:11,packag:[0,1,3,5,6,7],part:[9,12],password:10,readm:12,request:10,respons:10,retriev:10,rout:[3,5],run:12,sal:10,schema:10,schema_valid:0,scheme:10,search:10,serial:[0,6],set:3,signal:[0,6],submodul:[0,1,3,5,6,7],subpackag:[0,6],subscript:[5,10],summari:10,swap:10,system:12,tabl:8,telemetri:10,test:[1,7,12],test_command:1,test_lovecsc:1,test_schema_valid:1,test_view_thumbnail:7,tests_api:7,tests_auth_api:1,tests_configfil:1,tests_custom_api:7,tests_emergencycontact:1,tests_model:7,token:10,topic:10,type:10,ui_framework:[6,7],unauthent:10,unauthor:10,updat:10,url:[0,3,6],use:10,user:10,util:[3,7],valid:10,variabl:12,view:[0,6,10],websocket:10,welcom:8,work:9,workspac:10,workspaceview:10,wsgi:3}}) \ No newline at end of file diff --git a/docsrc/source/apidoc/api.tests.rst b/docsrc/source/apidoc/api.tests.rst index 232076ac..b18a5ad1 100644 --- a/docsrc/source/apidoc/api.tests.rst +++ b/docsrc/source/apidoc/api.tests.rst @@ -12,6 +12,14 @@ api.tests.test\_commander module :undoc-members: :show-inheritance: +api.tests.test\_lovecsc module +------------------------------ + +.. automodule:: api.tests.test_lovecsc + :members: + :undoc-members: + :show-inheritance: + api.tests.test\_schema\_validation module ----------------------------------------- @@ -28,10 +36,18 @@ api.tests.tests\_auth\_api module :undoc-members: :show-inheritance: -api.tests.tests\_config module ------------------------------- +api.tests.tests\_configfile module +---------------------------------- + +.. automodule:: api.tests.tests_configfile + :members: + :undoc-members: + :show-inheritance: + +api.tests.tests\_emergencycontact module +---------------------------------------- -.. automodule:: api.tests.tests_config +.. automodule:: api.tests.tests_emergencycontact :members: :undoc-members: :show-inheritance: diff --git a/docsrc/source/modules/how_it_works.rst b/docsrc/source/modules/how_it_works.rst index 63c4cf38..c3d0c8d8 100644 --- a/docsrc/source/modules/how_it_works.rst +++ b/docsrc/source/modules/how_it_works.rst @@ -62,7 +62,7 @@ Code organization Currently the application is divided in the following modules and files: -* :code:`api`: This module contains the :code:`API` Django app, which defines the models and API endpoints for authentication (:code:`Auth API`) and Commander (:code:`Commander API`) APIs. For more details please refer to the :ref:`ApiDoc` section +* :code:`api`: This module contains the :code:`API` Django app, which defines the models and API endpoints for authentication (:code:`Auth API`), Commander (:code:`Commander API`), ConfigFile (:code:`ConfigFile API`) and EmergencyContact (:code:`EmergencyContact API`) APIs. For more details please refer to the :ref:`ApiDoc` section * :code:`ui_framework`: This module contains the :code:`UI Framework` Django app, which defines the models and API endpoints for the UI Framework views (:code:`UI Framework API`) API. For more details please refer to the :ref:`ApiDoc` section * :code:`subscription`: This module contains the Django app that defines the consumers that handle the websocket communication. * :code:`manager`: This module contains basic Django configuration files, such as urls and channels routing, etc. diff --git a/docsrc/source/modules/how_to_use_it.rst b/docsrc/source/modules/how_to_use_it.rst index 5a07e37c..8916c723 100644 --- a/docsrc/source/modules/how_to_use_it.rst +++ b/docsrc/source/modules/how_to_use_it.rst @@ -65,7 +65,7 @@ Returns token, user data and permissions Validate token -------------- Validates a given authorization token, passed through HTTP Headers. -Returns a confirmation of validity, user data, permissions, server_time and (optionally) the LOVE configuraiton file. +Returns a confirmation of validity, user data, permissions, server_time and (optionally) the LOVE configuration file. If the :code:`no_config` flag is added to the end of the URL, then the LOVE config files is not read and the corresponding value is returned as :code:`null` - Url: :code:`/manager/api/validate-token/` or :code:`/manager/api/validate-token/no_config/` @@ -119,7 +119,7 @@ If the :code:`no_config` flag is added to the end of the URL, then the LOVE conf Swap token -------------- Validates a given authorization token, passed through HTTP Headers. -Returns a confirmation of validity, user data, permissions, server_time and (optionally) the LOVE configuraiton file. +Returns a confirmation of validity, user data, permissions, server_time and (optionally) the LOVE configuration file. If the :code:`no_config` flag is added to the end of the URL, then the LOVE config files is not read and the corresponding value is returned as :code:`null` - Url: :code:`/manager/api/swap-token/` or :code:`/manager/api/swap-token/no_config/` @@ -1202,3 +1202,71 @@ Delete WorkspaceView { "status": 204 } + + +EFD +============ + +Timeseries +~~~~~~~~~~~~~~~~~~~~ +Endpoint to request EFD timeseries. + +- Url: :code:`/manager/efd/timeseries` +- HTTP Operation: POST +- Message Payload: + +.. code-block:: json + + { + "start_date": "2020-03-16T12:00:00", + "time_window": 15, + "cscs": { + "ATDome": { + 0: { + "topic1": ["field1"] + }, + }, + "ATMCS": { + 1: { + "topic2": ["field2", "field3"] + }, + } + }, + "resample": "1min", + } + + +- Expected Response, if command successful: + +.. code-block:: json + + { + "status": 200, + "data": { + "ATDome-0-topic1": { + "field1": [ + { ts: "2020-03-06 21:49:41.471000", value: 0.21 }, + { ts: "2020-03-06 21:50:41.471000", value: 0.21 }, + { ts: "2020-03-06 21:51:41.471000", value: 0.21 }, + { ts: "2020-03-06 21:52:41.471000", value: 0.21 }, + { ts: "2020-03-06 21:53:41.471000", value: 0.21 } + ] + }, + "ATMCS-1-topic2": { + "field2": [ + { ts: "2020-03-06 21:49:41.471000", value: 0.21 }, + { ts: "2020-03-06 21:50:41.471000", value: 0.21 }, + { ts: "2020-03-06 21:51:41.471000", value: 0.21 }, + { ts: "2020-03-06 21:52:41.471000", value: 0.21 }, + { ts: "2020-03-06 21:53:41.471000", value: 0.21 } + ], + "field3": [ + { ts: "2020-03-06 21:49:41.471000", value: 0.21 }, + { ts: "2020-03-06 21:50:41.471000", value: 0.21 }, + { ts: "2020-03-06 21:51:41.471000", value: 0.21 }, + { ts: "2020-03-06 21:52:41.471000", value: 0.21 }, + { ts: "2020-03-06 21:53:41.471000", value: 0.21 } + ] + } + } + } \ No newline at end of file diff --git a/manager/api/__init__.py b/manager/api/__init__.py index 23ca7a69..23b0108b 100644 --- a/manager/api/__init__.py +++ b/manager/api/__init__.py @@ -1 +1 @@ -default_app_config = 'api.apps.ApiConfig' +default_app_config = "api.apps.ApiConfig" diff --git a/manager/api/admin.py b/manager/api/admin.py index c9a2815e..a96894dc 100644 --- a/manager/api/admin.py +++ b/manager/api/admin.py @@ -8,6 +8,9 @@ """ from django.contrib import admin from api.models import Token +from api.models import ConfigFile, EmergencyContact admin.site.register(Token) +admin.site.register(ConfigFile) +admin.site.register(EmergencyContact) diff --git a/manager/api/authentication.py b/manager/api/authentication.py index 107efa34..ebd7e038 100644 --- a/manager/api/authentication.py +++ b/manager/api/authentication.py @@ -55,7 +55,7 @@ def authenticate_credentials(self, key): return (token.user, token) @classmethod - def token_expire_handler(self, token): + def token_expire_handler(cls, token): """Check if a given token is expired or not, if it is the Token is deleted. Params @@ -68,14 +68,13 @@ def token_expire_handler(self, token): boolean, Token: True if it is expired, False if not; and the Token object """ - is_expired = self.is_token_expired(token) + is_expired = cls.is_token_expired(token) if is_expired: token.delete() - # token = Token.objects.create(user=token.user) # This line allows creation of a new token after expiration return is_expired, token @classmethod - def is_token_expired(self, token): + def is_token_expired(cls, token): """Check if a given token is expired or not. Params @@ -88,10 +87,10 @@ def is_token_expired(self, token): boolean True if it is expired, False if not """ - return self.expires_in(token) < timedelta(seconds=0) + return cls.expires_in(token) < timedelta(seconds=0) @classmethod - def expires_in(self, token): + def expires_in(cls, token): """Return the remaining time of a given token. Params diff --git a/manager/api/fixtures/configs/default.json b/manager/api/fixtures/configs/default.json new file mode 100644 index 00000000..b2a7258b --- /dev/null +++ b/manager/api/fixtures/configs/default.json @@ -0,0 +1,10 @@ +{ + "alarms": { + "minSeveritySound": "mute", + "minSeverityNotification": "mute" + }, + "camFeeds": { + "generic": "/gencam", + "allSky": "/gencam" + } +} diff --git a/manager/api/fixtures/initial_data.json b/manager/api/fixtures/initial_data.json new file mode 100644 index 00000000..df5bce65 --- /dev/null +++ b/manager/api/fixtures/initial_data.json @@ -0,0 +1,37 @@ +[ + { + "model": "api.configfile", + "pk": 1, + "fields": { + "creation_timestamp": "2020-12-29T14:21:31.040Z", + "update_timestamp": "2020-12-29T14:21:31.040Z", + "user": 1, + "file_name": "default.json", + "config_file": "configs/default.json" + } + }, + { + "model": "api.emergencycontact", + "pk": 1, + "fields": { + "creation_timestamp": "2021-01-05T17:03:55.382Z", + "update_timestamp": "2021-01-05T17:03:55.382Z", + "subsystem": "ATDome", + "name": "Name Lastname", + "contact_info": "+5689846515", + "email": "name@email.com" + } + }, + { + "model": "api.emergencycontact", + "pk": 2, + "fields": { + "creation_timestamp": "2021-01-05T17:04:24.127Z", + "update_timestamp": "2021-01-05T17:04:24.127Z", + "subsystem": "ATMCS", + "name": "Name2 Lastname2", + "contact_info": "none", + "email": "name2@email.com" + } + } +] diff --git a/manager/api/management/commands/createusers.py b/manager/api/management/commands/createusers.py index fc6589bc..50fc15b9 100644 --- a/manager/api/management/commands/createusers.py +++ b/manager/api/management/commands/createusers.py @@ -4,11 +4,11 @@ from django.contrib.auth.models import Permission, Group, User from django.core.management.base import BaseCommand -user_username = 'user' -cmd_user_username = 'cmd_user' -admin_username = 'admin' -cmd_groupname = 'cmd' -test_username = 'test' +user_username = "user" +cmd_user_username = "cmd_user" +admin_username = "admin" +cmd_groupname = "cmd" +test_username = "test" class Command(BaseCommand): @@ -41,7 +41,7 @@ class Command(BaseCommand): "cmd_user" and "test" users belong to "cmd_group".""" requires_migrations_checks = True - stealth_options = ('stdin',) + stealth_options = ("stdin",) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -72,16 +72,14 @@ def add_arguments(self, parser): parser for the arguments """ parser.add_argument( - '--adminpass', - help='Specifies the password for the "admin" user.' + "--adminpass", help='Specifies the password for the "admin" user.' ) parser.add_argument( - '--userpass', - help='Specifies the password for the regular users ("user").' + "--userpass", help='Specifies the password for the regular users ("user").' ) parser.add_argument( - '--cmduserpass', - help='Specifies password for the users with cmd permissions ("cmd_user" and "test").' + "--cmduserpass", + help='Specifies password for the users with cmd permissions ("cmd_user" and "test").', ) def handle(self, *args, **options): @@ -94,10 +92,9 @@ def handle(self, *args, **options): kwargs: dict Dictionary with addittional keyword arguments (indexed by keys in the dict) """ - admin_password = options['adminpass'] - user_password = options['userpass'] - cmd_password = options['cmduserpass'] - cmd_password = options['cmduserpass'] + admin_password = options["adminpass"] + user_password = options["userpass"] + cmd_password = options["cmduserpass"] # Create users admin = self._create_user(admin_username, admin_password) @@ -132,23 +129,24 @@ def _create_user(self, username, password): user: User The User object """ - while password is None or password.strip() == '': - print('Creating {}...'.format(username)) + while password is None or password.strip() == "": + print("Creating {}...".format(username)) password = getpass.getpass() - if password.strip() == '': + if password.strip() == "": self.stderr.write("Error: Blank passwords aren't allowed.") password = None - continue user = User.objects.filter(username=username).first() if not user: user = User.objects.create_user( username=username, - email='{}@fake.com'.format(username), - password=password + email="{}@fake.com".format(username), + password=password, ) else: - self.stderr.write("Warning: The {} user is already created".format(username)) + self.stderr.write( + "Warning: The {} user is already created".format(username) + ) return user def _create_cmd_group(self): @@ -160,7 +158,7 @@ def _create_cmd_group(self): The Group object """ group, created = Group.objects.get_or_create(name=cmd_groupname) - permissions = Permission.objects.filter(codename='command.execute_command') + permissions = Permission.objects.filter(codename="command.execute_command") for permission in permissions: group.permissions.add(permission) return group diff --git a/manager/api/management/commands/tests.py b/manager/api/management/commands/tests.py index abd532e9..3585a10b 100644 --- a/manager/api/management/commands/tests.py +++ b/manager/api/management/commands/tests.py @@ -9,7 +9,7 @@ cmd_groupname, ) -cmd_permission_codename = 'api.command.execute_command' +cmd_permission_codename = "api.command.execute_command" class CreateusersTestCase(TestCase): @@ -23,33 +23,39 @@ def test_command_creates_users(self): command = Command() # Act: options = { - 'adminpass': 'admin_pass', - 'userpass': 'user_pass', - 'cmduserpass': 'cmd_pass', + "adminpass": "admin_pass", + "userpass": "user_pass", + "cmduserpass": "cmd_pass", } command.handle(*[], **options) # Assert: - self.assertEqual(User.objects.count(), old_users_num + 4, 'There are no new users') - self.assertEqual(Group.objects.count(), old_groups_num + 1, 'There is no new group') + self.assertEqual( + User.objects.count(), old_users_num + 4, "There are no new users" + ) + self.assertEqual( + Group.objects.count(), old_groups_num + 1, "There is no new group" + ) admin = User.objects.filter(username=admin_username).first() user = User.objects.filter(username=user_username).first() cmd_user = User.objects.filter(username=cmd_user_username).first() cmd_group = Group.objects.filter(name=cmd_groupname).first() - self.assertTrue(admin, 'The {} user was not created'.format(admin_username)) - self.assertTrue(user, 'The {} user was not created'.format(user_username)) - self.assertTrue(cmd_user, 'The {} user was not created'.format(cmd_user_username)) - self.assertTrue(cmd_group, 'The {} group was not created'.format(cmd_groupname)) + self.assertTrue(admin, "The {} user was not created".format(admin_username)) + self.assertTrue(user, "The {} user was not created".format(user_username)) + self.assertTrue( + cmd_user, "The {} user was not created".format(cmd_user_username) + ) + self.assertTrue(cmd_group, "The {} group was not created".format(cmd_groupname)) self.assertTrue( admin.has_perm(cmd_permission_codename), - '{} user should have cmd_execute permissions'.format(admin_username) + "{} user should have cmd_execute permissions".format(admin_username), ) self.assertFalse( user.has_perm(cmd_permission_codename), - '{} user should not have cmd_execute permissions'.format(user_username) + "{} user should not have cmd_execute permissions".format(user_username), ) self.assertTrue( cmd_user.has_perm(cmd_permission_codename), - '{} user should have cmd_execute permissions'.format(cmd_user_username) + "{} user should have cmd_execute permissions".format(cmd_user_username), ) def test_command_sets_permissions_even_if_users_already_existed(self): @@ -59,42 +65,44 @@ def test_command_sets_permissions_even_if_users_already_existed(self): command = Command() User.objects.create_user( username=admin_username, - email='{}@fake.com'.format(admin_username), - password='dummy-pass' + email="{}@fake.com".format(admin_username), + password="dummy-pass", ) User.objects.create_user( username=user_username, - email='{}@fake.com'.format(user_username), - password='dummy-pass' + email="{}@fake.com".format(user_username), + password="dummy-pass", ) User.objects.create_user( username=cmd_user_username, - email='{}@fake.com'.format(cmd_user_username), - password='dummy-pass' + email="{}@fake.com".format(cmd_user_username), + password="dummy-pass", ) # Act: options = { - 'adminpass': 'admin_pass', - 'userpass': 'user_pass', - 'cmduserpass': 'cmd_pass', + "adminpass": "admin_pass", + "userpass": "user_pass", + "cmduserpass": "cmd_pass", } command.handle(*[], **options) # Assert: - self.assertEqual(Group.objects.count(), old_groups_num + 1, 'There is no new group') + self.assertEqual( + Group.objects.count(), old_groups_num + 1, "There is no new group" + ) admin = User.objects.filter(username=admin_username).first() user = User.objects.filter(username=user_username).first() cmd_user = User.objects.filter(username=cmd_user_username).first() cmd_group = Group.objects.filter(name=cmd_groupname).first() - self.assertTrue(cmd_group, 'The {} group was not created'.format(cmd_groupname)) + self.assertTrue(cmd_group, "The {} group was not created".format(cmd_groupname)) self.assertTrue( admin.has_perm(cmd_permission_codename), - '{} user should have cmd_execute permissions'.format(admin_username) + "{} user should have cmd_execute permissions".format(admin_username), ) self.assertFalse( user.has_perm(cmd_permission_codename), - '{} user should not have cmd_execute permissions'.format(user_username) + "{} user should not have cmd_execute permissions".format(user_username), ) self.assertTrue( cmd_user.has_perm(cmd_permission_codename), - '{} user should have cmd_execute permissions'.format(cmd_user_username) + "{} user should have cmd_execute permissions".format(cmd_user_username), ) diff --git a/manager/api/middleware.py b/manager/api/middleware.py index 40415612..a47a4224 100644 --- a/manager/api/middleware.py +++ b/manager/api/middleware.py @@ -20,7 +20,9 @@ def __call__(self, request): Response: The corresponding response object """ - if request.META['PATH_INFO'] == '/manager/api/get-token/': - if 'HTTP_COOKIE' in request.META: - request.META['HTTP_COOKIE'] = '' + if ( + request.META["PATH_INFO"] == "/manager/api/get-token/" + and "HTTP_COOKIE" in request.META + ): + request.META["HTTP_COOKIE"] = "" return self.get_response(request) diff --git a/manager/api/migrations/0001_initial.py b/manager/api/migrations/0001_initial.py index d984a05b..e142510d 100644 --- a/manager/api/migrations/0001_initial.py +++ b/manager/api/migrations/0001_initial.py @@ -15,17 +15,41 @@ class Migration(migrations.Migration): operations = [ migrations.CreateModel( - name='Token', + name="Token", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', models.DateTimeField(auto_now_add=True, verbose_name='Created')), - ('key', models.CharField(db_index=True, max_length=40, unique=True, verbose_name='Key')), - ('name', models.CharField(max_length=64, verbose_name='Name')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='auth_tokens', to=settings.AUTH_USER_MODEL, verbose_name='User')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "created", + models.DateTimeField(auto_now_add=True, verbose_name="Created"), + ), + ( + "key", + models.CharField( + db_index=True, max_length=40, unique=True, verbose_name="Key" + ), + ), + ("name", models.CharField(max_length=64, verbose_name="Name")), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="auth_tokens", + to=settings.AUTH_USER_MODEL, + verbose_name="User", + ), + ), ], ), migrations.AlterUniqueTogether( - name='token', - unique_together={('user', 'name')}, + name="token", + unique_together={("user", "name")}, ), ] diff --git a/manager/api/migrations/0002_auto_20190528_1546.py b/manager/api/migrations/0002_auto_20190528_1546.py index 82446b6e..0c1059c9 100644 --- a/manager/api/migrations/0002_auto_20190528_1546.py +++ b/manager/api/migrations/0002_auto_20190528_1546.py @@ -6,20 +6,20 @@ class Migration(migrations.Migration): dependencies = [ - ('api', '0001_initial'), + ("api", "0001_initial"), ] operations = [ migrations.AlterModelOptions( - name='token', - options={'verbose_name': 'Token', 'verbose_name_plural': 'Tokens'}, + name="token", + options={"verbose_name": "Token", "verbose_name_plural": "Tokens"}, ), migrations.AlterUniqueTogether( - name='token', + name="token", unique_together=set(), ), migrations.RemoveField( - model_name='token', - name='name', + model_name="token", + name="name", ), ] diff --git a/manager/api/migrations/0003_auto_20190528_1552.py b/manager/api/migrations/0003_auto_20190528_1552.py index c3f18aa1..d86f3b53 100644 --- a/manager/api/migrations/0003_auto_20190528_1552.py +++ b/manager/api/migrations/0003_auto_20190528_1552.py @@ -6,13 +6,19 @@ class Migration(migrations.Migration): dependencies = [ - ('api', '0002_auto_20190528_1546'), + ("api", "0002_auto_20190528_1546"), ] operations = [ migrations.AlterField( - model_name='token', - name='key', - field=models.CharField(blank=True, db_index=True, max_length=40, unique=True, verbose_name='Key'), + model_name="token", + name="key", + field=models.CharField( + blank=True, + db_index=True, + max_length=40, + unique=True, + verbose_name="Key", + ), ), ] diff --git a/manager/api/migrations/0004_globalpermissions.py b/manager/api/migrations/0004_globalpermissions.py index a76c113b..918bc016 100644 --- a/manager/api/migrations/0004_globalpermissions.py +++ b/manager/api/migrations/0004_globalpermissions.py @@ -6,18 +6,32 @@ class Migration(migrations.Migration): dependencies = [ - ('api', '0003_auto_20190528_1552'), + ("api", "0003_auto_20190528_1552"), ] operations = [ migrations.CreateModel( - name='GlobalPermissions', + name="GlobalPermissions", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), ], options={ - 'permissions': (('Commands.execute_commands', 'Execute Commands'), ('ScriptQueue.run_scripts', 'Run and Requeue scripts in ScriptQueues')), - 'managed': False, + "permissions": ( + ("Commands.execute_commands", "Execute Commands"), + ( + "ScriptQueue.run_scripts", + "Run and Requeue scripts in ScriptQueues", + ), + ), + "managed": False, }, ), ] diff --git a/manager/api/migrations/0005_auto_20190722_1622.py b/manager/api/migrations/0005_auto_20190722_1622.py index a5dbbd20..487125e7 100644 --- a/manager/api/migrations/0005_auto_20190722_1622.py +++ b/manager/api/migrations/0005_auto_20190722_1622.py @@ -6,12 +6,18 @@ class Migration(migrations.Migration): dependencies = [ - ('api', '0004_globalpermissions'), + ("api", "0004_globalpermissions"), ] operations = [ migrations.AlterModelOptions( - name='globalpermissions', - options={'managed': False, 'permissions': (('command.execute_command', 'Execute Commands'), ('command.run_script', 'Run and Requeue scripts in ScriptQueues'))}, + name="globalpermissions", + options={ + "managed": False, + "permissions": ( + ("command.execute_command", "Execute Commands"), + ("command.run_script", "Run and Requeue scripts in ScriptQueues"), + ), + }, ), ] diff --git a/manager/api/migrations/0006_configfile.py b/manager/api/migrations/0006_configfile.py new file mode 100644 index 00000000..272abeb6 --- /dev/null +++ b/manager/api/migrations/0006_configfile.py @@ -0,0 +1,62 @@ +# Generated by Django 3.0.7 on 2020-12-28 23:34 + +import api.models +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("api", "0005_auto_20190722_1622"), + ] + + operations = [ + migrations.CreateModel( + name="ConfigFile", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "creation_timestamp", + models.DateTimeField( + auto_now_add=True, verbose_name="Creation time" + ), + ), + ( + "update_timestamp", + models.DateTimeField(auto_now=True, verbose_name="Last Updated"), + ), + ("file_name", models.CharField(blank=True, max_length=30)), + ( + "config_file", + models.FileField( + default="configs/default.json", + upload_to="configs/", + validators=[api.models.ConfigFile.validate_file_extension], + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="config_files", + to=settings.AUTH_USER_MODEL, + verbose_name="User", + ), + ), + ], + options={ + "abstract": False, + }, + ), + ] diff --git a/manager/api/migrations/0007_emergencycontact.py b/manager/api/migrations/0007_emergencycontact.py new file mode 100644 index 00000000..c43124ca --- /dev/null +++ b/manager/api/migrations/0007_emergencycontact.py @@ -0,0 +1,44 @@ +# Generated by Django 3.0.7 on 2021-01-05 13:25 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("api", "0006_configfile"), + ] + + operations = [ + migrations.CreateModel( + name="EmergencyContact", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "creation_timestamp", + models.DateTimeField( + auto_now_add=True, verbose_name="Creation time" + ), + ), + ( + "update_timestamp", + models.DateTimeField(auto_now=True, verbose_name="Last Updated"), + ), + ("subsystem", models.CharField(blank=True, max_length=100)), + ("name", models.CharField(blank=True, max_length=100)), + ("contact_info", models.CharField(blank=True, max_length=100)), + ("email", models.EmailField(max_length=254)), + ], + options={ + "abstract": False, + }, + ), + ] diff --git a/manager/api/models.py b/manager/api/models.py index c121d728..772ecb0f 100644 --- a/manager/api/models.py +++ b/manager/api/models.py @@ -4,21 +4,48 @@ For more information see: https://docs.djangoproject.com/en/2.2/topics/db/models/ """ +import os from django.conf import settings from django.db import models from django.utils.translation import ugettext_lazy as _ +from django.core.exceptions import ValidationError import rest_framework.authtoken.models +from ui_framework.models import OverwriteStorage + + +class BaseModel(models.Model): + """Base Model for the models of this app.""" + + class Meta: + """Define attributes of the Meta class.""" + + abstract = True + """Make this an abstract class in order to be used as an enhanced base model""" + + creation_timestamp = models.DateTimeField( + auto_now_add=True, editable=False, verbose_name="Creation time" + ) + """Creation timestamp, autogenerated upon creation""" + + update_timestamp = models.DateTimeField( + auto_now=True, editable=False, verbose_name="Last Updated" + ) + """Update timestamp, autogenerated upon creation and autoupdated on every update""" class Token(rest_framework.authtoken.models.Token): """Custome Token model with ForeignKey relation to User model. Based on the DRF Token model.""" - key = models.CharField(_("Key"), max_length=40, db_index=True, unique=True, blank=True) + key = models.CharField( + _("Key"), max_length=40, db_index=True, unique=True, blank=True + ) """ Key attribute (the token string). It is no longer primary key, but still indexed and unique""" user = models.ForeignKey( - settings.AUTH_USER_MODEL, related_name='auth_tokens', - on_delete=models.CASCADE, verbose_name=_("User") + settings.AUTH_USER_MODEL, + related_name="auth_tokens", + on_delete=models.CASCADE, + verbose_name=_("User"), ) """ Relation to User model, it is a ForeignKey, so each user can have more than one token""" @@ -43,7 +70,51 @@ class Meta: """boolean: Define wether or not the model will be managed by the ORM (saved in the DB)""" permissions = ( - ('command.execute_command', 'Execute Commands'), - ('command.run_script', 'Run and Requeue scripts in ScriptQueues'), + ("command.execute_command", "Execute Commands"), + ("command.run_script", "Run and Requeue scripts in ScriptQueues"), ) """((string, string)): Tuple defining permissions in the format ((, ))""" + + +class ConfigFile(BaseModel): + """ConfigFile Model, that includes actual configuration files, creation date and user.""" + + def validate_file_extension(value): + ext = os.path.splitext(value.name)[1] # [0] returns path+filename + valid_extensions = [".json", ".sh"] + if not ext.lower() in valid_extensions: + raise ValidationError("Unsupported file extension.") + + user = models.ForeignKey( + settings.AUTH_USER_MODEL, + related_name="config_files", + on_delete=models.CASCADE, + verbose_name="User", + ) + """User who created the config file""" + + file_name = models.CharField(max_length=30, blank=True) + """The custom name for the configuration""" + + config_file = models.FileField( + upload_to="configs/", + default="configs/default.json", + validators=[validate_file_extension], + ) + """Reference to the config file""" + + +class EmergencyContact(BaseModel): + """EmergencyContact Model""" + + subsystem = models.CharField(max_length=100, blank=True) + """EC's subsystem""" + + name = models.CharField(max_length=100, blank=True) + """EC name""" + + contact_info = models.CharField(max_length=100, blank=True) + """EC's preferred contact information (work number, cell, none)""" + + email = models.EmailField(max_length=254) + """EC's email""" diff --git a/manager/api/schema_validator.py b/manager/api/schema_validator.py index 58c7a40e..ed791887 100644 --- a/manager/api/schema_validator.py +++ b/manager/api/schema_validator.py @@ -45,6 +45,26 @@ class DefaultingValidator: the data being validated. """ + @staticmethod + def set_default_properties(properties, skip_properties, instance): + for prop, subschema in properties.items(): + if not isinstance(subschema, dict): + continue + if not isinstance(instance, dict): + continue + if prop in skip_properties: + continue + if "default" in subschema: + instance.setdefault(prop, subschema["default"]) + elif subschema.get("type") == "object" and "properties" in subschema: + # Handle defaults for one level deep sub-object. + subdefaults = {} + for subpropname, subpropvalue in subschema["properties"].items(): + if "default" in subpropvalue: + subdefaults[subpropname] = subpropvalue["default"] + if subdefaults: + instance.setdefault(prop, subdefaults) + def __init__(self, schema, ValidatorClass=jsonschema.Draft7Validator): ValidatorClass.check_schema(schema) self.final_validator = ValidatorClass(schema=schema) @@ -88,23 +108,9 @@ def set_defaults(validator, properties, instance, schema): "uniqueItems", ) ) - for prop, subschema in properties.items(): - if not isinstance(subschema, dict): - continue - if not isinstance(instance, dict): - continue - if prop in skip_properties: - continue - if "default" in subschema: - instance.setdefault(prop, subschema["default"]) - elif subschema.get("type") == "object" and "properties" in subschema: - # Handle defaults for one level deep sub-object. - subdefaults = {} - for subpropname, subpropvalue in subschema["properties"].items(): - if "default" in subpropvalue: - subdefaults[subpropname] = subpropvalue["default"] - if subdefaults: - instance.setdefault(prop, subdefaults) + DefaultingValidator.set_default_properties( + properties, skip_properties, instance + ) for error in validate_properties(validator, properties, instance, schema): yield error diff --git a/manager/api/serializers.py b/manager/api/serializers.py index 12432085..16c5f664 100644 --- a/manager/api/serializers.py +++ b/manager/api/serializers.py @@ -5,17 +5,8 @@ from rest_framework import serializers from django.contrib.auth.models import User from manager import utils - - -def read_config_file(): - url = settings.CONFIG_URL - with open(url) as f: - content = f.read() - try: - data = json.loads(content) - except ValueError: - return None - return data +from api.models import ConfigFile, EmergencyContact +from typing import Union class UserSerializer(serializers.ModelSerializer): @@ -141,7 +132,7 @@ def get_time_data(self, token) -> dict: return utils.get_times() @swagger_serializer_method(serializer_or_field=serializers.JSONField()) - def get_config(self, token) -> dict: + def get_config(self, token) -> Union[dict, None]: """Return the config file. If the 'no_config' flag is present in the url of the original request, then the file is not read and the return value is None @@ -165,4 +156,69 @@ def get_config(self, token) -> dict: if no_config: return None else: - return read_config_file() + cf = ConfigFile.objects.first() + serializer = ConfigFileContentSerializer(cf) + return serializer.data + + +class ConfigFileSerializer(serializers.ModelSerializer): + """Serializer to map the Model instance into JSON format.""" + + filename = serializers.SerializerMethodField() + username = serializers.SerializerMethodField() + + def get_username(self, obj): + return str(obj.user) + + def get_filename(self, obj): + return str(obj.file_name) + + class Meta: + """Meta class to map serializer's fields with the model fields.""" + + model = ConfigFile + """The model class to serialize""" + + fields = ( + "id", + "username", + "filename", + "creation_timestamp", + "update_timestamp", + ) + """The fields of the model class to serialize""" + + +class ConfigFileContentSerializer(serializers.ModelSerializer): + """Serializer to map the Model instance into JSON format.""" + + content = serializers.SerializerMethodField() + filename = serializers.SerializerMethodField() + + def get_content(self, obj): + return json.loads(obj.config_file.read().decode("ascii")) + + def get_filename(self, obj): + return str(obj.file_name) + + class Meta: + """Meta class to map serializer's fields with the model fields.""" + + model = ConfigFile + """The model class to serialize""" + + fields = ("id", "filename", "content", "update_timestamp") + """The fields of the model class to serialize""" + + +class EmergencyContactSerializer(serializers.ModelSerializer): + """Serializer to map the Model instance into JSON format.""" + + class Meta: + """Meta class to map serializer's fields with the model fields.""" + + model = EmergencyContact + """The model class to serialize""" + + fields = "__all__" + """The fields of the model class to serialize""" diff --git a/manager/api/signals.py b/manager/api/signals.py index ead73e11..500ce233 100644 --- a/manager/api/signals.py +++ b/manager/api/signals.py @@ -18,9 +18,9 @@ class of the sender, in this case 'Token' arguments dictionary sent with the signal. It contains the key 'instance' with the Token instance that was deleted """ - deleted_token = str(kwargs['instance']) - groupname = 'token-{}'.format(deleted_token) - payload = {'type': 'logout', 'message': ''} + deleted_token = str(kwargs["instance"]) + groupname = "token-{}".format(deleted_token) + payload = {"type": "logout", "message": ""} loop = None try: loop = asyncio.get_event_loop() diff --git a/manager/api/tests/test_commander.py b/manager/api/tests/test_commander.py index c8bd5d25..6fdb4e62 100644 --- a/manager/api/tests/test_commander.py +++ b/manager/api/tests/test_commander.py @@ -6,6 +6,9 @@ import yaml from unittest.mock import patch, call +#python manage.py test api.tests.test_commander.CommanderTestCase +#python manage.py test api.tests.test_commander.SalinfoTestCase +#python manage.py test api.tests.test_commander.EFDTestCase @override_settings(DEBUG=True) class CommanderTestCase(TestCase): @@ -16,7 +19,7 @@ def setUp(self): # Arrange self.client = APIClient() self.user = User.objects.create_user( - username="an user", + username="user", password="password", email="test@user.cl", first_name="First", @@ -53,9 +56,7 @@ def test_authorized_commander_data(self, mock_requests, mock_environ): } with self.assertRaises(ValueError): - response = self.client.post(url, data, format="json") - fakehostname = "fakehost" - fakeport = "fakeport" + self.client.post(url, data, format="json") expected_url = f"http://fakehost:fakeport/cmd" self.assertEqual(mock_requests.call_args, call(expected_url, json=data)) @@ -95,7 +96,7 @@ def setUp(self): # Arrange self.client = APIClient() self.user = User.objects.create_user( - username="an user", + username="user", password="password", email="test@user.cl", first_name="First", @@ -123,9 +124,7 @@ def test_salinfo_metadata(self, mock_requests, mock_environ): url = reverse("salinfo-metadata") with self.assertRaises(ValueError): - response = self.client.get(url) - fakehostname = "fakehost" - fakeport = "fakeport" + self.client.get(url) expected_url = f"http://fakehost:fakeport/salinfo/metadata" self.assertEqual(mock_requests.call_args, call(expected_url)) @@ -142,9 +141,7 @@ def test_salinfo_topic_names(self, mock_requests, mock_environ): url = reverse("salinfo-topic-names") with self.assertRaises(ValueError): - response = self.client.get(url) - fakehostname = "fakehost" - fakeport = "fakeport" + self.client.get(url) expected_url = f"http://fakehost:fakeport/salinfo/topic-names" self.assertEqual(mock_requests.call_args, call(expected_url)) @@ -161,9 +158,7 @@ def test_salinfo_topic_names_with_param(self, mock_requests, mock_environ): url = reverse("salinfo-topic-names") + "?categories=telemetry" with self.assertRaises(ValueError): - response = self.client.get(url) - fakehostname = "fakehost" - fakeport = "fakeport" + self.client.get(url) expected_url = ( f"http://fakehost:fakeport/salinfo/topic-names?categories=telemetry" ) @@ -182,9 +177,7 @@ def test_salinfo_topic_data(self, mock_requests, mock_environ): url = reverse("salinfo-topic-data") with self.assertRaises(ValueError): - response = self.client.get(url) - fakehostname = "fakehost" - fakeport = "fakeport" + self.client.get(url) expected_url = f"http://fakehost:fakeport/salinfo/topic-data" self.assertEqual(mock_requests.call_args, call(expected_url)) @@ -201,11 +194,67 @@ def test_salinfo_topic_data_with_param(self, mock_requests, mock_environ): url = reverse("salinfo-topic-data") + "?categories=telemetry" with self.assertRaises(ValueError): - response = self.client.get(url) - fakehostname = "fakehost" - fakeport = "fakeport" + self.client.get(url) expected_url = ( f"http://fakehost:fakeport/salinfo/topic-data?categories=telemetry" ) self.assertEqual(mock_requests.call_args, call(expected_url)) +@override_settings(DEBUG=True) +class EFDTestCase(TestCase): + maxDiff = None + + def setUp(self): + """Define the test suite setup.""" + # Arrange + self.client = APIClient() + self.user = User.objects.create_user( + username="user", + password="password", + email="test@user.cl", + first_name="First", + last_name="Last", + ) + self.token = Token.objects.create(user=self.user) + self.client.credentials(HTTP_AUTHORIZATION="Token " + self.token.key) + self.user.user_permissions.add( + Permission.objects.get(codename="view_view"), + Permission.objects.get(codename="add_view"), + Permission.objects.get(codename="delete_view"), + Permission.objects.get(codename="change_view"), + ) + + @patch( + "os.environ.get", + side_effect=lambda arg: "fakehost" + if arg == "COMMANDER_HOSTNAME" + else "fakeport", + ) + @patch("requests.post") + def test_timeseries_query(self, mock_requests, mock_environ): + """Test authorized user can query and get a timeseries""" + # Act: + cscs = { + "ATDome": { + "0": { + "topic1": ["field1"] + }, + }, + "ATMCS": { + "1": { + "topic2": ["field2", "field3"] + }, + } + } + data = { + "start_date": "2020-03-16T12:00:00", + "time_window": 15, + "cscs": cscs, + "resample": "1min", + } + url = reverse("EFD-timeseries") + + with self.assertRaises(ValueError): + self.client.post(url, data, format="json") + expected_url = f"http://fakehost:fakeport/efd/timeseries" + self.assertEqual(mock_requests.call_args, call(expected_url, json=data)) diff --git a/manager/api/tests/test_lovecsc.py b/manager/api/tests/test_lovecsc.py new file mode 100644 index 00000000..c7a18ec0 --- /dev/null +++ b/manager/api/tests/test_lovecsc.py @@ -0,0 +1,81 @@ +from django.test import TestCase, override_settings +from django.urls import reverse +from api.models import Token +from rest_framework.test import APIClient +from django.contrib.auth.models import User, Permission +import yaml +from unittest.mock import patch, call + + +@override_settings(DEBUG=True) +class LOVECscTestCase(TestCase): + maxDiff = None + + def setUp(self): + """Define the test suite setup.""" + # Arrange + self.client = APIClient() + self.user = User.objects.create_user( + username="user", + password="password", + email="test@user.cl", + first_name="First", + last_name="Last", + ) + self.token = Token.objects.create(user=self.user) + self.client.credentials(HTTP_AUTHORIZATION="Token " + self.token.key) + self.user.user_permissions.add( + Permission.objects.get(codename="view_view"), + Permission.objects.get(codename="add_view"), + Permission.objects.get(codename="delete_view"), + Permission.objects.get(codename="change_view"), + ) + + @patch( + "os.environ.get", + side_effect=lambda arg: "fakehost" + if arg == "COMMANDER_HOSTNAME" + else "fakeport", + ) + @patch("requests.post") + def test_authorized_lovecsc_data(self, mock_requests, mock_environ): + """Test authorized user observing log is sent to love-commander""" + # Arrange: + self.user.user_permissions.add(Permission.objects.get(name="Execute Commands")) + + # Act: + url = reverse("lovecsc-observinglog") + data = { + "user": "user", + "message": "a message", + } + + with self.assertRaises(ValueError): + self.client.post(url, data, format="json") + + expected_url = f"http://fakehost:fakeport/lovecsc/observinglog" + self.assertEqual(mock_requests.call_args, call(expected_url, json=data)) + + @patch( + "os.environ.get", + side_effect=lambda arg: "fakehost" + if arg == "COMMANDER_HOSTNAME" + else "fakeport", + ) + @patch("requests.post") + def test_unauthorized_lovecsc(self, mock_requests, mock_environ): + """Test an unauthorized user can't send commands""" + # Act: + url = reverse("lovecsc-observinglog") + data = { + "user": "user", + "message": "a message", + } + + response = self.client.post(url, data, format="json") + result = response.json() + + self.assertEqual(response.status_code, 401) + self.assertEqual( + result, {"ack": "User does not have permissions to send observing logs."} + ) diff --git a/manager/api/tests/test_schema_validation.py b/manager/api/tests/test_schema_validation.py index 72349ab4..ab9f0b51 100644 --- a/manager/api/tests/test_schema_validation.py +++ b/manager/api/tests/test_schema_validation.py @@ -5,6 +5,7 @@ from django.contrib.auth.models import User, Permission import yaml + @override_settings(DEBUG=True) class SchemaValidationTestCase(TestCase): script_schema = """ @@ -40,127 +41,132 @@ def setUp(self): # Arrange self.client = APIClient() self.user = User.objects.create_user( - username='an user', - password='password', - email='test@user.cl', - first_name='First', - last_name='Last', + username="user", + password="password", + email="test@user.cl", + first_name="First", + last_name="Last", ) self.token = Token.objects.create(user=self.user) - self.client.credentials(HTTP_AUTHORIZATION='Token ' + self.token.key) - self.user.user_permissions.add(Permission.objects.get(codename='view_view'), - Permission.objects.get(codename='add_view'), - Permission.objects.get(codename='delete_view'), - Permission.objects.get(codename='change_view')) + self.client.credentials(HTTP_AUTHORIZATION="Token " + self.token.key) + self.user.user_permissions.add( + Permission.objects.get(codename="view_view"), + Permission.objects.get(codename="add_view"), + Permission.objects.get(codename="delete_view"), + Permission.objects.get(codename="change_view"), + ) def test_valid_config(self): """Test schema validation works for a valid config yaml string""" # Act: - url = reverse('validate-config-schema') - data = { - 'config': "wait_time: 3600", - 'schema': self.script_schema - } - response = self.client.post(url, data, format='json') + url = reverse("validate-config-schema") + data = {"config": "wait_time: 3600", "schema": self.script_schema} + response = self.client.post(url, data, format="json") # Assert: expected_data = { "title": "None", - "output": {'wait_time': 3600, 'fail_cleanup': False, 'fail_run': False} + "output": {"wait_time": 3600, "fail_cleanup": False, "fail_run": False}, } - self.assertEqual( - response.data, - expected_data - ) + self.assertEqual(response.data, expected_data) def test_syntax_error(self): """Test validation output of an unparsable config file""" configs = [ - "wait_time: -\na:""", # ScannerError + "wait_time: -\na:", # ScannerError "fail_cleanup: \nw:'", # ScannerError - ":" # ParserError + ":", # ParserError ] expected_data = [ - {'error': {'context': None, - 'note': None, - 'problem': 'sequence entries are not allowed here', - 'problem_mark': {'buffer': 'wait_time: -\na:\x00', - 'column': 11, - 'index': 11, - 'line': 0, - 'name': '', - 'pointer': 11}}, - 'title': 'ERROR WHILE PARSING YAML STRING'}, - {'error': {'context': 'while scanning a simple key', - 'note': None, - 'problem': "could not find expected ':'", - 'problem_mark': {'buffer': "fail_cleanup: \nw:'\x00", - 'column': 3, - 'index': 18, - 'line': 1, - 'name': '', - 'pointer': 18}}, - 'title': 'ERROR WHILE PARSING YAML STRING'}, - {'error': {'context': 'while parsing a block mapping', - 'note': None, - 'problem': "expected , but found ':'", - 'problem_mark': {'buffer': ':\x00', - 'column': 0, - 'index': 0, - 'line': 0, - 'name': '', - 'pointer': 0}}, - 'title': 'ERROR WHILE PARSING YAML STRING'} + { + "error": { + "context": None, + "note": None, + "problem": "sequence entries are not allowed here", + "problem_mark": { + "buffer": "wait_time: -\na:\x00", + "column": 11, + "index": 11, + "line": 0, + "name": "", + "pointer": 11, + }, + }, + "title": "ERROR WHILE PARSING YAML STRING", + }, + { + "error": { + "context": "while scanning a simple key", + "note": None, + "problem": "could not find expected ':'", + "problem_mark": { + "buffer": "fail_cleanup: \nw:'\x00", + "column": 3, + "index": 18, + "line": 1, + "name": "", + "pointer": 18, + }, + }, + "title": "ERROR WHILE PARSING YAML STRING", + }, + { + "error": { + "context": "while parsing a block mapping", + "note": None, + "problem": "expected , but found ':'", + "problem_mark": { + "buffer": ":\x00", + "column": 0, + "index": 0, + "line": 0, + "name": "", + "pointer": 0, + }, + }, + "title": "ERROR WHILE PARSING YAML STRING", + }, ] for config, expected_datum in zip(configs, expected_data): # Act: - url = reverse('validate-config-schema') - request_data = { - 'config': config, - 'schema': self.script_schema - } - response = self.client.post(url, request_data, format='json') + url = reverse("validate-config-schema") + request_data = {"config": config, "schema": self.script_schema} + response = self.client.post(url, request_data, format="json") # Assert: - self.assertEqual( - response.data, - expected_datum - ) + self.assertEqual(response.data, expected_datum) def test_invalid_config(self): """Test validation output of an invalid config file""" - configs = [ - "wait_time: 'asd'", - "asdfasfd" - ] + configs = ["wait_time: 'asd'", "asdfasfd"] - expected_data = [{ - 'error': { - 'message': "'asd' is not of type 'number'", - 'path': ['wait_time'], - 'schema_path': ['properties', 'wait_time', 'type'], + expected_data = [ + { + "error": { + "message": "'asd' is not of type 'number'", + "path": ["wait_time"], + "schema_path": ["properties", "wait_time", "type"], + }, + "title": "INVALID CONFIG YAML", + }, + { + "error": { + "message": "asdfasfd is not a dict", + "path": [], + "schema_path": [], + }, + "title": "INVALID CONFIG YAML", }, - 'title': 'INVALID CONFIG YAML' - }, - {'error': {'message': 'asdfasfd is not a dict', 'path': [], 'schema_path': []}, - 'title': 'INVALID CONFIG YAML'} - ] for config, expected_datum in zip(configs, expected_data): # Act: - url = reverse('validate-config-schema') - request_data = { - 'config': config, - 'schema': self.script_schema - } - response = self.client.post(url, request_data, format='json') + url = reverse("validate-config-schema") + request_data = {"config": config, "schema": self.script_schema} + response = self.client.post(url, request_data, format="json") # Assert: - self.assertEqual( - response.data, - expected_datum - ) + self.assertEqual(response.data, expected_datum) diff --git a/manager/api/tests/tests_auth_api.py b/manager/api/tests/tests_auth_api.py index 759c8401..efc3195f 100644 --- a/manager/api/tests/tests_auth_api.py +++ b/manager/api/tests/tests_auth_api.py @@ -1,19 +1,27 @@ """Test users' authentication through the API.""" import datetime +import io +import json from django.test import TestCase from django.urls import reverse from django.contrib.auth.models import User, Permission from freezegun import freeze_time from rest_framework.test import APIClient from rest_framework import status -from api.models import Token +from api.models import ConfigFile, Token from django.conf import settings +from django.core.files.base import ContentFile from manager import utils class AuthApiTestCase(TestCase): """Test suite for users' authentication.""" + @staticmethod + def get_config_file_sample(name, content): + f = ContentFile(json.dumps(content).encode("ascii"), name=name) + return f + def setUp(self): """Define the test suite setup.""" # Arrange: @@ -50,6 +58,16 @@ def setUp(self): } self.expected_config = {"setting1": {"setting11": 1, "setting12": 2}} + self.filename = "test.json" + self.content = {"key1": "this is the content of the file"} + self.configfile = ConfigFile.objects.create( + user=self.user, + config_file=AuthApiTestCase.get_config_file_sample( + "random_filename", self.content + ), + file_name=self.filename, + ) + def test_user_login(self): """Test that a user can request a token using name and password.""" # Arrange: @@ -87,8 +105,8 @@ def test_user_login(self): "Time data is not as expected", ) self.assertEqual( - response.data["config"], - self.expected_config, + response.data["config"]["filename"], + self.filename, "The config was not requested", ) @@ -153,7 +171,7 @@ def test_user_validate_token(self): """Test that a user can validate a token.""" # Arrange: data = {"username": self.username, "password": self.password} - response = self.client.post(self.login_url, data, format="json") + self.client.post(self.login_url, data, format="json") token = Token.objects.filter(user__username=self.username).first() self.client.credentials(HTTP_AUTHORIZATION="Token " + token.key) @@ -172,7 +190,10 @@ def test_user_validate_token(self): ) self.assertEqual( response.data["user"], - {"username": self.user.username, "email": self.user.email,}, + { + "username": self.user.username, + "email": self.user.email, + }, "The user is not as expected", ) self.assertTrue( @@ -180,8 +201,8 @@ def test_user_validate_token(self): "Time data is not as expected", ) self.assertEqual( - response.data["config"], - self.expected_config, + response.data["config"]["filename"], + self.filename, "The config was not requested", ) @@ -189,7 +210,7 @@ def test_user_validate_token_no_config(self): """Test that a user can validate a token and not receive the config passing the no_config query param.""" # Arrange: data = {"username": self.username, "password": self.password} - response = self.client.post(self.login_url, data, format="json") + self.client.post(self.login_url, data, format="json") token = Token.objects.filter(user__username=self.username).first() self.client.credentials(HTTP_AUTHORIZATION="Token " + token.key) @@ -211,7 +232,10 @@ def test_user_validate_token_no_config(self): ) self.assertEqual( response.data["user"], - {"username": self.user.username, "email": self.user.email,}, + { + "username": self.user.username, + "email": self.user.email, + }, "The user is not as expected", ) self.assertTrue( @@ -224,7 +248,7 @@ def test_user_validate_token_fail(self): """Test that a user fails to validate an invalid token.""" # Arrange: data = {"username": self.username, "password": self.password} - response = self.client.post(self.login_url, data, format="json") + self.client.post(self.login_url, data, format="json") token = Token.objects.filter(user__username=self.username).first() self.client.credentials(HTTP_AUTHORIZATION="Token " + token.key + "fake") @@ -238,7 +262,7 @@ def test_user_fails_to_validate_deleted_token(self): """Test that a user fails to validate an deleted token.""" # Arrange: data = {"username": self.username, "password": self.password} - response = self.client.post(self.login_url, data, format="json") + self.client.post(self.login_url, data, format="json") token = Token.objects.filter(user__username=self.username).first() self.client.credentials(HTTP_AUTHORIZATION="Token " + token.key) token.delete() @@ -255,7 +279,7 @@ def test_user_fails_to_validate_expired_token(self): initial_time = datetime.datetime.now() with freeze_time(initial_time) as frozen_datetime: data = {"username": self.username, "password": self.password} - response = self.client.post(self.login_url, data, format="json") + self.client.post(self.login_url, data, format="json") token = Token.objects.filter(user__username=self.username).first() token_num_0 = Token.objects.filter(user__username=self.username).count() self.client.credentials(HTTP_AUTHORIZATION="Token " + token.key) @@ -276,7 +300,7 @@ def test_user_logout(self): """Test that a user can logout and delete the token.""" # Arrange: data = {"username": self.username, "password": self.password} - response = self.client.post(self.login_url, data, format="json") + self.client.post(self.login_url, data, format="json") token = Token.objects.filter(user__username=self.username).first() old_tokens_count = Token.objects.filter(user__username=self.username).count() self.client.credentials(HTTP_AUTHORIZATION="Token " + token.key) @@ -311,7 +335,7 @@ def test_user_swap(self): data = {"username": self.username2, "password": self.password} token = Token.objects.filter(user__username=self.username).first() self.client.credentials(HTTP_AUTHORIZATION="Token " + token.key) - response = self.client.post(self.swap_url, data, format="json") + self.client.post(self.swap_url, data, format="json") user_1_tokens_num_1 = Token.objects.filter(user__username=self.username).count() user_2_tokens_num_1 = Token.objects.filter( user__username=self.username2 @@ -332,7 +356,9 @@ def test_user_swap(self): # Assert 2: self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual( - response.data["config"], self.expected_config, + response.data["config"]["filename"], + self.filename, + "The config was not requested", ) def test_user_swap_no_config(self): @@ -376,7 +402,7 @@ def test_user_swap_no_config(self): def test_user_swap_forbidden(self): """Test that a user that's not logged in cannot swap users""" # Arrange logout: - response = self.client.delete(self.logout_url, format="json") + self.client.delete(self.logout_url, format="json") self.client.logout() data = {"username": self.username, "password": self.password} # Act: diff --git a/manager/api/tests/tests_config.py b/manager/api/tests/tests_config.py deleted file mode 100644 index fbba6826..00000000 --- a/manager/api/tests/tests_config.py +++ /dev/null @@ -1,51 +0,0 @@ -"""Test users' authentication through the API.""" -import datetime -from django.test import TestCase -from django.urls import reverse -from django.contrib.auth.models import User, Permission -from freezegun import freeze_time -from rest_framework.test import APIClient -from rest_framework import status -from api.models import Token -from django.conf import settings -from manager import utils - - -class ConfigApiTestCase(TestCase): - """Test suite for config files handling.""" - - def setUp(self): - """Define the test suite setup.""" - # Arrange: - self.client = APIClient() - self.user = User.objects.create_user( - username="an user", - password="password", - email="test@user.cl", - first_name="First", - last_name="Last", - ) - self.url = reverse("config") - self.expected_data = {"setting1": {"setting11": 1, "setting12": 2}} - self.token = Token.objects.create(user=self.user) - # self.client.credentials(HTTP_AUTHORIZATION="Token " + self.token.key) - - def test_get_config(self): - """Test that an authenticated user can get the config file.""" - # Arrange - self.client.credentials(HTTP_AUTHORIZATION="Token " + self.token.key) - # Act: - response = self.client.get(self.url, format="json") - - # Assert: - self.assertEqual(response.status_code, 200) - self.assertEqual(response.data, self.expected_data) - - def test_unauthenticated_cannot_get_config(self): - """Test that an unauthenticated user cannot get the config file.""" - # Act: - response = self.client.get(self.url, format="json") - - # Assert: - self.assertEqual(response.status_code, 401) - self.assertNotEqual(response.data, self.expected_data) diff --git a/manager/api/tests/tests_configfile.py b/manager/api/tests/tests_configfile.py new file mode 100644 index 00000000..c037e7db --- /dev/null +++ b/manager/api/tests/tests_configfile.py @@ -0,0 +1,108 @@ +"""Test users' authentication through the API.""" +import datetime +import io +import json +from django.test import TestCase +from django.urls import reverse +from django.contrib.auth.models import User, Permission +from freezegun import freeze_time +from rest_framework.test import APIClient +from rest_framework import status +from api.models import ConfigFile, Token +from django.conf import settings +from manager import utils +from django.core.files.base import ContentFile +from django.conf import settings +import tempfile + +# python manage.py test api.tests.tests_configfile.ConfigFileApiTestCase + + +def setUp(self): + settings.MEDIA_ROOT = tempfile.mkdtemp() + + +class ConfigFileApiTestCase(TestCase): + """Test suite for config files handling.""" + + @staticmethod + def get_config_file_sample(name, content): + f = ContentFile(json.dumps(content).encode("ascii"), name=name) + return f + + def setUp(self): + """Define the test suite setup.""" + # Arrange: + + self.client = APIClient() + self.user = User.objects.create_user( + username="user", + password="password", + email="test@user.cl", + first_name="First", + last_name="Last", + ) + self.filename = "test.json" + self.content = {"key1": "this is the content of the file"} + self.configfile = ConfigFile.objects.create( + user=self.user, + config_file=ConfigFileApiTestCase.get_config_file_sample( + "random_filename", self.content + ), + file_name=self.filename, + ) + self.url = reverse("config") + self.token = Token.objects.create(user=self.user) + + def test_get_config_files_list(self): + """Test that an authenticated user can get a config file.""" + self.client.credentials(HTTP_AUTHORIZATION="Token " + self.token.key) + response = self.client.get(reverse("configfile-list"), format="json") + self.assertEqual(response.status_code, 200) + expected_data = { + "id": self.configfile.id, + "username": self.user.username, + "filename": self.filename, + } + self.assertEqual(len(response.data), 1) + self.assertEqual(response.data[0]["filename"], expected_data["filename"]) + + def test_get_config_file(self): + """Test that an authenticated user can get a config file.""" + self.client.credentials(HTTP_AUTHORIZATION="Token " + self.token.key) + response = self.client.get( + reverse("configfile-detail", args=[self.configfile.id]), format="json" + ) + self.assertEqual(response.status_code, 200) + expected_data = { + "id": self.configfile.id, + "username": self.user.username, + "filename": self.filename, + } + self.assertEqual(response.data["id"], expected_data["id"]) + self.assertEqual(response.data["username"], expected_data["username"]) + self.assertEqual(response.data["filename"], expected_data["filename"]) + + def test_get_config_file_content(self): + """Test that an authenticated user can get a config file content.""" + self.client.credentials(HTTP_AUTHORIZATION="Token " + self.token.key) + response = self.client.get( + reverse("configfile-content", args=[self.configfile.id]), format="json" + ) + self.assertEqual(response.status_code, 200) + expected_data = { + "id": self.configfile.id, + "content": self.content, + "filename": self.filename, + } + self.assertEqual(response.data["id"], expected_data["id"]) + self.assertEqual(response.data["content"], expected_data["content"]) + self.assertEqual(response.data["filename"], expected_data["filename"]) + + def test_unauthenticated_cannot_get_config_file(self): + """Test that an unauthenticated user cannot get the config file.""" + # Act: + response = self.client.get(self.url, format="json") + + # Assert: + self.assertEqual(response.status_code, 401) diff --git a/manager/api/tests/tests_emergencycontact.py b/manager/api/tests/tests_emergencycontact.py new file mode 100644 index 00000000..a67f99ca --- /dev/null +++ b/manager/api/tests/tests_emergencycontact.py @@ -0,0 +1,66 @@ +"""Test users' authentication through the API.""" +import datetime +import io +import json +from django.test import TestCase +from django.urls import reverse +from django.contrib.auth.models import User, Permission +from freezegun import freeze_time +from rest_framework.test import APIClient +from rest_framework import status +from api.models import EmergencyContact, Token +from django.conf import settings +from manager import utils +from django.core.files.base import ContentFile + +# python manage.py test api.tests.tests_emergencycontact.EmergencyContactApiTestCase + + +class EmergencyContactApiTestCase(TestCase): + """Test suite for config files handling.""" + + @staticmethod + def get_config_file_sample(name, content): + f = ContentFile(json.dumps(content).encode("ascii"), name=name) + return f + + def setUp(self): + """Define the test suite setup.""" + # Arrange: + + self.client = APIClient() + self.user = User.objects.create_user( + username="user", + password="password", + email="test@user.cl", + first_name="First", + last_name="Last", + ) + self.token = Token.objects.create(user=self.user) + self.ec1 = EmergencyContact.objects.create( + subsystem="ATDome", + name="Name Lastname", + contact_info="+568984861", + email="name@email.com", + ) + self.ec2 = EmergencyContact.objects.create( + subsystem="ATMCS", + name="Name2 Lastname2", + contact_info="no info", + email="name2@email.com", + ) + + def test_list_emergency_contacts(self): + """Test that an authenticated user can get a config file.""" + self.client.credentials(HTTP_AUTHORIZATION="Token " + self.token.key) + response = self.client.get(reverse("emergencycontact-list"), format="json") + self.assertEqual(response.status_code, 200) + self.assertEqual(len(response.data), 2) + self.assertEqual(response.data[0]["subsystem"], self.ec1.subsystem) + self.assertEqual(response.data[0]["name"], self.ec1.name) + self.assertEqual(response.data[0]["contact_info"], self.ec1.contact_info) + self.assertEqual(response.data[0]["email"], self.ec1.email) + self.assertEqual(response.data[1]["subsystem"], self.ec2.subsystem) + self.assertEqual(response.data[1]["name"], self.ec2.name) + self.assertEqual(response.data[1]["contact_info"], self.ec2.contact_info) + self.assertEqual(response.data[1]["email"], self.ec2.email) diff --git a/manager/api/urls.py b/manager/api/urls.py index 73b74d91..9a6de855 100644 --- a/manager/api/urls.py +++ b/manager/api/urls.py @@ -18,8 +18,8 @@ from django.conf.urls import include from django.urls import path from rest_framework.routers import DefaultRouter +from api.views import ConfigFileViewSet, EmergencyContactViewSet -# from api.views import validate_token, logout, CustomObtainAuthToken, validate_config_schema, commander, salinfo_metadata import api.views router = DefaultRouter() @@ -40,11 +40,19 @@ ), path("auth/", include("rest_framework.urls", namespace="rest_framework")), path("cmd/", api.views.commander, name="commander"), + path( + "lovecsc/observinglog", + api.views.lovecsc_observinglog, + name="lovecsc-observinglog", + ), path("salinfo/metadata", api.views.salinfo_metadata, name="salinfo-metadata"), path( "salinfo/topic-names", api.views.salinfo_topic_names, name="salinfo-topic-names" ), path("salinfo/topic-data", api.views.salinfo_topic_data, name="salinfo-topic-data"), path("config", api.views.get_config, name="config"), + path("efd/timeseries", api.views.query_efd, name="EFD-timeseries"), ] +router.register("configfile", ConfigFileViewSet) +router.register("emergencycontact", EmergencyContactViewSet) urlpatterns.append(path("", include(router.urls))) diff --git a/manager/api/views.py b/manager/api/views.py index c3e707d0..8ffa522f 100644 --- a/manager/api/views.py +++ b/manager/api/views.py @@ -10,12 +10,21 @@ from rest_framework import status from rest_framework.authtoken.views import ObtainAuthToken from rest_framework.decorators import api_view -from rest_framework.decorators import permission_classes -from rest_framework.permissions import IsAuthenticated +from rest_framework.decorators import permission_classes, authentication_classes +from rest_framework.decorators import action +from rest_framework.permissions import IsAuthenticated, AllowAny +from rest_framework.authentication import SessionAuthentication, BasicAuthentication from rest_framework.response import Response +from rest_framework import viewsets, status from api.models import Token -from api.serializers import TokenSerializer, read_config_file, ConfigSerializer +from api.serializers import TokenSerializer, ConfigSerializer +from api.serializers import ( + ConfigFileSerializer, + ConfigFileContentSerializer, + EmergencyContactSerializer, +) from .schema_validator import DefaultingValidator +from api.models import ConfigFile, EmergencyContact valid_response = openapi.Response("Valid token", TokenSerializer) invalid_response = openapi.Response("Invalid token") @@ -233,7 +242,7 @@ def commander(request): ------ request: Request The Request object - + Returns ------- Response @@ -249,6 +258,40 @@ def commander(request): return Response(response.json(), status=response.status_code) +@swagger_auto_schema( + method="post", + responses={ + 200: openapi.Response("Observing log sent"), + 400: openapi.Response("Missing parameters"), + 401: openapi.Response("Unauthenticated"), + 403: openapi.Response("Unauthorized"), + }, +) +@api_view(["POST"]) +@permission_classes((IsAuthenticated,)) +def lovecsc_observinglog(request): + """Sends an observing log message to the LOVE-commander according to the received parameters + + Params + ------ + request: Request + The Request object + + Returns + ------- + Response + The response and status code of the request to the LOVE-Commander + """ + if not request.user.has_perm("api.command.execute_command"): + return Response( + {"ack": "User does not have permissions to send observing logs."}, 401 + ) + url = f"http://{os.environ.get('COMMANDER_HOSTNAME')}:{os.environ.get('COMMANDER_PORT')}/lovecsc/observinglog" + response = requests.post(url, json=request.data) + + return Response(response.json(), status=response.status_code) + + @swagger_auto_schema( method="get", responses={ @@ -268,12 +311,12 @@ def commander(request): def salinfo_metadata(request): """Requests SalInfo.metadata from the commander containing a dict of : { "sal_version": ..., "xml_version": ....} - + Params ------ request: Request The Request object - + Returns ------- Response @@ -310,12 +353,12 @@ def salinfo_metadata(request): def salinfo_topic_names(request): """Requests SalInfo.topic_names from the commander containing a dict of : { "command_names": [], "event_names": [], "telemetry_names": []} - + Params ------ request: Request The Request object - + Returns ------- Response @@ -350,17 +393,18 @@ def salinfo_topic_names(request): 403: openapi.Response("Unauthorized"), }, ) + @api_view(["GET"]) @permission_classes((IsAuthenticated,)) def salinfo_topic_data(request): """Requests SalInfo.topic_data from the commander containing a dict of : { "command_data": [], "event_data": [], "telemetry_data": []} - + Params ------ request: Request The Request object - + Returns ------- Response @@ -392,13 +436,95 @@ def get_config(request): ------ request: Request The Request object - + Returns ------- Response Containing the contents of the config file """ - data = read_config_file() - if data is None: - return Response(None, status=status.HTTP_404_NOT_FOUND) - return Response(data, status=status.HTTP_200_OK) + try: + cf = ConfigFile.objects.first() + except ConfigFile.DoesNotExist: + return Response(status=status.HTTP_404_NOT_FOUND) + + serializer = ConfigFileContentSerializer(cf) + return Response(serializer.data) + + +class ConfigFileViewSet(viewsets.ModelViewSet): + """GET, POST, PUT, PATCH or DELETE instances the ConfigFile model.""" + + queryset = ConfigFile.objects.order_by("-update_timestamp").all() + """Set of objects to be accessed by queries to this viewsets endpoints""" + + serializer_class = ConfigFileSerializer + """Serializer used to serialize View objects""" + + @action(detail=True) + def content(self, request, pk=None): + """Serialize a ConfigFile's content. + + Params + ------ + request: Request + The Requets object + pk: int + The corresponding ConfigFile pk + + Returns + ------- + Response + The response containing the serialized ConfigFile content + """ + try: + cf = ConfigFile.objects.get(pk=pk) + except ConfigFile.DoesNotExist: + return Response(status=status.HTTP_404_NOT_FOUND) + + serializer = ConfigFileContentSerializer(cf) + return Response(serializer.data) + + +class EmergencyContactViewSet(viewsets.ModelViewSet): + """GET, POST, PUT, PATCH or DELETE instances the EmergencyContact model.""" + + queryset = EmergencyContact.objects.order_by("subsystem").all() + """Set of objects to be accessed by queries to this viewsets endpoints""" + + serializer_class = EmergencyContactSerializer + """Serializer used to serialize View objects""" + +@api_view(["POST"]) +@permission_classes((IsAuthenticated,)) +def query_efd(request, *args, **kwargs): + """Queries data from an EFD timeseries by redirecting the request to the Commander + + Params + ------ + request: Request + The Request object + args: list + List of addittional arguments. Currently unused + kwargs: dict + Dictionary with request arguments. Request should contain the following: + start_date (required): String specifying the start of the query range. Default current date minus 10 minutes + timewindow (required): Int specifying the number of minutes to query starting from start_date. Default 10 + topics (required): Dictionary of the form + { + CSC1: { + index: [topic1, topic2...], + }, + CSC2: { + index: [topic1, topic2...], + }, + } + resample (optional): The offset string representing target resample conversion, e.g. '15min', '10S' + + Returns + ------- + Response + The response and status code of the request to the LOVE-Commander + """ + url = f"http://{os.environ.get('COMMANDER_HOSTNAME')}:{os.environ.get('COMMANDER_PORT')}/efd/timeseries" + response = requests.post(url, json=request.data) + return Response(response.json(), status=response.status_code) diff --git a/manager/manage.py b/manager/manage.py index 05b2a2e2..6a5d00ea 100755 --- a/manager/manage.py +++ b/manager/manage.py @@ -4,8 +4,8 @@ import os import sys -if __name__ == '__main__': - os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'manager.settings') +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "manager.settings") try: from django.core.management import execute_from_command_line except ImportError as exc: diff --git a/manager/manager/routing.py b/manager/manager/routing.py index 165f580b..a8d3e639 100644 --- a/manager/manager/routing.py +++ b/manager/manager/routing.py @@ -2,8 +2,6 @@ from channels.routing import ProtocolTypeRouter, URLRouter import subscription.routing -application = ProtocolTypeRouter({ - 'websocket': URLRouter( - subscription.routing.websocket_urlpatterns - ) -}) +application = ProtocolTypeRouter( + {"websocket": URLRouter(subscription.routing.websocket_urlpatterns)} +) diff --git a/manager/manager/settings.py b/manager/manager/settings.py index 4bdfbbb6..59aa35de 100644 --- a/manager/manager/settings.py +++ b/manager/manager/settings.py @@ -22,7 +22,8 @@ # Define wether the system is being tested or not: TESTING = os.environ.get("TESTING", False) -"""Define wether or not this instance is being created for testing or not, get from the `TESTING` environment variable (`string`)""" +"""Define wether or not this instance is being created for testing or not, +get from the `TESTING` environment variable (`string`)""" # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = os.getenv( @@ -106,7 +107,7 @@ TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", - "DIRS": [os.path.join(BASE_DIR, "templates"),], + "DIRS": [os.path.join(BASE_DIR, "templates")], "APP_DIRS": True, "OPTIONS": { "context_processors": [ @@ -133,14 +134,15 @@ { "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", }, - {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",}, - {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",}, - {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",}, + {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"}, + {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"}, + {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"}, ] # Password for other processes PROCESS_CONNECTION_PASS = os.environ.get("PROCESS_CONNECTION_PASS", "dev_pass") -"""Password that Producers use to connect to eh Manager, read from the `PROCESS_CONNECTION_PASS` environment variable (`string`)""" +"""Password that Producers use to connect to eh Manager, +read from the `PROCESS_CONNECTION_PASS` environment variable (`string`)""" # Internationalization # https://docs.djangoproject.com/en/2.2/topics/i18n/ @@ -162,8 +164,6 @@ "rest_framework.permissions.DjangoModelPermissions", ), "DEFAULT_AUTHENTICATION_CLASSES": ( - # 'rest_framework.authentication.TokenAuthentication', - # 'api.authentication.TokenAuthentication', "api.authentication.ExpiringTokenAuthentication", "rest_framework.authentication.SessionAuthentication", ), @@ -197,11 +197,9 @@ if TESTING: MEDIA_BASE = os.path.join(BASE_DIR, "ui_framework", "tests") MEDIA_ROOT = os.path.join(BASE_DIR, "ui_framework", "tests", "media") - CONFIG_URL = os.path.join(BASE_DIR, "api", "tests", "config", "love.json") else: MEDIA_BASE = BASE_DIR MEDIA_ROOT = os.path.join(BASE_DIR, "media") - CONFIG_URL = os.path.join(BASE_DIR, "config", "love.json") # Channels ASGI_APPLICATION = "manager.routing.application" @@ -223,6 +221,8 @@ + "/0" ], "symmetric_encryption_keys": [SECRET_KEY], + "capacity": 1500, + "expiry": 10, }, }, } @@ -230,7 +230,7 @@ else: CHANNEL_LAYERS = { - "default": {"BACKEND": "channels.layers.InMemoryChannelLayer",}, + "default": {"BACKEND": "channels.layers.InMemoryChannelLayer"}, } # LDAP @@ -253,8 +253,8 @@ ) TRACE_TIMESTAMPS = True -"""Define wether or not to add tracing timestamps to websocket messages. Read from TRACE_TIMESTAMPS` environment variable (`bool`)""" +"""Define wether or not to add tracing timestamps to websocket messages. +Read from TRACE_TIMESTAMPS` environment variable (`bool`)""" if os.environ.get("HIDE_TRACE_TIMESTAMPS", False): TRACE_TIMESTAMPS = False - diff --git a/manager/manager/urls.py b/manager/manager/urls.py index d8ce598e..5dd143d8 100644 --- a/manager/manager/urls.py +++ b/manager/manager/urls.py @@ -27,7 +27,7 @@ schema_view = get_schema_view( openapi.Info( title="LOVE-manager API", - default_version='v1', + default_version="v1", description="This is the API of LOVE-manager's authentication and UI Framework modules", ), public=True, @@ -35,14 +35,27 @@ ) urlpatterns = [ - path('manager/admin/', admin.site.urls), - path('manager/test/', TemplateView.as_view(template_name="test.html")), - path('manager/login/', TemplateView.as_view(template_name="registration/login.html")), - path('manager/api/', include('api.urls')), - path('manager/ui_framework/', include('ui_framework.urls')), - re_path(r'^manager/apidoc/swagger(?P\.json|\.yaml)$', - schema_view.without_ui(cache_timeout=0), name='schema-json'), - path('manager/apidoc/swagger/', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'), - path('manager/apidoc/redoc/', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'), - path('manager/schema_validation/', TemplateView.as_view(template_name="test.html")), + path("manager/admin/", admin.site.urls), + path("manager/test/", TemplateView.as_view(template_name="test.html")), + path( + "manager/login/", TemplateView.as_view(template_name="registration/login.html") + ), + path("manager/api/", include("api.urls")), + path("manager/ui_framework/", include("ui_framework.urls")), + re_path( + r"^manager/apidoc/swagger(?P\.json|\.yaml)$", + schema_view.without_ui(cache_timeout=0), + name="schema-json", + ), + path( + "manager/apidoc/swagger/", + schema_view.with_ui("swagger", cache_timeout=0), + name="schema-swagger-ui", + ), + path( + "manager/apidoc/redoc/", + schema_view.with_ui("redoc", cache_timeout=0), + name="schema-redoc", + ), + path("manager/schema_validation/", TemplateView.as_view(template_name="test.html")), ] diff --git a/manager/manager/wsgi.py b/manager/manager/wsgi.py index 6d2fcf8a..a86267d3 100644 --- a/manager/manager/wsgi.py +++ b/manager/manager/wsgi.py @@ -11,6 +11,6 @@ from django.core.wsgi import get_wsgi_application -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'manager.settings') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "manager.settings") application = get_wsgi_application() diff --git a/manager/requirements.txt b/manager/requirements.txt index 4b7a7ab4..e13cf0fa 100644 --- a/manager/requirements.txt +++ b/manager/requirements.txt @@ -17,7 +17,7 @@ chardet==3.0.4 constantly==15.1.0 coreapi==2.3.3 coreschema==0.0.4 -cryptography==2.8 +cryptography==3.3.2 daphne==2.4.1 Django==3.0.7 django-auth-ldap==2.1.0 diff --git a/manager/runserver-dev.sh b/manager/runserver-dev.sh index 97e00340..6d4b92d2 100755 --- a/manager/runserver-dev.sh +++ b/manager/runserver-dev.sh @@ -15,7 +15,11 @@ python manage.py createusers --adminpass ${ADMIN_USER_PASS} --userpass ${USER_US echo -e "\nApplying fixtures" mkdir -p media/thumbnails cp -u ui_framework/fixtures/thumbnails/* media/thumbnails +mkdir -p media/configs +cp -u api/fixtures/configs/* media/configs + python manage.py loaddata ui_framework/fixtures/initial_data.json +python manage.py loaddata api/fixtures/initial_data.json echo -e "\nStarting server" python manage.py runserver 0.0.0.0:8000 diff --git a/manager/runserver.sh b/manager/runserver.sh index 78f97491..ceafc5f2 100755 --- a/manager/runserver.sh +++ b/manager/runserver.sh @@ -15,7 +15,11 @@ python manage.py createusers --adminpass ${ADMIN_USER_PASS} --userpass ${USER_US echo -e "\nApplying fixtures" mkdir -p media/thumbnails cp -u ui_framework/fixtures/thumbnails/* media/thumbnails +mkdir -p media/configs +cp -u api/fixtures/configs/* media/configs + python manage.py loaddata ui_framework/fixtures/initial_data.json +python manage.py loaddata api/fixtures/initial_data.json echo -e "\nStarting server" daphne -b 0.0.0.0 -p 8000 manager.asgi:application diff --git a/manager/static_files/css/love-ui.css b/manager/static_files/css/love-ui.css index d9dce921..269a95fc 100644 --- a/manager/static_files/css/love-ui.css +++ b/manager/static_files/css/love-ui.css @@ -268,10 +268,6 @@ p { border-radius: 6px; } -.panel-title { - -} - .panel-body { border-top-left-radius: 8px; border-top: 2px solid #81939e; diff --git a/manager/subscription/__init__.py b/manager/subscription/__init__.py index aa7522a3..c7fe0352 100644 --- a/manager/subscription/__init__.py +++ b/manager/subscription/__init__.py @@ -1 +1 @@ -default_app_config = 'subscription.apps.SubscriptionConfig' +default_app_config = "subscription.apps.SubscriptionConfig" diff --git a/manager/subscription/consumers.py b/manager/subscription/consumers.py index 368bef84..feb209a4 100644 --- a/manager/subscription/consumers.py +++ b/manager/subscription/consumers.py @@ -124,7 +124,7 @@ async def handle_action_message(self, message): Receives an action message and reacts according to each different action. - Currently supported actions: + Currently supported actions: - get_time_data: sends a message with the time_data and passes though a request_time received with the message. - Expected input message: @@ -166,7 +166,10 @@ async def handle_action_message(self, message): request_time = message["request_time"] time_data = utils.get_times() await self.send_json( - {"time_data": time_data, "request_time": request_time,} + { + "time_data": time_data, + "request_time": request_time, + } ) async def handle_heartbeat_message(self, message): @@ -226,7 +229,6 @@ async def handle_data_message(self, message, manager_rcv): """ data = message["data"] category = message["category"] - user = self.scope["user"] producer_snd = message["producer_snd"] if "producer_snd" in message else None # Store pairs of group, message to send: diff --git a/manager/subscription/heartbeat_manager.py b/manager/subscription/heartbeat_manager.py index 0c7515dd..59c9ded5 100644 --- a/manager/subscription/heartbeat_manager.py +++ b/manager/subscription/heartbeat_manager.py @@ -13,112 +13,127 @@ class HeartbeatManager: Runs 2 tasks in order to dispatch the heartbeats and request the LOVE-Commander's heartbeat periodically. """ - heartbeat_task = None - """Reference to the task that dispatches the heartbeats.""" - - commander_heartbeat_task = None - """Reference to the task that requests the LOVE_COmmander heartbeats.""" - - heartbeat_data = {} - """Dictionary comntaining the heartbeats data, indexed by source or component, e.g. "Commander".""" - - @classmethod - def initialize(self): - """Initialize the HeartbeatManager - - Run 2 async tasks in the event loop, one to dispatch the heartbeats periodically, - and the other to request the heartbeats from the LOVE-Commander periodically. - """ - self.heartbeat_data = {} - if not self.heartbeat_task: - self.heartbeat_task = asyncio.create_task(self.dispatch_heartbeats()) - if not self.commander_heartbeat_task: - self.heartbeat_task = asyncio.create_task(self.query_commander()) - - @classmethod - def set_heartbeat_timestamp(self, source, timestamp): - """Set a given timestamp as the heartbeat for a given source - - Parameters - ---------- - source: `string` - Name of the component to save the heartbeat, e.g. "Commander" - timestamp: `float` - timestamp of the heartbeat - """ - self.heartbeat_data[source] = timestamp - - @classmethod - async def query_commander(self): - """Query the heartbeat from the LOVE-Commander periodically. - - This is what the `commander_heartbeat_task` does - """ - heartbeat_url = f"http://{os.environ.get('COMMANDER_HOSTNAME')}:{os.environ.get('COMMANDER_PORT')}/heartbeat" - while True: - try: - # query commander - resp = requests.get(heartbeat_url) - timestamp = resp.json()["timestamp"] - # get timestamp - self.set_heartbeat_timestamp("Commander", timestamp) - await asyncio.sleep(3) - except Exception as e: - print(e) - await asyncio.sleep(3) - - @classmethod - async def dispatch_heartbeats(self): - """Dispatch all the heartbeats to the corresponding group in the Channels Layer. - - This is what the `heartbeat_task` does - """ - channel_layer = get_channel_layer() - while True: - try: - self.set_heartbeat_timestamp( - "Manager", datetime.datetime.now().timestamp() + class __HeartbeatManager: + + heartbeat_task = None + """Reference to the task that dispatches the heartbeats.""" + + commander_heartbeat_task = None + """Reference to the task that requests the LOVE_COmmander heartbeats.""" + + heartbeat_data = {} + """Dictionary comntaining the heartbeats data, indexed by source or component, e.g. "Commander".""" + + @classmethod + def initialize(cls): + """Initialize the HeartbeatManager + + Run 2 async tasks in the event loop, one to dispatch the heartbeats periodically, + and the other to request the heartbeats from the LOVE-Commander periodically. + """ + cls.heartbeat_data = {} + if not cls.heartbeat_task: + cls.heartbeat_task = asyncio.create_task(cls.dispatch_heartbeats()) + if not cls.commander_heartbeat_task: + cls.commander_heartbeat_task = asyncio.create_task( + cls.query_commander() ) - data = json.dumps( - { - "category": "heartbeat", - "data": [ - { - "csc": heartbeat_source, - "salindex": 0, - "data": { - "timestamp": self.heartbeat_data[heartbeat_source] - }, - } - for heartbeat_source in self.heartbeat_data - ], - "subscription": "heartbeat", - } - ) - await channel_layer.group_send( - "heartbeat-manager-0-stream", - {"type": "send_heartbeat", "data": data}, - ) - await asyncio.sleep(3) - except Exception as e: - print(e) - await asyncio.sleep(3) - - @classmethod - async def reset(self): - """Reset the `HeartbeatManager`, changing the tasks references and heartbeats dictionary back to their default values.""" - if self.heartbeat_task: - self.heartbeat_task = None - if self.commander_heartbeat_task: - self.commander_heartbeat_task = None - self.heartbeat_data = {} - self.commander_heartbeat_task = {} - - @classmethod - async def stop(self): - """Stop (cancel) the tasks.""" - if self.heartbeat_task: - self.heartbeat_task.cancel() - if self.commander_heartbeat_task: - self.commander_heartbeat_task.cancel() + @classmethod + def set_heartbeat_timestamp(cls, source, timestamp): + """Set a given timestamp as the heartbeat for a given source + + Parameters + ---------- + source: `string` + Name of the component to save the heartbeat, e.g. "Commander" + timestamp: `float` + timestamp of the heartbeat + """ + cls.heartbeat_data[source] = timestamp + + @classmethod + async def query_commander(cls): + """Query the heartbeat from the LOVE-Commander periodically. + + This is what the `commander_heartbeat_task` does + """ + heartbeat_url = f"http://{os.environ.get('COMMANDER_HOSTNAME')}:{os.environ.get('COMMANDER_PORT')}/heartbeat" + while True: + try: + # query commander + resp = requests.get(heartbeat_url) + timestamp = resp.json()['timestamp'] + #get timestamp + cls.set_heartbeat_timestamp('Commander', timestamp) + await asyncio.sleep(3) + except Exception as e: + print(e) + await asyncio.sleep(3) + + @classmethod + async def dispatch_heartbeats(cls): + """Dispatch all the heartbeats to the corresponding group in the Channels Layer. + + This is what the `heartbeat_task` does + """ + channel_layer = get_channel_layer() + while True: + try: + print("sending data") + cls.set_heartbeat_timestamp( + "Manager", datetime.datetime.now().timestamp() + ) + data = json.dumps( + { + "category": "heartbeat", + "data": [ + { + "csc": heartbeat_source, + "salindex": 0, + "data": { + "timestamp": cls.heartbeat_data[ + heartbeat_source + ] + }, + } + for heartbeat_source in cls.heartbeat_data + ], + "subscription": "heartbeat", + } + ) + await channel_layer.group_send( + "heartbeat-manager-0-stream", + {"type": "send_heartbeat", "data": data}, + ) + await asyncio.sleep(3) + except Exception as e: + print(e) + await asyncio.sleep(3) + + @classmethod + async def reset(cls): + """Reset the `HeartbeatManager`, changing the tasks references and heartbeats dictionary back to their default values.""" + if cls.heartbeat_task: + cls.heartbeat_task = None + if cls.commander_heartbeat_task: + cls.commander_heartbeat_task = None + cls.heartbeat_data = {} + cls.commander_heartbeat_task = {} + + @classmethod + async def stop(cls): + """Stop (cancel) the tasks.""" + if cls.heartbeat_task: + cls.heartbeat_task.cancel() + if cls.commander_heartbeat_task: + cls.commander_heartbeat_task.cancel() + + instance = None + + def __init__(self): + if not HeartbeatManager.instance: + HeartbeatManager.instance = HeartbeatManager.__HeartbeatManager() + + def __getattr__(self, name): + return getattr(self.instance, name) diff --git a/manager/subscription/tests/test_connection.py b/manager/subscription/tests/test_connection.py index e94e225f..92bcd68d 100644 --- a/manager/subscription/tests/test_connection.py +++ b/manager/subscription/tests/test_connection.py @@ -14,9 +14,13 @@ class TestClientConnection: """Test that clients can or cannot connect depending on different conditions.""" def setup_method(self): - self.user = User.objects.create_user('username', password='123', email='user@user.cl') + self.user = User.objects.create_user( + "username", password="123", email="user@user.cl" + ) self.token = Token.objects.create(user=self.user) - self.user2 = User.objects.create_user('username2', password='123', email='user@user.cl') + self.user2 = User.objects.create_user( + "username2", password="123", email="user@user.cl" + ) self.token2 = Token.objects.create(user=self.user2) @pytest.mark.asyncio @@ -24,12 +28,12 @@ def setup_method(self): async def test_connection_with_token(self): """Test that clients can connect with a valid token.""" # Arrange - url = 'manager/ws/subscription/?token={}'.format(self.token) + url = "manager/ws/subscription/?token={}".format(self.token) communicator = WebsocketCommunicator(application, url) # Act connected, subprotocol = await communicator.connect() # Assert - assert connected, 'Communicator was not connected' + assert connected, "Communicator was not connected" await communicator.disconnect() @pytest.mark.asyncio @@ -38,12 +42,12 @@ async def test_connection_with_password(self): """Test that clients can connect with a valid password.""" # Arrange password = PROCESS_CONNECTION_PASS - url = 'manager/ws/subscription/?password={}'.format(password) + url = "manager/ws/subscription/?password={}".format(password) communicator = WebsocketCommunicator(application, url) # Act connected, subprotocol = await communicator.connect() # Assert - assert connected, 'Communicator was not connected' + assert connected, "Communicator was not connected" await communicator.disconnect() @pytest.mark.asyncio @@ -51,12 +55,12 @@ async def test_connection_with_password(self): async def test_connection_failed_for_invalid_token(self): """Test that clients cannot connect with an invalid token.""" # Arrange - url = 'manager/ws/subscription/?token={}'.format(str(self.token) + 'fake') + url = "manager/ws/subscription/?token={}".format(str(self.token) + "fake") communicator = WebsocketCommunicator(application, url) # Act connected, subprotocol = await communicator.connect() # Assert - assert not connected, 'Communicator should not have connected' + assert not connected, "Communicator should not have connected" await communicator.disconnect() @pytest.mark.asyncio @@ -64,13 +68,13 @@ async def test_connection_failed_for_invalid_token(self): async def test_connection_failed_for_invalid_password(self): """Test that clients cannot connect with an invalid password.""" # Arrange - password = PROCESS_CONNECTION_PASS + '_fake' - url = 'manager/ws/subscription/?password={}'.format(password) + password = PROCESS_CONNECTION_PASS + "_fake" + url = "manager/ws/subscription/?password={}".format(password) communicator = WebsocketCommunicator(application, url) # Act connected, subprotocol = await communicator.connect() # Assert - assert not connected, 'Communicator should not have connected' + assert not connected, "Communicator should not have connected" await communicator.disconnect() @pytest.mark.asyncio @@ -84,41 +88,50 @@ async def test_connection_interrupted_when_logout_message_is_sent(self): "csc": "ScriptQueue", "salindex": 0, "stream": "stream1", - "category": "event" + "category": "event", } - expected_response = 'Successfully subscribed to event-ScriptQueue-0-stream1' + expected_response = "Successfully subscribed to event-ScriptQueue-0-stream1" channel_layer = get_channel_layer() # Connect 3 clients (2 users and 1 with password) - client1 = WebsocketCommunicator(application, 'manager/ws/subscription/?token={}'.format(self.token)) - client2 = WebsocketCommunicator(application, 'manager/ws/subscription/?token={}'.format(self.token2)) - client3 = WebsocketCommunicator(application, 'manager/ws/subscription/?password={}'.format(password)) + client1 = WebsocketCommunicator( + application, "manager/ws/subscription/?token={}".format(self.token) + ) + client2 = WebsocketCommunicator( + application, "manager/ws/subscription/?token={}".format(self.token2) + ) + client3 = WebsocketCommunicator( + application, "manager/ws/subscription/?password={}".format(password) + ) for client in [client1, client2, client3]: connected, subprotocol = await client.connect() - assert connected, 'Error, client was not connected, test could not be completed' + assert ( + connected + ), "Error, client was not connected, test could not be completed" # ACT await channel_layer.group_send( - 'token-{}'.format(str(self.token)), - {'type': 'logout', 'message': ''} + "token-{}".format(str(self.token)), {"type": "logout", "message": ""} ) - await asyncio.sleep(1) # Wait 1 second, to ensure the connection is closed before we continue + await asyncio.sleep( + 1 + ) # Wait 1 second, to ensure the connection is closed before we continue # ASSERT # Client 1 should not be able to send and receive messages with pytest.raises(AssertionError): await client1.send_json_to(subscription_msg) - response = await client1.receive_json_from() + await client1.receive_json_from() # Client 2 should be able to send and receive messages await client2.send_json_to(subscription_msg) response = await client2.receive_json_from() - assert response['data'] == expected_response + assert response["data"] == expected_response # Client 3 should be able to send and receive messages await client3.send_json_to(subscription_msg) response = await client3.receive_json_from() - assert response['data'] == expected_response + assert response["data"] == expected_response # Disconnect all clients await client1.disconnect() @@ -136,38 +149,48 @@ async def test_connection_interrupted_when_token_is_deleted(self): "csc": "ScriptQueue", "salindex": 0, "stream": "stream1", - "category": "event" + "category": "event", } - expected_response = 'Successfully subscribed to event-ScriptQueue-0-stream1' + expected_response = "Successfully subscribed to event-ScriptQueue-0-stream1" # Connect 3 clients (2 users and 1 with password) - client1 = WebsocketCommunicator(application, 'manager/ws/subscription/?token={}'.format(self.token)) - client2 = WebsocketCommunicator(application, 'manager/ws/subscription/?token={}'.format(self.token2)) - client3 = WebsocketCommunicator(application, 'manager/ws/subscription/?password={}'.format(password)) + client1 = WebsocketCommunicator( + application, "manager/ws/subscription/?token={}".format(self.token) + ) + client2 = WebsocketCommunicator( + application, "manager/ws/subscription/?token={}".format(self.token2) + ) + client3 = WebsocketCommunicator( + application, "manager/ws/subscription/?password={}".format(password) + ) for client in [client1, client2, client3]: connected, subprotocol = await client.connect() - assert connected, 'Error, client was not connected, test could not be completed' + assert ( + connected + ), "Error, client was not connected, test could not be completed" # ACT: delete de token # await self.delete_token() await database_sync_to_async(self.token.delete)() - await asyncio.sleep(1) # Wait 1 second, to ensure the connection is closed before we continue + await asyncio.sleep( + 1 + ) # Wait 1 second, to ensure the connection is closed before we continue # ASSERT # Client 1 should not be able to send and receive messages with pytest.raises(AssertionError): await client1.send_json_to(subscription_msg) - response = await client1.receive_json_from() + await client1.receive_json_from() # Client 2 should be able to send and receive messages await client2.send_json_to(subscription_msg) response = await client2.receive_json_from() - assert response['data'] == expected_response + assert response["data"] == expected_response # Client 3 should be able to send and receive messages await client3.send_json_to(subscription_msg) response = await client3.receive_json_from() - assert response['data'] == expected_response + assert response["data"] == expected_response # Disconnect all clients await client1.disconnect() diff --git a/manager/subscription/tests/test_heartbeat.py b/manager/subscription/tests/test_heartbeat.py index bb22fc85..a41cfdf7 100644 --- a/manager/subscription/tests/test_heartbeat.py +++ b/manager/subscription/tests/test_heartbeat.py @@ -16,16 +16,19 @@ class TestHeartbeat: def setup_method(self): """Set up the TestCase, executed before each test of the TestCase.""" - self.user = User.objects.create_user('username', password='123', email='user@user.cl') + self.user = User.objects.create_user( + "username", password="123", email="user@user.cl" + ) self.token = Token.objects.create(user=self.user) - self.user.user_permissions.add(Permission.objects.get(name='Execute Commands')) - self.url = 'manager/ws/subscription/?token={}'.format(self.token) + self.user.user_permissions.add(Permission.objects.get(name="Execute Commands")) + self.url = "manager/ws/subscription/?token={}".format(self.token) @pytest.mark.asyncio @pytest.mark.django_db(transaction=True) async def test_join_and_leave_subscription(self): # Arrange - await HeartbeatManager.reset() + hb_manager = HeartbeatManager() + await hb_manager.reset() communicator = WebsocketCommunicator(application, self.url) connected, subprotocol = await communicator.connect() @@ -41,10 +44,12 @@ async def test_join_and_leave_subscription(self): response = await communicator.receive_json_from() # Assert 1 - assert response['data'] == f'Successfully subscribed to heartbeat-manager-0-stream' + assert ( + response["data"] == f"Successfully subscribed to heartbeat-manager-0-stream" + ) response = await communicator.receive_json_from(timeout=10) - assert response['data'][0]['data']['timestamp'] is not None + assert response["data"][0]["data"]["timestamp"] is not None # Act 2 (Unsubscribe) msg = { "option": "unsubscribe", @@ -57,7 +62,10 @@ async def test_join_and_leave_subscription(self): response = await communicator.receive_json_from() # Assert 2 - assert response['data'] == f'Successfully unsubscribed to heartbeat-manager-0-stream' + assert ( + response["data"] + == f"Successfully unsubscribed to heartbeat-manager-0-stream" + ) await communicator.disconnect() @@ -76,10 +84,12 @@ async def test_join_and_leave_subscription(self): response = await communicator.receive_json_from() # Assert 1 - assert response['data'] == f'Successfully subscribed to heartbeat-manager-0-stream' + assert ( + response["data"] == f"Successfully subscribed to heartbeat-manager-0-stream" + ) response = await communicator.receive_json_from(timeout=10) - assert response['data'][0]['data']['timestamp'] is not None + assert response["data"][0]["data"]["timestamp"] is not None # Act 2 (Unsubscribe) msg = { "option": "unsubscribe", @@ -92,15 +102,19 @@ async def test_join_and_leave_subscription(self): response = await communicator.receive_json_from() # Assert 2 - assert response['data'] == f'Successfully unsubscribed to heartbeat-manager-0-stream' + assert ( + response["data"] + == f"Successfully unsubscribed to heartbeat-manager-0-stream" + ) await communicator.disconnect() - await HeartbeatManager.stop() + await hb_manager.stop() @pytest.mark.asyncio @pytest.mark.django_db(transaction=True) async def test_heartbeat_manager_setter(self): + hb_manager = HeartbeatManager() # Arrange - await HeartbeatManager.reset() + await hb_manager.reset() communicator = WebsocketCommunicator(application, self.url) connected, subprotocol = await communicator.connect() @@ -116,27 +130,30 @@ async def test_heartbeat_manager_setter(self): response = await communicator.receive_json_from() # Assert 1 - assert response['data'] == f'Successfully subscribed to heartbeat-manager-0-stream' + assert ( + response["data"] == f"Successfully subscribed to heartbeat-manager-0-stream" + ) response = await communicator.receive_json_from(timeout=10) - assert response['data'][0]['data']['timestamp'] is not None + assert response["data"][0]["data"]["timestamp"] is not None # Act 2 Set producer heartbeat timestamp = datetime.datetime.now().timestamp() - HeartbeatManager.set_heartbeat_timestamp('Producer', timestamp) + hb_manager.set_heartbeat_timestamp("Producer", timestamp) response = await communicator.receive_json_from(timeout=4) - + # Assert 2 - heartbeat_sources = [source['csc'] for source in response['data']] - assert 'Producer' in heartbeat_sources + heartbeat_sources = [source["csc"] for source in response["data"]] + assert "Producer" in heartbeat_sources await communicator.disconnect() - await HeartbeatManager.stop() - + await hb_manager.stop() + @pytest.mark.asyncio @pytest.mark.django_db(transaction=True) async def test_producer_heartbeat(self): # Arrange - await HeartbeatManager.reset() + hb_manager = HeartbeatManager() + await hb_manager.reset() communicator = WebsocketCommunicator(application, self.url) connected, subprotocol = await communicator.connect() @@ -152,9 +169,11 @@ async def test_producer_heartbeat(self): response = await communicator.receive_json_from() # Assert 1 - assert response['data'] == f'Successfully subscribed to heartbeat-manager-0-stream' + assert ( + response["data"] == f"Successfully subscribed to heartbeat-manager-0-stream" + ) response = await communicator.receive_json_from(timeout=5) - assert response['data'][0]['data']['timestamp'] is not None + assert response["data"][0]["data"]["timestamp"] is not None # Act 2 (Send producer heartbeat through websocket) msg = { @@ -165,17 +184,21 @@ async def test_producer_heartbeat(self): response = await communicator.receive_json_from(timeout=5) # Assert 2 (Get producer heartbeat data) - heartbeat_sources = [source['csc'] for source in response['data']] - assert 'Producer' in heartbeat_sources + heartbeat_sources = [source["csc"] for source in response["data"]] + assert "Producer" in heartbeat_sources await communicator.disconnect() - await HeartbeatManager.stop() + await hb_manager.stop() @pytest.mark.asyncio @pytest.mark.django_db(transaction=True) - @patch('requests.get', return_value = type('obj', (object,), {'json' : lambda: {'timestamp': 123123123}})) + @patch( + "requests.get", + return_value=type("obj", (object,), {"json": lambda: {"timestamp": 123123123}}), + ) async def test_unauthorized_commander(self, mock_requests): # Arrange - await HeartbeatManager.reset() + hb_manager = HeartbeatManager() + await hb_manager.reset() communicator = WebsocketCommunicator(application, self.url) connected, subprotocol = await communicator.connect() @@ -191,18 +214,24 @@ async def test_unauthorized_commander(self, mock_requests): response = await communicator.receive_json_from() # Assert 1 - assert response['data'] == f'Successfully subscribed to heartbeat-manager-0-stream' + assert ( + response["data"] == f"Successfully subscribed to heartbeat-manager-0-stream" + ) response = await communicator.receive_json_from(timeout=5) - assert response['data'][0]['data']['timestamp'] is not None + assert response["data"][0]["data"]["timestamp"] is not None # Act 2 (Wait for query to commander) - # await asyncio.sleep(3) response = await communicator.receive_json_from(timeout=5) + # Assert 2 (Get producer heartbeat data) - heartbeat_sources = [source['csc'] for source in response['data']] - assert 'Commander' in heartbeat_sources - commander_heartbeat = [source['data'] for source in response['data'] if source['csc'] == 'Commander'][0] - commander_timestamp = commander_heartbeat['timestamp'] + heartbeat_sources = [source["csc"] for source in response["data"]] + assert "Commander" in heartbeat_sources + commander_heartbeat = [ + source["data"] + for source in response["data"] + if source["csc"] == "Commander" + ][0] + commander_timestamp = commander_heartbeat["timestamp"] assert commander_timestamp == 123123123 await communicator.disconnect() - await HeartbeatManager.stop() \ No newline at end of file + await hb_manager.stop() diff --git a/manager/subscription/tests/test_lovecsc_subscriptions.py b/manager/subscription/tests/test_lovecsc_subscriptions.py index 38462ca5..947bfaea 100644 --- a/manager/subscription/tests/test_lovecsc_subscriptions.py +++ b/manager/subscription/tests/test_lovecsc_subscriptions.py @@ -7,7 +7,6 @@ class TestLOVECscSubscriptions: - def setup_method(self): """Set up the TestCase, executed before each test of the TestCase.""" self.user = User.objects.create_user( @@ -41,7 +40,8 @@ async def test_join_and_leave_subscription(self): # Assert 1 assert ( - response["data"] == f"Successfully subscribed to {category}-{csc}-{salindex}-{stream}" + response["data"] + == f"Successfully subscribed to {category}-{csc}-{salindex}-{stream}" ) # Act 2 (Unsubscribe) @@ -66,8 +66,8 @@ async def test_join_and_leave_subscription(self): @pytest.mark.asyncio @pytest.mark.django_db(transaction=True) async def test_observinglog_to_lovecsc(self): - """ Test that an observing log sent by a client is - correctly received by a subscribed LOVE-CSC (producer) client """ + """Test that an observing log sent by a client is + correctly received by a subscribed LOVE-CSC (producer) client""" # Arrange client_communicator = WebsocketCommunicator(application, self.url) @@ -100,9 +100,7 @@ async def test_observinglog_to_lovecsc(self): { "csc": "love", "salindex": 0, - "data": { - "observingLog": {"user": "an user", "message": "a message"} - }, + "data": {"observingLog": {"user": "user", "message": "a message"}}, } ], } diff --git a/manager/subscription/tests/test_subscriptions.py b/manager/subscription/tests/test_subscriptions.py index 1b893c2e..ec9dd8ac 100644 --- a/manager/subscription/tests/test_subscriptions.py +++ b/manager/subscription/tests/test_subscriptions.py @@ -182,7 +182,7 @@ async def test_receive_messages_from_every_subscription(self): "category": combination["category"], } await communicator.send_json_to(msg) - response = await communicator.receive_json_from() + await communicator.receive_json_from() # Act for combination in self.combinations: msg, expected = self.build_messages( @@ -213,7 +213,7 @@ async def test_receive_messages_from_all_subscription(self): "stream": "all", } await communicator.send_json_to(msg) - response = await communicator.receive_json_from() + await communicator.receive_json_from() # Act for combination in self.combinations: msg, expected = self.build_messages( diff --git a/manager/subscription/tests/test_time_data.py b/manager/subscription/tests/test_time_data.py index a59e0c3e..2f1ad30f 100644 --- a/manager/subscription/tests/test_time_data.py +++ b/manager/subscription/tests/test_time_data.py @@ -38,4 +38,4 @@ async def test_get_time_data(self): # Assert 1 assert utils.assert_time_data(time_data) assert request_time == 12312312341123 - await communicator.disconnect() \ No newline at end of file + await communicator.disconnect() diff --git a/manager/templates/index.html b/manager/templates/index.html index ce8e5764..b86ed454 100644 --- a/manager/templates/index.html +++ b/manager/templates/index.html @@ -1,6 +1,6 @@ {% load render_bundle from webpack_loader %} - + diff --git a/manager/templates/registration/login.html b/manager/templates/registration/login.html index 9b13f800..a7e0846d 100644 --- a/manager/templates/registration/login.html +++ b/manager/templates/registration/login.html @@ -1,6 +1,7 @@ {% load static %} - + + diff --git a/manager/templates/test.html b/manager/templates/test.html index 0aa9cd75..6e32f1f6 100644 --- a/manager/templates/test.html +++ b/manager/templates/test.html @@ -1,5 +1,5 @@ - + diff --git a/manager/ui_framework/__init__.py b/manager/ui_framework/__init__.py index 76b9ba27..5c88eb14 100644 --- a/manager/ui_framework/__init__.py +++ b/manager/ui_framework/__init__.py @@ -1 +1 @@ -default_app_config = 'ui_framework.apps.UiFrameworkConfig' \ No newline at end of file +default_app_config = "ui_framework.apps.UiFrameworkConfig" diff --git a/manager/ui_framework/migrations/0001_initial.py b/manager/ui_framework/migrations/0001_initial.py index 9f9d6c0d..319caa33 100644 --- a/manager/ui_framework/migrations/0001_initial.py +++ b/manager/ui_framework/migrations/0001_initial.py @@ -8,53 +8,118 @@ class Migration(migrations.Migration): initial = True - dependencies = [ - ] + dependencies = [] operations = [ migrations.CreateModel( - name='View', + name="View", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('creation_timestamp', models.DateTimeField(auto_now_add=True, verbose_name='Creation time')), - ('update_timestamp', models.DateTimeField(auto_now=True, verbose_name='Last Updated')), - ('name', models.CharField(max_length=20)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "creation_timestamp", + models.DateTimeField( + auto_now_add=True, verbose_name="Creation time" + ), + ), + ( + "update_timestamp", + models.DateTimeField(auto_now=True, verbose_name="Last Updated"), + ), + ("name", models.CharField(max_length=20)), ], options={ - 'abstract': False, + "abstract": False, }, ), migrations.CreateModel( - name='Workspace', + name="Workspace", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('creation_timestamp', models.DateTimeField(auto_now_add=True, verbose_name='Creation time')), - ('update_timestamp', models.DateTimeField(auto_now=True, verbose_name='Last Updated')), - ('name', models.CharField(max_length=20)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "creation_timestamp", + models.DateTimeField( + auto_now_add=True, verbose_name="Creation time" + ), + ), + ( + "update_timestamp", + models.DateTimeField(auto_now=True, verbose_name="Last Updated"), + ), + ("name", models.CharField(max_length=20)), ], options={ - 'abstract': False, + "abstract": False, }, ), migrations.CreateModel( - name='WorkspaceView', + name="WorkspaceView", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('creation_timestamp', models.DateTimeField(auto_now_add=True, verbose_name='Creation time')), - ('update_timestamp', models.DateTimeField(auto_now=True, verbose_name='Last Updated')), - ('view_name', models.CharField(blank=True, max_length=20)), - ('sort_value', models.PositiveIntegerField(default=0)), - ('view', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_views', to='ui_framework.View')), - ('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_views', to='ui_framework.Workspace')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "creation_timestamp", + models.DateTimeField( + auto_now_add=True, verbose_name="Creation time" + ), + ), + ( + "update_timestamp", + models.DateTimeField(auto_now=True, verbose_name="Last Updated"), + ), + ("view_name", models.CharField(blank=True, max_length=20)), + ("sort_value", models.PositiveIntegerField(default=0)), + ( + "view", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="workspace_views", + to="ui_framework.View", + ), + ), + ( + "workspace", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="workspace_views", + to="ui_framework.Workspace", + ), + ), ], options={ - 'ordering': ('sort_value',), - 'unique_together': {('workspace', 'view')}, + "ordering": ("sort_value",), + "unique_together": {("workspace", "view")}, }, ), migrations.AddField( - model_name='workspace', - name='views', - field=models.ManyToManyField(related_name='workspaces', through='ui_framework.WorkspaceView', to='ui_framework.View'), + model_name="workspace", + name="views", + field=models.ManyToManyField( + related_name="workspaces", + through="ui_framework.WorkspaceView", + to="ui_framework.View", + ), ), ] diff --git a/manager/ui_framework/migrations/0002_view_data.py b/manager/ui_framework/migrations/0002_view_data.py index 61d655cb..c2236e58 100644 --- a/manager/ui_framework/migrations/0002_view_data.py +++ b/manager/ui_framework/migrations/0002_view_data.py @@ -7,13 +7,13 @@ class Migration(migrations.Migration): dependencies = [ - ('ui_framework', '0001_initial'), + ("ui_framework", "0001_initial"), ] operations = [ migrations.AddField( - model_name='view', - name='data', + model_name="view", + name="data", field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True), ), ] diff --git a/manager/ui_framework/migrations/0003_view_thumbnail.py b/manager/ui_framework/migrations/0003_view_thumbnail.py index 336066c3..674ab27f 100644 --- a/manager/ui_framework/migrations/0003_view_thumbnail.py +++ b/manager/ui_framework/migrations/0003_view_thumbnail.py @@ -7,13 +7,17 @@ class Migration(migrations.Migration): dependencies = [ - ('ui_framework', '0002_view_data'), + ("ui_framework", "0002_view_data"), ] operations = [ migrations.AddField( - model_name='view', - name='thumbnail', - field=models.ImageField(default='thumbnails/default.png', storage=ui_framework.models.OverwriteStorage(), upload_to='thumbnails/'), + model_name="view", + name="thumbnail", + field=models.ImageField( + default="thumbnails/default.png", + storage=ui_framework.models.OverwriteStorage(), + upload_to="thumbnails/", + ), ), ] diff --git a/manager/ui_framework/serializers.py b/manager/ui_framework/serializers.py index 1c5a01cf..0f55a671 100644 --- a/manager/ui_framework/serializers.py +++ b/manager/ui_framework/serializers.py @@ -23,9 +23,9 @@ class Base64ImageField(serializers.ImageField): @staticmethod def _get_view_id_from_data(data): - """ Return a view_id integer for building the thumbnail file_namet + """Return a view_id integer for building the thumbnail file_namet by checking whether the id comes in the request data or if a new one - has to be created """ + has to be created""" # id field should come in req data if view exists if "id" in data: return data["id"] @@ -88,7 +88,10 @@ def to_internal_value(self, data): # Get the file name extension: file_extension = self.get_file_extension(file_name, decoded_file) - complete_file_name = "%s.%s" % (file_name, file_extension,) + complete_file_name = "%s.%s" % ( + file_name, + file_extension, + ) data = ContentFile(decoded_file, name=complete_file_name) diff --git a/manager/ui_framework/signals.py b/manager/ui_framework/signals.py index 3417f35a..13163dc8 100644 --- a/manager/ui_framework/signals.py +++ b/manager/ui_framework/signals.py @@ -17,7 +17,7 @@ class of the sender, in this case 'View' arguments dictionary sent with the signal. It contains the key 'instance' with the View instance that was deleted """ - deleted_view = kwargs['instance'] + deleted_view = kwargs["instance"] file_url = settings.MEDIA_BASE + deleted_view.thumbnail.url try: os.remove(file_url) diff --git a/manager/ui_framework/tests/test_view_thumbnail.py b/manager/ui_framework/tests/test_view_thumbnail.py index 9bcfe8f5..791d0e42 100644 --- a/manager/ui_framework/tests/test_view_thumbnail.py +++ b/manager/ui_framework/tests/test_view_thumbnail.py @@ -22,21 +22,23 @@ def setUp(self): # Arrange self.client = APIClient() self.user = User.objects.create_user( - username='an user', - password='password', - email='test@user.cl', - first_name='First', - last_name='Last', + username="user", + password="password", + email="test@user.cl", + first_name="First", + last_name="Last", ) self.token = Token.objects.create(user=self.user) - self.client.credentials(HTTP_AUTHORIZATION='Token ' + self.token.key) - self.user.user_permissions.add(Permission.objects.get(codename='view_view'), - Permission.objects.get(codename='add_view'), - Permission.objects.get(codename='delete_view'), - Permission.objects.get(codename='change_view')) + self.client.credentials(HTTP_AUTHORIZATION="Token " + self.token.key) + self.user.user_permissions.add( + Permission.objects.get(codename="view_view"), + Permission.objects.get(codename="add_view"), + Permission.objects.get(codename="delete_view"), + Permission.objects.get(codename="change_view"), + ) # delete existing test thumbnails - thumbnail_files_list = glob.glob(settings.MEDIA_ROOT + '/thumbnails/*') + thumbnail_files_list = glob.glob(settings.MEDIA_ROOT + "/thumbnails/*") for file in thumbnail_files_list: os.remove(file) @@ -45,19 +47,21 @@ def test_new_view(self): # Arrange # read test data (base64 string) old_count = View.objects.count() - mock_location = os.path.join(os.getcwd(), 'ui_framework', 'tests', 'media', 'mock', 'test') + mock_location = os.path.join( + os.getcwd(), "ui_framework", "tests", "media", "mock", "test" + ) with open(mock_location) as f: image_data = f.read() request_data = { "name": "view name", "data": {"key1": "value1"}, - "thumbnail": image_data + "thumbnail": image_data, } # Act 1 # send POST request with data - request_url = reverse('view-list') - response = self.client.post(request_url, request_data, format='json') + request_url = reverse("view-list") + response = self.client.post(request_url, request_data, format="json") # Assert # - response status code 201 @@ -69,43 +73,49 @@ def test_new_view(self): # - thumbnail url view = View.objects.get(name="view name") - self.assertEqual(view.thumbnail.url, '/media/thumbnails/view_1.png') + self.assertEqual(view.thumbnail.url, "/media/thumbnails/view_1.png") # - expected response data expected_response = { - 'id': view.id, - 'name': 'view name', - 'thumbnail': view.thumbnail.url, - 'data': {'key1': 'value1'}, + "id": view.id, + "name": "view name", + "thumbnail": view.thumbnail.url, + "data": {"key1": "value1"}, } self.assertEqual(response.data, expected_response) # - stored file content file_url = settings.MEDIA_BASE + view.thumbnail.url - expected_url = mock_location + '.png' - self.assertTrue(filecmp.cmp(file_url, expected_url), - f'\nThe image was not saved as expected\nsaved at {file_url}\nexpected at {expected_url}') + expected_url = mock_location + ".png" + self.assertTrue( + filecmp.cmp(file_url, expected_url), + f"\nThe image was not saved as expected\nsaved at {file_url}\nexpected at {expected_url}", + ) def test_delete_view(self): """ Test thumbnail behavior when deleting a view """ # Arrange # add view with thumbnail - mock_location = os.path.join(os.getcwd(), 'ui_framework', 'tests', 'media', 'mock', 'test') + mock_location = os.path.join( + os.getcwd(), "ui_framework", "tests", "media", "mock", "test" + ) with open(mock_location) as f: image_data = f.read() request_data = { "name": "view name", "data": {"key1": "value1"}, - "thumbnail": image_data + "thumbnail": image_data, } - request_url = reverse('view-list') - response = self.client.post(request_url, request_data, format='json') + request_url = reverse("view-list") + self.client.post(request_url, request_data, format="json") # Act # delete the view view = View.objects.get(name="view name") - delete_response = self.client.delete(reverse('view-detail', kwargs={'pk': view.pk})) + delete_response = self.client.delete( + reverse("view-detail", kwargs={"pk": view.pk}) + ) # Assert 2 @@ -115,9 +125,9 @@ def test_delete_view(self): # - file does not exist file_url = settings.MEDIA_BASE + view.thumbnail.url with pytest.raises(FileNotFoundError): - f = open(file_url, 'r') + f = open(file_url, "r") f.close() # - getting the file gives 404 - get_deleted_response = self.client.get('/manager' + view.thumbnail.url) + get_deleted_response = self.client.get("/manager" + view.thumbnail.url) self.assertEqual(get_deleted_response.status_code, status.HTTP_404_NOT_FOUND) diff --git a/manager/ui_framework/tests/tests_api.py b/manager/ui_framework/tests/tests_api.py index e6d28014..ff73434d 100644 --- a/manager/ui_framework/tests/tests_api.py +++ b/manager/ui_framework/tests/tests_api.py @@ -18,77 +18,85 @@ def test_unauthenticated_list_objects(self): """Test that unauthenticated users cannot retrieve the list of objects through the API.""" for case in self.cases: # Act - url = reverse('{}-list'.format(case['key'])) + url = reverse("{}-list".format(case["key"])) response = self.client.get(url) # Assert self.assertEqual( - response.status_code, status.HTTP_401_UNAUTHORIZED, - 'Get list of {} did not return status 401'.format(case['class']) + response.status_code, + status.HTTP_401_UNAUTHORIZED, + "Get list of {} did not return status 401".format(case["class"]), ) def test_unauthenticated_create_objects(self): """Test that unauthenticated users cannot create objects through the API.""" for case in self.cases: # Act - url = reverse('{}-list'.format(case['key'])) - response = self.client.post(url, case['new_data']) + url = reverse("{}-list".format(case["key"])) + response = self.client.post(url, case["new_data"]) # Assert self.assertEqual( - response.status_code, status.HTTP_401_UNAUTHORIZED, - 'Posting a new {} did not return status 401'.format(case['class']) + response.status_code, + status.HTTP_401_UNAUTHORIZED, + "Posting a new {} did not return status 401".format(case["class"]), ) self.assertEqual( - case['class'].objects.count(), case['old_count'], - 'The number of {} should not have changed'.format(case['class']) + case["class"].objects.count(), + case["old_count"], + "The number of {} should not have changed".format(case["class"]), ) def test_unauthenticated_retrieve_objects(self): """Test that unauthenticated users cannot retrieve objects through the API.""" for case in self.cases: # Act - obj = case['class'].objects.first() - url = reverse('{}-detail'.format(case['key']), kwargs={'pk': obj.pk}) + obj = case["class"].objects.first() + url = reverse("{}-detail".format(case["key"]), kwargs={"pk": obj.pk}) response = self.client.get(url) # Assert self.assertEqual( - response.status_code, status.HTTP_401_UNAUTHORIZED, - 'Getting a {} did not return status 401'.format(case['class']) + response.status_code, + status.HTTP_401_UNAUTHORIZED, + "Getting a {} did not return status 401".format(case["class"]), ) def test_unauthenticated_update_objects(self): """Test that unauthenticated users cannot update objects through the API.""" for case in self.cases: # Act - obj = case['class'].objects.first() + obj = case["class"].objects.first() old_data = get_dict(obj) - url = reverse('{}-detail'.format(case['key']), kwargs={'pk': obj.pk}) - response = self.client.put(url, case['new_data']) + url = reverse("{}-detail".format(case["key"]), kwargs={"pk": obj.pk}) + response = self.client.put(url, case["new_data"]) # Assert self.assertEqual( - response.status_code, status.HTTP_401_UNAUTHORIZED, - 'Updating a {} did not return status 401'.format(case['class']) + response.status_code, + status.HTTP_401_UNAUTHORIZED, + "Updating a {} did not return status 401".format(case["class"]), ) - new_data = get_dict(case['class'].objects.get(pk=obj.pk)) + new_data = get_dict(case["class"].objects.get(pk=obj.pk)) self.assertEqual( - new_data, old_data, - 'The object {} should not have been updated'.format(case['class']) + new_data, + old_data, + "The object {} should not have been updated".format(case["class"]), ) def test_unauthenticated_delete_objects(self): """Test that unauthenticated users cannot dalete objects through the API.""" for case in self.cases: # Act - obj = case['class'].objects.first() - url = reverse('{}-detail'.format(case['key']), kwargs={'pk': obj.pk}) + obj = case["class"].objects.first() + url = reverse("{}-detail".format(case["key"]), kwargs={"pk": obj.pk}) response = self.client.delete(url) # Assert self.assertEqual( - response.status_code, status.HTTP_401_UNAUTHORIZED, - 'Deleting a {} did not return status 401'.format(case['class']) + response.status_code, + status.HTTP_401_UNAUTHORIZED, + "Deleting a {} did not return status 401".format(case["class"]), ) self.assertEqual( - case['class'].objects.count(), case['old_count'], - 'The number of {} should not have changed'.format(case['class']) + case["class"].objects.count(), + case["old_count"], + "The number of {} should not have changed".format(case["class"]), ) @@ -99,106 +107,116 @@ def setUp(self): """Set testcase. Inherits from utils.BaseTestCase.""" # Arrange super().setUp() - self.login_url = reverse('login') - self.username = 'test' - self.password = 'password' + self.login_url = reverse("login") + self.username = "test" + self.password = "password" self.user = User.objects.create_user( username=self.username, - password='password', - email='test@user.cl', - first_name='First', - last_name='Last', + password="password", + email="test@user.cl", + first_name="First", + last_name="Last", ) - data = {'username': self.username, 'password': self.password} - self.client.post(self.login_url, data, format='json') + data = {"username": self.username, "password": self.password} + self.client.post(self.login_url, data, format="json") self.token = Token.objects.get(user__username=self.username) - self.client.credentials(HTTP_AUTHORIZATION='Token ' + self.token.key) + self.client.credentials(HTTP_AUTHORIZATION="Token " + self.token.key) def test_unauthorized_list_objects(self): """Test that unauthorized users can still retrieve the list of objects through the API.""" for case in self.cases: # Act - url = reverse('{}-list'.format(case['key'])) + url = reverse("{}-list".format(case["key"])) response = self.client.get(url) # Assert self.assertEqual( - response.status_code, status.HTTP_200_OK, - 'Retrieving list of {} did not return status 200'.format(case['class']) + response.status_code, + status.HTTP_200_OK, + "Retrieving list of {} did not return status 200".format(case["class"]), ) retrieved_data = [dict(data) for data in response.data] self.assertEqual( - retrieved_data, case['current_data'], - 'Retrieved list of {} is not as expected'.format(case['class']) + retrieved_data, + case["current_data"], + "Retrieved list of {} is not as expected".format(case["class"]), ) def test_unauthorized_create_objects(self): """Test that unauthorized users cannot create objects through the API.""" for case in self.cases: # Act - url = reverse('{}-list'.format(case['key'])) - response = self.client.post(url, case['new_data']) + url = reverse("{}-list".format(case["key"])) + response = self.client.post(url, case["new_data"]) # Assert self.assertEqual( - response.status_code, status.HTTP_403_FORBIDDEN, - 'Posting a new {} did not return status 403'.format(case['class']) + response.status_code, + status.HTTP_403_FORBIDDEN, + "Posting a new {} did not return status 403".format(case["class"]), ) self.assertEqual( - case['class'].objects.count(), case['old_count'], - 'The number of {} should not have changed'.format(case['class']) + case["class"].objects.count(), + case["old_count"], + "The number of {} should not have changed".format(case["class"]), ) def test_unauthorized_retrieve_objects(self): """Test that unauthorized users can still retrieve objects through the API.""" for case in self.cases: # Act - obj = case['class'].objects.get(id=case['selected_id']) - url = reverse('{}-detail'.format(case['key']), kwargs={'pk': obj.pk}) + obj = case["class"].objects.get(id=case["selected_id"]) + url = reverse("{}-detail".format(case["key"]), kwargs={"pk": obj.pk}) response = self.client.get(url) # Assert self.assertEqual( - response.status_code, status.HTTP_200_OK, - 'Getting a {} did not return status 200'.format(case['class']) + response.status_code, + status.HTTP_200_OK, + "Getting a {} did not return status 200".format(case["class"]), ) retrieved_data = dict(response.data) self.assertEqual( - retrieved_data, case['current_data'][0], - 'Retrieved list of {} is not as expected'.format(case['class']) + retrieved_data, + case["current_data"][0], + "Retrieved list of {} is not as expected".format(case["class"]), ) def test_unauthorized_update_objects(self): """Test that unauthorized users cannot update objects through the API.""" for case in self.cases: # Act - obj = case['class'].objects.get(id=case['selected_id']) + obj = case["class"].objects.get(id=case["selected_id"]) old_data = get_dict(obj) - url = reverse('{}-detail'.format(case['key']), kwargs={'pk': obj.pk}) - response = self.client.put(url, case['new_data']) + url = reverse("{}-detail".format(case["key"]), kwargs={"pk": obj.pk}) + response = self.client.put(url, case["new_data"]) # Assert self.assertEqual( - response.status_code, status.HTTP_403_FORBIDDEN, - 'Updating a {} did not return status 403'.format(case['class']) + response.status_code, + status.HTTP_403_FORBIDDEN, + "Updating a {} did not return status 403".format(case["class"]), ) - new_data = get_dict(case['class'].objects.get(pk=obj.pk)) + new_data = get_dict(case["class"].objects.get(pk=obj.pk)) self.assertEqual( - new_data, old_data, - 'The object {} should not have been updated'.format(case['class']) + new_data, + old_data, + "The object {} should not have been updated".format(case["class"]), ) def test_unauthorized_delete_objects(self): """Test that unauthorized users cannot dalete objects through the API.""" for case in self.cases: # Act - obj = case['class'].objects.first() - url = reverse('{}-detail'.format(case['key']), kwargs={'pk': obj.pk}) + obj = case["class"].objects.first() + url = reverse("{}-detail".format(case["key"]), kwargs={"pk": obj.pk}) response = self.client.delete(url) # Assert self.assertEqual( - response.status_code, status.HTTP_403_FORBIDDEN, - 'Deleting a {} did not return status 403'.format(case['class']) + response.status_code, + status.HTTP_403_FORBIDDEN, + "Deleting a {} did not return status 403".format(case["class"]), ) self.assertEqual( - case['class'].objects.count(), case['old_count'], - 'The number of {} should not have changed'.format(case['class']) + case["class"].objects.count(), + case["old_count"], + "The number of {} should not have changed".format(case["class"]), ) @@ -209,117 +227,137 @@ def setUp(self): """Set testcase. Inherits from utils.BaseTestCase.""" # Arrange super().setUp() - self.login_url = reverse('login') - self.username = 'test' - self.password = 'password' + self.login_url = reverse("login") + self.username = "test" + self.password = "password" self.user = User.objects.create_user( username=self.username, - password='password', - email='test@user.cl', - first_name='First', - last_name='Last', + password="password", + email="test@user.cl", + first_name="First", + last_name="Last", ) - data = {'username': self.username, 'password': self.password} - self.client.post(self.login_url, data, format='json') + data = {"username": self.username, "password": self.password} + self.client.post(self.login_url, data, format="json") self.token = Token.objects.get(user__username=self.username) - self.client.credentials(HTTP_AUTHORIZATION='Token ' + self.token.key) + self.client.credentials(HTTP_AUTHORIZATION="Token " + self.token.key) def test_authorized_list_objects(self): """Test that authorized users can retrieve the list of objects through the API.""" for case in self.cases: # Arrange - self.user.user_permissions.add(Permission.objects.get(codename='view_{}'.format(case['key']))) + self.user.user_permissions.add( + Permission.objects.get(codename="view_{}".format(case["key"])) + ) # Act - url = reverse('{}-list'.format(case['key'])) + url = reverse("{}-list".format(case["key"])) response = self.client.get(url) # Assert self.assertEqual( - response.status_code, status.HTTP_200_OK, - 'Retrieving list of {} did not return status 200'.format(case['class']) + response.status_code, + status.HTTP_200_OK, + "Retrieving list of {} did not return status 200".format(case["class"]), ) retrieved_data = [dict(data) for data in response.data] - expected_data = case['current_data'] + expected_data = case["current_data"] self.assertEqual( - retrieved_data, expected_data, - 'Retrieved list of {} is not as expected'.format(case['class']) + retrieved_data, + expected_data, + "Retrieved list of {} is not as expected".format(case["class"]), ) def test_authorized_create_objects(self): """Test that authorized users can create objects through the API.""" for case in self.cases: # Arrange - self.user.user_permissions.add(Permission.objects.get(codename='add_{}'.format(case['key']))) + self.user.user_permissions.add( + Permission.objects.get(codename="add_{}".format(case["key"])) + ) # Act - url = reverse('{}-list'.format(case['key'])) - response = self.client.post(url, case['new_data']) + url = reverse("{}-list".format(case["key"])) + response = self.client.post(url, case["new_data"]) # Assert self.assertEqual( - response.status_code, status.HTTP_201_CREATED, - 'Posting a new {} did not return status 201'.format(case['class']) + response.status_code, + status.HTTP_201_CREATED, + "Posting a new {} did not return status 201".format(case["class"]), ) self.assertEqual( - case['class'].objects.count(), case['old_count'] + 1, - 'The number of {} should have increased by 1'.format(case['class']) + case["class"].objects.count(), + case["old_count"] + 1, + "The number of {} should have increased by 1".format(case["class"]), ) def test_authorized_retrieve_objects(self): """Test that authorized users can retrieve objects through the API.""" for case in self.cases: # Arrange - self.user.user_permissions.add(Permission.objects.get(codename='view_{}'.format(case['key']))) + self.user.user_permissions.add( + Permission.objects.get(codename="view_{}".format(case["key"])) + ) # Act - obj = case['class'].objects.get(id=case['selected_id']) - url = reverse('{}-detail'.format(case['key']), kwargs={'pk': obj.pk}) + obj = case["class"].objects.get(id=case["selected_id"]) + url = reverse("{}-detail".format(case["key"]), kwargs={"pk": obj.pk}) response = self.client.get(url) # Assert self.assertEqual( - response.status_code, status.HTTP_200_OK, - 'Getting a {} did not return status 200'.format(case['class']) + response.status_code, + status.HTTP_200_OK, + "Getting a {} did not return status 200".format(case["class"]), ) retrieved_data = dict(response.data) - expected_data = case['current_data'][0] + expected_data = case["current_data"][0] self.assertEqual( - retrieved_data, expected_data, - 'Retrieved list of {} is not as expected'.format(case['class']) + retrieved_data, + expected_data, + "Retrieved list of {} is not as expected".format(case["class"]), ) def test_authorized_update_objects(self): """Test that authorized users can update objects through the API.""" for case in self.cases: # Arrange - self.user.user_permissions.add(Permission.objects.get(codename='change_{}'.format(case['key']))) + self.user.user_permissions.add( + Permission.objects.get(codename="change_{}".format(case["key"])) + ) # Act - obj = case['class'].objects.get(id=case['selected_id']) + obj = case["class"].objects.get(id=case["selected_id"]) old_data = get_dict(obj) - url = reverse('{}-detail'.format(case['key']), kwargs={'pk': obj.pk}) - response = self.client.put(url, case['new_data']) + url = reverse("{}-detail".format(case["key"]), kwargs={"pk": obj.pk}) + response = self.client.put(url, case["new_data"]) # Assert self.assertEqual( - response.status_code, status.HTTP_200_OK, - 'Updating a {} did not return status 200'.format(case['class']) + response.status_code, + status.HTTP_200_OK, + "Updating a {} did not return status 200".format(case["class"]), ) - new_data = get_dict(case['class'].objects.get(pk=obj.pk)) + new_data = get_dict(case["class"].objects.get(pk=obj.pk)) self.assertNotEqual( - new_data, old_data, - 'The object {} should have been updated'.format(case['class']) + new_data, + old_data, + "The object {} should have been updated".format(case["class"]), ) def test_authorized_delete_objects(self): """Test that authorized users can dalete objects through the API.""" for case in self.cases: # Arrange - old_count = case['class'].objects.count() - self.user.user_permissions.add(Permission.objects.get(codename='delete_{}'.format(case['key']))) + old_count = case["class"].objects.count() + self.user.user_permissions.add( + Permission.objects.get(codename="delete_{}".format(case["key"])) + ) # Act - obj = case['class'].objects.first() - url = reverse('{}-detail'.format(case['key']), kwargs={'pk': obj.pk}) + obj = case["class"].objects.first() + url = reverse("{}-detail".format(case["key"]), kwargs={"pk": obj.pk}) response = self.client.delete(url) # Assert self.assertEqual( - response.status_code, status.HTTP_204_NO_CONTENT, - 'Deleting a {} did not return status 204'.format(case['class']) + response.status_code, + status.HTTP_204_NO_CONTENT, + "Deleting a {} did not return status 204".format(case["class"]), ) self.assertEqual( - case['class'].objects.count(), old_count - 1, - 'The number of {} should have decreased by 1'.format(case['class']) + case["class"].objects.count(), + old_count - 1, + "The number of {} should have decreased by 1".format(case["class"]), ) diff --git a/manager/ui_framework/tests/tests_custom_api.py b/manager/ui_framework/tests/tests_custom_api.py index e96b1eae..626f4e17 100644 --- a/manager/ui_framework/tests/tests_custom_api.py +++ b/manager/ui_framework/tests/tests_custom_api.py @@ -15,64 +15,79 @@ def setUp(self): """Set testcase. Inherits from utils.BaseTestCase.""" # Arrange super().setUp() - self.login_url = reverse('login') - self.username = 'test' - self.password = 'password' + self.login_url = reverse("login") + self.username = "test" + self.password = "password" self.user = User.objects.create_user( username=self.username, - password='password', - email='test@user.cl', - first_name='First', - last_name='Last', + password="password", + email="test@user.cl", + first_name="First", + last_name="Last", ) - data = {'username': self.username, 'password': self.password} - self.client.post(self.login_url, data, format='json') + data = {"username": self.username, "password": self.password} + self.client.post(self.login_url, data, format="json") self.token = Token.objects.get(user__username=self.username) - self.client.credentials(HTTP_AUTHORIZATION='Token ' + self.token.key) + self.client.credentials(HTTP_AUTHORIZATION="Token " + self.token.key) def test_get_workspaces_with_view_name(self): """Test that authorized users can retrieve the list of available workspaces, with views ids and names.""" # Arrange - self.user.user_permissions.add(Permission.objects.get(codename='view_workspace')) + self.user.user_permissions.add( + Permission.objects.get(codename="view_workspace") + ) expected_data = [ - {**w, 'views': [{ - 'id': v_pk, - 'name': v.name, - 'thumbnail': settings.MEDIA_URL + v.thumbnail.name, - } for v_pk in w['views'] for v in [View.objects.get(pk=v_pk)]]} + { + **w, + "views": [ + { + "id": v_pk, + "name": v.name, + "thumbnail": settings.MEDIA_URL + v.thumbnail.name, + } + for v_pk in w["views"] + for v in [View.objects.get(pk=v_pk)] + ], + } for w in self.workspaces_data ] # Act - url = reverse('workspace-with-view-name') + url = reverse("workspace-with-view-name") response = self.client.get(url) # Assert self.assertEqual( - response.status_code, status.HTTP_200_OK, - 'Retrieving list of workspaces did not return status 200' + response.status_code, + status.HTTP_200_OK, + "Retrieving list of workspaces did not return status 200", ) retrieved_data = [dict(data) for data in response.data] self.assertEqual( - retrieved_data, expected_data, - 'Retrieved list of workspaces is not as expected' + retrieved_data, + expected_data, + "Retrieved list of workspaces is not as expected", ) def test_get_full_workspace(self): """Test that authorized users can retrieve a workspace with all its views fully subserialized.""" # Arrange - self.user.user_permissions.add(Permission.objects.get(codename='view_workspace')) + self.user.user_permissions.add( + Permission.objects.get(codename="view_workspace") + ) w = self.workspaces_data[0] - expected_data = {**w, 'views': self.views_data[0:2]} + expected_data = {**w, "views": self.views_data[0:2]} # Act - url = reverse('workspace-full', kwargs={'pk': w['id']}) + url = reverse("workspace-full", kwargs={"pk": w["id"]}) response = self.client.get(url) # Assert self.assertEqual( - response.status_code, status.HTTP_200_OK, - 'Retrieving list of workspaces did not return status 200' + response.status_code, + status.HTTP_200_OK, + "Retrieving list of workspaces did not return status 200", ) retrieved_data = dict(response.data) self.assertEqual( - retrieved_data, expected_data, - 'Retrieved list of workspaces is not as expected' + retrieved_data, + expected_data, + "Retrieved list of workspaces is not as expected", ) diff --git a/manager/ui_framework/tests/tests_models.py b/manager/ui_framework/tests/tests_models.py index e26dca8b..a0b88eed 100644 --- a/manager/ui_framework/tests/tests_models.py +++ b/manager/ui_framework/tests/tests_models.py @@ -11,7 +11,7 @@ class WorkspaceModelTestCase(TestCase): def setUp(self): """Testcase setup.""" # Arrange - self.workspace_name = 'My Workspace' + self.workspace_name = "My Workspace" self.old_workspaces_num = Workspace.objects.count() self.creation_timestamp = timezone.now() with freeze_time(self.creation_timestamp): @@ -22,20 +22,22 @@ def test_create_workspace(self): # Assert self.new_workspaces_num = Workspace.objects.count() self.assertEqual( - self.old_workspaces_num + 1, self.new_workspaces_num, - 'There is not a new object in the database' + self.old_workspaces_num + 1, + self.new_workspaces_num, + "There is not a new object in the database", ) self.assertEqual( - self.workspace.name, self.workspace_name, - 'The name is not as expected' + self.workspace.name, self.workspace_name, "The name is not as expected" ) self.assertEqual( - self.workspace.creation_timestamp, self.creation_timestamp, - 'The creation_timestamp is not as expected' + self.workspace.creation_timestamp, + self.creation_timestamp, + "The creation_timestamp is not as expected", ) self.assertEqual( - self.workspace.update_timestamp, self.creation_timestamp, - 'The update_timestamp is not as expected' + self.workspace.update_timestamp, + self.creation_timestamp, + "The update_timestamp is not as expected", ) def test_retrieve_workspace(self): @@ -49,20 +51,22 @@ def test_retrieve_workspace(self): # Assert self.new_workspaces_num = Workspace.objects.count() self.assertEqual( - self.old_workspaces_num, self.new_workspaces_num, - 'The number of objects in the DB should not change' + self.old_workspaces_num, + self.new_workspaces_num, + "The number of objects in the DB should not change", ) self.assertEqual( - self.workspace.name, self.workspace_name, - 'The name is not as expected' + self.workspace.name, self.workspace_name, "The name is not as expected" ) self.assertEqual( - self.workspace.creation_timestamp, self.creation_timestamp, - 'The creation_timestamp is not as expected' + self.workspace.creation_timestamp, + self.creation_timestamp, + "The creation_timestamp is not as expected", ) self.assertEqual( - self.workspace.update_timestamp, self.creation_timestamp, - 'The update_timestamp is not as expected' + self.workspace.update_timestamp, + self.creation_timestamp, + "The update_timestamp is not as expected", ) def test_update_workspace(self): @@ -73,31 +77,34 @@ def test_update_workspace(self): # Act self.update_timestamp = timezone.now() with freeze_time(self.update_timestamp): - self.workspace.name = 'This other name' + self.workspace.name = "This other name" self.workspace.save() # Assert self.workspace = Workspace.objects.get(pk=self.workspace.pk) self.new_workspaces_num = Workspace.objects.count() self.assertEqual( - self.old_workspaces_num, self.new_workspaces_num, - 'The number of objects in the DB should not change' + self.old_workspaces_num, + self.new_workspaces_num, + "The number of objects in the DB should not change", ) self.assertEqual( - self.workspace.name, 'This other name', - 'The name is not as expected' + self.workspace.name, "This other name", "The name is not as expected" ) self.assertEqual( - self.workspace.creation_timestamp, self.creation_timestamp, - 'The creation_timestamp is not as expected' + self.workspace.creation_timestamp, + self.creation_timestamp, + "The creation_timestamp is not as expected", ) self.assertEqual( - self.workspace.update_timestamp, self.update_timestamp, - 'The update_timestamp is not as expected' + self.workspace.update_timestamp, + self.update_timestamp, + "The update_timestamp is not as expected", ) self.assertNotEqual( - self.workspace.creation_timestamp, self.workspace.update_timestamp, - 'The update_timestamp should be updated after updating the object' + self.workspace.creation_timestamp, + self.workspace.update_timestamp, + "The update_timestamp should be updated after updating the object", ) def test_delete_workspace(self): @@ -113,8 +120,9 @@ def test_delete_workspace(self): # Assert self.new_workspaces_num = Workspace.objects.count() self.assertEqual( - self.old_workspaces_num - 1, self.new_workspaces_num, - 'The number of objects in the DB have decreased by 1' + self.old_workspaces_num - 1, + self.new_workspaces_num, + "The number of objects in the DB have decreased by 1", ) with self.assertRaises(Exception): Workspace.objects.get(pk=self.workspace_pk) @@ -126,7 +134,7 @@ class ViewModelTestCase(TestCase): def setUp(self): """Testcase setup.""" # Arrange - self.view_name = 'My View' + self.view_name = "My View" self.old_views_num = View.objects.count() self.creation_timestamp = timezone.now() with freeze_time(self.creation_timestamp): @@ -137,20 +145,20 @@ def test_create_view(self): # Assert self.new_views_num = View.objects.count() self.assertEqual( - self.old_views_num + 1, self.new_views_num, - 'There is not a new object in the database' + self.old_views_num + 1, + self.new_views_num, + "There is not a new object in the database", ) + self.assertEqual(self.view.name, self.view_name, "The name is not as expected") self.assertEqual( - self.view.name, self.view_name, - 'The name is not as expected' + self.view.creation_timestamp, + self.creation_timestamp, + "The creation_timestamp is not as expected", ) self.assertEqual( - self.view.creation_timestamp, self.creation_timestamp, - 'The creation_timestamp is not as expected' - ) - self.assertEqual( - self.view.update_timestamp, self.creation_timestamp, - 'The update_timestamp is not as expected' + self.view.update_timestamp, + self.creation_timestamp, + "The update_timestamp is not as expected", ) def test_retrieve_view(self): @@ -164,20 +172,20 @@ def test_retrieve_view(self): # Assert self.new_views_num = View.objects.count() self.assertEqual( - self.old_views_num, self.new_views_num, - 'The number of objects in the DB should not change' + self.old_views_num, + self.new_views_num, + "The number of objects in the DB should not change", ) + self.assertEqual(self.view.name, self.view_name, "The name is not as expected") self.assertEqual( - self.view.name, self.view_name, - 'The name is not as expected' + self.view.creation_timestamp, + self.creation_timestamp, + "The creation_timestamp is not as expected", ) self.assertEqual( - self.view.creation_timestamp, self.creation_timestamp, - 'The creation_timestamp is not as expected' - ) - self.assertEqual( - self.view.update_timestamp, self.creation_timestamp, - 'The update_timestamp is not as expected' + self.view.update_timestamp, + self.creation_timestamp, + "The update_timestamp is not as expected", ) def test_update_view(self): @@ -188,31 +196,34 @@ def test_update_view(self): # Act self.update_timestamp = timezone.now() with freeze_time(self.update_timestamp): - self.view.name = 'This other name' + self.view.name = "This other name" self.view.save() # Assert self.view = View.objects.get(pk=self.view.pk) self.new_views_num = View.objects.count() self.assertEqual( - self.old_views_num, self.new_views_num, - 'The number of objects in the DB should not change' + self.old_views_num, + self.new_views_num, + "The number of objects in the DB should not change", ) self.assertEqual( - self.view.name, 'This other name', - 'The name is not as expected' + self.view.name, "This other name", "The name is not as expected" ) self.assertEqual( - self.view.creation_timestamp, self.creation_timestamp, - 'The creation_timestamp is not as expected' + self.view.creation_timestamp, + self.creation_timestamp, + "The creation_timestamp is not as expected", ) self.assertEqual( - self.view.update_timestamp, self.update_timestamp, - 'The update_timestamp is not as expected' + self.view.update_timestamp, + self.update_timestamp, + "The update_timestamp is not as expected", ) self.assertNotEqual( - self.view.creation_timestamp, self.view.update_timestamp, - 'The update_timestamp should be updated after updating the object' + self.view.creation_timestamp, + self.view.update_timestamp, + "The update_timestamp should be updated after updating the object", ) def test_delete_view(self): @@ -228,8 +239,9 @@ def test_delete_view(self): # Assert self.new_views_num = View.objects.count() self.assertEqual( - self.old_views_num - 1, self.new_views_num, - 'The number of objects in the DB have decreased by 1' + self.old_views_num - 1, + self.new_views_num, + "The number of objects in the DB have decreased by 1", ) with self.assertRaises(Exception): Workspace.objects.get(pk=self.view_pk) @@ -241,9 +253,9 @@ class WorkspaceViewModelTestCase(TestCase): def setUp(self): """Testcase setup.""" # Arrange - self.workspace_name = 'My Workspace' - self.view_name = 'My View' - self.workspace_view_name = 'My WorkspaceView' + self.workspace_name = "My Workspace" + self.view_name = "My View" + self.workspace_view_name = "My WorkspaceView" self.old_workspace_views_num = WorkspaceView.objects.count() self.creation_timestamp = timezone.now() with freeze_time(self.creation_timestamp): @@ -252,7 +264,7 @@ def setUp(self): self.workspace_view = WorkspaceView.objects.create( view_name=self.workspace_view_name, workspace=self.workspace, - view=self.view + view=self.view, ) def test_create_workspace_view(self): @@ -260,20 +272,24 @@ def test_create_workspace_view(self): # Assert self.new_workspace_views_num = WorkspaceView.objects.count() self.assertEqual( - self.old_workspace_views_num + 1, self.new_workspace_views_num, - 'There is not a new object in the database' + self.old_workspace_views_num + 1, + self.new_workspace_views_num, + "There is not a new object in the database", ) self.assertEqual( - self.workspace_view.view_name, self.workspace_view_name, - 'The name is not as expected' + self.workspace_view.view_name, + self.workspace_view_name, + "The name is not as expected", ) self.assertEqual( - self.workspace_view.creation_timestamp, self.creation_timestamp, - 'The creation_timestamp is not as expected' + self.workspace_view.creation_timestamp, + self.creation_timestamp, + "The creation_timestamp is not as expected", ) self.assertEqual( - self.workspace_view.update_timestamp, self.creation_timestamp, - 'The update_timestamp is not as expected' + self.workspace_view.update_timestamp, + self.creation_timestamp, + "The update_timestamp is not as expected", ) def test_retrieve_workspace_view(self): @@ -287,20 +303,24 @@ def test_retrieve_workspace_view(self): # Assert self.new_workspace_views_num = WorkspaceView.objects.count() self.assertEqual( - self.old_workspace_views_num, self.new_workspace_views_num, - 'The number of objects in the DB should not change' + self.old_workspace_views_num, + self.new_workspace_views_num, + "The number of objects in the DB should not change", ) self.assertEqual( - self.workspace_view.view_name, self.workspace_view_name, - 'The name is not as expected' + self.workspace_view.view_name, + self.workspace_view_name, + "The name is not as expected", ) self.assertEqual( - self.workspace_view.creation_timestamp, self.creation_timestamp, - 'The creation_timestamp is not as expected' + self.workspace_view.creation_timestamp, + self.creation_timestamp, + "The creation_timestamp is not as expected", ) self.assertEqual( - self.workspace_view.update_timestamp, self.creation_timestamp, - 'The update_timestamp is not as expected' + self.workspace_view.update_timestamp, + self.creation_timestamp, + "The update_timestamp is not as expected", ) def test_update_workspace_view(self): @@ -311,7 +331,7 @@ def test_update_workspace_view(self): # Act self.update_timestamp = timezone.now() with freeze_time(self.update_timestamp): - self.workspace_view.view_name = 'This other name' + self.workspace_view.view_name = "This other name" self.workspace_view.sort_value = self.new_sort_value self.workspace_view.save() @@ -319,28 +339,32 @@ def test_update_workspace_view(self): self.workspace_view = WorkspaceView.objects.get(pk=self.workspace_view.pk) self.new_workspace_views_num = WorkspaceView.objects.count() self.assertEqual( - self.old_workspace_views_num, self.new_workspace_views_num, - 'The number of objects in the DB should not change' + self.old_workspace_views_num, + self.new_workspace_views_num, + "The number of objects in the DB should not change", ) self.assertEqual( - self.workspace_view.view_name, 'This other name', - 'The name was not updated' + self.workspace_view.view_name, "This other name", "The name was not updated" ) self.assertEqual( - self.workspace_view.sort_value, self.new_sort_value, - 'The sort value was not updated' + self.workspace_view.sort_value, + self.new_sort_value, + "The sort value was not updated", ) self.assertEqual( - self.workspace_view.creation_timestamp, self.creation_timestamp, - 'The creation_timestamp is not as expected' + self.workspace_view.creation_timestamp, + self.creation_timestamp, + "The creation_timestamp is not as expected", ) self.assertEqual( - self.workspace_view.update_timestamp, self.update_timestamp, - 'The update_timestamp is not as expected' + self.workspace_view.update_timestamp, + self.update_timestamp, + "The update_timestamp is not as expected", ) self.assertNotEqual( - self.workspace_view.creation_timestamp, self.workspace_view.update_timestamp, - 'The update_timestamp should be updated after updating the object' + self.workspace_view.creation_timestamp, + self.workspace_view.update_timestamp, + "The update_timestamp should be updated after updating the object", ) def test_delete_workspace_view(self): @@ -356,8 +380,9 @@ def test_delete_workspace_view(self): # Assert self.new_workspace_views_num = WorkspaceView.objects.count() self.assertEqual( - self.old_workspace_views_num - 1, self.new_workspace_views_num, - 'The number of objects in the DB have decreased by 1' + self.old_workspace_views_num - 1, + self.new_workspace_views_num, + "The number of objects in the DB have decreased by 1", ) with self.assertRaises(Exception): Workspace.objects.get(pk=self.workspace_view_pk) @@ -372,14 +397,14 @@ def setUp(self): self.setup_timestamp = timezone.now() with freeze_time(self.setup_timestamp): self.workspaces = [ - Workspace.objects.create(name='My Workspace 1'), - Workspace.objects.create(name='My Workspace 2'), - Workspace.objects.create(name='My Workspace 3'), + Workspace.objects.create(name="My Workspace 1"), + Workspace.objects.create(name="My Workspace 2"), + Workspace.objects.create(name="My Workspace 3"), ] self.views = [ - View.objects.create(name='My View 1'), - View.objects.create(name='My View 2'), - View.objects.create(name='My View 3'), + View.objects.create(name="My View 1"), + View.objects.create(name="My View 2"), + View.objects.create(name="My View 3"), ] def test_add_and_get_views_to_workspace(self): @@ -394,8 +419,16 @@ def test_add_and_get_views_to_workspace(self): retrieved_views = list(workspace.views.all()) retrieved_sorted_views = list(workspace.get_sorted_views()) # Assert - self.assertEqual(set(retrieved_views), set(self.views), 'The views were not assigned to the workspace') - self.assertEqual(retrieved_sorted_views, sorted_views, 'The views were not sorted properly workspace') + self.assertEqual( + set(retrieved_views), + set(self.views), + "The views were not assigned to the workspace", + ) + self.assertEqual( + retrieved_sorted_views, + sorted_views, + "The views were not sorted properly workspace", + ) def test_get_workspaces_from_a_view(self): """Test that a View can retrieve its Workspaces from the DB.""" @@ -410,4 +443,8 @@ def test_get_workspaces_from_a_view(self): # Act retrieved_workspaces = set(view.workspaces.all()) # Assert - self.assertEqual(retrieved_workspaces, set(workspaces), 'The workspaces from the view are not as expected') + self.assertEqual( + retrieved_workspaces, + set(workspaces), + "The workspaces from the view are not as expected", + ) diff --git a/manager/ui_framework/tests/utils.py b/manager/ui_framework/tests/utils.py index 0f4a336f..ae25b91a 100644 --- a/manager/ui_framework/tests/utils.py +++ b/manager/ui_framework/tests/utils.py @@ -13,27 +13,27 @@ def get_dict(obj): """Return a dictionary with the fields of a given object.""" if type(obj) == Workspace: return { - 'id': obj.id, - 'name': obj.name, - 'creation_timestamp': obj.creation_timestamp, - 'update_timestamp': obj.update_timestamp, + "id": obj.id, + "name": obj.name, + "creation_timestamp": obj.creation_timestamp, + "update_timestamp": obj.update_timestamp, } if type(obj) == View: return { - 'id': obj.id, - 'name': obj.name, - 'data': json.dumps(obj.data), - 'creation_timestamp': obj.creation_timestamp, - 'update_timestamp': obj.update_timestamp, + "id": obj.id, + "name": obj.name, + "data": json.dumps(obj.data), + "creation_timestamp": obj.creation_timestamp, + "update_timestamp": obj.update_timestamp, } if type(obj) == WorkspaceView: return { - 'id': obj.id, - 'view_name': obj.view_name, - 'view': obj.view.pk, - 'workspace': obj.workspace.pk, - 'creation_timestamp': obj.creation_timestamp, - 'update_timestamp': obj.update_timestamp, + "id": obj.id, + "view_name": obj.view_name, + "view": obj.view.pk, + "workspace": obj.workspace.pk, + "creation_timestamp": obj.creation_timestamp, + "update_timestamp": obj.update_timestamp, } @@ -53,71 +53,88 @@ def setUp(self): # Data to be used to create views and workspaces self.views_data = [ { - 'name': 'My View 0', - 'data': json.dumps({"data_name": "My View 0"}), + "name": "My View 0", + "data": json.dumps({"data_name": "My View 0"}), }, { - 'name': 'My View 1', - 'data': json.dumps({"data_name": "My View 1"}), + "name": "My View 1", + "data": json.dumps({"data_name": "My View 1"}), }, { - 'name': 'My View 2', - 'data': json.dumps({"data_name": "My View 2"}), + "name": "My View 2", + "data": json.dumps({"data_name": "My View 2"}), }, { - 'name': 'My View 3', - 'data': json.dumps({"data_name": "My View 3"}), - } + "name": "My View 3", + "data": json.dumps({"data_name": "My View 3"}), + }, ] self.workspaces_data = [ - {'name': 'My Workspace 0'}, - {'name': 'My Workspace 1'}, - {'name': 'My Workspace 2'}, + {"name": "My Workspace 0"}, + {"name": "My Workspace 1"}, + {"name": "My Workspace 2"}, ] self.views = [] self.workspaces = [] self.workspace_views_data = [] - default_thumbnail = settings.MEDIA_URL + View._meta.get_field('thumbnail').get_default() + default_thumbnail = ( + settings.MEDIA_URL + View._meta.get_field("thumbnail").get_default() + ) # Create views, store them in self.views and add auto-generated fields to self.views_data for i in range(0, len(self.views_data)): view = View.objects.create(**self.views_data[i]) - self.views_data[i]['id'] = view.id - self.views_data[i]['thumbnail'] = default_thumbnail + self.views_data[i]["id"] = view.id + self.views_data[i]["thumbnail"] = default_thumbnail self.views.append(view) # Create views, store them in self.views and add auto-generated fields to self.views_data for i in range(0, len(self.workspaces_data)): workspace = Workspace.objects.create(**self.workspaces_data[i]) - self.workspaces_data[i]['id'] = workspace.id - self.workspaces_data[i]['creation_timestamp'] = self.setup_ts_str - self.workspaces_data[i]['update_timestamp'] = self.setup_ts_str - self.workspaces_data[i]['views'] = [self.views[i].pk, self.views[i + 1].pk] + self.workspaces_data[i]["id"] = workspace.id + self.workspaces_data[i]["creation_timestamp"] = self.setup_ts_str + self.workspaces_data[i]["update_timestamp"] = self.setup_ts_str + self.workspaces_data[i]["views"] = [ + self.views[i].pk, + self.views[i + 1].pk, + ] self.workspaces.append(workspace) # Add view[i] - self.workspaces[i].views.add(self.views[i], through_defaults={'view_name': 'v{}'.format(i)}) - aux = WorkspaceView.objects.get(workspace=self.workspaces[i], view=self.views[i]) - self.workspace_views_data.append({ - 'id': aux.id, - 'creation_timestamp': self.setup_ts_str, - 'update_timestamp': self.setup_ts_str, - 'view_name': 'v{}'.format(i), - 'sort_value': 0, - 'view': self.views[i].pk, - 'workspace': self.workspaces[i].pk, - }) + self.workspaces[i].views.add( + self.views[i], through_defaults={"view_name": "v{}".format(i)} + ) + aux = WorkspaceView.objects.get( + workspace=self.workspaces[i], view=self.views[i] + ) + self.workspace_views_data.append( + { + "id": aux.id, + "creation_timestamp": self.setup_ts_str, + "update_timestamp": self.setup_ts_str, + "view_name": "v{}".format(i), + "sort_value": 0, + "view": self.views[i].pk, + "workspace": self.workspaces[i].pk, + } + ) # Add view[i + 1] - self.workspaces[i].views.add(self.views[i + 1], through_defaults={'view_name': 'v{}'.format(i)}) - aux = WorkspaceView.objects.get(workspace=self.workspaces[i], view=self.views[i + 1]) - self.workspace_views_data.append({ - 'id': aux.id, - 'creation_timestamp': self.setup_ts_str, - 'update_timestamp': self.setup_ts_str, - 'view_name': 'v{}'.format(i), - 'sort_value': 0, - 'view': self.views[i + 1].pk, - 'workspace': self.workspaces[i].pk, - }) + self.workspaces[i].views.add( + self.views[i + 1], through_defaults={"view_name": "v{}".format(i)} + ) + aux = WorkspaceView.objects.get( + workspace=self.workspaces[i], view=self.views[i + 1] + ) + self.workspace_views_data.append( + { + "id": aux.id, + "creation_timestamp": self.setup_ts_str, + "update_timestamp": self.setup_ts_str, + "view_name": "v{}".format(i), + "sort_value": 0, + "view": self.views[i + 1].pk, + "workspace": self.workspaces[i].pk, + } + ) # Client to test the API self.client = APIClient() @@ -125,39 +142,39 @@ def setUp(self): # Models to test: self.cases = [ { - 'class': Workspace, - 'key': 'workspace', - 'old_count': Workspace.objects.count(), - 'new_data': { - 'name': 'My new Workspace', + "class": Workspace, + "key": "workspace", + "old_count": Workspace.objects.count(), + "new_data": { + "name": "My new Workspace", }, - 'current_data': self.workspaces_data, - 'list_data': self.workspaces_data, - 'selected_id': self.workspaces_data[0]['id'], + "current_data": self.workspaces_data, + "list_data": self.workspaces_data, + "selected_id": self.workspaces_data[0]["id"], }, { - 'class': View, - 'key': 'view', - 'old_count': View.objects.count(), - 'new_data': { - 'name': 'My new View', - 'data': json.dumps({"dummy_key": "Dummy_value"}), + "class": View, + "key": "view", + "old_count": View.objects.count(), + "new_data": { + "name": "My new View", + "data": json.dumps({"dummy_key": "Dummy_value"}), }, - 'current_data': self.views_data, - 'selected_id': self.views_data[0]['id'], + "current_data": self.views_data, + "selected_id": self.views_data[0]["id"], }, { - 'class': WorkspaceView, - 'key': 'workspaceview', - 'old_count': WorkspaceView.objects.count(), - 'new_data': { - 'view_name': 'New view_name', + "class": WorkspaceView, + "key": "workspaceview", + "old_count": WorkspaceView.objects.count(), + "new_data": { + "view_name": "New view_name", # 'sort_value': 1, - 'view': self.views_data[3]['id'], - 'workspace': self.workspaces_data[0]['id'], + "view": self.views_data[3]["id"], + "workspace": self.workspaces_data[0]["id"], }, - 'current_data': self.workspace_views_data, - 'list_data': self.workspace_views_data, - 'selected_id': self.workspace_views_data[0]['id'], + "current_data": self.workspace_views_data, + "list_data": self.workspace_views_data, + "selected_id": self.workspace_views_data[0]["id"], }, ] diff --git a/manager/ui_framework/urls.py b/manager/ui_framework/urls.py index 7702c2f4..a69f7a74 100644 --- a/manager/ui_framework/urls.py +++ b/manager/ui_framework/urls.py @@ -19,7 +19,7 @@ from ui_framework.views import WorkspaceViewSet, ViewViewSet, WorkspaceViewViewSet router = DefaultRouter() -router.register('workspaces', WorkspaceViewSet) -router.register('views', ViewViewSet) -router.register('workspaceviews', WorkspaceViewViewSet) +router.register("workspaces", WorkspaceViewSet) +router.register("views", ViewViewSet) +router.register("workspaceviews", WorkspaceViewViewSet) urlpatterns = router.urls
                • ui_framework.urls (module)
                • ui_framework.views (module) @@ -986,9 +1142,19 @@

                  U

                • UnauthorizedCrudTestCase (class in ui_framework.tests.tests_api)
                • -
                • update_timestamp (ui_framework.models.BaseModel attribute) +
                • update_timestamp (api.models.BaseModel attribute) + +
                • +
                • user (api.models.ConfigFile attribute) + +
                • +
                • user_id (api.models.ConfigFile attribute)
                • UserPermissionsSerializer (class in api.serializers)
                • @@ -1005,6 +1171,8 @@

                  V

                • validate() (api.schema_validator.DefaultingValidator method)
                • validate_config_schema() (in module api.views) +
                • +
                • validate_file_extension() (api.models.ConfigFile method)
                • validate_token() (in module api.views)
                • diff --git a/docs/html/modules/how_it_works.html b/docs/html/modules/how_it_works.html index 3909ce37..42ace041 100644 --- a/docs/html/modules/how_it_works.html +++ b/docs/html/modules/how_it_works.html @@ -215,7 +215,7 @@

                  3.2. Example3.3. Code organization

                  Currently the application is divided in the following modules and files:

                    -
                  • api: This module contains the API Django app, which defines the models and API endpoints for authentication (Auth API) and Commander (Commander API) APIs. For more details please refer to the ApiDoc section

                  • +
                  • api: This module contains the API Django app, which defines the models and API endpoints for authentication (Auth API), Commander (Commander API), ConfigFile (ConfigFile API) and EmergencyContact (EmergencyContact API) APIs. For more details please refer to the ApiDoc section

                  • ui_framework: This module contains the UI Framework Django app, which defines the models and API endpoints for the UI Framework views (UI Framework API) API. For more details please refer to the ApiDoc section

                  • subscription: This module contains the Django app that defines the consumers that handle the websocket communication.

                  • manager: This module contains basic Django configuration files, such as urls and channels routing, etc.

                  • diff --git a/docs/html/modules/how_to_use_it.html b/docs/html/modules/how_to_use_it.html index 6758e7da..1e7e94cf 100644 --- a/docs/html/modules/how_to_use_it.html +++ b/docs/html/modules/how_to_use_it.html @@ -278,7 +278,7 @@

                    2.1.1. Request token

                    2.1.2. Validate token

                    Validates a given authorization token, passed through HTTP Headers. -Returns a confirmation of validity, user data, permissions, server_time and (optionally) the LOVE configuraiton file. +Returns a confirmation of validity, user data, permissions, server_time and (optionally) the LOVE configuration file. If the no_config flag is added to the end of the URL, then the LOVE config files is not read and the corresponding value is returned as null

                    • Url: <IP>/manager/api/validate-token/ or <IP>/manager/api/validate-token/no_config/

                    • @@ -332,7 +332,7 @@

                      2.1.2. Validate token

                      2.1.3. Swap token

                      Validates a given authorization token, passed through HTTP Headers. -Returns a confirmation of validity, user data, permissions, server_time and (optionally) the LOVE configuraiton file. +Returns a confirmation of validity, user data, permissions, server_time and (optionally) the LOVE configuration file. If the no_config flag is added to the end of the URL, then the LOVE config files is not read and the corresponding value is returned as null

                      • Url: <IP>/manager/api/swap-token/ or <IP>/manager/api/swap-token/no_config/

                      • diff --git a/docs/html/objects.inv b/docs/html/objects.inv index addfba4a..6bf88ebc 100644 Binary files a/docs/html/objects.inv and b/docs/html/objects.inv differ diff --git a/docs/html/py-modindex.html b/docs/html/py-modindex.html index 3fc48208..eb202318 100644 --- a/docs/html/py-modindex.html +++ b/docs/html/py-modindex.html @@ -218,6 +218,11 @@

                        Python Module Index

                    api.tests.test_commander
                    + api.tests.test_lovecsc +
                    @@ -231,7 +236,12 @@

                Python Module Index

                    - api.tests.tests_config + api.tests.tests_configfile +
                    + api.tests.tests_emergencycontact