Django - Existing DB Views and Foreign Keys - django

I have a simple view on the DB which selects from other DB's tables located on the same MSSQL Server to ultimately serve the collected info as a dropdown to the user.
So far I've added the Model with inspectdb:
class AutPricePlanView(models.Model):
priceplan_name = models.CharField(db_column='PricePlan', max_length=50, blank=True, unique=True)
class Meta:
managed = False # Created from a view. Don't remove.
db_table = 'AUT_PricePlanView'
Also I have a second existing (Django Native) Model where I want to use the values from the view for a Dropdown Field (to keep everything in sync):
class PricePlanDownload(models.Model):
requesting_user = models.CharField(blank=True, default=None, max_length=50, null=True)
requested_at = models.DateTimeField(auto_now_add=True)
document = models.FileField(upload_to='documents/price_plan_uploads/%Y/%m/%d', blank=True)
priceplan = models.ForeignKey(AutPricePlanView, null=True, on_delete=models.DO_NOTHING)
Makemigrations works fine but when I try to actually migrate I get the following issue: (shortened it a little bit)
django.db.utils.ProgrammingError: ('42000', "[42000] [FreeTDS][SQL Server]Foreign key references object 'AUT_PricePlanView' which is not a user table. (1768) (SQLExecDirectW)")
I would be really grateful if someone had an idea or a workaround since I can't figure out what the heck this has to do with a "user" table...

Since the view is not actually a table, you cannot set Foreign Key constraints. Since ForeignKey's default db_constraint value is True, Django tries to set Foreign Key constraints when performing migrations. This is the reason the migration fails.
So, you can turn off the db_constraint option. And you can remove the existing migration file, and re-create the migration file. Then, the migration will success and you can keep everything in sync.
class PricePlanDownload(models.Model):
... other fields ...
priceplan = models.ForeignKey(AutPricePlanView, null=True, on_delete=models.DO_NOTHING, db_constraint=False)
Pro Tip: You can review migration's SQL using python manage.py sqlmigrate <appname> <migration number>, like python manage.py sqlmigrate yourapp 0002.
Update: You can define __str__ to display the correct value at the dropdown menu.
class AutPricePlanView(models.Model):
priceplan_name = models.CharField(db_column='PricePlan', max_length=50, blank=True, unique=True, primary_key=True)
# null=False by default. See https://github.com/django/django/blob/master/django/db/models/fields/__init__.py#L132
def __str__(self):
return self.priceplan_name
class Meta:
managed = False # Created from a view. Don't remove.
db_table = 'AUT_PricePlanView'

Related

Integrity Error Not Null Constraint Failed When Attempting to Migrate

I am receiving this error when attempting to migrate:
"return Database.Cursor.execute(self, query, params)
django.db.utils.IntegrityError: NOT NULL constraint failed: unit_manifests.product_name_id"
This is in reference to the product_name field in the model below.
1) Why do I need to set a default value for a foreign key field?
2) I initially ran without default = none, blank = true, null = true. I have now run makemigrations again but when I migrate I still get the error, I believe it is attempting to run an old migrations first. How can I get around this?
MODELS.PY
class Manifests(models.Model):
reference = models.ForeignKey(Orders)
cases = models.IntegerField()
product_name = models.ForeignKey(Products, default=None, blank=True, null=True)
count = models.IntegerField()
CNF = models.DecimalField(max_digits=11, decimal_places=2, default=None, blank=True, null=True)
FOB = models.DecimalField(max_digits=11, decimal_places=2, default=None, blank=True, null=True)
def __str__(self):
return self.description
Django migrations works like this.
First you run makemigrations which will create a file based on changes from the last file generated from running makemigrations.
Then you run migrate which will push the changes in the migrations folder which are not yet registered in the database in table 'django_migrations'. The migration file names are important because django uses it to identify the migration.
If you want to fake a migration, you can manually create a database instance in the migrations table with the migrations file you want to fake.
Lets say the migration file is called '0002_auto_20190212_1240.py', then you insert a row in the 'django_migrations' table with app=[app_name], name=0002_auto_20190212_1240 and applied=[+1 second after last instance].

Making use of the users table, causing an error

In Django (2.x) I have an entry form, the model is here:
from django.db import models
from django.contrib.auth.models import User
from django.conf import settings
class Sample(models.Model):
sample_id = models.AutoField(primary_key=True)
area_easting = models.IntegerField()
area_northing = models.IntegerField()
context_number = models.IntegerField()
sample_number = models.IntegerField()
# taken_by = models.IntegerField()
taken_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete = models.PROTECT)
def __str__(self):
return str(self.sample_id)
class Meta:
db_table = 'samples\".\"sample'
#ordering = ["sample_id"]
managed = False
#verbose_name_plural = "samples"
This works as expected, a list of usernames drops down (while I would like to format - firstname lastname). However, when I return to the main viewing page I see an error.
django.db.utils.ProgrammingError: column sample.taken_by_id does not exist
LINE 1: ...text_number", "samples"."sample"."sample_number", "samples"....
^
HINT: Perhaps you meant to reference the column "sample.taken_by".
Clearly Django is adding the _id to the table name causing the error, I expect because it is a foreign key.
Any ideas how to remedy this behaviour?
You can explicitly set the underlying db column via the db_column attribute:
taken_by = models.ForeignKey(settings.AUTH_USER_MODEL, db_column='taken_by', on_delete=models.PROTECT)
https://docs.djangoproject.com/en/2.1/ref/models/fields/#database-representation
^ link to the docs where it specifies that it creates a _id field.
based from the error message you have posted. It seems that your database schema is not updated.
you might need to do manage makemigrations and migrate to apply your model changes to your db schema
e.g
$ python manage.py makemigrations
# to apply the new migrations file
$ python manage.py migrate

How can one change the type of a Django model field from CharField to ForeignKey?

I need to change the type of a field in one of my Django models from CharField to ForeignKey. The fields are already populated with data, so I was wondering what is the best or right way to do this. Can I just update the field type and migrate, or are there any possible 'gotchas' to be aware of? N.B.: I just use vanilla Django management operations (makemigrations and migrate), not South.
This is likely a case where you want to do a multi-stage migration. My recommendation for this would look something like the following.
First off, let's assume this is your initial model, inside an application called discography:
from django.db import models
class Album(models.Model):
name = models.CharField(max_length=255)
artist = models.CharField(max_length=255)
Now, you realize that you want to use a ForeignKey for the artist instead. Well, as mentioned, this is not just a simple process for this. It has to be done in several steps.
Step 1, add a new field for the ForeignKey, making sure to mark it as null:
from django.db import models
class Album(models.Model):
name = models.CharField(max_length=255)
artist = models.CharField(max_length=255)
artist_link = models.ForeignKey('Artist', null=True)
class Artist(models.Model):
name = models.CharField(max_length=255)
...and create a migration for this change.
./manage.py makemigrations discography
Step 2, populate your new field. In order to do this, you have to create an empty migration.
./manage.py makemigrations --empty --name transfer_artists discography
Once you have this empty migration, you want to add a single RunPython operation to it in order to link your records. In this case, it could look something like this:
def link_artists(apps, schema_editor):
Album = apps.get_model('discography', 'Album')
Artist = apps.get_model('discography', 'Artist')
for album in Album.objects.all():
artist, created = Artist.objects.get_or_create(name=album.artist)
album.artist_link = artist
album.save()
Now that your data is transferred to the new field, you could actually be done and leave everything as is, using the new field for everything. Or, if you want to do a bit of cleanup, you want to create two more migrations.
For your first migration, you will want to delete your original field, artist. For your second migration, rename the new field artist_link to artist.
This is done in multiple steps to ensure that Django recognizes the operations properly. You could create a migration manually to handle this, but I will leave that to you to figure out.
Adding on top of Joey's answer, detailed steps for Django 2.2.11.
Here are the models from my use case, that consists of a Company and Employee model. We have to convert designation to a foreign key field. The app name is called core
class Company(CommonFields):
name = models.CharField(max_length=255, blank=True, null=True
class Employee(CommonFields):
company = models.ForeignKey("Company", on_delete=models.CASCADE, blank=True, null=True)
designation = models.CharField(max_length=100, blank=True, null=True)
Step 1
Create a foreign key designation_link in Employee and mark it as null=True
class Designation(CommonFields):
name = models.CharField(max_length=255)
company = models.ForeignKey("Company", on_delete=models.CASCADE, blank=True, null=True)
class Employee(CommonFields):
company = models.ForeignKey("Company", on_delete=models.CASCADE, blank=True, null=True)
designation = models.CharField(max_length=100, blank=True, null=True)
designation_link = models.ForeignKey("Designation", on_delete=models.CASCADE, blank=True, null=True)
Step 2
Create empty migration. Using the command:
python app_code/manage.py makemigrations --empty --name transfer_designations core
This will create a following file in migrations directory.
# Generated by Django 2.2.11 on 2020-04-02 05:56
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('core', '0006_auto_20200402_1119'),
]
operations = [
]
Step 3
Populate the empty migration with a function that loops over all Employees, creates a Designation and links it to the Employee.
In my use case each Designation is also linked to a Company. Which means that Designation may contain two rows for "managers", one for company A, another for company B.
Final migration would look something like this:
# core/migrations/0007_transfer_designations.py
# Generated by Django 2.2.11 on 2020-04-02 05:56
from django.db import migrations
def link_designation(apps, schema_editor):
Employee = apps.get_model('core', 'Employee')
Designation = apps.get_model('core', 'Designation')
for emp in Employee.objects.all():
if(emp.designation is not None and emp.company is not None):
desig, created = Designation.objects.get_or_create(name=emp.designation, company=emp.company)
emp.designation_link = desig
emp.save()
class Migration(migrations.Migration):
dependencies = [
('core', '0006_auto_20200402_1119'),
]
operations = [
migrations.RunPython(link_designation),
]
Step 4
Finally run this migration using:
python app_code/manage.py migrate core 0007
That's a continuation of the great answer by Joey.
How to rename the new field to the original name?
If the field has data, it probably means that you are using it elsewhere in your project, therefore this solution will leave you with a field named differently, and you have to either refactor the project to use the new field or delete the old field and rename the new one.
Be aware that this process is not going to prevent you to refactor code. If you where using a CharField with CHOICES, you were accessing its content with get_filename_display(), for example.
If you try to delete the field to make a migration, for then renaming the other field and make another migration, you'll see Django complaining because you cannot delete a field that you are using in the project.
Just create an empty migration as Joey explained, and put this in operations:
operations = [
migrations.RemoveField(
model_name='app_name',
name='old_field_name',
),
migrations.RenameField(
model_name='app_name',
old_name='old_field_name_link',
new_name='old_field_name',
),
]
Then run migrate and you'll have the changes made in your database, but obviously not in your model, it's time now to delete the old field and to rename new ForeignKey field to the original name.
I don't think that doing this is particularly hacky, but still, only do this kind of things if you are fully understanding what are you messing with.

Django South - Change Foreign Key to Required

I changed my model from this:
class DistList(models.Model):
creator = models.ForeignKey(User, related_name='creator')
created_date = models.DateTimeField(auto_now_add=True)
name = models.CharField(max_length=200, unique=True)
description = models.TextField(blank=True, null=True)
company = models.ForeignKey(Company, blank=True, null=True)
To this:
class DistList(models.Model):
creator = models.ForeignKey(User, related_name='creator')
created_date = models.DateTimeField(auto_now_add=True)
name = models.CharField(max_length=200, unique=True)
description = models.TextField(blank=True, null=True)
company = models.ForeignKey(Company)
The only change was turning the company FK relationship from not required to required.
When I run the migration I specify a one off value that corresponds to the pk of the first company.
./manage.py schemamigration distlist --auto
? The field 'DistList.company' does not have a default specified, yet is NOT NULL.
? Since you are making this field non-nullable, you MUST specify a default
? value to use for existing rows. Would you like to:
? 1. Quit now, and add a default to the field in models.py
? 2. Specify a one-off value to use for existing columns now
? Please select a choice: 2
? Please enter Python code for your one-off default value.
? The datetime module is available, so you can do e.g. datetime.date.today()
>>> 1
But when I run the migration I get an error because it has a pending trigger event?
./manage.py migrate distlist
Running migrations for distlist:
- Migrating forwards to 0005_auto__chg_field_distlist_company.
> distlist:0005_auto__chg_field_distlist_company
FATAL ERROR - The following SQL query failed: ALTER TABLE "distlist_distlist" ALTER COLUMN "company_id" SET NOT NULL;
The error was: cannot ALTER TABLE "distlist_distlist" because it has pending trigger events
I'm not doing anything that seems weird from my point of view so I don't understand this error at all. Can anyone offer insight? I can post the full stack trace if it'll help but I feel like theres something obvious about south and postgresql that perhaps I'm missing?
So I believe I've found the answer. I think postgresql doesn't like altering schemas and adding data at the same time. I first created a datamigration:
./manage.py datamigration distlist add_default_values_to_existing_companies
Then I added this to the forwards method:
def forwards(self, orm):
"Write your forwards methods here."
for distlist in orm['distlist.Distlist'].objects.all():
distlist.company = orm['userprofile.Company'].objects.get(id=1)
distlist.save()
Then I altered the model to remove the blank and null from company.
Then I ran the schema migration and chose to specify a one off for the value as 1 (as I did in the question).
Then I edited that migration file thusly:
def forwards(self, orm):
# Changing field 'DistList.company'
# db.alter_column(u'distlist_distlist', 'company_id', self.gf('django.db.models.fields.related.ForeignKey')(default=1, to=orm['userprofile.Company']))
db.alter_column(u'distlist_distlist', 'company_id', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['userprofile.Company']))
I just commented out the generated line and removed the default=1 arg.
I don't know... maybe this isn't right but it seemed to work. Hopefully this will help someone.

How to add ManyToManyField in Existing django model?

I have a follwing two tables:
class Visit(models.Models):
date_created = models.DateTimeField(auto_now_add=True)
date_modified = models.DateTimeField(auto_now=True)
date_started = models.DateTimeField(null=True, blank=True)
date_completed = models.DateTimeField(null=True, blank=True)
# Here i want to add ManyToManyField
research = ManyToManyField(ResearchProtocol) #Here i will write for adding the field
class ResearchProtocol(models.Model):
title = models.CharField(max_length=30)
description = models.TextField()
start_date = models.DateField()
end_date = models.DateField()
def __unicode__(self):
return '%s' % self.title
For that i have written sql query :
CREATE TABLE "visit_visit_research" (
"id" serial NOT NULL PRIMARY KEY,
"visit_id" integer NOT NULL REFERENCES "visit_visit" ("id") DEFERRABLE INITIALLY DEFERRED,
"research_id" integer NOT NULL REFERENCES "www_researchprotocol" ("id") DEFERRABLE INITIALLY DEFERRED,
UNIQUE ("visit_id", "research_id")
)
;
When i execute this file the field is created somehow but when i open vist admin
and click to a particular id that leads to change form it gives me the following error:
http://localhost:8000/admin/visit/visit/20/
Exception Type: DatabaseError at /admin/visit/visit/20/
Exception Value: column visit_visit_research.researchprotocol_id does not exist
LINE 1: ...visit_research" ON ("www_researchprotocol"."id" = "visit_vis...
^
Somebody said that you need south and it cannot be done without south. Is that the only solution ? I am using Django 1.3.1, Python 2.7.2.
Can somebody guide me what mistake i am doing?
Any help will be appreciated.
Thanks in advance.
You've called your linking table visit_visit_research, and the field within it research_id, whereas Django is expecting visit_visit_researchprotocol and researchprotocol_id respectively.
Whenever I have modified the table models I used south and these commands to modify the structure and they always worked:
python manage.py convert_to_south "your_app"
python manage.py migrate "your_app"
You could try these and it should work, if you still have south installed.