I am building a web app in Django where one of the form fields is a multiplechoicefield from a many2many object that I use to select users that are allowed to edit the post. In standard form the field displays predominantly while it is only seldom required, hence I would like the field only to show under a dropdown button. As I spent considerable time to figure this out I thought it might be useful to share this little trick and to welcome any suggestions on alternatives.
The first hurdle to take was how to access form fields individually, so to be more flexible where to put them. For this I use Django Crispy Forms.
You can install it by:
pip install django-crispy-forms
and alter a few things in settings.py
INSTALLED_APPS = [
...
'crispy_forms',
]
CRISPY_TEMPLATE_PACK = 'bootstrap4'
In my case I am using bootstrap4
, but probably there are also other CRISPY_TEMPLATE_PACKs.
The relevant model Post
in models.py looks as follows:
from martor.models import MartorField
class Post(models.Model):
post_subject = models.CharField(max_length=POST_SUBJECT_SIZE, null=True)
message = MartorField(max_length=MESSAGE_FIELD_SIZE,
help_text=f'Maximum length is {MESSAGE_FIELD_SIZE} characters',
)
topic = models.ForeignKey(Topic, on_delete=models.CASCADE,
related_name='posts')
created_by = models.ForeignKey(User, on_delete=models.CASCADE,
related_name='posts')
created_at = models.DateTimeField(auto_now_add=True)
updated_by = models.ForeignKey(User, on_delete=models.CASCADE,
related_name='+', null=True)
updated_at = models.DateTimeField(null=True)
allowed_editor = models.ManyToManyField(User, blank=True)
def __str__(self):
return Truncator(self.post_subject).chars(30)
Note martorfield is a nice md editor for the message field.
The fields are defined in fields.py, where I have also changed the standard MultipleChoiceField
to a CheckboxSelectMultiple
widget
class PostForm(forms.ModelForm):
allowed_editor = forms.ModelMultipleChoiceField(widget=forms.CheckboxSelectMultiple(),
required=False,
label='',
help_text='Selection is not required',
queryset=User.objects.all().order_by('username'))
class Meta:
model = Post
fields = ['allowed_editor', 'post_subject', 'message']
labels = {'post_subject': 'Post subject'}
and as an example one of the views in views.py:
@method_decorator(login_required, name='dispatch')
class PostUpdateView(UpdateView):
model = Post
form_class = PostForm
template_name = 'boards/edit_post.html'
pk_url_kwarg = 'post_pk'
context_object_name = 'post'
def get_queryset(self):
self.topic = get_object_or_404(Topic,
board__pk=self.kwargs.get('board_pk'),
pk=self.kwargs.get('topic_pk'))
queryset = self.topic.posts.order_by('-updated_at')
def form_valid(self, form):
# note if commit=False, then post.save() must be followed by form.save_m2m()
post = form.save(commit=False)
post.updated_by = self.request.user
post.updated_at = timezone.now()
post.save()
form.save_m2m()
self.topic.last_updated = post.updated_at
self.topic.save()
topic_url = reverse('topic_posts',
kwargs={'board_pk': self.topic.board.pk,
'topic_pk': self.topic.pk},)
topic_post_url = f'{topic_url}?page={self.topic.get_page_number(post.pk)}'
return redirect(topic_post_url)
Finally then the trick is in the template, where the individual form fields are placed where needed using the as_crispy_field
tags. The allowed_editor
field is put under a dropdown button using the below snippet.
<button class="btn dropdown-toggle btn-primary btn-sm" data-toggle="dropdown"
>Allowed editors<span class="caret"></span></button>
<ul class="dropdown-menu">
{{ form.allowed_editor|as_crispy_field }}
</ul>
and the full template edit_post.html
{% extends 'base.html' %}
{% load static %}
{% load crispy_forms_tags %}
{% block title %}Edit post{% endblock %}
{% block breadcrumb %}
<ol class="breadcrumb my-4">
<li class="breadcrumb-item"><a href="{% url 'home' %}">Home</a></li>
<li class="breadcrumb-item"><a href="{% url 'boards' %}">Boards</a></li>
<li class="breadcrumb-item"><a href="{% url 'board_topics' post.topic.board.pk %}">{{ post.topic.board.name }}</a></li>
<li class="breadcrumb-item"><a href="{% url 'topic_posts' post.topic.board.pk post.topic.pk %}">{{ post.topic.topic_subject }}</a></li>
<li class="breadcrumb-item active">Edit contribution</li>
</ol>
{% endblock %}
{% block content %}
<form method="post" class="mb-4" novalidate>
{% csrf_token %}
<button type="submit" class="btn btn-primary btn-sm">Save changes</button>
<button style="margin:1px" class="btn btn-primary btn-sm"
name="deleted_post_pk" value="{{ post.pk }}"
onclick="return Validate()">Delete contribtion</button>
<button class="btn dropdown-toggle btn-primary btn-sm" data-toggle="dropdown"
>Allowed editors<span class="caret"></span></button>
<ul class="dropdown-menu">
{{ form.allowed_editor|as_crispy_field }}
</ul>
<a href="{% url 'topic_posts' post.topic.board.pk post.topic.pk %}"
class="btn btn-outline-secondary btn-sm"
role="button">Cancel</a>
<br></br>
{{ form.post_subject|as_crispy_field }}
{{ form.message|as_crispy_field }}
</form>
<br>
<br>
{% endblock %}
{% block javascript %}
<script src="{% static 'js/board.js' %}"></script>
{% endblock %}
If you are interested in all of the source code you can find these at github