Source code for pyoko.model

# -*-  coding: utf-8 -*-
"""
This module holds the pyoko's Model object
"""

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

from pyoko.exceptions import IntegrityError, ObjectDoesNotExist
from .node import Node, FakeContext
from . import fields as field
from .db.queryset import QuerySet
from .lib.utils import un_camel, lazy_property, pprnt, un_camel_id
import weakref
from pyoko.conf import settings
from pyoko.db.connection import cache
super_context = FakeContext()

# kept for backwards-compatibility
from .modelmeta import model_registry


[docs]class Model(Node): """ This is base class for any model object. Field instances are used as model attributes to represent values. .. code-block:: python class Permission(Model): name = field.String("Name") code = field.String("Code Name") def __unicode__(self): return "%s %s" % (self.name, self.code) Models may have inner classes to represent ManyToMany relations, inner data nodes or lists. Notes: - "reverse_name" does not supported on links from ListNode's. """ objects = QuerySet _TYPE = 'Model' _DEFAULT_BASE_FIELDS = { 'timestamp': field.DateTime(default='now'), 'updated_at': field.TimeStamp(), 'deleted_at': field.DateTime(), 'deleted': field.Boolean(default=False, index=True) } _SEARCH_INDEX = '' def __init__(self, context=None, **kwargs): # holds list of banned fields for current context # self._unpermitted_fields = [] # this indicates cell filters applied and we can filter on them # self._is_unpermitted_fields_set = False # self._context = context self.setattrs( key=kwargs.pop('key', None), _unpermitted_fields=[], _context=context, verbose_name=kwargs.get('verbose_name'), null=kwargs.get('null', False), unique=kwargs.get('unique'), reverse_name=kwargs.get('reverse_name'), _pass_perm_checks=kwargs.pop('_pass_perm_checks', False), _is_one_to_one=kwargs.pop('one_to_one', False), title=kwargs.pop('title', self.__class__.__name__), _root_node=self, new_back_links={}, _just_created=None, just_created=None, on_save=[], _exists=None, help_text=kwargs.get('help_text'), _initial_data={} ) # self.verbose_name = kwargs.get('verbose_name') # self.null = kwargs.get('null', False) # self.unique = kwargs.get('unique') # self.reverse_name = kwargs.get('reverse_name') # self._pass_perm_checks = kwargs.pop('_pass_perm_checks', False) # self._is_one_to_one = kwargs.pop('one_to_one', False) # self.title = kwargs.pop('title', self.__class__.__name__) # self._root_node = self # self.save_meta_data = None # used as a internal storage to wary of circular overwrite of the self.just_created # self._just_created = None # self._pre_save_hook_called = False # self._post_save_hook_called = False # self.new_back_links = {} self.objects._pass_perm_checks = self._pass_perm_checks kwargs['context'] = context super(Model, self).__init__(**kwargs) self.objects.set_model(model=self) self.setattrs(objects=self.row_level_access(self._context, self.objects)) self._instance_registry.add(weakref.ref(self)) # self.saved_models = [] def __str__(self): try: return self.__unicode__() except AttributeError: return "%s object" % self.__class__.__name__
[docs] def get_verbose_name(self): """ Returns: Verbose name of the model instance """ return self.verbose_name or self.Meta.verbose_name
[docs] def prnt(self): """ Prints DB data representation of the object. """ print("= = = =\n\n%s object key: \033[32m%s\033[0m" % (self.__class__.__name__, self.key)) pprnt(self._data or self.clean_value())
[docs] def __eq__(self, other): """ Equivalence of two model instance depends on uniformity of their self._data and self.key. """ return self._data == other._data and self.key == other.key
[docs] def __ne__(self, other): """ Ä°nequality of two model instance depends on uniformity of their self._data and self.key. """ return not self.__eq__(other)
def __hash__(self): # hash is based on self.key if exists or serialization of object's data. if self.key: return hash(self.key) else: clean_value = self.clean_value() clean_value['timestamp'] = '' return hash(str(clean_value))
[docs] def is_in_db(self): """ Deprecated: Use "exist" property instead. """ return self.exist
@property def exist(self): """ Used to check if a relation is exist or a model instance is saved to DB or not. Returns: True if this model instance stored in DB and has a key and False otherwise. """ return bool(self.key)
[docs] def get_choices_for(self, field): """ Get the choices for the given fields. Args: field (str): Name of field. Returns: List of tuples. [(name, value),...] """ choices = self._fields[field].choices if isinstance(choices, six.string_types): return [(d['value'], d['name']) for d in self._choices_manager.get_all(choices)] else: return choices
[docs] def set_data(self, data, from_db=False): """ Fills the object's fields with given data dict. Internally calls the self._load_data() method. Args: data (dict): Data to fill object's fields. from_db (bool): if data coming from db then we will use related field type's _load_data method Returns: Self. Returns objects itself for chainability. """ self._load_data(data, from_db) return self
def __repr__(self): if not self.is_in_db(): return six.text_type(self.__class__) else: return self.__str__() def _apply_cell_filters(self, context): """ Applies the field restrictions based on the return value of the context's "has_permission()" method. Stores them on self._unpermitted_fields. Returns: List of unpermitted fields names. """ self.setattrs(_is_unpermitted_fields_set=True) for perm, fields in self.Meta.field_permissions.items(): if not context.has_permission(perm): self._unpermitted_fields.extend(fields) return self._unpermitted_fields
[docs] def get_unpermitted_fields(self): """ Gives unpermitted fields for current context/user. Returns: List of unpermitted field names. """ return (self._unpermitted_fields if self._is_unpermitted_fields_set else self._apply_cell_filters(self._context))
[docs] @staticmethod def row_level_access(context, objects): """ Can be used to implement context-aware implicit filtering. You can define your query filters in here to enforce row level access control. If defined, will be called at queryset initialization step and it's return value used as Model.objects. Args: context: An object that contain required user attributes and permissions. objects (Queryset): QuerySet object. Examples: .. code-block:: python class FooBar(Model): return objects.filter(user=context.user) Returns: Queryset object. """ return objects
@lazy_property def _name(self): return un_camel(self.__class__.__name__) @lazy_property def _name_id(self): return "%s_id" % self._name def _update_new_linked_model(self, internal, linked_mdl_ins, link): """ Iterates through linked_models of given model instance to match it's "reverse" with given link's "field" values. """ # If there is a link between two sides (A and B), if a link from A to B, # link should be saved at B but it is not necessary to control again data in A. # If internal field is True, data control is not done and passes. if not internal: for lnk in linked_mdl_ins.get_links(): mdl = lnk['mdl'] if not isinstance(self, mdl) or lnk['reverse'] != link['field']: continue local_field_name = lnk['field'] # remote_name = lnk['reverse'] remote_field_name = un_camel(mdl.__name__) if not link['o2o']: if '.' in local_field_name: local_field_name, remote_field_name = local_field_name.split('.') remote_set = getattr(linked_mdl_ins, local_field_name) if remote_set._TYPE == 'ListNode' and self not in remote_set: remote_set(**{remote_field_name: self._root_node}) if linked_mdl_ins._exists is False: raise ObjectDoesNotExist('Linked %s on field %s with key %s doesn\'t exist' % ( linked_mdl_ins.__class__.__name__, remote_field_name, linked_mdl_ins.key, )) linked_mdl_ins.save(internal=True) else: linked_mdl_ins.setattr(remote_field_name, self._root_node) if linked_mdl_ins._exists is False: raise ObjectDoesNotExist('Linked object %s on field %s with key %s doesn\'t exist' % ( linked_mdl_ins.__class__.__name__, remote_field_name, linked_mdl_ins.key, )) linked_mdl_ins.save(internal=True) def _add_back_link(self, linked_mdl, link): # creates a new back_link reference self.new_back_links["%s_%s_%s" % (linked_mdl.key, link['field'], link['o2o'])] = (linked_mdl, link.copy()) def _handle_changed_fields(self, old_data): """ Looks for changed relation fields between new and old data (before/after save). Creates back_link references for updated fields. Args: old_data: Object's data before save. """ for link in self.get_links(is_set=False): fld_id = un_camel_id(link['field']) if not old_data or old_data.get(fld_id) != self._data[fld_id]: # self is new or linked model changed if self._data[fld_id]: # exists linked_mdl = getattr(self, link['field']) self._add_back_link(linked_mdl, link) def _process_relations(self,internal): buffer = [] for k, v in self.new_back_links.copy().items(): del self.new_back_links[k] buffer.append(v) for v in buffer: self._update_new_linked_model(internal, *v)
[docs] def reload(self): """ Reloads current instance from DB store """ self._load_data(self.objects.data().filter(key=self.key)[0][0], True)
[docs] def pre_save(self): """ Called before object save. Can be overriden to do things that should be done just before object saved to DB. """ pass
[docs] def post_save(self): """ Called after object save. Can be overriden to do things that should be done after object saved to DB. """ pass
[docs] def pre_delete(self): """ Called before object deletion. Can be overriden to do things that should be done before object is marked deleted. """ pass
[docs] def post_delete(self): """ Called after object deletion. Can be overriden to do things that should be done after object is marked deleted. """ pass
[docs] def post_creation(self): """ Called after object's creation (first save). Can be overriden to do things that should be done after object saved to DB. """ pass
[docs] def pre_creation(self): """ Called before object's creation (first save). Can be overriden to do things that should be done before object saved to DB. """ pass
def _handle_uniqueness(self): """ Checks marked as unique and unique_together fields of the Model at each creation and update, and if it violates the uniqueness raises IntegrityError. First, looks at the fields which marked as "unique". If Model's unique fields did not change, it means that there is still a record at db with same unique field values. So, it must be checked that if more than one result violates the uniqueness. If it is, raise an IntegrityError. Otherwise, when marked as unique fields in the list of changed fields, it must be checked that if exists any violation instead of more than one. And, if it is, again raise an IntegrityError. Then, looks at the fields which marked as "unique_together" with the same logic. Raises: IntegrityError if unique and unique_together checks does not pass """ def _getattr(u): try: return self._field_values[u] except KeyError: return getattr(self, u) if self._uniques: for u in self._uniques: val = _getattr(u) changed_fields = self.changed_fields(from_db=True) if self.exist and not (u in changed_fields if not callable(val) else (str(u) + "_id") in changed_fields): if val and self.objects.filter(**{u: val}).count() > 1: raise IntegrityError("Unique mismatch: %s for %s already exists for value: " "%s" % (u, self.__class__.__name__, val)) else: if val and self.objects.filter(**{u: val}).count(): raise IntegrityError("Unique mismatch: %s for %s already exists for value: " "%s" % (u, self.__class__.__name__, val)) if self.Meta.unique_together: changed_fields = self.changed_fields(from_db=True) for uniques in self.Meta.unique_together: vals = dict([(u, _getattr(u)) for u in uniques]) if self.exist: query_is_changed = [] for uni in vals.keys(): if callable(vals[uni]): is_changed = (str(uni) + "_id") in changed_fields query_is_changed.append(is_changed) else: is_changed = uni in changed_fields query_is_changed.append(is_changed) is_unique_changed = any(query_is_changed) if not is_unique_changed: if self.objects.filter(**vals).count() > 1: raise IntegrityError( "Unique together mismatch: %s combination already exists for %s" % (vals, self.__class__.__name__)) else: if self.objects.filter(**vals).count(): raise IntegrityError( "Unique together mismatch: %s combination already exists for %s" % (vals, self.__class__.__name__)) else: if self.objects.filter(**vals).count(): raise IntegrityError( "Unique together mismatch: %s combination already exists for %s" % (vals, self.__class__.__name__))
[docs] def save(self, internal=False, meta=None, index_fields=None): """ Save's object to DB. Do not override this method, use pre_save and post_save methods. Args: internal (bool): True if called within model. Used to prevent unneccessary calls to pre_save and post_save methods. meta (dict): JSON serializable meta data for logging of save operation. {'lorem': 'ipsum', 'dolar': 5} index_fields (list): Tuple list for indexing keys in riak (with 'bin' or 'int'). bin is used for string fields, int is used for integer fields. [('lorem','bin'),('dolar','int')] Returns: Saved model instance. """ for f in self.on_save: f(self) if not (internal or self._pre_save_hook_called): self._pre_save_hook_called = True self.pre_save() if not self.deleted: self._handle_uniqueness() if not self.exist: self.pre_creation() old_data = self._data.copy() if self.just_created is None: self.setattrs(just_created=not self.exist) if self._just_created is None: self.setattrs(_just_created=self.just_created) self.objects.save_model(self, meta_data=meta, index_fields=index_fields) self._handle_changed_fields(old_data) self._process_relations(internal) if not (internal or self._post_save_hook_called): self._post_save_hook_called = True self.post_save() if self._just_created: self.setattrs(just_created=self._just_created, _just_created=False) self.post_creation() self._pre_save_hook_called = False self._post_save_hook_called = False if not internal: self._initial_data = self.clean_value() return self
[docs] def changed_fields(self, from_db=False): """ Args: from_db (bool): Check changes against actual db data Returns: list: List of fields names which their values changed. """ if self.exist: current_dict = self.clean_value() # `from_db` attr is set False as default, when a `ListNode` is # initialized just after above `clean_value` is called. `from_db` flags # in 'list node sets' makes differences between clean_data and object._data. db_data = self._initial_data if from_db: # Thus, after clean_value, object's data is taken from db again. db_data = self.objects.data().get(self.key)[0] set_current, set_past = set(current_dict.keys()), set(db_data.keys()) intersect = set_current.intersection(set_past) return set(o for o in intersect if db_data[o] != current_dict[o])
[docs] def is_changed(self, field, from_db=False): """ Args: field (string): Field name. from_db (bool): Check changes against actual db data Returns: bool: True if given fields value is changed. """ return field in self.changed_fields(from_db=from_db)
[docs] def blocking_save(self, query_dict=None, meta=None, index_fields=None): """ Saves object to DB. Waits till the backend properly indexes the new object. Args: query_dict(dict) : contains keys - values of the model fields meta (dict): JSON serializable meta data for logging of save operation. {'lorem': 'ipsum', 'dolar': 5} index_fields (list): Tuple list for indexing keys in riak (with 'bin' or 'int'). bin is used for string fields, int is used for integer fields. [('lorem','bin'),('dolar','int')] Returns: Model instance. """ query_dict = query_dict or {} for query in query_dict: self.setattr(query, query_dict[query]) self.save(meta=meta, index_fields=index_fields) while not self.objects.filter(key=self.key, **query_dict).count(): time.sleep(0.3) return self
[docs] def blocking_delete(self, meta=None, index_fields=None): """ Deletes and waits till the backend properly update indexes for just deleted object. meta (dict): JSON serializable meta data for logging of save operation. {'lorem': 'ipsum', 'dolar': 5} index_fields (list): Tuple list for indexing keys in riak (with 'bin' or 'int'). bin is used for string fields, int is used for integer fields. [('lorem','bin'),('dolar','int')] """ self.delete(meta=meta, index_fields=index_fields) while self.objects.filter(key=self.key).count(): time.sleep(0.3)
def _traverse_relations(self): for lnk in self.get_links(link_source=False): yield (lnk, list(lnk['mdl'].objects.all(**{'%s_id' % un_camel(lnk['reverse']): self.key}))) def _delete_relations(self, dry=False): for lnk, rels in self._traverse_relations(): for rel in rels: key = lnk['reverse'].split('.')[0] lnkd_model = getattr(rel, key) if lnkd_model._TYPE == 'ListNode': del lnkd_model[self] elif lnkd_model._TYPE == 'Model': rel.setattr(key, lnkd_model.__class__()) # binding actual relation's save to our save self.on_save.append(lambda self: rel.save(internal=True)) return [], []
[docs] def delete(self, dry=False, meta=None, index_fields=None): """ Sets the objects "deleted" field to True and, current time to "deleted_at" fields then saves it to DB. Args: dry (bool): False. Do not execute the actual deletion. Just list what will be deleted as a result of relations. meta (dict): JSON serializable meta data for logging of save operation. {'lorem': 'ipsum', 'dolar': 5} index_fields (list): Tuple list for secondary indexing keys in riak (with 'bin' or 'int'). bin is used for string fields, int is used for integer fields. [('lorem','bin'),('dolar','int')] Returns: Tuple. (results [], errors []) """ from datetime import datetime # TODO: Make sure this works safely (like a sql transaction) if not dry: self.pre_delete() results, errors = self._delete_relations(dry) if not (dry or errors): self.deleted = True self.deleted_at = datetime.now() self.save(internal=True, meta=meta, index_fields=index_fields) self.post_delete() if settings.ENABLE_CACHING: cache.delete(self.key) return results, errors
[docs]class LinkProxy(object): """ Proxy object for "self" referencing model relations Example: .. code-block:: python class Unit(Model): name = field.String("Name") parent = LinkProxy('Unit', verbose_name='Upper unit', reverse_name='sub_units') """ _TYPE = 'Link' def __init__(self, link_to, one_to_one=False, verbose_name=None, reverse_name=None, null=False, unique=False): self.link_to = link_to self.unique = unique self.null = null self.one_to_one = one_to_one self.verbose_name = verbose_name self.reverse_name = reverse_name