SQLAlchemyAutoSchema Nested Deserialization - flask

I have the following:
Models.py
class Policy(db.Model):
policy_id = db.Column(db.Integer, primary_key=True)
client_reference = db.Column(db.String(50), nullable=False)
submission_id = db.Column(db.ForeignKey('submission.submission_id'), nullable=False)
submission = relationship("Submission", back_populates="policies", lazy='joined')
class Submission(db.Model):
submission_id = db.Column(db.Integer, primary_key=True)
client_reference = db.Column(db.String(50), nullable=False)
policies = db.relationship('Policy', back_populates='submission', lazy='joined')
Schema.py
class PolicySchema(SQLAlchemyAutoSchema):
class Meta:
model = Policy
include_relationships = True
load_instance = True
unknown = INCLUDE
exclude = ("submission",)
submission_id = auto_field("submission")
class SubmissionSchema(SQLAlchemyAutoSchema):
class Meta:
model = Submission
include_relationships = False
load_instance = True
policies = fields.Nested(PolicySchema, many=True, allow_none=True)
routes.py
#submission_api.route('/submission/', methods=['POST'])
def add_new():
body = request.get_json()
validated_request = SubmissionSchema().load(body, session=db.session)
db.session.add(validated_request)
db.session.commit()
return jsonify({"submission_id": validated_request.submission_id}), 200
If I try to add a new record into Submission using:
{
"client_reference": "POL1",
"description": "hello"
}
It works correctly.
If I call with:
{
"client_reference": "POL1","description": "hello",
"policies": [{"client_reference": "A1"}]
}
I receieve this error:
File "/usr/local/lib/python3.8/site-packages/marshmallow_sqlalchemy/schema/load_instance_mixin.py", line 89, in load
raise ValueError("Deserialization requires a session")
ValueError: Deserialization requires a session
I can't figure out why. I pass the session in the same way for both (its the same function). I've made a change to cause this because it was working (reminder to commit code more often).

I am afraid there is no real solution only, just a workaround discussed in this github issue. To quote TMiguelIT:
Would it not be reasonable for us to create our own Nested field that inherits from the original Marshmallow Nested, allowing us to pass the session object down? Would this diminish usability in any way?
As I understand it, the solution should look something like this:
from marshmallow import fields
class Nested(fields.Nested):
"""Nested field that inherits the session from its parent."""
def _deserialize(self, *args, **kwargs):
if hasattr(self.schema, "session"):
self.schema.session = db.session # overwrite session here
self.schema.transient = self.root.transient
return super()._deserialize(*args, **kwargs)
So you create your own Nested field (I just copied the code from marshmallow-sqlalchemy) at the top of schemy.py and overwrite the session.

Related

Add extra value from ImportForm to model instance before imported or saved

I have the following model that I want to import:
class Token(models.Model):
key = models.CharField(db_index=True,unique=True,primary_key=True, )
pool = models.ForeignKey(Pool, on_delete=models.CASCADE)
state = models.PositiveSmallIntegerField(default=State.VALID, choices=State.choices)
then a resource model:
class TokenResource(resources.ModelResource):
class Meta:
model = Token
import_id_fields = ("key",)
and a ImportForm for querying the pool:
class AccessTokenImportForm(ImportForm):
pool = forms.ModelChoiceField(queryset=Pool.objects.all(), required=True)
This value shouls be set for all the imported token objects.
The problem is, that I did not find a way to acomplish this yet.
How do I get the value from the form to the instance?
The before_save_instance and or similar methods I cannot access these values anymore. I have to pass this alot earlier I guess. Does someone ever done something similar?
Thanks and regards
Matt
It seems that you could pass the extra value in the Admin:
class TokenAdmin(ImportMixin, admin.ModelAdmin):
def get_import_data_kwargs(self, request, *args, **kwargs):
form = kwargs.get('form')
if form:
kwargs.pop('form')
if 'pool' in form.cleaned_data:
return {'pool_id' : form.cleaned_data['pool'].id }
return kwargs
return {}
And then use this data within your resources after_import_instance method to set this value
def after_import_instance(self, instance, new, row_number=None, **kwargs):
pool_id = kwargs.get('pool_id')
instance.pool_id = pool_id
instance.created_at = timezone.now()
Then the error from missing field (non null) is gone.

Select a valid choice while selecting Foreignkey in django admin mongodb

database in MongoDB (djongo)
I have registered a model in admin.py
admin.site.register(Media)
models.py
class Media(BaseModel):
_id = models.ObjectIdField(primary_key=True)
url = models.URLField()
media_type = models.CharField(max_length=100)
user = models.ForeignKey(User, db_column="user", on_delete=models.CASCADE)
post = models.ForeignKey(Post, db_column="post", on_delete=models.CASCADE)
group = models.ForeignKey(Group, db_column="group", on_delete=models.CASCADE)
class Meta:
db_table = "media"
while changing values using the admin site I got these errors.
can you help me to solve this error?
django==3.0.5
djongo==1.3.4
I noticed that when you try to save an instance of an object with PK an ObjectId that it is transformed into a string and consequently no longer corresponds to an instance of an object, so with the override of the get_form method in POST you can intercept this data and change the string to ObjectId, but as you can see in the [Django documentation][1]:
The QueryDicts at request.POST and request.GET will be immutable when
accessed in a normal request/response cycle.
so you can use the recommendation from the same documentation:
To get a mutable version you need to use QueryDict.copy()
or ... use a little trick, for example, if you need to keep a reference to an object for some reason or leave the object the same:
# remember old state
_mutable = data._mutable
# set to mutable
data._mutable = True
# сhange the values you want
data['param_name'] = 'new value'
# set mutable flag back
data._mutable = _mutable
where data it is your QueryDicts
In this case:
#admin.register(MyMode)
class MyModelAdmin(admin.ModelAdmin):
list_display = ('....')
....
def get_form(self, request, obj, change, **kwargs):
if request.POST:
# remember old state
_mutable = request.POST._mutable
# set to mutable
request.POST._mutable = True
# сhange the values you want
request.POST['user'] = ObjectId(request.POST['user'])
request.POST['post'] = ObjectId(request.POST['post'])
request.POST['group'] = ObjectId(request.POST['group'])
# set mutable flag back
request.POST._mutable = _mutable
return super().get_form(request, obj=obj, change=change, **kwargs)

Flask Admin One to One Relationship and Edit Form

I am building an admin dashboard for my web app using Flask-Admin. For the user/address relationship, I am using a one to one relationship. On the user edit form, I'd like to be able to edit the individual components of the address (i.e. street address, city or zip) similar to what inline_models provides. Instead, flask-admin generates a select field and only allows me to select a different addresses.
I tried using inline_models = ['address'] in the UserModelView definition. However, I got the address object not iterable error due to the user/address relationship being configured to uselist=False. Switching uselist to True would affect other parts of my code, so I'd prefer to leave it as False.
From looking in flask-admin/contrib/sqla/forms, within the function get_forms, its being assigned a one to many tag which is what drives the use of a select field.
Before diving in further, I figured it best to see if anyone else has come across this or has a recommended fix/workaround.
models.py
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(64))
address = db.relationship("Address", backref="user",
cascade="all, delete-orphan", lazy=False,
uselist=False, passive_deletes=True)
class Address(db.Model):
id = db.Column(db.Integer, primary_key=True)
line1 = db.Column(db.String(128))
zip = db.Column(db.String(20), index=True)
city = db.Column(db.String(64), index=True, nullable=False)
user_id = db.Column(db.Integer, db.ForeignKey("user.id",
ondelete="CASCADE"))
admin.py
class UserModelView(ModelView):
column_list = [User.username, 'address']
form_columns = (User.username, 'address')
admin = Admin(name='Ask', template_mode='bootstrap3')
admin.add_view(UserModelView(User, db.session))
You can create 2 relations
# Relation for flask admin inline model
address_cms_relationsip = db.relationship(
"Address", backref="user", cascade="all, delete-orphan", lazy=False,
uselist=True, passive_deletes=True)
address_relationship = db.relationship(
"Address", cascade="all, delete-orphan", lazy=False,
uselist=False, passive_deletes=True)
#property
def address(self):
return self.address_relationship
In your code you can use property address
user: User # some User object
user.address.city

Flask-admin: differentiate accessability between views

I would like to differentiate accessability to the views (index, create, edit) in flask-admin. It can be done at the level of all views concerning particular model by overriding the method: is_accessible.
def is_accessible(self):
return current_user.is_authenticated # using flask-login
I need that some users will be able to browse data, but without permission to create new records. On the othe rhand other users should be able to create
and edit records. Any help will be appreciated.
Solution
I have overriden _handle_view method which is called before every view.
def _handle_view(self, name, **kwargs):
if not current_user.is_authenticated:
return self.unauthorized_access()
permissions = self.get_permissions(name)
if not current_user.can(permissions):
return self.forbidden_access()
return None #access granted
It isn't terribly well documented, but I think you can override the is_action_allowed method on a ModelView class to get the behavior you want. The API documentation doesn't say much about this, but I found a better example from the changenotes when it was introduced:
You can control which actions are available for current request by
overriding is_action_allowed method:
from flask.ext.admin.actions import action
class MyModelAdmin(ModelAdmin):
def is_action_allowed(self, name):
if name == 'merge' and not user.superadmin:
return False
if name == 'delete' and not user.admin:
return False
return super(MyModelAdmin, self).is_action_allowed(name)
I haven't tried this myself, so I can't attest to whether the example actually works without other changes.
Flask-User seems like what you want. You specify a UserRoles class, a Roles class, and a User class which you already have. You assign a UserRole to a User and then in your is_accessible method can differentiate what you want to do (read/write/edit) based on your role.
https://pythonhosted.org/Flask-User/
# Define User model
class User(db.Model, UserMixin):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(50), nullable=True, unique=True)
...
roles = db.relationship('Role', secondary='user_roles',
backref=db.backref('users', lazy='dynamic'))
# Define Role model
class Role(db.Model):
id = db.Column(db.Integer(), primary_key=True)
name = db.Column(db.String(50), unique=True)
# Define UserRoles model
class UserRoles(db.Model):
id = db.Column(db.Integer(), primary_key=True)
user_id = db.Column(db.Integer(), db.ForeignKey('user.id', ondelete='CASCADE'))
role_id = db.Column(db.Integer(), db.ForeignKey('role.id', ondelete='CASCADE'))

manage QuerySelectField across Flask blueprints

I have been through various tutorials + search on Stackoverflow / Google, but failed to find some answers on how to use QuerySelectField using several Flask blueprints, may be you could help.
Basically, I have defined a Project class and a Client class in my models.py as follows:
class Project(db.Model):
__tablename__ = 'projects'
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(128), nullable=False)
description = db.Column(db.Text)
client_id = db.Column(db.Integer, db.ForeignKey('clients.id'))
class Client(db.Model):
__tablename__ = 'clients'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(128), nullable=False)
location = db.Column(db.Text, nullable=False)
projects = db.relationship('Project', lazy='dynamic', backref='project')
I then have a blueprint for each part, one to manage my projects, another one for my clients.
I then get stuck when I try to use a QuerySelectField to retrieve the client names in my projects form, here is what I have so far in my projects/forms.py:
from flask.ext.wtf import Form
from wtforms import StringField, TextAreaField, SubmitField
from wtforms.ext.sqlalchemy.fields import QuerySelectField
from wtforms.validators import Length, Required
from wtforms.fields.html5 import DateField
class ProjectForm(Form):
title = StringField('Title', validators=[Required(), Length(1, 128)])
description = TextAreaField('Desciption')
date = DateField('Date', format='%d/%m/%Y')
client_id = QuerySelectField('Select Client', validators=[Required()], coerce=int, choices = [(1, 'abc'), (2, 'defg'), (3, 'hij')])
submit = SubmitField('Submit')
def from_model(self, project):
self.title.data = project.title
self.description.data = project.description
self.client_id.data = project.client_id
self.date.data = project.date
def to_model(self, project):
project.title = self.title.data
project.description = self.description.data
project.client_id = self.client_id.data
project.date = self.date.data
I know this version is NOT picking any client as I am passing static data i/o dynamic ones, but at least it works and I can see my SelectField with possible values. I have read I should be using something like
client_id = QuerySelectField('Select Client', query_factory=lambda: Client.query.all())
and been also recommended to first declare something like the following before "class ProjectForm(Form)":
def client_id():
return Client.query.filter_by(enabled=True)
In all cases, I end up with a NameError: name 'Client' is not defined, I understand I have to say somewhere that the program should look at my class Client, but I failed finding where, unless the issue is somewhere else.
Thanks in advance for your help.
I did it simply this way:
def get_clients():
from forms import Client
return Client.query.all()
and then:
client_id = QuerySelectField('Select Client', query_factory=get_clients)