Saving a model with inlines in Django - python-3.x

This seems like a very easy problem, but I really can't figure out what's going on. I have some problems understanding the saving process on the Django admin site. This is the situation, simplified as much as possible:
models.py
import uuid
from django.conf import settings
from django.db import models
class BaseModel(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=settings.IS_DEV)
created = models.DateTimeField(auto_now_add=True)
modified = models.DateTimeField(auto_now=True)
class Order(BaseModel):
[various properties and functions]
class Article(BaseModel):
order = models.ForeignKey(Order, null=True, on_delete=models.CASCADE, related_name='articles')
[various properties and functions]
class PaymentOperation(BaseModel):
order = models.ForeignKey(Order, null=True, on_delete=models.CASCADE)
[various properties and functions]
admin.py
from django.conf import settings
from django.contrib import admin
from models import Order, Article, PaymentOperation
class BaseModelAdmin(admin.ModelAdmin):
readonly_fields = ['created', 'modified']
if not settings.IS_DEV:
readonly_fields.append('id')
class ArticleInline(admin.TabularInline):
fields = ['id', ...]
readonly_fields = ['id', ...]
can_delete = False
extra = 0
max_num = 0
[more code]
class PaymentOperationInline(admin.TabularInline):
fields = ['id', ...]
readonly_fields = ['id', ...]
can_delete = False
extra = 0
max_num = 0
[more code]
class OrderAdmin(BaseModelAdmin):
readonly_fields = BaseModelAdmin.readonly_fields + [...]
fieldsets = [...]
inlines = [ArticleInline, PaymentOperationInline]
[more code]
class ArticleAdmin(BaseModelAdmin):
readonly_fields = BaseModelAdmin.readonly_fields + [...]
fieldsets = [...]
[more code]
This is the main structure, but I'm not really sure if it's enough to generate the problem. I didn't want to clog the question with hundreds of lines of code. I'll try to be more specific if necessary.
Some fields in the Order and Article models are editable, while all fields in the PaymentOperation model are read-only.
If I edit an Article from its admin page, it works perfectly. If, on the other hand, I try to edit an Order, and then save it, the page behaves strangely. The order is not saved, and an error message appears on top of the page, saying "Please correct the errors below." All the fields, both editable and read-only, remain unchanged. The two inlines at the bottom of the page are in a worse state. All read-only fields are reset to the default value, if available, or nulled, and all IDs are different. Editable fields of the article inline remain the same. It looks to me like the page is trying to create new entries, instead of editing the old ones.
I tried commenting out the inlines in the OrderAdmin declaration one at a time, but with no effect. Commenting out both lets me save the order correctly.
I also tried adding a save_model and save_formset to OrderAdmin, but whatever the error is, it's produced before these functions are called. The shell where I run python manage.py runserver doesn't even show any error message.
There are many other models in models.py and in admin.py, and none of them has the same problem. Some of these models even have their own inlines.
I'm really puzzled. I'm trying to understand the difference between the part of the code shown above and the rest, but I can't find it. I know the saving process worked before I added the payment operation a few days ago, and I'm almost certain that I didn't change anything in the order and article models (I will check though, just to be sure). At this point I'm not sure I understood the saving process, or what's wrong with the code.
Edit: I can't find significant changes to the order and article models, as well as their admin counterparts.

Apparently, the whole problem is located in the editable=settings.IS_DEV setting for the ID of every model. I don't know exactly why, but setting editable=False solves the problem.
I noticed I never had this problem on the production server, only on the ones flagged as development. Fortunately being able to set a specific ID was only useful in a very limited set of cases, so I won't miss it, but I feel like I just traded a problem with a less annoying one. If someone has a better solution, I'll be happy to change my mind and recognize that as the answer to this question.

Related

django remove m2m instance when there are no more relations

In case we had the model:
class Publication(models.Model):
title = models.CharField(max_length=30)
class Article(models.Model):
publications = models.ManyToManyField(Publication)
According to: https://docs.djangoproject.com/en/4.0/topics/db/examples/many_to_many/, to create an object we must have both objects saved before we can create the relation:
p1 = Publication(title='The Python Journal')
p1.save()
a1 = Article(headline='Django lets you build web apps easily')
a1.save()
a1.publications.add(p1)
Now, if we called delete in either of those objects the object would be removed from the DB along with the relation between both objects. Up until this point I understand.
But is there any way of doing that, if an Article is removed, then, all the Publications that are not related to any Article will be deleted from the DB too? Or the only way to achieve that is to query first all the Articles and then iterate through them like:
to_delete = []
qset = a1.publications.all()
for publication in qset:
if publication.article_set.count() == 1:
to_delete(publication.id)
a1.delete()
Publications.filter(id__in=to_delete).delete()
But this has lots of problems, specially a concurrency one, since it might be that a publication gets used by another article between the call to .count() and publication.delete().
Is there any way of doing this automatically, like doing a "conditional" on_delete=models.CASCADE when creating the model or something?
Thanks!
I tried with #Ersain answer:
a1.publications.annotate(article_count=Count('article_set')).filter(article_count=1).delete()
Couldn't make it work. First of all, I couldn't find the article_set variable in the relationship.
django.core.exceptions.FieldError: Cannot resolve keyword 'article_set' into field. Choices are: article, id, title
And then, running the count filter on the QuerySet after filtering by article returned ALL the tags from the article, instead of just the ones with article_count=1. So finally this is the code that I managed to make it work with:
Publication.objects.annotate(article_count=Count('article')).filter(article_count=1).filter(article=a1).delete()
Definetly I'm not an expert, not sure if this is the best approach nor if it is really time expensive, so I'm open to suggestions. But as of now it's the only solution I found to perform this operation atomically.
You can remove the related objects using this query:
a1.publications.annotate(article_count=Count('article_set')).filter(article_count=1).delete()
annotate creates a temporary field for the queryset (alias field) which aggregates a number of related Article objects for each instance in the queryset of Publication objects, using Count function. Count is a built-in aggregation function in any SQL, which returns the number of rows from a query (a number of related instances in this case). Then, we filter out those results where article_count equals 1 and remove them.

migration issue, django.core.exceptions.FieldDoesNotExist: lightspeed.inventoryimagehistory has no field named 'imageID'

I'm having issues with this Django project, when I try to run migrations got the following message:
django.core.exceptions.FieldDoesNotExist: InventoryImageHistory has no field named 'InventoryImageID'
this is the class for InventoryImageHistory
class InventoryImageHistory(models.Model):
ImageID = models.IntegerField(db_index=True, unique=True, null=False, primary_key=True)
history = ListField(DictField())
objects = models.DjongoManager()
these are the migration files:
migration file number 40:
class Migration(migrations.Migration):
operations = [
migrations.RenameField(
model_name='inventoryimagehistory',
old_name='InventoryImageID',
new_name='imageID',
),
De model definition is InventoryImageHistory, but for some reason, it keeps returning an error
Well, a few minutes after I posted my comment I seem to have fixed it. It was because I had initially put the RenameField in the previous (not yet committed to repo, but already applied) migration, and even though that one had been applied, Django seems to parse all of the dependency migrations logically and decided that the rename had already happened.
This message might have to do with the fact you faked/manipulated some migrations in that app recently.
What I do when this pops up is to:
Create another change in that models.py file (i.e.: add an extra field to the model).
Run the migration
Do the change I wanted to do in the first place that triggered the error and run the migration (hopefully successfully this time)
Undo the change I added on 1. and run migrations again.

Flask-migrate: change model attributes and rename corresponding database columns

I have a bit of experience with Flask but not very much with databases (Flask-migrate / alembic / SqlAlchemy).
I'm following this tutorial and things are working quite alright.
I have a User model like this:
# user_model.py
from app import DB
... other imports
class User(UserMixin, DB.Model):
__tablename__ = 'users'
id = DB.Column(DB.Integer, primary_key=True)
username = DB.Column(DB.String(64), index=True, unique=True)
email = DB.Column(DB.String(120), index=True, unique=True)
password_hash = DB.Column(DB.String(128))
I can then initialize the db, do migrations, upgrades etc.
The problem started when I wanted to change that id attribute, which in Python is not a great variable name choice. Let's say I want to rename that to user_id instead.
Now obviously the db already exists and there is some data inside. I thought maybe by some kind of magic from Flask-Migrate/Alembic just modifying the User class would work. That is just change the id line above to:
user_id = DB.Column(DB.Integer, primary_key=True)
If I do this and run flask db migrate I get:
INFO [alembic.runtime.migration] Context impl SQLiteImpl.
INFO [alembic.runtime.migration] Will assume non-transactional DDL.
INFO [alembic.autogenerate.compare] Detected added column 'users.user_id'
INFO [alembic.autogenerate.compare] Detected removed column 'users.id'
So actually Alembic detects this as a column being removed and a new one added, which I suppose makes sense.
But this in fact doesn't work if I run flask db upgrade. I get the following error:
ERROR [alembic.env] (sqlite3.OperationalError) Cannot add a NOT NULL
column with default value NULL [SQL: 'ALTER TABLE users ADD COLUMN
user_id INTEGER NOT NULL']
The error is quite clear. The point is that I don't want to add a new column, I just want to rename an existing one.
Looking around I also tried to modify the script.py handling the upgrade to use the alter_column method:
def upgrade():
${upgrades if upgrades else "pass"}
# just added this line below
op.alter_column('users', 'id', nullable=False, new_column_name='user_id')
However this also doesn't seem to work (I get the same error as above).
So the question boils down to a very simple one: how do I rename a database columns in a Flask app using Flask-Migrate? Or in other words, if I wish to modify the attributes of a given model, what do I have to do so that the corresponding column names in the database are correctly renamed?
To simply rename a column in an alembic script (which is the same as flask-migrate), what you do is correct:
op.alter_column('users', 'id', nullable=False, new_column_name='user_id')
The problem comes from, in my opinion, that you need also to change its constraint as primary key:
op.drop_constraint('user_pkey', 'users', type_='primarykey')
op.create_primary_key('user_pkey', 'users', ['user_id'])
You may need to adjust the name of the primary key you re-create depending of your database type (It works like this for me with PostgreSQL)
Autogenerated alembic scripts should always be reviewed, quite often they do not do what we want if it's not for simple changes.
Note: If your column id was used as a foreign key, you may also want to change the foreign key constraints in other tables.
Alter Primary Key in Alembic describes the same kind of problem.

Avoid having 2 identical entries in SQlAlchemy using Flask

I need to find a way to alert the user that what he's introducing already exists in the database, I have a Flask application and a SQLAlchemy database, I'm also using Flask-WTF,
I tried with a very precarious solution: I stored the data captured by the forms in variables and I was thinking of concatenating them and using a Query to search if they exist.
nombre1 = form.nombre_primero.data
nombre2 = form.nombre_segundo.data
Anyway I think this is not the most appropriate way to handle the situation.
does Flask has some way to do this? Or would you recommend me something?
I'd grateful if you could help me!
I would approach this by creating a composite unique constraint made of the select fields in the sqlalchemy model.
The table can be configured additionally via __table_args__ class property of the declarative base.
from app import db
from sqlalchemy import UniqueConstraint
class Role(db.Model):
id = db.Column(db.Integer, primary_key=True)
nombre_primero = db.Column(db.String(64))
nombre_segundo = db.Column(db.String(64))
__table_args__ = (
UniqueConstraint('nombre_primero', 'nombre_segundo', name='uix_1'),
)
You can write the data to the table and handle what exception is raised when there is a conflict.
Okay, so there is a simple way to solve this, at the table itself, you make a condition that rejects duplicate entries based on some condition which you define.
So one easy way you can do this is make a hybrid function.
Read more about Hybrid Attributes here.
from sqlalchemy.ext.hybrid import hybrid_property
Now where you make the model for your table,
eg:
class xyz(db.Model):
__tablename__ = 'xyz'
#tablevalues defined here
#hybrid_property
def xyz()
#make a method here which rejects duplicate entries.
Once you read the documentation you will understand how this works.
I cant directly solve your problem because there isn't much information you have provided. But in this way, you can check the entries and make some method EASILY where your data is checked to be unique in anyway you want.

Failed to add fields.Selection to parent model

I'm working on access restriction to res.partner.
Currently I have two questions:
1.Why does this code don't create new field to inherited model (first error was something like "unknown object _", now it's gone): (Now it works, first question is answered)
from odoo import models, fields
class partner(models.Model):
_inherit = 'res.partner'
privacy_visibility = fields.Selection([
('followers', _('On invitation only')),
('employees', _('Visible by all employees')),
#error was here. according to odoo documentation, here is a comma
#if you remove it, the code works: [![enter image description here][1]][1]([('',''),('',''),('','')])
],
string='Privacy', required=True,
default='employees',
help="Holds visibility of the partner that affects currently logged user:\n"
"- On invitation only: Employee may only see the followed partners\n"
"- Visible by all employees: Employee may see selected partner\n")
Later, when my restrictions would be applied, how to make selected users to access all pertners anyway? (my ideas - 1.to hide "privacy_visibility" field to be visible in developer mode only, as it made for project's "sub-task project". 2.to create a new group, but I have no idea how to use access groups with row-level access), which way wold you recommend to go?
First Question Ans:
The Selection field syntax is not correct please follow this syntax
Example:
gender = fields.Selection([('male', 'Male'), ('female', 'Female'), ('any', 'Any')], string='Gender')
You are calling the translate object (the underscore "_") to translate your selection values, but you did not import it.
Change:
from odoo import models, fields
To:
from odoo import models, fields, _

Resources