From 3c3e4b958e8093bf5ad5b7610807b4dc25bfaa4c Mon Sep 17 00:00:00 2001 From: Lucas Connors Date: Sun, 1 Sep 2019 13:50:53 -0700 Subject: [PATCH] Autoformat Python code with black (#324) * Add black to format Python code * Reformat Python code with black * Check code formatting during CI with black --- .travis.yml | 1 + accounts/apps.py | 2 +- accounts/backends.py | 32 +- accounts/cache.py | 6 +- accounts/context_processors.py | 6 +- accounts/factories.py | 13 +- accounts/forms.py | 84 +++-- accounts/middleware.py | 9 +- accounts/migrations/0001_initial.py | 28 +- accounts/migrations/0002_userprofiles.py | 4 +- .../migrations/0003_auto_20160514_0528.py | 92 ++++- .../migrations/0004_auto_20160522_2139.py | 21 +- .../migrations/0005_auto_20160623_0657.py | 6 +- .../migrations/0006_auto_20161115_0635.py | 10 +- .../migrations/0007_auto_20170528_2250.py | 17 +- accounts/models.py | 78 +++-- accounts/pipeline.py | 46 +-- accounts/signals.py | 8 +- accounts/tests.py | 313 +++++++++--------- accounts/urls.py | 52 +-- accounts/views.py | 153 +++++---- api/apps.py | 2 +- api/tests.py | 261 +++++++-------- api/urls.py | 10 +- api/views.py | 56 ++-- artist/admin.py | 30 +- artist/apps.py | 2 +- artist/context_processors.py | 4 +- artist/factories.py | 14 +- artist/forms.py | 45 ++- artist/geolocator.py | 2 +- artist/managers.py | 47 +-- artist/migrations/0001_initial.py | 222 ++++++++++--- artist/migrations/0002_auto_20160317_0521.py | 16 +- artist/migrations/0003_auto_20160522_2139.py | 14 +- artist/migrations/0004_auto_20160522_2141.py | 12 +- artist/migrations/0005_auto_20160522_2328.py | 68 ++-- artist/migrations/0006_updatetitles.py | 8 +- artist/migrations/0007_artistadmin.py | 44 ++- artist/migrations/0008_auto_20160625_0134.py | 21 +- artist/migrations/0009_auto_20170201_0753.py | 42 ++- artist/migrations/0010_auto_20170201_0754.py | 28 +- artist/migrations/0011_auto_20170201_0754.py | 13 +- artist/migrations/0012_auto_20170201_0820.py | 12 +- artist/migrations/0013_auto_20170511_0508.py | 14 +- artist/models.py | 172 ++++++---- artist/tests.py | 103 +++--- artist/views.py | 182 +++++----- campaign/admin.py | 69 +++- campaign/apps.py | 2 +- campaign/factories.py | 18 +- campaign/forms.py | 8 +- campaign/migrations/0001_initial.py | 180 ++++++++-- .../migrations/0002_auto_20160411_0246.py | 19 +- .../migrations/0003_auto_20160418_0057.py | 15 +- .../0004_artistpercentagebreakdown.py | 51 ++- .../migrations/0005_auto_20160618_2310.py | 42 ++- .../migrations/0006_auto_20160618_2351.py | 10 +- .../migrations/0007_auto_20160618_2352.py | 46 +-- .../migrations/0008_auto_20160618_2352.py | 29 +- .../migrations/0009_auto_20160618_2353.py | 34 +- .../migrations/0010_auto_20160625_0134.py | 16 +- campaign/models.py | 174 ++++++---- campaign/signals.py | 30 +- campaign/tests.py | 109 +++--- campaign/views.py | 37 ++- emails/apps.py | 2 +- emails/factories.py | 3 +- emails/mailchimp.py | 26 +- emails/managers.py | 6 +- emails/messages.py | 77 ++--- emails/migrations/0001_initial.py | 28 +- emails/migrations/0002_auto_20160502_0538.py | 17 +- emails/migrations/0003_auto_20160531_0446.py | 45 ++- emails/models.py | 22 +- emails/signals.py | 31 +- emails/tests.py | 44 ++- emails/utils.py | 12 +- emails/views.py | 45 ++- fabfile.py | 25 +- manage.py | 2 +- music/admin/forms.py | 22 +- music/admin/model_admins.py | 20 +- music/admin/views.py | 36 +- music/apps.py | 2 +- music/factories.py | 15 +- music/migrations/0001_initial.py | 102 ++++-- music/migrations/0002_auto_20160725_0226.py | 47 ++- music/migrations/0003_auto_20160730_1802.py | 35 +- music/migrations/0004_track.py | 33 +- music/migrations/0005_auto_20160911_0829.py | 10 +- music/migrations/0006_auto_20160920_0724.py | 44 ++- music/models.py | 166 ++++++---- music/tests.py | 66 ++-- music/views.py | 32 +- perdiem/context_processors.py | 4 +- perdiem/gunicorn.py | 2 +- perdiem/settings/base.py | 243 +++++++------- perdiem/settings/prod.py | 12 +- perdiem/tests.py | 40 ++- perdiem/urls.py | 114 ++++--- perdiem/views.py | 35 +- perdiem/wsgi.py | 2 +- poetry.lock | 53 ++- pyproject.toml | 5 + 105 files changed, 2980 insertions(+), 1859 deletions(-) diff --git a/.travis.yml b/.travis.yml index 770e9fb6..cfc208c5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,6 +17,7 @@ before_script: - cp .env-sample .env script: + - poetry run black . --check - poetry run python manage.py migrate - poetry run python manage.py collectstatic --no-input - poetry run coverage run ./manage.py test diff --git a/accounts/apps.py b/accounts/apps.py index cac9b430..ffd2bc8e 100644 --- a/accounts/apps.py +++ b/accounts/apps.py @@ -3,7 +3,7 @@ class AccountsConfig(AppConfig): - name = 'accounts' + name = "accounts" def ready(self): import accounts.signals diff --git a/accounts/backends.py b/accounts/backends.py index ee35e698..6dff2db8 100644 --- a/accounts/backends.py +++ b/accounts/backends.py @@ -10,35 +10,43 @@ class GoogleOAuth2Login(GoogleOAuth2): - name = 'google-oauth2-login' - auth_operation = 'login' + name = "google-oauth2-login" + auth_operation = "login" def setting(self, name, default=None): - return self.strategy.setting(name, default=default, backend=super(GoogleOAuth2Login, self)) + return self.strategy.setting( + name, default=default, backend=super(GoogleOAuth2Login, self) + ) class GoogleOAuth2Register(GoogleOAuth2): - name = 'google-oauth2-register' - auth_operation = 'register' + name = "google-oauth2-register" + auth_operation = "register" def setting(self, name, default=None): - return self.strategy.setting(name, default=default, backend=super(GoogleOAuth2Register, self)) + return self.strategy.setting( + name, default=default, backend=super(GoogleOAuth2Register, self) + ) class FacebookOAuth2Login(FacebookOAuth2): - name = 'facebook-login' - auth_operation = 'login' + name = "facebook-login" + auth_operation = "login" def setting(self, name, default=None): - return self.strategy.setting(name, default=default, backend=super(FacebookOAuth2Login, self)) + return self.strategy.setting( + name, default=default, backend=super(FacebookOAuth2Login, self) + ) class FacebookOAuth2Register(FacebookOAuth2): - name = 'facebook-register' - auth_operation = 'register' + name = "facebook-register" + auth_operation = "register" def setting(self, name, default=None): - return self.strategy.setting(name, default=default, backend=super(FacebookOAuth2Register, self)) + return self.strategy.setting( + name, default=default, backend=super(FacebookOAuth2Register, self) + ) diff --git a/accounts/cache.py b/accounts/cache.py index a68ec388..cca3c86f 100644 --- a/accounts/cache.py +++ b/accounts/cache.py @@ -10,7 +10,9 @@ def cache_using_pk(func): @functools.wraps(func) def wrapper(instance, *args, **kwargs): - cache_key = '{func_name}-{pk}'.format(func_name=func.__name__, pk=instance.pk) - return cache.get_or_set(cache_key, functools.partial(func, instance, *args, **kwargs)) + cache_key = "{func_name}-{pk}".format(func_name=func.__name__, pk=instance.pk) + return cache.get_or_set( + cache_key, functools.partial(func, instance, *args, **kwargs) + ) return wrapper diff --git a/accounts/context_processors.py b/accounts/context_processors.py index 4b66d179..585911bc 100644 --- a/accounts/context_processors.py +++ b/accounts/context_processors.py @@ -9,9 +9,9 @@ def keys(request): return { - 'FB_APP_ID': settings.SOCIAL_AUTH_FACEBOOK_KEY, - 'GA_TRACKING_CODE': settings.GA_TRACKING_CODE, - 'JACO_API_KEY': settings.JACO_API_KEY, + "FB_APP_ID": settings.SOCIAL_AUTH_FACEBOOK_KEY, + "GA_TRACKING_CODE": settings.GA_TRACKING_CODE, + "JACO_API_KEY": settings.JACO_API_KEY, } diff --git a/accounts/factories.py b/accounts/factories.py index d030120c..96417e6a 100644 --- a/accounts/factories.py +++ b/accounts/factories.py @@ -7,16 +7,18 @@ def userfactory_factory(apps, has_password=True): class UserFactory(factory.DjangoModelFactory): - _PASSWORD = 'abc123' + _PASSWORD = "abc123" class Meta: model = apps.get_model(settings.AUTH_USER_MODEL) - username = factory.Faker('user_name') - email = factory.LazyAttribute(lambda user: "{username}@gmail.com".format(username=user.username)) + username = factory.Faker("user_name") + email = factory.LazyAttribute( + lambda user: "{username}@gmail.com".format(username=user.username) + ) if has_password: - password = factory.PostGenerationMethodCall('set_password', _PASSWORD) + password = factory.PostGenerationMethodCall("set_password", _PASSWORD) return UserFactory @@ -25,8 +27,7 @@ class Meta: class UserAvatarFactory(factory.DjangoModelFactory): - class Meta: - model = django_apps.get_model('accounts', 'UserAvatar') + model = django_apps.get_model("accounts", "UserAvatar") user = factory.SubFactory(UserFactory) diff --git a/accounts/forms.py b/accounts/forms.py index 6fd24f10..3e9c5b7f 100644 --- a/accounts/forms.py +++ b/accounts/forms.py @@ -14,9 +14,8 @@ class LoginAccountForm(AuthenticationForm): - def clean_username(self): - username = self.cleaned_data['username'] + username = self.cleaned_data["username"] return username.lower() @@ -26,36 +25,40 @@ class RegisterAccountForm(UserCreationForm): max_length=150, validators=[ validators.RegexValidator( - r'^[a-z0-9.@+_-]+$', + r"^[a-z0-9.@+_-]+$", ( - 'Enter a valid username. This value may contain only lowercase letters, ' - 'numbers and @/./+/-/_ characters.' - ) - ), - ] + "Enter a valid username. This value may contain only lowercase letters, " + "numbers and @/./+/-/_ characters." + ), + ) + ], ) email = forms.EmailField(required=True) subscribe_news = forms.BooleanField( - required=False, initial=True, label='Get exclusive updates before they\'re public' + required=False, + initial=True, + label="Get exclusive updates before they're public", ) class Meta(UserCreationForm.Meta): - fields = ('username', 'email', 'password1', 'password2',) + fields = ("username", "email", "password1", "password2") def clean_email(self): - email = self.cleaned_data['email'] + email = self.cleaned_data["email"] # Verify that there are no other users already with this email address if User.objects.filter(email=email).exists(): raise forms.ValidationError( - "The email address {email} already belongs to an existing user on PerDiem.".format(email=email) + "The email address {email} already belongs to an existing user on PerDiem.".format( + email=email + ) ) return email def save(self, commit=True): user = super(RegisterAccountForm, self).save(commit=False) - user.email = self.cleaned_data['email'] + user.email = self.cleaned_data["email"] if commit: user.save() return user @@ -67,13 +70,13 @@ class EditNameForm(forms.Form): max_length=150, validators=[ validators.RegexValidator( - r'^[a-z0-9.@+_-]+$', + r"^[a-z0-9.@+_-]+$", ( - 'Enter a valid username. This value may contain only ' - 'lowercase letters, numbers and @/./+/-/_ characters.' - ) - ), - ] + "Enter a valid username. This value may contain only " + "lowercase letters, numbers and @/./+/-/_ characters." + ), + ) + ], ) first_name = forms.CharField(max_length=30, required=False) last_name = forms.CharField(max_length=30, required=False) @@ -84,7 +87,7 @@ def __init__(self, user, *args, **kwargs): super(EditNameForm, self).__init__(*args, **kwargs) def clean_username(self): - username = self.cleaned_data['username'] + username = self.cleaned_data["username"] if User.objects.exclude(id=self.user.id).filter(username=username).exists(): raise forms.ValidationError("A user with that username already exists.") return username @@ -96,21 +99,25 @@ class EditAvatarForm(forms.Form): def __init__(self, user, *args, **kwargs): super(EditAvatarForm, self).__init__(*args, **kwargs) - self.fields['avatar'] = forms.ChoiceField( - choices=self.get_avatar_choices(user), required=False, widget=forms.RadioSelect + self.fields["avatar"] = forms.ChoiceField( + choices=self.get_avatar_choices(user), + required=False, + widget=forms.RadioSelect, ) def get_avatar_choices(self, user): user_avatars = UserAvatar.objects.filter(user=user) - return [('', 'Default',)] + [(avatar.id, avatar.get_provider_display(),) for avatar in user_avatars] + return [("", "Default")] + [ + (avatar.id, avatar.get_provider_display()) for avatar in user_avatars + ] def clean_avatar(self): - avatar_id = self.cleaned_data['avatar'] + avatar_id = self.cleaned_data["avatar"] if avatar_id: return UserAvatar.objects.get(id=avatar_id) def clean_custom_avatar(self): - custom_avatar = self.cleaned_data['custom_avatar'] + custom_avatar = self.cleaned_data["custom_avatar"] if custom_avatar and custom_avatar._size > settings.MAXIMUM_AVATAR_SIZE: raise forms.ValidationError("Image file too large (2MB maximum).") return custom_avatar @@ -119,10 +126,15 @@ def clean_custom_avatar(self): class EmailPreferencesForm(forms.Form): email = forms.EmailField() - subscription_news = forms.BooleanField(required=False, label='Let me know about new updates and happenings') - subscription_artup = forms.BooleanField(required=False, label='Subscribe to updates from artists you invest in') + subscription_news = forms.BooleanField( + required=False, label="Let me know about new updates and happenings" + ) + subscription_artup = forms.BooleanField( + required=False, label="Subscribe to updates from artists you invest in" + ) subscription_all = forms.BooleanField( - required=False, label='Uncheck this box to unsubscribe from all emails from PerDiem' + required=False, + label="Uncheck this box to unsubscribe from all emails from PerDiem", ) def __init__(self, user, *args, **kwargs): @@ -130,19 +142,23 @@ def __init__(self, user, *args, **kwargs): self.user = user def clean_email(self): - email = self.cleaned_data['email'] + email = self.cleaned_data["email"] # Verify that there are no other users already with this email address if User.objects.exclude(id=self.user.id).filter(email=email).exists(): raise forms.ValidationError( - "The email address {email} already belongs to an existing user on PerDiem.".format(email=email) + "The email address {email} already belongs to an existing user on PerDiem.".format( + email=email + ) ) return email def clean(self): d = self.cleaned_data - if (d['subscription_news'] or d['subscription_artup']) and not d['subscription_all']: + if (d["subscription_news"] or d["subscription_artup"]) and not d[ + "subscription_all" + ]: raise forms.ValidationError( "You cannot subscribe to general updates or artist updates if you are unsubscribed from all emails." ) @@ -152,9 +168,9 @@ def clean(self): class ContactForm(forms.Form): INQUIRY_CHOICES = ( - ('Support', 'Support',), - ('Feedback', 'Feedback',), - ('General Inquiry', 'General Inquiry',), + ("Support", "Support"), + ("Feedback", "Feedback"), + ("General Inquiry", "General Inquiry"), ) inquiry = forms.ChoiceField(choices=INQUIRY_CHOICES) diff --git a/accounts/middleware.py b/accounts/middleware.py index 39ee15cd..b7d7149a 100644 --- a/accounts/middleware.py +++ b/accounts/middleware.py @@ -11,20 +11,19 @@ class LoginFormMiddleware(MiddlewareMixin): - def process_request(self, request): - if request.method == 'POST' and 'login-username' in request.POST: + if request.method == "POST" and "login-username" in request.POST: # Process the request as a login request # if login-username is in the POST data - form = LoginAccountForm(data=request.POST, prefix='login') + form = LoginAccountForm(data=request.POST, prefix="login") if form.is_valid(): login(request, form.get_user()) # We have to change the request method here because # the page the user is currently on might not support POST - request.method = 'GET' + request.method = "GET" else: - form = LoginAccountForm(request, prefix='login') + form = LoginAccountForm(request, prefix="login") # Add the login form to the request (accessible in context) request.login_form = form diff --git a/accounts/migrations/0001_initial.py b/accounts/migrations/0001_initial.py index 32f23fda..13112ade 100644 --- a/accounts/migrations/0001_initial.py +++ b/accounts/migrations/0001_initial.py @@ -9,17 +9,29 @@ class Migration(migrations.Migration): initial = True - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] + dependencies = [migrations.swappable_dependency(settings.AUTH_USER_MODEL)] operations = [ migrations.CreateModel( - name='UserProfile', + name="UserProfile", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('invest_anonymously', models.BooleanField(default=False)), - ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("invest_anonymously", models.BooleanField(default=False)), + ( + "user", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), ], - ), + ) ] diff --git a/accounts/migrations/0002_userprofiles.py b/accounts/migrations/0002_userprofiles.py index 6e38b8c1..3c529906 100644 --- a/accounts/migrations/0002_userprofiles.py +++ b/accounts/migrations/0002_userprofiles.py @@ -8,7 +8,7 @@ class Migration(migrations.Migration): dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('accounts', '0001_initial'), + ("accounts", "0001_initial"), ] def create_initial_userprofiles(apps, schema_editor): @@ -18,5 +18,5 @@ def create_initial_userprofiles(apps, schema_editor): UserProfile.objects.create(user=user) operations = [ - migrations.RunPython(create_initial_userprofiles, migrations.RunPython.noop), + migrations.RunPython(create_initial_userprofiles, migrations.RunPython.noop) ] diff --git a/accounts/migrations/0003_auto_20160514_0528.py b/accounts/migrations/0003_auto_20160514_0528.py index b106f99a..c1944115 100644 --- a/accounts/migrations/0003_auto_20160514_0528.py +++ b/accounts/migrations/0003_auto_20160514_0528.py @@ -9,41 +9,97 @@ class Migration(migrations.Migration): dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('accounts', '0002_userprofiles'), + ("accounts", "0002_userprofiles"), ] operations = [ migrations.CreateModel( - name='UserAvatar', + name="UserAvatar", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('provider', models.CharField(choices=[('perdiem', 'PerDiem'), ('google-oauth2', 'Google'), ('facebook', 'Facebook')], max_length=15)), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "provider", + models.CharField( + choices=[ + ("perdiem", "PerDiem"), + ("google-oauth2", "Google"), + ("facebook", "Facebook"), + ], + max_length=15, + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), ], ), migrations.CreateModel( - name='UserAvatarImage', + name="UserAvatarImage", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('img', models.ImageField(upload_to=b'')), - ('avatar', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='accounts.UserAvatar')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("img", models.ImageField(upload_to=b"")), + ( + "avatar", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + to="accounts.UserAvatar", + ), + ), ], ), migrations.CreateModel( - name='UserAvatarURL', + name="UserAvatarURL", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('url', models.URLField()), - ('avatar', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='accounts.UserAvatar')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("url", models.URLField()), + ( + "avatar", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + to="accounts.UserAvatar", + ), + ), ], ), migrations.AddField( - model_name='userprofile', - name='avatar', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='accounts.UserAvatar'), + model_name="userprofile", + name="avatar", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="accounts.UserAvatar", + ), ), migrations.AlterUniqueTogether( - name='useravatar', - unique_together=set([('user', 'provider')]), + name="useravatar", unique_together=set([("user", "provider")]) ), ] diff --git a/accounts/migrations/0004_auto_20160522_2139.py b/accounts/migrations/0004_auto_20160522_2139.py index f65a2636..60bc2cc6 100644 --- a/accounts/migrations/0004_auto_20160522_2139.py +++ b/accounts/migrations/0004_auto_20160522_2139.py @@ -6,19 +6,24 @@ class Migration(migrations.Migration): - dependencies = [ - ('accounts', '0003_auto_20160514_0528'), - ] + dependencies = [("accounts", "0003_auto_20160514_0528")] operations = [ migrations.AlterField( - model_name='useravatar', - name='provider', - field=models.CharField(choices=[('perdiem', 'Custom'), ('google-oauth2', 'Google'), ('facebook', 'Facebook')], max_length=15), + model_name="useravatar", + name="provider", + field=models.CharField( + choices=[ + ("perdiem", "Custom"), + ("google-oauth2", "Google"), + ("facebook", "Facebook"), + ], + max_length=15, + ), ), migrations.AlterField( - model_name='useravatarimage', - name='img', + model_name="useravatarimage", + name="img", field=models.ImageField(upload_to=accounts.models.user_avatar_filename), ), ] diff --git a/accounts/migrations/0005_auto_20160623_0657.py b/accounts/migrations/0005_auto_20160623_0657.py index ba749a77..74b31071 100644 --- a/accounts/migrations/0005_auto_20160623_0657.py +++ b/accounts/migrations/0005_auto_20160623_0657.py @@ -6,9 +6,7 @@ class Migration(migrations.Migration): - dependencies = [ - ('accounts', '0004_auto_20160522_2139'), - ] + dependencies = [("accounts", "0004_auto_20160522_2139")] def usernames_to_lowercase(apps, schema_editor): User = apps.get_model(settings.AUTH_USER_MODEL) @@ -17,5 +15,5 @@ def usernames_to_lowercase(apps, schema_editor): user.save() operations = [ - migrations.RunPython(usernames_to_lowercase, migrations.RunPython.noop), + migrations.RunPython(usernames_to_lowercase, migrations.RunPython.noop) ] diff --git a/accounts/migrations/0006_auto_20161115_0635.py b/accounts/migrations/0006_auto_20161115_0635.py index 419732ce..592476e6 100644 --- a/accounts/migrations/0006_auto_20161115_0635.py +++ b/accounts/migrations/0006_auto_20161115_0635.py @@ -5,14 +5,12 @@ class Migration(migrations.Migration): - dependencies = [ - ('accounts', '0005_auto_20160623_0657'), - ] + dependencies = [("accounts", "0005_auto_20160623_0657")] operations = [ migrations.AlterField( - model_name='useravatarurl', - name='url', + model_name="useravatarurl", + name="url", field=models.URLField(max_length=2000), - ), + ) ] diff --git a/accounts/migrations/0007_auto_20170528_2250.py b/accounts/migrations/0007_auto_20170528_2250.py index a93c9e85..0b49e4f3 100644 --- a/accounts/migrations/0007_auto_20170528_2250.py +++ b/accounts/migrations/0007_auto_20170528_2250.py @@ -6,14 +6,17 @@ class Migration(migrations.Migration): - dependencies = [ - ('accounts', '0006_auto_20161115_0635'), - ] + dependencies = [("accounts", "0006_auto_20161115_0635")] operations = [ migrations.AlterField( - model_name='userprofile', - name='avatar', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='accounts.UserAvatar'), - ), + model_name="userprofile", + name="avatar", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="accounts.UserAvatar", + ), + ) ] diff --git a/accounts/models.py b/accounts/models.py index 439cc292..d7e248dc 100644 --- a/accounts/models.py +++ b/accounts/models.py @@ -18,29 +18,30 @@ class UserAvatar(models.Model): - PROVIDER_PERDIEM = 'perdiem' - PROVIDER_GOOGLE = 'google-oauth2' - PROVIDER_FACEBOOK = 'facebook' + PROVIDER_PERDIEM = "perdiem" + PROVIDER_GOOGLE = "google-oauth2" + PROVIDER_FACEBOOK = "facebook" PROVIDER_CHOICES = ( - (PROVIDER_PERDIEM, 'Custom'), - (PROVIDER_GOOGLE, 'Google'), - (PROVIDER_FACEBOOK, 'Facebook'), + (PROVIDER_PERDIEM, "Custom"), + (PROVIDER_GOOGLE, "Google"), + (PROVIDER_FACEBOOK, "Facebook"), ) user = models.ForeignKey(User, on_delete=models.CASCADE) provider = models.CharField(choices=PROVIDER_CHOICES, max_length=15) class Meta: - unique_together = (('user', 'provider',),) + unique_together = (("user", "provider"),) @staticmethod def default_avatar_url(): - return "{static_url}img/perdiem-avatar.svg".format(static_url=settings.STATIC_URL) + return "{static_url}img/perdiem-avatar.svg".format( + static_url=settings.STATIC_URL + ) def __str__(self): - return u'{user}: {provider}'.format( - user=str(self.user), - provider=self.get_provider_display() + return u"{user}: {provider}".format( + user=str(self.user), provider=self.get_provider_display() ) def avatar_url(self): @@ -48,7 +49,7 @@ def avatar_url(self): return self.useravatarurl.url elif self.provider == self.PROVIDER_PERDIEM: original = self.useravatarimage.img - return get_thumbnail(original, '150x150', crop='center').url + return get_thumbnail(original, "150x150", crop="center").url else: return self.default_avatar_url() @@ -63,12 +64,11 @@ def __str__(self): def user_avatar_filename(instance, filename): - extension = filename.split('.')[-1] - new_filename = '{user_id}.{extension}'.format( - user_id=instance.avatar.user.id, - extension=extension + extension = filename.split(".")[-1] + new_filename = "{user_id}.{extension}".format( + user_id=instance.avatar.user.id, extension=extension ) - return '/'.join(['avatars', new_filename]) + return "/".join(["avatars", new_filename]) class UserAvatarImage(models.Model): @@ -83,7 +83,9 @@ def __str__(self): class UserProfile(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE) - avatar = models.ForeignKey(UserAvatar, on_delete=models.SET_NULL, null=True, blank=True) + avatar = models.ForeignKey( + UserAvatar, on_delete=models.SET_NULL, null=True, blank=True + ) invest_anonymously = models.BooleanField(default=False) @staticmethod @@ -97,7 +99,7 @@ def __str__(self): def get_display_name(self): if self.invest_anonymously: - return 'Anonymous' + return "Anonymous" else: return self.user.get_full_name() or self.user.username @@ -113,7 +115,7 @@ def display_avatar_url(self): def public_profile_url(self): if not self.invest_anonymously: - return reverse('public_profile', args=(self.user.username,)) + return reverse("public_profile", args=(self.user.username,)) @cache_using_pk def profile_context(self): @@ -121,22 +123,22 @@ def profile_context(self): # Get artists the user has invested in investments = Investment.objects.filter( - charge__customer__user=self.user, - charge__paid=True, - charge__refunded=False + charge__customer__user=self.user, charge__paid=True, charge__refunded=False ) - campaign_ids = investments.values_list('campaign', flat=True).distinct() - campaigns = Campaign.objects.filter(id__in=campaign_ids).select_related('project') - context['campaigns'] = campaigns - artist_ids = campaigns.values_list('project__artist', flat=True).distinct() + campaign_ids = investments.values_list("campaign", flat=True).distinct() + campaigns = Campaign.objects.filter(id__in=campaign_ids).select_related( + "project" + ) + context["campaigns"] = campaigns + artist_ids = campaigns.values_list("project__artist", flat=True).distinct() artists = Artist.objects.filter(id__in=artist_ids) - context['artists'] = dict(map(self.prepare_artist_for_profile_context, artists)) + context["artists"] = dict(map(self.prepare_artist_for_profile_context, artists)) # Update context with total investments aggregate_context = investments.aggregate( total_investments=models.Sum( - models.F('campaign__value_per_share') * models.F('num_shares'), - output_field=models.FloatField() + models.F("campaign__value_per_share") * models.F("num_shares"), + output_field=models.FloatField(), ) ) context.update(aggregate_context) @@ -148,23 +150,27 @@ def profile_context(self): # Total invested investments_this_campaign = investments.filter(campaign=campaign) - num_shares_this_campaign = investments_this_campaign.aggregate(ns=models.Sum('num_shares'))['ns'] - context['artists'][artist.id].total_invested += num_shares_this_campaign * campaign.value_per_share + num_shares_this_campaign = investments_this_campaign.aggregate( + ns=models.Sum("num_shares") + )["ns"] + context["artists"][artist.id].total_invested += ( + num_shares_this_campaign * campaign.value_per_share + ) # Total earned generated_revenue_user = 0 for investment in investments_this_campaign: generated_revenue_user += investment.generated_revenue() - context['artists'][artist.id].total_earned += generated_revenue_user + context["artists"][artist.id].total_earned += generated_revenue_user total_earned += generated_revenue_user - context['total_earned'] = total_earned + context["total_earned"] = total_earned # Add percentage of return to context - total_investments = aggregate_context['total_investments'] or 0 + total_investments = aggregate_context["total_investments"] or 0 try: percentage = total_earned / total_investments * 100 except ZeroDivisionError: percentage = 0 - context['percentage'] = percentage + context["percentage"] = percentage return context diff --git a/accounts/pipeline.py b/accounts/pipeline.py index 388dc5c7..7337eac8 100644 --- a/accounts/pipeline.py +++ b/accounts/pipeline.py @@ -14,24 +14,22 @@ def require_email(strategy, details, user=None, is_new=False, *args, **kwargs): - if not details.get('email'): - return HttpResponseRedirect(reverse('error_email_required')) + if not details.get("email"): + return HttpResponseRedirect(reverse("error_email_required")) def verify_auth_operation(strategy, details, user=None, is_new=False, *args, **kwargs): - auth_operation = kwargs['backend'].auth_operation - if user and auth_operation == 'register': - return HttpResponseRedirect(reverse('error_account_exists')) - elif not user and auth_operation == 'login': - return HttpResponseRedirect(reverse('error_account_does_not_exist')) + auth_operation = kwargs["backend"].auth_operation + if user and auth_operation == "register": + return HttpResponseRedirect(reverse("error_account_exists")) + elif not user and auth_operation == "login": + return HttpResponseRedirect(reverse("error_account_does_not_exist")) def mark_email_verified(strategy, details, user=None, is_new=False, *args, **kwargs): if user: VerifiedEmail.objects.update_or_create( - defaults={'verified': True}, - user=user, - email=details['email'] + defaults={"verified": True}, user=user, email=details["email"] ) @@ -41,14 +39,14 @@ def save_avatar(strategy, details, user=None, is_new=False, *args, **kwargs): return # Get avatar from provider, skip if no avatar - provider = kwargs['backend'].name.replace('-login', '').replace('-register', '') + provider = kwargs["backend"].name.replace("-login", "").replace("-register", "") try: - if provider == 'google-oauth2': - avatar = kwargs['response']['image'] - is_default_avatar = avatar['isDefault'] - elif provider == 'facebook': - avatar = kwargs['response']['picture']['data'] - is_default_avatar = avatar['is_silhouette'] + if provider == "google-oauth2": + avatar = kwargs["response"]["image"] + is_default_avatar = avatar["isDefault"] + elif provider == "facebook": + avatar = kwargs["response"]["picture"]["data"] + is_default_avatar = avatar["is_silhouette"] else: return except KeyError: @@ -60,17 +58,21 @@ def save_avatar(strategy, details, user=None, is_new=False, *args, **kwargs): # Get avatar URL from provider try: - avatar_url = avatar['url'] + avatar_url = avatar["url"] except KeyError: return # For Google, use larger image than default - if provider == 'google-oauth2': - avatar_url = avatar_url.replace('?sz=50', '?sz=150') + if provider == "google-oauth2": + avatar_url = avatar_url.replace("?sz=50", "?sz=150") # Save avatar URL - user_avatar, created = UserAvatar.objects.get_or_create(user=user, provider=provider) - user_avatar_url, _ = UserAvatarURL.objects.update_or_create(avatar=user_avatar, defaults={'url': avatar_url}) + user_avatar, created = UserAvatar.objects.get_or_create( + user=user, provider=provider + ) + user_avatar_url, _ = UserAvatarURL.objects.update_or_create( + avatar=user_avatar, defaults={"url": avatar_url} + ) # Update user's current avatar if none was ever set if created and not user.userprofile.avatar: diff --git a/accounts/signals.py b/accounts/signals.py index 4b2310dd..bbccbf80 100644 --- a/accounts/signals.py +++ b/accounts/signals.py @@ -11,10 +11,12 @@ from accounts.models import UserProfile -@receiver(models.signals.post_save, sender=User, dispatch_uid="post_user_create_handler") +@receiver( + models.signals.post_save, sender=User, dispatch_uid="post_user_create_handler" +) def post_user_create_handler(sender, **kwargs): - user = kwargs['instance'] - created = kwargs['created'] + user = kwargs["instance"] + created = kwargs["created"] if created: UserProfile.objects.create(user=user) diff --git a/accounts/tests.py b/accounts/tests.py index 87ef70a0..2e27a13f 100644 --- a/accounts/tests.py +++ b/accounts/tests.py @@ -23,29 +23,33 @@ class CreateInitialUserProfilesMigrationTestCase(MigrationTestCase): - migrate_from = '0001_initial' - migrate_to = '0002_userprofiles' + migrate_from = "0001_initial" + migrate_to = "0002_userprofiles" def setUpBeforeMigration(self, apps): # Create a user - UserFactoryForMigrationTestCase = userfactory_factory(apps=apps, has_password=False) + UserFactoryForMigrationTestCase = userfactory_factory( + apps=apps, has_password=False + ) self.user = UserFactoryForMigrationTestCase() def testUsersHaveUserProfiles(self): - UserProfile = self.apps.get_model('accounts', 'UserProfile') + UserProfile = self.apps.get_model("accounts", "UserProfile") self.assertEqual(UserProfile.objects.get().user.id, self.user.id) class UsernamesToLowercaseMigrationTestCase(MigrationTestCase): - USERNAME = 'JSmith' + USERNAME = "JSmith" - migrate_from = '0004_auto_20160522_2139' - migrate_to = '0005_auto_20160623_0657' + migrate_from = "0004_auto_20160522_2139" + migrate_to = "0005_auto_20160623_0657" def setUpBeforeMigration(self, apps): # Create a user with a username that has uppercase characters - UserFactoryForMigrationTestCase = userfactory_factory(apps=apps, has_password=False) + UserFactoryForMigrationTestCase = userfactory_factory( + apps=apps, has_password=False + ) self.user = UserFactoryForMigrationTestCase(username=self.USERNAME) def testUsernamesAreLowercase(self): @@ -54,140 +58,138 @@ def testUsernamesAreLowercase(self): class AuthWebTestCase(PerDiemTestCase): - def get200s(self): return [ - '/', - '/accounts/register/', - '/accounts/password/reset/', - '/accounts/password/reset/0/0-0/', - '/accounts/password/reset/complete/', + "/", + "/accounts/register/", + "/accounts/password/reset/", + "/accounts/password/reset/0/0-0/", + "/accounts/password/reset/complete/", ] def testHomePageUnauthenticated(self): self.client.logout() - self.assertResponseRenders('/') + self.assertResponseRenders("/") def testLogout(self): - self.assertResponseRedirects('/accounts/logout/', '/') + self.assertResponseRedirects("/accounts/logout/", "/") def testLogin(self): self.client.logout() login_data = { - 'login-username': self.USER_USERNAME, - 'login-password': UserFactory._PASSWORD, + "login-username": self.USER_USERNAME, + "login-password": UserFactory._PASSWORD, } - response = self.assertResponseRenders('/', method='POST', data=login_data) - self.assertIn(b'LOGOUT', response.content) + response = self.assertResponseRenders("/", method="POST", data=login_data) + self.assertIn(b"LOGOUT", response.content) def testLoginWithUppercaseUsername(self): self.client.logout() login_data = { - 'login-username': self.USER_USERNAME.upper(), - 'login-password': UserFactory._PASSWORD, + "login-username": self.USER_USERNAME.upper(), + "login-password": UserFactory._PASSWORD, } - response = self.assertResponseRenders('/', method='POST', data=login_data) - self.assertIn(b'LOGOUT', response.content) + response = self.assertResponseRenders("/", method="POST", data=login_data) + self.assertIn(b"LOGOUT", response.content) def testRegister(self): self.client.logout() self.assertResponseRedirects( - '/accounts/register/', - '/profile', - method='POST', + "/accounts/register/", + "/profile", + method="POST", data={ - 'username': 'msmith', - 'email': 'msmith@example.com', - 'password1': UserFactory._PASSWORD, - 'password2': UserFactory._PASSWORD, - } + "username": "msmith", + "email": "msmith@example.com", + "password1": UserFactory._PASSWORD, + "password2": UserFactory._PASSWORD, + }, ) def testRegisterUsernameMustBeLowercase(self): self.client.logout() self.assertResponseRenders( - '/accounts/register/', - method='POST', + "/accounts/register/", + method="POST", data={ - 'username': 'Msmith', - 'email': 'msmith@example.com', - 'password1': UserFactory._PASSWORD, - 'password2': UserFactory._PASSWORD, + "username": "Msmith", + "email": "msmith@example.com", + "password1": UserFactory._PASSWORD, + "password2": UserFactory._PASSWORD, }, - has_form_error=True + has_form_error=True, ) def testPasswordReset(self): self.client.logout() self.assertResponseRedirects( - '/accounts/password/reset/', - '/accounts/password/reset/sent', - method='POST', - data={'email': self.user.email} + "/accounts/password/reset/", + "/accounts/password/reset/sent", + method="POST", + data={"email": self.user.email}, ) class OAuth2TestCase(PerDiemTestCase): - - @mock.patch('social_core.backends.google.BaseGoogleOAuth2API.user_data') - @mock.patch('social_core.backends.oauth.BaseOAuth2.request_access_token') + @mock.patch("social_core.backends.google.BaseGoogleOAuth2API.user_data") + @mock.patch("social_core.backends.oauth.BaseOAuth2.request_access_token") def testGoogleOAuth2Login(self, mock_request_access_token, mock_user_data): - mock_request_access_token.return_value = {'access_token': 'abc123'} + mock_request_access_token.return_value = {"access_token": "abc123"} mock_user_data.return_value = { - u'access_token': u'ya29.abc123', - u'expires_in': 3600, - u'scope': u' '.join([ - u'https://www.googleapis.com/auth/plus.me', - u'https://www.googleapis.com/auth/userinfo.profile', - u'https://www.googleapis.com/auth/userinfo.email', - ]), - u'token_type': u'Bearer', - u'id_token': u'abc123', - u'sub': u'1234', - u'name': u'John Smith', - u'given_name': u'John', - u'family_name': u'Smith', - u'profile': u'https://plus.google.com/+JohnSmith', - u'picture': u'https://lh4.googleusercontent.com/abc123/photo.jpg?sz=50', - u'email': u'jsmith@example.com', - u'email_verified': True, - u'gender': u'male', - u'locale': u'en', + u"access_token": u"ya29.abc123", + u"expires_in": 3600, + u"scope": u" ".join( + [ + u"https://www.googleapis.com/auth/plus.me", + u"https://www.googleapis.com/auth/userinfo.profile", + u"https://www.googleapis.com/auth/userinfo.email", + ] + ), + u"token_type": u"Bearer", + u"id_token": u"abc123", + u"sub": u"1234", + u"name": u"John Smith", + u"given_name": u"John", + u"family_name": u"Smith", + u"profile": u"https://plus.google.com/+JohnSmith", + u"picture": u"https://lh4.googleusercontent.com/abc123/photo.jpg?sz=50", + u"email": u"jsmith@example.com", + u"email_verified": True, + u"gender": u"male", + u"locale": u"en", } # Click on "Sign in with Google" button self.client.logout() - allowed_hosts_w_google = settings.ALLOWED_HOSTS + ['accounts.google.com'] + allowed_hosts_w_google = settings.ALLOWED_HOSTS + ["accounts.google.com"] with override_settings(ALLOWED_HOSTS=allowed_hosts_w_google): self.assertResponseRedirects( - '/login/google-oauth2-login/', - 'https://accounts.google.com/o/oauth2/auth', - status_code=404 # The actual page will fail since the client ID used in tests does not exist + "/login/google-oauth2-login/", + "https://accounts.google.com/o/oauth2/auth", + status_code=404, # The actual page will fail since the client ID used in tests does not exist ) # The user auths and consents # Google redirects to our redirect URI - state = Session.objects.get().get_decoded()['google-oauth2-login_state'] + state = Session.objects.get().get_decoded()["google-oauth2-login_state"] self.assertResponseRedirects( - '/complete/google-oauth2-login/?state={state}&code=4/abc123&session_state={state}'.format(state=state), - '/profile/' + "/complete/google-oauth2-login/?state={state}&code=4/abc123&session_state={state}".format( + state=state + ), + "/profile/", ) class ProfileWebTestCase(PerDiemTestCase): - def get200s(self): - return [ - '/profile/', - '/profile/{username}/'.format(username=self.user.username), - ] + return ["/profile/", "/profile/{username}/".format(username=self.user.username)] def testUserProfileContextCaches(self): # Request the profile context for a user self.user.userprofile.profile_context() # Verify that this user's profile context is in cache - self.assertIn('profile_context-{pk}'.format(pk=self.user.userprofile.pk), cache) + self.assertIn("profile_context-{pk}".format(pk=self.user.userprofile.pk), cache) # Create a new RevenueReport # We cannot use a factory to generate the RevenueReport here @@ -195,11 +197,18 @@ def testUserProfileContextCaches(self): RevenueReport.objects.create(project=ProjectFactory(), amount=100) # Verify that the user profile context is no longer in cache - self.assertNotIn('profile_context-{pk}'.format(pk=self.user.userprofile.pk), cache) + self.assertNotIn( + "profile_context-{pk}".format(pk=self.user.userprofile.pk), cache + ) def testUserProfileContextContainsInvestments(self): investment = InvestmentFactory() - self.assertGreater(investment.charge.customer.user.userprofile.profile_context()['total_investments'], 0) + self.assertGreater( + investment.charge.customer.user.userprofile.profile_context()[ + "total_investments" + ], + 0, + ) def testInvalidProfilesAndAnonymousProfilesLookIdentical(self): # Create a user that will invest anonymously @@ -208,27 +217,29 @@ def testInvalidProfilesAndAnonymousProfilesLookIdentical(self): anonymous_user.userprofile.save() # Get HTML from an invalid profile - invalid_profile_url = '/profile/does-not-exist/' + invalid_profile_url = "/profile/does-not-exist/" invalid_profile_response = self.assertResponseRenders( - invalid_profile_url, - status_code=404 + invalid_profile_url, status_code=404 + ) + invalid_profile_html = invalid_profile_response.content.decode("utf-8").replace( + invalid_profile_url, "" ) - invalid_profile_html = invalid_profile_response.content.decode('utf-8').replace(invalid_profile_url, '') # Get HTML from an anonymous profile - anonymous_profile_url = '/profile/{anonymous_username}/'.format( + anonymous_profile_url = "/profile/{anonymous_username}/".format( anonymous_username=anonymous_user.username ) anonymous_profile_response = self.assertResponseRenders( - anonymous_profile_url, - status_code=404 + anonymous_profile_url, status_code=404 ) - anonymous_profile_html = anonymous_profile_response.content.decode('utf-8').replace(anonymous_profile_url, '') + anonymous_profile_html = anonymous_profile_response.content.decode( + "utf-8" + ).replace(anonymous_profile_url, "") # Remove CSRF tokens from profiles - csrf_regex = r']+csrfmiddlewaretoken[^>]+>' - invalid_profile_html = re.sub(csrf_regex, '', invalid_profile_html) - anonymous_profile_html = re.sub(csrf_regex, '', anonymous_profile_html) + csrf_regex = r"]+csrfmiddlewaretoken[^>]+>" + invalid_profile_html = re.sub(csrf_regex, "", invalid_profile_html) + anonymous_profile_html = re.sub(csrf_regex, "", anonymous_profile_html) # Verify that the HTML from these two different pages are identical self.assertHTMLEqual(invalid_profile_html, anonymous_profile_html) @@ -237,14 +248,14 @@ def testRedirectToProfile(self): # Redirect to artist details artist = ArtistFactory() self.assertResponseRedirects( - '/{artist_slug}/'.format(artist_slug=artist.slug), - '/artist/{artist_slug}'.format(artist_slug=artist.slug) + "/{artist_slug}/".format(artist_slug=artist.slug), + "/artist/{artist_slug}".format(artist_slug=artist.slug), ) # Redirect to user's public profile self.assertResponseRedirects( - '/{username}/'.format(username=self.user.username), - '/profile/{username}'.format(username=self.user.username) + "/{username}/".format(username=self.user.username), + "/profile/{username}".format(username=self.user.username), ) # Create a new user that matches the artist slug @@ -252,88 +263,86 @@ def testRedirectToProfile(self): # We still redirect to the artist details self.assertResponseRedirects( - '/{username}/'.format(username=user_with_artist_username.username), - '/artist/{artist_slug}'.format(artist_slug=artist.slug) + "/{username}/".format(username=user_with_artist_username.username), + "/artist/{artist_slug}".format(artist_slug=artist.slug), ) def testRedirectToProfileDoesNotExistReturns404(self): - self.assertResponseRenders('/does-not-exist/', status_code=404) + self.assertResponseRenders("/does-not-exist/", status_code=404) class SettingsWebTestCase(PerDiemTestCase): - def get200s(self): - return [ - '/accounts/settings/', - ] + return ["/accounts/settings/"] def testEditUsernameMustBeLowercase(self): self.assertResponseRenders( - '/accounts/settings/', - method='POST', + "/accounts/settings/", + method="POST", data={ - 'action': 'edit_name', - 'username': self.USER_USERNAME.upper(), - 'invest_anonymously': False, + "action": "edit_name", + "username": self.USER_USERNAME.upper(), + "invest_anonymously": False, }, - has_form_error=True + has_form_error=True, ) def testEditName(self): self.assertResponseRenders( - '/accounts/settings/', - method='POST', + "/accounts/settings/", + method="POST", data={ - 'action': 'edit_name', - 'username': self.USER_USERNAME, - 'first_name': self.user.first_name, - 'last_name': self.user.last_name, - 'invest_anonymously': False, - } + "action": "edit_name", + "username": self.USER_USERNAME, + "first_name": self.user.first_name, + "last_name": self.user.last_name, + "invest_anonymously": False, + }, ) def testEditAvatar(self): user_avatar = UserAvatarFactory(user=self.user) self.assertResponseRenders( - '/accounts/settings/', - method='POST', - data={ - 'action': 'edit_avatar', - 'avatar': user_avatar.id, - } + "/accounts/settings/", + method="POST", + data={"action": "edit_avatar", "avatar": user_avatar.id}, ) def testChangePassword(self): self.assertResponseRenders( - '/accounts/settings/', - method='POST', + "/accounts/settings/", + method="POST", data={ - 'action': 'change_password', - 'old_password': UserFactory._PASSWORD, - 'new_password1': 'abc1234', - 'new_password2': 'abc1234', - } + "action": "change_password", + "old_password": UserFactory._PASSWORD, + "new_password1": "abc1234", + "new_password2": "abc1234", + }, ) - @mock.patch('emails.mailchimp.requests.put') - @override_settings(MAILCHIMP_API_KEY='FAKE_API_KEY', MAILCHIMP_LIST_ID='FAKE_LIST_ID') + @mock.patch("emails.mailchimp.requests.put") + @override_settings( + MAILCHIMP_API_KEY="FAKE_API_KEY", MAILCHIMP_LIST_ID="FAKE_LIST_ID" + ) def testUpdateEmailPreferences(self, mock_mailchimp_request): mock_mailchimp_request.return_value = mock.Mock(status_code=200) self.assertResponseRenders( - '/accounts/settings/', - method='POST', + "/accounts/settings/", + method="POST", data={ - 'action': 'email_preferences', - 'email': self.user.email, - 'subscription_all': True, - 'subscription_news': False, - 'subscription_artist_update': True, - } + "action": "email_preferences", + "email": self.user.email, + "subscription_all": True, + "subscription_news": False, + "subscription_artist_update": True, + }, ) - @mock.patch('emails.mailchimp.requests.put') - @override_settings(MAILCHIMP_API_KEY='FAKE_API_KEY', MAILCHIMP_LIST_ID='FAKE_LIST_ID') + @mock.patch("emails.mailchimp.requests.put") + @override_settings( + MAILCHIMP_API_KEY="FAKE_API_KEY", MAILCHIMP_LIST_ID="FAKE_LIST_ID" + ) def testUpdateEmailAddress(self, mock_mailchimp_request): mock_mailchimp_request.return_value = mock.Mock(status_code=200) @@ -344,12 +353,9 @@ def testUpdateEmailAddress(self, mock_mailchimp_request): # Update email address to something new self.assertResponseRenders( - '/accounts/settings/', - method='POST', - data={ - 'action': 'email_preferences', - 'email': 'newemail@example.com', - } + "/accounts/settings/", + method="POST", + data={"action": "email_preferences", "email": "newemail@example.com"}, ) # Check that new email is not verified @@ -359,11 +365,8 @@ def testUpdateEmailAddress(self, mock_mailchimp_request): def testCannotChangeEmailToExistingAccount(self): other_user = UserFactory() self.assertResponseRenders( - '/accounts/settings/', - method='POST', - data={ - 'action': 'email_preferences', - 'email': other_user.email, - }, - has_form_error=True + "/accounts/settings/", + method="POST", + data={"action": "email_preferences", "email": other_user.email}, + has_form_error=True, ) diff --git a/accounts/urls.py b/accounts/urls.py index 75a72f5f..f7d4fa37 100644 --- a/accounts/urls.py +++ b/accounts/urls.py @@ -6,7 +6,11 @@ from django.conf.urls import url from django.contrib.auth.views import ( - LogoutView, PasswordResetCompleteView, PasswordResetConfirmView, PasswordResetDoneView, PasswordResetView + LogoutView, + PasswordResetCompleteView, + PasswordResetConfirmView, + PasswordResetDoneView, + PasswordResetView, ) from django.views.generic import TemplateView @@ -14,32 +18,40 @@ urlpatterns = [ - url(r'^logout/?$', LogoutView.as_view(next_page='/'), name='logout'), - url(r'^register/?$', RegisterAccountView.as_view(), name='register'), - url(r'^settings/?$', SettingsView.as_view(), name='settings'), - - url(r'^password/reset/sent/?$', PasswordResetDoneView.as_view(), name='password_reset_done'), - url(r'^password/reset/complete/?$', PasswordResetCompleteView.as_view(), name='password_reset_complete'), + url(r"^logout/?$", LogoutView.as_view(next_page="/"), name="logout"), + url(r"^register/?$", RegisterAccountView.as_view(), name="register"), + url(r"^settings/?$", SettingsView.as_view(), name="settings"), + url( + r"^password/reset/sent/?$", + PasswordResetDoneView.as_view(), + name="password_reset_done", + ), url( - r'^password/reset/(?P[0-9A-Za-z_-]+)/(?P[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/?$', + r"^password/reset/complete/?$", + PasswordResetCompleteView.as_view(), + name="password_reset_complete", + ), + url( + r"^password/reset/(?P[0-9A-Za-z_-]+)/(?P[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/?$", PasswordResetConfirmView.as_view(), - name='password_reset_confirm' + name="password_reset_confirm", ), - url(r'^password/reset/?$', PasswordResetView.as_view(), name='password_reset'), - + url(r"^password/reset/?$", PasswordResetView.as_view(), name="password_reset"), url( - r'^error/email-required/?$', - TemplateView.as_view(template_name='registration/error/email-required.html'), - name='error_email_required' + r"^error/email-required/?$", + TemplateView.as_view(template_name="registration/error/email-required.html"), + name="error_email_required", ), url( - r'^error/account-exists/?$', - TemplateView.as_view(template_name='registration/error/account-exists.html'), - name='error_account_exists' + r"^error/account-exists/?$", + TemplateView.as_view(template_name="registration/error/account-exists.html"), + name="error_account_exists", ), url( - r'^error/account-does-not-exist/?$', - TemplateView.as_view(template_name='registration/error/account-does-not-exist.html'), - name='error_account_does_not_exist' + r"^error/account-does-not-exist/?$", + TemplateView.as_view( + template_name="registration/error/account-does-not-exist.html" + ), + name="error_account_does_not_exist", ), ] diff --git a/accounts/views.py b/accounts/views.py index 140ef43e..08775e50 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -15,8 +15,11 @@ from django.views.generic.edit import CreateView, FormView from accounts.forms import ( - RegisterAccountForm, EditNameForm, EditAvatarForm, EmailPreferencesForm, - ContactForm + RegisterAccountForm, + EditNameForm, + EditAvatarForm, + EmailPreferencesForm, + ContactForm, ) from accounts.models import UserAvatar, UserAvatarImage from artist.models import Artist, Update @@ -28,25 +31,27 @@ class RegisterAccountView(CreateView): - template_name = 'registration/register.html' + template_name = "registration/register.html" form_class = RegisterAccountForm def get_success_url(self): - return self.request.GET.get('next') or reverse('profile') + return self.request.GET.get("next") or reverse("profile") def form_valid(self, form): valid = super(RegisterAccountView, self).form_valid(form) # Login the newly-registered user d = form.cleaned_data - username, password = d['username'], d['password1'] + username, password = d["username"], d["password1"] user = authenticate(username=username, password=password) if user: login(self.request, user) # Create the user's newsletter subscription (if applicable) - if d['subscribe_news']: - EmailSubscription.objects.create(user=user, subscription=EmailSubscription.SUBSCRIPTION_NEWS) + if d["subscribe_news"]: + EmailSubscription.objects.create( + user=user, subscription=EmailSubscription.SUBSCRIPTION_NEWS + ) # Send Welcome email VerifiedEmail.objects.create(user=user, email=user.email) @@ -57,15 +62,17 @@ def form_valid(self, form): class VerifyEmailView(TemplateView): - template_name = 'registration/verify_email.html' + template_name = "registration/verify_email.html" def get_context_data(self, **kwargs): context = super(VerifyEmailView, self).get_context_data(**kwargs) - verified_email = get_object_or_404(VerifiedEmail, user__id=kwargs['user_id'], code=kwargs['code']) + verified_email = get_object_or_404( + VerifiedEmail, user__id=kwargs["user_id"], code=kwargs["code"] + ) verified_email.verified = True verified_email.save() - context['verified_email'] = verified_email + context["verified_email"] = verified_email return context @@ -78,10 +85,10 @@ class EditNameFormView(ConstituentFormView): def get_initial(self): user = self.request.user return { - 'username': user.username, - 'first_name': user.first_name, - 'last_name': user.last_name, - 'invest_anonymously': user.userprofile.invest_anonymously, + "username": user.username, + "first_name": user.first_name, + "last_name": user.last_name, + "invest_anonymously": user.userprofile.invest_anonymously, } def form_valid(self, form): @@ -89,13 +96,13 @@ def form_valid(self, form): d = form.cleaned_data # Update username and name - user.username = d['username'] - user.first_name = d['first_name'] - user.last_name = d['last_name'] + user.username = d["username"] + user.first_name = d["first_name"] + user.last_name = d["last_name"] user.save() # Update anonymity - user.userprofile.invest_anonymously = d['invest_anonymously'] + user.userprofile.invest_anonymously = d["invest_anonymously"] user.userprofile.save() @@ -107,20 +114,22 @@ class EditAvatarFormView(ConstituentFormView): def get_initial(self): user_profile = self.request.user.userprofile - return { - 'avatar': user_profile.avatar.id if user_profile.avatar else '', - } + return {"avatar": user_profile.avatar.id if user_profile.avatar else ""} def form_valid(self, form): user = self.request.user d = form.cleaned_data # Upload a custom avatar, if provided - user_avatar = d['avatar'] - custom_avatar = d['custom_avatar'] + user_avatar = d["avatar"] + custom_avatar = d["custom_avatar"] if custom_avatar: - user_avatar, _ = UserAvatar.objects.get_or_create(user=user, provider=UserAvatar.PROVIDER_PERDIEM) - UserAvatarImage.objects.update_or_create(avatar=user_avatar, defaults={'img': custom_avatar}) + user_avatar, _ = UserAvatar.objects.get_or_create( + user=user, provider=UserAvatar.PROVIDER_PERDIEM + ) + UserAvatarImage.objects.update_or_create( + avatar=user_avatar, defaults={"img": custom_avatar} + ) # Update user's avatar user.userprofile.avatar = user_avatar @@ -137,7 +146,7 @@ def form_valid(self, form): d = form.cleaned_data # Update user's password - user.set_password(d['new_password1']) + user.set_password(d["new_password1"]) user.save() update_session_auth_hash(self.request, user) @@ -148,15 +157,14 @@ class EmailPreferencesFormView(ConstituentFormView): provide_user = True def get_initial(self): - initial = { - 'email': self.request.user.email, - } + initial = {"email": self.request.user.email} for subscription_type, _ in EmailSubscription.SUBSCRIPTION_CHOICES: subscribed = EmailSubscription.objects.is_subscribed( - user=self.request.user, - subscription_type=subscription_type + user=self.request.user, subscription_type=subscription_type ) - initial['subscription_{stype}'.format(stype=subscription_type.lower())] = subscribed + initial[ + "subscription_{stype}".format(stype=subscription_type.lower()) + ] = subscribed return initial def form_valid(self, form): @@ -164,17 +172,19 @@ def form_valid(self, form): d = form.cleaned_data # Update user's email subscriptions - email_subscriptions = {k: v for k, v in d.items() if k.startswith('subscription_')} + email_subscriptions = { + k: v for k, v in d.items() if k.startswith("subscription_") + } for subscription_type, is_subscribed in email_subscriptions.items(): EmailSubscription.objects.update_or_create( user=user, subscription=getattr(EmailSubscription, subscription_type.upper()), - defaults={'subscribed': is_subscribed} + defaults={"subscribed": is_subscribed}, ) # Update user's email address - if d['email'] != user.email: - user.email = d['email'] + if d["email"] != user.email: + user.email = d["email"] user.save() if not VerifiedEmail.objects.is_current_email_verified(user): EmailVerificationEmail().send(user=user) @@ -182,12 +192,12 @@ def form_valid(self, form): class SettingsView(LoginRequiredMixin, MultipleFormView): - template_name = 'registration/settings.html' + template_name = "registration/settings.html" constituent_form_views = { - 'edit_name': EditNameFormView, - 'edit_avatar': EditAvatarFormView, - 'change_password': ChangePasswordFormView, - 'email_preferences': EmailPreferencesFormView, + "edit_name": EditNameFormView, + "edit_avatar": EditAvatarFormView, + "change_password": ChangePasswordFormView, + "email_preferences": EmailPreferencesFormView, } def get_context_data(self, **kwargs): @@ -195,61 +205,70 @@ def get_context_data(self, **kwargs): # Update context with available avatars user_avatars = UserAvatar.objects.filter(user=self.request.user) - avatars = { - 'Default': UserAvatar.default_avatar_url(), - } - avatars.update({avatar.get_provider_display(): avatar.avatar_url() for avatar in user_avatars}) - context['avatars'] = avatars + avatars = {"Default": UserAvatar.default_avatar_url()} + avatars.update( + { + avatar.get_provider_display(): avatar.avatar_url() + for avatar in user_avatars + } + ) + context["avatars"] = avatars return context class ProfileView(LoginRequiredMixin, TemplateView): - template_name = 'registration/profile.html' + template_name = "registration/profile.html" def get_context_data(self, **kwargs): context = super(ProfileView, self).get_context_data(**kwargs) # Update context with profile information context.update(self.request.user.userprofile.profile_context()) - context['albums'] = Album.objects.filter(project__campaign__in=context['campaigns']).distinct() - context['updates'] = Update.objects.filter(artist__in=context['artists']).order_by('-created_datetime') + context["albums"] = Album.objects.filter( + project__campaign__in=context["campaigns"] + ).distinct() + context["updates"] = Update.objects.filter( + artist__in=context["artists"] + ).order_by("-created_datetime") return context class PublicProfileView(TemplateView): - template_name = 'registration/public_profile.html' + template_name = "registration/public_profile.html" def get_context_data(self, **kwargs): context = super(PublicProfileView, self).get_context_data(**kwargs) - profile_user = get_object_or_404(User, username=kwargs['username']) + profile_user = get_object_or_404(User, username=kwargs["username"]) if profile_user.userprofile.invest_anonymously: raise Http404("No User matches the given query.") - context.update({ - 'profile_user': profile_user, - 'profile': profile_user.userprofile.profile_context(), - }) + context.update( + { + "profile_user": profile_user, + "profile": profile_user.userprofile.profile_context(), + } + ) return context class ContactFormView(FormView): - template_name = 'registration/contact.html' + template_name = "registration/contact.html" form_class = ContactForm def get_success_url(self): - return reverse('contact_thanks') + return reverse("contact_thanks") def get_initial(self): initial = super(ContactFormView, self).get_initial() user = self.request.user if user.is_authenticated: - initial['email'] = user.email - initial['first_name'] = user.first_name - initial['last_name'] = user.last_name + initial["email"] = user.email + initial["first_name"] = user.first_name + initial["last_name"] = user.last_name return initial def form_valid(self, form): @@ -257,10 +276,10 @@ def form_valid(self, form): context = form.cleaned_data user = self.request.user if user.is_authenticated: - context['user_id'] = user.id + context["user_id"] = user.id # Send contact email - ContactEmail().send_to_email(email='support@investperdiem.com', context=context) + ContactEmail().send_to_email(email="support@investperdiem.com", context=context) return super(ContactFormView, self).form_valid(form) @@ -272,12 +291,16 @@ def redirect_to_profile(request, slug): except Artist.DoesNotExist: pass else: - return HttpResponseRedirect(reverse('artist', kwargs={'slug': artist.slug})) + return HttpResponseRedirect(reverse("artist", kwargs={"slug": artist.slug})) # Try matching the slug to a public profile try: - user = User.objects.exclude(userprofile__invest_anonymously=True).get(username=slug) + user = User.objects.exclude(userprofile__invest_anonymously=True).get( + username=slug + ) except User.DoesNotExist: raise Http404 else: - return HttpResponseRedirect(reverse('public_profile', kwargs={'username': user.username})) + return HttpResponseRedirect( + reverse("public_profile", kwargs={"username": user.username}) + ) diff --git a/api/apps.py b/api/apps.py index d87006dd..14b89a82 100644 --- a/api/apps.py +++ b/api/apps.py @@ -2,4 +2,4 @@ class ApiConfig(AppConfig): - name = 'api' + name = "api" diff --git a/api/tests.py b/api/tests.py index 4f078ecd..5c64d1ac 100644 --- a/api/tests.py +++ b/api/tests.py @@ -16,10 +16,10 @@ class CoordinatesFromAddressTestCase(PerDiemTestCase): - url = '/api/coordinates/?address={address}' - valid_url = url.format(address='Willowdale%2C+Toronto%2C+Ontario%2C+Canada') + url = "/api/coordinates/?address={address}" + valid_url = url.format(address="Willowdale%2C+Toronto%2C+Ontario%2C+Canada") - @mock.patch('api.views.geolocator.geocode') + @mock.patch("api.views.geolocator.geocode") def testCoordinatesFromAddress(self, mock_geocode): # First the Geocoder service fails and so we return 503 mock_geocode.side_effect = GeocoderTimedOut @@ -29,12 +29,12 @@ def testCoordinatesFromAddress(self, mock_geocode): mock_geocode.side_effect = None mock_geocode.return_value = mock.Mock(latitude=43.766751, longitude=-79.410332) response = self.assertAPIResponseRenders(self.valid_url) - lat, lon = response['latitude'], response['longitude'] + lat, lon = response["latitude"], response["longitude"] self.assertEqual(lat, 43.7668) self.assertEqual(lon, -79.4103) def testCoordinatesFromAddressRequiresAddress(self): - for url in ['/api/coordinates/', self.url.format(address='')]: + for url in ["/api/coordinates/", self.url.format(address="")]: self.assertAPIResponseRenders(url, status_code=400) def testCoordinatesFromAddressFailsWithoutPermission(self): @@ -48,8 +48,7 @@ def testCoordinatesFromAddressFailsWithoutPermission(self): # Login as an ordinary user ordinary_user = UserFactory() self.client.login( - username=ordinary_user.username, - password=UserFactory._PASSWORD + username=ordinary_user.username, password=UserFactory._PASSWORD ) # Coordinates from Address API requires permission @@ -58,146 +57,147 @@ def testCoordinatesFromAddressFailsWithoutPermission(self): class PaymentChargeTestCase(PerDiemTestCase): - - @mock.patch('stripe.Charge.create') - @mock.patch('stripe.Customer.create') + @mock.patch("stripe.Charge.create") + @mock.patch("stripe.Customer.create") def testUserInvests(self, mock_stripe_customer_create, mock_stripe_charge_create): # Mock responses from Stripe mock_stripe_customer_create.return_value = { - 'account_balance': 0, - 'business_vat_id': None, - 'created': 1462665000, - 'currency': None, - 'default_source': 'card_2CXngrrA798I5wA01wQ74iTR', - 'delinquent': False, - 'description': None, - 'discount': None, - 'email': self.USER_EMAIL, - 'id': 'cus_2Pc8xEoaTAnVKr', - 'livemode': False, - 'metadata': {}, - 'object': 'customer', - 'shipping': None, - 'sources': { - 'data': [ + "account_balance": 0, + "business_vat_id": None, + "created": 1462665000, + "currency": None, + "default_source": "card_2CXngrrA798I5wA01wQ74iTR", + "delinquent": False, + "description": None, + "discount": None, + "email": self.USER_EMAIL, + "id": "cus_2Pc8xEoaTAnVKr", + "livemode": False, + "metadata": {}, + "object": "customer", + "shipping": None, + "sources": { + "data": [ { - 'address_city': None, - 'address_country': None, - 'address_line1': None, - 'address_line1_check': None, - 'address_line2': None, - 'address_state': None, - 'address_zip': None, - 'address_zip_check': None, - 'brand': 'Visa', - 'country': 'US', - 'customer': 'cus_2Pc8xEoaTAnVKr', - 'cvc_check': 'pass', - 'dynamic_last4': None, - 'exp_month': 5, - 'exp_year': 2019, - 'fingerprint': 'Lq9DFkUmxf7xWHkn', - 'funding': 'credit', - 'id': 'card_2CXngrrA798I5wA01wQ74iTR', - 'last4': '4242', - 'metadata': {}, - 'name': self.USER_EMAIL, - 'object': 'card', - 'tokenization_method': None, - }, + "address_city": None, + "address_country": None, + "address_line1": None, + "address_line1_check": None, + "address_line2": None, + "address_state": None, + "address_zip": None, + "address_zip_check": None, + "brand": "Visa", + "country": "US", + "customer": "cus_2Pc8xEoaTAnVKr", + "cvc_check": "pass", + "dynamic_last4": None, + "exp_month": 5, + "exp_year": 2019, + "fingerprint": "Lq9DFkUmxf7xWHkn", + "funding": "credit", + "id": "card_2CXngrrA798I5wA01wQ74iTR", + "last4": "4242", + "metadata": {}, + "name": self.USER_EMAIL, + "object": "card", + "tokenization_method": None, + } ], - 'has_more': False, - 'object': 'list', - 'total_count': 1, - 'url': '/v1/customers/cus_2Pc8xEoaTAnVKr/sources', + "has_more": False, + "object": "list", + "total_count": 1, + "url": "/v1/customers/cus_2Pc8xEoaTAnVKr/sources", }, - 'subscriptions': { - 'data': [], - 'has_more': False, - 'object': 'list', - 'total_count': 0, - 'url': '/v1/customers/cus_2Pc8xEoaTAnVKr/subscriptions', + "subscriptions": { + "data": [], + "has_more": False, + "object": "list", + "total_count": 0, + "url": "/v1/customers/cus_2Pc8xEoaTAnVKr/subscriptions", }, } mock_stripe_charge_create.return_value = { - 'amount': 235, - 'amount_refunded': 0, - 'application_fee': None, - 'balance_transaction': 'txn_Sazj9jMCau62PxJhOLzBXM3p', - 'captured': True, - 'created': 1462665010, - 'currency': 'usd', - 'customer': 'cus_2Pc8xEoaTAnVKr', - 'description': None, - 'destination': None, - 'dispute': None, - 'failure_code': None, - 'failure_message': None, - 'fraud_details': {}, - 'id': 'ch_Upra88VQlJJPd0JxeTM0ZvHv', - 'invoice': None, - 'livemode': False, - 'metadata': {}, - 'object': 'charge', - 'order': None, - 'paid': True, - 'receipt_email': None, - 'receipt_number': None, - 'refunded': False, - 'refunds': { - 'data': [], - 'has_more': False, - 'object': 'list', - 'total_count': 0, - 'url': '/v1/charges/ch_Upra88VQlJJPd0JxeTM0ZvHv/refunds', + "amount": 235, + "amount_refunded": 0, + "application_fee": None, + "balance_transaction": "txn_Sazj9jMCau62PxJhOLzBXM3p", + "captured": True, + "created": 1462665010, + "currency": "usd", + "customer": "cus_2Pc8xEoaTAnVKr", + "description": None, + "destination": None, + "dispute": None, + "failure_code": None, + "failure_message": None, + "fraud_details": {}, + "id": "ch_Upra88VQlJJPd0JxeTM0ZvHv", + "invoice": None, + "livemode": False, + "metadata": {}, + "object": "charge", + "order": None, + "paid": True, + "receipt_email": None, + "receipt_number": None, + "refunded": False, + "refunds": { + "data": [], + "has_more": False, + "object": "list", + "total_count": 0, + "url": "/v1/charges/ch_Upra88VQlJJPd0JxeTM0ZvHv/refunds", }, - 'shipping': None, - 'source': { - 'address_city': None, - 'address_country': None, - 'address_line1': None, - 'address_line1_check': None, - 'address_line2': None, - 'address_state': None, - 'address_zip': None, - 'address_zip_check': None, - 'brand': 'Visa', - 'country': 'US', - 'customer': 'cus_2Pc8xEoaTAnVKr', - 'cvc_check': None, - 'dynamic_last4': None, - 'exp_month': 5, - 'exp_year': 2019, - 'fingerprint': 'Lq9DFkUmxf7xWHkn', - 'funding': 'credit', - 'id': 'card_2CXngrrA798I5wA01wQ74iTR', - 'last4': '4242', - 'metadata': {}, - 'name': self.USER_EMAIL, - 'object': 'card', - 'tokenization_method': None, + "shipping": None, + "source": { + "address_city": None, + "address_country": None, + "address_line1": None, + "address_line1_check": None, + "address_line2": None, + "address_state": None, + "address_zip": None, + "address_zip_check": None, + "brand": "Visa", + "country": "US", + "customer": "cus_2Pc8xEoaTAnVKr", + "cvc_check": None, + "dynamic_last4": None, + "exp_month": 5, + "exp_year": 2019, + "fingerprint": "Lq9DFkUmxf7xWHkn", + "funding": "credit", + "id": "card_2CXngrrA798I5wA01wQ74iTR", + "last4": "4242", + "metadata": {}, + "name": self.USER_EMAIL, + "object": "card", + "tokenization_method": None, }, - 'source_transfer': None, - 'statement_descriptor': None, - 'status': 'succeeded', + "source_transfer": None, + "statement_descriptor": None, + "status": "succeeded", } # Create campaign campaign = CampaignFactory() # User sends payment to Stripe - self.assertResponseRenders('/artist/{slug}/'.format(slug=campaign.project.artist.slug)) + self.assertResponseRenders( + "/artist/{slug}/".format(slug=campaign.project.artist.slug) + ) self.assertAPIResponseRenders( - '/api/payments/charge/{campaign_id}/'.format(campaign_id=campaign.id), + "/api/payments/charge/{campaign_id}/".format(campaign_id=campaign.id), status_code=205, - method='POST', - data={'card': 'tok_6WqQnRecbRRrqvrdT1XXGP1d', 'num_shares': 1} + method="POST", + data={"card": "tok_6WqQnRecbRRrqvrdT1XXGP1d", "num_shares": 1}, ) class DeleteUpdateTestCase(PerDiemTestCase): - url = '/api/update/{update_id}/' + url = "/api/update/{update_id}/" @classmethod def setUpTestData(cls): @@ -206,7 +206,7 @@ def setUpTestData(cls): cls.valid_url = cls.url.format(update_id=cls.update.id) def testDeleteUpdate(self): - self.assertAPIResponseRenders(self.valid_url, status_code=204, method='DELETE') + self.assertAPIResponseRenders(self.valid_url, status_code=204, method="DELETE") def testDeleteUpdateRequiresValidUpdateId(self): self.assertResponseRenders(self.url.format(update_id=0), status_code=403) @@ -217,13 +217,12 @@ def testDeleteUpdateFailsWithoutPermission(self): # Delete Update API requires permission # but you're not authenticated - self.assertResponseRenders(self.valid_url, status_code=403, method='DELETE') + self.assertResponseRenders(self.valid_url, status_code=403, method="DELETE") # Login as ordinary user ordinary_user = UserFactory() self.client.login( - username=ordinary_user.username, - password=UserFactory._PASSWORD + username=ordinary_user.username, password=UserFactory._PASSWORD ) # Delete Update API the user to be an ArtistAdmin (or superuser) @@ -235,16 +234,18 @@ def testDeleteUpdateOnlyAllowsArtistAdminsToUpdateTheirArtists(self): self.client.logout() # Make the manager an ArtistAdmin - manager_username = 'manager' + manager_username = "manager" ArtistAdminFactory(artist=self.update.artist, user__username=manager_username) # Login as manager self.client.login(username=manager_username, password=UserFactory._PASSWORD) # Delete Update API allows ArtistAdmins to update - self.assertResponseRenders(self.valid_url, status_code=204, method='DELETE') + self.assertResponseRenders(self.valid_url, status_code=204, method="DELETE") # Delete Update API does not allow ArtistAdmins # to update artists they don't belong to update = UpdateFactory() - self.assertResponseRenders(self.url.format(update_id=update.id), status_code=403, method='DELETE') + self.assertResponseRenders( + self.url.format(update_id=update.id), status_code=403, method="DELETE" + ) diff --git a/api/urls.py b/api/urls.py index 38a0c780..d4c70897 100644 --- a/api/urls.py +++ b/api/urls.py @@ -10,7 +10,11 @@ urlpatterns = [ - url(r'^coordinates/?$', CoordinatesFromAddress.as_view()), - url(r'^payments/charge/(?P\d+)/?$', PaymentCharge.as_view(), name='pinax_stripe_charge'), - url(r'^update/(?P\d+)/?$', DeleteUpdate.as_view()), + url(r"^coordinates/?$", CoordinatesFromAddress.as_view()), + url( + r"^payments/charge/(?P\d+)/?$", + PaymentCharge.as_view(), + name="pinax_stripe_charge", + ), + url(r"^update/(?P\d+)/?$", DeleteUpdate.as_view()), ] diff --git a/api/views.py b/api/views.py index ec781de0..923b10fe 100644 --- a/api/views.py +++ b/api/views.py @@ -22,16 +22,16 @@ class AddArtistPermission(permissions.DjangoModelPermissions): - def get_required_permissions(self, method, model_cls): - return 'artist.add_artist' + return "artist.add_artist" class ArtistAdminPermission(permissions.BasePermission): - def has_permission(self, request, view): try: - update = Update.objects.select_related('artist').get(id=view.kwargs['update_id']) + update = Update.objects.select_related("artist").get( + id=view.kwargs["update_id"] + ) except Update.DoesNotExist: return False return update.artist.has_permission_to_submit_update(user=request.user) @@ -47,7 +47,7 @@ def get(self, request, *args, **kwargs): form = CoordinatesFromAddressForm(request.GET) if not form.is_valid(): return Response(form.errors, status=status.HTTP_400_BAD_REQUEST) - address = form.cleaned_data['address'] + address = form.cleaned_data["address"] # Return lat/lon for address try: @@ -55,12 +55,14 @@ def get(self, request, *args, **kwargs): except GeocoderTimedOut: return Response( "Geocoder service currently unavailable. Please try again later.", - status=status.HTTP_503_SERVICE_UNAVAILABLE + status=status.HTTP_503_SERVICE_UNAVAILABLE, ) - return Response({ - 'latitude': float("{0:.4f}".format(location.latitude)), - 'longitude': float("{0:.4f}".format(location.longitude)), - }) + return Response( + { + "latitude": float("{0:.4f}".format(location.latitude)), + "longitude": float("{0:.4f}".format(location.longitude)), + } + ) class PaymentCharge(APIView): @@ -73,14 +75,16 @@ def post(self, request, campaign_id, *args, **kwargs): campaign = Campaign.objects.get(id=campaign_id) except Campaign.DoesNotExist: return Response( - "Campaign with ID {campaign_id} does not exist.".format(campaign_id=campaign_id), - status=status.HTTP_400_BAD_REQUEST + "Campaign with ID {campaign_id} does not exist.".format( + campaign_id=campaign_id + ), + status=status.HTTP_400_BAD_REQUEST, ) else: if not campaign.open(): return Response( "This campaign is no longer accepting investments.", - status=status.HTTP_400_BAD_REQUEST + status=status.HTTP_400_BAD_REQUEST, ) form = PaymentChargeForm(request.data, campaign=campaign) if not form.is_valid(): @@ -88,36 +92,44 @@ def post(self, request, campaign_id, *args, **kwargs): d = form.cleaned_data # Get card and customer - card = d['card'] + card = d["card"] customer = customers.get_customer_for_user(request.user) if not customer: - customer = customers.create(request.user, card=card, plan=None, charge_immediately=False) + customer = customers.create( + request.user, card=card, plan=None, charge_immediately=False + ) card = Card.objects.get(customer=customer) else: # Check if we have the card the user is using # and if not, create it - card_fingerprint = stripe.Token.retrieve(card)['card']['fingerprint'] - cards_with_fingerprint = Card.objects.filter(customer=customer, fingerprint=card_fingerprint) + card_fingerprint = stripe.Token.retrieve(card)["card"]["fingerprint"] + cards_with_fingerprint = Card.objects.filter( + customer=customer, fingerprint=card_fingerprint + ) if cards_with_fingerprint.exists(): card = cards_with_fingerprint[0] else: card = sources.create_card(customer=customer, token=card) # Create charge - num_shares = d['num_shares'] + num_shares = d["num_shares"] amount = decimal.Decimal(campaign.total(num_shares)) try: - charge = charges.create(amount=amount, customer=customer.stripe_id, source=card) + charge = charges.create( + amount=amount, customer=customer.stripe_id, source=card + ) except stripe.CardError as e: return Response(e.message, status=status.HTTP_400_BAD_REQUEST) - Investment.objects.create(charge=charge, campaign=campaign, num_shares=num_shares) + Investment.objects.create( + charge=charge, campaign=campaign, num_shares=num_shares + ) return Response(status=status.HTTP_205_RESET_CONTENT) class DeleteUpdate(APIView): - permission_classes = (permissions.IsAuthenticated, ArtistAdminPermission,) + permission_classes = (permissions.IsAuthenticated, ArtistAdminPermission) def delete(self, request, *args, **kwargs): - Update.objects.get(id=self.kwargs['update_id']).delete() + Update.objects.get(id=self.kwargs["update_id"]).delete() return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/artist/admin.py b/artist/admin.py index d496ae0a..1f1be2da 100644 --- a/artist/admin.py +++ b/artist/admin.py @@ -18,36 +18,42 @@ class LocationWidget(AdminTextInputWidget): # TODO: Use template_name and refactor widget to use Django 1.11's new get_context() method # https://docs.djangoproject.com/en/1.11/ref/forms/widgets/#django.forms.Widget.get_context - template_name_dj110_to_dj111_compat = 'widgets/coordinates.html' + template_name_dj110_to_dj111_compat = "widgets/coordinates.html" def render(self, name, value, attrs=None, renderer=None): - html = super(LocationWidget, self).render(name, value, attrs=attrs, renderer=renderer) + html = super(LocationWidget, self).render( + name, value, attrs=attrs, renderer=renderer + ) return html + render_to_string(self.template_name_dj110_to_dj111_compat) class ArtistAdminForm(forms.ModelForm): - location = forms.CharField(help_text=Artist._meta.get_field('location').help_text, widget=LocationWidget) + location = forms.CharField( + help_text=Artist._meta.get_field("location").help_text, widget=LocationWidget + ) class Meta: model = Artist - fields = ('name', 'genres', 'slug', 'location', 'lat', 'lon',) + fields = ("name", "genres", "slug", "location", "lat", "lon") class ArtistAdministratorInline(admin.StackedInline): model = ArtistAdmin - raw_id_fields = ('user',) + raw_id_fields = ("user",) extra = 2 class BioAdminForm(forms.ModelForm): - bio = forms.CharField(help_text=Bio._meta.get_field('bio').help_text, widget=AdminPagedownWidget) + bio = forms.CharField( + help_text=Bio._meta.get_field("bio").help_text, widget=AdminPagedownWidget + ) class Meta: model = Bio - fields = ('bio',) + fields = ("bio",) class BioInline(admin.StackedInline): @@ -75,8 +81,14 @@ class SocialInline(admin.TabularInline): class ArtistAdmin(admin.ModelAdmin): form = ArtistAdminForm - prepopulated_fields = {'slug': ('name',)} - inlines = (ArtistAdministratorInline, BioInline, PhotoInline, PlaylistInline, SocialInline,) + prepopulated_fields = {"slug": ("name",)} + inlines = ( + ArtistAdministratorInline, + BioInline, + PhotoInline, + PlaylistInline, + SocialInline, + ) admin.site.register(Genre) diff --git a/artist/apps.py b/artist/apps.py index 0ba34017..4f67bdc8 100644 --- a/artist/apps.py +++ b/artist/apps.py @@ -2,4 +2,4 @@ class ArtistConfig(AppConfig): - name = 'artist' + name = "artist" diff --git a/artist/context_processors.py b/artist/context_processors.py index da88347c..bb246bb4 100644 --- a/artist/context_processors.py +++ b/artist/context_processors.py @@ -8,6 +8,4 @@ def artist_settings(request): - return { - 'PERDIEM_FEE': settings.PERDIEM_FEE, - } + return {"PERDIEM_FEE": settings.PERDIEM_FEE} diff --git a/artist/factories.py b/artist/factories.py index 8bd5debd..246dc51f 100644 --- a/artist/factories.py +++ b/artist/factories.py @@ -8,11 +8,10 @@ def artistfactory_factory(apps): class ArtistFactory(factory.DjangoModelFactory): - class Meta: - model = apps.get_model('artist', 'Artist') + model = apps.get_model("artist", "Artist") - name = factory.Faker('user_name') + name = factory.Faker("user_name") slug = factory.LazyAttribute(lambda artist: slugify(artist.name)) # Willowdale, Toronto, Ontario, Canada @@ -24,9 +23,8 @@ class Meta: def updatefactory_factory(apps): class UpdateFactory(factory.DjangoModelFactory): - class Meta: - model = apps.get_model('artist', 'Update') + model = apps.get_model("artist", "Update") artist = factory.SubFactory(artistfactory_factory(apps=apps)) @@ -38,15 +36,13 @@ class Meta: class GenreFactory(factory.DjangoModelFactory): - class Meta: - model = django_apps.get_model('artist', 'Genre') + model = django_apps.get_model("artist", "Genre") class ArtistAdminFactory(factory.DjangoModelFactory): - class Meta: - model = django_apps.get_model('artist', 'ArtistAdmin') + model = django_apps.get_model("artist", "ArtistAdmin") artist = factory.SubFactory(ArtistFactory) user = factory.SubFactory(UserFactory) diff --git a/artist/forms.py b/artist/forms.py index 45983c5d..ac0eb4d9 100644 --- a/artist/forms.py +++ b/artist/forms.py @@ -11,25 +11,44 @@ class ArtistApplyForm(forms.Form): - artist_name = forms.CharField(label='Artist / Band Name') + artist_name = forms.CharField(label="Artist / Band Name") genre = forms.CharField() hometown = forms.CharField() email = forms.EmailField() phone_number = forms.CharField() - bio = forms.CharField(widget=forms.Textarea(attrs={'placeholder': 'We started playing music because...'})) + bio = forms.CharField( + widget=forms.Textarea( + attrs={"placeholder": "We started playing music because..."} + ) + ) campaign_reason = forms.CharField( - label='Why are you raising money?', - widget=forms.Textarea(attrs={'placeholder': 'We are trying to record our album...'}) + label="Why are you raising money?", + widget=forms.Textarea( + attrs={"placeholder": "We are trying to record our album..."} + ), ) campaign_expenses = forms.CharField( - label='What do you need the money for?', - widget=forms.Textarea(attrs={'placeholder': 'Mixing, mastering, studio time, etc...'}) + label="What do you need the money for?", + widget=forms.Textarea( + attrs={"placeholder": "Mixing, mastering, studio time, etc..."} + ), + ) + facebook = forms.URLField( + required=False, widget=forms.TextInput(attrs={"placeholder": "http://"}) + ) + twitter = forms.CharField( + required=False, widget=forms.TextInput(attrs={"placeholder": "@"}) + ) + instagram = forms.CharField( + required=False, widget=forms.TextInput(attrs={"placeholder": "@"}) + ) + music_link = forms.URLField( + label="Link to music", widget=forms.TextInput(attrs={"placeholder": "http://"}) + ) + terms = forms.BooleanField( + label="Terms & Conditions", + help_text="I have read and agree to the Terms & Conditions", ) - facebook = forms.URLField(required=False, widget=forms.TextInput(attrs={'placeholder': 'http://'})) - twitter = forms.CharField(required=False, widget=forms.TextInput(attrs={'placeholder': '@'})) - instagram = forms.CharField(required=False, widget=forms.TextInput(attrs={'placeholder': '@'})) - music_link = forms.URLField(label='Link to music', widget=forms.TextInput(attrs={'placeholder': 'http://'})) - terms = forms.BooleanField(label='Terms & Conditions', help_text='I have read and agree to the Terms & Conditions') class ArtistUpdateForm(forms.Form): @@ -41,8 +60,8 @@ class ArtistUpdateForm(forms.Form): def clean(self): cleaned_data = super(ArtistUpdateForm, self).clean() - image = cleaned_data['image'] - youtube_url = cleaned_data['youtube_url'] + image = cleaned_data["image"] + youtube_url = cleaned_data["youtube_url"] provided = list(filter(lambda x: x, [image, youtube_url])) if len(provided) > 1: raise forms.ValidationError("Please only provide one image or video.") diff --git a/artist/geolocator.py b/artist/geolocator.py index 63b24a97..e1db2806 100644 --- a/artist/geolocator.py +++ b/artist/geolocator.py @@ -1,4 +1,4 @@ from geopy.geocoders import Nominatim -geolocator = Nominatim(user_agent='PerDiem (+https://www.investperdiem.com/)') +geolocator = Nominatim(user_agent="PerDiem (+https://www.investperdiem.com/)") diff --git a/artist/managers.py b/artist/managers.py index fd1ec086..ed65188f 100644 --- a/artist/managers.py +++ b/artist/managers.py @@ -12,10 +12,9 @@ class ArtistQuerySet(models.QuerySet): - @staticmethod def bounding_coordinates(distance, lat, lon): - origin = geopy.Point((lat, lon,)) + origin = geopy.Point((lat, lon)) geopy_distance = calc_distance(miles=distance) min_lat = geopy_distance.destination(origin, 180).latitude max_lat = geopy_distance.destination(origin, 0).latitude @@ -40,7 +39,7 @@ def valuation(artist): return valuation def filter_by_genre(self, genre): - if genre != 'All Genres': + if genre != "All Genres": return self.filter(genres__name=genre) return self.all() @@ -55,17 +54,16 @@ def filter_by_funded(self): return self.filter(id__in=funded_artist_ids) def filter_by_location(self, distance, lat, lon): - min_lat, max_lat, min_lon, max_lon = self.bounding_coordinates(distance, lat, lon) + min_lat, max_lat, min_lon, max_lon = self.bounding_coordinates( + distance, lat, lon + ) artists_within_bounds = self.filter( - lat__gte=min_lat, - lat__lte=max_lat, - lon__gte=min_lon, - lon__lte=max_lon + lat__gte=min_lat, lat__lte=max_lat, lon__gte=min_lon, lon__lte=max_lon ) nearby_artist_ids = [] for artist in artists_within_bounds: - if calc_distance((lat, lon,), (artist.lat, artist.lon,)).miles <= distance: + if calc_distance((lat, lon), (artist.lat, artist.lon)).miles <= distance: nearby_artist_ids.append(artist.id) return self.filter(id__in=nearby_artist_ids) @@ -83,21 +81,24 @@ def order_by_percentage_funded(self): return sorted(self, key=self.percentage_funded, reverse=True) def order_by_time_remaining(self): - artists = self.annotate(campaign_end_datetime=models.Max('project__campaign__end_datetime')) + artists = self.annotate( + campaign_end_datetime=models.Max("project__campaign__end_datetime") + ) artists_current_campaign = artists.filter( campaign_end_datetime__gte=timezone.now() - ).order_by('campaign_end_datetime') + ).order_by("campaign_end_datetime") artists_current_campaign_no_end = artists.filter( - project__campaign__isnull=False, - campaign_end_datetime__isnull=True + project__campaign__isnull=False, campaign_end_datetime__isnull=True ) artists_past_campaign = artists.filter( campaign_end_datetime__lt=timezone.now() - ).order_by('-campaign_end_datetime') + ).order_by("-campaign_end_datetime") artists_no_campaign = artists.filter(project__campaign__isnull=True) return ( - list(artists_current_campaign) + list(artists_current_campaign_no_end) - + list(artists_past_campaign) + list(artists_no_campaign) + list(artists_current_campaign) + + list(artists_current_campaign_no_end) + + list(artists_past_campaign) + + list(artists_no_campaign) ) def order_by_num_investors(self): @@ -107,12 +108,12 @@ def order_by_num_investors(self): models.When( project__campaign__investment__charge__paid=True, project__campaign__investment__charge__refunded=False, - then='project__campaign__investment__charge__customer__user' + then="project__campaign__investment__charge__customer__user", ) ), - distinct=True + distinct=True, ) - ).order_by('-num_investors') + ).order_by("-num_investors") def order_by_amount_raised(self): return self.annotate( @@ -122,15 +123,15 @@ def order_by_amount_raised(self): project__campaign__investment__charge__paid=True, project__campaign__investment__charge__refunded=False, then=( - models.F('project__campaign__investment__num_shares') - * models.F('project__campaign__value_per_share') + models.F("project__campaign__investment__num_shares") + * models.F("project__campaign__value_per_share") ), ), default=0, - output_field=models.IntegerField() + output_field=models.IntegerField(), ) ) - ).order_by('-amount_raised') + ).order_by("-amount_raised") def order_by_valuation(self): return sorted(self, key=self.valuation, reverse=True) diff --git a/artist/migrations/0001_initial.py b/artist/migrations/0001_initial.py index 5a95f2fd..c3268d41 100644 --- a/artist/migrations/0001_initial.py +++ b/artist/migrations/0001_initial.py @@ -8,77 +8,221 @@ class Migration(migrations.Migration): initial = True - dependencies = [ - ] + dependencies = [] operations = [ migrations.CreateModel( - name='Artist', + name="Artist", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(db_index=True, max_length=60)), - ('slug', models.SlugField(max_length=40)), - ('lat', models.DecimalField(db_index=True, decimal_places=4, help_text='Latitude of artist location', max_digits=6)), - ('lon', models.DecimalField(db_index=True, decimal_places=4, help_text='Longitude of artist location', max_digits=7)), - ('location', models.CharField(help_text='Description of artist location (usually city, state, country format)', max_length=40)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(db_index=True, max_length=60)), + ("slug", models.SlugField(max_length=40)), + ( + "lat", + models.DecimalField( + db_index=True, + decimal_places=4, + help_text="Latitude of artist location", + max_digits=6, + ), + ), + ( + "lon", + models.DecimalField( + db_index=True, + decimal_places=4, + help_text="Longitude of artist location", + max_digits=7, + ), + ), + ( + "location", + models.CharField( + help_text="Description of artist location (usually city, state, country format)", + max_length=40, + ), + ), ], ), migrations.CreateModel( - name='Bio', + name="Bio", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('bio', models.TextField(help_text='Short biography of artist. May contain HTML.')), - ('artist', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='artist.Artist')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "bio", + models.TextField( + help_text="Short biography of artist. May contain HTML." + ), + ), + ( + "artist", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, to="artist.Artist" + ), + ), ], ), migrations.CreateModel( - name='Genre', + name="Genre", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(db_index=True, max_length=40, unique=True)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(db_index=True, max_length=40, unique=True)), ], ), migrations.CreateModel( - name='Photo', + name="Photo", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('img', models.ImageField(help_text='Primary profile photo of artist', upload_to='artist')), - ('artist', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='artist.Artist')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "img", + models.ImageField( + help_text="Primary profile photo of artist", upload_to="artist" + ), + ), + ( + "artist", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, to="artist.Artist" + ), + ), ], ), migrations.CreateModel( - name='Social', + name="Social", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('medium', models.CharField(choices=[('facebook', 'Facebook'), ('twitter', 'Twitter'), ('instagram', 'Instagram'), ('youtube', 'YouTube'), ('soundcloud', 'SoundCloud')], help_text='The type of social network', max_length=10)), - ('url', models.URLField(help_text="The URL to the artist's social network page", unique=True)), - ('artist', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='artist.Artist')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "medium", + models.CharField( + choices=[ + ("facebook", "Facebook"), + ("twitter", "Twitter"), + ("instagram", "Instagram"), + ("youtube", "YouTube"), + ("soundcloud", "SoundCloud"), + ], + help_text="The type of social network", + max_length=10, + ), + ), + ( + "url", + models.URLField( + help_text="The URL to the artist's social network page", + unique=True, + ), + ), + ( + "artist", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="artist.Artist" + ), + ), ], ), migrations.CreateModel( - name='SoundCloudPlaylist', + name="SoundCloudPlaylist", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('playlist', models.URLField(help_text="The SoundCloud iframe URL to the artist's playlist", unique=True)), - ('artist', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='artist.Artist')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "playlist", + models.URLField( + help_text="The SoundCloud iframe URL to the artist's playlist", + unique=True, + ), + ), + ( + "artist", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="artist.Artist" + ), + ), ], ), migrations.CreateModel( - name='Update', + name="Update", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created_datetime', models.DateTimeField(auto_now_add=True, db_index=True)), - ('text', models.TextField(help_text='The content of the update. May contain HTML.')), - ('artist', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='artist.Artist')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "created_datetime", + models.DateTimeField(auto_now_add=True, db_index=True), + ), + ( + "text", + models.TextField( + help_text="The content of the update. May contain HTML." + ), + ), + ( + "artist", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="artist.Artist" + ), + ), ], ), migrations.AddField( - model_name='artist', - name='genres', - field=models.ManyToManyField(to='artist.Genre'), + model_name="artist", + name="genres", + field=models.ManyToManyField(to="artist.Genre"), ), migrations.AlterUniqueTogether( - name='social', - unique_together=set([('artist', 'medium')]), + name="social", unique_together=set([("artist", "medium")]) ), ] diff --git a/artist/migrations/0002_auto_20160317_0521.py b/artist/migrations/0002_auto_20160317_0521.py index 3f4b31d3..d2aa5263 100644 --- a/artist/migrations/0002_auto_20160317_0521.py +++ b/artist/migrations/0002_auto_20160317_0521.py @@ -5,14 +5,16 @@ class Migration(migrations.Migration): - dependencies = [ - ('artist', '0001_initial'), - ] + dependencies = [("artist", "0001_initial")] operations = [ migrations.AlterField( - model_name='artist', - name='slug', - field=models.SlugField(help_text='A short label for an artist (used in URLs)', max_length=40, unique=True), - ), + model_name="artist", + name="slug", + field=models.SlugField( + help_text="A short label for an artist (used in URLs)", + max_length=40, + unique=True, + ), + ) ] diff --git a/artist/migrations/0003_auto_20160522_2139.py b/artist/migrations/0003_auto_20160522_2139.py index 12a4c1f9..0fc35a80 100644 --- a/artist/migrations/0003_auto_20160522_2139.py +++ b/artist/migrations/0003_auto_20160522_2139.py @@ -5,14 +5,14 @@ class Migration(migrations.Migration): - dependencies = [ - ('artist', '0002_auto_20160317_0521'), - ] + dependencies = [("artist", "0002_auto_20160317_0521")] operations = [ migrations.AlterField( - model_name='bio', - name='bio', - field=models.TextField(help_text='Short biography of artist. Markdown syntax allowed, but no raw HTML. Examples: **bold**, *italic*, indent 4 spaces for a code block.'), - ), + model_name="bio", + name="bio", + field=models.TextField( + help_text='Short biography of artist. Markdown syntax allowed, but no raw HTML. Examples: **bold**, *italic*, indent 4 spaces for a code block.' + ), + ) ] diff --git a/artist/migrations/0004_auto_20160522_2141.py b/artist/migrations/0004_auto_20160522_2141.py index 6544ac1f..a6fb39ee 100644 --- a/artist/migrations/0004_auto_20160522_2141.py +++ b/artist/migrations/0004_auto_20160522_2141.py @@ -5,14 +5,12 @@ class Migration(migrations.Migration): - dependencies = [ - ('artist', '0003_auto_20160522_2139'), - ] + dependencies = [("artist", "0003_auto_20160522_2139")] operations = [ migrations.AlterField( - model_name='update', - name='text', - field=models.TextField(help_text='The content of the update.'), - ), + model_name="update", + name="text", + field=models.TextField(help_text="The content of the update."), + ) ] diff --git a/artist/migrations/0005_auto_20160522_2328.py b/artist/migrations/0005_auto_20160522_2328.py index 27232469..9a1cbb12 100644 --- a/artist/migrations/0005_auto_20160522_2328.py +++ b/artist/migrations/0005_auto_20160522_2328.py @@ -6,45 +6,69 @@ class Migration(migrations.Migration): - dependencies = [ - ('artist', '0004_auto_20160522_2141'), - ] + dependencies = [("artist", "0004_auto_20160522_2141")] operations = [ migrations.CreateModel( - name='UpdateImage', + name="UpdateImage", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('img', models.ImageField(upload_to='artist/updates')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("img", models.ImageField(upload_to="artist/updates")), ], ), migrations.CreateModel( - name='UpdateMediaURL', + name="UpdateMediaURL", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('media_type', models.CharField(choices=[('image', 'Image'), ('youtube', 'YouTube')], max_length=8)), - ('url', models.URLField()), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "media_type", + models.CharField( + choices=[("image", "Image"), ("youtube", "YouTube")], + max_length=8, + ), + ), + ("url", models.URLField()), ], ), migrations.AddField( - model_name='update', - name='title', - field=models.CharField(default='Update', max_length=75), + model_name="update", + name="title", + field=models.CharField(default="Update", max_length=75), preserve_default=False, ), migrations.AlterField( - model_name='update', - name='text', - field=models.TextField(help_text='The content of the update'), + model_name="update", + name="text", + field=models.TextField(help_text="The content of the update"), ), migrations.AddField( - model_name='updatemediaurl', - name='update', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='artist.Update'), + model_name="updatemediaurl", + name="update", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="artist.Update" + ), ), migrations.AddField( - model_name='updateimage', - name='update', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='artist.Update'), + model_name="updateimage", + name="update", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="artist.Update" + ), ), ] diff --git a/artist/migrations/0006_updatetitles.py b/artist/migrations/0006_updatetitles.py index 7bfb57cd..db1f0ed8 100644 --- a/artist/migrations/0006_updatetitles.py +++ b/artist/migrations/0006_updatetitles.py @@ -5,19 +5,17 @@ class Migration(migrations.Migration): - dependencies = [ - ('artist', '0005_auto_20160522_2328'), - ] + dependencies = [("artist", "0005_auto_20160522_2328")] def set_initial_update_titles(apps, schema_editor): Update = apps.get_model("artist", "Update") for update in Update.objects.all(): update.title = "{artist} Update: {date}".format( artist=update.artist.name, - date=update.created_datetime.strftime("%m/%d/%Y") + date=update.created_datetime.strftime("%m/%d/%Y"), ) update.save() operations = [ - migrations.RunPython(set_initial_update_titles, migrations.RunPython.noop), + migrations.RunPython(set_initial_update_titles, migrations.RunPython.noop) ] diff --git a/artist/migrations/0007_artistadmin.py b/artist/migrations/0007_artistadmin.py index 26fd831b..3fabc8c8 100644 --- a/artist/migrations/0007_artistadmin.py +++ b/artist/migrations/0007_artistadmin.py @@ -9,17 +9,47 @@ class Migration(migrations.Migration): dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('artist', '0006_updatetitles'), + ("artist", "0006_updatetitles"), ] operations = [ migrations.CreateModel( - name='ArtistAdmin', + name="ArtistAdmin", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('role', models.CharField(choices=[('musician', 'Musician'), ('manager', 'Manager'), ('producer', 'Producer')], help_text='The relationship of this user to the artist', max_length=12)), - ('artist', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='artist.Artist')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "role", + models.CharField( + choices=[ + ("musician", "Musician"), + ("manager", "Manager"), + ("producer", "Producer"), + ], + help_text="The relationship of this user to the artist", + max_length=12, + ), + ), + ( + "artist", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="artist.Artist" + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), ], - ), + ) ] diff --git a/artist/migrations/0008_auto_20160625_0134.py b/artist/migrations/0008_auto_20160625_0134.py index 3e193ffd..9e9e1b40 100644 --- a/artist/migrations/0008_auto_20160625_0134.py +++ b/artist/migrations/0008_auto_20160625_0134.py @@ -5,14 +5,21 @@ class Migration(migrations.Migration): - dependencies = [ - ('artist', '0007_artistadmin'), - ] + dependencies = [("artist", "0007_artistadmin")] operations = [ migrations.AlterField( - model_name='artistadmin', - name='role', - field=models.CharField(choices=[('musician', 'Musician'), ('manager', 'Manager'), ('producer', 'Producer'), ('songwriter', 'Songwriter')], help_text='The relationship of this user to the artist', max_length=12), - ), + model_name="artistadmin", + name="role", + field=models.CharField( + choices=[ + ("musician", "Musician"), + ("manager", "Manager"), + ("producer", "Producer"), + ("songwriter", "Songwriter"), + ], + help_text="The relationship of this user to the artist", + max_length=12, + ), + ) ] diff --git a/artist/migrations/0009_auto_20170201_0753.py b/artist/migrations/0009_auto_20170201_0753.py index 157d38f5..98faf30e 100644 --- a/artist/migrations/0009_auto_20170201_0753.py +++ b/artist/migrations/0009_auto_20170201_0753.py @@ -6,22 +6,44 @@ class Migration(migrations.Migration): - dependencies = [ - ('artist', '0008_auto_20160625_0134'), - ] + dependencies = [("artist", "0008_auto_20160625_0134")] operations = [ migrations.CreateModel( - name='Playlist', + name="Playlist", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('provider', models.CharField(choices=[('spotify', 'Spotify'), ('soundcloud', 'SoundCloud')], help_text='Provider of the playlist', max_length=10)), - ('uri', models.TextField(help_text='URI that with the provider uniquely identifies a playlist')), - ('artist', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='artist.Artist')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "provider", + models.CharField( + choices=[("spotify", "Spotify"), ("soundcloud", "SoundCloud")], + help_text="Provider of the playlist", + max_length=10, + ), + ), + ( + "uri", + models.TextField( + help_text="URI that with the provider uniquely identifies a playlist" + ), + ), + ( + "artist", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="artist.Artist" + ), + ), ], ), migrations.AlterUniqueTogether( - name='playlist', - unique_together=set([('provider', 'uri')]), + name="playlist", unique_together=set([("provider", "uri")]) ), ] diff --git a/artist/migrations/0010_auto_20170201_0754.py b/artist/migrations/0010_auto_20170201_0754.py index 7d4ee04b..e5e21143 100644 --- a/artist/migrations/0010_auto_20170201_0754.py +++ b/artist/migrations/0010_auto_20170201_0754.py @@ -7,30 +7,34 @@ class Migration(migrations.Migration): - dependencies = [ - ('artist', '0009_auto_20170201_0753'), - ] + dependencies = [("artist", "0009_auto_20170201_0753")] def copy_soundcloud_playlists_to_playlist(apps, schema_editor): - SoundCloudPlaylist = apps.get_model('artist', 'SoundCloudPlaylist') - Playlist = apps.get_model('artist', 'Playlist') + SoundCloudPlaylist = apps.get_model("artist", "SoundCloudPlaylist") + Playlist = apps.get_model("artist", "Playlist") for scplaylist in SoundCloudPlaylist.objects.all(): Playlist.objects.get_or_create( artist=scplaylist.artist, provider=PlaylistConst.PLAYLIST_PROVIDER_SOUNDCLOUD, - uri=scplaylist.playlist + uri=scplaylist.playlist, ) # Note: Running the reverse migration on a database that contains playlists # from providers other than SoundCloud will result in data loss def copy_playlists_to_soundcloud_playlist(apps, schema_editor): - SoundCloudPlaylist = apps.get_model('artist', 'SoundCloudPlaylist') - Playlist = apps.get_model('artist', 'Playlist') - - for playlist in Playlist.objects.filter(provider=PlaylistConst.PLAYLIST_PROVIDER_SOUNDCLOUD): - SoundCloudPlaylist.objects.get_or_create(artist=playlist.artist, playlist=playlist.uri) + SoundCloudPlaylist = apps.get_model("artist", "SoundCloudPlaylist") + Playlist = apps.get_model("artist", "Playlist") + + for playlist in Playlist.objects.filter( + provider=PlaylistConst.PLAYLIST_PROVIDER_SOUNDCLOUD + ): + SoundCloudPlaylist.objects.get_or_create( + artist=playlist.artist, playlist=playlist.uri + ) operations = [ - migrations.RunPython(copy_soundcloud_playlists_to_playlist, copy_playlists_to_soundcloud_playlist), + migrations.RunPython( + copy_soundcloud_playlists_to_playlist, copy_playlists_to_soundcloud_playlist + ) ] diff --git a/artist/migrations/0011_auto_20170201_0754.py b/artist/migrations/0011_auto_20170201_0754.py index aac5c76a..86572d0b 100644 --- a/artist/migrations/0011_auto_20170201_0754.py +++ b/artist/migrations/0011_auto_20170201_0754.py @@ -5,16 +5,9 @@ class Migration(migrations.Migration): - dependencies = [ - ('artist', '0010_auto_20170201_0754'), - ] + dependencies = [("artist", "0010_auto_20170201_0754")] operations = [ - migrations.RemoveField( - model_name='soundcloudplaylist', - name='artist', - ), - migrations.DeleteModel( - name='SoundCloudPlaylist', - ), + migrations.RemoveField(model_name="soundcloudplaylist", name="artist"), + migrations.DeleteModel(name="SoundCloudPlaylist"), ] diff --git a/artist/migrations/0012_auto_20170201_0820.py b/artist/migrations/0012_auto_20170201_0820.py index 7e9a2a69..b5503f8c 100644 --- a/artist/migrations/0012_auto_20170201_0820.py +++ b/artist/migrations/0012_auto_20170201_0820.py @@ -5,14 +5,12 @@ class Migration(migrations.Migration): - dependencies = [ - ('artist', '0011_auto_20170201_0754'), - ] + dependencies = [("artist", "0011_auto_20170201_0754")] operations = [ migrations.AlterField( - model_name='updatemediaurl', - name='media_type', - field=models.CharField(choices=[('youtube', 'YouTube')], max_length=8), - ), + model_name="updatemediaurl", + name="media_type", + field=models.CharField(choices=[("youtube", "YouTube")], max_length=8), + ) ] diff --git a/artist/migrations/0013_auto_20170511_0508.py b/artist/migrations/0013_auto_20170511_0508.py index be3b6ce0..c90bdddb 100644 --- a/artist/migrations/0013_auto_20170511_0508.py +++ b/artist/migrations/0013_auto_20170511_0508.py @@ -5,14 +5,14 @@ class Migration(migrations.Migration): - dependencies = [ - ('artist', '0012_auto_20170201_0820'), - ] + dependencies = [("artist", "0012_auto_20170201_0820")] operations = [ migrations.AlterField( - model_name='update', - name='text', - field=models.TextField(help_text='The content of the update. Markdown syntax allowed, but no raw HTML. Examples: **bold**, *italic*, indent 4 spaces for a code block.'), - ), + model_name="update", + name="text", + field=models.TextField( + help_text='The content of the update. Markdown syntax allowed, but no raw HTML. Examples: **bold**, *italic*, indent 4 spaces for a code block.' + ), + ) ] diff --git a/artist/models.py b/artist/models.py index fc7f8249..96b10eb7 100644 --- a/artist/models.py +++ b/artist/models.py @@ -28,11 +28,26 @@ class Artist(models.Model): name = models.CharField(max_length=60, db_index=True) genres = models.ManyToManyField(Genre) - slug = models.SlugField(max_length=40, unique=True, help_text='A short label for an artist (used in URLs)') - lat = models.DecimalField(max_digits=6, decimal_places=4, db_index=True, help_text='Latitude of artist location') - lon = models.DecimalField(max_digits=7, decimal_places=4, db_index=True, help_text='Longitude of artist location') + slug = models.SlugField( + max_length=40, + unique=True, + help_text="A short label for an artist (used in URLs)", + ) + lat = models.DecimalField( + max_digits=6, + decimal_places=4, + db_index=True, + help_text="Latitude of artist location", + ) + lon = models.DecimalField( + max_digits=7, + decimal_places=4, + db_index=True, + help_text="Longitude of artist location", + ) location = models.CharField( - max_length=40, help_text='Description of artist location (usually city, state, country format)' + max_length=40, + help_text="Description of artist location (usually city, state, country format)", ) objects = ArtistQuerySet.as_manager() @@ -41,7 +56,7 @@ def __str__(self): return self.name def url(self): - return reverse('artist', kwargs={'slug': self.slug}) + return reverse("artist", kwargs={"slug": self.slug}) def social_twitter(self): twitter_socials = self.social_set.filter(medium=Social.SOCIAL_TWITTER) @@ -50,27 +65,29 @@ def social_twitter(self): def latest_campaign(self): campaigns = Campaign.objects.filter( - project__artist=self, - start_datetime__lt=timezone.now() - ).order_by('-start_datetime') + project__artist=self, start_datetime__lt=timezone.now() + ).order_by("-start_datetime") if campaigns: return campaigns[0] def active_campaign(self): - active_campaigns = Campaign.objects.filter( - project__artist=self, - start_datetime__lt=timezone.now() - ).filter( - models.Q(end_datetime__isnull=True) | models.Q(end_datetime__gte=timezone.now()) - ).order_by('-start_datetime') + active_campaigns = ( + Campaign.objects.filter( + project__artist=self, start_datetime__lt=timezone.now() + ) + .filter( + models.Q(end_datetime__isnull=True) + | models.Q(end_datetime__gte=timezone.now()) + ) + .order_by("-start_datetime") + ) if active_campaigns: return active_campaigns[0] def past_campaigns(self): return Campaign.objects.filter( - project__artist=self, - end_datetime__lt=timezone.now() - ).order_by('-end_datetime') + project__artist=self, end_datetime__lt=timezone.now() + ).order_by("-end_datetime") def all_campaigns_failed(self): # Artists that have an active campaign have not failed @@ -89,10 +106,14 @@ def all_campaigns_failed(self): return True def has_permission_to_submit_update(self, user): - return user.is_authenticated and (user.is_superuser or self.artistadmin_set.filter(user=user).exists()) + return user.is_authenticated and ( + user.is_superuser or self.artistadmin_set.filter(user=user).exists() + ) def is_investor(self, user): - return Investment.objects.filter(charge__customer__user=user, campaign__project__artist=self).exists() + return Investment.objects.filter( + charge__customer__user=user, campaign__project__artist=self + ).exists() def investors(self): investors = {} @@ -103,21 +124,23 @@ def investors(self): class ArtistAdmin(models.Model): - ROLE_MUSICIAN = 'musician' - ROLE_MANAGER = 'manager' - ROLE_PRODUCER = 'producer' - ROLE_SONGWRITER = 'songwriter' + ROLE_MUSICIAN = "musician" + ROLE_MANAGER = "manager" + ROLE_PRODUCER = "producer" + ROLE_SONGWRITER = "songwriter" ROLE_CHOICES = ( - (ROLE_MUSICIAN, 'Musician',), - (ROLE_MANAGER, 'Manager',), - (ROLE_PRODUCER, 'Producer',), - (ROLE_SONGWRITER, 'Songwriter'), + (ROLE_MUSICIAN, "Musician"), + (ROLE_MANAGER, "Manager"), + (ROLE_PRODUCER, "Producer"), + (ROLE_SONGWRITER, "Songwriter"), ) artist = models.ForeignKey(Artist, on_delete=models.CASCADE) user = models.ForeignKey(User, on_delete=models.CASCADE) role = models.CharField( - choices=ROLE_CHOICES, max_length=12, help_text='The relationship of this user to the artist' + choices=ROLE_CHOICES, + max_length=12, + help_text="The relationship of this user to the artist", ) def __str__(self): @@ -127,7 +150,7 @@ def __str__(self): class Bio(models.Model): artist = models.OneToOneField(Artist, on_delete=models.CASCADE) - bio = models.TextField(help_text='Short biography of artist. ' + markdown_allowed()) + bio = models.TextField(help_text="Short biography of artist. " + markdown_allowed()) def __str__(self): return str(self.artist) @@ -136,7 +159,9 @@ def __str__(self): class Photo(models.Model): artist = models.OneToOneField(Artist, on_delete=models.CASCADE) - img = models.ImageField(upload_to='artist', help_text='Primary profile photo of artist') + img = models.ImageField( + upload_to="artist", help_text="Primary profile photo of artist" + ) def __str__(self): return str(self.artist) @@ -144,19 +169,25 @@ def __str__(self): class Playlist(models.Model): - PLAYLIST_PROVIDER_SPOTIFY = 'spotify' - PLAYLIST_PROVIDER_SOUNDCLOUD = 'soundcloud' + PLAYLIST_PROVIDER_SPOTIFY = "spotify" + PLAYLIST_PROVIDER_SOUNDCLOUD = "soundcloud" PLAYLIST_PROVIDER_CHOICES = ( - (PLAYLIST_PROVIDER_SPOTIFY, 'Spotify',), - (PLAYLIST_PROVIDER_SOUNDCLOUD, 'SoundCloud',), + (PLAYLIST_PROVIDER_SPOTIFY, "Spotify"), + (PLAYLIST_PROVIDER_SOUNDCLOUD, "SoundCloud"), ) artist = models.ForeignKey(Artist, on_delete=models.CASCADE) - provider = models.CharField(choices=PLAYLIST_PROVIDER_CHOICES, max_length=10, help_text='Provider of the playlist') - uri = models.TextField(help_text='URI that with the provider uniquely identifies a playlist') + provider = models.CharField( + choices=PLAYLIST_PROVIDER_CHOICES, + max_length=10, + help_text="Provider of the playlist", + ) + uri = models.TextField( + help_text="URI that with the provider uniquely identifies a playlist" + ) class Meta: - unique_together = (('provider', 'uri',),) + unique_together = (("provider", "uri"),) def __str__(self): return self.uri @@ -168,43 +199,50 @@ def html(self): src="https://w.soundcloud.com/player/?url={url}&color=ff5500" > - """.format(url=self.uri) + """.format( + url=self.uri + ) elif self.provider == self.PLAYLIST_PROVIDER_SPOTIFY: return """ - """.format(uri=self.uri) + """.format( + uri=self.uri + ) class Social(models.Model): - SOCIAL_TWITTER = 'twitter' + SOCIAL_TWITTER = "twitter" SOCIAL_CHOICES = ( - ('facebook', 'Facebook'), - (SOCIAL_TWITTER, 'Twitter'), - ('instagram', 'Instagram'), - ('youtube', 'YouTube'), - ('soundcloud', 'SoundCloud'), + ("facebook", "Facebook"), + (SOCIAL_TWITTER, "Twitter"), + ("instagram", "Instagram"), + ("youtube", "YouTube"), + ("soundcloud", "SoundCloud"), ) artist = models.ForeignKey(Artist, on_delete=models.CASCADE) - medium = models.CharField(choices=SOCIAL_CHOICES, max_length=10, help_text='The type of social network') - url = models.URLField(unique=True, help_text='The URL to the artist\'s social network page') + medium = models.CharField( + choices=SOCIAL_CHOICES, max_length=10, help_text="The type of social network" + ) + url = models.URLField( + unique=True, help_text="The URL to the artist's social network page" + ) class Meta: - unique_together = (('artist', 'medium',),) + unique_together = (("artist", "medium"),) def __str__(self): - return u'{artist}: {medium}'.format( - artist=str(self.artist), - medium=self.get_medium_display() + return u"{artist}: {medium}".format( + artist=str(self.artist), medium=self.get_medium_display() ) def username_twitter(self): if self.medium == self.SOCIAL_TWITTER: - return '@{username}'.format(username=self.url.split('/')[-1]) + return "@{username}".format(username=self.url.split("/")[-1]) return self.url @@ -213,7 +251,9 @@ class Update(models.Model): artist = models.ForeignKey(Artist, on_delete=models.CASCADE) created_datetime = models.DateTimeField(db_index=True, auto_now_add=True) title = models.CharField(max_length=75) - text = models.TextField(help_text='The content of the update. ' + markdown_allowed()) + text = models.TextField( + help_text="The content of the update. " + markdown_allowed() + ) def __str__(self): return self.title @@ -222,7 +262,7 @@ def __str__(self): class UpdateImage(models.Model): update = models.ForeignKey(Update, on_delete=models.CASCADE) - img = models.ImageField(upload_to='/'.join(['artist', 'updates'])) + img = models.ImageField(upload_to="/".join(["artist", "updates"])) def __str__(self): return str(self.update) @@ -230,10 +270,8 @@ def __str__(self): class UpdateMediaURL(models.Model): - MEDIA_YOUTUBE = 'youtube' - MEDIA_CHOICES = ( - (MEDIA_YOUTUBE, 'YouTube',), - ) + MEDIA_YOUTUBE = "youtube" + MEDIA_CHOICES = ((MEDIA_YOUTUBE, "YouTube"),) update = models.ForeignKey(Update, on_delete=models.CASCADE) media_type = models.CharField(choices=MEDIA_CHOICES, max_length=8) @@ -247,23 +285,27 @@ def clean_youtube_url(self): # A hack to correct youtu.be links and normal watch links into embed links # TODO: Make more robust using regex and getting all query parameters - if 'youtu.be/' in url: - url = url.replace('youtu.be/', 'youtube.com/watch?v=') + if "youtu.be/" in url: + url = url.replace("youtu.be/", "youtube.com/watch?v=") return url def thumbnail_html(self): if self.media_type == self.MEDIA_YOUTUBE: - url = self.clean_youtube_url().replace('www.youtube.com', 'youtube.com') + url = self.clean_youtube_url().replace("www.youtube.com", "youtube.com") thumbnail_url = "{base}/hqdefault.jpg".format( - base=url.replace('youtube.com/watch?v=', 'img.youtube.com/vi/') + base=url.replace("youtube.com/watch?v=", "img.youtube.com/vi/") + ) + return u''.format( + url=url, thumbnail_url=thumbnail_url ) - return u"".format(url=url, thumbnail_url=thumbnail_url) def embed_html(self): if self.media_type == self.MEDIA_YOUTUBE: - url = self.clean_youtube_url().replace('/watch?v=', '/embed/') + url = self.clean_youtube_url().replace("/watch?v=", "/embed/") return u"""
- """.format(url=url) + """.format( + url=url + ) diff --git a/artist/tests.py b/artist/tests.py index 7b21f366..31f77f25 100644 --- a/artist/tests.py +++ b/artist/tests.py @@ -14,7 +14,11 @@ from geopy.exc import GeocoderTimedOut from artist.factories import ( - ArtistAdminFactory, ArtistFactory, GenreFactory, artistfactory_factory, updatefactory_factory + ArtistAdminFactory, + ArtistFactory, + GenreFactory, + artistfactory_factory, + updatefactory_factory, ) from artist.models import Artist, Playlist as PlaylistConst from campaign.factories import CampaignFactory, InvestmentFactory @@ -23,8 +27,8 @@ class SetInitialUpdateTitlesMigrationTestCase(MigrationTestCase): - migrate_from = '0005_auto_20160522_2328' - migrate_to = '0006_updatetitles' + migrate_from = "0005_auto_20160522_2328" + migrate_to = "0006_updatetitles" def setUpBeforeMigration(self, apps): # Create an update @@ -34,32 +38,34 @@ def setUpBeforeMigration(self, apps): def testUpdatesHaveInitialTitles(self): today = timezone.now().strftime("%m/%d/%Y") self.update.refresh_from_db() - self.assertTrue(self.update.title.endswith("Update: {today}".format(today=today))) + self.assertTrue( + self.update.title.endswith("Update: {today}".format(today=today)) + ) class SoundCloudPlaylistToPlaylistMigrationTestCase(MigrationTestCase): - migrate_from = '0009_auto_20170201_0753' - migrate_to = '0010_auto_20170201_0754' + migrate_from = "0009_auto_20170201_0753" + migrate_to = "0010_auto_20170201_0754" def setUpBeforeMigration(self, apps): class SoundCloudPlaylistFactoryForMigrationTestCase(factory.DjangoModelFactory): class Meta: - model = apps.get_model('artist', 'SoundCloudPlaylist') + model = apps.get_model("artist", "SoundCloudPlaylist") + artist = factory.SubFactory(artistfactory_factory(apps=apps)) # Create a SoundCloudPlaylist self.soundcloudplaylist = SoundCloudPlaylistFactoryForMigrationTestCase() def testPlaylistURIIsFromSoundCloudPlaylist(self): - Playlist = self.apps.get_model('artist', 'Playlist') + Playlist = self.apps.get_model("artist", "Playlist") playlist = Playlist.objects.get() self.assertEqual(playlist.provider, PlaylistConst.PLAYLIST_PROVIDER_SOUNDCLOUD) self.assertEqual(playlist.uri, self.soundcloudplaylist.playlist) class ArtistModelsTestCase(TestCase): - def testUnicodeOfGenreIsGenreName(self): genre = GenreFactory() self.assertEqual(str(genre), genre.name) @@ -74,8 +80,7 @@ def testUnicodeOfArtistAdminIsUser(self): class ArtistManagerTestCase(TestCase): - - @mock.patch('campaign.models.Campaign.percentage_funded') + @mock.patch("campaign.models.Campaign.percentage_funded") def testFilterByFunded(self, mock_percentage_funded): mock_percentage_funded.return_value = 100 @@ -92,13 +97,11 @@ def testFilterByFunded(self, mock_percentage_funded): class ArtistAdminWebTestCase(PerDiemTestCase): - def testLocationWidgetRenders(self): - self.assertResponseRenders('/admin/artist/artist/add/') + self.assertResponseRenders("/admin/artist/artist/add/") class ArtistWebTestCase(PerDiemTestCase): - @classmethod def setUpTestData(cls): super(ArtistWebTestCase, cls).setUpTestData() @@ -107,67 +110,69 @@ def setUpTestData(cls): def get200s(self): return [ - '/artists/', - '/artists/?genre=Progressive+Rock', - '/artists/?distance=50&lat=43.7689&lon=-79.4138', - '/artists/?sort=recent', - '/artists/?sort=funded', - '/artists/?sort=time-remaining', - '/artists/?sort=investors', - '/artists/?sort=raised', - '/artists/?sort=valuation', - '/artist/apply/', - '/artist/{slug}/'.format(slug=self.artist.slug), + "/artists/", + "/artists/?genre=Progressive+Rock", + "/artists/?distance=50&lat=43.7689&lon=-79.4138", + "/artists/?sort=recent", + "/artists/?sort=funded", + "/artists/?sort=time-remaining", + "/artists/?sort=investors", + "/artists/?sort=raised", + "/artists/?sort=valuation", + "/artist/apply/", + "/artist/{slug}/".format(slug=self.artist.slug), ] def testArtistDetailPageUnauthenticated(self): self.client.logout() - self.assertResponseRenders('/artist/{slug}/'.format(slug=self.artist.slug)) + self.assertResponseRenders("/artist/{slug}/".format(slug=self.artist.slug)) def testArtistDetailPageWithInvestor(self): # User invests in the campaign InvestmentFactory(charge__customer__user=self.user, campaign=self.campaign) # Verify that the user appears as an investor in the campaign - response = self.assertResponseRenders('/artist/{slug}/'.format(slug=self.artist.slug)) - self.assertIn('user_investor', response.context) + response = self.assertResponseRenders( + "/artist/{slug}/".format(slug=self.artist.slug) + ) + self.assertIn("user_investor", response.context) - @mock.patch('artist.views.geolocator.geocode') + @mock.patch("artist.views.geolocator.geocode") def testGeocoderInArtistList(self, mock_geocode): - url = '/artists/?distance=50&location=Toronto,%20ON' + url = "/artists/?distance=50&location=Toronto,%20ON" # First the Geocoder service fails and so we display warning to user mock_geocode.side_effect = GeocoderTimedOut response = self.assertResponseRenders(url) - self.assertIn(b'Geocoding failed.', response.content) + self.assertIn(b"Geocoding failed.", response.content) # Then the Geocoder service kicks back online and we succeed mock_geocode.side_effect = None mock_geocode.return_value = mock.Mock(latitude=43.653226, longitude=-79.383184) response = self.assertResponseRenders(url) - self.assertNotIn(b'Geocoding failed.', response.content) + self.assertNotIn(b"Geocoding failed.", response.content) def testArtistDoesNotExistReturns404(self): - self.assertResponseRenders('/artist/does-not-exist/', status_code=404) + self.assertResponseRenders("/artist/does-not-exist/", status_code=404) def testArtistApplication(self): self.assertResponseRedirects( - '/artist/apply/', - '/artist/apply/thanks', - method='POST', + "/artist/apply/", + "/artist/apply/thanks", + method="POST", data={ - 'artist_name': 'Segmentation Fault', - 'genre': 'Heavy Metal', - 'hometown': 'Waterloo, ON, Canada', - 'email': self.user.email, - 'phone_number': '(226) 123-4567', - 'bio': ( - 'We are a really cool heavy metal band. We mostly perform covers but are excited to ' - 'create an album, and we\'re hoping PerDiem can help us do that.' + "artist_name": "Segmentation Fault", + "genre": "Heavy Metal", + "hometown": "Waterloo, ON, Canada", + "email": self.user.email, + "phone_number": "(226) 123-4567", + "bio": ( + "We are a really cool heavy metal band. We mostly perform covers but are excited to " + "create an album, and we're hoping PerDiem can help us do that." ), - 'campaign_reason': 'We want to record our next album: Access Granted.', - 'campaign_expenses': 'Studio time, mastering, mixing, etc.', - 'music_link': 'https://www.spotify.com/', - 'terms': True, - } + "campaign_reason": "We want to record our next album: Access Granted.", + "campaign_expenses": "Studio time, mastering, mixing, etc.", + "music_link": "https://www.spotify.com/", + "terms": True, + }, ) diff --git a/artist/views.py b/artist/views.py index b33fe9d4..8e1a5cab 100644 --- a/artist/views.py +++ b/artist/views.py @@ -22,40 +22,40 @@ class ArtistListView(ListView): - template_name = 'artist/artist_list.html' - context_object_name = 'artists' + template_name = "artist/artist_list.html" + context_object_name = "artists" ORDER_BY_NAME = { - 'recent': 'Recently Added', - 'funded': '% Funded', - 'time-remaining': 'Time to Go', - 'investors': '# Investors', - 'raised': 'Amount Raised', - 'valuation': 'Valuation', + "recent": "Recently Added", + "funded": "% Funded", + "time-remaining": "Time to Go", + "investors": "# Investors", + "raised": "Amount Raised", + "valuation": "Valuation", } ORDER_BY_METHOD = { - 'funded': 'order_by_percentage_funded', - 'time-remaining': 'order_by_time_remaining', - 'investors': 'order_by_num_investors', - 'raised': 'order_by_amount_raised', - 'valuation': 'order_by_valuation', + "funded": "order_by_percentage_funded", + "time-remaining": "order_by_time_remaining", + "investors": "order_by_num_investors", + "raised": "order_by_amount_raised", + "valuation": "order_by_valuation", } def dispatch(self, request, *args, **kwargs): # Filtering - self.active_genre = request.GET.get('genre', 'All Genres') - self.distance = request.GET.get('distance') - self.location = request.GET.get('location') - self.lat = request.GET.get('lat') - self.lon = request.GET.get('lon') + self.active_genre = request.GET.get("genre", "All Genres") + self.distance = request.GET.get("distance") + self.location = request.GET.get("location") + self.lat = request.GET.get("lat") + self.lon = request.GET.get("lon") # Sorting - order_by_slug = request.GET.get('sort') + order_by_slug = request.GET.get("sort") if order_by_slug not in self.ORDER_BY_NAME: - order_by_slug = 'recent' + order_by_slug = "recent" self.order_by = { - 'slug': order_by_slug, - 'name': self.ORDER_BY_NAME[order_by_slug], + "slug": order_by_slug, + "name": self.ORDER_BY_NAME[order_by_slug], } # Geolocate if location @@ -71,34 +71,47 @@ def dispatch(self, request, *args, **kwargs): def get_context_data(self, **kwargs): context = super(ArtistListView, self).get_context_data(**kwargs) - sort_options = [{'slug': s, 'name': n} for s, n in self.ORDER_BY_NAME.items()] - context.update({ - 'genres': Genre.objects.all().order_by('name').values_list('name', flat=True), - 'active_genre': self.active_genre, - 'distance': self.distance if (self.lat and self.lon) or self.location else None, - 'location': self.location, - 'lat': self.lat, - 'lon': self.lon, - 'geocoder_failed': self.geocoder_failed, - 'sort_options': sorted(sort_options, key=lambda o: o['name']), - 'order_by': self.order_by, - }) + sort_options = [{"slug": s, "name": n} for s, n in self.ORDER_BY_NAME.items()] + context.update( + { + "genres": Genre.objects.all() + .order_by("name") + .values_list("name", flat=True), + "active_genre": self.active_genre, + "distance": self.distance + if (self.lat and self.lon) or self.location + else None, + "location": self.location, + "lat": self.lat, + "lon": self.lon, + "geocoder_failed": self.geocoder_failed, + "sort_options": sorted(sort_options, key=lambda o: o["name"]), + "order_by": self.order_by, + } + ) return context def filter_by_location(self, artists): - if self.distance and ((self.lat and self.lon) or (self.location and self.location_coordinates)): + if self.distance and ( + (self.lat and self.lon) or (self.location and self.location_coordinates) + ): if self.lat and self.lon: lat, lon = self.lat, self.lon elif self.location and self.location_coordinates: - lat, lon = self.location_coordinates.latitude, self.location_coordinates.longitude - artists = artists.filter_by_location(distance=int(self.distance), lat=lat, lon=lon) + lat, lon = ( + self.location_coordinates.latitude, + self.location_coordinates.longitude, + ) + artists = artists.filter_by_location( + distance=int(self.distance), lat=lat, lon=lon + ) return artists def sort_artists(self, artists): - order_by_name = self.order_by['slug'] + order_by_name = self.order_by["slug"] if order_by_name in self.ORDER_BY_METHOD: return getattr(artists, self.ORDER_BY_METHOD[order_by_name]) - return artists.order_by('-id') + return artists.order_by("-id") def get_queryset(self): artists = Artist.objects.all() @@ -111,54 +124,69 @@ def get_queryset(self): class ArtistDetailView(FormView): - template_name = 'artist/artist_detail.html' + template_name = "artist/artist_detail.html" form_class = ArtistUpdateForm def get_success_url(self): - return reverse('artist', kwargs={'slug': self.slug}) + return reverse("artist", kwargs={"slug": self.slug}) def dispatch(self, request, *args, **kwargs): - self.slug = kwargs['slug'] + self.slug = kwargs["slug"] self.artist = get_object_or_404(Artist, slug=self.slug) return super(ArtistDetailView, self).dispatch(request, *args, **kwargs) def get_context_data(self, *args, **kwargs): context = super(ArtistDetailView, self).get_context_data(*args, **kwargs) - user_has_permission_to_submit_update = self.artist.has_permission_to_submit_update(self.request.user) - context.update({ - 'PINAX_STRIPE_PUBLIC_KEY': settings.PINAX_STRIPE_PUBLIC_KEY, - 'STRIPE_PERCENTAGE': settings.STRIPE_PERCENTAGE, - 'STRIPE_FLAT_FEE': settings.STRIPE_FLAT_FEE, - 'DEFAULT_MIN_PURCHASE': settings.DEFAULT_MIN_PURCHASE, - 'has_permission_to_submit_update': user_has_permission_to_submit_update, - }) + user_has_permission_to_submit_update = self.artist.has_permission_to_submit_update( + self.request.user + ) + context.update( + { + "PINAX_STRIPE_PUBLIC_KEY": settings.PINAX_STRIPE_PUBLIC_KEY, + "STRIPE_PERCENTAGE": settings.STRIPE_PERCENTAGE, + "STRIPE_FLAT_FEE": settings.STRIPE_FLAT_FEE, + "DEFAULT_MIN_PURCHASE": settings.DEFAULT_MIN_PURCHASE, + "has_permission_to_submit_update": user_has_permission_to_submit_update, + } + ) - context['artist'] = self.artist + context["artist"] = self.artist investors = self.artist.investors() - context['investors'] = sorted( + context["investors"] = sorted( investors.values(), - key=lambda investor: investor['total_investment'], - reverse=True + key=lambda investor: investor["total_investment"], + reverse=True, ) campaign = self.artist.active_campaign() if campaign: - context['campaign'] = campaign - context['fans_percentage'] = context['fans_percentage_display'] = campaign.project.total_fans_percentage() + context["campaign"] = campaign + context["fans_percentage"] = context[ + "fans_percentage_display" + ] = campaign.project.total_fans_percentage() if self.request.user.is_authenticated: user_investor = investors.get(self.request.user.id) if user_investor: - user_investor['percentage_display'] = max(0.5, user_investor.get('percentage', 0)) - context['fans_percentage'] -= user_investor['percentage'] - context['fans_percentage_display'] -= user_investor['percentage_display'] - context['user_investor'] = user_investor - - user_is_investor = self.request.user.is_authenticated and self.artist.is_investor(self.request.user) + user_investor["percentage_display"] = max( + 0.5, user_investor.get("percentage", 0) + ) + context["fans_percentage"] -= user_investor["percentage"] + context["fans_percentage_display"] -= user_investor[ + "percentage_display" + ] + context["user_investor"] = user_investor + + user_is_investor = ( + self.request.user.is_authenticated + and self.artist.is_investor(self.request.user) + ) if user_has_permission_to_submit_update or user_is_investor: - context['updates'] = self.artist.update_set.all().order_by('-created_datetime') - context['latest_campaign'] = self.artist.latest_campaign() + context["updates"] = self.artist.update_set.all().order_by( + "-created_datetime" + ) + context["latest_campaign"] = self.artist.latest_campaign() return context @@ -170,21 +198,25 @@ def form_valid(self, form): return HttpResponseForbidden() # Create the base update - update = Update.objects.create(artist=self.artist, title=d['title'], text=d['text']) + update = Update.objects.create( + artist=self.artist, title=d["title"], text=d["text"] + ) # Attach images/videos to the update - image = d['image'] + image = d["image"] if image: UpdateImage.objects.create(update=update, img=image) - youtube_url = d['youtube_url'] + youtube_url = d["youtube_url"] if youtube_url: - UpdateMediaURL.objects.create(update=update, media_type=UpdateMediaURL.MEDIA_YOUTUBE, url=youtube_url) + UpdateMediaURL.objects.create( + update=update, media_type=UpdateMediaURL.MEDIA_YOUTUBE, url=youtube_url + ) # Send email to users following the artist's updates investors = User.objects.filter( customer__charges__paid=True, customer__charges__refunded=False, - customer__charges__investment__campaign__project__artist=self.artist + customer__charges__investment__campaign__project__artist=self.artist, ).distinct() for investor in investors: ArtistUpdateEmail().send(user=investor, update=update) @@ -194,17 +226,17 @@ def form_valid(self, form): class ArtistApplyFormView(FormView): - template_name = 'artist/artist_application.html' + template_name = "artist/artist_application.html" form_class = ArtistApplyForm def get_success_url(self): - return reverse('artist_application_thanks') + return reverse("artist_application_thanks") def get_initial(self): initial = super(ArtistApplyFormView, self).get_initial() user = self.request.user if user.is_authenticated: - initial['email'] = user.email + initial["email"] = user.email return initial def form_valid(self, form): @@ -212,9 +244,11 @@ def form_valid(self, form): context = form.cleaned_data user = self.request.user if user.is_authenticated: - context['user_id'] = user.id + context["user_id"] = user.id # Send artist application email - ArtistApplyEmail().send_to_email(email='info@investperdiem.com', context=context) + ArtistApplyEmail().send_to_email( + email="info@investperdiem.com", context=context + ) return super(ArtistApplyFormView, self).form_valid(form) diff --git a/campaign/admin.py b/campaign/admin.py index 5ff06f4b..71197edd 100644 --- a/campaign/admin.py +++ b/campaign/admin.py @@ -8,16 +8,41 @@ from django.contrib import admin from pinax.stripe.models import ( - Account, BankAccount, Charge, Coupon, Customer, Event, EventProcessingException, Invoice, Plan, UserAccount + Account, + BankAccount, + Charge, + Coupon, + Customer, + Event, + EventProcessingException, + Invoice, + Plan, + UserAccount, ) from artist.models import ArtistAdmin -from campaign.models import ArtistPercentageBreakdown, Campaign, Expense, Investment, Project, RevenueReport +from campaign.models import ( + ArtistPercentageBreakdown, + Campaign, + Expense, + Investment, + Project, + RevenueReport, +) # Unregister Pinax Stripe models from admin for pinax_stripe_model in [ - Account, BankAccount, Charge, Coupon, Customer, Event, EventProcessingException, Invoice, Plan, UserAccount, + Account, + BankAccount, + Charge, + Coupon, + Customer, + Event, + EventProcessingException, + Invoice, + Plan, + UserAccount, ]: admin.site.unregister(pinax_stripe_model) @@ -25,26 +50,33 @@ class CampaignAdminForm(forms.ModelForm): fans_percentage = forms.IntegerField( - min_value=0, max_value=100, help_text=Campaign._meta.get_field('fans_percentage').help_text + min_value=0, + max_value=100, + help_text=Campaign._meta.get_field("fans_percentage").help_text, ) class Meta: model = Campaign fields = ( - 'project', 'amount', 'value_per_share', 'start_datetime', 'end_datetime', 'use_of_funds', 'fans_percentage', + "project", + "amount", + "value_per_share", + "start_datetime", + "end_datetime", + "use_of_funds", + "fans_percentage", ) def clean(self): cleaned_data = super(CampaignAdminForm, self).clean() - start_datetime = cleaned_data.get('start_datetime') - end_datetime = cleaned_data.get('end_datetime') + start_datetime = cleaned_data.get("start_datetime") + end_datetime = cleaned_data.get("end_datetime") if start_datetime and end_datetime and end_datetime < start_datetime: raise forms.ValidationError("Campaign cannot end before it begins.") return cleaned_data class ArtistPercentageBreakdownFormset(forms.models.BaseInlineFormSet): - def clean(self): super(ArtistPercentageBreakdownFormset, self).clean() total_artist_percentage = self.instance.total_artist_percentage() @@ -53,9 +85,9 @@ def clean(self): # Verify that the artist percentage adds up for form in self.forms: - if 'percentage' in form.cleaned_data and not form.cleaned_data['DELETE']: + if "percentage" in form.cleaned_data and not form.cleaned_data["DELETE"]: num_forms += 1 - percentage = form.cleaned_data['percentage'] + percentage = form.cleaned_data["percentage"] if percentage < 0 or percentage > 100: raise forms.ValidationError("Percentages must be between 0-100.") artist_percentage_so_far += percentage @@ -75,9 +107,13 @@ class ArtistPercentageBreakdownInline(admin.StackedInline): formset = ArtistPercentageBreakdownFormset def get_formset(self, request, obj=None, **kwargs): - formset = super(ArtistPercentageBreakdownInline, self).get_formset(request, obj, **kwargs) + formset = super(ArtistPercentageBreakdownInline, self).get_formset( + request, obj, **kwargs + ) # Limit ArtistAdmins to the ArtistAdmins for the artist in this campaign - formset.form.base_fields['artist_admin'].queryset = ArtistAdmin.objects.filter(artist=obj.artist) + formset.form.base_fields["artist_admin"].queryset = ArtistAdmin.objects.filter( + artist=obj.artist + ) return formset @@ -87,14 +123,15 @@ class ExpenseInline(admin.TabularInline): class ProjectAdmin(admin.ModelAdmin): - def get_inline_instances(self, request, obj=None, **kwargs): inline_instances = [] # Only show ArtistPercentageBreakdownInline in edit view # when the project has campaigns if obj and obj.campaign_set.exists(): - inline_instances.append(ArtistPercentageBreakdownInline(self.model, self.admin_site)) + inline_instances.append( + ArtistPercentageBreakdownInline(self.model, self.admin_site) + ) for inline in self.inlines: inline_instances.append(inline(self.model, self.admin_site)) @@ -104,13 +141,13 @@ def get_inline_instances(self, request, obj=None, **kwargs): class CampaignAdmin(admin.ModelAdmin): form = CampaignAdminForm - raw_id_fields = ('project',) + raw_id_fields = ("project",) inlines = (ExpenseInline,) class InvestmentAdmin(admin.ModelAdmin): - list_display = ('id', 'campaign', 'investor', 'transaction_datetime', 'num_shares',) + list_display = ("id", "campaign", "investor", "transaction_datetime", "num_shares") readonly_fields = list(map(lambda f: f.name, Investment._meta.get_fields())) def has_add_permission(self, request, obj=None): diff --git a/campaign/apps.py b/campaign/apps.py index d14c5e8f..fad9d2d2 100644 --- a/campaign/apps.py +++ b/campaign/apps.py @@ -3,7 +3,7 @@ class CampaignConfig(AppConfig): - name = 'campaign' + name = "campaign" def ready(self): import campaign.signals diff --git a/campaign/factories.py b/campaign/factories.py index b47dccab..30537911 100644 --- a/campaign/factories.py +++ b/campaign/factories.py @@ -8,9 +8,8 @@ def projectfactory_factory(apps): class ProjectFactory(factory.DjangoModelFactory): - class Meta: - model = apps.get_model('campaign', 'Project') + model = apps.get_model("campaign", "Project") artist = factory.SubFactory(artistfactory_factory(apps=apps)) @@ -19,9 +18,8 @@ class Meta: def campaignfactory_factory(apps, point_to_project=True): class CampaignFactory(factory.DjangoModelFactory): - class Meta: - model = apps.get_model('campaign', 'Campaign') + model = apps.get_model("campaign", "Campaign") amount = 10000 fans_percentage = 20 @@ -38,9 +36,8 @@ class Meta: def revenuereportfactory_factory(apps, point_to_project=True): class RevenueReportFactory(factory.DjangoModelFactory): - class Meta: - model = apps.get_model('campaign', 'RevenueReport') + model = apps.get_model("campaign", "RevenueReport") # Allow the RevenueReportFactory to point to a campaign directly # for migration test cases before the Project model was created @@ -58,17 +55,15 @@ class Meta: class CustomerFactory(factory.DjangoModelFactory): - class Meta: - model = django_apps.get_model('pinax_stripe', 'Customer') + model = django_apps.get_model("pinax_stripe", "Customer") user = factory.SubFactory(UserFactory) class ChargeFactory(factory.DjangoModelFactory): - class Meta: - model = django_apps.get_model('pinax_stripe', 'Charge') + model = django_apps.get_model("pinax_stripe", "Charge") customer = factory.SubFactory(CustomerFactory) paid = True @@ -76,9 +71,8 @@ class Meta: class InvestmentFactory(factory.DjangoModelFactory): - class Meta: - model = django_apps.get_model('campaign', 'Investment') + model = django_apps.get_model("campaign", "Investment") charge = factory.SubFactory(ChargeFactory) campaign = factory.SubFactory(CampaignFactory) diff --git a/campaign/forms.py b/campaign/forms.py index eee99cf0..992b1bed 100644 --- a/campaign/forms.py +++ b/campaign/forms.py @@ -13,11 +13,13 @@ class PaymentChargeForm(forms.Form): num_shares = forms.IntegerField(min_value=1) def __init__(self, *args, **kwargs): - self.campaign = kwargs.pop('campaign') + self.campaign = kwargs.pop("campaign") super(PaymentChargeForm, self).__init__(*args, **kwargs) def clean_num_shares(self): - num_shares = self.cleaned_data['num_shares'] + num_shares = self.cleaned_data["num_shares"] if num_shares > self.campaign.num_shares_remaining(): - raise forms.ValidationError("The number of shares requested exceeds the number of shares available.") + raise forms.ValidationError( + "The number of shares requested exceeds the number of shares available." + ) return num_shares diff --git a/campaign/migrations/0001_initial.py b/campaign/migrations/0001_initial.py index e200190d..ab962aac 100644 --- a/campaign/migrations/0001_initial.py +++ b/campaign/migrations/0001_initial.py @@ -12,53 +12,177 @@ class Migration(migrations.Migration): dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('artist', '0001_initial'), + ("artist", "0001_initial"), ] operations = [ migrations.CreateModel( - name='Campaign', + name="Campaign", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('amount', models.PositiveIntegerField(help_text='The amount of money the artist wishes to raise')), - ('reason', models.CharField(help_text='The reason why the artist is raising money, in a few words', max_length=40)), - ('value_per_share', models.PositiveIntegerField(default=1, help_text='The value (in dollars) per share the artist wishes to sell')), - ('start_datetime', models.DateTimeField(db_index=True, default=django.utils.timezone.now, help_text='When the campaign will start accepting investors')), - ('end_datetime', models.DateTimeField(blank=True, db_index=True, help_text='When the campaign ends and will no longer accept investors (no end date if blank)', null=True)), - ('use_of_funds', models.TextField(blank=True, help_text='A description of how the funds will be used', null=True)), - ('fans_percentage', models.PositiveSmallIntegerField(help_text='The percentage of revenue that goes back to the fans (a value from 0-100)')), - ('artist', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='artist.Artist')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "amount", + models.PositiveIntegerField( + help_text="The amount of money the artist wishes to raise" + ), + ), + ( + "reason", + models.CharField( + help_text="The reason why the artist is raising money, in a few words", + max_length=40, + ), + ), + ( + "value_per_share", + models.PositiveIntegerField( + default=1, + help_text="The value (in dollars) per share the artist wishes to sell", + ), + ), + ( + "start_datetime", + models.DateTimeField( + db_index=True, + default=django.utils.timezone.now, + help_text="When the campaign will start accepting investors", + ), + ), + ( + "end_datetime", + models.DateTimeField( + blank=True, + db_index=True, + help_text="When the campaign ends and will no longer accept investors (no end date if blank)", + null=True, + ), + ), + ( + "use_of_funds", + models.TextField( + blank=True, + help_text="A description of how the funds will be used", + null=True, + ), + ), + ( + "fans_percentage", + models.PositiveSmallIntegerField( + help_text="The percentage of revenue that goes back to the fans (a value from 0-100)" + ), + ), + ( + "artist", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="artist.Artist" + ), + ), ], ), migrations.CreateModel( - name='Expense', + name="Expense", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('expense', models.CharField(help_text='A description of one of the expenses for the artist (eg. Studio cost)', max_length=30)), - ('campaign', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='campaign.Campaign')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "expense", + models.CharField( + help_text="A description of one of the expenses for the artist (eg. Studio cost)", + max_length=30, + ), + ), + ( + "campaign", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="campaign.Campaign", + ), + ), ], ), migrations.CreateModel( - name='Investment', + name="Investment", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('transaction_datetime', models.DateTimeField(auto_now_add=True, db_index=True)), - ('num_shares', models.PositiveSmallIntegerField(default=1, help_text='The number of shares a user made in a transaction')), - ('campaign', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='campaign.Campaign')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "transaction_datetime", + models.DateTimeField(auto_now_add=True, db_index=True), + ), + ( + "num_shares", + models.PositiveSmallIntegerField( + default=1, + help_text="The number of shares a user made in a transaction", + ), + ), + ( + "campaign", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="campaign.Campaign", + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), ], ), migrations.CreateModel( - name='RevenueReport', + name="RevenueReport", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('amount', models.PositiveIntegerField(help_text='The amount of revenue generated (in dollars) being reported (since last report)')), - ('reported_datetime', models.DateTimeField(auto_now_add=True)), - ('campaign', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='campaign.Campaign')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "amount", + models.PositiveIntegerField( + help_text="The amount of revenue generated (in dollars) being reported (since last report)" + ), + ), + ("reported_datetime", models.DateTimeField(auto_now_add=True)), + ( + "campaign", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="campaign.Campaign", + ), + ), ], ), migrations.AlterUniqueTogether( - name='expense', - unique_together=set([('campaign', 'expense')]), + name="expense", unique_together=set([("campaign", "expense")]) ), ] diff --git a/campaign/migrations/0002_auto_20160411_0246.py b/campaign/migrations/0002_auto_20160411_0246.py index 91e4d1ce..e128c22b 100644 --- a/campaign/migrations/0002_auto_20160411_0246.py +++ b/campaign/migrations/0002_auto_20160411_0246.py @@ -7,19 +7,20 @@ class Migration(migrations.Migration): dependencies = [ - ('pinax_stripe', '0003_make_cvc_check_blankable'), - ('campaign', '0001_initial'), + ("pinax_stripe", "0003_make_cvc_check_blankable"), + ("campaign", "0001_initial"), ] operations = [ - migrations.RemoveField( - model_name='investment', - name='user', - ), + migrations.RemoveField(model_name="investment", name="user"), migrations.AddField( - model_name='investment', - name='charge', - field=models.OneToOneField(default=None, on_delete=django.db.models.deletion.CASCADE, to='pinax_stripe.Charge'), + model_name="investment", + name="charge", + field=models.OneToOneField( + default=None, + on_delete=django.db.models.deletion.CASCADE, + to="pinax_stripe.Charge", + ), preserve_default=False, ), ] diff --git a/campaign/migrations/0003_auto_20160418_0057.py b/campaign/migrations/0003_auto_20160418_0057.py index 47c08436..a4fdff53 100644 --- a/campaign/migrations/0003_auto_20160418_0057.py +++ b/campaign/migrations/0003_auto_20160418_0057.py @@ -5,14 +5,15 @@ class Migration(migrations.Migration): - dependencies = [ - ('campaign', '0002_auto_20160411_0246'), - ] + dependencies = [("campaign", "0002_auto_20160411_0246")] operations = [ migrations.AlterField( - model_name='investment', - name='num_shares', - field=models.PositiveSmallIntegerField(default=1, help_text='The number of shares an investor made in a transaction'), - ), + model_name="investment", + name="num_shares", + field=models.PositiveSmallIntegerField( + default=1, + help_text="The number of shares an investor made in a transaction", + ), + ) ] diff --git a/campaign/migrations/0004_artistpercentagebreakdown.py b/campaign/migrations/0004_artistpercentagebreakdown.py index 5be47d86..78a0a67f 100644 --- a/campaign/migrations/0004_artistpercentagebreakdown.py +++ b/campaign/migrations/0004_artistpercentagebreakdown.py @@ -7,19 +7,52 @@ class Migration(migrations.Migration): dependencies = [ - ('artist', '0007_artistadmin'), - ('campaign', '0003_auto_20160418_0057'), + ("artist", "0007_artistadmin"), + ("campaign", "0003_auto_20160418_0057"), ] operations = [ migrations.CreateModel( - name='ArtistPercentageBreakdown', + name="ArtistPercentageBreakdown", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('displays_publicly_as', models.CharField(help_text="The name shown on the artist's detail page", max_length=30)), - ('percentage', models.FloatField(help_text='The percentage of revenue that goes back to this group/individual (a value from 0-100)')), - ('artist_admin', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='artist.ArtistAdmin')), - ('campaign', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='campaign.Campaign')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "displays_publicly_as", + models.CharField( + help_text="The name shown on the artist's detail page", + max_length=30, + ), + ), + ( + "percentage", + models.FloatField( + help_text="The percentage of revenue that goes back to this group/individual (a value from 0-100)" + ), + ), + ( + "artist_admin", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="artist.ArtistAdmin", + ), + ), + ( + "campaign", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="campaign.Campaign", + ), + ), ], - ), + ) ] diff --git a/campaign/migrations/0005_auto_20160618_2310.py b/campaign/migrations/0005_auto_20160618_2310.py index 6a7334de..fe477137 100644 --- a/campaign/migrations/0005_auto_20160618_2310.py +++ b/campaign/migrations/0005_auto_20160618_2310.py @@ -7,23 +7,47 @@ class Migration(migrations.Migration): dependencies = [ - ('artist', '0007_artistadmin'), - ('campaign', '0004_artistpercentagebreakdown'), + ("artist", "0007_artistadmin"), + ("campaign", "0004_artistpercentagebreakdown"), ] operations = [ migrations.CreateModel( - name='Project', + name="Project", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('reason', models.CharField(help_text='The reason why the artist is raising money, in a few words', max_length=40)), - ('artist', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='artist.Artist')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "reason", + models.CharField( + help_text="The reason why the artist is raising money, in a few words", + max_length=40, + ), + ), + ( + "artist", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="artist.Artist" + ), + ), ], ), migrations.AddField( - model_name='campaign', - name='project', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='campaign.Project'), + model_name="campaign", + name="project", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="campaign.Project", + ), preserve_default=False, ), ] diff --git a/campaign/migrations/0006_auto_20160618_2351.py b/campaign/migrations/0006_auto_20160618_2351.py index a9c6cf32..36e99c1c 100644 --- a/campaign/migrations/0006_auto_20160618_2351.py +++ b/campaign/migrations/0006_auto_20160618_2351.py @@ -5,18 +5,18 @@ class Migration(migrations.Migration): - dependencies = [ - ('campaign', '0005_auto_20160618_2310'), - ] + dependencies = [("campaign", "0005_auto_20160618_2310")] def create_initial_projects(apps, schema_editor): Project = apps.get_model("campaign", "Project") Campaign = apps.get_model("campaign", "Campaign") for campaign in Campaign.objects.all(): - project = Project.objects.create(artist=campaign.artist, reason=campaign.reason) + project = Project.objects.create( + artist=campaign.artist, reason=campaign.reason + ) campaign.project = project campaign.save() operations = [ - migrations.RunPython(create_initial_projects, migrations.RunPython.noop), + migrations.RunPython(create_initial_projects, migrations.RunPython.noop) ] diff --git a/campaign/migrations/0007_auto_20160618_2352.py b/campaign/migrations/0007_auto_20160618_2352.py index 5b226d10..a5ba8f34 100644 --- a/campaign/migrations/0007_auto_20160618_2352.py +++ b/campaign/migrations/0007_auto_20160618_2352.py @@ -6,35 +6,41 @@ class Migration(migrations.Migration): - dependencies = [ - ('campaign', '0006_auto_20160618_2351'), - ] + dependencies = [("campaign", "0006_auto_20160618_2351")] operations = [ migrations.AlterField( - model_name='campaign', - name='project', - field=models.ForeignKey(default=0, on_delete=django.db.models.deletion.CASCADE, to='campaign.Project'), + model_name="campaign", + name="project", + field=models.ForeignKey( + default=0, + on_delete=django.db.models.deletion.CASCADE, + to="campaign.Project", + ), preserve_default=False, ), - migrations.RemoveField( - model_name='campaign', - name='artist', - ), - migrations.RemoveField( - model_name='campaign', - name='reason', - ), + migrations.RemoveField(model_name="campaign", name="artist"), + migrations.RemoveField(model_name="campaign", name="reason"), migrations.AddField( - model_name='artistpercentagebreakdown', - name='project', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='campaign.Project'), + model_name="artistpercentagebreakdown", + name="project", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="campaign.Project", + ), preserve_default=False, ), migrations.AddField( - model_name='revenuereport', - name='project', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='campaign.Project'), + model_name="revenuereport", + name="project", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="campaign.Project", + ), preserve_default=False, ), ] diff --git a/campaign/migrations/0008_auto_20160618_2352.py b/campaign/migrations/0008_auto_20160618_2352.py index 066c4943..d7c28c9c 100644 --- a/campaign/migrations/0008_auto_20160618_2352.py +++ b/campaign/migrations/0008_auto_20160618_2352.py @@ -5,20 +5,26 @@ class Migration(migrations.Migration): - dependencies = [ - ('campaign', '0007_auto_20160618_2352'), - ] + dependencies = [("campaign", "0007_auto_20160618_2352")] def point_artistpercentagebreakdown_to_project(apps, schema_editor): - ArtistPercentageBreakdown = apps.get_model("campaign", "ArtistPercentageBreakdown") + ArtistPercentageBreakdown = apps.get_model( + "campaign", "ArtistPercentageBreakdown" + ) for artistpercentagebreakdown in ArtistPercentageBreakdown.objects.all(): - artistpercentagebreakdown.project = artistpercentagebreakdown.campaign.project + artistpercentagebreakdown.project = ( + artistpercentagebreakdown.campaign.project + ) artistpercentagebreakdown.save() def point_artistpercentagebreakdown_to_campaign(apps, schema_editor): - ArtistPercentageBreakdown = apps.get_model("campaign", "ArtistPercentageBreakdown") + ArtistPercentageBreakdown = apps.get_model( + "campaign", "ArtistPercentageBreakdown" + ) for artistpercentagebreakdown in ArtistPercentageBreakdown.objects.all(): - artistpercentagebreakdown.campaign = artistpercentagebreakdown.project.campaign_set.first() + artistpercentagebreakdown.campaign = ( + artistpercentagebreakdown.project.campaign_set.first() + ) artistpercentagebreakdown.save() def point_revenuereport_to_project(apps, schema_editor): @@ -34,6 +40,11 @@ def point_revenuereport_to_campaign(apps, schema_editor): revenuereport.save() operations = [ - migrations.RunPython(point_artistpercentagebreakdown_to_project, point_artistpercentagebreakdown_to_campaign), - migrations.RunPython(point_revenuereport_to_project, point_revenuereport_to_campaign), + migrations.RunPython( + point_artistpercentagebreakdown_to_project, + point_artistpercentagebreakdown_to_campaign, + ), + migrations.RunPython( + point_revenuereport_to_project, point_revenuereport_to_campaign + ), ] diff --git a/campaign/migrations/0009_auto_20160618_2353.py b/campaign/migrations/0009_auto_20160618_2353.py index 6c5cc2ab..be7ae335 100644 --- a/campaign/migrations/0009_auto_20160618_2353.py +++ b/campaign/migrations/0009_auto_20160618_2353.py @@ -6,29 +6,29 @@ class Migration(migrations.Migration): - dependencies = [ - ('campaign', '0008_auto_20160618_2352'), - ] + dependencies = [("campaign", "0008_auto_20160618_2352")] operations = [ migrations.AlterField( - model_name='artistpercentagebreakdown', - name='project', - field=models.ForeignKey(default=0, on_delete=django.db.models.deletion.CASCADE, to='campaign.Project'), + model_name="artistpercentagebreakdown", + name="project", + field=models.ForeignKey( + default=0, + on_delete=django.db.models.deletion.CASCADE, + to="campaign.Project", + ), preserve_default=False, ), migrations.AlterField( - model_name='revenuereport', - name='project', - field=models.ForeignKey(default=0, on_delete=django.db.models.deletion.CASCADE, to='campaign.Project'), + model_name="revenuereport", + name="project", + field=models.ForeignKey( + default=0, + on_delete=django.db.models.deletion.CASCADE, + to="campaign.Project", + ), preserve_default=False, ), - migrations.RemoveField( - model_name='artistpercentagebreakdown', - name='campaign', - ), - migrations.RemoveField( - model_name='revenuereport', - name='campaign', - ), + migrations.RemoveField(model_name="artistpercentagebreakdown", name="campaign"), + migrations.RemoveField(model_name="revenuereport", name="campaign"), ] diff --git a/campaign/migrations/0010_auto_20160625_0134.py b/campaign/migrations/0010_auto_20160625_0134.py index e05aa27c..356762ef 100644 --- a/campaign/migrations/0010_auto_20160625_0134.py +++ b/campaign/migrations/0010_auto_20160625_0134.py @@ -5,14 +5,16 @@ class Migration(migrations.Migration): - dependencies = [ - ('campaign', '0009_auto_20160618_2353'), - ] + dependencies = [("campaign", "0009_auto_20160618_2353")] operations = [ migrations.AlterField( - model_name='revenuereport', - name='amount', - field=models.DecimalField(decimal_places=2, help_text='The amount of revenue generated (in dollars) being reported (since last report)', max_digits=9), - ), + model_name="revenuereport", + name="amount", + field=models.DecimalField( + decimal_places=2, + help_text="The amount of revenue generated (in dollars) being reported (since last report)", + max_digits=9, + ), + ) ] diff --git a/campaign/models.py b/campaign/models.py index dac32520..32da6b05 100644 --- a/campaign/models.py +++ b/campaign/models.py @@ -15,19 +15,23 @@ class Project(models.Model): - artist = models.ForeignKey('artist.Artist', on_delete=models.CASCADE) - reason = models.CharField(max_length=40, help_text='The reason why the artist is raising money, in a few words') + artist = models.ForeignKey("artist.Artist", on_delete=models.CASCADE) + reason = models.CharField( + max_length=40, + help_text="The reason why the artist is raising money, in a few words", + ) def __str__(self): - return u'{artist} project {reason}'.format( - artist=str(self.artist), - reason=self.reason + return u"{artist} project {reason}".format( + artist=str(self.artist), reason=self.reason ) def active(self): - return self.campaign_set.filter( - start_datetime__lte=timezone.now() - ).exclude(end_datetime__lte=timezone.now()).exists() + return ( + self.campaign_set.filter(start_datetime__lte=timezone.now()) + .exclude(end_datetime__lte=timezone.now()) + .exists() + ) def total_num_shares(self): total_num_shares = 0 @@ -36,7 +40,9 @@ def total_num_shares(self): return total_num_shares def total_fans_percentage(self): - return self.campaign_set.all().aggregate(fans_percentage=models.Sum('fans_percentage'))['fans_percentage'] + return self.campaign_set.all().aggregate( + fans_percentage=models.Sum("fans_percentage") + )["fans_percentage"] def total_artist_percentage(self): fans_percentage = self.total_fans_percentage() @@ -44,20 +50,29 @@ def total_artist_percentage(self): return 100 - fans_percentage def artist_percentage(self): - percentage_breakdowns = self.artistpercentagebreakdown_set.annotate( - name=models.F('displays_publicly_as') - ).values('name').annotate( - percentage=models.Sum('percentage') - ).order_by('-percentage') + percentage_breakdowns = ( + self.artistpercentagebreakdown_set.annotate( + name=models.F("displays_publicly_as") + ) + .values("name") + .annotate(percentage=models.Sum("percentage")) + .order_by("-percentage") + ) if not percentage_breakdowns: - percentage_breakdowns = [{'name': self.artist.name, 'percentage': self.total_artist_percentage()}] + percentage_breakdowns = [ + {"name": self.artist.name, "percentage": self.total_artist_percentage()} + ] return percentage_breakdowns def generated_revenue(self): - return self.revenuereport_set.all().aggregate(gr=models.Sum('amount'))['gr'] or 0 + return ( + self.revenuereport_set.all().aggregate(gr=models.Sum("amount"))["gr"] or 0 + ) def generated_revenue_fans(self): - return float(self.generated_revenue()) * (float(self.total_fans_percentage()) / 100) + return float(self.generated_revenue()) * ( + float(self.total_fans_percentage()) / 100 + ) def project_investors(self, investors=None): investors = investors or {} @@ -67,8 +82,10 @@ def project_investors(self, investors=None): # Calculate percentage ownership for each investor (if project is active) if self.active(): for investor_id, investor in investors.items(): - percentage = (float(investor['num_shares']) / self.total_num_shares()) * self.total_fans_percentage() - investors[investor_id]['percentage'] = percentage + percentage = ( + float(investor["num_shares"]) / self.total_num_shares() + ) * self.total_fans_percentage() + investors[investor_id]["percentage"] = percentage return investors @@ -76,20 +93,29 @@ def project_investors(self, investors=None): class Campaign(models.Model): project = models.ForeignKey(Project, on_delete=models.CASCADE) - amount = models.PositiveIntegerField(help_text='The amount of money the artist wishes to raise') + amount = models.PositiveIntegerField( + help_text="The amount of money the artist wishes to raise" + ) value_per_share = models.PositiveIntegerField( - default=1, help_text='The value (in dollars) per share the artist wishes to sell' + default=1, + help_text="The value (in dollars) per share the artist wishes to sell", ) start_datetime = models.DateTimeField( - db_index=True, default=timezone.now, help_text='When the campaign will start accepting investors' + db_index=True, + default=timezone.now, + help_text="When the campaign will start accepting investors", ) end_datetime = models.DateTimeField( - db_index=True, null=True, blank=True, - help_text='When the campaign ends and will no longer accept investors (no end date if blank)' + db_index=True, + null=True, + blank=True, + help_text="When the campaign ends and will no longer accept investors (no end date if blank)", + ) + use_of_funds = models.TextField( + null=True, blank=True, help_text="A description of how the funds will be used" ) - use_of_funds = models.TextField(null=True, blank=True, help_text='A description of how the funds will be used') fans_percentage = models.PositiveSmallIntegerField( - help_text='The percentage of revenue that goes back to the fans (a value from 0-100)' + help_text="The percentage of revenue that goes back to the fans (a value from 0-100)" ) @staticmethod @@ -99,10 +125,10 @@ def funded_rounding(n): return int(math.floor(n)) def __str__(self): - return u'{artist}: ${amount} {reason}'.format( + return u"{artist}: ${amount} {reason}".format( artist=str(self.project.artist), amount=self.amount, - reason=self.project.reason + reason=self.project.reason, ) def value_per_share_cents(self): @@ -110,17 +136,21 @@ def value_per_share_cents(self): def total(self, num_shares): subtotal = num_shares * self.value_per_share - total = (settings.PERDIEM_FEE + subtotal) * (1 + settings.STRIPE_PERCENTAGE) + settings.STRIPE_FLAT_FEE + total = (settings.PERDIEM_FEE + subtotal) * ( + 1 + settings.STRIPE_PERCENTAGE + ) + settings.STRIPE_FLAT_FEE return math.ceil(total * 100.0) / 100.0 def num_shares(self): return self.amount / self.value_per_share def total_shares_purchased(self): - return self.investment_set.filter( - charge__paid=True, - charge__refunded=False - ).aggregate(total_shares=models.Sum('num_shares'))['total_shares'] or 0 + return ( + self.investment_set.filter( + charge__paid=True, charge__refunded=False + ).aggregate(total_shares=models.Sum("num_shares"))["total_shares"] + or 0 + ) def num_shares_remaining(self): return self.num_shares() - self.total_shares_purchased() @@ -156,22 +186,23 @@ def open(self): def campaign_investors(self, investors=None): investors = investors or {} investments = self.investment_set.filter( - charge__paid=True, - charge__refunded=False - ).select_related('charge', 'charge__customer', 'charge__customer__user') + charge__paid=True, charge__refunded=False + ).select_related("charge", "charge__customer", "charge__customer__user") for investment in investments: investor = investment.investor() if investor.id not in investors: investors[investor.id] = { - 'name': investor.userprofile.get_display_name(), - 'avatar_url': investor.userprofile.display_avatar_url(), - 'public_profile_url': investor.userprofile.public_profile_url(), - 'num_shares': 0, - 'total_investment': 0, + "name": investor.userprofile.get_display_name(), + "avatar_url": investor.userprofile.display_avatar_url(), + "public_profile_url": investor.userprofile.public_profile_url(), + "num_shares": 0, + "total_investment": 0, } - investors[investor.id]['num_shares'] += investment.num_shares - investors[investor.id]['total_investment'] += investment.num_shares * investment.campaign.value_per_share + investors[investor.id]["num_shares"] += investment.num_shares + investors[investor.id]["total_investment"] += ( + investment.num_shares * investment.campaign.value_per_share + ) return investors @@ -179,17 +210,21 @@ def campaign_investors(self, investors=None): class ArtistPercentageBreakdown(models.Model): project = models.ForeignKey(Project, on_delete=models.CASCADE) - artist_admin = models.ForeignKey('artist.ArtistAdmin', on_delete=models.SET_NULL, null=True, blank=True) - displays_publicly_as = models.CharField(max_length=30, help_text='The name shown on the artist\'s detail page') + artist_admin = models.ForeignKey( + "artist.ArtistAdmin", on_delete=models.SET_NULL, null=True, blank=True + ) + displays_publicly_as = models.CharField( + max_length=30, help_text="The name shown on the artist's detail page" + ) percentage = models.FloatField( - help_text='The percentage of revenue that goes back to this group/individual (a value from 0-100)' + help_text="The percentage of revenue that goes back to this group/individual (a value from 0-100)" ) def __str__(self): - return u'{project}: {displays_publicly_as} - {percentage}%'.format( + return u"{project}: {displays_publicly_as} - {percentage}%".format( project=str(self.project), displays_publicly_as=self.displays_publicly_as, - percentage=self.percentage + percentage=self.percentage, ) @@ -197,16 +232,16 @@ class Expense(models.Model): campaign = models.ForeignKey(Campaign, on_delete=models.CASCADE) expense = models.CharField( - max_length=30, help_text='A description of one of the expenses for the artist (eg. Studio cost)' + max_length=30, + help_text="A description of one of the expenses for the artist (eg. Studio cost)", ) class Meta: - unique_together = (('campaign', 'expense',)) + unique_together = ("campaign", "expense") def __str__(self): - return u'{campaign} ({expense})'.format( - campaign=str(self.campaign), - expense=self.expense + return u"{campaign} ({expense})".format( + campaign=str(self.campaign), expense=self.expense ) @@ -216,14 +251,14 @@ class Investment(models.Model): campaign = models.ForeignKey(Campaign, on_delete=models.CASCADE) transaction_datetime = models.DateTimeField(db_index=True, auto_now_add=True) num_shares = models.PositiveSmallIntegerField( - default=1, help_text='The number of shares an investor made in a transaction' + default=1, help_text="The number of shares an investor made in a transaction" ) def __str__(self): - return u'{num_shares} shares in {campaign} to {investor}'.format( + return u"{num_shares} shares in {campaign} to {investor}".format( num_shares=self.num_shares, campaign=str(self.campaign), - investor=str(self.investor()) + investor=str(self.investor()), ) def investor(self): @@ -232,14 +267,19 @@ def investor(self): def generated_revenue(self): relevant_revenue_reports = RevenueReport.objects.filter( project=self.campaign.project, - reported_datetime__gt=self.transaction_datetime + reported_datetime__gt=self.transaction_datetime, + ) + total_relevant_revenue = ( + relevant_revenue_reports.aggregate(total_revenue=models.Sum("amount"))[ + "total_revenue" + ] + or 0 ) - total_relevant_revenue = relevant_revenue_reports.aggregate( - total_revenue=models.Sum('amount') - )['total_revenue'] or 0 - percentage_ownership = (float(self.num_shares) / self.campaign.num_shares()) - investor_ownership = percentage_ownership * (float(self.campaign.fans_percentage) / 100) + percentage_ownership = float(self.num_shares) / self.campaign.num_shares() + investor_ownership = percentage_ownership * ( + float(self.campaign.fans_percentage) / 100 + ) return investor_ownership * float(total_relevant_revenue) @@ -247,13 +287,13 @@ class RevenueReport(models.Model): project = models.ForeignKey(Project, on_delete=models.CASCADE) amount = models.DecimalField( - max_digits=9, decimal_places=2, - help_text='The amount of revenue generated (in dollars) being reported (since last report)' + max_digits=9, + decimal_places=2, + help_text="The amount of revenue generated (in dollars) being reported (since last report)", ) reported_datetime = models.DateTimeField(auto_now_add=True) def __str__(self): - return u'${amount} for {project}'.format( - amount=self.amount, - project=str(self.project) + return u"${amount} for {project}".format( + amount=self.amount, project=str(self.project) ) diff --git a/campaign/signals.py b/campaign/signals.py index de7d117c..c15db26c 100644 --- a/campaign/signals.py +++ b/campaign/signals.py @@ -12,24 +12,40 @@ from campaign.models import Investment, RevenueReport -@receiver(models.signals.post_save, sender=Investment, dispatch_uid="clear_leaderboard_from_investment_handler") -@receiver(models.signals.post_save, sender=RevenueReport, dispatch_uid="clear_leaderboard_from_revenue_report_handler") +@receiver( + models.signals.post_save, + sender=Investment, + dispatch_uid="clear_leaderboard_from_investment_handler", +) +@receiver( + models.signals.post_save, + sender=RevenueReport, + dispatch_uid="clear_leaderboard_from_revenue_report_handler", +) def clear_leaderboard_cache_handler(sender, instance, **kwargs): - cache.delete('leaderboard') + cache.delete("leaderboard") -@receiver(models.signals.post_save, sender=Investment, dispatch_uid="clear_profile_contexts_from_investment_handler") +@receiver( + models.signals.post_save, + sender=Investment, + dispatch_uid="clear_profile_contexts_from_investment_handler", +) def clear_profile_context(sender, instance, **kwargs): pk = instance.investor().userprofile.pk - cache.delete('profile_context-{pk}'.format(pk=pk)) + cache.delete("profile_context-{pk}".format(pk=pk)) @receiver( - models.signals.post_save, sender=RevenueReport, dispatch_uid="clear_profile_contexts_from_revenue_report_handler" + models.signals.post_save, + sender=RevenueReport, + dispatch_uid="clear_profile_contexts_from_revenue_report_handler", ) def clear_all_profile_contexts(sender, instance, **kwargs): # TODO(lucas): Review to improve performance # Instead of clearing out all of the profile contexts, we could just clear out # the profile contexts associated with the investors related to this revenue report - cache_keys = ['profile_context-{pk}'.format(pk=up.pk) for up in UserProfile.objects.all()] + cache_keys = [ + "profile_context-{pk}".format(pk=up.pk) for up in UserProfile.objects.all() + ] cache.delete_many(cache_keys) diff --git a/campaign/tests.py b/campaign/tests.py index 4646fc64..a8f84f8a 100644 --- a/campaign/tests.py +++ b/campaign/tests.py @@ -13,23 +13,29 @@ from artist.factories import ArtistFactory from campaign.factories import ( - CampaignFactory, ProjectFactory, RevenueReportFactory, campaignfactory_factory, revenuereportfactory_factory + CampaignFactory, + ProjectFactory, + RevenueReportFactory, + campaignfactory_factory, + revenuereportfactory_factory, ) from perdiem.tests import MigrationTestCase, PerDiemTestCase class CreateInitialProjectsMigrationTestCase(MigrationTestCase): - migrate_from = '0005_auto_20160618_2310' - migrate_to = '0006_auto_20160618_2351' + migrate_from = "0005_auto_20160618_2310" + migrate_to = "0006_auto_20160618_2351" def setUpBeforeMigration(self, apps): # Create a campaign - CampaignFactoryForMigrationTestCase = campaignfactory_factory(apps=apps, point_to_project=False) + CampaignFactoryForMigrationTestCase = campaignfactory_factory( + apps=apps, point_to_project=False + ) self.campaign = CampaignFactoryForMigrationTestCase() def testProjectsCreatedFromCampaigns(self): - Project = self.apps.get_model('campaign', 'Project') + Project = self.apps.get_model("campaign", "Project") # Verify that a project was created from the campaign self.assertEqual(Project.objects.count(), 1) @@ -39,30 +45,36 @@ def testProjectsCreatedFromCampaigns(self): self.assertEqual(project.reason, self.campaign.reason) -class PointArtistPercentageBreakdownsAndRevenueReportsToProjectsMigrationTestCase(MigrationTestCase): +class PointArtistPercentageBreakdownsAndRevenueReportsToProjectsMigrationTestCase( + MigrationTestCase +): - migrate_from = '0007_auto_20160618_2352' - migrate_to = '0008_auto_20160618_2352' + migrate_from = "0007_auto_20160618_2352" + migrate_to = "0008_auto_20160618_2352" def setUpBeforeMigration(self, apps): CampaignFactoryForMigrationTestCase = campaignfactory_factory(apps=apps) - RevenueReportFactoryForMigrationTestCase = revenuereportfactory_factory(apps=apps, point_to_project=False) + RevenueReportFactoryForMigrationTestCase = revenuereportfactory_factory( + apps=apps, point_to_project=False + ) - class ArtistPercentageBreakdownFactoryForMigrationTestCase(factory.DjangoModelFactory): + class ArtistPercentageBreakdownFactoryForMigrationTestCase( + factory.DjangoModelFactory + ): class Meta: - model = apps.get_model('campaign', 'ArtistPercentageBreakdown') + model = apps.get_model("campaign", "ArtistPercentageBreakdown") + campaign = factory.SubFactory(CampaignFactoryForMigrationTestCase) # Create a RevenueReport and ArtistPercentageBreakdown self.revenue_report = RevenueReportFactoryForMigrationTestCase(amount=1000) campaign = self.revenue_report.campaign self.artistpercentagebreakdown = ArtistPercentageBreakdownFactoryForMigrationTestCase( - campaign=campaign, - percentage=50 + campaign=campaign, percentage=50 ) def testArtistPercentageBreakdownAndRevenueReportPointsToProject(self): - Campaign = self.apps.get_model('campaign', 'Campaign') + Campaign = self.apps.get_model("campaign", "Campaign") campaign = Campaign.objects.get() self.artistpercentagebreakdown.refresh_from_db() self.assertEqual(self.artistpercentagebreakdown.project.id, campaign.project.id) @@ -71,7 +83,6 @@ def testArtistPercentageBreakdownAndRevenueReportPointsToProject(self): class CampaignModelTestCase(TestCase): - def testProjectGeneratedRevenue(self): # Generate campaign and revenue report campaign = CampaignFactory() @@ -87,7 +98,6 @@ def testCampaignRaisingZeroIsAlreadyFunded(self): class CampaignAdminWebTestCase(PerDiemTestCase): - @classmethod def setUpTestData(cls): super(CampaignAdminWebTestCase, cls).setUpTestData() @@ -95,17 +105,17 @@ def setUpTestData(cls): start_datetime = datetime.datetime(year=2017, month=2, day=1) end_datetime = datetime.datetime(year=2017, month=3, day=1) cls.campaign_add_data = { - 'project': cls.project.id, - 'amount': 10000, - 'value_per_share': 1, - 'start_datetime_0': start_datetime.strftime('%Y-%m-%d'), - 'start_datetime_1': start_datetime.strftime('%H:%M:%S'), - 'end_datetime_0': end_datetime.strftime('%Y-%m-%d'), - 'end_datetime_1': end_datetime.strftime('%H:%M:%S'), - 'use_of_funds': '', - 'fans_percentage': 50, - 'expense_set-TOTAL_FORMS': 0, - 'expense_set-INITIAL_FORMS': 0, + "project": cls.project.id, + "amount": 10000, + "value_per_share": 1, + "start_datetime_0": start_datetime.strftime("%Y-%m-%d"), + "start_datetime_1": start_datetime.strftime("%H:%M:%S"), + "end_datetime_0": end_datetime.strftime("%Y-%m-%d"), + "end_datetime_1": end_datetime.strftime("%H:%M:%S"), + "use_of_funds": "", + "fans_percentage": 50, + "expense_set-TOTAL_FORMS": 0, + "expense_set-INITIAL_FORMS": 0, } def testProjectAdminRenders(self): @@ -113,52 +123,55 @@ def testProjectAdminRenders(self): CampaignFactory(project=self.project) # Verify that the change project page on admin renders - self.assertResponseRenders('/admin/campaign/project/{project_id}/change/'.format(project_id=self.project.id)) + self.assertResponseRenders( + "/admin/campaign/project/{project_id}/change/".format( + project_id=self.project.id + ) + ) def testAddCampaign(self): self.assertResponseRedirects( - '/admin/campaign/campaign/add/', - '/admin/campaign/campaign/', - method='POST', - data=self.campaign_add_data + "/admin/campaign/campaign/add/", + "/admin/campaign/campaign/", + method="POST", + data=self.campaign_add_data, ) def testCampaignEndCannotComeBeforeStart(self): # Set the end datetime to a value from the past data = self.campaign_add_data.copy() end_datetime = datetime.datetime(year=2017, month=1, day=1) - data.update({ - 'end_datetime_0': end_datetime.strftime('%Y-%m-%d'), - 'end_datetime_1': end_datetime.strftime('%H:%M:%S'), - }) + data.update( + { + "end_datetime_0": end_datetime.strftime("%Y-%m-%d"), + "end_datetime_1": end_datetime.strftime("%H:%M:%S"), + } + ) # Campaigns cannot be added that have an end datetime before the start response = self.assertResponseRenders( - '/admin/campaign/campaign/add/', - method='POST', + "/admin/campaign/campaign/add/", + method="POST", data=data, - has_form_error=True + has_form_error=True, ) self.assertIn(b"Campaign cannot end before it begins.", response.content) def testCannotAddCampaignWithoutTime(self): - for dt in ['start', 'end']: + for dt in ["start", "end"]: # Erase the time from campaign add POST data data = self.campaign_add_data.copy() - del data['{dt}_datetime_1'.format(dt=dt)] + del data["{dt}_datetime_1".format(dt=dt)] # Fail to create a campaign without the time component self.assertResponseRenders( - '/admin/campaign/campaign/add/', - method='POST', + "/admin/campaign/campaign/add/", + method="POST", data=data, - has_form_error=True + has_form_error=True, ) class CampaignWebTestCase(PerDiemTestCase): - def get200s(self): - return [ - '/stats/', - ] + return ["/stats/"] diff --git a/campaign/views.py b/campaign/views.py index aad006eb..570b3642 100644 --- a/campaign/views.py +++ b/campaign/views.py @@ -12,16 +12,18 @@ class LeaderboardView(TemplateView): - template_name = 'leaderboard/leaderboard.html' + template_name = "leaderboard/leaderboard.html" def investor_context(self, investor, key_to_copy): context = investor.profile_context() - context['amount'] = context[key_to_copy] - context.update({ - 'name': investor.get_display_name(), - 'url': investor.public_profile_url(), - 'avatar_url': investor.avatar_url(), - }) + context["amount"] = context[key_to_copy] + context.update( + { + "name": investor.get_display_name(), + "url": investor.public_profile_url(), + "avatar_url": investor.avatar_url(), + } + ) return context # TODO(lucas): Review to improve performance @@ -30,16 +32,21 @@ def investor_context(self, investor, key_to_copy): def calculate_leaderboard(self): # Top earned investors user_profiles = UserProfile.objects.filter(invest_anonymously=False) - top_earned_investors = [self.investor_context(user_profile, 'total_earned') for user_profile in user_profiles] - top_earned_investors = list(filter(lambda context: context['amount'] > 0, top_earned_investors)) - top_earned_investors = sorted(top_earned_investors, key=lambda context: context['amount'], reverse=True)[:20] - - return { - 'top_earned_investors': top_earned_investors, - } + top_earned_investors = [ + self.investor_context(user_profile, "total_earned") + for user_profile in user_profiles + ] + top_earned_investors = list( + filter(lambda context: context["amount"] > 0, top_earned_investors) + ) + top_earned_investors = sorted( + top_earned_investors, key=lambda context: context["amount"], reverse=True + )[:20] + + return {"top_earned_investors": top_earned_investors} def get_context_data(self, **kwargs): context = super(LeaderboardView, self).get_context_data(**kwargs) - leaderboard = cache.get_or_set('leaderboard', self.calculate_leaderboard) + leaderboard = cache.get_or_set("leaderboard", self.calculate_leaderboard) context.update(leaderboard) return context diff --git a/emails/apps.py b/emails/apps.py index a4026104..a04be9d6 100644 --- a/emails/apps.py +++ b/emails/apps.py @@ -3,7 +3,7 @@ class EmailsConfig(AppConfig): - name = 'emails' + name = "emails" def ready(self): import emails.signals diff --git a/emails/factories.py b/emails/factories.py index ece1c90d..405c364a 100644 --- a/emails/factories.py +++ b/emails/factories.py @@ -6,8 +6,7 @@ class EmailSubscriptionFactory(factory.DjangoModelFactory): - class Meta: - model = apps.get_model('emails', 'EmailSubscription') + model = apps.get_model("emails", "EmailSubscription") user = factory.SubFactory(UserFactory) diff --git a/emails/mailchimp.py b/emails/mailchimp.py index c3d88a4a..f4ddc664 100644 --- a/emails/mailchimp.py +++ b/emails/mailchimp.py @@ -12,33 +12,29 @@ class MailChimpException(Exception): - def __init__(self, status_code, title, detail, type): - message = '{status_code} {title}: {detail}\nMore information: {type}'.format( - status_code=status_code, - title=title, - detail=detail, - type=type + message = "{status_code} {title}: {detail}\nMore information: {type}".format( + status_code=status_code, title=title, detail=detail, type=type ) super(MailChimpException, self).__init__(message) def update_user_subscription(email, subscribed): mailchimp_api_key = settings.MAILCHIMP_API_KEY - mailchimp_data_center = mailchimp_api_key.split('-')[-1] - url = 'https://{dc}.api.mailchimp.com/3.0/lists/{list_id}/members/{subscriber_hash}'.format( + mailchimp_data_center = mailchimp_api_key.split("-")[-1] + url = "https://{dc}.api.mailchimp.com/3.0/lists/{list_id}/members/{subscriber_hash}".format( dc=mailchimp_data_center, list_id=settings.MAILCHIMP_LIST_ID, - subscriber_hash=hashlib.md5(email.lower().encode('utf-8')).hexdigest() + subscriber_hash=hashlib.md5(email.lower().encode("utf-8")).hexdigest(), ) - status = 'subscribed' if subscribed else 'unsubscribed' - data = {'email_address': email, 'status': status} - response = requests.put(url, json=data, auth=('', mailchimp_api_key,)) + status = "subscribed" if subscribed else "unsubscribed" + data = {"email_address": email, "status": status} + response = requests.put(url, json=data, auth=("", mailchimp_api_key)) if response.status_code >= 400: response_json = response.json() raise MailChimpException( status_code=response.status_code, - title=response_json['title'], - detail=response_json['detail'], - type=response_json['type'] + title=response_json["title"], + detail=response_json["detail"], + type=response_json["type"], ) diff --git a/emails/managers.py b/emails/managers.py index b06667e8..f1695c08 100644 --- a/emails/managers.py +++ b/emails/managers.py @@ -8,7 +8,6 @@ class VerifiedEmailManager(models.Manager): - def get_current_email(self, user): verified_email, _ = self.get_or_create(user=user, email=user.email) return verified_email @@ -19,7 +18,6 @@ def is_current_email_verified(self, user): class EmailSubscriptionManager(models.Manager): - def is_subscribed(self, user, subscription_type=None): if not subscription_type: subscription_type = self.model.SUBSCRIPTION_ALL @@ -34,4 +32,6 @@ def is_subscribed(self, user, subscription_type=None): def unsubscribe_user(self, user, subscription_type=None): if not subscription_type: subscription_type = self.model.SUBSCRIPTION_ALL - self.update_or_create(user=user, subscription=subscription_type, defaults={'subscribed': False}) + self.update_or_create( + user=user, subscription=subscription_type, defaults={"subscribed": False} + ) diff --git a/emails/messages.py b/emails/messages.py index f1496683..658ac020 100644 --- a/emails/messages.py +++ b/emails/messages.py @@ -23,9 +23,9 @@ class BaseEmail(object): @staticmethod def get_host(): - return '{proto}://{domain}'.format( - proto='http' if settings.DEBUG else 'https', - domain=Site.objects.get_current().domain + return "{proto}://{domain}".format( + proto="http" if settings.DEBUG else "https", + domain=Site.objects.get_current().domain, ) def unsubscribe_message(self, user): @@ -36,16 +36,16 @@ def unsubscribe_message(self, user): else: message = "To unsubscribe from these emails" return { - 'plain': "{message}, go to: {host}{url}.".format(message=message, host=host, url=unsubscribe_url), - 'html': "{message}, click here.".format( - message=message, - host=host, - url=unsubscribe_url + "plain": "{message}, go to: {host}{url}.".format( + message=message, host=host, url=unsubscribe_url + ), + "html": '{message}, click here.'.format( + message=message, host=host, url=unsubscribe_url ), } def get_template_name(self): - if not hasattr(self, 'template_name'): + if not hasattr(self, "template_name"): raise NoTemplateProvided("No template was provided for the email message.") return self.template_name @@ -53,12 +53,9 @@ def get_from_email_address(self, **kwargs): return self.from_email def get_context_data(self, user, **kwargs): - context = { - 'host': self.get_host(), - 'user': user, - } + context = {"host": self.get_host(), "user": user} if not self.ignore_unsubscribed: - context['unsubscribe_message'] = self.unsubscribe_message(user) + context["unsubscribe_message"] = self.unsubscribe_message(user) return context def send_to_email(self, email, context=None, **kwargs): @@ -72,59 +69,66 @@ def send_to_email(self, email, context=None, **kwargs): template_name=self.get_template_name(), from_email=self.get_from_email_address(**kwargs), recipient_list=[email], - context=context + context=context, ) def send(self, user, context=None, **kwargs): context = context or {} context.update(self.get_context_data(user, **kwargs)) - user_is_subscribed = EmailSubscription.objects.is_subscribed(user, subscription_type=self.subscription_type) + user_is_subscribed = EmailSubscription.objects.is_subscribed( + user, subscription_type=self.subscription_type + ) user_subscription_okay = self.ignore_unsubscribed or user_is_subscribed - email_is_verified = self.send_to_unverified_emails or VerifiedEmail.objects.is_current_email_verified(user) + email_is_verified = ( + self.send_to_unverified_emails + or VerifiedEmail.objects.is_current_email_verified(user) + ) if user_subscription_okay and email_is_verified: self.send_to_email(user.email, context, **kwargs) class EmailVerificationEmail(BaseEmail): - template_name = 'email_verification' + template_name = "email_verification" send_to_unverified_emails = True def get_context_data(self, user, **kwargs): context = super(EmailVerificationEmail, self).get_context_data(user, **kwargs) - context['verify_email_url'] = VerifiedEmail.objects.get_current_email(user).url() + context["verify_email_url"] = VerifiedEmail.objects.get_current_email( + user + ).url() return context class WelcomeEmail(EmailVerificationEmail): - template_name = 'welcome' + template_name = "welcome" def get_context_data(self, user, **kwargs): context = super(WelcomeEmail, self).get_context_data(user, **kwargs) verified_email = VerifiedEmail.objects.get_current_email(user) if verified_email.verified: - del context['verify_email_url'] + del context["verify_email_url"] return context class ContactEmail(BaseEmail): - template_name = 'contact' + template_name = "contact" class ArtistApplyEmail(BaseEmail): - template_name = 'artist_apply' + template_name = "artist_apply" class ArtistUpdateEmail(BaseEmail): - template_name = 'artist_update' + template_name = "artist_update" subscription_type = EmailSubscription.SUBSCRIPTION_ARTUP def get_from_email_address(self, **kwargs): - update = kwargs['update'] + update = kwargs["update"] return "{artist_name} ".format( artist_name=update.artist.name ) @@ -132,27 +136,26 @@ def get_from_email_address(self, **kwargs): def get_context_data(self, user, **kwargs): context = super(ArtistUpdateEmail, self).get_context_data(user, **kwargs) - update = kwargs['update'] - context.update({ - 'artist': update.artist, - 'update': update, - }) + update = kwargs["update"] + context.update({"artist": update.artist, "update": update}) return context class InvestSuccessEmail(BaseEmail): - template_name = 'invest_success' + template_name = "invest_success" def get_context_data(self, user, **kwargs): context = super(InvestSuccessEmail, self).get_context_data(user, **kwargs) - investment = kwargs['investment'] - context.update({ - 'artist': investment.campaign.project.artist, - 'campaign': investment.campaign, - 'num_shares': investment.num_shares, - }) + investment = kwargs["investment"] + context.update( + { + "artist": investment.campaign.project.artist, + "campaign": investment.campaign, + "num_shares": investment.num_shares, + } + ) return context diff --git a/emails/migrations/0001_initial.py b/emails/migrations/0001_initial.py index 0e765d93..9dd096d9 100644 --- a/emails/migrations/0001_initial.py +++ b/emails/migrations/0001_initial.py @@ -9,17 +9,29 @@ class Migration(migrations.Migration): initial = True - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] + dependencies = [migrations.swappable_dependency(settings.AUTH_USER_MODEL)] operations = [ migrations.CreateModel( - name='EmailSubscription', + name="EmailSubscription", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('subscribed', models.BooleanField(default=True)), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("subscribed", models.BooleanField(default=True)), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), ], - ), + ) ] diff --git a/emails/migrations/0002_auto_20160502_0538.py b/emails/migrations/0002_auto_20160502_0538.py index 9929e20c..0acc5547 100644 --- a/emails/migrations/0002_auto_20160502_0538.py +++ b/emails/migrations/0002_auto_20160502_0538.py @@ -5,18 +5,19 @@ class Migration(migrations.Migration): - dependencies = [ - ('emails', '0001_initial'), - ] + dependencies = [("emails", "0001_initial")] operations = [ migrations.AddField( - model_name='emailsubscription', - name='subscription', - field=models.CharField(choices=[('ALL', 'General'), ('NEWS', 'Newsletter')], default='ALL', max_length=6), + model_name="emailsubscription", + name="subscription", + field=models.CharField( + choices=[("ALL", "General"), ("NEWS", "Newsletter")], + default="ALL", + max_length=6, + ), ), migrations.AlterUniqueTogether( - name='emailsubscription', - unique_together=set([('user', 'subscription')]), + name="emailsubscription", unique_together=set([("user", "subscription")]) ), ] diff --git a/emails/migrations/0003_auto_20160531_0446.py b/emails/migrations/0003_auto_20160531_0446.py index 4ad0a7a3..ef335a80 100644 --- a/emails/migrations/0003_auto_20160531_0446.py +++ b/emails/migrations/0003_auto_20160531_0446.py @@ -10,23 +10,48 @@ class Migration(migrations.Migration): dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('emails', '0002_auto_20160502_0538'), + ("emails", "0002_auto_20160502_0538"), ] operations = [ migrations.CreateModel( - name='VerifiedEmail', + name="VerifiedEmail", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('email', models.EmailField(max_length=254)), - ('verified', models.BooleanField(default=False)), - ('code', models.UUIDField(db_index=True, default=uuid.uuid4, unique=True)), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("email", models.EmailField(max_length=254)), + ("verified", models.BooleanField(default=False)), + ( + "code", + models.UUIDField(db_index=True, default=uuid.uuid4, unique=True), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), ], ), migrations.AlterField( - model_name='emailsubscription', - name='subscription', - field=models.CharField(choices=[('ALL', 'General'), ('NEWS', 'Newsletter'), ('ARTUP', 'Artist Updates')], default='ALL', max_length=6), + model_name="emailsubscription", + name="subscription", + field=models.CharField( + choices=[ + ("ALL", "General"), + ("NEWS", "Newsletter"), + ("ARTUP", "Artist Updates"), + ], + default="ALL", + max_length=6, + ), ), ] diff --git a/emails/models.py b/emails/models.py index 1f24beae..90fe5f60 100644 --- a/emails/models.py +++ b/emails/models.py @@ -26,28 +26,32 @@ def __str__(self): return self.email def url(self): - return reverse('verify_email', kwargs={'user_id': self.user.id, 'code': self.code}) + return reverse( + "verify_email", kwargs={"user_id": self.user.id, "code": self.code} + ) class EmailSubscription(models.Model): - SUBSCRIPTION_ALL = 'ALL' - SUBSCRIPTION_NEWS = 'NEWS' - SUBSCRIPTION_ARTUP = 'ARTUP' + SUBSCRIPTION_ALL = "ALL" + SUBSCRIPTION_NEWS = "NEWS" + SUBSCRIPTION_ARTUP = "ARTUP" SUBSCRIPTION_CHOICES = ( - (SUBSCRIPTION_ALL, 'General',), - (SUBSCRIPTION_NEWS, 'Newsletter',), - (SUBSCRIPTION_ARTUP, 'Artist Updates',), + (SUBSCRIPTION_ALL, "General"), + (SUBSCRIPTION_NEWS, "Newsletter"), + (SUBSCRIPTION_ARTUP, "Artist Updates"), ) user = models.ForeignKey(User, on_delete=models.CASCADE) - subscription = models.CharField(choices=SUBSCRIPTION_CHOICES, max_length=6, default=SUBSCRIPTION_ALL) + subscription = models.CharField( + choices=SUBSCRIPTION_CHOICES, max_length=6, default=SUBSCRIPTION_ALL + ) subscribed = models.BooleanField(default=True) objects = EmailSubscriptionManager() class Meta: - unique_together = (('user', 'subscription',),) + unique_together = (("user", "subscription"),) def __str__(self): return str(self.user) diff --git a/emails/signals.py b/emails/signals.py index 8f4bbe9f..665736a5 100644 --- a/emails/signals.py +++ b/emails/signals.py @@ -15,28 +15,43 @@ from emails.models import EmailSubscription -@receiver(models.signals.pre_save, sender=EmailSubscription, dispatch_uid="unsubscribe_from_all_handler") +@receiver( + models.signals.pre_save, + sender=EmailSubscription, + dispatch_uid="unsubscribe_from_all_handler", +) def unsubscribe_from_all_handler(sender, instance, **kwargs): - if instance.subscription == EmailSubscription.SUBSCRIPTION_ALL and not instance.subscribed: - for email_subscription in EmailSubscription.objects.filter(user=instance.user).exclude(id=instance.id): + if ( + instance.subscription == EmailSubscription.SUBSCRIPTION_ALL + and not instance.subscribed + ): + for email_subscription in EmailSubscription.objects.filter( + user=instance.user + ).exclude(id=instance.id): email_subscription.subscribed = False email_subscription.save() -@receiver(models.signals.pre_save, sender=EmailSubscription, dispatch_uid="sync_to_mailchimp_handler") +@receiver( + models.signals.pre_save, + sender=EmailSubscription, + dispatch_uid="sync_to_mailchimp_handler", +) def sync_to_mailchimp_handler(sender, instance, **kwargs): user_is_subscribed_news = EmailSubscription.objects.is_subscribed( - user=instance.user, - subscription_type=EmailSubscription.SUBSCRIPTION_NEWS + user=instance.user, subscription_type=EmailSubscription.SUBSCRIPTION_NEWS ) - if instance.subscription == EmailSubscription.SUBSCRIPTION_NEWS and instance.subscribed != user_is_subscribed_news: + if ( + instance.subscription == EmailSubscription.SUBSCRIPTION_NEWS + and instance.subscribed != user_is_subscribed_news + ): update_user_subscription(instance.user.email, instance.subscribed) @receiver(registry.get_signal("charge.succeeded")) def charge_succeeded_handler(sender, **kwargs): # Get investment this successful charge is related to - charge_id = kwargs['event'].message['data']['object']['id'] + charge_id = kwargs["event"].message["data"]["object"]["id"] charge = Charge.objects.get(stripe_id=charge_id) investment = charge.investment diff --git a/emails/tests.py b/emails/tests.py index 7e9ad476..42e44625 100644 --- a/emails/tests.py +++ b/emails/tests.py @@ -15,12 +15,10 @@ class UnsubscribeTestCase(TestCase): - def testUnsubscribeFromAllRemovesAllSubscriptions(self): # Create an artist update subscription email_subscription = EmailSubscriptionFactory( - subscription=EmailSubscription.SUBSCRIPTION_ARTUP, - subscribed=True + subscription=EmailSubscription.SUBSCRIPTION_ARTUP, subscribed=True ) # Create an explicit unsubscribe from all emails @@ -29,7 +27,7 @@ def testUnsubscribeFromAllRemovesAllSubscriptions(self): EmailSubscription.objects.create( user=email_subscription.user, subscription=EmailSubscription.SUBSCRIPTION_ALL, - subscribed=False + subscribed=False, ) # Verify that when the user unsubscribes from everything, this artist update subscription is turned off @@ -38,23 +36,21 @@ def testUnsubscribeFromAllRemovesAllSubscriptions(self): class SubscribeTestCase(PerDiemTestCase): - def testSubscribeToNewsletterSuccess(self): self.assertResponseRenders( - '/accounts/settings/', - method='POST', + "/accounts/settings/", + method="POST", data={ - 'action': 'email_preferences', - 'email': self.user.email, - 'subscription_all': True, - 'subscription_news': True, - 'subscription_artist_update': False, - } + "action": "email_preferences", + "email": self.user.email, + "subscription_all": True, + "subscription_news": True, + "subscription_artist_update": False, + }, ) class UnsubscribeWebTestCase(PerDiemTestCase): - @classmethod def setUpTestData(cls): super(UnsubscribeWebTestCase, cls).setUpTestData() @@ -71,15 +67,16 @@ def testUnsubscribeUnauthenticated(self): def testUnsubscribeInvalidLink(self): self.client.logout() - unsubscribe_url = '/unsubscribe/{user_id}/ALL/{invalid_token}/'.format( - user_id=self.user.id, - invalid_token='abc123' + unsubscribe_url = "/unsubscribe/{user_id}/ALL/{invalid_token}/".format( + user_id=self.user.id, invalid_token="abc123" ) response = self.assertResponseRenders(unsubscribe_url) self.assertIn(b"This link is invalid", response.content) - @mock.patch('emails.mailchimp.requests.put') - @override_settings(MAILCHIMP_API_KEY='FAKE_API_KEY', MAILCHIMP_LIST_ID='FAKE_LIST_ID') + @mock.patch("emails.mailchimp.requests.put") + @override_settings( + MAILCHIMP_API_KEY="FAKE_API_KEY", MAILCHIMP_LIST_ID="FAKE_LIST_ID" + ) def testUnsubscribeFromMailChimp(self, mock_mailchimp_request): mock_mailchimp_request.return_value = mock.Mock(status_code=200) @@ -87,10 +84,7 @@ def testUnsubscribeFromMailChimp(self, mock_mailchimp_request): # Simulate POST request received from MailChimp self.assertResponseRenders( - '/unsubscribe/from-mailchimp/', - method='POST', - data={ - 'data[list_id]': 'FAKE_LIST_ID', - 'data[email]': self.user.email, - } + "/unsubscribe/from-mailchimp/", + method="POST", + data={"data[list_id]": "FAKE_LIST_ID", "data[email]": self.user.email}, ) diff --git a/emails/utils.py b/emails/utils.py index 167ea30a..3048519b 100644 --- a/emails/utils.py +++ b/emails/utils.py @@ -17,18 +17,18 @@ def make_token(user): def create_unsubscribe_link(user, subscription_type=EmailSubscription.SUBSCRIPTION_ALL): user_id, token = make_token(user).split(":", 1) return reverse( - 'unsubscribe', + "unsubscribe", kwargs={ - 'user_id': user_id, - 'subscription_type': subscription_type, - 'token': token, - } + "user_id": user_id, + "subscription_type": subscription_type, + "token": token, + }, ) def check_token(user_id, token): try: - key = '%s:%s' % (user_id, token) + key = "%s:%s" % (user_id, token) TimestampSigner().unsign(key, max_age=60 * 60 * 48) # Valid for 2 days except (BadSignature, SignatureExpired): return False diff --git a/emails/views.py b/emails/views.py index b301659e..43414da2 100644 --- a/emails/views.py +++ b/emails/views.py @@ -17,41 +17,56 @@ class UnsubscribeView(TemplateView): - template_name = 'registration/unsubscribe.html' + template_name = "registration/unsubscribe.html" def dispatch(self, request, *args, **kwargs): - self.user = get_object_or_404(User, id=kwargs['user_id']) + self.user = get_object_or_404(User, id=kwargs["user_id"]) return super(UnsubscribeView, self).dispatch(request, *args, **kwargs) def get_context_data(self, **kwargs): context = super(UnsubscribeView, self).get_context_data(**kwargs) - user_is_logged_in = self.request.user.is_authenticated and self.request.user == self.user - user_is_authenticated = (user_is_logged_in or check_token(self.user, kwargs['token'])) - subscription_type = kwargs['subscription_type'] + user_is_logged_in = ( + self.request.user.is_authenticated and self.request.user == self.user + ) + user_is_authenticated = user_is_logged_in or check_token( + self.user, kwargs["token"] + ) + subscription_type = kwargs["subscription_type"] subscription_choices = dict(EmailSubscription.SUBSCRIPTION_CHOICES) if user_is_authenticated and subscription_type in subscription_choices: - EmailSubscription.objects.unsubscribe_user(self.user, subscription_type=subscription_type) - context.update({ - 'success': True, - 'email': self.user.email, - 'subscription_type_display': subscription_choices[subscription_type], - }) + EmailSubscription.objects.unsubscribe_user( + self.user, subscription_type=subscription_type + ) + context.update( + { + "success": True, + "email": self.user.email, + "subscription_type_display": subscription_choices[ + subscription_type + ], + } + ) else: - context['success'] = False + context["success"] = False return context @csrf_exempt def unsubscribe_from_mailchimp(request): - if request.method == 'POST' and request.POST['data[list_id]'] == settings.MAILCHIMP_LIST_ID: - email = request.POST['data[email]'] + if ( + request.method == "POST" + and request.POST["data[list_id]"] == settings.MAILCHIMP_LIST_ID + ): + email = request.POST["data[email]"] try: user = User.objects.get(email=email) except User.DoesNotExist: pass else: - EmailSubscription.objects.unsubscribe_user(user, subscription_type=EmailSubscription.SUBSCRIPTION_NEWS) + EmailSubscription.objects.unsubscribe_user( + user, subscription_type=EmailSubscription.SUBSCRIPTION_NEWS + ) return HttpResponse("") diff --git a/fabfile.py b/fabfile.py index d8b4294e..be08e7c9 100644 --- a/fabfile.py +++ b/fabfile.py @@ -11,29 +11,28 @@ import requests -env.hosts = os.environ.get('PERDIEM_REMOTE_HOSTS', '').split(',') +env.hosts = os.environ.get("PERDIEM_REMOTE_HOSTS", "").split(",") def send_notification(commits): - bot_token = os.environ.get('PERDIEM_DEPLOYBOT_TOKEN') + bot_token = os.environ.get("PERDIEM_DEPLOYBOT_TOKEN") if not bot_token: return - text = '```\n{commits}\n```\nhas been deployed'.format(commits=commits) if commits else 'Services were restarted' - data = { - 'token': bot_token, - 'channel': '#general', - 'text': text, - 'as_user': True, - } - requests.post('https://slack.com/api/chat.postMessage', data=data) + text = ( + "```\n{commits}\n```\nhas been deployed".format(commits=commits) + if commits + else "Services were restarted" + ) + data = {"token": bot_token, "channel": "#general", "text": text, "as_user": True} + requests.post("https://slack.com/api/chat.postMessage", data=data) def deploy(): - with cd('~/perdiem-django'): - previous_commit_hash = run("git log -1 --format=\"%H\" --no-color", pty=False) + with cd("~/perdiem-django"): + previous_commit_hash = run('git log -1 --format="%H" --no-color', pty=False) run("git pull") - cmd_changes_deployed = "git log {previous_hash}.. --reverse --format=\"%h : %an : %s\" --no-color".format( + cmd_changes_deployed = 'git log {previous_hash}.. --reverse --format="%h : %an : %s" --no-color'.format( previous_hash=previous_commit_hash ) changes_deployed = run(cmd_changes_deployed, pty=False) diff --git a/manage.py b/manage.py index ab5083cd..9353b504 100755 --- a/manage.py +++ b/manage.py @@ -10,7 +10,7 @@ load_dotenv() os.environ.setdefault("DJANGO_SETTINGS_MODULE", "perdiem.settings") - cbsettings.configure('perdiem.settings.switcher') + cbsettings.configure("perdiem.settings.switcher") from django.core.management import execute_from_command_line diff --git a/music/admin/forms.py b/music/admin/forms.py index 4c361e5e..88e83ee5 100644 --- a/music/admin/forms.py +++ b/music/admin/forms.py @@ -14,38 +14,38 @@ class AlbumBioAdminForm(forms.ModelForm): - bio = forms.CharField(help_text=AlbumBio._meta.get_field('bio').help_text, widget=AdminPagedownWidget) + bio = forms.CharField( + help_text=AlbumBio._meta.get_field("bio").help_text, widget=AdminPagedownWidget + ) class Meta: model = AlbumBio - fields = ('bio',) + fields = ("bio",) class ActivityEstimateAdminForm(forms.ModelForm): - class Meta: model = ActivityEstimate - fields = ('date', 'activity_type', 'content_type', 'object_id', 'total',) + fields = ("date", "activity_type", "content_type", "object_id", "total") def clean(self): cleaned_data = super(ActivityEstimateAdminForm, self).clean() if not self.errors: # Get the object associated with this ActivityEstimate - content_type = cleaned_data['content_type'] - object_id = cleaned_data['object_id'] + content_type = cleaned_data["content_type"] + object_id = cleaned_data["object_id"] try: obj = content_type.get_object_for_this_type(id=object_id) except ObjectDoesNotExist: raise forms.ValidationError( "The {object_name} with ID {invalid_id} does not exist.".format( - object_name=content_type.model, - invalid_id=object_id + object_name=content_type.model, invalid_id=object_id ) ) # Get the album associated with this ActivityEstimate - if hasattr(obj, 'album'): + if hasattr(obj, "album"): album = obj.album else: album = obj @@ -62,6 +62,8 @@ def clean(self): class DailyReportForm(forms.Form): - track = forms.ModelChoiceField(queryset=Track.objects.all(), widget=forms.HiddenInput()) + track = forms.ModelChoiceField( + queryset=Track.objects.all(), widget=forms.HiddenInput() + ) streams = forms.IntegerField(min_value=0) downloads = forms.IntegerField(min_value=0) diff --git a/music/admin/model_admins.py b/music/admin/model_admins.py index 89ce9dca..759ed404 100644 --- a/music/admin/model_admins.py +++ b/music/admin/model_admins.py @@ -41,19 +41,29 @@ class AudioInline(admin.TabularInline): class AlbumAdmin(admin.ModelAdmin): - raw_id_fields = ('project',) - prepopulated_fields = {'slug': ('name',)} - inlines = (TrackInline, ArtworkInline, AlbumBioInline, MarketplaceURLInline, AudioInline,) + raw_id_fields = ("project",) + prepopulated_fields = {"slug": ("name",)} + inlines = ( + TrackInline, + ArtworkInline, + AlbumBioInline, + MarketplaceURLInline, + AudioInline, + ) class ActivityEstimateAdmin(admin.ModelAdmin): - list_display = ('content_object', 'date', 'activity_type',) + list_display = ("content_object", "date", "activity_type") form = ActivityEstimateAdminForm def get_urls(self): urls = super(ActivityEstimateAdmin, self).get_urls() custom_urls = [ - url(r'^daily-report/?$', admin.site.admin_view(DailyReportAdminView.as_view()), name='daily_report'), + url( + r"^daily-report/?$", + admin.site.admin_view(DailyReportAdminView.as_view()), + name="daily_report", + ) ] return custom_urls + urls diff --git a/music/admin/views.py b/music/admin/views.py index 2b823e8c..a6fbf6ea 100644 --- a/music/admin/views.py +++ b/music/admin/views.py @@ -14,51 +14,53 @@ class DailyReportAdminView(FormsetView): - template_name = 'admin/music/activityestimate/daily-report.html' + template_name = "admin/music/activityestimate/daily-report.html" form_class = DailyReportForm def get_success_url(self): - return reverse('admin:music_activityestimate_changelist') + return reverse("admin:music_activityestimate_changelist") def get_context_data(self, **kwargs): context = super(DailyReportAdminView, self).get_context_data(**kwargs) - context.update({ - 'title': 'Enter Daily Report', - 'has_permission': self.request.user.is_superuser, - }) + context.update( + { + "title": "Enter Daily Report", + "has_permission": self.request.user.is_superuser, + } + ) return context def get_formset_factory_kwargs(self): num_tracks = Track.objects.all().count() return { - 'min_num': num_tracks, - 'max_num': num_tracks, - 'validate_min': True, - 'validate_max': True, + "min_num": num_tracks, + "max_num": num_tracks, + "validate_min": True, + "validate_max": True, } def get_initial(self): - tracks = Track.objects.all().order_by('album__project__artist__name', 'name') - return [{'track': track} for track in tracks] + tracks = Track.objects.all().order_by("album__project__artist__name", "name") + return [{"track": track} for track in tracks] def formset_valid(self, formset): for form in formset: d = form.cleaned_data - track = d['track'] - num_streams = d['streams'] - num_downloads = d['downloads'] + track = d["track"] + num_streams = d["streams"] + num_downloads = d["downloads"] if num_streams: ActivityEstimate.objects.create( activity_type=ActivityEstimate.ACTIVITY_STREAM, content_object=track, - total=num_streams + total=num_streams, ) if num_downloads: ActivityEstimate.objects.create( activity_type=ActivityEstimate.ACTIVITY_DOWNLOAD, content_object=track, - total=num_downloads + total=num_downloads, ) messages.success(self.request, "Daily Report was submitted successfully") diff --git a/music/apps.py b/music/apps.py index d909c7fb..05882f62 100644 --- a/music/apps.py +++ b/music/apps.py @@ -2,4 +2,4 @@ class MusicConfig(AppConfig): - name = 'music' + name = "music" diff --git a/music/factories.py b/music/factories.py index 2f8c6c26..86782b4f 100644 --- a/music/factories.py +++ b/music/factories.py @@ -8,28 +8,27 @@ class AlbumFactory(factory.DjangoModelFactory): - class Meta: - model = apps.get_model('music', 'Album') + model = apps.get_model("music", "Album") project = factory.SubFactory(ProjectFactory) - name = factory.Sequence(lambda n: 'Album #{n}'.format(n=n)) + name = factory.Sequence(lambda n: "Album #{n}".format(n=n)) slug = factory.LazyAttribute(lambda album: slugify(album.name)) class TrackFactory(factory.DjangoModelFactory): - class Meta: - model = apps.get_model('music', 'Track') + model = apps.get_model("music", "Track") album = factory.SubFactory(AlbumFactory) - track_number = factory.LazyAttribute(lambda track: track.album.track_set.count() + 1) + track_number = factory.LazyAttribute( + lambda track: track.album.track_set.count() + 1 + ) class ActivityEstimateFactory(factory.DjangoModelFactory): - class Meta: - model = apps.get_model('music', 'ActivityEstimate') + model = apps.get_model("music", "ActivityEstimate") activity_type = ActivityEstimateConst.ACTIVITY_STREAM content_object = factory.SubFactory(TrackFactory) diff --git a/music/migrations/0001_initial.py b/music/migrations/0001_initial.py index e24b9365..887f39b0 100644 --- a/music/migrations/0001_initial.py +++ b/music/migrations/0001_initial.py @@ -8,42 +8,100 @@ class Migration(migrations.Migration): initial = True - dependencies = [ - ('campaign', '0010_auto_20160625_0134'), - ] + dependencies = [("campaign", "0010_auto_20160625_0134")] operations = [ migrations.CreateModel( - name='Album', + name="Album", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=60)), - ('slug', models.SlugField(help_text='A short label for an album (used in URLs)', max_length=40)), - ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='campaign.Project')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=60)), + ( + "slug", + models.SlugField( + help_text="A short label for an album (used in URLs)", + max_length=40, + ), + ), + ( + "project", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="campaign.Project", + ), + ), ], ), migrations.CreateModel( - name='Artwork', + name="Artwork", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('img', models.ImageField(upload_to='artist/album')), - ('album', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='music.Album')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("img", models.ImageField(upload_to="artist/album")), + ( + "album", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, to="music.Album" + ), + ), ], - options={ - 'verbose_name_plural': 'Artwork', - }, + options={"verbose_name_plural": "Artwork"}, ), migrations.CreateModel( - name='MarketplaceURL', + name="MarketplaceURL", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('medium', models.CharField(choices=[('itunes', 'iTunes'), ('spotify', 'Spotify'), ('google', 'Google Play Music'), ('apple', 'Apple Music')], help_text='The type of marketplace', max_length=10)), - ('url', models.URLField(help_text="The URL to the album's page", unique=True)), - ('album', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='music.Album')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "medium", + models.CharField( + choices=[ + ("itunes", "iTunes"), + ("spotify", "Spotify"), + ("google", "Google Play Music"), + ("apple", "Apple Music"), + ], + help_text="The type of marketplace", + max_length=10, + ), + ), + ( + "url", + models.URLField( + help_text="The URL to the album's page", unique=True + ), + ), + ( + "album", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="music.Album" + ), + ), ], ), migrations.AlterUniqueTogether( - name='marketplaceurl', - unique_together=set([('album', 'medium')]), + name="marketplaceurl", unique_together=set([("album", "medium")]) ), ] diff --git a/music/migrations/0002_auto_20160725_0226.py b/music/migrations/0002_auto_20160725_0226.py index 74f4369a..bd68fd37 100644 --- a/music/migrations/0002_auto_20160725_0226.py +++ b/music/migrations/0002_auto_20160725_0226.py @@ -7,25 +7,46 @@ class Migration(migrations.Migration): - dependencies = [ - ('music', '0001_initial'), - ] + dependencies = [("music", "0001_initial")] operations = [ migrations.CreateModel( - name='Audio', + name="Audio", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('file', music.models.S3PrivateFileField(upload_to='artist/audio')), - ('album', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='music.Album')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("file", music.models.S3PrivateFileField(upload_to="artist/audio")), + ( + "album", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, to="music.Album" + ), + ), ], - options={ - 'verbose_name_plural': 'Audio', - }, + options={"verbose_name_plural": "Audio"}, ), migrations.AlterField( - model_name='marketplaceurl', - name='medium', - field=models.CharField(choices=[('spotify', 'Spotify'), ('itunes', 'iTunes'), ('apple', 'Apple Music'), ('google', 'Google Play'), ('amazon', 'Amazon'), ('tidal', 'Tidal'), ('youtube', 'YouTube')], help_text='The type of marketplace', max_length=10), + model_name="marketplaceurl", + name="medium", + field=models.CharField( + choices=[ + ("spotify", "Spotify"), + ("itunes", "iTunes"), + ("apple", "Apple Music"), + ("google", "Google Play"), + ("amazon", "Amazon"), + ("tidal", "Tidal"), + ("youtube", "YouTube"), + ], + help_text="The type of marketplace", + max_length=10, + ), ), ] diff --git a/music/migrations/0003_auto_20160730_1802.py b/music/migrations/0003_auto_20160730_1802.py index d3125d25..ed997df6 100644 --- a/music/migrations/0003_auto_20160730_1802.py +++ b/music/migrations/0003_auto_20160730_1802.py @@ -6,26 +6,39 @@ class Migration(migrations.Migration): - dependencies = [ - ('music', '0002_auto_20160725_0226'), - ] + dependencies = [("music", "0002_auto_20160725_0226")] operations = [ migrations.CreateModel( - name='AlbumBio', + name="AlbumBio", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('bio', models.TextField(help_text='Tracklisting and other info about the album. Markdown syntax allowed, but no raw HTML. Examples: **bold**, *italic*, indent 4 spaces for a code block.')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "bio", + models.TextField( + help_text='Tracklisting and other info about the album. Markdown syntax allowed, but no raw HTML. Examples: **bold**, *italic*, indent 4 spaces for a code block.' + ), + ), ], ), migrations.AddField( - model_name='album', - name='release_date', + model_name="album", + name="release_date", field=models.DateField(blank=True, null=True), ), migrations.AddField( - model_name='albumbio', - name='album', - field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='music.Album'), + model_name="albumbio", + name="album", + field=models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, to="music.Album" + ), ), ] diff --git a/music/migrations/0004_track.py b/music/migrations/0004_track.py index a81acb97..5c6dd25c 100644 --- a/music/migrations/0004_track.py +++ b/music/migrations/0004_track.py @@ -6,20 +6,31 @@ class Migration(migrations.Migration): - dependencies = [ - ('music', '0003_auto_20160730_1802'), - ] + dependencies = [("music", "0003_auto_20160730_1802")] operations = [ migrations.CreateModel( - name='Track', + name="Track", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('disc_number', models.PositiveSmallIntegerField(default=1)), - ('track_number', models.PositiveSmallIntegerField()), - ('name', models.CharField(max_length=60)), - ('duration', models.DurationField(blank=True, null=True)), - ('album', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='music.Album')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("disc_number", models.PositiveSmallIntegerField(default=1)), + ("track_number", models.PositiveSmallIntegerField()), + ("name", models.CharField(max_length=60)), + ("duration", models.DurationField(blank=True, null=True)), + ( + "album", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="music.Album" + ), + ), ], - ), + ) ] diff --git a/music/migrations/0005_auto_20160911_0829.py b/music/migrations/0005_auto_20160911_0829.py index 86d8faab..a4561f2f 100644 --- a/music/migrations/0005_auto_20160911_0829.py +++ b/music/migrations/0005_auto_20160911_0829.py @@ -5,13 +5,11 @@ class Migration(migrations.Migration): - dependencies = [ - ('music', '0004_track'), - ] + dependencies = [("music", "0004_track")] operations = [ migrations.AlterUniqueTogether( - name='track', - unique_together=set([('album', 'disc_number', 'track_number')]), - ), + name="track", + unique_together=set([("album", "disc_number", "track_number")]), + ) ] diff --git a/music/migrations/0006_auto_20160920_0724.py b/music/migrations/0006_auto_20160920_0724.py index 04323014..6e63f646 100644 --- a/music/migrations/0006_auto_20160920_0724.py +++ b/music/migrations/0006_auto_20160920_0724.py @@ -9,24 +9,46 @@ class Migration(migrations.Migration): dependencies = [ - ('contenttypes', '0002_remove_content_type_name'), - ('music', '0005_auto_20160911_0829'), + ("contenttypes", "0002_remove_content_type_name"), + ("music", "0005_auto_20160911_0829"), ] operations = [ migrations.CreateModel( - name='ActivityEstimate', + name="ActivityEstimate", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('date', models.DateField(default=django.utils.timezone.now)), - ('activity_type', models.CharField(choices=[('stream', 'Stream'), ('download', 'Download')], max_length=8)), - ('object_id', gfklookupwidget.fields.GfkLookupField()), - ('total', models.PositiveIntegerField()), - ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='contenttypes.ContentType')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("date", models.DateField(default=django.utils.timezone.now)), + ( + "activity_type", + models.CharField( + choices=[("stream", "Stream"), ("download", "Download")], + max_length=8, + ), + ), + ("object_id", gfklookupwidget.fields.GfkLookupField()), + ("total", models.PositiveIntegerField()), + ( + "content_type", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + to="contenttypes.ContentType", + ), + ), ], ), migrations.AlterUniqueTogether( - name='activityestimate', - unique_together=set([('date', 'activity_type', 'content_type', 'object_id')]), + name="activityestimate", + unique_together=set( + [("date", "activity_type", "content_type", "object_id")] + ), ), ] diff --git a/music/models.py b/music/models.py index f600c0c5..0b735777 100644 --- a/music/models.py +++ b/music/models.py @@ -24,14 +24,22 @@ class Album(models.Model): project = models.ForeignKey(Project, on_delete=models.CASCADE) name = models.CharField(max_length=60) - slug = models.SlugField(max_length=40, db_index=True, help_text='A short label for an album (used in URLs)') + slug = models.SlugField( + max_length=40, + db_index=True, + help_text="A short label for an album (used in URLs)", + ) release_date = models.DateField(null=True, blank=True) def __str__(self): return self.name def validate_unique(self, exclude=None): - if Album.objects.exclude(id=self.id).filter(project__artist=self.project.artist, slug=self.slug).exists(): + if ( + Album.objects.exclude(id=self.id) + .filter(project__artist=self.project.artist, slug=self.slug) + .exists() + ): raise ValidationError("Slug must be unique per artist") def save(self, *args, **kwargs): @@ -39,11 +47,22 @@ def save(self, *args, **kwargs): super(Album, self).save(*args, **kwargs) def url(self): - return reverse('album', kwargs={'artist_slug': self.project.artist.slug, 'album_slug': self.slug}) + return reverse( + "album", + kwargs={"artist_slug": self.project.artist.slug, "album_slug": self.slug}, + ) def discs(self): - disc_numbers = self.track_set.all().values_list('disc_number', flat=True).distinct().order_by('disc_number') - return (self.track_set.filter(disc_number=disc).order_by('track_number') for disc in disc_numbers) + disc_numbers = ( + self.track_set.all() + .values_list("disc_number", flat=True) + .distinct() + .order_by("disc_number") + ) + return ( + self.track_set.filter(disc_number=disc).order_by("track_number") + for disc in disc_numbers + ) def total_activity(self, activity_type): tracks = self.track_set.all() @@ -51,14 +70,19 @@ def total_activity(self, activity_type): return 0 all_activities = ActivityEstimate.objects.filter(activity_type=activity_type) - album_events = all_activities.filter( - content_type=ContentType.objects.get_for_model(self), - object_id=self.id - ).aggregate(total=models.Sum('total'))['total'] or 0 - track_events = all_activities.filter( - content_type=ContentType.objects.get_for_model(tracks[0]), - object_id__in=tracks.values_list('id', flat=True) - ).aggregate(total=models.Sum('total'))['total'] or 0 + album_events = ( + all_activities.filter( + content_type=ContentType.objects.get_for_model(self), object_id=self.id + ).aggregate(total=models.Sum("total"))["total"] + or 0 + ) + track_events = ( + all_activities.filter( + content_type=ContentType.objects.get_for_model(tracks[0]), + object_id__in=tracks.values_list("id", flat=True), + ).aggregate(total=models.Sum("total"))["total"] + or 0 + ) return album_events * tracks.count() + track_events @@ -78,21 +102,28 @@ class Track(models.Model): duration = models.DurationField(null=True, blank=True) class Meta: - unique_together = (('album', 'disc_number', 'track_number',),) + unique_together = (("album", "disc_number", "track_number"),) def __str__(self): return "{album} #{track_number}: {name}".format( - album=str(self.album), - track_number=self.track_number, - name=self.name + album=str(self.album), track_number=self.track_number, name=self.name ) def total_activity(self, activity_type): - return ActivityEstimate.objects.filter( - models.Q(content_type=ContentType.objects.get_for_model(self.album), object_id=self.album.id) | - models.Q(content_type=ContentType.objects.get_for_model(self), object_id=self.id), - activity_type=activity_type - ).aggregate(total=models.Sum('total'))['total'] or 0 + return ( + ActivityEstimate.objects.filter( + models.Q( + content_type=ContentType.objects.get_for_model(self.album), + object_id=self.album.id, + ) + | models.Q( + content_type=ContentType.objects.get_for_model(self), + object_id=self.id, + ), + activity_type=activity_type, + ).aggregate(total=models.Sum("total"))["total"] + or 0 + ) def total_downloads(self): return self.total_activity(ActivityEstimate.ACTIVITY_DOWNLOAD) @@ -104,10 +135,10 @@ def total_streams(self): class Artwork(models.Model): album = models.OneToOneField(Album, on_delete=models.CASCADE) - img = models.ImageField(upload_to='artist/album') + img = models.ImageField(upload_to="artist/album") class Meta: - verbose_name_plural = 'Artwork' + verbose_name_plural = "Artwork" def __str__(self): return str(self.album) @@ -116,7 +147,9 @@ def __str__(self): class AlbumBio(models.Model): album = models.OneToOneField(Album, on_delete=models.CASCADE) - bio = models.TextField(help_text='Tracklisting and other info about the album. ' + markdown_allowed()) + bio = models.TextField( + help_text="Tracklisting and other info about the album. " + markdown_allowed() + ) def __str__(self): return str(self.album) @@ -124,43 +157,47 @@ def __str__(self): class MarketplaceURL(models.Model): - MARKETPLACE_ITUNES = 'itunes' - MARKETPLACE_APPLE_MUSIC = 'apple' + MARKETPLACE_ITUNES = "itunes" + MARKETPLACE_APPLE_MUSIC = "apple" MARKETPLACE_CHOICES = ( - ('spotify', 'Spotify',), - (MARKETPLACE_ITUNES, 'iTunes',), - (MARKETPLACE_APPLE_MUSIC, 'Apple Music',), - ('google', 'Google Play',), - ('amazon', 'Amazon',), - ('tidal', 'Tidal',), - ('youtube', 'YouTube',), + ("spotify", "Spotify"), + (MARKETPLACE_ITUNES, "iTunes"), + (MARKETPLACE_APPLE_MUSIC, "Apple Music"), + ("google", "Google Play"), + ("amazon", "Amazon"), + ("tidal", "Tidal"), + ("youtube", "YouTube"), ) album = models.ForeignKey(Album, on_delete=models.CASCADE) - medium = models.CharField(choices=MARKETPLACE_CHOICES, max_length=10, help_text='The type of marketplace') - url = models.URLField(unique=True, help_text='The URL to the album\'s page') + medium = models.CharField( + choices=MARKETPLACE_CHOICES, max_length=10, help_text="The type of marketplace" + ) + url = models.URLField(unique=True, help_text="The URL to the album's page") class Meta: - unique_together = (('album', 'medium',),) + unique_together = (("album", "medium"),) def __str__(self): - return u'{album}: {medium}'.format( - album=str(self.album), - medium=self.get_medium_display() + return u"{album}: {medium}".format( + album=str(self.album), medium=self.get_medium_display() ) def marketplace_has_affiliate_token(self): - return self.medium in (self.MARKETPLACE_ITUNES, self.MARKETPLACE_APPLE_MUSIC,) + return self.medium in (self.MARKETPLACE_ITUNES, self.MARKETPLACE_APPLE_MUSIC) def affiliate_url(self): - if self.marketplace_has_affiliate_token() and hasattr(settings, 'ITUNES_AFFILIATE_TOKEN'): - return add_params_to_url(self.url, {'at': settings.ITUNES_AFFILIATE_TOKEN}) + if self.marketplace_has_affiliate_token() and hasattr( + settings, "ITUNES_AFFILIATE_TOKEN" + ): + return add_params_to_url(self.url, {"at": settings.ITUNES_AFFILIATE_TOKEN}) return self.url class S3PrivateFileField(models.FileField): - - def __init__(self, verbose_name=None, name=None, upload_to='', storage=None, **kwargs): + def __init__( + self, verbose_name=None, name=None, upload_to="", storage=None, **kwargs + ): super(S3PrivateFileField, self).__init__( verbose_name=verbose_name, name=name, @@ -174,33 +211,35 @@ def __init__(self, verbose_name=None, name=None, upload_to='', storage=None, **k class Audio(models.Model): album = models.OneToOneField(Album, on_delete=models.CASCADE) - file = S3PrivateFileField(upload_to='artist/audio') + file = S3PrivateFileField(upload_to="artist/audio") class Meta: - verbose_name_plural = 'Audio' + verbose_name_plural = "Audio" def __str__(self): return str(self.album) def get_temporary_url(self, ttl=60): - if hasattr(settings, 'AWS_S3_BUCKET_NAME'): + if hasattr(settings, "AWS_S3_BUCKET_NAME"): s3 = boto3.client( - 's3', + "s3", aws_access_key_id=settings.AWS_ACCESS_KEY_ID, - aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY + aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY, + ) + key = "{media}/{filename}".format( + media=settings.AWS_S3_KEY_PREFIX, filename=self.file.name ) - key = "{media}/{filename}".format(media=settings.AWS_S3_KEY_PREFIX, filename=self.file.name) return s3.generate_presigned_url( - 'get_object', - Params={'Bucket': settings.AWS_S3_BUCKET_NAME, 'Key': key}, - ExpiresIn=ttl + "get_object", + Params={"Bucket": settings.AWS_S3_BUCKET_NAME, "Key": key}, + ExpiresIn=ttl, ) return self.file.url def activity_content_type_choices(): return { - 'id__in': ( + "id__in": ( ContentType.objects.get_for_model(Album).id, ContentType.objects.get_for_model(Track).id, ) @@ -209,24 +248,23 @@ def activity_content_type_choices(): class ActivityEstimate(models.Model): - ACTIVITY_STREAM = 'stream' - ACTIVITY_DOWNLOAD = 'download' - ACTIVITY_CHOICES = ( - (ACTIVITY_STREAM, 'Stream',), - (ACTIVITY_DOWNLOAD, 'Download',), - ) + ACTIVITY_STREAM = "stream" + ACTIVITY_DOWNLOAD = "download" + ACTIVITY_CHOICES = ((ACTIVITY_STREAM, "Stream"), (ACTIVITY_DOWNLOAD, "Download")) date = models.DateField(default=timezone.now) activity_type = models.CharField(choices=ACTIVITY_CHOICES, max_length=8) content_type = models.ForeignKey( - ContentType, on_delete=models.PROTECT, limit_choices_to=activity_content_type_choices + ContentType, + on_delete=models.PROTECT, + limit_choices_to=activity_content_type_choices, ) - object_id = GfkLookupField('content_type') + object_id = GfkLookupField("content_type") content_object = GenericForeignKey() total = models.PositiveIntegerField() class Meta: - unique_together = (('date', 'activity_type', 'content_type', 'object_id',),) + unique_together = (("date", "activity_type", "content_type", "object_id"),) def __str__(self): return str(self.content_object) diff --git a/music/tests.py b/music/tests.py index 9899325b..8f3ea3ce 100644 --- a/music/tests.py +++ b/music/tests.py @@ -15,7 +15,6 @@ class MusicModelsTestCase(TestCase): - def testUnicodeOfAlbumIsAlbumName(self): album = AlbumFactory() self.assertEqual(str(album), album.name) @@ -40,14 +39,22 @@ def testUnicodeOfTrack(self): track = TrackFactory() self.assertEqual( str(track), - "{album_name} #1: {track_name}".format(album_name=track.album.name, track_name=track.name) + "{album_name} #1: {track_name}".format( + album_name=track.album.name, track_name=track.name + ), ) def testTrackTotalActivity(self): # Create ActivityEstimates - download_activity_estimate = ActivityEstimateFactory(activity_type=ActivityEstimate.ACTIVITY_DOWNLOAD, total=1) + download_activity_estimate = ActivityEstimateFactory( + activity_type=ActivityEstimate.ACTIVITY_DOWNLOAD, total=1 + ) track = download_activity_estimate.content_object - ActivityEstimateFactory(activity_type=ActivityEstimate.ACTIVITY_STREAM, content_object=track, total=1) + ActivityEstimateFactory( + activity_type=ActivityEstimate.ACTIVITY_STREAM, + content_object=track, + total=1, + ) # Verify that the track has one download and one stream self.assertEqual(track.total_downloads(), 1) @@ -59,55 +66,51 @@ def testUnicodeOfActivityEstimateIsContentObject(self): class MusicAdminWebTestCase(PerDiemTestCase): - def get200s(self): - return [ - '/admin/music/activityestimate/daily-report/', - ] + return ["/admin/music/activityestimate/daily-report/"] def testActivityEstimatesRequireCampaigns(self): album = AlbumFactory() response = self.assertResponseRenders( - '/admin/music/activityestimate/add/', - method='POST', + "/admin/music/activityestimate/add/", + method="POST", data={ - 'date': timezone.now().date(), - 'activity_type': ActivityEstimate.ACTIVITY_STREAM, - 'content_type': ContentType.objects.get_for_model(album).id, - 'object_id': album.id, - 'total': 10, + "date": timezone.now().date(), + "activity_type": ActivityEstimate.ACTIVITY_STREAM, + "content_type": ContentType.objects.get_for_model(album).id, + "object_id": album.id, + "total": 10, }, - has_form_error=True + has_form_error=True, ) self.assertIn( b"You cannot create activity estimates without defining the revenue percentages", - response.content + response.content, ) def testActivityEstimatesWhereAlbumDoesNotExist(self): invalid_album_id = Album.objects.count() + 1 response = self.assertResponseRenders( - '/admin/music/activityestimate/add/', - method='POST', + "/admin/music/activityestimate/add/", + method="POST", data={ - 'date': timezone.now().date(), - 'activity_type': ActivityEstimate.ACTIVITY_STREAM, - 'content_type': ContentType.objects.get_for_model(Album).id, - 'object_id': invalid_album_id, - 'total': 10, + "date": timezone.now().date(), + "activity_type": ActivityEstimate.ACTIVITY_STREAM, + "content_type": ContentType.objects.get_for_model(Album).id, + "object_id": invalid_album_id, + "total": 10, }, - has_form_error=True + has_form_error=True, ) self.assertIn( "The album with ID {invalid_album_id} does not exist.".format( invalid_album_id=invalid_album_id - ).encode('utf-8'), - response.content + ).encode("utf-8"), + response.content, ) class MusicWebTestCase(PerDiemTestCase): - @classmethod def setUpTestData(cls): cls.album = AlbumFactory() @@ -115,9 +118,8 @@ def setUpTestData(cls): def get200s(self): return [ - '/music/', - '/artist/{artist_slug}/{album_slug}/'.format( - artist_slug=self.artist.slug, - album_slug=self.album.slug + "/music/", + "/artist/{artist_slug}/{album_slug}/".format( + artist_slug=self.artist.slug, album_slug=self.album.slug ), ] diff --git a/music/views.py b/music/views.py index 491822d4..11703c29 100644 --- a/music/views.py +++ b/music/views.py @@ -14,34 +14,34 @@ class MusicListView(ListView): - template_name = 'music/music.html' - context_object_name = 'albums' + template_name = "music/music.html" + context_object_name = "albums" model = Album class AlbumDetailView(TemplateView): - template_name = 'music/album_detail.html' + template_name = "music/album_detail.html" def get_context_data(self, **kwargs): context = super(AlbumDetailView, self).get_context_data(**kwargs) album = get_object_or_404( Album, - slug=kwargs['album_slug'], - project__artist__slug=kwargs['artist_slug'] + slug=kwargs["album_slug"], + project__artist__slug=kwargs["artist_slug"], ) user = self.request.user - user_is_investor = user.is_authenticated and Investment.objects.filter( - campaign__project__album=album, - charge__customer__user=user, - charge__paid=True, - charge__refunded=False - ).exists() - - context.update({ - 'album': album, - 'user_is_investor': user_is_investor, - }) + user_is_investor = ( + user.is_authenticated + and Investment.objects.filter( + campaign__project__album=album, + charge__customer__user=user, + charge__paid=True, + charge__refunded=False, + ).exists() + ) + + context.update({"album": album, "user_is_investor": user_is_investor}) return context diff --git a/perdiem/context_processors.py b/perdiem/context_processors.py index c596a0db..263603ba 100644 --- a/perdiem/context_processors.py +++ b/perdiem/context_processors.py @@ -8,6 +8,4 @@ def request(request): - return { - 'host': Site.objects.get_current().domain, - } + return {"host": Site.objects.get_current().domain} diff --git a/perdiem/gunicorn.py b/perdiem/gunicorn.py index 508a0606..10074622 100644 --- a/perdiem/gunicorn.py +++ b/perdiem/gunicorn.py @@ -1,4 +1,4 @@ import multiprocessing -bind = '0.0.0.0:8000' +bind = "0.0.0.0:8000" workers = multiprocessing.cpu_count() * 2 + 1 diff --git a/perdiem/settings/base.py b/perdiem/settings/base.py index d4225c07..50915cc5 100644 --- a/perdiem/settings/base.py +++ b/perdiem/settings/base.py @@ -11,24 +11,26 @@ def aws_s3_bucket_url(settings_class, bucket_name_settings): - bucket_name = getattr(settings_class, bucket_name_settings, '') + bucket_name = getattr(settings_class, bucket_name_settings, "") if bucket_name: - return 'https://{bucket}.s3.amazonaws.com'.format(bucket=bucket_name) - return '' + return "https://{bucket}.s3.amazonaws.com".format(bucket=bucket_name) + return "" class BaseSettings(DjangoDefaults): - BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + BASE_DIR = os.path.dirname( + os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + ) SECRET_KEY = os.environ["PERDIEM_SECRET_KEY"] DEBUG = True - ACCEPTABLE_HOSTS = ['localhost', '127.0.0.1', '.investperdiem.com'] + ACCEPTABLE_HOSTS = ["localhost", "127.0.0.1", ".investperdiem.com"] @property def ALLOWED_HOSTS(self): # Get cached ALLOWED_HOSTS setting, if available - if hasattr(self, '_ALLOWED_HOSTS'): + if hasattr(self, "_ALLOWED_HOSTS"): return self._ALLOWED_HOSTS # When DEBUG == True, ALLOWED_HOSTS is just ACCEPTABLE_HOSTS @@ -39,7 +41,9 @@ def ALLOWED_HOSTS(self): # Otherwise, add EC2 IP to ACCEPTABLE_HOSTS hosts = self.ACCEPTABLE_HOSTS try: - ec2_ip = requests.get('http://169.254.169.254/latest/meta-data/local-ipv4', timeout=0.01).text + ec2_ip = requests.get( + "http://169.254.169.254/latest/meta-data/local-ipv4", timeout=0.01 + ).text except requests.exceptions.RequestException: pass else: @@ -49,176 +53,167 @@ def ALLOWED_HOSTS(self): # Application definition INSTALLED_APPS = ( - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'whitenoise.runserver_nostatic', - 'django.contrib.staticfiles', - 'django.contrib.sites', - 'django.contrib.humanize', - 'raven.contrib.django.raven_compat', - 'sorl.thumbnail', - 'django_s3_storage', - 'rest_framework', - 'social_django', - 'pinax.stripe', - 'markdown_deux', - 'pagedown', - 'accounts.apps.AccountsConfig', - 'api.apps.ApiConfig', - 'artist.apps.ArtistConfig', - 'campaign.apps.CampaignConfig', - 'emails.apps.EmailsConfig', - 'music.apps.MusicConfig', + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "whitenoise.runserver_nostatic", + "django.contrib.staticfiles", + "django.contrib.sites", + "django.contrib.humanize", + "raven.contrib.django.raven_compat", + "sorl.thumbnail", + "django_s3_storage", + "rest_framework", + "social_django", + "pinax.stripe", + "markdown_deux", + "pagedown", + "accounts.apps.AccountsConfig", + "api.apps.ApiConfig", + "artist.apps.ArtistConfig", + "campaign.apps.CampaignConfig", + "emails.apps.EmailsConfig", + "music.apps.MusicConfig", ) MIDDLEWARE = [ - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', - 'django.middleware.security.SecurityMiddleware', - 'whitenoise.middleware.WhiteNoiseMiddleware', - 'social_django.middleware.SocialAuthExceptionMiddleware', - 'accounts.middleware.LoginFormMiddleware', + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", + "django.middleware.security.SecurityMiddleware", + "whitenoise.middleware.WhiteNoiseMiddleware", + "social_django.middleware.SocialAuthExceptionMiddleware", + "accounts.middleware.LoginFormMiddleware", ] - ROOT_URLCONF = 'perdiem.urls' + ROOT_URLCONF = "perdiem.urls" SITE_ID = 1 TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [os.path.join(BASE_DIR, 'templates')], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', - 'social_django.context_processors.backends', - 'social_django.context_processors.login_redirect', - 'perdiem.context_processors.request', - 'accounts.context_processors.keys', - 'accounts.context_processors.profile', - 'artist.context_processors.artist_settings', - ], + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [os.path.join(BASE_DIR, "templates")], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + "social_django.context_processors.backends", + "social_django.context_processors.login_redirect", + "perdiem.context_processors.request", + "accounts.context_processors.keys", + "accounts.context_processors.profile", + "artist.context_processors.artist_settings", + ] }, - }, + } ] - WSGI_APPLICATION = 'perdiem.wsgi.application' + WSGI_APPLICATION = "perdiem.wsgi.application" # Cache and Database CACHES = { - 'default': { - 'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache', - 'LOCATION': '127.0.0.1:11211', + "default": { + "BACKEND": "django.core.cache.backends.memcached.MemcachedCache", + "LOCATION": "127.0.0.1:11211", } } DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.postgresql_psycopg2', - 'NAME': os.environ["PERDIEM_DB_NAME"], - 'USER': os.environ["PERDIEM_DB_USER"], - 'PASSWORD': os.environ["PERDIEM_DB_PASSWORD"], - 'HOST': os.environ["PERDIEM_DB_HOST"], - 'PORT': '5432', + "default": { + "ENGINE": "django.db.backends.postgresql_psycopg2", + "NAME": os.environ["PERDIEM_DB_NAME"], + "USER": os.environ["PERDIEM_DB_USER"], + "PASSWORD": os.environ["PERDIEM_DB_PASSWORD"], + "HOST": os.environ["PERDIEM_DB_HOST"], + "PORT": "5432", } } # Internationalization - TIME_ZONE = 'UTC' + TIME_ZONE = "UTC" USE_L10N = True USE_TZ = True # Static files (CSS, JavaScript, Images) - MEDIA_ROOT = os.path.join(BASE_DIR, 'media') - STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage' - STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles') - STATICFILES_DIRS = ( - os.path.join(BASE_DIR, 'static'), - ) - AWS_S3_KEY_PREFIX = 'media' - AWS_S3_KEY_PREFIX_STATIC = 'static' + MEDIA_ROOT = os.path.join(BASE_DIR, "media") + STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage" + STATIC_ROOT = os.path.join(BASE_DIR, "staticfiles") + STATICFILES_DIRS = (os.path.join(BASE_DIR, "static"),) + AWS_S3_KEY_PREFIX = "media" + AWS_S3_KEY_PREFIX_STATIC = "static" AWS_S3_BUCKET_AUTH = False AWS_S3_MAX_AGE_SECONDS = 60 * 60 * 24 * 365 # 1 year MAXIMUM_AVATAR_SIZE = 2 * 1024 * 1024 # 2MB @property def MEDIA_URL(self): - return '{aws_s3}/{media}/'.format( - aws_s3=aws_s3_bucket_url(self, 'AWS_S3_BUCKET_NAME'), - media=self.AWS_S3_KEY_PREFIX + return "{aws_s3}/{media}/".format( + aws_s3=aws_s3_bucket_url(self, "AWS_S3_BUCKET_NAME"), + media=self.AWS_S3_KEY_PREFIX, ) @property def STATIC_URL(self): - return '{aws_s3}/{static}/'.format( - aws_s3=aws_s3_bucket_url(self, 'AWS_S3_BUCKET_NAME_STATIC'), - static=self.AWS_S3_KEY_PREFIX_STATIC + return "{aws_s3}/{static}/".format( + aws_s3=aws_s3_bucket_url(self, "AWS_S3_BUCKET_NAME_STATIC"), + static=self.AWS_S3_KEY_PREFIX_STATIC, ) # Markdown MARKDOWN_DEUX_STYLES = { - 'default': { - 'extras': { - 'code-friendly': None, - }, - 'safe_mode': True, + "default": {"extras": {"code-friendly": None}, "safe_mode": True}, + "trusted": { + "extras": {"code-friendly": None}, + "safe_mode": False, # Allow raw HTML }, - 'trusted': { - 'extras': { - 'code-friendly': None, - }, - 'safe_mode': False, # Allow raw HTML - } } # Authentication AUTHENTICATION_BACKENDS = ( - 'accounts.backends.GoogleOAuth2Login', - 'accounts.backends.GoogleOAuth2Register', - 'accounts.backends.FacebookOAuth2Login', - 'accounts.backends.FacebookOAuth2Register', - 'django.contrib.auth.backends.ModelBackend', + "accounts.backends.GoogleOAuth2Login", + "accounts.backends.GoogleOAuth2Register", + "accounts.backends.FacebookOAuth2Login", + "accounts.backends.FacebookOAuth2Register", + "django.contrib.auth.backends.ModelBackend", ) SOCIAL_AUTH_PIPELINE = ( - 'social_core.pipeline.social_auth.social_details', - 'social_core.pipeline.social_auth.social_uid', - 'social_core.pipeline.social_auth.auth_allowed', - 'social_core.pipeline.social_auth.social_user', - 'social_core.pipeline.user.get_username', - 'social_core.pipeline.social_auth.associate_by_email', - 'accounts.pipeline.require_email', - 'accounts.pipeline.verify_auth_operation', - 'social_core.pipeline.user.create_user', - 'accounts.pipeline.mark_email_verified', - 'accounts.pipeline.save_avatar', - 'social_core.pipeline.social_auth.associate_user', - 'social_core.pipeline.social_auth.load_extra_data', - 'social_core.pipeline.user.user_details', - 'accounts.pipeline.send_welcome_email', + "social_core.pipeline.social_auth.social_details", + "social_core.pipeline.social_auth.social_uid", + "social_core.pipeline.social_auth.auth_allowed", + "social_core.pipeline.social_auth.social_user", + "social_core.pipeline.user.get_username", + "social_core.pipeline.social_auth.associate_by_email", + "accounts.pipeline.require_email", + "accounts.pipeline.verify_auth_operation", + "social_core.pipeline.user.create_user", + "accounts.pipeline.mark_email_verified", + "accounts.pipeline.save_avatar", + "social_core.pipeline.social_auth.associate_user", + "social_core.pipeline.social_auth.load_extra_data", + "social_core.pipeline.user.user_details", + "accounts.pipeline.send_welcome_email", ) - SOCIAL_AUTH_LOGIN_ERROR_URL = '/' + SOCIAL_AUTH_LOGIN_ERROR_URL = "/" SOCIAL_AUTH_GOOGLE_OAUTH2_KEY = os.environ["PERDIEM_GOOGLE_OAUTH2_KEY"] SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET = os.environ["PERDIEM_GOOGLE_OAUTH2_SECRET"] SOCIAL_AUTH_FACEBOOK_KEY = os.environ["PERDIEM_FACEBOOK_KEY"] SOCIAL_AUTH_FACEBOOK_SECRET = os.environ["PERDIEM_FACEBOOK_SECRET"] - SOCIAL_AUTH_FACEBOOK_SCOPE = ['email'] + SOCIAL_AUTH_FACEBOOK_SCOPE = ["email"] SOCIAL_AUTH_FACEBOOK_PROFILE_EXTRA_PARAMS = { - 'fields': ', '.join(['id', 'name', 'email', 'picture.width(150)']), + "fields": ", ".join(["id", "name", "email", "picture.width(150)"]) } - LOGIN_URL = '/' - LOGIN_REDIRECT_URL = '/profile/' + LOGIN_URL = "/" + LOGIN_REDIRECT_URL = "/profile/" # Email - EMAIL_BACKEND = 'django.core.mail.backends.filebased.EmailBackend' - EMAIL_FILE_PATH = '/tmp/perdiem/email' - TEMPLATED_EMAIL_TEMPLATE_DIR = 'email/' - DEFAULT_FROM_EMAIL = 'PerDiem ' + EMAIL_BACKEND = "django.core.mail.backends.filebased.EmailBackend" + EMAIL_FILE_PATH = "/tmp/perdiem/email" + TEMPLATED_EMAIL_TEMPLATE_DIR = "email/" + DEFAULT_FROM_EMAIL = "PerDiem " # Stripe PINAX_STRIPE_PUBLIC_KEY = os.environ["PERDIEM_STRIPE_PUBLIC_KEY"] diff --git a/perdiem/settings/prod.py b/perdiem/settings/prod.py index a7a5e9ab..10edb12d 100644 --- a/perdiem/settings/prod.py +++ b/perdiem/settings/prod.py @@ -13,17 +13,17 @@ class ProdSettings(BaseSettings): @property def RAVEN_CONFIG(self): return { - 'dsn': 'https://{public_key}:{secret_key}@app.getsentry.com/{project_id}'.format( + "dsn": "https://{public_key}:{secret_key}@app.getsentry.com/{project_id}".format( public_key=os.environ["PERDIEM_SENTRY_PUBLIC_KEY"], secret_key=os.environ["PERDIEM_SENTRY_SECRET_KEY"], - project_id=os.environ["PERDIEM_SENTRY_PROJECT_ID"] + project_id=os.environ["PERDIEM_SENTRY_PROJECT_ID"], ), - 'release': raven.fetch_git_sha(self.BASE_DIR), + "release": raven.fetch_git_sha(self.BASE_DIR), } # Static files (CSS, JavaScript, Images) - DEFAULT_FILE_STORAGE = 'django_s3_storage.storage.S3Storage' - STATICFILES_STORAGE = 'django_s3_storage.storage.StaticS3Storage' + DEFAULT_FILE_STORAGE = "django_s3_storage.storage.S3Storage" + STATICFILES_STORAGE = "django_s3_storage.storage.StaticS3Storage" @property def AWS_S3_BUCKET_NAME(self): @@ -42,7 +42,7 @@ def AWS_SECRET_ACCESS_KEY(self): return os.environ["PERDIEM_AWS_SECRET_ACCESS_KEY"] # Email - EMAIL_BACKEND = 'django_ses.SESBackend' + EMAIL_BACKEND = "django_ses.SESBackend" @property def AWS_SES_ACCESS_KEY_ID(self): diff --git a/perdiem/tests.py b/perdiem/tests.py index d91ccd9c..be38253e 100644 --- a/perdiem/tests.py +++ b/perdiem/tests.py @@ -13,16 +13,21 @@ from pigeon.test import RenderTestCase -@override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.MD5PasswordHasher',)) +@override_settings(PASSWORD_HASHERS=("django.contrib.auth.hashers.MD5PasswordHasher",)) class PerDiemTestCase(RenderTestCase): - USER_USERNAME = 'jsmith' - USER_EMAIL = 'jsmith@example.com' + USER_USERNAME = "jsmith" + USER_EMAIL = "jsmith@example.com" @classmethod def setUpTestData(cls): super(PerDiemTestCase, cls).setUpTestData() - cls.user = UserFactory(username=cls.USER_USERNAME, email=cls.USER_EMAIL, is_staff=True, is_superuser=True) + cls.user = UserFactory( + username=cls.USER_USERNAME, + email=cls.USER_EMAIL, + is_staff=True, + is_superuser=True, + ) def setUp(self): self.client.login(username=self.USER_USERNAME, password=UserFactory._PASSWORD) @@ -70,24 +75,13 @@ def setUpBeforeMigration(self, apps): class HealthCheckWebTestCase(RenderTestCase): - def get200s(self): - return [ - '/health-check/', - ] + return ["/health-check/"] class ExtrasWebTestCase(RenderTestCase): - def get200s(self): - return [ - '/faq/', - '/trust/', - '/terms/', - '/privacy/', - '/contact/', - '/resources/', - ] + return ["/faq/", "/trust/", "/terms/", "/privacy/", "/contact/", "/resources/"] def testContact(self): # Login as user @@ -96,8 +90,12 @@ def testContact(self): # Verify that contact form submits successfully self.assertResponseRedirects( - '/contact/', - '/contact/thanks', - method='POST', - data={'inquiry': 'General Inquiry', 'email': 'msmith@example.com', 'message': 'Hello World!'} + "/contact/", + "/contact/thanks", + method="POST", + data={ + "inquiry": "General Inquiry", + "email": "msmith@example.com", + "message": "Hello World!", + }, ) diff --git a/perdiem/urls.py b/perdiem/urls.py index 1ad8b9b2..404fb05e 100644 --- a/perdiem/urls.py +++ b/perdiem/urls.py @@ -12,8 +12,11 @@ from django.views.static import serve from accounts.views import ( - VerifyEmailView, ContactFormView, ProfileView, PublicProfileView, - redirect_to_profile + VerifyEmailView, + ContactFormView, + ProfileView, + PublicProfileView, + redirect_to_profile, ) from artist.views import ArtistListView, ArtistDetailView, ArtistApplyFormView from campaign.views import LeaderboardView @@ -22,57 +25,82 @@ urlpatterns = [ - url(r'^health-check/?$', lambda r: HttpResponse(""), name='health_check'), - - url('', include(('social_django.urls', 'social',))), - url(r'^admin/', admin.site.urls), - url(r'^api-auth/', include('rest_framework.urls')), - url(r'^api/', include('api.urls')), - url(r'^accounts/', include('accounts.urls')), - - url(r'^unsubscribe/from-mailchimp/$', unsubscribe_from_mailchimp, name='unsubscribe_from_mailchimp'), + url(r"^health-check/?$", lambda r: HttpResponse(""), name="health_check"), + url("", include(("social_django.urls", "social"))), + url(r"^admin/", admin.site.urls), + url(r"^api-auth/", include("rest_framework.urls")), + url(r"^api/", include("api.urls")), + url(r"^accounts/", include("accounts.urls")), + url( + r"^unsubscribe/from-mailchimp/$", + unsubscribe_from_mailchimp, + name="unsubscribe_from_mailchimp", + ), url( - r'^unsubscribe/(?P\d+)/(?P\w+)/(?P[\w.:\-_=]+)/$', + r"^unsubscribe/(?P\d+)/(?P\w+)/(?P[\w.:\-_=]+)/$", UnsubscribeView.as_view(), - name='unsubscribe' + name="unsubscribe", ), - url(r'^email/verify/(?P\d+)/(?P[\w-]+)/$', VerifyEmailView.as_view(), name='verify_email'), - url(r'^payments/', include('pinax.stripe.urls')), - - url(r'^artists/?$', ArtistListView.as_view(), name='artists'), - url(r'^artist/apply/?$', ArtistApplyFormView.as_view(), name='artist_application'), url( - r'^artist/apply/thanks/?$', - TemplateView.as_view(template_name='artist/artist_application_thanks.html'), - name='artist_application_thanks' + r"^email/verify/(?P\d+)/(?P[\w-]+)/$", + VerifyEmailView.as_view(), + name="verify_email", ), - url(r'^artist/(?P[\w_-]+)/?$', ArtistDetailView.as_view(), name='artist'), - - url(r'^artist/(?P[\w_-]+)/(?P[\w_-]+)/?$', AlbumDetailView.as_view(), name='album'), - url(r'^music/?$', MusicListView.as_view(), name='music'), - - url(r'^profile/?$', ProfileView.as_view(), name='profile'), - url(r'^profile/(?P[\w.@+-]+)/?$', PublicProfileView.as_view(), name='public_profile'), - url(r'^stats/?$', LeaderboardView.as_view(), name='leaderboard'), - - url(r'^resources/?$', TemplateView.as_view(template_name='extra/resources.html'), name='resources'), - url(r'^terms/?$', TemplateView.as_view(template_name='extra/terms.html'), name='terms'), - url(r'^trust/?$', TemplateView.as_view(template_name='extra/trust.html'), name='trust'), - url(r'^privacy/?$', TemplateView.as_view(template_name='extra/privacy.html'), name='privacy'), - url(r'^faq/?$', TemplateView.as_view(template_name='extra/faq.html'), name='faq'), + url(r"^payments/", include("pinax.stripe.urls")), + url(r"^artists/?$", ArtistListView.as_view(), name="artists"), + url(r"^artist/apply/?$", ArtistApplyFormView.as_view(), name="artist_application"), url( - r'^contact/thanks/?$', - TemplateView.as_view(template_name='registration/contact_thanks.html'), - name='contact_thanks' + r"^artist/apply/thanks/?$", + TemplateView.as_view(template_name="artist/artist_application_thanks.html"), + name="artist_application_thanks", ), - url(r'^contact/?$', ContactFormView.as_view(), name='contact'), - url(r'^$', TemplateView.as_view(template_name='home.html'), name='home'), - - url(r'^(?P[\w.@+_-]+)/?$', redirect_to_profile), + url(r"^artist/(?P[\w_-]+)/?$", ArtistDetailView.as_view(), name="artist"), + url( + r"^artist/(?P[\w_-]+)/(?P[\w_-]+)/?$", + AlbumDetailView.as_view(), + name="album", + ), + url(r"^music/?$", MusicListView.as_view(), name="music"), + url(r"^profile/?$", ProfileView.as_view(), name="profile"), + url( + r"^profile/(?P[\w.@+-]+)/?$", + PublicProfileView.as_view(), + name="public_profile", + ), + url(r"^stats/?$", LeaderboardView.as_view(), name="leaderboard"), + url( + r"^resources/?$", + TemplateView.as_view(template_name="extra/resources.html"), + name="resources", + ), + url( + r"^terms/?$", + TemplateView.as_view(template_name="extra/terms.html"), + name="terms", + ), + url( + r"^trust/?$", + TemplateView.as_view(template_name="extra/trust.html"), + name="trust", + ), + url( + r"^privacy/?$", + TemplateView.as_view(template_name="extra/privacy.html"), + name="privacy", + ), + url(r"^faq/?$", TemplateView.as_view(template_name="extra/faq.html"), name="faq"), + url( + r"^contact/thanks/?$", + TemplateView.as_view(template_name="registration/contact_thanks.html"), + name="contact_thanks", + ), + url(r"^contact/?$", ContactFormView.as_view(), name="contact"), + url(r"^$", TemplateView.as_view(template_name="home.html"), name="home"), + url(r"^(?P[\w.@+_-]+)/?$", redirect_to_profile), ] # Add media folder to urls when DEBUG = True if settings.DEBUG: urlpatterns.append( - url(r'^media/(?P.*)$', serve, {'document_root': settings.MEDIA_ROOT}) + url(r"^media/(?P.*)$", serve, {"document_root": settings.MEDIA_ROOT}) ) diff --git a/perdiem/views.py b/perdiem/views.py index 472e26b4..33e2878c 100644 --- a/perdiem/views.py +++ b/perdiem/views.py @@ -6,7 +6,11 @@ from django.core.exceptions import ImproperlyConfigured from django.forms import formset_factory -from django.http import HttpResponseRedirect, HttpResponseBadRequest, HttpResponseNotAllowed +from django.http import ( + HttpResponseRedirect, + HttpResponseBadRequest, + HttpResponseNotAllowed, +) from django.views.generic import TemplateView @@ -15,7 +19,6 @@ class Http405(Exception): class FormsetView(TemplateView): - def get_success_url(self): try: return self.success_url @@ -39,16 +42,18 @@ def get_initial(self): return [] def get_context_data(self, **kwargs): - view_formset = formset_factory(self.get_form_class(), **self.get_formset_factory_kwargs()) + view_formset = formset_factory( + self.get_form_class(), **self.get_formset_factory_kwargs() + ) - if self.request.method == 'GET': + if self.request.method == "GET": formset = view_formset(initial=self.get_initial()) - elif self.request.method == 'POST': + elif self.request.method == "POST": formset = view_formset(self.request.POST, initial=self.get_initial()) else: raise Http405 - context = {'formset': formset} + context = {"formset": formset} return context def formset_valid(self, formset): @@ -58,12 +63,12 @@ def dispatch(self, request, *args, **kwargs): try: return super(FormsetView, self).dispatch(request, *args, **kwargs) except Http405: - return HttpResponseNotAllowed(['GET', 'POST']) + return HttpResponseNotAllowed(["GET", "POST"]) def post(self, request, *args, **kwargs): context = self.get_context_data(**kwargs) - formset = context['formset'] + formset = context["formset"] if formset.is_valid(): return self.formset_valid(formset) @@ -92,26 +97,26 @@ class MultipleFormView(TemplateView): def get_context_data(self, **kwargs): context = super(MultipleFormView, self).get_context_data(**kwargs) - context['forms_with_errors'] = [] + context["forms_with_errors"] = [] for form_name, form_view_class in self.constituent_form_views.items(): form_view = form_view_class(self.request) form_context_name = "{form_name}_form".format(form_name=form_name) if form_context_name not in context: form_args = [] - form_kwargs = { - 'initial': form_view.get_initial(), - } + form_kwargs = {"initial": form_view.get_initial()} if form_view.provide_user: form_args.append(self.request.user) - context[form_context_name] = form_view.form_class(*form_args, **form_kwargs) + context[form_context_name] = form_view.form_class( + *form_args, **form_kwargs + ) elif context[form_context_name].errors: - context['forms_with_errors'].append(form_name) + context["forms_with_errors"].append(form_name) return context def post(self, request, *args, **kwargs): try: - form_name = request.POST['action'] + form_name = request.POST["action"] form_view_class = self.constituent_form_views[form_name] except KeyError: return HttpResponseBadRequest("Form action unrecognized or unspecified.") diff --git a/perdiem/wsgi.py b/perdiem/wsgi.py index d7d718a1..e7bf1f94 100644 --- a/perdiem/wsgi.py +++ b/perdiem/wsgi.py @@ -18,6 +18,6 @@ load_dotenv() os.environ.setdefault("DJANGO_SETTINGS_MODULE", "perdiem.settings") -cbsettings.configure('perdiem.settings.switcher') +cbsettings.configure("perdiem.settings.switcher") application = get_wsgi_application() diff --git a/poetry.lock b/poetry.lock index a3cdd49c..bef924cd 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,3 +1,11 @@ +[[package]] +category = "dev" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +name = "appdirs" +optional = false +python-versions = "*" +version = "1.4.3" + [[package]] category = "main" description = "Fast ASN.1 parser and serializer with definitions for private keys, public keys, certificates, CRL, OCSP, CMS, PKCS#3, PKCS#7, PKCS#8, PKCS#12, PKCS#5, X.509 and TSP" @@ -6,6 +14,14 @@ optional = false python-versions = "*" version = "0.24.0" +[[package]] +category = "dev" +description = "Classes Without Boilerplate" +name = "attrs" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "19.1.0" + [[package]] category = "main" description = "Modern password hashing for your software and your servers" @@ -18,6 +34,20 @@ version = "3.1.6" cffi = ">=1.1" six = ">=1.4.1" +[[package]] +category = "dev" +description = "The uncompromising code formatter." +name = "black" +optional = false +python-versions = ">=3.6" +version = "18.9b0" + +[package.dependencies] +appdirs = "*" +attrs = ">=17.4.0" +click = ">=6.5" +toml = ">=0.9.4" + [[package]] category = "main" description = "Amazon Web Services Library" @@ -86,6 +116,14 @@ optional = false python-versions = "*" version = "3.0.4" +[[package]] +category = "dev" +description = "Composable command line interface toolkit" +name = "click" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "7.0" + [[package]] category = "dev" description = "Code coverage measurement for Python" @@ -638,6 +676,14 @@ optional = false python-versions = "*" version = "1.2" +[[package]] +category = "dev" +description = "Python Library for Tom's Obvious, Minimal Language" +name = "toml" +optional = false +python-versions = "*" +version = "0.10.0" + [[package]] category = "main" description = "HTTP library with thread-safe connection pooling, file post, and more." @@ -655,18 +701,22 @@ python-versions = "*" version = "4.1.3" [metadata] -content-hash = "b45b0e11b3be24dfb02e632fbe402cd7999b325c4ee7fba2664752fd8f829fda" +content-hash = "76e432fc6e8eeb4e6bb4b228f8ddcae1f57447aca9486c8139143acfdb9f2238" python-versions = "^3.6" [metadata.hashes] +appdirs = ["9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92", "d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"] asn1crypto = ["2f1adbb7546ed199e3c90ef23ec95c5cf3585bac7d11fb7eb562a3fe89c64e87", "9d5c20441baf0cb60a4ac34cc447c6c189024b6b4c6cd7877034f4965c464e49"] +attrs = ["69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79", "f0b870f674851ecbfbbbd364d6b5cbdff9dcedbc7f3f5e18a6891057f21fe399"] bcrypt = ["0ba875eb67b011add6d8c5b76afbd92166e98b1f1efab9433d5dc0fafc76e203", "21ed446054c93e209434148ef0b362432bb82bbdaf7beef70a32c221f3e33d1c", "28a0459381a8021f57230954b9e9a65bb5e3d569d2c253c5cac6cb181d71cf23", "2aed3091eb6f51c26b7c2fad08d6620d1c35839e7a362f706015b41bd991125e", "2fa5d1e438958ea90eaedbf8082c2ceb1a684b4f6c75a3800c6ec1e18ebef96f", "3a73f45484e9874252002793518da060fb11eaa76c30713faa12115db17d1430", "3e489787638a36bb466cd66780e15715494b6d6905ffdbaede94440d6d8e7dba", "44636759d222baa62806bbceb20e96f75a015a6381690d1bc2eda91c01ec02ea", "678c21b2fecaa72a1eded0cf12351b153615520637efcadc09ecf81b871f1596", "75460c2c3786977ea9768d6c9d8957ba31b5fbeb0aae67a5c0e96aab4155f18c", "8ac06fb3e6aacb0a95b56eba735c0b64df49651c6ceb1ad1cf01ba75070d567f", "8fdced50a8b646fff8fa0e4b1c5fd940ecc844b43d1da5a980cb07f2d1b1132f", "9b2c5b640a2da533b0ab5f148d87fb9989bf9bcb2e61eea6a729102a6d36aef9", "a9083e7fa9adb1a4de5ac15f9097eb15b04e2c8f97618f1b881af40abce382e1", "b7e3948b8b1a81c5a99d41da5fb2dc03ddb93b5f96fcd3fd27e643f91efa33e1", "b998b8ca979d906085f6a5d84f7b5459e5e94a13fc27c28a3514437013b6c2f6", "dd08c50bc6f7be69cd7ba0769acca28c846ec46b7a8ddc2acf4b9ac6f8a7457e", "de5badee458544ab8125e63e39afeedfcf3aef6a6e2282ac159c95ae7472d773", "ede2a87333d24f55a4a7338a6ccdccf3eaa9bed081d1737e0db4dbd1a4f7e6b6"] +black = ["817243426042db1d36617910df579a54f1afd659adb96fc5032fcf4b36209739", "e030a9a28f542debc08acceb273f228ac422798e5215ba2a791a6ddeaaca22a5"] boto = ["147758d41ae7240dc989f0039f27da8ca0d53734be0eb869ef16e3adcfa462e8", "ea0d3b40a2d852767be77ca343b58a9e3a4b00d9db440efb8da74b4e58025e5a"] boto3 = ["47132bfc3061091b03cdf24c11f083641cd5686feccdb14cabab62d25297185f", "b62534d324bc806686dbdd7d4957d323fd61d55f2e9e30975c3042fabeb57d53"] botocore = ["786573c94cd3ea1ec58de0102f0924c24bf23d6dfcd5232714c27807a59f6a2d", "ae74ede86f5fd3e3c5cb63f066c9dbb21df12af79ceeb068e1bcb04b076dbb78"] certifi = ["59b7658e26ca9c7339e00f8f4636cdfe59d34fa37b9b04f6f9e9926b3cece1a5", "b26104d6835d1f5e49452a26eb2ff87fe7090b89dfcaee5ea2212697e1e1d7ae"] cffi = ["041c81822e9f84b1d9c401182e174996f0bae9991f33725d059b771744290774", "046ef9a22f5d3eed06334d01b1e836977eeef500d9b78e9ef693f9380ad0b83d", "066bc4c7895c91812eff46f4b1c285220947d4aa46fa0a2651ff85f2afae9c90", "066c7ff148ae33040c01058662d6752fd73fbc8e64787229ea8498c7d7f4041b", "2444d0c61f03dcd26dbf7600cf64354376ee579acad77aef459e34efcb438c63", "300832850b8f7967e278870c5d51e3819b9aad8f0a2c8dbe39ab11f119237f45", "34c77afe85b6b9e967bd8154e3855e847b70ca42043db6ad17f26899a3df1b25", "46de5fa00f7ac09f020729148ff632819649b3e05a007d286242c4882f7b1dc3", "4aa8ee7ba27c472d429b980c51e714a24f47ca296d53f4d7868075b175866f4b", "4d0004eb4351e35ed950c14c11e734182591465a33e960a4ab5e8d4f04d72647", "4e3d3f31a1e202b0f5a35ba3bc4eb41e2fc2b11c1eff38b362de710bcffb5016", "50bec6d35e6b1aaeb17f7c4e2b9374ebf95a8975d57863546fa83e8d31bdb8c4", "55cad9a6df1e2a1d62063f79d0881a414a906a6962bc160ac968cc03ed3efcfb", "5662ad4e4e84f1eaa8efce5da695c5d2e229c563f9d5ce5b0113f71321bcf753", "59b4dc008f98fc6ee2bb4fd7fc786a8d70000d058c2bbe2698275bc53a8d3fa7", "73e1ffefe05e4ccd7bcea61af76f36077b914f92b76f95ccf00b0c1b9186f3f9", "a1f0fd46eba2d71ce1589f7e50a9e2ffaeb739fb2c11e8192aa2b45d5f6cc41f", "a2e85dc204556657661051ff4bab75a84e968669765c8a2cd425918699c3d0e8", "a5457d47dfff24882a21492e5815f891c0ca35fefae8aa742c6c263dac16ef1f", "a8dccd61d52a8dae4a825cdbb7735da530179fea472903eb871a5513b5abbfdc", "ae61af521ed676cf16ae94f30fe202781a38d7178b6b4ab622e4eec8cefaff42", "b012a5edb48288f77a63dba0840c92d0504aa215612da4541b7b42d849bc83a3", "d2c5cfa536227f57f97c92ac30c8109688ace8fa4ac086d19d0af47d134e2909", "d42b5796e20aacc9d15e66befb7a345454eef794fdb0737d1af593447c6c8f45", "dee54f5d30d775f525894d67b1495625dd9322945e7fee00731952e0368ff42d", "e070535507bd6aa07124258171be2ee8dfc19119c28ca94c9dfb7efd23564512", "e1ff2748c84d97b065cc95429814cdba39bcbd77c9c85c89344b317dc0d9cbff", "ed851c75d1e0e043cbf5ca9a8e1b13c4c90f3fbd863dacb01c0808e2b5204201"] chardet = ["84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", "fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"] +click = ["2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", "5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"] coverage = ["08907593569fe59baca0bf152c43f3863201efb6113ecb38ce7e97ce339805a6", "0be0f1ed45fc0c185cfd4ecc19a1d6532d72f86a2bac9de7e24541febad72650", "141f08ed3c4b1847015e2cd62ec06d35e67a3ac185c26f7635f4406b90afa9c5", "19e4df788a0581238e9390c85a7a09af39c7b539b29f25c89209e6c3e371270d", "23cc09ed395b03424d1ae30dcc292615c1372bfba7141eb85e11e50efaa6b351", "245388cda02af78276b479f299bbf3783ef0a6a6273037d7c60dc73b8d8d7755", "331cb5115673a20fb131dadd22f5bcaf7677ef758741312bee4937d71a14b2ef", "386e2e4090f0bc5df274e720105c342263423e77ee8826002dcffe0c9533dbca", "3a794ce50daee01c74a494919d5ebdc23d58873747fa0e288318728533a3e1ca", "60851187677b24c6085248f0a0b9b98d49cba7ecc7ec60ba6b9d2e5574ac1ee9", "63a9a5fc43b58735f65ed63d2cf43508f462dc49857da70b8980ad78d41d52fc", "6b62544bb68106e3f00b21c8930e83e584fdca005d4fffd29bb39fb3ffa03cb5", "6ba744056423ef8d450cf627289166da65903885272055fb4b5e113137cfa14f", "7494b0b0274c5072bddbfd5b4a6c6f18fbbe1ab1d22a41e99cd2d00c8f96ecfe", "826f32b9547c8091679ff292a82aca9c7b9650f9fda3e2ca6bf2ac905b7ce888", "93715dffbcd0678057f947f496484e906bf9509f5c1c38fc9ba3922893cda5f5", "9a334d6c83dfeadae576b4d633a71620d40d1c379129d587faa42ee3e2a85cce", "af7ed8a8aa6957aac47b4268631fa1df984643f07ef00acd374e456364b373f5", "bf0a7aed7f5521c7ca67febd57db473af4762b9622254291fbcbb8cd0ba5e33e", "bf1ef9eb901113a9805287e090452c05547578eaab1b62e4ad456fcc049a9b7e", "c0afd27bc0e307a1ffc04ca5ec010a290e49e3afbe841c5cafc5c5a80ecd81c9", "dd579709a87092c6dbee09d1b7cfa81831040705ffa12a1b248935274aee0437", "df6712284b2e44a065097846488f66840445eb987eb81b3cc6e4149e7b6982e1", "e07d9f1a23e9e93ab5c62902833bf3e4b1f65502927379148b6622686223125c", "e2ede7c1d45e65e209d6093b762e98e8318ddeff95317d07a27a2140b80cfd24", "e4ef9c164eb55123c62411f5936b5c2e521b12356037b6e1c2617cef45523d47", "eca2b7343524e7ba246cab8ff00cab47a2d6d54ada3b02772e908a45675722e2", "eee64c616adeff7db37cc37da4180a3a5b6177f5c46b187894e633f088fb5b28", "ef824cad1f980d27f26166f86856efe11eff9912c4fed97d3804820d43fa550c", "efc89291bd5a08855829a3c522df16d856455297cf35ae827a37edac45f466a7", "fa964bae817babece5aa2e8c1af841bebb6d0b9add8e637548809d040443fee0", "ff37757e068ae606659c28c3bd0d923f9d29a85de79bf25b2b34b148473b5025"] cryptography = ["066f815f1fe46020877c5983a7e747ae140f517f1b09030ec098503575265ce1", "210210d9df0afba9e000636e97810117dc55b7157c903a55716bb73e3ae07705", "26c821cbeb683facb966045e2064303029d572a87ee69ca5a1bf54bf55f93ca6", "2afb83308dc5c5255149ff7d3fb9964f7c9ee3d59b603ec18ccf5b0a8852e2b1", "2db34e5c45988f36f7a08a7ab2b69638994a8923853dec2d4af121f689c66dc8", "409c4653e0f719fa78febcb71ac417076ae5e20160aec7270c91d009837b9151", "45a4f4cf4f4e6a55c8128f8b76b4c057027b27d4c67e3fe157fa02f27e37830d", "48eab46ef38faf1031e58dfcc9c3e71756a1108f4c9c966150b605d4a1a7f659", "6b9e0ae298ab20d371fc26e2129fd683cfc0cfde4d157c6341722de645146537", "6c4778afe50f413707f604828c1ad1ff81fadf6c110cb669579dea7e2e98a75e", "8c33fb99025d353c9520141f8bc989c2134a1f76bac6369cea060812f5b5c2bb", "9873a1760a274b620a135054b756f9f218fa61ca030e42df31b409f0fb738b6c", "9b069768c627f3f5623b1cbd3248c5e7e92aec62f4c98827059eed7053138cc9", "9e4ce27a507e4886efbd3c32d120db5089b906979a4debf1d5939ec01b9dd6c5", "acb424eaca214cb08735f1a744eceb97d014de6530c1ea23beb86d9c6f13c2ad", "c8181c7d77388fe26ab8418bb088b1a1ef5fde058c6926790c8a0a3d94075a4a", "d4afbb0840f489b60f5a580a41a1b9c3622e08ecb5eec8614d4fb4cd914c4460", "d9ed28030797c00f4bc43c86bf819266c76a5ea61d006cd4078a93ebf7da6bfd", "e603aa7bb52e4e8ed4119a58a03b60323918467ef209e6ff9db3ac382e5cf2c6"] defusedxml = ["6687150770438374ab581bb7a1b327a847dd9c5749e396102de3fad4e8a3ef93", "f684034d135af4c6cbb949b8a4d2ed61634515257a67299e5f940fbaa34377f5"] @@ -720,5 +770,6 @@ sorl-thumbnail = ["8dfe5fda91a5047d1d35a0b9effe7b000764a01d648e15ca076f44e9c34b6 sqlparse = ["40afe6b8d4b1117e7dff5504d7a8ce07d9a1b15aeeade8a2d10f130a834f8177", "7c3dca29c022744e95b547e867cee89f4fce4373f3549ccd8797d8eb52cdb873"] stripe = ["43cf1addbd5685d166c483f29f2b13e514304b091086561c2e22a3c7664043a2", "814e6a87fdb679cf2ddca9a12099a93aaf437dac0b09edac4851c6ba9ebd7e5f"] text-unidecode = ["5a1375bb2ba7968740508ae38d92e1f889a0832913cb1c447d5e2046061a396d", "801e38bd550b943563660a91de8d4b6fa5df60a542be9093f7abf819f86050cc"] +toml = ["229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", "235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e", "f1db651f9657708513243e61e6cc67d101a39bad662eaa9b5546f789338e07a3"] urllib3 = ["4c291ca23bbb55c76518905869ef34bdd5f0e46af7afe6861e8375643ffee1a0", "9a247273df709c4fedb38c711e44292304f73f39ab01beda9f6b9fc375669ac3"] whitenoise = ["59d880d25d0e90bcc6554fe0504a11195bd2e59b3d690b6fb42a8040d4e67ef5", "c9b7c47fdc1dba4d37bf2787a01a844dc7a521e174fcd22a2d429e0be65e1782"] diff --git a/pyproject.toml b/pyproject.toml index 09be2bbd..54a753c4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,6 +35,11 @@ python-dotenv = "^0.10.3" [tool.poetry.dev-dependencies] factory-boy = "^2.12.0" coverage = "^4.5.4" +black = {version = "^18.3-alpha.0",allows-prereleases = true} + +[tool.black] +line-length = 88 +target_version = ["py36"] [build-system] requires = ["poetry>=0.12"]