Django Model relationship refactor help? - django

I'm sorta having a django / db brainfart.
In words I'm trying to come up with a better way to represent Address objects in my project. I have 5 or 6 different models that have one or more associations to an Address (Billing / Shipping / etc) and I need to allow these addresses to be created/modified/deleted. My first thought was to use the Admin for this since it seems like a natural fit.
However, I can't seem to figure out how to tell the Admin to restrict the visible set of addresses to the specific model in question(Account/Partner/Invoice). I found a really nasty, not viable, and incredibility horrible to maintain way to do it, which I will show below.
How can I do this efficiently (preferably in the Admin)? I am open to going to a m2m relationship which feels much more natural to me, and using a custom view/form, but I wanted to see if I was missing some Admin trick before I went that route. If I go that route I'm thinking I will need to use a GenericRelation so that I don't end up with a ton of lookup tables (one for each different entity).
EDIT: The same address may be used for different models, and for different instances of a particular model, BUT if an address is reused we must track who is using what to maintain independence between models and/or instances. Or in other words in a m2m relationship we can track who is using what with the intermediate table. If we aren't using a lookup table then we need to always copy the Address instance so that both objects have their own copy. (If there is an edit we need to make sure we aren't changing the preexisting relationships of anyone else. In other words edits are actually creates and reassigns in the m2m case.)
Here is an example that should pretty much work as an empty project that sorta shows how I want the address(es) isolated during add/edit/delete, but it also shows how horrible the solution is.
models.py
from django.db import models
class Account(models.Model):
name = models.CharField(max_length=200,blank=True)
#...
def __unicode__(self):
return u"%s - (%s)" %(self.name, self.address_set.all())
class Partner(models.Model):
name = models.CharField(max_length=200,blank=True)
discount = models.DecimalField(max_digits= 3, decimal_places=1, default=0)
#...
def __unicode__(self):
return u"%s - (%s)" %(self.name, self.address_set.all())
class Invoice(models.Model):
invoice_number = models.IntegerField(default=1)
#...
def __unicode__(self):
return u"%s - (%s)" %(self.invoice_number, self.address_set.all())
class Address(models.Model):
street = models.CharField(max_length=200,blank=True)
zip = models.CharField(max_length=10, verbose_name='Zip Code')
account = models.ForeignKey(Account, blank=True, null=True)
partner = models.ForeignKey(Partner, blank=True, null=True)
invoice = models.ForeignKey(Invoice, blank=True, null=True)
type = models.CharField(max_length=25, choices=(('B','Billing'),('S','Shipping')))
class Meta:
unique_together = (('type', 'account' ),
('type', 'partner' ),
('type', 'invoice' ),)
def __unicode__(self):
return "(%s) - %s %s" %(self.get_type_display(), self.street, self.zip)
admin.py
from django.contrib import admin
from relationships.rels.models import Partner, Account, Address, Invoice
class AcctAddrInline(admin.TabularInline):
model = Address
extra = 1
max_num =3
exclude = ('partner', 'invoice')
class PartAddrInline(admin.TabularInline):
model = Address
extra = 1
max_num =3
exclude = ('account', 'invoice')
class InvAddrInline(admin.TabularInline):
model = Address
extra = 1
max_num =2
exclude = ('account', 'partner')
class AccountAdmin(admin.ModelAdmin):
inlines = [AcctAddrInline,]
class PartnerAdmin(admin.ModelAdmin):
inlines = [PartAddrInline,]
class InvoiceAdmin(admin.ModelAdmin):
inlines = [InvAddrInline,]
admin.site.register(Invoice, InvoiceAdmin)
admin.site.register(Partner, PartnerAdmin)
admin.site.register(Account, AccountAdmin)
admin.site.register(Address)

EDIT: The same address may be used for different models, and for different instances of a particular model, BUT if an address is reused we must track who is using what to maintain independence between models and/or instances
It looks like you want to use COW pattern for addresses, but I don't think it plays nice with the whole idea of database integrity.
If you just want to separate account addresses from invoice addresses I'd suggest to use Multi-table model inheritance. This way you will have several sets of addresses while being able to browse all addresses at once.
Here's an example.
models.py
from django.db import models
class Account(models.Model):
name = models.CharField(max_length=200, blank=True)
def __unicode__(self):
return u"%s - (%s)" % (self.name, self.address_set.all())
class Partner(models.Model):
name = models.CharField(max_length=200, blank=True)
discount = models.DecimalField(max_digits= 3, decimal_places=1, default=0)
def __unicode__(self):
return u"%s - (%s)" % (self.name, self.address_set.all())
class Invoice(models.Model):
invoice_number = models.IntegerField(default=1)
def __unicode__(self):
return u"%s - (%s)" % (self.invoice_number, self.address_set.all())
class Address(models.Model):
street = models.CharField(max_length=200, blank=True)
zip = models.CharField(max_length=10, verbose_name='Zip Code')
def __unicode__(self):
return "%s %s" % (self.street, self.zip)
class AccountAddress(Address):
account = models.ForeignKey(Account, related_name='address_set')
class InvoiceAddress(Address):
invoice = models.ForeignKey(Invoice, related_name='address_set')
class PartnerAddress(Address):
partner = models.ForeignKey(Partner, related_name='address_set')
admin.py
from django.contrib import admin
# Wildcard import used for brevity
from relationships.rels.models *
class AccountAddressInline(admin.TabularInline):
model = AccountAddress
extra = 1
max_num = 3
class PartnerAddressInline(admin.TabularInline):
model = PartnerAddress
extra = 1
max_num = 3
class InvoiceAddressInline(admin.TabularInline):
model = InvoiceAddress
extra = 1
max_num = 3
class AccountAdmin(admin.ModelAdmin):
inlines = [AccountAddressInline,]
class PartnerAdmin(admin.ModelAdmin):
inlines = [PartnerAddressInline,]
class InvoiceAdmin(admin.ModelAdmin):
inlines = [InvoiceAddressInline,]
admin.site.register(Account, AccountAdmin)
admin.site.register(Partner, PartnerAdmin)
admin.site.register(Invoice, InvoiceAdmin)
admin.site.register(AccountAddress)
admin.site.register(InvoiceAddress)
admin.site.register(PartnerAddress)
# Uncomment if you want to browse all addresses available at once
# admin.site.register(PartnerAddress)
Note the related_name='address_set' hack. I don't know why, but it is the only way inline editing works when using foreign key from an inherited model. It seems that it's a bug in Django similar to (but rather with a reversed use-case) #11120 and #11121.

I, personally, would put foreign keys to your Address model on Account, Partner and Invoice instead of having Address aware of what it is the address to. That, MAY, solve your problem.

Related

How to store multiple objects as a list in Django model fields? [duplicate]

I want to store some additional information in that, automatically created, ManyToMany join-table. How would I do that in Django?
In my case I have two tables: "Employees" and "Projects". What I want to store is how much each of the employees receives per hour of work in each of the projects, since those values are not the same. So, how would I do that?
What occurred to me was to, instead of the method "ManyToManyField", create explicitly a third class/table to store those new informations and to set its relationship with "Employees" and "Projects" using the "ForeignKey" method. I'm pretty sure it will work, but is this the best approach?
Here is example of what you want to achieve:
http://docs.djangoproject.com/en/dev/topics/db/models/#extra-fields-on-many-to-many-relationships
In case link ever breaks:
from django.db import models
class Person(models.Model):
name = models.CharField(max_length=128)
def __str__(self): # __unicode__ on Python 2
return self.name
class Group(models.Model):
name = models.CharField(max_length=128)
members = models.ManyToManyField(Person, through='Membership')
def __str__(self): # __unicode__ on Python 2
return self.name
class Membership(models.Model):
person = models.ForeignKey(Person)
group = models.ForeignKey(Group)
date_joined = models.DateField()
invite_reason = models.CharField(max_length=64)

Syntax to reverse-query a cached queryset

I have the following 3 models related by Foreign Key as following:
class Seller(models.Model):
name = models.CharField(max_length=20)
def __str__(self):
return self.name
class Genre(models.Model):
seller= models.ForeignKey(Seller, related_name="genre", on_delete=models.CASCADE)
name = models.CharField(max_length=20)
def __str__(self):
return self.name
class Book(models.Model):
genre= models.ForeignKey(Genre, related_name="book", on_delete=models.CASCADE)
name = models.CharField(max_length=20)
def __str__(self):
return self.name
And I want to retrieve the whole 3 tables in one database query, by querying the Seller objects, as following:
sellers = Seller.objects.select_related('genre', 'book').all().values('name')
seller_df = pd.DataFrame(list(sellers))
What is the syntax to filter for books carried by a particular seller, without hitting the database again (by utilizing either the Seller queryset or the pandas seller_df)
seller1 = seller_df ['name'].iloc[0]
seller1_books = Book.objects.filter(...)
seller_last = seller_df ['name'].iloc[-1]
seller_last_books = Book.objects.filter(...)
I dont know so mach about caching but I know something that you like:
We use select_related when the object is single like onetoone or fk.
.
for many to many or reverse fk like your example use prefetch_related

Many to Many in two directions

I have a friend that requested something that I was thinking would be simple and quick. It never turns out that way. Quick disclaimer, model design is a krux of mine. I often spend too long perfecting it only to have to rework it several times. Anyway, here is the current state of my model. For everything, it works, except when creating 'raids'.
from django.db import models
# Create your models here.
class PlayerRole(models.Model):
"""
PlayerRole Model
"""
role = models.CharField(max_length=20)
# this function will be invoked when this model object is foreign key of other model(for example Employee model.).
def __str__(self):
return self.role
# this is a inner class which is used to define unique index columns. You can specify multiple columns in a list or tuple.
class Meta:
unique_together = ['role']
class PlayerClass(models.Model):
"""
PlayerClass Model
"""
name = models.CharField(max_length=100)
color = models.CharField(max_length=6)
# this function will be invoked when this model object is foreign key of other model(for example Employee model.).
def __str__(self):
return self.name
# this is a inner class which is used to define unique index columns. You can specify multiple columns in a list or tuple.
class Meta:
unique_together = ['name']
class Player(models.Model):
"""
Player Model
"""
name = models.CharField(max_length=100)
playerclass = models.ForeignKey(PlayerClass, on_delete=models.CASCADE, blank=True, null=True)
playerrole = models.ForeignKey(PlayerRole, on_delete=models.CASCADE, blank=True, null=True)
value = models.IntegerField(default=0)
reliability = models.IntegerField(default=0)
last_drop = models.DateField(auto_now=False, blank=True, null=True)
last_raid_attended = models.DateField(auto_now=False, blank=True, null=True)
last_boss_attended = models.DateField(auto_now=False, blank=True, null=True)
drop_received = models.BooleanField(default=False)
note = models.TextField(null=True, blank=True)
core_raider = models.BooleanField(default=False)
enabled = models.BooleanField(default=True)
# this function will be invoked when this model object is foreign key of other model(for example Employee model.).
def __str__(self):
return self.name
# this is a inner class which is used to define unique index columns. You can specify multiple columns in a list or tuple.
class Meta:
unique_together = ['name']
class WoWInstance(models.Model):
"""
Instance Model
"""
name = models.CharField(max_length=100)
# this function will be invoked when this model object is foreign key of other model(for example Employee model.).
def __str__(self):
return self.name
# this is a inner class which is used to define unique index columns. You can specify multiple columns in a list or tuple.
class Meta:
unique_together = ['name']
class Boss(models.Model):
"""
Boss Model
"""
name = models.CharField(max_length=100)
instance = models.ForeignKey(WoWInstance, on_delete=models.CASCADE)
# this function will be invoked when this model object is foreign key of other model(for example Employee model.).
def __str__(self):
return self.name
# this is a inner class which is used to define unique index columns. You can specify multiple columns in a list or tuple.
class Meta:
unique_together = ['name']
class Raid(models.Model):
"""
Raid Model
"""
date = models.DateTimeField(auto_now_add=True)
boss = models.ForeignKey(Boss, on_delete=models.CASCADE, null=True, blank=True)
success = models.BooleanField()
attendees = models.ManyToManyField(Player)
# this function will be invoked when this model object is foreign key of other model(for example Employee model.).
def __str__(self):
return str(self.date) + ' + ' + self.boss.name
# this is a inner class which is used to define unique index columns. You can specify multiple columns in a list or tuple.
class Meta:
unique_together = ['boss']
The idea here is simply to track attendance on each attempt at fighting something in a game. In the most general and simple sense - Many Raids can contain many of the same Bosses. Many Raids can contain many of the same Players.
Now, I thought that because Django automatically creates ID fields, this method wouldn't be an issue. But, it seems not to be the case. If I create a raid with the same Boss more than once, I get...
Raid with this Boss already exists.
What am I missing here? Since all Raid and Boss are unique, shouldn't the two never overlap?
If I create a raid with the same Boss more than once, I get...
Raid with this Boss already exists.
Well that is due to the unique_together constraint:
class Raid(models.Model):
# ...
class Meta:
unique_together = ['boss']
If you write unique_together, it means you enforce a uniqness constraint on a combination of fields. But since you here have mentioned only one field ('boss'), you thus add a unique=True constraint on that specific field.
It thus means that no two Raids can exist with the same boss field. You probably want to remove that.
Since all Raid and Boss are unique, shouldn't the two never overlap?
The Raids and Bosses are already unique. An object does not belong to two models at once (unless one is the subclass of another, but let us ignore that case). You even made the name unique as well (although you better do that by adding a unique=True parameter to the name fields). So there is no need at all to make the 'boss' unique here, since you clearly do not want that.

Django: How to filter foreign key in admin detail view

core/models.py
from django.db import models
from django.db.models.signals import post_save
class Person(models.Model):
first_name = models.CharField(max_length=30)
last_name = models.CharField(max_length=30)
middle_name = models.CharField(max_length=30)
class Company(models.Model):
name = models.CharField(max_length=100)
class Entity(models.Model):
is_person = models.BooleanField(default=True)
person = models.ForeignKey(Person, on_delete=models.PROTECT, null=True)
company = models.ForeignKey(Company, on_delete=models.PROTECT, null=True)
name = models.CharField(max_length=30)
def __str__(self):
return self.name
#property
def title(self):
return self.name
class Meta:
verbose_name_plural = 'entities'
def post_save_person_receiver(sender, instance, created, *args, **kwargs):
if created:
entity, is_created = Entity.objects.get_or_create(is_person=True, person=instance, company=None, name=instance.last_name) # noqa
post_save.connect(post_save_person_receiver, sender=Person)
def post_save_company_receiver(sender, instance, created, *args, **kwargs):
if created:
entity, is_created = Entity.objects.get_or_create(is_person=False, person=None, company=instance, name=instance.short_name) # noqa
post_save.connect(post_save_company_receiver, sender=Company)
class Group(models.Model):
name = models.CharField(max_length=20)
is_individual = models.BooleanField(default=True)
members = models.ManyToManyField(Entity, through='Membership')
class Membership(models.Model):
group = models.ForeignKey(Group, on_delete=models.PROTECT, null=False)
entity = models.ForeignKey(Entity, on_delete=models.PROTECT, null=False)
class Meta:
unique_together = ("entity", "group")
For every Company and Person created, an Entity is automatically created where Entity.is_person=True if it's a Person. An Entity can then become a member of a Group such as 'Employee', 'Supplier' and 'Customer' through a ManyToMany relationship in the Membership Model.
How do I filter Membership.entity in Admin View (for add and update) that when the Group selected is an 'is_individual=True', such as 'Employee', Entity Field only shows 'is_person=True' Persons in the Entity combobox?
Admin View
I would consider writing your own view for this. In my opinion, Djangos admin is not a good site to base a frontend on. I use it only for making quick changes where validation doesn't need to be done as much because the users of admin should know what they're doing. When you start adding more responsive stuff, that's when I consider using another view and starting from scratch (or a framework) but a different view none-the-less.
There are several different names for what it sounds like you want: dependent dropdowns, chained fields, etc. Django itself doesn't have anything out of the box for this.
Your 2 options are: (1) do it yourself or (2) use a 3rd-party package.
As far as doing it yourself goes, you're going to need to do some work in JS for making it work on the frontend, and you're probably going to need some kind of flexible view that outputs JSON data, and you're going to need some kind of custom select field to handle this.
For using a 3rd-party package, I don't see anything particularly recent that does this. Here's one that's a few years old: https://github.com/runekaagaard/django-admin-flexselect
Hope this helps some!

Django: Create model instance within model

I have another probably quite basic Django question. I am writing a database app for my classes, and I would like to be able to set the current year from within the app. The idea behind this is that I can manually switch to the next academic year when all the marking etc is done. The function to switch the current year is then supposed to do other things as well - put the students in the new default courses etc.
So I created a model called MetaData:
class MetaData(models.Model):
data_id = models.IntegerField(default = 1, unique = True)
current_year = models.IntegerField(choices=ACADEMIC_YEARS)
It would be great if I could access the current_year in other models, for example to set it as default when I want to enter new courses/students. I tried this:
class Course(models.Model):
try:
meta = MetaData.objects.get(data_id=1)
current_year = meta.current_year
except MetaData.DoesNotExist:
current_year = 2010
year = models.IntegerField(choices=ACADEMIC_YEARS, default=current_year)
Unfortunately, I get the error "No such table: database_metadata" when I run syncdb for the first time - I would guess because syncdb has not created the tables when it tests the code.
Is there a better solution for this?
Either put the constant inside your code (if you could hard code the logic of computing academic years. Or always remember to change it manually, which sounds OK as you've already hard-coded ACADEMIC_YEARS), or in DB as danodonovan suggests. Or model academic years directly w/o storing a constant in MetaData
from django.core.exceptions import ValidationError
class AcademicYear(models.Model):
start = models.DateField() # or simply year
end = models.DateField()
is_current = models.BooleanField()
class Meta:
ordering = ('-is_current', 'start')
def clean(self): # only allow at most one current year
if self.is_current and \
self.__class__.objects.exclude(pk=self.pk).filter(is_current=True).exists():
raise ValidationError('Found another `current` academic year.'
def __unicode__(self):
return 'blahblah'
# then in Course
class Course(models.Model):
academic_year = models.ForeignKey(AcademicYear)
Thus in the Admin, you could easily step up AcademicYears and display them as choices of the academic_year field of the Course.
If you still would like to use MetaData, just change the current_year to be a function:
def get_current_year():
try:
return MetaData.objects.get(data_id=1).current_year
except MetaData.DoesNotExist:
return 2010
class Course(models.Model):
year = models.IntegerField(choices=ACADEMIC_YEARS, default=get_current_year)
This sounds like you need to use the ForeignKey field to link your models.
class MetaData(models.Model):
current_year = models.IntegerField(choices=ACADEMIC_YEARS)
class Course(models.Model):
year = models.ForeignKey(MetaData, default=this_year)
def this_year(self):
return MetaData.objects.get(current_year=datetime.now().year)
To set the default year I've added a this_year function to the MetaData model. The Course models year attribute will call this when setting the default year.
Having said that, it would be much simpler if you used a DateField for simple dates.
class Boat_Owner(models.Model):
username = models.CharField(max_length=200)
password = models.CharField(max_length=200)
name = models.CharField(max_length=200,unique=True,
verbose_name="Boat Owner Name")
address = models.CharField(max_length=200)
phone = models.CharField(max_length=200)
email = models.EmailField('email address',
unique=True, db_index=True)
avatar = models.ImageField('profile picture',
upload_to='static/media/images/avatars/',
null=True, blank=True)
def set_avatar(self):
self.has_picture = True
def __str__(self):
return u'%s %s %s %s %s %s' % (self.id,self.name,self.username,self.password,self.address,self.email)
class Meta:
verbose_name = "Boat Owner"
verbose_name_plural = "Boat Owner"