You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Django DeleteView误删父级Project问题求助

问题描述

我有两个Django模型:Project和ProjectNotes。ProjectNotes是关联Project的笔记,它的project字段是关联Project的外键。

遇到的问题:删除项目笔记时,整个Project被一并删除了,但我只需要删除对应的笔记。我已经在ProjectNotes模型里正确设置了on_delete=models.CASCADE,所以问题应该出在视图代码里。


模型代码

class ProjectManager(models.Manager):
    def search(self, query=None):
        qs = self.get_queryset()
        if query is not None:
            or_lookup = (
                         Q(title__icontains=query) |
                         Q(description__icontains=query)
                         # Q(slug__icontains=query)
                        )
            qs = qs.filter(or_lookup).distinct() # distinct() is often necessary with Q lookups
        return qs

    def get_with_counted_notes_documents_todos(self):
        queryset = self.get_queryset().annotate(
            num_notes=Count('notes'),
            num_documents=Count('project_documents'),
            num_todos=Count('todo_group')
        )

        return queryset

class Project(models.Model):
    title = models.CharField(max_length= 200)
    description = tinymce_models.HTMLField()
    status = models.CharField(max_length=20, choices=PROJECT_CHOICES, default="active")
    date = models.DateTimeField(auto_now_add=True, null=True)
    created_by = models.ForeignKey(CustomUser, editable=False, null=True, blank=True, on_delete=models.RESTRICT)
    tags = tagulous.models.TagField(to=SiteWideTags, blank=True, related_name='projects_tags')

    objects = ProjectManager()

    def __str__(self):
        return self.title

    def get_absolute_url(self):
        return reverse('company_project:project_detail', args=[str(self.id)])


class ProjectNotesManager(models.Manager):
    def search(self, query=None):
        qs = self.get_queryset()
        if query is not None:
            or_lookup = (
                         Q(title__icontains=query) |
                         Q(body__icontains=query)
                         # Q(slug__icontains=query)
                        )
            qs = qs.filter(or_lookup).distinct() # distinct() is often necessary with Q lookups
        return qs

class ProjectNotes(models.Model):
    title = models.CharField(max_length=200)
    body = tinymce_models.HTMLField()
    project = models.ForeignKey(Project, default=0, blank=True, on_delete=models.CASCADE, related_name='notes')
    date = models.DateTimeField(auto_now_add=True, null=True)
    created_by = models.ForeignKey(CustomUser, editable=False, null=True, blank=True, on_delete=models.RESTRICT)

    def __str__(self):
        return self.title

    def get_absolute_url(self):
        return reverse('company_project:project_note_detail', args=[self.project_id, str(self.id)])

    objects = ProjectNotesManager()

视图代码

class ProjectNoteDeleteView(DeleteBreadcrumbMixin, DeleteView):
    model = ProjectNotes
    template_name = 'company_accounts/delete_project_note.html'


    @cached_property
    def crumbs(self):
        project = self.get_object()
        relate_notes = self.get_note()
        bread_crumbs = [
            ("projects", reverse(
                "company_project:" + CompanyProjects.list_view_name, )
             ),
            (f"{project.title}",
             reverse(
                 "company_project:" + CompanyProjectsDetailView.detail_view_name,
                 kwargs={'pk': project.id})
             ),
            ("notes", reverse(
                "company_project:projectnotes_list",
                kwargs={'pk': project.id})
             ),
            (f"{relate_notes.title}",
             reverse(
                 "company_project:project_note_detail",
                 kwargs={'project_pk': project.id, 'pk': self.kwargs.get('pk')})
             ),
            (f"Delete: {relate_notes.title}",
             reverse(
                 "company_project:project_note_detail",
                 kwargs={'project_pk': project.id, 'pk': self.kwargs.get('pk')})
             )
        ]
        return bread_crumbs

    def get_object(self):
        return get_object_or_404(Project, id=self.kwargs.get('project_pk'))

    def get_note(self):
        project = self.get_object()
        return get_object_or_404(project.notes, id=self.kwargs.get('pk'))

    def related_project(self, **kwargs):
        project = get_object_or_404(Project, id=self.kwargs.get('project_pk'))
        return project

    def get_context_data(self, **kwargs):
        # Call the base implementation first to get a context
        context = super().get_context_data(**kwargs)
        context["note"] = self.get_note()
        context['project'] = self.get_object()
        return context

    def get_success_url(self):
        return reverse('company_project:project_detail', args=[(self.object.id)])

URL配置

path('project/<int:project_pk>/note/<int:pk>/delete', ProjectNoteDeleteView.as_view(), name='project_note_delete'),

模板代码

<!-- templates/company_accounts/delete_project_note.html -->

{% extends 'base.html' %}
{% block content %}
<div class="section-container container">
  <div class="general-main-column">
      <div class="row">

        <form method="post">{% csrf_token %}
     
         <p>Are you sure you want to permanently delete project note "{{ note }}"?</p>
  
         <input type="submit" class="btn btn-danger" value="Confirm">
        </form>
      </div>
    </div>
</div>
{% endblock content %}

问题原因及修复方案

核心问题

你重写了DeleteView的get_object()方法,让它返回的是Project对象而不是ProjectNotes对象。DeleteView默认会删除get_object()返回的实例,所以点击删除时,实际删除的是整个项目,而不是目标笔记。

修复后的视图代码

class ProjectNoteDeleteView(DeleteBreadcrumbMixin, DeleteView):
    model = ProjectNotes
    template_name = 'company_accounts/delete_project_note.html'

    @cached_property
    def crumbs(self):
        note = self.get_object()
        project = note.project
        bread_crumbs = [
            ("projects", reverse(
                "company_project:" + CompanyProjects.list_view_name, )
             ),
            (f"{project.title}",
             reverse(
                 "company_project:" + CompanyProjectsDetailView.detail_view_name,
                 kwargs={'pk': project.id})
             ),
            ("notes", reverse(
                "company_project:projectnotes_list",
                kwargs={'pk': project.id})
             ),
            (f"{note.title}",
             reverse(
                 "company_project:project_note_detail",
                 kwargs={'project_pk': project.id, 'pk': note.id})
             ),
            (f"Delete: {note.title}",
             reverse(
                 "company_project:project_note_detail",
                 kwargs={'project_pk': project.id, 'pk': note.id})
             )
        ]
        return bread_crumbs

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context["note"] = self.object  # self.object就是当前要删除的笔记实例
        context['project'] = self.object.project
        return context

    def get_success_url(self):
        return reverse('company_project:project_detail', args=[self.object.project.id])

关键修改点

  1. 移除自定义的get_object()方法:让DeleteView默认根据model = ProjectNotes和URL中的pk参数,自动获取要删除的ProjectNotes实例,这个实例会被存到self.object中。
  2. 简化依赖逻辑:所有需要项目对象的地方,直接通过self.object.project获取,无需额外查询。
  3. 修正成功跳转地址:get_success_url中改为跳转到笔记所属项目的详情页,而不是原来错误的self.object.id(原来的self.object是Project,现在是ProjectNotes)。
  4. 删除冗余方法:去掉get_note()和related_project(),因为现在可以直接通过self.object获取笔记,关联项目也能通过笔记直接拿到。

内容的提问来源于stack exchange,提问作者Finn

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.04 09:31:33