I have a need to track changes on Django model instances. I'm aware of solutions like django-reversion but they are overkill for my cause.
I had the idea to create a parameterized class decorator to fit this purpose. The arguments are the field names and a callback function. Here is the code I have at this time:
def audit_fields(fields, callback_fx):
def __init__(self, *args, **kwargs):
self.__old_init(*args, **kwargs)
self.__old_state = self.__get_state_helper()
def save(self, *args, **kwargs):
new_state = self.__get_state_helper()
for k,v in new_state.items():
if (self.__old_state[k] != v):
callback_fx(self, k, self.__old_state[k], v)
val = self.__old_save(*args, **kwargs)
self.__old_state = self.__get_state_helper()
return val
def __get_state_helper(self):
# make a list of field/values.
state_dict = dict()
for k,v in [(field.name, field.value_to_string(self)) for field in self._meta.fields if field.name in fields]:
state_dict[k] = v
return state_dict
def fx(clazz):
# Stash originals
clazz.__old_init = clazz.__init__
clazz.__old_save = clazz.save
# Override (and add helper)
clazz.__init__ = __init__
clazz.__get_state_helper = __get_state_helper
clazz.save = save
return clazz
return fx
And use it as follows (only relevant part):
#audit_fields(["status"], fx)
class Order(models.Model):
BASKET = "BASKET"
OPEN = "OPEN"
PAID = "PAID"
SHIPPED = "SHIPPED"
CANCELED = "CANCELED"
ORDER_STATES = ( (BASKET, 'BASKET'),
(OPEN, 'OPEN'),
(PAID, 'PAID'),
(SHIPPED, 'SHIPPED'),
(CANCELED, 'CANCELED') )
status = models.CharField(max_length=16, choices=ORDER_STATES, default=BASKET)
And test on the Django shell with:
from store.models import Order
o=Order()
o.status=Order.OPEN
o.save()
The error I receive then is:
TypeError: int() argument must be a string or a number, not 'Order'
The full stacktrace is here: https://gist.github.com/4020212
Thanks in advance and let me know if you would need more info!
EDIT: Question answered by randomhuman, code edited and usable as shown!
You do not need to explicitly pass a reference to self on this line:
val = self.__old_save(self, *args, **kwargs)
It is a method being called on an object reference. Passing it explicitly in this way is causing it to be seen as one of the other parameters of the save method, one which is expected to be a string or a number.
Related
Is there a way to add custom field attribute in Odoo? For example every field has attribute help where you can enter message explaining the field for the user. So I want to add custom attribute, so that would change the way field acts for all types of fields.
I want to add into Field class, so all fields would get that attribute. But it seems no matter what I do, Odoo does not see that such attribute was added.
If I simply add new custom attribute like:
some_field = fields.Char(custom_att="hello")
Then it is simply ignored. And I need it to be picked up by method fields_get, which can return wanted attribute value (info what it does:
def fields_get(self, cr, user, allfields=None, context=None, write_access=True, attributes=None):
""" fields_get([fields][, attributes])
Return the definition of each field.
The returned value is a dictionary (indiced by field name) of
dictionaries. The _inherits'd fields are included. The string, help,
and selection (if present) attributes are translated.
:param allfields: list of fields to document, all if empty or not provided
:param attributes: list of description attributes to return for each field, all if empty or not provided
"""
So calling it, does not return my custom attribute (it does return the ones originally defined by Odoo though).
I also tried updating _slots (with monkey patch or just testing by changing source code) attribute in Field class, but it seems it is not enough. Because my attribute is still ignored.
from openerp import fields
original_slots = fields.Field._slots
_slots = original_slots
_slots['custom_att'] = None
fields.Field._slots = _slots
Does anyone know how to properly add new custom attribute for field?
Assuming v9
The result of fields_get is a summary of fields defined on a model, the code shows that it will only add the attribute if the description was filled. It will fetch the description of the current field by calling field.get_description
So in order to ensure that your attribute gets inserted into this self.description_attrs you will need to add an attribute or method that starts with _description_customatt (customatt part from your example) and will return the required data.
I've not run any tests for this but you can look at the code for the fields and their attributes what they actually return. For instance the help attribute description (src)
def _description_help(self, env):
if self.help and env.lang:
model_name = self.base_field.model_name
field_help = env['ir.translation'].get_field_help(model_name)
return field_help.get(self.name) or self.help
return self.help
This is only something you can do if you run OpenERP/ODOO on your own server (in other words, not the cloud version whose code you cannot access).
You will need to modify the <base>/osv/fields.py file and add your changes to the field_to_dict function towards the bottom of the file (the base _column class already saves extra keyword arguments for you -- at least in version 7.0):
def field_to_dict(model, cr, user, field, context=None):
res = {'type': field._type}
...
...
for arg in ('string', 'readonly', ...) :
Somewhere in that long list of attributes you need to insert the name of the one you are interested in.
Alternatively, you could update _column.__init__ to save the names of the extra arguments, and field_to_dict to include them (untested):
diff -r a30d30db3cd9 osv/fields.py
--- a/osv/fields.py Thu Jun 09 17:18:29 2016 -0700
+++ b/osv/fields.py Mon Jun 13 18:11:26 2016 -0700
## -116,23 +116,24 ## class _column(object):
self._context = context
self.write = False
self.read = False
self.view_load = 0
self.select = select
self.manual = manual
self.selectable = True
self.group_operator = args.get('group_operator', False)
self.groups = False # CSV list of ext IDs of groups that can access this field
self.deprecated = False # Optional deprecation warning
- for a in args:
- if args[a]:
- setattr(self, a, args[a])
+ self._user_args = ()
+ for name, value in args:
+ setattr(self, name, value or False)
+ self._user_args += name
def restart(self):
pass
def set(self, cr, obj, id, name, value, user=None, context=None):
cr.execute('update '+obj._table+' set '+name+'='+self._symbol_set[0]+' where id=%s', (self._symbol_set[1](value), id))
def get(self, cr, obj, ids, name, user=None, offset=0, context=None, values=None):
raise Exception(_('undefined get method !'))
## -1559,20 +1560,22 ## def field_to_dict(model, cr, user, field
res['o2m_order'] = field._order or False
if isinstance(field, many2many):
(table, col1, col2) = field._sql_names(model)
res['m2m_join_columns'] = [col1, col2]
res['m2m_join_table'] = table
for arg in ('string', 'readonly', 'states', 'size', 'group_operator', 'required',
'change_default', 'translate', 'help', 'select', 'selectable', 'groups',
'deprecated', 'digits', 'invisible', 'filters'):
if getattr(field, arg, None):
res[arg] = getattr(field, arg)
+ for arg in field._user_args:
+ res[arg] = getattr(field, arg)
if hasattr(field, 'selection'):
if isinstance(field.selection, (tuple, list)):
res['selection'] = field.selection
else:
# call the 'dynamic selection' function
res['selection'] = field.selection(model, cr, user, context)
if res['type'] in ('one2many', 'many2many', 'many2one'):
res['relation'] = field._obj
res['domain'] = field._domain(model) if callable(field._domain) else field._domain
I am attempting to push data from a DJANGO view into the Tables object, passing it through as an argument. In this case, I would like to pass a variable called doc_id into a Tables2 object called tableName
In this example, I have set doc_id as 1, and pass it into the
View
def editorView(request):
doc_id = 1
table = tableName(UserProfile.objects.filter(), doc_id=doc_id)
Table
class tableName(tables.Table):
tbl_doc_id = None ## Creating a temporary variable
def __init__(self, *args, **kwargs):
temp = kwargs.pop("doc_id") ## Grab doc_ID from kwargs
super(tableName, self).__init__(*args, **kwargs)
self.tbl_doc_id = temp ## Assign to self.tbl_doc_id for use later
### Do something with tbl_doc_id
modelFilter = model.objects.filter(pk = tbl_doc_id)
When running the debugger, I can see that tbl_doc_id is still assigned as None, rather than 1.
What is the correct way to pass arguments into a Tables2 instance? Is it possible?
EDIT: Adding more information for context.
In the real world scenario, I have a view. That view takes an argument from the URL called doc_id. That doc_id is used to grab an object from a model called 'MaterialCollection', and return it as 'mc'.
'mc' is then passed into the table
View
def editorView(request, doc_id):
try:
mc = MaterialCollection.objects.get(pk = doc_id)
except Material.DoesNotExist:
raise Http404("Document does not exist")
config = RequestConfig(request)
unnassigned_User_Table = unassignedUserTable(UserProfile.objects.filter(), mc=mc)
... Other code + Render ...
From my table, I create a custom LinkColumn. That linkColumn is used to construct a URL based upon a number of Attributes from the model 'UserProfile', and from mc.
Table
class unassignedUserTable(tables.Table):
mc = None
def __init__(self, *args, **kwargs):
temp_mc = kwargs.pop("mc")
super(unassignedUserTable, self).__init__(*args, **kwargs)
self.mc = temp_mc
current_Assignment = "NONE"
new_Assignment = "AS"
assign_Reviewer = tables.LinkColumn('change_Review_AssignmentURL' , args=[ A('user'), current_Assignment, new_Assignment, mc, A('id')], empty_values=(), attrs={'class': 'btn btn-success'})
class Meta:
model = UserProfile
... Setup excludes/sequence/attributes...
In this particular instance. mc has a FK to UserProfile (in a 1:M) relationship.
I see that the name of your table class is tableName so if you want __init__ to work as expected please change the line:
super(unassignedUsers, self).__init__(*args, **kwargs)
to
super(tableName, self).__init__(*args, **kwargs)
Beyond this obvious problem, there are some more issues with your code:
Your classes must start with a capital letter (TableName instead of tableName)
Your table classes should end end with -Table (for example NameTable)
I am using django-tables2 for many years and never needed to pass something in __init__ as you are doing here. Are you sure that you really need to do this?
If you want to filter the table's data the filtering must be done to your view - the table will get the filtered data to display.
I created a form with a TypedChoiceField:
class EditProjectForm(ModelForm):
def __init__(self, action, *args, **kwargs):
super(EditProjectForm, self).__init__(*args, **kwargs)
now = datetime.datetime.now()
if action == 'edit':
project_year = kwargs['project_year']
self.fields['year'].choices = [(project_year, project_year)]
else:
self.fields['year'].choices = [(now.year, now.year), (now.year + 1, now.year + 1)]
year = forms.TypedChoiceField(coerce=int)
...
This works perfectly fine when using it inside a view. Now I want to write tests for this form:
form_params = {
'project_year': datetime.datetime.now().year,
}
form = EditProjectForm('new', form_params)
self.assertTrue(form.is_valid())
The test fails, because is_valid() returns False. This is because when calling super.__init__() in the EditProjectForm, the field year doesn't have its choices yet. So the validation for this field fails and an error is added to the error list inside the form.
Moving the super call after self.fields['year'].choices doesn't work either, because self.fields is only available after the super.__init__() call.
How can I add the choices dynamically and still be able to test this?
Okay, I found the problem.
The field year is a class variable, and is instantiated even before the tests setUp method and the forms __init__ method was called. Since I haven't passed the required choices parameter for this field, the error was issued way before the form object was created.
I changed the behaviour so I change the type of the fields in the __init__ method rather than using a class variable for that.
class EditProjectForm(ModelForm):
def __init__(self, action, *args, **kwargs):
super(EditProjectForm, self).__init__(*args, **kwargs)
now = datetime.datetime.now()
if action == 'edit':
project_year = kwargs['project_year']
choices = [(project_year, project_year)]
else:
choices = [(now.year, now.year), (now.year + 1, now.year + 1)]
self.fields['year'] = forms.TypedChoiceField(coerce=int, choices=choices)
I have a django model with foreigns. I want to limit the choices for it with depend on content of another field of this model.
This code works:
class PhysicalProperty(models.Model):
property_quantity = models.ForeignKey(Quantity)
default_unit = models.ForeignKey(MeasurementUnits, limit_choices_to = {'quantity': 1 )
But it takes from MeasurementUnits all records with MeasurementUnits.quantity = 1. And I need to set query as MeasurementUnits.quantity = PhysicalProperty.property_quantity.
This code doesn't work
class PhysicalProperty(models.Model):
property_quantity = models.ForeignKey(Quantity)
default_unit = models.ForeignKey(MeasurementUnits, limit_choices_to = {'quantity': property_quantity )
You cant use self in class. self is for instances of classes.
you could use the init method for that
class PhysicalProperty(models.Model):
property_quantity = models.ForeignKey(Quantity)
default_unit = models.ForeignKey(MeasurementUnits)
def __init__(self, *args, **kwargs)
super(self, PhysicalProperty).__init__(*args, **kwargs)
self.default_unit = self.property_quantity
that way, everytime you do physical_property = PhysicalProperty(), physical_property.default_unit will be the same as property_quantity of the same object.
I want to implement a simple VersionedModel base model class for my app engine app. I'm looking for a pattern that does not involve explicitly choosing fields to copy.
I am trying out something like this, but it is to hacky for my taste and did not test it in the production environment yet.
class VersionedModel(BaseModel):
is_history_copy = db.BooleanProperty(default=False)
version = db.IntegerProperty()
created = db.DateTimeProperty(auto_now_add=True)
edited = db.DateTimeProperty()
user = db.UserProperty(auto_current_user=True)
def put(self, **kwargs):
if self.is_history_copy:
if self.is_saved():
raise Exception, "History copies of %s are not allowed to change" % type(self).__name__
return super(VersionedModel, self).put(**kwargs)
if self.version is None:
self.version = 1
else:
self.version = self.version +1
self.edited = datetime.now() # auto_now would also affect copies making them out of sync
history_copy = copy.copy(self)
history_copy.is_history_copy = True
history_copy._key = None
history_copy._key_name = None
history_copy._entity = None
history_copy._parent = self
def tx():
result = super(VersionedModel, self).put(**kwargs)
history_copy._parent_key = self.key()
history_copy.put()
return result
return db.run_in_transaction(tx)
Does anyone have a simpler cleaner solution for keeping history of versions for app engine models?
EDIT: Moved copy out of tx. Thx #Adam Crossland for the suggestion.
Take a look at the properties static method on Model classes. With this, you can get a list of properties, and use that to get their values, something like this:
#classmethod
def clone(cls, other, **kwargs):
"""Clones another entity."""
klass = other.__class__
properties = other.properties().items()
kwargs.update((k, p.__get__(other, klass)) for k, p in properties)
return cls(**kwargs)