Shiko
Shiko

Reputation: 149

Using Validators in Django not working properly

I have recently learning about Validators and how they work but I am trying to add a function to my blog project to raise an error when a bad word is used. I have a list of bad words in a txt and added the code to be in the models.py the problem is that nothing is blocked for some reason I am not sure of.

Here is the models.py

class Post(models.Model):
       title = models.CharField(max_length=100, unique=True)
       ---------------other unrelated------------------------

def validate_comment_text(text):
    with open("badwords.txt") as f:
        censored_word = f.readlines()
    words = set(re.sub("[^\w]", " ", text).split())
    if any(censored_word in words for censored_word in CENSORED_WORDS):
        raise ValidationError(f"{censored_word} is censored!")

class Comment(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    post = models.ForeignKey(Post, on_delete=models.CASCADE)
    content = models.TextField(max_length=300, validators=[validate_comment_text])
    updated = models.DateTimeField(auto_now=True)
    created = models.DateTimeField(auto_now=True)

here is the views.py:

class PostDetailView(DetailView):
    model = Post
    template_name = "blog/post_detail.html"  # <app>/<model>_<viewtype>.html

    def get_context_data(self, *args, **kwargs):
        context = super(PostDetailView, self).get_context_data()
        post = get_object_or_404(Post, slug=self.kwargs['slug'])
        comments = Comment.objects.filter(
            post=post).order_by('-id')
        total_likes = post.total_likes()
        liked = False
        if post.likes.filter(id=self.request.user.id).exists():
            liked = True

        if self.request.method == 'POST':
            comment_form = CommentForm(self.request.POST or None)
            if comment_form.is_valid():
                content = self.request.POST.get('content')
                comment_qs = None

                comment = Comment.objects.create(
                    post=post, user=self.request.user, content=content)
                comment.save()
                return HttpResponseRedirect("blog/post_detail.html")
        else:
            comment_form = CommentForm()

        context["comments"] = comments
        context["comment_form"] = comment_form
        context["total_likes"] = total_likes
        context["liked"] = liked
        return context

    def get(self, request, *args, **kwargs):
        res = super().get(request, *args, **kwargs)
        self.object.incrementViewCount()
        if self.request.is_ajax():
            context = self.get_context_data(self, *args, **kwargs)
            html = render_to_string('blog/comments.html', context, request=self.request)
            return JsonResponse({'form': html})
        return res

class PostCommentCreateView(LoginRequiredMixin, CreateView):
    model = Comment
    form_class = CommentForm

    def form_valid(self, form):
        post = get_object_or_404(Post, slug=self.kwargs['slug'])
        form.instance.user = self.request.user
        form.instance.post = post
        return super().form_valid(form)

Here is my trial which didn't work

def validate_comment_text(sender,text, instance, **kwargs):
    instance.full_clean()
    with open("badwords.txt") as f:
        CENSORED_WORDS = f.readlines()

    words = set(re.sub("[^\w]", " ", text).split())
    if any(censored_word in words for censored_word in CENSORED_WORDS):
        raise ValidationError(f"{censored_word} is censored!")

pre_save.connect(validate_comment_text, dispatch_uid='validate_comment_text')

I am new learner so if you could provide some explanation to the answer I would be grateful so that I can avoid repeating the same mistakes.

Upvotes: 3

Views: 1511

Answers (2)

Mario Orlandi
Mario Orlandi

Reputation: 5849

I'm sure there are many ways to handle this, but I finally decided to adopt a common practice in all my Django projects:

when a Model requires validation, I override clean() to collect all validation logic in a single place and provide appropriate error messages.

In clean(), you can access all model fields, and do not need to return anything; just raise ValidationErrors as required:

from django.db import models
from django.core.exceptions import ValidationError


class MyModel(models.Model):

    def clean(self):
         
        if (...something is wrong in "self.field1" ...) {
            raise ValidationError({'field1': "Please check field1"})
        }
        if (...something is wrong in "self.field2" ...) {
            raise ValidationError({'field2': "Please check field2"})
        }

        if (... something is globally wrong in the model ...) {
            raise ValidationError('Error message here')
        }

The admin already takes advantages from this, calling clean() from ModelAdmin.save_model(), and showing any error in the change view; when a field is addressed by the ValidationError, the corresponding widget will be emphasized in the form.

To run the very same validation when saving a model programmatically, just override save() as follows:

class MyModel(models.Model):

    def save(self, *args, **kwargs):
        self.full_clean()
        ...
        return super().save(*args, **kwargs)

Proof:

file models.py

from django.db import models


class Model1(models.Model):

    def clean(self):
        print("Inside Model1.clean()")

    def save(self, *args, **kwargs):
        print('Enter Model1.save() ...')
        super().save(*args, **kwargs)
        print('Leave Model1.save() ...')
        return

class Model2(models.Model):

    def clean(self):
        print("Inside Model2.clean()")

    def save(self, *args, **kwargs):
        print('Enter Model2.save() ...')
        self.full_clean()
        super().save(*args, **kwargs)
        print('Leave Model2.save() ...')
        return

file test.py

from django.test import TestCase
from project.models import Model1
from project.models import Model2

class SillyTestCase(TestCase):

    def test_save_model1(self):
        model1 = Model1()
        model1.save()

    def test_save_model2(self):
        model2 = Model2()
        model2.save()

Result:

❯ python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
Enter Model1.save() ...
Leave Model1.save() ...
.Enter Model2.save() ...
Inside Model2.clean()
Leave Model2.save() ...
.
----------------------------------------------------------------------
Ran 2 tests in 0.002s

OK
Destroying test database for alias 'default'...

Upvotes: 4

Aman Garg
Aman Garg

Reputation: 2547

Validators run only when you use ModelForm. If you directly call comment.save(), validator won't run. link to docs

So either you need to validate the field using ModelForm or you can add a pre_save signal and run the validation there (you'll need to manually call the method, or use full_clean to run the validations). Something like:

from django.db.models.signals import pre_save

def validate_model(sender, instance, **kwargs):
    instance.full_clean()

pre_save.connect(validate_model, dispatch_uid='validate_models')

Upvotes: 0

Related Questions