Skip to content

opencodigos/django-crispyform

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

6 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Django Formset + CrispyForm Tutorial Simples

Formset no Django é um conjunto de formulários podemos gerenciar de uma vez só (como uma lista de formulários), criar/editar vários objetos de um mesmo modelo de uma vez.

Crispy Forms é um pacote que deixa os formulários do Django mais bonitos e personalizáveis sem precisar escrever muito HTML.

Gerenciador de Manuais

Objetivo

Um sistema para cadastrar, revisar e organizar manuais técnicos, vinculados a códigos de Produtos específicos.

Funcionalidades

  1. Cadastro de Manuais:
    • Cadastrar manuais com descrição, idioma, e um identificador único.
    • Ativar ou desativar manuais conforme necessário.
  2. Gerenciamento de Revisões:
    • Associar revisões aos manuais existentes.
    • Upload de arquivos PDF relacionados à revisão.
  3. Cadastro de Produto:
    • Associar manuais a códigos de produto específicos.
  4. Listagem:
    • Visualizar as revisões associadas a cada manual.

Requirements

  • Django
  • pillow
  • crispy-bootstrap5
  • django-crispy-forms==1.14.0
Criar o Projeto Django, Configurações, Models, Admin, PARTE 01

Passo 1: Criar o Projeto Django

  1. Crie um ambiente virtual:

    python -m venv venv
    source venv/bin/activate  
    # (ou venv\Scripts\activate no Windows)
  2. Instale o Django:

    pip install django
  3. Crie o projeto:

    django-admin startproject gerenciador_manual .
  4. Crie o app principal:

    python manage.py startapp manuais
  5. Adicione o app manuais ao projeto: No arquivo settings.py, localize a lista INSTALLED_APPS e adicione:

    INSTALLED_APPS = [
        ...
        'manuais',
    ]

    STATICS

    STATIC_ROOT = BASE_DIR / 'static'
    STATIC_URL = '/static/' 
    
    # STATICFILES_DIRS = [ # talvez em Produção podesse usar assim.
    #     BASE_DIR / 'staticfiles',
    # ]
    
    MEDIA_ROOT = BASE_DIR / 'media'
    MEDIA_URL = '/media/'
    LANGUAGE_CODE = 'pt-br'
    
    TIME_ZONE = 'America/Sao_Paulo'
    
    USE_I18N = True
    
    USE_L10N = True
    
    USE_TZ = True
    TEMPLATE_DIR = BASE_DIR / 'templates' 

    Passo 2: Configurar os Models

    1. Models

      Lista de Manuais

      from django.db import models 
      
      class Manual(models.Model):
          manual_id = models.CharField(max_length=200, unique=True)
      		
          descricao = models.CharField(max_length=200, null=True, blank=True) 
          
          lang = models.CharField(max_length=100, choices=[
              ('br', 'pt-BR'),
              ('en', 'en-US'),
              ('es', 'es-ES')])
              
          is_active = models.BooleanField(default=False)
      
          class Meta:
              verbose_name = 'Manual'
              verbose_name_plural = 'Manuais'
              ordering = ['id']
      
          def __str__(self):
              return self.manual_id 

      Lista de Modelos (Produto)

      class Produto(models.Model):
          manual = models.ForeignKey(Manual, on_delete=models.PROTECT,
                                     related_name="manual_produto")
                                     
          codigo_modelo = models.CharField(max_length=50)
          
          descricao = models.CharField(max_length=200, unique=True)
      
          class Meta:
              verbose_name = 'Cadastro de Produto (modelo)'
              verbose_name_plural = 'Cadastro de Produtos (modelos)'
              ordering = ['id']
      
          def __str__(self):
              return "{} ({})".format(self.manual, self.descricao)

      Lista de Revião

      class Revisao(models.Model):
          manual = models.ForeignKey(Manual,
                                        on_delete=models.PROTECT,
                                        related_name="manual_revisao")
      
          revisao = models.IntegerField()
          
          pdf = models.FileField(upload_to='manuais/pdf', null=True, blank=True)
          
          class Meta:
              verbose_name = 'Revisão Manual'
              verbose_name_plural = 'Revisões Manuais'
              ordering = ['-id']
      
          def __str__(self):
              return "{} (REV: {})".format(str(self.manual), self.revisao)
    2. Faça as migrações:

      python manage.py makemigrations
      python manage.py migrate

    Passo 3: Configurar o Django Admin

    1. No arquivo manuais/admin.py, registre os Produtos:

      from django.contrib import admin
      from manuais.models import *
       
      class ProdutoInline(admin.StackedInline): 
          model = Produto
          extra = 0
          min_num = 0  
      
      class RevisaoInline(admin.StackedInline): 
          model = Revisao
          extra = 0
          min_num = 0  
      
      class ManualAdmin(admin.ModelAdmin): 
          list_display = ('descricao', 'manual_id', 'lang', 'is_active') 
          search_fields = ('descricao', 'manual_id') 
          list_filter = ('lang', 'is_active') 
          ordering = ['id']
          inlines = (ProdutoInline, RevisaoInline)       
      
      admin.site.register(Manual, ManualAdmin)
    2. Rode o servidor e acesse o Django Admin:

      python manage.py runserver

      Vá para http://127.0.0.1:8000/admin, faça login (crie um superuser com python manage.py createsuperuser), e veja os produtos registrados.

Criar Views e Template, CrispyForm e Formset PARTE 02

Criar Views e Templates

  • FBV → simples, direto, bom para views pequenas.
  • CBV → reaproveita código, organiza por métodos (get, post), facilita herança e mixins.

Lista de Manuais

views.py

from django.views.generic import ListView
from .models import Manual

class ManualListView(ListView):
    model = Manual
    template_name = "manual_list.html"
    context_object_name = "manuais"  # Nome da variável usada no template 

urls.py

from django.urls import path
from .views import ManualListView

urlpatterns = [
    path('manuais/', ManualListView.as_view(), name='manual_list'),
]

manual_list.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Lista de Manuais</title>
    <!-- Bootstrap CSS -->
    <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
</head>
<body>
    <div class="container mt-5">
        <h1 class="mb-4 text-center">Lista de Manuais</h1>
        <div class="table-responsive">
            <table class="table table-striped">
                <thead class="table-light">
                    <tr>
                        <th scope="col">ID</th>
                        <th scope="col">Descrição</th>
                        <th scope="col">Idioma</th>
                        <th scope="col">Ativo</th>
                    </tr>
                </thead>
                <tbody>
                    {% for manual in manuais %}
                        <tr>
                            <td>{{ manual.manual_id }}</td>
                            <td>{{ manual.descricao }}</td>
                            <td>{{ manual.lang}}</td>
                            <td>
                                <span class="badge {{ manual.is_active|yesno:'bg-success,bg-danger' }}">
                                    {{ manual.is_active|yesno:'Sim,Não' }}
                                </span>
                            </td>
                        </tr>
                    {% empty %}
                        <tr>
                            <td colspan="4" class="text-center">Nenhum manual encontrado.</td>
                        </tr>
                    {% endfor %}
                </tbody>
            </table>
        </div> 
    </div>

    <!-- Bootstrap JS -->
		<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.min.js" integrity="sha384-0pUGZvbkm6XF6gxjEnlmuGrJXVbNuzT9qBBavbLwCsOGabYfZo0T0to5eqruptLy" crossorigin="anonymous"></script>
</body>
</html>

Conseguimos testar,

python manage.py runserver

CrispyForm e Formset

https://pypi.org/project/crispy-bootstrap5/

https://pypi.org/project/django-crispy-forms/

gera Requirements.txt

crispy-bootstrap5 
django-crispy-forms==1.14.0

Criar template base para renderizar nosso conteúdo

{% load static %}
<!doctype html>
<html lang="en">

<head>
	<!-- Required meta tags -->
	<meta charset="utf-8">
	<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

	<!-- Bootstrap CSS --> 
	<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">

	<title>My Site</title>
</head>

<body>
	<div class="container my-5">
		{% block content %}{% endblock %}
	</div>

	<!-- Optional JavaScript -->
	<!-- jQuery first, then Popper.js, then Bootstrap JS --> 
	<script src="https://code.jquery.com/jquery-3.7.1.min.js" integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo=" crossorigin="anonymous"></script> 
	
	<script src="https://cdn.jsdelivr.net/npm/@popperjs/[email protected]/dist/umd/popper.min.js" integrity="sha384-I7E8VVD/ismYTF4hNIPjVp/Zjvgyol6VFvRkX/vR+Vc4jQkC+hVqc2pM8ODewa9r" crossorigin="anonymous"></script>
	
	<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.min.js" integrity="sha384-0pUGZvbkm6XF6gxjEnlmuGrJXVbNuzT9qBBavbLwCsOGabYfZo0T0to5eqruptLy" crossorigin="anonymous"></script>
	
	<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.15.4/css/all.css" integrity="sha384-DyZ88mC6Up2uqS4h/KRgHuoeGwBcD4Ng9SiP4dIRy0EXTlnuz47vAwmeGwVChigm" crossorigin="anonymous">

</body>

</html>

manual_list.html

{% extends "base.html" %}  
{% block content %}  
<h1 class="mb-4 text-center">Lista de Manuais</h1>
<div class="table-responsive">
    <table class="table table-striped table-bordered">
        <thead class="table-dark">
            <tr>
                <th scope="col">ID</th>
                <th scope="col">Descrição</th>
                <th scope="col">Idioma</th>
                <th scope="col">Ativo</th>
            </tr>
        </thead>
        <tbody>
            {% for manual in manuais %}
                <tr>
                    <td>{{ manual.manual_id }}</td>
                    <td>{{ manual.descricao }}</td>
                    <td>{{ manual.lang}}</td>
                    <td>
                        <span class="badge {{ manual.is_active|yesno:'bg-success,bg-danger' }}">
                            {{ manual.is_active|yesno:'Sim,Não' }}
                        </span>
                    </td>
                </tr>
            {% empty %}
                <tr>
                    <td colspan="4" class="text-center">Nenhum manual encontrado.</td>
                </tr>
            {% endfor %}
        </tbody>
    </table>
</div> 
{% endblock content %}

Forms (Atualizados para Manual, Produto, Revisao)

Formulário para o Manual

from django import forms

from crispy_forms.helper import FormHelper

from crispy_forms.layout import Layout, Field, Fieldset, ButtonHolder, Submit, HTML

from crispy_forms.bootstrap import TabHolder, Tab

from .models import Manual, Produto, Revisao

from django.forms.models import inlineformset_factory

# Formulário para o Manual
class ManualForm(forms.ModelForm):
    class Meta:
        model = Manual
        exclude = ()
        labels = {
            'is_active': 'Ativar revisões do produto'
        }

    def __init__(self, *args, **kwargs):
        super(ManualForm, self).__init__(*args, **kwargs)
        self.helper = FormHelper()
        self.helper.form_show_labels = True
        self.helper.form_enctype = 'multipart/form-data'
        self.helper.form_class = 'form-horizontal'
        self.helper.label_class = 'col-md-3 create-label'
        self.helper.field_class = 'col-md-9'

        # Layout com abas
        self.helper.layout = Layout(
            TabHolder(
                Tab('Manual do Produto',
                    Field('descricao'),
                    Field('manual_id'),
                    Field('lang'),
                    CustomCheckbox('is_active')
                ),
                Tab('Produtos (Modelos)',
                    Fieldset('Adicionar Modelo', CustomFormset('produto'))
                ),
                Tab('Revisão do manual (PDF)',
                    Fieldset('Adicionar Revisão', CustomFormset('revisao'))
                )
            ),
            HTML("<br>"),
            ButtonHolder(
                Submit('submit', 'Salvar'),
            )
        )

Formulário para o Produto

class ProdutoForm(forms.ModelForm):
    class Meta:
        model = Produto
        exclude = ()

    def __init__(self, *args, **kwargs):
        super(ProdutoForm, self).__init__(*args, **kwargs)
        self.helper = FormHelper()
        self.helper.form_show_labels = False
        self.helper.layout = Layout(
            Field('codigo_modelo'),
            Field('descricao')
        )

ProdutoFormSet = inlineformset_factory(
								Manual, Produto, form=ProdutoForm, extra=1, can_delete=True)

Formulário para Revisao

class RevisaoForm(forms.ModelForm):
    pdf = forms.FileField(
        label="Upload do arquivo PDF",
        help_text="Selecione o arquivo PDF para upload.",
        error_messages={
            "required": "Escolha o arquivo PDF que foi exportado da planilha"
        },
    )

    class Meta:
        model = Revisao
        exclude = ()

    def __init__(self, *args, **kwargs):
        super(RevisaoForm, self).__init__(*args, **kwargs)
        self.helper = FormHelper()
        self.helper.form_show_labels = False
        self.helper.layout = Layout(
            Field('revisao'),
            Field('pdf'),
        )

RevisaoFormSet = inlineformset_factory(
									Manual, Revisao, form=RevisaoForm, extra=1, can_delete=True)

CustomFormset

from crispy_forms.layout import LayoutObject, Field 
from django.template.loader import render_to_string

###### Thanks!
###### https://stackoverflow.com/questions/15157262/django-crispy-forms-nesting-a-formset-within-a-form/22053952#22053952

class CustomFormset(LayoutObject):
    """ 
    Renders an entire formset, as though it were a Field.
    Accepts the names (as a string) of formset and helper as they
    are defined in the context

    Examples:
        Formset('cadastro_formset')
        Formset('cadastro_formset', 'cadastro_formset_helper')
    """

    template = "generic/custom_formset.html"

    def __init__(self, formset_context_name, helper_context_name=None,
                 template=None, label=None):

        self.formset_context_name = formset_context_name
        self.helper_context_name = helper_context_name

        # crispy_forms/layout.py:302 requires us to have a fields property
        self.fields = []

        # Overrides class variable with an instance level variable
        if template:
            self.template = template

    def render(self, form, form_style, context, **kwargs):
        formset = context.get(self.formset_context_name)
        helper = context.get(self.helper_context_name)
        # closes form prematurely if this isn't explicitly stated
        if helper:
            helper.form_tag = False

        context.update({'formset': formset, 'helper': helper})
        return render_to_string(self.template, context.flatten())
    
class CustomCheckbox(Field):
    template = 'generic/custom_checkbox.html'

generic/custom_formset.html generic/custom_checkbox.html

{% load crispy_forms_tags %}
<table class="table table-striped table-hover"> 
    {{ formset.management_form|crispy }} 
    {% if formset.forms %}
    <thead>
        <tr>
            {% for field in formset.forms.0.visible_fields %}
                <th>{{ field.label|capfirst }}</th>
            {% endfor %} 
        </tr>
    </thead>
    {% endif %}
    <tbody> 
    {% for form in formset.forms %}
        <tr class="{% cycle 'row1' 'row2' %} formset_row-{{ formset.prefix }}">
            {% for field in form.visible_fields %}
            <td> 
                {% if forloop.first %}
                    {% for hidden in form.hidden_fields %} 
                        {{ hidden }}
                    {% endfor %}
                {% endif %}
                {{ field.errors.as_ul }}  
                {{ field|as_crispy_field }}
            </td> 
            {% endfor %}
        </tr>
    {% endfor %} 
    </tbody>
</table>
<br>
<script src="https://code.jquery.com/jquery-3.7.1.min.js" integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo=" crossorigin="anonymous"></script>

<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery.formset/1.2.2/jquery.formset.min.js" integrity="sha512-ltwjKsDTo3hW/wV66ZaEkf2wOAFxmg7rWM76J8kOcYKLSKy44WBYO/BFaNNH3NGDS8BSz3meB9wtSnm41oL+pA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
 
<script type="text/javascript"> 
    $('.formset_row-{{ formset.prefix }}').formset({
        addText: 'Adicionar Identificador',
        deleteText: 'remove',
        prefix: '{{ formset.prefix }}',
        addCssClass: 'add-row btn btn-primary',
        added: function (row) {
            // Adiciona o ícone Font Awesome ao botão delete de cada nova linha
            $(row).find('.delete-row').html('<span class="text-danger"><i class="fas fa-trash fa-3x"></i></span>');
        }
    });

    // Adiciona o ícone de lixeira para as linhas  existentes
    $('.delete-row').html('<span class="text-danger"><i class="fas fa-trash fa-3x"></i></span>'); 
</script>
{% load crispy_forms_field %}
<div class="form-group">
  <div class="custom-control custom-checkbox">
    {% crispy_field field 'class' 'custom-control-input' %}
    <label class="custom-control-label" for="{{ field.id_for_label }}">{{ field.label }}</label>
  </div>
</div>

views.py

from django.urls import reverse_lazy
from django.shortcuts import render
from django.views.generic import ListView, CreateView
from django.db import transaction
from .forms import ManualForm, ProdutoFormSet, RevisaoFormSet
from .models import Manual

class ManualCreateView(CreateView):
    model = Manual
    template_name = 'form_create.html'
    form_class = ManualForm
    success_url = None

    def get_context_data(self, **kwargs):
        data = super().get_context_data(**kwargs)
        if self.request.POST:
            data['produto'] = ProdutoFormSet(self.request.POST, self.request.FILES)
            data['revisao'] = RevisaoFormSet(self.request.POST, self.request.FILES)
        else:
            data['produto'] = ProdutoFormSet()
            data['revisao'] = RevisaoFormSet()
        return data

    def form_valid(self, form):
        context = self.get_context_data()
        produto = context['produto']
        revisao = context['revisao']

        with transaction.atomic():
            self.object = form.save()
            if produto.is_valid() and revisao.is_valid():
                produto.instance = self.object
                revisao.instance = self.object
                produto.save()
                revisao.save()
            else:
                return self.form_invalid(form)

        return super().form_valid(form)

    def get_success_url(self):
        return reverse_lazy('manual_detail', kwargs={'pk': self.object.pk})

urls.py

from django.urls import path
from .views import ManualCreateView

urlpatterns = [
    path('manual-create/', ManualCreateView.as_view(), name='manual_create'), 
]

manual_create.html

{% extends "base.html" %} 
{% block title %}Page{% endblock %}
{% load crispy_forms_tags %}
{% block content %}  
<div class="card shadow-sm">
	<div class="card-header bg-success text-white">
		<h2 class="mb-0">Criar/Atualizar</h2>
	</div>
    <div class="card-body">
        <form class="form-horizontal" method="POST" enctype="multipart/form-data">
        {% crispy form %}    
        </form>
    </div>
</div> 
{% endblock content %}

erro

from crispy_forms.utils import TEMPLATE_PACK

def render(self, form, context, template_pack=TEMPLATE_PACK, **kwargs):

update

from django.views.generic.edit import UpdateView 
from .models import Manual
from .forms import ProdutoFormSet, RevisaoFormSet, ManualForm

class ManualUpdateView(UpdateView):
    model = Manual
    form_class = ManualForm
    template_name = 'generic/form_generic.html'

    def get_context_data(self, **kwargs):
        data = super(ManualUpdateView, self).get_context_data(**kwargs)
        if self.request.POST: # Salva os dados postados
            data['produto'] = ProdutoFormSet(self.request.POST, instance=self.object)
            data['revisao'] = RevisaoFormSet(self.request.POST, self.request.FILES, instance=self.object)
        else:
            data['produto'] = ProdutoFormSet(instance=self.object)
            data['revisao'] = RevisaoFormSet(instance=self.object)
        data['title'] = 'Editar Manual'
        return data

    def form_valid(self, form):
        context = self.get_context_data()
        produto = context['produto']
        revisao = context['revisao']
        with transaction.atomic():
            self.object = form.save()
            if produto.is_valid() and revisao.is_valid():
                produto.instance = self.object
                revisao.instance = self.object
                produto.save()
                revisao.save()
            else:
                return self.form_invalid(form)
        return super(ManualUpdateView, self).form_valid(form)

    def get_success_url(self):
        return reverse_lazy('manual_detail', kwargs={'pk': self.object.pk}) 
    

url.py

path('manual-update/<int:pk>/', views.ManualUpdateView.as_view(), name='manual_update'),
Pagina de Detalhes e Delete View PARTE 03

ManualDetailView

from django.views.generic import DetailView, DeleteView 

class ManualDetailView(DetailView):
    model = Manual
    template_name = 'manuais/manual_detail.html'

    def get_context_data(self, **kwargs):
        context = super(ManualDetailView, self).get_context_data(**kwargs)
        context['produtos'] = self.object.manual_produto.all()
        context['revisoes'] = self.object.manual_revisao.all()
        return context

ManualDeleteView

class ManualDeleteView(DeleteView):
    model = Manual
    template_name = 'manuais/manual_detail.html'
    success_url = reverse_lazy('manual_list')

Template Atualizado

{% extends "base.html" %}
{% block content %}

<div class="card shadow-sm">
    
    <div class="card-header bg-success text-white">
        <h2 class="mb-0">Manual ID: {{ object }}</h2>
    </div>
    
    <div class="card-body">
        <!-- Detalhes do Manual -->
        <div class="mb-4">
            {% if object.descricao %}
                <p><strong>Descrição:</strong> {{ object.descricao }}</p>
            {% endif %}
            {% if object.idioma %}
                <p><strong>Idioma:</strong> {{ object.idioma }}</p>
            {% endif %}
            {% if object.ativo %}
                <p><strong>Status:</strong> {{ object.ativo|yesno:"Ativo,Inativo" }}</p>
            {% endif %}
        </div>

        <!-- Modelos -->
        {% if produtos %}
            <h4 class="mb-3">Modelos Associados</h4>
            <table class="table table-bordered table-hover">
                <thead class="table-light">
                    <tr>
                        <th scope="col">Nome do Modelo</th>
                        <th scope="col">Descrição</th>
                    </tr>
                </thead>
                <tbody>
                    {% for p in produtos %}
                        <tr>
                            <td>{{ p.codigo_modelo }}</td>
                            <td>{{ p.descricao }}</td>
                        </tr>
                    {% endfor %}
                </tbody>
            </table>
        {% endif %}

        <!-- Revisões -->
        {% if revisoes %}
            <h4 class="mb-3">Revisões</h4>
            <table class="table table-bordered table-hover">
                <thead class="table-light">
                    <tr>
                        <th scope="col">Revisão</th>
                        <th scope="col">Documento</th>
                        <th scope="col">Link</th>
                    </tr>
                </thead>
                <tbody>
                    {% for revisao in revisoes %}
                        <tr>
                            <td>{{ revisao.revisao }}</td>
                            <td><a href="{{ revisao.pdf.url }}" target="_blank">Ver Documento</a></td>
                            <td><a href="" target="_blank">Link</a></td>
                        </tr>
                    {% endfor %}
                </tbody>
            </table>
        {% endif %}

        <!-- Ações -->
        <div class="d-flex justify-content-end mt-4">
            <a class="btn btn-outline-info me-2" href="{% url 'manual_update' pk=object.id %}">Atualizar</a>
            <a class="btn btn-outline-danger" data-bs-toggle="modal" data-bs-target="#deleteModal">Excluir</a>
        </div>

    </div>
</div>

<!-- Modal de Confirmação -->
<div class="modal fade" id="deleteModal" tabindex="-1" aria-labelledby="deleteModalLabel" aria-hidden="true">
    <div class="modal-dialog">
        <div class="modal-content">
            <div class="modal-header bg-danger text-white">
                <h5 class="modal-title" id="deleteModalLabel">Confirmar Exclusão</h5>
                <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Fechar"></button>
            </div>
            <div class="modal-body">
                <p>Você tem certeza de que deseja excluir este manual e todos os itens relacionados?</p>
                <p><strong>Total de itens relacionados:</strong> {{ produtos.count|add:revisoes.count }}</p>
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
                <form method="post" action="{% url 'manual_delete' pk=object.id %}">
                    {% csrf_token %}
                    <button type="submit" class="btn btn-danger">Confirmar Exclusão</button>
                </form>
            </div>
        </div>
    </div>
</div>
{% endblock content %}

Atualização das URLs

path('manual-detail/<int:pk>/', views.ManualDetailView.as_view(), name='manual_detail'),
path('manual-delete/<int:pk>/', views.ManualDeleteView.as_view(), name='manual_delete'),

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published