How to structure models in Django? - python

I'm working on a project using Python(3.7) and Django(3) in which I have to create some models to store reports. There are 4 different models to represents each type of report with only 2 common fields(RequestId, InstCode) but the rest of the fields are different. In the end, I have to display the 10 recent reports (mixed from all models) on the home page.
Here's how I have implemented my models at the moment:
From models.py:
class DistributionReport(models.Model):
RequestId = models.CharField(max_length=256, blank=False, default=0)
InstCode = models.CharField(max_length=3, blank=False)
Currency = models.CharField(max_length=3, blank=False)
Denomination = models.IntegerField(blank=False, default=0)decimal_places=2)
date = models.DateField(default=datetime.date.today)
class Meta:
verbose_name = 'Distribution Report'
def __str__(self):
return self.RequestId
class ExpenditureReport(models.Model):
RequestId = models.CharField(max_length=256, blank=False, default=0)
InstCode = models.CharField(max_length=3, blank=False)
StaffExpenditure = models.DecimalField(max_digits=12, decimal_places=2)
Month = models.IntegerField(blank=False, default=0)
Quarter = models.IntegerField(blank=False, default=0)
Year = models.IntegerField(blank=False, default=0)
class Meta:
verbose_name = 'Expenditure Report'
def __str__(self):
return self.RequestId
class StorageReport(models.Model):
RequestId = models.CharField(max_length=256, blank=False, default=0)
InstCode = models.CharField(max_length=3, blank=False)
Currency = models.CharField(max_length=5, blank=False)
Denomination = models.IntegerField(blank=False, default=0)
date = models.DateField(default=datetime.date.today)
class Meta:
verbose_name = 'Processing Report'
def __str__(self):
return self.RequestId
class AssetsReport(models.Model):
RequestId = models.CharField(max_length=256, blank=False, default=0)
InstCode = models.CharField(max_length=3, blank=False)
AssetClassificationId = models.IntegerField(blank=False, default=0)
VaultCapacity = models.DecimalField(max_digits=10, decimal_places=2)
Year = models.IntegerField(blank=False, default=0)
HalfOftheYear = models.IntegerField(blank=False, default=0)
class Meta:
verbose_name = 'Assets Report'
def __str__(self):
return self.RequestId
What is the best way to structure these models, so I can query the recent reports ( It can be of any type)?

Look into how normalization works, since all models have 2 common fields lets say you have a table called Report and it has the fields RequestId and InstCode. Now all your other models can be said to be in a XYZReport is a Report kind of relationship.
You can achieve this using a OneToOne Field like so:
class Report(models.Model):
RequestId = models.CharField(max_length=256, blank=False, default=0)
InstCode = models.CharField(max_length=3, blank=False)
DISTRIBUTION = 'DI'
EXPENDITURE = 'EX'
STORAGE = 'ST'
ASSETS = 'AS'
REPORT_TYPE_CHOICES = [
(DISTRIBUTION, 'Distribution Report'),
(EXPENDITURE, 'Expenditure Report'),
(STORAGE, 'Storage Report'),
(ASSETS, 'Assets Report'),
]
report_type = models.CharField(
max_length=2,
choices=REPORT_TYPE_CHOICES,
default=DISTRIBUTION,
)
# Any more common fields
class DistributionReport(models.Model):
report = models.OneToOneField(
Report,
on_delete=models.CASCADE,
related_name = 'distribution_report'
)
# Other fields
# Other Report models in similar fashion
Now whenever making an object of any kind of report also make an object of Report and assign it to report attribute of the models. Save both the models, also to figure out what kind of report an instance of Report is add the relevant report_type to the report instance like so:
report.report_type = Report.DISTRIBUTION # In case of Distribution Report
To figure out whether an instance of Report is of a particular type:
report.report_type == report.DISTRIBUTION # will get whether Report is a DistributionReport
To get the specific types object use the related_name set in the OneToOne Field:
distribution_report = report.distribution_report

Related

Django Relational managers

I was trying to delete my Apllication model:
class Application(models.Model):
app_type = models.ForeignKey(ApplicationCategory, on_delete=models.CASCADE, related_name='applications')
fio = models.CharField(max_length=40)
phone_number = models.CharField(max_length=90)
organisation_name = models.CharField(max_length=100, null=True, blank=True)
aid_amount = models.PositiveIntegerField()
pay_type = models.CharField(max_length=1, choices=PAY_CHOICES, default=PAY_CHOICES[0][0])
status = models.ForeignKey(AppStatus, on_delete=models.CASCADE, related_name='applications', null=True, blank=True)
created = models.DateTimeField(auto_now_add=True)
benefactor = models.ForeignKey(Benefactor, on_delete=models.CASCADE, related_name='applications', null=True)
def __str__(self):
return f"id={self.id} li {self.fio} ning mablag\'i!"
and this was my Benefactor model:
class Benefactor(models.Model):
fio = models.CharField(max_length=255)
phone_number = models.CharField(max_length=9)
image = models.ImageField(upload_to='media/')
sponsory_money = models.IntegerField()
organisation_name = models.CharField(max_length=55, null=True, blank=True)
def __str__(self):
return f"{self.fio}"
But I got the below message on superAdmin Panel:
TypeError at /admin/api/benefactor/
create_reverse_many_to_one_manager.\<locals\>.RelatedManager.__call__() missing 1 required keyword-only argument: 'manager'
I would expect delete smoothly!!
Your Benefactor model has several ForeignKey relationships that share the related_name. Give each a unique name and rerun your migrations.

django - Runpython function to turn charfield into foreignkey

I've been strugglin to relate a csv imported data model with a spatial data model based on a CharField.
I've created both models and now im trying to transfer the data from one field to a new one to be the ForeignKey field. I made a Runpython funtion to apply on the migration but it goives the an error:
ValueError: Cannot assign "'921-5'":
"D2015ccccccc.rol_fk" must be a "D_Base_Roles" instance.
Here are the models:
class D_Base_Roles(models.Model):
predio = models.CharField(max_length=254)
dest = models.CharField(max_length=254)
dir = models.CharField(max_length=254)
rol = models.CharField(primary_key=True, max_length=254)
vlr_tot = models.FloatField()
ub_x2 = models.FloatField()
ub_y2 = models.FloatField()
instrum = models.CharField(max_length=254)
codzona = models.CharField(max_length=254)
nomzona = models.CharField(max_length=254)
geom = models.MultiPointField(srid=32719)
def __str__(self):
return str(self.rol)
class Meta():
verbose_name_plural = "Roles"
class D2015ccccccc(models.Model):
id = models.CharField(primary_key=True, max_length=80)
nombre_archivo = models.CharField(max_length=180, blank=True, null=True)
derechos = models.CharField(max_length=120, blank=True, null=True)
dir_calle = models.CharField(max_length=120, blank=True, null=True)
dir_numero = models.CharField(max_length=120, blank=True, null=True)
fecha_certificado = models.CharField(max_length=50, blank=True, null=True)
numero_certificado = models.CharField(max_length=50, blank=True, null=True)
numero_solicitud = models.CharField(max_length=50, blank=True, null=True)
rol_sii = models.CharField(max_length=50, blank=True, null=True)
zona_prc = models.CharField(max_length=120, blank=True, null=True)
##NEW EMPTY FOREIGNKEY FIELD
rol_fk = models.ForeignKey(D_Base_Roles, on_delete=models.CASCADE, blank=True, null=True)
def __str__(self):
return str(self.numero_certificado)
class Meta:
managed = True
#db_table = 'domperm2015cip'
verbose_name_plural = "2015 Certificados Informaciones Previas"
ordering = ['numero_certificado']
The Runpython function:
def pop_rol(apps, schema_editor):
roles = apps.get_model('b_dom_edificacion', 'D2015ccccccc')
for r in roles.objects.all():
rol = roles
r.rol_fk = r.rol_sii
r.save()
D_Base_Roles.rol values are all unique, and 921-5 is one of those values.
What am I missing?
You probably need to assign an object, not a string. Change the line
r.rol_fk = r.rol_sii
to
r.rol_fk = D_Base_Roles.objects.get(rol=r.rol_sii)
Maybe adjust to whatever the correct field for looking up D_Base_Roles instances is.
Note: this will make a database query for every iteration of the for-loop

Django - conditional foreign key

I have the following 4 models in my program - User, Brand, Agency and Creator.
User is a superset of Brand, Agency and Creator.
A user can be a brand, agency or creator. They cannot take on more than one role. Their role is defined by the accountType property. If they are unset (i.e. 0) then no formal connection exists.
How do I express this in my model?
User model
class User(models.Model):
id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
email = models.EmailField(max_length=255, null=True, default=None)
password = models.CharField(max_length=255, null=True, default=None)
ACCOUNT_CHOICE_UNSET = 0
ACCOUNT_CHOICE_BRAND = 1
ACCOUNT_CHOICE_CREATOR = 2
ACCOUNT_CHOICE_AGENCY = 3
ACCOUNT_CHOICES = (
(ACCOUNT_CHOICE_UNSET, 'Unset'),
(ACCOUNT_CHOICE_BRAND, 'Brand'),
(ACCOUNT_CHOICE_CREATOR, 'Creator'),
(ACCOUNT_CHOICE_AGENCY, 'Agency'),
)
account_id = models.ForeignKey(Brand)
account_type = models.IntegerField(choices=ACCOUNT_CHOICES, default=ACCOUNT_CHOICE_UNSET)
class Meta:
verbose_name_plural = "Users"
def __str__(self):
return "%s" % self.email
Brand model
class Brand(models.Model):
id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
name = models.CharField(max_length=255, null=True, default=None)
brand = models.CharField(max_length=255, null=True, default=None)
email = models.EmailField(max_length=255, null=True, default=None)
phone = models.CharField(max_length=255, null=True, default=None)
website = models.CharField(max_length=255, null=True, default=None)
class Meta:
verbose_name_plural = "Brands"
def __str__(self):
return "%s" % self.brand
Creator model
class Creator(models.Model):
id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
first_name = models.CharField(max_length=255, null=True, default=None)
last_name = models.CharField(max_length=255, null=True, default=None)
email = models.EmailField(max_length=255, null=True, default=None)
youtube_channel_username = models.CharField(max_length=255, null=True, default=None)
youtube_channel_url = models.CharField(max_length=255, null=True, default=None)
youtube_channel_title = models.CharField(max_length=255, null=True, default=None)
youtube_channel_description = models.CharField(max_length=255, null=True, default=None)
photo = models.CharField(max_length=255, null=True, default=None)
youtube_channel_start_date = models.CharField(max_length=255, null=True, default=None)
keywords = models.CharField(max_length=255, null=True, default=None)
no_of_subscribers = models.IntegerField(default=0)
no_of_videos = models.IntegerField(default=0)
no_of_views = models.IntegerField(default=0)
no_of_likes = models.IntegerField(default=0)
no_of_dislikes = models.IntegerField(default=0)
location = models.CharField(max_length=255, null=True, default=None)
avg_views = models.IntegerField(default=0)
GENDER_CHOICE_UNSET = 0
GENDER_CHOICE_MALE = 1
GENDER_CHOICE_FEMALE = 2
GENDER_CHOICES = (
(GENDER_CHOICE_UNSET, 'Unset'),
(GENDER_CHOICE_MALE, 'Male'),
(GENDER_CHOICE_FEMALE, 'Female'),
)
gender = models.IntegerField(choices=GENDER_CHOICES, default=GENDER_CHOICE_UNSET)
class Meta:
verbose_name_plural = "Creators"
def __str__(self):
return "%s %s" % (self.first_name,self.last_name)
Agency model
class Agency(models.Model):
id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
name = models.CharField(max_length=255, null=True, default=None)
agency = models.CharField(max_length=255, null=True, default=None)
email = models.EmailField(max_length=255, null=True, default=None)
phone = models.CharField(max_length=255, null=True, default=None)
website = models.CharField(max_length=255, null=True, default=None)
class Meta:
verbose_name_plural = "Agencies"
def __str__(self):
return "%s" % self.agency
Update:
So I've whittled it down to this bit here in the model:
ACCOUNT_CHOICE_UNSET = 0
ACCOUNT_CHOICE_BRAND = 1
ACCOUNT_CHOICE_CREATOR = 2
ACCOUNT_CHOICE_AGENCY = 3
ACCOUNT_CHOICES = (
(ACCOUNT_CHOICE_UNSET, 'Unset'),
(ACCOUNT_CHOICE_BRAND, 'Brand'),
(ACCOUNT_CHOICE_CREATOR, 'Creator'),
(ACCOUNT_CHOICE_AGENCY, 'Agency'),
)
account_type = models.IntegerField(choices=ACCOUNT_CHOICES, default=ACCOUNT_CHOICE_UNSET)
limit = models.Q(app_label='api', model='Brand') | \
models.Q(app_label='api', model='Creator') | \
models.Q(app_label='api', model='Agency')
content_type = models.ForeignKey(ContentType, limit_choices_to=get_content_type_choices(), related_name='user_content_type')
content_object = GenericForeignKey('content_type', 'email')
If account_type = 1 then link to brand model
If account_type = 2 then link to creator model
If account_type = 3 then link to agency model
How do I accomplish this? Getting this error:
File "/Users/projects/adsoma-api/api/models.py", line 7, in <module>
class User(models.Model):
File "/Users/projects/adsoma-api/api/models.py", line 28, in User
content_type = models.ForeignKey(ContentType, limit_choices_to=get_content_type_choices(), related_name='user_content_type')
NameError: name 'get_content_type_choices' is not defined
Have you tried exploring Django's GenericForeignKey field?
class User(models.Model):
...
limit = models.Q(app_label='your_app_label', model='brand') |
models.Q(app_label='your_app_label', model='creator') |
models.Q(app_label='your_app_label', model='agency')
content_type = models.ForeignKey(ContentType, limit_choices_to=limit, related_name='user_content_type')
object_id = models.UUIDField()
content_object = GenericForeignKey('content_type', 'object_id')
You can access the User's brand/creator/agency by using the following notation:
User.objects.get(pk=1).content_object
This will be an instance of Brand/Creator/Agency as per the content_type.
https://docs.djangoproject.com/en/stable/ref/contrib/contenttypes/#django.contrib.contenttypes.fields.GenericForeignKey
Update based on your comment
Re 1: Using email:
class User(models.Model):
...
email = models.EmailField(max_length=255, unique=True)
limit = models.Q(app_label='your_app_label', model='brand') |
models.Q(app_label='your_app_label', model='creator') |
models.Q(app_label='your_app_label', model='agency')
content_type = models.ForeignKey(ContentType, limit_choices_to=get_content_type_choices, related_name='user_content_type')
content_object = GenericForeignKey('content_type', 'email')
Note: Email can not be a nullable field anywhere if you follow this approach! Also this approach seems hacky/wrong since the email field is now declared in multiple places; and the value can change if you re-assign the content objects. It is much cleaner to link the GenericForeignKey using the discrete UUIDField on each of the other three models
Re 2: Using account_type field:
ContentType is expected to be a reference to a Django Model; therefore it requires choices that are Models and not integers.
The function of limit_choices_to is to perform a filtering such that all possible models are not surfaced as potential GenericForeignKey
class ContentType(models.Model):
app_label = models.CharField(max_length=100)
model = models.CharField(_('python model class name'), max_length=100)
objects = ContentTypeManager()
However, limit_choices_to does accept callables; so you can write a helper method that translates your account_type to the correct Model
I am not clear about how this transaltion should work; so I leave that to you.
Here is how I solve it, override the save method check your condition
tested on Django 3.2
CUSTOMER = [
('customer', 'customer'),
('supplier', 'supplier'),
]
class Customer(models.Model):
name = models.CharField(max_length=256, null=False, blank=False)
user_type = models.CharField(max_length=32, choices=CUSTOMER)
class SupplierOrder(models.Model):
price = models.FloatField(default=0)
supplier = models.ForeignKey(Customer, related_name='supplier', on_delete=models.CASCADE)
def save(self, *args, **kwargs):
supplier = get_object_or_404(Customer, id=self.supplier.id)
if supplier.user_type != 'supplier':
raise ValueError('selected user must be supplier')
super().save(*args, **kwargs)

Pk fields always ends up in resulting query GROUP BY clause

I have my model hierarchy defined as follows:
class Meal(models.Model):
created_at = models.DateTimeField(auto_now_add=True)
discount_price = models.DecimalField(blank=False, null=False, decimal_places=2, max_digits=4)
normal_price = models.DecimalField(blank=True, null=True, decimal_places=2, max_digits=4)
available_count = models.IntegerField(blank=False, null=False)
name = models.CharField(blank=False, null=False, max_length=255)
active = models.BooleanField(blank=False, null=False, default=True)
class Order(models.Model):
created_at = models.DateTimeField(auto_now_add=True)
number = models.CharField(max_length=64, blank=True, null=True)
buyer_phone = models.CharField(max_length=32, blank=False, null=False)
buyer_email = models.CharField(max_length=64, blank=False, null=False)
pickup_time = models.DateTimeField(blank=False, null=False)
taken = models.BooleanField(blank=False, null=False, default=False)
class OrderItem(models.Model):
created_at = models.DateTimeField(auto_now_add=True)
order = models.ForeignKey(Order, on_delete=models.CASCADE, related_name='items')
meal = models.ForeignKey(Meal, on_delete=models.CASCADE)
amount = models.IntegerField(blank=False, null=False, default=1)
I'm trying to get some statistics about orders and I came up with django orm call that looks like this:
queryset.filter(created_at__range=[date_start, date_end])\
.annotate(price=Sum(F('items__meal__discount_price') * F('items__amount'), output_field=DecimalField()))
.annotate(created_at_date=TruncDate('created_at'))\
.annotate(amount=Sum('items__amount'))\
.values('created_at_date', 'price', 'amount')
The above however doesn't give me the expected results, because for some reason the id column still ends up in the GROUP BY clause of sql query. Any help with that?
To make it work I had to do the following:
qs.filter(created_at__range=[date_start, date_end])\
.annotate(created_at_date=TruncDate('created_at'))\
.values('created_at_date')\
.annotate(price=Sum(F('items__meal__discount_price') * F('items__amount'),
output_field=DecimalField()))
.annotate(amount=Sum('items__amount'))
Which kind of makes sense - I pull only the created_at field, transform it and then annotate the result with two other fields.

Django limit_choices_to on OneToOneField with upstream ForeignKey field

I am trying to use the "limit_choices_to" functionality in a Django OneToOneField where upstream of what I want to limit the choices on is another ForeignKey. The error I get in the admin with the way I have it set up is:
invalid literal for int() with base 10: 'Storage Array'
I assume this is because it is looking at the actual value in the column asset_type which is an integer foreign key. I need to be able to limit the choices based on a field value of the foreign key as opposed to the key value itself.
Basically what I am trying to accomplish is having the admin area (and other forms) only allow you to choose a valid asset type when adding a new asset. For example, if I am adding a "storage array" the upstream asset tied to it should only be allowed to be asset_type of storage array.
Here is my model:
from django.db import models
from localflavor.us.us_states import STATE_CHOICES
# Table of brand names of assets
class Brand(models.Model):
brand = models.CharField(max_length=128, unique=True)
def __unicode__(self):
return self.brand
# Table of device types for assets, e.g. "Storage Array"
class Device(models.Model):
device_type = models.CharField(max_length=128, unique=True)
device_url_slug = models.SlugField(max_length=70, unique=True)
class Meta:
verbose_name = "device type"
verbose_name_plural = "device types"
def __unicode__(self):
return self.device_type
# Table of asset locations
class Location(models.Model):
site_name = models.CharField(max_length=128, unique=True)
site_nick = models.CharField(max_length=5, unique=True)
address_line_one = models.CharField(max_length=256)
address_line_two = models.CharField(max_length=256, null=True, blank=True)
address_city = models.CharField(max_length=32)
address_state = models.CharField(max_length=2, choices=STATE_CHOICES, null=True, blank=True)
address_zip = models.CharField(max_length=5)
def __unicode__(self):
return self.site_name
# Table of Environments, e.g. "Production"
class Environment(models.Model):
environment = models.CharField(max_length=128, unique=True)
def __unicode__(self):
return self.environment
class Credentials(models.Model):
AUTH_CHOICES = (
('SSH', 'Standard SSH'),
('SSH-Key', 'SSH with Key-based login'),
('HTTP', 'Standard HTTP'),
('HTTPS', 'Secure HTTP'),
('API', 'API Based'),
('SNMP', 'SNMP Based'),
)
SNMP_VERSIONS = (
('v1', 'SNMP v1'),
('v3', 'SNMP v3'),
)
auth_method = models.CharField(max_length=32, choices=AUTH_CHOICES)
auth_username = models.CharField(max_length=32)
auth_password = models.CharField(max_length=32, null=True, blank=True)
auth_snmp_version = models.CharField(max_length=2, choices=SNMP_VERSIONS, null=True, blank=True)
auth_snmp_community = models.CharField(max_length=128, null=True, blank=True)
class Meta:
verbose_name = "credentials"
verbose_name_plural = "credentials"
def __unicode__(self):
return self.auth_method
class Asset(models.Model):
asset_name = models.CharField(max_length=128, unique=True)
asset_type = models.ForeignKey(Device)
brand = models.ForeignKey(Brand)
model = models.CharField(max_length=128)
serial = models.CharField(max_length=256)
location = models.ForeignKey(Location)
environment = models.ForeignKey(Environment)
datacenter_room = models.CharField(max_length=32, null=True, blank=True)
grid_location = models.CharField(max_length=32, null=True, blank=True)
mgmt_address = models.CharField(max_length=128)
notes = models.TextField(null=True, blank=True)
def __unicode__(self):
return self.asset_name
class StorageArray(models.Model):
OS_NAME_CHOICES = (
('Data OnTap', 'NetApp Data OnTap'),
)
OS_TYPE_CHOICES = (
('7-Mode', 'NetApp 7-Mode'),
('cDOT', 'NetApp Clustered'),
)
HA_TYPE_CHOICES = (
('Standalone', 'Standalone System'),
('HA Pair', 'HA Pair'),
('Clustered', 'Clustered'),
)
asset = models.OneToOneField(Asset, primary_key=True, limit_choices_to={'asset_type': 'Storage Array'})
os_name = models.CharField(max_length=32, choices=OS_NAME_CHOICES, null=True, blank=True)
os_version = models.CharField(max_length=16, null=True, blank=True)
os_type = models.CharField(max_length=16, choices=OS_TYPE_CHOICES, null=True, blank=True)
array_ha_type = models.CharField(max_length=32, choices=HA_TYPE_CHOICES, null=True, blank=True)
array_partner = models.CharField(max_length=128, null=True, blank=True)
credentials = models.ForeignKey(Credentials, null=True, blank=True)
class Meta:
verbose_name = "storage array"
verbose_name_plural = "storage arrays"
def __unicode__(self):
return self.asset.asset_name
class SANSwitch(models.Model):
OS_NAME_CHOICES = (
('FabricOS', 'Brocade FabricOS'),
)
SWITCH_TYPE_CHOICES = (
('Standalone', 'Standalone Switch'),
('Director', 'Director'),
('Router', 'Multiprotocol Router'),
('Blade', 'Blade Chassis IO Module'),
)
SWITCH_ROLE_CHOICES = (
('Core', 'Core'),
('Edge', 'Edge'),
('AG', 'Access Gateway'),
)
asset = models.OneToOneField(Asset, primary_key=True, limit_choices_to={'asset_type': 'SAN Switch'})
os_name = models.CharField(max_length=32, choices=OS_NAME_CHOICES, null=True, blank=True)
os_version = models.CharField(max_length=16, null=True, blank=True)
switch_type = models.CharField(max_length=32, choices=SWITCH_TYPE_CHOICES, null=True, blank=True)
switch_role = models.CharField(max_length=32, choices=SWITCH_ROLE_CHOICES, null=True, blank=True)
credentials = models.ForeignKey(Credentials, null=True, blank=True)
class Meta:
verbose_name = "san switch"
verbose_name_plural = "san switches"
def __unicode__(self):
return self.asset.asset_name
I fixed it all by myself!
It seems to translate further down a relationship you can make use of the double underscore notation that python/django has built in.
To fix my issue:
asset = models.OneToOneField(Asset, primary_key=True, limit_choices_to={'asset_type': 'Storage Array'})
became:
asset = models.OneToOneField(Asset, primary_key=True, limit_choices_to={'asset_type__device_type': 'Storage Array'})

Categories