I am using Flask-Admin for my Flask-based project. In it, I have some models (using peewee) where the primary-key is user-set, such as username for a User. However Flask-Admin is not showing these fields in the model's create/edit pages.
Now, when I try to create a new user, the "Save" button gives a peewee.UserDoesNotExist error, and the "Save & Add" says "Record successfully created" twice but doesn't actually do anything.
I had extended the save() method to auto-generate the username from the name if it's unset, but the problem persisted even when I removed the overriding.
The code...
Here's what my User model looks like:
# import peewee as pw
class User(BaseModel, UserMixin):
username = pw.CharField(32, primary_key=True)
password = pw.CharField(512, null=True)
name = pw.CharField(64)
# ... other fields not shown ... #
def save(self, *args, **kwargs):
# Set the username if field is blank
if self.username == 'auto' or not self.username:
self.username = self.name.replace(' ', '').lower()
# Do the real save
super(User, self).save(*args, **kwargs)
Here's my admin code:
# from flask_admin.contrib.peewee.view import ModelView
class AdminModelUser(ModelView):
can_create = True
column_list = ('username', 'name', 'group', 'active')
admin.add_view(AdminModelUser(User, name='Users', category='Accounts'))
Trying out stuff
I later tried to override the get_form() method, to use wtfpeewee directly and allow the pk, like this:
# from wtfpeewee.orm import model_form
class AdminModelUser(ModelView):
...
def get_form(self):
return model_form(User, allow_pk=True)
Now the field is showing, but saving still does not work. When I edit the username of an existing user, the admin says "Record was successfully saved", but it doesn't get saved. And when I try to create a new user, I still get a peewee.UserDoesNotExist error.
My guess is that I've done the overriding in the wrong place, with the fields showing in the form but not in the save methods. I couldn't find any mention of this in the docs: does anyone know how to do it?
When you've got a non-integer primary key, you must call save() with force_insert=True to add a new row.
http://docs.peewee-orm.com/en/latest/peewee/models.html#non-integer-primary-keys-composite-keys-and-other-tricks
Related
I would like to prevent a save in a django model when a certain constraint is not met and give a validation error so that a django staff user knows what went wrong.
The constraint is the count() from an intermediate table specified using the through parameter.
models.py:
class Goal(models.Model):
name = models.CharField(max_length=128)
class UserProfile(models.Model):
goals = models.ManyToManyField(Goal, through=UserProfileGoals, blank=True)
class UserProfileGoal(models.Model):
goal = models.ForeignKey(Goals)
user_profile = models.ForeignKey(UserProfile)
class UserGoalConstraint(models.Model):
user_profile = models.OneToOneField(UserProfile)
max_goals = models.PositiveIntegerField()
So the UserGoalConstraint.max_goals gives me the number of the maximum definable UserProfile.goal which are stored in the UserProfileGoal model (same UserGoal can be stored more often to the UserProfile)
I have read and tried solutions from several posts, which are using ModelForm's clean(), Model's clean() and pre_save signal events,
but the actual problem I have is, how do I know if it is just an update or a new database entry, because
class UserProfileGoal(models.Model):
goal = models.ForeignKey(Goals)
user_profile = models.ForeignKey(UserProfile)
def clean(self):
goal_counter = self.user_profile.goals.count() + 1
try:
qs = UserGoalConstraint.objects.get(user_profile=self.user_profile)
except UserGoalConstraint.DoesNotExist:
raise ObjectDoesNotExist('Goal Constraint does not exist')
if goal_counter > qs.max_goals:
raise ValidationError('There are more goals than allowed goals')
does not really work, as clean() can also be an update and the +1 gives me a wrong result which leads to the ValidationError.
My client should use the django-admin interface to add goals to the user profile directly via an Inline:
admin.py:
class UserProfileGoalInline(admin.TabularInline):
model=UserProfileGoal
class UserProfileAdmin(admin.ModelAdmin)
...
inlines = [UserProfileGoalInline, ]
So he needs to be nicely informed when he adds to many goals to a user profile.
Maybe I am missing something obvious on how to solve this problem...?
I am looking for a working and somehow user friendly solution (= get informed in admin interface).
[UPDATE]:
I tried know to check wether it is created or not with the self.pk is None trick at the beginning of the clean()
if self.pk is not None:
return # it is not a create
...
I thought that would deal with the issue...
However, in the admin inline, when the staff user adds more than one goal at the same time, the clean() does not recognize these. Debug output shows for 2 goals added, that the goal counter holds the same number even the second entry should have one more and should give an validation error
Thanks to #zaidfazil for a starting solution:
class UserProfileGoalForm(forms.ModelForm):
class Meta:
model = UserProfileGoal
...
def clean(self):
cleaned_data = super(UserProfileGoalForm, self).clean()
if self.instance.pk is not None:
return cleaned_data
user_profile = self.cleaned_data.get('user_profile')
goal_count = user_profile.goals.count()
goal_limit = UserGoalConstraint.objects.get(user_profile=user_profile).max_goals # removed try catch for get for easier reading
if goal_count >= goal_limit:
raise ValidationError('Maximum limit reached for goals')
return cleaned_data
However, this does not handle the inline in the UserProfile admin interface: clean() won't handle correctly if you add more than one Goal at the same time and press save.
So I applied the UserProfileGoalForm to the inline and defined max_num :
class UserProfileGoalInline(admin.TabularInline):
model=UserProfileGoal
form = UserProfileGoalForm
def get_max_num(self, request, obj=None, **kwargs):
if obj is None:
return
goal_limit = UserGoalConstraint.objects.get(training_profile=obj).max_goals
return goal_limit # which will overwrite the inline's max_num attribute
Now my client can only add at maximum the max_goals value from the UserGoalConstraint, and also a possible admin form for UserProfileGoal will handle the constraint:
class UserProfileGoalAdmin(admin.ModelAdmin):
form = UserProfileGoalForm
You could handle it in ModelForm clean method,
class GoalForm(forms.ModelForm):
class Meta:
model = Goal
.....
def clean(self):
cleaned_data = super(GoalForm, self).clean()
if self.instance.pk is not None:
return cleaned_data
goal_limit = self.user_profile.usergoalconstraint.max_goals
goal_count = self.user_profile.goals.count()
if goal_count >= goal_limit:
raise ValidationError("Maximum limit reached for goals")
return cleaned_data
I'm making a Django app with custom users. I've outlined the key components of my problem below, missing code is denoted by '...'. My custom user model has a foreign key relationship as follows:
class MyCustomUser(models.AbstractBaseUser, models.PermissionsMixin)
...
location = models.ForeignKey(Location)
class Location(models.Model)
name = models.CharField(max_length=50, blank=True, null=True)
I've written a custom user form that includes this field as follows:
class MyCustomUserCreationForm(models.ModelForm)
...
location = forms.ModelChoiceField(Location.objects.all())
This all appears to be working correctly, however, there is no plus button to the right of the select field for location. I want to be able to add a location when I create a user, in the same way that you can add polls when creating choices in the Django tutorial. According to this question, I might not see the green plus if I don't have permission to change the model, but I am logged in as a superuser with all permissions. Any idea what I'm doing wrong?
You need to set a RelatedFieldWidgetWrapper wrapper in your model form:
The RelatedFieldWidgetWrapper (found in django.contrib.admin.widgets)
is used in the Admin pages to include the capability on a Foreign Key
control to add a new related record. (In English: puts the little green plus sign to the right of the control.)
class MyCustomUserCreationForm(models.ModelForm)
...
location = forms.ModelChoiceField(queryset=Location.objects.all())
def __init__(self, *args, **kwargs):
super(MyCustomUserCreationForm, self).__init__(*args, **kwargs)
rel = ManyToOneRel(self.instance.location.model, 'id')
self.fields['location'].widget = RelatedFieldWidgetWrapper(self.fields['location'].widget, rel, self.admin_site)
I could make a mistake in the example code, so see these posts and examples:
RelatedFieldWidgetWrapper
More RelatedFieldWidgetWrapper – My Very Own Popup
Django admin - How can I add the green plus sign for Many-to-many Field in custom admin form
How can I manually use RelatedFieldWidgetWrapper around a custom widget?
Django: override RelatedFieldWidgetWrapper
I have created method based on the answers above:
def add_related_field_wrapper(form, col_name):
rel_model = form.Meta.model
rel = rel_model._meta.get_field(col_name).rel
form.fields[col_name].widget =
RelatedFieldWidgetWrapper(form.fields[col_name].widget, rel,
admin.site, can_add_related=True, can_change_related=True)
And then calling this method from my form:
class FeatureForm(forms.ModelForm):
offer = forms.ModelChoiceField(queryset=Offer.objects.all(), required=False)
package = forms.ModelChoiceField(queryset=Package.objects.all(), required=False)
def __init__(self, *args, **kwargs):
super(FeatureForm, self).__init__(*args, **kwargs)
add_related_field_wrapper(self, 'offer')
add_related_field_wrapper(self, 'package')
That works fine on Django 1.8.2.
Google pointed me to this page when searching how to get a "+" icon next to fields in a custom form with ForeignKey relationship, so I thought I'd add.
For me, using django-autocomplete-light did the trick very well, using the "add another" functionality.
You don't even need to go that far, and besides, these answers are probably outdated as NONE of them worked for me in any capacity.
What I did to solve this is, as long as you have the ForeignKey field already in your model, then you can just create your custom ModelChoiceField:
class LocationModelChoiceField(forms.ModelChoiceField):
def label_from_instance(self, obj):
return "%" % (obj.name)
The key next is NOT to create a custom field for the ModelChoiceField in your ModelForm (ie location = forms.ModelChoiceField(Location.objects.all()))
In other words, leave that out and in your ModelForm have something like this:
class UserAdminForm(forms.ModelForm):
class Meta:
model = User
fields = '__all__'
Lastly, in your ModelAdmin:
class UserAdmin(admin.ModelAdmin):
model = User
form = UserAdminForm
def formfield_for_foreignkey(self, db_field, request, **kwargs):
if db_field.name == 'location':
return LocationModelChoiceField(queryset=Location.objects.order_by('name')) # if you want to alphabetize your query
return super().formfield_for_foreignkey(db_field, request, **kwargs)
Alternative Method : Using .remote_field instead of rel
def add_related_field_wrapper(self,form, col_name):
rel_model = form.Meta.model
rel = rel_model._meta.get_field(col_name).remote_field
form.fields[col_name].widget = RelatedFieldWidgetWrapper(form.fields[col_name].widget, rel, admin.site, can_add_related=True, can_change_related=True)
def __init__(self, *args, **kwargs):
super(CustomerAdminForm, self).__init__(*args, **kwargs)
self.add_related_field_wrapper(self, 'offer')
self.add_related_field_wrapper(self, 'package')
Thankyou,
i'm using Django registration, and unlike everybody else, I seem to have the opposite problem. My User object is saved fine, but my UserProfile object isn't!
I followed this website:
http://birdhouse.org/blog/2009/06/27/django-profiles/
which was really good, and so now i have:
class ProfileForm(forms.ModelForm):
YESNO = [
(True,mark_safe('<img src="/static_files/greenTick.png"/>')),
(False,mark_safe('<img src="/static_files/redCross.png"/>'))]
class Meta:
model = UserProfile
exclude = ('isTweeting','points','user')
fields = ('display_name','first_name','last_name','email','gravatar')
def __init__(self, *args, **kwargs):
super(ProfileForm, self).__init__(*args, **kwargs)
self.fields['email'].initial = self.instance.user.email
self.fields['first_name'].initial = self.instance.user.first_name
self.fields['last_name'].initial = self.instance.user.last_name
self.fields['display_name'].initial = self.instance.user.username
self.fields['gravatar'].initial = self.instance.usesGravatar
#add in the input to size it juuuuust right.
email = forms.EmailField(label="Primary email",help_text='',widget=forms.TextInput(attrs={'class': 'wideInput'}))
first_name = forms.Field(label="First name",help_text='',required=False,widget=forms.TextInput(attrs={'class': 'wideInput'}))
last_name = forms.Field(label="Last name",help_text='',required=False,widget=forms.TextInput(attrs={'class': 'wideInput'}))
display_name = forms.Field(label="Display name",help_text='',widget=forms.TextInput(attrs={'class': 'wideInput'}))
gravatar = ImgModelChoiceField(label='Gravatar', choices=YESNO, widget=forms.RadioSelect(renderer=ImgRadioFieldRenderer))
def save(self, *args, **kwargs):
"""
Update the primary email address on the related User object as well.
"""
u = self.instance.user
u.email = self.cleaned_data['email']
u.username = self.cleaned_data['display_name']
u.first_name = self.cleaned_data['first_name']
u.last_name = self.cleaned_data['last_name']
u.save()
self.instance.gravatar = (self.cleaned_data['gravatar'] == 'True')
profile = super(ProfileForm, self).save(*args,**kwargs)
return profile
this object is passed into the Django-profile as the form_class for use, as described in the website above. The problem i have is that when i submit my form, while the "User" data is updated correctly - any changes in the email or whatnot propagate to the db - the change to the "gravatar" value is not sent. Also no error is thrown.
Any ideas what I should do?
I'm going to hazard a guess here
def save(self, *args, **kwargs):
...
self.instance.gravatar = (self.cleaned_data['gravatar'] == 'True')
profile = super(ProfileForm, self).save(*args,**kwargs)
return profile
It seems you are using a custom widget, and by the looks of things you need to change the string 'True' (passed back from the form) to a boolean True before saving it to the DB. When you call save() on the next line though, the ModelForm will overwrite the value you have given self.instance.gravatar with the data directly from the form's cleaned_data:
https://github.com/django/django/blob/master/django/forms/models.py#L351
Also, in __init__, you don't need to include
self.fields['gravatar'].initial = self.instance.usesGravatar
as this field is already bound to the model form and will be automatically populated (if the UserProfile is being edited for example) when you instantiate the form along with an instance in your view.
Finally, in your Meta, you don't need to include both excludes and fields, one or the other should be fine.
First of all consider suggestions from #Timmy.
The only thing which else should be noticed is in this line:
profile = super(ProfileForm, self).save(*args,**kwargs)
By default the save method has commit=True. Verify that the function which is calling this Form might be sending commit=False in args or kwargs. If yes then you have to manually save the profile profile.save() before returning because commit=False means the changes will not reflect to the db.
And why you are allowing user to update both username and email? How you will keep track of the registration process if you are allowing to update both fields? Usually user sign up using their email. Define your criteria which field(username or email) you want to kept unchanged.
Update
Also you are doing one more thing wrong in your save function. You are updating the email, username, firstname and lastname in user taken from instance.user. But that instance overwritten when profile form default save is call here profile = super(ProfileForm, self).save(*args,**kwargs). What you should do is to update those fields using the user = profile.user The profile which is returned by the super. Your save function should be looked like this:
def save(self, *args, **kwargs):
"""
Update the primary email address on the related User object as well.
"""
profile = super(ProfileForm, self).save(*args,**kwargs)
u = profile.user
u.email = self.cleaned_data['email']
u.username = self.cleaned_data['display_name']
u.first_name = self.cleaned_data['first_name']
u.last_name = self.cleaned_data['last_name']
u.save()
#profile.save() #if commit=False in kwargs
return profile
I have a Django model, which has a foreign key owner, referring to the user who owns this object.
To let other users edit these objects, I currently use a forms.ModelForm, which works fine so far.
But I know want to let the owner and only the owner change the owner of the object he owns (what an ownage! :). Thus I tried the following:
class FolderForm(forms.ModelForm):
def __init__(self, user, *args, **kwargs):
if kwargs.get("instance", False):
if user == kwargs["instance"].owner:
self._meta.fields += ("owner",)
super(FolderForm, self).__init__(*args, **kwargs)
class Meta:
model = Folder
fields = (
"name",
"description",
)
But this doesn't work, since Django uses some metaclass-magic to set the fields on the model, which seems to be done before my subclassed __init__ is called.
Anyone ever did something like this?
I like to use closure normally
def make_form(exclude_user=True):
class Form(forms.ModelForm):
class Meta:
model = Folder
exclude = ['user'] if exclude_user else None
return Form
form_cls = make_form(request.user != folder.owner)
Why don't you create two Forms:
One that excludes owner for users that don't own the data (mouthful) and do a simple if statement in your view:
if request.user == Model.owner:
form = OwnerForm
else:
form = OthersForm
Keep it as simple as possible has HUGE wins down the line.
I am trying to validate the uniqueness of an email address in datastore. The problem is that this does not allow me to edit/update the entry (e.g. if I only want to change the display_name - see models.py below).
I am submitting a form build using djangoforms (with an datastore entry prefilled as I already know the key of the datastore entry):
forms.UserForm(instance=db.get(db.Key(key)))
After submitting the form using POST method I get the details from datastore and associate these with the submitted form:
entry = db.get(db.Key(self.request.get('key')))
data = forms.UserForm(data=self.request.POST, instance=entry)
if data.is_valid():
...
The form then validates using the following form class (in forms.py):
from django import forms
from google.appengine.ext.db import djangoforms
import models
class UserForm(djangoforms.ModelForm):
class Meta:
model = models.AuthorizedUsers
def clean_email(self):
"""Prevent duplicate email addresses."""
if self.Meta.model.all().filter('email =', self.cleaned_data['email']).count():
raise forms.ValidationError('Duplicate Entry: %s.' %
self.cleaned_data['email'])
return self.cleaned_data['email']
Using the following model (in models.py)
class AuthorizedUsers(db.Model):
"""Base model class for storing user permissions."""
email = db.StringProperty()
display_name = db.StringProperty()
Any suggestions what I am missing here? How can I prevent the raising of ValidationError when just updating an entry? Keep in mind that I do want to prevent the adding of a new datastore entry with the same email address.
Thanks in advance for your help!
You can check to see if your email field has changed before checking whether or not to validate it against all previous email addresses. Your clean method would be changed to:
def clean_email(self):
"""Prevent duplicate email addresses."""
if 'email' in self.changed_data:
if self.Meta.model.all().filter('email =', self.cleaned_data['email']).count():
raise forms.ValidationError('Duplicate Entry: %s.' %
self.cleaned_data['email'])
return self.cleaned_data['email']