django admin fieldsets - adding fields of related models - django

I have a model Employee with a OneToOne relationship with User. I'm trying to include in the Employee admin some User fields (first_name, last_name, username, email), in order to edit those fields directly from the Employee Add/Change form, but I still haven't figured out how to do it.
I see another time do this kind of thing in a similar way, but now I have a fieldset error:
Unknown field(s) (first_name) specified for Employee. Check fields/fieldsets/exclude attributes of class EmployeeAdmin
here's the code:
# FIELDSETS
default_employee_fieldset = (
('General', {
'fields': (
('user', 'full_name',),
),
}),
# ..OTHER FIELDS NOT INCLUDED
)
finance_fields_employee_fieldset = [
('General', {
'fields': (
('full_name',),
('first_name', 'last_name')
),
}),
# ..OTHER FIELDS NOT INCLUDED
]
# ADMIN FORM
class EmployeeAdminForm(forms.ModelForm):
class Meta:
model = Employee
def __init__(self, *args, **kwargs):
super(EmployeeAdminForm, self).__init__(*args, **kwargs)
self.fields['first_name'] = forms.CharField(_('first name'), max_length=30, blank=True)
self.fields['last_name'] = forms.CharField(_('last name'), max_length=30, blank=True)
if 'instance' in kwargs:
user = kwargs['instance'].user
self.fields['first_name'].initial = user.first_name
self.fields['last_name'].initial = user.last_name
# MODEL ADMIN
class EmployeeAdmin(ExtendedAdmin):
list_display = ('user', 'department', 'line_manager', 'acting_line_manager', 'jobtitle', 'office', 'payroll_id',)
search_fields = ('user__username', 'user__first_name', 'user__last_name',
'line_manager__user__first_name', 'line_manager__user__last_name')
list_filter = ('department', 'grade', 'clearance', 'office', 'user__is_active', 'user__is_staff',
'ready_for_paid_work')
fieldsets = default_employee_fieldset
def changelist_view(self, request, extra_context=None):
extra_context = {'title': 'Employee details'}
return super(EmployeeAdmin, self).changelist_view(request, extra_context)
def change_view(self, request, object_id, form_url='', extra_context=None):
if object_id:
try:
record = Employee.objects.get(id=object_id)
extra_context = {'title': 'Edit employee record: %s' % (str(record),)}
except (ValueError, Employee.DoesNotExist):
pass
# Generate a new CSRF token as this page contains sensitive data
rotate_token(request)
return super(EmployeeAdmin, self).change_view(request, object_id, form_url, extra_context)
def queryset(self, request):
qs = super(EmployeeAdmin, self).queryset(request)
if request.user.is_superuser:
return qs
elif request.user.has_perm('myapp.change_all_employees'):
if request.user.has_perm('myapp.change_ex_employees'):
return qs.filter(user__is_staff=True)
else:
return qs.filter(user__is_staff=True, user__is_active=True)
else:
return qs.filter(user__is_staff=True, user__is_active=True).filter(
Q(line_manager=request.user.employee) |
Q(acting_line_manager=request.user.employee) |
Q(user=request.user)
)
def get_form(self, request, obj=None, *args, **kwargs):
self.form = EmployeeAdminForm
if obj:
if request.user.has_perm('myapp.change_hr_employee_data'):
self.readonly_fields = ('user', )
self.exclude = None
self.fieldsets = finance_fields_employee_fieldset
elif request.user.has_perm('myapp.change_employee_data_operations'):
if request.user == obj.user:
# set readonly fields
self.exclude = None
self.fieldsets = finance_fields_employee_fieldset
else:
# set readonly fields
self.exclude = ('employer_pension_contribution', 'employee_pension_contribution',)
self.fieldsets = default_employee_fieldset
else:
# if user is in AAA department and not employee's linemanager and not own record
if request.user.employee.department == settings.DPTS['AAA']
if request.user == obj.user:
# set readonly fields
else:
# set readonly fields
self.exclude = ('account_number', 'sort_code', 'salary',
'employer_pension_contribution', 'employee_pension_contribution',)
self.fieldsets = default_employee_fieldset
else:
# if user is editing his own record
if request.user == obj.user:
# set readonly fields
self.exclude = ('account_reference', 'payroll_id',)
self.fieldsets = finance_fields_employee_fieldset[0:-2]
# if user is editing some for whom is line manager
else:
if request.user.employee == obj.line_manager or \
request.user.employee == obj.acting_line_manager:
# set readonly fields
self.fieldsets = default_employee_fieldset
self.exclude = ('employer_pension_contribution', 'employee_pension_contribution',)
else:
self.readonly_fields = ()
self.exclude = ()
self.fieldsets = default_employee_fieldset
form = super(EmployeeAdmin, self).get_form(request, *args, **kwargs)
form.request = request
return form
def save_model(self, request, obj, form, change):
if change:
old_obj = Employee.objects.get(id=obj.id)
if obj.jobtitle != old_obj.jobtitle:
employee_detail_change_notification(obj, 'job title', obj.jobtitle.name)
if obj.line_manager != old_obj.line_manager:
employee_detail_change_notification(obj, 'line manager', obj.line_manager)
if obj.acting_line_manager != old_obj.acting_line_manager:
employee_detail_change_notification(obj, 'acting line manager', obj.acting_line_manager)
if obj.home_address_line1 != old_obj.home_address_line1 or \
obj.home_address_line2 != old_obj.home_address_line2 or\
obj.home_address_line3 != old_obj.home_address_line3 or\
obj.home_address_city != old_obj.home_address_city or\
obj.home_address_postcode != old_obj.home_address_postcode or\
obj.home_address_country != old_obj.home_address_country or\
obj.home_address_phone != old_obj.home_address_phone or\
obj.personal_email != old_obj.personal_email:
new_address = '%s\n%s\n%s\n%s\n%s\n%s\n\nPhone: %s\nEmail: %s' %(
obj.home_address_line1,
obj.home_address_line2,
obj.home_address_line3,
obj.home_address_city,
obj.home_address_postcode,
obj.get_home_address_country_display(),
obj.home_address_phone,
obj.personal_email,
)
employee_detail_change_notification(obj, 'home address details', new_address)
if obj.marital_status != old_obj.marital_status:
employee_detail_change_notification(obj, 'marital_status', obj.get_marital_status_display())
obj.save()
admin.site.register(Employee, EmployeeAdmin)
Any help on this?

The reason Admin doesn't find user_name is that the user_name field isn't defined on your custom form at class level. It is only defined in __init__. When the admin inspects the form for available fields it has access only to the class, not an initialized form. So inspecting the form class doesn't show that user_name is available.
Define all the fields directly on class level instead of the form's __init__ method. If you don't need all the fields for all cases, then delete fields as needed from the form's fields dictionary in the __init__ method as needed.
You could maybe also do this in the reverse direction - adding Employee information to User edit page? This can be done as documented in https://docs.djangoproject.com/en/1.4/topics/auth/#adding-userprofile-fields-to-the-admin.
Finally, it seems you are reaching the stage where it might be easier to implement the features you want without using Admin - while Django's Admin does have a lot of extensibility features, at some point writing your own implementation will be easier than using Admin.

Related

How to fix DateRangeFilter in order the date widget to appear properly

Using adminactions to my project I came up with the problem that my date widget(the calendar) is not appearing to my form at all.
I successfully install the django-admin-rangefilter and import it as well.
my model
class Order(models.Model):
stem_date = models.DateField("STEM")
delivery_date = models.DateField("Delivery Date")
my admin.py
from rangefilter.filter import DateRangeFilter, DateTimeRangeFilter
list_filter = [('stem_date', DateRangeFilter), ('delivery_date', DateRangeFilter), ('cost_center')]
I am using django version = 2.2.6 and python version 3.5.2
whole class concerning the order
class OrderAdmin(ReadPermissionModelAdmin):
### Default ordering with REF ###
ordering = ('-ref',)
#This Snippet is to order foreign key form fields
def formfield_for_foreignkey(self, db_field, request, **kwargs):
if db_field.name == "port":
kwargs["queryset"] = Port.objects.order_by('port_name')
if db_field.name == "customer":
kwargs["queryset"] = Customer.objects.order_by('customer_name')
if db_field.name == "contact":
kwargs["queryset"] = Contact.objects.order_by('surname')
return super(OrderAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)
actions = [export_model_as_csv]
inlines = [Order_ProductsInline, Order_Product_ExtrasInline, Order_CommissionInline]
#Function to make ref id readonly after save
def get_readonly_fields(self, request, obj=None):
if obj: # when editing an object
return ['ref']
return self.readonly_fields
#sos
list_filter = [('stem_date', DateRangeFilter), ('delivery_date', DateRangeFilter), ('cost_center')]
#list_filter = [('stem_date', DateRangeFilter), ('delivery_date', DateRangeFilter)]
search_fields = ('customer__customer_name', 'vessel__ship_name', 'port__port_name', 'id', 'ref', 'supplier__customer_name')#if you look in foreign keys, must refferance with double underscore relevant field to other model
list_display = ['ref', 'customer', 'supplier', 'vessel', 'port', 'cost_center', 'delivery_date', 'show_details','cancelled',]
formfield_overrides = {
models.DecimalField: {'widget': TextInput(attrs={'size':'6'})},
models.BigIntegerField: {'widget': TextInput(attrs={'size':'6'})},
}
fieldsets = (
(None, {
'fields': (('ref'), ('customer', 'supplier'), ('vessel', 'imo', 'port', 'operator'), ('stem_date', 'delivery_date', 'contact'), 'cost_center', ('currency', 'bank_charges', 'branch'), ('cancelled'),('remarks'),)
}),
)
list_per_page = 20
#Function to show ID with 6 digits
def order_id(self, obj):
return str(obj.id).zfill(6)
def show_details(self, obj):
return_string = format_html('Details')
return return_string
show_details.allow_tags = True
show_details.short_description = 'Details'
def response_change(self, request, obj, post_url_continue=None):
#This makes the response go to inquiry full detail page for an edited order
return HttpResponseRedirect(reverse('orders', args=(obj.id,)))
#return HttpResponseRedirect("../../orders/%s" % obj.id)
def response_add(self, request, obj, post_url_continue=None):
#This makes the response go to inquiry full detail page for a newlly added order to inquiry list
return HttpResponseRedirect(reverse('orders', args=(obj.id,)))
#return HttpResponseRedirect("../../orders/%s" % obj.id)#strip leading zeros
admin.site.register(Order, OrderAdmin)
Have anyone any idea why my widget is not loaded at all?
Did you add rangefilter to your INSTALLED_APPS?
If so try like this:
from rangefilter.filter import DateRangeFilter
from .models import Order
#admin.register(Order)
class OrderAdmin(admin.ModelAdmin):
list_filter = (
('stem_date', DateRangeFilter), ('delivery_date', DateRangeFilter)
)

Changing a form field's 'required' property with save_model in Django admin

I have tried all the different examples and methods I could find on Stack Overflow for this, and for whatever reason, can't get this to work properly.
So in my admin.py I have a UserForm and and UserAdmin. Based on the condition where a boolean is checked within the form, I want to change the 'required' attribute on a few different form fields to 'false' so I can effectively save. I've used a few different print statements to ascertain that the 'required' is in fact getting changed to false when the condition is met, however, when I try and save, it won't let me as the fields highlight and say they're still required.
It is almost like the save_model doesn't care how I edit the form, that the old form and its 'required' attributes are overriding my changes. Thanks for any help!
admin.py
class UserForm(forms.ModelForm):
state = forms.CharField(min_length=2, widget=forms.TextInput(attrs={'placeholder':'CA'}))
zipcode = forms.CharField(max_length=5, min_length=5,)
class Meta:
model = User
fields = '__all__'
class UserAdmin(admin.ModelAdmin):
model = Order
form = UserForm
def save_model(self, request, obj, form, change):
if obj.pickup_only == 1:
form.fields['address'].required = False
form.fields['city'].required = False
form.fields['state'].required = False
form.fields['zipcode'].required = False
return super(UserAdmin, self).save_model(request, obj, form, change)
revised code for UserForm:
def clean(self):
cleaned_data = super(UserForm, self).clean()
address = cleaned_data.get('address')
state = cleaned_data.get('state')
city = cleaned_data.get('city')
zipcode = cleaned_data.get('zipcode')
pickup_only = cleaned_data.get('pickup_only')
if pickup_only == True:
# I STUCK IN ADDRESS TO EMPHASIZE THE ERROR IN SCREENSHOT
self.fields_required(['first_name', 'last_name', 'dob', 'phone', 'email', 'address'])
else:
self.cleaned_data['address', 'city', 'state', 'zip code'] = ''
return self.cleaned_data
def fields_required(self, fields):
for field in fields:
print(field)
# RETURNS: first_name, last_name, dob, phone, email, address
if not self.cleaned_data.get(field, ''):
msg = forms.ValidationError("This field is required. Custom.")
self.add_error(field, msg)
When I save I get this:
You might have to tweak it a little bit to fit your needs, but you could try something like this in your clean method:
def clean(self):
cleaned_data = super(UserForm, self).clean()
state = cleaned_data.get('state')
zipcode = cleaned_data.get('zipcode')
pickup_only = cleaned_data.get('pickup_only')
if pickup_only == True:
self.fields_required(['any_fields_required',])
else:
self.cleaned_data['state', 'zip code'] = ''
return self.cleaned_data
def fields_required(self, fields):
for field in fields:
if not self.cleaned_data.get(field, ''):
msg = forms.ValidationError("This field is required.")
self.add_error(field, msg)
Also, as one of the commenters mentioned, you should not specify your model=Order in the UserAdmin. Your UserAdmin is for the User model. That isn't the proper way to add inlines. See below:
# You can also use (admin.TabularInline) depending on your needs.
class OrderInline(admin.StackedInline):
model = Order
list_display = ('order_fields',)
fieldsets = (
((''), {'fields': ('order_fields',)}),
)
class UserAdmin(admin.ModelAdmin):
model = User
inlines = [OrderInline,]
Add an init method to your form:
class UserForm(forms.ModelForm):
state = forms.CharField(min_length=2, widget=forms.TextInput(attrs={'placeholder':'CA'}))
zipcode = forms.CharField(max_length=5, min_length=5,)
class Meta:
model = User
fields = '__all__'
def __init__(self, *args, **kwargs):
super(UserForm, self).__init__(*args, **kwargs)
self.fields['address'].required = False
self.fields['city'].required = False
self.fields['state'].required = False
self.fields['zipcode'].required = False
def clean(self):
cleaned_data = super(UserForm, self).clean()
address = cleaned_data.get('address')
state = cleaned_data.get('state')
city = cleaned_data.get('city')
zipcode = cleaned_data.get('zipcode')
pickup_only = cleaned_data.get('pickup_only')
if pickup_only == True:
self.fields_required(['first_name', 'last_name', 'dob', 'phone', 'email', 'address'])
else:
self.cleaned_data['address', 'city', 'state', 'zip code'] = ''
return self.cleaned_data
def fields_required(self, fields):
for field in fields:
if not self.cleaned_data.get(field, ''):
msg = forms.ValidationError("This field is required. Custom.")
self.add_error(field, msg)
Try this. If it works then I don't think you have to override the save method in your UserAdmin. I would avoid that anyway unless absolutely necessary.

Validate formset according master's form field value

I have a admin master detail, using tabular inline forms.
I have some special validations to accomplish:
If "field_type" is "list", validate that at least one item in the formset has been added.
But if not (field_type has another value), do not validate.
If "field_type" is "list", then, make visible the formset, otherwise hide it. This is javascript. I also must validate that on the server. I do that on the clean() of ValueItemInlineFormSet. The problem is that is now validating formset always and it should just happen when field_type = "list". How can I get the value of my master field into the formset?
class ValueItemInlineFormSet(BaseInlineFormSet):
def clean(self):
"""Check that at least one service has been entered."""
super(ValueItemInlineFormSet, self).clean()
if any(self.errors):
return
print(self.cleaned_data)
if not any(cleaned_data and not cleaned_data.get('DELETE', False) for cleaned_data in self.cleaned_data):
raise forms.ValidationError('At least one item required.')
class ValueItemInline(admin.TabularInline):
model = ValueItem
formset = ValueItemInlineFormSet
class MySelect(forms.Select):
def render_option(self, selected_choices, option_value, option_label):
if option_value is None:
option_value = ''
option_value = force_text(option_value)
data_attrs = self.attrs['data_attrs']
option_attrs = ''
if data_attrs and option_value:
obj = self.choices.queryset.get(id=option_value)
for attr in data_attrs:
attr_value = getattr(obj, attr)
option_attrs += 'data-{}={} '.format(attr, attr_value)
if option_value in selected_choices:
selected_html = mark_safe(' selected="selected"')
if not self.allow_multiple_selected:
# Only allow for a single selection.
selected_choices.remove(option_value)
else:
selected_html = ''
return format_html('<option value="{}" {}{}>{}</option>', option_value, option_attrs, selected_html, force_text(option_label))
class RequirementFieldForm(forms.ModelForm):
field_type = forms.ModelChoiceField(queryset=FieldType.objects.all(),
widget=MySelect(attrs={'data_attrs': ('identifier', 'name')}))
def __init__(self, *args, **kwargs):
self.qs = FieldType.objects.all()
super(RequirementFieldForm, self).__init__(*args, **kwargs)
class Meta:
model = RequirementField
fields = ['field_type', 'name', 'description', 'identifier', 'mandatory', 'order_nr', 'active']

How to pass request.user to a model's clean method for a foreignkey unique_together validation?

I'm trying to run a validation where a user can't enter the same name_field twice but other users entering the same name will not interfere.
I tried using "unique_together = (("username","name_field"))" but when a user enters the same value twice the server generates an integrity error as opposed to rendering a warning message next to the form field.
then I tried overriding the clean() method in my model, Which runs fine if I only check "field_name" like so:
def clean(self):
existing = self.__class__.objects.filter(
name_field=self.name_field).count()
if existing > 0:
raise ValidationError({'name_field':self.username })
But I am running into trouble when checking the username value, for instance:
def clean(self):
existing = self.__class__.objects.filter(
username=self.username, ###This part crashes!!! (username not found)
name_field=self.name_field).count()
if existing > 0:
raise ValidationError({'name_field':self.username })
I'm guessing due to it not being an actual field in the form its not present during the call to clean(). So my question is am I doing the validation correctly for this kind of problem? And how can I pass or where can I find the value for the current user from within a models clean method (in a safe way hopefully without adding fields to my form)?
views.py
def add_stuff(request):
if request.user.is_authenticated():
form = addStuff(request.POST or None)
if request.method == 'POST':
if form.is_valid():
sub_form = form.save(commit=False)
sub_form.username = request.user
sub_form.save()
return redirect('../somewhere_else/')
forms.py
class addStuff(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(addStuff, self).__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_tag = False
self.helper.layout = Layout(
'name_field',
'type_field',
ButtonHolder(
Submit('Save', 'Save')
),
)
class Meta:
model = UserStuff
fields = ('name_field',
'type_field',
)
models.py
class UserStuff(models.Model):
username = models.ForeignKey(User)
name_field = models.CharField(max_length=24, blank=False,null=False)
type_field = models.CharField(max_length=24, blank=True,null=True)
def clean(self):
existing = self.__class__.objects.filter(
username=self.username, ###This part crashes!!! (username not found)
name_field=self.name_field).count()
if existing > 0:
raise ValidationError({'name_field':self.username })
def __unicode__(self):
return "%s For User: \"%s\" " % (self.name_field, self.username)
class Meta:
managed = True
db_table = 'my_db_table'
unique_together = (("username","name_field"))
Thanks for any insight!
I now am running the clean override from the form instead of the model (as recommended by Daniel). This has solved a bunch of issues and I now have a working concept:
models.py
class UserStuff(models.Model):
username = models.ForeignKey(User)
name_field = models.CharField(max_length=24, blank=False,null=False)
type_field = models.CharField(max_length=24, blank=True,null=True)
def clean(self):
existing = self.__class__.objects.filter(
username=self.username, ###This part crashes!!! (username not found)
name_field=self.name_field).count()
if existing > 0:
raise ValidationError({'name_field':self.username })
def __unicode__(self):
return "%s For User: \"%s\" " % (self.name_field, self.username)
class Meta:
managed = True
db_table = 'my_db_table'
forms.py
class addStuff(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(addStuff, self).__init__(*args, **kwargs)
initial = kwargs.pop('initial')
self.username = initial['user']
self.helper = FormHelper()
self.helper.form_tag = False
self.helper.layout = Layout(
'name_field',
'type_field',
ButtonHolder(
Submit('Save', 'Save')
),
)
def clean(self):
cleaned_data = super(addStuff, self).clean()
name_field = self.cleaned_data['name_field']
obj = UserStuff.objects.filter(username_id=self.username.id,
name_field=name_field,
)
if len(obj) > 0:
raise ValidationError({'name_field':
"This name already exists!" } )
return cleaned_data
class Meta:
model = UserStuff
fields = ('name_field',
'type_field',
)
views.py
def add_stuff(request):
if request.user.is_authenticated():
form = addStuff(request.POST or None,
initial={'user':request.user})
if request.method == 'POST':
if form.is_valid():
sub_form = form.save(commit=False)
sub_form.username = request.user
sub_form.save()
return redirect('../somewhere_else/')
best of luck!

Django: How to remove fields from the admin form for specific users?

My admin looks like this (with no exclude variable):
class MovieAdmin(models.ModelAdmin)
fields = ('name', 'slug', 'imdb_link', 'start', 'finish', 'added_by')
list_display = ('name', 'finish', 'added_by')
list_filter = ('finish',)
ordering = ('-finish',)
prepopulated_fields = {'slug': ('name',)}
form = MovieAdminForm
def get_form(self, request, obj=None, **kwargs):
form = super(MovieAdmin, self).get_form(request, obj, **kwargs)
form.current_user = request.user
return form
admin.site.register(Movie, MovieAdmin)
The form:
class MovieAdminForm(forms.ModelForm):
class Meta:
model = Movie
def save(self, commit=False):
instance = super(MovieAdminForm, self).save(commit=commit)
if not instance.pk and not self.current_user.is_superuser:
if not self.current_user.profile.is_manager:
instance.added_by = self.current_user.profile
instance.save()
return instance
I'm trying to remove the added_by field for users since I'd prefer to populate that from the session. I've tried methods from the following:
Django admin - remove field if editing an object
Remove fields from ModelForm
http://www.mdgart.com/2010/04/08/django-admin-how-to-hide-fields-in-a-form-for-certain-users-that-are-not-superusers/
However with each one I get: KeyError while rendering: Key 'added_by' not found in Form. It seems I need to remove the field earlier in the form rendering process but I'm stuck on where to do this.
So how can I exclude the added_by field for normal users?
You're probably getting that error when list_display is evaluated. You can't show a field that's excluded. The version with added_by removed also needs a corresponding list_display.
def get_form(self, request, obj=None, **kwargs):
current_user = request.user
if not current_user.profile.is_manager:
self.exclude = ('added_by',)
self.list_display = ('name', 'finish')
form = super(MovieAdmin, self).get_form(request, obj, **kwargs)
form.current_user = current_user
return form