diff --git a/credits/models.py b/credits/models.py index dd847f6..3a881b6 100644 --- a/credits/models.py +++ b/credits/models.py @@ -3,6 +3,7 @@ class Profile(models.Model): + """ For testing, track the number of "credits". """ diff --git a/docs/conf.py b/docs/conf.py index 1541dad..6c89a82 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -11,7 +11,8 @@ # All configuration values have a default; values that are commented out # serve to show the default. -import sys, os +import sys +import os sys.path.insert(0, os.path.abspath('..')) version = __import__('drip').__version__ @@ -21,7 +22,7 @@ # documentation root, use os.path.abspath to make it absolute, like shown here. #sys.path.insert(0, os.path.abspath('.')) -# -- General configuration ----------------------------------------------------- +# -- General configuration ----------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' @@ -90,7 +91,7 @@ #modindex_common_prefix = [] -# -- Options for HTML output --------------------------------------------------- +# -- Options for HTML output --------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. @@ -170,24 +171,24 @@ htmlhelp_basename = 'DjangoDripdoc' -# -- Options for LaTeX output -------------------------------------------------- +# -- Options for LaTeX output -------------------------------------------- latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', + # The paper size ('letterpaper' or 'a4paper'). + #'papersize': 'letterpaper', -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', + # The font size ('10pt', '11pt' or '12pt'). + #'pointsize': '10pt', -# Additional stuff for the LaTeX preamble. -#'preamble': '', + # Additional stuff for the LaTeX preamble. + #'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ - ('index', 'DjangoDrip.tex', u'Django Drip Documentation', - u'Bryan Helmig', 'manual'), + ('index', 'DjangoDrip.tex', u'Django Drip Documentation', + u'Bryan Helmig', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of @@ -211,7 +212,7 @@ #latex_domain_indices = True -# -- Options for manual page output -------------------------------------------- +# -- Options for manual page output -------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). @@ -224,15 +225,15 @@ #man_show_urls = False -# -- Options for Texinfo output ------------------------------------------------ +# -- Options for Texinfo output ------------------------------------------ # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - ('index', 'DjangoDrip', u'Django Drip Documentation', - u'Bryan Helmig', 'DjangoDrip', 'One line description of project.', - 'Miscellaneous'), + ('index', 'DjangoDrip', u'Django Drip Documentation', + u'Bryan Helmig', 'DjangoDrip', 'One line description of project.', + 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. diff --git a/drip/admin.py b/drip/admin.py index 9c1d5a1..363f457 100644 --- a/drip/admin.py +++ b/drip/admin.py @@ -15,8 +15,10 @@ class QuerySetRuleInline(admin.TabularInline): class DripForm(forms.ModelForm): message_class = forms.ChoiceField( - choices=((k, '%s (%s)' % (k, v)) for k, v in configured_message_classes().items()) + choices=((k, '%s (%s)' % (k, v)) + for k, v in configured_message_classes().items()) ) + class Meta: model = Drip exclude = [] @@ -30,6 +32,7 @@ class DripAdmin(admin.ModelAdmin): form = DripForm av = lambda self, view: self.admin_site.admin_view(view) + def timeline(self, request, drip_id, into_past, into_future): """ Return a list of people who should get emails. @@ -40,17 +43,22 @@ def timeline(self, request, drip_id, into_past, into_future): shifted_drips = [] seen_users = set() - for shifted_drip in drip.drip.walk(into_past=int(into_past), into_future=int(into_future)+1): + for shifted_drip in drip.drip.walk( + into_past=int(into_past), into_future=int(into_future) + 1): shifted_drip.prune() shifted_drips.append({ 'drip': shifted_drip, 'qs': shifted_drip.get_queryset().exclude(id__in=seen_users) }) - seen_users.update(shifted_drip.get_queryset().values_list('id', flat=True)) + seen_users.update( + shifted_drip.get_queryset().values_list( + 'id', + flat=True)) return render(request, 'drip/timeline.html', locals()) - def view_drip_email(self, request, drip_id, into_past, into_future, user_id): + def view_drip_email( + self, request, drip_id, into_past, into_future, user_id): from django.shortcuts import render, get_object_or_404 from django.http import HttpResponse drip = get_object_or_404(Drip, id=drip_id) @@ -90,17 +98,17 @@ def get_urls(self): from django.conf.urls import patterns, url urls = super(DripAdmin, self).get_urls() my_urls = patterns('', - url( - r'^(?P[\d]+)/timeline/(?P[\d]+)/(?P[\d]+)/$', - self.av(self.timeline), - name='drip_timeline' - ), - url( - r'^(?P[\d]+)/timeline/(?P[\d]+)/(?P[\d]+)/(?P[\d]+)/$', - self.av(self.view_drip_email), - name='view_drip_email' - ) - ) + url( + r'^(?P[\d]+)/timeline/(?P[\d]+)/(?P[\d]+)/$', + self.av(self.timeline), + name='drip_timeline' + ), + url( + r'^(?P[\d]+)/timeline/(?P[\d]+)/(?P[\d]+)/(?P[\d]+)/$', + self.av(self.view_drip_email), + name='view_drip_email' + ) + ) return my_urls + urls admin.site.register(Drip, DripAdmin) diff --git a/drip/drips.py b/drip/drips.py index 9a6650d..d184e9e 100644 --- a/drip/drips.py +++ b/drip/drips.py @@ -64,13 +64,17 @@ def context(self): @property def subject(self): if not self._subject: - self._subject = Template(self.drip_base.subject_template).render(self.context) + self._subject = Template( + self.drip_base.subject_template).render( + self.context) return self._subject @property def body(self): if not self._body: - self._body = Template(self.drip_base.body_template).render(self.context) + self._body = Template( + self.drip_base.body_template).render( + self.context) return self._body @property @@ -83,7 +87,8 @@ def plain(self): def message(self): if not self._message: if self.drip_base.from_email_name: - from_ = "%s <%s>" % (self.drip_base.from_email_name, self.drip_base.from_email) + from_ = "%s <%s>" % ( + self.drip_base.from_email_name, self.drip_base.from_email) else: from_ = self.drip_base.from_email @@ -97,6 +102,7 @@ def message(self): class DripBase(object): + """ A base object for defining a Drip. @@ -115,8 +121,12 @@ def __init__(self, drip_model, *args, **kwargs): self.name = kwargs.pop('name', self.name) self.from_email = kwargs.pop('from_email', self.from_email) - self.from_email_name = kwargs.pop('from_email_name', self.from_email_name) - self.subject_template = kwargs.pop('subject_template', self.subject_template) + self.from_email_name = kwargs.pop( + 'from_email_name', + self.from_email_name) + self.subject_template = kwargs.pop( + 'subject_template', + self.subject_template) self.body_template = kwargs.pop('body_template', self.body_template) if not self.name: @@ -124,7 +134,6 @@ def __init__(self, drip_model, *args, **kwargs): self.now_shift_kwargs = kwargs.get('now_shift_kwargs', {}) - ######################### ### DATE MANIPULATION ### ######################### @@ -211,7 +220,7 @@ def prune(self): exclude_user_ids = SentDrip.objects.filter(date__lt=conditional_now(), drip=self.drip_model, user__id__in=target_user_ids)\ - .values_list('user_id', flat=True) + .values_list('user_id', flat=True) self._queryset = self.get_queryset().exclude(id__in=exclude_user_ids) def send(self): @@ -224,7 +233,10 @@ def send(self): """ if not self.from_email: - self.from_email = getattr(settings, 'DRIP_FROM_EMAIL', settings.DEFAULT_FROM_EMAIL) + self.from_email = getattr( + settings, + 'DRIP_FROM_EMAIL', + settings.DEFAULT_FROM_EMAIL) MessageClass = message_class_for(self.drip_model.message_class) count = 0 @@ -243,11 +255,12 @@ def send(self): ) count += 1 except Exception as e: - logging.error("Failed to send drip %s to user %s: %s" % (self.drip_model.id, user, e)) + logging.error( + "Failed to send drip %s to user %s: %s" % + (self.drip_model.id, user, e)) return count - #################### ### USER DEFINED ### #################### diff --git a/drip/management/commands/send_drips.py b/drip/management/commands/send_drips.py index 5459b41..355a6a8 100644 --- a/drip/management/commands/send_drips.py +++ b/drip/management/commands/send_drips.py @@ -2,6 +2,7 @@ class Command(BaseCommand): + def handle(self, *args, **options): from drip.models import Drip diff --git a/drip/migrations/0001_initial.py b/drip/migrations/0001_initial.py index 914041f..b8738da 100644 --- a/drip/migrations/0001_initial.py +++ b/drip/migrations/0001_initial.py @@ -10,22 +10,33 @@ class Migration(SchemaMigration): def forwards(self, orm): # Adding model 'Drip' db.create_table('drip_drip', ( - ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - ('date', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)), - ('lastchanged', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, blank=True)), - ('name', self.gf('django.db.models.fields.CharField')(unique=True, max_length=255)), - ('enabled', self.gf('django.db.models.fields.BooleanField')(default=False)), - ('subject_template', self.gf('django.db.models.fields.TextField')(null=True, blank=True)), - ('body_html_template', self.gf('django.db.models.fields.TextField')(null=True, blank=True)), + ('id', self.gf('django.db.models.fields.AutoField') + (primary_key=True)), + ('date', self.gf('django.db.models.fields.DateTimeField') + (auto_now_add=True, blank=True)), + ('lastchanged', self.gf('django.db.models.fields.DateTimeField') + (auto_now=True, blank=True)), + ('name', self.gf('django.db.models.fields.CharField') + (unique=True, max_length=255)), + ('enabled', + self.gf('django.db.models.fields.BooleanField')(default=False)), + ('subject_template', self.gf('django.db.models.fields.TextField') + (null=True, blank=True)), + ('body_html_template', self.gf( + 'django.db.models.fields.TextField')(null=True, blank=True)), )) db.send_create_signal('drip', ['Drip']) # Adding model 'SentDrip' db.create_table('drip_sentdrip', ( - ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - ('date', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)), - ('drip', self.gf('django.db.models.fields.related.ForeignKey')(related_name='sent_drips', to=orm['drip.Drip'])), - ('user', self.gf('django.db.models.fields.related.ForeignKey')(related_name='sent_drips', to=orm['auth.User'])), + ('id', self.gf('django.db.models.fields.AutoField') + (primary_key=True)), + ('date', self.gf('django.db.models.fields.DateTimeField') + (auto_now_add=True, blank=True)), + ('drip', self.gf('django.db.models.fields.related.ForeignKey') + (related_name='sent_drips', to=orm['drip.Drip'])), + ('user', self.gf('django.db.models.fields.related.ForeignKey') + (related_name='sent_drips', to=orm['auth.User'])), ('subject', self.gf('django.db.models.fields.TextField')()), ('body', self.gf('django.db.models.fields.TextField')()), )) @@ -33,18 +44,25 @@ def forwards(self, orm): # Adding model 'QuerySetRule' db.create_table('drip_querysetrule', ( - ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - ('date', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)), - ('lastchanged', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, blank=True)), - ('drip', self.gf('django.db.models.fields.related.ForeignKey')(related_name='queryset_rules', to=orm['drip.Drip'])), - ('method_type', self.gf('django.db.models.fields.CharField')(default='filter', max_length=12)), - ('field_name', self.gf('django.db.models.fields.CharField')(max_length=128)), - ('lookup_type', self.gf('django.db.models.fields.CharField')(default='exact', max_length=12)), - ('field_value', self.gf('django.db.models.fields.CharField')(max_length=255)), + ('id', self.gf('django.db.models.fields.AutoField') + (primary_key=True)), + ('date', self.gf('django.db.models.fields.DateTimeField') + (auto_now_add=True, blank=True)), + ('lastchanged', self.gf('django.db.models.fields.DateTimeField') + (auto_now=True, blank=True)), + ('drip', self.gf('django.db.models.fields.related.ForeignKey') + (related_name='queryset_rules', to=orm['drip.Drip'])), + ('method_type', self.gf('django.db.models.fields.CharField') + (default='filter', max_length=12)), + ('field_name', + self.gf('django.db.models.fields.CharField')(max_length=128)), + ('lookup_type', self.gf('django.db.models.fields.CharField') + (default='exact', max_length=12)), + ('field_value', + self.gf('django.db.models.fields.CharField')(max_length=255)), )) db.send_create_signal('drip', ['QuerySetRule']) - def backwards(self, orm): # Deleting model 'Drip' db.delete_table('drip_drip') @@ -55,7 +73,6 @@ def backwards(self, orm): # Deleting model 'QuerySetRule' db.delete_table('drip_querysetrule') - models = { 'auth.group': { 'Meta': {'object_name': 'Group'}, diff --git a/drip/migrations/0002_auto__add_field_drip_from_email__add_field_drip_from_email_name__add_f.py b/drip/migrations/0002_auto__add_field_drip_from_email__add_field_drip_from_email_name__add_f.py index f287ae7..872eb83 100644 --- a/drip/migrations/0002_auto__add_field_drip_from_email__add_field_drip_from_email_name__add_f.py +++ b/drip/migrations/0002_auto__add_field_drip_from_email__add_field_drip_from_email_name__add_f.py @@ -10,25 +10,36 @@ class Migration(SchemaMigration): def forwards(self, orm): # Adding field 'Drip.from_email' db.add_column('drip_drip', 'from_email', - self.gf('django.db.models.fields.EmailField')(max_length=75, null=True, blank=True), + self.gf('django.db.models.fields.EmailField')( + max_length=75, + null=True, + blank=True), keep_default=False) # Adding field 'Drip.from_email_name' db.add_column('drip_drip', 'from_email_name', - self.gf('django.db.models.fields.CharField')(max_length=150, null=True, blank=True), + self.gf('django.db.models.fields.CharField')( + max_length=150, + null=True, + blank=True), keep_default=False) # Adding field 'SentDrip.from_email' db.add_column('drip_sentdrip', 'from_email', - self.gf('django.db.models.fields.EmailField')(default=None, max_length=75, null=True), + self.gf('django.db.models.fields.EmailField')( + default=None, + max_length=75, + null=True), keep_default=False) # Adding field 'SentDrip.from_email_name' db.add_column('drip_sentdrip', 'from_email_name', - self.gf('django.db.models.fields.CharField')(default=None, max_length=150, null=True), + self.gf('django.db.models.fields.CharField')( + default=None, + max_length=150, + null=True), keep_default=False) - def backwards(self, orm): # Deleting field 'Drip.from_email' db.delete_column('drip_drip', 'from_email') @@ -42,7 +53,6 @@ def backwards(self, orm): # Deleting field 'SentDrip.from_email_name' db.delete_column('drip_sentdrip', 'from_email_name') - models = { 'auth.group': { 'Meta': {'object_name': 'Group'}, @@ -116,4 +126,4 @@ def backwards(self, orm): } } - complete_apps = ['drip'] \ No newline at end of file + complete_apps = ['drip'] diff --git a/drip/migrations/0003_auto__add_field_drip_message_class.py b/drip/migrations/0003_auto__add_field_drip_message_class.py index 5851455..0ca3f57 100644 --- a/drip/migrations/0003_auto__add_field_drip_message_class.py +++ b/drip/migrations/0003_auto__add_field_drip_message_class.py @@ -10,7 +10,10 @@ class Migration(SchemaMigration): def forwards(self, orm): # Adding field 'Drip.message_class' db.add_column('drip_drip', 'message_class', - self.gf('django.db.models.fields.CharField')(default='default', max_length=120, blank=True), + self.gf('django.db.models.fields.CharField')( + default='default', + max_length=120, + blank=True), keep_default=False) def backwards(self, orm): @@ -91,4 +94,4 @@ def backwards(self, orm): } } - complete_apps = ['drip'] \ No newline at end of file + complete_apps = ['drip'] diff --git a/drip/models.py b/drip/models.py index 94fa400..4be6359 100644 --- a/drip/models.py +++ b/drip/models.py @@ -24,13 +24,16 @@ class Drip(models.Model): enabled = models.BooleanField(default=False) from_email = models.EmailField(null=True, blank=True, - help_text='Set a custom from email.') + help_text='Set a custom from email.') from_email_name = models.CharField(max_length=150, null=True, blank=True, - help_text="Set a name for a custom from email.") + help_text="Set a name for a custom from email.") subject_template = models.TextField(null=True, blank=True) body_html_template = models.TextField(null=True, blank=True, - help_text='You will have settings and user in the context.') - message_class = models.CharField(max_length=120, blank=True, default='default') + help_text='You will have settings and user in the context.') + message_class = models.CharField( + max_length=120, + blank=True, + default='default') @property def drip(self): @@ -49,23 +52,31 @@ def __unicode__(self): class SentDrip(models.Model): + """ Keeps a record of all sent drips. """ date = models.DateTimeField(auto_now_add=True) drip = models.ForeignKey('drip.Drip', related_name='sent_drips') - user = models.ForeignKey(getattr(settings, 'AUTH_USER_MODEL', 'auth.User'), related_name='sent_drips') + user = models.ForeignKey( + getattr( + settings, + 'AUTH_USER_MODEL', + 'auth.User'), + related_name='sent_drips') subject = models.TextField() body = models.TextField() from_email = models.EmailField( - null=True, default=None # For south so that it can migrate existing rows. + # For south so that it can migrate existing rows. + null=True, default=None ) from_email_name = models.CharField(max_length=150, - null=True, default=None # For south so that it can migrate existing rows. - ) - + # For south so that it can migrate + # existing rows. + null=True, default=None + ) METHOD_TYPES = ( @@ -90,19 +101,28 @@ class SentDrip(models.Model): ('iendswith', 'ends with (case insensitive)'), ) + class QuerySetRule(models.Model): date = models.DateTimeField(auto_now_add=True) lastchanged = models.DateTimeField(auto_now=True) drip = models.ForeignKey(Drip, related_name='queryset_rules') - method_type = models.CharField(max_length=12, default='filter', choices=METHOD_TYPES) - field_name = models.CharField(max_length=128, verbose_name='Field name of User') - lookup_type = models.CharField(max_length=12, default='exact', choices=LOOKUP_TYPES) + method_type = models.CharField( + max_length=12, + default='filter', + choices=METHOD_TYPES) + field_name = models.CharField( + max_length=128, + verbose_name='Field name of User') + lookup_type = models.CharField( + max_length=12, + default='exact', + choices=LOOKUP_TYPES) field_value = models.CharField(max_length=255, - help_text=('Can be anything from a number, to a string. Or, do ' + - '`now-7 days` or `today+3 days` for fancy timedelta.')) + help_text=('Can be anything from a number, to a string. Or, do ' + + '`now-7 days` or `today+3 days` for fancy timedelta.')) def clean(self): User = get_user_model() diff --git a/drip/tests.py b/drip/tests.py index 948d3a2..ec7c1b9 100644 --- a/drip/tests.py +++ b/drip/tests.py @@ -16,6 +16,7 @@ class RulesTestCase(TestCase): + def setUp(self): self.drip = Drip.objects.create( name='A Drip just for Rules', @@ -24,15 +25,27 @@ def setUp(self): ) def test_valid_rule(self): - rule = QuerySetRule(drip=self.drip, field_name='date_joined', lookup_type='lte', field_value='now-60 days') + rule = QuerySetRule( + drip=self.drip, + field_name='date_joined', + lookup_type='lte', + field_value='now-60 days') rule.clean() def test_bad_field_name(self): - rule = QuerySetRule(drip=self.drip, field_name='date__joined', lookup_type='lte', field_value='now-60 days') + rule = QuerySetRule( + drip=self.drip, + field_name='date__joined', + lookup_type='lte', + field_value='now-60 days') self.assertRaises(ValidationError, rule.clean) def test_bad_field_value(self): - rule = QuerySetRule(drip=self.drip, field_name='date_joined', lookup_type='lte', field_value='now-2 months') + rule = QuerySetRule( + drip=self.drip, + field_name='date_joined', + lookup_type='lte', + field_value='now-2 months') self.assertRaises(ValidationError, rule.clean) @@ -46,19 +59,45 @@ def setUp(self): self.User = get_user_model() start = timezone.now() - timedelta(hours=2) - num_string = ['first','second','third','fourth','fifth','sixth','seventh','eighth','ninth','tenth'] + num_string = [ + 'first', + 'second', + 'third', + 'fourth', + 'fifth', + 'sixth', + 'seventh', + 'eighth', + 'ninth', + 'tenth'] for i, name in enumerate(num_string): - user = self.User.objects.create(username='%s_25_credits_a_day' % name, email='%s@test.com' % name) - self.User.objects.filter(id=user.id).update(date_joined=start - timedelta(days=i)) + user = self.User.objects.create( + username='%s_25_credits_a_day' % + name, + email='%s@test.com' % + name) + self.User.objects.filter( + id=user.id).update( + date_joined=start - + timedelta( + days=i)) profile = Profile.objects.get(user=user) profile.credits = i * 25 profile.save() for i, name in enumerate(num_string): - user = self.User.objects.create(username='%s_no_credits' % name, email='%s@test.com' % name) - self.User.objects.filter(id=user.id).update(date_joined=start - timedelta(days=i)) + user = self.User.objects.create( + username='%s_no_credits' % + name, + email='%s@test.com' % + name) + self.User.objects.filter( + id=user.id).update( + date_joined=start - + timedelta( + days=i)) def test_users_exists(self): self.assertEqual(20, self.User.objects.all().count()) @@ -66,43 +105,48 @@ def test_users_exists(self): def test_day_zero_users(self): start = timezone.now() - timedelta(days=1) end = timezone.now() - self.assertEqual(2, self.User.objects.filter(date_joined__range=(start, end)).count()) + self.assertEqual( + 2, + self.User.objects.filter( + date_joined__range=( + start, + end)).count()) def test_day_two_users_active(self): start = timezone.now() - timedelta(days=3) end = timezone.now() - timedelta(days=2) self.assertEqual(1, self.User.objects.filter(date_joined__range=(start, end), - profile__credits__gt=0).count()) + profile__credits__gt=0).count()) def test_day_two_users_inactive(self): start = timezone.now() - timedelta(days=3) end = timezone.now() - timedelta(days=2) self.assertEqual(1, self.User.objects.filter(date_joined__range=(start, end), - profile__credits=0).count()) + profile__credits=0).count()) def test_day_seven_users_active(self): start = timezone.now() - timedelta(days=8) end = timezone.now() - timedelta(days=7) self.assertEqual(1, self.User.objects.filter(date_joined__range=(start, end), - profile__credits__gt=0).count()) + profile__credits__gt=0).count()) def test_day_seven_users_inactive(self): start = timezone.now() - timedelta(days=8) end = timezone.now() - timedelta(days=7) self.assertEqual(1, self.User.objects.filter(date_joined__range=(start, end), - profile__credits=0).count()) + profile__credits=0).count()) def test_day_fourteen_users_active(self): start = timezone.now() - timedelta(days=15) end = timezone.now() - timedelta(days=14) self.assertEqual(0, self.User.objects.filter(date_joined__range=(start, end), - profile__credits__gt=0).count()) + profile__credits__gt=0).count()) def test_day_fourteen_users_inactive(self): start = timezone.now() - timedelta(days=15) end = timezone.now() - timedelta(days=14) self.assertEqual(0, self.User.objects.filter(date_joined__range=(start, end), - profile__credits=0).count()) + profile__credits=0).count()) ######################## ### RELATION SNAGGER ### @@ -112,7 +156,8 @@ def test_get_simple_fields(self): from drip.utils import get_simple_fields simple_fields = get_simple_fields(self.User) - self.assertTrue(bool([sf for sf in simple_fields if 'profile' in sf[0]])) + self.assertTrue( + bool([sf for sf in simple_fields if 'profile' in sf[0]])) ################## ### TEST DRIPS ### @@ -150,14 +195,17 @@ def test_custom_drip(self): drip = model_drip.drip # ensure we are starting from a blank slate - self.assertEqual(2, drip.get_queryset().count()) # 2 people meet the criteria + # 2 people meet the criteria + self.assertEqual(2, drip.get_queryset().count()) drip.prune() - self.assertEqual(2, drip.get_queryset().count()) # no one is pruned, never sent before - self.assertEqual(0, SentDrip.objects.count()) # confirm nothing sent before + # no one is pruned, never sent before + self.assertEqual(2, drip.get_queryset().count()) + # confirm nothing sent before + self.assertEqual(0, SentDrip.objects.count()) # send the drip drip.send() - self.assertEqual(2, SentDrip.objects.count()) # got sent + self.assertEqual(2, SentDrip.objects.count()) # got sent for sent in SentDrip.objects.all(): self.assertIn('HELLO', sent.subject) @@ -165,24 +213,26 @@ def test_custom_drip(self): # subsequent runs reflect previous activity drip = Drip.objects.get(id=model_drip.id).drip - self.assertEqual(2, drip.get_queryset().count()) # 2 people meet the criteria + # 2 people meet the criteria + self.assertEqual(2, drip.get_queryset().count()) drip.prune() - self.assertEqual(0, drip.get_queryset().count()) # everyone is pruned + self.assertEqual(0, drip.get_queryset().count()) # everyone is pruned def test_custom_short_term_drip(self): model_drip = self.build_joined_date_drip(shift_one=3, shift_two=4) drip = model_drip.drip # ensure we are starting from a blank slate - self.assertEqual(2, drip.get_queryset().count()) # 2 people meet the criteria - + # 2 people meet the criteria + self.assertEqual(2, drip.get_queryset().count()) def test_custom_date_range_walk(self): model_drip = self.build_joined_date_drip() drip = model_drip.drip # vanilla (now-8, now-7), past (now-8-3, now-7-3), future (now-8+1, now-7+1) - for count, shifted_drip in zip([0, 2, 2, 2, 2], drip.walk(into_past=3, into_future=2)): + for count, shifted_drip in zip( + [0, 2, 2, 2, 2], drip.walk(into_past=3, into_future=2)): self.assertEqual(count, shifted_drip.get_queryset().count()) # no reason to change after a send... @@ -190,7 +240,8 @@ def test_custom_date_range_walk(self): drip = Drip.objects.get(id=model_drip.id).drip # vanilla (now-8, now-7), past (now-8-3, now-7-3), future (now-8+1, now-7+1) - for count, shifted_drip in zip([0, 2, 2, 2, 2], drip.walk(into_past=3, into_future=2)): + for count, shifted_drip in zip( + [0, 2, 2, 2, 2], drip.walk(into_past=3, into_future=2)): self.assertEqual(count, shifted_drip.get_queryset().count()) def test_custom_drip_with_count(self): @@ -203,9 +254,11 @@ def test_custom_drip_with_count(self): ) drip = model_drip.drip - self.assertEqual(1, drip.get_queryset().count()) # 1 person meet the criteria + # 1 person meet the criteria + self.assertEqual(1, drip.get_queryset().count()) - for count, shifted_drip in zip([0, 1, 1, 1, 1], drip.walk(into_past=3, into_future=2)): + for count, shifted_drip in zip( + [0, 1, 1, 1, 1], drip.walk(into_past=3, into_future=2)): self.assertEqual(count, shifted_drip.get_queryset().count()) def test_exclude_and_include(self): @@ -235,7 +288,8 @@ def test_exclude_and_include(self): lookup_type='exact', field_value=125 ) - self.assertEqual(7, model_drip.drip.get_queryset().count()) # 7 people meet the criteria + # 7 people meet the criteria + self.assertEqual(7, model_drip.drip.get_queryset().count()) def test_custom_drip_static_datetime(self): model_drip = self.build_joined_date_drip() @@ -243,11 +297,15 @@ def test_custom_drip_static_datetime(self): drip=model_drip, field_name='date_joined', lookup_type='lte', - field_value=(timezone.now() - timedelta(days=8)).strftime('%Y-%m-%d %H:%M:%S') + field_value=( + timezone.now() - + timedelta( + days=8)).strftime('%Y-%m-%d %H:%M:%S') ) drip = model_drip.drip - for count, shifted_drip in zip([0, 2, 2, 0, 0], drip.walk(into_past=3, into_future=2)): + for count, shifted_drip in zip( + [0, 2, 2, 0, 0], drip.walk(into_past=3, into_future=2)): self.assertEqual(count, shifted_drip.get_queryset().count()) def test_custom_drip_static_now_datetime(self): @@ -260,19 +318,25 @@ def test_custom_drip_static_now_datetime(self): drip=model_drip, field_name='date_joined', lookup_type='gte', - field_value=(timezone.now() - timedelta(days=1)).strftime('%Y-%m-%d 00:00:00') + field_value=( + timezone.now() - + timedelta( + days=1)).strftime('%Y-%m-%d 00:00:00') ) drip = model_drip.drip # catches "today and yesterday" users - for count, shifted_drip in zip([4, 4, 4, 4, 4], drip.walk(into_past=3, into_future=3)): + for count, shifted_drip in zip( + [4, 4, 4, 4, 4], drip.walk(into_past=3, into_future=3)): self.assertEqual(count, shifted_drip.get_queryset().count()) def test_admin_timeline_prunes_user_output(self): """multiple users in timeline is confusing.""" - admin = self.User.objects.create(username='admin', email='admin@example.com') - admin.is_staff=True - admin.is_superuser=True + admin = self.User.objects.create( + username='admin', + email='admin@example.com') + admin.is_staff = True + admin.is_superuser = True admin.save() # create a drip campaign that will surely give us duplicates. @@ -285,15 +349,18 @@ def test_admin_timeline_prunes_user_output(self): drip=model_drip, field_name='date_joined', lookup_type='gte', - field_value=(timezone.now() - timedelta(days=1)).strftime('%Y-%m-%d 00:00:00') + field_value=( + timezone.now() - + timedelta( + days=1)).strftime('%Y-%m-%d 00:00:00') ) # then get it's admin view. rf = RequestFactory() timeline_url = reverse('admin:drip_timeline', kwargs={ - 'drip_id': model_drip.id, - 'into_past': 3, - 'into_future': 3}) + 'drip_id': model_drip.id, + 'into_past': 3, + 'into_future': 3}) request = rf.get(timeline_url) request.user = admin @@ -305,7 +372,6 @@ def test_admin_timeline_prunes_user_output(self): # check that our admin (not excluded from test) is shown once. self.assertEqual(unicode(response.content).count(admin.email), 1) - ################## ### TEST M2M ### ################## @@ -340,7 +406,9 @@ def test_annotated_field_name_property_with_count(self): field_value=2 ) - self.assertEqual(qsr.annotated_field_name, 'num_userprofile_user_groups') + self.assertEqual( + qsr.annotated_field_name, + 'num_userprofile_user_groups') def test_apply_annotations_no_count(self): @@ -378,7 +446,10 @@ def test_apply_annotations_with_count(self): qs = qsr.apply_any_annotation(model_drip.drip.get_queryset()) - self.assertEqual(list(qs.query.aggregate_select.keys()), ['num_profile_user_groups']) + self.assertEqual( + list( + qs.query.aggregate_select.keys()), + ['num_profile_user_groups']) def test_apply_multiple_rules_with_aggregation(self): @@ -399,32 +470,41 @@ def test_apply_multiple_rules_with_aggregation(self): drip=model_drip, field_name='date_joined', lookup_type='gte', - field_value=(timezone.now() - timedelta(days=1)).strftime('%Y-%m-%d 00:00:00') + field_value=( + timezone.now() - + timedelta( + days=1)).strftime('%Y-%m-%d 00:00:00') ) - qsr.clean() - qs = model_drip.drip.apply_queryset_rules(model_drip.drip.get_queryset()) + qs = model_drip.drip.apply_queryset_rules( + model_drip.drip.get_queryset()) self.assertEqual(qs.count(), 4) # Used by CustomMessagesTest class PlainDripEmail(DripMessage): + @property def message(self): if not self._message: - email = mail.EmailMessage(self.subject, self.plain, self.from_email, [self.user.email]) + email = mail.EmailMessage( + self.subject, self.plain, self.from_email, [ + self.user.email]) self._message = email return self._message class CustomMessagesTest(TestCase): + def setUp(self): self.User = get_user_model() self.old_msg_classes = getattr(settings, 'DRIP_MESSAGE_CLASSES', None) - self.user = self.User.objects.create(username='customuser', email='custom@example.com') + self.user = self.User.objects.create( + username='customuser', + email='custom@example.com') self.model_drip = Drip.objects.create( name='A Custom Week Ago', subject_template='HELLO {{ user.username }}', @@ -468,11 +548,13 @@ def test_custom_added_and_used(self): self.assertEqual(1, result) self.assertEqual(1, len(mail.outbox)) email = mail.outbox.pop() - # In this case we did specify the custom key, so message should be of custom type. + # In this case we did specify the custom key, so message should be of + # custom type. self.assertIsInstance(email, mail.EmailMessage) def test_override_default(self): - settings.DRIP_MESSAGE_CLASSES = {'default': 'drip.tests.PlainDripEmail'} + settings.DRIP_MESSAGE_CLASSES = { + 'default': 'drip.tests.PlainDripEmail'} result = self.model_drip.drip.send() self.assertEqual(1, result) self.assertEqual(1, len(mail.outbox)) diff --git a/drip/utils.py b/drip/utils.py index 7bb2fe6..91b6dfb 100644 --- a/drip/utils.py +++ b/drip/utils.py @@ -17,7 +17,7 @@ unicode = str -def get_fields(Model, +def get_fields(Model, parent_field="", model_stack=None, stack_limit=2, @@ -41,7 +41,8 @@ def get_fields(Model, app_label, model_name = Model.split('.') Model = models.get_model(app_label, model_name) - fields = Model._meta.fields + Model._meta.many_to_many + Model._meta.get_all_related_objects() + fields = Model._meta.fields + Model._meta.many_to_many + \ + Model._meta.get_all_related_objects() model_stack.append(Model) # do a variety of checks to ensure recursion isnt being redundant @@ -61,7 +62,7 @@ def get_fields(Model, stop_recursion = True if stop_recursion: - return [] # give empty list for "extend" + return [] # give empty list for "extend" for field in fields: field_name = field.name @@ -81,8 +82,8 @@ def get_fields(Model, out_fields.append([full_field, field_name, Model, field.__class__]) if not stop_recursion and \ - (isinstance(field, ForeignKey) or isinstance(field, OneToOneField) or \ - isinstance(field, RelatedObject) or isinstance(field, ManyToManyField)): + (isinstance(field, ForeignKey) or isinstance(field, OneToOneField) or + isinstance(field, RelatedObject) or isinstance(field, ManyToManyField)): if isinstance(field, RelatedObject): RelModel = field.model @@ -90,10 +91,15 @@ def get_fields(Model, else: RelModel = field.related.parent_model - out_fields.extend(get_fields(RelModel, full_field, list(model_stack))) + out_fields.extend( + get_fields( + RelModel, + full_field, + list(model_stack))) return out_fields + def give_model_field(full_field, Model): """ Given a field_name and Model: @@ -108,11 +114,16 @@ def give_model_field(full_field, Model): if full_key == full_field: return full_key, name, _Model, _ModelField - raise Exception('Field key `{0}` not found on `{1}`.'.format(full_field, Model.__name__)) + raise Exception( + 'Field key `{0}` not found on `{1}`.'.format( + full_field, + Model.__name__)) + def get_simple_fields(Model, **kwargs): return [[f[0], f[3].__name__] for f in get_fields(Model, **kwargs)] + def get_user_model(): # handle 1.7 and back try: diff --git a/setup.py b/setup.py index d9c7b5f..62e9fcb 100755 --- a/setup.py +++ b/setup.py @@ -22,7 +22,8 @@ def get_version(package): Return package version as listed in `__version__` in `init.py`. """ init_py = open(os.path.join(package, '__init__.py')).read() - return re.search("^__version__ = ['\"]([^'\"]+)['\"]", init_py, re.MULTILINE).group(1) + return re.search( + "^__version__ = ['\"]([^'\"]+)['\"]", init_py, re.MULTILINE).group(1) def get_packages(package): diff --git a/test_urls.py b/test_urls.py index 9920823..292d18a 100644 --- a/test_urls.py +++ b/test_urls.py @@ -3,4 +3,4 @@ admin.autodiscover() urlpatterns = patterns("", - url(r'^admin/', include(admin.site.urls))) + url(r'^admin/', include(admin.site.urls))) diff --git a/testsettings.py b/testsettings.py index 0713522..d06f4b0 100755 --- a/testsettings.py +++ b/testsettings.py @@ -6,7 +6,7 @@ SECRET_KEY = 'whatever/you/want-goes-here' -SECRET_KEY="whatever" +SECRET_KEY = "whatever" DATABASES = { 'default': {