Source code for pyoko.listnode

# -*-  coding: utf-8 -*-
"""
This module holds the ListNode implementation of Pyoko Models.

ListNode's are used to model ManyToMany relations and other
list like data types on a Model.
"""

# Copyright (C) 2015 ZetaOps Inc.
#
# This file is licensed under the GNU General Public License v3
# (GPLv3).  See LICENSE.txt for details.
import six

from .node import Node
from .lib.utils import un_camel, un_camel_id, lazy_property


[docs]class ListNode(Node): """ ListNode's are used to store list of field sets. Their DB representation look like list of dicts: .. code-block:: python class Student(Model): class Lectures(ListNode): name = field.String() code = field.String(required=False) st = Student() st.Lectures(name="Math101", code='M1') st.Lectures(name="Math102", code='M2') st.clean_value() { 'deleted': False, 'timestamp': None 'lectures': [ {'code': 'M1', 'name': 'Math101'}, {'code': 'M2', 'name': 'Math102'}, ] } Notes: - Currently we disregard the ordering of ListNode items. - "reverse_name" dose not supported on linked models. """ _TYPE = 'ListNode' def __init__(self, **kwargs): # self._is_item = False # self._from_db = False # self.values = [] # self.node_stack = [] # self.node_dict = {} self.setattrs( _is_item=False, _from_db=False, values=[], node_stack=[], node_dict={}, ) super(ListNode, self).__init__(**kwargs) self.setattrs(_data=[]) @lazy_property def objects(self): links = self.get_links() if links: lnk = links[0] root_lnk = self._root_node.get_link(field=self.__class__.__name__, startswith=True) if root_lnk['reverse'].endswith('_set'): remote_name = un_camel_id("%s.%s" % (root_lnk['reverse'], root_lnk['reverse'][:-4])) else: remote_name = un_camel_id(root_lnk['reverse']) return lnk['mdl'].objects.filter(**{remote_name:self._root_node.key}) def _load_data(self, data, from_db=False): """ Stores the data at self._data, actual object creation done at _generate_instances() Args: data (list): List of dicts. from_db (bool): Default False. Is this data coming from DB or not. """ self._data = data[:] self.setattrs( values=[], node_stack=[], node_dict={}, ) self._from_db = from_db def _generate_instances(self): """ ListNode item generator. Will be used internally by __iter__ and __getitem__ Yields: ListNode items (instances) """ for node in self.node_stack: yield node while self._data: yield self._make_instance(self._data.pop(0)) def _make_instance(self, node_data): """ Create a ListNode instance from node_data Args: node_data (dict): Data to create ListNode item. Returns: ListNode item. """ node_data['from_db'] = self._from_db clone = self.__call__(**node_data) clone.setattrs(container = self, _is_item = True) for name in self._nodes: _name = un_camel(name) if _name in node_data: # check for partial data getattr(clone, name)._load_data(node_data[_name]) _key = clone._get_linked_model_key() if _key: self.node_dict[_key] = clone return clone def _get_linked_model_key(self): """ Only one linked model can represent a listnode instance, Returns: The first linked models key if exists otherwise None """ for lnk in self.get_links(): return getattr(self, lnk['field']).key
[docs] def clean_value(self): """ Populates json serialization ready data. This is the method used to serialize and store the object data in to DB Returns: List of dicts. """ result = [] for mdl in self: result.append(super(ListNode, mdl).clean_value()) return result
[docs] def __repr__(self): """ This works for two different object: - Main ListNode object - Items of the ListNode (like instance of a class) which created while iterating on main ListNode object Returns: String representation of object. """ if not self._is_item: return [obj for obj in self[:10]].__repr__() else: try: u = six.text_type(self) except (UnicodeEncodeError, UnicodeDecodeError): u = '[Bad Unicode data]' return six.text_type('<%s: %s>' % (self.__class__.__name__, u))
# def __hash__(self): # if self.HASH_BY: # return hash(getattr(self, self.HASH_BY))
[docs] def add(self, **kwargs): """ Stores node data without creating an instance of it. This is more efficient if node instance is not required. Args: kwargs: attributes of the ListNode """ self._data.append(kwargs)
[docs] def pre_add(self): """ A hook for doing things before adding new listnode item to the stack """ pass
[docs] def __call__(self, **kwargs): """ Stores created instance in node_stack and returns it's reference to callee """ kwargs['_root_node'] = self._root_node clone = self.__class__(**kwargs) clone.setattrs(_is_item = True) clone.pre_add() self.node_stack.append(clone) _key = clone._get_linked_model_key() if _key: self.node_dict[_key] = clone return clone
[docs] def clear(self): """ Clear outs the list node. Raises: TypeError: If it's called on a ListNode item (intstead of ListNode's itself) """ if self._is_item: raise TypeError("This an item of the parent ListNode") self.node_stack = [] self._data = []
def __contains__(self, item): if self._data: return any([d[un_camel_id(item.__class__.__name__)] == item.key for d in self._data]) else: return item.key in self.node_dict def __len__(self): # FIXME: Partial evolution of ListNode iterator can cause incorrect results return len(self._data or self.node_stack) def __getitem__(self, index): return list(self._generate_instances()).__getitem__(index) def __iter__(self): return self._generate_instances() def __setitem__(self, key, value): # This is not useful in current state. Should be refactored or removed. if self._is_item: raise TypeError("This an item of the parent ListNode") self.node_stack[key] = value
[docs] def __delitem__(self, obj, sync=True): """ Allow usage of "del" statement on ListNodes with bracket notation. Args: obj: ListNode item or relation key. Raises: TypeError: If it's called on a ListNode item (intstead of ListNode's itself) """ if self._is_item: raise TypeError("This an item of the parent ListNode") list(self._generate_instances()) _lnk_key = None if isinstance(obj, six.string_types): _lnk_key = obj _obj = self.node_dict[obj] elif not isinstance(obj, self.__class__): _lnk_key = obj.key _obj = self.node_dict[obj.key] del self.node_dict[obj.key] else: _obj = obj self.node_stack.remove(_obj) if _lnk_key and sync: # this is a "many_to_n" relationship, # we should cleanup other side too. rel_name = "%s.%s" % (_obj.__class__.__name__, _obj.get_link()['field']) remote_node_name = self._root_node.get_link(field=rel_name)['reverse'] _lnk_obj = getattr(_obj, _obj.get_link()['field']) getattr(_lnk_obj, remote_node_name).__delitem__(self._root_node.key, sync=False) # binding relation's save to root objects save self._root_node.on_save.append(_lnk_obj.save)
[docs] def remove(self): """ Removes an item from ListNode. Raises: TypeError: If it's called on container ListNode (intstead of ListNode's item) Note: Parent object should be explicitly saved. """ if not self._is_item: raise TypeError("Should be called on an item, not ListNode's itself.") self.container.node_stack.remove(self)