diff --git a/.dockerignore b/.dockerignore index d5e4f15f..51eb9317 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1 +1,2 @@ -**/__pycache__ \ No newline at end of file +**/__pycache__ +**/db_data diff --git a/.gitignore b/.gitignore index d1807c03..70d643d0 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,10 @@ webpack-stats.* *.sqlite3 **/__pycache__ **/.DS_Store +**/db_data + +# preserve folder structure with dummy gitinclude file +manager/media/thumbnails/** +!manager/media/thumbnails/.gitinclude +manager/ui_framework/tests/media/thumbnails/** +!manager/ui_framework/tests/media/thumbnails/.gitinclude diff --git a/Dockerfile b/Dockerfile index bb1a75c8..3b89ba6b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.6.7-stretch +FROM python:3.8.2-buster # Install required packages RUN apt-get update && \ @@ -22,12 +22,14 @@ RUN python manage.py collectstatic --noinput # Expose static files and port VOLUME /usr/src/love/manager/static +VOLUME /usr/src/love/manager/media EXPOSE 8000 # Set env variables for runtime (to be replaced in docker-cpomse files) ENV ADMIN_USER_PASS=test ENV USER_USER_PASS=test ENV CMD_USER_PASS=test +ENV NO_DEBUG=True # Run daphne server in runtime CMD ["./runserver.sh"] diff --git a/Dockerfile-dev b/Dockerfile-dev index b14c852a..ae00917b 100644 --- a/Dockerfile-dev +++ b/Dockerfile-dev @@ -1,4 +1,4 @@ -FROM python:3.6.7-stretch +FROM python:3.8.2-buster # Install required packages RUN apt-get update && \ @@ -10,7 +10,7 @@ RUN apt-get update && \ rm -rf /var/lib/apt/lists/* # Set workdir and install python requirements -WORKDIR /usr/src/love +WORKDIR /usr/src/love/manager COPY manager/requirements.txt . RUN pip install -r requirements.txt diff --git a/Jenkinsfile b/Jenkinsfile index b437d0e0..3f5b46b0 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -37,22 +37,22 @@ pipeline { } } } - stage("Test Docker Image") { - when { - anyOf { - branch "master" - branch "develop" - branch "bugfix/*" - branch "hotfix/*" - branch "release/*" - } - } - steps { - script { - sh "docker run ${dockerImageName} pytest" - } - } - } + // stage("Test Docker Image") { + // when { + // anyOf { + // branch "master" + // branch "develop" + // branch "bugfix/*" + // branch "hotfix/*" + // branch "release/*" + // } + // } + // steps { + // script { + // sh "docker run ${dockerImageName} pytest" + // } + // } + // } stage("Push Docker image") { when { anyOf { diff --git a/README.md b/README.md index 18f3c6c4..45ab6f0a 100644 --- a/README.md +++ b/README.md @@ -12,13 +12,21 @@ All these variables are initialized with default variables defined in :code:`.en * `ADMIN_USER_PASS`: password for the default `admin` user, which has every permission. * `USER_USER_PASS`: password for the default `user` user, which has readonly permissions and cannot execute commands. * `CMD_USER_PASS`: password for the default `cmd` user, which has readonly permissions but can execute commands. -* `LOVE_MANAGER_REDIS_HOST`: the location of the redis host that implements the `Channels Layer`. -* `REDIS_PASS`: the password that the LOVE-manager needs tio use to connect with `redis`. +* `REDIS_HOST`: the location of the redis host that implements the `Channels Layer`. +* `REDIS_PASS`: the password that the LOVE-manager needs to use to connect with `redis`. * `PROCESS_CONNECTION_PASS`: the password that the LOVE-producer will use to establish a websocket connection with the LOVE-manager. +* `DB_ENGINE`: describe which database engine should be used. If its value is `postgresql` Postgres will be used, otherwise it will use Sqlite3. +* `DB_NAME`: defines the name of the Database. Only used if `DB_ENGINE=postgresql`. +* `DB_USER`: defines the user of the Database. Only used if `DB_ENGINE=postgresql`. +* `DB_PASS`: defines the password of the Database. Only used if `DB_ENGINE=postgresql`. +* `DB_HOST`: defines the host of the Database. Only used if `DB_ENGINE=postgresql`. +* `DB_PORT`: defines the port of the Database. Only used if `DB_ENGINE=postgresql`. +* `NO_DEBUG`: defines wether or not the LOVE-.manager will be run using Django's debug mode. If the variable is defined, then Debug mode will be off. +* `SECRET_KEY`: overrides Django's SECRET_KEY, if not defined the default value (public in this repo) will be used. * `AUTH_LDAP_SERVER_URI`: (deprecated) the location of the LDAP authentication server. No LDAP server is used if this variable is empty # Local load for development -We provide a docker image and a docker-compose file in order to load the LOVE-manager locally for development purposes, i.e. run tests and build documentation. +We provide a docker image and a docker-compose file in order to load the LOVE-manager with a Postgres database locally, for development purposes, such as run tests and build documentation. This docker-compose does not copy the code into the image, but instead it mounts the repository inside the image, this way you can edit the code from outside the docker container with no need to rebuild or restart. diff --git a/docker-compose.yml b/docker-compose.yml index 0bfca3a8..f1d0ca10 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,14 +2,37 @@ version: "3.7" services: + database: + container_name: manager-postgres-local + image: postgres:12.0 + environment: + - POSTGRES_DB=postgres + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=postgres + volumes: + - ./db_data:/var/lib/postgresql/data + restart: always + manager: container_name: manager-local build: context: . dockerfile: Dockerfile-dev image: love-manager-image-mount + environment: + - DB_ENGINE=postgresql + - DB_NAME=postgres + - DB_USER=postgres + - DB_PASS=postgres + - DB_HOST=database + - DB_PORT=5432 + ports: + - "8000:8000" + depends_on: + - database + restart: always volumes: - .:/usr/src/love - command: "" + command: "sleep infinity" stdin_open: true tty: true diff --git a/docs/doctrees/apidoc/api.doctree b/docs/doctrees/apidoc/api.doctree index 78cf3d59..95730cf0 100644 Binary files a/docs/doctrees/apidoc/api.doctree and b/docs/doctrees/apidoc/api.doctree differ diff --git a/docs/doctrees/apidoc/api.tests.doctree b/docs/doctrees/apidoc/api.tests.doctree index edb52cab..14c043b3 100644 Binary files a/docs/doctrees/apidoc/api.tests.doctree and b/docs/doctrees/apidoc/api.tests.doctree differ diff --git a/docs/doctrees/apidoc/manager.doctree b/docs/doctrees/apidoc/manager.doctree index d84fcead..2005d564 100644 Binary files a/docs/doctrees/apidoc/manager.doctree and b/docs/doctrees/apidoc/manager.doctree differ diff --git a/docs/doctrees/apidoc/modules.doctree b/docs/doctrees/apidoc/modules.doctree index c39092bd..1c701749 100644 Binary files a/docs/doctrees/apidoc/modules.doctree and b/docs/doctrees/apidoc/modules.doctree differ diff --git a/docs/doctrees/apidoc/ui_framework.doctree b/docs/doctrees/apidoc/ui_framework.doctree new file mode 100644 index 00000000..90b428d7 Binary files /dev/null and b/docs/doctrees/apidoc/ui_framework.doctree differ diff --git a/docs/doctrees/apidoc/ui_framework.tests.doctree b/docs/doctrees/apidoc/ui_framework.tests.doctree new file mode 100644 index 00000000..dc09f68d Binary files /dev/null and b/docs/doctrees/apidoc/ui_framework.tests.doctree differ diff --git a/docs/doctrees/environment.pickle b/docs/doctrees/environment.pickle index b5237180..61b2411a 100644 Binary files a/docs/doctrees/environment.pickle and b/docs/doctrees/environment.pickle differ diff --git a/docs/doctrees/modules/how_to_use_it.doctree b/docs/doctrees/modules/how_to_use_it.doctree index 2395551b..1079cbc6 100644 Binary files a/docs/doctrees/modules/how_to_use_it.doctree and b/docs/doctrees/modules/how_to_use_it.doctree differ diff --git a/docs/doctrees/modules/readme_link.doctree b/docs/doctrees/modules/readme_link.doctree index 4e94af23..f725f37d 100644 Binary files a/docs/doctrees/modules/readme_link.doctree and b/docs/doctrees/modules/readme_link.doctree differ diff --git a/docs/html/.buildinfo b/docs/html/.buildinfo index 561da840..a03ced75 100644 --- a/docs/html/.buildinfo +++ b/docs/html/.buildinfo @@ -1,4 +1,4 @@ # Sphinx build info version 1 # This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. -config: 953d8bcde9ceff8398c74730e916263b +config: 0e75181af2d457cfd432dc4ec2676b09 tags: 645f666f9bcd5a90fca523b33c5a78b7 diff --git a/docs/html/_sources/apidoc/modules.rst.txt b/docs/html/_sources/apidoc/modules.rst.txt index ea95ba43..11acd33a 100644 --- a/docs/html/_sources/apidoc/modules.rst.txt +++ b/docs/html/_sources/apidoc/modules.rst.txt @@ -9,3 +9,4 @@ These are the ApiDocs of the project. manage manager subscription + ui_framework diff --git a/docs/html/_sources/apidoc/ui_framework.rst.txt b/docs/html/_sources/apidoc/ui_framework.rst.txt new file mode 100644 index 00000000..4c8b65ab --- /dev/null +++ b/docs/html/_sources/apidoc/ui_framework.rst.txt @@ -0,0 +1,69 @@ +ui\_framework package +===================== + +Subpackages +----------- + +.. toctree:: + + ui_framework.tests + +Submodules +---------- + +ui\_framework.admin module +-------------------------- + +.. automodule:: ui_framework.admin + :members: + :undoc-members: + :show-inheritance: + +ui\_framework.apps module +------------------------- + +.. automodule:: ui_framework.apps + :members: + :undoc-members: + :show-inheritance: + +ui\_framework.models module +--------------------------- + +.. automodule:: ui_framework.models + :members: + :undoc-members: + :show-inheritance: + +ui\_framework.serializers module +-------------------------------- + +.. automodule:: ui_framework.serializers + :members: + :undoc-members: + :show-inheritance: + +ui\_framework.urls module +------------------------- + +.. automodule:: ui_framework.urls + :members: + :undoc-members: + :show-inheritance: + +ui\_framework.views module +-------------------------- + +.. automodule:: ui_framework.views + :members: + :undoc-members: + :show-inheritance: + + +Module contents +--------------- + +.. automodule:: ui_framework + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/html/_sources/apidoc/ui_framework.tests.rst.txt b/docs/html/_sources/apidoc/ui_framework.tests.rst.txt new file mode 100644 index 00000000..1ecf78ef --- /dev/null +++ b/docs/html/_sources/apidoc/ui_framework.tests.rst.txt @@ -0,0 +1,38 @@ +ui\_framework.tests package +=========================== + +Submodules +---------- + +ui\_framework.tests.tests\_api module +------------------------------------- + +.. automodule:: ui_framework.tests.tests_api + :members: + :undoc-members: + :show-inheritance: + +ui\_framework.tests.tests\_models module +---------------------------------------- + +.. automodule:: ui_framework.tests.tests_models + :members: + :undoc-members: + :show-inheritance: + +ui\_framework.tests.utils module +-------------------------------- + +.. automodule:: ui_framework.tests.utils + :members: + :undoc-members: + :show-inheritance: + + +Module contents +--------------- + +.. automodule:: ui_framework.tests + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/html/_sources/modules/how_to_use_it.rst.txt b/docs/html/_sources/modules/how_to_use_it.rst.txt index c5b5aaf5..2c142b22 100644 --- a/docs/html/_sources/modules/how_to_use_it.rst.txt +++ b/docs/html/_sources/modules/how_to_use_it.rst.txt @@ -213,3 +213,20 @@ Specifying the variables necessary to subscribe a to a group in a JSON message, } Where pairs :code:`param1` and :code:`value1` represent the parameters (name and value) to be passed to the command. + + +UI Framework +============ + +The UI Framework backend is composed of 3 models: + + - **Workspace:** represents a workspace, composed by different views + - **View:** represents a view, all the data of the view is contained in JSON format in the :code:`data` field of the view + - **WorkspaceView:** relates a Workspace and a View, it is the intermediary table of the many-to-many relationship between Workspace and View. + +Currently the API provides a standard REST api for these models. +For more info you can either: + + - Use the browsable API available in: :code:`/manager/ui_framework/` + - See the apidoc in Swagger format, available in: :code:`/manager/apidoc/swagger/` + - See the apidoc in ReDoc format, available in: :code:`/manager/apidoc/redoc/` diff --git a/docs/html/_static/basic.css b/docs/html/_static/basic.css index c41d718e..ea6972d5 100644 --- a/docs/html/_static/basic.css +++ b/docs/html/_static/basic.css @@ -520,14 +520,15 @@ dl.citation > dd:after { } dl.field-list { - display: flex; - flex-wrap: wrap; + display: grid; + grid-template-columns: fit-content(30%) auto; } dl.field-list > dt { - flex-basis: 20%; font-weight: bold; word-break: break-word; + padding-left: 0.5em; + padding-right: 5px; } dl.field-list > dt:after { @@ -535,8 +536,8 @@ dl.field-list > dt:after { } dl.field-list > dd { - flex-basis: 70%; - padding-left: 1em; + padding-left: 0.5em; + margin-top: 0em; margin-left: 0em; margin-bottom: 0em; } diff --git a/docs/html/_static/jquery-3.2.1.js b/docs/html/_static/jquery-3.4.1.js similarity index 89% rename from docs/html/_static/jquery-3.2.1.js rename to docs/html/_static/jquery-3.4.1.js index d2d8ca47..773ad95c 100644 --- a/docs/html/_static/jquery-3.2.1.js +++ b/docs/html/_static/jquery-3.4.1.js @@ -1,5 +1,5 @@ /*! - * jQuery JavaScript Library v3.2.1 + * jQuery JavaScript Library v3.4.1 * https://jquery.com/ * * Includes Sizzle.js @@ -9,7 +9,7 @@ * Released under the MIT license * https://jquery.org/license * - * Date: 2017-03-20T18:59Z + * Date: 2019-05-01T21:04Z */ ( function( global, factory ) { @@ -71,16 +71,70 @@ var ObjectFunctionString = fnToString.call( Object ); var support = {}; +var isFunction = function isFunction( obj ) { + // Support: Chrome <=57, Firefox <=52 + // In some browsers, typeof returns "function" for HTML elements + // (i.e., `typeof document.createElement( "object" ) === "function"`). + // We don't want to classify *any* DOM node as a function. + return typeof obj === "function" && typeof obj.nodeType !== "number"; + }; - function DOMEval( code, doc ) { + +var isWindow = function isWindow( obj ) { + return obj != null && obj === obj.window; + }; + + + + + var preservedScriptAttributes = { + type: true, + src: true, + nonce: true, + noModule: true + }; + + function DOMEval( code, node, doc ) { doc = doc || document; - var script = doc.createElement( "script" ); + var i, val, + script = doc.createElement( "script" ); script.text = code; + if ( node ) { + for ( i in preservedScriptAttributes ) { + + // Support: Firefox 64+, Edge 18+ + // Some browsers don't support the "nonce" property on scripts. + // On the other hand, just using `getAttribute` is not enough as + // the `nonce` attribute is reset to an empty string whenever it + // becomes browsing-context connected. + // See https://github.com/whatwg/html/issues/2369 + // See https://html.spec.whatwg.org/#nonce-attributes + // The `node.getAttribute` check was added for the sake of + // `jQuery.globalEval` so that it can fake a nonce-containing node + // via an object. + val = node[ i ] || node.getAttribute && node.getAttribute( i ); + if ( val ) { + script.setAttribute( i, val ); + } + } + } doc.head.appendChild( script ).parentNode.removeChild( script ); } + + +function toType( obj ) { + if ( obj == null ) { + return obj + ""; + } + + // Support: Android <=2.3 only (functionish RegExp) + return typeof obj === "object" || typeof obj === "function" ? + class2type[ toString.call( obj ) ] || "object" : + typeof obj; +} /* global Symbol */ // Defining this global in .eslintrc.json would create a danger of using the global // unguarded in another place, it seems safer to define global only for this module @@ -88,7 +142,7 @@ var support = {}; var - version = "3.2.1", + version = "3.4.1", // Define a local copy of jQuery jQuery = function( selector, context ) { @@ -100,16 +154,7 @@ var // Support: Android <=4.0 only // Make sure we trim BOM and NBSP - rtrim = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, - - // Matches dashed string for camelizing - rmsPrefix = /^-ms-/, - rdashAlpha = /-([a-z])/g, - - // Used by jQuery.camelCase as callback to replace() - fcamelCase = function( all, letter ) { - return letter.toUpperCase(); - }; + rtrim = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g; jQuery.fn = jQuery.prototype = { @@ -209,7 +254,7 @@ jQuery.extend = jQuery.fn.extend = function() { } // Handle case when target is a string or something (possible in deep copy) - if ( typeof target !== "object" && !jQuery.isFunction( target ) ) { + if ( typeof target !== "object" && !isFunction( target ) ) { target = {}; } @@ -226,25 +271,28 @@ jQuery.extend = jQuery.fn.extend = function() { // Extend the base object for ( name in options ) { - src = target[ name ]; copy = options[ name ]; + // Prevent Object.prototype pollution // Prevent never-ending loop - if ( target === copy ) { + if ( name === "__proto__" || target === copy ) { continue; } // Recurse if we're merging plain objects or arrays if ( deep && copy && ( jQuery.isPlainObject( copy ) || ( copyIsArray = Array.isArray( copy ) ) ) ) { + src = target[ name ]; - if ( copyIsArray ) { - copyIsArray = false; - clone = src && Array.isArray( src ) ? src : []; - + // Ensure proper type for the source value + if ( copyIsArray && !Array.isArray( src ) ) { + clone = []; + } else if ( !copyIsArray && !jQuery.isPlainObject( src ) ) { + clone = {}; } else { - clone = src && jQuery.isPlainObject( src ) ? src : {}; + clone = src; } + copyIsArray = false; // Never move original objects, clone them target[ name ] = jQuery.extend( deep, clone, copy ); @@ -275,28 +323,6 @@ jQuery.extend( { noop: function() {}, - isFunction: function( obj ) { - return jQuery.type( obj ) === "function"; - }, - - isWindow: function( obj ) { - return obj != null && obj === obj.window; - }, - - isNumeric: function( obj ) { - - // As of jQuery 3.0, isNumeric is limited to - // strings and numbers (primitives or objects) - // that can be coerced to finite numbers (gh-2662) - var type = jQuery.type( obj ); - return ( type === "number" || type === "string" ) && - - // parseFloat NaNs numeric-cast false positives ("") - // ...but misinterprets leading-number strings, particularly hex literals ("0x...") - // subtraction forces infinities to NaN - !isNaN( obj - parseFloat( obj ) ); - }, - isPlainObject: function( obj ) { var proto, Ctor; @@ -319,9 +345,6 @@ jQuery.extend( { }, isEmptyObject: function( obj ) { - - /* eslint-disable no-unused-vars */ - // See https://github.com/eslint/eslint/issues/6125 var name; for ( name in obj ) { @@ -330,27 +353,9 @@ jQuery.extend( { return true; }, - type: function( obj ) { - if ( obj == null ) { - return obj + ""; - } - - // Support: Android <=2.3 only (functionish RegExp) - return typeof obj === "object" || typeof obj === "function" ? - class2type[ toString.call( obj ) ] || "object" : - typeof obj; - }, - // Evaluates a script in a global context - globalEval: function( code ) { - DOMEval( code ); - }, - - // Convert dashed to camelCase; used by the css and data modules - // Support: IE <=9 - 11, Edge 12 - 13 - // Microsoft forgot to hump their vendor prefix (#9572) - camelCase: function( string ) { - return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase ); + globalEval: function( code, options ) { + DOMEval( code, { nonce: options && options.nonce } ); }, each: function( obj, callback ) { @@ -473,37 +478,6 @@ jQuery.extend( { // A global GUID counter for objects guid: 1, - // Bind a function to a context, optionally partially applying any - // arguments. - proxy: function( fn, context ) { - var tmp, args, proxy; - - if ( typeof context === "string" ) { - tmp = fn[ context ]; - context = fn; - fn = tmp; - } - - // Quick check to determine if target is callable, in the spec - // this throws a TypeError, but we will just return undefined. - if ( !jQuery.isFunction( fn ) ) { - return undefined; - } - - // Simulated bind - args = slice.call( arguments, 2 ); - proxy = function() { - return fn.apply( context || this, args.concat( slice.call( arguments ) ) ); - }; - - // Set the guid of unique handler to the same of original handler, so it can be removed - proxy.guid = fn.guid = fn.guid || jQuery.guid++; - - return proxy; - }, - - now: Date.now, - // jQuery.support is not used in Core but other projects attach their // properties to it so it needs to exist. support: support @@ -526,9 +500,9 @@ function isArrayLike( obj ) { // hasOwn isn't used here due to false negatives // regarding Nodelist length in IE var length = !!obj && "length" in obj && obj.length, - type = jQuery.type( obj ); + type = toType( obj ); - if ( type === "function" || jQuery.isWindow( obj ) ) { + if ( isFunction( obj ) || isWindow( obj ) ) { return false; } @@ -537,14 +511,14 @@ function isArrayLike( obj ) { } var Sizzle = /*! - * Sizzle CSS Selector Engine v2.3.3 + * Sizzle CSS Selector Engine v2.3.4 * https://sizzlejs.com/ * - * Copyright jQuery Foundation and other contributors + * Copyright JS Foundation and other contributors * Released under the MIT license - * http://jquery.org/license + * https://js.foundation/ * - * Date: 2016-08-08 + * Date: 2019-04-08 */ (function( window ) { @@ -578,6 +552,7 @@ var i, classCache = createCache(), tokenCache = createCache(), compilerCache = createCache(), + nonnativeSelectorCache = createCache(), sortOrder = function( a, b ) { if ( a === b ) { hasDuplicate = true; @@ -639,8 +614,7 @@ var i, rcomma = new RegExp( "^" + whitespace + "*," + whitespace + "*" ), rcombinators = new RegExp( "^" + whitespace + "*([>+~]|" + whitespace + ")" + whitespace + "*" ), - - rattributeQuotes = new RegExp( "=" + whitespace + "*([^\\]'\"]*?)" + whitespace + "*\\]", "g" ), + rdescend = new RegExp( whitespace + "|>" ), rpseudo = new RegExp( pseudos ), ridentifier = new RegExp( "^" + identifier + "$" ), @@ -661,6 +635,7 @@ var i, whitespace + "*((?:-\\d)?\\d*)" + whitespace + "*\\)|)(?=[^-]|$)", "i" ) }, + rhtml = /HTML$/i, rinputs = /^(?:input|select|textarea|button)$/i, rheader = /^h\d$/i, @@ -715,9 +690,9 @@ var i, setDocument(); }, - disabledAncestor = addCombinator( + inDisabledFieldset = addCombinator( function( elem ) { - return elem.disabled === true && ("form" in elem || "label" in elem); + return elem.disabled === true && elem.nodeName.toLowerCase() === "fieldset"; }, { dir: "parentNode", next: "legend" } ); @@ -830,18 +805,22 @@ function Sizzle( selector, context, results, seed ) { // Take advantage of querySelectorAll if ( support.qsa && - !compilerCache[ selector + " " ] && - (!rbuggyQSA || !rbuggyQSA.test( selector )) ) { - - if ( nodeType !== 1 ) { - newContext = context; - newSelector = selector; + !nonnativeSelectorCache[ selector + " " ] && + (!rbuggyQSA || !rbuggyQSA.test( selector )) && - // qSA looks outside Element context, which is not what we want - // Thanks to Andrew Dupont for this workaround technique - // Support: IE <=8 + // Support: IE 8 only // Exclude object elements - } else if ( context.nodeName.toLowerCase() !== "object" ) { + (nodeType !== 1 || context.nodeName.toLowerCase() !== "object") ) { + + newSelector = selector; + newContext = context; + + // qSA considers elements outside a scoping root when evaluating child or + // descendant combinators, which is not what we want. + // In such cases, we work around the behavior by prefixing every selector in the + // list with an ID selector referencing the scope context. + // Thanks to Andrew Dupont for this technique. + if ( nodeType === 1 && rdescend.test( selector ) ) { // Capture the context ID, setting it first if necessary if ( (nid = context.getAttribute( "id" )) ) { @@ -863,17 +842,16 @@ function Sizzle( selector, context, results, seed ) { context; } - if ( newSelector ) { - try { - push.apply( results, - newContext.querySelectorAll( newSelector ) - ); - return results; - } catch ( qsaError ) { - } finally { - if ( nid === expando ) { - context.removeAttribute( "id" ); - } + try { + push.apply( results, + newContext.querySelectorAll( newSelector ) + ); + return results; + } catch ( qsaError ) { + nonnativeSelectorCache( selector, true ); + } finally { + if ( nid === expando ) { + context.removeAttribute( "id" ); } } } @@ -1037,7 +1015,7 @@ function createDisabledPseudo( disabled ) { // Where there is no isDisabled, check manually /* jshint -W018 */ elem.isDisabled !== !disabled && - disabledAncestor( elem ) === disabled; + inDisabledFieldset( elem ) === disabled; } return elem.disabled === disabled; @@ -1094,10 +1072,13 @@ support = Sizzle.support = {}; * @returns {Boolean} True iff elem is a non-HTML XML node */ isXML = Sizzle.isXML = function( elem ) { - // documentElement is verified for cases where it doesn't yet exist - // (such as loading iframes in IE - #4833) - var documentElement = elem && (elem.ownerDocument || elem).documentElement; - return documentElement ? documentElement.nodeName !== "HTML" : false; + var namespace = elem.namespaceURI, + docElem = (elem.ownerDocument || elem).documentElement; + + // Support: IE <=8 + // Assume HTML when documentElement doesn't yet exist, such as inside loading iframes + // https://bugs.jquery.com/ticket/4833 + return !rhtml.test( namespace || docElem && docElem.nodeName || "HTML" ); }; /** @@ -1519,11 +1500,8 @@ Sizzle.matchesSelector = function( elem, expr ) { setDocument( elem ); } - // Make sure that attribute selectors are quoted - expr = expr.replace( rattributeQuotes, "='$1']" ); - if ( support.matchesSelector && documentIsHTML && - !compilerCache[ expr + " " ] && + !nonnativeSelectorCache[ expr + " " ] && ( !rbuggyMatches || !rbuggyMatches.test( expr ) ) && ( !rbuggyQSA || !rbuggyQSA.test( expr ) ) ) { @@ -1537,7 +1515,9 @@ Sizzle.matchesSelector = function( elem, expr ) { elem.document && elem.document.nodeType !== 11 ) { return ret; } - } catch (e) {} + } catch (e) { + nonnativeSelectorCache( expr, true ); + } } return Sizzle( expr, document, null, [ elem ] ).length > 0; @@ -1996,7 +1976,7 @@ Expr = Sizzle.selectors = { "contains": markFunction(function( text ) { text = text.replace( runescape, funescape ); return function( elem ) { - return ( elem.textContent || elem.innerText || getText( elem ) ).indexOf( text ) > -1; + return ( elem.textContent || getText( elem ) ).indexOf( text ) > -1; }; }), @@ -2135,7 +2115,11 @@ Expr = Sizzle.selectors = { }), "lt": createPositionalPseudo(function( matchIndexes, length, argument ) { - var i = argument < 0 ? argument + length : argument; + var i = argument < 0 ? + argument + length : + argument > length ? + length : + argument; for ( ; --i >= 0; ) { matchIndexes.push( i ); } @@ -2848,11 +2832,9 @@ var rsingleTag = ( /^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>| -var risSimple = /^.[^:#\[\.,]*$/; - // Implement the identical functionality for filter and not function winnow( elements, qualifier, not ) { - if ( jQuery.isFunction( qualifier ) ) { + if ( isFunction( qualifier ) ) { return jQuery.grep( elements, function( elem, i ) { return !!qualifier.call( elem, i, elem ) !== not; } ); @@ -2872,16 +2854,8 @@ function winnow( elements, qualifier, not ) { } ); } - // Simple selector that can be filtered directly, removing non-Elements - if ( risSimple.test( qualifier ) ) { - return jQuery.filter( qualifier, elements, not ); - } - - // Complex selector, compare the two sets, removing non-Elements - qualifier = jQuery.filter( qualifier, elements ); - return jQuery.grep( elements, function( elem ) { - return ( indexOf.call( qualifier, elem ) > -1 ) !== not && elem.nodeType === 1; - } ); + // Filtered directly for both simple and complex selectors + return jQuery.filter( qualifier, elements, not ); } jQuery.filter = function( expr, elems, not ) { @@ -3002,7 +2976,7 @@ var rootjQuery, for ( match in context ) { // Properties of context are called as methods if possible - if ( jQuery.isFunction( this[ match ] ) ) { + if ( isFunction( this[ match ] ) ) { this[ match ]( context[ match ] ); // ...and otherwise set as attributes @@ -3045,7 +3019,7 @@ var rootjQuery, // HANDLE: $(function) // Shortcut for document ready - } else if ( jQuery.isFunction( selector ) ) { + } else if ( isFunction( selector ) ) { return root.ready !== undefined ? root.ready( selector ) : @@ -3195,18 +3169,18 @@ jQuery.each( { return siblings( elem.firstChild ); }, contents: function( elem ) { - if ( nodeName( elem, "iframe" ) ) { - return elem.contentDocument; - } + if ( typeof elem.contentDocument !== "undefined" ) { + return elem.contentDocument; + } - // Support: IE 9 - 11 only, iOS 7 only, Android Browser <=4.3 only - // Treat the template element as a regular one in browsers that - // don't support it. - if ( nodeName( elem, "template" ) ) { - elem = elem.content || elem; - } + // Support: IE 9 - 11 only, iOS 7 only, Android Browser <=4.3 only + // Treat the template element as a regular one in browsers that + // don't support it. + if ( nodeName( elem, "template" ) ) { + elem = elem.content || elem; + } - return jQuery.merge( [], elem.childNodes ); + return jQuery.merge( [], elem.childNodes ); } }, function( name, fn ) { jQuery.fn[ name ] = function( until, selector ) { @@ -3360,11 +3334,11 @@ jQuery.Callbacks = function( options ) { ( function add( args ) { jQuery.each( args, function( _, arg ) { - if ( jQuery.isFunction( arg ) ) { + if ( isFunction( arg ) ) { if ( !options.unique || !self.has( arg ) ) { list.push( arg ); } - } else if ( arg && arg.length && jQuery.type( arg ) !== "string" ) { + } else if ( arg && arg.length && toType( arg ) !== "string" ) { // Inspect recursively add( arg ); @@ -3479,11 +3453,11 @@ function adoptValue( value, resolve, reject, noValue ) { try { // Check for promise aspect first to privilege synchronous behavior - if ( value && jQuery.isFunction( ( method = value.promise ) ) ) { + if ( value && isFunction( ( method = value.promise ) ) ) { method.call( value ).done( resolve ).fail( reject ); // Other thenables - } else if ( value && jQuery.isFunction( ( method = value.then ) ) ) { + } else if ( value && isFunction( ( method = value.then ) ) ) { method.call( value, resolve, reject ); // Other non-thenables @@ -3541,14 +3515,14 @@ jQuery.extend( { jQuery.each( tuples, function( i, tuple ) { // Map tuples (progress, done, fail) to arguments (done, fail, progress) - var fn = jQuery.isFunction( fns[ tuple[ 4 ] ] ) && fns[ tuple[ 4 ] ]; + var fn = isFunction( fns[ tuple[ 4 ] ] ) && fns[ tuple[ 4 ] ]; // deferred.progress(function() { bind to newDefer or newDefer.notify }) // deferred.done(function() { bind to newDefer or newDefer.resolve }) // deferred.fail(function() { bind to newDefer or newDefer.reject }) deferred[ tuple[ 1 ] ]( function() { var returned = fn && fn.apply( this, arguments ); - if ( returned && jQuery.isFunction( returned.promise ) ) { + if ( returned && isFunction( returned.promise ) ) { returned.promise() .progress( newDefer.notify ) .done( newDefer.resolve ) @@ -3602,7 +3576,7 @@ jQuery.extend( { returned.then; // Handle a returned thenable - if ( jQuery.isFunction( then ) ) { + if ( isFunction( then ) ) { // Special processors (notify) just wait for resolution if ( special ) { @@ -3698,7 +3672,7 @@ jQuery.extend( { resolve( 0, newDefer, - jQuery.isFunction( onProgress ) ? + isFunction( onProgress ) ? onProgress : Identity, newDefer.notifyWith @@ -3710,7 +3684,7 @@ jQuery.extend( { resolve( 0, newDefer, - jQuery.isFunction( onFulfilled ) ? + isFunction( onFulfilled ) ? onFulfilled : Identity ) @@ -3721,7 +3695,7 @@ jQuery.extend( { resolve( 0, newDefer, - jQuery.isFunction( onRejected ) ? + isFunction( onRejected ) ? onRejected : Thrower ) @@ -3761,8 +3735,15 @@ jQuery.extend( { // fulfilled_callbacks.disable tuples[ 3 - i ][ 2 ].disable, + // rejected_handlers.disable + // fulfilled_handlers.disable + tuples[ 3 - i ][ 3 ].disable, + // progress_callbacks.lock - tuples[ 0 ][ 2 ].lock + tuples[ 0 ][ 2 ].lock, + + // progress_handlers.lock + tuples[ 0 ][ 3 ].lock ); } @@ -3832,7 +3813,7 @@ jQuery.extend( { // Use .then() to unwrap secondary thenables (cf. gh-3000) if ( master.state() === "pending" || - jQuery.isFunction( resolveValues[ i ] && resolveValues[ i ].then ) ) { + isFunction( resolveValues[ i ] && resolveValues[ i ].then ) ) { return master.then(); } @@ -3960,7 +3941,7 @@ var access = function( elems, fn, key, value, chainable, emptyGet, raw ) { bulk = key == null; // Sets many values - if ( jQuery.type( key ) === "object" ) { + if ( toType( key ) === "object" ) { chainable = true; for ( i in key ) { access( elems, fn, i, key[ i ], true, emptyGet, raw ); @@ -3970,7 +3951,7 @@ var access = function( elems, fn, key, value, chainable, emptyGet, raw ) { } else if ( value !== undefined ) { chainable = true; - if ( !jQuery.isFunction( value ) ) { + if ( !isFunction( value ) ) { raw = true; } @@ -4012,6 +3993,23 @@ var access = function( elems, fn, key, value, chainable, emptyGet, raw ) { return len ? fn( elems[ 0 ], key ) : emptyGet; }; + + +// Matches dashed string for camelizing +var rmsPrefix = /^-ms-/, + rdashAlpha = /-([a-z])/g; + +// Used by camelCase as callback to replace() +function fcamelCase( all, letter ) { + return letter.toUpperCase(); +} + +// Convert dashed to camelCase; used by the css and data modules +// Support: IE <=9 - 11, Edge 12 - 15 +// Microsoft forgot to hump their vendor prefix (#9572) +function camelCase( string ) { + return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase ); +} var acceptData = function( owner ) { // Accepts only: @@ -4074,14 +4072,14 @@ Data.prototype = { // Handle: [ owner, key, value ] args // Always use camelCase key (gh-2257) if ( typeof data === "string" ) { - cache[ jQuery.camelCase( data ) ] = value; + cache[ camelCase( data ) ] = value; // Handle: [ owner, { properties } ] args } else { // Copy the properties one-by-one to the cache object for ( prop in data ) { - cache[ jQuery.camelCase( prop ) ] = data[ prop ]; + cache[ camelCase( prop ) ] = data[ prop ]; } } return cache; @@ -4091,7 +4089,7 @@ Data.prototype = { this.cache( owner ) : // Always use camelCase key (gh-2257) - owner[ this.expando ] && owner[ this.expando ][ jQuery.camelCase( key ) ]; + owner[ this.expando ] && owner[ this.expando ][ camelCase( key ) ]; }, access: function( owner, key, value ) { @@ -4139,9 +4137,9 @@ Data.prototype = { // If key is an array of keys... // We always set camelCase keys, so remove that. - key = key.map( jQuery.camelCase ); + key = key.map( camelCase ); } else { - key = jQuery.camelCase( key ); + key = camelCase( key ); // If a key with the spaces exists, use it. // Otherwise, create an array by matching non-whitespace @@ -4287,7 +4285,7 @@ jQuery.fn.extend( { if ( attrs[ i ] ) { name = attrs[ i ].name; if ( name.indexOf( "data-" ) === 0 ) { - name = jQuery.camelCase( name.slice( 5 ) ); + name = camelCase( name.slice( 5 ) ); dataAttr( elem, name, data[ name ] ); } } @@ -4491,6 +4489,26 @@ var rcssNum = new RegExp( "^(?:([+-])=|)(" + pnum + ")([a-z%]*)$", "i" ); var cssExpand = [ "Top", "Right", "Bottom", "Left" ]; +var documentElement = document.documentElement; + + + + var isAttached = function( elem ) { + return jQuery.contains( elem.ownerDocument, elem ); + }, + composed = { composed: true }; + + // Support: IE 9 - 11+, Edge 12 - 18+, iOS 10.0 - 10.2 only + // Check attachment across shadow DOM boundaries when possible (gh-3504) + // Support: iOS 10.0-10.2 only + // Early iOS 10 versions support `attachShadow` but not `getRootNode`, + // leading to errors. We need to check for `getRootNode`. + if ( documentElement.getRootNode ) { + isAttached = function( elem ) { + return jQuery.contains( elem.ownerDocument, elem ) || + elem.getRootNode( composed ) === elem.ownerDocument; + }; + } var isHiddenWithinTree = function( elem, el ) { // isHiddenWithinTree might be called from jQuery#filter function; @@ -4505,7 +4523,7 @@ var isHiddenWithinTree = function( elem, el ) { // Support: Firefox <=43 - 45 // Disconnected elements can have computed display: none, so first confirm that elem is // in the document. - jQuery.contains( elem.ownerDocument, elem ) && + isAttached( elem ) && jQuery.css( elem, "display" ) === "none"; }; @@ -4534,8 +4552,7 @@ var swap = function( elem, options, callback, args ) { function adjustCSS( elem, prop, valueParts, tween ) { - var adjusted, - scale = 1, + var adjusted, scale, maxIterations = 20, currentValue = tween ? function() { @@ -4548,35 +4565,39 @@ function adjustCSS( elem, prop, valueParts, tween ) { unit = valueParts && valueParts[ 3 ] || ( jQuery.cssNumber[ prop ] ? "" : "px" ), // Starting value computation is required for potential unit mismatches - initialInUnit = ( jQuery.cssNumber[ prop ] || unit !== "px" && +initial ) && + initialInUnit = elem.nodeType && + ( jQuery.cssNumber[ prop ] || unit !== "px" && +initial ) && rcssNum.exec( jQuery.css( elem, prop ) ); if ( initialInUnit && initialInUnit[ 3 ] !== unit ) { + // Support: Firefox <=54 + // Halve the iteration target value to prevent interference from CSS upper bounds (gh-2144) + initial = initial / 2; + // Trust units reported by jQuery.css unit = unit || initialInUnit[ 3 ]; - // Make sure we update the tween properties later on - valueParts = valueParts || []; - // Iteratively approximate from a nonzero starting point initialInUnit = +initial || 1; - do { - - // If previous iteration zeroed out, double until we get *something*. - // Use string for doubling so we don't accidentally see scale as unchanged below - scale = scale || ".5"; + while ( maxIterations-- ) { - // Adjust and apply - initialInUnit = initialInUnit / scale; + // Evaluate and update our best guess (doubling guesses that zero out). + // Finish if the scale equals or crosses 1 (making the old*new product non-positive). jQuery.style( elem, prop, initialInUnit + unit ); + if ( ( 1 - scale ) * ( 1 - ( scale = currentValue() / initial || 0.5 ) ) <= 0 ) { + maxIterations = 0; + } + initialInUnit = initialInUnit / scale; - // Update scale, tolerating zero or NaN from tween.cur() - // Break the loop if scale is unchanged or perfect, or if we've just had enough. - } while ( - scale !== ( scale = currentValue() / initial ) && scale !== 1 && --maxIterations - ); + } + + initialInUnit = initialInUnit * 2; + jQuery.style( elem, prop, initialInUnit + unit ); + + // Make sure we update the tween properties later on + valueParts = valueParts || []; } if ( valueParts ) { @@ -4692,9 +4713,9 @@ jQuery.fn.extend( { } ); var rcheckableType = ( /^(?:checkbox|radio)$/i ); -var rtagName = ( /<([a-z][^\/\0>\x20\t\r\n\f]+)/i ); +var rtagName = ( /<([a-z][^\/\0>\x20\t\r\n\f]*)/i ); -var rscriptType = ( /^$|\/(?:java|ecma)script/i ); +var rscriptType = ( /^$|^module$|\/(?:java|ecma)script/i ); @@ -4764,7 +4785,7 @@ function setGlobalEval( elems, refElements ) { var rhtml = /<|&#?\w+;/; function buildFragment( elems, context, scripts, selection, ignored ) { - var elem, tmp, tag, wrap, contains, j, + var elem, tmp, tag, wrap, attached, j, fragment = context.createDocumentFragment(), nodes = [], i = 0, @@ -4776,7 +4797,7 @@ function buildFragment( elems, context, scripts, selection, ignored ) { if ( elem || elem === 0 ) { // Add nodes directly - if ( jQuery.type( elem ) === "object" ) { + if ( toType( elem ) === "object" ) { // Support: Android <=4.0 only, PhantomJS 1 only // push.apply(_, arraylike) throws on ancient WebKit @@ -4828,13 +4849,13 @@ function buildFragment( elems, context, scripts, selection, ignored ) { continue; } - contains = jQuery.contains( elem.ownerDocument, elem ); + attached = isAttached( elem ); // Append to fragment tmp = getAll( fragment.appendChild( elem ), "script" ); // Preserve script evaluation history - if ( contains ) { + if ( attached ) { setGlobalEval( tmp ); } @@ -4877,8 +4898,6 @@ function buildFragment( elems, context, scripts, selection, ignored ) { div.innerHTML = ""; support.noCloneChecked = !!div.cloneNode( true ).lastChild.defaultValue; } )(); -var documentElement = document.documentElement; - var @@ -4894,8 +4913,19 @@ function returnFalse() { return false; } +// Support: IE <=9 - 11+ +// focus() and blur() are asynchronous, except when they are no-op. +// So expect focus to be synchronous when the element is already active, +// and blur to be synchronous when the element is not already active. +// (focus and blur are always synchronous in other supported browsers, +// this just defines when we can count on it). +function expectSync( elem, type ) { + return ( elem === safeActiveElement() ) === ( type === "focus" ); +} + // Support: IE <=9 only -// See #13393 for more info +// Accessing document.activeElement can throw unexpectedly +// https://bugs.jquery.com/ticket/13393 function safeActiveElement() { try { return document.activeElement; @@ -5195,9 +5225,10 @@ jQuery.event = { while ( ( handleObj = matched.handlers[ j++ ] ) && !event.isImmediatePropagationStopped() ) { - // Triggered event must either 1) have no namespace, or 2) have namespace(s) - // a subset or equal to those in the bound event (both can have no namespace). - if ( !event.rnamespace || event.rnamespace.test( handleObj.namespace ) ) { + // If the event is namespaced, then each handler is only invoked if it is + // specially universal or its namespaces are a superset of the event's. + if ( !event.rnamespace || handleObj.namespace === false || + event.rnamespace.test( handleObj.namespace ) ) { event.handleObj = handleObj; event.data = handleObj.data; @@ -5286,7 +5317,7 @@ jQuery.event = { enumerable: true, configurable: true, - get: jQuery.isFunction( hook ) ? + get: isFunction( hook ) ? function() { if ( this.originalEvent ) { return hook( this.originalEvent ); @@ -5321,39 +5352,51 @@ jQuery.event = { // Prevent triggered image.load events from bubbling to window.load noBubble: true }, - focus: { + click: { - // Fire native event if possible so blur/focus sequence is correct - trigger: function() { - if ( this !== safeActiveElement() && this.focus ) { - this.focus(); - return false; - } - }, - delegateType: "focusin" - }, - blur: { - trigger: function() { - if ( this === safeActiveElement() && this.blur ) { - this.blur(); - return false; + // Utilize native event to ensure correct state for checkable inputs + setup: function( data ) { + + // For mutual compressibility with _default, replace `this` access with a local var. + // `|| data` is dead code meant only to preserve the variable through minification. + var el = this || data; + + // Claim the first handler + if ( rcheckableType.test( el.type ) && + el.click && nodeName( el, "input" ) ) { + + // dataPriv.set( el, "click", ... ) + leverageNative( el, "click", returnTrue ); } + + // Return false to allow normal processing in the caller + return false; }, - delegateType: "focusout" - }, - click: { + trigger: function( data ) { - // For checkbox, fire native event so checked state will be right - trigger: function() { - if ( this.type === "checkbox" && this.click && nodeName( this, "input" ) ) { - this.click(); - return false; + // For mutual compressibility with _default, replace `this` access with a local var. + // `|| data` is dead code meant only to preserve the variable through minification. + var el = this || data; + + // Force setup before triggering a click + if ( rcheckableType.test( el.type ) && + el.click && nodeName( el, "input" ) ) { + + leverageNative( el, "click" ); } + + // Return non-false to allow normal event-path propagation + return true; }, - // For cross-browser consistency, don't fire native .click() on links + // For cross-browser consistency, suppress native .click() on links + // Also prevent it if we're currently inside a leveraged native-event stack _default: function( event ) { - return nodeName( event.target, "a" ); + var target = event.target; + return rcheckableType.test( target.type ) && + target.click && nodeName( target, "input" ) && + dataPriv.get( target, "click" ) || + nodeName( target, "a" ); } }, @@ -5370,6 +5413,93 @@ jQuery.event = { } }; +// Ensure the presence of an event listener that handles manually-triggered +// synthetic events by interrupting progress until reinvoked in response to +// *native* events that it fires directly, ensuring that state changes have +// already occurred before other listeners are invoked. +function leverageNative( el, type, expectSync ) { + + // Missing expectSync indicates a trigger call, which must force setup through jQuery.event.add + if ( !expectSync ) { + if ( dataPriv.get( el, type ) === undefined ) { + jQuery.event.add( el, type, returnTrue ); + } + return; + } + + // Register the controller as a special universal handler for all event namespaces + dataPriv.set( el, type, false ); + jQuery.event.add( el, type, { + namespace: false, + handler: function( event ) { + var notAsync, result, + saved = dataPriv.get( this, type ); + + if ( ( event.isTrigger & 1 ) && this[ type ] ) { + + // Interrupt processing of the outer synthetic .trigger()ed event + // Saved data should be false in such cases, but might be a leftover capture object + // from an async native handler (gh-4350) + if ( !saved.length ) { + + // Store arguments for use when handling the inner native event + // There will always be at least one argument (an event object), so this array + // will not be confused with a leftover capture object. + saved = slice.call( arguments ); + dataPriv.set( this, type, saved ); + + // Trigger the native event and capture its result + // Support: IE <=9 - 11+ + // focus() and blur() are asynchronous + notAsync = expectSync( this, type ); + this[ type ](); + result = dataPriv.get( this, type ); + if ( saved !== result || notAsync ) { + dataPriv.set( this, type, false ); + } else { + result = {}; + } + if ( saved !== result ) { + + // Cancel the outer synthetic event + event.stopImmediatePropagation(); + event.preventDefault(); + return result.value; + } + + // If this is an inner synthetic event for an event with a bubbling surrogate + // (focus or blur), assume that the surrogate already propagated from triggering the + // native event and prevent that from happening again here. + // This technically gets the ordering wrong w.r.t. to `.trigger()` (in which the + // bubbling surrogate propagates *after* the non-bubbling base), but that seems + // less bad than duplication. + } else if ( ( jQuery.event.special[ type ] || {} ).delegateType ) { + event.stopPropagation(); + } + + // If this is a native event triggered above, everything is now in order + // Fire an inner synthetic event with the original arguments + } else if ( saved.length ) { + + // ...and capture the result + dataPriv.set( this, type, { + value: jQuery.event.trigger( + + // Support: IE <=9 - 11+ + // Extend with the prototype to reset the above stopImmediatePropagation() + jQuery.extend( saved[ 0 ], jQuery.Event.prototype ), + saved.slice( 1 ), + this + ) + } ); + + // Abort handling of the native event + event.stopImmediatePropagation(); + } + } + } ); +} + jQuery.removeEvent = function( elem, type, handle ) { // This "if" is needed for plain objects @@ -5421,7 +5551,7 @@ jQuery.Event = function( src, props ) { } // Create a timestamp if incoming event doesn't have one - this.timeStamp = src && src.timeStamp || jQuery.now(); + this.timeStamp = src && src.timeStamp || Date.now(); // Mark it as fixed this[ jQuery.expando ] = true; @@ -5482,6 +5612,7 @@ jQuery.each( { shiftKey: true, view: true, "char": true, + code: true, charCode: true, key: true, keyCode: true, @@ -5528,6 +5659,33 @@ jQuery.each( { } }, jQuery.event.addProp ); +jQuery.each( { focus: "focusin", blur: "focusout" }, function( type, delegateType ) { + jQuery.event.special[ type ] = { + + // Utilize native event if possible so blur/focus sequence is correct + setup: function() { + + // Claim the first handler + // dataPriv.set( this, "focus", ... ) + // dataPriv.set( this, "blur", ... ) + leverageNative( this, type, expectSync ); + + // Return false to allow normal processing in the caller + return false; + }, + trigger: function() { + + // Force setup before trigger + leverageNative( this, type ); + + // Return non-false to allow normal event-path propagation + return true; + }, + + delegateType: delegateType + }; +} ); + // Create mouseenter/leave events using mouseover/out and event-time checks // so that event delegation works in jQuery. // Do the same for pointerenter/pointerleave and pointerover/pointerout @@ -5620,14 +5778,13 @@ var /* eslint-enable */ - // Support: IE <=10 - 11, Edge 12 - 13 + // Support: IE <=10 - 11, Edge 12 - 13 only // In IE/Edge using regex groups here causes severe slowdowns. // See https://connect.microsoft.com/IE/feedback/details/1736512/ rnoInnerhtml = /\s*$/g; // Prefer a tbody over its parent table for containing new rows @@ -5635,7 +5792,7 @@ function manipulationTarget( elem, content ) { if ( nodeName( elem, "table" ) && nodeName( content.nodeType !== 11 ? content : content.firstChild, "tr" ) ) { - return jQuery( ">tbody", elem )[ 0 ] || elem; + return jQuery( elem ).children( "tbody" )[ 0 ] || elem; } return elem; @@ -5647,10 +5804,8 @@ function disableScript( elem ) { return elem; } function restoreScript( elem ) { - var match = rscriptTypeMasked.exec( elem.type ); - - if ( match ) { - elem.type = match[ 1 ]; + if ( ( elem.type || "" ).slice( 0, 5 ) === "true/" ) { + elem.type = elem.type.slice( 5 ); } else { elem.removeAttribute( "type" ); } @@ -5716,15 +5871,15 @@ function domManip( collection, args, callback, ignored ) { l = collection.length, iNoClone = l - 1, value = args[ 0 ], - isFunction = jQuery.isFunction( value ); + valueIsFunction = isFunction( value ); // We can't cloneNode fragments that contain checked, in WebKit - if ( isFunction || + if ( valueIsFunction || ( l > 1 && typeof value === "string" && !support.checkClone && rchecked.test( value ) ) ) { return collection.each( function( index ) { var self = collection.eq( index ); - if ( isFunction ) { + if ( valueIsFunction ) { args[ 0 ] = value.call( this, index, self.html() ); } domManip( self, args, callback, ignored ); @@ -5778,14 +5933,16 @@ function domManip( collection, args, callback, ignored ) { !dataPriv.access( node, "globalEval" ) && jQuery.contains( doc, node ) ) { - if ( node.src ) { + if ( node.src && ( node.type || "" ).toLowerCase() !== "module" ) { // Optional AJAX dependency, but won't run scripts if not present - if ( jQuery._evalUrl ) { - jQuery._evalUrl( node.src ); + if ( jQuery._evalUrl && !node.noModule ) { + jQuery._evalUrl( node.src, { + nonce: node.nonce || node.getAttribute( "nonce" ) + } ); } } else { - DOMEval( node.textContent.replace( rcleanScript, "" ), doc ); + DOMEval( node.textContent.replace( rcleanScript, "" ), node, doc ); } } } @@ -5807,7 +5964,7 @@ function remove( elem, selector, keepData ) { } if ( node.parentNode ) { - if ( keepData && jQuery.contains( node.ownerDocument, node ) ) { + if ( keepData && isAttached( node ) ) { setGlobalEval( getAll( node, "script" ) ); } node.parentNode.removeChild( node ); @@ -5825,7 +5982,7 @@ jQuery.extend( { clone: function( elem, dataAndEvents, deepDataAndEvents ) { var i, l, srcElements, destElements, clone = elem.cloneNode( true ), - inPage = jQuery.contains( elem.ownerDocument, elem ); + inPage = isAttached( elem ); // Fix IE cloning issues if ( !support.noCloneChecked && ( elem.nodeType === 1 || elem.nodeType === 11 ) && @@ -6065,8 +6222,6 @@ jQuery.each( { return this.pushStack( ret ); }; } ); -var rmargin = ( /^margin/ ); - var rnumnonpx = new RegExp( "^(" + pnum + ")(?!px)[a-z%]+$", "i" ); var getStyles = function( elem ) { @@ -6083,6 +6238,8 @@ var getStyles = function( elem ) { return view.getComputedStyle( elem ); }; +var rboxStyle = new RegExp( cssExpand.join( "|" ), "i" ); + ( function() { @@ -6096,25 +6253,35 @@ var getStyles = function( elem ) { return; } + container.style.cssText = "position:absolute;left:-11111px;width:60px;" + + "margin-top:1px;padding:0;border:0"; div.style.cssText = - "box-sizing:border-box;" + - "position:relative;display:block;" + + "position:relative;display:block;box-sizing:border-box;overflow:scroll;" + "margin:auto;border:1px;padding:1px;" + - "top:1%;width:50%"; - div.innerHTML = ""; - documentElement.appendChild( container ); + "width:60%;top:1%"; + documentElement.appendChild( container ).appendChild( div ); var divStyle = window.getComputedStyle( div ); pixelPositionVal = divStyle.top !== "1%"; // Support: Android 4.0 - 4.3 only, Firefox <=3 - 44 - reliableMarginLeftVal = divStyle.marginLeft === "2px"; - boxSizingReliableVal = divStyle.width === "4px"; + reliableMarginLeftVal = roundPixelMeasures( divStyle.marginLeft ) === 12; - // Support: Android 4.0 - 4.3 only + // Support: Android 4.0 - 4.3 only, Safari <=9.1 - 10.1, iOS <=7.0 - 9.3 // Some styles come back with percentage values, even though they shouldn't - div.style.marginRight = "50%"; - pixelMarginRightVal = divStyle.marginRight === "4px"; + div.style.right = "60%"; + pixelBoxStylesVal = roundPixelMeasures( divStyle.right ) === 36; + + // Support: IE 9 - 11 only + // Detect misreporting of content dimensions for box-sizing:border-box elements + boxSizingReliableVal = roundPixelMeasures( divStyle.width ) === 36; + + // Support: IE 9 only + // Detect overflow:scroll screwiness (gh-3699) + // Support: Chrome <=64 + // Don't get tricked when zoom affects offsetWidth (gh-4029) + div.style.position = "absolute"; + scrollboxSizeVal = roundPixelMeasures( div.offsetWidth / 3 ) === 12; documentElement.removeChild( container ); @@ -6123,7 +6290,12 @@ var getStyles = function( elem ) { div = null; } - var pixelPositionVal, boxSizingReliableVal, pixelMarginRightVal, reliableMarginLeftVal, + function roundPixelMeasures( measure ) { + return Math.round( parseFloat( measure ) ); + } + + var pixelPositionVal, boxSizingReliableVal, scrollboxSizeVal, pixelBoxStylesVal, + reliableMarginLeftVal, container = document.createElement( "div" ), div = document.createElement( "div" ); @@ -6138,26 +6310,26 @@ var getStyles = function( elem ) { div.cloneNode( true ).style.backgroundClip = ""; support.clearCloneStyle = div.style.backgroundClip === "content-box"; - container.style.cssText = "border:0;width:8px;height:0;top:0;left:-9999px;" + - "padding:0;margin-top:1px;position:absolute"; - container.appendChild( div ); - jQuery.extend( support, { - pixelPosition: function() { - computeStyleTests(); - return pixelPositionVal; - }, boxSizingReliable: function() { computeStyleTests(); return boxSizingReliableVal; }, - pixelMarginRight: function() { + pixelBoxStyles: function() { + computeStyleTests(); + return pixelBoxStylesVal; + }, + pixelPosition: function() { computeStyleTests(); - return pixelMarginRightVal; + return pixelPositionVal; }, reliableMarginLeft: function() { computeStyleTests(); return reliableMarginLeftVal; + }, + scrollboxSize: function() { + computeStyleTests(); + return scrollboxSizeVal; } } ); } )(); @@ -6180,7 +6352,7 @@ function curCSS( elem, name, computed ) { if ( computed ) { ret = computed.getPropertyValue( name ) || computed[ name ]; - if ( ret === "" && !jQuery.contains( elem.ownerDocument, elem ) ) { + if ( ret === "" && !isAttached( elem ) ) { ret = jQuery.style( elem, name ); } @@ -6189,7 +6361,7 @@ function curCSS( elem, name, computed ) { // but width seems to be reliably pixels. // This is against the CSSOM draft spec: // https://drafts.csswg.org/cssom/#resolved-values - if ( !support.pixelMarginRight() && rnumnonpx.test( ret ) && rmargin.test( name ) ) { + if ( !support.pixelBoxStyles() && rnumnonpx.test( ret ) && rboxStyle.test( name ) ) { // Remember the original values width = style.width; @@ -6236,30 +6408,13 @@ function addGetHookIf( conditionFn, hookFn ) { } -var +var cssPrefixes = [ "Webkit", "Moz", "ms" ], + emptyStyle = document.createElement( "div" ).style, + vendorProps = {}; - // Swappable if display is none or starts with table - // except "table", "table-cell", or "table-caption" - // See here for display values: https://developer.mozilla.org/en-US/docs/CSS/display - rdisplayswap = /^(none|table(?!-c[ea]).+)/, - rcustomProp = /^--/, - cssShow = { position: "absolute", visibility: "hidden", display: "block" }, - cssNormalTransform = { - letterSpacing: "0", - fontWeight: "400" - }, - - cssPrefixes = [ "Webkit", "Moz", "ms" ], - emptyStyle = document.createElement( "div" ).style; - -// Return a css property mapped to a potentially vendor prefixed property +// Return a vendor-prefixed property or undefined function vendorPropName( name ) { - // Shortcut for names that are not vendor prefixed - if ( name in emptyStyle ) { - return name; - } - // Check for vendor prefixed names var capName = name[ 0 ].toUpperCase() + name.slice( 1 ), i = cssPrefixes.length; @@ -6272,16 +6427,33 @@ function vendorPropName( name ) { } } -// Return a property mapped along what jQuery.cssProps suggests or to -// a vendor prefixed property. +// Return a potentially-mapped jQuery.cssProps or vendor prefixed property function finalPropName( name ) { - var ret = jQuery.cssProps[ name ]; - if ( !ret ) { - ret = jQuery.cssProps[ name ] = vendorPropName( name ) || name; + var final = jQuery.cssProps[ name ] || vendorProps[ name ]; + + if ( final ) { + return final; } - return ret; + if ( name in emptyStyle ) { + return name; + } + return vendorProps[ name ] = vendorPropName( name ) || name; } + +var + + // Swappable if display is none or starts with table + // except "table", "table-cell", or "table-caption" + // See here for display values: https://developer.mozilla.org/en-US/docs/CSS/display + rdisplayswap = /^(none|table(?!-c[ea]).+)/, + rcustomProp = /^--/, + cssShow = { position: "absolute", visibility: "hidden", display: "block" }, + cssNormalTransform = { + letterSpacing: "0", + fontWeight: "400" + }; + function setPositiveNumber( elem, value, subtract ) { // Any relative (+/-) values have already been @@ -6294,87 +6466,137 @@ function setPositiveNumber( elem, value, subtract ) { value; } -function augmentWidthOrHeight( elem, name, extra, isBorderBox, styles ) { - var i, - val = 0; - - // If we already have the right measurement, avoid augmentation - if ( extra === ( isBorderBox ? "border" : "content" ) ) { - i = 4; +function boxModelAdjustment( elem, dimension, box, isBorderBox, styles, computedVal ) { + var i = dimension === "width" ? 1 : 0, + extra = 0, + delta = 0; - // Otherwise initialize for horizontal or vertical properties - } else { - i = name === "width" ? 1 : 0; + // Adjustment may not be necessary + if ( box === ( isBorderBox ? "border" : "content" ) ) { + return 0; } for ( ; i < 4; i += 2 ) { - // Both box models exclude margin, so add it if we want it - if ( extra === "margin" ) { - val += jQuery.css( elem, extra + cssExpand[ i ], true, styles ); + // Both box models exclude margin + if ( box === "margin" ) { + delta += jQuery.css( elem, box + cssExpand[ i ], true, styles ); } - if ( isBorderBox ) { + // If we get here with a content-box, we're seeking "padding" or "border" or "margin" + if ( !isBorderBox ) { - // border-box includes padding, so remove it if we want content - if ( extra === "content" ) { - val -= jQuery.css( elem, "padding" + cssExpand[ i ], true, styles ); - } + // Add padding + delta += jQuery.css( elem, "padding" + cssExpand[ i ], true, styles ); + + // For "border" or "margin", add border + if ( box !== "padding" ) { + delta += jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); - // At this point, extra isn't border nor margin, so remove border - if ( extra !== "margin" ) { - val -= jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); + // But still keep track of it otherwise + } else { + extra += jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); } + + // If we get here with a border-box (content + padding + border), we're seeking "content" or + // "padding" or "margin" } else { - // At this point, extra isn't content, so add padding - val += jQuery.css( elem, "padding" + cssExpand[ i ], true, styles ); + // For "content", subtract padding + if ( box === "content" ) { + delta -= jQuery.css( elem, "padding" + cssExpand[ i ], true, styles ); + } - // At this point, extra isn't content nor padding, so add border - if ( extra !== "padding" ) { - val += jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); + // For "content" or "padding", subtract border + if ( box !== "margin" ) { + delta -= jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); } } } - return val; + // Account for positive content-box scroll gutter when requested by providing computedVal + if ( !isBorderBox && computedVal >= 0 ) { + + // offsetWidth/offsetHeight is a rounded sum of content, padding, scroll gutter, and border + // Assuming integer scroll gutter, subtract the rest and round down + delta += Math.max( 0, Math.ceil( + elem[ "offset" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 ) ] - + computedVal - + delta - + extra - + 0.5 + + // If offsetWidth/offsetHeight is unknown, then we can't determine content-box scroll gutter + // Use an explicit zero to avoid NaN (gh-3964) + ) ) || 0; + } + + return delta; } -function getWidthOrHeight( elem, name, extra ) { +function getWidthOrHeight( elem, dimension, extra ) { // Start with computed style - var valueIsBorderBox, - styles = getStyles( elem ), - val = curCSS( elem, name, styles ), - isBorderBox = jQuery.css( elem, "boxSizing", false, styles ) === "border-box"; + var styles = getStyles( elem ), + + // To avoid forcing a reflow, only fetch boxSizing if we need it (gh-4322). + // Fake content-box until we know it's needed to know the true value. + boxSizingNeeded = !support.boxSizingReliable() || extra, + isBorderBox = boxSizingNeeded && + jQuery.css( elem, "boxSizing", false, styles ) === "border-box", + valueIsBorderBox = isBorderBox, + + val = curCSS( elem, dimension, styles ), + offsetProp = "offset" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 ); - // Computed unit is not pixels. Stop here and return. + // Support: Firefox <=54 + // Return a confounding non-pixel value or feign ignorance, as appropriate. if ( rnumnonpx.test( val ) ) { - return val; + if ( !extra ) { + return val; + } + val = "auto"; } - // Check for style in case a browser which returns unreliable values - // for getComputedStyle silently falls back to the reliable elem.style - valueIsBorderBox = isBorderBox && - ( support.boxSizingReliable() || val === elem.style[ name ] ); - // Fall back to offsetWidth/Height when value is "auto" + // Fall back to offsetWidth/offsetHeight when value is "auto" // This happens for inline elements with no explicit setting (gh-3571) - if ( val === "auto" ) { - val = elem[ "offset" + name[ 0 ].toUpperCase() + name.slice( 1 ) ]; + // Support: Android <=4.1 - 4.3 only + // Also use offsetWidth/offsetHeight for misreported inline dimensions (gh-3602) + // Support: IE 9-11 only + // Also use offsetWidth/offsetHeight for when box sizing is unreliable + // We use getClientRects() to check for hidden/disconnected. + // In those cases, the computed value can be trusted to be border-box + if ( ( !support.boxSizingReliable() && isBorderBox || + val === "auto" || + !parseFloat( val ) && jQuery.css( elem, "display", false, styles ) === "inline" ) && + elem.getClientRects().length ) { + + isBorderBox = jQuery.css( elem, "boxSizing", false, styles ) === "border-box"; + + // Where available, offsetWidth/offsetHeight approximate border box dimensions. + // Where not available (e.g., SVG), assume unreliable box-sizing and interpret the + // retrieved value as a content box dimension. + valueIsBorderBox = offsetProp in elem; + if ( valueIsBorderBox ) { + val = elem[ offsetProp ]; + } } - // Normalize "", auto, and prepare for extra + // Normalize "" and auto val = parseFloat( val ) || 0; - // Use the active box-sizing model to add/subtract irrelevant styles + // Adjust for the element's box model return ( val + - augmentWidthOrHeight( + boxModelAdjustment( elem, - name, + dimension, extra || ( isBorderBox ? "border" : "content" ), valueIsBorderBox, - styles + styles, + + // Provide the current computed size to request scroll gutter calculation (gh-3589) + val ) ) + "px"; } @@ -6404,6 +6626,13 @@ jQuery.extend( { "flexGrow": true, "flexShrink": true, "fontWeight": true, + "gridArea": true, + "gridColumn": true, + "gridColumnEnd": true, + "gridColumnStart": true, + "gridRow": true, + "gridRowEnd": true, + "gridRowStart": true, "lineHeight": true, "opacity": true, "order": true, @@ -6415,9 +6644,7 @@ jQuery.extend( { // Add in properties whose names you wish to fix before // setting or getting the value - cssProps: { - "float": "cssFloat" - }, + cssProps: {}, // Get and set the style property on a DOM Node style: function( elem, name, value, extra ) { @@ -6429,7 +6656,7 @@ jQuery.extend( { // Make sure that we're working with the right name var ret, type, hooks, - origName = jQuery.camelCase( name ), + origName = camelCase( name ), isCustomProp = rcustomProp.test( name ), style = elem.style; @@ -6461,7 +6688,9 @@ jQuery.extend( { } // If a number was passed in, add the unit (except for certain CSS properties) - if ( type === "number" ) { + // The isCustomProp check can be removed in jQuery 4.0 when we only auto-append + // "px" to a few hardcoded values. + if ( type === "number" && !isCustomProp ) { value += ret && ret[ 3 ] || ( jQuery.cssNumber[ origName ] ? "" : "px" ); } @@ -6497,7 +6726,7 @@ jQuery.extend( { css: function( elem, name, extra, styles ) { var val, num, hooks, - origName = jQuery.camelCase( name ), + origName = camelCase( name ), isCustomProp = rcustomProp.test( name ); // Make sure that we're working with the right name. We don't @@ -6535,8 +6764,8 @@ jQuery.extend( { } } ); -jQuery.each( [ "height", "width" ], function( i, name ) { - jQuery.cssHooks[ name ] = { +jQuery.each( [ "height", "width" ], function( i, dimension ) { + jQuery.cssHooks[ dimension ] = { get: function( elem, computed, extra ) { if ( computed ) { @@ -6552,29 +6781,52 @@ jQuery.each( [ "height", "width" ], function( i, name ) { // in IE throws an error. ( !elem.getClientRects().length || !elem.getBoundingClientRect().width ) ? swap( elem, cssShow, function() { - return getWidthOrHeight( elem, name, extra ); + return getWidthOrHeight( elem, dimension, extra ); } ) : - getWidthOrHeight( elem, name, extra ); + getWidthOrHeight( elem, dimension, extra ); } }, set: function( elem, value, extra ) { var matches, - styles = extra && getStyles( elem ), - subtract = extra && augmentWidthOrHeight( - elem, - name, - extra, + styles = getStyles( elem ), + + // Only read styles.position if the test has a chance to fail + // to avoid forcing a reflow. + scrollboxSizeBuggy = !support.scrollboxSize() && + styles.position === "absolute", + + // To avoid forcing a reflow, only fetch boxSizing if we need it (gh-3991) + boxSizingNeeded = scrollboxSizeBuggy || extra, + isBorderBox = boxSizingNeeded && jQuery.css( elem, "boxSizing", false, styles ) === "border-box", - styles + subtract = extra ? + boxModelAdjustment( + elem, + dimension, + extra, + isBorderBox, + styles + ) : + 0; + + // Account for unreliable border-box dimensions by comparing offset* to computed and + // faking a content-box to get border and padding (gh-3699) + if ( isBorderBox && scrollboxSizeBuggy ) { + subtract -= Math.ceil( + elem[ "offset" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 ) ] - + parseFloat( styles[ dimension ] ) - + boxModelAdjustment( elem, dimension, "border", false, styles ) - + 0.5 ); + } // Convert to pixels if value adjustment is needed if ( subtract && ( matches = rcssNum.exec( value ) ) && ( matches[ 3 ] || "px" ) !== "px" ) { - elem.style[ name ] = value; - value = jQuery.css( elem, name ); + elem.style[ dimension ] = value; + value = jQuery.css( elem, dimension ); } return setPositiveNumber( elem, value, subtract ); @@ -6618,7 +6870,7 @@ jQuery.each( { } }; - if ( !rmargin.test( prefix ) ) { + if ( prefix !== "margin" ) { jQuery.cssHooks[ prefix + suffix ].set = setPositiveNumber; } } ); @@ -6728,9 +6980,9 @@ Tween.propHooks = { // Use .style if available and use plain properties where available. if ( jQuery.fx.step[ tween.prop ] ) { jQuery.fx.step[ tween.prop ]( tween ); - } else if ( tween.elem.nodeType === 1 && - ( tween.elem.style[ jQuery.cssProps[ tween.prop ] ] != null || - jQuery.cssHooks[ tween.prop ] ) ) { + } else if ( tween.elem.nodeType === 1 && ( + jQuery.cssHooks[ tween.prop ] || + tween.elem.style[ finalPropName( tween.prop ) ] != null ) ) { jQuery.style( tween.elem, tween.prop, tween.now + tween.unit ); } else { tween.elem[ tween.prop ] = tween.now; @@ -6789,7 +7041,7 @@ function createFxNow() { window.setTimeout( function() { fxNow = undefined; } ); - return ( fxNow = jQuery.now() ); + return ( fxNow = Date.now() ); } // Generate parameters to create a standard animation @@ -6893,9 +7145,10 @@ function defaultPrefilter( elem, props, opts ) { // Restrict "overflow" and "display" styles during box animations if ( isBox && elem.nodeType === 1 ) { - // Support: IE <=9 - 11, Edge 12 - 13 + // Support: IE <=9 - 11, Edge 12 - 15 // Record all 3 overflow attributes because IE does not infer the shorthand - // from identically-valued overflowX and overflowY + // from identically-valued overflowX and overflowY and Edge just mirrors + // the overflowX value there. opts.overflow = [ style.overflow, style.overflowX, style.overflowY ]; // Identify a display type, preferring old show/hide data over the CSS cascade @@ -7003,7 +7256,7 @@ function propFilter( props, specialEasing ) { // camelCase, specialEasing and expand cssHook pass for ( index in props ) { - name = jQuery.camelCase( index ); + name = camelCase( index ); easing = specialEasing[ name ]; value = props[ index ]; if ( Array.isArray( value ) ) { @@ -7128,9 +7381,9 @@ function Animation( elem, properties, options ) { for ( ; index < length; index++ ) { result = Animation.prefilters[ index ].call( animation, elem, props, animation.opts ); if ( result ) { - if ( jQuery.isFunction( result.stop ) ) { + if ( isFunction( result.stop ) ) { jQuery._queueHooks( animation.elem, animation.opts.queue ).stop = - jQuery.proxy( result.stop, result ); + result.stop.bind( result ); } return result; } @@ -7138,7 +7391,7 @@ function Animation( elem, properties, options ) { jQuery.map( props, createTween, animation ); - if ( jQuery.isFunction( animation.opts.start ) ) { + if ( isFunction( animation.opts.start ) ) { animation.opts.start.call( elem, animation ); } @@ -7171,7 +7424,7 @@ jQuery.Animation = jQuery.extend( Animation, { }, tweener: function( props, callback ) { - if ( jQuery.isFunction( props ) ) { + if ( isFunction( props ) ) { callback = props; props = [ "*" ]; } else { @@ -7203,9 +7456,9 @@ jQuery.Animation = jQuery.extend( Animation, { jQuery.speed = function( speed, easing, fn ) { var opt = speed && typeof speed === "object" ? jQuery.extend( {}, speed ) : { complete: fn || !fn && easing || - jQuery.isFunction( speed ) && speed, + isFunction( speed ) && speed, duration: speed, - easing: fn && easing || easing && !jQuery.isFunction( easing ) && easing + easing: fn && easing || easing && !isFunction( easing ) && easing }; // Go to the end state if fx are off @@ -7232,7 +7485,7 @@ jQuery.speed = function( speed, easing, fn ) { opt.old = opt.complete; opt.complete = function() { - if ( jQuery.isFunction( opt.old ) ) { + if ( isFunction( opt.old ) ) { opt.old.call( this ); } @@ -7396,7 +7649,7 @@ jQuery.fx.tick = function() { i = 0, timers = jQuery.timers; - fxNow = jQuery.now(); + fxNow = Date.now(); for ( ; i < timers.length; i++ ) { timer = timers[ i ]; @@ -7749,7 +8002,7 @@ jQuery.each( [ // Strip and collapse whitespace according to HTML spec - // https://html.spec.whatwg.org/multipage/infrastructure.html#strip-and-collapse-whitespace + // https://infra.spec.whatwg.org/#strip-and-collapse-ascii-whitespace function stripAndCollapse( value ) { var tokens = value.match( rnothtmlwhite ) || []; return tokens.join( " " ); @@ -7760,20 +8013,30 @@ function getClass( elem ) { return elem.getAttribute && elem.getAttribute( "class" ) || ""; } +function classesToArray( value ) { + if ( Array.isArray( value ) ) { + return value; + } + if ( typeof value === "string" ) { + return value.match( rnothtmlwhite ) || []; + } + return []; +} + jQuery.fn.extend( { addClass: function( value ) { var classes, elem, cur, curValue, clazz, j, finalValue, i = 0; - if ( jQuery.isFunction( value ) ) { + if ( isFunction( value ) ) { return this.each( function( j ) { jQuery( this ).addClass( value.call( this, j, getClass( this ) ) ); } ); } - if ( typeof value === "string" && value ) { - classes = value.match( rnothtmlwhite ) || []; + classes = classesToArray( value ); + if ( classes.length ) { while ( ( elem = this[ i++ ] ) ) { curValue = getClass( elem ); cur = elem.nodeType === 1 && ( " " + stripAndCollapse( curValue ) + " " ); @@ -7802,7 +8065,7 @@ jQuery.fn.extend( { var classes, elem, cur, curValue, clazz, j, finalValue, i = 0; - if ( jQuery.isFunction( value ) ) { + if ( isFunction( value ) ) { return this.each( function( j ) { jQuery( this ).removeClass( value.call( this, j, getClass( this ) ) ); } ); @@ -7812,9 +8075,9 @@ jQuery.fn.extend( { return this.attr( "class", "" ); } - if ( typeof value === "string" && value ) { - classes = value.match( rnothtmlwhite ) || []; + classes = classesToArray( value ); + if ( classes.length ) { while ( ( elem = this[ i++ ] ) ) { curValue = getClass( elem ); @@ -7844,13 +8107,14 @@ jQuery.fn.extend( { }, toggleClass: function( value, stateVal ) { - var type = typeof value; + var type = typeof value, + isValidValue = type === "string" || Array.isArray( value ); - if ( typeof stateVal === "boolean" && type === "string" ) { + if ( typeof stateVal === "boolean" && isValidValue ) { return stateVal ? this.addClass( value ) : this.removeClass( value ); } - if ( jQuery.isFunction( value ) ) { + if ( isFunction( value ) ) { return this.each( function( i ) { jQuery( this ).toggleClass( value.call( this, i, getClass( this ), stateVal ), @@ -7862,12 +8126,12 @@ jQuery.fn.extend( { return this.each( function() { var className, i, self, classNames; - if ( type === "string" ) { + if ( isValidValue ) { // Toggle individual class names i = 0; self = jQuery( this ); - classNames = value.match( rnothtmlwhite ) || []; + classNames = classesToArray( value ); while ( ( className = classNames[ i++ ] ) ) { @@ -7926,7 +8190,7 @@ var rreturn = /\r/g; jQuery.fn.extend( { val: function( value ) { - var hooks, ret, isFunction, + var hooks, ret, valueIsFunction, elem = this[ 0 ]; if ( !arguments.length ) { @@ -7955,7 +8219,7 @@ jQuery.fn.extend( { return; } - isFunction = jQuery.isFunction( value ); + valueIsFunction = isFunction( value ); return this.each( function( i ) { var val; @@ -7964,7 +8228,7 @@ jQuery.fn.extend( { return; } - if ( isFunction ) { + if ( valueIsFunction ) { val = value.call( this, i, jQuery( this ).val() ); } else { val = value; @@ -8106,18 +8370,24 @@ jQuery.each( [ "radio", "checkbox" ], function() { // Return jQuery for attributes-only inclusion -var rfocusMorph = /^(?:focusinfocus|focusoutblur)$/; +support.focusin = "onfocusin" in window; + + +var rfocusMorph = /^(?:focusinfocus|focusoutblur)$/, + stopPropagationCallback = function( e ) { + e.stopPropagation(); + }; jQuery.extend( jQuery.event, { trigger: function( event, data, elem, onlyHandlers ) { - var i, cur, tmp, bubbleType, ontype, handle, special, + var i, cur, tmp, bubbleType, ontype, handle, special, lastElement, eventPath = [ elem || document ], type = hasOwn.call( event, "type" ) ? event.type : event, namespaces = hasOwn.call( event, "namespace" ) ? event.namespace.split( "." ) : []; - cur = tmp = elem = elem || document; + cur = lastElement = tmp = elem = elem || document; // Don't do events on text and comment nodes if ( elem.nodeType === 3 || elem.nodeType === 8 ) { @@ -8169,7 +8439,7 @@ jQuery.extend( jQuery.event, { // Determine event propagation path in advance, per W3C events spec (#9951) // Bubble up to document, then to window; watch for a global ownerDocument var (#9724) - if ( !onlyHandlers && !special.noBubble && !jQuery.isWindow( elem ) ) { + if ( !onlyHandlers && !special.noBubble && !isWindow( elem ) ) { bubbleType = special.delegateType || type; if ( !rfocusMorph.test( bubbleType + type ) ) { @@ -8189,7 +8459,7 @@ jQuery.extend( jQuery.event, { // Fire handlers on the event path i = 0; while ( ( cur = eventPath[ i++ ] ) && !event.isPropagationStopped() ) { - + lastElement = cur; event.type = i > 1 ? bubbleType : special.bindType || type; @@ -8221,7 +8491,7 @@ jQuery.extend( jQuery.event, { // Call a native DOM method on the target with the same name as the event. // Don't do default actions on window, that's where global variables be (#6170) - if ( ontype && jQuery.isFunction( elem[ type ] ) && !jQuery.isWindow( elem ) ) { + if ( ontype && isFunction( elem[ type ] ) && !isWindow( elem ) ) { // Don't re-trigger an onFOO event when we call its FOO() method tmp = elem[ ontype ]; @@ -8232,7 +8502,17 @@ jQuery.extend( jQuery.event, { // Prevent re-triggering of the same event, since we already bubbled it above jQuery.event.triggered = type; + + if ( event.isPropagationStopped() ) { + lastElement.addEventListener( type, stopPropagationCallback ); + } + elem[ type ](); + + if ( event.isPropagationStopped() ) { + lastElement.removeEventListener( type, stopPropagationCallback ); + } + jQuery.event.triggered = undefined; if ( tmp ) { @@ -8278,31 +8558,6 @@ jQuery.fn.extend( { } ); -jQuery.each( ( "blur focus focusin focusout resize scroll click dblclick " + - "mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " + - "change select submit keydown keypress keyup contextmenu" ).split( " " ), - function( i, name ) { - - // Handle event binding - jQuery.fn[ name ] = function( data, fn ) { - return arguments.length > 0 ? - this.on( name, null, data, fn ) : - this.trigger( name ); - }; -} ); - -jQuery.fn.extend( { - hover: function( fnOver, fnOut ) { - return this.mouseenter( fnOver ).mouseleave( fnOut || fnOver ); - } -} ); - - - - -support.focusin = "onfocusin" in window; - - // Support: Firefox <=44 // Firefox doesn't have focus(in | out) events // Related ticket - https://bugzilla.mozilla.org/show_bug.cgi?id=687787 @@ -8346,7 +8601,7 @@ if ( !support.focusin ) { } var location = window.location; -var nonce = jQuery.now(); +var nonce = Date.now(); var rquery = ( /\?/ ); @@ -8404,7 +8659,7 @@ function buildParams( prefix, obj, traditional, add ) { } } ); - } else if ( !traditional && jQuery.type( obj ) === "object" ) { + } else if ( !traditional && toType( obj ) === "object" ) { // Serialize object item. for ( name in obj ) { @@ -8426,7 +8681,7 @@ jQuery.param = function( a, traditional ) { add = function( key, valueOrFunction ) { // If value is a function, invoke it and use its return value - var value = jQuery.isFunction( valueOrFunction ) ? + var value = isFunction( valueOrFunction ) ? valueOrFunction() : valueOrFunction; @@ -8434,6 +8689,10 @@ jQuery.param = function( a, traditional ) { encodeURIComponent( value == null ? "" : value ); }; + if ( a == null ) { + return ""; + } + // If an array was passed in, assume that it is an array of form elements. if ( Array.isArray( a ) || ( a.jquery && !jQuery.isPlainObject( a ) ) ) { @@ -8544,7 +8803,7 @@ function addToPrefiltersOrTransports( structure ) { i = 0, dataTypes = dataTypeExpression.toLowerCase().match( rnothtmlwhite ) || []; - if ( jQuery.isFunction( func ) ) { + if ( isFunction( func ) ) { // For each dataType in the dataTypeExpression while ( ( dataType = dataTypes[ i++ ] ) ) { @@ -8936,12 +9195,14 @@ jQuery.extend( { if ( !responseHeaders ) { responseHeaders = {}; while ( ( match = rheaders.exec( responseHeadersString ) ) ) { - responseHeaders[ match[ 1 ].toLowerCase() ] = match[ 2 ]; + responseHeaders[ match[ 1 ].toLowerCase() + " " ] = + ( responseHeaders[ match[ 1 ].toLowerCase() + " " ] || [] ) + .concat( match[ 2 ] ); } } - match = responseHeaders[ key.toLowerCase() ]; + match = responseHeaders[ key.toLowerCase() + " " ]; } - return match == null ? null : match; + return match == null ? null : match.join( ", " ); }, // Raw string @@ -9016,7 +9277,7 @@ jQuery.extend( { if ( s.crossDomain == null ) { urlAnchor = document.createElement( "a" ); - // Support: IE <=8 - 11, Edge 12 - 13 + // Support: IE <=8 - 11, Edge 12 - 15 // IE throws exception on accessing the href property if url is malformed, // e.g. http://example.com:80x/ try { @@ -9074,8 +9335,8 @@ jQuery.extend( { // Remember the hash so we can put it back uncached = s.url.slice( cacheURL.length ); - // If data is available, append data to url - if ( s.data ) { + // If data is available and should be processed, append data to url + if ( s.data && ( s.processData || typeof s.data === "string" ) ) { cacheURL += ( rquery.test( cacheURL ) ? "&" : "?" ) + s.data; // #9682: remove data so that it's not used in an eventual retry @@ -9312,7 +9573,7 @@ jQuery.each( [ "get", "post" ], function( i, method ) { jQuery[ method ] = function( url, data, callback, type ) { // Shift arguments if data argument was omitted - if ( jQuery.isFunction( data ) ) { + if ( isFunction( data ) ) { type = type || callback; callback = data; data = undefined; @@ -9330,7 +9591,7 @@ jQuery.each( [ "get", "post" ], function( i, method ) { } ); -jQuery._evalUrl = function( url ) { +jQuery._evalUrl = function( url, options ) { return jQuery.ajax( { url: url, @@ -9340,7 +9601,16 @@ jQuery._evalUrl = function( url ) { cache: true, async: false, global: false, - "throws": true + + // Only evaluate the response if it is successful (gh-4126) + // dataFilter is not invoked for failure responses, so using it instead + // of the default converter is kludgy but it works. + converters: { + "text script": function() {} + }, + dataFilter: function( response ) { + jQuery.globalEval( response, options ); + } } ); }; @@ -9350,7 +9620,7 @@ jQuery.fn.extend( { var wrap; if ( this[ 0 ] ) { - if ( jQuery.isFunction( html ) ) { + if ( isFunction( html ) ) { html = html.call( this[ 0 ] ); } @@ -9376,7 +9646,7 @@ jQuery.fn.extend( { }, wrapInner: function( html ) { - if ( jQuery.isFunction( html ) ) { + if ( isFunction( html ) ) { return this.each( function( i ) { jQuery( this ).wrapInner( html.call( this, i ) ); } ); @@ -9396,10 +9666,10 @@ jQuery.fn.extend( { }, wrap: function( html ) { - var isFunction = jQuery.isFunction( html ); + var htmlIsFunction = isFunction( html ); return this.each( function( i ) { - jQuery( this ).wrapAll( isFunction ? html.call( this, i ) : html ); + jQuery( this ).wrapAll( htmlIsFunction ? html.call( this, i ) : html ); } ); }, @@ -9491,7 +9761,8 @@ jQuery.ajaxTransport( function( options ) { return function() { if ( callback ) { callback = errorCallback = xhr.onload = - xhr.onerror = xhr.onabort = xhr.onreadystatechange = null; + xhr.onerror = xhr.onabort = xhr.ontimeout = + xhr.onreadystatechange = null; if ( type === "abort" ) { xhr.abort(); @@ -9531,7 +9802,7 @@ jQuery.ajaxTransport( function( options ) { // Listen to events xhr.onload = callback(); - errorCallback = xhr.onerror = callback( "error" ); + errorCallback = xhr.onerror = xhr.ontimeout = callback( "error" ); // Support: IE 9 only // Use onreadystatechange to replace onabort @@ -9622,24 +9893,21 @@ jQuery.ajaxPrefilter( "script", function( s ) { // Bind script tag hack transport jQuery.ajaxTransport( "script", function( s ) { - // This transport only deals with cross domain requests - if ( s.crossDomain ) { + // This transport only deals with cross domain or forced-by-attrs requests + if ( s.crossDomain || s.scriptAttrs ) { var script, callback; return { send: function( _, complete ) { - script = jQuery( " + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + +
+ +
+ + + + + + + + + + + + + + + + + +
+ + + + +
+
+
+
+ +
+

5.5. ui_framework package¶

+ +
+

5.5.2. Submodules¶

+
+
+

5.5.3. ui_framework.admin module¶

+

Defines the Django Admin model pages for this app .

+

Registers the models that will be available throgh the Djangpo Admin interface.

+

For more information see: +https://docs.djangoproject.com/en/2.2/ref/contrib/admin/

+
+
+

5.5.4. ui_framework.apps module¶

+

Django apps configuration for the ui_framework app.

+
+
+class ui_framework.apps.UiFrameworkConfig(app_name, app_module)¶
+

Bases: django.apps.config.AppConfig

+

General App config class. Currently defines the name of the app.

+
+
+name = 'ui_framework'¶
+
+ +
+ +
+
+

5.5.5. ui_framework.models module¶

+

Defines the Django models for this app.

+

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

+
+
+class ui_framework.models.BaseModel(*args, **kwargs)¶
+

Bases: django.db.models.base.Model

+

Base Model for the models of this app.

+
+
+class Meta¶
+

Bases: object

+

Define attributes of the Meta class.

+
+
+abstract = False¶
+

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

+
+ +
+ +
+
+creation_timestamp¶
+

Creation timestamp, autogenerated upon creation

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

Update timestamp, autogenerated upon creation and autoupdated on every update

+
+ +
+ +
+
+class ui_framework.models.View(*args, **kwargs)¶
+

Bases: ui_framework.models.BaseModel

+

View Model.

+
+
+exception DoesNotExist¶
+

Bases: django.core.exceptions.ObjectDoesNotExist

+
+ +
+
+exception MultipleObjectsReturned¶
+

Bases: django.core.exceptions.MultipleObjectsReturned

+
+ +
+
+data¶
+

The data that constitutes the View, stored as a JSON

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

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

+
+ +
+
+name¶
+

The name of the View. e.g ‘My View’

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

Accessor to the related objects manager on the reverse side of a +many-to-one relation.

+

In the example:

+
class Child(Model):
+    parent = ForeignKey(Parent, related_name='children')
+
+
+

Parent.children is a ReverseManyToOneDescriptor instance.

+

Most of the implementation is delegated to a dynamically defined manager +class built by create_forward_many_to_many_manager() defined below.

+
+ +
+
+workspaces¶
+

Accessor to the related objects manager on the forward and reverse sides of +a many-to-many relation.

+

In the example:

+
class Pizza(Model):
+    toppings = ManyToManyField(Topping, related_name='pizzas')
+
+
+

Pizza.toppings and Topping.pizzas are ManyToManyDescriptor +instances.

+

Most of the implementation is delegated to a dynamically defined manager +class built by create_forward_many_to_many_manager() defined below.

+
+ +
+ +
+
+class ui_framework.models.Workspace(*args, **kwargs)¶
+

Bases: ui_framework.models.BaseModel

+

Workspace Model.

+
+
+exception DoesNotExist¶
+

Bases: django.core.exceptions.ObjectDoesNotExist

+
+ +
+
+exception MultipleObjectsReturned¶
+

Bases: django.core.exceptions.MultipleObjectsReturned

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

Return the views sorted by the sort_value of their corresponding WorkView.

+
+
list:

List of View objects associated to this Workspace

+
+
+
+ +
+
+static has_read_permission(request)¶
+
+ +
+
+id¶
+

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

+
+ +
+
+name¶
+

The name of the Workspace. e.g ‘My Workspace’

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

Accessor to the related objects manager on the forward and reverse sides of +a many-to-many relation.

+

In the example:

+
class Pizza(Model):
+    toppings = ManyToManyField(Topping, related_name='pizzas')
+
+
+

Pizza.toppings and Topping.pizzas are ManyToManyDescriptor +instances.

+

Most of the implementation is delegated to a dynamically defined manager +class built by create_forward_many_to_many_manager() defined below.

+
+ +
+
+workspace_views¶
+

Accessor to the related objects manager on the reverse side of a +many-to-one relation.

+

In the example:

+
class Child(Model):
+    parent = ForeignKey(Parent, related_name='children')
+
+
+

Parent.children is a ReverseManyToOneDescriptor instance.

+

Most of the implementation is delegated to a dynamically defined manager +class built by create_forward_many_to_many_manager() defined below.

+
+ +
+ +
+
+class ui_framework.models.WorkspaceView(*args, **kwargs)¶
+

Bases: ui_framework.models.BaseModel

+

WorkspaceView Model, that relates a Works with a View.

+
+
+exception DoesNotExist¶
+

Bases: django.core.exceptions.ObjectDoesNotExist

+
+ +
+
+exception MultipleObjectsReturned¶
+

Bases: django.core.exceptions.MultipleObjectsReturned

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

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

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

Order of the View within the Workspace.

+
+ +
+
+view¶
+

The corresponding View

+
+ +
+
+view_id¶
+

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

+
+ +
+
+view_name¶
+

The custom name for the View within the Workspace

+
+ +
+
+workspace¶
+

The corresponding Workspace

+
+ +
+
+workspace_id¶
+

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

+
+ +
+ +
+
+

5.5.6. ui_framework.serializers module¶

+

Defines the serializer used by the REST API exposed by this app.

+
+
+class ui_framework.serializers.ViewSerializer(instance=None, data=<class 'rest_framework.fields.empty'>, **kwargs)¶
+

Bases: rest_framework.serializers.ModelSerializer

+

Serializer for the View model.

+
+
+class Meta¶
+

Bases: object

+

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

+
+
+fields = '__all__'¶
+
+ +
+
+model¶
+

alias of ui_framework.models.View

+
+ +
+ +
+ +
+
+class ui_framework.serializers.WorkspaceSerializer(instance=None, data=<class 'rest_framework.fields.empty'>, **kwargs)¶
+

Bases: rest_framework.serializers.ModelSerializer

+

Serializer for the Workspace model.

+
+
+class Meta¶
+

Bases: object

+

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

+
+
+fields = '__all__'¶
+
+ +
+
+model¶
+

alias of ui_framework.models.Workspace

+
+ +
+ +
+ +
+
+class ui_framework.serializers.WorkspaceViewSerializer(instance=None, data=<class 'rest_framework.fields.empty'>, **kwargs)¶
+

Bases: rest_framework.serializers.ModelSerializer

+

Serializer for the WorkspaceView model.

+
+
+class Meta¶
+

Bases: object

+

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

+
+
+fields = '__all__'¶
+
+ +
+
+model¶
+

alias of ui_framework.models.WorkspaceView

+
+ +
+ +
+ +
+
+

5.5.7. ui_framework.urls module¶

+

API app URL Configuration.

+
+
The urlpatterns list routes URLs to views. For more information please see:

https://docs.djangoproject.com/en/2.2/topics/http/urls/

+
+
+
+

5.5.7.1. Examples¶

+
+
Function views
    +
  1. Add an import: from my_app import views

  2. +
  3. Add a URL to urlpatterns: path(‘’, views.home, name=’home’)

  4. +
+
+
Class-based views
    +
  1. Add an import: from other_app.views import Home

  2. +
  3. Add a URL to urlpatterns: path(‘’, Home.as_view(), name=’home’)

  4. +
+
+
Including another URLconf
    +
  1. Import the include() function: from django.urls import include, path

  2. +
  3. Add a URL to urlpatterns: path(‘blog/’, include(‘blog.urls’))

  4. +
+
+
+
+
+
+

5.5.8. ui_framework.views module¶

+

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

+
+
+class ui_framework.views.ViewViewSet(**kwargs)¶
+

Bases: rest_framework.viewsets.ModelViewSet

+

Define API endpoints for the View model.

+
+
+basename = None¶
+
+ +
+
+description = None¶
+
+ +
+
+detail = None¶
+
+ +
+
+name = None¶
+
+ +
+
+queryset = <QuerySet []>¶
+

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

+
+ +
+
+serializer_class¶
+

alias of ui_framework.serializers.ViewSerializer

+
+ +
+
+suffix = None¶
+
+ +
+ +
+
+class ui_framework.views.WorkspaceViewSet(**kwargs)¶
+

Bases: rest_framework.viewsets.ModelViewSet

+

Define API endpoints for the Workspace model.

+
+
+basename = None¶
+
+ +
+
+description = None¶
+
+ +
+
+detail = None¶
+
+ +
+
+name = None¶
+
+ +
+
+queryset = <QuerySet []>¶
+

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

+
+ +
+
+serializer_class¶
+

alias of ui_framework.serializers.WorkspaceSerializer

+
+ +
+
+suffix = None¶
+
+ +
+ +
+
+class ui_framework.views.WorkspaceViewViewSet(**kwargs)¶
+

Bases: rest_framework.viewsets.ModelViewSet

+

Define API endpoints for the WorkspaceView model.

+
+
+basename = None¶
+
+ +
+
+description = None¶
+
+ +
+
+detail = None¶
+
+ +
+
+name = None¶
+
+ +
+
+queryset = <QuerySet []>¶
+

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

+
+ +
+
+serializer_class¶
+

alias of ui_framework.serializers.WorkspaceViewSerializer

+
+ +
+
+suffix = None¶
+
+ +
+ +
+
+

5.5.9. Module contents¶

+
+
+ + +
+ +
+ + +
+
+ +
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/docs/html/apidoc/ui_framework.tests.html b/docs/html/apidoc/ui_framework.tests.html new file mode 100644 index 00000000..0b3ff363 --- /dev/null +++ b/docs/html/apidoc/ui_framework.tests.html @@ -0,0 +1,532 @@ + + + + + + + + + + + 5.5.1.1. ui_framework.tests package — LOVE-manager 0.0.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + +
+ +
+ + + + + + + + + + + + + + + + + +
+ + + + +
+
+
+
+ +
+

5.5.1.1. ui_framework.tests package¶

+
+

5.5.1.1.1. Submodules¶

+
+
+

5.5.1.1.2. ui_framework.tests.tests_api module¶

+

Test the UI Framework API.

+
+
+class ui_framework.tests.tests_api.AuthorizedCrudTestCase(methodName='runTest')¶
+

Bases: ui_framework.tests.utils.BaseTestCase

+

Test that authorized users can use the CRUD API.

+
+
+setUp()¶
+

Set testcase. Inherits from utils.BaseTestCase.

+
+ +
+
+test_authorized_create_objects()¶
+

Test that authorized users can create objects through the API.

+
+ +
+
+test_authorized_delete_objects()¶
+

Test that authorized users can dalete objects through the API.

+
+ +
+
+test_authorized_list_objects()¶
+

Test that authorized users can retrieve the list of objects through the API.

+
+ +
+
+test_authorized_retrieve_objects()¶
+

Test that authorized users can retrieve objects through the API.

+
+ +
+
+test_authorized_update_objects()¶
+

Test that authorized users can update objects through the API.

+
+ +
+ +
+
+class ui_framework.tests.tests_api.UnauthenticatedCrudTestCase(methodName='runTest')¶
+

Bases: ui_framework.tests.utils.BaseTestCase

+

Test that unauthenticated users cannot use the CRUD API.

+
+
+setUp()¶
+

Set testcase. Inherits from utils.BaseTestCase.

+
+ +
+
+test_unauthenticated_create_objects()¶
+

Test that unauthenticated users cannot create objects through the API.

+
+ +
+
+test_unauthenticated_delete_objects()¶
+

Test that unauthenticated users cannot dalete objects through the API.

+
+ +
+
+test_unauthenticated_list_objects()¶
+

Test that unauthenticated users cannot retrieve the list of objects through the API.

+
+ +
+
+test_unauthenticated_retrieve_objects()¶
+

Test that unauthenticated users cannot retrieve objects through the API.

+
+ +
+
+test_unauthenticated_update_objects()¶
+

Test that unauthenticated users cannot update objects through the API.

+
+ +
+ +
+
+class ui_framework.tests.tests_api.UnauthorizedCrudTestCase(methodName='runTest')¶
+

Bases: ui_framework.tests.utils.BaseTestCase

+

Test that unauthorized users cannot use the CRUD API.

+
+
+setUp()¶
+

Set testcase. Inherits from utils.BaseTestCase.

+
+ +
+
+test_unauthorized_create_objects()¶
+

Test that unauthorized users cannot create objects through the API.

+
+ +
+
+test_unauthorized_delete_objects()¶
+

Test that unauthorized users cannot dalete objects through the API.

+
+ +
+
+test_unauthorized_list_objects()¶
+

Test that unauthorized users can still retrieve the list of objects through the API.

+
+ +
+
+test_unauthorized_retrieve_objects()¶
+

Test that unauthorized users can still retrieve objects through the API.

+
+ +
+
+test_unauthorized_update_objects()¶
+

Test that unauthorized users cannot update objects through the API.

+
+ +
+ +
+
+

5.5.1.1.3. ui_framework.tests.tests_models module¶

+

Test the models.

+
+
+class ui_framework.tests.tests_models.ViewModelTestCase(methodName='runTest')¶
+

Bases: django.test.testcases.TestCase

+

Test the view model.

+
+
+setUp()¶
+

Testcase setup.

+
+ +
+
+test_create_view()¶
+

Test that a view can be created in the database.

+
+ +
+
+test_delete_view()¶
+

Test that a view can be deleted from the database.

+
+ +
+
+test_retrieve_view()¶
+

Test that a view can be retrieved from the database.

+
+ +
+
+test_update_view()¶
+

Test that a view can be updated in the database.

+
+ +
+ +
+
+class ui_framework.tests.tests_models.WorkspaceAndViewsRelationsTestCase(methodName='runTest')¶
+

Bases: django.test.testcases.TestCase

+

Test the relationships vetween Workspace, View and WorkspaceView models.

+
+
+setUp()¶
+

Testcase setup.

+
+ +
+
+test_add_and_get_views_to_workspace()¶
+

Test that Views can be added/retrieved to/from a Workspace in the DB.

+
+ +
+
+test_get_workspaces_from_a_view()¶
+

Test that a View can retrieve its Workspaces from the DB.

+
+ +
+ +
+
+class ui_framework.tests.tests_models.WorkspaceModelTestCase(methodName='runTest')¶
+

Bases: django.test.testcases.TestCase

+

Test the workspace model.

+
+
+setUp()¶
+

Testcase setup.

+
+ +
+
+test_create_workspace()¶
+

Test that a workspace can be created in the database.

+
+ +
+
+test_delete_workspace()¶
+

Test that a workspace can be deleted from the database.

+
+ +
+
+test_retrieve_workspace()¶
+

Test that a workspace can be retrieved from the database.

+
+ +
+
+test_update_workspace()¶
+

Test that a workspace can be updated in the database.

+
+ +
+ +
+
+class ui_framework.tests.tests_models.WorkspaceViewModelTestCase(methodName='runTest')¶
+

Bases: django.test.testcases.TestCase

+

Test the workspace_view model.

+
+
+setUp()¶
+

Testcase setup.

+
+ +
+
+test_create_workspace_view()¶
+

Test that a workspace_view can be created in the database.

+
+ +
+
+test_delete_workspace_view()¶
+

Test that a workspace_view can be deleted from the database.

+
+ +
+
+test_retrieve_workspace_view()¶
+

Test that a workspace_view can be retrieved from the database.

+
+ +
+
+test_update_workspace_view()¶
+

Test that a workspace_view can be updated in the database.

+
+ +
+ +
+
+

5.5.1.1.4. ui_framework.tests.utils module¶

+

Utilities for testing purposes.

+
+
+class ui_framework.tests.utils.BaseTestCase(methodName='runTest')¶
+

Bases: django.test.testcases.TestCase

+

Base TestCase used to define a common setUp.

+
+
+setUp()¶
+

Set the base testcase.

+

We start with 3 workspaces and 4 views and we add view_i and view_i+1 to workspace_i

+
+ +
+ +
+
+ui_framework.tests.utils.get_dict(obj)¶
+

Return a dictionary with the fields of a given object.

+
+ +
+
+

5.5.1.1.5. Module contents¶

+
+
+ + +
+ +
+ + +
+
+ +
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/docs/html/genindex.html b/docs/html/genindex.html index 40f64739..7680a92a 100644 --- a/docs/html/genindex.html +++ b/docs/html/genindex.html @@ -153,6 +153,7 @@

Index

A + | B | C | D | E @@ -165,16 +166,20 @@

Index

| N | O | P + | Q | R | S | T | U | V + | W

A

+
+ +

B

+ + +
@@ -215,6 +244,8 @@

C

@@ -223,6 +254,26 @@

C

D

+ @@ -244,21 +295,73 @@

F

G

@@ -287,6 +392,12 @@

I

@@ -317,10 +428,10 @@

M

  • manager.settings (module)
  • - - + @@ -338,7 +455,21 @@

    N

    @@ -349,6 +480,12 @@

    O

    @@ -370,6 +507,20 @@

    P

    +

    Q

    + + +
    +

    R

      @@ -381,7 +532,37 @@

      R

      S

      + -

      T

      + -
      • test_user_validate_token_fail() (api.tests.tests_auth_api.AuthApiTestCase method)
      • Token (class in api.models) @@ -443,10 +688,40 @@

        T

        U

      • 3. How it works
      • diff --git a/docs/html/modules/how_to_use_it.html b/docs/html/modules/how_to_use_it.html index 242aeff8..4c321bb0 100644 --- a/docs/html/modules/how_to_use_it.html +++ b/docs/html/modules/how_to_use_it.html @@ -105,6 +105,7 @@ +
      • 2.4. UI Framework
      • 3. How it works
      • @@ -378,6 +379,26 @@

        2.3.2.3. Command messages +
        +

        2.4. UI Framework¶

        +

        The UI Framework backend is composed of 3 models:

        +
        +
          +
        • Workspace: represents a workspace, composed by different views

        • +
        • View: represents a view, all the data of the view is contained in JSON format in the data field of the view

        • +
        • WorkspaceView: relates a Workspace and a View, it is the intermediary table of the many-to-many relationship between Workspace and View.

        • +
        +
        +

        Currently the API provides a standard REST api for these models. +For more info you can either:

        +
        +
          +
        • Use the browsable API available in: <IP>/manager/ui_framework/

        • +
        • See the apidoc in Swagger format, available in: <IP>/manager/apidoc/swagger/

        • +
        • See the apidoc in ReDoc format, available in: <IP>/manager/apidoc/redoc/

        • +
        +
        +
        diff --git a/docs/html/modules/readme_link.html b/docs/html/modules/readme_link.html index d933f1d4..2a73fa60 100644 --- a/docs/html/modules/readme_link.html +++ b/docs/html/modules/readme_link.html @@ -178,16 +178,24 @@

        4.1.1. Initialize Environment variablesADMIN_USER_PASS: password for the default admin user, which has every permission.

      • USER_USER_PASS: password for the default user user, which has readonly permissions and cannot execute commands.

      • CMD_USER_PASS: password for the default cmd user, which has readonly permissions but can execute commands.

      • -
      • LOVE_MANAGER_REDIS_HOST: the location of the redis host that implements the Channels Layer.

      • -
      • REDIS_PASS: the password that the LOVE-manager needs tio use to connect with redis.

      • +
      • REDIS_HOST: the location of the redis host that implements the Channels Layer.

      • +
      • REDIS_PASS: the password that the LOVE-manager needs to use to connect with redis.

      • PROCESS_CONNECTION_PASS: the password that the LOVE-producer will use to establish a websocket connection with the LOVE-manager.

      • +
      • DB_ENGINE: describe which database engine should be used. If its value is postgresql Postgres will be used, otherwise it will use Sqlite3.

      • +
      • DB_NAME: defines the name of the Database. Only used if DB_ENGINE=postgresql.

      • +
      • DB_USER: defines the user of the Database. Only used if DB_ENGINE=postgresql.

      • +
      • DB_PASS: defines the password of the Database. Only used if DB_ENGINE=postgresql.

      • +
      • DB_HOST: defines the host of the Database. Only used if DB_ENGINE=postgresql.

      • +
      • DB_PORT: defines the port of the Database. Only used if DB_ENGINE=postgresql.

      • +
      • NO_DEBUG: defines wether or not the LOVE-.manager will be run using Django’s debug mode. If the variable is defined, then Debug mode will be off.

      • +
      • SECRET_KEY: overrides Django’s SECRET_KEY, if not defined the default value (public in this repo) will be used.

      • AUTH_LDAP_SERVER_URI: (deprecated) the location of the LDAP authentication server. No LDAP server is used if this variable is empty

      • 4.2. Local load for development¶

        -

        We provide a docker image and a docker-compose file in order to load the LOVE-manager locally for development purposes, i.e. run tests and build documentation.

        +

        We provide a docker image and a docker-compose file in order to load the LOVE-manager with a Postgres database locally, for development purposes, such as run tests and build documentation.

        This docker-compose does not copy the code into the image, but instead it mounts the repository inside the image, this way you can edit the code from outside the docker container with no need to rebuild or restart.

        4.2.1. Load and get into the docker image¶

        diff --git a/docs/html/objects.inv b/docs/html/objects.inv index 647aa63c..8cf144a3 100644 Binary files a/docs/html/objects.inv and b/docs/html/objects.inv differ diff --git a/docs/html/py-modindex.html b/docs/html/py-modindex.html index 87f223af..9dbdb5a9 100644 --- a/docs/html/py-modindex.html +++ b/docs/html/py-modindex.html @@ -154,7 +154,8 @@

        Python Module Index

        a | m | - s + s | + u
        @@ -280,6 +281,65 @@

        Python Module Index

        + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
            subscription.routing
         
        + u
        + ui_framework +
            + ui_framework.admin +
            + ui_framework.apps +
            + ui_framework.models +
            + ui_framework.serializers +
            + ui_framework.tests +
            + ui_framework.tests.tests_api +
            + ui_framework.tests.tests_models +
            + ui_framework.tests.utils +
            + ui_framework.urls +
            + ui_framework.views +
        diff --git a/docs/html/searchindex.js b/docs/html/searchindex.js index ce721820..91b86b58 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","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.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","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,"-"]},"api.apps":{ApiConfig:[0,1,1,""]},"api.apps.ApiConfig":{name:[0,2,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.serializers":{UserSerializer:[0,1,1,""]},"api.serializers.UserSerializer":{Meta:[0,1,1,""]},"api.serializers.UserSerializer.Meta":{fields:[0,2,1,""],model:[0,2,1,""]},"api.tests":{tests_auth_api:[1,0,0,"-"]},"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_validate_token:[1,3,1,""],test_user_validate_token_fail:[1,3,1,""]},"api.urls":{path:[0,5,1,""]},"api.views":{CustomObtainAuthToken:[0,1,1,""],logout:[0,5,1,""],validate_token:[0,5,1,""]},"api.views.CustomObtainAuthToken":{post:[0,3,1,""]},"manager.urls":{path:[3,5,1,""]},"subscription.auth":{TokenAuthMiddleware:[5,1,1,""]},"subscription.consumers":{SubscriptionConsumer:[5,1,1,""]},"subscription.consumers.SubscriptionConsumer":{connect:[5,3,1,""],disconnect:[5,3,1,""],handle_data_message:[5,3,1,""],handle_subscription_message:[5,3,1,""],receive_json:[5,3,1,""],subscription_ack:[5,3,1,""],subscription_all_data:[5,3,1,""],subscription_data:[5,3,1,""]},api:{admin:[0,0,0,"-"],apps:[0,0,0,"-"],authentication:[0,0,0,"-"],middleware:[0,0,0,"-"],models:[0,0,0,"-"],serializers:[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,"-"],wsgi:[3,0,0,"-"]},subscription:{auth:[5,0,0,"-"],consumers:[5,0,0,"-"],routing:[5,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"]},objtypes:{"0":"py:module","1":"py:class","2":"py:attribute","3":"py:method","4":"py:exception","5":"py:function"},terms:{"boolean":0,"case":7,"class":[0,1,3,5],"default":[7,10],"function":[0,3],"import":[0,3],"int":0,"new":8,"return":[0,8],"true":[0,8],"while":7,And:0,For:[0,3,7,9],The:[0,3,5,7,8,9,10],Then:7,These:[4,7],Use:[0,6],abov:7,access:[7,9],ack:5,acknowledg:[8,9],act:[7,10],action:7,add:[0,3],additt:0,admin:[3,4,10],admin_user_pass:10,alia:0,all:[5,7,9,10],also:[7,9],ani:9,anoth:[0,3],api:[4,6,8,9],apiconfig:0,apidoc:[6,7],app:[4,7],app_modul:0,app_nam:0,appconfig:0,append:8,appli:7,applic:[2,3,5,7,8,10],arg:[0,5],argument:0,as_view:[0,3],asgi:4,asgi_appl:3,associ:5,async:5,asynchron:8,asyncjsonwebsocketconsum:5,auth:[0,4],auth_ldap_server_uri:10,authapitestcas:1,authent:[1,4,5,6,7,9,10],authenticate_credenti:0,authenticationfail:0,author:[0,8,9],authtoken:0,avail:0,back:9,base:[0,1,3,5,8,9],bash:10,basic:7,becaus:7,been:0,befor:[0,10],behind:8,below:7,between:9,blog:[0,3],both:9,call:5,callabl:3,can:[0,1,7,10],cannot:[1,10],categori:[5,8],channel:[3,5,9,10],character:8,charg:7,check:[0,9],classmethod:0,client:[7,8,9],close_cod:5,cmd:[5,8,10],cmd_user_pass:10,code:[0,6,10],com:[0,3],command:[5,9,10],commandpath:[5,8],commun:[7,9],compar:7,compon:7,compos:[7,10],config:[0,3],configur:[0,3,7,10],confirm:8,connect:[5,6,7,9,10],consum:[4,8,9],contain:[0,5,7,10],content:[4,6,8],contrib:0,copi:10,core:0,correspond:[0,5,7,10],could:8,creat:[0,7],create_doc:10,credenti:[1,7,8,9],csc:[5,8],currenlti:0,current:[0,7,8],custom:[0,5],customobtainauthtoken:0,data1:8,data2:8,data:[0,5,7,8,9],databas:[0,7],datetimefield:0,defer:0,defin:[0,1,3,5,7,10],delet:[0,1,8],deploy:[3,10],deprec:10,describ:8,detail:[7,8,9],dev:7,develop:[6,7],dict:[0,5],dictionari:[0,5],differ:[1,7,8,9],disconnect:5,divid:7,django:[0,1,2,3,5,7,9,10],djangoproject:[0,3],djangpo:0,doc:[0,3],docker:7,docsrc:10,doe:10,doesnotexist:0,done:[7,8],drf:[0,9],each:[0,1,7,8],edit:10,email:0,empti:[0,10],end:8,endpoint:[0,7],entrypoint:3,env:10,environ:9,error:7,establish:[7,8,9,10],etc:7,event:[5,9],everi:[7,9,10],exampl:[4,6],except:0,exec:10,execut:[0,2,7,10],execute_command:8,expect:[5,8],expir:[0,1],expires_in:0,expiringtokenauthent:0,explain:7,expos:[0,3],fail:[0,1],fals:[0,8],field:0,figur:[7,9],file:[3,6,7],first:0,folder:10,follow:[5,7,8,9,10],foreignkei:0,format:[0,5],forward:[7,9],framework:9,from:[0,3,5,7,8,10],frontend:[7,9,10],full:3,further:9,gener:[0,3,5],get:8,get_next_by_cr:0,get_previous_by_cr:0,get_respons:0,gettokenmiddlewar:0,github:10,given:[0,7,8],globalpermiss:0,group:[5,6,7,9],handl:[0,5,7,9],handle_data_messag:5,handle_subscription_messag:5,handler:7,has:[0,7,9,10],have:[0,5,8,9],header:8,here:10,home:[0,3],host:10,how:[6,9],howto:3,html:10,http:[0,3,7,8,10],identifi:8,imag:7,implement:[7,10],includ:[0,3,9],incom:5,index:[0,6,10],inform:[0,3],ini:7,initi:7,inner:5,insid:[7,10],instanc:[0,5,7,8,9],instead:10,instruct:10,integr:10,intend:[5,8],interfac:0,intermediari:9,invalid:[0,1,7],is_next:0,is_token_expir:0,its:7,join:5,json:[0,5,8],kei:0,key1:8,key2:8,keyword:0,kwarg:[0,3,5],latter:[7,9],layer:10,ldap:10,leav:5,less:0,level:3,librari:7,list:[0,3],load:[0,6],local:6,locat:10,login:7,logout:[0,1],love:9,love_manager_redis_host:10,lsst:[9,10],mai:8,main:[2,7],make:[5,7],manag:[0,4,9,10],map:0,mechan:8,messag:[3,5,7,9],meta:0,methodnam:1,middlewar:[4,5,10],migrat:7,model:[4,7],modelseri:0,modul:[4,6,7],more:[0,3,7,9],mostli:7,mount:10,move:10,multipleobjectsreturn:0,must:[7,8,10],my_app:[0,3],name:[0,1,3,8],necessari:8,need:10,never:7,none:[0,3],number:[0,8],object:[0,5],objectdoesnotexist:0,obtain:0,obtainauthtoken:0,onc:[7,9,10],one:0,onli:[5,7],oper:[8,9],option:[5,8],order:[0,7,8,10],organ:6,other:[0,7,8],other_app:[0,3],otherwis:5,our:0,out:10,outsid:10,over:7,overview:6,packag:[4,6],page:[0,6],pair:8,param1:[5,8],param2:[5,8],param:[5,8],paramet:8,pars:5,part:[6,9],particular:[5,7,8,9],pass:8,password:[1,7,10],path:[0,3],pattern:[0,3],pend:5,permiss:[0,7,8,10],pipe:9,pleas:[0,3,7,9,10],post:[0,8],process_connection_pass:10,produc:[7,9,10],project:[0,3,4,9,10],provid:[0,7,8,9,10],purpos:[7,10],pytest:[7,10],python:[7,9],queri:0,rais:0,read:0,readm:6,readonli:10,reason:8,rebuild:10,receiv:[1,5,7,9],receive_json:5,recept:5,recommend:10,redi:10,redirect:9,redis_pass:10,ref:[0,3],refer:7,regist:0,reject:5,relat:0,remain:0,remian:0,remov:0,repli:[8,9],repo:10,repositori:10,repres:8,request:[0,1,7],requet:0,requier:7,requir:[7,8],resolv:[0,3],respect:7,respond:7,respons:[0,8],rest:[0,7,9],rest_framework:0,restart:10,rout:[0,4,7],routepattern:[0,3],rule:[3,5],run:[3,7],runserv:7,runtest:1,salindex:[5,8],script:7,scriptqueu:[5,8],search:[6,7],second:0,section:[7,9],see:[0,3,7,10],self:0,send:[5,7,9],sent:[8,9],serial:4,server:10,set:4,setup:1,shown:9,similarli:7,simpl:7,softwar:7,some:[7,10],sourc:8,specifi:8,sqlite3:7,src:10,stablish:8,startproject:[0,3],state:0,statu:[0,8],store:7,stream1:[5,8],stream2:5,stream:[5,8],string:0,submodul:4,subpackag:4,subscirpt:5,subscrib:[5,7,8,9],subscript:[4,6,7],subscription_ack:5,subscription_all_data:5,subscription_data:5,subscriptionconsum:5,succesfulli:8,success:8,suit:1,sure:7,system:[6,8],tel1:7,tel2:7,telemetri:[5,7,9],test:[0,4,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_validate_token:1,test_user_validate_token_fail:1,testcas:1,tests_auth_api:[0,4],than:0,thei:[0,8],them:8,therefor:9,thi:[0,3,7,8,9,10],thorugh:1,those:5,throgh:0,through:[7,8,9],time:[0,1],tio:10,token:[0,1,5,7,9],token_expire_handl:0,tokenauthent:0,tokenauthmiddlewar:5,tool:10,topic:[0,3],transfer:8,tri:7,trigger:8,turn:7,twie:1,txt:7,type:8,uniqu:7,unsubscrib:[5,8],unsubscript:5,unus:0,url:[4,7,8],urlconf:[0,3],urlpattern:[0,3],use:[0,5,6,10],used:[0,5,7,10],user:[0,1,5,7,10],user_data:8,user_user_pass:10,usernam:[0,7,8],userseri:0,uses:9,using:[0,1,3,9],usr:10,valid:[0,1,7],validate_token:0,valu:[0,3,7,8],value1:[5,8],value2:[5,8],variabl:[3,8],via:8,view:[3,4],visual:9,wai:[8,10],websocket:[3,5,6,7,9,10],when:[0,7,9],where:[8,9,10],which:[7,9,10],who:9,whole:3,work:[6,9],workflow:9,wrapper:0,written:9,wsgi:4,you:10},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","Welcome to LOVE-manager\u2019s documentation!","3. How it works","2. How to use it","1. Overview","4. Readme File"],titleterms:{Use:10,accept:8,admin:0,api:[0,1,7],apidoc:4,app:0,asgi:3,auth:5,authent:[0,8],build:10,channel:7,code:7,command:8,connect:8,consum:[5,7],content:[0,1,3,5],develop:10,docker:10,document:[6,10],environ:10,event:8,exampl:[0,3,7],file:10,get:10,group:8,how:[7,8],imag:10,indic:6,initi:10,layer:7,load:10,local:10,logout:8,love:[6,7,8,10],manag:[2,3,6,7,8],messag:8,middlewar:0,model:0,modul:[0,1,2,3,5],organ:7,overview:9,packag:[0,1,3,5],part:[7,10],password:8,readm:10,request:8,rout:[3,5],run:10,scheme:8,serial:0,set:3,submodul:[0,1,3,5],subpackag:0,subscript:[5,8],system:10,tabl:6,telemetri:8,test:[1,10],tests_auth_api:1,token:8,url:[0,3],use:8,user:8,valid:8,variabl:10,view:0,websocket:8,welcom:6,work:7,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.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,""]},"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.serializers":{UserSerializer:[0,1,1,""]},"api.serializers.UserSerializer":{Meta:[0,1,1,""]},"api.serializers.UserSerializer.Meta":{fields:[0,2,1,""],model:[0,2,1,""]},"api.tests":{tests_auth_api:[1,0,0,"-"]},"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_validate_token:[1,3,1,""],test_user_validate_token_fail:[1,3,1,""]},"api.urls":{path:[0,5,1,""]},"api.views":{CustomObtainAuthToken:[0,1,1,""],logout:[0,5,1,""],validate_token:[0,5,1,""]},"api.views.CustomObtainAuthToken":{post:[0,3,1,""]},"manager.urls":{path:[3,5,1,""]},"subscription.auth":{TokenAuthMiddleware:[5,1,1,""]},"subscription.consumers":{SubscriptionConsumer:[5,1,1,""]},"subscription.consumers.SubscriptionConsumer":{connect:[5,3,1,""],disconnect:[5,3,1,""],handle_data_message:[5,3,1,""],handle_subscription_message:[5,3,1,""],receive_json:[5,3,1,""],subscription_ack:[5,3,1,""],subscription_all_data:[5,3,1,""],subscription_data:[5,3,1,""]},"ui_framework.apps":{UiFrameworkConfig:[6,1,1,""]},"ui_framework.apps.UiFrameworkConfig":{name:[6,2,1,""]},"ui_framework.models":{BaseModel:[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.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,""],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":{ViewSerializer:[6,1,1,""],WorkspaceSerializer:[6,1,1,""],WorkspaceViewSerializer:[6,1,1,""]},"ui_framework.serializers.ViewSerializer":{Meta:[6,1,1,""]},"ui_framework.serializers.ViewSerializer.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.tests":{tests_api:[7,0,0,"-"],tests_models:[7,0,0,"-"],utils:[7,0,0,"-"]},"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_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,""],serializer_class:[6,2,1,""],suffix:[6,2,1,""]},"ui_framework.views.WorkspaceViewSet":{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,""]},"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,"-"],serializers:[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,"-"],wsgi:[3,0,0,"-"]},subscription:{auth:[5,0,0,"-"],consumers:[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,"-"],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"]},objtypes:{"0":"py:module","1":"py:class","2":"py:attribute","3":"py:method","4":"py:exception","5":"py:function"},terms:{"abstract":6,"boolean":0,"case":9,"class":[0,1,3,5,6,7],"default":[9,12],"function":[0,3,6],"import":[0,3,6],"int":0,"new":10,"public":12,"return":[0,6,7,10],"static":6,"true":[0,6,10],"while":9,And:0,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],__all__:6,abov:9,access:[6,9,11],accessor:6,ack:5,acknowledg:[10,11],act:[9,12],action:9,add:[0,3,6,7],added:7,additt:0,admin:[3,4,12],admin_user_pass:12,alia:[0,6],all:[5,9,10,11,12],also:[9,11],ani:11,anoth:[0,3,6],api:[4,6,7,8,10,11],apiconfig:0,apidoc:[8,9,10],app:[4,9],app_modul:[0,6],app_nam:[0,6],appconfig:[0,6],append:10,appli:9,applic:[2,3,5,9,10,12],arg:[0,5,6],argument:0,as_view:[0,3,6],asgi:4,asgi_appl:3,associ:[5,6],async:5,asynchron:10,asyncjsonwebsocketconsum:5,attribut:6,auth:[0,4],auth_ldap_server_uri:12,authapitestcas:1,authent:[1,4,5,8,9,11,12],authenticate_credenti:0,authenticationfail:0,author:[0,7,10,11],authorizedcrudtestcas:7,authtoken:0,autogener:6,autoupd:6,avail:[0,6,10],back:11,backend:10,base:[0,1,3,5,6,7,10,11],basemodel:6,basenam:6,basetestcas:7,bash:12,basic:9,becaus:9,been:0,befor:[0,12],behind:10,below:[6,9],between:[10,11],blog:[0,3,6],both:11,browsabl:10,built:6,call:5,callabl:3,can:[0,1,7,9,10,12],cannot:[1,7,12],categori:[5,10],channel:[3,5,11,12],character:10,charg:9,check:[0,11],child:6,children:6,classmethod:0,client:[9,10,11],close_cod:5,cmd:[5,10,12],cmd_user_pass:12,code:[0,8,12],com:[0,3,6],command:[5,11,12],commandpath:[5,10],common:7,commun:[9,11],compar:9,compon:9,compos:[9,10,12],config:[0,3,6],configur:[0,3,6,9,12],confirm:10,connect:[5,8,9,11,12],constitut:6,consum:[4,10,11],contain:[0,5,9,10,12],content:[4,8,10],contrib:[0,6],copi:12,core:[0,6],correspond:[0,5,6,9,12],could:10,creat:[0,7,9],create_doc:12,create_forward_many_to_many_manag:6,creation:6,creation_timestamp:6,credenti:[1,9,10,11],crud:7,csc:[5,10],currenlti:0,current:[0,6,9,10],custom:[0,5,6],customobtainauthtoken:0,dalet:7,data1:10,data2:10,data:[0,5,6,9,10,11],databas:[0,7,9,12],datetimefield:[0,6],db_engin:12,db_host:12,db_name:12,db_pass:12,db_port:12,db_user:12,debug:12,defer:[0,6],defin:[0,1,3,5,6,7,9,12],deleg:6,delet:[0,1,7,10],deploy:[3,12],deprec:12,describ:[10,12],descript:6,detail:[6,9,10,11],dev:9,develop:[8,9],dict:[0,5],dictionari:[0,5,7],differ:[1,9,10,11],disconnect:5,divid:9,django:[0,1,2,3,5,6,7,9,11,12],djangoproject:[0,3,6],djangpo:[0,6],doc:[0,3,6],docker:9,docsrc:12,doe:12,doesnotexist:[0,6],done:[9,10],drf:[0,11],dynam:6,each:[0,1,9,10],edit:12,either:10,email:0,empti:[0,6,12],end:10,endpoint:[0,6,9],engin:12,enhanc:6,entrypoint:3,env:12,environ:11,error:9,establish:[9,10,11,12],etc:9,event:[5,11],everi:[6,9,11,12],exampl:[4,8],except:[0,6],exec:12,execut:[0,2,6,9,12],execute_command:10,expect:[5,10],expir:[0,1],expires_in:0,expiringtokenauthent:0,explain:9,expos:[0,3,6],fail:[0,1],fals:[0,6,10],field:[0,6,7,10],figur:[9,11],file:[3,8,9],first:[0,6],folder:12,follow:[5,9,10,11,12],foreignkei:[0,6],format:[0,5,10],forward:[6,9,11],framework:[7,8,11],from:[0,3,5,6,7,9,10,12],frontend:[9,11,12],full:3,further:11,gener:[0,3,5,6],get:10,get_dict:7,get_next_by_cr:0,get_next_by_creation_timestamp:6,get_next_by_update_timestamp:6,get_previous_by_cr:0,get_previous_by_creation_timestamp:6,get_previous_by_update_timestamp:6,get_respons:0,get_sorted_view:6,gettokenmiddlewar:0,github:12,given:[0,7,9,10],globalpermiss:0,group:[5,8,9,11],handl:[0,5,9,11],handle_data_messag:5,handle_subscription_messag:5,handler:9,has:[0,9,11,12],has_read_permiss:6,have:[0,5,10,11],header:10,here:12,home:[0,3,6],host:12,how:[8,11],howto:3,html:12,http:[0,3,6,9,10,12],identifi:10,imag:9,implement:[6,9,12],includ:[0,3,6,11],incom:5,index:[0,8,12],info:10,inform:[0,3,6],inherit:7,ini:9,initi:9,inner:5,insid:[9,12],instanc:[0,5,6,9,10,11],instead:12,instruct:12,integr:12,intend:[5,10],interfac:[0,6],intermediari:[10,11],invalid:[0,1,9],is_next:[0,6],is_token_expir:0,its:[7,9,12],join:5,json:[0,5,6,10],kei:0,key1:10,key2:10,keyword:0,kwarg:[0,3,5,6],latter:[9,11],layer:12,ldap:12,leav:5,less:0,level:3,librari:9,list:[0,3,6,7],load:[0,6,8],local:8,locat:12,login:9,logout:[0,1],love:11,lsst:[11,12],mai:10,main:[2,9],make:[5,6,9],manag:[0,4,6,11,12],mani:[6,10],manytomanydescriptor:6,manytomanyfield:6,map:[0,6],mechan:10,messag:[3,5,9,11],meta:[0,6],methodnam:[1,7],middlewar:[4,5,12],migrat:9,mode:12,model:[4,7,9,10],modelseri:[0,6],modelviewset:6,modul:[4,8,9],more:[0,3,6,9,10,11],most:6,mostli:9,mount:12,move:12,multipleobjectsreturn:[0,6],must:[9,10,12],my_app:[0,3,6],name:[0,1,3,6,10,12],necessari:10,need:12,never:9,no_debug:12,none:[0,3,6],number:[0,10],obj:7,object:[0,5,6,7],objectdoesnotexist:[0,6],obtain:0,obtainauthtoken:0,off:12,onc:[9,11,12],one:[0,6],onli:[5,9,12],oper:[10,11],option:[5,10],order:[0,6,9,10,12],organ:8,other:[0,9,10],other_app:[0,3,6],otherwis:[5,12],our:0,out:12,outsid:12,over:9,overrid:12,overview:8,packag:[4,8],page:[0,6,8],pair:10,param1:[5,10],param2:[5,10],param:[5,10],paramet:10,parent:6,pars:5,part:[8,11],particular:[5,9,10,11],pass:10,password:[1,9,12],path:[0,3,6],pattern:[0,3],pend:5,permiss:[0,9,10,12],pipe:11,pizza:6,pleas:[0,3,6,9,11,12],port:12,post:[0,10],postgr:12,postgresql:12,process_connection_pass:12,produc:[9,11,12],project:[3,4,11,12],provid:[0,9,10,11,12],purpos:[7,9,12],pytest:[9,12],python:[9,11],queri:[0,6],queryset:6,rais:0,read:[0,6],readm:8,readonli:12,reason:10,rebuild:12,receiv:[1,5,9,11],receive_json:5,recept:5,recommend:12,redi:12,redirect:11,redis_host:12,redis_pass:12,redoc:10,ref:[0,3,6],refer:9,regist:[0,6],reject:5,relat:[0,6,10],related_nam:6,relationship:[7,10],remain:0,remian:0,remov:0,repli:[10,11],repo:12,repositori:12,repres:10,request:[0,1,6,9],requet:0,requier:9,requir:[9,10],resolv:[0,3],respect:9,respond:9,respons:[0,10],rest:[0,6,9,10,11],rest_framework:[0,6],restart:12,retriev:7,revers:6,reversemanytoonedescriptor:6,rout:[0,4,6,9],routepattern:[0,3],rule:[3,5],run:[3,9],runserv:9,runtest:[1,7],salindex:[5,10],script:9,scriptqueu:[5,10],search:[8,9],second:0,secret_kei:12,section:[9,11],see:[0,3,6,9,10,12],self:0,send:[5,9,11],sent:[10,11],serial:4,serializer_class:6,server:12,set:[4,6,7],setup:[1,7],should:12,shown:11,side:6,similarli:9,simpl:9,softwar:9,some:[9,12],sort:6,sort_valu:6,sourc:10,specifi:10,sqlite3:[9,12],src:12,stablish:10,standard:10,start:7,startproject:3,state:0,statu:[0,10],still:7,store:[6,9],stream1:[5,10],stream2:5,stream:[5,10],string:0,submodul:4,subpackag:4,subscirpt:5,subscrib:[5,9,10,11],subscript:[4,8,9],subscription_ack:5,subscription_all_data:5,subscription_data:5,subscriptionconsum:5,succesfulli:10,success:10,suffix:6,suit:1,sure:9,swagger:10,system:[8,10],tabl:10,tel1:9,tel2:9,telemetri:[5,9,11],test:[0,4,6,9],test_add_and_get_views_to_workspac:7,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_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_workspaces_from_a_view:7,test_retrieve_view:7,test_retrieve_workspac:7,test_retrieve_workspace_view:7,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_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_validate_token:1,test_user_validate_token_fail:1,testcas:[1,7],tests_api:[4,6],tests_auth_api:[0,4],tests_model:[4,6],than:0,thei:[0,10],them:10,therefor:11,thi:[0,3,6,9,10,11,12],those:5,throgh:[0,6],through:[1,7,9,10,11],time:[0,1,6],timestamp:6,token:[0,1,5,9,11],token_expire_handl:0,tokenauthent:0,tokenauthmiddlewar:5,tool:12,top:6,topic:[0,3,6],transfer:10,tri:9,trigger:10,turn:9,twie:1,txt:9,type:10,ui_framework:[4,8,10],uiframeworkconfig:6,unauthent:7,unauthenticatedcrudtestcas:7,unauthor:7,unauthorizedcrudtestcas:7,uniqu:9,unsubscrib:[5,10],unsubscript:5,unus:0,updat:[6,7],update_timestamp:6,upon:6,url:[4,9,10],urlconf:[0,3,6],urlpattern:[0,3,6],use:[0,5,7,8,12],used:[0,5,6,7,9,12],user:[0,1,5,7,9,12],user_data:10,user_user_pass:12,usernam:[0,9,10],userseri:0,uses:11,using:[1,3,11,12],usr:12,util:[4,6],valid:[0,1,9],validate_token:0,valu:[0,3,6,9,10,12],value1:[5,10],value2:[5,10],variabl:[3,10],vetween:7,via:10,view:[3,4,7,10],view_i:7,view_id:6,view_nam:6,viewmodeltestcas:7,viewseri:6,viewset:6,viewviewset:6,visual:11,wai:[10,12],websocket:[3,5,8,9,11,12],wether:12,when:[0,6,9,11],where:[10,11,12],which:[9,11,12],who:11,whole:3,within:6,work:[6,8,11],workflow:11,workspac:[6,7,10],workspace_i:7,workspace_id:6,workspace_view:[6,7],workspaceandviewsrelationstestcas:7,workspacemodeltestcas:7,workspaceseri:6,workspaceview:[6,7,10],workspaceviewmodeltestcas:7,workspaceviewseri:6,workspaceviewset:6,workspaceviewviewset:6,workview:6,wrapper:[0,6],written:11,wsgi:4,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,accept:10,admin:[0,6],api:[0,1,9],apidoc:4,app:[0,6],asgi:3,auth:5,authent:[0,10],build:12,channel:9,code:9,command:10,connect:10,consum:[5,9],content:[0,1,3,5,6,7],develop:12,docker:12,document:[8,12],environ:12,event:10,exampl:[0,3,6,9],file:12,framework:10,get:12,group:10,how:[9,10],imag:12,indic:8,initi:12,layer:9,load:12,local:12,logout:10,love:[8,9,10,12],manag:[2,3,8,9,10],messag:10,middlewar:0,model:[0,6],modul:[0,1,2,3,5,6,7],organ:9,overview:11,packag:[0,1,3,5,6,7],part:[9,12],password:10,readm:12,request:10,rout:[3,5],run:12,scheme:10,serial:[0,6],set:3,submodul:[0,1,3,5,6,7],subpackag:[0,6],subscript:[5,10],system:12,tabl:8,telemetri:10,test:[1,7,12],tests_api:7,tests_auth_api:1,tests_model:7,token:10,ui_framework:[6,7],url:[0,3,6],use:10,user:10,util:7,valid:10,variabl:12,view:[0,6],websocket:10,welcom:8,work:9,wsgi:3}}) \ No newline at end of file diff --git a/docsrc/source/apidoc/modules.rst b/docsrc/source/apidoc/modules.rst index ea95ba43..11acd33a 100644 --- a/docsrc/source/apidoc/modules.rst +++ b/docsrc/source/apidoc/modules.rst @@ -9,3 +9,4 @@ These are the ApiDocs of the project. manage manager subscription + ui_framework diff --git a/docsrc/source/apidoc/ui_framework.rst b/docsrc/source/apidoc/ui_framework.rst new file mode 100644 index 00000000..4c8b65ab --- /dev/null +++ b/docsrc/source/apidoc/ui_framework.rst @@ -0,0 +1,69 @@ +ui\_framework package +===================== + +Subpackages +----------- + +.. toctree:: + + ui_framework.tests + +Submodules +---------- + +ui\_framework.admin module +-------------------------- + +.. automodule:: ui_framework.admin + :members: + :undoc-members: + :show-inheritance: + +ui\_framework.apps module +------------------------- + +.. automodule:: ui_framework.apps + :members: + :undoc-members: + :show-inheritance: + +ui\_framework.models module +--------------------------- + +.. automodule:: ui_framework.models + :members: + :undoc-members: + :show-inheritance: + +ui\_framework.serializers module +-------------------------------- + +.. automodule:: ui_framework.serializers + :members: + :undoc-members: + :show-inheritance: + +ui\_framework.urls module +------------------------- + +.. automodule:: ui_framework.urls + :members: + :undoc-members: + :show-inheritance: + +ui\_framework.views module +-------------------------- + +.. automodule:: ui_framework.views + :members: + :undoc-members: + :show-inheritance: + + +Module contents +--------------- + +.. automodule:: ui_framework + :members: + :undoc-members: + :show-inheritance: diff --git a/docsrc/source/apidoc/ui_framework.tests.rst b/docsrc/source/apidoc/ui_framework.tests.rst new file mode 100644 index 00000000..1ecf78ef --- /dev/null +++ b/docsrc/source/apidoc/ui_framework.tests.rst @@ -0,0 +1,38 @@ +ui\_framework.tests package +=========================== + +Submodules +---------- + +ui\_framework.tests.tests\_api module +------------------------------------- + +.. automodule:: ui_framework.tests.tests_api + :members: + :undoc-members: + :show-inheritance: + +ui\_framework.tests.tests\_models module +---------------------------------------- + +.. automodule:: ui_framework.tests.tests_models + :members: + :undoc-members: + :show-inheritance: + +ui\_framework.tests.utils module +-------------------------------- + +.. automodule:: ui_framework.tests.utils + :members: + :undoc-members: + :show-inheritance: + + +Module contents +--------------- + +.. automodule:: ui_framework.tests + :members: + :undoc-members: + :show-inheritance: diff --git a/docsrc/source/modules/how_to_use_it.rst b/docsrc/source/modules/how_to_use_it.rst index c5b5aaf5..2c142b22 100644 --- a/docsrc/source/modules/how_to_use_it.rst +++ b/docsrc/source/modules/how_to_use_it.rst @@ -213,3 +213,20 @@ Specifying the variables necessary to subscribe a to a group in a JSON message, } Where pairs :code:`param1` and :code:`value1` represent the parameters (name and value) to be passed to the command. + + +UI Framework +============ + +The UI Framework backend is composed of 3 models: + + - **Workspace:** represents a workspace, composed by different views + - **View:** represents a view, all the data of the view is contained in JSON format in the :code:`data` field of the view + - **WorkspaceView:** relates a Workspace and a View, it is the intermediary table of the many-to-many relationship between Workspace and View. + +Currently the API provides a standard REST api for these models. +For more info you can either: + + - Use the browsable API available in: :code:`/manager/ui_framework/` + - See the apidoc in Swagger format, available in: :code:`/manager/apidoc/swagger/` + - See the apidoc in ReDoc format, available in: :code:`/manager/apidoc/redoc/` diff --git a/manager/api/admin.py b/manager/api/admin.py index 6cd1073b..c9a2815e 100644 --- a/manager/api/admin.py +++ b/manager/api/admin.py @@ -1,10 +1,10 @@ """ -Defines the Django Admin model pages for this app ('api'). +Defines the Django Admin model pages for this app . Registers the models that will be available throgh the Djangpo Admin interface. For more information see: -https://docs.djangoproject.com/en/2.1/ref/contrib/admin/ +https://docs.djangoproject.com/en/2.2/ref/contrib/admin/ """ from django.contrib import admin from api.models import Token diff --git a/manager/api/apps.py b/manager/api/apps.py index af8bf7f0..f71c0d2e 100644 --- a/manager/api/apps.py +++ b/manager/api/apps.py @@ -1,12 +1,12 @@ -""" -Django apps configuration for manager project. - -Generated by 'django-admin startproject' using Django 2.1.3. -""" +"""Django apps configuration for the api app.""" from django.apps import AppConfig class ApiConfig(AppConfig): - """General App config class. Currently defines the name of the app ('api').""" + """General App config class. Currently defines the name of the app.""" name = 'api' + + def ready(self): + """Import the signals module when the application is ready.""" + import api.signals diff --git a/manager/api/models.py b/manager/api/models.py index 43f55c42..c121d728 100644 --- a/manager/api/models.py +++ b/manager/api/models.py @@ -2,7 +2,7 @@ Defines the Django models for this app ('api'). For more information see: -https://docs.djangoproject.com/en/2.1/topics/db/models/ +https://docs.djangoproject.com/en/2.2/topics/db/models/ """ from django.conf import settings from django.db import models diff --git a/manager/api/schema_validator.py b/manager/api/schema_validator.py new file mode 100644 index 00000000..58c7a40e --- /dev/null +++ b/manager/api/schema_validator.py @@ -0,0 +1,151 @@ +# This file is part of ts_salobj. +# +# Developed for the LSST Telescope and Site Systems. +# This product includes software developed by the LSST Project +# (https://www.lsst.org). +# See the COPYRIGHT file at the top-level directory of this distribution +# for details of code ownership. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +__all__ = ["DefaultingValidator"] + +import jsonschema + + +class DefaultingValidator: + """A wrapper for jsonschema validators that applies default values. + + Parameters + ---------- + schema : `dict` + Schema against which to validate. + ValidatorClass : ``jsonschema.IValidator`` (optional) + jsonschema validator class, e.g. ``jsonschema.Draft7Validator``. + + Notes + ----- + This class is not a ``jsonschema.IValidator`` but it contains two + validators: + + * defaults_validator: a validator that sets default values in the + data being validated + * final_validator: a standard validator that does not alter + the data being validated. + """ + + def __init__(self, schema, ValidatorClass=jsonschema.Draft7Validator): + ValidatorClass.check_schema(schema) + self.final_validator = ValidatorClass(schema=schema) + + validate_properties = ValidatorClass.VALIDATORS["properties"] + + def set_defaults(validator, properties, instance, schema): + """Wrap a jsonschema Validator so that it sets default values. + + Parameters + ---------- + validator : ``jsonschema.IValidator`` + jsonschema validator. + properties : `dict` + The value of the property being validated within the instance + instance : `dict` + The item being checked and possibly set. + schema : `dict` + The schema being validated. + + Notes + ----- + This code is based on https://python-jsonschema.readthedocs.io/ + en/stable/faq/#why-doesn-t-my-schema- + s-default-property-set-the-default-on-my-instance + but I added skip_properties to avoid infinite recursion + """ + # most of these items cause infinite recursion if allowed through + # and none are needed for setting defaults + skip_properties = set( + ( + "additionalItems", + "additionalProperties", + "definitions", + "default", + "items", + "patternProperties", + "property", + "properties", + "readOnly", + "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) + + for error in validate_properties(validator, properties, instance, schema): + yield error + + WrappedValidator = jsonschema.validators.extend( + ValidatorClass, {"properties": set_defaults} + ) + WrappedValidator.check_schema(schema) + self.defaults_validator = WrappedValidator(schema=schema) + + def validate(self, data_dict): + """Validate data. + + Set missing values based on defaults in the schema, + then check the final result against the schema + (in case any defaults are not valid). + + Parameters + ---------- + data_dict : `dict` + Data to validate. + + Returns + ------- + result : `dict` or `None` + Validated data. A copy of data_dict with missing values + that have defaults set to those defaults. + If None then an empty dict is used. + + Raises + ------ + jsonschema.exceptions.ValidationError + If the data does not match the schema. + """ + if data_dict is None: + result = {} + elif type(data_dict) is not dict: + raise jsonschema.exceptions.ValidationError(f"{data_dict} is not a dict") + else: + result = data_dict.copy() + + self.defaults_validator.validate(result) + self.final_validator.validate(result) + return result diff --git a/manager/api/serializers.py b/manager/api/serializers.py index a2d2e59b..a07cc347 100644 --- a/manager/api/serializers.py +++ b/manager/api/serializers.py @@ -1,6 +1,8 @@ """Defines the serializer used by the REST API exposed by this app ('api').""" +from drf_yasg.utils import swagger_serializer_method from rest_framework import serializers from django.contrib.auth.models import User +from manager import utils class UserSerializer(serializers.ModelSerializer): @@ -12,5 +14,107 @@ class Meta: model = User """The model class to serialize""" - fields = ('username', 'email') + fields = ("username", "email") """The fields of the model class to serialize""" + + +class UserPermissionsSerializer(serializers.Serializer): + """Custom Serializer for user permissions.""" + + execute_commands = serializers.SerializerMethodField("can_execute_commands") + + def can_execute_commands(self, user) -> bool: + """Define wether or not the given user has permissions to execute commands. + + Params + ------ + user: User + The User object + + Returns + ------- + Bool + True if the user can execute commands, False if not. + """ + return user.has_perm("api.command.execute_command") + + +class TimeDataSerializer(serializers.Serializer): + """Custom Serializer for responses to validate and get token requests.""" + + utc = serializers.FloatField() + + tai = serializers.FloatField() + + mjd = serializers.FloatField() + + sidereal_summit = serializers.FloatField() + + sidereal_greenwich = serializers.FloatField() + + tai_to_utc = serializers.FloatField() + + +class TokenSerializer(serializers.Serializer): + """Custom Serializer for responses to validate and get token requests.""" + + user = UserSerializer() + + token = serializers.SerializerMethodField("get_token") + + permissions = serializers.SerializerMethodField("get_permissions") + + time_data = serializers.SerializerMethodField("get_time_data") + + @swagger_serializer_method(serializer_or_field=UserPermissionsSerializer) + def get_permissions(self, token): + """Return user permissions serialized as a dictionary with permission names as keys and bools as values. + + Params + ------ + token: Token + The Token object + + Returns + ------- + Bool + True if the user can execute commands, False if not. + """ + return UserPermissionsSerializer(token.user).data + + def get_token(self, token): + """Return the token key. + + Params + ------ + token: Token + The Token object + + Returns + ------- + String + The token key + """ + return token.key + + @swagger_serializer_method(serializer_or_field=TimeDataSerializer) + def get_time_data(self, token) -> dict: + """Return relevant time measures. + + Params + ------ + token: Token + The Token object + + Returns + ------- + Dict + Dictionary containing the following keys: + - utc: current time in UTC scale as a unix timestamp (seconds) + - tai: current time in UTC scale as a unix timestamp (seconds) + - mjd: current time as a modified julian date + - sidereal_summit: current time as a sidereal_time w/respect to the summit location (hourangles) + - sidereal_summit: current time as a sidereal_time w/respect to Greenwich location (hourangles) + - tai_to_utc: The number of seconds of difference between TAI and UTC times (seconds) + """ + return utils.get_times() diff --git a/manager/api/signals.py b/manager/api/signals.py new file mode 100644 index 00000000..ead73e11 --- /dev/null +++ b/manager/api/signals.py @@ -0,0 +1,33 @@ +from django.db.models.signals import post_delete +from django.dispatch import receiver +from channels.layers import get_channel_layer +from api.models import Token +import asyncio + + +@receiver(post_delete, sender=Token) +def handle_token_deletion(sender, **kwargs): + """Receive signal when a Token is deleted and send a message to consumers subscribed to the Tokne's group, + instructing them to logout. + + Parameters + ---------- + sender: `object` + class of the sender, in this case 'Token' + kwargs: `dict` + 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': ''} + loop = None + try: + loop = asyncio.get_event_loop() + except RuntimeError: + loop = asyncio.new_event_loop() + + if loop and loop.is_running(): + asyncio.create_task(get_channel_layer().group_send(groupname, payload)) + else: + loop.run_until_complete(get_channel_layer().group_send(groupname, payload)) diff --git a/manager/api/tests/test_commander.py b/manager/api/tests/test_commander.py new file mode 100644 index 00000000..cc4153c9 --- /dev/null +++ b/manager/api/tests/test_commander.py @@ -0,0 +1,89 @@ +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 CommanderTestCase(TestCase): + maxDiff = None + + 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.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_commander_data(self, mock_requests, mock_environ): + """Test authorized user commander data is sent to love-commander""" + # Arrange: + self.user.user_permissions.add(Permission.objects.get(name='Execute Commands')) + + # Act: + url = reverse('commander') + data = { + 'csc': 'Test', + 'salindex': 1, + 'cmd': 'cmd_setScalars', + 'params': { + 'a': 1, + 'b': 2 + } + } + + with self.assertRaises(ValueError): + response = self.client.post(url, data, format='json') + fakehostname = 'fakehost' + fakeport = 'fakeport' + expected_url = f"http://fakehost:fakeport/cmd" + 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_commander(self, mock_requests, mock_environ): + """Test an unauthorized user can't send commands""" + # Act: + url = reverse('commander') + data = { + 'csc': 'Test', + 'salindex': 1, + 'cmd': 'cmd_setScalars', + 'params': { + 'a': 1, + 'b': 2 + } + } + + 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 execute commands." + } + ) \ No newline at end of file diff --git a/manager/api/tests/test_schema_validation.py b/manager/api/tests/test_schema_validation.py new file mode 100644 index 00000000..72349ab4 --- /dev/null +++ b/manager/api/tests/test_schema_validation.py @@ -0,0 +1,166 @@ +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 + +@override_settings(DEBUG=True) +class SchemaValidationTestCase(TestCase): + script_schema = """ + $id: https://github.com/lsst-ts/ts_salobj/TestScript.yaml + $schema: http://json-schema.org/draft-07/schema# + additionalProperties: false + description: Configuration for TestScript + properties: + fail_cleanup: + default: false + description: If true then raise an exception in the "cleanup" method. + type: boolean + fail_run: + default: false + description: If true then raise an exception in the "run" method afer the "start" checkpoint but before waiting. + type: boolean + wait_time: + default: 0 + description: Time to wait, in seconds + minimum: 0 + type: number + required: + - wait_time + - fail_run + - fail_cleanup + title: TestScript v1 + type: object + """ + maxDiff = None + + 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.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')) + + 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') + + # Assert: + expected_data = { + "title": "None", + "output": {'wait_time': 3600, 'fail_cleanup': False, 'fail_run': False} + } + + self.assertEqual( + response.data, + expected_data + ) + + def test_syntax_error(self): + """Test validation output of an unparsable config file""" + configs = [ + "wait_time: -\na:""", # ScannerError + "fail_cleanup: \nw:'", # ScannerError + ":" # 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'} + ] + + 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') + # Assert: + self.assertEqual( + response.data, + expected_datum + ) + + def test_invalid_config(self): + """Test validation output of an invalid config file""" + + configs = [ + "wait_time: 'asd'", + "asdfasfd" + ] + + 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'} + + ] + + 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') + + # Assert: + 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 b98c7ad5..f6a9d3f5 100644 --- a/manager/api/tests/tests_auth_api.py +++ b/manager/api/tests/tests_auth_api.py @@ -1,4 +1,4 @@ -"""Test users' authentication thorugh the API.""" +"""Test users' authentication through the API.""" import datetime from django.test import TestCase from django.urls import reverse @@ -8,6 +8,7 @@ from rest_framework import status from api.models import Token from django.conf import settings +from manager import utils class AuthApiTestCase(TestCase): @@ -17,219 +18,221 @@ def setUp(self): """Define the test suite setup.""" # Arrange: self.client = APIClient() - self.username = 'test' - self.password = 'password' + 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', - ) - self.user.user_permissions.add(Permission.objects.get(name='Execute Commands')) - self.login_url = reverse('login') - self.validate_token_url = reverse('validate-token') - self.logout_url = reverse('logout') + password="password", + email="test@user.cl", + first_name="First", + last_name="Last", + ) + self.user.user_permissions.add(Permission.objects.get(name="Execute Commands")) + self.login_url = reverse("login") + self.validate_token_url = reverse("validate-token") + self.logout_url = reverse("logout") self.expected_permissions = { - 'execute_commands': True, + "execute_commands": True, } def test_user_login(self): """Test that a user can request a token using name and password.""" # Arrange: - data = {'username': self.username, 'password': self.password} + data = {"username": self.username, "password": self.password} tokens_num_0 = Token.objects.filter(user__username=self.username).count() # Act: - response = self.client.post(self.login_url, data, format='json') + response = self.client.post(self.login_url, data, format="json") # Assert: self.assertEqual(response.status_code, status.HTTP_200_OK) tokens_num = Token.objects.filter(user__username=self.username).count() - self.assertEqual(tokens_num_0 + 1, tokens_num, 'The user should have a new token') - tokens_in_db = [t.key for t in Token.objects.filter(user__username=self.username)] - retrieved_token = response.data['token'] - self.assertTrue(retrieved_token in tokens_in_db, 'The token should be in the DB') self.assertEqual( - response.data['permissions'], + tokens_num_0 + 1, tokens_num, "The user should have a new token" + ) + tokens_in_db = [ + t.key for t in Token.objects.filter(user__username=self.username) + ] + retrieved_token = response.data["token"] + self.assertTrue( + retrieved_token in tokens_in_db, "The token should be in the DB" + ) + self.assertEqual( + response.data["permissions"], self.expected_permissions, - 'The permissions are not as expected' + "The permissions are not as expected", ) self.assertEqual( - response.data['user_data'], - { - 'username': self.user.username, - 'email': self.user.email, - }, - 'The user_data is not as expected' + response.data["user"], + {"username": self.user.username, "email": self.user.email}, + "The user is not as expected", + ) + self.assertTrue( + utils.assert_time_data(response.data["time_data"]), + "Time data is not as expected", ) def test_user_login_failed(self): """Test that a user cannot request a token if the credentials are invalid.""" # Arrange: - data = {'username': self.username, 'password': 'wrong-password'} + data = {"username": self.username, "password": "wrong-password"} tokens_num_0 = Token.objects.filter(user__username=self.username).count() # Act: - response = self.client.post(self.login_url, data, format='json') + response = self.client.post(self.login_url, data, format="json") # Assert: self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) tokens_num_1 = Token.objects.filter(user__username=self.username).count() - self.assertEqual(tokens_num_0, tokens_num_1, 'The user should have no token') + self.assertEqual(tokens_num_0, tokens_num_1, "The user should have no token") def test_user_login_twice(self): """Test that a user can request a token twie receiving different tokens each time.""" # Arrange: - data = {'username': self.username, 'password': self.password} + data = {"username": self.username, "password": self.password} tokens_num_0 = Token.objects.filter(user__username=self.username).count() # Act 1: - response = self.client.post(self.login_url, data, format='json') + response = self.client.post(self.login_url, data, format="json") # Assert 1: self.assertEqual(response.status_code, status.HTTP_200_OK) tokens_num_1 = Token.objects.filter(user__username=self.username).count() - self.assertEqual(tokens_num_0 + 1, tokens_num_1, 'The user should have a new token') - retrieved_token_1 = response.data['token'] - tokens_in_db = [t.key for t in Token.objects.filter(user__username=self.username)] - self.assertTrue(retrieved_token_1 in tokens_in_db, 'The token should be in the DB') + self.assertEqual( + tokens_num_0 + 1, tokens_num_1, "The user should have a new token" + ) + retrieved_token_1 = response.data["token"] + tokens_in_db = [ + t.key for t in Token.objects.filter(user__username=self.username) + ] + self.assertTrue( + retrieved_token_1 in tokens_in_db, "The token should be in the DB" + ) # Act 2: - response = self.client.post(self.login_url, data, format='json') + response = self.client.post(self.login_url, data, format="json") # Assert after request 2: self.assertEqual(response.status_code, status.HTTP_200_OK) tokens_num_2 = Token.objects.filter(user__username=self.username).count() - self.assertEqual(tokens_num_1 + 1, tokens_num_2, 'The user should have another new token') - retrieved_token_2 = response.data['token'] - tokens_in_db = [t.key for t in Token.objects.filter(user__username=self.username)] - self.assertTrue(retrieved_token_1 in tokens_in_db, 'The token should be in the DB') - self.assertNotEqual(retrieved_token_1, retrieved_token_2, 'The tokens should be different') + self.assertEqual( + tokens_num_1 + 1, tokens_num_2, "The user should have another new token" + ) + retrieved_token_2 = response.data["token"] + tokens_in_db = [ + t.key for t in Token.objects.filter(user__username=self.username) + ] + self.assertTrue( + retrieved_token_1 in tokens_in_db, "The token should be in the DB" + ) + self.assertNotEqual( + retrieved_token_1, retrieved_token_2, "The tokens should be different" + ) 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') + data = {"username": self.username, "password": self.password} + response = 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) + self.client.credentials(HTTP_AUTHORIZATION="Token " + token.key) # Act: - response = self.client.get( - self.validate_token_url, data, format='json' - ) + response = self.client.get(self.validate_token_url, data, format="json") # Assert after request: self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual( - response.data['detail'], - 'Token is valid', - 'The response is not as expected' + response.data["token"], token.key, "The response is not as expected" ) self.assertEqual( - response.data['permissions'], + response.data["permissions"], self.expected_permissions, - 'The permissions are not as expected' + "The permissions are not as expected", ) self.assertEqual( - response.data['user_data'], - { - 'username': self.user.username, - 'email': self.user.email, - }, - 'The user_data is not as expected' + response.data["user"], + {"username": self.user.username, "email": self.user.email,}, + "The user is not as expected", + ) + self.assertTrue( + utils.assert_time_data(response.data["time_data"]), + "Time data is not as expected", ) 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') + data = {"username": self.username, "password": self.password} + response = 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') + self.client.credentials(HTTP_AUTHORIZATION="Token " + token.key + "fake") # Act: - response = self.client.get( - self.validate_token_url, data, format='json' - ) + response = self.client.get(self.validate_token_url, data, format="json") # Assert after request: self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) - self.assertNotEqual( - response.data, - {'detail': 'Token is valid'}, - 'The response indicates the validation was correct' - ) 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') + data = {"username": self.username, "password": self.password} + response = 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) + self.client.credentials(HTTP_AUTHORIZATION="Token " + token.key) token.delete() # Act: - response = self.client.get( - self.validate_token_url, format='json' - ) + response = self.client.get(self.validate_token_url, format="json") # Assert: self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) - self.assertNotEqual( - response.data, - {'detail': 'Token is valid'}, - 'The response indicates the validation was correct' - ) def test_user_fails_to_validate_expired_token(self): """Test that a user fails to validate an expired token.""" # Arrange: 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') + data = {"username": self.username, "password": self.password} + response = 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) + self.client.credentials(HTTP_AUTHORIZATION="Token " + token.key) # Act: - max_timedelta = datetime.timedelta(days=settings.TOKEN_EXPIRED_AFTER_DAYS, seconds=1) - frozen_datetime.tick(delta=max_timedelta) - response = self.client.get( - self.validate_token_url, format='json' + max_timedelta = datetime.timedelta( + days=settings.TOKEN_EXPIRED_AFTER_DAYS, seconds=1 ) + frozen_datetime.tick(delta=max_timedelta) + response = self.client.get(self.validate_token_url, format="json") # Assert: token_num_1 = Token.objects.filter(user__username=self.username).count() self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) - self.assertNotEqual( - response.data, - {'detail': 'Token is valid'}, - 'The response indicates the validation was correct' - ) self.assertEqual(token_num_0 - 1, token_num_1) 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') + data = {"username": self.username, "password": self.password} + response = 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) + self.client.credentials(HTTP_AUTHORIZATION="Token " + token.key) # Act: - response = self.client.delete(self.logout_url, format='json') + response = self.client.delete(self.logout_url, format="json") # Assert: self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) self.assertEqual( response.data, - {'detail': 'Logout successful, Token succesfully deleted'}, - 'The response is not as expected' + {"detail": "Logout successful, Token succesfully deleted"}, + "The response is not as expected", ) new_tokens_count = Token.objects.filter(user__username=self.username).count() - self.assertEqual(old_tokens_count - 1, new_tokens_count, 'The token was not deleted') + self.assertEqual( + old_tokens_count - 1, new_tokens_count, "The token was not deleted" + ) + diff --git a/manager/api/urls.py b/manager/api/urls.py index ec5e5e7a..c46cac69 100644 --- a/manager/api/urls.py +++ b/manager/api/urls.py @@ -18,7 +18,7 @@ from django.conf.urls import include from django.urls import path from rest_framework.routers import DefaultRouter -from api.views import validate_token, logout, CustomObtainAuthToken +from api.views import validate_token, logout, CustomObtainAuthToken, validate_config_schema, commander router = DefaultRouter() @@ -26,7 +26,9 @@ urlpatterns = [ path('get-token/', CustomObtainAuthToken.as_view(), name='login'), path('validate-token/', validate_token, name='validate-token'), + path('validate-config-schema/', validate_config_schema, name='validate-config-schema'), path('logout/', logout, name='logout'), path('auth/', include('rest_framework.urls', namespace='rest_framework')), + path('cmd/', commander, name='commander'), ] urlpatterns.append(path('', include(router.urls))) diff --git a/manager/api/views.py b/manager/api/views.py index 0a060cf9..23fb2a79 100644 --- a/manager/api/views.py +++ b/manager/api/views.py @@ -1,15 +1,25 @@ -"""Defines the views exposed by the REST API exposed by this app ('api').""" +"""Defines the views exposed by the REST API exposed by this app.""" +import os +import requests +import yaml +import jsonschema +import collections +from drf_yasg import openapi +from drf_yasg.utils import swagger_auto_schema from rest_framework import status -# from rest_framework.authtoken.models import Token -from api.models import Token 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.response import Response -from api.serializers import UserSerializer +from api.models import Token +from api.serializers import TokenSerializer +from .schema_validator import DefaultingValidator +valid_response = openapi.Response('Valid token', TokenSerializer) +invalid_response = openapi.Response('Invalid token') +@swagger_auto_schema(method='get', responses={200: valid_response, 401: invalid_response}) @api_view(['GET']) @permission_classes((IsAuthenticated,)) def validate_token(request): @@ -22,20 +32,12 @@ def validate_token(request): Response The response stating that the token is valid with a 200 status code. """ - user = request.user - user_data = UserSerializer(user).data - return Response( - { - 'detail': 'Token is valid', - 'user_data': user_data, - 'permissions': { - 'execute_commands': user.has_perm('api.command.execute_command') - }, - }, - status=status.HTTP_200_OK - ) + token_key = request.META.get('HTTP_AUTHORIZATION')[6:] + token = Token.objects.get(key=token_key) + return Response(TokenSerializer(token).data) +@swagger_auto_schema(method='delete', responses={204: openapi.Response('Logout Successful')}) @api_view(['DELETE']) @permission_classes((IsAuthenticated,)) def logout(request): @@ -56,6 +58,10 @@ def logout(request): class CustomObtainAuthToken(ObtainAuthToken): """API endpoint to obtain authorization tokens.""" + login_response = openapi.Response('Login succesful', TokenSerializer) + login_failed_response = openapi.Response('Login failed') + + @swagger_auto_schema(responses={200: login_response, 400: login_failed_response}) def post(self, request, *args, **kwargs): """Handle the (post) request for token. @@ -79,11 +85,62 @@ def post(self, request, *args, **kwargs): serializer.is_valid(raise_exception=True) user = serializer.validated_data['user'] token = Token.objects.create(user=user) - user_data = UserSerializer(user).data + return Response(TokenSerializer(token).data) + + +@swagger_auto_schema(method='post', responses={200: valid_response, 401: invalid_response}) +@api_view(['POST']) +@permission_classes((IsAuthenticated,)) +def validate_config_schema(request): + """Validate a configuration yaml with using a schema + + Returns + ------- + Response + Dictionary containing a 'title' and an 'error' key (if any) + or an 'output' with the output of the validator (config with defaults-autocomplete) + """ + try: + config = yaml.safe_load(request.data['config']) + except yaml.YAMLError as e: + error = e.__dict__ + error['problem_mark'] = e.problem_mark.__dict__ + del error['context_mark'] + return Response({ + 'title': 'ERROR WHILE PARSING YAML STRING', + 'error': error + }) + schema = yaml.safe_load(request.data['schema']) + validator = DefaultingValidator(schema) + + try: + output = validator.validate(config) + return Response({'title': 'None', "output": output}) + except jsonschema.exceptions.ValidationError as e: + error = e.__dict__ + for key in error: + if(type(error[key]) == collections.deque): + error[key] = list(error[key]) + return Response({ - 'token': token.key, - 'user_data': user_data, - 'permissions': { - 'execute_commands': user.has_perm('api.command.execute_command') - }, + 'title': 'INVALID CONFIG YAML', + 'error': { + 'message': str(error["message"]), + "path": [] if not error['path'] else list(error['path']), + "schema_path": [] if not error['schema_path'] else list(error['schema_path']), + } }) + + +@swagger_auto_schema(method='post', responses={200: valid_response, 401: invalid_response}) +@api_view(['POST']) +@permission_classes((IsAuthenticated,)) +def commander(request): + """Sends a command to the LOVE-commander according to the received parameters + """ + if not request.user.has_perm("api.command.execute_command"): + return Response({"ack": "User does not have permissions to execute commands."}, 401) + url = f"http://{os.environ.get('COMMANDER_HOSTNAME')}:{os.environ.get('COMMANDER_PORT')}/cmd" + response = requests.post(url, json=request.data) + + return Response(response.json(), status=response.status_code) \ No newline at end of file diff --git a/manager/manager/settings.py b/manager/manager/settings.py index 3a0ab783..4d377dc1 100644 --- a/manager/manager/settings.py +++ b/manager/manager/settings.py @@ -4,10 +4,10 @@ Generated by 'django-admin startproject' using Django 2.1.3. For more information on this file, see -https://docs.djangoproject.com/en/2.1/topics/settings/ +https://docs.djangoproject.com/en/2.2/topics/settings/ For the full list of settings and their values, see -https://docs.djangoproject.com/en/2.1/ref/settings/ +https://docs.djangoproject.com/en/2.2/ref/settings/ """ import os @@ -19,24 +19,45 @@ # Define wether the system is being tested or not: TESTING = os.environ.get('TESTING', False) - # Quick-start development settings - unsuitable for production -# See https://docs.djangoproject.com/en/2.1/howto/deployment/checklist/ +# See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = 'tbder3gzppu)kl%(u3awhhg^^zu#j&!ceh@$n&v0d38sjx43s8' +SECRET_KEY = os.getenv('SECRET_KEY', 'tbder3gzppu)kl%(u3awhhg^^zu#j&!ceh@$n&v0d38sjx43s8') # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = True +if os.environ.get('NO_DEBUG'): + DEBUG = False +else: + DEBUG = True + +# Database +# https://docs.djangoproject.com/en/2.2/ref/settings/#databases +if os.environ.get('DB_ENGINE') == 'postgresql': + DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.postgresql', + 'NAME': os.getenv('DB_NAME', 'postgres'), + 'USER': os.getenv('DB_USER', 'postgres'), + 'HOST': os.getenv('DB_HOST', 'postgres'), + 'PORT': os.getenv('DB_PORT', '5432'), + 'PASSWORD': os.getenv('DB_PASS', 'postgres') + } + } +else: + DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), + } + } ALLOWED_HOSTS = [ 'localhost', 'love-manager-mount', 'love-nginx-mount', 'manager', - 'love-manager', 'love-nginx', '10.0.100.1', '10.0.100.209', '127.0.0.1' + 'love-manager', 'love-nginx', '10.0.100.1', '10.0.100.209', '127.0.0.1', '0.0.0.0' ] - # Application definition - INSTALLED_APPS = [ 'django.contrib.auth', 'django.contrib.admin', @@ -49,8 +70,10 @@ 'rest_framework', # 'rest_framework.authtoken', 'corsheaders', + 'drf_yasg', 'api', 'subscription', + 'ui_framework', ] MIDDLEWARE = [ @@ -90,19 +113,8 @@ WSGI_APPLICATION = 'manager.wsgi.application' -# Database -# https://docs.djangoproject.com/en/2.1/ref/settings/#databases - -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), - } -} - - # Password validation -# https://docs.djangoproject.com/en/2.1/ref/settings/#auth-password-validators +# https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators AUTH_PASSWORD_VALIDATORS = [ { @@ -124,7 +136,7 @@ # Internationalization -# https://docs.djangoproject.com/en/2.1/topics/i18n/ +# https://docs.djangoproject.com/en/2.2/topics/i18n/ LANGUAGE_CODE = 'en-us' TIME_ZONE = 'UTC' @@ -136,6 +148,7 @@ REST_FRAMEWORK = { 'DEFAULT_PERMISSION_CLASSES': ( 'rest_framework.permissions.IsAuthenticated', + 'rest_framework.permissions.DjangoModelPermissions', ), 'DEFAULT_AUTHENTICATION_CLASSES': ( # 'rest_framework.authentication.TokenAuthentication', @@ -148,7 +161,7 @@ # Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/2.1/howto/static-files/ +# https://docs.djangoproject.com/en/2.2/howto/static-files/ STATIC_URL = '/manager/static/' STATIC_ROOT = os.path.join(BASE_DIR, "static") STATICFILES_DIRS = ( @@ -163,16 +176,25 @@ os.path.join(BASE_DIR, "static_files"), ] +MEDIA_URL = "/media/" +if TESTING: + MEDIA_BASE = os.path.join(BASE_DIR, 'ui_framework', 'tests') + MEDIA_ROOT = os.path.join(BASE_DIR, 'ui_framework', 'tests', 'media') +else: + MEDIA_BASE = BASE_DIR + MEDIA_ROOT = os.path.join(BASE_DIR, "media") + # Channels ASGI_APPLICATION = 'manager.routing.application' REDIS_HOST = os.environ.get('REDIS_HOST', False) +REDIS_PORT = os.environ.get('REDIS_PORT', "6379") REDIS_PASS = os.environ.get('REDIS_PASS', False) if REDIS_HOST and not TESTING: CHANNEL_LAYERS = { 'default': { 'BACKEND': 'channels_redis.core.RedisChannelLayer', 'CONFIG': { - "hosts": ["redis://:" + REDIS_PASS + "@" + REDIS_HOST + ":6379/0"], + "hosts": ["redis://:" + REDIS_PASS + "@" + REDIS_HOST + ":" + REDIS_PORT + "/0"], "symmetric_encryption_keys": [SECRET_KEY], }, }, diff --git a/manager/manager/urls.py b/manager/manager/urls.py index 7ee4dc81..d8ce598e 100644 --- a/manager/manager/urls.py +++ b/manager/manager/urls.py @@ -17,13 +17,32 @@ """ from django.conf.urls import include from django.contrib import admin -from django.urls import path +from django.urls import path, re_path from django.views.generic import TemplateView +from drf_yasg.views import get_schema_view +from drf_yasg import openapi +from rest_framework import permissions +from django.conf import settings +schema_view = get_schema_view( + openapi.Info( + title="LOVE-manager API", + default_version='v1', + description="This is the API of LOVE-manager's authentication and UI Framework modules", + ), + public=True, + permission_classes=(permissions.AllowAny,), +) 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")), ] diff --git a/manager/manager/utils.py b/manager/manager/utils.py new file mode 100644 index 00000000..0ccee1eb --- /dev/null +++ b/manager/manager/utils.py @@ -0,0 +1,60 @@ +from astropy.time import Time + + +def get_tai_to_utc() -> float: + """Return the difference in seconds between TAI and UTC Timestamps. + + Returns + ------- + Int + The number of seconds of difference between TAI and UTC times + """ + t = Time.now() + dt = t.datetime.timestamp() - t.tai.datetime.timestamp() + return dt + + +def get_times(): + """Return relevant time measures. + + Returns + ------- + Dict + Dictionary containing the following keys: + - utc: current time in UTC scale as a unix timestamp (seconds) + - tai: current time in UTC scale as a unix timestamp (seconds) + - mjd: current time as a modified julian date + - sidereal_summit: current time as a sidereal_time w/respect to the summit location (hourangles) + - sidereal_summit: current time as a sidereal_time w/respect to Greenwich location (hourangles) + - tai_to_utc: The number of seconds of difference between TAI and UTC times (seconds) + """ + t = Time.now() + t_utc = t.datetime.timestamp() + t_tai = t.tai.datetime.timestamp() + sidereal_summit = t.sidereal_time("apparent", longitude=-70.749417, model=None) + sidereal_greenwich = t.sidereal_time("apparent", longitude="greenwich", model=None) + return { + "utc": t_utc, + "tai": t_tai, + "mjd": t.mjd, + "sidereal_summit": sidereal_summit.value, + "sidereal_greenwich": sidereal_greenwich.value, + "tai_to_utc": t_utc - t_tai, + } + +def assert_time_data(time_data): + """Asserts the structure of the time_data dictionary.""" + + if not isinstance(time_data["utc"], float): + return False + if not isinstance(time_data["tai"], float): + return False + if not isinstance(time_data["mjd"], float): + return False + if not isinstance(time_data["sidereal_summit"], float): + return False + if not isinstance(time_data["sidereal_greenwich"], float): + return False + if not isinstance(time_data["tai_to_utc"], float): + return False + return True \ No newline at end of file diff --git a/manager/media/thumbnails/.gitinclude b/manager/media/thumbnails/.gitinclude new file mode 100644 index 00000000..e69de29b diff --git a/manager/pytestdebug.log b/manager/pytestdebug.log new file mode 100644 index 00000000..5fd56bb8 --- /dev/null +++ b/manager/pytestdebug.log @@ -0,0 +1,7290 @@ +versions pytest-5.2.1, py-1.8.0, python-3.7.2.final.0 +cwd=/usr/src/love/manager +args=['-k', 'delete', '--debug'] + + pytest_cmdline_main [hook] + config: <_pytest.config.Config object at 0x7f3418040dd8> + pytest_plugin_registered [hook] + plugin: + manager: <_pytest.config.PytestPluginManager object at 0x7f341c1b92b0> + finish pytest_plugin_registered --> [] [hook] + pytest_configure [hook] + config: <_pytest.config.Config object at 0x7f3418040dd8> + pytest_plugin_registered [hook] + plugin: <_pytest.cacheprovider.LFPlugin object at 0x7f3410ae0630> + manager: <_pytest.config.PytestPluginManager object at 0x7f341c1b92b0> + finish pytest_plugin_registered --> [] [hook] + pytest_plugin_registered [hook] + plugin: <_pytest.cacheprovider.NFPlugin object at 0x7f3410ae0828> + manager: <_pytest.config.PytestPluginManager object at 0x7f341c1b92b0> + finish pytest_plugin_registered --> [] [hook] + early skip of rewriting module: faulthandler [assertion] + pytest_plugin_registered [hook] + plugin: <_pytest.stepwise.StepwisePlugin object at 0x7f3410ae0940> + manager: <_pytest.config.PytestPluginManager object at 0x7f341c1b92b0> + finish pytest_plugin_registered --> [] [hook] + pytest_plugin_registered [hook] + plugin: <_pytest.config.PytestPluginManager object at 0x7f341c1b92b0> + manager: <_pytest.config.PytestPluginManager object at 0x7f341c1b92b0> + finish pytest_plugin_registered --> [] [hook] + pytest_plugin_registered [hook] + plugin: <_pytest.config.Config object at 0x7f3418040dd8> + manager: <_pytest.config.PytestPluginManager object at 0x7f341c1b92b0> + finish pytest_plugin_registered --> [] [hook] + pytest_plugin_registered [hook] + plugin: + manager: <_pytest.config.PytestPluginManager object at 0x7f341c1b92b0> + finish pytest_plugin_registered --> [] [hook] + pytest_plugin_registered [hook] + plugin: + manager: <_pytest.config.PytestPluginManager object at 0x7f341c1b92b0> + finish pytest_plugin_registered --> [] [hook] + pytest_plugin_registered [hook] + plugin: + manager: <_pytest.config.PytestPluginManager object at 0x7f341c1b92b0> + finish pytest_plugin_registered --> [] [hook] + pytest_plugin_registered [hook] + plugin: + manager: <_pytest.config.PytestPluginManager object at 0x7f341c1b92b0> + finish pytest_plugin_registered --> [] [hook] + pytest_plugin_registered [hook] + plugin: + manager: <_pytest.config.PytestPluginManager object at 0x7f341c1b92b0> + finish pytest_plugin_registered --> [] [hook] + pytest_plugin_registered [hook] + plugin: + manager: <_pytest.config.PytestPluginManager object at 0x7f341c1b92b0> + finish pytest_plugin_registered --> [] [hook] + pytest_plugin_registered [hook] + plugin: + manager: <_pytest.config.PytestPluginManager object at 0x7f341c1b92b0> + finish pytest_plugin_registered --> [] [hook] + pytest_plugin_registered [hook] + plugin: + manager: <_pytest.config.PytestPluginManager object at 0x7f341c1b92b0> + finish pytest_plugin_registered --> [] [hook] + pytest_plugin_registered [hook] + plugin: + manager: <_pytest.config.PytestPluginManager object at 0x7f341c1b92b0> + finish pytest_plugin_registered --> [] [hook] + pytest_plugin_registered [hook] + plugin: + manager: <_pytest.config.PytestPluginManager object at 0x7f341c1b92b0> + finish pytest_plugin_registered --> [] [hook] + pytest_plugin_registered [hook] + plugin: + manager: <_pytest.config.PytestPluginManager object at 0x7f341c1b92b0> + finish pytest_plugin_registered --> [] [hook] + pytest_plugin_registered [hook] + plugin: + manager: <_pytest.config.PytestPluginManager object at 0x7f341c1b92b0> + finish pytest_plugin_registered --> [] [hook] + pytest_plugin_registered [hook] + plugin: + manager: <_pytest.config.PytestPluginManager object at 0x7f341c1b92b0> + finish pytest_plugin_registered --> [] [hook] + pytest_plugin_registered [hook] + plugin: + manager: <_pytest.config.PytestPluginManager object at 0x7f341c1b92b0> + finish pytest_plugin_registered --> [] [hook] + pytest_plugin_registered [hook] + plugin: + manager: <_pytest.config.PytestPluginManager object at 0x7f341c1b92b0> + finish pytest_plugin_registered --> [] [hook] + pytest_plugin_registered [hook] + plugin: + manager: <_pytest.config.PytestPluginManager object at 0x7f341c1b92b0> + finish pytest_plugin_registered --> [] [hook] + pytest_plugin_registered [hook] + plugin: + manager: <_pytest.config.PytestPluginManager object at 0x7f341c1b92b0> + finish pytest_plugin_registered --> [] [hook] + pytest_plugin_registered [hook] + plugin: + manager: <_pytest.config.PytestPluginManager object at 0x7f341c1b92b0> + finish pytest_plugin_registered --> [] [hook] + pytest_plugin_registered [hook] + plugin: + manager: <_pytest.config.PytestPluginManager object at 0x7f341c1b92b0> + finish pytest_plugin_registered --> [] [hook] + pytest_plugin_registered [hook] + plugin: + manager: <_pytest.config.PytestPluginManager object at 0x7f341c1b92b0> + finish pytest_plugin_registered --> [] [hook] + pytest_plugin_registered [hook] + plugin: + manager: <_pytest.config.PytestPluginManager object at 0x7f341c1b92b0> + finish pytest_plugin_registered --> [] [hook] + pytest_plugin_registered [hook] + plugin: + manager: <_pytest.config.PytestPluginManager object at 0x7f341c1b92b0> + finish pytest_plugin_registered --> [] [hook] + pytest_plugin_registered [hook] + plugin: + manager: <_pytest.config.PytestPluginManager object at 0x7f341c1b92b0> + finish pytest_plugin_registered --> [] [hook] + pytest_plugin_registered [hook] + plugin: + manager: <_pytest.config.PytestPluginManager object at 0x7f341c1b92b0> + finish pytest_plugin_registered --> [] [hook] + pytest_plugin_registered [hook] + plugin: + manager: <_pytest.config.PytestPluginManager object at 0x7f341c1b92b0> + finish pytest_plugin_registered --> [] [hook] + pytest_plugin_registered [hook] + plugin: + manager: <_pytest.config.PytestPluginManager object at 0x7f341c1b92b0> + finish pytest_plugin_registered --> [] [hook] + pytest_plugin_registered [hook] + plugin: + manager: <_pytest.config.PytestPluginManager object at 0x7f341c1b92b0> + finish pytest_plugin_registered --> [] [hook] + pytest_plugin_registered [hook] + plugin: + manager: <_pytest.config.PytestPluginManager object at 0x7f341c1b92b0> + finish pytest_plugin_registered --> [] [hook] + pytest_plugin_registered [hook] + plugin: + manager: <_pytest.config.PytestPluginManager object at 0x7f341c1b92b0> + finish pytest_plugin_registered --> [] [hook] + pytest_plugin_registered [hook] + plugin: + manager: <_pytest.config.PytestPluginManager object at 0x7f341c1b92b0> + finish pytest_plugin_registered --> [] [hook] + pytest_plugin_registered [hook] + plugin: + manager: <_pytest.config.PytestPluginManager object at 0x7f341c1b92b0> + finish pytest_plugin_registered --> [] [hook] + pytest_plugin_registered [hook] + plugin: + manager: <_pytest.config.PytestPluginManager object at 0x7f341c1b92b0> + finish pytest_plugin_registered --> [] [hook] + pytest_plugin_registered [hook] + plugin: err= in_= _state='suspended' _in_suspended=''> _current_item=None> + manager: <_pytest.config.PytestPluginManager object at 0x7f341c1b92b0> + finish pytest_plugin_registered --> [] [hook] + pytest_plugin_registered [hook] + plugin: testsfailed=0 testscollected=0> + manager: <_pytest.config.PytestPluginManager object at 0x7f341c1b92b0> + finish pytest_plugin_registered --> [] [hook] + pytest_plugin_registered [hook] + plugin: <_pytest.cacheprovider.LFPlugin object at 0x7f3410ae0630> + manager: <_pytest.config.PytestPluginManager object at 0x7f341c1b92b0> + finish pytest_plugin_registered --> [] [hook] + pytest_plugin_registered [hook] + plugin: <_pytest.cacheprovider.NFPlugin object at 0x7f3410ae0828> + manager: <_pytest.config.PytestPluginManager object at 0x7f341c1b92b0> + finish pytest_plugin_registered --> [] [hook] + pytest_plugin_registered [hook] + plugin: <_pytest.stepwise.StepwisePlugin object at 0x7f3410ae0940> + manager: <_pytest.config.PytestPluginManager object at 0x7f341c1b92b0> + finish pytest_plugin_registered --> [] [hook] + pytest_plugin_registered [hook] + plugin: <_pytest.terminal.TerminalReporter object at 0x7f3410ae0be0> + manager: <_pytest.config.PytestPluginManager object at 0x7f341c1b92b0> + finish pytest_plugin_registered --> [] [hook] + pytest_plugin_registered [hook] + plugin: <_pytest.logging.LoggingPlugin object at 0x7f3410a76320> + manager: <_pytest.config.PytestPluginManager object at 0x7f341c1b92b0> + finish pytest_plugin_registered --> [] [hook] + finish pytest_configure --> [] [hook] + pytest_sessionstart [hook] + session: testsfailed=0 testscollected=0> + pytest_plugin_registered [hook] + plugin: <_pytest.config.PytestPluginManager object at 0x7f341c1b92b0> + manager: <_pytest.config.PytestPluginManager object at 0x7f341c1b92b0> + finish pytest_plugin_registered --> [] [hook] + pytest_plugin_registered [hook] + plugin: <_pytest.config.Config object at 0x7f3418040dd8> + manager: <_pytest.config.PytestPluginManager object at 0x7f341c1b92b0> + finish pytest_plugin_registered --> [] [hook] + pytest_plugin_registered [hook] + plugin: + manager: <_pytest.config.PytestPluginManager object at 0x7f341c1b92b0> + finish pytest_plugin_registered --> [] [hook] + pytest_plugin_registered [hook] + plugin: + manager: <_pytest.config.PytestPluginManager object at 0x7f341c1b92b0> + finish pytest_plugin_registered --> [] [hook] + pytest_plugin_registered [hook] + plugin: + manager: <_pytest.config.PytestPluginManager object at 0x7f341c1b92b0> + finish pytest_plugin_registered --> [] [hook] + pytest_plugin_registered [hook] + plugin: + manager: <_pytest.config.PytestPluginManager object at 0x7f341c1b92b0> + finish pytest_plugin_registered --> [] [hook] + pytest_plugin_registered [hook] + plugin: + manager: <_pytest.config.PytestPluginManager object at 0x7f341c1b92b0> + finish pytest_plugin_registered --> [] [hook] + pytest_plugin_registered [hook] + plugin: + manager: <_pytest.config.PytestPluginManager object at 0x7f341c1b92b0> + finish pytest_plugin_registered --> [] [hook] + pytest_plugin_registered [hook] + plugin: + manager: <_pytest.config.PytestPluginManager object at 0x7f341c1b92b0> + finish pytest_plugin_registered --> [] [hook] + pytest_plugin_registered [hook] + plugin: + manager: <_pytest.config.PytestPluginManager object at 0x7f341c1b92b0> + finish pytest_plugin_registered --> [] [hook] + pytest_plugin_registered [hook] + plugin: + manager: <_pytest.config.PytestPluginManager object at 0x7f341c1b92b0> + finish pytest_plugin_registered --> [] [hook] + pytest_plugin_registered [hook] + plugin: + manager: <_pytest.config.PytestPluginManager object at 0x7f341c1b92b0> + finish pytest_plugin_registered --> [] [hook] + pytest_plugin_registered [hook] + plugin: + manager: <_pytest.config.PytestPluginManager object at 0x7f341c1b92b0> + finish pytest_plugin_registered --> [] [hook] + pytest_plugin_registered [hook] + plugin: + manager: <_pytest.config.PytestPluginManager object at 0x7f341c1b92b0> + finish pytest_plugin_registered --> [] [hook] + pytest_plugin_registered [hook] + plugin: + manager: <_pytest.config.PytestPluginManager object at 0x7f341c1b92b0> + finish pytest_plugin_registered --> [] [hook] + pytest_plugin_registered [hook] + plugin: + manager: <_pytest.config.PytestPluginManager object at 0x7f341c1b92b0> + finish pytest_plugin_registered --> [] [hook] + pytest_plugin_registered [hook] + plugin: + manager: <_pytest.config.PytestPluginManager object at 0x7f341c1b92b0> + finish pytest_plugin_registered --> [] [hook] + pytest_plugin_registered [hook] + plugin: + manager: <_pytest.config.PytestPluginManager object at 0x7f341c1b92b0> + finish pytest_plugin_registered --> [] [hook] + pytest_plugin_registered [hook] + plugin: + manager: <_pytest.config.PytestPluginManager object at 0x7f341c1b92b0> + finish pytest_plugin_registered --> [] [hook] + pytest_plugin_registered [hook] + plugin: + manager: <_pytest.config.PytestPluginManager object at 0x7f341c1b92b0> + finish pytest_plugin_registered --> [] [hook] + pytest_plugin_registered [hook] + plugin: + manager: <_pytest.config.PytestPluginManager object at 0x7f341c1b92b0> + finish pytest_plugin_registered --> [] [hook] + pytest_plugin_registered [hook] + plugin: + manager: <_pytest.config.PytestPluginManager object at 0x7f341c1b92b0> + finish pytest_plugin_registered --> [] [hook] + pytest_plugin_registered [hook] + plugin: + manager: <_pytest.config.PytestPluginManager object at 0x7f341c1b92b0> + finish pytest_plugin_registered --> [] [hook] + pytest_plugin_registered [hook] + plugin: + manager: <_pytest.config.PytestPluginManager object at 0x7f341c1b92b0> + finish pytest_plugin_registered --> [] [hook] + pytest_plugin_registered [hook] + plugin: + manager: <_pytest.config.PytestPluginManager object at 0x7f341c1b92b0> + finish pytest_plugin_registered --> [] [hook] + pytest_plugin_registered [hook] + plugin: + manager: <_pytest.config.PytestPluginManager object at 0x7f341c1b92b0> + finish pytest_plugin_registered --> [] [hook] + pytest_plugin_registered [hook] + plugin: + manager: <_pytest.config.PytestPluginManager object at 0x7f341c1b92b0> + finish pytest_plugin_registered --> [] [hook] + pytest_plugin_registered [hook] + plugin: + manager: <_pytest.config.PytestPluginManager object at 0x7f341c1b92b0> + finish pytest_plugin_registered --> [] [hook] + pytest_plugin_registered [hook] + plugin: + manager: <_pytest.config.PytestPluginManager object at 0x7f341c1b92b0> + finish pytest_plugin_registered --> [] [hook] + pytest_plugin_registered [hook] + plugin: + manager: <_pytest.config.PytestPluginManager object at 0x7f341c1b92b0> + finish pytest_plugin_registered --> [] [hook] + pytest_plugin_registered [hook] + plugin: + manager: <_pytest.config.PytestPluginManager object at 0x7f341c1b92b0> + finish pytest_plugin_registered --> [] [hook] + pytest_plugin_registered [hook] + plugin: + manager: <_pytest.config.PytestPluginManager object at 0x7f341c1b92b0> + finish pytest_plugin_registered --> [] [hook] + pytest_plugin_registered [hook] + plugin: + manager: <_pytest.config.PytestPluginManager object at 0x7f341c1b92b0> + finish pytest_plugin_registered --> [] [hook] + pytest_plugin_registered [hook] + plugin: + manager: <_pytest.config.PytestPluginManager object at 0x7f341c1b92b0> + finish pytest_plugin_registered --> [] [hook] + pytest_plugin_registered [hook] + plugin: err= in_= _state='suspended' _in_suspended=''> _current_item=None> + manager: <_pytest.config.PytestPluginManager object at 0x7f341c1b92b0> + finish pytest_plugin_registered --> [] [hook] + pytest_plugin_registered [hook] + plugin: testsfailed=0 testscollected=0> + manager: <_pytest.config.PytestPluginManager object at 0x7f341c1b92b0> + finish pytest_plugin_registered --> [] [hook] + pytest_plugin_registered [hook] + plugin: <_pytest.cacheprovider.LFPlugin object at 0x7f3410ae0630> + manager: <_pytest.config.PytestPluginManager object at 0x7f341c1b92b0> + finish pytest_plugin_registered --> [] [hook] + pytest_plugin_registered [hook] + plugin: <_pytest.cacheprovider.NFPlugin object at 0x7f3410ae0828> + manager: <_pytest.config.PytestPluginManager object at 0x7f341c1b92b0> + finish pytest_plugin_registered --> [] [hook] + pytest_plugin_registered [hook] + plugin: <_pytest.stepwise.StepwisePlugin object at 0x7f3410ae0940> + manager: <_pytest.config.PytestPluginManager object at 0x7f341c1b92b0> + finish pytest_plugin_registered --> [] [hook] + pytest_plugin_registered [hook] + plugin: <_pytest.terminal.TerminalReporter object at 0x7f3410ae0be0> + manager: <_pytest.config.PytestPluginManager object at 0x7f341c1b92b0> + finish pytest_plugin_registered --> [] [hook] + pytest_plugin_registered [hook] + plugin: <_pytest.logging.LoggingPlugin object at 0x7f3410a76320> + manager: <_pytest.config.PytestPluginManager object at 0x7f341c1b92b0> + finish pytest_plugin_registered --> [] [hook] + pytest_plugin_registered [hook] + plugin: <_pytest.fixtures.FixtureManager object at 0x7f3410a76828> + manager: <_pytest.config.PytestPluginManager object at 0x7f341c1b92b0> + finish pytest_plugin_registered --> [] [hook] + pytest_report_header [hook] + config: <_pytest.config.Config object at 0x7f3418040dd8> + startdir: /usr/src/love/manager + finish pytest_report_header --> [['rootdir: /usr/src/love/manager, inifile: pytest.ini', 'plugins: env-0.6.2, asyncio-0.10.0, django-3.5.1'], ['Django settings: manager.settings (from ini file)'], ['using: pytest-5.2.1 pylib-1.8.0', 'setuptools registered plugins:', ' pytest-env-0.6.2 at /usr/local/lib/python3.7/site-packages/pytest_env/plugin.py', ' pytest-asyncio-0.10.0 at /usr/local/lib/python3.7/site-packages/pytest_asyncio/plugin.py', ' pytest-django-3.5.1 at /usr/local/lib/python3.7/site-packages/pytest_django/plugin.py']] [hook] + finish pytest_sessionstart --> [] [hook] + pytest_collection [hook] + session: testsfailed=0 testscollected=0> + perform_collect testsfailed=0 testscollected=0> ['/usr/src/love/manager'] [collection] + pytest_collectstart [hook] + collector: testsfailed=0 testscollected=0> + finish pytest_collectstart --> [] [hook] + pytest_make_collect_report [hook] + collector: testsfailed=0 testscollected=0> + processing argument /usr/src/love/manager [collection] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/.pytest_cache + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + early skip of rewriting module: py._code [assertion] + early skip of rewriting module: py._code.code [assertion] + early skip of rewriting module: repr [assertion] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/ui_framework + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_directory [hook] + path: /usr/src/love/manager/ui_framework + parent: testsfailed=0 testscollected=0> + finish pytest_collect_directory --> None [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/static_files + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_directory [hook] + path: /usr/src/love/manager/static_files + parent: testsfailed=0 testscollected=0> + finish pytest_collect_directory --> None [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/templates + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_directory [hook] + path: /usr/src/love/manager/templates + parent: testsfailed=0 testscollected=0> + finish pytest_collect_directory --> None [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/subscription + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_directory [hook] + path: /usr/src/love/manager/subscription + parent: testsfailed=0 testscollected=0> + finish pytest_collect_directory --> None [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/manager + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_directory [hook] + path: /usr/src/love/manager/manager + parent: testsfailed=0 testscollected=0> + finish pytest_collect_directory --> None [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/api + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_directory [hook] + path: /usr/src/love/manager/api + parent: testsfailed=0 testscollected=0> + finish pytest_collect_directory --> None [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/media + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_directory [hook] + path: /usr/src/love/manager/media + parent: testsfailed=0 testscollected=0> + finish pytest_collect_directory --> None [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/db.sqlite3 + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/db.sqlite3 + parent: testsfailed=0 testscollected=0> + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/manage.py + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/manage.py + parent: testsfailed=0 testscollected=0> + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/pytest.ini + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/pytest.ini + parent: testsfailed=0 testscollected=0> + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/pytestdebug.log + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/pytestdebug.log + parent: testsfailed=0 testscollected=0> + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/requirements.txt + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/requirements.txt + parent: testsfailed=0 testscollected=0> + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/runserver-dev.sh + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/runserver-dev.sh + parent: testsfailed=0 testscollected=0> + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/runserver.sh + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/runserver.sh + parent: testsfailed=0 testscollected=0> + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/api/management + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_directory [hook] + path: /usr/src/love/manager/api/management + parent: testsfailed=0 testscollected=0> + finish pytest_collect_directory --> None [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/api/tests + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_directory [hook] + path: /usr/src/love/manager/api/tests + parent: testsfailed=0 testscollected=0> + finish pytest_collect_directory --> None [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/api/migrations + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_directory [hook] + path: /usr/src/love/manager/api/migrations + parent: testsfailed=0 testscollected=0> + finish pytest_collect_directory --> None [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/api/__init__.py + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/api/__init__.py + parent: testsfailed=0 testscollected=0> + pytest_pycollect_makemodule [hook] + path: /usr/src/love/manager/api/__init__.py + parent: testsfailed=0 testscollected=0> + finish pytest_pycollect_makemodule --> [hook] + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/api/management/commands + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_directory [hook] + path: /usr/src/love/manager/api/management/commands + parent: testsfailed=0 testscollected=0> + finish pytest_collect_directory --> None [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/api/management/commands/createusers.py + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/api/management/commands/createusers.py + parent: testsfailed=0 testscollected=0> + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/api/management/commands/tests.py + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/api/management/commands/tests.py + parent: testsfailed=0 testscollected=0> + pytest_pycollect_makemodule [hook] + path: /usr/src/love/manager/api/management/commands/tests.py + parent: testsfailed=0 testscollected=0> + finish pytest_pycollect_makemodule --> [hook] + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/api/migrations/__init__.py + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/api/migrations/__init__.py + parent: testsfailed=0 testscollected=0> + pytest_pycollect_makemodule [hook] + path: /usr/src/love/manager/api/migrations/__init__.py + parent: testsfailed=0 testscollected=0> + finish pytest_pycollect_makemodule --> [hook] + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/api/tests/__init__.py + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/api/tests/__init__.py + parent: testsfailed=0 testscollected=0> + pytest_pycollect_makemodule [hook] + path: /usr/src/love/manager/api/tests/__init__.py + parent: testsfailed=0 testscollected=0> + finish pytest_pycollect_makemodule --> [hook] + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/manager/__init__.py + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/manager/__init__.py + parent: testsfailed=0 testscollected=0> + pytest_pycollect_makemodule [hook] + path: /usr/src/love/manager/manager/__init__.py + parent: testsfailed=0 testscollected=0> + finish pytest_pycollect_makemodule --> [hook] + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/media/thumbnails + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_directory [hook] + path: /usr/src/love/manager/media/thumbnails + parent: testsfailed=0 testscollected=0> + finish pytest_collect_directory --> None [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/media/thumbnails/0ba35267-f3a.png + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/media/thumbnails/0ba35267-f3a.png + parent: testsfailed=0 testscollected=0> + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/media/thumbnails/adb5bff8-ff3.png + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/media/thumbnails/adb5bff8-ff3.png + parent: testsfailed=0 testscollected=0> + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/media/thumbnails/e79df1ea-33a.png + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/media/thumbnails/e79df1ea-33a.png + parent: testsfailed=0 testscollected=0> + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/media/thumbnails/view_1.png + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/media/thumbnails/view_1.png + parent: testsfailed=0 testscollected=0> + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/media/thumbnails/view_13.png + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/media/thumbnails/view_13.png + parent: testsfailed=0 testscollected=0> + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/media/thumbnails/view_15.png + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/media/thumbnails/view_15.png + parent: testsfailed=0 testscollected=0> + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/media/thumbnails/view_17.png + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/media/thumbnails/view_17.png + parent: testsfailed=0 testscollected=0> + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/media/thumbnails/view_18.png + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/media/thumbnails/view_18.png + parent: testsfailed=0 testscollected=0> + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/media/thumbnails/view_19.png + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/media/thumbnails/view_19.png + parent: testsfailed=0 testscollected=0> + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/media/thumbnails/view_20.png + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/media/thumbnails/view_20.png + parent: testsfailed=0 testscollected=0> + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/media/thumbnails/view_21.png + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/media/thumbnails/view_21.png + parent: testsfailed=0 testscollected=0> + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/media/thumbnails/view_22.png + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/media/thumbnails/view_22.png + parent: testsfailed=0 testscollected=0> + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/media/thumbnails/view_23.png + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/media/thumbnails/view_23.png + parent: testsfailed=0 testscollected=0> + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/media/thumbnails/view_24.png + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/media/thumbnails/view_24.png + parent: testsfailed=0 testscollected=0> + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/media/thumbnails/view_25.png + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/media/thumbnails/view_25.png + parent: testsfailed=0 testscollected=0> + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/media/thumbnails/view_26.png + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/media/thumbnails/view_26.png + parent: testsfailed=0 testscollected=0> + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/media/thumbnails/view_27.png + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/media/thumbnails/view_27.png + parent: testsfailed=0 testscollected=0> + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/media/thumbnails/view_28.png + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/media/thumbnails/view_28.png + parent: testsfailed=0 testscollected=0> + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/media/thumbnails/view_29.png + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/media/thumbnails/view_29.png + parent: testsfailed=0 testscollected=0> + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/media/thumbnails/view_30.png + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/media/thumbnails/view_30.png + parent: testsfailed=0 testscollected=0> + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/media/thumbnails/view_31.png + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/media/thumbnails/view_31.png + parent: testsfailed=0 testscollected=0> + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/media/thumbnails/view_32.png + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/media/thumbnails/view_32.png + parent: testsfailed=0 testscollected=0> + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/media/thumbnails/view_33.png + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/media/thumbnails/view_33.png + parent: testsfailed=0 testscollected=0> + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/media/thumbnails/view_34.png + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/media/thumbnails/view_34.png + parent: testsfailed=0 testscollected=0> + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/media/thumbnails/view_37.png + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/media/thumbnails/view_37.png + parent: testsfailed=0 testscollected=0> + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/media/thumbnails/view_38.png + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/media/thumbnails/view_38.png + parent: testsfailed=0 testscollected=0> + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/media/thumbnails/view_40.png + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/media/thumbnails/view_40.png + parent: testsfailed=0 testscollected=0> + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/media/thumbnails/view_41.png + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/media/thumbnails/view_41.png + parent: testsfailed=0 testscollected=0> + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/static_files/Theme Template for Bootstrap_files + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_directory [hook] + path: /usr/src/love/manager/static_files/Theme Template for Bootstrap_files + parent: testsfailed=0 testscollected=0> + finish pytest_collect_directory --> None [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/static_files/css + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_directory [hook] + path: /usr/src/love/manager/static_files/css + parent: testsfailed=0 testscollected=0> + finish pytest_collect_directory --> None [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/static_files/Theme Template for Bootstrap_files/bootstrap-theme.min.css + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/static_files/Theme Template for Bootstrap_files/bootstrap-theme.min.css + parent: testsfailed=0 testscollected=0> + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/static_files/Theme Template for Bootstrap_files/bootstrap.min.css + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/static_files/Theme Template for Bootstrap_files/bootstrap.min.css + parent: testsfailed=0 testscollected=0> + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/static_files/Theme Template for Bootstrap_files/bootstrap.min.js + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/static_files/Theme Template for Bootstrap_files/bootstrap.min.js + parent: testsfailed=0 testscollected=0> + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/static_files/Theme Template for Bootstrap_files/docs.min.js + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/static_files/Theme Template for Bootstrap_files/docs.min.js + parent: testsfailed=0 testscollected=0> + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/static_files/Theme Template for Bootstrap_files/ie-emulation-modes-warning.js + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/static_files/Theme Template for Bootstrap_files/ie-emulation-modes-warning.js + parent: testsfailed=0 testscollected=0> + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/static_files/Theme Template for Bootstrap_files/ie10-viewport-bug-workaround.css + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/static_files/Theme Template for Bootstrap_files/ie10-viewport-bug-workaround.css + parent: testsfailed=0 testscollected=0> + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/static_files/Theme Template for Bootstrap_files/ie10-viewport-bug-workaround.js + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/static_files/Theme Template for Bootstrap_files/ie10-viewport-bug-workaround.js + parent: testsfailed=0 testscollected=0> + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/static_files/Theme Template for Bootstrap_files/jquery-1.12.4.min.js + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/static_files/Theme Template for Bootstrap_files/jquery-1.12.4.min.js + parent: testsfailed=0 testscollected=0> + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/static_files/Theme Template for Bootstrap_files/theme.css + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/static_files/Theme Template for Bootstrap_files/theme.css + parent: testsfailed=0 testscollected=0> + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/static_files/css/love-ui.css + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/static_files/css/love-ui.css + parent: testsfailed=0 testscollected=0> + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/subscription/tests + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_directory [hook] + path: /usr/src/love/manager/subscription/tests + parent: testsfailed=0 testscollected=0> + finish pytest_collect_directory --> None [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/subscription/migrations + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_directory [hook] + path: /usr/src/love/manager/subscription/migrations + parent: testsfailed=0 testscollected=0> + finish pytest_collect_directory --> None [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/subscription/__init__.py + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/subscription/__init__.py + parent: testsfailed=0 testscollected=0> + pytest_pycollect_makemodule [hook] + path: /usr/src/love/manager/subscription/__init__.py + parent: testsfailed=0 testscollected=0> + finish pytest_pycollect_makemodule --> [hook] + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/subscription/migrations/__init__.py + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/subscription/migrations/__init__.py + parent: testsfailed=0 testscollected=0> + pytest_pycollect_makemodule [hook] + path: /usr/src/love/manager/subscription/migrations/__init__.py + parent: testsfailed=0 testscollected=0> + finish pytest_pycollect_makemodule --> [hook] + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/subscription/tests/test_commands.py + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/subscription/tests/test_commands.py + parent: testsfailed=0 testscollected=0> + pytest_pycollect_makemodule [hook] + path: /usr/src/love/manager/subscription/tests/test_commands.py + parent: testsfailed=0 testscollected=0> + finish pytest_pycollect_makemodule --> [hook] + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/subscription/tests/test_connection.py + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/subscription/tests/test_connection.py + parent: testsfailed=0 testscollected=0> + pytest_pycollect_makemodule [hook] + path: /usr/src/love/manager/subscription/tests/test_connection.py + parent: testsfailed=0 testscollected=0> + finish pytest_pycollect_makemodule --> [hook] + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/subscription/tests/test_heartbeat.py + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/subscription/tests/test_heartbeat.py + parent: testsfailed=0 testscollected=0> + pytest_pycollect_makemodule [hook] + path: /usr/src/love/manager/subscription/tests/test_heartbeat.py + parent: testsfailed=0 testscollected=0> + finish pytest_pycollect_makemodule --> [hook] + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/subscription/tests/test_lovecsc_subscriptions.py + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/subscription/tests/test_lovecsc_subscriptions.py + parent: testsfailed=0 testscollected=0> + pytest_pycollect_makemodule [hook] + path: /usr/src/love/manager/subscription/tests/test_lovecsc_subscriptions.py + parent: testsfailed=0 testscollected=0> + finish pytest_pycollect_makemodule --> [hook] + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/subscription/tests/test_subscriptions.py + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/subscription/tests/test_subscriptions.py + parent: testsfailed=0 testscollected=0> + pytest_pycollect_makemodule [hook] + path: /usr/src/love/manager/subscription/tests/test_subscriptions.py + parent: testsfailed=0 testscollected=0> + finish pytest_pycollect_makemodule --> [hook] + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/templates/registration + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_directory [hook] + path: /usr/src/love/manager/templates/registration + parent: testsfailed=0 testscollected=0> + finish pytest_collect_directory --> None [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/templates/index.html + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/templates/index.html + parent: testsfailed=0 testscollected=0> + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/templates/test.html + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/templates/test.html + parent: testsfailed=0 testscollected=0> + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/templates/registration/login.html + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/templates/registration/login.html + parent: testsfailed=0 testscollected=0> + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/ui_framework/tests + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_directory [hook] + path: /usr/src/love/manager/ui_framework/tests + parent: testsfailed=0 testscollected=0> + finish pytest_collect_directory --> None [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/ui_framework/migrations + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_directory [hook] + path: /usr/src/love/manager/ui_framework/migrations + parent: testsfailed=0 testscollected=0> + finish pytest_collect_directory --> None [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/ui_framework/fixtures + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_directory [hook] + path: /usr/src/love/manager/ui_framework/fixtures + parent: testsfailed=0 testscollected=0> + finish pytest_collect_directory --> None [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/ui_framework/__init__.py + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/ui_framework/__init__.py + parent: testsfailed=0 testscollected=0> + pytest_pycollect_makemodule [hook] + path: /usr/src/love/manager/ui_framework/__init__.py + parent: testsfailed=0 testscollected=0> + finish pytest_pycollect_makemodule --> [hook] + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/ui_framework/fixtures/initial_data.json + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/ui_framework/fixtures/initial_data.json + parent: testsfailed=0 testscollected=0> + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/ui_framework/migrations/__init__.py + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/ui_framework/migrations/__init__.py + parent: testsfailed=0 testscollected=0> + pytest_pycollect_makemodule [hook] + path: /usr/src/love/manager/ui_framework/migrations/__init__.py + parent: testsfailed=0 testscollected=0> + finish pytest_pycollect_makemodule --> [hook] + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/ui_framework/tests/media + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_directory [hook] + path: /usr/src/love/manager/ui_framework/tests/media + parent: testsfailed=0 testscollected=0> + finish pytest_collect_directory --> None [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/ui_framework/tests/__init__.py + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/ui_framework/tests/__init__.py + parent: testsfailed=0 testscollected=0> + pytest_pycollect_makemodule [hook] + path: /usr/src/love/manager/ui_framework/tests/__init__.py + parent: testsfailed=0 testscollected=0> + finish pytest_pycollect_makemodule --> [hook] + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/ui_framework/tests/media/mock + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_directory [hook] + path: /usr/src/love/manager/ui_framework/tests/media/mock + parent: testsfailed=0 testscollected=0> + finish pytest_collect_directory --> None [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/ui_framework/tests/media/thumbnails + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_directory [hook] + path: /usr/src/love/manager/ui_framework/tests/media/thumbnails + parent: testsfailed=0 testscollected=0> + finish pytest_collect_directory --> None [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/ui_framework/tests/media/mock/test + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/ui_framework/tests/media/mock/test + parent: testsfailed=0 testscollected=0> + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/ui_framework/tests/media/mock/test.png + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/ui_framework/tests/media/mock/test.png + parent: testsfailed=0 testscollected=0> + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/ui_framework/tests/media/thumbnails/.gitinclude + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/ui_framework/tests/media/thumbnails/.gitinclude + parent: testsfailed=0 testscollected=0> + finish pytest_collect_file --> [] [hook] + finish pytest_make_collect_report --> [hook] + pytest_collectreport [hook] + report: + finish pytest_collectreport --> [] [hook] + genitems [collection] + pytest_collectstart [hook] + collector: + finish pytest_collectstart --> [] [hook] + pytest_make_collect_report [hook] + collector: + pytest_ignore_collect [hook] + path: /usr/src/love/manager/api/management + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_directory [hook] + path: /usr/src/love/manager/api/management + parent: + finish pytest_collect_directory --> None [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/api/tests + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_directory [hook] + path: /usr/src/love/manager/api/tests + parent: + finish pytest_collect_directory --> None [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/api/migrations + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_directory [hook] + path: /usr/src/love/manager/api/migrations + parent: + finish pytest_collect_directory --> None [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/api/admin.py + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/api/admin.py + parent: + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/api/apps.py + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/api/apps.py + parent: + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/api/authentication.py + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/api/authentication.py + parent: + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/api/middleware.py + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/api/middleware.py + parent: + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/api/models.py + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/api/models.py + parent: + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/api/schema_validator.py + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/api/schema_validator.py + parent: + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/api/serializers.py + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/api/serializers.py + parent: + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/api/signals.py + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/api/signals.py + parent: + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/api/urls.py + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/api/urls.py + parent: + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/api/views.py + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/api/views.py + parent: + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/api/management/commands + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_directory [hook] + path: /usr/src/love/manager/api/management/commands + parent: + finish pytest_collect_directory --> None [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/api/management/commands/createusers.py + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/api/management/commands/tests.py + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/api/migrations/__init__.py + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/api/tests/__init__.py + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + finish pytest_make_collect_report --> [hook] + pytest_collectreport [hook] + report: + finish pytest_collectreport --> [] [hook] + genitems [collection] + pytest_collectstart [hook] + collector: + finish pytest_collectstart --> [] [hook] + pytest_make_collect_report [hook] + collector: + find_module called for: tests [assertion] + matched test file '/usr/src/love/manager/api/management/commands/tests.py' [assertion] + found cached rewritten pyc for '/usr/src/love/manager/api/management/commands/tests.py' [assertion] + early skip of rewriting module: django.test [assertion] + early skip of rewriting module: django.test.client [assertion] + early skip of rewriting module: django.test.signals [assertion] + early skip of rewriting module: django.test.utils [assertion] + early skip of rewriting module: xml.dom [assertion] + early skip of rewriting module: xml.dom.domreg [assertion] + early skip of rewriting module: xml.dom.minidom [assertion] + early skip of rewriting module: xml.dom.minicompat [assertion] + early skip of rewriting module: xml.dom.xmlbuilder [assertion] + early skip of rewriting module: xml.dom.NodeFilter [assertion] + early skip of rewriting module: django.test.testcases [assertion] + early skip of rewriting module: django.core.management.sql [assertion] + early skip of rewriting module: django.test.html [assertion] + early skip of rewriting module: api.management [assertion] + early skip of rewriting module: api.management.commands [assertion] + early skip of rewriting module: api.management.commands.createusers [assertion] + pytest_pycollect_makeitem [hook] + collector: + name: __name__ + obj: tests + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __doc__ + obj: Test users' authentication thorugh the API. + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __package__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __loader__ + obj: <_pytest.assertion.rewrite.AssertionRewritingHook object at 0x7f34179d8898> + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __spec__ + obj: ModuleSpec(name='tests', loader=<_pytest.assertion.rewrite.AssertionRewritingHook object at 0x7f34179d8898>, origin='/usr/src/love/manager/api/management/commands/tests.py') + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __file__ + obj: /usr/src/love/manager/api/management/commands/tests.py + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __cached__ + obj: /usr/src/love/manager/api/management/commands/__pycache__/tests.cpython-37.pyc + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __builtins__ + obj: {'__name__': 'builtins', '__doc__': "Built-in functions, exceptions, and other objects.\n\nNoteworthy: None is the `nil' object; Ellipsis represents `...' in slices.", '__package__': '', '__loader__': , '__spec__': ModuleSpec(name='builtins', loader=), '__build_class__': , '__import__': , 'abs': , 'all': , 'any': , 'ascii': , 'bin': , 'breakpoint': , 'callable': , 'chr': , 'compile': , 'delattr': , 'dir': , 'divmod': , 'eval': , 'exec': , 'format': , 'getattr': , 'globals': , 'hasattr': , 'hash': , 'hex': , 'id': , 'input': , 'isinstance': , 'issubclass': , 'iter': , 'len': , 'locals': , 'max': , 'min': , 'next': , 'oct': , 'ord': , 'pow': , 'print': , 'repr': , 'round': , 'setattr': , 'sorted': , 'sum': , 'vars': , 'None': None, 'Ellipsis': Ellipsis, 'NotImplemented': NotImplemented, 'False': False, 'True': True, 'bool': , 'memoryview': , 'bytearray': , 'bytes': , 'classmethod': , 'complex': , 'dict': , 'enumerate': , 'filter': , 'float': , 'frozenset': , 'property': , 'int': , 'list': , 'map': , 'object': , 'range': , 'reversed': , 'set': , 'slice': , 'staticmethod': , 'str': , 'super': , 'tuple': , 'type': , 'zip': , '__debug__': True, 'BaseException': , 'Exception': , 'TypeError': , 'StopAsyncIteration': , 'StopIteration': , 'GeneratorExit': , 'SystemExit': , 'KeyboardInterrupt': , 'ImportError': , 'ModuleNotFoundError': , 'OSError': , 'EnvironmentError': , 'IOError': , 'EOFError': , 'RuntimeError': , 'RecursionError': , 'NotImplementedError': , 'NameError': , 'UnboundLocalError': , 'AttributeError': , 'SyntaxError': , 'IndentationError': , 'TabError': , 'LookupError': , 'IndexError': , 'KeyError': , 'ValueError': , 'UnicodeError': , 'UnicodeEncodeError': , 'UnicodeDecodeError': , 'UnicodeTranslateError': , 'AssertionError': , 'ArithmeticError': , 'FloatingPointError': , 'OverflowError': , 'ZeroDivisionError': , 'SystemError': , 'ReferenceError': , 'MemoryError': , 'BufferError': , 'Warning': , 'UserWarning': , 'DeprecationWarning': , 'PendingDeprecationWarning': , 'SyntaxWarning': , 'RuntimeWarning': , 'FutureWarning': , 'ImportWarning': , 'UnicodeWarning': , 'BytesWarning': , 'ResourceWarning': , 'ConnectionError': , 'BlockingIOError': , 'BrokenPipeError': , 'ChildProcessError': , 'ConnectionAbortedError': , 'ConnectionRefusedError': , 'ConnectionResetError': , 'FileExistsError': , 'FileNotFoundError': , 'IsADirectoryError': , 'NotADirectoryError': , 'InterruptedError': , 'PermissionError': , 'ProcessLookupError': , 'TimeoutError': , 'open': , 'quit': Use quit() or Ctrl-D (i.e. EOF) to exit, 'exit': Use exit() or Ctrl-D (i.e. EOF) to exit, 'copyright': Copyright (c) 2001-2018 Python Software Foundation. +All Rights Reserved. + +Copyright (c) 2000 BeOpen.com. +All Rights Reserved. + +Copyright (c) 1995-2001 Corporation for National Research Initiatives. +All Rights Reserved. + +Copyright (c) 1991-1995 Stichting Mathematisch Centrum, Amsterdam. +All Rights Reserved., 'credits': Thanks to CWI, CNRI, BeOpen.com, Zope Corporation and a cast of thousands + for supporting Python development. See www.python.org for more information., 'license': Type license() to see the full license text, 'help': Type help() for interactive help, or help(object) for help about object.} + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: @py_builtins + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: @pytest_ar + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: TestCase + obj: + finish pytest_pycollect_makeitem --> [hook] + pytest_pycollect_makeitem [hook] + collector: + name: User + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: Group + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: Command + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: admin_username + obj: admin + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: user_username + obj: user + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: cmd_user_username + obj: cmd_user + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: cmd_groupname + obj: cmd + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: cmd_permission_codename + obj: api.command.execute_command + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: CreateusersTestCase + obj: + finish pytest_pycollect_makeitem --> [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __repr__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __getattribute__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __setattr__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __delattr__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __init__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __new__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __dir__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __dict__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __hash__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __str__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __lt__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __le__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __eq__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __ne__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __gt__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __ge__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __reduce_ex__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __reduce__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __subclasshook__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __init_subclass__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __format__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __sizeof__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __class__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + finish pytest_make_collect_report --> [hook] + genitems [collection] + pytest_collectstart [hook] + collector: + finish pytest_collectstart --> [] [hook] + pytest_make_collect_report [hook] + collector: + finish pytest_make_collect_report --> [hook] + pytest_collectreport [hook] + report: + finish pytest_collectreport --> [] [hook] + genitems [collection] + pytest_collectstart [hook] + collector: + finish pytest_collectstart --> [] [hook] + pytest_make_collect_report [hook] + collector: + finish pytest_make_collect_report --> [hook] + genitems [collection] + pytest_itemcollected [hook] + item: + finish pytest_itemcollected --> [] [hook] + genitems [collection] + pytest_itemcollected [hook] + item: + finish pytest_itemcollected --> [] [hook] + pytest_collectreport [hook] + report: + finish pytest_collectreport --> [] [hook] + pytest_collectreport [hook] + report: + finish pytest_collectreport --> [] [hook] + genitems [collection] + pytest_collectstart [hook] + collector: + finish pytest_collectstart --> [] [hook] + pytest_make_collect_report [hook] + collector: + pytest_ignore_collect [hook] + path: /usr/src/love/manager/api/migrations/0001_initial.py + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/api/migrations/0001_initial.py + parent: + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/api/migrations/0002_auto_20190528_1546.py + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/api/migrations/0002_auto_20190528_1546.py + parent: + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/api/migrations/0003_auto_20190528_1552.py + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/api/migrations/0003_auto_20190528_1552.py + parent: + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/api/migrations/0004_globalpermissions.py + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/api/migrations/0004_globalpermissions.py + parent: + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/api/migrations/0005_auto_20190722_1622.py + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/api/migrations/0005_auto_20190722_1622.py + parent: + finish pytest_collect_file --> [] [hook] + finish pytest_make_collect_report --> [hook] + pytest_collectreport [hook] + report: + finish pytest_collectreport --> [] [hook] + genitems [collection] + pytest_collectstart [hook] + collector: + finish pytest_collectstart --> [] [hook] + pytest_make_collect_report [hook] + collector: + pytest_ignore_collect [hook] + path: /usr/src/love/manager/api/tests/test_schema_validation.py + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/api/tests/test_schema_validation.py + parent: + pytest_pycollect_makemodule [hook] + path: /usr/src/love/manager/api/tests/test_schema_validation.py + parent: + finish pytest_pycollect_makemodule --> [hook] + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/api/tests/tests_auth_api.py + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/api/tests/tests_auth_api.py + parent: + pytest_pycollect_makemodule [hook] + path: /usr/src/love/manager/api/tests/tests_auth_api.py + parent: + finish pytest_pycollect_makemodule --> [hook] + finish pytest_collect_file --> [] [hook] + finish pytest_make_collect_report --> [hook] + genitems [collection] + pytest_collectstart [hook] + collector: + finish pytest_collectstart --> [] [hook] + pytest_make_collect_report [hook] + collector: + find_module called for: api.tests [assertion] + find_module called for: api.tests.test_schema_validation [assertion] + matched test file '/usr/src/love/manager/api/tests/test_schema_validation.py' [assertion] + found cached rewritten pyc for '/usr/src/love/manager/api/tests/test_schema_validation.py' [assertion] + early skip of rewriting module: rest_framework.test [assertion] + early skip of rewriting module: rest_framework.compat [assertion] + early skip of rewriting module: coreapi [assertion] + early skip of rewriting module: coreapi.auth [assertion] + early skip of rewriting module: coreapi.utils [assertion] + early skip of rewriting module: coreapi.exceptions [assertion] + early skip of rewriting module: coreapi.compat [assertion] + early skip of rewriting module: urlparse [assertion] + early skip of rewriting module: click [assertion] + early skip of rewriting module: requests [assertion] + early skip of rewriting module: urllib3 [assertion] + early skip of rewriting module: urllib3.connectionpool [assertion] + early skip of rewriting module: urllib3.exceptions [assertion] + early skip of rewriting module: urllib3.packages [assertion] + early skip of rewriting module: urllib3.packages.ssl_match_hostname [assertion] + early skip of rewriting module: urllib3.packages.six [assertion] + early skip of rewriting module: urllib3.packages.six.moves [assertion] + early skip of rewriting module: urllib3.packages.six.moves.http_client [assertion] + early skip of rewriting module: urllib3.connection [assertion] + early skip of rewriting module: urllib3.util [assertion] + early skip of rewriting module: urllib3.util.connection [assertion] + early skip of rewriting module: urllib3.util.wait [assertion] + early skip of rewriting module: urllib3.contrib [assertion] + early skip of rewriting module: urllib3.contrib._appengine_environ [assertion] + early skip of rewriting module: urllib3.util.request [assertion] + early skip of rewriting module: brotli [assertion] + early skip of rewriting module: urllib3.util.response [assertion] + early skip of rewriting module: urllib3.util.ssl_ [assertion] + early skip of rewriting module: urllib3.util.url [assertion] + early skip of rewriting module: urllib3.util.timeout [assertion] + early skip of rewriting module: urllib3.util.retry [assertion] + early skip of rewriting module: urllib3._collections [assertion] + early skip of rewriting module: urllib3.request [assertion] + early skip of rewriting module: urllib3.filepost [assertion] + early skip of rewriting module: urllib3.fields [assertion] + early skip of rewriting module: urllib3.packages.six.moves.urllib [assertion] + early skip of rewriting module: urllib3.packages.six.moves.urllib.parse [assertion] + early skip of rewriting module: urllib3.response [assertion] + early skip of rewriting module: brotli [assertion] + early skip of rewriting module: urllib3.util.queue [assertion] + early skip of rewriting module: urllib3.poolmanager [assertion] + early skip of rewriting module: chardet [assertion] + early skip of rewriting module: chardet.compat [assertion] + early skip of rewriting module: chardet.universaldetector [assertion] + early skip of rewriting module: chardet.charsetgroupprober [assertion] + early skip of rewriting module: chardet.enums [assertion] + early skip of rewriting module: chardet.charsetprober [assertion] + early skip of rewriting module: chardet.escprober [assertion] + early skip of rewriting module: chardet.codingstatemachine [assertion] + early skip of rewriting module: chardet.escsm [assertion] + early skip of rewriting module: chardet.latin1prober [assertion] + early skip of rewriting module: chardet.mbcsgroupprober [assertion] + early skip of rewriting module: chardet.utf8prober [assertion] + early skip of rewriting module: chardet.mbcssm [assertion] + early skip of rewriting module: chardet.sjisprober [assertion] + early skip of rewriting module: chardet.mbcharsetprober [assertion] + early skip of rewriting module: chardet.chardistribution [assertion] + early skip of rewriting module: chardet.euctwfreq [assertion] + early skip of rewriting module: chardet.euckrfreq [assertion] + early skip of rewriting module: chardet.gb2312freq [assertion] + early skip of rewriting module: chardet.big5freq [assertion] + early skip of rewriting module: chardet.jisfreq [assertion] + early skip of rewriting module: chardet.jpcntx [assertion] + early skip of rewriting module: chardet.eucjpprober [assertion] + early skip of rewriting module: chardet.gb2312prober [assertion] + early skip of rewriting module: chardet.euckrprober [assertion] + early skip of rewriting module: chardet.cp949prober [assertion] + early skip of rewriting module: chardet.big5prober [assertion] + early skip of rewriting module: chardet.euctwprober [assertion] + early skip of rewriting module: chardet.sbcsgroupprober [assertion] + early skip of rewriting module: chardet.sbcharsetprober [assertion] + early skip of rewriting module: chardet.langcyrillicmodel [assertion] + early skip of rewriting module: chardet.langgreekmodel [assertion] + early skip of rewriting module: chardet.langbulgarianmodel [assertion] + early skip of rewriting module: chardet.langthaimodel [assertion] + early skip of rewriting module: chardet.langhebrewmodel [assertion] + early skip of rewriting module: chardet.hebrewprober [assertion] + early skip of rewriting module: chardet.langturkishmodel [assertion] + early skip of rewriting module: chardet.version [assertion] + early skip of rewriting module: requests.exceptions [assertion] + early skip of rewriting module: urllib3.contrib.pyopenssl [assertion] + early skip of rewriting module: OpenSSL [assertion] + early skip of rewriting module: requests.__version__ [assertion] + early skip of rewriting module: requests.utils [assertion] + early skip of rewriting module: requests.certs [assertion] + early skip of rewriting module: certifi [assertion] + early skip of rewriting module: certifi.core [assertion] + early skip of rewriting module: requests._internal_utils [assertion] + early skip of rewriting module: requests.compat [assertion] + early skip of rewriting module: simplejson [assertion] + early skip of rewriting module: requests.cookies [assertion] + early skip of rewriting module: requests.structures [assertion] + early skip of rewriting module: requests.packages [assertion] + early skip of rewriting module: idna [assertion] + early skip of rewriting module: idna.package_data [assertion] + early skip of rewriting module: idna.core [assertion] + early skip of rewriting module: idna.idnadata [assertion] + early skip of rewriting module: idna.intranges [assertion] + early skip of rewriting module: requests.models [assertion] + early skip of rewriting module: encodings.idna [assertion] + early skip of rewriting module: stringprep [assertion] + early skip of rewriting module: requests.hooks [assertion] + early skip of rewriting module: requests.auth [assertion] + early skip of rewriting module: requests.status_codes [assertion] + early skip of rewriting module: requests.api [assertion] + early skip of rewriting module: requests.sessions [assertion] + early skip of rewriting module: requests.adapters [assertion] + early skip of rewriting module: urllib3.contrib.socks [assertion] + early skip of rewriting module: socks [assertion] + early skip of rewriting module: coreapi.codecs [assertion] + early skip of rewriting module: coreapi.codecs.base [assertion] + early skip of rewriting module: itypes [assertion] + early skip of rewriting module: coreapi.codecs.corejson [assertion] + early skip of rewriting module: coreapi.document [assertion] + early skip of rewriting module: coreschema [assertion] + early skip of rewriting module: coreschema.schemas [assertion] + early skip of rewriting module: coreschema.compat [assertion] + early skip of rewriting module: coreschema.formats [assertion] + early skip of rewriting module: coreschema.utils [assertion] + early skip of rewriting module: coreschema.encodings [assertion] + early skip of rewriting module: coreschema.encodings.html [assertion] + early skip of rewriting module: coreapi.codecs.display [assertion] + early skip of rewriting module: coreapi.codecs.download [assertion] + early skip of rewriting module: coreapi.codecs.jsondata [assertion] + early skip of rewriting module: coreapi.codecs.python [assertion] + early skip of rewriting module: coreapi.codecs.text [assertion] + early skip of rewriting module: coreapi.transports [assertion] + early skip of rewriting module: coreapi.transports.base [assertion] + early skip of rewriting module: coreapi.transports.http [assertion] + early skip of rewriting module: uritemplate [assertion] + early skip of rewriting module: uritemplate.api [assertion] + early skip of rewriting module: uritemplate.orderedset [assertion] + early skip of rewriting module: uritemplate.template [assertion] + early skip of rewriting module: uritemplate.variable [assertion] + early skip of rewriting module: coreapi.client [assertion] + early skip of rewriting module: yaml [assertion] + early skip of rewriting module: yaml.error [assertion] + early skip of rewriting module: yaml.tokens [assertion] + early skip of rewriting module: yaml.events [assertion] + early skip of rewriting module: yaml.nodes [assertion] + early skip of rewriting module: yaml.loader [assertion] + early skip of rewriting module: yaml.reader [assertion] + early skip of rewriting module: yaml.scanner [assertion] + early skip of rewriting module: yaml.parser [assertion] + early skip of rewriting module: yaml.composer [assertion] + early skip of rewriting module: yaml.constructor [assertion] + early skip of rewriting module: yaml.resolver [assertion] + early skip of rewriting module: yaml.dumper [assertion] + early skip of rewriting module: yaml.emitter [assertion] + early skip of rewriting module: yaml.serializer [assertion] + early skip of rewriting module: yaml.representer [assertion] + early skip of rewriting module: yaml.cyaml [assertion] + early skip of rewriting module: _yaml [assertion] + early skip of rewriting module: markdown [assertion] + early skip of rewriting module: pygments [assertion] + early skip of rewriting module: pygments.util [assertion] + early skip of rewriting module: pygments.lexers [assertion] + early skip of rewriting module: pygments.lexers._mapping [assertion] + early skip of rewriting module: pygments.modeline [assertion] + early skip of rewriting module: pygments.plugin [assertion] + early skip of rewriting module: pygments.lexers.special [assertion] + early skip of rewriting module: pygments.lexer [assertion] + early skip of rewriting module: pygments.filter [assertion] + early skip of rewriting module: pygments.filters [assertion] + early skip of rewriting module: pygments.token [assertion] + early skip of rewriting module: pygments.regexopt [assertion] + early skip of rewriting module: pygments.formatters [assertion] + early skip of rewriting module: pygments.formatters._mapping [assertion] + early skip of rewriting module: pygments.formatters.html [assertion] + early skip of rewriting module: pygments.formatter [assertion] + early skip of rewriting module: pygments.styles [assertion] + early skip of rewriting module: ctags [assertion] + early skip of rewriting module: rest_framework.settings [assertion] + early skip of rewriting module: rest_framework.renderers [assertion] + early skip of rewriting module: rest_framework.exceptions [assertion] + early skip of rewriting module: rest_framework.status [assertion] + early skip of rewriting module: rest_framework.utils [assertion] + early skip of rewriting module: rest_framework.utils.serializer_helpers [assertion] + early skip of rewriting module: rest_framework.utils.json [assertion] + early skip of rewriting module: rest_framework.serializers [assertion] + early skip of rewriting module: rest_framework.fields [assertion] + early skip of rewriting module: rest_framework.utils.html [assertion] + early skip of rewriting module: rest_framework.utils.humanize_datetime [assertion] + early skip of rewriting module: rest_framework.utils.representation [assertion] + early skip of rewriting module: rest_framework.utils.formatting [assertion] + early skip of rewriting module: rest_framework.utils.model_meta [assertion] + early skip of rewriting module: rest_framework.utils.field_mapping [assertion] + early skip of rewriting module: rest_framework.validators [assertion] + early skip of rewriting module: rest_framework.relations [assertion] + early skip of rewriting module: rest_framework.reverse [assertion] + early skip of rewriting module: rest_framework.utils.urls [assertion] + early skip of rewriting module: rest_framework.request [assertion] + early skip of rewriting module: rest_framework.utils.encoders [assertion] + early skip of rewriting module: rest_framework.utils.breadcrumbs [assertion] + pytest_pycollect_makeitem [hook] + collector: + name: __name__ + obj: api.tests.test_schema_validation + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __doc__ + obj: None + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __package__ + obj: api.tests + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __loader__ + obj: <_pytest.assertion.rewrite.AssertionRewritingHook object at 0x7f34179d8898> + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __spec__ + obj: ModuleSpec(name='api.tests.test_schema_validation', loader=<_pytest.assertion.rewrite.AssertionRewritingHook object at 0x7f34179d8898>, origin='/usr/src/love/manager/api/tests/test_schema_validation.py') + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __file__ + obj: /usr/src/love/manager/api/tests/test_schema_validation.py + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __cached__ + obj: /usr/src/love/manager/api/tests/__pycache__/test_schema_validation.cpython-37.pyc + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __builtins__ + obj: {'__name__': 'builtins', '__doc__': "Built-in functions, exceptions, and other objects.\n\nNoteworthy: None is the `nil' object; Ellipsis represents `...' in slices.", '__package__': '', '__loader__': , '__spec__': ModuleSpec(name='builtins', loader=), '__build_class__': , '__import__': , 'abs': , 'all': , 'any': , 'ascii': , 'bin': , 'breakpoint': , 'callable': , 'chr': , 'compile': , 'delattr': , 'dir': , 'divmod': , 'eval': , 'exec': , 'format': , 'getattr': , 'globals': , 'hasattr': , 'hash': , 'hex': , 'id': , 'input': , 'isinstance': , 'issubclass': , 'iter': , 'len': , 'locals': , 'max': , 'min': , 'next': , 'oct': , 'ord': , 'pow': , 'print': , 'repr': , 'round': , 'setattr': , 'sorted': , 'sum': , 'vars': , 'None': None, 'Ellipsis': Ellipsis, 'NotImplemented': NotImplemented, 'False': False, 'True': True, 'bool': , 'memoryview': , 'bytearray': , 'bytes': , 'classmethod': , 'complex': , 'dict': , 'enumerate': , 'filter': , 'float': , 'frozenset': , 'property': , 'int': , 'list': , 'map': , 'object': , 'range': , 'reversed': , 'set': , 'slice': , 'staticmethod': , 'str': , 'super': , 'tuple': , 'type': , 'zip': , '__debug__': True, 'BaseException': , 'Exception': , 'TypeError': , 'StopAsyncIteration': , 'StopIteration': , 'GeneratorExit': , 'SystemExit': , 'KeyboardInterrupt': , 'ImportError': , 'ModuleNotFoundError': , 'OSError': , 'EnvironmentError': , 'IOError': , 'EOFError': , 'RuntimeError': , 'RecursionError': , 'NotImplementedError': , 'NameError': , 'UnboundLocalError': , 'AttributeError': , 'SyntaxError': , 'IndentationError': , 'TabError': , 'LookupError': , 'IndexError': , 'KeyError': , 'ValueError': , 'UnicodeError': , 'UnicodeEncodeError': , 'UnicodeDecodeError': , 'UnicodeTranslateError': , 'AssertionError': , 'ArithmeticError': , 'FloatingPointError': , 'OverflowError': , 'ZeroDivisionError': , 'SystemError': , 'ReferenceError': , 'MemoryError': , 'BufferError': , 'Warning': , 'UserWarning': , 'DeprecationWarning': , 'PendingDeprecationWarning': , 'SyntaxWarning': , 'RuntimeWarning': , 'FutureWarning': , 'ImportWarning': , 'UnicodeWarning': , 'BytesWarning': , 'ResourceWarning': , 'ConnectionError': , 'BlockingIOError': , 'BrokenPipeError': , 'ChildProcessError': , 'ConnectionAbortedError': , 'ConnectionRefusedError': , 'ConnectionResetError': , 'FileExistsError': , 'FileNotFoundError': , 'IsADirectoryError': , 'NotADirectoryError': , 'InterruptedError': , 'PermissionError': , 'ProcessLookupError': , 'TimeoutError': , 'open': , 'quit': Use quit() or Ctrl-D (i.e. EOF) to exit, 'exit': Use exit() or Ctrl-D (i.e. EOF) to exit, 'copyright': Copyright (c) 2001-2018 Python Software Foundation. +All Rights Reserved. + +Copyright (c) 2000 BeOpen.com. +All Rights Reserved. + +Copyright (c) 1995-2001 Corporation for National Research Initiatives. +All Rights Reserved. + +Copyright (c) 1991-1995 Stichting Mathematisch Centrum, Amsterdam. +All Rights Reserved., 'credits': Thanks to CWI, CNRI, BeOpen.com, Zope Corporation and a cast of thousands + for supporting Python development. See www.python.org for more information., 'license': Type license() to see the full license text, 'help': Type help() for interactive help, or help(object) for help about object.} + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: @py_builtins + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: @pytest_ar + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: TestCase + obj: + finish pytest_pycollect_makeitem --> [hook] + pytest_pycollect_makeitem [hook] + collector: + name: reverse + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: Token + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: APIClient + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: User + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: Permission + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: yaml + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: SchemaValidationTestCase + obj: + finish pytest_pycollect_makeitem --> [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __repr__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __getattribute__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __setattr__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __delattr__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __init__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __new__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __dir__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __dict__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __hash__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __str__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __lt__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __le__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __eq__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __ne__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __gt__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __ge__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __reduce_ex__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __reduce__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __subclasshook__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __init_subclass__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __format__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __sizeof__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __class__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + finish pytest_make_collect_report --> [hook] + genitems [collection] + pytest_collectstart [hook] + collector: + finish pytest_collectstart --> [] [hook] + pytest_make_collect_report [hook] + collector: + finish pytest_make_collect_report --> [hook] + pytest_collectreport [hook] + report: + finish pytest_collectreport --> [] [hook] + genitems [collection] + pytest_collectstart [hook] + collector: + finish pytest_collectstart --> [] [hook] + pytest_make_collect_report [hook] + collector: + finish pytest_make_collect_report --> [hook] + genitems [collection] + pytest_itemcollected [hook] + item: + finish pytest_itemcollected --> [] [hook] + genitems [collection] + pytest_itemcollected [hook] + item: + finish pytest_itemcollected --> [] [hook] + genitems [collection] + pytest_itemcollected [hook] + item: + finish pytest_itemcollected --> [] [hook] + pytest_collectreport [hook] + report: + finish pytest_collectreport --> [] [hook] + pytest_collectreport [hook] + report: + finish pytest_collectreport --> [] [hook] + genitems [collection] + pytest_collectstart [hook] + collector: + finish pytest_collectstart --> [] [hook] + pytest_make_collect_report [hook] + collector: + find_module called for: api.tests.tests_auth_api [assertion] + matched test file '/usr/src/love/manager/api/tests/tests_auth_api.py' [assertion] + found cached rewritten pyc for '/usr/src/love/manager/api/tests/tests_auth_api.py' [assertion] + early skip of rewriting module: freezegun [assertion] + early skip of rewriting module: freezegun.api [assertion] + early skip of rewriting module: dateutil [assertion] + early skip of rewriting module: dateutil._version [assertion] + early skip of rewriting module: dateutil.parser [assertion] + early skip of rewriting module: dateutil.parser._parser [assertion] + early skip of rewriting module: dateutil.relativedelta [assertion] + early skip of rewriting module: dateutil._common [assertion] + early skip of rewriting module: dateutil.tz [assertion] + early skip of rewriting module: dateutil.tz.tz [assertion] + early skip of rewriting module: dateutil.tz._common [assertion] + early skip of rewriting module: dateutil.tz._factories [assertion] + early skip of rewriting module: dateutil.tz.win [assertion] + early skip of rewriting module: six.moves.winreg [assertion] + early skip of rewriting module: contextmanager [assertion] + early skip of rewriting module: dateutil.parser.isoparser [assertion] + early skip of rewriting module: maya [assertion] + early skip of rewriting module: copy_reg [assertion] + early skip of rewriting module: freezegun._async [assertion] + early skip of rewriting module: sqlite3 [assertion] + early skip of rewriting module: sqlite3.dbapi2 [assertion] + early skip of rewriting module: _sqlite3 [assertion] + early skip of rewriting module: pymysql [assertion] + early skip of rewriting module: astropy [assertion] + early skip of rewriting module: astropy.version [assertion] + early skip of rewriting module: numpy [assertion] + early skip of rewriting module: numpy._globals [assertion] + early skip of rewriting module: numpy.__config__ [assertion] + early skip of rewriting module: numpy.version [assertion] + early skip of rewriting module: numpy._distributor_init [assertion] + early skip of rewriting module: numpy.core [assertion] + early skip of rewriting module: numpy.core.multiarray [assertion] + early skip of rewriting module: numpy.core.overrides [assertion] + early skip of rewriting module: numpy.core._multiarray_umath [assertion] + early skip of rewriting module: numpy.compat [assertion] + early skip of rewriting module: numpy.compat._inspect [assertion] + early skip of rewriting module: numpy.compat.py3k [assertion] + early skip of rewriting module: pickle5 [assertion] + early skip of rewriting module: numpy.core.umath [assertion] + early skip of rewriting module: numpy.core.numerictypes [assertion] + early skip of rewriting module: numpy.core._string_helpers [assertion] + early skip of rewriting module: numpy.core._type_aliases [assertion] + early skip of rewriting module: numpy.core._dtype [assertion] + early skip of rewriting module: numpy.core.numeric [assertion] + early skip of rewriting module: numpy.core.shape_base [assertion] + early skip of rewriting module: numpy.core._asarray [assertion] + early skip of rewriting module: numpy.core.fromnumeric [assertion] + early skip of rewriting module: numpy.core._methods [assertion] + early skip of rewriting module: numpy.core._exceptions [assertion] + early skip of rewriting module: numpy.core._ufunc_config [assertion] + early skip of rewriting module: numpy.core.arrayprint [assertion] + early skip of rewriting module: numpy.core.defchararray [assertion] + early skip of rewriting module: numpy.core.records [assertion] + early skip of rewriting module: numpy.core.memmap [assertion] + early skip of rewriting module: numpy.core.function_base [assertion] + early skip of rewriting module: numpy.core.machar [assertion] + early skip of rewriting module: numpy.core.getlimits [assertion] + early skip of rewriting module: numpy.core.einsumfunc [assertion] + early skip of rewriting module: numpy.core._add_newdocs [assertion] + early skip of rewriting module: numpy.core._multiarray_tests [assertion] + early skip of rewriting module: numpy.core._dtype_ctypes [assertion] + early skip of rewriting module: numpy.core._internal [assertion] + early skip of rewriting module: numpy._pytesttester [assertion] + early skip of rewriting module: numpy.lib [assertion] + early skip of rewriting module: numpy.lib.mixins [assertion] + early skip of rewriting module: numpy.lib.scimath [assertion] + early skip of rewriting module: numpy.lib.type_check [assertion] + early skip of rewriting module: numpy.lib.ufunclike [assertion] + early skip of rewriting module: numpy.lib.index_tricks [assertion] + early skip of rewriting module: numpy.matrixlib [assertion] + early skip of rewriting module: numpy.matrixlib.defmatrix [assertion] + early skip of rewriting module: numpy.linalg [assertion] + early skip of rewriting module: numpy.linalg.linalg [assertion] + early skip of rewriting module: numpy.lib.twodim_base [assertion] + early skip of rewriting module: numpy.linalg.lapack_lite [assertion] + early skip of rewriting module: numpy.linalg._umath_linalg [assertion] + early skip of rewriting module: numpy.lib.function_base [assertion] + early skip of rewriting module: numpy.lib.histograms [assertion] + early skip of rewriting module: numpy.lib.stride_tricks [assertion] + early skip of rewriting module: numpy.lib.nanfunctions [assertion] + early skip of rewriting module: numpy.lib.shape_base [assertion] + early skip of rewriting module: numpy.lib.polynomial [assertion] + early skip of rewriting module: numpy.lib.utils [assertion] + early skip of rewriting module: numpy.lib.arraysetops [assertion] + early skip of rewriting module: numpy.lib.npyio [assertion] + early skip of rewriting module: numpy.lib.format [assertion] + early skip of rewriting module: numpy.lib._datasource [assertion] + early skip of rewriting module: numpy.lib._iotools [assertion] + early skip of rewriting module: numpy.lib.financial [assertion] + early skip of rewriting module: numpy.lib.arrayterator [assertion] + early skip of rewriting module: numpy.lib.arraypad [assertion] + early skip of rewriting module: numpy.lib._version [assertion] + early skip of rewriting module: numpy.fft [assertion] + early skip of rewriting module: numpy.fft._pocketfft [assertion] + early skip of rewriting module: numpy.fft._pocketfft_internal [assertion] + early skip of rewriting module: numpy.fft.helper [assertion] + early skip of rewriting module: numpy.polynomial [assertion] + early skip of rewriting module: numpy.polynomial.polynomial [assertion] + early skip of rewriting module: numpy.polynomial.polyutils [assertion] + early skip of rewriting module: numpy.polynomial._polybase [assertion] + early skip of rewriting module: numpy.polynomial.chebyshev [assertion] + early skip of rewriting module: numpy.polynomial.legendre [assertion] + early skip of rewriting module: numpy.polynomial.hermite [assertion] + early skip of rewriting module: numpy.polynomial.hermite_e [assertion] + early skip of rewriting module: numpy.polynomial.laguerre [assertion] + early skip of rewriting module: numpy.random [assertion] + early skip of rewriting module: numpy.random._pickle [assertion] + early skip of rewriting module: numpy.random.mtrand [assertion] + early skip of rewriting module: numpy.random._bit_generator [assertion] + early skip of rewriting module: numpy.random._common [assertion] + early skip of rewriting module: secrets [assertion] + early skip of rewriting module: numpy.random._bounded_integers [assertion] + early skip of rewriting module: numpy.random._mt19937 [assertion] + early skip of rewriting module: numpy.random._philox [assertion] + early skip of rewriting module: numpy.random._pcg64 [assertion] + early skip of rewriting module: numpy.random._sfc64 [assertion] + early skip of rewriting module: numpy.random._generator [assertion] + early skip of rewriting module: numpy.ctypeslib [assertion] + early skip of rewriting module: numpy.ma [assertion] + early skip of rewriting module: numpy.ma.core [assertion] + early skip of rewriting module: numpy.ma.extras [assertion] + early skip of rewriting module: astropy.utils [assertion] + early skip of rewriting module: astropy.utils.codegen [assertion] + early skip of rewriting module: astropy.utils.introspection [assertion] + early skip of rewriting module: astropy.utils.decorators [assertion] + early skip of rewriting module: astropy.utils.exceptions [assertion] + early skip of rewriting module: astropy.utils.misc [assertion] + early skip of rewriting module: astropy.config [assertion] + early skip of rewriting module: astropy.config.paths [assertion] + early skip of rewriting module: astropy.config.configuration [assertion] + early skip of rewriting module: astropy.extern [assertion] + early skip of rewriting module: astropy.extern.configobj [assertion] + early skip of rewriting module: astropy.extern.configobj.configobj [assertion] + early skip of rewriting module: astropy.extern.configobj.validate [assertion] + early skip of rewriting module: astropy.config.affiliated [assertion] + find_module called for: astropy.tests [assertion] + early skip of rewriting module: astropy.tests.runner [assertion] + early skip of rewriting module: astropy.logger [assertion] + early skip of rewriting module: astropy.utils._compiler [assertion] + early skip of rewriting module: astropy.time [assertion] + early skip of rewriting module: astropy.time.formats [assertion] + early skip of rewriting module: astropy.units [assertion] + early skip of rewriting module: astropy.units.core [assertion] + early skip of rewriting module: astropy.units.utils [assertion] + early skip of rewriting module: fractions [assertion] + early skip of rewriting module: astropy.units.format [assertion] + early skip of rewriting module: astropy.units.format.base [assertion] + early skip of rewriting module: astropy.units.format.generic [assertion] + early skip of rewriting module: astropy.units.format.utils [assertion] + early skip of rewriting module: astropy.units.format.cds [assertion] + early skip of rewriting module: astropy.units.format.console [assertion] + early skip of rewriting module: astropy.units.format.fits [assertion] + early skip of rewriting module: astropy.units.format.latex [assertion] + early skip of rewriting module: astropy.units.format.ogip [assertion] + early skip of rewriting module: astropy.units.format.unicode_format [assertion] + early skip of rewriting module: astropy.units.format.vounit [assertion] + early skip of rewriting module: astropy.units.quantity [assertion] + early skip of rewriting module: astropy.utils.compat [assertion] + early skip of rewriting module: astropy.utils.compat.misc [assertion] + early skip of rewriting module: astropy.utils.compat.numpycompat [assertion] + early skip of rewriting module: astropy.utils.data_info [assertion] + early skip of rewriting module: astropy.utils.metadata [assertion] + early skip of rewriting module: astropy.units.quantity_helper [assertion] + early skip of rewriting module: astropy.units.quantity_helper.converters [assertion] + early skip of rewriting module: astropy.units.quantity_helper.helpers [assertion] + early skip of rewriting module: astropy.units.quantity_helper.scipy_special [assertion] + early skip of rewriting module: astropy.units.quantity_helper.erfa [assertion] + early skip of rewriting module: astropy.units.si [assertion] + early skip of rewriting module: astropy.constants [assertion] + early skip of rewriting module: astropy.constants.constant [assertion] + early skip of rewriting module: astropy.constants.si [assertion] + early skip of rewriting module: astropy.constants.codata2014 [assertion] + early skip of rewriting module: astropy.constants.iau2015 [assertion] + early skip of rewriting module: astropy.constants.cgs [assertion] + early skip of rewriting module: astropy.constants.utils [assertion] + early skip of rewriting module: astropy.units.cgs [assertion] + early skip of rewriting module: astropy.units.astrophys [assertion] + early skip of rewriting module: astropy.extern.ply [assertion] + early skip of rewriting module: astropy.extern.ply.yacc [assertion] + early skip of rewriting module: astropy.units.format.generic_parsetab [assertion] + early skip of rewriting module: astropy.extern.ply.lex [assertion] + early skip of rewriting module: astropy.units.format.generic_lextab [assertion] + early skip of rewriting module: astropy.units.photometric [assertion] + early skip of rewriting module: astropy.units.function [assertion] + early skip of rewriting module: astropy.units.function.core [assertion] + early skip of rewriting module: astropy.units.function.logarithmic [assertion] + early skip of rewriting module: astropy.units.function.units [assertion] + early skip of rewriting module: astropy.units.function.mixin [assertion] + early skip of rewriting module: astropy.units.physical [assertion] + early skip of rewriting module: astropy.units.imperial [assertion] + early skip of rewriting module: astropy.units.equivalencies [assertion] + early skip of rewriting module: astropy.units.function.magnitude_zero_points [assertion] + early skip of rewriting module: astropy.units.decorators [assertion] + early skip of rewriting module: astropy._erfa [assertion] + early skip of rewriting module: astropy._erfa.core [assertion] + early skip of rewriting module: astropy._erfa.ufunc [assertion] + early skip of rewriting module: astropy.time.utils [assertion] + early skip of rewriting module: astropy.time.core [assertion] + early skip of rewriting module: astropy.extern._strptime [assertion] + pytest_pycollect_makeitem [hook] + collector: + name: __name__ + obj: api.tests.tests_auth_api + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __doc__ + obj: Test users' authentication through the API. + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __package__ + obj: api.tests + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __loader__ + obj: <_pytest.assertion.rewrite.AssertionRewritingHook object at 0x7f34179d8898> + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __spec__ + obj: ModuleSpec(name='api.tests.tests_auth_api', loader=<_pytest.assertion.rewrite.AssertionRewritingHook object at 0x7f34179d8898>, origin='/usr/src/love/manager/api/tests/tests_auth_api.py') + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __file__ + obj: /usr/src/love/manager/api/tests/tests_auth_api.py + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __cached__ + obj: /usr/src/love/manager/api/tests/__pycache__/tests_auth_api.cpython-37.pyc + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __builtins__ + obj: {'__name__': 'builtins', '__doc__': "Built-in functions, exceptions, and other objects.\n\nNoteworthy: None is the `nil' object; Ellipsis represents `...' in slices.", '__package__': '', '__loader__': , '__spec__': ModuleSpec(name='builtins', loader=), '__build_class__': , '__import__': , 'abs': , 'all': , 'any': , 'ascii': , 'bin': , 'breakpoint': , 'callable': , 'chr': , 'compile': , 'delattr': , 'dir': , 'divmod': , 'eval': , 'exec': , 'format': , 'getattr': , 'globals': , 'hasattr': , 'hash': , 'hex': , 'id': , 'input': , 'isinstance': , 'issubclass': , 'iter': , 'len': , 'locals': , 'max': , 'min': , 'next': , 'oct': , 'ord': , 'pow': , 'print': , 'repr': , 'round': , 'setattr': , 'sorted': , 'sum': , 'vars': , 'None': None, 'Ellipsis': Ellipsis, 'NotImplemented': NotImplemented, 'False': False, 'True': True, 'bool': , 'memoryview': , 'bytearray': , 'bytes': , 'classmethod': , 'complex': , 'dict': , 'enumerate': , 'filter': , 'float': , 'frozenset': , 'property': , 'int': , 'list': , 'map': , 'object': , 'range': , 'reversed': , 'set': , 'slice': , 'staticmethod': , 'str': , 'super': , 'tuple': , 'type': , 'zip': , '__debug__': True, 'BaseException': , 'Exception': , 'TypeError': , 'StopAsyncIteration': , 'StopIteration': , 'GeneratorExit': , 'SystemExit': , 'KeyboardInterrupt': , 'ImportError': , 'ModuleNotFoundError': , 'OSError': , 'EnvironmentError': , 'IOError': , 'EOFError': , 'RuntimeError': , 'RecursionError': , 'NotImplementedError': , 'NameError': , 'UnboundLocalError': , 'AttributeError': , 'SyntaxError': , 'IndentationError': , 'TabError': , 'LookupError': , 'IndexError': , 'KeyError': , 'ValueError': , 'UnicodeError': , 'UnicodeEncodeError': , 'UnicodeDecodeError': , 'UnicodeTranslateError': , 'AssertionError': , 'ArithmeticError': , 'FloatingPointError': , 'OverflowError': , 'ZeroDivisionError': , 'SystemError': , 'ReferenceError': , 'MemoryError': , 'BufferError': , 'Warning': , 'UserWarning': , 'DeprecationWarning': , 'PendingDeprecationWarning': , 'SyntaxWarning': , 'RuntimeWarning': , 'FutureWarning': , 'ImportWarning': , 'UnicodeWarning': , 'BytesWarning': , 'ResourceWarning': , 'ConnectionError': , 'BlockingIOError': , 'BrokenPipeError': , 'ChildProcessError': , 'ConnectionAbortedError': , 'ConnectionRefusedError': , 'ConnectionResetError': , 'FileExistsError': , 'FileNotFoundError': , 'IsADirectoryError': , 'NotADirectoryError': , 'InterruptedError': , 'PermissionError': , 'ProcessLookupError': , 'TimeoutError': , 'open': , 'quit': Use quit() or Ctrl-D (i.e. EOF) to exit, 'exit': Use exit() or Ctrl-D (i.e. EOF) to exit, 'copyright': Copyright (c) 2001-2018 Python Software Foundation. +All Rights Reserved. + +Copyright (c) 2000 BeOpen.com. +All Rights Reserved. + +Copyright (c) 1995-2001 Corporation for National Research Initiatives. +All Rights Reserved. + +Copyright (c) 1991-1995 Stichting Mathematisch Centrum, Amsterdam. +All Rights Reserved., 'credits': Thanks to CWI, CNRI, BeOpen.com, Zope Corporation and a cast of thousands + for supporting Python development. See www.python.org for more information., 'license': Type license() to see the full license text, 'help': Type help() for interactive help, or help(object) for help about object.} + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: @py_builtins + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: @pytest_ar + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: datetime + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: TestCase + obj: + finish pytest_pycollect_makeitem --> [hook] + pytest_pycollect_makeitem [hook] + collector: + name: reverse + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: User + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: Permission + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: freeze_time + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: APIClient + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: status + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: Token + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: settings + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: Time + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: AuthApiTestCase + obj: + finish pytest_pycollect_makeitem --> [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __repr__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __getattribute__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __setattr__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __delattr__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __init__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __new__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __dir__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __dict__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __hash__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __str__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __lt__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __le__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __eq__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __ne__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __gt__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __ge__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __reduce_ex__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __reduce__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __subclasshook__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __init_subclass__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __format__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __sizeof__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __class__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + finish pytest_make_collect_report --> [hook] + genitems [collection] + pytest_collectstart [hook] + collector: + finish pytest_collectstart --> [] [hook] + pytest_make_collect_report [hook] + collector: + finish pytest_make_collect_report --> [hook] + pytest_collectreport [hook] + report: + finish pytest_collectreport --> [] [hook] + genitems [collection] + pytest_collectstart [hook] + collector: + finish pytest_collectstart --> [] [hook] + pytest_make_collect_report [hook] + collector: + finish pytest_make_collect_report --> [hook] + genitems [collection] + pytest_itemcollected [hook] + item: + finish pytest_itemcollected --> [] [hook] + genitems [collection] + pytest_itemcollected [hook] + item: + finish pytest_itemcollected --> [] [hook] + genitems [collection] + pytest_itemcollected [hook] + item: + finish pytest_itemcollected --> [] [hook] + genitems [collection] + pytest_itemcollected [hook] + item: + finish pytest_itemcollected --> [] [hook] + genitems [collection] + pytest_itemcollected [hook] + item: + finish pytest_itemcollected --> [] [hook] + genitems [collection] + pytest_itemcollected [hook] + item: + finish pytest_itemcollected --> [] [hook] + genitems [collection] + pytest_itemcollected [hook] + item: + finish pytest_itemcollected --> [] [hook] + genitems [collection] + pytest_itemcollected [hook] + item: + finish pytest_itemcollected --> [] [hook] + pytest_collectreport [hook] + report: + finish pytest_collectreport --> [] [hook] + pytest_collectreport [hook] + report: + finish pytest_collectreport --> [] [hook] + pytest_collectreport [hook] + report: + finish pytest_collectreport --> [] [hook] + genitems [collection] + pytest_collectstart [hook] + collector: + finish pytest_collectstart --> [] [hook] + pytest_make_collect_report [hook] + collector: + pytest_ignore_collect [hook] + path: /usr/src/love/manager/manager/asgi.py + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/manager/asgi.py + parent: + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/manager/routing.py + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/manager/routing.py + parent: + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/manager/settings.py + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/manager/settings.py + parent: + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/manager/urls.py + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/manager/urls.py + parent: + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/manager/wsgi.py + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/manager/wsgi.py + parent: + finish pytest_collect_file --> [] [hook] + finish pytest_make_collect_report --> [hook] + pytest_collectreport [hook] + report: + finish pytest_collectreport --> [] [hook] + genitems [collection] + pytest_collectstart [hook] + collector: + finish pytest_collectstart --> [] [hook] + pytest_make_collect_report [hook] + collector: + pytest_ignore_collect [hook] + path: /usr/src/love/manager/subscription/tests + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_directory [hook] + path: /usr/src/love/manager/subscription/tests + parent: + finish pytest_collect_directory --> None [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/subscription/migrations + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_directory [hook] + path: /usr/src/love/manager/subscription/migrations + parent: + finish pytest_collect_directory --> None [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/subscription/apps.py + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/subscription/apps.py + parent: + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/subscription/auth.py + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/subscription/auth.py + parent: + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/subscription/consumers.py + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/subscription/consumers.py + parent: + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/subscription/heartbeat_manager.py + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/subscription/heartbeat_manager.py + parent: + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/subscription/routing.py + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/subscription/routing.py + parent: + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/subscription/migrations/__init__.py + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/subscription/tests/test_commands.py + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/subscription/tests/test_connection.py + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/subscription/tests/test_heartbeat.py + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/subscription/tests/test_lovecsc_subscriptions.py + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/subscription/tests/test_subscriptions.py + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + finish pytest_make_collect_report --> [hook] + pytest_collectreport [hook] + report: + finish pytest_collectreport --> [] [hook] + genitems [collection] + pytest_collectstart [hook] + collector: + finish pytest_collectstart --> [] [hook] + pytest_make_collect_report [hook] + collector: + finish pytest_make_collect_report --> [hook] + pytest_collectreport [hook] + report: + finish pytest_collectreport --> [] [hook] + genitems [collection] + pytest_collectstart [hook] + collector: + finish pytest_collectstart --> [] [hook] + pytest_make_collect_report [hook] + collector: + find_module called for: test_commands [assertion] + matched test file '/usr/src/love/manager/subscription/tests/test_commands.py' [assertion] + found cached rewritten pyc for '/usr/src/love/manager/subscription/tests/test_commands.py' [assertion] + early skip of rewriting module: channels.testing [assertion] + early skip of rewriting module: asgiref.testing [assertion] + early skip of rewriting module: asgiref.compatibility [assertion] + early skip of rewriting module: asgiref.timeout [assertion] + early skip of rewriting module: channels.testing.http [assertion] + early skip of rewriting module: channels.testing.live [assertion] + early skip of rewriting module: daphne.testing [assertion] + early skip of rewriting module: multiprocessing [assertion] + early skip of rewriting module: multiprocessing.context [assertion] + early skip of rewriting module: multiprocessing.process [assertion] + early skip of rewriting module: multiprocessing.reduction [assertion] + early skip of rewriting module: channels.testing.websocket [assertion] + early skip of rewriting module: manager.routing [assertion] + early skip of rewriting module: subscription.routing [assertion] + early skip of rewriting module: django.conf.urls [assertion] + early skip of rewriting module: django.views.defaults [assertion] + early skip of rewriting module: subscription.auth [assertion] + early skip of rewriting module: subscription.consumers [assertion] + early skip of rewriting module: channels.generic [assertion] + early skip of rewriting module: channels.generic.websocket [assertion] + early skip of rewriting module: channels.consumer [assertion] + early skip of rewriting module: channels.db [assertion] + early skip of rewriting module: channels.utils [assertion] + early skip of rewriting module: subscription.heartbeat_manager [assertion] + pytest_pycollect_makeitem [hook] + collector: + name: __name__ + obj: test_commands + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __doc__ + obj: Tests for the sending of comands. + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __package__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __loader__ + obj: <_pytest.assertion.rewrite.AssertionRewritingHook object at 0x7f34179d8898> + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __spec__ + obj: ModuleSpec(name='test_commands', loader=<_pytest.assertion.rewrite.AssertionRewritingHook object at 0x7f34179d8898>, origin='/usr/src/love/manager/subscription/tests/test_commands.py') + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __file__ + obj: /usr/src/love/manager/subscription/tests/test_commands.py + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __cached__ + obj: /usr/src/love/manager/subscription/tests/__pycache__/test_commands.cpython-37.pyc + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __builtins__ + obj: {'__name__': 'builtins', '__doc__': "Built-in functions, exceptions, and other objects.\n\nNoteworthy: None is the `nil' object; Ellipsis represents `...' in slices.", '__package__': '', '__loader__': , '__spec__': ModuleSpec(name='builtins', loader=), '__build_class__': , '__import__': , 'abs': , 'all': , 'any': , 'ascii': , 'bin': , 'breakpoint': , 'callable': , 'chr': , 'compile': , 'delattr': , 'dir': , 'divmod': , 'eval': , 'exec': , 'format': , 'getattr': , 'globals': , 'hasattr': , 'hash': , 'hex': , 'id': , 'input': , 'isinstance': , 'issubclass': , 'iter': , 'len': , 'locals': , 'max': , 'min': , 'next': , 'oct': , 'ord': , 'pow': , 'print': , 'repr': , 'round': , 'setattr': , 'sorted': , 'sum': , 'vars': , 'None': None, 'Ellipsis': Ellipsis, 'NotImplemented': NotImplemented, 'False': False, 'True': True, 'bool': , 'memoryview': , 'bytearray': , 'bytes': , 'classmethod': , 'complex': , 'dict': , 'enumerate': , 'filter': , 'float': , 'frozenset': , 'property': , 'int': , 'list': , 'map': , 'object': , 'range': , 'reversed': , 'set': , 'slice': , 'staticmethod': , 'str': , 'super': , 'tuple': , 'type': , 'zip': , '__debug__': True, 'BaseException': , 'Exception': , 'TypeError': , 'StopAsyncIteration': , 'StopIteration': , 'GeneratorExit': , 'SystemExit': , 'KeyboardInterrupt': , 'ImportError': , 'ModuleNotFoundError': , 'OSError': , 'EnvironmentError': , 'IOError': , 'EOFError': , 'RuntimeError': , 'RecursionError': , 'NotImplementedError': , 'NameError': , 'UnboundLocalError': , 'AttributeError': , 'SyntaxError': , 'IndentationError': , 'TabError': , 'LookupError': , 'IndexError': , 'KeyError': , 'ValueError': , 'UnicodeError': , 'UnicodeEncodeError': , 'UnicodeDecodeError': , 'UnicodeTranslateError': , 'AssertionError': , 'ArithmeticError': , 'FloatingPointError': , 'OverflowError': , 'ZeroDivisionError': , 'SystemError': , 'ReferenceError': , 'MemoryError': , 'BufferError': , 'Warning': , 'UserWarning': , 'DeprecationWarning': , 'PendingDeprecationWarning': , 'SyntaxWarning': , 'RuntimeWarning': , 'FutureWarning': , 'ImportWarning': , 'UnicodeWarning': , 'BytesWarning': , 'ResourceWarning': , 'ConnectionError': , 'BlockingIOError': , 'BrokenPipeError': , 'ChildProcessError': , 'ConnectionAbortedError': , 'ConnectionRefusedError': , 'ConnectionResetError': , 'FileExistsError': , 'FileNotFoundError': , 'IsADirectoryError': , 'NotADirectoryError': , 'InterruptedError': , 'PermissionError': , 'ProcessLookupError': , 'TimeoutError': , 'open': , 'quit': Use quit() or Ctrl-D (i.e. EOF) to exit, 'exit': Use exit() or Ctrl-D (i.e. EOF) to exit, 'copyright': Copyright (c) 2001-2018 Python Software Foundation. +All Rights Reserved. + +Copyright (c) 2000 BeOpen.com. +All Rights Reserved. + +Copyright (c) 1995-2001 Corporation for National Research Initiatives. +All Rights Reserved. + +Copyright (c) 1991-1995 Stichting Mathematisch Centrum, Amsterdam. +All Rights Reserved., 'credits': Thanks to CWI, CNRI, BeOpen.com, Zope Corporation and a cast of thousands + for supporting Python development. See www.python.org for more information., 'license': Type license() to see the full license text, 'help': Type help() for interactive help, or help(object) for help about object.} + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: @py_builtins + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: @pytest_ar + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: pytest + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: User + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: Permission + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: WebsocketCommunicator + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: application + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: Token + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: TestCommands + obj: + finish pytest_pycollect_makeitem --> [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __repr__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __getattribute__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __setattr__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __delattr__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __init__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __new__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __dir__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __dict__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __hash__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __str__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __lt__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __le__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __eq__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __ne__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __gt__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __ge__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __reduce_ex__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __reduce__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __subclasshook__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __init_subclass__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __format__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __sizeof__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __class__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + finish pytest_make_collect_report --> [hook] + genitems [collection] + pytest_collectstart [hook] + collector: + finish pytest_collectstart --> [] [hook] + pytest_make_collect_report [hook] + collector: + finish pytest_make_collect_report --> [hook] + genitems [collection] + pytest_collectstart [hook] + collector: + finish pytest_collectstart --> [] [hook] + pytest_make_collect_report [hook] + collector: + pytest_pycollect_makeitem [hook] + collector: + name: __module__ + obj: test_commands + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __doc__ + obj: Test that clients can or cannot send commands depending on different conditions. + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: categories + obj: ['event', 'telemetry', 'cmd'] + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: cscs + obj: ['ScriptQueue', 'ATDome'] + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: salindices + obj: [1, 2] + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: streams + obj: ['stream1', 'stream2'] + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: combinations + obj: [] + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: no_reception_timeout + obj: 0.001 + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: setup_method + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: build_messages + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: test_authorized_user_can_send_command + obj: + pytest_generate_tests [hook] + metafunc: <_pytest.python.Metafunc object at 0x7f33f912d8d0> + finish pytest_generate_tests --> [] [hook] + finish pytest_pycollect_makeitem --> [] [hook] + pytest_pycollect_makeitem [hook] + collector: + name: test_unauthorized_user_cannot_send_command + obj: + pytest_generate_tests [hook] + metafunc: <_pytest.python.Metafunc object at 0x7f33f917fe48> + finish pytest_generate_tests --> [] [hook] + finish pytest_pycollect_makeitem --> [] [hook] + pytest_pycollect_makeitem [hook] + collector: + name: test_authorized_user_gets_ack + obj: + pytest_generate_tests [hook] + metafunc: <_pytest.python.Metafunc object at 0x7f33f9136080> + finish pytest_generate_tests --> [] [hook] + finish pytest_pycollect_makeitem --> [] [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __dict__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __weakref__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: _Class__pytest_setup_method + obj: .xunit_setup_method_fixture at 0x7f33f91259d8> + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __repr__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __hash__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __str__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __getattribute__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __setattr__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __delattr__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __lt__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __le__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __eq__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __ne__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __gt__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __ge__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __init__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __new__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __reduce_ex__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __reduce__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __subclasshook__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __init_subclass__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __format__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __sizeof__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __dir__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __class__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + finish pytest_make_collect_report --> [hook] + genitems [collection] + pytest_itemcollected [hook] + item: + finish pytest_itemcollected --> [] [hook] + genitems [collection] + pytest_itemcollected [hook] + item: + finish pytest_itemcollected --> [] [hook] + genitems [collection] + pytest_itemcollected [hook] + item: + finish pytest_itemcollected --> [] [hook] + pytest_collectreport [hook] + report: + finish pytest_collectreport --> [] [hook] + pytest_collectreport [hook] + report: + finish pytest_collectreport --> [] [hook] + pytest_collectreport [hook] + report: + finish pytest_collectreport --> [] [hook] + genitems [collection] + pytest_collectstart [hook] + collector: + finish pytest_collectstart --> [] [hook] + pytest_make_collect_report [hook] + collector: + find_module called for: test_connection [assertion] + matched test file '/usr/src/love/manager/subscription/tests/test_connection.py' [assertion] + found cached rewritten pyc for '/usr/src/love/manager/subscription/tests/test_connection.py' [assertion] + pytest_pycollect_makeitem [hook] + collector: + name: __name__ + obj: test_connection + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __doc__ + obj: Tests for the connection of users. + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __package__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __loader__ + obj: <_pytest.assertion.rewrite.AssertionRewritingHook object at 0x7f34179d8898> + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __spec__ + obj: ModuleSpec(name='test_connection', loader=<_pytest.assertion.rewrite.AssertionRewritingHook object at 0x7f34179d8898>, origin='/usr/src/love/manager/subscription/tests/test_connection.py') + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __file__ + obj: /usr/src/love/manager/subscription/tests/test_connection.py + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __cached__ + obj: /usr/src/love/manager/subscription/tests/__pycache__/test_connection.cpython-37.pyc + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __builtins__ + obj: {'__name__': 'builtins', '__doc__': "Built-in functions, exceptions, and other objects.\n\nNoteworthy: None is the `nil' object; Ellipsis represents `...' in slices.", '__package__': '', '__loader__': , '__spec__': ModuleSpec(name='builtins', loader=), '__build_class__': , '__import__': , 'abs': , 'all': , 'any': , 'ascii': , 'bin': , 'breakpoint': , 'callable': , 'chr': , 'compile': , 'delattr': , 'dir': , 'divmod': , 'eval': , 'exec': , 'format': , 'getattr': , 'globals': , 'hasattr': , 'hash': , 'hex': , 'id': , 'input': , 'isinstance': , 'issubclass': , 'iter': , 'len': , 'locals': , 'max': , 'min': , 'next': , 'oct': , 'ord': , 'pow': , 'print': , 'repr': , 'round': , 'setattr': , 'sorted': , 'sum': , 'vars': , 'None': None, 'Ellipsis': Ellipsis, 'NotImplemented': NotImplemented, 'False': False, 'True': True, 'bool': , 'memoryview': , 'bytearray': , 'bytes': , 'classmethod': , 'complex': , 'dict': , 'enumerate': , 'filter': , 'float': , 'frozenset': , 'property': , 'int': , 'list': , 'map': , 'object': , 'range': , 'reversed': , 'set': , 'slice': , 'staticmethod': , 'str': , 'super': , 'tuple': , 'type': , 'zip': , '__debug__': True, 'BaseException': , 'Exception': , 'TypeError': , 'StopAsyncIteration': , 'StopIteration': , 'GeneratorExit': , 'SystemExit': , 'KeyboardInterrupt': , 'ImportError': , 'ModuleNotFoundError': , 'OSError': , 'EnvironmentError': , 'IOError': , 'EOFError': , 'RuntimeError': , 'RecursionError': , 'NotImplementedError': , 'NameError': , 'UnboundLocalError': , 'AttributeError': , 'SyntaxError': , 'IndentationError': , 'TabError': , 'LookupError': , 'IndexError': , 'KeyError': , 'ValueError': , 'UnicodeError': , 'UnicodeEncodeError': , 'UnicodeDecodeError': , 'UnicodeTranslateError': , 'AssertionError': , 'ArithmeticError': , 'FloatingPointError': , 'OverflowError': , 'ZeroDivisionError': , 'SystemError': , 'ReferenceError': , 'MemoryError': , 'BufferError': , 'Warning': , 'UserWarning': , 'DeprecationWarning': , 'PendingDeprecationWarning': , 'SyntaxWarning': , 'RuntimeWarning': , 'FutureWarning': , 'ImportWarning': , 'UnicodeWarning': , 'BytesWarning': , 'ResourceWarning': , 'ConnectionError': , 'BlockingIOError': , 'BrokenPipeError': , 'ChildProcessError': , 'ConnectionAbortedError': , 'ConnectionRefusedError': , 'ConnectionResetError': , 'FileExistsError': , 'FileNotFoundError': , 'IsADirectoryError': , 'NotADirectoryError': , 'InterruptedError': , 'PermissionError': , 'ProcessLookupError': , 'TimeoutError': , 'open': , 'quit': Use quit() or Ctrl-D (i.e. EOF) to exit, 'exit': Use exit() or Ctrl-D (i.e. EOF) to exit, 'copyright': Copyright (c) 2001-2018 Python Software Foundation. +All Rights Reserved. + +Copyright (c) 2000 BeOpen.com. +All Rights Reserved. + +Copyright (c) 1995-2001 Corporation for National Research Initiatives. +All Rights Reserved. + +Copyright (c) 1991-1995 Stichting Mathematisch Centrum, Amsterdam. +All Rights Reserved., 'credits': Thanks to CWI, CNRI, BeOpen.com, Zope Corporation and a cast of thousands + for supporting Python development. See www.python.org for more information., 'license': Type license() to see the full license text, 'help': Type help() for interactive help, or help(object) for help about object.} + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: @py_builtins + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: @pytest_ar + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: pytest + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: asyncio + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: User + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: WebsocketCommunicator + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: get_channel_layer + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: application + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: PROCESS_CONNECTION_PASS + obj: my_dev_password + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: Token + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: TestClientConnection + obj: + finish pytest_pycollect_makeitem --> [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __repr__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __getattribute__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __setattr__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __delattr__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __init__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __new__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __dir__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __dict__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __hash__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __str__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __lt__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __le__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __eq__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __ne__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __gt__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __ge__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __reduce_ex__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __reduce__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __subclasshook__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __init_subclass__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __format__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __sizeof__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __class__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + finish pytest_make_collect_report --> [hook] + genitems [collection] + pytest_collectstart [hook] + collector: + finish pytest_collectstart --> [] [hook] + pytest_make_collect_report [hook] + collector: + finish pytest_make_collect_report --> [hook] + genitems [collection] + pytest_collectstart [hook] + collector: + finish pytest_collectstart --> [] [hook] + pytest_make_collect_report [hook] + collector: + pytest_pycollect_makeitem [hook] + collector: + name: __module__ + obj: test_connection + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __doc__ + obj: Test that clients can or cannot connect depending on different conditions. + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: test_connection_with_token + obj: + pytest_generate_tests [hook] + metafunc: <_pytest.python.Metafunc object at 0x7f33f9143278> + finish pytest_generate_tests --> [] [hook] + finish pytest_pycollect_makeitem --> [] [hook] + pytest_pycollect_makeitem [hook] + collector: + name: test_connection_with_password + obj: + pytest_generate_tests [hook] + metafunc: <_pytest.python.Metafunc object at 0x7f33f9143e80> + finish pytest_generate_tests --> [] [hook] + finish pytest_pycollect_makeitem --> [] [hook] + pytest_pycollect_makeitem [hook] + collector: + name: test_connection_failed_for_invalid_token + obj: + pytest_generate_tests [hook] + metafunc: <_pytest.python.Metafunc object at 0x7f33f9148ac8> + finish pytest_generate_tests --> [] [hook] + finish pytest_pycollect_makeitem --> [] [hook] + pytest_pycollect_makeitem [hook] + collector: + name: test_connection_failed_for_invalid_password + obj: + pytest_generate_tests [hook] + metafunc: <_pytest.python.Metafunc object at 0x7f33f9150710> + finish pytest_generate_tests --> [] [hook] + finish pytest_pycollect_makeitem --> [] [hook] + pytest_pycollect_makeitem [hook] + collector: + name: test_connection_interrupted_when_logout_message_is_sent + obj: + pytest_generate_tests [hook] + metafunc: <_pytest.python.Metafunc object at 0x7f33f9150a90> + finish pytest_generate_tests --> [] [hook] + finish pytest_pycollect_makeitem --> [] [hook] + pytest_pycollect_makeitem [hook] + collector: + name: test_connection_interrupted_when_token_is_deleted + obj: + pytest_generate_tests [hook] + metafunc: <_pytest.python.Metafunc object at 0x7f33f91546d8> + finish pytest_generate_tests --> [] [hook] + finish pytest_pycollect_makeitem --> [] [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __dict__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __weakref__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __repr__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __hash__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __str__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __getattribute__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __setattr__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __delattr__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __lt__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __le__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __eq__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __ne__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __gt__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __ge__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __init__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __new__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __reduce_ex__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __reduce__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __subclasshook__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __init_subclass__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __format__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __sizeof__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __dir__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __class__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + finish pytest_make_collect_report --> [hook] + genitems [collection] + pytest_itemcollected [hook] + item: + finish pytest_itemcollected --> [] [hook] + genitems [collection] + pytest_itemcollected [hook] + item: + finish pytest_itemcollected --> [] [hook] + genitems [collection] + pytest_itemcollected [hook] + item: + finish pytest_itemcollected --> [] [hook] + genitems [collection] + pytest_itemcollected [hook] + item: + finish pytest_itemcollected --> [] [hook] + genitems [collection] + pytest_itemcollected [hook] + item: + finish pytest_itemcollected --> [] [hook] + genitems [collection] + pytest_itemcollected [hook] + item: + finish pytest_itemcollected --> [] [hook] + pytest_collectreport [hook] + report: + finish pytest_collectreport --> [] [hook] + pytest_collectreport [hook] + report: + finish pytest_collectreport --> [] [hook] + pytest_collectreport [hook] + report: + finish pytest_collectreport --> [] [hook] + genitems [collection] + pytest_collectstart [hook] + collector: + finish pytest_collectstart --> [] [hook] + pytest_make_collect_report [hook] + collector: + find_module called for: test_heartbeat [assertion] + matched test file '/usr/src/love/manager/subscription/tests/test_heartbeat.py' [assertion] + found cached rewritten pyc for '/usr/src/love/manager/subscription/tests/test_heartbeat.py' [assertion] + pytest_pycollect_makeitem [hook] + collector: + name: __name__ + obj: test_heartbeat + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __doc__ + obj: Tests for the subscription of consumers to love_csc streams. + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __package__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __loader__ + obj: <_pytest.assertion.rewrite.AssertionRewritingHook object at 0x7f34179d8898> + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __spec__ + obj: ModuleSpec(name='test_heartbeat', loader=<_pytest.assertion.rewrite.AssertionRewritingHook object at 0x7f34179d8898>, origin='/usr/src/love/manager/subscription/tests/test_heartbeat.py') + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __file__ + obj: /usr/src/love/manager/subscription/tests/test_heartbeat.py + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __cached__ + obj: /usr/src/love/manager/subscription/tests/__pycache__/test_heartbeat.cpython-37.pyc + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __builtins__ + obj: {'__name__': 'builtins', '__doc__': "Built-in functions, exceptions, and other objects.\n\nNoteworthy: None is the `nil' object; Ellipsis represents `...' in slices.", '__package__': '', '__loader__': , '__spec__': ModuleSpec(name='builtins', loader=), '__build_class__': , '__import__': , 'abs': , 'all': , 'any': , 'ascii': , 'bin': , 'breakpoint': , 'callable': , 'chr': , 'compile': , 'delattr': , 'dir': , 'divmod': , 'eval': , 'exec': , 'format': , 'getattr': , 'globals': , 'hasattr': , 'hash': , 'hex': , 'id': , 'input': , 'isinstance': , 'issubclass': , 'iter': , 'len': , 'locals': , 'max': , 'min': , 'next': , 'oct': , 'ord': , 'pow': , 'print': , 'repr': , 'round': , 'setattr': , 'sorted': , 'sum': , 'vars': , 'None': None, 'Ellipsis': Ellipsis, 'NotImplemented': NotImplemented, 'False': False, 'True': True, 'bool': , 'memoryview': , 'bytearray': , 'bytes': , 'classmethod': , 'complex': , 'dict': , 'enumerate': , 'filter': , 'float': , 'frozenset': , 'property': , 'int': , 'list': , 'map': , 'object': , 'range': , 'reversed': , 'set': , 'slice': , 'staticmethod': , 'str': , 'super': , 'tuple': , 'type': , 'zip': , '__debug__': True, 'BaseException': , 'Exception': , 'TypeError': , 'StopAsyncIteration': , 'StopIteration': , 'GeneratorExit': , 'SystemExit': , 'KeyboardInterrupt': , 'ImportError': , 'ModuleNotFoundError': , 'OSError': , 'EnvironmentError': , 'IOError': , 'EOFError': , 'RuntimeError': , 'RecursionError': , 'NotImplementedError': , 'NameError': , 'UnboundLocalError': , 'AttributeError': , 'SyntaxError': , 'IndentationError': , 'TabError': , 'LookupError': , 'IndexError': , 'KeyError': , 'ValueError': , 'UnicodeError': , 'UnicodeEncodeError': , 'UnicodeDecodeError': , 'UnicodeTranslateError': , 'AssertionError': , 'ArithmeticError': , 'FloatingPointError': , 'OverflowError': , 'ZeroDivisionError': , 'SystemError': , 'ReferenceError': , 'MemoryError': , 'BufferError': , 'Warning': , 'UserWarning': , 'DeprecationWarning': , 'PendingDeprecationWarning': , 'SyntaxWarning': , 'RuntimeWarning': , 'FutureWarning': , 'ImportWarning': , 'UnicodeWarning': , 'BytesWarning': , 'ResourceWarning': , 'ConnectionError': , 'BlockingIOError': , 'BrokenPipeError': , 'ChildProcessError': , 'ConnectionAbortedError': , 'ConnectionRefusedError': , 'ConnectionResetError': , 'FileExistsError': , 'FileNotFoundError': , 'IsADirectoryError': , 'NotADirectoryError': , 'InterruptedError': , 'PermissionError': , 'ProcessLookupError': , 'TimeoutError': , 'open': , 'quit': Use quit() or Ctrl-D (i.e. EOF) to exit, 'exit': Use exit() or Ctrl-D (i.e. EOF) to exit, 'copyright': Copyright (c) 2001-2018 Python Software Foundation. +All Rights Reserved. + +Copyright (c) 2000 BeOpen.com. +All Rights Reserved. + +Copyright (c) 1995-2001 Corporation for National Research Initiatives. +All Rights Reserved. + +Copyright (c) 1991-1995 Stichting Mathematisch Centrum, Amsterdam. +All Rights Reserved., 'credits': Thanks to CWI, CNRI, BeOpen.com, Zope Corporation and a cast of thousands + for supporting Python development. See www.python.org for more information., 'license': Type license() to see the full license text, 'help': Type help() for interactive help, or help(object) for help about object.} + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: @py_builtins + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: @pytest_ar + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: asyncio + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: pytest + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: User + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: Permission + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: WebsocketCommunicator + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: application + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: Token + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: HeartbeatManager + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: TestHeartbeat + obj: + finish pytest_pycollect_makeitem --> [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __repr__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __getattribute__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __setattr__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __delattr__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __init__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __new__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __dir__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __dict__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __hash__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __str__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __lt__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __le__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __eq__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __ne__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __gt__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __ge__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __reduce_ex__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __reduce__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __subclasshook__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __init_subclass__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __format__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __sizeof__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __class__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + finish pytest_make_collect_report --> [hook] + genitems [collection] + pytest_collectstart [hook] + collector: + finish pytest_collectstart --> [] [hook] + pytest_make_collect_report [hook] + collector: + finish pytest_make_collect_report --> [hook] + genitems [collection] + pytest_collectstart [hook] + collector: + finish pytest_collectstart --> [] [hook] + pytest_make_collect_report [hook] + collector: + pytest_pycollect_makeitem [hook] + collector: + name: __module__ + obj: test_heartbeat + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: setup_method + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: test_join_and_leave_subscription + obj: + pytest_generate_tests [hook] + metafunc: <_pytest.python.Metafunc object at 0x7f33f9158710> + finish pytest_generate_tests --> [] [hook] + finish pytest_pycollect_makeitem --> [] [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __dict__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __weakref__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __doc__ + obj: None + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: _Class__pytest_setup_method + obj: .xunit_setup_method_fixture at 0x7f33f9125ae8> + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __repr__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __hash__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __str__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __getattribute__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __setattr__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __delattr__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __lt__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __le__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __eq__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __ne__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __gt__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __ge__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __init__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __new__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __reduce_ex__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __reduce__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __subclasshook__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __init_subclass__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __format__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __sizeof__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __dir__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __class__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + finish pytest_make_collect_report --> [hook] + genitems [collection] + pytest_itemcollected [hook] + item: + finish pytest_itemcollected --> [] [hook] + pytest_collectreport [hook] + report: + finish pytest_collectreport --> [] [hook] + pytest_collectreport [hook] + report: + finish pytest_collectreport --> [] [hook] + pytest_collectreport [hook] + report: + finish pytest_collectreport --> [] [hook] + genitems [collection] + pytest_collectstart [hook] + collector: + finish pytest_collectstart --> [] [hook] + pytest_make_collect_report [hook] + collector: + find_module called for: test_lovecsc_subscriptions [assertion] + matched test file '/usr/src/love/manager/subscription/tests/test_lovecsc_subscriptions.py' [assertion] + found cached rewritten pyc for '/usr/src/love/manager/subscription/tests/test_lovecsc_subscriptions.py' [assertion] + pytest_pycollect_makeitem [hook] + collector: + name: __name__ + obj: test_lovecsc_subscriptions + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __doc__ + obj: Tests for the subscription of consumers to love_csc streams. + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __package__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __loader__ + obj: <_pytest.assertion.rewrite.AssertionRewritingHook object at 0x7f34179d8898> + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __spec__ + obj: ModuleSpec(name='test_lovecsc_subscriptions', loader=<_pytest.assertion.rewrite.AssertionRewritingHook object at 0x7f34179d8898>, origin='/usr/src/love/manager/subscription/tests/test_lovecsc_subscriptions.py') + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __file__ + obj: /usr/src/love/manager/subscription/tests/test_lovecsc_subscriptions.py + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __cached__ + obj: /usr/src/love/manager/subscription/tests/__pycache__/test_lovecsc_subscriptions.cpython-37.pyc + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __builtins__ + obj: {'__name__': 'builtins', '__doc__': "Built-in functions, exceptions, and other objects.\n\nNoteworthy: None is the `nil' object; Ellipsis represents `...' in slices.", '__package__': '', '__loader__': , '__spec__': ModuleSpec(name='builtins', loader=), '__build_class__': , '__import__': , 'abs': , 'all': , 'any': , 'ascii': , 'bin': , 'breakpoint': , 'callable': , 'chr': , 'compile': , 'delattr': , 'dir': , 'divmod': , 'eval': , 'exec': , 'format': , 'getattr': , 'globals': , 'hasattr': , 'hash': , 'hex': , 'id': , 'input': , 'isinstance': , 'issubclass': , 'iter': , 'len': , 'locals': , 'max': , 'min': , 'next': , 'oct': , 'ord': , 'pow': , 'print': , 'repr': , 'round': , 'setattr': , 'sorted': , 'sum': , 'vars': , 'None': None, 'Ellipsis': Ellipsis, 'NotImplemented': NotImplemented, 'False': False, 'True': True, 'bool': , 'memoryview': , 'bytearray': , 'bytes': , 'classmethod': , 'complex': , 'dict': , 'enumerate': , 'filter': , 'float': , 'frozenset': , 'property': , 'int': , 'list': , 'map': , 'object': , 'range': , 'reversed': , 'set': , 'slice': , 'staticmethod': , 'str': , 'super': , 'tuple': , 'type': , 'zip': , '__debug__': True, 'BaseException': , 'Exception': , 'TypeError': , 'StopAsyncIteration': , 'StopIteration': , 'GeneratorExit': , 'SystemExit': , 'KeyboardInterrupt': , 'ImportError': , 'ModuleNotFoundError': , 'OSError': , 'EnvironmentError': , 'IOError': , 'EOFError': , 'RuntimeError': , 'RecursionError': , 'NotImplementedError': , 'NameError': , 'UnboundLocalError': , 'AttributeError': , 'SyntaxError': , 'IndentationError': , 'TabError': , 'LookupError': , 'IndexError': , 'KeyError': , 'ValueError': , 'UnicodeError': , 'UnicodeEncodeError': , 'UnicodeDecodeError': , 'UnicodeTranslateError': , 'AssertionError': , 'ArithmeticError': , 'FloatingPointError': , 'OverflowError': , 'ZeroDivisionError': , 'SystemError': , 'ReferenceError': , 'MemoryError': , 'BufferError': , 'Warning': , 'UserWarning': , 'DeprecationWarning': , 'PendingDeprecationWarning': , 'SyntaxWarning': , 'RuntimeWarning': , 'FutureWarning': , 'ImportWarning': , 'UnicodeWarning': , 'BytesWarning': , 'ResourceWarning': , 'ConnectionError': , 'BlockingIOError': , 'BrokenPipeError': , 'ChildProcessError': , 'ConnectionAbortedError': , 'ConnectionRefusedError': , 'ConnectionResetError': , 'FileExistsError': , 'FileNotFoundError': , 'IsADirectoryError': , 'NotADirectoryError': , 'InterruptedError': , 'PermissionError': , 'ProcessLookupError': , 'TimeoutError': , 'open': , 'quit': Use quit() or Ctrl-D (i.e. EOF) to exit, 'exit': Use exit() or Ctrl-D (i.e. EOF) to exit, 'copyright': Copyright (c) 2001-2018 Python Software Foundation. +All Rights Reserved. + +Copyright (c) 2000 BeOpen.com. +All Rights Reserved. + +Copyright (c) 1995-2001 Corporation for National Research Initiatives. +All Rights Reserved. + +Copyright (c) 1991-1995 Stichting Mathematisch Centrum, Amsterdam. +All Rights Reserved., 'credits': Thanks to CWI, CNRI, BeOpen.com, Zope Corporation and a cast of thousands + for supporting Python development. See www.python.org for more information., 'license': Type license() to see the full license text, 'help': Type help() for interactive help, or help(object) for help about object.} + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: @py_builtins + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: @pytest_ar + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: asyncio + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: pytest + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: User + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: Permission + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: WebsocketCommunicator + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: application + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: Token + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: TestLOVECscSubscriptions + obj: + finish pytest_pycollect_makeitem --> [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __repr__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __getattribute__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __setattr__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __delattr__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __init__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __new__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __dir__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __dict__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __hash__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __str__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __lt__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __le__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __eq__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __ne__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __gt__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __ge__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __reduce_ex__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __reduce__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __subclasshook__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __init_subclass__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __format__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __sizeof__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __class__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + finish pytest_make_collect_report --> [hook] + genitems [collection] + pytest_collectstart [hook] + collector: + finish pytest_collectstart --> [] [hook] + pytest_make_collect_report [hook] + collector: + finish pytest_make_collect_report --> [hook] + genitems [collection] + pytest_collectstart [hook] + collector: + finish pytest_collectstart --> [] [hook] + pytest_make_collect_report [hook] + collector: + pytest_pycollect_makeitem [hook] + collector: + name: __module__ + obj: test_lovecsc_subscriptions + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: setup_method + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: test_join_and_leave_subscription + obj: + pytest_generate_tests [hook] + metafunc: <_pytest.python.Metafunc object at 0x7f33f91317f0> + finish pytest_generate_tests --> [] [hook] + finish pytest_pycollect_makeitem --> [] [hook] + pytest_pycollect_makeitem [hook] + collector: + name: test_observinglog_to_lovecsc + obj: + pytest_generate_tests [hook] + metafunc: <_pytest.python.Metafunc object at 0x7f33f9163518> + finish pytest_generate_tests --> [] [hook] + finish pytest_pycollect_makeitem --> [] [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __dict__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __weakref__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __doc__ + obj: None + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: _Class__pytest_setup_method + obj: .xunit_setup_method_fixture at 0x7f33f913ba60> + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __repr__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __hash__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __str__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __getattribute__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __setattr__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __delattr__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __lt__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __le__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __eq__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __ne__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __gt__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __ge__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __init__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __new__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __reduce_ex__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __reduce__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __subclasshook__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __init_subclass__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __format__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __sizeof__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __dir__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __class__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + finish pytest_make_collect_report --> [hook] + genitems [collection] + pytest_itemcollected [hook] + item: + finish pytest_itemcollected --> [] [hook] + genitems [collection] + pytest_itemcollected [hook] + item: + finish pytest_itemcollected --> [] [hook] + pytest_collectreport [hook] + report: + finish pytest_collectreport --> [] [hook] + pytest_collectreport [hook] + report: + finish pytest_collectreport --> [] [hook] + pytest_collectreport [hook] + report: + finish pytest_collectreport --> [] [hook] + genitems [collection] + pytest_collectstart [hook] + collector: + finish pytest_collectstart --> [] [hook] + pytest_make_collect_report [hook] + collector: + find_module called for: test_subscriptions [assertion] + matched test file '/usr/src/love/manager/subscription/tests/test_subscriptions.py' [assertion] + found cached rewritten pyc for '/usr/src/love/manager/subscription/tests/test_subscriptions.py' [assertion] + pytest_pycollect_makeitem [hook] + collector: + name: __name__ + obj: test_subscriptions + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __doc__ + obj: Tests for the subscription of consumers to streams. + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __package__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __loader__ + obj: <_pytest.assertion.rewrite.AssertionRewritingHook object at 0x7f34179d8898> + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __spec__ + obj: ModuleSpec(name='test_subscriptions', loader=<_pytest.assertion.rewrite.AssertionRewritingHook object at 0x7f34179d8898>, origin='/usr/src/love/manager/subscription/tests/test_subscriptions.py') + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __file__ + obj: /usr/src/love/manager/subscription/tests/test_subscriptions.py + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __cached__ + obj: /usr/src/love/manager/subscription/tests/__pycache__/test_subscriptions.cpython-37.pyc + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __builtins__ + obj: {'__name__': 'builtins', '__doc__': "Built-in functions, exceptions, and other objects.\n\nNoteworthy: None is the `nil' object; Ellipsis represents `...' in slices.", '__package__': '', '__loader__': , '__spec__': ModuleSpec(name='builtins', loader=), '__build_class__': , '__import__': , 'abs': , 'all': , 'any': , 'ascii': , 'bin': , 'breakpoint': , 'callable': , 'chr': , 'compile': , 'delattr': , 'dir': , 'divmod': , 'eval': , 'exec': , 'format': , 'getattr': , 'globals': , 'hasattr': , 'hash': , 'hex': , 'id': , 'input': , 'isinstance': , 'issubclass': , 'iter': , 'len': , 'locals': , 'max': , 'min': , 'next': , 'oct': , 'ord': , 'pow': , 'print': , 'repr': , 'round': , 'setattr': , 'sorted': , 'sum': , 'vars': , 'None': None, 'Ellipsis': Ellipsis, 'NotImplemented': NotImplemented, 'False': False, 'True': True, 'bool': , 'memoryview': , 'bytearray': , 'bytes': , 'classmethod': , 'complex': , 'dict': , 'enumerate': , 'filter': , 'float': , 'frozenset': , 'property': , 'int': , 'list': , 'map': , 'object': , 'range': , 'reversed': , 'set': , 'slice': , 'staticmethod': , 'str': , 'super': , 'tuple': , 'type': , 'zip': , '__debug__': True, 'BaseException': , 'Exception': , 'TypeError': , 'StopAsyncIteration': , 'StopIteration': , 'GeneratorExit': , 'SystemExit': , 'KeyboardInterrupt': , 'ImportError': , 'ModuleNotFoundError': , 'OSError': , 'EnvironmentError': , 'IOError': , 'EOFError': , 'RuntimeError': , 'RecursionError': , 'NotImplementedError': , 'NameError': , 'UnboundLocalError': , 'AttributeError': , 'SyntaxError': , 'IndentationError': , 'TabError': , 'LookupError': , 'IndexError': , 'KeyError': , 'ValueError': , 'UnicodeError': , 'UnicodeEncodeError': , 'UnicodeDecodeError': , 'UnicodeTranslateError': , 'AssertionError': , 'ArithmeticError': , 'FloatingPointError': , 'OverflowError': , 'ZeroDivisionError': , 'SystemError': , 'ReferenceError': , 'MemoryError': , 'BufferError': , 'Warning': , 'UserWarning': , 'DeprecationWarning': , 'PendingDeprecationWarning': , 'SyntaxWarning': , 'RuntimeWarning': , 'FutureWarning': , 'ImportWarning': , 'UnicodeWarning': , 'BytesWarning': , 'ResourceWarning': , 'ConnectionError': , 'BlockingIOError': , 'BrokenPipeError': , 'ChildProcessError': , 'ConnectionAbortedError': , 'ConnectionRefusedError': , 'ConnectionResetError': , 'FileExistsError': , 'FileNotFoundError': , 'IsADirectoryError': , 'NotADirectoryError': , 'InterruptedError': , 'PermissionError': , 'ProcessLookupError': , 'TimeoutError': , 'open': , 'quit': Use quit() or Ctrl-D (i.e. EOF) to exit, 'exit': Use exit() or Ctrl-D (i.e. EOF) to exit, 'copyright': Copyright (c) 2001-2018 Python Software Foundation. +All Rights Reserved. + +Copyright (c) 2000 BeOpen.com. +All Rights Reserved. + +Copyright (c) 1995-2001 Corporation for National Research Initiatives. +All Rights Reserved. + +Copyright (c) 1991-1995 Stichting Mathematisch Centrum, Amsterdam. +All Rights Reserved., 'credits': Thanks to CWI, CNRI, BeOpen.com, Zope Corporation and a cast of thousands + for supporting Python development. See www.python.org for more information., 'license': Type license() to see the full license text, 'help': Type help() for interactive help, or help(object) for help about object.} + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: @py_builtins + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: @pytest_ar + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: asyncio + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: pytest + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: User + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: Permission + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: WebsocketCommunicator + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: application + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: Token + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: TestSubscriptionCombinations + obj: + finish pytest_pycollect_makeitem --> [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __repr__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __getattribute__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __setattr__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __delattr__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __init__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __new__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __dir__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __dict__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __hash__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __str__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __lt__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __le__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __eq__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __ne__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __gt__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __ge__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __reduce_ex__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __reduce__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __subclasshook__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __init_subclass__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __format__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __sizeof__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __class__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + finish pytest_make_collect_report --> [hook] + genitems [collection] + pytest_collectstart [hook] + collector: + finish pytest_collectstart --> [] [hook] + pytest_make_collect_report [hook] + collector: + finish pytest_make_collect_report --> [hook] + genitems [collection] + pytest_collectstart [hook] + collector: + finish pytest_collectstart --> [] [hook] + pytest_make_collect_report [hook] + collector: + pytest_pycollect_makeitem [hook] + collector: + name: __module__ + obj: test_subscriptions + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __doc__ + obj: Test that clients can or cannot establish to subscriptions depending on different conditions. + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: categories + obj: ['event', 'telemetry', 'cmd'] + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: cscs + obj: ['ScriptQueue', 'ATDome'] + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: salindices + obj: [1, 2] + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: streams + obj: ['stream1', 'stream2'] + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: combinations + obj: [] + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: no_reception_timeout + obj: 0.001 + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: setup_method + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: build_messages + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: test_join_and_leave_every_subscription + obj: + pytest_generate_tests [hook] + metafunc: <_pytest.python.Metafunc object at 0x7f33f90f3048> + finish pytest_generate_tests --> [] [hook] + finish pytest_pycollect_makeitem --> [] [hook] + pytest_pycollect_makeitem [hook] + collector: + name: test_join_and_leave_all_subscription + obj: + pytest_generate_tests [hook] + metafunc: <_pytest.python.Metafunc object at 0x7f33f90f3d30> + finish pytest_generate_tests --> [] [hook] + finish pytest_pycollect_makeitem --> [] [hook] + pytest_pycollect_makeitem [hook] + collector: + name: test_receive_messages_from_every_subscription + obj: + pytest_generate_tests [hook] + metafunc: <_pytest.python.Metafunc object at 0x7f33f90f6a58> + finish pytest_generate_tests --> [] [hook] + finish pytest_pycollect_makeitem --> [] [hook] + pytest_pycollect_makeitem [hook] + collector: + name: test_receive_messages_from_all_subscription + obj: + pytest_generate_tests [hook] + metafunc: <_pytest.python.Metafunc object at 0x7f33f90fa780> + finish pytest_generate_tests --> [] [hook] + finish pytest_pycollect_makeitem --> [] [hook] + pytest_pycollect_makeitem [hook] + collector: + name: test_receive_message_for_subscribed_group_only + obj: + pytest_generate_tests [hook] + metafunc: <_pytest.python.Metafunc object at 0x7f33f90ff4a8> + finish pytest_generate_tests --> [] [hook] + finish pytest_pycollect_makeitem --> [] [hook] + pytest_pycollect_makeitem [hook] + collector: + name: test_receive_message_for_subscribed_groups_only + obj: + pytest_generate_tests [hook] + metafunc: <_pytest.python.Metafunc object at 0x7f33f90f6e10> + finish pytest_generate_tests --> [] [hook] + finish pytest_pycollect_makeitem --> [] [hook] + pytest_pycollect_makeitem [hook] + collector: + name: test_receive_part_of_message_for_subscribed_groups_only + obj: + pytest_generate_tests [hook] + metafunc: <_pytest.python.Metafunc object at 0x7f33f9103438> + finish pytest_generate_tests --> [] [hook] + finish pytest_pycollect_makeitem --> [] [hook] + pytest_pycollect_makeitem [hook] + collector: + name: test_request_initial_state_when_subscribing_to_event + obj: + pytest_generate_tests [hook] + metafunc: <_pytest.python.Metafunc object at 0x7f33f9104160> + finish pytest_generate_tests --> [] [hook] + finish pytest_pycollect_makeitem --> [] [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __dict__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __weakref__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: _Class__pytest_setup_method + obj: .xunit_setup_method_fixture at 0x7f33f90eb598> + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __repr__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __hash__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __str__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __getattribute__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __setattr__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __delattr__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __lt__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __le__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __eq__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __ne__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __gt__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __ge__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __init__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __new__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __reduce_ex__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __reduce__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __subclasshook__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __init_subclass__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __format__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __sizeof__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __dir__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __class__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + finish pytest_make_collect_report --> [hook] + genitems [collection] + pytest_itemcollected [hook] + item: + finish pytest_itemcollected --> [] [hook] + genitems [collection] + pytest_itemcollected [hook] + item: + finish pytest_itemcollected --> [] [hook] + genitems [collection] + pytest_itemcollected [hook] + item: + finish pytest_itemcollected --> [] [hook] + genitems [collection] + pytest_itemcollected [hook] + item: + finish pytest_itemcollected --> [] [hook] + genitems [collection] + pytest_itemcollected [hook] + item: + finish pytest_itemcollected --> [] [hook] + genitems [collection] + pytest_itemcollected [hook] + item: + finish pytest_itemcollected --> [] [hook] + genitems [collection] + pytest_itemcollected [hook] + item: + finish pytest_itemcollected --> [] [hook] + genitems [collection] + pytest_itemcollected [hook] + item: + finish pytest_itemcollected --> [] [hook] + pytest_collectreport [hook] + report: + finish pytest_collectreport --> [] [hook] + pytest_collectreport [hook] + report: + finish pytest_collectreport --> [] [hook] + pytest_collectreport [hook] + report: + finish pytest_collectreport --> [] [hook] + genitems [collection] + pytest_collectstart [hook] + collector: + finish pytest_collectstart --> [] [hook] + pytest_make_collect_report [hook] + collector: + pytest_ignore_collect [hook] + path: /usr/src/love/manager/ui_framework/tests + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_directory [hook] + path: /usr/src/love/manager/ui_framework/tests + parent: + finish pytest_collect_directory --> None [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/ui_framework/migrations + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_directory [hook] + path: /usr/src/love/manager/ui_framework/migrations + parent: + finish pytest_collect_directory --> None [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/ui_framework/fixtures + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_directory [hook] + path: /usr/src/love/manager/ui_framework/fixtures + parent: + finish pytest_collect_directory --> None [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/ui_framework/admin.py + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/ui_framework/admin.py + parent: + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/ui_framework/apps.py + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/ui_framework/apps.py + parent: + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/ui_framework/models.py + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/ui_framework/models.py + parent: + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/ui_framework/serializers.py + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/ui_framework/serializers.py + parent: + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/ui_framework/urls.py + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/ui_framework/urls.py + parent: + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/ui_framework/views.py + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/ui_framework/views.py + parent: + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/ui_framework/fixtures/initial_data.json + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/ui_framework/migrations/__init__.py + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/ui_framework/tests/media + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_directory [hook] + path: /usr/src/love/manager/ui_framework/tests/media + parent: + finish pytest_collect_directory --> None [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/ui_framework/tests/__init__.py + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/ui_framework/tests/media/mock + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_directory [hook] + path: /usr/src/love/manager/ui_framework/tests/media/mock + parent: + finish pytest_collect_directory --> None [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/ui_framework/tests/media/thumbnails + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_directory [hook] + path: /usr/src/love/manager/ui_framework/tests/media/thumbnails + parent: + finish pytest_collect_directory --> None [hook] + finish pytest_make_collect_report --> [hook] + pytest_collectreport [hook] + report: + finish pytest_collectreport --> [] [hook] + genitems [collection] + pytest_collectstart [hook] + collector: + finish pytest_collectstart --> [] [hook] + pytest_make_collect_report [hook] + collector: + pytest_ignore_collect [hook] + path: /usr/src/love/manager/ui_framework/migrations/0001_initial.py + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/ui_framework/migrations/0001_initial.py + parent: + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/ui_framework/migrations/0002_view_data.py + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/ui_framework/migrations/0002_view_data.py + parent: + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/ui_framework/migrations/0003_view_thumbnail.py + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/ui_framework/migrations/0003_view_thumbnail.py + parent: + finish pytest_collect_file --> [] [hook] + finish pytest_make_collect_report --> [hook] + pytest_collectreport [hook] + report: + finish pytest_collectreport --> [] [hook] + genitems [collection] + pytest_collectstart [hook] + collector: + finish pytest_collectstart --> [] [hook] + pytest_make_collect_report [hook] + collector: + pytest_ignore_collect [hook] + path: /usr/src/love/manager/ui_framework/tests/media + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_directory [hook] + path: /usr/src/love/manager/ui_framework/tests/media + parent: + finish pytest_collect_directory --> None [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/ui_framework/tests/test_view_thumbnail.py + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/ui_framework/tests/test_view_thumbnail.py + parent: + pytest_pycollect_makemodule [hook] + path: /usr/src/love/manager/ui_framework/tests/test_view_thumbnail.py + parent: + finish pytest_pycollect_makemodule --> [hook] + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/ui_framework/tests/tests_api.py + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/ui_framework/tests/tests_api.py + parent: + pytest_pycollect_makemodule [hook] + path: /usr/src/love/manager/ui_framework/tests/tests_api.py + parent: + finish pytest_pycollect_makemodule --> [hook] + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/ui_framework/tests/tests_custom_api.py + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/ui_framework/tests/tests_custom_api.py + parent: + pytest_pycollect_makemodule [hook] + path: /usr/src/love/manager/ui_framework/tests/tests_custom_api.py + parent: + finish pytest_pycollect_makemodule --> [hook] + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/ui_framework/tests/tests_models.py + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/ui_framework/tests/tests_models.py + parent: + pytest_pycollect_makemodule [hook] + path: /usr/src/love/manager/ui_framework/tests/tests_models.py + parent: + finish pytest_pycollect_makemodule --> [hook] + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/ui_framework/tests/utils.py + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_file [hook] + path: /usr/src/love/manager/ui_framework/tests/utils.py + parent: + finish pytest_collect_file --> [] [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/ui_framework/tests/media/mock + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_directory [hook] + path: /usr/src/love/manager/ui_framework/tests/media/mock + parent: + finish pytest_collect_directory --> None [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/ui_framework/tests/media/thumbnails + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_collect_directory [hook] + path: /usr/src/love/manager/ui_framework/tests/media/thumbnails + parent: + finish pytest_collect_directory --> None [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/ui_framework/tests/media/mock/test + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/ui_framework/tests/media/mock/test.png + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + pytest_ignore_collect [hook] + path: /usr/src/love/manager/ui_framework/tests/media/thumbnails/.gitinclude + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_ignore_collect --> False [hook] + finish pytest_make_collect_report --> [hook] + genitems [collection] + pytest_collectstart [hook] + collector: + finish pytest_collectstart --> [] [hook] + pytest_make_collect_report [hook] + collector: + find_module called for: ui_framework.tests [assertion] + find_module called for: ui_framework.tests.test_view_thumbnail [assertion] + matched test file '/usr/src/love/manager/ui_framework/tests/test_view_thumbnail.py' [assertion] + found cached rewritten pyc for '/usr/src/love/manager/ui_framework/tests/test_view_thumbnail.py' [assertion] + early skip of rewriting module: filecmp [assertion] + pytest_pycollect_makeitem [hook] + collector: + name: __name__ + obj: ui_framework.tests.test_view_thumbnail + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __doc__ + obj: Test the UI Framework thumbnail behavior. + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __package__ + obj: ui_framework.tests + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __loader__ + obj: <_pytest.assertion.rewrite.AssertionRewritingHook object at 0x7f34179d8898> + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __spec__ + obj: ModuleSpec(name='ui_framework.tests.test_view_thumbnail', loader=<_pytest.assertion.rewrite.AssertionRewritingHook object at 0x7f34179d8898>, origin='/usr/src/love/manager/ui_framework/tests/test_view_thumbnail.py') + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __file__ + obj: /usr/src/love/manager/ui_framework/tests/test_view_thumbnail.py + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __cached__ + obj: /usr/src/love/manager/ui_framework/tests/__pycache__/test_view_thumbnail.cpython-37.pyc + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __builtins__ + obj: {'__name__': 'builtins', '__doc__': "Built-in functions, exceptions, and other objects.\n\nNoteworthy: None is the `nil' object; Ellipsis represents `...' in slices.", '__package__': '', '__loader__': , '__spec__': ModuleSpec(name='builtins', loader=), '__build_class__': , '__import__': , 'abs': , 'all': , 'any': , 'ascii': , 'bin': , 'breakpoint': , 'callable': , 'chr': , 'compile': , 'delattr': , 'dir': , 'divmod': , 'eval': , 'exec': , 'format': , 'getattr': , 'globals': , 'hasattr': , 'hash': , 'hex': , 'id': , 'input': , 'isinstance': , 'issubclass': , 'iter': , 'len': , 'locals': , 'max': , 'min': , 'next': , 'oct': , 'ord': , 'pow': , 'print': , 'repr': , 'round': , 'setattr': , 'sorted': , 'sum': , 'vars': , 'None': None, 'Ellipsis': Ellipsis, 'NotImplemented': NotImplemented, 'False': False, 'True': True, 'bool': , 'memoryview': , 'bytearray': , 'bytes': , 'classmethod': , 'complex': , 'dict': , 'enumerate': , 'filter': , 'float': , 'frozenset': , 'property': , 'int': , 'list': , 'map': , 'object': , 'range': , 'reversed': , 'set': , 'slice': , 'staticmethod': , 'str': , 'super': , 'tuple': , 'type': , 'zip': , '__debug__': True, 'BaseException': , 'Exception': , 'TypeError': , 'StopAsyncIteration': , 'StopIteration': , 'GeneratorExit': , 'SystemExit': , 'KeyboardInterrupt': , 'ImportError': , 'ModuleNotFoundError': , 'OSError': , 'EnvironmentError': , 'IOError': , 'EOFError': , 'RuntimeError': , 'RecursionError': , 'NotImplementedError': , 'NameError': , 'UnboundLocalError': , 'AttributeError': , 'SyntaxError': , 'IndentationError': , 'TabError': , 'LookupError': , 'IndexError': , 'KeyError': , 'ValueError': , 'UnicodeError': , 'UnicodeEncodeError': , 'UnicodeDecodeError': , 'UnicodeTranslateError': , 'AssertionError': , 'ArithmeticError': , 'FloatingPointError': , 'OverflowError': , 'ZeroDivisionError': , 'SystemError': , 'ReferenceError': , 'MemoryError': , 'BufferError': , 'Warning': , 'UserWarning': , 'DeprecationWarning': , 'PendingDeprecationWarning': , 'SyntaxWarning': , 'RuntimeWarning': , 'FutureWarning': , 'ImportWarning': , 'UnicodeWarning': , 'BytesWarning': , 'ResourceWarning': , 'ConnectionError': , 'BlockingIOError': , 'BrokenPipeError': , 'ChildProcessError': , 'ConnectionAbortedError': , 'ConnectionRefusedError': , 'ConnectionResetError': , 'FileExistsError': , 'FileNotFoundError': , 'IsADirectoryError': , 'NotADirectoryError': , 'InterruptedError': , 'PermissionError': , 'ProcessLookupError': , 'TimeoutError': , 'open': , 'quit': Use quit() or Ctrl-D (i.e. EOF) to exit, 'exit': Use exit() or Ctrl-D (i.e. EOF) to exit, 'copyright': Copyright (c) 2001-2018 Python Software Foundation. +All Rights Reserved. + +Copyright (c) 2000 BeOpen.com. +All Rights Reserved. + +Copyright (c) 1995-2001 Corporation for National Research Initiatives. +All Rights Reserved. + +Copyright (c) 1991-1995 Stichting Mathematisch Centrum, Amsterdam. +All Rights Reserved., 'credits': Thanks to CWI, CNRI, BeOpen.com, Zope Corporation and a cast of thousands + for supporting Python development. See www.python.org for more information., 'license': Type license() to see the full license text, 'help': Type help() for interactive help, or help(object) for help about object.} + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: @py_builtins + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: @pytest_ar + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: settings + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: User + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: Permission + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: reverse + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: TestCase + obj: + finish pytest_pycollect_makeitem --> [hook] + pytest_pycollect_makeitem [hook] + collector: + name: status + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: APIClient + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: Token + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: View + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: os + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: glob + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: filecmp + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: ViewThumbnailTestCase + obj: + finish pytest_pycollect_makeitem --> [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __repr__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __getattribute__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __setattr__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __delattr__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __init__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __new__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __dir__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __dict__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __hash__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __str__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __lt__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __le__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __eq__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __ne__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __gt__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __ge__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __reduce_ex__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __reduce__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __subclasshook__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __init_subclass__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __format__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __sizeof__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __class__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + finish pytest_make_collect_report --> [hook] + genitems [collection] + pytest_collectstart [hook] + collector: + finish pytest_collectstart --> [] [hook] + pytest_make_collect_report [hook] + collector: + finish pytest_make_collect_report --> [hook] + pytest_collectreport [hook] + report: + finish pytest_collectreport --> [] [hook] + genitems [collection] + pytest_collectstart [hook] + collector: + finish pytest_collectstart --> [] [hook] + pytest_make_collect_report [hook] + collector: + finish pytest_make_collect_report --> [hook] + genitems [collection] + pytest_itemcollected [hook] + item: + finish pytest_itemcollected --> [] [hook] + genitems [collection] + pytest_itemcollected [hook] + item: + finish pytest_itemcollected --> [] [hook] + pytest_collectreport [hook] + report: + finish pytest_collectreport --> [] [hook] + pytest_collectreport [hook] + report: + finish pytest_collectreport --> [] [hook] + genitems [collection] + pytest_collectstart [hook] + collector: + finish pytest_collectstart --> [] [hook] + pytest_make_collect_report [hook] + collector: + find_module called for: ui_framework.tests.tests_api [assertion] + matched test file '/usr/src/love/manager/ui_framework/tests/tests_api.py' [assertion] + found cached rewritten pyc for '/usr/src/love/manager/ui_framework/tests/tests_api.py' [assertion] + early skip of rewriting module: ui_framework.tests.utils [assertion] + pytest_pycollect_makeitem [hook] + collector: + name: __name__ + obj: ui_framework.tests.tests_api + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __doc__ + obj: Test the UI Framework REST API. + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __package__ + obj: ui_framework.tests + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __loader__ + obj: <_pytest.assertion.rewrite.AssertionRewritingHook object at 0x7f34179d8898> + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __spec__ + obj: ModuleSpec(name='ui_framework.tests.tests_api', loader=<_pytest.assertion.rewrite.AssertionRewritingHook object at 0x7f34179d8898>, origin='/usr/src/love/manager/ui_framework/tests/tests_api.py') + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __file__ + obj: /usr/src/love/manager/ui_framework/tests/tests_api.py + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __cached__ + obj: /usr/src/love/manager/ui_framework/tests/__pycache__/tests_api.cpython-37.pyc + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __builtins__ + obj: {'__name__': 'builtins', '__doc__': "Built-in functions, exceptions, and other objects.\n\nNoteworthy: None is the `nil' object; Ellipsis represents `...' in slices.", '__package__': '', '__loader__': , '__spec__': ModuleSpec(name='builtins', loader=), '__build_class__': , '__import__': , 'abs': , 'all': , 'any': , 'ascii': , 'bin': , 'breakpoint': , 'callable': , 'chr': , 'compile': , 'delattr': , 'dir': , 'divmod': , 'eval': , 'exec': , 'format': , 'getattr': , 'globals': , 'hasattr': , 'hash': , 'hex': , 'id': , 'input': , 'isinstance': , 'issubclass': , 'iter': , 'len': , 'locals': , 'max': , 'min': , 'next': , 'oct': , 'ord': , 'pow': , 'print': , 'repr': , 'round': , 'setattr': , 'sorted': , 'sum': , 'vars': , 'None': None, 'Ellipsis': Ellipsis, 'NotImplemented': NotImplemented, 'False': False, 'True': True, 'bool': , 'memoryview': , 'bytearray': , 'bytes': , 'classmethod': , 'complex': , 'dict': , 'enumerate': , 'filter': , 'float': , 'frozenset': , 'property': , 'int': , 'list': , 'map': , 'object': , 'range': , 'reversed': , 'set': , 'slice': , 'staticmethod': , 'str': , 'super': , 'tuple': , 'type': , 'zip': , '__debug__': True, 'BaseException': , 'Exception': , 'TypeError': , 'StopAsyncIteration': , 'StopIteration': , 'GeneratorExit': , 'SystemExit': , 'KeyboardInterrupt': , 'ImportError': , 'ModuleNotFoundError': , 'OSError': , 'EnvironmentError': , 'IOError': , 'EOFError': , 'RuntimeError': , 'RecursionError': , 'NotImplementedError': , 'NameError': , 'UnboundLocalError': , 'AttributeError': , 'SyntaxError': , 'IndentationError': , 'TabError': , 'LookupError': , 'IndexError': , 'KeyError': , 'ValueError': , 'UnicodeError': , 'UnicodeEncodeError': , 'UnicodeDecodeError': , 'UnicodeTranslateError': , 'AssertionError': , 'ArithmeticError': , 'FloatingPointError': , 'OverflowError': , 'ZeroDivisionError': , 'SystemError': , 'ReferenceError': , 'MemoryError': , 'BufferError': , 'Warning': , 'UserWarning': , 'DeprecationWarning': , 'PendingDeprecationWarning': , 'SyntaxWarning': , 'RuntimeWarning': , 'FutureWarning': , 'ImportWarning': , 'UnicodeWarning': , 'BytesWarning': , 'ResourceWarning': , 'ConnectionError': , 'BlockingIOError': , 'BrokenPipeError': , 'ChildProcessError': , 'ConnectionAbortedError': , 'ConnectionRefusedError': , 'ConnectionResetError': , 'FileExistsError': , 'FileNotFoundError': , 'IsADirectoryError': , 'NotADirectoryError': , 'InterruptedError': , 'PermissionError': , 'ProcessLookupError': , 'TimeoutError': , 'open': , 'quit': Use quit() or Ctrl-D (i.e. EOF) to exit, 'exit': Use exit() or Ctrl-D (i.e. EOF) to exit, 'copyright': Copyright (c) 2001-2018 Python Software Foundation. +All Rights Reserved. + +Copyright (c) 2000 BeOpen.com. +All Rights Reserved. + +Copyright (c) 1995-2001 Corporation for National Research Initiatives. +All Rights Reserved. + +Copyright (c) 1991-1995 Stichting Mathematisch Centrum, Amsterdam. +All Rights Reserved., 'credits': Thanks to CWI, CNRI, BeOpen.com, Zope Corporation and a cast of thousands + for supporting Python development. See www.python.org for more information., 'license': Type license() to see the full license text, 'help': Type help() for interactive help, or help(object) for help about object.} + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: @py_builtins + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: @pytest_ar + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: User + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: Permission + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: reverse + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: status + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: Token + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: get_dict + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: BaseTestCase + obj: + finish pytest_pycollect_makeitem --> [hook] + pytest_pycollect_makeitem [hook] + collector: + name: UnauthenticatedCrudTestCase + obj: + finish pytest_pycollect_makeitem --> [hook] + pytest_pycollect_makeitem [hook] + collector: + name: UnauthorizedCrudTestCase + obj: + finish pytest_pycollect_makeitem --> [hook] + pytest_pycollect_makeitem [hook] + collector: + name: AuthorizedCrudTestCase + obj: + finish pytest_pycollect_makeitem --> [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __repr__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __getattribute__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __setattr__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __delattr__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __init__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __new__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __dir__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __dict__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __hash__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __str__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __lt__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __le__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __eq__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __ne__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __gt__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __ge__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __reduce_ex__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __reduce__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __subclasshook__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __init_subclass__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __format__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __sizeof__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __class__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + finish pytest_make_collect_report --> [hook] + genitems [collection] + pytest_collectstart [hook] + collector: + finish pytest_collectstart --> [] [hook] + pytest_make_collect_report [hook] + collector: + finish pytest_make_collect_report --> [hook] + genitems [collection] + pytest_itemcollected [hook] + item: + finish pytest_itemcollected --> [] [hook] + genitems [collection] + pytest_itemcollected [hook] + item: + finish pytest_itemcollected --> [] [hook] + genitems [collection] + pytest_itemcollected [hook] + item: + finish pytest_itemcollected --> [] [hook] + genitems [collection] + pytest_itemcollected [hook] + item: + finish pytest_itemcollected --> [] [hook] + genitems [collection] + pytest_itemcollected [hook] + item: + finish pytest_itemcollected --> [] [hook] + pytest_collectreport [hook] + report: + finish pytest_collectreport --> [] [hook] + genitems [collection] + pytest_collectstart [hook] + collector: + finish pytest_collectstart --> [] [hook] + pytest_make_collect_report [hook] + collector: + finish pytest_make_collect_report --> [hook] + genitems [collection] + pytest_itemcollected [hook] + item: + finish pytest_itemcollected --> [] [hook] + genitems [collection] + pytest_itemcollected [hook] + item: + finish pytest_itemcollected --> [] [hook] + genitems [collection] + pytest_itemcollected [hook] + item: + finish pytest_itemcollected --> [] [hook] + genitems [collection] + pytest_itemcollected [hook] + item: + finish pytest_itemcollected --> [] [hook] + genitems [collection] + pytest_itemcollected [hook] + item: + finish pytest_itemcollected --> [] [hook] + pytest_collectreport [hook] + report: + finish pytest_collectreport --> [] [hook] + genitems [collection] + pytest_collectstart [hook] + collector: + finish pytest_collectstart --> [] [hook] + pytest_make_collect_report [hook] + collector: + finish pytest_make_collect_report --> [hook] + genitems [collection] + pytest_itemcollected [hook] + item: + finish pytest_itemcollected --> [] [hook] + genitems [collection] + pytest_itemcollected [hook] + item: + finish pytest_itemcollected --> [] [hook] + genitems [collection] + pytest_itemcollected [hook] + item: + finish pytest_itemcollected --> [] [hook] + genitems [collection] + pytest_itemcollected [hook] + item: + finish pytest_itemcollected --> [] [hook] + genitems [collection] + pytest_itemcollected [hook] + item: + finish pytest_itemcollected --> [] [hook] + pytest_collectreport [hook] + report: + finish pytest_collectreport --> [] [hook] + genitems [collection] + pytest_collectstart [hook] + collector: + finish pytest_collectstart --> [] [hook] + pytest_make_collect_report [hook] + collector: + finish pytest_make_collect_report --> [hook] + pytest_collectreport [hook] + report: + finish pytest_collectreport --> [] [hook] + pytest_collectreport [hook] + report: + finish pytest_collectreport --> [] [hook] + genitems [collection] + pytest_collectstart [hook] + collector: + finish pytest_collectstart --> [] [hook] + pytest_make_collect_report [hook] + collector: + find_module called for: ui_framework.tests.tests_custom_api [assertion] + matched test file '/usr/src/love/manager/ui_framework/tests/tests_custom_api.py' [assertion] + found cached rewritten pyc for '/usr/src/love/manager/ui_framework/tests/tests_custom_api.py' [assertion] + pytest_pycollect_makeitem [hook] + collector: + name: __name__ + obj: ui_framework.tests.tests_custom_api + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __doc__ + obj: Test the UI Framework Custom API. + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __package__ + obj: ui_framework.tests + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __loader__ + obj: <_pytest.assertion.rewrite.AssertionRewritingHook object at 0x7f34179d8898> + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __spec__ + obj: ModuleSpec(name='ui_framework.tests.tests_custom_api', loader=<_pytest.assertion.rewrite.AssertionRewritingHook object at 0x7f34179d8898>, origin='/usr/src/love/manager/ui_framework/tests/tests_custom_api.py') + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __file__ + obj: /usr/src/love/manager/ui_framework/tests/tests_custom_api.py + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __cached__ + obj: /usr/src/love/manager/ui_framework/tests/__pycache__/tests_custom_api.cpython-37.pyc + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __builtins__ + obj: {'__name__': 'builtins', '__doc__': "Built-in functions, exceptions, and other objects.\n\nNoteworthy: None is the `nil' object; Ellipsis represents `...' in slices.", '__package__': '', '__loader__': , '__spec__': ModuleSpec(name='builtins', loader=), '__build_class__': , '__import__': , 'abs': , 'all': , 'any': , 'ascii': , 'bin': , 'breakpoint': , 'callable': , 'chr': , 'compile': , 'delattr': , 'dir': , 'divmod': , 'eval': , 'exec': , 'format': , 'getattr': , 'globals': , 'hasattr': , 'hash': , 'hex': , 'id': , 'input': , 'isinstance': , 'issubclass': , 'iter': , 'len': , 'locals': , 'max': , 'min': , 'next': , 'oct': , 'ord': , 'pow': , 'print': , 'repr': , 'round': , 'setattr': , 'sorted': , 'sum': , 'vars': , 'None': None, 'Ellipsis': Ellipsis, 'NotImplemented': NotImplemented, 'False': False, 'True': True, 'bool': , 'memoryview': , 'bytearray': , 'bytes': , 'classmethod': , 'complex': , 'dict': , 'enumerate': , 'filter': , 'float': , 'frozenset': , 'property': , 'int': , 'list': , 'map': , 'object': , 'range': , 'reversed': , 'set': , 'slice': , 'staticmethod': , 'str': , 'super': , 'tuple': , 'type': , 'zip': , '__debug__': True, 'BaseException': , 'Exception': , 'TypeError': , 'StopAsyncIteration': , 'StopIteration': , 'GeneratorExit': , 'SystemExit': , 'KeyboardInterrupt': , 'ImportError': , 'ModuleNotFoundError': , 'OSError': , 'EnvironmentError': , 'IOError': , 'EOFError': , 'RuntimeError': , 'RecursionError': , 'NotImplementedError': , 'NameError': , 'UnboundLocalError': , 'AttributeError': , 'SyntaxError': , 'IndentationError': , 'TabError': , 'LookupError': , 'IndexError': , 'KeyError': , 'ValueError': , 'UnicodeError': , 'UnicodeEncodeError': , 'UnicodeDecodeError': , 'UnicodeTranslateError': , 'AssertionError': , 'ArithmeticError': , 'FloatingPointError': , 'OverflowError': , 'ZeroDivisionError': , 'SystemError': , 'ReferenceError': , 'MemoryError': , 'BufferError': , 'Warning': , 'UserWarning': , 'DeprecationWarning': , 'PendingDeprecationWarning': , 'SyntaxWarning': , 'RuntimeWarning': , 'FutureWarning': , 'ImportWarning': , 'UnicodeWarning': , 'BytesWarning': , 'ResourceWarning': , 'ConnectionError': , 'BlockingIOError': , 'BrokenPipeError': , 'ChildProcessError': , 'ConnectionAbortedError': , 'ConnectionRefusedError': , 'ConnectionResetError': , 'FileExistsError': , 'FileNotFoundError': , 'IsADirectoryError': , 'NotADirectoryError': , 'InterruptedError': , 'PermissionError': , 'ProcessLookupError': , 'TimeoutError': , 'open': , 'quit': Use quit() or Ctrl-D (i.e. EOF) to exit, 'exit': Use exit() or Ctrl-D (i.e. EOF) to exit, 'copyright': Copyright (c) 2001-2018 Python Software Foundation. +All Rights Reserved. + +Copyright (c) 2000 BeOpen.com. +All Rights Reserved. + +Copyright (c) 1995-2001 Corporation for National Research Initiatives. +All Rights Reserved. + +Copyright (c) 1991-1995 Stichting Mathematisch Centrum, Amsterdam. +All Rights Reserved., 'credits': Thanks to CWI, CNRI, BeOpen.com, Zope Corporation and a cast of thousands + for supporting Python development. See www.python.org for more information., 'license': Type license() to see the full license text, 'help': Type help() for interactive help, or help(object) for help about object.} + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: @py_builtins + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: @pytest_ar + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: settings + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: User + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: Permission + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: reverse + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: status + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: Token + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: View + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: BaseTestCase + obj: + finish pytest_pycollect_makeitem --> [hook] + pytest_pycollect_makeitem [hook] + collector: + name: AuthorizedCrudTestCase + obj: + finish pytest_pycollect_makeitem --> [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __repr__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __getattribute__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __setattr__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __delattr__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __init__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __new__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __dir__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __dict__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __hash__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __str__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __lt__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __le__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __eq__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __ne__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __gt__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __ge__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __reduce_ex__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __reduce__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __subclasshook__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __init_subclass__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __format__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __sizeof__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __class__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + finish pytest_make_collect_report --> [hook] + genitems [collection] + pytest_collectstart [hook] + collector: + finish pytest_collectstart --> [] [hook] + pytest_make_collect_report [hook] + collector: + finish pytest_make_collect_report --> [hook] + genitems [collection] + pytest_itemcollected [hook] + item: + finish pytest_itemcollected --> [] [hook] + genitems [collection] + pytest_itemcollected [hook] + item: + finish pytest_itemcollected --> [] [hook] + pytest_collectreport [hook] + report: + finish pytest_collectreport --> [] [hook] + genitems [collection] + pytest_collectstart [hook] + collector: + finish pytest_collectstart --> [] [hook] + pytest_make_collect_report [hook] + collector: + finish pytest_make_collect_report --> [hook] + pytest_collectreport [hook] + report: + finish pytest_collectreport --> [] [hook] + pytest_collectreport [hook] + report: + finish pytest_collectreport --> [] [hook] + genitems [collection] + pytest_collectstart [hook] + collector: + finish pytest_collectstart --> [] [hook] + pytest_make_collect_report [hook] + collector: + find_module called for: ui_framework.tests.tests_models [assertion] + matched test file '/usr/src/love/manager/ui_framework/tests/tests_models.py' [assertion] + found cached rewritten pyc for '/usr/src/love/manager/ui_framework/tests/tests_models.py' [assertion] + pytest_pycollect_makeitem [hook] + collector: + name: __name__ + obj: ui_framework.tests.tests_models + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __doc__ + obj: Test the models. + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __package__ + obj: ui_framework.tests + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __loader__ + obj: <_pytest.assertion.rewrite.AssertionRewritingHook object at 0x7f34179d8898> + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __spec__ + obj: ModuleSpec(name='ui_framework.tests.tests_models', loader=<_pytest.assertion.rewrite.AssertionRewritingHook object at 0x7f34179d8898>, origin='/usr/src/love/manager/ui_framework/tests/tests_models.py') + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __file__ + obj: /usr/src/love/manager/ui_framework/tests/tests_models.py + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __cached__ + obj: /usr/src/love/manager/ui_framework/tests/__pycache__/tests_models.cpython-37.pyc + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __builtins__ + obj: {'__name__': 'builtins', '__doc__': "Built-in functions, exceptions, and other objects.\n\nNoteworthy: None is the `nil' object; Ellipsis represents `...' in slices.", '__package__': '', '__loader__': , '__spec__': ModuleSpec(name='builtins', loader=), '__build_class__': , '__import__': , 'abs': , 'all': , 'any': , 'ascii': , 'bin': , 'breakpoint': , 'callable': , 'chr': , 'compile': , 'delattr': , 'dir': , 'divmod': , 'eval': , 'exec': , 'format': , 'getattr': , 'globals': , 'hasattr': , 'hash': , 'hex': , 'id': , 'input': , 'isinstance': , 'issubclass': , 'iter': , 'len': , 'locals': , 'max': , 'min': , 'next': , 'oct': , 'ord': , 'pow': , 'print': , 'repr': , 'round': , 'setattr': , 'sorted': , 'sum': , 'vars': , 'None': None, 'Ellipsis': Ellipsis, 'NotImplemented': NotImplemented, 'False': False, 'True': True, 'bool': , 'memoryview': , 'bytearray': , 'bytes': , 'classmethod': , 'complex': , 'dict': , 'enumerate': , 'filter': , 'float': , 'frozenset': , 'property': , 'int': , 'list': , 'map': , 'object': , 'range': , 'reversed': , 'set': , 'slice': , 'staticmethod': , 'str': , 'super': , 'tuple': , 'type': , 'zip': , '__debug__': True, 'BaseException': , 'Exception': , 'TypeError': , 'StopAsyncIteration': , 'StopIteration': , 'GeneratorExit': , 'SystemExit': , 'KeyboardInterrupt': , 'ImportError': , 'ModuleNotFoundError': , 'OSError': , 'EnvironmentError': , 'IOError': , 'EOFError': , 'RuntimeError': , 'RecursionError': , 'NotImplementedError': , 'NameError': , 'UnboundLocalError': , 'AttributeError': , 'SyntaxError': , 'IndentationError': , 'TabError': , 'LookupError': , 'IndexError': , 'KeyError': , 'ValueError': , 'UnicodeError': , 'UnicodeEncodeError': , 'UnicodeDecodeError': , 'UnicodeTranslateError': , 'AssertionError': , 'ArithmeticError': , 'FloatingPointError': , 'OverflowError': , 'ZeroDivisionError': , 'SystemError': , 'ReferenceError': , 'MemoryError': , 'BufferError': , 'Warning': , 'UserWarning': , 'DeprecationWarning': , 'PendingDeprecationWarning': , 'SyntaxWarning': , 'RuntimeWarning': , 'FutureWarning': , 'ImportWarning': , 'UnicodeWarning': , 'BytesWarning': , 'ResourceWarning': , 'ConnectionError': , 'BlockingIOError': , 'BrokenPipeError': , 'ChildProcessError': , 'ConnectionAbortedError': , 'ConnectionRefusedError': , 'ConnectionResetError': , 'FileExistsError': , 'FileNotFoundError': , 'IsADirectoryError': , 'NotADirectoryError': , 'InterruptedError': , 'PermissionError': , 'ProcessLookupError': , 'TimeoutError': , 'open': , 'quit': Use quit() or Ctrl-D (i.e. EOF) to exit, 'exit': Use exit() or Ctrl-D (i.e. EOF) to exit, 'copyright': Copyright (c) 2001-2018 Python Software Foundation. +All Rights Reserved. + +Copyright (c) 2000 BeOpen.com. +All Rights Reserved. + +Copyright (c) 1995-2001 Corporation for National Research Initiatives. +All Rights Reserved. + +Copyright (c) 1991-1995 Stichting Mathematisch Centrum, Amsterdam. +All Rights Reserved., 'credits': Thanks to CWI, CNRI, BeOpen.com, Zope Corporation and a cast of thousands + for supporting Python development. See www.python.org for more information., 'license': Type license() to see the full license text, 'help': Type help() for interactive help, or help(object) for help about object.} + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: @py_builtins + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: @pytest_ar + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: TestCase + obj: + finish pytest_pycollect_makeitem --> [hook] + pytest_pycollect_makeitem [hook] + collector: + name: timezone + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: freeze_time + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: Workspace + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: View + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: WorkspaceView + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: WorkspaceModelTestCase + obj: + finish pytest_pycollect_makeitem --> [hook] + pytest_pycollect_makeitem [hook] + collector: + name: ViewModelTestCase + obj: + finish pytest_pycollect_makeitem --> [hook] + pytest_pycollect_makeitem [hook] + collector: + name: WorkspaceViewModelTestCase + obj: + finish pytest_pycollect_makeitem --> [hook] + pytest_pycollect_makeitem [hook] + collector: + name: WorkspaceAndViewsRelationsTestCase + obj: + finish pytest_pycollect_makeitem --> [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __repr__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __getattribute__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __setattr__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __delattr__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __init__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __new__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __dir__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __dict__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __hash__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __str__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __lt__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __le__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __eq__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __ne__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __gt__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __ge__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __reduce_ex__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __reduce__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __subclasshook__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __init_subclass__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __format__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __sizeof__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + pytest_pycollect_makeitem [hook] + collector: + name: __class__ + obj: + finish pytest_pycollect_makeitem --> None [hook] + finish pytest_make_collect_report --> [hook] + genitems [collection] + pytest_collectstart [hook] + collector: + finish pytest_collectstart --> [] [hook] + pytest_make_collect_report [hook] + collector: + finish pytest_make_collect_report --> [hook] + pytest_collectreport [hook] + report: + finish pytest_collectreport --> [] [hook] + genitems [collection] + pytest_collectstart [hook] + collector: + finish pytest_collectstart --> [] [hook] + pytest_make_collect_report [hook] + collector: + finish pytest_make_collect_report --> [hook] + genitems [collection] + pytest_itemcollected [hook] + item: + finish pytest_itemcollected --> [] [hook] + genitems [collection] + pytest_itemcollected [hook] + item: + finish pytest_itemcollected --> [] [hook] + genitems [collection] + pytest_itemcollected [hook] + item: + finish pytest_itemcollected --> [] [hook] + genitems [collection] + pytest_itemcollected [hook] + item: + finish pytest_itemcollected --> [] [hook] + pytest_collectreport [hook] + report: + finish pytest_collectreport --> [] [hook] + genitems [collection] + pytest_collectstart [hook] + collector: + finish pytest_collectstart --> [] [hook] + pytest_make_collect_report [hook] + collector: + finish pytest_make_collect_report --> [hook] + genitems [collection] + pytest_itemcollected [hook] + item: + finish pytest_itemcollected --> [] [hook] + genitems [collection] + pytest_itemcollected [hook] + item: + finish pytest_itemcollected --> [] [hook] + genitems [collection] + pytest_itemcollected [hook] + item: + finish pytest_itemcollected --> [] [hook] + genitems [collection] + pytest_itemcollected [hook] + item: + finish pytest_itemcollected --> [] [hook] + pytest_collectreport [hook] + report: + finish pytest_collectreport --> [] [hook] + genitems [collection] + pytest_collectstart [hook] + collector: + finish pytest_collectstart --> [] [hook] + pytest_make_collect_report [hook] + collector: + finish pytest_make_collect_report --> [hook] + genitems [collection] + pytest_itemcollected [hook] + item: + finish pytest_itemcollected --> [] [hook] + genitems [collection] + pytest_itemcollected [hook] + item: + finish pytest_itemcollected --> [] [hook] + genitems [collection] + pytest_itemcollected [hook] + item: + finish pytest_itemcollected --> [] [hook] + genitems [collection] + pytest_itemcollected [hook] + item: + finish pytest_itemcollected --> [] [hook] + pytest_collectreport [hook] + report: + finish pytest_collectreport --> [] [hook] + genitems [collection] + pytest_collectstart [hook] + collector: + finish pytest_collectstart --> [] [hook] + pytest_make_collect_report [hook] + collector: + finish pytest_make_collect_report --> [hook] + genitems [collection] + pytest_itemcollected [hook] + item: + finish pytest_itemcollected --> [] [hook] + genitems [collection] + pytest_itemcollected [hook] + item: + finish pytest_itemcollected --> [] [hook] + pytest_collectreport [hook] + report: + finish pytest_collectreport --> [] [hook] + pytest_collectreport [hook] + report: + finish pytest_collectreport --> [] [hook] + pytest_collectreport [hook] + report: + finish pytest_collectreport --> [] [hook] + pytest_collection_modifyitems [hook] + session: testsfailed=0 testscollected=0> + config: <_pytest.config.Config object at 0x7f3418040dd8> + items: [, , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , ] + pytest_deselected [hook] + items: [, , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , ] + finish pytest_deselected --> [] [hook] + finish pytest_collection_modifyitems --> [] [hook] + pytest_collection_finish [hook] + session: testsfailed=0 testscollected=0> + pytest_report_collectionfinish [hook] + config: <_pytest.config.Config object at 0x7f3418040dd8> + startdir: /usr/src/love/manager + items: [, , , , , , , , ] + finish pytest_report_collectionfinish --> [] [hook] + finish pytest_collection_finish --> [] [hook] + pytest_warning_captured [hook] + warning_message: {message : DeprecationWarning("Using or importing the ABCs from 'collections' instead of from 'collections.abc' is deprecated, and in 3.8 it will stop working"), category : 'DeprecationWarning', filename : '/usr/local/lib/python3.7/site-packages/itypes.py', lineno : 2, line : None} + when: collect + item: None + finish pytest_warning_captured --> [] [hook] + pytest_warning_captured [hook] + warning_message: {message : DeprecationWarning("Using or importing the ABCs from 'collections' instead of from 'collections.abc' is deprecated, and in 3.8 it will stop working"), category : 'DeprecationWarning', filename : '/usr/local/lib/python3.7/site-packages/itypes.py', lineno : 2, line : None} + when: collect + item: None + finish pytest_warning_captured --> [] [hook] + finish pytest_collection --> [, , , , , , , , ] [hook] + pytest_runtestloop [hook] + session: testsfailed=0 testscollected=9> + pytest_runtest_protocol [hook] + item: + nextitem: + pytest_runtest_logstart [hook] + nodeid: subscription/tests/test_connection.py::TestClientConnection::test_connection_interrupted_when_token_is_deleted + location: ('subscription/tests/test_connection.py', 128, 'TestClientConnection.test_connection_interrupted_when_token_is_deleted') + early skip of rewriting module: py._io.capture [assertion] + finish pytest_runtest_logstart --> [] [hook] + pytest_runtest_setup [hook] + item: + pytest_fixture_setup [hook] + fixturedef: + request: > + finish pytest_fixture_setup --> None [hook] + pytest_fixture_setup [hook] + fixturedef: + request: > + finish pytest_fixture_setup --> None [hook] + pytest_fixture_setup [hook] + fixturedef: + request: > + finish pytest_fixture_setup --> [hook] + pytest_fixture_setup [hook] + fixturedef: + request: > + finish pytest_fixture_setup --> None [hook] + pytest_fixture_setup [hook] + fixturedef: + request: > + finish pytest_fixture_setup --> None [hook] + pytest_fixture_setup [hook] + fixturedef: + request: > + finish pytest_fixture_setup --> None [hook] + pytest_fixture_setup [hook] + fixturedef: + request: > + pytest_fixture_setup [hook] + fixturedef: + request: > + finish pytest_fixture_setup --> True [hook] + pytest_fixture_setup [hook] + fixturedef: + request: > + finish pytest_fixture_setup --> False [hook] + pytest_fixture_setup [hook] + fixturedef: + request: > + finish pytest_fixture_setup --> False [hook] + pytest_fixture_setup [hook] + fixturedef: + request: > + finish pytest_fixture_setup --> None [hook] + pytest_fixture_setup [hook] + fixturedef: + request: > + finish pytest_fixture_setup --> None [hook] + pytest_fixture_setup [hook] + fixturedef: + request: > + matched marked file 'pytest_django.compat' (from 'pytest_django') [assertion] + find_module called for: pytest_django.compat [assertion] + found cached rewritten pyc for '/usr/local/lib/python3.7/site-packages/pytest_django/compat.py' [assertion] + early skip of rewriting module: django.core.management.commands.migrate [assertion] + early skip of rewriting module: django.db.migrations.autodetector [assertion] + early skip of rewriting module: django.db.migrations.optimizer [assertion] + early skip of rewriting module: django.db.migrations.questioner [assertion] + early skip of rewriting module: django.db.migrations.loader [assertion] + early skip of rewriting module: django.db.migrations.graph [assertion] + early skip of rewriting module: django.db.migrations.recorder [assertion] + early skip of rewriting module: django.db.migrations.utils [assertion] + early skip of rewriting module: django.db.migrations.executor [assertion] + early skip of rewriting module: django.contrib.admin.management [assertion] + early skip of rewriting module: django.contrib.sessions.management [assertion] + early skip of rewriting module: django.contrib.sessions.management [assertion] + early skip of rewriting module: django.contrib.messages.management [assertion] + early skip of rewriting module: rest_framework.management [assertion] + early skip of rewriting module: rest_framework.management [assertion] + early skip of rewriting module: corsheaders.management [assertion] + early skip of rewriting module: drf_yasg.management [assertion] + early skip of rewriting module: drf_yasg.management [assertion] + early skip of rewriting module: subscription.management [assertion] + early skip of rewriting module: ui_framework.management [assertion] + early skip of rewriting module: django.contrib.auth.migrations [assertion] + early skip of rewriting module: django.contrib.auth.migrations.0010_alter_group_name_max_length [assertion] + early skip of rewriting module: django.contrib.auth.migrations.0003_alter_user_email_max_length [assertion] + early skip of rewriting module: django.contrib.auth.migrations.0006_require_contenttypes_0002 [assertion] + early skip of rewriting module: django.contrib.auth.migrations.0002_alter_permission_name_max_length [assertion] + early skip of rewriting module: django.contrib.auth.migrations.0008_alter_user_username_max_length [assertion] + early skip of rewriting module: django.contrib.auth.migrations.0004_alter_user_username_opts [assertion] + early skip of rewriting module: django.contrib.auth.migrations.0011_update_proxy_permissions [assertion] + early skip of rewriting module: django.contrib.auth.migrations.0009_alter_user_last_name_max_length [assertion] + early skip of rewriting module: django.contrib.auth.migrations.0007_alter_validators_add_error_messages [assertion] + early skip of rewriting module: django.contrib.auth.migrations.0001_initial [assertion] + early skip of rewriting module: django.contrib.auth.migrations.0005_alter_user_last_login_null [assertion] + early skip of rewriting module: django.contrib.admin.migrations [assertion] + early skip of rewriting module: django.contrib.admin.migrations.0003_logentry_add_action_flag_choices [assertion] + early skip of rewriting module: django.contrib.admin.migrations.0002_logentry_remove_auto_add [assertion] + early skip of rewriting module: django.contrib.admin.migrations.0001_initial [assertion] + early skip of rewriting module: django.contrib.contenttypes.migrations [assertion] + early skip of rewriting module: django.contrib.contenttypes.migrations.0002_remove_content_type_name [assertion] + early skip of rewriting module: django.contrib.contenttypes.migrations.0001_initial [assertion] + early skip of rewriting module: django.contrib.sessions.migrations [assertion] + early skip of rewriting module: django.contrib.sessions.migrations.0001_initial [assertion] + early skip of rewriting module: django.contrib.messages.migrations [assertion] + early skip of rewriting module: django.contrib.staticfiles.migrations [assertion] + early skip of rewriting module: channels.migrations [assertion] + early skip of rewriting module: rest_framework.migrations [assertion] + early skip of rewriting module: corsheaders.migrations [assertion] + early skip of rewriting module: drf_yasg.migrations [assertion] + early skip of rewriting module: api.migrations [assertion] + early skip of rewriting module: api.migrations.0002_auto_20190528_1546 [assertion] + early skip of rewriting module: api.migrations.0003_auto_20190528_1552 [assertion] + early skip of rewriting module: api.migrations.0005_auto_20190722_1622 [assertion] + early skip of rewriting module: api.migrations.0004_globalpermissions [assertion] + early skip of rewriting module: api.migrations.0001_initial [assertion] + early skip of rewriting module: subscription.migrations [assertion] + early skip of rewriting module: ui_framework.migrations [assertion] + early skip of rewriting module: ui_framework.migrations.0003_view_thumbnail [assertion] + early skip of rewriting module: ui_framework.migrations.0001_initial [assertion] + early skip of rewriting module: ui_framework.migrations.0002_view_data [assertion] + early skip of rewriting module: django.db.models.sql.compiler [assertion] + early skip of rewriting module: django.contrib.auth.migrations [assertion] + early skip of rewriting module: django.contrib.admin.migrations [assertion] + early skip of rewriting module: django.contrib.contenttypes.migrations [assertion] + early skip of rewriting module: django.contrib.sessions.migrations [assertion] + early skip of rewriting module: django.contrib.messages.migrations [assertion] + early skip of rewriting module: django.contrib.staticfiles.migrations [assertion] + early skip of rewriting module: channels.migrations [assertion] + early skip of rewriting module: rest_framework.migrations [assertion] + early skip of rewriting module: corsheaders.migrations [assertion] + early skip of rewriting module: drf_yasg.migrations [assertion] + early skip of rewriting module: api.migrations [assertion] + early skip of rewriting module: subscription.migrations [assertion] + early skip of rewriting module: ui_framework.migrations [assertion] + early skip of rewriting module: django.core.serializers.xml_serializer [assertion] + early skip of rewriting module: xml.dom.pulldom [assertion] + early skip of rewriting module: xml.sax [assertion] + early skip of rewriting module: xml.sax.xmlreader [assertion] + early skip of rewriting module: xml.sax.handler [assertion] + early skip of rewriting module: xml.sax._exceptions [assertion] + early skip of rewriting module: xml.sax.expatreader [assertion] + early skip of rewriting module: xml.sax.saxutils [assertion] + early skip of rewriting module: django.utils.xmlutils [assertion] + early skip of rewriting module: django.core.serializers.pyyaml [assertion] + early skip of rewriting module: django.core.management.commands.createcachetable [assertion] + early skip of rewriting module: django.core.cache.backends.db [assertion] + early skip of rewriting module: django.core.cache.backends.locmem [assertion] + finish pytest_fixture_setup --> None [hook] + pytest_fixture_setup [hook] + fixturedef: + request: > + finish pytest_fixture_setup --> None [hook] + finish pytest_fixture_setup --> None [hook] + pytest_fixture_setup [hook] + fixturedef: + request: > + finish pytest_fixture_setup --> None [hook] + pytest_fixture_setup [hook] + fixturedef: + request: > + finish pytest_fixture_setup --> None [hook] + pytest_fixture_setup [hook] + fixturedef: + request: > + finish pytest_fixture_setup --> None [hook] + pytest_fixture_setup [hook] + fixturedef: + request: > + finish pytest_fixture_setup --> <_UnixSelectorEventLoop running=False closed=False debug=False> [hook] + finish pytest_runtest_setup --> [] [hook] + pytest_runtest_makereport [hook] + item: + call: + finish pytest_runtest_makereport --> [hook] + pytest_runtest_logreport [hook] + report: + pytest_report_teststatus [hook] + report: + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_report_teststatus --> ('', '', '') [hook] + finish pytest_runtest_logreport --> [] [hook] + pytest_runtest_call [hook] + item: + pytest_pyfunc_call [hook] + pyfuncitem: + finish pytest_pyfunc_call --> True [hook] + finish pytest_runtest_call --> [] [hook] + pytest_runtest_makereport [hook] + item: + call: + finish pytest_runtest_makereport --> [hook] + pytest_runtest_logreport [hook] + report: + pytest_report_teststatus [hook] + report: + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_report_teststatus --> ('passed', '.', 'PASSED') [hook] + finish pytest_runtest_logreport --> [] [hook] + pytest_runtest_teardown [hook] + item: + nextitem: + pytest_fixture_post_finalizer [hook] + fixturedef: + request: > + finish pytest_fixture_post_finalizer --> [] [hook] + pytest_fixture_post_finalizer [hook] + fixturedef: + request: > + finish pytest_fixture_post_finalizer --> [] [hook] + pytest_fixture_post_finalizer [hook] + fixturedef: + request: > + finish pytest_fixture_post_finalizer --> [] [hook] + pytest_fixture_post_finalizer [hook] + fixturedef: + request: > + finish pytest_fixture_post_finalizer --> [] [hook] + pytest_fixture_post_finalizer [hook] + fixturedef: + request: > + finish pytest_fixture_post_finalizer --> [] [hook] + pytest_fixture_post_finalizer [hook] + fixturedef: + request: > + finish pytest_fixture_post_finalizer --> [] [hook] + early skip of rewriting module: django.core.management.commands.flush [assertion] + early skip of rewriting module: django.contrib.admin.management [assertion] + early skip of rewriting module: django.contrib.messages.management [assertion] + early skip of rewriting module: corsheaders.management [assertion] + early skip of rewriting module: subscription.management [assertion] + early skip of rewriting module: ui_framework.management [assertion] + pytest_fixture_post_finalizer [hook] + fixturedef: + request: > + finish pytest_fixture_post_finalizer --> [] [hook] + pytest_fixture_post_finalizer [hook] + fixturedef: + request: > + finish pytest_fixture_post_finalizer --> [] [hook] + pytest_fixture_post_finalizer [hook] + fixturedef: + request: > + finish pytest_fixture_post_finalizer --> [] [hook] + pytest_fixture_post_finalizer [hook] + fixturedef: + request: > + finish pytest_fixture_post_finalizer --> [] [hook] + finish pytest_runtest_teardown --> [] [hook] + pytest_runtest_makereport [hook] + item: + call: + finish pytest_runtest_makereport --> [hook] + pytest_runtest_logreport [hook] + report: + pytest_report_teststatus [hook] + report: + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_report_teststatus --> ('', '', '') [hook] + finish pytest_runtest_logreport --> [] [hook] + pytest_runtest_logfinish [hook] + nodeid: subscription/tests/test_connection.py::TestClientConnection::test_connection_interrupted_when_token_is_deleted + location: ('subscription/tests/test_connection.py', 128, 'TestClientConnection.test_connection_interrupted_when_token_is_deleted') + finish pytest_runtest_logfinish --> [] [hook] + finish pytest_runtest_protocol --> True [hook] + pytest_runtest_protocol [hook] + item: + nextitem: + pytest_runtest_logstart [hook] + nodeid: api/tests/tests_auth_api.py::AuthApiTestCase::test_user_fails_to_validate_deleted_token + location: ('api/tests/tests_auth_api.py', 174, 'AuthApiTestCase.test_user_fails_to_validate_deleted_token') + finish pytest_runtest_logstart --> [] [hook] + pytest_runtest_setup [hook] + item: + pytest_fixture_setup [hook] + fixturedef: + request: > + finish pytest_fixture_setup --> None [hook] + pytest_fixture_setup [hook] + fixturedef: + request: > + finish pytest_fixture_setup --> None [hook] + pytest_fixture_setup [hook] + fixturedef: + request: > + finish pytest_fixture_setup --> None [hook] + pytest_fixture_setup [hook] + fixturedef: + request: > + finish pytest_fixture_setup --> None [hook] + pytest_fixture_setup [hook] + fixturedef: + request: > + finish pytest_fixture_setup --> None [hook] + pytest_fixture_setup [hook] + fixturedef: + request: > + finish pytest_fixture_setup --> None [hook] + pytest_fixture_setup [hook] + fixturedef: + request: > + finish pytest_fixture_setup --> None [hook] + pytest_fixture_setup [hook] + fixturedef: + request: > + finish pytest_fixture_setup --> None [hook] + finish pytest_runtest_setup --> [] [hook] + pytest_runtest_makereport [hook] + item: + call: + finish pytest_runtest_makereport --> [hook] + pytest_runtest_logreport [hook] + report: + pytest_report_teststatus [hook] + report: + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_report_teststatus --> ('', '', '') [hook] + finish pytest_runtest_logreport --> [] [hook] + pytest_runtest_call [hook] + item: + early skip of rewriting module: manager.urls [assertion] + early skip of rewriting module: drf_yasg.views [assertion] + early skip of rewriting module: django.views.decorators.vary [assertion] + early skip of rewriting module: rest_framework.response [assertion] + early skip of rewriting module: rest_framework.views [assertion] + early skip of rewriting module: rest_framework.schemas [assertion] + early skip of rewriting module: rest_framework.schemas.coreapi [assertion] + early skip of rewriting module: rest_framework.schemas.generators [assertion] + early skip of rewriting module: django.contrib.admindocs [assertion] + early skip of rewriting module: django.contrib.admindocs.views [assertion] + early skip of rewriting module: django.contrib.admin.views.decorators [assertion] + early skip of rewriting module: django.contrib.auth.decorators [assertion] + early skip of rewriting module: django.shortcuts [assertion] + early skip of rewriting module: django.contrib.admindocs.utils [assertion] + early skip of rewriting module: docutils [assertion] + early skip of rewriting module: docutils.core [assertion] + early skip of rewriting module: docutils.frontend [assertion] + early skip of rewriting module: optparse [assertion] + early skip of rewriting module: docutils.utils [assertion] + early skip of rewriting module: docutils.nodes [assertion] + early skip of rewriting module: docutils.io [assertion] + early skip of rewriting module: docutils.utils.error_reporting [assertion] + early skip of rewriting module: docutils.readers [assertion] + early skip of rewriting module: docutils.parsers [assertion] + early skip of rewriting module: docutils.transforms [assertion] + early skip of rewriting module: docutils.languages [assertion] + early skip of rewriting module: docutils.transforms.universal [assertion] + early skip of rewriting module: docutils.utils.smartquotes [assertion] + early skip of rewriting module: docutils.writers [assertion] + early skip of rewriting module: docutils.readers.doctree [assertion] + early skip of rewriting module: docutils.parsers.rst [assertion] + early skip of rewriting module: docutils.statemachine [assertion] + early skip of rewriting module: docutils.parsers.rst.states [assertion] + early skip of rewriting module: docutils.parsers.rst.directives [assertion] + early skip of rewriting module: docutils.parsers.rst.languages [assertion] + early skip of rewriting module: docutils.parsers.rst.languages.en [assertion] + early skip of rewriting module: docutils.parsers.rst.tableparser [assertion] + early skip of rewriting module: docutils.parsers.rst.roles [assertion] + early skip of rewriting module: docutils.utils.code_analyzer [assertion] + early skip of rewriting module: docutils.utils.punctuation_chars [assertion] + early skip of rewriting module: docutils.utils.roman [assertion] + early skip of rewriting module: docutils.utils.urischemes [assertion] + early skip of rewriting module: rest_framework.schemas.inspectors [assertion] + early skip of rewriting module: rest_framework.schemas.utils [assertion] + early skip of rewriting module: rest_framework.mixins [assertion] + early skip of rewriting module: rest_framework.schemas.openapi [assertion] + early skip of rewriting module: api.authentication [assertion] + early skip of rewriting module: rest_framework.authentication [assertion] + early skip of rewriting module: rest_framework.permissions [assertion] + early skip of rewriting module: rest_framework.parsers [assertion] + early skip of rewriting module: rest_framework.negotiation [assertion] + early skip of rewriting module: rest_framework.utils.mediatypes [assertion] + early skip of rewriting module: rest_framework.metadata [assertion] + early skip of rewriting module: drf_yasg.app_settings [assertion] + early skip of rewriting module: drf_yasg.renderers [assertion] + early skip of rewriting module: drf_yasg.codecs [assertion] + early skip of rewriting module: ruamel.yaml [assertion] + early skip of rewriting module: ruamel.yaml.cyaml [assertion] + early skip of rewriting module: _ruamel_yaml [assertion] + early skip of rewriting module: ruamel.yaml.error [assertion] + early skip of rewriting module: ruamel.yaml.compat [assertion] + early skip of rewriting module: ruamel.ordereddict [assertion] + early skip of rewriting module: ruamel.yaml.reader [assertion] + early skip of rewriting module: ruamel.yaml.util [assertion] + early skip of rewriting module: ruamel.yaml.scanner [assertion] + early skip of rewriting module: ruamel.yaml.tokens [assertion] + early skip of rewriting module: ruamel.yaml.parser [assertion] + early skip of rewriting module: ruamel.yaml.events [assertion] + early skip of rewriting module: ruamel.yaml.composer [assertion] + early skip of rewriting module: ruamel.yaml.nodes [assertion] + early skip of rewriting module: ruamel.yaml.constructor [assertion] + early skip of rewriting module: ruamel.yaml.comments [assertion] + early skip of rewriting module: ruamel.yaml.scalarstring [assertion] + early skip of rewriting module: ruamel.yaml.anchor [assertion] + early skip of rewriting module: ruamel.yaml.scalarint [assertion] + early skip of rewriting module: ruamel.yaml.scalarfloat [assertion] + early skip of rewriting module: ruamel.yaml.scalarbool [assertion] + early skip of rewriting module: ruamel.yaml.timestamp [assertion] + early skip of rewriting module: ruamel.yaml.emitter [assertion] + early skip of rewriting module: ruamel.yaml.serializer [assertion] + early skip of rewriting module: ruamel.yaml.representer [assertion] + early skip of rewriting module: ruamel.yaml.resolver [assertion] + early skip of rewriting module: ruamel.yaml.main [assertion] + early skip of rewriting module: ruamel.yaml.loader [assertion] + early skip of rewriting module: ruamel.yaml.dumper [assertion] + early skip of rewriting module: drf_yasg.openapi [assertion] + early skip of rewriting module: inflection [assertion] + early skip of rewriting module: drf_yasg.utils [assertion] + early skip of rewriting module: drf_yasg.errors [assertion] + early skip of rewriting module: django.conf.urls.static [assertion] + early skip of rewriting module: drf_yasg.generators [assertion] + early skip of rewriting module: rest_framework.versioning [assertion] + early skip of rewriting module: rest_framework.templatetags [assertion] + early skip of rewriting module: rest_framework.templatetags.rest_framework [assertion] + early skip of rewriting module: drf_yasg.inspectors [assertion] + early skip of rewriting module: drf_yasg.inspectors.base [assertion] + early skip of rewriting module: drf_yasg.inspectors.field [assertion] + early skip of rewriting module: djangorestframework_camel_case [assertion] + early skip of rewriting module: rest_framework_recursive [assertion] + early skip of rewriting module: drf_yasg.inspectors.query [assertion] + early skip of rewriting module: rest_framework.pagination [assertion] + early skip of rewriting module: drf_yasg.inspectors.view [assertion] + early skip of rewriting module: django.contrib.contenttypes.views [assertion] + early skip of rewriting module: api.urls [assertion] + early skip of rewriting module: rest_framework.routers [assertion] + early skip of rewriting module: rest_framework.schemas.views [assertion] + early skip of rewriting module: rest_framework.urlpatterns [assertion] + early skip of rewriting module: api.views [assertion] + early skip of rewriting module: jsonschema [assertion] + early skip of rewriting module: jsonschema.exceptions [assertion] + early skip of rewriting module: jsonschema._utils [assertion] + early skip of rewriting module: jsonschema.compat [assertion] + early skip of rewriting module: jsonschema._format [assertion] + early skip of rewriting module: rfc3987 [assertion] + early skip of rewriting module: rfc3986_validator [assertion] + early skip of rewriting module: strict_rfc3339 [assertion] + early skip of rewriting module: rfc3339_validator [assertion] + early skip of rewriting module: webcolors [assertion] + early skip of rewriting module: jsonpointer [assertion] + early skip of rewriting module: uritemplate.exceptions [assertion] + early skip of rewriting module: jsonschema._types [assertion] + early skip of rewriting module: pyrsistent [assertion] + early skip of rewriting module: pyrsistent._pmap [assertion] + early skip of rewriting module: pyrsistent._compat [assertion] + early skip of rewriting module: pyrsistent._pvector [assertion] + early skip of rewriting module: pyrsistent._transformations [assertion] + early skip of rewriting module: pvectorc [assertion] + early skip of rewriting module: pyrsistent._pset [assertion] + early skip of rewriting module: pyrsistent._pbag [assertion] + early skip of rewriting module: pyrsistent._plist [assertion] + early skip of rewriting module: pyrsistent._pdeque [assertion] + early skip of rewriting module: pyrsistent._checked_types [assertion] + early skip of rewriting module: pyrsistent._field_common [assertion] + early skip of rewriting module: pyrsistent._precord [assertion] + early skip of rewriting module: pyrsistent._pclass [assertion] + early skip of rewriting module: pyrsistent._immutable [assertion] + early skip of rewriting module: pyrsistent._helpers [assertion] + early skip of rewriting module: pyrsistent._toolz [assertion] + early skip of rewriting module: jsonschema.validators [assertion] + early skip of rewriting module: jsonschema._legacy_validators [assertion] + early skip of rewriting module: jsonschema._validators [assertion] + early skip of rewriting module: importlib.metadata [assertion] + early skip of rewriting module: rest_framework.authtoken.views [assertion] + early skip of rewriting module: rest_framework.authtoken.serializers [assertion] + early skip of rewriting module: rest_framework.decorators [assertion] + early skip of rewriting module: api.serializers [assertion] + early skip of rewriting module: api.schema_validator [assertion] + early skip of rewriting module: rest_framework.urls [assertion] + early skip of rewriting module: django.contrib.auth.views [assertion] + early skip of rewriting module: ui_framework.urls [assertion] + early skip of rewriting module: ui_framework.views [assertion] + early skip of rewriting module: rest_framework.viewsets [assertion] + early skip of rewriting module: rest_framework.generics [assertion] + early skip of rewriting module: ui_framework.serializers [assertion] + early skip of rewriting module: readline [assertion] + pytest_enter_pdb [hook] + config: <_pytest.config.Config object at 0x7f3418040dd8> + pdb: <_pytest.debugging.pytestPDB._get_pdb_wrapper_class..PytestPdbWrapper object at 0x7f33f805deb8> + finish pytest_enter_pdb --> [] [hook] + pytest_keyboard_interrupt [hook] + excinfo: + finish pytest_keyboard_interrupt --> [] [hook] + pytest_sessionfinish [hook] + session: testsfailed=0 testscollected=9> + exitstatus: ExitCode.INTERRUPTED + pytest_fixture_post_finalizer [hook] + fixturedef: + request: > + finish pytest_fixture_post_finalizer --> [] [hook] + pytest_fixture_post_finalizer [hook] + fixturedef: + request: > + finish pytest_fixture_post_finalizer --> [] [hook] + pytest_fixture_post_finalizer [hook] + fixturedef: + request: > + finish pytest_fixture_post_finalizer --> [] [hook] + pytest_fixture_post_finalizer [hook] + fixturedef: + request: > + finish pytest_fixture_post_finalizer --> [] [hook] + pytest_fixture_post_finalizer [hook] + fixturedef: + request: > + finish pytest_fixture_post_finalizer --> [] [hook] + pytest_fixture_post_finalizer [hook] + fixturedef: + request: > + finish pytest_fixture_post_finalizer --> [] [hook] + pytest_fixture_post_finalizer [hook] + fixturedef: + request: > + finish pytest_fixture_post_finalizer --> [] [hook] + pytest_fixture_post_finalizer [hook] + fixturedef: + request: > + finish pytest_fixture_post_finalizer --> [] [hook] + pytest_fixture_post_finalizer [hook] + fixturedef: + request: > + finish pytest_fixture_post_finalizer --> [] [hook] + pytest_fixture_post_finalizer [hook] + fixturedef: + request: > + finish pytest_fixture_post_finalizer --> [] [hook] + pytest_fixture_post_finalizer [hook] + fixturedef: + request: > + finish pytest_fixture_post_finalizer --> [] [hook] + pytest_fixture_post_finalizer [hook] + fixturedef: + request: > + finish pytest_fixture_post_finalizer --> [] [hook] + pytest_fixture_post_finalizer [hook] + fixturedef: + request: > + finish pytest_fixture_post_finalizer --> [] [hook] + pytest_fixture_post_finalizer [hook] + fixturedef: + request: > + finish pytest_fixture_post_finalizer --> [] [hook] + pytest_fixture_post_finalizer [hook] + fixturedef: + request: > + finish pytest_fixture_post_finalizer --> [] [hook] + pytest_fixture_post_finalizer [hook] + fixturedef: + request: > + finish pytest_fixture_post_finalizer --> [] [hook] + pytest_fixture_post_finalizer [hook] + fixturedef: + request: > + finish pytest_fixture_post_finalizer --> [] [hook] + pytest_fixture_post_finalizer [hook] + fixturedef: + request: > + finish pytest_fixture_post_finalizer --> [] [hook] + pytest_fixture_post_finalizer [hook] + fixturedef: + request: > + finish pytest_fixture_post_finalizer --> [] [hook] + pytest_fixture_post_finalizer [hook] + fixturedef: + request: > + finish pytest_fixture_post_finalizer --> [] [hook] + pytest_fixture_post_finalizer [hook] + fixturedef: + request: > + finish pytest_fixture_post_finalizer --> [] [hook] + pytest_fixture_post_finalizer [hook] + fixturedef: + request: > + finish pytest_fixture_post_finalizer --> [] [hook] + pytest_fixture_post_finalizer [hook] + fixturedef: + request: > + finish pytest_fixture_post_finalizer --> [] [hook] + pytest_fixture_post_finalizer [hook] + fixturedef: + request: > + finish pytest_fixture_post_finalizer --> [] [hook] + pytest_fixture_post_finalizer [hook] + fixturedef: + request: > + finish pytest_fixture_post_finalizer --> [] [hook] + pytest_fixture_post_finalizer [hook] + fixturedef: + request: > + finish pytest_fixture_post_finalizer --> [] [hook] + pytest_fixture_post_finalizer [hook] + fixturedef: + request: > + finish pytest_fixture_post_finalizer --> [] [hook] + pytest_fixture_post_finalizer [hook] + fixturedef: + request: > + finish pytest_fixture_post_finalizer --> [] [hook] + pytest_fixture_post_finalizer [hook] + fixturedef: + request: > + finish pytest_fixture_post_finalizer --> [] [hook] + pytest_fixture_post_finalizer [hook] + fixturedef: + request: > + finish pytest_fixture_post_finalizer --> [] [hook] + pytest_fixture_post_finalizer [hook] + fixturedef: + request: > + finish pytest_fixture_post_finalizer --> [] [hook] + pytest_fixture_post_finalizer [hook] + fixturedef: + request: > + finish pytest_fixture_post_finalizer --> [] [hook] + pytest_fixture_post_finalizer [hook] + fixturedef: + request: > + finish pytest_fixture_post_finalizer --> [] [hook] + pytest_fixture_post_finalizer [hook] + fixturedef: + request: > + finish pytest_fixture_post_finalizer --> [] [hook] + pytest_fixture_post_finalizer [hook] + fixturedef: + request: > + finish pytest_fixture_post_finalizer --> [] [hook] + pytest_fixture_post_finalizer [hook] + fixturedef: + request: > + finish pytest_fixture_post_finalizer --> [] [hook] + pytest_fixture_post_finalizer [hook] + fixturedef: + request: > + finish pytest_fixture_post_finalizer --> [] [hook] + pytest_fixture_post_finalizer [hook] + fixturedef: + request: > + finish pytest_fixture_post_finalizer --> [] [hook] + pytest_fixture_post_finalizer [hook] + fixturedef: + request: > + finish pytest_fixture_post_finalizer --> [] [hook] + pytest_fixture_post_finalizer [hook] + fixturedef: + request: > + finish pytest_fixture_post_finalizer --> [] [hook] + pytest_fixture_post_finalizer [hook] + fixturedef: + request: > + finish pytest_fixture_post_finalizer --> [] [hook] + pytest_fixture_post_finalizer [hook] + fixturedef: + request: > + finish pytest_fixture_post_finalizer --> [] [hook] + pytest_fixture_post_finalizer [hook] + fixturedef: + request: > + finish pytest_fixture_post_finalizer --> [] [hook] + pytest_fixture_post_finalizer [hook] + fixturedef: + request: > + finish pytest_fixture_post_finalizer --> [] [hook] + pytest_fixture_post_finalizer [hook] + fixturedef: + request: > + finish pytest_fixture_post_finalizer --> [] [hook] + pytest_fixture_post_finalizer [hook] + fixturedef: + request: > + finish pytest_fixture_post_finalizer --> [] [hook] + pytest_fixture_post_finalizer [hook] + fixturedef: + request: > + finish pytest_fixture_post_finalizer --> [] [hook] + pytest_fixture_post_finalizer [hook] + fixturedef: + request: > + finish pytest_fixture_post_finalizer --> [] [hook] + pytest_fixture_post_finalizer [hook] + fixturedef: + request: > + finish pytest_fixture_post_finalizer --> [] [hook] + pytest_fixture_post_finalizer [hook] + fixturedef: + request: > + finish pytest_fixture_post_finalizer --> [] [hook] + pytest_fixture_post_finalizer [hook] + fixturedef: + request: > + finish pytest_fixture_post_finalizer --> [] [hook] + pytest_fixture_post_finalizer [hook] + fixturedef: + request: > + finish pytest_fixture_post_finalizer --> [] [hook] + pytest_fixture_post_finalizer [hook] + fixturedef: + request: > + finish pytest_fixture_post_finalizer --> [] [hook] + pytest_fixture_post_finalizer [hook] + fixturedef: + request: > + finish pytest_fixture_post_finalizer --> [] [hook] + pytest_fixture_post_finalizer [hook] + fixturedef: + request: > + finish pytest_fixture_post_finalizer --> [] [hook] + pytest_fixture_post_finalizer [hook] + fixturedef: + request: > + finish pytest_fixture_post_finalizer --> [] [hook] + pytest_terminal_summary [hook] + terminalreporter: <_pytest.terminal.TerminalReporter object at 0x7f3410ae0be0> + exitstatus: ExitCode.INTERRUPTED + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_terminal_summary --> [] [hook] + finish pytest_sessionfinish --> [] [hook] + pytest_unconfigure [hook] + config: <_pytest.config.Config object at 0x7f3418040dd8> + finish pytest_unconfigure --> [] [hook] diff --git a/manager/requirements.txt b/manager/requirements.txt index cd2f2165..2111e3e6 100644 --- a/manager/requirements.txt +++ b/manager/requirements.txt @@ -1,46 +1,92 @@ -aioredis==1.2.0 -asgiref==2.3.2 -asn1crypto==0.24.0 -async-timeout==2.0.1 +aioredis==1.3.1 +alabaster==0.7.12 +asgiref==3.2.5 +asn1crypto==1.3.0 +astropy==4.0 +async-timeout==3.0.1 atomicwrites==1.3.0 -attrs==18.2.0 -autobahn==18.11.1 -Automat==0.7.0 -cffi==1.11.5 -channels==2.1.7 -channels-redis==2.3.3 +attrs==19.3.0 +autobahn==20.3.1 +Automat==20.2.0 +Babel==2.8.0 +certifi==2019.11.28 +cffi==1.14.0 +channels==2.4.0 +channels-redis==2.4.2 +chardet==3.0.4 constantly==15.1.0 -cryptography==2.4.2 -daphne==2.2.3 -Django==2.1.11 -django-auth-ldap==1.7.0 -django-cors-headers==2.4.0 -djangorestframework==3.9.1 -django-webpack-loader==0.6.0 -flake8==3.7.7 -freezegun==0.3.12 -hiredis==0.2.0 -hyperlink==18.0.0 -idna==2.7 +coreapi==2.3.3 +coreschema==0.0.4 +cryptography==2.8 +daphne==2.4.1 +Django==3.0.4 +django-auth-ldap==2.1.0 +django-cors-headers==3.2.1 +django-webpack-loader==0.7.0 +djangorestframework==3.11.0 +docutils==0.16 +drf-yasg==1.17.1 +entrypoints==0.3 +flake8==3.7.9 +freezegun==0.3.15 +hiredis==1.0.1 +hyperlink==19.0.0 +idna==2.9 +imagesize==1.2.0 +importlib-metadata==1.5.0 incremental==17.5.0 +inflection==0.3.1 +itypes==1.1.0 +Jinja2==2.11.1 +jsonschema==3.2.0 m2r==0.2.1 -more-itertools==5.0.0 -msgpack==0.6.0 -pluggy==0.8.1 -py==1.7.0 -pyasn1==0.4.4 -pyasn1-modules==0.2.2 -pycparser==2.19 -PyHamcrest==1.9.0 -pytest==4.2.0 +MarkupSafe==1.1.1 +mccabe==0.6.1 +mistune==0.8.4 +more-itertools==8.2.0 +msgpack==1.0.0 +numpy==1.18.1 +packaging==20.3 +Pillow==7.0.0 +pip-licenses==2.1.1 +pluggy==0.13.1 +psycopg2==2.8.4 +py==1.8.1 +pyasn1==0.4.8 +pyasn1-modules==0.2.8 +pycodestyle==2.5.0 +pycparser==2.20 +pyflakes==2.1.1 +Pygments==2.6.1 +PyHamcrest==2.0.2 +pyparsing==2.4.6 +pyrsistent==0.15.7 +pytest==5.3.5 pytest-asyncio==0.10.0 -pytest-django==3.4.7 +pytest-django==3.8.0 pytest-env==0.6.2 -python-ldap==3.1.0 -pytz==2018.7 -six==1.12.0 -Sphinx==2.1.2 +python-dateutil==2.8.1 +python-ldap==3.2.0 +pytz==2019.3 +PyYAML==5.3 +requests==2.23.0 +ruamel.yaml==0.16.10 +ruamel.yaml.clib==0.2.0 +six==1.14.0 +snowballstemmer==2.0.0 +Sphinx==2.4.4 sphinx-rtd-theme==0.4.3 -Twisted==19.7.0 -txaio==18.8.1 -zope.interface==4.6.0 +sphinxcontrib-applehelp==1.0.2 +sphinxcontrib-devhelp==1.0.2 +sphinxcontrib-htmlhelp==1.0.3 +sphinxcontrib-jsmath==1.0.1 +sphinxcontrib-qthelp==1.0.3 +sphinxcontrib-serializinghtml==1.1.4 +sqlparse==0.3.1 +Twisted==20.3.0 +txaio==20.1.1 +uritemplate==3.0.1 +urllib3==1.25.8 +wcwidth==0.1.8 +zipp==3.1.0 +zope.interface==4.7.2 diff --git a/manager/requirements_no_version.txt b/manager/requirements_no_version.txt new file mode 100644 index 00000000..1c88c29f --- /dev/null +++ b/manager/requirements_no_version.txt @@ -0,0 +1,91 @@ +aioredis +alabaster +asgiref +asn1crypto +astropy +async-timeout +atomicwrites +attrs +autobahn +Automat +Babel +certifi +cffi +channels +channels-redis +chardet +constantly +coreapi +coreschema +cryptography +daphne +Django +django-auth-ldap +django-cors-headers +django-webpack-loader +djangorestframework +docutils +drf-yasg +entrypoints +flake8 +freezegun +hiredis +hyperlink +idna +imagesize +importlib-metadata +incremental +inflection +itypes +Jinja2 +jsonschema +m2r +MarkupSafe +mccabe +mistune +more-itertools +msgpack +numpy +packaging +Pillow +pluggy +psycopg2 +py +pyasn1 +pyasn1-modules +pycodestyle +pycparser +pyflakes +Pygments +PyHamcrest +pyparsing +pyrsistent +pytest +pytest-asyncio +pytest-django +pytest-env +python-dateutil +python-ldap +pytz +PyYAML +requests +ruamel.yaml +ruamel.yaml.clib +six +snowballstemmer +Sphinx +sphinx-rtd-theme +sphinxcontrib-applehelp +sphinxcontrib-devhelp +sphinxcontrib-htmlhelp +sphinxcontrib-jsmath +sphinxcontrib-qthelp +sphinxcontrib-serializinghtml +sqlparse +Twisted +txaio +uritemplate +urllib3 +wcwidth +zipp +zope.interface diff --git a/manager/runserver-dev.sh b/manager/runserver-dev.sh index ad026d67..97e00340 100755 --- a/manager/runserver-dev.sh +++ b/manager/runserver-dev.sh @@ -1,7 +1,21 @@ #!/bin/bash +echo -e "\nMaking migrations" +while ! python manage.py makemigrations +do + echo -e "Sleeping 1 second waiting for database ${DB_HOST} ${DB_PORT}" + sleep 1 +done +echo -e "\nConected to ${DB_HOST} ${DB_PORT}" -python manage.py makemigrations +echo -e "\nApplying migrations" python manage.py migrate +echo -e "\nCreating default users" python manage.py createusers --adminpass ${ADMIN_USER_PASS} --userpass ${USER_USER_PASS} --cmduserpass ${CMD_USER_PASS} +echo -e "\nApplying fixtures" +mkdir -p media/thumbnails +cp -u ui_framework/fixtures/thumbnails/* media/thumbnails +python manage.py loaddata ui_framework/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 ca93dbbf..78f97491 100755 --- a/manager/runserver.sh +++ b/manager/runserver.sh @@ -1,7 +1,21 @@ #!/bin/bash +echo -e "\nMaking migrations" +while ! python manage.py makemigrations +do + echo -e "Sleeping 1 second waiting for database ${DB_HOST} ${DB_PORT}" + sleep 1 +done +echo -e "\nConected to ${DB_HOST} ${DB_PORT}" -python manage.py makemigrations +echo -e "\nApplying migrations" python manage.py migrate +echo -e "\nCreating default users" python manage.py createusers --adminpass ${ADMIN_USER_PASS} --userpass ${USER_USER_PASS} --cmduserpass ${CMD_USER_PASS} +echo -e "\nApplying fixtures" +mkdir -p media/thumbnails +cp -u ui_framework/fixtures/thumbnails/* media/thumbnails +python manage.py loaddata ui_framework/fixtures/initial_data.json + +echo -e "\nStarting server" daphne -b 0.0.0.0 -p 8000 manager.asgi:application diff --git a/manager/subscription/__init__.py b/manager/subscription/__init__.py index e69de29b..aa7522a3 100644 --- a/manager/subscription/__init__.py +++ b/manager/subscription/__init__.py @@ -0,0 +1 @@ +default_app_config = 'subscription.apps.SubscriptionConfig' diff --git a/manager/subscription/apps.py b/manager/subscription/apps.py new file mode 100644 index 00000000..94c83ccb --- /dev/null +++ b/manager/subscription/apps.py @@ -0,0 +1,8 @@ +"""Django apps configuration for the api app.""" +from django.apps import AppConfig + + +class SubscriptionConfig(AppConfig): + """General Subscription config class.""" + + name = 'subscription' diff --git a/manager/subscription/auth.py b/manager/subscription/auth.py index 86a95797..4d4fcd0a 100644 --- a/manager/subscription/auth.py +++ b/manager/subscription/auth.py @@ -2,9 +2,22 @@ import urllib.parse as urlparse from django.contrib.auth.models import AnonymousUser from django.db import close_old_connections +from channels.db import database_sync_to_async from api.models import Token +@database_sync_to_async +def get_user(token): + if not token: + return AnonymousUser() + + token_obj = Token.objects.filter(key=token).first() + if token_obj: + return token_obj.user + else: + return AnonymousUser() + + class TokenAuthMiddleware: """Custom middleware to use a token for user authentication on websockets connections.""" @@ -19,22 +32,20 @@ def __call__(self, scope): scope: `dict` dictionary defining parameters for the authentication """ - query_string = scope.get('query_string').decode() - data = urlparse.parse_qs(query_string) - token_key = None - scope['user'] = AnonymousUser() - scope['password'] = False + return TokenAuthMiddlewareInstance(scope, self) - if 'token' in data: - token_key = data['token'][0] - token = Token.objects.filter(key=token_key).first() - if token is not None: - scope['user'] = token.user - if 'password' in data: - password = data['password'][0] - if password is not None: - scope['password'] = password +class TokenAuthMiddlewareInstance: + def __init__(self, scope, middleware): + self.middleware = middleware + self.scope = dict(scope) + self.inner = self.middleware.inner + async def __call__(self, receive, send): close_old_connections() - return self.inner(scope) + query_string = self.scope.get('query_string').decode() + data = urlparse.parse_qs(query_string) + self.scope['user'] = await get_user(data['token'][0] if 'token' in data else None) + self.scope['password'] = data['password'][0] if 'password' in data else None + inner = self.inner(self.scope) + return await inner(receive, send) diff --git a/manager/subscription/consumers.py b/manager/subscription/consumers.py index 2e190088..c58e765e 100644 --- a/manager/subscription/consumers.py +++ b/manager/subscription/consumers.py @@ -1,30 +1,46 @@ """Contains the Django Channels Consumers that handle the reception/sending of channels messages.""" import json import random +import asyncio +import datetime +from channels.db import database_sync_to_async from channels.generic.websocket import AsyncJsonWebsocketConsumer from manager.settings import PROCESS_CONNECTION_PASS +from manager import utils +from subscription.heartbeat_manager import HeartbeatManager class SubscriptionConsumer(AsyncJsonWebsocketConsumer): """Consumer that handles incoming websocket messages.""" + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.first_connection = asyncio.Future() + self.heartbeat_manager = HeartbeatManager() + self.heartbeat_manager.initialize() + async def connect(self): """Handle connection, rejects connection if no authenticated user.""" self.stream_group_names = [] - self.pending_commands = set() # Reject connection if no authenticated user: - if self.scope['user'].is_anonymous: - if self.scope['password'] and self.scope['password'] == PROCESS_CONNECTION_PASS: + if self.scope["user"].is_anonymous: + if ( + self.scope["password"] and self.scope["password"] == PROCESS_CONNECTION_PASS + ): await self.accept() + self.first_connection.set_result(True) else: await self.close() else: await self.accept() + self.first_connection.set_result(True) + url_token = self.scope["query_string"][6:].decode() + personal_group_name = "token-{}".format(url_token) + await self.channel_layer.group_add(personal_group_name, self.channel_name) async def disconnect(self, close_code): """Handle disconnection.""" # Leave telemetry_stream group - self.pending_commands = set() for telemetry_stream in self.stream_group_names: await self._leave_group(*telemetry_stream) @@ -39,8 +55,12 @@ async def receive_json(self, message): message: `dict` dictionary containing the message parsed as json """ - if 'option' in message: + if "option" in message: await self.handle_subscription_message(message) + elif "action" in message: + await self.handle_action_message(message) + elif "heartbeat" in message: + await self.handle_heartbeat_message(message) else: await self.handle_data_message(message) @@ -57,37 +77,60 @@ async def handle_subscription_message(self, message): The expected format of the message is as follows: { option: 'subscribe'/'unsubscribe' - category: 'event'/'telemetry'/'cmd', + category: 'event'/'telemetry', csc: 'ScriptQueue', salindex: 1, stream: 'stream1', } """ - option = message['option'] - category = message['category'] + option = message["option"] + category = message["category"] - if option == 'subscribe': + if option == "subscribe": # Subscribe and send confirmation - csc = message['csc'] - salindex = message['salindex'] - stream = message['stream'] + csc = message["csc"] + salindex = message["salindex"] + stream = message["stream"] await self._join_group(category, csc, str(salindex), stream) - await self.send_json({ - 'data': 'Successfully subscribed to %s-%s-%s-%s' % (category, csc, salindex, stream) - }) - return + await self.send_json( + { + "data": "Successfully subscribed to %s-%s-%s-%s" + % (category, csc, salindex, stream) + } + ) - if option == 'unsubscribe': + if option == "unsubscribe": # Unsubscribe and send confirmation - csc = message['csc'] - salindex = message['salindex'] - stream = message['stream'] + csc = message["csc"] + salindex = message["salindex"] + stream = message["stream"] await self._leave_group(category, csc, str(salindex), stream) - await self.send_json({ - 'data': 'Successfully unsubscribed to %s-%s-%s-%s' % (category, csc, salindex, stream) - }) + await self.send_json( + { + "data": "Successfully unsubscribed to %s-%s-%s-%s" + % (category, csc, salindex, stream) + } + ) return + + async def handle_action_message(self, message): + time_data = utils.get_times() + await self.send_json( + { + "time_data": json.dumps(time_data) + } + ) + return + + # Expects a message with the format: + # { + # heartbeat: + # timestamp: (optional) + # } + async def handle_heartbeat_message(self, message): + timestamp = message["timestamp"] if "timestamp" in message else datetime.datetime.now().timestamp() + self.heartbeat_manager.set_heartbeat_timestamp(message["heartbeat"], timestamp) async def handle_data_message(self, message): """Handle a data message. @@ -110,90 +153,52 @@ async def handle_data_message(self, message): } }] } - The expected format of the message for a command/ack is as follows: - { - category: 'cmd'/'ack', - data: [{ - csc: 'ScriptQueue', - salindex: 1, - data: { - stream: { - cmd: 'CommandPath', - params: { - 'param1': 'value1', - 'param2': 'value2', - ... - }, - } - } - }] - } """ - data = message['data'] - category = message['category'] - user = self.scope['user'] - if category == 'cmd' and not user.has_perm('api.command.execute_command'): - await self.send_json({ - 'data': 'Command not sent. User does not have permissions to send commands.' - }) - return + data = message["data"] + category = message["category"] + user = self.scope["user"] + # Send data to telemetry_stream groups for csc_message in data: - csc = csc_message['csc'] - salindex = csc_message['salindex'] - data_csc = csc_message['data'] - csc_message['data'] = data_csc + csc = csc_message["csc"] + salindex = csc_message["salindex"] + data_csc = csc_message["data"] + csc_message["data"] = data_csc streams = data_csc.keys() streams_data = {} for stream in streams: sub_category = category msg_type = "subscription_data" - group_name = '-'.join([sub_category, csc, str(salindex), stream]) - if category == "cmd": - await self._join_group("cmd_acks", "all", "all", "all") - cmd_id = random.getrandbits(128) - if 'cmd_id' in data_csc[stream]: - cmd_id = data_csc[stream]['cmd_id'] - self.pending_commands.add(cmd_id) - print('New Command', self.pending_commands) - if category == "ack": - print('New Ack', self.pending_commands, message) - sub_category = "cmd" # Use sub group from cmds - msg_type = "subscription_ack" - group_name = 'cmd_acks-all-all-all' + group_name = "-".join([sub_category, csc, str(salindex), stream]) await self.channel_layer.group_send( group_name, { - 'type': msg_type, - 'category': category, - 'csc': csc, - 'salindex': salindex, - 'data': {stream: data_csc[stream]}, - 'subscription': group_name - } + "type": msg_type, + "category": category, + "csc": csc, + "salindex": salindex, + "data": {stream: data_csc[stream]}, + "subscription": group_name, + }, ) streams_data[stream] = data_csc[stream] await self.channel_layer.group_send( - '-'.join([category, csc, str(salindex), 'all']), + "-".join([category, csc, str(salindex), "all"]), { - 'type': 'subscription_data', - 'category': category, - 'csc': csc, - 'salindex': salindex, - 'data': {csc: streams_data}, - 'subscription': '-'.join([category, csc, str(salindex), 'all']) - } + "type": "subscription_data", + "category": category, + "csc": csc, + "salindex": salindex, + "data": {csc: streams_data}, + "subscription": "-".join([category, csc, str(salindex), "all"]), + }, ) # Send all data to consumers subscribed to "all" subscriptions of the same category await self.channel_layer.group_send( - '{}-all-all-all'.format(category), - { - 'type': 'subscription_all_data', - 'category': category, - 'data': data - } + "{}-all-all-all".format(category), + {"type": "subscription_all_data", "category": category, "data": data}, ) async def _join_group(self, category, csc, salindex, stream): @@ -202,7 +207,7 @@ async def _join_group(self, category, csc, salindex, stream): Parameters ---------- category: `string` - category of the message, it can be either: 'cmd', 'event' or 'telemetry' + category of the message, it can be either: 'event' or 'telemetry' csc : `string` CSC associated to the message. E.g. 'ScriptQueue' salindex : `string` @@ -210,14 +215,30 @@ async def _join_group(self, category, csc, salindex, stream): stream : `string` Stream to subscribe to. E.g. 'stream_1' """ - key = '-'.join([category, csc, salindex, stream]) + key = "-".join([category, csc, salindex, stream]) if [category, csc, salindex, stream] in self.stream_group_names: return self.stream_group_names.append([category, csc, salindex, stream]) - await self.channel_layer.group_add( - key, - self.channel_name - ) + await self.channel_layer.group_add(key, self.channel_name) + + # If subscribing to an event, send the initial_state + if category == "event": + await self.channel_layer.group_send( + "initial_state-all-all-all", + { + "type": "subscription_all_data", + "category": "initial_state", + "data": [ + { + "csc": csc, + "salindex": int(salindex) + if salindex != "all" + else salindex, + "data": {"event_name": stream}, + } + ], + }, + ) async def _leave_group(self, category, csc, salindex, stream): """Leave a group in order to receive messages from it. @@ -225,7 +246,7 @@ async def _leave_group(self, category, csc, salindex, stream): Parameters ---------- category: `string` - category of the message, it can be either: 'cmd', 'event' or 'telemetry' + category of the message, it can be either: 'event' or 'telemetry' csc : `string` CSC associated to the message. E.g. 'ScriptQueue' salindex : `string` @@ -233,13 +254,10 @@ async def _leave_group(self, category, csc, salindex, stream): stream : `string` Stream to subscribe to. E.g. 'stream_1' """ - key = '-'.join([category, csc, salindex, stream]) + key = "-".join([category, csc, salindex, stream]) if [category, csc, salindex, stream] in self.stream_group_names: self.stream_group_names.remove([category, csc, salindex, stream]) - await self.channel_layer.group_discard( - key, - self.channel_name - ) + await self.channel_layer.group_discard(key, self.channel_name) async def subscription_data(self, message): """ @@ -252,72 +270,70 @@ async def subscription_data(self, message): message: `dict` dictionary containing the message parsed as json """ - data = message['data'] - category = message['category'] - salindex = message['salindex'] - csc = message['csc'] - subscription = message['subscription'] + data = message["data"] + category = message["category"] + salindex = message["salindex"] + csc = message["csc"] + subscription = message["subscription"] # Send data to WebSocket - await self.send(text_data=json.dumps({ - 'category': category, - 'data': [{ - 'csc': csc, - 'salindex': salindex, - 'data': data - }], - 'subscription': subscription - })) - - async def subscription_ack(self, message): + await self.send( + text_data=json.dumps( + { + "category": category, + "data": [{"csc": csc, "salindex": salindex, "data": data}], + "subscription": subscription, + } + ) + ) + + async def subscription_all_data(self, message): """ Send a message to all the instances of a consumer that have joined the group. - It is used to send ack messages associated to subscriptions to all the groups of a particular category. - Only sends messages to those groups with a corresponding pending cmd. + It is used to send messages associated to subscriptions to all the groups of a particular category Parameters ---------- message: `dict` dictionary containing the message parsed as json """ - data = message['data'] - category = message['category'] - salindex = message['salindex'] - csc = message['csc'] - for stream in data: - print('stream', data[stream]) - cmd_id = data[stream]['cmd_id'] - if cmd_id in self.pending_commands: - self.pending_commands.discard(cmd_id) - await self.send(text_data=json.dumps({ - 'category': category, - 'data': [{ - 'csc': csc, - 'salindex': salindex, - 'data': data, - }], - 'subscription': 'cmd_acks-all-all-all' - })) + data = message["data"] + category = message["category"] + # subscription = '{}-all-all-all'.format(category) - async def subscription_all_data(self, message): + # Send data to WebSocket + await self.send( + text_data=json.dumps( + { + "category": category, + "data": data, + "subscription": "{}-all-all-all".format(category), + } + ) + ) + + async def send_heartbeat(self, message): """ - Send a message to all the instances of a consumer that have joined the group. + Send a heartbeat to all the instances of a consumer that have joined the heartbeat-manager-0-stream. It is used to send messages associated to subscriptions to all the groups of a particular category Parameters ---------- - message: `dict` - dictionary containing the message parsed as json + message: `string` + dictionary containing the heartbeat message """ - data = message['data'] - category = message['category'] - # subscription = '{}-all-all-all'.format(category) - # Send data to WebSocket - await self.send(text_data=json.dumps({ - 'category': category, - 'data': data, - 'subscription': '{}-all-all-all'.format(category) - })) + await self.send(text_data=message["data"]) + + async def logout(self, message): + """Closes the connection. + + Parameters + ---------- + message: `string` + message received, it is part of the API (as this function is called by a message reception) + but it is not used + """ + await self.close() diff --git a/manager/subscription/heartbeat_manager.py b/manager/subscription/heartbeat_manager.py new file mode 100644 index 00000000..96b70662 --- /dev/null +++ b/manager/subscription/heartbeat_manager.py @@ -0,0 +1,59 @@ +import asyncio +import datetime +import json +from channels.layers import get_channel_layer + + +class HeartbeatManager: + + heartbeat_task = None + heartbeat_data = {} + + @classmethod + def initialize(self): + self.heartbeat_data = {} + if not self.heartbeat_task: + self.heartbeat_task = asyncio.create_task(self.dispatch_heartbeats()) + + @classmethod + def set_heartbeat_timestamp(self, source, timestamp): + self.heartbeat_data[source] = timestamp + + @classmethod + async def dispatch_heartbeats(self): + + channel_layer = get_channel_layer() + while True: + try: + self.set_heartbeat_timestamp('Manager', datetime.datetime.now().timestamp()) + 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): + if self.heartbeat_task: + self.heartbeat_task = None + self.heartbeat_data = {} + + @classmethod + async def stop(self): + if self.heartbeat_task: + self.heartbeat_task.cancel() + diff --git a/manager/subscription/tests/test_commands.py b/manager/subscription/tests/test_commands.py deleted file mode 100644 index cd742adb..00000000 --- a/manager/subscription/tests/test_commands.py +++ /dev/null @@ -1,180 +0,0 @@ -"""Tests for the sending of comands.""" -import pytest -from django.contrib.auth.models import User, Permission -from channels.testing import WebsocketCommunicator -from manager.routing import application -from api.models import Token - - -class TestCommands: - """Test that clients can or cannot send commands depending on different conditions.""" - - categories = ['event', 'telemetry', 'cmd'] - - cscs = ['ScriptQueue', 'ATDome'] - - salindices = [1, 2] - - streams = ['stream1', 'stream2'] - - combinations = [] - - no_reception_timeout = 0.001 - - 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.token = Token.objects.create(user=self.user) - self.url = 'manager/ws/subscription/?token={}'.format(self.token) - - def build_messages(self, category, csc, salindex, streams): - """Build and return messages to send and expect, for testing purposes. - - Parameters - ---------- - category: `string` - category of the message - csc: `string` - CSC of the message - streams: `[string]` - list of streams for which to add values in the message - - Returns - ------- - sent: `{}` - Dictionary containing the message to send in the test - expected: `{}` - Dictionary containing the expected response to be received by the client in the test - """ - response = { - 'category': category, - 'data': [{ - 'csc': csc, - 'salindex': salindex, - 'data': {stream: {'value': 1.02813957817852497, 'dataType': 'Float'} for stream in streams} - }], - 'subscription': '{}-{}-{}-{}'.format(category, csc, salindex, streams[0]) - } - return response, response - - @pytest.mark.asyncio - @pytest.mark.django_db - async def test_authorized_user_can_send_command(self): - """Test that an authorized user can send commands.""" - # Arrange - communicator = WebsocketCommunicator(application, self.url) - self.user.user_permissions.add(Permission.objects.get(name='Execute Commands')) - connected, subprotocol = await communicator.connect() - msg = { - "option": "subscribe", - "csc": "ScriptQueue", - "salindex": 1, - "stream": "stream1", - "category": "cmd" - } - await communicator.send_json_to(msg) - response = await communicator.receive_json_from() - # Act - msg, expected = self.build_messages('cmd', 'ScriptQueue', 1, ['stream1']) - await communicator.send_json_to(msg) - # Assert - response = await communicator.receive_json_from() - assert response == expected - await communicator.disconnect() - - @pytest.mark.asyncio - @pytest.mark.django_db - async def test_unauthorized_user_cannot_send_command(self): - """Test that an unauthorized user cannot send commands.""" - # Arrange - communicator = WebsocketCommunicator(application, self.url) - connected, subprotocol = await communicator.connect() - msg = { - "option": "subscribe", - "csc": "ScriptQueue", - "salindex": 1, - "stream": "stream1", - "category": "cmd" - } - await communicator.send_json_to(msg) - await communicator.receive_json_from() - # Act - msg, expected = self.build_messages('cmd', 'ScriptQueue', 1, ['stream1']) - await communicator.send_json_to(msg) - # Assert - response = await communicator.receive_json_from() - assert 'Command not sent' in response['data'] - await communicator.disconnect() - - @pytest.mark.asyncio - @pytest.mark.django_db - async def test_authorized_user_gets_ack(self): - """Test that commands get acknowledged.""" - # Arrange - communicator = WebsocketCommunicator(application, self.url) - self.user.user_permissions.add(Permission.objects.get(name='Execute Commands')) - connected, subprotocol = await communicator.connect() - msg = { - "option": "subscribe", - "csc": "ScriptQueue", - "salindex": 1, - "stream": "stream", - "category": "cmd" - } - await communicator.send_json_to(msg) - response = await communicator.receive_json_from() - # Act - msg_send = { - "category": "cmd", - "data": [{ - "csc": "ScriptQueue", - "salindex": 1, - "data": { - "stream": { - "cmd": "test", - "cmd_params": {"param1": 1, "param2": 2}, - "cmd_id": 12345 - } - } - }] - } - msg_receive = { - "category": "ack", - "data": [{ - "csc": "ScriptQueue", - "salindex": 1, - "data": { - "stream": { - "cmd": "test", - "cmd_params": {"param1": 1, "param2": 2}, - "cmd_id": 12345 - } - } - }], - "subscription": "cmd_acks-all-all-all" - } - ack = { - "category": "ack", - "data": [{ - "csc": "ScriptQueue", - "salindex": 1, - "data": { - "stream": { - "cmd": "test", - "cmd_params": {"param1": 1, "param2": 2}, - "cmd_id": 12345 - } - } - }], - "subscription": "cmd_acks-all-all-all" - } - await communicator.send_json_to(msg_send) - await communicator.receive_json_from() - await communicator.send_json_to(msg_receive) - # Assert - response = await communicator.receive_json_from() - print(ack) - print('\n\n\n') - print(response) - assert ack == response - await communicator.disconnect() \ No newline at end of file diff --git a/manager/subscription/tests/test_connection.py b/manager/subscription/tests/test_connection.py index a648761a..e94e225f 100644 --- a/manager/subscription/tests/test_connection.py +++ b/manager/subscription/tests/test_connection.py @@ -1,7 +1,10 @@ """Tests for the connection of users.""" import pytest +import asyncio from django.contrib.auth.models import User +from channels.db import database_sync_to_async from channels.testing import WebsocketCommunicator +from channels.layers import get_channel_layer from manager.routing import application from manager.settings import PROCESS_CONNECTION_PASS from api.models import Token @@ -10,14 +13,18 @@ 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.token = Token.objects.create(user=self.user) + self.user2 = User.objects.create_user('username2', password='123', email='user@user.cl') + self.token2 = Token.objects.create(user=self.user2) + @pytest.mark.asyncio - @pytest.mark.django_db + @pytest.mark.django_db(transaction=True) async def test_connection_with_token(self): """Test that clients can connect with a valid token.""" # Arrange - user = User.objects.create_user('username', password='123', email='user@user.cl') - token = Token.objects.create(user=user) - url = 'manager/ws/subscription/?token={}'.format(token) + url = 'manager/ws/subscription/?token={}'.format(self.token) communicator = WebsocketCommunicator(application, url) # Act connected, subprotocol = await communicator.connect() @@ -26,7 +33,7 @@ async def test_connection_with_token(self): await communicator.disconnect() @pytest.mark.asyncio - @pytest.mark.django_db + @pytest.mark.django_db(transaction=True) async def test_connection_with_password(self): """Test that clients can connect with a valid password.""" # Arrange @@ -40,13 +47,11 @@ async def test_connection_with_password(self): await communicator.disconnect() @pytest.mark.asyncio - @pytest.mark.django_db + @pytest.mark.django_db(transaction=True) async def test_connection_failed_for_invalid_token(self): """Test that clients cannot connect with an invalid token.""" # Arrange - user = User.objects.create_user('username', password='123', email='user@user.cl') - token = Token.objects.create(user=user) - url = 'manager/ws/subscription/?token={}'.format(str(token) + 'fake') + url = 'manager/ws/subscription/?token={}'.format(str(self.token) + 'fake') communicator = WebsocketCommunicator(application, url) # Act connected, subprotocol = await communicator.connect() @@ -55,7 +60,7 @@ async def test_connection_failed_for_invalid_token(self): await communicator.disconnect() @pytest.mark.asyncio - @pytest.mark.django_db + @pytest.mark.django_db(transaction=True) async def test_connection_failed_for_invalid_password(self): """Test that clients cannot connect with an invalid password.""" # Arrange @@ -67,3 +72,104 @@ async def test_connection_failed_for_invalid_password(self): # Assert assert not connected, 'Communicator should not have connected' await communicator.disconnect() + + @pytest.mark.asyncio + @pytest.mark.django_db(transaction=True) + async def test_connection_interrupted_when_logout_message_is_sent(self): + """Test that a client gets disconnected when a message is sent for it to logout, for only that client""" + # ARRANGE + password = PROCESS_CONNECTION_PASS + subscription_msg = { + "option": "subscribe", + "csc": "ScriptQueue", + "salindex": 0, + "stream": "stream1", + "category": "event" + } + 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)) + for client in [client1, client2, client3]: + connected, subprotocol = await client.connect() + 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': ''} + ) + 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() + + # 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 + + # 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 + + # Disconnect all clients + await client1.disconnect() + await client2.disconnect() + await client3.disconnect() + + @pytest.mark.asyncio + @pytest.mark.django_db(transaction=True) + async def test_connection_interrupted_when_token_is_deleted(self): + """Test that a client gets disconnected when the token is deleted from the database""" + # ARRANGE + password = PROCESS_CONNECTION_PASS + subscription_msg = { + "option": "subscribe", + "csc": "ScriptQueue", + "salindex": 0, + "stream": "stream1", + "category": "event" + } + 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)) + for client in [client1, client2, client3]: + connected, subprotocol = await client.connect() + 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 + + # 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() + + # 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 + + # 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 + + # Disconnect all clients + await client1.disconnect() + await client2.disconnect() + await client3.disconnect() diff --git a/manager/subscription/tests/test_heartbeat.py b/manager/subscription/tests/test_heartbeat.py new file mode 100644 index 00000000..f039ce03 --- /dev/null +++ b/manager/subscription/tests/test_heartbeat.py @@ -0,0 +1,170 @@ +"""Tests for the subscription of consumers to love_csc streams.""" +import asyncio +import datetime +import pytest +from django.contrib.auth.models import User, Permission +from channels.testing import WebsocketCommunicator +from manager.routing import application +from api.models import Token +from subscription.heartbeat_manager import HeartbeatManager + + +class TestHeartbeat: + + no_reception_timeout = 4 + + 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.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) + + @pytest.mark.asyncio + @pytest.mark.django_db(transaction=True) + async def test_join_and_leave_subscription(self): + # Arrange + await HeartbeatManager.reset() + communicator = WebsocketCommunicator(application, self.url) + connected, subprotocol = await communicator.connect() + + # Act 1 (Subscribe) + msg = { + "option": "subscribe", + "category": "heartbeat", + "csc": "manager", + "salindex": 0, + "stream": "stream", + } + await communicator.send_json_to(msg) + response = await communicator.receive_json_from() + + # Assert 1 + 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 + # Act 2 (Unsubscribe) + msg = { + "option": "unsubscribe", + "category": "heartbeat", + "csc": "manager", + "salindex": 0, + "stream": "stream", + } + await communicator.send_json_to(msg) + response = await communicator.receive_json_from() + + # Assert 2 + assert response['data'] == f'Successfully unsubscribed to heartbeat-manager-0-stream' + + await communicator.disconnect() + + communicator = WebsocketCommunicator(application, self.url) + connected, subprotocol = await communicator.connect() + + # Act 1 (Subscribe) + msg = { + "option": "subscribe", + "category": "heartbeat", + "csc": "manager", + "salindex": 0, + "stream": "stream", + } + await communicator.send_json_to(msg) + response = await communicator.receive_json_from() + + # Assert 1 + 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 + # Act 2 (Unsubscribe) + msg = { + "option": "unsubscribe", + "category": "heartbeat", + "csc": "manager", + "salindex": 0, + "stream": "stream", + } + await communicator.send_json_to(msg) + response = await communicator.receive_json_from() + + # Assert 2 + assert response['data'] == f'Successfully unsubscribed to heartbeat-manager-0-stream' + await communicator.disconnect() + await HeartbeatManager.stop() + + @pytest.mark.asyncio + @pytest.mark.django_db(transaction=True) + async def test_heartbeat_manager_setter(self): + # Arrange + await HeartbeatManager.reset() + communicator = WebsocketCommunicator(application, self.url) + connected, subprotocol = await communicator.connect() + + # Act 1 (Subscribe) + msg = { + "option": "subscribe", + "category": "heartbeat", + "csc": "manager", + "salindex": 0, + "stream": "stream", + } + await communicator.send_json_to(msg) + response = await communicator.receive_json_from() + + # Assert 1 + 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 + + # Act 2 Set producer heartbeat + timestamp = datetime.datetime.now().timestamp() + HeartbeatManager.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 + await communicator.disconnect() + await HeartbeatManager.stop() + + @pytest.mark.asyncio + @pytest.mark.django_db(transaction=True) + async def test_producer_heartbeat(self): + # Arrange + await HeartbeatManager.reset() + communicator = WebsocketCommunicator(application, self.url) + connected, subprotocol = await communicator.connect() + + # Act 1 (Subscribe) + msg = { + "option": "subscribe", + "category": "heartbeat", + "csc": "manager", + "salindex": 0, + "stream": "stream", + } + await communicator.send_json_to(msg) + response = await communicator.receive_json_from() + + # Assert 1 + 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 + + # Act 2 (Send producer heartbeat through websocket) + msg = { + "heartbeat": "producer", + "timestamp": 1000, + } + await communicator.send_json_to(msg) + 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 + await communicator.disconnect() + await HeartbeatManager.stop() \ No newline at end of file diff --git a/manager/subscription/tests/test_lovecsc_subscriptions.py b/manager/subscription/tests/test_lovecsc_subscriptions.py new file mode 100644 index 00000000..38462ca5 --- /dev/null +++ b/manager/subscription/tests/test_lovecsc_subscriptions.py @@ -0,0 +1,117 @@ +"""Tests for the subscription of consumers to love_csc streams.""" +import pytest +from django.contrib.auth.models import User, Permission +from channels.testing import WebsocketCommunicator +from manager.routing import application +from api.models import Token + + +class TestLOVECscSubscriptions: + + 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.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) + + @pytest.mark.asyncio + @pytest.mark.django_db(transaction=True) + async def test_join_and_leave_subscription(self): + # Arrange + category = "love_csc" + csc = "love" + salindex = 0 + stream = "observingLog" + communicator = WebsocketCommunicator(application, self.url) + connected, subprotocol = await communicator.connect() + + # Act 1 (Subscribe) + msg = { + "option": "subscribe", + "category": category, + "csc": csc, + "salindex": salindex, + "stream": stream, + } + await communicator.send_json_to(msg) + response = await communicator.receive_json_from() + + # Assert 1 + assert ( + response["data"] == f"Successfully subscribed to {category}-{csc}-{salindex}-{stream}" + ) + + # Act 2 (Unsubscribe) + msg = { + "option": "unsubscribe", + "csc": csc, + "salindex": salindex, + "stream": stream, + "category": category, + } + await communicator.send_json_to(msg) + response = await communicator.receive_json_from() + + # Assert 2 + assert ( + response["data"] + == f"Successfully unsubscribed to {category}-{csc}-{salindex}-{stream}" + ) + + await communicator.disconnect() + + @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 """ + + # Arrange + client_communicator = WebsocketCommunicator(application, self.url) + lovecsc_communicator = WebsocketCommunicator(application, self.url) + await client_communicator.connect() + await lovecsc_communicator.connect() + + # Act 1: Subscribe love_csc and client + await lovecsc_communicator.send_json_to( + { + "option": "subscribe", + "category": "love_csc", + "csc": "love", + "salindex": "0", + "stream": "observingLog", + } + ) + + subscription_response = await lovecsc_communicator.receive_json_from() + + # Assert 1: + assert subscription_response == { + "data": "Successfully subscribed to love_csc-love-0-observingLog" + } + + # Act 2: Client sends observing logs + message = { + "category": "love_csc", + "data": [ + { + "csc": "love", + "salindex": 0, + "data": { + "observingLog": {"user": "an user", "message": "a message"} + }, + } + ], + } + await client_communicator.send_json_to(message) + + # Assert 2: the love_csc receives the observing logs + log_response = await lovecsc_communicator.receive_json_from() + + expected_message = message.copy() + expected_message["subscription"] = "love_csc-love-0-observingLog" + + assert log_response == expected_message diff --git a/manager/subscription/tests/test_subscriptions.py b/manager/subscription/tests/test_subscriptions.py index 60fbafb1..78d35d36 100644 --- a/manager/subscription/tests/test_subscriptions.py +++ b/manager/subscription/tests/test_subscriptions.py @@ -71,7 +71,7 @@ def build_messages(self, category, csc, salindex, streams): return response, response @pytest.mark.asyncio - @pytest.mark.django_db + @pytest.mark.django_db(transaction=True) async def test_join_and_leave_every_subscription(self): """Test that clients can join and then leave any subscription stream.""" # Arrange @@ -110,7 +110,7 @@ async def test_join_and_leave_every_subscription(self): await communicator.disconnect() @pytest.mark.asyncio - @pytest.mark.django_db + @pytest.mark.django_db(transaction=True) async def test_join_and_leave_all_subscription(self): """Test that clients can subscribe and leave all streams.""" # Arrange @@ -145,7 +145,7 @@ async def test_join_and_leave_all_subscription(self): await communicator.disconnect() @pytest.mark.asyncio - @pytest.mark.django_db + @pytest.mark.django_db(transaction=True) async def test_receive_messages_from_every_subscription(self): """Test that clients subscribed (individually) to every stream receive messages from all of them.""" # Arrange @@ -173,7 +173,7 @@ async def test_receive_messages_from_every_subscription(self): await communicator.disconnect() @pytest.mark.asyncio - @pytest.mark.django_db + @pytest.mark.django_db(transaction=True) async def test_receive_messages_from_all_subscription(self): """Test that clients subscribed to all streams receive messages from all of them.""" # Arrange @@ -198,13 +198,11 @@ async def test_receive_messages_from_all_subscription(self): expected['subscription'] = '{}-all-all-all'.format(combination['category']) response = await communicator.receive_json_from() # Assert - print(response) - print(expected) assert response == expected await communicator.disconnect() @pytest.mark.asyncio - @pytest.mark.django_db + @pytest.mark.django_db(transaction=True) async def test_receive_message_for_subscribed_group_only(self): """Test that clients subscribed to some groups only receive messages from those.""" # Arrange @@ -240,7 +238,7 @@ async def test_receive_message_for_subscribed_group_only(self): await communicator.disconnect() @pytest.mark.asyncio - @pytest.mark.django_db + @pytest.mark.django_db(transaction=True) async def test_receive_message_for_subscribed_groups_only(self): """Test that clients subscribed to some groups only receive messages from those.""" # Arrange @@ -282,7 +280,7 @@ async def test_receive_message_for_subscribed_groups_only(self): await communicator.disconnect() @pytest.mark.asyncio - @pytest.mark.django_db + @pytest.mark.django_db(transaction=True) async def test_receive_part_of_message_for_subscribed_groups_only(self): """Test that clients subscribed to some groups only receive the corresponding part of incoming messages.""" # Arrange @@ -324,3 +322,69 @@ async def test_receive_part_of_message_for_subscribed_groups_only(self): await communicator.send_json_to(subscription_msg) await communicator.receive_json_from() await communicator.disconnect() + + @pytest.mark.asyncio + @pytest.mark.django_db(transaction=True) + async def test_request_initial_state_when_subscribing_to_event(self): + """ + Must send a request for initial_state to the Producer whenever + a client subscribes to events + """ + # Arrange + client_communicator = WebsocketCommunicator(application, self.url) + producer_communicator = WebsocketCommunicator(application, self.url) + await client_communicator.connect() + await producer_communicator.connect() + + # Act 1 (Subscribe producer) + await producer_communicator.send_json_to({ + 'option': 'subscribe', + 'category': 'initial_state', + 'csc': 'all', + 'salindex': 'all', + 'stream': 'all' + }) + await producer_communicator.receive_json_from() + + # Act 2 (Subscribe client) + for combination in self.combinations: + # initial state is only useful for events + if combination["category"] != "event": + continue + + msg = { + "option": "subscribe", + "csc": combination["csc"], + "salindex": combination["salindex"], + "stream": combination["stream"], + "category": combination["category"] + } + + # Subscribe the first time + await client_communicator.send_json_to(msg) + producer_consumer_response = await producer_communicator.receive_json_from() + + # Assert first subscription + assert producer_consumer_response == { + 'category': 'initial_state', + 'data': [{ + 'csc': combination["csc"], + 'salindex': combination["salindex"], + 'data': { + 'event_name': combination["stream"] + }, + }], + 'subscription': 'initial_state-all-all-all' + + } + + # Assert second subscription doesn't produce a message + await client_communicator.send_json_to(msg) + + with pytest.raises(asyncio.TimeoutError): + producer_consumer_response = await asyncio.wait_for( + producer_communicator.receive_json_from(), timeout=self.no_reception_timeout + ) + + await client_communicator.disconnect() + await producer_communicator.disconnect() diff --git a/manager/subscription/tests/test_time_data.py b/manager/subscription/tests/test_time_data.py new file mode 100644 index 00000000..b072a19d --- /dev/null +++ b/manager/subscription/tests/test_time_data.py @@ -0,0 +1,39 @@ +"""Tests for the subscription of consumers to love_csc streams.""" +import pytest +import json +from django.contrib.auth.models import User, Permission +from channels.testing import WebsocketCommunicator +from manager.routing import application +from api.models import Token +from manager import utils + + +class TestTimeData: + 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.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) + + @pytest.mark.asyncio + @pytest.mark.django_db(transaction=True) + async def test_get_time_data(self): + # Arrange + communicator = WebsocketCommunicator(application, self.url) + connected, subprotocol = await communicator.connect() + + # Act 1 (Subscribe) + msg = { + "action": "get_time_data", + } + await communicator.send_json_to(msg) + response = await communicator.receive_json_from() + time_data = json.loads(response["time_data"]) + print('time_data:', time_data) + + # Assert 1 + assert utils.assert_time_data(time_data) + await communicator.disconnect() \ No newline at end of file diff --git a/manager/ui_framework/__init__.py b/manager/ui_framework/__init__.py new file mode 100644 index 00000000..76b9ba27 --- /dev/null +++ b/manager/ui_framework/__init__.py @@ -0,0 +1 @@ +default_app_config = 'ui_framework.apps.UiFrameworkConfig' \ No newline at end of file diff --git a/manager/ui_framework/admin.py b/manager/ui_framework/admin.py new file mode 100644 index 00000000..93a998ae --- /dev/null +++ b/manager/ui_framework/admin.py @@ -0,0 +1,14 @@ +""" +Defines the Django Admin model pages for this app . + +Registers the models that will be available throgh the Djangpo Admin interface. + +For more information see: +https://docs.djangoproject.com/en/2.2/ref/contrib/admin/ +""" +from django.contrib import admin +from .models import Workspace, View, WorkspaceView + +admin.site.register(Workspace) +admin.site.register(View) +admin.site.register(WorkspaceView) diff --git a/manager/ui_framework/apps.py b/manager/ui_framework/apps.py new file mode 100644 index 00000000..2df4b2ea --- /dev/null +++ b/manager/ui_framework/apps.py @@ -0,0 +1,12 @@ +"""Django apps configuration for the ui_framework app.""" +from django.apps import AppConfig + + +class UiFrameworkConfig(AppConfig): + """General App config class. Currently defines the name of the app.""" + + name = 'ui_framework' + + def ready(self): + """ Import signals module when app is ready """ + import ui_framework.signals \ No newline at end of file diff --git a/manager/ui_framework/fixtures/initial_data.json b/manager/ui_framework/fixtures/initial_data.json new file mode 100644 index 00000000..62617a53 --- /dev/null +++ b/manager/ui_framework/fixtures/initial_data.json @@ -0,0 +1,654 @@ +[ + { + "model": "ui_framework.view", + "pk": 1, + "fields": { + "creation_timestamp": "2020-01-14T19:47:37.600Z", + "update_timestamp": "2020-03-18T20:22:48.437Z", + "name": "Watcher", + "data": { + "content": { + "newPanel-1": { + "config": {}, + "content": "Watcher", + "properties": { + "h": 41, + "i": 1, + "w": 64, + "x": 0, + "y": 0, + "type": "component" + } + }, + "newPanel-2": { + "config": { + "name": "Watcher", + "salindex": 0, + "_functionProps": [] + }, + "content": "CSCExpanded", + "properties": { + "h": 29, + "i": 2, + "w": 24, + "x": 64, + "y": 0, + "type": "component" + } + } + }, + "properties": { + "h": 2, + "i": 0, + "w": 100, + "x": 0, + "y": 0, + "cols": 100, + "type": "container", + "allowOverflow": true + } + }, + "thumbnail": "thumbnails/view_1.png" + } + }, + { + "model": "ui_framework.view", + "pk": 13, + "fields": { + "creation_timestamp": "2020-01-28T18:20:02.787Z", + "update_timestamp": "2020-03-24T00:28:56.810Z", + "name": "ScripQueue", + "data": { + "content": { + "newPanel-1": { + "config": { + "title": "ScriptQueue", + "salindex": 1, + "titleBar": true + }, + "content": "ScriptQueue", + "properties": { + "h": 41, + "i": 1, + "w": 74, + "x": 0, + "y": 0, + "type": "component" + } + } + }, + "properties": { + "h": 2, + "i": 0, + "w": 100, + "x": 0, + "y": 0, + "cols": 100, + "type": "container", + "allowOverflow": true + } + }, + "thumbnail": "thumbnails/view_13.png" + } + }, + { + "model": "ui_framework.view", + "pk": 15, + "fields": { + "creation_timestamp": "2020-01-29T14:37:29.977Z", + "update_timestamp": "2020-03-24T00:35:39.815Z", + "name": "ObservingLog", + "data": { + "content": { + "newPanel-1": { + "config": { + "title": "Log input", + "margin": true, + "titleBar": true + }, + "content": "ObservingLogInput", + "properties": { + "h": 30, + "i": 1, + "w": 42, + "x": 0, + "y": 0, + "type": "component" + } + }, + "newPanel-2": { + "config": { "title": "Log", "margin": true, "titleBar": true }, + "content": "ObservingLogMessages", + "properties": { + "h": 30, + "i": 2, + "w": 34, + "x": 42, + "y": 0, + "type": "component" + } + } + }, + "properties": { + "h": 2, + "i": 0, + "w": 100, + "x": 0, + "y": 0, + "cols": 100, + "type": "container", + "allowOverflow": true + } + }, + "thumbnail": "thumbnails/view_15.png" + } + }, + { + "model": "ui_framework.view", + "pk": 17, + "fields": { + "creation_timestamp": "2020-01-30T20:50:00.479Z", + "update_timestamp": "2020-03-18T20:22:21.035Z", + "name": "CSCGroup", + "data": { + "content": { + "newPanel-1": { + "config": { + "cscs": [ + { "name": "ATMCS", "salindex": 0 }, + { "name": "ATPtg", "salindex": 0 }, + { "name": "ATDome", "salindex": 0 }, + { "name": "ATDomeTrajectory", "salindex": 0 }, + { "name": "ATAOS", "salindex": 0 }, + { "name": "ATPneumatics", "salindex": 0 }, + { "name": "ATHexapod", "salindex": 0 } + ], + "name": "CSC group" + }, + "content": "CSCGroup", + "properties": { + "h": 19, + "i": 1, + "w": 12, + "x": 0, + "y": 0, + "type": "component" + } + } + }, + "properties": { + "h": 2, + "i": 0, + "w": 100, + "x": 0, + "y": 0, + "cols": 100, + "type": "container", + "allowOverflow": true + } + }, + "thumbnail": "thumbnails/view_17.png" + } + }, + { + "model": "ui_framework.view", + "pk": 18, + "fields": { + "creation_timestamp": "2020-02-06T18:25:23.153Z", + "update_timestamp": "2020-03-24T00:36:24.016Z", + "name": "HealthStatusSummary", + "data": { + "content": { + "newPanel-1": { + "config": { "title": "Health status summary", "titleBar": true }, + "content": "HealthStatusSummary", + "properties": { + "h": 44, + "i": 1, + "w": 78, + "x": 0, + "y": 0, + "type": "component" + } + } + }, + "properties": { + "h": 2, + "i": 0, + "w": 100, + "x": 0, + "y": 0, + "cols": 100, + "type": "container", + "allowOverflow": true + } + }, + "thumbnail": "thumbnails/view_18.png" + } + }, + { + "model": "ui_framework.view", + "pk": 19, + "fields": { + "creation_timestamp": "2020-02-06T18:27:24.173Z", + "update_timestamp": "2020-03-18T20:15:38.101Z", + "name": "TimeSeriesPlot", + "data": { + "content": { + "newPanel-1": { + "config": { + "layers": { + "Dome Azimuth": { "mark": { "interpolate": "linear" } } + }, + "encoding": { + "color": { + "scale": { + "range": ["hsl(201, 70%, 40%)"], + "domain": ["Dome Azimuth"] + } + } + }, + "accessors": "{ \"Dome Azimuth\": (data) => data.azimuthPosition.value }", + "groupNames": { "Dome Azimuth": "telemetry-ATDome-0-position" }, + "dataSources": ["Dome Azimuth"], + "_functionProps": ["accessors"] + }, + "content": "TimeSeriesPlot", + "properties": { + "h": 8, + "i": 1, + "w": 31, + "x": 0, + "y": 0, + "type": "component" + } + } + }, + "properties": { + "h": 2, + "i": 0, + "w": 100, + "x": 0, + "y": 0, + "cols": 100, + "type": "container", + "allowOverflow": true + } + }, + "thumbnail": "thumbnails/view_19.png" + } + }, + { + "model": "ui_framework.view", + "pk": 20, + "fields": { + "creation_timestamp": "2020-02-06T18:38:24.784Z", + "update_timestamp": "2020-03-18T20:15:31.100Z", + "name": "Int. TimeSeries", + "data": { + "content": { + "newPanel-1": { + "config": {}, + "content": "InteractiveTimeSeries", + "properties": { + "h": 44, + "i": 1, + "w": 78, + "x": 0, + "y": 0, + "type": "component" + } + } + }, + "properties": { + "h": 2, + "i": 0, + "w": 100, + "x": 0, + "y": 0, + "cols": 100, + "type": "container", + "allowOverflow": true + } + }, + "thumbnail": "thumbnails/view_20.png" + } + }, + { + "model": "ui_framework.view", + "pk": 21, + "fields": { + "creation_timestamp": "2020-02-06T18:39:27.152Z", + "update_timestamp": "2020-03-24T00:34:29.182Z", + "name": "CSCSummary", + "data": { + "content": { + "newPanel-1": { + "config": { + "title": "CSC summary", + "margin": true, + "titleBar": true, + "hierarchy": { + "Observatory": { + "Queue": [ + { "name": "ScriptQueue", "salindex": 1 }, + { "name": "ScriptQueue", "salindex": 2 } + ], + "Environment": [ + { "name": "DIMM", "salindex": 1 }, + { "name": "DIMM", "salindex": 2 }, + { "name": "Environment", "salindex": 1 } + ] + }, + "Aux Telescope": { + "ATTCS": [ + { "name": "ATMCS", "salindex": 0 }, + { "name": "ATPtg", "salindex": 0 }, + { "name": "ATDome", "salindex": 0 }, + { "name": "ATDomeTrajectory", "salindex": 0 }, + { "name": "ATAOS", "salindex": 0 }, + { "name": "ATPneumatics", "salindex": 0 }, + { "name": "ATHexapod", "salindex": 0 }, + { "name": "GenericCamera", "salindex": 1 } + ], + "LATISS": [ + { "name": "ATCamera", "salindex": 0 }, + { "name": "ATArchiver", "salindex": 0 }, + { "name": "ATHeaderService", "salindex": 0 }, + { "name": "ATSpectrograph", "salindex": 0 } + ], + "ATCalSys": [ + { "name": "ATMonochromator", "salindex": 0 }, + { "name": "FiberSpectrograph", "salindex": 0 }, + { "name": "ATWhiteLight", "salindex": 0 }, + { "name": "Electrometer", "salindex": 1 }, + { "name": "Electrometer", "salindex": 2 }, + { "name": "LinearStage", "salindex": 1 }, + { "name": "LinearStage", "salindex": 2 } + ] + }, + "Main Telescope": { + "CSC Group 1": [ + { "name": "Test", "salindex": 1 }, + { "name": "Test", "salindex": 2 } + ], + "CSC Group 2": [] + } + } + }, + "content": "CSCSummary", + "properties": { + "h": 43, + "i": 1, + "w": 79, + "x": 0, + "y": 0, + "type": "component" + } + } + }, + "properties": { + "h": 2, + "i": 0, + "w": 100, + "x": 0, + "y": 0, + "cols": 100, + "type": "container", + "allowOverflow": true + } + }, + "thumbnail": "thumbnails/view_21.png" + } + }, + { + "model": "ui_framework.view", + "pk": 22, + "fields": { + "creation_timestamp": "2020-02-06T18:41:02.213Z", + "update_timestamp": "2020-03-18T20:15:15.774Z", + "name": "ATMount state", + "data": { + "content": { + "newPanel-1": { + "config": {}, + "content": "MountSummaryPanel", + "properties": { + "h": 22, + "i": 1, + "w": 16, + "x": 54, + "y": 0, + "type": "component" + } + }, + "newPanel-2": { + "config": {}, + "content": "MotorTable", + "properties": { + "h": 11, + "i": 2, + "w": 53, + "x": 0, + "y": 0, + "type": "component" + } + } + }, + "properties": { + "h": 2, + "i": 0, + "w": 100, + "x": 0, + "y": 0, + "cols": 100, + "type": "container", + "allowOverflow": true + } + }, + "thumbnail": "thumbnails/view_22.png" + } + }, + { + "model": "ui_framework.view", + "pk": 23, + "fields": { + "creation_timestamp": "2020-02-06T18:44:05.822Z", + "update_timestamp": "2020-03-24T00:37:51.090Z", + "name": "ATMount overview", + "data": { + "content": { + "newPanel-2": { + "config": { "title": "ATMount overview", "titleBar": true, "hasRawMode": true}, + "content": "Dome", + "properties": { + "h": 42, + "i": 2, + "w": 57, + "x": 0, + "y": 0, + "type": "component" + } + } + }, + "properties": { + "h": 2, + "i": 0, + "w": 100, + "x": 0, + "y": 0, + "cols": 100, + "type": "container", + "allowOverflow": true + } + }, + "thumbnail": "thumbnails/view_23.png" + } + }, + { + "model": "ui_framework.view", + "pk": 24, + "fields": { + "creation_timestamp": "2020-02-06T18:45:04.971Z", + "update_timestamp": "2020-03-24T00:39:19.128Z", + "name": "AT Lightpath", + "data": { + "content": { + "newPanel-1": { + "config": { + "title": "AT Lightpath", + "margin": true, + "titleBar": true, + "hasRawMode": true + }, + "content": "LightPath", + "properties": { + "h": 36, + "i": 1, + "w": 23, + "x": 0, + "y": 0, + "type": "component", + "allowOverflow": true + } + }, + "newPanel-2": { + "config": {}, + "content": "MountSummaryPanel", + "properties": { + "h": 22, + "i": 2, + "w": 15, + "x": 23, + "y": 1, + "type": "component" + } + } + }, + "properties": { + "h": 2, + "i": 0, + "w": 100, + "x": 0, + "y": 0, + "cols": 100, + "type": "container", + "allowOverflow": true + } + }, + "thumbnail": "thumbnails/view_24.png" + } + }, + { + "model": "ui_framework.view", + "pk": 25, + "fields": { + "creation_timestamp": "2020-02-06T18:45:55.011Z", + "update_timestamp": "2020-03-24T00:40:17.172Z", + "name": "LATISS", + "data": { + "content": { + "newPanel-1": { + "config": { "title": "LATISS", "titleBar": true, "hasRawMode": true}, + "content": "LATISS", + "properties": { + "h": 31, + "i": 1, + "w": 61, + "x": 0, + "y": 0, + "type": "component" + } + } + }, + "properties": { + "h": 2, + "i": 0, + "w": 100, + "x": 0, + "y": 0, + "cols": 100, + "type": "container", + "allowOverflow": true + } + }, + "thumbnail": "thumbnails/view_25.png" + } + }, + { + "model": "ui_framework.view", + "pk": 26, + "fields": { + "creation_timestamp": "2020-02-06T18:46:23.602Z", + "update_timestamp": "2020-03-24T00:39:59.273Z", + "name": "ATCamera", + "data": { + "content": { + "newPanel-1": { + "config": { "title": "AT camera", "titleBar": true }, + "content": "Camera", + "properties": { + "h": 23, + "i": 1, + "w": 44, + "x": 0, + "y": 0, + "type": "component" + } + } + }, + "properties": { + "h": 2, + "i": 0, + "w": 100, + "x": 0, + "y": 0, + "cols": 100, + "type": "container", + "allowOverflow": true + } + }, + "thumbnail": "thumbnails/view_26.png" + } + }, + { + "model": "ui_framework.view", + "pk": 27, + "fields": { + "creation_timestamp": "2020-02-06T19:17:11.021Z", + "update_timestamp": "2020-03-24T00:34:57.871Z", + "name": "AT generic camera", + "data": { + "content": { + "newPanel-1": { + "config": { + "title": "Generic Camera", + "titleBar": true, + "serverURL": "http://localhost/gencam" + }, + "content": "GenericCamera", + "properties": { + "h": 36, + "i": 1, + "w": 52, + "x": 0, + "y": 0, + "type": "component" + } + } + }, + "properties": { + "h": 2, + "i": 0, + "w": 100, + "x": 0, + "y": 0, + "cols": 100, + "type": "container", + "allowOverflow": true + } + }, + "thumbnail": "thumbnails/view_27.png" + } + } +] diff --git a/manager/ui_framework/fixtures/thumbnails/view_1.png b/manager/ui_framework/fixtures/thumbnails/view_1.png new file mode 100644 index 00000000..a7234cd7 Binary files /dev/null and b/manager/ui_framework/fixtures/thumbnails/view_1.png differ diff --git a/manager/ui_framework/fixtures/thumbnails/view_13.png b/manager/ui_framework/fixtures/thumbnails/view_13.png new file mode 100644 index 00000000..3ab25b86 Binary files /dev/null and b/manager/ui_framework/fixtures/thumbnails/view_13.png differ diff --git a/manager/ui_framework/fixtures/thumbnails/view_15.png b/manager/ui_framework/fixtures/thumbnails/view_15.png new file mode 100644 index 00000000..4f6a0ab0 Binary files /dev/null and b/manager/ui_framework/fixtures/thumbnails/view_15.png differ diff --git a/manager/ui_framework/fixtures/thumbnails/view_17.png b/manager/ui_framework/fixtures/thumbnails/view_17.png new file mode 100644 index 00000000..41326e8a Binary files /dev/null and b/manager/ui_framework/fixtures/thumbnails/view_17.png differ diff --git a/manager/ui_framework/fixtures/thumbnails/view_18.png b/manager/ui_framework/fixtures/thumbnails/view_18.png new file mode 100644 index 00000000..e66be117 Binary files /dev/null and b/manager/ui_framework/fixtures/thumbnails/view_18.png differ diff --git a/manager/ui_framework/fixtures/thumbnails/view_19.png b/manager/ui_framework/fixtures/thumbnails/view_19.png new file mode 100644 index 00000000..9581c758 Binary files /dev/null and b/manager/ui_framework/fixtures/thumbnails/view_19.png differ diff --git a/manager/ui_framework/fixtures/thumbnails/view_20.png b/manager/ui_framework/fixtures/thumbnails/view_20.png new file mode 100644 index 00000000..329fdac1 Binary files /dev/null and b/manager/ui_framework/fixtures/thumbnails/view_20.png differ diff --git a/manager/ui_framework/fixtures/thumbnails/view_21.png b/manager/ui_framework/fixtures/thumbnails/view_21.png new file mode 100644 index 00000000..53665d5f Binary files /dev/null and b/manager/ui_framework/fixtures/thumbnails/view_21.png differ diff --git a/manager/ui_framework/fixtures/thumbnails/view_22.png b/manager/ui_framework/fixtures/thumbnails/view_22.png new file mode 100644 index 00000000..9a0dd88f Binary files /dev/null and b/manager/ui_framework/fixtures/thumbnails/view_22.png differ diff --git a/manager/ui_framework/fixtures/thumbnails/view_23.png b/manager/ui_framework/fixtures/thumbnails/view_23.png new file mode 100644 index 00000000..63c46223 Binary files /dev/null and b/manager/ui_framework/fixtures/thumbnails/view_23.png differ diff --git a/manager/ui_framework/fixtures/thumbnails/view_24.png b/manager/ui_framework/fixtures/thumbnails/view_24.png new file mode 100644 index 00000000..eb12747d Binary files /dev/null and b/manager/ui_framework/fixtures/thumbnails/view_24.png differ diff --git a/manager/ui_framework/fixtures/thumbnails/view_25.png b/manager/ui_framework/fixtures/thumbnails/view_25.png new file mode 100644 index 00000000..25a6a6f9 Binary files /dev/null and b/manager/ui_framework/fixtures/thumbnails/view_25.png differ diff --git a/manager/ui_framework/fixtures/thumbnails/view_26.png b/manager/ui_framework/fixtures/thumbnails/view_26.png new file mode 100644 index 00000000..aae4185a Binary files /dev/null and b/manager/ui_framework/fixtures/thumbnails/view_26.png differ diff --git a/manager/ui_framework/fixtures/thumbnails/view_27.png b/manager/ui_framework/fixtures/thumbnails/view_27.png new file mode 100644 index 00000000..46c1b140 Binary files /dev/null and b/manager/ui_framework/fixtures/thumbnails/view_27.png differ diff --git a/manager/ui_framework/fixtures/thumbnails/view_28.png b/manager/ui_framework/fixtures/thumbnails/view_28.png new file mode 100644 index 00000000..198eec0b Binary files /dev/null and b/manager/ui_framework/fixtures/thumbnails/view_28.png differ diff --git a/manager/ui_framework/migrations/0001_initial.py b/manager/ui_framework/migrations/0001_initial.py new file mode 100644 index 00000000..9f9d6c0d --- /dev/null +++ b/manager/ui_framework/migrations/0001_initial.py @@ -0,0 +1,60 @@ +# Generated by Django 2.2.6 on 2019-10-08 19:32 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + 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)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + 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)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + 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')), + ], + options={ + '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'), + ), + ] diff --git a/manager/ui_framework/migrations/0002_view_data.py b/manager/ui_framework/migrations/0002_view_data.py new file mode 100644 index 00000000..61d655cb --- /dev/null +++ b/manager/ui_framework/migrations/0002_view_data.py @@ -0,0 +1,19 @@ +# Generated by Django 2.2.6 on 2019-10-08 19:48 + +import django.contrib.postgres.fields.jsonb +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('ui_framework', '0001_initial'), + ] + + operations = [ + migrations.AddField( + 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 new file mode 100644 index 00000000..336066c3 --- /dev/null +++ b/manager/ui_framework/migrations/0003_view_thumbnail.py @@ -0,0 +1,19 @@ +# Generated by Django 2.2.9 on 2020-02-11 18:46 + +from django.db import migrations, models +import ui_framework.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('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/'), + ), + ] diff --git a/manager/ui_framework/migrations/__init__.py b/manager/ui_framework/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/manager/ui_framework/models.py b/manager/ui_framework/models.py new file mode 100644 index 00000000..36204009 --- /dev/null +++ b/manager/ui_framework/models.py @@ -0,0 +1,134 @@ +""" +Defines the Django models for this app. + +For more information see: +https://docs.djangoproject.com/en/2.2/topics/db/models/ +""" +from django.contrib.postgres.fields import JSONField +from django.db import models +from django.core.files.storage import FileSystemStorage +import os +from django.conf import settings + + +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 OverwriteStorage(FileSystemStorage): + + def get_available_name(self, name, max_length): + """Returns a filename that's free on the target storage system, and + available for new content to be written to. + + Found at http://djangosnippets.org/snippets/976/ + + This file storage solves overwrite on upload problem. Another + proposed solution was to override the save method on the model + like so (from https://code.djangoproject.com/ticket/11663): + + def save(self, *args, **kwargs): + try: + this = MyModelName.objects.get(id=self.id) + if this.MyImageFieldName != self.MyImageFieldName: + this.MyImageFieldName.delete() + except: pass + super(MyModelName, self).save(*args, **kwargs) + """ + # If the filename already exists, remove it as if it was a true file system + if self.exists(name): + os.remove(os.path.join(settings.MEDIA_ROOT, name)) + return name + + +class View(BaseModel): + """View Model.""" + + name = models.CharField(max_length=20) + """The name of the View. e.g 'My View'""" + + data = JSONField(null=True, blank=True) + """The data that constitutes the View, stored as a JSON""" + + thumbnail = models.ImageField(upload_to='thumbnails/', storage=OverwriteStorage(), default='thumbnails/default.png') + + def __str__(self): + """Redefine how objects of this class are transformed to string.""" + return self.name + + +class Workspace(BaseModel): + """Workspace Model.""" + + name = models.CharField(max_length=20) + """The name of the Workspace. e.g 'My Workspace'""" + + views = models.ManyToManyField(View, through='WorkspaceView', related_name='workspaces') + + def __str__(self): + """Redefine how objects of this class are transformed to string.""" + return self.name + + def get_sorted_views(self): + """Return the views sorted by the sort_value of their corresponding WorkView. + + Returns + ------- + list: + List of View objects associated to this Workspace + """ + return [wv.view for wv in self.workspace_views.all()] + + @staticmethod + def has_read_permission(request): + return request.user.has_perm('ui_framework.view_workspace') + + +class WorkspaceView(BaseModel): + """WorkspaceView Model, that relates a Works with a View.""" + + workspace = models.ForeignKey(Workspace, on_delete=models.CASCADE, related_name='workspace_views') + """The corresponding Workspace""" + + view = models.ForeignKey(View, on_delete=models.CASCADE, related_name='workspace_views') + """The corresponding View""" + + view_name = models.CharField(max_length=20, blank=True) + """The custom name for the View within the Workspace""" + + sort_value = models.PositiveIntegerField(default=0, blank=False, null=False) + """Order of the View within the Workspace.""" + + class Meta: + """Define attributes of the Meta class.""" + + unique_together = ('workspace', 'view') + """Forbid duplicated pairs of Workspace and View.""" + + ordering = ('sort_value',) + """Set ordering according to 'sort_value' field.""" + + def __str__(self): + """Redefine how objects of this class are transformed to string.""" + if self.view_name and self.view_name != '': + return '{}: {} - {}'.format(self.view_name, self.workspace.name, self.view.name) + else: + return '{} - {}'.format(self.workspace.name, self.view.name) diff --git a/manager/ui_framework/serializers.py b/manager/ui_framework/serializers.py new file mode 100644 index 00000000..a5cf6857 --- /dev/null +++ b/manager/ui_framework/serializers.py @@ -0,0 +1,143 @@ +"""Defines the serializer used by the REST API exposed by this app.""" +from rest_framework import serializers +from ui_framework.models import Workspace, View, WorkspaceView +from django.conf import settings +from django.db.models import Max + + +class Base64ImageField(serializers.ImageField): + """ + A Django REST framework field for handling image-uploads through raw post data. + It uses base64 for encoding and decoding the contents of the file. + + Heavily based on + https://github.com/tomchristie/django-rest-framework/pull/1268 + + Updated for Django REST framework 3. + """ + @staticmethod + def _get_view_id_from_data(data): + """ Returns 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 """ + # id field should come in req data if view exists + if 'id' in data: + return data['id'] + + # safe to assume that max(id)+1 can be overwritten + view_id_max = View.objects.aggregate(Max('id')) + if view_id_max['id__max'] is None: + return 1 + + return view_id_max['id__max'] + 1 + + def to_representation(self, value): + return settings.MEDIA_URL + str(value) + + def to_internal_value(self, data): + from django.core.files.base import ContentFile + import base64 + import six + import uuid + + # Check if this is a base64 string + if isinstance(data, six.string_types): + # Check if the base64 string is in the "data:" format + if 'data:' in data and ';base64,' in data: + # Break out the header from the base64 content + header, data = data.split(';base64,') + else: + return None + + # Try to decode the file. Return validation error if it fails. + try: + decoded_file = base64.b64decode(data) + except TypeError: + self.fail('invalid_image') + + # Generate file name: + view_id = self._get_view_id_from_data(self.parent.context['request'].data) + file_name = f'view_{view_id}' + + # Get the file name extension: + file_extension = self.get_file_extension(file_name, decoded_file) + + complete_file_name = "%s.%s" % (file_name, file_extension, ) + + data = ContentFile(decoded_file, name=complete_file_name) + + return super(Base64ImageField, self).to_internal_value(data) + + def get_file_extension(self, file_name, decoded_file): + import imghdr + + extension = imghdr.what(file_name, decoded_file) + extension = "jpg" if extension == "jpeg" else extension + + return extension + + +class ViewSerializer(serializers.ModelSerializer): + """Serializer for the View model.""" + thumbnail = Base64ImageField( + required=False, max_length=None, use_url=False, allow_empty_file=True, allow_null=True) + + class Meta: + """Meta class to map serializer's fields with the model fields.""" + + model = View + fields = ('id', 'name', 'thumbnail', 'data') + + +class ViewSummarySerializer(serializers.ModelSerializer): + """Serializer for the View model including only id and name.""" + + class Meta: + """Meta class to map serializer's fields with the model fields.""" + + model = View + fields = ('id', 'name', 'thumbnail') + + +class WorkspaceSerializer(serializers.ModelSerializer): + """Serializer for the Workspace model.""" + + class Meta: + """Meta class to map serializer's fields with the model fields.""" + + model = Workspace + fields = '__all__' + + +class WorkspaceFullSerializer(serializers.ModelSerializer): + """Serializer for the Workspace model, including the views fully subserialized.""" + + views = ViewSerializer(many=True, read_only=True) + + class Meta: + """Meta class to map serializer's fields with the model fields.""" + + model = Workspace + fields = '__all__' + + +class WorkspaceWithViewNameSerializer(serializers.ModelSerializer): + """Serializer for the Workspace model, including names of views in the view field.""" + + views = ViewSummarySerializer(many=True, read_only=True) + + class Meta: + """Meta class to map serializer's fields with the model fields.""" + + model = Workspace + fields = '__all__' + + +class WorkspaceViewSerializer(serializers.ModelSerializer): + """Serializer for the WorkspaceView model.""" + + class Meta: + """Meta class to map serializer's fields with the model fields.""" + + model = WorkspaceView + fields = '__all__' diff --git a/manager/ui_framework/signals.py b/manager/ui_framework/signals.py new file mode 100644 index 00000000..3417f35a --- /dev/null +++ b/manager/ui_framework/signals.py @@ -0,0 +1,25 @@ +import os +from django.db.models.signals import post_delete +from django.dispatch import receiver +from manager import settings +from ui_framework.models import View + + +@receiver(post_delete, sender=View) +def hanlde_view_deletion(sender, **kwargs): + """Receive signal when a View is deleted and delete its thumbnail image from disk. + + Parameters + ---------- + sender: `object` + class of the sender, in this case 'View' + kwargs: `dict` + arguments dictionary sent with the signal. It contains the key 'instance' with the View instance + that was deleted + """ + deleted_view = kwargs['instance'] + file_url = settings.MEDIA_BASE + deleted_view.thumbnail.url + try: + os.remove(file_url) + except FileNotFoundError: + pass diff --git a/manager/ui_framework/tests/__init__.py b/manager/ui_framework/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/manager/ui_framework/tests/media/mock/test b/manager/ui_framework/tests/media/mock/test new file mode 100644 index 00000000..336e0e2b --- /dev/null +++ b/manager/ui_framework/tests/media/mock/test @@ -0,0 +1 @@ +data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg== \ No newline at end of file diff --git a/manager/ui_framework/tests/media/mock/test.png b/manager/ui_framework/tests/media/mock/test.png new file mode 100644 index 00000000..8e9b046e Binary files /dev/null and b/manager/ui_framework/tests/media/mock/test.png differ diff --git a/manager/ui_framework/tests/media/thumbnails/.gitinclude b/manager/ui_framework/tests/media/thumbnails/.gitinclude new file mode 100644 index 00000000..e69de29b diff --git a/manager/ui_framework/tests/test_view_thumbnail.py b/manager/ui_framework/tests/test_view_thumbnail.py new file mode 100644 index 00000000..9bcfe8f5 --- /dev/null +++ b/manager/ui_framework/tests/test_view_thumbnail.py @@ -0,0 +1,123 @@ +"""Test the UI Framework thumbnail behavior.""" +import pytest +from manager import settings +from django.contrib.auth.models import User, Permission +from django.urls import reverse +from django.test import TestCase, override_settings +from rest_framework import status +from rest_framework.test import APIClient +from api.models import Token +from ui_framework.models import View +import os +import glob +import filecmp + + +@override_settings(DEBUG=True) +class ViewThumbnailTestCase(TestCase): + """Thumbnail files are created and managed properly.""" + + def setUp(self): + """Creates user/client for requests.""" + # 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.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')) + + # delete existing test thumbnails + thumbnail_files_list = glob.glob(settings.MEDIA_ROOT + '/thumbnails/*') + for file in thumbnail_files_list: + os.remove(file) + + def test_new_view(self): + """ Test thumbnail behavior when adding a new view """ + # Arrange + # read test data (base64 string) + old_count = View.objects.count() + 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 + } + # Act 1 + # send POST request with data + request_url = reverse('view-list') + response = self.client.post(request_url, request_data, format='json') + + # Assert + # - response status code 201 + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + # - new object was created + new_count = View.objects.count() + self.assertEqual(old_count + 1, new_count) + + # - thumbnail url + view = View.objects.get(name="view name") + 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'}, + } + 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}') + + 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') + with open(mock_location) as f: + image_data = f.read() + + request_data = { + "name": "view name", + "data": {"key1": "value1"}, + "thumbnail": image_data + } + request_url = reverse('view-list') + response = 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})) + + # Assert 2 + + # - response status code + self.assertEqual(delete_response.status_code, status.HTTP_204_NO_CONTENT) + + # - file does not exist + file_url = settings.MEDIA_BASE + view.thumbnail.url + with pytest.raises(FileNotFoundError): + f = open(file_url, 'r') + f.close() + + # - getting the file gives 404 + 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 new file mode 100644 index 00000000..e6d28014 --- /dev/null +++ b/manager/ui_framework/tests/tests_api.py @@ -0,0 +1,325 @@ +"""Test the UI Framework REST API.""" +from django.contrib.auth.models import User, Permission +from django.urls import reverse +from rest_framework import status +from api.models import Token +from ui_framework.tests.utils import get_dict, BaseTestCase + + +class UnauthenticatedCrudTestCase(BaseTestCase): + """Test that unauthenticated users cannot use the CRUD API.""" + + def setUp(self): + """Set testcase. Inherits from utils.BaseTestCase.""" + # Arrange + super().setUp() + + 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'])) + 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']) + ) + + 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']) + # Assert + self.assertEqual( + 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']) + ) + + 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}) + 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']) + ) + + 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() + old_data = get_dict(obj) + 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']) + ) + 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']) + ) + + 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}) + 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']) + ) + self.assertEqual( + case['class'].objects.count(), case['old_count'], + 'The number of {} should not have changed'.format(case['class']) + ) + + +class UnauthorizedCrudTestCase(BaseTestCase): + """Test that unauthorized users cannot use the CRUD API.""" + + def setUp(self): + """Set testcase. Inherits from utils.BaseTestCase.""" + # Arrange + super().setUp() + 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', + ) + 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) + + 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'])) + 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']) + ) + 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']) + ) + + 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']) + # Assert + self.assertEqual( + 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']) + ) + + 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}) + 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']) + ) + retrieved_data = dict(response.data) + self.assertEqual( + 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']) + old_data = get_dict(obj) + 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']) + ) + 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']) + ) + + 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}) + 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']) + ) + self.assertEqual( + case['class'].objects.count(), case['old_count'], + 'The number of {} should not have changed'.format(case['class']) + ) + + +class AuthorizedCrudTestCase(BaseTestCase): + """Test that authorized users can use the CRUD API.""" + + def setUp(self): + """Set testcase. Inherits from utils.BaseTestCase.""" + # Arrange + super().setUp() + 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', + ) + 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) + + 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']))) + # Act + 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']) + ) + retrieved_data = [dict(data) for data in response.data] + expected_data = case['current_data'] + self.assertEqual( + 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']))) + # Act + 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']) + ) + self.assertEqual( + 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']))) + # Act + 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']) + ) + retrieved_data = dict(response.data) + expected_data = case['current_data'][0] + self.assertEqual( + 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']))) + # Act + 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']) + # Assert + self.assertEqual( + 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)) + self.assertNotEqual( + 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']))) + # Act + 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']) + ) + self.assertEqual( + 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 new file mode 100644 index 00000000..e96b1eae --- /dev/null +++ b/manager/ui_framework/tests/tests_custom_api.py @@ -0,0 +1,78 @@ +"""Test the UI Framework Custom API.""" +from django.conf import settings +from django.contrib.auth.models import User, Permission +from django.urls import reverse +from rest_framework import status +from api.models import Token +from ui_framework.models import View +from ui_framework.tests.utils import BaseTestCase + + +class AuthorizedCrudTestCase(BaseTestCase): + """Test that authorized users can use the CRUD API.""" + + def setUp(self): + """Set testcase. Inherits from utils.BaseTestCase.""" + # Arrange + super().setUp() + 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', + ) + 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) + + 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')) + 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)]]} + for w in self.workspaces_data + ] + # Act + 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' + ) + retrieved_data = [dict(data) for data in response.data] + self.assertEqual( + 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')) + w = self.workspaces_data[0] + expected_data = {**w, 'views': self.views_data[0:2]} + + # Act + 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' + ) + retrieved_data = dict(response.data) + self.assertEqual( + 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 new file mode 100644 index 00000000..e26dca8b --- /dev/null +++ b/manager/ui_framework/tests/tests_models.py @@ -0,0 +1,413 @@ +"""Test the models.""" +from django.test import TestCase +from django.utils import timezone +from freezegun import freeze_time +from ui_framework.models import Workspace, View, WorkspaceView + + +class WorkspaceModelTestCase(TestCase): + """Test the workspace model.""" + + def setUp(self): + """Testcase setup.""" + # Arrange + self.workspace_name = 'My Workspace' + self.old_workspaces_num = Workspace.objects.count() + self.creation_timestamp = timezone.now() + with freeze_time(self.creation_timestamp): + self.workspace = Workspace.objects.create(name=self.workspace_name) + + def test_create_workspace(self): + """Test that a workspace can be created in the database.""" + # 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.assertEqual( + 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.assertEqual( + self.workspace.update_timestamp, self.creation_timestamp, + 'The update_timestamp is not as expected' + ) + + def test_retrieve_workspace(self): + """Test that a workspace can be retrieved from the database.""" + # Arrange: + self.old_workspaces_num = Workspace.objects.count() + + # Act + self.workspace = Workspace.objects.get(pk=self.workspace.pk) + + # 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.assertEqual( + 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.assertEqual( + self.workspace.update_timestamp, self.creation_timestamp, + 'The update_timestamp is not as expected' + ) + + def test_update_workspace(self): + """Test that a workspace can be updated in the database.""" + # Arrange: + self.old_workspaces_num = Workspace.objects.count() + + # Act + self.update_timestamp = timezone.now() + with freeze_time(self.update_timestamp): + 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.assertEqual( + 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.assertEqual( + 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' + ) + + def test_delete_workspace(self): + """Test that a workspace can be deleted from the database.""" + # Arrange: + self.old_workspaces_num = Workspace.objects.count() + + # Act + self.workspace = Workspace.objects.get(pk=self.workspace.pk) + self.workspace_pk = self.workspace.pk + self.workspace.delete() + + # 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' + ) + with self.assertRaises(Exception): + Workspace.objects.get(pk=self.workspace_pk) + + +class ViewModelTestCase(TestCase): + """Test the view model.""" + + def setUp(self): + """Testcase setup.""" + # Arrange + self.view_name = 'My View' + self.old_views_num = View.objects.count() + self.creation_timestamp = timezone.now() + with freeze_time(self.creation_timestamp): + self.view = View.objects.create(name=self.view_name) + + def test_create_view(self): + """Test that a view can be created in the database.""" + # 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.assertEqual( + self.view.name, self.view_name, + 'The name 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' + ) + + def test_retrieve_view(self): + """Test that a view can be retrieved from the database.""" + # Arrange: + self.old_views_num = View.objects.count() + + # Act + self.view = View.objects.get(pk=self.view.pk) + + # 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.assertEqual( + self.view.name, self.view_name, + 'The name 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' + ) + + def test_update_view(self): + """Test that a view can be updated in the database.""" + # Arrange: + self.old_views_num = View.objects.count() + + # Act + self.update_timestamp = timezone.now() + with freeze_time(self.update_timestamp): + 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.assertEqual( + 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.assertEqual( + 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' + ) + + def test_delete_view(self): + """Test that a view can be deleted from the database.""" + # Arrange: + self.old_views_num = View.objects.count() + + # Act + self.view = View.objects.get(pk=self.view.pk) + self.view_pk = self.view.pk + self.view.delete() + + # 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' + ) + with self.assertRaises(Exception): + Workspace.objects.get(pk=self.view_pk) + + +class WorkspaceViewModelTestCase(TestCase): + """Test the workspace_view model.""" + + def setUp(self): + """Testcase setup.""" + # Arrange + 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): + self.workspace = Workspace.objects.create(name=self.workspace_name) + self.view = View.objects.create(name=self.view_name) + self.workspace_view = WorkspaceView.objects.create( + view_name=self.workspace_view_name, + workspace=self.workspace, + view=self.view + ) + + def test_create_workspace_view(self): + """Test that a workspace_view can be created in the database.""" + # 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.assertEqual( + 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.assertEqual( + self.workspace_view.update_timestamp, self.creation_timestamp, + 'The update_timestamp is not as expected' + ) + + def test_retrieve_workspace_view(self): + """Test that a workspace_view can be retrieved from the database.""" + # Arrange: + self.old_workspace_views_num = WorkspaceView.objects.count() + + # Act + self.workspace_view = WorkspaceView.objects.get(pk=self.workspace_view.pk) + + # 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.assertEqual( + 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.assertEqual( + self.workspace_view.update_timestamp, self.creation_timestamp, + 'The update_timestamp is not as expected' + ) + + def test_update_workspace_view(self): + """Test that a workspace_view can be updated in the database.""" + # Arrange: + self.old_workspace_views_num = WorkspaceView.objects.count() + self.new_sort_value = self.workspace_view.sort_value + 1 + # Act + self.update_timestamp = timezone.now() + with freeze_time(self.update_timestamp): + self.workspace_view.view_name = 'This other name' + self.workspace_view.sort_value = self.new_sort_value + self.workspace_view.save() + + # Assert + 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.assertEqual( + 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.assertEqual( + 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.assertNotEqual( + 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): + """Test that a workspace_view can be deleted from the database.""" + # Arrange: + self.old_workspace_views_num = WorkspaceView.objects.count() + + # Act + self.workspace_view = WorkspaceView.objects.get(pk=self.workspace_view.pk) + self.workspace_view_pk = self.workspace_view.pk + self.workspace_view.delete() + + # 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' + ) + with self.assertRaises(Exception): + Workspace.objects.get(pk=self.workspace_view_pk) + + +class WorkspaceAndViewsRelationsTestCase(TestCase): + """Test the relationships vetween Workspace, View and WorkspaceView models.""" + + def setUp(self): + """Testcase setup.""" + # Arrange + 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'), + ] + self.views = [ + 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): + """Test that Views can be added/retrieved to/from a Workspace in the DB.""" + # Act + # - Add + sorted_views = [self.views[0], self.views[2], self.views[1]] + for view in sorted_views: + self.workspaces[0].views.add(view) + # - Get + workspace = Workspace.objects.get(pk=self.workspaces[0].pk) + 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') + + def test_get_workspaces_from_a_view(self): + """Test that a View can retrieve its Workspaces from the DB.""" + # Arrange + view = self.views[0] + workspaces = [ + self.workspaces[2], + self.workspaces[0], + ] + for workspace in workspaces: + workspace.views.add(view) + # Act + retrieved_workspaces = set(view.workspaces.all()) + # Assert + 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 new file mode 100644 index 00000000..0f4a336f --- /dev/null +++ b/manager/ui_framework/tests/utils.py @@ -0,0 +1,163 @@ +"""Utilities for testing purposes.""" +import json +from django.conf import settings +from django.test import TestCase +from django.utils import timezone +from freezegun import freeze_time +from rest_framework import serializers +from rest_framework.test import APIClient +from ui_framework.models import Workspace, View, WorkspaceView + + +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, + } + 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, + } + 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, + } + + +class BaseTestCase(TestCase): + """Base TestCase used to define a common setUp.""" + + def setUp(self): + """Set the base testcase. + + We start with 3 workspaces and 4 views and we add view_i and view_i+1 to workspace_i + """ + # Arrange + # Populate the Database + self.setup_ts = timezone.now() + self.setup_ts_str = serializers.DateTimeField().to_representation(self.setup_ts) + with freeze_time(self.setup_ts): + # 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 1', + 'data': json.dumps({"data_name": "My View 1"}), + }, + { + 'name': 'My View 2', + 'data': json.dumps({"data_name": "My View 2"}), + }, + { + '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'}, + ] + self.views = [] + self.workspaces = [] + self.workspace_views_data = [] + 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.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.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, + }) + # 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, + }) + + # Client to test the API + self.client = APIClient() + + # Models to test: + self.cases = [ + { + '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'], + }, + { + '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'], + }, + { + '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'], + }, + '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 new file mode 100644 index 00000000..7702c2f4 --- /dev/null +++ b/manager/ui_framework/urls.py @@ -0,0 +1,25 @@ +"""API app URL Configuration. + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/2.2/topics/http/urls/ + +Examples +-------- +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from rest_framework.routers import DefaultRouter +from ui_framework.views import WorkspaceViewSet, ViewViewSet, WorkspaceViewViewSet + +router = DefaultRouter() +router.register('workspaces', WorkspaceViewSet) +router.register('views', ViewViewSet) +router.register('workspaceviews', WorkspaceViewViewSet) +urlpatterns = router.urls diff --git a/manager/ui_framework/views.py b/manager/ui_framework/views.py new file mode 100644 index 00000000..658c059f --- /dev/null +++ b/manager/ui_framework/views.py @@ -0,0 +1,142 @@ +"""Defines the views exposed by the REST API exposed by this app.""" +from drf_yasg import openapi +from drf_yasg.utils import swagger_auto_schema +from rest_framework import viewsets, status +from rest_framework.decorators import action +from rest_framework.response import Response +from ui_framework.models import Workspace, View, WorkspaceView +from ui_framework.serializers import ( + WorkspaceSerializer, + ViewSerializer, + ViewSummarySerializer, + WorkspaceViewSerializer, + WorkspaceFullSerializer, + WorkspaceWithViewNameSerializer, +) + + +class WorkspaceViewSet(viewsets.ModelViewSet): + """GET, POST, PUT, PATCH or DELETE instances the Workspace model.""" + + queryset = Workspace.objects.all() + """Set of objects to be accessed by queries to this viewsets endpoints""" + + serializer_class = WorkspaceSerializer + """Serializer used to serialize Workspace objects""" + + @swagger_auto_schema( + method='get', + responses={200: openapi.Response('Responsee', WorkspaceFullSerializer)}) + @action(detail=True) + def full(self, request, pk=None): + """Serialize a Workspace including the view's fully subserialized. + + Params + ------ + request: Request + The Requets object + pk: int + The corresponding Workspace pk + + Returns + ------- + Response + The response containing the serialized Workspaces, with the views fully subserialized + """ + try: + workspace = Workspace.objects.get(pk=pk) + except Workspace.DoesNotExist: + return Response(status=status.HTTP_404_NOT_FOUND) + + serializer = WorkspaceFullSerializer(workspace) + return Response(serializer.data) + + @swagger_auto_schema( + method='get', + responses={200: openapi.Response('Responsee', WorkspaceWithViewNameSerializer)}) + @action(detail=False) + def with_view_name(self, request): + """Serialize Workspaces including the view's names in the views response. + + Params + ------ + request: Request + The Requets object + + Returns + ------- + Response + The response containing the serialized Workspaces, + but returning a list of dicts with each view's id and name + """ + workspaces = Workspace.objects.all() + serializer = WorkspaceWithViewNameSerializer(workspaces, many=True) + return Response(serializer.data) + + +class ViewViewSet(viewsets.ModelViewSet): + """GET, POST, PUT, PATCH or DELETE instances the View model.""" + + queryset = View.objects.order_by('-update_timestamp').all() + """Set of objects to be accessed by queries to this viewsets endpoints""" + + serializer_class = ViewSerializer + """Serializer used to serialize View objects""" + + @swagger_auto_schema( + method='get', + responses={200: openapi.Response('Responsee', ViewSerializer)}) + @action(detail=False) + def search(self, request): + """Serialize Views containing the query string. + + Params + ------ + request: Request + The Requets object + + Returns + ------- + Response + The response containing the serialized Views. + """ + + views = View.objects.order_by('-update_timestamp').all() + query = self.request.query_params.get('query', None) + if query is not None: + views = views.filter(name__icontains=query) + + serializer = ViewSerializer(views, many=True) + return Response(serializer.data) + + @swagger_auto_schema( + method='get', + responses={200: openapi.Response('Responsee', ViewSerializer)}) + @action(detail=False) + def summary(self, request): + """Serialize Views containing the query string. + + Params + ------ + request: Request + The Requets object + + Returns + ------- + Response + The response containing the serialized Views. + """ + + views = View.objects.order_by('-update_timestamp').all() + serializer = ViewSummarySerializer(views, many=True) + return Response(serializer.data) + + +class WorkspaceViewViewSet(viewsets.ModelViewSet): + """GET, POST, PUT, PATCH or DELETE instances the WorkspaceView model.""" + + queryset = WorkspaceView.objects.all() + """Set of objects to be accessed by queries to this viewsets endpoints""" + + serializer_class = WorkspaceViewSerializer + """Serializer used to serialize View objects"""