I want to hide field name when user is creating new object, but it must be visible if user wants to edit this object.
I tried exclude method but it makes field invisible when i try to edit this field. for example i want to hide status field.
class Toys(BaseModel):
name = models.CharField(max_length=255)
tags = models.ManyToManyField(Tag, related_name='Item_tags')
price = models.CharField(max_length=255)
status = models.BooleanField(default=False)
In the model admin that you register in your admin.py file you can overload the get_form(self, request, obj=None, **kwargs) method. As you can see it takes the obj argument, it is None only on add (not None on change).
From there you could mess around with the form to exclude the form field "name" from it only if the obj is None.
In Django 1.10 that method is in django.contrib.admin.options.ModelAdmin.get_form.
EDIT 1 (this is by far not the best solution)
I can't give you a full solution here, but you can start with something like:
# admin.py
from django.contrib import admin
from models import Toys
class ToysModelAdmin(admin.ModelAdmin):
def get_form(self, request, obj=None, **kwargs):
# all the code you have in the original
# django.contrib.admin.options.ModelAdmin.get_form
# up to the last try except
if obj is not None:
defaults['fields'] = ('tags', 'price', 'status', )
try:
return modelform_factory(self.model, **defaults)
except FieldError as e:
raise FieldError(
'%s. Check fields/fieldsets/exclude attributes of class %s.'
% (e, self.__class__.__name__)
)
admin.site.register(Toys, ToysModelAdmin)
EDIT 2 (this is a better example)
# admin.py
from collections import OrderedDict
from django.contrib import admin
from models import Toys
class ToysModelAdmin(admin.ModelAdmin):
# this is not perfect as you'll need to keep track of your
# model changes also here, but you won't accidentally add
# a field that is not supposed to be editable
_add_fields = ('tags', 'price', 'status', )
def get_form(self, request, obj=None, **kwargs):
model_form = super(ToysModelAdmin, self).get_form(
request, obj, **kwargs
)
if obj is None:
model_form._meta.fields = self._add_fields
model_form.base_fields = OrderedDict(**[
(field, model_form.base_fields[field])
for field in self._add_fields
])
return model_form
I had same problem.
I wanted to exclude some columns only in create form.
You can override get_exclude() method
from django.contrib import admin
from . import models
#admin.register(models.Toys)
class ToysAdmin(admin.ModelAdmin):
def get_exclude(self, request, obj=None):
exclude = list(super().get_exclude(request, obj) or [])
if obj==None:
# columns to exclude when it's create form
exclude += ["status","column_to_hide_on_create"]
return tuple(set(exclude))
Related
I have the following example where I have buildings(address, location,...) and apartments(name, size, type, building). One building containing multiple apartments.
class BuildingAdmin(admin.ModelAdmin):
inlines = [ApartmentInline,]
class ApartmentInline(admin.StackedInline):
def get_formset(self, request, obj=None, **kwargs):
formset = super(ApartmentInline, self).get_formset(request, obj=None, **kwargs)
#Here i'd like to see the values of inline fields, for example size or building that.
#Similar to how one can access ModelAdmin fields with obj.location within get_form
formset.form.base_fields["type"].widget = SelectMultiple(choices=custom_choices)
return formset
I'd like to be able to get the current apartments instance and field values when editing the object (for example size), so that I can create custom choices (querying other DB's or API's) for another field (type).
To modify the inline form widget you can override ModelInlineForm which gives each inline instance access after initializing.
from django.contrib import admin
from django.urls import resolve
class ApartmentInlineForm(forms.ModelForm):
class Meta:
model = A
fields = (
'...',
'...',
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# calculate widget choices
self.fields['type'].widget = SelectMultiple(choices=custom_choices)
class ApartmentInline(admin.StackedInline):
model = Apartment
form = ApartmentInlineForm
I want to connect each post with the logged in user who posted it.
models.py
from django.conf import settings
from django.db import models
# Create your models here.
class Campagin(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL, default=1)
title = models.CharField(max_length=120)
media = models.FileField()
description = models.TextField(max_length=220)
timestamp = models.DateTimeField(auto_now=False, auto_now_add=True)
updated = models.DateTimeField(auto_now=True, auto_now_add=False)
def __str__(self):
return self.title`
As you can see the posts were made by two different users, but the relation shows that it is made by the first user
this image shows the registered users..
Views.py
class NewCampagin(LoginRequiredMixin, CreateView):
template_name = 'campagin/new_campagin.html'
model = Campagin
fields = ['title','media','description']
def get_absolute_url(self):
return reverse('campagin:active_campagin')
Okay so CreateView allows you to specify the model and fields attributes to implicitly create a form for you. It's quite neat for quick form submissions but in your case, you will need to make some customizations before saving the Campaign object into the database (linking up the current logged in user).
As a result, you will need to create your own form first (create a file called forms.py which can be next to your views.py) and enter this code:
class CampaignForm(ModelForm): # Import ModelForm too.
def __init__(self, *args, **kwargs):
# We need to get access the currently logged in user so set it as an instance variable of CampaignForm.
self.user = kwargs.pop('user', None)
super(CampaignForm, self).__init__(*args, **kwargs)
class Meta:
model = models.Campaign # you need to import this from your models.py class
fields = ['title','media','description']
def save(self, commit=True):
# This is where we need to insert the currently logged in user into the Campaign instance.
instance = super(CampaignForm, self).save(commit=False)
# Once the all the other attributes are inserted, we just need to insert the current logged in user
# into the instance.
instance.user = self.user
if commit:
instance.save()
return instance
Now that we have our forms.py all ready to go we just need to modify your views.py:
class NewCampagin(LoginRequiredMixin, CreateView):
template_name = 'campagin/new_campagin.html'
form_class = forms.CampaignForm # Again, you'll need to import this carefully from our newly created forms.py
model = models.Campaign # Import this.
queryset = models.Campaign.objects.all()
def get_absolute_url(self):
return reverse('campagin:active_campagin') # Sending user object to the form, to verify which fields to display/remove (depending on group)
def get_form_kwargs(self):
# In order for us to access the current user in CampaignForm, we need to actually pass it accross.
# As such, we do this as shown below.
kwargs = super(NewCampaign, self).get_form_kwargs()
kwargs.update({'user': self.request.user})
return kwargs
What's actually happening with my POST requests under the bonnet??
Note: This is just extra information for the sake of learning. You do
not need to read this part if you don't care about how your class
based view is actually handling your post request.
Essentially CreateView looks like this:
class CreateView(SingleObjectTemplateResponseMixin, BaseCreateView):
"""
View for creating a new object instance,
with a response rendered by template.
"""
template_name_suffix = '_form'
Doesn't look that interesting but if we analyse BaseCreateView:
class BaseCreateView(ModelFormMixin, ProcessFormView):
"""
Base view for creating an new object instance.
Using this base class requires subclassing to provide a response mixin.
"""
def post(self, request, *args, **kwargs):
self.object = None
return super(BaseCreateView, self).post(request, *args, **kwargs)
we can see we are inheriting from two very important classes ModelFormMixin and ProcessFormView. Now the line, return super(BaseCreateView, self).post(request, *args, **kwargs), essentially calls the post function in ProcessFormView which looks like this:
def post(self, request, *args, **kwargs):
"""
Handles POST requests, instantiating a form instance with the passed
POST variables and then checked for validity.
"""
form = self.get_form()
if form.is_valid():
return self.form_valid(form)
else:
return self.form_invalid(form)
As you can see, your CreateView really just boils down to this small post function which simply gets a specified form and validates + saves it. There's 2 questions to ask at this point.
1) What does form = self.get_form() do since I didn't even specify my form?
2) What is self.form_valid(form) actually doing?
To answer the first question, self.get_form() essentially calls another function form_class = self.get_form_class() and this function is actually found in ModelFormMixin (the one where inherited from!):
def get_form_class(self):
"""
Returns the form class to use in this view.
"""
if self.fields is not None and self.form_class:
raise ImproperlyConfigured(
"Specifying both 'fields' and 'form_class' is not permitted."
)
if self.form_class:
return self.form_class
else:
if self.model is not None:
# If a model has been explicitly provided, use it
model = self.model
elif hasattr(self, 'object') and self.object is not None:
# If this view is operating on a single object, use
# the class of that object
model = self.object.__class__
else:
# Try to get a queryset and extract the model class
# from that
model = self.get_queryset().model
if self.fields is None:
raise ImproperlyConfigured(
"Using ModelFormMixin (base class of %s) without "
"the 'fields' attribute is prohibited." % self.__class__.__name__
)
# THIS IS WHERE YOUR FORM WAS BEING IMPLICITLY CREATED.
return model_forms.modelform_factory(model, fields=self.fields)
As you can see, this function is where your form was being implicitly created (see very last line). We needed to add more functionality in your case so we created our own forms.py and specified form_class in the views.py as a result.
To answer the second question, we need to look at the function (self.form_valid(form)) call's source code:
def form_valid(self, form):
"""
If the form is valid, save the associated model.
"""
# THIS IS A CRUCIAL LINE.
# This is where your actual Campaign object is created. We OVERRIDE the save() function call in our forms.py so that you could link up your logged in user to the campaign object before saving.
self.object = form.save()
return super(ModelFormMixin, self).form_valid(form)
So here we are simply saving the object.
I hope this helps you!
More information at https://docs.djangoproject.com/en/1.10/ref/class-based-views/generic-editing/#createview
I have two apps in Django where one app's model (ScopeItem) on its instance creation must create an instance of the other app's model as well (Workflow); i.e. the ScopeItem contains it's workflow.
This works nicely when tried from the shell. Creating a new ScopeItem creates a Workflow and stores it in the ScopeItem. In admin I get an error, that the workflow attribute is required. The attribute is not filled in and the model definition requires it to be set. The overwritten save method though does this. Hence my question is, how to call save before the check in admin happens?
If I pick an existing Workflow instance in admin and save (successfully then), then I can see that my save method is called later and a new Workflow is created and attached to the ScopeItem instance. It is just called too late.
I am aware that I could allow empty workflow attributes in a ScopeItem or merge the ScopeItem and the Workflow class to avoid the issue with admin. Both would cause trouble later though and I like to avoid such hacks.
Also I do not want to duplicate code in save_item. Just calling save from there apparently does not cut it.
Here is the code from scopeitems/models.py:
class ScopeItem(models.Model):
title = models.CharField(max_length=64)
description = models.CharField(max_length=4000, null=True)
workflow = models.ForeignKey(Workflow)
def save(self, *args, **kwargs):
if not self.id:
workflow = Workflow(
description='ScopeItem %s workflow' % self.title,
status=Workflow.PENDING)
workflow.save()
self.workflow = workflow
super(ScopeItem, self).save(*args, **kwargs)
And workflow/models.py:
from django.utils.timezone import now
class Workflow(models.Model):
PENDING = 0
APPROVED = 1
CANCELLED = 2
STATUS_CHOICES = (
(PENDING, 'Pending'),
(APPROVED, 'Done'),
(CANCELLED, 'Cancelled'),
)
description = models.CharField(max_length=4000)
status = models.IntegerField(choices=STATUS_CHOICES)
approval_date = models.DateTimeField('date approved', null=True)
creation_date = models.DateTimeField('date created')
update_date = models.DateTimeField('date updated')
def save(self, *args, **kwargs):
if not self.id:
self.creation_date = now()
self.update_date = now()
super(Workflow, self).save(*args, **kwargs)
In scopeitems/admin.py I have:
from django.contrib import admin
from .models import ScopeItem
from workflow.models import Workflow
class ScopeItemAdmin(admin.ModelAdmin):
list_display = ('title', 'description', 'status')
list_filter = ('workflow__status', )
search_fields = ['title', 'description']
def save_model(self, request, obj, form, change):
obj.save()
def status(self, obj):
return Workflow.STATUS_CHOICES[obj.workflow.status][1]
admin.site.register(ScopeItem, ScopeItemAdmin)
You could set the field blank=True on workflow.
You said you don't want to allow "empty workflow attributes in a ScopeItem." Setting blank=True is purely validation-related. Thus, on the backend workflow will still be NOT NULL. From the Django docs:
If a field has blank=True, form validation will allow entry of an empty value.
Referring to your example you should be able to use:
workflow = models.ForeignKey(Workflow, blank=True)
You need to exclude the field from the form used in the admin, so that it won't be validated.
class ScopeItemForm(forms.ModelForm):
class Meta:
exclude = ('workflow',)
model = ScopeItem
class ScopeItemAdmin(admin.ModelAdmin):
form = ScopeItemForm
...
admin.site.register(ScopeItem, ScopeItemAdmin)
#Daniel Roseman's answer is correct as long as you don't need to edit the workflow field in admin at any time. If you do need to edit it then you'll need to write a custom clean() method on the admin form.
forms.py
class ScopeItemAdminForm(forms.ModelForm):
class Meta:
model = ScopeItem
def clean(self):
cleaned_data = super(ScopeItemAdminForm, self).clean()
if 'pk' not in self.instance:
workflow = Workflow(
description='ScopeItem %s workflow' % self.title,
status=Workflow.PENDING)
workflow.save()
self.workflow = workflow
return cleaned_data
admin.py
class ScopeItemAdmin(admin.ModelAdmin):
form = ScopeItemAdminForm
...
admin.site.register(ScopeItem, ScopeItemAdmin)
Answering my own question:
As #pcoronel suggested, the workflow attribute in ScopeItem must have blank=True set to get out of the form in the first place.
Overwriting the form's clean method as suggested by #hellsgate was also needed to create and store the new Workflow.
To prevent code duplication I added a function to workflow/models.py:
def create_workflow(title="N/A"):
workflow = Workflow(
description='ScopeItem %s workflow' % title,
status=Workflow.PENDING)
workflow.save()
return workflow
This makes the ScopeItemAdminForm look like this:
class ScopeItemAdminForm(forms.ModelForm):
class Meta:
model = ScopeItem
def clean(self):
cleaned_data = super(ScopeItemAdminForm, self).clean()
cleaned_data['workflow'] = create_workflow(cleaned_data['title'])
return cleaned_data
Additionally I changed the save method in scopeitems/models.py to:
def save(self, *args, **kwargs):
if not self.id:
if not self.workflow:
self.workflow = create_workflow(self.title)
super(ScopeItem, self).save(*args, **kwargs)
Today i came up with a requirement where i need to implement field level permission so looking for the best possible way.
class ABC(models.Model):
field1 = .....
field2 = .....
field3 = .....
Create two groups(A and B) and assigned permission that both one can add/edit/delete
and the other can only add/edit. But now need some help in this :-
I want if the first group user logs in in the admin he should be able to see all the three fields but if second group user logs in they should only see field1.
I want this in django admin as i need to perform some manipulations after these.My django version is 1.3
Thanks in advance
In your admin.py
class ABCAdmin(admin.ModelAdmin):
fields = [.....] # here comes the fields open to all users
def change_view(self, request, object_id, extra_context=None): # override default admin change behaviour
if request.user in gruop2: # an example
self.fields.append('field2') # add field 2 to your `fields`
self.fields.append('field3') # add field 3 to your `fields`
You can use the docs to see what is available. Above is an example taken from one of my usages. You may also need to define change_view and add_view too.
Just in case someone else stumble about this, I had some issues with the given accepted answer. Every time the view got refreshed, it appended the fields over and over again. As well to the desired restricted view, where it shouldn't appear.
So, according to the docs, I made it working as follows:
Creating a custom ModelForm
class AbcStaffForm(ModelForm):
class Meta:
model = Abc
exclude = ["manager", "foo", "bar",]
Overwrite get_form() in AbcModelAdmin & refered the custom ModelForm
class AbcAdmin(admin.ModelAdmin):
# some other code
# ...
def get_form(self, request, obj=None, **kwargs):
if not request.user.is_superuser:
kwargs['form'] = AbcStaffForm # ModelForm
return super().get_form(request, obj, **kwargs)
You can also override readonly_fields in changeform_view.
Try this in admin.py
class ABCAdmin(admin.ModelAdmin):
def changeform_view(self, request, *args, **kwargs)
self.readonly_fields = list(self.readonly_fields)
if request.user in group: #or another condition
self.readonly_fields.append('field2')
return super(ABCAdmin, self).changeform_view(request, *args, **kwargs)
Overwrite get_fields() in ABCAdmin (group B cannot view "is_on" field):
class ABCAdmin(admin.ModelAdmin):
fields = ['name', 'title', 'price', 'is_on', 'create_time']
def get_fields(self, request, obj=None):
if request.user in groupB:
if 'is_on' not in self.fields:
self.fields.append('is_on')
else:
if 'is_on' in self.fields:
self.fields.remove('is_on')
return super(ABCAdmin, self).get_fields(request, obj)
I need a way to have an intermediate page shown when I´ve saved a model in django admin.
What I want to accomplish is after "saving" a model, show a page with all the attributes of the model lined up and then have a button that says Print. I used to solve this with Jquery dialog div when clicking save. That meant that I showed the settings print view before actually saving the model but I need the model to validate first now.
Its like the way that the "delete model" action is implemented. I just can´t seem to find out where to start looking though.
Edit:
I´ve started looking in the django.contrib.admin.options.py for the response_change and response_add methods. Not sure how to override them though. And its only needed for one specific model so its not generic. Also I´ve discovered the list of templates in the Class ModelAdmin. Still not sure about how to proceed without hacking the admin to bits.
Edit 2:
Added my working solution down below.
You could create a form that had an extra step of validation for the 'are you sure' step.
Given this model in our models.py:
from django.db import models
class Person(models.Model):
name = models.CharField(max_length=100)
Add a form in forms.py:
from django import forms
from .models import Person
class PersonForm(forms.ModelForm):
i_am_sure = forms.BooleanField(required=False, widget=forms.HiddenInput())
def __init__(self, *args, **kwargs):
super(PersonForm, self).__init__(*args, **kwargs)
if self.errors.get('i_am_sure'):
# show the 'are you sure' checkbox when we want confirmation
self.fields['i_am_sure'].widget = forms.CheckboxInput()
def clean(self):
cleaned_data = super(PersonForm, self).clean()
if not self.errors:
# only validate i_am_sure once all other validation has passed
i_am_sure = cleaned_data.get('i_am_sure')
if self.instance.id and not i_am_sure:
self._errors['i_am_sure'] = self.error_class(["Are you sure you want to change this person?"])
del cleaned_data['i_am_sure']
return cleaned_data
class Meta:
model = Person
If you want to use this with Django admin. specify this form in your admin.py:
from django.contrib import admin
from .forms import PersonForm
from .models import Person
class PersonAdmin(admin.ModelAdmin):
form = PersonForm
admin.site.register(Person, PersonAdmin)
Note however that there's a bug with hidden inputs on Django admin forms. There's a solution to that on this Stack Overflow question.
You can add views and urls to your ModelAdmin and overwrite your modeladmin add view to redirect accordingly.
class MyModelAdmin(admin.ModelAdmin):
def get_urls(self):
urls = super(MyModelAdmin, self).get_urls()
my_urls = patterns('',
(r'^my_view/$', self.my_view)
)
return my_urls + urls
def my_view(self, request):
# custom view which should return an HttpResponse
pass
So, after a bit of coding I got it to work.
My modeladmin looks simular to this now
class MyModelAdmin(admin.ModelAdmin):
def get_urls(self):
urls = super(MyModelAdmin, self).get_urls()
my_urls = patterns('',
(r'^my_view/$', self.my_view)
)
return my_urls + urls
def my_view(self,request,pk):
from django.shortcuts import render_to_response
from django.template import RequestContext
object = Model.objects.get(pk=pk)
model_dict = model_object.__dict__
return render_to_response('admin/app_name/model/model_view.html',locals(),context_instance=RequestContext(request))
#csrf_protect_m
#transaction.commit_on_success
def add_view(self, request, form_url='', extra_context=None):
"The 'add' admin view for this model."
model = self.model
opts = model._meta
if not self.has_add_permission(request):
raise PermissionDenied
ModelForm = self.get_form(request)
formsets = []
inline_instances = self.get_inline_instances(request)
if request.method == 'POST':
form = ModelForm(request.POST, request.FILES)
if form.is_valid():
new_object = self.save_form(request, form, change=False)
form_validated = True
else:
form_validated = False
new_object = self.model()
prefixes = {}
for FormSet, inline in zip(self.get_formsets(request), inline_instances):
prefix = FormSet.get_default_prefix()
prefixes[prefix] = prefixes.get(prefix, 0) + 1
if prefixes[prefix] != 1 or not prefix:
prefix = "%s-%s" % (prefix, prefixes[prefix])
formset = FormSet(data=request.POST, files=request.FILES,
instance=new_object,
save_as_new="_saveasnew" in request.POST,
prefix=prefix, queryset=inline.queryset(request))
formsets.append(formset)
if all_valid(formsets) and form_validated:
self.save_model(request, new_object, form, False)
self.save_related(request, form, formsets, False)
self.log_addition(request, new_object)
log.info('The new object has %s id' % new_object.id)
return HttpResponseRedirect('/admin/draws/contest/contest_view/%s' % new_object.id) <-- changed to my new one
.................
.................
Created a html template in templates/admin/app_name/model_view.html and that is it!