"""
base.py : The Row class
* Copyright: 2017-2018 Sampsa Riikonen
* Authors : Sampsa Riikonen
* Date : 2017
* Version : 0.2
This file is part of the cute_mongo_forms library
License: LGPLv3+ (see the COPYING.LESSER file)
"""
# from PyQt5 import QtWidgets, QtCore, QtGui # Qt5
from PySide2 import QtWidgets, QtCore, QtGui
import sys
import copy
import collections
from cute_mongo_forms.tools import typeCheck, dictionaryCheck, objectCheck, parameterInitCheck, noCheck
from cute_mongo_forms.column.base import Column, LineEditColumn
pre_mod = "cute_mongo_forms.row.base : " # a string for aux debuggin purposes
verbose=False # module's verbosity
class ColumnSpec:
def __init__(self,column_class,**kwargs):
self.column_class=column_class
self.kwargs =kwargs
def instantiate(self):
return self.column_class(**self.kwargs)
class RowWatcher(type):
def __init__(cls, name, bases, clsdict):
if len(cls.mro()) > 2:
# print("was subclassed by " + name +" "+cls.__name__)
cls.genKeys()
# print(cls.keys)
super(RowWatcher, cls).__init__(name, bases, clsdict)
[docs]class Row(metaclass=RowWatcher):
"""
Defines a common columns structure for documents in a [document database] collection. The column structure (i.e. "Row") is defined in the child class header like this:
::
columns=[
ColumnSpec(LineEditColumn, key_name="firstname", label_name="First Name"),
...
]
This class knows how to create a Qt form, corresponding to the column structure
"""
name=None # if not defined, use classes __name__ attribute
columns=[]
parameter_defs={}
keys=[] # filled by first call to getKeys
keyset=None # re-generated each time you call "class Classname(Row) ..". Why this? We need the "keyset" at class level, not at object level
[docs] @classmethod
def genKeys(cls):
"""This is called when you declare the class, i.e. at "Class MyNewRow(Row) ...". It updates the class variables "keys" and "keyset"
"""
cls.keys=[]
for col in cls.columns: # ColumnSpec instances
if ("key_name" in col.kwargs):
cls.keys.append(col.kwargs["key_name"]) # remember.. ColumnSpec has kwargs thats going to be passed to each column when they're instantiated. "key_name" has the name of the column key.
cls.keyset=set(cls.keys)
@classmethod
def genChecker(cls):
pass
""" # TODO associated "orm" class for programmatic (i.e. non-widget) manipulation of the underlying database: we want to check that what we write to database is correct
class RowCheck: #
def __init__(self):
for col in cls.columns:
pass
# or ..
for key, column in column_by_name.items()
self.column_check_by_name[key] =column.ColumnCheck() # ColumnCheck instance. Houses a copy of the value.
def __getattr__(self,key):
# classname and _id must be handled separately
# _id is saved as an attribute of RowCheck
# classname should be a constant
column_check=self.column_check_by_name[key]
return column_check.get()
def __setattr__(self,key):
column_check=self.column_check_by_name[key]
column_check.set(value)
cls.RowCheck=RowCheck
obj=Row.jsonToObj(doc) # instantiates and returns a new RowCheck object
#
value=obj.key1 => returns value (from RowCheck.column_check_by_name[key1].value == ColumnCheck.value)
obj.key1=value => perform the check => if ok, value goes into RowCheck.column_check_by_name[key1] == ColumnCheck.value
class ColumnCheck: # internal class of each Column class
def __init__(self):
self.value=default_value
def get(self):
return self.value # always correct, of course..
def set(self,value):
# perform type and limits check here
self.value=value
"""
def __init__(self,**kwargs):
self.pre=self.__class__.__name__+" : " # auxiliary string for debugging output
parameterInitCheck(self.parameter_defs,kwargs,self) # check kwargs agains parameter_defs, attach ok'd parameters to this object as attributes
self.initColumns()
self.makeWidget()
self.customInit()
def customInit(self):
pass
def initColumns(self):
# find class variables ..
self.column_by_name=collections.OrderedDict()
# self.columns_=[] # list of tuples: name, label, instance
self.columns_=[] # now just a list of column instances
for column_def in self.columns:
col_instance=column_def.instantiate() # instantiate the column
self.column_by_name[col_instance.key_name]=col_instance
self.columns_.append(col_instance)
[docs] def getName(self):
"""Return a name of this class that can be displayed (instead of just the classname)
"""
if self.__class__.name:
return self.__class__.name
return self.__class__.__name__
def __getattr__(self,key):
try:
column=self.column_by_name[key]
except KeyError:
raise(AttributeError("No such column "+str(key)))
return
else:
return column
def __getitem__(self,key):
try:
column=self.column_by_name[key]
except KeyError:
raise(AttributeError("No such column "+str(key)))
return
else:
return column
def __str__(self):
st=""
for key, column in self.column_by_name.items():
st+=key+":"+str(column.getValue())+" "
return st
def __collect__(self):
"""Collect dictionary from QWidgets
"""
dic={} # this will be written to db
for key, column in self.column_by_name.items():
dic[key]=column.getValue()
# dic["classname"]=self.__class__.__name__ # not here, but in collection
return dic
[docs] def new(self,collection):
"""New entry to collection
"""
dic=self.__collect__()
return collection.new(self.__class__,dic)
[docs] def update(self,collection,_id):
"""Save from QtWidgets to collection
"""
dic=self.__collect__()
dic["_id"]=_id
# print(self.pre,"update",dic)
collection.update(self.__class__,dic)
[docs] def delete(self,collection,_id):
"""Delete a record
"""
collection.delete(_id)
def clear(self):
for key, column in self.column_by_name.items():
# print(self.pre,"clear",key)
column.reset()
[docs] def get(self,collection,_id):
"""Load one entry from db to QtWidgets
"""
it =collection.get({"_id":_id}) # there should be only one ..
try:
res=next(it)
except StopIteration:
print(self.pre,"get : Could not get _id=",_id)
return
# print(self.pre,"get",res)
for key, column in self.column_by_name.items():
try:
val=res[key]
except KeyError:
print(self.pre,"get : WARNING : record does not have column",key,"full record=",res)
column.reset()
else:
column.setValue(val)
[docs] def set_(self,dic):
"""Sets the widget values
"""
for key, column in self.column_by_name.items():
try:
val=dic[key]
except KeyError:
print(self.pre,"get : WARNING : record does not have column",key,"full record=",res)
column.reset()
else:
column.setValue(val)
[docs] def set_column_value(self, col_key, value):
"""Sets the value of one column in the widget
"""
self.column_by_name[col_key].setValue(value)
[docs] def get_column_value(self, col_key):
"""Gets a value from one column of the widget
"""
return self.column_by_name[col_key].getValue()
# print(self.pre,"makeWidget :",labelname,label,column.widget)
def update_notify_slot(self):
# print(self.pre, "update_notify_slot")
pass
def purge(self, dic):
if (verbose): print(self.pre,"purge",dic)
# uses self.keyset
needs_update=False
tmpdic=copy.deepcopy(dic)
tmpdic.pop("_id"); tmpdic.pop("classname") # remove metadata
dicset=set(tmpdic) # fields in the record to be checked
# the document record has extra fields:
for df in dicset.difference(self.keyset): # dicset-keyset = extra keys in dicset
if (verbose): print(self.pre,"purge : removing key",df,"from",dic)
dic.pop(df) # remove fields from the record
needs_update=True
# the document record is missing fields:
for df in self.keyset.difference(dicset): # keyset-dicset = keyset has more keys
dic[df]=self.column_by_name[df].getValue() # gets the default value
if (verbose): print(self.pre,"purge : appending key/value",df,dic[df],"full record now=",dic)
needs_update=True
# by now the document should be consistent with the schema in self.keys
for key in self.keys: # run through all key value pairs
col=self.column_by_name[key]
update, val=col.purge(dic[key]) # purge dangling foreign key references. Return valid reference(s) in val
if (update):
if (verbose): print(self.pre,"purge : updated value in key",key,"to",val)
dic[key] =val
needs_update=True
return needs_update, dic
def test1():
st="""Empty test
"""
pre=pre_mod+"test1 :"
print(pre,st)
def test2():
st="""Empty test
"""
pre=pre_mod+"test2 :"
print(pre,st)
def main():
pre=pre_mod+"main :"
print(pre,"main: arguments: ",sys.argv)
if (len(sys.argv)<2):
print(pre,"main: needs test number")
else:
st="test"+str(sys.argv[1])+"()"
exec(st)
if (__name__=="__main__"):
main()