import datetime from typing import Any, Dict from django.contrib import messages from django.contrib.auth.decorators import login_required from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.messages.views import SuccessMessageMixin from django.db.models import Count, Q from django.forms import modelform_factory from django.http import Http404, HttpRequest, HttpResponse, JsonResponse from django.shortcuts import get_object_or_404, redirect from django.urls import reverse from django.utils import timezone from django.utils.decorators import method_decorator from django.utils.translation import gettext, gettext_lazy as _ from django.views.generic import ( CreateView, DeleteView, DetailView, FormView, ListView, TemplateView, UpdateView, View, ) from colossus.apps.core.models import Country from colossus.apps.subscribers.constants import ( ActivityTypes, Status, TemplateKeys, Workflows, ) from colossus.apps.subscribers.models import ( Activity, Subscriber, SubscriptionFormTemplate, Tag, ) from colossus.apps.subscribers.subscription_settings import ( SUBSCRIPTION_FORM_TEMPLATE_SETTINGS, ) from colossus.utils import get_absolute_url, is_uuid from .charts import ( ListDomainsChart, ListLocationsChart, SubscriptionsSummaryChart, ) from .forms import ( BulkTagForm, ConfirmSubscriberImportForm, MailingListSMTPForm, PasteImportSubscribersForm, ) from .mixins import FormTemplateMixin, MailingListMixin from .models import MailingList, SubscriberImport @method_decorator(login_required, name='dispatch') class MailingListListView(ListView): model = MailingList context_object_name = 'mailing_lists' ordering = ('name',) paginate_by = 25 def get_context_data(self, **kwargs): kwargs['menu'] = 'lists' kwargs['total_count'] = MailingList.objects.count() return super().get_context_data(**kwargs) @method_decorator(login_required, name='dispatch') class MailingListCreateView(CreateView): model = MailingList fields = ('name', 'slug', 'campaign_default_from_name', 'campaign_default_from_email', 'website_url') def get_context_data(self, **kwargs): kwargs['menu'] = 'lists' return super().get_context_data(**kwargs) @method_decorator(login_required, name='dispatch') class MailingListDetailView(DetailView): model = MailingList context_object_name = 'mailing_list' def get_context_data(self, **kwargs) -> Dict: locations = self.object.get_active_subscribers() \ .select_related('location') \ .values('location__country__code', 'location__country__name') \ .annotate(total=Count('location__country__code')) \ .order_by('-total')[:10] last_campaign = self.object.campaigns.order_by('-send_date').first() domains = self.object.get_active_subscribers() \ .select_related('domain') \ .values('domain__name') \ .annotate(total=Count('domain__name')) \ .order_by('-total')[:10] thirty_days_ago = timezone.now() - datetime.timedelta(30) subscribed_expression = Count('id', filter=Q(activity_type=ActivityTypes.SUBSCRIBED)) unsubscribed_expression = Count('id', filter=Q(activity_type=ActivityTypes.UNSUBSCRIBED)) cleaned_expression = Count('id', filter=Q(activity_type=ActivityTypes.CLEANED)) summary_last_30_days = Activity.objects \ .filter(subscriber__mailing_list=self.object, date__gte=thirty_days_ago) \ .aggregate(subscribed=subscribed_expression, unsubscribed=unsubscribed_expression, cleaned=cleaned_expression) kwargs['menu'] = 'lists' kwargs['submenu'] = 'details' kwargs['subscribed_count'] = self.object.subscribers.filter(status=Status.SUBSCRIBED).count() kwargs['unsubscribed_count'] = self.object.subscribers.filter(status=Status.UNSUBSCRIBED).count() kwargs['cleaned_count'] = self.object.subscribers.filter(status=Status.CLEANED).count() kwargs['locations'] = locations kwargs['last_campaign'] = last_campaign kwargs['summary_last_30_days'] = summary_last_30_days kwargs['domains'] = domains return super().get_context_data(**kwargs) @method_decorator(login_required, name='dispatch') class MailingListCountryReportView(MailingListMixin, DetailView): model = MailingList context_object_name = 'mailing_list' template_name = 'lists/country_report.html' def get_context_data(self, **kwargs): country_code = self.kwargs.get('country_code') country = get_object_or_404(Country, code=country_code) country_total_subscribers = self.object.get_active_subscribers() \ .filter(location__country__code=country_code) \ .values('location__country__code') \ .aggregate(total=Count('location__country__code')) cities = self.object.get_active_subscribers() \ .filter(location__country__code=country_code) \ .select_related('location') \ .values('location__name') \ .annotate(total=Count('location__name')) \ .order_by('-total')[:100] kwargs['menu'] = 'lists' kwargs['country'] = country kwargs['country_total_subscribers'] = country_total_subscribers['total'] kwargs['cities'] = cities return super().get_context_data(**kwargs) @method_decorator(login_required, name='dispatch') class SubscriberListView(MailingListMixin, ListView): model = Subscriber context_object_name = 'subscribers' paginate_by = 100 template_name = 'lists/subscriber_list.html' def get_context_data(self, **kwargs): kwargs['submenu'] = 'subscribers' kwargs['total_count'] = self.model.objects.count() return super().get_context_data(**kwargs) def get_queryset(self): queryset = self.model.objects.filter(mailing_list_id=self.kwargs.get('pk')) tags_filter = self.request.GET.getlist('tags__in') if tags_filter: queryset = queryset.filter(tags__in=tags_filter) if self.request.GET.get('q', ''): query = self.request.GET.get('q').strip() if is_uuid(query): queryset = queryset.filter(uuid=query) else: queryset = queryset.filter(Q(email__icontains=query) | Q(name__icontains=query)) self.extra_context = { 'is_filtered': True, 'query': query } return queryset.order_by('optin_date') @method_decorator(login_required, name='dispatch') class SubscriberCreateView(MailingListMixin, CreateView): model = Subscriber fields = ('email', 'name') template_name = 'lists/subscriber_form.html' def form_valid(self, form): self.object = form.save(commit=False) self.object.mailing_list_id = self.kwargs.get('pk') self.object.status = Status.SUBSCRIBED self.object.save() return redirect('lists:subscribers', pk=self.kwargs.get('pk')) @method_decorator(login_required, name='dispatch') class SubscriberDetailView(MailingListMixin, DetailView): model = Subscriber pk_url_kwarg = 'subscriber_pk' template_name = 'lists/subscriber_detail.html' context_object_name = 'subscriber' @method_decorator(login_required, name='dispatch') class SubscriberUpdateView(MailingListMixin, UpdateView): model = Subscriber fields = '__all__' pk_url_kwarg = 'subscriber_pk' template_name = 'lists/subscriber_form.html' def get_success_url(self): return reverse('lists:subscribers', kwargs={'pk': self.kwargs.get('pk')}) @method_decorator(login_required, name='dispatch') class SubscriberDeleteView(MailingListMixin, DeleteView): model = Subscriber pk_url_kwarg = 'subscriber_pk' context_object_name = 'subscriber' template_name = 'lists/subscriber_confirm_delete.html' def get_success_url(self): return reverse('lists:subscribers', kwargs={'pk': self.kwargs.get('pk')}) @method_decorator(login_required, name='dispatch') class ImportSubscribersView(MailingListMixin, TemplateView): template_name = 'lists/import_subscribers.html' def get_context_data(self, **kwargs): kwargs['submenu'] = 'subscribers' return super().get_context_data(**kwargs) @method_decorator(login_required, name='dispatch') class PasteEmailsImportSubscribersView(MailingListMixin, FormView): template_name = 'lists/import_subscribers_form.html' form_class = PasteImportSubscribersForm extra_context = {'title': _('Paste Emails')} def form_valid(self, form): try: mailing_list_id = self.kwargs.get('pk') mailing_list = MailingList.objects.only('pk').get(pk=mailing_list_id) form.import_subscribers(mailing_list) return redirect('lists:subscribers', pk=mailing_list_id) except MailingList.DoesNotExist: raise Http404 @method_decorator(login_required, name='dispatch') class SubscriptionFormsView(MailingListMixin, TemplateView): template_name = 'lists/subscription_forms.html' def get_context_data(self, **kwargs): kwargs['submenu'] = 'forms' kwargs['sub'] = get_absolute_url('subscribers:subscribe', {'mailing_list_uuid': self.mailing_list.uuid}) kwargs['sub_short'] = get_absolute_url('subscribe_shortcut', {'mailing_list_slug': self.mailing_list.slug}) kwargs['unsub'] = get_absolute_url('subscribers:unsubscribe_manual', { 'mailing_list_uuid': self.mailing_list.uuid }) kwargs['unsub_short'] = get_absolute_url('unsubscribe_shortcut', {'mailing_list_slug': self.mailing_list.slug}) return super().get_context_data(**kwargs) class TagMixin: model = Tag extra_context = {'submenu': 'tags'} pk_url_kwarg = 'tag_pk' def get_queryset(self): return super().get_queryset().filter(mailing_list_id=self.kwargs.get('pk')) @method_decorator(login_required, name='dispatch') class TagListView(TagMixin, MailingListMixin, ListView): context_object_name = 'tags' paginate_by = 100 template_name = 'lists/tag_list.html' def get_queryset(self): queryset = super().get_queryset() if self.request.GET.get('q', ''): query = self.request.GET.get('q').strip() queryset = queryset.filter(Q(name__icontains=query) | Q(description__icontains=query)) self.extra_context = { 'is_filtered': True, 'query': query } queryset = queryset.annotate(subscribers_count=Count('subscribers')) return queryset.order_by('name') class BulkTagSubscribersView(LoginRequiredMixin, TagMixin, MailingListMixin, FormView): form_class = BulkTagForm context_object_name = 'tag' template_name = 'lists/bulk_tag_form.html' def get_form_kwargs(self): kwargs = super().get_form_kwargs() kwargs['mailing_list'] = self.mailing_list return kwargs def form_valid(self, form): form.tag_subscribers() return redirect('lists:tags', pk=self.mailing_list.pk) @method_decorator(login_required, name='dispatch') class TagCreateView(TagMixin, MailingListMixin, CreateView): fields = ('name', 'description') context_object_name = 'tag' template_name = 'lists/tag_form.html' def form_valid(self, form): tag = form.save(commit=False) tag.mailing_list_id = self.kwargs.get('pk') tag.save() messages.success(self.request, _('Tag "%(name)s" created with success.') % form.cleaned_data) return redirect('lists:tags', pk=self.kwargs.get('pk')) @method_decorator(login_required, name='dispatch') class TagUpdateView(SuccessMessageMixin, TagMixin, MailingListMixin, UpdateView): fields = ('name', 'description') context_object_name = 'tag' template_name = 'lists/tag_form.html' success_message = _('Tag "%(name)s" updated with success.') def get_success_url(self): return reverse('lists:tags', kwargs={'pk': self.kwargs.get('pk')}) @method_decorator(login_required, name='dispatch') class TagDeleteView(TagMixin, MailingListMixin, DeleteView): context_object_name = 'tag' template_name = 'lists/tag_confirm_delete.html' def get_success_url(self): return reverse('lists:tags', kwargs={'pk': self.kwargs.get('pk')}) @method_decorator(login_required, name='dispatch') class AbstractSettingsView(UpdateView): model = MailingList context_object_name = 'mailing_list' template_name = 'lists/settings.html' def get_context_data(self, **kwargs): kwargs['menu'] = 'lists' kwargs['submenu'] = 'settings' kwargs['subsubmenu'] = self.subsubmenu kwargs['title'] = self.title return super().get_context_data(**kwargs) def get_success_url(self): return reverse(self.success_url_name, kwargs={'pk': self.kwargs.get('pk')}) class ListSettingsView(AbstractSettingsView): fields = ('name', 'slug', 'website_url', 'contact_email_address',) success_url_name = 'lists:settings' subsubmenu = 'list_settings' title = _('Settings') class SubscriptionSettingsView(AbstractSettingsView): fields = ('list_manager', 'enable_recaptcha', 'recaptcha_site_key', 'recaptcha_secret_key') success_url_name = 'lists:subscription_settings' subsubmenu = 'subscription_settings' title = _('Subscription settings') class CampaignDefaultsView(AbstractSettingsView): fields = ('campaign_default_from_name', 'campaign_default_from_email', 'campaign_default_email_subject',) success_url_name = 'lists:defaults' subsubmenu = 'defaults' title = _('Campaign defaults') class SMTPCredentialsView(AbstractSettingsView): form_class = MailingListSMTPForm success_url_name = 'lists:smtp' subsubmenu = 'smtp' title = _('SMTP credentials') @method_decorator(login_required, name='dispatch') class FormsEditorView(MailingListMixin, TemplateView): template_name = 'lists/forms_editor.html' def get_context_data(self, **kwargs): kwargs['template_keys'] = TemplateKeys kwargs['workflows'] = Workflows kwargs['subscription_forms'] = SUBSCRIPTION_FORM_TEMPLATE_SETTINGS return super().get_context_data(**kwargs) @method_decorator(login_required, name='dispatch') class SubscriptionFormTemplateUpdateView(FormTemplateMixin, MailingListMixin, UpdateView): model = SubscriptionFormTemplate context_object_name = 'form_template' template_name = 'lists/form_template_form.html' def get_success_url(self): return reverse('lists:edit_form_template', kwargs=self.kwargs) def get_context_data(self, **kwargs): kwargs['template_keys'] = TemplateKeys kwargs['workflows'] = Workflows kwargs['subscription_forms'] = SUBSCRIPTION_FORM_TEMPLATE_SETTINGS return super().get_context_data(**kwargs) def get_form_class(self): fields = self.object.settings['fields'] form_class = modelform_factory(self.model, fields=fields) return form_class @method_decorator(login_required, name='dispatch') class ResetFormTemplateView(FormTemplateMixin, MailingListMixin, View): def post(self, request: HttpRequest, pk: int, form_key: str): form_template = self.get_object() form_template.load_defaults() messages.success(request, gettext('Default template restored with success!')) return redirect('lists:edit_form_template', pk=pk, form_key=form_key) @method_decorator(login_required, name='dispatch') class PreviewFormTemplateView(FormTemplateMixin, MailingListMixin, View): def post(self, request, pk, form_key): self.form_template = self.get_object() content = request.POST.get('content_html') html = self.form_template.render_template({'content': content, 'preview': True}) return HttpResponse(html) def get(self, request, pk, form_key): self.form_template = self.get_object() html = self.form_template.render_template({'preview': True}) return HttpResponse(html) @method_decorator(login_required, name='dispatch') class CustomizeDesignView(UpdateView): model = MailingList fields = ('forms_custom_css', 'forms_custom_header') context_object_name = 'mailing_list' template_name = 'lists/customize_design.html' def get_context_data(self, **kwargs): kwargs['menu'] = 'lists' return super().get_context_data(**kwargs) def get_success_url(self): return reverse('lists:forms_editor', kwargs={'pk': self.kwargs.get('pk')}) @method_decorator(login_required, name='dispatch') class SubscriberImportView(MailingListMixin, CreateView): model = SubscriberImport fields = ('file',) template_name = 'lists/import_subscribers_form.html' extra_context = {'title': _('Import CSV File')} def get_context_data(self, **kwargs): kwargs['subscriber_imports'] = SubscriberImport.objects.order_by('-upload_date') return super().get_context_data(**kwargs) def form_valid(self, form): mailing_list_id = self.kwargs.get('pk') subscriber_import = form.save(commit=False) subscriber_import.user = self.request.user subscriber_import.mailing_list_id = mailing_list_id subscriber_import.save() subscriber_import.set_size() return redirect('lists:import_preview', pk=mailing_list_id, import_pk=subscriber_import.pk) @method_decorator(login_required, name='dispatch') class SubscriberImportPreviewView(MailingListMixin, UpdateView): model = SubscriberImport form_class = ConfirmSubscriberImportForm template_name = 'lists/import_preview.html' pk_url_kwarg = 'import_pk' context_object_name = 'subscriber_import' def get_success_url(self): submit = self.request.POST.get('submit', 'save') if submit == 'import': return reverse('lists:import_queued', kwargs=self.kwargs) return reverse('lists:csv_import_subscribers', kwargs={'pk': self.kwargs.get('pk')}) @method_decorator(login_required, name='dispatch') class SubscriberImportQueuedView(MailingListMixin, DetailView): model = SubscriberImport template_name = 'lists/import_queued.html' pk_url_kwarg = 'import_pk' context_object_name = 'subscriber_import' @method_decorator(login_required, name='dispatch') class SubscriberImportDeleteView(MailingListMixin, DeleteView): model = SubscriberImport pk_url_kwarg = 'import_pk' context_object_name = 'subscriber_import' template_name = 'lists/subscriber_import_confirm_delete.html' def get_success_url(self): return reverse('lists:csv_import_subscribers', kwargs={'pk': self.kwargs.get('pk')}) class ChartView(View): chart_class: Any def get(self, request, pk): try: mailing_list = MailingList.objects.get(pk=pk) chart = self.chart_class(mailing_list) return JsonResponse({'chart': chart.get_settings()}) except MailingList.DoesNotExist: # bad request status code return JsonResponse(data={'message': gettext('Invalid mailing list id.')}, status_code=400) @method_decorator(login_required, name='dispatch') class SubscriptionsSummaryChartView(ChartView): chart_class = SubscriptionsSummaryChart @method_decorator(login_required, name='dispatch') class ListDomainsChartView(ChartView): chart_class = ListDomainsChart @method_decorator(login_required, name='dispatch') class ListLocationsChartView(ChartView): chart_class = ListLocationsChart @login_required def download_subscriber_import(request, pk, import_pk): subscriber_import = get_object_or_404(SubscriberImport, pk=import_pk, mailing_list_id=pk) filename = subscriber_import.file.name.split('/')[-1] response = HttpResponse(subscriber_import.file.read(), content_type='text/csv') response['Content-Disposition'] = 'attachment; filename="%s"' % filename return response