Django Model for mandatory/excatly one cross table relations - python

I am trying to find guidance on how to build a model representing the following:
We have IT environments (consisting of multiple components, like webservers, databases, etc.)
SLAs are not specific to one environment, it's more a set of general contracts a concrete environment references to (aka should be a separate table)
each environment must have at least one or more SLA(s) associated
From all SLAs associated to an environment exactly one must have the state "effective"
I implemented a model sufficiently reflecting the first two points (at least, I think so), but especially the last point seems to be cumbersome.
Sufficiently meaning, using this implementation, the relation using the cross table is optional, not mandatory. This is OK currently, but not in the long run.
class Environment(models.Model):
fullname = models.CharField(max_length=45)
...
sla = models.ManyToManyField(SLA, through='EnvironmentSLA')
creation_date = models.DateTimeField(auto_now_add=True)
class Meta:
unique_together = (('fullname', 'projectid', 'regionid', 'account'),)
class SLA(models.Model):
description = models.CharField(max_length=255)
reaction_time = models.CharField(max_length=45)
service_level = models.CharField(max_length=45)
creation_date = models.DateTimeField(auto_now_add=True)
class EnvironmentSLA(models.Model):
PLANNED = 'pl'
EFFECTIVE = 'ef'
DEPRECATED = 'dp'
SLA_STATE = (
( PLANNED, 'planned' ),
( EFFECTIVE, 'effective'),
( DEPRECATED, 'deprecated'),
)
environment = models.ForeignKey('Environment', on_delete=models.CASCADE)
sla = models.ForeignKey(SLA, on_delete=models.CASCADE)
state = models.CharField(max_length=2, choices=SLA_STATE, default=PLANNED)
So my questions are:
Am I generally on the right track, but capturing the last constraint is not possible solely focusing on the model?
What would be an elegant way?

We implemented a save() method for the model EnvironmentSLA that does the following:
check if the object to be saved set state == EFFECTIVE
if not just save the object
try to get currently effective SLA
change the state SLA of the currently effective SLA to DEPRECATED
save the object
The save()-function looks like this:
class EnvironmentSLA(models.Model):
[...]
def save(self, *args, **kwargs):
if self.state != self.EFFECTIVE:
super(EnvironmentSLA, self).save(*args, **kwargs)
return
try:
effective_sla = EnvironmentSLA.objects.filter(environment=self.environment, state=self.EFFECTIVE).get()
except Exception as e:
effective_sla = None
if effective_sla:
effective_sla.state = self.DEPRECATED
effective_sla.save()
super(EnvironmentSLA, self).save(*args, **kwargs)
This way we don't loose the defined SLAs but always have only one active SLA.

Related

FactoryBoy - nested factories / max depth?

I am writing tests for a large Django application, as part of this process I am gradually creating factories for all models of the different apps within the Django project.
However, I've run into some confusing behavior with FactoryBoy where it almost seems like SubFactories have an max depth beyond which no instances are generated.
The error occurs when I try to run the following test:
def test_subfactories(self):
""" Verify that the factory is able to initialize """
user = UserFactory()
self.assertTrue(user)
self.assertTrue(user.profile)
self.assertTrue(user.profile.tenant)
order = OrderFactory()
self.assertTrue(order)
self.assertTrue(order.user.profile.tenant)
The last line will fail (AssertionError: None is not true), running this test through a debugger reveals that indeed order.user.profile.tenant returns None instead of the expected Tenant instance.
There are quite a few factories / models involved here, but the layout is relatively simple.
The User (django default) and the Profile model are linked through a OneToOneField, which (after some trouble) is represented by the UserFactory and ProfileFactory
#factory.django.mute_signals(post_save)
class ProfileFactory(factory.django.DjangoModelFactory):
class Meta:
model = yuza_models.Profile
django_get_or_create = ('user',)
user = factory.SubFactory('yuza.factories.UserFactory')
birth_date = factory.Faker('date_of_birth')
street = factory.Faker('street_name')
house_number = factory.Faker('building_number')
city = factory.Faker('city')
country = factory.Faker('country')
avatar_file = factory.django.ImageField(color='blue')
tenant = factory.SubFactory(TenantFactory)
#factory.django.mute_signals(post_save)
class UserFactory(factory.django.DjangoModelFactory):
class Meta:
model = auth_models.User
username = factory.Sequence(lambda n: "user_%d" % n)
first_name = factory.Faker('first_name')
last_name = factory.Faker('last_name')
email = factory.Faker('email')
is_staff = False
is_superuser = False
is_active = True
last_login = factory.LazyFunction(timezone.now)
#factory.post_generation
def profile(self, create, extracted):
if not create:
return
if extracted is None:
ProfileFactory(user=self)
The TenantFactory below is represented as a SubFactory on the ProfileFactory above.
class TenantFactory(factory.django.DjangoModelFactory):
class Meta:
model = elearning_models.Tenant
name = factory.Faker('company')
slug = factory.LazyAttribute(lambda obj: text.slugify(obj.name))
name_manager = factory.Faker('name')
title_manager = factory.Faker('job')
street = factory.Faker('street_name')
house_number = factory.Faker('building_number')
house_number_addition = factory.Faker('secondary_address')
The Order is linked to a User, but many of its methods call fields of its self.user.profile.tenant
class OrderFactory(factory.DjangoModelFactory):
class Meta:
model = Order
user = factory.SubFactory(UserFactory)
order_date = factory.LazyFunction(timezone.now)
price = factory.LazyFunction(lambda: Decimal(random.uniform(1, 100)))
site_tenant = factory.SubFactory(TenantFactory)
no_tax = fuzzy.FuzzyChoice([True, False])
Again, most of the asserts in the test pass without failing, all separate factories are able to initialize fetch values from their immediate foreignkey relations. However, as soon as factories/models are three steps removed from each other the call will return None instead of the expected Tenant instance.
Since I was unable to find any reference to this behaviour in the FactoryBoy documentation its probably a bug on my side, but so far I've been unable to determine its origin. Does anyone know what I am doing wrong?
post_save method
def create_user_profile(sender, instance, created, **kwargs):
if created:
profile = Profile.objects.create(user=instance)
resume = profile.get_resume()
resume.initialize()
post_save.connect(create_user_profile, sender=User)
As I mentioned in a comment, I've discovered the source of the problem: the post-save method linked to the UserProfile (I've included the code in my post).
This post-save method created a Profile on User creation. I accounted for this signal by using the #factory.django.mute_signals decorater on both the UserFactoryand the ProfileFactory.
I had assumed that any calls on Order.user would trigger the UserFactory which had already been enclosed with the decorator, but this is not assumption proved to be wrong. Only when I applied the decorated to the OrderFactory as well did the tests pass.
Thus the #factory.django.mute_signals decorator should not just be used on factories that are affected by these signals, but also on any factory that is using those factories as a SubFactory!

One table can not be foreign key to others except one table

For example
class Room(models.Model):
visitor = models.ForeignKey(Visitor)
number = models.PositiveIntegerField()
capacity = models.ForeignKey(Capacity, on_delete=models.PROTECT)
floor = models.ForeignKey(Floor, on_delete=models.PROTECT)
price = models.PositiveIntegerField()
is_premium = models.BooleanField(default=False)
is_vip = models.BooleanField(default=False)
expiry_date = models.DateTimeField()
class Meta:
unique_together = ('')
def __str__(self):
return '№{0}'.format(self.number)
class Reserved(models.Model):
room = models.ForeignKey(Room)
begin_date = models.DateTimeField()
def __str__(self):
return 'Reserved Room {0}'.format(self.room)
class Busy(models.Model):
room = models.ForeignKey(Room)
Table Room can not be connected to Tables Reserved and Busy at the same time. Room should be reserved or busy. Is there way put validation for this?
I tried to use unique_together but if for fields of table
Thanks
There is no way to enforce this at DB level nor a simple way to do it in Django level. With your structure you should add some validation before creating (or modifying) both Busy and Reserved. Something like:
class Busy(models.Model):
room = models.ForeignKey(Room)
def __save__(self, *args, **kwargs):
if Reserved.object.filter(room=self.room).exists():
raise RuntimeError('Trying to make a reserved room busy.')
super(Busy, self).__save__(*args, **kwargs)
If you are creating Busy and Reserved objects concurrently it's subject to race condition. I suggest to move room state into Room model itself and add some helper functions (something like in room_manager.py beside models.py) to change its state and make sure related models are created/modified in a consistent manner.
The only way to ensure at the database level that you have one single "status" at a given time for a room is to have your relationship the other way round - with Room have a foreign key on whatever represents the status. To make this work you'll need to use either some form of model inheritance or django's "generic" relationship (which can be handy sometimes but are really not SQL-friendly).
Here's an example using the very simplest form of "model inheritance" (which is not actually inheritance at all):
class Status(models.Model):
BUSY = 1
RESERVED = 2
TYPES = (
(BUSY,"busy"),
(RESERVED,"reserved")
)
type = models.CharField("Status type", max_length=10, choices=TYPES)
# only used for reservations
begin_date = models.DateTimeField(null=True, blank=True)
def save(self, *args, **kw):
# TODO : this should belong to `full_clean()`,
# cf the FineManual model's validation
if self.type == self.RESERVED and not self.begin_date:
raise ValueError("RESERVED statuses need a begin_date")
super(Status, self).save(*args, **kw)
class Room(models.Model):
status = models.ForeignKey(Status)
Note that this allows for a same status to be used for multiple rooms at the same time, which might be a problem too. Using a OneToOneField field instead might help on the Django side but will still be treated as a foreign key at the database level.

Django: Overriding the Save (Either Model or Admin Section)

I've been playing around with Django recently, however I'm stuck on how to approach this problem. I have a 'Person' model which has a one to many relationship with a 'Voucher' Model. Now the person has a quota and each time a Voucher is generated, the quota will decrease by one. What I'm stuck on is doing this through the save method. So how does one do this?
Below are my models:
class Person(AbstractDetail):
# Note: Model to maintain information about Person
vc = models.IntegerField(default = 3, verbose_name = 'Vouchers',
null = False, validators = [ validate_voucher ])
class Voucher(models.Model):
# Note: Model to maintain information about Voucher
vc = models.CharField (max_length = 25, verbose_name = 'Voucher ID',
help_text = 'Voucher Identifier')
ps = models.ForeignKey(Person, on_delete = models.CASCADE,
verbose_name = 'Person')
Don't do it in save(), it's easily messed up with django. Try to use django signal post_save():
from django.db.models.signals import post_save
#receiver(post_save, sender=Voucher)
def decrease_quota(sender, instance, created, *args, **kwargs):
if created:
instance.vc -= 1
instance.save()
Check django doc for django signals.
I'd take a slight performance hit in favour of avoiding the duplication of the voucher count information (call me old-fashioned).
In stead of saving the voucher count in Person.vc, I'd evaluate the voucher count when needed by creating a function for that in the Voucher object manager:
def get_voucher_count(self, person):
return self.filter(ps=person).count()
OK, I manage to work it out (with the help of Shang Wang & Ytsen de Boer answer as well, which I'm grateful for). The way which I did it was to create an instance of the object Person and take away from it there.
Fortunately enough, I quick time check the doc's which Shang just showed me (and look way deeper as well). Below is the way to do it:
#receiver(post_save, sender = Voucher, dispatch_uid = "voucher_identifier")
def decrease_quota(sender, instance, **kwargs):
obj = Person.objects.get(pk = instance.ps.id)
if obj.vc < 4 and obj.vc > 0:
obj.vc = obj.vc - 1
obj.save()
Of course I have to do abit more to it (simple validations and stuff like that), but this how I needed it.

Integer Field Math in Django

from django.db import models
from django.contrib.auth.models import User
class Product(models.Model):
name = models.CharField(max_length = 127)
description = models.TextField()
code = models.CharField(max_length = 30)
lot_no = models.CharField(max_length = 30)
inventory = models.IntegerField()
commited = models.IntegerField()
available = models.IntegerField()
reorder = models.IntegerField()
created_date = models.DateField(auto_now_add = True)
comment_user = models.ForeignKey(User, null=True)
comment_txt = models.TextField()
def __unicode__(self):
return self.code + " - " + self.name + " - " + self.lot_no + " - " + str(self.created_date)
#property
def available(self):
return self.inventory - self.commited
Im trying to have available calculated by (inventory - self) when a person enters in the data for inventory and commited in django admin template. But I'm not sure how.
Thanks,
Jon
Try overriding the save method on the model:
def save(self, *args, **kwargs):
"update number available on save"
self.available = self.inventory - self.committed
super(Product, self).save(*args, **kwargs)
You could also put logic in there that would do something if self.available became negative.
It seems like you may have two problems; the overlapping available property+field and availability not showing up as you expect in the admin.
Choose one way (property or field) to represent the availability and go with it. Don and Seth have shown a way to do it using a field and Daniel and Ignacio have suggested going with a property.
Since you really want this field to show up in the admin just go with the field; give it a helpful help_text="...", remove the #property, and override save().
class Product(models.Model):
# ...
availability = models.IntegerField(help_text="(updated on save)")
# Use Seth's save()
def save(self, *args, **kwargs):
self.availability = self.inventory - self.commited
super(Product, self).save(*args, **kwargs)
This is not the best way to do things in terms of normalized data but it will probably be the simplest solution to your current problem.
If you are using trunk instead of Django-1.1.1, you can also use readonly_fields in the admin.
Don is correct that you have the name available duplicated, because you have both a field and a property. Drop the field.
This is what I said when I gave you the solution to this problem in your original question - I explicitly said "drop the existing 'available' field". Following half a solution is never going to work.
However I fundamentally disagree with Seth and Don who recommend overriding the save() function to calculate this value. That is a totally unnecessary duplication of data. The property is the correct solution.
You've bound both a Django field and a vanilla Python property to the same name on your model. One of these attributes is masking the other, which is why you're getting unexpected behavior in the Django admin. This is almost certainly not what you want.
Override the save method and remove your def available property entirely.
Ignacio is trying to help you keep your data normalized by not storing information in your DB twice. It's a good practice to follow in the general case, but there are many times when you want to store calculated values in your DB. This seems like a practical use of data duplication.
The property is actually removing the available integer field off the admin page it seems
See the Django documentation for model properties

Django manytomany signals? [duplicate]

This question already has answers here:
django manytomanyfield .add() method
(2 answers)
Closed 4 years ago.
Let's say I have such model
class Event(models.Model)
users_count = models.IntegerField(default=0)
users = models.ManyToManyField(User)
How would you recommend to update users_count value if Event add/delete some users ?
If possible in your case, you could introduce Participation model which would join Event and User:
class Participation(models.Model):
user = models.ForeignKey(User)
event = models.ForeignKey(Event)
class Event(models.Model):
users = models.ManyToManyField(User, through='Participation')
And handle pre_save signal sent by Participation to update instance.event counts. It would simplify handling of m2m significantly. And in most cases, it turns out later on that some logic and data fits best in the middle model. If that's not your case, try a custom solution (you should not have many code paths adding Users to Events anyway).
I have a fixed the problem using the built-in signal django.db.models.signals.m2m_changed.
In my case, I have to update a related instance of another model each time the ManyToMany change and as you know, overriding Model.save() does not works.
Here my (french & simplified) models :
class BaseSupport(EuidModel):
nom = models.CharField(max_length=100, blank=True)
periodicite = models.CharField('périodicité', max_length=16,
choices=PERIODICITE_CHOICES)
jours_de_parution_semaine = models.ManyToManyField('JourDeLaSemaine', blank=True)
class Meta:
abstract = True
class Support(BaseSupport):
pass
def save(self, *args, **kwargs):
create_cahier_principal = False
if not self.pk:
create_cahier_principal = True
super(Support, self).save(*args, **kwargs)
if create_cahier_principal:
c = Cahier.objects.create(support=self,ordre=1, numero=1,
nom=self.nom, nom_court=self.nom_court,
euid=self.euid, periodicite=self.periodicite)
class Cahier(BaseSupport):
"""Ex : Cahier Saumon du Figaro Quotidien."""
support = models.ForeignKey('Support', related_name='cahiers')
ordre = models.PositiveSmallIntegerField()
numero = models.PositiveSmallIntegerField(u'numéro', null=True, blank=True)
def sync_m2m_cahier_principal(sender, **kwargs):
if kwargs['action'] not in ('post_add', 'post_clear', 'post_remove'):
return
support = kwargs['instance']
cahier_principal = support.cahiers.get(euid=support.euid)
cahier_principal.jours_de_parution_semaine.clear()
if kwargs['action'] == 'post_clear':
return
for jour in support.jours_de_parution_semaine.all():
cahier_principal.jours_de_parution_semaine.add(jour)
m2m_changed.connect(sync_m2m_cahier_principal,
sender=Support.jours_de_parution_semaine.through)
Maybe this solution is a far from ideal but I hate monkey-patching Django !
Overriding save() may not help you because the update to the M2M is not atomic and happens after the save of the Event instance (I haven't studied the delete() semantics but they are probably similar). This was discussed in another thread.
People are talking about and working on this problem. The best solution I have seen so far is this MonkeyPatch by gregoirecachet. I don't know if this will make it into 1.2 or not. Probably not since the Release Manager (James Bennett) is trying to get people to respect the freeze dates (a major one just passed).

Categories

Resources