1
\$\begingroup\$

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

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.