Search code examples
pythondjangodjango-models

Django circular import (models-views(-forms))


Watched all the previous related topics devoted to this problem, but haven't found a proper solution, so decided to create my own question.

I'm creating a forum project (as a part of the site project). Views are made via class-based views: SubForumListView leads to a main forum page, where its main sections ("subforums") are listed. TopicListView, in its turn, leads to the pages of certain subforums with the list of active topics created within the subforum. ShowTopic view leads to a certain topic page with a list of comments.

The problem manifests itself because of:

  1. Models.py: Method get_absolute_url in the model Subforum, which in its return section's reverse function takes a view as the 1st argument; I've tried to avoid a direct import of the view, but the program doesn't accept other variants;
  2. Views.py: most of the views have imported models either in instance arguments (model = Subforum), or in methods using querysets (like in get_context_data: topics = Topic.objects.all()); I can't surely say whether the change of instance argument model = Subforum to model = 'Subforum' really helps, as it's impossible to do with queryset methods and thus can't be proved;
  3. Forms.py: my form classes were created via forms.ModelForm and include class Meta, where the model instance argument is provided the same way as in 2): model = Topic. For now I've commented them (again, without being sure whether it was helpful or not), as well as the import of models, but when they were active, there was a triple circular import "models-views-forms" (funny enough).

I see this problem, I know what and where provokes it, but I don't know how to solve it, that is: I don't know how to better define views and forms (or, maybe, models with their "get_absolute_url" methods) to avoid CI and how to better organize the connection between different parts of the program.

Corresponding files:

models.py:

from django.db import models
from django.contrib.auth.models import User
from django.urls import reverse
from django.utils.text import slugify

from .consts import *
from .views import TopicListView, ShowTopic

'''
class User(AbstractUser):
    class Meta:
        app_label = 'forum'
'''


class Profile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    surname = models.CharField(max_length=32, default='')
    name = models.CharField(max_length=32, default='')
    email = models.EmailField(max_length=254, blank=True, unique=True)
    bio = models.TextField(max_length=500, default="Write a couple of words about yourself")
    avatar = models.ImageField(default=None, blank=True, max_length=255)
    status = models.CharField(max_length=25, blank=True, default='')
    slug = models.SlugField()
    age = models.IntegerField(verbose_name='Возраст', null=True, blank=True)
    gender = models.CharField(verbose_name='Пол', max_length=32, choices=Genders.GENDER_CHOICES, default="H", blank=True)
    reputation = models.IntegerField(verbose_name='Репутация', default=0)

    def __str__(self):
        return f'{self.user} profile'

    def get_absolute_url(self):
        return reverse('user_profile', kwargs={'profile_slug': self.slug})

    def save(self, *args, **kwargs):
        if not self.id:
            self.slug = slugify(self.user.username)
            return super(Profile, self).save(*args, **kwargs)


class Subforum(models.Model):
    title = models.CharField(verbose_name='Название', max_length=32, choices=Theme.THEME_CHOICES, default=1)
    slug = models.SlugField(default='News')
    objects = models.Manager()

    class Meta:
        ordering = ['title']
        verbose_name = 'Разделы форума'
        verbose_name_plural = 'Разделы форума'

    def __str__(self):
        return self.title

    def save(self, *args, **kwargs):
        if not self.id:
            self.slug = slugify(self.title)
            return super(Subforum, self).save(*args, **kwargs)

    def get_absolute_url(self):
        return reverse(TopicListView, kwargs={'name': self.title, 'subforum_slug': self.slug})


class Topic(models.Model):
    subject = models.CharField(verbose_name='Заголовок', max_length=255, unique=True)
    first_comment = models.TextField(verbose_name='Сообщение', max_length=2000, default='')
    slug = models.SlugField(default='', unique=True, max_length=25, editable=False)
    subforum = models.ForeignKey('Subforum',
                                 verbose_name='Раздел',
                                 on_delete=models.CASCADE,
                                 related_name='subforum')
    creator = models.ForeignKey(User,
                                verbose_name='Создатель темы',
                                on_delete=models.SET('deleted'),
                                related_name='creator')
    created = models.DateTimeField(auto_now_add=True)
    closed = models.BooleanField(default=False)
    objects = models.Manager()

    class Meta:
        ordering = ['id']
        verbose_name = 'Обсуждения'
        verbose_name_plural = 'Обсуждения'

    def __str__(self):
        return self.subject

    def save(self, *args, **kwargs):
        if not self.id:
            self.slug = f'topic-{slugify(self.subject)}'[0:25]
            return super(Topic, self).save(*args, **kwargs)

    def get_absolute_url(self):
        return reverse(ShowTopic, kwargs={'topic_slug': self.slug})


class Comment(models.Model):
    topic = models.ForeignKey('Topic',
                              verbose_name='Тема',
                              on_delete=models.CASCADE,
                              related_name='topic')
    author = models.ForeignKey(User,
                               verbose_name='Комментатор',
                               on_delete=models.SET('deleted'),
                               related_name='author')
    content = models.TextField(verbose_name='Текст', max_length=2000)
    created = models.DateTimeField(verbose_name='Дата публикации', auto_now_add=True)
    updated = models.DateTimeField(verbose_name='Дата изменения', auto_now=True)
    objects = models.Manager()

    class Meta:
        ordering = ['created']
        verbose_name = 'Комментарии'
        verbose_name_plural = 'Комментарии'

    def __str__(self):
        return f'Post of {self.topic.subject} is posted by {self.author.username}.'

views.py:

from django.contrib.auth.mixins import LoginRequiredMixin
from django.shortcuts import get_object_or_404
from django.urls import reverse_lazy
from django.views.generic import ListView, DetailView, CreateView, UpdateView, DeleteView

from core.views import menu
from .forms import AddTopicForm, AddCommentForm
from .models import Subforum, Topic, Comment, Profile
from .utils import DataMixin


class SubForumListView(ListView):
    model = Subforum
    context_object_name = 'subforum_list'
    template_name = "forum/forum.html"

    def get_context_data(self, **kwargs):
        subforums = Subforum.objects.all()
        context = {'subforums': subforums}
        return context


class TopicListView(ListView):
    model = Topic
    template_name = "forum/subforum.html"
    slug_url_kwarg = 'subforum_slug'
    context_object_name = 'subforum'

    def get_context_data(self, **kwargs):
        topics = Topic.objects.all()
        context = {'topics': topics}
        return context


class ShowTopic(DetailView):
    model = Topic
    template_name = "forum/topic.html"
    slug_url_kwarg = 'topic_slug'
    context_object_name = 'topic'

    def get_context_data(self, topic_slug, **kwargs):
        topic = get_object_or_404(Topic, slug=topic_slug)
        comments = Comment.objects.filter(topic=topic)
        comments_number = len(Comment.objects.filter(topic=topic))
        context = {'menu': menu,
                   'topic': topic,
                   'comments': comments,
                   'comm_num': comments_number}
        return context


class AddTopic(LoginRequiredMixin, DataMixin, CreateView):
    form_class = AddTopicForm
    template_name = 'forum/addtopic.html'
    page_title = 'Создание новой темы'


class AddComment(LoginRequiredMixin, DataMixin, CreateView):
    form_class = AddCommentForm
    template_name = 'forum/addcomment.html'
    page_title = 'Оставить комментарий'
    success_url = reverse_lazy('topic')


class UpdateComment(LoginRequiredMixin, DataMixin, UpdateView):
    form_class = AddCommentForm
    template_name = 'forum/addcomment.html'
    page_title = 'Редактировать комментарий'
    success_url = reverse_lazy('topic')


class UserProfile(DetailView):
    model = Profile
    template_name = "profile.html"

forms.py:

from django import forms
from django.core.exceptions import ValidationError

#from forum.models import Topic, Comment


class AddTopicForm(forms.ModelForm):
    subject = forms.CharField(label="Заголовок", max_length=100, min_length=7)
    first_comment = forms.CharField(label="Сообщение", widget=forms.Textarea())

    class Meta:
        #model = Topic
        fields = ['subject', 'first_comment']

    def clean_subject(self):
        subject = self.cleaned_data['subject']
        if len(subject) > 100:
            raise ValidationError("Длина превышает 100 символов")
        if len(subject) < 7:
            raise ValidationError("Слишком короткое заглавие, требуется не менее 7 символов")
        return subject


class AddCommentForm(forms.ModelForm):
    content = forms.CharField(label="Текст комментария", max_length=2000, min_length=1, widget=forms.Textarea())

    class Meta:
        #model = Comment
        fields = ['content']

I am not sure whether it's necessary or not, but also urls.py for you:

from django.urls import path

from forum.views import *

urlpatterns = [
    #path('<slug:profile_slug>/', user_profile, name='user_profile'),
    path('', SubForumListView.as_view(), name='forum'),
    path('<slug:subforum_slug>/', TopicListView.as_view(), name='subforum'),
    path('subforum/<slug:topic_slug>/', ShowTopic.as_view(), name='topic'),
    path('subforum/add-topic/', AddTopic.as_view(), name="add_topic"),
    path('subforum/<slug:topic_slug>/add-comment/', AddComment.as_view(), name="add_comment"),
    path('subforum/<slug:topic_slug>/edit/<int:id>/', UpdateComment.as_view(), name="edit_comment"),

If some additional files/information are necessary, I'm ready to provide them. For now, I can't continue implementation of the project, as CI doesn't allow me to test the forum page. So I have to solve this problem before any further actions.


Solution

  • Don't import views in the models. Views should depend on models, never in the opposite way.

    You can use the name of the path, so:

    class Subforum(models.Model):
        # …
    
        def get_absolute_url(self):
            return reverse(
                'subforum',
                kwargs={'name': self.title, 'subforum_slug': self.slug},
            )

    The same for ShowTopic, and thus remove the imports.


    Note: You can make use of django-autoslug [GitHub] to automatically create a slug based on other field(s).