Adding + button to ManytoMany widget on manual form

I want to add the function to add item in my form choices fields

how can I get the related Model Class of the field? My ideia is to get the model name to acces the ModelForm and pass it on Modal.

The plus icon shoud open a modal that return a form of the related item. By now it’s rendering a boilerplate form

I want the same funcionality of the admin panel ( the + icon )

form.py


class CheckListForm(forms.ModelForm):
    class Meta:
        model = CheckList
        fields = "__all__"

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # Customize the widget for the fields
        self.fields['atividade'].widget = forms.SelectMultiple()
        self.fields['aplicacao_check_list'].widget = forms.SelectMultiple()
        self.fields['descricao_check_list'].widget = forms.SelectMultiple()


        # Populate choices for atividade, aplicacao_check_list, and descricao_check_list
        self.fields['atividade'].queryset = Atividade.objects.all()
        self.fields['aplicacao_check_list'].queryset = Aplicacao.objects.all()
        self.fields['descricao_check_list'].queryset = DescricaoAplicacao.objects.all()

model.py

class CheckList(CommonInfo):
    
    projeto = models.OneToOneField(Projeto, on_delete=models.PROTECT)
    atividade = models.ManyToManyField(to='operation.Atividade')
    aplicacao_check_list = models.ManyToManyField(Aplicacao, verbose_name='aplicação check-list')
    descricao_check_list = models.ManyToManyField(DescricaoAplicacao, verbose_name='descrição check-list')
    observacao = models.CharField(max_length=50, blank=True, default=None, verbose_name='observação')

    class Meta:
        verbose_name = "Check-list"
        verbose_name_plural = "Check-lists"


    def __str__(self):
        return self.codigo

form template

	{% comment %} 


	{% endcomment %}
	{% load widget_tweaks %}
	{% load portal_utils %}


	{% for field in form %}
		{{field|test}}
		<div class="fieldWrapper mb-3 justify-content-center ">
			<label for="{{ field.id_for_label }}" class="form-label">{{ field.label }}</label>
			
			{% if field.field.required %}
				<span class="required">*</span>
			{% endif %}	
			{% if 'Select' in field|all %}
			<button type="button" class="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#modal_{{field.name}}">
				<i class="bi bi-plus"></i>
			</button>
			{% include "modal_create.html" with target="modal_"|add:field.name modal_title=field.label modal_content=form %}
			{% endif %}

			{% if forloop.first %}
				{{ field|add_class:'form-control'|attr:'autofocus' }}
			{% else %}
				{{ field|add_class:'form-control'}}
			{% endif %}

			{% if field.help_text %}
					<div id="{{ field.auto_id }}_helptext" class="form-text">
					<p class="help">{{ field.help_text|safe }}</p>
					</div>       
			{% endif %}

			{% for error in field.errors %}
					<div class="alert alert-warning" role="alert">
					{{ error }}
					</div>
			{% endfor %}
		</div>
	{% endfor %}

url.py

def generate_urlpatterns(models_views):
    urlpatterns = []
    for model, view in models_views:
        urlpatterns += [
            path(f'{slugify(view.categoria.lower())}/{slugify(model._meta.verbose_name)}/', getattr(view(), 'list'), name=f'{model.__name__.lower()}_list'),
            path(f'{slugify(view.categoria.lower())}/{slugify(model._meta.verbose_name)}/new/', getattr(view(), 'create'), name=f'{model.__name__.lower()}_create'),
            path(f'{slugify(view.categoria.lower())}/{slugify(model._meta.verbose_name)}/<pk>/', getattr(view(), 'detail'), name=f'{model.__name__.lower()}_detail'),
            path(f'{slugify(view.categoria.lower())}/{slugify(model._meta.verbose_name)}/<pk>/edit/', getattr(view(), 'update'), name=f'{model.__name__.lower()}_update'),
            path(f'{slugify(view.categoria.lower())}/{slugify(model._meta.verbose_name)}/<pk>/delete/', getattr(view(), 'delete'), name=f'{model.__name__.lower()}_delete'),
        ]
    return urlpatterns

models_views = [
    (CheckList, CheckListView),
    (Aplicacao, AplicacaoView),
    (DescricaoAplicacao, DescricaoAplicacaoView),
    (Projeto, ProjetoView),
    (AmarracaoDasNormas, AmarracaoDasNormasView),
    (CadastroDeNormas, CadastroDeNormasView),
    (CadastroRequisitosDaNorma, CadastroRequisitosDaNormaView),
    (CadastroDeSecaoDaNorma, CadastroDeSecaoDaNormaView),    
    (Setor, SetorView),
    (Atividade, AtividadeView),
    (Etapa, EtapaView), 
    (Secao, SecaoView), 
    (Subgrupo, SubgrupoView), 
    (Subsecao, SubsecaoView), 
    (EtapaCronograma, EtapaCronogramaView), 
    (ExcecaoCalendario, ExcecaoCalendarioView), 
    (NaoConformidade, NaoConformidadeView), 
    (Produto, ProdutoView), 
    (FichaDados, FichaDadosView),   
]

views.py

class GenericModelView:
	model = None
	form_class = None
	list_template_name = 'crud/item_list.html' 
	detail_template_name = 'crud/item_detail.html'
	create_template_name = 'crud/item_create.html'
	update_template_name = 'crud/item_update.html'
	url_redirect = None
	
	def verbose_name(self):
		return self.model._meta.verbose_name
	
	def url_create(self):
		return self.model.__name__.lower() + '_create'
	
	def url_list(self):
		return self.model.__name__.lower() + '_list'
	
	def url_detail(self):
		return self.model.__name__.lower() + '_detail'

	def url_update(self):
		return self.model.__name__.lower() + '_update'
	
	def url_delete(self):
		return self.model.__name__.lower() + '_delete'

	def list(self, request):
		
		context['title'] = self.verbose_name()
		context['create_url'] = self.url_create()
		context['add_url'] = self.url_create()
		context['item_url'] = self.url_detail()
		context['data'] = self.model.objects.values()
		
		return render(request, self.list_template_name, context)

	def detail(self, request, pk):
		item = get_object_or_404(self.model, pk=pk)
		item_dict = model_to_dict(item)
		
		context["item"] = item
		context["item_dict"] = item_dict
		context['back_url'] = self.url_list()
		context['update_url'] = self.url_update()
		context['delete_url'] = self.url_delete()
		
				
		return render(request, self.detail_template_name, context)

	def create(self, request):
		
		context['title'] = self.verbose_name()
		context['back_url'] = self.url_list()
		context['action_url'] = self.url_create()   
		
		if request.method == 'POST':
			form = self.form_class(request.POST)
			
			if form.is_valid():
				form.save()
				return redirect(self.url_redirect)
		else:
			form = self.form_class()
			
		context['form'] = form
			
		return render(request, self.create_template_name, context)

	def update(self, request, pk):
		_context = {}
		item = get_object_or_404(self.model, pk=pk)
		_context['back_url'] = self.url_list()
		_context['title'] = self.verbose_name()
		
		if request.method == 'POST':
			form = self.form_class(request.POST, instance=item)
			if form.is_valid():
				form.save()
				return redirect(self.url_redirect)
		else:
			form = self.form_class(instance=item)
			
		_context['form'] = form
		return render(request, self.update_template_name, _context)

	def delete(self, request, pk):
		if request.user.is_authenticated:
			delete_it = get_object_or_404(self.model, pk=pk)
			delete_it.delete()
			messages.success(request, "Aplicação deletada com sucesso.")
			return redirect(self.url_redirect)
		else:
			messages.success(request, "Você tem que deve ter permissão para fazer isso.")
			return redirect(self.url_redirect)

#######################
##### Check-List
#######################

class AplicacaoView(GenericModelView):
	model = Aplicacao
	form_class = AplicacaoForm
	url_redirect = f'{model.__name__.lower()}_list'
	categoria = 'Check-list'
	css_id = f'{slugify(categoria)}/{slugify(model._meta.verbose_name)}/'
	name = model.__name__.lower()
	
	def __init__(self):
		GenericModelView.__init__(self)
		
class CheckListView(GenericModelView):
	model = CheckList
	form_class = CheckListForm
	url_redirect = f'{model.__name__.lower()}_list'
	categoria = 'Check-list'
	css_id = f'{slugify(categoria)}/{slugify(model._meta.verbose_name)}/'
	
	def __init__(self):
		GenericModelView.__init__(self)

If you read the source behind the actions of the “+” button in the admin, you’ll discover that it is a link that opens a new browser window that is accessing the “Add” view of that related model.

This means that in your view that is creating this base page, you would be assigning the URL for that link.

The admin does not open a modal on the same page.

If that is the functionality you want, it’ll be up to you to write the JavaScript that will call a view to retrieve the HTML for the modal, and have the modal post back to that view. It’ll also be your responsibility to cause the base portion of the page to be updated with that data.

1 Like