0

I have used the default Django admin panel as my backend. I have a Blogpost model. What I am trying to do is whenever an admin user saves a blogpost object on Django admin, I need to send an email to the newsletter subscribers notifying them that there is a new blog on the website.

Since Django admin automatically saves the blog by calling the save function, I don't know where to write send email api logic. Hope I have explained it well.

My Blogpost:

class BlogPost(models.Model):

    author = models.CharField(max_length=64, default='Admin')
    CATEGORY_CHOICES = (
        ('travel_news', 'Travel News',),
        ('travel_tips', 'Travel Tips',),
        ('things_to_do', 'Things to Do',),
        ('places_to_go', 'Places to Go'),
    )
    image = models.ImageField(blank=True, null=True)
    title = models.CharField(max_length=255)
    categories = models.CharField(max_length=64, choices=CATEGORY_CHOICES, default='travel_news')
    caption = models.CharField(max_length=500)
    content = RichTextUploadingField()

    # todo support for tags
    # tags = models.CharField(max_length=255, default='travel') #todo
    tag = models.ManyToManyField(Tag)
    date_created = models.DateField()

I have overwritten the Django admin form by my model form like this.

class BlogForm(forms.ModelForm):
    CATEGORY_CHOICES = (
        ('travel_news', 'Travel News',),
        ('travel_tips', 'Travel Tips',),
        ('things_to_do', 'Things to Do',),
        ('places_to_go', 'Places to Go'),
    )
    # categories = forms.MultipleChoiceField(choices = CATEGORY_CHOICES)

    class Meta:
        model = BlogPost
        fields = ['author','image', 'title','categories',
                  'caption','content','tag','date_created']

@register(BlogPost)
class BlogPostAdmin(ModelAdmin):


    # autocomplete_fields = ['author']
    def edit(self, obj):
        return format_html('<a class="btn-btn" href="/admin/blogs/blogpost/{}/change/">Change</a>', obj.id)

    def delete(self, obj):
        return format_html('<a class="btn-btn" href="/admin/blogs/blogpost/{}/delete/">Delete</a>', obj.id)

    list_display = ('categories', 'title', 'date_created','edit', 'delete')
    icon_name = 'chrome_reader_mode'
    # inlines = [TagInline]
    form = BlogForm

I have written a view, but it doesnt make sense since there is no any url calling it as we dont need any view and url when saving from the django admin itself.

from apps.blogs.admin import BlogForm
def blogform(request):
    if request.method == 'POST':
        form = BlogForm(request.POST)
        if form.is_valid():
            form.save() 

Without this logic too, the form is working fine.

My subscribers model:

class Subscribers(models.Model):
    email = models.EmailField(unique=True)
    date_subscribed = models.DateField(auto_now_add=True)

    def __str__(self):
        return self.email

    class Meta:
        verbose_name_plural = "Newsletter Subscribers"
Reactoo
  • 916
  • 2
  • 12
  • 40
  • What about to [override `save`](https://docs.djangoproject.com/en/3.2/ref/models/instances/#saving-objects) blog method? Or just to [bind to post_save signal](https://docs.djangoproject.com/en/3.2/topics/signals/) ? – dani herrera Jul 12 '21 at 11:29
  • Ok but where to write the post save signal call?? and also where to write the logic after the signal is sent?? – Reactoo Jul 12 '21 at 11:37
  • 1
    I can write an answer based on signal if it is a solution for your issue. – dani herrera Jul 12 '21 at 11:44
  • sure. I will try to implement it. @daniherrera – Reactoo Jul 12 '21 at 11:45
  • I just posted a sample with all elements: blog, blogpost, signal, send_mail, subscribers, ... – dani herrera Jul 12 '21 at 12:09

3 Answers3

2

This is how you can send and email to subscribers after a new Post is created:

from django.db.models.signals import post_save
from django.dispatch import receiver

class BlogPost(models.Model):
    subscribers = models.ManyToManyField(User) # <-- subscribers
    title = models.CharField(max_length=255)

class BlogPost(models.Model):
    blog = models.ForeignKey(Blog, on_delete= models.CASCADE) #<-- blog
    title = models.CharField(max_length=255)
    # ....

# binding sinal:
@receiver(post_save, sender=BlogPost)
def send_mail_to_subs(sender, instance, created, **kwargs):
    if Not created:
        return
        
    for mysub in instance.blog.subscribers.all():
        send_mail(
            f'New Post {instance.title}',
            'Hi .... ',
            'from@example.com',
            [sub.email],
        )

Be free to adapt this sample to your custom scenario. For example, maybe do you have other relations to get blog or subscribers.

About performance.

If do you have lots of subscribers, this is not a real solution. For massive mailing do you need a background process to send mails.

For example, you can add a field to store if a post was notified to subscribers:

class BlogPost(models.Model):
    blog = models.ForeignKey(Blog, on_delete= models.CASCADE) #<-- blog
    title = models.CharField(max_length=255)
    notified = models.BooleanField(editable=False, default=False)

Then execute a process each X minutes to get not notified posts and send mails to all subscribers.

Do you have two options:

dani herrera
  • 48,760
  • 8
  • 117
  • 177
  • Wont it be slow, if emails are sent using for loop if there are like 1000 emails?? @dani – Reactoo Jul 12 '21 at 12:12
  • Also, I have a separate model named Subscribers which contains fields email and date_subscribed. – Reactoo Jul 12 '21 at 12:13
  • 1
    These two comments contain relevant information I miss in your initial question. I modified my answer to match with this new constraints. – dani herrera Jul 12 '21 at 12:34
  • Hello @dani, newsletter feature was added latest on the backend. Blogpost model was already created a long time ago and now there are many clients data on it. I have updated my above code with the subscribers model. Now I need to pick users from that model. How to do that istead of adding user field to Blogpost model?? – Reactoo Jul 13 '21 at 05:30
  • Hi @Django-Rocks, for me this Q is already answered. If you have new requirements be free to post new Qs :) Regards! – dani herrera Jul 13 '21 at 06:10
  • hey @dani can you help me on this?? https://stackoverflow.com/questions/68426185/function-call-in-another-file-not-working-in-django-rest-framework/68429016#68429016 – Reactoo Jul 18 '21 at 16:29
1

You can override the save method in your model.


class BlogPost(models.Model):
    author = models.CharField(max_length=64, default='Admin')
    .....
    
    def save(self, *args, **kwargs):
         if self.pk:
             # send mail here
              send_mail()
         return super().save(*args, **kwargs)

Or you can write the signals also.


@receiver(post_save, sender=BlogPost)
def send_mail_to_user(sender, instance, created, **kwargs):
    if created:
       # send mail
         send_mail()
arjun
  • 7,230
  • 4
  • 12
  • 29
  • Hey @Arjun can you help me on this?? https://stackoverflow.com/questions/68426185/function-call-in-another-file-not-working-in-django-rest-framework/68429016#68429016 – Reactoo Jul 18 '21 at 16:29
0

Better to use signals. If you need to send it to all subscribers you need to create a loop in signal function.

You can create a signal in blog post view like this:

@receiver(post_save, sender=BlogPost)
def send_mail_to_subs(sender, instance, created, **kwargs):
    if created:
        for subs in instance.author.subscribed.all():
            send_mail(
                f'New Post from {instance.author}',
                f'Title: {instance.post_title}',
                'youremail',
                [subs.email],
            )

Good coding :)

andronova
  • 21
  • 8