Exemple #1
0
def validate_request(limit=None,
                     offset=None,
                     perpage=None,
                     page=None,
                     query_type=None,
                     is_querystring_defined=False):
    """
    Performs various checks on the consistency of the request.
    Add here all the checks that you want to do, except validity of the page
    number that is done in paginate().
    Future additional checks must be added here
    """

    # TODO Consider using **kwargs so to make easier to add more validations
    # 1. perpage incompatible with offset and limits
    if perpage is not None and (limit is not None or offset is not None):
        raise RestValidationError("perpage key is incompatible with "
                                  "limit and offset")
    # 2. /page/<int: page> in path is incompatible with limit and offset
    if page is not None and (limit is not None or offset is not None):
        raise RestValidationError("requesting a specific page is "
                                  "incompatible "
                                  "with limit and offset")
    # 3. perpage requires that the path contains a page request
    if perpage is not None and page is None:
        raise RestValidationError("perpage key requires that a page is "
                                  "requested (i.e. the path must contain "
                                  "/page/)")
    # 4. if resource_type=='schema'
    if query_type == 'schema' and is_querystring_defined:
        raise RestInputValidationError("schema requests do not allow "
                                       "specifying a query string")
Exemple #2
0
    def validate_request(
        self, limit=None, offset=None, perpage=None, page=None, query_type=None, is_querystring_defined=False
    ):
        # pylint: disable=fixme,no-self-use,too-many-arguments,too-many-branches
        """
        Performs various checks on the consistency of the request.
        Add here all the checks that you want to do, except validity of the page
        number that is done in paginate().
        Future additional checks must be added here
        """

        # TODO Consider using **kwargs so to make easier to add more validations
        # 1. perpage incompatible with offset and limits
        if perpage is not None and (limit is not None or offset is not None):
            raise RestValidationError('perpage key is incompatible with limit and offset')
        # 2. /page/<int: page> in path is incompatible with limit and offset
        if page is not None and (limit is not None or offset is not None):
            raise RestValidationError('requesting a specific page is incompatible with limit and offset')
        # 3. perpage requires that the path contains a page request
        if perpage is not None and page is None:
            raise RestValidationError(
                'perpage key requires that a page is '
                'requested (i.e. the path must contain '
                '/page/)'
            )
        # 4. No querystring if query type = projectable_properties'
        if query_type in ('projectable_properties',) and is_querystring_defined:
            raise RestInputValidationError('projectable_properties requests do not allow specifying a query string')
Exemple #3
0
    def _check_id_validity(self, node_id):
        """
        Checks whether id corresponds to an object of the expected type,
        whenever type is a valid column of the database (ex. for nodes,
        but not for users)

        :param node_id: id (or id starting pattern)

        :return: True if node_id valid, False if invalid. If True, sets the id
          filter attribute correctly

        :raise RestValidationError: if no node is found or id pattern does
          not identify a unique node
        """
        from aiida.common.exceptions import MultipleObjectsError, NotExistent
        from aiida.orm.utils.loaders import IdentifierType, get_loader

        loader = get_loader(self._aiida_class)

        if self._has_uuid:

            # For consistency check that id is a string
            if not isinstance(node_id, six.string_types):
                raise RestValidationError('parameter id has to be a string')

            identifier_type = IdentifierType.UUID
            qbobj, _ = loader.get_query_builder(
                node_id, identifier_type, sub_classes=(self._aiida_class, ))
        else:

            # Similarly, check that id is an integer
            if not isinstance(node_id, int):
                raise RestValidationError('parameter id has to be an integer')

            identifier_type = IdentifierType.ID
            qbobj, _ = loader.get_query_builder(
                node_id, identifier_type, sub_classes=(self._aiida_class, ))

        # For efficiency I don't go further than two results
        qbobj.limit(2)

        try:
            pk = qbobj.one()[0].pk
        except MultipleObjectsError:
            raise RestInputValidationError(
                'More than one node found. Provide longer starting pattern for id.'
            )
        except NotExistent:
            raise RestInputValidationError(
                "either no object's id starts"
                " with '{}' or the corresponding object"
                ' is not of type aiida.orm.{}'.format(node_id,
                                                      self._aiida_type))
        else:
            # create a permanent filter
            self._id_filter = {'id': {'==': pk}}
            return True
Exemple #4
0
    def _check_id_validity(self, id):
        """
        Checks whether a id full id or id starting pattern) corresponds to
         an object of the expected type,
        whenever type is a valid column of the database (ex. for nodes,
        but not for users)
        
        :param id: id, or id starting pattern
        
        :return: True if id valid (invalid). If True, sets the
            id filter attribute correctly
            
        :raise: RestValidationError if No node is found or id pattern does
        not identify a unique node
        """
        from aiida.common.exceptions import MultipleObjectsError, NotExistent

        from aiida.orm.utils import create_node_id_qb

        if self._has_uuid:

            # For consistency check that tid is a string
            if not isinstance(id, (str, unicode)):
                raise RestValidationError('parameter id has to be an string')

            qb = create_node_id_qb(uuid=id, parent_class=self._aiida_class)
        else:

            # Similarly, check that id is an integer
            if not isinstance(id, int):
                raise RestValidationError('parameter id has to be an integer')

            qb = create_node_id_qb(pk=id, parent_class=self._aiida_class)

        # project only the pk
        qb.add_projection('node', ['id'])
        # for efficiency i don;t go further than two results
        qb.limit(2)

        try:
            pk = qb.one()[0]
        except MultipleObjectsError:
            raise RestValidationError("More than one node found."
                                      " Provide longer starting pattern"
                                      " for id.")
        except NotExistent:
            raise RestValidationError("either no object's id starts"
                                      " with '{}' or the corresponding object"
                                      " is not of type aiida.orm.{}".format(
                                          id, self._aiida_type))
        else:
            # create a permanent filter
            self._id_filter = {'id': {'==': pk}}
            return True
Exemple #5
0
    def set_query(self, filters=None, orders=None, projections=None, id=None):
        """
        Adds filters, default projections, order specs to the query_help,
        and initializes the qb object

        :param filters: dictionary with the filters
        :param orders: dictionary with the order for each tag
        :param orders: dictionary with the projections
        :param id: id of a specific node
        :type id: int
        """

        tagged_filters = {}

        ## Check if filters are well defined and construct an ad-hoc filter
        # for id_query
        if id is not None:
            self._is_id_query = True
            if self._result_type == self.__label__ and len(filters) > 0:
                raise RestInputValidationError("selecting a specific id does "
                                               "not "
                                               "allow to specify filters")

            try:
                self._check_id_validity(id)
            except RestValidationError as e:
                raise RestValidationError(e.message)
            else:
                tagged_filters[self.__label__] = self._id_filter
                if self._result_type is not self.__label__:
                    tagged_filters[self._result_type] = filters
        else:
            tagged_filters[self.__label__] = filters

        ## Add filters
        self.set_filters(tagged_filters)

        ## Add projections
        if projections is None:
            self.set_default_projections()
        else:
            tagged_projections = {self._result_type: projections}
            self.set_projections(tagged_projections)

        ##Add order_by
        if orders is not None:
            tagged_orders = {self._result_type: orders}
            self.set_order(tagged_orders)

        ## Initialize the query_object
        self.init_qb()
Exemple #6
0
    def set_query(self, filters=None, orders=None, projections=None, pk=None):
        """
        Adds filters, default projections, order specs to the query_help,
        and initializes the qb object

        :param filters: dictionary with the filters
        :param orders: dictionary with the order for each tag
        :param pk (integer): pk of a specific node
        """

        tagged_filters = {}

        ## Check if filters are well defined and construct an ad-hoc filter
        # for pk_query
        if pk is not None:
            self._is_pk_query = True
            if self._result_type == self.__label__ and len(filters) > 0:
                raise RestInputValidationError("selecting a specific pk does "
                                               "not "
                                               "allow to specify filters")
            elif not self._check_pk_validity(pk):
                raise RestValidationError(
                    "either the selected pk does not exist "
                    "or the corresponding object is not of "
                    "type aiida.orm.{}".format(self._aiida_type))
            else:
                tagged_filters[self.__label__] = {'id': {'==': pk}}
                if self._result_type is not self.__label__:
                    tagged_filters[self._result_type] = filters
        else:
            tagged_filters[self.__label__] = filters

        ## Add filters
        self.set_filters(tagged_filters)

        ## Add projections
        if projections is None:
            self.set_default_projections()
        else:
            tagged_projections = {self._result_type: projections}
            self.set_projections(tagged_projections)

        ##Add order_by
        if orders is not None:
            tagged_orders = {self._result_type: orders}
            self.set_order(tagged_orders)

        ## Initialize the query_object
        self.init_qb()
Exemple #7
0
    def get_repo_contents(node, filename=''):
        """
        Every node in AiiDA is having repo folder.
        This function returns the metadata using get_object() method
        :param node: node object
        :param filename: folder or file name (optional)
        :return: file content in bytes to download
        """

        if filename:
            try:
                data = node.get_object_content(filename, mode='rb')
                return data
            except IOError:
                raise RestInputValidationError('No such file is present')
        raise RestValidationError('filename is not provided')
Exemple #8
0
    def build_datetime_filter(dtobj):
        """
        This function constructs a filter for a datetime object to be in a
        certain datetime interval according to the precision.

        The interval is [reference_datetime, reference_datetime + delta_time],
        where delta_time is a function fo the required precision.

        This function should be used to replace a datetime filter based on
        the equality operator that is inehrently "picky" because it implies
        matching two datetime objects down to the microsecond or something,
        by a "tolerant" operator which checks whether the datetime is in an
        interval.

        :return: a suitable entry of the filter dictionary
        """

        if not isinstance(dtobj, DatetimePrecision):
            TypeError('dtobj argument has to be a DatetimePrecision object')

        reference_datetime = dtobj.dtobj
        precision = dtobj.precision

        ## Define interval according to the precision
        if precision == 1:
            delta_time = timedelta(days=1)
        elif precision == 2:
            delta_time = timedelta(hours=1)
        elif precision == 3:
            delta_time = timedelta(minutes=1)
        elif precision == 4:
            delta_time = timedelta(seconds=1)
        else:
            raise RestValidationError('The datetime resolution is not valid.')

        filters = {
            'and': [{
                '>=': reference_datetime
            }, {
                '<': reference_datetime + delta_time
            }]
        }

        return filters
Exemple #9
0
    def set_limit_offset(self, limit=None, offset=None):
        """
        sets limits and offset directly to the query_builder object

        :param limit:
        :param offset:
        :return:
        """

        ## mandatory params
        # none

        ## non-mandatory params
        if limit is not None:
            try:
                limit = int(limit)
            except ValueError:
                raise InputValidationError('Limit value must be an integer')
            if limit > self.limit_default:
                raise RestValidationError(
                    'Limit and perpage cannot be bigger than {}'.format(
                        self.limit_default))
        else:
            limit = self.limit_default

        if offset is not None:
            try:
                offset = int(offset)
            except ValueError:
                raise InputValidationError('Offset value must be an integer')

        if self._is_qb_initialized:
            if limit is not None:
                self.qbobj.limit(limit)
            else:
                pass
            if offset is not None:
                self.qbobj.offset(offset)
            else:
                pass
        else:
            raise InvalidOperation(
                'query builder object has not been initialized.')
Exemple #10
0
    def set_limit_offset(self, limit=None, offset=None):
        """
        sets limits and offset directly to the query_builder object

        :param limit:
        :param offset:
        :return:
        """

        ## mandatory params
        # none

        ## non-mandatory params
        if limit is not None:
            try:
                limit = int(limit)
            except ValueError:
                raise InputValidationError("Limit value must be an integer")
            if limit > self.LIMIT_DEFAULT:
                raise RestValidationError("Limit and perpage cannot be bigger "
                                          "than {}".format(self.LIMIT_DEFAULT))
        else:
            limit = self.LIMIT_DEFAULT

        if offset is not None:
            try:
                offset = int(offset)
            except ValueError:
                raise InputValidationError("Offset value must be an "
                                           "integer")

        if self._is_qb_initialized:
            if limit is not None:
                self.qb.limit(limit)
            else:
                pass
            if offset is not None:
                self.qb.offset(offset)
            else:
                pass
        else:
            raise InvalidOperation("query builder object has not been "
                                   "initialized.")
Exemple #11
0
    def _get_content(self):
        """
        Used by get_results() in case of endpoint include "content" option
        :return: data: a dictionary containing the results obtained by
        running the query
        """
        if not self._is_qb_initialized:
            raise InvalidOperation("query builder object has not been "
                                   "initialized.")

        ## Initialization
        data = {}

        ## Count the total number of rows returned by the query (if not
        # already done)
        if self._total_count is None:
            self.count()

        if self._total_count > 0:
            n = self.qb.first()[0]
            if self._content_type == "attributes":
                # Get all attrs if nalist and alist are both None
                if self._alist is None and self._nalist is None:
                    data[self._content_type] = n.get_attrs()
                # Get all attrs except those contained in nalist
                elif self._alist is None and self._nalist is not None:
                    attrs = {}
                    for key in n.get_attrs().keys():
                        if key not in self._nalist:
                            attrs[key] = n.get_attr(key)
                    data[self._content_type] = attrs
                # Get all attrs contained in alist
                elif self._alist is not None and self._nalist is None:
                    attrs = {}
                    for key in n.get_attrs().keys():
                        if key in self._alist:
                            attrs[key] = n.get_attr(key)
                    data[self._content_type] = attrs
                else:
                    raise RestValidationError("you cannot specify both alist "
                                              "and nalist")
            elif self._content_type == "extras":
                # Get all extras if nelist and elist are both None
                if self._elist is None and self._nelist is None:
                    data[self._content_type] = n.get_extras()
                # Get all extras except those contained in nelist
                elif self._elist is None and self._nelist is not None:
                    extras = {}
                    for key in n.get_extras().keys():
                        if key not in self._nelist:
                            extras[key] = n.get_extra(key)
                    data[self._content_type] = extras
                # Get all extras contained in elist
                elif self._elist is not None and self._nelist is None:
                    extras = {}
                    for key in n.get_extras().keys():
                        if key in self._elist:
                            extras[key] = n.get_extra(key)
                    data[self._content_type] = extras
                else:
                    raise RestValidationError("you cannot specify both elist "
                                              "and nelist")
            else:
                raise ValidationError("invalid content type")
                # Default case
        else:
            pass

        return data
Exemple #12
0
    def set_query(self,
                  filters=None,
                  orders=None,
                  projections=None,
                  query_type=None,
                  node_id=None,
                  attributes=None,
                  attributes_filter=None,
                  extras=None,
                  extras_filter=None):
        # pylint: disable=too-many-arguments,unused-argument,too-many-locals,too-many-branches
        """
        Adds filters, default projections, order specs to the query_help,
        and initializes the qb object

        :param filters: dictionary with the filters
        :param orders: dictionary with the order for each tag
        :param projections: dictionary with the projection. It is discarded
            if query_type=='attributes'/'extras'
        :param query_type: (string) specify the result or the content ("attr")
        :param id: (integer) id of a specific node
        :param filename: name of the file to return its content
        :param attributes: flag to show attributes in nodes endpoint
        :param attributes_filter: list of node attributes to query
        :param extras: flag to show attributes in nodes endpoint
        :param extras_filter: list of node extras to query
        """

        tagged_filters = {}

        ## Check if filters are well defined and construct an ad-hoc filter
        # for id_query
        if node_id is not None:
            self._is_id_query = True
            if self._result_type == self.__label__ and filters:
                raise RestInputValidationError(
                    'selecting a specific id does not allow to specify filters'
                )

            try:
                self._check_id_validity(node_id)
            except RestValidationError as exc:
                raise RestValidationError(str(exc))
            else:
                tagged_filters[self.__label__] = self._id_filter
                if self._result_type is not self.__label__:
                    tagged_filters[self._result_type] = filters
        else:
            tagged_filters[self.__label__] = filters

        ## Add filters
        self.set_filters(tagged_filters)

        ## Add projections
        if projections is None:
            if attributes is None and extras is None:
                self.set_default_projections()
            else:
                default_projections = self.get_default_projections()

                if attributes is True:
                    if attributes_filter is None:
                        default_projections.append('attributes')
                    else:
                        ## Check if attributes_filter is not a list
                        if not isinstance(attributes_filter, list):
                            attributes_filter = [attributes_filter]
                        for attr in attributes_filter:
                            default_projections.append('attributes.' +
                                                       str(attr))
                elif attributes is not None and attributes is not False:
                    raise RestValidationError(
                        'The attributes filter is false by default and can only be set to true.'
                    )

                if extras is True:
                    if extras_filter is None:
                        default_projections.append('extras')
                    else:
                        ## Check if extras_filter is not a list
                        if not isinstance(extras_filter, list):
                            extras_filter = [extras_filter]
                        for extra in extras_filter:
                            default_projections.append('extras.' + str(extra))
                elif extras is not None and extras is not False:
                    raise RestValidationError(
                        'The extras filter is false by default and can only be set to true.'
                    )

                self.set_projections({self.__label__: default_projections})
        else:
            tagged_projections = {self._result_type: projections}
            self.set_projections(tagged_projections)

        ##Add order_by
        if orders is not None:
            tagged_orders = {self._result_type: orders}
            self.set_order(tagged_orders)

        ## Initialize the query_object
        self.init_qb()
Exemple #13
0
    def _get_content(self):
        """
        Used by get_results() in case of endpoint include "content" option
        :return: data: a dictionary containing the results obtained by
        running the query
        """
        if not self._is_qb_initialized:
            raise InvalidOperation("query builder object has not been "
                                   "initialized.")

        ## Count the total number of rows returned by the query (if not
        # already done)
        if self._total_count is None:
            self.count()

        # If count==0 return
        if self._total_count == 0:
            return {}

        # otherwise ...
        n = self.qb.first()[0]

        # content/attributes
        if self._content_type == "attributes":
            # Get all attrs if nalist and alist are both None
            if self._alist is None and self._nalist is None:
                data = {self._content_type: n.get_attrs()}
            # Get all attrs except those contained in nalist
            elif self._alist is None and self._nalist is not None:
                attrs = {}
                for key in n.get_attrs().keys():
                    if key not in self._nalist:
                        attrs[key] = n.get_attr(key)
                data = {self._content_type: attrs}
            # Get all attrs contained in alist
            elif self._alist is not None and self._nalist is None:
                attrs = {}
                for key in n.get_attrs().keys():
                    if key in self._alist:
                        attrs[key] = n.get_attr(key)
                data = {self._content_type: attrs}
            else:
                raise RestValidationError("you cannot specify both alist "
                                          "and nalist")
        # content/extras
        elif self._content_type == "extras":

            # Get all extras if nelist and elist are both None
            if self._elist is None and self._nelist is None:
                data = {self._content_type: n.get_extras()}

            # Get all extras except those contained in nelist
            elif self._elist is None and self._nelist is not None:
                extras = {}
                for key in n.get_extras().keys():
                    if key not in self._nelist:
                        extras[key] = n.get_extra(key)
                data = {self._content_type: extras}

            # Get all extras contained in elist
            elif self._elist is not None and self._nelist is None:
                extras = {}
                for key in n.get_extras().keys():
                    if key in self._elist:
                        extras[key] = n.get_extra(key)
                data = {self._content_type: extras}

            else:
                raise RestValidationError("you cannot specify both elist "
                                          "and nelist")

        # Data needed for visualization appropriately serialized (this
        # actually works only for data derived classes)
        # TODO refactor the code so to have this option only in data and
        # derived classes
        elif self._content_type == 'visualization':
            # In this we do not return a dictionary but just an object and
            # the dictionary format is set by get_visualization_data
            data = {
                self._content_type:
                self.get_visualization_data(n, self._visformat)
            }

        elif self._content_type == 'download':
            # In this we do not return a dictionary but download the file in
            # specified format if available
            data = {
                self._content_type:
                self.get_downloadable_data(n, self._downloadformat)
            }

        elif self._content_type == 'retrieved_inputs':
            # This type is only available for calc nodes. In case of job calc it
            # returns calc inputs prepared to submit calc on the cluster else []
            data = {
                self._content_type:
                self.get_retrieved_inputs(n, self._filename, self._rtype)
            }

        elif self._content_type == 'retrieved_outputs':
            # This type is only available for calc nodes. In case of job calc it
            # returns calc outputs retrieved from the cluster else []
            data = {
                self._content_type:
                self.get_retrieved_outputs(n, self._filename, self._rtype)
            }

        else:
            raise ValidationError("invalid content type")

        return data
Exemple #14
0
    def get_visualization_data(node, supercell_factors=[1, 1, 1]):
        """

        Returns: data in a format required by chemdoodle to visualize a
        structure.

        """
        import numpy as np
        from itertools import product


        # Validate supercell factors
        if type(supercell_factors) is not list:
            raise RestValidationError('supercell factors have to be a list of three integers')

        for fac in supercell_factors:
            if type(fac) is not int:
                raise RestValidationError('supercell factors have to be '
                                          'integers')

        # Get cell vectors and atomic position
        lattice_vectors = np.array(node.get_attr('cell'))
        base_sites = node.get_attr('sites')

        start1 = -int(supercell_factors[0] / 2)
        start2 = -int(supercell_factors[1] / 2)
        start3 = -int(supercell_factors[2] / 2)

        stop1 = start1 + supercell_factors[0]
        stop2 = start2 + supercell_factors[1]
        stop3 = start3 + supercell_factors[2]

        grid1 = range(start1, stop1)
        grid2 = range(start2, stop2)
        grid3 = range(start3, stop3)

        atoms_json = []

        # Manual recenter of the structure
        center = (lattice_vectors[0] + lattice_vectors[1] +
                  lattice_vectors[2])/2.

        for ix, iy, iz in product(grid1, grid2, grid3):
            for base_site in base_sites:

                shift = (ix*lattice_vectors[0] + iy*lattice_vectors[1] + \
                iz*lattice_vectors[2] - center).tolist()

                kind_name = base_site['kind_name']
                kind_string = node.get_kind(kind_name).get_symbols_string()

                atoms_json.append(
                    {'l': kind_name,
                     'x': base_site['position'][0] + shift[0],
                     'y': base_site['position'][1] + shift[1],
                     'z': base_site['position'][2] + shift[2],
                     # 'atomic_elments_html': kind_string
                     'atomic_elments_html': atom_kinds_to_html(kind_string)
                    })

        cell_json = {
                "t": "UnitCell",
                "i": "s0",
                "o": (-center).tolist(),
                "x": (lattice_vectors[0]-center).tolist(),
                "y": (lattice_vectors[1]-center).tolist(),
                "z": (lattice_vectors[2]-center).tolist(),
                "xy": (lattice_vectors[0] + lattice_vectors[1]
                       - center).tolist(),
                "xz": (lattice_vectors[0] + lattice_vectors[2]
                       - center).tolist(),
                "yz": (lattice_vectors[1] + lattice_vectors[2]
                       - center).tolist(),
                "xyz": (lattice_vectors[0] + lattice_vectors[1]
                        + lattice_vectors[2] - center).tolist(),
            }

        # These will be passed to ChemDoodle
        json_content = {"s": [cell_json],
                        "m": [{"a": atoms_json}],
                        "units": '&Aring;'
                        }
        return json_content
Exemple #15
0
    def get_visualization_data(node, format=None, supercell_factors=[1, 1, 1]):
        """
        Returns: data in specified format. If format is not specified returns data
        in a format required by chemdoodle to visualize a structure.
        """
        response = {}
        response["str_viz_info"] = {}

        if format in node.get_export_formats():
            try:
                response["str_viz_info"]["data"] = node._exportstring(
                    format)[0]
                response["str_viz_info"]["format"] = format
            except LicensingException as e:
                response = e.message

        else:
            import numpy as np
            from itertools import product

            # Validate supercell factors
            if type(supercell_factors) is not list:
                raise RestValidationError(
                    'supercell factors have to be a list of three integers')

            for fac in supercell_factors:
                if type(fac) is not int:
                    raise RestValidationError('supercell factors have to be '
                                              'integers')

            # Get cell vectors and atomic position
            lattice_vectors = np.array(node.get_attr('cell'))
            base_sites = node.get_attr('sites')

            start1 = -int(supercell_factors[0] / 2)
            start2 = -int(supercell_factors[1] / 2)
            start3 = -int(supercell_factors[2] / 2)

            stop1 = start1 + supercell_factors[0]
            stop2 = start2 + supercell_factors[1]
            stop3 = start3 + supercell_factors[2]

            grid1 = range(start1, stop1)
            grid2 = range(start2, stop2)
            grid3 = range(start3, stop3)

            atoms_json = []

            # Manual recenter of the structure
            center = (lattice_vectors[0] + lattice_vectors[1] +
                      lattice_vectors[2]) / 2.

            for ix, iy, iz in product(grid1, grid2, grid3):
                for base_site in base_sites:

                    shift = (ix*lattice_vectors[0] + iy*lattice_vectors[1] + \
                    iz*lattice_vectors[2] - center).tolist()

                    kind_name = base_site['kind_name']
                    kind_string = node.get_kind(kind_name).get_symbols_string()

                    atoms_json.append({
                        'l':
                        kind_string,
                        'x':
                        base_site['position'][0] + shift[0],
                        'y':
                        base_site['position'][1] + shift[1],
                        'z':
                        base_site['position'][2] + shift[2],
                        # 'atomic_elements_html': kind_string
                        'atomic_elements_html':
                        atom_kinds_to_html(kind_string)
                    })

            cell_json = {
                "t":
                "UnitCell",
                "i":
                "s0",
                "o": (-center).tolist(),
                "x": (lattice_vectors[0] - center).tolist(),
                "y": (lattice_vectors[1] - center).tolist(),
                "z": (lattice_vectors[2] - center).tolist(),
                "xy":
                (lattice_vectors[0] + lattice_vectors[1] - center).tolist(),
                "xz":
                (lattice_vectors[0] + lattice_vectors[2] - center).tolist(),
                "yz":
                (lattice_vectors[1] + lattice_vectors[2] - center).tolist(),
                "xyz": (lattice_vectors[0] + lattice_vectors[1] +
                        lattice_vectors[2] - center).tolist(),
            }

            # These will be passed to ChemDoodle
            response["str_viz_info"]["data"] = {
                "s": [cell_json],
                "m": [{
                    "a": atoms_json
                }],
                "units": '&Aring;'
            }
            response["str_viz_info"]["format"] = "default (ChemDoodle)"

        # Add extra information
        response["dimensionality"] = node.get_dimensionality()
        response["pbc"] = node.pbc
        response["formula"] = node.get_formula()

        return response