diff --git a/credits/models.py b/credits/models.py index dd847f6..bc307a5 100644 --- a/credits/models.py +++ b/credits/models.py @@ -6,7 +6,11 @@ class Profile(models.Model): """ For testing, track the number of "credits". """ - user = models.OneToOneField('auth.User', related_name='profile') + user = models.OneToOneField( + 'auth.User', + related_name='profile', + on_delete=models.CASCADE, + ) credits = models.PositiveIntegerField(default=0) diff --git a/drip/admin.py b/drip/admin.py index 9c1d5a1..387d7af 100644 --- a/drip/admin.py +++ b/drip/admin.py @@ -87,9 +87,9 @@ def change_view(self, request, object_id, extra_context=None): request, object_id, extra_context=self.build_extra_context(extra_context)) def get_urls(self): - from django.conf.urls import patterns, url + from django.conf.urls import url urls = super(DripAdmin, self).get_urls() - my_urls = patterns('', + my_urls = [ url( r'^(?P[\d]+)/timeline/(?P[\d]+)/(?P[\d]+)/$', self.av(self.timeline), @@ -100,7 +100,7 @@ def get_urls(self): 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 87b27aa..a123564 100644 --- a/drip/drips.py +++ b/drip/drips.py @@ -4,7 +4,7 @@ from django.conf import settings from django.db.models import Q from django.template import Context, Template -from django.utils.importlib import import_module +from importlib import import_module from django.core.mail import EmailMultiAlternatives from django.utils.html import strip_tags @@ -184,12 +184,10 @@ def apply_queryset_rules(self, qs): ################## def get_queryset(self): - try: - return self._queryset - except AttributeError: - self._queryset = self.apply_queryset_rules(self.queryset())\ - .distinct() - return self._queryset + queryset = getattr(self,'_queryset', None) + if queryset is None: + self._queryset = self.apply_queryset_rules(self.queryset()).distinct() + return self._queryset def run(self): """ diff --git a/drip/helpers.py b/drip/helpers.py new file mode 100644 index 0000000..d0bfb31 --- /dev/null +++ b/drip/helpers.py @@ -0,0 +1,145 @@ +import re +import datetime +from decimal import Decimal + +STRFDATETIME = re.compile('([dgGhHis])') +STRFDATETIME_REPL = lambda x: '%%(%s)s' % x.group() + +def parse(string): + """ + Parse a string into a timedelta object. + >>> parse("1 day") + datetime.timedelta(1) + >>> parse("2 days") + datetime.timedelta(2) + >>> parse("1 d") + datetime.timedelta(1) + >>> parse("1 hour") + datetime.timedelta(0, 3600) + >>> parse("1 hours") + datetime.timedelta(0, 3600) + >>> parse("1 hr") + datetime.timedelta(0, 3600) + >>> parse("1 hrs") + datetime.timedelta(0, 3600) + >>> parse("1h") + datetime.timedelta(0, 3600) + >>> parse("1wk") + datetime.timedelta(7) + >>> parse("1 week") + datetime.timedelta(7) + >>> parse("1 weeks") + datetime.timedelta(7) + >>> parse("2 wks") + datetime.timedelta(14) + >>> parse("1 sec") + datetime.timedelta(0, 1) + >>> parse("1 secs") + datetime.timedelta(0, 1) + >>> parse("1 s") + datetime.timedelta(0, 1) + >>> parse("1 second") + datetime.timedelta(0, 1) + >>> parse("1 seconds") + datetime.timedelta(0, 1) + >>> parse("1 minute") + datetime.timedelta(0, 60) + >>> parse("1 min") + datetime.timedelta(0, 60) + >>> parse("1 m") + datetime.timedelta(0, 60) + >>> parse("1 minutes") + datetime.timedelta(0, 60) + >>> parse("1 mins") + datetime.timedelta(0, 60) + >>> parse("2 ws") + Traceback (most recent call last): + ... + TypeError: '2 ws' is not a valid time interval + >>> parse("2 ds") + Traceback (most recent call last): + ... + TypeError: '2 ds' is not a valid time interval + >>> parse("2 hs") + Traceback (most recent call last): + ... + TypeError: '2 hs' is not a valid time interval + >>> parse("2 ms") + Traceback (most recent call last): + ... + TypeError: '2 ms' is not a valid time interval + >>> parse("2 ss") + Traceback (most recent call last): + ... + TypeError: '2 ss' is not a valid time interval + >>> parse("") + Traceback (most recent call last): + ... + TypeError: '' is not a valid time interval + >>> parse("1.5 days") + datetime.timedelta(1, 43200) + >>> parse("3 weeks") + datetime.timedelta(21) + >>> parse("4.2 hours") + datetime.timedelta(0, 15120) + >>> parse(".5 hours") + datetime.timedelta(0, 1800) + >>> parse(" hours") + Traceback (most recent call last): + ... + TypeError: 'hours' is not a valid time interval + >>> parse("1 hour, 5 mins") + datetime.timedelta(0, 3900) + >>> parse("-2 days") + datetime.timedelta(-2) + >>> parse("-1 day 0:00:01") + datetime.timedelta(-1, 1) + >>> parse("-1 day, -1:01:01") + datetime.timedelta(-2, 82739) + >>> parse("-1 weeks, 2 days, -3 hours, 4 minutes, -5 seconds") + datetime.timedelta(-5, 11045) + >>> parse("0 seconds") + datetime.timedelta(0) + >>> parse("0 days") + datetime.timedelta(0) + >>> parse("0 weeks") + datetime.timedelta(0) + >>> zero = datetime.timedelta(0) + >>> parse(nice_repr(zero)) + datetime.timedelta(0) + >>> parse(nice_repr(zero, 'minimal')) + datetime.timedelta(0) + >>> parse(nice_repr(zero, 'short')) + datetime.timedelta(0) + >>> parse(' 50 days 00:00:00 ') + datetime.timedelta(50) + """ + string = string.strip() + + if string == "": + raise TypeError("'%s' is not a valid time interval" % string) + # This is the format we get from sometimes Postgres, sqlite, + # and from serialization + d = re.match(r'^((?P[-+]?\d+) days?,? )?(?P[-+]?)(?P\d+):' + r'(?P\d+)(:(?P\d+(\.\d+)?))?$', + str(string)) + if d: + d = d.groupdict(0) + if d['sign'] == '-': + for k in 'hours', 'minutes', 'seconds': + d[k] = '-' + d[k] + d.pop('sign', None) + else: + # This is the more flexible format + d = re.match( + r'^((?P-?((\d*\.\d+)|\d+))\W*w((ee)?(k(s)?)?)(,)?\W*)?' + r'((?P-?((\d*\.\d+)|\d+))\W*d(ay(s)?)?(,)?\W*)?' + r'((?P-?((\d*\.\d+)|\d+))\W*h(ou)?(r(s)?)?(,)?\W*)?' + r'((?P-?((\d*\.\d+)|\d+))\W*m(in(ute)?(s)?)?(,)?\W*)?' + r'((?P-?((\d*\.\d+)|\d+))\W*s(ec(ond)?(s)?)?)?\W*$', + string) + if not d: + raise TypeError("'%s' is not a valid time interval" % string) + d = d.groupdict(0) + + return datetime.timedelta(**dict(( (k, float(v)) for k,v in d.items()))) diff --git a/drip/migrations/0001_initial.py b/drip/migrations/0001_initial.py index 914041f..0c1c8b9 100644 --- a/drip/migrations/0001_initial.py +++ b/drip/migrations/0001_initial.py @@ -1,128 +1,58 @@ -# -*- coding: utf-8 -*- -import 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 '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)), - )) - 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'])), - ('subject', self.gf('django.db.models.fields.TextField')()), - ('body', self.gf('django.db.models.fields.TextField')()), - )) - db.send_create_signal('drip', ['SentDrip']) - - # 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)), - )) - db.send_create_signal('drip', ['QuerySetRule']) - - - def backwards(self, orm): - # Deleting model 'Drip' - db.delete_table('drip_drip') - - # Deleting model 'SentDrip' - db.delete_table('drip_sentdrip') - - # Deleting model 'QuerySetRule' - db.delete_table('drip_querysetrule') - - - models = { - 'auth.group': { - 'Meta': {'object_name': 'Group'}, - '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': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) - }, - 'auth.permission': { - 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, - 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) - }, - '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', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), - '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', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), - 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) - }, - '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'}), - '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'}) - }, - 'drip.drip': { - 'Meta': {'object_name': 'Drip'}, - 'body_html_template': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), - 'date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), - 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'lastchanged': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), - 'subject_template': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}) - }, - 'drip.querysetrule': { - 'Meta': {'object_name': 'QuerySetRule'}, - 'date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), - 'drip': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'queryset_rules'", 'to': "orm['drip.Drip']"}), - 'field_name': ('django.db.models.fields.CharField', [], {'max_length': '128'}), - 'field_value': ('django.db.models.fields.CharField', [], {'max_length': '255'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'lastchanged': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), - 'lookup_type': ('django.db.models.fields.CharField', [], {'default': "'exact'", 'max_length': '12'}), - 'method_type': ('django.db.models.fields.CharField', [], {'default': "'filter'", 'max_length': '12'}) - }, - 'drip.sentdrip': { - 'Meta': {'object_name': 'SentDrip'}, - 'body': ('django.db.models.fields.TextField', [], {}), - 'date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), - 'drip': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'sent_drips'", 'to': "orm['drip.Drip']"}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'subject': ('django.db.models.fields.TextField', [], {}), - 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'sent_drips'", 'to': "orm['auth.User']"}) - } - } - - complete_apps = ['drip'] +# Generated by Django 3.0.6 on 2020-05-29 20:38 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Drip', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('date', models.DateTimeField(auto_now_add=True)), + ('lastchanged', models.DateTimeField(auto_now=True)), + ('name', models.CharField(help_text='A unique name for this drip.', max_length=255, unique=True, verbose_name='Drip Name')), + ('enabled', models.BooleanField(default=False)), + ('from_email', models.EmailField(blank=True, help_text='Set a custom from email.', max_length=254, null=True)), + ('from_email_name', models.CharField(blank=True, help_text='Set a name for a custom from email.', max_length=150, null=True)), + ('subject_template', models.TextField(blank=True, null=True)), + ('body_html_template', models.TextField(blank=True, help_text='You will have settings and user in the context.', null=True)), + ('message_class', models.CharField(blank=True, default='default', max_length=120)), + ], + ), + migrations.CreateModel( + name='SentDrip', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('date', models.DateTimeField(auto_now_add=True)), + ('subject', models.TextField()), + ('body', models.TextField()), + ('from_email', models.EmailField(default=None, max_length=254, null=True)), + ('from_email_name', models.CharField(default=None, max_length=150, null=True)), + ('drip', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sent_drips', to='drip.Drip')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sent_drips', to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='QuerySetRule', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('date', models.DateTimeField(auto_now_add=True)), + ('lastchanged', models.DateTimeField(auto_now=True)), + ('method_type', models.CharField(choices=[('filter', 'Filter'), ('exclude', 'Exclude')], default='filter', max_length=12)), + ('field_name', models.CharField(max_length=128, verbose_name='Field name of User')), + ('lookup_type', models.CharField(choices=[('exact', 'exactly'), ('iexact', 'exactly (case insensitive)'), ('contains', 'contains'), ('icontains', 'contains (case insensitive)'), ('regex', 'regex'), ('iregex', 'contains (case insensitive)'), ('gt', 'greater than'), ('gte', 'greater than or equal to'), ('lt', 'less than'), ('lte', 'less than or equal to'), ('startswith', 'starts with'), ('endswith', 'starts with'), ('istartswith', 'ends with (case insensitive)'), ('iendswith', 'ends with (case insensitive)')], default='exact', max_length=12)), + ('field_value', models.CharField(help_text='Can be anything from a number, to a string. Or, do `now-7 days` or `today+3 days` for fancy timedelta.', max_length=255)), + ('drip', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='queryset_rules', to='drip.Drip')), + ], + ), + ] 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 deleted file mode 100644 index f287ae7..0000000 --- a/drip/migrations/0002_auto__add_field_drip_from_email__add_field_drip_from_email_name__add_f.py +++ /dev/null @@ -1,119 +0,0 @@ -# -*- coding: utf-8 -*- -import datetime -from south.db import db -from south.v2 import SchemaMigration -from django.db import models - - -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), - 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), - 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), - 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), - keep_default=False) - - - def backwards(self, orm): - # Deleting field 'Drip.from_email' - db.delete_column('drip_drip', 'from_email') - - # Deleting field 'Drip.from_email_name' - db.delete_column('drip_drip', 'from_email_name') - - # Deleting field 'SentDrip.from_email' - db.delete_column('drip_sentdrip', 'from_email') - - # Deleting field 'SentDrip.from_email_name' - db.delete_column('drip_sentdrip', 'from_email_name') - - - models = { - 'auth.group': { - 'Meta': {'object_name': 'Group'}, - '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': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) - }, - 'auth.permission': { - 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, - 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) - }, - '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', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), - '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', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), - 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) - }, - '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'}), - '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'}) - }, - 'drip.drip': { - 'Meta': {'object_name': 'Drip'}, - 'body_html_template': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), - 'date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), - 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'from_email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'null': 'True', 'blank': 'True'}), - 'from_email_name': ('django.db.models.fields.CharField', [], {'max_length': '150', 'null': 'True', 'blank': 'True'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'lastchanged': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), - 'subject_template': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}) - }, - 'drip.querysetrule': { - 'Meta': {'object_name': 'QuerySetRule'}, - 'date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), - 'drip': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'queryset_rules'", 'to': "orm['drip.Drip']"}), - 'field_name': ('django.db.models.fields.CharField', [], {'max_length': '128'}), - 'field_value': ('django.db.models.fields.CharField', [], {'max_length': '255'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'lastchanged': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), - 'lookup_type': ('django.db.models.fields.CharField', [], {'default': "'exact'", 'max_length': '12'}), - 'method_type': ('django.db.models.fields.CharField', [], {'default': "'filter'", 'max_length': '12'}) - }, - 'drip.sentdrip': { - 'Meta': {'object_name': 'SentDrip'}, - 'body': ('django.db.models.fields.TextField', [], {}), - 'date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), - 'drip': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'sent_drips'", 'to': "orm['drip.Drip']"}), - 'from_email': ('django.db.models.fields.EmailField', [], {'default': 'None', 'max_length': '75', 'null': 'True'}), - 'from_email_name': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '150', 'null': 'True'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'subject': ('django.db.models.fields.TextField', [], {}), - 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'sent_drips'", 'to': "orm['auth.User']"}) - } - } - - complete_apps = ['drip'] \ No newline at end of file diff --git a/drip/migrations/0003_auto__add_field_drip_message_class.py b/drip/migrations/0003_auto__add_field_drip_message_class.py deleted file mode 100644 index 5851455..0000000 --- a/drip/migrations/0003_auto__add_field_drip_message_class.py +++ /dev/null @@ -1,94 +0,0 @@ -# -*- coding: utf-8 -*- -import datetime -from south.db import db -from south.v2 import SchemaMigration -from django.db import models - - -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), - keep_default=False) - - def backwards(self, orm): - # Deleting field 'Drip.message_class' - db.delete_column('drip_drip', 'message_class') - - models = { - 'auth.group': { - 'Meta': {'object_name': 'Group'}, - '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': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) - }, - 'auth.permission': { - 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, - 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) - }, - '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', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), - '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', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), - 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) - }, - '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'}), - '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'}) - }, - 'drip.drip': { - 'Meta': {'object_name': 'Drip'}, - 'body_html_template': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), - 'date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), - 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'from_email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'null': 'True', 'blank': 'True'}), - 'from_email_name': ('django.db.models.fields.CharField', [], {'max_length': '150', 'null': 'True', 'blank': 'True'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'lastchanged': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), - 'message_class': ('django.db.models.fields.CharField', [], {'default': "'default'", 'max_length': '120', 'blank': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), - 'subject_template': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}) - }, - 'drip.querysetrule': { - 'Meta': {'object_name': 'QuerySetRule'}, - 'date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), - 'drip': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'queryset_rules'", 'to': "orm['drip.Drip']"}), - 'field_name': ('django.db.models.fields.CharField', [], {'max_length': '128'}), - 'field_value': ('django.db.models.fields.CharField', [], {'max_length': '255'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'lastchanged': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), - 'lookup_type': ('django.db.models.fields.CharField', [], {'default': "'exact'", 'max_length': '12'}), - 'method_type': ('django.db.models.fields.CharField', [], {'default': "'filter'", 'max_length': '12'}) - }, - 'drip.sentdrip': { - 'Meta': {'object_name': 'SentDrip'}, - 'body': ('django.db.models.fields.TextField', [], {}), - 'date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), - 'drip': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'sent_drips'", 'to': "orm['drip.Drip']"}), - 'from_email': ('django.db.models.fields.EmailField', [], {'default': 'None', 'max_length': '75', 'null': 'True'}), - 'from_email_name': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '150', 'null': 'True'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'subject': ('django.db.models.fields.TextField', [], {}), - 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'sent_drips'", 'to': "orm['auth.User']"}) - } - } - - complete_apps = ['drip'] \ No newline at end of file diff --git a/drip/models.py b/drip/models.py index 94fa400..9653d92 100644 --- a/drip/models.py +++ b/drip/models.py @@ -8,8 +8,7 @@ # just using this to parse, but totally insane package naming... # https://bitbucket.org/schinckel/django-timedelta-field/ -import timedelta as djangotimedelta - +from drip.helpers import parse class Drip(models.Model): date = models.DateTimeField(auto_now_add=True) @@ -54,8 +53,16 @@ class SentDrip(models.Model): """ 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') + drip = models.ForeignKey( + 'drip.Drip', + related_name='sent_drips', + on_delete=models.CASCADE, + ) + user = models.ForeignKey( + getattr(settings, 'AUTH_USER_MODEL', 'auth.User'), + related_name='sent_drips', + on_delete=models.CASCADE, + ) subject = models.TextField() body = models.TextField() @@ -94,7 +101,11 @@ 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') + drip = models.ForeignKey( + Drip, + related_name='queryset_rules', + on_delete=models.CASCADE, + ) 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') @@ -137,16 +148,16 @@ def filter_kwargs(self, qs, now=datetime.now): # set time deltas and dates if self.field_value.startswith('now-'): field_value = self.field_value.replace('now-', '') - field_value = now() - djangotimedelta.parse(field_value) + field_value = now() - parse(field_value) elif self.field_value.startswith('now+'): field_value = self.field_value.replace('now+', '') - field_value = now() + djangotimedelta.parse(field_value) + field_value = now() + parse(field_value) elif self.field_value.startswith('today-'): field_value = self.field_value.replace('today-', '') - field_value = now().date() - djangotimedelta.parse(field_value) + field_value = now().date() - parse(field_value) elif self.field_value.startswith('today+'): field_value = self.field_value.replace('today+', '') - field_value = now().date() + djangotimedelta.parse(field_value) + field_value = now().date() + parse(field_value) # F expressions if self.field_value.startswith('F_'): diff --git a/drip/templates/admin/drip/SentDrip/change_form.html b/drip/templates/admin/drip/SentDrip/change_form.html index a57983b..346871c 100644 --- a/drip/templates/admin/drip/SentDrip/change_form.html +++ b/drip/templates/admin/drip/SentDrip/change_form.html @@ -1,6 +1,5 @@ {% extends "admin/change_form.html" %} -{% load i18n admin_static admin_list %} -{% load url from future %} +{% load i18n static admin_list %} {% load admin_urls %} {% block object-tools-items %} diff --git a/drip/templates/admin/drip/change_form.html b/drip/templates/admin/drip/change_form.html index 56fe9a6..d8e490f 100644 --- a/drip/templates/admin/drip/change_form.html +++ b/drip/templates/admin/drip/change_form.html @@ -1,6 +1,5 @@ {% extends "admin/change_form.html" %} -{% load i18n admin_static admin_list %} -{% load url from future %} +{% load i18n static admin_list %} {% load admin_urls %} {% block object-tools-items %} diff --git a/drip/templates/drip/timeline.html b/drip/templates/drip/timeline.html index be50bb2..173f4e4 100644 --- a/drip/templates/drip/timeline.html +++ b/drip/templates/drip/timeline.html @@ -1,6 +1,5 @@ {% extends "admin/base_site.html" %} -{% load i18n admin_static admin_modify %} -{% load url from future %} +{% load i18n static admin_modify %} {% load admin_urls %} {% block title %}Viewing Timeline for {{ drip.name }}{% endblock title %} diff --git a/drip/tests.py b/drip/tests.py index 948d3a2..661525a 100644 --- a/drip/tests.py +++ b/drip/tests.py @@ -3,7 +3,7 @@ from django.test import TestCase from django.test.client import RequestFactory from django.core.exceptions import ValidationError -from django.core.urlresolvers import resolve, reverse +from django.urls import resolve, reverse from django.core import mail from django.conf import settings from django.utils import timezone @@ -377,8 +377,7 @@ 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.annotation_select.keys()), ['num_profile_user_groups']) def test_apply_multiple_rules_with_aggregation(self): diff --git a/drip/utils.py b/drip/utils.py index 7bb2fe6..5d124f3 100644 --- a/drip/utils.py +++ b/drip/utils.py @@ -2,7 +2,7 @@ from django.db import models from django.db.models import ForeignKey, OneToOneField, ManyToManyField -from django.db.models.related import RelatedObject +from django.db.models.fields.related import ForeignObjectRel as RelatedObject # taking a nod from python-requests and skipping six _ver = sys.version_info @@ -19,7 +19,7 @@ def get_fields(Model, parent_field="", - model_stack=None, + model_stack=[], stack_limit=2, excludes=['permissions', 'comment', 'content_type']): """ @@ -33,15 +33,12 @@ def get_fields(Model, """ out_fields = [] - if model_stack is None: - model_stack = [] - # github.com/omab/python-social-auth/commit/d8637cec02422374e4102231488481170dc51057 if isinstance(Model, basestring): 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_fields() model_stack.append(Model) # do a variety of checks to ensure recursion isnt being redundant @@ -88,7 +85,7 @@ def get_fields(Model, RelModel = field.model #field_names.extend(get_fields(RelModel, full_field, True)) else: - RelModel = field.related.parent_model + RelModel = field.related_model out_fields.extend(get_fields(RelModel, full_field, list(model_stack))) diff --git a/requirements.txt b/requirements.txt index 932bd37..a1f4176 100755 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,5 @@ # production Django>=1.4 -django-timedeltafield==0.7.2 # development Sphinx==1.1.3 -South==1.0 diff --git a/test_urls.py b/test_urls.py index 9920823..587bce5 100644 --- a/test_urls.py +++ b/test_urls.py @@ -1,6 +1,7 @@ -from django.conf.urls import patterns, url, include +from django.conf.urls import url, include from django.contrib import admin admin.autodiscover() -urlpatterns = patterns("", - url(r'^admin/', include(admin.site.urls))) +urlpatterns = [ + url(r'^admin/', admin.site.urls), +] diff --git a/testsettings.py b/testsettings.py index 0713522..791e6a7 100755 --- a/testsettings.py +++ b/testsettings.py @@ -21,6 +21,7 @@ 'django.contrib.sessions', 'django.contrib.contenttypes', 'django.contrib.staticfiles', + 'django.contrib.messages', 'drip', @@ -28,7 +29,7 @@ 'credits', ) -MIDDLEWARE_CLASSES = ( +MIDDLEWARE = ( 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', @@ -37,6 +38,29 @@ 'django.middleware.clickjacking.XFrameOptionsMiddleware', ) +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [ + # insert your TEMPLATE_DIRS here + ], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + # Insert your TEMPLATE_CONTEXT_PROCESSORS here or use this + # list if you haven't customized them: + 'django.contrib.auth.context_processors.auth', + 'django.template.context_processors.debug', + 'django.template.context_processors.i18n', + 'django.template.context_processors.media', + 'django.template.context_processors.static', + 'django.template.context_processors.tz', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + USE_TZ = True TIME_ZONE = 'UTC'