jazzband / django-newsletter

An email newsletter application for the Django web application framework, including an extended admin interface, web (un)subscription, dynamic e-mail templates, an archive and HTML email support.
GNU Affero General Public License v3.0
850 stars 206 forks source link

Subscribe to newsletters using ajax requests #263

Open jskitz opened 6 years ago

jskitz commented 6 years ago

I see that there has been some discussion around being able to subscribe to a newsletter via ajax and in multiple places on the site. However, it does not seem like anyone has yet created an ajaxified view such as what is described here in the Django docs. In order to wrap views from outside the project is a bit clunky. It would make sense to have these views be able to handle ajax subscribe. Is there anything on the roadmap to add this feature?

dokterbob commented 6 years ago

Sounds like a great feature. If you're happy to implement it, it's hereby on the roadmap. ^^

Also take a look at https://django-newsletter.readthedocs.io/en/latest/usage.html#embed-a-sign-up-form-within-any-page - perhaps this can be incorporated.

jskitz commented 6 years ago

Yes, I've looked at that in detail and I also am able to ajax submit, ignoring the response, but this is not a robust solution to this. With the ajax submit, the backend still redirects to the splash page, and renders a template in the ajax response. In the current, we would have to parse out the returned html to figure out what happened on the submission, and after the redirection. What I think we want here, is to get back a json success message or error depending on what happens with the request.

I'll see if I can get this working, and will submit a PR if so. I guess another solution would be to wrap the form in a Django Rest Framework view by creating an API endpoint to the newsletter model. This perhaps could be a bit cleaner. Do you have an opinion on how you would solve this in your own project?

Thanks!

jskitz commented 6 years ago

Hi @dokterbob

I've put a bit of work into this, and basically the solution looks like the following in ActionRequestView in views.py

    def no_email_confirm(self, form):
        """
        Subscribe/unsubscribe user and redirect to action activated page.
        """
        self.subscription.update(self.action)

        if self.request.is_ajax():
            return JsonResponse({'msg': 'Your action is confirmed.'})
        else:    
            return redirect(
                self.get_url_from_viewname('newsletter_action_activated')
            )

    def get_success_url(self):
        return self.get_url_from_viewname('newsletter_activation_email_sent')

    def form_invalid(self, form):
        response = super(ActionRequestView, self).form_invalid(form)
        if self.request.is_ajax():
            return JsonResponse(form.errors, status=400)
        else:
            return response

    def form_valid(self, form):
        self.subscription = self.get_subscription(form)

        if not getattr(
                newsletter_settings,
                'CONFIRM_EMAIL_%s' % self.action.upper()
        ):
            # Confirmation email for this action was switched off in settings.
            return self.no_email_confirm(form)

        try:
            self.subscription.send_activation_email(action=self.action)

        except (SMTPException, socket.error) as e:
            logger.exception(
                'Error %s while submitting email to %s.',
                e, self.subscription.email
            )
            self.error = True

            # Although form was valid there was error while sending email,
            # so stay at the same url.
            return self.form_invalid(form)

        response = super(ActionRequestView, self).form_valid(form)
        if self.request.is_ajax():
            return JsonResponse({'msg': 'Thank you for signing up. Please check your email to confirm your email address.'})
        else:
            return response

This is sort of hacked together just to see what's possible. The one thing I'm struggling about, is where we should hold JSON strings? Should we store in templates or in source code? I'm guessing templates would generalize this a bit more and just render to string.

Here is the html:

<form enctype="multipart/form-data" method="post" action="/newsletter/general/subscribe/" class="form" data-newsletter-form>
    {% csrf_token %}
    <div class="input-container_signup">
      <input type="email" class="input-signup" name="email_field" required="" id="id_email_field">
      <label class="label">Email</label>
      <button class="button button-signup" id="id_submit" name="submit" value="Subscribe" type="submit">{% trans "Subscribe" %}</button>
    </div>
</form>

And the javascript using axios.

// Submit newsletter forms via ajax

$(function() {
  $('form[data-newsletter-form]').submit( function(event) {
    event.preventDefault();

    $form = $(this);
    url = $form.attr('action');
    data = getFormData($form.serializeArray());
    const config = {
      headers: {
        'Content-Type': 'multipart/form-data',
        'X-Requested-With': 'XMLHttpRequest'
      }
    };
    console.log(config);
    axios.post(url, data, config)
    .then(function(response) {
      $('input[name=email_field]').val('');
      $form.after('<div class="alert alert-success">' + response.data.msg + '</div>');
    })
    .catch(function(error) {
      $form.append('<div class="alert alert-failure">' + error + '</div>');
    })
  });
});

function getFormData(formArray) {
  //serialize data function
  const fd = new FormData();
  for (var i = 0; i < formArray.length; i++) {
    fd.append(formArray[i]['name'], formArray[i]['value'])
  }
  return fd;
}
dokterbob commented 5 years ago

Quickly skimmed through this. One thing I noticed, which has nothing to do with this, is that newsletter subscription without confirmation, especially in light of very sensible EU regulation, seems like a real antifeature and I would like to suggest to remove it soon.

With regards to strings; note that we currently use the messages framework. Therefore, I suggest we essentially use the same views, except we return a JSON representation of the messages:

{"messages": ["one", "more", "message"]}

It might even make sense to have the different message classes also included, this way it becomes obvious for an AJAX user what type of message they're getting (regardless of return status, although it's important to have that too). There might be some code doing this floating around the web. Example:

{
"success":[],
"failure":[],
"info": []
}

When doing AJAX, make sure to take CSRF into account, it's a bit of a hassle from what I remember.

Also note that there is already a form_invalid(), which might as well be as generic as it could.

Only in case of an actual server error should it return

I suggest you make a start in a branch based on this. Do expect that some of the things might need to be changed - but I can read the code much better if it's properly formatted and/or diff'able against the original.

ekerstein commented 2 years ago

I'm thinking of putting together a PR on this. If I understand correctly, the following would be accepted?

On the ActionRequestView:

  1. Check if ajax header exists (.is_ajax() is deprecated but I can write a separate function to check the request headers)
  2. If ajax, return form errors or response in json format
  3. Messages should be in a list with information about status / type
  4. Provide example of form / javascript for documentation
w3ichen commented 2 years ago

I forked this repo and implemented a django rest framework endpoint for my own purposes. It GET and POST user subscriptions. You're more than welcome to use it at: https://github.com/MecSimCalc/django-newsletter


GET Request:

Gets the subscription of signed in user to all newsletters.

GET: http://localhost:8000/newsletter/my_subscriptions/

Returns:

[
    {
        "user": "585c0a22-0e70-42f5-afa5-05958a99c149",
        "newsletter": {
            "id": 1,
            "title": "Announcements",
            "slug": "announcements"
        },
        "subscribed": false
    },
    {
        "user": "585c0a22-0e70-42f5-afa5-05958a99c149",
        "newsletter": {
            "id": 2,
            "title": "Blog",
            "slug": "blog"
        },
        "subscribed": false
    },
]

POST Request:

Sets subscription to newsletters for signed in user.

POST: http://localhost:8000/newsletter/my_subscriptions/

Request body:

[
  {
    "newsletter": 2,
    "subscribed": true
  },
  {
    "newsletter": 3,
    "subscribed": true
  }
]