diff --git a/newsletter/admin.py b/newsletter/admin.py index dcdc7190..4ae48fa1 100644 --- a/newsletter/admin.py +++ b/newsletter/admin.py @@ -25,12 +25,16 @@ from sorl.thumbnail.admin import AdminImageMixin from .models import ( - Newsletter, Subscription, Article, Message, Submission + Newsletter, Subscription, Article, Message, Submission, Blacklist ) from django.utils.timezone import now +from copy import deepcopy +from random import randint + from .admin_forms import * +from .admin_blacklist_forms import * from .admin_utils import * from .settings import newsletter_settings @@ -218,6 +222,8 @@ class MessageAdmin(admin.ModelAdmin, ExtendibleModelAdminMixin): inlines = [ArticleInline, ] + actions = ['duplicate_message'] + """ List extensions """ def admin_title(self, obj): return '%s' % (obj.id, obj.title) @@ -244,6 +250,29 @@ def preview(self, request, object_id): RequestContext(request, {}), ) + """ Actions """ + def duplicate_message(modeladmin, request, queryset): + title_suffix = " (Copy)" + slug_suffix = "-" + str(randint(1000, 99999)) + + original_message_id = None + new_message_id = None + for message in queryset: + new_message = deepcopy(message) + original_message_id = new_message.id + new_message.id = None + new_message.slug = new_message.slug + slug_suffix + new_message.title = new_message.title + title_suffix + new_message.save() + new_message_id = new_message.id + # Duplicate the articles as well + for article in Article.objects.filter(post_id=original_message_id): + new_article = deepcopy(article) + new_article.id = None + new_article.post_id = new_message_id + new_article.save() + duplicate_message.short_description = "Duplicate selected messages" + @xframe_options_sameorigin def preview_html(self, request, object_id): message = self._getobj(request, object_id) @@ -432,7 +461,7 @@ def subscribers_import(self, request): def subscribers_import_confirm(self, request): # If no addresses are in the session, start all over. - if not 'addresses' in request.session: + if 'addresses' not in request.session: return HttpResponseRedirect('../') addresses = request.session['addresses'] @@ -486,7 +515,126 @@ def get_urls(self): return my_urls + urls +class BlacklistAdmin(admin.ModelAdmin, ExtendibleModelAdminMixin): + form = BlacklistAdminForm + list_display = ('name', 'email', 'newsletter_name') + list_display_links = ('name', 'email') + list_filter = () + search_fields = ( + 'name_field', 'email_field', 'user__first_name', 'user__last_name', + 'user__email' + ) + readonly_fields = () + date_hierarchy = '' + actions = [] + + """ List extensions """ + def newsletter_name(self, obj): + if obj.newsletter is None: + return "Global Blacklist" + else: + return obj.newsletter + + """ Views """ + def blacklist_import(self, request): + if request.POST: + form = BlacklistImportForm(request.POST, request.FILES) + form.importing_blacklist = True + if form.is_valid(): + request.session['addresses'] = form.get_addresses() + if form.get_newsletter(): + request.session['newsletter'] = form.get_newsletter() + else: + request.session['newsletter'] = 0 + return HttpResponseRedirect('confirm/') + else: + form = ImportForm() + + return render_to_response( + "admin/newsletter/blacklist/importform.html", + {'form': form}, + RequestContext(request, {}), + ) + + def blacklist_import_confirm(self, request): + # If no addresses are in the session, start all over. + if 'addresses' not in request.session: + return HttpResponseRedirect('../') + + addresses = request.session['addresses'] + newsletter = request.session['newsletter'] + logger.debug('Confirming addresses: %s', addresses) + if request.POST: + form = ConfirmForm(request.POST) + if form.is_valid(): + try: + for email_address in addresses.keys(): + new_blacklist = Blacklist( + email_field=email_address, + name=addresses[email_address]) + # 0 means no newsletter is selected, + # which means this user is going into the + # Global Blacklist + if newsletter != 0: + new_blacklist.newsletter_id = newsletter + new_blacklist.save() + finally: + del request.session['addresses'] + del request.session['newsletter'] + + messages.success( + request, + _('%s blacklist have been successfully added.') % + len(addresses) + ) + + return HttpResponseRedirect('../../') + else: + form = ConfirmForm() + + newsletter_name = _("the Global Blacklist") + if newsletter != 0: + try: + newsletter_name = Newsletter.objects.get(pk=newsletter).title + except Newsletter.DoesNotExist: + pass + + return render_to_response( + "admin/newsletter/blacklist/confirmimportform.html", + { + 'form': form, + 'blacklist_people': addresses, + 'newsletter_name': newsletter_name + }, + RequestContext(request, {}), + ) + + """ URLs """ + def get_urls(self): + urls = super(BlacklistAdmin, self).get_urls() + + my_urls = patterns( + '', + url(r'^import/$', + self._wrap(self.blacklist_import), + name=self._view_name('import')), + url(r'^import/confirm/$', + self._wrap(self.blacklist_import_confirm), + name=self._view_name('import_confirm')), + + # Translated JS strings - these should be app-wide but are + # only used in this part of the admin. For now, leave them here. + url(r'^jsi18n/$', + 'django.views.i18n.javascript_catalog', + {'packages': ('newsletter',)}, + name='newsletter_js18n') + ) + + return my_urls + urls + + admin.site.register(Newsletter, NewsletterAdmin) admin.site.register(Submission, SubmissionAdmin) admin.site.register(Message, MessageAdmin) admin.site.register(Subscription, SubscriptionAdmin) +admin.site.register(Blacklist, BlacklistAdmin) diff --git a/newsletter/admin_blacklist_forms.py b/newsletter/admin_blacklist_forms.py new file mode 100644 index 00000000..b783812e --- /dev/null +++ b/newsletter/admin_blacklist_forms.py @@ -0,0 +1,286 @@ +import logging + +logger = logging.getLogger(__name__) + +from django import forms + +from django.core.exceptions import ValidationError + +from django.core.validators import validate_email + +from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import ugettext + +from .models import Newsletter, Blacklist +from .admin_forms import check_name, check_email + + +def check_if_email_is_already_blacklisted(newsletter, email): + # returns the email if the user isn't already blacklisted. + if newsletter: + qs = Blacklist.objects.filter( + newsletter_id=newsletter.id, + email_field__exact=email) + else: + qs = Blacklist.objects.filter( + email_field__exact=email) + + if qs.count(): + return None + + return email + + +def blacklist_parse_csv(myfile, newsletter, ignore_errors=False): + from newsletter.addressimport.csv_util import UnicodeReader + import codecs + import csv + + # Detect encoding + from chardet.universaldetector import UniversalDetector + + detector = UniversalDetector() + + for line in myfile.readlines(): + detector.feed(line) + if detector.done: + break + + detector.close() + charset = detector.result['encoding'] + + # Reset the file index + myfile.seek(0) + + # Attempt to detect the dialect + encodedfile = codecs.EncodedFile(myfile, charset) + dialect = csv.Sniffer().sniff(encodedfile.read(1024)) + + # Reset the file index + myfile.seek(0) + + logger.info('Detected encoding %s and dialect %s for CSV file', + charset, dialect) + + myreader = UnicodeReader(myfile, dialect=dialect, encoding=charset) + + firstrow = myreader.next() + + # Find name column + colnum = 0 + namecol = None + for column in firstrow: + if "name" in column.lower() or ugettext("name") in column.lower(): + namecol = colnum + + if "display" in column.lower() or \ + ugettext("display") in column.lower(): + break + + colnum += 1 + + if namecol is None: + raise forms.ValidationError(_( + "Name column not found. The name of this column should be " + "either 'name' or '%s'.") % ugettext("name") + ) + + logger.debug("Name column found: '%s'", firstrow[namecol]) + + # Find email column + colnum = 0 + mailcol = None + for column in firstrow: + if 'email' in column.lower() or \ + 'e-mail' in column.lower() or \ + ugettext("e-mail") in column.lower(): + + mailcol = colnum + + break + + colnum += 1 + + if mailcol is None: + raise forms.ValidationError(_( + "E-mail column not found. The name of this column should be " + "either 'email', 'e-mail' or '%(email)s'.") % + {'email': ugettext("e-mail")} + ) + + logger.debug("E-mail column found: '%s'", firstrow[mailcol]) + + if namecol == mailcol: + raise forms.ValidationError( + _( + "Could not properly determine the proper columns in the " + "CSV-file. There should be a field called 'name' or " + "'%(name)s' and one called 'e-mail' or '%(e-mail)s'." + ) % { + "name": _("name"), + "e-mail": _("e-mail") + } + ) + + logger.debug('Extracting data.') + + addresses = {} + for row in myreader: + if not max(namecol, mailcol) < len(row): + logger.warn("Column count does not match for row number %d", + myreader.line_num, extra=dict(data={'row': row})) + + if ignore_errors: + # Skip this record + continue + else: + raise forms.ValidationError(_( + "Row with content '%(row)s' does not contain a name and " + "email field.") % {'row': row} + ) + + name = check_name(row[namecol], ignore_errors) + email = check_email(row[mailcol], ignore_errors) + + logger.debug("Going to add %s <%s>", name, email) + + try: + validate_email(email) + addr = check_if_email_is_already_blacklisted(newsletter, email) + except ValidationError: + if ignore_errors: + logger.warn( + "Entry '%s' at line %d does not contain a valid " + "e-mail address.", + name, myreader.line_num, extra=dict(data={'row': row})) + else: + raise forms.ValidationError(_( + "Entry '%s' does not contain a valid " + "e-mail address.") % name + ) + + if addr: + if email in addresses: + logger.warn( + "Entry '%s' at line %d contains a " + "duplicate entry for '%s'", + name, myreader.line_num, email, + extra=dict(data={'row': row})) + + if not ignore_errors: + raise forms.ValidationError(_( + "The address file contains duplicate entries " + "for '%s'.") % email) + + addresses.update({addr: name}) + else: + logger.warn( + "Entry '%s' at line %d is already subscribed to " + "with email '%s'", + name, myreader.line_num, email, extra=dict(data={'row': row})) + + if not ignore_errors: + raise forms.ValidationError( + _("Some entries are already subscribed to.")) + + return addresses + + +class BlacklistImportForm(forms.Form): + + def clean(self): + # If there are validation errors earlier on, don't bother. + if not ('address_file' in self.cleaned_data and + 'ignore_errors' in self.cleaned_data): + return self.cleaned_data + + ignore_errors = self.cleaned_data['ignore_errors'] + newsletter = self.cleaned_data['newsletter'] + + myfield = self.base_fields['address_file'] + myvalue = myfield.widget.value_from_datadict( + self.data, self.files, self.add_prefix('address_file')) + + content_type = myvalue.content_type + allowed_types = ('text/plain', 'application/octet-stream', + 'text/vcard', 'text/directory', 'text/x-vcard', + 'application/vnd.ms-excel', + 'text/comma-separated-values', 'text/csv', + 'application/csv', 'application/excel', + 'application/vnd.msexcel', 'text/anytext') + if content_type not in allowed_types: + raise forms.ValidationError(_( + "File type '%s' was not recognized.") % content_type) + + self.addresses = [] + + ext = myvalue.name.rsplit('.', 1)[-1].lower() + if ext == 'csv': + self.addresses = blacklist_parse_csv( + myvalue.file, newsletter, ignore_errors) + + else: + raise forms.ValidationError( + _("File extention '%s' was not recognized.") % ext) + + if len(self.addresses) == 0: + raise forms.ValidationError( + _("No entries could found in this file.")) + + return self.cleaned_data + + def get_addresses(self): + if hasattr(self, 'addresses'): + logger.debug('Getting addresses: %s', self.addresses) + return self.addresses + else: + return {} + + def get_newsletter(self): + if self.cleaned_data['newsletter']: + return self.cleaned_data['newsletter'].id + else: + return None + + newsletter = forms.ModelChoiceField( + label=_("Newsletter"), + queryset=Newsletter.objects.all(), + initial=Newsletter.get_default_id(), + required=False) + address_file = forms.FileField(label=_("Address file")) + ignore_errors = forms.BooleanField( + label=_("Ignore non-fatal errors"), + initial=False, required=False) + + +class BlacklistAdminForm(forms.ModelForm): + + class Meta: + model = Blacklist + + def clean_email_field(self): + data = self.cleaned_data['email_field'] + if self.cleaned_data['user'] and data: + raise forms.ValidationError(_( + 'If a user has been selected this field ' + 'should remain empty.')) + return data + + def clean_name_field(self): + data = self.cleaned_data['name_field'] + if self.cleaned_data['user'] and data: + raise forms.ValidationError(_( + 'If a user has been selected ' + 'this field should remain empty.')) + return data + + def clean(self): + cleaned_data = super(BlacklistAdminForm, self).clean() + if not (cleaned_data.get('user', None) or + cleaned_data.get('email_field', None)): + + raise forms.ValidationError(_( + 'Either a user must be selected or an email address must ' + 'be specified.') + ) + return cleaned_data diff --git a/newsletter/admin_forms.py b/newsletter/admin_forms.py index be9f0087..67ebe1bd 100644 --- a/newsletter/admin_forms.py +++ b/newsletter/admin_forms.py @@ -13,7 +13,7 @@ from django.conf import settings -from .models import Subscription, Newsletter, Submission +from .models import Subscription, Newsletter, Submission, Blacklist def make_subscription(newsletter, email, name=None): @@ -156,8 +156,6 @@ def parse_csv(myfile, newsletter, ignore_errors=False): logger.debug("E-mail column found: '%s'", firstrow[mailcol]) - #assert namecol != mailcol, \ - # 'Name and e-mail column should not be the same.' if namecol == mailcol: raise forms.ValidationError( _( @@ -338,6 +336,7 @@ def handle(self, dn, entry): class ImportForm(forms.Form): + importing_blacklist = False def clean(self): # If there are validation errors earlier on, don't bother. @@ -345,8 +344,6 @@ def clean(self): 'ignore_errors' in self.cleaned_data and 'newsletter' in self.cleaned_data): return self.cleaned_data - # TESTME: Should an error be raised here or not? - #raise forms.ValidationError(_("No file has been specified.")) ignore_errors = self.cleaned_data['ignore_errors'] newsletter = self.cleaned_data['newsletter'] @@ -401,7 +398,7 @@ def get_addresses(self): newsletter = forms.ModelChoiceField( label=_("Newsletter"), queryset=Newsletter.objects.all(), - initial=Newsletter.get_default_id()) + initial=Newsletter.get_default_id(),) address_file = forms.FileField(label=_("Address file")) ignore_errors = forms.BooleanField( label=_("Ignore non-fatal errors"), diff --git a/newsletter/migrations/0007_auto__add_blacklist.py b/newsletter/migrations/0007_auto__add_blacklist.py new file mode 100644 index 00000000..055ff92d --- /dev/null +++ b/newsletter/migrations/0007_auto__add_blacklist.py @@ -0,0 +1,137 @@ +# -*- coding: utf-8 -*- +from south.utils import datetime_utils as datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding model 'Blacklist' + db.create_table(u'newsletter_blacklist', ( + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'], null=True, blank=True)), + ('name_field', self.gf('django.db.models.fields.CharField')(max_length=30, null=True, db_column='name', blank=True)), + ('email_field', self.gf('django.db.models.fields.EmailField')(db_index=True, max_length=75, null=True, db_column='email', blank=True)), + ('newsletter', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['newsletter.Newsletter'], null=True, blank=True)), + )) + db.send_create_signal(u'newsletter', ['Blacklist']) + + + def backwards(self, orm): + # Deleting model 'Blacklist' + db.delete_table(u'newsletter_blacklist') + + + models = { + u'auth.group': { + 'Meta': {'object_name': 'Group'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + u'auth.permission': { + 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + u'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + u'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + u'newsletter.article': { + 'Meta': {'ordering': "('sortorder',)", 'object_name': 'Article'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'image': (u'sorl.thumbnail.fields.ImageField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), + 'post': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'articles'", 'to': u"orm['newsletter.Message']"}), + 'sortorder': ('django.db.models.fields.PositiveIntegerField', [], {'default': '10', 'db_index': 'True'}), + 'text': ('django.db.models.fields.TextField', [], {}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '200'}), + 'url': ('django.db.models.fields.URLField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}) + }, + u'newsletter.blacklist': { + 'Meta': {'object_name': 'Blacklist'}, + 'email_field': ('django.db.models.fields.EmailField', [], {'db_index': 'True', 'max_length': '75', 'null': 'True', 'db_column': "'email'", 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name_field': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True', 'db_column': "'name'", 'blank': 'True'}), + 'newsletter': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['newsletter.Newsletter']", 'null': 'True', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'}) + }, + u'newsletter.message': { + 'Meta': {'unique_together': "(('slug', 'newsletter'),)", 'object_name': 'Message'}, + 'date_create': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'date_modify': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'newsletter': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['newsletter.Newsletter']"}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '50'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '200'}) + }, + u'newsletter.newsletter': { + 'Meta': {'object_name': 'Newsletter'}, + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'send_html': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'sender': ('django.db.models.fields.CharField', [], {'max_length': '200'}), + 'site': ('django.db.models.fields.related.ManyToManyField', [], {'default': '[1]', 'to': u"orm['sites.Site']", 'symmetrical': 'False'}), + 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '50'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '200'}), + 'visible': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'db_index': 'True'}) + }, + u'newsletter.submission': { + 'Meta': {'object_name': 'Submission'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'message': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['newsletter.Message']"}), + 'newsletter': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['newsletter.Newsletter']"}), + 'prepared': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'publish': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'db_index': 'True'}), + 'publish_date': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2014, 7, 15, 0, 0)', 'null': 'True', 'db_index': 'True', 'blank': 'True'}), + 'sending': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'sent': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'subscriptions': ('django.db.models.fields.related.ManyToManyField', [], {'db_index': 'True', 'to': u"orm['newsletter.Subscription']", 'symmetrical': 'False', 'blank': 'True'}) + }, + u'newsletter.subscription': { + 'Meta': {'unique_together': "(('user', 'email_field', 'newsletter'),)", 'object_name': 'Subscription'}, + 'activation_code': ('django.db.models.fields.CharField', [], {'default': "'1fa74676cfda1958d3eb4b54324c9df94bb1e5a0'", 'max_length': '40'}), + 'create_date': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email_field': ('django.db.models.fields.EmailField', [], {'db_index': 'True', 'max_length': '75', 'null': 'True', 'db_column': "'email'", 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ip': ('django.db.models.fields.IPAddressField', [], {'max_length': '15', 'null': 'True', 'blank': 'True'}), + 'name_field': ('django.db.models.fields.CharField', [], {'max_length': '30', 'null': 'True', 'db_column': "'name'", 'blank': 'True'}), + 'newsletter': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['newsletter.Newsletter']"}), + 'subscribe_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'subscribed': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'unsubscribe_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'unsubscribed': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'}) + }, + u'sites.site': { + 'Meta': {'ordering': "(u'domain',)", 'object_name': 'Site', 'db_table': "u'django_site'"}, + 'domain': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + } + } + + complete_apps = ['newsletter'] \ No newline at end of file diff --git a/newsletter/models.py b/newsletter/models.py index 7a90b515..fd1d1be6 100644 --- a/newsletter/models.py +++ b/newsletter/models.py @@ -13,6 +13,8 @@ from django.core.mail import EmailMultiAlternatives +from django.db.models import Q + from django.contrib.sites.models import Site from django.contrib.sites.managers import CurrentSiteManager @@ -155,6 +157,55 @@ def get_default_id(cls): return None +class Blacklist(models.Model): + user = models.ForeignKey( + User, blank=True, null=True, verbose_name=_('user') + ) + + name_field = models.CharField( + db_column='name', max_length=30, blank=True, null=True, + verbose_name=_('name'), help_text=_('optional') + ) + + def get_name(self): + if self.user: + return self.user.get_full_name() + elif self.name_field: + return self.name_field + else: + return "" + + def set_name(self, name): + if not self.user: + self.name_field = name + name = property(get_name, set_name) + + email_field = models.EmailField( + db_column='email', verbose_name=_('e-mail'), db_index=True, + blank=True, null=True + ) + + def get_email(self): + if self.user: + return self.user.email + return self.email_field + + def set_email(self, email): + if not self.user: + self.email_field = email + email = property(get_email, set_email) + + newsletter = models.ForeignKey( + 'Newsletter', + blank=True, + null=True, + verbose_name=_('newsletter') + ) + + def __unicode__(self): + return _(u"%(email)s") % {'email': self.email} + + class Subscription(models.Model): user = models.ForeignKey( User, blank=True, null=True, verbose_name=_('user') @@ -538,7 +589,9 @@ def __unicode__(self): } def submit(self): - subscriptions = self.subscriptions.filter(subscribed=True) + blacklist_global_email_field = Blacklist.objects.filter(Q(newsletter_id__isnull = True) | Q(newsletter_id = self.newsletter.id)).values('email_field').distinct() + blacklist_list = [blacklist['email_field'] for blacklist in blacklist_global_email_field] + subscriptions = self.subscriptions.filter(subscribed=True).exclude(email_field__in = blacklist_list) logger.info( ugettext(u"Submitting %(submission)s to %(count)d people"), diff --git a/newsletter/templates/admin/newsletter/blacklist/change_list.html b/newsletter/templates/admin/newsletter/blacklist/change_list.html new file mode 100644 index 00000000..a8238c02 --- /dev/null +++ b/newsletter/templates/admin/newsletter/blacklist/change_list.html @@ -0,0 +1,17 @@ +{% extends "admin/change_list.html" %} +{% load i18n %} + +{% block object-tools %} + {% if has_add_permission %} + + {% endif %} +{% endblock %} diff --git a/newsletter/templates/admin/newsletter/blacklist/confirmimportform.html b/newsletter/templates/admin/newsletter/blacklist/confirmimportform.html new file mode 100644 index 00000000..4840b8da --- /dev/null +++ b/newsletter/templates/admin/newsletter/blacklist/confirmimportform.html @@ -0,0 +1,46 @@ +{% extends "admin/base_site.html" %} +{% load i18n %} +{% block title %}{% trans "Import addresses" %}{{ block.super }}{% endblock %} + +{% block breadcrumbs %} + +{% endblock %} + +{% block content %} +

{% trans "The following blacklist line(s) will be added to" %} {{ newsletter_name }}

+
+ +
+ + {{ form.as_table }} +
+ {% csrf_token %} + +
+
+
+
+ +{% endblock %} diff --git a/newsletter/templates/admin/newsletter/blacklist/importform.html b/newsletter/templates/admin/newsletter/blacklist/importform.html new file mode 100644 index 00000000..a7a9a6e8 --- /dev/null +++ b/newsletter/templates/admin/newsletter/blacklist/importform.html @@ -0,0 +1,43 @@ +{% extends "admin/base_site.html" %} +{% load i18n %} +{% block title %}{% trans "Import addresses" %}{{ block.super }}{% endblock %} + +{% block breadcrumbs %} + +{% endblock %} + +{% block content %} +

{% trans "Import addresses" %}

+
+ +
+ + {{ form.as_table }} +
+ {% csrf_token %} + +
+
+
+
+ +{% endblock %}