the situation
In my example I want to create a Page model with a many to many relationship with a content-blocks model.
A page has a title, slug, and main content block.
content blocks have a title and a content block.
What I can get:
Showing page.blocks in the admin form displays a multi select of content blocks
Creating an inline form for the content blocks on the page admin shows several selects with a + sign to add more
What I am trying to accomplish:
Full CRUD on content block on the page admin
Note: Due to the difficulty of my request, I'm beginning to believe the UX pattern im trying to accomplish is wrong. If I want a content creator to come in and create a page, pick some existing content blocks (ex: an existing sidebar content block), and then create a new custom block. I don't think i want him to have to jump all over the place to do this...
Related Question without solutions:
How do I use a TabularInline with editable fields on a ManyToMany relationship?
EDIT
my admin.py
from django.contrib import admin
from django.contrib.flatpages.admin import FlatpageForm, FlatPageAdmin
from django.contrib.flatpages.models import FlatPage
from my_flatpages.models import ExtendedFlatPage, ContentBlock
from mptt.admin import MPTTModelAdmin
from django import forms
import settings
"""
Extended Flatpage Form
"""
class ExtendedFlatPageForm(FlatpageForm):
class Meta:
model = ExtendedFlatPage
"""
Page Content Block inline form
"""
class ContentBlockInlineAdminForm(forms.ModelForm):
# Add form field for selecting an existing content block
content_block_choices = [('', 'New...')]
content_block_choices.extend([(c.id, c) for c in ContentBlock.objects.all()])
content_blocks = forms.ChoiceField(choices=content_block_choices, label='Content Block')
def __init(self, *args, **kwargs):
super(ContentBlockInlineAdminForm, self).__init__(*args, **kwargs)
# Show as existing content block if it already exists
if self.instance.pk:
self.fields['content_block'].initial = self.instance.pk
self.fields['title'].initial = ''
self.fields['content'].initial = ''
# Make title and content not required so user can opt to select existing content block
self.fields['title'].required = False
self.fields['content'].required = False
def clean(self):
content_block = self.cleaned_data.get('content_block')
title = self.cleaned_data.get('title')
content = self.cleaned_data.get('content')
# Validate that either user has selected existing content block or entered info for new content block
if not content_block and not title and not content:
raise forms.ValidationError('You must either select an existing content block or enter the title and content for a new content block')
"""
Content Block Inline Admin
"""
class ContentBlockInlineAdmin(admin.TabularInline):
form = ContentBlockInlineAdminForm
class Meta:
model = ContentBlock
extra = 1
"""
Extended Flatpage Admin
"""
class ExtendedFlatPageAdmin(FlatPageAdmin, MPTTModelAdmin):
form = ExtendedFlatPageForm
fieldsets = (
(
None,
{
'fields': ('url', 'title', 'content', ('parent', 'sites'))
}
),
(
'SEO Fields',
{
'fields': ('seo_title', 'seo_keywords', 'seo_description'),
'classes': ('collapse', )
}
),
(
'Advanced options',
{
'fields': ('enable_comments', 'registration_required', 'template_name'),
'classes': ('collapse', )
}
),
)
inlines = (ContentBlockInlineAdmin,)
class Media:
js = (
'https://ajax.googleapis.com/ajax/libs/jquery/1.5.2/jquery.min.js',
settings.MEDIA_URL + 'js/tinymce/jquery.tinymce.js',
settings.MEDIA_URL + 'js/init_tinymce.js'
)
admin.site.unregister(FlatPage)
admin.site.register(ExtendedFlatPage, ExtendedFlatPageAdmin)
Haven't had the opportunity to test this, but it should work:
class ContentBlockInlineAdminForm(forms.ModelForm):
# Add form field for selecting an existing content block
content_block_choices = [('', 'New...')]
content_block_choices.extend([(c.id, c) for c in ContentBlock.objects.all()])
content_blocks = forms.ChoiceField(choices=content_block_choices, label='Content Block')
def __init(self, *args, **kwargs):
super(ContentBlockInlineAdminForm, self).__init__(*args, **kwargs)
# Show as existing content block if it already exists
if self.instance.pk:
self.fields['content_block'].initial = self.instance.pk
self.fields['title'].initial = ''
self.fields['content'].initial = ''
# Make title and content not required so user can opt to select existing content block
self.fields['title'].required = False
self.fields['content'].required = False
def clean(self):
content_block = self.cleaned_data.get('content_block')
title = self.cleaned_data.get('title')
content = self.cleaned_data.get('content')
# Validate that either user has selected existing content block or entered info for new content block
if not content_block and not title and not content:
raise forms.ValidationError('You must either select an existing content block or enter the title and content for a new content block')
class ContentBlockInlineAdmin(admin.TabularInline):
form = ContentBlockInlineAdminForm
class Meta:
model = ContentBlock
extra = 1
class PageAdmin(admin.ModelAdmin):
inlines = [
ContentBlockInlineAdmin,
]
"""
Override saving of formset so that if a form has an existing content block selected, it
sets the form instance to have the pk of that existing object (resulting in update rather
than create). Also need to set all the fields on ContentType so the update doesn't change
the existing obj.
"""
def save_formset(self, request, form, formset, change):
for form in formset:
if form.cleaned_data.get('content_block'):
content_block = ContentBlock.objects.get(pk=form.cleaned_data.get('content_block'))
instance = form.save(commit=False)
instance.pk = content_block.pk
instance.title = content_block.title
instance.content = content_block.content
instance.save()
else:
form.save()
You could then actually add some javascript to show/hide the ContentBlock fields depending on whether the content_block field is set to 'New..' or an existing one.
This isn't the answer I was looking for, BUT, What I ended up going with is
class Page(models.Model):
....
class ContentBlock(models.Model):
page = models.ForeignKey(
Page,
blank = True,
null = True,
)
....
and then having a regular tabular inline for ContentBlock on the page admin form.
So that way I can have page specific content blocks related to a page, AND be able to have generic content blocks able to be used wherever.
Then, I created an inclusion tag to render a content block by name that I use in my templates.
The project https://github.com/caktus/django-pagelets sounds like exactly what you are looking for. A page can have 'pagelets' and 'shared pagelets' with a nice admin for the two (pagelets are simply content blocks).
The non-shared pagelets are shown as inlines with the ability to add extra blocks directly on the page admin screen. For shared pagelets you get the drop-down with a plus-sign.
Related
I am trying to get a file upload form field working in Django and the part I am having problems with is dynamically changing the form field required attribute. I have tried using "self.fields['field_name'].required=True' in the init method of the form but that isn't working for me.
I have looked at Django dynamically changing the required property on forms but I don't want to build several custom models and a custom render function for one form as surely it must be easier than that.
The reason I am trying to do this is because when a django form validates and has errors it doesn't pass any uploaded files back to the browser form for reediting. It will pass text areas and text inputs that didn't validate back to the form for reediting but not file uploads. I thought if I made the file upload fields mandatory for the first time the record is created mandatory and for subsequent times make them optional. That is basically what I am trying to do.
So here is what I have been trying so far:
In forms.py
from django.forms import fields
from .widgets import PDFUploadWidget, PlainTextWidget
class WQPDFField(fields.Field):
widget = PDFUploadWidget
def widget_attrs(self, widget):
attrs = super().widget_attrs(widget)
attrs['label'] = self.label
return attrs
def clean(self, *args, **kwargs):
return super().clean(*args, **kwargs)
In widgets.py
from django.forms import widgets
from django.utils.safestring import mark_safe
# We subclass from HiddenInput because we want to suppress the printing
# of the label and prefer to print it ourselves.
class PDFUploadWidget(widgets.HiddenInput):
template_name = 'webquest_widgets/widgets/pdf_upload.html'
input_type = 'file'
def __init__(self, *args, **kwargs):
style = 'visibility:hidden'
attrs = kwargs.pop('attrs', None)
if attrs:
attrs['style'] = style
else:
attrs = {'style':style}
attrs['accept'] = '.pdf'
print (attrs)
super().__init__(attrs)
def get_context(self, name, value, attrs):
context = super().get_context(name, value, attrs)
return context
#property
def is_hidden(self):
return True
class Media:
css = { 'all': ( 'css/pdfupload.css', ) }
js = ('js/pdfupload.js', )
and finally in forms.py
class WorkWantedForm(forms.Form):
category = forms.ChoiceField(choices=CHOICES)
about = forms.CharField(label="About Yourself", widget=forms.Textarea())
static1 = WQStaticField(text="Enter either phone or email")
phone = forms.CharField(required=False)
email = forms.EmailField(required=False)
cv = WQPDFField(label="Upload CV")
supporting_document = WQPDFField(label="Supporting Document (optional)", required=False)
I am not sure how to pass the "required" attribute to the custom field class after the initialisation of the form but before rendering the form as HTML.
I have a model called Coverletter with 4 fields: company, role, job_posting, content.
I am trying to use the data from the first 3 fields to populate data in the 4th field.
Right now, the user inputs values for 3 fields in a form: (company, role, job_posting) using a class based view CreateView.
class CoverletterCreateView(LoginRequiredMixin, CreateView):
model = Coverletter
fields = ['company', 'role', 'job_posting']
template_name = 'coverletter/coverletter_create_form.html'
def form_valid(self, form):
form.instance.owner = self.request.user
return super().form_valid(form)
def get_success_url(self):
return reverse('coverletter:coverletter-create-content',
kwargs={'pk': self.object.pk,}
)
The user is then redirected to another class based view UpdateView via get_success_url, where the first three fields are showing with populated values.
class CoverletterCreateContentView(LoginRequiredMixin, UpdateView):
model = Coverletter
fields = ['company', 'role', 'job_posting', 'content']
template_name = 'coverletter/coverletter_create_content_form.html'
Right now the 4th field (content) is blank, but I am trying to populate the content field when the user submits the data from the first CreateView.
The data is an html string that would look something like this:
<p>Dear {{ company }} I am excited to be applying for the {{ role }} role.</p>
I am very new to Django and web development, and am struggling to understand how to do this. I am unsure where in the app to put this logic. Any guidance is appreciated!
First off, get intimate with ccbv.
Second, you're using a UpdateView, which - as you can determine above - implements, among others:
get_initial()
get_object()
Inside, get_initial(), you can get the model instance you're trying to update using get_object(). Once you have the instance, you can compose your string. Then add it to the initial data.
Result:
class CoverletterCreateContentView(LoginRequiredMixin, UpdateView):
... # fields
def get_initial(self):
if not hasattr(self, 'object'):
self.object = self.get_object(self.get_queryset())
initial = super().get_initial()
opening_sentence = (
f"Dear {self.object.company} I am excited to"
f" be applying for the {self.object.role} role."
)
initial['content'] = opening_sentence # initial[field_name] (dict)
return initial
I have an extended admin model that creates action buttons. I have created a view to do pretty much the same thing. I have used tables2 and everything is just fine except for the actions column. I cannot find a way to generate the same button in the table. Is there a way to do this at all?
tables.py
from .models import Ticket
import django_tables2 as tables
'''from .admin import river_actions, create_river_button'''
class TicketTable(tables.Table):
class Meta:
model=Ticket
template_name='django_tables2/table.html'
fields = ('id','subject','request_type','material_type','productline','business','measurement_system',
'created_at','updated_at','status','river_action','project') # fields to display
attrs = {'class': 'mytable'}
'''attrs = {"class": "table-striped table-bordered"}'''
empty_text = "There are no tickets matching the search criteria..."
admin.py (the part that includes the model etc)
# Define a new User admin to get client info too while defining users
class UserAdmin(BaseUserAdmin):
inlines = (UserExtendInline, )
def create_river_button(obj,proceeding):
return '''
<input
type="button"
style=margin:2px;2px;2px;2px;"
value="%s"
onclick="location.href=\'%s\'"
/>
'''%(proceeding.meta.transition,
reverse('proceed_ticket',kwargs={'ticket_id':obj.pk, 'next_state_id':proceeding.meta.transition.destination_state.pk})
)
class TicketAdmin(admin.ModelAdmin):
list_display=('id','client','subject','request_type','material_type','productline','business','measurement_system', \
'created_at','updated_at','created_by','status','river_actions')
#list_display_links=None if has_model_permissions(request.user,Ticket,['view_ticket'],'mmrapp')==True else list_display
#list_display_links=list_display #use None to remove all links, or use a list to make some fields clickable
#search_fields = ('subject','material_type')
list_filter=[item for item in list_display if item!='river_actions'] #exclude river_actions since it is not related to a field and cannot be filtered
#Using fieldset, we can control which fields should be filled by the user in the ADD method. This way, created_by will be the
#logged in user and not a drop down choice on the admin site
fieldsets = [
(None, {
'fields': ('client','subject','description','request_type','material_type', \
'productline','business','measurement_system', 'project')
} ), #to make some field appear horizontal, put them into a []
]
formfield_overrides = {
models.CharField: {'widget': TextInput (attrs={'size':'40'})},
models.TextField: {'widget': Textarea(attrs={'rows':4, 'cols':80})},}
def get_list_display(self, request):
self.user=request.user
return super(TicketAdmin,self).get_list_display(request)
def river_actions(self,obj):
content=""
for proceeding in obj.get_available_proceedings(self.user):
content+=create_river_button(obj, proceeding)
return content
river_actions.allow_tags=True
#override save_model method to save current user since it is not on the admin page form anymore
def save_model(self, request, obj, form, change):
if not change:
# the object is being created and not changed, so set the user
obj.created_by = request.user
obj.save()
views.py
def display_tickets(request):
table = TicketTable(Ticket.objects.all())
RequestConfig(request).configure(table)
'''table = CustomerTable(Customer.objects.filter(self.kwargs['company']).order_by('-pk'))'''
return render(request,'mmrapp/ticket_display.html',{'table':table})
buttons created in admin page:
table created using tables2 in views missing buttons:
You must pass empty_values=() to the column, because by default, django-tables2 only renders the column if the value is not contained in empty_values for that column.
import django_tables2 as tables
from .admin import river_actions, create_river_button
from .models import Ticket
class TicketTable(tables.Table):
river_action = tables.Column(empty_values=())
class Meta:
model=Ticket
template_name='django_tables2/table.html'
fields = (
'id', 'subject', 'request_type', 'material_type', 'productline', 'business', 'measurement_system',
'created_at', 'updated_at', 'status', 'river_action', 'project'
) # fields to display
attrs = {'class': 'mytable'}
empty_text = "There are no tickets matching the search criteria..."
def render_river_action(self, record):
return create_river_button(record, ...)
This is also documented as Table.render_foo methods
I am new to Django and have been doing lots of reading so perhaps this is a noob question.
We have applications that involve many forms that users fill out along the way. One user might fill out the budget page and another user might fill out the project description page. Along the way any data they input will be SAVED but NOT validated.
On the review page only data is shown and no input boxes / forms. At the bottom is a submit button. When the user submits the application I then want validation to be performed on all the parts / pages / forms of the application. If there are validation errors then the application can not be submitted.
My model fields are mostly marked as blank=True or null=True depending on the field type. Some fields are required but most I leave blank or null to allow the users to input data along the way.
Any advice on best practices or do not repeat yourself is greatly appreciated.
There is an app in django called form wizard. Using it you can split form submission process for multiple steps.
After a lot of learning, playing and reading I think I have figured a few things and will share them here. I do not know if this is right, however it is progress for me.
So first comes the models. Everything needs to accept blank or null depending on the field type. This will allow the end user to input data as they get it:
class exampleModel(models.Model):
field_1 = models.CharField(blank=True, max_length=25)
field_2 = models.CharField(blank=True, max_length=50)
.........
Then we create our model form:
from your.models import exampleModel
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Layout, Row, Column
class exampleForm(ModelForm):
class Meta:
model = exampleModel
fields = ('field_1','field_2')
def __init__(self, *args, **kwargs):
# DID WE GET A VALIDATE ARGUMENT?
self.validate = kwargs.pop('validate', False)
super(ExampleForm, self).__init__(*args, **kwargs)
# SEE IF WE HAVE TO VALIDATE
for field in self.fields:
if self.validate:
self.fields[field].required = True
else:
self.fields[field].required = False
self.helper = FormHelper()
self.helper.form_tag = False
self.helper.layout = Layout(
Row(
Column('field_1', css_class='col-lg-4 col-md-4'),
Column('field_2', css_class='col-lg-4 col-md-4')
)
)
def clean(self):
cleaned_data = super(ExampleForm, self).clean()
field_1 = cleaned_data.get('field1')
field_2 = cleaned_data.get('field2')
if self.validate and field_2 != field_2:
self.add_error('field_1', 'Field 1 does not match field2')
return cleaned_data
Here is the important part. I've learned a lot about forms and binding. As I mentioned I needed users to be able to fill out forms and not validate the data till the very end. This is my solution which helped me. I could not find a way to bind a form to the model data, so I created a function in my lib called bind_queryset_to_form which looks like this:
def bind_queryset_to_form(qs, form):
form_data = {}
my_form = form()
for field in my_form.fields:
form_data[field] = getattr(qs, field, None)
my_form = form(data=form_data, validate=True)
return my_form
The view:
from your.models import exampleModel
from your.form import exampleForm
from your.lib.bind_queryset_to_form import bind_queryset_to_form
from django.shortcuts import render, get_object_or_404
def your_view(request, pk):
query_set = get_object_or_404(exampleModel, id=pk)
context = dict()
context['query_set'] = query_set
# SAVE THE FORM (POST)
if request.method == 'POST':
form = exampleForm(request.POST, instance=query_set)
form.save()
context['form'] = form
# GET THE DATA.
if request.method == 'GET':
if request.session.get('validate_data'):
# BIND AND VALIDATE
context['form'] = bind_queryset_to_form(query_set, exampleForm)
else:
# NO BIND, NO VALIDATE
context['form'] = exampleForm(instance=query_set)
return render(request, 'dir/your.html', context)
The template:
{% load crispy_forms_tags %}
<div id="div_some_tab">
<form id="form_some_tab" action="{% url 'xx:xx' query_set.id %}" method="post">
{% crispy form form.helper %}
</form>
</div>
What does all the above allow?
I have many views with many data inputs. The user can visit each view and add data as they have it. On the review page I set the flag / session "validate_data". This causes the app to start validating all the fields. Any errors will all be displayed on the review page. When the user goes to correct the errors for the given view the bind_queryset_to_form(query_set, exampleForm) is called binding the form with data from the queryset and highlighting any errors.
I cut out a lot of the exceptions and permission to keep this as transparent as possible (the goat would hate that). Hope this idea might help someone else or someone else might improve upon it.
I have a 'Farm' model and a corresponding ModelForm as follows:
class FarmForm(ModelForm):
class Meta:
model = Farm
fields = ['farm_name','address','farm_size', 'latitude', 'longitude']
I can save a new Farm object through my client app (it requires that I fill in all the fields mentioned in my ModelForm).
I want to have another view where in I can update an existing Farm where the user can perhaps insert/update only those fields he/she wants to change. I tried something like following by passing only one of the field values through Postman but it gives me Form_not_valid error:
#api_view(['POST'])
def updateFarm(request, farmId):
farm = Farm.objects.get(id=farmId)
form = FarmForm(instance=farm, data=request.POST)
if form.is_valid():
farm = form.save()
farm = Farm.objects.filter(id=farm.id)
serializer = FarmSerializer(farm, many=True)
return JSONResponse(serializer.data)
#return Response("Data saved")
else:
return Response("Form not valid, insert correct fields.")
How can I build my view that let's user update only those fields he thinks are relevant? My url: url(r'^farms/update/(?P<farmId>\d\d)/$', views.updateFarm),
You can generate a boolean hidden form field for every field in your model, that gets set when a field is modified. For example name input:
<input id="id_name" maxlength="100" name="name" type="text">
will be followed by a name__specified hidden input:
<input id="id_name__specified" name="name__specified" type="hidden">
You track changes to field name with some js (very easy with plain js or jquery) and update name__specified accordingly to true/false.
In order to do this automatically and be able to re-use it, you can abstract this in a base form class and keep your form simple:
class BaseForm(forms.ModelForm):
suffix = '__specified'
def __init__(self, **kwargs):
super(BaseForm, self).__init__(**kwargs)
fields = list(self.fields)
for f in fields:
# Set the field default value from the instance
self.fields[f].widget.attrs['default'] = getattr(self.instance, f)
# JS tracking field changes
js = """
document.getElementById("id_%s").value =
this.value != this.getAttribute("default");
""" % (f + self.suffix)
self.fields[f].widget.attrs['onchange'] = js
self.fields[f + self.suffix] = forms.BooleanField(
widget=forms.HiddenInput(),
required=False
)
def clean(self):
data = super(BaseForm, self).clean()
flags = [f for f in self.fields if self.suffix in f]
for x in flags:
specified = data.get(x, False)
if not specified:
field = x[:-len(self.suffix)]
# If not specified grab it's current value from the instance
data[field] = getattr(self.instance, field)
# If the form validation complains that it's missing
# clear the error since we are not changing it's value
if field in self.errors:
del self.errors[field]
return data
So your modified form:
class FarmForm(BaseForm):
class Meta:
model = Farm
fields = ['farm_name','address','farm_size', 'latitude', 'longitude']
Note, you should pass the instance when instantiating a form in your GET function or simply inherit your view from UpdateView so that will be handled automatically:
class MyView(UpdateView):
template_name = 'my_template.html'
form_class = FarmForm
queryset = Farm.objects.all()
Now you can do partial updates!