Flask-admin and prepopulated value - flask

I have small issue: simple class
class AModel(db.Model):
id = db.Column(....)
title = db.Column(....)
uniq_text_id = db.Column(db.String(50), unique=True. nullable=False)
def __init__(self):
uniq_text_id = uuid4().hex
Now i adjusted flask-admin package and what administrator be able to create AModel instances. But there is 1 case: field uniq_text_id must be created automatically.
So, right now I cannot create mode because flask-admin says that fiel uniq_text_id in required, but also does not shows prepopulated value.
Is there any way to use prepopulated value in flask-admin forms or avoid somehow this problem without dropping nullable=False constraint?
UPD: #codegeek provided good solution for auto generated fields which still shown on the form. My own solution uses provided by flask-admin functionality allows to explicitly declares which columns are shown and which - hidden.

You need to override the ModelView class for your model. Something like:
class AModelAdmin(sqlamodel.ModelView):
uuidtext = uuid4()
form_args = dict(
uniq_text_id=dict(default=uuidtext)
)
def __init__(self, session):
super(AModelAdmin, self).__init__(AModel, session)
Then you just need to add this to your admin
admin.add_view(AModelAdmin(db.session))

It was my fault - read documentation too briefly. Created my model view like fillowing:
class MyUserLessonModelView(MyModelView):
form_excluded_columns = ('uniq_text_id', 'created_by', 'created_date', 'modified_by', 'modified_date')
And all autogenerated fields are disappeared from form.

Related

Can I override default CharField to ChoiceField in a ModelForm?

I have a (horrible) database table that will be imported from a huge spreadsheet. The data in the fields is for human consumption and is full of "special cases" so its all stored as text. Going forwards, I'd like to impose a bit of discipline on what users are allowed to put into some of the fields. It's easy enough with custom form validators in most cases.
However, there are a couple of fields for which the human interface ought to be a ChoiceField. Can I override the default form field type (CharField)? (To clarify, the model field is not and cannot be constrained by choices, because the historical data must be stored. I only want to constrain future additions to the table through the create view).
class HorribleTable( models.Model):
...
foo = models.CharField( max_length=16, blank=True, ... )
...
class AddHorribleTableEntryForm( models.Model)
class Meta:
model = HorribleTable
fields = '__all__' # or a list if it helps
FOO_CHOICES = (('square', 'Square'), ('rect', 'Rectangular'), ('circle', 'Circular') )
...?
Perhaps you could render the forms manually, passing the options through the context and make the fields in html.
Take a look at here:https://docs.djangoproject.com/en/4.0/topics/forms/#rendering-fields-manually
I think you can easily set your custom form field as long it will match the data type with the one set in your model (e.g. do not set choices longer than max_length of CharField etc.). Do the following where foo is the same name of the field in your model:
class AddHorribleTableEntryForm(forms.ModelForm):
foo = forms.ChoiceField(choices=FOO_CHOICES)
class Meta:
model = HorribleTable
...
I think this is perfectly fine for a creation form. It's will not work for updates as the values in the DB will most probably not match your choices. For that, I suggest adding a second form handling data updates (maybe with custom permission to restrict it).
UPDATE
Another approach will be to override the forms init method. That way you can handle both actions (create and update) within the same form. Let the user select from a choice field when creating an object. And display as a normal model field for existing objects:
class AddHorribleTableEntryForm(forms.ModelForm):
foo = forms.ChoiceField(choices=FOO_CHOICES)
class Meta:
model = HorribleTable
fields = '__all__' # or a list if it helps
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
instance = kwargs.get("instance", None)
if instance is None:
self.fields["foo"].widget = forms.widgets.Select(choices=self.FOO_CHOICES)

How to make non-editable fields appear when creating objects in Django admin?

I have a model with a non-editable field in my models file.
class Table(models.Model):
label = models.CharField(max_length=40, editable=False)
In my admin site, when updating existing Table objects, I can't edit the label. That is fine, this is exactly what I want with this constraint. However, when trying to create an object using the admin site, the field is still hidden, so I can only create Table objects using the shell.
How can I make this field appear only on creation, but on updates, it will be read-only? Thanks.
Try to use readonly_fields in admin.py file
class TableAdmin(admin.ModelAdmin):
readonly_fields = ('label',)
admin.site.register(Table, TableAdmin)
Approach 1
Make label field presented on creation but completely remove it while updating. We will be using ModelAdmin.get_exclude and ModelAdmin.get_fields hooks to accomplish this.
## models.py
class Table(models.Model):
label = models.CharField(max_length=40) # remove editable option
## admin.py
#admin.register(Table)
class TableAdmin(admin.ModelAdmin):
non_editable_fields = ['label']
def get_exclude(self, request, obj=None):
defaults = super().get_exclude(request, obj=obj) or ()
if obj: # if we are updating an object
defaults = (*defaults, *self.non_editable_fields)
return defaults or None
def get_fields(self, request, obj=None):
defaults = super().get_fields(request, obj=obj)
if obj: # if we are updating an object
defaults = tuple(f for f in defaults if f not in self.non_editable_fields)
return defaults
Approach 2
Make label field presented on both creation and update but make it read only while updating. django admin provides a hook for this functioanlity and it is called ModelAdmin.get_readonly_fields. You can find the documentation here.
So we can write the following code to create a field which can be presented/added when creating an object but can not be edited any further despite set value is being displayed(through admin site).
## models.py
class Table(models.Model):
label = models.CharField(max_length=40) # remove editable option
## admin.py
#admin.register(Table)
class TableAdmin(admin.ModelAdmin):
def get_readonly_fields(self, request, obj=None):
defaults = super().get_readonly_fields(request, obj=obj)
if obj: # if we are updating an object
defaults = tuple(defaults) + ('label', ) # make sure defaults is a tuple
return defaults
Bonus for Approach 2
Also if you have multiple fields on that table you can use fields property to set the ordering(read only fields which are not specifically ordered will be shown at the end of the field list). Down side for this ordering approach is that you have to remember to reflect model changes to fields property every time you make a change in your model.

could I check through my view state of checkbox from db

My problem is next: I have device table with some params, one of them is device_able, if it is 'enable' I will do something with it. I added two actions to admin actions that change state of device_able on True or False and it works, but when I open any device from table my checkbox is always checked even if it False. I understood that I don't check data from db about state of checkbox but how to do it? I must use template but I do not understand how to connect my template that checks state of checkbox from db to my admin view of Dev app. Could you give me some useful links for exploring? Or I could check state in my admin.py file?
in my models.py
class Dev(models.Model):
#some params for device
device_able = models.BooleanField(default=False, choices=((True, 'enable'), (False, 'disable')))
def __unicode__(self):
return self.device_model
in admin.py
class DevAdminForm(forms.ModelForm):
class Meta:
widgets = {
'device_able': forms.CheckboxInput
}
full code of my admin.py
from django.contrib import admin
from dev.models import Dev
from django import forms
def make_enable(self, request, queryset):
queryset.update(device_able=True)
make_enable.short_description = "Mark selected devices as enable"
def make_disable(self, request, queryset):
queryset.update(device_able=False)
make_disable.short_description = "Mark selected devices as disable"
class DevAdminForm(forms.ModelForm):
class Meta:
widgets = {
'device_able': forms.CheckboxInput
}
class DevAdmin(admin.ModelAdmin):
fields = ['device_model', 'resolution', 'assets_format', 'scale_factor', 'device_able']
list_display = ('device_model', 'resolution', 'assets_format', 'scale_factor', 'device_able')
search_fields = ['device_model']
actions = [make_enable, make_disable]
form = DevAdminForm
class DevInline(admin.SimpleListFilter):
model = Dev
admin.site.register(Dev, DevAdmin)
If you're on the admin view you should use the AdminModel class from the admin module to have your model bound to the form. If you use a ModelForm you must supply the model it is bound to.
That said, in your cse the only need to subclass the AdminModle for your model is to insert tha admin actions, BooleanField are represented by CheckboxInput by default.
I would try in your admin.py:
class DevAdmin(admin.AdminModel):
def make_device_able()
...
actions = [make_device_able]
And register the the class with:
admin.site.register(Dev, DevAdmin)
Hope it helps.
I've found a solution for my problem. First of all, django is so cool that does all work for you. When I created my model I've set 'device_able' as models.CharField. It created in db field with type varchar. After that I've changed in my model 'device_able' to models.BooleanField and changed directly in db type of 'device_able' field on 'bool'. But my checkbox was always checked because only empty string returns False. when I created new project with with the same code, I mean device_able = models.BooleanField, my checkbox start to work correctly!

Setting the initial value for a readonly Choice (ForeignKey) field

I'm trying to make a CreateView have a readonly field with a set value, but I'm unable to make that work.
I have a model with a ForeignKey to another model:
class CompanyNote(TimeStampedModel):
company = models.ForeignKey(Company)
note = models.TextField(blank=True)
And I have a CreateView:
class CompanyNoteCreateView(CreateView):
model = models.CompanyNote
form_class = CompanyNoteForm
That uses a custom ModelForm:
class CompanyNoteForm(forms.ModelForm):
company = forms.ChoiceField(
widget=forms.widgets.Select(attrs={'readonly': 'readonly'}))
class Meta:
model = models.CompanyNote
As you see, the widget for the field in question is readonly. This is because I pick up the company as a part of the URL, as in company/1/note/add . I have no trouble picking up the "1" and finding the right company object, but I don't know how to set the readonly field.
I tried:
def get_initial(self):
initial = super(CompanyNoteCreateView, self).get_initial()
initial['company'] = self.get_company().id
return initial
But that didn't work. The Widget is empty, which may be the problem. Perhaps I'm barking up the wrong tree here. Any ideas welcome.
Have you tried setting the attribute in the Form's Meta class?
I experienced an issue where Form attributes were not applied for Model Fields if set in the base class definition, but they worked correctly in the Meta class:
class CompanyNoteForm(forms.ModelForm):
class Meta:
model = models.CompanyNote
widgets = {'company': forms.widgets.Select(attrs={'readonly': True,
'disabled': True})}
Otherwise check this answer out.
Worst case scenario, make company a hidden field?
Use a ModelChoiceField
class CompanyNoteForm(forms.ModelForm):
company = forms.ModelChoiceField(queryset=models.Company.objects.all(), widget=forms.widgets.Select(attrs={'readonly': 'readonly'}))
I could not find this answer anywhere, that I could actually get to work. But I found a different approach. Set the field to be hidden with forms.HiddenInput() widget. Then the value you pass in from the view will be assigned but the user cannot access it.
widgets = {'field_name': forms.HiddenInput()}
I'm using ModelForm class so my syntax might be different from yours.

Copying a Django Field description from an existing Model to a new one

I'm trying to dynamically generate a new Model, based on fields from an existing Model. Both are defined in /apps/main/models.py. The existing model looks something like this:
from django.db import models
class People(models.Model):
name = models.CharField(max_length=32)
age = models.IntegerField()
height = models.IntegerField()
I have a list containing the names of fields that I would like to copy:
target_fields = ["name", "age"]
I want to generate a new model the has all of the Fields named in target_fields, but in this case they should be indexed (db_index = True).
I originally hoped that I would just be able to iterate over the class properties of People and use copy.copy to copy the field descriptions that are defined on it. Like this:
from copy import copy
d = {}
for field_name in target_fields:
old_field = getattr(People, field_name) # alas, AttributeError
new_field = copy(old_field)
new_field.db_index = True
d[field_name] = new_field
IndexedPeople = type("IndexedPeople", (models.Model,), d)
I wasn't sure if copy.copy()ing Fields would work, but I didn't get far enough to find out: the fields listed in the class definition don't aren't actually included as properties on the class object. I assume they're used for some metaclass shenanigans instead.
After poking around in the debugger, I found some type of Field objects listed in People._meta.local_fields. However, these aren't just simple description that can be copy.copy()ed and used to describe another model. For example, they include a .model property referring to People.
How can I create a field description for a new model based on a field of an existing model?
From poking around in the debugger and the source: all Django models use the ModelBase metaclass defined in /db/models/base.py. For each field in a model's class definition, ModelBase's .add_to_class method will call the field's .contribute_to_class method.
Field.contribute_to_class is defined in /db/models/fields/__init__.py and it is what's responsible for associating a field definition with a particular model. The field is modified by adding the .model property and by calling the .set_attributes_from_name method with the name used in the model's class definition. This in turn adds adds the .attname and .column properties and sets .name and .verbose_name if necessary.
When I inspect the __dict__ property of a newly-defined CharField and compare it with that of a CharField that was already associated with a model, I also see that these are the only differences:
The .creation_counter property is unique for each instance.
The .attrname, .column and .model properties do not exist on the new instance.
The .name and .verbose_name properties is None on the new instance.
It doesn't seem possible to distinguish between .name/.verbose_name properties that were manually specified to the constructor and ones that were automatically generated. You'll need to chose either to always reset them, ignoring any manually-specified values, or never clear them, which would cause them to always ignore any new name they were given in the new model. I want to use the same name as the original fields, so I am not going to touch them.
Knowing what differences exist, I am using copy.copy() to clone the existing instance, then apply these changes to make it behave like a new instance.
import copy
from django.db import models
def copy_field(f):
fp = copy.copy(f)
fp.creation_counter = models.Field.creation_counter
models.Field.creation_counter += 1
if hasattr(f, "model"):
del fp.attname
del fp.column
del fp.model
# you may set .name and .verbose_name to None here
return fp
Given this function, I create the new Model with the following:
target_field_name = "name"
target_field = People._meta.get_field_by_name(target_field_name)[0]
model_fields = {}
model_fields["value"] = copy_field(target_field)
model_fields["value"].db_index = True
model_fields["__module__"] = People.__module__
NewModel = type("People_index_" + field_name, (models.Model,), model_fields)
It works!
Solution
There is build in way for fields copying Field.clone() - method which deconstructs field removing any model dependent references:
def clone(self):
"""
Uses deconstruct() to clone a new copy of this Field.
Will not preserve any class attachments/attribute names.
"""
name, path, args, kwargs = self.deconstruct()
return self.__class__(*args, **kwargs)
So you can use following util to copy fields ensuring that you'll not accidentally affect source fields of model you're copying from:
def get_field(model, name, **kwargs):
field = model._meta.get_field(name)
field_copy = field.clone()
field_copy.__dict__.update(kwargs)
return field_copy
Also can pass some regular kwargs like verbose_name and etc:
def get_field_as_nullable(*args, **kwargs):
return get_field(*args, null=True, blank=True, **kwargs)
Does not work for m2m fields inside of model definition. (m2m.clone() on model definition raises AppRegistryNotReady: Models aren't loaded yet)
Why this instead of abstract models?
Well, depends on case. Some times you don't need inheristance but actuall fields copying. When? For example:
I have a User model and model which represents an application (document for user data update request) for user data update:
class User(models.Model):
first_name = ...
last_name = ...
email = ...
phone_number = ...
birth_address = ...
sex = ...
age = ...
representative = ...
identity_document = ...
class UserDataUpdateApplication(models.Model):
# This application must ONLY update these fields.
# These fiends must be absolute copies from User model fields.
user_first_name = ...
user_last_name = ...
user_email = ...
user_phone_number = ...
So, i shouldn't carry out duplicated fields from my User model to abstract class due to the fact that some other non-user-logic-extending model wants to have exact same fields. Why? Because it's not directly related to User model - User model shouldn't care what depends on it (excluding cases when you want to extend User model), so it shouldn't be separated due to fact that some other model with it's own non User related logic want's to have exact same fields.
Instead you can do this:
class UserDataUpdateApplication(models.Model):
# This application must ONLY update these fields.
user_first_name = get_field(User, 'first_name')
user_last_name = get_field(User, 'last_name')
user_email = get_field(User, 'user_email')
user_phone_number = get_field(User, 'phone_number')
You also would make som util which would generate some abc class "on fly" to avoid code duplication:
class UserDataUpdateApplication(
generate_abc_for_model(
User,
fields=['first_name', 'last_name', 'email', 'phone_number'],
prefix_fields_with='user_'),
models.Model,
):
pass