django simple history - using model methods? - python

I'm using django-simple-history:
http://django-simple-history.readthedocs.io/en/latest/
I have a model, which I would like to apply its methods on an historical instance. Example:
from simple_history.models import HistoricalRecords
class Person(models.Model):
firstname = models.CharField(max_length=20)
lastname = models.CharField(max_length=20)
history = HistoricalRecords()
def fullName(self):
return firstname + lastname
person = Person.objects.get(pk=1) # Person instance
for historyPerson in person.history:
historyPerson.fullName() # wont work.
Since the class HistoricalPerson does not inherit the methods of Person. But using Person methods actually make sense, since they share the same fields..
Any solution for this? I'd prefer something simple, not like duplicating every method in my models for the history instances..

I found another workaround (maybe it's just the addon had been updated and got this feature). It's based on the documentation: adding-additional-fields-to-historical-models
HistoricalRecords field accepts bases parameter which sets a class that history objects will inherit. But you can't just set bases=[Person] inside Person class description, because it's not yet initialized.
So I ended up with an abstract class, which is inherited by both Person class and HistoricalRecords field. So the example from the question would look like:
class AbstractPerson(models.Model):
class Meta:
abstract = True
firstname = models.CharField(max_length=20)
lastname = models.CharField(max_length=20)
def fullName(self):
return firstname + lastname
class Person(AbstractPerson):
history = HistoricalRecords(bases=[AbstractPerson])
And now history objects can use fullName method.

For anyone else having the same problem, I made it work by calling the method from the original class on the historical record object. So for the example in the question, a solution could be:
for historyPerson in person.history:
Person.fullName(historyPerson)
This works because methods are very much like functions in Python, except that when you call a method on an instance, the instance is implicitly passed as the first parameter for the method. So if you have a class like:
class Foo:
def method(self):
....
doing
f = Foo()
f.method()
is the same as:
f = Foo()
Foo.method(f)
I don't know exactly why simple-history does not copy the original model's methods though. One reason might be that since it allows you to exclude fields to be recorded, having the original methods might not make sense, since a method might not work if it uses fields that are not recorded in the historical record.

Related

Is there a way to reference variable name as a string in that variable's declaration in Python?

For my Django application, I have a dictionary of field names (keys) mapped to help texts (values) (which I read in from a csv file). In models.py, I want to retrieve the appropriate help text for each field name.
If I have a sample dictionary like the following, with an entry for each field:
test_dict = {
'host_name': 'The name of a host',
...
}
And my models.py looks like this:
class Host_main(models.Model):
host_name = models.CharField(max_length=20, unique=True, help_text=test_dict['host_name'])
def __str__(self):
return self.host_name
Is there a way to call the variable name (host_name) dynamically in each help_text definition?
I do have the option to do something like the following:
from varname import nameof
host_name = models.CharField(max_length=20, unique=True, help_text=test_dict[nameof(host_name)])
But if possible, I'd like to reference the current variable name with something consistent to avoid typing out the field name a second time, like help_text=test_dict[nameof(**this**)] in pseudocode.
Thanks!
Let's simplify your question a little bit, and let's remove the django part first. Let's say you have a class with name A, then you can set an attribute on A with
class A: pass
A.foo = 'bar'
Alternatively, you can also do
setattr(A,'bar','baz')
You can verify that those are indeed equivalent by checking
print(A.foo) # bar
print(A.bar) # baz
So if you are defining a normal class, you can pretty much just loop through your dict and set things with setattr calls, in which case you have the control of the variable name.
And when we bring Django into the question, things are a bit more complicated...There are some very strange and magical things happening when you initialize a Model class. Basically it does a lookup on things already defined, and transforms all the defined fields through the pretty much public private _meta API. This means that adding class attributes outside of definition time won't work, and you might have to use a bit of a hack to construct a class directly through its metaclass (in case you are not familiar with Python's metaclasses, a class is basically an instance of its metaclass).
The basic syntax for dynamically constructing a class is through the type call with 3 arguments: type(name, bases, dict). In your example,
class Host_main(models.Model):
host_name = models.CharField(max_length=20, unique=True, help_text=test_dict['host_name'])
def __str__(self):
return self.host_name
is the equivalent of
def Host_main_str(self): # the name here doesn't really matter unless you care about the __name__ attribute of the function, which you can overwrite.
return self.host_name
Host_main = type('Host_main', (models.Model,), {
'__str__': Host_main_str,
'host_name': models.CharField(max_length=20, unique=True, help_text=test_dict['host_name'])
})
It's actually how django constructs all their QuerySet classes (there were some more crazy things django did with dynamic class constructions if I remember correctly but I couldn't find a good example)
So, you can do things like
attr_dict = {'__str__':'Host_main_str'}
for name, help_text in test_dict.values():
attr_dict[name] = models.CharField(max_length=20, unique=True, help_text=help_text)
Host_main = type('Host_main', (models.Model,), attr_dict)
if you really insist on loop through a dictionary.

Using a django model method as static function

I would like to use a django model method as a static function. More specifically, I would like to use a model method called age, indicated below, both on an instance of my table Patient but I would also like apply the age function to a rows in a pandas dataframe using the pandas apply function. Is this possible to do or will I have to write another function specifically to work on my dataframe?
class Patient(models.Model):
pat_id1 = models.AutoField(db_column='Pat_ID1', primary_key=True)
birth_dttm = models.DateTimeField(db_column='Birth_DtTm', blank=True, null=True)
objects = models.Manager()
class Meta:
managed = False
db_table = 'Patient'
#property
def age(self):
return relativedelta(date.today(), self.birth_dttm.date()).years
Your age function is using self. This means this function is dependent on an instance of your class to give an output. So, this method cannot be declared as a staticmethod.
EDIT
Excerpt from journaldev Static methods have a very clear use-case. When we need some functionality not w.r.t an Object but w.r.t the complete class, we make a method static. This is pretty much advantageous when we need to create Utility methods as they aren’t tied to an object lifecycle.

Django model fields from constants?

How to define a Django model field in constant and use everywhere.
For example, if I have a model like:-
class Author(models.Model):
first_name = models.CharField(max_length=30)
last_name = models.CharField(max_length=40)
email = models.EmailField()
And what I want to do is define constant for fields in Author model and provide the constant instead of field name in model like:-
KEY_FIRST_NAME = 'first_name'
KEY_LAST_NAME = 'last_name'
KEY_EMAIL = 'email'
And Author model should use the constant instead of exact key like:-
class Author(models.Model):
KEY_FIRST_NAME = models.CharField(max_length=30)
KEY_LAST_NAME = models.CharField(max_length=40)
KEY_EMAIL = models.EmailField()
How to do something like this, direct assignment to constant won't work here.
I want to store all the field name in constant, and everywhere when it required I want to use the constant instead of string field name.
The purpose of doing this is If there is any change in filed name in future version then I want to only change at one place and it should reflect on all the places.
If it is not possible or it will make code too complex as suggested by one approach by #dirkgroten than what can be the best practice to define the model field as constant and use them in other places (other than inside models like if we are referring those field for admin portal or any other place).
Short answer: you can't do this in Python, period (actually I don't think you could do so in any language but someone will certainly prove me wrong xD).
Now if we go back to your real "problem" - not having to change client code if your model's fields names are ever to change - you'd first need to tell whether you mean "the python attribute name" or "the underlying database field name".
For the second case, the database field name does not have to match the Python attribute name, Django models fields take a db_column argument to handle this case.
For the first case, I'd have to say that it's a very generic (and not new by any mean) API-design problem, and the usual answer is "you shouldn't change names that are part of your public API once it's been released". Now sh!t happens and sometimes you have to do it. The best solution here is then to use computed attributes redirecting the old name to the new one for the deprecation period and remove them once all the client code has been ported.
An example with your model, changing 'first_name' to 'firstname':
class Author(models.Model):
# assuming the database column name didn't change
# so we can also show how to us `db_column` ;)
firstname = models.CharField(
max_length=30,
db_column='first_name'
)
#property
def first_name(self):
# shoud issue a deprecation warning here
return self.firstname
#first_name.setter
def first_name(self, value):
# shoud issue a deprecation warning here
self.firstname = value
If you have a dozen fields to rename you will certainly want to write a custom descriptor (=> computed attribute) instead to keep it dry:
class Renamed(object):
def __init__(self, new_name):
self.new_name = new_name
def __get__(self, instance, cls):
if instance is None:
return self
# should issue a deprecation warning here
return getattr(instance, self.new_name)
def __set__(self, instance, value):
# should issue a deprecation warning here
setattr(instance, self.new_name, value)
class Author(models.Model):
firstname = models.CharField(
max_length=30,
db_column='first_name'
)
first_name = Renamed("firstname")
I think the following information could prove beneficial:
To achieve this you first need to think how you can define class parameters from strings. Hence, I came across a way to dynamically create derived classes from base classes: link
Particularly this answer is what I was looking for. You can dynamically create a class with the type() command.
From here on, search how to integrate that with Django. Unsurprisingly someone has tried that already - here.
In one of the answers they mention dynamic Django models. I haven't tried it, but it might be what you are searching for.

foreignkey relationships

Lets say i have 3 classes, A, B, C.
class A(models.Model):
comment = models.CharField(max_length=600, default="None")
rating = models.IntegerField(default=1, choices=CHOICES, name='rating')
date = models.CharField(max_length=50, default='nonee')
class B(models.Model):
Aname = models.ForeignKey('A', related_name='AB')
classC = models.ForeignKey('C', related_name='BC')
class C(models.Model)
#some info
def average_rating(self):
return self.?????.all().aggregate(Avg('rating')).values()[0]
How is it that I go from a view where my self is an object, all the way back to Class A so that I can aggregate the rating numbers. If i understand this correctly, the whole point of class B is just to be an object which shows relationships? I have been able to go between two classes, but when a third "relational" one is there i can't seem to get it to work.
When an operation needs to be performed on a recordset (queryset) basis rather than single record (model), then you should consider custom managers.
Adding extra Manager methods is the preferred way to add “table-level” functionality to your models. (For “row-level” functionality – i.e., functions that act on a single instance of a model object – use Model methods, not custom Manager methods.)
You don't need class B at all. What you need is a ManyToManyField between A and C; that will, behind the scenes, create a table similar to B, but unless you actually need to add fields on that table you're better off not defining it explicitly.
Once you've added the M2M on C, your average_rating method can use it directly:
class C(models.Model)
model_a_s = models.ManyToManyField('A')
def average_rating(self):
return self.model_a_s.all().aggregate(Avg('rating')).values()[0]
(Note, the title of your question is a bit confusing; there are no views involved here at all.)

Using related_name correctly in Django

I have two models that are related together using ForeignKey and related_name is used. Here is an example.
class Student(models.Model):
name = models.CharField(max_length=255)
birthday = models.DateField(blank=True)
class Class(models.Model):
name = models.CharField(max_length=255)
student = models.ForeignKey(Student,
related_name='classes',
null=True)
def __unicode__(self):
return self.name
For example, I would like to access the class name.
This is what i tried.
john = Student.objects.get(username = 'john')
print john.classes.name
nothing's get printed.
But when i try john.classes
i get django.db.models.fields.related.RelatedManager object at 0x109911410. This is shows that they are related. But i would like to get the class name.
Am i doing something wrong? How do i access the name of the class using related_name? Need some guidance.
Yes, classes is a manager. It can be several classes for one teacher. So to output their names you should do:
john = Student.objects.get(username='john')
for class2 in john.classes.all():
print class2.name
If you want only one class for one student then use one-to-one relation. In this case you can access the related field with your method.
Just be aware: you are defining a 1-many relationship. Thus, student could have multiple classes, therefore john.classes.name cannot work, since you have not specified the class of which you want to have the name. in john.classes "classes" is just a manager that you can use like any other Django Model Manager. You can do a john.classes.all() (like sergzach propsed), but also things like john.classes.get(...) or john.classes.filter(...).
you can do like this to access the first row in the table
john = Student.objects.get(username = 'john')
john.classes.all().first().name # to access first row
john.classes.all().last().name # to access last row
in the above example you don't want to iterate over the objects
it will give you the name of the class in the first row

Categories