- - 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
|
|
- |
+ |
+
+ |
+
+ api.tests.test_lovecsc |
+ |
|
@@ -231,7 +236,12 @@ Python Module Index
|
|
- api.tests.tests_config |
+ api.tests.tests_configfile |
+ |
+
+ |
+
+ api.tests.tests_emergencycontact |
|
|
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
|